logo le blog invivoo blanc

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

21 janvier 2021 | Design & Code, Python | 0 comments

Retrouvez la partie 1 de l’article ici : Tk Python.

3. Les gestionnaires de positionnement

Il existe trois gestionnaires de positionnement (aussi appelés geometry managers en anglais) permettant de disposer les objets dans une fenêtre :

  • pack (que nous avons vu sans rentrer dans les détails jusqu’à présent)
  • grid (une grille qui permet de définir des espacements réguliers entre les objets)
  • place (qui permet de placer des objets à partir de coordonnées absolues)

Nous ne nous attarderons pas sur place car cette méthode est en pratique assez peu utilisée. En revanche, les deux autres nous seront utiles pour coder le Mastermind. Il est à noter que lorsqu’on commence à utiliser l’un de ces gestionnaires de positionnement dans une fenêtre, on ne doit pas se mettre tout à coup à en utiliser un autre. Autrement dit, ils ne doivent pas être mélangés dans un même conteneur. Sinon, nous risquerions de passer un temps inimaginable à tenter de les réconcilier pour obtenir le rendu souhaité.

3.a Le gestionnaire pack

Nous avons jusqu’à maintenant utilisé cet outil sans jamais passer le moindre argument. Le résultat fut qu’il plaçait systématiquement les widgets les uns sous les autres. C’est un des deux comportements principaux, pack ne permettant que d’empiler des composants verticalement ou les uns à côté des autres horizontalement. Ceci est cependant parfois suffisant comme nous le verrons dans le code du Mastermind.

On peut cependant utiliser trois arguments pour gérer le positionnement des widgets :

  • side
  • fill
  • expand

Le paramètre side permet de définir depuis quel bord de la fenêtre empiler les objets. Ces valeurs indiquant des bords sont des constantes définies par Tkinter et qu’il suffit d’importer : TOP, BOTTOM, LEFT et RIGHT. La valeur par défaut est TOP, ce qui explique pourquoi depuis le début pack empile les objets de haut en bas. Illustration :

from tkinter import Button, Label, Tk
from tkinter import BOTTOM, LEFT, RIGHT, TOP

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

Label(root, text='Bienvenue dans Tkinter').pack()
Button(root, text='Bouton du bas').pack(side=BOTTOM)
Button(root, text='Bouton à gauche').pack(side=LEFT)
Button(root, text='Bouton à droite').pack(side=RIGHT)
Button(root, text='Bouton en haut').pack(side=TOP)

root.mainloop()
Avant étirement (initialement)
Après étirement

On constate que le Label du début, étant par défaut disposé contre le bord du haut, forme une pile avec le seul autre widget disposé via side=TOP.

Le paramètre fill, quant à lui, permet de demander de faire en sorte que le widget remplisse tout l’espace qui lui est alloué dans la direction horizontale (X), verticale (Y) ou les deux (BOTH).

from tkinter import Button, Label, Tk
from tkinter import BOTH, X, Y

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

Label(root, text='Bienvenue dans Tkinter').pack(fill=X)
Button(root, text='Bouton 1').pack(fill=X)
Button(root, text='Bouton 2').pack(fill=Y)
Button(root, text='Bouton 3').pack(fill=BOTH)

root.mainloop()

Ce code produit une fenêtre qui se comporte ainsi lorsqu’on étire la fenêtre :

Sans étirement
Avec étirement

On constate que fill=X permet d’occuper tout l’espace horizontal alloué aux boutons concernés (tous sauf le second, qui laisse apparaître la couleur de fond de la fenêtre sur ses côtés). De la même manière, fill=Y permet d’occuper la totalité de l’espace vertical alloué, ce qui n’est pas perceptible ici car cette allocation ne varie pas avec la taille de la fenêtre, comme illustré par la seconde image. La spécification fill=BOTH combine les deux effets. À noter que les constantes X, Y et BOTH sont des variables globales définies par Tkinter, qu’on peut remplacer respectivement par les chaînes de caractère ‘x’, ‘y’ et ‘both’.

