做 SolidWorks 二次开发的朋友,十有八九都会遇到自定义属性(Custom Properties)的读写需求。这个需求在 PDM 集成、BOM 导出、模型自动校验、工程图模板映射这些场景里几乎天天出现,而CustomPropertyManager就是官方提供的关键 API 之一。今天要聊的GetType2方法,专门用来在读取属性值的同时拿到属性的类型信息,很多朋友在批量处理模型属性时遇到“日期变成数字”、“是/否被当成字符串”这类问题,根源就是没有正确区分属性类型。这篇文章就把这个 API 从签名到实战、从单个文档到批量处理的完整玩法拆开讲一遍,适合正在用 C# 写 SolidWorks 插件或者脚本的开发者参考。
1. 先搞清楚为什么要读属性类型
1.1 自定义属性就是三维模型的“户口本”
SolidWorks 里的自定义属性,本质上是一组挂在模型文件上的键值对,存在于“文件 → 属性 → 自定义”这个对话框里。你可以把它当成模型的“户口本”,记录编号、名称、材料、重量、设计者、日期、审核状态这些元数据。工程图标题栏的填写、BOM 表的自动生成、PDM 系统的字段映射,底层数据源基本都来自这些属性。
对开发者来说,这个“户口本”是二次开发中最稳定的数据入口。模型几何形状千变万化,但属性结构是可控的。很多企业会规定一套属性模板,比如“图号”“名称”“材料”“重量”“版本”“审批人”,然后要求所有模型都必须填齐。二次开发里最常见的需求就是:批量扫描一批零件,把属性读出来,检查漏填项,或者汇总到 Excel 里。这时候你面对的每一个字段,不一定都是纯文本。
1.2 类型信息在自动化流程里的角色
属性值看起来都是字符串,但 SolidWorks 在设计时给属性定义了类型。创建属性时,界面里可以选择文本、数字、日期、是/否。这就像一个表单里既有普通输入框、下拉框、日期选择器,也有复选框,你不能用处理文本的方式一视同仁。
如果没有类型信息,你会遇到几个很实际的问题。第一,数字属性取出后在 C# 里本质是double,但如果你统一走ToString()再拼接或存库,就会受到系统区域设置影响,出现小数点变逗号这种问题。第二,日期属性在底层是日期类型,直接ToString()得到的是带时分秒的格式,跟 Excel 里的日期格式对不上。第三,是/否属性读出后的值在不同版本里可能是true/false或“是/否”/“Y/N”,不做类型判断去转换就会出错。GetType2就是官方提供的“一眼看清属性到底属于哪种类型”的方法,拿到类型之后,你再决定怎么转换、怎么校验、怎么输出,整个流程才谈得上可靠。
2. 认识 CustomPropertyManager 和 GetType2
2.1 属性管理器怎么拿到手
在 SolidWorks API 里,CustomPropertyManager并不是一个你 new 出来的对象,而是通过文档对象获取的。不同文档(零件、装配体、工程图)都有自己的属性管理器,同一个文档里不同配置也有各自的属性管理器。
// 连接 SolidWorks 应用 SldWorks.sldworks swApp = (SldWorks.sldworks)Marshal.GetActiveObject("SldWorks.Application"); // 取当前激活文档 SldWorks.ModelDoc2 swDoc = (SldWorks.ModelDoc2)swApp.ActiveDoc; // 获取模型特定(Model Specific)属性的管理器,配置名传空字符串 SldWorks.ICustomPropertyManager propMgr = swDoc.Extension.CustomPropertyManager[""]; if (propMgr == null) { // 处理异常:部分文档类型可能拿不到属性管理器 }注意这里CustomPropertyManager后面跟了一个中括号,它本质上是一个带字符串索引的属性,""代表模型级属性。如果你传入具体的配置名称,比如"默认",拿到的就是该配置特有的属性集合。工程图、装配体、零件的行为一致,这一点极大方便了批量脚本的编写。
2.2 GetType2 的签名与参数解析
GetType2的官方签名如下:
bool GetType2(string Name, out object Value, out swCustomPropertyType_e Type);Name:属性名,即你要查询的那个键。Value:输出参数,属性当前值,以object形式返回。如果属性不存在,这个值可能是null。Type:输出参数,返回属性的类型,类型是枚举swCustomPropertyType_e。
返回值是bool,表示这次查询是否成功。属性存在时通常返回true,属性不存在或发生异常时返回false。实战中我建议把返回值当作“属性是否查得到”的第一判断条件,不要只根据Value是否为null来判断,因为一个合法的空字符串属性,Value也可能是空。
这里有必要提一个容易被忽略的细节:SolidWorks 还有另一个方法GetType,签名是bool GetType(string Name, out swCustomPropertyType_e Type)。它只能输出类型,不能输出值。所以如果你既要值又要类型,就得调用两次方法。GetType2相当于把这两个操作合并成一次 COM 调用,减少跨进程消耗,在高频遍历大批模型时,性能差别是肉眼可见的。最简单的经验就是:能在一次 API 调用里解决的事,别拆成两次。
2.3 swCustomPropertyType_e 到底有哪些值
swCustomPropertyType_e是官方定义的自定义属性类型枚举,常见取值如下:
| 枚举名称 | 业务含义 | 底层 C# 中的常见形态 |
|---|---|---|
swCustomPropertyTypeUnknown | 未知类型,无法识别或表达式未解析 | 可能是字符串或 null |
swCustomPropertyTypeString | 文本类型,最常用的默认类型 | string |
swCustomPropertyTypeNumber | 数字类型 | double |
swCustomPropertyTypeDate | 日期类型 | DateTime |
swCustomPropertyTypeYesNo | 是/否类型 | bool |
不同 SolidWorks 版本对枚举值的定义可能略有扩展,但绝大多数场景就是这五种。编写代码时不要硬编码数值,永远用枚举名做判断,否则升级版本后很容易踩坑。
3. C# 实操:获取属性类型的完整示例
3.1 创建测试项目与引用准备
在 Visual Studio 里我建议创建一个控制台应用,目标框架根据你的 SolidWorks 版本选择。传统做法是.NET Framework 4.8加SolidWorks.Interop.sldworks的 COM 引用,新版也可以用.NET 6/8配合 NuGet 上的互操作包,但考虑到很多企业环境还是老版本,我下面的示例按 .NET Framework 风格写,这样兼容性最好。
添加引用后,在代码文件顶部引入命名空间:
using SolidWorks.Interop.sldworks; using System; using System.Collections.Generic; using System.Runtime.InteropServices;需要说明的是,SldWorks.sldworks这个接口类型在 COM 互操作里很常用,如果你用的是SldWorks.dll里的互操作类型,ModelDoc2和ICustomPropertyManager都在同一个命名空间里。
3.2 遍历所有属性并输出名称、值、类型
一个完整的读取函数是这样写的:
public static void DumpAllCustomProperties(ModelDoc2 doc) { if (doc == null) return; ICustomPropertyManager propMgr = doc.Extension.CustomPropertyManager[""]; if (propMgr == null) { Console.WriteLine("获取属性管理器失败"); return; } List<string> names; bool gotNames = propMgr.GetCustomPropertyNames(out names); if (!gotNames || names == null || names.Count == 0) { Console.WriteLine("文档没有任何自定义属性"); return; } foreach (string name in names) { object value; swCustomPropertyType_e propType; bool ok = propMgr.GetType2(name, out value, out propType); if (ok) { Console.WriteLine($"属性名: {name} | 类型: {propType} | 值: {value}"); } else { Console.WriteLine($"属性名: {name} | 查询失败(属性可能已删除或不可访问)"); } } }这段逻辑看起来简单,但有几个地方需要解释。第一,属性名的获取必须走GetCustomPropertyNames返回的列表,不要自己去猜属性名,因为 SolidWorks 的属性名是区分大小写的,手写名字一旦大小写不一致就会查不到。第二,如果属性的值是中文或者含特殊字符,控制台输出可能乱码,这属于控制台编码问题,不是 API 的问题。第三,属性顺序不保证与界面显示一致,需要排序的话自己处理。
3.3 按类型做分支处理的经典写法
拿到类型之后,最推荐的写法就是直接switch分支,把三种最容易出问题的类型单独处理:
switch (propType) { case swCustomPropertyType_e.swCustomPropertyTypeString: HandleString(name, value?.ToString() ?? ""); break; case swCustomPropertyType_e.swCustomPropertyTypeNumber: if (value is double dValue) { // 数字格式统一用 InvariantCulture,避免中文环境小数点变逗号 string numText = dValue.ToString("0.####", System.Globalization.CultureInfo.InvariantCulture); HandleNumber(name, numText); } break; case swCustomPropertyType_e.swCustomPropertyTypeDate: if (value is DateTime dtValue) { string dateText = dtValue.ToString("yyyy-MM-dd"); HandleDate(name, dateText); } break; case swCustomPropertyType_e.swCustomPropertyTypeYesNo: // value 可能是 bool,也可能是字符串,兼容处理 bool boolValue = false; if (value is bool b) { boolValue = b; } else if (value != null) { bool.TryParse(value.ToString(), out boolValue); } HandleYesNo(name, boolValue); break; default: // 未知类型,按字符串兜底 HandleUnknown(name, value?.ToString() ?? ""); break; }有朋友可能会问:value是object,直接ToString()不就行了吗?不行,这正是很多报错的来源。日期类型直接ToString()输出的是类似2024/1/5 0:00:00的格式,存进数据库或 Excel 后很难看;数字类型在高精度要求下直接转字符串也容易丢精度。类型分支处理的价值就在这里:每种类型用最合适的方式进行格式化。
3.4 链接属性和表达式怎么处理
SolidWorks 里有一种特殊属性,值是表达式形式,比如="SW-文件名称(SW-File Name)"或者="质量"。这类属性在界面显示的是求值结果,但底层存的是表达式。用GetType2获取时,Value返回的通常是解析后的当前值,类型多半是字符串。
如果你需要拿到原始的表达式文本,用Get6方法:
string expression; bool ok = propMgr.Get6(propName, out expression, out bool evaluatedValue, out swCustomPropertyType_e type);这里expression返回未解析的表达式,evaluatedValue返回解析后的当前值。在做属性模板迁移、跨文档复制属性时,必须用Get6拿表达式,否则你会把求值结果当成属性源文本存过去,结果就变成一堆失去联动能力的死值。这个坑我见过不少人在做“批量重置属性”时踩过。
4. 批量处理与工程应用
4.1 遍历装配体里所有零件的通用写法
单文档读取只是热身,真正有生产力的是批量处理。最常见的需求是从一个装配体出发,遍历其所有子零件和子装配,读取每个文件的属性。这里给出一个可以落地的思路:
public static void TraverseComponents(AssemblyDoc swAssy, HashSet<string> visitedFiles) { object[] components = (object[])swAssy.GetComponents(false); if (components == null) return; foreach (object compObj in components) { Component2 comp = (Component2)compObj; string path = comp.GetPathName(); if (string.IsNullOrEmpty(path)) continue; // 去重,避免同一个件被多处引用时重复处理 if (!visitedFiles.Add(path)) continue; // 只读方式打开模型 ModelDoc2 childDoc = OpenModelReadOnly(path); if (childDoc != null) { DumpAllCustomProperties(childDoc); ReleaseComObject(childDoc); } // 如果该组件是子装配,递归进去 if (comp.GetModelDoc2() is AssemblyDoc childAssy) { TraverseComponents(childAssy, visitedFiles); } } }打开模型时注意使用只读模式,避免干扰用户正在编辑的文档。参考做法是用IOpenDoc2或OpenDoc6并传swOpenDocOptions_Silent和只读标志。批量遍历时还要注意内存:每个打开过的文档都要释放 COM 引用,否则处理几百个模型后 SolidWorks 可能明显卡顿。
4.2 属性规范校验与自动补全
拿到类型和值之后,可以做的事情就开始变多了。比如做属性规范校验:
- 如果属性类型是数字,但值解析失败,说明模型里这个字段填了非法内容,需要标记出来。
- 如果属性类型是日期,但日期格式不是预期格式,可以自动转换为标准格式。
- 如果是/否类型,要求必须为明确的是或否,不允许空值。
- 字符串类型,可以检查是否包含非法字符、是否超长。
这类校验在制造业企业里非常实用。下游 ERP 系统对物料编码、名称有严格的格式约束,上流模型里属性填得不规范,下游就会报错或者生成脏数据。用 C# 写一个校验脚本挂在菜单上,让设计人员提交前自查,能省掉大量返工。
4.3 导出 Excel BOM 时的类型转换
在生成 BOM 场景里,属性类型更是直接影响输出质量。重量、面积是数字,要保留合适的小数位数;日期是日期,要在 Excel 里显示成日期单元格格式;是/否可以映射成“是/否”而不是true/false。
我的做法是把每个属性统一封装成一个自定义类:
public class CustomPropInfo { public string Name { get; set; } public object RawValue { get; set; } public swCustomPropertyType_e Type { get; set; } public string ToDisplayString() { switch (Type) { case swCustomPropertyType_e.swCustomPropertyTypeNumber: return Convert.ToDouble(RawValue).ToString("0.##", System.Globalization.CultureInfo.InvariantCulture); case swCustomPropertyType_e.swCustomPropertyTypeDate: return Convert.ToDateTime(RawValue).ToString("yyyy-MM-dd"); case swCustomPropertyType_e.swCustomPropertyTypeYesNo: return Convert.ToBoolean(RawValue) ? "是" : "否"; default: return RawValue?.ToString() ?? ""; } } }这样你在后续无论是写 Excel、写文本还是推送到数据库,都只调用ToDisplayString(),格式统一且可维护。先读取到一个Dictionary<string, CustomPropInfo>,再统一处理的套路,比“读一个属性立即写一个属性”的散装逻辑清晰得多。
5. 踩坑实录与排查指南
5.1 属性名区分大小写和空格
第一个高频问题:明明模型里能看到“零件号”这个属性,代码里GetType2("零件号", ...)就是返回false。排查半天发现属性名实际是“零件号 ”(带尾部空格),或者大小写不一样。SolidWorks 的属性名是敏感的,推荐的做法是永远用GetCustomPropertyNames返回的真实名称去查询,不要手敲。如果你要判断某个固定属性是否存在,可以先对名称列表做一次不区分大小写的匹配,找到真实名称后再操作。
5.2 日期和数字类型转换失败
有朋友反馈value is DateTime这个判断根本不成立,日期属性拿到的 value 其实是一个字符串。这种情况通常不是 API 的问题,而是这个属性根本就不是真正的日期类型,只是创建者用文本格式存了一个日期字符串。届时要靠类型枚举值来判断:如果swCustomPropertyTypeDate都没成立,那它就是字符串,按字符串解析就好。反过来,如果类型是日期,但value解析失败,多半是系统区域设置和创建时的格式不一致,用DateTime.TryParse加多种格式兜底是更稳妥的做法。
数字类型同理。注意有些版本的 SolidWorks 在特定区域设置下,double值的ToString()会产生科学计数法,输出 1E+05 这种你不想看到的结果。统一用ToString("0.####", CultureInfo.InvariantCulture)可以规避。
5.3 COM 对象释放与进程稳定
这是二次开发老生常谈的问题,但每次都要强调。SolidWorks 的 API 是 COM 接口,ModelDoc2、ICustomPropertyManager、AssemblyDoc都是 COM 对象。批量脚本跑完后如果不释放,可能会让 SolidWorks 进程内存暴涨。
释放的标准姿势是:
private static void ReleaseComObject(object obj) { if (obj != null && Marshal.IsComObject(obj)) { Marshal.ReleaseComObject(obj); } }但注意,Marshal.ReleaseComObject在循环里频繁调用也可能导致过度释放的问题。我的个人习惯是:只对“高成本对象”做释放,比如文档对象、应用对象,对于属性管理器这类轻量对象不用太较真,防止在复杂循环里释放过头引发莫名其妙的异常。更安全的做法是把打开文档、读取属性、关闭文档封装成一个独立方法,保证每个文档在方法内使用try/finally释放。
5.4 配置特定属性带来的优先级混淆
一个零件如果有多个配置,模型特定属性和配置特定属性是两套不同的集合。CustomPropertyManager[""]拿到的只包含模型级属性,而你在属性对话框的“配置特定”标签页里看到的内容,不一定在这个集合里。
需要读取配置特定属性时,先获取配置名称列表:
List<string> configNames; swDoc.GetConfigurationNames(out configNames); foreach (string configName in configNames) { ICustomPropertyManager cfgMgr = swDoc.Extension.CustomPropertyManager[configName]; if (cfgMgr == null) continue; // 读取该配置的属性 }实际业务中,“属性到底存在哪一层”经常引发文书混乱。我的建议是开发前先定好规则:图号、名称等需全局一致的放模型特定属性,与配置有关的重量、材料放配置特定属性。代码里要做兼容逻辑:优先读配置特定属性,读不到再回退到模型特定属性,这样用户体验最好。
5.5 GetType2 判定未知类型时的处理
当属性值是表达式且表达式无法求值(比如引用了不存在的自定义属性)时,GetType2的类型可能返回swCustomPropertyTypeUnknown。这种情况别硬编码处理,建议当成字符串兜底,同时把属性值原样输出,方便排查。如果你在做数据导出,遇到Unknown类型时要专门打日志,因为它往往意味着模型里存在失联的链接表达式。
6. 从属性类型到完整自动化流程的扩展思路
GetType2只是属性读写工具箱里的一把扳手,但当你理解了属性类型在整个数据链路中的位置后,能做的事情会多很多。
目前我在实际项目中用属性类型做了几件事:第一是用类型信息动态生成属性录入界面,字符串类型给文本框、数字类型给数值输入框、是/否类型给复选框、日期类型给日期选择器,设计人员录入时不容易填错。第二是基于属性类型做迁移工具,从旧模板属性映射到新模板属性,期间严格按照类型匹配转换,避免把日期字段映射到文本字段造成数据含义丢失。第三是把属性读取封装成 Websocket 或 HTTP 接口,供上游系统按需拉取模型信息,此时类型信息直接映射成 JSON Schema 的字段类型,下游不用再做无意义的猜测。
如果你正在规划自己的 SolidWorks 二次开发框架,我的建议是:把“属性读取”模块做成独立的基础服务,统一走GetType2获取类型,对外输出结构化对象。这样无论你后面要接 PDM、做 BOM、做图纸校验,都不用再关心底层 SolidWorks API 的细节,所有调用方拿到的都是“名称 + 值 + 类型”的干净数据。做到这一步,你的二次开发才算真正站在了可复用的地基上,而不是每个新需求都要重新从GetType2开始摸一遍。