Виджеты и макеты
Contents
Виджеты и макеты¶
Виджеты¶
Виджеты — атомы графического интерфейса. Виджет принимает события мыши, клавиатуры и другие события от системы, виджет рисует себя на экране.
Виджеты не встроенные в родительский виджет называют окнами (window
). Обычно у окон есть рамка и заголовочная планка, хотя и это тоже можно кастомизировать. Почти все встречаемые окна — это или QMainWindow
или диалоговое окно (обычно производный класс от класса QDialog), про которые речь пойдет позже.
Конструктор любого виджета принимает параметр parent
, который и отвечает за то, будет ли этот виджет встроенным в родительский виджет (в качестве parent
передан какой-то другой виджет) или независимым окном (значение по умолчанию parent=None
). Родительский виджет можно назначить в любой момент методом setParent. Если виджет добавляется в макет (layout
) другого виджета, то метод setParent
вызывается автоматически и виджет становится встроенным.
Большинство виджетов в Qt
предназначено для использования в качестве встроенных дочерних виджетов (child widget
).

Все виджеты наследуются от QWidget и наследуют от него ряд методов. Например,
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()
Сверху размещаются пара виджетов, видимость и активность которых управляется кнопками в нижнем ряду.

Связывание сигналов происходит в этом цикле:
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
деактивирует все виджеты в верхней строке.

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

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()

Есть несколько недостатков такого подхода, среди которых:
при указании координат можно ошибиться с координатами;
положения и размеры таких виджетов фиксированы в абсолютных единицах измерения — в пикселях, а значит на экране с другим разрешением такое приложение может выглядеть хуже;
изменение размеров родительского виджета по умолчанию никак не повлияет на размеры и положения его дочерних виджетов.
Все эти недостатки можно нивелировать, используя макеты.
Центральный виджет¶
У главного окна 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()

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()

Макет старается максимально распределить виджеты по всему доступному ему пространству. При распределении пространства он учитывает два фактора:
параметр
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()

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

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.

Код ниже демонстрирует все перечисленные возможности 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()