☰
NX二次开发:UG外部模式创建Block的C++实现与保存
2026/10/14 6:32:36 网站建设 项目流程

简介:该资源聚焦UG NX外部开发(NX Open API)场景,是一份可直接运行的Block创建示例及讲解文档,面向掌握C/C++基础、希望用开发模式自定义建模流程的工程师或二次开发学习者。包内共1个docx文本,压缩包仅14KB,便于快速查阅代码结构与调用逻辑。内容围绕UF_initialize初始化、UF_PART_new新建公制零件、UF_MODL_create_block1创建块特征、UF_PART_save保存、UF_PART_close关闭及UF_terminate收尾等关键API展开,并附带环境中报错输出与步骤提示,适合用来理解外部开发的基本流程和函数调用顺序。目前已有619人学习下载,适合入门阶段对照练习,也可作为后续封装更复杂建模命令的起步模板。

1. 为什么用外部模式创建Block:一次手工操作引发的自动化需求

我在做NX二次开发时遇到过一类场景:模板模型里需要连续创建几十个等距排列的长方体,尺寸还要随参数变化。如果在NX界面里手动拉伸长方体再逐个改尺寸,不仅耗时,还容易漏改。后来我把这条流程用UG外部开发模式写成了一个独立运行的C++程序,调用UF_PART_new和UF_MODL_create_block1,几秒钟就把零件生成好并保存成prt文件。这里要拆解的正是这个最小可复现示例:在NX进程外创建新零件、创建Block、保存并关闭,前后不到40行代码。适合正在做NX二次开发的工程师,也适合想从内部模式转向外部批处理的开发者。

2. 搭建UG外部开发环境:从UF_initialize到编译链接的完整链路

2.1 为什么选外部模式:批处理场景下的环境优势

UG外部开发模式指的是程序以独立进程运行,通过NX Open API与NX内核交互。与之相对的是内部模式,也就是在NX进程内加载DLL。内部模式能调用UI、直接访问Session和显示窗口,适合做交互式功能;缺点是要打开NX图形界面,而且程序异常时可能拖垮整个NX进程。

外部模式的优势在于“无人值守”。它不依赖图形界面,可以在命令行、脚本甚至计划任务里运行。批量创建零件、批量转换格式、批量做几何检查,这些场景没有界面交互需求,外部模式是最省事的方案。本文示例就是在外部模式下完成“建零件、建Block、存盘、关文件”的完整闭环,关键词“UG外部开发”指的就是这种模式。

选型时我一般遵守这样的原则:功能只要不涉及界面交互,就优先用外部模式;如果需要用户拾取对象、实时预览,才考虑内部模式。Block创建本身是纯数据操作,不需要选中体和对话框,所以这里用外部模式非常合适。

2.2 环境准备:NX安装目录、环境变量和库链接

外部模式程序依赖NX安装目录下的头文件和库文件。以NX9系列为例,NX根目录下通常有UGII子目录,头文件在UGII\UGOPEN,库文件在UGII\lib。工程配置时最好用UGII_BASE_DIR环境变量来引用根目录,不要写死绝对路径,因为不同机器上的NX安装位置可能不同。

我习惯在编译前确认三件事:

  • 系统环境变量UGII_BASE_DIR存在,且指向NX根目录。
  • PATH中包含%UGII_BASE_DIR%\UGII,运行时才能找到NX动态库。
  • 本机NX许可证能正常启动,否则UF_initialize会直接失败。

新建C++控制台工程后,配置要点如下:

  • 附加包含目录加$(UGII_BASE_DIR)\UGII\UGOPEN,uf.h、uf_part.h、uf_modl.h都位于这里。
  • 附加库目录加$(UGII_BASE_DIR)\UGII\lib。
  • 链接器输入中增加libufun.lib和libugopenint.lib。

如果编译报“无法解析的外部符号”,先检查库目录是否指向lib目录,再检查链接器输入的库文件名有没有拼错。另一个常见问题是release版本编译却链接了debug库,也会造成符号冲突。

2.3 最小验证程序:先让UF_initialize跑通

环境配置完成后,不要急着写建模代码,先编译一个空壳程序,验证外部进程能不能和NX内核握手:

#include <uf.h> int main(int argc, char *argv[]) { // 外部模式第一步:建立与NX内核的连接 int irc = UF_initialize(); if (irc == 0) { UF_terminate(); } return irc; }

这段代码只做两件事:UF_initialize建立连接,UF_terminate释放连接。UF_initialize返回0表示成功;返回非0时需要先查环境变量和许可证。控制台程序如果没有任何输出但能正常退出,说明环境链路是通的。

很多老示例会把main写成extern void main(char argc, char *argv[]),这在C++标准里是不合法的,main必须返回int,argc类型也应该是int。新写的工程直接使用标准签名,可以避免编译阶段踩坑。

2.4 UF_CALL宏:把错误码变成可读日志

示例代码里的#define UF_CALL(X) (report_error(__FILE__, __LINE__, #X, (X)))是NX Open老工程里常见的错误处理宏。它把函数调用表达式X转成字符串,连同文件名、行号传给report_error。只要函数返回非0,report_error就调用UF_get_fail_message拿错误文本,再通过UF_print_syslog输出到系统日志。

static int report_error(char *file, int line, char *call, int irc) { if (irc) { char err[133], msg[133]; sprintf(msg, "*** ERROR code %d at line %d in %s:\n+++ ", irc, line, file); UF_get_fail_message(irc, err); UF_print_syslog(msg, FALSE); UF_print_syslog(err, FALSE); UF_print_syslog("\n", FALSE); UF_print_syslog(call, FALSE); UF_print_syslog(";\n", FALSE); } return (irc); }

用UF_CALL包裹函数调用有三个好处:出错时能看到具体文件行号;错误码能转成人类可读文本;调用链上某一步失败时,程序不会继续往下走。外部模式下UF_print_syslog默认输出到标准输出和系统日志,调试时非常直观。

如果不想用宏,也可以手动判断每个函数返回值并打印错误码,但代码会膨胀很多。示例代码用这个宏是合理选择。

3. 逐行拆解创建Block的内部逻辑:UF_PART_new与UF_MODL_create_block1

3.1 完整可运行代码骨架

先放完整主程序,代码逻辑和示例一致,只把文件名改成中性的block_demo.prt:

#include <uf.h> #include <uf_part.h> #include <uf_modl.h> #include <iostream> using namespace std; #define UF_CALL(X) report_error(__FILE__, __LINE__, #X, (X)) static int report_error(char *file, int line, char *call, int irc) { if (irc) { char err[133], msg[133]; sprintf(msg, "*** ERROR code %d at line %d in %s:\n+++ ", irc, line, file); UF_get_fail_message(irc, err); UF_print_syslog(msg, FALSE); UF_print_syslog(err, FALSE); UF_print_syslog("\n", FALSE); UF_print_syslog(call, FALSE); UF_print_syslog(";\n", FALSE); } return (irc); } int main(int argc, char *argv[]) { if (UF_CALL(UF_initialize())) return 1; tag_t newPart = NULL_TAG, blockTag = NULL_TAG; // 1. 创建公制新零件,路径必须已存在 UF_CALL(UF_PART_new("C:\\temp\\block_demo.prt", UF_PART_METRIC, &newPart)); cout << "Step(1)-->Create the part without errors" << endl; // 2. 用角点和边长创建Block特征 double corner[3] = {0.0, 0.0, 0.0}; const char *edges[] = {"100", "100", "100"}; UF_CALL(UF_MODL_create_block1(UF_NULLSIGN, corner, edges, &blockTag)); cout << "Step(2)-->Create the block without errors" << endl; // 3. 保存当前零件 UF_CALL(UF_PART_save()); cout << "Step(3)-->Save the block without errors" << endl; // 4. 保存并关闭零件,释放内存 UF_CALL(UF_PART_close(newPart, 1, 1)); cout << "Step(4)-->Close the part without errors" << endl; UF_CALL(UF_terminate()); return 0; }

运行后控制台会依次打印Step(1)到Step(4)。如果某一步失败,UF_CALL会打印包含文件名和行号的错误信息,程序不再继续。

这里与原始示例代码有三处主动改动。第一处是把extern void main(char argc, char *argv[])改成标准int main(int argc, char *argv[]),否则C++编译器报错。第二处是把char *edges[]改成const char *edges[],因为字符串字面量在C++里类型是const char*,用char*会产生编译警告或错误。第三处是把UF_PART_close(newPart, 0, 1)改成UF_PART_close(newPart, 1, 1),避免在没保存的情况下把数据丢掉,后面详解。