Le paramètre expand accepte comme valeur un booléen (ou un entier faisant office de booléen, 0 ou 1) et vaut par défaut False. S’il est mis à True, le widget voit alors son espace alloué varier lorsque la fenêtre est étirée.

from tkinter import Button, Label, Tk
from tkinter import BOTH, X, Y

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

Label(root, text='Bienvenue dans Tkinter').pack(fill='x')
Button(root, text='Bouton 1').pack(fill='x', expand=True)
Button(root, text='Bouton 2').pack(fill='y', expand=True)
Button(root, text='Bouton 3').pack(fill='both', expand=True)

root.mainloop()

Le rendu obtenu par ce code au lancement est identique à celui du code précédent (avant étirement), et voici ce qui se passe si on étire la fenêtre :

Tous les boutons voient leur espace alloué augmenter, mais ne réagissent pas de la même manière. Le bouton 1 continue à remplir tout l’espace horizontal mais se déplace pour rester au centre de son espace verticalement. Pour le bouton 2 c’est l’opposé, il reste au centre horizontalement mais remplit tout son espace verticalement. Enfin, le bouton 3 occupe tout l’espace qui lui est alloué.

3.b Le gestionnaire grid

Ce gestionnaire est plus simple d’utilisation et plus flexible que pack. S’il ne fallait en apprendre qu’un pour commencer, ce serait sans conteste celui-ci. Il permet de définir une grille 2D régulière, dans laquelle on manipulera explicitement des indices de lignes et de colonnes, et où les cellules des tailles cohérentes les unes avec les autres. Dans chaque colonne la largeur est celle de la plus large cellule, et dans chaque ligne la hauteur est celle de la plus haute cellule (en fonction des widgets qui sont placés çà et là dans la grille).

L’utilisation de ce gestionnaire se fait simplement en appelant la méthode grid() sur chacun des widgets à placer. Démonstration :

from tkinter import Button, Label, Tk

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

Label(root, text='Bienvenue dans Tkinter').grid(row=0, column=0)

Label(root, text='Ici c\'est le NW').grid(row=1, column=0)
Button(root, text='Bouton du NE').grid(row=1, column=1)
Label(root, text='Ici c\'est le SW').grid(row=2, column=0)
Button(root, text='Bouton du SE').grid(row=2, column=1)

root.mainloop()

Comme toujours en Python, les indices démarrent à 0. On passe explicitement les numéros de ligne et de colonne via les arguments nommés row et column. La grille ainsi obtenue n’est pas étirable par défaut :

On voit que la grille ne suit effectivement pas le mouvement. Il est possible de remédier à ça par des méthodes de configuration par ligne et par colonne, à savoir respectivement rowconfigure et columnconfigure appliquées directement à la fenêtre contenant les widgets et où chaque ligne et colonne est identifiée par son indice. Exemple :

from tkinter import Button, Label, Tk

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

Label(root, text='Bienvenue dans Tkinter').grid(row=0, column=0)

Label(root, text='Ici c\'est le NW').grid(row=1, column=0)
Button(root, text='Bouton du NE').grid(row=1, column=1)
Label(root, text='Ici c\'est le SW').grid(row=2, column=0)
Button(root, text='Bouton du SE').grid(row=2, column=1)

for i in range(3):
    root.rowconfigure(i, weight=1)
for j in range(2):
    root.columnconfigure(j, weight=1)

root.mainloop()

Il est nécessaire de préciser à grid comment répartir l’espace supplémentaire pour qu’il puisse redimensionner la grille en même temps que la fenêtre. Ici, nous constatons que la grille s’est bien étirée de manière homogène (c’est-à-dire en gardant les proportions initiales), car nous avons attribué des poids égaux à toutes les lignes et colonnes. Il est possible de définir des poids différents pour que certaines lignes et colonnes prennent moins ou davantage de place que d’autres.

