当前位置: 首页 > article >正文

Qt QGraphicsView 深度解析:从架构设计到源码内幕

一、QGraphicsView 框架Qt 最强大的 2D 图形引擎QGraphicsView 不是普通的控件它是 Qt 官方定义的Graphics View Framework一套完整的三层架构┌─────────────────────────────────────────────────────────────────────┐ │ QGraphicsView (视图层) │ │ 继承自 QAbstractScrollArea负责渲染、事件分发、视口变换 │ │ 可以有多个 View 指向同一个 Scene │ │ 提供 scale() / rotate() / translate() / setTransform() │ └────────────────────────────┬────────────────────────────────────────┘ │ paint() / event() │ ┌────────────────────────────▼────────────────────────────────────────┐ │ QGraphicsScene (场景层) │ │ 继承自 QObject负责管理所有 Item、事件转发、选中/焦点管理 │ │ 提供 BSP 树加速空间索引O(log n) 查找 │ │ 支持多层渲染Background / Item / Foreground │ └────────────────────────────┬────────────────────────────────────────┘ │ itemAt() / items() │ ┌────────────────────────────▼────────────────────────────────────────┐ │ QGraphicsItem (图元层抽象基类) │ │ 所有可见元素的基类支持嵌套父子关系 │ │ 内置子类QGraphicsRectItem / QGraphicsEllipseItem / │ │ QGraphicsPixmapItem / QGraphicsPathItem / │ │ QGraphicsTextItem / QGraphicsLineItem │ │ 自定义继承 QGraphicsItem重写 paint() boundingRect() │ └─────────────────────────────────────────────────────────────────────┘核心设计原则数据与视图分离一个 Scene 可以绑定多个 View同步显示不同视角事件链转发View → Scene → Item逐层分发可拦截空间索引加速Scene 内部用 BSP 树大规模 Item 下保持流畅坐标系统独立场景坐标逻辑、视图坐标屏幕、项坐标局部二、坐标系统三套坐标三种用途2.1 三种坐标系// 场景坐标Scene Coordinates // - Scene 的逻辑坐标系所有 Item 的统一参考系 // - Item::scenePos() / Item::sceneBoundingRect() 返回此坐标 // - 由 Scene 管理与 View 无关 // 视图坐标View Coordinates // - View 视口的物理像素坐标 // - 鼠标事件 QMouseEvent::pos() 是视图坐标 // - 滚动条偏移、缩放变换都会影响此坐标 // 项坐标Item Coordinates // - Item 的局部坐标系原点在 Item 的 boundingRect 左上角 // - Item::pos() 相对于父 Item 的坐标 // - Item::mapFromScene() / mapToScene() 进行转换2.2 坐标转换方法class QGraphicsView : public QAbstractScrollArea { public: // 视图坐标 → 场景坐标 QPointF mapToScene(const QPoint point) const; QRectF mapToScene(const QRect rect) const; // 场景坐标 → 视图坐标 QPoint mapFromScene(const QPointF point) const; QRect mapFromScene(const QRectF rect) const; // 获取当前变换矩阵 QTransform transform() const; void setTransform(const QTransform matrix, bool combine false); }; class QGraphicsItem { public: // 项坐标 → 场景坐标 QPointF mapToScene(const QPointF point) const; QRectF mapToScene(const QRectF rect) const; // 场景坐标 → 项坐标 QPointF mapFromScene(const QPointF point) const; QRectF mapFromScene(const QRectF rect) const; // 项坐标 → 父项坐标 QPointF mapToParent(const QPointF point) const; // 父项坐标 → 项坐标 QPointF mapFromParent(const QPointF point) const; // 设置项在场景中的位置 void setPos(const QPointF pos); void setPos(qreal x, qreal y); // 获取项在场景中的位置 QPointF pos() const; // 相对于父项 QPointF scenePos() const; // 相对于场景 };2.3 坐标转换源码// qtbase/src/widgets/graphicsview/qgraphicsview.cpp QPointF QGraphicsView::mapToScene(const QPoint point) const { // 获取视口变换矩阵的逆矩阵 QTransform viewTransform viewportTransform(); if (viewTransform.isIdentity()) return QPointF(point); // 应用逆变换视图坐标 → 场景坐标 return viewTransform.inverted().map(QPointF(point)); } QTransform QGraphicsView::viewportTransform() const { Q_D(const QGraphicsView); // 1. 基础变换平移滚动条偏移 QTransform transform; transform.translate(-d-horizontalScrollValue(), -d-horizontalScrollValue()); // 2. 应用缩放和旋转 transform * d-matrix; // 用户设置的 transform // 3. 视口中心偏移alignment if (d-alignment Qt::AlignLeft) ; // 无偏移 else if (d-alignment Qt::AlignRight) transform.translate(viewport()-width(), 0); else // AlignHCenter transform.translate(viewport()-width() / 2, 0); return transform; }三、QGraphicsView 渲染源码解析3.1 paintEvent 入口// qtbase/src/widgets/graphicsview/qgraphicsview.cpp void QGraphicsView::paintEvent(QPaintEvent *event) { Q_D(QGraphicsView); // 创建画家 QPainter painter(viewport()); painter.setRenderHints(d-renderHints); // 保存原始状态 painter.save(); // 1. 应用视口变换滚动、缩放、旋转 painter.setTransform(viewportTransform()); // 2. 设置裁剪区域只渲染视口可见部分 if (!d-clipPath.isEmpty()) painter.setClipPath(d-clipPath); // 3. 渲染场景 // 这会调用 QGraphicsScene::render() d-scene-render(painter, event-rect(), viewport()-rect()); // 4. 恢复状态 painter.restore(); // 5. 渲染前景层可选 if (d-foregroundBrush.style() ! Qt::NoBrush) { painter.setBrushOrigin(0, 0); painter.fillRect(event-rect(), d-foregroundBrush); } }3.2 QGraphicsScene::render 源码// qtbase/src/widgets/graphicsview/qgraphicsscene.cpp void QGraphicsScene::render(QPainter *painter, const QRectF target, const QRectF source, Qt::AspectRatioMode aspectRatioMode) { // 1. 计算源区域默认整个场景 QRectF sourceRect source.isNull() ? sceneRect() : source; if (!sourceRect.isValid()) return; // 2. 计算目标区域默认整个视口 QRectF targetRect target.isNull() ? QRectF(painter-transform().mapRect(QRectF(painter-viewport()))) : target; // 3. 获取可见项核心BSP 树索引 QListQGraphicsItem * items items(sourceRect, Qt::IntersectsItemBoundingRect, Qt::AscendingOrder); // 4. 按渲染层级分组 struct Layer { int level; QListQGraphicsItem * items; }; QListLayer layers; for (QGraphicsItem *item : items) { int level item-d_ptr-layer; // 渲染层级 auto it std::find_if(layers.begin(), layers.end(), [level](const Layer l) { return l.level level; }); if (it layers.end()) { layers.append({level, {item}}); } else { it-items.append(item); } } // 5. 按层级顺序渲染 std::sort(layers.begin(), layers.end(), [](const Layer a, const Layer b) { return a.level b.level; }); for (const Layer layer : layers) { for (QGraphicsItem *item : layer.items) { // 跳过不可见项 if (!item-isVisible()) continue; // 保存画家的状态 painter-save(); // 应用项的变换位置、旋转、缩放 item-d_ptr-paint(painter, option, nullptr); // 恢复状态 painter-restore(); } } }3.3 QGraphicsItem::paint 虚函数// qtbase/src/widgets/graphicsview/qgraphicsitem.cpp void QGraphicsItemPrivate::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) { Q_Q(QGraphicsItem); // 1. 应用项的变换 painter-translate(q-x(), q-y()); if (!transform.isIdentity()) painter-concatTransform(transform); // 2. 应用不透明度 if (opacity 1.0) { painter-setOpacity(painter-opacity() * opacity); } // 3. 可选应用缓动效果如阴影、模糊 if (effect) { effect-draw(painter); return; } // 4. 调用用户实现的 paint() q-paint(painter, option, widget); } // 用户必须重写的虚函数 virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget nullptr) 0;四、BSP 树空间索引加速的核心4.1 为什么需要空间索引当场景中有 10000 个项时QGraphicsScene::items(point)如果线性遍历复杂度是 O(n)每次鼠标移动都要调用会卡死。Qt 的解决方案BSP 树Binary Space Partitioning将场景递归分割查询复杂度降到 O(log n)。4.2 BSP 树结构// qtbase/src/widgets/graphicsview/qgraphicsscene_bsp.cpp class QGraphicsSceneBspTree { public: struct Node { enum Type { Leaf, Horizontal, Vertical }; Type type; int firstChild; int secondChild; QRectF rect; QListQGraphicsItem * items; // 仅叶子节点存储 }; void initialize(const QRectF rect, int depth); void insertItem(QGraphicsItem *item); void removeItem(QGraphicsItem *item); QListQGraphicsItem * items(const QRectF rect) const; QListQGraphicsItem * items(const QPointF point) const; private: QVectorNode m_nodes; int m_depth; // 默认 10 层 QRectF m_rect; };4.3 BSP 树插入与查询源码// qtbase/src/widgets/graphicsview/qgraphicsscene_bsp.cpp void QGraphicsSceneBspTree::insertItem(QGraphicsItem *item) { insertItem(item, 0); } void QGraphicsSceneBspTree::insertItem(QGraphicsItem *item, int index) { Node node m_nodes[index]; if (node.type Node::Leaf) { // 叶子节点直接插入 node.items.append(item); return; } // 分支节点根据项的位置决定进入哪个子树 QRectF itemRect item-sceneBoundingRect(); QRectF firstRect m_nodes[node.firstChild].rect; QRectF secondRect m_nodes[node.secondChild].rect; if (itemRect.left() firstRect.left() itemRect.right() firstRect.right()) { // 完全在第一子树 insertItem(item, node.firstChild); } else if (itemRect.left() secondRect.left() itemRect.right() secondRect.right()) { // 完全在第二子树 insertItem(item, node.secondChild); } else { // 跨越分割线存储在当前节点叶子节点不会走到这里 node.items.append(item); } } QListQGraphicsItem * QGraphicsSceneBspTree::items(const QPointF point) const { QListQGraphicsItem * result; climbTree(result, point, 0); return result; } void QGraphicsSceneBspTree::climbTree(QListQGraphicsItem * result, const QPointF point, int index) const { const Node node m_nodes[index]; // 检查当前节点的项跨越分割线的项 for (QGraphicsItem *item : node.items) { if (item-sceneBoundingRect().contains(point)) result.append(item); } if (node.type Node::Leaf) return; // 递归查询包含该点的子树 if (m_nodes[node.firstChild].rect.contains(point)) climbTree(result, point, node.firstChild); else if (m_nodes[node.secondChild].rect.contains(point)) climbTree(result, point, node.secondChild); }4.4 BSP 树深度配置// 场景初始化时设置 BSP 深度 scene new QGraphicsScene(this); scene-setSceneRect(-5000, -5000, 10000, 10000); scene-setItemIndexMethod(QGraphicsScene::BspTreeIndex); // 默认 scene-setBspTreeDepth(12); // 默认 10可根据项数量调整 // 估算最优深度 int optimalDepth static_castint(std::log2(itemCount) 1); scene-setBspTreeDepth(qBound(1, optimalDepth, 20)); // 如果项都在移动BSP 树频繁重建会有开销 // 可以切换为线性索引适合动态场景 scene-setItemIndexMethod(QGraphicsScene::NoIndex);五、事件处理链View → Scene → Item5.1 鼠标事件分发流程// 1. View 接收原始鼠标事件 void QGraphicsView::mousePressEvent(QMouseEvent *event) { Q_D(QGraphicsView); // 转换为场景坐标 QPointF scenePoint mapToScene(event-pos()); // 调用 Scene 的事件处理器 QGraphicsSceneMouseEvent sceneEvent(QEvent::GraphicsSceneMousePress); sceneEvent.setPos(scenePoint); sceneEvent.setButtonDownScenePos(event-button(), scenePoint); sceneEvent.setButton(event-button()); sceneEvent.setModifiers(event-modifiers()); d-scene-mousePressEvent(sceneEvent); // 如果事件被接受记录鼠标捕获项 if (sceneEvent.isAccepted()) d-mouseGrabberItem sceneEvent.widget(); } // 2. Scene 分发到具体 Item void QGraphicsScene::mousePressEvent(QGraphicsSceneMouseEvent *event) { Q_D(QGraphicsScene); // 使用 BSP 树快速查找鼠标下的项 QGraphicsItem *item itemAt(event-scenePos()); if (item item-isEnabled()) { // 转换为项坐标 QPointF itemPos item-mapFromScene(event-scenePos()); event-setPos(itemPos); // 分发到项 item-mousePressEvent(event); // 如果项接受了事件设置鼠标捕获 if (event-isAccepted()) { d-mouseGrabberItem item; return; } } // 没有项接受事件Scene 自己处理如框选 d-mouseGrabberItem nullptr; event-accept(); } // 3. Item 处理事件 void QGraphicsItem::mousePressEvent(QGraphicsSceneMouseEvent *event) { if (!isSelectable()) { event-ignore(); return; } // 默认行为选中 setSelected(true); event-accept(); }5.2 事件传播链中断// 自定义 Item拖拽移动 class DraggableItem : public QGraphicsRectItem { public: DraggableItem(const QRectF rect) : QGraphicsRectItem(rect) { setFlag(QGraphicsItem::ItemIsMovable, true); setFlag(QGraphicsItem::ItemSendsGeometryChanges, true); setFlag(QGraphicsItem::ItemIsSelectable, true); } protected: // 不需要重写 mousePressEvent基类已经处理了选中 // 重写鼠标释放可添加吸附逻辑 void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override { // 吸附到网格 QPointF pos scenePos(); qreal gridSize 20.0; qreal x qRound(pos.x() / gridSize) * gridSize; qreal y qRound(pos.y() / gridSize) * gridSize; setPos(x, y); // 继续默认行为 QGraphicsRectItem::mouseReleaseEvent(event); } // 拦截变化通知限制移动范围 QVariant itemChange(GraphicsItemChange change, const QVariant value) override { if (change ItemPositionChange scene()) { QPointF newPos value.toPointF(); QRectF rect scene()-sceneRect(); if (!rect.contains(newPos)) { // 限制在场景范围内 newPos.setX(qBound(rect.left(), newPos.x(), rect.right())); newPos.setY(qBound(rect.top(), newPos.y(), rect.bottom())); return newPos; } } return QGraphicsRectItem::itemChange(change, value); } };六、自定义 QGraphicsItem从入门到精通6.1 必须实现的两个虚函数class MyItem : public QGraphicsItem { public: explicit MyItem(QGraphicsItem *parent nullptr) : QGraphicsItem(parent), m_size(100, 100) { // 启用悬停事件 setAcceptHoverEvents(true); // 启用缓存模式性能优化 setCacheMode(DeviceCoordinateCache); } // 1. 返回项的边界矩形必须实现 QRectF boundingRect() const override { // 边界矩形要稍大一点包含绘制笔刷的宽度 qreal penWidth 2.0; return QRectF(0, 0, m_size.width() penWidth, m_size.height() penWidth); } // 2. 绘制项必须实现 void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget nullptr) override { Q_UNUSED(widget); // 提取悬停状态 bool hovered option-state QStyle::State_MouseOver; // 绘制背景 painter-setPen(QPen(Qt::black, 2)); painter-setBrush(hovered ? QColor(100, 150, 255) : QColor(70, 130, 230)); painter-drawRoundedRect(boundingRect().adjusted(1, 1, -1, -1), 5, 5); // 绘制文字 painter-setFont(QFont(Arial, 10)); painter-drawText(boundingRect(), Qt::AlignCenter, m_text); } // 3. 可选精确的形状用于碰撞检测 QPainterPath shape() const override { QPainterPath path; path.addRoundedRect(boundingRect().adjusted(1, 1, -1, -1), 5, 5); return path; } void setText(const QString text) { if (m_text ! text) { prepareGeometryChange(); // 重要通知 Scene 边界可能变化 m_text text; update(); // 触发重绘 } } private: QSizeF m_size; QString m_text; };6.2 带连接线的图元节点// 节点项 class NodeItem : public QGraphicsItem { public: explicit NodeItem(const QString name, QGraphicsItem *parent nullptr) : QGraphicsItem(parent), m_name(name) { setFlag(ItemIsMovable, true); setFlag(ItemSendsGeometryChanges, true); setFlag(ItemIsSelectable, true); setCacheMode(DeviceCoordinateCache); } QRectF boundingRect() const override { return QRectF(-50, -25, 100, 50); } void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override { Q_UNUSED(widget); // 选中高亮 if (option-state QStyle::State_Selected) { painter-setPen(QPen(Qt::blue, 2)); painter-setBrush(QColor(200, 220, 255)); } else { painter-setPen(QPen(Qt::black, 1)); painter-setBrush(Qt::white); } painter-drawRoundedRect(boundingRect(), 8, 8); painter-drawText(boundingRect(), Qt::AlignCenter, m_name); } QVariant itemChange(GraphicsItemChange change, const QVariant value) override { // 移动时通知连接线更新 if (change ItemPositionHasChanged) { for (EdgeItem *edge : m_edges) edge-adjust(); } return QGraphicsItem::itemChange(change, value); } void addEdge(EdgeItem *edge) { m_edges.append(edge); } QPointF connectionPoint() const { return pos(); } private: QString m_name; QListEdgeItem * m_edges; }; // 连接线项 class EdgeItem : public QGraphicsItem { public: EdgeItem(NodeItem *source, NodeItem *dest, QGraphicsItem *parent nullptr) : QGraphicsItem(parent), m_source(source), m_dest(dest) { setFlag(ItemIsSelectable, true); setZValue(-1); // 连接线在节点下方 m_source-addEdge(this); m_dest-addEdge(this); adjust(); } void adjust() { prepareGeometryChange(); m_sourcePoint m_source-connectionPoint(); m_destPoint m_dest-connectionPoint(); // 计算控制点贝塞尔曲线 m_path QPainterPath(); m_path.moveTo(m_sourcePoint); QPointF ctrl1, ctrl2; qreal dx m_destPoint.x() - m_sourcePoint.x(); if (qAbs(dx) 100) { ctrl1 m_sourcePoint QPointF(dx * 0.3, 0); ctrl2 m_destPoint - QPointF(dx * 0.3, 0); m_path.cubicTo(ctrl1, ctrl2, m_destPoint); } else { m_path.lineTo(m_destPoint); } update(); } QRectF boundingRect() const override { qreal penWidth 2; return m_path.boundingRect().adjusted(-penWidth, -penWidth, penWidth, penWidth); } void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override { Q_UNUSED(option); Q_UNUSED(widget); QPen pen(Qt::black, 2); if (isSelected()) pen.setColor(Qt::blue); painter-setPen(pen); painter-setBrush(Qt::NoBrush); painter-drawPath(m_path); // 绘制箭头 qreal arrowSize 10; QPointF end m_destPoint; QLineF line(m_sourcePoint, m_destPoint); qreal angle std::atan2(line.dy(), line.dx()); QPointF arrowP1 end - QPointF(std::cos(angle - M_PI / 3) * arrowSize, std::sin(angle - M_PI / 3) * arrowSize); QPointF arrowP2 end - QPointF(std::cos(angle M_PI / 3) * arrowSize, std::sin(angle M_PI / 3) * arrowSize); painter-setBrush(Qt::black); painter-drawPolygon(QPolygonF() end arrowP1 arrowP2); } private: NodeItem *m_source; NodeItem *m_dest; QPointF m_sourcePoint; QPointF m_destPoint; QPainterPath m_path; };七、缓存模式性能优化的关键7.1 三种缓存模式enum CacheMode { NoCache, // 无缓存每次 paint() 都重绘 ItemCoordinateCache, // 在项坐标系下缓存适合缩放 DeviceCoordinateCache // 在设备坐标系下缓存适合静态项 }; // 使用 item-setCacheMode(QGraphicsItem::DeviceCoordinateCache);7.2 缓存模式源码// qtbase/src/widgets/graphicsview/qgraphicsitem.cpp void QGraphicsItemPrivate::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) { Q_Q(QGraphicsItem); // 检查缓存模式 if (cacheMode ! NoCache) { // 尝试从缓存获取 QPixmap cachedPixmap; QPoint cacheKey calculateCacheKey(); if (QPixmapCache::find(cacheKey, cachedPixmap)) { // 缓存命中直接绘制 painter-drawPixmap(0, 0, cachedPixmap); return; } // 缓存未命中绘制到缓存 cachedPixmap QPixmap(boundingRect.size().toSize()); cachedPixmap.fill(Qt::transparent); QPainter cachePainter(cachedPixmap); q-paint(cachePainter, option, widget); QPixmapCache::insert(cacheKey, cachedPixmap); painter-drawPixmap(0, 0, cachedPixmap); return; } // 无缓存直接绘制 q-paint(painter, option, widget); }缓存选择原则模式适用场景缓存失效条件NoCache频繁变化、动态绘制无ItemCoordinateCache会旋转/缩放但形状不变update()调用、几何变化DeviceCoordinateCache完全静态如背景图视口缩放、update()八、性能优化清单8.1 场景优化// 1. 限制场景大小避免 BSP 树过大 scene-setSceneRect(-5000, -5000, 10000, 10000); // 2. 动态调整 BSP 深度 int itemCount scene-items().size(); int depth static_castint(std::log2(itemCount) 1); scene-setBspTreeDepth(qBound(1, depth, 20)); // 3. 大量移动项时禁用索引 scene-setItemIndexMethod(QGraphicsScene::NoIndex); // 动画结束后恢复 scene-setItemIndexMethod(QGraphicsScene::BspTreeIndex); // 4. 批量添加项减少 BSP 重建次数 scene-blockSignals(true); for (int i 0; i 10000; i) { scene-addItem(new MyItem()); } scene-blockSignals(false);8.2 视图优化// 1. 启用视口更新模式 view-setViewportUpdateMode(QGraphicsView::MinimalViewportUpdate); // 2. 使用 OpenGL 渲染大幅提升性能 QOpenGLWidget *glWidget new QOpenGLWidget; view-setViewport(glWidget); // 3. 关闭抗锯齿如果不需要 view-setRenderHints(QPainter::Antialiasing, false); // 4. 限制渲染区域 view-setOptimizationFlag(QGraphicsView::DontAdjustForAntialiasing, true); view-setOptimizationFlag(QGraphicsView::DontSavePainterState, true);8.3 项优化// 1. 设置合理的缓存模式 item-setCacheMode(QGraphicsItem::DeviceCoordinateCache); // 2. 精确的边界矩形避免不必要的重绘 QRectF boundingRect() const override { return QRectF(0, 0, 100, 100); // 精确值 } // 3. 使用 QPainterPath::setFillRule(Qt::WindingFill) // 减少自相交判断开销 // 4. 分层渲染静态项与动态项分开 staticLayer-setZValue(0); dynamicLayer-setZValue(10); // 5. 减少信号连接特别是 itemChange // 在批量操作时临时禁用信号 item-blockSignals(true); // ... 批量操作 item-blockSignals(false);九、常见陷阱与解决方案陷阱 1boundingRect 太小导致裁剪// 错误边界不包含笔刷宽度 QRectF boundingRect() const override { return QRectF(0, 0, 100, 100); } void paint(...) { painter-setPen(QPen(Qt::black, 5)); // 笔宽 5 painter-drawRect(0, 0, 100, 100); // 右下角被裁剪 } // 正确扩展边界 QRectF boundingRect() const override { qreal penWidth 5; return QRectF(-penWidth/2, -penWidth/2, 100 penWidth, 100 penWidth); }陷阱 2忘记 prepareGeometryChange// 错误直接改变几何属性 void setSize(const QSizeF size) { m_size size; // 没有 prepareGeometryChange update(); // 只触发重绘不更新 BSP 树 } // 正确先通知再更新 void setSize(const QSizeF size) { prepareGeometryChange(); // 必须通知 Scene 边界变化 m_size size; update(); }陷阱 3在 paint 中修改项的状态// 错误paint 中修改状态 void paint(...) { if (m_needsUpdate) { m_data recalculate(); // ❌ paint 中不应有副作用 m_needsUpdate false; } // ... 绘制 } // 正确状态更新在 paint 外部 void updateData() { m_data recalculate(); m_needsUpdate false; update(); // 触发重绘 }陷阱 4内存泄漏忘记设置父项或手动删除// 错误创建项但未设置父项或添加到场景 QGraphicsRectItem *item new QGraphicsRectItem(rect); // 无父项无人管理 // 正确设置父项或添加到场景 QGraphicsRectItem *item new QGraphicsRectItem(rect, parentItem); // 或 scene-addItem(item); // 场景会管理项的生命周期 // 正确手动管理 QGraphicsRectItem *item new QGraphicsRectItem(rect); item-setParentItem(parentItem); // 父项销毁时自动删除子项十、实战案例简易流程图编辑器// flowchart.h #include QGraphicsView #include QGraphicsScene #include QGraphicsItem #include QMenu class FlowChartScene : public QGraphicsScene { Q_OBJECT public: explicit FlowChartScene(QObject *parent nullptr); protected: void contextMenuEvent(QGraphicsSceneContextMenuEvent *event) override; private slots: void addProcessNode(); void addDecisionNode(); void addTerminalNode(); private: QPointF m_contextMenuPos; }; class FlowChartView : public QGraphicsView { Q_OBJECT public: explicit FlowChartView(QWidget *parent nullptr); protected: void wheelEvent(QWheelEvent *event) override; void mousePressEvent(QMouseEvent *event) override; void mouseMoveEvent(QMouseEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override; private: bool m_panning false; QPoint m_lastPos; qreal m_zoomFactor 1.0; }; // flowchart.cpp FlowChartScene::FlowChartScene(QObject *parent) : QGraphicsScene(parent) { setSceneRect(-2000, -2000, 4000, 4000); setItemIndexMethod(QGraphicsScene::BspTreeIndex); } void FlowChartScene::contextMenuEvent(QGraphicsSceneContextMenuEvent *event) { m_contextMenuPos event-scenePos(); QMenu menu; menu.addAction(添加处理节点, this, FlowChartScene::addProcessNode); menu.addAction(添加判断节点, this, FlowChartScene::addDecisionNode); menu.addAction(添加终止节点, this, FlowChartScene::addTerminalNode); menu.exec(event-screenPos()); } void FlowChartScene::addProcessNode() { ProcessNodeItem *node new ProcessNodeItem(); node-setPos(m_contextMenuPos); addItem(node); } FlowChartView::FlowChartView(QWidget *parent) : QGraphicsView(parent) { setRenderHint(QPainter::Antialiasing); setDragMode(QGraphicsView::RubberBandDrag); setOptimizationFlag(DontSavePainterState); setViewportUpdateMode(MinimalViewportUpdate); setTransformationAnchor(AnchorUnderMouse); } void FlowChartView::wheelEvent(QWheelEvent *event) { qreal factor 1.15; if (event-angleDelta().y() 0) scale(factor, factor); else scale(1.0 / factor, 1.0 / factor); m_zoomFactor transform().m11(); } void FlowChartView::mousePressEvent(QMouseEvent *event) { if (event-button() Qt::MiddleButton) { m_panning true; m_lastPos event-pos(); setCursor(Qt::ClosedHandCursor); event-accept(); return; } QGraphicsView::mousePressEvent(event); } void FlowChartView::mouseMoveEvent(QMouseEvent *event) { if (m_panning) { QScrollBar *hBar horizontalScrollBar(); QScrollBar *vBar verticalScrollBar(); QPoint delta event-pos() - m_lastPos; hBar-setValue(hBar-value() - delta.x()); vBar-setValue(vBar-value() - delta.y()); m_lastPos event-pos(); event-accept(); return; } QGraphicsView::mouseMoveEvent(event); } void FlowChartView::mouseReleaseEvent(QMouseEvent *event) { if (event-button() Qt::MiddleButton) { m_panning false; setCursor(Qt::ArrowCursor); event-accept(); return; } QGraphicsView::mouseReleaseEvent(event); } // 使用 int main(int argc, char *argv[]) { QApplication app(argc, argv); FlowChartScene *scene new FlowChartScene(); FlowChartView *view new FlowChartView(); view-setScene(scene); view-resize(800, 600); view-show(); return app.exec(); }十一、源码阅读路线图qtbase/src/widgets/graphicsview/ ├── qgraphicsview.h / .cpp ← 视图层渲染、事件入口、变换 ├── qgraphicsscene.h / .cpp ← 场景层项管理、BSP 树、事件分发 ├── qgraphicsitem.h / .cpp ← 项基类几何、绘制、事件处理 ├── qgraphicsscene_bsp.cpp ← BSP 树实现关键 ├── qgraphicswidget.cpp ← 支持布局的项用于复杂 UI └── qgraphicsproxywidget.cpp ← 嵌入 QWidget 的代理 qtbase/src/gui/painting/ └── qtransform.h / .cpp ← 变换矩阵实现关键断点// 1. QGraphicsView::paintEvent ← 渲染入口 // 2. QGraphicsScene::render ← BSP 树查询可见项 // 3. QGraphicsSceneBspTree::items ← 空间索引核心 // 4. QGraphicsItemPrivate::paint ← 缓存模式分支 // 5. QGraphicsScene::mousePressEvent ← 事件分发链 // 6. QGraphicsItem::itemChange ← 几何变化通知结语QGraphicsView 框架的设计精髓是三层解耦 空间索引 事件转发。View负责怎么画视口变换、渲染设置、用户交互Scene负责画什么项管理、空间索引、事件分发Item负责画得怎样自定义绘制、几何计算、事件响应理解了 BSP 树、坐标转换、事件链、缓存模式QGraphicsView 就不再是黑盒——每一个items(point)调用、每一次setPos()更新、每一场paint()绘制都是有迹可循的设计决策。注若有发现问题欢迎大家提出来纠正

