wxPython 实践(四)事件响应

wxPython 实践(一)概述
wxPython 实践(二)基础控件
wxPython 实践(三)页面布局
wxPython 实践(四)事件响应
wxPython 实践(五)高级控件


官网:
https://docs.wxpython.org/wx.ComboCtrl.html
https://www.w3ccoo.com/wxpython/wxpython_event_handling.html
https://wintel.blog.youkuaiyun.com/article/details/130788569?spm=1001.2014.3001.5502
https://wintel.blog.youkuaiyun.com/article/details/130788586?spm=1001.2014.3001.5502
https://wintel.blog.youkuaiyun.com/article/details/130788606?spm=1001.2014.3001.5502

1. 事件驱动编程模型

1.1. 事件循环机制解析

在 GUI 编程中,事件循环是程序响应用户操作的基础。
wxPython 使用事件循环来处理各种用户交互事件,如按键、鼠标点击、窗口大小调整等。事件循环的工作方式是不断地检查事件队列,取出事件并分发给相应的处理函数进行响应。
事件循环的运行大致分为以下几个步骤:

  1. 事件捕获:当用户进行某些操作时,如点击鼠标或按键,系统会生成一个事件。
  2. 事件排队:生成的事件会被放入到一个事件队列中。
  3. 事件分发:事件循环从事件队列中取出事件,根据事件类型和相关联的控件,将事件分发到相应的事件处理函数。
  4. 事件处理:事件处理函数根据传入的事件对象的属性和方法执行相应的逻辑,并返回结果。

以下是一个简化的代码示例,展示如何在 wxPython 应用中启动事件循环:

import wx
 
def OnQuit(event):
    print("退出程序")
    wx.GetApp().Exit()
 
# 创建应用实例
app = wx.App(False)
# 创建一个窗口
frame = wx.Frame(None, title="事件循环示例")
# 绑定事件
frame.Bind(wx.EVT_CLOSE, OnQuit)
 
# 显示窗口
frame.Show()
# 进入事件循环
app.MainLoop()

在这个例子中,我们创建了一个 wx.Frame 窗口对象,并将其绑定到了关闭事件 EVT_CLOSE 上,事件处理函数为 OnQuit 。当窗口收到关闭事件时,会调用 OnQuit 函数,并退出程序。

1.2. 事件处理函数的设计原则

设计良好的事件处理函数是构建高效、稳定 GUI 应用程序的关键。以下是一些设计事件处理函数的基本原则:

  • 专注单一功能 :每个事件处理函数应该只负责一种类型事件的处理,避免函数过于复杂。
  • 代码可重用性 :尽量编写通用的代码片段,使其能够用于多个事件处理函数。
  • 避免阻塞操作 :不要在事件处理函数中进行长时间的计算或耗时操作,这将阻塞事件循环,影响 GUI 的响应性。
  • 合理使用回调和委托 :对于一些耗时操作,可以采用回调函数或委托给其他线程处理。

例如,在处理文本输入框的输入事件时,可以使用如下方式设计事件处理函数,确保程序响应性:

def OnTextEnter(event):
    text_content = event.GetEventObject().GetValue()
    print(f"输入内容为: {text_content}")
    # 其他处理逻辑...

通过上述原则设计的事件处理函数,能够确保事件响应流程的高效性和程序的稳定性。

2. 事件绑定和响应函数编写

2.1. 绑定事件

在 GUI 应用中,事件绑定是将特定事件与事件处理函数关联起来的过程。wxPython 提供了多种方式来绑定事件:

  • 直接绑定 :使用控件的 Bind() 方法直接绑定事件类型和对应的事件处理函数。这是最常见的方式,适用于大多数情况。
Bind(event, handler, source=None, id=wx.ID_ANY, id2=wx.ID_ANY)
event 是一个EVT_*对象,它用于指定事件类型;
handler 为事件处理程序,就是程序绑定到事件的方法;
source 当我们希望区分来自不同控件的相同事件类型时,这时候需要使用 source 参数。
id 参数在有多个按钮、菜单项等时使用。id 用于区分它们。
id2 当需要将一个处理程序绑定到一个 id 范围时,例如 EVT_MENU_RANGE,则使用id2。
  • 通过类内部绑定 :在 wx.App 派生类中使用 init 方法或者 init.EventTable 方法绑定。
