☰
NX Open C++二次开发实战:从环境配置到测量实现
2026/10/25 17:05:35 网站建设 项目流程

简介:本资源是面向NX二次开发初学者与工程技术人员的C++实践项目包,聚焦UG Open C++在家族表(Family Table)自动化管理中的典型应用,解决企业参数化设计与批量变体生成的定制开发需求。压缩包共19个文件,含3个核心头文件(.h)定义接口与回调,3个C源文件(.c)实现主逻辑与对话框功能,3个目标文件(.obj)及1个动态链接库(.dll),辅以VC6工程配置文件(.dsw/.dsp)、调试符号(.pdb)和链接库(.lib/.exp),完整覆盖编译、调试与部署环节,总大小仅61KB,轻量易集成。已有230人学习下载,资源结构清晰,包含可直接编译运行的fam_test工程,配套函数封装(fam_test_fun.h/c)、UI交互模块(fam_test_dialog.h/c)及NX会话初始化、模型加载、家族表创建与参数写入等关键代码片段,为理解NX数据模型与Open API调用提供可实操的入门范例。 看到fam_test.rar_NX Open C++_nx_nx open_open nx_ug open c++这个标题,我第一反应是:这大概率是一个 NX 二次开发练手项目。fam_test是工程名,后面挂的NX Open、NX、UG Open C++基本把技术栈写清楚了——用 C++ 写 NX(也就是以前的 UG)插件,跑的是 NX Open 接口。

这类项目在制造业里太常见了:自动化出图、批量建模、参数化设计、自动测量分析,全是这个路子。fam_test这种命名方式,一般是工程师在验证某个功能时随手起的测试工程名,里面很可能放了一个测试部件和几段测试代码,用来验证“光标位置能不能拿到”“这个面是孔面还是轴面”“测距能不能跑通”这类问题。

这篇文章我就以fam_test这个测试工程为切入点,把 NX Open C++ 二次开发从环境准备、项目配置到高频代码实现的完整链路拆开讲一遍。不管你是刚接触 NX 二开的新手,还是从 Journal 录制转到 C++ 深度开发的工程师,这篇文章都能给你一份可以直接抄作业的参考。

1. 项目拆解:fam_test 到底在测什么

1.1 从文件名反推项目边界

压缩包名字里带着fam_test,结合常见二开场景,我推测这个工程至少包含三样东西:一个 NX 测试部件文件(比如带孔、带轴的实体模型)、一个 Visual Studio 工程(或者 NX Open Wizard 生成的工程文件)、以及编译好的 DLL。命名里fam可能是“Family”的缩写,也可能是一个模块名,但test后缀基本确定这是个功能验证工程。

这种测试工程最大的价值不是“能跑”,而是“验证某个 API 在当前 NX 版本下能不能用、用法是不是和文档一致”。我见过太多工程师在正式项目里被一个 API 卡住,最后发现是版本行为差异导致的。所以fam_test这类工程,本质上是给自己留的技术验证基线——以后再遇到同样问题,直接翻这个工程就能确认。

1.2 三个决定成败的准备工作

很多人在 NX 二开上栽跟头,不是代码写不对,而是最开始的三个准备没做好。

第一个准备是确认 NX 版本和编译器版本的匹配关系。这是老生常谈但永远有人踩坑的点。NX 12.0 官方推荐 Visual Studio 2015 或 2017,NX 2206 系列推荐 VS 2017 或 2019,NX 2306 之后逐步转向 VS 2019/2022。你用 VS 2022 去编译给 NX 12 用的 DLL,运行时经常会出现奇怪的崩溃,就是因为 C++ 运行库对不上。

第二个准备是环境变量。每次启动 NX 之前,UGII_BASE_DIR、UGII_ROOT_DIR必须指向正确的安装目录,PATH里也要包含UGII_ROOT_DIR。如果不确定,可以在 NX 安装目录下找到ugii_env.dat文件,里面会写明所有关键环境变量的默认值。

第三个准备是明确的入口函数约定。NX Open C++ 插件和普通 C++ 程序完全不同,它不是从main函数开始跑的,而是从 NX 进程内加载你编译出来的 DLL,然后调用约定的入口函数ufusr。这个入口函数会接收 NX 传进来的参数,执行完毕后还要告诉 NX“我这个 DLL 能被卸载吗”。这些约定如果没搞清楚,后面写再多代码都白搭。

2. 环境基础:NX Open C++ 开发的底层配置

2.1 版本选型:编译器怎么和 NX 配对