相关文章:

Qt QGraphicsView 深度解析:从架构设计到源码内幕

一、QGraphicsView 框架:Qt 最强大的 2D 图形引擎QGraphicsView 不是普通的控件,它是 Qt 官方定义的 Graphics View Framework,一套完整的三层架构:┌─────────────────────────────────────…...

别再乱删频道了!Conda报‘invalid character(s)’错误的深层原因与一劳永逸的修复指南

Conda版本字符串报错全解析:从根因诊断到永久修复方案 当你在终端输入conda create -n myenv python3.8时,突然跳出的Malformed version string ~: invalid character(s)错误提示,往往让人措手不及。这个看似简单的报错背后,隐藏…...

Pixel Language Portal 赋能网站开发:从需求到前端静态页面代码自动生成

Pixel Language Portal 赋能网站开发:从需求到前端静态页面代码自动生成 1. 效果惊艳的开场 想象一下这样的场景:你刚和客户开完需求会议,手上只有一份简单的网站描述文档。传统开发流程下,前端工程师需要至少1-2天才能完成静态…...

AI 全域营销技术体系迎来全新迭代 重构数智时代企业增长主要

多智能体协同技术实现全链路突破 开启企业营销数智化转型新纪元随着生成式人工智能技术的深度产业化落地,全球商业生态的数字化进程迎来了根本性变革。用户注意力的全域分散、信息获取渠道的碎片化、消费决策链路的全场景延伸,使得传统营销模式面临渠道割…...

