web-dev-qa-db-fra.com

Tkinter: Comment utiliser les threads pour empêcher la boucle d'événements principale de "geler"

J'ai un petit test GUI avec un bouton "Démarrer" et une barre de progression. Le comportement souhaité est:

  • Cliquez sur Démarrer
  • La barre de progression oscille pendant 5 secondes
  • La barre de progression s'arrête

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?

38
Dirty Penguin

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:

  1. Créer un objet Queue dans le thread principal
  2. Créer un nouveau thread avec accès à cette file d'attente
  3. Vérifier périodiquement la file d'attente dans le thread principal

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")
51
A. Rodas

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!

4
BuvinJ

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

4
jmihalicza