wxPython官方文档中文翻译 - 事件和事件处理

129 阅读13分钟

官方文档:docs.wxpython.org/events_over…

和所有其他图形用户界面(GUI)框架一样,wxPython应用程序中的流程控制也是基于事件的:程序通常会响应由用户生成的事件来执行其大部分操作。这些事件可以通过直接使用输入设备(如键盘、鼠标、操纵杆)来触发,或者更常见的是,由一个标准控件将此类输入事件综合成更高级别的事件来触发:例如,当用户在wx.Button上按下鼠标左键,然后在不按下Esc键的情况下松开鼠标左键时,wx.Button就会生成一个点击事件。也存在一些并不直接对应于用户操作的事件,比如wx.TimerEvent

但在所有情况下,wxPython都以统一的方式来表示这些事件,并且无论这些事件源自何处,都允许你以相同的方式对它们进行处理。而且,虽然这些事件通常是由wxPython自身生成的,但你也可以自行生成事件,这在使用自定义事件时(请参阅“自定义事件概述”)尤为有用。

更确切地说,每个事件是由以下内容来描述的:

  • 事件类型:这仅仅是一个EventType类型的值,它能唯一标识事件的类型。例如,点击按钮、从列表框中选择一项以及在键盘上按下一个键,都会生成具有不同事件类型的事件。
  • 事件类:由事件携带:每个事件都有一些与其相关联的信息,这些数据由一个从wx.Event派生的类的对象来表示。不同类型的事件可以使用相同的事件类,例如,按钮点击事件和列表框选择事件都使用wx.CommandEvent类(所有其他简单的控件事件也是如此),但按键事件使用wx.KeyEvent,因为与之相关联的信息是不同的。
  • 事件源wx.Event存储了生成该事件的对象,对于窗口而言,还存储了其标识符(请参阅“窗口标识符”)。由于通常会有多个对象生成相同类型的事件(例如,一个典型的窗口包含多个按钮,所有按钮都会生成相同的按钮点击事件),因此检查事件源对象或其标识符有助于区分这些事件。

事件处理

在wxPython中,有一种主要的事件处理方式,即使用wx.EvtHandler.Bind调用,并且该方式可用于动态地绑定和解除绑定事件处理函数,也就是说,在运行时根据某些条件来进行操作。它还允许将事件直接绑定到:

  • 同一个或另一个对象中的一个处理方法。
  • 一个普通函数,比如静态方法或全局函数。
  • 一个任意的可调用对象。

动态事件处理

让我们先来看一下语法:在你代码中的任何位置,但通常是在定义处理函数本身的类的代码中,像这样调用其Bind()方法:

class MyFrame(wx.Frame):
    def __init__(self, parent):
        wx.Frame.__init__(self, parent)

        # 其他初始化代码……

        self.Bind(wx.EVT_MENU, self.OnExit, id=wx.ID_EXIT)

事件处理程序可以在任何时候进行绑定。例如,可以先进行一些初始化操作,只有在初始化成功时才绑定这些处理程序。这可以避免在事件处理程序本身中去测试对象是否已正确初始化。使用Bind()方法的话,如果对象没有正确初始化,这些处理程序根本就不会被调用。

作为上述内容的一点扩展,事件处理程序也可以在任何时候使用wx.EvtHandler.Unbind来解除绑定(并且之后也许还可以重新绑定)。

几乎是最后一点,但绝不是最不重要的一点,那就是其灵活性,这种灵活性允许将一个事件绑定到:

  • 另一个对象中的一个方法。
  • 一个普通函数,比如静态方法或全局函数。
  • 一个任意的函数。

现在让我们来看更多关于如何使用Bind()函数的两种重载形式来使用不同事件处理程序的示例:第一种重载形式用于对象方法,另一种用于任意函子(可调用对象,包括简单函数)。

除了使用生成事件的对象自身的一个方法之外,你还可以使用一个完全不同对象的方法来作为事件处理程序:

def OnFrameExit(event):
    # 做一些有用的事……
    pass

class MyFrame(wx.Frame):
    def __init__(self, parent):
        wx.Frame.__init__(self, parent)

        # 其他初始化代码……

        self.Bind(wx.EVT_MENU, OnFrameExit, id=wx.ID_EXIT)

请注意,MyFrameHandler无需从wx.EvtHandler派生。

事件是如何被处理的

前面的章节解释了如何定义事件处理程序,但没有涉及wxPython究竟是如何找到要为给定事件调用的处理程序这一问题。本节将详细描述所使用的算法。

