TD interface graphique

Introduction

Un module pour interface graphique (GUI pour Graphical User Interface en anglais) est un module qui fournit des fonctions et des classes permettant de créer des fenêtres par lesquelles l'utilisateur pourra interagir dynamiquement avec l'ordinateur. Nous allons utiliser le module Tkinter fourni avec Python. Nous l'importons ici sous le nom raccourci tk:

In [ ]:
import tkinter as tk

Attention: durant des essais répétés sous IPython, il se peut que des messages d'erreurs se présentent alors que tout devrait bien se passer. Il peut être alors utile de redémarrer le noyau (Kernel) par la commande $\circlearrowright$.

Nous allons voir comment cela fonctionne sur des exemples.

Création d'une fenêtre

Premier exemple, on ouvre une fenêtre qui affiche un texte (regarder dans la barre des tâches si la fenêtre ne s'ouvre pas directement).

In [ ]:
fenetre = tk.Tk()
widget = tk.Label(fenetre, text="Hello, world!")
widget.pack()
fenetre.mainloop()

Fermer la fenêtre comme habituellement. Les fenêtres, boutons, etc... sont appelés widgets en anglais (traduction difficile: gadget, truc, bidule, chose,...). La première ligne crée un widget générique de type Tk et qui va correspondre à la fenêtre principale. Ici une fenêtre vide si rien de plus n'est précisé (vous pouvez commenter les deux lignes du milieu pour voir l'effet). On affecte l'instance créée à la variable fenetre. La seconde ligne crée un widget de type Label (étiquette en anglais) qui est une classe fournie par TKinter, en la liant à la variable fenetre. On lui passe en argument un texte à afficher. Ensuite, on appelle la méthode pack du widget qui fait en sorte que le texte soit affiché dans une fenêtre aux dimensions adaptées. Finalement, le véritablement lancement de la fenêtre se fait avec la méthode mainloop. Elle lance l'interface graphique en créant la fenêtre et attend une réponse de l'utilisateur. Ici, la fermeture de la fenêtre met fin à mainloop.

Utilisation d'un bouton

Deuxième exemple. On crée un widget simple avec une classe appelée Application et qui va utiliser le type Button fourni par **TKinter.

On peut comprendre le fonctionnement de Button en regardant le cas simplifié suivant:

In [ ]:
fenetre = tk.Tk()
bouton = tk.Button(fenetre, text="Quitter", command=fenetre.destroy) 
bouton.pack()
fenetre.mainloop() 

L'objet Button prend comme arguments un widget, un texte à afficher et enfin une fonction passée à la variable command. Cette dernière est particulièrement importante: elle va indiquer à l'interface graphique ce qu'il faut faire si l'utilisateur appuie sur le bouton. Dans cet exemple, on lui passe la méthode destroy de la fenetre qui va détruire celle-ci. Ainsi, lorsqu'on appuie sur le bouton, la fenêtre est détruire. C'est bien le comportement attendu pour un bouton Quitter.

On crée une mini-application en définissant la classe suivante:

In [ ]:
import tkinter as tk

class Application:

    def __init__(self, widget):
        self.ref = widget
        cadre = tk.Frame(widget)
        cadre.pack()
        self.bouton = ???? # à compléter
        ????               # à compléter

    def afficher(self):
        print("Bien, merci.")
        ????  # a compléter

fenetre = tk.Tk()
app = Application(fenetre)
fenetre.mainloop()

Cet exemple est construit comme suit: on crée une classe Application dont on crée une instance en lui passant en argument un autre widget, typiquement, le widget générique Tk correspondant à une fenêtre principale. Ensuite, le constructeur init crée un cadre de type Frame et on utilise la méthode pack pour le rendre visible.

Questions

  • Créer dans init un objet de type Button affecté à l'attribut bouton de la classe, à qui on passe en argument le widget cadre et le texte suivante "Comment ça va?". Le bouton activer la méthode afficher de la classe. On remplacer les ???? par du code Python.
  • Rajouter une ligne au-dessous de la précédente pour faire en sorte que le bouton s'affiche en utilisant la méthode pack.
  • Dans la méthode afficher, écrire une ligne qui détruise la fenêtre principale dont une référence est stockée dans l'attribut ref da la classe.

Quelques méthodes utiles communes à tous les widgets:

  • pour manipuler les options données en arguments, w désigne un widget
    • w.config(option=value) : configure les options
    • value = w.cget("option") : renvoie en str() la valeur d'une option
    • k = w.keys() : liste toutes les options
    • w.option_add(pattern, value)
    • w.option_get(name, class)
  • gestion des évènements
    • w.mainloop()
    • w.quit()
    • w.wait_variable(var) : attend que la valeur d'une variable change
    • w.destroy(): détruit le widget

