做NX二次开发的朋友,十有八九会遇到类似需求:把一个装配里每个实体零件的重量算出来,或者把某几个关键件的质量属性导出到报表。这个需求看起来简单,真上手写代码时,却发现一堆细节——显示部件和工作部件哪个才是当前要统计的?实体里混着片体怎么办?密度从哪来?算出来的体积、重量单位又是什么?今天这个NX二次开发python样例,专门解决“获取当前显示部件中的每个实体的重量(质量)信息”这个高频问题。我把它从原理到代码再到踩坑记录完整拆给你,希望能帮你少走弯路。
1. 功能拆解:先搞懂“显示部件里的实体重量”到底在算什么
很多人拿到这个需求的第一反应是“遍历实体,拿质量属性,完事”。真写起来就会发现,第一步“遍历哪个部件”就已经能栽跟头。所以先把三个核心概念理清楚,后面代码才不会写拧巴。
1.1 显示部件和工作部件:别再傻傻分不清
NX在打开多个模型时,界面上同时存在两个角色:显示部件(Display Part)和工作部件(Work Part)。显示部件就是当前在图形窗口里显示的那个部件,用户眼睛看到的、鼠标能点到的都是它。工作部件则是当前处于“可编辑状态”的部件,建模命令操作的目标是它。
大多数情况下两者是同一个,但当你打开一个装配、然后在装配导航器里双击某个子零件时,工作部件就会变成那个子零件,而显示部件可能仍然是整个装配。这时候如果你只取工作部件,统计到的实体就和用户“眼睛看到的”对不上;如果只取显示部件,又要小心装配中的引用集、隐藏组件等干扰。
我们这个样例的需求关键词是“当前显示部件”,所以代码里取的是Session.Parts.Display,这也是最符合用户直觉的做法——屏幕上显示什么,就统计什么。如果你确实需要按工作部件统计,代码几乎不用改,换一行属性即可。
1.2 哪些实体需要统计:实体对象遍历规则
NX里Part.Bodies()返回的是这个部件下的所有Body对象,但Body不全是实体。片体(Sheet Body)、小平面体(Faceted Body)、多面体等也都在Bodies集合里,它们的“质量”概念和实体完全不同。一个封闭的片体虽然也能算出表面积,但谈重量就没有意义。
所以遍历时不能无脑全收,必须用IsSolid属性过滤掉非实体对象。这里有一个容易踩的坑:有些Body在创建时是封闭的,但中间被操作改成了开放状态,IsSolid判断依然可能为True,但UF计算质量属性时会报错或返回异常数据。稳妥的做法是在调用质量计算前,再用UF函数做一次类型确认,或者至少把异常状态捕捉住,不要让单个坏体毁掉整个脚本。
另外还要注意“抑制(Suppressed)”状态。装配里被抑制的组件,其体对象可能在Bodies集合里仍然存在,但物理上已经被移出模型空间,计算它的质量没有意义。我一般会在遍历时加一个条件,跳过IsSuppressed的对象。
1.3 质量信息不止是重量:体积、质心、密度一个都不能少
标题里写的是“重量(质量)信息”,但实际项目里,拿到实体的质量只是开始。你要做配重分析,就需要质心位置;要做装箱估算,需要体积;要验证材料设置,需要密度数据。
UF_MODL_ask_mass_props_3d这个函数返回的数据里,质量、体积、表面积、质心坐标、主惯性矩全都有。我在这个样例里把常用几个值都打印出来,并且在导出的CSV里留了扩展列。这样无论你是做BOM汇总还是做后续分析,都不用再二次开发一个工具。
顺便说一个容易混淆的点:NX里“质量”和“重量”在数值上经常被当一回事,但严格说,质量是物体的固有属性,重量是质量乘以重力加速度。NX默认的材料密度和质量属性设置中,返回的是质量,单位一般是克(g),而不是牛顿。如果你要的是“重量值”,要么自己乘9.8,要么在单位设置里切换结果单位。
2. 环境准备与实现思路
代码本身不复杂,但运行环境没配好,再完美的脚本也跑不起来。这一节把NXOpen Python的开发方式、脚本运行逻辑和API选型讲清楚。
2.1 NXOpen Python环境这么配
NX从很早的版本开始就支持Python日记功能。你不需要额外安装Python解释器,NX安装时已经内置了Python运行环境,并绑定好了NXOpen相关模块。常见入口有两种:
一是NX界面里的“工具→日记→新建→Python”,会自动打开内置的日记编辑器,在里面写代码,然后通过“工具→日记→回放”直接运行。
二是把脚本保存成.py文件,然后在“工具→日记→回放”里选择该文件执行。这种方式适合你已经用VS Code、PyCharm等外部编辑器写好了代码,再丢进NX里验证。
如果你非要在外部IDE里调试,也不是不行,但需要手动把NX安装目录下的Python模块路径加到环境变量里。以我的经验,这样折腾的效率并不高,调试NX脚本最顺手的方式还是“内联运行+print输出”。NXOpen在运行时会自动把当前会话、当前部件这些上下文绑定好,你不需要自己初始化COM连接。
2.2 一个脚本的完整逻辑链路
整体流程可以用一句话概括:取到当前显示部件,遍历它下面的所有实体体,逐个调用质量属性函数,把结果打印或导出。具体到代码层面,顺序是这样的:
- 获取
NXOpen.Session实例,这是所有NXOpen二次开发的总入口。 - 通过
Session.Parts.Display拿到当前显示部件。 - 遍历
Part.Bodies(),用IsSolid过滤出实体体。 - 对每个实体体,构造UF计算请求,调用
UF_MODL_ask_mass_props_3d。 - 解析返回数组中的体积、面积、质量、质心等值。
- 打印到NX日志窗口,同时写入CSV文件。
这套逻辑可以复用到很多衍生需求,比如把“当前显示部件”换成“选中组件”,或者把“实体体”换成“片体集合”,后面第6节会讲。
2.3 为什么样例里我会优先用UF函数而不是纯NXOpen对象
NXOpen的API体系很庞大,理论上质量属性也可以用MeasureManager这类更加“面向对象”的接口来做。但我在实际项目里踩过不少版本坑:NX 1899、NX 2206、NX 2306这几个大版本之间,测量相关的类名、方法签名甚至返回值结构都有调整。你今天写好的代码,过两年升级NX版本后可能直接编译不过。
而UF函数(User Function)是NX底层更稳定的一套接口,从NX老版本一直延续到现在,UF_MODL_ask_mass_props_3d就是其中之一。它的参数风格比较“C语言化”,看起来不够优雅,但胜在稳定、跨版本兼容性好。对质量属性这种几十年没变过的计算逻辑,用UF函数是最省心的选择。
| 对比维度 | UF函数 | NXOpen对象式API |
|---|---|---|
| 跨版本稳定性 | 高,老接口稳定 | 低,版本迭代频繁调整 |
| 代码可读性 | 一般,大量数组下标 | 较好,属性语义清晰 |
| 二次开发扩展性 | 偏底层,适合计算类功能 | 面向前端交互,适合UI集成 |
| 推荐场景 | 批量计算、后台处理、版本兼容优先 | 交互式命令、带对话框的工具 |
不是说NXOpen对象式API不好,而是“求质量属性”这个动作本身就是纯计算,跟UI交互无关,用UF函数可以把不确定性降到最低。等你的工具要加对话框、要支持用户交互选择时,再结合NXOpen的UI模块不迟。
3. 样例代码实现:逐段拆解
下面从第一行代码开始拆,每个关键段都讲讲为什么这么写、改成别的会有什么后果。如果你急着要完整代码,可以直接跳到第4节,但我建议你还是看一眼拆解,因为很多坑藏在这些细节里。
3.1 获取当前显示部件:Session到底怎么拿
import NXOpen import NXOpen.UF theSession = NXOpen.Session.GetSession() displayPart = theSession.Parts.Display if displayPart is None: raise RuntimeError("当前没有打开任何显示部件,请先打开模型文件")Session.GetSession()是所有NXOpen脚本的固定起手式,它拿到的是当前NX进程的全局会话对象。Parts.Display属性返回当前显示部件,类型是NXOpen.Part。注意,它不是工作部件!我见过不少同事把这里写成Parts.Work,然后在装配环境下对不上数据,排查半天才发现取错对象。
这里还做了一个空判断。如果NX里一个文件都没打开,Display会返回None,后面的遍历直接抛异常,加上这个判断后脚本会给出明确提示,而不是报一堆底层错误。
3.2 遍历实体:过滤实体体,避免把片体也算进去
bodies = [] for body in displayPart.Bodies(): if body.IsSolid: if not hasattr(body, "IsSuppressed") or not body.IsSuppressed: bodies.append(body)displayPart.Bodies()返回一个集合,遍历它即可拿到该部件下的所有Body。我在这里做了两层过滤:第一层用IsSolid排除片体,第二层用IsSuppressed排除被抑制的体。
关于IsSuppressed,不同版本的NX在Body对象上的属性可能略有差异,所以我用hasattr做了个保护,万一某个版本没有这个属性,也不至于脚本崩溃。如果你确定自己的NX版本一定支持,可以直接写if not body.IsSuppressed。
另一个可选的过滤条件是“是否可见”。如果显示部件里有某个实体体被隐藏了,但它仍属于这个部件,严格说它还在模型里,质量仍然存在。所以要不要过滤可见性,取决于你的业务口径。统计“整套模型重量”时,隐藏件也必须算;统计“当前屏幕上可视重量”时,才需要额外判断可见性。
3.3 调用UF_MODL_ask_mass_props_3d计算质量属性
这是整个脚本的核心,也是网上资料最少、最容易写错的地方。
ufSession = NXOpen.UF.UFSession.GetUFSession() # UF_MODL_ask_mass_props_3d的输入输出 bodyTag = body.Tag density = 7.85 # g/cm^3,钢的常见密度 accuracy = 1 # 精度级别,1-3,越高越慢但越准 massPropsType = 0 # 0表示实体属性计算 massProps = [0.0] * 47 errors = [0.0] * 6 status = ufSession.Modl.AskMassProps3d( [bodyTag], # 实体Tag列表 1, # 对象数量 density, # 密度 accuracy, # 精度 massPropsType, # 类型 massProps, # 返回值数组 errors # 误差数组 )先解释参数,AskMassProps3d这名字里的“3d”表示三维实体,不是某个固定写法。第一个参数传入实体Tag的列表,因为UF函数底层支持一次传入多个体批量计算,这里我每次只传一个,因为我们要逐个输出每个实体的独立信息。density是密度值,单位要和模型单位制匹配,这个很重要,我单独在3.4节说。accuracy精度值一般取1或2,模型复杂度高时再考虑3,越高计算越慢。
massPropsType填0表示按实体属性计算。这个常量在不同UF头文件里的名称可能不同,但数值0对应的就是实体类型。如果你算的是片体,这里要换成对应类型。我在代码里直接注释了“0表示实体属性计算”,防止过几个月自己都忘了。
massProps数组是输出区,长度我先给了47个元素。不同NX版本的UF文档对这个数组长度的定义不完全一致,有些资料写的是至少20个,有些写47个。如果你执行时报数组越界,直接调大这个长度,比如改成64,不影响结果解析。
函数执行完后,massProps里存的就是计算结果,关键下标如下:
| 数组下标 | 含义 | 常规单位(公制毫米模板) |
|---|---|---|
| 0 | 体积 | mm³ |
| 1 | 表面积 | mm² |
| 2 | 质量 | g |
| 3 | 质心X坐标 | mm |
| 4 | 质心Y坐标 | mm |
| 5 | 质心Z坐标 | mm |
| 6 | 主惯性矩Ixx | g·mm² |
| 7 | 主惯性矩Iyy | g·mm² |
| 8 | 主惯性矩Izz | g·mm² |
我特意把单位和下标对照写出来,是因为这里出错的概率极高。很多人取完volume和mass后,看到数值大得离谱或小得离谱,十有八九就是单位制没对上。
3.4 密度参数和单位体系处理
密度是质量计算的关键输入。NX里的单位制可以设置成公制或英制,公制下还有“毫米-克-秒”和“米-千克-秒”的差异。同一个实体,你在不同单位制下调用UF函数算质量,传入的密度值必须跟着单位制走。
默认的公制模板一般是毫米和克,对应密度单位就是g/cm³。钢的密度约7.85,铝约2.7,塑料约1.0左右。如果你用的是英制模板,密度单位可能是lb/in³,那7.85这个值就完全不对了。
我在脚本里把密度写成了函数参数,并给了默认值7.85,方便直接跑通。实际项目中,更好的做法是从NX的材料属性里读取密度,而不是在代码里写死。比如通过NXOpen.Material相关API拿到实体的材料,再取材料密度。不过那是另一个话题,需要结合你公司的材料库建设情况,这里先不展开。
还有一个细节:UF函数文档里说,如果传入的密度为0,函数可能返回全0的质量结果,不报错。所以当你看到“质量为0”时,第一反应不一定是代码bug,先查密度参数是否真的传进去了。
3.5 结果输出:控制台打印和写入CSV
拿到数据后,输出方式决定了这个工具好不好用。我提供了两种输出:一是打印到NX日志窗口,适合快速验证;二是写入CSV文件,适合导出给其他同事或Excel处理。
import csv import tempfile import os resultLine = "Body:%s, Volume:%.3f, Area:%.3f, Mass:%.3f, Centroid:(%.3f, %.3f, %.3f)" % ( body.JournalIdentifier, massProps[0], massProps[1], massProps[2], massProps[3], massProps[4], massProps[5]) print(resultLine) csvRow = [ body.JournalIdentifier, round(massProps[0], 3), round(massProps[1], 3), round(massProps[2], 3), round(massProps[3], 3), round(massProps[4], 3), round(massProps[5], 3), ] rows.append(csvRow)这里用body.JournalIdentifier做标识,它通常是NX内部给这个体生成的唯一标识名,类似“BLOCK(1)”这样的字符串。虽然不一定对应建模树里的用户自定义名称,但作为唯一键已经足够。
CSV文件写入到系统临时目录,避免污染NX的工程目录。写完后用os.path.join(tempfile.gettempdir(), "mass_report.csv")拼出完整路径,打印出来,这样用户能直接看到文件在哪。实际生产环境里,可能还要支持用户自己选择保存路径,这属于UI优化范畴,后面扩展章节会提。
4. 完整可运行代码:直接复制到NX里测试
这一节给出完整代码。你把它保存成.py文件,在NX里通过“工具→日记→回放”运行即可。运行前建议先保存一下当前模型,万一有问题也不至于影响数据。
4.1 完整脚本
import NXOpen import NXOpen.UF import csv import os import tempfile def main(): theSession = NXOpen.Session.GetSession() displayPart = theSession.Parts.Display if displayPart is None: raise RuntimeError("当前没有打开任何显示部件,请先打开模型文件") ufSession = NXOpen.UF.UFSession.GetUFSession() # 输入参数 density = 7.85 # g/cm^3,默认按钢材密度,可根据模型实际材料修改 accuracy = 1 # 计算精度 1-3 massPropsType = 0 # 0=实体属性 rows = [] header = ["BodyName", "Volume_mm3", "Area_mm2", "Mass_g", "CentroidX", "CentroidY", "CentroidZ"] bodies = [] for body in displayPart.Bodies(): if body.IsSolid: if not hasattr(body, "IsSuppressed") or not body.IsSuppressed: bodies.append(body) print("共找到 %d 个实体体" % len(bodies)) for body in bodies: massProps = [0.0] * 47 errors = [0.0] * 6 try: status = ufSession.Modl.AskMassProps3d( [body.Tag], 1, density, accuracy, massPropsType, massProps, errors ) if status != 0: print("计算失败,Body: %s,状态码: %s" % (body.JournalIdentifier, status)) continue except Exception as e: print("计算异常,Body: %s,错误: %s" % (body.JournalIdentifier, str(e))) continue resultLine = "Body:%s, Volume:%.3f, Area:%.3f, Mass:%.3f, Centroid:(%.3f, %.3f, %.3f)" % ( body.JournalIdentifier, massProps[0], massProps[1], massProps[2], massProps[3], massProps[4], massProps[5]) print(resultLine) csvRow = [ body.JournalIdentifier, round(massProps[0], 3), round(massProps[1], 3), round(massProps[2], 3), round(massProps[3], 3), round(massProps[4], 3), round(massProps[5], 3), ] rows.append(csvRow) if rows: outPath = os.path.join(tempfile.gettempdir(), "mass_report.csv") with open(outPath, "w", newline="", encoding="utf-8") as f: writer = csv.writer(f) writer.writerow(header) writer.writerows(rows) print("CSV文件已写入: %s" % outPath) else: print("没有可导出的实体数据") if __name__ == "__main__": main()这段代码我已经去掉了复杂的界面逻辑,只保留核心计算和输出,方便你理解和移植。如果你要在自己的环境里跑,核心要改的就是density这个值,以及确认你的NX版本支持Bodies()和IsSolid这些属性。
4.2 运行步骤说明
- 打开NX,加载你要统计的模型,确保它在图形窗口中处于显示状态。
- 点击“工具→日记→回放”,在弹出的文件选择框里选中刚才保存的
.py文件。 - 脚本执行后,NX日志窗口会逐行打印每个实体的体积、面积、质量和质心。
- 打开系统临时目录,找到
mass_report.csv,就是完整的导出表。
第一遍跑的时候,建议先拿一个只有两三个实体的简单模型试,确认结果和手工用“分析→测量体”查到的数值一致后,再放到复杂装配上跑。这样能快速发现密度、单位等参数是否设对。
5. 踩坑记录:我在实际测试中遇到的那些问题
这部分内容是常规文档里不会写的东西。我把这几年实际操作中遇到的问题整理成速查表,再挑几个典型的展开讲。
5.1 获取到的质量全是0是怎么回事
最常见的质量全为0的原因,是密度参数传入了0或非法值。很多人从默认模板里复制代码,忘记改密度,结果算出来全是0。但这不算最隐蔽的,更隐蔽的是:你传入了密度,但NX当前模型用的是英制单位制,你传的7.85在英制体系下根本不匹配,即便函数不报错,结果也没有意义。
还有一种情况:实体本身是“小平面体”或“缝合体”,在NX中虽然能被IsSolid识别为实体,但UF函数对它计算质量时可能返回空数据。遇到这种体,建议先用网格诊断或“优化体”功能处理一下,再用脚本计算。
5.2 密度数据不准确,导致重量偏差很大
很多工程师在自己的模型里并不会主动设置材料属性,导致这个实体其实没有真正的密度数据。脚本里如果写死一个默认密度,算出来的重量自然和实际情况不符。
我的建议是,在正式使用前,先统计一下你的模型库里有多少实体是设置了材料的。如果比例很低,那就得考虑在脚本里增加一个“根据自定义属性读密度”的逻辑,比如读取实体的“Material”属性或“Density”属性,读不到再用默认值兜底。这样至少不会算出一个明显离谱的数。
5.3 实体数量很多时,脚本执行特别慢
一个复杂装配里可能有上千个实体,逐个调用UF函数计算质量属性,时间会线性增长。我实际测过,一个500个实体的小总成,跑完整轮大约要几十秒,用户体验确实一般。
如果想提速,可以考虑两个方向:一是把多个实体一次性传给AskMassProps3d,让它一次计算多个实体,但这样返回的是一组汇总值,拿不到每个实体的独立质量,不符合我们“逐个输出”的需求;二是加上进度提示,让用户知道脚本还在跑,而不是卡死了。NXOpen里可以用NXOpen.UF.UFUi或状态栏更新当前进度,代码量不大但体验提升很明显。
5.4 不同NX版本的API差异:别指望一次写好
这是NX二次开发最让人头大的地方。我手上的项目横跨NX 1899和NX 2306两个版本,同样的脚本在NX 1899上正常,到NX 2306上AskMassProps3d的入参顺序竟然有小变化。最稳妥的方法是:在你自己的NX版本里,手动录制一次“分析→测量体”操作,看看生成的日记代码长什么样,然后把其中的API调用格式作为基准。
我的习惯是,在每个NX版本环境里跑一次“录制日记”,把生成的代码留存一份,作为该版本的API基准。这样不管是升级NX版本,还是换到同事的电脑上,都能快速定位差异点。这个小习惯帮我省了很多排查时间。
5.5 无法打开部件或采集不到实体
有时候脚本会报“没有显示部件”,但用户明明打开了模型。这种情况多半是NX正处于某个命令的未完成状态,比如正在草图中编辑,此时Parts.Display可能返回None或异常。对策是在脚本开头明确提示用户退出当前命令再运行。
采集不到实体的情况,多数发生在“装配环境下的显示部件是装配体”时。装配体本身没有实体,实体都在子组件里。如果你想统计整个装配,就需要递归遍历每个组件里的体对象,而不是只用displayPart.Bodies()。
5.6 常见问题速查表
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 质量为0 | 密度没传或传成0 | 检查密度参数 |
| 数值量级不对 | 单位制与密度单位不匹配 | 统一为公制毫米模板 |
| 找不到实体 | 当前是装配体,实体在子组件里 | 递归遍历组件 |
| 脚本报API不存在 | NX版本API差异 | 录制自己的日记做基准 |
| 执行很慢 | 实体数量多、逐体计算 | 加进度提示或批量处理 |
| 返回结果部分为0 | 个别体损坏或为小平面体 | 诊断修复该实体后重算 |
6. 后续还能怎么玩:从“取重量”到“自动出报表”
脚本跑通之后,你会发现这套逻辑的复用价值非常高,稍微改改就能变成不同工具。下面这几个方向我都试过,按实施成本从低到高列出来。
6.1 一键导出整套模型的BOM重量清单
这个是最直接的扩展。把CSV导出逻辑改成写Excel,或者在CSV里增加“序号”“零件名”“材料”等列,就能直接当BOM清单用。如果公司里已经有BOM模板,把输出列和模板对齐,导出后就能直接导入ERP或PLM系统。
我做过一个版本,输出时直接用csv模块写多级表头,然后在NX里给工具加了一个菜单项,用户点一下按钮就自动生成重量报表,再也不用一个个实体截图汇报了。
6.2 按实体名称、属性筛选统计范围
有些场景不需要统计所有实体,比如只想统计“外购件”,或者只想统计“需要发运单独包装的零件”。这时可以在遍历时加过滤条件,比如读取实体的用户属性或自定义属性,根据属性值决定是否纳入统计。
NX里读取属性可以用NXOpen.NXObject.GetUserAttribute或UF的UF_UGM_ask_user_attribute。虽然代码比单纯遍历多几行,但效果很好,相当于给统计工具加了一个“筛选器”。
6.3 批量修改材料密度后自动更新重量
如果你的模型库材料设置不完善,可以写一个预处理脚本:批量把某个装配下所有实体的密度属性统一设置成指定材料密度,然后再跑重量统计。我通常会把“设置密度”和“计算重量”分成两个独立工具,因为业务上这两件事经常发生在不同时间点。
一个实际案例:公司从钣金件切换到铸件,材料密度全变了。我用脚本批量把所有实体的密度换成铸铝密度,然后重新统计总重量,5分钟搞定,而手工修改至少得半天。
6.4 在装配体中遍历组件实体
回到5.5说的装配体场景。如果你想统计整个装配的总重量和每个子件的重量,就需要递归遍历所有组件:
- 获取装配的所有组件
displayPart.Components。 - 对每个组件,获取其引用的部件
component.Prototype。 - 在该部件上执行和前文一样的实体遍历与质量计算。
- 把组件路径和实体质量合并输出。
递归逻辑不复杂,但是要注意重复引用的问题。同一个子零件可能在装配中出现了多次,直接遍历会把它的重量重复计算。是否要去重,取决于你要的是“所有实例的重量总和”还是“每种零件的单件重量”。这两个业务口径在BOM场景里都很常见,脚本逻辑要区分清楚。
最后再分享一点个人体会
写这个工具时我最大的感悟是:NX二次开发里,代码从来不是最难的,难的是把NX的模型概念、单位体系、业务口径都对齐。你在做“获取实体重量”之前,最好先亲手用“分析→测量体”查一下目标模型,看看NX默认给的质量、体积和单位是什么。这样脚本跑出来,你一眼就能判断结果对不对,而不是陷入“数值看起来挺像,但不知道到底准不准”的迷局。
如果你在实际使用中遇到其他坑,比如某个版本的UF函数参数对不上、某个特殊类型的体算不出质量,欢迎在评论区把现象和版本发出来。这类小众开发问题,一个人踩坑苦恼半天,大家一句话可能就点破了。