← 返回首页

CATIA CAA 交互状态机开发指南

CATIA CAA 交互状态机开发指南

作者:CATIA开发者指南
发布时间:2026年6月15日 10:14
原文链接:https://mp.weixin.qq.com/s?__biz=MzkxNTQ0MTY0NQ==&mid=2247486522&idx=1&sn=50080395ff4940885e211afe2e18fb7f&chksm=c15e5eb8f629d7aee112f4f054ee216cb3f10ac2a4e1c23335a7d7696dc7e5b50c3c227627aa&cur_album_id=2800898825187262468&scene=189#wechat_redirect


图片

CAA 里的很多交互命令,本质上不是“一次函数调用”,而是一段由用户输入驱动的流程。用户先选择一个平面,再点圆心,再输入半径;或者先选择第一点,再选择第二点,最后创建一条线。这样的命令如果用普通 C++ 的顺序代码硬写,很快就会被鼠标事件、选择过滤、状态提示、取消、预览和 Undo 搅在一起。

CATStateCommand 的价值就在这里:它把一个交互命令建模成状态机。

你不再直接追着每一次鼠标点击写流程,而是声明:

  • 当前有几个状态;
  • 每个状态等待哪些输入;
  • 输入满足什么条件才能离开当前状态;
  • 离开状态时执行什么动作;
  • 下一个状态是什么;
  • 到哪个状态时命令结束。

本文专门讲这套状态机的心智模型和写法。读完之后,再看 BuildGraph 里的 GetInitialStateAddDialogStateAddDialogAgentAddTransitionIsOutputSetConditionConditionAction,就不会像在看一串神秘咒语。

1. 先建立一张状态图

一个典型 CATStateCommand 可以抽象成下面这张图:

State A
  等待 Agent A 输出
  -> Condition 为 TRUE
  -> 执行 Action
  -> 进入 State B

State B
  等待 Agent B 输出
  -> Condition 为 TRUE
  -> 执行 Action
  -> 进入 NULL
  -> 命令结束

在 CAA 代码里对应关系如下:

状态机概念 CAA 类/函数 作用
命令 CATStateCommand 整个交互命令的容器,也是状态图的拥有者
状态 CATDialogState 一个输入阶段,例如“选择平面”“指定圆心”“输入半径”
输入代理 CATDialogAgent 派生类 收集用户输入,例如选择对象、点屏幕、点按钮
条件 CATStateCondition 判断 transition 是否可以触发
动作 CATDiaAction transition 触发时执行的业务逻辑
转移 CATDialogTransition 从一个状态到另一个状态的边
结束 target state 为 NULL 到达空状态后命令结束

最核心的一句话是:

State 负责等输入,Transition 负责判断和推进,Action 负责真正干活。

很多初学者会把业务代码写进选择回调,或者在 agent 里塞太多逻辑。更稳的做法是让 agent 只负责采集输入,让 condition 判断输入是否合格,让 action 修改模型或保存阶段结果。

2. CATStateCommand 的生命周期

一个主交互命令通常继承 CATStateCommand

#include"CATStateCommand.h"

classMyCreateLineCmd : public CATStateCommand
{
CmdDeclareResource(MyCreateLineCmd, CATStateCommand);

public:
MyCreateLineCmd();
virtual ~MyCreateLineCmd();

virtualvoidBuildGraph();

private:
CATBoolean CheckStartPoint(void *iData);
CATBoolean CheckEndPoint(void *iData);
CATBoolean StoreStartPoint(void *iData);
CATBoolean CreateLine(void *iData);
};

主命令常见生命周期是:

命令头触发命令
  -> 创建 CATStateCommand 派生对象
  -> DialogEngine 调用 BuildGraph
  -> 第一次 Activate
  -> 状态机开始接收 agent 输出
  -> transition 条件满足时执行 action 并切换状态
  -> 到达 NULL state 或 cancel state
  -> 命令结束并由内部机制释放

BuildGraph 很重要。它通常只在命令第一次 Activate 之前调用一次,用来定义整张状态图。不要把需要每次输入都变化的临时逻辑塞进 BuildGraph;它更像“搭舞台”,不是“演每一场戏”。