class MyApp(wx.App):
    def __init__(self, redirect=False, filename=None):
        wx.App.__init__(self, redirect, filename)
        self.InitEventTable()
    def InitEventTable(self):
        self.Bind(wx.EVT_BUTTON, self.OnButtonClick, self.button)
 
app = MyApp()
  • 使用 EVT_CMD_* 宏 :这种方式更底层一些,用于更复杂的事件绑定场景。
  • 使用 event.Unbind() 解绑 :如果需要取消已经绑定的事件,可以使用 Unbind 方法。

绑定事件需要明确指定事件类型和对应的处理函数。每个控件都有自己的事件类型,例如 wx.EVT_BUTTON 用于按钮点击事件。事件处理函数则根据事件对象的类型来执行不同的逻辑。

2.2. 事件传播(Skip)

在 wxPython 中,有两种类型的事件,基本事件和命令事件,这两者的传播方式是不同的:

  • 命令事件可以传播,它沿子控件向父控件进行传播;
  • 基本事件则不会传播到父控件,比如对于 wx.CloseEvent,这是一个基本事件,将其传播到父控件是没有意义的。

在默认情况下,在事件处理程序中捕获事件后,事件将停止传播。为了继续传播,可以调用 Skip() 方法使事件继续传播。

def OnButtonClicked(self, e):
        print("event reached button class")
        e.Skip()

通过 Skip,可以将按钮事件,一层一层传播出去。

2.3. 否决事件(Veto)

在某些时候,我们需要终止事件处理,在这种情况下,我们可以调用方法Veto()来实现这个功能。

import wx
 
class MyEvent(wx.Frame):
    def __init__(self, parent, title):
        # super(MyWin, self).__init__(parent, title)
        super().__init__(parent, title = title) 
        self.SetTitle(title=title)

        self.Bind(wx.EVT_CLOSE, self.OnCloseWindow)

        self.Centre()
        self.Show()

    def OnCloseWindow(self, e):
        dlg = wx.MessageDialog(None, "确定要退出?", "问题", wx.YES_NO|wx.NO_DEFAULT|wx.ICON_QUESTION)
        ret = dlg.ShowModal()
 
        if(ret == wx.ID_YES):
            self.Destroy()
        else:
            e.Veto()
        
if __name__ == '__main__':
    app = wx.App()
    window = MyEvent(None, title="Event Veto")
    window.Show()
    app.MainLoop()

在这里插入图片描述
在这个例子中,当我们处理 wx.CloseEvent 时,OnCloseWindow 方法被调用。在许多应用程序中,我们希望防止在进行一些更改时意外关闭窗口。要做到这一点,我们可以绑定 wx.EVT_CLOSE 事件绑定器 来实现。
通过在关闭窗口前,弹出一个消息对话框,根据消息对话框的返回值,来决定关闭应用,还是取消关闭应用,取消即否决,不做处理

3. 事件对象和事件传递

3.1. 标识符

3.1.1. 窗口标识符

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

  • 让系统自动生成一个标志符;
  • 使用系统中定义的标志符;
  • 创建自己使用的标识符。
wx.Button(parent, -1)
wx.Button(parent, wx.ID_ANY)

将标志符参数设置为 -1 或者 wx.ID_ANY,则表示让 wxPython 系统自动生成一个标志符。
自动创建的标识符的值总是为负值,而用户自己指定的标志符则必须为正值。
如果需要获得控件的标志符,可以使用方法 GetId() 。
标准标识符

3.1.2. 事件标准标识符

在 wxPython 系统中,包含一些标准标志符,比如 wx.ID_SAVE,wx.ID_New 等等,下面的代码演示了如何使用标志标志符。

import wx
 
