16 - GUI (ouverture)
Ouverture hors programme. Les interfaces graphiques ne sont pas au programme de NSI, mais elles rendent les projets plus attrayants. Cette fiche présente trois bibliothèques :
tkinter, fournie avec Python,pygamepour les jeux etstreamlitpour les applications web. Les deux dernières s’installent avecpip install pygameetpip install streamlit.
Une interface graphique (GUI, graphical user interface) se programme toujours de la même façon : on crée une fenêtre et ses composants (boutons, champs de saisie, textes), on associe des fonctions aux actions de l’utilisateur, puis on lance une boucle d’événements qui attend ces actions.
Tkinter
Tkinter, exemple 1 : saisie et affichage
L’exemple suivant crée une fenêtre avec un texte, un champ de saisie et un bouton. La fonction afficher est appelée quand on valide la saisie avec la touche Entrée.
import tkinter as tk
def afficher(event):
"""Affiche un message de bienvenue avec le prénom saisi."""
accueil.configure(text="Bonjour " + champ.get() + " !")
# Fenêtre principale
fenetre = tk.Tk()
fenetre.title("Ma première fenêtre")
fenetre.minsize(width=300, height=80)
# Un texte, un champ de saisie et un bouton, empilés dans l'ordre
consigne = tk.Label(fenetre, text="Entrez votre prénom puis validez :")
consigne.pack()
champ = tk.Entry(fenetre, width=30)
champ.bind("<Return>", afficher) # touche Entrée : appel de afficher
champ.pack()
accueil = tk.Label(fenetre, text="")
accueil.pack()
bouton_quitter = tk.Button(fenetre, text="Quitter", command=fenetre.destroy)
bouton_quitter.pack()
# Boucle d'événements, interrompue quand on ferme la fenêtre
fenetre.mainloop()
Les composants de l’interface (widgets) sont placés les uns sous les autres, dans l’ordre des appels à pack(). La méthode grid() de l’exemple suivant permet de les disposer dans une grille.
Tkinter, exemple 2 : conversion Fahrenheit vers Celsius
import tkinter as tk
def convertir(event=None):
"""Convertit la température saisie, en degrés Fahrenheit, en degrés Celsius."""
fahrenheit = float(temperature.get())
celsius = 5 / 9 * (fahrenheit - 32)
resultat.config(text=f"{celsius:.2f} °C")
fenetre = tk.Tk()
fenetre.title("Conversion de Fahrenheit en Celsius")
consigne = tk.Label(fenetre, text="Température en degrés Fahrenheit :")
temperature = tk.Entry(fenetre)
temperature.bind("<Return>", convertir)
resultat = tk.Label(fenetre, text="")
bouton_convertir = tk.Button(fenetre, text="Convertir", command=convertir)
bouton_quitter = tk.Button(fenetre, text="Quitter", command=fenetre.destroy)
# Disposition en grille : ligne (row) et colonne (column)
consigne.grid(row=0, column=0, sticky="w", padx=5, pady=5)
temperature.grid(row=0, column=1, padx=5, pady=5)
bouton_convertir.grid(row=1, column=1, padx=5, pady=5)
resultat.grid(row=1, column=0, sticky="w", padx=5, pady=5)
bouton_quitter.grid(row=2, column=1, padx=5, pady=5)
fenetre.mainloop()
Le paramètre event=None permet d’appeler convertir aussi bien depuis le bouton (sans argument) que depuis la touche Entrée (avec un argument décrivant l’événement).
Tkinter, exemple 3 : image de fond
Pour afficher une photo, on utilise la bibliothèque Pillow (module PIL), qui s’installe avec pip install pillow. L’image est placée dans une étiquette (Label) qui occupe toute la fenêtre ; le titre et le bouton sont ensuite posés sur cette étiquette avec place(), qui les positionne en proportion de sa taille : relx=0.5 signifie « au milieu de la largeur ». Le programme suppose qu’une image nommée paysage.jpg se trouve dans le même dossier.
import tkinter as tk
from PIL import Image, ImageTk
fenetre = tk.Tk()
fenetre.title("Image de fond")
# Ouverture de l'image (fichier placé dans le même dossier que le programme)
image = Image.open("paysage.jpg")
image = image.resize((600, 400))
photo = ImageTk.PhotoImage(image)
# L'image occupe toute la fenêtre ; les autres widgets sont placés par-dessus
fond = tk.Label(fenetre, image=photo)
fond.pack()
titre = tk.Label(fond, text="J'adore Python !", font=("Arial", 20))
titre.place(relx=0.5, rely=0.3, anchor="center")
bouton = tk.Button(fond, text="Quitter", command=fenetre.destroy)
bouton.place(relx=0.5, rely=0.7, anchor="center")
fenetre.mainloop()
La variable photo doit rester définie tant que la fenêtre est ouverte : si l’image n’était plus stockée dans aucune variable, Python la supprimerait de la mémoire et la fenêtre resterait vide.
Pygame
Pygame dessine dans une fenêtre et gère le clavier, la souris et le temps. Un programme Pygame est une boucle qui, à chaque tour, lit les événements, met à jour l’état du jeu, dessine l’écran et attend le tour suivant.
Pygame, exemple 1 : déplacer un carré au clavier
À chaque tour de boucle, pygame.key.get_pressed() indique quelles touches sont enfoncées : tant qu’une flèche est maintenue, le carré avance de 4 pixels par image dans sa direction. Les fonctions min et max le retiennent à l’intérieur de la fenêtre.
import pygame
FOND = (30, 30, 60)
JAUNE = (250, 200, 40)
LARGEUR, HAUTEUR = 600, 400
COTE = 40
pygame.init()
ecran = pygame.display.set_mode((LARGEUR, HAUTEUR))
pygame.display.set_caption("Déplacer un carré au clavier")
horloge = pygame.time.Clock()
# Position du coin supérieur gauche du carré, et vitesse en pixels par image
x, y = 280, 180
vitesse = 4
fini = False
while not fini:
# 1. Événements : seule la fermeture de la fenêtre nous intéresse ici
for evenement in pygame.event.get():
if evenement.type == pygame.QUIT:
fini = True
# 2. Mise à jour : on regarde quelles flèches sont enfoncées
touches = pygame.key.get_pressed()
if touches[pygame.K_LEFT]:
x = x - vitesse
if touches[pygame.K_RIGHT]:
x = x + vitesse
if touches[pygame.K_UP]:
y = y - vitesse
if touches[pygame.K_DOWN]:
y = y + vitesse
# Le carré ne doit pas sortir de la fenêtre
x = max(0, min(x, LARGEUR - COTE))
y = max(0, min(y, HAUTEUR - COTE))
# 3. Dessin
ecran.fill(FOND)
pygame.draw.rect(ecran, JAUNE, [x, y, COTE, COTE])
pygame.display.flip()
# 4. Attente : 60 images par seconde au plus
horloge.tick(60)
pygame.quit()
L’origine des coordonnées est le coin supérieur gauche de la fenêtre : y augmente quand on descend, c’est pourquoi la flèche vers le bas ajoute la vitesse à y.
Pygame, exemple 2 : attraper une cible à la souris
Une cible ronde apparaît à un endroit tiré au hasard. Un clic de souris produit un événement MOUSEBUTTONDOWN, dont l’attribut pos donne la position du clic. Si le clic tombe dans le disque, le score augmente et la cible change de place.
import random
import pygame
BLANC = (255, 255, 255)
ROUGE = (220, 40, 40)
NOIR = (0, 0, 0)
LARGEUR, HAUTEUR = 600, 400
RAYON = 25
pygame.init()
ecran = pygame.display.set_mode((LARGEUR, HAUTEUR))
pygame.display.set_caption("Attraper la cible")
horloge = pygame.time.Clock()
police = pygame.font.SysFont("Arial", 24)
def nouvelle_position():
"""Renvoie un centre au hasard, la cible restant entière dans la fenêtre."""
return (random.randint(RAYON, LARGEUR - RAYON),
random.randint(RAYON + 40, HAUTEUR - RAYON))
centre = nouvelle_position()
score = 0
fini = False
while not fini:
for evenement in pygame.event.get():
if evenement.type == pygame.QUIT:
fini = True
elif evenement.type == pygame.MOUSEBUTTONDOWN:
# Distance entre le clic et le centre de la cible
dx = evenement.pos[0] - centre[0]
dy = evenement.pos[1] - centre[1]
if dx * dx + dy * dy <= RAYON * RAYON:
score = score + 1
centre = nouvelle_position()
ecran.fill(BLANC)
pygame.draw.circle(ecran, ROUGE, centre, RAYON)
texte = police.render(f"Score : {score}", True, NOIR)
ecran.blit(texte, (10, 10))
pygame.display.flip()
horloge.tick(60)
pygame.quit()
Le clic est dans le disque quand sa distance au centre est au plus égale au rayon. Comparer les carrés de ces deux longueurs évite de calculer une racine carrée.
Pour aller plus loin, la documentation de Pygame (en anglais) décrit toutes les fonctions et propose des tutoriels.
Streamlit
Streamlit transforme un script Python en application web : chaque appel à st.… ajoute un composant à la page. On exécute le script avec la commande streamlit run mon_fichier.py, qui ouvre l’application dans le navigateur ; le script est réexécuté à chaque action de l’utilisateur.
import streamlit as st
st.title("Convertisseur de températures")
fahrenheit = st.slider("Température en degrés Fahrenheit", -40, 120, 68)
celsius = 5 / 9 * (fahrenheit - 32)
st.write(f"{fahrenheit} °F correspondent à {celsius:.1f} °C.")
gauche, droite = st.columns(2)
if gauche.button("Eau qui gèle"):
droite.write("0 °C, soit 32 °F")
if gauche.button("Eau qui bout"):
droite.write("100 °C, soit 212 °F")
with st.expander("Comment est faite la conversion ?"):
st.write("On retire 32, puis on multiplie par 5/9.")
st.code("celsius = 5 / 9 * (fahrenheit - 32)")