Optimisation avec QPixmapCache
Date de publication : 08/04/2010. Date de mise à jour : 10/02/2012.
Par
Mark Summerfield
traducteur : Guillaume Belz
adaptation Python : Grégoire Lothe
Qt Quarterly
Mettre à jour à répétition les pixmaps des widgets peut bloquer
les programmes. Cet article montre comment améliorer la vitesse
d'exécution des applications en mettant en cache les pixmaps
obtenues en redessinant les widgets.
I. L'article original
II. Introduction
III. Cas des widgets pouvant prendre peu d'états différents
IV. Mises à jour de widgets coûteuses en temps CPU
V. Divers
I. L'article original
Qt Quarterly est une revue trimestrielle électronique proposée par Qt à destination des développeurs et
utilisateurs de Qt. Vous pouvez trouver les
versions originales.
Nokia, Qt, Qt Quarterly et leurs logos sont des marques déposées de Nokia Corporation en Finlande
et/ou dans les autres pays. Les autres marques déposées sont détenues par leurs propriétaires respectifs.
Cet article est une traduction d'un des tutoriels écrits par Nokia Corporation and/or its subsidiary(-ies)
et inclus dans la documentation de Qt, en anglais. Les éventuels problèmes résultant d'une mauvaise traduction ne sont
pas imputables à Nokia.
II. Introduction
Mettre à jour à répétition les pixmaps des widgets peut bloquer les programmes. Cet article montre comment améliorer la vitesse
d'exécution des applications en mettant en cache les pixmaps obtenues en redessinant les widgets.
La classe
QPixmapCache
fournit un cache global pour
QPixmap. Son interface est constituée entièrement de méthodes statiques
pour l'insertion, la suppression et la recherche d'une pixmap basée sur une chaîne de caractères utilisée comme clé arbitraire.
On va voir deux exemples de widgets qui peuvent être optimisés en utilisant la classe
QPixmapCache.
Les exemples sont réalisés sous Python 3.2 et PyQt 4.7.
III. Cas des widgets pouvant prendre peu d'états différents
Un cas fréquemment rencontré est celui d'un widget personnalisé qui a un petit nombre d'états, chacun ayant sa propre apparence.
Par exemple, un
QRadioButton
a deux apparences possibles (
On et
Off),
chaque apparence étant stockée dans un cache la première fois qu'elle est utilisée.
Par la suite, peu importe le nombre de boutons radio qui sont utilisés par une application, les apparences en cache sont
utilisées et aucune nouvelle mise à jour des pixmaps ne sera effectuée.
Cette approche est utilisée tout au long de Qt et peut facilement être utilisée dans les widgets personnalisés. On va illustrer
la manière de le réaliser en créant un widget personnalisé appelé Lights (représentant des feux tricolores).
class Lights(QtGui.QWidget):
def __init__(self, parent=None, color=QtCore.Qt.yellow, diameter = 150):
QtGui.QWidget.__init__(self, parent = parent)
self.__diameter = diameter
self.setColor(color)
|
On commence donc par hériter de la classe QWidget. On implémente juste après les deux méthodes employées.
def setDiameter(self, diameter):
self.diameter=diameter
self.update()
self.updateGeometry()
|
Lorsque le diamètre change, on appelle la méthode
update()
pour planifier un événement de mise à jour du widget ainsi que la méthode
updateGeometry()
pour prévenir le gestionnaire d'affichage responsable de ce widget que sa taille a changé.
def setColor(self, color):
if color in (QtCore.Qt.green, QtCore.Qt.yellow, QtCore.Qt.red):
self.color=color
self.update()
return True
else:
return False
|
La méthode setColor est simple : elle s'assure que la couleur désirée est valable, sinon elle renvoie False.
def mousePressEvent(self, event):
if event.y()<self.__diameter:
self.__color=QtCore.Qt.red
elif event.y()<self.__diameter*2:
self.__color=QtCore.Qt.yellow
else:
self.__color=QtCore.Qt.green
self.emit(QtCore.SIGNAL("changed(PyQt_PyObject)"), self.color)
self.update()
|
Pour que la classe soit complète, on ajoute la gestion des clics souris. Si l'utilisateur clique dans le premier tiers du widget (à partir du haut),
on change la couleur en rouge, de même pour le jaune et vert. Ensuite, on appelle la méthode
update()
pour planifier un événement de mise à jour du widget.
def paintEvent(self, event):
key="lights:{}:{}".format(self.__color, self.__diameter)
pixmap=QtGui.QPixmap()
painter=QtGui.QPainter(self)
if not QtGui.QPixmapCache.find(key, pixmap):
pixmap=self.generatePixmap()
QtGui.QPixmapCache.insert(key, pixmap)
painter.drawPixmap(QtCore.QPoint(0,0), pixmap)
|
Dans la méthode de mise à jour du widget, on commence par générer une clé sous forme d'une chaîne de caractères afin d'identifier chaque apparence. Dans cet exemple,
l'apparence dépend de deux facteurs : la couleur qui est "allumée" et le diamètre du widget. On crée ensuite une pixmap vide.
La méthode
QPixmapCache::find() () recherche une pixmap avec la clé donnée. Si elle trouve une
correspondance, elle renvoie
True et copie la pixmap dans son deuxième argument, sinon elle retourne
False et ignore le second argument.
Ainsi, si on ne trouve pas la pixmap (par exemple, si c'est la première fois que la combinaison particulière de couleur et de diamètre est utilisée), on génère la pixmap demandée
et on l'insère dans le cache global de pixmaps. Dans les deux cas, la pixmap contient finalement l'apparence du widget et on finit par l'appliquer la pixmap sur le widget à l'aide
de drawPixmap().
def generatePixmap(self):
w=self.__diameter
h=self.__diameter*3
pixmap=QtGui.QPixmap(w,h)
painter=QtGui.QPainter(pixmap)
painter.setRenderHint(QtGui.QPainter.Antialiasing)
painter.setPen(QtGui.QPen(0))
painter.setBrush(QtCore.Qt.darkGray)
painter.drawRect(0,0,w,h)
painter.setBrush(self.color)
if self.__color==QtCore.Qt.red:
painter.drawEllipse(0,0,w,w)
elif self.__color==QtCore.Qt.yellow:
painter.drawEllipse(0,w,w,w)
else:
painter.drawEllipse(0,w*2,w,w)
return pixmap
|
Enfin, on a ici le code pour redessiner l'apparence du widget. On crée une pixmap de la bonne taille et puis un objet
QPainter
pour dessiner dessus. On commence par dessiner un rectangle gris foncé sur l'ensemble de la surface du pixmap, puisque les objets
QPixmap
ne sont pas initialisés lors de leur création. Ensuite, pour chaque couleur, on adapte la couleur de l'outil de dessin et dessine le cercle approprié.
En utilisant
QPixmapCache, on s'assure que, peu importe le nombre d'instances de la
classe
Lights, on n'aura besoin de dessiner qu'une seule fois chaque combinaison de couleur et de diamètre qui est utilisée, à moins que
le cache soit plein.
IV. Mises à jour de widgets coûteuses en temps CPU
Certains widgets personnalisés ont un nombre potentiellement infini d'états, comme un widget représentant une courbe. De toute évidence, si on met en
cache l'apparence de toutes les courbes que l'utilisateur trace, on va consommer beaucoup de mémoire sans aucune amélioration de la vitesse d'affichage,
puisque les données varient potentiellement en permanence (on n'affichera jamais la même courbe à deux reprises).
Cependant, dans certains cas, les données ne changent pas et où la mise en cache est utile - par exemple, si le widget est masqué et doit être redessiné
quand il est à nouveau visible ou si certaines opérations sont effectuées (par exemple, dessiner un rectangle de sélection).
On va prendre l'exemple très simple d'un widget Graph traçant une courbe représentée par une seule série de points. Sa définition est
assez semblable à la classe Lights précédente, on se concentrera uniquement sur les méthodes essentielles, à commencer par la méthode de
mise à jour du widget :
def paintEvent(self, event):
if self.__width<=0 or self.__height<=0:
return
pixmap=QtGui.QPixmap()
painter=QtGui.QPainter(self)
if not QtGui.QPixmapCache.find(self.key(), pixmap):
pixmap=self.generatePixmap()
QtGui.QPixmapCache.insert(self.key(), pixmap)
painter.drawPixmap(0,0 , pixmap)
|
La méthode de mise à jour du widget est presque identique à celle de la classe Lights, sauf que la génération de clés est réalisée par
une méthode distincte :
def key(self):
return repr(self)
|
La clé produite identifie simplement l'instance de la classe Graph.
def generatePixmap(self):
pixmap=QtGui.QPixmap(self.__width, self.__height)
pixmap.fill(self, 0, 0)
painter=QtGui.QPainter(pixmap)
painter.setRenderHint(QtGui.QPainter.Antialiasing)
painter.drawPolyline(self.__points)
return pixmap
|
On crée une pixmap de la bonne taille et, cette fois, on l'initialise en la remplissant avec notre couleur d'arrière-plan à l'aide de QPixmap.fill().
Ensuite, on dessine la courbe en traçant une ligne polygonale passant par tous les points. Tout cela est très similaire à ce qui a été fait pour l'exemple Lights.
La différence essentielle se trouve dans la méthode setData() :
def setData(self, width, height, points):
self.__width=width
self.__height=height
self.__points=points
QtGui.QPixmapCache.remove(self.key())
self.updateGeometry()
self.update()
|
Lorsque les données sont modifiées, on supprime l'apparence conservée dans le cache et on planifie une mise à jour du widget. Lorsque la mise à jour se produit,
la clé ne sera pas trouvée dans le cache et l'apparence sera générée de nouveau. Ainsi, lorsque l'utilisateur affiche une courbe, l'aspect graphique sera mis en cache.
Cependant, dès que l'utilisateur modifie les données, l'ancienne courbe est supprimée et une nouvelle courbe est générée et mise en cache.
Seule une pixmap par instance du widget est conservée dans le cache et cette pixmap est utilisée chaque fois qu'il est nécessaire d'afficher la courbe pour des raisons
autres qu'un changement de données (par exemple, si la courbe est masquée puis de nouveau affichée), évitant ainsi des appels inutiles à la méthode
generatePixmap(), potentiellement coûteuse.
Une autre solution serait d'avoir une variable membre
QPixmap
dans la classe
Graph qui conserve la pixmap en cache
(voir la section
Double Buffering de l'ouvrage
C++ GUI
Programming with Qt 3 pour un exemple de cette approche).
Cependant, cela présente l'inconvénient suivant : si le graphique est
très grand ou si l'application crée de nombreuses instances
de la classe
Graph, l'application risque de dépasser la mémoire
allouée pour les pixmaps. L'utilisation de la
QPixmapCache globale
est plus sûre, parce que
QPixmapCache
utilise une limite plus importante
pour la taille cumulée des éléments stockés (10 Mo par défaut).
V. Divers
Le code source de l'exemple présenté dans cet article est disponible :
graph.py
et
lights.py.
Merci à
dourouc05 et
à
eusebe19
pour leur relecture de cet article et leurs conseils.
Au nom de toute l'équipe Qt, j'aimerais adresser le plus grand remerciement à
Nokia pour nous avoir autorisé à traduire cet article !


Copyright © 2005 Mark Summerfield. Aucune reproduction, même partielle, ne peut être faite
de ce site et de l'ensemble de son contenu : textes, documents, images, etc.
sans l'autorisation expresse de l'auteur.
Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 €
de dommages et intérêts.
Cette page est déposée.