class MyEvent(wx.Frame):
    def __init__(self, parent, title):
        super().__init__(parent, title = title) 
        self.SetTitle(title=title)
        panel = wx.Panel(self)
        grid = wx.GridSizer(3, 2, 0, 0)
 
        grid.AddMany([(wx.Button(panel, wx.ID_CANCEL), 0, wx.TOP | wx.LEFT, 9),
            (wx.Button(panel, wx.ID_DELETE), 0, wx.TOP, 9),
            (wx.Button(panel, wx.ID_SAVE), 0, wx.LEFT, 9),
            (wx.Button(panel, wx.ID_EXIT)),
            (wx.Button(panel, wx.ID_STOP), 0, wx.LEFT, 9),
            (wx.Button(panel, wx.ID_NEW))])
        
        self.Bind(wx.EVT_BUTTON, self.OnQuitApp, id=wx.ID_EXIT)
 
        panel.SetSizer(grid)

    def OnQuitApp(self, e):
        self.Close()

if __name__ == '__main__':
    app = wx.App()
    window = MyEvent(None, title="Event ID")
    window.Show()
    app.MainLoop()

在上面的示例中,我们使用一些标准按钮标志符,如果在 Linux 系统中运行,则会在这些按钮上自动添加相应的图标。
六个按钮,使用了六个标准标志符 wx.ID_CANCEL, wx.ID_DELETE, wx.ID_SAVE, wx.ID_EXIT, wx.ID_STOP和wx.ID_New。
在 wxPython 系统中,建议在可能的情况下,使用标准标识符, 因为这些标准标志符可以在某些平台上提供一些标准的图形或者行为。

3.1.3. 自定义事件标识符

使用 wx.NewId() 方法 可创建一个新的独一无二的标志符。

ID_MENU_NEW = wx.NewId()
ID_MENU_OPEN = wx.NewId()
ID_MENU_SAVE = wx.NewId()

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)

eid = e.GetId()

在事件处理方法中通过 GetId() 函数获得事件标识符值,通过判断该值,就可以确定当前被点击的菜单项,然后显示相应的信息。

3.2. 事件对象方法

当事件被触发时,wxPython 会创建一个事件对象,并将其传递给对应的事件处理函数。事件对象包含了事件相关的详细信息,如事件类型、事件发生的时间等。通过这些信息,开发者可以编写相应的逻辑来响应事件。
事件对象主要有以下几个重要的属性和方法:

  • GetEventObject() :获取触发事件的控件对象。
  • GetId() :获取事件相关联的控件ID。
  • GetEventType() :获取事件类型。
  • GetTime() :获取事件发生的时间。

示例代码:

def OnButtonClick(event):
    button = event.GetEventObject()
    print(f"按钮被点击,按钮ID为: {button.GetId()}")
    print(f"事件类型为: {event.GetEventType()}")
    print(f"事件发生时间为: {event.GetTime()}")

3.3. 事件传递机制和中断

事件传递(或事件冒泡)是指事件从发生控件开始,逐级向上层控件传递直到根控件的过程。在某些情况下,可能需要中断事件的进一步传递。wxPython 允许开发者在事件处理函数中控制事件的传递行为。
中断事件传递的方法:

  • 使用 event.Skip(False) 可以阻止事件继续向上层控件传递。如果传递为 True ,则继续事件的传递。
  • 使用 event.StopPropagation() 会阻止事件继续传播,并阻止默认行为。

示例代码:

def OnButtonClick(event):
    # 阻止事件继续传递
    event.Skip(False)
    print("按钮点击事件已被处理,不会继续传递")

通过合理使用事件传递机制,可以精确控制事件处理流程,提高程序的灵活性和控制力。
以上就是关于事件处理机制和函数编写的详细章节内容,包括了事件驱动编程模型的介绍、事件绑定和响应函数编写以及事件对象和事件传递的深入探讨。在掌握了这些基础知识后,我们可以更好地构建交互式的 GUI 应用程序。

4. 常见事件处理函数的实现

在 wxPython 中,包含了 GUI 应用所必须的一些常用事件,这些事件是构建一个 GUI 应用所必不可少的,比如绘制事件(wx.PaintEvent),焦点事件(wx.FocusEvent), 键盘事件(wx.KeyEvent),鼠标事件(wx.MouseEvent)等等。

4.1. 绘制事件(wx.PaintEvent)

当窗口的内容需要重新绘制的时候,比如当我们调整窗口大小或者最大化的时候,会发送一个绘制事件(Paint Event)。
当然我们也可以通过程序来触发绘制事件,比如,在调用 SetLabel() 方法来修改wx.StaticText 控件的文字信息时,就会触发绘制事件。
注意:窗口最小化不会触发绘制事件。

