ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

NX二次开发回调函数uc1616实战:从DLL配置到菜单加载全解析

NX二次开发回调函数uc1616实战:从DLL配置到菜单加载全解析 搞UG二次开发的人翻老项目的时候大概率见过这个函数uc1616。它在NX Open C API里属于老资格功能但一直到NX10.0都没退役而且很多公司内部的批量处理、参数修改、报表导出小工具就是靠“一个DLL 一个uc1616回调 几个菜单项”撑起来的。我下面就从一个最小可跑的DLL工程说起把uc1616的前因后果、函数参数、VS工程配置、NX加载流程和常见坑一次性讲清楚适合刚入门NX二次开发、准备接手老代码的人也适合想快速搭一个“菜单回调”模板的工程师。1. 项目整体设计与思路拆解1.1 uc1616到底是个什么角色uc1616在NX Open C API体系里属于User Function用户函数这一派。User Function是早期UG提供给二次开发者的C接口函数名基本都是uc开头加四位数字比如uc1601创建菜单、uc1602添加菜单项、uc1603加分隔条。uc1616是这一族里的“回调函数”它不负责创建任何东西而是负责接收通知当NX菜单栏上某个由我们创建的菜单项被点击时NX会把控制权交到uc1616手里。用生活里的例子来理解回调你去餐厅点菜菜单挂在墙上uc1601创建的菜单你点了某个菜用menu_id和user_id定位到某个菜单项服务员不会立刻把菜送来而是把订单传给后厨后厨做完了再端出来。后厨就是uc1616NX就是服务员点击菜单这个动作就是“订单”。uc1616不需要主动去轮询菜单状态NX会在合适的时机主动找上门。所以uc1616的定位是“被NX调用”而不是我们自己的代码去调用它。这是理解这个函数的核心。很多初学者一上来就想着“在哪调用uc1616”方向就反了。正确的做法是把uc1616的实现写好然后通过uc1601注册菜单时把它的地址告诉NX剩下的事全交给NX调度。1.2 回调机制里的menu_id和user_id既然NX会在菜单被点击时调用uc1616那被点击的到底是哪个菜单、哪个菜单项答案就是函数签名里的两个IDmenu_id标识是哪一条菜单user_id标识这条菜单里的哪个菜单项。注册菜单和添加菜单项时我们手工给每个菜单项分配一个user_id比如1号菜单项、2号菜单项后续回调里就能用switch去判断该执行哪段功能。这种设计在老式菜单API里非常通用本质上就是“事件源 事件参数”的思路。menu_id和user_id就是事件参数。NX不需要知道回调函数内部怎么处理它只管把这两个ID原样递过来。我们在回调里根据user_id分发到对应逻辑就行。注意menu_id不是我们自己随意指定的而是uc1601返回的这是因为NX内部要给菜单分配唯一句柄我们自己编一个数字进去很可能和别的菜单冲突。实际写代码时建议用宏或枚举来定义菜单项ID比如#define MY_MENU_ITEM_ONE 1 #define MY_MENU_ITEM_TWO 2这样回调里的switch可读性好很多改菜单项时也不容易把数字写错。等菜单多了、项目大了这套“ID映射”会越来越重要否则回调里一堆魔法数字过一个月自己都不认识。1.3 为什么现在还在用这套老方案看到这里可能有疑问NX10.0都2014年之后的版本了为什么还要用uc1616这种老式菜单直接上NXOpen C Ribbon菜单不是更现代吗这话没错但实际情况里至少有三类场景绕不开uc1616。第一类是存量代码维护。很多企业十年前的自动化DLL就是用uc1601加uc1616这种模式写的功能稳定、集成到了生产流程里做维护的人往往只需要改一小块逻辑没必要把整个DLL推倒重来。第二类是快速原型和内部小工具。临时要一个“导出当前部件BOM”或“批量改属性”的功能用uc1616加几十行代码就能在一个DLL里实现无需编辑器、无需Ribbon工程、无需UI Styler。第三类是跨版本兼容需求。uc1616这种C接口在很长的NX版本周期里变化很小一个DLL经常能在NX7到NX12之间来回用前提是编译器和运行库匹配这对需要同时支持多版本的团队很有价值。当然它也有明显短板界面是老式下拉菜单没有图标和分组如果要做复杂对话框还得另起UF_UI或UI Styler工程。所以方案选型时要说清楚uc1616适合做“菜单入口轻量功能”不适合做“复杂交互工具”。理解了这一点用起来就顺手了。两种方案的差异可以看这张表对比维度uc1616老式菜单方案NXOpen C Ribbon方案代码量一个DLL几十行即可需要工程模板和Ribbon布局界面表现传统下拉菜单无图标分组现代Ribbon支持图标、分组依赖复杂度只依赖UGOPEN头文件和两个库依赖NXOpen C库和UI Styler高版本兼容性老接口稳定但不再演进官方主推、持续更新适用场景内部小工具、老项目维护全新项目、复杂交互2. 核心细节解析与实操要点2.1 uc1616的完整签名与参数语义uc1616在头文件里的声明一般是这样的int uc1616(int *response, int menu_id, int user_id);返回值为0表示正常处理完毕NX可以继续执行后续动作。第一个参数response是输出参数用来向NX反馈本次回调的处理状态通常我们在函数开头就写*response 0。如果确实遇到异常可以把response置为非零值告诉NX“这个回调没有正常完成”NX在日志里会体现出来但不同版本对非零response的处理力度不太一样所以不要过度依赖这个状态该弹错还是自己弹错。menu_id是触发本次回调的菜单ID它在调用uc1601注册菜单时得到。一个DLL里可以注册多条菜单每条菜单都有自己的menu_id回调里通过menu_id区分是哪条菜单被点击。user_id是菜单项的ID它和uc1602添加菜单项时传入的user_id一一对应。这里的ID是“回调标识”而不是“位置编号”即使菜单项在菜单里的位置变动了只要user_id不变回调逻辑就不用改。这个特性在维护动态调整菜单顺序时非常有用。细心的读者会发现uc1616这个函数名本身也只是一个“约定俗成的回调名”。关键不在于函数叫什么而在于uc1601注册时传入的函数地址。你可以把函数命名为任意名称例如myCallback只要函数签名一致就没问题。旧代码里普遍叫uc1616是因为官方文档习惯用它做示例没有规定必须叫这个名字。2.2 配套的uc1601和uc1602到底怎么用uc1616不是光杆司令它必须和菜单创建函数配合使用。最基本的两个是uc1601和uc1602。uc1601用于创建一条带回调能力的菜单典型用法int menu_id 0; int rc uc1601(MyMenu, menu_id, uc1616, menu_id);第一个参数是菜单标题可以带字符指定快捷键字母第二个参数传入菜单ID变量函数返回时会得到分配好的真实ID第三个参数就是我们写好并要注册的回调函数指针第四个参数一般也传同一个menu_id变量的地址用来接收实际生成的菜单ID。如果返回值rc不是0说明注册失败常见的失败原因包括菜单标题冲突、NX菜单系统异常等。如果编译时提示uc1601参数个数不对说明当前NX版本的头文件可能是三参数版本把最后一个参数去掉即可。菜单建好之后用uc1602添加菜单项uc1602(Item One, menu_id, 1, 0); uc1602(Item Two, menu_id, 2, 0);第一个参数是菜单项标题第二个参数是要添加到哪条菜单用uc1601返回的menu_id第三个参数是前面说的user_id第四个参数是快捷键传0表示不设置快捷键。如果想给菜单项加分隔效果在需要分隔的位置调用uc1603它会把后续菜单项和之前的内容分隔开。这些函数都属于User Function菜单族头文件都集中在UGOPEN目录下的相关文件里编译不过时优先检查头文件是否齐全。2.3 回调函数里的API使用纪律uc1616虽然是被NX调用的但它在DLL里做实质性工作时绕不开NX Open的其他API。这里有一条非常重要的纪律任何UF_* API调用之前都要先调用UF_initialize()所有调用结束之后再调用UF_terminate()。UF_initialize的作用是让当前线程进入NX Open环境相当于告诉NX“我要开始用你的API了请把环境准备好”。省掉UF_initialize的典型后果是莫名其妙的崩溃或返回错误码。如果你在回调里写完一段代码运行时处理具体功能时也没报错但不定期崩溃先检查是不是初始化漏了。反过来调用了UF_initialize却不调UF_terminate会让NX觉得这个线程一直占用环境长时间运行会积累资源问题。最稳妥的写法是在回调里把每个case都包成“初始化-干活-终止”三个步骤即使业务逻辑只有几行也是如此。另外还要注意uc1616回调里不要做耗时的同步操作。比如批量处理几百个对象这种事如果直接在回调里做完NX界面会卡住用户体验很差。正确做法是把重活交给后台线程去跑回调里只负责把任务启动起来。但后台线程里用UF_* API又涉及NX Open对不同线程的支持限制没那么简单。所以更务实的建议是功能是轻量级的几十个对象以内的遍历、属性读写、简单参数修改就直接在回调里做功能一旦变重老老实实引入进度条和合理的切分策略别硬塞在回调里。2.4 DLL编译链接时这几个配置不能搞错uc1616要跑起来工程配置里最核心的是“头文件路径、库文件路径、链接库列表、字符集、平台位数”这五件事。头文件和库文件都在NX安装目录的UGOPEN文件夹下比如我装的是D盘Siemens NX 10.0目录就是D:\Program Files\Siemens\NX 10.0\UGOPEN。附加包含目录指向这里附加库目录也指向这里链接依赖项加上libufun.lib和libugopenint.lib两个库——前者装uc系列函数后者装UF_*系列函数。字符集必须设置为“使用多字节字符集”。NX Open C API的函数参数基本都是char*类型VS2013新工程默认是Unicode字符集直接编译会报一堆类型不匹配的错误。平台位数也要和NX版本对齐64位NX就必须编译x64位的DLL用x86编译出来的DLL在64位进程里根本加载不上。很多新手的第一个“加载失败”就是这么来的。把关键配置列成一张表照着检查就行配置项推荐值/路径字符集使用多字节字符集附加包含目录{NX安装目录}\UGOPEN附加库目录{NX安装目录}\UGOPEN附加依赖项libufun.lib;libugopenint.lib平台x64对应64位NX3. 实操过程与核心环节实现3.1 VS2013工程创建与目录配置以NX10.0为例我在NX10.0上习惯用VS2013来编DLL原因很简单NX10.0官方支持的就是VS2012和VS2013这两个编译器VS2012太老VS2013最常见。具体操作说一遍。先打开VS2013新建一个Win32项目向导里选“下一步”而不是直接点完成把应用程序类型设为DLL勾上空项目这样裸工程干净好配置。项目建好后右键项目-属性先看右上角配置和平台把平台选为x64没有x64就先在配置管理器里新建一个x64平台。然后逐项设置通用属性-项目默认值-字符集“使用多字节字符集”。C/C-常规-附加包含目录填NX10.0的UGOPEN目录绝对路径。链接器-常规-附加库目录同样填UGOPEN目录。链接器-输入-附加依赖项加libufun.lib;libugopenint.lib。C/C-预处理器-预处理器定义加_CRT_SECURE_NO_WARNINGS用来屏蔽一些老API的编译警告。如果你的NX10.0还安装了UGII下的其他开发库比如要用到C版的NXOpen接口则还需要在附加包含目录里加NXOpen子目录并在附加依赖项里加libnxopencpp.lib。但本文只做uc1616不需要C库保持最小配置即可。配置完之后先别急着写代码编译一个空的DLL验证工程本身没问题能让后面的问题排查范围缩小一半。3.2 一份可以直接抄的完整代码下面这份代码是我整理过的最小模板包含菜单注册、两个菜单项、回调分发、卸载设置四部分。把路径配置好以后这份代码直接复制到main.cpp里编译能过。#include uf.h #include uf_object_types.h #include uf_exit.h #include ug_ufun.h #define ITEM_ONE 1 #define ITEM_TWO 2 static int uc1616(int *response, int menu_id, int user_id); extern C __declspec(dllexport) void ufusr(char *param, int *returnCode, int rlen) { int menu_id 0; int rc 0; UF_initialize(); rc uc1601(MyMenu, menu_id, uc1616, menu_id); if (rc ! 0) { UF_terminate(); *returnCode 1; return; } uc1602(Item One, menu_id, ITEM_ONE, 0); uc1602(Item Two, menu_id, ITEM_TWO, 0); UF_terminate(); *returnCode 0; } extern C __declspec(dllexport) int ufusr_ask_unload(void) { return UF_UNLOAD_SEL_DIALOG; } static int uc1616(int *response, int menu_id, int user_id) { *response 0; switch (user_id) { case ITEM_ONE: UF_initialize(); // 这里写第一个菜单项的功能 UF_terminate(); break; case ITEM_TWO: UF_initialize(); // 这里写第二个菜单项的功能 UF_terminate(); break; default: break; } return 0; }代码的关键点有三个。第一ufusr是DLL的入口NX加载DLL后第一件事就是执行ufusr菜单注册放在这里最合适。第二uc1616被我定义成static函数但不影响NX调用因为我们通过uc1601把函数指针传出去了地址是有效的static可以避免这个回调符号被外部误引用反而更干净。第三ufusr_ask_unload返回UF_UNLOAD_SEL_DIALOG这是保证DLL在NX会话期间不随便卸载的核心设置等会儿细说。如果编译时提示uc1601或uc1616未声明多半是某种头文件没包含进来。不同NX版本的头文件名略有差异到UGOPEN目录下翻一翻找名字类似ug_ufun.h或ug_menu.h的文件把对应的include加上。NXDOC里的官方示例很少直接用uc1616多数是老的UG Open文档所以遇到这种问题不要慌以本机头文件为准。3.3 加载运行菜单怎么出来、回调怎么触发代码编译通过后在工程输出目录里会有一个MyDll.dll文件。打开NX10.0新建或打开一个模型文件然后点菜单File-Execute-NX Open在文件选择框里选中刚编译的DLL点确定。此时NX会执行ufusr菜单栏里应该立刻多出一个“MyMenu”菜单。点击MyMenu里的Item OneNX会调用uc1616user_id等于ITEM_ONE进入第一个case。怎么确认回调真的执行了最简单的方法是临时在case里加一行UG日志输出UF_UI_open_listing_window(); UF_UI_write_listing_window(Item One callback hit\n);注意用这两个函数需要包含uf_ui.h。重新编译DLL再执行加载一次点击菜单项NX信息窗口里就会出现这行字。看到字说明整个链路就跑通了。这里有一个值得注意的加载行为每次修改DLL代码后需要先让NX卸载旧DLL再加载新的。直接在Session里重复加载同一个DLLNX可能保留旧模块改的效果看不到。卸载的方式是在File-Execute-User Function相关功能里做或者直接把NX关掉重开。实际开发时我一般直接重启NX省心。3.4 卸载模式一个容易忽略的决定性细节很多教程里的DLL例子都返回UF_UNLOAD_IMMEDIATELY意思是ufusr执行完就立刻卸载。对“一次性任务”的程序比如加载DLL后弹个对话框做点事就结束没问题。但我们的uc1616菜单是要长期留在NX菜单栏里的如果ufusr返回后DLL被立刻卸载内存里的指令代码就没了可NX还保留着菜单回调的函数指针这时候点菜单会发生什么轻则毫无反应重则NX崩溃。所以带菜单回调的DLL必须选择延迟卸载返回UF_UNLOAD_SEL_DIALOG或UF_UNLOAD_TERMINATE。SEL_DIALOG的意思是NX在退出前或需要卸载时弹一个对话框询问用户TERMINATE的意思是直到NX进程结束才允许卸载。作为模板我习惯用SEL_DIALOG这样调试时还能在NX的卸载交互里主动卸载DLL。如果用了IMMEDIATELY菜单刷新一下就没了典型的“刚才明明加载成功了怎么菜单点了没反应”。另外如果用VS的调试器附加到NX进程里调试DLL调试结束时不要强制“停止调试”就完事。先让NX正常退出或者先卸载DLL再停止调试否则NX进程被中断菜单注册信息可能残留下一次启动会有各种奇怪状态。这条经验坑过我好几次写在这里。4. 常见问题与排查技巧实录4.1 DLL加载失败的几个直接原因我在网上看到最多的求助就是“File-Execute-NX Open选完DLL以后没反应”或“提示无法加载DLL”。这种事情九成以上出在下面几个地方。一是位数不匹配。NX是64位进程DLL必须是x64你用Win32编译出来的DLL加载时直接失败。到项目属性里看平台的配置即可。二是VC运行库缺失。VS2013编译的DLL依赖vc120运行库有些精简版NX安装环境里没有装加载就报“找不到msvcp120.dll”之类的错误。解决方法是把对应版本的Visual C Redistributable装上或者把DLL改成静态链接运行库在项目属性的C/C-代码生成-运行库里选多线程//MT后者更适合给公司内部多台电脑分发。三是NX环境变量没配好。DLL运行时可能需要从UGII目录加载一些动态库比如libufun.dll对应的运行支持。如果UGII_ROOT_DIR或者系统PATH里没有包含NX的UGII目录加载就可能失败。NX正常安装后一般都会配好但如果你搞过环境变量精简要检查一下。四是链接库不对。libufun.lib和libugopenint.lib没链接编译阶段就会报错根本走不到加载阶段所以这类问题一般在编译阶段就会被卡住反而容易发现。把常见现象汇总到这里现象大概率原因快速定位方法提示加载失败/没反应32/64位不匹配检查项目平台缺msvcp120.dll等运行库VC运行库缺失装VC运行库或静态链接/MT找不到相关NX动态库UGII目录不在PATH检查环境变量加载后菜单栏无变化菜单标题冲突或DLL被卸载换标题、检查卸载模式4.2 菜单加载了但不显示回调不触发DLL加载成功但菜单栏里看不到新菜单或者菜单在但点了没反应这种问题比较隐蔽。先取消“菜单标题冲突”这个嫌疑如果之前曾经用同样的标题注册过菜单而旧DLL还在NX进程里新加载的DLL注册同名菜单可能会失败。解决方法是换一个标题测试或者清理卸载旧DLL后再注册。假如菜单在但点击没进回调优先怀疑DLL已经被卸载。检查ufusr_ask_unload的返回值是不是UF_UNLOAD_IMMEDIATELY。如果是就是前面说的卸载问题。另外还有一种情况DLL虽然返回SEL_DIALOG但NX加载时弹出了卸载询问对话框被误点成“卸载”菜单在但回调函数指针已经失效。遇到这种迷糊情况重启NX重新加载一次就好。还有一种比较隐蔽的DLL里的uc1616定义成了某个成员函数或加了错误的函数签名导致函数指针类型不匹配。uc1601注册回调时编译会对函数指针做类型检查类型不对会编译报错一般来不到运行阶段。如果你是用C项目但没加合适的externC处理也有可能在某些编译器配置下出问题。最省事的是照着模板定义成普通C函数不放在类里。4.3 回调进去了一干活就崩溃回调能被触发说明基础链路没问题。一旦在回调里调用具体功能就崩溃往往是三件事。第一是UF_initialize/UF_terminate没有成对出现或调用顺序有误。在UF_initialize之前就调UF_*系列函数NX会直接异常在UF_terminate之后再做UF_*调用也一样。把case里的逻辑检查一遍确保初始化在最前释放在最后。第二是访问了空指针或持有过期的对象Tag。比如你事先缓存了一个部件或用例的Tag再去使用时部件已经关闭这类Tag在下次会话里是无效的。回调属于事件驱动的运行环境不能用全局变量保存NX对象的Tag长期复用。稳妥的做法是每次回调进来时通过UF_ask_*系列函数现场获取当前显示部件、当前工作部件再用。第三是混合了C API和C API时生命周期没管理好。uc1616是C风格的API如果项目同时链接了NXOpen C库在C回调里获取C对象后要注意释放。很多老工程师的原则是“一个DLL尽量只用一套API”要么走UC/UF纯C接口要么整体搬到NXOpen C混着用调试成本倍增。4.4 我个人常用的三招排查手法第一招写日志。在uc1616第一行就写文件日志记录进入回调时的menu_id和user_id。如果在日志里能看到这两个值说明回调通了看不到问题在菜单注册或加载阶段。日志比断点更可靠因为断点可能需要附加调试器附加上去时NX的某些线程状态可能变化反而不容易复现。第二招VS附加进程调试。以管理员身份启动VS2013在uc1616里下一个断点然后菜单“调试-附加到进程”选中nx.exe进程。回到NX里点击菜单项断点就会命中。这一步能看到真实的调用栈、变量值和函数参数排查效率极高。附加调试需要NX和VS以相同版本和位数运行否则符号加载会有问题。第三招用依赖检查工具看DLL依赖。当年我用Dependency Walker查过好几次这种老式DLL一查就能看出少了哪个运行库、哪个系统DLL对不上。现在Windows下可以用微软的dumpbin命令快速看DLL依赖在VS开发命令行里执行dumpbin /dependents MyDll.dll如果看到没有名的DLL带问号就去安全模式下判定该补什么运行库。5. 项目扩展与个人体会5.1 把uc1616回调接上实际业务模板通了之后接实际业务就方便了。比如给第二个菜单项写一个“导出当前部件所有属性到CSV”的功能在case ITEM_TWO里先用UF_ask_current_part拿到当前显示部件Tag再遍历属性链直到为空最后用C标准库写文件。整个过程差不多五十行代码uc1616只负责把它挂到菜单上。case ITEM_TWO: UF_initialize(); { tag_t part_tag NULL_TAG; UF_ask_current_part(part_tag); if (part_tag ! NULL_TAG) { // 遍历属性、导出CSV } } UF_terminate(); break;还有一个很实用的变体同一个DLL里注册多条菜单。用uc1601分别注册两个菜单各用不同的menu_id回调里先判断menu_id再判断user_id这样可以把“建模辅助”“检查工具”分门别类放好避免一个菜单项列表长得没边。菜单之间回调依然是同一个uc1616也无所谓反正有menu_id做区分。5.2 新项目要不要还用uc1616如果是新项目、从零开始我会推荐考虑NXOpen C和Ribbon菜单毕竟NX11以后官方把大量精力放在新界面上老式菜单在高版本上的测试覆盖会越来越少。但如果是NX10.0环境下的内部工具、或者需要快速交付的小功能uc1616依然是非常高效的起点。记住这个判断标准功能复杂度低、界面要求不高、交付速度快优先就用老方案界面复杂、交互多、要长期迭代直接上NXOpen C。5.3 我的一点实际操作体会这套东西我从NX6一路用到NX10.0最大的体会是“别看它老稳定才是硬道理”。uc1616的机制几十年没变过各种踩坑经验在网上可以搜到一大堆遇到问题不怕没参考。而且这套思路对理解NX Open体系很有帮助ufusr是入口回调是事件初始化是环境卸载是生命周期——这套四段论在任何NX开发方式里都成立。先把这套老路子吃透再去啃NXOpen C你会发现框架其实是共通的。
返回列表