当从窗口系统接收到一个事件时,wxPython会在属于生成该事件的窗口的第一个事件处理程序对象上调用wx.EvtHandler.ProcessEvent方法。ProcessEvent按正常顺序搜索事件表的步骤如下,一旦找到一个处理程序,事件处理就会停止(除非该处理程序调用了wx.Event.Skip,在这种情况下,它不被视为已处理该事件,搜索会继续进行):

  1. 在其他任何事情发生之前,会先调用wx.AppConsole.FilterEvent。如果它返回的值不是-1(默认值),那么事件处理将立即停止。
  2. 如果此事件处理程序通过调用wx.EvtHandler.SetEvtHandlerEnabled被禁用,那么接下来的三个步骤将被跳过,并且事件处理会在步骤 (5) 继续进行。
  3. 如果该对象是一个wx.Window并且有一个关联的验证器,那么wx.Validator就有机会处理该事件。
  4. 查阅动态绑定的事件处理程序列表,也就是那些调用了Bind()方法所绑定的事件处理程序。
  5. 检查包含在这个类及其基类中使用事件表宏所定义的所有处理程序的事件表。请注意,这意味着在基类中定义的任何事件处理程序都将在这一步被执行。
  6. 如果事件处理程序链中存在下一个事件处理程序,那么该事件会被传递给它,也就是说,会对其执行步骤(1)到(4)。通常不存在下一个事件处理程序,所以控制权会传递到下一步,但关于如何定义下一个处理程序,请参阅“事件处理程序链”。
  7. 如果该对象是一个wx.Window并且事件被设置为可传播(默认情况下,只有从wx.CommandEvent派生的事件类型才被设置为可传播),那么对于其父窗口,处理过程会从步骤(1)重新开始(且不包括步骤(7))。如果此对象不是一个窗口,但存在下一个处理程序,并且下一个处理程序是一个窗口的话,该事件会被传递给它的父窗口。这就确保了在一种常见的情形下,即(可能有多个)非窗口事件处理程序位于一个窗口之上时,事件最终会到达该窗口的父窗口。
  8. 最后,也就是说,如果该事件仍未得到处理,wx.App对象本身(它派生自wx.EvtHandler)将获得最后一次处理该事件的机会。

请密切关注第6步!人们常常会忽略或被wxPython事件处理系统的这一强大功能所迷惑。关于事件在窗口层级结构中向上传播的详细内容将在下一节中介绍。

事件是如何向上传播的

如上文所述,从wx.CommandEvent派生的类的事件,如果在当前窗口本身未得到处理,默认情况下会传播到父窗口。不过,尽管默认情况下只有命令事件会以这种方式传播,其他事件也同样可以传播,因为事件处理代码会使用wx.Event.ShouldPropagate来检查某个事件是否应该传播。也有可能只让事件传播有限的次数,而不是一直传播到它被处理(或者到达顶级父窗口)为止。

最后,还有另一个额外的复杂情况(实际上,这极大地简化了wxPython程序员的工作):当将命令事件向上传播到父窗口时,如果存在父对话框,那么一旦事件到达父对话框,事件传播就会停止。这意味着,当弹出模态对话框时,你不用担心会收到来自对话框控件的意外事件(对话框本身可能不会处理这些事件,因为它并不关心这些事件)。不过,事件确实会传播到框架之外。做出这种选择的理由是,在典型的应用程序中框架数量很少,程序员也很清楚它们之间的父子关系,而在一个复杂的程序中,要追踪所有可能弹出的对话框(记住,有些对话框是由wxPython自动创建的),即便不是不可能,也可能会很困难。如果出于某种原因你需要指定不同的行为,你可以显式地使用wx.Window.SetExtraStyle(wx.WS_EX_BLOCK_EVENTS)来阻止事件传播到给定窗口之外,或者为默认情况下已设置了该标志的对话框取消设置此标志。

一般来说,将窗口当作一个窗口来处理的事件(如大小调整、移动、绘制、鼠标、键盘等相关事件)只会发送到该窗口本身。而具有更高层次含义或者由窗口自身生成的事件(如按钮点击、菜单选择、树状结构展开等)是命令事件,这些事件会被发送到父窗口,以查看父窗口是否对该事件感兴趣。更确切地说,正如上文所述,所有不从wx.CommandEvent派生的事件类(请参阅wx.Event的继承关系图)都不会向上传播。

