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

Диалоговые окна чаще всего используются для кратковременного взаимодействия с пользователем. Все они наследуются от класса QDialog. Диалоговое окно появляется поверх родительского (посередине родительского, если указан параметр parent), при этом обычно родительское окно становится не активных до тех пор, пока пользователь не закрое диалоговое окно.

Создавать диалоговые окна можно было бы и вручную (например, наследуясь от QDialog и модифицируя его поведение под свои цели), но для большинства ситуаций в Qt заготовлены удобные шаблонные варианты.

Отображение сообщений. QMessageBox

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

Есть два подхода использовать класс QMessageBox.

  1. Создать его экземпляр и вызвать его метод exec;

  2. Использовать статический метод (например, QMessageBox.information).

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

  • about — сообщение о приложении, иконка родительского виджета по умолчанию;

  • image critical — сообщение о критической ошибке;

  • image information — информационное сообщение;

  • image question — вопрос пользователю;

  • image warning — предупреждение (например, сообщение о некритической ошибке).

При ручном создании экземпляра иконку можно выставить методом setIcon, текст сообщения — setText, заголовок диалогового окна — setWindowTitle. Статические методы принимают эти значения в качестве параметров, а иконка определяется типом статического метода.

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

../_images/message_box_1.png
Код.
import sys

from PySide6.QtWidgets import *


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        widget = QWidget()
        layout = QHBoxLayout()
        widget.setLayout(layout)
        self.setCentralWidget(widget)

        # buttons
        push_button = QPushButton("Show message")
        push_button.clicked.connect(self.show_message)
        layout.addWidget(push_button)

    def show_message(self):
        QMessageBox.information(
            self,
            "My first dialog",
            "Hello, World!"
        )

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

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

  • закрыть программу без сохранения изменений,

  • сохранить изменения и выйти,

  • отменить закрытие программы и продолжить работу.

Виджет QMessageBox позволяет без труда добавлять в диалоговое окно ряд стандартных кнопок, Для этого используется перечисление QMessageBox.StandardButton, у которого возможны следующие значения:

  • QMessageBox.Ok

  • QMessageBox.Open

  • QMessageBox.Save

  • QMessageBox.Cancel

  • QMessageBox.Close

  • QMessageBox.Discard

  • QMessageBox.Apply

  • QMessageBox.Reset

  • QMessageBox.RestoreDefaults

  • QMessageBox.Help

  • QMessageBox.SaveAll

  • QMessageBox.Yes

  • QMessageBox.YesToAll

  • QMessageBox.No

  • QMessageBox.NoToAll

  • QMessageBox.Abort

  • QMessageBox.Retry

  • QMessageBox.Ignore

Выставить их можно передав значения этого перечисления (объединить несколько кнопок можно знаком логического “или” |) в качестве параметра конструктора или одного из упомянутых методов или используя метод setStandardButtons у созданного экземпляра класса QMessageBox. Методами setDefaultButton и setEscapeButton можно выставить кнопки, которые будут считаться при нажатии клавиш enter и esc соответственно.

Note

Положение стандартных кнопок на разных платформах отличается. Так, например, в операционных системах семейства windows кнопка Ok располагается левее кнопки Cancel, а в maxOS — наоборот. Qt сам определит корректное с точки зрения операционной системы положение переданных кнопок, если использовать предлагаемые им методы и кнопки. При ручном подходе можно по ошибке нарушить стандарты операционной системы и тем самым вызвать путаницу для пользователя: там где он привык видеть кнопку Cancel оказалась кнопка Ok.

Кроме того, что кнопки необходимо вывести на экран, необходимо также знать о том, какая из кнопок была нажата пользователем (нажатие на кнопку в QMessageBox закрывает диалоговое окно и возвращает управление родительскому приложению). Если используется один из упомянутых статических методов, то для этого необходимо использовать возвращаемое значение. Если создаётся экземпляр класса QMessageBox, то можно использовать возвращаемое значение метода exec или после его завершения метод clickedButton. В любом случае на выходе вы получите одно из приведенных выше значений перечисления QMessageBox.StandardButton и простым сравнением, можно выяснить какая кнопка была нажата.

В качестве примера под спойлером ниже приводится диалоговое окно, которое задает пользователю вопрос, хочет ли он изменить заголовок главного окна приложения и предлагает варианты ответа Yes и No.

../_images/message_box_2.png
Код.
import sys

from PySide6.QtWidgets import *


class ChangeTitleQuestion(QMessageBox):
    def __init__(self, parent=None):
        super().__init__(parent=parent)
        self.setText('Change the window title to "Hello, World!" string?')
        self.setIcon(QMessageBox.Question)
        self.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
        self.setDefaultButton(QMessageBox.No)


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        widget = QWidget()
        layout = QHBoxLayout()
        widget.setLayout(layout)
        self.setCentralWidget(widget)
        self.setMinimumWidth(300)

        # buttons
        push_button = QPushButton("Show question")
        push_button.clicked.connect(self.show_question)
        layout.addWidget(push_button)

    def show_question(self):
        message_box = ChangeTitleQuestion(parent=self)

        ret = message_box.exec()
        if ret == QMessageBox.Yes:
            self.setWindowTitle("Hello, World!")


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

Окно ввода данных. QInputDialog

Для ввода данных удобно использовать диалоговое окно QInputDialog. Как и в случае с QMessageBox можно создать экземпляр класса, настроить его и вызвать метод exec. Но во многих случаях удобнее использовать статические методы

  • getDouble — диалоговое окно для ввода действительного числа;

  • getInt — диалоговое окно для ввода целого числа;

  • getItem — диалоговое окно для выбора элемента из списка элементов;

  • getText — диалоговое окно для строки текста и getMultiLineText для ввода нескольких строк текста.

Эти методы всегда возвращают кортеж из двух значений: введенное пользователем значение и статус, который позволяет определить, нажал пользователь Ok или Cancel. Первое значение стоит использовать, только если статус не является None.

Метод getText возвращает первым значением введенную пользователем строку текста.

Метод getItem принимает в качестве параметра items список строк и возвращает первым значением элемент списка, выбранный пользователем.

Методы ввода чисел getInt и getDouble создают окна с spin box и принимают параметры для него в качестве аргументов:

  • value — значение при появлении;

  • minValue, maxValue — минимальное и максимальное значение;

  • step — шаг.

Первым значением эти методы возвращают сразу числа.

../_images/input_dialog.png

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

Код.
import sys

import matplotlib.pyplot as plt
import numpy as np
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg

from PySide6.QtWidgets import *


class MPLGraph(FigureCanvasQTAgg):
    def __init__(self):
        self.fig = plt.figure(figsize=(2, 2), layout="tight")
        self.ax = None
        super().__init__(self.fig)
        self.style = "default"
        self.title = ""
        self.noise_scale = 0.1
        self.plot()

    def plot(self):
        with plt.style.context(self.style):
            if self.ax:
                self.fig.delaxes(self.ax)
            self.ax = self.fig.add_subplot(111)
            x = np.linspace(0, 1)
            noise = np.random.normal(0, self.noise_scale, size=(len(x),))
            y = x + noise
            self.ax.plot(x, y)
            self.ax.set_title(self.title)
            self.ax.set_xlabel("t")
            self.ax.set_ylabel("signal")
            self.draw()

    def set_style(self, style):
        self.style = style
        self.plot()

    def set_title(self, title):
        self.title = title
        self.plot()

    def set_noise_scale(self, scale):
        self.noise_scale = scale
        self.plot()


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setGeometry(0, 0, 600, 400)
        widget = QWidget()
        layout = QVBoxLayout()
        widget.setLayout(layout)
        self.setCentralWidget(widget)

        # widgets
        self.graph = MPLGraph()
        set_title_button = QPushButton("Set title")
        set_style_button = QPushButton("Choose style")
        set_noise_scale_button = QPushButton("Set noice scale")

        # layout
        layout.addWidget(self.graph)

        bottom_row = QHBoxLayout()
        bottom_row.addWidget(set_title_button)
        bottom_row.addWidget(set_style_button)
        bottom_row.addWidget(set_noise_scale_button)
        layout.addLayout(bottom_row)

        # connections
        set_title_button.clicked.connect(self.on_set_title_button_click)
        set_style_button.clicked.connect(self.on_set_style_button_clicked)
        set_noise_scale_button.clicked.connect(
            self.on_set_noise_scale_button_click
        )

    def on_set_title_button_click(self):
        title, ok = QInputDialog.getText(
            self,
            "Choose title",
            "Choose the title for the figure",
        )
        if ok:
            self.graph.set_title(title)

    def on_set_style_button_clicked(self):
        style_list = ['default'] + plt.style.available
        style, ok = QInputDialog.getItem(
            self,
            "Choose style",
            "Pick a style",
            style_list
        )
        self.graph.set_style(style)

    def on_set_noise_scale_button_click(self):
        scale, ok = QInputDialog.getDouble(
            self,
            "Choose noise scale",
            "Type in standard deviation of noise to be applied",
            0.1,
            0,
            0.2,
            decimals=2,
            step=0.01
        )
        if ok:
            self.graph.set_noise_scale(scale)


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

Выбор файла. QFileDialog

Предоставить пользователю возможность выбирать файл(ы)/директорию(и) проще всего файловым диалогом QFileDialog и его статическими методами, среди которых мы разберем getOpenFile, позволяющий выбрать один файл.

Этот метод принимает параметры

  • parent — родительский виджет;

  • caption — заголовок файлового диалога;

  • dir — путь к папке, которая открыта изначально;

  • filter — фильтр допустимых файлов или их список;

  • selectedFilter — выбранный изначально фильтр;

  • и др.

Параметр filter позволяет отсеивать неподходящие файлы по их расширению. Например, если приложение ожидает таблицу, то подходящими расширениями могут считаться csv файлы, а также Excel таблицы — файлы с расширениями xls и xlsx. В таком случае можно указать в качестве фильтра строку "*.csv *.xls *.xlsx". Тогда файлы с расширениями отличными от перечисленных не будут показывать в диалоге выбора файлов.

Чтобы яснее дать пользователю, что ожидаются файлы-таблицы, можно в качестве параметра filter передать строку "Tables (*.csv *.xls *.xlsx)", т.е. указать расширения в скобках, а перед ними указать подсказку пользователю.

Для изображений такая строка могла бы выглядеть "Images (*.png *.jpg *.xpm)", для файлов исходного кода python"Python source code (*.py)", для всех файлов — "All files (*)". Чтобы указать список фильтров, эти фильтры объединяются в одну строку с разделителем ;;.

Возвращает этот метод кортеж из двух значений. Первое значение — путь к выбранному пользователем файлу в виде строки, второе — фильтр, при котором был выбран этот файл. Если пользователь нажимает кнопку cancel, то в качестве пути возвращается пустая строка.

../_images/input_dialog.png

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

Код.
import os.path
import sys

from PySide6.QtGui import *
from PySide6.QtWidgets import *


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        widget = QWidget()
        layout = QVBoxLayout()
        widget.setLayout(layout)
        self.setCentralWidget(widget)

        # buttons
        self.label = QLabel("An image will be displayed here")
        push_button = QPushButton("Choose Image")
        layout.addWidget(self.label)
        layout.addWidget(push_button)

        # connections
        push_button.clicked.connect(self.choose_image)

    def choose_image(self):
        path, _ = QFileDialog.getOpenFileName(
            self,
            caption="Pick an image",
            dir=os.path.join("..", "images"),
            filter="Images (*.png *.xpm *.jpg)"
        )
        if path:
            self.label.setPixmap(QPixmap(path))


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