QCAD杂记
源代码及环境
url:https://github.com/qcad/qcad branch:master tag:v3.27.8.4 sha:13c1383e83828fc7120f1acc697e76bd201bdeba
Win10 MSVC15 Qt5.15.2 Qt6.4.1
配置编译
文档几乎为零,网络上讨论的内容也很少。当然,商业化的项目多少会在开源代码上些障碍以保证商业利益,开源代码开源部分基本处于放羊状态。Qt 从源代码编译也是十分困难的事情,需要对 Qt 有长时间的研究才能做到。
Qt5
Qt5 的配置只有 pro 文件,没有编写对应的 CMake 文件
QCAD 官网给的是用 qt5.13.2, 然而由于 qt 强推新版本,没有办法直接获取到旧版本,而手动编译旧版本不太可能,qt 的编译相当困难。使用目前还能获取到的 5.15.2 版本,官网记录的使用 qmake 的方法无法正常输出。qtcreator 可以正常配置,但是只能构建 src 下的目录,无法在 qtcreator 中运行或调试,会陷入无限循环的 pre-build 事件。
目测该问题的原因在于 pro 文件没有使用比较标准的,qt 官方的环境配置方式,而是简单的设置了手动拷贝文件的命令。而且各个项目之间没有正确的依赖。
构建成功后,手动配置环境后,可以成功的运行初始化。但是没有后续,由于无法调试,无法分析具体问题
Qt6
Qt6 提供了 CMake 文件,但是有很多问题,项目依赖,代码,环境设置,宏定义均有问题。这个CMake 看起来像是使用某种工具将 pro 文件自动转化得来的。
代码制定的是 C++17,然而使用的第三方库里却有 binary_function 这种早就被弃用的内容。
耗了不少功夫才成功的将代码编译运行调试起来。然而存在一个致命的问题——QCAD 使用了 qt 旧版的 jsEngine,这部分的内容还没在 Qt6 上解决,导致 QCAD 核心的脚本引擎没有办法运行起来,程序无法初始化该部分内容。
代码分析
公共静态成员变量
在 RVector 类中发现了个没见过的写法
1 | // in class |
代码的作用基本就是提供了一些用于判断的变量。好处是省去了一次构造函数。
但是个人不推荐这种写法,同样的作用不如
1 | // in class |
同样的作用,而且可以将 invalid 等变量内置,避免在输出成动态链接库时一些链接的问题。实际上由于原来的 CMake 写得有些问题,导致这部分的代码在动态库链接时就产生了问题。
js脚本系统
QCAD 搭载了一个 js 脚本系统,可以方便二次开发。然而目前看来有代码中有很多功能都使用了这个脚本系统来运行特定的脚本进行替代,例如 gui 的初始化等等。
虽然一个脚本系统确实是有助于二次开发,但是个人不是很推荐脚本系统深入到各个核心系统的运行中。效率是一方面,而开发效率又是另一方面。可以理解这样的初衷是想通过修改脚本来达到快速修改程序运行逻辑。
但是笔者实际的工作经验表明,这种做法,随着开发时间的不断推移,最后就会变成既需要维护脚本代码的烂摊子,又要维护C++代码的烂摊子,一项任务,双倍工作,四倍难度。
当然不是说脚本构思这种设计不好。只是从实际的角度来看,只有超大型的公司开发的超大型软件,例如 AutoDesk CAD,3ds Max 等软件才能稍微的把这些脚本系统利用起来,用户自行开发真正需求的自定义插件。小公司、小软件,不如完善开发文档开发流程,优化基础功能来满足大多数用户的需求来得实际。
而且说实话,没有类型提示的编程语言真的是让人讨厌。到处都是 any,到处找各种变量的实际类型真的很浪费时间。
绘制系统
代码基本集中在 RGraphicsViewImage.cpp
看看有没有比较出彩的地方。
总体来说渲染的流程和渲染对象系统都是比较经典的设计,各种东西比较完整。使用 Qt 的渲染系统,没看到有什么特殊的优化的地方。
代码中看样子是有做多线程渲染,但是并没有真正的实现,从残余的代码来看是通过排序后,按序划分到各个 QPainter
能够满足各种线型、不同缩放下的细分。但是感觉效率不会太高,内容少时还凑合,东西多点就会很卡。
绘制流程
- paintEntitiesMulti -> 绘制制定区域内的实体
- 获取一些颜色有关的配置
- 查询区域内的实体,并且按 Z 轴排序
- 遍历排序后的实体,获取所有 drawable 且遍历
- 检查是否需要重新生成 drawable 绘制的内容,判定的属性有 AlwaysRegen、AutoRegen 和 PixelSizeHint 和当前不符。包括一些射线,细分过大过小的曲线等
- 只要有一个 drawable 需要重新生成绘制内容则该 entity 重新生成全部内容
- 经过一系列的判断
- sceneQt->exportEntity(entityId:id, allBlocks:bool)
- 这里 sceneQt 算是一个 RExporter,可以看出无论是显示,还是打印或者导出文件,都算 RExporter 子类的动作
- entity->exportEntity()
- RExporter 基类定义了各种设置数据的接口,以 RArcEntity::exportEntity 为例就可以看到使用了 RExporter::setBrush 和 RExporter::exportArc, 将圆弧的数据写入到了 sceneQt 中
- 注意这里只是设置了数据,并没有真正的绘制
- 然后开始通过多线程绘制 entity list, 但是这里把多线程的代码注释了,实际还是单线程
- paintEntitiesThread(threadId, list, start, end)
- 这里值得注意的是,drawable 是和 entity 对象分开的。也就是说 entity 只拥有抽象的数据,drawable 由 RGraphicsScene 管理,感觉没什么必要,基本每个 entity 都有自己的 drawable,当 entity 的数量多起来时,通过 id 在 drawable map 中查找还是挺耗时间和内存的。
- 然后就针对各种不同 RGraphicsSceneDrawable::Type 进行绘制,粗略过了一下各种类型绘制的代码,都是使用 Qt 的绘制系统。感觉性能可能不会很高。感觉有点不一样的地方是,绘制变换内容需要使用 Transform 和 EndTransform 的 drawable 向 transform stack 中压入和弹出变换矩阵。
RGraphicsSceneDrawable 划分
1 | // in class |
Type 是用于表示绘制的内容——路径、图像、文字之类的。Mode 用于表示属于范围,比如说是辅助对象还是绘制对象等。感觉划分得比较粗,有挺多内容其实不太好实现——例如填充的内容该属于哪类,归属于 PainterPath ?
RPainterPath
继承自 QPainterPath,添加了一些 CAD 系统有关的内容。一部分设置直接使用了 QPen、QBrush,这部分内容确实是挺繁琐的,东西又多又杂,反正最后也是直接使用 QPainterEngine,直接用 Qt 的内容无缝对接也挺省事的。
数据位于一个 originalShapes:QList<QSharedPointer<RShape>>
关注下 RShape 就好
RShape 拥有的类型如下
1 | // in class |
比较常见的设计,只是类型上多了一些 XLine、Ray 等一般 CAD 会有,而平面绘制引擎没有的东西。
对象系统
基类是 RObject,使用 Id:int objectId作为索引。这里的 id 并不是全局唯一的。两个 entity 可以拥有同一个 id,具体会访问到哪个对象,要看 RDocument 中记录的是哪个。
属性储存使用的是一个 customProperties:QMap<QString, QVariantMap>
低属性时有一定的好出,占用空间小。极限情况可能会比较吃力。另外就是使用 QString 作为索引,可能会产生冲突。
另外对象系统管理得比较散乱,虽然有指定 document 的函数,但是没有和 RDocument 中联系起来,两边数据能否对应起来全看编码有没有出 bug。
RTTI 使用 enum 作为识别和区分。简单的设置。
GUI 交互
交互系统,当然这里指的是底层的框架,而不是产品经理设计操作逻辑。这个系统其实挺难设计的,特别是要能支撑复杂的操作逻辑的时候。
RAction
定义了一个 RAction 的基类,用于表示所有的指令。其中有一些鼠标事件,键盘事件的纯虚函数,事件的类型有不少是在 Qt 事件的基础上改造的。例如
class RMouseEvent: public QMouseEvent, public RInputEvent
但是这个基本只是个架子,实际没多少代码写在 cpp 里面,只有 RNavigationAction,是关于用鼠标移动页面的操作的。
大部分操作写在了 js,通过写一个转发事件到 js 的 REcmaAction 子类(REcmaAction 有关的代码是自动生成的)。例如一个三点画圆的功能,写在了 scripts\Draw\Arc\Arc3P.js 里,具体的逻辑看Arc3P.prototype.setState
和Arc3P.prototype.pickCoordinate
就知道了。挺简单的一个switch分支。
简单交互功能的代码没什么好看的。本来想看点复杂的,但是好像没有太过复杂的逻辑。倒是有一个 RAction 的栈,一层套一层的 js Action 组成了挺复杂的操作逻辑。这个分层实现倒是有点意思。而且对应的 gui 状态全由 js 脚本里定义
但是其实灵活度不高。有一些操作,例如编辑对象,通过的是 RRefPoint 这个类去实现的。而相关的管理是写在 RStorage 子类中
默认的 RAction 是 DefaultAction.js,从中可以分析出许多功能具体的实现方式。
ROperation
ROperation 则是定义了操作对象动作的基类,例如添加或移除一个 entity。但是这部分的代码比较少,都是些常见的操作,没看到有操作具体对象的代码。可能全放在了它的 ECMA 脚本系统上了。
事件系统
代码从 RMainWindowQt::event() 开始入手
使用了 Qt 作为 GUI 框架的程序,基本在代码里找下 QMainWindow 相关的 event 函数就好。可以看到事件运作流程是是
- 注册监听器
- 触发某一类型的事件
- event 中识别,再 notifyXXXXListeners)()
- 监听器运行绑定的函数
说实话,这用那么多的 dynamic_cast 感觉有点粗糙,还是在主event循环里
接着看主要的显示窗口类 RGraphicsViewQt 里的事件处理函数,传递的过程大致如下
RMainWindowQt -> RGraphicsViewQt -> RGriphicsScene -> RDocumentInterface -> CurrentAction
不过这里还不是太关键的内容,没显露出是怎么和 js 交互的。这部分绑定是由 qt-labs-qtscriptgenerator 生成的,比较复杂。
杂项
- 使用 id 查询返回到具体的对象是智能指针类型
- 同样使用了空间索引进行包围框查询
- entity 属性的设置直接使用了 RPropertyTypeId 和 QVariant。感觉编码上挺省事的,但是使用起来不太方便,没有类型提示的接口用起来很麻烦
- 注释里写了一些和没写没什么区别的 doxygen 格式的文档