本来打算介绍一下wxFormBuilder这个可视化设计工具的,但我觉得事件处理挺重要的,有必要说明一下。
在wxPython中,事件可以来自输入设备,例如鼠标、键盘,也可以来自于标准控件,例如按钮。 还有一些事件并不是来自于交互行为,例如 wx.TimerEvent
,当然,我们也可以自定义事件。 但无论事件来自于哪里,wxPython都将其封装成一致的数据结构。具体来说, 每个事件都是由以下三部分组成:
- 事件类型。点击按钮和按下键盘就是不同的事件类型。
- 事件类。每个事件都关联一个对象,该对象继承自
wx.CommandEvent
或者wx.KeyEvent
, 这两种事件类又继承自wx.Event
。 - 事件来源。
wx.Event
中会含有事件来源的对象,通过该对象可以准确区分出来源。
事件绑定
我们可以使用 wx.EvtHandler.Bind
来绑定事件和对应的处理方法,该方法签名为: Bind(self, event, handler, source=None, id=wx.ID_ANY, id2=wx.ID_ANY)
。 各参数的含义如下:
- event: 指定事件类型,通常以
EVT_
开头,例如wx.EVT_BUTTON
表示按钮点击事件,wx.EVT_CHOICE
表示下拉框选中事件。 - handler: 可以调用的事件处理方法,如果是
None
会取消该事件绑定。 - source: 事件来源对象,例如一个窗体里面有很多按钮,就可以通过该参数来区分不同的事件来源。
- id: 通过id来区分事件来源。
- id2: 如果希望将事件处理方法绑定到很多ID上,可以使用此参数。
先来看个简单的小例子。
import wx
class MyFrame(wx.Frame):
def __init__(self):
super().__init__(None, title='事件绑定')
self.count = 0
self.btn_add_counter = wx.Button(self, label='点击了0次')
self.btn_reset_counter = wx.Button(self, label='重置计数器')
self.btn_show_counter = wx.Button(self, label='显示计数器')
self.sizer = wx.BoxSizer(wx.VERTICAL)
self.sizer.AddStretchSpacer()
self.sizer.Add(self.btn_add_counter, flag=wx.ALIGN_CENTER_HORIZONTAL|wx.BOTTOM, border=10)
self.sizer.Add(self.btn_reset_counter, flag=wx.ALIGN_CENTER_HORIZONTAL|wx.BOTTOM, border=10)
self.sizer.Add(self.btn_show_counter, flag=wx.ALIGN_CENTER_HORIZONTAL)
self.sizer.AddStretchSpacer()
self.SetSizer(self.sizer)
self.Bind(wx.EVT_BUTTON, self._add_counter, self.btn_add_counter)
self.Bind(wx.EVT_BUTTON, self._reset_counter, id=self.btn_reset_counter.Id)
self.btn_show_counter.Bind(wx.EVT_BUTTON, self._show_counter)
def _add_counter(self, e):
print(e.Id == self.btn_add_counter.Id)
self.count += 1
self.btn_add_counter.SetLabel(f'点击了{self.count}次')
def _reset_counter(self, e):
print(e.Id == self.btn_reset_counter.Id)
self.count = 0
self.btn_add_counter.SetLabel('点击了0次')
def _show_counter(self, e):
print(e.Id == self.btn_show_counter.Id)
wx.MessageBox(f'当前计数器:{self.count}', '提示')
app = wx.App()
MyFrame().Show()
app.MainLoop()
运行上面的代码,如图所示。
我们通过设置一个垂直排列的BoxSizer将三个按钮放置在窗体的中心位置,并让按钮之间保留10像素的垂直间距。
点击第一个按钮,按钮的文字会相应变化。点击第二个按钮,count的值重置为0,第一个按钮的文字也会变化。点击第三个按钮,会弹窗提示当前count数值。并且控制台输出了很多的True,这表明传递给事件处理方法的对象e就是当前的事件来源对象。
三个按钮使用了三种方式来实现事件绑定。第一个按钮通过source参数来指定事件来源,第二个按钮通过id来指定事件来源,第三个按钮没有采用事件冒泡传播的思路,而是直接将事件处理方法绑定到了按钮上。
在熟悉了如何进行事件绑定之后,让我们来了解一下事件处理的流程。
事件处理流程
当事件发生时, wx.EvtHandler.ProcessEvent
方法会自动调用第一个对应的事件处理方法。其查找过程如下。
wx.AppConsole.FilterEvent
调用,如果返回值不是-1,事件处理会立即结束。- 如果
wx.EvtHandler.SetEvtHandlerEnabled
禁用了事件处理方法,那么接下来的三步会跳过,事件处理方法将会在 第5步恢复。 - 如果事件来源自
wx.Window
并且关联一个验证器,wx.Validator
将有机会处理该事件。 - 通过
Bind
方法动态绑定的事件处理方法,会开始分析。 - 事件表中包含了事件类型对应的事件处理方法,另外,事件来源的父类定义的事件处理方法,也会开始执行。
- 如果步骤1~4中找到了一个事件处理方法,那么事件处理方法将继续进行链式查找。通常事件处理方法只有一个,将执行到 下一步。
- 如果事件源是一个
wx.Window
对象,并且事件允许冒泡传播,针对事件源的父类,事件处理将从步骤1开始再次进行。 如果事件源不是wx.Window
对象但还有一个事件处理方法,并且事件源的父类是一个Window对象,此事件将传递给父类。 - 如果事件依然没有处理,
wx.App
将处理此事件。
事件的传播机制
继承自 wx.CommandEvent
的事件如果没有处理,默认会自动传播给父组件。其他事件类型也具备类似的传播特性, 本质上是通过 wx.Event.ShouldPropagate
方法来检测是否能够传播。
如果 wx.CommandEvent
类型的事件传播过程中如果发现对应的父组件是一个对话框,就会立即停止传播,如果父组件是 Frame,则会正常传播。如果不希望在当前窗体上进行事件传播,可以给窗体设置一个标志 wx.Window.SetExtraStyle
(wx.WS_EX_BLOCK_EVENTS)。
只有继承自 wx.CommandEvent
的事件才会向上传播。
通过使用wx.EvtHandler.SetNextHandler
和wx.EvtHandler.SetPreviousHandler
方法可以建立事件处理双向链表, 如图所示。
如果A.ProcessEvent
调用了但并没有处理事件,B.ProcessEvent
将会执行,以此类推。
自定义事件
除了使用系统预定义的事件类型,我们还可以借助wx.lib.newlib这个模块,来很方便的自定义事件,来看一个例子。
import wx
import random
import wx.lib.newevent
class GuessNumberPanel(wx.Panel):
GuessResultEvent, EVT_GUESS_RESULT = wx.lib.newevent.NewCommandEvent()
def __init__(self, parent):
super().__init__(parent)
self.number = random.randint(1, 100)
self.sizer = wx.BoxSizer(wx.VERTICAL)
self.sizer.Add(wx.StaticText(self, label='猜一个1到100之间的整数'), flag=wx.ALIGN_CENTER_HORIZONTAL|wx.TOP, border=20)
self.btn_reset = wx.Button(self, label='重新开始')
self.sizer.Add(self.btn_reset, flag=wx.ALIGN_CENTER_HORIZONTAL|wx.TOP, border=20)
self.tc_guess = wx.TextCtrl(self)
self.sizer.Add(self.tc_guess, flag=wx.ALIGN_CENTER_HORIZONTAL|wx.TOP, border=20)
self.btn_guess = wx.Button(self, label='试一试')
self.sizer.Add(self.btn_guess, flag=wx.ALIGN_CENTER_HORIZONTAL|wx.TOP|wx.BOTTOM, border=20)
self.btn_guess.Bind(wx.EVT_BUTTON, self._check_result)
self.btn_reset.Bind(wx.EVT_BUTTON, self._reset)
self.SetSizer(self.sizer)
self.SetBackgroundColour('#0099CC')
def _reset(self, _):
self.tc_guess.Clear()
self.number = random.randint(1, 100)
def _check_result(self, _):
correct = False
message = ''
try:
guessed_number = int(self.tc_guess.Value)
if guessed_number < 1 or guessed_number > 100:
message = '数字需要在1到100之间'
elif guessed_number < self.number:
message = '小了'
elif guessed_number == self.number:
message = '正确'
correct = True
else:
message = '大了'
except:
message = '请输入数字'
finally:
event = self.GuessResultEvent(self.Id, is_correct=correct, message=message)
wx.PostEvent(self.GetEventHandler(), event)
class GuessNumberFrame(wx.Frame):
def __init__(self):
super().__init__(None, title='猜数字')
self.guess_panel = GuessNumberPanel(self)
self.st_guess_result = wx.StaticText(self)
self.sizer = wx.BoxSizer()
self.sizer.Add(self.guess_panel, proportion=1, flag=wx.EXPAND)
self.sizer.Add(self.st_guess_result, proportion=1, flag=wx.TOP|wx.LEFT, border=30)
self.SetSizer(self.sizer)
self.Bind(self.guess_panel.EVT_GUESS_RESULT, self._handle_result)
def _handle_result(self, e):
print(e.message)
if e.is_correct:
self.st_guess_result.SetLabel('猜对了!')
else:
self.st_guess_result.SetLabel(e.message)
app = wx.App()
GuessNumberFrame().Show()
app.MainLoop()
这是一个猜数字的小游戏,游戏的截图如下所示。
我们来分析上面的代码。首先看GuessNumberFrame这个类,初始化的时候创建了一个GuessNumberPanel实例和一个StaticText实例,然后使用BoxSizer把两者横向排列成一行,通过proportion参数来设定各自占据一半的空间,Panel还通过wx.EXPAND实现了纵向扩展。
然后通过self.Bind(self.guess_panel.EVT_GUESS_RESULT, self._handle_result)来处理自定义事件。在_handle_result方法中可以看到,传进去的事件对象有message和is_correct两个属性,这是如何实现的呢?我们来看GuessNumberPanel这个类。
通过BoxSizer将四个控件垂直排列成一列,并通过border参数来设置间距,关于具体的布局方式可以参考前面的文章,这里就不再说明了。重点是看_check_result方法,我们提取文本框的输入值,和当前的随机数进行比较,将对应的结果放到一个自定义事件GuessResultEvent里面,这个事件在前面已经定义好了,这里只需要构造一个实例即可,所需要的参数有事件源的Id和一些自定义参数,这里我们添加了is_correct和message两个参数,GuessNumberFrame里面相对应的事件处理方法就可以提取到这两个参数了。
从以上的例子可以看到,自定义事件实际上有以下几步构成。
- 定义事件类型。通过wx.lib.newevent.NewCommandEvent()方法实现,如果不希望事件传播,可以使用
wx.lib.newevent.NewEvent()
方法。 - 发送事件。注意:如果是wx.lib.newevent.NewEvent()创建的,就不需要Id参数
event = self.GuessResultEvent(self.Id, is_correct=correct, message=message)
wx.PostEvent(self.GetEventHandler(), event)
3. 后面就是绑定事件了,这个上文已经提及过。
在熟悉了事件处理机制之后,接下来我们就可以使用可视化设计工具快速创建界面了,下一篇将详细介绍。