励志不掉头发的内向程序员头像
关注
【LibreCAD 2D架构】从 init() 到 finish():Action 基类与一次交互的完整生命周期封面图

【LibreCAD 2D架构】从 init() 到 finish():Action 基类与一次交互的完整生命周期

请添加图片描述


🫧 励志不掉头发的内向程序员个人主页

 ✨️ 个人专栏: 《C++语言》《Linux学习》

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


👓️博主简介:

请添加图片描述



前言

在第四篇里,我们分析了 LibreCAD 的 Action 与命令系统:用户点击工具栏之后,LC_ActionFactory 创建 QActionQG_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();
    // ...
};

在这里插入图片描述

从这个类的声明里,我们至少能读出三件事。

第一,它同时继承了 QObjectRS_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 = 0finished = falseactionType = 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 = 0SetEndpoint = 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

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--