В первом материале мы рассказали о создании первого окна, о сигналах, слотах и событиях, а также о виджетах. Сегодня, к старту курса по Fullstack-разработке на Python, делимся продолжением — о макетах, работе с панелями инструментов и меню при помощи QAction, дополнительных и диалоговых окнах. За подробностями приглашаем под кат.

  1. Макеты

  2. Панели инструментов, меню и QAction

  3. Диалоговые окна и окна предупреждений

  4. Дополнительные окна


Макеты

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

Класс макета

Тип макета

QHBoxLayout

Горизонтальный линейный макет

QVBoxLayout

Вертикальный линейный макет

QGridLayout

Индексируемая сетка X на Y

QStackedLayout

Уложенные друг на друга по оси Z виджеты

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

В Qt есть три макета расположения виджетов: VBoxLayout, QHBoxLayout и QGridLayout. И есть QStackedLayout, позволяющий размещать виджеты один над другим в одном месте, одновременно отображая только один макет. Сначала понадобится простая схема приложения, в котором мы будем экспериментировать с различными макетами. Сохраните следующий код в файле app.py:

import sys
from PyQt6.QtWidgets import QApplication, QMainWindow, QWidget
from PyQt6.QtGui import QPalette, QColor

class MainWindow(QMainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()

        self.setWindowTitle("My App")


app = QApplication(sys.argv)

window = MainWindow()
window.show()

app.exec()

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

class Color(QWidget):

    def __init__(self, color):
        super(Color, self).__init__()
        self.setAutoFillBackground(True)

        palette = self.palette()
        palette.setColor(QPalette.ColorRole.Window, QColor(color))
        self.setPalette(palette)

В этом коде мы пишем подкласс QWidget для пользовательского виджета Color, при создании которого принимаем один параметр — color (str). Сначала устанавливаем .setAutoFillBackground в True, чтобы фон виджета автоматически заполнялся цветом окна. Затем получаем текущую палитру (по умолчанию это глобальная палитра рабочего стола) и меняем текущий цвет QPalette.Window на новый QColor, который соответствует переданному значению color. Мы применяем эту палитру к виджету. Результат — виджет, заполненный сплошным цветом (каким именно — указывается при его создании).

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

Color('red')

Сначала протестируем новый виджет Color, используя его для заполнения всего окна одним цветом. По завершении добавим его в QMainWindow с помощью .setCentralWidget и получим окно со сплошным красным цветом:

class MainWindow(QMainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()

        self.setWindowTitle("My App")

        widget = Color('red')
        self.setCentralWidget(widget)

Запускаем. Появится полностью красное окно, при этом виджет расширяется, заполняя всё доступное пространство.

Далее рассмотрим каждый из макетов Qt по очереди. Обратите внимание: макеты будут добавляться в окно, находясь в фиктивном QWidget.

QVBoxLayout: вертикально расположенные виджеты

В макете QVBoxLayout виджеты размещаются один над другим. При добавлении виджет отправляется в низ столбца.

QVBoxLayout, заполняемый сверху вниз
QVBoxLayout, заполняемый сверху вниз

Добавим виджет в макет. Чтобы добавить макет в QMainWindow, нужно применить его к фиктивному QWidget, а затем использовать .setCentralWidget, чтобы применить виджет и макет к окну. Цветные виджеты расположатся в макете, находящемся в QWidget, в окне. Сначала просто добавляем красный виджет, как раньше:

class MainWindow(QMainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()

        self.setWindowTitle("My App")

        layout = QVBoxLayout()

        layout.addWidget(Color('red'))

        widget = QWidget()
        widget.setLayout(layout)
        self.setCentralWidget(widget)

Теперь вокруг красного виджета видна рамка. Это интервал между макетами — позже посмотрим, как его настроить.

Если добавить в макет ещё цветных виджетов, они разместятся вертикально в порядке добавления:

class MainWindow(QMainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()

        self.setWindowTitle("My App")

        layout = QVBoxLayout()

        layout.addWidget(Color('red'))
        layout.addWidget(Color('green'))
        layout.addWidget(Color('blue'))

        widget = QWidget()
        widget.setLayout(layout)
        self.setCentralWidget(widget)

QHBoxLayout: горизонтально расположенные виджеты

Макет QHBoxLayout такой же, только виджеты здесь размещаются горизонтально. Виджет добавляется с правой стороны.

QHBoxLayout, заполняемый слева направо
QHBoxLayout, заполняемый слева направо

Просто меняем макет QVBoxLayout на QHBoxLayout. Виджеты теперь располагаются слева направо:

class MainWindow(QMainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()

        self.setWindowTitle("My App")

        layout = QHBoxLayout()

        layout.addWidget(Color('red'))
        layout.addWidget(Color('green'))
        layout.addWidget(Color('blue'))

        widget = QWidget()
        widget.setLayout(layout)
        self.setCentralWidget(widget)

Вложенные макеты

Есть более сложные макеты, состоящие из вложенных друг в друга макетов. Такие вложения делаются в макете с помощью .addLayout. Ниже мы добавляем QVBoxLayout в основной макет QHBoxLayout. Если добавить в QVBoxLayout несколько виджетов, они примут вертикальное расположение в первом слоте макета-предка:

class MainWindow(QMainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()

        self.setWindowTitle("My App")

        layout1 = QHBoxLayout()
        layout2 = QVBoxLayout()
        layout3 = QVBoxLayout()

        layout2.addWidget(Color('red'))
        layout2.addWidget(Color('yellow'))
        layout2.addWidget(Color('purple'))

        layout1.addLayout( layout2 )

        layout1.addWidget(Color('green'))

        layout3.addWidget(Color('red'))
        layout3.addWidget(Color('purple'))

        layout1.addLayout( layout3 )

        widget = QWidget()
        widget.setLayout(layout1)
        self.setCentralWidget(widget)

Запускаем. Виджеты должны располагаться в 3 столбцах горизонтально, причём в первом столбце тоже будет 3 расположенных вертикально виджета. Попробуйте!

Установим интервал между макетами с помощью .setContentMargins, а между элементами — с помощью .setSpacing:

layout1.setContentsMargins(0,0,0,0)
layout1.setSpacing(20)

Ниже показаны вложенные виджеты с отступами и интервалами между макетами. Поэкспериментируйте с цифрами, чтобы освоиться:

class MainWindow(QMainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()

        self.setWindowTitle("My App")

        layout1 = QHBoxLayout()
        layout2 = QVBoxLayout()
        layout3 = QVBoxLayout()

        layout1.setContentsMargins(0,0,0,0)
        layout1.setSpacing(20)

        layout2.addWidget(Color('red'))
        layout2.addWidget(Color('yellow'))
        layout2.addWidget(Color('purple'))

        layout1.addLayout( layout2 )

        layout1.addWidget(Color('green'))

        layout3.addWidget(Color('red'))
        layout3.addWidget(Color('purple'))

        layout1.addLayout( layout3 )

        widget = QWidget()
        widget.setLayout(layout1)
        self.setCentralWidget(widget)

QGridLayout: виджеты в сетке

Несмотря на все достоинства QVBoxLayout и QHBoxLayout, очень сложно добиться ровного расположения виджетов разного размера, если использовать эти макеты, например, для размещения нескольких элементов формы. Проблему решает QGridLayout.

В QGridLayout показываются позиции сетки для каждого местоположения
В QGridLayout показываются позиции сетки для каждого местоположения

Элементы в сетке QGridLayout размещаются особым образом. Для каждого виджета указывается его положение в строке и столбце. Если пропустить элементы, они останутся пустыми. При этом с QGridLayout не нужно заполнять все позиции в сетке.

QGridLayout с незаполненными слотами
QGridLayout с незаполненными слотами
class MainWindow(QMainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()

        self.setWindowTitle("My App")

        layout = QGridLayout()

        layout.addWidget(Color('red'), 0, 0)
        layout.addWidget(Color('green'), 1, 0)
        layout.addWidget(Color('blue'), 1, 1)
        layout.addWidget(Color('purple'), 2, 1)

        widget = QWidget()
        widget.setLayout(layout)
        self.setCentralWidget(widget)

QStackedLayout: несколько виджетов в одном месте

Последним рассмотрим макет QStackedLayout. В нём элементы размещаются друг за другом. Можно выбрать, какой виджет показывать. QStackedLayout используется для слоёв векторной графики в графическом приложении или для имитации интерфейса вкладок. Есть и виджет-контейнер QStackedWidget с точно таким же принципом работы. Он применяется, когда с помощью .setCentralWidget стопка виджетов добавляется прямо в QMainWindow.

QStackedLayout — здесь оказывается видимым только самый верхний виджет, который первым добавляется в макет
QStackedLayout — здесь оказывается видимым только самый верхний виджет, который первым добавляется в макет
QStackedLayout — здесь выбран 2-й виджет (обозначен цифрой 1) и выдвинут вперёд
QStackedLayout — здесь выбран 2-й виджет (обозначен цифрой 1) и выдвинут вперёд
from PyQt6.QtWidgets import QStackedLayout  # импортируем модули


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

        self.setWindowTitle("My App")

        layout = QStackedLayout()

        layout.addWidget(Color("red"))
        layout.addWidget(Color("green"))
        layout.addWidget(Color("blue"))
        layout.addWidget(Color("yellow"))

        layout.setCurrentIndex(3)

        widget = QWidget()
        widget.setLayout(layout)
        self.setCentralWidget(widget)

Именно с помощью QStackedWidget работают представления с вкладками. В любой момент времени видимым оказывается только одна вкладка (таб). С помощью .setCurrentIndex() или .setCurrentWidget() определяется, какой виджет отображать в тот или иной момент: здесь элемент задаётся по индексу в порядке добавления виджетов или по самому виджету.

Вот краткое демо с использованием QStackedLayout вместе с QButton при реализации интерфейса в виде вкладок:

import sys

from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import (
    QApplication,
    QHBoxLayout,
    QLabel,
    QMainWindow,
    QPushButton,
    QStackedLayout,
    QVBoxLayout,
    QWidget,
)

from layout_colorwidget import Color


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

        self.setWindowTitle("My App")

        pagelayout = QVBoxLayout()
        button_layout = QHBoxLayout()
        self.stacklayout = QStackedLayout()

        pagelayout.addLayout(button_layout)
        pagelayout.addLayout(self.stacklayout)

        btn = QPushButton("red")
        btn.pressed.connect(self.activate_tab_1)
        button_layout.addWidget(btn)
        self.stacklayout.addWidget(Color("red"))

        btn = QPushButton("green")
        btn.pressed.connect(self.activate_tab_2)
        button_layout.addWidget(btn)
        self.stacklayout.addWidget(Color("green"))

        btn = QPushButton("yellow")
        btn.pressed.connect(self.activate_tab_3)
        button_layout.addWidget(btn)
        self.stacklayout.addWidget(Color("yellow"))

        widget = QWidget()
        widget.setLayout(pagelayout)
        self.setCentralWidget(widget)

    def activate_tab_1(self):
        self.stacklayout.setCurrentIndex(0)

    def activate_tab_2(self):
        self.stacklayout.setCurrentIndex(1)

    def activate_tab_3(self):
        self.stacklayout.setCurrentIndex(2)


app = QApplication(sys.argv)

window = MainWindow()
window.show()

app.exec()
Пользовательский интерфейс в виде вкладок, реализованный с помощью QStackedLayout
Пользовательский интерфейс в виде вкладок, реализованный с помощью QStackedLayout

В Qt есть TabWidget, предоставляющий такой макет «из коробки», хотя и в виде виджета. Вот демо вкладки, воссоздаваемой с помощью QTabWidget:

import sys

from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import (
    QApplication,
    QLabel,
    QMainWindow,
    QPushButton,
    QTabWidget,
    QWidget,
)

from layout_colorwidget import Color


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

        self.setWindowTitle("My App")

        tabs = QTabWidget()
        tabs.setTabPosition(QTabWidget.West)
        tabs.setMovable(True)

        for n, color in enumerate(["red", "green", "blue", "yellow"]):
            tabs.addTab(Color(color), color)

        self.setCentralWidget(tabs)


app = QApplication(sys.argv)

window = MainWindow()
window.show()

app.exec()
Интерфейс в виде вкладок с использованием QTabWidget
Интерфейс в виде вкладок с использованием QTabWidget

Видите? Немного проще и красивее! Расположение вкладок устанавливается по сторонам света, а возможность их перемещения — с помощью .setMoveable. Панель вкладок на macOS отличается от других: по умолчанию они здесь даны в виде кружков и обычно используются в панелях конфигурации. Для документов включается режим документа — здесь создаются тонкие вкладки, похожие на вкладки других платформ. Эта опция относится только к macOS:

    tabs = QTabWidget()
    tabs.setDocumentMode(True)
QTabWidget в режиме документа на macOS
QTabWidget в режиме документа на macOS

Позже мы рассмотрим другие виджеты сложнее.

Продолжить изучение Python вы сможете по книге автора этих статей или на наших курсах:


Панели инструментов, меню и QAction

Рассмотрим элементы стандартного пользовательского интерфейса, которые вы наверняка видели во многих приложениях: панели инструментов и меню. Также изучим чёткую систему Qt для минимизации дублирования различных частей пользовательского интерфейса — QAction.

Панели инструментов

Панель инструментов — один из самых часто встречающихся элементов пользовательского интерфейса. Это панели с иконками и/или текстом, используемые в приложении для выполнения стандартных задач, доступ к которым через меню затруднителен. Этот функционал пользовательского интерфейса есть почти во всех приложениях. Хотя некоторые сложные приложения, в частности в пакете Microsoft Office, перешли на контекстные ленточные интерфейсы, для большинства создаваемых приложений обычно достаточно стандартной панели инструментов.

Стандартные элементы графического интерфейса
Стандартные элементы графического интерфейса

Начнём со «скелета» простого приложения и его настройки. Сохраните этот код в файле app.py (в нём прописан весь импорт для последующих этапов):

import sys
from PyQt6.QtWidgets import (
    QMainWindow, QApplication,
    QLabel, QToolBar, QStatusBar
)
from PyQt6.QtGui import QAction, QIcon
from PyQt6.QtCore import Qt

class MainWindow(QMainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()

        self.setWindowTitle("My Awesome App")


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

Если вы переходите с PyQt5 на PyQt6, QAction в новой версии доступен через модуль QtGui.

Добавление панели инструментов

Панель инструментов в Qt создаётся из класса QToolBar. Добавим панель в приложение, создав сначала экземпляр класса, а затем вызвав .addToolbar в QMainWindow. Передав первым параметром QToolBar строку, задаём имя панели инструментов: по нему эта панель идентифицируется в пользовательском интерфейсе:

class MainWindow(QMainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()

        self.setWindowTitle("My Awesome App")

        label = QLabel("Hello!")
        label.setAlignment(Qt.AlignmentFlag.AlignCenter)

        self.setCentralWidget(label)

        toolbar = QToolBar("My main toolbar")
        self.addToolBar(toolbar)


    def onMyToolBarButtonClick(self, s):
        print("click", s)

Запускаем. Появится тонкая серая полоска наверху окна. Это панель инструментов. Нажмите правую кнопку и выберите имя панели, чтобы отключить её.

Окно с панелью инструментов
Окно с панелью инструментов

Но как теперь вернуть панель инструментов? После её удаления негде нажать правой кнопкой, чтобы снова её добавить. Поэтому нужно или оставлять одну панель неудалённой, или иметь альтернативный интерфейс, чтобы включать и выключать панели.

Сделаем панель чуть интереснее. Вместо добавления виджета QButton используем дополнительный функционал Qt — класс QAction для описания абстрактных пользовательских интерфейсов.

С его помощью внутри одного объекта определяется несколько элементов интерфейса, с которыми пользователь сможет взаимодействовать. Например, опция «Вырезать» есть в меню «Правка», и в панели инструментов (значок ножниц) и доступна по комбинации клавиш Ctrl-X (Cmd-X на Mac).

Без QAction пришлось бы определять её в нескольких местах. А в Qt определяем один QAction с запущенным действием, которое добавляется и в меню, и в панель инструментов. У каждого QAction есть имена, сообщения о состоянии, иконки и сигналы, к которым можно подключиться (и многое другое).

Вот первый добавленный QAction:

class MainWindow(QMainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()

        self.setWindowTitle("My Awesome App")

        label = QLabel("Hello!")
        label.setAlignment(Qt.AlignmentFlag.AlignCenter)

        self.setCentralWidget(label)

        toolbar = QToolBar("My main toolbar")
        self.addToolBar(toolbar)

        button_action = QAction("Your button", self)
        button_action.setStatusTip("This is your button")
        button_action.triggered.connect(self.onMyToolBarButtonClick)
        toolbar.addAction(button_action)

    def onMyToolBarButtonClick(self, s):
        print("click", s)

Сначала создаём функцию, принимающую сигнал от QAction (так мы проверяем её работоспособность). Затем определяем сам QAction. Когда создаётся экземпляр, передаётся метка для действия и/или иконка.

Также нужно передать любой QObject (это предок действия). Передаём self как ссылку на главное окно. Как ни странно, для QAction элемент-предок передаётся в последнем параметре.

Дальше настраиваем подсказку статуса — этот текст будет отображаться в строке состояния, как только она появится. Наконец, подключаем сигнал .triggered к пользовательской функции. Он срабатывает, когда вызывается (или активируется) QAction.

Запускаем! Появится кнопка с определённой нами меткой. Нажимаем её, и пользовательская функция выдаст click («Нажатие») и статус кнопки.

Панель инструментов с кнопкой QAction
Панель инструментов с кнопкой QAction

Почему сигнал всегда false? Переданный сигнал указывает, нажата ли кнопка. В нашем случае она не допускает нажатия, поэтому всегда false. Скоро покажем, как включить возможность её нажатия.

Добавляем строку состояния. Создаём объект строки состояния, вызывая QStatusBar, и передаём его в .setStatusBar. Настройки этого statusBar (строки состояния) менять не нужно: просто передаём её в одной строке при создании:

class MainWindow(QMainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()

        self.setWindowTitle("My Awesome App")

        label = QLabel("Hello!")
        label.setAlignment(Qt.AlignmentFlag.AlignCenter)

        self.setCentralWidget(label)

        toolbar = QToolBar("My main toolbar")
        self.addToolBar(toolbar)

        button_action = QAction("Your button", self)
        button_action.setStatusTip("This is your button")
        button_action.triggered.connect(self.onMyToolBarButtonClick)
        toolbar.addAction(button_action)

        self.setStatusBar(QStatusBar(self))

    def onMyToolBarButtonClick(self, s):
        print("click", s)

Запускаем. Наводим курсор мыши на кнопку панели инструментов и видим в строке состояния текст статуса.

Текст строки состояния обновляется при наведении курсора на actions (действия)
Текст строки состояния обновляется при наведении курсора на actions (действия)

Теперь сделаем QAction переключаемым: при первом нажатии он включается, при повторном — отключается. Для этого просто вызываем setCheckable(True) в объекте QAction:

class MainWindow(QMainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()

        self.setWindowTitle("My Awesome App")

        label = QLabel("Hello!")
        label.setAlignment(Qt.AlignmentFlag.AlignCenter)

        self.setCentralWidget(label)

        toolbar = QToolBar("My main toolbar")
        self.addToolBar(toolbar)

        button_action = QAction("Your button", self)
        button_action.setStatusTip("This is your button")
        button_action.triggered.connect(self.onMyToolBarButtonClick)
        button_action.setCheckable(True)
        toolbar.addAction(button_action)

        self.setStatusBar(QStatusBar(self))

    def onMyToolBarButtonClick(self, s):
        print("click", s)

Запускаем и нажимаем кнопку — её состояние переключается из нажатого в ненажатое. При этом пользовательская функция слота теперь чередует вывод True и False.

Включённая кнопка на панели инструментов
Включённая кнопка на панели инструментов

Есть .toggled, который сигнализирует только при включении/отключении кнопки. Эффект тот же, поэтому в .toggled особой надобности нет.

Добавим к кнопке иконку. Скачаем отличный набор красивых иконок Fugue 16 х 16 пикселей Юсукэ Камияманэ, которые придадут приложениям приятный профессиональный вид. Это бесплатно — при распространении приложения требуется только ссылка на автора.

Набор иконок Fugue от Юсукэ Камияманэ
Набор иконок Fugue от Юсукэ Камияманэ

Выбираем изображение (я выбрал файл bug.png) и копируем его в папку с исходным кодом. Создаём объект QIcon, передав имя файла классу, например QIcon('bug.png'). Если поместить файл в другую папку, нужен полный относительный или абсолютный путь к нему. Наконец, чтобы добавить иконку и кнопку в QAction, просто передаём её первым параметром при создании QAction.

Также нужно указать размер иконок, иначе вокруг них будет множество отступов. Сделаем это, вызвав функцию .setIconSize() с объектом QSize:

class MainWindow(QMainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()

        self.setWindowTitle("My Awesome App")

        label = QLabel("Hello!")
        label.setAlignment(Qt.AlignmentFlag.AlignCenter)

        self.setCentralWidget(label)

        toolbar = QToolBar("My main toolbar")
        toolbar.setIconSize(QSize(16,16))
        self.addToolBar(toolbar)

        button_action = QAction(QIcon("bug.png"), "Your button", self)
        button_action.setStatusTip("This is your button")
        button_action.triggered.connect(self.onMyToolBarButtonClick)
        button_action.setCheckable(True)
        toolbar.addAction(button_action)

        self.setStatusBar(QStatusBar(self))


    def onMyToolBarButtonClick(self, s):
        print("click", s)

Запускаем. QAction теперь в виде иконки. Всё должно работать точно так же, как и раньше.

Кнопка действий теперь с иконкой
Кнопка действий теперь с иконкой

Внимание! Чтобы определить, что отображать на панели инструментов: иконку, текст или иконку с текстом, в Qt используются стандартные настройки ОС. Выбрать можно и самостоятельно с помощью .setToolButtonStyle. Этот слот принимает из пространства имён Qt такие флаги:

Флаг PyQt6 (полный код)

Расположение

Qt.ToolButtonIconOnly

Только иконка, без текста

Qt.ToolButtonTextOnly

Только текст, без иконки

Qt.ToolButtonTextBesideIcon

Иконка и текст рядом с иконкой

Qt.ToolButtonTextUnderIcon

Иконка и текст под иконкой

Qt.ToolButtonFollowStyle

Согласно установленному стилю рабочего стола

Значение по умолчанию — Qt.ToolButtonFollowStyle. То есть в приложении будут применяться стандартные/глобальные настройки рабочего стола, на котором оно работает. Обычно рекомендуется этот флаг: приложение с ним максимально нативно.

Наконец, добавляем на панель инструментов вторую кнопку и виджет чекбокса. Смело добавляйте любой виджет:

import sys

from PyQt6.QtCore import QSize, Qt
from PyQt6.QtGui import QAction, QIcon
from PyQt6.QtWidgets import (
    QApplication,
    QCheckBox,
    QLabel,
    QMainWindow,
    QStatusBar,
    QToolBar,
)


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

        self.setWindowTitle("My App")

        label = QLabel("Hello!")
        label.setAlignment(Qt.AlignmentFlag.AlignCenter)

        self.setCentralWidget(label)

        toolbar = QToolBar("My main toolbar")
        toolbar.setIconSize(QSize(16, 16))
        self.addToolBar(toolbar)

        button_action = QAction(QIcon("bug.png"), "&Your button", self)
        button_action.setStatusTip("This is your button")
        button_action.triggered.connect(self.onMyToolBarButtonClick)
        button_action.setCheckable(True)
        toolbar.addAction(button_action)

        toolbar.addSeparator()

        button_action2 = QAction(QIcon("bug.png"), "Your &button2", self)
        button_action2.setStatusTip("This is your button2")
        button_action2.triggered.connect(self.onMyToolBarButtonClick)
        button_action2.setCheckable(True)
        toolbar.addAction(button_action2)

        toolbar.addWidget(QLabel("Hello"))
        toolbar.addWidget(QCheckBox())

        self.setStatusBar(QStatusBar(self))

    def onMyToolBarButtonClick(self, s):
        print("click", s)


app = QApplication(sys.argv)

window = MainWindow()
window.show()

app.exec()

Запускаем! Появится несколько кнопок и чекбокс.

Панель инструментов с action и двумя виджетами
Панель инструментов с action и двумя виджетами

Создавайте приложения с графическим интерфейсом с помощью Python и Qt6

Простой способ создания настольных приложений

Полное руководство, обновлённое для 2021 года и PyQt6. Всё, что нужно для создания реальных приложений.

Меню

Меню — ещё один стандартный компонент пользовательского интерфейса. Обычно оно находится в верхней части окна или экрана в macOS. Меню даёт доступ ко всем стандартным функциям приложения. В обычном меню есть элементы файл, правка, справка и т. д. Меню могут быть вложенными. Часто в меню поддерживаются и отображаются клавиши быстрого доступа к функциям.

Стандартные элементы графического интерфейса. Меню
Стандартные элементы графического интерфейса. Меню

Чтобы создать меню, нужно прописать в QMainWindow строку меню .menuBar() и добавить в неё меню, вызвав .addMenu() и передав название создаваемого меню, например &File. Символом амперсанда определяется клавиша быстрого доступа, которая используется для перехода в это меню после нажатия Alt.

На macOS она не видна. Клавишы быстрого доступа в этой ОС работают иначе. Скоро я расскажу об этом.

Здесь и пригодятся QAction. Повторно применяем уже имеющийся QAction, чтобы добавить ту же функцию в меню. Добавляем действие, вызывая .addAction и передавая одно из уже определённых действий:

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

        self.setWindowTitle("My App")

        label = QLabel("Hello!")
        label.setAlignment(Qt.AlignmentFlag.AlignCenter)

        self.setCentralWidget(label)

        toolbar = QToolBar("My main toolbar")
        toolbar.setIconSize(QSize(16, 16))
        self.addToolBar(toolbar)

        button_action = QAction(QIcon("bug.png"), "&Your button", self)
        button_action.setStatusTip("This is your button")
        button_action.triggered.connect(self.onMyToolBarButtonClick)
        button_action.setCheckable(True)
        toolbar.addAction(button_action)

        toolbar.addSeparator()

        button_action2 = QAction(QIcon("bug.png"), "Your &button2", self)
        button_action2.setStatusTip("This is your button2")
        button_action2.triggered.connect(self.onMyToolBarButtonClick)
        button_action2.setCheckable(True)
        toolbar.addAction(button_action2)

        toolbar.addWidget(QLabel("Hello"))
        toolbar.addWidget(QCheckBox())

        self.setStatusBar(QStatusBar(self))

        menu = self.menuBar()

        file_menu = menu.addMenu("&File")
        file_menu.addAction(button_action)

    def onMyToolBarButtonClick(self, s):
        print("click", s)

Нажимаем на пункт меню. Он переключаемый — наследует функционал QAction.

Меню, отображаемое в окне (на macOS оно будет в верхней части экрана)
Меню, отображаемое в окне (на macOS оно будет в верхней части экрана)

Добавим в меню разделитель — горизонтальную линию, а ещё второй QAction:

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

        self.setWindowTitle("My App")

        label = QLabel("Hello!")
        label.setAlignment(Qt.AlignmentFlag.AlignCenter)

        self.setCentralWidget(label)

        toolbar = QToolBar("My main toolbar")
        toolbar.setIconSize(QSize(16, 16))
        self.addToolBar(toolbar)

        button_action = QAction(QIcon("bug.png"), "&Your button", self)
        button_action.setStatusTip("This is your button")
        button_action.triggered.connect(self.onMyToolBarButtonClick)
        button_action.setCheckable(True)
        toolbar.addAction(button_action)

        toolbar.addSeparator()

        button_action2 = QAction(QIcon("bug.png"), "Your &button2", self)
        button_action2.setStatusTip("This is your button2")
        button_action2.triggered.connect(self.onMyToolBarButtonClick)
        button_action2.setCheckable(True)
        toolbar.addAction(button_action2)

        toolbar.addWidget(QLabel("Hello"))
        toolbar.addWidget(QCheckBox())

        self.setStatusBar(QStatusBar(self))

        menu = self.menuBar()

        file_menu = menu.addMenu("&File")
        file_menu.addAction(button_action)
        file_menu.addSeparator()
        file_menu.addAction(button_action2)

    def onMyToolBarButtonClick(self, s):
        print("click", s)

И запускаем. Появятся два пункта меню, разделённые линией.

Действия отображающиеся в меню:
Действия отображающиеся в меню:

Используя символ амперсанда, добавим в меню клавиши-ускорители, чтобы одной клавишей переходить к пункту меню, когда это меню открыто. На macOS это не работает.

Чтобы добавить подменю, просто создаём новое меню, вызывая addMenu() в меню-предке. Затем добавляем в него действия, как в обычное меню:

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

        self.setWindowTitle("My App")

        label = QLabel("Hello!")
        label.setAlignment(Qt.AlignmentFlag.AlignCenter)

        self.setCentralWidget(label)

        toolbar = QToolBar("My main toolbar")
        toolbar.setIconSize(QSize(16, 16))
        self.addToolBar(toolbar)

        button_action = QAction(QIcon("bug.png"), "&Your button", self)
        button_action.setStatusTip("This is your button")
        button_action.triggered.connect(self.onMyToolBarButtonClick)
        button_action.setCheckable(True)
        toolbar.addAction(button_action)

        toolbar.addSeparator()

        button_action2 = QAction(QIcon("bug.png"), "Your &button2", self)
        button_action2.setStatusTip("This is your button2")
        button_action2.triggered.connect(self.onMyToolBarButtonClick)
        button_action2.setCheckable(True)
        toolbar.addAction(button_action2)

        toolbar.addWidget(QLabel("Hello"))
        toolbar.addWidget(QCheckBox())

        self.setStatusBar(QStatusBar(self))

        menu = self.menuBar()

        file_menu = menu.addMenu("&File")
        file_menu.addAction(button_action)
        file_menu.addSeparator()

        file_submenu = file_menu.addMenu("Submenu")
        file_submenu.addAction(button_action2)

    def onMyToolBarButtonClick(self, s):
        print("click", s)
Подменю, вложенное в меню «File»
Подменю, вложенное в меню «File»

Наконец, добавляем в QAction клавишу быстрого доступа. Определяем её, передавая setKeySequence(), и сочетание клавиш. В меню появятся все написанные сочетания клавиш.

Эта клавиша быстрого доступа связана с QAction и будет работать независимо от того, куда добавлен QAction, то есть в меню или на панель инструментов.

Сочетания клавиш определяются так: они передаются в виде текста с названиями клавиш из пространства имён Qt или определённые сочетания клавиш берутся оттуда же. По возможности используйте последний подход, чтобы обеспечить соответствие стандартам ОС.

Вот полный код, показывающий кнопки и меню панели инструментов:

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

        self.setWindowTitle("My App")

        label = QLabel("Hello!")

        # У пространства имён Qt много атрибутов для настройки
        # виджетов. См. http://doc.qt.io/qt-5/qt.html
        label.setAlignment(Qt.AlignmentFlag.AlignCenter)

        # Устанавливает центральный виджет окна. Виджет расширится,
        # по умолчанию займёт всё пространство окна.
        self.setCentralWidget(label)

        toolbar = QToolBar("My main toolbar")
        toolbar.setIconSize(QSize(16, 16))
        self.addToolBar(toolbar)

        button_action = QAction(QIcon("bug.png"), "&Your button", self)
        button_action.setStatusTip("This is your button")
        button_action.triggered.connect(self.onMyToolBarButtonClick)
        button_action.setCheckable(True)
        # Клавиши быстрого доступа вводим, используя их названия (например, Ctrl+p),
        # идентификаторы пространства имён Qt (например, Qt.CTRL + Qt.Key_P)
        # или системо-независимые идентификаторы (например, QKeySequence.Print)
        button_action.setShortcut(QKeySequence("Ctrl+p"))
        toolbar.addAction(button_action)

        toolbar.addSeparator()

        button_action2 = QAction(QIcon("bug.png"), "Your &button2", self)
        button_action2.setStatusTip("This is your button2")
        button_action2.triggered.connect(self.onMyToolBarButtonClick)
        button_action2.setCheckable(True)
        toolbar.addAction(button_action)

        toolbar.addWidget(QLabel("Hello"))
        toolbar.addWidget(QCheckBox())

        self.setStatusBar(QStatusBar(self))

        menu = self.menuBar()

        file_menu = menu.addMenu("&File")
        file_menu.addAction(button_action)

        file_menu.addSeparator()

        file_submenu = file_menu.addMenu("Submenu")

        file_submenu.addAction(button_action2)

    def onMyToolBarButtonClick(self, s):
        print("click", s)

Попробуйте создать свои меню с помощью QAction и QMenu.

Напомним о книге автора статей и о наших курсах:


Диалоговые окна и окна предупреждений

Диалоги — это компоненты графического интерфейса, позволяющие общаться с пользователем (отсюда название «Диалог»). Они обычно используются при открытии и сохранении файлов, в настройках, предпочтениях или функциях, которые не помещаются в основном пользовательском интерфейсе приложения. Это небольшие модальные (или блокирующие) окна, которые находятся перед основным приложением, пока их не закроют. В Qt есть специальные диалоговые окна для самых распространённых ситуаций пользовательского взаимодействия на той или иной платформе:

Стандартные функции графического интерфейса: окно поиска
Стандартные функции графического интерфейса: окно поиска
Стандартные функции графического интерфейса: окно открытия файла
Стандартные функции графического интерфейса: окно открытия файла

Диалоговые окна в Qt обрабатываются классом QDialog. Чтобы создать такое окно, просто создаём новый объект типа QDialog, передающий другой виджет, например QMainWindow, в качестве родительского. Создадим собственный QDialog. Начнём со «скелета» простого приложения с нажимаемой кнопкой, подключённой к методу слота:

import sys

from PyQt6.QtWidgets import QApplication, QMainWindow, QPushButton


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

        self.setWindowTitle("My App")

        button = QPushButton("Press me for a dialog!")
        button.clicked.connect(self.button_clicked)
        self.setCentralWidget(button)

    def button_clicked(self, s):
        print("click", s)


app = QApplication(sys.argv)

window = MainWindow()
window.show()

app.exec()

В слоте button_clicked, который получает сигнал от нажатия кнопки, создаём экземпляр диалога, передавая в качестве родительского экземпляр QMainWindow. Так диалоговое окно станет модальным окном QMainWindow, то есть полностью заблокирует взаимодействие с родительским окном:

import sys

from PyQt6.QtWidgets import QApplication, QDialog, QMainWindow, QPushButton


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

        self.setWindowTitle("My App")

        button = QPushButton("Press me for a dialog!")
        button.clicked.connect(self.button_clicked)
        self.setCentralWidget(button)

    def button_clicked(self, s):
        print("click", s)

        dlg = QDialog(self)
        dlg.setWindowTitle("HELLO!")
        dlg.exec()


app = QApplication(sys.argv)

window = MainWindow()
window.show()

app.exec()

Запускаем. При нажатии кнопки появится пустое диалоговое окно.

чтобы создать основной цикл событий приложения, запустим его с помощью .exec() точно так же, как в QApplication. Это не совпадение: когда QDialog выполняется в exec, создаётся совершенно новый цикл событий — именно для диалогового окна.

QDialog полностью блокирует выполнение приложения. Не запускайте диалоговое окно и не ожидайте, что ещё где-то в приложении произойдёт ещё что-то. Позже мы увидим, как использовать потоки и процессы, чтобы решить эту проблему:

Пустой диалог, перекрывающий окно
Пустой диалог, перекрывающий окно

Пока это окно не очень интересное. Добавим ему заголовок и кнопки ОК и Cancel, чтобы пользователь мог принять или отклонить модальное окно. А чтобы настроить QDialog, создадим из него подкласс:

class CustomDialog(QDialog):
    def __init__(self):
        super().__init__()

        self.setWindowTitle("HELLO!")

        QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel

        self.buttonBox = QDialogButtonBox(QBtn)
        self.buttonBox.accepted.connect(self.accept)
        self.buttonBox.rejected.connect(self.reject)

        self.layout = QVBoxLayout()
        message = QLabel("Something happened, is that OK?")
        self.layout.addWidget(message)
        self.layout.addWidget(self.buttonBox)
        self.setLayout(self.layout)

В этом коде из QDialog мы сначала создаём подкласс CustomDialog. Затем в QMainWindow применяем настройки в блоке класса __init__: они применяются, когда объект создаётся. Устанавливаем заголовок для QDialog с помощью .setWindowTitle() — точно так же, как для главного окна.

Следующий блок кода отвечает за создание и отображение кнопок диалогового окна. Он чуть сложнее, чем можно ожидать. Это обусловлено гибкостью Qt в расположении кнопок диалогового окна на разных платформах.

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

Первый шаг при создании диалогового окна с кнопками — определить отображаемые кнопки, используя атрибуты пространства имён из QDialogButtonBox:

Вот полный список кнопок
  • QDialogButtonBox.StandardButton.Ok (стандартная кнопка Ok).

  • QDialogButtonBox.StandardButton.Open (стандартная кнопка «Открыть»).

  • QDialogButtonBox.StandardButton.Save (стандартная кнопка «Сохранить»).

  • QDialogButtonBox.StandardButton.Cancel (стандартная кнопка «Отмена»).

  • QDialogButtonBox.StandardButton.Close (стандартная кнопка «Закрыть»).

  • QDialogButtonBox.StandardButton.Discard (стандартная кнопка «Отменить»).

  • QDialogButtonBox.StandardButton.Apply (стандартная кнопка «Применить»).

  • QDialogButtonBox.StandardButton.Reset (стандартная кнопка «Сброс»).

  • QDialogButtonBox.StandardButton.RestoreDefaults (стандартная кнопка «Восстановить значения по умолчанию»).

  • QDialogButtonBox.StandardButton.Help (стандартная кнопка «Справка»).

  • QDialogButtonBox.StandardButton.SaveAll (стандартная кнопка «Сохранить всё»).

  • QDialogButtonBox.StandardButton.Yes (стандартная кнопка «Да»).

  • QDialogButtonBox.StandardButton.YesToAll (стандартная кнопка «Да, для всех»).

  • QDialogButtonBox.StandardButton.No (стандартная кнопка «Нет»).

  • QDialogButtonBox.StandardButton.Abort (стандартная кнопка «Прервать»).

  • QDialogButtonBox.StandardButton.Retry (стандартная кнопка «Повторить попытку»).

  • QDialogButtonBox.StandardButton.Ignore (стандартная кнопка «Пропустить»).

  • QDialogButtonBox.StandardButton.NoButton (стандартная кнопка «Кнопка отсутствует»).

Этого должно быть достаточно для создания любого диалогового окна. Создадим строку из нескольких кнопок, пропустив их через логическое «ИЛИ» с помощью канала (|). В Qt очерёдность обрабатывается автоматически согласно стандартам платформы. Вот код для кнопок «ОК» и «Cancel»:

buttons = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel

Переменная buttons теперь представлена целочисленным значением. Дальше для хранения кнопок нужно создать экземпляр QDialogButtonBox. Флаг для отображения кнопок передаётся в первом параметре.

Чтобы задействовать кнопки, к слотам в диалоговом окне нужно подключить подходящие сигналы QDialogButtonBox. В нашем случае подключены сигналы.accepted и .rejected от QDialogButtonBox к их обработчикам в подклассе из QDialog.

Чтобы QDialogButtonBox появился в диалоговом окне, надо добавить его в макет диалога. Для главного окна создаём макет, добавляем в него виджет QDialogButtonBox и устанавливаем в диалоговом окне. Запускаем CustomDialog в слоте MainWindow.button_clicked:

class MainWindow(QMainWindow):

    # ... добавляем следующий метод после __init__

    def button_clicked(self, s):
        print("click", s)

        dlg = CustomDialog()
        if dlg.exec():
            print("Success!")
        else:
            print("Cancel!")

Установим значение по умолчанию parent=None, чтобы при желании убрать этот параметр.

Создав экземпляр CustomDialog, передаём в качестве параметра главное окно. В методе button_clicked объект главного окна — self:

    def button_clicked(self, s):
        print("click", s)

        dlg = CustomDialog(self)
        if dlg.exec():
            print("Success!")
        else:
            print("Cancel!")

Запускаем! Теперь диалоговое окно появляется точно по центру родительского:

Диалоговое окно, расположенное в центре поверх родительского
Диалоговое окно, расположенное в центре поверх родительского

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

Простые окна сообщений с QMessageBox

Многие диалоговые окна строятся по этому принципу сообщений с кнопками, нажатием на которые предложенное в окне принимается или отклоняется. Такие окна можно создавать самостоятельно, но в Qt есть класс QMessageBox для создания разных диалоговых окон: с информацией, предупреждениями, справкой или вопросами.

В примере ниже создаётся и показывается простой QMessageBox:

import sys

from PyQt6.QtWidgets import QApplication, QDialog, QMainWindow, QMessageBox, QPushButton


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

        self.setWindowTitle("My App")

        button = QPushButton("Press me for a dialog!")
        button.clicked.connect(self.button_clicked)
        self.setCentralWidget(button)

    def button_clicked(self, s):
        dlg = QMessageBox(self)
        dlg.setWindowTitle("I have a question!")
        dlg.setText("This is a simple dialog")
        button = dlg.exec()

        if button == QMessageBox.StandardButton.Ok:
            print("OK!")

app = QApplication(sys.argv)

window = MainWindow()
window.show()

app.exec()

Запускаем! Появится простое диалоговое окно с кнопкой ОК:

Диалоговое окно QMessageBox
Диалоговое окно QMessageBox

Как и с диалоговым окном с кнопками, показываемые в QMessageBox кнопки настроены с тем же набором констант, которые используются с | (логическим оператором «ИЛИ») для отображения нескольких кнопок.

Вот полный список доступных типов кнопок
  • QMessageBox.StandardButton.Ok (стандартная кнопка Ok).

  • QMessageBox.StandardButton.Open (стандартная кнопка «Открыть»).

  • QMessageBox.StandardButton.Save (стандартная кнопка «Сохранить»).

  • QMessageBox.StandardButton.Cancel (стандартная кнопка «Отмена»).

  • QMessageBox.StandardButton.Close (стандартная кнопка «Закрыть»).

  • QMessageBox.StandardButton.Discard (стандартная кнопка «Отменить»).

  • QMessageBox.StandardButton.Apply (стандартная кнопка «Применить»).

  • QMessageBox.StandardButton.Reset (стандартная кнопка «Сброс»).

  • QMessageBox.StandardButton.RestoreDefaults (стандартная кнопка «Восстановить значения по умолчанию»).

  • QMessageBox.StandardButton.Help (стандартная кнопка «Справка»).

  • QMessageBox.StandardButton.SaveAll (стандартная кнопка «Сохранить всё»).

  • QMessageBox.StandardButton.Yes (стандартная кнопка «Да»).

  • QMessageBox.StandardButton.YesToAll (стандартная кнопка «Да, для всех»).

  • QMessageBox.StandardButton.No (стандартная кнопка «Нет»).

  • QMessageBox.StandardButton.NoToAll (стандартная кнопка «Нет, для всех»).

  • QMessageBox.StandardButton.Abort (стандартная кнопка «Прервать»).

  • QMessageBox.StandardButton.Retry (стандартная кнопка «Повторить попытку»).

  • QMessageBox.StandardButton.Ignore (стандартная кнопка «Пропустить»).

  • QMessageBox.StandardButton.NoButton (стандартная кнопка «Кнопка отсутствует»).

Можно также настроить иконку диалогового окна одним из следующих способов:

Состояние иконки

QMessageBox.Icon.NoIcon («Иконка отсутствует»).

QMessageBox.Icon.Question («Иконка окна-вопроса»).

QMessageBox.Icon.Information («Иконка окна-информации»).

QMessageBox.Icon.Warning («Иконка окна-предупреждения»).

QMessageBox.Icon.Critical («Иконка окна с критически важной информацией»).

В примере ниже создаётся диалоговое окно-вопрос с кнопками Yes («Да») и No («Нет»):

    def button_clicked(self, s):
        dlg = QMessageBox(self)
        dlg.setWindowTitle("I have a question!")
        dlg.setText("This is a question dialog")
        dlg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
        dlg.setIcon(QMessageBox.Icon.Question)
        button = dlg.exec()

        if button == QMessageBox.Yes:
            print("Yes!")
        else:
            print("No!")

Появится диалоговое окно-вопрос с кнопками Yes («Да») и No («Нет»):

Диалоговое окно-вопрос, созданное с помощью QMessageBox
Диалоговое окно-вопрос, созданное с помощью QMessageBox

Книга автора

Диалоги QMessageBox

В QMessageBox есть методы, упрощающие создание диалоговых окон сообщений такого типа:

QMessageBox.about(parent, title, message)
QMessageBox.critical(parent, title, message)
QMessageBox.information(parent, title, message)
QMessageBox.question(parent, title, message)
QMessageBox.warning(parent, title, message)

Параметр parent — это окно, дочерним элементом которого является диалоговое окно. Если запускаете диалоговое окно из главного, просто передайте self. В примере ниже снова создаётся диалоговое окно-вопрос с кнопками Yes («Да») и No («Нет»):

    def button_clicked(self, s):

        button = QMessageBox.question(self, "Question dialog", "The longer message")

        if button == QMessageBox.StandardButton.Yes:
            print("Yes!")
        else:
            print("No!")

Запускаем! Результат будет тот же, но на этот раз мы добиваемся его с помощью метода .question().

Вместо вызова exec() теперь вызывается метод диалогового окна, которое и создаётся. Возвращаемое значение в каждом из методов — нажатая кнопка. Определяем, какая кнопка нажата, сравнивая это значение с константами кнопки.

Эти методы диалоговых окон — information, question, warning и critical — принимают также дополнительные аргументы buttons и defaultButton для настройки кнопок диалогового окна и выбора одного из них по умолчанию. Обычно это значение по умолчанию не меняют:

    def button_clicked(self, s):

        button = QMessageBox.critical(
            self,
            "Oh dear!",
            "Something went very wrong.",
            buttons=QMessageBox.StandardButton.Discard | QMessageBox.StandardButton.NoToAll | QMessageBox.StandardButton.Ignore,
            defaultButton=QMessageBox.StandardButton.Discard,
        )

        if button == QMessageBox.StandardButton.Discard:
            print("Discard!")
        elif button == QMessageBox.StandardButton.NoToAll:
            print("No to all!")
        else:
            print("Ignore!")

Появится диалоговое окно критически важной информации с настраиваемыми кнопками:

Критическая ошибка! Это ужасное диалоговое окно
Критическая ошибка! Это ужасное диалоговое окно

В большинстве ситуаций этих простых диалоговых окон достаточно. А в изучении Python вам помогут книга автора статей и наши курсы:


Дополнительные окна

Ранее мы уже рассказывали о том, как создавать диалоговые окна. Это специальные окна, которые переключают на себя внимание пользователя и запускают собственный цикл событий, блокируя выполнение остальной части приложения.

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

Открывать новые окна несложно, но кое-что стоит учесть, чтобы они работали корректно. В этой части мы расскажем, как создать новое окно и при необходимости показать и/или скрыть внешние окна.

Создание нового окна

В Qt любой виджет без родительского элемента — это окно. То есть, чтобы показать новое окно, нужно просто создать новый экземпляр виджета. Это может быть виджет любого типа (в принципе любой подкласс QWidget), в том числе другой QMainWindow.

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

Как и главное окно, новое окно нужно не только создать, но и показать:

from PyQt6.QtWidgets import QApplication, QMainWindow, QPushButton, QLabel, QVBoxLayout, QWidget

import sys


class AnotherWindow(QWidget):
    """
    This "window" is a QWidget. If it has no parent, it
    will appear as a free-floating window as we want.
    """
    def __init__(self):
        super().__init__()
        layout = QVBoxLayout()
        self.label = QLabel("Another Window")
        layout.addWidget(self.label)
        self.setLayout(layout)


class MainWindow(QMainWindow):

    def __init__(self):
        super().__init__()
        self.button = QPushButton("Push for Window")
        self.button.clicked.connect(self.show_new_window)
        self.setCentralWidget(self.button)

    def show_new_window(self, checked):
        w = AnotherWindow()
        w.show()


app = QApplication(sys.argv)
w = MainWindow()
w.show()
app.exec()
Главное окно с кнопкой для запуска дочернего окна
Главное окно с кнопкой для запуска дочернего окна

Если запустить это, появится главное окно. При нажатии на кнопку может появиться второе окно, но лишь на долю секунды. Что происходит?

    def show_new_window(self, checked):
        w = AnotherWindow()
        w.show()

Внутри этого метода объект окна (виджета) создаётся, сохраняется в переменной w и отображается. Но, покинув метод, мы остаёмся без ссылки на эту локальную переменную w. Поэтому она очищается, а окно уничтожается. Проблема решается сохранением ссылки на окно где-нибудь, например в объекте self:

    def show_new_window(self, checked):
        self.w = AnotherWindow()
        self.w.show()

Теперь, если нажать кнопку, чтобы показать новое окно, это окно сохранится. Но что, если нажать кнопку ещё раз? Окно будет создано снова! Это новое окно заменит старое в переменной self.w, а старое будет уничтожено (ведь ссылки на него теперь нет).

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

from random import randint


class AnotherWindow(QWidget):
    """
    This "window" is a QWidget. If it has no parent, it
    will appear as a free-floating window as we want.
    """
    def __init__(self):
        super().__init__()
        layout = QVBoxLayout()
        self.label = QLabel("Another Window % d" % randint(0,100))
        layout.addWidget(self.label)
        self.setLayout(layout)

Блок __init__ запускается только при создании окна. Если продолжить нажимать кнопку, число будет меняться, а окно — создаваться заново.

Решение: прежде чем создавать окно, просто проверить, не было ли оно уже создано. Покажем это на примере:

from PyQt6.QtWidgets import QApplication, QMainWindow, QPushButton, QLabel, QVBoxLayout, QWidget

import sys

from random import randint


class AnotherWindow(QWidget):
    """
    This "window" is a QWidget. If it has no parent, it
    will appear as a free-floating window as we want.
    """
    def __init__(self):
        super().__init__()
        layout = QVBoxLayout()
        self.label = QLabel("Another Window % d" % randint(0,100))
        layout.addWidget(self.label)
        self.setLayout(layout)


class MainWindow(QMainWindow):

    def __init__(self):
        super().__init__()
        self.w = None  # No external window yet.
        self.button = QPushButton("Push for Window")
        self.button.clicked.connect(self.show_new_window)
        self.setCentralWidget(self.button)

    def show_new_window(self, checked):
        if self.w is None:
            self.w = AnotherWindow()
        self.w.show()


app = QApplication(sys.argv)
w = MainWindow()
w.show()
app.exec()
Дочернее окно со случайно сгенерированной при создании меткой
Дочернее окно со случайно сгенерированной при создании меткой

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

Это отличный подход для временно создаваемых окон. Например, если нужно вызвать окно, чтобы показать график или вывод логов. Но во многих приложениях есть ещё и стандартные окна, которые нужно при необходимости показать и/или скрыть.

Переключение окна

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

    def show_new_window(self, checked):
        if self.w is None:
            self.w = AnotherWindow()
            self.w.show()

        else:
            self.w = None  # Discard reference, close wi

Установив для self.w значение None, мы теряем ссылку на окно, и оно закрывается.

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

Но это только если ссылка на это окно не сохранена где-то ещё. Чтобы убедиться, что окно так или иначе закрывается, явно вызовем в нём функцию .close(). Вот полный пример:

from PyQt6.QtWidgets import QApplication, QMainWindow, QPushButton, QLabel, QVBoxLayout, QWidget

import sys

from random import randint


class AnotherWindow(QWidget):
    """
    This "window" is a QWidget. If it has no parent, it
    will appear as a free-floating window as we want.
    """
    def __init__(self):
        super().__init__()
        layout = QVBoxLayout()
        self.label = QLabel("Another Window % d" % randint(0,100))
        layout.addWidget(self.label)
        self.setLayout(layout)


class MainWindow(QMainWindow):

    def __init__(self):
        super().__init__()
        self.w = None  # No external window yet.
        self.button = QPushButton("Push for Window")
        self.button.clicked.connect(self.show_new_window)
        self.setCentralWidget(self.button)

    def show_new_window(self, checked):
        if self.w is None:
            self.w = AnotherWindow()
            self.w.show()

        else:
            self.w.close()  # Close window.
            self.w = None  # Discard reference.


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

Создавайте приложения с графическим интерфейсом с помощью Python и Qt5 Простой способ создания настольных приложений

Постоянные окна

Мы рассмотрели, как создавать новые окна, когда их надо показать. Но есть и стандартные окна приложений. В этом случае обычно лучше создавать окна при запуске и, когда нужно, показывать их с помощью .show().

В следующем примере для главного окна в блоке __init__ создаётся внешнее окно, а затем показывается посредством простого вызова self.w.show() с помощью метода show_new_window:

from PyQt6.QtWidgets import QApplication, QMainWindow, QPushButton, QLabel, QVBoxLayout, QWidget

import sys

from random import randint


class AnotherWindow(QWidget):
    """
    This "window" is a QWidget. If it has no parent, it
    will appear as a free-floating window as we want.
    """
    def __init__(self):
        super().__init__()
        layout = QVBoxLayout()
        self.label = QLabel("Another Window % d" % randint(0,100))
        layout.addWidget(self.label)
        self.setLayout(layout)


class MainWindow(QMainWindow):

    def __init__(self):
        super().__init__()
        self.w = AnotherWindow()
        self.button = QPushButton("Push for Window")
        self.button.clicked.connect(self.show_new_window)
        self.setCentralWidget(self.button)

    def show_new_window(self, checked):
        self.w.show()


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

Если запустить код, при нажатии кнопки снова откроется окно. Обратите внимание: окно создаётся только раз, и вызов .show() в уже видимом окне не даст никакого эффекта.

Отображение и скрытие постоянных окон

Создав постоянное окно, его можно показать и/или скрыть, не создавая заново. Скрытое окно продолжает существовать: будучи невидимым, оно продолжит принимать ввод с помощью мыши и/или другим способом. Более того, в окне можно вызывать методы и обновлять его состояние, в том числе менять внешний вид окна. После повторного показа будут видны любые изменения.

Обновим главное окно и создадим метод .toggle_window, которым с помощью .isVisible() проверяется, видно ли сейчас окно. Если нет, окно показывается с помощью .show(), а если уже видно, то используем .hide(), чтобы скрыть его:

class MainWindow(QMainWindow):

    def __init__(self):
        super().__init__()
        self.w = AnotherWindow()
        self.button = QPushButton("Push for Window")
        self.button.clicked.connect(self.toggle_window)
        self.setCentralWidget(self.button)

    def toggle_window(self, checked):
        if self.w.isVisible():
            self.w.hide()

        else:
            self.w.show()

Вот полный рабочий пример с постоянным окном и переключением состояния «показать/скрыть»:

from PyQt6.QtWidgets import QApplication, QMainWindow, QPushButton, QLabel, QVBoxLayout, QWidget

import sys

from random import randint


class AnotherWindow(QWidget):
    """
    This "window" is a QWidget. If it has no parent, it
    will appear as a free-floating window as we want.
    """
    def __init__(self):
        super().__init__()
        layout = QVBoxLayout()
        self.label = QLabel("Another Window % d" % randint(0,100))
        layout.addWidget(self.label)
        self.setLayout(layout)


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.w = AnotherWindow()
        self.button = QPushButton("Push for Window")
        self.button.clicked.connect(self.toggle_window)
        self.setCentralWidget(self.button)

    def toggle_window(self, checked):
        if self.w.isVisible():
            self.w.hide()

        else:
            self.w.show()


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

Напоминаем: окно создаётся только один раз и блок окна __init__ не запускается повторно (поэтому число в метке не меняется) всякий раз, когда повторно показывается окно.

Несколько окон

Тот же принцип применим для создания нескольких окон: пока ссылка на окно сохраняется, всё будет, как ожидается. Самый простой подход — создать отдельный метод для переключения показа каждого окна:

import sys
from random import randint

from PyQt6.QtWidgets import (
    QApplication,
    QLabel,
    QMainWindow,
    QPushButton,
    QVBoxLayout,
    QWidget,
)


class AnotherWindow(QWidget):
    """
    This "window" is a QWidget. If it has no parent,
    it will appear as a free-floating window.
    """

    def __init__(self):
        super().__init__()
        layout = QVBoxLayout()
        self.label = QLabel("Another Window % d" % randint(0, 100))
        layout.addWidget(self.label)
        self.setLayout(layout)


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.window1 = AnotherWindow()
        self.window2 = AnotherWindow()

        l = QVBoxLayout()
        button1 = QPushButton("Push for Window 1")
        button1.clicked.connect(self.toggle_window1)
        l.addWidget(button1)

        button2 = QPushButton("Push for Window 2")
        button2.clicked.connect(self.toggle_window2)
        l.addWidget(button2)

        w = QWidget()
        w.setLayout(l)
        self.setCentralWidget(w)

    def toggle_window1(self, checked):
        if self.window1.isVisible():
            self.window1.hide()

        else:
            self.window1.show()

    def toggle_window2(self, checked):
        if self.window2.isVisible():
            self.window2.hide()

        else:
            self.window2.show()


app = QApplication(sys.argv)
w = MainWindow()
w.show()
app.exec()
Главное окно с двумя дочерними
Главное окно с двумя дочерними

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

Это показано и в примере ниже: для перехвата сигнала от каждой кнопки и передачи через соответствующее окно используется лямбда-функция. Значение checked можно отбросить, ведь оно не используется:

import sys
from random import randint

from PyQt6.QtWidgets import (
    QApplication,
    QLabel,
    QMainWindow,
    QPushButton,
    QVBoxLayout,
    QWidget,
)


class AnotherWindow(QWidget):
    """
    This "window" is a QWidget. If it has no parent,
    it will appear as a free-floating window.
    """

    def __init__(self):
        super().__init__()
        layout = QVBoxLayout()
        self.label = QLabel("Another Window % d" % randint(0, 100))
        layout.addWidget(self.label)
        self.setLayout(layout)


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.window1 = AnotherWindow()
        self.window2 = AnotherWindow()

        l = QVBoxLayout()
        button1 = QPushButton("Push for Window 1")
        button1.clicked.connect(
            lambda checked: self.toggle_window(self.window1)
        )
        l.addWidget(button1)

        button2 = QPushButton("Push for Window 2")
        button2.clicked.connect(
            lambda checked: self.toggle_window(self.window2)
        )
        l.addWidget(button2)

        w = QWidget()
        w.setLayout(l)
        self.setCentralWidget(w)

    def toggle_window(self, window):
        if window.isVisible():
            window.hide()

        else:
            window.show()


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

А научиться писать приложения на Python вы сможете по книге автора или на наших курсах, где есть другие преимущества:

Выбрать другую востребованную специальность.

Профессии и курсы

Комментарии (9)


  1. Monsterovich
    10.02.2022 02:32

    В pyqt нельзя импортировать .ui файлы из исходника напрямую, приходится юзать moc программу, чтобы из каждого .ui файла сгенерировать .py исходник (либо фигачить гуй руками, но это уже другой подход). Даже в с++ это автоматически при сборке делается, а в динамическом питоне не смогли сделать проще.

    P.S. Лучше бы Qt Company сделали Qt для Java или С# чисто ради увеличения популярности фреймворка.


    1. CrinitusFeles
      10.02.2022 08:15

      Вполне можно импортировать из исходников с помощью модуля uic. Хоть PyCharm почему-то и не распознает этот модуль, но тем не менее все равно отлично работает.

      import os
      from PyQt5 import uic
      from PyQt5.QtWidgets import QApplication, QMainWindow

      class MainWindow(QMainWindow):
      def init(self, **kwargs):
      super().init()
      uic.loadUi(os.path.join(os.path.dirname(file), 'your_ui_file.ui'), self)


  1. Terranz
    10.02.2022 03:26

    В 2022 набивать ui руками?


    1. Monsterovich
      10.02.2022 11:08

      Именно так, особенно когда юзаешь какой-нибудь wxWidgets, GTK или даже наверное GDI, но там есть макеты нормальные как в Qt, просто GDI уже устарел морально.


    1. HemulGM
      10.02.2022 11:27

      Скажи это рекатистам, электронистам и прочим веб-разработчикам, которые возможно, даже не знают, что можно всё делать проще, быстрее и нагляднее


  1. freedbrt
    10.02.2022 11:46

    В Qt6 виджеты если не ошибаюсь deprecated, есть ли смысл на них сейчас писать?


    1. Sazonov
      10.02.2022 12:50

      Ошибаетесь. Виджеты не депрекейтнут пока не появится полноценной альтернативы для десктопов. QtQuick для этого пока слабоват. Поэтому многие современные приложения для десктопов это микс из виджетов и qtquick.


      1. freedbrt
        10.02.2022 13:06

        А почему слабоват? Насколько я знаю в QtQuick давно все есть для дестопа, тот же MainWindow, диалоги, меню, все уже есть.


        1. Sazonov
          10.02.2022 13:21

          Возможно что-то поменялось, но когда я последний раз смотрел, то не было внятных решений для dock панелей и мультиоконных приложений. Приходилось очень много колдовать.