Виджеты и макеты

Виджеты

Виджеты — атомы графического интерфейса. Виджет принимает события мыши, клавиатуры и другие события от системы, виджет рисует себя на экране.

Виджеты не встроенные в родительский виджет называют окнами (window). Обычно у окон есть рамка и заголовочная планка, хотя и это тоже можно кастомизировать. Почти все встречаемые окна — это или QMainWindow или диалоговое окно (обычно производный класс от класса QDialog), про которые речь пойдет позже.

Конструктор любого виджета принимает параметр parent, который и отвечает за то, будет ли этот виджет встроенным в родительский виджет (в качестве parent передан какой-то другой виджет) или независимым окном (значение по умолчанию parent=None). Родительский виджет можно назначить в любой момент методом setParent. Если виджет добавляется в макет (layout) другого виджета, то метод setParent вызывается автоматически и виджет становится встроенным.

Большинство виджетов в Qt предназначено для использования в качестве встроенных дочерних виджетов (child widget).

../_images/parent_child_widgets.png

Все виджеты наследуются от QWidget и наследуют от него ряд методов. Например,

  • hide (скрыть) и show (показать);

  • setEnabled (установить активным) и setDisabled (установить неактивным);

Следующий пример демонстрирует эффект применения этих методов.

import sys
from functools import partial

from PySide6.QtGui import Qt
from PySide6.QtWidgets import *


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        widget = QWidget()
        main_layout = QVBoxLayout()
        widget.setLayout(main_layout)
        self.setCentralWidget(widget)

        # top row
        button = QPushButton("Button")
        label = QLabel("Label")
        label.setAlignment(Qt.AlignCenter)
        edit = QLineEdit()

        # bottom row
        enable_button = QPushButton("Enable")
        disable_button = QPushButton("Disable")
        show_button = QPushButton("Show")
        hide_button = QPushButton("Hide")

        # layout

        top_layout = QHBoxLayout()
        top_layout.addWidget(button)
        top_layout.addWidget(label)
        top_layout.addWidget(edit)
        main_layout.addLayout(top_layout)

        bottom_layout = QHBoxLayout()
        bottom_layout.addWidget(enable_button)
        bottom_layout.addWidget(disable_button)
        bottom_layout.addWidget(show_button)
        bottom_layout.addWidget(hide_button)
        main_layout.addLayout(bottom_layout)

        # connections
        for widget in [button, label, edit]:
            enable_button.clicked.connect(partial(widget.setEnabled, True))
            disable_button.clicked.connect(partial(widget.setDisabled, True))
            show_button.clicked.connect(widget.show)
            hide_button.clicked.connect(widget.hide)


app = QApplication(sys.argv)
main_window = MainWindow()
main_window.show()
app.exec()

Сверху размещаются пара виджетов, видимость и активность которых управляется кнопками в нижнем ряду.

../_images/disable_hide_1.png

Связывание сигналов происходит в этом цикле:

for widget in [button, label, edit]:
    enable_button.clicked.connect(partial(widget.setEnabled, True))
    disable_button.clicked.connect(partial(widget.setDisabled, True))
    show_button.clicked.connect(widget.show)
    hide_button.clicked.connect(widget.hide)

Так, как методы setEnabled и setDisabled принимают в качестве параметра True или False, то в качестве слота передаётся не сам метод, а объект partial из модуля functools, связывающий первый и единственный параметр метода со значением True.

Нажатие на кнопку Disable деактивирует все виджеты в верхней строке.

../_images/disable_hide_2.png

Нажатие на кнопку Hide скроет все виджеты в верхней строке.

../_images/disable_hide_3.png

Note

Элементы нижней строки виджетов подтягиваются наверх при скрытии элементов верхней строки, так как эти строки виджетов помещены в вертикальный layout, который динамически старается выделить виджетам как можно больше пространства.

Макеты

Чтобы располагать виджеты внутри главного окна или внутри другого виджета, обычно используют макеты (layout). Чтобы изучить особенности ряда встроенных макетов определим виджет ColorWidget, который принимает цвет в качестве параметра при создании и заливает свой фон полностью указанным цветом. Добиться такого эффекта можно, например, следующим кодом.

from PySide6.QtGui import QColor, QPalette
from PySide6.QtWidgets import QWidget

rainbow_colors = ["Red", "Orange", "Yellow", "Green", "Blue", "Magenta", "Violet"]


class ColorWidget(QWidget):
    def __init__(self, color, parent=None):
        super().__init__(parent=parent)
        self.setAutoFillBackground(True)
        palette = self.palette()
        palette.setColor(QPalette.Window, QColor(color))
        self.setPalette(palette)

Единственная функция этого виджета — занимать пространство, чтобы иллюстрировать, как работает тот или иной макет. Во всех следующих примерах предполагается, что файл с этим исходным кодом располагается в той же папке с именем colorwidget.py.

Без макета