import wx
 
class MyEvent(wx.Frame):
    def __init__(self, parent, title):
        super().__init__(parent, title = title) 
        self.SetTitle(title=title)
        self.count = 0
        self.Bind(wx.EVT_PAINT, self.OnPaint)
 
        # self.SetSize(400, 280)
        self.Centre()
 
    def OnPaint(self, e):
        self.count += 1
        dc = wx.PaintDC(self)
        text = "Number of paint events: {0}".format(self.count)
        # 从客户区指定像素点(20, 20)显示text
        dc.DrawText(text, 20, 20)
        
if __name__ == '__main__':
    app = wx.App()
    window = MyEvent(None, title="PaintEvent")
    window.Show()
    app.MainLoop()

在这里插入图片描述
在上面的例子中,我们对绘制事件进行计数,并在窗口中打印出计数值。

4.2. 焦点事件(wx.FocusEvent)

当窗口的焦点发生变化时,将发送焦点事件。
焦点表明了当前应用中被选中的控件(widget),当控件被选中时,从键盘输入或从剪贴板拷入的文本将发送到该控件。

  • 当一个控件获得焦点时,就会触发 wx.EVT_SET_FOCUS 事件,
  • 当一个控件失去焦点时,则会触发 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 = "#ff0000"
        self.Refresh()
 
    def OnKillFocus(self, e):
        # 当离开焦点区域时,将画笔颜色恢复为初始颜色并重绘客户区
        self.color = "#b3b3b3"
        self.Refresh()
 
class MyEvent(wx.Frame):
    def __init__(self, parent, title):
        super().__init__(parent, title = title) 
        self.SetTitle(title=title)

        # 创建一个2x2网格布局
        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(400, 280)
        self.Centre()
        
if __name__ == '__main__':
    app = wx.App()
    window = MyEvent(None, title="FocusEvent")
    window.Show()
    app.MainLoop()

在这里插入图片描述
在上面的例子中,创建了 4个panel,获得焦点的 panel 被高亮显示。
在 panel 获得焦点时,在 OnPaint() 方法中将绘制一个红色的边框。

4.3. 键盘事件(wx.KeyEvent)

键盘事件类包含有关按键和释放事件的信息。
键盘事件所携带的主要信息是正在按下或释放的键。它可以使用 GetUnicodeKey,GetKeyCode 或 GetRawKeyCode 函数之一来访问。

  • GetUnicodeKey 适用于可打印字符,它适用于任何键,包括使用国家键盘布局时可以输入的非 latin -1 字符。
  • GetKeyCode 应该用于处理与 wx 对应的特殊字符(如光标箭头键或 HOME 或 INS 等)。虽然因为兼容性要求,GetKeyCode 还返回 Latin-1 键的字符代码,但它一般不适用于Unicode 字符,并且对于任何非 Latin-1 键将返回 WXK_NONE。
  • 如果 GetUnicodeKey 和 GetKeyCode 都返回 WXK_NONE,那么该键没有 WXK_xxx 映射,GetRawKeyCode 可以用来区分键,但原始键代码是特定于平台上的。

出于这些原因,建议总是使用 GetUnicodeKey,只有当 GetUnicodeKey 返回 WXK_NONE 时才返回 GetKeyCode,这意味着该事件对应于一个不可打印的特殊键,如果 GetKeyCode 也返回 WXK_NONE,则可考虑检查 GetRawKeyCode,或者直接忽略该键。

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

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

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

import wx

class MyEvent(wx.Frame):
    def __init__(self, parent, title):
        super().__init__(parent, title = title) 
        self.SetTitle(title=title)

        panel = wx.Panel(self)
        panel.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
        panel.SetFocus()

        self.Centre()
 
    def OnKeyDown(self, e):
        key = e.GetKeyCode()
        if key == wx.WXK_ESCAPE:
            ret = wx.MessageBox("确定要退出应用?", "问题", wx.YES_NO|wx.NO_DEFAULT, self)
            if ret == wx.YES:
                self.Close()