如果命令修改模型,构造 CATStateCommand 时通常使用默认的 CATCommandModeExclusive。默认独占模式会拿到焦点并取消其它独占命令,这对建模命令是合理的。共享模式 CATCommandModeShared 更适合不直接修改模型、只临时查看或辅助选择的命令。

3. 一个最小状态机

假设要创建一条线:用户点第一点,再点第二点,然后命令结束。状态图可以写成:

stStartPoint
  agent: indication
  condition: 有点输入 && 起点合法
  action: 保存/创建起点
  -> stEndPoint

stEndPoint
  agent: indication
  condition: 有点输入 && 终点合法
  action: 创建线
  -> NULL

对应的 BuildGraph 骨架:

#include"CATIndicationAgent.h"
#include"CATDialogState.h"
#include"CATDialogTransition.h"

voidMyCreateLineCmd::BuildGraph()
{
  _pointAgent = newCATIndicationAgent("PointIndicationId");
  _pointAgent->SetBehavior(CATDlgEngWithUndo);

  CATDialogState *stStart = GetInitialState("stStartPointId");
  stStart->AddDialogAgent(_pointAgent);

  CATDialogState *stEnd = AddDialogState("stEndPointId");
  stEnd->AddDialogAgent(_pointAgent);

AddTransition(
    stStart,
    stEnd,
AndCondition(
IsOutputSetCondition(_pointAgent),
Condition((ConditionMethod)&MyCreateLineCmd::CheckStartPoint)),
Action((ActionMethod)&MyCreateLineCmd::StoreStartPoint));

AddTransition(
    stEnd,
NULL,
AndCondition(
IsOutputSetCondition(_pointAgent),
Condition((ConditionMethod)&MyCreateLineCmd::CheckEndPoint)),
Action((ActionMethod)&MyCreateLineCmd::CreateLine));
}

这就是 CAA 状态机最经典的形式:两个状态、一个 agent、两条 transition。第一条 transition 进入下一个状态,第二条 transition 的目标是 NULL,表示命令结束。

4. BuildGraph 里做哪几件事

BuildGraph 通常按四步写:

1. 创建 agent
2. 设置 agent 类型、行为、过滤器
3. 创建 state,并把 agent 加到 state
4. 创建 transition,把 state、condition、action 串起来

示意结构如下:

voidMyCommand::BuildGraph()
{
// 1. Agents
  _selectionAgent = newCATPathElementAgent("SelectionId");
  _selectionAgent->AddElementType(IID_CATIExpectedInterface);
  _selectionAgent->SetBehavior(CATDlgEngWithPSOHSO | CATDlgEngWithPrevaluation);

  _okAgent = newCATDialogAgent("OkId");

// 2. States
  CATDialogState *stSelect = GetInitialState("stSelectId");
  stSelect->AddDialogAgent(_selectionAgent);

  CATDialogState *stConfirm = AddDialogState("stConfirmId");
  stConfirm->AddDialogAgent(_okAgent);

// 3. Transitions
AddTransition(
    stSelect,
    stConfirm,
IsOutputSetCondition(_selectionAgent),
Action((ActionMethod)&MyCommand::StoreSelection));

AddTransition(
    stConfirm,
NULL,
IsOutputSetCondition(_okAgent),
Action((ActionMethod)&MyCommand::Apply));
}

有一个小习惯很有用:先在纸上或注释中写状态图,再写 BuildGraph。如果状态图说不清,代码通常也会绕。

5. State:一个状态就是一个输入阶段

CATDialogState 不是业务对象,它是交互阶段。你不要直接 new CATDialogState,而应该通过 CATStateCommand 创建:

CATDialogState *stFirst = GetInitialState("stFirstId");
CATDialogState *stSecond = AddDialogState("stSecondId");

GetInitialState 创建或取得初始状态。一个命令只有一个初始状态。AddDialogState 创建普通状态。

每个 state 可以添加一个或多个 agent:

stInput->AddDialogAgent(_planeAgent);
stInput->AddDialogAgent(_pointAgent);
stInput->AddDialogAgent(_okAgent);

一个 state 有多个 agent 时,意思是这个阶段可以接收多种输入。例如:

  • 同一状态中既可以选择对象,也可以点对话框按钮;
  • 同一状态中需要两个 agent 都有输出才往下走;
  • 同一状态中用户点 OK、Cancel、Close 会触发不同 transition;
  • 一个 agent 用于主输入,另一个 agent 用于动态预览或结束命令。

