LibreCAD 2D架构】从鼠标点击到屏幕像素:LibreCAD绘图架构全链路解析之鼠标事件与RS_ActionDrawLine

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

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


👓️博主简介:

文章目录

  • 前言
  • 一、Qt鼠标事件从哪里产生?
    • [1.1、用户操作并不是直接调用 mousePressEvent()](#1.1、用户操作并不是直接调用 mousePressEvent())
    • [1.2、QMouseEvent 是什么?](#1.2、QMouseEvent 是什么?)
    • [1.3、Qt 如何把事件交给窗口?](#1.3、Qt 如何把事件交给窗口?)
    • [1.4、mousePressEvent() 是怎么出现的?](#1.4、mousePressEvent() 是怎么出现的?)
    • [1.5、放到 LibreCAD 中](#1.5、放到 LibreCAD 中)
  • 二、GraphicView如何接收鼠标事件?
    • [2.1、GraphicView 并不是一个独立的"鼠标监听器"](#2.1、GraphicView 并不是一个独立的“鼠标监听器”)
    • [2.2、Qt 中的三个核心鼠标事件](#2.2、Qt 中的三个核心鼠标事件)
    • [2.3、LibreCAD 中真正重要的是"事件继续往哪里走"](#2.3、LibreCAD 中真正重要的是“事件继续往哪里走”)
    • [2.4、为什么还需要 EventHandler?](#2.4、为什么还需要 EventHandler?)
    • 2.5、一个值得注意的问题
  • 三、EventHandler如何把事件交给当前Action?
    • [3.1、EventHandler 为什么需要知道"当前 Action"?](#3.1、EventHandler 为什么需要知道“当前 Action”?)
    • [3.2、当前 Action 是怎么设置进去的?](#3.2、当前 Action 是怎么设置进去的?)
    • [3.3、currentActions:EventHandler 手里的 Action](#3.3、currentActions:EventHandler 手里的 Action)
    • 3.4、鼠标事件来了以后怎么办?
    • [3.5、为什么不是 GraphicView 直接调用 RS_ActionDrawLine?](#3.5、为什么不是 GraphicView 直接调用 RS_ActionDrawLine?)
    • 3.6、一个鼠标点击到底经历了什么?
    • [3.7、为什么 currentActions 是一个容器?](#3.7、为什么 currentActions 是一个容器?)
    • [3.8、EventHandler 和 Action 的关系](#3.8、EventHandler 和 Action 的关系)
  • 总结

前言

上一篇我们已经分析了 LibreCAD 的 Action 与命令系统,知道了当用户点击"画线"工具后,LibreCAD 会创建一个具体的 RS_ActionDrawLine 对象,并通过 EventHandler 将它设置为当前正在工作的 Action。但到这里其实还有一个非常关键的问题没有解决:用户点击工具栏之后,鼠标真正移动、点击时,LibreCAD 又是怎么知道的?比如我们点击"画线"按钮,鼠标移动到绘图区,屏幕上出现跟随鼠标移动的预览线;第一次点击确定起点,第二次点击确定终点,最终一条直线被创建出来。这个过程看起来只是简单的"移动鼠标 → 点击两下",但从源码角度来看,中间实际上经历了一条完整的事件传递链:

QMouseEvent → GraphicView → EventHandler → RS_ActionDrawLine → 坐标处理 → 图形创建

上一篇我们解决的是:"点击画线按钮之后,谁负责执行画线功能?"这一篇继续往下追踪:"鼠标点击之后,这个事件又是如何一步步传递到 RS_ActionDrawLine 的?"我们将从 Qt 的鼠标事件开始,沿着 GraphicView、RS_EventHandler 一路跟踪到 RS_ActionDrawLine,重点分析第一次点击、鼠标移动、第二次点击分别发生了什么。通过这一过程,也可以进一步理解 LibreCAD 中非常重要的一种设计方式:GUI 负责接收用户输入,EventHandler 负责分发事件,Action 负责实现具体的交互逻辑。而这,才是真正进入 LibreCAD"鼠标点击 → CAD 操作"内部执行流程的开始。


一、Qt鼠标事件从哪里产生?

在上一节中,我们已经知道,点击"画线"工具之后,RS_ActionDrawLine 就成为了当前正在工作的 Action。但是,RS_ActionDrawLine 并不会自己主动去读取鼠标。

那么问题来了:当用户移动鼠标、按下鼠标、松开鼠标时,这些操作到底是怎么进入 Qt 程序的?要回答这个问题,需要先理解 Qt 中的事件系统(Event System)。

1.1、用户操作并不是直接调用 mousePressEvent()

当我们在 LibreCAD 绘图区按下鼠标时,最直观的理解可能是:

用户点击鼠标 -> mousePressEvent() -> 处理点击

但实际上并不是这样。鼠标的物理操作首先发生在操作系统层面。操作系统检测到鼠标按下、移动、松开等操作后,会将这些输入信息传递给正在运行的应用程序。Qt 再将这些底层输入转换成 Qt 自己的事件对象。例如:

鼠标按下 -> 操作系统检测到输入 -> Qt 接收到鼠标输入 -> 创建鼠标事件 -> QMouseEvent。所以,从 Qt 应用程序的角度来看,我们真正需要关注的是:QMouseEvent。

1.2、QMouseEvent 是什么?

QMouseEvent 是 Qt 用来描述鼠标事件的类。当用户进行鼠标操作时,Qt 会生成对应的 QMouseEvent,其中包含了这次鼠标操作的重要信息。例如:鼠标发生了什么操作、鼠标位于什么位置、哪个鼠标按键被按下、当前有哪些鼠标按键处于按下状态例如鼠标左键按下,可以抽象成:

QMouseEvent:

  1. 事件类型:MouseButtonPress
  2. 鼠标位置:x, y
  3. 按键:LeftButton

鼠标移动则对应:

QMouseEvent

  1. 事件类型:MouseMove
  2. 鼠标位置:x, y

鼠标松开则对应:

QMouseEvent

  1. 事件类型:MouseButtonRelease
  2. 鼠标位置:x, y
  3. 按键:LeftButton

因此,后面的 LibreCAD 代码其实并不需要直接面对操作系统的鼠标驱动。它看到的是 Qt 已经封装好的:QMouseEvent

1.3、Qt 如何把事件交给窗口?

有了 QMouseEvent 之后,Qt 还需要解决一个问题:这个鼠标事件到底应该交给哪个控件?

例如 LibreCAD 界面中同时存在:菜单栏、工具栏、图层面板、命令行、绘图区。用户点击不同的位置,事件自然应该交给不同的控件。Qt 的事件系统会根据鼠标所在的位置以及窗口、控件之间的层级关系,将事件发送给对应的 QWidget。而 LibreCAD 中,我们真正关心的是:鼠标点击绘图区之后,这个事件最终会进入哪个对象?这就引出了下一层:

操作系统 -> Qt 事件系统 -> QMouseEvent -> 对应的 QWidget -> GraphicView。LibreCAD 的绘图区正是后续鼠标事件处理的重要入口。

1.4、mousePressEvent() 是怎么出现的?

当鼠标事件被发送给对应的 Qt 控件之后,Qt 会调用这个控件相应的事件处理函数。例如,对于鼠标按下事件:

cpp 复制代码
void QWidget::mousePressEvent(QMouseEvent *event);

鼠标移动:

cpp 复制代码
void QWidget::mouseMoveEvent(QMouseEvent *event);

鼠标松开:

cpp 复制代码
void QWidget::mouseReleaseEvent(QMouseEvent *event);

因此我们经常看到这样的代码:

cpp 复制代码
void MyWidget::mousePressEvent(QMouseEvent *event)
{
    // 处理鼠标按下
}

这里需要特别注意:不是用户点击鼠标后,程序主动去调用 mousePressEvent()。而是:

用户按下鼠标 -> 操作系统产生输入 -> Qt 接收到输入 -> Qt 创建/封装 QMouseEvent -> Qt 将事件发送给目标 QWidget -> 调用对应的 mousePressEvent()。这就是 Qt 事件驱动模型的基本工作方式。

1.5、放到 LibreCAD 中

现在把视线重新拉回 LibreCAD。我们最终想要追踪的是:

用户点击鼠标 -> QMouseEvent -> GraphicView -> RS_EventHandler -> 当前 Action -> RS_ActionDrawLine

上一篇我们已经了解了后半段:

setCurrentAction() -> RS_ActionDrawLine -> RS_EventHandler

而现在开始解决前半段:

鼠标 -> QMouseEvent -> GraphicView -> RS_EventHandler

把这两部分连接起来之后,就能够完整解释:用户点击"画线"按钮之后,再点击绘图区的第一下,RS_ActionDrawLine 到底是怎么收到这个鼠标事件的?这也是下一步分析 RS_ActionDrawLine::mousePressEvent() 的基础。

二、GraphicView如何接收鼠标事件?

我们已经知道,用户操作鼠标后,Qt 会将底层输入封装成 QMouseEvent,然后交给对应的 Qt 控件处理。那么放到 LibreCAD 中,一个很自然的问题就是:用户点击绘图区之后,这个 QMouseEvent 最终是怎么进入 LibreCAD 的?前面我们已经知道,GraphicView 是 LibreCAD 中负责绘图显示以及用户交互的重要组件。因此这一节就从 GraphicView 入手,看看鼠标事件是如何从 Qt 一层一层进入 LibreCAD 的。

2.1、GraphicView 并不是一个独立的"鼠标监听器"

首先需要明确一点:GraphicView 并不是通过某个死循环不断检测鼠标位置。它采用的是 Qt 的事件机制。也就是说,当用户操作鼠标时,Qt 会把鼠标事件发送给当前鼠标所在的窗口/控件。LibreCAD 的绘图区对应的 Qt 控件接收到事件之后,再通过重写 Qt 提供的事件处理函数,对鼠标操作进行处理。整体过程可以先抽象成:

用户操作鼠标 -> 操作系统 -> Qt 事件系统 -> QMouseEvent -> LibreCAD 绘图区控件 -> mousePressEvent()、mouseMoveEvent()、mouseReleaseEvent()。这里最重要的就是最后三个函数。

2.2、Qt 中的三个核心鼠标事件

Qt 对常见的鼠标操作进行了封装。当用户按下鼠标时:

cpp 复制代码
mousePressEvent(QMouseEvent *event);

当用户移动鼠标时:

cpp 复制代码
mouseMoveEvent(QMouseEvent *event);

当用户松开鼠标时:

cpp 复制代码
mouseReleaseEvent(QMouseEvent *event);

这些函数接收到的参数就是前面介绍的:

cpp 复制代码
QMouseEvent *event

因此,从程序员的角度来看,可以把整个过程理解成:

鼠标左键按下 -> QMouseEvent -> mousePressEvent(event)

而 LibreCAD 要做的事情,就是在这些 Qt 事件处理函数中继续向自己的事件处理体系传递。

2.3、LibreCAD 中真正重要的是"事件继续往哪里走"

如果只停留在:

QMouseEvent -> mousePressEvent()。其实还没有解决 LibreCAD 的问题。

因为我们最终想知道的是:

** 鼠标点击 -> QMouseEvent -> ? -> RS_ActionDrawLine** 。

也就是说:mousePressEvent() 接收到事件之后,LibreCAD 又做了什么?

这就需要继续追踪 GraphicView 相关源码。在 LibreCAD 中,绘图区并不是最终处理鼠标交互逻辑的地方GraphicView 更像是一个事件入口:

我们之前有简单的了解到,当我们去按一些工具按钮,就会触发对应的槽函数,此时就会调用对应分支的 case 去创建对应的对象,但是结尾有一个语句是这样的:

cpp 复制代码
if (a) {
    view->setCurrentAction(a);
}

此时我们按 f2 就能查看到这个函数的内容以及知道 view 的类型是什么。

cpp 复制代码
void RS_GraphicView::setCurrentAction(RS_ActionInterface* action) {
	if (eventHandler) {
		eventHandler->setCurrentAction(action);
	}
}

可以看到,RS_GraphicView 是继承自 QWidget 的,所以它可以接收到 Qt 鼠标事件。从函数中可以看到,它又调用了eventHandler,将 action 设置到 eventHandler 中去了。

cpp 复制代码
currentActions.push_back(action);
action->init();

在 eventHandler 函数里面,它将 action 放到一个数据库中去管理,然后初始化该 action。

从源码可以看出:GraphicView 负责接收来自 Qt 的输入,而真正的绘图交互逻辑则交给 LibreCAD 自己的 EventHandler 系统处理。

2.4、为什么还需要 EventHandler?

可能有人会产生一个疑问:既然 GraphicView 已经收到了鼠标事件,为什么不直接调用 RS_ActionDrawLine而还要经过一个 EventHandler?原因就在于当前工作的 Action 并不一定是画线。例如用户当前可能处于画线、画圆、移动、复制旋转、删除、选择。不同状态下,同样的一次鼠标点击,需要执行完全不同的逻辑。因此,GraphicView 不应该关心:"现在到底是画线还是画圆?"

它只需要负责 "我收到了一个鼠标事件。"然后把这个事件交给 LibreCAD 的事件处理机制。之后再由 EventHandler 找到当前正在工作的 Action。

于是就形成了:

就像上面的代码那样,当我们去激活一个 action 的时候,事件响应最后会传到 RS_EventHandler 结构中,如果你本来之前就有别的事情激活了,那RS_EventHandler 就会先暂停当前的 action,然后出 push_back 新的任务,初始化后激活,当我们的新的任务结束了,此时 pop_back 掉,再去激活刚才冻结的 action 即可。这就是 LibreCAD 事件系统非常重要的一层解耦。

2.5、一个值得注意的问题

这里还需要注意一个容易混淆的地方:GraphicView 接收到鼠标事件,并不意味着它负责实现具体的画线逻辑。可以简单地把几个对象的职责划分为:

对象 主要职责
QMouseEvent 描述鼠标发生了什么
GraphicView 接收绘图区相关的 Qt 输入
RS_EventHandler 将输入事件分发给当前 Action
RS_ActionDrawLine 实现具体的画线交互逻辑

因此整个设计可以概括成一句话:GraphicView 负责"接",EventHandler 负责"传",Action 负责"做"。这也是理解 LibreCAD 鼠标交互源码的关键。下一步,我们就可以直接进入 RS_ActionDrawLine,看看第一次鼠标点击发生时,源码究竟执行了哪些操作。

三、EventHandler如何把事件交给当前Action?

从上面可知,用户操作鼠标之后,Qt 会产生 QMouseEvent,LibreCAD 的绘图区接收到事件后,还需要继续将事件交给 LibreCAD 自己的事件处理系统。那么问题来了:RS_EventHandler 是怎么知道这个鼠标事件应该交给谁的?答案其实就在它维护的 当前 Action 中。

3.1、EventHandler 为什么需要知道"当前 Action"?

LibreCAD 中可能同时存在很多不同的 Action:RS_ActionDrawLine、RS_ActionDrawCircle、RS_ActionMove、RS_ActionCopy、RS_ActionRotate...。

但是用户在某一个时刻通常只是在执行其中一个操作。例如:点击"画线",当前 Action = RS_ActionDrawLine,此时用户点击鼠标,事件就应该交给 RS_ActionDrawLine,而如果用户切换到了"画圆",点击"画圆",当前 Action = RS_ActionDrawCircle,那么同样一个鼠标左键点击,就应该交给 RS_ActionDrawCircle,所以,EventHandler 的一个重要职责就是维护当前正在工作的 Action,并把用户输入转发给它。

3.2、当前 Action 是怎么设置进去的?

上一篇我们已经分析过,当用户点击"画线"工具时,会进入 QG_ActionHandler::setCurrentAction(...) 然后创建具体的RS_ActionDrawLine之后,这个 Action 会继续被设置到 View 和 EventHandler。前面我们追踪过类似这样的调用链:

QG_ActionHandler -> view -> setCurrentAction(action) -> RS_GraphicView::setCurrentAction() -> RS_EventHandler::setCurrentAction()

最终来到 RS_EventHandler::setCurrentAction(RS_ActionInterface* action),这里就完成了一件非常重要的事情:告诉 EventHandler:从现在开始,这个 Action 是当前正在工作的 Action。

3.3、currentActions:EventHandler 手里的 Action

在 RS_EventHandler 中,有一个非常重要的数据结构 currentActions,

cpp 复制代码
QList<RS_ActionInterface*> currentActions;

它保存当前正在处理的 Action。当执行 RS_EventHandler::setCurrentAction(action) 时,Action 会被加入到 currentActions 中。我们看到的核心代码可以概括成:

cpp 复制代码
currentActions.push_back(action);
action->init();

这里实际上完成了两件事情:

currentActions.push_back(action) -> EventHandler 记住这个 Action -> action->init() -> 初始化这个 Action

所以,当 RS_ActionDrawLine 被设置为当前 Action 后,EventHandler 就知道 "接下来用户产生的输入,需要交给这个 Action 处理。"

3.4、鼠标事件来了以后怎么办?

现在假设用户已经点击了"画线"工具。此时:currentActions -> RS_ActionDrawLine。然后用户在绘图区点击鼠标。Qt 产生:

cpp 复制代码
QMouseEvent *event

事件经过前面的流程进入 LibreCAD:

用户点击 -> QMouseEvent -> GraphicView -> EventHandler

那么 EventHandler 接下来要做的事情就是:

找到 currentActions -> 找到当前 Action -> 调用 Action 对应的鼠标事件处理函数

也就是说:

QMouseEvent -> RS_EventHandler -> currentActions -> RS_ActionDrawLine -> mousePressEvent(...)

这就是 EventHandler 最核心的工作。

3.5、为什么不是 GraphicView 直接调用 RS_ActionDrawLine?

这其实就是 LibreCAD 架构设计比较有意思的地方。如果让 GraphicView 直接写:

cpp 复制代码
if (当前是画线)
    RS_ActionDrawLine::mousePressEvent(...);

if (当前是画圆)
    RS_ActionDrawCircle::mousePressEvent(...);

if (当前是移动)
    RS_ActionMove::mousePressEvent(...);

那么随着 Action 越来越多,GraphicView 就会越来越复杂。LibreCAD 没有采用这种方式。而是把两者分开,这样 GraphicView 根本不需要知道当前到底是什么 Action。它只需要把事件交给 EventHandler。EventHandler 再根据当前状态决定 "这个事件应该交给谁?"

3.6、一个鼠标点击到底经历了什么?

现在可以把整个过程串起来。假设当前正在执行:RS_ActionDrawLine 用户按下鼠标左键。完整过程可以抽象为:

用户按下鼠标 -> Qt 产生 QMouseEvent -> GraphicView 接收到鼠标事件 -> 事件进入 RS_EventHandler -> EventHandler 查找 currentActions -> 找到 RS_ActionDrawLine -> 调用 Action 的鼠标事件处理函数 -> RS_ActionDrawLine 开始处理这次点击

到这里,事件才真正进入了"画线"逻辑。

3.7、为什么 currentActions 是一个容器?

这里还有一个值得注意的问题。从名字来看 currentActions,并不是 currentAction,而是一个复数形式。这说明 EventHandler 的设计并不一定只考虑"一个 Action"这种简单情况,而是使用容器保存当前的 Action。

从源码角度来看,可以理解成:

因此,事件到来之后,EventHandler 可以遍历当前的 Action,并将事件传递给相应的 Action。这也是为什么 LibreCAD 的事件系统并不是简单的一个事件 -> 一个固定函数,而是:

一个事件 -> EventHandler -> 当前 Action 集合 -> 对应的 Action

3.8、EventHandler 和 Action 的关系

到这里,我们可以重新看一下两者的职责。

  1. EventHandler:管理当前 Action,并负责将用户输入事件分发给 Action。
  2. Action:接收到事件之后,执行具体的交互逻辑。

所以可以用一句话概括:EventHandler 决定"谁来处理",Action 决定"怎么处理"。这也是 LibreCAD Action 系统和事件系统之间最重要的连接点。


总结

通过前面的分析,我们已经把 LibreCAD 中"鼠标事件如何进入当前 Action"这条链路串了起来。当用户在绘图区进行鼠标操作时,底层输入首先由操作系统产生,随后经过 Qt 的事件系统封装成 QMouseEvent,再由作为 QWidget 的绘图区对象接收并处理。LibreCAD 并不会让绘图区直接判断当前到底是在画线、画圆还是移动,而是将事件进一步交给 RS_EventHandler。RS_EventHandler 通过 currentActions 管理当前正在工作的 Action。当用户激活"画线"工具时,RS_ActionDrawLine 会被加入到事件处理体系中并完成初始化。之后用户产生的鼠标输入,就能够继续传递到对应的 Action,由 Action 实现具体的交互逻辑。

可以用一句话概括这一套机制:GraphicView 负责"接",EventHandler 负责"传",Action 负责"做"。到这里,我们解决的是**"鼠标事件如何找到 RS_ActionDrawLine"**的问题。但事件真正进入 RS_ActionDrawLine 后,还有一个更关键的问题:第一次点击鼠标时,RS_ActionDrawLine 到底做了什么?它又是如何把鼠标所在的屏幕位置转换成 CAD 中的坐标,并最终确定直线的起点?下一节就可以直接进入 RS_ActionDrawLine 的源码,从它的 mousePressEvent() 开始,继续追踪第一次鼠标点击 → 获取坐标 → 保存起点这一过程。


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

相关推荐
国科安芯1 小时前
空间数据总线的新范式:ASM1042S2S型抗辐射CANFD收发器在低轨遥感卫星星座中的应用研究
网络·嵌入式硬件·架构·canfd·数据总线·星载数据处理
旖旎夜光1 小时前
LeetCode 525:连续数组(前缀和) —— 题解
数据结构·c++·算法·leetcode·前缀和
fpcc1 小时前
c++编程实践—如何理解生命周期
c++
Rauser Mack1 小时前
从交互困境到语音闭环:桌面AI全语音数字秘书架构解析
人工智能·架构·交互
koi77u2 小时前
嵌入式学习————————TCP并发服务器(1)
linux·学习
mldong2 小时前
流程图上没画人,单子却走对了:工作流引擎内置处理人解析器
java·架构
jianqiang.xue5 小时前
ESP-IDF保姆级入门17|GPIO外部中断全解与工程化设计:分层架构/防抖策略/长按双击/优先级配置,掌握工业级可靠输入处理
stm32·单片机·物联网·架构·esp32
小淮AI10 小时前
从“刷题”到“追问”:AI课堂正在重塑哪些学习旧习惯?
人工智能·学习
wixzjsh11 小时前
TCP通信与HTTP协议学习笔记:粘包、三次握手、四次挥手与C/S、B/S模型
学习·tcp/ip·http