在某些情况下,程序员可能希望在父窗口中获取一定数量的系统事件,例如所有发送到对话框中本机控件但未被其使用的按键事件。在这种情况下,就必须编写一个特殊的事件处理程序来重写ProcessEvent()方法,以便将所有事件(或其中任意选定的事件)传递给父窗口。

事件处理程序链

事件传播算法的第4步会检查事件处理程序链中的下一个处理程序。这个链可以通过使用wx.EvtHandler.SetNextHandler来形成:

(参考该图,如果调用了A.ProcessEvent且它没有处理该事件,那么将会调用B.ProcessEvent,依此类推……)。

此外,对于wx.Window而言,你可以使用wx.Window.PushEventHandler来构建一个栈(通过wx.EvtHandler双向链表来实现):

(参考该图,如果调用了W.ProcessEvent,它会立即调用A.ProcessEvent;如果A和B都没有处理该事件,那么就会使用wx.Window自身来处理——也就是说,在测试完所有已压入的事件处理程序之后,wx.Window的动态绑定事件处理程序和静态事件表条目将作为最后的处理可能性来进行检查。)

默认情况下,这个链是空的,也就是说,不存在下一个处理程序。

自定义事件概述

通用方法

自定义事件类允许控件的使用者在无需对控件进行子类化的情况下处理更新,从而使你能够创建出看起来更为完善的控件。然而,要有效地使用事件,通常你需要创建自定义事件类。

本方法为你提供了一些用于创建你自己的自定义事件类的样板代码:

import wx
import wx.lib.newevent

SomeNewEvent, EVT_SOME_NEW_EVENT = wx.lib.newevent.NewEvent()
SomeNewCommandEvent, EVT_SOME_NEW_COMMAND_EVENT = wx.lib.newevent.NewCommandEvent()

你可以通过以下任意一种绑定语法来正常绑定事件:

self.Bind(EVT_SOME_NEW_EVENT, self.handler)
EVT_SOME_NEW_EVENT(self, self.handler)

你还可以在创建事件时将任意数据附加到该事件上,然后将其发布到你选择的任何窗口上:

# 创建事件
evt = SomeNewEvent(attr1="hello", attr2=654)
# 发布事件
wx.PostEvent(target, evt)

当处理带有这类任意数据的事件时,你可以通过属性来获取数据,这些属性的名称与在创建事件实例时传入的名称相同。也就是说,对于上面传递给SomeNewEvent的两个关键字参数:

def handler(self, evt):
    # 鉴于上述构造的事件,以下情况是正确的
    evt.attr1 == "hello"
    evt.attr2 == 654

杂项说明

用户生成的事件与通过编程方式生成的事件

一般来说,wx.Event既可以由用户操作(例如,wx.Window的大小调整)生成,也可以通过函数调用(例如,wx.Window.SetSize)生成,不过wxPython控件通常仅针对用户生成的事件发送从wx.CommandEvent派生的事件。这条规则的唯一例外情况是:

可以使用wx.TextEntry.ChangeValue来替代wx.TextEntry.SetValue,但其他函数,比如wx.TextEntry.Replacewx.TextCtrl.WriteText并没有无事件的等效函数。

窗口标识符

窗口标识符是整数,用于在事件系统中唯一确定窗口的身份(尽管你也可以将其用于其他目的)。实际上,标识符并不需要在整个应用程序中都保持唯一,只要在你所关注的特定上下文中(例如一个框架及其子窗口)是唯一的就行。例如,你可以在任意数量的对话框上使用wx.ID_OK标识符,只要在同一个对话框内不出现多个该标识符即可。

如果你将wx.ID_ANY或-1传递给窗口构造函数,wxPython会自动为你生成一个标识符。当你不关心具体的标识符时,这很有用,原因可能是你不打算处理正在创建的控件所产生的事件,或者你会在一处统一处理所有控件的事件(这种情况下,你在调用wx.EvtHandler.Bind时也应该指定wx.ID_ANY)。自动生成的标识符始终为负数,因此永远不会与用户指定的标识符(必须始终为正数)冲突。

另请参阅:有关可用的标准标识符列表,请查看“标准事件标识符”。你可以使用wx.ID_HIGHEST来确定一个数值,高于该数值定义你自己的标识符是安全的。或者,你也可以使用低于wx.ID_LOWEST的标识符。最后,你还可以使用wx.NewIdRef函数动态分配标识符。如果你在应用程序中始终如一地使用wx.NewIdRef,就可以确保你的标识符不会意外发生冲突。