状态还有进入动作和离开动作:

stInput->SetEnterAction(Action((ActionMethod)&MyCommand::EnterInputState));
stInput->SetLeaveAction(Action((ActionMethod)&MyCommand::LeaveInputState));

实际项目里更常见的是把业务动作挂到 transition 上。SetEnterActionSetLeaveAction 适合处理“进入这个阶段时初始化 UI”“离开这个阶段时清理临时显示”这类状态级逻辑。

6. Agent:状态机的输入源

agent 是状态机感知用户输入的入口。常见 agent 有:

Agent 用途
CATPathElementAgent 选择模型对象,输出 CATPathElement
CATIndicationAgent 在视图区点一个位置,常用于点、方向、距离
CATDialogAgent 接收对话框控件通知,例如 OK、Apply、Close
CATFeatureAgent 选择 feature
CATOtherDocumentAgent 在其它已打开文档中选择对象

一个 agent 的典型配置包括选择类型和行为:

_pathAgent = newCATPathElementAgent("PathSelectionId");
_pathAgent->AddElementType(IID_CATISpecObject);
_pathAgent->SetBehavior(
  CATDlgEngWithPSOHSO |
  CATDlgEngWithPrevaluation |
  CATDlgEngRepeat |
  CATDlgEngNewHSOManager);

常见 behavior 可以这样理解:

Behavior 直观含义
CATDlgEngWithPSOHSO 预选/选中时参与高亮显示
CATDlgEngWithPrevaluation 鼠标预选时也做预估值处理
CATDlgEngRepeat agent 可以重复使用
CATDlgEngNewHSOManager 管理 HSO,让最后选择更干净
CATDlgEngWithUndo agent 输出参与 Undo 机制
CATDlgEngMultiAcquisition 多选采集

behavior 不只是视觉效果,它会影响 agent 输出何时被认为有效、是否保留上一次值、是否支持多选等。状态机出问题时,除了看 condition,也要看 behavior。

7. Transition:状态图里的边

AddTransition 是状态机真正推进的地方:

AddTransition(
  sourceState,
  targetState,
  condition,
  action);

四个参数可以这样读:

当 sourceState 处于活动状态时,
如果 condition 为 TRUE,
就执行 action,
然后进入 targetState。

target state 可以是:

  • 普通状态:进入下一阶段;
  • 当前状态:形成自循环,用于重复输入或动态预览;
  • NULL:结束命令;
  • cancel state:结束命令并执行全局 Undo。

最简单的 transition:

AddTransition(
  stSelect,
NULL,
IsOutputSetCondition(_selectionAgent),
Action((ActionMethod)&MyCommand::CreateObject));

如果 condition 为 NULL,CAA 文档里说明它会被当成 TRUE。也就是说,某个 agent 一有 valuation,transition 就可能触发。简单命令可以这么用,但实际项目里建议显式写 condition,代码可读性更好。

8. Condition:判断能不能往下走

最常见条件是:

IsOutputSetCondition(_agent)

它表示 agent 已经有输出。对于选择 agent,就是用户选到了对象;对于 indication agent,就是用户点了一个位置;对于 dialog agent,就是对应通知到达。

业务检查通常用 Condition 包一层成员函数:

CATBoolean MyCommand::CheckSelection(void *iData)
{
  CATPathElement *path = _selectionAgent->GetValue();
if (NULL == path || 0 == path->GetSize())
  {
return FALSE;
  }

  CATBaseUnknown *leaf = (*path)[path->GetSize() - 1];
if (NULL == leaf)
  {
return FALSE;
  }

  CATISpecObject *spec = NULL;
  HRESULT hr = leaf->QueryInterface(IID_CATISpecObject, (void **)&spec);
if (FAILED(hr) || NULL == spec)
  {
return FALSE;
  }

  spec->Release();
  spec = NULL;

return TRUE;
}

在 transition 中组合:

AddTransition(
  stSelect,
  stNext,
AndCondition(
IsOutputSetCondition(_selectionAgent),
Condition((ConditionMethod)&MyCommand::CheckSelection)),
Action((ActionMethod)&MyCommand::StoreSelection));

常用组合有:

组合 含义
AndCondition(cond1, cond2) 两个条件都满足
OrCondition(cond1, cond2) 任一条件满足
自定义 Condition 调用成员函数做业务判断

一个很重要的习惯:condition 不要修改模型。它最好只做判断。如果需要保存输入、创建对象、更新显示,放到 action 里。

9. Action:真正执行命令逻辑

action 是 transition 被触发时执行的业务方法:

CATBoolean MyCommand::StoreSelection(void *iData)
{
  CATPathElement *path = _selectionAgent->GetValue();
if (NULL == path || 0 == path->GetSize())
  {
return FALSE;
  }

  CATBaseUnknown *leaf = (*path)[path->GetSize() - 1];
if (NULL != leaf)
  {
    leaf->AddRef();
    _selectedObject = leaf;
  }

return TRUE;
}

创建 action:

Action((ActionMethod)&MyCommand::StoreSelection)

action 返回 CATBoolean。通常:

  • 返回 TRUE 表示动作成功;
  • 返回 FALSE 表示动作失败,具体行为要结合 transition 和 command 设计;
  • action 中如果可能抛异常,应自己捕获并转成可控返回,不要把异常交给 DialogEngine 才发现。

CAA 文档提到,condition 和 action 中抛出的异常如果被 DialogEngine 捕获,主命令会被反激活并显示默认错误对话框,关闭错误对话框后再恢复。这个行为对用户来说比较突兀,所以业务代码最好自己处理错误。

多个 action 也可以组合:

AndAction(
Action((ActionMethod)&MyCommand::StoreInput),
Action((ActionMethod)&MyCommand::UpdatePreview))

但不要滥用。组合太多时,通常说明这个 transition 的职责过重,可以拆成更清楚的私有方法。

10. 复用同一个 agent

本地 CAA 示例里创建线命令使用同一个 CATIndicationAgent 采集起点和终点:

_daIndication = newCATIndicationAgent("StartPointIndication");

CATDialogState *stStartState = GetInitialState("stStartPointId");
stStartState->AddDialogAgent(_daIndication);

CATDialogState *stEndState = AddDialogState("stEndPointId");
stEndState->AddDialogAgent(_daIndication);

这很常见。一个 agent 可以被多个 state 使用,前提是你清楚它的输出什么时候被消费、什么时候需要重新初始化。

例如第二个点不能等于第一个点,检查失败时可以重新初始化采集:

CATBoolean MyCreateLineCmd::CheckEndPoint(void *iData)
{
  CATMathPoint2D point2D = _pointAgent->GetValue();

if (IsSameAsStartPoint(point2D))
  {
    _pointAgent->InitializeAcquisition();
return FALSE;
  }

return TRUE;
}

复用 agent 的优点是代码少、交互一致;缺点是如果忘记清理输出,旧值可能影响下一条 transition。遇到“状态突然跳过去”“condition 一直为 TRUE”这类问题,优先检查 agent 是否还保留着上一轮输出。

11. 自循环 transition

并不是所有 transition 都要进入下一个状态。很多命令会使用自循环:

AddTransition(
  stInput,
  stInput,
IsOutputSetCondition(_previewAgent),
Action((ActionMethod)&MyCommand::UpdatePreview));

自循环适合:

  • 鼠标移动时更新临时预览;
  • 用户反复选择多个点;
  • 参数改变后保持在当前阶段;
  • 对话框 Apply 后不结束命令;
  • 多段线、草图点列、重复创建同类对象。

自循环要特别注意 action 的副作用。因为状态没有真正离开,state 的 leave action 和 enter action 通常不会按普通切换那样执行。临时几何、HSO、ISO、预览对象的清理要自己设计好。

12. 多输入状态

有时一个状态要等多个输入都完成。例如创建圆柱时,同一个状态可以同时等待一条线和一个圆:

stCylinder->AddDialogAgent(_lineAgent);
stCylinder->AddDialogAgent(_circleAgent);

AddTransition(
  stCylinder,
NULL,
AndCondition(
IsOutputSetCondition(_lineAgent),
IsOutputSetCondition(_circleAgent)),
Action((ActionMethod)&MyCommand::CreateCylinder));

这类写法适合“输入顺序不重要”的命令。用户可以先选线,也可以先选圆。只有两个 agent 都有输出时,才创建目标对象。

