21 - Tkinter
Ouverture hors programme. Le module
tkintern’est pas au programme de NSI, mais il sert dans beaucoup de projets : quiz, calculatrices, petits jeux. Il est fourni avec Python : rien à installer. Le programme de première demande en revanche d’identifier les événements que les composants d’une interface peuvent recevoir : c’est l’idée centrale de cette page. Les programmes s’exécutent dans Thonny (ou tout autre éditeur installé sur l’ordinateur), pas dans le navigateur.
Deux façons d’écrire un programme
Jusqu’ici, nos programmes étaient séquentiels : les instructions s’exécutent dans l’ordre, de la première à la dernière ligne, et c’est le programme qui décide quand demander quelque chose à l’utilisateur, avec input().
Une interface graphique fonctionne autrement. Le programme prépare une fenêtre et ses composants, puis il attend. C’est l’utilisateur qui décide : il clique sur un bouton, tape au clavier, ferme la fenêtre. Chacune de ces actions est un événement, et le programme y répond en appelant la fonction qu’on lui a associée. On parle de programmation événementielle.
L’attente est assurée par la boucle d’événements, lancée par fenetre.mainloop(). Elle tourne jusqu’à la fermeture de la fenêtre : elle surveille les événements et appelle les fonctions associées. Nous n’écrivons donc jamais cette boucle nous-mêmes.
Une première fenêtre
import tkinter as tk
fenetre = tk.Tk() # la fenêtre principale
fenetre.title("Ma première fenêtre") # texte de la barre de titre
fenetre.geometry("360x120") # largeur x hauteur, en pixels
message = tk.Label(fenetre, text="Bonjour !", font=("Arial", 24))
message.pack(pady=30) # placer le texte dans la fenêtre
fenetre.mainloop() # boucle d'événements

La première fenêtre.
import tkinter as tkimporte le module sous le nom courttk: on écrittk.Labelau lieu detkinter.Label.tk.Tk()crée la fenêtre principale. Un programme n’en crée qu’une seule.tk.Label(fenetre, text=...)crée une étiquette, c’est-à-dire un texte affiché. Le premier argument indique dans quoi on la range : ici, la fenêtre.- Créer un composant ne suffit pas à l’afficher : il faut le placer, ici avec
pack(). Un composant créé mais jamais placé reste invisible. fenetre.mainloop()doit être la dernière instruction : les lignes écrites après ne s’exécutent qu’à la fermeture de la fenêtre.
Les composants d’une interface s’appellent des widgets (de window gadget). Les plus courants :
| Widget | Rôle | Création |
|---|---|---|
Label | afficher un texte ou une image | tk.Label(fenetre, text="Score : 0") |
Button | bouton cliquable | tk.Button(fenetre, text="OK", command=valider) |
Entry | champ de saisie d’une ligne | tk.Entry(fenetre, width=20) |
Frame | cadre qui regroupe d’autres widgets | tk.Frame(fenetre) |
Canvas | zone de dessin | tk.Canvas(fenetre, width=400, height=300) |
Checkbutton | case à cocher | tk.Checkbutton(fenetre, text="...", variable=v) |
Radiobutton | choix unique dans une liste | tk.Radiobutton(fenetre, text="...", variable=v, value=1) |
Un bouton qui réagit
Un bouton reçoit un paramètre command : la fonction à appeler quand on clique dessus. Cette fonction s’appelle un gestionnaire d’événement (en anglais callback, « fonction rappelée »).
import tkinter as tk
compteur = 0
def cliquer():
"""Appelée par tkinter à chaque clic sur le bouton."""
global compteur
compteur = compteur + 1
affichage.config(text=f"Nombre de clics : {compteur}")
fenetre = tk.Tk()
fenetre.title("Compteur")
affichage = tk.Label(fenetre, text="Nombre de clics : 0", font=("Arial", 18))
affichage.pack(padx=40, pady=15)
bouton = tk.Button(fenetre, text="Cliquer ici", command=cliquer)
bouton.pack(pady=10)
fenetre.mainloop()

