À propos de : thread python tkinter

Bonjour,
je voudrais réaliser un objet graphique animé représentant une pompe pouvant être mise en rotation et à l'arrêt.
La création de l'objet pompe est concluante (voir image jointe). Mais quand la fonction run est présente (pas en commentaire), rien ne se passe et la fenêtre de l'image jointe ne s'affiche même pas.
Évidemment le fichier python joint est un peu long et peut-être pas très documenté ni peut-être très bien construit. Aussi je remercie par avance ceux qui prendraient le temps de se pencher dessus.
Cordialement.
"""   Pompes -----------------------------------------------------------------
--
--            lA FONCTION RUN NE DONNE RIEN
--            
--            
----------------------------------------------------------------------------"""

from threading import Thread
import time
from tkinter import *  #-- application graphique (canevas , boutons , label)

#==============================================================================
class Point():    #-- les points s'affichent(si etat=1) dans le canevas
                  #--   principal cnv à créer

    def __init__(self, x, y , etat=0):
        self.x = x
        self.y = y
        self.etat=etat
        if self.etat == 1 : cnv.create_oval(self.x-3 , self.y-3 ,self.x+3,self.y+3 ,
                                            fill="black")

    def getX(self):
        return self.x

    def getY(self):
        return self.y

    def setX(self,val): self.x=val
    def setY(self,val): self.y=val

    def affich(self):
        cnv.create_oval(self.x-3 , self.y-3 ,self.x+3,self.y+3 , fill="black")

#==============================================================================
class Ligne ():    #-- les lignes s'affichent dans le canevas 
                   #--   principal cnv à créer
    
    def __init__(self,pto, ptd):
        self.xo=pto.x
        self.yo=pto.y
        self.xd=ptd.x
        self.yd=ptd.y
        self.lgn=cnv.create_line (self.xo , self.yo , self.xd , self.yd ,
                                            fill="blue" , width=3)
        
    def on (self): cnv.itemconfig(self.lgn, fill="blue")

    def off (self): cnv.itemconfig(self.lgn, fill="white")

#==============================================================================
class T_pompe(Thread): #-- 
    
    def __init__(self, p_x , p_y):
        Thread.__init__(self)
        self.pos_x = p_x
        self.pos_y = p_y
        self.en_cours = True
        self._pause = False
        self.tab_lignes = []  #-- lignes représentant la pompe
        ec = 10 * 2**0.5

        deb = Point(p_x , p_y) #-- début de toutes les lignes 
        t_fin = [Point(p_x , p_y - 20) , Point(p_x-ec , p_y-ec) , Point(p_x-20 , p_y) ,
                 Point(p_x-ec , p_y+ec) , Point(p_x , p_y+20) , Point(p_x+ec , p_y+ec) ,
                 Point(p_x+20 , p_y) ,Point(p_x+ec , p_y-ec)]
        for i in range(8):  #-- affichage de la pompe
            self.tab_lignes.append(Ligne(deb,t_fin[ i]))
        cnv.create_oval (p_x-22 , p_y-22 , p_x+22 , p_y+22 )
                       
    #---------------------------------------------------------------------                               
                
    def run(self):  #-- Code à exécuter pendant l'exécution du thread
        self.i = 0
        self.j = 7
        while self.en_cours == True :
            if self._pause == True :
                time.sleep(1.0)
                continue
            time.sleep(2.0)
            self.i = self.i+1
            if self.i == 8 : self.i=0
            cnv.itemconfig(self.tab_lignes[self.i], fill="white")
            time.sleep(0.1)
            cnv.itemconfig(self.tab_lignes[self.j], fill="blue")
            self.j=self.i-1
            if self.j<0 : self.j=7
                        
    #---------------------------------------------------------------------                               
           
    def reprise(self):  self._pause = False
    #---------------------------------------------------------------------                               

    def pause(self):  self._pause = True
    #---------------------------------------------------------------------                               
      
    def stop(self):  self.en_cours = False