如果输入顺序很重要,比如必须先选支撑平面,再基于平面投影点,那么更适合拆成多个 state:

stPlane -> stCenter -> stRadius -> NULL

判断该拆还是该合,可以用一个简单标准:

  • 输入之间没有依赖,放在同一个 state;
  • 后一个输入依赖前一个输入的结果,拆成多个 state;
  • 中间要更新提示、预览、过滤器或临时坐标系,也拆成多个 state。

13. 结束命令和取消命令

AddTransition 的目标状态为 NULL 时,表示命令结束:

AddTransition(
  stLast,
NULL,
IsOutputSetCondition(_okAgent),
Action((ActionMethod)&MyCommand::Apply));

CAA 文档里还提到 cancel state:

CATDialogState *stCancel = GetCancelState();

到达 cancel state 时,命令像到达 NULL 一样结束,同时执行 global Undo。一般场景下,用户按 Esc 或其它独占命令抢占焦点时,命令也会被 focus manager 取消。

实现命令时可以按需要覆盖:

CATStatusChangeRC MyCommand::Activate(CATCommand *cmd, CATNotification *notif);
CATStatusChangeRC MyCommand::Desactivate(CATCommand *cmd, CATNotification *notif);
CATStatusChangeRC MyCommand::Cancel(CATCommand *cmd, CATNotification *notif);

但不要一上来就覆盖这些生命周期函数。多数命令只需要 BuildGraph、condition 和 action。生命周期函数适合做:

  • 命令激活时创建或显示对话框;
  • 反激活时临时隐藏 UI;
  • 取消时清理临时对象;
  • 释放 HSO/ISO 里的临时高亮;
  • 关闭自定义面板或取消订阅通知。

14. 一个较完整的创建线示例

下面是一个压缩后的创建线命令,保留状态机主干:

classMyCreateLineCmd : public CATStateCommand
{
CmdDeclareResource(MyCreateLineCmd, CATStateCommand);

public:
MyCreateLineCmd();
virtual ~MyCreateLineCmd();
virtualvoidBuildGraph();

private:
CATBoolean CheckStartPoint(void *iData);
CATBoolean CheckEndPoint(void *iData);
CATBoolean StoreStartPoint(void *iData);
CATBoolean CreateLine(void *iData);

private:
  CATIndicationAgent *_pointAgent;
  CATMathPoint _startPoint;
  CATBoolean _hasStartPoint;
};

实现:

MyCreateLineCmd::MyCreateLineCmd()
  : CATStateCommand("MyCreateLineCmdId"),
    _pointAgent(NULL),
    _hasStartPoint(FALSE)
{
}

MyCreateLineCmd::~MyCreateLineCmd()
{
if (NULL != _pointAgent)
  {
    _pointAgent->RequestDelayedDestruction();
    _pointAgent = NULL;
  }
}

voidMyCreateLineCmd::BuildGraph()
{
  _pointAgent = newCATIndicationAgent("PointIndicationId");
  _pointAgent->SetBehavior(CATDlgEngWithUndo);

  CATDialogState *stStart = GetInitialState("stStartPointId");
  stStart->AddDialogAgent(_pointAgent);

  CATDialogState *stEnd = AddDialogState("stEndPointId");
  stEnd->AddDialogAgent(_pointAgent);

AddTransition(
    stStart,
    stEnd,
AndCondition(
IsOutputSetCondition(_pointAgent),
Condition((ConditionMethod)&MyCreateLineCmd::CheckStartPoint)),
Action((ActionMethod)&MyCreateLineCmd::StoreStartPoint));

AddTransition(
    stEnd,
NULL,
AndCondition(
IsOutputSetCondition(_pointAgent),
Condition((ConditionMethod)&MyCreateLineCmd::CheckEndPoint)),
Action((ActionMethod)&MyCreateLineCmd::CreateLine));
}

条件和动作:

CATBoolean MyCreateLineCmd::CheckStartPoint(void *iData)
{
return TRUE;
}

CATBoolean MyCreateLineCmd::StoreStartPoint(void *iData)
{
  CATMathPoint2D point2D = _pointAgent->GetValue();
  CATMathPlane plane = _pointAgent->GetMathPlane();

  plane.EvalPoint(point2D.GetX(), point2D.GetY(), _startPoint);
  _hasStartPoint = TRUE;

return TRUE;
}

