Informations générales et transversales

\(\newcommand{\ds}{\displaystyle}\) \(\newcommand{\Frac}{\ds\frac}\) \(\renewcommand{\r}{\mathbb{ R}}\) \(\newcommand{\C}{\mathbb{ C}}\) \(\newcommand{\n}{\mathbb{ N}}\) \(\newcommand{\z}{\mathbb{ Z}}\) \(\newcommand{\Q}{\mathbb{ Q}}\) \(\newcommand{\N}{\mathbb{ N}}\) \(\newcommand{\n}{\mathbb{ N}}\) \(\newcommand{\ol}{\overline}\) \(\newcommand{\abs}[1]{\left| \,{#1} \right|}\) \(\newcommand{\pv}{\;;\;}\) \(\newcommand{\ens}[1]{\left\{ {#1} \right\}}\) \(\newcommand{\mens}[1]{\setminus\left\{ {#1} \right\}}\) \(\newcommand{\Par}[1]{\left({#1}\right)}\) \(\newcommand{\pe}[1]{\left\lfloor {#1} \right\rfloor}\)

Informations générales et transversales

L’écosystème Tkinter

Documentation de Tkinter

La documentation (au sens large) de Tkinter est très inégale.

Documentations de référence

La documentation réalisant le meilleur compromis entre fiabilité, lisibilité et exhaustivité est celle de Fredrik Lundh qui est un contributeur historique du module Tkinter. Depuis mai 2020, cette documentation n’est plus disponible sur le site de l’auteur. Dans ce fil de discussion, il été demandé à l’auteur s’il pouvait donner une date de remise en service mais, à ce jour, sans réponse de sa part. Toutefois le site a été archivé sur web archive. La documentation de effbot n’est pas mise à jour mais comme Tkinter évolue très peu, ce n’est pas vraiment gênant.

Sinon, l’unique document de référence et très complet qui existait jusqu’en 2019 a été archivé : Tkinter 8.5 reference: a GUI for Python (Shipman, 2013). Un autre lien, plus simplement accessible, est sur tkdocs ou encore . L’ancien lien vers l’université de New-Mexico n’est définitivement plus valide, comme expliqué ICI. On peut télécharger une version au format pdf ICI ou encore et très agréablement présentée (en Latex). Il traite aussi l’extension Ttk de Tkinter. Noter cependant que le document n’a pas été modifié depuis 2013 et il n’est pas parfaitement exhaustif. Par exemple, il ne décrit pas la classe Tk (et donc aucune de ses méthodes qui sont parfois indispensables), ne décrit pas la méthode pack (très courante d’emploi) et ne décrit que très peu la classe PhotoImage.

Il existe une traduction en français, soignée et très bien présentée : Tkinter pour ISN. L’auteur semble être Étienne Florent. La documentation ne traite pas du module Ttk et ne propose pas de version au format pdf.

Il n’existe pas de documentation officielle. La documentation du module Tkinter dans le document officiel est très incomplète et sommaire. Elle renvoie à d’autres documentations. Le code source officiel contient en revanche quelques codes de démonstration. L’incomplétude de cette documentation a été signalée à diverses occasions, par exemple ICI.

Pour une compréhension en profondeur de Tkinter, il faut se plonger dans le code-source Python du module Tkinter puis dans la documentation de Tcl-Tk, en particulier les deux liens suivants :

Les livres spécifiques

Il y a peu de livres spécifiques à Tkinter. En voici trois.

  • Bhaskar Chaudhary, Tkinter GUI Application Development Blueprints, (Packt, 2e édition, 2018) : livre très complet et bien organisé. Bien qu’il parte de zéro concernant la programmation graphique, la progression est assez rapide et le contenu dense voire très dense dans certains chapitres. Il contient la réalisation de nombreux projets, très variés, non triviaux et présentés avec progressivité : une boîte à rythmes, un jeu d’échecs, une application type Paint, etc. Les codes-source sont téléchargeables sur Github et sont fonctionnels quel que soit le système d’exploitation. Les applications sont accompagnées de copies d’écran et de schémas explicatifs. Les applications présentées ne sont pas minimalistes mais en jouant avec leur code source on peut beaucoup apprendre. En contre-partie, le livre traitant de beaucoup de méthodes et de widgets, il n’entre pas toujours dans les détails. Et il vaut mieux assez bien connaître Python, en particulier la programmation objets qu’il utilise à partir du chapitre 3. Un chapitre est consacré au traitement de la boucle événementielle, qui est un concept essentiel en pogrammation graphique. Il y a deux chapitre décrivant respectivement les widgets Canvas et Text.
  • Mark Roseman (2020) : Modern Tkinter. L’auteur est un bon connaisseur du language Tcl/Tk dont Tkinter dérive, il maintient le site TkDocs. La 3e édition est beaucoup plus fournie que les précédentes et décrit tous les aspects de Tkinter. Elle a été mise-à-jour pour Python 3.9. Le code source des exemples du livre est accessible sur ce dépôt. Dans un chapitre en début d’ouvrage, l’auteur expose, sur un exemple de programme typique, la problématique d’une interface graphique qu’il reprend dans le chapitre suivant. Il passe ensuite en revue, chapitre par chapitre, les différentes composantes de Tkinter. Il manque un traitement des threads sous Tkinter. Il y a un chapitre original montrant quelles personnalisations il serait possible d’apporter à l’interface graphique classique (IDLE) qui est écrite en Tkinter. Je recommande cet ouvrage à tout lecteur ayant une certaine maturité et qui cherche des informations fiables sur Tkinter.
  • Jason Briggs : Python for kids (no starch press, 2012). Livre très pédagogique et progressif. La 1re moitié du livre utilise Turtle et la seconde utilise Tkinter. L’ouvrage fournit des explications très méticuleuses avec commentaire de code, numérotation et surlignage des lignes de code. L’objectif de la partie sur Tkinter est de créer un petit jeu de plateforme avec des sprites. Le code source est téléchargeable sur le site de l’éditeur. Hélas, le livre souffre de quelques défauts. Le code n’est pas très pythonique, on dirait que l’auteur a traduit en Python des procédés venant du C ou de Java, il y a des complications ou des anomalies de codage dans des simples instructions if et la POO est assez factice. Mais surtout, comme expliqué dans un message sur StackOverflow, l’auteur utilise un procédé inadapté à Tkinter pour gérer la boucle de jeu : il crée sa propre mainloop qu’il temporise de quelques ms avec un appel à sleep et met à jour avec les méthodes update et update_idletasks.

On pourra aussi consulter Tkinter GUI Programming by Example édité chez Pack (2018) dont l’exposé est essentiellement basé sur la réalisation de projets.

Il existe également un mémento d’Alejandro Rodas de Paz, Tkinter GUI Application Development Cookbook datant de 2018. Des codes-types sont donnés et suivis d’explications. Toutefois, le livre est loin d’être exhaustif et ne parle parfois que des situations les plus simples. Noter un intéressant chapitre sur l’utilisation de threads sous Tkinter.

Tutoriels ou FAQ sur Internet

  • Zetcode: Tkinter tutorial. Propose un tutoriel gratuit de présentation de Tkinter et qui culmine avec le codage d’un snake. Il existe aussi une version plus complète mais payante. Le code source et les images du snake sont à récupérer sur une page Github. Fait le tour des possibilités de Tkinter ce qui permet de se donner un aperçu. Les exemples de widgets sont décontextualisés donc on ne voit pas forcément comme en agréger plusieurs. Le jeu snake proposé est fluide et jouable et le code montre bien comment utiliser les méthodes du canevas pour gérer les collisions ; toutefois la qualité du code du snake pourrait être meilleure (confusion entre boucle for et boucle while, absence de else, non-utilisation de booléens, utilisation non nécessaire de la bibliothèque tierce Pillow) et l’auteur ne semble pas connaître l’astuce pour faire un snake (inutile de décaler chaque bloc, il suffit de placer en tête de snake le dernier bloc en tenant compte de la direction de déplacement fournie par le joueur).
  • Python Tkinter By Example. Cet ouvrage qui n’est pas destiné à des débutants propose le codage de plusieurs applications assez avancées (par exemple une todo-list avec barres de défilement et base de données sqlite, un éditeur de texte avec coloration syntaxique et complétion, une application de traduction utilisant l’API Translation de Google).
  • Xavier Dupré. Exposé assez rapide avec quelques exemples et quelques concepts.
  • FAQ de developpez.com Tkinter pour ISN : contient quelques conseils, basés sur des exemples, à respecter par le public ISN qui veut débuter dans la création d’une interface graphique sous Tkinter.

Les livres généralistes

Comme Tkinter est le toolkit graphique par défaut de Python, de nombreux ouvrages généralistes le décrivent, avec plus ou moins de détails. Voici une sélection.

  • Gérard Swinnen, Apprendre à programmer avec Python 3 : le plus grand classique sur Python en langue française et sans équivalent en anglais. Livre au format pdf, téléchargeable sur le site de l’auteur (il existe une version imprimée chez Eyrolles). De nombreux programmes Tkinter sont proposés dont certains assez sophistiqués. Code de qualité. Le livre date de 2012.
  • Liang, Introduction to Programming Using Python 3 est assez bien fait du point de vue pédagogique et contient beaucoup d’exemples qui aident à débuter. Attention, le code n’est pas vraiment pythonnique, certaines pratiques ne sont pas à conseiller (usage récurrent de la fonction eval) et les animations ne sont pas codées de façon canonique (boucle infinie + méthode update au lieu d’utiliser la méthode after).
  • Lutz, Programming Python (OReilly, 2011): exposé très long (presque 400 pages). Assez complet et d’un niveau plutôt élevé.
  • Summerfield : Programming in Python 3, Prentice Hall (2011) : contient deux exemples complets mais le code est assez dense et ne s’adresse pas à des débutants.
  • Summerfield : Python in practice, Prentice Hall (2014) : utilise l’extension ttk, le code est très dense, il y a un chapitre assez court proposant un jeu complet.

Forums

  • OpenClassrooms, Python : Tkinter fait l’objet de nombreuses questions.
  • Stackoverflow : une mine de réponses, en particulier pour les questions délicates. Prêter une attention particulière aux interventions du membre nommé Bryan Oakley. Voir aussi celles d” A. Rodas qui a reviewé plusieurs livres sur Tkinter.

Logiciels écrits en Tkinter

L’écosystème de Tkinter semble très réduit. En février 2019, la page Wikipedia recensait encore cinq interfaces graphiques écrites avec cette bibliothèque mais désormais (vérifié en novembre 2021), plus aucune n’est mentionnée.

Le logiciel écrit en Tkinter le plus utilisé est probablement IDLE qui est l’IDE proposé par la distribution officielle de Python.

Une autre source d’utilisation courante de Tkinter est via Matplotlib dont les graphiques peuvent être générés dans un canevas Tkinter :

../../../_images/demo_matplotlib_tkinter.png

Il semblerait qu’il soit utilisé comme backend dans les installations par défaut sous Windows et Linux. Pour le savoir, lancer le code suivant :

from matplotlib import get_backend

print(get_backend())

et qui devrait afficher :

TkAgg

Il existe un éditeur de code Python, fort réussi, du nom de Thonny

../../../_images/thonny.png

qui a fait l’objet de quelques reportages et d’une discussion sur Reddit.

A lire des messages sur stackoverflow : tkinter applications, il semble toutefois que Tkinter soit aussi utilisé dans des projets « locaux » pour créer des interfaces graphiques.

Sur Github, on peut trouver une interface graphique pour un client FTP.

Il semble surtout que Tkinter soit utilisé dans un cadre d’apprentissage de Python ou de la programmation graphique.

Sur Reddit, un jeu 3D écrit en Tkinter, réalisé dans le cadre d’un projet de lycée, a été annoncé mais le code source n’est pas disponible.

La bibliothèque TCL/Tk écrite en langage C et qui sert de base à Tkinter est aussi utilisée pour gérer le graphisme en Ocaml.

Origine de Tkinter

La bibliothèque Tkinter trouve son origine dans le langage de programmation TCL. Ce langage dispose d’une extension nommée Tk et permettant de réaliser des interfaces graphiques. Tkinter est un binding de l’extension Tk. D’ailleurs, Tkinter signifie Tk interface. Python n’est pas le seul langage ayant réalisé un binding de Tk ; c’est aussi le cas des langages OCaml, Perl et Ruby.

Tk est lié à CPython par une extension écrite en C, dans le fichier _tkinter.c. La documentation de Python explique l’architecture de liaison de Tk. Le livre Fluent Python analyse la hiérarchie de classes définissant le module Tkinter (chapitre 12).

L’identité du créateur de Tkinter n’est pas claire. Selon Shipman, 2013, pdf, page 3, repris par Wikipedia, c’est Fredrik Lundh (qui a aussi écrit par exemple le moteur du module d’expressions régulières). La documentation et le code source officiels indiquent que les auteurs sont Steen Lumholt and Guido van Rossum ; Fredrik Lundh aurait adapté l’interface à Tk 4.2.

Selon le code source officiel, Steen Lumholt est l’auteur du code de _tkinter.c.

D’ailleurs, dans le forum comp.lang.python en 2001, Fredrik Lundh dit lui-même : I’m pretty sure Steen Lumholt wrote it. Guido has been hacking on it, and I’ve been hacking my way around it, but Steen did the original work.

Programmation par événements

La notion de programmation événementielle

Quand vous utilisez une interface graphique (par exemple, un traitement de texte), la plupart du temps il ne se passe rien : le programme attend que vous interveniez en écrivant quelque chose ou en cliquant sur un bouton. Vos interventions qui font réagir le programme sont ce qu’on appelle des événements. La surveillance des évenements (on dit parfois l” écoute) est réalisée par un programme qu’on appelle une boucle d’événements car c’est une boucle infinie qui scanne les événements et réagit à ceux-ci. Ce type de programmation est dite programmation événementielle ou asynchrone. Ce n’est pas propre aux interfaces graphiques, par exemple un serveur http sous Nodejs fonctionne ainsi.

En Tkinter, la boucle des événements est gérée par la méthode mainloop. Voici un exemple typique :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
from tkinter import *
from random import randrange

def tracer():
    cnv.delete("all")
    u, v = randrange(SIDE), randrange(SIDE)
    R=SIDE//4
    cnv.create_oval(u-R,v-R,u+R, v+R, fill='orange')

SIDE=400

root=Tk()
cnv=Canvas(root, width=SIDE, height=SIDE, background="ivory")
cnv.pack(side=LEFT)


bouton=Button(root, text="Tracer", command=tracer)
bouton.pack()

root.mainloop()

Typiquement, le programme ci-dessous agit en fonction des événements qu’il capture (des clics de souris) :

../../../_images/evenement_typique.gif

La plupart du temps, le programme ne fait rien, il est en attente d’événements provoqués par l’utilisateur. La méthode mainloop (ligne 20) surveille l’action de l’utilisateur. Si le bouton défini à la ligne 17 est cliqué, le programme est conçu pour réagir : la fonction tracer (lignes 4-8) est appelée (cf. ligne 17) et dessine un disque orange. Une fois le disque dessiné et affiché, le programme se remet en attente.

Particularités de la programmation événementielle

Ce qui suit peut être réservé à une seconde lecture.

Un programme événementiel a les deux particularités suivantes :

  • pendant l’exécution de la boucle d’événements, l’exécution du programme est confinée dans la boucle (donc du code qui viendrait après ne s’exécuterait pas, cf. ligne 11 ci-dessous);
  • la boucle principale ne doit pas être bloquée.

Voici un code qui illustre le premier point :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
from tkinter import *

SIDE=400

root=Tk()
cnv=Canvas(root, width=SIDE, height=SIDE, background="ivory")
cnv.pack(side=LEFT)

root.mainloop()

print("Bonjour !")

Tant que la fenêtre créé en ligne 5 n’est pas détruite (en cliquant sur la croix), le message de la ligne 11 n’apparaîtra pas car le programme est confiné à la ligne 9.

Concernant le blocage de la boucle événementielle : cette dernière doit d’abord surveiller la réalisation d’événements. Si le programme réalise une opération qui mobilise ses ressources (un calcul intensif par exemple), le programme ne peut plus écouter et il ne va donc plus réagir. Soit par exemple le programme suivant :

../../../_images/bloquer_mainloop.gif

Le code du programme (visible ci-dessous) est écrit pour que, au bout de 5 secondes après son ouverture (cf. ligne 30), il effectue un gros calcul (cf. lignes 13-14) qui va empêcher l’interface graphique de réagir et si on clique sur le bouton rien ne se passe. Juste avant le lancement du calcul (10 ms), un petit message annonce que l’interface va être bloquée (lignes 29 et 10-11). Une fois le calcul terminé, le programme redevient réactif, un message que le calcul est terminé (ligne 15) et le clic sur le bouton montre à nouveau le disque orange.

Ci-dessous, le code du programme :

from tkinter import *
from random import randrange

def tracer():
    cnv.delete("all")
    u, v = randrange(SIDE), randrange(SIDE)
    R=SIDE//4
    cnv.create_oval(u-R,v-R,u+R, v+R, fill='orange')

def msg():
    lbl["text"]="Occupé !"

def calcul():
    z=10**6000000
    lbl["text"]="Libre !"

SIDE=400

root=Tk()
cnv=Canvas(root, width=SIDE, height=SIDE, background="ivory")
cnv.pack(side=LEFT)

bouton=Button(root, text="Tracer", command=tracer)
bouton.pack()

lbl=Label(root, text="Libre !", font="Times 12 bold")
lbl.pack(pady=30)

root.after(4990, msg)
root.after(5000, calcul)

root.mainloop()

En particulier, sauf cas exceptionnel ou programme à visée pédagogique, une interface graphique en Tkinter n’utilise jamais la fonction sleep car elle bloque la boucle événementielle et empêche donc l’écoute des événements comme c’est expliqué dans de nombreux messages sur StackOverflow :

Création d’une fenêtre et d’un widget

Une application graphique de bureau s’exécute dans une fenêtre qui contient des widgets. Un widget est juste un composant graphique comme un menu ou un bouton. Voici un code « Hello Tkinter! » qui crée une fenêtre et un widget sous Tkinter :

fenetre_widget.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from tkinter import *

WIDTH=600
HEIGHT=400

my_root=Tk()
cnv=Canvas(my_root, width=WIDTH, height=HEIGHT, background='ivory')
cnv.pack()

my_root.mainloop()

ce qui ouvre l”« application » suivante

../../../_images/fenetre_tk1.png
  • Ligne 1 : on importe toutes les fonctionnalités de Tkinter même si on n’a pas besoin de toutes celles-ci. C’est pratique pour de petits programmes. On procède légèrement autrement dans des usages plus avancés.
  • Ligne 6 : on créé une fenêtre Tkinter en appelant le constructeur Tk, dite fenêtre « maîtresse » (master).
  • Ligne 7 : on crée un « canevas » dans la fenêtre : c’est un widget permettant d’effectuer du graphisme, des animations, etc.
  • Ligne 8 : on indique avec pack comment le widget doit être intégré dans le widget-maître (ici la fenêtre my_root).
  • Ligne 10 : on lance la « boucle principale » (mainloop) de l’application. C’est typique de la programmation événementielle.

Important

Pour créer un widget (ligne 7), on utilise le constructeur approprié, par exemple ici Canvas. Ensuite, on passe des arguments à ce constructeur :

  • le premier argument est le widget-maître qui va contenir le widget qu’on va construire. Par exemple, pour le canevas (ligne 7), le premier argument est la fenêtre my_root.
  • les autres arguments sont les options de construction du widget. Ces options sont nommées, c’est-à-dire de la forme option=truc, où option est, par exemple, une dimension comme width, une option de couleur telle background, etc.

Remarques sur l’écriture et l’exécution d’une interface graphique

  • Les interfaces graphiques sont construites par emboîtement de widgets comme des boutons, des entrées, des menus, etc. Le conteneur qui n’est contenu dans aucun autre conteneur est souvent référencé (ligne 6) par une variable appelée root, master, window, etc.
  • Il est courant de placer en début de programme des variables écrites en majuscules (lignes 3 et 4) et qui représentent des données invariables du programme, comme certaines dimensions, le chemin vers certaines resources, etc.
  • Si on retire la dernière ligne du programme (ligne 10), la fenêtre ne sera pas créée : l’application graphique tourne forcément dans la mainloop.
  • Si on écrit du code après la ligne contenant le lancement de la mainloop (ligne 10), il ne sera pas exécuté, à moins de tuer la fenêtre de l’application graphique.
  • Beaucoup d’éléments d’une application graphique sont des objets au sens de la programmation objet. Voilà pourquoi on utilise des notations pointées comme cnv.pack() (ligne 8 ou 10) pour faire des appels de méthodes.
  • La ligne 6 est indispensable : on est obligé d’indiquer explicitement que l’on construit une fenêtre racine.
  • La ligne 8 (ou un équivalent) est aussi indispensable : il faut indiquer au toolkit comment un widget est placé dans son contenant. Ici, le placement est effectué avec le gestionnaire pack.
  • Notre programme ici est en attente d’événements. Mais, comme il est minimal, le seul événement qu’il peut enregistrer est un clic de fermeture sur la croix de la fenêtre.
  • Programmer une interface graphique n’est pas incompatible avec la lecture de message dans la console produit avec la fonction print (ça permet de déboguer un minimum).

Comprendre le fonctionnement de la boucle principale

Ce qui suit sera réservé à une 2e lecture. Il contient essentiellement des références provenant toutes de StackOverFlow.

La mainloop de Tkinter est en fait une gigantesque boucle while infinie comme expliqué dans le message Python3, Understanding the GUI loop. Il est donc inutile d’utiliser sa propre boucle infinie appelant périodiquement la méthode update, comme expliqué dans ce message Tkinter understanding mainloop 1.

Si on utilise la fonction sleep dans une interface graphique Tkinter, l’interface ne répond plus. On n’utilise donc quasiment jamais sleep dans une application Tkinter. Les messages suivants détaillent ce point :

La méthode mainloop étant bloquante, dans certaines circonstances, il peut être envisagé de ne pas l’utiliser comme illustré dans Use TkInter without mainloop.

Les fenêtres

Le constructeur Tk

Tk est une classe définie dans le module tkinter et qui permet la création de la fenêtre-maîtresse de l’application :

../../../_images/bonjour_tk.png
1
2
3
4
5
6
from tkinter import Tk, Label

racine=Tk()
annonce=Label(height=5, width= 15, text="BONJOUR !", bg='ivory')
annonce.pack()
racine.mainloop()
  • Ligne 3 : construction de la fenêtre « racine » de l’application.
  • Ligne 4 : création d’un widget, ici un widget Label (un court texte).
  • Ligne 5 : placement du widget dans la fenêtre racine.
  • Ligne 6 : lancement de l’application graphique.

On dit que c’est un constructeur car un appel à Tk construit véritablement une fenêtre-maîtresse. En principe, on ne donne aucun argument à Tk. Les éléments de positionnement, comme le bord d’un widget par rapport au bord de la fenêtre principale sont définis par la méthode qui placera le widget dans la fenêtre-maîtresse et non cette dernière. Par exemple, le code ci-dessous place entre la fenêtre-maîtresse et le widget un bord de 20 pixels :

../../../_images/bonjour_tk_pad.png

Le code correspondant est :

1
2
3
4
5
6
from tkinter import Tk, Label

racine=Tk()
annonce=Label(height=5, width= 15, text="BONJOUR !", bg='ivory')
annonce.pack(padx=20, pady=20)
racine.mainloop()
  • Ligne 5 : le widget lui-même (non la fenêtre principale définie en ligne 3) gère le placement d’un widget dans la fenêtre qui le contient.

Bien qu’une fenêtre Tk ne puisse prendre aucun argument, on peut toutefois lui donner une couleur de fond (mais qui pourrait être caché par un widget) :

../../../_images/fen_couleur.png
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from tkinter import Tk, Label

racine=Tk()

racine['bg']="lavender"

annonce=Label(height=20, width= 20, text="BONJOUR !", font= "Times 20", bg='ivory')
annonce.pack(padx=10, pady=10)

racine.mainloop()
  • Ligne 5 : racine donne accès à un dictionnaire dont un des éléments est la chaîne bg (pour background) et reçoit une couleur Tkinter (ici couleur lavande).
  • Ligne 8 : les options de padding du label laissent découverte la fenêtre ce qui permet d’apercevoir sa couleur.

Titre de fenêtre

Par défaut, le bandeau d’une fenêtre porte le titre de tk. On peut changer ce titre par défaut en appelant la méthode title de Tk :

1
2
3
4
5
6
7
8
9
from tkinter import Tk, Label

racine=Tk()

racine.title("Ma fenêtre principale")

annonce=Label(height=5, width= 50, text="BONJOUR ! ", bg='ivory')
annonce.pack()
racine.mainloop()
../../../_images/ma_fenetre_principale.png

Fenêtre non redimensionnable

Dans de nombreuses situations, redimensionner la fenêtre n’a pas de sens car le contenu de la fenêtre n’est pas redessiné pour s’adapter aux nouvelles dimensions. Dans ce cas, il vaut mieux bloquer le redimensionnement, ce qui peut se faire comme ci-dessous :

1
2
3
4
5
6
7
8
9
from tkinter import Tk, Canvas

root = Tk()
cnv = Canvas(root, width=400, height=400)
cnv.pack()

root.resizable(False, False)

root.mainloop()
  • Ligne 7 : les dimensions de la fenêtre sont bloquées.
../../../_images/non-redimensionable.png

On observera qu” il n’y a plus d’icône pour modifier la taille de la fenêtre.

En réalité, le redimensionnement n’est pas absolu : la modification d’un widget interne à la fenêtre a une influence sur la taille de la fenêtre qui le contient.

La position de la fenêtre principale dans le bureau

Lorsqu’une application utilise une seule fenêtre, la position de cette fenêtre dans le bureau est codée dans une chaîne de caractères appelée la chaîne de géométrie de la fenêtre et à laquelle on accède par la méthode geometry (ligne 8 ci-dessous):

1
2
3
4
5
6
7
8
9
from tkinter import *

root = Tk()
cnv = Canvas(root, width=600, height=600, bg='ivory')
cnv.pack()

root.update_idletasks()
print(root.geometry())
root.mainloop()
10
300x200-5+40

Cette chaîne donne les informations suivantes concernant la fenêtre générée :

  • sa largeur est de 300 px,
  • sa hauteur est de 200 px
  • le bord droit de la fenêtre est à 5 px du bord droit du bureau,
  • le bord supérieur de la fenêtre est à 40 px du haut du bureau

La ligne root.update_idletasks() est parfois nécessaire pour initialiser certaines tâches (sinon, dans notre cas, le message est 1x1+0+0).

D’une manière générale, une chaîne de géométrie de fenêtre a la syntaxe générale suivante :

\(\mathtt{WxH\pm X\pm Y}\)

\(\mathtt{W, H, X, Y}\) désignent des mesures en pixels avec la signification suivante :

Lettre Signification
W Largeur de la fenêtre
H Hauteur de la fenêtre
X Distance à l’un des deux bords verticaux du bureau. Bord gauche du bureau si signe +. Bord droit du bureau si signe -.
Y Distance à l’un des deux bords horizontaux du bureau. Bord supérieur du bureau si signe +. Bord inférieur du bureau si signe -.

Le schéma ci-dessous explique les mesures :

../../../_images/chaine_geometrie.png

En particulier, on comprend que le couple (+X,+Y) représente les coordonnées du coin supérieur gauche de la fenêtre dans le repère habituel du bureau (origine en haut à gauche, abscisses = bord supérieur orienté vers la droite, ordonnées = bord gauche orienté vers le bas).

On peut modifier la position initiale de la fenêtre principale en changeant les paramètres que porte cette chaîne.

Par exemple, pour placer la fenêtre à 500 pixels des bords gauche et supérieur :

from tkinter import Tk, Canvas

root = Tk()
cnv = Canvas(root, width=400, height=400)
cnv.pack()

root.geometry("+500+500")

root.mainloop()

Pour centrer une fenêtre dont les dimensions sont données à l’avance, on peut utiliser le code suivant provenant d’un message sur StackOverFlow :

import tkinter as tk


root = tk.Tk() # create a Tk root window

w = 800 # width for the Tk root
h = 650 # height for the Tk root

# get screen width and height
ws = root.winfo_screenwidth() # width of the screen
hs = root.winfo_screenheight() # height of the screen

# calculate x and y coordinates for the Tk root window
x = (ws/2) - (w/2)
y = (hs/2) - (h/2)

# set the dimensions of the screen
# and where it is placed
root.geometry('%dx%d+%d+%d' % (w, h, x, y))

root.mainloop() # starts the mainloop

Modifier dynamiquement la géométrie de la fenêtre

La géométrie de la fenêtre peut changer en cours d’exécution :

../../../_images/expand-auto.gif
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from tkinter import Tk, Canvas

root = Tk()
cnv = Canvas(root, width=400, height=400)
cnv.pack()

def printgeom():
    root.geometry("600x600+200+50")
    print(root.geometry())

print(root.geometry())
cnv.after(2500, printgeom)

root.mainloop()
  • Ligne 12 : au bout de 2,5 s, la géométrie de la fenêtre est modifiée
  • Ligne 8 : modification de la géométrie qui est affichée.

Ouvrir plusieurs fenêtres

Le code suivant

1
2
3
4
5
6
7
from tkinter import *

root = Tk()
a = Toplevel(root, bg='red')
b = Toplevel(root, bg='blue')

root.mainloop()

va ouvrir 3 fenêtres : deux fenêtres et leur fenêtre maîtresse comme on le voit ci-dessous :

../../../_images/plusieurs_fenetres.png

Pour cacher la fenêtre maîtresse, utiliser withdraw :

1
2
3
4
5
6
7
8
from tkinter import *

root = Tk()
a = Toplevel(root, bg='red')
b = Toplevel(root, bg='blue')
root.withdraw()

root.mainloop()

ce qui produit :

../../../_images/cacher_root.png

Toutefois, comme la fenêtre est cachée, il n’y a plus moyen de fermer définitivement l’application puisque la croix de fermeture de l’application est invisible. On peut y remédier de la manière suivante :

from tkinter import *

def quit_a():
    a.destroy()
    if closed[1]:
        root.destroy()
    else:
        closed[0]=True

def quit_b():
    b.destroy()
    if closed[0]:
        root.destroy()
    else:
        closed[1]=True

root = Tk()
a = Toplevel(root, bg='red')
b = Toplevel(root, bg='blue')

a.protocol("WM_DELETE_WINDOW", quit_a)
b.protocol("WM_DELETE_WINDOW", quit_b)

closed=[False, False]

root.withdraw()

root.mainloop()

Résolution de l’écran

Il peut être utile parfois de coder une application graphique en fonction de la résolution de l’écran de l’hôte. Tkinter permet de récupérer ces informations sur la résolution :

1
2
3
4
5
6
7
8
9
from tkinter import Tk, Canvas

root = Tk()
cnv = Canvas(root, width=400, height=400)
cnv.pack()

print(root.winfo_screenwidth(), root.winfo_screenheight())

root.mainloop()
10
1920 1080
  • Ligne 7 : les deux dimensions de la résolution.

Le mode plein écran

On peut placer une fenêtre en mode plein écran avec l’option "fullscreen" qu’on place à True. La sortie du mode plein écran n’est pas prévue par défaut donc il faut l’écrire soit même en liant par exemple la touche Echap au retour de l’écran à sa position normale :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
from tkinter import *

SIDE = 1000


def normalscreen(event):
    master.attributes("-fullscreen", False)


master = Tk()
master.attributes("-fullscreen", True)

cnv = Canvas(master, width=SIDE, height=1.3 * SIDE, bg='ivory')
cnv.pack()

master.bind("<Escape>", normalscreen)

master.mainloop()

Appeler plusieurs fois Tk

On lit parfois dans du code de débutants plusieurs appels successifs au constructeur Tk afin de créer plusieurs fenêtres. Dans l’immense majorité des cas c’est inutile, comme expliqué dans ce message Why are multiple instances of Tk discouraged? (B. Oakley). Dans une discussion du forum OpenClassroom, le type d’inaccessibilité que provoque l’usage de plusieurs appels à Tk est illustré par un exemple. Voir aussi cette discussion où le même problème a été rencontré.

Si l’objectif est juste de créer plusieurs fenêtres, le faire en appelant le widget TopLevel :

from tkinter import *

root = Tk()
a = Toplevel(root, bg='red')
b = Toplevel(root, bg='blue')

root.mainloop()

Géométrie des widgets

Gestionnaires de géométrie

Une fois une fenêtre créée, il faut placer ses widgets à l’intérieur. Le placement de tout widget se fait obligatoirement par l’intermédiaire d’un gestionnaire de géométrie. Tkinter dispose de trois gestionnaires de géométrie : pack, grid et place.

Voici un résumé des principales caractéristiques de ces trois gestionnaires :

Nom du gestionnaire Commentaire Documentation
pack Empilements verticaux ou horizontaux. Le plus simple si interface simple. Complexe, peu robuste si interface complexe avec beaucoup de contraintes d’alignement. Nombreuses options pour affiner le placement. Lundh : pack
grid Place selon une grille 2D. Assez simple. Adapté si interface non triviale ou nécessitant des alignements et des regroupements. Nombreuses options pour affiner le placement. Seul gestionnaire expliqué dans la doc non officielle. Lundh : grid Tkinter ISN
place Place à une position définie en pixels ou en valeur relative. Usage spécialisé seulement. Lundh : place

Bien noter que dans une fenêtre conteneur donnée, on ne peut pas utiliser simultanément les gestionnaires pack et grid (une exception sera levée).

En pratique, tout placement de widget, disons w, dans une fenêtre, disons root, nécessite un appel de la forme root.geometrie(w)geometrie est le nom de l’un des trois gestionnaires ci-dessus.

Voici un exemple représentatif :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from tkinter import *

root=Tk()

cnv=Canvas(root, width=300, height=200, bg="orange")
cnv.pack()

bouton=Button(root, text="Pomme")
bouton.pack()

entree=Label(root, width='10', text="Kiwi")
entree.pack()

root.mainloop()
../../../_images/pack_typique.png

Les widgets sont incorporés dans la fenêtre root (ligne 3, la fenêtre initiale) à la ligne 9 et à la ligne 12 avec le gestionnaire pack.

Organiser des widgets, de façon transparente pour le programmeur, de manière souple et visuellement satisfaisante est une tâche complexe, remplie de calculs géométriques. L’intérêt des deux premiers gestionnaires est qu’ils nous en dispensent tout en assurant un placement satisfaisant.

Pour une description des gestionnaires et des conseils d’utilisation, consulter les messages suivants sur le forum StackOverflow :

Dans ce document, le gestionnaire place n’est pas décrit : les deux autres gestionnaires répondent, et répondent bien, à l’immense majorité des usages.

Attention que les gestionnaires grid et pack ne sont pas à fenêtre fixe : si un widget change de taille (par exemple, on ajoute ou on retranche du texte quelque part) alors toute la fenêtre contenante change de taille ce qui est souvent souhaité mais parfois aussi non souhaité.

Enfin, comme indiqué par Bryan Oakley, que ce soit la géométrie pack ou grid, par défaut les widgets contenants ou même n’importe quel widget va adapter ses dimensions pour faire tenir tout ce qu’il contient (et c’est le comportement souhaité dans 99,9 % des cas).

Le gestionnaire grid

Le gestionnaire grid permet d’organiser ses widgets selon une grille 2D. C’est un gestionnaire assez simple à utiliser et très efficace. Un cas typique d’utilisation est la réalisation d’un calendrier car il y a beaucoup d’alignements à gérer :

../../../_images/calendrier_tkinter.png

Un autre cas est la réalisation d’un quizz car il y a plusieurs widgets et qui en outre se placent naturellement en grille :

../../../_images/tables_multiplication_tkinter.png

Voici un exemple représentatif de code utilisant le gestionnaire grid :

from tkinter import *

root=Tk()

cnv=Canvas(root, width=300, height=200, bg="orange")
cnv.grid(row=0, column=0)

pomme=Button(root, text="Pomme")
pomme.grid(row=1, column=0)

kiwi=Label(root, width='10', text="Kiwi")
kiwi.grid(row=2, column=0)

begonia=Label(root, width='10', text="Begonia")
begonia.grid(row=1, column=1)

root.mainloop()

qui produit

../../../_images/grid_manager.png

La grille est implicite (il n’y a pas de traits séparateurs visibles). Et on ne définit pas la grille à l’avance : on donne aux widgets un placement dans la grille en utilisant les arguments nommés row (pour l’indice de ligne) et column (l’indice de colonne) et Tkinter se charge de comprendre combien de lignes et de colonnes il y aura. Les indices des cellules commencent à 0.

Les options du gestionnaire grid

On peut affiner le placement des widgets avec le gestionnaire grid. Les principales options sont les suivantes :

Option Caractéristiques Valeurs possibles Valeur par défaut
sticky Étirement du widget dans certaines directions nord, sud, etc pour remplir place restante dans la cellule Un ancre : N, E, W, etc CENTER
columnspan Répartir sur plusieurs colonnes consécutives Entier positif 1
rowspan Répartir sur plusieurs lignes consécutives Entier positif 1
padx Marge horizontale (en pixels) Entier positif 0
pady Marge verticale (en pixels) Entier positif 0

Gestionnaire grid : l’option span

Le gestionnaire grid permet d’étendre un widget sur plusieurs lignes ou colonnes consécutives :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from tkinter import *

root=Tk()

a=Label(root, text="A", bg='red', width=20, height=5)
a.grid(row=0, column=0)

b=Label(root, text="B", bg='lightblue', width=20, height=5)
b.grid(row=0, column=1)

x=Label(root, text="X", bg='gray', width=20, height=5)
x.grid(row=1, column=0,columnspan=2)

y=Label(root, text="X", bg='gray', width=20, height=5)
y.grid(row=0, column=2,rowspan=2)

c=Label(root, text="C", bg='lime', width=20, height=5)
c.grid(row=2, column=0)

d=Label(root, text="D", bg='orange', width=20, height=5)
d.grid(row=2, column=1)

root.mainloop()

qui produit

../../../_images/grid_span.png

Par exemple, ligne 11, on définit un label gris, qui affiche le texte X. Ce label s’étend sur 2 colonnes (à cause de l’option columnspan=2) et commence ligne 1 et colonne 0.

Gestionnaire grid : les options padx et pady

Les options padx et pady ajoutent de l’espace autour du widget.

Le dessin ci-dessous illustre le positionnement des marges padx et pady :

../../../_images/pad_grid.png

Voici un exemple d’utilisation :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
from tkinter import *

root=Tk()

a=Label(root, text="A", bg='red', width=20, height=5)
a.grid(row=0, column=0, padx=20)

b=Label(root, text="B", bg='lightblue', width=20, height=5)
b.grid(row=0, column=1, padx=50, pady=100)

c=Label(root, text="C", bg='lime', width=20, height=5)
c.grid(row=1, column=0)

d=Label(root, text="D", bg='orange', width=20, height=5)
d.grid(row=1, column=1)

root.mainloop()
../../../_images/pad_grid_fenetre.png

La distance padx est comptée sur l’axe des x (horizontal) et pady sur l’axe vertical. la marge padx semble affecter toute la colonne où cette option est placée : ci-dessus, par exemple, la widget a est affecté d’un padx=20 tandis que dans la même colonne, le widget c ne reçoit aucun padx déclaré et pourtant le widget est autant décalé que a.

Gestionnaire grid : l’option sticky

Partons de l’interface suivante :

from tkinter import *

root=Tk()

a=Label(root, text="A", bg='red', width=20, height=5)
a.grid(row=0, column=0)

b=Label(root, text="B", bg='lightblue', width=10)
b.grid(row=0, column=1)


c=Label(root, text="C", bg='lime',  height=5)
c.grid(row=1, column=0)

d=Label(root, text="D", bg='orange', width=5, height=2)
d.grid(row=1, column=1)

root.mainloop()

qui produit

../../../_images/avant_sticky.png

et modifions-la pour observer l’effet de l’option sticky :

from tkinter import *

root=Tk()

a=Label(root, text="A", bg='red', width=20, height=5)
a.grid(row=0, column=0)

b=Label(root, text="B", bg='lightblue', width=10)
b.grid(row=0, column=1, sticky=N)


c=Label(root, text="C", bg='lime',  height=5)
c.grid(row=1, column=0, sticky=E+W)

d=Label(root, text="D", bg='orange', width=5, height=2)
d.grid(row=1, column=1, sticky=NE)

root.mainloop()

qui produit

../../../_images/apres_sticky.png

Dans l’option sticky, on indique des directions nord, sud, etc mais écrites en anglais et abrégées en une seule lettre capitale, par exemple W pour la direction ouest. On peut aussi utiliser des combinaisons comme NE (pour nord est) ou encore N+E. L’effet de sticky est de coller le widget dans la direction indiquée, avec un éventuel effet de remplissage par étirement de la zone libre de la cellule. L’option N+E+S+W est de remplir toute la cellule.

Gestionnaire pack

Le gestionnaire pack est très direct d’utilisation et est très employé pour les interfaces très simples comportant peu de widgets et dont l’organisation n’est pas cruciale :

from tkinter import *

root=Tk()
cnv=Canvas(root, width=400, height=400, background="ivory")
btn = Button(root, text="Menu", bg="pink")
lbl=Label(root, text="Orange", bg="orange", width=15)

cnv.pack()
btn.pack()
lbl.pack()

root.mainloop()

ce qui produit

../../../_images/pack.png

et qui a pour effet d’empiler (pack) les widgets les uns sur les autres, bord à bord. Par défaut, on voit que les widgets sont alignés de haut en bas.

S’il s’agit d’obtenir des alignements précis, le gestionnaire pack est peu approprié et les placements ne sont pas toujours faciles à réaliser.

La méthode pack dispose en outre de plusieurs options pour affiner le placement des widgets. Les principales options sont les suivantes :

Option Caractéristiques Valeurs possibles Valeur par défaut
side Le côté de la zone restante sur lequel le widget va s’appuyer TOP, BOTTOM, RIGHT, LEFT TOP
fill Remplissage vertical ou horizontal X, Y, BOTH, NONE NONE
expand Expansion verticale ou horizontale dans la fenêtre maîtresse True, False False
anchor Ancrage du widget dans l’espace qui lui reste 9 ancres possibles : CENTER, N, NE, E, etc CENTER
padx Marge (en pixels) horizontale à l’extérieur et de part et d’autre du widget entier positif O
pady Marge (en pixels) verticale à l’extérieur et de part et d’autre du widget entier positif O

Gestionnaire pack : l’option side

Au fur et à mesure que des widgets sont placés avec le gestionnaire pack, une zone à remplir est redéfinie.

Quand on place l’option side à un widget, par exemple side=BOTTOM, cela signifie que le widget va être comme collé à la partie inférieure de l’espace restant. En particulier, une fois le widget placé, la partie inférieure du widget ne fera plus partie de l’espace restant.

Par défaut, un widget est placé avec l’option side=TOP et cela explique pourquoi, par défaut, les widgets sont empilés de haut en bas.

Voici une succession de trois placements (les numéros sont les numéros d’ordre d’apparition dans le code source) :

from tkinter import *

root=Tk()

Label(bg="red", width=30, height=2, text="1",
      font="arial 30").pack(side=BOTTOM)
Label(bg="purple", width=20, height=2, text="2",
      font="arial 30").pack()
Label(bg="green", width=40, height=2, text="3",
      font="arial 30").pack()
root.mainloop()

ce qui produit :

../../../_images/pack_side.png
  • Le widget rouge est collé en bas (side=BOTTOM).
  • Le widget mauve a un placement par défaut, donc il va se placer en haut (TOP) de la zone restante sonc au-dessus du widget rouge.
  • Le widget vert est lui aussi placé avec l’option par défaut side=TOP donc dans la partie supérieure de la zone restante. Le widget mauve étant collé tout en haut de la fenêtre, le vert ne peut pas s’y placer. La zone restante est coincée au-dessus du widget rouge et sous le widget mauve, ce qui explique le placement.

Faisons trois nouveaux placements :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from tkinter import *

root=Tk()

Label(bg="red", width=30, height=2, text="1", font="arial 30").pack(side=BOTTOM)
Label(bg="purple", width=20, height=2, text="2", font="arial 30").pack()
Label(bg="green", width=40, height=2, text="3", font="arial 30").pack()
Label(bg="pink", width=15, height=2, text="4", font="arial 30").pack(side=RIGHT)
Label(bg="orange", width=25, height=2, text="5", font="arial 30").pack(side=LEFT)
Label(bg="lightblue", width=25, height=2, text="6", font="arial 30").pack()
Label(bg="lavender", width=5, height=2, text="7", font="arial 30").pack(side=RIGHT)
root.mainloop()

ce qui donne :

../../../_images/pack_side_plus.png
  • Ligne 8 : le widget rose n°4 est placé à droite donc collé à droite de la fenêtre et sous les widgets n°2 et 3
  • Ligne 9 : le widget orange n°5 est placé à gauche donc collé à gauche de la fenêtre et sous les widgets n°2 et 3
  • Ligne 10 : le widget bleu clair n°6 est placé en haut de la zone restante donc collé sous le widget n°3
  • Ligne 11 : le widget lavande n°7 est placé à droite donc collé à gauche du widget n°4 (qui lui était collé à droite de la fenêtre).

On remarque que la progression se fait de l’extérieur vers l’intérieur.

Pour placer côte à côte de la gauche vers la droite, utiliser l’option side=LEFT : chaque widget sera placé à gauche du précédent. Exemple :

from tkinter import *

root=Tk()

Label(bg="red", width=5, height=2, text="1", font="arial 30").pack(side=LEFT)
Label(bg="purple", width=5, height=2, text="2", font="arial 30").pack(side=LEFT)
Label(bg="green", width=5, height=2, text="3", font="arial 30").pack(side=LEFT)

root.mainloop()

ce qui donne :

../../../_images/pack_left.png

Gestionnaire pack : l’option fill

L’option fill permet d’étendre le widget dans sa zone de placement. On donne l’option sous la fome fill=v ou v vaut

  • X pour une expansion dans le sens horizontal,
  • Y dans le sens vertical
  • NONE pour une absence d’expansion (valeur par défaut)
  • BOTH pour une expansion dans toutes les directions

Par exemple, le code suivant

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from tkinter import *

root=Tk()
cnv=Canvas(root, width=400, height=400, background="ivory")
lbl1=Label(root, text="Orange", bg="orange", width=15)
lbl2=Label(root, text="Orange", bg="red", width=15)

cnv.pack()
lbl1.pack(fill=X)
lbl2.pack()

root.mainloop()

produit :

../../../_images/pack_fill.png

Le label orange a été élargi à sa gauche et à sa droite, le label rouge lui n’est pas étendu.

Gestionnaire pack : l’option expand

L’option expand permet d’étendre le widget par rapport à la fenêtre maîtresse. On donne l’option sous la fome expand=v ou v vaut True ou False. Par défaut, l’option est placée à False. S’il ne reste pas de place dans la fenêtre maîtresse, l’option est sans action.

Voici un exemple :

1
2
3
4
5
6
7
from tkinter import *
root=Tk()

Label(root, text='Label 1', bg='green', height=10).pack(side='left')
Label(root, text='Label 2', bg='lightblue').pack(fill=Y, expand=False)

root.mainloop()

produit :

../../../_images/expand_false.png

Malgré l’option fill=Y (ligne 5), l’espace disponible dans la fenêtre (et fourni à cause de la hauteur du label vert ligne 4) n’est pas utilisé.

Si on place l’option expand à True :

from tkinter import *
root=Tk()

Label(root, text='Label 1', bg='green', height=10).pack(side='left')
Label(root, text='Label 2', bg='lightblue').pack(fill=Y, expand=True)

root.mainloop()

le résultat est différent, l’espace disponible dans la fenêtre est utilisé :

../../../_images/expand_true.png

Gestionnaire pack : les options padx et pady

Quand on place un widget W dans un conteneur C grâce au gestionnaire pack, il y a la possibilité de placer une « marge » autour de W dans C : cela s’obtient avec les arguments nommés padx et pady :

1
2
3
4
5
6
from tkinter import Tk, Canvas

root=Tk()
cnv=Canvas(root, width= 300, height=200, bg="ivory")
cnv.pack(padx=150, pady=100)
root.mainloop()
../../../_images/pad_pack.png

padx=150 désigne une marge horizontale de 150 pixels à gauche et à droite du canevas dessiné. Et pady=100 désigne une marge verticale de 100 pixels en haut et en bas du canevas

L’algorithme de placement pack

L’algorithe complet de placement de pack est assez complexe lorsqu’on prend en compte toutes les options possibles. Dans les documentations, les descriptions de l’algorithme sont assez rares.

Une description très minutieuse en est faite dans la documentation officielle de la bibliothèque Tk : the packer algorithm.

Une description plus lisible mais moins détaillée est fournie par Fredrik Lundh qui suggère d’imaginer un domaine fermé entouré par une frontière élastique et que l’on va remplir par des widget en s’appuyant sur un bord du domaine élastique.

Pour mieux observer les placements opérés par le gestionnaire pack, voici le résultat d’une interface graphique générée automatiquement et aléatoirement et qui utilise les différentes options possibles :

../../../_images/random_pack.png

Le code source correspondant est :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
from tkinter import *

fen=Tk()

Label(fen,
      text='order=1\ndir=LEFT\nfill=NONE\nanchor=n\nexpand=1\npadx=2 pady=5',
      font='Arial 10 bold', bg='light sky blue', justify=LEFT).pack(
          side=LEFT, padx=2, pady=5, fill=NONE, anchor='n', expand=1)
Label(fen,
      text='order=2\ndir=RIGHT\nfill=Y\nanchor=se\nexpand=1\npadx=4 pady=2',
      font='Arial 10 bold', bg='ivory', justify=LEFT).pack(
          side=RIGHT, padx=4, pady=2, fill=Y, anchor='se', expand=1)
Label(fen,
      text='order=3\ndir=TOP\nfill=X\nanchor=sw\nexpand=0\npadx=6 pady=4',
      font='Arial 10 bold', bg='orange', justify=LEFT).pack(
          side=TOP, padx=6, pady=4, fill=X, anchor='sw', expand=0)
Label(fen,
      text='order=4\ndir=RIGHT\nfill=NONE\nanchor=sw\nexpand=1\npadx=4 pady=4',
      font='Arial 10 bold', bg='orange', justify=LEFT).pack(
          side=RIGHT, padx=4, pady=4, fill=NONE, anchor='sw', expand=1)
Label(fen,
      text='order=5\ndir=LEFT\nfill=X\nanchor=nw\nexpand=0\npadx=11 pady=4',
      font='Arial 10 bold', bg='cyan', justify=LEFT).pack(
          side=LEFT, padx=11, pady=4, fill=X, anchor='nw', expand=0)
Label(fen,
      text='order=6\ndir=LEFT\nfill=X\nanchor=sw\nexpand=1\npadx=2 pady=3',
      font='Arial 10 bold', bg='salmon', justify=LEFT).pack(
          side=LEFT, padx=2, pady=3, fill=X, anchor='sw', expand=1)
Label(fen,
      text='order=7\ndir=TOP\nfill=Y\nanchor=w\nexpand=1\npadx=10 pady=3',
      font='Arial 10 bold', bg='red', justify=LEFT).pack(
          side=TOP, padx=10, pady=3, fill=Y, anchor='w', expand=1)
Label(fen,
      text='order=8\ndir=RIGHT\nfill=X\nanchor=se\nexpand=1\npadx=9 pady=2',
      font='Arial 10 bold', bg='red', justify=LEFT).pack(
          side=RIGHT, padx=9, pady=2, fill=X, anchor='se', expand=1)
Label(fen,
      text='order=9\ndir=TOP\nfill=BOTH\nanchor=se\nexpand=0\npadx=8 pady=4',
      font='Arial 10 bold', bg='salmon', justify=LEFT).pack(
          side=TOP, padx=8, pady=4, fill=BOTH, anchor='se', expand=0)
Label(fen,
      text='order=10\ndir=LEFT\nfill=BOTH\nanchor=w\nexpand=1\npadx=8 pady=5',
      font='Arial 10 bold', bg='orange', justify=LEFT).pack(
          side=LEFT, padx=8, pady=5, fill=BOTH, anchor='w', expand=1)
Label(fen,
      text='order=11\ndir=RIGHT\nfill=NONE\nanchor=sw\nexpand=1\npadx=2 pady=4',
      font='Arial 10 bold', bg='brown', justify=LEFT).pack(
          side=RIGHT, padx=2, pady=4, fill=NONE, anchor='sw', expand=1)
Label(fen,
      text='order=12\ndir=TOP\nfill=NONE\nanchor=center\nexpand=0\npadx=4 pady=3',
      font='Arial 10 bold', bg='green', justify=LEFT).pack(
          side=TOP, padx=4, pady=3, fill=NONE, anchor='center', expand=0)
Label(fen,
      text='order=13\ndir=BOTTOM\nfill=BOTH\nanchor=nw\nexpand=0\npadx=4 pady=3',
      font='Arial 10 bold', bg='blue', justify=LEFT).pack(
          side=BOTTOM, padx=4, pady=3, fill=BOTH, anchor='nw', expand=0)
Label(fen,
      text='order=14\ndir=TOP\nfill=NONE\nanchor=w\nexpand=1\npadx=9 pady=5',
      font='Arial 10 bold', bg='ivory', justify=LEFT).pack(
          side=TOP, padx=9, pady=5, fill=NONE, anchor='w', expand=1)

fen.mainloop()

Ce code crée une suite de labels, par exemple, lignes 29-32, le label n° 7. Des options aléatoires sont fournies à la méthode pack, par exemple, pour le label n°7, regarder ligne 32. Le label rend visibles ces options, cf. ligne 30 pour le label n°7.

Bien comprendre que le remplissage se fait de manière successive et va de l’extérieur vers l’intérieur. Ainsi, side=RIGHT va placer le widget courant dans la partie droite de l’intérieur de la zone restante. Par ailleurs, l’ajout d’un widget peut modifier le placement relatif des précédents.

Un fichier Python a servi à générer le code ci-dessus dont voici le code :

from random import randrange

COLORS=["red", "blue", "gray", "orange", "brown",
        "magenta", "green", "salmon", "ivory",
        "cyan", 'sky blue', 'light sky blue']
DIR=["RIGHT", "LEFT", "TOP", "BOTTOM"]

cnv = """\
Label(fen,
      text='%s',
      font='Arial 10 bold',
      bg='%s', justify=LEFT).pack(
      side=%s, padx=%s, pady=%s, fill=%s, anchor='%s', expand=%s)"""

FILL=["NONE", "NONE", "BOTH", "X", "Y"]
ANCHOR="NW N NE E SE S SW W CENTER".lower().split()
JUSTIFY="LEFT CENTER RIGHT".split()
EXPAND="0 1".split()

L=[]

for  i in range(30):
    b=COLORS[randrange(len(COLORS))]
    c= DIR[randrange(4)]
    d=FILL[randrange(len(FILL))]
    e=ANCHOR[randrange(len(ANCHOR))]
    f="%s" %JUSTIFY[randrange(len(JUSTIFY))]
    x=EXPAND[randrange(2)]

    padx=randrange(2,12)
    pady=randrange(2,6)
    options=(r"order=%s\ndir=%s\nfill=%s\n"\
              r"anchor=%s\nexpand=%s\npadx=%s pady=%s")
    a= options%(i+1, c, d, e, x, padx, pady)
    L.append(cnv %(a,b,c,padx, pady, d,e, x))

code=["from tkinter import *"]
for i in range(1,15):
    code.append("# Fenêtre n°%s" %i)
    code.append("fen=Tk()\n")
    code.append("\n".join(L[:i]))
    code.append("#------------------------------------\n\n")
code.append("fen.mainloop() ")

open("demo_pack.py", "w").write('\n'.join(code))

Ce code génère un fichier de démonstration demo_pack.py. L’exécution de ce fichier crée un certain de nombre de fenêtres (ici 14) chacune utilisant la méthode pack. La fenêtre visible ci-dessus est l’une d’entre.

Centrer un widget et méthode pack

Soit l’interface suivante :

../../../_images/centrer_pack.png

où le widget de droite n’est pas verticalement centré. Le code est :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
from tkinter import *

root=Tk()

cnv=Canvas(root, width=300, height=200, bg="orange")
cnv.pack(side='left')

bouton=Button(root, text="Pomme")
bouton.pack()

root.mainloop()

L’option side=left de la ligne 6 assure que les widgets sont côte à côte. Le 2e widget est placé par défaut avec l’option side=TOP. Cela explique qu’il ne soit pas centré. Pour obtenir un centrage, il suffit de placer l’option side="left" ou side="right" :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
from tkinter import *

root=Tk()

cnv=Canvas(root, width=300, height=200, bg="orange")
cnv.pack(side='left')

bouton=Button(root, text="Pomme")
bouton.pack(side="left")

root.mainloop()

ce qui produit :

../../../_images/centrer_pack_ok.png

Placer en grille avec la méthode pack

La méthode pack n’est pas adaptée à présenter une interface dont les widgets sont placés en grille.

Typiquement, soit à réaliser le motif suivant

../../../_images/grid_grid.png

où un canevas est entouré, en bas et à droite de barres de défilement.

Ce motif se réalise très facilement avec la méthode grid:

from tkinter import *

root=Tk()

Canvas(root, bg="ivory").grid(row=0, column=0)
Scrollbar(root, orient="vertical").grid(row=0, column=1, rowspan=2, sticky=NS)
Scrollbar(root, orient="horizontal").grid(row=1, column=0, sticky=EW)

root.mainloop()

Avec la méthode pack, c’est moins immédiat. On peut se dire qu’on empile le canevas sur la barre de défilement horizontale puis on place à droite ou gauche la barre de défilement vertical :

from tkinter import *

root=Tk()

Canvas(root, bg="ivory").pack()
Scrollbar(root, orient="horizontal").pack(fill=X)
Scrollbar(root, orient="vertical").pack(side=LEFT, fill=Y)

root.mainloop()

mais voilà ce que ça donne :

../../../_images/grid_pack_ko.png

et ça ne marche pas non plus si side=RIGHT.

Une méthode qui fonctionne est la suivante :

from tkinter import *

root=Tk()

Scrollbar(root, orient="vertical", command=None).pack(side=RIGHT, fill=Y)
Canvas(root, bg="ivory").pack()
Scrollbar(root, orient="horizontal", command=None).pack(fill=X)

root.mainloop()

qui produit :

../../../_images/grid_pack_ok.png

Quelques techniques générales

Version de Tkinter utilisée

Pour connaître la version de Tk que vous utilisez, tapez dans une console le code suivant :

1
2
3
4
>>> from tkinter import *
>>> TkVersion
8.6
>>>

Le code ci-dessus est obtenu pour les versions 3.4 à 3.7 de Python.

Tk a évolué assez sensiblement à sa version 8.5 (au moins 2003) : thèmes natifs sous Windows et Mac Os X, création de nouveaux widgets, antialising sous Linux pour le fenêtrage X-11, cf. Tcl/Tk 8.5

La version 8.6 de Tk (à partir de 2008) se distingue de la version 8.5 en ce qu’elle supporte désormais le format d’image png, cf. Tcl/Tk 8.6. En 2019, la dernière version de Tk est la version 8.6.9 et il existe une version alpha 8.7.

Certaines installation sous Mac Os X ont parfois des retards d’installation des dernières versions de Tk, cf. IDLE and tkinter with Tcl/Tk on macOS. À partir de la version 3.7 de Python, il semblerait que la version de Tcl/Tk qui sert de base à Tkinter soit 8.6, laquelle prend en charge le format png, cf. par exemple le bug tracker.

Importer tkinter

D’abord, le nom du module sous Python 3 est tkinter en minuscule. Le nom Tkinter s’applique à Python 2 et ne sera pas valide sous Python 3 (rappel : Python 2 n’est plus maintenu en 2020).

En Python, d’une manière générale, il n’est pas recommandé d’importer un module avec la syntaxe suivante :

from this_module import *

car cela peut créer des collision de noms. Concernant l’importation de Tkinter par :

from tkinter import *

elle est souvent déconseillée. Elle ajoute à l’espace de noms de Python environ 140 noms dont certains relativement communs comme

N       |      TOP      |      WORD      |      BOTTOM
E       |      YES      |      enum      |      Button
S       |      sys      |      Entry     |      CENTER
W       |      CHAR     |      FALSE     |      INSIDE
X       |      Grid     |      Image     |      NORMAL
Y       |      LEFT     |      Label     |      Widget
NO      |      Menu     |      PAGES     |      Message
ON      |      Misc     |      Place     |      VERTICAL
re      |      NONE     |      RIGHT     |      mainloop
ALL     |      Pack     |      ROUND     |      constants
END     |      TRUE     |      SOLID
OFF     |      Text     |      UNITS

et que l’on risque d’écraser accidentellement. Par ailleurs, certains considèrent que l’usage libre des noms de la bibliothèque nuit à la lisibilité du code (car on ne sait pas quel nom provient de Tkinter et quel nom provient d’un autre module).

Les programmes Tkinter peu élaborés importent peu de noms, donc ce genre de programme pourrait se contenter d’une importation du type

from tkinter import Tk, Canvas, Button

si par exemple vous avez juste besoin d’une fenêtre Tk, des widgets Canvas et Button.

Toutefois, des besoins plus importants nécessiteraient parfois d’importer beaucoup plus de noms.

Certains utilisateurs et connaisseurs de Tkinter proposent d’écrire plutôt :

import tkinter

ce qui oblige à systématiquement préfixer, par exemple tkinter.Canvas et impose des noms relativement long ; d’où la simplification suivante

import tkinter as tk

ce qui permet d’accéder au Canvas avec la syntaxe assez légère tk.Canvas. C’est ainsi que procède le module turtle dans CPython.

Enfin, il faut relativiser ces mises-en-garde : beaucoup de code, y compris du code-source disponible dans CPython (l’implémentation officielle et largement majoritaire de Python), utilise, à des degrés divers, une importation de type from tkinter import *. Par exemple, c’est le cas de la plupart des modules de IDLE, comme le fichier editor.py. Pour du code destiné à l’apprentissage, du code de petits jeux à caractère pédagogique, ou pour des essais de fonctionnalités, ce type d’importation présente peu d’inconvénients et présente même des avantages pour le débutant.

Le codage des couleurs sous Tkinter

On est amené à utiliser des couleurs sous Tkinter dans de nombreuses situations, par exemple, donner une couleur de fond à un bouton ou à un canevas. Il existe deux codages des couleurs sous Tkinter :

  • nom de couleur : les couleurs standard du html, peuvent être appelées par leur nom, typiquement des noms courants red ou d’autres comme ivory ;
  • codage hexadécimal : on fournit une chaîne hexadécimale RGB commençant par le caractère # ; il existe plusieurs formes, la plus simple étant un code à 3 chiffres hexadécimaux, du type "#5fc" où chacune des trois composantes R, G et B est représentée par un seul chiffre hexadécimal parmi les caractères "0", "1", etc jusqu’à "F" (on peut aussi utiliser des lettres minuscules pour les lettres hexadécimales). Ce codage permet de représenter \(16^3=4096\) couleurs.

Ainsi, typiquement, on pourra rencontrer des codes comme :

1
2
annonce=Label(height=5, width= 15, text="BONJOUR !", bg='ivory')
cnv=Canvas(root, width=200, height=200, bg="#5fc")

Le codage hexadécimal permet aussi d’utiliser deux chiffres hexadécimaux par couleur ; par exemple, la couleur turquoise sera codée par "#40e0d0". On peut même utiliser trois chiffres hexadécimaux par composante RGB.

On trouvera un très pratique nuancier par nom de couleurs HTML dans le message Colour chart for Tkinter and Tix Using Python sur StackOverFlow qui produit le résultat suivant :

../../../_images/named_color_chart.png

Nuancier Tkinter (cliquer pour voir les noms de couleurs).

Le code Tkinter produisant ce nuancier est disponible dans le message.

A toutes fins utiles, voici une fonction renvoie une chaîne de couleur Tkinter depuis un triplet (r, g, b) :

def rgb_10to16(r,g,b):
    return "#%.02x" %r + "%.02x" %g + "%.02x" %b

# Royal Fuschia
print(rgb_10to16(202, 44, 146))
#ca2c92

Pour convertir depuis ou vers du format HSV, utiliser le module standard colorsys.

Donner un canal alpha à un objet

Le canvas de Tkinter n’accepte pas de canal alpha, cf. ce message sur stackoverflow.

Les polices

Dans un label, un bouton, ou encore pour écrire du texte dans un canevas, on dispose de l’option font permattant de définir une fonte. Elle admet deux syntaxes :

font="Times 12 bold"
font=("Times", 12, "bold")

La 2e est utile si le nom de la police contient des espaces.

Voici un exemple d’utilisation (parmi d’autres) :

from tkinter import *
root=Tk()

Label(root, text="ABCDEFGHIJKLMNOPQRSTUVWXYZ",
      font=('Arial', 12)).pack(side=TOP, anchor="w")
Label(root, text="ABCDEFGHIJKLMNOPQRSTUVWXYZ",
      font=("Times New roman", 12)).pack(side=TOP, anchor="w")
Label(root, text="ABCDEFGHIJKLMNOPQRSTUVWXYZ",
      font=("Times New roman", 12)).pack(side=TOP, anchor="w")
Label(root, text="ABCDEFGHIJKLMNOPQRSTUVWXYZ",
      font=("Courier", 18)).pack(side=TOP, anchor="w")
Label(root, text="01234567890123456789012345",
      font=("Courier", 18)).pack(side=TOP, anchor="w")

root.mainloop()

qui affiche

../../../_images/exemples_polices.png

Exemples de polices

Catégorie de polices

Catégorie Remarque Exemples Remarques
Monospace Chaque lettre a même largeur. Écrire du code. Alignements verticaux. Courier, Deja Vu Sans Mono, Monaco, Consolas, Fixedsys. Courier : laide dit-on Monaco : vient de mac, moins laide que courier, pas toujours mono.
Sans empâtement Moins ornée. Adaptée aux écrans. Arial, Deja Vu Sans, Helvetica, Ubuntu, Verdana Arial et Helvetica : très proches. Helvetica : commune. Verdana : Microsoft.
À empâtement   Times New Roman, Palatino, Très courant

Pour obtenir la liste des polices utilisables :

1
2
3
from tkinter import font, Tk
Tk()
print(*font.families(), sep='\n')

Fontes disponibles sous Ubuntu

Ci-dessous, quelques fontes disponibles sous une Ubuntu de base :

Century Schoolbook L
Ubuntu
DejaVu Sans Mono
Dingbats
DejaVu Sans
DejaVu Serif
Courier 10 Pitch

Pour utiliser une autre police :

from tkinter import font as tkfont, Tk, Canvas

root = Tk()
canvas = Canvas(root, width=500, height=500, bg="ivory")
canvas.pack()


my_font=tkfont.Font(family="cmsy10")

text=canvas.create_text(420,100,fill="black", font = my_font,
                        text="Portez ce vieux whisky au juge blond qui fume")
root.mainloop()

Les ancres

Les ancres permettent d’ancrer un widget ou du texte au nord, au sud, etc d’une certaine zone.

Il y a 9 ancres possibles :

NW   N    NE
W  CENTER  E
SW   S    SE

Elle s’utilisent en tant qu’option dans différents contextes, pour ajuster :

  • un widget avec le gestionnaire pack
  • placer du texte dans certains widgets (Canvas, Button, etc)

L’utilisation d’une ancre (anchor) permet d’ajuster un objet dans un contexte, le placer plus haut, plus bas, etc.

Voici un exemple d’utilisation :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
from tkinter import *

root = Tk()

lbl1 = Label(root, text="Rose", font='Arial 30 bold', anchor=NW, width=20)
lbl2 = Label(root, text="begonia", font='Arial 30 bold', width=20)
lbl3 = Label(root, text="Kiwi", font='Arial 30 bold', anchor='se', width=20)
lbl1.pack()
lbl2.pack()
lbl3.pack()
root.mainloop()

qui produit :

../../../_images/ancres.png

Noter aux lignes 5 et 7 les deux syntaxes possibles pour déclarer l’ancre. Si on utilise une chaîne (ligne 7), il faut l’écrire en minuscule. Dans l’autre cas, pas de guillemets, il s’agit de noms que l’on doit importer de Tkinter.

Modifier dynamiquement une variable de contrôle

La notion de variable de contrôle n’est absolument pas indispensable quand on débute en Tkinter. On peut écrire de nombreux programmes variés sans avoir besoin d’en utiliser.

Tkinter possède un mécanisme original pour mettre à jour automatiquement des variables définies comme options de certains widgets (comme des boutons radio, des entrées ou des labels).

Dans l’interface ci-dessous,

../../../_images/variable_dynamique.gif

si on clique sur l’un des boutons + ou -, un compteur représentant des objets est mis-à-jour automatiquement dans un label sous les deux boutons.

Le nombre total d’objets est défini de la manière suivante :

total=IntVar()

Ici, total n’est pas exactement un entier mais ce qu’on appelle une Variable Tkinter, ou encore une variable dynamique ou encore une variable de contrôle. Cette variable stocke un entier que l’on peut récupérer par un appel total.get(). On peut aussi modifier la valeur interne avec la méthode set, par exemple total.set(42).

Le code correspondant de l’application ci-dessus est :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
from tkinter import *

root=Tk()
total=IntVar()

def incr():
    total.set(total.get()+1)

def decr():
    total.set(total.get()-1)


btn_plus=Button(root, height=5, width=5, text="+", command=incr)
btn_plus.pack(padx=5, pady=5)
btn_moins=Button(root, height=5, width=5, text="-", command=decr)
btn_moins.pack(padx=5, pady=5)

msg=Label(root, height=5, width=20, textvariable=total)
msg.pack(padx=5, pady=5)

root.mainloop()
  • Ligne 4 : une variable de contrôle de type IntVar est définie. Elle représente une variable entière qui compte un certain nombre d’objets (les objets comptés sont ici imaginaires, c’est juste pour l’exemple). Par défaut, la valeur interne de total est 0.
  • Lignes 13-26 : on définit deux boutons + et -. Quand on clique sur le bouton +, on appelle la fonction incr qui se charge d’incrémenter la variable de contrôle total (lignes 6-7). De même pour le bouton - et la fonction decr.
  • Lignes 7 et 11: noter que le contenu de la variable de contrôle est capturé avec la méthode get de la variable.
  • Lignes 7 et 10 : pour mettre à jour (ou parfois initialiser) une variable de contrôle, on utilise sa méthode set et on lui indique la nouvelle valeur.
  • La mise à jour est automatique : le clic sur + par exemple incrémente (ligne 7) la variable de contrôle total avec la méthode set ce qui change automatiquement le contenu du label.

En pratique, une variable IntVar est toujours inialisée à 0. Une autre façon d’initialiser (à 42 par exemple) aurait été d’écrire :

total = Intvar(value=42)

L’intérêt d’une variable de contrôle est de pouvoir mettre à jour automatiquement certains paramètres de différents widgets. On trouvera des détails dans la documentation de effbot.

On reconnait la nécessité d’utiliser des variables de contrôle Tkinter lorsqu’un widget a une option qui s’appelle variable ou textvariable.

Dans de beaucoup de codes, on observe une sur-utilisation des variables Tkinter. Dans de nombreuses situations statiques, elles ne sont pas utiles, comme le confirme Bryan Oakley sur StackOverflow. Elle ne servent vraiment que lorsque des objets variables définis par des événements non contrôlables sont partagées dynamiquement par plusieurs widgets. Je donne un cas typique est dans Exemple d’utilisation de StringVar. Elles sont parfois utiles aussi pour mettre à jour automatiquement un grand nombre d’objets. Certains widgets, pour être convenablement contrôlés, doivent absolument utiliser une variable de contrôle, typiquement le widget entrée, le widget bouton-radio ou le widget case à cocher.

Exemple d’utilisation de StringVar

StringVar est une classe propre à Tkinter et qui encapsule une variable option d’un widget et permettant une mise à jour automatique du widget. Voici un exemple avec un widget de type Label :

../../../_images/coucou.gif

produit par le code :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
from tkinter import *

root=Tk()
msg=StringVar()
entree=Entry(root, textvariable=msg)
entree.pack( padx=20, pady=10)

lbl=Label(root, textvariable=msg)
lbl.pack(padx=20, pady=10)

root.mainloop()
  • Ligne 4 : la variable msg est de type StringVar.
  • Lignes 5 et 8 : chaque fois que le champ de l’entrée est modifié, le texte entré est capturé dans msg (ligne 5) et il est automatiquement mis à jour dans le label (ligne 8).

Curseur : placer le code dans des fonctions

Ci-dessous, on explique comment placer du code utilisant des widgets dans des fonctions et les conséquences que cela peut avoir.

Reprenons le code de démo du slider :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
from tkinter import *

root = Tk()
cnv = Canvas(root, width=400, height=400)
cnv.pack()

old=None

def rayon(r):
    global old
    r=int(r)
    cnv.delete(old)
    old=cnv.create_oval(200-r,200-r,200+r, 200+r)

curseur = Scale(root, orient = "horizontal",
                command=rayon, from_=0, to=200)
curseur.pack()

root.mainloop()

Le code est hors fonction sauf la fonction rayon qui elle est obligatoire puisque le slider a besoin d’une fonction pour agir. L’avantage d’avoir du code hors fonction est que la fonction rayon a accès à des données comme le canevas que, de toute façon, on ne peut pas transmettre à la fonction rayon car son paramètre r est imposé par Tkinter lui-même. Et pour la même raison, l’identifiant old du rayon du précédent cercle ne peut être transmis à la fonction via un paramètre. Noter que le déclarateur global sert uniquement à la modification de la variable globale old.

Essayons d’écrire le code dans des fonctions :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
from tkinter import *


def rayon(r):
    global old
    r=int(r)
    cnv.delete(old)
    old=cnv.create_oval(200-r,200-r,200+r, 200+r)


def demo():
    root = Tk()
    cnv = Canvas(root, width=400, height=400)
    cnv.pack()
    curseur = Scale(root, orient = "horizontal",
                    command=rayon, from_=0, to=200)
    curseur.pack()

    root.mainloop()

    old=None
demo()

Ce code ne va pas fonctionner quand on déplace le curseur car cnv (ligne 13) est inconnu de la fonction rayon (ligne 5). Par ailleurs, le code ci-dessus ne permet pas de modifier la largeur du canevas (ligne 13) et cette largeur est en outre nécessaire à la fonction rayon pour placer le centre du cercle (ligne 8). Comme la fonction rayon ne peut prendre qu’un seul paramètre qui nous est imposé (la valeur lue sur le curseur), il est obligatoire de passer par une déclaration globale (lignes 5 et 21). Toutefois, on peut la minimiser, de la manière suivante par exemple :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from tkinter import Tk, Canvas, Scale

def rayon(r):
    cnv, side, old=data
    r=int(r)
    m=side/2
    cnv.delete(old)
    data[2]=cnv.create_oval(m-r,m-r,m+r, m+r)

def demo(side):
    global data
    root = Tk()
    cnv = Canvas(root, width=side, height=side)
    cnv.pack()
    old=None
    curseur = Scale(root, orient = "horizontal",
                    command=rayon, from_=0, to=200)
    curseur.pack()
    data=[cnv, side, old]
    root.mainloop()

side=400
demo(side)

Lorsque demo est lancée, une variable globale data est créée. C’est une liste (cf. ligne 19) et on y place toutes les données qui seront nécessaires à la fonction rayon et qu’on ne peut lui donner en paramètres.

Lorsqu’un nouveau cercle est créé, ne pas oublier d’enregistrer son id dans data, à l’indice 2, cf. lignes 8 et 19.

Noter (ligne 1) qu’a juste été importé le strict nécessaire de Tkinter.

Au total, le code ainsi obtenu est beaucoup plus lisible que le code initial.

Eviter global avec une clôture

Certains pourront regretter que le code précédent utilise le déclarateur global. Si on veut l’éviter, il faut que la fonction rayon, qui admet un unique argument, ait accès à data. Pour cela, il suffit de créer une « clôture » ainsi que le montre le code suivant :

1
2
3
4
5
6
7
8
def make_move(data):
    cnv, side, old=data
    def rayon(r):
        r=int(r)
        m=side/2
        cnv.delete(data[2])
        data[2]=cnv.create_oval(m-r,m-r,m+r, m+r)
    return rayon

Un appel make_move(data) renvoie une fonction rayon prenant un unique paramètre mais cette fonction à accès à data. Bien noter qu’il faut écrire (ligne 6) cnv.delete(data[2]) et non cnv.delete(old). D’où le code suivant, qui évite d’utiliser global :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from tkinter import Tk, Canvas, Scale

def make_move(data):
    cnv, side, old=data
    def rayon(r):
        r=int(r)
        m=side/2
        cnv.delete(data[2])
        data[2]=cnv.create_oval(m-r,m-r,m+r, m+r)
    return rayon

def demo(side):
    root = Tk()
    cnv = Canvas(root, width=side, height=side)
    cnv.pack()
    data=[cnv, side, None]
    rayon=make_move(data)
    curseur = Scale(root, orient = "horizontal",
                    command=rayon, from_=0, to=200)
    curseur.pack()
    root.mainloop()

def main():
    side=400
    demo(side)

main()

Curseur : placer le code dans une classe

Ci-dessous, on va montrer l’avantage à utiliser des classes pour gérer une interface graphique Tkinter. Reprenons pour cela le code découpé en fonctions de la démo du slider :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from tkinter import Tk, Canvas, Scale

def rayon(r):
    cnv, side, old=data
    r=int(r)
    m=side/2
    cnv.delete(old)
    data[2]=cnv.create_oval(m-r,m-r,m+r, m+r)

def demo(side):
    global data
    root = Tk()
    cnv = Canvas(root, width=side, height=side)
    cnv.pack()
    old=None
    curseur = Scale(root, orient = "horizontal",
                    command=rayon, from_=0, to=200)
    curseur.pack()
    data=[cnv, side, old]
    root.mainloop()

side=400
demo(side)

Une façon d’éviter les variables globales (ligne 11) et de coder de manière plus répandue est d’utiliser une classe (ligne 3 ci-dessous) : ainsi, chaque méthode de la classe aura accès aux attributs de la classe. Ainsi, si on transforme la fonction rayon initiale en une méthode, on n’aura plus besoin de variable globale.

D’où le code suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from tkinter import Tk, Canvas, Scale

class ScaleDemo:

    def __init__(self, side):
        self.side=side
        self.m=side/2
        root = Tk()
        self.cnv = Canvas(root, width=side, height=side)
        self.cnv.pack()
        self.old=None
        curseur = Scale(root, orient = "horizontal",
                        command=self.rayon, from_=0, to=200)
        curseur.pack()
        root.mainloop()

    def rayon(self, r):
        r=int(r)
        m=self.m
        self.cnv.delete(self.old)
        self.old=self.cnv.create_oval(m-r,m-r,m+r, m+r)

side=400
ScaleDemo(side)

Noter que la mainloop reste dans la fonction __init__ lancée par l’appel ScaleDemo(side).

Le curseur peut appeler la fonction rayon (lignes 17-21) en appelant la méthode rayon qui est référencée en utilisant self (ligne 13). Quand le curseur est modifié par l’utilisateur, Tkinter lance un appel rayon(r) et comme rayon est une méthode de ScaleDemo, l’interpréteur Python rajoute automatiquement comme premier argument à rayon une instance de la classe (self). Cela explique pourquoi r est le 2e paramètre dans la définition ligne 17.

Par ailleurs, les variables globales ont disparu. Par exemple, à l’initialisation, le canevas est déclaré comme attribut d’instance (avec self ligne 9) si bien que lorsque la méthode rayon est appelée, elle a accès, via self.cnv au canevas.

Noter qu’on n’a pas eu besoin de donner au curseur le statut d’attribut d’instance (puisque rayon n’en a pas explicitement besoin).

La ligne 19 est juste un raccourci pour éviter d’écrire 4 fois self.m ligne 21.

Installer Pygame sous Windows

Pygame est une bibliothèque de jeux. Dans ce qui suit, on va expliquer comment installer Pygame sous Windows 10 et sous Linux.

Installation sous Windows 10

Je suppose que vous avez installé Python depuis le site python.org. Probablement que ce qui suit ne s’applique pas si vous avez installé Python via Anaconda auquel cas il faut effectuer ce qui va suivre depuis la ligne de commande propre à Anaconda. Je suppose aussi que vous n’utilisez pas Python sous un environnement virtuel (virtualenv).

Le principe d’installation est assez simple : on tape une ligne de commande dans un shell et cela installe automatiquement Pygame.

Ce qui suit suppose que vous travaillez avec une version 3 de Python.

Pygame serait-il déjà installé ?

Pour le savoir, ouvrir IDLE, l’éditeur par défaut de Python (Menu Démarrer > Python > IDLE). Devant le prompt Python avec les trois chevrons écrire :

import pygame

Si vous avez un message d’erreur, c’est que Pygame n’est pas installé ou pas accessible.

Le programme pip est-il déjà installé ?

Tout ce qui suit suppose que vous disposez du programme pip. Vous devez donc vérifier que pip est installé sur votre système.

pip est un programme d’installation de paquets Python. Son principe est d’aller télécharger sur un dépôt Internet un programme Python et d’installer ce programme sur votre système.

Ouvrir une ligne de commande Windows ; pour cela, taper cmd dans Cortana, ce qui devrait montrer une icône portant le nom d”« invite de commandes », cliquer sur l’icône et un terminal noir, avec un prompt devrait s’ouvrir. Il est encore plus simple de taper command.exe dans la barre de recherche de Cortana. Le terminal obtenu est un terminal système et pas un terminal propre à Python. Dans ce terminal, écrire

pip

puis appuyer sur la touche Entrée. Si la réponse est :

'pip' n'est pas reconnu en tant que commande interne
ou externe, un programme exécutable ou un fichier de commandes.

c’est que pip n’est pas installé (voir plus loin pour savoir comment y remédier).

La méthode la plus simple

Ouvrir une ligne de commande Windows ; pour cela, taper cmd dans Cortana, ce qui devrait montrer une icône portant le nom d”« invite de commandes », cliquer sur l’icône et un terminal noir, avec un prompt devrait s’ouvrir. Ce terminal est un terminal système et pas un terminal propre à Python. Dans ce terminal, écrire

pip install pygame

puis appuyer sur la touche Entrée.

pip est un programme d’installation de paquets Python. Son principe est d’aller télécharger sur un dépôt Internet un programme Python et d’installer ce programme sur votre système.

Si le programme pip est reconnu par votre système, le déroulé de l’installation devrait être retranscrit dans le terminal et en conclusion vous devriez lire la version de Pygame qui a été installée, par exemple :

Successfully installed pygame-1.9.3

Pour vérifier que Pygame est bien installé, ré-ouvrir IDLE et devant les 3 chevron du prompt Python écrire, en minuscule :

import pygame

et aucun message d’erreur ne devrait apparaître. Vous pouvez même déterminer depuis le prompt Python la version de Pygame qui est installée, en tapant :

pygame.version.ver

ce qui devrait afficher la version, par exemple

'1.9.3'

Installation via un fichier wheel

Il se peut que la méthode précédente échoue. Vous pouvez alors télécharger sur Internet un binaire d’extension whl (ce qui signifie « wheel ») contenant Pygame et l’installer comme ci-dessus avec pip. L’ensemble se fait en trois étapes :

Étape 1 : Déterminer l’adressage de votre version de Python

Quand vous téléchargez Python depuis le site python.org, il est disponible en version 32 bits ou 64 bits. Il ne faut pas confondre la version 32 ou 64 bits de Python avec l’adressage de votre version de Windows 10 qui elle est très probablement sous 64 bits.

Par défaut, c’est la version 32 bits qui est installée. Mais, pour connaître le mode d’adressage de votre version de Python, ouvrir IDLE et regarder tout en haut de la fenêtre, à la fin de la ligne, ce qui est indiqué, soit « 32 bit (Intel) » soit « 64 bit (AMD64) » ce qui vous précise votre version. Vous devriez aussi lire votre version de Python, par exemple, Python 3.6.

Étape 2 : télécharger le fichier wheel

Dans Google, taper « unofficial binaries Pygame » et cliquer sur le premier lien transmis par Google. On arrive sur le site Unofficial Windows Binaries for Python Extension Packages. Une fois sur le site, chercher la section Pygame qui contient une suite de fichiers « wheel » d’extension whl pour différentes versions de Pygame.

Puis télécharger le fichier correspondant à votre système. Pour illustrer la suite, je vais supposer que vous êtes sous Python 3.5 en 32 bits,et donc que vous téléchargez le fichier nommé pygame‑1.9.4‑cp35‑cp35m‑win32.whl, le code 35 devant être compris comme se référant à Python 3.5 et le code win32 se référant à 32 bits ; ce fichier est un binaire contenant la version 1.9.4 de Pygame.

Étape 3 : installer le fichier wheel

  • Une fois le fichier téléchargé, ouvrir le répertoire de téléchargement et débrouillez-vous pour obtenir le nom complet du dossier où se trouve le fichier. Le plus simple pour cela est de cliquer sur la barre d’adresse du dossier et de copier le nom complet du dossier, typiquement C:\Users\Moi\Downloads.
  • Ouvrir une invite de commandes, par exemple en passant par Cortana et y tapant cmd. Taper cd dans la ligne de commandes (ce qui signifie « change directory ») puis après un espace, coller dans la ligne de commandes le nom de dossier que vous aviez copié, puis taper sur la touche Entrée : la ligne de commande est alors placée dans le dossier où se trouve le fichier whl à installer.
  • appeler le programme pip sur le fichier que vous avez téléchargé. Pour cela taper dans la ligne de commande pip suivi d’un espace puis indiquer le nom du fichier, dans notre exemple, c’était pygame‑1.9.4‑cp35‑cp35m‑win32.whl. Inutile de tout taper lettre par lettre, il suffit de taper les 4 ou 5 premières lettres et ensuite d’appuyer sur la touche TAB et l’invite de commande complètera avec le bon nom de fichier.
  • Valider en appyant sur la touche Entrée et sauf anaomalie, cela devrait installer Pygame sur votre système. Vérifier en ouvrant IDLE et en tapant import pygame.

Que faire si pip n’est pas reconnu sur votre système ?

En principe, depuis la version 3.4 de Python, le programme pip est disponible sur votre système car installé en même temps que Python. Toutefois, si lors de l’installation de Python, vous avez oublié de cocher la case autorisant le placement des répertoires des binaires Python dans le PATH du système, pip ne sera pas reconnu.

Vous pouvez d’ailleurs vérifier en allant voir si le fichier pip.exe est présent dans le répértoire des scripts Python. On trouve ce répertoire à une adresse du type

C:\Users\MonNom\AppData\Local\Programs\Python\Python37-32\Scripts

Si pip n’est pas reconnu sur votre système, le plus probable est que le PATH soit incomplet. Deux solutions :

  • soit vous désinstallez Python et vous le réinstallez en prenant soin de cocher dans le panneau d’installation, tout au début du processus d’installation, la case d’inclusion de Python au PATH,
  • soit vous complétez vous-même le PATH en vous aidant par exemple de Add PIP to the Windows Environment Variables

Installer Pygame sous Linux

Il s’installe aidément en utilisant l’installeur pip3 :

pip3 install pygame

ce qui affiche

$ pip3 install pygame
Collecting pygame
  Downloading https://files.pythonhosted.org/
  packages/b3/5e/fb7c85304ad1fd52008fd25fce97
  a7f59e6147ae97378afc86cf0f5d9146/
  pygame-1.9.4-cp36-cp36m-manylinux1_x86_64.whl
  (12.1MB)
    100% |████████████████████████████████| 12.1MB 155kB/s
Installing collected packages: pygame
Successfully installed pygame-1.9.4

Audio sous Tkinter avec Pygame

Nativement, Tkinter ne prend pas en charge la diffusion de flux audio (fichiers mp3, wav, etc). Il faut faire appel à une bibliothèque tierce pour réaliser l’incorporation d’un flux audio dans un programme Tkinter.

La bibliothèque de jeux Pygame prend en charge l’audio que ce soit sous Windows, Linux ou OsX. On peut donc l’associer à Tkinter pour faire émettre du son. C’est ce qu’on va faire ci-dessous. Toutefois, d’autres choix seraient possibles comme Pyglet.

Pygame prend en charge les fichiers de format wav. Pour l’utilisation du format mp3, voir tout à la fin de cette unité consacrée à l’audio avec Pygame sous Tkinter. Le programme audio.py ci-dessous dépend d’un fichier clap.wav placé à côté du fichier audio.py. Le fichier wav est téléchargeable ICI. C’est un programme minimal qui associe Tkinter et un flux audio géré par Pygame :

audio.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
from tkinter import *
import pygame

pygame.mixer.init()

mon_audio=pygame.mixer.Sound("clap.wav")

def lancer():
    mon_audio.play(-1)

def couper():
    mon_audio.stop()

fen = Tk()

Button(fen,text="Son",command=lancer, width=40).pack(pady=20)
Button(fen,text="Couper",command=couper, width=40).pack(pady=20)

fen.mainloop()

L’interface graphique a l’allure suivante :

../../../_images/audio11.png

Le bouton du haut active un son et ce son tourne en boucle, le bouton du bas interrompt le flux audio.

Commentaire de code

  • Ligne 2 : on importe Pygame

  • Ligne 4 : on initialise le module mixer de Pygame (le module qui gère le son)

  • Ligne 6 : on charge le fichier dont on veut écouter le flux audio en indiquant l’adresse du fichier. Cela crée un objet qui permet d’accéder au flux vidéo.

  • Ligne 1 : on importe Tkinter.

  • Les éléments graphiques :

    • ligne 14 : la fenêtre
    • lignes 16-17 : deux boutons
    • lignes 9 et 13 : les fonctions associées aux boutons
  • Ligne 9 : le son est joué (fonction play). L’argument -1 a pour effet que le flux audio est joué en boucle, indéfiniment si on ne l’interrompt pas. Le volume par défaut est maximal.

  • Ligne 12 : interruption du flux audio.

Volume du son

Il est possible de définir un volume sonore avec la méthode set_volume, par exemple placer entre les lignes 8 et 9 l’instruction :

mon_audio.set_volume(0.5)

La méthode set_volume accepte en un argument un nombre flottant entre 0 (aucun son) et 1 (son d’intensité maximale), donc 0.5 correspond à une intensité médiane.

Ne pas jouer en boucle

On souhaite parfois qu’un son soit joué non pas en boucle mais juste une seule fois. Pour cela il suffit de passer un argument autre que -1 à la fonction pygame.mixer.music.play. Par exemple, pygame.mixer.music.play() va jouer le fichier audio une seule fois. Ou encore pygame.mixer.music.play(5) va le jouer 6 fois (et non pas 5, au moins sous Linux en tous cas).

Pré-initialisation

Parfois, le lancement du flux audio se lance avec un certain retard ou encore le flux audio est ralenti. Pour y remédier, essayer de placer avant l’appel à pygame.mixer.init un appel à la fonction pre_init, par exemple :

pygame.mixer.pre_init(44100, -16, 1, 512)

Les valeurs ci-dessous ont été retranscrises d’une réponse sur StackOverFlow.

Documentation

La bibliothèque Pygame permet une prise de l’audio. La documentation officielle de Pygame du package gérant le son est disponible sur pygame.mixer.music. Il pourra aussi être utile de consulter les fichiers d’exemples proposés dans le code source de Pygame.

Sortie correcte de Pygame

Si on reprend le programme audio.py, qu’on clique sur le bouton Son alors on entend le flux audio. Et si on interrompt alors le programme en cliquant sur la croix de la fenêtre alors le flux audio sera toujours audible. Pourquoi ? Parce que la croix ferme uniquement ce qui est de la responsabilité de Tkinter, ce qui n’est pas le cas de la gestion du son.

De la même façon qu’on a initialisé l’audio dans Pygame avec pygame.mixer.init, il faut quitter proprement Pygame. Pour cela, Pygame propose la méthode pygame.quit. Ci-dessous, le programme audio.py a été modifié pour que la sortie de Pygame soit correcte :

audio_sortie.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from tkinter import *
import pygame

pygame.mixer.init()

mon_audio=pygame.mixer.Sound("clap.wav")


def lancer():
    mon_audio.set_volume(1)
    mon_audio.play(-1)

def couper():
    mon_audio.stop()

def quitter():
    pygame.quit()
    fen.destroy()

fen = Tk()

Button(fen,text="Son",command=lancer, width=40).pack(pady=20)
Button(fen,text="Couper",command=couper, width=40).pack(pady=20)
fen.protocol("WM_DELETE_WINDOW", quitter)

fen.mainloop()
  • Ligne 17 : on quittera Pygame avec la fonction dédiée quit.
  • Ligne 24 : Cette ligne détecte une tentative de fermeture de la fenêtre en cliquant sur la croix. Fermer la fenêtre est l’événement "WM_DELETE_WINDOW". Lorsque cet événement est détecté par Tkinter, la fonction quitter, définie lignes 16-18, sera automatiquement appelée.
  • Lignes 16-18 : fonction appelée pour fermer proprement l’interface graphique et Pygame. pygame.quit ferme Pygame et libère les ressources que Pygame utilisait. De même, fen.destroy() fait disparaître le fenêtre fen.

Le cas du format mp3

Pour les flux audio mp3, la documentation de Pygame précise que la prise en charge est limitée. Le conseil qui est souvent donné est de convertir ses fichiers mp3 au format wav ou encore au format ogg (qui, comme le format mp3, est compressé, à la différence du format wav) qui eux sont pleinement pris en charge par Pygame.

Il semble toutefois que l’on puisse jouer des fichiers mp3 sous Tkinter en utilisant Pygame, que ce soit sous Windows comme sous Linux. Les exemples qui suivent utiliseront le fichier clap.mp3 téléchargeable ICI et qu’on placera à côté du code source Python.

D’abord, hors de Tkinter, on peut écouter un fichier mp3 sous Python avec Pygame en suivant l’exemple suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import pygame

pygame.init()

pygame.mixer.music.load("clap.mp3")
pygame.mixer.music.play()

while pygame.mixer.music.get_busy():
    pass

pygame.quit()

Cet exemple suit la démo donnée dans le code-source de Pygame. On notera que la méthode Sound applicable au format wav ne semble pas s’appliquer au format mp3. On prendra aussi garde que certains fichiers audio au format mp3, parfaitement audibles dans un lecteur audio, ne seront pas décodés par Pygame, comme rappellé dans cette discussion Pygame fails to play some mp3 files but not others.

La boucle aux lignes 8-9 doit être présente sinon le son n’est pas émis et le programme s’interrompt.

Pour finir, je reprends ci-dessous l’application utilisée sous Tkinter pour décrire l’usage d’un fichier wav, sans commentaire supplémentaire puisque les codes sont assez proches :

from tkinter import *
import pygame

pygame.mixer.init()

pygame.mixer.music.load("clap.mp3")

def lancer():
    pygame.mixer.music.play(-1)

def couper():
    pygame.mixer.music.stop()

fen = Tk()

Button(fen,text="Son",command=lancer, width=40).pack(pady=20)
Button(fen,text="Couper",command=couper, width=40).pack(pady=20)

fen.mainloop()
../../../_images/audio11.png

Il semblerait que le module mixer s’exécute dans un thread distinct du thread principal dans lequel s’éxecute Tkinter.

Audio sous Tkinter avec winsound

Sous Windows, il est possible de jouer des fichiers audio avec le module winsound. L’intérêt essentiel par rapport à des solutions utilisant Pygame ou Pyglet est qu’il n’y a rien à installer puisque winsound est un module standard de Python. Cette possibilité est offerte uniquement sous Windows ce qui rend le code non portable mais ça peut dépanner. La documentation de winsound est disponible dans la documentation officielle. Bien que ce ne soit pas indiqué, seul le format wav est pris en charge, en particulier le format mp3 n’est pas utilisable avec winsound, comme c’est indiqué dans ce message.

A des fins de démontration, voici un programme permettant de tester winsound sous Tkinter. Le fichier clap.wav (téléchargeable ICI) doit être placé à côté du code-source Python :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
from tkinter import *
import winsound

def lancer():
    winsound.PlaySound('clap.wav', winsound.SND_LOOP | winsound.SND_ASYNC)

def couper():
    winsound.PlaySound(None, winsound.SND_PURGE)

def close_sound():
    couper()
    fen.destroy()

fen = Tk()

fen.protocol("WM_DELETE_WINDOW", close_sound)

Button(fen,text="Son",command=lancer, width=40).pack(pady=20)
Button(fen,text="Couper",command=couper, width=40).pack(pady=20)
fen.mainloop()
  • ligne 5 : pour jouer un fichier de format wav, on utilise la fonction PlaySound à qui on transmet le nom du fichier et en 2e argument, on place des flags, ici un flag pour jouer en boucle et un flag pour que le flux audio ne bloque pas le programme principal.
  • ligne 8 : pour couper (provisoirement) le son, on utilise encore la fonction Playsound mais avec un argument valant None.
  • Lignes 10-12 et 16 : si on quitte le programme en fermant la fenêtre avec la croix et alors qu’un flux audio est émis, le flux lui n’est pas interrompu, ce qu’il faut corriger manuellement. Il faut donc intercepter le signal de fermeture de la fenêtre (ligne 16) pour appeler une fonction close_sound (lignes 10-12) qui avant de fermer l’application graphique (ligne 12) va couper le son (ligne 11).
../../../_images/audio11.png

Pour une solution portable Windows, MacOs et Linux, et prenant en charge le format mp3, il aurait pu être envisageable d’utiliser le module non standard playsound mais il ne semble plus maintenu depuis juin 2017.

Conseils généraux pour écrire de petites applications Tkinter

Vous devez écrire un petit jeu Tkinter, avec un plateau par exemple. Voici quelques conseils très élémentaires et qui s’adressent à des débutants en Python.

  • Encapsuler toutes les données et les distances fixes dans des variables bien nommées, autrement dit éviter les constantes magiques dans le code ; ces variables qui seront globales à votre programme sont en fait des constantes et seront écrites en capitales. A terme, limiter toutes ces variables globales et prétendues inamovibles et qui à l’usage ont besoin d’évoluer.
  • Découper votre code en fonctions simples et réutilisables et avec une interface adéquate
  • Créer une classe qui encapsule des données et des méthodes interdépendantes.
  • Séparer la partie vue de l’application et la partie calcul (ce point est essentiel dans de nombreuses situations)
  • Faire un croquis, de préférence sur une feuille quadrillée en indiquant les coordonnées
  • Ne pas abuser du multi-fenêtrage
  • Si l’interface est formée de nombreux widgets, répartir ces widgets selon différents frames.

Changement de repère

Beaucoup de dessins sont naturellement construits avec un repérage défini par le repère mathématique habituel. Par exemple, considérons le dessin d’un jeu de solitaire :

../../../_images/peg_solitaire.png

A cause des symétries, il est assez naturel de se placer dans le repère ci-dessous, avec une origine au centre du dessin et les axes mathématiques habituels :

../../../_images/repere_math.png

Or, le repère naturel du canevas est différent :

  • l’origine est en haut à gauche de la fenêtre
  • l’axe vertical est orienté vers le bas et non vers le haut.

Donc une solution envisageable est de faire ses calculs dans le repère mathématique habituel et de convertir toutes les coordonnées dans le repère naturel du canevas. On pourra alors écrire un code tel que celui-ci :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
def chgt(X,Y, center):
    """(X, Y): coordonnées dans le repère mathématique habituel
    center=(x0,y0) coordonnées dans le repère du
    canevas du centre du repère mathématique
    habituel renvoie coordonnées (x, y) du canevas"""

    return (X+center[0], -Y+center[1])

center=(SIDE//2, SIDE//2)

X=-200
Y=40
x,y=chgt(X,Y, center)
cnv.create_text(x, y, text ="Coucou !")
  • Lignes 1-7 : la fonction de changement de coordonnées. Les explications sont données dans la docstring. On donne les coordonnées mathématiques et les coordonnées du centre de notre repère (par rapport au repère du canevas) et on récupère des coordonnées pour le canevas (utiles pour utiliser Tkinter)
  • Ligne 9 : typiquement, le centre du repère mathématique est le centre de la fenêtre
  • Ligne 13 : on effectue le changement de coordonnées ; pour cela, on récupère les coordonnées (x,y) dans le canevas du point de coordonnées (X, Y)=(-200, 40) dans le repère d’origine le point centre.
  • ligne 14 : on donne ces coordonnées à une méthode du canevas.

Importer des vecteurs

Les vecteurs (des maths ou de la physique) sont souvent utiles pour faire des calculs avec des dessins. Cependant, Tkinter ne semble pas disposer d’une classe Vector qui permettrait de générer des vecteurs et de faire des opérations vectorielles. Heureusement, le module standard Turtle dispose d’une classe Vec2D qui représente des vecteurs du plan. On peut faire des opérations usuelles avec des vecteurs :

  • somme, différence
  • produit par un nombre
  • produit scalaire habituel
  • calcul de la norme (de la longueur du vecteur) avec abs (et non len)
  • opposé d’un vecteur
  • accès aux coordonnées aves les indices 0 et 1.

On peut aussi effectuer une rotation vectorielle.

Voici une illustration

from turtle import Vec2D

v=Vec2D(3,4)
w=Vec2D(-8,6)

print(v)
print(v[0], v[1])
print(-v)
print(abs(v))
print(10*v)
print(v+w)
print(v*w)
(3.00,4.00)
3 4
(-3.00,-4.00)
5.0
(30.00,40.00)
(-5.00,10.00)
0

Attention toutefois que certaines opérations n’existent pas :

  • on peut écrire u=Vec2D((1,3), (2,5)) et même 2*u mais cela n’aura pas le sens attendu pour des vecteurs (et qui serait ici de créer le vecteur d’origine (1,3) et d’extrémité (2,5)) ;
  • si v est un vecteur, on n’accède pas à sa 1re coordonnéee par v.x ;
  • un vecteur est immuable (il dérive de la classe tuple)
  • on ne peut déclarer sans initialiser u=Vec2D() (on se serait attendu à obtenir le vecteur nul)
  • une opération comme Vec2D(3,4)/2 de division d’un vecteur par un scalaire n’est pas reconnue.

Si u=(2, 3) on ne peut pas écrire Vec2D(u) ; il faut extraire les coordonnées avec l’opérateur splat comme ceci : Vec2D(*u). Voici, par exemple, comment on calcule la distance entre deux points \(A\) et \(B\) :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from turtle import Vec2D as vect

A=(2,3)
B=(4,1)

# Accès habituel aux coordonnées
AB=vect(B[0], B[1])-vect(A[0], A[1])
print(abs(AB))

# Accès par décompression aux coordonnées
AB=vect(*B)-vect(*A)
print(abs(AB))
13
14
2.8284271247461903
2.8284271247461903

Donnons un autre exemple de calcul. On se donne une sommet A et un point C et on cherche à construire un point B tel que A, B et C soient alignés dans cet ordre et que AB=LL est une longueur donnée. L’idée de base est que si \(v\) est un vecteur et si \(N\) est la norme du vecteur alors \(v/N\) est un vecteur de norme 1 et donc que \(kv/N\) est de longueur \(k\). Le code ci-dessous calcule le point A:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
from turtle import Vec2D

A=(5,2)
C=(-1, 5)

A=Vec2D(*A)
C=Vec2D(*C)
AC=C-A
u=1/abs(AC)*AC
print(u)

lg=3
B=A+lg*u

print(B)
16
17
(-0.89,0.45)
(2.32,3.34)

Rotation de vecteur

On peut avoir besoin d’effectuer une rotation d’un item placé sur un canevas Tkinter. Pour des items géométriques du type line, rectangle, etc, il suffit de savoir coder une fonction de rotation et de l’appliquer aux sommets de l’item. Pour d’autres types d’items, voir plus bas.

On peut coder soi-même une fonction de rotation. Toutefois, le module Turtle en fournit une comme méthode de sa classe Vec2D (Vec2D n’oblige nullement à coder en Turtle).

Voici un exemple d’utilisation. Calculons les coordonnées dans le repère mathématique habituel de l’image N du point M de coordonnées (150, 100) par la rotation de centre le point C=(100, 100) et d’angle 60° :

from turtle import Vec2D

C=(100, 100)
M=(150, 100)


CM=Vec2D(*M)-Vec2D(*C)
v=CM.rotate(60)
N=Vec2D(*C)+v
print(N)
(125.00,143.30)

Comme prévu, l’abscisse du point cherché est \(\mathtt{\frac{C[0]+M[0]}2}\) puisque le cosinus de 60° est 1/2. Mathématiquement, CM désigne le vecteur \(\mathtt{\overrightarrow{CM}=\overrightarrow{OM} - \overrightarrow{OC}}\) . Ci-dessus, v désigne l’image de \(\mathtt{\overrightarrow{CM}}\) par la rotation vectorielle d’angle 60° (et le centre est à l’origine des axes, comme pour toute rotation vectorielle) . L’affectation N=Vec2D(*C)+v est une conséquence de \(\mathtt{\overrightarrow{CN}=\overrightarrow{ON}-\overrightarrow{OC}=\overrightarrow{v}}\).

Rreste le cas d’item non géométriques. Pour du texte, cela doit être réalisable car un item texte possède une option angle. Reste le cas des images (format PhotoImage) pour lequel il faudra utiliser le module (non standard) PIL.

Définir une map avec une chaîne triple

Un jeu nécessite parfois de définir une map. Il est fréquent que cette map soit représentée par une liste de listes d’entiers, par exemple :

map2D=[
    [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
    [1, 3, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1],
    [1, 0, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1],
    [1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1],
    [1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1],
    [1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1],
    [1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1],
    [1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1],
    [1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1],
    [1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1],
    [1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1],
    [1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1],
    [1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 0, 1],
    [1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 4, 1],
    [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]]

Il sera toutefois plus lisible et plus maintenable de définir ses map sous forme de chaînes triples :

map_str="""\
111111111111111
130222222222221
101212121212121
122222222222221
121212121212121
122222222222221
121212121212121
122222222222221
121212121212121
122222222222221
121212121212121
122222222222221
121212121212101
122222222222041
111111111111111"""

Mais, on perd l’accès aux éléments par ligne et par colonne ainsi que les dimension de la map ; donc, derrière ça on peut construire la map sous forme de liste 2D :

ma_map="""\
111111111111111
130222222222221
101212121212121
122222222222221
121212121212121
122222222222221
121212121212121
122222222222221
121212121212121
122222222222221
121212121212121
122222222222221
121212121212101
122222222222041
111111111111111"""

map2D=[list(map(int, L)) for L in ma_map.split()]

print(*map2D, sep='\n')
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
[1, 3, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1]
[1, 0, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1]
[1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1]
[1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1]
[1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1]
[1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1]
[1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1]
[1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1]
[1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1]
[1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1]
[1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1]
[1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 0, 1]
[1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 4, 1]
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]

map2D a été construite avec une liste en compréhension. Attention que dans l’avant dernière ligne de code, le nom map représente une fonction standard de Python, à ne pas confondre avec une map (au sens de carte) dans un jeu …