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

Виджеты#

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

Виджеты не встроенные в родительский виджет называют окнами (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()
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/horizontal_rainbow.png
../../_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()