#===================================================================== 

def reset():  #--  application 
    print("recommencer")
    message.set("Résultats ou info")  

#------------------------------------------------------------------------------

#=========  Fenêtre principale =======================================================
    
fen = Tk()   #--  Mise en place de la fenetre et du canevas principal
fen.title("MODELE")

#=========  VARIABLES  ====================================================
message = StringVar() #-- utilisée dans info_label (affichage divers)

#== zone  pour afficher des textes (résultats ou autre) par message.set("....")   
info_label = Label(fen, textvariable=message , font = ("Helvetica 16 bold italic"))
info_label.pack(side="top")
message.set("Démarrage")

#====== canevas éventuel =================================================
cnv = Canvas(fen, width=300, height= 200, bg="aliceblue")
cnv.pack()

#==========  Boutons de base  ============================================

#-- bouton possible   -------
btn_new = Button(fen , text="Reset" ,font=("Helvetica", 18), command = reset)
btn_new.pack(side="left")

#-- bouton pour quiter  -------
btn_quit = Button(fen , text=" Quitter " ,font=("Helvetica", 18), command= fen.destroy)
btn_quit.pack(side="right")
              
#==============================================================================
p_1 = T_pompe(50,50)
p_2 = T_pompe(100,50)

#p_1.run()

fen.mainloop()
113778

