在我的项目中,含有大量重复的图形,重复绘制这些相同的图形无疑会增加很多多余的时间开销。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
的大小,可以根据需要自行设置,例如我创建的是32x32
的QPixmap
,当绘制区域大于该范围时,则不再使用缓存。
坐标映射
在我的项目中,缓存主要是为了减少绘制多次相同图形时的开销,相比基于逻辑坐标的缓存,基于设备坐标的缓存更适用于该目的(关于二者的区别,可以查看描述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 {
// 绘制进通用分配的缓存
}
...
}