
🫧 励志不掉头发的内向程序员 :个人主页
✨️ 个人专栏: 《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:
- 事件类型:MouseButtonPress
- 鼠标位置:x, y
- 按键:LeftButton
鼠标移动则对应:
QMouseEvent
- 事件类型:MouseMove
- 鼠标位置:x, y
鼠标松开则对应:
QMouseEvent
- 事件类型:MouseButtonRelease
- 鼠标位置:x, y
- 按键: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 的关系
到这里,我们可以重新看一下两者的职责。
- EventHandler:管理当前 Action,并负责将用户输入事件分发给 Action。
- 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() 开始,继续追踪第一次鼠标点击 → 获取坐标 → 保存起点这一过程。
🎇坚持到这里已经很厉害啦,辛苦啦🎇 ʕ • ᴥ • ʔ づ♡ど

