Optimisation avec QPixmapCache

| Mark Summerfield | traducteur : Guillaume Belz | 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.

Cet article est une traduction autorisée de http://qt.nokia.com/doc/qq/qq12-qpixmapcache.html, par Mark Summerfield.

- Titre : Optimisation avec QPixmapCache

- Auteurs : Mark Summerfield, traducteur : Guillaume Belz, Qt Quarterly

- Parution : 8 avril 2010

- Mise à jour : 30 avril 2010

- Licence : 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.

Sommaire

- 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 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 ou Qt Software.

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

Nous allons voir deux exemples de widgets qui peuvent être optimisés en utilisant la classe QPixmapCache.

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 à deux apparences possibles (On et Off), chaque apparence étant stockée dans un cache la première fois où 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 mise à jour des pixmaps ne sera de nouveau réalisée.

Cette approche est utilisée tout au long de Qt et peut facilement être utilisée dans les widgets personnalisés. Nous allons illustrer comment cela est réalisé en créant un widget personnalisé appelé Lights (représentant des feux tricolores). Commençons par sa définition :

class Lights : public QWidget
{
Q_OBJECT

public:
Lights(QWidget *parent)
: QWidget(parent), m_color(Qt::red),
m_diameter(80)
{}

QColor color() const { return m_color; }
int diameter() const { return m_diameter; }
QSize sizeHint() const
{ return QSize(m_diameter, 3 * m_diameter); }

public slots:
void setDiameter(int diameter);

signals:
void changed(QColor color);

protected:
void mousePressEvent(QMouseEvent *event);
void paintEvent(QPaintEvent *event);

private:
QPixmap generatePixmap();

QColor m_color;
int m_diameter;
};

La définition ne présente pas de surprise, à l'exception de la fonction generatePixmap() que nous allons examiner par la suite.

void Lights::setDiameter(int diameter)
{
m_diameter = diameter;
update();
updateGeometry();
}

Lorsque le diamètre est changé, nous appelons la fonction update() pour planifier un événement de mise à jour du widget ainsi que la fonction updateGeometry() pour prévenir le gestionnaire d'affichage responsable de ce widget que la taille du widget a changé.

void Lights::mousePressEvent(QMouseEvent *event)
{
if (event->y() < m_diameter) {
m_color = Qt::red;
} else if (event->y() < 2 * m_diameter) {
m_color = Qt::yellow;
} else {
m_color = Qt::green;
}

emit changed(m_color);
update();
}

Pour que la classe soit complète, la gestion des clics souris est également ajoutée. Si l'utilisateur clique dans le premier tiers du widget (à partir du haut), nous changeons la couleur en rouge, de même pour le jaune et vert. Ensuite, nous appelons la fonction update() pour planifier un événement de mise à jour du widget.

void Lights::paintEvent(QPaintEvent *)
{
QString key = QString("lights:%1:%2")
.arg(m_color.name())
.arg(m_diameter);
QPixmap pixmap;

if (!QPixmapCache::find(key, pixmap)) {
pixmap = generatePixmap();
QPixmapCache::insert(key, pixmap);
}
bitBlt(this, 0, 0, &pixmap);
}

Dans la fonction de mise à jour du widget, nous commençons 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. Nous créons ensuite une pixmap vide.

La fonction QPixmapCache::find() recherche une pixmap avec la clé donnée. Si elle trouve une correspondance, elle renvoie Vrai et copie la pixmap dans son deuxième argument (une référence non constante), sinon elle retourne Faux et ignore le second argument.

