Python : une interface avec tkinter

Après avoir conçu une application de lancer de dés, il faudrait la rendre plus conviviale. Et par convivial, on entends en général avec une interface graphique.

Créer une interface graphique est en général une tâche énorme car il ne suffit pas que ça marche techniquement. La rendre convivial et ergonomique (en tout cas un minimum) nécessite de choisir les bons composants, s’assurer que les utilisateurs comprennent comment ils interagissent avec l’application et anticiper un mauvais usage. Il faut aussi avoir conscience qu’avec une interface graphique, nous nous adressions à un autre public qui n’interagît pas avec la ligne de commande.

Dans cet article, nous allons voir les bases de la création d’une petite interface avec tkinter et donc les bases du fonctionnement de cette bibliothèque graphique. Nous allons cependant être un peu hybride par rapport à l’utilisateur et nous contenter de produire le résultat dans le terminal.

Objectif de l’application

Nous allons ajouter une interface graphique à notre fonction de lancer de dés. Celle-ci devra permettre de ne pas avoir à saisir une ligne complète pour répéter une action, une fois certaines informations saisies dans l’interface, il suffit de clicker sur un bouton pour rejeter les dés. Ou ne modifier qu’une partie. L’intérêt de ce type d’interface est également d’éviter certaines erreurs de saisie.

Une bonne interface doit également présenter les résultats. Nous allons négliger cette partie ici pour une question de simplicité.

Nous allons donc voir

  • comment créer une fenêtre tkinter
  • des exemples de widgets tkinter (Button, Spinbox et Scale et et comment les placer dans une fenêtre)
  • comment déclencher des actions avec les boutons
  • certains éléments pour choisir le widget le plus adapté

Utilisation de tkinter

L’intérêt de tkinter est qu’il fait parti de la lib standard. Il n’y a donc pas de package tiers à installer, vous pouvez l’importer directement. Faites attention au sujet de l’import, de nombreux tutos importent tout avec from tkinter import *. J’estime que c’est une très mauvaise pratique qui masque les composant tkinter et rends le code difficilement maintenable. J’utiliserai un namespace avec import tkinter as tk.

Sachez avant d’aller plus loin que tkinter est une bibliothèque historique et bien que modernisée avec Python 3, cet aspect s’en ressent. Ne vous attendez donc pas à une interface « moderne ».

Organisation du code

Ajouter une interface graphique, c’est ajouter une nouvelle fonctionnalité. Elle doit donc être dans un module dédié. Nous allons même réorganiser la structure du projet en ajoutant un package pour grouper les modules d’interface graphique et en ligne de commande.

Attention ! Pour l’interface tkinter, il arrive régulièrement que vous nommiez votre module avec ce nom. Mais dans ce cas, lorsque vous ferez votre import, vous importerez votre propre module et rien ne marchera… Je nommerai donc mon module tkapp.py.

Création d’une fenêtre

La première chose à créer sera la fenêtre. Pour cela, tkinter fournit une classe que nous allons instancier.

import tkinter as tk

window = tk.Tk()

window.mainloop()

Si vous exécutez ce module, vous afficherez une fenêtre vide. Entre temps, vous verrez que l’interpréteur ne vous aura pas rendu la main. C’est la méthode .mainloop() qui est responsable de ça. Et oui, elle déclenche une boucle infinie qui, d’une part, affiche le contenu prévu, et d’autre part, attends les interactions de l’utilisateur.

Du coup, vous n’avez aucun composant pour fermer la fenêtre, il faudra passer par le contrôle de la fenêtre elle même ou tuer le process en ligne de commande.

Nous allons remédier à ça en ajoutant notre premier widget.

Ajout d’un widget

Widget est un mot-valise composé de window et gadget. il représente tous les composants que vous affichez dans une interface graphique : bouton, champs de texte, menu… Tkinter propose évidemment un ensemble de widgets, chacun représenté par une classe.

