关于 QGraphicsItemGroup 内部项目发生变化后group重新定位的问题

在项目中为了便于对组合后的图元进行管理,一般会继承 QGraphicsItemGroup 实现自己的 group 类,这样可以方便的借用 QGraphicsItemGroup 对内部图元进行管理,但同时也受到了 QGraphicsItemGroup 实现的约束。例如:QGraphicsItemGroup 对象的默认原点坐标为{0,0};对鼠标键盘的消息默认由 QGraphicsItemGroup 处理,内部图元控件不会处理等。这里主要讨论使用 addToGroup() 或 removeFromGroup() 时 group 的 bound 会发生变化,此时需要调整 group 的坐标及尺寸。因为我们可能一次向 group 添加/移除一个控件,也可能添加、移除多个,group 的坐标及尺寸最好在添加/移除后进行调整。但是为了提高代码的内聚性,我们更希望在 group 内部图元发生变化后由 group 自动调整位置及大小。继承 QGraphicsItemGroup 后重载 itemChange() 方法,当内部图元发生变化时可以通过 change == QGraphicsItem::ItemChildAddedChange 监听添加图元的信号、change == QGraphicsItem::ItemChildRemovedChange 监听移除图元的信号。如果在 itemChange() 方法中处理 group 的坐标及尺寸就会发生不可思议的问题:明明位置与尺寸都计算正确,但是内部图元的位置却发生莫名的偏移。通过监控内部图元的坐标发现计算的坐标完全正确,但是显示位置就是不对。如下图,图一是组合前的位置,图二是组合后的位置,组合后显示的选择框就是重新调整后的 group 的位置及大小,内部的矩形与圆形已经偏离了原位置。


图一 组合前的位置


图二 组合后的位置

发生这个问题的原因是:不能在 itemChange() 方法内处理 group 的位置及坐标,因为此时addToGroup() 或 removeFromGroup() 的代码还未执行完毕。看一下 addToGroup() 的源码:

复制代码
// 文件位置 qt-everywhere-src-6.7.3\qtbase\src\widgets\graphicsview\qgraphicsitem.cpp
void QGraphicsItemGroup::addToGroup(QGraphicsItem *item)
{
    Q_D(QGraphicsItemGroup);
    if (!item) {
        qWarning("QGraphicsItemGroup::addToGroup: cannot add null item");
        return;
    }
    if (item == this) {
        qWarning("QGraphicsItemGroup::addToGroup: cannot add a group to itself");
        return;
    }

    // COMBINE
    bool ok;
    QTransform itemTransform = item->itemTransform(this, &ok);

    if (!ok) {
        qWarning("QGraphicsItemGroup::addToGroup: could not find a valid transformation from item to group coordinates");
        return;
    }

    QTransform newItemTransform(itemTransform);
    item->setPos(mapFromItem(item, 0, 0));
	// 设置父项目时会触发 itemChange() 方法
    item->setParentItem(this);

    // removing position from translation component of the new transform
    if (!item->pos().isNull())
        newItemTransform *= QTransform::fromTranslate(-item->x(), -item->y());

    // removing additional transformations properties applied with itemTransform()
    QPointF origin = item->transformOriginPoint();
    QMatrix4x4 m;
    QList<QGraphicsTransform*> transformList = item->transformations();
    for (int i = 0; i < transformList.size(); ++i)
        transformList.at(i)->applyTo(&m);
    newItemTransform *= m.toTransform().inverted();
    newItemTransform.translate(origin.x(), origin.y());
    newItemTransform.rotate(-item->rotation());
    newItemTransform.scale(1/item->scale(), 1/item->scale());
    newItemTransform.translate(-origin.x(), -origin.y());

    // ### Expensive, we could maybe use dirtySceneTransform bit for optimization

    item->setTransform(newItemTransform);
    item->d_func()->setIsMemberOfGroup(true);
    prepareGeometryChange();
    d->itemsBoundingRect |= itemTransform.mapRect(item->boundingRect() | item->childrenBoundingRect());
    update();
}

