在项目中,当我移动一个具有较多子项的图形项时,界面的刷新非常缓慢,而如果把子项的visible设置为false,则刷新速度能得到显著的提升。
通过查看qgraphicsscene.cpp源码,我们发现,当scene对item进行绘制时,会递归地对图形项进行处理,而在这个函数中,它首先判断item的visible属性:
void QGraphicsScenePrivate::drawSubtreeRecursive(QGraphicsItem *item, QPainter *painter,
const QTransform *const viewTransform,
QRegion *exposedRegion, QWidget *widget,
qreal parentOpacity, const QTransform *const effectTransform)
{
Q_ASSERT(item);
if (!item->d_ptr->visible)
return;
...
}
这个函数下的处理步骤不少,比如,会处理层次间的变换矩阵以及透明度等信息,而且是在绘制时递归的调用,因此可能会显著影响绘图的效率。我还发现,对于包含子项的图形项,如果需要绘制该图形项,那么这个图形项的所有子项都会被绘制,这部分我们需要专门的优化,因为有些图形项会包含数十万子项。另外,scene可能还会检测item的碰撞等事件,对于包含很多子项的图形项,似乎每一个子图形项都会进行这样的计算,这也导致了移动图形时的卡顿。
因为项目的以下特点,我决定自己管理图形项,而不是由scene进行管理:
-
需要处理大量的图形项
-
一个图形项下可能包含很多的子项
-
只有对视图整体的缩放变换,图形项之间没有任何变换关系
顶层图形项
在这里,我使用一个继承自QGraphicsItem的topItem,将topItem放入scene中,然后在topItem中对我的图形进行管理,这样,对scene来说,它只拥有topItem这一个图形项。
坐标系统
在原本的QGraphicsView框架中,我们只需要设置item的相对坐标,框架会自行处理这些item之间的关系,并进行显示或者事件传递,为了能够自己管理item,我们首先要自己定义一个坐标系统:
// myitem.h
class myItem : public QGraphicsItem
{
public:
QPointF getGlobalPos() const; // 获取全局坐标
virtual void setGlobalPos(const QPointF &parentPos); // 传入父item的全局坐标,设置该item的全局坐标
void setMyPos(const QPointF &pos);
void setMyPos(qreal x, qreal y);
QPointF myPos() const; // 相对坐标
...
protected:
QPointF m_pos;
QPointF m_globalPos; // 此处将globalPos缓存一份,避免在每次获取时现场计算
topItem* m_parent = nullptr;
}
// myitem.cpp
QPointF myItem::getParentGlobalPos() const
{
if(m_parent != nullptr)
return m_parent->getGlobalPos();
return QPointF(0, 0);
}
void myItem::setGlobalPos(const QPointF &parentPos)
{
m_globalPos = parentPos + m_pos;
}
void myItem::setMyPos(const QPointF &pos)
{
graphicsGlobal.needSaveAddItem(instanceName(), this);
const QVariant newPos = itemChange(myItemChange::ItemPositionChange, pos);
if(m_parent != nullptr) // pos改变时,重设它在topItem下的索引位置
m_parent->removeItemFromIndex(this);
m_pos = newPos.toPointF();
setGlobalPos(getParentGlobalPos());
m_relativeBoundingRect = boundingRect().translated(m_pos);
synChangesToBound();
if(m_parent != nullptr)
m_parent->addItemToIndex(this);
}
void myItem::setMyPos(qreal x, qreal y)
{
setMyPos(QPointF(x, y));
}
QPointF myItem::myPos() const
{
return m_pos;
}
创建索引
为了能够快速查找特定位置下的item,我们必须为它创建一个索引,在我的项目中,我用到了两种索引,一种是四叉树,用于管理平面中的矩形,另一种是特化的2D-Tree,用于管理平面中的线段(实际是宽度非常小的矩形)。
// 以四叉树为例,它提供了添加、删除、查找等基本功能:
namespace quadtree
{
static Box<double> getBox(myItem* item) {
QRectF rect = item->relativeBoundRect();
return Box<double>(rect.x(), rect.y(), rect.width(), rect.height());
}
class MyQuadtree {
public:
MyQuadtree(QRectF region) :
quadtree(Box<double>(region.x(), region.y(), region.width(), region.height()), getBox) {
}
~MyQuadtree() {}
void add(myItem* item);
void remove(myItem* item);
QList<myItem*> query(QRectF area) const;
QList<myItem*> query(QPointF pos) const;
private:
void rebuild(Box<double> newBox);
private:
Quadtree<myItem*, decltype(&getBox), std::equal_to<myItem*>, double> quadtree;
};
} // end namespace
关于这一部分的内容,我会在之后写一篇文章详细讲述。
图形项的选择操作
至此,我们已经有能力快速的获取到自己管理的任何一个item,然后通过重载mouseReleaseEvent,我们可以将鼠标事件传递给myItem。
// topItem.cpp
void topItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
{
if(event->scenePos() == event->buttonDownScenePos(Qt::LeftButton)) {
QList<myItem *> queryItems = m_innerItems.query(event->scenePos());
if(!queryItems.empty()) {
queryItems[0]->mouseReleaseEvent(event);
}
}
...
}
例如,当鼠标左键松开,并且在点击与松开过程中鼠标坐标不变,则触发item的选中事件。对于选中状态的item,为了方便操作,我希望scene能对其进行管理,因此,除了自定义的myItem类外,我还定义了一个selectBoundaryItem类,当myItem被选中时,创建一个对应的myBoundaryItem,并将它加入scene中。其函数调用流程大致如下:
// 函数调用过程:
// myItem::setSelected() -->
// myItem::itemChange() -->
// myItem::mySelectionChanged() -->
// topItem::addSelectedBoundary(this) -->
// myScene::createBoundaryItem(QList<myItem*>)
void myScene::createBoundaryItem(const QList<myItem*>& relatedItemList)
{
blockSignals(true);
foreach (myItem* relatedItem, relatedItemList) {
// 避免重复创建,在boundaryItem的构造函数中,将relatedItem的selected和selectable设置为false
// 相对的,在boundaryItem的析构函数中,将relatedItem的selectable设置为true
selectBoundaryItem* boundaryItem = boundaryItemFactory::createBoundary(relatedItem);
if(boundaryItem == nullptr) continue;
addItem(boundaryItem);
m_selectedBounds.append(boundaryItem);
}
blockSignals(false);
emit selectionChanged();
}
// 创建完成后,boundaryItem为选中状态,当它离开选中状态时,则销毁该对象
void myScene::selectionItemChangedSlot()
{
blockSignals(true);
// destory all boundary item which is unSelected
foreach(selectBoundaryItem* item, m_selectedBounds) {
if(!item->isSelected()) {
removeItem(item);
m_selectedBounds.removeOne(item);
delete item;
}
}
blockSignals(false);
emit ItemSelectedOrDeleted();
}
在这里,我们讨论了怎样对图形项使用自定义的管理方法,需要注意的是,本篇文章讲述得较为简略,还有很多未提及的地方,但只要认识到,topItem后的事件与绘制函数都需要手动进行传递,并正确处理这些事件,即可实现对item的自定义管理。 经过这样的改造后,在我的项目中,大约可以减少70%的绘制时间。另外,因为单独为选中图形创建了一个对象,在拖动图形时只改变了一个外框的位置,拖动结束后才改变图形自身的位置,因此可以避免图形编辑过程中出现卡顿