Exemple d'une mini-calculatrice

On veut créer une calculatrice dans laquelle on entre une expression mathématique à calculer, écrite en langage Python, et dont on veut faire afficher le résultat dans la fenêtre, juste en-dessous.

Pour ce faire, étudions le fonctionnement du widget Entry fourni par TKinter et qui permet de récupérer un texte entré par l'utilisateur. Ce widget permet à l'utilisateur de rentrer un texte qui est ensuite récupéré par la méthode get de Entry. Ci-dessous, ce qui est récupéré de l'objet entree de type Entry est passé dans l'option text d'un Label pour pouvoir être affiché. La récupération de la donnée se fait via un bouton qui appel la fonction repondre.

In [ ]:
def repondre():
    affichage['text'] = entree.get()

fenetre = tk.Tk()

entree = tk.Entry(fenetre)
action = tk.Button(fenetre, text ='Faire afficher le texte', command=repondre)
affichage = tk.Label(fenetre, width=30)
entree.pack()
action.pack()
affichage.pack()

fenetre.mainloop()

Nous avons maintenant quelques mécanismes de base pour créer une mini-calculette. Une proposition à compléter est faite ci-dessous. On notera que la fonction pack n'est pas utilisée au profit de grid qui positionne les widgets créés sur une grille dont on précise le numéro de la colonne (column) et de la ligne (row). On notera également que en dehors des widgets Entry et Label qui reservent dans d'autres méthodes, on n'a pas besoin de stocker dans des variables tous les widgets créés dans init. Enfin, ce widget est créé sous forme d'une classe pour montrer qu'il sera facilement réutilisable en quelques lignes, évitant de devoir taper une série longue d'instruction.

In [ ]:
import tkinter as tk
from math import *

class Calculette:

    def __init__(self, parent):
        self.ref = parent
        parent.grid()
        tk.Label(parent, text="Expression").grid(row=0,column=0)
        self.entree = tk.Entry(parent, width=30)
        self.entree.grid(row=0,column=1)
        tk.Button(parent, text="Calculer", command=????).grid(row=0,column=2) # à compléter
        tk.Label(self.ref, text='Le résultat est :').grid(row=1,column=0)
        self.affichage = tk.Label(self.ref, width=30)
        tk.Button(parent, text="Remettre à zéro", command=????).grid(row=2,column=1)  # à compléter
        tk.Button(parent, text="Quitter", command=????).grid(row=2,column=2)  # à compléter

    def reset(self):
        self.entree.delete(0,tk.END)
        self.affichage['text'] = ''
        
    def calculer(self):
        self.affichage['text'] = ????  # à compléter
        self.affichage.grid(row=1,column=1)
        
fenetre = tk.Tk()
fenetre.title("Calculatrice Python")
cal = Calculette(fenetre)
fenetre.mainloop()

Questions

  • La fonction eval() de python permet d'interpréter une expression en Python écrite dans une string (on pour essayer eval("1.0/3.0 + abs(-2.0)") dans une cellule). Compléter la première ligne de la méthode calculer pour que soit exécutée la commande passée par l'utilisateur.
  • Compléter les passages d'arguments command= des trois boutons Button présents.
  • À quoi sert chacune des lignes de la fonction reset?
  • Jouer un peu avec la calculette en notant que toutes les fonctions mathématiques de la bibliothèque math ont été importées.

Une autre manière de faire la calculette

On veut pouvoir utiliser la touche Return ou Entree du clavier pour lancer le calcul plutôt qu'utiliser le clic sur un bouton. Pour cela, on peut lier (bind en anglais) l'appui d'une touche clavier au déclenchement d'une fonction. La syntaxe est décrite ci-dessous: la variable entree de type Entry est liée par la touche "Return" à la fonction calculer. Le déclenchement de "Return" dans le champ de entree entraînera donc l'appel de la fonction calculer. Cette fonction prend comme argument event (un évènement) qui est géré par la librairie TKinter.

In [ ]:
import tkinter as tk
from math import *

def calculer(event):
    expression['text'] = "Le résultat est "+???? # à compléter
    
fenetre = tk.Tk()
entree = tk.Entry(fenetre)
entree.bind("<Return>", calculer)
expression = tk.Label(fenetre)
entree.pack()
expression.pack()

fenetre.mainloop()

Question

  • Compléter la ligne de la fonction calculer en utilisant la fonction eval() et la consersion en chaîne de caractère par la fonction str().

Animation et simulation d'une image de diffraction

fonctionnement d'un curseur

Le widget Scale permet de créer un curseur dont la valeur détectée en cas de mouvement du curseur est transmise en argument de la fonction qui est donnée à l'option command dans la définition du curseur. Voici un exemple.