Python 函数进阶:参数、装饰器、匿名函数全精讲

阅读指南:本文专为 Python 初中级工程师打造,从参数底层规则到装饰器高阶实战,再到 lambda 高效场景,全程代码可直接复制运行,覆盖 90% 面试高频考点与工程最佳实践,读完即可独立封装通用装饰器、写出优雅高…...

LPC数字保存快速指南,精准破局数字保存难题

​​关注我们 - 数字罗塞塔计划 -01数字保存快速指南介绍在数字时代,图书馆出版商面临着技术快速迭代与用户需求不断变化带来的数字内容保存难题。2025年9月15日,图书馆出版联盟(Library Publishing Coalition,LPC)的保…...

Python 核心数据结构实战全攻略:列表 / 字典 / 元组 / 集合从入门到精通

前言在 Python 编程中,列表(list)、字典(dict)、元组(tuple)、集合(set) 是最核心、最常用的四大内置数据结构,是所有 Python 开发者必须熟练掌握的基础核心能…...

两台 H.323 终端点对点直连通信完整步骤

下面给你最精简、最标准、可用于考试/开发/调试的: 两台 H.323 终端点对点直连通信完整步骤 无网守(Gatekeeper)、纯终端对终端,一步不落。一、前提条件 终端A:主叫(比如 192.168.1.10)终端B&am…...

