stu:python_gui:pyqt_projet_animation

Warning: Undefined array key "lang" in /home/signac/doku/lib/plugins/wrap/helper.php on line 94

Warning: Undefined array key "id" in /home/signac/doku/lib/plugins/wrap/helper.php on line 117

Warning: Undefined array key "width" in /home/signac/doku/lib/plugins/wrap/helper.php on line 119

Warning: Undefined array key "dir" in /home/signac/doku/lib/plugins/wrap/helper.php on line 128

Warning: Undefined array key "lang" in /home/signac/doku/lib/plugins/wrap/helper.php on line 94

Warning: Undefined array key "id" in /home/signac/doku/lib/plugins/wrap/helper.php on line 117

Warning: Undefined array key "width" in /home/signac/doku/lib/plugins/wrap/helper.php on line 119

Warning: Undefined array key "dir" in /home/signac/doku/lib/plugins/wrap/helper.php on line 128

Warning: Undefined array key "lang" in /home/signac/doku/lib/plugins/wrap/helper.php on line 94

Warning: Undefined array key "id" in /home/signac/doku/lib/plugins/wrap/helper.php on line 117

Warning: Undefined array key "width" in /home/signac/doku/lib/plugins/wrap/helper.php on line 119

Warning: Undefined array key "dir" in /home/signac/doku/lib/plugins/wrap/helper.php on line 128

Un jeu agaçant - une animation en tâche de fond avec un timer

