Aller au contenu

Ressources⚓︎

Tutoriels divers sur Tkinter

Deux tutoriels de présentation du module d'interface graphique Tkinter :

Sitographie de sites ressources sur Tkinter :

Introduction⚓︎

Ancienne version⚓︎

À partir de l'année scolaire 2026/2027, l'ancienne version de l'introduction aux projets Tkinter est remplacé par un nouveau TP. Cependant, l'ancienne version reste intéressante en complément.

Introduction ancienne version

Téléchargement des scripts à compléter des exercices 1 à 4

Exercice 1

  1. Télécharger le script hello_world.py et l'exécuter dans l'IDE Spyder.

    code source
    🐍 Script Python
    from tkinter import *                   # import du module
    
    fen = Tk()                              # création de la fenêtre racine
    msg = Label(fen, text="Hello World")    # création d'un widget Label dans la fenêtre racine
    msg.pack()                              # positionnement géométrique du widget avec la méthode `pack`
    fen.mainloop()                          # boucle infinie avec réceptionnaire d'événements
    
    À retenir
    • tkinter est un module de la bibliothèque standard qui permet de réaliser des interfaces graphiques.
    • Chaque objet graphique, appelé widget, est créé avec un constructeur, par exemple Label dont le premier argument est l'objet parent dans lequel il est placé.
    • Le widget créé avec le constructeur Tk est l'ancêtre de tous les autres, il n'a pas de parent, c'est la fenêtre racine. De plus il n'est pas nécessaire de positionner géométriquement cet objet.
    • Pour que le widget construit s'affiche il faut deux instructions supplémentaires :
      • le positionnement géométrique du widget dans son parent : dans l'exemple ci-dessus avec la méthode pack. On utilise la notation pointée objet.methode()
      • en dernière instruction du programme, il faut lancer la méthodemainloop sur la fenêtre racine : c'est une boucle infinie avec un réceptionnaire d'événements qui va capturer tous les événements (appui sur touche, mouvement de la souris ...). En effet une interface graphique fonctionne avec une programmation par événements.
  2. Remplacer msg = Label(fen, text="Hello World") par msg = Label(fen, text="Hello World", font=("Arial", 20)). Exécuter, observer.

  3. La fenêtre est-elle redimensionnable ? Insérer les instructions suivantes, juste après la définition de la fenêtre racine. Exécuter, observer.

    🐍 Script Python
    fen.geometry("300x50")
    fen.resizable(width=False, height=False)
    

Exercice 2

  1. Télécharger le script controle_barre.py et l'exécuter dans l'IDE Spyder.

    Code source
    🐍 Script Python
    from tkinter import *   # import du module tkinter
    
    ### Constantes (valeurs non modifiables à l'exécution)
    LARG_BARRE = 60  # largeur de la barre en pixels
    HAUT_BARRE = 20  # hauteur de la barre en pixels
    DX = 10          # déplacement horizontal en pixels pour un appui sur touche
    DY = 10          # déplacement vertical en pixels pour un appui sur touche
    HAUT_FENETRE = 400
    LARG_FENETRE = 400
    
    ### Variables globales (valeurs modifiables à l'exécution)
    x_barre = 160   # abscisse de la barre
    y_barre = 180   # ordonnée de la barre
        
    
    ### Modèle
    
    def deplacement(event):
        """Fonction gestionnaire de l'événement
        appui sur une touche"""
        global x_barre, y_barre   # variables globales
        if event.keysym == 'Up':
                y_barre = y_barre - DY
        elif event.keysym == 'Down':
                y_barre = y_barre + DY
        # à compléter  avec les déplacements droite / gauche  
        # on modifie les coordonnées de la barre dans le widget canevas
        can.coords(barre, x_barre, y_barre, x_barre + LARG_BARRE, y_barre + HAUT_BARRE)
        # on met à jour l'affichage de la fenêtre
        fen.update()
        
    ### Vue : interface graphique
    
    # Fenêtre racine
    fen = Tk()
    fen.title("Controle Barre")
    
    # Canevas d'affichage de la barre
    can = Canvas(fen, background="#000000", width=LARG_FENETRE, height=HAUT_FENETRE)
    can.pack()
    # Barre rectangulaire dessinée dans le canevas
    barre = can.create_rectangle(x_barre, y_barre, x_barre + LARG_BARRE, y_barre + HAUT_BARRE, fill="white")
            
    
    ### Controleur
    
    # Si l'événement "Appui sur une touche" est intercepté, on appelle la fonction deplacement
    fen.bind("<KeyPress>",  deplacement)
                
            
    ### Boucle infinie , réceptionnaire d'événement
    fen.mainloop()
    
    À retenir

    Dans cet exemple, on organise le programme selon l'architecture Modèle - Vue - Contrôleur :

    alt

    Source : Wikipedia

    Partie Vue

    C'est la mise en place de tous les widgets de l'interface graphique. Ici la fenêtre racine contient un widget Canvas qui est une zone de dessin. Dans ce canevas, on dessine un rectangle. Ce n'est pas un widget, c'est un item du canevas : la méthode de positionnement géométrique pack s'applique uniquement au canevas pour le positonner dans son parent la fenêtre racine.

    Partie Modèle

    La fonction deplacement va être appelée par un contrôleur pour mettre à jour un objet graphique, ici l'item barre dessiné dans le canevas. Elle prend en paramètre l'événement event qui lui a été transmis et selon sa valeur (le symbole de la touche appuyée) modifie l'abscisse ou l'ordonnée avec la méthode coords du canevas. Après avoir modifié l'objet graphique, il faut mettre à jour l'affichage de toute la fenêtre racine avec fen.update().

    deplacement est un gestionnaire d'événement.

    Repérage dans le canevas

    Traditionnellement en informatique, l'origine du repère est en haut à gauche de la zone de dessin, l'axe des abscisses est orienté vers la droite et la 'axe des ordonnées vers le bas.

    alt

    Partie Contrôleur

    Le réceptionnaire d'événements de la boucle mainloop() capture tous les événements (appui touche, mouvement de la souris). L'instruction fen.bind("<KeyPress>", deplacement) met en place un contrôleur qui va appeler l'exécution de la fonction deplacement (partie Modèle) si l'événement Appui sur une touche est capturé.

  2. Pourquoi faut-il retrancher DY si on veut déplacer verticalement la barre vers le haut ?

  3. Dans le programme les noms des constantes sont en majuscules et les noms des variables en minuscules. Quelle est la différence entre constantes et variables ?
  4. Dans le programme on utilise deux variables globales x_barre et y_barre. Quelle est la différence avec la variable locale var_locale ? A quoi sert le mot clef global dans une fonction ?
  5. Modifier la fonction deplacement pour qu'elle gère aussi les déplacements horizontaux, lorsqu'on appuie sur la flèche correspondante du pavé directionnel.
  6. Modifier la fonction deplacement pour que la barre soit bloquée lorsqu'un de ses bords touche un bord du canevas. La barre ne doit pas pouvoir sortir du canevas. On pourra utiliser les fonctions min et max. Par exemple pour que la barre ne puisse pas sortir par le bas du canevas, on peut écrire :

    🐍 Script Python
    y_barre = max(0, y_barre - DY)
    

Exercice 3

  1. Télécharger le script clic_ballon.py et l'exécuter dans l'IDE Spyder.

    Code source
    🐍 Script Python
    from tkinter import *
    import random
    
    ### Constantes (valeurs non modifiables à l'execution)
    HAUT_FENETRE = 400
    LARG_FENETRE = 400
    
    
    ### Fonctions : boite à outils 
    
    def from_rgb(r, g, b):
        """
        Description :
    
        Renvoie le codage RGB d'une couleur en notation HTML au format #FF00AA
        à partir des composantes rouge r, vert v et bleue b
    
        Paramètre :
            r : entier entre 0 et 255
            g : entier entre 0 et 255
            b : entier entre 0 et 255
    
        Renvoie : 
            une chaien de caractères de type str
        """
        # Préconditions sur les paramètres
        assert isinstance(r, int) and 0 <= r <= 255, "r doit être un entier entre 0 et 255"
        assert isinstance(g, int) and 0 <= g <= 255, "g doit être un entier entre 0 et 255"
        assert isinstance(b, int) and 0 <= g <= 255, "b doit être un entier entre 0 et 255"
        # valeur renvoyée
        return f'#{r:02x}{g:02x}{b:02x}'
    
    ### Modèle
    
    def ballon(event):
        """Fonction gestionnaire de l'événement
        clic sur bouton gauche de souris"""
        rouge = 255
        vert = 0
        bleu = 0
        can.create_oval(event.x - 10, event.y - 10, event.x + 10, event.y + 10, fill=from_rgb(rouge, vert, bleu))
        fen.update()
    
    ### Vue : interface graphique
    # Fenetre racine
    fen = Tk()
    fen.title("Clic ballon")
    
    
    # Canevas d'affichage de la barre
    can = Canvas(fen, background="#000000", width=LARG_FENETRE, height=HAUT_FENETRE)
    can.pack()
                
    
    
    ### Controleur
    
    # Si l'événement "Appui sur une touche" est intercepté, on appelle la fonction deplacement
    fen.bind("<ButtonPress-1>",  ballon)
    
    ### Boucle infinie , réceptionnaire d'événement
    fen.mainloop()
    
  2. Quel est le type de la valeur renvoyée par la fonction from_rgb ?

  3. Insérer print(help(from_rgb)) avant fen.mainloop(). Que s'affiche-t-il dans la console interactive ?
  4. Dans la fonction from_rgb a quoi servent les instructions assert ?
  5. Dans la partie Contrôleur, ajouter une instruction pour mettre en place un contrôleur qui appelle la fonction ballon si l'événement "<ButtonPress-1>" (clic sur le bouton gauche de la souris) est capturé. Exécuter de nouveau et décrire le fonctionnement de ce programme.
  6. Qu'est-ce que le codage (Rouge, Vert, Bleu) d'une couleur ?
  7. Dans la console interactive afficher l'aide de la fonction randint du module random (importé au début du programme) avec help(random.randint). Modifier alors la fonction ballon pour que lorsqu'on clique, un ballon de couleur aléatoire soit créé. tkinter utilise-t-il la notation décimale pour représenter les valeurs des composantes (Rouge, Vert, Bleu) d'un pixel ?