LAYONTHEGROUND栈

一、什么是requests? requests 是一个用于发送HTTP请求的 Python 库。 它可以帮助你: 轻松发送GET、POST、PUT、DELETE等请求 处理Cookie、会话等复杂性 自动解压缩内容 处理国际化域名和URL 二、应用场景 requests 广泛应用于以下实际场景: …...

我不是在用 AI 助手,我在把自己的能力沉淀成组织资产衫

1. 什么是 Apache SeaTunnel? Apache SeaTunnel 是一个非常易于使用、高性能、支持实时流式和离线批处理的海量数据集成平台。它的目标是解决常见的数据集成问题,如数据源多样性、同步场景复杂性以及资源消耗高的问题。 核心特性 丰富的数据源支持&#…...

实测PyTorch-2.x-Universal-Dev-v1.0:开箱即用,GPU验证到Jupyter启动全流程

实测PyTorch-2.x-Universal-Dev-v1.0:开箱即用,GPU验证到Jupyter启动全流程 1. 引言:为什么选择这个镜像 深度学习开发环境配置一直是让开发者头疼的问题。从CUDA驱动安装到各种Python库的版本兼容性,每一步都可能遇到意想不到的…...

Qwen2.5-72B-Instruct-GPTQ-Int4多场景:医疗问诊记录结构化+术语标准化