if __name__ == '__main__':
    app = wx.App()
    window = MyEvent(None, title="KeyEvent")
    window.Show()
    app.MainLoop()

在这个例子中,我们处理了 Esc 键按下事件,当按下 Esc 键时,会弹出一个对话框,询问是否关闭应用。
上面代码将 EVT_KEY_DOWN 事件绑定至 self.OnKeyDown() 方法。在 OnKeyDown方法中,通过 key = e.GetKeyCode() 获得按键值,然后检查按键值是否为wx.WXK_EACAPE,如果是,则弹出对话框,询问是否退出应用。
在这里插入图片描述

4.4. 鼠标事件(wx.MouseEvent)

鼠标事件类包含关于鼠标生成的事件的信息。
包括鼠标按钮按下并释放事件和鼠标移动事件。

  • MOUSE_BTN_LEFT 作为鼠标左键,
  • MOUSE_BTN_MIDDLE 作为中间键,
  • MOUSE_BTN_RIGHT作为右边键。
  • 如果系统支持更多按钮,还可以生成 MOUSE_BTN_AUX1 和 MOUSE_BTN_AUX2 事件。

注意: 并不是所有的鼠标都有一个中间按钮,所以便携式应用程序应该避免依赖于它的事件(在Mac平台下,可以使用鼠标左键和控制键来模拟单击右键)。
注意: 对于 wxEVT_ENTER_WINDOW 和 wxEVT_LEAVE_WINDOW 事件的目的,如果鼠标在窗口客户端区域中,而不在它的一个子窗口中,则认为鼠标在窗口内。换句话说,父窗口不仅在鼠标完全离开窗口时接收 wxEVT_LEAVE_WINDOW 事件,而且在鼠标进入其中一个子窗口时也接收 wxEVT_LEAVE_WINDOW 事件。

与鼠标事件相关的位置用生成事件窗口的窗口坐标表示:

  • wx.Window.ClientToScreen 将其转换为屏幕坐标
  • wx.Window.ScreenToClient 将其转换为另一个窗口的窗口坐标。
import wx

class MyEvent(wx.Frame):
    def __init__(self, parent, title):
        super().__init__(parent, title = title) 
        self.SetTitle(title=title)
        self.info = ""
 
        self.Bind(wx.EVT_PAINT, self.OnPaint)
 
        self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
        self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp)
        self.Bind(wx.EVT_MOTION, self.OnMouseMove)

        self.Centre()
 
    def OnPaint(self, e):
        dc = wx.PaintDC(self)
        dc.DrawText(self.info, 20, 20)
 
    def OnLeftDown(self, e):
        self.info = "Mouse left button is pressed"
        self.Refresh()
 
    def OnLeftUp(self, e):
        self.info = "Mouse left button is released"
        self.Refresh()
 
    def OnMouseMove(self, e):
        # 如果按下左键并移动鼠标,则显示当前鼠标的坐标信息
        if e.Dragging() and e.LeftIsDown():
            x,y = e.GetPosition()
            self.info = "current pos: x=" + str(x) + ", y=" + str(y)
            self.Refresh()

if __name__ == '__main__':
    app = wx.App()
    window = MyEvent(None, title="MouseEvent")
    window.Show()
    app.MainLoop()

在这里插入图片描述
上面的例子我们实现了,鼠标左键按下,鼠标左键释放,以及左键按下移动的情况,当鼠标左键按下或者释放时,在窗口中输出相应的信息,如果鼠标左键按下且移动,则在窗口中显示鼠标的位置信息。

4.5. 其他事件

wx.KeyEvent:按下或释放按键时发生;
wx.PaintEvent:每当需要重绘窗口的内容时生成;
wx.MouseEvent:包含有关鼠标活动(例如按下或拖动鼠标按钮)引起的任何事件的数据;
wx.ScrollEvent:与 wxScrollbar 和 wxSlider 等可滚动控件相关联;
wx.CommandEvent:包含源自许多小部件的事件数据,例如按钮、对话框、剪贴板等;
wx.MenuEvent:不同的菜单相关事件,不包括菜单命令按钮的点击;
wx.ColourPickerEvent:wxColourPickerCtrl 生成的事件;
wx.DirFilePickerEvent:FileDialog 和 DirDialog 生成的事件。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值