Exercice 4

  1. Télécharger le script mention.py et l'exécuter dans l'IDE Spyder.

    Code source
    🐍 Script Python
    from tkinter import *
            
    ### Modèle
    def calcul_mention():
        """Calcul de la mention
        Modifie la variable de contrôle mention"""
        m = float(moyenne.get())
        if 0<= m <=20:
            if m < 8:
                mention.set("Refusé")
            elif m < 10:
                mention.set("Passe second groupe")
            elif m < 12:
                mention.set("Passable")
            elif m < 14:
                mention.set("Mention Assez Bien")
            # à compléter avec tous les autres cas de mention
        else:
            mention.set("Valeur non conforme")
        
    ### Vue : interface graphique
    
    # Fenêtre racine
    fen = Tk()
    fen.title("Compte à rebours")
    fen.geometry("600x300")
    fen.resizable(width=False, height=False)
    # Variables de contrôles (variables globales)
    moyenne = StringVar()  # variable de contrôle du widget saisie_moyenne
    mention = StringVar()  # variable de contrôle du widget etiq_mention 
    # Etiquette du champ de saisie
    etiq_moy = Label(fen, text="Moyenne : ", font=("Arial", 20))
    etiq_moy.pack()
    # Champ de saisie de la moyenne
    saisie_moyenne = Entry(fen, textvariable=moyenne, font=("Arial", 20), fg='blue')
    saisie_moyenne.pack()
    # Bouton de commande
    bouton = Button(fen, text="Calcul mention", command=calcul_mention, font=("Arial", 20))
    bouton.pack()
    # Etiquette d'affichage de la mention
    etiq_mention = Label(fen, textvariable=mention, font=("Arial", 20), fg='red')
    etiq_mention.pack()
        
    ### Boucle infinie , réceptionnaire d'événement
    fen.mainloop()
    
    À retenir

    Dans le programme clic_ballon.py on utilise deux nouveaux types de widgets :

    • Entry qui est un champe de saisie permettant de récupérer une valeur saisie par l'utilisateur.
    • Button qui permet de lancer l'exécution d'une fonction lorsqu'on clique dessus. En fait il s'agit d'un contrôleur visible destiné à capturer le clic sur un emplacement précis de la fenêtre (le bouton en l'occurrence). C'est pourquoi on a un paramètre command qui prend pour valeur la fonction appelée lorsqu'on clique sur le bouton, c'est le même principe que pour une fonction appelée par un contrôleur d'événement comme dans fen.bind("<ButtonPress-1>", ballon).

    Où est stockée la valeur de la saisie de l'utilisateur dans le widget Entry ?

    • Dans la variable de contrôle qui lui est passée en valeur du paramètre textvariable. Ici on a :

      🐍 Script Python
      saisie_moyenne = Entry(fen, textvariable=moyenne, font=("Arial", 20), fg='blue')
      

      La variable de contrôle est donc moyenne. Elle doit avoir été définie avant par :

      🐍 Script Python
      moyenne = StringVar()  
      

      Notez qu'il s'agit d'un type de variable spécifique à tkinter. Les variables de contrôles sont des variables globales qui permettent de contrôler des valeurs liées à des widgets : par exemple la valeur du champ de saisie pour un widget Entry ou d'une étiquette pour un widget Label.

      Lecture / écriture dans une variable de contrôle

      La syntaxe est spécifique pour une variable de contrôle tkinter :

      🐍 Script Python
      moyenne = StringVar()   # définition de la variable de contôle de type chaîne de caractère
      valeur = moyenne.get()  # lecture
      moyenne.set("nouvelle valeur")
      

      On peut modifier une variable globale de contrôle dans une fonction sans utiliser le mot clef global.

    • On a une autre variable de contrôle mention qui contrôle la valeur affichée dans le widget Label. Dans la fonction calcul_mention et la variable moyenne est lue et la variable mention est modifiée.

  2. Modifier la fonction calcul_mention() pour qu'elle traite tous les cas possibles de mention au bac.

    Mentions du bac

    Source : https://www.education.gouv.fr/reussir-au-lycee/le-baccalaureat-general-10457

    Les mentions ne sont attribuées qu'aux candidats obtenant le baccalauréat à l'issue du premier groupe d'épreuves, en fonction de la moyenne obtenue :

    • mention assez bien (AB) : pour une moyenne entre 12 et 14 ;
    • mention bien (B) : pour une moyenne entre 14 et 16 ;
    • mention très bien (TB) : pour une moyenne entre 16 et 18.

    Depuis la session 2021, la mention très bien avec les félicitations du jury peut être accordée aux candidats présentant une moyenne supérieure à 18.

Correction des exercices 1 à 4

Il n'est pas possible d'exécuter le module tkinter dans Capytale. Pour exécuter tkinter dans un navigateur, vous pouvez utiliser la plateforme replit.com, il existe une version gratuite mais il faut se créer un compte. Sinon il faut installer une distribution Python sur son PC et dans ce cas Edupyter est un très bon choix car c'est une distribution portable que pouvez exécuter depuis une clef USB sur n'importe quel poste sous Windows.

Code source hello_world.py
🐍 Script Python
from tkinter import *

fen = Tk()
msg = Label(fen, text="Hello World")
msg.pack()
fen.mainloop()
Code source controle_barre.py
🐍 Script Python
from tkinter import *

### Constantes (valeurs non modifiables à l'exécution)
LARG_BARRE = 60  # largeur de la barre en pixels
HAUT_BARRE = 20  # hauteur de la barre en pixels
DX = 10          # déplacement horizontal en pixels pour un appui sur touche
DY = 10          # déplacement vertical en pixels pour un appui sur touche
HAUT_FENETRE = 400
LARG_FENETRE = 400

### Variables globales (valeurs modifiables à l'exécution)
x_barre = 160
y_barre = 180
    
### Vue : interface graphique
# Fenêtre racine
fen = Tk()
fen.title("Controle Barre")


# Canevas d'affichage de la barre
can = Canvas(fen, background="#000000", width=LARG_FENETRE, height=HAUT_FENETRE)
can.pack()
# Barre rectangulaire dessinée dans le canevas
barre = can.create_rectangle(x_barre, y_barre, x_barre + LARG_BARRE, y_barre + HAUT_BARRE, fill="white")

### Modèle

def deplacement(event):
    """Fonction gestionnaire de l'événement
    appui sur une touche"""
    global x_barre, y_barre
    if event.keysym == 'Up':
            y_barre = max(0, y_barre - DY)
    elif event.keysym == 'Down':
            y_barre = min(y_barre + DY,  HAUT_FENETRE - HAUT_BARRE)
    elif event.keysym == 'Right':
            x_barre = min(x_barre + DX, LARG_FENETRE - LARG_BARRE,)  
    elif event.keysym == 'Left':
            x_barre = max(0, x_barre - DX)       
    can.coords(barre, x_barre, y_barre, x_barre + LARG_BARRE, y_barre + HAUT_BARRE)
    fen.update()
            


### Controleur

# Si l'événement "Appui sur une touche" est intercepté, on appelle la fonction deplacement
fen.bind("<KeyPress>",  deplacement)

            
        
### Boucle infinie , réceptionnaire d'événement
fen.mainloop()
Code source clic_ballon.py
🐍 Script Python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from tkinter import *
import random

### Constantes (valeurs non modifiables à l'exécution)
HAUT_FENETRE = 400
LARG_FENETRE = 400


### Fonctions :boite à outils 

def from_rgb(r, g, b):
    """
    Description :

    Renvoie le codage RGB d'une couleur en notation HTML au format #FF00AA
    à partir des composantes rouge r, vert v et bleue b

    Paramètre :
        r : entier entre 0 et 255
        g : entier entre 0 et 255
        b : entier entre 0 et 255

    Renvoie : 
        une chaien de caractères de type str
    """
    # Préconditions sur les paramètres
    assert isinstance(r, int) and 0 <= r <= 255, "r doit être un entier entre 0 et 255"
    assert isinstance(g, int) and 0 <= g <= 255, "g doit être un entier entre 0 et 255"
    assert isinstance(b, int) and 0 <= g <= 255, "b doit être un entier entre 0 et 255"
    # valeur renvoyée
    return f'#{r:02x}{g:02x}{b:02x}'


