Programmation évènementielle
Nous utiliserons la bibliothèque pyglet pour ce projet. Commencez par l'installer :
python -m pip install pyglet
Une fois la bibliothèque installée, on peut créer notre projet :
- créez un dossier nommé
essais-pyglet
où vous placerez vos fichiers - créez un projet vscode dans ce dossier
La programmation événementielle est un paradigme de programmation très utiliser dans les interfaces graphiques. Cette méthode consiste à réagir à des événements issus du programme comme de cliquer sur un bouton, appuyer sur une touche, etc.
Le principe est le suivant :
- on inscrit une fonction $f$ à un type d'événement $e$
- lorsque l'événement $e$ arrive, la fonction $f(e)$ est exécutée
Dans ce projet, les événements que nous aurons à manipuler seront les appuies sur les touches du clavier, le dessin de notre interface, la taille des fenêtres, etc, etc.
Cette méthode de programmation est basée sur le patron de conception observateur.
Nous allons utiliser pyglet qui est une bibliothèque permettant de créer des interfaces pour illustrer cela. Nous ne rentrerons pas dans tous les détails de son implémentation (utilisation d'opengl, la gestion du son, etc) mais nous utiliserons assez de ses fonctionnalités pour que vous puissiez aller plus loin de votre côté.
La documentation de pyglet est très bien faite et fourmille d'exemples.
Nous allons placer les différents essais de pyglet dans un sous-dossier de notre projet. Créer un dossier essais-pyglet
.
Hello world
Méthode subclassing window
Il y a plusieurs moyen me mettre en place une fenêtre pyglet. Nous allons utiliser la méthode la plus proche de la programmation objet qui consiste à faire de l'héritage.
une fenêtre
Fichier : essais-pyglet/fenêtre.py
:
import pyglet
class HelloWorldWindow(pyglet.window.Window):
def __init__(self):
super().__init__(400, 200, "texte")
self.label = pyglet.text.Label("Hello, world!")
def on_draw(self):
self.clear()
self.label.draw()
window = HelloWorldWindow()
print(window.get_size())
pyglet.app.run()
print("c'est fini !")
Exécutez le programme précédent.
Vous devriez voir apparaître une fenêtre avec écrit "Hello, world!" en blanc sur fond noir en bas à gauche de la fenêtre. Vous devriez aussi voir dans le terminal le texte : (400, 200)
qui est la taille de la fenêtre.
En revanche, le texte c'est fini !
ne devrait apparaître dans le terminal que lorsque la fenêtre se ferme.
Comprenez
- comment fonctionne le programme
- l'héritage de la classe
Window
de pyglet - la fonction de la ligne
pyglet.app.run()
Notre classe HelloWorldWindow
commence par appeler le constructeur de sa classe mère.
En pyglet, il est indispensable que ce soit la première ligne, car ce constructeur va gérer tout l'affichage de la fenêtre. Si vous ne faite pas cela, vous risquez d'avoir des erreurs de contexte opengl.
La méthode on_draw
sert à dessiner la fenêtre et est exécutée à chaque rafraîchissement de la fenêtre, c'est à dire beaucoup de fois par seconde (60 IPS = 60 images par seconde = 60 rafraîchissement de fenêtre par seconde = 60 FPS). Son code stipule que pour dessiner la fenêtre :
- on efface son contenu
- on dessine le label
Ajoutez au code de la méthode HelloWorldWindow.on_draw()
un print("Coucou du rafraîchissement !")
puis exécutez le code et :
- repérez où doit s'afficher notre phrase
- voyez comment elle est affichée souvent
corrigé
corrigé
# ...
def on_draw(self):
self.clear()
self.label.draw()
print("Coucou du rafraîchissement !")
# ...
print
affiche du texte à l'endroit où vous avez exécuté le programme. Vous verrez donc l'affichage dans le terminal et non dans le fenêtre.
En utilisant la fonction datetime.now()
du module datetime
:
>>> from datetime import datetime
>>> print(datetime.now())
2023-03-13 08:44:13.740575
>>>
Déterminez le nombre d'images par secondes de notre application.
corrigé
corrigé
from datetime import datetime
# ...
class HelloWorldWindow(pyglet.window.Window):
# ...
def on_draw(self):
self.clear()
self.label.draw()
print(datetime.now())
# ...
Le texte s'affiche toutes les 20ms environ soit, $1000/20 = 50$ IPS.
Une fenêtre redimensionnable
La fenêtre que nous venons de créer n'est pas redimensionnable (essayez d'augmenter sa taille, vous verrez qe vous n'y arrivez pas).
Lisez la documentation de la classe fenêtre de pyglet pour trouver comment créer une fenêtre redimensionnable. Puis faites le et testez le tout.
corrigé
corrigé
C'est le paramètre resizable
qu'il faut positionner à True
.
# ...
class HelloWorldWindow(pyglet.window.Window):
def __init__(self):
super().__init__(400, 200, "texte", resizable=True)
# ...
# ...
La méthode on_draw
est une méthode spéciale. A chaque fois que l'événement draw
est activé, cette méthode est exécutée. Pour le voir concrètement, regardons l'effet d'un redimensionnement sur la taille de la fenêtre :
import pyglet
class HelloWorldWindow(pyglet.window.Window):
def __init__(self):
super().__init__(400, 200, "texte", resizable=True)
self.label = pyglet.text.Label("Hello, world!")
def on_draw(self):
print("taille de la fenêtre :", self.get_size())
self.clear()
self.label.draw()
window = HelloWorldWindow()
print(window.get_size())
pyglet.app.run()
print("c'est fini !")
On récapitule
- l'ajout d'un paramètre lors de l'appel au construction de
Window
qui rend la fenêtre redimensionnable - l'ajout d'un
print
dans la méthodeon_draw
- lorsque l'on change la taille de la fenêtre, la méthode
on_draw
est exécutée
La méthode on_draw
étant exécutée à chaque rafraîchissement, le print
dans cette fonction va vite devenir pénible. Supprimez le.
Modifiez le code de la méthode on_draw
pour qu'il soit identique au code ci-dessous :
# ...
def on_draw(self):
self.clear()
self.label.draw()
# ...
Texte au milieu de la fenêtre
Un label est un objet non modifiable qui peut-être affiché (dans la méthode on_draw
).
Remplacez sa création dans le fichier essais-pyglet/fenêtre.py
par :
# ...
self.label = pyglet.text.Label(
"Hello, world!",
x=self.width // 2,
y=self.height // 2,
anchor_x="center",
anchor_y="center",
)
# ...
En exécutant le code, le texte est placé au milieu de l'écran ! En revanche, lorsque vous modifiez la taille de la fenêtre pendant l'exécution du programme, la position ne change pas.
- déduire l'origine de la fenêtre en utilisant le redimensionnement de la fenêtre.
- utiliser l’événement
on_resize(width, height)
pour replacer le label à la bonne position après chaque redimensionnement en modifiant ses attributsx
ety
.
La méthode on_resize
est utilisée par Window
, n’oubliez pas de l’appeler avec un super
.
solution
solution
l'origine est en bas à gauche de la fenêtre.
class HelloWorldWindow(pyglet.window.Window):
#...
def on_resize(self, width, height):
super().on_resize(width, height)
self.label.x = width // 2
self.label.y = height // 2
# ...
Gestion du clavier
On utilise deux événements pour gérer le clavier :
on_key_press(symbol, modifiers)
qui s'active lorsque qu'une touche est appuyéeon_key_release(symbol, modifiers)
qui s'active lorsque qu'une touche est relâchée
Le paramètre symbol
est un entier qui correspond au code de la touche et modifiers
gère les touches comme shift
, control
ou encore alt
.
Ajoutons une gestion basique du clavier dans le programme :
class HelloWorldWindow(pyglet.window.Window):
#...
def on_key_press(self, symbol, modifiers):
print("press:", symbol, modifiers)
def on_key_release(self, symbol, modifiers):
print("release:", symbol, modifiers)
# ...
Nous n'avons pas utilisé de super
pour appeler la méthode de la classe mère, car Window
ne gère pas le clavier par défaut.
Exécutez le code précédent et remarquez
- que chaque touche a bien un code, ainsi que les touches de modification
- shift gauche et shift droit sont discernables
- qu'après chaque touche appuyée ou relâchée l'évènement
on_draw
est lancé - que même si on laisse appuyé la touche longtemps, il n'y a qu'un seul événement
on_key_press
qui est lancé.
Flèches gauche et droite
Les code des différentes touches est disponible dans l'objet pyglet.window.key.
Chaque touche est une constante dont le nom correspond à la la touche et sa valeur au code. Par exemple, la constante pyglet.window.key.SPACE
correspond au nombre 32.
Vérifiez que lorsque vous appuyez sur la touche espace de votre clavier, c'est bien le symbole 32 qui et affiché
Nous allons maintenant faire bouger d'un cran notre texte lorsque l'on appuie sur les touches "flèche gauche" et "flèche droite".
En utilisant le fait que les deux attributs x
et y
contiennent la position du label : faite en sorte que lorsque l'on appuie sur une flèche du clavier (le nom des constantes de pyglet.window.key
correspondant aux flèches sont disponible là), le texte se déplace de 10 pixels vers la direction de la flèche.
solution
solution
# ...
from pyglet.window import key
# ...
class HelloWorldWindow(pyglet.window.Window):
# ...
def on_key_press(self, symbol, modifiers):
if symbol == key.UP:
self.label.y += 10
elif symbol == key.DOWN:
self.label.y -= 10
elif symbol == key.LEFT:
self.label.x -= 10
elif symbol == key.RIGHT:
self.label.x += 10
# ...
Avec cette technique, on ne peut se déplacer que d'un cran par appui sur la touche. Pour gérer les déplacements continus, il faut prendre en compte le temps d'appui sur la touche. C'est le boulot de la partie suivante.
Touches de modifications
Les touches de modifications sont gérées par un bit field : une touche de modification correspond à un bit qui est positionné à 1 si la touche est enfoncée et à 0 sinon. Ceci permet :
- de gérer plusieurs modificateurs avec un seul entier
- de savoir rapidement avec des traitements logiques si tel ou tel touches de modification est enfoncée.
Les différents modificateurs sont donnés :
>>> from pyglet.window import key
>>> print(key.MOD_SHIFT)
1
>>> print(bin(key.MOD_SHIFT))
0b1
>>> print(key.MOD_ALT)
4
>>> print(bin(key.MOD_ALT))
0b100
>>>
Savoir si un modificateur particulier est appuyé se fait avec des conditions sur les bits comme &
ou |
par exemple.
Essayez le code suivant pour savoir si la touche shift
est enfoncée ou non :
from pyglet.window import key
# ...
class HelloWorldWindow(pyglet.window.Window):
# ...
def on_key_press(self, symbol, modifiers):
if modifiers & key.MOD_SHIFT:
print("touche shift appuyée")
# ...
Allons un peu plus loin :
Comment faire pour savoir si la touche shift
ou la touche control
est enfoncée ?
corrigé
corrigé
class HelloWorldWindow(pyglet.window.Window):
# ...
def on_key_press(self, symbol, modifiers):
if modifiers & (key.MOD_SHIFT | key.MOD_CTRL):
print("touche shift ou ctrl appuyée")
# ...
Comment faire pour savoir si la touche shift
et la touche control
est enfoncée ?
corrigé
corrigé
class HelloWorldWindow(pyglet.window.Window):
# ...
def on_key_press(self, symbol, modifiers):
if (modifiers & (key.MOD_SHIFT | key.MOD_CTRL)) == key.MOD_SHIFT | key.MOD_CTRL:
print("touche shift ou ctrl appuyée")
# ...
Gestion du temps
La gestion du temps se fait également par un événement. Sa mise en place est cependant différente des événements que l'on a vu jusqu'à présent :
class HelloWorldWindow(pyglet.window.Window):
# ...
def __init__(self):
super().__init__(400, 200)
# ...
pyglet.clock.schedule_interval(self.update, 1)
# ...
def update(self, dt):
print(dt)
# ...
Le code précédent fait en sorte que la méthode update
soit exécutée toute les secondes. Le paramètre dt
donne le nombre de secondes exactes depuis le dernier appel de la fonction. Cela permet de gérer le lag s'il existe (remarquez qu'il vaut toujours un peut plus que 1).
Faites en sorte que qu'un texte avance de 10 pixels toutes les 0.5s si une touche est appuyée.
Pour cela on ne va pas modifier la position du label dans on_key_press
mais dans update :
- créez deux attributs
dx
etdy
à notre objetHelloWorldWindow
. Par défaut ces deux attributs vaudront 0 - à chaque appelle de
update
, bougez la position du label deself.dx
etself.dy
- gérez les valeurs de
self.dx
etself.dy
danson_key_press
eton_key_release
(par exempleself.dx = -10
lorsque l'on appuie sur la flèche gauche etself.dx = 0
lorsque la flèche gauche est relâchée)
solution
solution
# ...
class HelloWorldWindow(pyglet.window.Window):
def __init__(self):
self.label = pyglet.text.Label(
"Hello, world!",
x=self.width // 2,
y=self.height // 2,
anchor_x="center",
anchor_y="center",
)
pyglet.clock.schedule_interval(self.update, .5)
self.dx = 0
self.dy = 0
# ...
def update(self, dt):
self.label.x += self.dx
self.label.y += self.dy
# ...
def on_key_press(self, symbol, modifiers):
if symbol == key.UP:
self.dy = 10
elif symbol == key.DOWN:
self.dy = -10
elif symbol == key.LEFT:
self.dx = -10
elif symbol == key.RIGHT:
self.dx = +10
def on_key_release(self, symbol, modifiers):
if symbol == key.UP:
self.dy = 0
elif symbol == key.DOWN:
self.dy = 0
elif symbol == key.LEFT:
self.dx = 0
elif symbol == key.RIGHT:
self.dx = 0
# ...
# ...
Avec cette méthode le texte va continuer de se déplacer tant qu'une flèche reste appuyée. On peut même taper plusieurs flèches en même temps pour se déplacer en diagonale (cool, non ?).
Il reste un problème : le texte va sortir de la fenêtre si on reste appuyé trop longtemps. Corrigez ça :
Ajoutez à votre code une sentinelle qui empêche les coordonnées x
et y
du label de sortir hors de la fenêtre.
Les dimensions de la fenêtres sont données par ses attributs width
et height
.
solution
solution
class HelloWorldWindow(pyglet.window.Window):
# ...
def update(self, dt):
self.label.x += self.dx
if self.label.x < 0:
self.label.x = 0
elif self.label.x > self.width:
self.label.x = self.width
self.label.y += self.dy
if self.label.y < 0:
self.label.y = 0
elif self.label.y > self.height:
self.label.y = self.height
# ...
Gestion de la souris
Pour gérer la souris, comme vous pouvez vous en douter, il s'agit de s'abonner à des événements. Il en existe plusieurs. Commençons par voir ce que ça donne avec les événements on_mouse_press(x, y, button, modifiers)
et on_mouse_release(x, y, button, modifiers)
:
class HelloWorldWindow(pyglet.window.Window):
# ...
def on_mouse_press(self, x, y, button, modifiers):
print("press:", x, y, button)
if (abs(self.label.x - x) <= self.label.content_width / 2) and (
abs(self.label.y - y) <= self.label.content_height / 2
):
print("clique dans le label")
def on_mouse_release(self, x, y, button, modifiers):
print("release:", x, y, button)
if (abs(self.label.x - x) <= self.label.content_width / 2) and (
abs(self.label.y - y) <= self.label.content_height / 2
):
print("relâcher le bouton dans le label")
# ...
Lorsque vous cliquez sur un bouton de la souris puis que vous le relâchez, vous devriez voir affiché à l'écran la position du curseur ainsi que le numéro du bouton de la souris qui a servi à cliquer.
Cerise sur le gâteau, lorsque vous cliquez ou relâchez le bouton de la souris sur le label, cela devrait vous l'indiquer.
Boutons
Se fait comme pour les modificateurs de touches, avec un bit field. Les valeurs des bits concernés sont définis
par les constantes suivantes, accessibles après l'import from pyglet.window import mouse
:
mouse.LEFT
mouse.MIDDLE
mouse.RIGHT
Modifiez le code précédent pour que l'on ne prenne en compte l'appuie dans le label que si l'on a cliqué avec le bouton de gauche.
solution
solution
from pyglet.window import mouse
class HelloWorldWindow(pyglet.window.Window):
# ...
def on_mouse_press(self, x, y, button, modifiers):
print("press:", x, y, button)
if not (button & mouse.LEFT):
return
if (abs(self.label.x - x) <= self.label.content_width / 2) and (
abs(self.label.y - y) <= self.label.content_height / 2
):
print("clique dans le label")
# ...
Mouvements
En utilisant l'événement on_mouse_motion(self, x, y, dx, dy)
repérez quand la souris rentre et sort du label. N'hésitez pas à regarder la documentation de l'événement pour comprendre la définition de chaque paramètre.
solution
solution
class HelloWorldWindow(pyglet.window.Window):
# ...
def on_mouse_motion(self, x, y, dx, dy):
if (abs(self.label.x - x) <= self.label.content_width / 2) and (
abs(self.label.y - y) <= self.label.content_height / 2
):
if (abs(self.label.x - (x - dx)) > self.label.content_width / 2) or (
abs(self.label.y - (y - dy)) > self.label.content_height / 2
):
print("entre dans label")
else:
if (abs(self.label.x - (x - dx)) <= self.label.content_width / 2) and (
abs(self.label.y - (y - dy)) <= self.label.content_height / 2
):
print("sort du label")
# ...
Dessiner des formes
La documentation permet de voir que l'on peut facilement dessiner des cercle ou des rectangles en pyglet.
De façon générale, la gestion des graphique en pyglet
se fait directement en opengl, ce qui dépasse de loin le cadre de ce cours (même si c'est chouette de parler directement à la carte graphique). Nous allons donc uniquement nous restreindre au dessin d'un cercle et d'un rectangle ce qui sera suffisant pour notre projet.
Fichier : essais-pyglet/forme.py
:
import pyglet
from pyglet import shapes
from pyglet.window import mouse
class Formes(pyglet.window.Window):
def __init__(self):
super().__init__(640, 480, "formes")
self.rectangle = shapes.Rectangle(200, 200, 200, 200, color=(55, 55, 255))
self.circle = shapes.Circle(0, 0, 50, color=(255, 0, 0))
self.circle.opacity = 128
def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers):
if buttons & mouse.LEFT and (
(self.circle.x - x) ** 2 + (self.circle.y - y) ** 2
<= self.circle.radius ** 2
):
self.circle.x += dx
self.circle.y += dy
def on_draw(self):
self.clear()
self.rectangle.draw()
self.circle.draw()
forme = Formes()
pyglet.app.run()
print("c'est fini !")
Testez l'exemple ci-dessus et comprenez ce qu'il fait.
Les couleurs sont décrites au format RGB sous la forme de 3 entiers allant de 0 à 255 en base 10 :
- le premier décrit la composante rouge
- le second la composante verte
- le dernier la composante bleue
On a souvent coutume (dans le monde du web par exemple) de représenter ces 3 nombres par un nombre hexadécimal de 6 chiffres (2 par composante, chaque composante étant codée par un nombre allant de 00 à FF). Par exemple, le nombre #F58318
correspond à la couleur ayant F5 en rouge, 83 en vert et 18 en bleu, les 3 nombres étant en codage hexadécimal. Ce qui en python donne avec un tuple de 3 coordonnées : (0xF5, 0x83, 0x18)
, ou (245, 131, 24)
en base 10 (un nombre écrit en hexadécimal en python commence par 0x
).
Pour gérer et trouver des couleurs sympathiques, utilisez une roue des couleurs, comme celle d'adobe par exemple.