1. 背景与设计动机:为什么需要 Graphics View
1.1 QPainter 直接绘制的痛点
在 Graphics View 出现之前(Qt3 时代叫 QCanvas,Qt4 起正式命名为 Graphics View),在 Qt 里画 2D 图形基本只有一条路:重写 QWidget::paintEvent(),拿到 QPainter 后一顿 drawLine / drawRect / drawText。
这套"直接绘制"(Immediate Painting)模式有三个绕不开的痛点:
痛点一:画完即忘,图形不是对象。 你用 painter->drawRect(rect) 画了个矩形,这个矩形只是屏幕上的几个像素,程序里没有一个对象代表它。你想让用户点一下这个矩形------做不到,你得自己记录矩形的坐标,然后在 mousePressEvent 里手动做"点是否在矩形内"的判断。图形越多,这种手工命中测试(hit-testing)的代码越爆炸。
痛点二:重绘是"全量重画",性能随复杂度直线下降。 任何局部变化(比如一条线变色),你都得让整个 widget 重绘。图形一多、图形一复杂,界面就开始卡。
痛点三:交互能力全靠手搓。 选中、拖拽、缩放、旋转、批量操作......这些都是图形编辑器的基础需求,但 QPainter 直绘时代每一项都要自己实现,而且每次实现都会遇到"坐标换算"这堵墙。
1.2 Graphics View 架构解决什么问题
Graphics View 的核心思想是:把"画布上的每一个图形"提升为"框架托管的一等公民对象"。
- 每个图形是一个 QGraphicsItem(图元),它有位置、尺寸、层级、变换,框架自动完成命中测试 (点哪个图元)和局部重绘(哪个图元变了只重画它);
- 图元放在 QGraphicsScene(场景)这个"世界容器"里,场景负责管理所有图元、分发事件、执行碰撞检测、做空间索引加速;
- 用户看到的是 QGraphicsView(视图),它是"观察世界的窗口"------同一个场景可以挂多个视图(多窗口、鹰眼导航),每个视图可以独立缩放、平移、旋转观察。
💡 通俗类比:把 QPainter 直绘想象成"用粉笔在黑板上画一幅画"------画完就是粉笔灰,想改一处就得擦掉重画;Graphics View 则像"用乐高搭世界"------每个积木都是一个对象,你可以单独拿起、放下、换位置,还有一架可以拉近拉远的望远镜(View)在观察这个世界(Scene)。
一句话总结:QPainter 解决"怎么画",Graphics View 解决"画出来之后怎么管、怎么交互"。
1.3 Graphics View 与 QCustomPlot、Model/View 的边界
| 维度 | QPainter 直绘 | Graphics View | QCustomPlot | Qt Model/View |
|---|---|---|---|---|
| 定位 | 底层绘制 API | 通用 2D 场景框架 | 统计图表专用库 | 数据展示架构 |
| 图形是对象吗 | 否,画完即像素 | 是,QGraphicsItem | 部分(曲线/轴对象) | 否,数据在 Model |
| 命中测试/选中 | 手搓 | 框架内置 | 部分内置 | 通过索引选中 |
| 批量图元性能 | 差 | 优(BSP 空间索引) | 中 | N/A(网格控件) |
| 典型场景 | 自定义控件绘制 | 流程图/地图/画板/游戏 | 曲线图/柱状图 | 表格/树/列表 |
| 与本文关系 | 被替代/互补 | 本篇主角 | 图表场景可用它实现 | 完全不同问题域 |
2. 核心概念:Scene / View / Item 三角架构
2.1 三角色的职责划分
┌─────────────────────────────────────────────────┐
│ QGraphicsScene │
│ 世界的容器:管理所有图元、事件分发、碰撞检测 │
│ (无边框的抽象世界,坐标单位是"场景坐标") │
└──────────────────────┬──────────────────────────┘
│ 观察/渲染
┌─────────────┼─────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│QGraphicsView │ │QGraphicsView │← |可以有多个视图|
└──────────────┘ └──────────────┘ └──────────────┘
QGraphicsScene(场景):
- 职责:管理图元的"世界容器",维护图元列表、执行碰撞检测、把事件转发给图元、决定场景背景/网格如何绘制。
- 它不是一个 widget,没有可见边框,坐标系是抽象的"场景坐标"(可以理解为"世界坐标")。
- 内部默认用 BSP(二叉空间分割)树索引图元位置,用于加速"点选/框选/碰撞"查询。
QGraphicsView(视图):
- 职责:把场景渲染到屏幕上,处理滚动/缩放/平移等"镜头"操作,把鼠标键盘事件翻译成场景坐标后转给场景。
- 它是真正的 QWidget(或 QGraphicsView 子类),是用户唯一能"看到摸到"的东西。
- 同一个 scene 可以被多个 view 观察,各自独立缩放平移------经典用途:主视图 + 右上角"鹰眼导航图"。
QGraphicsItem(图元):
- 职责:场景里的"一个图形对象",如一个矩形、一张图片、一段文字、一个节点。
- 框架内置了大量现成图元:QGraphicsRectItem、QGraphicsEllipseItem、QGraphicsLineItem、QGraphicsPathItem、QGraphicsPixmapItem、QGraphicsTextItem、QGraphicsPolygonItem、QGraphicsSimpleTextItem 等。
- 复杂业务图形(流程图节点、电路元件、CAD 零件)需要你继承 QGraphicsItem 自定义,见第 6 章。
2.2 三者如何协同(一条完整链路)
- 用户鼠标点下 → QGraphicsView 收到 widget 事件;
- View 把 widget 坐标换算为场景坐标,交给 Scene;
- Scene 用空间索引找到"命中的图元",把事件转成 QGraphicsSceneMouseEvent 分发给图元;
- 图元处理事件(如 mousePressEvent 里标记"我被选中")→ 调用 update() 请求重绘;
- Scene 只对发生变化的图元区域发出重绘请求,View 把该区域重画到屏幕。
⚠️ 新手最容易犯的第一个错误 :试图在 View 上直接 addItem。记住------图元只属于 Scene,View 只是观察者。所有 addItem / removeItem 都是 Scene 的方法。
3. 坐标系统三件套:scene / view / item 与 Z 值层级
3.1 三套坐标系,各管一摊
| 坐标系 | 属于谁 | 单位 | 通俗理解 |
|---|---|---|---|
| Item 坐标 | 每个图元自己 | 图元本地单位 | "我身体内部"的坐标,(0,0) 是图元的本地原点(通常取中心或左上角) |
| Scene 坐标 | 场景 | 场景单位 | "整个世界"的坐标,图元的位置 setPos() 就填场景坐标 |
| View 坐标 | 视图窗口 | 像素 | "望远镜里看到"的坐标,即屏幕上的 widget 坐标 |
三套坐标的关系一句话:Item 坐标 →(图元变换 + 位置)→ Scene 坐标 →(视图变换 + 缩放平移)→ View 坐标。
3.2 三个重量级映射 API
| API | 作用 | 典型用法 |
|---|---|---|
| item->scenePos() | 图元(局部某点)→ 场景坐标 | 想知道节点在世界里的绝对位置 |
| view->mapToScene(QPoint) | 视图像素点 → 场景坐标 | 鼠标点击处对应世界哪个位置(最常用) |
| view->mapFromScene(QPointF) | 场景坐标 → 视图像素 | 把图元世界位置换算成屏幕位置画标记 |
| item->mapToScene(QPointF) / item->mapFromScene(...) | 图元局部 ↔ 场景 | 在自定义 item 内部换算 |
| item->mapToItem(other, pt) / mapFromItem(...) | 图元 ↔ 图元 | 判断两个图元局部坐标互相转换 |
cpp
// 例:在 view 的 mousePressEvent 里拿到"点在哪个图元上"
void MyView::mousePressEvent(QMouseEvent *e)
{
QPointF scenePos = mapToScene(e->pos()); // 1. 视图像素 → 场景坐标(关键一步)
QGraphicsItem *hit = scene()->itemAt(scenePos, QTransform());
// 2. 用场景坐标去问场景"这里有什么图元"(自动做命中测试)
if (hit) qDebug() << "点到图元:" << hit;
}
⚠️ 避坑预警:itemAt() 的第二个参数是"视图变换"。如果视图做了缩放/旋转,这里必须传 view->transform()(通常写 QTransform() 空变换也行,但涉及非等比缩放时会不精确,标准写法是 itemAt(pos, view->transform()))。
3.3 Z 值层级:谁压谁
场景里多个图元重叠时,谁画在上面?由 item->setZValue(double z) 决定:
- Z 值大的画在上面(后绘制),Z 值小的画在下面;
- 默认所有图元 Z 值都是 0;
- Z 值相同(包括同为 0)时,按 addItem 的加入顺序绘制,后加入的在上;
- Z 值只影响绘制顺序和命中优先级,不影响图元在场景中的位置。
cpp
QGraphicsRectItem *bg = scene.addRect(0, 0, 100, 100, pen, brush);
QGraphicsRectItem *fg = scene.addRect(20, 20, 50, 50, pen, brush);
bg->setZValue(0);
fg->setZValue(10); // fg 盖在 bg 上面
💡 实战经验:给"选中态高亮框""拖拽手柄"这类永远在顶层的装饰图元设置很大的 Z 值(如 1000+),避免被业务图元遮挡。
4. 环境搭建:CMake 工程 + Qt6 Widgets
先决条件:已安装 Qt 6.x(含 Widgets 模块)与 CMake 3.16+,且 Qt 已加入 CMAKE_PREFIX_PATH(或用 Qt Creator 打开时自动检测)。
项目结构:
graphics_view_demo/
├── CMakeLists.txt
└── main.cpp
CMakeLists.txt(关键行已注释):
cmake_minimum_required(VERSION 3.16)
project(GraphicsViewDemo VERSION 1.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Qt6 官方推荐:AUTOMOC 自动处理 Q_OBJECT 元对象代码
set(CMAKE_AUTOMOC ON)
# 找 Qt6 的 Widgets 模块(Graphics View 随 Widgets 一起提供)
find_package(Qt6 REQUIRED COMPONENTS Widgets)
qt_add_executable(graphics_view_demo
main.cpp
)
target_link_libraries(graphics_view_demo PRIVATE Qt6::Widgets)
💡 如果没装 Qt6 而是 Qt5:find_package(Qt5 REQUIRED COMPONENTS Widgets) + Qt5::Widgets,其余代码几乎通用。本系列其余示例均按 Qt6 书写。
配置 Qt 路径(命令行方式):
bash
cmake -S . -B build -DCMAKE_PREFIX_PATH="C:/Qt/6.7.2/mingw_64"
cmake --build build
5. 第一个最小示例:场景-视图-图元三步走
把"三个角色 + 一次坐标换算"跑通的最短完整程序(main.cpp):
cpp
#include <QApplication>
#include <QGraphicsScene>
#include <QGraphicsView>
#include <QGraphicsRectItem>
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
// 第 1 步:创建场景(世界的容器)
QGraphicsScene scene;
// 第 2 步:往场景里放图元(画布上的对象)
// addRect 返回一个 QGraphicsRectItem*,我们把它存下来以便后续操作
QGraphicsRectItem *rect = scene.addRect(
-50, -50, 100, 100, // 位置和大小(场景坐标,中心在原点)
QPen(Qt::blue, 3), // 边框:蓝色、3 像素
QBrush(Qt::yellow)); // 填充:黄色
// 图元还可以有自己的变换:旋转 45 度(绕自身原点)
rect->setRotation(45);
// 第 3 步:创建视图并显示(观察世界的窗口)
QGraphicsView view(&scene);
view.setRenderHint(QPainter::Antialiasing); // 抗锯齿,边缘更平滑
view.resize(400, 300);
view.show();
return app.exec();
}
编译运行后你应该看到一个蓝色边框的黄色菱形(矩形旋转 45°)出现在窗口中央。
逐行解读:
- scene.addRect(...) 返回图元指针,同时把图元加入场景------不需要手动 new + addItem 两步;
- setRotation(45) 是图元自带的变换属性,只影响这一个图元,这正是对象化图形与 QPainter 直绘的本质区别;
- view(&scene) 构造时传入场景指针,View 就"看"上了这个场景。
⚠️ 注意:scene 和 view 在这里是栈对象,main 返回时自动析构,析构顺序:view 先于 scene(后构造先析构),这是安全的;反过来如果手动 new 而不管理生命周期,就是第 11 章要讲的内存泄漏坑。
6. 自定义 QGraphicsItem:boundingRect / paint / shape 三件套
内置图元只能覆盖矩形/圆/线/图片等基础形状。真实业务(流程图节点、电路符号、CAD 零件)几乎都要子类化 QGraphicsItem。自定义图元的核心是三个虚函数,理解它们等于掌握了 Graphics View 的自定义契约。
6.1 三个虚函数的分工
| 虚函数 | 返回/作用 | 通俗理解 |
|---|---|---|
| boundingRect() | 返回图元的外接矩形(Item 坐标) | "我占多大地方"------框架用它做裁剪 (只重绘这个区域)和命中测试的粗略判断 |
| paint() | 真正绘制图形(拿到 QPainter) | "我长什么样"------在里面画圆、画线、画文字 |
| shape() | 返回图元的精确外形(QPainterPath) | "我的真实轮廓"------默认是 boundingRect 矩形,复杂图形要重写,否则点图元空白角也算命中 |
⚠️ 最核心的避坑规则 :boundingRect() 必须永远返回一个包含全部绘制内容的矩形,且要比实际图形至少大一点点(常用 adjust(-2,-2,2,2)) 。如果画的内容超出 boundingRect,超出部分会被裁剪掉;如果画的内容小于 boundingRect,会产生无谓的重绘区域导致性能浪费。paint 里画的一切都必须在 boundingRect 范围内。
6.2 完整示例:一个可缩放显示文本的"节点"图元
cpp
#include <QGraphicsItem>
#include <QPainter>
class NodeItem : public QGraphicsItem
{
public:
// 构造:记录节点文字;setFlag 开启"可移动"和"可选中"
explicit NodeItem(const QString &text)
: m_text(text)
{
// ItemIsMovable:允许鼠标拖拽移动
// ItemIsSelectable:允许被点击选中(配合 selectionChanged 信号使用)
// ItemSendsGeometryChanges:位置变化时触发 itemChange 回调(用于拖拽时联动)
setFlags(QGraphicsItem::ItemIsMovable
| QGraphicsItem::ItemIsSelectable
| QGraphicsItem::ItemSendsGeometryChanges);
}
// ① 外接矩形:返回图元占用的矩形(Item 坐标,以 (0,0) 为中心)
// 注意:必须覆盖所有绘制内容,且留出 2px 余量避免裁剪/闪烁
QRectF boundingRect() const override
{
return QRectF(-60, -30, 120, 60).adjusted(-2, -2, 2, 2);
}
// ② 精确外形:默认返回 boundingRect;圆角矩形类图形建议给出精确 path,
// 否则点击圆角外的"透明角"也会命中本图元
QPainterPath shape() const override
{
QPainterPath path;
path.addRoundedRect(-60, -30, 120, 60, 8, 8); // 圆角矩形路径
return path;
}
// ③ 绘制:所有绘图代码都在这里,QPainter 用法与 QPainter 直绘完全一致
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option,
QWidget *widget) override
{
Q_UNUSED(widget);
// 选中态高亮:option->state 里有 Qt 提供的选中信息
if (option->state & QStyle::State_Selected) {
painter->setBrush(QColor(255, 235, 120)); // 选中时浅黄底
} else {
painter->setBrush(QColor(200, 220, 255)); // 未选中浅蓝底
}
painter->setPen(QPen(Qt::black, 2));
painter->drawRoundedRect(-60, -30, 120, 60, 8, 8); // 画圆角矩形
// 画文字:居中显示
painter->drawText(QRectF(-60, -30, 120, 60),
Qt::AlignCenter, m_text);
}
private:
QString m_text;
};
6.3 加入场景并交互
cpp
// 在主窗口/视图初始化处
NodeItem *node = new NodeItem("开始");
scene->addItem(node); // 场景接管所有权,不需要手动 delete
node->setPos(100, 100); // 放到场景坐标 (100,100)
// 选中状态监听:图元选中/取消选中时打印
connect(scene, &QGraphicsScene::selectionChanged, [](){
qDebug() << "选中状态变化!";
});
因为第 6.2 节里设置了 ItemIsMovable,现在鼠标按住节点就能直接拖动------你没写一行拖拽代码,框架全包了,这就是对象化图形的威力。
7. 交互:鼠标键盘事件、碰撞检测、拖拽与动画
7.1 鼠标与键盘事件
自定义图元里重写事件虚函数即可:
cpp
class InteractiveItem : public QGraphicsItem
{
protected:
// 鼠标按下:只在图元内部区域触发(框架已做命中测试,不需要自己判断)
void mousePressEvent(QGraphicsSceneMouseEvent *event) override
{
// event->pos() 是 Item 坐标
// event->scenePos() 是场景坐标
qDebug() << "按下,局部坐标:" << event->pos()
<< "场景坐标:" << event->scenePos();
// 注意:这里可以不调用基类,但想保持默认行为(如触发 movable)
// 或让事件继续传递,就调用基类版本
QGraphicsItem::mousePressEvent(event);
}
// 双击
void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override
{
qDebug() << "双击了图元";
QGraphicsItem::mouseDoubleClickEvent(event);
}
// 键盘事件:需要先 setFlag(ItemIsFocusable) 才能收到键盘事件
void keyPressEvent(QKeyEvent *event) override
{
if (event->key() == Qt::Key_Delete) {
qDebug() << "删除键:准备删除自己";
// 注意:不能在事件处理里直接 delete 图元(可能仍在栈上)
// 标准做法:deleteLater()
this->deleteLater();
} else {
QGraphicsItem::keyPressEvent(event);
}
}
};
键盘事件要点:图元默认不接收键盘事件,必须:
- setFlag(QGraphicsItem::ItemIsFocusable, true);
- 场景里至少有一个图元获得焦点(item->setFocus(),或在图元被点击后由框架自动授予);
- 之后图元才能收到 keyPressEvent。
7.2 碰撞检测
QGraphicsScene 提供两个碰撞检测入口:
| API | 语义 | 典型用法 |
|---|---|---|
| scene->items()(可传矩形/路径/多边形) | 找出与某区域相交的所有图元 | 框选、区域查询 |
| scene->collidingItems(item) | 找出与指定图元碰撞的所有图元 | 游戏撞墙、连线吸附、元素重叠检测 |
cpp
// 例:检测矩形选区内的所有图元(框选)
QList<QGraphicsItem*> hitList = scene->items(
QRectF(10, 10, 200, 150), // 场景坐标下的矩形区域
Qt::IntersectsItemShape); // 相交判定方式:与图元精确外形相交才算
// 例:游戏里子弹图元撞到了谁
QList<QGraphicsItem*> collided = scene->collidingItems(bullet);
for (QGraphicsItem *other : collided) {
if (other->data(0).toString() == "enemy") { // data() 可挂自定义属性,见 9 章
qDebug() << "命中敌人!";
}
}
7.3 拖拽:图元拖到视图、视图外部文件拖进来
Graphics View 有两层拖拽,别混淆:
第一层:图元自身的移动(第 6 章已覆盖)------设置 ItemIsMovable 即可,无需任何代码。
第二层:拖放(Drag & Drop)------把图元拖出/拖入视图,或从文件管理器拖文件到视图上。实现要点:
cpp
// 视图子类里启用拖放接收
class DropView : public QGraphicsView
{
public:
DropView(QWidget *parent = nullptr) : QGraphicsView(parent)
{
setAcceptDrops(true); // 视图接受拖放
setDragMode(QGraphicsView::RubberBandDrag); // 顺便支持橡皮筋框选
}
protected:
void dragEnterEvent(QDragEnterEvent *e) override
{
// 只接受文件拖入
if (e->mimeData()->hasUrls()) e->acceptProposedAction();
}
void dropEvent(QDropEvent *e) override
{
// 把拖入的文件路径换算成场景坐标,创建图元
QPointF scenePos = mapToScene(e->position().toPoint());
for (const QUrl &url : e->mimeData()->urls()) {
auto *pix = new QGraphicsPixmapItem(QPixmap(url.toLocalFile()));
pix->setPos(scenePos);
scene()->addItem(pix); // 场景接管所有权
}
}
};
💡 若想让图元自己支持"可被拖出"(比如拖到别的 widget),要重写图元的 mouseMoveEvent + 启动 QDrag,并配合 ItemIsMovable 关闭默认移动。这是进阶话题,多数场景用不到。
7.4 动画:QGraphicsItemAnimation 与 QPropertyAnimation
方式一:QGraphicsItemAnimation(经典,配合 QTimeLine)
cpp
QTimeLine *timeline = new QTimeLine(1000); // 动画时长 1000ms
timeline->setFrameRange(0, 100); // 0~100 帧
QGraphicsItemAnimation *anim = new QGraphicsItemAnimation;
anim->setItem(nodeItem);
anim->setTimeLine(timeline);
// 第 0 帧在 (0,0),第 50 帧移动到 (200,0),第 100 帧到 (200,200)
anim->setPosAt(0.0, QPointF(0, 0));
anim->setPosAt(0.5, QPointF(200, 0));
anim->setPosAt(1.0, QPointF(200, 200));
timeline->start();
方式二:QPropertyAnimation(现代推荐,图元需要 QGraphicsObject)
QGraphicsObject 是「QObject + QGraphicsItem」的合体(见第 9 章),它让图元拥有属性系统,从而可以被 QPropertyAnimation 直接动画:
cpp
// MyGraphicsObject : public QGraphicsObject
QPropertyAnimation *posAnim = new QPropertyAnimation(myObj, "pos");
posAnim->setDuration(800);
posAnim->setStartValue(QPointF(0, 0));
posAnim->setEndValue(QPointF(300, 150));
posAnim->setEasingCurve(QEasingCurve::OutBounce); // 弹性出射,效果更生动
posAnim->start();
对比:QGraphicsItemAnimation 需要手动管 QTimeLine,适合"轨道动画"(预先定义多关键帧);QPropertyAnimation 更符合现代 Qt 声明式动画习惯,配合 QGraphicsObject 还能直接用信号槽(如 itemChange 通知状态)。
⚠️ 动画避坑:动画期间频繁修改图元位置是高频更新,务必确保 boundingRect() 精确且不夸张,否则每帧都触发大区域重绘,动画会卡。
8. 视图导航:缩放与平移
这是"地图/画板/监控大屏"类应用必备能力。三种常用手段:
8.1 setTransform 手动缩放(最灵活)
cpp
void zoomView(QGraphicsView *view, qreal factor)
{
// 以鼠标当前位置为锚点缩放,体验更贴近"放大镜跟手"
view->setTransformationAnchor(QGraphicsView::AnchorUnderMouse);
view->scale(factor, factor); // 等比缩放
}
setTransformationAnchor 三个选项:
- AnchorViewCenter(默认):以视图中心为锚点;
- AnchorUnderMouse:以鼠标位置为锚点(地图类应用首选);
- AnchorNoAnchor:不保持锚点(变换后内容会跑位,一般不推荐)。
8.2 滚轮缩放(重写 wheelEvent)
cpp
void MyView::wheelEvent(QWheelEvent *e)
{
// 每格滚轮 15°,放大 1.15 倍 / 缩小 1/1.15 倍
const qreal factor = std::pow(1.15, e->angleDelta().y() / 120.0);
setTransformationAnchor(QGraphicsView::AnchorUnderMouse);
scale(factor, factor);
}
⚠️ 必须给缩放设置上下限,否则无限放大到数值溢出、无限缩小到图元消失:
cppQTransform t = transform(); if (t.m11() < 0.1 && factor < 1.0) return; // 已最小,拒绝继续缩小 if (t.m11() > 10.0 && factor > 1.0) return; // 已最大,拒绝继续放大
8.3 平移(拖拽画布 / 空格+拖拽)
cpp
// 方案 A:ScrollHandDrag 模式------按住左键拖拽整张画布
view->setDragMode(QGraphicsView::ScrollHandDrag);
// 缺点:此模式下左键无法选中图元;常见做法是空格按下时切到该模式
// 方案 B:手动记录平移量(用 scrollbars)
void MyView::mouseMoveEvent(QMouseEvent *e)
{
if (m_panning) { // m_panning 由 mousePress/Release 维护
QPoint delta = e->pos() - m_lastPos;
horizontalScrollBar()->setValue(horizontalScrollBar()->value() - delta.x());
verticalScrollBar()->setValue(verticalScrollBar()->value() - delta.y());
m_lastPos = e->pos();
}
QGraphicsView::mouseMoveEvent(e);
}
8.4 鹰眼导航(一 Scene 多 View 的经典应用)
cpp
// 主视图 + 鹰眼视图共用一个场景
QGraphicsView *mainView = new QGraphicsView(&scene);
QGraphicsView *miniView = new QGraphicsView(&scene);
// 鹰眼:固定小尺寸、只读、随主视图联动
miniView->setFixedSize(180, 120);
miniView->scale(0.1, 0.1); // 鹰眼缩小比例
miniView->setInteractive(false); // 鹰眼不做交互
miniView->setDragMode(QGraphicsView::NoDrag);
// 主视图滚动时同步鹰眼视野(伪代码示意)
connect(mainView->horizontalScrollBar(), &QScrollBar::valueChanged,
miniView, [&](){ miniView->centerOn(mainView->mapToScene(mainView->viewport()->rect().center())); });
9. 核心 API 详解
9.1 QGraphicsScene ------ 世界的容器
| API | 语义 | 备注 |
|---|---|---|
| addItem(QGraphicsItem*) | 加入图元并接管所有权 | 图元删除用 removeItem 或直接 delete |
| addRect / addEllipse / addLine / addPath / addPixmap / addText / addSimpleText / addPolygon | 快速创建内置图元并加入 | 返回对应图元指针,方便链式设置 |
| removeItem(item) | 把图元从场景移除(不删除对象) | 移除后需自行管理内存 |
| clear() | 删除并清空所有图元 | 会真正 delete 图元,注意别再用旧指针 |
| items() | 返回场景内所有/某区域图元 | 可传矩形/路径/多边形做区域查询 |
| itemAt(pos, transform) | 返回指定场景坐标处最顶层图元 | 传视图 transform 更精确 |
| collidingItems(item) | 返回与指定图元碰撞的图元列表 | 默认按 shape 相交判定 |
| setSceneRect(rect) | 设置场景"世界边界" | 影响视图滚动范围 |
| selectionChanged 信号 | 选中集合变化时发射 | 常用于状态栏显示选中数 |
| changed(rects) 信号 | 场景某区域重绘时发射 | 可用于自定义同步 |
| setItemIndexMethod(BspTreeIndex/NoIndex) | 设置图元空间索引方式 | 性能调优关键,见 11.4 |
| setBackgroundBrush() | 设置背景(纯色/网格纹理) | 画板/网格地图常用 |
| focusItem() | 当前拥有焦点的图元 | 配合键盘事件 |
9.2 QGraphicsView ------ 观察世界的窗口
| API | 语义 | 备注 |
|---|---|---|
| setScene(QGraphicsScene*) | 绑定/切换场景 | 一个 view 同时只绑一个 scene |
| scale(sx, sy) / rotate(a) / translate(dx, dy) | 视图级变换(镜头操作) | 多次调用会叠加到 transform() |
| centerOn(item/point) | 让场景某点/某图元居中 | 跳转定位神器 |
| ensureVisible(item/rect) | 确保某区域可见(必要时滚动) | 自动滚动到指定图元 |
| setRenderHint(QPainter::Antialiasing) | 开启抗锯齿 | 图形圆润的关键开关 |
| setDragMode(NoDrag/ScrollHandDrag /RubberBandDrag) | 左键拖拽行为 | 橡皮筋框选 / 抓手平移 |
| setTransformationAnchor(...) | 缩放锚点 | 见第 8 章 |
| setInteractive(bool) | 是否把事件转给场景交互 | false 时纯展示,常用鹰眼 |
| setOptimizationFlag (DontSavePainterState/DontAdjustForAntialiasing) | 性能优化标志 | 大批量图元时酌情开启 |
| mapToScene / mapFromScene | 视图↔场景坐标映射 | 核心坐标换算 |
| viewport() | 返回内部视口 widget | 自定义重绘/事件拦截点 |
| setViewportUpdateMode(...) | 重绘策略(Full/Minimal/BoundingRect) | 性能调优,见 11.4 |
| setAlignment(...) | 场景比视图小时的对齐方式 | 默认居中 |
9.3 QGraphicsItem ------ 图元的基类契约
| API | 语义 | 备注 |
|---|---|---|
| setPos / pos() | 图元在场景中的位置 | 存的是父图元坐标系(无父时=场景坐标) |
| setZValue / zValue() | Z 层级 | 见 3.3 |
| setRotation / setScale / setTransform / setTransformOriginPoint | 图元级变换 | 影响自身绘制与命中 |
| setParentItem(parent) | 建立父子图元 | 父移动子跟随,变换叠加,见 11.6 |
| boundingRect() | 外接矩形(必须重写) | 见 6.1 |
| paint() | 绘制(必须重写) | 见 6.1 |
| shape() | 精确外形 | 默认=boundingRect,复杂图形重写 |
| contains(pt) | 判断点是否在图元内 | 基于 shape |
| setFlag(Flag, bool) | 开关图元能力 | 见下方 Flag 列表 |
| setFlags(flags) | 批量设置能力 | 常用组合见 6.2 |
| setSelected / isSelected | 选中状态 | 配合 selectionChanged |
| setFocus / hasFocus | 键盘焦点 | 需 ItemIsFocusable |
| setCursor() | 悬停光标 | 如十字光标/手型 |
| setToolTip() | 悬停提示 | 零成本体验提升 |
| setData(key, value) | 挂任意自定义数据 | 免建子类的轻量方案 |
| setAcceptHoverEvents(bool) | 开启悬停事件 | 不开启收不到 hoverEnter/Move/Leave |
| itemChange(Change, value) | 属性变化回调 | 需 ItemSendsGeometryChanges 等 flag |
| update() | 请求重绘 | 属性变化后手动调用 |
| mapToScene / mapFromScene / mapToItem / mapFromItem | 坐标转换族 | 见 3.2 |
| scenePos() | 图元在场景中的绝对位置 | 等价 mapToScene(0,0) |
QGraphicsItem::GraphicsItemFlag 常用枚举:
| Flag | 效果 | 常见组合场景 |
|---|---|---|
| ItemIsMovable | 可被鼠标拖动 | 流程图节点 |
| ItemIsSelectable | 可被选中 | 配合框选 |
| ItemIsFocusable | 可接收键盘焦点 | 快捷键删除节点 |
| ItemSendsGeometryChanges | 位置/尺寸变化时触发 itemChange | 拖拽时实时联动连线 |
| ItemIgnoresTransformations | 忽略视图缩放(保持屏幕尺寸不变) | 标注文字/图钉,见 11.5 |
| ItemIsPanel | 作为"面板"接收键盘事件 | 复杂面板图元 |
| ItemAcceptsInputMethod | 接收输入法(中文输入) | 图元内文本编辑 |
9.4 QGraphicsObject ------ 图元 + QObject
QGraphicsObject : public QObject, public QGraphicsItem,把两棵类树合一,获得:
- 信号槽能力(图元之间直接 connect);
- Q_PROPERTY 属性系统(可被 QPropertyAnimation 动画、被 QML 使用);
- QObject 父子/定时器。
cpp
class MyNode : public QGraphicsObject
{
Q_OBJECT
Q_PROPERTY(QPointF pos READ pos WRITE setPos) // 暴露给属性动画
public:
// 必须实现这两个纯虚函数(来自 QGraphicsItem)
QRectF boundingRect() const override;
void paint(QPainter *painter, const QStyleOptionGraphicsItem *o, QWidget *w) override;
signals:
void clicked(); // 图元也能发信号!
protected:
void mousePressEvent(QGraphicsSceneMouseEvent *e) override
{
emit clicked();
QGraphicsObject::mousePressEvent(e);
}
};
与 QGraphicsItem 的选择:纯绘制无交互逻辑用 QGraphicsItem(轻量);需要信号槽/属性动画/定时器/嵌入 QML 时用 QGraphicsObject(代价:多一个 vtable + QObject 开销)。
9.5 QGraphicsSceneEvent ------ 事件对象家族
场景给图元分发事件时,鼠标事件不是裸 QMouseEvent,而是包了一层场景专用的子类:
| 类 | 携带的额外信息 | 触发时机 |
|---|---|---|
| QGraphicsSceneMouseEvent | pos()(Item 坐标)、scenePos()(场景坐标)、screenPos()(屏幕坐标)、button()、buttons()、modifiers()、lastPos/lastScenePos(上一次位置,拖拽增量计算神器) | 鼠标按下/移动/释放/双击 |
| QGraphicsSceneWheelEvent | delta()、scenePos()、modifiers() | 滚轮 |
| QGraphicsSceneHoverEvent | pos()、scenePos()、modifiers() | 悬停进入/移动/离开 |
| QGraphicsSceneDragDropEvent | mimeData()、scenePos()、dropAction() | 拖放 |
| QGraphicsSceneContextMenuEvent | scenePos()、screenPos()、modifiers() | 右键菜单 |
| QGraphicsSceneHelpEvent | scenePos()、screenPos() | 帮助(Shift+F1) |
| QGraphicsSceneMoveEvent / ResizeEvent | oldPos/newPos、oldSize/newSize | 图元移动/缩放后 |
关键理解 :event->pos() 是 Item 坐标 (图元本地),event->scenePos() 是场景坐标。在 paint 里写局部坐标,在事件里若想"图元局部位置",用 pos();若想"世界位置",用 scenePos()。
9.6 QGraphicsItemAnimation ------ 关键帧动画
见 7.4 方式一。核心 API:setItem(item)、setTimeLine(timeline)、setPosAt(step, pos)、setRotationAt(step, angle)、setScaleAt(step, scale)、setTranslationAt(step, dx, dy)。step 为 0.0~1.0 的时间比例。
9.7 其他常用类
| 类 | 用途 |
|---|---|
| QGraphicsRectItem / EllipseItem / LineItem / PathItem / PixmapItem / TextItem / SimpleTextItem / PolygonItem | 内置图元,直接可用 |
| QGraphicsProxyWidget | 把普通 QWidget 嵌入场景当图元(如场景内放 QLineEdit) |
| QGraphicsEffect(含 QGraphicsDropShadowEffect 等) | 图元特效(阴影/模糊/透明) |
| QGraphicsItemGroup | 把多个图元打包成组(可整体移动) |
| QGraphicsPathItem | 任意路径图元,画折线/贝塞尔最灵活 |
| QGraphicsSimpleTextItem | 轻量文本图元(比 TextItem 便宜,无富文本) |
| QGraphicsDropShadowEffect | 阴影特效,卡片感 |
| QStyleOptionGraphicsItem | paint 里携带的选项(选中态/细节级别/变换) |
10. 使用场景与选型建议
10.1 典型场景一览
| 场景 | 为什么用 Graphics View | 关键能力组合 |
|---|---|---|
| 流程图 / 拓扑编辑器(节点-连线) | 节点是图元(可拖拽选中),连线是 LineItem/PathItem,拖拽节点时联动更新连线 | ItemIsMovable + ItemSendsGeometryChanges + itemChange 重算连线 |
| 地图与画板 | 图元=地物/笔画;平移缩放导航 + 鹰眼 | 滚轮缩放 + ScrollHandDrag + 多 View |
| 监控大屏 / 工业组态 | 设备图标是图元,告警变色=改 paint,状态上报=信号槽 | QGraphicsObject + data() 挂设备 ID + 定时刷新 |
| 游戏地图(2D 俯视) | 地图块/单位都是图元,碰撞检测内置 | collidingItems + itemAt + 动画 |
| CAD 轻量查看器 | 矢量图元可缩放不失真,选中/拖拽是基础编辑能力 | 高精度变换 + ItemIgnoresTransformations 处理标注 |
| 图表标注 / 截图标注 | 画矩形/箭头/文字标注,可拖可删 | 内置图元 + 自由坐标系统 |
| 关系图谱 / 思维导图 | 节点+连线图元,力导向布局配合动画 | 父子图元 + QGraphicsItemAnimation |
10.2 选型决策树
要画的是什么?
├─ 统计数据图表(折线/柱状/饼)→ 优先 QCustomPlot / QtCharts(现成图表库)
│ └─ 只是简单曲线?→ QPainter 直绘就够了
├─ 表格/树/列表式数据展示 → Qt Model/View
├─ 自由画布上的对象管理(可选中/拖拽/碰撞/多层)→ Graphics View ★
│ └─ 图元数量巨大(>10 万)且都是简单形状?→ 考虑自定义 OpenGL/Vulkan 渲染
└─ 一次性静态绘制(不交互)→ QPainter 直绘即可
核心判断标准 :是否需要"图形是对象、可交互"。是 → Graphics View;否 → 考虑更轻的方案。
11. 常见问题与避坑指南
11.1 坐标转换错误(最常见的坑)
症状 :鼠标点击位置和图元实际位置对不上,越缩放越偏。 原因 :没有把 View 像素坐标换算成 Scene 坐标就直接使用,或用了错误的映射 API。 正确姿势:
cpp
// 在 QGraphicsView 子类里:e->pos() 是视图像素坐标
QPointF scenePos = mapToScene(e->pos()); // 必须换算!
// 在 QGraphicsItem 事件里:
QPointF localPos = event->pos(); // 已是 Item 坐标,直接用
QPointF worldPos = event->scenePos(); // 已是场景坐标
进阶坑:视图旋转/非等比缩放后,itemAt() 的 transform 参数传错会导致命中偏移。统一用 scene()->itemAt(scenePos, view->transform())。
11.2 boundingRect 不准确导致重绘异常
症状 :图元边缘有残影、闪烁、内容被裁剪、移动后留"鬼影"。 原因 :paint 画的内容超出 boundingRect(),框架只重绘 boundingRect 区域。 修复:
- boundingRect 必须完全覆盖所有绘制内容;
- 留 adjusted(-2, -2, 2, 2) 余量(抵消抗锯齿和 pen 宽度);
- 图元内坐标变化(如文本变长)后主动 update();
- 不要在 paint 里依赖外部可变状态却不触发 update。
11.3 图元数量大性能差
症状 :几千个图元后,缩放/拖动明显卡顿。 原因 :命中测试和重绘范围过大。 优化三板斧(按性价比排序):
cpp
// ① 索引方式:大量图元用 BSP(默认);图元常变(频繁 add/remove)时用 NoIndex 反而更快
scene.setItemIndexMethod(QGraphicsScene::NoIndex);
// ② 重绘策略:静态场景用 Minimal,动态大批量用 Full(或不更新)
view.setViewportUpdateMode(QGraphicsView::BoundingRectViewportUpdate);
// 或 QGraphicsView::FullViewportUpdate(简单粗暴但可能更流畅)
// ③ 图元本身瘦身
// - boundingRect 精确(避免大空白区域被当作重绘区)
// - 避免每帧 new QPen/QBrush(复用成员变量)
// - 非必要不开抗锯齿的优化标志
view.setOptimizationFlag(QGraphicsView::DontAdjustForAntialiasing);
终极方案:若单场景图元数达到数十万级别且形状简单,考虑 QGraphicsScene::NoIndex + 视图层 setViewport() 换成 QOpenGLWidget,或干脆脱离 Graphics View 用自绘/GPU 方案。
11.4 缩放后线条/文字变粗变大
症状 :视图放大后,本来 1px 的线变成粗线,文字被撑大变形。 原因 :视图变换作用于所有图元,线宽/字号都被缩放。 修复:给需要"屏幕恒定尺寸"的图元加 Flag:
cpp
textItem->setFlag(QGraphicsItem::ItemIgnoresTransformations, true);
// 效果:位置随场景缩放平移,但自身尺寸/线宽/字号保持不变(始终屏幕大小)
⚠️ 注意:ItemIgnoresTransformations 让图元忽略视图缩放(还有视图旋转也会被忽略)。若只想"线宽恒定"而文字/图形仍缩放,需要自己写 setCosmetic 风格笔:QPen pen(QColor(0,0,0), 1, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin); pen.setCosmetic(true);------cosmetic pen 是真正的"屏幕像素恒定线宽",与 ItemIgnoresTransformations 无关,两者可组合。
11.5 父子图元变换叠加
规则 :子图元的 setPos/rotation/scale 都是相对父图元坐标系 的;父图元的变换会叠加到子图元上。因此:
- 移动父图元 → 子图元跟随移动(无需手动);
- 子图元的 scenePos() 是"父变换 + 自身变换"后的绝对位置;
- 经典坑:把子图元 setPos 到"场景坐标"的值,但父图元有偏移/缩放,子图元位置就会"跑偏"------因为 setPos 相对父系。要么确认父系坐标,要么用 mapToScene/mapFromScene 换算后再 setPos。
cpp
// 正确示例:把子图元放到场景坐标 (500, 500)
QPointF targetScene = QPointF(500, 500);
QPointF localInParent = child->mapFromScene(targetScene); // 场景坐标 → 父系坐标
child->setPos(localInParent);
11.6 内存泄漏与场景清理
规则一:场景接管所有权。 scene->addItem(p) 之后,不要再手动 delete p(可能引发双重释放);删除图元统一用 scene->removeItem(p); delete p; 或直接 delete p;(QGraphicsScene 析构会自动 delete 所有图元,但手动 delete 单个图元时它会自动从场景移除)。
规则二:注意删除时序。 在 keyPressEvent 等事件处理中 delete 图元(尤其 delete 自己)有风险,用 deleteLater():
cpp
// 危险:事件还在分发中 delete 自己
// delete this; ❌
// 安全:
this->deleteLater(); // ✅ 事件循环空闲后安全销毁
规则三:场景清空用 clear()。 scene->clear() 会删除所有图元并清空列表;清空后不要再引用旧图元指针。
规则四:Scene/View 生命周期。 谁 new 谁负责。局部栈对象自动析构(注意析构顺序:View 先于 Scene)。堆对象建议 view->setParent(widget) + scene 作为成员或由 view 管理。
11.7 线程安全
铁律:QGraphicsScene / QGraphicsView / QGraphicsItem 不是线程安全的,必须只在其所属 GUI 线程(主线程)操作。
- 在子线程更新图元属性、增删图元 → 未定义行为,轻则闪烁,重则崩溃;
- 正确姿势:子线程计算数据,通过信号槽(QueuedConnection) 投递到主线程,在主线程槽函数里更新图元;
- 后台持续计算(如仿真)→ 用 QTimer(主线程节拍)或 QThread + 信号投递,避免直接跨线程摸图元。
cpp
// 正确:子线程发信号 → 主线程槽更新图元
// class Worker : QObject { signals: void newDataReady(QPointF pos); };
// class UiBridge : QObject { public slots: void updateNode(QPointF pos){ node->setPos(pos); } };
connect(worker, &Worker::newDataReady, bridge, &UiBridge::updateNode, Qt::QueuedConnection);
11.8 其他高频坑
| 坑 | 现象 | 解法 |
|---|---|---|
| 图元不响应鼠标 | 点击没反应 | 检查是否设置了对应 flag(ItemIsSelectable/ItemIsMovable);没设任何交互 flag 时图元默认只接收 hover 提示 |
| 收不到 hover 事件 | hoverEnter 不触发 | setAcceptHoverEvents(true) |
| 收不到键盘事件 | 按 Delete 没反应 | setFlag(ItemIsFocusable) + 确保图元有焦点(点击后 setFocus()) |
| 图元透明区域也触发点击 | 点矩形外圈也选中 | 重写 shape() 返回精确路径 |
| 缩放后命中不准 | 放大后点不中/点错图元 | itemAt(pos, view->transform()) 传正确变换 |
| 文字图元缩放模糊 | 放大后文字糊 | 设置 view->setRenderHint(QPainter::Antialiasing) + TextAntialiasing;或对文字图元加 ItemIgnoresTransformations |
| 视图不显示内容 | 只有空白窗口 | 检查 scene rect(scene->setSceneRect)是否覆盖图元区域;view 默认按 scene rect 滚动 |
| 图元被裁剪 | 图元一部分消失 | 检查 boundingRect 是否覆盖全部绘制内容;检查 scene rect |
12. FAQ 速查表
Q1:Graphics View 和 QPainter 有什么区别? QPainter 是底层绘制 API(怎么画),Graphics View 是场景化对象框架(画出的东西怎么管、怎么交互)。Graphics View 内部也是用 QPainter 渲染的,二者是层次关系不是替代关系。
Q2:怎么判断一个项目该用 Graphics View? 核心看"图形是否需要作为对象被选中/拖拽/碰撞/单独更新"。需要 → Graphics View;只是画个静态图表 → QPainter 或 QCustomPlot/QtCharts。
Q3:Graphics View 和 Qt Model/View 有什么关系? 没有直接关系,名字像但问题域不同:Model/View 管"数据如何在表格/树/列表展示",Graphics View 管"图形对象如何在自由画布上摆放交互"。二者可以结合(Model 存数据,Graphics View 画图),但不是同一层的东西。
Q4:图元数量多大需要优化? 经验值:几千个简单图元正常流畅;上万图元开始要开 NoIndex/调整重绘策略;10 万+ 简单图元需考虑 GPU 视口或放弃 Graphics View。
Q5:怎么实现"图钉/标注文字"不被缩放? setFlag(QGraphicsItem::ItemIgnoresTransformations, true),图元保持屏幕尺寸不变。
Q6:怎么让线宽不随缩放变粗? 用 cosmetic pen:QPen pen(...); pen.setCosmetic(true);。
Q7:拖动图元时怎么实时更新连到它的连线? 图元设置 ItemSendsGeometryChanges,重写 itemChange(ItemPositionHasChanged, ...),在其中重算连线端点并 update()。
Q8:怎么实现框选? view->setDragMode(QGraphicsView::RubberBandDrag),然后监听场景 selectionChanged;配合图元 ItemIsSelectable。
Q9:怎么让场景支持"无限画布"? 设置 scene->setSceneRect 为足够大的范围,或动态扩展(在图元移动越界时扩大 rect)。注意 BSP 索引在超大 rect 下命中性能会下降。
Q10:图元上的文本怎么支持中文输入? 图元需要 setFlag(ItemAcceptsInputMethod) 并实现 inputMethodEvent;或者把 QGraphicsTextItem 设为可编辑(setTextInteractionFlags(Qt::TextEditorInteraction))。
Q11:多个视图观察同一场景会冲突吗? 不会。同一场景多视图各自独立缩放平移(鹰眼导航原理);事件分发时视图自动换算坐标,互不干扰。
Q12:图元可以嵌入普通 QWidget 吗? 方向反了:是把 QWidget 嵌入场景------用 scene->addWidget(new QLineEdit(...)) 返回 QGraphicsProxyWidget。反向(在 widget 上放图元)不是 Graphics View 的职责。
Q13:Graphics View 支持 GPU 加速吗? 视图可以 setViewport(new QOpenGLWidget) 走 OpenGL 渲染管线(Qt5.4+ 稳定可用),但图元命中/管理仍是 CPU 的;纯 GPU 大批量绘制建议自定义 OpenGL 方案。
Q14:QGraphicsItem 和 QGraphicsObject 怎么选? 需要信号槽/属性动画/定时器/QML 集成 → QGraphicsObject;纯绘制轻量图元 → QGraphicsItem。
结语
Graphics View 是 Qt 里"性价比"极高的框架:学习成本不高,却能把流程图编辑器、地图查看器、监控大屏、游戏地图这类项目从"手搓几千行命中测试代码"解放成"几十行配置 + 自定义图元"。
记住三条主线,你就掌握了 80%:
- 三角架构:Scene 管世界、View 管镜头、Item 管图形;
- 坐标三件套:Item → Scene → View,一切交互先换算;
- 自定义三虚函数:boundingRect 定疆域、paint 定长相、shape 定轮廓。