# 第三章 在事件驅動環境中開發
1. [在事件驅動環境中工作](#A.2BVyhOi072mnFSqHOvWINOLV3lT1w-)
1. [要理解事件,我們需要知道哪些術語?](#A.2BiYF0BonjTotO9v8MYhFO7JcAiYF35ZBTVOpOm2cvi.2B3.2FHw-)
2. [什么是事件驅動編程?](#A.2BTsBOSGYvTotO9ppxUqh.2FFnoL.3F)
1. [編寫事件處理器](#A.2BfxZRmU6LTvZZBHQGVmg-)
2. [設計事件驅動程序](#A.2Bi76LoU6LTvaacVKoegtejw-)
3. [事件觸發](#A.2BTotO9onmU9E-)
3. [如何將事件綁定到處理器?](#A.2BWYJPVVwGTotO9n7RW5pSMFkEdAZWaA.3F)
1. [使用wx.EvtHandler的方法工作](#A.2BT391KA-wx.EvtHandler.2BdoRluWzVXeVPXA-)
4. [wxPython是如何處理事件的?](#wxPython.2BZi9Zgk9VWQR0Bk6LTvZ2hA.3F)
1. [理解事件處理過程](#A.2BdAaJ406LTvZZBHQGj8d6Cw-)
2. [使用Skip()方法](#A.2BT391KA-Skip.28.29.2BZbls1Q-)
5. [在應用程序對象中還包含哪些其它的屬性?](#A.2BVyhelHUoegtej1v5jGFOLY.2FYUwVUK1TqTptRdluDdoRcXmAn.2Fx8-)
6. [如何創建自己的事件?](#A.2BWYJPVVIbXvqB6l3xdoROi072.2Fx8-)
1. [為一個定制的窗口部件定義一個定制的事件](#A.2BTjpOAE4qW5pSNnaEepdT45DoTvZbmk5JTgBOKluaUjZ2hE6LTvY-)
7. [總結](#A.2BYDt.2B0w-)
事件處理是`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`進行開發工作。
- 活學活用wxPython
- 前言
- 致謝
- 關于本書
- 第一部分
- 第一章 歡迎使用wxPython
- 第二章 給wxPython程序一個堅實的基礎
- 第三章 在事件驅動環境中開發
- 第四章 用PyCrust使得wxPython更易處理
- 第五章 繪制藍圖
- 第六章 使用wxPython基本構件
- 第二部分 基礎wxPython
- 第七章 使用基礎控件
- 第八章 將構件放入窗體中
- 第九章 通過對話框讓用戶選擇
- 第十章 創建和使用wxPython菜單
- 第十一章 使用sizer放置構件
- 第十二章 操作基本圖像
- 第三部分 高級wxPython
- 第十三章 建造列表控件并管理列表項
- 第十四章 網格控件
- 第十五章 樹形控件
- 第十六章 在應用程序中加入HTML
- 第十七章 wxPython的打印構架
- 第十八章 使用wxPython的其他功能