ZetCode

wxPython中的事件

最后修改于 2023 年 1 月 10 日

事件是每个GUI应用程序不可或缺的一部分。所有GUI应用程序都是事件驱动的。应用程序对其生命周期中生成的不同事件类型做出反应。事件主要由应用程序的用户生成。但它们也可以通过其他方式生成,例如:互联网连接、窗口管理器或计时器。因此,当我们调用MainLoop()方法时,我们的应用程序会等待事件的生成。当退出应用程序时,MainLoop()方法结束。

定义

事件是来自底层框架(通常是GUI工具包)的应用程序级信息。事件循环是一个编程结构,它等待并分派程序中的事件或消息。事件循环会重复查找要处理的事件。分发器是将事件映射到事件处理程序的进程。事件处理程序是响应事件的方法。

事件对象是与事件关联的对象。它通常是一个窗口。事件类型是已生成的唯一事件。事件绑定器是将事件类型与事件处理程序绑定的对象。

wxPython wx.EVT_MOVE 示例

在下面的示例中,我们对wx.MoveEvent事件做出反应。当我们移动窗口到新位置时,会生成该事件。该事件的事件绑定器是wx.EVT_MOVE

simple_event.py
#!/usr/bin/env python

"""
ZetCode wxPython tutorial

This is a wx.MoveEvent event demostration.

author: Jan Bodnar
website: www.zetcode.com
last modified: July 2020
"""

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((350, 250))
        self.SetTitle('Move event')
        self.Centre()

    def OnMove(self, e):

        x, y = e.GetPosition()
        self.st1.SetLabel(str(x))
        self.st2.SetLabel(str(y))


def main():

    app = wx.App()
    ex = Example(None)
    ex.Show()
    app.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()方法中的事件参数是特定于特定事件类型的对象。在我们的例子中,它是wx.MoveEvent类的实例。此对象保存有关事件的信息,例如事件对象或窗口的位置。在我们的例子中,事件对象是wx.Frame小部件。我们可以通过调用事件的GetPosition()方法来找出当前位置。

Move event
图:移动事件

wxPython事件绑定

在wxPython中处理事件的三个步骤是

在wxPython中,我们说将方法绑定到事件。有时会使用单词hook(钩子)。您可以通过调用Bind()方法来绑定事件。该方法具有以下参数

Bind(event, handler, source=None, id=wx.ID_ANY, id2=wx.ID_ANY)

event是EVT_*对象之一。它指定事件的类型。handler是要调用的对象。换句话说,它是一个程序员绑定到事件的方法。当我们要区分来自不同小部件的相同事件类型时,使用source参数。当有多个按钮、菜单项等时,使用id参数。使用id来区分它们。当需要将处理程序绑定到一系列id(例如使用EVT_MENU_RANGE)时,使用id2

请注意,方法Bind()定义在类EvtHandler中。它是一个wx.Window继承的类。wx.Window是wxPython中大多数小部件的基类。还有一个相反的过程。如果要从事件中取消绑定方法,则调用Unbind()方法。它具有与上述方法相同的参数。

否决事件

有时我们需要停止处理事件。为此,我们调用方法Veto()

event_veto.py
#!/usr/bin/env python

import wx

"""
ZetCode wxPython tutorial

In this example we veto an event.

author: Jan Bodnar
website: www.zetcode.com
last modified: July 2020
"""

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

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

    app = wx.App()
    ex = Example(None)
    ex.Show()
    app.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事件传播

事件有两种类型:基本事件和命令事件。它们在传播方面有所不同。事件传播是将事件从子小部件传递到父小部件和祖父小部件。基本事件不会传播。命令事件会传播。例如,wx.CloseEvent是一个基本事件。此事件传播到父小部件没有意义。

默认情况下,在事件处理程序中捕获的事件会停止传播。要继续传播,我们调用Skip()方法。

event_propagation.py
#!/usr/bin/env python