Nous allons commencer par ajouter un bouton pour fermer la fenêtre. Toutes les classes pour un widget suivront la même signature, c’est à dire Widget(conteneur, [options]).

  • Widget représente la classe du widget que nous voulons créer
  • conteneur est le conteneur auquel le widget sera associé. Il s’agit communément soit de la (ou d’une) fenêtre, soit d’une Frame. Ces dernières sont des widgets et servent à organiser les widgets.
  • options représente l’ensemble des paramètres optionnels qui permettent de configurer le widget. Ils seront tous nommés. En effet, tkinter utilise le variadic **kw et non des paramètres concrets.

Mais ceci ne suffit pas. Il faudra ensuite appeler pour le widget une méthode permettant son agencement. Il y a 2 méthodes principales : .place() et .grid(). La première permet de placer des composants les un après les autres, la seconde sous forme d’une grille à l’aide de coordonnées.

Je vais commencer par ajouter un bouton de la manière suivante. Le paramètre text= attribue un label au bouton.

import tkinter as tk

window = tk.Tk()

tk.Button(window, text="close").pack()

window.mainloop()

L’exécution du code va nous afficher une fenêtre avec un bouton. Mais le click sur ce bouton ne fait rien. C’est normal, aucune action ne lui est associée.

Notre fenêtre avec un simple bouton

Ajouter une action au bouton

La prochaine étape consiste à associer le click du bouton à l’action de fermeture de la fenêtre. Pour cela, le bouton possède un paramètre command= qui prends en argument une fonction. Cette fonction sera appelée lorsque l’utilisateur cliquera sur le bouton.

Lorsque vous voudrez ajouter vos actions, vous devrez définir vos propres fonctions que vous associerez à ce paramètre. Pour quitter l’application, nous allons utiliser une méthode fournie par l’objet Tk() : .quit(). Attention, c’est bien la fonction et non son retour qu’il faut passer au paramètre, donc sans les parenthèses.

Notre code devient alors

import tkinter as tk

window = tk.Tk()

tk.Button(window, text="close", command=window.quit).pack()

window.mainloop()

Nous avons maintenant une fenêtre qui fait globalement quelque chose. Nous allons pouvoir ajouter un bouton et une fonction pour le lancer de dés.

Commençons par la fonction. Celle-ci va faire appel à la fonction roller.roll_dice().

Il faut donc importer le module contenant cette fonction puis déclarer une fonction qui y fera appel. Nous allons pour l’instant faire une application simple qui affichera le résultat dans le terminal. De même, nous lancerons 3 dés à 6 faces pour jouer au 421… Par exemple.

from diceroller import roller

def rolle_dices():
    print(f"dices rolled {roller.roll_dice(3, 6)}")

Il nous faut maintenant ajouter un bouton pour lancer les dés. Nous allons déclarer ce dernier avant le bouton pour quitter.

tk.Button(window, text="Roll", command=rolle_dices).pack()
tk.Button(window, text="Close", command=window.quit).pack()

window.mainloop()

Vous remarquez que vous obtenez une interface où les boutons sont l’un au dessus de l’autre. Clicker sur le bouton Roll affiche le résultat dans la sortie standard et le bouton Close ferme la fenêtre comme précédemment.

Nos deux boutons alignés verticalement

Ajouter un choix pour les dés

Nous avons une fonction de lancer de dés qui autorise un certain nombre de paramètres que nous n’exploitons pas encore. Il faut donc réfléchir à la manière dont l’utilisateur pourra choisir des valeurs.

Globalement, on va éviter une saisie libre. Et donc, le champ de texte. Une saisie libre nécessite de valider la valeur, prévoir un dialogue en cas d’erreur de saisie… C’est assez complexe et dans notre cas, les valeurs possible étant limitées, nous pouvons préférer d’autres moyens.

Le widget Spinbox

Une Spinbox permet de sélectionner une valeur parmi un ensemble de valeurs. Similaire à un champ de saisie, il permet également d’en saisir une nouvelle. Visuellement, il présente la valeur courante et une paire de flèches permettant de passer aux valeurs suivantes. L’ensemble des valeurs peut être un intervalle de nombres ou un ensemble de chaines de caractères.

