wxpython编程之 事件处理原理

 原文出自:http://lihf198628.blog.163.com/blog/static/1138145200862633419155/

第三章 在事件驱动环境中工作


事件处理是wxPython程序工作的基本机制。主要执行事件处理的工作称为事件驱动。在这章中我们将讨论什么是事件驱动应用程序,它与传统的应用程序有什么不同。我们将对在GUI编程中所使用的概念和术语提供一些介绍,包括与用户交互,工具包和编程逻辑。也将包括典型事件驱动程序的生命周期。
事件就是发生在你的系统中的事,你的应用程序通过触发相应的功能以响应它。事件可以是低级的用户动作,如鼠标移动或按键按下,也可以是高级的用户动作(定义在wxPython的窗口部件中的),如单击按钮或菜单选择。事件可以产生自系统,如关机。你甚至可以创建你自己的对象去产生你自己的事件。wxPython应用程序通过将特定类型的事件和特定的一块代码相关联来工作,该代码在响应事件时执行。事件被映射到代码的过程称为事件处理。
本章将说明事件是什么,你如何写响应一个事件的代码,以及wxPython在事件发生的时候是如何知道去调用你的代码的。我们也将说明如何将定制的事件增加到wxPython库中,该库包含了关于用户和系统行为的标准事件的一个列表。

3.1 要理解事件,我们需要知道哪些术语?

本章包含了大量的术语,很多都是以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类,它允许它的实例在一个特定类型,一个事件源,和一个事件处理器之间创建绑定。注意,这个类与先前定义的事件处理函数或方法不是同一个东西。

3.2 什么是事件驱动编程?

事件驱动程序主要是一个控制结构,它接受事件并响应它们。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更多的细节,我们把事件响应的基本单元“事件处理器”作为开始。

3.2.1 编写事件处理器

在你的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()
上面的伪代码意思是如果没有未处理的消息,则做一些空闲时做的事;如果有消息进入,那么将这个消息分派给适当的事件处理方法。

3.2.2 设计事件驱动程序

对于事件驱动程序的设计,由于没有假设事件何时发生,所以程序员将大量的控制交给了用户。你的wxPython程序中的大多数代码通过用户或系统的行为被直接或间接地执行。例如在用户选择了一个菜单项、或按下一个工具栏按钮、或按下了特定的按键组合后,你的程序中有关保存工作的代码被执行了。
另一方面,事件驱动体系通常是分散性的。响应一个窗口部件事件的代码通常不是定义在该部件的定义中的。例如,响应一个按钮单击事件的代码不必是该按钮定义的一部分,而可以存在在该按钮所附的框架中或其它地方。当与面向对象设计结合时,这个体系导致了松散和高度可重用的代码。你将会发现Python的灵活使得重用不同的wxPython应用程序的通常的事件处理器和结构变得非常容易。

3.2.3 事件触发

在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类创建,它是定期的事件。
通常,事件对象需要使用事件绑定器和事件处理系统将它们传递给相关的事件处理器。

3.3 如何将事件绑定到处理器?

事件绑定器由类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类中。

3.3.1 使用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, "&Exit...")
        menuBar.Append(menu1, "&File")
        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对象放入事件处理系统中以便立即处理。

3.4 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用来决定如何响应事件的过程来得到回答。

你可能感兴趣的:(python)