Après trois clics.
Le programme principal crée la fenêtre, puis lance mainloop(). À partir de là, il ne se passe rien tant que l’utilisateur ne clique pas. À chaque clic, la boucle d’événements appelle cliquer().
| Moment | Ce qui se passe | compteur | Texte affiché |
|---|---|---|---|
| lancement | création des widgets, mainloop() | 0 | Nombre de clics : 0 |
| 1er clic | appel de cliquer() | 1 | Nombre de clics : 1 |
| 2e clic | appel de cliquer() | 2 | Nombre de clics : 2 |
| 3e clic | appel de cliquer() | 3 | Nombre de clics : 3 |
Deux points de ce programme reviennent dans toutes les interfaces.
- Modifier un widget :
affichage.config(text=...)change le texte d’une étiquette déjà affichée. La méthodeconfig(ouconfigure, c’est la même) modifie n’importe quelle option :text,fg(couleur du texte),bg(couleur du fond),font,state… - Garder une information d’un clic à l’autre : la variable
compteurest définie dans le programme principal. Comme la fonction la modifie, elle doit l’annoncer avecglobal compteur; sinon Python la considère comme une variable locale et lève l’erreurUnboundLocalError. Une fonction qui se contente de lire une variable globale, ou d’appeler une méthode d’un widget commeaffichage.config(...), n’a pas besoin deglobal.
Le piège des parenthèses
Question. Que se passe-t-il si l’on écrit command=cliquer() au lieu de command=cliquer ?
Réponse
Avec les parenthèses, Python appelle la fonction tout de suite, au moment où il crée le bouton : le compteur passe à 1 avant même l’affichage de la fenêtre. Ensuite, command reçoit la valeur renvoyée par cliquer(), c’est-à-dire None, et le bouton ne fait plus rien.
Sans parenthèses, on donne le nom de la fonction : tkinter la garde de côté et l’appellera lui-même, à chaque clic.
À l’inverse, à l’intérieur d’une fonction, écrire valider sans parenthèses ne fait rien du tout : Python lit le nom de la fonction sans l’appeler. Pour appeler une fonction soi-même, il faut toujours les parenthèses : valider().
| On écrit | Signification |
|---|---|
command=valider | « appelle valider plus tard, à chaque clic » : correct |
command=valider() | appelle valider tout de suite et donne son résultat au bouton : erreur |
valider() dans une fonction | appelle valider maintenant : correct |
valider seul dans une fonction | ne fait rien : erreur |
Lire une saisie
Un champ Entry permet à l’utilisateur de taper du texte. Le programme lit ce texte avec la méthode get(), au moment où il en a besoin, en général quand on clique sur un bouton.
import tkinter as tk
def verifier():
"""Compare la réponse tapée au résultat de 7 x 8."""
saisie = champ.get() # le texte tapé, toujours une chaîne
if saisie == "":
resultat.config(text="Taper une réponse.", fg="black")
elif saisie.isdigit() and int(saisie) == 56:
resultat.config(text="Bravo !", fg="green")
else:
resultat.config(text=f"Non, 7 x 8 = 56 et non {saisie}.", fg="red")
champ.delete(0, "end") # vider le champ
fenetre = tk.Tk()
fenetre.title("Calcul mental")
question = tk.Label(fenetre, text="Combien font 7 x 8 ?", font=("Arial", 18))
question.pack(padx=30, pady=10)
champ = tk.Entry(fenetre, width=10, font=("Arial", 18), justify="center")
champ.pack()
bouton = tk.Button(fenetre, text="Vérifier", command=verifier)
bouton.pack(pady=10)
resultat = tk.Label(fenetre, text="", font=("Arial", 16))
resultat.pack(pady=(0, 15))
fenetre.mainloop()

