Afin de pouvoir personnaliser votre classeur sans détruire le classeur sur lequel travaille votre voisin, vous allez tout d'abord aller dans le menu File puis Make a copy.... Renommez le classeur en ajoutant votre nom à la fin du nom de fichier par exemple.

Animation d'un objet sur un Canvas

L'activité suivante va consister à animer une bille sur un Canvas. On peut en première approche penser qu'il suffit simplement de changer les coordonnées d'un objet dans une bouche pour que l'objet se déplace, nous allons voir qu'il n'en est rien !

Regardons tout d'abord à quoi doit ressembler notre application :

Attelons nous d'abord à la conception de l'interface sans nous attarder sur le mouvement :

In [1]:
from tkinter import *

def move():
    pass # nous allons la définir plus tard

def stop_it():
    """arret de l'animation"""
    global flag
    flag =0
    
def start_it():
    """demarrage de l'animation"""
    global flag
    if flag ==0:
        # pour ne lancer qu'une seule boucle
        flag =1
        move()

#========== Programme principal =============
# les variables suivantes seront utilisees de maniere globale :
x1, y1 = 10, 10  # coordonnees initiales
dx, dy = 15, 0   # 'pas' du deplacement
flag =0          # indicateur de mouvement modifié par start_it et stop_it

# Creation du widget principal ("parent") :
fenetre = Tk()
fenetre.title("Exercice d'animation avec Tkinter")

# creation des widgets "enfants" :
can1 = Canvas(fenetre,bg='dark grey',height=250, width=250)
can1.pack(side=LEFT, padx =5, pady =5)

# Creation de la balle. On memorise ici son nom, c'est important !!
oval1 = can1.create_oval(x1, y1, x1+30, y1+30, width=2, fill='red')

bou1 = Button(fenetre,text='Quitter', width =8, command=fenetre.destroy)
bou1.pack(side=BOTTOM)
bou2 = Button(fenetre, text='Demarrer', width =8, command=start_it)
bou2.pack()
bou3 = Button(fenetre, text='Arreter', width =8, command=stop_it)
bou3.pack()

# demarrage du receptionnaire d'evenements (boucle principale) :
fenetre.mainloop()

L'application prend forme mais rien ne bouge ! c'est normal, nous avons fait l'impasse sur la fonction move() chargée du mouvement.

Commençons par définir une fonction move() qui aura pour but de déplacer la bille sur le canvas. Utilisons une première appproche naïve :

In [ ]:
def move():
    "deplacement de la balle"
    global x1, y1, dx, dy, flag
    while flag>0:
        x1, y1 = x1 +dx, y1 + dy
        if x1 >210:
            x1, dx, dy = 210, 0, 15
        if y1 >210:
            y1, dx, dy = 210, -15, 0
        if x1 <10:
            x1, dx, dy = 10, 0, -15
        if y1 <10:
            y1, dx, dy = 10, 15, 0
        can1.coords(oval1,x1,y1,x1+30,y1+30)

Dans cette approche, nous utilisons une boucle qui s'arrêtera lorsque l'indicateur flag sera modifié par l'appui sur le bouton stop. Dans cette boucle on ajuste les coordonnées en ajoutant un déplacement sur x : dx et sur y : dy. Ces déplacements sont calculés selon le bord ou on se trouve. Quand on atteint l'extrémité d'un bord, on ajuste les incréments dx et dy afin que le mouvement change de direction. Facile non ? En théorie, cela doit marcher.

Vérifions cela tout de suite en validant la cellule de notre fonction move() et de notre programme principal :

In [2]:
#========== Programme principal =============
# les variables suivantes seront utilisees de maniere globale :
x1, y1 = 10, 10  # coordonnees initiales
dx, dy = 15, 0   # 'pas' du deplacement
flag =0          # indicateur de mouvement modifié par start_it et stop_it

# Creation du widget principal ("parent") :
fenetre = Tk()
fenetre.title("Exercice d'animation avec Tkinter")

# creation des widgets "enfants" :
can1 = Canvas(fenetre,bg='dark grey',height=250, width=250)
can1.pack(side=LEFT, padx =5, pady =5)

# Creation de la balle. On memorise ici son nom, c'est important !!
oval1 = can1.create_oval(x1, y1, x1+30, y1+30, width=2, fill='red')

bou1 = Button(fenetre,text='Quitter', width =8, command=fenetre.destroy)
bou1.pack(side=BOTTOM)
bou2 = Button(fenetre, text='Demarrer', width =8, command=start_it)
bou2.pack()
bou3 = Button(fenetre, text='Arreter', width =8, command=stop_it)
bou3.pack()

# demarrage du receptionnaire d'evenements (boucle principale) :
fenetre.mainloop()

Catastrophe notre programme plante ! Que s'est-il passé ? Nous devons l'interrompre avec le bouton