选编译器这件事,直接决定你后面三个月的开发体验。

我个人的经验是:优先选择 NX 官方帮助文档里明确列出的 Visual Studio 版本,不要追新。比如 NX 12.0 的帮助文档中就明确写了支持 VS 2015 和 VS 2017,你用 VS 2019 编译也能弹窗跑起来,但一旦用到某些依赖内部 ABI 的接口,就可能出现“莫名其妙的内存错误”。这不是代码问题,是编译器版本导致的二进制兼容问题。

关于 Windows SDK 版本,建议使用 VS 安装时自带的对应版本即可,不要刻意追求最新版。NX 毕竟是工业软件,求稳比求新重要得多。

2.2 项目属性配置:include、lib、dll 一个都不能少

在 Visual Studio 中创建一个空的 DLL 项目后,需要手动配置以下几项。

附加包含目录要指向 NX 的二次开发头文件目录。NX 12 里通常是$(UGII_BASE_DIR)\UGOPEN,里面放着NXOpen、uf*.h等头文件。如果你用的是 NX 2206 及以上版本,还可能出现NXOPEN目录,要把这两个路径都加进去。

附加库目录同样指向$(UGII_BASE_DIR)\UGOPEN目录。链接时通常需要加入这些库:

库文件名作用
libnxopencpp.libNXOpen C++ 接口基础库,含 Session、Part、Point 等核心类
libnxopencpp_features.libNXOpen 特征建模相关类,如 Feature、Body
libnxopencpp_annotations.lib注释、尺寸标注相关
libnxopencpp_undo.lib撤销栈支持
libnxopencpp_fbm.lib基于特征的建模相关,常用于同步建模替代方案
libugopenint.libUF 风格入口函数库,包含 ufusr、UF_initialize 等符号
libufun.libUF 函数库(UF_MODL、UF_UI 等)
libnxopenuicpp.lib对话框 UI 相关

这里有个容易漏的坑:ufusr和UF_initialize这些符号在libugopenint.lib里,很多新手只添加了libnxopencpp.lib,结果编译通过但链接时报一堆LNK2019,就是因为少了这个库。

配置完包含目录和库目录后,还要在“预处理器定义”里加上_CRT_SECURE_NO_WARNINGS,否则 NX 头文件里的一些老式字符串函数会触发一堆警告,虽然不影响编译,但看着心烦。

2.3 在 NX 里跑起来第一个测试程序

环境配置完成后,先不要写复杂逻辑,按下面的最小结构验证整个链路是否打通。

#include <uf.h> #include <uf_ui.h> extern "C" DllExport void ufusr(char* param, int* returnCode, int rlen) { UF_initialize(); UF_UI_open_listing_window(); UF_UI_write_listing_window("fam_test: Hello NX Open!\n"); UF_terminate(); } extern "C" DllExport int ufusr_ask_unload(void) { return UF_UNLOAD_IMMEDIATELY; }

这段代码做了几件事:包含 UF 核心头文件,声明ufusr为导出函数,在 NX 的 Listing Window 里打印一行文字,最后声明 DLL 可以立即卸载。UF_initialize和UF_terminate的配对是关键,所有 UF 接口调用都必须在两者之间进行。

编译生成 DLL 后,在 NX 里可以按Ctrl+U选择这个 DLL,也可以直接用File -> Execute -> NX Open加载。如果 Listing Window 里出现了那行文字,说明环境全部打通了。

3. 代码实现:四个高频需求的完整示例

3.1 获取光标位置并创建点

获取光标位置是 NX 二开里一个很常见的交互需求,比如用户点击屏幕某个位置,程序在那个位置放置特征或创建对象。

较新的 NX 版本提供了UF_UI_ask_cursor_position,可以直接拿到光标在 WCS 下的坐标值。注意是 WCS,不是绝对坐标系,如果你的工作坐标系被旋转过,这个值就会和绝对坐标不同。

#include <uf.h> #include <uf_ui.h> #include <uf_obj.h> #include <NXOpen/Session.hxx> #include <NXOpen/Part.hxx> #include <NXOpen/Point.hxx> #include <NXOpen/PointCollection.hxx> #include <NXOpen/Coordinates.hxx> extern "C" DllExport void ufusr(char* param, int* returnCode, int rlen) { UF_initialize(); double cursor_pos[3] = { 0.0, 0.0, 0.0 }; UF_UI_ask_cursor_position(&cursor_pos[0], &cursor_pos[1], &cursor_pos[2]); NXOpen::Session* session = NXOpen::Session::GetSession(); NXOpen::Part* workPart = session->Parts()->Work(); NXOpen::Point3d pnt(cursor_pos[0], cursor_pos[1], cursor_pos[2]); workPart->Points()->CreatePoint(pnt); UF_terminate(); }

