我有一个小的GUI测试,带有“开始”按钮和进度条。期望的行为是:
观察到的行为是“开始”按钮冻结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 here提供的信息,我知道我需要使用线程。我尝试创建一个线程,但我猜测,因为线程是从主线程中启动的,所以没有帮助。
我有想法将逻辑部分放在不同的类中,并从该类中实例化GUI,类似于A. Rodas here的示例代码。
我的问题:
我无法弄清楚如何对其进行编码以便执行此命令:
self.test_button = Button(self.master, command=self.tb_click)
调用位于另一个类中的函数。这是一件坏事还是可能?我如何创建一个可以处理self.tb_click的第二类?我试着跟随A. Rodas的示例代码,它的工作非常精彩。但是我无法弄清楚如何在触发操作的Button小部件的情况下实现他的解决方案。
如果我应该在单个GUI类中处理线程,那么如何创建一个不会干扰主线程的线程呢?
答案 0 :(得分:46)
当您在主线程中加入新线程时,它将等待线程完成,因此即使您使用多线程,GUI也会阻塞。
如果要将逻辑部分放在不同的类中,可以直接对Thread进行子类化,然后在按下按钮时启动该类的新对象。 Thread的这个子类的构造函数可以接收Queue对象,然后您就可以将它与GUI部分进行通信。所以我的建议是:
然后你必须解决如果用户点击两次相同按钮会发生什么的问题(它会在每次点击时产生一个新线程),但你可以通过禁用开始按钮并在你之后再次启用它来修复它致电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")
答案 1 :(得分:2)
问题是t.join()阻止了click事件,主线程没有回到事件循环来处理重绘。 请参阅Why ttk Progressbar appears after process in Tkinter或TTK progress bar blocked when sending email
答案 2 :(得分:2)
我将提交替代解决方案的基础。它本身并不特定于Tk进度条,但它当然可以很容易地实现。
以下是一些允许您在Tk后台运行其他任务的类,在需要时更新Tk控件,而不是锁定gui!
这是类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()
这是一个Tk测试,演示了这些的使用。如果你想看到实际的演示,只需将它附加到模块的底部,其中包含这些类:
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已关闭,该线程仍会继续运行,如果您没有解决这个问题!
答案 3 :(得分:1)
我使用了RxPY,它具有一些不错的线程处理功能,可以相当干净地解决此问题。没有队列,我提供了一个在后台线程完成后在主线程上运行的函数。这是一个工作示例:
import rx
from rx.scheduler import ThreadPoolScheduler
import time
import tkinter as tk
class UI:
def __init__(self):
self.root = tk.Tk()
self.pool_scheduler = ThreadPoolScheduler(1) # thread pool with 1 worker thread
self.button = tk.Button(text="Do Task", command=self.do_task).pack()
def do_task(self):
rx.empty().subscribe(
on_completed=self.long_running_task,
scheduler=self.pool_scheduler
)
def long_running_task(self):
# your long running task here... eg:
time.sleep(3)
# if you want a callback on the main thread:
self.root.after(5, self.on_task_complete)
def on_task_complete(self):
pass # runs on main thread
if __name__ == "__main__":
ui = UI()
ui.root.mainloop()
使用此构造的另一种方法可能更简洁(取决于偏好)
tk.Button(text="Do Task", command=self.button_clicked).pack()
...
def button_clicked(self):
def do_task(_):
time.sleep(3) # runs on background thread
def on_task_done():
pass # runs on main thread
rx.just(1).subscribe(
on_next=do_task,
on_completed=lambda: self.root.after(5, on_task_done),
scheduler=self.pool_scheduler
)