wxPython和pycairo练习记录11

wxPython 事件机制

了解事件机制是学习 wxPython 绕不过去的坎,结合各种资料和个人理解整理了一下,如果有问题请留言指出。

先看下《活学活用wxPython》书中介绍的最小实例。

import wx


class App(wx.App):

    def OnInit(self):
        frame = wx.Frame(parent=None, title="Bare")
        frame.Show()
        return True


app = App()
app.MainLoop()

每个wxPython程序必须有一个application对象和至少一个 frame对象。application对象必须是wx.App的一个实例或你在OnInit()方法中定义 的一个子类的一个实例。当你的应用程序启动的时候,OnInit()方法将被 wx.App父类调用。
一旦进入主事件循环,控制权将转交给wxPython。wxPython GUI程序主要 响应用户的鼠标和键盘事件。当一个应用程序的所有框架被关闭后,这个 app.MainLoop()方法将返回且程序退出。(出自《活学活用wxPython》)

最后两行代码的作用是:创建一个应用程序实例并进入它的主事件循环。

主事件循环

wx.App.MainLoop 的作用是执行主的 GUI 事件循环,继承自 wx.AppConsole.MainLoop,可被重写用来执行自己定义的主事件循环。

那么默认主事件循环究竟来自哪里呢?wx.AppTraits 类定义了 wx.App 的各种可配置方面。而其中一个方法 wx.AppTraits.CreateEventLoop,wxPython 用它来创建可被 wx.App.OnRun 调用的主循环,主循环为 wx.EventLoopBase 实例。
在这里插入图片描述

主事件循环到底做了些什么呢?最好的办法当然是通过 wxPython 核心源代码去分析,但是它是 C++ 写的,暂时看不懂。幸运的是,demo 文件里刚好有一个自定义主事件循环的例子,路径 wxPython-demo-4.1.1/samples/mainloop/mainloop.py 。最新的 demo 可通过这个链接下载 https://extras.wxpython.org/wxPython4/extras/4.2.0/

主事件循环简化为:

while True: # 无限循环
    self.ProcessIdle() # 空闲时处理空闲事件
    wx.GetApp().ProcessPendingEvents() # 处理挂起事件

程序进入一个无限的循环,如果没有挂起事件,就处理空闲事件,否则就处理挂起事件。每隔一段时间查看事物状态,也被称为轮询。

事件的处理

事件的定义:事件是可以被控件识别的操作,如按下确定按钮,选择某个单选按钮或者复选框。每一种控件有自己可以识别的事件,如窗体的加载、单击、双击等事件,编辑框(文本框)的文本改变事件,等等。(出自百度百科)

