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 la traduction de l'article Optimizing with QPixmapCache de Mark Summerfield paru dans la Qt Quarterly Issue 12.

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.

Image non disponible

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).

 
Sélectionnez
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.

 
Sélectionnez
    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é.

 
Sélectionnez
    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.

 
Sélectionnez
    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.

 
Sélectionnez
    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().

 
Sélectionnez
    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).

Image non disponible

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 :

 
Sélectionnez
    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 :

 
Sélectionnez
    def key(self):
        return repr(self)

La clé produite identifie simplement l'instance de la classe Graph.

 
Sélectionnez
    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() :

 
Sélectionnez
    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 !