目标读者:已经写过几版 Qt 桌面应用,知道 QWidget 布局怎么用,但一碰到"自由拖拽的拓扑图/流程图/示意图"就容易写崩溃的开发者。
示例环境:Qt 5.12/5.15 + Qt Creator + CMake(你习惯
.pro也没问题,文中工程结构简单,改一下就能用)。
目录
[一、问题背景:WIDGET 布局很香,但真不适合流程图](#一、问题背景:WIDGET 布局很香,但真不适合流程图)
[二、核心知识点:QGraphicsScene / QGraphicsView / QGraphicsItem 三件套](#二、核心知识点:QGraphicsScene / QGraphicsView / QGraphicsItem 三件套)
[1. 场景 / 视图 / 图元之间是什么关系?](#1. 场景 / 视图 / 图元之间是什么关系?)
[2. 三种坐标系统与缩放/平移](#2. 三种坐标系统与缩放/平移)
[3. QGraphicsItem:boundingRect 与 paint 是"生死大事"](#3. QGraphicsItem:boundingRect 与 paint 是“生死大事”)
[4. Item 的选择、拖拽和连线](#4. Item 的选择、拖拽和连线)
[5. 碰撞检测和选框选择](#5. 碰撞检测和选框选择)
[6. 性能优化:OpenGL 视口 + 图元缓存](#6. 性能优化:OpenGL 视口 + 图元缓存)
[1. 工程结构与 CMakeLists.txt](#1. 工程结构与 CMakeLists.txt)
[2. NodeItem:流程节点图元](#2. NodeItem:流程节点图元)
[3. EdgeItem:带箭头的连线](#3. EdgeItem:带箭头的连线)
[4. DiagramScene:自定义场景(添加/删除/JSON)](#4. DiagramScene:自定义场景(添加/删除/JSON))
[5. MainWindow:视图 + 菜单 + Ctrl 滚轮缩放](#5. MainWindow:视图 + 菜单 + Ctrl 滚轮缩放)
[6. main.cpp](#6. main.cpp)
[1. 不给 Item 设置合适的 boundingRect](#1. 不给 Item 设置合适的 boundingRect)
[2. 在 paint() 里做复杂逻辑甚至访问数据库](#2. 在 paint() 里做复杂逻辑甚至访问数据库)
[3. 场景里塞数千图元卡顿:分层 / 简化绘制 / 视口缓存](#3. 场景里塞数千图元卡顿:分层 / 简化绘制 / 视口缓存)
[五、小结:凡是"自由布局 + 重交互",优先考虑图形视图框架](#五、小结:凡是“自由布局 + 重交互”,优先考虑图形视图框架)
一、问题背景:WIDGET 布局很香,但真不适合流程图
做后台管理界面、配置窗口时,Qt 的 QWidget 布局(QVBoxLayout、QHBoxLayout、QGridLayout...)用起来非常顺手:
控件从上到下、从左到右摆好,界面大小变化时自动伸缩,代码也相对整齐。
可一旦需求变成"能拖、能连、能任意摆"时,这套模型立刻显得别扭:
- 画一个网络拓扑图:路由器、交换机、服务器等设备要随意拖来拖去;
- 做一个流程图/状态机编辑器:节点能自由放置、连线跟着节点走;
- 做一个简单的可视化画板:框图、箭头、注释随便画。
如果你用 QWidget 生搬硬套,常见症状是:
- 每个节点都是一个 QWidget ,用
move()改位置; - 连线要自己算起点终点,把
QPainter画在背景里或单独的绘图层; - 想支持缩放和平移就更痛苦------所有坐标、尺寸都要跟着算一遍。
结果是:
- 代码充满"坐标黑魔法"和"像素偏移";
- 性能惨不忍睹:几十个节点还行,上百就开始卡;
- 一旦业务要改样式/换布局,你几乎只能推倒重写。
问题的本质在于:普通 QWidget + 布局管理器是为标准控件表单准备的,而流程图这类场景需要的是一个"可无限扩展的 2D 场景坐标系统"。
Qt 早就给了答案:图形视图框架(Graphics View Framework)。
二、核心知识点:QGraphicsScene / QGraphicsView / QGraphicsItem 三件套
1. 场景 / 视图 / 图元之间是什么关系?
可以先把这三者理解为一套 MVC:
- QGraphicsScene :模型 + 数据容器
- 负责管理所有图元(QGraphicsItem)
- 维护一个统一的"场景坐标系"
- QGraphicsView :视图
- 是一个 QWidget,用来"看"某个 QGraphicsScene 的一部分
- 可以有多个视图看同一个场景(多视角、缩略图预览)
- QGraphicsItem :图元
- 场景中的一个个小对象:节点、连线、图标、文字...
- 自己决定怎么画(paint)、自己提供自己的 boundingRect
简单关系图:
bash
┌────────────────┐
│ QGraphicsView │ ← 一个或多个视图
└──────┬─────────┘
│
▼
┌────────────────┐
│ QGraphicsScene │ ← 统一的场景坐标系
└──────┬─────────┘
/ | \
▼ ▼ ▼
QGraphicsItem QGraphicsItem QGraphicsItem ← 一个个图元
2. 三种坐标系统与缩放/平移
图形视图框架里有三种常见坐标:
-
Item 坐标(局部坐标)
- 每个 QGraphicsItem 自己的局部坐标系
- 通常左上角是 (0,0) 或居中是 (0,0)
-
Scene 坐标(场景坐标)
- 整个 QGraphicsScene 的全局坐标系
- 所有 Item 的
pos()都是相对这个坐标系的
-
View 坐标(视图坐标)
- QGraphicsView 自己的 widget 坐标
- 左上角是 (0,0),右下角是 (width, height)
对应转换函数你会非常常用:
cpp
// 视图坐标 -> 场景坐标(比如鼠标位置)
QPointF scenePos = view->mapToScene(viewPoint);
// 场景坐标 -> 视图坐标(比如把某个场景点移动到视图中心)
QPoint viewPos = view->mapFromScene(scenePoint);
// Item 坐标 -> 场景坐标
QPointF scenePos2 = item->mapToScene(QPointF(0, 0));
// 场景坐标 -> Item 坐标
QPointF itemPos = item->mapFromScene(scenePos2);
缩放和平移,实际上都是对视图做变换:
cpp
// 放大 20%
view->scale(1.2, 1.2);
// 缩小 20%
view->scale(1/1.2, 1/1.2);
// 平移(滚动)
view->translate(dx, dy);
在我们的示例里,会实现:Ctrl + 滚轮 缩放视图,并且通过拖拽中键或按空格+左键来平移。
3. QGraphicsItem:boundingRect 与 paint 是"生死大事"
实现自定义图元时,最核心的两个函数:
cpp
QRectF boundingRect() const override;
void paint(QPainter *painter,
const QStyleOptionGraphicsItem *option,
QWidget *widget = nullptr) override;
-
boundingRect()
- 必须返回该图元可能绘制到的"最大矩形区域"
- Qt 用它来做重绘范围、碰撞检测、选择框判断
- 如果返回太小:部分内容画不出来/点不中
- 如果返回太大:一操作就重绘半个场景 → 卡顿
-
paint()
- 真正的绘制逻辑
- 只能做绘图相关工作,绝不能在里面访问数据库、走网络、做复杂运算
- 这是很多项目"画得越多越卡"的根源之一
4. Item 的选择、拖拽和连线
在图形视图框架中,图元是否可选、可拖动,由 setFlags() 决定:
cpp
setFlags(QGraphicsItem::ItemIsMovable
| QGraphicsItem::ItemIsSelectable
| QGraphicsItem::ItemSendsGeometryChanges);
连线通常有两种实现方式:
-
连线也是一个 QGraphicsItem(比如自定义 LineItem)
- 内部持有指向两个 NodeItem 的指针
- 在
paint()里画一条从 source 到 dest 的线段 - 节点位置变化时,更新连线几何
-
在场景级统一管理连线
- 节点只管自己,连线由场景根据节点位置统一重算
- 适合需要复杂路由(避障、弯折)的场景
本期示例采用第一种方式,更直观。
5. 碰撞检测和选框选择
-
单点选择 :QGraphicsScene 内部会根据
boundingRect()和shape()做点选判断你只要给 Item 设置
ItemIsSelectable标志即可。 -
框选:QGraphicsView 已经内置好选框,只要设置:
cpp
view->setDragMode(QGraphicsView::RubberBandDrag);
view->setRubberBandSelectionMode(Qt::IntersectsItemShape);
你也可以像本期示例那样在自定义 QGraphicsScene 里,实现自己的选框逻辑(比如按住 Ctrl 再框选)。
6. 性能优化:OpenGL 视口 + 图元缓存
图形视图框架有两个非常实用的优化点:
- 视口使用 QOpenGLWidget:
cpp
QGraphicsView *view = new QGraphicsView(scene);
view->setViewport(new QOpenGLWidget);
view->setRenderHint(QPainter::Antialiasing);
view->setViewportUpdateMode(QGraphicsView::FullViewportUpdate);
在图元多、线条多的场景下,OpenGL 可以显著减轻 CPU 压力。
- 图元缓存(Item Cache):
cpp
item->setCacheMode(QGraphicsItem::DeviceCoordinateCache);
对于不会频繁变化的图元(比如背景网格、只读节点),可以让 Qt 把绘制结果缓存成像素图,下次直接贴图,减少重绘成本。
三、代码实战:一个可用的"简单流程图编辑器"
下面的工程叫 DiagramEditorDemo ,你可以直接在 Qt Creator 中用 CMake 打开构建。
功能包括:
- 图元类型:流程节点(矩形)、连线(带箭头的线段)
- 支持:
- 添加节点(在视图中心)
- 拖拽移动节点
- 选中、框选(RubberBand)
- 删除选中节点/连线(Delete)
- 简单连线(示例里用右键菜单或快捷键扩展很方便)
- 视图操作:
- Ctrl + 鼠标滚轮缩放
- 中键拖动平移
- 文件:
- 将流程图保存为简单 JSON
- 从 JSON 反序列化加载
为了篇幅和聚焦,下面的代码以"节点 + 线段 + JSON(节点+线的关系)+ 视图交互"为主,你可以在此基础上自由扩展 UI 皮肤和连线方式。
1. 工程结构与 CMakeLists.txt
工程结构说明:
bash
DiagramEditorDemo/
├── CMakeLists.txt
├── include/
│ ├── diagramscene.h
│ ├── nodeitem.h
│ ├── edgeitem.h
│ └── mainwindow.h
└── src/
├── main.cpp
├── diagramscene.cpp
├── nodeitem.cpp
├── edgeitem.cpp
└── mainwindow.cpp
CMakeLists.txt
2. NodeItem:流程节点图元
include/nodeitem.h
cpp
#pragma once
#include <QGraphicsItem>
#include <QColor>
#include <QString>
class NodeItem : public QGraphicsItem
{
public:
enum { Type = UserType + 1 };
explicit NodeItem(const QString &text = QString(), QGraphicsItem *parent = nullptr);
int type() const override { return Type; }
// 几何与绘制
QRectF boundingRect() const override;
QPainterPath shape() const override;
void paint(QPainter *painter,
const QStyleOptionGraphicsItem *option,
QWidget *widget = nullptr) override;
// 文本与颜色
void setText(const QString &text);
QString text() const { return m_text; }
void setColor(const QColor &color);
QColor color() const { return m_color; }
// 唯一 ID(用于 JSON 存储)
int id() const { return m_id; }
void setId(int newId) { m_id = newId; }
protected:
// 交互
void mousePressEvent(QGraphicsSceneMouseEvent *event) override;
void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override;
QVariant itemChange(GraphicsItemChange change,
const QVariant &value) override;
private:
static int s_nextId;
int m_id;
QString m_text;
QColor m_color;
QRectF m_rect;
};
src/nodeitem.cpp
cpp
#include "nodeitem.h"
#include <QPainter>
#include <QStyleOptionGraphicsItem>
#include <QGraphicsSceneMouseEvent>
#include <QRandomGenerator>
#include <QDateTime>
int NodeItem::s_nextId = 1;
NodeItem::NodeItem(const QString &text, QGraphicsItem *parent)
: QGraphicsItem(parent),
m_id(s_nextId++),
m_text(text),
m_color(Qt::cyan),
m_rect(-60, -30, 120, 60) // 居中矩形
{
// 随机一点颜色,让多个节点看着不太一样
static QRandomGenerator generator(QDateTime::currentMSecsSinceEpoch());
int h = generator.bounded(360);
m_color = QColor::fromHsl(h, 160, 180);
setFlags(ItemIsMovable
| ItemIsSelectable
| ItemSendsGeometryChanges
| ItemSendsScenePositionChanges);
setAcceptHoverEvents(true);
}
QRectF NodeItem::boundingRect() const
{
// 在基础矩形外部留一点边框空间
const qreal margin = 4;
return m_rect.adjusted(-margin, -margin, margin, margin);
}
QPainterPath NodeItem::shape() const
{
// 精确的碰撞/选中区域
QPainterPath path;
path.addRoundedRect(m_rect, 8, 8);
return path;
}
void NodeItem::paint(QPainter *painter,
const QStyleOptionGraphicsItem *option,
QWidget *widget)
{
Q_UNUSED(widget);
// 设置抗锯齿
painter->setRenderHint(QPainter::Antialiasing, true);
// 选中/悬停状态
QColor fill = m_color;
if (option->state & QStyle::State_MouseOver) {
fill = fill.lighter(120);
}
if (option->state & QStyle::State_Selected) {
fill = fill.darker(110);
}
// 绘制矩形
QPen pen(Qt::black, (option->state & QStyle::State_Selected) ? 2.5 : 1.5);
painter->setPen(pen);
painter->setBrush(fill);
painter->drawRoundedRect(m_rect, 8, 8);
// 绘制文本
if (!m_text.isEmpty()) {
painter->setPen(Qt::black);
painter->setFont(QFont("Microsoft YaHei", 9));
painter->drawText(m_rect.adjusted(4, 4, -4, -4),
Qt::AlignCenter, m_text);
}
}
void NodeItem::setText(const QString &text)
{
m_text = text;
update();
}
void NodeItem::setColor(const QColor &color)
{
m_color = color;
update();
}
void NodeItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
// 左键按下时可拖动,右键由场景处理上下文菜单
if (event->button() == Qt::LeftButton) {
QGraphicsItem::mousePressEvent(event);
} else {
event->ignore();
}
}
void NodeItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
{
QGraphicsItem::mouseMoveEvent(event);
}
QVariant NodeItem::itemChange(GraphicsItemChange change,
const QVariant &value)
{
// 位置变化时,通知场景更新连线
if (change == ItemPositionChange && scene()) {
// 可在这里限制移动范围等
}
return QGraphicsItem::itemChange(change, value);
}
3. EdgeItem:带箭头的连线
include/edgeitem.h
cpp
#pragma once
#include <QGraphicsItem>
#include <QLineF>
class NodeItem;
class EdgeItem : public QGraphicsItem
{
public:
enum { Type = UserType + 2 };
EdgeItem(NodeItem *source, NodeItem *dest, QGraphicsItem *parent = nullptr);
int type() const override { return Type; }
NodeItem *sourceNode() const { return m_source; }
NodeItem *destNode() const { return m_dest; }
void adjust(); // 重新计算起点终点
QRectF boundingRect() const override;
void paint(QPainter *painter,
const QStyleOptionGraphicsItem *option,
QWidget *widget = nullptr) override;
private:
NodeItem *m_source;
NodeItem *m_dest;
QPointF m_sourcePoint;
QPointF m_destPoint;
qreal m_arrowSize;
};
src/edgeitem.cpp
cpp
#include "edgeitem.h"
#include "nodeitem.h"
#include <QPainter>
#include <QStyleOptionGraphicsItem>
#include <QtMath>
EdgeItem::EdgeItem(NodeItem *source, NodeItem *dest, QGraphicsItem *parent)
: QGraphicsItem(parent),
m_source(source),
m_dest(dest),
m_arrowSize(10.0)
{
setFlags(ItemIsSelectable | ItemSendsGeometryChanges);
setZValue(-1); // 线在节点下面
adjust();
}
void EdgeItem::adjust()
{
if (!m_source)
return;
// 节点中心点
QPointF src = m_source->scenePos();
QPointF dst;
if (m_dest) {
dst = m_dest->scenePos();
} else {
// 如果目标节点为nullptr,使用源节点位置
dst = src;
}
QLineF line(src, dst);
if (qFuzzyCompare(line.length(), qreal(0.0))) {
m_sourcePoint = m_destPoint = src;
return;
}
// 从节点外缘开始画,简单起见,以中心点计算,不做精确边缘计算
m_sourcePoint = src;
m_destPoint = dst;
prepareGeometryChange();
}
QRectF EdgeItem::boundingRect() const
{
if (!m_source)
return QRectF();
qreal extra = (m_arrowSize + 2.0);
QRectF rect(m_sourcePoint, m_destPoint);
return rect.normalized().adjusted(-extra, -extra, extra, extra);
}
void EdgeItem::paint(QPainter *painter,
const QStyleOptionGraphicsItem *option,
QWidget *widget)
{
Q_UNUSED(option)
Q_UNUSED(widget)
if (!m_source)
return;
QLineF line(m_sourcePoint, m_destPoint);
if (qFuzzyCompare(line.length(), qreal(0.0)))
return;
painter->setRenderHint(QPainter::Antialiasing, true);
// 画线
QPen pen(Qt::darkGray, isSelected() ? 2.5 : 1.5);
painter->setPen(pen);
painter->setBrush(Qt::NoBrush);
painter->drawLine(line);
// 只有当目标节点存在时才绘制箭头
if (m_dest) {
// 箭头
double angle = std::atan2(-line.dy(), line.dx());
QPointF arrowP1 = line.p2() + QPointF(std::sin(angle + M_PI / 3) * m_arrowSize,
std::cos(angle + M_PI / 3) * m_arrowSize);
QPointF arrowP2 = line.p2() + QPointF(std::sin(angle + M_PI - M_PI / 3) * m_arrowSize,
std::cos(angle + M_PI - M_PI / 3) * m_arrowSize);
QPolygonF arrowHead;
arrowHead << line.p2() << arrowP1 << arrowP2;
painter->setBrush(Qt::darkGray);
painter->drawPolygon(arrowHead);
}
}
4. DiagramScene:自定义场景(添加/删除/JSON)
include/diagramscene.h
cpp
#pragma once
#include <QGraphicsScene>
#include <QVector>
class NodeItem;
class EdgeItem;
class DiagramScene : public QGraphicsScene
{
Q_OBJECT
public:
explicit DiagramScene(QObject *parent = nullptr);
void addNodeAt(const QPointF &scenePos);
void deleteSelectedItems();
void clearAll();
void updateEdgesForNode(NodeItem *node);
// 简单 JSON 序列化
bool saveToJson(const QString &filePath) const;
bool loadFromJson(const QString &filePath);
protected:
void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override;
void mousePressEvent(QGraphicsSceneMouseEvent *event) override;
void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override;
void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override;
private:
QVector<NodeItem*> m_nodes;
QVector<EdgeItem*> m_edges;
// 连线创建相关变量
NodeItem *m_startNode; // 连线起点节点
EdgeItem *m_tempEdge; // 临时连线(预览用)
};
src/diagramscene.cpp
cpp
#include "diagramscene.h"
#include "nodeitem.h"
#include "edgeitem.h"
#include <QGraphicsSceneMouseEvent>
#include <QFile>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QDebug>
DiagramScene::DiagramScene(QObject *parent)
: QGraphicsScene(parent),
m_startNode(nullptr),
m_tempEdge(nullptr)
{
setSceneRect(-2000, -2000, 4000, 4000);
setBackgroundBrush(QColor(245, 245, 245));
}
void DiagramScene::addNodeAt(const QPointF &scenePos)
{
auto *node = new NodeItem(QStringLiteral("节点 %1").arg(m_nodes.size() + 1));
node->setPos(scenePos);
addItem(node);
m_nodes.append(node);
}
void DiagramScene::deleteSelectedItems()
{
const auto itemsSel = selectedItems();
for (QGraphicsItem *item : itemsSel) {
if (item->type() == NodeItem::Type) {
// 先移除相关连线
for (int i = m_edges.size() - 1; i >= 0; --i) {
EdgeItem *edge = m_edges[i];
if (!edge) {
m_edges.removeAt(i);
continue;
}
if (edge->sourceNode() == item || edge->destNode() == item) {
removeItem(edge);
delete edge;
m_edges.removeAt(i);
}
}
removeItem(item);
m_nodes.removeOne(static_cast<NodeItem *>(item));
delete item;
} else if (item->type() == EdgeItem::Type) {
removeItem(item);
m_edges.removeOne(static_cast<EdgeItem *>(item));
delete item;
}
}
}
void DiagramScene::clearAll()
{
for (auto e : m_edges) {
if (e) removeItem(e);
delete e;
}
m_edges.clear();
for (auto n : m_nodes) {
if (n) removeItem(n);
delete n;
}
m_nodes.clear();
clear();
}
void DiagramScene::updateEdgesForNode(NodeItem *node)
{
// 更新与指定节点相关的所有连线
for (auto edge : m_edges) {
if (edge && (edge->sourceNode() == node || edge->destNode() == node)) {
edge->adjust();
}
}
}
bool DiagramScene::saveToJson(const QString &filePath) const
{
QJsonObject root;
QJsonArray nodesArr;
for (auto n : m_nodes) {
if (!n) continue;
QJsonObject obj;
obj["id"] = n->id();
obj["text"] = n->text();
obj["x"] = n->pos().x();
obj["y"] = n->pos().y();
obj["color"] = n->color().name();
nodesArr.append(obj);
}
root["nodes"] = nodesArr;
// edges: 简化处理,这里不真正保存,读者可按需求扩展
QJsonArray edgesArr;
for (auto e : m_edges) {
if (!e) continue;
QJsonObject obj;
obj["source"] = e->sourceNode()->id();
obj["dest"] = e->destNode()->id();
edgesArr.append(obj);
}
root["edges"] = edgesArr;
QJsonDocument doc(root);
QFile f(filePath);
if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) {
qWarning() << "无法写入文件:" << filePath;
return false;
}
f.write(doc.toJson(QJsonDocument::Indented));
return true;
}
bool DiagramScene::loadFromJson(const QString &filePath)
{
clearAll();
QFile f(filePath);
if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) {
qWarning() << "无法读取文件:" << filePath;
return false;
}
QByteArray data = f.readAll();
QJsonParseError err;
QJsonDocument doc = QJsonDocument::fromJson(data, &err);
if (err.error != QJsonParseError::NoError) {
qWarning() << "JSON 解析失败:" << err.errorString();
return false;
}
if (!doc.isObject()) return false;
QJsonObject root = doc.object();
QJsonArray nodesArr = root["nodes"].toArray();
QMap<int, NodeItem*> idMap;
for (const QJsonValue &v : nodesArr) {
QJsonObject o = v.toObject();
auto *node = new NodeItem(o["text"].toString());
node->setId(o["id"].toInt());
node->setColor(QColor(o["color"].toString()));
node->setPos(o["x"].toDouble(), o["y"].toDouble());
addItem(node);
m_nodes.append(node);
idMap[node->id()] = node;
}
QJsonArray edgesArr = root["edges"].toArray();
for (const QJsonValue &v : edgesArr) {
QJsonObject o = v.toObject();
int sid = o["source"].toInt();
int did = o["dest"].toInt();
NodeItem *s = idMap.value(sid, nullptr);
NodeItem *d = idMap.value(did, nullptr);
if (s && d) {
auto *e = new EdgeItem(s, d);
addItem(e);
m_edges.append(e);
}
}
return true;
}
void DiagramScene::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
{
// 双击空白处添加节点
if (itemAt(event->scenePos(), QTransform()) == nullptr) {
addNodeAt(event->scenePos());
} else {
QGraphicsScene::mouseDoubleClickEvent(event);
}
}
void DiagramScene::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
// 检查是否点击在节点上
QGraphicsItem *item = itemAt(event->scenePos(), QTransform());
if (item && item->type() == NodeItem::Type) {
// 开始连线创建
m_startNode = static_cast<NodeItem*>(item);
// 创建临时连线用于预览
m_tempEdge = new EdgeItem(m_startNode, nullptr);
m_tempEdge->setZValue(-1);
addItem(m_tempEdge);
}
// 始终调用父类的鼠标按下事件处理函数,以确保节点可以正常移动
QGraphicsScene::mousePressEvent(event);
}
void DiagramScene::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
{
// 如果正在创建连线,更新临时连线的终点
if (m_startNode && m_tempEdge) {
// 更新临时连线的位置
m_tempEdge->adjust();
} else {
// 检查是否有节点正在移动
QGraphicsItem *item = mouseGrabberItem();
if (item && item->type() == NodeItem::Type) {
NodeItem *movingNode = static_cast<NodeItem*>(item);
// 更新与移动节点相关的所有连线
updateEdgesForNode(movingNode);
}
}
// 始终调用父类的鼠标移动事件处理函数,以确保节点可以正常移动
QGraphicsScene::mouseMoveEvent(event);
// 每次鼠标移动后,更新所有连线
// 这是一个简单但有效的解决方案
for (auto edge : m_edges) {
if (edge) {
edge->adjust();
}
}
}
void DiagramScene::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
{
// 如果正在创建连线
if (m_startNode && m_tempEdge) {
// 检查是否点击在另一个节点上
QGraphicsItem *item = itemAt(event->scenePos(), QTransform());
if (item && item->type() == NodeItem::Type) {
NodeItem *endNode = static_cast<NodeItem*>(item);
// 确保不是连接到自身
if (endNode != m_startNode) {
// 创建正式的连线
EdgeItem *edge = new EdgeItem(m_startNode, endNode);
addItem(edge);
m_edges.append(edge);
}
}
// 清理临时连线
removeItem(m_tempEdge);
delete m_tempEdge;
m_tempEdge = nullptr;
m_startNode = nullptr;
}
// 始终调用父类的鼠标释放事件处理函数,以确保节点可以正常移动
QGraphicsScene::mouseReleaseEvent(event);
}
5. MainWindow:视图 + 菜单 + Ctrl 滚轮缩放
include/mainwindow.h
src/mainwindow.cpp
6. main.cpp
试着自己实现。
四、实战中的坑与优化(结合本例)
1. 不给 Item 设置合适的 boundingRect
在 NodeItem / EdgeItem 里,boundingRect 决定了:
- 选中区域;
- 刷新区域;
- 碰撞检测。
如果写成:
cpp
QRectF NodeItem::boundingRect() const
{
return QRectF(0, 0, 0, 0); // 或者一个很小的区域
}
后果是:
- 鼠标怎么点都点不中;
- 画面虽然看见了,但拖拽、选中一律失效;
- Qt 认为它"不占面积",在优化时会把它当空气。
正确做法是:根据你的实际绘制内容留出一点余量。本例中:
cpp
QRectF NodeItem::boundingRect() const
{
const qreal margin = 4;
return m_rect.adjusted(-margin, -margin, margin, margin);
}
2. 在 paint() 里做复杂逻辑甚至访问数据库
在 paint() 里做昂贵操作是很多项目的性能地雷:
- 每次重绘都会调用 paint();
- 拖拽、缩放时会频繁重绘;
- 一旦 paint() 做了数据库访问 / 网络请求 / 复杂计算,界面会肉眼可见地卡顿。
一定要坚持:
- paint() 只做与绘制强相关的简单逻辑;
- 稍微复杂一点的准备工作都放到外面,画之前先算好;
- 需要的数据显示放到成员变量里,paint() 只读取。
3. 场景里塞数千图元卡顿:分层 / 简化绘制 / 视口缓存
当图元数量从几十、几百飙到上千甚至上万,单纯的 QGraphicsView 也会吃力。这时可以考虑:
-
分层:
- 静态背景一层(缓存成图片)
- 动态节点一层
- 高亮/选框一层
可以用多个场景 + 多个视图叠加实现,或用 Z 值区分。
-
简化绘制:
- 缩放很小(整体缩成 10%)时,没必要画那么精细,可以只画矩形轮廓,不画文字和细节;
- 可以根据 view 的 transform 判断当前缩放级别,粗略绘制。
-
视口缓存 + OpenGL:
setViewport(new QOpenGLWidget)setCacheMode(QGraphicsView::CacheBackground)- 对大面积不变的背景使用 QPixmap 缓存。
五、小结:凡是"自由布局 + 重交互",优先考虑图形视图框架
把这一期的内容压缩成一段话就是:
只要场景里有很多可以任意拖拽的图形元素,而且这些元素之间还有大量连线/交互逻辑,就不要再用 QWidget 硬扛了,直接上 QGraphicsScene/QGraphicsView/QGraphicsItem。
具体建议可以写进你的项目规范:
-
判断场景是否适合 Graphics View
- 元素数量 > 50,位置自由,频繁拖动/缩放/连线 → 用
- 简单表单/按钮/静态布局 → 继续用 QWidget/布局管理器。
-
自定义 Item 时,先写好 boundingRect 和 paint,再考虑交互
- 这两个是"基础中的基础",踩稳才能往上堆功能。
-
视图缩放和平移尽量交给 QGraphicsView 处理
- 不要自己改每个 Item 的坐标来模拟缩放/平移;
- 正确姿势是对视图做 transform。
-
性能问题优先从"减少重绘 + 缓存 + OpenGL 视口"三方面考虑
- 优化前先确认是不是 paint()/boundingRect() 写得太过头。
-
序列化逻辑(JSON/XML/DB)尽量和 Item 解耦
- 让 Scene 提供 save/load,Item 只暴露属性,不亲自读写文件。
本期的 DiagramEditorDemo 已经是一个可以直接插入项目、快速改造的"流程图编辑器内核"。
你可以在这之上:
- 加节点右键菜单(重命名、改变颜色、复制/粘贴);
- 加连线交互(按快捷键进入"连线模式",点源节点再点目标节点);
- 加左侧工具箱(不同形状/类型的节点);
- 加属性面板(选中节点后显示属性,可编辑)。
等你把这套框架真正用在自己的项目中,你会非常直观地体会到:
图形视图框架,把原本一团乱麻的"坐标 + 绘图 + 事件",拆成了结构清晰的三层,让你能把更多精力放在业务逻辑和交互体验上。