转自: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。

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 -*-

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()

上面的例子展示了窗口的当前位置。

1
self.Bind(wx.EVT_MOVE, self.OnMove)

这里,我们将 wx.EVT_MOVE 事件绑定到 OnMove() 方法上。

1
2
3
4
5
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 事件 的处理并不复杂,包括以下三步:

  1. 确定 wxPython 事件 绑定器的名字,如 wx.EVT_SIZE、wx.EVT_COLSE 等;
  2. 创建一个 wxPython 事件 处理函数,该函数在事件产生时会被调用;
  3. 绑定 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 -*-


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 事件处理。

1
2
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
if ret == 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 -*-


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 定义了事件处理函数。

1
2
3
4
def OnButtonClicked(self, e):

print 'event reached button class'
e.Skip()

我们在自定义类中处理了按钮点击事件, Skip() 函数使得事件继续向上层传播。

1
2
3
event reached button class
event reached panel class
event reached frame class

我们得到了上面的输出结果,可见事件从 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 -*-


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 值。

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 -*-


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 中,这些按钮都会有图标。

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 -*-


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。

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()

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 组件的文字时,就会触发绘制事件。注意,窗口最小化不会触发绘制事件。

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 -*-


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 窗口的标题。

1
self.Bind(wx.EVT_PAINT, self.OnPaint)

上面的代码将 wx.EVT_PAINT 事件绑定至 OnPaint 函数。

1
2
3
4
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 键可以改变焦点。

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 -*-


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 被高亮显示。

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
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() 函数中,我们在窗口上进行了绘制。外框的颜色取决于窗口是否获得焦点,如果获得焦点,则使用蓝色。

1
2
3
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 键被按下时,退出整个应用。

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 -*-

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 时,会弹出对话框询问,是否关闭应用。

1
pnl.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)

上面代码将 EVT_KEY_DOWN 事件绑定至 self.OnKeyDown() 函数。

1
key = e.GetKeyCode()

上面代码得到了按下键的编号。

1
if key == wx.WXK_ESCAPE:

我们检查了键编号,看所按下的键是否是 Esc,它的键编号是 wx.WXK_ESCAPE。

在本节中,我们讨论了 wxPython 事件 的相关知识。