logo le blog invivoo blanc

Réaliser un Mastermind avec Tkinter (Python) – Part 1

14 janvier 2021 | Design & Code, Python | 1 comment

Il existe plusieurs outils pour créer des interfaces graphiques en Python, parmi lesquels on peut citer par exemple Kivy, PyQt, wxPython et Tkinter, même s’il en existe bien d’autres. À l’exception de Kivy, tous ceux-ci sont des interfaces pour des bibliothèques graphiques bien connues et utilisables dans d’autres langages (Qt, wxWidgets, Tk). Les principaux avantages de Tkinter sont sa facilité de prise en main, le fait que la bibliothèque soit installée par défaut avec Python et, en conséquence, la très grande quantité de ressources disponibles pour aider les développeurs confirmés comme ceux qui débutent dans la programmation d’interfaces graphiques.

Tkinter est une interface (le nom signifiant « Tk Interface ») pour manipuler les objets de la bibliothèque Tk, créée par John Ousterhout entre la fin des années 80, à l’origine comme une extension pour son nouveau langage de scripts Tcl. Tant et si bien que la combinaison des deux est depuis célèbre sous le nom de Tcl/Tk. Tkinter n’est en réalité qu’une fine couche orientée objet au-dessus de Tcl/Tk, la bibliothèque Python embarquant son propre interpréteur Tcl.

Ce tutoriel se divisera en deux parties principales. Nous ferons d’abord une revue des concepts mis en œuvre dans tout projet basé sur Tkinter, avec une présentation succincte (et non-exhaustive) des principaux widgets disponibles, puis nous étudierons un exemple d’implémentation du célèbre jeu Mastermind. Tous les tests seront effectués à partir de la version 3.6 de Python et le rendu visuel sera obtenu à partir d’un MacBook Pro.

1. Premiers pas

À grand traits, une application graphique consiste en une fenêtre principale (voire plusieurs) dans laquelle on peut disposer divers composants avec lesquels l’utilisateur pourra interagir, les placer selon ses vœux, à partir de laquelle d’autres fenêtres et boîtes de dialogue pourront apparaître, et où l’on peut définir des réactions du programme en fonction des différentes actions effectuées par l’utilisateur. D’emblée, cela implique une façon de programmer très différente des programmes à but non interactif, où le flot peut être connu dès le départ en fonction des données d’entrée qui seront injectées. Ici, c’est un humain (a priori) qui choisira quelles actions mener, qui pourront être considérées comme autant de nouveaux points d’entrée du programme.

Voyons tout de suite comment écrire un programme minimal faisant apparaître la fenêtre principale de notre future application.

from tkinter import Tk

root = Tk()
root.mainloop()

L’objet racine de toute application Tkinter est une instance de la classe Tk. Si nous exécutons ces 3 lignes de code, nous voyons bien apparaître une fenêtre vide, avec laquelle il n’est pas encore possible d’interagir. La dernière instruction est essentielle. Sans elle, la fenêtre disparait aussitôt l’application lancée. Il s’agit en effet d’une boucle infinie servant à capter les signaux émis par les actions de l’utilisateur. On la placera généralement à la toute fin du programme.

Comme beaucoup d’autres objets que nous verrons, il est possible de modifier a posteriori la fenêtre principale. Par exemple, la fenêtre créée par défaut avec le code précédent est étirable. Nous pouvons figer ses dimensions à l’aide de la méthode resizable. Ajoutons-lui également un titre. Le code devient alors :

from tkinter import Tk

root = Tk()
root.resizable(False, False)
root.title("Tuto Tkinter")

root.mainloop()

Voici ce que nous constatons maintenant à l’exécution :

Nous pouvons constater que la fenêtre n’est plus étirable et que son titre a effectivement changé.

2. Rajouter des éléments

2.a Quelques exemples de base détaillés

En l’état, nous sommes bien d’accord que l’application n’a pas de grande utilité. Mais nous pouvons à présent y ajouter des composants. Ces éléments, aussi appelés widgets en anglais (contraction de window et gadget), seront des instances d’autres classes disponibles dans le module tkinter. Le plus simple d’entre eux est probablement le Label, un objet dont le seul but est d’afficher du texte à l’écran. Voyons comment l’insérer dans notre fenêtre vide.

