【LibreCAD 2D架构】从鼠标点击到屏幕像素:LibreCAD绘图架构全链路解析之Action与命令系统

🫧 励志不掉头发的内向程序员个人主页
✨️ 个人专栏: 《C++语言》《Linux学习》

🌅偶尔悲伤,偶尔被幸福所完善


👓️博主简介:

文章目录

  • 前言
  • 一、QAction:工具栏按钮背后的对象
  • [二、从 QAction 到 LibreCAD 的 Action](#二、从 QAction 到 LibreCAD 的 Action)
  • [三、QAction 的创建与映射:a_map 如何连接菜单和工具栏?](#三、QAction 的创建与映射:a_map 如何连接菜单和工具栏?)
  • [四、Action 的创建与分发:从 RS2::ActionXXX 到 RS_ActionXXX](#四、Action 的创建与分发:从 RS2::ActionXXX 到 RS_ActionXXX)
  • [五、EventHandler:用户输入事件如何进入 LibreCAD?](#五、EventHandler:用户输入事件如何进入 LibreCAD?)
  • 七、RS_ActionDrawLine:真正进入"画线状态"
  • 总结

前言

上一章节我们已经追踪了 LibreCAD 从 main() 到 GraphicView 的初始化过程。此时程序已经进入事件循环,开始等待用户操作。那么我们用户在进行操作的时候,LibreCAD 会执行些什么流程呢,当用户点击工具栏中的"直线"按钮时,LibreCAD 是如何将一次普通的 GUI 操作转换成具体的 CAD 绘图命令的?我们本章节便来了搞懂这个问题。


一、QAction:工具栏按钮背后的对象

我们上一章节就说了我们的控件是在 LC_ActionFactory 中统一去创建的,我们要是想要研究用户点击不同的控件会发生什么事情,我们最好还是先来看看创建这些控件的内容。

我们可以看见,我们的各种不同的控件,都是通过以下语句来创建的:

cpp 复制代码
new QAction(tr("...."), agm->....);
action->setIcon(QIcon("...."));
connect(action, SIGNAL(triggered()),
action_handler, SLOT(....));
action->setObjectName("....");
a_map["...."] = action;

它们都要去新建一个 QAction 对象,而在 Qt 的 GUI 架构中,QAction 用于描述用户可以执行的一个操作。它本身并不是按钮,而是连接菜单、工具栏等 UI 控件与实际功能的中间对象。

有的小伙伴可能会觉得,这里不就是创建了一个按钮吗,但其实QAction 和按钮不是一回事,例如:

cpp 复制代码
QAction *action = new QAction(this);

创建的是一个动作对象,它本身不是一个按钮。真正显示在工具栏上的可能是:

cpp 复制代码
toolBar->addAction(action);

这样做就可以把这个对应的行为和按钮隔离开来,QAction 不但可以通过按钮来进行一个动作,如果我们需要,那么在菜单项可以使用它、工具栏按钮可以使用它、快捷键可以触发它等等。也就是说,一个 QAction 可以被多个 UI 入口复用,这样就可以很好的避免出现代码重复的问题,我们可以通过一个 QAction 去在不同的场景,但是同一个需求中去调用它。

当然,一个 QAction 不单单只是一个"信号"。它可以包含与这个动作有关的信息,例如:

cpp 复制代码
QAction *action = new QAction(this);
action->setText("Draw Line");
action->setIcon(...);
action->setShortcut(...);

因此一个 Action 可以包含:名称 / 文本、图标、快捷键、是否可用、是否选中、triggered() 信号等,然后工具栏只负责把它显示出来。

然后再通过用户操作(点击、快捷键、菜单栏等)激发出对应的信号,此时就会开始执行对应的槽函数啦,如果是画直线的操作,那就是开始执行画直线的槽函数,让计算机做好画直线的准备啦。

二、从 QAction 到 LibreCAD 的 Action

QAction 负责描述和触发 Qt 中的用户操作,但它本身并不包含具体的 CAD 功能逻辑。当用户点击菜单或工具栏按钮时,对应的 QAction 会发出 triggered() 信号。LibreCAD 再通过自己的 Action 体系对这一操作进行处理,并执行对应的 CAD 功能。

cpp 复制代码
action = new QAction(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 如何连接菜单和工具栏?

细心的小伙伴可能会注意的创建控件的时候调用的函数会传递两个参数。

cpp 复制代码
void LC_ActionFactory::fillActionContainer(QMap<QString, QAction*>& a_map, LC_ActionGroupManager* agm) {
    QAction* action;

    // <[~ Zoom ~]>
    action = new QAction(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_map

a_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、agm

agm的作用是 LibreCAD 中的 Action Group 进行统一管理。这里的 Group 可以理解为"相关操作的集合"。例如 CAD 中可以把一些操作归到一起:

  1. 绘图工具组:直线、圆、圆弧、矩形
  2. 修改工具组:移动、复制、删除、旋转

在 CAD 中有很多工具是有"当前状态"的。比如:选择工具、直线工具、圆工具、矩形工具等。通常用户当前只是在使用其中一个绘图工具。

当用户从:直线切换到圆时就涉及当前 Action 退出/切换到新的 Action,此时 Action Group 可以帮助系统把具有相关关系的操作组织起来。

四、Action 的创建与分发:从 RS2::ActionXXX 到 RS_ActionXXX

前面的 QAction 主要负责响应菜单、工具栏等 GUI 操作。当用户触发某个 CAD 命令后,Qt 的 QAction 通过信号槽机制进入 LibreCAD 的处理逻辑。在 LibreCAD 内部,部分处理函数会使用 RS2::ActionXXX 作为 Action 类型标识,并根据该类型创建对应的具体 RS_ActionXXX 对象。

cpp 复制代码
void QG_ActionHandler::slotFileExportMakerCam() {
    setCurrentAction(RS2::ActionFileExportMakerCam);
}
void QG_ActionHandler::slotZoomIn() {
    setCurrentAction(RS2::ActionZoomIn);
}
void QG_ActionHandler::slotZoomOut() {
    setCurrentAction(RS2::ActionZoomOut);
}
void QG_ActionHandler::slotZoomAuto() {
    setCurrentAction(RS2::ActionZoomAuto);
}
void QG_ActionHandler::slotZoomWindow() {
    setCurrentAction(RS2::ActionZoomWindow);
}

通过不同信号就会去调用不同的槽函数,而去给我们的 setCurrentAction 去传递不同的值。

cpp 复制代码
RS_ActionInterface* QG_ActionHandler::setCurrentAction(RS2::ActionType id) {
    RS_ActionInterface* a = NULL;
    auto a_layer = document->getLayerList()->getActive();
    switch (id) {
    case RS2::ActionFileNewTemplate:
        a = new RS_ActionFileNewTemplate(*document, *view);
        break;
    case RS2::ActionFileOpen:
        a = new RS_ActionFileOpen(*document, *view);
        break;
    case RS2::ActionFileSaveAs:
        a = new RS_ActionFileSaveAs(*document, *view);
        break;
    case RS2::ActionFileExportMakerCam:
		a = new LC_ActionFileExportMakerCam(*document, *view);
        break;
    case RS2::ActionEditUndo:
        a = new RS_ActionEditUndo(true, *document, *view);
        break;
    case RS2::ActionEditRedo:
        a = new RS_ActionEditUndo(false, *document, *view);
        break;
    case RS2::ActionEditCutNoSelect:
        a = new RS_ActionEditCopy(false, *document, *view);
        break;
	// ....
}

不同的值就会进入不同的分支去创建不同的对象,这些对象就是我们的动作类了,就比如我们需要画直线,我们点击画直线按钮,这个时候就经过之前的流程,到这里终于是创建出了我们动作的对象了。我们可以来看看画直线的动作类。

我们可以看到要画直线得收集两个点的左边,初始点和结束点,同时这个里面还重载了鼠标移动事件,鼠标释放事件等等。当我们点击画直线后,计算机准备妥当后,这个时候我们的鼠标的移动和释放就是在执行这里重载后的事件了,不同的动作重载了不同的事件,这样我们就可以实现不同的动作我们的操作可以实现不同的效果,这些动作都是继承自 RS_PreviewActionInterface 类,这个类我们后面再来讲吧。

有的小伙伴可能有疑惑,为什么不直接在按钮的槽函数里 new?这里可以简单从代码组织的角度理解。如果每一个按钮的处理代码都自己负责创建 Action:

cpp 复制代码
void onLineClicked() {
    new RS_ActionDrawLine(...);
}
void onCircleClicked() {
    new RS_ActionDrawCircle(...);
}
void onCopyClicked() {
    new RS_ActionEditCopy(...);
}

随着 CAD 功能越来越多,创建 Action 的逻辑就会散落在大量 GUI 代码中。而通过 Factory,可以把 "根据 Action 类型创建具体 Action"集中到一个地方。于是 GUI 层只需要负责用户点击 -> 触发操作,而 ActionFactory 负责操作类型到具体 Action。这体现的是一种很典型的工厂思想:把对象的创建过程集中管理,让使用对象的代码不需要直接了解具体对象的创建细节。

五、EventHandler:用户输入事件如何进入 LibreCAD?

前面我们已经知道,用户点击菜单或工具栏后,会进入 LibreCAD 的 Action 体系,并由 ActionFactory 创建具体的 Action。但 Action 创建出来以后,还需要有一个对象来管理当前正在执行的 Action,这就是 EventHandler。

cpp 复制代码
RS_ActionInterface* QG_ActionHandler::setCurrentAction(RS2::ActionType id) {
	// ...
	if (a) {
	    view->setCurrentAction(a);
	}
	// ...
}

void RS_GraphicView::setCurrentAction(RS_ActionInterface* action) {
	if (eventHandler) {
		eventHandler->setCurrentAction(action);
	}
}

void RS_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 中同时存在很多不同的 Action:RS_ActionDrawLine、RS_ActionDrawCircle、RS_ActionEditCopy、RS_ActionEditMove...。程序需要知道当前用户正在执行的是哪一个 Action。例如用户点击"直线"后:ActionHandler -> 当前 Action = RS_ActionDrawLine,接下来用户在绘图区移动鼠标、点击鼠标时,就可以由当前的 RS_ActionDrawLine 继续处理。如果用户之后切换到"圆":RS_ActionDrawLine -> 切换 -> RS_ActionDrawCircle,当前负责处理用户操作的 Action 也随之发生变化。因此,这一层可以简单概括为:

ActionFactory 负责"创建谁",EventHandler 负责"当前由谁工作"。

七、RS_ActionDrawLine:真正进入"画线状态"

最后可以发现,我们会调用对应的动作的初始化,这样就会进入了具体的绘图操作流程了。

cpp 复制代码
void RS_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 操作的?"而后续要解决的则是"用户点击的坐标,究竟是怎么一步步变成屏幕上的一条线的?"


🎇坚持到这里已经很厉害啦,辛苦啦🎇 ʕ • ᴥ • ʔ づ♡ど

相关推荐
fengyehongWorld1 小时前
Linux caddy的安装与配置
linux·运维·服务器
晚风叙码1 小时前
Linux(三)apt/yum 软件安装与 vim 编辑器入门指南
linux·编辑器·vim
洪流之源1 小时前
Windows 远程 Ubuntu 24.04 桌面
linux·windows·ubuntu
小道士写程序1 小时前
自然语言处理NLP - 第8章 词向量与神经网络:从手工特征到学习表示
神经网络·学习·自然语言处理
民乐团扒谱机1 小时前
【超详细】PyQt6 实现 AU 风格波形图编辑器:多级缩放与采样点逐级渲染,附全过程代码
开发语言·python·编辑器·pyqt·audition·时域·频谱图
2401_862880821 小时前
Linux应用层开发 --- HTTP
linux·运维·http
s_w.h1 小时前
【 linux 】线程互斥与同步
java·linux·服务器·开发语言
码匠许师傅1 小时前
【设计模式精讲】15.享元模式(Flyweight)
c++·设计模式·享元模式·uml
可爱系程序猿1 小时前
libcrypto-3-x64.dll 加载失败排查:OpenSSL 3 版本、PATH 顺序与 x64 依赖如何对应
linux·运维·服务器·程序人生·电脑