Wiki

Optimizing with QPixmapCache

by Mark Summerfield
Widget painting that ends up being done repeatedly can make programs unresponsive. This article shows how to speed up applications by caching the results of painting.

[Download Source Code]

The QPixmapCache class provides a global QPixmap cache. Its API consists entirely of static functions for inserting, removing, and looking up a pixmap based on an arbitrary QString key.

We will see two examples of widgets that can be optimized using QPixmapCache.

Widgets with Few States

A common case is a custom widget that has a few states, each with its own appearance. For example, a QRadioButton has two possible appearances (on and off), each of which is stored in a cache the first time it is needed.

Thereafter, no matter how many radio buttons an application uses, the cached appearances are used and no further drawing takes place.

Lights2

This approach is used throughout Qt and can easily be used in custom widgets. We'll illustrate how it is done by creating a simple "traffic lights" custom widget. Let's start with its definition:

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;
};

The definition is unsurprising, except for the generatePixmap() function that we'll review shortly.

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

When the diameter is changed, we call update() to schedule a paint event and updateGeometry() to tell any layout manager responsible for this widget that the widget's size hint has changed.

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();
}

The mouse press event is included for completeness. If the user clicks in the first third of the widget (from the top), we change the color to red, and similarly for yellow and for green. Then we call update() to schedule a paint event.

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);
}

In the paint event, we start by generating a key string to identify each appearance. In this example, the appearance depends on two factors: which color is "lit up" and the widget's diameter. We then create an empty pixmap.

The QPixmapCache::find() function looks for a pixmap with the given key. If it finds a match, it returns true and copies the pixmap into its second argument (a non-const reference); otherwise it returns false and ignores the second argument.

So if we don't find the pixmap (for example, if this is the first time we've used this particular color and diameter combination), we generate the required pixmap and insert it into Qt's global pixmap cache. In both cases, pixmap ends up containing the widget's appearance and we finish by bit-blitting it onto the widget's surface.

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;
}

Finally, we have the code to draw the widget's appearance. We create a pixmap of the right size and then create a painter to paint on the pixmap. We begin by drawing a dark gray rectangle over the entire pixmap's surface, since QPixmaps are uninitialized when created. Then, for each color, we set the brush and draw the appropriate circle.

By using QPixmapCache, we have ensured that no matter how many instances of the Lights class we have, we will only need to draw its appearance once for each color x diameter combination that's used - unless the pixmap cache is full.

Computationally Expensive Painting

Some custom widgets have a potentially infinite number of states, for example, a graph widget. Clearly, if we were to cache the appearance of every graph the user plotted, we would consume a lot of memory for no benefit, since the user might constantly vary their data and never view the same graph twice.

But there are situations where the data does not change and caching is beneficial - for example, if the widget is obscured and needs to be repainted when it is made visible again, or if some drawing operations are performed on top of it (for example, drawing a selection rectangle).

Graph

We'll look at an example of a very simple graph widget that can plot a single set of points. Its definition is quite similar to the Lights class, so we'll just focus on the essential functions, starting with the paint event handler:

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);
}

The paint event handler is almost identical to the Lights class's paint event, except that the key generation is handled by a separate function:

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

The key we produce simply identifies the Graph instance. It would be impractical to encode the entire state of the graph as a string.

QPixmap Graph::generatePixmap()
{
    QPixmap pixmap(m_width, m_height);
    pixmap.fill(this, 0, 0);
 
    QPainter painter(&pixmap, this);
    painter.drawPolyline(m_points);
    return pixmap;
}

We create a pixmap of the right size, and this time we initialize it by filling it. Then we draw the points as a polygonal line. This is all very similar to what we did for the Lights example. The crucial difference is in the setData() function:

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

Whenever the data is changed, we delete the cached appearance and schedule a paint event. When the paint event occurs, the key won't be found in the cache and the appearance will be freshly generated. So when the user creates a graph, that graph's appearance will be cached. But as soon as the user changes the data a new graph is generated and cached, and the old one is discarded.

Only one pixmap per instance of the Graph widget is held in the cache, and this pixmap is used whenever a repaint is necessary for reasons other than a data change (for example, if the graph is obscured and then revealed), thus avoiding unnecessary calls to a potentially expensive generatePixmap() function.

An alternative solution would be to have a QPixmap member in the Graph class that holds the cached pixmap (see the "Double Buffering" section of C++ GUI Programming with Qt 3 for an example of this approach). But this has the disadvantage that if the graph is extremely large, or if the application creates many Graph instances, the application might run out of pixmap memory. Using the global QPixmapCache is safer because QPixmapCache enforces an upper limit on the cumulative size of stored items (1 MB by default).


Copyright © 2005 Trolltech Trademarks