By Mark Summerfield(原文请见:http://doc.qt.nokia.com/qq/qq12-qpixmapcache.html)
TroyCheng新博客:
http://troychengspace.appspot.com/2010/07/7/linux16.htmlWidget在反复绘制图像的时候常常导致程序不响应,针对这个问题,这篇文章主要介绍一下如何使用缓存机制来加速应用程序。
[下载源代码]
QPixmapCache 类提供了一个全局的QPixmap缓存,它的API由一组用于插入、查找和删除的静态函数组成,这些函数所使用的QString类型的key由用户定义。(还有Key类型文章里面没有提)
我们通过两个示例来看看如何通过QPixmapCache来进行优化。
具有较少状态的Widgets
举一个简单的例子,对于一个具有较少状态的Widget,其中每一种状态又有自己的一套外观。例如, QRatioButton有两种状态,开和关,每种状态在第一次使用的时候都被存在缓存中。之后,无论程序使用了多少个ratio button,使用的都是在缓存中的外观副本,而不会再有重复的painting发生。
Qt所使用的这种方法可以很容易的使用到自定义的Widget中。我们可以通过下面的这个简单的红绿灯的例子来进行说明。
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;
};除了generagePixmap()函数外定义过程没有什么特别的地方。generatePixmap函数稍后介绍。
void Lights::setDiameter(int diameter)
{
m_diameter = diameter;
update();
updateGeometry();
}当diameter改变的时候,我们调用update函数去schedule一个paint event然后调用updateGeometry ( )来告诉任意一个layout manager这个widget的size hint已经发生了变化
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();
}为完整性我们添加了一个鼠标点击事件,当用户点击Widget上的第一个图标时,我们将它的颜色改为红色,对黄色和绿色采取类似的操作。然后调用paint函数来调度一个paint事件。
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);
}在paint事件中,我们首先生成每一个外观所需要的key。在这个例子中,外观依赖两种因素:需要点亮哪种颜色以及Widget的直径。有这两个因素之后我们就可以创建一个空的pixmap。
QPixmapCache:: find() 函数用过指定的key来查找pixmap,如果找到了匹配的缓存图片那么该函数会返回true并将缓存图片copy给第二个参数pixmap (一个非const引用),否则返回false并且忽略第二个参数。如果不需要在缓存中查找pixmap(例如第一次使用这种颜色和直径的组合),我们将会生成所需的pixmap并将它插入到Qt的全局缓存空间中去。在这两种情况中,用作外观的pixmap最终都会绘制到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;
}最终,这是绘制widget外观的代码。首先创建一个有正确大小的pixmap然后创建一个painter用来绘制pixmap。接着,在pixmap的表面绘制一个暗灰色的矩形框,然后,设置brush并且画一个适当大小的圆圈。
通过使用QPixmapCache,我们确保无论有多少个Light类,在缓存未满之前,我们都只需要对每种颜色和直径的组合绘制一次
计算开销较大的Painting操作
对于一些自定义的Widget,通常它的状态无法确定,例如,一个图片Widget,如果会与用户绘制的每一个图片都做缓存的话,可能会得不偿失,因为用户可能不会经常去改变它的图片或者不会绘制同样的图片。
但是,对于数据不改变的情况,缓存也会有一定好处,例如如果Widget被遮挡然后需要重新呈现的时候,或者是在该Widget之上另需绘制其它图片的时候(例如绘制一个选择方框)。
我们来看一个简单的例子,在一个Graph widget上画一系列点,它的定义和Light类很想,因此我们只关注一些必要的函数,从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);
}Paint event handler几乎和Light类的一样,除了产生key的函数是由另一个单独的函数提供。
QString Graph::key() const
{
QString result;
result.sprintf("%p", static_cast<const void *>(this));
return result;
}我们生成的key仅标示Graph实例。将整个Graph对象转换为字符串用作key就有点太impractical了。
QPixmap Graph::generatePixmap()
{
QPixmap pixmap(m_width, m_height);
pixmap.fill(this, 0, 0);
QPainter painter(&pixmap, this);
painter.drawPolyline(m_points);
return pixmap;
}上述代码首先创建了一个pixmap,这次使用填充的方式来初始化。然后以polygonal line的方式画些点。这个在Lights例子中的处理方式一样,最不同的地方在于setData函数。
void Graph::setData(const QPointArray &points,
int width, int height)
{
m_points = points;
m_width = width;
m_height = height;
QPixmapCache::remove(key());
update();
}当数据发生改变的时候,删除缓存的appearance然后产生一个paint event。当paint event发生的时候,使用key在缓存中查找不到副本,程序就会重新绘制apperance。因此,当用户创建graph的时候,这个graph会被缓存,当用户改变数据的时候会产生一个新的图并被缓存下来,原来的图会被丢弃掉。
在缓存中自始至终只有Graph widget的一个实例,因此,除非当用户重新绘图的时候,其它的类似隐藏然后显示等等状态都不会造成重新绘制图像,也就避免了不需要的画图的开销。
另一个可选的方案是使用一个QPixmap对象作为Graph对象的成员,用于缓存Pixmap(参见 C++ GUI programming with Qt 3 中的Double Buffering一节)。但是这种方案的缺点也非常明显,如果application创建了许多实例,这种方式会很快耗尽pixmap的内存空间。使用全局的QPixmapCache要安全一些,because QPixmapCache enforces an upper limit on the cumulative size of stored items (1 MB by default).