Qt图形编辑器进阶:基于QGraphicsView实现可拖拽箭头与节点吸附

发布时间:2026/9/18 8:02:55
Qt图形编辑器进阶:基于QGraphicsView实现可拖拽箭头与节点吸附 做图形编辑器项目的时候箭头连接这玩意儿几乎绕不开。前不久有个读者问我模板编辑器里想把两个节点用带箭头的线连起来拖拽调整端点也要实时更新用Qt做怎么又快又稳。我第一反应是直接QPainter画几条线的事儿后来细想根本不是这么简单——你要的不只是一张画出来的图而是能编辑、能响应鼠标、能跟业务数据绑定的图形对象这正好是QGraphicsView框架擅长的事。这篇文章就把我踩过一遍之后沉淀的方案完整分享出来核心是基于QGraphicsItem体系实现一个带手柄的可拖拽箭头端点还支持吸附到节点上节点移动时箭头跟着走。完整代码都在文章里能直接组成一个最小可运行Demo。适合有一定Qt基础、正在做画布编辑器、拓扑图、流程图、思维导图这类项目的朋友照着抄完基本能塞进自己的项目里改。1. 画箭头这个需求为什么最终选了QGraphicsItem方案1.1 先拆需求你要的到底是什么很多人一听到“用Qt画箭头”第一反应是重写paintEventQPainter画两条线加个三角一个下午就能出活。如果你的需求是“在界面上固定位置显示一个箭头”那这么做没问题。但只要往后多想几步需求就变样了箭头两端要能拖拽拖的时候箭头线实时跟着变箭头的起点终点要能吸附到某个节点框上节点被拖动时连在它身上的箭头要自动跟着走可能要同时存在几十上百条箭头每条都要能单独选中、删除一旦需求里出现“可编辑”“可拖拽”“自动更新”这几个词直接在QWidget的paintEvent里一次性绘制就非常痛苦。你不得不自己维护一个对象列表自己处理鼠标命中的热区判断自己计算哪个区域脏了需要重绘往后的每一步都是在跟QPainter的坐标系和整块重绘做斗争。1.2 三种实现方案的横向对比我用一个表格把实际感受列出来这几种方案我都试过优缺点很清晰实现方案画一条箭头难度拖拽交互成本命中检测重绘开销QWidget paintEvent里全量绘制低高全部自己算坐标自己写矩形/路径判断每次整体重绘箭头多了明显卡顿自己维护图形对象局部update中中要自己管对象状态自己遍历对象可控但对象逻辑越写越重QGraphicsView QGraphicsItem低低item自带鼠标事件shape()自动命中scene按变化区域局部重绘我自己实际做下来QGraphicsView这套框架对“可编辑场景”的支持是QPainter直接画完全比不了的。核心原因有三个第一每个QGraphicsItem就是独立的图形对象箭头是它、节点也是它每个item自己管自己的绘制、事件、坐标代码结构天然清晰。第二鼠标事件的派发是框架自动处理的。你不用纠结“我点到了箭头还是点到了节点”item的shape()方法返回自己的有效点击区域框架帮你把事件准确送到该去的item上。第三场景只重绘“变化的区域”。拖一条箭头时Qt不会把整个画布清掉重新画它根据boundingRect自动计算脏区域性能上比全量绘制好一个量级。1.3 这套方案也不是万能药当然QGraphicsItem方案没有银弹有几个情况它反而是负担如果你的箭头数量只有两三条、位置固定不需要交互那QPainter几行代码就画完了引入QGraphicsScene反而显得重如果你要做的是类似GIS那种海量图元渲染需要考虑自绘瓦片或GPU加速QGraphicsView也不是首选。但对绝大多数“编辑器类”项目来说QGraphicsItem这套是性价比最高的路径。下面的代码和踩坑记录都是在这个选型基础上展开的。2. ArrowItem的几何设计箭头三角不是画上去的是算出来的2.1 继承QGraphicsPathItem看看路径是怎么拼出来的画箭头看起来很简单一条直线一个小三角。但直接继承QGraphicsLineItem并不顺手因为QGraphicsLineItem只能管理两个端点画不出箭头头部那个闭合三角形。所以这里的箭头主体继承QGraphicsPathItem把所有几何都塞进一条QPainterPath里让框架统一处理绘制、拾取和边界计算。箭头儿的几何就是三件事主杆从起点到终点的一条粗线箭头三角以终点为顶点逆时针和顺时针各偏转30度向后退18像素得到两个翼点这两个翼点和终点围成一个等腰三角形看起来才是箭头而不是一个V字起终点坐标我直接存在ArrowItem自己的局部坐标里箭头的item位置固定在scene的(0,0)不动。这样起点和终点就是简单的相对坐标拖拽手柄时换算逻辑清晰很多。2.2 箭头三角形为什么要用atan2手算看看完整的ArrowItem头文件和实现。这是整个方案里最关键的一个类后面的手柄、吸附都建立在它的方法上。// arrowitem.h #ifndef ARROWITEM_H #define ARROWITEM_H #include QGraphicsPathItem #include QPointF class EndPointHandle; class ArrowItem : public QGraphicsPathItem { public: explicit ArrowItem(QGraphicsItem *parent nullptr); void setStart(const QPointF pt); void setEnd(const QPointF pt); QPointF start() const { return m_startPt; } QPointF end() const { return m_endPt; } EndPointHandle *startHandle() const { return m_startHandle; } EndPointHandle *endHandle() const { return m_endHandle; } QRectF boundingRect() const override; QPainterPath shape() const override; protected: void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; private: void updatePath(); void createHandles(); QPointF m_startPt; QPointF m_endPt; EndPointHandle *m_startHandle; EndPointHandle *m_endHandle; }; #endif // ARROWITEM_H// arrowitem.cpp #include arrowitem.h #include endpointhandle.h #include QPainter #include QPainterPathStroker #include QtMath ArrowItem::ArrowItem(QGraphicsItem *parent) : QGraphicsPathItem(parent) , m_startPt(0, 0) , m_endPt(100, 0) { setFlag(QGraphicsItem::ItemIsSelectable, true); setZValue(1); createHandles(); updatePath(); } void ArrowItem::setStart(const QPointF pt) { if (m_startPt pt) return; m_startPt pt; updatePath(); } void ArrowItem::setEnd(const QPointF pt) { if (m_endPt pt) return; m_endPt pt; updatePath(); } void ArrowItem::updatePath() { QPainterPath path; path.moveTo(m_startPt); path.lineTo(m_endPt); const qreal angle std::atan2(m_endPt.y() - m_startPt.y(), m_endPt.x() - m_startPt.x()); const qreal headLen 18.0; const qreal headHalfWidth 7.0; QPointF p1 m_endPt; QPointF p2 m_endPt - QPointF(std::cos(angle - M_PI / 6), std::sin(angle - M_PI / 6)) * headLen; QPointF p3 m_endPt - QPointF(std::cos(angle M_PI / 6), std::sin(angle M_PI / 6)) * headLen; path.lineTo(p2); path.lineTo(p3); path.closeSubpath(); setPath(path); if (m_startHandle) m_startHandle-setPos(m_startPt); if (m_endHandle) m_endHandle-setPos(m_endPt); update(); } void ArrowItem::createHandles() { m_startHandle new EndPointHandle(this, true); m_endHandle new EndPointHandle(this, false); m_startHandle-setPos(m_startPt); m_endHandle-setPos(m_endPt); } QRectF ArrowItem::boundingRect() const { return path().boundingRect().adjusted(-6, -6, 6, 6); } QPainterPath ArrowItem::shape() const { QPainterPathStroker stroker; stroker.setWidth(6); return stroker.createStroke(path()).united(path()); } void ArrowItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) { Q_UNUSED(option) Q_UNUSED(widget) painter-setRenderHint(QPainter::Antialiasing, true); QPen pen(QColor(#3B6FB6), 2.5, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin); painter-setPen(pen); painter-setBrush(QColor(#3B6FB6)); painter-drawPath(path()); if (isSelected()) { QPen selPen(QColor(#FF9800), 0.5, Qt::DashLine); painter-setPen(selPen); painter-setBrush(Qt::NoBrush); painter-drawRect(boundingRect()); } }这里头的关键是std::atan2求角度。为什么要手算而不是直接画两条斜线碰运气因为箭头头部必须和主杆方向严格对齐。你把起点挪到终点右边去也就是箭头从右往左指时角度会从0度附近跳到π弧度只有用atan2才能正确处理四个象限的方向保证三角形始终指向真正的终点一侧不会出现箭头“吃”进主杆里的怪样子。2.3 boundingRect和shape为什么都要自己重写boundingRect不写的话默认会拿path的边界计算。但箭头有2.5像素的粗线还有选中态周围的虚线框如果边界贴太紧拖到边缘时线条会被裁掉一半视觉上就是箭头尖“秃了”。所以我统一往外扩了6像素给笔宽和选中框留出余量。shape()则解决“哪里能点中箭头”的问题。默认path的命中区域只包含描边中心线2.5像素的细线非常难点中。我通过QPainterPathStroker把命中宽度扩到6像素再用united(path())把三角形内部也囊括进来。这样用户点击三角形附近、线段附近都能轻松选中箭头。3. 让箭头能拖拽手柄Handle的设计以及两个容易踩的坐标坑3.1 一个极简的手柄item拖拽能力不直接写在ArrowItem自身而是由两个独立的EndPointHandle手柄item来承担。这样做的好处很明显箭头本体只管绘制和几何交互职责拆给手柄两者互不干扰。手柄是挂在ArrowItem下面的子item当箭头被选中时才显示不选中就隐藏这也是编辑器类软件里最常见的交互习惯。手柄的实现如下// endpointhandle.h #ifndef ENDPOINTHANDLE_H #define ENDPOINTHANDLE_H #include QGraphicsRectItem class ArrowItem; class EndPointHandle : public QGraphicsRectItem { public: EndPointHandle(ArrowItem *owner, bool isStart); protected: void hoverEnterEvent(QGraphicsSceneHoverEvent *event) override; void hoverLeaveEvent(QGraphicsSceneHoverEvent *event) override; void mousePressEvent(QGraphicsSceneMouseEvent *event) override; void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override; void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override; private: ArrowItem *m_owner; bool m_isStart; }; #endif // ENDPOINTHANDLE_H// endpointhandle.cpp #include endpointhandle.h #include arrowitem.h #include QCursor #include QGraphicsSceneMouseEvent EndPointHandle::EndPointHandle(ArrowItem *owner, bool isStart) : m_owner(owner) , m_isStart(isStart) { setRect(-7, -7, 14, 14); setBrush(QColor(#FFFFFF)); setPen(QPen(QColor(#3B6FB6), 1.5)); setZValue(10); setAcceptHoverEvents(true); setCursor(Qt::SizeAllCursor); } void EndPointHandle::hoverEnterEvent(QGraphicsSceneHoverEvent *event) { Q_UNUSED(event) setBrush(QColor(#D8E6FF)); } void EndPointHandle::hoverLeaveEvent(QGraphicsSceneHoverEvent *event) { Q_UNUSED(event) setBrush(QColor(#FFFFFF)); } void EndPointHandle::mousePressEvent(QGraphicsSceneMouseEvent *event) { event-accept(); } void EndPointHandle::mouseMoveEvent(QGraphicsSceneMouseEvent *event) { QPointF localPos m_owner-mapFromScene(event-scenePos()); if (m_isStart) m_owner-setStart(localPos); else m_owner-setEnd(localPos); setPos(localPos); event-accept(); } void EndPointHandle::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) { event-accept(); }注意这里我没给手柄设置ItemIsMovable标志而是在mouseMoveEvent里手动setPos。因为ItemIsMovable会触发一整套移动机制配合我手动setPos反而容易冲突。网格吸附、undo栈这些扩展逻辑也最好都集中在mouseMoveEvent里手动处理控制力最强。3.2 坐标转换子项坐标、场景坐标别搞混手柄拖拽最常见的bug就是直接把event-pos()当成场景坐标去计算。event-pos()是鼠标在item自身局部坐标系的坐标对挂在ArrowItem下的手柄来说它是以手柄中心为原点的局部坐标。这个值通常是零点附近的小数你拿它去更新箭头端点箭头一下缩成一团。正确做法是用event-scenePos()拿到场景坐标再通过m_owner-mapFromScene(scenePos)换算成ArrowItem局部坐标最终更新到端点上。ArrowItem的局部坐标和场景坐标在这个方案里恰好可以画等号因为ArrowItem本身没有移动过但代码里绝不能依赖这个巧合。你之后如果要支持箭头整体移动、旋转、缩放现在就养成“所有交互坐标统一走mapFromScene”的习惯能少踩一半坑。3.3 手柄收尾的一个细节purge残留位置手柄在箭头没选中时要隐藏隐藏状态下如果临时调整箭头端点手柄位置也得同步。我建议在ArrowItem里增加一个setHandlesVisible(bool)方法统一管理两个手柄的显示状态。当箭头被选中时调用show取消选中时调用hide。如果不这样做手柄会一直挂在场景里挡住箭头的视觉焦点也会干扰scene的点击事件分发。4. 从“拖拽”到“连接”端点吸附与触发器的配合4.1 一个带节点的最小场景箭头单看着没什么意义得真实放到一张图里和节点连起来才能看出它是不是真的能用。我先给一个简单的节点类它就是一个圆角矩形加一个文字标签同时负责在自身移动时通知箭头刷新。// nodeitem.h #ifndef NODEITEM_H #define NODEITEM_H #include QGraphicsRectItem #include QVector class ArrowItem; class NodeItem : public QGraphicsRectItem { public: NodeItem(const QString text, QGraphicsItem *parent nullptr); void addArrow(ArrowItem *arrow); void removeArrow(ArrowItem *arrow); void updateConnectedArrows(); QVariant itemChange(GraphicsItemChange change, const QVariant value) override; private: QVectorArrowItem * m_arrows; }; #endif // NODEITEM_H// nodeitem.cpp #include nodeitem.h #include arrowitem.h #include QFont #include QGraphicsTextItem #include QPen NodeItem::NodeItem(const QString text, QGraphicsItem *parent) : QGraphicsRectItem(parent) { setRect(-60, -25, 120, 50); setPen(QPen(QColor(#607D8B), 1.5)); setBrush(QColor(#FFF8E1)); setFlags(ItemIsMovable | ItemIsSelectable | ItemSendsGeometryChanges); setZValue(0); auto *label new QGraphicsTextItem(text, this); label-setFont(QFont(Microsoft YaHei, 10)); label-setDefaultTextColor(QColor(#333333)); label-setPos(-30, -12); } void NodeItem::addArrow(ArrowItem *arrow) { if (!m_arrows.contains(arrow)) m_arrows.append(arrow); } void NodeItem::removeArrow(ArrowItem *arrow) { m_arrows.removeAll(arrow); } void NodeItem::updateConnectedArrows() { for (ArrowItem *arrow : m_arrows) { arrow-update(); } } QVariant NodeItem::itemChange(GraphicsItemChange change, const QVariant value) { if (change ItemPositionHasChanged) updateConnectedArrows(); return QGraphicsRectItem::itemChange(change, value); }节点只要开启ItemSendsGeometryChanges标志移动时系统就会回调itemChange在这里把箭头刷一下。这里的箭头更新只是调了update()重绘真正要到“箭头端点跟着节点跑”还得ArrowItem自己记录自己连了哪两个节点。4.2 让箭头跟着节点一起移动我准备给ArrowItem增加setStartNode和setEndNode两个方法让箭头持有节点的指针。节点移动后在updateConnectedArrows里用节点当前坐标重新计算箭头端点。// 在 ArrowItem 中增加 void ArrowItem::setStartNode(NodeItem *node); void ArrowItem::setEndNode(NodeItem *node); void ArrowItem::updateAnchors() { if (m_startNode) setStart(m_startNode-scenePos()); if (m_endNode) setEnd(m_endNode-scenePos()); }这里默认是把端点和节点的中心点对齐。实际产品里箭头线最好连到节点的边缘而不是正中心等真正打磨的时候可以再做一次“矩形边界最近点”的计算。4.3 场景中通过鼠标拖拽动态创建箭头放下节点了创建箭头的方式就得让用户感受到“拖拽”的快感。最自然的交互是在场景空白处按住鼠标拖拽拖出一条带箭头的虚拟线松手时如果终点位置落到某个节点上就生成正式箭头并吸附到节点中心。这里用一个自定义Scene来承载创建逻辑// editorscene.h #ifndef EDITORSCENE_H #define EDITORSCENE_H #include QGraphicsScene #include QPointF class ArrowItem; class NodeItem; class EditorScene : public QGraphicsScene { Q_OBJECT public: explicit EditorScene(QObject *parent nullptr); protected: void mousePressEvent(QGraphicsSceneMouseEvent *event) override; void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override; void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override; private: NodeItem *nodeAt(const QPointF pos); ArrowItem *m_tempArrow; bool m_dragging; }; #endif // EDITORSCENE_H// editorscene.cpp #include editorscene.h #include arrowitem.h #include nodeitem.h #include QGraphicsSceneMouseEvent EditorScene::EditorScene(QObject *parent) : QGraphicsScene(parent) , m_tempArrow(nullptr) , m_dragging(false) { } void EditorScene::mousePressEvent(QGraphicsSceneMouseEvent *event) { if (event-button() Qt::RightButton) { m_dragging true; m_tempArrow new ArrowItem(); m_tempArrow-setStart(event-scenePos()); m_tempArrow-setEnd(event-scenePos()); m_tempArrow-setHandlesVisible(false); m_tempArrow-setZValue(100); addItem(m_tempArrow); event-accept(); return; } QGraphicsScene::mousePressEvent(event); } void EditorScene::mouseMoveEvent(QGraphicsSceneMouseEvent *event) { if (m_dragging m_tempArrow) { m_tempArrow-setEnd(event-scenePos()); event-accept(); return; } QGraphicsScene::mouseMoveEvent(event); } void EditorScene::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) { if (m_dragging m_tempArrow) { m_dragging false; NodeItem *endNode nodeAt(event-scenePos()); NodeItem *startNode nodeAt(m_tempArrow-start()); if (startNode endNode startNode ! endNode) { m_tempArrow-setStartNode(startNode); m_tempArrow-setEndNode(endNode); m_tempArrow-setStart(startNode-scenePos()); m_tempArrow-setEnd(endNode-scenePos()); m_tempArrow-setHandlesVisible(false); startNode-addArrow(m_tempArrow); endNode-addArrow(m_tempArrow); } else { removeItem(m_tempArrow); delete m_tempArrow; } m_tempArrow nullptr; event-accept(); return; } QGraphicsScene::mouseReleaseEvent(event); } NodeItem *EditorScene::nodeAt(const QPointF pos) { const auto items this-items(pos, Qt::IntersectsItemShape, Qt::DescendingOrder); for (QGraphicsItem *item : items) { NodeItem *node dynamic_castNodeItem *(item); if (node) return node; } return nullptr; }这中间其实有个小技巧如果按住右键拖拽时鼠标正好从一个节点的框上出发我想让起点自动吸附到这个节点上。上面的代码里nodeAt同时判断了起点和终点这样保证箭头两端的拖拽和节点的相对位置不打架。如果让箭头从节点框里出发却连到空白处直接删掉temp箭头就行避免半截虚线挂在画布上。5. 实测中遇到的坑按复现概率排个序5.1 拖拽过程中箭头撕裂、残影这是最容易撞上的坑表现是拖动手柄时箭头线从中间断开或者原来位置残留着上一帧的图像。原因大多是boundingRect()给得不够。QGraphicsView的局部重绘机制是“先擦掉旧boundingRect再画新boundingRect”如果你的boundingRect比实际绘制区域小旧线条超出边界的部分就永远擦不到看起来就是残影、拖尾。解决办法没有窍门靠经验预判绘制边界。箭头有笔宽、有选中虚线框、有向外的三角我统一给6像素余量。你在自己的项目里把余量再放大一些也没问题boundingRect大一点对性能影响微乎其微小一点则立刻出现残影。这个维度上“宁大勿小”。5.2 手柄被其他item压住点不到手柄的z值开始没设置默认是0节点也是0。结果手柄在ArrowItem上却经常被节点盖住用户根本点不到。解决起来倒是很简单手柄setZValue(10)让它永远浮在节点和箭头上面。临时箭头在拖拽过程中设成100保证它在新创建的那一刻最显眼。z值管理的核心原则是别依赖创建顺序。addItem后创建的item默认盖在先创建的上面一旦你的图形越来越多、增删频繁顺序就会乱。给每个类别设一个合理的z值区间节点0、箭头1、手柄10、临时元素100这就是非常稳的层级策略。5.3 鼠标拖到边界外导致item丢失场景的鼠标事件默认只在自己的范围内有效。用户把手柄往画布外拖松开时鼠标在场景外mouseReleaseEvent可能到不了item身上手柄就永远保持拖拽状态直到下一次鼠标移回来。处理这个问题一种做法是把ArrowItem设置为ItemIsPanel并启用ItemSendsGeometryChanges让它在拖拽过程中吞掉鼠标抓取另一种更简单在场景的mouseReleaseEvent里主动判断m_dragging并把临时箭头收尾。对于手柄的纯局部拖拽实测中只要在主场景的mouseReleaseEvent里统一对mouseGrabberItem做个releaseMouse处理问题就能规避void EditorScene::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) { if (m_dragging) { /* ... */ } if (QGraphicsItem *grabber mouseGrabberItem()) grabber-ungrabMouse(); QGraphicsScene::mouseReleaseEvent(event); }5.4 端点重合时箭头形状变得怪异当起点和终点几乎重合时atan2(0,0)返回0箭头头会朝一个固定方向。线长度不够时这个头看起来像一个多余的小三角视觉上有点怪。加个判断当两点距离小于某个阈值时不绘制三角只画一个圆点表示端点void ArrowItem::updatePath() { QPainterPath path; qreal dx m_endPt.x() - m_startPt.x(); qreal dy m_endPt.y() - m_startPt.y(); qreal dist std::sqrt(dx * dx dy * dy); if (dist 8.0) { path.addEllipse(m_endPt, 4, 4); setPath(path); return; } // 正常绘制分支... }注意椭圆是通过若干贝塞尔曲线逼近的QPainterPath里直接addEllipse就行拾取和绘制都由框架处理不用自己采样点。5.5 大量箭头下的性能表现几十条箭头时完全不用考虑性能。等到两三百条时拖拽过程会开始有掉帧感。QGraphicsView的视口更新模式默认是MinimalViewportUpdate这模式其实已经比较积极了但如果场景里同时有很多移动的item反而会导致多次局部擦除。实测下来我把视图设置成BoundingRectViewportUpdate后拖动箭头过程中每个item只管自己的boundingRect整体流畅度反而更好一点。代价是每次拖动都会涉及到重叠区域的额外重绘不过对编辑器类项目这点开销是可接受的。另外如果场景中item数量上千还有两个蠢但有效的优化一是给ArrowItem也开启ItemIsSelectable但不开启ItemIsFocusable减少焦点管理系统开销二是用scene-setItemIndexMethod(QGraphicsScene::NoIndex)对大量移动的item来说这能省掉每次重建索引的时间。6. 完整Demo的组装方式把这些类穿起来一个可运行的Demo大概就是这样#include QApplication #include QGraphicsView #include editorscene.h #include nodeitem.h int main(int argc, char *argv[]) { QApplication app(argc, argv); EditorScene scene; scene.setSceneRect(-400, -300, 800, 600); auto *nodeA new NodeItem(QStringLiteral(开始节点)); nodeA-setPos(-150, -50); scene.addItem(nodeA); auto *nodeB new NodeItem(QStringLiteral(结束节点)); nodeB-setPos(150, 80); scene.addItem(nodeB); QGraphicsView view(scene); view.setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing | QPainter::SmoothPixmapTransform); view.setViewportUpdateMode(QGraphicsView::BoundingRectViewportUpdate); view.show(); return app.exec(); }交互方式鼠标右键在场景中拖拽即可创建箭头如果拖出箭头后起点和终点都落在节点上则自动生成实线箭头节点被拖动时连接节点的箭头自动跟随。把文件按这个结构放好就能直接编译运行. ├── arrowitem.h ├── arrowitem.cpp ├── endpointhandle.h ├── endpointhandle.cpp ├── nodeitem.h ├── nodeitem.cpp ├── editorscene.h ├── editorscene.cpp └── main.cpp箭头类、手柄类、节点类、场景类各管一摊互相通过接口协作自己往项目里迁移的时候也容易裁剪。做完这套东西我最大的体会是QGraphicsItem的坑大多数不在API本身而在“几何与事件的边界”。boundingRect给大一点、shape描粗一点、坐标转换做规范一点这个应用跑起来就能少掉八成bug。拖拽箭头这种事看着是绘制问题实际上更多是事件和坐标管理的问题——把这层想通了后面加撤销重做、加多选、加保存加载都不会太难。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询