### Vue : interface graphique
# Fenêtre racine
fen = Tk()
fen.title("Clic ballon")


# Canevas d'affichage de la barre
can = Canvas(fen, background="#000000", width=LARG_FENETRE, height=HAUT_FENETRE)
can.pack()

### Modèle

def ballon(event):
    """Fonction gestionnaire de l'événement
    clic sur bouton gauche de souris"""
    rouge = random.randint(0, 255)
    vert = random.randint(0, 255)
    bleu = random.randint(0, 255)
    can.create_oval(event.x - 10, event.y - 10, event.x + 10, event.y + 10, fill=from_rgb(rouge, vert, bleu))
    fen.update()
            


### Controleur

# Si l'événement "Appui sur une touche" est intercepté, on appelle la fonction deplacement
fen.bind("<ButtonPress-1>",  ballon)

            
        
### Boucle infinie , réceptionnaire d'événement
fen.mainloop()
Code source mention.py
🐍 Script Python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from tkinter import *


### Modèle
def calcul_mention():
    m = float(moyenne.get())
    if 0 <= m <= 20:
        if m < 8:
            mention.set("Refusé")
        elif m < 10:
            mention.set("Passe second groupe")
        elif m < 12:
            mention.set("Passable")
        elif m < 14:
            mention.set("Mention Assez Bien")
        elif m < 16:
            mention.set("Mention Bien")
        elif m < 18:
            mention.set("Mention Très Bien")
        else:
            mention.set("Mention Très Bien avec félicitations du jury")
    else:
        mention.set("Valeur non conforme")


### Vue : interface graphique


# Fenêtre racine
fen = Tk()
fen.title("Mention du bac")
fen.geometry("600x300")

# Variables de contrôle (variables globales)
moyenne = StringVar()  # variable de contrôle du widget saisie_moyenne
mention = StringVar()  # variable de contrôle du widget etiq_mention

# Etiquette du champ de saisie
etiq_moy = Label(fen, text="Moyenne : ", font=("Arial", 20))
etiq_moy.pack()
# Champ de saisie de la moyenne
saisie_moyenne = Entry(
    fen, textvariable=moyenne, font=("Arial", 20), fg="blue"
)
saisie_moyenne.pack()
# Bouton de commande
bouton = Button(
    fen, text="Calcul mention", command=calcul_mention, font=("Arial", 20)
)
bouton.pack()
# Etiquette d'affichage de la mention
etiq_mention = Label(fen, textvariable=mention, font=("Arial", 20), fg="red")
etiq_mention.pack()


### Boucle infinie , réceptionnaire d'événement
fen.mainloop()

Nouvelle version⚓︎

À partir de l'année scolaire 2026/2027, on introduit les projets Tkinter avec le TP en lien ci-dessous.

TP de prise en main de Tkinter

Projets⚓︎

Exemple de gestion d'une grille

Le code ci-dessous permet de dessiner une grille de \(20 \times 30\) carrés de côté 20 pixels (fonds blanc et bord rouge) et de colorier en bleu un carré choisi aléatoirement chaque fois qu'on appuie sur le bouton.

Les carrés sont dessinés avec la méthode create_rectangle du canevas et sont stockés dans une liste de listes grille :

  • id_rec = grille[y][x] permet de récupérer l'identifiant du carré en ligne y et colonne x
  • can.itemcget(id_rec, "fill") permet de lire la couleur de ce carré
  • can.itemconfig(id_rec, fill="#0000ff") permet de changer en bleu la couleur de fonds du carré (on utilise le formar hexadécimal du codage RGB)

Résultat attendu

alt

🐍 Script Python
from tkinter import *
import random

def action_bouton():
    y = random.randint(0, 19)
    x = random.randint(0, 29)
    id_rec = grille[y][x]
    print(can.itemcget(id_rec, "fill"))
    can.itemconfig(id_rec, fill="#0000ff")
    
# Définition de l'interface graphique

# Fenêtre racine
fen = Tk()
fen.title("Crible aléatoire")
# Canevas
can = Canvas(fen, width=300, height=200)
can.pack() # positionnement
# dessin d'une grillee de 20 x 30 rectangles de coté 10 pixels stockés dans une liste de listes 
# pour récupérer leurs identifiants
grille = [
        [
            can.create_rectangle(
                x * 10,
                y * 10,
                (x + 1) * 10,
                (y + 1) * 10,
                outline="red",
                fill="#ffffff",
            )
            for x in range(30)
        ]
        for y in range(20)
    ]

# Bouton de contrôle
bouton = Button(fen, text="Crible !", command=action_bouton)
bouton.pack()

fen.mainloop()
💯 Grille d'évaluation sur 10 points

Tâches à réaliser :

  • Réaliser un code répondant au cahier des charges.
  • Présentation orale du projet de \(3 \times 4\) minutes maximum. Exemple de répartition de la parole pour un un binôme :
    • Introduction avec présentation du cahier des charges (élève 1)
    • Premier focus sur une partie du code (élève 1)
    • Second focus sur une autre partie du code (élève 2)
    • Conclusion : bilan sur les compétences acquises (élève 2)
Domaine Compétence Points
Global, code Exécution conforme et sans bugs du code 1
Global, code Découpage en fonctions pertinent 0,5
Global, code Choix pertinent des structures de données (types de base, liste, dictionnaire ...) 0,5
Global, code Choix pertinent des structures de contrôle (boucles, tests ...) 1
Global, code Lisibilité du code et commentaires 1
Global, oral Répartition équilibrée et fluide de la parole dans le groupe 1
Individuel, oral Prise de parole fluide, sans hésitations, sans notes 1
Individuel, oral Positionnement de la voix et du corps 1
Individuel, oral Précision du vocabulaire 1
Individuel, oral Qualité des explications en tenant compte de la difficulté du contenu 2

Projet 1 : color picker

Niveau de difficulté

Facile, moins de 80 lignes de code.

Cahier des charges

Réaliser une interface graphique en tkinter qui propose trois échelles graduées de 0 à 255 pour choisir les composantes de Rouge, Vert et Bleu et affiche le codage en hexadécimal #RRVVBB de la couleur (R, V, B) et un canevas rectangulaire colorié avec cette couleur

Résultat attendu

alt

🐍 Script Python
fen.bind_all("<Motion>", changer_canevas) # fen étant la fenêtre racine
  • Codage d'un entier positif en base 16 (hexadécimal) : voir page 7 du Cours sur la représentation des entiers
  • Fonctions, variables, tests, boucles, fondamentaux sur tkinter vus dans les quatre exercices d'introduction.
🐍 Script Python
from tkinter import *


#%% Constantes


CHIFFRES_HEX = "0123456789ABCDEF"

#%% Modèle / Fonctions

def dec_to_hex(d):
    """Convertit un entier compris entre 0 et 255
    en un nombre hexadécimal sur deux chiffres
    Renvoie une chaîne de caractères
    """
    # à compléter


def test_dec_to_hex():
    assert dec_to_hex(4) == "04"
    assert dec_to_hex(10) == "0A"
    assert dec_to_hex(255) == "FF"
    assert dec_to_hex(166) == "A6"

def changer_canevas(event):
    # à compléter
    # ne pas oublier de mettre à jour l'affichage de la fenêtre
    fen.update()

#%% Vue : interface graphique

# Fenêtre racine
fen = Tk()
fen.title("Color_picker")
fen.geometry("400x500")

# Echelle de graduation du rouge
rouge = IntVar()
graduation_rouge = Scale(
    fen,
    orient="horizontal",
    from_=0,
    to=255,
    resolution=1,
    tickinterval=50,
    length=350,
    variable=rouge,
    label="Rouge",
)
graduation_rouge.pack()

# Echelle de graduation du vert

# à compléter


# Echelle de graduation du bleu

# à compléter


# Code HTML de la couleur

# à compléter


# Canevas d'affichage de la couleur

# à compléter


#%% Controleur

fen.bind_all("<Motion>", changer_canevas)


#%% Boucle infinie , réceptionnaire d'événement

fen.mainloop()

Projet 2 : chiffrement de César

Niveau de difficulté

Facile, moins de 80 lignes de code.

Cahier des charges

Réaliser une interface graphique en tkinter qui permet de saisir un mot source en lettre capitale, de sélectionner un décalage entre 0 et 25 et d'appuyer sur un bouton pour afficher le chiffrement du mot source avec l'algorithme de chiffrement par décalage de César.

alt

Source : Wikipedia Le chiffre de César fonctionne par décalage des lettres de l'alphabet. Par exemple dans l'image ci-dessus, il y a une distance de 3 caractères, donc B devient E dans le texte codé.

Résultat attendu

alt

  • Codage des caractères, voir le cours. Vous aurez surtout besoin des fonctions chr et ord :

    🐍 Script Python
    >>> ord('A')
    65
    >>> chr(65)
    'A'
    >>> chr(ord('A') + 3)
    'D'
    >>> chr(ord('A') + (3 + 26) % 26)
    'D'
    
  • Fonctions, variables, tests, boucles, fondamentaux sur tkinter vus dans les quatre exercices d'introduction.

🐍 Script Python
from tkinter import *

#%% Modèle / Fonctions
def chiffrement():
    """
    Fonction de rappel appelée par un appui sur le bouton bouton_chiffre

    Returns
    -------
    Modifie le contenu de la variable de contrôle mot_chiffre
    avec le chiffrement de la variable de contrôle mot_source
    par décalage donné par la variable de contrôle decalage
    """
    # à compléter


