SolidWorks二次开发模板:插件骨架、命令回调与避坑实践 简介《SolidWorks二次开发模板》面向希望基于C#扩展SolidWorks功能的机械设计工程师、CAD二次开发初学者及.NET开发者提供从SDK接口调用、COM插件注册到界面定制与事件处理的实践起点可用于快速搭建自定义工具栏、命令和窗体交互。压缩包共99个文件约10.24MB包含C#源码、项目与解决方案文件、DLL引用、资源位图、resx资源及配置文件等较完整地保留了插件工程的编译与运行结构。已有730人学习下载读者可据此理解ISldWorks接口、Add-in生命周期、命令事件、数据交换和错误处理等关键环节并在此基础上改造出贴合自身设计流程的自动化工具。1. 从一张改到凌晨两点的装配体说起之前帮一个做非标设备的小团队梳理出图流程三十来号人的设计组每周要交一批总装图。图纸标题栏里的材料、图号、版本号靠人工一行行敲改漏一处就得整批返工最狠的一次改到凌晨两点。他们不是没想过自动化而是每次打开 API 帮助文档看到ISwAddin、CommandManager、COM 注册这一串就劝退了。SolidWorks二次开发模板解决的正是这个卡点插件注册、菜单按钮、事件订阅、调试配置这些一次性的脏活提前搭好你只管往回调函数里填业务逻辑。它适合两类人一类是想把手上重复操作固化成按钮的机械设计工程师另一类是接了 CAD 工具链外包、需要快速起项目的开发者。下面按目录结构、骨架链路、功能走读、踩坑排查、改造思路逐层拆开。2. 模板的地基目录结构、Interop 引用与编译目标怎么定先把话说前面SolidWorks 插件跑不跑得起来八成问题出在工程配置而不是业务代码。模板之所以有价值就是它把这层配置固化成了可复制的骨架你换台机器、换个 SolidWorks 版本照着改几个数字就能继续用。2.1 目录分层把一次性配置和业务代码隔开拿到模板先看目录它决定了你后面往哪儿加功能。模板典型的分层是这样SolidWorksAddinTemplate/ ├─ src/ │ ├─ Addin/ │ │ ├─ SwAddin.cs # ISwAddin 实现、COM 注册表读写 │ │ ├─ CommandHub.cs # 命令组创建、按钮 ID 与回调分发 │ │ └─ EventBridge.cs # 文档事件订阅与退订 │ ├─ Features/ │ │ ├─ BatchPropertyFeature.cs # 批量写自定义属性 │ │ └─ BomExportFeature.cs # BOM 导出 │ ├─ Infrastructure/ │ │ ├─ SwContext.cs # 当前文档、配置名、单位的取用封装 │ │ └─ SwLog.cs # 写本地日志现场排查靠它 │ └─ Properties/AssemblyInfo.cs # GUID、ComVisible 等程序集特性 ├─ installer/ │ ├─ register.bat # regasm /codebase 注册脚本 │ └─ addin.reg # 备用注册表脚本 └─ tests/关键在Addin/和Features/的边界。SwAddin.cs只负责和 SolidWorks 打交道——连上、断开、注册CommandHub.cs只负责按钮长在哪儿、点了之后分发到谁真正的业务逻辑一律下沉到Features/。这条边界不划清后面加第十个功能时你会得到一个两千行的SwAddin.cs改一处怕崩三处。Infrastructure/SwContext.cs是模板里最容易被低估的文件它把“当前活动文档可能为 null”“装配体里可能全是轻化件”这类判断收在一处业务代码就不用到处写if (doc null)。2.2 三份 Interop 引用与那个必须关掉的开关模板的 csproj 里会引用 SolidWorks 安装目录下的几个托管 DLL少一个都编译不过程序集用途是否必须SolidWorks.Interop.sldworksISldWorks、ModelDoc2、AssemblyDoc等主接口是SolidWorks.Interop.swconst枚举常量如swDocumentTypes_e、swSaveAsOptions_e是SolidWorks.Interop.swpublishedISwAddin接口插件的入口契约是SolidWorks.Interop.swdimxpert尺寸专家相关接口否按需比引用更关键的是一个属性Embed Interop Types 必须设成 False。默认模板为了省事会打开它结果就是你写(ISwAddin)转换时抛InvalidCastException或者事件订阅死活绑不上代码看着一点问题没有。我一般还会把Copy Local也关掉让程序集直接吃本机安装目录里的那份避免插件和 SolidWorks 自带版本打架。!-- 只截关键几行路径按本机安装位置改 -- Reference IncludeSolidWorks.Interop.sldworks HintPathC:\Program Files\SOLIDWORKS Corp\SOLIDWORKS\api\redist\SolidWorks.Interop.sldworks.dll/HintPath EmbedInteropTypesFalse/EmbedInteropTypes PrivateFalse/Private /ReferencePrivate为 False 表示不复制到输出目录运行时由 SolidWorks 进程自己解析。这个取舍有代价用户机器上的 SolidWorks 版本必须和编译时引用的 DLL 匹配跨版本分发要按版本各编一份。模板里通常用条件编译符号或者多套 csproj 解决二次开发时先确认你要覆盖哪几个版本别一上来就想通吃。2.3 编译目标与调试配置x64 加 .NET FrameworkSolidWorks 从 2018 起只出 64 位所以插件的平台目标必须是 x64AnyCPU 在部分加载路径下会翻车。目标框架用 .NET Framework跟着 SolidWorks 版本走就行常见是 4.6.2 到 4.8 之间别想着上 .NET 6/8——Add-in 是进程内加载的 COM 组件加载器只认 .NET Framework。调试配置这块是模板最省事的地方。项目属性里勾上“启动外部程序”指向SLDWORKS.exeF5 会直接拉起 SolidWorks 并挂上调试器断点能停在ConnectToSW里。已经开着 SolidWorks 的话就用“附加到进程”选SLDWORKS.exe注意附加类型要选托管代码选错了断点是空心圆。// AssemblyInfo.cs 里这三样东西缺一不可GUID 必须和注册表脚本里的一致 [assembly: ComVisible(true)] [assembly: Guid(A1B2C3D4-0000-0000-0000-000000000001)] [assembly: AssemblyVersion(1.0.0.0)]ComVisible(true)不加regasm 根本不认这个类型GUID 换一次注册表里的键名就全变用户那边旧版本会残留成僵尸条目。改 GUID 是血泪经验里最不该犯的错除非你确实要做并行安装的多个版本否则一个项目就用一个固定 GUID 到底。3. 插件骨架拆解从 ConnectToSW 到命令按钮出现的完整链路知道文件放哪儿之后就该看运行时链路了。从 SolidWorks 启动到你的按钮出现在菜单上中间只经过三个动作加载程序集、调ConnectToSW、建命令组。顺序错一步就是“插件勾上了但按钮没出现”。3.1 ConnectToSW 里的初始化顺序ISwAddin只有两个方法要你实现ConnectToSW和DisconnectFromSW。模板里ConnectToSW的写法基本固定重点是顺序public bool ConnectToSW(object ThisSW, int Cookie) { swApp (ISldWorks)ThisSW; // 先拿到主接口后面全靠它 addinCookie Cookie; // Cookie 是 SolidWorks 分配的会话内唯一 ID // 把插件对象回填给宿主某些回调路径尤其混合 VBA 的工程依赖这一步 swApp.SetAddinCallbackInfo2(0, this, addinCookie); cmdMgr swApp.GetCommandManager(addinCookie); // 拿命令管理器空引用说明加载异常 BuildCommandGroup(); // 建组、加按钮 SubscribeEvents(); // 事件订阅放在最后避免早于 UI 就绪 return true; // 返回 false 会被卸载 }Cookie这个参数很多人忽略以为是垃圾值。它其实是本次会话里 SolidWorks 给你的插件分配的身份标识GetCommandManager和后续的CreateCommandGroup2都要用到它传错就是命令组建不出来。返回值那行也别乱改返回false会让 SolidWorks 立刻卸载你日志里什么都看不到排查起来像个黑匣子。DisconnectFromSW是镜像操作顺序反过来先退订事件再清命令组最后释放引用。模板里写得完整自己加功能时容易只记得加订阅忘了加退订问题会在关闭 SolidWorks 的时候集中爆发。3.2 命令组与 AddCommandItem2 的参数含义按钮不是直接加进菜单的要先建一个命令组CommandGroup再往组里塞条目。这层抽象的好处是一组按钮共享图标、标题和显示位置激活一次全部生效。private void BuildCommandGroup() { object imageList null; // 传 ImageList 句柄可自定义 16x16 图标null 用默认 object iconList null; // 参数依次是组 ID、组标题、悬停提示、菜单位置、子菜单位置、图标、描述、要工具栏、要菜单、大图标 cmdGroup cmdMgr.CreateCommandGroup2( CommandGroupId, 批量属性工具, 批量写入自定义属性并导出 BOM, 5, 0, ref imageList, 批量属性工具, true, true, ref iconList); // 返回值就是这条命令的真实 ID后面分发必须用它不能自己写死常量 batchPropCmdId cmdGroup.AddCommandItem2( 批量写属性, -1, 把表格里的属性写进模型, 批量写属性, 0, (int)swCommandItemType_e.swMenuItemType, 0, 0); bomExportCmdId cmdGroup.AddCommandItem2( 导出 BOM, -1, 按当前配置导出 BOM, 导出 BOM, 1, (int)swCommandItemType_e.swMenuItemType, 0, 0); cmdGroup.HasToolbar true; cmdGroup.HasMenu true; cmdGroup.Activate(); // 不 Activate 等于白建 }几个参数值得单独说。CommandGroupId是你在插件内的组标识建议从 1000 往上取避开 SolidWorks 自带命令占用的低位段如果有多个插件同机部署ID 撞车会导致按钮行为串台这是很隐蔽的坑。AddCommandItem2的Position传 -1 表示追加到末尾。最后Activate()那一行最容易漏漏了就是编译通过、日志正常、按钮永远不出现。不同 SolidWorks 版本里CreateCommandGroup2的参数个数和顺序略有差异模板通常会按版本做一层封装或者干脆只支持某几个版本。拿到别人的模板之后第一件事是打开本地 Interop 定义对一遍签名别直接信注释。3.3 回调分发与事件订阅的持有问题按钮点下去之后回调怎么落到你的方法上。老式写法是SetAddinCallbackInfo2配合 VBA 风格的函数名约定现代做法是订阅CommandManager的事件统一分发private void SubscribeEvents() { // 用字段持有事件处理器不要写成匿名 lambda否则可能被 GC 回收后静默失效 cmdMgr.CommandNotify OnCommandNotify; swApp.ActiveDocChangeNotify OnActiveDocChange; } private void OnCommandNotify(int commandId) { // 用 AddCommandItem2 返回的 ID 做判断不要手写数字常量 if (commandId batchPropCmdId) new BatchPropertyFeature(swApp).Run(); else if (commandId bomExportCmdId) new BomExportFeature(swApp).Run(); }SwLog在这里要记一笔进了哪个命令、当前文档什么类型、耗时多少。现场用户描述问题永远是“点了没反应”有日志就能立刻区分是回调没进、还是进了但被异常吞掉。4. 核心功能走读一个批量写属性并导出 BOM 的完整流程骨架讲完看模板里那两个示例功能。它们不是玩具而是把最容易踩坑的三类操作各演示了一遍装配体遍历、自定义属性读写、文件另存。4.1 遍历装配体组件与轻化件的判断装配体遍历的坑集中在“拿不到模型文档”。大型装配默认开轻化模式组件对象拿得到但它底下的ModelDoc2可能是 null你不判空就直接崩。private void WalkComponents(AssemblyDoc asmDoc, ActionComponent2 visit) { // 参数 false 表示只取顶层递归由下面自己控制避免一次拉爆内存 object[] comps asmDoc.GetComponents(false) as object[]; if (comps null) return; foreach (object o in comps) { Component2 comp o as Component2; if (comp null) continue; // 压缩件直接跳过轻化件先尝试解轻化解不开就记日志放过 if (comp.IsSuppressed()) continue; ModelDoc2 compDoc comp.GetModelDoc2() as ModelDoc2; if (compDoc null) { SwLog.Warn($组件 {comp.Name2} 未加载文档已跳过); } else { visit(comp); // 真正干活的回调 } // 需要下钻子装配时再递归深度优先即可 object[] children comp.GetChildren() as object[]; if (children ! null children.Length 0) WalkComponents((AssemblyDoc)compDoc, visit); } }GetComponents(false)和GetComponents(true)的区别是前者只取顶层、后者取全部平铺结果。平铺看起来省事但拿到的组件顺序和层级关系会丢做装配层级相关的功能时用不了。递归时注意compDoc可能为 null得先判空再转成AssemblyDoc。判断组件是否被压缩用IsSuppressed()别用GetSuppression2()然后自己比枚举两个方法在不同版本下行为有细微差异。4.2 自定义属性读写文件级和配置级的区别自定义属性分两级文件级整个零件共用一份和配置级每个配置各有一份。这两者在 API 上的唯一区别就是CustomPropertyManager的索引器参数——空字符串是文件级配置名是配置级。写错层级的结果是模型里明明能看到属性代码就是读不到。public static bool SetProperty(ModelDoc2 doc, string configName, string name, string value) { // configName 传 表示文件级传具体配置名则是配置级 CustomPropertyManager cpm doc.Extension.CustomPropertyManager[configName]; if (cpm null) return false; string raw , resolved ; int result cpm.Get4(name, out raw, out resolved); // 不存在就先 Add3 建档已存在就用 Set2 覆盖注意第二个参数是类型 if (result (int)swCustomInfoGetResult_e.swCustomInfoGetResult_NotPresent) cpm.Add3(name, (int)swCustomInfoType_e.swCustomInfoText, value, 1); else cpm.Set2(name, value); return true; }Get4的第三个out参数是解析后的值模型里的属性可以写SW-Mass零件1.SLDPRT这种引用表达式raw是原文、resolved是算出来的结果写报表时要用后者。Set2不会自动建键第一次写新属性必须走Add3这个顺序反了会返回 false 但不抛异常静默失败最难查。提示批量写属性前先doc.Extension.get_CustomPropertyManager()把管理器缓存一次循环里反复取会有可感知的性能损耗几百个零件时差距明显。4.3 导出与错误回传SaveAs3 的 ref 参数怎么读导出这块的坑在于静默选项。不加静默SolidWorks 会弹“是否覆盖”的对话框批量任务直接卡死在第一个文件上。public static bool Export(ModelDoc2 doc, string fullPath) { int errors 0, warnings 0; // 第二个参数 0 表示保持当前版本静默选项抑制覆盖确认框 bool ok doc.Extension.SaveAs3( fullPath, (int)swSaveAsVersion_e.swSaveAsCurrentVersion, (int)swSaveAsOptions_e.swSaveAsOptions_Silent, null, ref errors, ref warnings); if (!ok) { // errors 是位掩码不一定是 0/1要按位判断并记录原始值 SwLog.Error($导出失败 {fullPath}errors{errors}warnings{warnings}); } return ok; }SaveAs3返回 false 时errors里是位组合可能是“路径不存在”叠加“文件被占用”直接if (errors 1)这种判断一定会漏。我的做法是把原始数值连同文件路径一起写日志出问题时对照枚举值查。还有一点导出前如果做过选择操作先doc.ClearSelection2(true)清一遍残留选择会让某些版本的 SaveAs 导出成“仅所选实体”。5. 避坑与排查五类让模板装上了却不动弹的现场这一章全部来自实际调试记录。共同点是现象看起来像“代码没跑”实际都在工程配置或者生命周期上。5.1 编译与注册阶段的三种翻车第一类工具-插件里能看到条目勾选后重启又自动取消。现象是对话框里确实出现了你的插件名但每次重启 SolidWorks 又变回未勾选状态。原因是注册只写了一半——[ComRegisterFunction]里写了HKLM\Software\SolidWorks\Addins\{GUID}这段描述、标题、是否默认加载但没写HKCU\Software\SolidWorks\AddInsStartup\{GUID}的 DWORD 值 1宿主拿不到“用户已启用”的记忆。解决办法是在注册函数里两处都写路径里的 GUID 用Type.GUID.ToString(B)生成别手抄。另外注意位数64 位系统上要用RegistryView.Registry64打开基键否则写进了重定向节点SolidWorks 根本读不到。第二类编译通过一点按钮就抛 COMException 0x80004005。现象是插件能加载、按钮也在点下去直接弹异常对话框堆栈里全在 interop 层。多半是Embed Interop Types被打开了编译器把接口类型内嵌进程序集运行时的类型身份和宿主对不上另一个可能是引用的 interop DLL 版本低于本机 SolidWorks调用了新版本才有的方法。解决方法是逐条检查引用属性EmbedInteropTypes一律 FalseHintPath指向本机安装目录下api\redist里的那份 DLL换版本时整组引用一起换。第三类调试附加后断点全是空心圆。现象是进程附加成功断点却不命中。原因是工程的平台目标不是 x64或者附加时选成了本机代码调试。把平台目标改成 x64、重新生成附加时调试类型选“托管代码”问题就消失了。这个坑的迷惑性在于它和代码质量完全无关却最耽误时间。5.2 运行期的两类血泪经验第四类关闭 SolidWorks 时崩溃或者事件偶尔不触发。现象一是退出时弹“SolidWorks 遇到问题需要关闭”现象二是ActiveDocChangeNotify十次里有一两次不响应。前者是DisconnectFromSW里没退订事件宿主已经销毁了还往你的回调里发通知后者是事件委托没有用字段持有被垃圾回收器收走了订阅关系还在但目标成了空引用。解决方法是把事件处理器统一放进EventBridge类用字段保存订阅和退订成对出现在ConnectToSW/DisconnectFromSW里一处进一处出。第五类后台任务里调 API 直接卡死。现象是点按钮后界面假死任务管理器里 SolidWorks 无响应。原因是 SolidWorks API 是 STA 的 COM 组件从后台线程直接调用会阻塞在消息泵上。解决方法是把调用切回主线程WinForms 里用Control.InvokeWPF 里用Dispatcher.Invoke实在没有 UI 上下文就捕获SynchronizationContext再Post回去。另外耗时任务执行期间可以设置swApp.CommandInProgress防止用户在任务跑一半时又点一次按钮造成重入。6. 二次开发改造把模板变成自己的生产力工具模板本身没什么业务价值值钱的是它省下的那两三天配置时间。要让它变成你自己的工具改造顺序建议按“ID 规划 → 命令拆分 → 配置外置”三步走。命令 ID 是第一个要规划的先画一张表把每个功能固定在一段号里后续加功能只往后追加不插空。模板里那两个示例的 ID 段直接沿用改标题即可号段用途示例1000–1099属性读写类批量写属性、属性对照1100–1199导出与报表导出 BOM、导图 PDF1200–1299检查与校验图号重复检查、缺失属性扫描第二步是拆命令。别把所有功能塞进一个按钮弹一个大对话框SolidWorks 用户的操作习惯是“在当前文档上点一下立刻看到结果”。每个命令独立成一个Feature类只接收ISldWorks自己负责取当前文档、判类型、执行、写日志。这样加功能就是加一个类、加一条命令项SwAddin.cs一个字都不用改。// 新增功能的固定套路照着抄就行 public sealed class BomExportFeature : ISwFeature { public void Run(ISldWorks app) { ModelDoc2 doc app.ActiveDoc; if (doc null || doc.GetType() ! (int)swDocumentTypes_e.swDocASSEMBLY) { SwLog.Warn(当前不是装配体命令忽略); // 类型不符就静默退出别弹框 return; } // 业务逻辑... } }第三步是配置外置把属性名、模板路径、导出目录这些会变的东西放进插件同目录的 JSON 或 ini 里别硬编码。用户那边换个命名规范你改个配置文件就行不用重新编译注册一遍。验证方式很简单找一台没装过开发环境的机器走一遍“注册 → 重启 → 勾选 → 点按钮 → 看日志”的完整闭环中途任何一步不对都别往下走。从那以后我每次发版本都强制走一遍这个闭环再急也不跳——快就是慢这个习惯帮我拦下过好几次注册表残留导致的灵异问题。希望帮到你。本文还有配套的精品资源点击获取