La taille de la fenêtre s’est automatiquement adaptée à son contenu, une seule zone de texte assez petite, raison pour laquelle la fenêtre est elle-même très petite. Afin de pouvoir l’étirer, retirons dès à présent l’instruction « root.resizable(False, False) ».

Il y a plusieurs choses intéressantes à relever sur la manière dont nous venons de rajouter notre premier widget, qui sont seront également vraies pour tous les autres :

  • Un widget est créé simplement en instanciant une classe, sans obligation de récupérer cette instance dans une variable et de faire autre chose avec.
  • Le premier argument sera toujours la fenêtre (pas un identifiant, l’objet lui-même) dans laquelle le widget doit s’insérer, suivi par des arguments nommés optionnels de configuration (il en existe beaucoup).
  • Une fois le widget créé par instanciation, il est indispensable de dire à Tkinter où le placer. C’est le rôle ici de la méthode pack. Attention, si cette étape n’est pas effectuée et bien qu’il existe, le widget ne s’affichera tout simplement pas.

La méthode pack fait appel à ce qu’on appelle un geometry manager chargé de répartir les éléments selon les demandes du développeur. On reviendra un peu plus tard sur cet aspect essentiel de la programmation d’interfaces graphiques.

Tous les paramètres de configuration pouvant être affectés à la création d’un widget sont modifiables a posteriori via la méthode config ou configure (les deux sont identiques). Exemple :

Pour effectuer cette opération, il a suffi de récupérer l’instance du widget dans une variable, de bien penser à n’appeler pack que dans un second temps (car pack modifie l’objet « sur place » et renvoie None), puis d’appeler config ou configure avec les mêmes mots-clés qu’on aurait employés à l’instanciation.

Parmi les paramètres de configuration les plus courants, on peut citer :

  • bg (ou background) qui prend comme valeur un code couleur (sous forme de chaine de caractères) remplissant le fond du widget
  • fg (ou foreground) qui fait la même chose pour le reste du widget (typiquement, le texte affiché sur un Label ou un Button)
  • font pour préciser la police de caractères
  • cursor pour modifier la forme du curseur de la souris quand on passe sur le widget
  • state, spécifique à certains widgets, pour les rendre inactifs (grisés) ou non

Le pendant en lecture du widget Label est l’Entry, qui comme son nom l’indique permet d’entrer du texte (en français, on dit aussi « une zone de saisie »). Il est courant d’utiliser un Label à proximité d’une Entry pour indiquer à l’utilisateur le sens qui sera donné par l’application au texte attendu. Exemple :

Avant l’édition
Pendant l’édition

Ici, nous avons créé une zone de saisie de largeur 10 puis inséré un texte (« John Doe ») en partant du premier caractère (0) de la zone. La largeur se mesure en nombre de caractères. Toutefois, notre exemple ne signifie pas qu’on ne pourra saisir que 10 caractères, mais plutôt que seulement 10 caractères seront assurés d’être visibles quel que soit le texte entré.

Ce texte entré par l’utilisateur peut être récupéré à n’importe quel moment par l’instruction my_entry.get(). Une autre manière très classique de l’obtenir, partagée par quasiment tous les autres widgets pouvant se voir attribuer une valeur (à la différence de la méthode get que tous les objets Tkinter ne possèdent pas), consiste à déclarer au préalable un variable d’un type spécial qui recevra la valeur retournée par l’action de l’utilisateur (dans ce cas, le texte tapé dans la zone de saisie). Il existe trois types de telles variables :

  • IntVar (pour recevoir des entiers)
  • DoubleVar (pour recevoir des nombres flottants)
  • StringVar (pour recevoir des chaînes de caractères)
  • BooleanVar (pour recevoir des booléens sous forme entière, c’est-à-dire 0 ou 1)

Pour notre cas, nous voudrons récupérer du texte. Il faut donc créer une variable de type StringVar et l’associer à un paramètre précis du widget : textvariable. Exemple :

