ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C#二次开发:一键批量合并DWG并挂载Xref

C#二次开发:一键批量合并DWG并挂载Xref 做CAD二次开发这些年最常被问到的需求里批量整理图纸绝对排前三。特别是那种几十个文件夹、上百个DWG图纸要汇总成一张总图的情况纯手工操作能从早加到晚还不一定能保证不漏。我这次直接用C#写了一个批量合并工具把多文件夹里的DWG图统一合并到当前图形里同时把外部参考Xrefs也一并挂载处理好。今天就把整个项目的设计思路、关键代码和踩过的坑完整记录下来供同样做AutoCAD二次开发的朋友参考。这套方案适合已经会一点C#、又做过CAD插件开发的人也适合那些被图纸汇总逼到想写脚本的工程师。只要你理解DWG文件本质上是“一张数据库表”后续所有操作就都好理解了——合并图纸其实就是把数据库里的块、图层、样式等记录搬进另一个数据库而Xrefs则是往这块表里挂一条指向外部文件的“引用路径”。下面我按自己的开发顺序来拆。1. 这个项目到底在解决什么问题1.1 图纸汇总的典型场景我遇到的需求主要来自两类人。一类是设计院或总包单位的工程师手上有建筑、结构、机电、暖通各个专业的图每个专业单独一个文件夹里面可能又有子文件夹按楼层或区域划分。他们要出一张“汇总布置图”把所有专业的平面图按坐标叠到一张总图上方便管线冲突检查。这时候最省事的办法不是复制粘贴而是直接把每张DWG作为外部参考挂进来或者把每张图作为块插入到当前模型空间。另一类是设备厂商也有类似场景——甲方提供一个总装图框架厂商要把自己做的零件图、部件图全部合并进来。这些零件图分散在几十个目录里还经常更新。如果每次都手工INSERT或者手动改Xref路径不仅累还容易漏掉最新的版本。批量自动扫描目录、自动合并、自动建立引用才是正解。1.2 为什么不能靠手工硬扛人力操作最大的问题是不可重复。手工方式下同一个目录扫描一遍可能要15到20分钟而且你没法保证每次都扫到同一批文件。尤其遇到文件名重名、图纸版本交错、Xref路径失效的情况手工处理很容易出错。其次纯手工无法做“增量更新”。比如这周合并过一次下周某个子文件夹里的图纸更新了如果全靠人工再去替换一次那维护成本就很高。写成C#工具之后我的做法是把“扫描文件夹合并DWG添加Xref”全流程脚本化以后只需重新跑一遍命令最新的图纸就全都进总图了。所以批量化是这类需求的最优解——节省时间还能把重复劳动变成可复用的工具。2. 动手前必须搞清楚的核心机制2.1 DWG合并的本质是什么很多人以为DWG就是一堆图形但用过AutoCAD .NET API之后就会发现DWG本质上是一个数据库文件。这个数据库里不只有块表记录存储图形实体还有图层表、线型表、文字样式表、标注样式表、尺寸样式表等等。所谓“合并DWG”从API层面看就是把源DWG数据库里的这些符号表记录复制到目标数据库里然后把实体数据注入到目标图形的指定空间。具体到.NET开发里核心类是Database。你打开一个DWG文件就是实例化一个Database对象读完之后把它Insert进当前活动文档的DatabaseAutoCAD内部会负责把源数据库中的符号表和实体都复制过去。这个过程中要注意块表记录ID的映射复制到目标库之后原来源库里的ObjectId不能直接用必须通过返回的新ID来引用。我建议所有刚接触这功能的人先自己画两个最简单的矩形框手动INSERT一次再对比一下块表内容就能直观理解“数据库合并”这个概念了。这个理解到位了后面写代码就顺了。2.2 外部参考Xrefs是怎么工作的Xref外部参考和“插入块”最大的区别在于数据存储方式。块插入后图形数据已经复制进当前DWG文件本身失效也不影响而Xref只是建立了一条指向外部DWG文件的引用路径当前图里保存的只是一个路径和块定义壳真正的内容每次打开时再加载。用API添加Xref时底层做两件事第一在当前图形的块表里创建一个“外部参考型块表记录”存的是文件路径第二在模型空间画布上添加一个BlockReference对象指向这个块表记录。你的C#代码要做的就是这两步所以Database.AttachXref方法负责前者手动追加BlockReference负责后者。正因为Xref不复制实际数据如果你要把一批散图全部“挂”到总图上就要注意相对路径问题。我只存绝对路径因为工具服务单机项目但如果图形要发给别人建议改用相对路径或者在代码里做路径替换。这个问题我放在后面章节细说。3. 环境与项目初始化3.1 需要引用的程序集在Visual Studio里新建一个类库项目目标框架建议选.NET Framework 4.7.2或4.8因为AutoCAD自带的托管程序集是面向.NET Framework的。不要选.NET Core/.NET 5不然运行时版本对不上加载会直接失败。需要添加以下几个引用acdbmgd.dll数据库核心程序集包含Database、BlockTable、BlockTableRecord等类。acmgd.dll应用层程序集包含Application、Document、Editor等类。这两个文件在AutoCAD安装目录里比如C:\Program Files\Autodesk\AutoCAD 2024。注意项目平台要设成x64因为AutoCAD从2013版开始基本都是64位进程AnyCPU模式下调用原生程序集时偶尔会有初始化异常我建议直接固定x64。3.2 调试环境的搭建常规的C#程序按F5就能跑但AutoCAD插件不行。我的习惯是编译成DLL然后在AutoCAD命令行敲NETLOAD加载这个DLL再运行我们定义的命令。如果想断点调试可以这样设置在Visual Studio里右键项目进入“调试-启动外部程序”把acad.exe路径填进去。启动之后AutoCAD会带调试器起来这时候再NETLOAD加载当前编译的DLL命中断点就非常方便。这里有个小坑acad.exe运行后会打开一个默认空白图纸如果你用了Application.DocumentManager.MdiActiveDocument直接获取当前文档务必保证这个文档存在否则会得到null。4. 核心实现批量合并与Xrefs挂载4.1 多文件夹递归扫描AutoCAD的Database只能处理单个文件所以批量操作的第一步是把所有需要合并的DWG路径收集起来。我的做法是用Directory.GetFiles递归搜索using System.IO; string rootFolder D:\Project\AllDrawings; string[] allFiles Directory.GetFiles(rootFolder, *.dwg, SearchOption.AllDirectories);只按扩展名过滤还不够实际工程里文件夹里经常混着一堆临时文件和备份文件比如~$开头的Office临时文件、.dwg.bak、.bak这些都要剔除。我一般是这样处理Liststring dwgFiles new Liststring(); foreach (string file in allFiles) { string fileName Path.GetFileName(file); if (fileName.StartsWith(~$)) continue; if (fileName.EndsWith(.bak, StringComparison.OrdinalIgnoreCase)) continue; if (fileName.EndsWith(.dwl, StringComparison.OrdinalIgnoreCase)) continue; if (fileName.EndsWith(.dwl2, StringComparison.OrdinalIgnoreCase)) continue; dwgFiles.Add(file); }顺便提醒一句Directory.GetFiles遇到没有权限的目录会直接抛异常整个程序就停了。我在项目里加了递归函数并配合try-catch跳过无法访问的子目录。虽然写法上比一行代码长但实际跑的稳定性提升了很多。递归扫描时还有一点要注意文件顺序。磁盘返回的顺序并不是文件系统里显示的顺序如果希望合并的顺序跟文件夹目录结构一致你可以先排序或者自己写一个按目录层级排序的函数。我的项目里因为总图有定位要求所以后来用了配置文件的方案指定哪些文件插到哪个坐标。4.2 单图合并的核心代码扫描完成后真正的重头戏是把单个DWG合并进当前图。我的合并模式是“作为块插入”也就是每张图纸在总图里变成一个图块引用。核心代码如下using Autodesk.AutoCAD.ApplicationServices; using Autodesk.AutoCAD.DatabaseServices; using Autodesk.AutoCAD.EditorInput; using Autodesk.AutoCAD.Geometry; using Autodesk.AutoCAD.Runtime; namespace DwgBatchMerge { public class MergeCommands { [CommandMethod(MERGEDWGS)] public void MergeCommand() { Document doc Application.DocumentManager.MdiActiveDocument; Database destDb doc.Database; Editor ed doc.Editor; // 选择根目录 using (System.Windows.Forms.FolderBrowserDialog fbd new System.Windows.Forms.FolderBrowserDialog()) { fbd.Description 请选择包含DWG文件的根目录; if (fbd.ShowDialog() ! System.Windows.Forms.DialogResult.OK) return; string[] files Directory.GetFiles(fbd.SelectedPath, *.dwg, SearchOption.AllDirectories); if (files.Length 0) { ed.WriteMessage(\n未找到任何DWG文件。); return; } // 开启事务批量合并 using (Transaction tx destDb.TransactionManager.StartTransaction()) { BlockTable destBt tx.GetObject(destDb.BlockTableId, OpenMode.ForRead) as BlockTable; BlockTableRecord destMs tx.GetObject( destBt[BlockTableRecord.ModelSpace], OpenMode.ForWrite) as BlockTableRecord; int count 0; foreach (string file in files) { if (TryInsertAsBlock(destDb, destMs, file)) count; } tx.Commit(); ed.WriteMessage($\n合并完成成功插入 {count} 个文件。); } } } private static bool TryInsertAsBlock(Database destDb, BlockTableRecord destMs, string filePath) { try { using (Database srcDb new Database(false, true)) { srcDb.ReadDwgFile(filePath, FileOpenMode.OpenForReadAndAllShare, false, ); string blockName Path.GetFileNameWithoutExtension(filePath); ObjectId blockId destDb.Insert(blockName, srcDb, true); BlockReference br new BlockReference(Point3d.Origin, blockId); destMs.AppendEntity(br); destMs.Database.TransactionManager.AddNewlyCreatedDBObject(br, true); } return true; } catch (System.Exception ex) { System.Windows.Forms.MessageBox.Show( $处理 {filePath} 时出错{ex.Message}); return false; } } } }这段代码里最核心的就是destDb.Insert(blockName, srcDb, true)。第三个参数true表示允许系统自动处理重名块如果源图里已经有同名块AutoCAD会自动加后缀。我把返回的ObjectId直接用来创建BlockReference这样每次合并的图纸都作为独立块出现在模型空间原点。有一个细节要注意AddNewlyCreatedDBObject(br, true)这个调用一定要在AppendEntity之后、事务提交之前。之前见过很多初学者漏掉这一步然后报“无法在未添加对象之前调用GetObject”的错误。定位方面我这里统一插到原点实际项目中你可以在插入后设置br.Position、br.ScaleFactors、br.Rotation。比如根据文件名解析楼层或专业批量设置插入点这样合并出来的总图更有条理。4.3 添加外部参考Xrefs除了“作为块插入”另一种合并思路是把源图作为外部参考挂到总图上。这种模式适合“保留可编辑源头”的场景源DWG更新后总图会自动更新。我封装了一个方法代码如下public static ObjectId AttachXrefToModelSpace(Database db, string filePath) { string blockName Path.GetFileNameWithoutExtension(filePath); using (Transaction tx db.TransactionManager.StartTransaction()) { BlockTable bt tx.GetObject(db.BlockTableId, OpenMode.ForRead) as BlockTable; BlockTableRecord ms tx.GetObject( bt[BlockTableRecord.ModelSpace], OpenMode.ForWrite) as BlockTableRecord; ObjectId xrefDefId Database.AttachXref(filePath, blockName); BlockReference br new BlockReference(Point3d.Origin, xrefDefId); ms.AppendEntity(br); tx.AddNewlyCreatedDBObject(br, true); tx.Commit(); return br.ObjectId; } }这里Database.AttachXref是静态方法它负责把外部文件注册为块表里的xref定义。注意这个方法的fileName参数推荐传绝对路径但如果你需要在不同机器间分发图纸最好在调用前自己把路径转成相对于总图所在目录的相对路径。另外一个容易踩的坑是“重复附着”。如果你脚本跑了两遍同一个Xref文件会被挂两次。我在工具里会先遍历一遍已有的外部参考判断是否存在同名定义如果存在就先DetachXref再重新挂。遍历逻辑可以参考下面这一段ExternalReferenceManager xrefMgr db.ExternalReferenceManager; foreach (ExternalReferenceInfo info in xrefMgr.GetExternalReferences()) { ed.WriteMessage($\nXref名称: {info.Name}, 路径: {info.Path}); }如果只是想解除外部参考而不删除块表记录可以用db.ExternalReferenceManager.DetachExternalReference(name)想彻底删除则可以考虑“绑定并分解”等操作。实际工作中“绑定Xref”的需求也很多把外部参考变成普通块定义的操作可以用xrefMgr.BindExternalReference(...)但是绑定后图层名、块名都会带上$0$之类的前缀这点要有心理准备。4.4 同时处理块合并与Xrefs的思路我的工具最后其实提供了一个配置选项用户可以决定当前文件夹是“合并为块”还是“挂为Xref”。因为有些专业图纸需要随总图发布合并成块最稳妥有些图纸还在频繁修改挂Xref最方便。这个模式我是在主命令里加了一个选择对话框把两种模式串联起来// 伪代码展示选择逻辑 if (mode MergeMode.Block) { foreach (string file in dwgFiles) TryInsertAsBlock(destDb, destMs, file); } else if (mode MergeMode.Xref) { foreach (string file in dwgFiles) AttachXrefToModelSpace(destDb, file); }如果既想合并块又想给某些大文件挂Xref即可把文件放在不同子目录代码里可以根据目录名判断走哪个分支。这个设计并不复杂但是可扩展性很高后面加需求很方便。5. 常见问题与排查技巧实录5.1 块名冲突与自动后缀用destDb.Insert(blockName, srcDb, true)进行合并时true允许重名时自动加后缀比如同样叫“平面图”的两张图第二张会变成“平面图1”或“平面图$0$”。结果虽然不报错但总图里会冒出一堆名字不像人起的块定义后续查找定位很痛苦。我的处理办法是在插入之前先检查目标数据库里是否已有同名块如果有就强制给本次文件加上专业前缀比如“结构_平面图”和“建筑_平面图”。虽然不如手工起名灵活但总比“平面图$0$1”强得多。还有一个细节AutoCAD的块名最长255个字符但实际不建议用太长的文件名因为块名太长在块编辑器里显示很难看。如果文件名本身就很长我会在代码里做一次截断。5.2 图形版本兼容问题不同版本的AutoCAD保存的DWG文件格式不同。用2024的API去读2010版的DWG通常没问题但用2024的API去读2025版保存的DWG会报“图形版本过新”。这个问题在大型项目里很常见特别是团队里有人用新版本保存了图纸。解决办法有两个方向一是要求上游统一保存为兼容版本二是在工具里捕获异常后提示用户手动另存。我实测下来使用srcDb.ReadDwgFile时遇到版本太新的图会抛Autodesk.AutoCAD.Runtime.Exception错误代码是eNotForWrite或eOpenFailed之类的不友好。所以我在工具里加了版本预检读取每个文件的密码头信息判断版本。用HostApplicationServices.Current读取二进制头过于繁琐最简单的方式是直接用new Database(false, true)尝试ReadDwgFile失败就记录文件名并跳过最后输出一个未处理列表。这样既不会因为一张坏图导致全部失败也能定位问题文件。5.3 事务嵌套与文档锁我在第一版代码里犯过最典型的错误在主事务里又开了一个独立事务结果AutoCAD直接报“事务不匹配”或者“无法在没有活动文档的情况下操作”。现在我的原则是一个命令只开一个顶层事务所有数据库操作都塞进这个事务里项目里所有对数据库的读写都在同一个Transaction里完成。如果某个操作非常耗时可以在逻辑上拆成多个命令分批执行而不是嵌套事务。另外AutoCAD的数据库在后台打开文件时可能没有获得文档锁。如果你直接ReadDwgFile一个正在被其他图纸引用的DWG也有可能出现文件占用冲突。我在ReadDwgFile的第二个参数用的FileOpenMode.OpenForReadAndAllShare这个模式允许其他进程同时读取该文件实测下来很少遇到权限冲突。5.4 性能优化与超大图纸批量合并几百张图纸不是一两点代码就能跑完的性能坑主要出现在三处一是每张图都维护完整符号表插入时开销大二是插入后在模型空间创建块引用时如果坐标很分散AutoCAD的图形数据库会做大量几何更新三是每次事务提交或命令结束都会触发全图重生成。我的优化习惯是插入过程一律不做屏幕刷新用doc.LockDocument()锁定文档并在批处理期间把doc.Editor的绘图输出降到最低。如果图纸量真的很大可以关掉显示用Application.DocumentManager.MdiActiveDocument.Editor.Regen()在最后刷一次。我曾经处理过一个包含500多张图纸的项目每次合并耗时接近40分钟后来发现瓶颈是每次ReadDwgFile都要初始化字体和线型缓存于是改用进程内Database缓存池把同一文件夹里打开过相同字体定义的图复用字体映射时间降到15分钟左右。这个优化方案细节不少这里不展开但方向你是可以试试的。5.5 路径与引用失效挂Xref最痛苦的问题就是换台电脑后路径失效。我在上面提到可以用相对路径解决但在批量工具里需要把图纸放到同一个总目录下相对路径才有意义。我的做法是读取文件夹后先计算总图目录和源文件目录的相对关系然后生成相对路径Uri totalUri new Uri(totalFolder); Uri fileUri new Uri(filePath); string relativePath Uri.UnescapeDataString( totalUri.MakeRelativeUri(fileUri).ToString());如果是挂Xref把这个relativePath传给Database.AttachXref。这样生成的DWG拷贝到任何机器只要保持文件夹结构不变外部参考就不会断。如果你用的是“合并为块”模式就不会受Xref路径影响因为所有数据已经复制到总图里了这也是很多归档场景必须用块合并的原因。6. 后续扩展与个人体会最后分享两个我在这套工具基础上加过的小功能抛砖引玉。第一个是“增量合并”。我维护了一个配置文件记录了每个源文件的最新修改时间。下一轮合并时只处理修改时间有变化的文件其他文件跳过。这样在项目频繁更新阶段能明显减少合并等待时间。第二个是“图层改名规则”。很多外单位的图纸图层不规范直接合并进来会污染总图的图层列表。我在插入完成后遍历一遍块表记录里的所有实体把图层名统一加上前缀比如“外来图_图层名”再配合图层状态管理器统一冻结掉外来图不需要的图层。处理完总图的图层面板会干净很多。这个工具我用了大半年从最开始只能合并几十张图到现在能稳定处理几百张图的项目。踩过最多的坑其实不是代码本身而是DWG文件里的不可控因素——版本太新、图层被锁定、代理实体、文件被占用等等。做这类批量工具最重要的不是把命令写得花哨而是“不因为一张乱图挂掉整个任务”该跳过的跳过该记录的记录最后给用户一份清晰的日志。等到你手里的批量命令能安安静静跑完不报错你的CAD二次开发水平就真正过关了。
返回列表