Прежде чем изучать предлагаемые Qt макеты, покажем в чем их польза. Вообще говоря, без макетов можно обойтись. Тогда необходимо придерживаться следующих правил:

  • при создании (или после создания методом setParent) виджета указывать в качестве родителя (parent) виджет, внутри которого необходимо отображать создаваемый виджет;

  • вызывать метод setGeometry и передать ему в качестве аргумента:

    • координаты \(x\) и \(y\) левого верхнего угла в системе координат родительского виджета (весь экран для виджет без родителя) в пикселях;

    • высоту \(h\) и ширину \(w\) виджета в пикселях.

В качестве примера создадим тройку виджетов в таком стиле.

import sys

from PySide6.QtWidgets import *

from colorwidget import ColorWidget


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setGeometry(0, 0, 300, 300)

        red = ColorWidget("Red", parent=self)
        red.setGeometry(0, 0, 100, 100)

        green = ColorWidget("Green", parent=self)
        green.setGeometry(100, 100, 100, 100)

        blue = ColorWidget("Blue", parent=self)
        blue.setGeometry(200, 200, 100, 100)


app = QApplication(sys.argv)
main_window = MainWindow()
main_window.show()
app.exec()
../_images/no_layout.png

Есть несколько недостатков такого подхода, среди которых:

  • при указании координат можно ошибиться с координатами;

  • положения и размеры таких виджетов фиксированы в абсолютных единицах измерения — в пикселях, а значит на экране с другим разрешением такое приложение может выглядеть хуже;

  • изменение размеров родительского виджета по умолчанию никак не повлияет на размеры и положения его дочерних виджетов.

Все эти недостатки можно нивелировать, используя макеты.

Центральный виджет

У главного окна QMainWindow имеется свой уникальный макет, который позволяет легко добавлять к нему меню, панель инструментов, закрепляемые виджеты и другое. В связи с этим у QMainWindow нельзя поменять макет на произвольный другой и, чтобы добавлять в него виджеты, необходимо использовать центральный виджет. Если виджет всего один, то можно прямо его и установить в качестве центрального. Код ниже демонстрирует это.

import sys

from PySide6.QtWidgets import *

from colorwidget import ColorWidget


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        widget = ColorWidget("Blue")
        self.setCentralWidget(widget)


app = QApplication(sys.argv)
main_window = MainWindow()
main_window.show()
app.exec()
../_images/central_widget.png

Note

Заметьте, что этот виджет занимает всё доступное ему пространство даже при изменении размеров окна, т.к. метод setCentralWidget помещает его в макет главного окна.

Если виджетов в окне должно быть несколько, то чаще всего в качестве центрального виджета устанавливается пустой виджет QWidget, а уже внутри него размещаются необходимые виджеты в макете. При этом Qt предоставляет разные макеты на такие случаи.

Вертикальные и горизонтальные макеты. QVBoxLayout и QHBoxLayout

QVBoxLayout располагает виджеты вертикальной стопкой, а QHBoxLayout — горизонтальной. Их работа уже коротко демонстрировалась.

Все макеты создаются пустым и затем в них добавляются виджеты методом addWidget.

Код ниже демонстрирует поведение макетов QHBoxLayout и QVBoxLayout, добавляя в них виджеты ColorWidget 7 цветов радуги.

import sys

from PySide6.QtWidgets import *

from colorwidget import ColorWidget, rainbow_colors


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        widget = QWidget()
        layout = QHBoxLayout()
        widget.setLayout(layout)
        self.setCentralWidget(widget)

        for color in rainbow_colors:
            layout.addWidget(ColorWidget(color))


app = QApplication(sys.argv)
main_window = MainWindow()
main_window.show()
app.exec()
../_images/horizontal_rainbow.png
import sys

from PySide6.QtWidgets import *

from colorwidget import ColorWidget, rainbow_colors


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        widget = QWidget()
        layout = QVBoxLayout()
        widget.setLayout(layout)
        self.setCentralWidget(widget)

        for color in rainbow_colors:
            layout.addWidget(ColorWidget(color))


app = QApplication(sys.argv)
main_window = MainWindow()
main_window.show()
app.exec()
../_images/vertical_rainbow.png

Макет старается максимально распределить виджеты по всему доступному ему пространству. При распределении пространства он учитывает два фактора:

  • параметр stretch, который отвечает за то, какая доля свободного пространства какому элементу макета отведется;

  • политику размеров самого виджета и ограничения на размер самого виджета, которые можно выставить такими методами setMinimumHeight, setMaximumHeight, setMinimumWidth и setMaximumWidth.

Код ниже демонстрирует, как эти параметры управляют распределением пространства.

import sys

from PySide6.QtWidgets import *

from colorwidget import ColorWidget


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        widget = QWidget()
        layout = QHBoxLayout()
        widget.setLayout(layout)
        self.setCentralWidget(widget)

        red = ColorWidget("Red")
        red.setMinimumWidth(100)
        red.setMaximumWidth(200)
        violet = ColorWidget("Violet")
        violet.setMinimumWidth(100)
        violet.setMaximumWidth(200)

        layout.addWidget(red, stretch=1)
        layout.addStretch(2)
        layout.addWidget(violet, stretch=1)