Après avoir tapé 56 et cliqué sur « Vérifier ».
Trois règles évitent la plupart des erreurs.
champ.get()renvoie toujours une chaîne, même si l’utilisateur a tapé un nombre :"56"et non56. Pour calculer, on convertit avecint()oufloat(), exactement comme avecinput().- On compare le texte du champ, pas le champ lui-même. La condition
champ == 56est toujours fausse :champest un widget, pas un nombre. Il faut écrireint(champ.get()) == 56. - On vérifie la saisie avant de la convertir.
int("")ouint("abc")lève l’erreurValueError. La méthodeisdigit()renvoieTruesi la chaîne n’est faite que de chiffres (elle refuse donc aussi les nombres négatifs) : on ne convertit qu’après ce test.
La méthode champ.delete(0, "end") efface le contenu du champ, du premier caractère (indice 0) jusqu’à la fin. Pour y écrire un texte, on utilise champ.insert(0, "texte").
Question. L’utilisateur tape 56 (avec une espace à la fin). Quel message s’affiche ?
Réponse
« Non, 7 x 8 = 56 et non 56 . » : la chaîne "56 " contient une espace, donc isdigit() renvoie False. Pour être plus tolérant, on peut retirer les espaces au début et à la fin avec saisie = champ.get().strip().
Valider avec la touche Entrée
Pour que la touche Entrée fasse la même chose que le bouton, on lie l’événement « touche Entrée » à une fonction avec bind :
champ.bind("<Return>", verifier)
Une fonction appelée par bind reçoit toujours un argument : un objet qui décrit l’événement (touche enfoncée, position de la souris…). Une fonction appelée par command n’en reçoit aucun. Pour qu’une même fonction serve aux deux, on lui donne un paramètre avec une valeur par défaut :
def verifier(event=None):
...
bouton = tk.Button(fenetre, text="Vérifier", command=verifier) # appel sans argument
champ.bind("<Return>", verifier) # appel avec un argument
Les noms d’événements s’écrivent entre chevrons :
| Événement | Signification |
|---|---|
"<Return>" | touche Entrée |
"<KeyPress>" | n’importe quelle touche ; event.keysym donne son nom ("a", "Left", "space"…) |
"<Left>", "<Right>", "<Up>", "<Down>" | flèches du clavier |
"<Button-1>" | clic gauche ; event.x et event.y donnent la position du clic |
"<Motion>" | déplacement de la souris |
Disposer les widgets
Tkinter propose trois façons de placer les widgets dans une fenêtre. On en choisit une seule par conteneur (la fenêtre ou un même cadre) : mélanger pack et grid dans le même conteneur provoque une erreur.
| Méthode | Principe | Usage |
|---|---|---|
pack() | empile les widgets, de haut en bas par défaut | interfaces simples, en colonne ou en ligne |
grid(row=..., column=...) | range les widgets dans un tableau de lignes et de colonnes | formulaires, claviers, grilles de jeu |
place(x=..., y=...) | position exacte, en pixels | décors, superpositions |
Une grille : le calcul de l’IMC
Un formulaire s’organise naturellement en grille : les libellés dans la colonne 0, les champs dans la colonne 1.
import tkinter as tk
def calculer():
"""Calcule l'indice de masse corporelle : masse / taille²."""
try:
masse = float(champ_masse.get().replace(",", "."))
taille = float(champ_taille.get().replace(",", "."))
imc = masse / taille ** 2
resultat.config(text=f"IMC : {imc:.1f}")
except ValueError:
resultat.config(text="Saisir deux nombres.")
except ZeroDivisionError:
resultat.config(text="La taille ne peut pas être nulle.")
fenetre = tk.Tk()
fenetre.title("Indice de masse corporelle")
tk.Label(fenetre, text="Masse (kg) :").grid(row=0, column=0, sticky="e", padx=5, pady=5)
champ_masse = tk.Entry(fenetre, width=10)
champ_masse.grid(row=0, column=1, padx=5, pady=5)
tk.Label(fenetre, text="Taille (m) :").grid(row=1, column=0, sticky="e", padx=5, pady=5)
champ_taille = tk.Entry(fenetre, width=10)
champ_taille.grid(row=1, column=1, padx=5, pady=5)
tk.Button(fenetre, text="Calculer", command=calculer).grid(row=2, column=0, columnspan=2, pady=5)
resultat = tk.Label(fenetre, text="", font=("Arial", 16))
resultat.grid(row=3, column=0, columnspan=2, pady=(0, 10))
fenetre.mainloop()

