Jusqu'à présent, nous avons toujours présenté nos résultats à l'utilisateur en affichant des chaînes de caractères avec la fonction print(). Il est maintenant temps d'aller plus loin et de commencer à créer des applications graphiques. On entend par applications graphiques, des applications qui s'ouvriront dans une fenêtre et qui pourront recevoir des événements (clavier, souris, ...). Les fenêtres et les événements sont gérés par le système d'exploitation. Il faudra donc interagir avec l'OS pour réaliser ce genre d'application.
Comme vous le savez peut-être, Python est un langage extrêmement populaire. C'est dû en partie au fait que Python est assez facile à apprendre mais aussi au fait qu'il existe énormément de bibliothèques pour Python.
Une bibliothèque est un ensemble de fonctionnalités que l'on peut installer en plus de l'installation de base de Python. Ces bibliothèques peuvent être créées par n'importe qui. Il existe plusieurs moyen de les rendre disponibles au monde entier mais le plus courant est le dépôt de paquets PyPI.
Nous n'allons pas apprendre ici à créer et publier des paquets mais nous allons devoir en installer un. En effet, nous allons créer nos applications graphiques grâce à la bibliothèque Pygame. Pygame a l'avantage d'être facile à utiliser. De plus, c'est une librairie de bas niveau qui permet de bien comprendre ce qu'il se passe dans une application graphique.
Pour installer une bibliothèque provenant de PyPI, on utilise le module pip (Pip Install Package) de Python. Cela veut dire que nous allons utiliser l'interpréteur Python pour installer la bibliothèque. Comme toujours, l'interpréteur s'utilise en ligne de commande et nous allons donc taper la commande suivante pour installer Pygame :
Une fois que vous validerez la commande, vous verrez que le téléchargement de Pygame commence.
Lorsque Pygame est installé, il est possible de l'importer dans vos programmes :
import pygame
Voici le code d'un programme simple utilisant Pygame :
import pygame
# initialisation de pygame
pygame.init()
# demande une fenêtre de 800x600 pixels
screen = pygame.display.set_mode((800, 600))
# boucle jusqu'à ce qu'on quitte
while not pygame.event.peek(pygame.QUIT):
# dessine un cercle rouge centré en (100, 300) de
# rayon 20 dans le tampon caché
pygame.draw.circle(screen, (255, 0, 0), (100, 300), 20)
# échange les tampons
pygame.display.flip()
Nous pouvons voir dans ce programme que toutes les fonctionnalités de Pygame sont accessibles en tapant "pygame.". On dit que pygame est un espace de noms. Les espaces de noms permettent aux différentes bibliothèques d'utiliser les mêmes noms de fonctions sans qu'il y ait de conflits entre elles.
Un autre élément important dans le programme ci-dessus est qu'il se compose essentiellement d'une boucle while. Les programmes en ligne de commande que nous avons réalisés jusqu'à présent, quittaient immédiatement après avoir affiché leurs résultats. Les applications graphiques ne se comportent généralement pas comme ça. Elles continuent de tourner tant qu'on ne ferme pas la fenêtre. Le while qui est ici tourne tant que l'application ne reçoit pas l'événement pygame.QUIT. Cette boucle garde donc le programme en exécution. L'événement pygame.QUIT peut être déclenché de plusieurs façons. L'une d'entre elles est de fermer la fenêtre de l'application.
Avant de démarrer la boucle, on peut voir qu'il est nécessaire d'initialiser Pygame et de demander au système d'exploitation d'ouvrir une fenêtre.
Pour bien comprendre ce qu'il se passe dans la boucle, il faut savoir que chaque fenêtre contient au moins deux tampons de dessin. L'un d'eux est utilisé par la carte graphique de l'ordinateur pour afficher le contenu de la fenêtre à l'écran. Si on dessine sur ce tampon, le résultat est directement visible à l'écran. Cela peut poser problème car le dessin de l'interface d'un programme ne se fait pas instantanément et les étapes intermédiaires du dessin peuvent être visibles. C'est pour palier à ce problème qu'il existe un deuxième tampon de dessin qui est caché. L'idée est de faire le dessin de l'application sur le tampon caché et de ne l'échanger avec l'autre qu'au moment où le dessin est terminé. Dans la boucle, nous faisons donc deux choses : nous dessinons un cercle rouge dans le tampon caché de la fenêtre puis nous échangeons les tampons.
Le programme que nous venons de réaliser redessine le cercle rouge à chaque tour de boucle exactement au même endroit. On a donc l'impression que rien ne se passe dans notre application. Nous pourrions essayer de changer la position du cercle à chaque fois qu'on le dessine. Cela permettrait de donner l'impression que le cercle bouge.
import pygame
# initialisation de pygame
pygame.init()
# demande une fenêtre de 800x600 pixels
screen = pygame.display.set_mode((800, 600))
xCircle = 100
# boucle jusqu'à ce qu'on quitte
while not pygame.event.peek(pygame.QUIT):
# dessine un rectangle noir pour effacer l'image
pygame.draw.rect(screen, (0, 0, 0), pygame.Rect(0, 0, 800, 600))
# dessine un cercle rouge centré en (xCircle, 300) de rayon 20
pygame.draw.circle(screen, (255, 0, 0), (xCircle, 300), 20)
# déplace le cercle
xCircle += 1
# affiche l'image qui vient d'être dessinée
pygame.display.flip()
Dans ce code, nous avons créé une variable xCircle qui servira pour l'abscisse du centre du cercle et nous modifions sa valeur à chaque tour de boucle. Vous pouvez aussi voir qu'avant de dessiner le cercle, nous effaçons le tampon. Si nous ne le faisions pas, nous verrions toujours les cercles dessinés aux tours de boucle précédents.
En fonction de votre ordinateur, il est possible que l'animation du cercle soit trop rapide. En effet, dans son état actuel, la boucle while peut faire plusieurs centaines de tours par seconde. C'est inutile d'en faire autant sachant que la plupart des écrans ne se rafraichissent que 60 fois par seconde. Nous allons donc utiliser une Clock de Pygame pour temporiser notre boucle while.
import pygame
# initialisation de pygame
pygame.init()
# demande une fenêtre de 800x600 pixels
screen = pygame.display.set_mode((800, 600))
xCircle = 100
# la clock sert à limiter le nombre d'images par seconde
clock = pygame.time.Clock()
# boucle jusqu'à ce qu'on quitte
while not pygame.event.peek(pygame.QUIT):
# attend qu'il se soit passé au moins 1/60
# de seconde depuis le dernier tick
clock.tick(60)
# dessine un rectangle noir pour effacer l'image
pygame.draw.rect(screen, (0, 0, 0), pygame.Rect(0, 0, 800, 600))
# dessine un cercle rouge centré en (xCircle, 300) de rayon 20
pygame.draw.circle(screen, (255, 0, 0), (xCircle, 300), 20)
# déplace le cercle
xCircle += 1
# affiche l'image qui vient d'être dessinée
pygame.display.flip()
L'instruction pygame.time.Clock() crée une valeur de type Clock. Ce type de valeur a été créé par la bibliothèque Pygame. Remarquez que la valeur de type Clock est sauvegardée dans une variable nommée clock (avec un c minuscule). Il s'agit là d'une pratique assez courante en programmation. Une fois créée, la clock nous fournit la fonctionnalité tick qui nous permet de temporiser le programme.
La vitesse du cercle devrait maintenant être raisonnable et identique d'un ordinateur à l'autre.
On utilise généralement la souris et le clavier pour interagir avec une application graphique. Le système d'exploitation envoie ces interactions à notre application sous forme d'événements.
Dans le code que nous avons écrit plus haut, nous gérions déjà un événement particulier : pygame.QUIT. Cet événement est envoyé à notre application quand l'utilisateur la quitte (par exemple en fermant la fenêtre).
Tous les événements que le système d'exploitation envoie à notre application sont conservés par Pygame dans une file d'attente.
Pour gérer l'événement pygame.QUIT, nous avons utilisé la fonction pygame.event.peek() qui permet de vérifier si un événement d'un type particulier attend dans la file. La fonction pygame.event.peek() est surtout pratique quand on n'a qu'un type d'événement à gérer.
Pour gérer plusieurs types d'événements, nous allons utiliser une boucle pour parcourir la file d'événements. Nous allons donc transformer notre
# boucle jusqu'à ce qu'on quitte
while not pygame.event.peek(pygame.QUIT):
# ...
en
# boucle infinie
while True
# Boucler sur tous les events
for event in pygame.event.get():
# Faire quelque chose avec l'event
Nous pouvons voir que la boucle principale de notre programme est devenue une boucle infinie et qu'à chacun de ses tours, nous parcourons tous les événements de la file avec le for event in ....
Pour continuer de gérer l'événement pygame.QUIT, il faut faire quelque chose avec dans la boucle for. Ajoutons donc un if qui s'exécutera pour ce type d'événement :
while True
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit() # nécessite d'importer le module sys
Lorsque le type de l'événement est pygame.QUIT, on met fin au programme avec sys.exit(). Si l'on souhaite gérer d'autres types d'événements, il suffit d'ajouter les if correspondants.
Il y a deux types d'événements qui nous intéressent lorsqu'on veut gérer le clavier :
pygame.KEYDOWN : il a lieu lorsqu'une touche est appuyée,
pygame.KEYUP : il a lieu lorsqu'une touche est relâchée.
Pour savoir à quelle touche un événement clavier se rapporte, il faut inspecter event.key.
Voici l'exemple de notre cercle rouge qui bouge. Lorsqu'on appuie sur espace (event.key == pygame.K_SPACE), il change de sens.
import pygame
import sys
pygame.init()
screen = pygame.display.set_mode((800, 600))
clock = pygame.time.Clock()
xCircle = 100
vxCircle = 1
while True:
clock.tick(60)
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
# Change la vitesse de la balle si on appuie sur espace
if event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE:
vxCircle = -vxCircle
pygame.draw.rect(screen, (0, 0, 0), pygame.Rect(0, 0, 800, 600))
pygame.draw.circle(screen, (255, 0, 0), (xCircle, 300), 20)
# déplace le cercle
xCircle += vxCircle
pygame.display.flip()
Nous allons voir trois types d'événements souris :
pygame.MOUSEBUTTONDOWN : il a lieu quand on enfonce un bouton de la souris.
pygame.MOUSEBUTTONUP : il a lieu quand on relâche un bouton de la souris.
pygame.MOUSEMOTION : il a lieu lorsqu'on déplace la souris.
Pour les événements concernant les boutons de la souris, on peut consulter event.button pour savoir de quel bouton il s'agit. La valeur de event.button sera 1, 2 ou 3 en fonction du bouton pressé (gauche, milieu ou droit).
Les événements de bouton contiennent aussi la position de la souris au moment du clic. event.pos contient les coordonnées (x, y) de la souris au moment de l'événement.
Les événements pygame.MOUSEMOTION contiennent aussi la position de la souris dans event.pos. Ils contiennent également la différence de position depuis l'événement de déplacement précédent dans event.rel.
A titre d'exemple, on pourrait faire en sorte que le cercle rouge de notre programme précédent suive les déplacements horizontaux de la souris :
import pygame
import sys
pygame.init()
screen = pygame.display.set_mode((800, 600))
clock = pygame.time.Clock()
xCircle = 100
while True:
clock.tick(60)
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
# Change la position de la balle quand la souris se déplace
if event.type == pygame.MOUSEMOTION:
xCircle = event.pos[0]
pygame.draw.rect(screen, (0, 0, 0), pygame.Rect(0, 0, 800, 600))
pygame.draw.circle(screen, (255, 0, 0), (xCircle, 300), 20)
pygame.display.flip()
La bibliothèque pygame offre, bien entendu, beaucoup d'autres possibilités. Par exemple, nous n'avons vu que les fonctions de dessin pygame.draw.circle et pygame.draw.rect mais il en existe bien d'autres. Pour les découvrir, vous pouvez vous rendre sur la documentation officielle de Pygame.