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

news2026/4/11 21:54:18
一、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()绘制都是有迹可循的设计决策。注若有发现问题欢迎大家提出来纠正

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2507514.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SpringBoot-17-MyBatis动态SQL标签之常用标签

文章目录 1 代码1.1 实体User.java1.2 接口UserMapper.java1.3 映射UserMapper.xml1.3.1 标签if1.3.2 标签if和where1.3.3 标签choose和when和otherwise1.4 UserController.java2 常用动态SQL标签2.1 标签set2.1.1 UserMapper.java2.1.2 UserMapper.xml2.1.3 UserController.ja…

wordpress后台更新后 前端没变化的解决方法

使用siteground主机的wordpress网站,会出现更新了网站内容和修改了php模板文件、js文件、css文件、图片文件后,网站没有变化的情况。 不熟悉siteground主机的新手,遇到这个问题,就很抓狂,明明是哪都没操作错误&#x…

网络编程(Modbus进阶)

思维导图 Modbus RTU(先学一点理论) 概念 Modbus RTU 是工业自动化领域 最广泛应用的串行通信协议,由 Modicon 公司(现施耐德电气)于 1979 年推出。它以 高效率、强健性、易实现的特点成为工业控制系统的通信标准。 包…

UE5 学习系列(二)用户操作界面及介绍

这篇博客是 UE5 学习系列博客的第二篇,在第一篇的基础上展开这篇内容。博客参考的 B 站视频资料和第一篇的链接如下: 【Note】:如果你已经完成安装等操作,可以只执行第一篇博客中 2. 新建一个空白游戏项目 章节操作,重…

IDEA运行Tomcat出现乱码问题解决汇总

最近正值期末周,有很多同学在写期末Java web作业时,运行tomcat出现乱码问题,经过多次解决与研究,我做了如下整理: 原因: IDEA本身编码与tomcat的编码与Windows编码不同导致,Windows 系统控制台…

利用最小二乘法找圆心和半径

#include <iostream> #include <vector> #include <cmath> #include <Eigen/Dense> // 需安装Eigen库用于矩阵运算 // 定义点结构 struct Point { double x, y; Point(double x_, double y_) : x(x_), y(y_) {} }; // 最小二乘法求圆心和半径 …

使用docker在3台服务器上搭建基于redis 6.x的一主两从三台均是哨兵模式

一、环境及版本说明 如果服务器已经安装了docker,则忽略此步骤,如果没有安装,则可以按照一下方式安装: 1. 在线安装(有互联网环境): 请看我这篇文章 传送阵>> 点我查看 2. 离线安装(内网环境):请看我这篇文章 传送阵>> 点我查看 说明&#xff1a;假设每台服务器已…

XML Group端口详解

在XML数据映射过程中&#xff0c;经常需要对数据进行分组聚合操作。例如&#xff0c;当处理包含多个物料明细的XML文件时&#xff0c;可能需要将相同物料号的明细归为一组&#xff0c;或对相同物料号的数量进行求和计算。传统实现方式通常需要编写脚本代码&#xff0c;增加了开…

LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器的上位机配置操作说明

LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器专为工业环境精心打造&#xff0c;完美适配AGV和无人叉车。同时&#xff0c;集成以太网与语音合成技术&#xff0c;为各类高级系统&#xff08;如MES、调度系统、库位管理、立库等&#xff09;提供高效便捷的语音交互体验。 L…

(LeetCode 每日一题) 3442. 奇偶频次间的最大差值 I (哈希、字符串)

题目&#xff1a;3442. 奇偶频次间的最大差值 I 思路 &#xff1a;哈希&#xff0c;时间复杂度0(n)。 用哈希表来记录每个字符串中字符的分布情况&#xff0c;哈希表这里用数组即可实现。 C版本&#xff1a; class Solution { public:int maxDifference(string s) {int a[26]…

【大模型RAG】拍照搜题技术架构速览:三层管道、两级检索、兜底大模型

