1. 项目定位:一个用uc1616做文件选择的NX DLL
做NX/UG二次开发的人,多多少少都被老代码里的uc1616绊过脚。我前阵子接了个UG10.0的项目,需求本身很简单:在NX菜单里加一个按钮,点下去之后弹出一个文件选择框,选一个prt文件,程序自动读取这个模型上的自定义属性,比如“型号”“材质”,最后把这些信息写到一份txt清单里。
听起来像是十分钟能写完的活,但真正跑起来才发现,NX二次开发的水远比你想象得深。那个文件选择框,我第一反应就是用uc1616,因为它是UG/Open C时代留下的标准UI函数,不依赖.NET环境,不依赖NXOpen UI,老版本新版本通吃。可是网上关于uc1616的教程少得可怜,零星几篇也大多是复制粘贴的代码片段,连参数含义都说不清。我把自己从零到一搭工程、写代码、编译DLL、挂菜单、调试排错的全过程记下来,希望能帮后来人少走几步弯路。
这篇文章适合两类人:一类是刚接触UG二次开发的新手,被各种头文件、库文件、启动目录、环境变量搞晕,不知道DLL到底怎么编出来、怎么被NX加载;另一类是手头有老项目必须用uc1616,想搞清楚这个函数到底怎么调、踩过的坑有哪些的老工程师。我会尽量把所有关键步骤都讲透,包括那些官方文档里不会写的细节。
1.1 需求拆解
这个项目的技术点其实没有一样是新的,但组合起来就是一套完整的NX二次开发链路:
- 编译一个能被NX加载的DLL,入口函数是
ufusr; - 在DLL里调用
uc1616弹出一个文件选择对话框,拿到用户选中的文件路径; - 用UFUN函数打开这个prt文件;
- 读取模型上的自定义属性;
- 把读取结果输出到文本文件;
- 通过
.men菜单脚本把这个DLL挂到NX的菜单栏上,让普通用户点一下就能触发。
这些步骤单独拆开看都不复杂,难的是把整个链条串起来。尤其是uc1616这个东西,很多年轻工程师压根没接触过,因为NX10.0年代已经有NXOpen .NET了,大家都在用FileSelect、Selection这类控件,很少有人还记得UFUN里还有这么个弹文件对话框的老家伙。
1.2 为什么选DLL而不是外部EXE
NX二次开发有两种常见交付形式:内部DLL和外部可执行程序。内部DLL以ufusr为入口,由NX进程加载,相当于在NX进程里跑一段代码,可以直接访问UG当前会话的数据结构;外部EXE则是独立运行的程序,通过UF_initialize连接NX,一般用于批处理、后台任务这类不需要界面的场景。
我这个项目要给车间工程师用,需要他们在NX里选中一个prt、读取属性、生成报告,天然适合做成内部DLL。而且DLL可以和菜单脚本绑定,用户点菜单就自动触发,不需要额外打开一个黑窗口,体验上更接近NX原生功能。
1.3 uc1616在NX开发里的地位
uc1616属于UG/Open C里的UI函数,这类函数以uc开头,是UFUN体系的一部分。说到文件选择,NX提供了好几个层次的接口:老派的uc1616、后来的UF_UI_select_file、再后来的NXOpen .NET的FileSelection。uc1616是最老的一批,但正因为老,它在NX10.0上依然稳定可用,而且代码量极小。
如果你接手的是一个维护了十多年的老项目,里面到处是ufusr、uc1616、uc1601这类函数,那你必须学会和它们打交道。如果你是新项目,我建议优先用UF_UI_select_file,这个函数更正规、参数更清晰,稳定性也更好。但既然标题就是uc1616,我还是会把这个老函数讲透,同时给你对比说明。
2. 开发环境:NX10.0 + VS工程配置
开始写代码之前,得先把开发环境调理好。NX10.0是64位软件,所以你的DLL必须是64位的,这个坑我见过太多人踩了。VS这边,NX10.0官方推荐的编译器是Visual Studio 2012和2013,我用的是VS2012。如果你习惯用新版VS,也不是不能用,但平台工具集最好选成v110或v120,不然链接阶段可能会冒出一些莫名其妙的符号问题。
2.1 环境变量准备
先确认NX安装路径,通常类似C:\Program Files\Siemens\NX 10.0。开发时需要两个关键环境变量:
| 变量名 | 示例值 | 作用 |
|---|---|---|
UGII_BASE_DIR | C:\Program Files\Siemens\NX 10.0 | 提供头文件、库文件、启动脚本的根目录 |
UGII_USER_DIR | D:\NXDev\FileSelector | 自定义开发目录,NX启动时扫描这个目录下的startup子目录 |
UGII_BASE_DIR一般装了NX就有,但很多时候只存在于NX的启动脚本里,不会写进系统环境变量。我在VS里需要用它来配include路径,所以直接在系统环境变量里手动加了一个。加完之后记得重启VS,否则读不到。
UGII_USER_DIR是放我们自己开发文件的地方。在这个目录下建一个startup子目录,把.men菜单脚本放进去,NX启动时就会自动扫描并加载。DLL文件也可以放在startup里,这是最简单粗暴也最不容易出错的方案。
2.2 VS项目设置
新建一个Win32项目,注意不是MFC,不是Windows应用,就是一个空项目。项目属性里几个关键点:
- 平台:选择
x64,这个绝对不能忘。NX10.0没有32位版本,你编一个x86 DLL它铁定不加载。 - 字符集:选择“使用多字节字符集”。
uc1616接收的是char*,如果项目默认使用Unicode,字符串常量都变成宽字符,传进去会直接编译报错或者运行时乱码。 - 附加包含目录:
$(UGII_BASE_DIR)\UGOPEN。所有UFUN头文件都在这个目录下,包括uf.h、uf_ui.h、uc.h。 - 附加库目录:
$(UGII_BASE_DIR)\UGOPEN。库文件也在同一个目录。
配置好这两条,VS就能找到头文件和lib文件了。接下来是链接库的选择,这里有个特别容易搞混的点。
2.3 链接库与运行时注意事项
NX内部DLL的链接库和外部程序是不一样的。内部DLL需要链接:
libugopenint.lib libufun.liblibugopenint.lib是内部开发库,专门给在NX进程内运行的DLL用;libufun.lib是UFUN函数的入口库。如果你链接的是libugopen.lib而不是libugopenint.lib,链接阶段就会报一堆找不到符号的错,因为那个库是给外部EXE用的。
另外,运行时库推荐使用/MD,也就是“多线程DLL”。NX本身是DLL方式组织运行时,如果你的DLL用/MT静态链接运行时,可能在加载时出现运行时库冲突,表现就是DLL加载失败或者莫名其妙崩溃。这个经验是踩出来的,开发阶段尽早改成/MD能省很多事。
3. uc1616怎么用
这东西在中文资料里极其难找,我当初只能去翻UGOPEN目录下的uc.h头文件,才把它的原型搞清楚。所以先教你一个最实用的方法:在你自己的开发目录里加一行#include <uc.h>,然后把鼠标悬停在uc1616上,VS会直接显示函数原型。比我在这儿打字给你抄要可靠得多,因为不同NX版本的头文件可能有细微差异。
3.1 函数功能
uc1616做的事情用一句话说就是:弹出一个标准文件选择对话框,把用户选择的文件路径写进一个字符数组,并返回一个表示操作结果的整型值。
它和Windows自带的GetOpenFileName不一样,它走的是NX自己的UI体系,外观风格和NX原生对话框一致。函数内部会阻塞住,等用户选完文件或者点了取消才返回。所以它只能用在有界面的交互式场景里,不能用在无人值守的批处理脚本里。
3.2 基本调用代码
在我手头NX10.0的uc.h里,uc1616是三个参数的写法:
#include <uf.h> #include <uc.h> char fileName[UF_CFI_MAX_PATH_NAME_SIZE] = ""; int response = 0; int irc = uc1616("*.prt", fileName, &response); if (irc != 0) { uc1601("文件对话框调用失败", 1); } else if (response == 1) { uc1601(fileName, 1); } else { uc1601("用户取消了选择", 1); }解释一下参数:
- 第一个参数是文件过滤器,
*.prt表示只显示prt文件。想显示所有文件就传空字符串"",想同时显示多种类型就按你实际NX版本支持的方式来,有的版本支持*.prt;*.stp这种写法,有的不认识,按头文件注释来。 - 第二个参数是输出缓冲区,函数会把用户选中的完整路径拷贝到这里。
- 第三个参数是响应码,重点说这个。
3.3 返回值和响应码的坑
很多人分不清函数的返回值和response响应码。在我这个NX10.0版本里,uc1616的int返回值表示函数本身是否正常执行,0表示对话框弹出来了、流程走完了;非0表示调用异常,比如没有初始化UI环境。而response表示用户在对话框里的操作结果,1通常代表用户确认选择,其他值代表取消或异常。
为什么我反复强调“我这个版本”?因为NX的历史版本比较多,uc这个函数族在不同版本里的行为偶有差异。有的版本可能response==0才是确认,有的版本第四个参数类型都不一样。稳妥的调试办法是在第一次调用时把irc和response都打印出来,亲自确认一遍再往下写逻辑。
还有一点:uc1616不支持多选。文件选择对话框一次只能选一个文件。如果你需要用户批量选择好几个文件,常规做法是循环调用,让用户一次选一个,全部选完之后点一个“结束”按钮。我在项目里就是这么做的,用一个while循环,每次调完uc1616,如果响应码不是确认就跳出循环,否则把路径追加到一个字符串数组里。
3.4 与UF_UI_select_file的对比
新的UFUN体系里有个更正规的函数叫UF_UI_select_file,它的参数更结构化:
int UF_UI_select_file( char *message, char *filter, char *directory, char *response, int *response_type);它比uc1616多了提示信息和初始目录,response_type明确告诉你用户是选了文件还是取消,语义清晰很多。如果你是新项目,我真心建议用这个。但老项目已经用uc1616写死的代码,除非UI逻辑有大改,否则不必非要迁过去。
4. 完整DLL实现:从选文件到读取属性
光说不练没意思,我把项目里实际跑通的代码精简了一下放出来。这个DLL做的事情是:用户从菜单栏点按钮,弹文件对话框选prt,程序打开prt,读取Material属性,输出到文本报告里。
4.1 ufusr入口
NX加载DLL后,会去找ufusr这个导出函数。没有它,NX会直接报“无法定位程序输入点”,DLL根本不会执行。所以入口函数的名字和导出方式必须严格对齐:
#define DllExport __declspec(dllexport) extern "C" DllExport void ufusr(char *param, int *retcode, int paramLen) { // 具体逻辑在这里 }注意几个细节:
extern "C"必须写。C++编译器默认会做名称修饰,导出符号会被改成乱七八糟的名字,NX找不到,所以必须用extern "C"关掉名称修饰。param、retcode、paramLen这三个参数是NX调用DLL时传进来的,一般用不到,但函数原型必须对上。ufusr内部不要调用UF_initialize()。NX加载DLL时UF已经初始化过了,你再手动初始化反而会出错。这是和外部EXE最大的区别。
4.2 读取属性并输出报告
下面的代码实现了完整的流程:
#include <uf.h> #include <uf_part.h> #include <uf_ui.h> #include <uf_attr.h> #include <uc.h> #include <stdio.h> #include <string.h> #define DllExport __declspec(dllexport) extern "C" DllExport void ufusr(char *param, int *retcode, int paramLen) { int irc = 0; char fileName[UF_CFI_MAX_PATH_NAME_SIZE] = ""; int response = 0; FILE *fp = fopen("D:/NX_Report.txt", "a"); if (fp == NULL) { uc1601("无法创建报告文件", 1); return; } while (1) { // 清空缓冲区,避免上次残留 memset(fileName, 0, sizeof(fileName)); response = 0; irc = uc1616("*.prt", fileName, &response); if (irc != 0 || response != 1) { break; } tag_t partTag = NULL_TAG; irc = UF_UG_open(fileName, &partTag); if (irc != 0) { continue; } UF_ATTR_value_t attrValue; memset(&attrValue, 0, sizeof(attrValue)); attrValue.type = UF_ATTR_STRING; char value[UF_ATTR_MAX_STRING_LEN] = ""; attrValue.value.string = value; irc = UF_ATTR_read_value(partTag, "Material", &attrValue); if (irc == 0) { fprintf(fp, "%s -> %s\n", fileName, value); } UF_PART_close(partTag, 1, 1); } fclose(fp); uc1601("报告生成完成", 1); }这段代码有几个容易出问题的地方,逐个说明:
ufusr里可以直接用fopen写文件,不需要额外初始化,因为DLL跑在NX进程里,I/O环境和普通Windows程序一样。UF_UG_open打开的是prt文件,如果这个文件已经被其他进程占用,或者版本不兼容,返回非0,这里我直接continue跳过,生产环境里最好加上错误提示。UF_ATTR_read_value读取的是部件标签上的属性,属性类型要匹配。如果实际属性是整型而这里指定了UF_ATTR_STRING,会返回错误,所以先确定属性类型再读。UF_ATTR_value_t里有个联合体,字符串类型的指针指向外部缓冲区,所以必须先分配好空间再把指针塞进去,不能省略。
4.3 编译与导出
编辑完代码,直接生成项目。DLL生成位置在VS的输出目录里,我习惯把输出目录直接设到自定义开发目录下的startup里,省得每次拷贝。具体做法:项目属性 -> 常规 -> 输出目录,填$(UGII_USER_DIR)\startup\。
编译结果是一个FileSelector.dll。如果编译都通过,但NX加载时报错“找不到dll”,先检查生成的是不是x64,再看有没有把libugopenint.lib链接进去。
4.4 菜单脚本与部署
DLL没有菜单入口是不行的,需要写一个.men文件。在我的开发目录里建D:\NXDev\FileSelector\startup\FileSelector.men,内容如下:
VERSION 120 EDIT UG_GATEWAY_MAIN_MENUBAR BEFORE UG_UTILITY BUTTON UG_FILE_SELECTOR_BUTTON LABEL 文件选择工具 MESSAGE 使用uc1616选择prt并生成报告 ACTIONS FileSelector.dll END_OF_BUTTON这段脚本的大致逻辑是:在NX Gateway主菜单栏的UG_UTILITY工具菜单之前,插入一个按钮,按钮标签显示“文件选择工具”,点击后执行FileSelector.dll。ACTIONS后面写的DLL名字,不需要写.dll后缀,但实际文件名必须是FileSelector.dll,和脚本里的名字对应。如果对不上,NX会假装这个按钮不存在,连报错都不给。
把FileSelector.dll和FileSelector.men都放到同一个startup目录下,然后设置好UGII_USER_DIR环境变量,重启NX。正常情况下,菜单栏就会出现“文件选择工具”,点一下就能弹uc1616的文件选择框。
5. 实操实录:我踩过的坑
流程说起来简单,实际跑的时候我跟它斗智斗勇了一下午。挑几个最典型的坑说说,每一个都是真金白银换来的。
5.1 DLL加载失败
第一次启动NX,菜单按钮没出现,我的第一反应是.men脚本写错了。检查脚本发现没问题,于是怀疑UGII_USER_DIR没生效,打开系统环境变量看了也没问题。最后灵机一动,用Dependency Walker查看DLL的依赖,发现项目链接的时候居然把libugopen.lib写进去了,而正确应该是libugopenint.lib。链接时没报错是因为两个库都存在,但运行时NX加载DLL时找不到内部符号,菜单按钮自然出不来。
这个问题的排查思路是这样的:菜单按钮不出现,不要只盯着.men文件,先确认DLL能不能被NX正确加载。可以在ufusr入口最前面加一个uc1601("loaded", 1);,如果按钮出现了但点了没反应,那说明加载成功、入口执行失败;如果按钮根本不存在,说明DLL加载或者菜单解析就有问题。
5.2 中文乱码
第二次把DLL跑起来了,但uc1601弹出的中文全变成乱码。查了一圈,原因是项目字符集是Unicode,字符串常量被编译成宽字符,而uc1601期望的是多字节char*。把项目属性里的“字符集”改成“使用多字节字符集”之后,重新编译,中文正常显示。
这个问题在纯英文开发环境里根本碰不到,但国内做NX二次开发的几乎都会遇到。所以项目建好之后,第一步就去把字符集改掉,别等乱码了再折腾。
5.3 uc1616挂起与线程问题
还有一个隐蔽的坑:uc1616不能在后台工作线程里调用。NX的UI操作只能在主线程里做,如果你在一个后台线程里调uc1616弹对话框,轻则对话框不刷新,重则整个NX界面卡死。我一开始想做一个后台批处理功能,在Worker线程里循环调用uc1616选文件,结果UI卡得一动不能动。
后来改成由主线程驱动:点按钮弹一次uc1616,用户确认后把路径加入列表,然后再弹一次,循环往复直到用户取消。虽然交互上略笨,但稳定可靠。如果你真需要后台批量处理,应该先把文件路径收集好,再丢给后台线程做打开、读取、写文件这些不涉及UI的操作。
6. 常见问题速查表
我把这段时间遇到和帮同事排查过的问题整理成一张速查表,方便你遇到类似情况直接对号入座。
6.1 典型报错对照
| 现象 | 原因 | 解决办法 |
|---|---|---|
| 菜单按钮不出现 | .men文件路径错误,或DLL未被加载 | 确认startup目录下有.men,DLL名字与ACTIONS一致,入口函数导出正确 |
| 点击按钮无反应 | ufusr入口缺失或链接错误 | 检查extern "C"和DllExport,链接libugopenint.lib |
| NX启动报“无法定位程序输入点” | DLL没有导出ufusr | 用extern "C"导出,检查函数签名 |
| 编译报找不到头文件 | 附加包含目录未配置 | 添加$(UGII_BASE_DIR)\UGOPEN |
| 链接报大量未解析符号 | 链接了错误的库 | 内部DLL用libugopenint.lib和libufun.lib,不要用libugopen.lib |
| uc1616调用即崩溃 | 缓冲区未分配或字符集不匹配 | 分配UF_CFI_MAX_PATH_NAME_SIZE大小的缓冲区,使用多字节字符集 |
| 中文乱码 | 项目使用Unicode字符集 | 改成使用多字节字符集 |
| x86 DLL无法加载 | 平台选错 | 改成x64重新编译 |
6.2 排查思路
当你遇到DLL相关的问题,我建议按照这个顺序排查:
- 先确认DLL文件是否真的生成到了startup目录。
- 用VS自带的dumpbin工具看一下导出符号,确认
ufusr在不在导出表里。 - 检查DLL依赖的库文件是否静态链接进去,或者运行环境里能否找到。
- 在
ufusr入口写一条消息,排除入口执行问题。 - 最后才去检查菜单脚本和按钮逻辑。
按这个顺序能减少大量无效排查,尤其是新手,别一上来就怀疑NX坏了。
6.3 经验总结
uc1616这个函数本身非常简单,难的是围绕它的整个DLL开发和部署链路。环境变量、VS配置、链接库、导出符号、菜单脚本,每一个环节出问题都可能导致你写好的代码跑不起来。把这些链路搞清楚,你不光会写uc1616,以后换UF_UI_select_file、换NXOpen .NET也只是换个API的事。
我个人更推荐新项目用UF_UI_select_file,但如果你已经有一个用uc1616写好的老模块,把它跑通、维护好,完全不丢人。NX二次开发这么多年,底层框架一直在变,但核心思想从来没变过:搞清楚NX的加载机制,搞清楚你用的函数的契约,剩下的都是工程问题。