Mettre du mouvement ou de l’animation

ghmesnil   22 mars 2016   Commentaires fermés sur Mettre du mouvement ou de l’animation

En utilisant la méthode .after() de Tkinter

La méthode after de la bibliothèque tkinter permet de relancer une fonction après un certain délai. Par exemple, pour faire rouler une boule, on peut écrire une fonction rouler qui déplace la boule sur une certaine longueur. Lorsque l’on appelle cette fonction plusieurs fois à la suite en espaçant les appels de quelques millisecondes, cela créer un effet d’animation comme sur la vidéo ci-dessous.

Exemple

Le code de l’animation est donné ci-dessous.

La boule est créée sur le Canvas Terrain à l’aide de la méthode create_oval (documentation sur ce sujet).

La fonction rouler fait avancer la boule de 10 pixels vers la gauche ou vers la droite selon la valeur de la variable globale direction. En fonction des arguments utilisés, la méthode coords permet soit de récupérer les coordonnées de l’objet Boule, soit de lui en attribuer de nouveaux (documentation sur ce sujet).

Pour créer un effet d’animation, on utilise la méthode after qui relance la fonction rouler et raffraichit le Canvas Terrain après une pause 40 millisecondes (documentation sur ce sujet).

from tkinter import *

direction='D'  # direction prise par la boule (Gauche='G', Droite='D')

def rouler():
    global direction
    # on récupère les coordonnées de la boule
    (x0,y0,x1,y1)=Terrain.coords(Boule)

    # selon la direction en cours, on teste si la boule est au bord
    # du terrain ou si elle peut avancer
    # cas où la boule roule vers la droite
    if direction=='D':
        if x1>=600: direction='G'    # la boule est à l'extrème droite
        else : Terrain.coords(Boule, x0+10 ,y0 ,x1+10 ,y1)
    # cas où la boule roule vers la gauche
    if direction=='G':
        if x0<=0: direction='D'     # la boule est à l'extrème gauche
        else : Terrain.coords(Boule, x0-10, y0, x1-10, y1)

    Terrain.after(40,rouler)

# fenêtre Tkinter
fenetre=Tk()

fenetre.title("Animation")
Terrain=Canvas(fenetre,height=50,width=600, bg='#ffffff')
Terrain.pack()

Boule=Terrain.create_oval(0,0,50,50,fill='#12EF58')

Bouton=Button(fenetre,text='Roule la Boule', command=rouler)
Bouton.pack(pady='10px')

fenetre.mainloop()

Problème

Cependant,  si l’on appuie plusieurs fois sur le bouton ‘Roule la Boule’, on s’aperçoit que la boule roule de plus en plus vite. En effet la pause de 40 millisecondes indiqué par la méthode after va être en concurrence avec le rafraîchissement de la fenêtre tkinter par la méthode mainloop. En fait, à chaque fois que l’on clique sur le bouton ‘Roule la Boule’, on lance une nouvelle fois la fonction rouler qui s’exécutera en plus de l’exécution « tranquille et régulière » prévue par les 40 millisecondes. Plus on clique sur le bouton, plus on aura d’exécutions de la fonctions rouler.

Pour ne plus avoir ce phénomène, il faut empêcher l’utilisateur de cliquer sur le bouton ‘Roule la Boule’. Par exemple, on peut rendre inactif le bouton en rajoutant l’instruction suivante dans la fonction rouler.

    Bouton['state']='disabled'

Une solution sans bouton

Le programme ci-dessous permet la même animation. Celle-ci démarre lorsque l’on clique sur le Terrain. Ceci se fait à l’aide de la méthode bind qui lance la fonction GO qui, à son tour, appelle une première fois la fonction rouler.

Pour empêcher l’effet d’accélération, on coupe le lien entre l’événement '<ButtonRelease>' et le Canvas Terrain en utilisant la méthode unbind (voir documentation). Ceci se fait dès l’exécution de la fonction GO.

from tkinter import *

direction='D' # direction prise par la boule (Gauche='G', Droite='D')

def rouler():
   global direction
   # on récupère les coordonnées de la boule
   (x0,y0,x1,y1)=Terrain.coords(Boule)

   # selon la direction en cours, on teste si la boule est au bord
   # du terrain ou si elle peut avancer
   # cas où la boule roule vers la droite
   if direction=='D':
     if x1>=600: direction='G' # la boule est à l'extrème droite
     else : Terrain.coords(Boule, x0+10 ,y0 ,x1+10 ,y1)
   # cas où la boule roule vers la gauche
   if direction=='G':
     if x0<=0: direction='D' # la boule est à l'extrème gauche
     else : Terrain.coords(Boule, x0-10, y0, x1-10, y1)
   Terrain.after(50,rouler)

def GO(event):
   Terrain.unbind('<ButtonRelease>')
   rouler()

# fenêtre Tkinter
fenetre=Tk()

fenetre.title("Animation")
Terrain=Canvas(fenetre,height=50,width=600, bg='#ffffff')
Terrain.pack()
Terrain.bind('<ButtonRelease>', GO)

Boule=Terrain.create_oval(0,0,50,50,fill='#12EF58')

fenetre.mainloop()

En utilisant la fonction sleep() du module time

La fonction sleep() est expliquée dans l’article « Utiliser le module time« . Accompagnée de la méthode .update() pour rafraichir l’écran, son utilité est similaire à .after().

Dans l’exemple suivant, la boule verte se déplace toujours dans son tuyau dans les deux sens. Elle effectue dix aller-retour en accélérant le mouvement.

from tkinter import *
from time import *

def GO(event):
   (x0,y0,x1,y1)=Terrain.coords(Boule)
   for k in range(10):
     while x1<600:
       x0=x0+10
       x1=x1+10
       Terrain.coords(Boule,x0,y0,x1,y1)
       Terrain.update()
       sleep(0.1/(k+1))
     while x0>0:
       x0=x0-10
       x1=x1-10
       Terrain.coords(Boule,x0,y0,x1,y1)
       Terrain.update()
       sleep(0.1/(k+1))

# fenêtre Tkinter
fenetre=Tk()

fenetre.title("Animation")
Terrain=Canvas(fenetre,height=50,width=600, bg='#ffffff')
Terrain.pack()
Terrain.bind('<ButtonRelease>', GO)

Boule=Terrain.create_oval(0,0,50,50,fill='#12EF58')

fenetre.mainloop()