Pas de panique, c'est tout à fait normal. Rappelons nous comment fonctionne un programme avec une interface graphique : Le programme principal est constitué du mainloop qui est une boucle infinie qui attend un événement particulier pour passer la main à une partie du programme. Si nous la fonction move avec une boucle while pour bouger la balle, notre programme est piégé à l'intérieur de cette boucle while et ne peut rendre la main à la mainloop en charge de gérer entre autre la détection des appuis sur les boutons. Notre programme est figé car plus aucun événement n'est traité !!

Comment ne pas figer le programme ?

Il faut obligatoirement rendre la main à notre mainloop à intervalles réguliers afin qu'elle puisse traiter les événements comme l'appui sur les boutons. Pour ce faire, nous ferons appel à la méthode after() de notre fenêtre principale. Cette méthode permet de planifier le lancement d'une action au bout d'un certain temps. En attendant, la main est rendue à la mainloop.

Voici donc la solution à notre problème : remplacer le while bloquant par des appels réguliers à la fonction move() grâce à la méthode after(). Voici comment on procède :

In [5]:
def move():
    """deplacement de la balle"""
    global x1, y1, dx, dy, flag
    x1, y1 = x1 +dx, y1 + dy
    if x1 >210:
        x1, dx, dy = 210, 0, 15
    if y1 >210:
        y1, dx, dy = 210, -15, 0
    if x1 <10:
        x1, dx, dy = 10, 0, -15
    if y1 <10:
        y1, dx, dy = 10, 15, 0
    # On peut changer les coordonnees de la balle grace au nom memorise
    can1.coords(oval1,x1,y1,x1+30,y1+30)
    if flag >0:
        fenetre.after(50,move)
    # => boucler apres 50 millisecondes

Testons à présent notre programme :

In [6]:
#========== Programme principal =============
# les variables suivantes seront utilisees de maniere globale :
x1, y1 = 10, 10  # coordonnees initiales
dx, dy = 15, 0   # 'pas' du deplacement
flag =0          # indicateur de mouvement modifié par start_it et stop_it

# Creation du widget principal ("parent") :
fenetre = Tk()
fenetre.title("Exercice d'animation avec Tkinter")

# creation des widgets "enfants" :
can1 = Canvas(fenetre,bg='dark grey',height=250, width=250)
can1.pack(side=LEFT, padx =5, pady =5)

# Creation de la balle. On memorise ici son nom, c'est important !!
oval1 = can1.create_oval(x1, y1, x1+30, y1+30, width=2, fill='red')

bou1 = Button(fenetre,text='Quitter', width =8, command=fenetre.destroy)
bou1.pack(side=BOTTOM)
bou2 = Button(fenetre, text='Demarrer', width =8, command=start_it)
bou2.pack()
bou3 = Button(fenetre, text='Arreter', width =8, command=stop_it)
bou3.pack()

# demarrage du receptionnaire d'evenements (boucle principale) :
fenetre.mainloop()

Magnifique, cela fonctionne ! La fonction move() ne monopolise plus le programme mais effecture un petit déplacement, puis oblige la fenêtre principale du programme à la rappeler au bout de 50 milliseconde si l'ordre d'arrêter n'a pas été passé. Cela revient au final à une boucle tant que sauf que la mainloop reprend la main régulièrement et donc notre programme répond aux demandes de l'utilisateur.

On retiendra que pour toute animation, il ne faut jamais bloquer la mainloop. Pour cela on fait appel à la méthode after()

A vous de jouer !

En guise d'entrainement, vous allez écrire un programme en vous basant sur l'exercice précédent qui affichait des carrés verts à l'emplacement du clic de souris. Sauf que cette fois-ci, vous allez les faire tomber par terre avec une jolie animation !

Au travail. Voici le code de l'exemple servant de base à votre travail. Faîtes tomber les rectangles par terre.

In [4]:
from tkinter import *

def Clic(event):
    """ Gestion de l'événement Clic gauche sur la zone graphique """
    # position du pointeur de la souris
    X = event.x
    Y = event.y
    # on dessine un carré
    r = 20
    Canevas.create_rectangle(X-r, Y-r, X+r, Y+r, outline='black',fill='green')

def Effacer():
    """ Efface la zone graphique """
    Canevas.delete(ALL)

# Création de la fenêtre principale
Mafenetre = Tk()
Mafenetre.title('Carrés')

# Création d'un widget Canvas
Largeur = 480
Hauteur = 320
Canevas = Canvas(Mafenetre, width = Largeur, height =Hauteur, bg ='white')

# La méthode bind() permet de lier un événement avec une fonction :
# un clic gauche sur le canvas provoquera l'appel de la fonction Clic()
Canevas.bind('<Button-1>', Clic)
Canevas.pack(padx =5, pady =5)

# Création d'un widget Button (bouton Effacer)
Button(Mafenetre, text ='Effacer', command = Effacer).pack(side=LEFT,padx = 5,pady = 5)
# Création d'un widget Button (bouton Quitter)
Button(Mafenetre, text ='Quitter', command = Mafenetre.destroy).pack(side=LEFT)

Mafenetre.mainloop()
In [ ]: