J'ai un petit test GUI avec un bouton "Démarrer" et une barre de progression. Le comportement souhaité est:
Le comportement observé est que le bouton "Démarrer" se bloque pendant 5 secondes, puis une barre de progression s'affiche (pas d'oscillation).
Voici mon code jusqu'à présent:
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()
Sur la base des informations de Bryan Oakley ici , je comprends que je dois utiliser des threads. J'ai essayé de créer un fil, mais je suppose que puisque le fil est démarré à partir du fil principal, cela n'aide pas.
J'ai eu l'idée de placer la partie logique dans une classe différente et d'instancier l'interface graphique à partir de cette classe, semblable à l'exemple de code d'A. Rodas ici .
Ma question:
Je ne peux pas comprendre comment le coder pour que cette commande:
self.test_button = Button(self.master, command=self.tb_click)
appelle une fonction qui se trouve dans l'autre classe. Est-ce une mauvaise chose à faire ou est-ce même possible? Comment créer une 2e classe capable de gérer le self.tb_click? J'ai essayé de suivre l'exemple de code d'A. Rodas qui fonctionne à merveille. Mais je ne peux pas comprendre comment implémenter sa solution dans le cas d'un widget Button qui déclenche une action.
Si je devais plutôt gérer le thread à partir de la seule classe GUI, comment créer un thread qui n'interfère pas avec le thread principal?
Lorsque vous joignez le nouveau thread dans le thread principal, il attendra la fin du thread, donc l'interface graphique se bloquera même si vous utilisez le multithreading.
Si vous souhaitez placer la partie logique dans une classe différente, vous pouvez directement sous-classer Thread, puis démarrer un nouvel objet de cette classe lorsque vous appuyez sur le bouton. Le constructeur de cette sous-classe de Thread peut recevoir un objet Queue et vous pourrez alors le communiquer avec la partie GUI. Donc, ma suggestion est:
Ensuite, vous devez résoudre le problème de ce qui se passe si l'utilisateur clique deux fois sur le même bouton (il engendrera un nouveau thread à chaque clic), mais vous pouvez le résoudre en désactivant le bouton de démarrage et en le réactivant après avoir appelé 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")
Je présenterai la base d'une autre solution. Elle n'est pas spécifique à une barre de progression Tk en soi, mais elle peut certainement être implémentée très facilement pour cela.
Voici quelques classes qui vous permettent d'exécuter d'autres tâches en arrière-plan de Tk, de mettre à jour les contrôles Tk si vous le souhaitez et de ne pas verrouiller l'interface graphique!
Voici les classes TkRepeatingTask et 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()
Voici un test Tk qui démontre l'utilisation de ces derniers. Ajoutez simplement cela au bas du module avec ces classes si vous voulez voir la démo en action:
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 __== "__main__":
tkThreadingTest()
Deux points d'importation que je soulignerai à propos de BackgroundTask:
1) La fonction que vous exécutez dans la tâche d'arrière-plan doit prendre un pointeur de fonction qu'il invoquera et respectera, ce qui permet d'annuler la tâche à mi-chemin - si possible.
2) Vous devez vous assurer que la tâche d'arrière-plan est arrêtée lorsque vous quittez votre application. Ce fil continuera de fonctionner même si votre interface graphique est fermée si vous ne répondez pas à cela!
Le problème est que t.join () bloque l'événement click, le thread principal ne revient pas à la boucle d'événement pour traiter les repeints. Voir Pourquoi ttk Progressbar apparaît après le processus dans Tkinter ou Barre de progression TTK bloquée lors de l'envoi d'e-mails