Le formulaire après un calcul.
sticky="e"colle le libellé au côté droit (east) de sa case, contre le champ. Les autres valeurs sont"w"(gauche),"n"(haut) et"s"(bas).columnspan=2fait occuper deux colonnes au bouton et au résultat, qui se retrouvent centrés.padxetpadyajoutent une marge, en pixels, autour du widget.- Quand un widget n’a jamais besoin d’être modifié (les libellés), on peut le créer et le placer en une ligne, sans le ranger dans une variable. Mais
grid()renvoieNone: écrirechamp = tk.Entry(fenetre).grid(...)rangeNonedanschamp, etchamp.get()lève alorsAttributeError. Un widget qu’on utilisera plus tard se crée et se place en deux lignes.
Le bloc try … except attrape les erreurs de conversion : si l’utilisateur tape des lettres, float() lève ValueError, et au lieu de planter, le programme affiche un message. L’appel replace(",", ".") accepte la virgule, que tout le monde tape en français.
Des cadres pour organiser la fenêtre
Un Frame est un conteneur invisible : on y range plusieurs widgets, puis on place le cadre comme un seul bloc. Chaque cadre a sa propre disposition : on peut ainsi utiliser pack dans la fenêtre et grid à l’intérieur d’un cadre.
import tkinter as tk
fenetre = tk.Tk()
fenetre.title("Cadres")
titre = tk.Label(fenetre, text="Clavier numérique", font=("Arial", 16))
titre.pack(pady=5)
clavier = tk.Frame(fenetre) # un cadre pour les boutons
clavier.pack(padx=10, pady=10)
for i in range(9):
chiffre = i + 1
bouton = tk.Button(clavier, text=str(chiffre), width=3)
bouton.grid(row=i // 3, column=i % 3)
fenetre.mainloop()

Le titre est placé avec pack, les boutons avec grid dans un cadre.
Question. Dans quelle ligne et dans quelle colonne se trouve le bouton 6 ?
Réponse
Le bouton 6 est créé quand i vaut 5 : il est en ligne 5 // 3 = 1 et en colonne 5 % 3 = 2, c’est-à-dire sur la deuxième ligne, dans la troisième colonne (on compte à partir de 0). Le quotient // donne la ligne, le reste % la colonne.
Des boutons qui transmettent une valeur
Les neuf boutons du clavier ci-dessus ne font rien. Pour qu’ils affichent leur chiffre, il faudrait que chacun appelle une fonction avec son chiffre en argument. Or command attend une fonction sans argument, et command=ajouter(chiffre) appellerait la fonction tout de suite.
La solution la plus simple est d’écrire une petite fonction par bouton :
def ajouter_1():
ajouter(1)
def ajouter_2():
ajouter(2)
C’est correct mais répétitif. Beaucoup de programmes utilisent à la place une fonction anonyme, écrite avec le mot-clé lambda (hors programme) : lambda: ajouter(1) est une fonction sans nom et sans paramètre, qui appelle ajouter(1) quand on l’appelle, c’est-à-dire au moment du clic.
import tkinter as tk
def ajouter(chiffre):
"""Ajoute le chiffre cliqué à la fin de l'écran."""
ecran.config(text=ecran.cget("text") + str(chiffre))
def effacer():
ecran.config(text="")
fenetre = tk.Tk()
fenetre.title("Clavier")
ecran = tk.Label(fenetre, text="", width=12, font=("Courier", 22),
bg="white", relief="sunken")
ecran.pack(padx=10, pady=10)
clavier = tk.Frame(fenetre)
clavier.pack(padx=10, pady=(0, 10))
for i in range(9):
chiffre = i + 1
bouton = tk.Button(clavier, text=str(chiffre), width=3,
command=lambda c=chiffre: ajouter(c))
bouton.grid(row=i // 3, column=i % 3)
tk.Button(clavier, text="C", width=3, command=effacer).grid(row=3, column=1)
fenetre.mainloop()

Après des clics sur 1, 9, 8 et 4.
La méthode cget("text") lit une option d’un widget : ici, le texte déjà affiché sur l’écran.
Pourquoi écrire lambda c=chiffre: ajouter(c) et pas simplement lambda: ajouter(chiffre) ? Une fonction anonyme ne lit la variable chiffre qu’au moment du clic. À ce moment-là, la boucle est terminée depuis longtemps et chiffre vaut 9 : tous les boutons ajouteraient 9. Écrire c=chiffre donne au paramètre c une valeur par défaut, calculée à la création du bouton : chaque bouton garde ainsi son propre chiffre.
Question. Le clavier n’a pas de touche 0. Quelle ligne faut-il ajouter, et où placer le bouton pour qu’il soit à gauche du bouton C ?
Réponse
tk.Button(clavier, text="0", width=3, command=lambda: ajouter(0)).grid(row=3, column=0)
Le bouton C est en ligne 3, colonne 1 : le 0 va en ligne 3, colonne 0. Ici, pas besoin de c=..., car la valeur 0 est écrite directement, sans variable de boucle.
Le temps : after remplace while et sleep
Dans un programme séquentiel, on attend avec time.sleep(1) et on répète avec while. Dans une interface graphique, ces deux instructions bloquent la fenêtre : pendant qu’une fonction tourne, la boucle d’événements est arrêtée. La fenêtre ne se redessine plus, les boutons ne répondent plus, et le système finit par signaler que l’application ne répond pas.
La règle est donc : un gestionnaire d’événement doit se terminer vite. Pour faire quelque chose plus tard, on demande à tkinter de rappeler une fonction après un délai, avec fenetre.after(delai, fonction) ; le délai est en millisecondes.
import tkinter as tk
restant = 10
def decompter():
"""Appelée toutes les secondes jusqu'à zéro."""
global restant
affichage.config(text=str(restant))
if restant > 0:
restant = restant - 1
fenetre.after(1000, decompter) # rappel dans 1000 ms
else:
affichage.config(text="Temps écoulé !", fg="red")
fenetre = tk.Tk()
fenetre.title("Compte à rebours")
affichage = tk.Label(fenetre, text="", font=("Arial", 40), width=12)
affichage.pack(padx=20, pady=20)
decompter() # premier appel : la suite se fait toute seule
fenetre.mainloop()

Le compte à rebours, trois secondes après le lancement.
after n’attend pas : il programme un appel et rend la main tout de suite. La fonction se reprogramme elle-même tant que le décompte n’est pas fini, et entre deux appels, la boucle d’événements reste libre de traiter les clics.
| Instant | Appel | Affiché | restant après l’appel | Nouvel appel programmé ? |
|---|---|---|---|---|
| 0 s | 1er | 10 | 9 | oui, à 1 s |
| 1 s | 2e | 9 | 8 | oui, à 2 s |
| … | … | … | … | … |
| 9 s | 10e | 1 | 0 | oui, à 10 s |
| 10 s | 11e | « Temps écoulé ! » (le 0, aussitôt remplacé, n’est jamais visible) | 0 | non |
Question. Pourquoi ce programme ne fonctionne-t-il pas ?
def decompter():
for n in range(10, 0, -1):
affichage.config(text=str(n))
time.sleep(1)
Réponse
Pendant les dix secondes de la boucle, la fonction ne rend pas la main à mainloop() : la fenêtre n’est jamais redessinée. Elle reste figée, puis affiche directement 1 à la fin. Les modifications d’un widget ne deviennent visibles qu’au retour dans la boucle d’événements.
Dessiner dans un Canvas
Un Canvas est une zone de dessin. On y crée des formes : rectangles, ovales, lignes, polygones, textes, images. Chaque création renvoie un numéro qui identifie la forme ; on s’en sert ensuite pour la déplacer, la modifier ou la supprimer.
Attention au repère : comme dans presque toutes les bibliothèques graphiques, l’origine (0, 0) est le coin supérieur gauche du canevas, et l’axe des y va vers le bas. C’est l’inverse de turtle et des mathématiques.
import tkinter as tk
fenetre = tk.Tk()
fenetre.title("Dessin")
dessin = tk.Canvas(fenetre, width=400, height=250, bg="lightblue")
dessin.pack()
dessin.create_rectangle(0, 200, 400, 250, fill="forestgreen", outline="") # herbe
dessin.create_oval(300, 30, 360, 90, fill="gold", outline="orange") # soleil
dessin.create_rectangle(80, 120, 200, 200, fill="wheat") # maison
dessin.create_polygon(70, 120, 140, 60, 210, 120, fill="firebrick") # toit
dessin.create_line(0, 230, 400, 230, fill="darkgreen", width=3) # chemin
dessin.create_text(140, 225, text="Ma maison", font=("Arial", 14), fill="white")
fenetre.mainloop()

Un dessin fait de six formes.
| Méthode | Coordonnées | Exemple |
|---|---|---|
create_rectangle(x1, y1, x2, y2) | deux coins opposés | create_rectangle(80, 120, 200, 200, fill="wheat") |
create_oval(x1, y1, x2, y2) | rectangle qui contient l’ovale | un cercle si le rectangle est un carré |
create_line(x1, y1, x2, y2, ...) | les points successifs | create_line(0, 230, 400, 230, width=3) |
create_polygon(x1, y1, x2, y2, ...) | les sommets | le toit : trois sommets |
create_text(x, y, text=...) | centre du texte | create_text(140, 225, text="Ma maison") |
move(forme, dx, dy) | décalage | dessin.move(balle, 5, 0) |
coords(forme) | renvoie les coordonnées actuelles | [x1, y1, x2, y2] |
itemconfig(forme, ...) | modifie une option | dessin.itemconfig(balle, fill="red") |
delete(forme) | supprime une forme ; delete("all") efface tout | dessin.delete(balle) |
Question. Le soleil est créé par create_oval(300, 30, 360, 90). Quel est son rayon, et où est son centre ?
Réponse
Le rectangle qui le contient mesure 60 pixels de côté : c’est un cercle de rayon 30, de centre (330, 60), c’est-à-dire 330 pixels à droite du bord gauche et 60 pixels sous le bord haut.
Un petit jeu : déplacer un objet au clavier
On lie les flèches du clavier à des fonctions qui déplacent une forme avec move. Pour recevoir les touches, on lie l’événement à la fenêtre entière.
import tkinter as tk
LARGEUR, HAUTEUR = 400, 300
PAS = 10
fenetre = tk.Tk()
fenetre.title("Flèches du clavier")
dessin = tk.Canvas(fenetre, width=LARGEUR, height=HAUTEUR, bg="white")
dessin.pack()
joueur = dessin.create_oval(180, 130, 220, 170, fill="royalblue")
def deplacer(event):
"""Déplace le joueur selon la flèche enfoncée, sans sortir du canevas."""
x1, y1, x2, y2 = dessin.coords(joueur)
dx, dy = 0, 0
if event.keysym == "Left" and x1 > 0:
dx = -PAS
elif event.keysym == "Right" and x2 < LARGEUR:
dx = PAS
elif event.keysym == "Up" and y1 > 0:
dy = -PAS
elif event.keysym == "Down" and y2 < HAUTEUR:
dy = PAS
dessin.move(joueur, dx, dy)
fenetre.bind("<KeyPress>", deplacer)
fenetre.mainloop()
La flèche du haut diminue y : c’est la conséquence du repère tourné vers le bas. Les tests x1 > 0, x2 < LARGEUR… empêchent le joueur de sortir du canevas.
Une animation avec after
Pour qu’un objet bouge tout seul, on combine move et after : une fonction déplace l’objet d’un petit pas, puis se reprogramme 20 millisecondes plus tard, soit environ 50 images par seconde.
import tkinter as tk
LARGEUR, HAUTEUR = 400, 300
fenetre = tk.Tk()
fenetre.title("Rebonds")
dessin = tk.Canvas(fenetre, width=LARGEUR, height=HAUTEUR, bg="black")
dessin.pack()
balle = dessin.create_oval(20, 20, 50, 50, fill="orange")
vx, vy = 4, 3
def animer():
global vx, vy
x1, y1, x2, y2 = dessin.coords(balle)
if x1 + vx < 0 or x2 + vx > LARGEUR:
vx = -vx # rebond sur un bord vertical
if y1 + vy < 0 or y2 + vy > HAUTEUR:
vy = -vy # rebond sur un bord horizontal
dessin.move(balle, vx, vy)
fenetre.after(20, animer)
animer()
fenetre.mainloop()
Cases à cocher, boutons radio et variables tkinter
Une case à cocher ou un groupe de boutons radio stocke son état dans une variable tkinter : tk.IntVar() contient un entier, tk.StringVar() une chaîne. On lit sa valeur avec get() et on la modifie avec set(). Tous les boutons radio d’un même groupe partagent la même variable : cocher l’un décoche les autres.
import tkinter as tk
question = "Quel est le type de la valeur 3.0 ?"
choix = ["int", "float", "str", "bool"]
bonne = "float"
def valider():
if reponse.get() == "aucune":
message.config(text="Choisir une réponse.", fg="black")
elif reponse.get() == bonne:
message.config(text="Bonne réponse !", fg="green")
else:
message.config(text=f"Non : c'est {bonne}.", fg="red")
fenetre = tk.Tk()
fenetre.title("QCM")
tk.Label(fenetre, text=question, font=("Arial", 16)).pack(padx=20, pady=10)
reponse = tk.StringVar(value="aucune") # aucune case cochée au départ
for c in choix:
tk.Radiobutton(fenetre, text=c, variable=reponse, value=c,
font=("Arial", 14)).pack(anchor="w", padx=40)
tk.Button(fenetre, text="Valider", command=valider).pack(pady=10)
message = tk.Label(fenetre, text="", font=("Arial", 14))
message.pack(pady=(0, 10))
fenetre.mainloop()

Le QCM après avoir coché « float » et validé.
Chaque Radiobutton reçoit variable=reponse (la variable du groupe) et value=c (la valeur que prend la variable quand on le coche). Pour une case à cocher, on utilise de même tk.Checkbutton(fenetre, text="...", variable=v) avec v = tk.IntVar() : v.get() vaut 1 si la case est cochée, 0 sinon.
Une image dans la fenêtre
Tkinter affiche directement les images PNG et GIF avec tk.PhotoImage. Pour d’autres formats (JPEG) ou pour redimensionner, on utilise la bibliothèque Pillow (pip install pillow).
import tkinter as tk
fenetre = tk.Tk()
fenetre.title("Image")
logo = tk.PhotoImage(file="logo.png") # fichier dans le même dossier
cadre_image = tk.Label(fenetre, image=logo)
cadre_image.pack(padx=10, pady=10)
tk.Label(fenetre, text="Une image dans un Label").pack(pady=(0, 10))
fenetre.mainloop()

Une image PNG affichée dans une étiquette.
Deux précautions :
- le fichier doit se trouver dans le dossier courant, qui n’est pas toujours celui du programme. Le plus sûr est de construire le chemin à partir de l’emplacement du programme, comme dans la page Pygame ;
- la variable qui contient l’image (
logo) doit exister tant que la fenêtre est ouverte. Si l’image est créée dans une fonction et rangée dans une variable locale, Python la supprime à la fin de la fonction et l’étiquette devient vide. On la range alors dans une variable globale, ou on la garde dans le widget :cadre_image.image = logo.
Un projet complet : dix questions de calcul mental
Ce programme réunit tout ce qui précède : il pose dix multiplications tirées au hasard, compte les bonnes réponses, puis affiche le score. Il n’y a pas de boucle while pour enchaîner les questions : c’est chaque validation qui prépare la question suivante.
import random
import tkinter as tk
NB_QUESTIONS = 10
numero = 0 # numéro de la question en cours
score = 0
a, b = 0, 0 # les deux facteurs de la question en cours
def nouvelle_question():
"""Tire une nouvelle multiplication et l'affiche, ou termine la partie."""
global numero, a, b
if numero == NB_QUESTIONS:
question.config(text=f"Score final : {score} / {NB_QUESTIONS}")
champ.config(state="disabled")
bouton.config(state="disabled")
return
numero = numero + 1
a = random.randint(2, 9)
b = random.randint(2, 9)
question.config(text=f"Question {numero} : {a} x {b} = ?")
champ.delete(0, "end")
champ.focus_set() # le curseur va dans le champ
def valider(event=None):
"""Vérifie la réponse, met à jour le score, passe à la question suivante."""
global score
if champ.cget("state") == "disabled": # partie terminée : Entrée ne fait plus rien
return
saisie = champ.get().strip()
if not saisie.isdigit():
message.config(text="Taper un nombre.", fg="black")
return
if int(saisie) == a * b:
score = score + 1
message.config(text="Juste !", fg="green")
else:
message.config(text=f"Faux : {a} x {b} = {a * b}", fg="red")
affichage_score.config(text=f"Score : {score}")
nouvelle_question()
fenetre = tk.Tk()
fenetre.title("Calcul mental")
question = tk.Label(fenetre, text="", font=("Arial", 20))
question.pack(padx=30, pady=(15, 5))
champ = tk.Entry(fenetre, width=6, font=("Arial", 20), justify="center")
champ.pack()
champ.bind("<Return>", valider)
bouton = tk.Button(fenetre, text="Valider", command=valider)
bouton.pack(pady=8)
message = tk.Label(fenetre, text="", font=("Arial", 14))
message.pack()
affichage_score = tk.Label(fenetre, text="Score : 0", font=("Arial", 14))
affichage_score.pack(pady=(0, 15))
nouvelle_question()
fenetre.mainloop()

La partie en cours : question 4, trois bonnes réponses.
L’état de la partie tient dans quatre variables globales : numero, score, a et b. Les deux fonctions les lisent et les modifient ; c’est pourquoi chacune déclare avec global celles qu’elle modifie. Le déroulement est le suivant :
- au lancement,
nouvelle_question()affiche la question 1 ; - à chaque validation,
valider()corrige la réponse puis appellenouvelle_question(); - quand
nouvelle_question()est appelée alors quenumerovaut déjà 10, c’est-à-dire après la dixième réponse, elle affiche le score et désactive le champ et le bouton (state="disabled") au lieu de poser une onzième question.
Question. On veut rejouer sans relancer le programme. Que doit faire une fonction rejouer(), appelée par un bouton « Rejouer » ?
Réponse
Remettre l’état à zéro, réactiver les widgets, puis poser la première question :
def rejouer():
global numero, score
numero = 0
score = 0
affichage_score.config(text="Score : 0")
message.config(text="")
champ.config(state="normal")
bouton.config(state="normal")
nouvelle_question()
Il ne faut pas détruire la fenêtre pour en recréer une : on met simplement à jour les widgets existants.
Plusieurs écrans et boîtes de dialogue
Un jeu a souvent plusieurs écrans : accueil, partie, résultats. Il ne faut pas créer une nouvelle fenêtre tk.Tk() à chaque fois : un programme n’a qu’une fenêtre principale. Deux solutions :
- changer le contenu de la fenêtre : chaque écran est un
Frame; on cache l’un avecpack_forget()et on affiche l’autre avecpack(); - ouvrir une fenêtre secondaire avec
tk.Toplevel(fenetre), qui s’utilise comme la fenêtre principale mais se ferme sans arrêter le programme.
Le module messagebox affiche des boîtes de dialogue toutes prêtes :
from tkinter import messagebox
messagebox.showinfo("Fin de la partie", "Score : 8 / 10") # information
messagebox.showerror("Erreur", "Le fichier est introuvable.") # erreur
if messagebox.askyesno("Quitter", "Voulez-vous vraiment quitter ?"):
fenetre.destroy() # ferme la fenêtre
askyesno renvoie True si l’on clique sur « Oui ». fenetre.destroy() ferme la fenêtre principale, ce qui termine mainloop() et donc le programme.
Erreurs fréquentes
| Symptôme | Cause | Correction |
|---|---|---|
| la fonction s’exécute au lancement, puis le bouton ne fait plus rien | command=valider() | command=valider, sans parenthèses |
| une fonction ne fait rien | valider écrit sans parenthèses à l’intérieur d’une autre fonction | valider() |
| la réponse juste est refusée | comparaison du widget au lieu de son texte, ou d’une chaîne à un nombre | int(champ.get()) == resultat |
ValueError: invalid literal for int() with base 10: '' | conversion d’un champ vide ou non numérique | tester saisie.isdigit() avant int(), ou try … except ValueError |
AttributeError: 'NoneType' object has no attribute 'get' | champ = tk.Entry(...).grid(...) : grid renvoie None | créer, puis placer, en deux lignes |
TypeError: verifier() takes 0 positional arguments but 1 was given | fonction appelée par bind, qui transmet un événement | def verifier(event=None): |
UnboundLocalError: cannot access local variable 'score' | la fonction modifie une variable globale | global score en tête de la fonction |
TclError: cannot use geometry manager grid inside . which already has slaves managed by pack | pack et grid mélangés dans le même conteneur | une seule méthode par conteneur, ou des Frame |
| la fenêtre se fige | while ou time.sleep dans un gestionnaire | fenetre.after(delai, fonction) |
| tous les boutons créés dans une boucle font la même chose | lambda: f(i) lit i au moment du clic | lambda i=i: f(i) |
| l’image n’apparaît pas | l’image n’est rangée que dans une variable locale | la garder dans une variable globale |
| un widget n’apparaît pas | il n’a jamais été placé | appeler pack(), grid() ou place() |
| une fenêtre vide apparaît en plus | plusieurs tk.Tk() dans le programme | une seule tk.Tk(), des Toplevel pour les autres fenêtres |
Pour aller plus loin
- Compléter le clavier numérique avec un bouton
0et un bouton « Valider » qui compare le nombre tapé à un nombre mystère. - Ajouter au calcul mental un compte à rebours de 5 secondes par question, avec
after. - Transformer le QCM en questionnaire de plusieurs questions rangées dans une liste de dictionnaires (
{"question": ..., "choix": [...], "bonne": ...}). - La bibliothèque
customtkinter(pip install customtkinter) donne un aspect plus moderne aux mêmes widgets :customtkinter.CTkButtonremplacetk.Button, et tout ce qui précède reste valable. - La documentation de tkinter (en français).
À retenir
- Une interface graphique est événementielle : le programme crée les widgets, associe des fonctions aux événements, puis lance
mainloop(), qui attend et appelle ces fonctions. - Un widget se crée (
tk.Label(fenetre, ...)) puis se place (pack,gridouplace), une seule méthode de placement par conteneur. command=fetbind("<Return>", f)donnent le nom de la fonction, sans parenthèses ; une fonction appelée parbindreçoit un argumentevent.champ.get()renvoie toujours une chaîne : on la teste, puis on la convertit avecint()oufloat().widget.config(text=...)modifie un widget déjà affiché.- Une fonction qui modifie une variable du programme principal la déclare avec
global. - Jamais de
whileni detime.sleepdans un gestionnaire :fenetre.after(delai, f)programme un appel futur. - Dans un
Canvas, l’origine est en haut à gauche etyaugmente vers le bas.
Résumé
| Concept | Syntaxe | Exemple |
|---|---|---|
| Fenêtre | tk.Tk(), title, geometry | fenetre.geometry("400x300") |
| Texte | tk.Label(parent, text=...) | tk.Label(fenetre, text="Score : 0") |
| Bouton | tk.Button(parent, text=..., command=f) | command=valider |
| Champ | tk.Entry(parent), get(), delete(0, "end") | int(champ.get()) |
| Modifier | widget.config(option=valeur) | message.config(text="Bravo", fg="green") |
| Placer | pack(), grid(row, column), place(x, y) | grid(row=0, column=1, sticky="e") |
| Regrouper | tk.Frame(parent) | un cadre par zone de la fenêtre |
| Événement | widget.bind("<Evenement>", f), f(event) | champ.bind("<Return>", valider) |
| Bouton avec valeur | command=lambda v=v: f(v) | lambda c=chiffre: ajouter(c) |
| Temps | fenetre.after(ms, f) | fenetre.after(1000, decompter) |
| Dessin | tk.Canvas, create_..., move, coords | dessin.move(balle, vx, vy) |
| Choix | tk.StringVar(), Radiobutton, Checkbutton | reponse.get() |
| Image | tk.PhotoImage(file=...) | garder la variable |
| Boucle d’événements | fenetre.mainloop() | dernière ligne |