别再只会用QGraphicsView显示图片了!手把手教你用它做个简易的流程图编辑器(Qt 6.5实战)
用QGraphicsView构建流程图编辑器的实战指南在Qt开发中QGraphicsView常被简单地用作图片或图形的显示工具这其实大大低估了它的潜力。本文将带你深入探索如何将这个强大的框架转化为一个功能完备的流程图编辑器实现节点拖拽、连线绘制、框选操作等核心功能。1. 基础架构搭建首先需要理解流程图编辑器的基本组成元素。一个典型的流程图由节点矩形、菱形等和连接线组成这些都需要继承自QGraphicsItem类。class FlowChartNode : public QGraphicsRectItem { public: FlowChartNode(QGraphicsItem *parent nullptr); void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; private: QString m_text; QListFlowChartEdge* m_edges; }; class FlowChartEdge : public QGraphicsPathItem { public: FlowChartEdge(FlowChartNode *source, FlowChartNode *dest, QGraphicsItem *parent nullptr); void updatePath(); private: FlowChartNode *m_source; FlowChartNode *m_dest; };关键点说明每个节点需要维护与其相连的边列表边需要在节点移动时实时更新路径自定义绘制方法实现不同节点类型的视觉效果2. 交互功能实现2.1 节点拖拽与位置同步实现节点的拖拽功能需要处理鼠标事件void FlowChartNode::mousePressEvent(QGraphicsSceneMouseEvent *event) { if (event-button() Qt::LeftButton) { m_dragStartPos pos(); } QGraphicsRectItem::mousePressEvent(event); } void FlowChartNode::mouseMoveEvent(QGraphicsSceneMouseEvent *event) { if (event-buttons() Qt::LeftButton) { QPointF newPos mapToParent(event-pos() - event-buttonDownPos(Qt::LeftButton)); setPos(newPos); // 更新所有连接边的路径 for (FlowChartEdge *edge : m_edges) { edge-updatePath(); } } }2.2 连线功能实现连线功能需要处理以下交互流程点击源节点时开始连线鼠标移动时显示临时连线点击目标节点完成连线void FlowChartScene::mousePressEvent(QGraphicsSceneMouseEvent *event) { if (m_currentMode ConnectMode) { QGraphicsItem *item itemAt(event-scenePos(), QTransform()); if (FlowChartNode *node dynamic_castFlowChartNode*(item)) { if (!m_tempEdge) { // 开始新连线 m_tempEdge new FlowChartTempEdge(node, event-scenePos()); addItem(m_tempEdge); } else { // 完成连线 FlowChartEdge *edge new FlowChartEdge(m_tempEdge-sourceNode(), node); addItem(edge); removeItem(m_tempEdge); delete m_tempEdge; m_tempEdge nullptr; } } } QGraphicsScene::mousePressEvent(event); } void FlowChartScene::mouseMoveEvent(QGraphicsSceneMouseEvent *event) { if (m_tempEdge) { m_tempEdge-setEndPoint(event-scenePos()); m_tempEdge-update(); } QGraphicsScene::mouseMoveEvent(event); }3. 高级功能扩展3.1 框选与多选操作QGraphicsView内置了橡皮筋选择功能只需简单设置m_view-setDragMode(QGraphicsView::RubberBandDrag); m_view-setRubberBandSelectionMode(Qt::IntersectsItemShape);自定义选择行为可以重写场景的mousePressEventvoid FlowChartScene::mousePressEvent(QGraphicsSceneMouseEvent *event) { if (event-button() Qt::LeftButton m_currentMode SelectMode) { if (!itemAt(event-scenePos(), QTransform())) { // 点击空白处清空选择 clearSelection(); } } QGraphicsScene::mousePressEvent(event); }3.2 视图缩放与导航实现流畅的缩放和平移功能void FlowChartView::wheelEvent(QWheelEvent *event) { // 缩放视图 const double scaleFactor 1.15; if (event-angleDelta().y() 0) { scale(scaleFactor, scaleFactor); } else { scale(1.0 / scaleFactor, 1.0 / scaleFactor); } } void FlowChartView::mousePressEvent(QMouseEvent *event) { if (event-button() Qt::MiddleButton) { // 中键拖动平移视图 m_lastPanPoint event-pos(); setCursor(Qt::ClosedHandCursor); } QGraphicsView::mousePressEvent(event); } void FlowChartView::mouseMoveEvent(QMouseEvent *event) { if (event-buttons() Qt::MiddleButton) { // 计算平移量 QPointF delta mapToScene(event-pos()) - mapToScene(m_lastPanPoint); m_lastPanPoint event-pos(); // 平移视图 horizontalScrollBar()-setValue(horizontalScrollBar()-value() - delta.x()); verticalScrollBar()-setValue(verticalScrollBar()-value() - delta.y()); } QGraphicsView::mouseMoveEvent(event); }4. 性能优化技巧随着流程图复杂度的增加性能问题会逐渐显现。以下是几个关键优化点4.1 渲染优化// 启用OpenGL加速 QOpenGLWidget *glWidget new QOpenGLWidget(); m_view-setViewport(glWidget); // 设置渲染提示 m_view-setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform); // 启用背景缓存 m_view-setCacheMode(QGraphicsView::CacheBackground);4.2 场景更新策略更新模式适用场景性能影响FullViewportUpdate简单场景或OpenGL视图高MinimalViewportUpdate大多数情况中SmartViewportUpdate复杂场景低NoViewportUpdate自定义更新控制最低// 根据场景复杂度动态调整更新策略 if (scene()-items().size() 100) { m_view-setViewportUpdateMode(QGraphicsView::SmartViewportUpdate); } else { m_view-setViewportUpdateMode(QGraphicsView::MinimalViewportUpdate); }4.3 项绘制优化在自定义项的paint方法中避免复杂计算void FlowChartNode::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *) { // 只绘制可见部分 if (!(option-state QStyle::State_Selected)) { painter-setPen(Qt::NoPen); } else { painter-setPen(QPen(Qt::blue, 2)); } // 简化绘制逻辑 painter-setBrush(m_color); painter-drawRoundedRect(boundingRect(), 5, 5); // 文本绘制 painter-setPen(Qt::black); painter-drawText(boundingRect(), Qt::AlignCenter, m_text); }5. 实用功能增强5.1 对齐与分布工具实现节点的对齐功能void alignSelectedItems(Qt::Alignment alignment) { QListQGraphicsItem* items scene()-selectedItems(); if (items.size() 2) return; // 计算基准位置 QRectF firstRect items.first()-sceneBoundingRect(); qreal reference 0; switch (alignment) { case Qt::AlignLeft: reference firstRect.left(); break; case Qt::AlignRight: reference firstRect.right(); break; case Qt::AlignTop: reference firstRect.top(); break; case Qt::AlignBottom: reference firstRect.bottom(); break; case Qt::AlignHCenter: reference firstRect.center().x(); break; case Qt::AlignVCenter: reference firstRect.center().y(); break; } // 对齐其他项 for (QGraphicsItem *item : items) { QRectF rect item-sceneBoundingRect(); QPointF newPos item-pos(); switch (alignment) { case Qt::AlignLeft: newPos.rx() (reference - rect.left()); break; case Qt::AlignRight: newPos.rx() (reference - rect.right()); break; case Qt::AlignTop: newPos.ry() (reference - rect.top()); break; case Qt::AlignBottom: newPos.ry() (reference - rect.bottom()); break; case Qt::AlignHCenter: newPos.rx() (reference - rect.center().x()); break; case Qt::AlignVCenter: newPos.ry() (reference - rect.center().y()); break; } item-setPos(newPos); } }5.2 撤销/重做功能利用Qt的Undo框架实现命令模式class MoveCommand : public QUndoCommand { public: MoveCommand(QGraphicsItem *item, const QPointF oldPos, QUndoCommand *parent nullptr) : QUndoCommand(parent), m_item(item), m_oldPos(oldPos), m_newPos(item-pos()) { setText(Move Item); } void undo() override { m_item-setPos(m_oldPos); } void redo() override { m_item-setPos(m_newPos); } private: QGraphicsItem *m_item; QPointF m_oldPos; QPointF m_newPos; }; // 在节点移动时记录命令 void FlowChartNode::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) { if (event-button() Qt::LeftButton pos() ! m_dragStartPos) { MoveCommand *cmd new MoveCommand(this, m_dragStartPos); m_undoStack-push(cmd); } QGraphicsRectItem::mouseReleaseEvent(event); }5.3 导入导出功能实现流程图的序列化void FlowChartScene::saveToFile(const QString fileName) { QFile file(fileName); if (!file.open(QIODevice::WriteOnly)) return; QDataStream out(file); // 写入节点数据 QListFlowChartNode* nodes; for (QGraphicsItem *item : items()) { if (FlowChartNode *node dynamic_castFlowChartNode*(item)) { nodes.append(node); } } out nodes.size(); for (FlowChartNode *node : nodes) { out node-pos() node-rect().size() node-text(); } // 写入边数据 QListFlowChartEdge* edges; for (QGraphicsItem *item : items()) { if (FlowChartEdge *edge dynamic_castFlowChartEdge*(item)) { edges.append(edge); } } out edges.size(); for (FlowChartEdge *edge : edges) { out nodes.indexOf(edge-sourceNode()) nodes.indexOf(edge-destNode()); } } void FlowChartScene::loadFromFile(const QString fileName) { clear(); QFile file(fileName); if (!file.open(QIODevice::ReadOnly)) return; QDataStream in(file); // 读取节点数据 int nodeCount; in nodeCount; QListFlowChartNode* nodes; for (int i 0; i nodeCount; i) { QPointF pos; QSizeF size; QString text; in pos size text; FlowChartNode *node new FlowChartNode(); node-setRect(QRectF(QPointF(), size)); node-setPos(pos); node-setText(text); addItem(node); nodes.append(node); } // 读取边数据 int edgeCount; in edgeCount; for (int i 0; i edgeCount; i) { int sourceIdx, destIdx; in sourceIdx destIdx; if (sourceIdx 0 sourceIdx nodes.size() destIdx 0 destIdx nodes.size()) { FlowChartEdge *edge new FlowChartEdge(nodes[sourceIdx], nodes[destIdx]); addItem(edge); } } }