☰
NXOpen CustomFeature 开发实战:从配置到回调的完整指南
2026/10/15 3:45:45 网站建设 项目流程

简介:这份资源面向从事NX二次开发的C++工程师,聚焦CustomFeature自定义特征的开发实践,整理了测试记录与常见问题处理思路。内容围绕核心库与UI库的拆分、CustomFeatureConfiguration.xml的配置要点展开,涵盖FeatureClass、FeatureName、FeatureIcon、FeatureLibrary、FeatureUILibrary、IsWithoutBody等属性的含义与易错点,并梳理了创建、修改、更新前回调等关键方法的实现逻辑,以及头文件引用与自定义属性配置方式。资源包共1个docx文件,约18KB,以文档形式集中记录调试过程与排错经验,便于对照查阅。目前已有348人学习下载,适合希望掌握NX自定义特征开发流程、规避配置异常与实体判定问题的开发者参考,可帮助快速理解特征注册、界面交互与数据管理的整体脉络。

1. 从两个 DLL 到一条历史记录:CustomFeature 到底解决了什么问题

如果你做过 NX 二次开发,大概率经历过这种尴尬:用 Block UI Styler 搭了个漂亮的对话框,参数一填、按钮一按,几何体是出来了,但历史树里干干净净——用户想改个孔径,只能删掉重来。这不是 UI 的问题,是特征没有"身份"。NXOpen 的 CustomFeature 机制就是给自定义几何操作发一张正式身份证:它让用户自己写的 C++ 逻辑,像拉伸、孔、倒角一样,出现在部件导航器里,支持双击编辑、参数回读、撤销重做,甚至参与装配约束。

这套东西的核心是"两个 DLL + 一个 XML":一个 Core 库负责几何创建与更新回调,一个 UI 库负责对话框交互,再加一份CustomFeatureConfiguration.xml把两者绑到 NX 的特征框架上。听起来不复杂,但真正动手时,XML 里一个属性写错、回调注册顺序不对、IsWithoutBody判断反了,都会让你在"特征创建失败"和"无法编辑"之间反复横跳。这篇笔记按我实际调试的顺序拆一遍:先讲清楚配置和类名的对应关系,再落到 UI 库的创建/编辑双模式、Core 库的五个回调、追踪对象的注册,最后把几个让我卡了半天的坑摊开说。适合已经能写 NXOpen C++ 对话框、但还没把自定义特征跑通的人。

2. 配置先行:XML 字段与 C++ 类名的硬绑定关系

CustomFeature 的加载入口是 NX 启动时扫描application文件夹下的CustomFeatureConfiguration.xml。这个文件名不能改,改了 NX 根本不认。文件里每个<CustomFeature>节点描述一个特征,字段和代码的对应关系是整件事的地基,错一个字母就是"特征类未找到"。

2.1 六个字段各自管什么

拿正文里的配置片段逐条对:

<?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是命名空间加类名,必须和 Core 库里GetClassFromName传入的字符串完全一致,大小写敏感。FeatureName是历史树里显示的文字,可以带空格。FeatureIcon对应资源里的图标名,不带扩展名。FeatureLibrary和FeatureUILibrary分别指向两个 DLL 的文件名(不含.dll),NX 会按名字去application目录加载。IsWithoutBody决定这个特征产不产实体,后面单独说。

2.2 部署路径与加载验证

两个 DLL 加 XML 一起丢进application文件夹,这是 NX 的标准搜索路径之一。我一般会同时确认三件事:DLL 的位数和 NX 一致(64 位对 64 位)、依赖的 NXOpen 版本和当前 NX 匹配、XML 编码是 UTF-8 无 BOM。加载是否成功,最直接的验证是打开 NX 后看特征对话框列表里有没有你的FeatureName;如果没出现,先查 XML 语法,再查 DLL 是否被系统拦截。

提示:XML 里xsi:noNamespaceSchemaLocation指向的 xsd 文件如果缺失,部分 NX 版本会静默跳过整个配置,不报错。部署时把 schema 一起带上更稳。

2.3 IsWithoutBody 的判定逻辑

这个字段是新手最容易翻车的地方。规则很简单:特征创建出来的东西里有没有实体(Body)。如果只有曲线、点、基准,设true;只要有实体,设false。正文里说得很直白——不含体却写了false,创建时会抛"不能创建体或者没有目标体"的异常。反过来,含实体却写true,双击编辑的行为会变得诡异。判断依据不是"我想不想让它有体",而是回调里实际SetBodies或几何输出的结果。

3. UI 库:创建与编辑双模式的对话框写法

UI 库的职责是把用户输入收集起来,塞进CustomFeatureData,再交给 Core 库去建几何。难点在于同一个对话框要同时服务"第一次创建"和"双击编辑"两种场景,而这两种场景下数据的来源完全相反:创建时从控件读,编辑时从已有特征读。