"""
ZetCode wxPython tutorial

This example demonstrates event propagation.

author: Jan Bodnar
website: www.zetcode.com
last modified: July 2020
"""

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

    def OnButtonClicked(self, e):

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


def main():

    app = wx.App()
    ex = Example(None)
    ex.Show()
    app.MainLoop()


if __name__ == '__main__':
    main()

在我们的示例中,我们在一个面板上有一个按钮。面板放置在一个框架小部件中。我们为所有小部件定义一个处理程序。

def OnButtonClicked(self, e):

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

我们在自定义按钮类中处理按钮点击事件。Skip()方法将事件进一步传播到面板类。

尝试省略一些Skip()方法,看看会发生什么。

窗口标识符

窗口标识符是唯一确定事件系统中窗口标识的整数。有三种方法可以创建窗口ID。

wx.Button(parent, -1)
wx.Button(parent, wx.ID_ANY)

如果我们为id参数提供-1或wx.ID_ANY,则允许wxPython为我们自动创建ID。自动创建的id始终为负数,而用户指定的id必须始终为正数。当不需要更改小部件状态时,我们通常使用此选项。例如,在应用程序的生命周期中永远不会更改的静态文本。如果需要,我们仍然可以获取ID。有一个方法GetId()用于确定ID。

default_ids.py
#!/usr/bin/env python

"""
ZetCode wxPython tutorial

In this example we use automatic ids
with wx.ID_ANY.

author: Jan Bodnar
website: www.zetcode.com
last modified: July 2020
"""

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 ids")
        self.Centre()

    def OnExit(self, event):

        self.Close()


def main():

    app = wx.App()
    ex = Example(None)
    ex.Show()
    app.MainLoop()


if __name__ == '__main__':
    main()

在此示例中,我们不在乎实际的ID值。

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

我们通过调用GetId()方法获取自动生成的ID。

建议使用标准标识符。标识符可以在某些平台上提供一些标准图形或行为。

wxPython标准ID

wxPython包含一些标准ID,例如wx.ID_SAVEwx.ID_NEW

standard_ids.py
#!/usr/bin/env python

"""
ZetCode wxPython tutorial

In this example we create buttons with standard ids.

author: Jan Bodnar
website: www.zetcode.com
last modified: July 2020
"""

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.SetTitle("Standard ids")
        self.Centre()

    def OnQuitApp(self, event):

        self.Close()


def main():

    app = wx.App()
    ex = Example(None)
    ex.Show()
    app.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))])

我们向网格布局添加了六个按钮。wx.ID_CANCELwx.ID_DELETEwx.ID_SAVEwx.ID_EXITwx.ID_STOPwx.ID_NEW是标准标识符。

self.Bind(wx.EVT_BUTTON, self.OnQuitApp, id=wx.ID_EXIT)

我们将按钮单击事件绑定到OnQuitApp()事件处理程序。id参数用于区分按钮。我们唯一地标识事件的来源。

Standard identifiers
图:标准标识符

自定义事件ID

最后一个选项是使用自己的标识符。我们定义自己的全局ID。

custom_ids.py
#!/usr/bin/env python

"""
ZetCode wxPython tutorial

In this example we use custom event ids.

author: Jan Bodnar
website: www.zetcode.com
last modified: July 2020
"""

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((350, 250))
        self.SetTitle('Custom ids')
        self.Centre()

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

    app = wx.App()
    ex = Example(None)
    ex.Show()
    app.MainLoop()


if __name__ == '__main__':
    main() 

在代码示例中,我们创建了一个带有三个菜单项的菜单。这些菜单项的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'

我们从事件对象中检索ID。根据ID值,我们准备在应用程序的状态栏中显示的消息。

wx.PaintEvent

当窗口被重绘时,会生成一个绘制事件。当我们调整窗口大小或将其最大化时,会发生这种情况。也可以通过编程方式生成绘制事件。例如,当我们调用SetLabel()方法来更改wx.StaticText小部件时。请注意,当我们最小化窗口时,不会生成绘制事件。

