Python Tkinter:如何使用线程防止主事件循环;冻结“;
我有一个带有“开始”按钮和进度条的小型GUI测试。期望的行为是:Python Tkinter:如何使用线程防止主事件循环;冻结“;,python,multithreading,tkinter,progress-bar,event-loop,Python,Multithreading,Tkinter,Progress Bar,Event Loop,我有一个带有“开始”按钮和进度条的小型GUI测试。期望的行为是: 单击开始 Progressbar振荡5秒 进度条停止 观察到的行为是“开始”按钮冻结5秒钟,然后显示进度条(无振荡) 以下是我目前的代码: class GUI: def __init__(self, master): self.master = master self.test_button = Button(self.master, command=self.tb_click)
- 单击开始
- Progressbar振荡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提供的信息,我知道我需要使用线程。我试着创建一个线程,但我猜,由于线程是从主线程中开始的,所以没有帮助
我的想法是将逻辑部分放在另一个类中,并从该类中实例化GUI,类似于a.Rodas的示例代码
我的问题:
我不知道如何对其进行编码,以便此命令:
self.test_button = Button(self.master, command=self.tb_click)
调用位于另一个类中的函数。这是一件坏事还是有可能?我如何创建第二个类来处理self.tb_单击?我试着遵循A.Rodas的示例代码,该代码工作得非常出色。但是我不知道如何在触发操作的按钮小部件的情况下实现他的解决方案
如果改为在单个GUI类中处理线程,如何创建一个不干扰主线程的线程?问题是t.join()会阻止单击事件,主线程不会返回事件循环来处理重绘。
请参阅或当您在主线程中加入新线程时,它将等待线程完成,因此即使您正在使用多线程,GUI也将阻塞 如果您想将逻辑部分放在不同的类中,可以直接将Thread子类化,然后在按下按钮时启动该类的新对象。这个线程子类的构造函数可以接收一个队列对象,然后您将能够与GUI部件进行通信。因此,我的建议是:
self.prog\u bar.stop()
后再次启用它来修复此问题
我将提交替代解决方案的依据。它本身并不特定于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,我要强调两点:
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()
1) 在后台任务中运行的函数需要获取一个函数指针,它将调用并尊重该指针,这允许在中途取消该任务(如果可能)
2) 您需要确保退出应用程序时后台任务已停止。如果您不解决这个问题,即使您的gui已关闭,该线程仍将运行 我使用了RxPY,它有一些很好的线程功能,可以以相当干净的方式解决这个问题。没有队列,我提供了一个在后台线程完成后在主线程上运行的函数。以下是一个工作示例:
导入接收
从rx.scheduler导入ThreadPoolScheduler
导入时间
将tkinter作为tk导入
类用户界面:
定义初始化(自):
self.root=tk.tk()
self.pool_调度程序=ThreadPoolScheduler(1)#具有1个工作线程的线程池
self.button=tk.button(text=“Do Task”,command=self.Do_Task).pack()
def do_任务(自我):
rx.empty().subscribe(
on_completed=self.long_running_任务,
调度程序=self.pool\u调度程序
)
def长时间运行任务(自我):
#你的长期任务在这里。。。如:
时间。睡眠(3)
#如果希望在主线程上进行回调:
self.root.after(5,self.on_任务完成)
任务完成时的def(自我):
pass#在主线程上运行
如果名称=“\uuuuu main\uuuuuuuu”:
ui=ui()
ui.root.mainloop()
使用此结构的另一种方法可能更干净(取决于首选项):
tk.Button(text=“Do Task”,command=self.Button\u clicked).pack()
...
已单击def按钮(自行):
def do_任务(u):
time.sleep(3)#在后台线程上运行
任务完成时的定义()
pass#在主线程上运行
rx.just(1)。订阅(
on_next=do_任务,
on_completed=lambda:self.root.after(5,on_task_done),
调度程序=self.pool\u调度程序
)
另一个漂亮的例子。谢谢你A.罗达斯:)我有一个后续问题:如果我在(100,self.process\u queue)之后注释掉self.master.after,并将其替换为简单的self.process\u queue(),行为是相同的。有没有一个很好的理由拥有自我。大师。在。。。部分?是的,使用self.master.after(100,self.process\u queue)
可以每100毫秒安排一次此方法,而self.process\u queue()
在每次调用之间不延迟地持续执行它。没有必要这样做,因此after
是一个更好的解决方案,可以定期检查内容。对不起,Rodas,我的情况与OP解释的类似,但在我的情况下,我应该在按下按钮时调用另一个类的函数,但它仍然会冻结。我不太熟悉线程
,这就是为什么我要问我应该怎么做。由于我必须调用在GUI应用程序的构造函数中创建的对象的函数(就在我单击GUI的按钮时),我应该让我的另一个类派生自