#%% Vue : interface graphique
# Fenêtre racine
fen = Tk()
fen.title("Chiffrement de César")
fen.geometry("600x400")


# Cadre source
cadre_source = Frame(fen)
etiq_source = Label(cadre_source, text="Mot source : ", font=("Arial", 20))
mot_source = StringVar()
saisie_source = Entry(
    cadre_source, textvariable=mot_source, font=("Arial", 20)
)
etiq_source.pack(side=LEFT)
saisie_source.pack(side=LEFT)
cadre_source.pack(anchor=W)


# Cadre décalage
cadre_decalage = Frame(fen)
decalage = IntVar()
# à compléter

# Cadre bouton
cadre_bouton = Frame(fen)
# à compléter


# Cadre chiffre
cadre_chiffre = Frame(fen)
mot_chiffre = StringVar()
# à compléter

#%% Boucle infinie , réceptionnaire d'événement
fen.mainloop()

Projet 3 : alternatif (jeu)

Niveau de difficulté

Facile, moins de 80 lignes de code.

Cahier des charges

Réaliser une interface graphique en tkinter qui présente une grille carrée avec un des cases noires délimitées par des traits rouges. Lorsque le joueur clique sur une case il change sa couleur ainsi que celle des cases de son voisinage vertical ou horizontal. La couleur d'une case peut alterner entre noir et blanc. Le but du jeu est de colorier toute la grille en blanc.

Résultat attendu

alt

🐍 Script Python
fen.bind_all("<ButtonPress-1>", clic) # fen étant la fenêtre racine
  • liste de listes pour créer une grille de rectangles
  • Fonctions, variables, tests, boucles, fondamentaux sur tkinter vus dans les quatre exercices d'introduction.
🐍 Script Python
from tkinter import *


### Constantes

COTE_CASE = 100  # coté d'une case en pixels
NB_LIG = 4  # nombre de lignes
NB_COL = 4  # nombre de colonnes

#%% Modèle / Fonctions
def voisinage(x, y):
    """
    Renvoie la liste des coordonnées des cases voisines
    de la case de coordonnées (x, y)

    Parameters
    ----------
    x : abscisse de type int
    y : ordonnée de type int

    Returns
    -------
    lis : liste des coordonnées des cases voisines

    """
    # à compléter


def clic(event):
    """
    Gestionnaire de l'événement clic avec bouton gauche sur une case

    """
    # à compléter
    # ne pas oublier de mettre à jour la fenêtre
    fen.update()


#%% Vue : interface graphique


# Fenêtre racine
fen = Tk()
fen.title("ALTERNATIF JEU")


# Canevas
# à compléter

# grille de rectangles dessinés dans le canevas
# à compléter

#%% Controleur

fen.bind_all("<ButtonPress-1>", clic)


#%% Boucle infinie , réceptionnaire d'événement
fen.mainloop()

Projet 4 : calculateur d'adresses IP

Niveau de difficulté

Difficulté moyenne mais long, 200 lignes de codes voir plus.

Cahier des charges

Réaliser une interface graphique en tkinter qui présente :

  • deux widgets d'entrée : un champ de saisie d'adresse IP en notation décimale pointée et une échelle de sélection de la longueur du masque réseau
  • plusieurs widgets de sortie de type étiquettes avec l'affichage :
    • de l'adresse du réseau
    • de la première et de la dernière adresse de machine du réseau
    • du nombre d'adresses de machines disponibles dans ce réseau
    • de l'adresse de broadcast (dernière adresse de la plage)

Modèle de l'outil : Calculateur de masque réseau du CRIC de Grenoble

Résultat attendu

alt

  • Codage en base deux et conversion d'une écriture en base dix sous la forme d'un entier à une écriture en base deux sous forme de liste de bits (0 ou 1). Voir le cours sur la représentation des entiers
  • Format des adresses IPV4 : voir le Point de Cours 5 du Cours sur les Réseaux
  • Listes et chaînes de caractères
  • Fonctions, variables, tests, boucles, fondamentaux sur tkinter vus dans les quatre exercices d'introduction.

On propose plusieurs fonctions outils : pour passer de l'écriture décimale d'un entier en base dix à son écriture en base deux sous forme de liste de bits et vice-versa etc ...

🐍 Script Python
from tkinter import *


#%% Modèle / Fonctions 
def dec_to_bits(dec):
    """
    Convertit en binaire un entier

    Parameters
    ----------
    dec :un entier entre 0 et 255

    Returns
    -------
    Représentation binaire de l'entier sous forme de liste de 8 bits

    """
    bits = [0 for i in range(8)]
    # à compléter
    return bits


def test_dec_to_bits():
    assert dec_to_bits(15) == [0, 0, 0, 0, 1, 1, 1, 1]
    assert dec_to_bits(132) == [1, 0, 0, 0, 0, 1, 0, 0]


def bits_to_dec(bits):
    """
    Convertit une liste de bits en décimal

    Parameters
    ----------
    bits : liste de 8 bits

    Returns
    -------
    entier entre 0 et 255

    """
    # à compléter


def test_bits_to_dec():
    assert bits_to_dec([0, 0, 0, 0, 1, 1, 1, 1]) == 15
    assert bits_to_dec([1, 0, 0, 0, 0, 1, 0, 0]) == 132


def ip_to_bits(ip):
    """
    Convertit une adresse ip sous forme de str en liste de 32 bits

    Parameters
    ----------
    ip : adresse ip sous forme de str comme '172.17.232.6'

    Returns
    -------
    adresse ip sous forme de liste de 32 bits

    """
    # à compléter


def test_ip_to_bits():
    assert ip_to_bits("172.17.232.6") == [
        1,
        0,
        1,
        0,
        1,
        1,
        0,
        0,
        0,
        0,
        0,
        1,
        0,
        0,
        0,
        1,
        1,
        1,
        1,
        0,
        1,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        1,
        1,
        0,
    ]


def bits_to_ip(ip_bits):
    """
    Convertit une adresse ip sous forme de str en liste de 32 bits


    Parameters
    ----------
    ip_bits : adresse ip sous forme de liste de 32 bits

    Returns
    -------
    adresse ip sous forme de str comme '172.17.232.6'

    """
    # à compléter


def test_bits_to_ip():
    assert (
        bits_to_ip(
            [
                1,
                0,
                1,
                0,
                1,
                1,
                0,
                0,
                0,
                0,
                0,
                1,
                0,
                0,
                0,
                1,
                1,
                1,
                1,
                0,
                1,
                0,
                0,
                0,
                0,
                0,
                0,
                0,
                0,
                1,
                1,
                0,
            ]
        )
        == "172.17.232.6"
    )


def verifier_ip(ip):
    """
    Vérifie si une adresse ip sous forme de  str comme '172.17.232.6'
    est valide

    Parameters
    ----------
    ip : adresse ip sous forme de str comme '172.17.232.6'
    Returns
    -------
    booléen
    """
    # à compléter


def test_verifier_ip():
    assert verifier_ip("172.17.232.6") == True
    assert verifier_ip("172.17.256.6") == False
    assert verifier_ip("172.17.256") == False
    assert verifier_ip("172.17.256") == False


def prefixe_reseau(ip_bits, mask):
    """
    Renvoie le préfixe réseau d'une adresse IP sous forme de liste de 32 bits
    à partir de la longueur du masque réseau

    Parameters
    ----------
    ip_bits : liste de bits (0 ou 1)
    masque : entier entre 1 et 31 bits

    Returns
    -------
    Renvoie une liste de 32 bits

    """
    prefixe = [0 for i in range(32)]
    ip_masque = [1 for i in range(mask)] + [0 for j in range(32 - mask)]
    # à compléter


def test_prefixe_reseau():
    assert prefixe_reseau(
        [
            1,
            0,
            1,
            0,
            1,
            1,
            0,
            0,
            0,
            0,
            0,
            1,
            0,
            0,
            0,
            1,
            1,
            1,
            1,
            0,
            1,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            1,
            1,
            0,
        ],
        21,
    ) == [
        1,
        0,
        1,
        0,
        1,
        1,
        0,
        0,
        0,
        0,
        0,
        1,
        0,
        0,
        0,
        1,
        1,
        1,
        1,
        0,
        1,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
    ]


def calcul_ip():
    """Fonction de rappel du bouton bouton_calcul
    Récupère l'adresse IP  de la variable de contrôle  adresse_ip
    Vérifie si cette adresse est valide avec verifier_ip
    Si elle est valide, convertit l'adresse en liste de 32 bits,
    puis calcule le préfixe réseau, la première adresse, la dernière adresse
    et l'adresse de broadcast (dernière adresse de la plage)
    Modifie pour cela les variables de contrôles liées aux étiquettes
    d'affichage de ces différents éléments
    """
    ip = adresse_ip.get()
    if not verifier_ip(ip):
        message_erreur.set("Adresse non conforme")
    else:
        message_erreur.set("")
        ip_bits = ip_to_bits(ip)
        adresse = prefixe_reseau(ip_bits, masque.get())
        adresse_reseau.set("Adresse  réseau : " + bits_to_ip(adresse))
        adresse[31] = 1
        premiere_adresse.set("Première adresse : " + bits_to_ip(adresse))
        # à compléter


