Собственные диалоговые окна, класс Dialog

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

wx.Dialog(parent, id=ID_ANY, title="", pos=DefaultPosition, size=DefaultSize, style=DEFAULT_DIALOG_STYLE, name=DialogNameStr)

  • parent – ссылка на родительское окно (или значение None);
  • id – идентификатор окна;
  • title – заголовок окна;
  • pos, size – позиция и размер окна;
  • style – стилизация окна.

Подробную документацию по этому классу можно посмотреть на странице:

https://docs.wxpython.org/wx.Dialog.html

Из этой документации можно увидеть значения констант, используемых в параметре style. Вот некоторые из них:

  • wx.CAPTION – разрешает заголовок окна;
  • wx.DEFAULT_DIALOG_STYLE (комбинация констант: wx.CAPTION, wx.CLOSE_BOX и wx.SYSTEM_MENU);
  • wx.RESIZE_BORDER – разрешает изменять размеры окна;
  • wx.SYSTEM_MENU – для отображения системного меню;
  • wx.CLOSE_BOX – для отображения кнопки закрытия (с крестиком);
  • wx.MAXIMIZE_BOX – для возможности распахивания окна;
  • wx.MINIMIZE_BOX – для возможности свертывания окна;
  • wx.STAY_ON_TOP – для отображения окна поверх всех остальных.

Давайте в качестве примера создадим свое простое диалоговое окно. Для этого объявим класс MyDlg, который унаследуем от Dialog:

class MyDlg(wx.Dialog):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

Смотрите, мы здесь объявили свой конструктор, в который можно передавать произвольные параметры *args и именованные параметры **kwargs. Затем, вызываем конструктор базового класса, то есть, Dialog и передаем ему все те же самые параметры: *args и **kwargs. Благодаря этому мы можем через класс MyDlg создавать диалоговые окна с разной стилизацией.

Далее, в классе MyFrame создадим тулбар с двумя кнопками и вторую свяжем с вызовом диалога:

class MyFrame(wx.Frame):
    def __init__(self, parent, title):
        super().__init__(parent, title=title, size=(600, 300))
 
        toolbar = self.CreateToolBar()
        br_quit = toolbar.AddTool(wx.ID_ANY, "Выход", wx.Bitmap("exit32.png"))
        toolbar.AddSeparator()
        dialog = toolbar.AddTool(wx.ID_ANY, "Диалог", wx.Bitmap("sound32.png"))
        toolbar.Realize()
 
        self.Bind(wx.EVT_TOOL, self.onQuit, br_quit)
        self.Bind(wx.EVT_TOOL, self.onDialog, dialog)

Сами обработчики будут следующими:

    def onDialog(self, e):
        dlg = MyDlg(self, title="Мой диалог...")
        res = dlg.ShowModal()
        dlg.Destroy()
        print(res)
 
    def onQuit(self, e):
        self.Close()

В обработчике onDialog мы создаем экземпляр класса MyDlg, указывая родительское окно MyFrame и заголовок title. Затем, отображаем окно как модальное с помощью метода ShowModal. Данный метод возвращает id элемента, через который произошло закрытие этого окна. И следующей строчкой мы должны вызвать метод Destroy для уничтожения окна. Иначе оно просто будет скрыто, но продолжит существовать в памяти компьютера.

При запуске этой программы увидим вот такой результат диалогового окна:

А в консоли появится значение wx.ID_CANCEL, равное 5101. Мы в этом можем легко убедиться, если запишем:

print(res, wx.ID_CANCEL)

Модальные и немодальные окна

Запустим программу еще раз и обратим внимание на такой факт: пока не закроем это диалоговое окно, другие окна программы будут для нас недоступны. Про такие окна говорят, что они модальные. То есть, мы можем взаимодействовать только с ним, пока его не закроем.

Для модальных окон создается свой независимый цикл обработки событий. Условно это можно изобразить так:

В нашей программе модальное окно создается благодаря вызову метода ShowModal и программа переходит на следующую строчку dlg.Destroy() только после закрытия диалога. Однако, мы можем изменить это поведение и отобразить диалоговое окно как немодальное, вызвав метод Show:

res = dlg.Show()

Смотрите, теперь при запуске наше окно показывается и тут же исчезает, так как программа сразу идет дальше и вызывается метод dlg.Destroy(). Давайте поставим его в комментарий и снова запустим программу. Теперь наше окно не исчезло и, кроме того, мы можем взаимодействовать и с главным окном, например, открыть еще одно диалоговое окно. Здесь уже нет отдельного цикла событий и потому интерфейс реагирует на действия пользователя.

Вот в этом основные отличия модальных и немодальных окон. Конечно, я здесь привел только пример отображения окна как немодального. По идее, в таком случае нужно контролировать их число и, например, не показывать более одного. Для этого к классе MyFrame создадим локальное свойство:

self.dlg = None

и в обработчике onDialog запишем:

        if self.dlg is None:
            self.dlg = MyDlg(self, title="Мой диалог...")
            self.dlg.Show()

То есть, прежде чем создать новое окно, мы проверяем значение свойства self.dlg и если оно равно None, то делаем вывод, что окно еще не отображено, создаем его и показываем на экране.

А в конструкторе класса MyDlg добавим обязательный параметр parent:

class MyDlg(wx.Dialog):
    def __init__(self, parent, *args, **kwargs):
        super().__init__(parent, *args, **kwargs)
        self.parent = parent

и сохраним его в отдельном локальном свойстве этого класса. Далее, здесь же мы должны добавить обработчик события wx.EVT_CLOSE:

self.Bind(wx.EVT_CLOSE, self.onClose)

а сам обработчик запишем следующим образом:

    def onClose(self, e):
        self.Destroy()
        self.parent.dlg = None

В нем мы уничтожаем окно с помощью метода Destroy и свойство dlg класса MyFrame снова устанавливаем в None.

Запустим программу, и теперь отображается только одно диалоговое окно. Конечно, это лишь пример того, как можно работать с такими немодальными окнами. Логика обработки вполне может быть и другой. Все зависит от конкретной решаемой задачи.

Отображение модального окна через менеджер контекста

Давайте вернем окно в модальный режим, запишем обработчик onDialog в виде:

    def onDialog(self, e):
        dlg = MyDlg(self, title="Мой диалог...")
        res = dlg.ShowModal()
        dlg.Destroy()
        print(res)

В этом фрагменте программы есть один недостаток: в случае каких-либо ошибок (исключений) метод Destroy может быть не вызван и окно останется в памяти. Конечно, мы можем воспользоваться конструкцией try/except/finally, чтобы решить эту проблему и записать все вот в таком виде:

    def onDialog(self, e):
        try: 
            dlg = MyDlg(self, title="Мой диалог...")
            res = dlg.ShowModal()
            print(res)
        except Exception as e:
            print(e)
        finally:
            dlg.Destroy()

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

        with MyDlg(self, title="Мой диалог...") as dlg:
            res = dlg.ShowModal()
            print(res)

Здесь в менеджере создается экземпляр окна и далее, через ссылку dlg, мы отображаем это окно как модальное. При выходе из этого менеджера диалоговое окно будет автоматически уничтожено. Мы в этом можем легко убедиться, если переопределим в классе MyDlg этот метод:

    def Destroy(self):
        print("Вызван метод Destroy")
        super().Destroy()

Теперь при закрытии окна увидим сообщение, что был вызван метод Destroy.

Стилизация диалоговых окон

Давайте теперь посмотрим на различные способы стилизации диалоговых окон. И для начала вызовем окно со стандартными стилями:

with MyDlg(self, title="Мой диалог...", style=wx.CAPTION | wx.CLOSE_BOX | wx.SYSTEM_MENU) as dlg:

Далее, уберем стиль wx.CLOSE_BOX:

with MyDlg(self, title="Мой диалог...", style=wx.CAPTION | wx.SYSTEM_MENU) as dlg:

и смотрите, кнопка закрытия окна неактивна, окно можно закрыть лишь комбинацией клавиш Alt+F4.

Если убрать все стили:

with MyDlg(self, title="Мой диалог...", style=0) as dlg:

то получим окно в виде вот такого полотна. Его также можно закрыть комбинацией клавиш Alt+F4.

Добавим туда wx.CAPTION, wx.CLOSE_BOX и еще wx.RESIZE_BORDER:

with MyDlg(self, title="Мой диалог...", style=wx.CAPTION | wx.CLOSE_BOX | wx.RESIZE_BORDER) as dlg:

Теперь наше окно можно менять в размерах. И так далее, комбинируя стили различным образом можно получать окна с разными свойствами.

Стандартные элементы управления

Давайте разместим в нашем окне текстовое поле ввода и две кнопки «Да» и «Отмена». Класс MyDlg можно представить так:

class MyDlg(wx.Dialog):
    def __init__(self, parent, *args, **kwargs):
        super().__init__(parent, *args, **kwargs)
        self.SetSize(500, 150)
 
        vb = wx.BoxSizer(wx.VERTICAL)
        vb.Add(wx.TextCtrl(self, wx.ID_ANY), flag=wx.EXPAND | wx.ALL, border=10)
 
        vh = wx.BoxSizer(wx.HORIZONTAL)
        bOk = wx.Button(self, wx.ID_ANY, label="Да", size=(100, 30))
        bCn = wx.Button(self, wx.ID_ANY, label="Отмена", size=(100, 30))
        vh.Add(bOk, flag=wx.LEFT, border = 10)
        vh.Add(bCn, flag=wx.LEFT, border=10)
 
        vb.Add(vh, flag=wx.ALIGN_CENTER|wx.ALL, border=10)
        self.SetSizer(vb)

Запустим нашу программу и смотрите, при нажатии на кнопки ничего не происходит, что, впрочем, ожидаемо, так как мы не назначали им никаких обработчиков. Если решать эту задачу «в лоб», то можно это реализовать так. На первую кнопку «Да» повесить обработчик onOk:

bOk.Bind(wx.EVT_BUTTON, self.onOk)

и реализовать его в следующем виде:

    def onOk(self, event): 
        self.EndModal(wx.ID_OK)

Смотрите, мы здесь вызываем метод EndModal класса Dialog, который завершает цикл выполнения диалогового окна и определяет возвращаемое значение в виде константы ID_OK. И, затем, переменная res в обработчике onDialog будет принимать это значение. То есть, мы можем сделать такую проверку:

            if res == wx.ID_OK:
                print("Нажата кнопка да")

Но это не лучший подход к обработке стандартных элементов управления в диалогах. Гораздо проще и удобнее у кнопки bOk прописать этот стандартный id:

bOk = wx.Button(self, wx.ID_OK, label="Да", size=(100, 30))

а назначение обработчика убрать. При запуске программы получим абсолютно такой же функционал для этой кнопки. По аналогии объявим и кнопку bCn:

bCn = wx.Button(self, wx.ID_CANCEL, label="Отмена", size=(100, 30))

В результате у нас будут две кнопки со стандартным поведением.

Вот так создаются свои собственные диалоговые окна в wxPython.

Видео по теме