尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

【LibreCAD 2D架构】从鼠标点击到屏幕像素:LibreCAD绘图架构全链路解析之程序启动与GUI初始化

【LibreCAD 2D架构】从鼠标点击到屏幕像素:LibreCAD绘图架构全链路解析之程序启动与GUI初始化 励志不掉头发的内向程序员个人主页✨️ 个人专栏: 《C语言》《Linux学习》偶尔悲伤偶尔被幸福所完善️博主简介:文章目录前言一、LibreCAD程序入口1.1、LibreCAD为什么需要命令行参数1.2、main()开头做了什么1、Qt版本检查2、初始化命令行参数二、QApplicationQt应用程序的创建三、LibreCAD初始化流程1、QSettings2、handleArgs(argc, argv, argClean)四、MainWindow主窗口的创建五、UI界面的初始化1、控件初始化2、界面管理六、GraphicViewCAD绘图区的创建七、QApplication::exec()进入事件循环总结前言上一篇文章我们简单的了解了一些 LibreCAD 的架构方面的内容以及去推测了功能是怎么运行的从本章节开始我们便来开始看源码啦一边来验证自己的猜想一边来感受工程代码是怎么把理论变成一款可以使用的软件的这一章节我们便来从 LibreCAD 的核心文件夹 librecad 的 mian.cpp 的文件开始看起吧。一、LibreCAD程序入口我们来看看 LibreCAD 程序的入口main.cpp。这个文件里面的内容只有三百多行是整个程序的启动入口和初始化中心当用户双击 LibreCAD 或从命令行启动程序时操作系统首先进入 main()然后由它完成程序运行前的一系列准备工作最后启动 Qt 的事件循环。如果我们不是打开软件而是保存的cad文件这个还是也会去试着解析文件来初始化文件的内容。1.1、LibreCAD为什么需要命令行参数这里的 main 函数的参数叫做命令行参数我之前的文章有说过这里面存储的就是在命令行去调用函数时在命令行后面加入的参数。但是我们打开软件就直接打开软件不就行了吗还要在后面带上参数干什么呢或者说这些参数有什么用呢相信有 window 系统使用经验的人都知道当我们打开一款软件去进行创作的时候然后保存文件当我们下次想要打开保存的文件可以先打开对应的软件一般软件里面有打开文件然后找到对应路径和对应文件名后打开即可但是一般大家不是这样做的而是直接双击文件这个时候我们也会直接打开对应软件同时加载好了时我们想要的文件也就自动打开了这就是命令行参数的作用。有人就纳闷了我也没有传递命令行参数啊难道是计算机偷偷传递的吗的确是如此当我们去双击打开一个文件的时候我们的计算机回去查找对应文件的对应后缀比如 LibreCAD 支持的 .dxf 后缀的文件此时 Windows 会根据 .dxf 的文件关联找到 LibreCAD 的可执行程序然后启动类似librecad.exeC:/test/example.dxf于是进入 LibreCAD 的intmain(intargc,char*argv[])有的小伙伴们可能又有别的问题啦那假如我们的电脑中有多个支持相同文件后缀的文件此时打开文件计算机怎么知道打开的是哪一个软件呢这就涉及到 Windows 的 文件关联File Association机制了。window系统中有对应文件的默认关联程序保存在注册表中当我们直接打开对应文件此时就是默认关联程序打开该文件。当然如果你不想要这个文件你可以鼠标右键点击打开方式去更改更改后以后这个文件对应的打开文件也就改变了。这就是以 .doc 为后缀的文件计算机的执行策略。LinbrCAD 的命令行参数还有例如支持不同的启动模式、方便脚本和批量处理实现自动化处理等。1.2、main()开头做了什么1、Qt版本检查main 函数的第一条执行指令就是这个这个作用很简单就是查看 Qt 的版本的看看 Qt 版本够不够新。原因也比较简单因为代码里面的一些特性可能老版的 Qt 没法执行所以得检测是否可以正常执行的 Qt 版本。2、初始化命令行参数我们应该已经大致了解了 LibreCAD 的命令行参数的作用这里再来看看它是怎么初始化的。这个 for 循环中qMin(argc,2);是指选命令行参数的数量和 2 中的较小值所以说这个循环最多只检查argv[0]、argv[1]而 argv[0] 肯定是librecad.exe也就是说这里只关心程序自身名称和第一个命令行参数。循环中QStringarg(argv[i]);把 C 风格字符串转换成 Qt 的字符串类型方便后面使用 Qt 提供的字符串比较函数。第一个 ifif(i0){argQFileInfo(QFile::decodeName(argv[i])).baseName();}判断当前是不是第一个参数。这里的“第一个参数”需要特别注意 argv[0] 并不是用户真正传入的参数而是程序自身的名称/路径。例如argv[0]C:/LibreCAD/librecad.exeargv[1]dxf2pdf所以这里专门处理 argv[0]。而 QFile::decodeName(argv[i]) 它的作用是把 argv[i] 中的字节数据转换成 Qt 的字符串编码。argQFileInfo(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”;第二个 ifif(arg.compare(dxf2pdf)0){returnconsole_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 的一种命令行模式。二、QApplicationQt应用程序的创建LibreCAD 是基于 Qt 框架开发的图形界面应用程序因此程序启动时首先需要创建 Qt 应用程序对象为后续的窗口创建、事件处理和 GUI 操作提供基础运行环境。跟着执行流我们接着往下面来看。在 LibreCAD 中通过封装 LC_Application 完成 Qt 应用对象程序的创建。我们可以把光标放在我们想要知道的变量声明的位置然后按 F2 就能跳到对象声明的地方或者按住 ctrl 再拿鼠标点击也可以实现一样的效果。我们来看看 LC_Application 对象。可以看到它是通过继承 qapplication 得来的再来看看它的构造如果是在声明的地方按 F2 就会自动跳到定义的地方另外一种方法同理。可以看到这里的构造只有将参数传递给父类去构造它的父类。在这里将 qapplication 进行封装再去进行创建一个 LibreCAD 应用程序对象并初始化 LibreCAD/Qt 应用程序运行所需的基础环境的方式体现了面向对象的封装思想。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、QSettingsQSettings settings;QSettings 是 Qt 提供的配置管理类用来保存和读取应用程序的设置。例如 LibreCAD 中用户可能会设置窗口大小、工具栏状态、绘图单位、用户偏好、最近使用的设置、界面相关配置。程序关闭后这些设置如果需要保留就可以通过 QSettings 保存。boolfirst_loadsettings.value(Startup/FirstLoad,1).toBool();QString unitsettings.value(Defaults/Unit,Invalid).toString();boolshow_splashsettings.value(Startup/ShowSplash,1).toBool();QString langsettings.value(Language,en).toString();QString langCmdsettings.value(LanguageCmd,en).toString();intwindowWidthsettings.value(WindowWidth,1024).toInt();intwindowHeightsettings.value(WindowHeight,1024).toInt();intwindowXsettings.value(WindowX,32).toInt();intwindowYsettings.value(WindowY,32).toInt();settings.setValue(UseQtFileOpenDialog,QVariant(1));settings.setValue(UseQtFileOpenDialog,QVariant(0));boolmaximizesettings.value(Startup/Maximize,0).toBool();这些是 LibreCAD 在启动阶段通过 QSettings 读取上一次运行保存的用户配置并将这些配置转换成程序内部使用的变量为后续主窗口创建和运行环境初始化提供参数。2、handleArgs(argc, argv, argClean)QStringList fileListhandleArgs(argc,argv,argClean);它是连接 操作系统启动参数 → LibreCAD 内部文件处理 的一个关键节点。handleArgs(argc, argv, argClean) 负责处理命令行参数并返回一个 QStringList。然后QStringList fileList 把处理后的结果保存到 fileList 中。简单来说解析 LibreCAD 启动时传入的命令行参数并提取其中需要打开或处理的文件列表。最开始int main(int argc, char *argv[]) 操作系统启动 LibreCAD 时如果用户通过文件关联打开test.dxf 或者命令行执行librecad.exe test1.dxf、test2.dxf 那么这些信息都会进入argc、argv例如librecad.exe test1.dxf test2.dxf大致对应argc3argv[0]librecad.exe argv[1]test1.dxf argv[2]test2.dxfhandleArgs() 做了一层封装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 编辑环境。我们来看看这个对象的构造吧。这个对象继承了 MainWindowX 类所以说它在 Qt 提供的主窗口能力之上加入了自己的 CAD 应用逻辑。我们点开这个对象的文件去看它的构造我们会发现它的构造的内容也是十分多的大概有400行作用。这个构造函数中就会开始对我们所看到的图形界面来进行创建和初始化通过这个构造函数我们便能得知 LibreCAD 的界面长什么样、各个界面组件怎么组织而不是通过这些并不是 main() 一个个创建的。QC_ApplicationWindow 对象在 LibreCAD中充当“协调者”的角色例如用户去点击菜单就会激活对应的QC_ApplicationWindow 对象然后找到对应 Action再去执行CAD相关逻辑。它并不是把所有事情都自己完成而是把不同功能模块组织起来让它们协同工作。这体现的是比较典型的分层 组合 职责分离而不是把所有功能都写进一个巨大类里。五、UI界面的初始化在创建 QC_ApplicationWindow 后LibreCAD 开始初始化图形用户界面。主窗口负责组织菜单栏、工具栏、绘图区、图层管理器、命令行和状态栏等 UI 组件并建立这些组件与对应功能模块之间的联系。1、控件初始化LC_ActionFactorya_factory(this,actionHandler);在 QC_ApplicationWindow 构造中会去创建该对象这个对象就是一个控件工厂。它是专门用来创建和初始化各种控件的并且将这些控件对应的信号和对应的槽函数进行连接这样用户去点击不同的控件就能够去调用不同的槽函数去实现不同的功能了。而这个对象的第二个参数actionHandler从名字可以看出它是一个 Action 处理器。可以简单理解成LC_ActionFactory - 负责创建 Action-ActionHandler - 负责管理/处理 Action.两者配合起来用户点击菜单/工具栏 - ActionHandler - 对应 Action - 执行 CAD 操作以上便是建立了 LibreCAD 的各种控件的创建和初始化同时进行好了连接但是我们每个控件放到哪里该怎么放却还是没有确立。2、界面管理我们在设置好了控件后接下来就是给它们进行管理啦。LC_WidgetFactorywidget_factory(this,a_map,ag_manager);这个便是用来管理我们每个控件的到底放到哪里以及界面应该长什么样子的对象啦。我们先来看看它的参数首先就是第 2 个参数a_map从 LibreCAD 的命名来看它和 Action Map 有关。你前面已经看到了LC_ActionFactorya_factory(this,actionHandler);ActionFactory 建立了 Action 体系。而这里LC_WidgetFactorywidget_factory(this,a_map,ag_manager);又把Action - a_map - Widget 联系起来。简单理解a_map 用于让 UI 控件能够找到或关联对应的 Action。例如工具栏“画直线”按钮 - a_map - Line Action这样点击按钮之后就可以执行对应的 CAD 操作。第 3个参数ag_manager它一般可以理解为 Action Group ManagerAction 组管理器。它主要用于对 Action 进行分组和管理。分组是因为 LibreCAD 中很多工具存在互斥关系。比如绘图工具的直线、圆、矩形、弧等。用户选择“画直线”以后可能需要让直线工具 - 当前激活、其他绘图工具 - 非激活。这种情况下就需要对 Action 进行分组管理。所以可以粗略理解ag_manager - 管理 Action Group - 控制相关 Action 的组织和状态。经过这一阶段LibreCAD 的 GUI 框架基本建立完成为后续的用户交互和 CAD 图形操作提供界面基础。六、GraphicViewCAD绘图区的创建在 LibreCAD 的 UI 初始化过程中GraphicView 是最核心的界面组件之一它可以看作 CAD 图形的主要显示和交互区域。与菜单栏、工具栏等 UI 组件不同GraphicView 不仅负责显示 Document 中的 CAD 实体还负责接收鼠标、键盘等用户输入。在图形显示方面GraphicView 将 CAD 世界坐标经过视图变换转换为屏幕坐标最终通过 Qt 的绘图机制呈现到屏幕上在用户交互方面则将鼠标点击、移动、缩放等事件转换为 CAD 系统可以处理的信息。因此GraphicView 在 LibreCAD 中承担了连接 CAD 数据、图形显示和用户交互 的重要作用。它的创建主要是在 QC_MdiWindow 这个对象中创建的这个对象在初始化的时候就会去创建 GraphicView 对象。我们在创建好QC_ApplicationWindow 对象后我们就会去检测之前的文件列表中是否拥有文件如果有则打开文件本质上就是加载 GraphicView如果没有则是去创建一个新的 GraphicView主要操作是main.cppintmain{if(!files_loaded){appWin.slotFileNewNew();}}qc_applicationwindow.cppvoidQC_ApplicationWindow::slotFileNewNew{if(!slotFileNewHelper(fileName))}boolQC_ApplicationWindow::slotFileNewHelper{wslotFileNew();}QC_MDIWindow*QC_ApplicationWindow::slotFileNew(RS_Document*doc){QC_MDIWindow*wnewQC_MDIWindow(doc,mdiAreaCAD,0);}这样我们变得到了我们的绘画区啦。七、QApplication::exec()进入事件循环完成前面的应用程序、配置、命令行参数以及主窗口等初始化之后LibreCAD 最终需要进入 Qt 的事件循环。intreturn_codeapp.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 与命令系统。谢谢大家的观看。坚持到这里已经很厉害啦辛苦啦ʕ • ᴥ • ʔづ♡ど
返回列表