Qwen2.5-72B-Instruct-GPTQ-Int4多场景:医疗问诊记录结构化术语标准化 1. 模型简介与核心能力 1.1 Qwen2.5系列模型概述 Qwen2.5是通义千问大模型系列的最新版本,提供了从0.5B到720B参数规模的基础模型和指令调优模型。相比前代Qwen2,Qwen…...

SensitivityMatcher:终极游戏鼠标灵敏度精准转换指南

SensitivityMatcher:终极游戏鼠标灵敏度精准转换指南 【免费下载链接】SensitivityMatcher Script that can be used to convert your mouse sensitivity between different 3D games. 项目地址: https://gitcode.com/gh_mirrors/se/SensitivityMatcher 想要…...

WHAT - Shell 工具 warp 介绍(融合 AI)

文章目录Warp Terminal 是什么和传统 Terminal 最大区别可以“用人话操作终端”输入体验像代码编辑器Block(块)概念(非常关键)AI Agent团队协作(Warp Drive)UI 和体验完全现代化和 iTerm2 / Terminal 的本…...

Markdown Viewer浏览器扩展:终极Markdown预览解决方案

Markdown Viewer浏览器扩展:终极Markdown预览解决方案 【免费下载链接】markdown-viewer Markdown Viewer / Browser Extension 项目地址: https://gitcode.com/gh_mirrors/ma/markdown-viewer 还在为浏览器中无法直接预览Markdown文件而烦恼吗?M…...

