Graphismes en PyQt
Sylvain Malacria
[Link]
[Link]
Diapositives inspirées de Gilles Bailly
Objectifs
Introduction
‣ Signaux et slots
‣ Bases de PyQt
‣ Les principales classes Qt
Graphisme avancé
‣ Le dessin en PyQt
‣ Programmation événementielle
‣ Notion avancée de graphisme avec Qt
#1 Le dessin en PyQt
Dessin en PyQt
Paint system
QPainter QPaintEngine QPaintDevice
L’API de peinture de Qt permet de peindre à l’écran, dans un fichier, etc.
3 classes principales :
‣ QPainter pour effectuer des opérations de dessin
‣ QPaintDevice abstraction 2D dans laquelle on dessine
‣ QPaintEngine interface pour relier les deux
Dessin en PyQt
Paint system
QPainter QPaintDevice
L’API de peinture de Qt permet de peindre à l’écran, dans un fichier, etc.
3 classes principales :
‣ QPainter pour effectuer des opérations de dessin
‣ QPaintDevice abstraction 2D dans laquelle on dessine
‣ QPaintEngine interface pour relier les deux
QPaintEngine utilisée de manière interne (cachée) par QPainter et QPaintDevice
QPainter
QPainter est l’outil de dessin
‣ lignes simples
‣ path
‣ formes géometriques (ellipse, rectangle, etc.)
‣ texte
‣ images
‣ etc.
Utilise deux objets principaux
‣ QBrush (pour les fill)
‣ QPen (pour les stroke)
Fonction principale est de dessiner, mais dispose d’autres fonctions pour
optimiser son rendu
Peut dessiner sur n’importe quel objet qui hérite de la classe QPaintDevice
Exemples de QPaintDevice
Classe de base dans laquelle on peut peindre avec un QPainter
‣ QWidget
‣ QImage
‣ QPixmap
‣ QPicture
‣ QPrinter
‣ …
Dessiner dans un QWidget ?
Le widget se “dessine” lorsqu’il est repeint
Le widget est repeint lorsque :
‣ une fenêtre passe au dessus
‣ on redimensionne la fenêtre
‣ on lui demande explicitement
- Repaint(), force le widget à être redessiné
- Update(), un évènement de dessin est ajouté en file d’attente
Dessiner dans un QWidget ?
Le widget se “dessine” lorsqu’il est repeint
Le widget est repeint lorsque :
‣ une fenêtre passe au dessus
‣ on redimensionne la fenêtre
‣ on lui demande explicitement
- Repaint(), force le widget à être redessiné
- Update(), un évènement de dessin est ajouté en file d’attente
Dans tous les cas, c’est la méthode :
paintEvent(self, QPaintEvent)
qui est appelée (et vous ne devez jamais l’appeler manuellement)
0;0 x
y
0;0 x
1920;0
1080;0 1920;1080
Dessiner dans un QWidget
class Dessin(QWidget):
#evenement QPaintEvent
def paintEvent(self, event): # event de type QPaintEvent
# Blabla de dessin ici
def main(args):
app = QApplication(args)
win = QMainWindow()
[Link](Dessin())
[Link](300,200)
[Link]()
app.exec_()
return
if __name__ == "__main__":
main([Link])
Dessiner dans un QWidget
class Dessin(QWidget):
#evenement QPaintEvent
def paintEvent(self, event): # event de type QPaintEvent
painter = QPainter(self) # recupere le QPainter du widget
[Link](5,5,120,40) # dessiner un rectangle noir
return
def main(args):
app = QApplication(args)
win = QMainWindow()
[Link](Dessin())
[Link](300,200)
[Link]()
app.exec_()
return
if __name__ == "__main__":
main([Link])
Dessiner dans un QWidget
Pourquoi cela fonctionne?
class Dessin(QWidget):
#evenement QPaintEvent
def paintEvent(self, event): # event de type QPaintEvent
painter = QPainter(self) # recupere le QPainter du widget
[Link](5,5,120,40) # dessiner un rectangle noir
return
QPainter QPaintDevice
QPainter comme outil de dessin
QWidget hérite de QPaintDevice
Dessin avancé
QPainter:
‣ DrawLine(), drawEllipse(), drawRect(), drawPath(), etc.
‣ fillRect(), fillEllipse()
‣ drawText()
‣ drawPixMap(), drawImage()
‣ setPen(), setBrush()
QPainter QPaintDevice
Dessin coloré
class Dessin(QWidget):
#evenement QPaintEvent
def paintEvent(self, event):
painter = QPainter(self) # recupere le QPainter du widget
[Link]([Link]) # add a red pen
[Link]([Link]) # set a light gray brush
[Link](5,5,120,40) # dessine le rectangle
Instancier un Pen
class Dessin(QWidget):
#evenement QPaintEvent
def paintEvent(self, event):
painter = QPainter(self) # recupere le QPainter du widget
pen = QPen([Link]) # instancier un pen
[Link](5) # change l'epaisseur
[Link](pen) # appliquer ce pen au painter
[Link]([Link]) # set a light gray brush
[Link](5,5,120,40) # dessine le rectangle
class Dessin(QWidget):
#evenement QPaintEvent
def paintEvent(self, event):
painter = QPainter(self)
pen = QPen([Link])
[Link](5)
[Link](pen)
[Link]([Link])
[Link](5,5,120,40)
[Link](QColor(120, 255, 255, 150))
[Link](0,0,100,130)
Questions
Où dessiner?
‣ Dans la méthode paintEvent(self, QPaintEvent )
Comment demander le réaffichage d’un widget
‣ update()
‣ repaint()
Quelle est la différence entre update() et repaint()?
‣ update() indique qu’une zone est à réafficher (mais l’affichage n’est pas instantané)
‣ repaint() réaffiche immédiatement (mais peut introduire de la latence)
Comment dessiner dans paintEvent(QPaintEvent)
‣ instancier un QPainter: p = QPainter(self )
Dans quoi dessiner?
QPainter QPaintDevice
Tous ce qui hérite de QPaintDevice
‣ QWidget
‣ QPrinter
‣ QPixmap
‣ QImage
‣ etc.
Possibilité de rendu de “haut niveau” (SVG)
‣ QSvgRenderer
‣ QSvgwidget
#2 Gestion des évènements
Gestions des évènements souris dans un QWidget
Méthodes qui héritent de QWidget
def mouseMoveEvent(self, event):
def mousePressEvent(self, event):
def mouseReleaseEvent(self, event):
def enterEvent(self, event):
def leaveEvent(self, event):
class Dessin(QWidget):
def mousePressEvent(self, event): # evenement mousePress
[Link] = [Link]()
print("press: ", [Link])
def mouseReleaseEvent(self, event): # evenement mouseRelease
[Link] = [Link]()
print("release: ", [Link]())
193-51-236-93:TPs sylvain$ python3 [Link]
press: [Link](141, 28)
release: [Link](141, 28)
press: [Link](274, 129)
release: [Link](274, 129)
Les méthodes sont appelées automatiquement car la classe hérite de QWidget !!
class Dessin(QWidget):
def __init__(self):
super().__init__()
[Link](True) #activer le "mouse tracking"
def mouseMoveEvent(self, event): # evenement mouseMove
[Link] = [Link]()
print("move: ", [Link])
Par défaut, mouseMoveEvent envoyés seulement si bouton de souris enfoncé (Drag)
Possible d’activer/désactiver en permanence en utilisant setMouseTracking(bool)
Exemple
class Dessin(QWidget):
def __init__(self):
super().__init__()
[Link](True) # on active le mouseTracking
[Link] = None
def mouseMoveEvent(self, event): # evenement mouseMove
[Link] = [Link]() # on stocke la position du curseur
[Link]() # on met à jour l'affichage
#evenement QPaintEvent
def paintEvent(self, event):
painter = QPainter(self)
if [Link] != None:
[Link](\
[Link].x()-5,\
[Link].y()-5,10,10) # On dessine l'ellipse autour du curseur
Exemple
class Dessin(QWidget):
def __init__(self):
super().__init__()
[Link](True) # on active le mouseTracking
[Link] = None
def mouseMoveEvent(self, event): # evenement mouseMove
[Link] = [Link]() # on stocke la position du curseur
[Link]() # on met à jour l'affichage
#evenement QPaintEvent
def paintEvent(self, event):
painter = QPainter(self)
if [Link] != None:
[Link](\
[Link].x()-5,\
[Link].y()-5,10,10) # On dessine l'ellipse autour du curseur
Exemple
class Dessin(QWidget):
def __init__(self):
super().__init__()
[Link](True) # on active le mouseTracking
[Link] = None
def mouseMoveEvent(self, event): # evenement mouseMove
[Link] = [Link]() # on stocke la position du curseur
[Link]() # on met à jour l'affichage
#evenement QPaintEvent
def paintEvent(self, event):
painter = QPainter(self)
if [Link] != None:
[Link](\
[Link].x()-5,\
[Link].y()-5,10,10) # On dessine l'ellipse autour du curseur
QMouseEvent
De type QMouseEvent
def mousePressEvent(self, event):
QMouseEvent permet de récupérer (selon versions) :
‣ button( ) : bouton souris qui a déclenché l’événement. ex: [Link]#Button
‣ buttons( ) : état des autres boutons. ex: [Link]#Button | [Link]
‣ modifiers( ) : modificateurs clavier. ex: [Link] | [Link]#Modifier
‣ pos( ) : position locale (relative au widget)
‣ globalPos( ), windowPos( ), screenPos( ) : position globale ou relative à ce référentiel
- utile si on déplace le widget interactivement !
Synthèse
événements def mousePressEvent(self, QMouseEvent):
......
......
update();
}
boucle de gestion
des événements
def mouseMoveEvent(self, QMouseEvent):
......
......
update();
}
def mouseReleaseEvent(QMouseEvent* e):
def paintEvent(self, QPaintEvent):
......
painter = QPainter(self ) ......
......
update();
return
}
#3 Dessin avancé
QPainter
Attributs
‣ setPen( ) : lignes et contours
‣ setBrush( ) : remplissage
‣ setFont( ) : texte
‣ setTransform( ), etc. : transformations affines
‣ setClipRect/Path/Region( ) : clipping (découpage)
‣ setCompositionMode( ) : composition
QPainter
Lignes et contours
‣ drawPoint(), drawPoints()
‣ drawLine(), drawLines()
‣ drawRect(), drawRects()
‣ drawArc(), drawEllipse()
‣ drawPolygon(), drawPolyline(), etc...
‣ drawPath() : chemin complexe
Remplissage
‣ fillRect(), fillPath()
Divers
‣ drawText()
‣ drawPixmap(), drawImage(), drawPicture()
‣ etc.
QPainter
Classes utiles
‣ entiers: QPoint, QLine, QRect, QPolygon
‣ flottants: QPointF, QLineF, ...
‣ chemin complexe: QPainterPath
‣ zone d’affichage: QRegion
Pinceau: QPen
Attributs
‣ style : type de ligne
‣ width : épaisseur
‣ brush : attributs du pinceau (couleur...)
‣ capStyle : terminaisons
‣ joinStyle : jointures
PenStyle
Cap Style
Join Style
Pinceau: QPen
Exemple
// dans méthode PaintEvent()
pen = QPen() // default pen
[Link]([Link])
[Link](3)
[Link]([Link])
[Link]([Link]) PenStyle
[Link]([Link])
Cap Style
painter = QPainter( self )
[Link]( pen )
Join Style
Remplissage: QBrush
Attributs
‣ style
BrushStyle
‣ color
‣ gradient
‣ texture
brush = QBrush( ... )
.....
painter = QPainter(self)
[Link](brush)
Remplissage: QBrush
Attributs
‣ style
‣ color
‣ gradient
‣ texture
QColor [Link]
‣ modèles RGB, HSV or CMYK
‣ composante alpha (transparence) :
- alpha blending
‣ couleurs prédéfinies:
- [Link]
Remplissage: gradients
Type de gradients
‣ lineaire,
‣ radial
‣ conique
gradient = QLinearGradient(QPointF(0, 0), QPointF(100, 100))
[Link](0,[Link])
[Link](1,[Link])
[Link](gradient)
QLinearGradient QRadialGradient QConicalGradient répétition: setSpread()
Composition
Modes de composition
‣ opérateurs de Porter Duff:
‣ définissent : F(source, destination)
‣ défaut : SourceOver
- avec alpha blending
- dst <= asrc * src + (1-asrc) * adst * dst
‣ limitations
- selon implémentation et Paint Device
Méthode:
setCompositionMode( )
Découpage (clipping)
Découpage
‣ selon un rectangle, une région ou un path
‣ méthodes de QPainter
setClipping(), setClipRect(), setClipRegion(), setClipPath()
QRegion
‣ united(), intersected(), subtracted(), xored()
r1 = QRegion( QRect(100, 100, 200, 80), [Link]) # r1: elliptic region
r2 = QRegion( QRect(100, 120, 90, 30) ) # r2: rectangular region
r3 = [Link](r2); # r3: intersection
painter = QPainter(self);
[Link](r3,[Link]);
...etc... // paint clipped graphics
Example
def paintEvent(self, event):
painter = QPainter(self)
[Link](QColor(255,0,0))
rect1 = QRect(10, 10, 100, 80)
rect2 = QRect(50, 2, 90, 130)
rect3 = QRect(5, 5, 100,100)
r1 = QRegion( rect1, [Link]) # definition des regions
r2 = QRegion( rect2)
rc = [Link](r2) # combinaison de regions
[Link](rc) # on attribue la clipregion
[Link](rect3) # on dessine
united() xor() intersected() subtracted()
Transformations affines
Transformations
‣ translate()
‣ rotate()
‣ scale()
‣ shear()
‣ setTransform()
#evenement QPaintEvent
def paintEvent(self, event):
painter = QPainter(self)
[Link]() # empile l'état courant
[Link]([Link]()/2,
[Link]()/2) # on "centre" le painter
for k in range(0,10):
[Link](0, 0, 400, 0) # dessine une ligne
[Link](45) # rotation de 45°
[Link]() # dépile l'état courant
Anti-aliasing
Anti-aliasing
‣ éviter l’effet d’escalier
‣ particulièrement utile pour les polices de caractères
Subpixel rendering
‣ exemples : ClearType, texte sous MacOSX
MacOSX
ClearType
(Wikipedia)
Anti-aliasing
Anti-aliasing sous Qt
QPainter painter(this);
[Link]([Link]);
[Link]([Link]);
[Link](2, 7, 6, 1);
Rendering hints
‣ “hint” = option de rendu
- effet non garanti
- dépend de l’implémentation et du matériel
‣ méthode setRenderingHints( ) de QPainter
Anti-aliasing
[Link]([Link])
Avec Sans
Antialiasing et cordonnées
Epaisseurs impaire
‣ pixels dessinés à droite et en dessous
[Link]() = left() + width() -1
Dessin anti-aliasé [Link]() = top() + height() -1
Mieux : QRectF (en flottant)
‣ pixels répartis autour de la ligne idéale
Paths
QPainterPath
‣ figure composée d’une suite arbitraire de lignes et courbes
- affichée par: [Link]()
- peut aussi servir pour remplissage, profilage, découpage
Méthodes
‣ déplacements: moveTo(), arcMoveTo()
‣ dessin: lineTo(), arcTo()
‣ courbes de Bezier: quadTo(), cubicTo()
‣ addRect(), addEllipse(), addPolygon(), addPath() ...
‣ addText()
‣ translate(), union, addition, soustraction...
‣ et d’autres encore ...
Path
exemples
#evenement QPaintEvent
def paintEvent(self, event):
painter = QPainter(self)
path = QPainterPath() #instancie le path
[Link](3, 3) # position initiale du path
[Link](50,130) # Tracé droit
[Link](120,30) # tracé droit
[Link](120,30,60,50,3,3) # tracé bezier
[Link](path) #dessiner le path
Path
exemples
#evenement QPaintEvent
def paintEvent(self, event):
painter = QPainter(self)
path = QPainterPath() #instancie le path
[Link](3, 3) # position initiale du path
[Link](50,130) # Tracé droit
[Link](120,30) # tracé droit
[Link](120,30,60,50,3,3) # tracé bezier
[Link](path) #dessiner le path
Paths
exemples
#evenement QPaintEvent
def paintEvent(self, event):
painter = QPainter(self)
center = QPointF([Link]()/2.0,[Link]()/2.0)
myPath = QPainterPath()
[Link]( center )
[Link]( QRectF(0,0,[Link](),[Link]()), 0, 270 )
[Link]([Link])
[Link]( myPath )
#evenement QPaintEvent
def paintEvent(self, event):
painter = QPainter(self)
myPath = QPainterPath()
[Link](QPointF(40,60),QFont('SansSerif',50),"Qt")
[Link](myPath)
Paths
#evenement QPaintEvent
def paintEvent(self, event):
painter = QPainter(self)
path = QPainterPath()
[Link](20, 20, 60, 60)
[Link](0, 0)
[Link](99, 0, 50, 50, 99, 99)
[Link](0, 99, 50, 50, 0, 0)
[Link](0, 0, 100, 100, [Link])
[Link]( QPen(QColor(79, 106, 25), 1,
[Link], [Link], [Link]))
[Link]( QColor(122, 163, 39));
[Link](path);
[Link] [Link]
(default)
Paths
#evenement QPaintEvent
def paintEvent(self, event):
painter = QPainter(self)
path = QPainterPath()
[Link](20, 20, 60, 60)
[Link](0, 0)
[Link](99, 0, 50, 50, 99, 99)
[Link](0, 99, 50, 50, 0, 0)
[Link](0, 0, 100, 100, [Link])
[Link]( QPen(QColor(79, 106, 25), 1,
[Link], [Link], [Link]))
[Link]( QColor(122, 163, 39));
[Link](path);
[Link] [Link]
(default)
Paths
#evenement QPaintEvent
def paintEvent(self, event):
painter = QPainter(self)
path = QPainterPath()
[Link](20, 20, 60, 60)
[Link](0, 0)
[Link](99, 0, 50, 50, 99, 99)
[Link](0, 99, 50, 50, 0, 0)
[Link](0, 0, 100, 100, [Link])
[Link]( QPen(QColor(79, 106, 25), 1,
[Link], [Link], [Link]))
[Link]( QColor(122, 163, 39));
[Link](path);
[Link] [Link]
(default)
Surfaces d’affichage
Images
Types d’images
‣ QImage: optimisé pour E/S et accès/manipulation des pixels
- QPixmap, QBitmap : optimisés pour affichage l’écran
‣ QPicture: pour enregistrer et rejouer les commandes d’un QPainter
‣ dans tous les cas : on peut dessiner dedans avec un QPainter
Entrées/sorties
‣ load() / save() : depuis/vers un fichier, principaux formats supportés
‣ loadFromData() : depuis la mémoire
Accès aux pixels
Format 32 bits : accès direct
image = QImage(3, 3, QImage.Format_RGB32)
value = QRgb(122, 163, 39) // 0xff7aa327
[Link](0, 1, value)
[Link](1, 0, value)
Format 8 bits : indexé
image = QImage(3, 3, QImage::Format_Indexed8)
value = QRgb(122, 163, 39) // 0xff7aa327
[Link](0, value)
value = QRgb(237, 187, 51) // 0xffedba31
[Link](1, value)
[Link](0, 1, 0)
[Link](1, 0, 1)
Autres surfaces d’affichage
SVG
‣ QSvgWidget
- QSvgRenderer
OpenGL
‣ QGLWidget
- QGLPixelBuffer
- QGLFramebufferObject
Impression
‣ QPrinter
QSvgWidget
#4 Interaction avec formes géométriques
Picking
Picking avec QRect, QRectF
‣ intersects()
‣ contains()
Picking avec QPainterPath
‣ intersects(const QRectF & rectangle)
‣ intersects(const QPainterPath & path)
‣ contains(const QPointF & point)
‣ contains(const QRectF & rectangle)
‣ contains(const QPainterPath & path)
Retourne l’intersection
‣ QPainterPath intersected(const QPainterPath & path)
Picking / Interaction
Exemple
‣ teste si la souris est dans le rectangle quand on appuie sur le bouton de la souris
‣ change la couleur du rectangle à chaque clique
def __init__(self):
super().__init__()
[Link] = True # variable d'instance booleen
[Link] = QRect(5,5,140,120) # variable d'instance rectangle
def mousePressEvent(self, event): # evenement mousePress
[Link] = [Link]()
if [Link]([Link]()): # test la position
[Link] = not [Link]
[Link]() # demande la MAJ du dessin
#evenement QPaintEvent
def paintEvent(self, event):
painter = QPainter(self)
if [Link]:
[Link]([Link])
else:
[Link](QColor([Link]))
[Link]([Link])
Formes non rectangulaires
Utilisation des regions
def __init__(self):
super().__init__()
[Link] = True # variable d'instance booleen
[Link] = QRect(5,5,140,120) # variable d'instance rectangle
def mousePressEvent(self, event): # evenement mousePress
[Link] = [Link]()
ellipse = QRegion([Link],[Link]) # défini une region elliptique
if [Link]([Link]()): # test la position
[Link] = not [Link]
[Link]() # demande la MAJ du dessin
#evenement QPaintEvent
def paintEvent(self, event):
painter = QPainter(self)
if [Link]:
[Link]([Link])
else:
[Link](QColor([Link]))
[Link]([Link])
#5 Performances de l’affichage
Performance de l’affichage
Problèmes classiques :
‣ Flickering et tearing (scintillement et déchirement)
‣ Lag (latence)
Flickering
Flickering
‣ scintillement de l’affichage car l’oeil perçoit les images intermédiaires
‣ exemple : pour rafraichir cette page il faut :
- 1) tout « effacer » (repeindre le fond)
- 2) tout redessiner
- => scintillement si le fond du transparent est sombre alors que le fond de la fenêtre est
blanc
Double buffering
Double buffering
‣ solution au flickering :
- dessin dans le back buffer
- recopie dans le front buffer (le buffer vidéo
qui contrôle ce qui est affiché sur l’écran)
‣ par défaut avec Qt4
source: AnandTech
Tearing
Possible problème : Tearing
‣ l’image apparait en 2 (ou 3...) parties horizontales
‣ problème : recopie du back buffer
avant que le dessin soit complet
- mélange de plusieurs "frames" vidéo
- en particulier avec jeux vidéo et autres
applications graphiquement demandantes
source: AnandTech
Tearing
Solution au Tearing
‣ VSync (Vertical synchronization )
‣ rendu synchronisé avec l'affichage
‣ inconvénient : ralentit l'affichage
source: AnandTech
Performance de l’affichage
Latence (lag)
‣ effet : l’affichage «ne suit pas » l’interaction
‣ raison : le rendu n'est pas assez rapide
Performance de l’affichage
Latence (lag)
‣ effet : l’affichage ne suit pas l’interaction
‣ raison : le rendu n'est pas assez rapide
Solutions
‣ afficher moins de choses :
- dans l’espace
- dans le temps
‣ afficher en mode XOR
Performance de l’affichage
Afficher moins de choses dans l'espace
‣ Clipping : réduire la zone d'affichage
- méthodes rect() et region() de QPaintEvent
‣ "Backing store" ou équivalent
- 1) copier ce qui ne change pas dans une image
- 2) afficher cette image dans la zone de dessin
- 3) afficher la partie qui change par dessus
Performance de l’affichage
Afficher moins de choses dans le temps
‣ sauter les images intermédiaires :
- réafficher une fois sur deux… ou selon l'heure
‣ les timers peuvent être utiles (cf. QTimer)
void mouseMoveEvent(QMouseEvent* e)
......
événements
if (delay < MAX_DELAY) update();
}
boucle de gestion
des événements
Ne pas réafficher si le délai est trop long
Suivant le cas (et le toolkit), test dans une
de ces 2 fonctions
void paintEvent(QPaintEvent* e) {
if (delay > MAX_DELAY) return;
......
}