事件是每个 GUI 应用所必须的组成部分,所有的 GUI 应用都是事件驱动的。在应用的生命周期内,需要对各种不同类型的时间做出反应。事件主要来自于应用用户的操作触发,但也可以来源于其他方式:网络连接、窗口管理、定时器等。在应用一开始,我们调用MainLoop()函数,这使得应用开始等待处理所有将生成的事件,直到我们退出程序。本节,我们将讨论 wxPython 事件 相关知识。
事件定义
事件(events)是来源于底层框架如 GUI 工具包的应用层信息。事件循环主要用来分发事件和等待信息。事件分配器将事件匹配到对应的事件处理器,事件处理器即用来对响应事件做出特定反应的函数。
简单的 wxPython 事件 样例
下面我们将描述一个简单的 移动 事件样例。
当我们移动窗口到一个新位置时,会产生一个移动事件,它的类型是 wx.MoveEvent. 该事件的绑定器是 wx.EVT_MOVE。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
#!/usr/bin/python
# -*- coding: utf-8 -*-
importwx
classExample(wx.Frame):
def__init__(self,*args,**kw):
super(Example,self).__init__(*args,**kw)
self.InitUI()
defInitUI(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)
defOnMove(self, e):
x, y=e.GetPosition()
self.st1.SetLabel(str(x))
self.st2.SetLabel(str(y))
defmain():
ex=wx.App()
Example(None)
ex.MainLoop()
if__name__=='__main__':
main()
|
上面的例子展示了窗口的当前位置。
1
|
self.Bind(wx.EVT_MOVE,self.OnMove)
|
这里,我们将 wx.EVT_MOVE 事件绑定到 OnMove() 方法上。
1
2
3
4
5
|
defOnMove(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() 方法绑定事件,该方法有以下参数:
1
|
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() 方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
#!/usr/bin/python
# -*- coding: utf-8 -*-
importwx
classExample(wx.Frame):
def__init__(self,*args,**kw):
super(Example,self).__init__(*args,**kw)
self.InitUI()
defInitUI(self):
self.Bind(wx.EVT_CLOSE,self.OnCloseWindow)
self.SetTitle('Event veto')
self.Centre()
self.Show(True)
defOnCloseWindow(self, e):
dial=wx.MessageDialog(None,'Are you sure to quit?','Question',
wx.YES_NO | wx.NO_DEFAULT | wx.ICON_QUESTION)
ret=dial.ShowModal()
ifret==wx.ID_YES:
self.Destroy()
else:
e.Veto()
defmain():
ex=wx.App()
Example(None)
ex.MainLoop()
if__name__=='__main__':
main()
|
在我们的例子中,我们处理了一个 wx.CloseEvent 事件。当我们点击窗口的X关闭按钮、按下 Alt+F4 或者从菜单选择退出应用时, 这个事件将会被触发。在很多应用中,我们需要在用户做过改动之后阻止意外退出。为了实现这一目标,我们可以绑定 wx.EVT_CLOSE 事件处理。
1
2
3
4
|
dial=wx.MessageDialog(None,'Are you sure to quit?','Question',
wx.YES_NO | wx.NO_DEFAULT | wx.ICON_QUESTION)
ret=dial.ShowModal()
|
上面的代码显示,在处理关闭事件时,我们显示了一个消息对话框。
1
2
3
4
|
ifret==wx.ID_YES:
self.Destroy()
else:
event.Veto()
|
根据对话框的返回值,我们可以销毁窗口或者停止这一事件。需要注意,必须使用 Destroy() 来关闭窗口。因为如果调用 Close() 函数, 该程序将陷入死循环。
事件传播
wxPython 事件 分两种,基础事件和命令事件(command events),他们在事件传播上存在不同。事件传播是指将事件从子组件传播至父组件乃至更层组件。基础事件不传播,而命令事件会传播。wx.CloseEvent 是一个基础事件,这意味着它不会向上传播。
默认情况下,如果事件被事件处理函数捕获,那么就会停止后续的传播。如果我们要让它继续传播,需要调用 Skip() 函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
|
#!/usr/bin/python
# -*- coding: utf-8 -*-
importwx
classMyPanel(wx.Panel):
def__init__(self,*args,**kw):
super(MyPanel,self).__init__(*args,**kw)
self.Bind(wx.EVT_BUTTON,self.OnButtonClicked)
defOnButtonClicked(self, e):
print'event reached panel class'
e.Skip()
classMyButton(wx.Button):
def__init__(self,*args,**kw):
super(MyButton,self).__init__(*args,**kw)
self.Bind(wx.EVT_BUTTON,self.OnButtonClicked)
defOnButtonClicked(self, e):
print'event reached button class'
e.Skip()
classExample(wx.Frame):
def__init__(self,*args,**kw):
super(Example,self).__init__(*args,**kw)
self.InitUI()
defInitUI(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)
defOnButtonClicked(self, e):
print'event reached frame class'
e.Skip()
defmain():
ex=wx.App()
Example(None)
ex.MainLoop()
if__name__=='__main__':
main()
|
在这个例子中,我们在 Frame 上的 Panel 中放置了一个按钮,并对所有的widgets 定义了事件处理函数。
1
2
3
4
|
defOnButtonClicked(self, e):
print'event reached button class'
e.Skip()
|
我们在自定义类中处理了按钮点击事件, Skip() 函数使得事件继续向上层传播。
1
2
3
|
event reached buttonclass
event reached panelclass
event reached frameclass
|
我们得到了上面的输出结果,可见事件从 button 传播至 panel,然后再传播至 frame。
试试注释掉一些 Skip() 函数,看看会输出什么结果。
窗口标识符
窗口标识符是指在 wxPython 事件 系统中唯一确定窗口的整数标记。有三种创建窗口标识符的方法:
- 系统自动创建 id
- 使用标准标识符
- 创建自定义 id
每个 widget 都有一个 id 参数, 这是在事件系统中的唯一数字。如果我们有多个 widgets,必须区分开它们:
1
2
|
wx.Button(parent,-1)
wx.Button(parent, wx.ID_ANY)
|
如果我们将 -1 或者 wx.ID_ANY 赋值给 id 参数,意味着我们让 wxPython 自动创建 id。自动创建的 id 总是负值, 而用户创建的必须是正值。 在不需要修改 widget 状态的时候,我们一般让系统自动创建,比如一个不需要改变的静态文本。但仍然可以通过 GetId() 来获取 id。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
#!/usr/bin/python
# -*- coding: utf-8 -*-
importwx
classExample(wx.Frame):
def__init__(self,*args,**kw):
super(Example,self).__init__(*args,**kw)
self.InitUI()
defInitUI(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)
defOnExit(self, event):
self.Close()
defmain():
ex=wx.App()
Example(None)
ex.MainLoop()
if__name__=='__main__':
main()
|
在上面的例子中,我们不关心实际的 id 值。
1
|
self.Bind(wx.EVT_BUTTON, self.OnExit,id=exitButton.GetId())
|
而是直接通过 GetId() 函数直接获取自动生成的 id。
推荐使用标准标识符,这些标识符可以在一些平台提供一些标准的图形或行为。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
#!/usr/bin/python
# -*- coding: utf-8 -*-
importwx
classExample(wx.Frame):
def__init__(self,*args,**kw):
super(Example,self).__init__(*args,**kw)
self.InitUI()
defInitUI(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)
defOnQuitApp(self, event):
self.Close()
defmain():
ex=wx.App()
Example(None)
ex.MainLoop()
if__name__=='__main__':
main()
|
上面的例子中,我们使用了标准标识符,在 Linux 中,这些按钮都会有图标。
1
2
3
4
5
6
|
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都是标准的标识符。
1
|
self.Bind(wx.EVT_BUTTON,self.OnQuitApp,id=wx.ID_EXIT)
|
我们把 button 事件绑定到 OnQuitAPP() 处理函数,使用 id 参数来区分不同的 button, 并唯一标识了事件的来源。
图:标准标识符
最后我们可以使用自定义的窗口标识符。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
|
#!/usr/bin/python
# -*- coding: utf-8 -*-
importwx
ID_MENU_NEW=wx.NewId()
ID_MENU_OPEN=wx.NewId()
ID_MENU_SAVE=wx.NewId()
classExample(wx.Frame):
def__init__(self,*args,**kw):
super(Example,self).__init__(*args,**kw)
self.InitUI()
defInitUI(self):
self.CreateMenuBar()
self.CreateStatusBar()
self.SetSize((250,180))
self.SetTitle('Global ids')
self.Centre()
self.Show(True)
defCreateMenuBar(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)
defDisplayMessage(self, e):
sb=self.GetStatusBar()
eid=e.GetId()
ifeid==ID_MENU_NEW:
msg='New menu item selected'
elifeid==ID_MENU_OPEN:
msg='Open menu item selected'
elifeid==ID_MENU_SAVE:
msg='Save menu item selected'
sb.SetStatusText(msg)
defmain():
ex=wx.App()
Example(None)
ex.MainLoop()
if__name__=='__main__':
main()
|
在上面的例子中, 我们创建了一个包含 3 个菜单项的菜单, 我们全局申明了菜单项的 id。
1
2
3
|
ID_MENU_NEW=wx.NewId()
ID_MENU_OPEN=wx.NewId()
ID_MENU_SAVE=wx.NewId()
|
函数 wx.NewId() 可以创建新的唯一 id。
1
2
3
|
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 可是识别所有三个菜单项。
1
2
3
4
5
6
7
8
|
eid=e.GetId()
ifeid==ID_MENU_NEW:
msg='New menu item selected'
elifeid==ID_MENU_OPEN:
msg='Open menu item selected'
elifeid==ID_MENU_SAVE:
msg='Save menu item selected'
|
从 event 对象我们得到 id, 根据 id 的不同,我们准备不同的信息,并将它输出在应用的状态栏。
绘制事件
绘制事件即 Paint Event,当窗口重绘时会触发该事件,比如当我们调整窗口大小或者最大化的时候。 当然,也可以程序化的触发绘制事件。比如,当我们调用 SetLabel() 函数来修改 wx.StaticText 组件的文字时,就会触发绘制事件。注意,窗口最小化不会触发绘制事件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
#!/usr/bin/python
# -*- coding: utf-8 -*-
importwx
classExample(wx.Frame):
def__init__(self,*args,**kw):
super(Example,self).__init__(*args,**kw)
self.InitUI()
defInitUI(self):
self.count=0
self.Bind(wx.EVT_PAINT,self.OnPaint)
self.SetSize((250,180))
self.Centre()
self.Show(True)
defOnPaint(self, e):
self.count+=1
self.SetTitle(str(self.count))
defmain():
ex=wx.App()
Example(None)
ex.MainLoop()
if__name__=='__main__':
main()
|
在上面的例子中,我们对绘制事件进行计数,并将当前数目设置为 frame 窗口的标题。
1
|
self.Bind(wx.EVT_PAINT,self.OnPaint)
|
上面的代码将 wx.EVT_PAINT 事件绑定至 OnPaint 函数。
1
2
3
4
|
defOnPaint(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 键可以改变焦点。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
|
#!/usr/bin/python
# -*- coding: utf-8 -*-
importwx
classMyWindow(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)
defOnPaint(self, e):
dc=wx.PaintDC(self)
dc.SetPen(wx.Pen(self.color))
x, y=self.GetSize()
dc.DrawRectangle(0,0, x, y)
defOnSize(self, e):
self.Refresh()
defOnSetFocus(self, e):
self.color='#0099f7'
self.Refresh()
defOnKillFocus(self, e):
self.color='#b3b3b3'
self.Refresh()
classExample(wx.Frame):
def__init__(self,*args,**kw):
super(Example,self).__init__(*args,**kw)
self.InitUI()
defInitUI(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)
defOnMove(self, e):
printe.GetEventObject()
x, y=e.GetPosition()
self.st1.SetLabel(str(x))
self.st2.SetLabel(str(y))
defmain():
ex=wx.App()
Example(None)
ex.MainLoop()
if__name__=='__main__':
main()
|
在上面这个例子中,我们有4个 panel。 获得当前焦点的 panel 被高亮显示。
1
2
|
self.Bind(wx.EVT_SET_FOCUS,self.OnSetFocus)
self.Bind(wx.EVT_KILL_FOCUS,self.OnKillFocus)
|
上面的代码中,我们把两个焦点事件绑定至事件处理函数。
1
2
3
4
5
6
7
|
defOnPaint(self, e):
dc=wx.PaintDC(self)
dc.SetPen(wx.Pen(self.color))
x, y=self.GetSize()
dc.DrawRectangle(0,0, x, y)
|
在 OnPaint() 函数中,我们在窗口上进行了绘制。外框的颜色取决于窗口是否获得焦点,如果获得焦点,则使用蓝色。
[pytho]
def OnSetFocus(self, e):
self.color = ‘#0099f7’
self.Refresh()
[/python]
在 OnSetFocus() 函数中,我们设置了 self.color 为某种蓝色,接着刷新 frame 窗口,这会触发所有子部件的绘制事件。各个窗口会被重绘,获取焦点的窗口将得到一个蓝色外框。
图:焦点事件
键盘事件
当我们在键盘上按下按钮时,一个 wx.KeyEvent 会被触发并被发送到当前焦点 widget。有三种不同的键盘事件:
- wx.EVT_KEY_DOWN
- wx.EVT_KEY_UP
- wx.EVT_CHAR
一个常用的需求是,当 Esc 键被按下时,退出整个应用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
#!/usr/bin/python
# -*- coding: utf-8 -*-
importwx
classExample(wx.Frame):
def__init__(self,*args,**kw):
super(Example,self).__init__(*args,**kw)
self.InitUI()
defInitUI(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)
defOnKeyDown(self, e):
key=e.GetKeyCode()
ifkey==wx.WXK_ESCAPE:
ret =wx.MessageBox('Are you sure to quit?','Question',
wx.YES_NO | wx.NO_DEFAULT,self)
ifret==wx.YES:
self.Close()
defmain():
ex=wx.App()
Example(None)
ex.MainLoop()
if__name__=='__main__':
main()
|
在这个例子中,我们处理了 Esc 键的按下事件,当按下 Esc 时,会弹出对话框询问,是否关闭应用。
1
|
pnl.Bind(wx.EVT_KEY_DOWN,self.OnKeyDown)
|
上面代码将 EVT_KEY_DOWN 事件绑定至 self.OnKeyDown() 函数。
1
|
key=e.GetKeyCode()
|
上面代码得到了按下键的编号。
1
|
ifkey==wx.WXK_ESCAPE:
|
我们检查了键编号,看所按下的键是否是 Esc,它的键编号是 wx.WXK_ESCAPE。