#%% Vue : interface graphique


# Fenêtre racine
fen = Tk()
fen.title("Calculateur d'adresses IP")
fen.geometry("600x400")

### Variables de contrôles
adresse_ip = StringVar()
premiere_adresse = StringVar()
premiere_adresse.set("Première adresse : ")
adresse_reseau = StringVar()
adresse_reseau.set("Adresse réseau : ")
derniere_adresse = StringVar()
derniere_adresse.set("Dernière adresse : ")
adresse_broadcast = StringVar()
adresse_broadcast.set("Adresse broadcast : ")
message_erreur = StringVar()
nombre_adresses_ip = StringVar()
nombre_adresses_ip.set("Nombre d'adresses IP : ")
masque = IntVar()

# Cadre adresse
cadre_adresse = Frame(fen)
etiq_adresse = Label(cadre_adresse, text="Adresse IP : ", font=("Arial", 20))
saisie_adresse = Entry(
    cadre_adresse, textvariable=adresse_ip, font=("Arial", 20)
)
etiq_adresse.pack(side=LEFT)
saisie_adresse.pack(side=LEFT)
cadre_adresse.pack()

# Cadre masque
cadre_masque = Frame(fen)
# à compléter

# Cadre calcul
cadre_calcul = Frame(fen)
# à compléter

#%% Boucle infinie , réceptionnaire d'événement
fen.mainloop()

Projet 5 : jeu de pendu

Niveau de difficulté

Difficile, 200 lignes de codes voir plus.

Cahier des charges

Réaliser une interface graphique en tkinter pour un jeu de pendu avec :

  • un alphabet de 26 lettres cliquables, chaque lettre déjà sélectionnée s'affichant en rouge
  • un bouton pour choisir un nouveau mot secret (dans le fichier dico_pendu.txt que vous téléchargerez)
  • un affichage des lettres du mot secret sous la forme de rectangles qui font apparaître les lettres découvertes
  • un champ de saisie du mot complet proposé par le joueur et en dessous un bouton pour valider sa saisie
  • une étiquette d'affichage des messages : nombre d'esssais restants (10 au départ), résultat et révélation du mot en fin de partie

Résultat attendu

alt

  • Listes et chaînes de caractères
  • Fonctions, variables, tests, boucles, fondamentaux sur tkinter vus dans les quatre exercices d'introduction.
🐍 Script Python
from tkinter import *
import random

#%% Constantes
# Récupération de la listes des mots
f = open("dico_pendu.txt")
LISTE_MOTS = [ligne.rstrip() for ligne in f]
f.close()

#%% Variables globales
mot_secret = (
    "SECRET"  # valeur initiale du mot secret au lancement de l'interface
)
nb_essais = 0  # nombre d'essais
jouer = False  # un jeu est en cours

#%% Modèle / Fonctions
def clic(event):
    """Gestionnaire de clic sur une lettre de l'alphabet"""
    global selection, nb_essais, nb_carac_restants, jouer

    if jouer:
        x = (
            event.x
        ) // 50  # si on considère qu'on trace les lettres dans des carrés de coté 50 pixels (à voir)
        y = (event.y) // 50
        # à compléter


def choix_mot():
    """Fonction de rappel pour le bouton nouveau_mot"""
    global mot_secret, lettre_mot, jouer, nb_carac_restants, nb_essais
    jouer = True
    can2.delete("all")
    can1.delete("all")
    generer_alphabet()
    # à compléter
    # on n'oublie pas de rafraichir la fenêtre
    fen.update()


def valider_proposition():
    """Fonction de rappel pour le bouton bouton_proposition"""
    global jouer, proposition_mot, nb_essais
    # à compléter


#%% Vue : interface graphique


# Fenêtre racine
fen = Tk()
fen.title("Pendu")
fen.geometry("1000x900")

# Consigne
consigne = Label(
    fen,
    text="Choisissez une lettre en cliquant dessus",
    font=("Arial Bold", 30),
    pady=10,
)
consigne.pack()

# Cadre pour l'alphabet

cadre_alphabet = Frame(fen)
can1 = Canvas(cadre_alphabet)


def generer_alphabet():
    """Dessine un nouvel alphabet dans le canevas can1"""
    global alphabet, selection
    # liste de listes des lettres de l'alphabet qui seront dessinées dans des rectangles
    alphabet = [[None for x in range(7)] for y in range(4)]
    # liste de listes de boolennes indiquant pour chaque lettre si elle a été sélectionnée
    selection = [[False for x in range(7)] for y in range(4)]
    # à compléter


generer_alphabet()
can1.pack()
cadre_alphabet.pack()

# Cadre pour le mot à deviner

cadre_mot = Frame(fen)
etiq_mot = Label(
    cadre_mot, text="Mot à deviner", font=("Arial Bold", 30), pady=10
)
nouveau_mot = Button(
    cadre_mot, text="Nouveau mot", font=("Arial Bold", 30), command=choix_mot
)

# les cases des lettres du mot à deviner seront dessinées dans le Canevas can2
# après appel de la fonction choix_mot

can2 = Canvas(cadre_mot, height=200)

# à compléter
# Etiquette du champ de saisie saisie_mot
# Champe d saisie saisie_mot
# Bouton bouton_proposition

# Essais
msg_essais = StringVar()
# à compléter
# Etiquette de message au joueur

#%%Controleurs
can1.bind("<ButtonPress-1>", clic)


#%% Boucle infinie , réceptionnaire d'événement
fen.mainloop()

Projet 6 : jeu de la vie, automate cellulaire en deux dimensions

Niveau de difficulté

Moyennement difficile, entre 150 et 200 lignes de code.

Cahier des charges

Réaliser une interface graphique en tkinter pour un jeu de la vie, automate cellulaire en deux dimensions.

  • une grille de cases blanches délimitées pas un bord rouge : chaque case représente une cellule, morte si blanche et vivante si noire
  • la possibilité d'initialiser une configuration en cliquant sur chaque case pour changer sa couleur de blanc à noir ou vice versa
  • un bouton pour lancer le jeu et l'affichage des générations successives par application des trois règles ci-dessous du jeu de la vie. Lorsque le jeu est lancé il n'est plus possible de changer la couleur des cases.
  • un bouton pour arrêter le jeu en cours

Jeu de la vie

Le jeu de la vie inventé par le mathématicien anglais J. H. Conway en 1970, est un automate cellulaire en deux dimensions.

Les automates cellulaires et le jeu de la vie en particulier sont des systèmes complexes étudiés en mathématiques et en informatique théorique. Ainsi, l'automate de Conway a été prouvé turing-complet c'est-à-dire qu'il peut exécuter les mêmes algorithmes qu'un ordinateur.

L'univers du jeu est une grille infinie de cases appelées cellules, en pratique on utilise une grille finie dont les bords se rejoignent (haut <-> bas et gauche <-> droite) : il suffit d'effectuer les calculs de voisinage modulo le nombre de lignes ou de colonnes.

Au départ un nombre fini de cellules sont vivantes et toutes les autres sont mortes.

Chaque cellule possède un voisinage de 8 cellules voisines (sauf les cellules sur les bords dans notre simulation).

voisinage

A chaque génération, l'univers évolue selon trois règles simples de changement d'état pour chaque cellule :

  1. Règle 1 : Une cellule vivante qui n'a pas au moins 2 voisines vivantes meurt par isolement.
  2. Règle 2 : Une cellule vivante qui possède 4 voisines vivantes ou plus meurt par étouffement.
  3. Règle 3 : Une cellule morte qui possède exactement trois cellules vivantes, devient vivante, sinon elle reste morte.

Résultat attendu

La configuration iniitiale est celle d'un planeur.

alt

  • Listes de listes et chaînes de caractères
  • Fonctions, variables, tests, boucles, fondamentaux sur tkinter vus dans les quatre exercices d'introduction.
🐍 Script Python
from tkinter import *
import time

# CONSTANTES
COTE_CASE = 15
NB_LIG = 50
NB_COL = 50
VIVANTE = "#000000"
MORTE = "#ffffff"
VOISINAGE = [
    (-1, 0),
    (-1, -1),
    (-1, 1),
    (0, -1),
    (0, 1),
    (1, -1),
    (1, 0),
    (1, 1),
]

# Variables globales
jouer = False
generation = 0

## Modèle / Foctions


def clic(event):
    """Gestionnaire de clic avec le bouton gauche de la souris
    sur une case"""
    if not jouer:

        x = event.x // COTE_CASE
        y = event.y // COTE_CASE
        # à compléter
        # on n'oublie pas de mettre à jour la fenêtre
        fen.update()


def nb_voisins_vivants(x, y):
    """
    Renvoie le nombe de voisins vivants de la case
    de coordonnées (x, y)

    """
    # à compléter


def vie():
    """
    Permet de changer la variable globale jouer
    avant de lancer le jeu de la vie
    """
    global jouer
    jouer = True
    jeu_vie()


def jeu_vie():
    """
    Lance un jeu de la vie à partir de la configuration de la grille
    """
    global generation
    while jouer:
        generation = generation + 1
        # à compléter

        # on n'oublie pas de mettre à jour la fenêtre
        fen.update()
        # on attend 0.1 secondes entre chaque génération
        time.sleep(0.1)


