18.6 创建一个多线程的wxPython应用程序
在大多数的GUI应用程序中,在应用程序的后台中长期执行一个处理过程而不干涉用户与应用程序的其它部分的交互是有好处的。允许后台处理的机制通常是产生一个线程并在该线程中长期执行一个处理过程。对于wxPython的多线程,在这一节我们有两点需要特别说明。
最重要的一点是,GUI的操作必须发生在主线程或应用程序的主循环所处的地方中。在一个单独的线程中执行GUI操作对于无法预知的程序崩溃和调试来说是一个好的办法。基于技术方面的原因,如许多Unix的GUI库不是线程安全性的,以及在微软Windows下UI对象的创建问题,wxPython没有设计它自己的发生在多线程中的事件,所以我们建议你也不要尝试。
上面的禁令包括与屏幕交互的任何项目,尤其包括wx.Bitmap对象。
对于wxPython应用程序,关于所有UI的更新,后台线程只负责发送消息给UI线程,而不关心GUI的更新。幸运的是,wxPython没有强制限定你能够有的后台线程的数量。
在这一节,我们将关注几个wxPython中实现多线程的方法。最常用的技术是使用wx.CallAfter()函数,一会我们会讨论它。然后,我们将看一看如何使用Python的队列对象来设置一个并行事件队列。最后,我们将讨论如何为多线程开发一个定制的解决方案。
18.6.1 使用全局函数wx.CallAfter()
例18.5显示了一个使用线程的例子,它使用了wxPython的全局函数wx.CallAfter(),该函数是传递消息给你的主线程的最容易的方法。wx.CallAfter()使得主线程在当前的事件处理完成后,可以对一个不同的线程调用一个函数。传递给wx.CallAfter()的函数对象总是在主线程中被执行。
图18.4显示了多线程窗口的运行结果。
图18.4
例18.5显示了产生图18.4的代码
例18.5 使用wx.CallAfter()来传递消息给主线程的一个线程例子
#-*- encoding:UTF-8 -*-
import wx
import threading
import random
class WorkerThread(threading.Thread):
"""
This just simulates some long-running task that periodically sends
a message to the GUI thread.
"""
def __init__(self, threadNum, window):
threading.Thread.__init__(self)
self.threadNum = threadNum
self.window = window
self.timeToQuit = threading.Event()
self.timeToQuit.clear()
self.messageCount = random.randint(10,20)
self.messageDelay = 0.1 + 2.0 * random.random()
def stop(self):
self.timeToQuit.set()
def run(self):#运行一个线程
msg = "Thread %d iterating %d times with a delay of %1.4f\n" \
% (self.threadNum, self.messageCount, self.messageDelay)
wx.CallAfter(self.window.LogMessage, msg)
for i in range(1, self.messageCount+1):
self.timeToQuit.wait(self.messageDelay)
if self.timeToQuit.isSet():
break
msg = "Message %d from thread %d\n" % (i, self.threadNum)
wx.CallAfter(self.window.LogMessage, msg)
else:
wx.CallAfter(self.window.ThreadFinished, self)
class MyFrame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, title="Multi-threaded GUI")
self.threads = []
self.count = 0
panel = wx.Panel(self)
startBtn = wx.Button(panel, -1, "Start a thread")
stopBtn = wx.Button(panel, -1, "Stop all threads")
self.tc = wx.StaticText(panel, -1, "Worker Threads: 00")
self.log = wx.TextCtrl(panel, -1, "",
style=wx.TE_RICH|wx.TE_MULTILINE)
inner = wx.BoxSizer(wx.HORIZONTAL)
inner.Add(startBtn, 0, wx.RIGHT, 15)
inner.Add(stopBtn, 0, wx.RIGHT, 15)
inner.Add(self.tc, 0, wx.ALIGN_CENTER_VERTICAL)
main = wx.BoxSizer(wx.VERTICAL)
main.Add(inner, 0, wx.ALL, 5)
main.Add(self.log, 1, wx.EXPAND|wx.ALL, 5)
panel.SetSizer(main)
self.Bind(wx.EVT_BUTTON, self.OnStartButton, startBtn)
self.Bind(wx.EVT_BUTTON, self.OnStopButton, stopBtn)
self.Bind(wx.EVT_CLOSE, self.OnCloseWindow)
self.UpdateCount()
def OnStartButton(self, evt):
self.count += 1
thread = WorkerThread(self.count, self)#创建一个线程
self.threads.append(thread)
self.UpdateCount()
thread.start()#启动线程
def OnStopButton(self, evt):
self.StopThreads()
self.UpdateCount()
def OnCloseWindow(self, evt):
self.StopThreads()
self.Destroy()
def StopThreads(self):#从池中删除线程
while self.threads:
thread = self.threads[0]
thread.stop()
self.threads.remove(thread)
def UpdateCount(self):
self.tc.SetLabel("Worker Threads: %d" % len(self.threads))
def LogMessage(self, msg):#注册一个消息
self.log.AppendText(msg)
def ThreadFinished(self, thread):#删除线程
self.threads.remove(thread)
self.UpdateCount()
app = wx.PySimpleApp()
frm = MyFrame()
frm.Show()
app.MainLoop()
上面这个例子使用了Python的threading模块。上面的代码使用wx.CallAfter(func,*args)传递方法给主线程。这将发送一个事件给主线程,之后,事件以标准的方式被处理,并触发对func(*args)的调用。因些,在这种情况中,线程在它的生命周期期间调用LogMessage(),并在线程结束前调用ThreadFinished()。
18.6.2 使用队列对象管理线程的通信
尽管使用CallAfter()是管理线程通信的最简单的方法,但是它并不是唯一的机制。你可以使用Python的线程安全的队列对象去发送命令对象给UI线程。这个UI线程应该在wx.EVT_IDLE事件的处理函数中写成需要接受来自该队列的命令。
本质上,你要为线程通信设置一个并行的事件队列。如果使用这一方法,那么工作线程在当它们增加一个命令对象到队列时,应该调用全局函数wx.WakeUpIdle()以确保尽可能存在在一个空闲事件。这个技术比wx.CallAfter()更复杂,但也更灵活。特别是,这个机制可以帮助你在后台线程间通信,虽然所有的GUI处理仍在主线程上。
18.6.3 开发你自已的解决方案
你也可以让你自己的工作线程创建一个wxPython事件(标准的或自定义的),并使用全局函数wx.PostEvent(window, event)将它发送给UI线程中的一个特定的窗口。该事件被添加到特定窗口的未决事件队列中,并且wx.WakeUpIdle自动被调用。这条道的好处是事件将遍历的wxPython事件设置,这意味你将自由地得到许多事件处理能力,坏处是你不得不自已管理所有的线程和wx.CallAfter()函数所为你做的事件处理。