简介:这是一份面向机械设计开发者与 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 宏 | 独立 EXE(C# 控制台/WinForm) | Add-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,否则类型会被内嵌,跨版本跑的时候各种InvalidCastException;Copy Local建议设为False,让它跟随本机安装,避免目录里躺着一份旧版本 DLL 抢加载。
<!-- 模板 csproj 里最关键的两段,路径按本机安装位置改 --> <ItemGroup> <Reference Include="SolidWorks.Interop.sldworks"> <HintPath>C:\Program Files\...\api\redist\SolidWorks.Interop.sldworks.dll</HintPath> <EmbedInteropTypes>False</EmbedInteropTypes> <Private>False</Private> </Reference> <Reference Include="SolidWorks.Interop.swconst"> <HintPath>C:\Program Files\...\api\redist\SolidWorks.Interop.swconst.dll</HintPath> <EmbedInteropTypes>False</EmbedInteropTypes> <Private>False</Private> </Reference> </ItemGroup>EmbedInteropTypes控制是否把接口定义编进你的程序集,设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失败抛COMException(HRESULT 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, Action<ModelDoc2, 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["配置名"])、以及工程图里通过链接引用零件属性。工程图标题栏链接的通常是“配置特定”属性,你往文档级写,图纸上就是空白。
| 枚举 | 值 | 用途 |
|---|---|---|
swCustomInfoText | 30 | 图号、名称、材料等字符串 |
swCustomInfoDate | 64 | 日期,格式随系统区域设置 |
swCustomInfoNumber | 3 | 整数类,如数量 |
swCustomInfoDouble | 5 | 浮点,如重量 |
读属性时要注意版本差异。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 Dictionary<string, Dictionary<string, string>> LoadSpec(string xlsxPath) { var map = new Dictionary<string, Dictionary<string, 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 Dictionary<string, 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)激活目标视图,再另存为——不同版本对这个组合的支持程度不一样,动手前先用界面手工导一次,确认本机行为再写进脚本,别直接抄别处的代码。
| 导出类型 | 控制对象 | 关键参数 | 常见故障 |
|---|---|---|---|
ExportPdfData | ViewPdfAfterSaving、SetSheets | 导出空白页,多为图纸未激活 | |
| DXF | 另存为 + 用户偏好 | DXF 版本、字体映射文件 | 中文变问号,字体映射缺失 |
| DWG | 同 DXF | 同上 | 图层名被截断 |
4.3 命名规则与覆盖策略:别让第二次运行覆盖第一次的结果
命名规则不要硬编码在循环里,做成模板字符串,比如{图号}_{版本}_{图纸名},再配一个冲突策略:覆盖、跳过、加后缀。默认策略建议是“跳过并记录”,因为批处理最怕的是跑了两遍之后发现第一遍的结果被第二遍的错误数据覆盖了。
// 文件名模板 + 冲突处理,冲突时写日志而不是静默覆盖 string BuildPath(string dir, string pattern, Dictionary<string, 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全部设为False,Copy 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 确认不是空白页,再对比文件数量。这套动作我后来做成了脚本,跑完自动抽样并生成一份对比表。
有次赶一个交付节点,我图省事跳过了预检,直接对全量两百多个模型跑属性同步,结果输入表里有一列表头写错了两个字,脚本按“找不到就跳过”的逻辑静默忽略了那一列,跑完看着一切正常,第二天出图时才发现所有标题栏都少了材料。从那以后我每次改模板,都强制先拿五个文件走一遍预检清单,确认通过再放全量。希望帮到你。
本文还有配套的精品资源,点击获取