{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# PySide. Основы\n", "\n", "## Установка `PySide`\n", "\n", "Установить `PySide6` можно используя `pip`\n", "\n", "```sh\n", "pip install PySide6\n", "```\n", "\n", "## Репозиторий \n", "\n", "Здесь и везде далее по теме `PySide` будет приводиться исходный код, эффект запуска которого не получится в полной мере продемонстрировать в рамках статического примера. Чтобы лучше почувствовать возможности `Qt`, лучше самостоятельно запускать приводимые примеры на своей машине. Все необходимое для этого можно найти в репозитории по [ссылке](https://github.com/FadeevLecturer/PySide6_intro), клонировать который можно командой (потребуется установленный [git](https://git-scm.com/))\n", "\n", "```sh\n", "git clone git@github.com:FadeevLecturer/PySide6_intro.git\n", "```\n", "\n", "Все зависимости, необходимые для запуска всех примеров, перечисленны в файле [requirements.txt](https://github.com/NeuroFuzzyLab/possibility_aggregation/blob/main/requirements.txt).\n", "\n", "Установить все необходимые библиотеки в пустое виртуальное окружение можно командой\n", "\n", "```sh\n", "python -m pip install -r requirements.txt\n", "```\n", "\n", "## Пространство имен `PySide`\n", "\n", "В `PySide` можно выделить три основных модуля.\n", "\n", "1. [QtCore](https://doc.qt.io/qtforpython/PySide6/QtCore/index.html#module-PySide6.QtCore) --- ядро библиотеки, предоставляет доступ к ключевому функционалу, который не завязан на графическом интерфейсе: сигналы, слоты, базовые классы и др.\n", "2. [QtGui](https://doc.qt.io/qtforpython/PySide6/QtGui/index.html#module-PySide6.QtGui) --- расширяет ядро некоторыми элементами `gui`: события, экраны, окна и др.\n", "3. [QtWidgets](https://doc.qt.io/qtforpython/PySide6/QtWidgets/index.html#module-PySide6.QtWidgets) --- содержит в себе набор готовых к использованию элементов интерфейса (виджетов): кнопки и многое другое.\n", "\n", "```{note}\n", "С полным списком модулей можно ознакомиться [здесь](https://doc.qt.io/qtforpython/modules.html).\n", "```\n", "\n", "Часто в приложении используются десятки имен из модулей `PySide`. В связи с этим нередко встречается `wildcard` импорты следующего вида: \n", "\n", "```python\n", "from PySide6.QtCore import *\n", "from PySide6.QtGui import *\n", "from PySide6.QtWidgets import *\n", "```\n", "\n", "В данном случае это не так страшно, т.к. все имена в модулях `PySide` имеют специфическую форму и не перекрываются с именами встроенной библиотеки. В крупных программах принято реализовывать графический интерфейс и остальную логику программы в разных местах, что ещё в большей степени нивелирует проблему \"запутанности\" пространства имен. \n", "\n", "## Qt приложение. `QApplication`\n", "\n", "Чтобы разрабатывать приложение с графическим интерфейсом на основе `Qt` необходимо в первую очередь создать объект типа [QApplication](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QApplication.html). При этом такой объект **должен быть ровно один** вне зависимости от обстоятельств, количества окон у приложения и т.п. `QApplication` делает очень много закулисной работы по обработке событий и организации виджетов. \n", "\n", "Самое примитивное приложение на `PySide` без каких либо окон выглядит примерно так.\n", "```python\n", "import sys\n", "from PySide6.QtWidgets import QApplication\n", "\n", "\n", "app = QApplication(sys.argv)\n", "app.exec()\n", "print(\"Application is closed!\")\n", "```\n", "\n", "Команда `app.exec()` запускает [цикл событий](https://ru.wikipedia.org/wiki/%D0%A6%D0%B8%D0%BA%D0%BB_%D1%81%D0%BE%D0%B1%D1%8B%D1%82%D0%B8%D0%B9) ([event loop](https://en.wikipedia.org/wiki/Event_loop)), который ожидает [события](https://ru.wikipedia.org/wiki/%D0%A1%D0%BE%D0%B1%D1%8B%D1%82%D0%B8%D0%B9%D0%BD%D0%BE-%D0%BE%D1%80%D0%B8%D0%B5%D0%BD%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%BD%D0%BE%D0%B5_%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5) ([events](https://en.wikipedia.org/wiki/Event-driven_programming)) и вызывает соответствующий обработчик события (`event handler`). Цикл события из себя представляет бесконечный цикл типа `while`, выход из которого осуществляется при возникновении события `exit`. \n", "\n", "Важно понимать, что когда `Qt` приложение запущено (`app.exec()`), программа попадает в событийный цикл, из которого она не выйдет, пока приложение не будет закрыто. Иными словами исполнение кода в скрипте `python` приостанавливается, управление потоком исполнения программы передаётся `Qt`. Так, инструкция `print` в скрипте выше не выполнится до тех пор, пока не будет вызван метод `app.exit()`, к которому мы не предоставили прямого доступа.\n", "\n", "Итого, в примере выше цикл событий будет крутиться в пустую, так как мы не обрабатываем ни одного события. Создадим окно у приложения, чтобы начать обрабатывать хоть какие-то события.\n", "\n", "## Главное окно приложения. `QMainWindow`\n", "\n", "`QApplication` самого по себе недостаточно, чтобы на экране появился графический интерфейс. Для этого необходимо создать какой-нибудь элемент интерфейса (`widget`) и вывести его на экран, т.к. по умолчанию они скрыты. Для главного окна приложения удобнее всего использовать виджет [QMainWindow](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QMainWindow.html).\n", "\n", "Следующий пример расширяет предыдущий, добавляя создание экземпляр класса `QMainWindow` и выводя его на экран методом [show](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QWidget.html#PySide6.QtWidgets.PySide6.QtWidgets.QWidget.show) (по умолчанию все виджеты скрыты). \n", "\n", "```python\n", "import sys\n", "from PySide6.QtWidgets import QApplication, QMainWindow\n", "\n", "\n", "app = QApplication(sys.argv)\n", "main_window = QMainWindow()\n", "main_window.show()\n", "app.exec()\n", "print(\"Application is closed!\")\n", "```\n", "\n", "При запуске этого приложения должно появиться окно, которое в зависимости от операционной системы может выглядеть следующим образом.\n", "```{figure} ../_static/lecture_specific/qt/main_window_1.png\n", "```\n", "\n", "```{note}\n", "У метода `show` есть аналоги \n", "[showFullScreen](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QWidget.html#PySide6.QtWidgets.PySide6.QtWidgets.QWidget.showFullScreen), \n", "[showNormal](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QWidget.html#PySide6.QtWidgets.PySide6.QtWidgets.QWidget.showNormal), \n", "[showMaximized](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QWidget.html#PySide6.QtWidgets.PySide6.QtWidgets.QWidget.showMaximized) и \n", "[showMinimized](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QWidget.html#PySide6.QtWidgets.PySide6.QtWidgets.QWidget.showMinimized).\n", "```\n", "\n", "Это окно уже можно\n", "- перемещать;\n", "- менять его размер, растягивая или сжимая его, а также растягивая его на весь экран;\n", "- сворачивать и разворачивать обратно;\n", "- закрывать его.\n", "\n", "Т.е. `Qt` сам сгенерировал ряд элементов графического интерфейса, а также реализовал обработку ряда событий (например, нажатие на кнопку закрытия приложения). \n", " \n", "При этом только после закрытия этого окна программа покинет цикл событий, сработает инструкция `print` и в консоли появится сообщение \"Application is closed!\".\n", "\n", "## Кастомизация `QMainWindow`\n", "\n", "В предыдущем примере окно было создано с дефолтным размером, дефолтной иконкой и дефолтным заголовком \"python\", который даже полностью не влезает из-за размеров окна. Все это можно настроить под свои нужды. \n", "\n", "- Размер окна можно изменить методом [setGeometry](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QWidget.html#PySide6.QtWidgets.PySide6.QtWidgets.QWidget.setGeometry), который на вход принимает четыре параметра: координаты `x` и `y` левого верхнего угла окна, ширину `w` и высоту `h` окна. Ось $Ox$ и $Oy$ направлены слева направо и сверху вниз соответственно.\n", "```{figure} ../_static/lecture_specific/qt/geometry.png\n", "```\n", "\n", "- Заголовок окна можно изменить методом [setWindowTitle](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QWidget.html#PySide6.QtWidgets.PySide6.QtWidgets.QWidget.setWindowTitle), который на вход принимает строку.\n", "\n", "- Иконку окна можно изменить с помощью метода [setWindowIcon](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QWidget.html#PySide6.QtWidgets.PySide6.QtWidgets.QWidget.setWindowIcon), которая на вход принимает объект [QtGui.QIcon](https://doc.qt.io/qtforpython/PySide6/QtGui/QIcon.html).\n", "Конструктор `QIcon` в свою очередь на вход принимает изображение или строку, содержащую путь к файлу с иконкой. \n", "\n", "Изменим размер, заголовок и иконку окна из предыдущего примера. Можно было бы вызывать все перечисленные методы у уже существующего окна, но гораздо нагляднее будет унаследовать от класса `QMainWindow` и модифицировать окно в момент инициализации.\n", "\n", "```python\n", "import sys\n", "import os\n", "\n", "from PySide6.QtWidgets import QApplication, QMainWindow\n", "from PySide6.QtGui import QIcon\n", "\n", "\n", "path_to_the_icon = os.path.join(\"..\", \"icons\", \"phys.png\")\n", "\n", "\n", "class MainWindow(QMainWindow):\n", " def __init__(self):\n", " super().__init__()\n", "\n", " self.setGeometry(100, 100, 300, 200)\n", " self.setWindowTitle(\"My Qt Application\")\n", " self.setWindowIcon(QIcon(path_to_the_icon))\n", "\n", "\n", "app = QApplication(sys.argv)\n", "main_window = MainWindow()\n", "main_window.show()\n", "app.exec()\n", "```\n", "\n", "Разберем этот пример детальнее, так как модификацию `Qt` виджетов часто удобнее всего делать именно так.\n", "\n", "```python\n", "class MainWindow(QMainWindow):\n", " def __init__(self):\n", " super().__init__()\n", "```\n", "Мы собираемся модифицировать класс `QMainWindow`, поэтому от него и наследуемся. В методе `__init__` в первую очередь делегируем инициализацию окна базовому классу.\n", "\n", "```python\n", "self.setGeometry(100, 100, 300, 200)\n", "self.setWindowTitle(\"My Qt Application\")\n", "self.setWindowIcon(QIcon(path_to_the_icon))\n", "```\n", "Затем настраиваем положение и размер окна, его заголовок и путь к окну.\n", "\n", "```{note}\n", "Для простоты в примере создаётся окно в абсолютных единицах, но можно было бы настроить геометрию окна под разрешение экрана. Метод [primaryScreen](https://doc.qt.io/qtforpython/PySide6/QtGui/QGuiApplication.html#PySide6.QtGui.PySide6.QtGui.QGuiApplication.primaryScreen) объекта `QApplication` возвращает объект [QtGui.QScreen](https://doc.qt.io/qtforpython/PySide6/QtGui/QScreen.html), который позволяет получить информацию о главном экране (ситуация несколько усложняется, если экранов несколько), в том числе и геометрию экрана методом [availableGeometry](https://doc.qt.io/qtforpython/PySide6/QtGui/QScreen.html#PySide6.QtGui.PySide6.QtGui.QScreen.availableGeometry).\n", "```\n", "```python\n", "main_window = MainWindow()\n", "```\n", "Ну и создаем мы теперь экземпляр нового класса `MainWindow`, а не исходного `QMainWindow`.\n", "\n", "В итоге должны получить нечто следующее.\n", "```{figure} ../_static/lecture_specific/qt/main_window_2.png\n", "```\n", " \n", "## Виджет `QLabel`\n", "\n", "Чтобы внутри окна появились элементы графического управления необходимо добавить виджеты. Все виджеты располагаются в модуле `QtWidgets`, в том числе и один из самых простых --- [QLabel](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QLabel.html), который позволяет отображать текст (или изображение, но об этом в следующем разделе).\n", "\n", "Любой виджет принимает ссылку на родительский виджет `parent`. `QLabel` на вход принимает ещё и `python` строку. Метод [setALignment](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QLabel.html#PySide6.QtWidgets.PySide6.QtWidgets.QLabel.setAlignment) ожидает на вход [Qt.Core.AlignmentFlag](https://doc.qt.io/qtforpython/PySide6/QtCore/Qt.html#PySide6.QtCore.PySide6.QtCore.Qt.AlignmentFlag) и отвечает за выравнивание текста по центру (`QtCore.Qt.AlignCenter`), по левому краю (`QtCore.Qt.AlignRight`) и т.п.\n", "\n", "Чтобы добавить виджет на окно, мало просто указать `parent` при его создании. Необходимо также сказать самому окну этот виджет отрисовать. Пока у нас всего один виджет можно воспользоваться методом [QMainWindow.setCentralWidget](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QMainWindow.html#PySide6.QtWidgets.PySide6.QtWidgets.QMainWindow.setCentralWidget), чтобы сразу добавить наш текст в центр окна.\n", "\n", "```python\n", "import sys\n", "import os\n", "\n", "from PySide6.QtWidgets import QApplication, QMainWindow, QLabel\n", "from PySide6.QtGui import QIcon, Qt\n", "\n", "\n", "path_to_the_icon = os.path.join(\"..\", \"icons\", \"../python.png\")\n", "\n", "\n", "class MainWindow(QMainWindow):\n", " def __init__(self):\n", " super().__init__()\n", "\n", " self.setGeometry(100, 100, 300, 150)\n", " self.setWindowTitle(\"My Qt Application\")\n", " self.setWindowIcon(QIcon(path_to_the_icon))\n", "\n", " self.label = QLabel(\"Hello, World!\", parent=self)\n", " self.label.setAlignment(Qt.AlignCenter)\n", " self.setCentralWidget(self.label)\n", "\n", "app = QApplication(sys.argv)\n", "main_window = MainWindow()\n", "main_window.show()\n", "app.exec()\n", "```\n", "\n", "```{figure} ../_static/lecture_specific/qt/label_1.png\n", "```\n", "## Макеты. `Layout`\n", "\n", "Добавить несколько виджетов на окно проще всего используя макеты (`layout`). \n", "\n", "Вообще говоря, можно расположить виджеты на окне, указав окно в качестве родителя (`parent`) и вызвав метод `setGeometry`. При таком подходе можно указывать произвольное положение кнопок, но нет никакой гарантии, что с изменением геометрии самого окна элементы будут адекватно перестраиваться.\n", "\n", "`Qt` предоставляет набор стандартных макетов, которые сами следят за эффективным использованием пространства, занимаемого элементами интерфейса. Чтобы установить макет, необходимо вызвать метод [setLayout](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QWidget.html#PySide6.QtWidgets.PySide6.QtWidgets.QWidget.setLayout) у какого-нибудь разумного виджета (), но у [QMainWindow](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QWidget.html#PySide6.QtWidgets.PySide6.QtWidgets.QWidget.setLayout)\n", "\n", "\n", "\n", "Чтобы добавить несколько виджетов на окно, необходимо:\n", "- установить в качестве центрального виджет [QWidget](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QWidget.html), т.к. у `QMainWindow` всегда должен быть центральный виджет;\n", "- расположить внутри центрального виджета все остальные, используя макеты (`layouts`).\n", "\n", "```{note}\n", "Подробнее о макетах можно почитать [здесь](https://doc.qt.io/qtforpython/overviews/layout.html#layout-management).\n", "```\n", "\n", "### `QHBoxLayout` и `QVBoxLayout`\n", "\n", "Макет [QHBoxLayout](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QHBoxLayout.html) выравнивает виджеты горизонтально слева направо , а [QVBoxLayout](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QVBoxLayout.html) вертикально сверху вниз. У обоих из них есть среди прочих следующие методы:\n", "\n", "- [addWidget](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QBoxLayout.html#PySide6.QtWidgets.PySide6.QtWidgets.QBoxLayout.addWidget) --- добавляет виджет. В качестве параметра `alignment` можно указать выравнивание, как и `QLabel`;\n", "- [addSpacing](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QBoxLayout.html#PySide6.QtWidgets.PySide6.QtWidgets.QBoxLayout.addSpacing) --- добавляет пустой заполнитель заданного размера;\n", "- [addLayout](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QBoxLayout.html#PySide6.QtWidgets.PySide6.QtWidgets.QBoxLayout.addLayout) --- добавляет подмакет. Например, можно встроить `QHBoxLayout` в `QVBoxLayout`. \n", "\n", "Макет сам распределяет пространство, которое будет занимать виджет исходя из геометрии окна, политики размера каждого из виджетов ([QSizePolicy](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QSizePolicy.html), который среди прочего определяем минимальный размер виджета) и коэффициента растяжения (`stretching`), который определяет насколько жадно этот виджет будет занимать пространство. По умолчанию под каждый виджет отводится одинаковое количество места, но если указать параметр `stretch` (его принимают все три вышеописанных метода), то виджета с большим значением будет занимать больше пространства.\n", "\n", "В качестве примера, создадим два виджета `QLabel`, положим в них картинку методом [setPixmap](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QLabel.html#PySide6.QtWidgets.PySide6.QtWidgets.QLabel.setPixmap), который ожидает на вход объект [QPixmap](https://doc.qt.io/qtforpython/PySide6/QtGui/QPixmap.html), и добавим их в макет.\n", "\n", "````{tabbed} QHBoxLayout\n", "```python\n", "import sys\n", "import os\n", "\n", "from PySide6.QtWidgets import QApplication, QMainWindow, QLabel, QWidget, QHBoxLayout\n", "from PySide6.QtGui import QIcon, Qt, QPixmap\n", "\n", "\n", "path_to_the_icon = os.path.join(\"..\", \"icons\", \"phys.png\")\n", "path_to_py_logo = os.path.join(\"..\", \"icons\", \"python.png\")\n", "path_to_qt_logo = os.path.join(\"..\", \"icons\", \"qt.png\")\n", "\n", "\n", "class MainWindow(QMainWindow):\n", " def __init__(self):\n", " super().__init__()\n", "\n", " self.setGeometry(100, 100, 300, 150)\n", " self.setWindowTitle(\"My Qt Application\")\n", " self.setWindowIcon(QIcon(path_to_the_icon))\n", "\n", " widget = QWidget(parent=self)\n", " self.setCentralWidget(widget)\n", "\n", " python_logo = QPixmap(path_to_py_logo)\n", " python_logo_label = QLabel()\n", " python_logo_label.setPixmap(python_logo)\n", "\n", " qt_logo = QPixmap(path_to_qt_logo)\n", " qt_logo_label = QLabel()\n", " qt_logo_label.setPixmap(qt_logo)\n", "\n", " layout = QHBoxLayout()\n", " layout.addWidget(python_logo_label, alignment=Qt.AlignCenter)\n", " layout.addWidget(qt_logo_label, alignment=Qt.AlignCenter)\n", " widget.setLayout(layout)\n", "\n", "\n", "app = QApplication(sys.argv)\n", "main_window = MainWindow()\n", "main_window.show()\n", "app.exec()\n", "```\n", "```{figure} ../_static/lecture_specific/qt/HBoxLayout.png\n", "```\n", "\n", "````\n", "````{tabbed} QVBoxLayout\n", "```python\n", "import sys\n", "import os\n", "\n", "from PySide6.QtWidgets import QApplication, QMainWindow, QLabel, QWidget, QVBoxLayout\n", "from PySide6.QtGui import QIcon, Qt, QPixmap\n", "\n", "\n", "path_to_the_icon = os.path.join(\"..\", \"icons\", \"phys.png\")\n", "path_to_py_logo = os.path.join(\"..\", \"icons\", \"python.png\")\n", "path_to_qt_logo = os.path.join(\"..\", \"icons\", \"qt.png\")\n", "\n", "\n", "class MainWindow(QMainWindow):\n", " def __init__(self):\n", " super().__init__()\n", "\n", " self.setGeometry(100, 100, 300, 150)\n", " self.setWindowTitle(\"My Qt Application\")\n", " self.setWindowIcon(QIcon(path_to_the_icon))\n", "\n", " widget = QWidget(parent=self)\n", " self.setCentralWidget(widget)\n", "\n", " python_logo = QPixmap(path_to_py_logo)\n", " python_logo_label = QLabel()\n", " python_logo_label.setPixmap(python_logo)\n", "\n", " qt_logo = QPixmap(path_to_qt_logo)\n", " qt_logo_label = QLabel()\n", " qt_logo_label.setPixmap(qt_logo)\n", "\n", " layout = QVBoxLayout()\n", " layout.addWidget(python_logo_label, alignment=Qt.AlignCenter)\n", " layout.addWidget(qt_logo_label, alignment=Qt.AlignCenter)\n", " widget.setLayout(layout)\n", "\n", "\n", "app = QApplication(sys.argv)\n", "main_window = MainWindow()\n", "main_window.show()\n", "app.exec()\n", "```\n", "```{figure} ../_static/lecture_specific/qt/VBoxLayout.png\n", "```\n", "````\n", "\n" ] } ], "metadata": { "language_info": { "name": "python" }, "orig_nbformat": 4 }, "nbformat": 4, "nbformat_minor": 2 }