Utilisation de QGraphicsItem
Par
Vincent Vande Vyvre (vincent-vande-vyvre) (Blog)
Ce tutoriel traite de l'utilisation des QGraphicsItem.
Il est destiné aux développeurs Python ayant déjà de bonnes connaissances
dans l'usage du framework Qt.
I. Introduction
II. Les items graphiques de base
III. Les scripts Python
IV. Principes de base
V. Les drapeaux
VI. Le script principal
VII. Création de la loupe
VIII. Création de la loupe de forme ronde.
IX. Les déplacements
X. Les signaux propres aux QGraphicsItem
XI. Animation
XII. Les effets graphiques
XIII. Conclusions
XIV. Liens
I. Introduction
QGraphicsItem est la classe de base pour tous les objets graphiques pouvant être affichés dans un QGraphicsScene.
Cette classe permet de créer ses propres objets graphiques et de gérer leurs propriétés telles que les données de
géométrie de l'objet et ses interactions avec les autres éléments présents dans la scène (dont la détection de
collisions entre éléments graphiques).
II. Les items graphiques de base
Qt propose une série de QGraphicsItem de base destinés à représenter les objets graphiques les plus courants :
-
QGraphicsEllipseItem : définit un item ellipse ;
-
QGraphicsLineItem : définit un item ligne ;
-
QGraphicsPathItem : définit un item chemin de tracé ;
-
QGraphicsPixmapItem : définit un item pixmap ;
-
QGraphicsPolygonItem : définit un item polygone ;
-
QGraphicsRectItem : définit un item rectangle ;
-
QGraphicsSimpleTextItem : définit un item simple texte ;
-
QGraphicsTextItem : définit un item texte plus complexe que l'item précédent.
Nous nous intéresserons plus particulièrement, dans cette article au QGraphicsPixmapItem en réalisant un objet
graphique utilisant à la fois les classes QPixmap, QPainter, QPainterPath, l'application de texture,
la découpe de zone transparente, les évènements souris et clavier et les signaux pour communiquer avec l'interface utilisateur.
Pour cela nous réaliserons un outil loupe pour visionneuse d'image.
III. Les scripts Python
En bas de cet article, se trouvent les liens permettant de télécharger l'archive contenant les scripts nécessaires.
Le code
main.py
est celui qu'il faut lancer,
magnifier.py
est celui de la loupe et
sera principalement décrit ici et le script
miniView.py
est une version simplifiée de la visionneuse
décrite dans
cet article,
et dont le code ne demande plus à être détaillé.
IV. Principes de base
Voyons d'abord le fonctionnement de la loupe. On part du principe que l'on dispose d'une image créée en tant que QImage
à sa taille réelle et une pixmap QPixmap issue de l'image, réduite au dimensions de la vue et affichée dans celle-ci.
La loupe aura trois paramètres : sa forme, sa taille et son facteur de grossissement.
L'image créée en premier étant la référence nécessaire de la visionneuse pour recréer la pixmap lors de zoom, on va en créer
une copie et la mettre directement au facteur de grossissement choisi pour la loupe. on déposera ensuite sur cette copie un
gabarit de la forme et de la taille de la loupe, qui servira à prélever la texture qui sera « peinte » dans le QGraphicsPixmapItem.
Ensuite, par un système de pantographe informatique on assurera le déplacement du gabarit sur l'image de référence suivant les déplacements
de la loupe effectués par l'utilisateur.
La classe Magnifier se présentera comme ceci :
magnifier.py
class Magnifier (QtGui.QGraphicsPixmapItem):
def __init__ (self, main, parent= None , graphic= None , scene= None ,
img= None , zoom= None ):
super (Magnifier, self).__init__ (parent, scene)
self.main = main
d = dict (shape = " square " ,
size = 160 ,
factor = 1 .0 ,
image = img,
graphic = graphic,
scene = scene,
cur_pos = None ,
zoom = zoom,
background_color = (255 , 255 , 255 , 255 ),
old_X = None ,
old_Y = None )
for key, value in d.iteritems ():
setattr (self, key, value)
self.setFlags (QtGui.QGraphicsItem.ItemIsMovable |
QtGui.QGraphicsItem.ItemIsFocusable)
self.setAcceptHoverEvents (True )
self.build_settings ()
self._moved = Moved ()
self._optionChanged = OptionChanged ()
|
Les arguments nécessaires à l'instanciation de la classe sont les suivants :
-
main
: l'application principale ;
-
parent
: le parent, généralement laissé à None ;
-
graphic
: l'instance du QGraphicsView de notre visionneuse utilisée pour centrer la loupe
et pour mapper des coordonnées entre scène et vue ;
-
scene
: l'instance de la QGraphicsScene ;
-
img
: l'instance de la QImage originale ;
-
zoom
: le facteur de zoom actuel de la pixmap affichée dans la visionneuse.
La loupe nécessite quelques attributs supplémentaires :
-
shape
: la forme de la loupe, "square" ou "circle" ;
-
size
: sa taille en pixels ;
-
factor
: le facteur de grossissement ;
-
cur_pos
: la position courante de la loupe une fois affichée ;
-
background_color
: la couleur de fond du QGraphicsView ;
-
old_X et old_Y
: des références de positionnement lors de déplacement.
Après les attributs d'instances il faut initialiser quelques drapeaux nécessaires au QGraphicsItem.
V. Les drapeaux
La liste suivante décrit les drapeaux les plus couramment utilisés.
-
QGraphicsItem.ItemIsMovable :
L'item peut être déplacé avec la souris ; si l'item comporte des items enfants, ceux-ci seront déplacés avec lui et,
si l'item fait partie d'un groupe d'items sélectionnés, l'ensemble de ceux-ci sera déplacé ;
-
QGraphicsItem.ItemIsSelectable :
L'item peut être sélectionné. La sélection pourra se faire au moyen de la souris, en cliquant dessus, ou en traçant un
QRubberBand, ou de la méthode QGraphicsItem.setSelected(), ou encore par QGraphicsScene.setSelectionArea() ;
-
QGraphicsItem.ItemIsFocusable :
L'item peut avoir le focus. Indispensable pour des interactions avec le clavier ;
-
QGraphicsItem.ItemSendsGeometryChanges :
L'item peut notifier les changements décrits par QGraphicsItem.itemChange().
L'utiliser peut avoir une incidence sur les performances du programme ;
-
QGraphicsItem.ItemSendsScenePositionChanges :
L'item peut notifier ses changements de position. L'usage de ce drapeau est moins réducteur en performances que
ItemSendsGeometryChanges, car il ne notifie que les déplacements de l'item, soit ItemScenePositionHasChanged ;
-
QGraphicsItem.ItemIgnoresTransformations :
L'item n'hérite pas des transformations de son parent. Lorsqu'un item possède des enfants, ceux-ci subissent aussi ses
transformations (rotation, dimensionnement, etc.). Avec ce drapeau l'item enfant devient insensible aux transformations
de son parent. Utile si l'item contient du texte, pour garantir que celui-ci reste toujours lisible ;
-
QGraphicsItem.ItemIgnoresParentOpacity :
L'item est insensible aux changements d'opacité de son parent. Par défaut, l'opacité d'un item, définie avec setOpacity(),
s'applique aussi à ses items enfants ;
-
QGraphicsItem.ItemDoesntPropagateOpacityToChildren :
À l'inverse du drapeau précédent, ici c'est l'item qui ne transmet pas sa valeur d'opacité à ses items enfants.
Voir les documentations respectives de
PyQt
et
PySide
des
GraphicsItemFlag.
Pour la loupe, il est nécessaire que celle-ci soit déplaçable et puisse recevoir le focus.
magnifier.py
self.setFlags (QtGui.QGraphicsItem.ItemIsMovable |
QtGui.QGraphicsItem.ItemIsFocusable)
|
Les dernières lignes du code de
__init__()
seront vues plus loin dans l'article.
VI. Le script principal
Le script
main.py
sert de script principal. Il instanciera l'interface graphique et ensuite la loupe lors d'un clic sur le bouton « Loupe ».
main.py
import sys
from PyQt4 import QtCore, QtGui
from miniView import MiniView
from magnifier import Magnifier
class Main (object ):
def __init__ (self):
MainWindow = QtGui.QMainWindow ()
self.ui = MiniView ()
self.ui.setupUi (MainWindow, self)
self.magnifier_current_settings = [1 .0 , " square " , 160 ]
self.window_geometry = MainWindow.geometry ()
self.ui.magnifier_btn.clicked.connect (self.set_magnifier)
def set_magnifier (self):
self.mgnf = Magnifier (self, graphic= self.ui.viewer,
scene= self.ui.scene,
img= self.ui.image,
zoom= self.ui.zoom)
self.set_magnifier_options ()
self.mgnf.setup ()
self.mgnf.positionChanged.connect (self.on_magnifier_moved)
self.mgnf.settingsChanged.connect (self.show_magnifier_settings)
self.mgnf.setFocus ()
def set_magnifier_options (self):
self.mgnf.set_factor (self.magnifier_current_settings[0 ])
self.mgnf.set_shape (self.magnifier_current_settings[1 ])
self.mgnf.set_size (self.magnifier_current_settings[2 ])
self.show_magnifier_settings (self.magnifier_current_settings)
...
if __name__ = = " __main__ " :
app = QtGui.QApplication (sys.argv)
main = Main ()
sys.exit (app.exec_ ())
|
Les premières lignes ne devraient pas poser de questions, remarquons que l'on définit les trois
paramètres de notre loupe dans une liste.
Lors d'un clic sur le bouton loupe, la fonction
set_magnifier()
est appelée,
la loupe est instanciée avec ses arguments, ses paramètres de départ sont passés à l'instance
par la fonction
set_magnifier_options()
, l'appel de
setup()
créera
l'objet lui-même; pour finir, ses signaux sont connectés et le focus est placé sur la loupe.
Les trois autres fonctions non représentées ici ne servent qu'à l'affichage de données dans l'interface et
à l'application d'un effet qui sera vu en fin d'article.
VII. Création de la loupe
Examinons la fonction
setup()
.
magnifier.py
def setup (self):
self.cur_pos = self.__get_center ()
self.setPos (self.cur_pos[2 ], self.cur_pos[3 ])
self._set_brush ()
self.multi = self.factor * (1 / self.zoom)
self.__create_magnifier ()
if not self.old_X:
self.recenter_magnifying_glass ()
self.old_X, self.old_Y = self.pos ().x (), self.pos ().y ()
|
La fonction
__get_center()
retourne une liste de quatre éléments; les coordonnées
x et y du centre de la scène et les coordonnées x et y de coin supérieur gauche de notre loupe.
La fonction
_set_brush()
va simplement créer une brosse à la couleur de l'arrière-plan
du QGraphicsView, blanche par défaut, mais on verra plus loin le problème qui se présente lorsque
cette couleur est différente.
self.multi
est le paramètre multiplicateur nécessaire pour le pantographe, il est le
produit du facteur de grossissement multiplié par l'inverse du facteur de zoom.
La loupe est ensuite créée avec l'appel de
__create_magnifier()
, après quoi celle-ci sera
recentrée dans la vue. Cette dernière opération est nécessaire lorsque le centre de la scène n'est pas
au centre de la fenêtre.
magnifier.py
def __create_magnifier (self):
self.__set_pixmap ()
self.orig = self.__get_orig_center ()
if self.shape = = " square " :
self.__set_square ()
else :
self.__set_circle ()
def __set_pixmap (self):
w, h = self.image.width (), self.image.height ()
self.base = self.image.scaled (w * self.factor, h * self.factor,
QtCore.Qt.KeepAspectRatio,
QtCore.Qt.SmoothTransformation)
|
Tout d'abord
__set_pixmap()
est appelée qui construira
self.base
, l'image originale
redimensionnée au facteur de grossissement de la loupe et qui devient l'image de référence dans
laquelle on prélèvera la texture à placer dans le QGraphicsPixmapItem.
Ensuite,
__get_orig_center()
nous retourne une même liste que
__get_center()
sauf qu'ici c'est self.base qui sert de référence.
Pour finir, en fonction de la forme choisie, celle-ci est créée.
Commençons par la plus simple, la forme carrée.
magnifier.py
def __set_pix (self):
color = QtGui.QColor ()
color.setRgb (0 , 0 , 0 , 0 )
pix = QtGui.QPixmap (QtCore.QSize (self.size + 4 , self.size + 4 ))
pix.fill (color)
return pix
def __set_square (self):
self.pix = self.__set_pix ()
rect = QtCore.QRect (0 , 0 , self.size + 4 , self.size + 4 )
painter = QtGui.QPainter ()
painter.begin (self.pix)
painter.setPen (QtGui.QPen (QtCore.Qt.blue, 2 ,
QtCore.Qt.SolidLine, QtCore.Qt.RoundCap,
QtCore.Qt.MiterJoin))
painter.drawRect (rect)
painter.end ()
self.__fill_square ()
def __fill_square (self):
painter = QtGui.QPainter ()
painter.begin (self.pix)
rect = QtCore.QRect (2 , 2 , self.size, self.size)
painter.eraseRect (rect)
irect = QtCore.QRect (self.orig[2 ], self.orig[3 ], self.size, self.size)
painter.drawImage (rect, self.base, irect)
painter.end ()
self.setPixmap (self.pix)
|
La fonction
__set_square()
appelle tout d'abord
__set_pix()
qui lui retourne
une instance de
QPixmap transparente
(color.setRgb(0, 0, 0, 0))
et de la taille choisie pour la
loupe (augmentée de quatre pixels afin de lui tracer un cadre de deux pixels d'épaisseur).
Une instance de
QPainter est créée, il n'est pas nécessaire dans ce code d'en conserver une
référence, par contre; le painter demande que l'on lui désigne le support sur lequel peindre, appelé
device dans la documentation anglaise, c'est ce que fait
painter.begin(self.pix)
,
puis un pinceau est créé, ici, de couleur bleue et de deux pixels d'épaisseur. Les arguments
Qt.SolidLine,
Qt.RoundCap et
Qt.MiterJoin désignent respectivement le style de
ligne, de terminaison et de jonction.
Une fois n'est pas coutume, la documentation nous montre cela en images :
PyQt,
PySide
Ensuite, le cadre est tracé avec la méthode
drawRect(rect)
en lui donnant en argument le
rectangle définit à la deuxième ligne de la fonction après quoi, le painter est fermé avec
painter.end()
.
On passe à la fonction
__fill_square()
, où une instance de
QPainter est créée et
le contenu du cadre est éffacé avec la méthode
eraseRect(rect)
. À la création, on sais que
l'intérieur du cadre est vide mais lorsque la loupe sera déplacée, il faudra repeindre ce contenu.
Bien sur, la nouvelle texture remplace la précédente, mais uniquement dans la zone réellement repeinte.
Ceci sera détaillé lors de la création de la loupe ronde.
Maintenant, la texture est appliquée au moyen de la méthode
drawImage(cible, source, aire)
.
Ses arguments devraient être clair, la cible est la zone dans la pixmap où on désire peindre
(donc l'intérieur du cadre), la source est l'image de base et l'aire est la superficie d'image à y prélever,
donnée elle aussi avec une instance de
QRect. Dans ce cas, les deux rectangles ont les mêmes dimensions,
mais pas les mêmes origines. Si on utilise cette méthode avec deux rectangles de tailles différentes,
Qt dimensionnera la source pour la faire entrer dans la cible. Des effets plus amusants qu'esthétiques
peuvent être obtenus de cette manière.
Quelques exemples :
Après avoir fermé le painter, nous appliquons la pixmap à l'instance du QGraphicsPixmapItem.
La loupe est maintenant visible et se positionnera au centre de la vue si nécessaire.
Ce schéma représente la création de la loupe avec les noms utilisés dans le code.
VIII. Création de la loupe de forme ronde.
magnifier.py
def __set_circle (self):
self.pix = self.__set_pix ()
rect = QtCore.QRect (1 , 1 , self.size + 3 , self.size + 3 )
painter = QtGui.QPainter ()
painter.begin (self.pix)
painter.setRenderHints (QtGui.QPainter.Antialiasing, True )
painter.setPen (QtGui.QPen (QtCore.Qt.blue, 1 ,
QtCore.Qt.SolidLine, QtCore.Qt.RoundCap,
QtCore.Qt.MiterJoin))
painter.drawEllipse (rect)
rect = QtCore.QRectF (2 , 2 , self.size, self.size)
self.path = QtGui.QPainterPath ()
self.path.addEllipse (rect)
painter.drawPath (self.path)
painter.end ()
self.__fill_circle ()
|
A nouveau une pixmap transparente est créée comme support pour le painter auquel est appliqué un
nouveau paramètre avec la méthode
setRenderHints()
, le mode de rendu.
Ceux-ci sont détaillés plus bas.
Pour le cadre nous utilisons la méthode
drawEllipse(rect)
qui prend en argument le
rectangle dans lequel s'inscrit l'ellipse. Notons la légère différence de dimension du cadre,
due au mode de tracé, si on avait utilisé les mêmes dimensions, le cercle aurait eu les points
cardinaux légèrement aplatis.
Ensuite, un chemin de traçage est créé, un QPainterPath, circulaire et légèrement plus petit
que notre cadre et que l'on trace dans le pixmap.
Les QPainterPath sont des chemins utilisés pour le tracé de formes diverses et/ou pour leur
remplissage. Il peut s'agir de formes géométrique de base, lignes, arcs, rectangles, ellipses ou de
formes géométriques complexes conçues à partir des formes de bases.
Si vous avez déjà fait du dessin vectoriel, alors vous avez déjà utilisé des chemins ils en sont la
base.
Les modes de rendus de QPainter :
-
QPainter.Antialiasing : utilise le mode anticrénelage ;
-
QPainter.TextAntialiasing : utilise le mode anticrénelage pour le texte si possible.
Pour empêcher l'utilisation de ce rendu, n'utilisez pas (QPainter.TextAntialiasing, False)
mais plutôt QFont.NoAntialias dans la configuration de polices ;
-
QPainter.SmoothPixmapTransform : utilise un algorithme bilinéaire à la place de
l'algorithme de voisinage utilisé dans le premier mode ;
-
QPainter.HighQualityAntialiasing : (rendu OpenGl), nécessite de s'assurer du support
OpenGl sur la plateforme où tournera le programme ;
-
QPainter.NonCosmeticDefaultPen : applique par défaut la plume non cosmétique.
Une plume est dite cosmétique lorsque son épaisseur ne sera pas modifiée par des changements d'échelle de
l'objet graphique.
magnifier.py
def __fill_circle (self):
painter = QtGui.QPainter ()
brush = QtGui.QBrush ()
brush.setTexture (self.__set_texture ())
painter.begin (self.pix)
painter.fillPath (self.path, brush)
painter.end ()
self.setPixmap (self.pix)
def __set_texture (self):
texture = QtGui.QPixmap (QtCore.QSize (self.size + 1 , self.size + 1 ))
irect = QtCore.QRect (self.orig[2 ], self.orig[3 ],
self.size + 1 , self.size + 1 )
prect = QtCore.QRect (0 , 0 , self.size + 1 , self.size + 1 )
painter = QtGui.QPainter ()
painter.begin (texture)
painter.fillRect (prect, self.brush)
painter.drawImage (prect, self.base, irect)
painter.end ()
return texture
|
Le remplissage du cercle se fera ensuite avec la méthode
fillPath(path, brush)
où la brosse représente
une pixmap créée par la fonction
__set_texture()
. Celle-ci demande d'être vue en détail.
D'abord, une pixmap de la taille de notre loupe est créée; ensuite, deux rectangles sont définis;
irect
qui a
pour origine le coin supérieur gauche du gabarit dans l'image de référence et
prect
destiné au painter.
Un QPainter est instancié et appliqué au pixmap texture, après quoi on remplit ce pixmap avec la
brosse créée précédemment et qui contient la couleur d'arrière-plan du QGraphicsView puis on peint la
zone d'image définie par
irect
sur le pixmap.
Pourquoi remplir d'abord le pixmap avec la couleur d'arrière-plan de la visionneuse ?
Parce que lorsque la loupe sortira de l'image, la zone de texture ne remplira plus totalement la loupe
et les anciennes textures resteront visibles. Pour vérifier ceci, il faut commenter la ligne 221
painter.eraseRect(rect)
, lancer le script, créer une loupe et la sortir de l'image.
On devrait obtenir un résultat comme ceci:
Résumé en images de la construction de notre loupe ronde.
IX. Les déplacements
Afin d'être déplacé au moyen de la souris, un QGraphicsItem doit accepter les signaux
d'évènements de celle-ci.
Pour rappel, la construction du QGraphicsItem définissait les propriétés suivantes :
self.setFlags (QtGui.QGraphicsItem.ItemIsMovable |
QtGui.QGraphicsItem.ItemIsFocusable)
self.setAcceptHoverEvents (True )
|
Outre les drapeaux dont on a vu la signification plus haut, il est définit que l'item accepte les
évènements de survol de la souris par la méthode
setAcceptHoverEvents(True)
.
La réimplémentation de l'évènement hoverEnterEvent() sera sans effet si cette propriété n'est
pas explicitement définie.
L'évènement hoverEnterEvent() est utilisé ici pour changer l'aspect du pointeur de la souris
afin de signifier à l'utilisateur la possibilité de déplacer la loupe.
magnifier.py
def hoverEnterEvent (self, event):
shape = QtGui.QPixmap (" medias / cross_cursor_32 . png " )
cursor = QtGui.QCursor (shape, - 1 , - 1 )
self.setCursor (cursor)
|
Ce pointeur personnalisé est attribué à self, c'est-à-dire à la loupe, il n'y a donc aucune utilité
d'utiliser un hoverLeaveEvent() pour replacer l'ancienne forme du pointeur lorsque celui-ci
quitte la loupe.
Une remarque s'impose ici. L'évènement hoverEnterEvent() ne sera émis que si la zone du
QGraphicsItem survolée est d'une opacité suffisante. Dans le cas de la loupe ronde on sais
que la forme réelle de l'objet est carrée, les quatre coins étant simplement transparents.
Ceci explique que le signal du survol n'est émis que si le pointeur entre dans la zone visible de
la loupe.
Après moult essais, il semble que l'opacité minimale requise pour émettre un signal de survol soit
de 50 %. Il n'est pas exclu que ceci soit aussi dépendant de la plate-forme.
À titre d'exemple, un objet tel que le curseur personnalisé n'émettra jamais de signal de survol,
sa transparence correspond à une opacité de 15 % approximativement.
Suite à une pression du bouton gauche de la souris le déplacement de la loupe peut être amorcé.
magnifier.py
def mousePressEvent (self, event):
pos = event.scenePos ()
if event.button () = = 1 :
if self.menu_wdg.isVisible ():
self.menu_wdg.hide ()
self.old_X, self.old_Y = pos.x (), pos.y ()
event.accept ()
else :
event.ignore ()
def mouseMoveEvent (self, event):
pos = event.scenePos ()
if event.button () = = 2 :
return
cur = self.pos ()
X, Y = pos.x (), pos.y ()
m_X, m_Y = X - self.old_X, Y - self.old_Y
self.old_X, self.old_Y = X, Y
self.moveBy (m_X, m_Y)
self.__update (m_X, m_Y)
event.accept ()
|
Ici, si le bouton cliqué est le gauche (
event.button() == 1
) la position de la souris en
coordonnées scène est sauvegardée.
Ensuite, la fonction
mouseMoveEvent()
comparera la position courante de la souris avec
la dernière position connue et appliquera au QGraphicsItem les mêmes mouvements au moyen de la
méthode
moveBy(x, y)
, où
x
et
y
indique le nombre de pixels
du déplacement.
La fonction
__update(x, y)
est alors appelée qui procèdera au renouvellement de la texture de la loupe.
magnifier.py
def __update (self, x, y):
self.orig[2 ] + = x * self.multi
self.orig[3 ] + = y * self.multi
if self.shape = = " square " :
self.__fill_square ()
else :
self.__fill_circle ()
|
En premier lieu il faut convertir la grandeur du déplacement avec le facteur multiplicateur.
Le gabarit sur l'image originale aura un déplacement plus grand que la loupe sur l'écran.
Le fameux pantographe, en somme.
Puis, selon sa forme, le remplissage de la loupe est appelé.
Le code de la loupe comporte aussi une gestion des évènements clavier permettant de déplacer la
loupe avec les flèches ainsi que de fermer la loupe avec la touche Escape.
Cette partie de code étant particulièrement triviale, elle ne nécessite pas de développements
particuliers.
Le code réimplémente aussi un contextMenuEvent() qui affichera un menu destiné à paramétrer
la loupe.
magnifier.py
def contextMenuEvent (self, event):
self.__show_settings (event.screenPos ())
event.accept ()
|
Remarquons qu'on utilise ici la position du pointeur de la souris en coordonnées écran.
Ce menu se présentera comme ceci :
Lorsqu'un des paramètres de la loupe est modifié dans le menu, un signal est émis par celle-ci.
Ce signal permettra, par exemple, à l'application de mettre à jour les paramètres courants de la
loupe, les préférences utilisateur ou encore un menu popup associé au bouton de la loupe.
Étant donné que les QGraphicsItem n'héritent pas directement de QObject,
il faut créer les signaux séparément et les implémenter dans la classe
Magnifier()
de cette manière :
magnifier.py
class Magnifier (QtGui.QGraphicsPixmapItem):
def __init__ (self, main, parent= None , graphic= None ,
scene= None , img= None , zoom= None ):
super (Magnifier, self).__init__ (parent, scene)
...
self._moved = Moved ()
self._optionChanged = OptionChanged ()
def positionChanged ():
def fget (self):
return self._moved.positionChanged
return locals ()
positionChanged = property (* * positionChanged ())
def settingsChanged ():
def fget (self):
return self._optionChanged.settingsChanged
return locals ()
settingsChanged = property (* * settingsChanged ())
|
Et, tout à la fin du script
magnifier.py
, les deux classes implémentant les signaux :
class Moved (QtCore.QObject):
positionChanged = QtCore.pyqtSignal (list )
def __init__ (self):
super (Moved, self).__init__ ()
class OptionChanged (QtCore.QObject):
settingsChanged = QtCore.pyqtSignal (list )
def __init__ (self):
super (OptionChanged, self).__init__ ()
|
On a donc un signal qui transmettra les déplacements de la loupe (
positionChanged
)
et un deuxième signal pour les changements de paramètres (
settingsChanged
).
Ces signaux s'utilisent avec la méthode emit(), comme dans la fonction
set_pos()
:
magnifier.py
def set_pos (self, pos):
if isinstance (pos, QtCore.QPointF):
X, Y = pos.x (), pos.y ()
else :
X, Y = pos[0 ], pos[1 ]
cur = self.pos ()
m_X, m_Y = X - self.pos ().x (), Y - self.pos ().y ()
self.moveBy (m_X, m_Y)
self.__update (m_X, m_Y)
self.positionChanged.emit ([cur.x (), cur.y (), X, Y])
|
X. Les signaux propres aux QGraphicsItem
On a vu en décrivant les drapeaux, au début de cet article, que les QGraphicsItem pouvaient
eux aussi communiquer leur changement de géométrie, de positionnement, etc. Cette fonctionnalité
n'est pas utilisée dans le code de cet article, elle est donc détaillée à titre complémentaire.
Cette propriété des QGraphicsItem s'utilise en réimplementant QGraphicsItem.itemChange().
En voici un exemple :
def itemChange (self, change, value):
if change = = QtGui.QGraphicsItem.ItemScenePositionHasChanged:
print " ItemScenePositionHasChanged : { 0 } " .format (value.toPoint ())
elif change = = QtGui.QGraphicsItem.ItemVisibleChange:
print " ItemVisibleChange : { 0 } " .format (value.toBool ())
|
XI. Animation
Pour des raisons de simplification, le centre de l'image a été utilisé comme référence pour la
création de la loupe. Il est tout à fait possible, cependant, que le centre de l'image ne soit
pas au centre de la fenêtre graphique et pourrait même, si l'image est fortement zoomée, être en
dehors de celle-ci. La loupe pourrait être créée au centre du QGraphicsView, moyennant
quelques calculs supplémentaires mais, cela m'aurait ôté le plaisir de vous parler des animations
des QGraphicsItem.
Pour recentrer la loupe dans l'espace graphique, on peut utiliser la méthode setPos().
magnifier.py
def recenter_magnifying_glass (self):
c = self.size / 2 .0
dest = self.graphic.mapToScene (self.graphic.width () / 2 - c,
self.graphic.height () / 2 - c)
deltaX = dest.x () - self.pos ().x ()
deltaY = dest.y () - self.pos ().y ()
j, k, l, m = deltaX / 50 , deltaY / 50 , self.pos ().x (), self.pos ().y ()
for i in range (50 ):
l, m = l + j, m + k
self.setPos (QtCore.QPointF (l, m))
self.__update (j, k)
QtCore.QCoreApplication.processEvents ()
|
Tout d'abord, on fixe les déplacements
deltaX
et
deltaY
, respectivement
sur l'axe horizontal et l'axe vertical; ensuite, nous divisons ces déplacements par cinquante et
enfin, on applique ces mouvements dans une boucle de cinquante itérations. La valeur cinquante est
totalement arbitraire.
C'est cette méthode qui est utilisée par défaut. Inutile de préciser que la fluidité du mouvement
sera dépendante des performances de l'ordinateur. La loupe ronde nécessitant plus d'opérations
pour être mise à jour sera légèrement plus lente.
Qt propose aussi les QGraphicsItemAnimation() pour automatiser les transformations
de QGraphicsItem, avec, pour avantage, une meilleure maîtrise de la fluidité indépendamment
de la plate-forme.
magnifier.py
def recenter_magnifying_glass (self):
timer = QtCore.QTimeLine (1000 );
timer.setFrameRange (0 , 50 )
self.animation = QtGui.QGraphicsItemAnimation ()
self.animation.setItem (self);
self.animation.setTimeLine (timer)
c = self.size / 2 .0
dest = self.graphic.mapToScene (self.graphic.width () / 2 - c,
self.graphic.height () / 2 - c)
deltaX = dest.x () - self.pos ().x ()
deltaY = dest.y () - self.pos ().y ()
j, k, l, m = deltaX / 50 , deltaY / 50 , self.pos ().x (), self.pos ().y ()
for i in range (50 ):
l, m = l + j, m + k
self.animation.setPosAt (i / 50 .0 , QtCore.QPointF (l, m))
self.__update (j, k)
timer.start ()
|
On utilise une QTimeLine() pour définir la durée de notre animation. Son argument (1000)
est en millisecondes.
Un nombre de frames, qui seront créées pour cette animation, est choisit, toujours arbitrairement,
soit cinquante.
Après calcul des
deltaX
et
deltaY
, on procède à la création des frames
puis,
timer.start()
lancera l'animation.
L'utilisation de la timeline fera que les déplacements de la loupe ronde ou carrée seront
identiques ; toutefois, on notera que, avec cette méthode, la texture de la loupe est, dès le
départ, la texture finale.
Remarque : dans le code, cette fonction est renommée
recenter_magnifying_glass2(self)
,
il faudra donc, modifier les noms de ces deux dernières fonctions pour les tester alternativement,
ou renommer leur appel à la ligne 163.
XII. Les effets graphiques
Les effets graphiques sont des modifications de rendu d'un objet graphique intervenant entre cet
objet et l'espace graphique, donc sans altération de l'objet source lui-même.
Du fait de l'absence de modification de l'objet source, un effet graphique peut être appliqué et
annulé à volonté, permettant ainsi, par exemple, de recréer un effet avec des paramètres différents,
comme dans une boîte de dialogue de filtres graphiques proposant une fonction d'aperçu.
Pour créer ses propres effets graphiques il faudra sous-classer QGraphicsEffect et y
réimplémenter sa méthode draw(). Celle-ci prend une instance de QPainter en argument.
L'effet sera appliqué au QGraphicsItem avec la méthode setGraphicsEffect() et
peut être annulé simplement avec QGraphicsEffect.setEnabled(False).
Quatre effets graphiques de base sont proposé par Qt :
-
QGraphicsOpacityEffect : modifie l'opacité de l'objet ;
-
QGraphicsBlurEffect : applique un flou d'une largeur donnée en pixels ;
-
QGraphicsColorizeEffect : colorise l'objet ;
-
QGraphicsDropShadowEffect : applique une ombre portée.
Les trois premiers effets n'ayant pas beaucoup d'intérêt pour l'objet loupe, on utilisera l'effet
d'ombrage comme exemple.
La création de l'effet consiste principalement à le paramétrer.
main.py
def apply_shadow_effect (self):
self.shadow = QtGui.QGraphicsDropShadowEffect ()
self.shadow.setColor (QtGui.QColor (50 , 50 , 50 , 200 ))
self.shadow.setXOffset (5 )
self.shadow.setYOffset (5 )
self.shadow.setBlurRadius (3 )
self.mgnf.setGraphicsEffect (self.shadow)
self.shadow.setEnabled (False )
|
Une instance de QGraphicsDropShadowEffect() est créée, La couleur de l'ombre est définie,
ici avec une opacité de 200, les paramètres
XOffset
et
YOffset
représentent la valeur du décalage de l'ombre mais aussi le côté où elle sera appliquée
(ici en bas à droite) ; si les deux valeurs avaient été négatives, l'ombre aurait été placée
en haut à gauche.
Un flou de trois pixels est appliqué à l'ombre et l'effet est appliqué au QGraphicsItem avec
sa méthode setGraphicsEffect().
L'effet peut être désactivé avec
self.shadow.setEnabled(False)
, pour l'exemple,
dans les évènements clavier de notre loupe on trouvera ces lignes :
magnifier.py
def keyPressEvent (self, event):
...
elif event.key () = = 83 :
try :
self.main.shadow.setEnabled (not self.main.shadow.isEnabled ())
except :
pass
|
Ces lignes permettent de tester l'effet ombrage avec le raccourci S.
XIII. Conclusions
Cet article est une approche des très nombreuses possibilités des QGraphicsItem qui sont
à considérer comme de puissants outils de travail pouvant apporter une réelle plus-value
aux applications où l'aspect graphique est un critère important.
XIV. Liens