
👓️博主简介:

文章目录
前言
在第四篇里,我们分析了 LibreCAD 的 Action 与命令系统:用户点击工具栏之后,LC_ActionFactory 创建 QAction,QG_ActionHandler::setCurrentAction() 根据类型创建出具体的 RS_ActionXXX 对象,最后交给 RS_EventHandler 管理。当时我在文末留了一句:“这些动作都是继承自 RS_PreviewActionInterface 类,这个类我们后面再来讲吧。”
这一篇就是来还这笔账的。
不过在正式开始之前,我还要先修正前面两篇里的两个说法。
第一个是第五篇。当时讲到 EventHandler 管理 Action 时,我写过这样一句:新任务结束之后会"pop_back 掉,再去激活刚才冻结的 action"。这句话当时说得比较简略,严格来讲并不准确。真实的回收逻辑不是子类自己去弹栈,而是由 EventHandler 统一扫描、统一回收,本文第六节会把它讲清楚。
第二个是第六篇。当时我们贴过 RS_ActionDrawLine::mouseReleaseEvent() 里的一段右键分支:
case SetStartpoint:
init( getStatus() - 1);
break;
我只说了这是"右键取消、回退一步",但这个 -1 到底是什么、为什么减一就能结束一次画线操作,当时并没有解释。
所以这一篇我们要回答三个问题:
1、Action 这个基类到底定义了什么?为什么它能当状态机用?
2、一次用户交互从开始到结束,中间必须经过哪些固定环节?
3、为什么 Action 能"暂停一下,处理完别的事情再回来",还能顺手把预览线收掉?
把这几个问题讲明白,前面十篇里所有 getStatus()、init()、trigger()、suspend() 才算真正有了根。

一、Action 的三重身份
1.1、从继承链看起
我们第一次见到 Action 的基类,应该是在第四篇的 setCurrentAction() 里:
RS_ActionInterface* QG_ActionHandler::setCurrentAction(RS2::ActionType id) {
RS_ActionInterface* a = NULL;
switch (id) {
case RS2::ActionFileOpen:
a = new RS_ActionFileOpen(*document, *view);
break;
// ....
}
if (a) {
view->setCurrentAction(a);
}
}
所有的 RS_ActionXXX 都是 RS_ActionInterface 的子类。那我们来看看这个基类长什么样。
class RS_ActionInterface : public QObject, public RS_Snapper {
Q_OBJECT
public:
RS_ActionInterface(const char* name,
RS_EntityContainer& container,
RS_GraphicView& graphicView);
virtual RS2::ActionType rtti() const;
virtual void init(int status=0);
virtual void mouseMoveEvent(QMouseEvent*);
virtual void mousePressEvent(QMouseEvent*);
virtual void mouseReleaseEvent(QMouseEvent*);
virtual void keyPressEvent(QKeyEvent* e);
virtual void coordinateEvent(RS_CoordinateEvent*);
virtual void commandEvent(RS_CommandEvent*);
virtual void setStatus(int status);
virtual int getStatus();
virtual void trigger();
virtual void updateMouseButtonHints();
virtual void updateMouseCursor();
virtual bool isFinished();
virtual void finish(bool updateTB = true);
virtual void setPredecessor(RS_ActionInterface* pre);
virtual void suspend();
virtual void resume();
virtual void hideOptions();
virtual void showOptions();
// ...
};