3.1 构造类里拿到特征类句柄

在对话框构造类里,先把管理器和特征类指针准备好,作为成员变量:

// 从 session 拿到自定义特征类管理器 NXOpen::Features::CustomFeatureClassManager *m_CusFeatManager = theSession->CustomFeatureClassManager(); // 按 XML 里的 FeatureClass 字符串取到类句柄 NXOpen::Features::CustomFeatureClass *m_CusFeatClass = m_CusFeatManager->GetClassFromName("NXOpen::CustomFeature::SamplePolygonalHole");

GetClassFromName的入参必须和 XML 的FeatureClass一字不差。这一步失败通常意味着 XML 没加载成功,或者字符串拼错。我习惯在这里加个空指针断言,省得后面在回调里才崩。

3.2 Show 时区分创建与编辑

编辑模式下 NX 不希望你再显示 Apply 按钮,所以Show要传对模式:

// 判断当前是否处于编辑已有特征的状态 NXOpen::Features::CustomFeature *editedFeature = m_CusFeatManager->GetEditedCustomFeature(); BlockDialog::DialogMode mode = (editedFeature != NULL) ? BlockDialog::DialogModeEdit : BlockDialog::DialogModeCreate; theDialog->Show(mode);

GetEditedCustomFeature返回非空就说明用户是双击历史记录进来的。这个判断在初始化、Apply 里会反复用到,逻辑保持一致。

3.3 初始化:编辑时回读属性值

初始化阶段要分两条路。创建时给控件设默认值,编辑时按属性名从CustomFeatureData里把值读回来填到控件上:

NXOpen::Part *workPart(theSession->Parts()->Work()); NXOpen::Features::CustomFeature *editedFeature = mgr->GetEditedCustomFeature(); if (editedFeature == NULL) { // 第一次创建,走默认参数 UF_UI_write_listing_window("初始化中:第一次创建\n"); } else { // 二次编辑,从特征数据回读 NXOpen::Features::CustomFeatureData *cfData = editedFeature->FeatureData(); PopulateUIFromCustomFeatureData(cfData); UF_UI_write_listing_window("初始化中:二次编辑\n"); }

回读时按属性类型分别取值。表达式属性用CustomExpressionAttributeByName,TAG 属性用CustomTagAttributeByName再dynamic_cast到具体类型:

// 读表达式 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()); // 读点 TAG Features::CustomTagAttribute *ptAttrib = cfData->CustomTagAttributeByName("m_polygonPoint"); NXOpen::Point *pt = dynamic_cast<NXOpen::Point*>(ptAttrib->Value()); // 读体 TAG Features::CustomTagAttribute *bodyAttrib = cfData->CustomTagAttributeByName("m_targetBody"); NXOpen::Body *body = dynamic_cast<NXOpen::Body*>(bodyAttrib->Value());

对于可能不存在的属性,用HasCustomAttribute先判断再取,避免空指针:

Features::CustomTagAttribute *sectionAttrib = cfData->HasCustomAttribute("section", NXOpen::Features::CustomAttribute::TypeTag) ? dynamic_cast<Features::CustomTagAttribute*>(cfData->CustomTagAttributeByName("section")) : NULL;

3.4 Apply:创建与编辑共用一套提交逻辑

Apply 里同样先判断模式,创建时新建CustomFeatureData并挂属性,编辑时直接复用已有的 data,最后统一SetFeatureData再CommitFeature:

Features::CustomFeatureBuilder *builder = NULL; try { NXOpen::Part *workPart(theSession->Parts()->Work()); NXOpen::Features::CustomFeatureData *cfData = NULL; NXOpen::Features::CustomFeature *editedFeature = mgr->GetEditedCustomFeature(); builder = workPart->Features()->CreateCustomFeatureBuilder(editedFeature); if (editedFeature == NULL) { // 创建:先建属性集合,再建 data std::vector<NXOpen::Features::CustomAttribute*> attrs(0); CreateFeatureAttributes(attrs); cfData = workPart->Features()->CustomFeatureDataCollection() ->CreateData(m_CusFeatClass, attrs); } else { // 编辑:直接拿已有 data cfData = editedFeature->FeatureData(); } PopulateAttributeValues(cfData); // 把控件值写进 data builder->SetFeatureData(cfData); // 交给构造器 Features::Feature *feature1 = 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时,编辑模式会自动关联到原特征;传 NULL 就是新建。CommitFeature返回的Feature指针可以留着做后续操作,但别忘了Destroy构造器,否则内存泄漏。

3.5 属性创建与赋值

属性分两类:表达式属性(数值参数)和 TAG 属性(几何引用)。创建时用CustomAttributeCollection逐个建,TAG 属性可以带PropertyMandatoryInput或PropertyIsReferencingTargetBody这类标志:

std::vector<NXOpen::Features::CustomAttribute*> attrs; NXOpen::Features::CustomAttributeCollection *attribCollection = workPart->Features()->CustomAttributeCollection(); std::vector<NXOpen::Features::CustomAttribute::Property> props; // 表达式属性 auto *sidesAttrib = dynamic_cast<NXOpen::Features::CustomExpressionAttribute*>( attribCollection->CreateCustomExpressionAttribute("m_polygonSides", props)); attrs.push_back(sidesAttrib); // TAG 属性,标记为必填 props.push_back(NXOpen::Features::CustomAttribute::Property::PropertyMandatoryInput); auto *dirAttrib = dynamic_cast<NXOpen::Features::CustomTagAttribute*>( attribCollection->CreateCustomTagAttribute("m_holeDirection", props)); attrs.push_back(dirAttrib); // TAG 属性,标记为引用目标体 props.push_back(NXOpen::Features::CustomAttribute::PropertyIsReferencingTargetBody); auto *bodyAttrib = dynamic_cast<NXOpen::Features::CustomTagAttribute*>( attribCollection->CreateCustomTagAttribute("m_targetBody", props)); attrs.push_back(bodyAttrib);

赋值时表达式属性要处理"表达式对象为空"的情况,先建系统表达式再设值:

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 属性从选择控件拿对象,注意GetSelectedObjects返回的是 vector,要判 size:

std::vector<NXOpen::TaggedObject*> selVec; auto *dirAttrib = cfData->CustomTagAttributeByName("m_holeDirection"); selVec = this->holeDirection->GetSelectedObjects(); if (selVec.size() == 1) { dirAttrib->SetValue(selVec[0]); } selVec.clear();

4. Core 库:五个回调的注册顺序与追踪对象

Core 库是真正干活的地方。NX 在特征生命周期的不同阶段调用你注册的回调,你在这里创建几何、修改几何、输出信息。回调注册在 Core 类的构造函数里完成,入口函数负责实例化。

4.1 入口函数与回调注册

Core DLL 的入口函数里 new 一个 Core 对象再 delete,触发构造和析构,注册就发生在构造里:

PolygonalHoleCore *thePolygonalHoleCore; thePolygonalHoleCore = new PolygonalHoleCore(); delete thePolygonalHoleCore;

构造函数里拿到特征类句柄,逐个挂回调:

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 各回调的职责

preUpdateCallBack在特征更新前触发,适合做参数校验、准备构造特征。createGeometryCallBack负责首次创建几何。modifyGeometryCallBack在参数变化后重建几何。postUpdateCallBack在更新完成后触发,适合做收尾。informationCallBack输出特征信息,用SetInformation设置显示文本:

int PolygonalHoleCore::informationCallBack( NXOpen::Features::CustomFeatureInformationEvent* event) { 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 追踪对象:让几何参与显示与布尔

创建出来的几何如果不注册追踪对象,NX 不知道它属于这个特征,显示和布尔运算都会出问题。追踪对象分两种来源:Parasolid 实体和 NX 对象。

Parasolid 实体用CreateTrackingDataForParasolidEntity,再把源实体加进去:

Features::TrackingData *trackObject1 = event->CreateTrackingDataForParasolidEntity( (NXOpen::Features::CustomFeatureEvent::ParasolidId)output1); std::vector<NXOpen::NXObject*> objVector1; objVector1.push_back(dynamic_cast<NXOpen::NXObject*>(body)); trackObject1->AddTagSourceEntities(objVector1); trackingObjectVector.push_back(trackObject1);

NX 对象用CreateTrackingDataForNXObject,可以设 application key:

Features::TrackingData *trackObject2 = event->CreateTrackingDataForNXObject(line1Tag); std::vector<NXOpen::NXObject*> objVector2; objVector2.push_back(dynamic_cast<NXOpen::NXObject*>(line1Tag)); trackObject2->AddTagSourceEntities(objVector2); trackObject2->SetIntApplicationKey(1); trackingObjectVector.push_back(trackObject2);

实体的面也要逐个加进去,否则面选不中:

for (int i = 0; i < numFaces; i++) { Features::TrackingData *trackObject = event->CreateTrackingDataForParasolidEntity( (NXOpen::Features::CustomFeatureEvent::ParasolidId)faces[i]); trackObject->AddIntSourceEntity(i + 1); trackingObjectVector.push_back(trackObject); }

最后统一AppendOutputTrackingData:

event->AppendOutputTrackingData(trackingObjectVector);

不参与显示的体用SetBodies排除:

std::vector<NXOpen::Features::CustomFeatureEvent::ParasolidId> bodiesVector; bodiesVector.push_back((NXOpen::Features::CustomFeatureEvent::ParasolidId)output1); event->SetBodies(bodiesVector);

4.4 用 NX 函数建构造特征

如果几何是用 NX 函数(比如UF_MODL_create_cyl1)建的,要在preUpdateCallBack里注册构造特征数据,并设ShowInGraphicView:

std::vector<NXOpen::Features::ConstructionFeatureData*> ConstructionFeatureDataVector; 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); ConstructionFeatureDataVector.push_back(outputFeatData);

5. 避坑排查:那些让我反复重启 NX 的瞬间

这一节全是血泪经验,每条按"现象 → 原因 → 解决"写,都是我在调试 CustomFeature 时真实撞过的。

现象一:特征对话框列表里找不到自定义特征。原因通常是 XML 没被加载。可能是文件名不是CustomFeatureConfiguration.xml,或者 DLL 没放进application目录,或者 XML 编码带了 BOM 导致解析失败。解决:确认文件名一字不差,DLL 和 XML 同目录,用无 BOM 的 UTF-8 重新保存 XML,重启 NX 再看。

现象二:创建时抛"不能创建体或者没有目标体"。原因是IsWithoutBody和实际几何输出不匹配。特征只输出曲线却写了false,NX 认为你该给实体却没给。解决:检查回调里到底有没有SetBodies或实体输出,只有曲线就改成true。

现象三:双击历史记录无法进入编辑,或者编辑时控件是空的。原因是初始化阶段没走编辑分支,或者属性名对不上。CustomExpressionAttributeByName和CustomTagAttributeByName的字符串必须和创建时用的完全一致。解决:在初始化里打印editedFeature是否为空,逐个核对属性名,用HasCustomAttribute做保护。

现象四:几何创建出来了但选不中、不参与布尔。原因是追踪对象没注册或注册不全。只注册了体没注册面,面就选不中。解决:按 4.3 把体和面都加进trackingObjectVector,确认AppendOutputTrackingData被调用。

现象五:编辑后几何没更新,还是旧的。原因是modifyGeometryCallBack没实现重建逻辑,或者CommitFeature后没触发更新。解决:在modifyGeometryCallBack里根据新参数重建几何,确认回调注册成功,必要时在postUpdateCallBack里加日志确认调用顺序。

注意:调试期间多用UF_UI_write_listing_window打日志,比断点靠谱,因为 NX 进程崩了断点也留不住信息。

6. 进阶:用日志窗口把回调执行顺序变成可验证的黑匣子

回调到底按什么顺序触发,是 CustomFeature 调试里最玄学的部分。官方文档给的是理想顺序,实际运行时因为参数变化、撤销重做、装配更新,顺序会变。我的做法是在每个回调入口都打一行日志,把执行轨迹变成可回看的记录。

int PolygonalHoleCore::preUpdateCallBack( NXOpen::Features::CustomFeaturePreUpdateEvent* event) { UF_UI_open_listing_window(); UF_UI_write_listing_window("preUpdateCallBack\n"); return 0; } int PolygonalHoleCore::createGeometryCallBack( NXOpen::Features::CustomFeatureCreateFeatureGeometryEvent* event) { UF_UI_open_listing_window(); UF_UI_write_listing_window("createGeometryCallBack\n"); return 0; } int PolygonalHoleCore::modifyGeometryCallBack( NXOpen::Features::CustomFeatureModifyFeatureGeometryEvent* event) { UF_UI_open_listing_window(); UF_UI_write_listing_window("modifyGeometryCallBack\n"); return 0; } int PolygonalHoleCore::postUpdateCallBack( NXOpen::Features::CustomFeaturePostUpdateEvent* event) { UF_UI_open_listing_window(); UF_UI_write_listing_window("postUpdateCallBack\n"); return 0; }

跑一遍创建、编辑、撤销,日志窗口里就能看到完整轨迹。我整理了一张常见操作的预期顺序表,实际对不上就说明某处注册或逻辑有问题:

操作预期回调顺序
首次创建preUpdate → createGeometry → postUpdate
双击编辑改参数preUpdate → modifyGeometry → postUpdate
撤销postUpdate(回滚)
查看信息information

这张表不是让你背,是让你在日志对不上时有个参照。比如编辑时只看到preUpdate没看到modifyGeometry,那多半是AddModifyFeatureGeometryHandler没注册成功,或者参数根本没变。

另一个进阶技巧是给追踪对象设 application key,用SetIntApplicationKey标记不同来源的几何,后续在postUpdateCallBack里按 key 区分处理。这在特征输出多种几何(体 + 曲线 + 基准)时特别有用,能避免把所有东西混在一起。

从那以后我每次新建 CustomFeature 工程,第一件事就是把五个回调的日志骨架搭好,再往里填几何逻辑。先让轨迹跑通,再谈功能,比反过来省太多重启 NX 的时间。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询