← 返回首页

CATIA V5 CAA 对象模型、接口与扩展开发指南

前面几篇文章已经分别讨论了自定义特征、命令/工作台接入和 mkmk 构建体系。本文换一个更底层的角度:CAA 里的对象到底是怎样被“看见”和“扩展”的。

很多 C++ 开发者刚接触 CAA 时,会自然地把问题想成“我写一个类,继承某个基类,然后 new 出来”。但在 CATIA V5 的 CAA 世界里,更常见的思路不是直接继承使用,而是:对象保持自己的运行时身份,外部代码通过接口去请求能力;某个 late type 能不能支持某个接口,由 dictionary、TIE/BOA 和模块加载机制共同决定。

如果说自定义特征回答的是“我要定义什么模型对象”,命令/工作台回答的是“用户如何触发功能”,那么对象模型和接口机制回答的是“CATIA 运行时如何找到你的能力”。理解这一层之后,再看 CATISpecObject_var spFeature = object;TIE_xxx(MyClass).dico 里的三列注册信息,就不会像在看一堆魔法宏。

1. 先建立一条主线

CAA 的核心思路可以压缩成一条链路:

调用方拿到一个对象
  -> 请求某个接口
  -> 运行时根据对象 late type 和接口查 dictionary
  -> 加载声明的模块
  -> 创建 TIE/BOA 或扩展对象
  -> 返回可调用的接口指针

这和普通 C++ 虚函数调用很不一样。普通 C++ 依赖编译期类型和继承层级,CAA 更像 COM 风格的组件模型:你不关心对象的真实 C++ 类名,而是问它“你是否支持这个接口”。

典型代码会长这样:

CATISpecObject_var spSpecObject = iObject;
if (!!spSpecObject)
{
// Use CATISpecObject services here.
}

或者显式写成:

CATISpecObject *specObject = NULL;
HRESULT rc = iObject->QueryInterface(IID_CATISpecObject, (void **)&specObject);
if (SUCCEEDED(rc) && specObject != NULL)
{
// Use CATISpecObject services here.
  specObject->Release();
  specObject = NULL;
}

前一种 _var handler 写法更常见,也更不容易忘记 Release。后一种写法适合在调试接口查询、阅读旧代码或解释引用计数时使用。

2. 核心词汇表

名称 常见形式 作用
CATBaseUnknown 接口和很多实现类的共同基础 提供 CAA/COM 风格对象基础能力,支持接口查询和引用计数语义。
Interface class MyInterface : public CATBaseUnknown 对外暴露的能力契约,只声明纯虚函数,不保存业务状态。
IID IID_MyInterface 接口的唯一标识,运行时通过它判断调用方请求的是哪个接口。
Handler MyInterface_var CAA 智能指针风格包装,简化接口查询、引用计数和空值判断。
Implementation class MyCompanyEMeasure 真正实现接口函数的 C++ 类。
CATImplementClass CATImplementClass(MyClass, CodeExtension, CATBaseUnknown, LateType) 把 C++ 类声明给 CAA 元对象系统。
TIE TIE_MyInterface(MyClass) 把接口调用转发到实现类,生成运行时可返回的接口对象。
BOA BOA_MyInterface(MyClass)

 或 CATImplementBOA
另一种接口适配方式,通常按向导或本地样例使用。
Dictionary CNext/code/dictionary/*.dico 声明 late type、接口和承载模块之间的映射关系。
IID file CNext/code/dictionary/*.iid 给自定义接口分配稳定 IID。

这张表里最容易混淆的是“接口”和“实现类”。接口是别人能请求到的能力名称;实现类是你写业务逻辑的地方。一个实现类可以实现多个接口,一个 late type 也可以通过不同实现类挂接多个接口。

3. 接口不是普通基类

一个 CAA 接口通常放在 framework 的 PublicInterfacesProtectedInterfaces 或 PrivateInterfaces 中。公开给其他 framework 使用的接口放 PublicInterfaces;只给受保护依赖使用的放 ProtectedInterfaces;只在本 framework 内部使用的放 PrivateInterfaces

一个最小接口可以写成这样:

#ifndef MyCompanyIMeasure_H
#define MyCompanyIMeasure_H

#include"MyCompanyModel.h"
#include"CATBaseUnknown.h"

classCATISpecObject_var;

#ifndef LOCAL_DEFINITION_FOR_IID
extern ExportedByMyCompanyModel IID IID_MyCompanyIMeasure;
#else
extern"C"const IID IID_MyCompanyIMeasure;
#endif

classExportedByMyCompanyModel MyCompanyIMeasure : public CATBaseUnknown
{
  CATDeclareInterface;

public:
virtual HRESULT SetInput(const CATISpecObject_var &iInput)= 0;
virtual HRESULT GetInput(CATISpecObject_var &oInput)= 0;

virtual HRESULT SetLength(double iLength)= 0;
virtual HRESULT GetLength(double &oLength)= 0;
};

CATDeclareHandler(MyCompanyIMeasure, CATBaseUnknown);

#endif

几个细节值得养成习惯:

  • 接口继承 CATBaseUnknown

  • 接口类中使用 CATDeclareInterface

  • 接口函数返回 HRESULT,业务输出通过输出参数返回。

  • 接口类通常没有构造函数、析构函数和数据成员。

  • 文件底部用 CATDeclareHandler 声明 MyCompanyIMeasure_var

  • IID 名称、接口类名、handler 名称要保持一致,后面 .iid.dico 和 TIE 都会依赖这些名字。

如果接口是客户自定义 API,要特别注意二进制兼容性。已经发布给其他模块使用的接口,不要随手改函数签名、参数顺序或虚函数顺序。更稳妥的做法是新增 MyCompanyIMeasure2 之类的新接口,而不是破坏旧接口。

4. 实现类负责真正干活

接口只定义契约,真正访问属性、创建几何、更新状态的是 implementation class。假设我们要给 late type MyCompanyMeasure 挂一个 MyCompanyIMeasure 接口,实现类可以放在模块的 LocalInterfaces 和 src 中。

头文件示意如下:

#ifndef MyCompanyEMeasure_H
#define MyCompanyEMeasure_H

#include"CATBaseUnknown.h"
#include"CATISpecObject.h"

classMyCompanyEMeasure : public CATBaseUnknown
{
  CATDeclareClass;

public:
MyCompanyEMeasure();
virtual ~MyCompanyEMeasure();

HRESULT SetInput(const CATISpecObject_var &iInput);
HRESULT GetInput(CATISpecObject_var &oInput);

HRESULT SetLength(double iLength);
HRESULT GetLength(double &oLength);

private:
MyCompanyEMeasure(const MyCompanyEMeasure &);
  MyCompanyEMeasure &operator=(const MyCompanyEMeasure &);
};

#endif

实现文件里有两组关键宏:一组把类注册成 CAA class,另一组把接口绑到实现类。

#include"MyCompanyEMeasure.h"
#include"MyCompanyIMeasure.h"

#include"CATISpecAttrAccess.h"
#include"CATISpecAttrKey.h"
#include"CATIGSMAttributes.h"

CATImplementClass(
  MyCompanyEMeasure,
  CodeExtension,
  CATBaseUnknown,
  MyCompanyMeasure);

#include"TIE_MyCompanyIMeasure.h"
TIE_MyCompanyIMeasure(MyCompanyEMeasure);

MyCompanyEMeasure::MyCompanyEMeasure()
  : CATBaseUnknown()
{
}

MyCompanyEMeasure::~MyCompanyEMeasure()
{
}

CATImplementClass 的几个参数大致可以这样理解:

参数 示例 含义
C++ 类 MyCompanyEMeasure 当前实现类。
类种类 Implementation

CodeExtensionDataExtension
该类在 CAA 元对象系统中的角色。
父类 CATBaseUnknown C++ 继承基础。
late type MyCompanyMeasure

 或 CATNull
这个扩展挂在哪种运行时类型上。

普通可直接实例化的类常见写法是 Implementation, CATBaseUnknown, CATNull。挂在某个 StartUp、容器或已有对象 late type 上的扩展,常见写法是 CodeExtension 或 DataExtension,具体采用哪一种要优先跟随 CAA 向导和同类官方样例。

在本机安装库的 CAADoc 示例中,CAACircleSweepTg 这类自定义几何 StartUp 的多个能力就是通过 CodeExtension 挂上去的:同一个 late type 同时支持业务接口、CATIBuildCATIMf3DBehaviorCATIAttrBehaviorCATIEdit 和 CATIIcon 等接口。

5. TIE 的作用:把接口调用转给实现类

TIE_MyCompanyIMeasure(MyCompanyEMeasure) 看起来像一行简单宏,但它做的事情很关键:为接口生成一个适配层,让运行时请求 MyCompanyIMeasure 时,可以创建一个接口对象,并把接口函数调用转发到 MyCompanyEMeasure

可以把它理解成这种关系:

调用方持有 MyCompanyIMeasure
  -> 实际拿到 TIE object
  -> TIE object 转调 MyCompanyEMeasure::SetInput
  -> MyCompanyEMeasure 访问 feature 属性或执行业务逻辑

因此,如果你只写了实现类函数,但忘记 include TIE_接口名.h 或忘记调用 TIE_接口名(实现类),外部代码通常无法通过接口查询拿到你的能力。

生成的 TIE_*.h 里还经常能看到 BOA_接口名 宏。BOA 和 TIE 都是接口适配方式,不建议在没有明确理由时随意切换。团队项目中最稳妥的原则是:

  • 新接口按 CAA 向导生成的方式走。

  • 官方样例使用 TIE 的地方继续使用 TIE。

  • 已有项目使用 BOA 的接口不要局部改成 TIE,除非你明确知道对象生命周期和二进制兼容影响。

6. Dictionary 决定接口能不能被找到

实现类和 TIE 写好之后,还需要 dictionary 告诉运行时:哪个 late type 支持哪个接口,这个接口实现在哪个库里。

典型 .dico 内容如下:

MyCompanyMeasure   MyCompanyIMeasure      libMyCompanyModel
MyCompanyMeasure   CATIBuild              libMyCompanyModel
MyCompanyMeasure   CATIMf3DBehavior       libMyCompanyModel
MyCompanyMeasure   CATIEdit               libMyCompanyUI
MyCompanyMeasure   CATIIcon               libMyCompanyUI

CATFeatCont        MyCompanyIFactory      libMyCompanyModel

三列分别是:

示例 作用
late type MyCompanyMeasure 被扩展的对象类型,可以是 StartUp late type,也可以是容器或其他运行时类型。
interface MyCompanyIMeasure 调用方要请求的接口。
library libMyCompanyModel 承载实现类和 TIE 的模块名。

注意库名通常写 lib 前缀,不写 .dll。这和 command header 里填写命令模块名的习惯不同,容易混淆。

自定义接口还需要在 .iid 文件中声明稳定 IID:

{2f7b6bb0-31c8-4b78-9c4a-9e16c0c1a001}     MyCompanyIMeasure

IID 一旦发布就不要随便改。调用方编译时认的是接口名和头文件,运行时匹配的是 IID 和 dictionary 映射。IID 改了,旧模块可能还能编译,却在运行时查询不到接口。

7. 接口查询的两种写法

CAA 代码里常见两种接口获取方式。

第一种是 _var handler:

MyCompanyIMeasure_var spMeasure = iFeature;
if (!!spMeasure)
{
double length = 0.0;
  HRESULT rc = spMeasure->GetLength(length);
if (SUCCEEDED(rc))
  {
// Use length.
  }
}

这种写法简洁,适合大多数业务代码。!!spMeasure 是 CAA 代码中很常见的非空判断形式。

第二种是显式 QueryInterface

MyCompanyIMeasure *measure = NULL;
HRESULT rc = iFeature->QueryInterface(
  IID_MyCompanyIMeasure,
  (void **)&measure);

if (SUCCEEDED(rc) && measure != NULL)
{
double length = 0.0;
  rc = measure->GetLength(length);

  measure->Release();
  measure = NULL;
}

这种写法更啰嗦,但能清楚暴露三件事:

  • 接口查询可能失败,要检查 HRESULT

  • 成功返回的接口指针需要释放。

  • 查询失败不一定代表对象不存在,可能是 .dico 没注册、模块没加载、IID 不一致、late type 不匹配,或者实现模块没有进入运行时视图。

8. StartUp、late type 和接口扩展的关系

在自定义特征开发中,OSM 里定义的 feature 名通常就是 late type。例如:

feature MyCompanyMeasure MechanicalElement@`MechMod.feat` #startup {
    specobject InputFeature #in
    double Length #out
}

这个 MyCompanyMeasure 后续会出现在三个地方:

CATImplementClass(MyCompanyEMeasure, CodeExtension, CATBaseUnknown, MyCompanyMeasure);
MyCompanyMeasure   MyCompanyIMeasure   libMyCompanyModel
MyCompanyMeasure="Company Measure";

第一处是 C++ 扩展绑定,第二处是接口 dictionary 注册,第三处是 NLS 显示名称。三者名字不一致时,问题会非常绕:模型实例可能创建成功,规格树也能显示,但接口查不到;或者接口能查到,却不是挂在你以为的 StartUp 上。

建议把 late type 当作自定义特征的“运行时身份证”。凡是 .CATfct、C++ 扩展、dictionary、NLS、图标、工厂创建代码里引用它,都要保持同一个拼写。

9. 一个接口从创建到可用的最小清单

假设要让 MyCompanyMeasure 支持 MyCompanyIMeasure,最小闭环如下:

步骤 文件 要点
1 PublicInterfaces/MyCompanyIMeasure.h 声明接口、IID、CATDeclareInterfaceCATDeclareHandler
2 CNext/code/dictionary/MyCompanyModel.iid 为接口分配 GUID。
3 MyCompanyModel.m/LocalInterfaces/MyCompanyEMeasure.h 声明实现类,使用 CATDeclareClass
4 MyCompanyModel.m/src/MyCompanyEMeasure.cpp CATImplementClass

,include TIE_*.h,调用 TIE_接口名(实现类)
5 CNext/code/dictionary/MyCompanyModel.dico 注册 late type / interface / libModule
6 Imakefile.mk 链接接口、对象模型、特征模型等所需模块。
7 IdentityCard 声明所需 framework prerequisites。
8 mkmk 编译接口、生成 TIE、编译实现模块。
9 mkCreateRuntimeView 确保 dictionary、资源和 DLL 进入运行时视图。
10 CATIA 测试 用 _var 或 QueryInterface 验证接口可查询并可调用。

这里第 8 步很关键:TIE_MyCompanyIMeasure.h 通常是根据接口生成的文件。如果生成头文件不存在,先别急着手写,应该检查接口放置位置、mkmk 生成步骤和 framework 公开头文件导出是否正常。

10. 实现函数里如何访问特征属性

自定义特征接口最常见的工作,是封装 StartUp 属性访问。不要让业务代码到处写字符串属性名,最好集中在接口实现类里。

示意代码如下:

HRESULT MyCompanyEMeasure::SetInput(const CATISpecObject_var &iInput)
{
  HRESULT rc = E_FAIL;

CATISpecObject_var spFeature(this);
  CATISpecAttrAccess_var spAttrAccess = spFeature;
CATIGSMAttributes_var spAttributes(this);

if (!!spAttrAccess && !!spAttributes)
  {
    CATISpecAttrKey_var spKey = spAttrAccess->GetAttrKey("InputFeature");
if (!!spKey)
    {
if (!!iInput)
      {
        rc = spAttributes->SetAttrSpecObject(spKey, iInput);
      }
else
      {
        rc = spAttributes->UnsetAttrValue(spKey);
      }
    }
  }

return rc;
}

这个例子刻意把属性名 InputFeature 收敛在实现类内部。命令、factory、更新行为和外部模块只调用 SetInput,不直接知道 OSM 属性字符串。这样以后属性读写策略变化时,修改面会小得多。

实际项目中还要继续补几类保护:

  • 输入对象类型检查。

  • 空输入的业务含义。

  • 属性 key 获取失败时的错误返回。

  • 更新前后是否需要触发 dirty/update。

  • 对 #in#out#neutral 属性使用合适的读写接口。

11. 为什么一个对象能支持很多接口

CAA 的设计鼓励把能力拆成多个窄接口,而不是做一个巨大基类。例如一个几何特征可能同时具备:

接口 作用
自定义业务接口 设置输入、半径、方向、模式等业务参数。
CATIBuild 更新时生成几何结果。
CATIMf3DBehavior 告诉机械模型这是 shape、solid、datum 还是 volume。
CATIInputDescription 描述输入,用于 replace、reroute、依赖分析等场景。
CATIReplace

 / CATIReplaceUI
支持替换输入和替换界面行为。
CATIAttrBehavior 控制属性行为。
CATIEdit 双击或上下文菜单编辑时启动 UI。
CATIIcon 规格树图标。

这些接口不一定由同一个 C++ 类实现。模型侧接口可以在 libMyCompanyModel,UI 接口可以在 libMyCompanyUI。这样做的好处是模型模块不必依赖 UI 模块,批处理或无界面场景也能加载模型能力。

dictionary 可以自然表达这种拆分:

MyCompanyMeasure   MyCompanyIMeasure        libMyCompanyModel
MyCompanyMeasure   CATIBuild                libMyCompanyModel
MyCompanyMeasure   CATIMf3DBehavior         libMyCompanyModel
MyCompanyMeasure   CATIEdit                 libMyCompanyUI
MyCompanyMeasure   CATIIcon                 libMyCompanyUI

这也是 CAA 项目划分模块时很重要的依据:不要因为“都是同一个特征的能力”就把模型、命令、面板、图标、编辑器全放进一个库里。

12. Extension 类型怎么选

CATImplementClass 的第二个参数经常让人困惑。实际项目里,不要孤立背宏名,而要看这个类的运行方式。

场景 常见写法 判断依据
普通实现类,可直接由代码实例化 Implementation 不挂在某个 late type 上,第四个参数常为 CATNull
给已有 late type 增加接口能力 CodeExtension dictionary 中通过 late type 查询接口,官方 CAADoc 示例大量使用。
给 StartUp 或数据对象增加数据相关接口 DataExtension 特征模型或向导可能生成这种形式,按本地框架约定使用。

如果你不确定,先找同一 framework 里最接近的官方样例或团队旧代码。CAA 的宏和 dictionary 是一套运行时约定,局部“看起来也能编译”的改法,可能在运行时加载、对象生命周期或接口查询上留下隐患。

13. 模块边界:Model 和 UI 尽量分开

建议把 CAA 扩展按职责拆成几个模块:

MyCompanyModel.m
  - StartUp 业务接口实现
  - factory
  - CATIBuild
  - replace/input/attribute behavior

MyCompanyUI.m
  - command
  - dialog/state command
  - CATIEdit
  - CATIIcon
  - workbench/add-in entry

这样拆分后,模型能力可以在批处理、文件打开、update、数据迁移等无界面场景下独立加载;UI 只在用户交互时加载。很多“在 CATIA 界面里能用,但批处理一跑就崩”的问题,本质上是模型代码错误依赖了 UI 模块。

对应的 LINK_WITH 也要保持单向、克制:

  • Model 模块可以依赖 ObjectModeler、MechanicalModeler、GSM、数学/拓扑等模型 API。

  • UI 模块可以依赖 ApplicationFrame、Dialog、Visualization 和自己的 Model 模块。

  • Model 模块不要反向依赖 UI 模块。

  • Public 接口所在 framework 的 IdentityCard 要声明调用方需要的 prerequisites。

14. 调试接口查不到的问题

接口查询失败是 CAA 开发中最常见也最容易绕远的问题。建议按下面顺序排查。

现象 优先检查
编译时找不到接口头文件 IdentityCard prerequisite、公开/受保护接口位置、mkGetPreq、公开头文件导出。
编译时找不到 TIE_*.h 接口是否被 mkmk 处理、接口是否有 CATDeclareInterface、IID 是否声明、生成目录是否刷新。
链接时报未解析外部符号 LINK_WITH

 是否缺接口所在模块或实现模块,TIE 宏是否放在某个 .cpp 中。
运行时 _var 为空 .dico

 是否注册 late type/interface/lib,IID 是否一致,late type 拼写是否一致。
运行时提示库加载失败 DLL 是否进入 runtime view,依赖库是否可见,模块名是否写错。
CATIA 中界面能启动但模型更新失败 UI 模块能加载不代表 Model 接口可查,检查模型侧 dictionary 和 CATIBuild
换机器后才失败 runtime view、dictionary、环境变量、prerequisite concatenation 是否一致。

一个很实用的调试办法是把问题拆成两段:

对象本身是否存在?
  -> 能否 QueryInterface 到系统接口,例如 CATISpecObject?

自定义接口是否存在?
  -> 能否 QueryInterface 到 MyCompanyIMeasure?

如果系统接口能查到,自定义接口查不到,重点看 .dico.iid、TIE 和实现模块加载。如果系统接口也查不到,先确认你手里的对象是不是预期对象,选择路径、container、StartUp 实例或文档上下文可能就已经错了。

15. 常见设计建议

  • 接口命名尽量表达业务能力,而不是实现方式,例如 MyCompanyIMeasure 好过 MyCompanyIAttrAccessImpl

  • 接口函数保持小而稳定,避免把 UI 类型、对话框类型、临时实现细节暴露到 PublicInterfaces。

  • 不要在接口中返回裸内部对象让调用方随意改状态,优先提供明确的 get/set 或服务函数。

  • 已发布接口需要扩展时,优先新增接口版本,而不是修改旧接口虚函数布局。

  • dictionary 中模型接口和 UI 接口分库注册,避免模型层被迫加载 UI。

  • 使用 _var handler 管理接口生命周期,只有在必要时才使用裸指针和显式 Release

  • HRESULT

    不要被忽略,尤其是属性访问、接口查询、工厂创建和 update 相关代码。

  • late type、StartUp 名、NLS key、图标名、dictionary 第一列要放在一起核对。

  • 修改 .dico.iid、NLS、图标、CATfct 后,记得刷新 runtime view 再进 CATIA 测试。

16. 和前几篇文章如何串起来

现在可以把几篇文章连成一条更完整的开发路径:

自定义特征开发
  -> 定义 StartUp 和属性
  -> late type 成为运行时身份

对象模型与接口扩展
  -> 给 late type 挂业务接口、CATIBuild、CATIEdit 等能力
  -> 通过 dictionary 和 TIE 让运行时能找到实现

命令、工作台与 Add-in 开发
  -> 用户点击命令
  -> 命令调用 factory 创建特征
  -> 命令通过接口设置输入和参数

mkmk 编译体系
  -> 编译接口、生成 TIE、链接模块
  -> 复制 dictionary、资源和 DLL 到 runtime view

从这个视角看,一个 CAA 功能不是“一个类”或“一个 DLL”,而是一组相互配合的运行时声明:StartUp 声明数据形状,接口声明能力边界,实现类承载逻辑,dictionary 负责发现,构建系统负责生成和部署,命令负责把能力交到用户手里。

理解这套机制之后,CAA 开发会少很多玄学感。遇到问题时也可以沿着链路逐段定位:对象有没有创建,late type 对不对,接口有没有声明,IID 是否稳定,TIE 有没有生成,实现模块有没有链接,dictionary 有没有进入运行时视图,最后再看业务逻辑本身。

评论