用最接地气的比喻来说,整个过程就像你在玩一个"客厅摄像头直播"游戏。
- QGraphicsScene = 你家客厅(一个超级大的、无边界的空间)
- QGraphicsItem = 客厅里摆放的各种家具、玩具、挂画、灯......(这些东西才是真正要被看到的"内容")
- QGraphicsView = 你架在客厅里的一个摄像头 + 显示器(它决定观众最终在屏幕上看到什么)
关键一句话:客厅本身是不会出现在电视上的,只有摄像头拍到的画面才会显示。
整个绘制是怎么一步步发生的?
- 某件事触发了"需要重新画画面"
比如:
- 窗口第一次显示出来
- 你拖动了视图(像转动摄像头)
- 你滚轮缩放了(像变焦镜头)
- 你移动了某个家具(某个 Item 位置变了)
- 你手动调用了 update() 或 scene->invalidate()
2.摄像头(QGraphicsView)收到"要重画"的信号
它会先问自己:"我现在对着客厅的哪个区域?要拍多大的范围?"
它根据当前的
- 缩放比例
- 平移位置
- 旋转角度
计算出"我现在能看到客厅的哪一块矩形区域"(这个区域用场景坐标表示)
3.摄像头对客厅喊:"给我把这个区域的东西都画出来!"
4.客厅(QGraphicsScene)收到请求,开始按顺序整理和绘画
客厅会分几个大阶段画(从最底层画到最上层):
- 先画背景(你设置的纯色、网格、图片背景......)
- 然后画所有家具(也就是所有的 QGraphicsItem)
- 最后画前景(比如你自己加的水印、调试用的坐标尺......)
最关键、最花时间的其实是第二步------画家具
5.客厅怎么知道该画哪些家具、不该画哪些?
它会先看"摄像头要拍的这个矩形区域",然后快速找一找: "哪些家具哪怕只露一点点在这个区域里?"
(Qt 内部用一种叫 BSP 树的空间索引来快速找,通常比暴力遍历快非常多)
找到这些"可能要画到的家具"之后,客厅会把它们按 z 值从低到高 排好队(z 值大的会盖在 z 值小的上面,就像前景盖背景一样)
6.轮到每个家具自己上场表演了
客厅会一个接一个地把"画笔"交给每个家具,说:
"现在轮到你了,你画你自己吧!"
每个家具(QGraphicsItem)就会执行自己的 paint() 函数:
void paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *)
{
painter->setBrush(Qt::blue);
painter->drawEllipse(-20, -20, 40, 40); // 画一个圆
painter->drawText(0, 0, "我是小球");
}
注意:这时 painter 已经自动处于这个家具自己的坐标系里了,(0,0) 就是这个家具的中心点或你定义的原点,你不用管它在客厅的绝对位置。
7.所有家具都画完后
客厅把这一整张画好的"大图"交给摄像头
8.摄像头(View)把这张图显示到电视屏幕(也就是窗口)上
完成了!观众(用户)就看到画面了。
小结:
"场景管东西,视图管镜头"
- 东西(家具)放在场景(客厅)里
- 镜头(视图)决定拍哪里、怎么拍(放大缩小旋转平移)
- 要重画时 → 镜头告诉场景:给我看这个区域
- 场景按顺序(背景 → 家具按 z 排序 → 前景)画
- 每个家具自己负责把自己画出来
- 镜头把画好的画面显示到屏幕
常见坑:
- 忘记写 boundingRect() → 家具等于隐身,什么都看不到
- boundingRect() 写得太小 → 鼠标点不到、碰撞检测失效
- 在 paint() 里面做超级复杂的计算 → 界面直接卡死
- 改了数据却不调用 update() → 改了也看不见
往场景里一股脑塞几万几十万个小 Item → 不优化基本必崩