Nous allons l’utiliser pour choisir le nombre de dés, de 1 à 10. La création et le positionnement du widget pour cette plage se déclare de la manière suivante.

tk.Spinbox(window, from_=1, to=10).pack()

Les paramètres from_ et to permettent de déclarer les bornes inférieur et supérieur, toutes deux incluses.

Nous pouvons utiliser le même composant pour choisir le nombre de faces pour le dé. Mais pour le nombre de faces, nous n’avons pas une plage de nombre mais un ensemble. Nous allons donc utiliser, à la place des bornes, le paramètre values auquel on attribue un tuple ou une liste.

tk.Spinbox(window, values=(4, 6, 8, 10, 12, 20, 30, 100)).pack()

Si vous exécutez ce code dans certains IDE, vous verrez un avertissement sur le type. En effet, la séquence, liste ou tuple, doit (devraient) contenir des valeurs de type chaine de caractères. Ce n’est qu’un avertissement qui ne changera en rien le comportement du widget.

Cependant, avec la configuration actuelle de ces widgets, l’utilisateur peut remplacer la valeur par ce qu’il souhaite en saisissant la valeur dans le champ de texte. Nous devons donc restreindre l’accès à ce widget en interdisant son édition. Cela se fera simplement avec un paramètre :

tk.Spinbox(window, from_=1, to=10, state="readonly").pack()
tk.Spinbox(window, values=(4, 6, 8, 10, 12, 20, 30, 100), state="readonly").pack()

Nous avons donc deux widgets qui permettent de choisir une valeur uniquement parmi des valeurs pré-définies.

Notre application avec deux widgets de type Spinbox

Le widget Scale

La finalité du widget Scale est de proposer le choix d’un entier (ou flottant) dans un intervalle. Visuellement, il se présente comme un curseur à déplacer. La déclaration est très similaire à un Spinbox et n’accepte évidemment que la possibilité de définir des bornes (pas d’ensemble de valeurs). Il n’a pas besoin d’être configuré en lecture seule puisqu’il l’est déjà. Cependant, sans autre paramètre, il apparait comme un curseur vertical, ce qui n’est pas très beau pour notre cas. Nous allons ajouter un paramètre pour définir son orientation et notre dose devient :

tk.Scale(window, from_=1, to=10, orient="horizontal").pack()
tk.Spinbox(window, values=(4, 6, 8, 10, 12, 20, 30, 100), state="readonly").pack()

Il ne nous reste plus qu’à récupérer les valeurs sélectionnées

Notre interface avec un widget Scale

Récupérer les valeurs sélectionnées

Pour récupérer la valeur sélectionné, ces deux widgets possèdent une méthode .get() qui retourne la valeur en question. Mais nous allons plutôt utiliser des variables de contrôle.

Une variable de contrôle est un objet qui est un conteneur pour une valeur. Son principal intérêt est d’être partagé entre plusieurs widgets qui seront automatiquement mis à jour. Mais elles se comportent aussi comme des validateurs de type. Ainsi, dans notre cas, nous avons besoin d’entier, nous utiliserons donc des IntVar() :

dices_var = tk.IntVar()
sides_var = tk.IntVar()

Il faut maintenant associer les variables de contrôle aux widgets. Pour cela, Scale possède un paramètre variable et Spinbox, un paramètre textvariable. Nous allons donc affecter les variables de contrôle respectives.

tk.Scale(window, from_=1, to=10, variable=dices_var, orient="horizontal").pack()
tk.Spinbox(window, values=(4, 6, 8, 10, 12, 20, 30, 100),
           textvariable=sides_var, state="readonly").pack()

Les variables de contrôle possèdent également une méthode .get() qui permet de récupérer la valeur qu’elles référencent. Nous allons pouvoir vérifier ceci avec la fonction suivante dont l’appel est associé au click du bouton Roll.

def rolle_dices():
    print(dices_var.get(), sides_var.get())