Réponses

  • Peux-tu nettoyer les [ i] qui traînent ?
    Je regarde ça.

    Habituellement, on crée une classe pour la fenêtre.
    class fenêtres(Tk):
      def __init__(self):
        super().__init__()
        self.title("bidule")
        self.resizable(False,False)
      def run(self):
        self.mainloop()
    
    if __name__ == "__main__":
        fenêtre=fenêtres()
        fenêtre.run()
    
    Algebraic symbols are used when you do not know what you are talking about.
            -- Schnoebelen, Philippe
  • 1) Le code ne fonctionne pas tel quel à cause de :
    Traceback (most recent call last):
      File "./pompe.py", line 142, in <module>
        p_1 = T_pompe(50,50)
      File "./pompe.py", line 72, in __init__
        self.tab_lignes.append(Ligne(deb,t_fin))
      File "./pompe.py", line 46, in __init__
        self.xd=ptd.x
    AttributeError: 'list' object has no attribute 'x'
    
    2) Le problème évoqué dans la question est sans doute dû à l'utilisation de T_pompe.run() au lieu de Thread.start() (via p_1.start() ou p_2.start(), bien entendu) : avec l'appel de T_pompe.run(), aucun thread n'est créé...

    3) Il va falloir revoir l'architecture. La plupart, sinon la totalité des toolkits graphiques ne supportent pas (sans risque élevé de crash) qu'on appelle leurs fonctions depuis un thread autre que le principal. Il faut donc faire en sorte que les “worker threads” passent des messages au thread principal quand celui-ci doit interagir avec Tkinter, et c'est lui qui doit appeler les fonctions ou méthodes de Tkinter. Cela doit pouvoir se faire par exemple avec les Condition Objects du module 'threading'. Il serait peut-être sage de réaliser l'ébauche du programme d'abord purement en mode texte, sans Tkinter, afin de bien se familiariser avec les threads. Une fois que ça marche bien, tu peux ajouter la couche graphique.

    4) Les worker threads doivent être join()és par le thread principal, ou alors il faut les créer avec l'argument 'daemon=True' — s'ils supportent d'être arrêtés brutalement quand le thread principal se termine.

    5) fen.quit() est sans doute un peu plus propre que fen.destroy() pour dire au-revoir à la fenêtre racine Tk.
  • L'objet Condition dont j'ai parlé ci-dessus n'est peut-être pas le meilleur choix pour transmettre un message au thread qui s'occupe de l'interface Tk. Un truc pratique que j'ai utilisé dans un programme utilisant Tkinter et des threads par le passé repose sur le fait que si 'root' est l'objet renvoyé par tkinter.Tk() (chez toi, c'est 'fen'), alors ceci est, par exception, documenté comme sûr depuis n'importe quel thread:
    root.event_generate("<<NomDeTonÉvÈnement>>", when="tail")
    
    Ceci permet à un thread autre que le thread gérant l'interface Tk (que l'on va l'appeler $I$ comme « interface » pour faire court) de déclencher un traitement spécial dans $I$. Ce fait très utile, qui permet de ne pas recourir au hideux polling, est documenté ici et (confirmé par Guido van Rossum).

    Pour passer les données du “worker thread” au thread $I$, on peut utiliser un objet du type queue.Queue. Si je reprends mon code de l'époque et schématise la partie qui peut t'intéresser, cela donne ce qui suit (c'est un schéma, hein ; il faut compléter à certains endroits) :
    import locale
    import functools
    import queue as queue_mod
    import threading
    import tkinter as tk
    
    
    class App:
    
        def __init__(self):
            self.root = tk.Tk()
            self.queue = queue_mod.Queue()
            self.root.bind("<<NomDeTonEvenement>>",
                           functools.partial(self._onQueueUpdated,
                                             queue=self.queue))
            un_arg = "..."
            t = threading.Thread(name="Nom_du_thread",
                                 target=self._workerThreadFunc,
                                 args=(self.queue, un_arg),
                                 daemon=True)
            t.start()
    
        def _workerThreadFunc(self, queue, arg):
            try:
                report = ...           # préparer les données
                queue.put(report)
            except BaseException:
                # Gérer les erreurs sans appel à Tkinter
    
            try:
                # Prévenir le thread I (GUI thread) qu'il y a des données à traiter
                self.root.event_generate("<<NomDeTonEvenement>>",
                                           when="tail")
            # In case Tk is not here anymore
            except tk.TclError:
                # Gérer les erreurs sans appel à Tkinter
    
        # Cette méthode sera exécutée par le “GUI thread“
        def _onQueueUpdated(self, event, queue=None):
            # Traiter ce qu'il y a dans l'objet 'queue'
    
        def mainLoop(self):
            self.root.mainloop()
    
    
    locale.setlocale(locale.LC_ALL, '')
    App().mainLoop()
    
  • Merci de donner du temps et des explications à mes interrogations . Je vais regarder tout ça de près même si ça me dépasse un peu et vous tiens au courant .
    Cordialement .
  • De rien, fm_31. J'ai pensé à un autre truc : si ce que tu veux faire dans les threads peut être découpé en tranches qui ne prennent pas plus de quelques millisecondes chacune, tu peux utiliser root.after() ou root.after_idle() (root étant l'objet Tk()) pour les faire exécuter par la boucle d'évènements de Tk, donc dans le thread de l'interface. Il y a un exemple d'utilisation d'after() ici. Pour after_idle(), on passe simplement une fonction ou méthode en argument qui elle-même ne prend pas d'argument (plus généralement, on passe un callable qui ne prend pas d'argument) et la boucle d'évènements de Tk l'appellera à chaque fois qu'elle a fini de traiter les évèvements qui étaient en attente. Tes traitements sont effectués par ce que l'on appelle la “idle function” du toolkit.

    C'est a priori plus simple de cette façon car tout se passe dans le thread de l'interface. En revanche, si le code que tu fais appeler par la “idle function” prend une demi seconde pour s'exécuter, il va “freezer” l'interface graphique pendant une demi seconde, ce qui est très laid (ceci car la “idle function” est exécutée par la boucle d'évènements de Tk ; elle interrompt donc le traitement de ces derniers). Cette technique est acceptable si les durées d'exécution sont courtes à chaque fois.
  • Il y a un exemple simple et sympathique d'utilisation de la méthode after() des objets Tk ici. Elle est utilisée pour déplacer à intervalle régulier un objet placé sur un Canvas (utiliser les flèches du clavier pour modifier la direction et le sens de déplacement). Dans cet exemple, on peut supprimer la ligne :
    from tkinter.ttk import *
    
    car Ttk n'y est pas utilisé.

    Tu peux sans doute utiliser cette technique pour mettre à profit le temps processeur disponible quand la boucle d'évènements de Tk n'a plus rien à faire et le répartir entre tes différentes « unités de travail » (qui ne sont pas des threads, dans cette approche).

    Edit : formulation plus précise.
  • Bonjour et merci de m'avoir fait découvrir la fonction after que je ne connaissais pas . Effectivement elle convient parfaitement pour ce que je souhaite faire et surtout elle est très simple à mettre en place .
    Je vais pouvoir continuer mon appli .
    Cordialement
  • Parfait. Comme tu l'as peut-être déjà compris, il ne faut pas utiliser time.sleep() depuis le thread qui gère l'interface Tk, car cela empêche l'exécution de la boucle d'évènements de Tk pendant ce temps (évènements clavier, souris...), ce qui a pour effet de « geler » l'interface graphique. Si tu veux exécuter un certain bout de code dans $n$ millisecondes, tu le mets dans une fonction (éventuellement anonyme) puis tu passes le $n$ et cette fonction à after(). Ainsi, la boucle d'évènements de Tk peut s'exécuter normalement. Elle appellera automatiquement ton bout de code au moment choisi (cela peut être un peu après si au moment en question, elle était occupée par un traitement qui prend du temps — traitement qui, dans tous les cas, proviendrait de ton code). Comme tu l'as vu dans l'exemple, le bout de code en question peut lui-même enregistrer d'autres choses à exécuter plus tard au moyen d'after().

    after_idle(), c'est un peu le même principe sauf que la fonction que tu passes en argument sera exécutée dès que Tk aura traité tous les évènements en attente (autrement dit, très très bientôt, sauf si l'interface est dans un traitement particulièrement lourd, auquel cas elle apparaît comme « gelée » à l'utilisateur).
  • Avec after , j'ai pu éliminer tous les time.sleep() mais je retiens l'astuce car il m'est arrivé de me faire piéger avec des time.sleep() qui n'opéraient pas comme souhaité .
  • J'ai été tellement surpris par la puissance et la simplicité de mise en œuvre de cette fonction after que je n'hésite pas à publier ce à quoi je suis arrivé juste pour ceux qui n'ont pas encore eu, comme moi il y a peu, connaissance de son existence. Évidemment le code est très certainement perfectible mais c'est l'idée qui compte. Et de cela, je suis redevable et reconnaissant de l'aide apportée par des spécialistes dévoués.

    [En typographie, on ne met jamais d'espace avant un point ou une virgule, mais toujours après. ;-) AD]
    [code]
    """ Pompes
    --
    -- Exemple d'animation d'objets sous tkinter
    --
    --
    """

    from tkinter import * #-- application graphique (canevas , boutons , label)

    #==============================================================================
    class Point(): #-- les points s'affichent(si etat=1) dans le canevas
    #-- principal cnv à créer

    def __init__(self, x, y , etat=0):
    self.x = x
    self.y = y
    self.etat = etat
    if self.etat == 1 : cnv.create_oval(self.x-3 , self.y-3 ,
    self.x+3 , self.y+3 , fill="black")

    def getX(self): return self.x

    def getY(self): return self.y

    def setX(self,val): self.x=val
    def setY(self,val): self.y=val

    def affich(self):
    cnv.create_oval(self.x-3 , self.y-3 ,self.x+3,self.y+3 , fill="black")

    #==============================================================================
    class Ligne (): #-- les lignes s'affichent dans le canevas
    #-- principal cnv à créer

    def __init__(self,pto, ptd):
    self.xo=pto.x
    self.yo=pto.y
    self.xd=ptd.x
    self.yd=ptd.y
    self.lgn=cnv.create_line (self.xo , self.yo ,
    self.xd , self.yd , fill="blue" , width=3)

    def on (self): cnv.itemconfig(self.lgn, fill="blue")

    def off (self): cnv.itemconfig(self.lgn, fill="cyan")

    #==============================================================================
    class T_pompe(): #-- chaque pompe est représentée par 8 rayons dont on change
    #-- un par un la couleur pour simuler la rotation

    def __init__(self, p_x , p_y):
    self.pos_x = p_x
    self.pos_y = p_y
    self.en_marche = False
    self.tab_lignes = [] #-- rayons représentant la pompe
    ec = 10 * 2**0.5

    self.ray = 0 #-- n° du rayon de la pompe
    self.j = 7 #-- rayon précédent

    deb = Point(p_x , p_y) #-- début de toutes les lignes
    t_fin = [Point(p_x , p_y - 20) , Point(p_x-ec , p_y-ec) , Point(p_x-20 , p_y) ,
    Point(p_x-ec , p_y+ec) , Point(p_x , p_y+20) , Point(p_x+ec , p_y+ec) ,
    Point(p_x+20 , p_y) ,Point(p_x+ec , p_y-ec)]
    for i in range(8): #-- affichage de la pompe
    self.tab_lignes.append(Ligne(deb,t_fin[ i]))
    self.rond = cnv.create_oval (p_x-22 , p_y-22 , p_x+22 , p_y+22 ,
    width=4 , outline="blue")

    #

    def marche(self): #-- animation des pompes par rotation des rayons
    if self.en_marche == True :
    self.ray = self.ray + 1
    if self.ray == 8 : self.ray = 0
    self.tab_lignes[self.ray].off()
    cnv.itemconfig(self.rond, outline="red")
    self.tab_lignes[self.j].on()

    self.j = self.ray - 1
    if self.j < 0 : self.j = 7
    fen.after(50,self.marche)#-- la fonction marche est relancée toutes les 50ms
    else:
    self.tab_lignes[self.ray].on()
    cnv.itemconfig(self.rond, outline="blue")
    #

    def start(self):
    self.en_marche = True
    self.marche()
    #

    def stop(self):
    self.en_marche = False
    self.marche()
    #

    def etat(self): return self.en_marche

    #=====================================================================================

    #========= Fenêtre principale =======================================================

    fen = Tk() #-- Mise en place de la fenetre et du canevas principal
    fen.title("POMPES")

    #====== canevas =================================================
    cnv = Canvas(fen, width=300, height= 200, bg="aliceblue")
    cnv.pack()

    #========= VARIABLES ====================================================

    p1 = T_pompe(30,50)
    p2 = T_pompe(80,50)
    p3 = T_pompe(130,50)

    #========= action des boutons ====================================================

    def p1_cmd(): #-- commande p1
    etat = p1.etat()
    if etat == False : p1.start()
    else : p1.stop()
    #

    def p2_cmd(): #-- commande p2
    etat = p2.etat()
    if etat == False : p2.start()
    else : p2.stop()

    #

    def p3_cmd(): #-- commande p3
    etat = p3.etat()
    if etat == False : p3.start()
    else : p3.stop()

    #========== Boutons de base ============================================

    #-- bouton pour quiter
    btn_quit = Button(fen , text=" Quitter " ,font=("Helvetica", 18), command= fen.destroy)
    btn_quit.pack(side="right")

    #========== Boutons pompes ============================================

    #-- bouton p1
    btn_p1 = Button(fen , text="P1" ,font=("Helvetica", 16), command = p1_cmd)
    btn_p1.pack(side="left")

    #-- bouton p2
    btn_p2 = Button(fen , text="P2 " ,font=("Helvetica", 16), command= p2_cmd)
    btn_p2.pack(side="left")

    #-- bouton p2
    btn_p3 = Button(fen , text="P3 " ,font=("Helvetica", 16), command= p3_cmd)
    btn_p3.pack(side="left")

    fen.mainloop()
    [code]113838
  • Je n'ai pas trop le temps de lire le code, mais j'ai testé et ça marche bien. (:D
Connectez-vous ou Inscrivez-vous pour répondre.