wxPython 事件机制
了解事件机制是学习 wxPython 绕不过去的坎,结合各种资料和个人理解整理了一下,如果有问题请留言指出。
先看下《活学活用wxPython》书中介绍的最小实例。
import wx
class App(wx.App):
def OnInit(self):
frame = wx.Frame(parent=None, title="Bare")
frame.Show()
return True
app = App()
app.MainLoop()
每个wxPython程序必须有一个application对象和至少一个 frame对象。application对象必须是wx.App的一个实例或你在OnInit()方法中定义 的一个子类的一个实例。当你的应用程序启动的时候,OnInit()方法将被 wx.App父类调用。
一旦进入主事件循环,控制权将转交给wxPython。wxPython GUI程序主要 响应用户的鼠标和键盘事件。当一个应用程序的所有框架被关闭后,这个 app.MainLoop()方法将返回且程序退出。(出自《活学活用wxPython》)
最后两行代码的作用是:创建一个应用程序实例并进入它的主事件循环。
主事件循环
wx.App.MainLoop 的作用是执行主的 GUI 事件循环,继承自 wx.AppConsole.MainLoop,可被重写用来执行自己定义的主事件循环。
那么默认主事件循环究竟来自哪里呢?wx.AppTraits 类定义了 wx.App 的各种可配置方面。而其中一个方法 wx.AppTraits.CreateEventLoop,wxPython 用它来创建可被 wx.App.OnRun 调用的主循环,主循环为 wx.EventLoopBase 实例。
主事件循环到底做了些什么呢?最好的办法当然是通过 wxPython 核心源代码去分析,但是它是 C++ 写的,暂时看不懂。幸运的是,demo 文件里刚好有一个自定义主事件循环的例子,路径 wxPython-demo-4.1.1/samples/mainloop/mainloop.py 。最新的 demo 可通过这个链接下载 https://extras.wxpython.org/wxPython4/extras/4.2.0/
主事件循环简化为:
while True: # 无限循环
self.ProcessIdle() # 空闲时处理空闲事件
wx.GetApp().ProcessPendingEvents() # 处理挂起事件
程序进入一个无限的循环,如果没有挂起事件,就处理空闲事件,否则就处理挂起事件。每隔一段时间查看事物状态,也被称为轮询。
事件的处理
事件的定义:事件是可以被控件识别的操作,如按下确定按钮,选择某个单选按钮或者复选框。每一种控件有自己可以识别的事件,如窗体的加载、单击、双击等事件,编辑框(文本框)的文本改变事件,等等。(出自百度百科)
事件是一种结构,它保存有关传递给回调或成员函数的事件的信息。(来源:https://docs.wxpython.org/wx.Event.html)
每个事件的信息由三部分构成:
- 事件类型:这是一个 EventType 类型的值,事件的类型的唯一标识。
- 事件类:由事件携带,每个事件都有一些与之关联的信息,这些数据由派生自 wx.Event 的类的对象表示。不同类型的事件可以使用同一个事件类。
- 事件源:wx.Event 存储生成事件的对象和窗口标识符。由于通常有多个对象生成相同类型的事件(例如,一个典型的窗口包含多个按钮,所有按钮都生成相同的按钮单击事件),因此检查事件源对象或其 id 可以区分它们。(来源:https://docs.wxpython.org/events_overview.html)
事件分类,按触发对象划分为用户事件(如 wx.EVT_MOTION)和系统事件(如 wx.EVT_PAINT),按是否可向上级窗口传播划分为命令事件(如 wx.EVT_BUTTON)和非命令事件(如 wx.EVT_KEY_DOWN)。
在 wxPython 中有一种处理事件的主要方法,它使用 wx.EvtHandler.Bind 调用并且可用于动态绑定和取消绑定处理程序,即在运行时根据某些条件,它还允许将事件直接绑定到:
- 相同或另一个对象中的处理程序方法。
- 普通函数,如静态方法或全局函数。
- 任意可调用对象。
(来源:https://docs.wxpython.org/events_overview.html)
wx.EvtHandler.Bind 的方法签名是:
Bind( self , event , handler , source=None , id=wx.ID_ANY , id2=wx.ID_ANY )
将事件绑定到事件处理程序。
参数
event – EVT_ 指定要绑定的事件类型的事件绑定器对象之一。
handler – 事件传递给 self 时要调用的可调用对象。传递 None 参数以断开事件处理程序。
source – 有时事件来自与 self 不同的窗口,但你仍想在 self 中捕获它。(例如,传递到框架的按钮事件)通过传递事件源,事件处理系统能够区分来自不同控件的相同事件类型。
id – 用于指定事件源 ID 而不是实例。
id2 – 当需要将处理程序绑定到一系列 ID 时使用,例如使用 EVT_MENU_RANGE。
第一个参数是新出现的概念,事件绑定器对象,即 wx.PyEventBinder 的实例,用于将特定事件绑定到事件处理程序。打印一下,会得到这样的结果:
>>> print(wx.EVT_PAINT)
<wx.core.PyEventBinder object at 0x0306E148>
为了避免背景擦除造成闪烁,通常会给 wx.EVT_ERASE_BACKGROUND 事件绑定一个空的处理方法,他这里的用法演示了绑定到匿名函数。
self.Bind(wx.EVT_ERASE_BACKGROUND, lambda evt:evt.Skip())
frame.Bind(wx.EVT_BUTTON, lambda evt:win32gui.SendMessage(frame.GetHandle(), win32con.WM_CLOSE,0,0), self)
frame.Bind(wx.EVT_BUTTON, lambda evt:win32gui.SendMessage(frame.GetHandle(), win32con.WM_SYSCOMMAND, win32con.SC_MAXIMIZE,0), self)
(来源:wxPython实现仿QQ登录界面https://blog.csdn.net/xugangjava/article/details/8024356)
具体事件处理和传播可以查看:https://docs.wxpython.org/events_overview.html
要点:
- 事件队列(event queue):已发生的但未处理的事件的一个列表。
- 动态绑定事件处理器的动态事件表(通过 Bind 添加事件表项)
- 事件表宏定义的所有事件处理器的静态事件表(框架内置的事件表)
- 事件处理器链表
触发事件后通常会调用 wx.EvtHandler.AddPendingEvent 方法,将事件对象加入待处理事件队列。
这两个事件表就是用来查找事件对应的事件处理方法。
什么是宏?宏(英语:Macro)是一种批量处理的称谓。计算机科学里的宏是一种抽象(Abstraction),它根据一系列预定义的规则替换一定的文本模式。解释器或编译器在遇到宏时会自动进行这一模式替换。(出自百度百科)
应该就相当于静态变量吧,只不过它存储的值可能更长。
为什么要用到表?这个得提到表驱动。表驱动法,可以在表中查找信息而不必用很多的逻辑语句(if或Case)来把它们找出来的方法。(出自百度百科)
应该相当于平时面对很多判断的时候,使用字典直接返回键对应的值,只不过这里的键是事件,值是事件处理方法。类似的还有数据库的表,还可以通过建立索引加快查找数据位置的速度。wxPython 是通过建立事件哈希表来加快事件表的查找速度,暂时只是知道有这么回事,具体得自己看核心源码。
所以,可以把整个事件机制流程概括为:进入主事件循环,触发事件,加入待处理事件队列,轮询事件队列,查找事件对应处理方法,执行处理方法。
常见问题
self.Bind 和 self.button.Bind
https://wiki.wxpython.org/self.Bind%20vs.%20self.button.Bind
说实话,文章里的图我没看懂。概括就是,只有命令事件可以向上级窗口传播,可以用 event.Skip() 使处理程序继续搜索执行其他绑定相同事件的处理器,如果在命令事件中用 event.Skip() 则可能继续向上级窗口搜索。
重绘控件
绑定 wx.EVT_PAINT 事件,在对应事件处理器中使用 GDI 来绘制界面。绘制事件由系统触发,轮询到绘制事件就会重新绘制。在绘制事件处理器方法之外,想要主动刷新界面,也可以调用 wx.Window.Refresh 触发绘制事件,这里触发的事件也需要等待主循环轮询到才会执行。如果要立即执行绘制事件,可以使用 wx.Window.Update,但是用 Update 一般会出现一些莫名的问题。
界面卡住
如果给按钮事件 wx.EVT_BUTTON 绑定一个执行 time.sleep 的方法,点击按钮后界面就会卡住。由前面的事件机制可以知道,主线程在执行按钮点击处理方法时,时间过长,造成阻塞,后面的事件都无法处理。
wxPython 推荐的三个线程安全的方法是:wx.PostEvent wx.CallAfter wx.CallLater。
从上面的调用图可以看出,其实这三个方法也是把事件加入到待处理事件列表,和直接触发并没有太大不同。那么唯一可取的就是线程安全,所以要解决界面卡住的问题就需要在新的线程中调用这三个方法,通过回调来执行耗时较长的方法。
什么是线程安全?一个函数被多个并发线程反复调用,它会一直产生正确的结果,则该函数是线程安全函数。(来源:线程安全与可重入函数 https://blog.csdn.net/sdoyuxuan/article/details/73382395)
具体用法参考:
https://wiki.wxpython.org/CallAfter
https://www.blog.pythonlibrary.org/2010/05/22/wxpython-and-threads/
还有支持异步传输的 wx.lib.delayedresult 和 wxasync :
https://docs.wxpython.org/wx.lib.delayedresult.html
https://github.com/sirk390/wxasync
自定义事件
第一种:
1.继承 wx.PyCommandEvent,定义 get 和 set 方法获取和设置事件参数
2.创建一个事件类型和一个绑定器对象去绑定该事件到特定的对象
3.创建自定义事件对象,设置事件参数,并用 ProcessEvent() 方法将这个实例引入事件处理系统
4.绑定自定义事件的 event handler
5.在 event handler 中响应事件
# -*- coding: utf-8 -*-
import wx
class CustomEvent(wx.PyCommandEvent):
def __init__(self, eventType, eventId):
super(CustomEvent, self).__init__(eventType, eventId)
self.eventArgs = None
def getEventArgs(self):
return self.eventArgs
def setEventArgs(self, args):
self.eventArgs = args
customEventType = wx.NewEventType()
EVT_CUSTOM = wx.PyEventBinder(customEventType, 1)
class App(wx.App):
def OnInit(self):
self.frame = wx.Frame(parent=None, title="Test", size=(400, 300))
self.frame.Center()
panel = wx.Panel(parent=self.frame)
width, height = self.frame.GetClientSize()
button = wx.Button(parent=panel, label="button")
bwidth, bheight = button.GetSize()
button.SetPosition(((width - bwidth) / 2, (height - bheight) / 2))
button.Bind(wx.EVT_BUTTON, self.OnClick)
self.Bind(EVT_CUSTOM, self.OnHandler)
self.frame.Show()
return True
def OnExit(self):
return 0
def OnClick(self, event):
evt = CustomEvent(customEventType, self.frame.GetId())
evt.setEventArgs("test123")
self.ProcessEvent(evt)
#wx.PostEvent(self.frame, evt)
def OnHandler(self, event):
dlg = wx.MessageDialog(parent=self.frame, message=event.getEventArgs(), caption="MessageBox", style=wx.OK | wx.ICON_INFORMATION)
dlg.ShowModal()
dlg.Destroy()
if __name__ == "__main__":
app = App()
app.MainLoop()
第二种:
命令事件实例 wx.lib.newevent.NewCommandEvent()
非命令事件实例 wx.lib.newevent.NewEvent()
绑定和传递同第一种方法一样,不同的是传递事件参数的方法
# -*- coding: utf-8 -*-
import wx
import wx.lib.newevent
CustomEvent, EVT_CUSTOM = wx.lib.newevent.NewEvent()
CustomCommandEvent, EVT_CUSTOM_COMMAND = wx.lib.newevent.NewCommandEvent()
self.Bind(EVT_CUSTOM, self.handler)
EVT_CUSTOM(self, self.handler)
evt = CustomEvent(attr1="hello", attr2=654)
wx.PostEvent(target, evt)
def handler(self, evt):
attr1 = evt.attr1
attr2 = evt.attr2
第三种:
(来源:https://wiki.wxpython.org/LongRunningTasks)
1.绑定器对象通过 wx.EvtHandler.Connect 实现
2.自定义事件类继承 wx.PyEvent
# -*- coding: utf-8 -*-
import time
from threading import *
import wx
# Button definitions
ID_START = wx.NewId()
ID_STOP = wx.NewId()
# Define notification event for thread completion
EVT_RESULT_ID = wx.NewId()
def EVT_RESULT(win, func):
"""Define Result Event."""
win.Connect(-1, -1, EVT_RESULT_ID, func) #wx.EvtHandler.Connect在动态事件表中为事件绑定创建一个条目
class ResultEvent(wx.PyEvent):
"""Simple event to carry arbitrary result data."""
def __init__(self, data):
"""Init Result Event."""
wx.PyEvent.__init__(self)
self.SetEventType(EVT_RESULT_ID)
self.data = data
# Thread class that executes processing
class WorkerThread(Thread):
"""Worker Thread Class."""
def __init__(self, notify_window):
"""Init Worker Thread Class."""
Thread.__init__(self)
self._notify_window = notify_window
self._want_abort = 0
# This starts the thread running on creation, but you could
# also make the GUI thread responsible for calling this
self.start()
def run(self):
"""Run Worker Thread."""
# This is the code executing in the new thread. Simulation of
# a long process (well, 10s here) as a simple loop - you will
# need to structure your processing so that you periodically
# peek at the abort variable
for i in range(10):
time.sleep(1)
if self._want_abort:
# Use a result of None to acknowledge the abort (of
# course you can use whatever you'd like or even
# a separate event type)
wx.PostEvent(self._notify_window, ResultEvent(None))
return
# Here's where the result would be returned (this is an
# example fixed result of the number 10, but it could be
# any Python object)
wx.PostEvent(self._notify_window, ResultEvent(10))
def abort(self):
"""abort worker thread."""
# Method for use by main thread to signal an abort
self._want_abort = 1
# GUI Frame class that spins off the worker thread
class MainFrame(wx.Frame):
"""Class MainFrame."""
def __init__(self, parent, id):
"""Create the MainFrame."""
wx.Frame.__init__(self, parent, id, 'Thread Test')
# Dumb sample frame with two buttons
wx.Button(self, ID_START, 'Start', pos=(0,0))
wx.Button(self, ID_STOP, 'Stop', pos=(0,50))
self.status = wx.StaticText(self, -1, '', pos=(0,100))
self.Bind(wx.EVT_BUTTON, self.OnStart, id=ID_START)
self.Bind(wx.EVT_BUTTON, self.OnStop, id=ID_STOP)
# Set up event handler for any worker thread results
EVT_RESULT(self,self.OnResult)
# And indicate we don't have a worker thread yet
self.worker = None
def OnStart(self, event):
"""Start Computation."""
# Trigger the worker thread unless it's already busy
if not self.worker:
self.status.SetLabel('Starting computation')
self.worker = WorkerThread(self)
def OnStop(self, event):
"""Stop Computation."""
# Flag the worker thread to stop if running
if self.worker:
self.status.SetLabel('Trying to abort computation')
self.worker.abort()
def OnResult(self, event):
"""Show Result status."""
if event.data is None:
# Thread aborted (using our convention of None return)
self.status.SetLabel('Computation aborted')
else:
# Process results here
self.status.SetLabel('Computation Result: %s' % event.data)
# In either event, the worker is done
self.worker = None
class MainApp(wx.App):
"""Class Main App."""
def OnInit(self):
"""Init Main App."""
self.frame = MainFrame(None, -1)
self.frame.Show(True)
self.SetTopWindow(self.frame)
return True
if __name__ == '__main__':
app = MainApp(0)
app.MainLoop()