Tkinter:如何使用线程来防止主要事件循环“冻结”
我有一个“开始”button和一个进度条小GUItesting。 期望的行为是:
- 点击开始
- 进度条振荡5秒钟
- 进度条停止
观察到的行为是“开始”button冻结5秒,然后显示一个进度条(不振荡)。
这是我的代码到目前为止:
class GUI: def __init__(self, master): self.master = master self.test_button = Button(self.master, command=self.tb_click) self.test_button.configure( text="Start", background="Grey", padx=50 ) self.test_button.pack(side=TOP) def progress(self): self.prog_bar = ttk.Progressbar( self.master, orient="horizontal", length=200, mode="indeterminate" ) self.prog_bar.pack(side=TOP) def tb_click(self): self.progress() self.prog_bar.start() # Simulate long running process t = threading.Thread(target=time.sleep, args=(5,)) t.start() t.join() self.prog_bar.stop() root = Tk() root.title("Test Button") main_ui = GUI(root) root.mainloop()
根据Bryan Oakley的信息,我明白我需要使用线程。 我试图创build一个线程,但我猜测,因为线程是从主线程内启动,它并没有帮助。
我的想法是将逻辑部分放在不同的类中,并在该类中实例化GUI,类似于A. Rodas的示例代码。
我的问题:
我无法弄清楚如何编码,所以这个命令:
self.test_button = Button(self.master, command=self.tb_click)
调用位于另一个类中的函数。 这是一件坏事要做还是甚至可能? 我将如何创build一个可以处理self.tb_click的第二类? 我试着跟着A. Rodas的例子代码,它工作的很好。 但是我不知道如何在触发一个动作的Button小部件的情况下实现他的解决scheme。
如果我应该从单个GUI类中处理线程,那么如何创build一个不干扰主线程的线程呢?
当您在主线程中join新线程时,将等待线程完成,因此即使使用multithreading,GUI也会阻塞。
如果要将逻辑部分放在不同的类中,可以直接对Thread进行子类化,然后在按下button时启动该类的新对象。 Thread的这个子类的构造函数可以接收一个Queue对象,然后你就可以和GUI部分进行通信。 所以我的build议是:
- 在主线程中创build一个队列对象
- 创build一个访问该队列的新线程
- 定期检查主线程中的队列
然后你必须解决用户点击两次相同的button会发生什么问题(每次点击都会产生一个新的线程),但是你可以通过禁用启动button并在你自己调用之后再次启用它来修复它self.prog_bar.stop()
。
import Queue class GUI: # ... def tb_click(self): self.progress() self.prog_bar.start() self.queue = Queue.Queue() ThreadedTask(self.queue).start() self.master.after(100, self.process_queue) def process_queue(self): try: msg = self.queue.get(0) # Show result of the task if needed self.prog_bar.stop() except Queue.Empty: self.master.after(100, self.process_queue) class ThreadedTask(threading.Thread): def __init__(self, queue): threading.Thread.__init__(self) self.queue = queue def run(self): time.sleep(5) # Simulate long running process self.queue.put("Task finished")
问题是t.join()阻塞了click事件,主线程不回到事件循环来处理重绘。 请参阅为什么tkk进度条在Tkinter进程中出现或TTK进度条在发送电子邮件时被阻止
我将提交替代解决scheme的基础。 它本身并不是特定于Tk进度条的,但是对于这一点当然可以很容易地实现。
这里有一些类允许你在Tk的后台运行其他任务,在需要的时候更新Tk控件,而不是lockinggui!
这里是类TkRepeatingTask和BackgroundTask:
import threading class TkRepeatingTask(): def __init__( self, tkRoot, taskFuncPointer, freqencyMillis ): self.__tk_ = tkRoot self.__func_ = taskFuncPointer self.__freq_ = freqencyMillis self.__isRunning_ = False def isRunning( self ) : return self.__isRunning_ def start( self ) : self.__isRunning_ = True self.__onTimer() def stop( self ) : self.__isRunning_ = False def __onTimer( self ): if self.__isRunning_ : self.__func_() self.__tk_.after( self.__freq_, self.__onTimer ) class BackgroundTask(): def __init__( self, taskFuncPointer ): self.__taskFuncPointer_ = taskFuncPointer self.__workerThread_ = None self.__isRunning_ = False def taskFuncPointer( self ) : return self.__taskFuncPointer_ def isRunning( self ) : return self.__isRunning_ and self.__workerThread_.isAlive() def start( self ): if not self.__isRunning_ : self.__isRunning_ = True self.__workerThread_ = self.WorkerThread( self ) self.__workerThread_.start() def stop( self ) : self.__isRunning_ = False class WorkerThread( threading.Thread ): def __init__( self, bgTask ): threading.Thread.__init__( self ) self.__bgTask_ = bgTask def run( self ): try : self.__bgTask_.taskFuncPointer()( self.__bgTask_.isRunning ) except Exception as e: print repr(e) self.__bgTask_.stop()
这是一个Tktesting,演示了这些的使用。 只要将这个附加到模块的底部,如果你想看到演示的实践:
def tkThreadingTest(): from tkinter import Tk, Label, Button, StringVar from time import sleep class UnitTestGUI: def __init__( self, master ): self.master = master master.title( "Threading Test" ) self.testButton = Button( self.master, text="Blocking", command=self.myLongProcess ) self.testButton.pack() self.threadedButton = Button( self.master, text="Threaded", command=self.onThreadedClicked ) self.threadedButton.pack() self.cancelButton = Button( self.master, text="Stop", command=self.onStopClicked ) self.cancelButton.pack() self.statusLabelVar = StringVar() self.statusLabel = Label( master, textvariable=self.statusLabelVar ) self.statusLabel.pack() self.clickMeButton = Button( self.master, text="Click Me", command=self.onClickMeClicked ) self.clickMeButton.pack() self.clickCountLabelVar = StringVar() self.clickCountLabel = Label( master, textvariable=self.clickCountLabelVar ) self.clickCountLabel.pack() self.threadedButton = Button( self.master, text="Timer", command=self.onTimerClicked ) self.threadedButton.pack() self.timerCountLabelVar = StringVar() self.timerCountLabel = Label( master, textvariable=self.timerCountLabelVar ) self.timerCountLabel.pack() self.timerCounter_=0 self.clickCounter_=0 self.bgTask = BackgroundTask( self.myLongProcess ) self.timer = TkRepeatingTask( self.master, self.onTimer, 1 ) def close( self ) : print "close" try: self.bgTask.stop() except: pass try: self.timer.stop() except: pass self.master.quit() def onThreadedClicked( self ): print "onThreadedClicked" try: self.bgTask.start() except: pass def onTimerClicked( self ) : print "onTimerClicked" self.timer.start() def onStopClicked( self ) : print "onStopClicked" try: self.bgTask.stop() except: pass try: self.timer.stop() except: pass def onClickMeClicked( self ): print "onClickMeClicked" self.clickCounter_+=1 self.clickCountLabelVar.set( str(self.clickCounter_) ) def onTimer( self ) : print "onTimer" self.timerCounter_+=1 self.timerCountLabelVar.set( str(self.timerCounter_) ) def myLongProcess( self, isRunningFunc=None ) : print "starting myLongProcess" for i in range( 1, 10 ): try: if not isRunningFunc() : self.onMyLongProcessUpdate( "Stopped!" ) return except : pass self.onMyLongProcessUpdate( i ) sleep( 1.5 ) # simulate doing work self.onMyLongProcessUpdate( "Done!" ) def onMyLongProcessUpdate( self, status ) : print "Process Update: %s" % (status,) self.statusLabelVar.set( str(status) ) root = Tk() gui = UnitTestGUI( root ) root.protocol( "WM_DELETE_WINDOW", gui.close ) root.mainloop() if __name__ == "__main__": tkThreadingTest()
我将强调BackgroundTask的两个导入点:
1)在后台任务中运行的函数需要一个函数指针,它将调用和尊重,这样可以在中途取消任务 – 如果可能的话。
2)当您退出应用程序时,您需要确保后台任务已经停止。 即使你的gui被closures了,那个线程仍然会运行,如果你不解决的话!