3.2 UF_PART_new:创建新零件的单位、路径和返回tag

UF_PART_new是外部模式创建新零件的第一步。函数原型概括为:

int UF_PART_new(const char *part_name, int units, tag_t *part_tag);

第二个参数units是单位制,UF_PART_METRIC表示公制毫米,UF_PART_ENGLISH表示英制。第三个参数part_tag是输出参数,成功后会返回新零件的tag句柄。后续所有建模函数都依赖这个tag定位操作的零件。

一个常见误区是认为路径不存在时函数会自动创建目录。实际上UF_PART_new只负责创建文件,不负责创建文件夹。如果C:\temp不存在,调用就会返回错误码。我在自动化脚本里一般先用系统API创建目录,再调用UF_PART_new。

文件名后缀必须是.prt,NX允许不带后缀,但为了其他程序识别方便,建议显式写.prt。文件名里尽量不放中文和空格,虽然NX能处理,但外部程序日志和脚本传参容易出问题。

3.3 UF_MODL_create_block1:用corner和edges定义Block

创建Block的函数是UF_MODL_create_block1,四个参数分别是UF_NULLSIGN、corner、edges、blockTag。先看参数含义:

  • 第一个参数是sign,代表Block特征在特征树上的布尔类型。UF_NULLSIGN表示没有目标体、直接创建独立块特征。
  • 第二个参数corner是double[3],表示块的起始角点坐标。示例是{0,0,0},也就是原点。
  • 第三个参数edges是字符串数组,表示X/Y/Z三个方向的边长。示例里三个"100"代表长宽高都是100的正方体。
  • 第四个参数是输出参数blockTag,返回的是Block特征tag,不是体tag。

edges数组每个元素都是“表达式字符串”而不只是数字。传"100"和传"2*50"效果等价,因为NX会把这个字符串当作表达式解析。这意味着可以传一个部件里已存在的表达式名,比如"L"、"W"、"H",后续通过修改表达式值来驱动块尺寸,这个特性在参数化建模中非常有用。

double corner[3] = {0.0, 0.0, 0.0}; const char *edges[] = {"200", "50", "30"}; UF_CALL(UF_MODL_create_block1(UF_NULLSIGN, corner, edges, &blockTag));

这段会把默认100x100x100改成200x50x30的长方体。corner是角点不是中心点,所以Block会从原点沿X/Y/Z正方向伸展。

3.4 保存与关闭:UF_PART_save和UF_PART_close的配合

UF_PART_save不接收参数,保存当前外部程序正在操作的零件。它只把内存中的模型数据写入磁盘,不会关闭文件,因此save之后还能继续创建特征。

UF_PART_close有三个参数,建议统一使用:

UF_CALL(UF_PART_close(newPart, 1, 1));

第二个参数表示关闭模式:1代表保存后关闭,0代表不保存直接关闭。第三个参数表示是否释放该零件在内存中的数据,1代表释放。如果在调用UF_MODL_create_block1之后直接close而中间没有save,那么mode传0会丢失全部对象。这正是很多新手“程序跑完没生成文件”的根源。

原始示例先UF_PART_save再UF_PART_close(newPart, 0, 1),因为数据已经落盘,mode传0不会丢文件。不过我在实际工程中更推荐统一用mode=1,少一层记忆负担。

4. 参数背后的建模语义:corner、edges、UF_NULLSIGN与特征tag的边界

4.1 corner坐标:绝对坐标下的起点约定

UF_MODL_create_block1里corner数组的三个元素分别代表X、Y、Z坐标。外部模式下没有打开的图形窗口,这个坐标通常按部件绝对坐标系解释,而不是工作坐标系。这一点很多人栽过:界面里看到WCS被旋转过,但程序创建的Block仍然从绝对原点生长,位置和预期不符。

如果需要跟随WCS,常见做法是先读取工作坐标系的矩阵,再把corner坐标变换到绝对坐标系。对简单Block场景,我一般直接按绝对坐标规划位置,避免坐标变换带来的麻烦。