这段代码混合了 NXOpen C++ 接口(Session、Part、Points)和 UF 接口(UF_UI),这在 NX 二开里非常常见,两者可以共存。注意 C++ 风格接口不需要UF_initialize,但 UF 接口必须要有,所以UF_initialize和UF_terminate包住全部代码是最稳妥的写法。

如果遇到返回坐标始终是(0,0,0)的情况,检查一下是不是在非交互上下文里调用。这个函数必须在 NX 图形窗口有鼠标交互时才有实际意义。

3.2 获取任意点的坐标,含装配环境处理

获取点的坐标听上去很简单,但如果点在装配环境里,或者点不在工作部件里,问题就来了。

单机环境下,遍历所有点对象,读取坐标最简单的方式是这样:

#include <uf.h> #include <uf_obj.h> #include <uf_part.h> #include <uf_point.h> #include <uf_assem.h> #include <vector> static void CollectPointCoordinates(tag_t partTag, std::vector<UF_POINT_data_t>& outPoints) { tag_t objectTag = NULL_TAG; int type = 0; int subtype = 0; UF_OBJ_cycle_objs_in_part(partTag, UF_point_type, &objectTag); while (objectTag != NULL_TAG) { UF_POINT_data_t pointData; UF_POINT_ask_data(objectTag, &pointData); outPoints.push_back(pointData); UF_OBJ_cycle_objs_in_part(partTag, UF_point_type, &objectTag); } }

这里用了UF_OBJ_cycle_objs_in_part遍历部件里指定类型(UF_point_type)的所有对象。有个隐藏问题:如果点对象不在工作部件里,比如是在引用集里,你需要在遍历前确认 partTag 对应的部件已经加载,否则返回的 tag 可能无效。

如果在装配环境下需要把坐标转换到绝对坐标系,可以获取所属实例的变换矩阵,然后做一次坐标变换。这个属于进阶操作,很多工程师直接忽略,导致在装配里拿到的坐标和实际显示位置不一致。用UF_ASSEM_ask_component_data拿到实例的 origin 和 matrix,再与原始坐标做矩阵乘法即可。

3.3 判断孔面还是轴面

判断一个圆柱面是孔面(内表面)还是轴面(外表面),是很多自动化建模需求里的基础操作。比如自动化倒角,就要区分是孔口倒角还是轴端倒角。

核心思路是:拿到圆柱面的轴线方向、轴线上的参考点、面上某个参数点的法向,然后判断法向和径向向量的点积符号。如果法向指向轴线,说明是内表面(孔面);如果法向背离轴线,说明是外表面(轴面)。

#include <uf.h> #include <uf_modl.h> #include <uf_curve.h> // 返回值:true 表示孔面,false 表示轴面 bool IsHoleFace(tag_t faceTag) { int faceType = 0; UF_MODL_ask_face_type(faceTag, &faceType); if (faceType != UF_cylinder_type && faceType != UF_cone_type) return false; double point[3] = { 0.0 }; double dir[3] = { 0.0 }; double box[6] = { 0.0 }; double radius = 0.0; double radData[2] = { 0.0 }; double pData[4] = { 0.0 }; int normDir = 0; UF_MODL_ask_face_data(faceTag, &faceType, point, dir, box, &radius, radData, pData, &normDir); double u = (pData[0] + pData[2]) * 0.5; double v = (pData[1] + pData[3]) * 0.5; double pt[3] = { 0.0 }; double u1[3] = { 0.0 }; double v1[3] = { 0.0 }; double u2[3] = { 0.0 }; double v2[3] = { 0.0 }; UF_MODL_ask_face_props(faceTag, u, v, pt, u1, v1, u2, v2); double n[3] = { 0.0 }; n[0] = u1[1] * v1[2] - u1[2] * v1[1]; n[1] = u1[2] * v1[0] - u1[0] * v1[2]; n[2] = u1[0] * v1[1] - u1[1] * v1[0]; // normDir 为负时法向需要取反 if (normDir < 0) { n[0] = -n[0]; n[1] = -n[1]; n[2] = -n[2]; } // 计算面上点到轴线的投影点 double vx = pt[0] - point[0]; double vy = pt[1] - point[1]; double vz = pt[2] - point[2]; double d = dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2]; if (d < 1e-12) return false; double t = (vx * dir[0] + vy * dir[1] + vz * dir[2]) / d; double projX = point[0] + t * dir[0]; double projY = point[1] + t * dir[1]; double projZ = point[2] + t * dir[2]; double radialX = pt[0] - projX; double radialY = pt[1] - projY; double radialZ = pt[2] - projZ; double dotVal = n[0] * radialX + n[1] * radialY + n[2] * radialZ; // 法向与径向同向,说明法向朝外,是轴面;法向朝内,是孔面 return dotVal < 0.0; }

