在项目中,当我移动一个具有较多子项的图形项时,界面的刷新非常缓慢,而如果把子项的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%的绘制时间。另外,因为单独为选中图形创建了一个对象,在拖动图形时只改变了一个外框的位置,拖动结束后才改变图形自身的位置,因此可以避免图形编辑过程中出现卡顿