NX二次开发CustomFeature持久化回放与测试避坑

发布时间:2026/10/29 5:20:29
NX二次开发CustomFeature持久化回放与测试避坑 简介面向具备C与NX Open基础开发者的CustomFeature实战记录文档聚焦用户自定义特征从配置到运行的落地与排错。内容围绕CustomFeatureConfiguration.xml展开逐项说明FeatureClass、FeatureName、FeatureIcon、FeatureLibrary、FeatureUILibrary与IsWithoutBody的含义及取值影响并记录核心库、UI库两个DLL与XML部署到application目录时的注意事项同时梳理特征创建、几何修改、更新前回调所依赖的头文件以及对话框如何区分创建与编辑模式、通过GetEditedCustomFeature判断状态并按属性名回填界面参数的写法。包内仅1个docx文档压缩包约18KB篇幅精炼便于开发时随查随用。已有348人学习适合希望减少配置失误、快速定位运行异常的中级开发者参考。1. 从一次“打开部件特征变红”说起CustomFeature 难在哪做 NX二次开发的人多半遇到过这种情况本地跑得好好的自定义特征换一台机器打开部件导航器里那个特征直接变成红叉模型上少了一块体日志里只有一行看不出因果的报错。参数还在几何没了。CustomFeature 的难点从来不是把几何造出来那部分录一段脚本就能模仿出来难的是让这段逻辑在别人的机器上、在部件被重新打开、被回滚、被编辑、被抑制之后还能原地恢复。它考验的是对引用持久化、参数存储、更新时机和输出体归属的理解。这篇文章记录我在一个槽类自定义特征上的测试过程包括哪些位置必须打日志、哪几个参数必须留出调节入口以及五次翻车分别是怎么发生的。2. CustomFeature 的代码骨架回调分工、GUID 注册和参数契约2.1 Journal 脚本为什么撑不起 CustomFeature 的回放录制出来的 Journal 是一条线性脚本写死了“先选中这条线再输入 12再确定”。它假设运行时的那条线还在、还叫那个名字、还是当前选中的对象。而 CustomFeature 的执行环境完全不满足这些假设。NX 会在下面这些时刻重新调用你的代码部件被别人打开、上游特征被编辑后向下更新、特征被抑制再取消抑制、用户按了撤销和重做、装配里换了引用集。每一次调用你的类都会被重新实例化一次上一次的内存状态不可能跨进程留下来。这意味着代码里任何“一次性假设”都会在回放时暴露出来读一个写死的绝对路径、依赖 UI 里选中的对象数组、把上一次构建出来的 Body 引用存在字段里、在构造函数里做几何运算。我一般把判断标准简化成一句话假设你的代码明天早上会在一个刚启动的进程、一个刚打开的部件、一个完全空的字段状态下被调用一次它还能不能算出和昨天一样的几何。不能那就是结构问题不是调试问题。这条标准直接决定了后面所有设计输入必须是可重新解析的持久标识输出必须交给 NX 的体管理参数必须存在特征自己身上副作用必须放在回放路径之外。2.2 Compute 与 Update 的分工把几何构建和状态维护拆开继承NXOpen.Features.CustomFeature之后Visual Studio 里对着类名敲一遍重写列表就能看到当前版本要求实现的抽象成员。不同版本的方法名和签名会有细微差别以本机 NXOpen 程序集列出来的为准我一般不去背方法名而是先看抽象成员再动手。常见的分工是一个入口负责把几何算出来并把结果体返回给 NX另一个入口负责在特征被更新时维护自身状态。下面这段是几何入口的骨架用拉伸造一个槽体关键点在注释里// 每次都从零构建不读取上一次留下的任何对象引用 public override Feature[] Compute() { Session session Session.GetSession(); Part workPart session.Parts.Work; // 1) 输入用持久标识重新解析而不是用内存里存的 Body/Edge 引用 Section section ResolveSection(workPart); // 2) 临时 builder 用完必须销毁否则会在会话里堆积 ExtrudeBuilder builder workPart.Features.CreateExtrudeBuilder(null); try { builder.Section section; // 用不变文化格式化数字避免小数点在其它区域设置下变成逗号 builder.Limits.StartExtend.Value.RightHandSide 0; builder.Limits.EndExtend.Value.RightHandSide _depth.ToString(CultureInfo.InvariantCulture); builder.BooleanOperation.Type BooleanOperation.BooleanType.Create; // 3) CommitFeature 返回真正的 Feature输出体挂在它下面 Feature result builder.CommitFeature(); return new Feature[] { result }; } finally { builder.Destroy(); } }ResolveSection是最容易出错的一环。它要根据特征上记录的标识去部件里重新找到那条曲线或那个面找不到就抛出可读的异常而不是返回 null 让后面崩在更远的地方。参数_depth不是字段里的默认值而是从特征自身读出来的值这一点在 2.3 里展开。还有一条经验几何入口里不要主动触发部件更新。它本身就在更新流程内部再调一次更新就是递归轻则卡住重则栈溢出。需要刷新视图的话交给 NX 在本次计算结束后自己做。2.3 参数契约Edit 里写、Compute 里读的必须是同一份值我见过最常见的 bug 是参数漂移用户在编辑对话框里把深度从 10 改成 15点确定模型没变再打开编辑框显示的还是 10。原因通常是编辑时把值写进了 Builder 的字段而几何入口读的是另一个字段两边根本不是同一份数据。解决办法是把参数收敛成一张契约表所有读写都走同一组方法参数类型存储位置默认值有效范围越界处理槽深 depthdouble特征自身10.00.1 ~ 500抛 NXException 并写明期望范围槽宽 widthdouble特征自身5.00.05 ~ 200同上圆角半径 filletdouble特征自身0.00 ~ min(width, depth)/2自动收敛到上限并写日志截面引用 sectionRef标识字符串特征自身无必须可解析解析失败直接报错不做兜底// 编辑入口把对话框里的值写回特征NX 保存部件时一起持久化 public override void SetValues(CustomFeature target) { SlotCustomFeature slot target as SlotCustomFeature; if (slot null) return; // 只在这里做一次范围校验Compute 里不再重复判断 slot._depth Clamp(_depth, 0.1, 500.0); slot._width Clamp(_width, 0.05, 200.0); slot._fillet Clamp(_fillet, 0.0, Math.Min(slot._width, slot._depth) / 2.0); } // 读取入口无论谁来问参数都从这里拿 public override void GetValues(CustomFeature source) { SlotCustomFeature slot source as SlotCustomFeature; if (slot null) return; _depth slot._depth; _width slot._width; _fillet slot._fillet; }要点在于几何入口只认特征上的值编辑入口只负责把值从对话框搬回特征。中间不设第二份缓存也不做“先存字段、提交时再同步”的两段式。Clamp这种兜底逻辑只能出现在一处出现在两处早晚会打架。校验失败时抛出带参数名和期望范围的NXException比返回一个默认值让用户以为操作成功了要好得多后者是典型的后悔药设计吃下去才发现问题被藏起来了。2.4 注册与加载GUID 一旦发布就别再动自定义特征要在部件里被认出来靠的是一个稳定的类标识通常用 GUID 字符串表达。这个 GUID 一旦交付出去就不能再改。改了会怎样旧部件里存的是旧 GUIDNX 打开时找不到对应的类特征就变成无法识别的节点几何还在但不可编辑用户只能删掉重建。加载环节的检查我习惯按这个顺序做出问题时逐条排除编译产物的目标框架位数要和 NX 进程一致64 位进程加载 32 位程序集只会给一句轻描淡写的加载失败。程序集放在启动时会被扫描到的自定义目录里路径本身不要有中文、空格和特殊符号这一条在家里机器上很常见在别人机器上很致命。依赖的第三方 DLL 要么一起放进去要么装在全局程序集缓存缺一个就是“找不到类型”。注册完后重启 NX新建一个部件能把这个特征从命令里调出来才算加载成功。注意GUID、类名、程序集名这三样构成对外契约任何一样变动都等价于发布了一个新特征。版本升级时如果确实需要改结构用新 GUID 加迁移逻辑不要在原 GUID 上改语义。3. 测试记录怎么留把一次特征回放拆成能复现的现场3.1 五个必打日志的位置和字段设计测 CustomFeature 最怕的不是报错而是“偶发”——某台机器上某次更新时少了一块面重开一次又好了。这种问题靠人眼观察永远抓不住只能靠日志把现场固定下来。我一般在这五个位置埋点第一进入几何入口的瞬间。记录时间戳、进程号、部件名、特征名、特征的 GUID、以及这次调用是被什么触发的打开部件、编辑后更新、抑制恢复。触发来源很多时候靠上下文推断拿不到就记录当时是否有活动命令。第二输入解析完成之后。记录解析到了几个输入对象、每个对象的稳定标识、有没有解析失败的。这一条能排掉一半“打开就红叉”的问题。第三几何构建完成之后。记录结果体的数量、面数、边数、包围盒尺寸。包围盒尤其有用尺寸差一个数量级往往说明单位或者坐标系数搞错了。第四输出挂接完成之后。记录返回给 NX 的 Feature 数组长度。返回空数组和返回 null 在行为上不一样前者通常安静地失败后者可能直接崩。第五异常出口。记录异常类型、消息和堆栈并且一定要把前面四个阶段中最后一个成功的阶段标出来。// 极简日志追加写、带锁、按部件名分文件避免多会话互相覆盖 internal static class FeatLog { private static readonly object _lock new object(); private static string _path; public static void Init(string dir, string partName, int pid) { // 文件名里带进程号同一台机器开两个 NX 也不会串 _path Path.Combine(dir, ${partName}_{pid}.log); } public static void Write(string stage, string feature, string detail) { if (_path null) return; string line ${DateTime.Now:HH:mm:ss.fff}\t{stage}\t{feature}\t{detail}; lock (_lock) { // 每次打开关闭宁可慢一点也不要留下半个缓冲区 using (StreamWriter sw new StreamWriter(_path, true, Encoding.UTF8)) { sw.WriteLine(line); } } } }字段顺序固定成“时间、阶段、特征、详情”是为了后面能直接拿文本工具筛。日志写得花哨但不统一等于没写。3.2 用例矩阵参数、输入、操作序列三轴怎么组合手工点选测试的覆盖率取决于测试的人当天心情这是不可接受的。我用一张固定的用例矩阵每次改代码跑一遍跑完贴进记录里。表的三个维度是输入几何形态、参数取值、操作序列。输入形态至少要覆盖单条直线、闭合轮廓、带缺口轮廓、退化轮廓长度接近零的线。参数取值覆盖下界、常用值、上界、越界。操作序列覆盖新建、编辑参数、抑制恢复、回滚到特征之前再回滚回来、另存为新部件再打开。编号输入depthwidth操作序列期望主要关注点C01单直线105新建成功1 个体基线C02单直线0.055新建报错提示范围边界校验位置C03闭合轮廓105新建后编辑 depth20体尺寸同步变化参数契约是否一致C04闭合轮廓105抑制后恢复几何与抑制前一致参数持久化C05带缺口轮廓100新建后编辑 fillet3无圆角不报错自动收敛逻辑C06退化轮廓105新建可读异常不崩溃异常出口C07闭合轮廓105另存后重开特征正常可编辑GUID 与加载C08闭合轮廓105回滚到特征前再回来几何恢复回放幂等性C04 和 C08 是最容易被忽略、也最能暴露结构性问题的两条。很多实现在 C01 到 C03 全绿一到 C04 就丢参数一到 C08 就多重影分身。3.3 批量回归用 run_journal 一次跑完一批部件单个用例手工点没问题八条跑三遍就没人愿意干了。我的做法是给每个用例准备一个部件副本再配一段固定的回归脚本用 NX 自带的脚本执行器批量跑。#!/usr/bin/env bash # 批量回归每个用例部件跑一遍固定脚本日志各自落盘 set -u NXBIN${NXBIN:-/opt/nx/bin} CASE_DIR./cases LOG_DIR./logs mkdir -p $LOG_DIR for part in $CASE_DIR/*.prt; do name$(basename $part .prt) # -args 之后是这个脚本自己的参数这里把期望值传进去 $NXBIN/run_journal ./regress.cs $part \ $LOG_DIR/$name.log 21 # 记下退出码非零直接标红别等到翻日志才发现 echo $name exit$? $LOG_DIR/summary.txt donerun_journal的路径随安装方式不同脚本里用环境变量兜底比写死绝对路径稳。回归脚本本身要做的事很简单打开部件、触发一次更新、把特征数量和体的包围盒尺寸打到日志里、保存退出。不要在这个脚本里断言结果断言交给外部的文本比对因为脚本里一旦抛异常后面的检查就全断了。跑完一批之后把summary.txt和各个日志里的包围盒数字横向比一遍差异超过千分之一就人工看一眼。这套流程搭起来大概半天之后每改一次代码跑一遍比自己骗自己“这次改动很小应该没事”靠谱得多。4. 四个必调参数公差、更新时机、输出体归属和撤销标记4.1 建模公差别在代码里写死 0.001几何构建里判断两个点是否重合、两条边是否连续都要用到公差。写死一个1e-3在毫米级零件上通常没事一旦零件尺度变成米级或者微米级这个值要么太松导致小特征被吞掉要么太紧导致本该缝合的面缝不上。我的做法是从部件自身的建模首选项里读当前公差读不到再退化到一个保守默认值并且把这个值打进日志。不同版本读公差的入口略有差异我一般先在部件首选项对象下找公差项找不到再试部件本身的公差属性两个都拿不到才用默认值。这段代码写脏一点没关系关键是不能静默。// 优先用部件当前建模公差取不到才退化并且一定记日志 private static double ResolveTolerance(Part workPart) { double tol 0.0; try { tol workPart.Preferences.Modeling.Tolerance; } catch { tol 0.0; } if (tol 0.0) { tol 1e-3; // 保守默认值仅作为兜底 FeatLog.Write(TOL, -, fallback to 1e-3); } return tol; }还有一个容易忽略的点公差影响的不只是几何还影响你判断“这条线够不够长、这个轮廓够不够闭合”。用错公差最常见的症状是小尺寸特征被判定为退化然后悄悄跳过模型看起来成功了实际上少了一块。4.2 更新时机Compute 里调 Update 会把自己绕进去自定义特征的计算本身就在更新流程里。如果几何入口里再调一次部件更新等于在更新的中途要求重新更新好一点的情况是性能骤降坏一点的情况是递归到栈溢出最坏的情况是状态半新半旧日志里看不出任何异常但几何是错的。需要刷新界面或者让下游特征感知变化时交给 NX 在本次计算结束后统一处理。确实有必须立刻见效的场景我的处理是把动作排队在计算返回之后由外层触发而不是在计算内部直接调。提示调试期如果发现某个特征更新特别慢先在几何入口和出口各打一条时间戳算一下单次计算耗时。如果单次只有几十毫秒但整体更新要几秒问题多半不在你的代码而在反复触发更新。4.3 输出体归属返回还是删除中间态最危险几何入口里创建的体最终只有两种合法归宿作为结果返回给 NX或者被显式删除。第三种状态——创建了、没返回、也没删——就是游离体它不属于任何特征删不掉也管不住保存后还在部件里占地方。我在这上面栽过早期版本为了“先试试看”在计算中途造了临时体做布尔运算算完把结果返回临时体忘了删。单次测试完全正常跑二十次回归之后部件体积翻了三倍打开慢到没法用。处理原则很简单在一个明确的代码块里创建的所有中间体在该块的finally里统一清理只把最终结果交出去。清理时注意别把已经作为输出返回的那个体也删了判断依据用对象标识做比对不要用名字。ListBody temporaries new ListBody(); try { temporaries.Add(BuildToolBody(workPart)); // 中间体登记 Body result BooleanSubtract(workPart, target, temporaries[0]); temporaries.Remove(temporaries[0]); // 已并入结果不能删 return new Feature[] { result.OwningFeature }; } finally { foreach (Body b in temporaries) { if (b ! null) b.Delete(); } }4.4 撤销标记让一次失败的 Compute 能干净退回自定义特征计算失败时最糟糕的结果不是报错是留下半个模型体建了一半、布尔做了一半、参数改了一半。用户只能撤消但撤消到什么程度取决于你之前有没有打过标记。我的习惯是在进入几何入口之前打一个不可见标记正常结束时不管异常时回退到这个标记。这样一次失败的计算对模型来说等于没发生。Session.UndoMarkId mark session.SetUndoMark( Session.MarkVisibility.Invisible, SlotFeature.Compute); try { // ... 几何构建 ... } catch (NXException ex) { // 回退到入口状态避免留下半成品 session.UndoToMark(mark, SlotFeature.Compute); FeatLog.Write(FAIL, featureName, ex.Message); throw; }标记名字带上方法名出问题时在日志里一眼能看出是哪一段回退的。回退之后不要吞掉异常该往上抛就往上抛NX 会在界面上给出提示用户至少知道刚才那一下没成功。5. 避坑与排查CustomFeature 测试里最常翻车的五个现场5.1 现象换台机器打开部件特征变红叉且几何消失现象很直接同一个 prt 文件开发机上打开正常交付到另一台机器上打开导航器里特征节点显示异常模型上对应的体不见了右键也没有编辑入口。原因基本集中在加载这一环。按概率排序程序集没有被扫描到通常是因为放在了非启动目录位数不匹配64 位进程加载不了 32 位程序集依赖的第三方库缺失主程序集能加载但类型初始化失败路径里带中文或空格导致解析失败GUID 在两次编译之间被重新生成旧部件里的标识对不上。排查顺序我固定成这样先看 NX 启动目录里到底加载了哪些程序集确认你的 dll 在列表里再确认位数再确认 GUID 和交付版本一致。GUID 这一条尤其容易犯用模板新建项目时随手生成的 GUID如果每次重编译都重新生成那每次发布都是一个新特征。解决办法是把 GUID 写死在源码里作为常量编译器不会替你保管这个值。注意交付前在干净机器上做一次“拷过去、打开、编辑、保存”的全流程验证比在开发机上跑一百遍都有用。5.2 现象改完参数提交模型纹丝不动编辑对话框里把深度从 10 改成 20确定模型没反应再打开编辑框显示的还是 10。原因是参数写到了错误的地方。常见有三种写进了 Builder 的私有字段但没同步回特征同步回特征了但几何入口读的是另一个字段几何入口读对了但特征没有被标脏NX 认为不需要重算。前两种属于契约问题第三种属于更新机制问题。排查方式是在几何入口第一行把读到的参数值打进日志。如果日志里是 20 而模型没变问题在更新机制如果日志里还是 10问题在参数同步。定位之后按 2.3 的契约表收敛所有读写只留一个入口。顺带说一句这个问题在中文环境下还有一个变体参数是数值但被格式化成带千分位的字符串再解析回去结果解析失败静默用了默认值。数值和字符串之间的转换一律指定不变文化别依赖当前线程的区域设置。5.3 现象连续更新两次模型上多出一块重影第一次更新正常触发第二次更新后模型上多出一块位置偏移的体或者同一个位置叠了两层。原因是几何入口不具备幂等性。每次调用都新建一个体但没有把上一次的输出清理掉NX 又把新旧两个都当成了输出。还有一种情况是中间体没删布尔运算把它并进了结果看起来像重影。判断方法很简单连续触发三次更新看体的数量是不是线性增长。是就是幂等性问题。修法不是在下游去重而是在入口处保证“给定同样的输入和参数必然得到同样数量的输出体”中间体在finally里清干净。5.4 现象撤销一次之后特征树里出现幽灵体用户按了一次撤销界面上特征没了但模型里还留着一块体删不掉也选不中保存后还在。原因是几何入口里做了非事务性的写操作直接改了部件属性、往文件里写了东西、或者创建了不受撤销栈管理的对象。撤销只能回退事务内的变更事务外的副作用会留下来。日志文件写到一半也是同样的道理那个不属于模型但会让人误判。修法是把这个入口当成纯函数看待只读输入、只产出几何、不写任何外部状态。日志和统计信息在入口之外记录或者至少保证写日志本身失败不影响几何计算。5.5 现象大批量跑回归时日志互相覆盖批量跑的时候二十个用例的日志混在一个文件里时间戳交错根本没法按用例读。原因是日志路径写死成一个固定文件多个进程或者多个部件共用了同一个句柄。修法在 3.1 的代码里已经体现文件名带部件名和进程号写入时加锁每次追加都重新打开文件。还有一个隐蔽变体日志路径依赖当前工作目录而批量脚本执行时的工作目录和手工执行时不一样结果日志写到了一个谁也没想到的地方。路径一律用绝对路径拼出来不要用相对路径。6. 最小复现部件把“偶发失败”钉成“必然失败”测试做到后面最耗时间的不是写用例是把一个“偶尔出错”的现象固定成“每次必错”。我的办法是维护一组最小复现部件每个部件只保留触发问题所必需的那几个对象多余的特征全部删掉。建这组部件的过程有固定套路。先在出问题的部件上做二分删掉一半上游特征看还复现不复现复现就继续删另一半不复现就把删掉的加回来换个方向删。通常三到四轮就能把几十个特征砍到三五个。然后把参数推到边界上——深度取到最小值、轮廓改成刚好不闭合、引用换成一个被抑制过的面——问题从概率事件变成必然事件。我常用的三个对照部件是这样的部件内容用途判定标准base.prt单条直线 一个自定义特征默认参数基线任何改动后先跑它体数 1包围盒与记录值误差小于千分之一edge.prt退化轮廓 边界参数验证异常出口和公差处理抛出可读异常日志有 FAIL 记录loop.prt闭合轮廓 上游被抑制的特征验证回放和引用重解析抑制恢复后几何与抑制前一致三个部件跑完再把base.prt另存一份、关掉 NX、重新打开、编辑一次参数、再保存。这一套下来能覆盖住绝大多数回放相关的问题。剩下的那些只在特定装配上下文里出现的才需要单独再建。验证几何是否一致别用眼睛看。我一般在回归脚本里取三个数体的数量、面的总数、包围盒对角线长度输出成一行固定格式的文本和上一次的基线做字符串比对。数字对不上就人工看数字对得上基本可以放心。这一招比截图对比省事得多也不受显示模式和视角影响。最后说个我自己的习惯。每次改完 CustomFeature 的代码我不急着打开那个复杂的总装部件而是先在base.prt上跑一遍新建、编辑、抑制恢复、另存重开这四步四步都过再往上叠复杂度。这个习惯是踩坑踩出来的早些年图省事直接在总装上验证一个报错要花半天才能定位到是自己代码的问题还是上游引用变了那种感觉就像在黑匣子里找一根针。基础部件跑通只要两分钟能挡掉八成低级错误剩下两成才值得动复杂模型。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询