Animations intervactives avec Pyside6 (Qt avec Python)
Préambule Par rapport au cours de NSI
L'utilisation d'objet permettant un rendu graphique n'est pas un attendu en NSI.
Je présente cependant ici le module Pyside6 (Qt pour Python) qui permet de contruire des interfaces graphiques.
Le but est d'être au service des élèves pour libérer leur créativité lors des projets.
Un maximum de la programmation orienté objet est volontairement gommé, pour une utilisation dès la classe de première.
Prérequis
On suppose abordées les bases de la programmation en Python ainsi que la notion d'événement (vu en Web)
Installation Environnement et PySide6
Comme pour tout projet en python, il est bon de préparer un dossier et un environnement de travail.
Pour mes élèves de premières, qui travaillent, en général, sous VSCode (proposé à l'installation sur les ordinateurs fournis par la région), voici un pense-bête :
Créer un nouveau dossier
Y créer le fichier requirements.txt contenant le simple mot pyside6
Ouvrir le dossier comme l'espace de travail sous VSCode
Appuyer [Ctrl]+[Mal]+[P] et taper Python: Create Environment...
Cliquer Venv
Cliquer sur la version de Python (déjà installée sur la machine) souhaitée
Cocher l'utilisation du fichier requirements.txt
L'espace de travail s'installe alors et devient prêt à tester un premier script...
Dans un script Python :
from PySide6.QtWidgets import QApplication, QWidget
# Création de l'application (une seule, quelque soit le nombre de fenêtres)
app = QApplication()
# Création d'un Widget (composant graphique)
widget = QWidget()
# Affichage du Widget comme nouvelle fenêtre
widget.show()
# Lance la boucle événementielle de l'application (indispensable)
app.exec()
Widget
Un Widget (WIndows gadGET) est la partie élémentaire (rectangulaire) de l'interface utilisateur.
Un Widget non intégré dans un Widget parent est une fenêtre et son affichage à l'écran est provoqué par la méthode show().
Généralement, on utilise une QMainWindow. Les autres widgets étant vus comme des composants de la fenêtre.
Exemple avec un QLabel (une étiquette ou post-it) qui peut contenir du texte et/ou une image.
from PySide6.QtWidgets import (
QApplication,
QMainWindow,
QLabel
)
# Application
app = QApplication()
# Fenêtre
fenetre = QMainWindow()
# Widget dans la fenêtre
message = QLabel(fenetre)
message.setText('Hello World')
# Affichage une fois tous les widgets configurés
fenetre.show()
app.exec()
QMainWindow
Le widget QMainWindow est en fait organisé pour acceuillir une barre de menu, des barres d'outils, ...
et une place centrale, libre, pour ses (sous-)widgets
Pour définir le Widget central, on utilise la méthode setCentralWidget()
Décommenter la ligne 19 pour visualiser la différence. (Au lieu de s'ajouter en haut à gauche de la fenêtre, le QLabel prend la place centrale.)
from PySide6.QtWidgets import (
QApplication,
QMainWindow,
QLabel
)
# Application
app = QApplication()
# Fenêtre
fenetre = QMainWindow()
# Widget dans la fenêtre
message = QLabel(fenetre)
message.setText('Hello World')
message.setStyleSheet('background-color:red')
#fenetre.setCentralWidget(message)
# Affichage une fois tous les widgets configurés
fenetre.show()
app.exec()
Layout
On peut définir un Layout (un agencement) de widgets les uns par rapport aux autres dans le plan.
Il existe trois Layers principaux :
Vertical : QVBoxLayout
Horizontal : QHBoxLayout
Grille : QGridLayout
On ne peut pas définir un tel Layout directement dans une QMainWindow, mais on peut le
faire dans sa partie centrale. Il suffit d'y placer un QWidget générique :
from PySide6.QtWidgets import (
QApplication,
QMainWindow,
QWidget,
QVBoxLayout,
QLabel,
QPushButton
)
app = QApplication()
fenetre = QMainWindow()
centralWidget = QWidget(fenetre)
fenetre.setCentralWidget(centralWidget)
# Layout
layout = QVBoxLayout(centralWidget)
# Premier Widget du layout
message = QLabel(centralWidget)
message.setText('Hello World')
layout.addWidget(message)
# Second Widget du layout
bouton = QPushButton(centralWidget)
bouton.setText('OK')
nb_click = 0
# Réponse au clique : il faut définir une fonction
def click_ok():
global nb_click
nb_click = nb_click + 1
message.setText(str(nb_click))
# Connexion de la gestion au click de ce bouton
bouton.clicked.connect(click_ok)
layout.addWidget(bouton)
fenetre.show()
app.exec()
Scène 2D
Pour pouvoir dessiner des formes géométriques ou des images, les faire bouger,
et gérer l'interactivité avec ces formes,
on définit la scène avec QGraphicsScene,
on définit ses éléments (item) avec des QGraphicsItem, par exemple
QGraphicsRectItem, QGraphicsPolygonItem, QGraphicsEllipseItem
, QGraphicsPixmapItem...
on les ajoute à la scène avec la méthode addItem(),
on constuit définit un Widget, vue de la scène, avec QGraphicsView
On peut priciser le remplissage en créant une QBrush et le contour avec un QPen
from PySide6.QtGui import QBrush, QPen, QColor
from PySide6.QtWidgets import (
QApplication, QMainWindow, QWidget,
QVBoxLayout,
QGraphicsScene, QGraphicsRectItem, QGraphicsView
)
app = QApplication()
fenetre = QMainWindow()
centralWidget = QWidget(fenetre)
fenetre.setCentralWidget(centralWidget)
layout = QVBoxLayout(centralWidget)
# Création de la scène
scene = QGraphicsScene(0, 0, 400, 200)
# Définition d'un rectangle
rect = QGraphicsRectItem(0, 0, 200, 50)
# Repositionnement
rect.setPos(50, 20)
# Couleur de remplissage
brush = QBrush(QColor(255,0,0))
rect.setBrush(brush)
# Bordure
pen = QPen(QColor(0,255,255))
pen.setWidth(10)
rect.setPen(pen)
# Ajout à la scène
scene.addItem(rect)
# Création de la vue
view = QGraphicsView(scene)
# Ajout de la vue dans la fenêtre
layout.addWidget(view)
fenetre.show()
app.exec()
Animation
Pour générer une animation dans une zone de dessin,
il suffit de mettre régulièrement à jour les coordonnées
des formes dessinées.
On ne peut pas faire une boucle pour ça, car une fois
l'application lancée par la méthode exec(),
on est coincé dans la boucle de lecteure des événement.
On va donc créer à intervalle régulier un événement qui
déclenchera la mise à jour des coordonnées. On le fait
avec la méthode un QTimer en définissant un
intervalle de temps et une fonction à appeller à l'issue de chacun.
Pour générer environ 30 image par seconde (30FPS) on peut
choisir un intervalle de 16ms.
from PySide6.QtCore import QTimer
from PySide6.QtGui import QBrush, QColor
from PySide6.QtWidgets import (
QApplication, QMainWindow, QWidget,
QVBoxLayout,
QGraphicsScene, QGraphicsEllipseItem, QGraphicsView
)
app = QApplication()
fenetre = QMainWindow()
centralWidget = QWidget(fenetre)
fenetre.setCentralWidget(centralWidget)
layout = QVBoxLayout(centralWidget)
# Création de la scène
scene = QGraphicsScene(0, 0, 400, 200)
# Ajout d'une balle
balle = QGraphicsEllipseItem(0, 0, 20, 20)
balle.setBrush(QBrush(QColor(255,0,0)))
scene.addItem(balle)
# Création de la vue
view = QGraphicsView(scene)
layout.addWidget(view)
# Définition du déplacement (sur un intervalle de temps)
x, y = 0, 0
def update():
global x, y
x = x + 1
y = y + 1
balle.setPos(x, y)
# Appel de update toutes les 16ms
timer = QTimer()
timer.timeout.connect(update)
timer.start(16)
fenetre.show()
app.exec()
Interactivité
Les Widgets, en particulier QGraphicsView ont des méthodes
qui se déclenchent quand un événement se déclenche sur eux :
keyPressEvent(), keyReleaseEvent(),
mousePressEvent(), mouseDoubleClickEvent(),
mouseReleaseEvent(), mouseMoveEvent()...
Pour réagir à un événement, il suffir de réécrire ces fonctions.
from PySide6.QtCore import Qt, QTimer
from PySide6.QtGui import QBrush, QColor
from PySide6.QtWidgets import (
QApplication, QMainWindow, QWidget,
QVBoxLayout,
QGraphicsScene, QGraphicsEllipseItem, QGraphicsView
)
app = QApplication()
fenetre = QMainWindow()
centralWidget = QWidget(fenetre)
fenetre.setCentralWidget(centralWidget)
layout = QVBoxLayout(centralWidget)
# Création de la scène
scene = QGraphicsScene(0, 0, 400, 200)
# Ajout d'une balle
balle = QGraphicsEllipseItem(0, 0, 20, 20)
balle.setBrush(QBrush(QColor(255,0,0)))
scene.addItem(balle)
# Création de la vue
view = QGraphicsView(scene)
layout.addWidget(view)
# Définition du déplacement (sur un intervalle de temps)
x, y = 0, 0
dx, dy = 0, 0
def update():
global x, y
x = (x+10 + dx)%410-10
y = (y+10 + dy)%210-10
balle.setPos(x, y)
# Appel de update toutes les 16ms
timer = QTimer()
timer.timeout.connect(update)
timer.start(16)
# Gestion de l'interactivité
def keyPressEvent(event):
"""Réaction aux touches du clavier"""
global dx, dy
if event.key() == Qt.Key_Up:
dy = -1
if event.key() == Qt.Key_Down:
dy = 1
if event.key() == Qt.Key_Left:
dx = -1
if event.key() == Qt.Key_Right:
dx = 1
view.keyPressEvent = keyPressEvent
def mousePressEvent(event):
"""Réaction aux clics de la souris"""
global x, y
x = event.position().x()-10
y = event.position().y()-10
view.mousePressEvent = mousePressEvent
fenetre.show()
app.exec()