Le redimensionnement rend visible le fait que les widgets sont placés par défaut aux centres des cellules. On peut modifier cette disposition à l’aide d’une option de grid appelée sticky, grâce à laquelle on peut préciser contre quel(s) bord(s) de cellule placer le widget. Les bords sont repérés à l’aide de constantes (N, S, E, W, les points cardinaux, variables globales du module Tkinter), comme pour les valeurs du paramètre fill de pack. Voici un exemple :

from tkinter import Button, Label, Tk
from tkinter import E, N, S, W

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

Label(root, text='Bienvenue dans Tkinter').grid(row=0, column=0)

Label(root, text='Ici c\'est le NW').grid(row=1, column=0, sticky=N+E)
Button(root, text='Bouton du NE').grid(row=1, column=1, sticky=E)
Label(root, text='Ici c\'est le SW').grid(row=2, column=0, sticky=S+W+N)
Button(root, text='Bouton du SE').grid(row=2, column=1, sticky=W+E)

for i in range(3):
    root.rowconfigure(i, weight=1)
for j in range(2):
    root.columnconfigure(j, weight=1)

root.mainloop()

On constate bel et bien que non seulement les boutons sont déplacés vers les bords, mais aussi qu’on peut combiner les points cardinaux pour définir des coins de cellules et que si deux points cardinaux opposés sont combinés, cela a pour effet d’étirer le widget dans la direction concernée. Pour information, ces points cardinaux peuvent aussi être assignés sous forme de chaînes de caractères : par exemple ‘ne’ équivaut à N+E, ‘swe’ à S+W+E, etc.

Notons également qu’il est possible de demander à un widget de recouvrir plusieurs lignes et/ou colonnes :

from tkinter import Button, Label, Tk
from tkinter import E, N, S, W

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

Label(root, text='Bienvenue dans Tkinter').grid(row=0, column=0, columnspan=2, sticky='nsew')

Label(root, text='Ici c\'est le NW').grid(row=1, rowspan=2, column=0, sticky=N+E+S)
Button(root, text='Bouton du NE').grid(row=1, column=1, sticky=E)
Label(root, text='Ici c\'est le SW').grid(row=2, column=0, sticky=S+W+N)
Button(root, text='Bouton du SE').grid(row=2, column=1, sticky=W+E)

for i in range(3):
    root.rowconfigure(i, weight=1)
for j in range(2):
    root.columnconfigure(j, weight=1)

root.mainloop()

Voilà qui conclut notre tour d’horizon, bien sûr non-exhaustif, des gestionnaires de positionnement. Comme annoncé nous ne parlons pas de place, que nous n’utiliserons de toute façon pas par la suite, et il y a encore quelques options de configuration dont nous n’avons pas parlé, mais nous avons vu quasiment tout ce que nous utiliserons dans le Mastermind à venir et même plus.

4. Les conteneurs et nouvelles fenêtres

4.a Les conteneurs

Les conteneurs sont simplement des widgets invisibles mais dans lesquelles nous pouvons agencer d’autres objets d’une manière totalement indépendante de la fenêtre dans laquelle ils s’insèrent. À tel point qu’on changer de gestionnaire de positionnement sans aucun problème. Illustration avec le plus simple d’entre eux, l’objet Frame :

from tkinter import Button, Frame, Label, Tk

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

Label(root, text='Bienvenue dans Tkinter', bg='cyan').pack(fill='both', expand=True)

my_frame = Frame(root)
my_frame.pack(side='left')

Label(my_frame, text='Ici c\'est le NW', bg='red').grid(row=0, column=0)
Button(my_frame, text='Bouton du NE').grid(row=0, column=1)
Label(my_frame, text='Ici c\'est le SW', bg='red').grid(row=1, column=0)
Button(my_frame, text='Bouton du SE').grid(row=1, column=1)

root.mainloop()