In [ ]:
import tkinter as tk

def afficher(valeur):
    print("Votre prochaine note sera",valeur+"/10")

fenetre = tk.Tk()
curseur = tk.Scale(fenetre, from_=0, to=10, resolution=0.5, length=120, orient="horizontal",\
                   command=afficher, label = "Note de l'étudiant")
curseur.pack()
curseur.set(5.0)
fenetre.mainloop()

Questions

  • quelle est le type de la variable valeur?
  • expliquer à quoi correspondent les options de la classe Scale.
  • pourquoi y a-t-il le caractère '_' pour l'argument 'from_'?
  • à quoi sert la méthode set()?

Création d'une image et insertion de pixel

On peut créer ou importer une image dans un dessin (classe Canvas) à l'aide de la classe PhotoImage. Les unités de dimensions sont en pixels pour l'affichage à l'écran. Les lignes

"#%02x%02x%02x" % (R,G,B)

assure la conversion de valeurs entre 0 et 255 en hexadecimaux codant pour une couleur RGB.

In [ ]:
import tkinter as tk

fenetre = tk.Tk()
dessin = tk.Canvas(fenetre)
dessin.pack()
img = tk.PhotoImage(width=100, height=100)
dessin.create_image((3,3),image=img, anchor="nw", state="normal")
R,G,B = 200, 156, 101
img.put("#%02x%02x%02x" % (R,G,B), to=(50,50,70,70))
img.put("#%02x%02x%02x" % (0,0,255), to=(0,0,10,10))
img.put("#%02x%02x%02x" % (0,255,0), to=(90,90,10,100))
fenetre.mainloop()

Question

  • Quelle est le rôle de la fonction put() et de son argument to?

Animation d'une figure de diffraction

On associe les différents mécanismes vus ci-dessus pour créer une image 2D d'une (presque, la formule n'est pas tout à fait correcte physiquement) figure de diffraction. Lire attentivement le code ci-dessus puis répondre aux questions avant de pouvoir le lancer et voir le résultat:

In [ ]:
import tkinter as tk
import numpy as np

# paramètres
Lx, Ly, taille_pixel = 60, 60, 8
largeur, hauteur = Lx*taille_pixel, Ly*taille_pixel
i0, j0 = Lx/2., Ly/2.

# création de la fenêtre principale
fenetre = tk.Tk()
fenetre.title("Figure de diffraction")
fenetre.geometry('+50+50')

# création du dessin à l'intérieur
dessin = tk.Canvas(fenetre, width=largeur, height=hauteur)
dessin.pack()
img = tk.PhotoImage(width=largeur, height=hauteur)
dessin.create_image((3, 3), image=img, anchor="nw", state="normal")

# fonctions de mise à jour de l'image
sinc = lambda x: (np.sin(x)/x)**2 if abs(x) > 1e-15 else 1.0

def majFreq(f): afficher(freq=float(f),cutoff=curseur_c.get())
def majCutoff(c): afficher(freq=curseur_f.get(),cutoff=float(c))

def afficher(freq=1.0,cutoff=0.5):
    s = np.ones((Lx, Ly), float)
    for i in range(Lx):
        for j in range(Ly):
            radius = np.sqrt((i-i0)**2+(j-j0)**2)/float(Lx)
            s[i,j] = min(sinc(2*np.pi*freq*radius),cutoff)
    s /= np.max(s)
    for i in range(Lx):
        for j in range(Ly):
            gl = 255*s[i,j]
            img.put("#%02x%02x%02x" % (int(gl),int(gl),int(0.5*gl)), \
                    to=(i*taille_pixel,j*taille_pixel,(i+1)*taille_pixel,(j+1)*taille_pixel))

# création d'une sous-fenêtre pour les curseurs
fenetre_controle = tk.Frame(fenetre)
fenetre_controle.pack()

#insertion des curseurs, à compléter
curseur_f = ????
curseur_f.pack(side="left")
????
curseur_c = ????
curseur_c.pack(side="left")
????

# lancement de l'application
fenetre.mainloop()

Questions

  • Insérer deux curseurs dans le cadre contenu dans la variable fenetre_controle qui contrôlent chacun les variables freq et cutoff en complétant les ???? à la fin de l'exemple. Le premier parcourera $[0.4,10.0]$ avec une résolution de 0.2, le second $[0.05,0.7]$ avec une résolution de 0.05.
    • Quelles fonctions mettre pour l'argument command dans chacun des cas?
    • Rajouter une ligne après l'appel de pack pour initialiser la position du curseur.
  • Pourquoi a-t-on besoin des fonctions majFreq et majCutoff?
  • Pourquoi y a-t-il la ligne s /= np.max(s) dans la fonction afficher?