{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Диалоговые окна\n", "\n", "Диалоговые окна чаще всего используются для кратковременного взаимодействия с пользователем. Все они наследуются от класса [QDialog](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QDialog.html). Диалоговое окно появляется поверх родительского (посередине родительского, если указан параметр `parent`), при этом обычно родительское окно становится не активных до тех пор, пока пользователь не закрое диалоговое окно.\n", "\n", "Создавать диалоговые окна можно было бы и вручную (например, наследуясь от `QDialog` и модифицируя его поведение под свои цели), но для большинства ситуаций в `Qt` заготовлены удобные шаблонные варианты.\n", "\n", "## Отображение сообщений. `QMessageBox`\n", "\n", "Простейшее назначение, ради которого может применяться диалоговое окно --- вывести некоторое сообщение пользователю. В таких случаях удобнее всего использовать [QMessageBox](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QMessageBox.html#qmessagebox). \n", "\n", "Есть два подхода использовать класс `QMessageBox`.\n", "1. Создать его экземпляр и вызвать его метод `exec`;\n", "2. Использовать статический метод (например, [QMessageBox.information](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QMessageBox.html#PySide6.QtWidgets.PySide6.QtWidgets.QMessageBox.information)).\n", "\n", "Список статических методов, главная разница между которыми --- иконка, следующий:\n", "- [about](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QMessageBox.html#PySide6.QtWidgets.PySide6.QtWidgets.QMessageBox.about) --- сообщение о приложении, иконка родительского виджета по умолчанию;\n", "- ![image](../_static/lecture_specific/qt/qmessagebox-crit.png) [critical](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QMessageBox.html#PySide6.QtWidgets.PySide6.QtWidgets.QMessageBox.critical) --- сообщение о критической ошибке;\n", "- ![image](../_static/lecture_specific/qt/qmessagebox-info.png) [information](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QMessageBox.html#PySide6.QtWidgets.PySide6.QtWidgets.QMessageBox.information) --- информационное сообщение;\n", "- ![image](../_static/lecture_specific/qt/qmessagebox-quest.png) [question](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QMessageBox.html#PySide6.QtWidgets.PySide6.QtWidgets.QMessageBox.question) --- вопрос пользователю;\n", "- ![image](../_static/lecture_specific/qt/qmessagebox-warn.png) [warning](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QMessageBox.html#PySide6.QtWidgets.PySide6.QtWidgets.QMessageBox.warning) --- предупреждение (например, сообщение о некритической ошибке).\n", "\n", "При ручном создании экземпляра иконку можно выставить методом [setIcon](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QMessageBox.html#PySide6.QtWidgets.PySide6.QtWidgets.QMessageBox.setIcon), текст сообщения --- [setText](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QMessageBox.html#PySide6.QtWidgets.PySide6.QtWidgets.QMessageBox.setText), заголовок диалогового окна --- [setWindowTitle](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QMessageBox.html#PySide6.QtWidgets.PySide6.QtWidgets.QMessageBox.setWindowTitle). Статические методы принимают эти значения в качестве параметров, а иконка определяется типом статического метода.\n", "\n", "Этих методов в целом достаточно, чтобы создать полноценное информационное сообщение. Код под спойлером ниже создаёт приложение, которое выводит диалоговое сообщение методом `QMessageBox.information` при нажатии на кнопку.\n", "\n", "```{figure} ../_static/lecture_specific/qt/message_box_1.png\n", "```\n", "\n", "
\n", "Код.\n", "\n", "```python\n", "import sys\n", "\n", "from PySide6.QtWidgets import *\n", "\n", "\n", "class MainWindow(QMainWindow):\n", " def __init__(self):\n", " super().__init__()\n", " widget = QWidget()\n", " layout = QHBoxLayout()\n", " widget.setLayout(layout)\n", " self.setCentralWidget(widget)\n", "\n", " # buttons\n", " push_button = QPushButton(\"Show message\")\n", " push_button.clicked.connect(self.show_message)\n", " layout.addWidget(push_button)\n", "\n", " def show_message(self):\n", " QMessageBox.information(\n", " self,\n", " \"My first dialog\",\n", " \"Hello, World!\"\n", " )\n", "\n", "app = QApplication(sys.argv)\n", "main_window = MainWindow()\n", "main_window.show()\n", "app.exec()\n", "```\n", "
\n", "\n", "Заметим, что в прошлом примере в окне была единственная кнопка `Ok`. Для информационных сообщений такого функционала может быть достаточно, но, например, для вопросов может потребоваться несколько кнопок, чтобы пользователь имел несколько опций ответа. В качестве иллюстрации таких диалоговых окон можно привести сообщения, которые появляются при попытке закрыть программу с несохраненными изменениями файла: большинство программ в таких случаях не закрываются сразу, а спрашивают пользователя, хочет ли он \n", "- закрыть программу без сохранения изменений, \n", "- сохранить изменения и выйти, \n", "- отменить закрытие программы и продолжить работу.\n", "\n", "Виджет `QMessageBox` позволяет без труда добавлять в диалоговое окно ряд стандартных кнопок, Для этого используется перечисление [QMessageBox.StandardButton](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QMessageBox.html#PySide6.QtWidgets.PySide6.QtWidgets.QMessageBox.StandardButton), у которого возможны следующие значения:\n", "- `QMessageBox.Ok`\n", "- `QMessageBox.Open`\n", "- `QMessageBox.Save`\n", "- `QMessageBox.Cancel`\n", "- `QMessageBox.Close`\n", "- `QMessageBox.Discard`\n", "- `QMessageBox.Apply`\n", "- `QMessageBox.Reset`\n", "- `QMessageBox.RestoreDefaults`\n", "- `QMessageBox.Help`\n", "- `QMessageBox.SaveAll`\n", "- `QMessageBox.Yes`\n", "- `QMessageBox.YesToAll`\n", "- `QMessageBox.No`\n", "- `QMessageBox.NoToAll`\n", "- `QMessageBox.Abort`\n", "- `QMessageBox.Retry`\n", "- `QMessageBox.Ignore`\n", "\n", "Выставить их можно передав значения этого перечисления (объединить несколько кнопок можно знаком логического \"или\" `|`) в качестве параметра конструктора или одного из упомянутых методов или используя метод [setStandardButtons](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QMessageBox.html#PySide6.QtWidgets.PySide6.QtWidgets.QMessageBox.setStandardButtons) у созданного экземпляра класса `QMessageBox`. Методами [setDefaultButton](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QMessageBox.html#PySide6.QtWidgets.PySide6.QtWidgets.QMessageBox.setDefaultButton) и [setEscapeButton](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QMessageBox.html#PySide6.QtWidgets.PySide6.QtWidgets.QMessageBox.setEscapeButton) можно выставить кнопки, которые будут считаться при нажатии клавиш `enter` и `esc` соответственно.\n", "\n", "```{note}\n", "Положение стандартных кнопок на разных платформах отличается. Так, например, в операционных системах семейства `windows` кнопка `Ok` располагается левее кнопки `Cancel`, а в `maxOS` --- наоборот. `Qt` сам определит корректное с точки зрения операционной системы положение переданных кнопок, если использовать предлагаемые им методы и кнопки. При ручном подходе можно по ошибке нарушить стандарты операционной системы и тем самым вызвать путаницу для пользователя: там где он привык видеть кнопку `Cancel` оказалась кнопка `Ok`.\n", "```\n", "\n", "Кроме того, что кнопки необходимо вывести на экран, необходимо также знать о том, какая из кнопок была нажата пользователем (нажатие на кнопку в `QMessageBox` закрывает диалоговое окно и возвращает управление родительскому приложению). Если используется один из упомянутых статических методов, то для этого необходимо использовать возвращаемое значение. Если создаётся экземпляр класса `QMessageBox`, то можно использовать возвращаемое значение метода `exec` или после его завершения метод [clickedButton](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QMessageBox.html#PySide6.QtWidgets.PySide6.QtWidgets.QMessageBox.clickedButton). В любом случае на выходе вы получите одно из приведенных выше значений перечисления `QMessageBox.StandardButton` и простым сравнением, можно выяснить какая кнопка была нажата.\n", "\n", "В качестве примера под спойлером ниже приводится диалоговое окно, которое задает пользователю вопрос, хочет ли он изменить заголовок главного окна приложения и предлагает варианты ответа `Yes` и `No`.\n", "\n", "```{figure} ../_static/lecture_specific/qt/message_box_2.png\n", "```\n", "\n", "
\n", "Код.\n", "\n", "```python\n", "import sys\n", "\n", "from PySide6.QtWidgets import *\n", "\n", "\n", "class ChangeTitleQuestion(QMessageBox):\n", " def __init__(self, parent=None):\n", " super().__init__(parent=parent)\n", " self.setText('Change the window title to \"Hello, World!\" string?')\n", " self.setIcon(QMessageBox.Question)\n", " self.setStandardButtons(QMessageBox.Yes | QMessageBox.No)\n", " self.setDefaultButton(QMessageBox.No)\n", "\n", "\n", "class MainWindow(QMainWindow):\n", " def __init__(self):\n", " super().__init__()\n", " widget = QWidget()\n", " layout = QHBoxLayout()\n", " widget.setLayout(layout)\n", " self.setCentralWidget(widget)\n", " self.setMinimumWidth(300)\n", "\n", " # buttons\n", " push_button = QPushButton(\"Show question\")\n", " push_button.clicked.connect(self.show_question)\n", " layout.addWidget(push_button)\n", "\n", " def show_question(self):\n", " message_box = ChangeTitleQuestion(parent=self)\n", "\n", " ret = message_box.exec()\n", " if ret == QMessageBox.Yes:\n", " self.setWindowTitle(\"Hello, World!\")\n", "\n", "\n", "app = QApplication(sys.argv)\n", "main_window = MainWindow()\n", "main_window.show()\n", "app.exec()\n", "```\n", "
\n", "\n", "## Окно ввода данных. `QInputDialog`\n", "\n", "Для ввода данных удобно использовать диалоговое окно [QInputDialog](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QInputDialog.html). Как и в случае с `QMessageBox` можно создать экземпляр класса, настроить его и вызвать метод `exec`. Но во многих случаях удобнее использовать статические методы\n", "- [getDouble](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QInputDialog.html#PySide6.QtWidgets.PySide6.QtWidgets.QInputDialog.getDouble) --- диалоговое окно для ввода действительного числа;\n", "- [getInt](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QInputDialog.html#PySide6.QtWidgets.PySide6.QtWidgets.QInputDialog.getInt) --- диалоговое окно для ввода целого числа;\n", "- [getItem](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QInputDialog.html#PySide6.QtWidgets.PySide6.QtWidgets.QInputDialog.getItem) --- диалоговое окно для выбора элемента из списка элементов;\n", "- [getText](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QInputDialog.html#PySide6.QtWidgets.PySide6.QtWidgets.QInputDialog.getText) --- диалоговое окно для строки текста и [getMultiLineText](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QInputDialog.html#PySide6.QtWidgets.PySide6.QtWidgets.QInputDialog.getMultiLineText) для ввода нескольких строк текста.\n", "\n", "Эти методы всегда возвращают кортеж из двух значений: введенное пользователем значение и статус, который позволяет определить, нажал пользователь `Ok` или `Cancel`. Первое значение стоит использовать, только если статус не является `None`. \n", "\n", "Метод `getText` возвращает первым значением введенную пользователем строку текста.\n", "\n", "Метод `getItem` принимает в качестве параметра `items` список строк и возвращает первым значением элемент списка, выбранный пользователем. \n", "\n", "Методы ввода чисел `getInt` и `getDouble` создают окна с `spin box` и принимают параметры для него в качестве аргументов:\n", "- `value` --- значение при появлении;\n", "- `minValue`, `maxValue` --- минимальное и максимальное значение;\n", "- `step` --- шаг.\n", "\n", "Первым значением эти методы возвращают сразу числа.\n", "\n", "```{figure} ../_static/lecture_specific/qt/input_dialog.png\n", "```\n", "\n", "В качестве примера под спойлером ниже приводится приложение, которое выводит синтетически сгенерированный зашумленный сигнал и позволяет пользователю выбирать заголовок графика (строка), среднеквадратичное отклонение (действительное число) и стиль графика (элемент списка, см. [документацию по стилям](https://matplotlib.org/3.5.1/gallery/style_sheets/style_sheets_reference.html)). \n", "\n", "
\n", "Код.\n", "\n", "```python\n", "import sys\n", "\n", "import matplotlib.pyplot as plt\n", "import numpy as np\n", "from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg\n", "\n", "from PySide6.QtWidgets import *\n", "\n", "\n", "class MPLGraph(FigureCanvasQTAgg):\n", " def __init__(self):\n", " self.fig = plt.figure(figsize=(2, 2), layout=\"tight\")\n", " self.ax = None\n", " super().__init__(self.fig)\n", " self.style = \"default\"\n", " self.title = \"\"\n", " self.noise_scale = 0.1\n", " self.plot()\n", "\n", " def plot(self):\n", " with plt.style.context(self.style):\n", " if self.ax:\n", " self.fig.delaxes(self.ax)\n", " self.ax = self.fig.add_subplot(111)\n", " x = np.linspace(0, 1)\n", " noise = np.random.normal(0, self.noise_scale, size=(len(x),))\n", " y = x + noise\n", " self.ax.plot(x, y)\n", " self.ax.set_title(self.title)\n", " self.ax.set_xlabel(\"t\")\n", " self.ax.set_ylabel(\"signal\")\n", " self.draw()\n", "\n", " def set_style(self, style):\n", " self.style = style\n", " self.plot()\n", "\n", " def set_title(self, title):\n", " self.title = title\n", " self.plot()\n", "\n", " def set_noise_scale(self, scale):\n", " self.noise_scale = scale\n", " self.plot()\n", "\n", "\n", "class MainWindow(QMainWindow):\n", " def __init__(self):\n", " super().__init__()\n", " self.setGeometry(0, 0, 600, 400)\n", " widget = QWidget()\n", " layout = QVBoxLayout()\n", " widget.setLayout(layout)\n", " self.setCentralWidget(widget)\n", "\n", " # widgets\n", " self.graph = MPLGraph()\n", " set_title_button = QPushButton(\"Set title\")\n", " set_style_button = QPushButton(\"Choose style\")\n", " set_noise_scale_button = QPushButton(\"Set noice scale\")\n", "\n", " # layout\n", " layout.addWidget(self.graph)\n", "\n", " bottom_row = QHBoxLayout()\n", " bottom_row.addWidget(set_title_button)\n", " bottom_row.addWidget(set_style_button)\n", " bottom_row.addWidget(set_noise_scale_button)\n", " layout.addLayout(bottom_row)\n", "\n", " # connections\n", " set_title_button.clicked.connect(self.on_set_title_button_click)\n", " set_style_button.clicked.connect(self.on_set_style_button_clicked)\n", " set_noise_scale_button.clicked.connect(\n", " self.on_set_noise_scale_button_click\n", " )\n", "\n", " def on_set_title_button_click(self):\n", " title, ok = QInputDialog.getText(\n", " self,\n", " \"Choose title\",\n", " \"Choose the title for the figure\",\n", " )\n", " if ok:\n", " self.graph.set_title(title)\n", "\n", " def on_set_style_button_clicked(self):\n", " style_list = ['default'] + plt.style.available\n", " style, ok = QInputDialog.getItem(\n", " self,\n", " \"Choose style\",\n", " \"Pick a style\",\n", " style_list\n", " )\n", " self.graph.set_style(style)\n", "\n", " def on_set_noise_scale_button_click(self):\n", " scale, ok = QInputDialog.getDouble(\n", " self,\n", " \"Choose noise scale\",\n", " \"Type in standard deviation of noise to be applied\",\n", " 0.1,\n", " 0,\n", " 0.2,\n", " decimals=2,\n", " step=0.01\n", " )\n", " if ok:\n", " self.graph.set_noise_scale(scale)\n", "\n", "\n", "app = QApplication(sys.argv)\n", "main_window = MainWindow()\n", "main_window.show()\n", "app.exec()\n", "```\n", "
\n", "\n", "## Выбор файла. `QFileDialog`\n", "\n", "Предоставить пользователю возможность выбирать файл(ы)/директорию(и) проще всего файловым диалогом [QFileDialog](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QFileDialog.html) и его статическими методами, среди которых мы разберем [getOpenFile](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QFileDialog.html#PySide6.QtWidgets.PySide6.QtWidgets.QFileDialog.getOpenFileName), позволяющий выбрать один файл.\n", "\n", "Этот метод принимает параметры\n", "- `parent` --- родительский виджет;\n", "- `caption` --- заголовок файлового диалога;\n", "- `dir` --- путь к папке, которая открыта изначально;\n", "- `filter` --- фильтр допустимых файлов или их список;\n", "- `selectedFilter` --- выбранный изначально фильтр;\n", "- и др.\n", "\n", "Параметр `filter` позволяет отсеивать неподходящие файлы по их расширению. Например, если приложение ожидает таблицу, то подходящими расширениями могут считаться `csv` файлы, а также `Excel` таблицы --- файлы с расширениями `xls` и `xlsx`. В таком случае можно указать в качестве фильтра строку `\"*.csv *.xls *.xlsx\"`. Тогда файлы с расширениями отличными от перечисленных не будут показывать в диалоге выбора файлов. \n", "\n", "Чтобы яснее дать пользователю, что ожидаются файлы-таблицы, можно в качестве параметра `filter` передать строку `\"Tables (*.csv *.xls *.xlsx)\"`, т.е. указать расширения в скобках, а перед ними указать подсказку пользователю. \n", "\n", "Для изображений такая строка могла бы выглядеть `\"Images (*.png *.jpg *.xpm)\"`, для файлов исходного кода `python` --- `\"Python source code (*.py)\"`, для всех файлов --- `\"All files (*)\"`. Чтобы указать список фильтров, эти фильтры объединяются в одну строку с разделителем `;;`. \n", "\n", "Возвращает этот метод кортеж из двух значений. Первое значение --- путь к выбранному пользователем файлу в виде строки, второе --- фильтр, при котором был выбран этот файл. Если пользователь нажимает кнопку `cancel`, то в качестве пути возвращается пустая строка.\n", "\n", "```{figure} ../_static/lecture_specific/qt/input_dialog.png\n", "```\n", "\n", "В качестве примера под спойлером ниже приводится приложение, которое выводит изображение, выбранное пользователем в файловом диалоге. \n", "\n", "
\n", "Код.\n", "\n", "```python\n", "import os.path\n", "import sys\n", "\n", "from PySide6.QtGui import *\n", "from PySide6.QtWidgets import *\n", "\n", "\n", "class MainWindow(QMainWindow):\n", " def __init__(self):\n", " super().__init__()\n", " widget = QWidget()\n", " layout = QVBoxLayout()\n", " widget.setLayout(layout)\n", " self.setCentralWidget(widget)\n", "\n", " # buttons\n", " self.label = QLabel(\"An image will be displayed here\")\n", " push_button = QPushButton(\"Choose Image\")\n", " layout.addWidget(self.label)\n", " layout.addWidget(push_button)\n", "\n", " # connections\n", " push_button.clicked.connect(self.choose_image)\n", "\n", " def choose_image(self):\n", " path, _ = QFileDialog.getOpenFileName(\n", " self,\n", " caption=\"Pick an image\",\n", " dir=os.path.join(\"..\", \"images\"),\n", " filter=\"Images (*.png *.xpm *.jpg)\"\n", " )\n", " if path:\n", " self.label.setPixmap(QPixmap(path))\n", "\n", "\n", "app = QApplication(sys.argv)\n", "main_window = MainWindow()\n", "main_window.show()\n", "app.exec()\n", "```\n", "
" ] } ], "metadata": { "language_info": { "name": "python" }, "orig_nbformat": 4 }, "nbformat": 4, "nbformat_minor": 2 }