corner变量名在示例里写成了coner,这应该是corner的拼写笔误。复制代码时注意修正,否则别人阅读时会产生困惑。

4.2 edges参数是边长表达式,不是“边ID”

示例摘要里提到“边缘ID都是100”,这是一个明显的误读。UF_MODL_create_block1的第三个参数是指向三个边长字符串的数组,不是边的标识符,更不是边tag。每个字符串代表一个方向上的尺寸表达式。

理解成“边缘ID”会带来两个问题:一是没法解释为什么三个都是"100";二是会误以为需要先创建边再引用边。实际上edges是纯数值或表达式输入,和边没有任何关系。

边长字符串可以使用数字、算术表达式、表达式名三种形式:

  • "100"表示固定尺寸。
  • "2*50"等价于100。
  • "L"引用已存在的表达式L,Block尺寸随L变化。

其中第三种写法最有用。比如先通过UF_MODL_edit_exp创建名为L的表达式,然后edges里填{"L","L","L"},之后只需要改一个参数,整个块就变了。使用表达式驱动是NX参数化的核心思路。

4.3 UF_NULLSIGN:特征创建时的“无布尔”约定

sign参数在UF_MODL函数里是特征标识,用来决定特征与已有几何的关系。UF_NULLSIGN通常代表不执行布尔操作,直接生成一个独立特征。在只有一个Block的新零件里,没有可操作的目标体,用UF_NULLSIGN是最合理的。

如果零件里已有一个实体,想在这个Block基础上做布尔求和或求差,则需要使用其他sign枚举值,并且还要传入目标体tag。这个场景超出本文默认示例范围,等需要处理复杂装配或加工模型时再展开。

4.4 返回tag是特征tag,不是体tag

UF_MODL_create_block1返回的blockTag是特征tag。特征tag被用于修饰、删除、查询特征参数,但不能直接用于体查询接口。要拿到底层实体,需要转换:

tag_t bodyTag = NULL_TAG; UF_MODL_ask_feat_body(blockTag, &bodyTag); if (bodyTag == NULL_TAG) { cout << "No body found for this feature" << endl; } else { cout << "Body tag: " << bodyTag << endl; }

UF_MODL_ask_feat_body的作用是“给定特征tag,找到它生成或所属的体tag”。需要测体积、做布尔、导出时,后面用的都是bodyTag。如果没有这一步,直接用blockTag调用体相关API通常会得到NULL_TAG或错误码。

4.5 容器保存tag:从单块扩展到多块

当程序从创建单块扩展为“创建多个块再统一保存”时,blockTag会不断被新特征覆盖。如果后续还需要回头修改或查询每个Block,应该在每次创建后立刻存到STL容器里。

std::vector<tag_t> blockTags; for (int i = 0; i < 5; ++i) { tag_t oneBlock = NULL_TAG; double corner[3] = {i * 120.0, 0.0, 0.0}; const char *edges[] = {"100", "100", "100"}; UF_CALL(UF_MODL_create_block1(UF_NULLSIGN, corner, edges, &oneBlock)); blockTags.push_back(oneBlock); }

这段循环代码里,每个Block的起点沿X方向按120mm递增,保证它们不会重叠。保存tag到容器后,即使循环变量覆盖了oneBlock,vector里仍然保留每个Block的特征句柄。

5. 外部模式常见问题与避坑:四个高频翻车现场与排查姿势

5.1 先学会看错误输出:syslog与UF_get_fail_message

外部模式没有图形界面的“信息窗口”可看,所有UF错误默认通过UF_print_syslog打印。在控制台程序里,syslog信息会出现在标准输出或系统日志中。看到*** ERROR code xxx at line yyy in zzz.cpp,就说明某个UF_CALL返回非0了。

此时把错误码记录下来,再调用UF_get_fail_message就能得到文本描述:

char err[133]; UF_get_fail_message(irc, err); printf("Error text: %s\n", err);

排查顺序我一般从UF_initialize开始检查,然后是UF_PART_new,最后才是建模函数。如果UF_initialize就失败,后面所有调用都不会成功,不要去怀疑建模参数。

