Quelques éléments sur Tkinter
Introduction
Tkinter est l'une des bibliothèques standards de Python pour
créer des applications graphiques. Elle est multi-plateformes. Il existe bien évidemment d'autres API, bibliothèques et
frameworks pour faire du développement graphique. Nous l'utilisons ici plus comme illustration rapide à déployer.
Par conséquent, les exemples fournies ici le sont dans l'unique but d'être rapidement opérationnel sur de simples cas de figure et non dans l'intention d'être complets et exhaustifs.
Démarrage et principe de fonctionnement
Démarrer une application Tkinter se fait avec quelques (genre trois) lignes de code : l'inclusion de la bibliothèque
avec import,
la création de la fenêtre principale (tkinter.TK()) et le démarrage de la boucle de gestion des événements (mainloop()).
Un exemple minimaliste est reproduit ci-dessous.
import tkinter
if __name__ == '__main__':
# Création de la fenêtre principale
root = tkinter.Tk()
# Addition d'un titre de fenêtre
root.title("Mon application")
# Démarrage effectif de l'application (boucle de gestion des événements)
root.mainloop()
# Le print ne s'exécute qu'une fois l'application graphique fermée et terminée
print("Message")
Le code est également téléchargeable ici.
La boucle d'événements mainloop()
Le point important à relever est la boucle d'événements. Il s'agit d'une boucle infinie, ne s'arrêtant que lorsque
l'application est explicitement terminée par une action (par exemple, la fermeture de la fenêtre). Tout code figurant
après mainloop() ne sera donc exécuté seulement après sa terminaison.
Son rôle est de gérér une pile d'événements et de les traiter de façon séquentielle. On parle de paradigme de Programation Événementielle. Toute interaction avec l'interface (frappe clavier, click ou mouvement de souris, apparition, affichage ou disparition d'éléments graphiques ...) peut être captée, horodatée et enregistrée dans la pile d'événements à traiter.
La fonction mainloop() consiste donc à prendre successivement et à l'heure prévue les événements enregistrés, de
vérifier quelle action est associée à l'événement, et d'exécution la fonction de gestion associée.
Le princpe de la Programation Événementielle consiste donc à identifier les événements pertinents à faire traiter par mainloop() et de coder les fonctions de traitement associés.
Ajout et placement de Widgets
Définition
Les Widgets sont les objets graphiques composant l'interface (zones de texte, menus, boutons, ...) et qui sont généralement les objets de base à travers desquels on interagit avec l'application. Des exemples de base de Widgets standards sont accessibles ici.
Les charactéristiques principales des Widgets sont :
- ils ont une forme et des attributs de forme (taille, couleur, ombre ...) ;
- ils sont positionnés à un endroit précis de l'interface graphique ;
- ils sont organisés de façon hiérarchique : un Widget a un parent - soit la fenêtre principale, soit un autre Widget dans lequel il est inclus.
Placer des Widgets dans l'interface graphique
Nous n'aborderons pas les différentes possibilités de gérer le placement et le comportement de position des Widgets. La question est assez complexe et hors objectifs de ce module.
Il est néanmoins important de savoir qu'il existe trois façons de gérer le placement des Widgets :
- le positionnement automatique (avec
pack()); - le positionnement sur grille régulière (avec
grid()); - le positionnement libre (avec
place()).
IMPORTANT ! Lors du développement d'une application vous ne devez pas mélanger les 3 modes de gestion dans une même fenêtre, faute de quoi des comportements erratiques peuvent apparaître.
Dans la mesure où le travail sur l'UX et l'esthétisme est accessoire dans ce module, nous prendrons l'approche pack().
Créer des Widgets
Créer des Widgets se fait en appelant leur constructeur et en lui fournissant a minima le parent auquel le rattacher (la fenêtre principale ou un autre Widget). Il existe ensuite de nombreux paramètres de taille, apparence ou de comportement. Il convient de consulter la documentation de chaque Widget pour bien le créer et le configurer.
Dans l'exemple ci-dessous on crée un Canvas rouge de 300x300 pixels. Les Canvas sont des zones de dessin.
import tkinter
if __name__ == '__main__':
# Création de la fenêtre principale
root = tkinter.Tk()
# Addition d'un titre de fenêtre
root.title("Mon application")
# Ajout d'un widget dans la fenêtre principale
myCanvas = tkinter.Canvas(root, bg="red", height=300, width=300)
# Noter que l'appel à `.pack()` (ou, par extension, `.grid()` ou `.place()`) est nécessaire
# pour que le widget soit effectivement positionné dans la fenêtre
#
# myCanvas.pack()
# Démarrage effectif de l'application (boucle de gestion des événements)
root.mainloop()
Le code est également téléchargeable ici.
Vous remarquerez que lorsque vous tenterez d'exécuter le code ci-dessous, le Canvas n'apparait pas. C'est normal.
La création d'un Widget est séparé de son placement (et affichage) dans l'interface.
Décommentez la ligne myCanvas.pack() qui place automatiquement le Widget dans l'interface, et exécuter
le code à nouveau pour observer le Canvas rouge.
L'exemple de code ci-dessous ajoute deux autres Widgets: un Label (zone de texte simple, non éditable) et un
Button (bouton cliquable). Comme prédédemment, tant que la méthode .pack() n'est pas appelée, le Widget n'apparaît
pas.
import tkinter
if __name__ == '__main__':
# Création de la fenêtre principale
root = tkinter.Tk()
# Addition d'un titre de fenêtre
root.title("Mon application")
# Ajout d'un widget dans la fenêtre principale
myCanvas = tkinter.Canvas(root, bg="white", height=300, width=300)
myCanvas.pack()
# Ajout d'un widget `Label` dans la fenêtre principale
lbl = tkinter.Label(root, text="Rien")
# Ajout d'un widget `Button` dans la fenêtre principale
btn = tkinter.Button(root, text="Bouton à cliquer")
# Noter que l'appel à `.pack()` (ou, par extension, `.grid()` ou `.place()`) est nécessaire
# pour que le widget soit effectivement positionné dans la fenêtre
#
# lbl.pack()
# btn.pack()
# Démarrage effectif de l'application (boucle de gestion des événements)
root.mainloop()
Le code est également téléchargeable ici.
Dessiner dans un Canvas
Dessiner dans un Canvas consiste simplement à créer des objets graphiques particuliers (liste disponible
ici) et de leur attribuer les coordonnées appropriées.
Dans l'example ci-dessous la fonction petitRectangle() crée aléatoirement un rectangle de taille 30x30 dans
le Canvas appelé cv passé en argument via la méthode cv.create_rectangle.
import tkinter
from random import randint
def randomRGBString():
"""
:return: une chaîne de caractères représentant une couleur RGB aléatoire
"""
return "#" + ("%06x" % randint(0, 16777215))
def petitRectangle(cv: tkinter.Canvas, nom: str = None):
"""
Dessine un petit rectangle de taille 30x30 dans un `Canvas` de couleur aléatoire et portant
potentiellement un `tag`.
:param cv: le `Canvas` dans lequel dessiner le rectangle
:param nom: le tag éventuel à associer au rectangle
:return: un objet de type Tkinter.Rectangle positionné aléatoirement dans `cv`
"""
x = 30 + randint(0, 270)
y = 30 + randint(0, 270)
return cv.create_rectangle(x, y, x + 30, y + 30, fill=randomRGBString(), tags=nom)
if __name__ == '__main__':
# Création de la fenêtre principale
root = tkinter.Tk()
# Addition d'un titre de fenêtre
root.title("Mon application")
# Ajout d'un widget dans la fenêtre principale
myCanvas = tkinter.Canvas(root, bg="white", height=300, width=300)
myCanvas.pack()
# Ajout d'un widget `Label` dans la fenêtre principale
lbl = tkinter.Label(root, text="Rien")
# Ajout d'un widget `Button` dans la fenêtre principale
btn = tkinter.Button(root, text="Bouton à cliquer")
lbl.pack()
btn.pack()
# Dessin de 2 rectangles aléatoires dans le `Canvas`
rectangle1 = petitRectangle(myCanvas)
rectangle2 = petitRectangle(myCanvas)
# Noter qu'ici `.pack()` n'est pas nécessaire.
# Il ne s'agit pas de widgets, mais de formes dessinées dans un widget donné.
# Démarrage effectif de l'application (boucle de gestion des événements)
root.mainloop()
Le code est également téléchargeable ici. Vous pouvez vous référer aux autres formes graphiques disponibles ici.
Associer des gestionnaires d'événements à des actions ou des objets
L'intérêt principal de la Programation Événementielle est surtout de rendre l'interface active en associant des actions ou traitements à des événements qui peuvent se produire.
Un événement est toute modification d'un état des données, une interaction avec l'interface graphique (clic, saisie, déplacement...) ou un modification dans l'affichage de l'interface (apparition, disparition, changement de taille...). L'un des modèles les plus répendus pour développer des interfaces événementielles est le MVC (Modèle-Vue-Contrôleur)
Ici, nous nous contenterons juste à expliquer comment associer des actions particulières à des événements bien définis.
Exemple simple
Nous prenons l'exemple prédécent et définissons une fonction handler() qui prend en argument un événement e.
handler() est un gestionnaire d'événement qui change aléatoirement la couleur de l'objet rectangle1 et qui change
le texte du label lbl en "Coucou".
On associe handler() au bouton btn avec l'instruction btn.config(command=handler). On observe qu'en exécutant le
code, des clics sur le bouton btn change effectivement la couleur du rectangle.
import tkinter
from random import randint
def randomRGBString(): pass # cf. exemple précédent
def petitRectangle(cv: tkinter.Canvas, nom: str = None): pass # cf. exemple précédent
def changerCouleur(canvas, objet):
"""
Change de façon aléatoire la couleur d'un objet dans un canvas
:param canvas: le `Canvas` auquel appartient l'objet
:param objet: l'objet à modifier
:return: None
"""
canvas.itemconfig(objet, fill=randomRGBString())
if __name__ == '__main__':
# Création de la fenêtre principale
root = tkinter.Tk()
# Addition d'un titre de fenêtre
root.title("Mon application")
# Ajout d'un widget dans la fenêtre principale
myCanvas = tkinter.Canvas(root, bg="white", height=300, width=300)
myCanvas.pack()
# Ajout d'un widget `Label` dans la fenêtre principale
lbl = tkinter.Label(root, text="Rien")
# Ajout d'un widget `Button` dans la fenêtre principale
btn = tkinter.Button(root, text="Bouton à cliquer")
lbl.pack()
btn.pack()
# Dessin de 2 rectangles aléatoires dans le `Canvas`
rectangle1 = petitRectangle(myCanvas)
rectangle2 = petitRectangle(myCanvas)
def handler(e=None):
"""
Gestionnaire d'événement qui change aléatoirement la couleur de l'objet `rectangle1`
et qui change le texte du label `lbl` en "Coucou"
:param e: l'événement à l'origine de l'exécution
:return: None
"""
if e:
print(e.widget)
print(e.x)
changerCouleur(myCanvas, rectangle1)
lbl.config(text="Coucou")
# Noter que les événements peuvent être ajoutés à la main à la boucle d'événements
# Ici, on crée une boucle infinie d'événements se répétant toutes les 1000 millisecondes.
# myCanvas.after(1000, handler,e)
# Association du gestionnaire d'événement au bouton `btn`
# On observe qu'à chaque clic sur le bouton, le `rectangle1 change de couleur
btn.config(command=handler)
# Démarrage effectif de l'application (boucle de gestion des événements)
root.mainloop()
Le code est également téléchargeable ici.
Il est important de remarquer que les gestionnaires d'événements ne doivent pas forcément être rattachés à des
événements, mais peuvent être ajoutés directement dans la boucle de gestion mainloop(). C'est ce qui se produit
lorsqu'on décommente l'instruction myCanvas.after(1000, handlerBouton,e).
Cette instruction a comme effet d'insérer dans la boucle de gestion de myCanvas l'exéction de handlerbutton(e)
après un délai de 1000ms. Par conséquent, un clic sur btn va provoquer le changement de couleur de rectangle1 puis
un rappel de la fonction handler 1000ms plus tard... et ainsi de suite, provoquant un effet de clignotement.
Question pourquoi observe-t-on une augmentation de la fréquence de clignotement lors d'un second clic sur btn?
Associer des handler à des événements précis
Dans l'exemple précédent on avait associé un gestionnaire d'événements à un Widget spécialement conçu pour être activé
(un bouton). Il est possible de configurer bien plus finement l'association de gestionnaires à des combinaisons
objet+événement.
Dans l'exemple ci-dessous, l'instruction myCanvas.tag_bind("rect", "<Button-1>", handler) associe le gestionnaire
handler à tous les objets de myCanvas ayant le tag "rect" lorsqu'ils reçoivent un clic gauche de souris
("<Button-1>").
De façon similaire, l'instruction root.bind("<Key>", handler) associe le gestionnaire handler() à n'impporte quelle
frappe de clavier "<Key>" dans la fenêtre principale root.
Une liste assez exhaustive des événements possibles est disponible ici.
import tkinter
from random import randint
def randomRGBString():
"""
:return: une chaîne de caractères représentant une couleur RGB aléatoire
"""
return "#" + ("%06x" % randint(0, 16777215))
def petitRectangle(cv: tkinter.Canvas, nom: str = None):
"""
Dessine un petit rectangle de taille 30x30 dans un `Canvas` de couleur aléatoire et portant
potentiellement un `tag`.
:param cv: le `Canvas` dans lequel dessiner le rectangle
:param nom: le tag éventuel à associer au rectangle
:return: un objet de type Tkinter.Rectangle positionné aléatoirement dans `cv`
"""
x = 30 + randint(0, 270)
y = 30 + randint(0, 270)
return cv.create_rectangle(x, y, x + 30, y + 30, fill=randomRGBString(), tags=nom)
def changerCouleur(canvas, objet):
"""
Change de façon aléatoire la couleur d'un objet dans un canvas
:param canvas: le `Canvas` auquel appartient l'objet
:param objet: l'objet à modifier
:return: None
"""
canvas.itemconfig(objet, fill=randomRGBString())
if __name__ == '__main__':
# Création de la fenêtre principale
root = tkinter.Tk()
# Addition d'un titre de fenêtre
root.title("Mon application")
# Ajout d'un widget dans la fenêtre principale
myCanvas = tkinter.Canvas(root, bg="white", height=300, width=300)
myCanvas.pack()
# Ajout d'un widget `Label` dans la fenêtre principale
lbl = tkinter.Label(root, text="Rien")
# Ajout d'un widget `Button` dans la fenêtre principale
btn = tkinter.Button(root, text="Bouton à cliquer")
lbl.pack()
btn.pack()
# Dessin de 2 rectangles aléatoires dans le `Canvas` (dont `rectangle2` avec le tag "rect")
rectangle1 = petitRectangle(myCanvas)
rectangle2 = petitRectangle(myCanvas, "rect")
def handler(e=None):
"""
Gestionnaire d'événement qui change aléatoirement la couleur de l'objet `rectangle1`
et qui change le texte du label `lbl` en "Coucou"
:param e: l'événement à l'origine de l'exécution
:return: None
"""
if e:
print(e.widget)
print(e.x)
changerCouleur(myCanvas, rectangle1)
lbl.config(text="Coucou")
# Noter que les événements peuvent être ajoutés à la main à la boucle d'événements
# Ici, on crée une boucle infinie d'événements se répétant toutes les 1000 millisecondes.
# myCanvas.after(1000, handler,e)
# Association du gestionnaire d'événement au bouton `btn`
# On observe qu'à chaque clic sur le bouton, le `rectangle1 change de couleur
btn.config(command=handler)
# Association du gestionnaire d'événement au clic de bouton gauche à tous les objets
# du Canvas portant le label "rect" (en occurrence `rectangle2`
# Observer que des clics sur `rectangle2` changent la couleur de `rectangle1`
# (tout comme les clics sur le bouton `btn`)
myCanvas.tag_bind("rect", "<Button-1>", handler)
# Association du gestionnaire d'événement à une frappe clavier quelconque n'importe où dans
# la fenêtre principale `root`
# Observer que des frappes clavier changent la couleur de `rectangle1`
# (tout comme les autre actions précédentes)
root.bind("<Key>", handler)
# Démarrage effectif de l'application (boucle de gestion des événements)
root.mainloop()
Le code est également téléchargeable ici.