第三章 在事件驱动环境中开发
事件处理是wxPython
程序工作的基本机制。主要执行事件处理的工作称为事件驱动。在这章中我们将讨论什么是事件驱动应用程序,它与传统的应用程序有什么不同。我们将对在GUI
编程中所使用的概念和术语提供一些介绍,包括与用户交互,工具包和编程逻辑。也将包括典型事件驱动程序的生命周期。
事件就是发生在你的系统中的事,你的应用程序通过触发相应的功能以响应它。事件可以是低级的用户动作,如鼠标移动或按键按下,也可以是高级的用户动作(定义在wxPython
的窗口部件中的),如单击按钮或菜单选择。事件可以产生自系统,如关机。你甚至可以创建你自己的对象去产生你自己的事件。wxPython
应用程序通过将特定类型的事件和特定的一块代码相关联来工作,该代码在响应事件时执行。事件被映射到代码的过程称为事件处理。
本章将说明事件是什么,你如何写响应一个事件的代码,以及wxPython
在事件发生的时候是如何知道去调用你的代码的。我们也将说明如何将定制的事件增加到wxPython
库中,该库包含了关于用户和系统行为的标准事件的一个列表。
要理解事件,我们需要知道哪些术语?
本章包含了大量的术语,很多都是以event
开头的。下表3.1是我们将要用到的术语的一个快速参考:
事件(event)
:在你的应用程序期间发生的事情,它要求有一个响应。
事件对象(event
object)
:在wxPython
中,它具体代表一个事件,其中包括了事件的数据等属性。它是类wx.Event
或其子类的实例,子类如wx.CommandEvent
和wx.MouseEvent
。
事件类型(event
type)
:wxPython
分配给每个事件对象的一个整数ID
。事件类型给出了关于该事件本身更多的信息。例如,wx.MouseEvent
的事件类型标识了该事件是一个鼠标单击还是一个鼠标移动。
事件源(event
source)
:任何wxPython
对象都能产生事件。例如按钮、菜单、列表框和任何别的窗口部件。
事件驱动(event
-driven)
:一个程序结构,它的大部分时间花在等待或响应事件上。
事件队列(event
queue)
:已发生的但未处理的事件的一个列表。
事件处理器(event
handler)
:响应事件时所调用的函数或方法。也称作处理器函数或处理器方法。
事件绑定器(event
binder)
:一个封装了特定窗口部件,特定事件类型和一个事件处理器的wxPython
对象。为了被调用,所有事件处理器必须用一个事件绑定器注册。
wx.EvtHandler
:一个wxPython
类,它允许它的实例在一个特定类型,一个事件源,和一个事件处理器之间创建绑定。注意,这个类与先前定义的事件处理函数或方法不是同一个东西。
什么是事件驱动编程?
事件驱动程序主要是一个控制结构,它接受事件并响应它们。wxPython
程序(或任何事件驱动程序)的结构与平常的Python
脚本不同。标准的Python
脚本有一个特定的开始点和结束点,程序员使用条件、循环、和函数来控制执行顺序。
从用户的角度上来看,wxPython
程序大部分时间什么也不做,一直闲着直到用户或系统做了些什么来触发这个wxPython
程序动作。wxPython
程序的结构就是一个事件驱动程序体系的例子。图3.1是事件处理循环的示意,它展示了主程序的生命、用户事件、和分派到的处理器函数。
事件驱动系统的主循环类似于客户服务呼叫中心的操作者。当没有呼叫的进入的时候,这个操作者处于等待状态。当一个事件发生的时候,如电话铃响了,这个操作者开始一个响应过程,他与客户交谈直到他获得足够的信息以分派该客户给一个合适的回答者。然后操作者等待下一个事件。
尽管每个事件驱动系统之间有一些不同,但它们有很多相似的地方。下面列出了事件驱动程序结构的主要特点:
1、在初始化设置之后,程序的大部分时间花在了一个空闭的循环之中。进入这个循环就标志着程序与用户交互的部分的开始,退出这个循环就标志结束。在wxPython
中,这个循环的方法是:wx.App.MainLoop()
,并且在你的脚本中显式地被调用。当所有的顶级窗口关闭时,主循环退出。
2、程序包含了对应于发生在程序环境中的事情的事件。事件通常由用户的行为触发,但是也可以由系统的行为或程序中其他任意的代码。在wxPython
中,所有的事件都是类wx.Event
或其子类的一个实例。每个事件都有一个事件类型属性,它使得不同的事件能够被辨别。例如,鼠标释放和鼠示按下事件都被认为是同一个类的实例,但有不同的事件类型。
3、作为这个空闭的循环部分,程序定期检查是否有任何请求响应事情发生。有两种机制使得事件驱动系统可以得到有关事件的通知。最常被wxPython
使用的方法是,把事件传送到一个中心队列,由该队列触发相应事件的处理。另一种方法是使用轮询的方法,所有可能引发事件的事件主被主过程定期查询并询问是否有没有处理的事件。
4、当事件发生时,基于事件的系统试着确定相关代码来处理该事件,如果有,相关代码被执行。在wxPython
中,原系统事件被转换为wx.Event
实例,然后使用wx.EvtHandler.ProcessEvent()
方法将事件分派给适当的处理器代码。图3.3呈现了这个过程:
事件机制的组成部分是事件绑定器对象和事件处理器。事件绑定器是一个预定义的wxPython
对象。每个事件都有各自的事件绑定器。事件处理器是一个函数或方法,它要求一个wxPython
事件实例作为参数。当用户触发了适当的事件时,一个事件处理器被调用。
下面我们将讨论有关wxPython
更多的细节,我们把事件响应的基本单元“事件处理器”作为开始。
编写事件处理器
在你的wxPython
代码中,事件和事件处理器是基于相关的窗口部件的。例如,一个按钮被单击被分派给一个基于该按钮的专用的事件处理器。为了要把一个来自特定窗口部件的事件绑定到一个特定的处理器方法,你要使用一个绑定器对象来管理这个连接。例如:
self.Bind(wx.EVT_BUTTON, self.OnClick, aButton)
上例使用了预定义的事件绑定器对象wx.EVT_BUTTON
来将aButton
对象上的按钮单击事件与方法self.OnClick
相关联起来。这个Bind()
方法是wx.EvtHandler
的一个方法,wx.EvtHandler
是所有可显示对象的父类。因此上例代码行可以被放置在任何显示类。
即使你的wxPython
程序表面上看起来在被动地等待事件,但它仍在做事。它在运行方法wx.App.MainLoop()
,该方法是一个无限的循环。MainLoop()
方法可以使用Python
伪代码表示如下:
while True:
while not self.Pending():
self.ProcessIdle()
self.DoMessage()
上面的伪代码意思是如果没有未处理的消息,则做一些空闲时做的事;如果有消息进入,那么将这个消息分派给适当的事件处理方法。
设计事件驱动程序
对于事件驱动程序的设计,由于没有假设事件何时发生,所以程序员将大量的控制交给了用户。你的wxPython
程序中的大多数代码通过用户或系统的行为被直接或间接地执行。例如在用户选择了一个菜单项、或按下一个工具栏按钮、或按下了特定的按键组合后,你的程序中有关保存工作的代码被执行了。
另一方面,事件驱动体系通常是分散性的。响应一个窗口部件事件的代码通常不是定义在该部件的定义中的。例如,响应一个按钮单击事件的代码不必是该按钮定义的一部分,而可以存在在该按钮所附的框架中或其它地方。当与面向对象设计结合时,这个体系导致了松散和高度可重用的代码。你将会发现Python
的灵活使得重用不同的wxPython
应用程序的通常的事件处理器和结构变得非常容易。
事件触发
在wxPython
中,大部分窗口部件在响应低级事件时都导致高级事件发生。例如,在一个wx.Button
上的鼠标单击导致一个EVT_BUTTON
事件的生成,该事件是wx.CommandEvent
的特定类型。类似的,在一个窗口的角中拖动鼠标将导致wxPython
为你自动创建一个wx.SizeEvent
事件。高级事件的用处是让你的系统的其它部分更容易聚焦于最有关联的事件上,而不是陷于追踪每个鼠标单击。高级事件能够封装更多关于事件的有用的信息。当你创建你自已的定制的窗口部件时,你能定义你自己的定制事件以便管理事件的处理。
在wxPython
中,代表事件的是事件对象。事件对象是类wx.Event
或其子类的一个实例。父类wx.Event
相对小且抽象,它只是包含了对所有事件的一些通常的信息。wx.Event
的各个子类都添加了更多的信息。
在wxPython
中,有一些wx.Event
的子类。表3.2包含了你将最常遇到的一些事件类。记住,一个事件类可以有多个事件类型,每个都对应于一个不同的用户行为。下表3.2是wx.Event
的重要的子类。
wx.CloseEvent
:当一个框架关闭时触发。这个事件的类型分为一个通常的框架关闭和一个系统关闭事件。 wx.CommandEvent
:与窗口部件的简单的各种交互都将触发这个事件,如按钮单击、菜单项选择、单选按钮选择。这些交互有它各自的事件类型。许多更复杂的窗口部件,如列表等则定义wx.CommandEvent
的子类。事件处理系统对待命令事件与其它事件不同。 wx.KeyEvent
:按按键事件。这个事件的类型分按下按键、释放按键、整个按键动作。 wx.MouseEvent
:鼠标事件。这个事件的类型分鼠标移动和鼠标敲击。对于哪个鼠标按钮被敲击和是单击还是双击都有各自的事件类型。 wx.PaintEvent
:当窗口的内容需要被重画时触发。 wx.SizeEvent
:当窗口的大小或其布局改变时触发。 wx.TimerEvent
:可以由类wx.Timer
类创建,它是定期的事件。
通常,事件对象需要使用事件绑定器和事件处理系统将它们传递给相关的事件处理器。
如何将事件绑定到处理器?
事件绑定器由类wx.PyEventBinder
的实例组成。一个预定义的wx.PyEventBinder
的实例被提供给所有支持的事件类型,并且在你需要的时候你可以为你定制的事件创建你自己的事件绑定器。每个事件类型都有一个事件绑定器,这意味着一个wx.Event
的子类对应多个绑定器。
在wxPython
中,事件绑定器实例的名字是全局性的。为了清楚地将事件类型与处理器联系起来,它们的名字都是以wx.EVT_
开头并且对应于使用在C++ wxWidgets
代码中宏的名字。值得强调的是,wx.EVT
绑定器名字的值不是你通过调用一个wx.Event
实例的GetEventType()
方法得到的事件类型的实际的整数码。事件类型整数码有一套完全不同的全局名,并且在实际中不常被使用。
作为wx.EVT
名字的例子,让我们看看wx.MouseEvent
的事件类型。正如我们所提到的,它们有十四个,其中的九个涉及到了基于在按钮上的敲击,如鼠标按下、鼠标释放、或双击事件。这九个事件类型使用了下面的名字:
wx.EVT_LEFT_DOWN
wx.EVT_LEFT_UP
wx.EVT_LEFT_DCLICK
wx.EVT_MIDDLE_DOWN
wx.EVT_MIDDLE_UP
wx.EVT_MIDDLE_DCLICK
wx.EVT_RIGHT_DOWN
wx.EVT_RIGHT_UP
wx.EVT_RIGHT_DCLICK
另外,类型wx.EVT_MOTION
产生于用户移动鼠标。类型wx.ENTER_WINDOW
和wx.LEAVE_WINDOW
产生于当鼠标进入或离开一个窗口部件时。类型wx.EVT_MOUSEWHEEL
被绑定到鼠标滚轮的活动。最后,你可以使用类型wx.EVT_MOUSE_EVENTS
一次绑定所有的鼠标事件到一个函数。
同样,类wx.CommandEvent
有28个不同的事件类型与之关联;尽管有几个仅针对老的Windows
操作系统。它们中的大多数是专门针对单一窗口部件的,如wx.EVT_BUTTON
用于按钮敲击,wx.EVT_MENU
用于菜单项选择。用于专门窗口部件的命令事件在part2
中讨论。
绑定机制的好处是它使得wxPython
可以很细化地分派事件,而仍然允许同类的类似事件发生并且共享数据和功能。这使得在wxPython
中写事件处理比在其它界面工具包中清细得多。
事件绑定器被用于将一个wxPython
窗口部件与一个事件对象和一个处理器函数连接起来。这个连接使得wxPython
系统能够通过执行处理器函数中的代码来响应相应窗口部件上的事件。在wxPython
中,任何能够响应事件的对象都是wx.EvtHandler
的子类。所有窗口对象都是wx.EvtHandler
的子类,因些在wxPython
应用程序中的每个窗口部件都能够响应事件。类wx.EvtHandler
也能够被非窗口部件对象所使用,如wx.App
,因此事件处理功能不是限于可显示的窗口部件。我们所说的窗口部件能响应事件的意思是:该窗口部件能够创建事件绑定,在分派期间wxPython
能够识别该事件绑定。由绑定器调用的在事件处理器函数中的实际代码不是必须位于一个wx.EvtHandler
类中。
使用wx.EvtHandler的方法工作
wx.EvtHandler
类定义的一些方法在一般情况下用不到。你会经常使用的wx.EvtHandler
的方法是Bind()
,它创建事件绑定。该方法的用法如下:
Bind(event, handler, source=None, id=wx.ID_ANY, id2=wx.ID_ANY)
Bind()
函数将一个事件和一个对象与一个事件处理器函数关联起来。参数event
是必选的,它是我们在3.3节中所说的wx.PyEventBinder
的一个实例。参数handler
也是必选的,它是一个可调用的Python
对象,通常是一个被绑定的方法或函数。处理器必须是可使用一个参数(事件对象本身)来调用的。参数handler
可以是None
,这种情况下,事件没有关联的处理器。参数source
是产生该事件的源窗口部件,这个参数在触发事件的窗口部件与用作事件处理器的窗口部件不相同时使用。通常情况下这个参数使用默认值None
,这是因为你一般使用一个定制的wx.Frame
类作为处理器,并且绑定来自于包含在该框架内的窗口部件的事件。父窗口的__init__
是一个用于声明事件绑定的方便的位置。但是如果父窗口包含了多个按钮敲击事件源(比如OK
按钮和Cancel
按钮),那么就要指定source
参数以便wxPython
区分它们。下面是该方法的一个例子:
self.Bind(wx.EVT_BUTTON, self.OnClick, button)
下例3.1演示了使用参数source
和不使用参数source
的方法,它改编自第二章中的代码:
def __init__(self, parent, id):
wx.Frame.__init__(self, parent, id, 'Frame With Button',
size=(300, 100))
panel = wx.Panel(self, -1)
button = wx.Button(panel, -1, "Close", pos=(130, 15),
size=(40, 40))
self.Bind(wx.EVT_CLOSE, self.OnCloseWindow) #1 绑定框架关闭事件
self.Bind(wx.EVT_BUTTON, self.OnCloseMe, button) #2 绑定按钮事件
def OnCloseMe(self, event):
self.Close(True)
def OnCloseWindow(self, event):
self.Destroy()
说明:
#1 这行绑定框架关闭事件到self.OnCloseWindow
方法。由于这个事件通过该框架触发且用于帧,所以不需要传递一个source
参数。
#2 这行将来自按钮对象的按钮敲击事件绑定到self.OnCloseMe
方法。这样做是为了让wxPython
能够区分在这个框架中该按钮和其它按钮所产生的事件。
你也可以使用source
参数来标识项目,即使该项目不是事件的源。例如,你可以绑定一个菜单事件到事件处理器,即使这个菜单事件严格地说是由框架所触发的。下例3.2演示了绑定一个菜单事件的例子:
#!/usr/bin/env python
import wx
class MenuEventFrame(wx.Frame):
def __init__(self, parent, id):
wx.Frame.__init__(self, parent, id, 'Menus',
size=(300, 200))
menuBar = wx.MenuBar()
menu1 = wx.Menu()
menuItem = menu1.Append(-1, " ")
menuBar.Append(menu1, " ")
self.SetMenuBar(menuBar)
self.Bind(wx.EVT_MENU, self.OnCloseMe, menuItem)
def OnCloseMe(self, event):
self.Close(True)
if __name__ == '__main__':
app = wx.PySimpleApp()
frame = MenuEventFrame(parent=None, id=-1)
frame.Show()
app.MainLoop()
Bind()
方法中的参数id
和id2
使用ID
号指定了事件的源。一般情况下这没必要,因为事件源的ID
号可以从参数source
中提取。但是某些时候直接使用ID
是合理的。例如,如果你在使用一个对话框的ID
号,这比使用窗口部件更容易。如果你同时使用了参数id
和id2
,你就能够以窗口部件的ID
号形式将这两个ID
号之间范围的窗口部件绑定到事件。这仅适用于窗口部件的ID
号是连续的。
注意:Bind()
方法出现在wx.Python2.5
中,以前版本的事件绑定中,EVT_
*的用法如同函数对象,因此你会看到如下的绑定调用:
wx.EVT_BUTTON(self, self.button.GetId(), self.OnClick)
这个方式的缺点是它不像是面向对象的方法调用。然而,这个老的样式仍可工作在2.5的版本中(因为wx.EVT
*对象仍是可调用的)。
下表3.3列出了最常使用的wx.EvtHandler
的方法:
AddPendingEvent(event)
:将这个event
参数放入事件处理系统中。类似于ProcessEvent()
,但它实际上不会立即触发事件的处理。相反,该事件被增加到事件队列中。适用于线程间的基于事件的通信。
Bind(event, handler, source=None, id=wx.ID_ANY, id2=wx.ID_ANY):
完整的说明见3.3.1节。
GetEvtHandlerEnabled()
SetEvtHandlerEnabled(
boolean)
:如果处理器当前正在处理事件,则属性为True
,否则为False
。
ProcessEvent(event)
:把event
对象放入事件处理系统中以便立即处理。
wxPython是如何处理事件的?
基于事件系统的关键组成部分是事件处理。通过它,一个事件被分派到了相应的用于相应该事件的一块代码。在这一节,我们将讨论wxPython
处理事件的过程。我们将使用小段的代码来跟踪这个处理的步骤。图3.2显示了一个带有一个按钮的简单窗口,这个按钮将被用来产生一个简单的事件。
下例3.3包含了生成这个窗口的代码。在这个代码中,通过敲击按钮和将鼠标移动到按钮上都可产生wxPython
事件。
例3.3绑定多个鼠标事件
#!/usr/bin/env python
import wx
class MouseEventFrame(wx.Frame):
def __init__(self, parent, id):
wx.Frame.__init__(self, parent, id, 'Frame With Button',
size=(300, 100))
self.panel = wx.Panel(self)
self.button = wx.Button(self.panel,
label="Not Over", pos=(100, 15))
self.Bind(wx.EVT_BUTTON, self.OnButtonClick,
self.button) #1 绑定按钮事件
self.button.Bind(wx.EVT_ENTER_WINDOW,
self.OnEnterWindow) #2 绑定鼠标位于其上事件
self.button.Bind(wx.EVT_LEAVE_WINDOW,
self.OnLeaveWindow) #3 绑定鼠标离开事件
def OnButtonClick(self, event):
self.panel.SetBackgroundColour('Green')
self.panel.Refresh()
def OnEnterWindow(self, event):
self.button.SetLabel("Over Me!")
event.Skip()
def OnLeaveWindow(self, event):
self.button.SetLabel("Not Over")
event.Skip()
if __name__ == '__main__':
app = wx.PySimpleApp()
frame = MouseEventFrame(parent=None, id=-1)
frame.Show()
app.MainLoop()
说明:
MouseEventFrame
包含了一个位于中间的按钮。在其上敲击鼠标将导致框架的背景色改变为绿色。#1绑定了鼠标敲击事件。当鼠标指针位于这个按钮上时,按钮上的标签将改变,这用#2绑定。当鼠标离开这个按钮时,标签变回原样,这用#3绑定。
通过观察上面的鼠标事件例子,我们引出了在wxPython
中的事件处理的一些问题。#1中,按钮事件由附着在框架上的按钮触发,那么wxPython
怎么知道在框架对象中查找绑定而不是在按钮对象上呢?在#2和#3中,鼠标的进入和离开事件被绑定到了按钮,为什么这两个事件不能被绑到框架上呢。这些问题将通过检查wxPython
用来决定如何响应事件的过程来得到回答。
理解事件处理过程
wxPython
的事件处理过程被设计来简化程序员关于事件绑定的创建,使他们不必考虑哪些不重要的事件。 隐藏在简化设计之下的底层机制是有些复杂的。接下来,我们将跟踪关于按钮敲击和鼠标进入事件的过程。
图3.3显示了事件处理过程的一个基本的流程。矩形代表过程的开始和结束,环形代表各种wxPython
对象(它们是这个过程的一部分),棱形代表判断点,带条的矩形代表实际的事件处理方法。
事件处理过程开始于触发事件的对象。通常,wxPython
首先在触发对象中查找匹配事件类型的被绑定的处理器函数。如果找到,则相应的方法被执行。否则,wxPython
将检查该事件是否传送到了上一级的容器。如果是的话,父窗口部件将被检查,这样一级一级向上寻找,直到wxPython
找到了一个处理器函数或到达了顶级窗口。如果事件没有传播,在处理过程完成之前,wxPython
仍将为了处理器函数而检查应用程序对象。
当事件处理器运行时,过程通常就结束了。然而,函数可以告诉wxPython
去继续查找处理器。 下面让我们仔细观察一下这个过程的每一个步骤。我们的每步分析都有图3.3的一个相关略图。
第一步,创建事件
这个过程开始于事件被创建时。 在wxPython
架构中已经创建了大多数的事件类型,它们用于响应特定的用户动作或系统通知。例如,当wxPython
通知“鼠标移进了一个新窗口部件对象时”,鼠标进入事件被触发,鼠标敲击事件在鼠标按下或释放后被创建。
事件首先被交给创建事件的对象。对于按钮敲击,这个对象是按钮;对于鼠标进入事件,这个对象是所进入的窗口部件。
第二步,确定事件对象是否被允许处理事件。
事件处理过程检查的下一步是看相关窗口部件当前是否被允许去处理事件。 通过调用wx.EvtHandler
的SetEvtHandlerEnabled(boolean)
方法,一个窗口可以被设置为允许或不允许事件处理。不允许事件处理的结果是该窗口部件在事件处理中被完全绕过,与该对象关联的绑定对象也不会被搜索,并且在这步中的处理没有向下的分支。
在事件处理器级使一个窗口部件有效或无效与在用户界面级(UI)
不一样。在UI
级使一个窗口部件无效或有效,使用wx.Window
的方法Disable()
和Enable()
。在UI
级使一个窗口部件无效意味用户不能与这个无效的窗口部件交互。通常无效的窗口部件在屏幕上以灰化的状态表示。一个在UI
级无效的窗口不能产生任何事件;但是,如果它对于别的事件是容器的级别,它仍然能够处理它接受到的事件。本节的剩余内容,我们将在wx.EvtHandler
层面上使用有效和无效,这涉及到窗口部件是否被允许处理事件。
对于初始对象有效或无效状态的检查,这发生在ProcessEvent()
方法中,该方法由wxPython
系统调用以开始和处理事件分配机制。我们将在事件处理过程中一再看到ProcessEvent()
方法,它是类wx.EvtHandler
中的方法,它实际上执行图3.3所描绘的大量事件处理。如果ProcessEvent()
方法最后完成了事件处理,则ProcessEvent()
返回True
。如果一个处理器被发现和组合事件被处理,则认为处理完成。处理器函数可以通过调用wx.Event
的Skip()
方法来显式地请求进一步的处理。另处,如果初始对象是wx.Window
的一个子类,那么它能够使用一个称为validator
的对象来过滤事件。Validator
将在第九章中详细讨论。
第三步 定位绑定器对象
如图3.6所示
然后ProcessEvent()
方法寻找一个绑定器对象,该绑定器对象确定当前对象和事件类型之间的绑定。
如果对象自身的绑定器没有被找到,那么向上到该对象的超类中去寻找。如果一个绑定器对象被发现,wxPython
调用相关的处理器函数。在处理器被调用后,该事件的事件处理停止,除非处理器函数显式地要求作更多的处理。
在例子3.3中,因为在按钮对象,绑定器对象wx.EVT_ENTER_WINDOW
,和相关的方法OnEnterWindow()
之间定义了绑定,所以鼠标进入事件被捕获,OnEnterWindow()
方法被调用。由于我们没有绑定鼠标敲击事件 wx.EVT_LEFT_DOWN
,在这种情况下,wxPython
将继续搜索。
第四步 决定是否继续处理
如图3.7所示
在调用了第一个事件处理器之后,wxPython
查看是否有进一步的处理要求。事件处理器通过调用wx.Event
的方法Skip()
要求更多的处理。如果Skip()
方法被调用,那么处理将继续,并且任何定义在超类中的处理器在这一步中被发现并执行。Skip()
方法在处理中的任一点或处理器所调用的任何代码中都可以被调用。Skip()
方法在事件实例中设置一个标记,在事件处理器方法完成后,wxPython
检查这个标记。在例3.3中,OnButtonClick()
不调用Skip()
,因此在那种情况下,处理器方法结束后,事件处理完成。在另两个事件处理器中调用了Skip()
,所以系统将保持搜索“匹配事件绑定”,最后对于原窗口部件的鼠标进入和离开事件调用默认的功能,如鼠标位于其上的事件。
第五步 决定是否展开 如图3.8所示
最后,wxPython
决定是否将事件处理向上展开到容器级以发现一个事件处理器。所谓的容器级是从一个特定的窗口部件到顶层框架的路径,这个路径是从窗口部件到它的父容器,一直向上沿升。
如果当前对象没有关于该事件的一个处理器,或如果处理器调用了Skip()
,wxPython
将决定是否这个事件将沿容器级向上展开。如果决定不,那么在wx.App
实例中再找寻一次处理器,然后停止。如果决定是,则事件处沿该窗口的容器级向上搜索,直到发现适当的绑定,或到达顶层框架对象,或到达一个wx.Dialog
对象(即使这个对话框不是顶级的)。如果ProcessEvent()
返回True
,事件则被认为发现了一个适当的绑定,这表示处理完成。到达一个wx.Dialog
停止的目的是防止父框架被来自对话框的无关的或未预期的假事件干扰。
一个事件是否向上展开至容器级,这是每个事件实例的一个动态属性,尽管实际上默认值几乎总是使用那几个。默认情况,只有wx.CommandEvent
及其子类的实例向上展开至容器级。其它的所有事件不这样做。
在例3.3中,按钮敲击事件得到处理。在wx.Button
上敲击鼠标产生一个命令类型的事件wx.EVT_BUTTON
。由于wx.EVT_BUTTON
属于一个wx.CommandEvent
,所以wxPython
在这个按钮对象中?已鞍蠖ㄊО芎螅蛏险箍寥萜骷叮仁前磁サ母复翱趝。由于panel
中没有相匹配的绑定,所以又向上至panel
的父窗口frame
。由于frame
中有匹配的绑定,所以ProcessEvent()
调用相关函数 OnButtonClick()
。
第五步同时也说明了为什么鼠标进入和离开事件必须被绑定到按钮而不是框架。由于鼠标事件不是wx.CommandEvent
的子类,所以鼠标进入和离开事件不向上展开至容器级。如果鼠标进入和离开事件被绑定到了框架,那么当鼠标进入或离开框架时,wxPython
触发鼠标进入或离开事件。
在这种方式中,命令事件是被优先对待的。因为它们被认为是高级事件,表示用户正在应用程序空间中做一些事,而非窗口系统。窗口系统类型事件只对窗口部件感兴趣,而应用级事件对容器级。这个规则不防碍我们在任何地方声明绑定,不管被绑定的是什么对象或什么对象定义事件处理器。例如,即使这个绑定的鼠标敲击事件针对于按钮对象,而绑定则被定义在这个框架类中,且调用这个框架内的方法。换句话说,低级的非命令事件通常用于窗口部件或一些系统级的通知,如鼠标敲击、按键按下、绘画请求、调整大小或移动。另一方面,命令事件,如在按钮上敲击鼠标、或列表框上的选择,通常由窗口部件自己生成。例如,在适当的窗口部件上按下和释放鼠标后,按钮命令事件产生。
最后,如果遍历了容器级后,事件没有被处理,那么应用程序的wx.App
对象调用ProcessEvent()
。默认情况下,这什么也不做,但是你可以给你的wx.App
增加事件绑定,以便以非标准的方式来传递事件。例如,假如你在写一个GUI
构建器,你可能想把你构建器窗口中的事件传到你的代码窗口中,即使它们都是顶级窗口。方法之一是捕获应用程序对象中的事件,并把它们传递到代码窗口上。
使用Skip()方法
事件的第一个处理器函数被发现并执行完后,该事件处理将终止,除非在处理器返回之前调用了该事件的Skip()
方法。调用Skip()
方法允许另外被绑定的处理器被搜索,搜索依据3.4.1节中的第四步中声明的规则,因此父类和父窗口被搜索,就如同这第一个处理器不存在一样。在某些情况下,你想继续处理事件,以便原窗口部件的默认行为和你定制的处理能被执行。例3.4显示了一个使用Skip()
的例子,它使得程序能够同时响应同一按钮上的鼠标左按键按下和按钮敲击。
例3.4 同时响应鼠标按下和按钮敲击
#!/usr/bin/env python
import wx
class DoubleEventFrame(wx.Frame):
def __init__(self, parent, id):
wx.Frame.__init__(self, parent, id, 'Frame With Button',
size=(300, 100))
self.panel = wx.Panel(self, -1)
self.button = wx.Button(self.panel, -1, "Click Me", pos=(100, 15))
self.Bind(wx.EVT_BUTTON, self.OnButtonClick,
self.button) #1 绑定按钮敲击事件
self.button.Bind(wx.EVT_LEFT_DOWN, self.OnMouseDown) #2 绑定鼠标左键按下事件
def OnButtonClick(self, event):
self.panel.SetBackgroundColour('Green')
self.panel.Refresh()
def OnMouseDown(self, event):
self.button.SetLabel("Again!")
event.Skip() #3 确保继续处理
if __name__ == '__main__':
app = wx.PySimpleApp()
frame = DoubleEventFrame(parent=None, id=-1)
frame.Show()
app.MainLoop()
#1 这行绑定按钮敲击事件到OnButtonClick()
处理器,这个处理器改变框架的背景色。
#2 这行绑定鼠标左键按下事件到OnMouseDown()
处理器,这个处理器改变按钮的标签文本。由于鼠标左键按下事件不是命令事件,所以它必须被绑定到按钮(self.button.Bind
)而非框架(self.Bind
)。
当用户在按钮上敲击鼠标时,通过直接与底层操作系统交互,鼠标左键按下事件首先被产生。通常情况下,鼠标左键按下事件改变按钮的状态,随着鼠标左键的释放,产生了wx.EVT_BUTTON
敲击事件。由于行#3的Skip()
语句,DoubleEventFrame
维持处理。没有Skip()
语句,事件处理规则发现在#2创建的绑定,而在按钮能产生wx.EVT_BUTTON
事件之前停止。由于Skip()
的调用,事件处理照常继续,并且按钮敲击被创建。
记住,当绑定低级事件时如鼠标按下或释放,wxPython
期望捕获这些低级事件以便生成进一步的事件,为了进一步的事件处理,你必须调用Skip()
方法,否则进一步的事件处理将被阻止。
在应用程序对象中还包含哪些其它的属性?
要更直接地管理主事件循环,你可以使用一些wx.App
方法来修改它。例如,按你的计划,你可能想开始处理下一个有效的事件,而非等待wxPython
去开始处理。如果你正在执行一个长时间的过程,并且不想图形界面被冻结,那么这个特性是必要的,通常你不需要使用这节中的这些方法,但是,这些性能有时是很重要的。
下表3.4列出了你可以用来修改主循环的wx.App
方法:
Dispatch()
:迫使事件队列中的下一个事件被发送。通过MainLoop()
使用或使用在定制的事件循环中。 Pending()
:如果在wxPython
应用程序事件队列中有等待被处理的事件,则返回True
。 Yield(onlyIfNeeded
=False)
:允许等候处理的wxWidgets
事件在一个长时间的处理期间被分派,否则窗口系统将被锁定而不能显示或更新。如果等候处理的事件被处理了,则返回True
,否则返回False
。 onlyIfNeeded
参数如果为True
,那么当前的处理将让位于等候处理的事件。如果该参数为False
,那么递归调用Yield
是错误的。 这里也有一个全局函数wx.SafeYield()
,它阻止用户在Yield
期间输入数据(这通过临时使用来输入的窗口部件无效来达到目的),以免干扰Yield
任务。
另一管理事件的方法是通过定制的方式,它创建你自己的事件类型,以匹配你的应用程序中特定的数据和窗口部件。下一节我们将讨论如何创建你自己的定制事件。
如何创建自己的事件?
尽管这是一个更高级的主题,但是我们将在这里讨论定制事件。当你第一次阅读的时候,你可以跳过并且以后再回过头来读。 为了要与wxPython
提供的事件类相区别,你可以创建你自己定制的事件。你可以定制事件以响应哪些针对你的应用程序的数据更新或其它改变,此处定制的事件必须负责你的自定义数据。创建定制的事件类的另一个原因是:你可以针对所定制的窗口部件,使用它自己独特的命令事件类型。下一节中,我们将看一个定制窗口部件的例子。
为一个定制的窗口部件定义一个定制的事件
。
图3.9显示了这个窗口部件,一个画板(panel)
包含了两个按钮。自定义的事件TwoButtonEvent
仅当用户敲击了这两个按钮之后被触发。这个事件包含了一个关于用户在该部件上敲击次数的计数。
创建自定义事件的步骤:
1、定义一个新的事件类,它是wxPython
的wx.PyEvent
类的子类。如果你想这个事件被作为命令事件,你可以创建wx.PyCommandEvent
的子类。像许多wxPython
中的覆盖一样,一个类的py
版本使得wxWidget
系统明白用Python
写的覆盖C++方法的方法。
2、创建一个事件类型和一个绑定器对象去绑定该事件到特定的对象。
3、添加能够建造这个新事件实例的代码,并且使用ProcessEvent()
方法将这个实例引入事件处理系统。一旦该事件被创建,你就可以像使用其它的wxPython
事件一样创建绑定和处理器方法。
下例3.5显示了管理窗口部件的代码:
import wx
class TwoButtonEvent(wx.PyCommandEvent): #1 定义事件
def __init__(self, evtType, id):
wx.PyCommandEvent.__init__(self, evtType, id)
self.clickCount = 0
def GetClickCount(self):
return self.clickCount
def SetClickCount(self, count):
self.clickCount = count
myEVT_TWO_BUTTON = wx.NewEventType() #2 创建一个事件类型
EVT_TWO_BUTTON = wx.PyEventBinder(myEVT_TWO_BUTTON, 1) #3 创建一个绑定器对象
class TwoButtonPanel(wx.Panel):
def __init__(self, parent, id=-1, leftText="Left",
rightText="Right"):
wx.Panel.__init__(self, parent, id)
self.leftButton = wx.Button(self, label=leftText)
self.rightButton = wx.Button(self, label=rightText,
pos=(100,0))
self.leftClick = False
self.rightClick = False
self.clickCount = 0
#4 下面两行绑定更低级的事件
self.leftButton.Bind(wx.EVT_LEFT_DOWN, self.OnLeftClick)
self.rightButton.Bind(wx.EVT_LEFT_DOWN, self.OnRightClick)
def OnLeftClick(self, event):
self.leftClick = True
self.OnClick()
event.Skip() #5 继续处理
def OnRightClick(self, event):
self.rightClick = True
self.OnClick()
event.Skip() #6 继续处理
def OnClick(self):
self.clickCount += 1
if self.leftClick and self.rightClick:
self.leftClick = False
self.rightClick = False
evt = TwoButtonEvent(myEVT_TWO_BUTTON, self.GetId()) #7 创建自定义事件
evt.SetClickCount(self.clickCount) # 添加数据到事件
self.GetEventHandler().ProcessEvent(evt) #8 处理事件
class CustomEventFrame(wx.Frame):
def __init__(self, parent, id):
wx.Frame.__init__(self, parent, id, 'Click Count: 0',
size=(300, 100))
panel = TwoButtonPanel(self)
self.Bind(EVT_TWO_BUTTON, self.OnTwoClick, panel) #9 绑定自定义事件
def OnTwoClick(self, event): #10 定义一个事件处理器函数
self.SetTitle("Click Count: %s" % event.GetClickCount())
if __name__ == '__main__':
app = wx.PySimpleApp()
frame = CustomEventFrame(parent=None, id=-1)
frame.Show()
app.MainLoop()
说明:
#1 这个关于事件类的构造器声明为wx.PyCommandEvent
的一个子类。 wx.PyEvent
和wx.PyCommandEvent
是wxPython
特定的结构,你可以用来创建新的事件类并且可以把C++类和你的Python
代码连接起来。如果你试图直接使用wx.Event
,那么在事件处理期间wxPython
不能明白你的子类的新方法,因为C++事件处理不了解该Python
子类。如果你wx.PyEvent
,一个对该Python
实例的引用被保存,并且以后被直接传递给事件处理器,使得该Python
代码能被使用。
#2 全局函数wx.NewEventType()
的作用类似于wx.NewId()
;它返回一个唯一的事件类型ID
。这个唯一的值标识了一个应用于事件处理系统的事件类型。
#3 这个绑定器对象的创建使用了这个新事件类型作为一个参数。这第二个参数的取值位于[0,2]之间,它代表wxId
标识号,该标识号用于wx.EvtHandler.Bind()
方法去确定哪个对象是事件的源。
#4 为了创建这个新的更高级的命令事件,程序必需响应特定的用户事件,例如,在每个按钮对象上的鼠标左键按下。依据哪个按钮被敲击,该事件被绑定到OnLeftClick()
和OnRightClick()
方法。处理器设置了布尔值,以表明按键是否被敲击。
#5 #6 Skip()
的调用允许在该事件处理完成后的进一步处理。在这里,这个新的事件不需要skip
调用;它在事件处理器完成之前被分派了(self.OnClick())
。但是所有的鼠标左键按下事件需要调用Skip()
,以便处理器不把最后的按钮敲击挂起。这个程序没有处理按钮敲击事件,但是由于使用了Skip()
,wxPython
在敲击期间使用按钮敲击事件来正确地绘制按钮。如果被挂起了,用户将不会得到来自按钮按下的反馈。
#7 如果两个按钮都被敲击了,该代码创建这个新事件的一个实例。事件类型和两个按钮的ID
作为构造器的参数。通常,一个事件类可以有多个事件类型,尽管本例中不是这样。
#8 ProcessEvent()
的调用将这个新事件引入到事件处理系统中,ProcessEvent()
的说明见3.4.1节。GetEventHandler()
调用返回wx.EvtHandler
的一个实例。大多数情况下,返回的实例是窗口部件对象本身,但是如果其它的wx.EvtHandler()
方法已经被压入了事件处理器堆栈,那么返回的将是堆栈项的项目。
#9 该自定义的事件的绑定如同其它事件一样,在这里使用#3所创建的绑定器。
#10 这个例子的事件处理器函数改变窗口的标题以显示敲击数。
至此,你的自定义的事件可以做任何预先存在的wxPython
事件所能做的事,比如创建不同的窗口部件,它们响应同样的事件。创建事件是wxPython
的定制的一个重要部分。
总结
1、wxPython
应用程序使用基于事件的控制流。应用程序的大部分时间花费在一个主循环中,等待事件并分派它们到适当的处理器函数。
2、所有的wxPython
事件是wx.Event
类的子类。低级的事件,如鼠标敲击,被用来建立高级的事件,如按钮敲击或菜单项选择。这些由wxPython
窗口部件引起的高级事件是类wx.CommandEvent
的子类。大多的事件类通过一个事件类型字段被进一步分类,事件类型字段区分事件。
3、为了捕获事件和函数之间的关联,wxPython
使用类wx.PyEventBinder
的实例。类wx.PyEventBinder
有许多预定义的实例,每个都对应于一个特定的事件类型。每个wxPython
窗口部件都是类wx.EvtHandler
的子类。类wx.EvtHandler
有一个方法Bind()
,它通常在初始化时被调用,所带参数是一个事件绑定器实例和一个处理器函数。根据事件的类型,别的wxPython
对象的ID
可能也需要被传递给Bind()
调用。
4、事件通常被发送给产生它们的对象,以搜索一个绑定对象,这个绑定对象绑定事件到一个处理器函数。如果事件是命令事件,这个事件沿容器级向上传递直到一个窗口部件被发现有一个针对该事件类型的处理器。一旦一个事件处理器被发现,对于该事件的处理就停止,除非这个处理器调用了该事件的Skip()
方法。你可以允许多个处理器去响应一个事件,或去核查该事件的所有默认行为。主循环的某些方面可以使用wx.App
的方法来控制。
5、在wxPython
中可以创建自定义事件,并作为定制(自定义)的窗口部件的行为的一部分。自定义的事件是类wx.PyEvent
的子类,自定义的命令事件是类wx.PyCommandEvent
的子类。为了创建一个自定义事件,新的类必须被定义,并且关于每个事件类型(这些事件类型被这个新类所管理)的绑定器必须被创建。最后,这个事件必须在系统的某处被生成,这通过经由ProcessEvent()
方法传递一个新的实例给事件处理器系统来实现。
在本章中,我们已经讨论了应用程序对象,它们对于你的wxPython
应用程序是最重要的。在下一章,我们将给你看一个有用的工具,它是用wxPython
写成的,它将帮助你使用wxPython
进行开发工作。