事件是一种结构,它保存有关传递给回调或成员函数的事件的信息。(来源:https://docs.wxpython.org/wx.Event.html

每个事件的信息由三部分构成:

  • 事件类型:这是一个 EventType 类型的值,事件的类型的唯一标识。
  • 事件类:由事件携带,每个事件都有一些与之关联的信息,这些数据由派生自 wx.Event 的类的对象表示。不同类型的事件可以使用同一个事件类。
  • 事件源:wx.Event 存储生成事件的对象和窗口标识符。由于通常有多个对象生成相同类型的事件(例如,一个典型的窗口包含多个按钮,所有按钮都生成相同的按钮单击事件),因此检查事件源对象或其 id 可以区分它们。(来源:https://docs.wxpython.org/events_overview.html

事件分类,按触发对象划分为用户事件(如 wx.EVT_MOTION)和系统事件(如 wx.EVT_PAINT),按是否可向上级窗口传播划分为命令事件(如 wx.EVT_BUTTON)和非命令事件(如 wx.EVT_KEY_DOWN)。

在 wxPython 中有一种处理事件的主要方法,它使用 wx.EvtHandler.Bind 调用并且可用于动态绑定和取消绑定处理程序,即在运行时根据某些条件,它还允许将事件直接绑定到:

wx.EvtHandler.Bind 的方法签名是:

Bind( self , event , handler , source=None , id=wx.ID_ANY , id2=wx.ID_ANY ) 
将事件绑定到事件处理程序。

参数
event – EVT_ 指定要绑定的事件类型的事件绑定器对象之一。

handler – 事件传递给 self 时要调用的可调用对象。传递 None 参数以断开事件处理程序。

source – 有时事件来自与 self 不同的窗口,但你仍想在 self 中捕获它。(例如,传递到框架的按钮事件)通过传递事件源,事件处理系统能够区分来自不同控件的相同事件类型。

id – 用于指定事件源 ID 而不是实例。

id2 – 当需要将处理程序绑定到一系列 ID 时使用,例如使用 EVT_MENU_RANGE。

第一个参数是新出现的概念,事件绑定器对象,即 wx.PyEventBinder 的实例,用于将特定事件绑定到事件处理程序。打印一下,会得到这样的结果:

>>> print(wx.EVT_PAINT)
<wx.core.PyEventBinder object at 0x0306E148>

为了避免背景擦除造成闪烁,通常会给 wx.EVT_ERASE_BACKGROUND 事件绑定一个空的处理方法,他这里的用法演示了绑定到匿名函数。

self.Bind(wx.EVT_ERASE_BACKGROUND, lambda evt:evt.Skip())
frame.Bind(wx.EVT_BUTTON, lambda evt:win32gui.SendMessage(frame.GetHandle(), win32con.WM_CLOSE,0,0), self)
frame.Bind(wx.EVT_BUTTON, lambda evt:win32gui.SendMessage(frame.GetHandle(), win32con.WM_SYSCOMMAND, win32con.SC_MAXIMIZE,0), self)

(来源:wxPython实现仿QQ登录界面https://blog.csdn.net/xugangjava/article/details/8024356

具体事件处理和传播可以查看:https://docs.wxpython.org/events_overview.html
在这里插入图片描述

要点:

  • 事件队列(event queue):已发生的但未处理的事件的一个列表。
  • 动态绑定事件处理器的动态事件表(通过 Bind 添加事件表项)
  • 事件表宏定义的所有事件处理器的静态事件表(框架内置的事件表)
  • 事件处理器链表

触发事件后通常会调用 wx.EvtHandler.AddPendingEvent 方法,将事件对象加入待处理事件队列。

这两个事件表就是用来查找事件对应的事件处理方法。

什么是宏?宏(英语:Macro)是一种批量处理的称谓。计算机科学里的宏是一种抽象(Abstraction),它根据一系列预定义的规则替换一定的文本模式。解释器或编译器在遇到宏时会自动进行这一模式替换。(出自百度百科)
应该就相当于静态变量吧,只不过它存储的值可能更长。

为什么要用到表?这个得提到表驱动。表驱动法,可以在表中查找信息而不必用很多的逻辑语句(if或Case)来把它们找出来的方法。(出自百度百科)
应该相当于平时面对很多判断的时候,使用字典直接返回键对应的值,只不过这里的键是事件,值是事件处理方法。类似的还有数据库的表,还可以通过建立索引加快查找数据位置的速度。wxPython 是通过建立事件哈希表来加快事件表的查找速度,暂时只是知道有这么回事,具体得自己看核心源码。

所以,可以把整个事件机制流程概括为:进入主事件循环,触发事件,加入待处理事件队列,轮询事件队列,查找事件对应处理方法,执行处理方法。

常见问题

self.Bind 和 self.button.Bind

https://wiki.wxpython.org/self.Bind%20vs.%20self.button.Bind
说实话,文章里的图我没看懂。概括就是,只有命令事件可以向上级窗口传播,可以用 event.Skip() 使处理程序继续搜索执行其他绑定相同事件的处理器,如果在命令事件中用 event.Skip() 则可能继续向上级窗口搜索。

重绘控件

绑定 wx.EVT_PAINT 事件,在对应事件处理器中使用 GDI 来绘制界面。绘制事件由系统触发,轮询到绘制事件就会重新绘制。在绘制事件处理器方法之外,想要主动刷新界面,也可以调用 wx.Window.Refresh 触发绘制事件,这里触发的事件也需要等待主循环轮询到才会执行。如果要立即执行绘制事件,可以使用 wx.Window.Update,但是用 Update 一般会出现一些莫名的问题。

界面卡住

如果给按钮事件 wx.EVT_BUTTON 绑定一个执行 time.sleep 的方法,点击按钮后界面就会卡住。由前面的事件机制可以知道,主线程在执行按钮点击处理方法时,时间过长,造成阻塞,后面的事件都无法处理。

wxPython 推荐的三个线程安全的方法是:wx.PostEvent wx.CallAfter wx.CallLater。
在这里插入图片描述

从上面的调用图可以看出,其实这三个方法也是把事件加入到待处理事件列表,和直接触发并没有太大不同。那么唯一可取的就是线程安全,所以要解决界面卡住的问题就需要在新的线程中调用这三个方法,通过回调来执行耗时较长的方法。

什么是线程安全?一个函数被多个并发线程反复调用,它会一直产生正确的结果,则该函数是线程安全函数。(来源:线程安全与可重入函数 https://blog.csdn.net/sdoyuxuan/article/details/73382395

具体用法参考:
https://wiki.wxpython.org/CallAfter
https://www.blog.pythonlibrary.org/2010/05/22/wxpython-and-threads/

还有支持异步传输的 wx.lib.delayedresult 和 wxasync :
https://docs.wxpython.org/wx.lib.delayedresult.html
https://github.com/sirk390/wxasync

自定义事件

第一种:
1.继承 wx.PyCommandEvent,定义 get 和 set 方法获取和设置事件参数
2.创建一个事件类型和一个绑定器对象去绑定该事件到特定的对象
3.创建自定义事件对象,设置事件参数,并用 ProcessEvent() 方法将这个实例引入事件处理系统
4.绑定自定义事件的 event handler
5.在 event handler 中响应事件

# -*- coding: utf-8 -*-
import wx


class CustomEvent(wx.PyCommandEvent):
    def __init__(self, eventType, eventId):
        super(CustomEvent, self).__init__(eventType, eventId)
        self.eventArgs = None

    def getEventArgs(self):
        return self.eventArgs

    def setEventArgs(self, args):
        self.eventArgs = args


customEventType = wx.NewEventType()
EVT_CUSTOM = wx.PyEventBinder(customEventType, 1)


class App(wx.App):
    def OnInit(self):
        self.frame = wx.Frame(parent=None, title="Test", size=(400, 300))
        self.frame.Center()
        panel = wx.Panel(parent=self.frame)
        width, height = self.frame.GetClientSize()
        button = wx.Button(parent=panel, label="button")
        bwidth, bheight = button.GetSize()
        button.SetPosition(((width - bwidth) / 2, (height - bheight) / 2))
        button.Bind(wx.EVT_BUTTON, self.OnClick)
        self.Bind(EVT_CUSTOM, self.OnHandler)
        self.frame.Show()
        return True

    def OnExit(self):
        return 0

    def OnClick(self, event):
        evt = CustomEvent(customEventType, self.frame.GetId())
        evt.setEventArgs("test123")
        self.ProcessEvent(evt)
        #wx.PostEvent(self.frame, evt)

    def OnHandler(self, event):
        dlg = wx.MessageDialog(parent=self.frame, message=event.getEventArgs(), caption="MessageBox", style=wx.OK | wx.ICON_INFORMATION)
        dlg.ShowModal()
        dlg.Destroy()


if __name__ == "__main__":
    app = App()
    app.MainLoop()

第二种:
命令事件实例 wx.lib.newevent.NewCommandEvent()
非命令事件实例 wx.lib.newevent.NewEvent()
绑定和传递同第一种方法一样,不同的是传递事件参数的方法

# -*- coding: utf-8 -*-
import wx
import wx.lib.newevent


CustomEvent, EVT_CUSTOM = wx.lib.newevent.NewEvent()
CustomCommandEvent, EVT_CUSTOM_COMMAND = wx.lib.newevent.NewCommandEvent()

self.Bind(EVT_CUSTOM, self.handler)
EVT_CUSTOM(self, self.handler)

evt = CustomEvent(attr1="hello", attr2=654)

wx.PostEvent(target, evt)

def handler(self, evt):
    attr1 = evt.attr1
    attr2 = evt.attr2

第三种:
(来源:https://wiki.wxpython.org/LongRunningTasks
1.绑定器对象通过 wx.EvtHandler.Connect 实现
2.自定义事件类继承 wx.PyEvent

# -*- coding: utf-8 -*-
import time
from threading import *
import wx

# Button definitions
ID_START = wx.NewId()
ID_STOP = wx.NewId()

# Define notification event for thread completion
EVT_RESULT_ID = wx.NewId()

def EVT_RESULT(win, func):
    """Define Result Event."""
    win.Connect(-1, -1, EVT_RESULT_ID, func) #wx.EvtHandler.Connect在动态事件表中为事件绑定创建一个条目

class ResultEvent(wx.PyEvent):
    """Simple event to carry arbitrary result data."""
    def __init__(self, data):
        """Init Result Event."""
        wx.PyEvent.__init__(self)
        self.SetEventType(EVT_RESULT_ID)
        self.data = data

# Thread class that executes processing
class WorkerThread(Thread):
    """Worker Thread Class."""
    def __init__(self, notify_window):
        """Init Worker Thread Class."""
        Thread.__init__(self)
        self._notify_window = notify_window
        self._want_abort = 0
        # This starts the thread running on creation, but you could
        # also make the GUI thread responsible for calling this
        self.start()

    def run(self):
        """Run Worker Thread."""
        # This is the code executing in the new thread. Simulation of
        # a long process (well, 10s here) as a simple loop - you will
        # need to structure your processing so that you periodically
        # peek at the abort variable
        for i in range(10):
            time.sleep(1)
            if self._want_abort:
                # Use a result of None to acknowledge the abort (of
                # course you can use whatever you'd like or even
                # a separate event type)
                wx.PostEvent(self._notify_window, ResultEvent(None))
                return
        # Here's where the result would be returned (this is an
        # example fixed result of the number 10, but it could be
        # any Python object)
        wx.PostEvent(self._notify_window, ResultEvent(10))

    def abort(self):
        """abort worker thread."""
        # Method for use by main thread to signal an abort
        self._want_abort = 1

# GUI Frame class that spins off the worker thread
class MainFrame(wx.Frame):
    """Class MainFrame."""
    def __init__(self, parent, id):
        """Create the MainFrame."""
        wx.Frame.__init__(self, parent, id, 'Thread Test')

        # Dumb sample frame with two buttons
        wx.Button(self, ID_START, 'Start', pos=(0,0))
        wx.Button(self, ID_STOP, 'Stop', pos=(0,50))
        self.status = wx.StaticText(self, -1, '', pos=(0,100))

        self.Bind(wx.EVT_BUTTON, self.OnStart, id=ID_START)
        self.Bind(wx.EVT_BUTTON, self.OnStop, id=ID_STOP)

        # Set up event handler for any worker thread results
        EVT_RESULT(self,self.OnResult)

        # And indicate we don't have a worker thread yet
        self.worker = None

    def OnStart(self, event):
        """Start Computation."""
        # Trigger the worker thread unless it's already busy
        if not self.worker:
            self.status.SetLabel('Starting computation')
            self.worker = WorkerThread(self)

    def OnStop(self, event):
        """Stop Computation."""
        # Flag the worker thread to stop if running
        if self.worker:
            self.status.SetLabel('Trying to abort computation')
            self.worker.abort()

    def OnResult(self, event):
        """Show Result status."""
        if event.data is None:
            # Thread aborted (using our convention of None return)
            self.status.SetLabel('Computation aborted')
        else:
            # Process results here
            self.status.SetLabel('Computation Result: %s' % event.data)
        # In either event, the worker is done
        self.worker = None

class MainApp(wx.App):
    """Class Main App."""
    def OnInit(self):
        """Init Main App."""
        self.frame = MainFrame(None, -1)
        self.frame.Show(True)
        self.SetTopWindow(self.frame)
        return True

if __name__ == '__main__':
    app = MainApp(0)
    app.MainLoop()

猜你喜欢

转载自blog.csdn.net/SmileBasic/article/details/127319433