
🫧 励志不掉头发的内向程序员 :个人主页
✨️ 个人专栏: 《C++语言》《Linux学习》
🌅偶尔悲伤,偶尔被幸福所完善
👓️博主简介:

文章目录
- 前言
- 一、RS_Line创建之后,为什么还不能直接显示?
- 二、RS_Line如何进入图形容器?
-
- [2.1、创建出来的 RS_Line 处于什么状态?](#2.1、创建出来的 RS_Line 处于什么状态?)
- 2.2、container到底是什么?
- 2.3、为什么一定要加入容器?
- 2.4、Graphic和EntityContainer是什么关系?
- 2.5、为什么不用vector直接保存?
- 2.6、加入容器后是不是马上显示?
- 三、图形容器变化后,视图为什么需要更新?
-
- [3.1、加入图形容器 ≠ 立即显示](#3.1、加入图形容器 ≠ 立即显示)
- [3.2、为什么 CAD 需要单独的刷新机制?](#3.2、为什么 CAD 需要单独的刷新机制?)
- 3.3、谁负责通知视图重新绘制?
- 3.4、Qt为什么不会自动发现数据变化?
- 3.5、Qt中的更新流程
- 3.6、为什么不直接在addEntity里面绘制?
- 四、RS_Line如何交给绘图接口?
- 总结
前言
在上一篇文章中,我们分析了 RS_ActionDrawLine 如何获取起点和终点,并通过 new RS_Line(startPoint, endPoint) 创建直线实体,最后将其加入当前图形容器。至此,用户的鼠标操作已经转化为 CAD 中真实存在的图形数据。但一个新的问题随之出现:RS_Line 创建完成并加入 Graphic 之后,为什么我们能够在屏幕上看到这条直线?从代码层面看,创建实体只是完成了图形数据的构建;而从用户体验来看,绘图操作还必须经过视图更新、实体绘制、坐标转换以及底层绘图等环节,才能最终呈现为屏幕上的像素。
因此,本文将继续沿着"从鼠标点击到屏幕像素"的主线,分析 RS_Line 从图形实体到屏幕显示的过程,重点梳理 Graphic、GraphicView、RS_Line 绘制接口以及 RS_Painter 之间的协作关系,理解 LibreCAD 如何将几何数据转换为可见图形。通过这一过程,可以进一步认识到:CAD 软件中的图形并不是被直接画在屏幕上的,而是先作为数据实体被管理,再由视图和绘图系统根据当前坐标变换进行显示。

一、RS_Line创建之后,为什么还不能直接显示?
上一篇文章中,我们已经分析了 RS_ActionDrawLine 如何在第二次鼠标点击后创建 RS_Line:
cpp
RS_Line* line = new RS_Line(container, pPoints->data);
此时,程序已经得到了一个表示直线的实体对象。例如:
test
起点:(10, 20)
终点:(100, 80)
从几何意义上看,这条直线已经存在了。但是,"直线对象已经创建"并不等于"直线已经显示在屏幕上"。这是理解 LibreCAD 绘图架构时必须区分的两个概念。
1.1、创建对象,只是完成了图形数据的构造
执行:
cpp
RS_Line* line = new RS_Line(container, pPoints->data);
主要完成的是对象创建。可以理解为:

此时,程序内存中已经存在一个 RS_Line对象。但它还只是一个几何实体对象,并不是一个已经绘制到窗口上的像素图像。可以把它类比为创建一个学生对象 ≠ 把学生的照片显示在屏幕上,同样创建RS_Line对象 ≠ 把直线绘制到屏幕,创建对象解决的是图形是什么?而屏幕显示解决的是图形应该显示在哪里?应该使用什么颜色、线宽和线型?最终如何交给绘图设备?这两个问题属于不同的处理阶段。
1.2、RS_Line保存的是几何信息,而不是屏幕像素
RS_Line的核心数据是两个端点:
cpp
struct RS_LineData {
RS_Vector startpoint;
RS_Vector endpoint;
};
例如:
test
startpoint = (10, 20)
endpoint = (100, 80)
这些坐标属于 CAD 图形空间。它们描述的是直线在 CAD 世界中的几何位置。而屏幕使用的是窗口坐标,例如:
test
起点:(250, 180)
终点:(430, 300)
因此,RS_Line不能直接把:
test
(10, 20)
(100, 80)
当作屏幕上的像素位置使用。程序还需要根据当前视图的:
test
缩放比例;
平移位置;
窗口大小;
坐标系方向;
将 CAD 坐标转换为屏幕坐标。所以,创建RS_Line之后,至少还需要经过:
几何数据 -> 视图转换 -> 屏幕坐标 -> 绘图接口
1.3、RS_Line还没有成为当前图形的一部分
创建对象之后,代码还需要执行:
cpp
container->addEntity(line);
这一步非常重要。因为:
cpp
new RS_Line(...)
只是创建了一个独立的实体对象。而:
cpp
container->addEntity(line);
才是把它加入当前图形的数据结构中。整体过程可以表示为:
创建RS_Line -> 得到一个独立实体 -> 加入RS_EntityContainer -> 保存到实体列表 -> 成为当前Graphic中的正式图形
如果没有加入图形容器,那么这个对象就没有真正进入当前图形的实体管理体系。后续的:
test
显示;
选择;
移动;
删除;
保存;
都无法按照正常的图形实体流程处理它。因此实体创建解决"对象是否存在",实体加入容器解决"对象是否属于当前图形"。
1.4、图形容器也不负责直接绘制屏幕
需要注意,RS_EntityContainer的主要职责是管理实体,而不是直接操作屏幕。例如:
cpp
void RS_EntityContainer::addEntity(RS_Entity* entity)
{
if (!entity) {
return;
}
entities.append(entity);
}
这里的核心操作是:
cpp
entities.append(entity);
也就是把实体保存到容器中。它完成的是:
实体对象 -> 加入实体列表
而不是:
实体对象 -> 调用QPainter -> 绘制屏幕
因此,不能简单理解为:
addEntity() -> 立即画到屏幕
更准确的理解应该是:
addEntity() -> 修改图形数据 -> 后续由视图和绘制流程读取这些数据 -> 最终完成显示
这体现了 LibreCAD 中比较明显的分层设计:
| 模块 | 主要职责 |
|---|---|
| RS_Line | 描述一条直线 |
| RS_EntityContainer | 管理实体对象 |
| Graphic | 管理当前图形数据 |
| GraphicView | 管理图形的显示过程 |
| RS_Painter | 封装绘图操作 |
| QPainter | 执行底层绘制 |
1.5、图形数据发生变化后,还需要进入重绘流程
当执行:
cpp
container->addEntity(line);
时,图形数据发生了变化:
test
原来 Graphic:RS_Circle + RS_Arc;
加入直线后 Graphic:RS_Circle + RS_Arc + RS_Line;
但是,屏幕上当前显示的内容可能仍然是上一次绘制的结果。因此,程序还需要让视图知道,当前图形数据已经发生变化,需要重新绘制。概念上可以表示为:
加入RS_Line -> Graphic数据发生变化 -> 通知视图更新 -> GraphicView进入重绘流程 -> 重新读取图形实体 -> 绘制新的图形内容
这里要特别区分修改图形数据和刷新屏幕显示,它们不是同一个操作。可以类比为修改数据库中的数据 ≠浏览器已经自动显示最新数据。同样加入RS_Line ≠ 屏幕已经完成重新绘制
1.6、屏幕显示还需要经过视图和绘图接口
一条正式直线最终显示在屏幕上,通常需要经过多个阶段:
RS_Line创建 -> 加入实体容器 -> Graphic保存图形 -> 视图收到更新请求 -> GraphicView组织绘制 -> 读取RS_Line几何数据 -> CAD坐标转换为屏幕坐标 -> 设置绘图属性 -> RS_Painter -> QPainter -> 屏幕显示
因此,RS_Line并不是直接负责整个显示过程。它主要负责提供:
test
这是一条什么直线?
直线的起点在哪里?
直线的终点在哪里?
直线具有什么绘图属性?
而其他模块负责:
test
什么时候绘制?
在哪里绘制?
如何转换坐标?
使用什么绘图工具?
如何刷新窗口?
1.7、为什么不能把所有功能都写进RS_Line?
如果让RS_Line自己完成:
test
坐标转换;
窗口刷新;
鼠标事件处理;
图形容器管理;
Qt绘制;
文件保存;
那么一个实体类就会承担过多职责。如果这样操作会导致:
test
几何数据和界面显示紧密耦合;
实体难以复用;
不同图形实体之间难以统一处理;
修改绘图方式时需要修改大量实体类;
后续增加选择、编辑和保存功能会更加复杂。
而 LibreCAD采用分层方式:
test
RS_Line -> 只负责描述实体
Graphic -> 负责管理实体
GraphicView -> 负责组织显示
RS_Painter -> 负责封装绘图
QPainter -> 负责底层绘制
这样每个模块都有明确的职责。
二、RS_Line如何进入图形容器?
上一节我们分析了 RS_Line创建之后为什么不能直接显示?原因在于创建RS_Line 只是生成一个几何实体对象,它还没有真正成为 LibreCAD 当前图形的一部分。因此,创建完成之后,下一步非常关键 RS_Line如何被加入到 LibreCAD 的图形管理体系中?这一步对应的就是:
cpp
container->addEntity(line);
也就是实体进入图形容器的过程。
2.1、创建出来的 RS_Line 处于什么状态?
首先回顾一下前面的代码:
cpp
RS_Line* line = new RS_Line(container, lineData);
执行之后,内存中已经存在 RS_Line对象,此时内存中有了 RS_Line,但是当前CAD图形还没有 RS_Line 也就是说,它只是一个普通的 C++ 对象。类似:
cpp
Student* s = new Student();
只是创建了一个学生对象。但是学校名单里还没有这个学生同理:
cpp
new RS_Line()
只是创建实体。还需要:
cpp
container->addEntity(line);
把它加入 CAD 图形结构。
2.2、container到底是什么?
在创建 RS_Line 时,我们看到:
cpp
RS_Line* line = new RS_Line(container, lineData);
这里有一个参数 container,它非常重要。它表示当前这个实体应该属于哪个图形容器。在 LibreCAD 中,图形不是直接由 RS_Line、RS_Circle 等对象散乱保存,而是通过容器统一管理。结构大概如下:

也就是说所有图形实体最终都挂在容器下面。
2.3、为什么一定要加入容器?
可能有人会问,既然 RS_Line已经有起点和终点了,为什么不能直接绘制?原因是 CAD 软件不是只需要"显示"。后续还有大量操作,例如:
test
选择
用户点击直线:
鼠标 -> 查找附近实体 -> 找到RS_Line
移动
用户拖动直线
删除
用户按Delete
保存DXF
如果 RS_Line没有加入容器,系统不知道它存在那么:
test
无法选择;
无法保存;
无法编辑;
无法参与绘制。
所以加入容器不是为了显示,而是为了让实体进入 CAD 的生命周期。
2.4、Graphic和EntityContainer是什么关系?
这里容易混淆,为什么有:
test
Graphic
EntityContainer
两个东西?可以简单理解:
Graphic 代表:当前打开的CAD图形。例如 test.dxf 里面所有内容:
test
线
圆
文字
图层
块
都属于 Graphic。
EntityContainer 代表:管理这些实体对象的容器。例如:

所以:
test
Graphic -> 管理整个图形
EntityContainer -> 管理具体实体
2.5、为什么不用vector直接保存?
可能有人会想为什么不:
cpp
std::vector<RS_Line*> lines;
然后保存直线?因为 CAD 中实体类型很多:
test
RS_Line
RS_Circle
RS_Arc
RS_Text
RS_Block
...
如果使用:
cpp
vector<RS_Line*>
只能保存直线。而 LibreCAD 使用基类 RS_Entity,例如:
RS_Entity
|
±-- RS_Line
|
±-- RS_Circle
|
±-- RS_Arc
因此容器可以保存:
cpp
QList<RS_Entity*>
实现了统一管理。这也是面向对象设计中的:多态。
2.6、加入容器后是不是马上显示?
不是。这是非常容易误解的地方。执行:
cpp
container->addEntity(line);
之后发生的是内存数据变化,不是屏幕绘制,流程应该是:
addEntity() -> 实体进入Graphic -> 通知视图更新 -> GraphicView重新绘制 -> 读取EntityContainer中的实体 -> 调用实体绘制函数 -> 显示到屏幕
所以:
cpp
addEntity()
解决图形数据在哪里?而:
cpp
RS_Painter + QPainter
解决图形如何显示?这两个阶段必须分开理解。
三、图形容器变化后,视图为什么需要更新?
我们了解了 RS_Line 如何进入图形容器,此时,从数据层面来看,一条新的直线已经存在于 LibreCAD 内部。但是问题来了:为什么用户此时还需要看到屏幕上的直线?addEntity()之后,屏幕是如何知道图形发生变化的?
这就涉及 CAD 软件中的一个重要概念:图形数据管理和屏幕显示是两个独立的过程。
3.1、加入图形容器 ≠ 立即显示
很多刚接触 GUI 程序的人容易产生一个误解:
cpp
container->addEntity(line);
执行之后 RS_Line,屏幕出现直线,实际上并不是这样。addEntity() 做的事情只是内存中的图形数据发生变化。变化的是当前 CAD 文档中的实体数量。但是屏幕上显示的内容是窗口像素,并不会自动改变。可以类比网页修改数据库,users表增加一条数据并不会导致浏览器页面自动刷新中间还需要:
test
通知页面更新
重新读取数据
重新渲染
CAD绘图也是一样:
修改图形数据 -> 请求界面更新 -> 重新绘制 -> 屏幕显示新图形
3.2、为什么 CAD 需要单独的刷新机制?
因为 CAD 软件不是简单画一张图片。例如当前屏幕 Graphic:
test
RS_Line
RS_Circle
RS_Arc
RS_Text
用户进行:
test
缩放
放大2倍
实体数据没有变化,变化的是:
test
显示方式
平移
用户拖动画布,图形移动到窗口另一位置,实际上 RS_Line 坐标没有变化,变化的是:
test
视图偏移量
改变窗口大小
例如 800×600 变成 1920×1080,图形数据依旧不变。所以 LibreCAD 必须把图形数据和屏幕显示分开。
3.3、谁负责通知视图重新绘制?
前面我们知道:
RS_Line -> RS_EntityContainer -> Graphic
这里属于模型层(Model),负责保存:
test
直线坐标;
圆的信息;
图层;
属性。
但是显示属于视图层(View),也就是 GraphicView,它负责:
test
当前显示范围;
缩放比例;
坐标转换;
绘制请求。
因此两者之间需要一个连接:
Graphic数据变化 -> 通知 -> GraphicView -> 重新绘制
3.4、Qt为什么不会自动发现数据变化?
这里涉及 Qt 的设计思想。Qt 的绘制不是:
数据改变 -> 自动找到窗口 -> 立即绘制
而是:
数据改变 -> 程序请求更新 -> Qt事件系统安排绘 -> paintEvent() -> 完成显示
原因是如果每修改一次数据就立即绘制,例如加载10000个实体,可能变成:
test
添加一个实体
绘制一次
添加一个实体
绘制一次
添加一个实体
绘制一次
...
效率非常低。所以 GUI 框架通常采用延迟更新机制。
3.5、Qt中的更新流程
在 Qt 中,常见流程:
调用 update() -> Qt记录需要刷新 -> 进入事件循环 -> 产生绘制事件 -> 调用 paintEvent() -> 重新绘制窗口
注意:
update()并不是马上画。它只是告诉 Qt,这个窗口需要重新绘制。真正绘制发生在 paintEvent() 里面。
3.6、为什么不直接在addEntity里面绘制?
可能有人会想为什么不这样:
cpp
void addEntity(RS_Entity* entity)
{
entities.append(entity);
entity->draw();
}
这样不是更简单?实际上这样设计会产生很多问题。
问题1:实体不知道当前视图状态
例如:当前缩放比例 = 2,实体本身不知道:
test
应该放大多少
问题2:多个实体绘制顺序难管理
CAD中:
test
线
圆
文字
块
都有绘制顺序。
应该由 GraphicView 统一控制。
问题2:无法利用Qt刷新机制
Qt希望:数据变化 -> 统一刷新 -> 一次绘制
而不是每个对象自己画自己,因此 LibreCAD采用:
数据层(RS_Line、RS_Arc...) -> 管理层(Graphic)-> 显示层(GraphicView)-> 绘制层(RS_Painter)-> Qt -> QPainter
四、RS_Line如何交给绘图接口?
到了现在,我们 LibreCAD 已经知道:直线是什么、直线在哪里。但是还有最后一个问题:谁真正把这条直线画出来?这一节我们继续追踪:
RS_Line -> draw() -> RS_Painter -> QPainter -> 屏幕显示
4.1、RS_Line为什么不直接调用QPainter?
很多初学者看到绘图代码,可能会想到,既然最终是 Qt 绘制:
cpp
QPainter::drawLine()
那么为什么不直接:
cpp
RS_Line::draw(QPainter*)
{
painter.drawLine(start,end);
}
这样不是最简单吗?实际上 LibreCAD 没有这么设计。原因是图形实体和底层绘图接口不应该直接耦合。如果 RS_Line 直接依赖 Qt:
RS_Line -> QPainter
那么:
test
RS_Line必须知道Qt;
几何实体和界面绑定;
以后更换绘图后端困难。例如未来:
test
OpenGL渲染;
Vulkan;
离屏渲染;
打印输出;
那么所有实体类都需要修改。所以 LibreCAD 引入了一层:
RS_Line -> RS_Painter -> QPainter
4.2、RS_Painter是什么?
RS_Painter可以理解为 LibreCAD自己的绘图抽象层。它封装了底层绘制操作。
例如实体只需要告诉,我要画一条线
test
起点:(x1,y1)
终点:(x2,y2)
而不用关心:
test
用什么API;
如何设置画笔;
如何处理设备。
4.3、绘制流程从哪里开始?
回到上一节 GraphicView 收到 Qt 绘制事件 paintEvent(),然后开始刷新整个绘图区。大致流程:
paintEvent() -> GraphicView开始绘制 -> 遍历Graphic中的实体 -> 调用实体draw() -> RS_Line执行绘制 -> RS_Painter绘制线段 -> QPainter输出
也就是说不是 RS_Line 主动跑去绘制,而是 GraphicView 需要刷新,主动询问每个实体你应该怎么画?
4.4、RS_Entity为什么有draw()?
在LibreCAD中,所有图形实体都有共同基类 RS_Entity
例如:

每种实体都有自己的绘制方式。
例如:
直线 RS_Line:
两个点 -> drawLine()
圆 RS_Circle:
圆心+半径 -> drawEllipse()
文字 RS_Text:
字符串+位置 -> drawText()
因此基类定义统一接口:
cpp
virtual void draw(RS_Painter* painter);
然后不同实体实现自己的绘制。
4.5、RS_Line的draw()做什么?
假设当前绘制 RS_Line
test
start: (10,20)
end: (100,80)
调用:
cpp
line->draw(painter);
进入:
cpp
RS_Line::draw(RS_Painter* painter)
它主要完成:
第一步:读取自身数据
例如:
test
data.startpoint;
data.endpoint;
得到:
test
起点: (10,20)
终点: (100,80)
第二步调用RS_Painter绘制接口,类似:
cpp
painter->drawLine(
startpoint,
endpoint
);
此时 RS_Line 只负责:我是一条线,我提供两个点。
4.6、RS_Painter如何调用QPainter?
继续向下:
RS_Line -> RS_Painter::drawLine() -> QPainter::drawLine()
cpp
void RS_PainterQt::drawLine(const RS_Vector& p1, const RS_Vector& p2) {
QPainter::drawLine(toScreenX(p1.x), toScreenY(p1.y),
toScreenX(p2.x), toScreenY(p2.y));
}
Qt开始操作:
test
像素;
颜色;
线宽。
4.7、绘制属性从哪里来?
一条直线不仅有:
test
起点
终点
还需要:
test
颜色;
线宽;
线型。
例如:
test
红色
0.5mm
虚线
这些信息来自:
RS_Line -> Layer -> Pen -> RS_Painter
大概流程:
RS_Line(读取属性) -> 设置Painter状态 -> 开始绘制
类似:
cpp
void RS_ActionDrawLine::trigger() {
line->setLayerToActive();
line->setPenToActive();
}
void RS_Entity::setLayerToActive() {
RS_Graphic* graphic = getGraphic();
if (graphic) {
layer = graphic->getActiveLayer();
}
}
void RS_Entity::setPenToActive() {
RS_Document* doc = getDocument();
if (doc) {
pen = doc->getActivePen();
}
}
创建时就会自动通过对应的图层的各种属性来进行设置对应的属性。
总结
本文沿着"从鼠标点击到屏幕像素"的主线,梳理了 RS_Line 从创建到显示在屏幕上的完整过程。可以得出这样一条清晰的认识:
在 LibreCAD 中,new RS_Line(...) 只是完成了图形数据的构造,它保存的是 CAD 世界坐标系下的几何信息,而不是屏幕像素;只有通过 container->addEntity(line) 把实体加入图形容器后,它才真正成为当前图形的一部分。而图形容器本身只负责管理实体,并不直接绘制屏幕。
真正让直线显示出来的是 GraphicView 驱动的刷新流程:paintEvent 触发视图重绘,视图遍历图形中的实体并调用其 draw() 接口,实体再通过 RS_Painter 这一抽象绘图层,最终交给 Qt 的 QPainter 完成屏幕输出。颜色、线宽、线型等绘制属性则来自实体所在的图层和画笔设置。
这种设计的本质,就是把图形看作"先被管理的数据",再由视图和绘图系统根据当前坐标变换统一显示:
数据层(RS_Line、RS_Arc...)→ 管理层(Graphic)→ 显示层(GraphicView)→ 绘图层(RS_Painter)→ Qt(QPainter)
其中,实体负责"是什么",容器负责"属于谁",视图负责"何时画",RS_Painter 和 QPainter 负责"怎么画"。理解这四层各自的职责和协作关系,是读懂 LibreCAD 绘图机制的关键,也为后续分析更多实体类型以及视图交互打好了基础。
🎇坚持到这里已经很厉害啦,辛苦啦🎇 ʕ • ᴥ • ʔ づ♡ど