Avec cette technique, plutôt que faire my_entry.get() lorsque nous souhaiterons récupérer le texte entré, nous pourrons faire my_var.get() (la variable my_entry n’ayant au passage plus de raison d’être dans ce code).

Un autre exemple de widget simple est le bouton (Button), qui se résume à afficher un texte, sur lequel on peut appuyer et qui déclenche une action en réponse. Nous allons en créer deux, le premier ne faisant rien et le second modifiant le premier.

from tkinter import Button, DISABLED, Label, Tk

root = Tk()
root.title("Tuto Tkinter")
root.config(bg='silver')

Label(root, text='Hello world').pack()

button1 = Button(root, text='Activé', command=lambda: None)
button2 = Button(root, text='Modifier l\'autre bouton',
                 command=lambda: button1.config(text='Désactivé',
                                                state=DISABLED,
                                                cursor='watch'))
button1.pack()
button2.pack()
root.mainloop()

Voyons le résultat avant et après avoir appuyé sur le second bouton :

La partie un peu plus complexe par rapport aux précédents widgets consiste en la définition de l’action (command) à effectuer en cas d’appui. La valeur de cet argument est nécessairement une fonction qui n’accepte aucun argument, comme nous le voyons ici. Les fonctions anonymes (lambda) seront souvent très utiles à cet endroit, mais rien n’empêche de définir des fonctions classiques complexes ailleurs ou dans d’autres modules et de les utiliser à la place. On appelle généralement les fonctions ainsi passées en argument des callbacks (fonctions appelées non pas directement mais en cascade depuis un autre appel). C’est une technique très largement utilisée par Tkinter.

2.b Quelques autres objets disponibles

Comme nous pouvons nous en douter, il existe beaucoup plus de widgets que ces trois-là. Bien comprendre comment utiliser ces trois premiers permet néanmoins de saisir les notions essentielles mises en œuvre pour tous les autres.

Checkbutton

Il s’agit d’une case à cocher avec du texte. Le résultat se récupère via la technique de la textvariable, sauf que le paramètre s’appelle cette fois-ci variable. Exemple :

Le statut « coché ou non » se récupère à tout moment via l’instruction my_var.get().

Radiobutton

Ce widget sert à représenter un choix multiple. Illustration :

Dans cet exemple c’est la couleur « rouge » qui est sélectionnée. Le texte et la valeur récupérée sont décorrélés. Ici, le résultat de my_var.get() renverra « red ». Le lien d’exclusion mutuelle entre ces différents choix n’existe que par le fait que les trois widgets partagent la même variable.

Spinbox

Cet objet définit une entrée dans laquelle récupérer des valeurs qui peuvent être incrémentées à la hausse ou à la baisse en respectant des bornes. Illustration :

Le résultat pourra s’obtenir ici à tout moment avec la méthode my_box.get().

Combobox

La Combobox est un widget inclus dans une extension de Tkinter (disponible nativement) appelée ttk (Tk themed widgets), qui est un sous-module de Tkinter. Cette extension redéfinit un certain nombre de widgets de Tkinter (comme Button ou Spinbox) avec un rendu visuel et une configuration légèrement différents, et en définit aussi de nouveaux. Nous ne nous attarderons pas sur les spécificités de ttk mais il est nécessaire de mentionner son existence dans un tutoriel sur Tkinter. La Combobox est typiquement un widget intéressant disponible uniquement dans ttk. Son principe rejoint celui de la Spinbox.

from tkinter import Label, Tk
from tkinter.ttk import Combobox

root = Tk()
root.title("Tuto Tkinter")
root.config(bg='silver')

Label(root, text='Choisissez un nombre:').pack()
my_box = Combobox(root, values=(2, 4, 6, 8))
my_box.current(2)
my_box.pack()

root.mainloop()

À partir de ce code, nous obtenons le rendu suivant :

Avant la sélection
Pendant la sélection

On spécifie une liste de valeurs à parcourir via l’argument values. La méthode my_box.current(i) permet de placer le choix sur une des valeurs (i) de la liste à partir de son indice (en partant de 0, comme toujours en Python). Le résultat pourra être récupéré à tout moment par l’instruction my_box.get().

Pour lire la partie 2 de l’article Tkinter Python : c’est ici !