免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

C# + AutoCAD .NET API 批量合并DWG并处理外部参照的完整方案

C# + AutoCAD .NET API 批量合并DWG并处理外部参照的完整方案 干这行的朋友应该都遇到过这种需求手上几十张DWG分散在好几个文件夹里要合成一张总图图里还挂着一堆外部参照Xrefs。手工一个个打开、复制、粘贴先不说效率光是处理外部参照就能把人搞崩溃。我之前在项目里就是用C#基于AutoCAD .NET API写了个批量合并DWG的工具把多文件夹扫描、DWG合并、Xrefs处理一条龙搞定。这篇文章把这个工具的完整思路、关键代码和踩坑经历都整理出来给做CAD二次开发、土木BIM、机械制图以及要批量整理图纸的朋友一个可以直接抄作业的参考。1. 需求拆解与方案选型1.1 标题背后到底藏着几个需求单看“c# cad 合并dwg 多文件夹合并 添加外部参考Xrefs”这串关键词实际上至少拆出了三个独立需求。第一是合并DWG本身。它和普通的手工INSERT不一样指的是程序化地把多个DWG文件内容汇聚到一个DWG里而且不仅仅是简单复制粘贴要考虑块定义、图层、样式这些依赖项是否跟着一起过来。第二是多文件夹扫描。图纸可能分布在总包、分包、专业负责人各自的目录里有些还有嵌套子目录不能靠用户手动一个个选。第三是外部参照Xrefs的处理。这才是最容易出问题的地方——合并后的文件要么保留Xref链接关系要么干脆把Xref绑死成内部块让文件完全自包含。这三件事看着不复杂合在一起就非常考验对AutoCAD数据库模型的理解了。这种需求最常见的现实场景是竣工图归档要把建筑、结构、机电各专业的图纸拼成一张总图送审图要求所有外部参照必须绑定不能给审图老师一个链接一堆外部文件的半成品还有一种是装配图汇总多台设备各自有独立图纸需要统一落到一张总图里。理解了这些业务背景写代码时才知道该往哪个方向设计。1.2 为什么不能靠手工完成可能有人会问合并DWG为什么不直接在CAD里打开一张图然后用INSERT命令把其他图插进来几十张图的话手工操作确实也能做完但问题在于三个环节。第一是排序和命名几十张图哪张在前哪张在后块名叫什么手工敲很容易出错。第二是Xrefs如果源图里的外部参照路径是相对路径INSERT进来之后新文件一挪位置外部参照全失效图面上一堆红叉。第三是重复定义不同图纸里可能有同名的图层、同名的块比如都叫“S-GONGZHE”内容却不一样手工合并时CAD只会保留先加载的那个后面的图全部错乱。这些恰恰是程序批量处理的强项——可控、可重复、可追溯。1.3 方案选型为什么是C#而不是LISP或Python我在实际项目里试过几种方案最后选了C# AutoCAD .NET API。下面是几个常见方案的横向对比方案优点短板LISPCAD内置、上手快、适合小规模脚本文件遍历、界面、异常控制太弱批量处理几十张图容易卡死Python (pyautocad / win32com)生态好、数据处理方便依赖COM服务器、批量操作速度慢、版本兼容性一般C ObjectARX性能最强、功能最全开发成本高内存管理风险大C# .NET API开发效率高、API覆盖全、托管内存更安全需要理解AutoCAD数据库模型个别API有版本差异C#的托管环境做批量任务非常舒服比如遍历文件夹、写日志、弹窗选择目录这些都是LISP的弱项。而且AutoCAD从2006版开始提供完整的托管APIAcDbMgd.dll、AcMgd.dll这套类库和ObjectARX C接口基本一一对应做DWG级别的批量操作完全够用。这里我用的开发环境是Visual Studio 2019 .NET Framework 4.8CAD版本为2020/2023平台目标设为x64引用的dll设置“复制本地False”。2. 核心原理先把DWG数据库模型说透2.1 DWG文件在API眼里到底是个什么结构很多刚接触AutoCAD .NET API的人会被Database、Transaction、BlockTable这些概念绕晕。用一个日常类比来解释一个DWG文件就像一个大的文件柜文件柜里有几个固定的抽屉也就是符号表。符号表里最重要的是块表BlockTable块表里又有若干块表记录BlockTableRecord比如模型空间、图纸空间、以及各种用户定义的块。模型空间可以理解成一张大桌子平时画的东西都摊在这张桌子上块表记录就是桌面上的文件夹把一组实体打包成一个整体。Database代表整个文件柜Transaction则是操作这个文件柜时的“日志本”——每个改动都要记录在案最终一次性提交。合并DWG的本质就是从源文件柜里把需要的内容搬到目标文件柜里。但是光搬实体不够实体依赖的图层、线型、标注样式、块定义必须一起搬否则目标文件打开后全是“未知图层”“无法解析的代理对象”。这也是为什么我强烈建议用API里现成的高层方法而不是遍历实体AppendEntity硬拷。2.2 合并的核心操作Database.Insert和WblockClone.NET API里合并DWG有几种典型方式各有利弊。一种是逐实体复制也就是遍历源块表记录里的每个实体Clone之后附加到目标块表记录里。这种方式最灵活可以逐个调整坐标、过滤对象但依赖项要自己处理比如块引用内部引用的块表记录不会自动搬过来写起来很累。第二种是WblockCloneObjects按ObjectId集合做深度克隆。它可以指定克隆后归属的目标块表记录也能自动把图层、块定义等依赖项一起拷贝但调用参数相对复杂还要处理IdMapping映射关系。第三种就是我这次用的Database.Insert把整个源数据库作为一块插入到目标数据库。这个方法本质上是对WblockClone的封装直接以源数据库的模型空间为内容创建一个新的块表记录然后返回这个块表记录的ObjectId。后面再创建一个BlockReference指向它放到目标模型空间里就实现了“把一张图作为块插入”。这种方式代码量最少、依赖项处理最省心特别适合把每张DWG变成总图里的一个块这种需求。2.3 外部参照Xrefs为什么在合并时特别麻烦外部参照在日常出图里用得非常普遍最常见的是把户型图、总平面作为底图引用进来。Xref的精髓在于“懒加载”——主图里存的只是一个路径和引用标记真正的几何实体都在被参照的那个DWG文件里。AutoCAD打开主图时会顺着路径去查找并加载外部文件的内容如果路径变了、文件没了图面就显示为“未解析”也就是常说的红叉。理解了这个机制就明白为什么合并时Xrefs棘手。你把主图内容搬进新文件如果只是把Xref引用标记带过去新文件打开后依然要去原来的路径找外部文件。原文件挪了目录或者发给别人路径就断了。所以合并前必须做一个决策保留Xref关系并在新环境里重设路径或者把Xref绑定成内部块也就是把外部文件的几何实体真正“焊死”进来。对应到API就是BindXrefs方法绑定后块名会变成类似“外部图名$0$图名”的形式这就是块重命名以避开冲突的机制。3. 实操写一个批量合并DWG的命令3.1 项目搭建与前期准备先在Visual Studio里新建一个类库项目目标框架选.NET Framework 4.8平台目标x64。引用AutoCAD安装目录下的三个核心dllAcDbMgd.dll、AcMgd.dll、AcCoreMgd.dll。引用时“复制本地”设为False避免把几百兆的dll拷到输出目录。类上加一个CommandClass特性方法上加CommandMethod特性这样NETLOAD加载之后在CAD命令行直接敲命令名就能运行。整个工具不需要对话框界面路径、开关参数我用代码顶部数组写死实际项目里改成让用户拖拽文件夹或者读配置文件都很容易。3.2 遍历多文件夹收集所有DWG这一步没有技术难度但有几个细节非常影响后面合并的质量。一是用SearchOption.AllDirectories递归子目录否则子文件夹里的图纸容易漏二是过滤掉CAD临时文件也就是文件名以“~$”开头的文件三是去重并排序保证合并顺序对用户透明可预期。private static Liststring CollectDwgFiles(string[] folders) { Liststring files new Liststring(); foreach (string folder in folders) { if (!Directory.Exists(folder)) continue; files.AddRange(Directory.GetFiles(folder, *.dwg, SearchOption.AllDirectories)); } return files .Where(f !Path.GetFileName(f).StartsWith(~$)) .Distinct(StringComparer.OrdinalIgnoreCase) .OrderBy(f f, StringComparer.OrdinalIgnoreCase) .ToList(); }排序方式在批量合并场景里特别重要。默认按文件路径排序如果文件名本身就带图号比如“建-01.dwg”那基本就按图号顺序合并了。如果文件是靠内部属性排序的比如图框里的项目编号那就要在读取DWG后再抽取属性来排序后面我会在扩展部分说。3.3 打开源数据库并预处理Xrefs对每一张源DWG我用new Database(false, true)创建内存数据库然后调用ReadDwgFile读取文件。这里有个大坑必须提醒ReadDwgFile读出来的数据库默认是只读的直接调用BindXrefs会报“数据库以只读方式打开”之类的异常。解决办法是调用CloseInput(true)把它切换成可写状态。using (Database srcDb new Database(false, true)) { srcDb.ReadDwgFile(dwgPath, FileShare.ReadWrite, false, ); srcDb.CloseInput(true); if (bindXrefs) { BindAllXrefsInDatabase(srcDb); } else { try { XrefManager.LoadXrefs(srcDb, 10000); } catch { // 低版本CAD API差异保留链接时让CAD打开后再重载即可 } } }BindAllXrefsInDatabase的逻辑是遍历块表把所有IsExternalReference为true的块表记录收集起来然后调用db.BindXrefs(ids, true)。第二个参数allowNested设为true表示允许嵌套外部参照一并绑定这个在项目里基本是必须的因为经常出现A参照B、B又参照C的情况。3.4 用Insert把源DWG变成目标库里的一个块这是整个合并过程的核心一行ObjectId blockRecId targetDb.Insert(targetMs, srcDb, true);targetMs是目标数据库的模型空间块表记录。这行代码的意思是把srcDb整个数据库的内容打包生成一个新的块表记录并且放入目标数据库的块表中。注意新生成的块内容来源是srcDb的模型空间方法内部会处理图层、线型、标注样式等依赖项。第三个参数preserveSourceDatabase传true表示保留源库内容因为在循环里还要继续用这个源库做后续处理不能让它被掏空。插入之后在目标模型空间里创建一个BlockReference引用这个块就完成了“一张图变成总图里一个块”的效果BlockReference bref new BlockReference(pos, blockRecId); targetMs.AppendEntity(bref); transaction.AddNewlyCreatedDBObject(bref, true);BlockReference的位置由pos决定。这里我按顺序排列来放置各个图纸以及设置列数、行距参数。每张图以一个块的形式出现在总图模型空间里移动、缩放、改名字都很方便。3.5 块命名防冲突策略几十张DWG合并进来块名冲突是必然的因为很多设计院的图框块都叫“TITLE_BORDER”或者“A0-HORIZONTAL”。如果直接沿用文件名做块名重名概率也很高。我的策略是string blockName baseName; int suffix 1; while (usedBlockNames.Contains(blockName) || targetBt.Has(blockName)) { blockName baseName _ suffix; suffix; } usedBlockNames.Add(blockName);这里同时检查了两个集合一个是本次合并已经用过的块名一个是目标数据库块表里本来就存在的块名。双重检查能避免两张源图里恰好有同名块时互相覆盖。块名冲突如果不处理Insert会自动跳过或换名结果就是某些图明明插入了但内部内容却对不上排查起来非常折磨人。3.6 保存输出文件所有源DWG处理完毕后把目标数据库保存到指定路径targetDb.SaveAs(outputPath, DwgVersion.Current);用new Database(true, true)在内存中创建的全新数据库SaveAs会把它落成独立的DWG文件不会影响当前CAD文档。这里我不建议在命令运行过程中直接用Application.DocumentManager.Open去打开新文件因为会打断当前命令的事务上下文。稳妥的做法是合并完成后弹一个提示用户自己按需打开。4. 参数选择这些选项背后的逻辑4.1 preserveSourceDatabase该传true还是false很多从LISP转过来的朋友会忽略这个参数但它的影响很大。preserveSourceDatabase传true时源数据库的对象在Insert后保留原样传false时源库内容会被移动到目标库源库中原有的块表记录会处于失效状态。在批处理循环里我必须传true。原因很简单这张DWG处理完之后我还要读下一张如果源库内容被移动了虽然理论上我们不会再访问它了但万一脚本中途跳出、调试时想再看看源库结构就会收到一堆奇怪的异常。传false省的那点内存和性能在批量场景下微乎其微不值得冒险。4.2 事务提交的节奏与内存控制批量合并几十张图最忌讳的是把所有操作塞进一个大事务里等到全部完成才Commit。这样做的后果是事务管理器要维护海量对象状态内存占用飙升而且一旦中途某张图出问题整个事务回滚前面所有进展全部白费。我的节奏是目标数据库的顶层事务贯穿全程但每处理完一张源DWG就单独启动一个源数据库事务并立即Commit。这样源库的变更边界清晰目标库的实体逐步累积即便某张图处理失败也只是跳过这一张不会影响前面已经合并好的内容。用TransactionManager嵌套的方式处理大批量实体是我在项目上了十几次当之后的经验总结。4.3 Xrefs保留还是绑定是个业务决策之前说过Xrefs的两种处理方式没有绝对的好坏完全看业务场景。我整理了一个对比表场景选择原因自己的项目文件目录结构不变保留Xref上游图纸更新后总图自动更新送审、打印、归档、发送外部绑定Xref文件自包含不依赖外部路径需要继续引用底图做协同设计保留Xref保持设计联动配合另外交付一批源图绑定Xref避免对方打开后红叉一片这个决策最好做成配置项由用户在运行前选择。我在工具里加了一个bool参数bindXrefs默认true因为国内大多数合并需求最后都是为了交付和归档绑定最省心。5. 踩坑记录与问题排查实录5.1 保存时报“文件被占用”错误合并完成后保存输出文件偶尔会报“另一个程序正在使用此文件”。最常见的元凶是云端同步盘比如OneDrive、坚果云正在扫描你即将要写的目录有时候是安全软件把DWG当可疑文件短暂锁定。我的处理办法是分两步。先在目标路径所在的目录下生成一个随机临时文件名保存成功后再用File.Copy覆盖到最终路径如果覆盖时还是失败就自动把文件保存到同目录下的“_合并失败恢复_时间戳.dwg”文件里。这样至少不会让用户辛苦合并的成果因为一个文件锁而作废。5.2 插入后的图跑到天边去了这个问题通常出现在源DWG的图纸空间/模型空间基点不是原点的情况下。有些图纸是别人从其他软件导出的模型空间里所有实体分布在坐标(1000000, 2000000)附近插入后自然离原点十万八千里。处理方式是Extents3d ext srcMs.GeometricExtents; Vector3d offset Point3d.Origin - ext.MinPoint; bref.Position insertPos offset;也就是先算出源模型空间的实际范围平移到原点附近再放到排布位置上。这个细节在合并建筑总图的时候几乎必踩。5.3 外部参照合并后变成空块有朋友反馈绑定Xref后插入结果新文件里外部参照内容不见了只剩一个空块。这个多半是因为在绑定之前没有把外部参照加载进内存。主图里只存了路径和引用标记如果不提前加载绑定操作面对的是一个“空壳”自然绑不出内容。所以我在预处理阶段调用了XrefManager.LoadXrefs或者确保外部文件路径可访问。绑定之前先加载这步不能省。5.4 同名图层被覆盖导致图面错乱两张源图都有图层“S-ELEV-DIMS”但一个线型是连续线另一个是虚线。合并后后处理的图会覆盖先处理的图层定义导致先处理的那张图里所有尺寸标注样式错乱。这是合并DWG最隐蔽的坑之一。我的策略是如果对图层一致性要求高就在工具运行前先做一次图层检查列出重名但属性不同的图层让用户决定是统一以某一张图为准还是自动重命名成“源文件名-图层名”。如果只是机械地合并至少要保证业务上任何人都知道“同名图层以后可能会被顶掉”这件事。5.5 批处理中途内存爆炸一百多张DWG每张都几十MB循环里如果Dispose写得不干净内存占用会线性上涨直到程序崩溃。关键点是源数据库一定要用using包裹并在处理完后主动Dispose目标数据库的块表记录在添加BlockReference后如果不再需要引用也要考虑释放。另一个经验是不要把源数据库的Transaction对象跨迭代保存每次循环都新建、提交、释放让GC能及时回收托管资源。6. 完整代码清单把上面所有环节串起来就是一份可以直接编译运行的代码。我基于AutoCAD 2020/2023、.NET Framework 4.8调试通过不同CAD版本API命名会略有差异但整体逻辑是通用的。using System; using System.Collections.Generic; using System.IO; using System.Linq; using Autodesk.AutoCAD.ApplicationServices; using Autodesk.AutoCAD.DatabaseServices; using Autodesk.AutoCAD.Geometry; using Autodesk.AutoCAD.Runtime; [assembly: CommandClass(typeof(DwgMergeTool.DwgMergeCommands))] namespace DwgMergeTool { public class DwgMergeCommands { [CommandMethod(MergeDwgsFromFolders)] public static void MergeDwgsFromFolders() { // 配置区实际项目中可改为UI选择 string[] folders new string[] { D:\Project\建筑, D:\Project\结构, D:\Project\机电 }; string outputPath D:\Project\_合并总图.dwg; bool bindXrefs true; // true绑定外部参照, false保留外部参照 int columns 4; // 每行放置几张图 double gap 1000; // 图间距单位毫米 // Liststring dwgFiles CollectDwgFiles(folders); if (dwgFiles.Count 0) { Application.ShowAlertDialog(没有找到任何DWG文件请检查目录。); return; } Database targetDb new Database(true, true); try { using (Transaction targetTx targetDb.TransactionManager.StartTransaction()) { BlockTable targetBt targetTx.GetObject( targetDb.BlockTableId, OpenMode.ForRead) as BlockTable; BlockTableRecord targetMs targetTx.GetObject( targetBt[BlockTableRecord.ModelSpace], OpenMode.ForWrite) as BlockTableRecord; int index 0; HashSetstring usedBlockNames new HashSetstring(StringComparer.OrdinalIgnoreCase); foreach (string dwgPath in dwgFiles) { if (Path.GetFileName(dwgPath).StartsWith(~$)) continue; string baseName Path.GetFileNameWithoutExtension(dwgPath); string blockName baseName; int suffix 1; while (usedBlockNames.Contains(blockName) || targetBt.Has(blockName)) { blockName baseName _ suffix; suffix; } usedBlockNames.Add(blockName); using (Database srcDb new Database(false, true)) { srcDb.ReadDwgFile(dwgPath, FileShare.ReadWrite, false, ); srcDb.CloseInput(true); if (bindXrefs) { BindAllXrefsInDatabase(srcDb); } else { try { XrefManager.LoadXrefs(srcDb, 10000); } catch { // 版本差异保留链接时忽略即可 } } using (Transaction srcTx srcDb.TransactionManager.StartTransaction()) { BlockTable srcBt srcTx.GetObject( srcDb.BlockTableId, OpenMode.ForRead) as BlockTable; BlockTableRecord srcMs srcTx.GetObject( srcBt[BlockTableRecord.ModelSpace], OpenMode.ForRead) as BlockTableRecord; // 把源DWG整体作为块插入目标数据库 ObjectId blockRecId targetDb.Insert(targetMs, srcDb, true); // 计算排布位置考虑源图基点偏移 Extents3d ext srcMs.GeometricExtents; Vector3d offset Point3d.Origin - ext.MinPoint; int col index % columns; int row index / columns; Point3d insertPos new Point3d( col * 20000 gap * col, -row * 15000 - gap * row, 0); BlockReference bref new BlockReference(insertPos offset, blockRecId); targetMs.AppendEntity(bref); targetTx.AddNewlyCreatedDBObject(bref, true); srcTx.Commit(); } } index; } targetTx.Commit(); } targetDb.SaveAs(outputPath, DwgVersion.Current); Application.ShowAlertDialog(合并完成 outputPath); } catch (Exception ex) { Application.ShowAlertDialog(合并失败 ex.Message); } finally { targetDb.Dispose(); } } private static Liststring CollectDwgFiles(string[] folders) { Liststring files new Liststring(); foreach (string folder in folders) { if (!Directory.Exists(folder)) continue; files.AddRange(Directory.GetFiles(folder, *.dwg, SearchOption.AllDirectories)); } return files .Where(f !Path.GetFileName(f).StartsWith(~$)) .Distinct(StringComparer.OrdinalIgnoreCase) .OrderBy(f f, StringComparer.OrdinalIgnoreCase) .ToList(); } private static void BindAllXrefsInDatabase(Database db) { ObjectIdCollection xrefIds new ObjectIdCollection(); using (Transaction tx db.TransactionManager.StartTransaction()) { BlockTable bt tx.GetObject(db.BlockTableId, OpenMode.ForRead) as BlockTable; foreach (ObjectId btrId in bt) { BlockTableRecord btr tx.GetObject(btrId, OpenMode.ForRead) as BlockTableRecord; if (btr.IsExternalReference) { xrefIds.Add(btrId); } } tx.Commit(); } if (xrefIds.Count 0) { db.BindXrefs(xrefIds, true); } } } }代码里有一个细节值得强调在创建一个BlockReference之前我先获取了源模型空间的GeometricExtents然后算出偏移量。这个处理让每张图都保证从自己的左下角开始排布不会出现前面说的“图跑到天边”的问题。如果你希望图纸按图框中心对齐也可以改成用ext的中心点计算偏移。7. 扩展思路与个人体会这个工具做到了“多文件夹扫描 批量合并 Xrefs绑定”之后其实还有很多可以继续延展的方向。如果需求反过来不是合并DWG而是要给一批DWG统一添加外部参照比如把所有图纸都挂上最新版的项目总平面图做底图思路是一样的。遍历文件夹打开每张DWG在块表里创建一个IsExternalReference为true的块表记录设置Path指向要被参照的DWG然后在模型空间里添加一个BlockReference设置好Scale和Position最后另存。主从关系反过来了但数据库CRUD的框架完全复用。还可以做自动识别图框排布。现在的排布逻辑是按列数粗暴等距放置遇到大小图幅混杂的情况会浪费空间。更进一步的做法是遍历每个块的GeometricExtents拿到每张图的实际尺寸然后像拼图一样自动化排布。这个逻辑跟排版算法类似也不算太复杂。我还建议把合并信息导出一个CSV或者写入DWG扩展字典。比如每张图对应的源文件路径、图号、版本、合并时间都记录下来以后出问题查图能直接溯源。这些信息用XDATA或扩展记录写回块表记录即可代码量不大但对实际交付很有价值。个人用下来的最大体会是DWG批量处理项目七成时间花在异常处理和边界情况上。代码最核心的几行反而不复杂——Insert一行、BlockReference一行、SaveAs一行。真正决定工具好不好用的是文件名冲突怎么处理、外部参照加载后能不能绑定成功、图元基点偏移怎么校正、中途失败怎么恢复。做这类工具不能把自己当业务流水线看待要把自己放在“数据搬运工”的位置上每一张图都有各自的脾气只有把所有意外都提前想到了工具才敢真正交到别人手上。
返回列表