SolidWorks二次开发模板:C#工程化插件骨架设计
简介:本资源是一套开箱即用的SolidWorks二次开发C#模板工程,面向机械设计工程师、CAD自动化开发者及具备基础C#编程能力的初学者,旨在解决SolidWorks功能定制化门槛高、插件开发环境搭建复杂、COM接口调用不熟悉等实际问题。压缩包共99个文件,涵盖18个核心C#源码(.cs)、2个Visual Studio解决方案(.sln)与项目文件(.csproj)、34个运行依赖DLL及5个资源文件(.resx/.bmp),完整复现了SwCSharpAddin1插件项目的标准目录结构——含事件处理(EventHandling.cs)、界面窗体(Form1/XtraForm1)、插件主类(SwAddin.cs)、工具栏图标与配置文件,支持快速编译部署为COM Add-in。资源包大小10.24MB,结构清晰、注释规范,已为开发者预置SDK引用、事件注册、命令绑定与错误日志框架。目前已有730人学习下载,可直接用于SolidWorks 2016–2022版本的插件开发实践,显著缩短从零搭建到功能验证的周期。
1. SolidWorks二次开发模板:不是“写个宏就完事”,而是搭好可复用、能交付、经得起工程验收的插件骨架
你有没有遇到过这种场景:客户提了个需求——“在装配体里批量给所有螺钉加防松标记”,你吭哧吭哧写了300行VBA宏,双击运行能跑通;结果一交到产线同事手上,对方点开就报错“找不到引用库”,或者改了SolidWorks版本后直接白屏;再一问,原来宏里硬编码了D:\Projects\temp\mark.slddrw路径,还依赖一个没打包的.dll;更糟的是,三个月后客户要加“按紧固力矩分级着色”功能,你翻出原始代码,发现变量命名全是a,b1,xx2,连自己都看不懂逻辑在哪一层。这不是个别现象——大量SolidWorks现场开发卡在“能跑就行”的临界点,缺的不是语法能力,而是一套开箱即用、结构清晰、错误可控、升级有路的工程化起点。这个“SolidWorks二次开发模板”就是为此而生:它不教你如何画螺栓,而是给你一套已预置项目结构、自动注册机制、日志兜底、异常隔离、配置驱动的C#/.NET插件骨架。适合机械设计自动化推进组、PLM实施团队、以及想把个人脚本升级为部门级工具的资深工程师。它解决的不是“能不能做”,而是“能不能稳定交付、能不能快速迭代、能不能让别人也维护”。
2. 技术栈与工程结构:为什么选.NET Framework 4.7.2 + C# + SwAddin + NuGet包管理
SolidWorks二次开发长期存在技术路线撕裂:VBA轻量但难维护,C++ SDK强大但编译链复杂,Python通过COM调用又受限于GIL和部署环境。这个模板选择.NET Framework 4.7.2(非.NET Core/.NET 5+)是经过产线实测验证的务实决策——SolidWorks 2018–2024所有主流版本均原生支持该框架,且其COM互操作层(SolidWorks.Interop.sldworks等P/Invoke封装)成熟稳定,无运行时兼容性黑匣子。更重要的是,它天然支持Windows服务式插件(SwAddin),能实现菜单注入、命令响应、文档事件监听三位一体,比宏更贴近原生体验。
2.1 模板目录结构:6大核心文件夹定义职责边界
项目采用分层架构,拒绝“所有代码塞进一个.cs文件”的反模式。解压后你会看到如下结构(已剔除bin/obj等生成物):
SolidWorksAddInTemplate/ ├── AddIn/ # 插件主入口:SwAddin继承类、注册/注销逻辑、全局事件总线 ├── Commands/ # 命令模块:每个ICommand实现类对应一个菜单项(如“批量标注”) ├── Models/ # 数据模型:与SolidWorks对象解耦的业务实体(如BoltSpec、MarkRule) ├── Services/ # 服务层:封装SolidWorks API调用(如DrawingService.CreateView)、错误重试、事务包装 ├── Utils/ # 工具集:通用扩展方法(SldPartDoc.GetMassProperties())、路径解析、配置读取 └── Properties/ # 程序集信息、注册表键值、版本号(关键!影响安装路径和注册行为)提示:
AddIn/SwAddIn.cs是整个插件的“心脏”,它必须继承SolidWorks.Interop.sldworks.SwAddin并实现ConnectToSW()和DisconnectFromSW()。模板中已预置Application.SldWorks实例缓存、Document事件订阅模板、以及OnIdle()轮询钩子——这些不是可选项,而是避免UI卡死、资源泄漏的刚需。
2.2 核心依赖项:NuGet包已锁定版本,杜绝“本地能跑线上炸”
模板通过packages.config明确定义以下依赖(全部为官方NuGet源可获取):
| 包名 | 版本 | 作用 | 是否必需 |
|---|---|---|---|
SolidWorks.Interop.sldworks | 33.0.0.0 | SolidWorks主API互操作程序集 | ✅ 必需 |
SolidWorks.Interop.swconst | 33.0.0.0 | 常量枚举(swDocumentTypes_e等) | ✅ 必需 |
Microsoft.Extensions.Logging.Abstractions | 5.0.0 | 轻量日志接口,便于后期替换Serilog等 | ⚠️ 推荐 |
Newtonsoft.Json | 13.0.3 | 配置文件序列化(如rules.json) | ⚠️ 推荐 |
注意:sldworks和swconst版本号(33.0.0.0)对应SolidWorks 2023 SP0。若你使用2022,请手动降级至32.0.0.0(需从SolidWorks安装目录api/redist复制对应DLL并更新引用)。这是新手最易翻车的第一步——不要试图用高版本DLL兼容低版SolidWorks,会触发System.Runtime.InteropServices.COMException且无有效堆栈。
2.3 注册机制:不靠regsvr32,用注册表脚本+InstallUtil双保险
模板摒弃手工注册DLL的玄学操作。它提供两套注册方案:
- 开发调试期:运行
RegisterForDebug.bat(内含regasm /codebase /tlb命令),将插件注册到当前用户HKEY_CURRENT_USER\Software\SolidWorks\AddIns64下; - 正式部署期:使用
InstallUtil.exe执行InstallUtil.exe /i SolidWorksAddInTemplate.dll,自动写入HKEY_LOCAL_MACHINE,支持多用户环境。
关键参数说明:
regasm /codebase /tlb SolidWorksAddInTemplate.dll/codebase:强制注册DLL绝对路径(避免GAC冲突);/tlb:生成类型库(.tlb文件),供SolidWorks加载时解析COM接口;- 若提示“无法注册类型”,大概率是.NET Framework未正确安装或DLL平台目标(x64/x86)与SolidWorks位数不匹配(SolidWorks 2018+全为x64,务必设为
Platform Target: x64)。
3. 核心模块拆解:从菜单点击到模型修改的完整链路
我们以一个典型功能——“为当前零件添加自定义属性(Custom Property)”为例,追踪从UI交互到SolidWorks底层操作的每一步。这不仅是功能演示,更是理解模板设计哲学的关键切口。
3.1 命令注册:Commands/AddCustomPropertyCommand.cs—— 让菜单“活”起来
public class AddCustomPropertyCommand : ICommand { public string Name => "AddCustomProperty"; public string Title => "添加自定义属性"; public string Tooltip => "为当前零件添加[设计者]、[审核日期]字段"; public void OnCommand() { // 1. 获取当前活动文档(安全检查) var doc = SwAddIn.Instance.Application.ActiveDoc; if (doc == null || doc.GetType() != (int)swDocumentTypes_e.swDocPART) { MessageBox.Show("请先打开一个零件文件"); return; } // 2. 构造业务参数(解耦UI与逻辑) var props = new Dictionary<string, string> { ["设计者"] = Environment.UserName, ["审核日期"] = DateTime.Now.ToString("yyyy-MM-dd") }; // 3. 委托给服务层执行(非阻塞UI线程) try { var service = new CustomPropertyService(SwAddIn.Instance.Application); service.AddProperties(doc, props); MessageBox.Show("自定义属性添加成功!"); } catch (Exception ex) { // 4. 统一日志记录(见Utils/Logger.cs) SwAddIn.Instance.Logger.LogError(ex, "AddCustomPropertyCommand.OnCommand failed"); MessageBox.Show($"操作失败:{ex.Message}"); } } }逻辑说明:
ICommand接口是模板定义的命令契约,所有菜单项必须实现它,确保统一生命周期管理;SwAddIn.Instance.Application是单例持有的SolidWorks Application对象,避免重复获取导致性能损耗;CustomPropertyService被设计为无状态服务类,可被多个命令复用,符合单一职责原则;- 异常捕获后调用
Logger.LogError()而非裸抛,保证错误可追溯(日志默认输出到%APPDATA%\SolidWorksAddInTemplate\logs\)。
3.2 服务实现:Services/CustomPropertyService.cs—— 封装SolidWorks API的“安全壳”
public class CustomPropertyService { private readonly SldWorks _swApp; public CustomPropertyService(SldWorks swApp) { _swApp = swApp ?? throw new ArgumentNullException(nameof(swApp)); } public void AddProperties(ModelDoc2 model, Dictionary<string, string> properties) { // 关键:使用Transaction确保原子性(失败则回滚,不残留半成品) using (var transaction = new Transaction(_swApp)) { foreach (var kvp in properties) { // SolidWorks API要求属性名必须为ASCII,值可UTF-8 var propName = Encoding.ASCII.GetString(Encoding.ASCII.GetBytes(kvp.Key)); var propValue = kvp.Value; // 调用底层API(此处为简化,实际含重试逻辑) var result = model.Extension.SetCustomProperty3( propName, (int)swCustomInfoType_e.swCustomInfoText, propValue, (int)swCustomInfoOptions_e.swCustomInfoDeleteIfExists); if (result != 1) // 返回1表示成功 { throw new InvalidOperationException($"设置属性 '{propName}' 失败,返回码: {result}"); } } transaction.Commit(); // 显式提交事务 } } }参数说明:
model.Extension.SetCustomProperty3()是SolidWorks官方推荐的自定义属性设置方法(替代已废弃的SetCustomProperty2);- 第三个参数
swCustomInfoOptions_e.swCustomInfoDeleteIfExists确保同名属性被覆盖而非追加,避免脏数据; Transaction类是模板自研的简易事务包装器(非SolidWorks原生),内部通过_swApp.CommandInProgress = true/false模拟事务边界,防止用户中途关闭文档导致状态不一致。
3.3 配置驱动:Utils/ConfigManager.cs—— 让规则脱离代码,交给配置文件
模板预置appsettings.json用于管理可变参数:
{ "CustomPropertyRules": { "AutoAddOnSave": true, "RequiredFields": ["设计者", "版本号", "校验人"], "DefaultValueMap": { "版本号": "V1.0", "校验人": "QA-Team" } }, "Logging": { "Level": "Information", "MaxFileSizeKB": 10240 } }ConfigManager通过强类型绑定读取:
public static class ConfigManager { private static readonly IConfigurationRoot _config = new ConfigurationBuilder() .SetBasePath(AppDomain.CurrentDomain.BaseDirectory) .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) .Build(); public static CustomPropertyConfig CustomProperty => _config.GetSection("CustomPropertyRules").Get<CustomPropertyConfig>(); } public class CustomPropertyConfig { public bool AutoAddOnSave { get; set; } public List<string> RequiredFields { get; set; } = new(); public Dictionary<string, string> DefaultValueMap { get; set; } = new(); }注意:
reloadOnChange: true使配置文件修改后无需重启SolidWorks即可生效(需配合IOptionsMonitor<T>模式,模板中已预留接入点)。这是产线快速响应规则变更的核心能力。
4. 常见问题排查:那些让你加班到凌晨的“经典翻车现场”
SolidWorks二次开发的调试环境极其特殊——它运行在SolidWorks主进程内,IDE断点常失效,错误信息常被吞掉。以下是模板用户高频反馈的5个真实问题,附带血泪经验总结:
4.1 现象:插件菜单显示为灰色不可点击
原因:AddIn/SwAddin.cs中ConnectToSW()方法未正确设置AddInEnabled = true,或ISwAddin.ConnectToSW()返回false未处理。
解决:检查ConnectToSW()末尾是否遗漏return true;;若需条件启用(如仅限特定SolidWorks版本),必须显式返回true,否则SolidWorks认为插件初始化失败而禁用菜单。
4.2 现象:调用ModelDoc2.Extension.SelectByID2()选中特征失败,返回false
原因:SolidWorks坐标系与.NET坐标系Y轴方向相反,传入的屏幕坐标未做转换;或特征名称含空格/特殊字符未转义。
解决:使用模板提供的ScreenPointConverter.ToSolidWorksSpace()方法转换坐标;特征名用Regex.Replace(name, @"[^a-zA-Z0-9_]", "_")清洗后再传入。
4.3 现象:插件安装后SolidWorks启动变慢,甚至崩溃
原因:SwAddin.DisconnectFromSW()中未释放Document事件订阅(如doc.OnSaveNotify += OnSaveHandler),导致内存泄漏;或Dispose()未调用GC.SuppressFinalize(this)。
解决:模板SwAddin.cs已内置UnsubscribeAllEvents()方法,在DisconnectFromSW()中强制调用;所有事件处理器必须用+=注册,-=注销,禁止匿名函数(无法注销)。
4.4 现象:中文路径下Assembly.LoadFrom()加载DLL失败,报FileNotFoundException
原因:SolidWorks进程工作目录(Working Directory)默认为SolidWorks安装路径(如C:\Program Files\SOLIDWORKS Corp\SOLIDWORKS),非插件所在目录。
解决:改用Assembly.Load(File.ReadAllBytes(assemblyPath))绕过路径限制;或在ConnectToSW()开头执行Directory.SetCurrentDirectory(AppDomain.CurrentDomain.BaseDirectory)重置工作目录。
4.5 现象:多线程调用SolidWorks API(如后台导出PDF)随机崩溃
原因:SolidWorks COM对象非线程安全,所有API调用必须在主线程(STA线程)执行。
解决:模板Utils/ThreadHelper.cs提供InvokeOnMainThread(Action action)方法,强制回调到UI线程:
ThreadHelper.InvokeOnMainThread(() => { var pdfPath = Path.Combine(tempDir, "output.pdf"); model.Extension.SaveAs(pdfPath, (int)swSaveAsOptions_e.swSaveAsOptions_Silent, ...); });5. 二次开发实战:如何在30分钟内新增一个“BOM自动编号”功能
现在我们动手改造模板,添加一个真实产线需求:“为装配体BOM中的每个零部件按层级深度自动编号(如1.1, 1.2, 2.1.1)”。这不是概念演示,而是可立即投入使用的增量开发流程。
5.1 步骤1:定义命令入口(5分钟)
在Commands/下新建AutoNumberBomCommand.cs:
public class AutoNumberBomCommand : ICommand { public string Name => "AutoNumberBom"; public string Title => "BOM自动编号"; public string Tooltip => "按装配层级为BOM项生成唯一编号"; public void OnCommand() { var doc = SwAddIn.Instance.Application.ActiveDoc; if (doc?.GetType() != (int)swDocumentTypes_e.swDocASSEMBLY) { MessageBox.Show("请在装配体中使用此功能"); return; } try { var service = new BomNumberingService(SwAddIn.Instance.Application); var result = service.GenerateNumbering((AssemblyDoc)doc); MessageBox.Show($"已为{result.Count}个零部件编号\n示例:{string.Join(", ", result.Take(3))}"); } catch (Exception ex) { SwAddIn.Instance.Logger.LogError(ex, "AutoNumberBomCommand failed"); MessageBox.Show($"编号失败:{ex.Message}"); } } }关键动作:
ICommand实现类只需30行,业务逻辑全委托给服务层,保持命令层极度轻量。
5.2 步骤2:实现服务逻辑(15分钟)
在Services/下新建BomNumberingService.cs,核心是递归遍历装配体结构:
public class BomNumberingService { private readonly SldWorks _swApp; private int _counter = 0; public BomNumberingService(SldWorks swApp) => _swApp = swApp; public Dictionary<string, string> GenerateNumbering(AssemblyDoc assembly) { var numberingMap = new Dictionary<string, string>(); var rootComp = assembly.GetRootComponent3(true); TraverseComponent(rootComp, "", numberingMap); return numberingMap; } private void TraverseComponent(Component2 comp, string prefix, Dictionary<string, string> map) { if (comp == null) return; // 生成编号:根组件为"1",子组件为"1.1", "1.2"... var number = string.IsNullOrEmpty(prefix) ? (++_counter).ToString() : $"{prefix}.{++_counter}"; // 获取零部件路径(唯一标识) var path = comp.GetPathName(); if (!string.IsNullOrEmpty(path)) { map[path] = number; } // 递归子装配体 var subAsm = comp.GetModelDoc2() as AssemblyDoc; if (subAsm != null) { var children = subAsm.GetComponents(false); for (int i = 0; i < children.Length; i++) { var child = children[i] as Component2; TraverseComponent(child, number, map); // 传递当前编号作为前缀 } } } }逻辑说明:
TraverseComponent采用深度优先遍历,确保编号顺序与BOM展开顺序一致;- 使用
Component2.GetPathName()获取绝对路径作为key,避免同名零件冲突; prefix参数实现层级编号拼接,无需额外栈结构,空间复杂度O(1)。
5.3 步骤3:注册到UI并测试(10分钟)
- 打开
AddIn/SwAddin.cs,在CreateCommandManager()方法中添加:_commandManager.RegisterCommand(new AutoNumberBomCommand()); - 重新编译项目,运行
RegisterForDebug.bat; - 启动SolidWorks → 打开任意装配体 → 查看“工具”菜单 → 点击“BOM自动编号”;
- 检查弹窗结果,并验证SolidWorks任务窗格中BOM表格是否同步更新(需配合
IEdmBomTable接口,此处为简化演示)。
提示:若需持久化编号到自定义属性,只需在
TraverseComponent中追加:var model = comp.GetModelDoc2(); if (model != null) model.Extension.SetCustomProperty3("BOM编号", (int)swCustomInfoType_e.swCustomInfoText, number, 0);
从创建命令类、编写服务、到注册启用,全程无需修改模板核心框架代码。这就是工程化模板的价值——新功能开发=写业务逻辑+注册命令,其他一切由骨架兜底。从那以后我每次接到产线需求,都强制走一遍“Commands→Services→注册”三步,哪怕只是临时脚本,也先搭好这个架子。因为我知道,今天省下的10分钟,会在三个月后某个深夜的紧急补丁里,变成3小时的定位时间。希望帮到你。
本文还有配套的精品资源,点击获取