def mort():
    global jouer
    jouer = False


### Vue : interface graphique


# Fenêtre racine
fen = Tk()
fen.title("JEU DE LA VIE")

# Canevas de dessin de la grille
can = Canvas(fen, width=COTE_CASE * NB_COL, height=COTE_CASE * NB_LIG)
can.pack()
# à compléter


### Controleur

can.bind("<ButtonPress-1>", clic)


### Boucle infinie , réceptionnaire d'événement
fen.mainloop()

Projet 7 : automate cellulaire à une dimension

Niveau de difficulté

Difficile, entre 150 et 200 lignes de code.

Cahier des charges

Réaliser une interface graphique en tkinter pour un automate cellulaire à une dimension.

  • une grille de cases blanches délimitées pas un bord rouge : chaque case représente une cellule, morte si blanche et vivante si noire
  • la possibilité d'initialiser une configuration en cliquant sur chaque case de la première ligne pour changer sa couleur de blanc à noir ou vice versa
  • un champ de saisie du numéro de la règle
  • un bouton pour lancer le jeu et l'affichage des générations successives sur les lignes suivantes
  • un bouton pour réinitialiser le jeu en cours : il efface toutes les cases noires

Automate cellulaire à une dimension

Prenons la règle numéro 90. On commence par écrire 90 en base deux sur 8 bits

Poids \(2^{7}\) Poids \(2^{6}\) Poids \(2^{5}\) Poids \(2^{4}\) Poids \(2^{3}\) Poids \(2^{2}\) Poids \(2^{1}\) Poids \(2^{0}\)
0 1 0 1 1 0 1 0

