{
"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
}