[Перевод] PyQt6 — полное руководство для новичков. Продолжение

image-loader.svg

В первом материале мы рассказали о создании первого окна, о сигналах, слотах и событиях, а также о виджетах. Сегодня, к старту курса по 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 в режиме документа на macOSQTabWidget в режиме документа на 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. В нашем случае п

© Habrahabr.ru