CATBoolean MyCreateLineCmd::CheckEndPoint(void *iData)
{
if (FALSE == _hasStartPoint)
  {
return FALSE;
  }

  CATMathPoint2D point2D = _pointAgent->GetValue();
  CATMathPlane plane = _pointAgent->GetMathPlane();

  CATMathPoint endPoint;
  plane.EvalPoint(point2D.GetX(), point2D.GetY(), endPoint);

if (endPoint.DistanceTo(_startPoint) < 1e-6)
  {
    _pointAgent->InitializeAcquisition();
return FALSE;
  }

return TRUE;
}

CATBoolean MyCreateLineCmd::CreateLine(void *iData)
{
  CATMathPoint2D point2D = _pointAgent->GetValue();
  CATMathPlane plane = _pointAgent->GetMathPlane();

  CATMathPoint endPoint;
  plane.EvalPoint(point2D.GetX(), point2D.GetY(), endPoint);

// 这里调用自己的 factory 创建 line,并刷新模型显示。
return TRUE;
}

真实项目里,CreateLine 会查询容器、调用 factory、把对象加入集合、派发 CATCreate/CATModify 等通知。状态机只负责把“什么时候创建”这件事安排清楚。

15. 对话框和状态机怎么配合

很多命令不是纯鼠标交互,还会有对话框。推荐方式是:

  • 对话框控件发通知;
  • CATDialogAgent 或相关 agent 接收通知;
  • state transition 判断哪个按钮被点了;
  • action 读取对话框参数并执行。

示意:

_okAgent = newCATDialogAgent("OkId");
_applyAgent = newCATDialogAgent("ApplyId");
_cancelAgent = newCATDialogAgent("CancelId");

stPanel->AddDialogAgent(_okAgent);
stPanel->AddDialogAgent(_applyAgent);
stPanel->AddDialogAgent(_cancelAgent);

AddTransition(
  stPanel,
NULL,
IsOutputSetCondition(_okAgent),
Action((ActionMethod)&MyCommand::ApplyAndClose));

AddTransition(
  stPanel,
  stPanel,
IsOutputSetCondition(_applyAgent),
Action((ActionMethod)&MyCommand::ApplyPreview));

AddTransition(
  stPanel,
NULL,
IsOutputSetCondition(_cancelAgent),
Action((ActionMethod)&MyCommand::CancelPanel));

这种设计比在按钮回调里直接改模型更清楚,因为所有交互入口都回到状态图里,命令流程不会散。

16. 状态提示和资源 ID

GetInitialState("stStartPointId")AddDialogState("stEndPointId") 里的字符串不是随便起的。它们通常会关联命令消息资源,用于状态栏提示。

例如:

CATDialogState *stStart = GetInitialState("stStartPointId");
CATDialogState *stEnd = AddDialogState("stEndPointId");

资源里可以配置类似:

MyCreateLineCmd.stStartPointId = Select the start point.
MyCreateLineCmd.stEndPointId = Select the end point.

实际团队中建议资源 ID 命名保持语义清楚:

  • stSelectSupportId
  • stPickCenterId
  • stInputRadiusId
  • stConfirmId

状态提示是交互命令体验的一部分。状态机写得再漂亮,如果用户不知道下一步该点什么,命令还是会显得笨。

17. 常见设计模式

顺序采集

stInputA -> stInputB -> stInputC -> NULL

适合创建线、圆、平面、拉伸等明确步骤。

同状态多输入

stInput(A agent + B agent)
  -> A 和 B 都有输出
  -> NULL

适合输入顺序无关的场景。

Apply 自循环

stPanel --Apply--> stPanel
stPanel --OK-----> NULL

适合对话框命令。

预览自循环

stInput --鼠标移动/参数变化--> stInput
stInput --确认-------------> NULL

适合 rubber band、临时几何、动态显示。

可重复创建

stInput -> action 创建一个对象 -> stInput
stInput -> 用户结束 -> NULL

适合连续画点、连续创建孔、连续选择对象做同一操作。

18. 调试状态机的方法

状态机问题通常表现为:

  • 用户点了对象但命令没反应;
  • transition 过早触发;
  • action 执行了但没有进入下一个状态;
  • 命令突然结束;
  • Esc 后临时对象没有清掉;
  • 重复输入时拿到了旧值。

建议按这个顺序查:

  1. agent 是否添加到了当前 state。
  2. agent 的 AddElementType 或过滤器是否过严。
  3. behavior 是否符合预期,尤其是 repeat、多选和 HSO 相关设置。
  4. IsOutputSetCondition(agent) 是否真的变为 TRUE。
  5. 自定义 Condition 是否返回 FALSE。
  6. action 是否返回 FALSE 或抛异常。
  7. transition 的 target 是否误写成 NULL
  8. 复用 agent 时是否需要 InitializeAcquisition
  9. 是否有另一个 transition 更早匹配。
  10. 命令是否被其它独占命令抢占焦点。

在 CAA 示例里经常能看到 cout 打印构造、析构、BuildGraph、condition 和 action。这种方法虽然朴素,但对理解状态机推进非常有效。建议初学阶段也这么做。

19. 常见坑

1. 在构造函数里创建 state

不要。state、transition、condition、action 应该在 BuildGraph 中创建。构造函数适合初始化成员、取得当前 editor、保存根对象或容器指针。

2. 直接 new CATDialogState

不要直接使用 CATDialogState 构造函数。用 GetInitialStateAddDialogState,让 CATStateCommand 管理状态生命周期。

3. 把模型修改写进 condition

condition 应该判断,action 才应该执行。否则 condition 被重复评估时,很容易产生重复创建或难以回滚的副作用。

4. 忘记 target 为 NULL 会结束命令

AddTransition(st, NULL, cond, action) 是结束命令,不是“没有下一个状态先放空”。如果还要继续采集,target 应该写下一个 state 或当前 state。

5. 复用 agent 却不清空旧输出

同一个 agent 被多个 state 使用时,旧输出可能让下一条 transition 立即满足。必要时调用 InitializeAcquisition

6. 多条 transition 条件重叠

同一 state 有多条 transition 时,条件要互斥或优先级要清楚。否则用户一次输入可能触发你没想到的路径。

7. action 里忘记刷新显示

状态机只负责流程,不自动帮你更新模型树或 3D 显示。创建或修改模型后,该派发 CATCreateCATModify 或相关通知的地方仍然要做。

8. 没有处理 Cancel

命令结束路径不只有成功创建。Esc、窗口切换、其它命令抢焦点、对话框 Close 都可能中断流程。临时对象和引用计数要能被清理。

9. 把状态拆得太碎或合得太大

状态太碎,代码像流水账;状态太大,condition 复杂得像谜题。经验上,一个 state 对应用户脑子里的一步,通常最容易维护。

20. 推荐写法清单

写一个新的 CATStateCommand 时,可以按下面顺序做:

1. 用自然语言写出用户流程
2. 画出状态图
3. 为每个状态命名,准备资源 ID
4. 为每类输入选择 agent
5. 决定输入类型、过滤器和 behavior
6. 在 BuildGraph 中创建 agent
7. 创建 initial state 和普通 state
8. 把 agent 添加到对应 state
9. 为每条边写 condition
10. 为每条边写 action
11. target 为 NULL 的路径只放真正结束命令的地方
12. 析构中 RequestDelayedDestruction agent,并释放 AddRef 的对象
13. 在 condition/action 中打印日志调试状态推进
14. 最后再补状态栏提示和错误提示

一个命令的状态机设计得好,后面加功能也会比较顺:新增一个输入,通常就是新增一个 state 或 agent;新增一个按钮,通常就是新增一条 transition;新增一个业务校验,通常就是新增一个 condition。

21. 总结

CATStateCommand 的状态机不是 CAA 里的附加概念,而是交互命令的骨架。它把“用户如何一步步输入”和“程序什么时候执行业务逻辑”分开,让命令可以在选择、对话框、预览、取消、Undo 和模型更新之间保持秩序。

可以把它记成一句话:

Agent 采集输入,State 组织阶段,Condition 判断有效性,Action 执行业务,Transition 推进流程。

当命令很简单时,状态机看起来像多写了几行模板代码;当命令开始支持多输入、动态预览、Apply、Cancel、跨文档选择或连续创建时,这套结构就会显出价值。它让复杂交互仍然能被拆成一张可读的图,而不是一团事件回调。

评论