Механизм обработки событий

На этом занятии подробнее разберемся в работе событий в wxPython. Мы уже знаем, что для каждого типа объекта имеется свой id события. Например,

  • wx.EVT_BUTTON – событие, генерируемое виджетом wx.Button;
  • wx.EVT_MENU – событие, генерируемое меню;
  • wx.EVT_TEXT – событие, генерируемое wx.TextCtrl;
  • wx.EVT_TOOL – событие, генерируемое toolbox;
  • wx.EVT_MOVE – событие при перемещении окна;
  • wx.EVT_PAINT – событие при перерисовки элемента (обычно, окна);
  • wx.EVT_KEY_DOWN – событие при нажатии на клавишу;
  • wx.EVT_KEY_UP – событие при отпускании клавиши.

И так далее. Где можно посмотреть весь список этих констант? Верно, на официальном сайте по wxPython:

docs.wxpython.org/wx.Event.html

Посмотрите здесь на содержимое подклассов класса Event и вы найдете много разных типов событий. Так вот, в самом простом варианте мы можем связать метод класса с тем или иным событием с помощью функции Bind:

объект.Bind(<тип события>, <метод>)

Предположим, что у нас есть вот такая заготовка:

import wx
 
class MyFrame(wx.Frame ):
    def __init__(self, parent, title):
        super().__init__(parent, title=title, size=(600,300))
 
app = wx.App()
frame = MyFrame(None, 'wxPython')
frame.Show()
app.MainLoop()

И в конструкторе класса MyFrame пропишем метод Bind, в котором свяжем левое нажатие кнопки мыши с обработчиком onLeftDown:

self.Bind(wx.EVT_LEFT_DOWN, self.onLeftDown)

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

    def onLeftDown(self, event):
        print("Нажатие на левую кнопку мыши")

Все, теперь при возникновении этого типа события у нас будет срабатывать метод onLeftDown.

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

btn1 = wx.Button(self, wx.ID_ANY, "Кнопка 1")
btn2 = wx.Button(self, wx.ID_ANY, "Кнопка 2")
btn1.SetPosition(wx.Point(10, 10))
btn2.SetPosition(wx.Point(200, 10))

А затем назначить обработчики:

self.Bind(wx.EVT_BUTTON, self.onButton1)
self.Bind(wx.EVT_BUTTON, self.onButton2)

То при возникновении события EVT_BUTTON будет срабатывать только второй метод onButton2, так как мы здесь попросту затираем первый onButton1 и назначаем второй. Причем он будет срабатывать при нажатии на любую из этих кнопок. Чтобы программа могла их различать в методе Bind следует дополнительно указать id объекта, от которого пришло это событие:

self.Bind(wx.EVT_BUTTON, self.onButton1, id=BUTTON1)
self.Bind(wx.EVT_BUTTON, self.onButton2, id=BUTTON2)

У самих кнопок прописать эти id:

btn1 = wx.Button(self, BUTTON1, "Кнопка 1")
btn2 = wx.Button(self, BUTTON2, "Кнопка 2")

и определить их как глобальные, например, так:

BUTTON1 = 1
BUTTON2 = 2

Или можно третьим параметром передать ссылку на соответствующий виджет:

self.Bind(wx.EVT_BUTTON, self.onButton1, btn1)
self.Bind(wx.EVT_BUTTON, self.onButton2, btn2)

Все будет работать аналогично. Причем здесь разделение также будет осуществляться по id этих объектов: в Bind будет вызван метод:

btn1.GetId() и btn2.GetId()

для получения id этих виджетов. То есть, это эквивалентно вот такому вызову:

self.Bind(wx.EVT_BUTTON, self.onButton1, id=btn1.GetId())
self.Bind(wx.EVT_BUTTON, self.onButton2, id=btn2.GetId())

Общий вывод здесь такой: если имеется несколько объектов, генерирующие одинаковые типы событий, то для их разделения следует использовать id.

Наконец, последнее, что касается метода Bind. При его вызове мы указываем объект, из которого он вызывается. Например, в нашей программе вместо self (то есть, объекта Frame) можно использовать и объекты кнопок:

btn1.Bind(wx.EVT_BUTTON, self.onButton1, id=btn1.GetId())
btn2.Bind(wx.EVT_BUTTON, self.onButton2, id=btn2.GetId())

В чем разница между такими вызовами? В действительности события от большинства виджетов распространяются от дочернего объекта (где возникли) и переходят по всем его родителям. Например, если создать вот такую иерархию: в окне (Frame) расположить панель (Panel), а на панели кнопку (Button), то событие от кнопки перейдет к панели, а затем, к окну.

По-английски это звучит как propagation – распространение. И когда мы привязываем обработчик onButton1 к кнопке:

btn1.Bind(wx.EVT_BUTTON, self.onButton1)

то тем самым «вешаем» его именно на эту кнопку, то есть, событие EVT_BUTTON перехватывается сразу на этом виджете. Если аналогичный обработчик повесить на панель:

panel.Bind(wx.EVT_BUTTON, self.onButtonPanel)

то всплывшее событие EVT_BUTTON от кнопки будет перехвачено на уровне панели и обработано. Ну, а если записать через self (Frame):

self.Bind(wx.EVT_BUTTON, self.onButtonFrame)

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

        panel = wx.Panel(self)
        btn = wx.Button(panel, wx.ID_ANY, "Нажать")
 
        btn.Bind(wx.EVT_BUTTON, self.onButton)
        panel.Bind(wx.EVT_BUTTON, self.onButtonPanel)
        self.Bind(wx.EVT_BUTTON, self.onButtonFrame)

и добавим методы:

    def onButton(self, event):
        print("Уровень кнопки")
        event.Skip()
 
    def onButtonPanel(self, event):
        print("Уровень панели")
        event.Skip()
 
    def onButtonFrame(self, event):
        print("Уровень окна")

В каждом методе мы здесь вызываем метод Skip объекта Event. Это необходимо, чтобы текущее событие не завершалось на данном уровне, а переходило дальше к родителям. Запустим программу, нажмем на кнопку и в консоли увидим три сообщения:

Уровень кнопки
Уровень панели
Уровень окна

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

Но так ведут себя не все события. Есть, так называемые, базовые (basic events), которые не распространяются по родительским объектам. Например, к таких относится EVT_CLOSE, возникающее при закрытии окна. И такое его поведение вполне логично. Представьте, что если бы при многооконном интерфейсе это событие переходило к родителю:

Тогда, закрывая дочернее окно, закрывалось бы и главное. Наверное, это не то, что мы бы хотели? Поэтому такие специальные события не всплывают. К ним же относятся и все другие, что связаны с изменением вида окна: распахивание, свертывание, изменение размеров и т.п. Часто, чтобы понять какие события распространяются, а какие нет, достаточно логически подумать и задаться вопросом: безопасно ли распространение данного события? И, если да, то, скорее всего, событие переходит по родительским элементам.

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

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

и добавляем обработчик:

    def OnCloseWindow(self, event):
        dial = wx.MessageDialog(None, 'Вы действительно хотите выйти?', 'Вопрос',  
                     wx.YES_NO | wx.NO_DEFAULT | wx.ICON_QUESTION)
 
        ret = dial.ShowModal()
 
        if ret == wx.ID_YES:
            self.Destroy()
        else:
            event.Veto()

Когда пользователь пытается закрыть окно, отображается диалоговое окно:

Если будет нажата кнопка «Да», то вызовется метод Destroy() и главное окно будет уничтожено. Иначе, вызывается метод Veto() объекта event, которое прерывает дальнейшую обработку данного события. И, обратите внимание, мы здесь должны уничтожить окно методом Destroy(), а не просто закрыть его методом Close(), т.к. оно спровоцирует сообщение EVT_CLOSE и наша программа зациклится.

Наконец, если нам нужно сбросить какой-либо обработчик, то следует воспользоваться методом:

Unbind(self, event, source=None, id=wx.ID_ANY, id2=wx.ID_ANY, handler=None)

Например, уберем обработку сообщения EVT_BUTTON на уровне панели:

panel.Unbind(wx.EVT_BUTTON)

Теперь при нажатии на кнопку увидим только два сообщения в консоли:

Уровень кнопки
Уровень окна

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

Видео по теме