Виджеты и макеты
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()