从这个类的声明里,我们至少能读出三件事。
第一,它同时继承了 QObject 和 RS_Snapper。头文件里对 QObject 有一句很直白的注释:“Inherited from QObject for Qt translation features.”——继承 QObject 主要是为了用 Qt 的翻译功能和信号槽机制,这一点和绘图逻辑本身没什么关系。
第二,它继承了 RS_Snapper。这个我们在第十篇刚刚分析过,正是捕捉系统的核心类。也就是说:每一个绘图 Action,天生就是一个"捕捉器"。 这就解释了为什么在 RS_ActionDrawLine 里可以直接写 snapPoint(e),而不需要额外去创建任何一个捕捉对象——捕捉能力是从基类那儿继承来的。
第三,它的函数列表几乎就是一份"生命周期清单":init()、suspend()、resume()、trigger()、finish()、isFinished()。这已经暗示我们,Action 管理的不只是"一次绘图计算",而是一整段交互过程。
1.2、构造函数里藏着的两个指针
光看声明还不够,我们来看看构造函数到底做了什么。
RS_ActionInterface::RS_ActionInterface(const char* name,
RS_EntityContainer& container,
RS_GraphicView& graphicView) :
RS_Snapper(container, graphicView) {
this->name = name;
status = 0;
finished = false;
// graphic provides a pointer to the graphic if the
// entity container is a graphic (i.e. can also hold layers).
graphic = container.getGraphic();
// document pointer will be used for undo / redo
document = container.getDocument();
actionType=RS2::ActionNone;
}
这段代码很朴素,但有几处非常值得留意。
首先,它先把参数交给了父类 RS_Snapper(container, graphicView) 去构造。所以 Action 一出生,就已经握住了图形容器和视图,也就有了后面做捕捉的资本。
其次,它初始化了三个成员:status = 0、finished = false、actionType = RS2::ActionNone。这三个变量几乎就是整个 Action 的"生命体征"——交互进行到第几步、有没有结束、我是哪一种 Action。
最后,也是我个人觉得最有意思的地方,是两行赋值和它们上面的注释:
graphic = container.getGraphic();
document = container.getDocument();
注释写得很清楚:graphic 是"如果这个容器本身就是一张图纸,那就顺便把图纸指针也拿到";而 document 的注释直接写着 “will be used for undo / redo”。
这两行其实提前给我们后面两篇文章埋下了伏笔:
`graphic` 是图层、画笔这些图纸级属性的入口,后面讲图层与画笔时要用;
`document` 是撤销与重做的入口,下一篇我们就会看到它是怎么被用上的。
顺便说一句,rtti() 的实现也很简单:
RS2::ActionType RS_ActionInterface::rtti() const{
return actionType;
}
它返回的就是构造函数里那个 RS2::ActionNone。每种具体 Action 在构造时会把自己的 actionType 设置好,比如 RS_ActionDrawLine 里就有一句 actionType=RS2::ActionDrawLine;。所以 rtti() 并不是什么黑魔法,它就是"我是谁"这个问题的答案。后面我们会看到,finish() 里还要靠它来做一个特殊判断。
二、status:一个 int 撑起整个状态机
2.1、setStatus 只有五行
第六篇我们花了很大篇幅去讲"状态机",但那时候我们是在 RS_ActionDrawLine 里看现象——同样的鼠标左键,为什么第一次是起点、第二次是终点。现在我们把镜头拉回基类,看看这个状态到底是怎么被管理的。整个状态管理的核心,就是这么一个函数:
void RS_ActionInterface::setStatus(int status) {
this->status = status;
updateMouseButtonHints();
updateMouseCursor();
if(status<0) finish();
}
总共五行。我们一行一行看。
- 第一行:把新状态记录下来。
- 第二行:刷新鼠标按钮提示(就是你在状态栏看到的那句"指定第一个点"之类的话)。
- 第三行:刷新鼠标光标。
- 第四行:如果状态是负数,就结束这个 Action。