Les quatre objets en bas à gauche de la fenêtre root appartiennet au Frame my_frame. Pour réaliser ceci, il suffit d’instancier la classe Frame et de passer l’instance en premier paramètre des widgets visés à la place de root. C’est l’isolation procurée par my_frame qui permet de passer de pack (utilisé pour le premier Label), à grid comme gestionnaire de positionnement.

Un exemple plus visible mais extrêmement proche est le widget LabelFrame qui permet de donner un titre à la zone occupée par le Frame :

from tkinter import Button, LabelFrame, Label, Tk

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

Label(root, text='Bienvenue dans Tkinter', bg='cyan').pack(fill='both', expand=True)

my_frame = LabelFrame(root, text="Partie isolée")
my_frame.pack(side='left')

Label(my_frame, text='Ici c\'est le NW', bg='red').grid(row=0, column=0)
Button(my_frame, text='Bouton du NE').grid(row=0, column=1)
Label(my_frame, text='Ici c\'est le SW', bg='red').grid(row=1, column=0)
Button(my_frame, text='Bouton du SE').grid(row=1, column=1)

root.mainloop()

4.b Créer de nouvelles fenêtres

Une nouvelle fenêtre peut être générée à tout moment en créant une instance de la classe Toplevel. Cette fenêtre sera tout de même rattachée à la fenêtre principale. Si on ferme la fenêtre principale, toutes les autres se fermeront automatiquement. Exemple :

from tkinter import Label, Tk, Toplevel

root = Tk()
root.title("Tuto Tkinter")
Label(root, text='Bienvenue dans Tkinter', bg='yellow').pack(fill='both', expand=True)

new_window = Toplevel(root)
new_window.title("Nouvelle fenêtre")
Label(new_window, text='Bienvenue dans la fenêtre secondaire', bg='green').pack(fill='both', expand=True)

root.mainloop()

Cette nouvelle fenêtre se comporte comme la fenêtre principale : elle pourra recevoir les mêmes widgets, avoir son propre gestionnaire de positionnement, définir des conteneurs pour isoler certaines parties, générer à son tour de nouvelles fenêtres, etc.

Il existe un certain nombre de fenêtres avec un style prédéfini, qu’on appellera volontiers des boîtes de dialogue, disponibles dans Tkinter sans passer par une quelconque extension. Elles sont situées dans des sous-modules de la bibliothèque. Montrons-en quelques-unes :

from tkinter import Label, Tk
from tkinter.messagebox import askquestion, askretrycancel, showerror, showinfo, showwarning
from tkinter.filedialog import askopenfile
from tkinter.colorchooser import askcolor

root = Tk()
root.title("Tuto Tkinter")
Label(root, text='Bienvenue dans Tkinter', bg='yellow').pack(fill='both', expand=True)

question = askquestion(title="Question", message="Aimez-vous Python ?")
retry_cancel = askretrycancel(title="RetryCancel", message="Une autre chance ?")

showerror(title="Oups", message="J'ai tout cassé")
showwarning(title="Attention", message="Ce message s'auto-détruira dans 2s")
showinfo(title="Scoop", message="La Terre aurait plutôt la forme d'une grosse patate. Coïncidence ?")

file = askopenfile()
print(file)
color = askcolor()
print(color)

root.mainloop()

Ce code affiche successivement les différentes boites de dialogues, car elles ne rendent la main qu’une fois que nous les avons quittées. Ainsi, on verra successivement apparaître :

question vaut ensuite ‘yes’ ou ‘no’.

retry_cancel vaut ensuite True ou False.

Ces 3 fenêtres ne retournent aucun résultat.

file est ensuite l’objet Python représentant le fichier ouvert, si on appuie sur « Open ».

color est ensuite un tuple (codes RGB, code couleur) si on appuie sur « OK »

Prochaine et dernière étape : comment coder le Mastermind !

Cette article vous est proposé par l’expertise Design & Code.