paint_event.py
#!/usr/bin/env python

"""
ZetCode wxPython tutorial

In this example we count paint events.

author: Jan Bodnar
website: www.zetcode.com
last modified: July 2020
"""

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.SetTitle('Paint events')
        self.SetSize((350, 250))
        self.Centre()

    def OnPaint(self, e):

        self.count += 1
        dc = wx.PaintDC(self)
        text = "Number of paint events: {0}".format(self.count)
        dc.DrawText(text, 20, 20)


def main():

    app = wx.App()
    ex  = Example(None)
    ex.Show()
    app.MainLoop()


if __name__ == '__main__':
    main()

在我们的示例中,我们计算绘制事件的数量,并在窗口上绘制生成的事件的当前数量。

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

我们将EVT_PAINT事件绑定到OnPaint()方法。

def OnPaint(self, e):

    self.count += 1
    dc = wx.PaintDC(self)
    text = "Number of paint events: {0}".format(self.count)
    dc.DrawText(text, 20, 20)

OnPaint()事件内部,我们增加计数器,并使用DrawText()方法在窗口上绘制绘制事件的数量。

wx.FocusEvent

焦点指示应用程序中当前选定的小部件。从键盘输入的或从剪贴板粘贴的文本将发送到具有焦点的那个小部件。有两个关于焦点的事件类型。wx.EVT_SET_FOCUS事件在小部件获得焦点时生成。当小部件失去焦点时,会生成wx.EVT_KILL_FOCUS。焦点通过单击或键盘键更改,通常是 TabShift+Tab

focus_event.py
#!/usr/bin/env python

"""
ZetCode wxPython tutorial

In this example we work with wx.FocusEvent.

author: Jan Bodnar
website: www.zetcode.com
last modified: July 2020
"""

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 = '#ff0000'
        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()


    def OnMove(self, e):

        print(e.GetEventObject())
        x, y = e.GetPosition()
        self.st1.SetLabel(str(x))
        self.st2.SetLabel(str(y))


def main():

    app = wx.App()
    ex = Example(None)
    ex.Show()
    app.MainLoop()


if __name__ == '__main__':
    main()

在我们的示例中,我们有四个面板。具有焦点的面板突出显示。

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 = '#ff0000'
    self.Refresh()

OnSetFocus()方法中,我们将self.color变量设置为红色。我们刷新框架窗口,它将为其所有子小部件生成绘制事件。窗口被重绘,具有焦点的一个窗口的轮廓具有新的颜色。

def OnKillFocus(self, e):

    self.color = '#b3b3b3'
    self.Refresh()

当窗口失去焦点时,将调用OnKillFocus()方法。我们更改颜色值并刷新。

Focus event
图:焦点事件

wx.KeyEvent

当我们在键盘上按下一个键时,会生成一个wx.KeyEvent。此事件将发送到当前具有焦点的小部件。有三种不同的按键处理程序

一个常见的请求是,当按下 Esc 键时关闭应用程序。

key_event.py
#!/usr/bin/env python

"""
ZetCode wxPython tutorial

In this example we work with wx.KeyEvent.

author: Jan Bodnar
website: www.zetcode.com
last modified: July 2020
"""

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((350, 250))
        self.SetTitle('Key event')
        self.Centre()

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

    app = wx.App()
    ex = Example(None)
    ex.Show()
    app.MainLoop()


if __name__ == '__main__':
    main()

在此示例中,我们处理 Esc 键按下。显示一个消息框以确认应用程序的终止。

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

我们将事件处理程序绑定到wx.EVT_KEY_DOWN事件。

key = e.GetKeyCode()

在这里,我们获取按下键的键码。

if key == wx.WXK_ESCAPE:

我们检查键码。Esc键具有wx.WXK_ESCAPE代码。

在本章中,我们讨论了wxPython中的事件。