摘要 拍照搜题系统采用“三层管道&#xff08;多模态 OCR → 语义检索 → 答案渲染&#xff09;、两级检索&#xff08;倒排 BM25 向量 HNSW&#xff09;并以大语言模型兜底”的整体框架&#xff1a; 多模态 OCR 层 将题目图片经过超分、去噪、倾斜校正后&#xff0c;分别用…

【Axure高保真原型】引导弹窗

今天和大家中分享引导弹窗的原型模板&#xff0c;载入页面后&#xff0c;会显示引导弹窗&#xff0c;适用于引导用户使用页面&#xff0c;点击完成后&#xff0c;会显示下一个引导弹窗&#xff0c;直至最后一个引导弹窗完成后进入首页。具体效果可以点击下方视频观看或打开下方…

接口测试中缓存处理策略

在接口测试中&#xff0c;缓存处理策略是一个关键环节&#xff0c;直接影响测试结果的准确性和可靠性。合理的缓存处理策略能够确保测试环境的一致性&#xff0c;避免因缓存数据导致的测试偏差。以下是接口测试中常见的缓存处理策略及其详细说明&#xff1a; 一、缓存处理的核…

龙虎榜——20250610

上证指数放量收阴线&#xff0c;个股多数下跌&#xff0c;盘中受消息影响大幅波动。 深证指数放量收阴线形成顶分型&#xff0c;指数短线有调整的需求&#xff0c;大概需要一两天。 2025年6月10日龙虎榜行业方向分析 1. 金融科技 代表标的&#xff1a;御银股份、雄帝科技 驱动…

观成科技:隐蔽隧道工具Ligolo-ng加密流量分析

1.工具介绍 Ligolo-ng是一款由go编写的高效隧道工具&#xff0c;该工具基于TUN接口实现其功能&#xff0c;利用反向TCP/TLS连接建立一条隐蔽的通信信道&#xff0c;支持使用Let’s Encrypt自动生成证书。Ligolo-ng的通信隐蔽性体现在其支持多种连接方式&#xff0c;适应复杂网…

铭豹扩展坞 USB转网口 突然无法识别解决方法

当 USB 转网口扩展坞在一台笔记本上无法识别,但在其他电脑上正常工作时,问题通常出在笔记本自身或其与扩展坞的兼容性上。以下是系统化的定位思路和排查步骤,帮助你快速找到故障原因: 背景: 一个M-pard(铭豹)扩展坞的网卡突然无法识别了,扩展出来的三个USB接口正常。…

未来机器人的大脑:如何用神经网络模拟器实现更智能的决策?

编辑&#xff1a;陈萍萍的公主一点人工一点智能 未来机器人的大脑&#xff1a;如何用神经网络模拟器实现更智能的决策&#xff1f;RWM通过双自回归机制有效解决了复合误差、部分可观测性和随机动力学等关键挑战&#xff0c;在不依赖领域特定归纳偏见的条件下实现了卓越的预测准…

Linux应用开发之网络套接字编程(实例篇)

服务端与客户端单连接 服务端代码 #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #include <pthread.h> …

华为云AI开发平台ModelArts

华为云ModelArts&#xff1a;重塑AI开发流程的“智能引擎”与“创新加速器”&#xff01; 在人工智能浪潮席卷全球的2025年&#xff0c;企业拥抱AI的意愿空前高涨&#xff0c;但技术门槛高、流程复杂、资源投入巨大的现实&#xff0c;却让许多创新构想止步于实验室。数据科学家…

深度学习在微纳光子学中的应用

深度学习在微纳光子学中的主要应用方向 深度学习与微纳光子学的结合主要集中在以下几个方向&#xff1a; 逆向设计 通过神经网络快速预测微纳结构的光学响应&#xff0c;替代传统耗时的数值模拟方法。例如设计超表面、光子晶体等结构。 特征提取与优化 从复杂的光学数据中自…