python - ventana - Tkinter: Cómo utilizar los hilos para evitar que el ciclo principal de eventos se congele
tkinter tamaño de ventana (3)
Tengo una pequeña prueba de GUI con un botón de "Inicio" y una barra de progreso. El comportamiento deseado es:
- Haga clic en Start
- Progressbar oscila durante 5 segundos
- Barra de progreso se detiene
El comportamiento observado es el botón "Inicio" que se congela durante 5 segundos, luego se muestra una barra de progreso (sin oscilación).
Aquí está mi código hasta ahora:
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()
Basado en la información de Bryan Oakley here , entiendo que necesito usar hilos. Traté de crear un hilo, pero supongo que dado que el hilo se inició desde el hilo principal, no ayuda.
Tuve la idea de colocar la porción lógica en una clase diferente y crear una instancia de la GUI desde esa clase, similar al código de ejemplo de A. Rodas here .
Mi pregunta:
No puedo descifrar cómo codificarlo para que este comando:
self.test_button = Button(self.master, command=self.tb_click)
llama a una función que se encuentra en la otra clase. ¿Es esto algo malo o incluso posible? ¿Cómo crearía una segunda clase que pueda manejar self.tb_click? Intenté seguir el código de ejemplo de A. Rodas, que funciona muy bien. Pero no puedo encontrar la manera de implementar su solución en el caso de un widget Button que desencadena una acción.
Si, en cambio, debo manejar el hilo desde la única clase de GUI, ¿cómo crearía un hilo que no interfiera con el hilo principal?
Cuando se une al nuevo subproceso en el subproceso principal, esperará hasta que finalice el subproceso, por lo que la GUI se bloqueará aunque esté utilizando subprocesamiento múltiple.
Si desea colocar la porción de lógica en una clase diferente, puede subclase Thread directamente y luego puede iniciar un nuevo objeto de esta clase al presionar el botón. El constructor de esta subclase de Thread puede recibir un objeto Queue y luego podrá comunicarlo con la parte GUI. Entonces mi sugerencia es:
- Crear un objeto Queue en el hilo principal
- Crea un nuevo hilo con acceso a esa cola
- Compruebe periódicamente la cola en el hilo principal
Luego debe resolver el problema de qué sucede si el usuario hace clic dos veces en el mismo botón (generará un nuevo hilo con cada clic), pero puede solucionarlo desactivando el botón de inicio y habilitándolo de nuevo después de llamar a 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")
El problema es que t.join () bloquea el evento de clic, el hilo principal no vuelve al bucle de evento para procesar los repintados. Consulte Por qué ttk Barra de progreso aparece después de que el proceso en Tkinter o la barra de progreso TTK está bloqueado cuando se envía un correo electrónico.
Presentaré la base para una solución alternativa. No es específico para una barra de progreso Tk per se, pero ciertamente puede implementarse muy fácilmente para eso.
¡Aquí hay algunas clases que le permiten ejecutar otras tareas en el fondo de Tk, actualizar los controles Tk cuando lo desee y no bloquear la GUI!
Aquí está la clase TkRepeatingTask y 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()
Aquí hay una prueba Tk que demuestra el uso de estos. Simplemente añada esto al final del módulo con esas clases si desea ver la demostración en acción:
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()
Dos puntos de importación que enfatizaré sobre BackgroundTask:
1) La función que ejecuta en la tarea de fondo necesita tomar un puntero de función que invocará y respetará, lo que permite cancelar la tarea en el medio, si es posible.
2) Debe asegurarse de que la tarea en segundo plano se detenga cuando salga de su aplicación. ¡Ese hilo se ejecutará incluso si su GUI está cerrada si no aborda eso!