Donc, si nous ne trouvons pas la pixmap (par exemple, si c'est la première fois que nous utilisons une combinaison particulière de couleur et de diamètre), nous générons la pixmap demandée et nous l'insérons dans le cache global de pixmaps. Dans les deux cas, la pixmap contient finalement l'apparence du widget et nous finissons par une copie bit par bit de la pixmap sur la surface du widget.

QPixmap Lights::generatePixmap()
{
int w = m_diameter;
int h = 3 * m_diameter;

QPixmap pixmap(w, h);
QPainter painter(&pixmap, this);

painter.setBrush(darkGray);
painter.drawRect(0, 0, w, h);

painter.setBrush(
m_color == Qt::red ? Qt::red
: Qt::lightGray);
painter.drawEllipse(0, 0, w, w);

painter.setBrush(
m_color == Qt::yellow ? Qt::yellow
: Qt::lightGray);
painter.drawEllipse(0, w, w, w);

painter.setBrush(
m_color == Qt::green ? Qt::green
: Qt::lightGray);
painter.drawEllipse(0, 2 * w, w, w);

return pixmap;
}

Enfin, nous avons ici le code pour redessiner l'apparence du widget. Nous créons une pixmap de la bonne taille et puis nous créons un objet QPainter pour dessiner sur la pixmap. Nous commençons 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 leurs créations. Ensuite, pour chaque couleur, nous adaptons la couleur de l'outil de dessin et dessinons le cercle approprié.

En utilisant QPixmapCache, nous sommes assurés que, peu importe le nombre d'instances de la classe Light, nous n'aurons besoin de dessiner qu'une seule fois son apparition pour 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, par exemple un widget représentant une courbe. De toute évidence, si nous mettons en cache l'apparence de toutes les courbes que l'utilisateur trace, nous allons consommer beaucoup de mémoire sans aucune amélioration de la vitesse d'affichage puisque l'utilisateur peut faire varier en permanence ses données et ne jamais afficher la même courbe à deux reprises.

Mais il ya des situations où 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 au-dessus de celui-ci (par exemple dessiner un rectangle de sélection).

Nous allons voir 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 donc nous allons nous concentrer uniquement sur les fonctions essentielles, à commencer par la fonction de mise à jour du widget :

void Lights::mousePressEvent(QMouseEvent *event)
void Graph::paintEvent(QPaintEvent *)
{
if (m_width <= 0 || m_height <= 0)
return;

QPixmap pixmap;

if (!QPixmapCache::find(key(), pixmap)) {
pixmap = generatePixmap();
QPixmapCache::insert(key(), pixmap);
}
bitBlt(this, 0, 0, &pixmap);
}

La fonction 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 fonction distincte :

void Lights::mousePressEvent(QMouseEvent *event)
QString Graph::key() const
{
QString result;
result.sprintf("%p", static_cast<const void *>(this));
return result;
}

La clé que nous produisons identifie simplement l'instance de la classe Graph. Il serait difficile de coder tout l'état du graphique sous forme de chaînes de caractères.

void Lights::mousePressEvent(QMouseEvent *event)
QPixmap Graph::generatePixmap()
{
QPixmap pixmap(m_width, m_height);
pixmap.fill(this, 0, 0);

QPainter painter(&pixmap, this);
painter.drawPolyline(m_points);
return pixmap;
}

Nous créons une pixmap de la bonne taille et, cette fois, nous l'initialisons en la remplissant. Ensuite, nous dessinons la courbe en traçant une ligne polygonale passant par tous les points. Tout cela est très similaire à ce que nous avons fait pour l'exemple Lights. La différence essentielle se trouve dans la fonction setData() :

void Lights::mousePressEvent(QMouseEvent *event)
void Graph::setData(const QPointArray &points,
int width, int height)
{
m_points = points;
m_width = width;
m_height = height;
QPixmapCache::remove(key());
update();
}

Lorsque les données sont modifiées, nous supprimons l'apparence conservée dans le cache et planifions une mise à jour du widget. Lorsque la mise à jour se produit effectivement, 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. Mais 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 graphique 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 fonction 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). Mais 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 (1 Mo par défaut).

V - Divers

Le code source de l'exemple présenté dans cet article est disponible.

Merci à dourouc05 et à eusebe19 pour leurs relectures 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 !