
励志不掉头发的内向程序员个人主页✨️ 个人专栏: 《C语言》《Linux学习》偶尔悲伤偶尔被幸福所完善️博主简介:文章目录前言一、QAction工具栏按钮背后的对象二、从 QAction 到 LibreCAD 的 Action三、QAction 的创建与映射a_map 如何连接菜单和工具栏1、a_map2、agm四、Action 的创建与分发从 RS2::ActionXXX 到 RS_ActionXXX五、EventHandler用户输入事件如何进入 LibreCAD七、RS_ActionDrawLine真正进入“画线状态”总结前言上一章节我们已经追踪了 LibreCAD 从 main() 到 GraphicView 的初始化过程。此时程序已经进入事件循环开始等待用户操作。那么我们用户在进行操作的时候LibreCAD 会执行些什么流程呢当用户点击工具栏中的“直线”按钮时LibreCAD 是如何将一次普通的 GUI 操作转换成具体的 CAD 绘图命令的我们本章节便来了搞懂这个问题。一、QAction工具栏按钮背后的对象我们上一章节就说了我们的控件是在 LC_ActionFactory 中统一去创建的我们要是想要研究用户点击不同的控件会发生什么事情我们最好还是先来看看创建这些控件的内容。我们可以看见我们的各种不同的控件都是通过以下语句来创建的newQAction(tr(....),agm-....);action-setIcon(QIcon(....));connect(action,SIGNAL(triggered()),action_handler,SLOT(....));action-setObjectName(....);a_map[....]action;它们都要去新建一个 QAction 对象而在 Qt 的 GUI 架构中QAction 用于描述用户可以执行的一个操作。它本身并不是按钮而是连接菜单、工具栏等 UI 控件与实际功能的中间对象。有的小伙伴可能会觉得这里不就是创建了一个按钮吗但其实QAction 和按钮不是一回事例如QAction*actionnewQAction(this);创建的是一个动作对象它本身不是一个按钮。真正显示在工具栏上的可能是toolBar-addAction(action);这样做就可以把这个对应的行为和按钮隔离开来QAction 不但可以通过按钮来进行一个动作如果我们需要那么在菜单项可以使用它、工具栏按钮可以使用它、快捷键可以触发它等等。也就是说一个 QAction 可以被多个 UI 入口复用这样就可以很好的避免出现代码重复的问题我们可以通过一个 QAction 去在不同的场景但是同一个需求中去调用它。当然一个 QAction 不单单只是一个“信号”。它可以包含与这个动作有关的信息例如QAction*actionnewQAction(this);action-setText(Draw Line);action-setIcon(...);action-setShortcut(...);因此一个 Action 可以包含名称 / 文本、图标、快捷键、是否可用、是否选中、triggered() 信号等然后工具栏只负责把它显示出来。然后再通过用户操作点击、快捷键、菜单栏等激发出对应的信号此时就会开始执行对应的槽函数啦如果是画直线的操作那就是开始执行画直线的槽函数让计算机做好画直线的准备啦。二、从 QAction 到 LibreCAD 的 ActionQAction 负责描述和触发 Qt 中的用户操作但它本身并不包含具体的 CAD 功能逻辑。当用户点击菜单或工具栏按钮时对应的 QAction 会发出 triggered() 信号。LibreCAD 再通过自己的 Action 体系对这一操作进行处理并执行对应的 CAD 功能。actionnewQAction(tr(Zoom Panning),agm-other);action-setIcon(QIcon(:/icons/zoom_pan.svg));connect(action,SIGNAL(triggered()),action_handler,SLOT(slotZoomPan()));action-setObjectName(ZoomPan);a_map[ZoomPan]action;因此LibreCAD 将 Qt 的 GUI 操作与具体 CAD 功能进行了分离菜单 / 工具栏 - QAction - triggered() - Action处理机制 - LibreCAD Action - CAD核心这种设计使菜单、工具栏和快捷键等不同操作入口能够共享统一的 CAD 功能逻辑同时降低 GUI 层与 CAD 核心之间的耦合。三、QAction 的创建与映射a_map 如何连接菜单和工具栏细心的小伙伴可能会注意的创建控件的时候调用的函数会传递两个参数。voidLC_ActionFactory::fillActionContainer(QMapQString,QAction*a_map,LC_ActionGroupManager*agm){QAction*action;// [~ Zoom ~]actionnewQAction(tr(Zoom Panning),agm-other);action-setIcon(QIcon(:/icons/zoom_pan.svg));connect(action,SIGNAL(triggered()),action_handler,SLOT(slotZoomPan()));action-setObjectName(ZoomPan);a_map[ZoomPan]action;// ....}这两个参数的有什么作用呢1、a_mapa_map是一个QString - QAction*的映射表。可以简单理解为命令名称QAction对象“file_open”QAction*“file_save”QAction*“draw_line”QAction*“draw_circle”QAction*例如a_map[“draw_line”]就可以找到“画直线”对应的 QAction。这里需要注意这个 a_map 保存的是 Qt 的 QAction不是 RS_ActionDrawLine 等 LibreCAD Action。因此它主要解决的是 GUI 层面的 Action 查找问题可以被其他 UI 组件快速找到并复用。2、agmagm的作用是 LibreCAD 中的 Action Group 进行统一管理。这里的 Group 可以理解为“相关操作的集合”。例如 CAD 中可以把一些操作归到一起绘图工具组直线、圆、圆弧、矩形修改工具组移动、复制、删除、旋转在 CAD 中有很多工具是有“当前状态”的。比如选择工具、直线工具、圆工具、矩形工具等。通常用户当前只是在使用其中一个绘图工具。当用户从直线切换到圆时就涉及当前 Action 退出/切换到新的 Action此时 Action Group 可以帮助系统把具有相关关系的操作组织起来。四、Action 的创建与分发从 RS2::ActionXXX 到 RS_ActionXXX前面的 QAction 主要负责响应菜单、工具栏等 GUI 操作。当用户触发某个 CAD 命令后Qt 的 QAction 通过信号槽机制进入 LibreCAD 的处理逻辑。在 LibreCAD 内部部分处理函数会使用 RS2::ActionXXX 作为 Action 类型标识并根据该类型创建对应的具体 RS_ActionXXX 对象。voidQG_ActionHandler::slotFileExportMakerCam(){setCurrentAction(RS2::ActionFileExportMakerCam);}voidQG_ActionHandler::slotZoomIn(){setCurrentAction(RS2::ActionZoomIn);}voidQG_ActionHandler::slotZoomOut(){setCurrentAction(RS2::ActionZoomOut);}voidQG_ActionHandler::slotZoomAuto(){setCurrentAction(RS2::ActionZoomAuto);}voidQG_ActionHandler::slotZoomWindow(){setCurrentAction(RS2::ActionZoomWindow);}通过不同信号就会去调用不同的槽函数而去给我们的 setCurrentAction 去传递不同的值。RS_ActionInterface*QG_ActionHandler::setCurrentAction(RS2::ActionType id){RS_ActionInterface*aNULL;autoa_layerdocument-getLayerList()-getActive();switch(id){caseRS2::ActionFileNewTemplate:anewRS_ActionFileNewTemplate(*document,*view);break;caseRS2::ActionFileOpen:anewRS_ActionFileOpen(*document,*view);break;caseRS2::ActionFileSaveAs:anewRS_ActionFileSaveAs(*document,*view);break;caseRS2::ActionFileExportMakerCam:anewLC_ActionFileExportMakerCam(*document,*view);break;caseRS2::ActionEditUndo:anewRS_ActionEditUndo(true,*document,*view);break;caseRS2::ActionEditRedo:anewRS_ActionEditUndo(false,*document,*view);break;caseRS2::ActionEditCutNoSelect:anewRS_ActionEditCopy(false,*document,*view);break;// ....}不同的值就会进入不同的分支去创建不同的对象这些对象就是我们的动作类了就比如我们需要画直线我们点击画直线按钮这个时候就经过之前的流程到这里终于是创建出了我们动作的对象了。我们可以来看看画直线的动作类。我们可以看到要画直线得收集两个点的左边初始点和结束点同时这个里面还重载了鼠标移动事件鼠标释放事件等等。当我们点击画直线后计算机准备妥当后这个时候我们的鼠标的移动和释放就是在执行这里重载后的事件了不同的动作重载了不同的事件这样我们就可以实现不同的动作我们的操作可以实现不同的效果这些动作都是继承自 RS_PreviewActionInterface 类这个类我们后面再来讲吧。有的小伙伴可能有疑惑为什么不直接在按钮的槽函数里 new这里可以简单从代码组织的角度理解。如果每一个按钮的处理代码都自己负责创建 ActionvoidonLineClicked(){newRS_ActionDrawLine(...);}voidonCircleClicked(){newRS_ActionDrawCircle(...);}voidonCopyClicked(){newRS_ActionEditCopy(...);}随着 CAD 功能越来越多创建 Action 的逻辑就会散落在大量 GUI 代码中。而通过 Factory可以把 “根据 Action 类型创建具体 Action”集中到一个地方。于是 GUI 层只需要负责用户点击 - 触发操作而 ActionFactory 负责操作类型到具体 Action。这体现的是一种很典型的工厂思想把对象的创建过程集中管理让使用对象的代码不需要直接了解具体对象的创建细节。五、EventHandler用户输入事件如何进入 LibreCAD前面我们已经知道用户点击菜单或工具栏后会进入 LibreCAD 的 Action 体系并由 ActionFactory 创建具体的 Action。但 Action 创建出来以后还需要有一个对象来管理当前正在执行的 Action这就是 EventHandler。RS_ActionInterface*QG_ActionHandler::setCurrentAction(RS2::ActionType id){// ...if(a){view-setCurrentAction(a);}// ...}voidRS_GraphicView::setCurrentAction(RS_ActionInterface*action){if(eventHandler){eventHandler-setCurrentAction(action);}}voidRS_EventHandler::setCurrentAction(RS_ActionInterface*action){// ...currentActions.push_back(action);action-init();// ...}可以简单理解为EventHandler是 LibreCAD 中 Action 的管理者负责让当前的 Action 进入执行流程并在用户操作过程中将相应的事件交给当前 Action 处理。整体流程可以先理解为用户点击“画直线” - QAction - LibreCAD - ActionFactory - 创建 RS_ActionDrawLine - EventHandler - 当前 Action 开始工作但是我们为什么需要 EventHandler因为 LibreCAD 中同时存在很多不同的 ActionRS_ActionDrawLine、RS_ActionDrawCircle、RS_ActionEditCopy、RS_ActionEditMove…。程序需要知道当前用户正在执行的是哪一个 Action。例如用户点击“直线”后ActionHandler - 当前 Action RS_ActionDrawLine接下来用户在绘图区移动鼠标、点击鼠标时就可以由当前的 RS_ActionDrawLine 继续处理。如果用户之后切换到“圆”RS_ActionDrawLine - 切换 - RS_ActionDrawCircle当前负责处理用户操作的 Action 也随之发生变化。因此这一层可以简单概括为ActionFactory 负责“创建谁”EventHandler 负责“当前由谁工作”。七、RS_ActionDrawLine真正进入“画线状态”最后可以发现我们会调用对应的动作的初始化这样就会进入了具体的绘图操作流程了。voidRS_EventHandler::setCurrentAction(RS_ActionInterface*action){// ...currentActions.push_back(action);action-init();// ...}之后的操作就是通过调用不同的 RS_ActionXXX 的行为去进行不同的操作了就比如RS_ActionDrawLine 是 LibreCAD 中负责交互式绘制直线的 Action。它会保存当前画线操作所需要的信息并在后续接收到用户的鼠标输入后逐步完成直线的创建。用户第一次点击第一次点击 - 确定直线起点。第二次点击第二次点击 - 确定直线终点 - 创建直线实体。前面的内容主要解决的是“用户想画什么以及创建哪个 Action”从 RS_ActionDrawLine 开始我们就真正进入了“这条线是怎么被画出来的”。总结本文从 LibreCAD 的程序入口 main() 出发按照程序的实际初始化流程对 LibreCAD 从程序启动、Qt 应用程序创建到主窗口和绘图界面初始化的过程进行了梳理。首先main() 负责完成程序启动所需的基础工作包括 Qt 应用程序的创建、程序配置读取、语言环境初始化以及命令行参数处理等。随后LibreCAD 创建 QC_ApplicationWindow完成主窗口、菜单栏、工具栏、状态栏、图层面板、命令行以及绘图区等 GUI 组件的初始化。在此基础上文章进一步进入 LibreCAD 的用户操作流程。从 QAction 出发分析菜单和工具栏中的操作是如何进入 LibreCAD 内部的再通过 ActionFactory 创建对应的具体 Action。以画直线为例最终创建出 RS_ActionDrawLine程序也由此从单纯的界面操作进入具体的 CAD 交互过程。因此到目前为止可以将 LibreCAD 的整体执行流程概括为程序启动 - main() - QApplication - LibreCAD 应用程序初始化 - QC_ApplicationWindow - GUI 界面初始化- QAction - 用户触发 CAD 命令 - ActionFactory - 创建具体 Action - RS_ActionDrawLine - 进入具体绘图操作这一部分主要建立了 LibreCAD 的程序启动与用户操作之间的联系。接下来继续向下追踪 RS_ActionDrawLine就可以看到用户的鼠标事件是如何进入 Action、如何确定绘图点以及最终如何创建 CAD 实体并显示到 GraphicView 上。换句话说前面的内容解决的是“LibreCAD 是怎么启动起来的以及用户点击一个工具后程序是怎么进入对应的 CAD 操作的”而后续要解决的则是“用户点击的坐标究竟是怎么一步步变成屏幕上的一条线的”坚持到这里已经很厉害啦辛苦啦ʕ • ᴥ • ʔづ♡ど