做二次开发的人多半都经历过这种场景:插件里写着写着,突然就需要知道用户此刻在装配体里到底选中了哪个零件,再把它的名称弹出来,或者作为参数传给下一步功能。这个需求用一句话说就是——C# 获取选中零件的名称,可真到动手的时候,你会发现一堆琐碎的细节等着你去踩。这篇文章就是把我实际项目中反复折腾过的完整套路摊开讲一遍,包括环境怎么搭、API 怎么调、为什么有些接口改名了、拿到的是“实例名”还是“文件名”,以及几个最容易让人懵圈的坑。代码以常见版本为准,SolidWorks 2018 到 2022 之间应该都能直接用,版本差异我会在对应位置说清楚。
1. 先搞清楚这个需求到底在问什么
1.1 一句话需求背后的真实场景
只从标题看,“c# solidworks 获取选中零件的名称”好像没什么难度。但你真去拆这个需求,会发现它至少能拆出三种意思:
第一,用户在工作区里选中了一个装配体中的零部件,程序要拿到这个零部件的实例名称,比如“螺丝M6-1@装配体1”。第二,用户打开的是单个零件文件,程序要拿到当前文档的文件名,比如“支架.SLDPRT”。第三,用户选中了一个子装配体,程序不仅要知道它叫什么,还得知道它在顶层装配体里的完整层级,比如“子装-A-1/机架/门板”。
这三种意思对应的 API 完全不同,返回结果的字段也不一样。很多新手拿到的名字不对,大多不是代码写错,而是没搞明白自己到底要哪一种。
从我的经验看,最频繁出现的场景是第一种:程序作为插件跑在 SolidWorks 里,用户打开一个装配体,然后在特征树上点选一个零件,程序需要弹出这个零件的名字,或者拿这个名字去读写自定义属性、做 BOM 导出。这篇文章的核心逻辑也主要围绕这个场景展开。
1.2 两条路线的选择:搞文档还是录宏
你在网上搜这个问题,搜出来的结果基本分两派。一派是照搬 API 文档,把 COM 接口一个一个列出来;另一派是教你直接在 SolidWorks 里录制宏,看 VBA 代码怎么写的,再翻译成 C#。
我个人的建议是:两条路都要走,但是顺序很重要。先录宏,再用 API 文档去确认细节。
因为 SolidWorks API 的对象层级比较深,直接看文档容易迷失。但是宏录制能很直观地告诉你,当你选中一个零件时,SolidWorks 真正调用了什么方法。录制一段、点选一个零件、停止,VBA 代码里就会留下 SelectionManager 相关的调用痕迹,你照着翻译,错的概率会低很多。
当然宏也有局限。它的 VBA 代码经常会用一堆类型不明确的变量,你根本不知道它最终拿到的是对象还是字符串。这时候再去查 API 文档,思路就会清晰很多。
1.3 “选中零件”的歧义点
还有个必须提前说明的事:SolidWorks 里的“选中”有不同层级。
在装配体模式下,你选中特征树里的零部件,返回的是 IComponent2 接口对象;在零件模式下,你选中一个面或一个特征,返回的可能是 IFace2、IFeature;在草图模式里选中一段线,返回的又是 IEntity。标题里说的是“选中零件”,但不同情况下,返回对象的类型完全不同。
所以第一步反而应该是判断当前模式,再去走对应的逻辑分支。我后面给出的完整示例里,会先判断当前文档是装配体还是零件,再决定是通过 SelectionManager 拿选中组件,还是直接读取当前文档标题。
2. 核心技术拆解:从 COM 到 SelectionMgr
2.1 这些对象到底有什么关系
SolidWorks 提供的 API 本质是一套 COM 接口,C# 通过互操作(Interop)来调用。
调用的时候,你通常会先拿到一个最顶层的入口对象——SldWorks,它代表整个 SolidWorks 进程。然后通过它拿到当前激活的模型文档对象 ModelDoc2 或 ModelDocEx。在文档对象下面,有一个 SelectionManager(选择管理器),它负责告诉你当前用户选中了什么、选中了几个、每个选中对象的类型是什么。
很多人搞不懂为什么要这么绕。你可能会想,既然是在装配体里选零件,为什么不直接从装配体里拿?难点在于 SolidWorks 没有“当前选中项”这种全局静态属性,它把选择信息都挂在文档对象下面。所以你必须按 SldWorks → ModelDoc → SelectionManager → 选中对象这条链路一层层往下剥。
如果你是在插件环境里跑,其实第一步可以更简单:插件自带的连接回调函数会直接给你一个 SldWorks 对象。如果是外部独立程序,就需要先启动或连接到 SolidWorks 进程。
2.2 为什么是 SelectionManager 而不是 ModelDoc
我见过不少新手试图用 ModelDoc2 的某个属性直接拿选中项,翻半天找不到,然后开始怀疑 API 是不是有问题。实际上,SolidWorks 的设计就是把选择管理单独做成一个管理器。这不是为了为难你,而是因为“选择”这个概念本身很复杂。
一个文档里可能同时选中了多个对象,可能有装配体里的零部件,也有装配体自身,还可能混着注解和尺寸。SelectionManager 需要统一处理这些东西,所以它提供了一组带索引的方法:
- GetSelectedObjectCount2 用来获取选中总数。
- GetSelectedObject6 用来取第 i 个选中对象。
- GetSelectedObjectType 用来判断第 i 个对象的类型。
理解了这种设计,你就会明白为什么代码里到处是循环和类型判断,而不是简单的SelectedPart.Name。
2.3 版本与接口的取舍
SolidWorks 的 API 迭代很快,很多方法都有多个版本后缀。比如获取选中对象的方法就有 GetSelectedObject、GetSelectedObject3、GetSelectedObject6 等。后缀数字越大,通常代表能传的参数越多、支持的场景越完整。
这里有一个常见的坑:低版本 SolidWorks 不认识高版本的方法,反之高版本大多数兼容低版本。如果你要写一个通用工具,建议优先用后缀数字比较高的方法,同时做好 catch,遇到方法不存在时再降级。
为了减少这种版本折腾,我会在示例代码里大量使用 C# 的 dynamic 关键字。它可以让 COM 方法调用的写法变得很简洁,不用在编译期绑定具体接口版本。缺点也很明显,IDE 提示会变弱,而且运行时错误要到运行那一刻才会暴露。对于这种二次开发的小工具来说,利大于弊。
3. 从零到一:实现获取选中零件名称
3.1 准备你的项目环境
写代码之前,先建立一个普通控制台应用程序或 WinForms 项目。我常用的配置是 .NET Framework 4.8,因为 SolidWorks 的 COM 互操作在这套运行时下最省心。如果你非要用 .NET Core 或 .NET 5/6,也不是不行,但 COM 引用的生成和 runtime 兼容会多出不少麻烦,我还真遇到过某些 API 在 .NET Core 下调用直接崩溃的情况。
项目建好后,给项目添加 SolidWorks 的互操作程序集。方式是:右键项目引用 → 添加引用 → COM → 找到类似 “SOLIDWORKS 2018 Type Library - sldworks” 的条目,勾选添加。
如果你在程序集列表里找不到,说明你的 SolidWorks 安装可能不完整,或者系统里缺少对应组件。一般安装完整版 SolidWorks 都会带工具库。实在不行,可以从安装目录里找到 sldworks.dll 对应互操作文件,手动放入项目的引用目录。
注意:不要在项目里同时引用多个版本的 SolidWorks 互操作程序集,轻则程序集冲突,重则运行时直接告诉你“指定的强制转换无效”。一个解决方案只绑定一个主要版本就好。
3.2 连接正在运行的 SolidWorks 实例
如果我们的程序要在 SolidWorks 界面里被触发,最简单的做法是先从正在运行的实例里获取对象。
C# 里可以通过 Marshal.GetActiveObject 拿到这个实例。如果当前没有实例,再用 ProgID 创建一个新的 COM 实例。两个分支加起来,能覆盖绝大多数使用场景。
using System; using System.Collections.Generic; using System.Runtime.InteropServices; namespace SolidWorksSelectionDemo { internal static class Program { [STAThread] private static void Main() { try { dynamic swApp = GetSwApplication(); if (swApp == null) { Console.WriteLine("无法连接 SolidWorks."); return; } List<string> partNames = GetSelectedPartNames(swApp); foreach (string name in partNames) { Console.WriteLine(name); } } catch (Exception ex) { Console.WriteLine("发生异常: " + ex.Message); } } private static dynamic GetSwApplication() { // 方法一:抓取已经打开的 SolidWorks 实例 try { return Marshal.GetActiveObject("SldWorks.Application"); } catch (Exception) { // 方法二:没有运行实例时,创建新的实例 Type swType = Type.GetTypeFromProgID("SldWorks.Application"); if (swType == null) return null; dynamic swApp = Activator.CreateInstance(swType); swApp.Visible = true; return swApp; } } } }这段代码有两点要说明。第一,[STAThread]很重要,COM 对象很多时候要求调用线程是 STA 模型,尤其是涉及拖拽、剪贴板、窗口操作时。如果你不加,程序可能在某个看似无关的位置莫名奇妙地崩溃。第二,GetActiveObject拿到的对象会持有 COM 引用,理想情况下用完要释放。对于短小的小工具,不释放问题也不大,但如果你在插件里高频调用,建议掌握 Marshal.ReleaseComObject 的用法。
3.3 拿到当前的激活文档
拿到 SldWorks 之后,下一步就是拿当前用户看到的文档。绝大部分业务逻辑都要求用户先打开一个装配体或零件,然后程序才能继续。
可以用 swApp.ActiveDoc 来拿。这个属性返回的是当前活动文档,也就是用户在 SolidWorks 窗口最前面看到、正在编辑的那个模型文档。别小看这一步,如果用户打开了三个窗口,但焦点在浏览器上,ActiveDoc 可能仍然会返回最后一个被激活的 SolidWorks 文档。实测下来,这个属性在面对多窗口场景时还算可靠。
补充一点:在插件环境里,如果当前没有任何文档打开,ActiveDoc 会返回 null,这时候你就得提示用户先开个模型再操作。示例里我会检查到 null 就直接返回。
3.4 遍历所有选中对象
拿到文档对象后,用它的 SelectionManager 获取选择信息。完整代码如下:
private static List<string> GetSelectedPartNames(dynamic swApp) { List<string> result = new List<string>(); // 1. 获取当前激活文档 dynamic swModel = swApp.ActiveDoc; if (swModel == null) { Console.WriteLine("当前没有打开的文档。"); return result; } // 2. 获取选择管理器 dynamic swSelMgr = swModel.SelectionManager; // 3. 统计选中对象数量 int count = swSelMgr.GetSelectedObjectCount2(-1); Console.WriteLine("选中对象数量: " + count); // 4. 循环遍历 for (int i = 1; i <= count; i++) { int selType = swSelMgr.GetSelectedObjectType(i, -1); // swSelCOMPONENTS 在 SolidWorks API 中通常为 2,代表装配体里的零部件 if (selType == 2) { dynamic swComp = swSelMgr.GetSelectedObject6(i, -1); string compName = swComp.Name2; result.Add(compName); } } return result; }这段代码已经能跑通大多数装配体场景。核心逻辑就是三步:取数量、按索引取类型、从类型匹配到组件对象。
这里出现的-1参数经常有人问。它表示采用默认的选择标记(Selection Mark)。SolidWorks 允许你用不同的标记过滤选择集,大部分外部程序都用默认标记,所以传-1就好。如果你的程序内部需要区分自己通过 API 添加的临时高亮和用户手动点选的内容,才会用到其他标记值。
3.5 区分“装配体里的零件”和“单独打开的零件”
上面代码只处理了装配体里的 IComponent2。如果你的当前文档本来就是零件文件,那用户选中的其实不是“零件组件”,而是零件里的特征、面、草图等元素。这种情况下,要显示“零件名称”,直接拿当前文档的标题就好。
我平时会把两种模式合并到一个方法里,这样用户无论开的是装配体还是零件,点击按钮后都能得到正确结果。
private static List<string> GetSelectedPartNames(dynamic swApp) { List<string> result = new List<string>(); dynamic swModel = swApp.ActiveDoc; if (swModel == null) return result; // 判断文档类型 string docType = swModel.GetTypeName(); // 零件文档通常是 "Part",装配体文档通常是 "Assembly" if (docType == "Part") { result.Add(swModel.GetTitle()); return result; } // 装配体文档走选择管理器 dynamic swSelMgr = swModel.SelectionManager; int count = swSelMgr.GetSelectedObjectCount2(-1); for (int i = 1; i <= count; i++) { int selType = swSelMgr.GetSelectedObjectType(i, -1); if (selType == 2) { dynamic swComp = swSelMgr.GetSelectedObject6(i, -1); string compName = swComp.Name2; string fileName = ""; dynamic compDoc = swComp.GetModelDoc2(); if (compDoc != null) { fileName = compDoc.GetTitle(); } result.Add(compName + " | " + fileName); } } return result; }注意,GetModelDoc2()返回的并不是文件名,而是一个模型文档对象。要拿到这个文档的名称,需要再调用文档对象的GetTitle()。很多初学者卡在这一步,会误以为组件对象的 Name 就是文件名,结果拿去打开文件时发现路径不对。
3.6 一个可以直接拿去改的完整示例
把前面的逻辑拼到一起,我这边写了一个顺手示例,可以直接复制到控制台项目里,改个命名空间就能用:
using System; using System.Collections.Generic; using System.Runtime.InteropServices; namespace SolidWorksSelectionDemo { internal static class Program { [STAThread] private static void Main() { try { dynamic swApp = GetSwApplication(); if (swApp == null) { Console.WriteLine("无法连接 SolidWorks."); return; } swApp.Visible = true; List<string> names = GetSelectedPartNames(swApp); if (names.Count == 0) { Console.WriteLine("没有选中任何零件。"); } else { Console.WriteLine("选中零件的名称如下:"); foreach (string name in names) { Console.WriteLine(name); } } Console.WriteLine("按任意键退出..."); Console.ReadKey(); } catch (COMException comEx) { Console.WriteLine("COM 调用异常: " + comEx.Message); } catch (Exception ex) { Console.WriteLine("普通异常: " + ex.Message); } } private static dynamic GetSwApplication() { try { return Marshal.GetActiveObject("SldWorks.Application"); } catch (Exception) { Type swType = Type.GetTypeFromProgID("SldWorks.Application"); if (swType == null) return null; dynamic swApp = Activator.CreateInstance(swType); swApp.Visible = true; return swApp; } } private static List<string> GetSelectedPartNames(dynamic swApp) { List<string> result = new List<string>(); dynamic swModel = swApp.ActiveDoc; if (swModel == null) return result; string docType = swModel.GetTypeName(); if (docType == "Part") { result.Add(swModel.GetTitle()); return result; } dynamic swSelMgr = swModel.SelectionManager; int count = swSelMgr.GetSelectedObjectCount2(-1); for (int i = 1; i <= count; i++) { int selType = swSelMgr.GetSelectedObjectType(i, -1); if (selType == 2) { dynamic swComp = swSelMgr.GetSelectedObject6(i, -1); string compName = swComp.Name2; string fileName = ""; dynamic compDoc = swComp.GetModelDoc2(); if (compDoc != null) { fileName = compDoc.GetTitle(); } result.Add(compName + " | " + fileName); } } return result; } } }运行这个程序前,建议先在 SolidWorks 里打开一个有装配体的项目,然后在装配体中选中几个零件,再运行程序。你会看到控制台输出两列信息,左边是组件实例名,右边是文件名。
3.7 拿到名称之后能做什么
名称往往只是入口。实际开发里,拿到 IComponent2 对象之后,通常还要继续往下挖。
比如要读取零件的自定义属性,可以用swComp.GetModelDoc2()拿到文档对象,再调用GetCustomInfo或Extension.CustomPropertyManager。要获取零件的物理质量,可以用swComp.GetMassProperties()。要控制零件隐藏或显示,可以用swComp.Visible属性。
只要你保住了 IComponent2 这个对象引用,后续扩展非常方便。所以写代码时不要把方法限定成“只返回字符串”,很多时候直接返回对象,让上层调用方按需取值会更灵活。
4. 实操之后才懂的细节与避坑清单
4.1 实例名、文档名和文件名的分叉
这是整个需求里最容易踩的坑。
组件对象 IComponent2 的Name2属性返回的是“实例名”,不是“文件名”。简单理解,实例名是这个零件在当前装配体里那一次放置的编号。比如你往装配体里放了同一个“螺丝6.SLDPRT”三次,那么这三处分别叫“螺丝6-1@装配体1”“螺丝6-2@装配体1”“螺丝6-3@装配体1”。如果你在同一个装配体里再次放置同一个零件,序号会接着往上走。
而文件名是磁盘上的实际文件名,比如“螺丝6.SLDPRT”。
如果你要显示给用户看的“零件名”,大多数场景用户期望的是文件名,而不是实例名。因为实例名会变,重装装配体后序号重新排列;文件名才是稳定的标识。所以我代码里特意把两个都取出来,用|分隔,供你取舍。
如果你只需要唯一的零件标识,我建议优先取文件名。如果你需要区分同一文件在装配体里被多次放置的不同实例,那就必须取实例名。
4.2 在子装配体里选中的零件,怎么拿完整路径
这个问题有业务场景时会很头疼。
假设顶层装配体里有一个子装配体“子装-A”,用户点选了“子装-A”里面的一个“盖板”。此时你拿到的组件对象位于子装配体内,comp.Name2返回的是类似“盖板-1@子装-A”这样的字符串,而不是顶层装配体里的完整路径“子装-A/盖板-1@顶层”。
如果你需要构造整机的 BOM 树,这种“局部名称”显然不够用。此时可以玩一个技巧:利用GetPathName或者递归向上寻找父层级。
但我更常用的方式,是借助 SolidWorks 的Component2.GetChildren递归遍历整棵组件树,然后比对组件对象引用是否相等。这个方法稳定、直观,就是代码量多一点。核心代码如下:
private static void WalkTree(dynamic parentComponent, dynamic targetComp, string path, List<string> results) { if (parentComponent == null) return; if (ReferenceEquals(parentComponent, targetComp)) { results.Add(path); return; } dynamic children = parentComponent.GetChildren(); if (children == null) return; foreach (dynamic child in children) { string childPath = path + "/" + child.Name2; WalkTree(child, targetComp, childPath, results); } }注意,ReferenceEquals对 COM 对象不一定可靠,因为 RCW(Runtime Callable Wrapper)可能不是同一个包装对象。比较稳妥的方式是通过GetModelDoc2()拿到的文档对象指针,或者用GetType()配合对象特有属性做对比。如果你只是简单应用,也可以比较Name2字符串,但要承担重名实例带来的风险。
4.3 界面线程和 COM 调用的稳定性问题
外部程序调用 SolidWorks API,偶尔会出现程序假死或者 API 调用超时。一个常见原因是线程模型不对。
前面提到[STAThread],这里再展开一点。SolidWorks 的 COM 对象很多是 STA(单线程套间)对象,如果你从后台线程直接调用,COM 会帮你做线程切换,但如果主线程正在等待这个后台线程结束,就会互相等待,造成死锁。
我见过最典型的死锁场景是:界面上放了一个“开始导出”按钮,点击后开了一个后台线程去遍历装配体,遍历过程中又弹出进度条窗口,进度条窗口在主线程上显示。这时候后台线程在等主线程处理消息,而主线程在等后台线程完成,程序就卡死了。
解决办法有三种。第一,除非耗时不长,否则尽量在 UI 线程直接调用 API,别开多线程。第二,必须多线程时,在后台线程里调用 API 前后不要阻塞主线程等待,把结果用异步回调带回。第三,如果一定需要等待,那就用Dispatcher.Invoke或Control.Invoke把 UI 更新丢回 UI 线程。
这种问题不会在前几十次调用中出现,但等到你处理大型装配体,操作次数突然变多,线程切换频繁,问题就会集中爆发。提前在设计阶段考虑清楚,能省不少事。
4.4 常见错误速查表
我整理了一下平时二次开发里最常碰到的错误,直接做成速查表,遇到问题先对照一遍。
| 错误信息 | 可能原因 | 对应解决办法 |
|---|---|---|
| 0x80040154 Class not registered | 本机没装 SolidWorks,或互操作组件未正确注册 | 检查 SolidWorks 安装完整性,修复安装 |
| 0x80004005 一般性错误 | 调用了当前版本不支持的 API,或对象已释放 | 确认 API 版本,避免释放后继续使用对象 |
| 0x80020009 异常被捕获,但参数不正确 | dynamic 调用时参数类型不对 | 检查 GetSelectedObjectType / GetSelectedObject6 的参数类型 |
| ActiveDoc 为 null | SolidWorks 已启动但没有打开文档 | 提示用户先打开模型文件 |
| Name 属性获取失败 | 对象类型不是 IComponent2 而是 IFeature | 先判断 SelectionManager 返回的类型再取值 |
| 程序启动就崩溃 | 控制台项目没加 [STAThread] 或缺少引用 | 补上 STAThread,重新添加互操作引用 |
这张表不能覆盖所有情况,但能覆盖我遇到过的百分之七八十。尤其第一行,很多刚接触 COM 的同事一看到“class not registered”就慌,其实多半就是本机 SolidWorks 没装好或者没启动。
4.5 一个被低估的调试方法:录制宏,反向翻译
再分享一个很多老鸟都在用但新人不太知道的技巧:宏录制。
需求复杂到一定程度时,靠文档猜测 API 调用顺序是不现实的。我的流程是这样的:先在 SolidWorks 里打开“工具 → 宏 → 录制”,手动执行你要实现的操作,比如在装配体里选中一个零件、切换一下文档视图、读取某个属性。操作完成后停止录制,SolidWorks 会生成一段 VBA 代码。
这段 VBA 代码虽然语法老,但它把 API 调用的顺序、参数个数、对象层级都写得很清楚。比如你想知道“如何拿到当前选中的零件名称”,录制完选择操作后,代码里会看到SelectionManager、GetSelectedObject、GetTitle这些关键字。然后你在 C# 里照着逻辑写一遍即可。
这个方法不仅能帮你确认 API,还能帮你快速了解某个按钮在后台到底做了什么。很多时候所谓“接口不会调”,其实是没找到正确的入口,录制一段就全明白了。
4.6 批量处理与性能注意事项
如果你要遍历一个大型装配体里的所有选中零件,然后逐个读取属性,那性能就要重视起来。
第一个原则是能复用对象就不要重复获取。比如GetModelDoc2()第一次拿到文档对象后,同一个组件多次调用理论上返回同一个文档,但每次调用仍有 COM 开销。最好是拿到一次,缓存起来,后面循环使用。
第二个原则是注意内存释放。遍历几百上千个组件后,COM 包装对象不会自动全部销毁。如果你发现处理多次后内存暴涨、程序变慢,多半是 RCW 对象堆积。建议在处理完单批数据后,调用Marshal.ReleaseComObject释放对象。
不过这里有个更接地气的提醒:不要在一开始就为性能优化写一堆复杂代码。先保证功能正确,在中等规模装配体上测试,如果速度能接受,就不要再折腾。过早优化会引入生命周期管理的 bug,到时候追起来很痛苦。
5. 关于这个功能的一些个人体会
技术点讲完了,最后说点我在实际项目里的体会。
“获取选中零件的名称”作为需求是简单的,但它牵扯出来的问题非常多:你到底要哪个名称、当前是什么文档类型、对象是组件还是特征、要不要考虑多选、要不要处理子装配体的层级关系。这些问题如果不提前想清楚,往往写出来的代码只能在特定场景下运行,用户换个操作方式就崩。
我现在的习惯是,写这类小工具时先把需求边界画清楚,然后用宏录一遍用户操作,再决定用哪一组 API。宁可开头多花半小时梳理,也不要代码跑两天才发现拿错名字。
另外,如果你的程序要发布给别人用,尽量把“名称”的定义写进说明里。比如界面上显示的是“文件名|实例名”,还是只显示“文件名”。这个细节很多人注意不到,但实际使用时特别影响体验。我在交付过一个工具后,被用户连续追问了几次“为什么名字对不上”,最后发现是双方对这个名词的理解不一样。
最后再分享一个小技巧:如果你在 C# 里用 dynamic 调试很痛苦,可以把 COM 对象序列化成关键字符串打印出来,比如先打出类型名、可用属性名,初步定位问题后再回到正式代码。这种“ Dump 式调试”在 COM 互操作这种黑盒环境里,比断点单步调试要高效得多。
希望这篇分享能帮你少走一些弯路。如果看完后你的项目里已经把零件名称顺利拿到手了,说明这套思路对你是有效的。后续如果还要做批量导出 BOM、按名称批量改属性,也欢迎在评论区继续聊。