一、痛点引入
做过 CAA 开发的同学都懂:拿到一个陌生零件,第一件事就是在特征树里翻来找去——这个凸台叫什么名字?那个凹槽的参数是多少?孔特征有没有统一命名规范?
场景:结构审查时,项目经理让你把零件里所有直径小于 5mm 的孔特征找出来重命名为"SMALL_HOLE_序号",再把相关参数批量导出。你点开模型树,逐个点开 80 多个特征,手动记录、改名、抄参数,一上午没了。
这不是你慢,是 CAA 的 SpecTree 操作本来就有门槛。今天这篇把遍历、过滤、重命名、参数读写一次性讲透。
二、核心思路
CAA 里所有能出现在模型树里的对象,底层都是 CATISpecObject。想操作特征树,核心就四张牌:
CATISpecObject 特征树根基接口(拿类型、拿名称)
CATIDescendants 遍历子元素(GetDirectChildren / GetAllChildren)
CATIAlias 读写特征显示名称(GetAlias / SetAlias)
CATICkeParmFactory 参数工厂(创建/读写参数)你需要准备:
- CAA RADE 环境(VS + RADE + CAA 文档,环境配置参考本系列第 1 篇)
- 当前零件文档已打开
- Imakefile.mk 里链接
ObjectSpecsModeler、ObjectModelerBase、MecModInterfaces、KnowledgeInterfaces
三、环境搭建(3 分钟)
在模块的 Imakefile.mk 中追加依赖:
LINK_WITH = $(MKMK_WARNING:Mkmk::UndefinedLinkWith) \
ObjectSpecsModeler ObjectModelerBase \
MecModInterfaces KnowledgeInterfaces头文件引用:
#include "CATISpecObject.h"
#include "CATIDescendants.h"
#include "CATIAlias.h"
#include "CATIPrtPart.h"
#include "CATICkeParmFactory.h"
#include "CATICkeParm.h"
#include "CATICkeInst.h"
#include "CATInit.h"四、核心代码
// ========== 第1步:获取当前 Part 根对象 ==========
CATDocument* pDoc = NULL;
CATBaseUnknown_var spActiveObj;
// 从当前编辑器获取文档(命令初始化时通常已持有)
CATInit_var spInit = spActiveObj; // 或从命令参数传入
if (NULL_var != spInit) {
spInit->GetRootContainer(&pDoc);
}
if (!pDoc) return;
// 取 Part 根 SpecObject
CATIPrtPart_var spPart = pDoc->GetRoot();
CATISpecObject_var spRoot = spPart;
// ========== 第2步:递归遍历特征树 ==========
void TraverseSpecTree(CATISpecObject_var& spObj, int depth) {
if (spObj == NULL_var) return;
// 获取类型与显示名称
CATUnicodeString type = spObj->GetType();
CATUnicodeString name = spObj->GetDisplayName();
// 打印缩进层级
CString indent;
indent.Format("%*s", depth * 2, "");
printf("%s[%s] %s\n", indent, type.ConvertToChar(),
name.ConvertToChar());
// 获取直接子元素(关键接口:CATIDescendants)
CATIDescendants_var spDesc;
HRESULT hr = spObj->QueryInterface(IID_CATIDescendants,
(void**)&spDesc);
if (FAILED(hr) || spDesc == NULL_var) return;
CATListValCATISpecObject_var children;
hr = spDesc->GetDirectChildren("CATISpecObject", children);
if (FAILED(hr)) return;
// CAA 列表是 1-based 索引!
for (int i = 1; i <= children.Size(); i++) {
CATISpecObject_var spChild = children[i];
TraverseSpecTree(spChild, depth + 1);
}
}
// 执行遍历
TraverseSpecTree(spRoot, 0);
// ========== 第3步:按类型过滤 + 批量重命名 ==========
void RenameFeaturesByType(CATISpecObject_var& spObj,
const CATUnicodeString& targetType,
const CATUnicodeString& prefix) {
if (spObj == NULL_var) return;
CATUnicodeString type = spObj->GetType();
if (type == targetType) {
static int seq = 1;
CATUnicodeString newName;
newName.BuildFromCS(prefix.ConvertToChar());
newName.Append("_");
char buf[8];
itoa(seq++, buf, 10);
newName.Append(buf);
// CATIAlias 修改显示名称
CATIAlias_var spAlias = spObj;
if (spAlias != NULL_var) {
spAlias->SetAlias(newName);
}
}
// 递归处理子节点
CATIDescendants_var spDesc = spObj;
if (spDesc == NULL_var) return;
CATListValCATISpecObject_var children;
if (SUCCEEDED(spDesc->GetDirectChildren("CATISpecObject", children))) {
for (int i = 1; i <= children.Size(); i++) {
RenameFeaturesByType(children[i], targetType, prefix);
}
}
}
// 把所有 "Pad" 特征重命名为 "BOSS_序号"
RenameFeaturesByType(spRoot, "Pad", "BOSS");
// ========== 第4步:参数批量读写 ==========
void BatchReadParameters(CATISpecObject_var& spObj) {
if (spObj == NULL_var) return;
// 尝试查询参数接口
CATICkeParm_var spParm = spObj;
if (spParm != NULL_var) {
CATUnicodeString parmName = spParm->Name();
double val = spParm->Value()->AsReal();
printf("参数: %s = %.3f\n", parmName.ConvertToChar(), val);
// 批量修改:长度参数乘以 1.1(放大 10%)
if (parmName.Search("Length") >= 0) {
spParm->Valuate(val * 1.1);
}
}
// 递归子节点
CATIDescendants_var spDesc = spObj;
if (spDesc == NULL_var) return;
CATListValCATISpecObject_var children;
if (SUCCEEDED(spDesc->GetDirectChildren("CATISpecObject", children))) {
for (int i = 1; i <= children.Size(); i++) {
BatchReadParameters(children[i]);
}
}
}
BatchReadParameters(spRoot);代码解析:
CATIDescendants::GetDirectChildren("CATISpecObject", list):只取一层子元素,避免递归过深时性能爆炸;如需全量展开可用GetAllChildrenCATISpecObject::GetType():返回特征的具体类型字符串(如"Pad"、"Pocket"、"Hole"),是过滤的关键CATIAlias::SetAlias():修改模型树里看到的显示名称,不会改变内部特征名CATICkeParm::Valuate()/AsReal():参数写/读的标准方法,数值单位均为 MKS(米、千克、秒),长度值 0.01 表示 10mm
五、效果对比
| 对比项 | 传统手动操作 | CAA 自动化脚本 |
|---|---|---|
| 遍历 200 个特征 | 逐个点开,15 分钟 | 一键输出,< 1 秒 |
| 批量重命名 50 个孔 | 逐个 F2 改名,10 分钟 | 递归过滤改名,< 1 秒 |
| 参数提取与修改 | 手抄到 Excel,易错 | 内存直读直写,零出错 |
| 可重复性 | 每次重做 | 编译一次,随时运行 |
六、进阶用法
1. 批量删除指定类型特征
// 删除所有 "Groove" 特征(注意:父节点删除后子节点失效,
// 建议从叶子往根删,或收集到列表后统一删)
CATIDescendants_var spDesc = spRoot;
CATListValCATISpecObject_var toDelete;
// ... 收集目标特征到 toDelete ...
for (int i = toDelete.Size(); i >= 1; i--) {
CATISpecObject_var spTarget = toDelete[i];
spTarget->Remove(); // 从特征树移除
}2. 安全释放与 NULL_var 检查
CAA 的 QueryInterface 和智能指针 _var 混用时,一个常见坑是:接口查询失败返回 NULL,但代码继续往下跑导致崩溃。养成习惯:每次 QueryInterface 后判 FAILED(hr),每次 _var 赋值后判 == NULL_var。
关注回复「特征树」获取完整源码
下一篇:CAA 中的 Undo/Redo 事务控制——批量操作如何安全回滚
_栏目:开发实战 | 发布日期:2026-08-26 | 字数:约 1180 字_