Optimizing with QPixmapCache
Материал из Wiki.crossplatform.ru
Qt Quarterly | Выпуск 12 | Документация |
by Mark Summerfield
Widget painting that ends up being donerepeatedly can make programs unresponsive. This article shows howto speed up applications by caching the results of painting.
The QPixmapCache class provides a global QPixmap cache. ItsAPI 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 usingQPixmapCache.
[править] Widgets with Few States
A common case is a custom widget that has a few states, each with itsown appearance. For example, a QRadioButton has two possibleappearances (on and off), each of which is stored in a cache thefirst time it is needed.
Thereafter, no matter how many radio buttonsan application uses, the cached appearances are used and no furtherdrawing takes place.
This approach is used throughout Qt and caneasily be used in custom widgets.We'll illustrate how it is done by creating a simple "trafficlights" 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 apaint event and updateGeometry() to tell any layout managerresponsible 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 userclicks in the first third of the widget (from the top), we change thecolor to red, and similarly for yellow and for green. Then we callupdate() 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 identifyeach appearance. In this example, the appearance depends on twofactors: which color is "lit up" and the widget's diameter. We thencreate an empty pixmap.
The QPixmapCache::find() function looksfor a pixmap with the given key. If it finds a match, it returnstrue and copies the pixmap into its second argument (anon-const reference); otherwise it returns false and ignores thesecond argument.
So if we don't find the pixmap (for example, if this is thefirst time we've used this particular color and diameter combination),we generate the required pixmap and insert it into Qt's global pixmapcache. In both cases, pixmap ends up containing the widget'sappearance 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 createa pixmap of the right size and then create a painter to paint on thepixmap. We begin by drawing a dark gray rectangle over the entirepixmap'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 manyinstances of the Lights class we have, we will only need to drawits appearance once for each color x diameter combinationthat's used - unless the pixmap cache is full.
[править] Computationally Expensive Painting
Some custom widgets have a potentially infinite number of states, forexample, a graph widget. Clearly, if we were to cache the appearanceof every graph the user plotted, we would consume a lot of memory forno benefit, since the user might constantly vary their data and neverview the same graph twice.
But there are situations where the data does not change and cachingis beneficial - for example, if the widget is obscured and needs to berepainted when it is made visible again, or if some drawingoperations are performed on top of it (for example, drawing a selection rectangle).
We'll look at an example of a very simple graph widget that can plot asingle set of points. Its definition is quite similar to the Lightsclass, so we'll just focus on the essential functions, starting withthe 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'spaint event, except that the key generation is handled by a separatefunction:
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 wouldbe 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 itby filling it. Then we draw the points as a polygonal line. This isall very similar to what we did for the Lights example. Thecrucial 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 andschedule a paint event. When the paint event occurs, the keywon't be found in the cache and the appearance will be freshlygenerated. So when the user creates a graph, that graph's appearance willbe cached. But as soon as the user changes the data a new graph isgenerated and cached, and the old one is discarded.
Only one pixmapper instance of the Graph widget is held in the cache, and thispixmap is used whenever a repaint is necessary for reasons other thana data change (for example, if the graph is obscured and thenrevealed), thus avoiding unnecessary calls to a potentially expensivegeneratePixmap() function.
An alternative solution would be to have a QPixmap member in theGraph class that holds the cached pixmap (see the "DoubleBuffering" section of C++ GUI Programming with Qt 3 for anexample of this approach). But this has the disadvantage that if thegraph is extremely large, or if the application creates manyGraph instances, the application might run out of pixmap memory.Using the global QPixmapCache is safer because QPixmapCacheenforces an upper limit on the cumulative size of stored items (1 MBby default).