app = QApplication(sys.argv)
main_window = MainWindow()
main_window.show()
app.exec()

Если в один элемент вертикального макета необходимо встроить строку виджетов, то методом addLayout в него можно добавить горизонтальный макет (или макет любого другого типа) вместо виджета. Таким образом макеты могут быть встроенными друг в друга.

Макет-таблица. QGridLayout

Макет QGridLayout позволяет располагать виджеты как-бы в ячейках таблицы. При этом один виджет может занимать несколько соседних ячеек и можно не заполнять все ячейки таблицы.

Макет QGridLayout создаётся пустым. Виджеты добавляются методом addWidget, принимающим первым параметром сам виджет, а остальными параметрами — ячейки таблицы, которые он должен занимать:

  • если виджет widget должен занимать одну ячейку макета grid_layout на пересечении строки с номером row и столбца с номером column, то сигнатура вызова выглядит примерно следующим образом:

grid_layout.addWidget(widget, row, column)
  • если этот же виджет должен начинаться в ячейке на пересечении строки с номером row и столбца с номером column и должен занимать row_span ячеек по горизонтали и column_span ячеек по вертикали, то сигнатура вызова выглядит примерно следующим образом:

grid_layout.addWidget(widget, row, column, row_span, column_span)

Note

Нумерация строк идёт сверху вниз, столбцов слева справа и начинается с 1 в обоих случаях.

Следующий код демонстрирует некоторые возможности этого макета.

import sys

from PySide6.QtWidgets import *

from colorwidget import ColorWidget


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        widget = QWidget()
        layout = QGridLayout()
        widget.setLayout(layout)
        self.setCentralWidget(widget)

        layout.addWidget(ColorWidget("Red"), 1, 1)
        layout.addWidget(ColorWidget("Green"), 2, 4)
        layout.addWidget(ColorWidget("Blue"), 3, 1, 1, 2)


app = QApplication(sys.argv)
main_window = MainWindow()
main_window.show()
app.exec()
../_images/grid_layout.png

Макет-анкета. QFormLayout

Часто при создании интерфейса приложения необходимо расположить несколько виджетов с подписями друг под другом. Такая необходимость часто возникает, если интерфейс представляет собой форму, анкету и так далее.

Добиться этого можно было бы и с помощью QGridLayout с двумя столбцами: один для виджетов, другой для их подписей. Но гораздо удобнее воспользоваться специальным макетом QFormLayout. Добавление виджетов с названиями в него осуществляется методом addRow, которому первым параметром передаётся строка текста, а вторым виджет.

../_images/form_layout.png
import sys

from PySide6.QtWidgets import *


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        widget = QWidget()
        layout = QFormLayout()
        widget.setLayout(layout)
        self.setCentralWidget(widget)

        layout.addRow("Фамилия", QLineEdit())
        layout.addRow("Имя", QLineEdit())
        layout.addRow("Отчество", QLineEdit())
        layout.addRow("Год рождения", QSpinBox())
        layout.addRow("Гражданство РФ", QCheckBox())


app = QApplication(sys.argv)
main_window = MainWindow()
main_window.show()
app.exec()

Вкладки. Виджет QTabWidget

Хотя QTabWidget и не является макетом, но так как принцип его действия очень похож, то он упоминается здесь. Этот виджет позволяет делать виджеты с вкладками, примером которых можно считать вкладки браузеров. Создаётся QTabWidget пустым, а добавление вкладок осуществляется методом addTab, который первым параметром принимает виджет. В простом варианте, вторым параметром передаётся строка с названием вкладки. В более сложном варианте это строка передаётся третьим параметром, а вторым передаётся иконка этой вкладки.

Перечислим ещё пару полезных особенностей:

  • метод setMovable позволяет перемещать вкладки между собой;

  • метод setTabsClosable добавляет кнопки закрытия вкладок, нажатие на которые излучает сигнал tabCloseRequested;

  • соединить этот сигнал можно напрямую со слотом removeTab.

../_images/tab_widget.png

Код ниже демонстрирует все перечисленные возможности QTabWidget на примере 7 закрываемых, перемещаемых вкладок всех цветов радуги.

import sys

from PySide6.QtWidgets import *

from colorwidget import ColorWidget, rainbow_colors


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.tabs = QTabWidget()
        self.setCentralWidget(self.tabs)

        self.tabs.setMovable(True)
        self.tabs.setTabsClosable(True)
        self.tabs.tabCloseRequested.connect(self.tabs.removeTab)

        for color in rainbow_colors:
            self.tabs.addTab(ColorWidget(color), color)


app = QApplication(sys.argv)
main_window = MainWindow()
main_window.show()
app.exec()