5.2 高频踩坑清单

  1. 编译就报错:main函数签名不对 现象:把示例代码直接粘进新工程,编译报“main函数必须返回int”或“参数类型不匹配”。 原因:示例代码是老式extern void main(char argc, char *argv[]),C++标准不允许void main,且argc类型应为int。 解决:改为int main(int argc, char *argv[]),并把return补上。这个改动不影响UF调用,只影响编译入口。

  2. UF_initialize返回非0,程序直接退出 现象:控制台显示错误码,程序还没进入建模逻辑就return。 原因:外部程序找不到NX安装环境;或者许可证没有启动;也可能是调用了UF_initialize时NX正在运行导致连接冲突。 解决:先确认UGII_BASE_DIR和PATH环境变量,再确认NX许可证服务正常。外部模式程序最好使用与当前NX匹配的API版本,跨版本运行不一定兼容。

  3. UF_PART_new报错,找不到或无法创建文件 现象:错误信息里提到文件路径或IO失败。 原因:最常见是路径目录不存在,比如直接写C:\temp但C盘下根本没有temp目录。 解决:在调用UF_PART_new前先用系统API创建目录,比如CreateDirectory("C:\\temp", NULL)或std::filesystem::create_directories。另外确认路径可写、文件名以.prt结尾。

  4. 程序跑完,磁盘上找不到生成的文件 现象:控制台显示Step(3)成功,但到prt所在目录查看文件不存在,或者文件是0字节。 原因:UF_PART_save的返回值没有被检查,可能根本没有保存成功;或者UF_PART_close用了mode=0且前面没有save。 解决:用UF_CALL包装UF_PART_save,并确认返回0。close时统一用UF_PART_close(newPart, 1, 1),保底保存。再不行就到目标目录看文件时间戳,确认是本轮运行生成。

  5. blockTag拿到后无法测量或导出 现象:调用体相关API时传blockTag,得到NULL_TAG或错误码。 原因:blockTag是特征tag,不是体tag。 解决:先调用UF_MODL_ask_feat_body(blockTag, &bodyTag),拿到体tag再继续测量、导出或布尔操作。

5.3 外部模式特有的资源释放习惯

外部模式程序如果中途return,最好统一走到UF_terminate再退出。我见过很多代码在错误分支里直接return -1,导致UF内核连接没有被释放。虽然进程结束系统会回收资源,但长期运行的批处理程序多次泄漏会造成许可证句柄堆积,最终无法连接NX。

建议把主逻辑放到初始化成功之后,所有return分支在退出前都调用UF_terminate。示例用UF_CALL包裹UF_terminate,失败也能看到日志,虽然正常情况下terminate不会失败。

这个习惯也适用于UF_PART_close:关闭零件失败不能忽略。如果一直开着不关,批处理循环里第二次UF_PART_new可能会因为部分对象占用而失败。

6. 进阶用法:批量创建多个Block并验证结果

最后分享一个日常工作常用的扩展。用循环创建5个Block,每个沿X方向间隔120mm,然后保存关闭:

#include <vector> std::vector<tag_t> blockTags; for (int i = 0; i < 5; ++i) { tag_t blockTag = NULL_TAG; // 每个Block的起点沿X方向错开120mm double corner[3] = {i * 120.0, 0.0, 0.0}; const char *edges[] = {"100", "100", "100"}; UF_CALL(UF_MODL_create_block1(UF_NULLSIGN, corner, edges, &blockTag)); blockTags.push_back(blockTag); }

批量生成后需要验证。我通常做两件事。第一件,用UF_MODL_ask_feat_body检查每个blockTag是否都能找到体:

for (auto tag : blockTags) { tag_t bodyTag = NULL_TAG; UF_MODL_ask_feat_body(tag, &bodyTag); if (bodyTag == NULL_TAG) { cerr << "Feature tag " << tag << " has no body" << endl; } }

第二件,内存里验证全部通过后,再UF_PART_save并UF_PART_close。保存后去prt目录检查文件大小和数量,确认不是空文件。这个“save->验证->close”的顺序我已经形成了习惯。

最后讲一个真实教训:有一次为了省事,把UF_PART_save放到循环外面,结果循环中途某个Block创建失败,错误分支直接return,save从来没执行,一上午生成的十几个零件全部没有落盘。从那以后,我每次关零件之前都强制走一遍“检查每个特征tag->save并确认返回0->close并确认返回0”的流程,再也没有出现过空文件。希望帮到你。

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

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

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

立即咨询