C’est au moment de l’appel de cette fonction (donc au click du bouton Roll) que les valeurs des widgets seront récupérées. Vous pouvez essayer en modifiant les curseurs.

Vous pouvez également observer que le type de ces valeurs est int. Nous pouvons donc les utiliser directement pour appeler la fonction de lancer de dés.

def rolle_dices():
    print(f"dices rolled {roller.roll_dice(dices_var.get(), sides_var.get())}")

Je n’irai pas plus loin sur la présentation. Cet affichage fera l’affaire pour ce tuto.

Soigner la présentation

Si on a une application qui fait le job, il faut avouer que l’interface n’est pas terrible. Sans avoir la prétention d’aller vers quelque chose de joli ou même harmonieux (tkinter n’as pas vraiment d’outils efficace pour cela), on peut rendre cette interface plus conviviale en structurant la partie formulaire.

Le formulaire

Nous avons un formulaire composé de deux champs. Nous allons ajouter deux labels afin d’informer sur ce à quoi servent ces champs. L’agencement le plus adapté est donc sur une grille que nous allons isoler dans une Frame.

form_frame = tk.Frame(window)

dices_var = tk.IntVar()
sides_var = tk.IntVar()

tk.Label(form_frame, text="Dices").grid(column=0, row=0)
tk.Label(form_frame, text="Sides").grid(column=0, row=1)
tk.Scale(form_frame, from_=1, to=10, variable=dices_var,
         orient="horizontal").grid(column=1, row=0)
tk.Spinbox(form_frame, values=(4, 6, 8, 10, 12, 20, 30, 100),
           textvariable=sides_var, state="readonly").grid(column=1, row=1)

Le conteneur Frame sert à trois choses avec tkinter :

  • isoler des composants afin de les agencer entre eux
  • isoler l’usage d’un gestionnaire de layout (pack ou grid).
  • avoir des composants réutilisables

Nous n’allons évidemment pas réutiliser cette frame, mais elle permet donc d’isoler la partie formulaire et ainsi, placer les composants dans une grille alors que le reste de l’interface gardera un agencement de type pack.

Les composants dans la fenêtre

Nous allons placer les composants dans la fenêtre avec la méthode .pack(). Sans paramètre, celle-ci place les composants de haut en bas dans l’ordre de leur déclaration.

form_frame.pack()

tk.Button(window, text="Roll", command=rolle_dices).pack()
tk.Button(window, text="Close", command=window.quit).pack()

window.mainloop()

Nous avons alors une fenêtre un peu plus conviviale.

L’apparence finale de notre appli tkinter

Ce qu’il faut retenir

Ajouter une interface avec tkinter n’est techniquement pas très compliqué : on crée une fenêtre, place des composants, ajoute les fonctions… Ceci est vrai pour une petite interface simple comme celle qui illustre cet article. La difficulté ici est de choisir les composants et la présentation afin d’avoir une interface compréhensible. Mais pour une interface plus importante, le code risquera d’être très compliqué. La prochaine étape sera de définir des composants, mais ce sera dans un autre article.

Le code complet de cette version est disponible sur GitHub.

Si vous avez aimé ce post, n’hésitez pas à laisser un commentaire ci-dessous ou sur la page Facebook 😉

À propos de... Darko Stankovski

iT guy, photographe et papa 3.0, je vous fais partager mon expérience et découvertes dans ces domaines. Vous pouvez me suivre sur les liens ci-dessous.

Vous aimerez aussi...

Laisser un commentaire

En naviguant sur Dad 3.0, vous acceptez l’utilisation de cookies pour une navigation optimale et nous permettre de réaliser des statistiques de visites. Plus d'informations

Le blog Dad 3.0 utilise les cookies pour vous permettre une navigation optimale et nous permettre de réaliser des statistiques de visite. Dad 3.0 affichant des publicités, celles-si utilisent également des cookies pour un ciblage publicitaire. En continuant la navigation sur Dad 3.0, vous acceptez le dépôt et la lecture de cookies.

Fermer