wxPython 教程 (五): 事件

总目录:wxPython 教程目录 
本节内容:wxPython 事件 events
本节译自:zetcode
上一篇:wxpython 教程 (四): 布局管理
下一篇:wxpython 教程 (六): 对话框

事件是每个 GUI 应用所必须的组成部分,所有的 GUI 应用都是事件驱动的。在应用的生命周期内,需要对各种不同类型的时间做出反应。事件主要来自于应用用户的操作触发,但也可以来源于其他方式:网络连接、窗口管理、定时器等。在应用一开始,我们调用MainLoop()函数,这使得应用开始等待处理所有将生成的事件,直到我们退出程序。本节,我们将讨论 wxPython 事件 相关知识。

事件定义

事件(events)是来源于底层框架如 GUI 工具包的应用层信息。事件循环主要用来分发事件和等待信息。事件分配器将事件匹配到对应的事件处理器,事件处理器即用来对响应事件做出特定反应的函数。

简单的 wxPython 事件 样例

下面我们将描述一个简单的 移动 事件样例。

当我们移动窗口到一个新位置时,会产生一个移动事件,它的类型是 wx.MoveEvent. 该事件的绑定器是 wx.EVT_MOVE。


#!/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()  

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


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() 函数来得到当前位置。

tutorial wxpython-jiaocheng

图:移动事件

事件绑定

对 wxPython 事件 的处理并不复杂,包括以下三步:

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


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

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() 函数。


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


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。

#!/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 值。


self.Bind(wx.EVT_BUTTON,  self.OnExit, id=exitButton.GetId())

而是直接通过 GetId() 函数直接获取自动生成的 id。

推荐使用标准标识符,这些标识符可以在一些平台提供一些标准的图形或行为。


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


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, 并唯一标识了事件的来源。

tutorial wxpython-jiaocheng

图:标准标识符

最后我们可以使用自定义的窗口标识符。

#!/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。

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

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

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

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

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

[pytho]

def OnSetFocus(self, e):

self.color = ‘#0099f7’
self.Refresh()

[/python]

在 OnSetFocus() 函数中,我们设置了 self.color 为某种蓝色,接着刷新 frame 窗口,这会触发所有子部件的绘制事件。各个窗口会被重绘,获取焦点的窗口将得到一个蓝色外框。

tutorial wxpython-jiaocheng

图:焦点事件

键盘事件

当我们在键盘上按下按钮时,一个 wx.KeyEvent 会被触发并被发送到当前焦点 widget。有三种不同的键盘事件:

  • wx.EVT_KEY_DOWN
  • wx.EVT_KEY_UP
  • wx.EVT_CHAR

一个常用的需求是,当 Esc 键被按下时,退出整个应用。


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

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

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

key = e.GetKeyCode()

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

if key == wx.WXK_ESCAPE:

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

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


上一篇:wxpython 教程 (四): 布局管理            下一篇:wxpython 教程 (六): 对话框

《wxPython 教程 (五): 事件》有4个想法

  1. 您好,最近在通过您的教程自学wxpython,感谢您的分享,从您这儿学到不少知识。在这一章遇到了问题,标准标识符介绍内容中的案例代码运行失败报错,始终没能找到原因,盼能指导释疑,谢谢!是在Pycharm上使用Python3.6, wxPython4.1.1,盼复。
    错误代码:
    Traceback (most recent call last):
    File “D:/Python工程文件夹/Python程序设计教程清华版习题实操/Lib/临时.py”, line 42, in
    main()
    File “D:/Python工程文件夹/Python程序设计教程清华版习题实操/Lib/临时.py”, line 37, in main
    Example(None)
    File “D:/Python工程文件夹/Python程序设计教程清华版习题实操/Lib/临时.py”, line 9, in __init__
    self.InitUI()
    File “D:/Python工程文件夹/Python程序设计教程清华版习题实操/Lib/临时.py”, line 13, in InitUI
    grid = wx.GridSizer(3, 2)
    TypeError: GridSizer(): arguments did not match any overloaded call:
    overload 1: not enough arguments
    overload 2: argument 2 has unexpected type ‘int’
    overload 3: not enough arguments
    overload 4: not enough arguments

发表评论

电子邮件地址不会被公开。 必填项已用*标注