​ 在我的项目中,含有大量重复的图形,重复绘制这些相同的图形无疑会增加很多多余的时间开销。QGraphicsItem自身提供了缓存的方法,我们可以通过setCacheMode()来设置,但是它为每个QGraphicsItem对象创建一个缓存,并不能让多个相同的item共享缓存,为了减少多次绘制相同item时的时间开销,我需要自己实现缓存的机制。

缓存定义

​ 一些图形项会引用相同的数据,图形项的形状则是基于这些数据,例如一些图形项引用myMacro对象,因此,我们可以在myMacro中定义它的缓存:

class myMacro
{
public:
    bool pixmapCacheEnable();
    void setPaintCacheEnable(bool enable);
    QPixmap* getPixmapCache();
private:
    QPixmap* macroPixCache;
    bool pixCacheEnable = false;
    ...
}

​ 在第一次绘制时,将绘制的结果放入macroPixCache,并将pixCacheEnable设置为true,后续需要绘制相同的Macro时,就可以直接由缓存绘制。关于macroPixCache的大小,可以根据需要自行设置,例如我创建的是32x32QPixmap,当绘制区域大于该范围时,则不再使用缓存。

坐标映射

​ 在我的项目中,缓存主要是为了减少绘制多次相同图形时的开销,相比基于逻辑坐标的缓存,基于设备坐标的缓存更适用于该目的(关于二者的区别,可以查看描述QGraphicsItem::CacheMode属性的文档),当视图缩放比例比较小时,视图中会出现很多相同的图形,相同的图形可能会出现数百次;当视图缩放比例比较大时,视图中出现的图形数量较少,此时则不需要使用缓存。另外,我使用的item,其逻辑坐标数值比较大(长宽至少也有几百,有些能达到几万至上百万),从这一点上看,也不太适用逻辑坐标。

​ 为了实现基于设备坐标的缓存,我们需要完成由逻辑坐标到设备坐标的转换:

  • 首先进行缩放变换,将逻辑坐标的矩形区域转换为设备坐标上的大小
  • 将转换后的矩形区域向外拓展一圈,这一步是为了让缓存能够完整的保存边界,否则可能会出现边界缺失的情况
  • 然后,将矩形区域的起始点定位到(0, 0)的位置

​ 通过这样的操作,我们可以得到由item到cache的转换矩阵,创建一个新的painter,设置它的transform矩阵,并绑定pixmap作为绘制设备,完成这一步设置后,再调用实际的绘制函数,即可将图像绘制进缓存中。

// myItem.cpp
void myItem::paintIntoCache(QPainter *painter, QPaintDevice *paintDevice, const QStyleOptionGraphicsItem *option)
{
    QRectF deviceBounds = painter->worldTransform().mapRect(boundingRect());
    QRect deviceRect = deviceBounds.toRect().adjusted(-1, -1, 1, 1);
    QPointF p = deviceRect.topLeft();
    QTransform itemToPixmap = painter->worldTransform();
    if (!p.isNull())
        itemToPixmap *= QTransform::fromTranslate(-p.x(), -p.y());

    QPainter pixmapPainter;
    pixmapPainter.begin(paintDevice);
    pixmapPainter.setRenderHint(QPainter::Antialiasing, painter->testRenderHint(QPainter::Antialiasing));
    pixmapPainter.setTransform(itemToPixmap);
    _m_paintItem(&pixmapPainter, true, option); // 设置好painter的转化矩阵后,调用实际的绘制函数
    pixmapPainter.end();
}

void myItem::_m_paintItem(QPainter *painter, bool paintToCache, const QStyleOptionGraphicsItem *option)
{
    ...
}

​ 如果缓存有效,则由缓存进行绘制,这一步比较简单,因为我们绘制进pixmap时,已经是基于view的图形了,不需要再进行额外的转化。

// myItem.cpp
void myItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
{
    ...
    qreal widthInView  = m_w * lod;
    qreal heightInView = m_h * lod;
    bool cacheEnable = m_macro->pixmapCacheEnable();
    if(cacheEnable) {
        painter->save();
        QRectF sourceRect = QRectF(-lod, lod , widthInView + 2, heightInView + 2);
        QRectF deviceBounds = painter->worldTransform().mapRect(getGlobalBoundRect());
        QRect deviceRect = deviceBounds.toRect().adjusted(-1, -1, 1, 1);
        painter->setTransform(QTransform());
        painter->drawPixmap(deviceRect.topLeft(), *blockPixCache, sourceRect);
        painter->restore();    
    }
    ...
}

缓存刷新

​ 当缓存起效后,在某些情况下应该对缓存进行刷新,主要有以下两个时机:

  • 当视图的缩放比例改变时
  • 当视图的显示内容发生改变时

​ 可以将这些操作写在槽函数中,以便更加灵活的调用。

// 缩放改变时,重置cache
void myView::onZoomChanged()
{
    if(GLOBAL_VARS::mydb)
        GLOBAL_VARS::mydb->disableMacroPixCache();
}
// 绘图状态改变时,重置cache
void myView::graphicStateChangedSlot(graphicStateManager::GraphicStateItemChange changeType) {
    if (changeType == graphicStateManager::VisibleChanged ||
        changeType == graphicStateManager::PaddingChanged) {
        if(GLOBAL_VARS::hmdb) GLOBAL_VARS::mydb->disableMacroPixCache();
    }
    ...
}

多级缓存机制

​ 对于一些较大图形项,其绘制开销有时会特别大,但之前我们只分配了一块很小的缓存区域,绝大多数情况下,缓存对较大的图形项是不起效的。为此,我们可以为它们特殊分配一块更大的缓存,例如,在创建所有的图形后,利用小根堆对图形的面积进行排序,然后分配特殊的缓存区域:

void myDB::allocateBlockPixCache()
{
    auto cmp = [](myItem* a, myItem* b) {
        QRectF ra = a->boundingRect();
        QRectF rb = b->boundingRect();
        return ra.width() * ra.height() > rb.width() * rb.height(); // min-heap
    };
    std::priority_queue<myItem*, std::vector<myItem*>, decltype(cmp)> pq(cmp);
    foreach(myItem* item, itemNameHash) {
        if(item->type() == blockItem::type) {
            item->clearBlockPixCacheSpace();
            pq.push(item);
            if(pq.size() > 32)
                pq.pop();
        }
    }
    // 16 for 64*64, 8 for 128*128, 4 for 256*256, 2 for 512*512, 1 for 1024*1024, 1 for 2048*2048
    while(!pq.empty()) {
        if(pq.size() == 1) pq.top()->setBlockCacheSize(QSize(2048, 2048));
        else if(pq.size() == 2) pq.top()->setBlockCacheSize(QSize(1024, 1024));
        else if(pq.size() <= 4) pq.top()->setBlockCacheSize(QSize(512, 512));
        else if(pq.size() <= 8) pq.top()->setBlockCacheSize(QSize(256, 256));
        else if(pq.size() <= 16) pq.top()->setBlockCacheSize(QSize(128, 128));
        else pq.top()->setBlockCacheSize(QSize(64, 64));
        pq.pop();
    }
}

​ 在绘制时,如果该item分配了特殊的缓存区域,判断能否将图形绘制进特殊缓存。

void myItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
{
    ...
    if(widthInView > 30 || heightInView > 30) {
        if(!blockPixCache || widthInView > blockPixCache->width() || heightInView > blockPixCache->height()) {
            // 直接绘制
        } else {
            // 绘制进特殊缓存
        }
    } else {
        // 绘制进通用分配的缓存
    }
    ...
}