第四行是整个 Action 体系里最值得记住的一行。它意味着:
在 LibreCAD 里,"结束"不是一个布尔标志,而是一个负数状态。
这一点很重要,因为它解释了很多我们之前看不懂的写法。比如为什么 init() 既能用来开始一个动作,又能用来结束一个动作?为什么状态的定义总是从 0 开始往上数?
2.2、回过头看第六篇那个 -1
现在我们终于可以回答前言里提出的第三个问题了。
RS_ActionDrawLine 的状态定义是这样的:
/// Action States
enum Status {
SetStartpoint, ///< Setting the startpoint
SetEndpoint ///< Setting the endpoint
};
枚举默认从 0 开始,所以 SetStartpoint = 0,SetEndpoint = 1,而第六篇里我们贴过的那段右键处理代码是:
else if (e->button() == Qt::RightButton) {
deletePreview();
switch (getStatus()) {
default:
case SetStartpoint:
init( getStatus() - 1);
break;
case SetEndpoint:
next();
break;
}
}
现在我们可以把数字代进去算了:
当用户刚点完"画线"工具、还没点第一个点时,当前状态是 SetStartpoint,也就是 0。此时按下右键,程序执行 init( 0 - 1 ),也就是 init(-1)。
而 init(-1) 内部会先执行 setStatus(-1);setStatus() 发现参数小于 0,于是调用 finish()。所以整条链路是这样的:
右键 -> init(getStatus() - 1) -> init(-1) -> setStatus(-1) -> finish() -> Action 结束
"右键取消绘图"这个操作,本质上就是把状态退到负数,从而触发结束流程。
这也是为什么在等待起点的状态下右键是"取消",而在等待终点的状态下右键是 next()——两者走的是完全不同的分支,前者结束整个动作,后者只是回到上一步。
2.3、status 的取值含义
我们把 RS_ActionDrawLine 里会出现的状态值整理一下:
| status | 含义 | 谁会设置它 |
|---|---|---|
| -1 | 已结束,等待被回收 | finish()、右键回退、setFinished() |
| 0 | 等待起点(SetStartpoint) | 构造函数、init(0) |
| 1 | 等待终点(SetEndpoint) | 第一次点击确认起点之后 |
有了这张表,第六篇里那个状态转换图才算真正落地——它背后并不是什么抽象的概念,就是这一个 int 在 0 和 1 之间来回跳,跳到负数就结束。
三、init():Action 的入口
3.1、基类里的 init
搞清楚了 status,init() 就很好理解了。
void RS_ActionInterface::init(int status)
{
setStatus(status);
if (status>=0) {
RS_Snapper::init();
updateMouseButtonHints();
updateMouseCursor();
}else{
//delete snapper when finished, bug#3416878
deleteSnapper();
}
}
第一句就是 setStatus(status),这也是为什么我们说 init() 同时承担了"开始"和"结束"两种职能——差别只在传进来的数字是正还是负。
状态非负时,做三件事:初始化捕捉器、刷新按钮提示、刷新鼠标光标。也就是说,用户点下"画线"按钮之后,Action 一切准备就绪,就等鼠标事件了。
状态为负时,则走另一条路:把捕捉器删掉。注意那行注释——//delete snapper when finished, bug#3416878。这是官方仓库里一个真实存在过的缺陷编号,作者在修复之后把编号留在了注释里。读工程源码的时候经常能看到这类痕迹,它提醒我们:这些看起来理所当然的代码,背后都是有故事的。
3.2、子类怎么用 init
基类负责生命周期的公共部分,子类则负责自己的准备工作。我们来看看 RS_ActionDrawLine 的实现:
void RS_ActionDrawLine::init(int status)
{
RS_PreviewActionInterface::init(status);
reset();
drawSnapper();
}
三行代码,分工非常清楚:
第一行,先调用父类的 `init()`,把上面那一整套公共流程走完;
第二行,`reset()` 清空自己的历史数据(内部就是重新 `new` 一个 `Points` 结构);
第三行,`drawSnapper()` 把捕捉标记画出来。
所以要记住一个模式:子类的 init() 永远是"先交给父类,再处理自己"。这个模式在后面很多 Action 里都会反复出现。
四、一次交互的推进:基类只留接口
那事件来了之后呢?我们看看基类对这些事件函数的默认实现:
void RS_ActionInterface::mouseMoveEvent(QMouseEvent*) {}
void RS_ActionInterface::mousePressEvent(QMouseEvent*) {}
void RS_ActionInterface::mouseReleaseEvent(QMouseEvent*) {}
void RS_ActionInterface::keyPressEvent(QKeyEvent* e) { e->ignore(); }
void RS_ActionInterface::coordinateEvent(RS_CoordinateEvent*) {}
void RS_ActionInterface::commandEvent(RS_CommandEvent*) {}
全部是空的。
这不是偷懒,而是刻意的设计:基类只保证"这些接口存在",至于收到事件之后要做什么,完全交给子类。
正因如此,同一个鼠标左键,在 RS_ActionDrawLine 里意味着"确定起点或终点",在 RS_ActionDrawCircle 里意味着"确定圆心或半径",在 RS_ActionEditMove 里又意味着"选择实体或指定目标点"。EventHandler 只管把事件转发出去,怎么解释事件是每个 Action 自己的事——这正是第五篇里那句"EventHandler 负责传,Action 负责做"能成立的前提。不过有两点是基类帮子类做了的,值得单独提一句,就是前面 setStatus() 里出现过的这两个函数:
virtual void updateMouseButtonHints();
virtual void updateMouseCursor();
它们一个负责更新状态栏/命令行里的操作提示,一个负责把鼠标光标切换成对应的形状(十字光标、方框准星等等)。关键在于:它们在 setStatus() 里是被自动调用的。也就是说,只要状态一变,提示和光标就跟着变,子类不需要在每个事件处理函数里手动通知。这就是为什么你在 LibreCAD 里点第一个点和点第二个点时,底下的提示文字会自然变化。
五、trigger() 与 finish():两种不一样的"结束"
接下来是这一篇里最容易混淆的一对函数。第六篇和第七篇里,trigger() 出现的频率非常高,而第五篇又提到过"任务结束后"要恢复上一个 Action。那到底谁才是结束?
答案是:它们都不是同一个层面的东西。
5.1、trigger 是"动作完成"
我们看看基类里的 trigger():
void RS_ActionInterface::trigger() {}
空的,什么也不做。真正干活的是子类。比如 RS_ActionDrawLine::trigger():
void RS_ActionDrawLine::trigger()
{
RS_PreviewActionInterface::trigger();
RS_Line* line = new RS_Line(container, pPoints->data);
line->setLayerToActive();
line->setPenToActive();
container->addEntity(line);
// update undo list
if (document) {
document->startUndoCycle();
document->addUndoable(line);
document->endUndoCycle();
}
graphicView->redraw(RS2::RedrawDrawing);
graphicView->moveRelativeZero(pPoints->history.at(pPoints->index()).currPt);
}
这段代码和我们第七篇分析的流程完全对得上:创建 RS_Line、设置图层与画笔、加入容器、触发重绘。但请特别注意一点:trigger() 执行完之后,这个 Action 并没有结束。
RS_ActionDrawLine 是支持连续绘图的,画完一条线之后,它会继续等待下一个起点。所以 trigger() 的语义是"这一次动作完成了",而不是"这个工具退出了"。另外这段代码里的 // update undo list 那三行,第七篇贴 trigger() 的时候正好被我用 // ... 省略掉了。它们涉及撤销系统,我们下一篇文章会专门讲。
5.2、finish 是"Action 结束"
真正负责结束的是 finish():
void RS_ActionInterface::finish(bool /*updateTB*/)
{
//refuse to quit the default action
if(rtti() != RS2::ActionDefault) {
status = -1;
finished = true;
hideOptions();
RS_Snapper::finish();
}
}
它做了四件事:
把 `status` 置为 -1,和前面 `setStatus()` 的约定保持一致;
把 `finished` 置为 true,这是给 EventHandler 看的"可以回收我"的信号;
调用 `hideOptions()` 收起工具选项栏;
调用 `RS_Snapper::finish()` 结束捕捉器的生命周期。
还有一个小函数 setFinished():
void RS_ActionInterface::setFinished() {
status = -1;
}
它只改状态、不做清理,所以注释里写着 “Forces a termination of the action without any cleanup.” 一般情况下不会用它。
5.3、一个很妙的自保
finish() 里那行判断值得单独说:
//refuse to quit the default action
if(rtti() != RS2::ActionDefault) {
翻译过来就是:默认 Action 拒绝退出。
这里的 RS2::ActionDefault 指的是 RS_ActionDefault,它是 LibreCAD 在"用户没有执行任何工具"时挂着的那个 Action,负责处理空载状态下的鼠标行为(比如点选实体、框选)。如果它也被 finish() 掉了,那么在没有绘图工具激活的时候,界面就没人接收鼠标事件了。
所以这里用 rtti() 做了一次特判。这也回答了第一节里那个问题:rtti() 不只是用来"标识身份"的,在关键分支上它是有实际作用的。
六、Action 栈:挂起、恢复与回收
到这里,单个 Action 的生命周期已经清楚了。但还有一个问题没解决:当用户在一个动作进行到一半时,又点了另一个工具,会发生什么?
比如正在画线,画到一半突然点了一下"缩放"。这时候 RS_ActionDrawLine 还挂在半路,RS_ActionZoom 又要开始工作。LibreCAD 不能简单地把前一个删掉(用户可能还想回来继续画),于是就有了 Action 栈。
6.1、setCurrentAction:先挂起旧的,再启动新的
第五篇我们说过,EventHandler 里用 currentActions 这个容器来管理当前正在工作的 Action。现在我们来看完整的实现:
void RS_EventHandler::setCurrentAction(RS_ActionInterface* action) {
RS_DEBUG->print("RS_EventHandler::setCurrentAction");
if (action==NULL) {
return;
}
// Predecessor of the new action or NULL:
RS_ActionInterface* predecessor = NULL;
// Suspend current action:
if(hasAction()){
predecessor = currentActions.last();
predecessor->suspend();
predecessor->hideOptions();
}
else {
if (defaultAction) {
predecessor = defaultAction;
predecessor->suspend();
predecessor->hideOptions();
}
}
// Set current action:
currentActions.push_back(action);
// Initialisation of our new action:
action->init();
if (action->isFinished()==false) {
currentActions.last()->showOptions();
action->setPredecessor(predecessor);
}
cleanUp();
}
我们把它拆成四步来看:
第一步,找到"前一个 Action"。如果栈里已经有正在工作的 Action,那就是栈顶那个;如果栈是空的,那用户刚才在做的事其实就是默认 Action 在处理,所以把 defaultAction 记下来。
第二步,让前一个 Action 挂起:suspend() 加上 hideOptions()。这里的 suspend() 并不是"暂停线程",而是让这个 Action 收起自己的预览、收起工具选项栏、暂时不再响应操作。
第三步,把新 Action 压入栈,并调用它的 init() 让它开工。如果 init() 之后它没有立刻结束,就把前面记下的 predecessor 告诉它,同时把它的选项栏显示出来。
第四步,调用 cleanUp()。
6.2、cleanUp:这才是"回收"的真相
第五篇里我写过"新任务结束了,pop_back 掉,再去激活刚才冻结的 action"。现在我们把真正的实现贴出来,它会说明为什么那句话不够准确:
void RS_EventHandler::cleanUp() {
RS_DEBUG->print("RS_EventHandler::cleanUp");
for (auto it=currentActions.begin(); it != currentActions.end();)
{
if( (*it)->isFinished())
{
delete *it;
it= currentActions.erase(it);
}else{
++it;
}
}
if(hasAction()){
currentActions.last()->resume();
currentActions.last()->showOptions();
} else {
if (defaultAction) {
defaultAction->resume();
defaultAction->showOptions();
}
}
}
这里有几件事和我们之前的理解不太一样。
第一,回收不是"弹栈",而是"遍历整个栈删除已结束的 Action"。代码里是遍历 currentActions,凡是 isFinished() 为真的就地删除。这意味着一次调用可能清掉不止一个——比如用户连着按了几次 Esc,栈里可能同时有好几个已经结束的 Action 等着被收走。
第二,回收之后并不是简单地"回到上一个",而是"激活现在的栈顶"。如果栈里还有没结束的 Action,就恢复栈顶那个;如果栈空了,就恢复默认 Action。所以真正决定"接下来谁接鼠标事件"的,永远是栈顶。
第三,resume() 和 suspend() 是配对出现的。被挂起的 Action 在这里被唤醒,同时它的选项栏也重新显示出来。这也是为什么你从"缩放"返回"画线"时,画线工具的选项栏会自动回来。
6.3、谁在栈顶,谁接事件
理解了栈的结构,EventHandler 里那些鼠标事件函数就一目了然了:
void RS_EventHandler::mouseReleaseEvent(QMouseEvent* e) {
if(hasAction()){
currentActions.last()->mouseReleaseEvent(e);
// Clean up actions - one might be finished now
cleanUp();
e->accept();
} else {
// ...
}
}
所有事件都是 currentActions.last()->xxx(),也就是栈顶。而且注意 cleanUp() 的调用位置——每次鼠标释放之后都会检查一遍,因为一次点击很可能就让某个 Action 结束了。
mousePressEvent()、mouseMoveEvent()、keyPressEvent() 的结构完全一样,这里就不一一贴了。
6.4、一个彩蛋:返回按钮其实是伪造的右键
这里还有一个我觉得很有意思的实现。LibreCAD 的工具栏和菜单里有一个"返回/后退"的按钮,用来在绘图过程中退回上一步。按直觉,它应该有一套自己的回退逻辑。但实际上它是这样的:
void RS_EventHandler::back() {
QMouseEvent e(QEvent::MouseButtonRelease, QPoint(0,0),
Qt::RightButton, Qt::RightButton,Qt::NoModifier);
mouseReleaseEvent(&e);
// ...
}
它手工构造了一个"右键鼠标释放"事件,然后直接丢给 mouseReleaseEvent() 去处理。
也就是说,点"返回"按钮和按鼠标右键,在底层走的是同一条路。这样才能保证两种操作的行为永远一致,不需要把同一套回退逻辑写两遍。这种"用伪造事件复用逻辑"的手法,在 GUI 程序里其实很常见,但在这个地方看到还是让人眼前一亮。
七、RS_PreviewActionInterface:预览型 Action 多出来的东西
现在我们可以兑现第四篇的承诺了。有些 Action 需要预览——画线的时候鼠标一动,屏幕上就有一条跟着走的线;画圆的时候能看到半径变化。这类 Action 都继承自 RS_PreviewActionInterface。
class RS_PreviewActionInterface : public RS_ActionInterface {
public:
RS_PreviewActionInterface(const char* name,
RS_EntityContainer& container,
RS_GraphicView& graphicView);
void init(int status=0) override;
void finish(bool updateTB=true) override;
void suspend() override;
void resume() override;
void trigger() override;
void drawPreview();
void deletePreview();
protected:
std::unique_ptr<RS_Preview> preview;
bool hasPreview;
};
它比基类多了一个成员 preview 和一个标记 hasPreview,以及两个方法。而它重写的四个生命周期函数,每一个都只多做了一件事:
void RS_PreviewActionInterface::init(int status) {
deletePreview();
RS_ActionInterface::init(status);
}
void RS_PreviewActionInterface::finish(bool updateTB) {
deletePreview();
RS_ActionInterface::finish(updateTB);
}
void RS_PreviewActionInterface::suspend() {
RS_ActionInterface::suspend();
deletePreview();
}
void RS_PreviewActionInterface::resume() {
RS_ActionInterface::resume();
drawPreview();
}
规律非常整齐:
开始之前,把上一次残留的预览清掉;
结束的时候,把预览清掉;
挂起的时候,把预览收起来(不然切换到别的工具,屏幕上还飘着一条假线);
恢复的时候,把预览重新画出来。
用一句话概括就是:普通 Action 只管交互,预览型 Action 还要额外负责"临时图形"的出生、隐藏和复活。
而且这里正好能解释第六篇留下的一个疑问。当时我们看到 mouseMoveEvent() 里反复出现这样两行:
deletePreview();
RS_Line *line = new RS_Line(pPoints->data.startpoint, mouse);
preview->addEntity(line);
...
drawPreview();
当时我们只说了"这是预览线"。现在我们看看 drawPreview() 到底做了什么:
void RS_PreviewActionInterface::drawPreview() {
RS_EntityContainer *container=graphicView->getOverlayContainer(RS2::ActionPreviewEntity);
container->clear();
container->setOwner(false); // Little hack for now so we don't delete the preview twice
container->addEntity(preview.get());
graphicView->redraw(RS2::RedrawOverlay);
hasPreview=true;
}
关键在于最后两行的前两行:预览实体并不是加到图纸里去的,而是加到了一个叫 getOverlayContainer(RS2::ActionPreviewEntity) 的叠加层容器里;而且重绘时用的标志不是 RedrawDrawing(重绘图纸),而是 RedrawOverlay(只重绘叠加层)。
这就说明了一件很重要的事:预览线从头到尾都不是图纸数据,它只是压在图纸上面的一层临时图形。所以无论你怎么移动鼠标,RS_Document 里的实体数量都不会变,保存文件时也不会有多出来的线。
关于叠加层和重绘标志的完整机制,我们留到后面专门用一篇来讲。
八、一个画线工具是怎么"退出的"
最后我们把生命周期收个尾。前面讲了这么多,其实读者的感受是次要的——用户真正关心的是:我怎么退出这个画线工具?
在 LibreCAD 里,退出画线有三条路,而它们背后的实现完全不一样。
第一条路,按鼠标右键。这就是我们第二节讲的那条链路:右键触发 init(getStatus() - 1),状态变成 -1,setStatus() 调用 finish(),Action 结束。
第二条路,按 Esc 键。这个和 Action 本身没什么关系,它发生在更上层。QC_ApplicationWindow::keyPressEvent() 里有一个分支:
case Qt::Key_Escape:
slotKillAllActions();
它会去调用每一个视图的 killAllActions(),最终落到 EventHandler 上:
void RS_EventHandler::killAllActions()
{
if (q_action)
{
q_action->setChecked(false);
q_action = nullptr;
}
for(auto p: currentActions)
{
if (!p->isFinished())
{
p->finish();
}
}
if (!defaultAction->isFinished())
{
defaultAction->finish();
}
}
它是"一口气结束所有 Action",包括默认 Action。
这里有两个细节值得注意。一是第一段 q_action->setChecked(false)——这解释了一个用户能直接观察到的现象:按完 Esc,工具栏上那个"画线"按钮会自己弹起来。因为那个按钮是 checkable 的,程序在这里主动把它取消选中了。
二是 defaultAction->finish() 这个调用——按我们前面的分析,finish() 里明明有一句"默认 Action 拒绝退出",这里为什么又能调成功?答案是它确实什么都没做,只是被调用了一次而已。这也算是对第五节那个自保逻辑的一个有趣印证。
第三条路,在命令行里输入 escape。这条路径比较有意思:QG_ActionHandler 里把 escape 这个命令映射到了 RS2::ActionEditKillAllActions,然后又绕回上面那个 killAllActions()。
也就是说,从命令行敲 escape,和按键盘上的 Esc 键,最终是殊途同归的。这条线索也提前告诉我们:命令行的命令和工具栏的按钮,最后都会汇聚到同一套 Action 体系上——这个我们会在讲命令行系统的时候展开。
总结
这篇文章我们做了一件前几篇一直没做但很重要的事:把 Action 这个基类翻开,看看设计者给"一次用户交互"规定了哪些环节。 回过头看,可以整理成这样一张表:
| 阶段 | 入口 | 谁调用 | 发生了什么 |
|---|---|---|---|
| 诞生 | 构造函数 | LC_ActionFactory / QG_ActionHandler | 抓住 graphic 与 document,status=0,finished=false |
| 启动 | init(0) | RS_EventHandler::setCurrentAction() | 初始化捕捉器,刷新按钮提示与鼠标光标 |
| 交互 | 鼠标 / 坐标 / 命令事件 | EventHandler 转发给栈顶 Action | 子类推进 status,更新预览 |
| 完成 | trigger() | 子类自己调用 | 创建实体、登记撤销、继续或退出 |
| 结束 | finish() | setStatus(-1) / killAllActions() | status=-1,finished=true,隐藏选项,结束捕捉器 |
| 回收 | cleanUp() | EventHandler | 从栈中删除,恢复栈顶或默认 Action |
几个关键结论可以再强调一遍:
第一,Action 不是"一个画图函数",而是一次完整交互的管理者。 它有明确的出生、待命、被打断、恢复和退场流程,这些流程全部都写在基类里,子类只负责填"这次交互具体要做什么"。
第二,"结束"在这套体系里是一个负数状态。 这正是第六篇里 init(getStatus() - 1) 的含义:把状态退到负数,就等于结束。
第三,trigger() 和 finish() 不是一回事。 前者是"这一次动作完成了",后者是"这个工具退出了"。画完一条线还能接着画第二条,靠的就是这个区别。
第四,预览与图纸数据是分开的。 预览型 Action 通过叠加层来显示临时图形,图纸数据本身不受影响。
到这一步,再回头看第四篇那句"这些动作都是继承自 RS_PreviewActionInterface",它就不再只是一个名词了。Action 这个基类规定的,其实是一次用户交互的完整礼仪:怎么进场、怎么待命、怎么被打断、怎么体面地退场。
🎇坚持到这里已经很厉害啦,辛苦啦🎇 ʕ • ᴥ • ʔ づ♡ど
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/2302_80177460/article/details/165008695





