UG10.0二次开发uc1616函数实战:DLL菜单选择与编译配置指南
前阵子接手一个UG10.0的老项目要从一堆历史数据里把测量结果整理出来工具是用老UG/Open C API写的编译完就是一个DLL。翻代码时看到一行uc1616(请选择操作, ...)我当时愣了一下——NXOpen都这么成熟了怎么还有这种老掉牙的交互函数但真跑起来发现这个函数在UG10.0里依然稳得很几行代码就能弹出一个菜单选择框比搭BlockStyler对话框省太多事。这篇文章就围绕UG10.0二次开发里编译DLL uc1616菜单选择这条线讲讲这个老函数到底怎么用、为什么NX二次开发默认交付物是DLL、工程环境和踩坑点有哪些。主要面向刚接触NX二次开发、或者需要维护老C API代码的工程师。如果你手头正好有类似的老代码要看懂、改通这篇应该能帮你省不少时间。1. uc1616到底是干什么的从UC函数族看NX的老牌交互API1.1 UC系列不是某个神秘库而是NX C API里的老UINX的C语言二次开发接口大体分两套一套是比较新的NXOpen C另一套是老牌的UG/Open C API。UC开头的函数就属于后者编号规则很简单UC是User Interface的缩写后面四位数字是功能编号。16xx系列专门管界面交互比如消息框、列表选择、菜单选择、文件对话框这些。uc1616属于这个系列里做菜单选择的函数官方文档的说法是显示一个包含菜单项的对话框并返回用户选中的那些项。为什么要关心这么老的函数因为NX的公司级插件里有大量存量代码是从上世纪九十年代的UG继承下来的。这些代码不一定漂亮但经过了十几年生产验证。遇到这类工程你不能一上来就说重写吧得先看懂、能改、能编译。1.2 函数原型拆解参数、返回值、多选行为uc1616的原型在uc.h头文件里长这样int uc1616(char *menu_title, int number_of_menu_items, char *menu_items[], int response[]);四个参数一个返回值menu_title对话框标题用C字符串传入。number_of_menu_items菜单项个数。menu_items菜单项字符串数组每个元素指向一个C字符串。responseint数组长度至少等于菜单项个数。函数返回后response[i]为1表示第i项被选中0表示未选中。返回值用户选中的菜单项总数。如果用户直接取消或一项都没选返回0。我实际用下来它表现最像的是一个多选菜单选项竖着排在对话框里用户可以点选多个然后点OK确认。想做单选逻辑得自己在代码里判断比如限制只允许选一个就检查返回值是否等于1不等于就给出提示。1.3 为什么到今天还要用uc1616加速效果是最直观的理由。NXOpen写一个下拉菜单选择功能要先起BlockStyler编辑器、拖控件、生成回调类代码量一两百行起步。uc1616呢一个函数调用就完事。制作内部工具、测试脚本、数据批处理时让用户从三五个选项里挑一个这种需求非常高频用uc1616配DLL20分钟能从空工程跑到弹窗。另一个理由是兼容性。老项目里的UF函数调用链往往是连环套UF_initialize初始化会话、UF_PART_open打开模型、UF_MODL_create_block创建特征最后用uc1616做交互分支。你把uc1616换成NXOpen的Selection看起来是在现代化实际上动了整个交互模型回归测试成本不低。2. 为什么NX二次开发的交付物总是DLL加载形态带来的自由度2.1 外部程序、内部DLL、脚本/Journal三种路线的差异NX二次开发里程序可以做成三种形态形态运行方式优点缺点外部可执行程序(exe)从NX外部启动独立进程开发调试简单崩溃不影响NX共享数据麻烦UI脱离NX窗口很多UF函数用不了内部DLL由NX进程加载和NX同进程运行能用全部UF/NXOpen APIUI挂在NX里菜单集成方便编译配置稍复杂崩溃可能连累NXJournal/GRIP脚本解释执行不用编译适合录制回放功能弱不适合做复杂的生产工具实战项目大多数选内部DLL。原因很实在你想在NX里画个特征、查个对象、读个属性这些API都需要在NX进程的上下文里才能拿到真实数据。外部exe是另一条路但有会话初始化、环境变量、进程通信一堆麻烦事不是首选。2.2 NX如何定位和调用DLL里的入口函数NX加载DLL时会在导出表里找特定名字的符号找到后按约定调用。最基本的是ufusr相当于DLL的main函数void ufusr(char *param, int *retcod, int param_len)param外部传入的参数一般用不到。retcod返回码给NX判断执行是否成功。param_len参数长度。除了ufusr还有几个可选的约定入口ufusr_ask_name返回程序显示名ufusr_initialize和ufusr_terminate在加载、卸载时调用ufusr_update用于更新参数。最少的情况下只要一个ufusr就能跑但建议把ufusr_ask_name也写上否则Execute对话框里显示的名字会是DLL文件名不好看。2.3 DLL在NX工程里的目录分工交付物不是把DLL丢给用户就完了NX需要知道去哪找它。标准做法是用custom_dirs.dat配置自定义目录目录里放startup文件夹NX启动时会去扫描MyTool/ Uc1616Demo.dll startup/ MyTool.menDLL和.men菜单文件可以都放在startup下。custom_dirs.dat里登记MyTool这个目录的路径。NX启动后扫描startup发现.men就加载菜单点击菜单时按ACTIONS指定去加载DLL。3. UG10.0下的编译环境配置版本匹配是少踩坑的第一步3.1 编译器选型NX10.0该配哪一代VSUG10.0那个年代的NX官方支持的是VS2010/VS2012我在实际项目中两个都用过都能正常编译、加载。比较常见的是VS2012因为工程向导更顺滑MSVC运行库在Win7/Win10上兼容性也还行。一个重要原则不要用太新的VS去编老NX版本的DLL。NX10.0的运行库和C运行时依赖是老一套新编译器生成的DLL很可能因为动态运行库不匹配而加载失败。你要是在NX10.0上碰壁了第一步就该问用什么VS编的。3.2 VS工程属性逐项核对手动建一个空DLL工程时重点检查这几个配置项配置项推荐值原因平台x64NX是64位程序DLL必须64位配置ReleaseDebug版要带调试运行库交付麻烦字符集使用多字节字符集(MBCS)UF/UC函数全是char*Unicode字符集会编译报错附加包含目录$(UGII_BASE_DIR)\UGOPEN这里放着uf.h、uc.h等头文件附加库目录$(UGII_BASE_DIR)\UGOPEN这里放着libufun.lib等库文件附加依赖项libufun.lib; libugopenint.lib;C API的核心库运行库多线程DLL(/MD)减少DLL体积和静态库冲突3.3 头文件与库文件UGOPEN目录里的那些.lib每次配置工程我都会先确认环境变量UGII_BASE_DIR指向哪。它指向NX安装根目录比如D:\NX10.0。UGOPEN是这个目录下的子文件夹里面几十个头文件、十几个.lib是NX C API的宝藏库。libufun.lib承载老UFUN函数比如uf.h、uf_modl.h这些对应的函数。libugopenint.lib是NX Open内部接口uc.h里的UC函数在链接时经常需要它。偷懒起见我一般两个都链上链接器只提取用到的符号多写一个不会让DLL体积变大。4. 从零写一个调用uc1616的DLL完整代码与逐行分析4.1 入口函数的标准骨架先放一个最小可用的完整代码直接在VS里建空DLL工程就能编译#include stdio.h #include string.h #include uf.h #include uf_ui.h #include uc.h extern C __declspec(dllexport) void ufusr(char *param, int *retcod, int param_len) { int rc UF_initialize(); if (rc ! 0) { *retcod rc; return; } char *items[4]; char item0[] 创建方块; char item1[] 创建圆柱; char item2[] 创建球体; char item3[] 退出程序; items[0] item0; items[1] item1; items[2] item2; items[3] item3; int selected[4] {0}; int count uc1616(请选择要执行的功能, 4, items, selected); if (count 0) { char msg[256] 选中项; for (int i 0; i 4; i) { if (selected[i]) { strcat(msg, ); strcat(msg, items[i]); } } UF_UI_open_listing_window(); UF_UI_write_listing_window(msg); UF_UI_close_listing_window(); } UF_terminate(); } extern C __declspec(dllexport) char *ufusr_ask_name(void) { return Uc1616Demo; }4.2 uc1616的调用代码与结果处理逐行说几个要点。UF_initialize()是必须的。NX C API所有UF函数都需要一个会话上下文才可以调用uc1616虽然是UI函数但在NX10.0上我建议也先初始化因为后面大概率会跟着用UF_MODL之类的建模函数你不初始化那些函数直接返回错误码。items数组用二维字符数组初始化而不是直接拿字符串字面量往char*塞。原因很简单函数原型是char *menu_items[]不是const char *说明老接口没承诺不修改内容。虽然实际运行基本只读但用栈上可写数组最稳妥还省得管理内存。response数组清零很重要。万一老函数只更新被选中的项不碰未选中的你不清零就会读到垃圾值。我见过有人在这个地方踩坑选第一项出来四个全选中。4.3 链接和编译阶段最容易撞上的两个错误编译期最常见的报错是无法将参数从const char转换为char或者干脆在宽字符和窄字符之间报类型不匹配。八成是工程字符集还留在Unicode切到多字节字符集立刻消停。链接期最经典的是LNK2019 unresolved external symbol uc1616。这个错码意思很简单头文件让你调用uc1616但链接器在lib文件里没找到uc1616的实现。解决办法就是回到第3章的附加依赖项把libufun.lib、libugopenint.lib确认加上并确认附加库目录指向UGOPEN。另一个链接期大坑是extern C忘写。C编译器默认会对函数名做名称改编导出的符号会变成?ufusrYAX...这种。NX按ufusr这个纯名字去找找不到就报无法加载DLL。我用一个小工具查看过导出表加extern C前后函数名完全不一样这是新手最容易忽视的问题。5. 把DLL挂进UG10.0三种加载方式从调试到交付5.1 最省事的调试加载File→Execute→NX Open开发调试阶段最快的验证方式是NX菜单栏选择File→Execute→NX Open...弹出的对话框里直接浏览到DLL文件选中后点击OK。如果一切正常uc1616的菜单框立刻弹出来。这个方式有个好处绕过菜单注册、custom_dirs.dat配置等一堆前置条件直接验证DLL本身能不能跑。如果这一步都加载失败问题一定出在DLL编译本身而不是NX配置。前提是UF_initialize要返回0然后uc1616才会弹窗。Execute对话框需要你看到Uc1616Demo这个名字说明ufusr_ask_name生效了如果显示的是一串乱码或空格优先检查字符集。5.2 一劳永逸的菜单集成custom_dirs.dat startup目录开发稳定后再考虑集成到NX菜单否则每次都得手动Execute交付给同事也很别扭。第一步在NX安装目录下找UGII\menus\custom_dirs.dat用记事本追加一行MyTools D:\dev\MyTools前面的MyTools是标识名后面是自定义目录的绝对路径。第二步在D:\dev\MyTools下建startup文件夹把编译好的Uc1616Demo.dll放进去再写一个菜单文件VERSION 120 EDIT UG_GATEWAY_MAIN_MENUBAR BUTTON BTN_UC1616_DEMO LABEL uc1616演示 MESSAGE 菜单选择演示 ACTIONS Uc1616Demo.dll第三步重启NX。startup里的.men会被扫描到菜单栏会多出一个uc1616演示按钮点击后NX自动去startup目录找Uc1616Demo.dll并调用ufusr。5.3 菜单DLL加载失败时的排查顺序会遇到的情况很多我按概率排个排查清单dumpbin /exports Uc1616Demo.dll确认导出表里有ufusr。没有就回去加extern C重新编译。确认编译平台是x64。任务管理器看NX位数的时代早过去了NX10.0默认就是64位x86 DLL直接就加载失败。换一台没有装VS运行库的机器时缺MSVCR120.dll这类文件也会加载失败把对应年份的VC Redistributable装一遍。菜单没出现先看custom_dirs.dat路径对不对、startup文件夹名字是否拼错。NX对这层约束挺死板startup不能叫StartUp。都查完还不行退回手动Execute加载如果手动能跑问题基本锁定在菜单注册环节。6. uc1616在NX10.0上的实践笔记字符集、内存与UI线程6.1 中文菜单项的编码问题在UG10.0上uc1616的标题和菜单项支持中文前提是DLL内部用的是当前系统代码页。VS工程用MBCS字符集编译时中文字符串按GBK存进char[]NX在中文Windows上读出来通常正常。但有一个细节NX10.0的老函数对UTF-8支持并不友好。如果你在VS里把源文件另存为UTF-8带BOM字符串常量会按UTF-8写入而老函数按本地代码页解释菜单项就成了乱码。我建议源代码文件保持系统默认代码页保存不要追新上UTF-8。6.2 response数组与菜单项的生命周期管理uc1616是一个同步函数调用期间会阻塞住直到用户关掉对话框才返回。所以items数组和response数组只要在调用期间存在即可放在栈上完全OK不用new。但要警惕一种错误用循环从外部数据源填items时把局部临时变量的指针存进items数组。函数返回后那些临时变量已经析构指针成了悬挂指针。正确做法是先拷贝到持久buffer里保证每个指针在uc1616调用期间都是有效的。6.3 阻塞式UI不能乱用别在回调里嵌套弹出uc1616是阻塞式UI它会在里面自旋等待用户操作。如果在NX的UI回调线程里再次调用uc1616就可能出现交互嵌套轻则无响应重则把整个NX拖死。我的经验是uc1616只放在用户主动点击菜单触发的ufusr入口里用不要在Journal录制、批处理、后台状态更新里调用。遇到无界面的批处理场景宁可写死参数走默认分支。6.4 什么时候该把uc1616换成NXOpen方案说句公道话uc1616适合两三个选项的轻量分支选择但不适合复杂表单没有文本框、没有下拉框、没有动态刷新。你如果要做参数化建模工具老老实实用NXOpen的BlockStyler如果只是让用户在方块、圆柱、球体里挑一个uc1616依然是最短路径。我自己维护的一个判断标准交互需求超过单选/多选确认这两层就换NXOpen。不超过继续uc1616省下的开发时间不是一点半点。最后分享一个小技巧如果你的内部工具要频繁用uc1616可以把它封装成一个通用函数int SelectAction(const char *title, int n, char *options[], int *result) { UF_initialize(); int count uc1616(title, n, options, result); UF_terminate(); return count; }之后所有DLL里都用这个封装代码整洁不少菜单项统一从配置表读取改起来也方便。这个模式我在好几个老项目里都用着稳定又省心。