Ensuite, considérons une case de la première ligne. Elle peut être vivante (codage 1) ou morte (codage 0) et de même pour sa voisine de gauche (on repart de l'autre bout avec un modulo pour la case la plus à gauche) ou de droite (on repart au début pour la case la plus à droite). Il existe donc \(2^{3} = 8\) triplets de 0 ou 1 représentant tous les états possibles de ce voisinage de trois cases (gauche, case considérée, droite). Par exemple si (gauche, case considérée, droite) = (1 ,1 0), vu comme une écriture en base deux cela donne \(1 \times 2^{2}+1 \times 2^{1}+ 0 \times 2^{0}= 6\). Le coefficient du poids de \(2^{6}\) dans l'écriture binaire du numéro de règle \(90\) est \(1\). Ce sera l'état de la cellule en dessous de la case considérée. On procède de même pour chaque valeur du triplet (gauche, case considérée, droite) : on le regarde comme une écriture binaire d'un entier \(k\) entre 0 et 7, on lit la valeur du coefficient de \(2^{k}\) dans l'écriture binaire du numéro de la règle et on obtient l'état de la case du dessous. Tout repose sur le codage binaire !

Résultat attendu

Exemple des premières générations pour la règle numéro 90.

alt

  • Listes de listes et chaînes de caractères
  • Fonctions, variables, tests, boucles, fondamentaux sur tkinter vus dans les quatre exercices d'introduction.
🐍 Script Python
from tkinter import *
import time


#%% Constantes et variables globales

# CONSTANTES
COTE_CASE = 20
NB_LIG = 31
NB_COL = 31
VIVANTE = "#000000"
MORTE = "#ffffff"


# Variables globales
action = False


#%% Modèle / Fonctions
def dec_to_bits(n):
    """
    Convertit en binaire un entier

    Parameters
    ----------
    dec :un entier entre 0 et 255

    Returns
    -------
    Représentation binaire de l'entier sous forme de liste de 8 bits
    """
    # précondition
    assert isinstance(n, int) and 0 <= n < 256
    t = [0 for _ in range(8)]
    # à compléter
    return t


def test_dec_to_bits():
    assert dec_to_bits(15) == [0, 0, 0, 0, 1, 1, 1, 1]
    assert dec_to_bits(132) == [1, 0, 0, 0, 0, 1, 0, 0]


def generer_table(regle):
    """
    Renvoie la table de correspondance pour un numéro de règle entre 0
    et 255. C'est la liste des bits de l'écriture binaire du numéro
    """
    return dec_to_bits(regle)


def cellule_dessous(gauche, milieu, droite, table):
    """
    Renvoie l'état 1 pour vivant ou 0 pour mort de la cellule en dessous
    de la cellule du milieu en fonction des états (1 ou 0) de la cellule
    du milieu, de sa voisine de gauche et de celle de droite
    """
    # à compléter


#%% Modèle


def clic(event):
    """Gestionnaire de clic avec le bouton gauche sur une case
    de la première ligne (lesautres cases ne peuvent être sélectionnées)"""
    # à compléter


def vie():
    """Met à jour la variable globale action avant de lancer l'automate"""
    global action
    action = True
    automate()


def automate():
    """
    Lance l'automate et génère les lignes suivantes
    à la vitesse de 1 ligne par seconde
    """
    # à compléter


def reset():
    """Met à jour la variable globale action avant de lancer l'automate"""
    global action
    action = False
    can.delete(ALL)
    # à compléter : il faut redessiner toutes les cases blanches à bords rouges


#%% Vue : interface graphique
# Fenêtre racine
fen = Tk()
fen.title("AUTOMATE CELLULAIRE")
# Canevas
can = Canvas(fen, width=COTE_CASE * NB_COL, height=COTE_CASE * NB_LIG)
can.pack()
# à compléter

# Cadre de commande
cadre = Frame(fen)
cadre.pack()
# Bouton de lancement de l'automate
# à compléter
# Bouton de réinitialisation (reset)
# à compléter
# Etiquette de règle
etiq_regle = Label(cadre, text="Numéro de règle :")
etiq_regle.pack()
# Champ de saisie de règle
regle = StringVar()
saisie_regle = Entry(cadre, textvariable=regle)
saisie_regle.pack()

#%% Controleur
can.bind("<ButtonPress-1>", clic)


#%% Boucle infinie , réceptionnaire d'événement
fen.mainloop()

Projet 8 : jeu du recto-verso

Niveau de difficulté

Difficulté moyenne, entre 100 et 150 lignes de code.

Cahier des charges

Réaliser une interface graphique en tkinter pour jouer au jeu Recto-Verso.

Le jeu se compose de neuf jetons disposés sur un plateau carré de trois lignes et trois colonnes. Chaque jeton possède deux faces :

  • une face Recto, représentée en noir ;
  • une face Verso, représentée en blanc.

Le but du jeu est de placer tous les jetons sur leur face Recto. À chaque tour, le joueur choisit une ligne, une colonne ou une diagonale, puis retourne simultanément les trois jetons correspondants.

L'interface doit comporter :

  • une grille de neuf jetons dessinés dans un canevas ;
  • huit boutons radio permettant de sélectionner l'une des trois lignes, l'une des trois colonnes ou l'une des deux diagonales ;
  • un bouton Action ! qui retourne les trois jetons sélectionnés et met à jour l'affichage.
  • un bouton Nouveau jeu qui génère une nouvelle configuration pouvant être résolue.

Représentation du plateau

L'état du plateau est représenté par une liste de neuf bits, lue ligne par ligne :

Position dans la grille 0 1 2 3 4 5 6 7 8
Coordonnées (ligne, colonne) (0, 0) (0, 1) (0, 2) (1, 0) (1, 1) (1, 2) (2, 0) (2, 1) (2, 2)

Le bit 0 représente une face noire et le bit 1 une face blanche. Ainsi, la liste

🐍 Script Python
[0, 1, 0, 1, 0, 0, 0, 1, 1]

représente le plateau suivant :

noir blanc noir
blanc noir noir
noir blanc blanc

Retourner un jeton avec le XOR

Une action est également représentée par une liste de neuf bits. Un 1 indique que le jeton doit être retourné et un 0 qu'il doit rester inchangé.

Pour retourner un jeton, on applique l'opération logique XOR entre son état et le bit de l'action :

a b a XOR b
0 0 0
0 1 1
1 0 1
1 1 0

La première ligne est donc représentée par l'action :

🐍 Script Python
[1, 1, 1, 0, 0, 0, 0, 0, 0]

Quelques questions pour préparer la réalisation

  1. Quel est l'indice, dans la liste représentant le plateau, de la case située à la ligne y et à la colonne x ?
  2. Écrire les listes de neuf bits qui représentent la deuxième diagonale et la troisième colonne.
  3. Vérifier, à l'aide de la table du XOR, qu'appliquer deux fois la même action ramène le plateau dans son état initial.
  4. Dans quel ordre faut-il modifier le modèle du jeu et actualiser son affichage ?
  5. Pour générer une grille dont on est certain qu'elle possède une solution, pourquoi peut-on partir de la grille noire et lui appliquer plusieurs actions choisies au hasard ?

Résultat attendu

alt

  • Listes et listes de listes.
  • Parcours d'une liste par indice et construction d'une liste par compréhension.
  • Opérateurs logiques et tables de vérité.
  • Fonctions, variables, tests, boucles et fondamentaux sur tkinter vus dans les exercices d'introduction.
  • Séparation entre le modèle, qui contient l'état du jeu, et la vue, qui assure son affichage.
🐍 Script Python
from tkinter import *
import random


#%% Constantes

COTE_CASE = 100
NB_LIGNES = 3
NB_COLONNES = 3


#%% Variables globales et modèle

# Chaque sous-liste indique les jetons retournés par une action.
liste_action = [
    [1, 1, 1, 0, 0, 0, 0, 0, 0],  # ligne 1
    [0, 0, 0, 1, 1, 1, 0, 0, 0],  # ligne 2
    [0, 0, 0, 0, 0, 0, 1, 1, 1],  # ligne 3
    [1, 0, 0, 1, 0, 0, 1, 0, 0],  # colonne 1
    [0, 1, 0, 0, 1, 0, 0, 1, 0],  # colonne 2
    [0, 0, 1, 0, 0, 1, 0, 0, 1],  # colonne 3
    [1, 0, 0, 0, 1, 0, 0, 0, 1],  # diagonale 1
    [0, 0, 1, 0, 1, 0, 1, 0, 0],  # diagonale 2
]

# Au départ, les neuf jetons présentent leur face noire.
grille_modele = [0 for _ in range(9)]


#%% Modèle / Fonctions

def xor(a, b):
    """Renvoie le XOR des bits a et b."""
    # à compléter


def test_xor():
    assert xor(0, 0) == 0
    assert xor(0, 1) == 1
    assert xor(1, 0) == 1
    assert xor(1, 1) == 0


def xor_liste(liste1, liste2):
    """Renvoie le XOR terme à terme de deux listes de bits."""
    # à compléter
    ...

def test_xor_liste():
    assert xor_liste([0, 1, 1], [1, 1, 0]) == [1, 0, 1]
    assert xor_liste([1, 0, 1], [1, 0, 1]) == [0, 0, 0]


def maj_modele():
    """Applique au plateau l'action sélectionnée."""
    global grille_modele
    # Récupérer l'indice de l'action sélectionnée.
    # Appliquer cette action à la grille avec xor_liste.
    # à compléter


def maj_vue():
    """Actualise la couleur des rectangles à partir du modèle."""
    # Parcourir les neuf cases de la grille.
    # Colorier chaque rectangle en noir ou en blanc selon
    # la valeur correspondante dans grille_modele.
    # à compléter
    


def maj():
    """Met à jour le modèle, puis la vue."""
    # à compléter


def nouveau_jeu():
    """Génère une configuration aléatoire qui possède une solution."""
    global grille_modele
    # Repartir de la grille entièrement noire, puis appliquer
    # au hasard certaines des huit actions de liste_action.
    # Recommencer si la grille obtenue est entièrement noire.
    # Enfin, actualiser la vue.
    # à compléter


#%% Vue : interface graphique

# Fenêtre racine
fen = Tk()
fen.title("RECTO-VERSO")

# Cadre contenant les commandes
cadre_action = Frame(fen)
cadre_action.pack()

# L'entier mémorisé désigne l'action sélectionnée :
# 0, 1, 2 pour les lignes ; 3, 4, 5 pour les colonnes ;
# 6 et 7 pour les diagonales.
index_action = IntVar(value=0)

ligne1 = Radiobutton(
    cadre_action,
    text="Ligne 1",
    value=0,
    variable=index_action,
)
ligne1.pack()

# Créer et placer les sept autres boutons radio.
# à compléter


# Canevas contenant la grille de jeu
can = Canvas(
    fen,
    width=COTE_CASE * NB_COLONNES,
    height=COTE_CASE * NB_LIGNES,
)
can.pack()

# grille_vue contient les identifiants des neuf rectangles.
grille_vue = [
    [
        can.create_rectangle(
            x * COTE_CASE,
            y * COTE_CASE,
            (x + 1) * COTE_CASE,
            (y + 1) * COTE_CASE,
            outline="red",
            fill="black",
        )
        for x in range(NB_COLONNES)
    ]
    for y in range(NB_LIGNES)
]


#%% Controleurs 

# Bouton qui déclenche l'action sélectionnée
bouton_action = Button(cadre_action, text="Action !", command=maj)
bouton_action.pack()

# Bouton qui génère une nouvelle grille à résoudre
# à compléter avec une bouton qui génère une nouvelle grille

#%% Boucle infinie, réceptionnaire d'événements

fen.mainloop()

Conseil de réalisation

Commencez par compléter et tester les fonctions xor et xor_liste avant de construire la mise à jour graphique. Une grille formée de neuf bits choisis indépendamment au hasard ne possède pas nécessairement une solution. Pour le bouton Nouveau jeu, partez donc du plateau entièrement noir et appliquez-lui des actions légales choisies au hasard.

Prolongement facultatif

Ajouter une étiquette qui affiche le nombre de coups joués et un message lorsque les neuf jetons sont noirs.

Projet 9 : jeu du morpion

Niveau de difficulté

Difficulté moyenne, entre 100 et 150 lignes de code.

Cahier des charges

Réaliser une interface graphique en tkinter permettant à deux joueurs de jouer au morpion sur une grille de \(3 \times 3\) cases.

Les joueurs jouent à tour de rôle :

  • le joueur 1 colorie ses cases en rouge ;
  • le joueur 2 colorie ses cases en bleu ;
  • un clic sur une case déjà occupée ne produit aucun effet ;
  • le premier joueur qui aligne trois cases horizontalement, verticalement ou en diagonale remporte la partie ;
  • si les neuf cases sont occupées sans alignement, la partie est déclarée nulle ;
  • après la victoire d'un joueur ou un match nul, aucun nouveau coup ne peut être joué ;
  • un bouton Nouvelle partie permet de vider la grille et de recommencer.

Une étiquette placée au-dessus de la grille indique le joueur dont c'est le tour, puis le résultat de la partie.

Représentation de la grille

La grille du jeu est représentée par une liste de trois listes :

🐍 Script Python
grille_modele = [
    [0, 0, 0],
    [0, 0, 0],
    [0, 0, 0],
]

La valeur 0 représente une case vide, 1 une case occupée par le joueur 1 et 2 une case occupée par le joueur 2.

Une seconde liste de listes, grille_vue, contient les identifiants des neuf rectangles dessinés dans le canevas. Elle permet de modifier leur couleur avec la méthode itemconfig.

Quelques questions préparatoires

  1. Un clic a pour coordonnées en pixels (event.x, event.y). Comment calculer la ligne et la colonne de la case sélectionnée ?
  2. La variable joueur vaut toujours 1 ou 2. Vérifier que l'instruction joueur = 3 - joueur permet de changer de joueur.
  3. Depuis la dernière case jouée, dans quelles quatre directions suffit-il de rechercher un alignement ?
  4. Pourquoi faut-il tester la victoire avant de tester si toutes les cases sont occupées ?

Résultat attendu

alt

  • listes et listes de listes ;
  • fonctions, variables globales, tests et boucles ;
  • quotient entier // ;
  • fondamentaux sur tkinter vus dans les exercices d'introduction ;
  • parcours d'une grille et vérification qu'un indice appartient à ses limites.
🐍 Script Python
from tkinter import *


#%% Constantes

COTE_CASE = 100
NB_LIGNES = 3
NB_COLONNES = 3

VIDE = 0
JOUEUR1 = 1
JOUEUR2 = 2

COULEUR = ["white", "red", "blue"]

# Les quatre directions possibles d'un alignement :
# verticale, horizontale et les deux diagonales.
DIRECTIONS = [[1, 0], [0, 1], [1, 1], [1, -1]]


#%% Variables globales et modèle

joueur = JOUEUR1
nb_cases_libres = 9
partie_terminee = False

grille_modele = [
    [VIDE for colonne in range(NB_COLONNES)]
    for ligne in range(NB_LIGNES)
]


#%% Modèle / Fonctions

def coup_gagnant(ligne, colonne):
    """Renvoie True si le dernier coup forme un alignement
    de trois cases, et False sinon.

    Pour chaque direction, on examine les deux sens à partir
    de la dernière case jouée.
    """
    for dligne, dcolonne in DIRECTIONS:
        nb_cases_joueur = 1

        for sens in [-1, 1]:
            # Calculer le déplacement dans le sens choisi.
            # à compléter

            # Coordonnées de la première case à examiner.
            # à compléter

            # Avancer tant que la case existe et appartient
            # au joueur courant.
            while False:  # condition à remplacer
                # à compléter
                pass

        # Tester si trois cases sont alignées.
        # à compléter

    return False


def maj_modele(ligne, colonne):
    """Joue un coup puis met à jour l'état de la partie."""
    global joueur, nb_cases_libres, partie_terminee

    # Placer le coup du joueur dans grille_modele et diminuer
    # le nombre de cases libres.
    # à compléter

    # Traiter dans cet ordre : victoire, match nul,
    # puis changement de joueur.
    # Mettre à jour etat_jeu dans chaque cas.
    # à compléter


def maj_vue():
    """Actualise la couleur des neuf rectangles."""
    for ligne in range(NB_LIGNES):
        for colonne in range(NB_COLONNES):
            # Modifier la couleur du rectangle correspondant
            # à grille_modele[ligne][colonne].
            # à compléter
            pass


def clic(event):
    """Gestionnaire d'un clic gauche dans le canevas."""
    # Convertir les coordonnées en pixels en indices de case.
    ligne = 0       # à compléter
    colonne = 0     # à compléter

    # Jouer uniquement si la partie continue et si la case
    # sélectionnée est vide.
    # à compléter


def reinitialiser():
    """Replace le jeu dans son état initial."""
    global joueur, nb_cases_libres, partie_terminee

    # Vider les neuf cases de grille_modele.
    # à compléter

    # Réinitialiser les trois variables globales et le message.
    # à compléter

    maj_vue()


#%% Vue : interface graphique

# Fenêtre racine
fen = Tk()
fen.title("Morpion")

# État du jeu
etat_jeu = StringVar()
etat_jeu.set("Joueur " + str(joueur))
label_etat = Label(fen, textvariable=etat_jeu)
label_etat.pack()

# Canevas
can = Canvas(
    fen,
    width=COTE_CASE * NB_COLONNES,
    height=COTE_CASE * NB_LIGNES,
)
can.pack()

# Grille de rectangles dessinés dans le canevas
grille_vue = [
    [
        can.create_rectangle(
            colonne * COTE_CASE,
            ligne * COTE_CASE,
            (colonne + 1) * COTE_CASE,
            (ligne + 1) * COTE_CASE,
            outline="black",
            fill=COULEUR[VIDE],
        )
        for colonne in range(NB_COLONNES)
    ]
    for ligne in range(NB_LIGNES)
]

#%% Contrôleur

# Bouton de réinitialisation
# à compléter avec un bouton pour réinitialiser une partie


# gestion du clic droit de la souris
can.bind("<ButtonPress-1>", clic)


#%% Boucle de réception des événements

fen.mainloop()

Projet 10 : sokoban (jeu)

Niveau de difficulté

Difficulté moyenne, entre 130 et 180 lignes de code.

Cahier des charges

Réaliser une version simplifiée du jeu Sokoban avec une interface graphique en tkinter.

Le joueur se déplace dans un entrepôt à l'aide des quatre touches fléchées du clavier. Il doit pousser toutes les caisses sur les emplacements prévus.

Les règles sont les suivantes :

  • le joueur peut se déplacer sur une case libre ou sur un emplacement ;
  • il ne peut pas traverser un mur ;
  • il peut pousser une caisse uniquement si la case située derrière elle est libre ;
  • il ne peut pas tirer une caisse ;
  • la partie est gagnée lorsque chaque emplacement contient une caisse ;
  • un bouton Nouvelle partie permet de replacer le joueur et les caisses dans leur position initiale.

La carte du jeu est enregistrée dans le fichier sokoplan1.xsb, placé dans le même dossier que le programme Python. Chaque caractère du fichier représente le contenu d'une case :

Caractère Élément Couleur
espace case libre blanc
# mur noir
. emplacement d'une caisse vert
$ caisse rouge
@ personnage bleu

Le plan est chargé dans une liste de listes appelée GRILLE. Par exemple, GRILLE[2][5] contient le symbole de la case située sur la ligne 2 et la colonne 5.

Le programme utilisera deux grilles :

  • grille_modele contient les symboles décrivant l'état courant du jeu ;
  • grille_vue contient les identifiants des rectangles dessinés dans le Canvas.

Modèle et vue

Lors d'un déplacement, on modifie d'abord grille_modele. La fonction maj_vue() parcourt ensuite cette grille et actualise la couleur des rectangles de grille_vue.

La liste LIS_EMPLACEMENT mémorise séparément les coordonnées des emplacements. Elle permet de retrouver un emplacement lorsqu'un personnage ou une caisse quitte cette case.

Quelques questions pour préparer la réalisation⚓︎

  1. Ouvrir le fichier sokoplan1.xsb (cliquer sur le lien pour le télécharger) dans un éditeur de texte. Déterminer le nombre de lignes et de colonnes du plan, puis repérer les coordonnées initiales du personnage.
  2. Si le personnage est en (lig_perso, col_perso), quelles sont les coordonnées de la case voisine après un déplacement décrit par (dlig, dcol) ? Donner les quatre couples associés aux touches fléchées.
  3. Lorsqu'une caisse occupe cette case voisine, quelles coordonnées faut-il tester pour savoir si elle peut être poussée ?
  4. Pourquoi faut-il restaurer le symbole ".", et non une espace, lorsque le personnage quitte une case appartenant à LIS_EMPLACEMENT ?
  5. Expliquer comment vérifier que toutes les caisses sont placées sur les emplacements sans comparer toute la grille à une grille finale.

Résultat attendu

alt

  • lecture d'un fichier texte ligne par ligne ;
  • chaînes de caractères et listes de listes ;
  • parcours d'une grille avec deux boucles imbriquées ;
  • fonctions, variables, tests et boucles ;
  • fondamentaux de tkinter vus dans le TP d'introduction ;
  • coordonnées d'un rectangle dans un Canvas.

Le fichier sokoplan1.xsb (cliquer sur le lien pour le télécharger) doit être placé dans le même dossier que ce programme.

🐍 Script Python
from tkinter import *


# %% Constantes et chargement du plan

CHEMIN_PLAN = "sokoplan1.xsb"

fichier = open(CHEMIN_PLAN, encoding="utf-8")
GRILLE = [list(ligne.rstrip("\n")) for ligne in fichier]
fichier.close()

NBLIG = len(GRILLE)
NBCOL = len(GRILLE[0])
COTE_CASE = 25

LIBRE = "white"
MUR = "black"
CAISSE = "red"
EMPLACEMENT = "green"
PERSO = "blue"


# Repérage des emplacements et de la position initiale du personnage
LIS_EMPLACEMENT = []

for lig in range(NBLIG):
    for col in range(NBCOL):
        # à compléter
        pass


# %% Modèle / Fonctions

def couleur_case(symb):
    """Renvoie la couleur associée au symbole symb."""
    # à compléter
    pass


def case_libre(lig, col):
    """Indique si la case (lig, col) est libre."""
    # Une case libre peut être une case blanche ou un emplacement.
    # à compléter
    pass


def case_avec_caisse(lig, col):
    """Indique si la case (lig, col) contient une caisse."""
    # à compléter
    pass


def fin_partie():
    """Indique si chaque emplacement contient une caisse."""
    # à compléter
    pass


# %% Mise à jour de la vue

def maj_vue():
    """Actualise la couleur de tous les rectangles du Canvas."""
    for lig in range(NBLIG):
        for col in range(NBCOL):
            # à compléter avec can.itemconfig(...)
            pass


# %% Gestion des déplacements

def deplacement(event):
    """Gère l'appui sur une touche fléchée du clavier."""
    global lig_perso, col_perso

    # 1. Associer à la touche un déplacement (dlig, dcol).
    if event.keysym == "Up":
        dlig, dcol = -1, 0
    elif event.keysym == "Down":
        # à compléter
        pass
    # à compléter

    # 2. Calculer les coordonnées de la case voisine.
    nouvelle_lig = lig_perso + dlig
    nouvelle_col = col_perso + dcol

    # 3. Traiter le déplacement vers une case libre.
    if case_libre(nouvelle_lig, nouvelle_col):
        # Restaurer la case que le personnage quitte.
        # Mettre à jour sa position puis la nouvelle case.
        # à compléter
        pass

    # 4. Traiter le cas d'une caisse qui peut être poussée.
    elif case_avec_caisse(nouvelle_lig, nouvelle_col):
        # Calculer la case située derrière la caisse.
        # Si elle est libre, déplacer le personnage et la caisse.
        # à compléter
        pass

    # 5. Actualiser l'affichage et tester la victoire.
    maj_vue()
    if fin_partie():
        etat_jeu.set("Fin de partie")


def nouvelle_partie():
    """Restaure le plan et les positions initiales."""
    global grille_modele, lig_perso, col_perso

    # Construire une copie de GRILLE et non un simple alias.
    # à compléter
    grille_modele = []

    lig_perso = LIG_PERSO_INIT
    col_perso = COL_PERSO_INIT
    etat_jeu.set("Partie en cours")
    maj_vue()


# %% Vue : interface graphique

fen = Tk()
fen.title("Sokoban")

etat_jeu = StringVar(value="Partie en cours")
etiquette_etat = Label(fen, textvariable=etat_jeu)
etiquette_etat.pack()

can = Canvas(
    fen,
    width=NBCOL * COTE_CASE,
    height=NBLIG * COTE_CASE,
)
can.pack()

# Chaque élément de grille_vue doit être l'identifiant d'un rectangle.
grille_vue = []
for lig in range(NBLIG):
    ligne_vue = []
    for col in range(NBCOL):
        identifiant = can.create_rectangle(
            col * COTE_CASE,
            lig * COTE_CASE,
            (col + 1) * COTE_CASE,
            (lig + 1) * COTE_CASE,
            outline="black",
            fill=couleur_case(GRILLE[lig][col]),
        )
        ligne_vue.append(identifiant)
    grille_vue.append(ligne_vue)

# %% Contrôleur

# à compléter avec un bouton pour lancer une nouvelle partie
...

# Gestion du déplacement du personnage (rectangle bleu)
fen.bind("<KeyPress>", deplacement)


# %% Lancement de la première partie

nouvelle_partie()


# %% Boucle de réception des événements

fen.mainloop()

Ordre de réalisation conseillé

  1. Charger le plan et afficher correctement la grille.
  2. Programmer le déplacement du personnage sur une case libre.
  3. Empêcher le passage à travers les murs.
  4. Ajouter la poussée d'une caisse lorsque la case suivante est libre.
  5. Détecter la victoire et programmer le bouton Nouvelle partie.