简介这是一份面向机械设计开发者与 SolidWorks 二次开发初学者的入门模板工程围绕 C# 语言调用 SolidWorks SDK 展开帮助读者解决从零搭建 COM Add-in 插件、不熟悉接口调用与界面定制的问题。资源共 99 个文件以 18 个 cs 源码、34 个 dll 程序集、2 个 csproj 工程文件与 1 个 sln 解决方案为主另有 bmp 图标、resx 与 resources 资源文件、xml 配置、pdb 调试符号及 tlb 类型库等压缩包约 10.24MB构成可直接打开编译的完整工程结构。内容覆盖插件接口实现、工具栏与菜单界面定制、命令注册与事件处理、属性管理页面、模型操作封装以及数据交换、异常处理与日志记录等实践环节源码中按模块拆分出插件主类、事件处理、属性页管理与窗体设计等文件便于对照理解 SDK 调用流程。目前已有 730 人学习下载适合希望快速上手 C# 扩展开发、参考工程组织方式与插件注册部署思路的初中级开发者。1. 从录一段宏说起SolidWorks 二次开发模板真正省掉的是哪几步第一次被要求批量改属性的人路径几乎都一样打开 SolidWorks工具 → 宏 → 录制手点一遍“打开零件、改自定义属性、保存、关闭”停止录制然后把生成的 VBA 代码里的文件名改成变量加个循环跑三百个零件。前十几个能过第十几个开始报错报错信息还只有一句“对象未设置”。这套 SolidWorks 二次开发模板要解决的正是这一步到下一步之间的空隙连接实例、判断文档状态、读写属性、导出图纸、异常兜底这些骨架代码在每个项目里都要重写一遍重写就意味着再踩一次同样的坑。它适合三类人设计岗出身、只会一点 C# 或 Python 但需要把重复劳动自动化的人做 PDM/PLM 或 ERP 对接、需要从模型里抽属性的人以及要给整个部门做批量出图工具的工程师。模板不是万能的它只能把容易出错的那部分固定下来业务逻辑仍然要你自己写。2. 模板骨架与环境打通三条技术路线怎么选、工程怎么建2.1 宏、独立 EXE、Add-in先按“谁按按钮”来选型三条路线的差别不在技术难度而在使用姿势。选错了后面每加一个功能都别扭。维度VBA 宏独立 EXEC# 控制台/WinFormAdd-in 插件部署方式存成 .swp随文档或目录分发拷贝 exe 目录双击运行需注册 COM随 SolidWorks 启动加载调试体验断点弱报错信息含糊完整断点、日志、单元测试可调试但每次改动要重启宿主能否脱离界面跑否可以静默批处理否跟着 SolidWorks 生命周期接外部数据源费劲读写 Excel 容易卡方便能直连数据库和表格方便但要做界面就得写 WinForm/WPF适合的任务一次性的小改动、验证 API 行为批量任务、定时任务、和外部系统对接给不写代码的同事用的常驻工具判断法则很土但很准这个功能一天跑一次以上、且不需要人盯着选独立 EXE需要在 SolidWorks 菜单里给同事点选 Add-in只是想确认某个 API 到底返回什么先录宏。模板本身通常只提供后两种的骨架宏那一层是拿来验证接口的。2.2 建工程与引用类型库Interop 的引法决定了后面报什么错C# 工程要引用两个库SolidWorks.Interop.sldworks.dll和SolidWorks.Interop.swconst.dll都在安装目录的api\redist下面。做 Add-in 还要多引一个SolidWorks.Interop.swpublished.dll。这里有两个必设项漏了任意一个编译能过、运行就翻车Embed Interop Types必须设为False否则类型会被内嵌跨版本跑的时候各种InvalidCastExceptionCopy Local建议设为False让它跟随本机安装避免目录里躺着一份旧版本 DLL 抢加载。!-- 模板 csproj 里最关键的两段路径按本机安装位置改 -- ItemGroup Reference IncludeSolidWorks.Interop.sldworks HintPathC:\Program Files\...\api\redist\SolidWorks.Interop.sldworks.dll/HintPath EmbedInteropTypesFalse/EmbedInteropTypes PrivateFalse/Private /Reference Reference IncludeSolidWorks.Interop.swconst HintPathC:\Program Files\...\api\redist\SolidWorks.Interop.swconst.dll/HintPath EmbedInteropTypesFalse/EmbedInteropTypes PrivateFalse/Private /Reference /ItemGroupEmbedInteropTypes控制是否把接口定义编进你的程序集设True时程序集自带一份类型副本换一台机器上的 SolidWorks 版本不同就会对不上Private等价于“复制到输出目录”设False是为了让运行时只用本机注册的那一份。这两条不是玄学是血泪经验换来的默认值。工程的目标平台必须和 SolidWorks 一致64 位宿主就只能是 x64。很多人卡在第一步就是这个AnyCPU 在 64 位系统上默认跑成 64 位进程没问题但一旦引用了别的 32 位组件就会被迫降级然后在Dispatch时抛“类工厂检索失败”。2.3 最小闭环连上实例、开一个文档、写一条属性模板里第一段能跑通的代码建议就干四件事拿到 Application 对象、打开一个指定路径的零件、对它写一条自定义属性、存盘关闭。跑通了后面所有功能都是在这四步之间加东西。using System; using System.Runtime.InteropServices; using SolidWorks.Interop.sldworks; using SolidWorks.Interop.swconst; public class SwSession : IDisposable { private SldWorks _swApp; private bool _ownsInstance; // 区分“连上的”还是“自己起的”决定要不要退出 public SldWorks App _swApp; public SwSession(bool createIfMissing true) { try { // 先尝试连接已经运行的实例避免用户开着界面时又起一个 _swApp (SldWorks)Marshal.GetActiveObject(SldWorks.Application); _ownsInstance false; } catch (COMException) { if (!createIfMissing) throw; // 没有实例才新建ProgID 一定带 .Application 后缀 _swApp (SldWorks)Activator.CreateInstance( Type.GetTypeFromProgID(SldWorks.Application)); _swApp.Visible true; _ownsInstance true; } // 告诉宿主“有命令在跑”防止用户中途手动操作打断批处理 _swApp.CommandInProgress true; } public void Dispose() { if (_swApp null) return; _swApp.CommandInProgress false; // 必须复位否则界面会一直卡在忙状态 if (_ownsInstance) _swApp.ExitApp(); Marshal.FinalReleaseComObject(_swApp); _swApp null; } }Marshal.GetActiveObject失败抛COMExceptionHRESULT 0x800401E3是正常分支不是错误抓到就降级去新建实例。CommandInProgress是个容易被忽略但很要紧的属性置true后 SolidWorks 会拒绝用户的手动交互批处理不会被打断但如果异常路径没复位界面会一直显示忙只能重启这就是很多人说的“跑一次就废一次”。_ownsInstance用来区分实例归属连上别人的实例就绝不能调ExitApp。接着是打开文档和写属性public ModelDoc2 OpenPart(string path) { int errors 0, warnings 0; var doc (ModelDoc2)_swApp.OpenDoc6( path, (int)swDocumentTypes_e.swDocPART, (int)swOpenDocOptions_e.swOpenDocOptions_Silent, // 静默打开不弹轻化提示 , // 配置名留空表示用上次激活的 ref errors, ref warnings); if (doc null) throw new Exception($打开失败 errors{errors} warnings{warnings} path{path}); return doc; } public void SetTextProperty(ModelDoc2 doc, string name, string value, string config ) { // 索引器参数是“配置名”空字符串代表文档级自定义属性 var mgr doc.Extension.CustomPropertyManager[config]; // Add3 的第四个参数必须是常量值不能写成裸数字跑循环 mgr.Add3(name, (int)swCustomInfoType_e.swCustomInfoText, value, (int)swCustomPropertyAddOption_e.swCustomPropertyReplaceValue); } public void SaveAndClose(ModelDoc2 doc) { int errors 0, warnings 0; doc.Save3((int)swSaveAsOptions_e.swSaveAsOptions_Silent, ref errors, ref warnings); _swApp.CloseDoc(doc.GetTitle()); }OpenDoc6的errors和warnings是出参返回null时这两个值就是唯一线索errors为 2 一般是文件不存在为 4 通常是版本不兼容为 8 多半是文件被占用。swOpenDocOptions_Silent会抑制轻化、外部参考等提示框批处理里必加否则一个模态框就能让整条流水线停在那里等人点。Add3的最后一个参数swCustomPropertyReplaceValue表示“存在就覆盖”用swCustomPropertyOnlyIfNew则只新增不覆盖做初始化脚本和做同步脚本要选不同的值。2.4 收尾比开头更重要释放、复位、别把宿主拖成僵尸进程批处理工具最常见的差评不是“功能不对”而是“跑完之后 SolidWorks 关不掉”。原因基本都在收尾COM 引用没释放干净、CommandInProgress没复位、ExitApp强行退出导致下次启动加载失败。模板里建议放一个统一的Cleanup方法把所有需要复位的东西列成清单finally里无条件调用一遍。不建议在循环里对每个对象都Marshal.ReleaseComObject再GC.Collect()那样性能会掉得很难看。合理做法是外层几个大对象Application、Document显式释放循环内的中间对象交给 GC只在批处理结束时调一次GC.Collect()和GC.WaitForPendingFinalizers()。真正需要担心的是 Excel Interop 那类“后台进程残留”SolidWorks 这边的对象只要不长期持有问题不大。3. 遍历与批处理把“一个个点”变成一棵树一次跑完3.1 装配递归GetComponents 的几个重载差别很大装配体的遍历不是“打开就完事”。AssemblyDoc.GetComponents(bool)返回的是Component2数组参数为true时只返回顶层为false时返回所有层级的平铺列表。很多人用false之后就以为不用递归了结果发现返回的组件里被压缩的、轻化的、外部参考丢失的GetModelDoc2()一律返回null而这些组件恰恰是最容易让脚本崩的地方。// 递归遍历装配树只处理“已加载且未压缩”的零件 public void Walk(Component2 comp, ActionModelDoc2, string onPart) { if (comp null) return; // 压缩件没有模型数据硬取会抛异常 if (comp.IsSuppressed()) return; var mdl comp.GetModelDoc2() as ModelDoc2; if (mdl ! null mdl.GetType() (int)swDocumentTypes_e.swDocPART) { onPart(mdl, comp.GetPathName()); } // GetChildren 返回的是 object[]必须逐个转 Component2 var children comp.GetChildren() as object[]; if (children null) return; foreach (var child in children) Walk(child as Component2, onPart); }IsSuppressed()要在取模型之前判断压缩状态下GetModelDoc2()会直接返回null。GetChildren()返回的是object[]而不是强类型数组这是 COM 互操作的常态用as转换比强制转换安全。如果装配层级很深递归深度可能超过栈改成显式栈的迭代写法更稳。至于轻化组件GetModelDoc2()有时能拿到一个“半加载”的文档读属性没问题改属性就可能写不进去后面 3.4 会专门处理。3.2 自定义属性的三种作用域文档级、配置特定、以及 Get4 与 Get6 的版本差属性写不进去或者写进去了但工程图不显示十有八九是作用域搞错了。SolidWorks 的自定义属性有三个层次文档级CustomPropertyManager[]、配置特定CustomPropertyManager[配置名]、以及工程图里通过链接引用零件属性。工程图标题栏链接的通常是“配置特定”属性你往文档级写图纸上就是空白。枚举值用途swCustomInfoText30图号、名称、材料等字符串swCustomInfoDate64日期格式随系统区域设置swCustomInfoNumber3整数类如数量swCustomInfoDouble5浮点如重量读属性时要注意版本差异。Get4、Get6、Get7在不同版本上参数个数不一样Get6多返回一个WasResolved用来判断链接属性是否解析成功。稳妥写法是优先用本机 API 帮助里存在的最高版本并在模板里封一层把版本差异挡在业务代码之外。// 封装读取返回是否解析成功解析失败时 Val 往往是空串 public (string Val, bool Resolved) ReadProperty(ModelDoc2 doc, string name, string config ) { var mgr doc.Extension.CustomPropertyManager[config]; mgr.Get6(name, false, out string val, out string resolved, out bool wasResolved); // UseCached false 表示读实时值改成 true 会拿到上次缓存调试时容易被骗 return (val, wasResolved); }UseCached参数是个陷阱设为true时读的是缓存值如果你刚写完还没重建读出来可能是旧值看着像“写入失败”。调试阶段一律用false批量跑的时候为了性能可以用true但要接受读到旧数据的风险。3.3 从表格驱动把 Excel 当数据源字典当索引实际项目里属性值几乎不会写在代码里都在一张 Excel 表里第一列图号后面若干列属性名。模板要做的是把这张表读成字典再按图号匹配零件写回去。// 用不依赖 Office 的库读表避免 Interop 的进程残留问题 public Dictionarystring, Dictionarystring, string LoadSpec(string xlsxPath) { var map new Dictionarystring, Dictionarystring, string(StringComparer.OrdinalIgnoreCase); using var wb new ClosedXML.Excel.XLWorkbook(xlsxPath); var ws wb.Worksheet(1); // 第一行是表头第一列是图号 var headers ws.Row(1).CellsUsed().Select(c c.GetString().Trim()).ToList(); foreach (var row in ws.RowsUsed().Skip(1)) { var code row.Cell(1).GetString().Trim(); if (string.IsNullOrEmpty(code)) continue; // 空行跳过不要写成空属性 var dict new Dictionarystring, string(); for (int i 1; i headers.Count; i) dict[headers[i]] row.Cell(i 1).GetString().Trim(); map[code] dict; // 重复图号后者覆盖前者需在预检里报出来 } return map; }用ClosedXML这类不依赖 Office 安装的库比Microsoft.Office.Interop.Excel稳得多——后者最常见的故障是进程残留导致文件被锁然后整个批处理卡在“文件正被另一进程使用”。表头用CellsUsed()而不是固定列数是为了兼容有人多加了一列备注。重复图号必须显式检测字典的覆盖行为会静默吃掉数据跑到最后发现少了几十个零件回头查很痛苦。匹配键的选择也有讲究用文件名匹配最快但改名就失效用图号属性匹配要先读一遍属性、多一次 IO用配置名匹配则只适用于多配置零件。模板里建议把“匹配键”做成配置项默认文件名出问题时能一键切换。3.4 只读、轻化、压缩那些“看起来打开成功”的文档批处理跑到一半报“对象引用未设置”八成是碰到只读文件或者轻化组件。判断文档是否可写不能只看文件系统属性SolidWorks 自己有一套状态// 打开后先做状态体检不通过就跳过并记日志别硬写 bool CanWrite(ModelDoc2 doc, out string reason) { reason ; if (doc.IsOpenedViewOnly()) { reason 以只读方式打开; return false; } if (doc.IsOpenedReadOnly()) { reason 文件只读或被占用; return false; } // 轻化状态下属性可读不可写必须先完全还原 if (doc.GetLightWeightState() ! 0) { reason 轻化未还原; return false; } return true; }IsOpenedViewOnly和IsOpenedReadOnly是两个不同概念前者是打开方式比如从 PDM 里以查看模式打开后者是文件权限或被其他进程占用。轻化状态用GetLightWeightState()判断非 0 时需要显式还原或者用OpenDoc6加swOpenDocOptions_OverrideDefaultLoadLightweight强制完整加载。这一步放在循环开头比写到一半抛异常再回滚要省事得多。4. 批量出图PDF 与 DXF 导出的参数、命名与队列4.1 导出前的三项确认图纸激活、视图更新、比例正确工程图批量导出最常见的失败不是代码错而是图纸本身状态不对某个视图被手动隐藏了、图纸处于“未更新”状态、或者某张图纸的比例被改动导致线宽异常。模板里应该在导出前做一次预检把有问题的图纸挑出来而不是让它静默导出一张空白 PDF。确认项包括当前激活的图纸页DrawingDoc.GetCurrentSheet返回的 sheet 是否为目标页、视图中是否有“未解析”的外部参考View.GetStatus或视图的 referenced document 是否存在、以及模型是否处于重建待处理状态。发现待重建时先调doc.EditRebuild3()不要指望导出时会自动重建——导出用的是当前显示状态不是重算后的状态。4.2 PDF 与 DXF 两条路ExportPdfData 与 SaveAs 的差别PDF 有专门的导出数据对象能控制导出哪些图纸、是否导出后自动打开DXF 则依赖另存为和本机的输出配置。// 导出指定图纸到 PDF public void ExportPdf(ModelDoc2 drawDoc, string pdfPath, string[] sheetNames) { int errors 0, warnings 0; var pdfData (ExportPdfData)_swApp.GetExportFileData( (int)swExportDataFileType_e.swExportPdfData); pdfData.ViewPdfAfterSaving false; // 批量时绝不能弹阅读器 pdfData.SetSheets( (int)swExportDataSheetsToExport_e.swExportData_ExportSpecifiedSheets, sheetNames); // 传 null 表示导出全部图纸 bool ok drawDoc.Extension.SaveAs( pdfPath, (int)swSaveAsVersion_e.swSaveAsCurrentVersion, (int)swSaveAsOptions_e.swSaveAsOptions_Silent, pdfData, // 关键把导出配置塞进去 ref errors, ref warnings); if (!ok) throw new Exception($PDF 导出失败 errors{errors} warnings{warnings}); }SetSheets的第二个参数在不同版本上接受字符串数组或单个字符串传之前先用object包一层能少踩一次类型转换异常。swSaveAsOptions_Silent抑制“是否覆盖”之类的确认框批量场景必加。SaveAs返回bool失败时errors是唯一线索日志里务必把它打出来。DXF 的路径要谨慎一些。整张图纸导出可以直接用SaveAs指定.dxf扩展名输出格式由文件名决定前提是本机已经配置好 DXF 输出映射文件.map和版本号。要按视图导出需要先drawDoc.ActivateView(viewName)激活目标视图再另存为——不同版本对这个组合的支持程度不一样动手前先用界面手工导一次确认本机行为再写进脚本别直接抄别处的代码。导出类型控制对象关键参数常见故障PDFExportPdfDataViewPdfAfterSaving、SetSheets导出空白页多为图纸未激活DXF另存为 用户偏好DXF 版本、字体映射文件中文变问号字体映射缺失DWG同 DXF同上图层名被截断4.3 命名规则与覆盖策略别让第二次运行覆盖第一次的结果命名规则不要硬编码在循环里做成模板字符串比如{图号}_{版本}_{图纸名}再配一个冲突策略覆盖、跳过、加后缀。默认策略建议是“跳过并记录”因为批处理最怕的是跑了两遍之后发现第一遍的结果被第二遍的错误数据覆盖了。// 文件名模板 冲突处理冲突时写日志而不是静默覆盖 string BuildPath(string dir, string pattern, Dictionarystring, string fields) { var name pattern; foreach (var kv in fields) name name.Replace({ kv.Key }, Sanitize(kv.Value)); // 去掉 \ / : * ? | var full Path.Combine(dir, name); if (File.Exists(full)) full Path.Combine(dir, ${Path.GetFileNameWithoutExtension(name)}_{DateTime.Now:HHmmss}.pdf); return full; }Sanitize这一步不能省属性里带斜杠、冒号很常见直接拼进路径会抛ArgumentException而且报错位置离真正原因很远。时间戳后缀能保证不覆盖但会制造大量重复文件实际项目里更稳妥的做法是先输出到临时目录跑完校验通过再整体替换。5. 避坑与排查五种跑不通的现场怎么定位5.1 连接与环境类的三个坑现象一控制台程序启动就抛COMException提示“由于以下错误: 800401E3 无法检索到组件”。原因有两个可能一是 SolidWorks 根本没启动GetActiveObject找不到运行中的实例二是你的进程位数和 SolidWorks 不一致64 位进程找不到 32 位注册的 COM 类。 解决把连接逻辑改成“先尝试连接、失败再新建”新建用Activator.CreateInstance(Type.GetTypeFromProgID(SldWorks.Application))同时把工程目标平台锁死为 x64别留 AnyCPU。如果两台机器一台能跑一台不能先比api\redist下的 DLL 版本是否一致。现象二编译通过运行时提示“无法将类型为 System.__ComObject 的对象转换为接口”。原因引用了错误的 Interop 版本或者Embed Interop Types被设成了True类型被内嵌后与实际注册的版本对不上。 解决把两个 Interop 引用的EmbedInteropTypes全部设为FalseCopy Local设为False确保运行时只用本机注册的那一份。清理bin目录重新编译一次很多时候旧 DLL 还躺在输出目录里。现象三第一次运行正常第二次启动 SolidWorks 界面一直处于忙碌状态点什么都没反应。原因上一次运行异常退出CommandInProgress停在true没复位或者ExitApp被强行调用后实例处于半死状态。 解决把CommandInProgress true/false放进try/finally异常路径也要复位ExitApp只在“自己创建的实例”上调用。已经卡住的机器只能手动结束进程所以模板里这段代码的正确性优先级最高。5.2 循环与数据类的两个坑现象四属性明明写进去了工程图标题栏还是空的。原因写的是文档级属性而工程图链接的是配置特定属性或者写完之后没有重建图纸引用的还是旧值。 解决先确认目标工程图链接的是哪个作用域在工程图里点标题栏文字看属性链接路径再决定CustomPropertyManager的索引器传空串还是配置名。写完属性后对相关文档调用EditRebuild3批量场景下统一在批次末尾重建一次比每个零件都重建快得多。现象五跑到第一百多个零件时程序崩溃或者卡住不动日志停在某个文件名上。原因COM 对象在循环里不断累积没释放或者碰到了以只读/轻化状态打开、写不进去的文档异常被吞掉后循环继续但状态已经乱了。 解决循环内不要长期持有ModelDoc2引用用完就置null每个文件外层套try/catch捕获后记日志、跳过、继续绝不让单个文件拖死整批。同时把 3.4 的状态检查前置宁可跳过也不要硬写。5.3 日志与最小复现把黑匣子拆开批处理工具没有日志就是黑匣子。模板里至少要记录四项时间戳、当前处理的文件全路径、每一步的返回值尤其是errors和warnings、以及异常堆栈。文件名单独占一列方便直接筛出失败项重跑。最小复现的做法是从失败清单里拿第一个文件写一个只处理这一个文件的三行脚本把OpenDoc6的errors、warnings打出来再一步步加属性写入和保存。九成的“批量才出错”最后都能归到单文件状态异常或者对象没释放这两类上。别一上来就怀疑 API 有问题先怀疑输入。6. 把模板改成自己的配置化、预检清单与一次收敛模板拿到手之后第一件事不是加功能而是把所有可变的东西从代码里抽出来。路径、文件前缀、导出格式、匹配键、冲突策略、并发数全部收进一个配置文件。这样换个项目只改配置不改代码也方便别人接手。{ rootDir: D:\\\\work\\\\models, filePattern: *.sldprt, matchKey: fileName, propertyMap: { 图号: Code, 名称: Name, 材料: Material }, export: { pdf: true, dxf: false, conflict: skip }, logPath: D:\\\\work\\\\logs\\\\batch.log }读取时给每一项都设默认值配置文件缺字段不能让程序起不来。这条经验是用一次次“换台机器就报空引用”换来的。预检清单建议固定成五项跑全量之前先拿五个文件过一遍检查项通过标准不通过时的动作实例连接能拿到 Application 且版本一致终止提示位数或安装问题输入清单文件数、去重后数量一致打印差异人工确认后再跑文档状态非只读、非轻化、未压缩跳过并写入待处理清单属性映射表头全部能在模板里找到对应缺项直接报错不猜输出目录可写、剩余空间足够终止别写到一半没空间最后一件事是关于验证批量跑完不要只看“程序没报错”要抽查三类样本——第一个文件、最后一个文件、以及中间随机一个。打开它们的属性确认值正确打开导出的 PDF 确认不是空白页再对比文件数量。这套动作我后来做成了脚本跑完自动抽样并生成一份对比表。有次赶一个交付节点我图省事跳过了预检直接对全量两百多个模型跑属性同步结果输入表里有一列表头写错了两个字脚本按“找不到就跳过”的逻辑静默忽略了那一列跑完看着一切正常第二天出图时才发现所有标题栏都少了材料。从那以后我每次改模板都强制先拿五个文件走一遍预检清单确认通过再放全量。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?