这段代码里有几个关键点容易出错。

第一个是UF_MODL_ask_face_data返回的pData是面的参数范围(对应 u、v 的最小值和最大值),中间值就是面上接近中心的参数点。第二个是normDir的符号含义,它表示 NX 内部存储的法向方向与由曲面参数导数计算的叉积方向是否一致,必须根据它调整法向符号。第三个是UF_MODL_ask_face_props中的u1、v1、u2、v2,这里u1和v1是 u 向和 v 向的导数,叉积可以求出法向。

如果筒面轴线方向是任意方向的,这段代码都能正确处理,因为投影计算是通用的。

3.4 测量功能:优先用 NXOpen 测量接口

热词里出现了“pk 测量”,这里我多说一句。PK 是 NX 底层的参数化内核接口,直接调 PK 做测量的门槛很高,而且版本兼容性比较差。对大多数二次开发需求来说,NXOpen.Measurement系列接口或者 UF 的UF_MODL_ask_min_dist已经足够。

#include <uf.h> #include <uf_modl.h> extern "C" DllExport void ufusr(char* param, int* returnCode, int rlen) { UF_initialize(); tag_t obj1 = UF_UI_ask_sel_object("选择第一个面", UF_face_type); tag_t obj2 = UF_UI_ask_sel_object("选择第二个面", UF_face_type); if (obj1 != NULL_TAG && obj2 != NULL_TAG) { double minDist = 0.0; double pt1[3] = { 0.0 }; double pt2[3] = { 0.0 }; UF_MODL_ask_min_dist(obj1, 0, obj2, 0, &minDist, pt1, pt2); UF_UI_open_listing_window(); UF_UI_write_listing_window("最小距离: "); UF_UI_write_listing_window(UF_UI_...); } UF_terminate(); }

注意UF_MODL_ask_min_dist的第二个和第四个参数是给“精确解析”用的,传 0 表示用默认方式。这个函数拿到的是最小距离和对应的两个最近点坐标,在自动化报告、碰撞检查里非常实用。

4. 编译、运行、调试:踩坑最集中的一段路

4.1 编译链接失败的典型错误

错误一:LNK2019 无法解析的外部符号 ufusr。这个报错十有八九是少了libugopenint.lib,或者入口函数前面没有加extern "C" DllExport。C++ 编译器会做名字修饰,不加extern "C"符号名就会变成一堆乱码,链接器自然找不到。

错误二:C2065 "UF_point_type" 未声明的标识符。这说明头文件没包含完整,你要检查是否包含了uf_object_types.h,因为很多对象类型的宏定义在这个头文件里。建议写代码时直接包含uf.h,它会带出大部分基础定义。

错误三:编译时一堆“无法打开包括文件 NXOpen/Session.hxx”。这是附加包含目录没配置对,检查项目属性里的 VC++ 目录或 C/C++ 常规附加包含目录,确认指向了正确的 NX 头文件目录。

错误四:链接时库解析冲突。如果你同时链接了多个版本的libnxopencpp*.lib,比如 NX 12 的库和 NX 2206 的库混在一起,会出现一堆重定义错误。确认项目里没有混用不同版本的库目录。

4.2 NX 进程内调试的两种思路

调试 NX 插件和调试普通程序不一样,插件运行在 NX 进程内,你不能直接按 F5 启动它。常用的调试方式有两种。

第一种是附加到进程:先把 NX 启动起来,然后在 VS 里选择“调试 -> 附加到进程”,选择UGII或nx进程,断点就能命中。这种方式最灵活,推荐使用。

第二种是设置 NX 启动路径:在项目属性 -> 调试 -> 命令里填入 NX 的可执行文件路径(比如C:\Program Files\Siemens\NX12.0\NXBIN\ugraf.exe),再设置命令参数为空,这样按 F5 就会启动 NX,比较适合每次从头验证的场合。

