🫧 励志不掉头发的内向程序员:个人主页
✨️ 个人专栏: 《 LibreCAD架构源码分析》
🌅偶尔悲伤,偶尔被幸福所完善
👓️博主简介:
前言
上一篇文章我们简单的了解了一些 LibreCAD 的架构方面的内容,以及去推测了功能是怎么运行的,从本章节开始,我们便来开始看源码啦,一边来验证自己的猜想,一边来感受工程代码是怎么把理论变成一款可以使用的软件的,这一章节我们便来从 LibreCAD 的核心文件夹 librecad 的 mian.cpp 的文件开始看起吧。
一、LibreCAD程序入口
我们来看看 LibreCAD 程序的入口,main.cpp。
这个文件里面的内容只有三百多行,是整个程序的启动入口和初始化中心,当用户双击 LibreCAD 或从命令行启动程序时,操作系统首先进入 main(),然后由它完成程序运行前的一系列准备工作,最后启动 Qt 的事件循环。如果我们不是打开软件,而是保存的cad文件,这个还是也会去试着解析文件来初始化文件的内容。
1.1、LibreCAD为什么需要命令行参数
这里的 main 函数的参数叫做命令行参数,我之前的文章有说过,这里面存储的就是在命令行去调用函数时在命令行后面加入的参数。
相信有 window 系统使用经验的人都知道,当我们打开一款软件去进行创作的时候,然后保存文件,当我们下次想要打开保存的文件,可以先打开对应的软件,一般软件里面有打开文件,然后找到对应路径和对应文件名后打开即可,但是一般大家不是这样做的,而是直接双击文件,这个时候我们也会直接打开对应软件,同时加载好了时我们想要的文件也就自动打开了,这就是命令行参数的作用。
有人就纳闷了,我也没有传递命令行参数啊,难道是计算机偷偷传递的吗?
的确是如此,当我们去双击打开一个文件的时候,我们的计算机回去查找对应文件的对应后缀,比如 LibreCAD 支持的 .dxf 后缀的文件,此时 Windows 会根据 .dxf 的文件关联找到 LibreCAD 的可执行程序,然后启动类似:
librecad.exe "C:/test/example.dxf"
于是进入 LibreCAD 的:
int main(int argc, char *argv[])
有的小伙伴们可能又有别的问题啦,那假如我们的电脑中有多个支持相同文件后缀的文件,此时打开文件,计算机怎么知道打开的是哪一个软件呢?
这就涉及到 Windows 的 文件关联(File Association)机制了。window系统中有对应文件的默认关联程序,保存在注册表中,当我们直接打开对应文件,此时就是默认关联程序打开该文件。当然,如果你不想要这个文件,你可以鼠标右键点击打开方式去更改,更改后以后这个文件对应的打开文件也就改变了。
LinbrCAD 的命令行参数还有例如:支持不同的启动模式、方便脚本和批量处理实现自动化处理等。
1.2、main()开头做了什么?
1、Qt版本检查
main 函数的第一条执行指令就是这个,这个作用很简单,就是查看 Qt 的版本的,看看 Qt 版本够不够新。
2、初始化命令行参数
我们应该已经大致了解了 LibreCAD 的命令行参数的作用,这里再来看看它是怎么初始化的。
qMin(argc, 2);
是指选命令行参数的数量和 2 中的较小值,所以说这个循环最多只检查argv[0]、argv[1],而 argv[0] 肯定是librecad.exe,也就是说,这里只关心程序自身名称和第一个命令行参数。
循环中:
QString arg(argv[i]);
把 C 风格字符串转换成 Qt 的字符串类型,方便后面使用 Qt 提供的字符串比较函数。
第一个 if:
if (i == 0) {
arg = QFileInfo(QFile::decodeName(argv[i])).baseName();
}
判断当前是不是第一个参数。这里的“第一个参数”需要特别注意 argv[0] 并不是用户真正传入的参数,而是程序自身的名称/路径。例如:
argv[0] = "C:/LibreCAD/librecad.exe"
argv[1] = "dxf2pdf"
所以这里专门处理 argv[0]。而 QFile::decodeName(argv[i]) 它的作用是:把 argv[i] 中的字节数据转换成 Qt 的字符串编码。
arg = QFileInfo(QFile::decodeName(argv[i])).baseName();
这里实际上连续做了两件事情。假设:argv[0] ="C:/Program Files/LibreCAD/librecad.exe"。 首先:QFile::decodeName(argv[i]) 得到 Qt 可以处理的文件名字符串。然后:QFileInfo(...) 把这个字符串当成一个文件路径。最后:.baseName() 获取文件的基本名称,不包含路径和扩展名。例如: C:/Program Files/LibreCAD/librecad.exe -> librecad.exe -> librecad,所以最终:arg = "librecad";
第二个 if:
if (arg.compare("dxf2pdf") == 0) {
return console_dxf2pdf(argc, argv);
}
这里是在判断:arg 是否等于 "dxf2pdf"。QString::compare() 返回 0,表示两个字符串相等。如果发现:arg == "dxf2pdf",那么:return console_dxf2pdf(argc, argv); 直接调用 LibreCAD 的:console_dxf2pdf(),进入 DXF -> PDF 的命令行转换模式。而且这里有一个很重要的点:return 会直接结束 main()。也就是说:发现 dxf2pdf ->console_dxf2pdf() -> return -> main() 结束,不会继续执行后面的:QApplication、MainWindow、事件循环,因此它实际上支持 LibreCAD 的一种命令行模式。
二、QApplication:Qt应用程序的创建
LibreCAD 是基于 Qt 框架开发的图形界面应用程序,因此程序启动时首先需要创建 Qt 应用程序对象,为后续的窗口创建、事件处理和 GUI 操作提供基础运行环境。跟着执行流我们接着往下面来看。
LibreCAD 将 Qt 提供的底层应用程序能力封装到自身的应用程序类中,使 LC_Application 成为 LibreCAD 与 Qt 框架之间的桥梁。这样既可以复用 Qt 提供的 GUI 和事件处理能力,又可以在 LC_Application 中加入 LibreCAD 自己的应用级初始化逻辑,从而避免将大量初始化代码直接堆积在 main() 中。
接下来的三行:
QCoreApplication::setOrganizationName("LibreCAD");
QCoreApplication::setApplicationName("LibreCAD");
QCoreApplication::setApplicationVersion(XSTR(LC_VERSION));
则是在设置应用程序的基本信息,
- 设置组织名称:组织 = LibreCAD;
- 设置应用程序名称:应用程序 = LibreCAD;
- 设置应用程序版本:版本 = LC_VERSION;
这些信息后面可能被 Qt 的配置系统、日志、关于对话框等功能使用。
三、LibreCAD初始化流程
在上面的阶段我们解决了“LibreCAD 这个程序本身如何运行?”这一个问题。接下来便是创建 LibreCAD 的核心运行环境和界面啦,但是在这两个流程之间还有大概两百行代码,这些代码的主要内容是在创建主窗口之前,LibreCAD 还需要完成一些全局运行环境的初始化,包括配置管理、系统路径、字体以及调试环境等。这些模块为后续 GUI 和 CAD 核心功能提供基础支持。这里不会细讲,我们主要来看看一些比较重要的内容。
1、QSettings
QSettings settings;
QSettings 是 Qt 提供的配置管理类,用来保存和读取应用程序的设置。例如 LibreCAD 中用户可能会设置:窗口大小、 工具栏状态、绘图单位、用户偏好、最近使用的设置、界面相关配置。程序关闭后,这些设置如果需要保留,就可以通过 QSettings 保存。
bool first_load = settings.value("Startup/FirstLoad", 1).toBool();
QString unit = settings.value("Defaults/Unit", "Invalid").toString();
bool show_splash = settings.value("Startup/ShowSplash", 1).toBool();
QString lang = settings.value("Language", "en").toString();
QString langCmd = settings.value("LanguageCmd", "en").toString();
int windowWidth = settings.value("WindowWidth", 1024).toInt();
int windowHeight = settings.value("WindowHeight", 1024).toInt();
int windowX = settings.value("WindowX", 32).toInt();
int windowY = settings.value("WindowY", 32).toInt();
settings.setValue("UseQtFileOpenDialog", QVariant(1));
settings.setValue("UseQtFileOpenDialog", QVariant(0));
bool maximize = settings.value("Startup/Maximize", 0).toBool();
这些是 LibreCAD 在启动阶段通过 QSettings 读取上一次运行保存的用户配置,并将这些配置转换成程序内部使用的变量,为后续主窗口创建和运行环境初始化提供参数。
2、handleArgs(argc, argv, argClean)
QStringList fileList = handleArgs(argc, argv, argClean);
它是连接 操作系统启动参数 → LibreCAD 内部文件处理 的一个关键节点。handleArgs(argc, argv, argClean) 负责处理命令行参数,并返回一个 QStringList。然后:QStringList fileList = 把处理后的结果保存到 fileList 中。简单来说:解析 LibreCAD 启动时传入的命令行参数,并提取其中需要打开或处理的文件列表。
librecad.exe test1.dxf test2.dxf,大致对应:
argc = 3
argv[0] = librecad.exe
argv[1] = test1.dxf
argv[2] = test2.dxf
handleArgs() 做了一层封装,LibreCAD 没有直接在 main() 里面写大量:
argv[1]
argv[2]
argv[3]
进行判断,而是把参数处理封装到了:handleArgs() 里面。于是: 操作系统 -> argc / argv -> handleArgs() -> 参数解析、过滤 -> QStringList fileList -> LibreCAD后续打开文件。
而 handleArgs() 接收的 argClean 是一个 QList,用于记录已经被前置逻辑处理过的命令行参数下标。在遍历 argv 时,通过 argClean.indexOf(i) 判断当前参数是否已经处理,如果已经存在于列表中,则通过 continue 跳过。这样可以避免同一个命令行参数被多个解析阶段重复处理,同时将未处理的普通参数继续交给 handleArgs(),最终提取为文件路径列表。
这样我们就得到了处理好的文件列表了,等到后面再去通过这个文件列表去打开我们对应的图纸,返回 QStringList 是因为用户可能一次打开多个文件。
四、MainWindow:主窗口的创建
完成 Qt 应用程序环境、配置以及命令行参数等初始化后,LibreCAD 开始创建程序的主窗口。
QC_ApplicationWindow appWin;
QC_ApplicationWindow 是 LibreCAD GUI 层的主窗口类,负责组织整个 CAD 应用程序的用户界面。菜单栏、工具栏、绘图区、图层管理器、命令行和状态栏等界面组件都围绕主窗口进行组织。
从架构上看,QC_ApplicationWindow 位于应用程序基础环境和具体 GUI 组件之间,承担着连接和协调各个界面模块的作用。它本身并不直接负责 CAD 图形实体的核心数据处理,而是通过组合不同的 GUI 和 CAD 功能模块,共同构成完整的 CAD 编辑环境。我们来看看这个对象的构造吧。
QC_ApplicationWindow 对象在 LibreCAD中充当“协调者”的角色,例如用户去点击菜单就会激活对应的QC_ApplicationWindow 对象,然后找到对应 Action,再去执行CAD相关逻辑。它并不是把所有事情都自己完成,而是把不同功能模块组织起来,让它们协同工作。这体现的是比较典型的:分层 + 组合 + 职责分离而不是把所有功能都写进一个巨大类里。
五、UI界面的初始化
在创建 QC_ApplicationWindow 后,LibreCAD 开始初始化图形用户界面。主窗口负责组织菜单栏、工具栏、绘图区、图层管理器、命令行和状态栏等 UI 组件,并建立这些组件与对应功能模块之间的联系。
1、控件初始化
LC_ActionFactory a_factory(this, actionHandler);
在 QC_ApplicationWindow 构造中会去创建该对象,这个对象就是一个控件工厂。
两者配合起来: 用户点击菜单/工具栏 -> ActionHandler -> 对应 Action -> 执行 CAD 操作
以上便是建立了 LibreCAD 的各种控件的创建和初始化,同时进行好了连接,但是我们每个控件放到哪里,该怎么放却还是没有确立。
2、界面管理
我们在设置好了控件后,接下来就是给它们进行管理啦。
LC_WidgetFactory widget_factory(this, a_map, ag_manager);
这个便是用来管理我们每个控件的到底放到哪里以及界面应该长什么样子的对象啦。
我们先来看看它的参数,首先就是第 2 个参数a_map,从 LibreCAD 的命名来看,它和 Action Map 有关。你前面已经看到了:
LC_ActionFactory a_factory(this, actionHandler);
ActionFactory 建立了 Action 体系。而这里:
LC_WidgetFactory widget_factory(this, a_map, ag_manager);
又把: Action -> a_map -> Widget 联系起来。简单理解:a_map 用于让 UI 控件能够找到或关联对应的 Action。例如: 工具栏“画直线”按钮 -> a_map -> Line Action,这样点击按钮之后,就可以执行对应的 CAD 操作。
第 3个参数ag_manager,它一般可以理解为 Action Group Manager(Action 组管理器)。它主要用于对 Action 进行分组和管理。分组是因为 LibreCAD 中很多工具存在互斥关系。比如:绘图工具的直线、圆、矩形、弧等。用户选择“画直线”以后,可能需要让:直线工具 -> 当前激活、其他绘图工具 -> 非激活。这种情况下就需要对 Action 进行分组管理。所以可以粗略理解: ag_manager -> 管理 Action Group -> 控制相关 Action 的组织和状态。
经过这一阶段,LibreCAD 的 GUI 框架基本建立完成,为后续的用户交互和 CAD 图形操作提供界面基础。
六、GraphicView:CAD绘图区的创建
在 LibreCAD 的 UI 初始化过程中,GraphicView 是最核心的界面组件之一,它可以看作 CAD 图形的主要显示和交互区域。与菜单栏、工具栏等 UI 组件不同,GraphicView 不仅负责显示 Document 中的 CAD 实体,还负责接收鼠标、键盘等用户输入。
在图形显示方面,GraphicView 将 CAD 世界坐标经过视图变换转换为屏幕坐标,最终通过 Qt 的绘图机制呈现到屏幕上;在用户交互方面,则将鼠标点击、移动、缩放等事件转换为 CAD 系统可以处理的信息。
因此,GraphicView 在 LibreCAD 中承担了连接 CAD 数据、图形显示和用户交互 的重要作用。
它的创建,主要是在 QC_MdiWindow 这个对象中创建的,这个对象在初始化的时候就会去创建 GraphicView 对象。
int main {
if (!files_loaded)
{
appWin.slotFileNewNew();
}
}
qc_applicationwindow.cpp
void QC_ApplicationWindow::slotFileNewNew {
if (!slotFileNewHelper(fileName))
}
bool QC_ApplicationWindow::slotFileNewHelper {
w = slotFileNew();
}
QC_MDIWindow* QC_ApplicationWindow::slotFileNew(RS_Document* doc) {
QC_MDIWindow* w = new QC_MDIWindow(doc, mdiAreaCAD, 0);
}
这样我们便得到了我们的绘画区啦。
七、QApplication::exec():进入事件循环
完成前面的应用程序、配置、命令行参数以及主窗口等初始化之后,LibreCAD 最终需要进入 Qt 的事件循环。
int return_code = app.exec();
Qt GUI 程序采用事件驱动模型。进入事件循环后,程序会持续等待鼠标、键盘、窗口重绘等事件,并将这些事件分发给对应的 Qt 对象进行处理。例如用户在 GraphicView 中点击鼠标,会产生鼠标事件,经过 Qt 的事件分发机制后由相应的 LibreCAD 对象进行处理。
因此,app.exec() 是 LibreCAD 从启动初始化阶段进入持续运行阶段的关键节点。事件循环会一直运行,直到应用程序退出,此时 exec() 返回退出状态,main() 随之结束。
它标志着 LibreCAD 从启动初始化阶段进入了真正的运行阶段。 以上便是 main 中的操作,这里画了一张思维导图供大家理解:
总结
通过对 LibreCAD 程序启动流程的分析,我们可以看到,一个看似简单的 CAD 窗口背后,其实经历了一套完整的初始化过程。
程序从 main() 进入,首先建立 Qt 应用程序运行环境,随后完成 LibreCAD 自身相关资源与配置的初始化,并处理启动参数。在此基础上创建 MainWindow,由主窗口进一步组织菜单栏、工具栏、命令行、图层窗口以及 GraphicView 等核心界面组件。最后调用 QApplication::exec() 进入 Qt 事件循环,使程序开始持续接收和处理用户操作。整个启动过程可以概括为: main() -> QApplication -> LibreCAD环境初始化 -> MainWindow -> UI组件初始化 -> GraphicView -> QApplication::exec() -> Qt事件循环
至此,我们已经完成了从程序入口到 CAD 绘图界面建立的源码追踪。此时程序已经“准备就绪”,接下来真正有趣的问题也随之出现:当用户点击工具栏中的“画直线”按钮,LibreCAD又是如何识别这个操作,并最终进入对应的绘图逻辑的? 下一篇将继续沿着这条链路,从用户的一次点击出发,深入分析 LibreCAD 的 Action 与命令系统。谢谢大家的观看。