Lorsque le programme doit effectuer des actions en tâche de fond, c'est à dire faire quelquechose, sans que l'application ne soit gelée (ne réponde plus aux actions de l'utilisateur), une des solutions est d'utilser un Timer. Un timer est un objet qui déclenche une action particulière à intervalles réguliers.

Un exemple typique consiste à déplacer un objet à l'écran, en permettant à l'utilisateur de continuer d'interagir avec l'application. Pour résoudre ce problème, on peut par exemple déplacer un petit peu l'objet régulièrement. C'est l'événement associé au timer qui devra déplacer l'objet.

Dans l'exemple qui suit, nous allons déplacer un disque rouge, qui rebondira sur les bords de la fenêtre. Si l'utilisateur parvient à cliquer sur l'objet, celui-ci s'arrêtera, pour repartir au prochain clic.

Pour que l'application soit réactive, le temps d'exécution des méthodes que nous écrivons doit être le plus court possible. Tant que nos méthodes n'ont pas rendu la main et que le contrôle n'est pas repassé à la boucle de gestion des événements, l'application est gelée.

- Première étape : Afficher l'objet et prévoir de le déplacer

Voici un premier programme, inspiré de la méthode de dessin utilisant QPainter décrite un peu plus haut.

La fenêtre principale contient un objet ZoneDessin que nous avons créé. Cet objet a sa méthode paintEvent redéfinie : elle trace un disque aux coordonnées self.posx,self.posy. Nous comprenons donc qu'il suffira par la suite de modifier ces coordonnées et de provoquer un affichage pour voir l'objet bouger.

# Balle animée
from PySide import QtGui,QtCore
import sys
 
class ZoneDessin(QtGui.QWidget) :
    def __init__(self,parent=None) :
        super().__init__(parent)
        self.posx=100
        self.posy=100
 
    def paintEvent(self,e) :
        p=QtGui.QPainter(self)
        p.setBrush(QtGui.QBrush(QtCore.Qt.SolidPattern))
        p.drawEllipse(QtCore.QPoint(self.posx,self.posy),15,15)
 
class Fenetre(QtGui.QMainWindow):
    def __init__(self,parent=None) :
        super().__init__(parent)
        self.resize(420,420)
        self.setWindowTitle("Balle") 
        dessin=ZoneDessin(self)
        dessin.setGeometry(10,10,400,400)
 
app=QtGui.QApplication(sys.argv)
frame=Fenetre()
frame.show()
sys.exit(app.exec_())

Créons maintenant un [[pysidedoc>QtCore/QBasicTimer.html|timer]], qui fera partie de l'objet ZoneDessin :

class ZoneDessin(QtGui.QWidget) :
    def __init__(self,parent=None) :
        super().__init__(parent)
        self.posx=100
        self.posy=100
        self.timer=QtCore.QBasicTimer()
        self.timer.start(1000,self)

L'avant-dernière ligne crée le timer (self.timer devient un attribut de ZoneDessin). La dernière ligne le démarre avec un délai de 1000ms. Le dernier paramètre indique que c'est self (c'est à dire la zone de dessin) qui recevra les événements (c'est le slot timerEvent de zoneDessin qui sera utilisé).

Pour recevoir ces évènemns, il suffit de redéfinir la méthode timerEvent() :

class ZoneDessin(QtGui.QWidget) :
    ...
    def timerEvent(self,e) :
        print("OK")

Si on ne redéfinit pas le slot timerEvent, le programme reste néanmoins exécitable, mais rien ne se passe.

Testez le programme après avoir redéfinit la méthode timerEvent, et vérifiez que la console affiche le message OK toutes les secondes.

Il ne nous reste plus qu'à remplacer timerEvent par une méthode un peu plus sophistiquée, qui va modifier les coordonnées du disque et provoquer le réaffichage.

def timerEvent(self,e) :
    self.posx+=1
    self.posy+=1
    self.repaint()

Testez ce nouveau programme. Le disque doit se déplacer vers le bas et vers la droite. Ça ne va pas très vite….

Nous avons deux solutions pour accélérer le déplacement :

  1. déplacer le disque de plus d'un pixel (peu gourmand en ressource, mais animation plus saccadée
  2. déclencher le timer plus souvent (plus gourmand, mais on obtiendra une animation plus fluide

Essayez ces deux méthodes, puis constatez que le disque finit par sortir de l'écran.

- Des rebonds

Pour que le disque rebondisse sur les rebords, il faut qu'il soit repéré par sa position, et par sa vitesse. Puis, s'il sort de l'écran, il suffit d'inverser une des composantes de son vecteur vitesse et de continuer.

class ZoneDessin(QtGui.QWidget) :
    def __init__(self,parent=None) :
        super().__init__(parent)
        self.posx=100
        self.posy=100
        self.dirx,self.diry=2,-2
        self.timer=QtCore.QBasicTimer()
        self.timer.start(10,self)
 
    def timerEvent(self,e) :
        self.posx+=self.dirx
        self.posy+=self.diry
        self.repaint()
        if self.posx<15 or self.posx>self.width()-15 : 
            self.dirx=-self.dirx        
        if self.posy<15 or self.posy>self.height()-15 : 
            self.diry=-self.diry        

Testez votre programme. Le disque doit maintenant rebondir sur les bords de l'écran.

- Interaction utilisateur

Il se reste plus qu'à permettre à l'utilisateur de stopper ou redémarrer le disque.

Pour cela, nous allons récupérer les événements souris de ZoneDessin et, quel que soit le point cliqué, nous arrêterons le timer s'il est démarré et le démarrerons s'il est arrêté :

class ZoneDessin(QtGui.QWidget) :
    ...
    def mousePressEvent(self,e) :
        if self.timer.isActive() : self.timer.stop()
        else : self.timer.start(10,self)

Une fois ces modifications effectuées, un clic sur l'écran arrête ou redémarre le déplacement.

Pour rendre le programme plus ludique et illustrer les interactions avec l'utilisateur, nous allons faire en sorte que seuls les clics sur l'objet soient pris en compte. Pour cela, il suffit de vérifier si le point cliqué est à une distance inférieure à 15 (c'est le rayon du disque) du centre de l'objet :

def mousePressEvent(self,e) :
    if (self.posx-e.x())**2+(self.posy-e.y())**2<15**2 :
        if self.timer.isActive() : self.timer.stop()
        else : self.timer.start(10,self)

Ce n'est pas si facile de cliquer sur l'objet en mouvement. Une amélioration possible est d'accélérer l'objet à chaque clic, pour que le jeu devienne de plus en plus difficile. Enfin, il est assez facile de limiter les parties à, par exemple, 2 minutes, et d'afficher comme score le nombre de clics réussis.

- Affichage d'une image

Plutôt qu'un disque noir, nous allons afficher une image, provenant d'un fichier PNG. Un exemple de fichier (une balle de tennis) est téléchargeable ci-dessous :

Les modifications à apporter dans le programme sont très simples. On charge l'image dans le constructeur de la zone de dessin (on obtient un objet de type QPixmap). Puis on affiche ce pixmap à l'aide de la fonction drawPixmap, à utiliser à la place de ''drawEllipse''.

On redonne ci-dessous le programme complet :

anim2.py
# Dessiner une image en utilisant paintEvent
from PySide import QtGui,QtCore
import sys
 
class ZoneDessin(QtGui.QWidget) :
    def __init__(self,parent=None) :
        super().__init__(parent)
        self.pixmap=QtGui.QPixmap("balle.png")
        self.posx=100
        self.posy=100
        self.dirx,self.diry=2,-2
        self.timer=QtCore.QBasicTimer()
        self.timer.start(10,self)
 
    def timerEvent(self,e) :
        self.posx+=self.dirx
        self.posy+=self.diry
        self.repaint()
        if self.posx<15 or self.posx>self.width()-15 : 
            self.dirx=-self.dirx        
        if self.posy<15 or self.posy>self.height()-15 : 
            self.diry=-self.diry        
 
    def mousePressEvent(self,e) :
        if (self.posx-e.x())**2+(self.posy-e.y())**2<15**2 :
            if self.timer.isActive() : self.timer.stop()
            else : self.timer.start(10,self)
 
    def paintEvent(self,e) :
        p=QtGui.QPainter(self)
        p.drawPixmap(self.posx-16,self.posy-16,32,32,self.pixmap)
 
class Fenetre(QtGui.QMainWindow):
    def __init__(self,parent=None) :
        super().__init__(parent)
        self.resize(420,420)
        self.setWindowTitle("Balle")
        dessin=ZoneDessin(self)
        dessin.setGeometry(10,10,400,400)
 
app=QtGui.QApplication(sys.argv)
frame=Fenetre()
frame.show()
sys.exit(app.exec_())
stu/python_gui/pyqt_projet_animation.txt · Dernière modification: 2014/03/31 16:45 (modification externe)