调试时有个细节:断点命中的是 DLL 里的代码,如果 DLL 是 Release 版,局部变量可能显示不出来。建议调试用 Debug 配置编译,但发布给用户时用 Release,因为 Release 的性能和启动速度都更好。

4.3 稳定性问题:内存、句柄和异常

NX 二开插件运行在 NX 进程内,你的崩溃就是 NX 的崩溃,所以代码质量要求比普通程序高很多。常见的稳定性问题有三类。

第一类是UF_initialize/UF_terminate不配对。早期版本里UF_initialize是每线程有计数器的,多次调用就要多次UF_terminate配对。现在虽然强壮了一些,但最好还是保持一次调用一次终止的习惯。

第二类是 tag 值过期问题。NX 中对象 tag 在模型修改后可能失效,你缓存了一个面 tag,执行了布尔运算后再用它,轻则返回失败,重则崩溃。解决办法是尽量在使用前重新获取 tag,不要长期缓存。

第三类是异常跨模块边界的问题。在 C++ 插件里抛出异常,如果跨越了 DLL 边界而且没有捕获,可能导致未定义行为。我的习惯是:在ufusr入口处用try/catch(...)把所有代码包住,捕获所有异常后返回一个错误码,避免异常泄漏到 NX 内部。

extern "C" DllExport void ufusr(char* param, int* returnCode, int rlen) { int errorCode = 0; try { UF_initialize(); // 实际业务逻辑 UF_terminate(); } catch (...) { errorCode = 1; } *returnCode = errorCode; }

5. 常见问题速查表与避坑经验

我在 NX 二次开发里踩过不少坑,下面把这些年高频出现的问题整理成速查表,对照查找会很方便。

现象可能原因解决方案
编译后 DLL 加载到 NX 里没反应入口函数名字不对或没导出确认ufusr前有extern "C" DllExport
装配里获取点的坐标和显示位置不一致没有考虑实例变换矩阵用UF_ASSEM_ask_component_data获取矩阵做变换
中文注释或字符串乱码编码方式不对设置环境变量UGII_UTF8_MODE=1,源码保存为 UTF-8
UF_UI_ask_cursor_position返回 0NX 版本不支持或调用上下文错误改用UF_UI_get_current_cursor试一下
运行时找不到 DLL(1309 错误)PATH 没包含 NX 运行目录把UGII_ROOT_DIR加入系统 PATH
链接一堆 LNK2019缺少libugopenint.lib等库对照 2.2 节的库清单逐项检查
在 NX 打印中文正常但保存文件乱码文件编码和 NX 编码不一致统一使用 UTF-8 保存源文件
面遍历时某些面取不到对象类型判断错误或面在非工作部件先用UF_OBJ_ask_type_and_subtype打印类型确认

还有一个容易被忽视的坑:ugii_env.dat 里的环境变量设置。有些用户自己改了UGII_UTF8_MODE或者自定义了路径,导致程序在不同 NX 环境里表现不一致。如果排查问题最终找到环境变量头上,一定要对比ugii_env.dat的差异,这是很多“我这正常但用户那不行”的问题根源。

另外建议给 VS 工程开启/bigobj编译选项。NX 头文件模板展开量很大,默认的目标文件格式可能不够用,加上这个选项可以避免一些莫名其妙的编译错误。

6. 从 fam_test 到正式项目:开发流程的最后一块拼图

fam_test验证完功能后,如果要把代码整理成正式项目,我会建议做三件事。

第一,把验证用的测试代码从正式逻辑里抽离。单独建一个test目录,放那些打印坐标、辅助验证的代码,不要和正式功能混在一起。这样后续维护时不会误删正式逻辑。

第二,封装一个统一的 NX 工具函数库。比如“判断孔面轴面”这个函数,实际项目里可能在倒角、测量、工艺识别等多个模块里用到,把它放到公共库里统一维护,比在三个地方复制粘贴好得多。

第三,写一份 API 版本兼容性记录。你在fam_test里验证过的每个关键接口,记录下 NX 版本、VS 版本、接口行为是否有异常。这个记录在 NX 大版本升级时特别有用,能帮你提前发现哪些接口有变化。

从我个人的经验看,NX 二次开发的核心不是学会调用几个 API,而是建立一套稳定的验证、封装和版本管理机制。fam_test这样的测试工程,如果能坚持做下去,慢慢就会沉淀成你自己的私有技术手册——遇到问题查工程,比翻帮助文档快得多。

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

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

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

立即咨询