简介:面向NX二次开发者的CustomFeature实战记录文档,聚焦C++自定义特征开发中配置、注册与调试环节的常见问题。内容围绕CustomFeatureConfiguration.xml的字段含义展开,说明FeatureClass、FeatureName、FeatureIcon、FeatureLibrary、FeatureUILibrary与IsWithoutBody等属性的正确写法,并梳理核心库与UI库的分工、相关NXOpen头文件的引用方式,以及CreateFeature、ModifyFeatureGeometry、OnPreUpdate等关键方法的实现要点;同时记录了不含实体特征误设IsWithoutBody所引发的异常,以及UI库中区分创建与编辑模式、读取特征数据回填界面控件的处理思路,可作为排查同类报错的参考。资源包共1个docx文件,约18KB,篇幅精简、便于随查随用。已有351人学习,适合具备一定C++基础、正在上手NX自定义特征开发的中级开发者对照实践。
1. 两个 DLL 加一个 XML,CustomFeature 到底怎么落地
NX 二次开发里做自定义特征,很多人第一反应是写 UFUN 或者直接调建模 API,但真要把一个特征做成历史树里可回放、可双击编辑、参数能追溯的“正规军”,绕不开 CustomFeature 这套框架。它的核心思路很清晰:把特征拆成核心库和 UI 库两个 DLL,再用一个固定命名的 XML 把类名、图标、库名、是否含体这些元信息登记进去,NX 启动时扫描 application 目录完成注册。听起来简单,实际动手时文件名写错一个字母、XML 里IsWithoutBody填反、回调没注册上,都会让你在历史树里看到一个灰掉或者双击报错的特征。这篇笔记按我实际拆包和调试的顺序,把配置、UI 侧创建与编辑分流、Core 侧五个回调、追踪对象这几块讲透,适合已经能编译 NXOpen 工程、准备把自定义特征塞进正式项目的人照着复现。
2. 配置先行:XML 字段与 DLL 部署的硬约束
CustomFeature 的注册完全依赖CustomFeatureConfiguration.xml这个文件,名字不能改,位置不能错,内部字段和代码的对应关系一旦错位,NX 不会给你任何友好提示,只会在加载时静默跳过或者抛一个看不出根因的异常。所以第一步不是写代码,而是把这份配置和 DLL 的部署路径先钉死。
2.1 部署目录与文件清单
两个 DLL 加一个 XML,全部放进 NX 安装目录下的application文件夹。核心库(FeatureLibrary)负责几何创建逻辑,UI 库(FeatureUILibrary)负责 Block Styler 对话框,XML 负责登记。三个文件缺一不可,且 DLL 的导出入口必须能被 NX 加载到。
常见做法是先在本地建一个部署脚本,每次编译完自动拷贝,避免手工拖文件漏掉。下面这个 bash 片段是我一般会用的,路径按自己环境替换:
# 部署 CustomFeature 三件套到 NX application 目录 NX_APP="/path/to/nx/application" BUILD_OUT="./build/Release" # 核心库与 UI 库,文件名必须和 XML 里的 FeatureLibrary/FeatureUILibrary 一致 cp "${BUILD_OUT}/PolygonalHoleCore.dll" "${NX_APP}/" cp "${BUILD_OUT}/PolygonalHoleUI.dll" "${NX_APP}/" # 配置文件名字固定,不能改成别的 cp "./config/CustomFeatureConfiguration.xml" "${NX_APP}/" # 确认三个文件都在位,少一个都别启动 NX ls -l "${NX_APP}/PolygonalHoleCore.dll" \ "${NX_APP}/PolygonalHoleUI.dll" \ "${NX_APP}/CustomFeatureConfiguration.xml"这段脚本的逻辑很直白:DLL 文件名和 XML 里的FeatureLibrary、FeatureUILibrary字段是字符串匹配关系,不是靠导出符号猜的,所以拷贝时文件名不能带版本后缀或者调试后缀。参数上唯一要改的是NX_APP和BUILD_OUT,其余保持原样。跑完ls三个文件都在,再启动 NX,否则先别急着怀疑代码。
2.2 XML 字段逐项对照
配置文件的结构不复杂,但每个属性都直接绑定到运行时行为,填错就是血泪经验。下面这张表是我对照实际调试结果整理的字段含义和常见误填:
| 字段 | 作用 | 填错后的现象 |
|---|---|---|
| FeatureClass | 与 C++ 类名完全一致,含命名空间 | 特征不注册,历史树无图标 |
| FeatureName | 历史模型记录中显示的名字 | 显示为空或乱码 |
| FeatureIcon | 历史树图标资源名 | 图标缺失,特征仍可用 |
| FeatureLibrary | 核心库 DLL 名(不含扩展名) | 创建时找不到实现,抛异常 |
| FeatureUILibrary | UI 库 DLL 名(不含扩展名) | 双击无对话框或直接报错 |
| IsWithoutBody | 特征是否不含实体 | 纯曲线特征填 false 会创建失败 |
一个容易翻车的点是FeatureClass必须和代码里GetClassFromName传入的字符串一模一样,包括NXOpen::CustomFeature::前缀。我见过有人 XML 里写短名、代码里写全名,结果GetClassFromName返回空指针,后面所有注册回调全部落空。另一个点是IsWithoutBody:如果特征只产出曲线,必须设true,这样双击曲线能直接进编辑;设成false时 NX 会认为你要创建体,找不到目标体就抛“不能创建体或者没有目标体”的异常。
<?xml version="1.0" encoding="utf-8"?> <CustomFeatureLib xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="CustomFeatureConfiguration_schema.xsd"> <CustomFeature FeatureClass="NXOpen::CustomFeature::SamplePolygonalHole" FeatureName="Polygonal Hole" FeatureIcon="polygonal_hole" FeatureLibrary="PolygonalHoleCore" FeatureUILibrary="PolygonalHoleUI" IsWithoutBody="true"/> </CustomFeatureLib>这段 XML 里FeatureLibrary和FeatureUILibrary不带.dll后缀,NX 自己拼扩展名;IsWithoutBody="true"对应纯曲线特征。改字段时只动值不动属性名,属性名是 schema 定死的。
提示:XML 里可以登记多个
CustomFeature节点,但每个节点的FeatureClass必须唯一,重复登记会导致后加载的覆盖前一个,调试时表现为“明明改了代码行为却没变”。
3. UI 库:创建与编辑分流的三处关键代码
UI 库的职责是把 Block Styler 对话框和 CustomFeature 的数据接起来。它最容易出问题的地方不是控件布局,而是“当前到底是新建还是编辑”这个状态判断。NX 通过GetEditedCustomFeature()返回当前正在编辑的特征指针,新建时为空,编辑时非空,所有初始化、Apply、Show 的逻辑都要围绕这个指针分流。
3.1 构造类里拿管理器与类指针
对话框构造类里先把CustomFeatureClassManager和对应的CustomFeatureClass拿到手,作为成员变量保存,后面 Show 和 Apply 都要用。注意成员变量声明时去掉前面的类型定义,只留变量名和初始化。
// 对话框构造类成员:从 session 拿管理器,再按类名拿特征类 NXOpen::Features::CustomFeatureClassManager *m_CusFeatManager = theSession->CustomFeatureClassManager(); NXOpen::Features::CustomFeatureClass *m_CusFeatClass = m_CusFeatManager->GetClassFromName("NXOpen::CustomFeature::SamplePolygonalHole");GetClassFromName的入参必须和 XML 的FeatureClass逐字符一致。如果这里返回空,后面AddPreUpdateHandler之类的注册全部无效,特征能创建但回调不触发,表现就是历史树里特征存在、几何却不更新。参数没有可调项,唯一要检查的就是字符串拼写。
3.2 Show 时区分创建与编辑
Show阶段决定对话框以哪种模式弹出。编辑模式下不应该出现 Apply 按钮,这是 NX 的约定,也是用户能感知到的状态差异。
// 编辑时 editedFeature 非空,走 Edit 模式;新建时为空,走 Create 模式 NXOpen::Features::CustomFeature *editedFeature = m_CusFeatManager->GetEditedCustomFeature(); BlockDialog::DialogMode mode = (editedFeature != NULL) ? BlockDialog::DialogModeEdit : BlockDialog::DialogModeCreate; theDialog->Show(mode);这段代码没有参数可调,逻辑就是二选一。坑在于有人把editedFeature的判断写在Show之后,导致模式已经定了才去读状态,编辑时照样弹出 Apply 按钮,用户点下去就重复创建。顺序必须是先读状态再 Show。
3.3 初始化时按属性名回填控件
初始化阶段是创建和编辑差异最大的地方。新建时给默认值,编辑时从CustomFeatureData里按属性名把值读出来回填到控件。读取时要区分表达式属性、Tag 属性,Tag 属性还要dynamic_cast到具体类型。
NXOpen::Part *workPart(theSession->Parts()->Work()); NXOpen::Features::CustomFeature *editedFeature = m_CusFeatManager->GetEditedCustomFeature(); if (editedFeature == NULL) { // 第一次创建:初始化默认参数 UF_UI_open_listing_window(); UF_UI_write_listing_window("初始化中:第一次创建\n"); } else { // 二次编辑:从特征数据回填 UI NXOpen::Features::CustomFeatureData *cfData = editedFeature->FeatureData(); PopulateUIFromCustomFeatureData(cfData); UF_UI_open_listing_window(); UF_UI_write_listing_window("初始化中:二次编辑\n"); }PopulateUIFromCustomFeatureData内部按属性名逐个读。表达式属性用CustomExpressionAttributeByName拿Expression,Tag 属性用CustomTagAttributeByName拿TaggedObject再转具体类型:
// 读表达式属性 Features::CustomExpressionAttribute *sidesAttrib = cfData->CustomExpressionAttributeByName("m_polygonSides"); NXOpen::Expression *sidesExp = sidesAttrib->Value(); // 读 Tag 属性:方向、点、目标体分别转成对应类型 Features::CustomTagAttribute *dirAttrib = cfData->CustomTagAttributeByName("m_holeDirection"); NXOpen::Direction *dir = dynamic_cast<NXOpen::Direction *>(dirAttrib->Value()); Features::CustomTagAttribute *ptAttrib = cfData->CustomTagAttributeByName("m_polygonPoint"); NXOpen::Point *pt = dynamic_cast<NXOpen::Point *>(ptAttrib->Value()); Features::CustomTagAttribute *bodyAttrib = cfData->CustomTagAttributeByName("m_targetBody"); NXOpen::Body *body = dynamic_cast<NXOpen::Body *>(bodyAttrib->Value());属性名m_polygonSides、m_holeDirection这些必须和创建属性时用的字符串完全一致,大小写敏感。dynamic_cast失败会返回空指针,后面SetValue就崩,所以编辑态回填前最好加空判断。还有一种写法是先判断属性是否存在再取,避免旧模型里缺属性导致崩溃:
// 属性可能不存在时先 HasCustomAttribute 再取,避免空指针 Features::CustomTagAttribute *sectionAttrib = cfData->HasCustomAttribute("section", NXOpen::Features::CustomAttribute::TypeTag) ? dynamic_cast<Features::CustomTagAttribute *>( cfData->CustomTagAttributeByName("section")) : NULL;3.4 Apply 里创建或复用特征数据
Apply 是真正落特征的地方。新建时创建CustomFeatureData并挂属性,编辑时直接复用editedFeature->FeatureData(),最后统一SetFeatureData再CommitFeature。异常处理里必须销毁 builder,否则下次打开对话框会残留。
Features::CustomFeatureBuilder *builder = NULL; try { NXOpen::Part *workPart(theSession->Parts()->Work()); NXOpen::Features::CustomFeatureData *cfData = NULL; NXOpen::Features::CustomFeature *editedFeature = m_CusFeatManager->GetEditedCustomFeature(); builder = workPart->Features()->CreateCustomFeatureBuilder(editedFeature); if (editedFeature == NULL) { // 新建:先建属性集合,再建数据 std::vector<NXOpen::Features::CustomAttribute *> attrs(0); CreateFeatureAttributes(attrs); cfData = workPart->Features()->CustomFeatureDataCollection() ->CreateData(m_CusFeatClass, attrs); } else { // 编辑:复用已有数据 cfData = editedFeature->FeatureData(); } PopulateAttributeValues(cfData); // 把 UI 值写回属性 builder->SetFeatureData(cfData); builder->CommitFeature(); builder->Destroy(); builder = NULL; } catch (exception &ex) { if (builder != NULL) { builder->Destroy(); builder = NULL; } PolygonalHoleUI::theUI->NXMessageBox()->Show( "Block Styler", NXOpen::NXMessageBox::DialogTypeError, ex.what()); }CreateCustomFeatureBuilder传editedFeature时,编辑态会基于已有特征构建,新建态传空。CommitFeature之后 builder 必须Destroy,这是 NX 对象生命周期的基本要求,漏掉会在连续操作时出现内存增长或者对话框卡死。PopulateAttributeValues里写属性值时,表达式属性要判断Value()是否为空,为空就CreateSystemExpression兜底,否则SetValue会失败。
4. Core 库:五个回调与追踪对象的注册顺序
Core 库是特征几何的实际执行者。它不负责界面,只负责在特征生命周期的各个节点被 NX 回调。五个回调分别是 preUpdate、createGeometry、modifyGeometry、postUpdate、information,注册顺序和回调内的追踪对象处理决定了特征能不能正确显示、能不能被布尔运算识别。
4.1 入口注册五个回调
Core 库的入口函数里先实例化核心类,构造时拿到CustomFeatureClass并注册五个 handler。注册必须在特征被使用之前完成,通常放在 DLL 加载入口。
// 构造类中注册五个回调 NXOpen::Features::CustomFeatureClassManager *mgr = theSession->CustomFeatureClassManager(); NXOpen::Features::CustomFeatureClass *cfClass = mgr->GetClassFromName("NXOpen::CustomFeature::SamplePolygonalHole"); cfClass->AddPreUpdateHandler( make_callback(&PolygonalHoleCore::preUpdateCallBack)); cfClass->AddCreateFeatureGeometryHandler( make_callback(&PolygonalHoleCore::createGeometryCallBack)); cfClass->AddModifyFeatureGeometryHandler( make_callback(&PolygonalHoleCore::modifyGeometryCallBack)); cfClass->AddPostUpdateHandler( make_callback(&PolygonalHoleCore::postUpdateCallBack)); cfClass->AddInformationHandler( make_callback(&PolygonalHoleCore::informationCallBack));五个 handler 的签名必须和声明一致,make_callback负责把静态成员函数转成 NX 要求的回调类型。注册失败通常不是代码问题,而是GetClassFromName返回空,回到 XML 检查FeatureClass。回调函数本身返回 0 表示成功,返回非 0 会中断特征更新,调试阶段可以在每个回调里往 listing window 打一行日志确认触发顺序。
4.2 追踪对象:让几何被正确识别
createGeometry 回调里创建了几何之后,必须通过TrackingData告诉 NX 这些几何属于当前特征,否则特征虽然创建成功,几何却不参与显示或者布尔运算。PK 创建的实体和 NX 函数创建的对象,追踪方式不同。
std::vector<Features::TrackingData *> trackingObjectVector; // PK 创建的实体:按 Parasolid 实体建追踪,并挂源实体 Features::TrackingData *trackObj1 = event->CreateTrackingDataForParasolidEntity( (NXOpen::Features::CustomFeatureEvent::ParasolidId)output1); std::vector<NXOpen::NXObject *> srcVec1; srcVec1.push_back(dynamic_cast<NXOpen::NXObject *>(body)); trackObj1->AddTagSourceEntities(srcVec1); trackingObjectVector.push_back(trackObj1); // NX 函数创建的对象:按 NXObject 建追踪,可设应用键 Features::TrackingData *trackObj2 = event->CreateTrackingDataForNXObject(line1Tag); std::vector<NXOpen::NXObject *> srcVec2; srcVec2.push_back(dynamic_cast<NXOpen::NXObject *>(line1Tag)); trackObj2->AddTagSourceEntities(srcVec2); trackObj2->SetIntApplicationKey(1); trackingObjectVector.push_back(trackObj2); // 实体的面也要逐个加追踪,否则面级操作找不到归属 for (int i = 0; i < numFaces; i++) { Features::TrackingData *faceTrack = event->CreateTrackingDataForParasolidEntity( (NXOpen::Features::CustomFeatureEvent::ParasolidId)faces[i]); faceTrack->AddIntSourceEntity(i + 1); trackingObjectVector.push_back(faceTrack); } event->AppendOutputTrackingData(trackingObjectVector);AppendOutputTrackingData必须在几何创建完成后调用一次,把整个 vector 交出去。SetIntApplicationKey用于区分同一特征内多个 NX 对象的来源,面追踪里的AddIntSourceEntity(i + 1)是给面编号,方便后续按索引找回。如果实体不参与显示,还要单独SetBodies声明:
// 不参与显示的实体单独声明,避免被当成输出体 std::vector<NXOpen::Features::CustomFeatureEvent::ParasolidId> bodiesVector; bodiesVector.push_back( (NXOpen::Features::CustomFeatureEvent::ParasolidId)output1); event->SetBodies(bodiesVector);4.3 用 NX 函数建对象时走 ConstructionFeatureData
如果几何是用 UFUN 比如UF_MODL_create_cyl1创建的,不能直接当普通对象处理,要在 preUpdate 回调里包成ConstructionFeatureData并设置显示,否则这个构造特征不会出现在图形窗口。
// preUpdate 中把 UFUN 创建的构造特征登记为可见 std::vector<NXOpen::Features::ConstructionFeatureData *> cfDataVec; tag_t tCyFeatTag = NULL_TAG; UF_MODL_create_cyl1(..., &tCyFeatTag); tag_t g_tCylTag = NULL_TAG; UF_MODL_ask_feat_body(tCyFeatTag, &g_tCylTag); NXOpen::Features::Feature *nxFeat = dynamic_cast<NXOpen::Features::Feature *>(NXObjectManager::Get(tCyFeatTag)); NXOpen::Features::ConstructionFeatureData *outFeatData = event->CreateConstructionFeatureData(nxFeat); outFeatData->SetShowInGraphicView(true); cfDataVec.push_back(outFeatData);SetShowInGraphicView(true)是关键,不设的话构造特征默认不显示,用户看到的就是“特征建了但什么都没出来”。这段逻辑放在 preUpdate 而不是 createGeometry,是因为构造特征要在几何创建之前登记。
5. 避坑排查:五条实际翻车记录
CustomFeature 的调试信息很少,大部分问题表现为“特征不出现”“双击报错”“编辑后几何不更新”,根因往往在配置或注册阶段。下面五条是我实际踩过的,按现象、原因、解决写清楚。
5.1 特征在历史树里灰掉、双击无反应
现象:XML 登记了,DLL 也拷了,历史树能看到特征名,但图标是灰的,双击不弹对话框。原因:FeatureUILibrary字段写的 DLL 名和实际文件名不一致,或者 UI DLL 缺少导出入口,NX 加载不到对话框。解决:核对 XML 里FeatureUILibrary的值和application目录下 DLL 文件名逐字符一致,再用依赖查看工具确认 UI DLL 的导出符号存在。改完重启 NX,不要热加载。
5.2 纯曲线特征创建时报“不能创建体”
现象:特征只产出曲线,Apply 时抛异常提示不能创建体或者没有目标体。原因:XML 里IsWithoutBody设成了false,NX 按含体特征处理,找不到目标体就失败。解决:纯曲线特征必须IsWithoutBody="true",改完 XML 后重启 NX 让配置重新加载。这个字段和代码逻辑无关,纯粹是注册元信息,但影响运行时分支。
5.3 编辑态回填时崩溃在 SetValue
现象:第一次创建正常,双击编辑时对话框初始化阶段崩溃。原因:CustomTagAttributeByName返回的属性在旧模型里不存在,dynamic_cast得到空指针,后续SetValue解引用崩溃。解决:回填前用HasCustomAttribute判断属性是否存在,不存在就跳过或者给默认值;所有dynamic_cast结果加空判断。旧版本模型升级时尤其容易触发。
5.4 回调全部不触发、几何不更新
现象:特征能创建,但改参数后几何不变,listing window 里五个回调的日志一条都没有。原因:GetClassFromName返回空,AddPreUpdateHandler等注册全部作用在空指针上,或者注册代码根本没执行到。解决:在注册前打印cfClass是否为空,确认 XML 的FeatureClass和代码字符串一致;确认 Core DLL 的入口函数确实被调用,可以在入口加一行日志验证。
5.5 实体创建了但图形窗口不显示
现象:createGeometry 返回成功,追踪对象也加了,但图形窗口看不到实体。原因:用 UFUN 创建的构造特征没有包成ConstructionFeatureData并SetShowInGraphicView(true),或者实体没做布尔运算又没走SetBodies声明。解决:UFUN 创建的构造特征在 preUpdate 里登记并设显示;不参与布尔运算的实体用SetBodies明确声明输出体,避免被 NX 当成中间对象隐藏。
6. 进阶验证:用日志和最小特征反推回调时序
调 CustomFeature 最有效的手段不是猜,而是让每个回调自己说话。我习惯在五个回调里统一打带时间戳的日志,再配一个最小可用的单实体特征做对照,这样一旦时序不对,日志顺序会直接暴露问题。下面这个日志封装可以放在 Core 库里复用:
// 统一日志:回调名 + 关键计数,输出到 listing window static void LogCallback(const char *name, int extra = -1) { char buf[256]; if (extra >= 0) { snprintf(buf, sizeof(buf), "[CF] %s extra=%d\n", name, extra); } else { snprintf(buf, sizeof(buf), "[CF] %s\n", name); } UF_UI_open_listing_window(); UF_UI_write_listing_window(buf); }在每个回调开头调用LogCallback("preUpdate")之类,正常创建一次特征的日志顺序应该是 preUpdate → createGeometry → postUpdate,编辑时是 preUpdate → modifyGeometry → postUpdate,information 只在查询信息时触发。如果 createGeometry 没出现而 postUpdate 出现了,说明几何创建分支被跳过,回去查IsWithoutBody和追踪对象。如果 preUpdate 出现两次,通常是注册重复,检查入口函数是否被多次调用。
验证阶段还有一张对照表,用来快速定位是配置问题还是代码问题:
| 验证动作 | 预期结果 | 不符合时先查 |
|---|---|---|
| 启动 NX 看历史树图标 | 图标正常、非灰 | XML 的 FeatureIcon 与 DLL 部署 |
| 新建特征 | 日志三连、几何可见 | createGeometry 与追踪对象 |
| 双击编辑 | 弹对话框、无 Apply | Show 模式判断顺序 |
| 改参数 Apply | 几何更新、日志走 modify | 属性名与回填逻辑 |
| 查询特征信息 | information 回调触发 | AddInformationHandler 注册 |
这张表我一般贴在工位上,出问题先跑一遍,比翻代码快。最小特征的做法是只保留一个表达式属性和一个 Tag 属性,几何只创建一个圆柱,把变量降到最少,确认五回调时序正确后再往上加面追踪和构造特征。从那以后我每次接新的 CustomFeature 需求,都强制先用最小特征跑通日志时序,再动业务几何,省下来的调试时间远比搭这个骨架多。希望帮到你。
本文还有配套的精品资源,点击获取