简介:面向NX二次开发者的CustomFeature实战记录文档,聚焦C++环境下用户自定义特征的创建、配置与问题排查,适合已具备NXOpen基础、希望扩展NX几何构造能力的中高级开发者参考。压缩包内仅1个docx文件,约18KB,以文字记录形式整理了CustomFeatureConfiguration.xml的配置要点、核心库与UI库的分工,以及特征类注册、编辑模式区分、属性读取等关键环节的测试过程与异常处理思路。内容涉及FeatureClass与代码类名一致性、IsWithoutBody对无实体特征编辑的影响、对话框构造与Show模式切换等易错点,并给出头文件引用清单与常见报错场景的应对方式。已有351人学习,可作为CustomFeature开发调试阶段的对照笔记,帮助读者少走配置与注册环节的弯路。
1. 从一次“特征双击没反应”说起
去年帮一个做模具检具的团队排查问题,他们在 NX 里做了一套多边形孔的自定义特征,创建时一切正常,模型树里也能看到节点,可用户双击那个特征想改参数时,界面纹丝不动,历史记录里连个编辑入口都没有。折腾半天才发现,问题不在 C++ 代码,而在一个只有几十行的CustomFeatureConfiguration.xml里——IsWithoutBody写成了false,而他们的特征只产出曲线,没有实体。NX 在尝试进入编辑态时找不到目标体,直接抛了异常,被上层吞掉了。
这件事很典型。NX 二次开发里,CustomFeature(用户自定义特征)是把 C++ 几何构造能力封装成“像原生特征一样可创建、可编辑、可回放”的机制,涉及两个 DLL、一个 XML 配置、五个回调函数和一套属性数据模型。它适合已经能写 NXOpen 基础代码、想把内部算法沉淀成标准特征的中高级开发者。下面按“配置怎么摆 → UI 怎么接 → Core 怎么算 → 坑怎么避 → 怎么改”的顺序拆一遍,代码都来自实际测试记录,能直接对照着抄。
2. 工程骨架与 XML 配置:两个 DLL 加一个固定文件名
2.1 目录结构与部署位置
CustomFeature 的产物不是单个 DLL,而是一组文件必须落到 NX 能扫描到的application目录下。测试时我用的结构是这样的:
my_custom_feature/ ├── application/ │ ├── CustomFeatureConfiguration.xml # 固定文件名,不能改 │ ├── CustomFeatureConfiguration_schema.xsd │ ├── BlockAndCylinderCore.dll # 特征几何实现 │ ├── BlockAndCylinderUI.dll # 对话框逻辑 │ ├── PolygonalHoleCore.dll │ └── PolygonalHoleUI.dll └── startup/ └── (可选) 菜单、工具栏注册文件application目录是 NX 启动时自动加载的约定路径,放在用户目录或环境变量指向的路径下都行。两个 DLL 的分工必须清楚:FeatureLibrary对应的 Core DLL 负责几何创建和更新回调,FeatureUILibrary对应的 UI DLL 负责 Block Styler 对话框。把它们混在一个 DLL 里,NX 加载时会出现类注册冲突,这是血泪经验。
2.2 XML 每个字段到底管什么
配置文件的名字必须是CustomFeatureConfiguration.xml,一个字母都不能差,否则 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::BlockAndCylinder" FeatureName="Block And Cylinder" FeatureIcon="block_and_cylinder" FeatureLibrary="BlockAndCylinderCore" FeatureUILibrary="BlockAndCylinderUI" IsWithoutBody="false"/> <CustomFeature FeatureClass="NXOpen::CustomFeature::SamplePolygonalHole" FeatureName="Polygonal Hole" FeatureIcon="polygonal_hole" FeatureLibrary="PolygonalHoleCore" FeatureUILibrary="PolygonalHoleUI" IsWithoutBody="true"/> </CustomFeatureLib>逐项说明,这些是排查问题时最先要核对的:
| 属性 | 作用 | 常见错误 |
|---|---|---|
FeatureClass | 与 C++ 里注册的类名完全一致,含命名空间 | 少写NXOpen::CustomFeature::前缀导致找不到类 |
FeatureName | 历史模型记录里显示的名字 | 与 UI 标题不一致,用户困惑 |
FeatureIcon | 历史树节点图标资源名 | 图标文件缺失时显示空白但不报错 |
FeatureLibrary | Core DLL 文件名(不含 .dll) | 与实际输出名大小写不符 |
FeatureUILibrary | UI DLL 文件名(不含 .dll) | 两个特征写反,编辑时打开错误对话框 |
IsWithoutBody | 特征是否不含实体 | 只出曲线却写 false,双击编辑直接异常 |
IsWithoutBody是最容易翻车的一项。规则很硬:创建结果只有曲线、没有体,必须写true,这样双击曲线就能进编辑;如果实际没有体却写false,NX 会提示“不能创建体或者没有目标体”。反过来,有实体却写true,编辑时实体引用会丢失。判断标准就看你的createGeometryCallBack最终往SetBodies里塞了什么。
2.3 头文件清单与引入顺序
Core 和 UI 两个工程都要引一批 NXOpen 头文件,测试记录里整理出的最小集合如下:
#include <NXOpen/Features_Feature.hxx> #include <NXOpen/Features_FeatureCollection.hxx> #include <NXOpen/Features_CustomAttribute.hxx> #include <NXOpen/Features_CustomAttributeCollection.hxx> #include <NXOpen/Features_CustomExpressionAttribute.hxx> #include <NXOpen/Features_CustomIntegerAttribute.hxx> #include <NXOpen/Features_CustomLogicalAttribute.hxx> #include <NXOpen/Features_CustomTagAttribute.hxx> #include <NXOpen/Features_CustomFeature.hxx> #include <NXOpen/Features_CustomFeatureBuilder.hxx> #include <NXOpen/Features_CustomFeatureClass.hxx> #include <NXOpen/Features_CustomFeatureClassManager.hxx> #include <NXOpen/Features_CustomFeaturePreUpdateEvent.hxx> #include <NXOpen/Features_CustomFeatureData.hxx> #include <NXOpen/Features_CustomFeatureDataCollection.hxx> #include <NXOpen/Features_CustomFeatureCreateFeatureGeometryEvent.hxx> #include <NXOpen/Features_CustomFeatureModifyFeatureGeometryEvent.hxx> #include <NXOpen/Features_CustomFeaturePostUpdateEvent.hxx> #include <NXOpen/Features_CustomFeatureInformationEvent.hxx>属性相关的头文件(CustomExpressionAttribute、CustomTagAttribute等)在 UI 和 Core 里都要用,因为属性既要在对话框里读写,也要在几何回调里取值。工程属性里记得把 NXOpen 的 include 路径和 lib 路径配好,链接libnxopen.lib一类的基础库,这部分按 NX 版本自带的向导生成最稳。
3. UI 层拆解:对话框如何区分创建与编辑
3.1 构造阶段拿到特征类句柄
UI DLL 的对话框类里,第一步是把CustomFeatureClassManager和具体的CustomFeatureClass存成成员变量。注意是成员变量,添加时去掉前面的类型定义,只留变量名和初始化:
// 对话框类成员变量 NXOpen::Features::CustomFeatureClassManager *m_CusFeatManager = theSession->CustomFeatureClassManager(); NXOpen::Features::CustomFeatureClass *m_CusFeatClass = m_CusFeatManager->GetClassFromName("NXOpen::CustomFeature::SamplePolygonalHole");GetClassFromName的字符串必须和 XML 里的FeatureClass一字不差。这里拿到的m_CusFeatClass后面创建CustomFeatureData时要用,所以别用局部变量。
3.2 Show 阶段判断编辑还是创建
同一个对话框要服务两种入口:新建特征和双击编辑。区分靠GetEditedCustomFeature(),返回非空就是编辑态:
NXOpen::Features::CustomFeature* editedFeature = m_CusFeatManager->GetEditedCustomFeature(); BlockDialog::DialogMode mode = editedFeature != NULL ? BlockDialog::DialogModeEdit : BlockDialog::DialogModeCreate; theDialog->Show(mode);编辑态下 Block Styler 会自动隐藏 Apply 按钮,只留 OK,这是框架行为,不用自己控制。如果发现编辑时还能点 Apply,多半是Show传错了 mode。
3.3 初始化阶段回填属性值
初始化回调里要分两条路:首次创建给默认值,二次编辑从FeatureData里按属性名读回来。读取的关键是属性类型要对上,表达式属性用CustomExpressionAttributeByName,Tag 属性用CustomTagAttributeByName:
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 { NXOpen::Features::CustomFeatureData* cfData = editedFeature->FeatureData(); PopulateUIFromCustomFeatureData(cfData); UF_UI_write_listing_window("初始化中:二次编辑\n"); }PopulateUIFromCustomFeatureData里按类型取值,表达式、方向、点、体各有各的写法:
// 表达式属性 Features::CustomExpressionAttribute *sidesAttrib = cfData->CustomExpressionAttributeByName("m_polygonSides"); NXOpen::Expression* sidesExp = sidesAttrib->Value(); // 方向 Tag 属性 NXOpen::Features::CustomTagAttribute* dirAttrib = cfData->CustomTagAttributeByName("m_holeDirection"); NXOpen::Direction* dir = dynamic_cast<NXOpen::Direction*>(dirAttrib->Value()); // 点 Tag 属性 NXOpen::Features::CustomTagAttribute* ptAttrib = cfData->CustomTagAttributeByName("m_polygonPoint"); NXOpen::Point* pt = dynamic_cast<NXOpen::Point*>(ptAttrib->Value()); // 体 Tag 属性 NXOpen::Features::CustomTagAttribute* bodyAttrib = cfData->CustomTagAttributeByName("m_targetBody"); NXOpen::Body* body = dynamic_cast<NXOpen::Body*>(bodyAttrib->Value());dynamic_cast不能省,Value()返回的是基类指针,直接强转在类型不匹配时会崩。可选属性建议先判断存在性再取,避免空指针:
Features::CustomTagAttribute *sectionAttrib = cfData->HasCustomAttribute("section", NXOpen::Features::CustomAttribute::TypeTag) ? dynamic_cast<Features::CustomTagAttribute*>(cfData->CustomTagAttributeByName("section")) : NULL;3.4 Apply 阶段提交特征
Apply 回调是整个 UI 的核心,负责创建或更新CustomFeatureData,再通过 Builder 提交。结构上先建 Builder,再按创建/编辑分支准备数据,最后SetFeatureData加CommitFeature:
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); UF_UI_write_listing_window("APPLY 中:第一次创建\n"); } else { cfData = editedFeature->FeatureData(); UF_UI_write_listing_window("APPLY 中:二次编辑\n"); } PopulateAttributeValues(cfData); builder->SetFeatureData(cfData); Features::Feature *feature1 = builder->CommitFeature(); builder->Destroy(); builder = NULL; } catch (exception& ex) { errorCode = 1; if (builder != NULL) { builder->Destroy(); builder = NULL; } PolygonalHoleUI::theUI->NXMessageBox()->Show( "Block Styler", NXOpen::NXMessageBox::DialogTypeError, ex.what()); }CreateCustomFeatureBuilder(editedFeature)传 NULL 表示新建,传已有特征表示编辑,这是框架约定。CommitFeature之后一定要DestroyBuilder,否则内存泄漏在反复编辑时会累积。异常分支里也要销毁,测试时漏掉这一步,连续编辑十几次后 NX 直接卡死。
3.5 属性创建与赋值
属性是特征数据的骨架,创建时用CustomAttributeCollection逐个建,注意Property标志位:
std::vector<NXOpen::Features::CustomAttribute*> attrs; NXOpen::Part *workPart(theSession->Parts()->Work()); NXOpen::Features::CustomAttributeCollection *attribCollection = workPart->Features()->CustomAttributeCollection(); std::vector<NXOpen::Features::CustomAttribute::Property> attributeProperties; // 表达式属性:边数、直径 auto *sidesAttrib = dynamic_cast<NXOpen::Features::CustomExpressionAttribute*>( attribCollection->CreateCustomExpressionAttribute("m_polygonSides", attributeProperties)); attrs.push_back(sidesAttrib); // Tag 属性:方向,标记为必填输入 attributeProperties.push_back(NXOpen::Features::CustomAttribute::Property::PropertyMandatoryInput); auto *dirAttrib = dynamic_cast<NXOpen::Features::CustomTagAttribute*>( attribCollection->CreateCustomTagAttribute("m_holeDirection", attributeProperties)); attrs.push_back(dirAttrib); // Tag 属性:目标体,标记为引用目标体 attributeProperties.push_back(NXOpen::Features::CustomAttribute::PropertyIsReferencingTargetBody); auto *bodyAttrib = dynamic_cast<NXOpen::Features::CustomTagAttribute*>( attribCollection->CreateCustomTagAttribute("m_targetBody", attributeProperties)); attrs.push_back(bodyAttrib);PropertyMandatoryInput让 NX 在特征更新时校验该属性非空,PropertyIsReferencingTargetBody告诉系统这个属性引用目标体,影响更新顺序和依赖追踪。赋值时表达式属性要判断原表达式是否为空,为空就新建系统表达式:
auto *sidesAttrib = cfData->CustomExpressionAttributeByName("m_polygonSides"); NXOpen::Expression* sidesExp = sidesAttrib->Value(); if (sidesExp == NULL) { sidesExp = workPart->Expressions()->CreateSystemExpression("3"); } sidesExp->SetValue(this->numSides->Value()); sidesAttrib->SetValue(sidesExp);Tag 属性从选择控件取对象,取到唯一一个才赋值:
std::vector<NXOpen::TaggedObject*> selected = this->holeDirection->GetSelectedObjects(); if (selected.size() == 1) { cfData->CustomTagAttributeByName("m_holeDirection")->SetValue(selected[0]); }4. Core 层拆解:五个回调与几何追踪
4.1 入口注册
Core DLL 的入口函数里实例化核心类,构造函数中完成回调注册:
PolygonalHoleCore *thePolygonalHoleCore = new PolygonalHoleCore(); delete thePolygonalHoleCore;构造函数里拿到类句柄并挂五个 handler:
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));五个回调的声明都是静态成员,签名固定:
static int preUpdateCallBack(NXOpen::Features::CustomFeaturePreUpdateEvent* event); static int createGeometryCallBack(NXOpen::Features::CustomFeatureCreateFeatureGeometryEvent* event); static int modifyGeometryCallBack(NXOpen::Features::CustomFeatureModifyFeatureGeometryEvent* event); static int postUpdateCallBack(NXOpen::Features::CustomFeaturePostUpdateEvent* event); static int informationCallBack(NXOpen::Features::CustomFeatureInformationEvent* event);4.2 回调职责与执行顺序
测试时在 listing window 打日志,能清楚看到顺序:preUpdateCallBack→createGeometryCallBack(或modifyGeometryCallBack)→postUpdateCallBack,informationCallBack只在查看特征信息时触发。职责划分如下:
preUpdateCallBack:更新前准备,用 NX 函数建临时构造特征时在这里做。createGeometryCallBack:首次创建几何,产出体或曲线并登记追踪数据。modifyGeometryCallBack:参数变化后重建几何,逻辑通常和 create 共用。postUpdateCallBack:收尾,清理临时对象。informationCallBack:设置特征信息面板显示的文字。
informationCallBack里用SetInformation写自定义说明:
NXString string = "This is custom information \n \t\t Custom feature has not yet implemented custom code to show custom information"; event->SetInformation(string); return 0;4.3 追踪数据:让几何“活”下来
这是 CustomFeature 最容易漏的一步。创建出的几何如果不登记追踪数据,NX 不认为它属于这个特征,后续编辑、删除、回放都会出问题。追踪对象统一塞进一个 vector,最后AppendOutputTrackingData:
std::vector<Features::TrackingData*> trackingObjectVector; // ... 逐个创建追踪数据 ... event->AppendOutputTrackingData(trackingObjectVector);PK 创建的实体用CreateTrackingDataForParasolidEntity,NX 创建的对象用CreateTrackingDataForNXObject:
// PK 实体 Features::TrackingData *track1 = event->CreateTrackingDataForParasolidEntity( (NXOpen::Features::CustomFeatureEvent::ParasolidId)output1); std::vector<NXOpen::NXObject*> objVec1; objVec1.push_back(dynamic_cast<NXOpen::NXObject*>(body)); track1->AddTagSourceEntities(objVec1); trackingObjectVector.push_back(track1); // NX 对象 Features::TrackingData *track2 = event->CreateTrackingDataForNXObject(line1Tag); std::vector<NXOpen::NXObject*> objVec2; objVec2.push_back(dynamic_cast<NXOpen::NXObject*>(line1Tag)); track2->AddTagSourceEntities(objVec2); track2->SetIntApplicationKey(1); trackingObjectVector.push_back(track2);实体的面也要单独登记,否则面级别的编辑和选择会失效:
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); }不参与显示的中间体,用SetBodies排除:
std::vector<NXOpen::Features::CustomFeatureEvent::ParasolidId> bodiesVector; bodiesVector.push_back((NXOpen::Features::CustomFeatureEvent::ParasolidId)output1); event->SetBodies(bodiesVector);4.4 用 NX 函数建构造特征
如果几何是用UF_MODL_create_cyl1这类 NX 函数建的,要在preUpdateCallBack里包成ConstructionFeatureData,并设置显示:
std::vector<NXOpen::Features::ConstructionFeatureData*> constructionVec; tag_t tCyFeatTag, g_tCylTag; UF_MODL_create_cyl1(..., &tCyFeatTag); UF_MODL_ask_feat_body(tCyFeatTag, &g_tCylTag); NXOpen::Features::Feature *nxFeat = dynamic_cast<NXOpen::Features::Feature*>(NXObjectManager::Get(tCyFeatTag)); NXOpen::Features::ConstructionFeatureData *outputFeatData = event->CreateConstructionFeatureData(nxFeat); outputFeatData->SetShowInGraphicView(true); constructionVec.push_back(outputFeatData);SetShowInGraphicView(true)决定这个构造特征在图形窗口是否可见,调试阶段建议开着,方便确认几何位置对不对。
5. 避坑清单:五个真实翻车现场
5.1 双击特征没反应,历史树无编辑入口
现象:特征创建成功,双击节点或曲线毫无反应。原因:IsWithoutBody与实际产出不符,只出曲线却写false,NX 进编辑态时找不到目标体抛异常。解决:核对createGeometryCallBack里SetBodies的内容,无实体一律改true,重新部署 XML 后重启 NX。
5.2 编辑时对话框打开的是另一个特征
现象:编辑多边形孔,弹出的是 Block And Cylinder 的界面。原因:XML 里两个CustomFeature的FeatureUILibrary写反了,或者FeatureClass字符串和代码注册名不一致。解决:逐字比对 XML 与GetClassFromName的参数,注意命名空间前缀和大小写。
5.3 属性读取返回空指针导致崩溃
现象:二次编辑时程序在dynamic_cast处崩溃。原因:属性名拼写错误,CustomTagAttributeByName返回 NULL,直接Value()解引用。解决:所有按名取属性的地方先判空,可选属性用HasCustomAttribute加类型判断兜底。
5.4 几何创建了但编辑后消失
现象:首次创建正常,改参数后实体不见了。原因:modifyGeometryCallBack里重建了几何但没重新登记追踪数据,旧追踪数据指向已删除对象。解决:create 和 modify 共用同一套追踪登记逻辑,每次重建都重新AppendOutputTrackingData。
5.5 反复编辑后 NX 卡顿甚至无响应
现象:连续编辑十几次,NX 越来越慢。原因:异常分支或正常分支漏掉builder->Destroy(),Builder 对象累积。解决:用 RAII 思路包一层,确保任何路径退出前都销毁 Builder,catch 块里也要处理。
6. 二次开发改造:从测试 Demo 到可维护特征库
测试记录里的代码是能跑通的最小闭环,但要变成团队能长期维护的特征库,得做几件事。第一,把属性名抽成常量头文件,UI 和 Core 共用,避免字符串拼写错误——我见过太多因为一个字母导致属性读空的案例。第二,createGeometryCallBack和modifyGeometryCallBack抽公共函数,只保留入口差异,减少两处逻辑漂移。第三,追踪数据登记封装成模板函数,PK 实体和 NX 对象各一个重载,调用处只传对象和 key。
验证改造是否成功,我一般用三步:新建特征看历史树节点和图标是否正确;双击编辑改一个参数看几何是否按预期重建;删除特征看关联几何是否干净移除。三步都过,再考虑加第二个特征进同一个 XML。
// 属性名常量,UI/Core 共用 namespace PolygonalHoleAttr { constexpr const char* Sides = "m_polygonSides"; constexpr const char* Diameter = "m_polygonDiameter"; constexpr const char* Direction= "m_holeDirection"; constexpr const char* Point = "m_polygonPoint"; constexpr const char* TargetBody = "m_targetBody"; }还有一个具体技巧:调试阶段在五个回调里都往 listing window 打一行带时间戳的日志,能快速定位是哪个回调没触发、触发顺序对不对。上线前把日志级别降下来,只保留异常分支的输出。从那以后我每次新增一个 CustomFeature,都强制先只注册回调打日志、不写几何,确认五个回调按预期触发后再填几何逻辑,这样能把配置问题和代码问题彻底分开。希望帮到你。
本文还有配套的精品资源,点击获取