RVC多语言支持实测:中文/日文/韩文/英文语音转换效果横向对比

RVC多语言支持实测:中文/日文/韩文/英文语音转换效果横向对比 1. 引言:当AI学会“说”多国语言 想象一下,你手头有一段自己的中文录音,但你需要一段日文配音的视频,或者一段韩文的产品介绍。传统方法要么找专业配音&…...

我的OpenClaw使用体验:从怀疑到依赖的“数字员工”

最初接触OpenClaw时,我和许多人一样,抱着怀疑的态度。一个开源项目,真的能成为我口中那个“能干活”的AI助手吗?然而,经过几个月的深度使用,它已经从一个新奇的玩具,变成了我工作流中不可或缺的…...

LLM安全对齐工程白皮书(工业级落地版):覆盖92%企业场景的12项强制校验清单

第一章:LLM安全对齐工程化的核心范式与工业落地挑战 2026奇点智能技术大会(https://ml-summit.org) 大型语言模型的安全对齐已从实验室研究阶段迈入规模化工程实践的关键转折点。当前主流工业场景中,对齐不再仅依赖RLHF单点优化,而是演进为覆…...

大厂 HR 直言:IT 简历里最加分的 3 个项目类型,别乱写

每年金三银四、秋招旺季,我作为大厂HR,每天要刷几百份IT简历,平均每份停留不超过10秒。很多程序员明明技术不错,却因为项目写得乱七八糟,直接被ATS系统筛掉,连面试机会都没有。重点说一句:IT简历…...

GPU 租用:智星云抢占式实例的极致省钱攻略

按小时计费怎么省?GPU 租用竞价策略与抢占式实例实操——以智星云为例,解锁高性价比算力开篇:算力焦虑的最佳解药大模型时代的科研与开发,往往是一场“算力”的比拼。对于个人开发者、学生群体乃至初创团队来说,动辄数…...

PHP代码加密:2026年开发者必须面对的“最后一道防线“

开篇:一个真实的故事 2024年11月,一位做电商SaaS的朋友找到我,语气当中带着掩饰不住的沮丧。他的核心定价算法,也就是团队花了两年时间打磨出来的东西,被客户的技术团队完整地复制了。没有反编译,也没有逆向…...

3步从零到精通:Krita AI Diffusion插件模型加载全流程指南

3步从零到精通:Krita AI Diffusion插件模型加载全流程指南 【免费下载链接】krita-ai-diffusion Streamlined interface for generating images with AI in Krita. Inpaint and outpaint with optional text prompt, no tweaking required. 项目地址: https://git…...

SEATA分布式事务——AT模式撂

简介 AI Agent 不仅仅是一个能聊天的机器人(如普通的 ChatGPT),而是一个能够感知环境、进行推理、自主决策并调用工具来完成特定任务的智能系统,更够完成更为复杂的AI场景需求。 AI Agent 功能 根据查阅的资料,agent的…...

SkillLite 多入口架构实战:CLI / Python SDK / MCP / Desktop / Swarm 一页理清

摘要 SkillLite 是轻量级 AI Agent Skills 执行引擎:同一套 Rust workspace 拆分多 crate,向上提供「开箱即用的 Agent 产品」与「可嵌入的安全执行内核」。集成方既可通过终端 CLI 与 MCP 接入 IDE,也可在 Python 中调用 scan_code、execut…...

做质检员其实太容易了|云质QMS为您揭秘

质检员入门全流程手册一、质检员的基本业务知识1. 质检员的岗位使命严格执行质量检验标准,对各类生产原料、成品、辅料的质量检验,监控生产工艺的运行情况,对发现的问题及时上报,不断提升产品质量,维护企业质量信誉。2…...

Prompt工程已进入“微秒级响应”时代:奇点大会实测数据显示——提示结构优化带来3.7倍推理吞吐提升

第一章:Prompt工程已进入“微秒级响应”时代:奇点大会实测数据总览 2026奇点智能技术大会(https://ml-summit.org) 在2026奇点智能技术大会上,全球首个面向生产环境的Prompt编译器——PromptLLVM v0.9正式发布,并同步公开其端到…...

5分钟掌握CAD_Sketcher:Blender中实现精确参数化设计的终极指南

5分钟掌握CAD_Sketcher:Blender中实现精确参数化设计的终极指南 【免费下载链接】CAD_Sketcher Constraint-based geometry sketcher for blender 项目地址: https://gitcode.com/gh_mirrors/ca/CAD_Sketcher CAD_Sketcher是Blender中基于约束的几何草图工具…...

告别无效流量!亚马逊关键词挖掘:新手 7 天精准获客不浪费

亚马逊日常运营,关键词选不对,广告全白费:​花大价钱投热门大词,点击多、转化少,ACoS 居高不下;​自己想的关键词没人搜,广告预算花不出去,零曝光零订单;​只盯着 10 几个…...

S32K3XX时钟树实战:从EB配置到外设时钟精准分配

1. S32K3XX时钟树基础:从晶振到外设的时钟旅程 第一次接触S32K3XX系列芯片时,我被它的时钟系统搞得晕头转向。直到把整个时钟链路比作城市供水系统才豁然开朗——晶振就像水源,PLL是增压泵,而七大时钟则是通往不同区域的主管道。这…...

博客建站选购香港云主机要注意哪些

博客建站选购香港云主机要注意哪些?很多人一上来就看价格,哪个便宜买哪个。这是个误区。选配置之前,先问自己三个问题:我的博客是什么类型?纯文字博客、图片站,还是会有视频?预计每天有多少访问量?我的技术能力如何?能自己折…...