Infoforall

Python 15 : Interface Tk - ANIMATION DANS UN CANVAS

Cette partie vous montrera comment faire bouger les objets dans votre Canvas, et comment modifier en cours de route les valeurs des attributs de vos objets.

Vous allez pouvoir obtenir ceci en quelques lignes (code .exe via l'image):

animation pacman

La structure va être la suivante :

  1. Instanciation (création) des objets à animer
  2. Création des fonctions visant à faire bouger les objets
  3. Utilisation de la récursivité (appel d'une fonction par elle-même) pour animer l'objet.

1 - Instanciation et identification des objets

Cette partie contient surtout des rappels et un petit exercice de compréhension de codage. Elle est importante pour bien maitriser le vocabulaire et le stockage de l'identification des objets. Traitez la correctement mais n'y passez pas un temps démesuré.

Rappels d'introduction :

Une variable est l'association :

  • d'un ensemble d'octets encodés
  • dans un identifiant mémoire (qu'on peut connaître avec id(variable).
  • avec un encodage dépendant du type de contenu à stocker (qu'on peut connaître avec type(variable).

Une fonction est un ensemble de lignes de codes qu'on peut activer simplement en tapant son nom suivi de () ou en fournissant des arguments entre les parenthèses.

Un objet est une entité complexe qui possède :

  • Une variable qui contient son "adresse" et permet d'accéder à son contenu. Par exemple : monObjet dans l'exemple ci-dessous.

  • Des variables propres (internes) qu'on nomme attributs. Ex : monObjet.attribut permet d'y accéder dans certains cas.
  • Des fonctions propres (internes) qu'on nomme méthodes. Ex: monObjet.methode() permet de l'utiliser.

Comme toutes les autres entités informatiques, un objet possède un identifiant qui permet de retrouver l'objet et d'agir sur lui. On peut le connaître avec id(monObjet).

Pour connaître le type exact de l'objet, il faut utiliser type(monObjet).

L'instanciation est le nom donné à la création d'un objet à partir d'un moule qu'on nomme une Classe. On génère cet objet à l'aide d'une méthode particulière, qu'on nomme un constructeur : cette méthode va créer l'objet et va renvoyer l'adresse de ce nouvel objet.

Reprenons l'un des codes utilisés dans le chapitre "Interface graphique" précédent.

01° Tenter de trouver les noms des constructeurs utilisés dans ce programme.

#!/usr/bin/env python

# -*- coding: utf-8 -*-

from tkinter import *


fen_princ = Tk()

fen_princ.title("ESSAI AVEC CANVAS")

fen_princ.geometry("600x600")


monCanvas = Canvas(fen_princ, width=500, height=500, bg='ivory', bd=0, highlightthickness=0)

monCanvas.place(x=50,y=50)


monCanvas.create_arc(50,50,150,150,fill="yellow",start=15,extent=330)

monCanvas.create_arc(250,50,350,150,fill="yellow",start=30,extent=300)


fen_princ.mainloop()

La réponse :

Les constructeurs (d'objets, objects dans la documentation anglaise) évidents sont :

  • Tk() est le constructeur de la classe Tk. On retrouve la majuscule T. On pourra agir à l'avenir sur l'object en utilisant son identifiant fen_princ.
    • Instanciation (création) et stockage : fen_princ = Tk()
    • Utilisation via une méthode : fen_princ.title("ESSAI AVEC CANVAS")
    • Utilisation via une méthode : fen_princ.geometry("600x600")

  • Canvas() est le constructeur de la classe Canvas. On stocke la référence obtenue après cette instanciation dans monCanvas :
    • Instanciation (création) et stockage : monCanvas = Canvas(...)
    • Utilisation via une méthode : monCanvas.place(x=50,y=50)
    • Utilisation via une méthode : monCanvas.create_arc(50,50,150,150,...)

Il existe néanmoins une autre méthode qui crée des objets non indépendants (items en anglais), rattachés au Canvas :

  • create_arc est également un "constructeur" qui crée un item mais cet objet doit être à l'intérieur d'un Canvas : on ne peut y faire appel qu'à partir d'un objet de classe Canvas. C'est pour cela qu'on n'y trouve pas de majusucule. Pour l'instant, on crée les dessins sans stocker les références : c'est génant, on ne pourra pas les modifier simplement par la suite.
  • monCanvas.create_arc(50,50,150,150,fill="yellow",start=15,extent=330)

    monCanvas.create_arc(250,50,350,150,fill="yellow",start=30,extent=300)

Conclusion : en fonction de ce qu'on veut faire de l'objet,

  • on stocke son identifiant en mémoire (mais ça prend de place d'en garder un grand nombre en mémoire) ou
  • on décide de ne pas garder la référence (mais on ne pourra pas agir sur lui après création).

Des avantages et des inconvénients dans les deux cas. Comme le nombre de données stockées est relativement petit dans vos programmes pour l'instant, vous pourriez vous contenter de toujours tout stocker. Mais autant commencer à réfléchir sur la notion de ressources disponibles.

2 - Modification d'un item de Canvas à l'aide de l'identifiant

Bien, après autant de blabla, il est temps de passer à l'action : nous allons faire un programme qui va :

#!/usr/bin/env python

# -*- coding: utf-8 -*-

from tkinter import *


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

# Corps du programme

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

fen_princ = Tk()

fen_princ.title("ESSAI AVEC CANVAS")

fen_princ.geometry("600x600")


# Création du canvas

monCanvas = Canvas(fen_princ, width=500, height=500, bg='ivory', bd=0, highlightthickness=0)

monCanvas.place(x=50,y=50)


# Création et stockage des références des formes dans le canvas

pacman_1 = monCanvas.create_arc(50,50,150,150,fill="yellow",start=15,extent=330)

pacman_2 = monCanvas.create_arc(250,50,350,150,fill="yellow",start=30,extent=300)


# Modication des items après leurs créations

monCanvas.itemconfig(pacman_1, fill='red')

monCanvas.itemconfig(pacman_2, fill='blue')


fen_princ.mainloop()

02° Tester le code pour vérifier que les deux arcs ne restent pas jaunes alors qu'ils sont créés avec fill='yellow'.

Ce n'est pas très visuel. Nous allons donc passer par un bouton qui va activer une fonction modification : c'est dans cette fonction qu'on va placer la modification.

03° Modifier le programme pour y inclure un widget bouton lié à la fonction modification. Les arcs ne devraient donc pas changer de couleur avant l'appui sur le bouton.

Une solution à l'ensemble des questions 3 à 6 est donnée plus bas.

Imaginons que nous voulions laissez un petit écart de temps avant la modification. On peut alors utiliser une méthode des fenêtres de classe Tk qui laisse passer une certain nombre de millisecondes : la méthode after :

fen_princ.after(500,modification)

Cela veut dire :

04° Créer un second bouton qui va activer une fonction temporisation. Dans cette fonction temporisation, on fera appel à la fonction modification au bout de 1 s, soit 1000 ms.

Les boutons ne fonctionnent qu'une seule fois du coup. Les couleurs sont fixées avant et après utilisation. Mais souvenons-nous du module random qu'on peut importer. Avec random.choice(liste), on peut choisir un élément de la variable liste = ['red','green','blue','yellow'].

05° Utiliser la méthode choice pour définir aléatoirement les couleurs des deux items lors de l'activation de modification.

Nous allons voir maintenant la méthode itemcget permettant d'obtenir la valeur d'un des attributs d'un item dont on connait l'identifiant :

nom_du_canvas.itemcget(identifiantItem,'nom_attribut')

Si nous cherchons par exemple la couleur de remplissage (attribut fill) du pacman_1, nous pourrions utiliser

monCanvas.itemcget(pacman_1,'fill')

06° Utiliser la méthode itemcget pour parvenir à afficher la couleur des pacmans sur la console (avec un print).

Voici une proposition de solution aux questions 3 à 6. Les couleurs des pacman_1 et 2 sont affichées directement dans des widgets-Label plutôt que sur la console.

#!/usr/bin/env python

# -*- coding: utf-8 -*-

from tkinter import *

import random


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

# Fonctions

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

def modification():

    lesCouleurs = ['white','yellow','red','blue','green','cyan','ivory','grey','gray','orange']

    monCanvas.itemconfig(pacman_1, fill=random.choice(lesCouleurs))

    monCanvas.itemconfig(pacman_2, fill=random.choice(lesCouleurs))

    txt_couleur_1.set(monCanvas.itemcget(pacman_1,'fill'))

    txt_couleur_2.set(monCanvas.itemcget(pacman_2,'fill'))


def temporisation():

    fen_princ.after(1000,modification)


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

# Corps du programme

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


fen_princ = Tk()

fen_princ.title("ESSAI AVEC CANVAS")

fen_princ.geometry("600x600")


monCanvas = Canvas(fen_princ, width=500, height=600, bg='ivory', bd=0, highlightthickness=0)

monCanvas.grid(row=0,column=0, padx=10,pady=10)

pacman_1 = monCanvas.create_arc(50,50,150,150,fill="yellow",start=15,extent=330)

pacman_2 = monCanvas.create_arc(250,50,350,150,fill="yellow",start=30,extent=300)


zone2 = Frame(fen_princ, bg='#777777')

zone2.grid(row=0,column=1,ipadx=5)

Button(zone2, text="Couleurs", fg="yellow", bg="black", command=modification).pack(fill=X)

Button(zone2, text="Tempo", fg="yellow", bg="black", command=temporisation).pack(fill=X)


txt_couleur_1 = StringVar()

txt_couleur_1.set(monCanvas.itemcget(pacman_1,'fill'))

couleur_1 = Label(zone2, textvariable=txt_couleur_1)


txt_couleur_2 = StringVar()

txt_couleur_2.set(monCanvas.itemcget(pacman_2,'fill'))

couleur_2 = Label(zone2, textvariable=txt_couleur_2)


couleur_1.pack()

couleur_2.pack()


fen_princ.mainloop()

Et voici les explications ligne par ligne :

#!/usr/bin/env python

# -*- coding: utf-8 -*-

from tkinter import *

import random

Habituel : déclaration du type de langage, du type d'encodage du fichier et importation des modules nécéssaires.

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

# Corps du programme

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


fen_princ = Tk()

fen_princ.title("ESSAI AVEC CANVAS")

fen_princ.geometry("600x600")

On crée l'objet-fenêtre de classe Tk puis on lui donne certaines caractéristiques (titre et taille).

monCanvas = Canvas(fen_princ, width=500, height=600, bg='ivory', bd=0, highlightthickness=0)

monCanvas.grid(row=0,column=0, padx=10,pady=10)

pacman_1 = monCanvas.create_arc(50,50,150,150,fill="yellow",start=15,extent=330)

pacman_2 = monCanvas.create_arc(250,50,350,150,fill="yellow",start=30,extent=300)

On crée un Canvas et on y dessine deux items de type arc.

zone2 = Frame(fen_princ, bg='#777777')

zone2.grid(row=0,column=1,ipadx=5)

Button(zone2, text="Couleurs", fg="yellow", bg="black", command=modification).pack(fill=X)

Button(zone2, text="Tempo", fg="yellow", bg="black", command=temporisation).pack(fill=X)

On crée une zone Frame dans la fenêtre qu'on place avec grid dans la deuxième colonne, à droite donc du Canvas.

On crée ensuite deux boutons (sans stocker leurs références) dans zone2 avec la méthode pack et demandant aux widgets-Button de prendre toute la place disponible horizontalement (fill=X).

txt_couleur_1 = StringVar()

txt_couleur_1.set(monCanvas.itemcget(pacman_1,'fill'))

couleur_1 = Label(zone2, textvariable=txt_couleur_1)


txt_couleur_2 = StringVar()

txt_couleur_2.set(monCanvas.itemcget(pacman_2,'fill'))

couleur_2 = Label(zone2, textvariable=txt_couleur_2)


couleur_1.pack()

couleur_2.pack()

Pour chaque pacman :

  • On crée un objet de class StringVar avec le constructeur StringVar().
  • Ensuite, on y place avec la méthode set la couleur du pacman en allant la chercher avec la méthode itemcget.
  • On crée un objet-Label en lui rattachant l'objet StringVar comme texte variable. De cette façon, à chaque fois qu'on va modifier le StringVar, le Label va automatiquement modifier son contenu affiché.
  • On affecte le Label à zone2 avec la méthode pack.

fen_princ.mainloop()

On active la surveillance des événements sur la fenêtre.

Regardons maintenant les fonctions associées aux boutons :

def modification():

    lesCouleurs = ['white','yellow','red','blue','green','cyan','ivory','grey','gray','orange']

    monCanvas.itemconfig(pacman_1, fill=random.choice(lesCouleurs))

    monCanvas.itemconfig(pacman_2, fill=random.choice(lesCouleurs))

    ...

On crée une liste contenant certaines couleurs.

On modifie la couleur de remplissage du pacman_1 à l'aide de la méthode itemconfig. La nouvelle couleur est choisie au hasard dans la liste avec la méthode choice.

On fait la même chose avec le pacman_2.

def modification():

    ...

    txt_couleur_1.set(monCanvas.itemcget(pacman_1,'fill'))

    txt_couleur_2.set(monCanvas.itemcget(pacman_2,'fill'))

On modifie le StringVar txt_couleur_1 à l'aide de la méthode set. La nouvelle valeur correspond à la nouvelle couleur du pacman, qu'on va récupérer à l'aide de la méthode itemcget.

Le Label correspondant va alors être automatiquement modifié sans autre intervention.

On fait la même chose pour le second pacman.

def temporisation():

    fen_princ.after(1000,modification)

On définit la seconde fonction, celle qui temporise l'appel de modification.

Et on peut changer quoi avec la méthode itemconfig ?

Et bien, n'importe quel paramètre de la méthode create_truc. Pour savoir ce qu'on peut modifier, il faut donc pouvoir trouver l'information sur la méthode elle-même.

Vous commencez certainement à connaitre quelques sources d'informations :

En cliquant sur le dernier lien, vous devriez facilement trouver l'information que vous recherchez dans le cadre de votre création.

Nous allons utiliser cette source d'informations pour réaliser nos animations de pacman.

Point htm : Si vous regardez bien le lien vers effbot, vous remarquerez que la page est en .htm. C'est quoi ce truc ? C'est un résidu historique. Il y a bien longtemps, lorsque la taille mémoire était limitée, Windows n'acceptait que 8 caractères pour décrire le nom du fichier et 3 caractères pour l'extension. D'où les .exe, .jpg, .gif, .doc et donc, à l'époque .htm. Le L est venu plus tard, lorsqu'on a pu rajouter le L pour language dans Hyper Text Markup Language : htm est devenu html !

3 - Première animation : alternance d'affichage entre deux items

On trouve notamment pour create_arc :

    state=Item state. One of NORMAL (default value), DISABLED, or HIDDEN

Nous pourrons ainsi, afficher un item (avec state=NORMAL) ou le cacher(avec state=HIDDEN).

07° Lire la nouvelle fonction modification ci-dessous et tenter de comprendre ce qu'elle va provoquer. Vérifier en appuyant sur les boutons.

def modification():

    if (monCanvas.itemcget(pacman_1, 'state')== HIDDEN) :

        monCanvas.itemconfig(pacman_1, state=NORMAL)

        monCanvas.itemconfig(pacman_2, state=HIDDEN)

    else:

        monCanvas.itemconfig(pacman_2, state=NORMAL)

        monCanvas.itemconfig(pacman_1, state=HIDDEN)

Remarque : On notera que dans itemcget, on doit donner l'attribut sous forme d'une chaîne de caractère, alors que dans itemconfig, on donne directement le nom de l'attribut à viser. Tentez d'y penser lorsque vous tapez votre code, cela vous évitera le bug habituel sur ce type de demande.

08° Changer les arguments donnés lorsqu'on crée pacman_2 de façon à ce qu'il soit superposé à pacman_1. Lors de l'utilisation de create_arc, on rajoutera state=NORMAL et state=HIDDEN respectivement sur pacman_1 et pacman_2. Lancer le programme et appuyer sur le bouton pour vérifier que tout se passe bien.

Ce n'est pas encore de l'animation, c'est vrai. Nous verrons comment en faire une vraie dans les parties suivantes. Pour l'heure, nous allons en créer une qu'on ne pourra pas arrêter . Pour cela, nous allons utiliser la récursivité, c'est à dire la faculté d'une fonction de pouvoir s'appeler elle-même.

09° Rajouter cette simple ligne à la fin de la fonction modification : fen_princ.after(300,modification) pour que la fonction se relance automatiquement toutes les 300 ms, soit 0,3s.

Voilà le programme au total :

#!/usr/bin/env python

# -*- coding: utf-8 -*-

from tkinter import *


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

# Fonctions

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

def modification():

    if (monCanvas.itemcget(pacman_1, 'state')== HIDDEN) :

        monCanvas.itemconfig(pacman_1, state=NORMAL)

        monCanvas.itemconfig(pacman_2, state=HIDDEN)

    else:

        monCanvas.itemconfig(pacman_2, state=NORMAL)

        monCanvas.itemconfig(pacman_1, state=HIDDEN)

    fen_princ.after(300,modification)


def temporisation():

    fen_princ.after(500,modification)


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

# Corps du programme

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


fen_princ = Tk()

fen_princ.title("ESSAI AVEC CANVAS")

fen_princ.geometry("600x600")


monCanvas = Canvas(fen_princ, width=500, height=600, bg='ivory', bd=0, highlightthickness=0)

monCanvas.grid(row=0,column=0, padx=10,pady=10)

pacman_1 = monCanvas.create_arc(50,50,150,150,fill="yellow",start=15,extent=330, state=NORMAL)

pacman_2 = monCanvas.create_arc(50,50,150,150,fill="yellow",start=30,extent=300, state=HIDDEN)


zone2 = Frame(fen_princ, bg='#777777')

zone2.grid(row=0,column=1,ipadx=5)

Button(zone2, text="Couleurs", fg="yellow", bg="black", command=modification).pack(fill=X)

Button(zone2, text="Tempo", fg="yellow", bg="black", command=temporisation).pack(fill=X)


fen_princ.mainloop()

4 - Deuxième animation : alternance entre deux états du même item

En réalité, la méthode précédente n'est pas la meilleure ici. Mais elle nous aura permis de découvrir l'attribut state.

Comment faire plus simple ? C'est facile : on ne crée qu'un seul item-arc (pacman_1 par exemple) et on modifie ses valeurs d'attributs start et extent pour alterner entre les valeurs (start=15, extent=330) et (start=30, extent=300).

10° Reprendre le programme de la partie précédente et le modifier pour obtenir le même effet visuel mais avec l'utilisation d'un seul pacman, pacman_1.

Si vous bloquez, tentons de simplifier le problème en plusieurs sous-problèmes :

Voici une solution possible (ne la regardez pas trop vite, c'est en tentant de voir où se trouve le problème qu'on progresse) :

#!/usr/bin/env python

# -*- coding: utf-8 -*-

from tkinter import *


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

# Fonctions

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

def modification():

    # Lignes de code pour voir le contenu et le type de ce que retourne itemcget

    # Vous pouvez les enlever hors du cadre du débogage

    print(monCanvas.itemcget(pacman_1, 'start'))

    print(type(monCanvas.itemcget(pacman_1, 'start')))

    # Retour au vrai programme

    if (monCanvas.itemcget(pacman_1, 'start') == '15.0') :

        print("cas 1")

        monCanvas.itemconfig(pacman_1, start=30, extent=300)

    else:

        print("cas 2")

        monCanvas.itemconfig(pacman_1, start=15, extent=330)

    fen_princ.after(300,modification)


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

# Corps du programme

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


fen_princ = Tk()

fen_princ.title("ESSAI AVEC CANVAS")

fen_princ.geometry("600x600")


monCanvas = Canvas(fen_princ, width=500, height=600, bg='ivory', bd=0, highlightthickness=0)

monCanvas.grid(row=0,column=0, padx=10,pady=10)

pacman_1 = monCanvas.create_arc(50,50,150,150,fill="yellow",start=15,extent=330)


zone2 = Frame(fen_princ, bg='#777777')

zone2.grid(row=0,column=1,ipadx=5)

Button(zone2, text="Bouche", fg="yellow", bg="black", command=modification).pack(fill=X)


fen_princ.mainloop()

Nous garderons cette solution car elle ne fait appel qu'à un seul item (pacman_1).

Il nous reste à voir comment le déplacer. Une solution "bourrine" serait de créer l'item, de le détruire et d'en recréer un plus loin. Mais heureusement, nous allons pouvoir faire autrement.

5 - Troisième animation : déplacer un item

Alors, comment déplacer un item d'un Canvas ? Allons voir la documentation : Vers Effbot.org : http://effbot.org/tkinterbook/canvas.htm

11° Trouver les deux méthodes capables de déplacer un item.

Sur la page donnée en lien, vous devriez avoir trouvé ceci :

coords(item, *coords) [#]

    Returns the coordinates for an item.

    item

        Item specifier (tag or id).

    *coords

        Optional list of coordinate pairs. If given, the coordinates will replace the current coordinates for all matching items.

    Returns:

        If no coordinates are given, this method returns the coordinates for the matching item. If the item specifier matches more than one item, the coordinates for the first item found is returned.

move(item, dx, dy) [#]

    Moves matching items by an offset.

    item

        Item specifier.

    dx

        Horizontal offset.

    dy

        Vertical offset.

Commençons par le second, plus facile à comprendre que le premier. Visiblement, si on veut déplacer un objet de 10 pixels à droite, on peut écrire une ligne de code de type monCanvas.move(pacman_1,10,0) où :

12° Rajouter la ligne de code de mouvement dans la fonction modification Relancer l'animation. Modifier éventuellement la valeur du after pour avoir un mouvement plus fluide.

Nous arrivons à quelque chose comme :

def modification():

    if (monCanvas.itemcget(pacman_1, 'start') == '15.0') :

        monCanvas.itemconfig(pacman_1, start=30, extent=300)

    else:

        monCanvas.itemconfig(pacman_1, start=15, extent=330)

    monCanvas.move(pacman_1,10,0)

    fen_princ.after(100,modification)

Alors comment faire pour que l'animation ne sorte pas de l'écran ?

Il faudrait un test sur la position du pacman pour qu'on puisse lui dire d'avancer ou de reculer en fonction de la situation. Comment faire ?

Il faut utiliser la seconde méthode : monCanvas.coords(pacman_1) va renvoyer les coordonnées de l'item. Pour savoir ce que cela veut dire, nous allons demander au programme d'afficher ce que renvoie la méthode :

13° Rajouter print(monCanvas.coords(pacman_1)) dans la fonction avance() ou modification. Observer le résultat pour répondre à la question suivante : la réponse renvoyée est-elle un 2-uplets, un 4-uplets ou une liste ?

14° Rajouter un test dans modification pour faire avancer le pacman si la coordonnée x du coin inférieur droit est bien inférieure à la taille du Canvas. Sinon, il faudra faire reculer le pacman (c'est à dire faire un move de -10).

Pour rappel, voici comme obtenir le 4e élément d'une liste : liste[3].

Zut. Ca fonctionne mais pas comme nous le pensions. Le pacman se cogne sur la limite droite. Il va falloir gérer le sens de déplacment.

Nous allons gérer cela à l'aide d'une variable globale vit_x mais on peut faire mieux en réalité : nous pourrions gérer le déplacement en connaissant l'orientation du pacman. L'avantage de la variable est que la méthode sera applicable quelque soit l'item à déplacer.

Reprenons le code suivant :

def modification():

    liste_coord = monCanvas.coords(pacman_1)

    # C'est ici que nous allons décider de changer ou non vit_x

    if (monCanvas.itemcget(pacman_1, 'start') == '15.0') :

        monCanvas.itemconfig(pacman_1, start=30, extent=300)

    else:

        monCanvas.itemconfig(pacman_1, start=15, extent=330)

    monCanvas.move(pacman_1,10,0)

    fen_princ.after(100,modification)

15° Réaliser les actions suivantes qui vont permettre d'avoir un pacman autonome sur l'axe x :

Nous pourrions croire que tout marche pour le mieux avec le code ci-dessous. Mais non. Essayez par exemple d'appuyer plusieurs fois sur les boutons : en réalité, il n'existe pas une fonction avance et une fonction recule. En fait, à chaque fois que vous cliquez sur le bouton, le programme 'recrée' une fonction avance par exemple. Et donc, il y a deux fonctions qui font avancer le pacman de 10 pixels chacune, soit 20 pixels en tout. C'est rapidement ingérable. Il faudrait donc trouver un système central de contrôle d'activation et de désactivation des fonctions.

C'est le sujet de la prochaine partie.

Voici le code que vous pourriez avoir obtenu : (attention, j'ai utilisé les noms de fonctions avance_x et recule_x pour pouvoir ensuite gérer les déplacements en y aussi.

#!/usr/bin/env python

# -*- coding: utf-8 -*-

from tkinter import *

vit_x = 10


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

# Fonctions

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

def modification():

    global vit_x

    mod_angle = 0

    liste_coord = monCanvas.coords(pacman_1)

    if liste_coord[2]>500 :

        vit_x = -10

    elif liste_coord[0]<0 :

        vit_x = 10

    if (vit_x <0) :

        mod_angle = 180

    if (monCanvas.itemcget(pacman_1, 'start') == '15.0' or monCanvas.itemcget(pacman_1, 'start') == '195.0') :

        monCanvas.itemconfig(pacman_1, start=30+mod_angle, extent=300)

    else:

        monCanvas.itemconfig(pacman_1, start=15+mod_angle, extent=330)

    monCanvas.move(pacman_1,vit_x,0)

    fen_princ.after(100,modification)


def avance_x():

    global vit_x

    vit_x = 10

    modification()


def recule_x():

    global vit_x

    vit_x = -10

    modification()


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

# Corps du programme

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

fen_princ = Tk()

fen_princ.title("ESSAI AVEC CANVAS")

fen_princ.geometry("600x600")


monCanvas = Canvas(fen_princ, width=500, height=600, bg='ivory', bd=0, highlightthickness=0)

monCanvas.grid(row=0,column=0, padx=10,pady=10)

pacman_1 = monCanvas.create_arc(50,50,150,150,fill="yellow",start=15,extent=330)


zone2 = Frame(fen_princ, bg='#777777')

zone2.grid(row=0,column=1,ipadx=5)

Button(zone2, text="Bouge x", fg="yellow", bg="black", command=modification).pack(fill=X)

Button(zone2, text="Avance x", fg="yellow", bg="black", command=avance_x).pack(fill=X)

Button(zone2, text="Recule x", fg="yellow", bg="black", command=recule_x).pack(fill=X)


fen_princ.mainloop()

Avant de passer à la suite, il nous reste à voir néanmoins la deuxième utilisation de la méthode coords.

coords(item, *coords) [#]

    Returns the coordinates for an item.

    item

        Item specifier (tag or id).

    *coords

        Optional list of coordinate pairs. If given, the coordinates will replace the current coordinates for all matching items.

    Returns:

        If no coordinates are given, this method returns the coordinates for the matching item. If the item specifier matches more than one item, the coordinates for the first item found is returned.

On voit donc qu'on peut également fournir des coordonnées en plus de l'identifiant de l'item (sous la forme x0, y0, x1, y1).

Dans ce cas, on va déplacer l'item aux coordonnées fournies.

Si on voulait faire passer le pacman d'un côté à l'autre de l'écran, on pourrait donc utiliser par exemple :

monCanvas.coords(pacman_1,50,50,150,150) pour faire apparaitre le pacman au point de départ.

16° Créer un bouton et une fonction point_depart qui va faire revenir le pacman aux coordonnées de départ.

6 - Gestion du démarrage et de l'arrêt des méthodes after

En réalité, comme souvent dans les vrais projets, s'il y a un problème, c'est que nous nous y sommes mal pris. Nous avons voulu commencer à coder sans trop réfléchir, en tentant d'avancer sans se soucier de savoir s'il s'agissait d'une solution correcte, à défaut de bonne solution. Vous constaterez souvent qu'on a tendance à trouver la bonne solution à la fin du projet, une fin qu'on est parvenue à mettre au point une méthode finalement pas si bonne que ça. Retenez donc qu'il faut réflechir AVANT de coder. Je vous ai un peu forcé la main sur ce coup là en même temps.

Ce que nous avons oublié, c'est que nous sommes dans une gestion évenementielle et non pas séquentielle.

Que fait la fonction modification ? Elle gère le déplacement du pacman en fonction des valeurs de et de la position du pacman sur le Canvas.

On pourrait donc envisager une solution simple :

17° Réaliser les tâches suivantes :

  1. Rajouter une variable globale nommée animation_active initialisée à False (ce qui veut dire que le pacman ne bouge pas).
  2. Rajouter cette variable en global à modification et la passer à True dès les premières lignes de modification (ce qui veut dire que le pacman bouge).
  3. Modifier avance_x et recule_x pour qu'elles ne lancent l'appel à modification que si animation_active==False.
  4. Lancer les tests pour voir si cela fonctionne.

Normalement, ça devrait fonctionner mais pas pouvoir s'arrêter... Comment faire ? Nous allons créer un bouton et une fonction qui vont avoir comme tâche de mettre vit_x à 0 et de placer une variable globale stop_animation à True.

18° Réaliser les tâches suivantes :

  1. Rajouter une variable globale nommée stop_animation initialisée à False (ce qui veut dire qu'on ne veut pas stopper l'animation pour l'instant)
  2. Créer un bouton associé à une fonction arret.
  3. Créer la fonction arret : placer stop_animation en global et la forcer à True, puis faire de même avec vit_x à 0. De cette façon, la prochaine fois que modification va s'executer, la vitesse sera à 0.
  4. Modifier modification : l'appel à la méthode after ne doit se faire que si stop_animation est False. Sinon, stop_animation est True et on place animation_active à False pour indiquer que l'animation est stoppée. Comme on ne relance plus modification via after, l'animation est stoppée !

Encore une fois, tout devrait fonctionner correctement ... sauf que vous ne devriez plus parvenir à redemarrer une animation après l'appui sur le bouton STOP. Pourquoi ?

Alors ?

La réponse : la variable stop_animation est à quelle valeur depuis l'appui sur STOP ? Et oui, True. Du coup, dès qu'on appuie sur avance ou recule, on ne lance plus la méthode after dans modification.

19° Je vous laisse choisir la façon de règler la situation. Vous pouvez soit remettre la variable stop_animation à False directement dans modification (au bon endroit !) ou lors de l'appel d'avance_x et recule_x.

Voici ce que vous devriez avoir obtenu :

Les passages concernant le second pacman sont surlignés.

#!/usr/bin/env python

# -*- coding: utf-8 -*-

from tkinter import *

vit_x = 10

vit_y = 0

animation_active = False

stop_animation = False


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

# Fonctions

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

def modification():

    global vit_x

    global animation_active

    global stop_animation

    animation_active = True

    mod_angle = 0

    liste_coord = monCanvas.coords(pacman_1)

    if liste_coord[2]>500 :

        vit_x = -10

    elif liste_coord[0]<0 :

        vit_x = 10

    if (vit_x <0) :

        mod_angle = 180

    if (monCanvas.itemcget(pacman_1, 'start') == '15.0' or monCanvas.itemcget(pacman_1, 'start') == '195.0') :

        monCanvas.itemconfig(pacman_1, start=30+mod_angle, extent=300)

    else:

        monCanvas.itemconfig(pacman_1, start=15+mod_angle, extent=330)

    monCanvas.move(pacman_1,vit_x,0)

    if stop_animation == False :

        fen_princ.after(100, modification)

    else:

        stop_animation = False

        animation_active = False


def avance_x():

    global vit_x

    global vit_y

    global stop_animation

    stop_animation = False

    vit_x = 10

    vit_y = 0

    if animation_active == False :

        modification()


def recule_x():

    global vit_x

    global vit_y

    global stop_animation

    stop_animation = False

    vit_x = -10

    vit_y = 0

    if animation_active == False :

        modification()


def point_depart():

    global vit_x

    global vit_y

    vit_x = 0

    vit_y = 0

    arret()

    monCanvas.coords(pacman_1,50,50,150,150)


def arret():

    global stop_animation

    stop_animation = True


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

# Corps du programme

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

fen_princ = Tk()

fen_princ.title("ESSAI AVEC CANVAS")

fen_princ.geometry("600x600")


monCanvas = Canvas(fen_princ, width=500, height=600, bg='ivory', bd=0, highlightthickness=0)

monCanvas.grid(row=0,column=0, padx=10,pady=10)

pacman_1 = monCanvas.create_arc(50,50,150,150,fill="yellow",start=15,extent=330)


zone2 = Frame(fen_princ, bg='#777777')

zone2.grid(row=0,column=1,ipadx=5)


Button(zone2, text="Bouge", fg="yellow", bg="black", command=modification).pack(fill=X)

Button(zone2, text="Avance x", fg="yellow", bg="black", command=avance_x).pack(fill=X)

Button(zone2, text="Recule x", fg="yellow", bg="black", command=recule_x).pack(fill=X)

Button(zone2, text="STOP", fg="yellow", bg="red", command=arret).pack(fill=X)


fen_princ.mainloop()

20° Pour la dernière question de cette partie, vous allez gérer le déplacement en y. On considère comme dans le vieux pacman qu'on ne peut pas aller en diagonale. Il faudra donc vous inspirer du code ci-dessus pour parvenir à rajouter les boutons de déplacement vertical, les fonctions monte_y et descend_y ainsi que les modifications nécessaires dans modification.

C'est possible puisqu'un executable (windows) est téléchargeable en début de chapitre !

7 - COMPLEMENTS : Modification des widgets eux-mêmes : méthode config()

Nous avons vu comment modifier les items des Canvas, mais on peut également modifier les widgets de classe Label, Button ou Canvas eux-mêmes. Voici comment.

Nous allons travailler sur le programme suivant qui propose une correction des questions de la partie 6 et crée l'animation d'un second pacman autonome.

#!/usr/bin/env python

# -*- coding: utf-8 -*-

from tkinter import *

import random

vit_x = 10

vit_y = 0

vit_x2 = -10

vit_y2 = 0

animation_active = False

stop_animation = False


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

# Fonctions

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

def modification():

    global vit_x

    global vit_y

    global animation_active

    global stop_animation

    animation_active = True

    mod_angle = 0

    liste_coord = monCanvas.coords(pacman_1)

    if liste_coord[2]>500 :

        vit_x = -10

    elif liste_coord[0]<0 :

        vit_x =10

    elif liste_coord[1]<0 :

        vit_y = 10

    elif liste_coord[3]>600 :

        vit_y = -10

    if (vit_x <0 or vit_y>0) :

        mod_angle = 180

    if vit_y == 0 :

        if (monCanvas.itemcget(pacman_1, 'start') == '15.0' or monCanvas.itemcget(pacman_1, 'start') == '195.0') :

            monCanvas.itemconfig(pacman_1, start=30+mod_angle, extent=300)

        else:

            monCanvas.itemconfig(pacman_1, start=15+mod_angle, extent=330)

    if vit_x ==0 :

        if (monCanvas.itemcget(pacman_1, 'start') == '105.0' or monCanvas.itemcget(pacman_1, 'start') == '285.0') :

            monCanvas.itemconfig(pacman_1, start=120+mod_angle, extent=300)

        else:

            monCanvas.itemconfig(pacman_1, start=105+mod_angle, extent=330)

    monCanvas.move(pacman_1,vit_x,vit_y)

    modification2()

    if stop_animation == False :

        fen_princ.after(100, modification)

    else:

        stop_animation = False

        animation_active = False


def avance_x():

    global vit_x

    global vit_y

    global stop_animation

    stop_animation = False

    vit_x = 10

    vit_y = 0

    if animation_active == False :

        modification()


def recule_x():

    global vit_x

    global vit_y

    global stop_animation

    stop_animation = False

    vit_x = -10

    vit_y = 0

    if animation_active == False :

        modification()


monte_y():

    global vit_x

    global vit_y

    global stop_animation

    stop_animation = False

    vit_x = 0

    vit_y = -10

    if animation_active == False :

        modification()


def descend_y():

    global vit_x

    global vit_y

    global stop_animation

    stop_animation = False

    vit_x = 0

    vit_y = 10

    if animation_active == False :

        modification()


def point_depart():

    global vit_x

    global vit_y

    vit_x = 0

    vit_y = 0

    arret()

    monCanvas.coords(pacman_1,50,50,150,150)


def arret():

    global stop_animation

    stop_animation = True


def modification2():

    global vit_x2

    global vit_y2

    if animation_active == True :

        direction = random.randint(1,100)

        if direction > 80 :

            if direction > 95 :

                vit_x2 = 10

                vit_y2 = 0

            elif direction > 90 :

                vit_x2 = -10

                vit_y2 = 0

            elif direction > 85 :

                vit_x2 = 0

                vit_y2 = 10

            else:

                vit_x2 = 0

                vit_y2 = -10

        mod_angle = 0

        liste_coord = monCanvas.coords(pacman_2)

        if liste_coord[2]>500 :

            vit_x2 = -10

        elif liste_coord[0]<0 :

            vit_x2 =10

        elif liste_coord[1]<0 :

            vit_y2 = 10

        elif liste_coord[3]>600 :

            vit_y2 = -10

        if (vit_x2 <0 or vit_y2>0) :

            mod_angle = 180

        if vit_y2 == 0 :

            if (monCanvas.itemcget(pacman_2, 'start') == '15.0' or monCanvas.itemcget(pacman_2, 'start') == '195.0') :

                monCanvas.itemconfig(pacman_2, start=30+mod_angle, extent=300)

            else:

                monCanvas.itemconfig(pacman_2, start=15+mod_angle, extent=330)

        if vit_x2 ==0 :

            if (monCanvas.itemcget(pacman_2, 'start') == '105.0' or monCanvas.itemcget(pacman_2, 'start') == '285.0') :

                monCanvas.itemconfig(pacman_2, start=120+mod_angle, extent=300)

            else:

                monCanvas.itemconfig(pacman_2, start=105+mod_angle, extent=330)

        monCanvas.move(pacman_2,vit_x2,vit_y2)


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

# Corps du programme

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

fen_princ = Tk()

fen_princ.title("ESSAI AVEC CANVAS")

fen_princ.geometry("600x600")


monCanvas = Canvas(fen_princ, width=500, height=600, bg='ivory', bd=0, highlightthickness=0)

monCanvas.grid(row=0,column=0, padx=10,pady=10)

pacman_1 = monCanvas.create_arc(50,50,150,150,fill="yellow",start=15,extent=330)

pacman_2 = monCanvas.create_arc(350,350,450,450,fill="orange",start=15,extent=330)


zone2 = Frame(fen_princ, bg='#777777')

zone2.grid(row=0,column=1,ipadx=5)


Button(zone2, text="Droite", fg="yellow", bg="black", command=avance_x).pack(fill=X)

Button(zone2, text="Gauche", fg="yellow", bg="black", command=recule_x).pack(fill=X)

Button(zone2, text="Monte", fg="yellow", bg="black", command=monte_y).pack(fill=X)

Button(zone2, text="Descend", fg="yellow", bg="black", command=descend_y).pack(fill=X)

Button(zone2, text="STOP", fg="yellow", bg="red", command=arret).pack(fill=X)

Button(zone2, text="INIT", fg="yellow", bg="red", command=point_depart).pack(fill=X)


fen_princ.mainloop()

Nous voudrions changer la couleur du bouton activé.

Dans les deux cas, nous allons avoir besoin d'une méthode propre aux widgets de Tkinter : la méthode config.

La page d'accueil de Tkinter sur le site http://effbot.org : Vers Effbot.org : http://effbot.org/tkinterbook/

En allant chercher dans Part II Class reference - Basic Widget Methods vous trouverez toutes les méthodes qui fonctionnent sur tous les widgets : Vers Effbot.org

Si vous voulez connaitre les méthodes spécifiques aux widgets, il faudra aller dans la documentation de votre widget.

21° En allant chercher dans la documentation, trouver la description des méthodes liées à after et les méthodes pour lire ou changer un p

Comme vous le voyez, il y en a plein que nous n'avons pas encore abordé. Conclusion : lorsqu'on veut faire quelque chose dans un projet, il est souvent plus simple de commencer par vérifier dans la documentation. Pas la peine de réinventer une méthode déja présente et certainement optimisée.

Résultat des courses :

cget(key)[#]

    Returns the current value for an option.

    Note that option values are always returned as strings (also if you gave a nonstring value when you configured the widget). Use int and float where appropriate.

config(cnf=None, **kw) [#]

    Modifies one or more widget options.

    If called without an argument, this method returns a dictionary containing the current settings for all widget options. For each option key in the dictionary, the value is either a five-tuple (option, option database key, option database class, default value, current value), or a two-tuple (option alias, option). The latter case is used for aliases like bg (background) and bd (borderwidth).

    Note that the value fields aren’t correctly formatted for some option types. See the description of the keys method for more information, and a workaround.

22° De façon à pouvoir utiliser ces méthodes, créer des noms de références aux différents boutons lors des instanciations.

La solution globale aux questions 22+ est fournie en fin de page.

23° Utiliser alors la méthode cget (qui fonctionne un peu comme itemcget ) pour afficher dans la console la couleur de remplissage du bouton :

24° Utiliser la méthode config (qui fonctionne un peu comme itemconfig ) pour réaliser les tâches suivantes :

Rappel : du coup, on peut faire varier le text d'un widget Label avec un config(text="changement !") plutôt qu'avec un StringVar.

8 - COMPLEMENTS : Cas simple de collision

Cette partie est vraiment hors-sujet par rapport au sujet du chapitre. Mais comment avoir un jeu sans détection des collisions ? Je donne donc quelques explications en attendant un chapitre dédié pour ceux qui veulent aller plus loin.

Comment modifier la couleur du pacman_1 s'il rencontre le pacman_2 ? C'est simple en utilisant la méthode adaptée, à savoir find_overlapping qui renvoie un tuple comportant les numéros d'identification des items compris dans le rectangle défini par les 4 coordonnées x1, y1, x2, y2.

Dans la documentation, on trouve :

find_overlapping(x1, y1, x2, y2) [#]

    Finds all items that overlap the given rectangle, or that are completely enclosed by it.

    x1 :Left edge.

    y1 : Upper edge.

    x2 : Right edge.

    y2 : Lower edge.

    Returns:

        A tuple containing all matching items.

Si on résume : on donne les coordonnées du rectangle, et la méthode renvoie un tuple contenant le ou les numéros des items trouvés.

Des numéros ? Oui, je n'en ai pas encore parlé mais le Canvas reconnait les items qu'on lui crée dessus à l'aide d'un numéro attribué par ordre de création, en commençant par 1 pour une fois ...

Ainsi pacman_1 aura le numéro 1 et pacman_2 le numéro ... 2. Et on s'arrête là puisque ce sont les seuls items du Canvas.

Voici le code de détection à placer dans la fonction modification  :

liste_coord = monCanvas.coords(pacman_1)

liste_items = monCanvas.find_overlapping(liste_coord[0],liste_coord[1],liste_coord[2],liste_coord[3])

if len(liste_items) > 1 :

    monCanvas.itemconfig(pacman_1, fill="red")

else:

    monCanvas.itemconfig(pacman_1, fill="yellow")

Explication ligne par ligne :

liste_coord = monCanvas.coords(pacman_1) : on récupère les coordonnées du rectangle définissant les limites du pacman_1.

liste_items = monCanvas.find_overlapping(liste_coord[0],liste_coord[1],liste_coord[2],liste_coord[3]) : on crée le tuple des numéros d'items trouvés comme ayant au moins un bout en contact avec ce rectangle.

if len(liste_items) > 1 : : Si on trouve plus qu'un objet (c'est à dire autre que pacman_1 plus pacman_2 ici)

on change la couleur du pacman_1 en rouge.

Sinon on revient à la couleur jaune.

25° Utiliser ce bout de code à insérer au même endroit que le liste_coord de la fonction modification.

On peut compliquer un peu les choses si nous avons plus que deux items. Le code réel est fourni dans la correction ci-dessous. En gros : on crée une variable test_collision, initialement fausse. Pour la faire passer à vrai, on teste un à un les n° des items détectés pour vérifier qu'il ne s'agit pas simplement de pacman_1.

Dans ce cas, on break de la boucle for puisque la détection est positive.

    liste_coord = monCanvas.coords(pacman_1)

    liste_items = monCanvas.find_overlapping(liste_coord[0],liste_coord[1],liste_coord[2],liste_coord[3])

    test_collision = False

    if len(liste_items) > 1 :

        for x in liste_items :

            if x != pacman_1 :

                test_collision = True

                break

    if test_collision == True :

        monCanvas.itemconfig(pacman_1, fill="red")

    else:

        monCanvas.itemconfig(pacman_1, fill="yellow")

Dans le prochain chapitre sur les interfaces graphiques, nous verrons comment gérer les événements autres que l'appui sur les boutons de l'interface. La position de la souris, les touches de la souris ou du clavier sont autant de moyens de contrôler les interfaces, surtout pour faire un jeu.

Et le code final sur lequel vous pouvez bricoler plein de choses (position des boutons, un autre pacman...) est :

Avec un print de test qui reste là, oublié de tous... A vous de le supprimer si vous voulez.

#!/usr/bin/env python

# -*- coding: utf-8 -*-

from tkinter import *

import random

vit_x = 10

vit_y = 0

vit_x2 = -10

vit_y2 = 0

animation_active = False

stop_animation = False


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

# Fonctions

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

def modification():

    global vit_x

    global vit_y

    global animation_active

    global stop_animation

    animation_active = True

    mod_angle = 0

    liste_coord = monCanvas.coords(pacman_1)

    liste_items = monCanvas.find_overlapping(liste_coord[0],liste_coord[1],liste_coord[2],liste_coord[3])

    test_collision = False

    if len(liste_items) > 1 :

        for x in liste_items :

            if x != pacman_1 :

                test_collision = True

                break

    if test_collision == True :

        monCanvas.itemconfig(pacman_1, fill="red")

    else:

        monCanvas.itemconfig(pacman_1, fill="yellow")

    if liste_coord[2]>500 :

        vit_x = -10

    elif liste_coord[0]<0 :

        vit_x = 10

    elif liste_coord[1]<0 :

        vit_y = 10

    elif liste_coord[3]>600 :

        vit_y = -10

    if (vit_x <0 or vit_y>0) :

        mod_angle = 180

    if vit_y == 0 :

        if (monCanvas.itemcget(pacman_1, 'start') == '15.0' or monCanvas.itemcget(pacman_1, 'start') == '195.0') :

            monCanvas.itemconfig(pacman_1, start=30+mod_angle, extent=300)

        else:

            monCanvas.itemconfig(pacman_1, start=15+mod_angle, extent=330)

    if vit_x ==0 :

        if (monCanvas.itemcget(pacman_1, 'start') == '105.0' or monCanvas.itemcget(pacman_1, 'start') == '285.0') :

            monCanvas.itemconfig(pacman_1, start=120+mod_angle, extent=300)

        else:

            monCanvas.itemconfig(pacman_1, start=105+mod_angle, extent=330)

    monCanvas.move(pacman_1,vit_x,vit_y)

    modification2()

    if stop_animation == False :

        fen_princ.after(100, modification)

    else:

        stop_animation = False

        animation_active = False


def init_couleurs():

    but_avance.config(bg="black")

    but_recule.config(bg="black")

    but_monte.config(bg="black")

    but_descend.config(bg="black")

    but_init.config(bg="red")

    but_arret.config(bg="red")


def avance_x():

    print(but_avance.cget('bg'))

    init_couleurs()

    but_avance.config(bg="blue")

    global vit_x

    global vit_y

    global stop_animation

    stop_animation = False

    vit_x = 10

    vit_y = 0

    if animation_active == False :

        modification()


def recule_x():

    init_couleurs()

    but_recule.config(bg="blue")

    global vit_x

    global vit_y

    global stop_animation

    stop_animation = False

    vit_x = -10

    vit_y = 0

    if animation_active == False :

        modification()


def monte_y():

    init_couleurs()

    but_monte.config(bg="blue")

    global vit_x

    global vit_y

    global stop_animation

    stop_animation = False

    vit_x = 0

    vit_y = -10

    if animation_active == False :

        modification()


def descend_y():

    init_couleurs()

    but_descend.config(bg="blue")

    global vit_x

    global vit_y

    global stop_animation

    stop_animation = False

    vit_x = 0

    vit_y = 10

    if animation_active == False :

        modification()


def point_depart():

    init_couleurs()

    but_init.config(bg="blue")

    global vit_x

    global vit_y

    vit_x = 0

    vit_y = 0

    arret()

    monCanvas.coords(pacman_1,50,50,150,150)

    monCanvas.itemconfig(pacman_1, start=15,extent=330)

    monCanvas.coords(pacman_2,350,350,450,450)

    monCanvas.itemconfig(pacman_2, start=15,extent=330)


def arret():

    init_couleurs()

    but_arret.config(bg="blue")

    global stop_animation

    stop_animation = True


def modification2():

    global vit_x2

    global vit_y2

    if animation_active == True :

        direction = random.randint(1,100)

        if direction > 80 :

            if direction > 95 :

                vit_x2 = 10

                vit_y2 = 0

            elif direction > 90 :

                vit_x2 = -10

                vit_y2 = 0

            elif direction > 85 :

                vit_x2 = 0

                vit_y2 = 10

            else:

                vit_x2 = 0

                vit_y2 = -10

        mod_angle = 0

        liste_coord = monCanvas.coords(pacman_2)

        if liste_coord[2]>500 :

            vit_x2 = -10

        elif liste_coord[0]<0 :

            vit_x2 =10

        elif liste_coord[1]<0 :

            vit_y2 = 10

        elif liste_coord[3]>600 :

            vit_y2 = -10

        if (vit_x2 <0 or vit_y2>0) :

            mod_angle = 180

        if vit_y2 == 0 :

            if (monCanvas.itemcget(pacman_2, 'start') == '15.0' or monCanvas.itemcget(pacman_2, 'start') == '195.0') :

                monCanvas.itemconfig(pacman_2, start=30+mod_angle, extent=300)

            else:

                monCanvas.itemconfig(pacman_2, start=15+mod_angle, extent=330)

        if vit_x2 ==0 :

            if (monCanvas.itemcget(pacman_2, 'start') == '105.0' or monCanvas.itemcget(pacman_2, 'start') == '285.0') :

                monCanvas.itemconfig(pacman_2, start=120+mod_angle, extent=300)

            else:

                monCanvas.itemconfig(pacman_2, start=105+mod_angle, extent=330)

        monCanvas.move(pacman_2,vit_x2,vit_y2)


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

# Corps du programme

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

fen_princ = Tk()

fen_princ.title("ESSAI AVEC CANVAS")

fen_princ.geometry("600x600")


monCanvas = Canvas(fen_princ, width=500, height=600, bg='ivory', bd=0, highlightthickness=0)

monCanvas.grid(row=0,column=0, padx=10,pady=10)

pacman_1 = monCanvas.create_arc(50,50,150,150,fill="yellow",start=15,extent=330)

pacman_2 = monCanvas.create_arc(350,350,450,450,fill="orange",start=15,extent=330)


zone2 = Frame(fen_princ, bg='#777777')

zone2.grid(row=0,column=1,ipadx=5)


but_avance = Button(zone2, text="Droite", fg="yellow", bg="black", command=avance_x)

but_recule = Button(zone2, text="Gauche", fg="yellow", bg="black", command=recule_x)

but_monte = Button(zone2, text="Monte", fg="yellow", bg="black", command=monte_y)

but_descend = Button(zone2, text="Descend", fg="yellow", bg="black", command=descend_y)

but_arret = Button(zone2, text="STOP", fg="yellow", bg="red", command=arret)

but_init = Button(zone2, text="INIT", fg="yellow", bg="red", command=point_depart)

but_avance.pack(fill=X)

but_recule.pack(fill=X)

but_monte.pack(fill=X)

but_descend.pack(fill=X)

but_arret.pack(fill=X)

but_init.pack(fill=X)


fen_princ.mainloop()

Attention, cette solution est loin d'être optimisée. Elle possède notamment un défaut majeur : on active l'animation du second pacman à partir de la fonction du premier. Si on veut créer plusieurs pacmans supplémentaires, ça risque d'être rapidement ennuyeux...

Vous avez sous les yeux un exemple de code qui a été créé de façon organique par un élève : on part d'un code simple et on le complexifie au fur et à mesure. Si on avait pris un peu plus de recul, nous aurions pu facilement rendre les deux animations plus indépendantes. Nous verrons comme faire lors des activités programmation objet.