void QGraphicsItem::setParentItem(QGraphicsItem *newParent)
{
    if (newParent == this) {
        qWarning("QGraphicsItem::setParentItem: cannot assign %p as a parent of itself", this);
        return;
    }
    if (newParent == d_ptr->parent)
        return;

    const QVariant newParentVariant(itemChange(QGraphicsItem::ItemParentChange,
                                               QVariant::fromValue<QGraphicsItem *>(newParent)));
    newParent = qvariant_cast<QGraphicsItem *>(newParentVariant);
    if (newParent == d_ptr->parent)
        return;

    const QVariant thisPointerVariant(QVariant::fromValue<QGraphicsItem *>(this));
	// setParentItemHelper 内部触发 itemChange() 方法
    d_ptr->setParentItemHelper(newParent, &newParentVariant, &thisPointerVariant);
}

void QGraphicsItemPrivate::setParentItemHelper(QGraphicsItem *newParent, const QVariant *newParentVariant, const QVariant *thisPointerVariant)
{
    Q_Q(QGraphicsItem);
    if (newParent == parent)
        return;
	...
	if (parent) {
        // Remove from current parent
        parent->d_ptr->removeChild(q);
        if (thisPointerVariant)
            parent->itemChange(QGraphicsItem::ItemChildRemovedChange, thisPointerVariant);
    }
	...
	 // Deliver post-change notification
    if (newParentVariant)
        q->itemChange(QGraphicsItem::ItemParentHasChanged, *newParentVariant);

    if (isObject)
        emit static_cast<QGraphicsObject *>(q)->parentChanged();
}

通过源码可以发现,如果在 itemChange() 内部处理 group 的坐标及尺寸,确实会出现很多问题,因为此时 addToGroup() 还未执行 transform 变换。
要解决此问题,就必须等待addToGroup() 执行完成再去计算坐标及尺寸。可以在 itemChange() 发射一个信号,采用异步处理该信号,将处理过程推迟到下一个事件循环。这样就能够完美解决问题。

具体代码可以参考项目 Compelling Data Designer 中 dashboard/BIDesigner/bigraphicsview.cpp 的处理过程。该项目用于数据的可视化设计,软件采用可扩展架构,支持扩展图形插件、数据接口。项目仍在开发中,目前已设计完成基本图形、多属性配置、动画等功能。

相关推荐
小欣加油11 小时前
leetcode1926 迷宫中离入口最近的出口
数据结构·c++·算法·leetcode·职场和发展
Jun62611 小时前
QT(12)-制作lib库
开发语言·qt
星恒随风12 小时前
C++ 类和对象入门(五):初始化列表、explicit 和 static 成员详解
开发语言·c++·笔记·学习·状态模式
浪客灿心12 小时前
项目篇:模块设计与实现
数据库·c++
牛油果子哥q13 小时前
【C++ STL vector】C++ STL vector 终极精讲:动态数组底层原理、两倍扩容机制、迭代器失效、增删查改、性能剖析与工程避坑指南
开发语言·c++
为何创造硅基生物14 小时前
独占指针的创建std::make_unique 本身自带堆出现
c++
kyle~15 小时前
ROS 2 与 Isaac Sim 联合仿真(一)体系架构、环境选型与基础通信闭环
c++·机器人·nvidia·仿真·ros2
努力努力再努力wz15 小时前
【内存管理与高并发内存池系列】从 mmap 到 malloc:文件映射、匿名映射与 glibc 内存分配机制详解
linux·c语言·数据结构·数据库·c++·qt·链表
八解毒剂15 小时前
数据结构-平衡二叉树——对二叉搜索树的优化
数据结构·c++·算法
JdSnE27zv15 小时前
Qt 操作SQLite数据库
数据库·qt·sqlite