转自:http://thisis.yorven.site/blog/index.php/2017/09/22/wxpython-jiaocheng-shijian/
事件是每个 GUI 应用所必须的组成部分,所有的 GUI 应用都是事件驱动的。在应用的生命周期内,需要对各种不同类型的时间做出反应。事件主要来自于应用用户的操作触发,但也可以来源于其他方式:网络连接、窗口管理、定时器等。在应用一开始,我们调用MainLoop()函数,这使得应用开始等待处理所有将生成的事件,直到我们退出程序。本节,我们将讨论 wxPython 事件 相关知识。
事件定义
事件(events)是来源于底层框架如 GUI 工具包的应用层信息。事件循环主要用来分发事件和等待信息。事件分配器将事件匹配到对应的事件处理器,事件处理器即用来对响应事件做出特定反应的函数。
简单的 wxPython 事件 样例
下面我们将描述一个简单的 移动 事件样例。
当我们移动窗口到一个新位置时,会产生一个移动事件,它的类型是 wx.MoveEvent. 该事件的绑定器是 wx.EVT_MOVE。
import wx class Example(wx.Frame): def __init__(self, *args, **kw): super(Example, self).__init__(*args, **kw) self.InitUI() def InitUI(self): wx.StaticText(self, label='x:', pos=(10,10)) wx.StaticText(self, label='y:', pos=(10,30)) self.st1 = wx.StaticText(self, label='', pos=(30, 10)) self.st2 = wx.StaticText(self, label='', pos=(30, 30)) self.Bind(wx.EVT_MOVE, self.OnMove) self.SetSize((250, 180)) self.SetTitle('Move event') self.Centre() self.Show(True) def OnMove(self, e): x, y = e.GetPosition() self.st1.SetLabel(str(x)) self.st2.SetLabel(str(y)) def main(): ex = wx.App() Example(None) ex.MainLoop() if __name__ == '__main__': main()
|
上面的例子展示了窗口的当前位置。
self.Bind(wx.EVT_MOVE, self.OnMove)
|
这里,我们将 wx.EVT_MOVE 事件绑定到 OnMove() 方法上。
def OnMove(self, e): x, y = e.GetPosition() self.st1.SetLabel(str(x)) self.st2.SetLabel(str(y))
|
OnMove()函数的事件参数 e 是一个特定事件类型的对象。这里,e 是 wx.MoveEvent 类的一个实例, 它包含了该 event 的一些信息, 例如包括事件对象和窗口位置等。这里,事件对象即是 wx.Frame 部件。我们可以通过 事件的 GetPosition() 函数来得到当前位置。
事件绑定
对 wxPython 事件 的处理并不复杂,包括以下三步:
- 确定 wxPython 事件 绑定器的名字,如 wx.EVT_SIZE、wx.EVT_COLSE 等;
- 创建一个 wxPython 事件 处理函数,该函数在事件产生时会被调用;
- 绑定 wxPython 事件 至自定义的事件处理函数。
在 wxPython 中我们称上面的操作为 绑定方法到事件,在其他地方可能将其称为 事件钩子 (hook)。使用 Bind() 方法绑定事件,该方法有以下参数:
Bind(event, handler, source=None, id=wx.ID_ANY, id2=wx.ID_ANY)
|
参数 event 是 某种EVT_* 对象,它指定了事件的类型。参数 handler 指定了该事件所绑定的处理函数。 当我们想区分来自不同 widgets 的同一类型的时间,可以使用参数 source。当我们有多个 button、菜单项时,可以使用参数 id, 用它来区分不同的组件。当想将一个处理函数绑定至一系列 id 时, 可以使用参数 id2, 比如使用 EVT_MENU_RANGE 的时候。
注意,Bind() 方式在 EvtHandler 类中被定义, wx.Window 就是继承于该类的,而 wx.Winddow 是 wxPython 中大多数 widgets 的基类。Bind() 拥有一个逆操作方法,即 UnBind() 方法。如果想要从一个事件上解除绑定某个事件处理器时,我们可以使用 UnBind() 方法,参数与 Bind() 方法一致。
停止事件
有时,我们需要停止某个事件的继续处理,这时,可以调用 Veto() 方法。
import wx class Example(wx.Frame): def __init__(self, *args, **kw): super(Example, self).__init__(*args, **kw) self.InitUI() def InitUI(self): self.Bind(wx.EVT_CLOSE, self.OnCloseWindow) self.SetTitle('Event veto') self.Centre() self.Show(True) def OnCloseWindow(self, e): dial = wx.MessageDialog(None, 'Are you sure to quit?', 'Question', wx.YES_NO | wx.NO_DEFAULT | wx.ICON_QUESTION) ret = dial.ShowModal() if ret == wx.ID_YES: self.Destroy() else: e.Veto() def main(): ex = wx.App() Example(None) ex.MainLoop() if __name__ == '__main__': main()
|
在我们的例子中,我们处理了一个 wx.CloseEvent 事件。当我们点击窗口的X关闭按钮、按下 Alt+F4 或者从菜单选择退出应用时, 这个事件将会被触发。在很多应用中,我们需要在用户做过改动之后阻止意外退出。为了实现这一目标,我们可以绑定 wx.EVT_CLOSE 事件处理。
dial = wx.MessageDialog(None, 'Are you sure to quit?', 'Question', wx.YES_NO | wx.NO_DEFAULT | wx.ICON_QUESTION) ret = dial.ShowModal()
|
上面的代码显示,在处理关闭事件时,我们显示了一个消息对话框。
if ret == wx.ID_YES: self.Destroy() else: event.Veto()
|
根据对话框的返回值,我们可以销毁窗口或者停止这一事件。需要注意,必须使用 Destroy() 来关闭窗口。因为如果调用 Close() 函数, 该程序将陷入死循环。
事件传播
wxPython 事件 分两种,基础事件和命令事件(command events),他们在事件传播上存在不同。事件传播是指将事件从子组件传播至父组件乃至更层组件。基础事件不传播,而命令事件会传播。wx.CloseEvent 是一个基础事件,这意味着它不会向上传播。
默认情况下,如果事件被事件处理函数捕获,那么就会停止后续的传播。如果我们要让它继续传播,需要调用 Skip() 函数。
import wx class MyPanel(wx.Panel): def __init__(self, *args, **kw): super(MyPanel, self).__init__(*args, **kw) self.Bind(wx.EVT_BUTTON, self.OnButtonClicked) def OnButtonClicked(self, e): print 'event reached panel class' e.Skip() class MyButton(wx.Button): def __init__(self, *args, **kw): super(MyButton, self).__init__(*args, **kw) self.Bind(wx.EVT_BUTTON, self.OnButtonClicked) def OnButtonClicked(self, e): print 'event reached button class' e.Skip() class Example(wx.Frame): def __init__(self, *args, **kw): super(Example, self).__init__(*args, **kw) self.InitUI() def InitUI(self): mpnl = MyPanel(self) MyButton(mpnl, label='Ok', pos=(15, 15)) self.Bind(wx.EVT_BUTTON, self.OnButtonClicked) self.SetTitle('Propagate event') self.Centre() self.Show(True) def OnButtonClicked(self, e): print 'event reached frame class' e.Skip() def main(): ex = wx.App() Example(None) ex.MainLoop() if __name__ == '__main__': main()
|
在这个例子中,我们在 Frame 上的 Panel 中放置了一个按钮,并对所有的widgets 定义了事件处理函数。
def OnButtonClicked(self, e): print 'event reached button class' e.Skip()
|
我们在自定义类中处理了按钮点击事件, Skip() 函数使得事件继续向上层传播。
event reached button class event reached panel class event reached frame class
|
我们得到了上面的输出结果,可见事件从 button 传播至 panel,然后再传播至 frame。
试试注释掉一些 Skip() 函数,看看会输出什么结果。
窗口标识符
窗口标识符是指在 wxPython 事件 系统中唯一确定窗口的整数标记。有三种创建窗口标识符的方法:
- 系统自动创建 id
- 使用标准标识符
- 创建自定义 id
每个 widget 都有一个 id 参数, 这是在事件系统中的唯一数字。如果我们有多个 widgets,必须区分开它们:
wx.Button(parent, -1) wx.Button(parent, wx.ID_ANY)
|
如果我们将 -1 或者 wx.ID_ANY 赋值给 id 参数,意味着我们让 wxPython 自动创建 id。自动创建的 id 总是负值, 而用户创建的必须是正值。 在不需要修改 widget 状态的时候,我们一般让系统自动创建,比如一个不需要改变的静态文本。但仍然可以通过 GetId() 来获取 id。
import wx class Example(wx.Frame): def __init__(self, *args, **kw): super(Example, self).__init__(*args, **kw) self.InitUI() def InitUI(self): pnl = wx.Panel(self) exitButton = wx.Button(pnl, wx.ID_ANY, 'Exit', (10, 10)) self.Bind(wx.EVT_BUTTON, self.OnExit, id=exitButton.GetId()) self.SetTitle("Automatic id") self.Centre() self.Show(True) def OnExit(self, event): self.Close() def main(): ex = wx.App() Example(None) ex.MainLoop() if __name__ == '__main__': main()
|
在上面的例子中,我们不关心实际的 id 值。
self.Bind(wx.EVT_BUTTON, self.OnExit, id=exitButton.GetId())
|
而是直接通过 GetId() 函数直接获取自动生成的 id。
推荐使用标准标识符,这些标识符可以在一些平台提供一些标准的图形或行为。
import wx class Example(wx.Frame): def __init__(self, *args, **kw): super(Example, self).__init__(*args, **kw) self.InitUI() def InitUI(self): pnl = wx.Panel(self) grid = wx.GridSizer(3, 2) grid.AddMany([(wx.Button(pnl, wx.ID_CANCEL), 0, wx.TOP | wx.LEFT, 9), (wx.Button(pnl, wx.ID_DELETE), 0, wx.TOP, 9), (wx.Button(pnl, wx.ID_SAVE), 0, wx.LEFT, 9), (wx.Button(pnl, wx.ID_EXIT)), (wx.Button(pnl, wx.ID_STOP), 0, wx.LEFT, 9), (wx.Button(pnl, wx.ID_NEW))]) self.Bind(wx.EVT_BUTTON, self.OnQuitApp, id=wx.ID_EXIT) pnl.SetSizer(grid) self.SetSize((220, 180)) self.SetTitle("Standard ids") self.Centre() self.Show(True) def OnQuitApp(self, event): self.Close() def main(): ex = wx.App() Example(None) ex.MainLoop() if __name__ == '__main__': main()
|
上面的例子中,我们使用了标准标识符,在 Linux 中,这些按钮都会有图标。
grid.AddMany([(wx.Button(pnl, wx.ID_CANCEL), 0, wx.TOP | wx.LEFT, 9), (wx.Button(pnl, wx.ID_DELETE), 0, wx.TOP, 9), (wx.Button(pnl, wx.ID_SAVE), 0, wx.LEFT, 9), (wx.Button(pnl, wx.ID_EXIT)), (wx.Button(pnl, wx.ID_STOP), 0, wx.LEFT, 9), (wx.Button(pnl, wx.ID_NEW))])
|
我们将6个按钮加入到一个 grid sizer 中。wx.ID_CANCEL、wx.ID_DELETE、wx.ID_SAVE、wx.ID_EXIT、wx.ID_STOP 和 wx.ID_NEW都是标准的标识符。
self.Bind(wx.EVT_BUTTON, self.OnQuitApp, id=wx.ID_EXIT)
|
我们把 button 事件绑定到 OnQuitAPP() 处理函数,使用 id 参数来区分不同的 button, 并唯一标识了事件的来源。
最后我们可以使用自定义的窗口标识符。
import wx ID_MENU_NEW = wx.NewId() ID_MENU_OPEN = wx.NewId() ID_MENU_SAVE = wx.NewId() class Example(wx.Frame): def __init__(self, *args, **kw): super(Example, self).__init__(*args, **kw) self.InitUI() def InitUI(self): self.CreateMenuBar() self.CreateStatusBar() self.SetSize((250, 180)) self.SetTitle('Global ids') self.Centre() self.Show(True) def CreateMenuBar(self): mb = wx.MenuBar() fMenu = wx.Menu() fMenu.Append(ID_MENU_NEW, 'New') fMenu.Append(ID_MENU_OPEN, 'Open') fMenu.Append(ID_MENU_SAVE, 'Save') mb.Append(fMenu, '&File') self.SetMenuBar(mb) self.Bind(wx.EVT_MENU, self.DisplayMessage, id=ID_MENU_NEW) self.Bind(wx.EVT_MENU, self.DisplayMessage, id=ID_MENU_OPEN) self.Bind(wx.EVT_MENU, self.DisplayMessage, id=ID_MENU_SAVE) def DisplayMessage(self, e): sb = self.GetStatusBar() eid = e.GetId() if eid == ID_MENU_NEW: msg = 'New menu item selected' elif eid == ID_MENU_OPEN: msg = 'Open menu item selected' elif eid == ID_MENU_SAVE: msg = 'Save menu item selected' sb.SetStatusText(msg) def main(): ex = wx.App() Example(None) ex.MainLoop() if __name__ == '__main__': main()
|
在上面的例子中, 我们创建了一个包含 3 个菜单项的菜单, 我们全局申明了菜单项的 id。
ID_MENU_NEW = wx.NewId() ID_MENU_OPEN = wx.NewId() ID_MENU_SAVE = wx.NewId()
|
函数 wx.NewId() 可以创建新的唯一 id。
self.Bind(wx.EVT_MENU, self.DisplayMessage, id=ID_MENU_NEW) self.Bind(wx.EVT_MENU, self.DisplayMessage, id=ID_MENU_OPEN) self.Bind(wx.EVT_MENU, self.DisplayMessage, id=ID_MENU_SAVE)
|
通过唯一 id 可是识别所有三个菜单项。
eid = e.GetId() if eid == ID_MENU_NEW: msg = 'New menu item selected' elif eid == ID_MENU_OPEN: msg = 'Open menu item selected' elif eid == ID_MENU_SAVE: msg = 'Save menu item selected'
|
从 event 对象我们得到 id, 根据 id 的不同,我们准备不同的信息,并将它输出在应用的状态栏。
绘制事件
绘制事件即 Paint Event,当窗口重绘时会触发该事件,比如当我们调整窗口大小或者最大化的时候。 当然,也可以程序化的触发绘制事件。比如,当我们调用 SetLabel() 函数来修改 wx.StaticText 组件的文字时,就会触发绘制事件。注意,窗口最小化不会触发绘制事件。
import wx class Example(wx.Frame): def __init__(self, *args, **kw): super(Example, self).__init__(*args, **kw) self.InitUI() def InitUI(self): self.count = 0 self.Bind(wx.EVT_PAINT, self.OnPaint) self.SetSize((250, 180)) self.Centre() self.Show(True) def OnPaint(self, e): self.count += 1 self.SetTitle(str(self.count)) def main(): ex = wx.App() Example(None) ex.MainLoop() if __name__ == '__main__': main()
|
在上面的例子中,我们对绘制事件进行计数,并将当前数目设置为 frame 窗口的标题。
self.Bind(wx.EVT_PAINT, self.OnPaint)
|
上面的代码将 wx.EVT_PAINT 事件绑定至 OnPaint 函数。
def OnPaint(self, e): self.count += 1 self.SetTitle(str(self.count))
|
在 OnPaint() 内部,我们增加了计数器并设置了新的窗口标题。
焦点事件
焦点表明了当前应用中被选择的 widget,从键盘输入或剪切板拷入的文本将被发送到该 widget。有两个事件与焦点有关,包括 wx.EVT_SET_FOCUS 和 wx.EVT_KILL_FOCUS。当一个 widget 获得焦点时,会触发 wx.EVT_SET_FOCUS;当 widget 丢失焦点时,会触发 wx.EVT_KILL_FOCUS。通过点击或者键盘按键比如 Tab 键或 Shift+Tab 键可以改变焦点。
import wx class MyWindow(wx.Panel): def __init__(self, parent): super(MyWindow, self).__init__(parent) self.color = '#b3b3b3' self.Bind(wx.EVT_PAINT, self.OnPaint) self.Bind(wx.EVT_SIZE, self.OnSize) self.Bind(wx.EVT_SET_FOCUS, self.OnSetFocus) self.Bind(wx.EVT_KILL_FOCUS, self.OnKillFocus) def OnPaint(self, e): dc = wx.PaintDC(self) dc.SetPen(wx.Pen(self.color)) x, y = self.GetSize() dc.DrawRectangle(0, 0, x, y) def OnSize(self, e): self.Refresh() def OnSetFocus(self, e): self.color = '#0099f7' self.Refresh() def OnKillFocus(self, e): self.color = '#b3b3b3' self.Refresh() class Example(wx.Frame): def __init__(self, *args, **kw): super(Example, self).__init__(*args, **kw) self.InitUI() def InitUI(self): grid = wx.GridSizer(2, 2, 10, 10) grid.AddMany([(MyWindow(self), 0, wx.EXPAND|wx.TOP|wx.LEFT, 9), (MyWindow(self), 0, wx.EXPAND|wx.TOP|wx.RIGHT, 9), (MyWindow(self), 0, wx.EXPAND|wx.BOTTOM|wx.LEFT, 9), (MyWindow(self), 0, wx.EXPAND|wx.BOTTOM|wx.RIGHT, 9)]) self.SetSizer(grid) self.SetSize((350, 250)) self.SetTitle('Focus event') self.Centre() self.Show(True) def OnMove(self, e): print e.GetEventObject() x, y = e.GetPosition() self.st1.SetLabel(str(x)) self.st2.SetLabel(str(y)) def main(): ex = wx.App() Example(None) ex.MainLoop() if __name__ == '__main__': main()
|
在上面这个例子中,我们有4个 panel。 获得当前焦点的 panel 被高亮显示。
self.Bind(wx.EVT_SET_FOCUS, self.OnSetFocus) self.Bind(wx.EVT_KILL_FOCUS, self.OnKillFocus)
|
上面的代码中,我们把两个焦点事件绑定至事件处理函数。
def OnPaint(self, e): dc = wx.PaintDC(self) dc.SetPen(wx.Pen(self.color)) x, y = self.GetSize() dc.DrawRectangle(0, 0, x, y)
|
在 OnPaint() 函数中,我们在窗口上进行了绘制。外框的颜色取决于窗口是否获得焦点,如果获得焦点,则使用蓝色。
def OnSetFocus(self, e): self.color = ‘#0099f7’ self.Refresh()
|
在 OnSetFocus() 函数中,我们设置了 self.color 为某种蓝色,接着刷新 frame 窗口,这会触发所有子部件的绘制事件。各个窗口会被重绘,获取焦点的窗口将得到一个蓝色外框。
键盘事件
当我们在键盘上按下按钮时,一个 wx.KeyEvent 会被触发并被发送到当前焦点 widget。有三种不同的键盘事件:
- wx.EVT_KEY_DOWN
- wx.EVT_KEY_UP
- wx.EVT_CHAR
一个常用的需求是,当 Esc 键被按下时,退出整个应用。
import wx class Example(wx.Frame): def __init__(self, *args, **kw): super(Example, self).__init__(*args, **kw) self.InitUI() def InitUI(self): pnl = wx.Panel(self) pnl.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown) pnl.SetFocus() self.SetSize((250, 180)) self.SetTitle('Key event') self.Centre() self.Show(True) def OnKeyDown(self, e): key = e.GetKeyCode() if key == wx.WXK_ESCAPE: ret = wx.MessageBox('Are you sure to quit?', 'Question', wx.YES_NO | wx.NO_DEFAULT, self) if ret == wx.YES: self.Close() def main(): ex = wx.App() Example(None) ex.MainLoop() if __name__ == '__main__': main()
|
在这个例子中,我们处理了 Esc 键的按下事件,当按下 Esc 时,会弹出对话框询问,是否关闭应用。
pnl.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
|
上面代码将 EVT_KEY_DOWN 事件绑定至 self.OnKeyDown() 函数。
上面代码得到了按下键的编号。
我们检查了键编号,看所按下的键是否是 Esc,它的键编号是 wx.WXK_ESCAPE。
在本节中,我们讨论了 wxPython 事件 的相关知识。