ARTICLE DETAIL

资讯详情

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

C#.NET读写DWG:基于ODA DWGdirect库的无AutoCAD服务端方案

C#.NET读写DWG:基于ODA DWGdirect库的无AutoCAD服务端方案 简介面向需要在C#.NET项目中直接读取DWG格式CAD文件的开发者这份基于DWGdirect_NET_3_02动态库的资源包能有效解决在.NET环境下解析DWG图纸信息的常见难题适用场景包括图纸数据批量提取、CAD文件信息识别以及与企业现有系统对接。压缩包为ZIP格式整体大小约8.89MB内含可直接运行的DEMO项目与动态库文件DEMO在VS2010下测试通过并带有自测注释便于快速理解调用流程和集成方式可避开许多常见的环境配置问题。已有1317人学习下载对CAD二次开发入门者及有DWG数据读取需求的工程师具有参考价值无论是纯新手还是有一定经验的开发人员都能从中受益。通过该资源可掌握DWGdirect动态库的核心调用方法、DWG文件信息的读取流程并能基于DEMO进行二次开发或移植到实际项目中大幅节省自行寻找方案和调试的时间为后续业务集成提供可靠基础。1. C#.NET 读写 DWG为什么选 DWGdirect_NET_3_02接到一个纯后端任务每天从合作方丢过来的几千个 DWG 图纸里取图层和实体坐标写进业务库。第一反应是装 AutoCAD 用 COM 调可服务器不能装桌面软件转 DXF 又怕丢数据。最后选定的是 ODAOpen Design Alliance的 DWGdirect_NET_3_02——一套让 C#.NET 直接读写二进制 DWG 的托管封装不需要图纸软件参与目标机器上只有我们的服务和这份库。这篇按实际落地顺序来写先讲为什么选它而不是 DXF 中转或 COM 自动化再给最小读取代码、写出代码最后是回读校验和版本兼容上的坑。适合没有 AutoCAD 环境、需要在服务端批量把 DWG 导入业务系统的 .NET 工程师也适合想自研轻量 CAD 看图工具、又不愿意碰 DXF 解析的人。2. DWG 是封闭二进制格式选型先想清楚再动手2.1 DWG 与 DXF 的本质差别句柄、对象映射和增量保存DWG 不是文本格式。文件开头有版本码AC1015 对应 AutoCAD 2000AC1018 对应 2004中间是对象映射表、句柄表和按需延迟加载区实体之间的引用靠句柄Handle串联。这个结构是 Autodesk 为 AutoCAD 增量保存设计的从来没有公开成正式规范。这就是LibreCAD 解析 DWG 是逆向技术吗这个问题的来源开源社区里 LibreCAD 默认走的其实是 DXF 通路libdxfrw对 DWG 的解析能力一直很弱真正把 DWG 当成二进制格式完整解出来的是 ODA 这个联盟用干净室逆向的方式维护了一套跨平台 SDK成员付费使用。DWGdirect 就是 ODA 这套 SDK 的 .NET 托管封装DWGdirect_NET_3_02 是其中一个较早的交付版本。相比之下 DXF 是 ASCII 交换格式文档齐全、解析容易但图纸里不少数据特别是代理实体、自定义对象、某些扩展字典走 DXF 会丢失或变形。如果上游只给 DWG绕开 DXF 是更稳的路径。2.2 C# 环境下四种读写方案的取舍下面这张表是我在服务器场景里的真实对比结果方案是否依赖 AutoCAD许可与成本典型场景AutoCAD COM/ActiveXAcadApplication必须安装 AutoCAD桌面许可证服务器部署麻烦有 CAD 的办公机上的交互插件DWGdirect/TeighaODA SDK否纯 SDK按 ODA 会员授权服务端批量处理、无头读图、自研看图DXF 中转netDxf、ezdxf否开源免费自己生成的图纸、数据交换libredwg / libdxfrw否GPL 或弱许可开源工具链商用要注意 GPL 传染我一般会选 ODA 路线核心理由是不依赖桌面软件、进程内直接读写、支持 x86/x64 服务部署。网上那些dwg 转换 shp 免费软件类工具底层也大量是 ODA 库或类似实现可见这条路在 GIS 和 BIM 转换链路上已经被验证过。缺点是授权不便宜而且老版本 SDK 对新版 DWG 的支持有一个明确的天花板这个在第四章展开。2.3 DWGdirect_NET_3_02 的对象模型和命名空间DWGdirect 的 .NET API 沿用了 ODA 一贯的对象模型理解下面五层就够了Database对应一个 DWG 文件负责打开、保存、版本转换。TransactionManager所有读写都要起事务防止对象缓存和底层数据库状态不一致。BlockTable图纸里的块表里面是若干BlockTableRecord。BlockTableRecord一个块定义模型空间本身就是一条特殊的记录。Entity直线、圆、多段线、块引用等图元全部继承自它。对象引用同时存在两个身份ObjectId是本次会话内的索引用完即失效Handle是文件内持久句柄跨会话稳定适合做去重和外键映射。3.02 时代的程序集命名空间在不同发行版里不完全一致代码开头一般是下面这样以你实际引用的程序集为准// 3.02 的命名空间常见为 ODA.DwgDirect / ODA.Kernel // ODA 后来改名为 Teigha命名空间变成 Teigha.DatabaseServices / Teigha.Runtime using ODA.DwgDirect; using ODA.Kernel;核心类的职责可以先用这张表记着后面代码里还会反复出现类职责Database打开/创建/保存 DWG持有块表和其他字典Transaction读写操作的隔离边界控制对象状态BlockTable所有块定义的容器BlockTableRecord块内实体集合模型空间是其中一条Entity图元基类含Layer、ColorIndex、HandleOpenModeForRead/ForWrite/ForNotify三种打开方式这里有个容易踩的认知偏差很多人以为 DWG 解析就是把文件按偏移量读出来拼成对象实际用 DWGdirect 时你基本不碰偏移量只操作这套对象模型。它内部负责把句柄和对象映射还原成可遍历的实体集合这正是商业库的价值所在。3. 最小读取链路初始化宿主、打开数据库、遍历模型空间3.1 引用配置和 x86/x64 本机库部署DWGdirect_NET_3_02 是托管封装但底层仍是原生库。部署时有三个容易翻车的地方。第一项目引用里除了托管程序集还要把 SDK 自带的原生动态库和授权文件放进输出目录。授权文件缺失或路径不对进程启动后第一次打开数据库就会抛异常而且错误信息很隐晦类似无效的许可证指纹。第二3.02 年代的库大多是 32 位编译的。如果你的程序跑在 x64 服务上IIS 应用池要启用 32 位应用程序或者让进程跑 WOW64纯 64 位进程加载 32 位本机库会直接报 BadImageFormatException。这一点在部署文档里最容易忽略因为编译时托管代码不会报错。第三引用方式直接用 HintPath 指向 SDK 里的 DLL 最简单不要扔进 GAC方便切换版本ItemGroup Reference IncludeDwgDirectNet HintPathlib\DwgDirectNet.dll/HintPath Privatetrue/Private /Reference /ItemGroupPrivate设为 true 保证拷贝到输出目录和本机库、授权文件放一起。启动时把工作目录设为 exe 所在目录避免 Windows 服务里相对路径找不到这些依赖。3.2 打开 DWG 并枚举模型空间实体的最小代码下面的代码完成了从打开文件到枚举模型空间全部实体的一次完整闭环using System; using System.IO; using ODA.DwgDirect; using ODA.Kernel; public static class MinimalReader { public static void Run(string dwgPath) { // 第一个参数 false不创建默认图面只打开已有文件 // 第二个参数 true不绑定到文档界面适合无头环境 using (Database db new Database(false, true)) { db.ReadDwgFile(dwgPath, FileShare.Read, true, ); using (Transaction tr db.TransactionManager.StartTransaction()) { BlockTable bt (BlockTable)tr.GetObject( db.BlockTableId, OpenMode.ForRead); BlockTableRecord ms (BlockTableRecord)tr.GetObject( bt[BlockTableRecord.ModelSpace], OpenMode.ForRead); int count 0; foreach (ObjectId id in ms) { // ForRead 打开不会触发写锁多个线程读同一文件安全 Entity ent (Entity)tr.GetObject(id, OpenMode.ForRead); Console.WriteLine({0,-14} layer{1,-12} handle{2}, ent.GetType().Name, ent.Layer, ent.Handle); count; } tr.Commit(); // 读事务也要提交否则内部缓存可能被延迟清理 Console.WriteLine($共 {count} 个实体); } } } }几个参数说明ReadDwgFile(path, FileShare.Read, true, )第三个参数代表是否允许代码页转换老图纸经常需要第四个是密码没有就传空字符串。如果文件正被 AutoCAD 以独占方式打开这里会收到IOException而不是静默失败。foreach (ObjectId id in ms)遍历的是模型空间内的实体引用不直接返回对象这是 ODA 的统一模式。同一个事务里对同一个ObjectId多次GetObject返回的是同一个托管实例可以直接用比较不要用Equals去比句柄。这个最小代码段就是后面做cad 快速看图类工具的地基把Console.WriteLine换成把实体类型、图层、句柄写进列表再结合几何提取就能画预览。3.3 从实体上提取几何Line、Circle、Polyline、BlockReference上一步只拿到实体外壳真正要入库的是几何数据。不同实体类型的读取方式差异很大下面是我常用的分派写法private static void DumpGeometry(Entity ent, Transaction tr) { if (ent is Line line) { Console.WriteLine($LINE ({line.StartPoint.X:F3},{line.StartPoint.Y:F3}) - $({line.EndPoint.X:F3},{line.EndPoint.Y:F3})); } else if (ent is Circle circle) { Console.WriteLine($CIRCLE center({circle.Center.X:F3},{circle.Center.Y:F3}) $r{circle.Radius:F3}); } else if (ent is Polyline pline) { // 轻量多段线顶点是二维点圆弧段用 bulge凸度表达 for (int i 0; i pline.NumberOfVertices; i) { Point2d p pline.GetPoint2dAt(i); Console.WriteLine($ vertex[{i}] ({p.X:F3},{p.Y:F3}) bulge{pline.GetBulgeAt(i):F4}); } } else if (ent is BlockReference br) { Console.WriteLine($INSERT block{br.Name} pos({br.Position.X:F3},{br.Position.Y:F3})); } }多段线的 bulge 是很多人的盲区bulge 为 0 表示直线段非 0 时表示从当前顶点到下一个顶点之间有一段圆弧圆弧夹角theta 4 * atan(bulge)半径r d * (1 bulge^2) / (4 * |bulge|)其中d是弦长。如果只取顶点画折线所有圆弧段都会变成直线图纸细节肉眼可见地变形。BlockReference只给了插入点和块名块内部的实体要再去块表里按br.Name找到对应BlockTableRecord再遍历而且还要叠加插入点的平移、缩放系数和旋转角。这也是 DWG 转 SHP、转 GIS 工具里最容易做错的一层——炸弹块嵌套块要递归展开主流实现都会写一个递归深度限制防止环引用死循环。3.4 读取时的三个必调参数OpenMode、FileShare 和事务生命周期读取阶段真正需要反复斟酌的参数就三个其他都可以用默认值参数可选值使用建议OpenModeForRead/ForWrite/ForNotify只读操作用ForRead要改属性必须ForWrite先 Read 再升级会抛异常FileShareRead/ReadWrite服务器上同时有 AutoCAD 打开同一文件时用Read可能拿不到独占锁要捕获重试事务生命周期Commit()/Dispose()读事务也建议Commit()否则延迟清理可能带着未提交状态一起回收这里有一个实战细节GetObject拿到实体后即使不再使用也尽量不要在事务提交前让托管对象被 GC 回收。ODA 库的内部对象表和托管对象是一一绑定的提前析构会导致后续遍历时出现对象已释放的诡异异常。规律就是一个事务里集中读取、集中处理、最后统一提交释放。4. 写出 DWG创建实体、追加到模型空间、保存版本4.1 新文件从 Database(true, true) 开始再设单位和精度创建新文件和打开旧文件不同第一个参数要传 true表示同时构建默认图面0 图层、默认样式、模型空间记录都就绪。第二个参数保持 true不挂接文档界面using (Database db new Database(true, true)) // 创建完整默认图纸 { // 插入单位告诉宿主 CAD 这个文件按什么单位解释 db.Insunits UnitsValue.Millimeters; // 线性单位和精度等价于设置 LUNITS / LUPREC 系统变量 db.SetVariable(LUNITS, 2); // 十进制 db.SetVariable(LUPREC, 3); // 3 位小数 // ... 在这里创建实体并保存 }Insunits很关键。DWG 内部坐标本身没有单位长度 1000 到底是一米还是一毫米取决于读取方在插入时怎么解释。如果图纸要交给做结构或机械的人统一设为毫米要进 GIS 管线可能更希望是米。SetVariable设置的是系统变量表效果和在 AutoCAD 命令行敲LUNITS相同。注意不同版本里SetVariable的签名可能不同有的接受 short有的接受 object以你引用的程序集为准。4.2 在事务里新增实体并提交写出实体必须走写事务而且每个新实体都要做两步追加到容器再注册进事务。完整示例using (Transaction tr db.TransactionManager.StartTransaction()) { BlockTable bt (BlockTable)tr.GetObject(db.BlockTableId, OpenMode.ForWrite); BlockTableRecord ms (BlockTableRecord)tr.GetObject( bt[BlockTableRecord.ModelSpace], OpenMode.ForWrite); // 直线起点 (0,0) 到 (5000,3000) Line line new Line(new Point3d(0, 0, 0), new Point3d(5000, 3000, 0)); line.Layer 0; line.ColorIndex 1; // 1 红色 ms.AppendEntity(line); tr.AddNewlyCreatedDBObject(line, true); // 圆圆心、法向、半径 Circle circle new Circle(new Point3d(2500, 1500, 0), new Vector3d(0, 0, 1), 800); circle.ColorIndex 2; // 2 黄色 ms.AppendEntity(circle); tr.AddNewlyCreatedDBObject(circle, true); // 单行文字Height 是字高和图纸单位一致 DBText text new DBText(); text.Position new Point3d(100, 100, 0); text.TextString C#.NET 写出的 DWG; text.Height 200; // 单位是图纸单位不是像素 ms.AppendEntity(text); tr.AddNewlyCreatedDBObject(text, true); tr.Commit(); // 不 Commit 则全部回滚 } db.SaveAs(output.dwg, dwgVersion); // dwgVersion 见 4.3ms.AppendEntity(line)负责分配 ObjectId 并挂到模型空间记录的实体表里tr.AddNewlyCreatedDBObject(line, true)把对象生命周期交给事务管理第二个参数传 true 表示添加到数据库后允许在事务里继续修改。少了这一步最常见的表现是保存成功但文件里找不到这个实体或者下次打开报数据库对象未添加。DBText对应单行文字MText是多行文字。单行文字适合程序自动标注多行文字内部带格式控制码拼接时需要额外处理批量标注场景我一般首选DBText。4.3 SaveAs 的版本参数表与兼容性取舍SaveAs的第二个参数决定输出文件能被哪个版本开。ODA 不同版本里这个枚举可能叫DwgVersion、SaveFormat甚至直接传整型对照关系是一致的DwgVersionAutoCAD 版本说明AC1012R13最老兼容性最好但特性最少AC1014R14老工地图纸常见AC10152000大多数处理管线的最低要求AC10182004中等兼容文件更紧凑AC10212007支持注释性缩放AC10242010常见交付格式AC10272013当前主流交付AC10322018新图纸老库不支持这里有个必须诚实面对的限制DWGdirect_NET_3_02 是 ODA 较早的版本对高版本 DWG 的支持停在 AC1015/AC1018 这个区间。拿 3.02 去读 AC1027 图纸会直接报版本不支持拿它去写 AC1027 也会失败。生产环境遇到这种图纸有两个选择换用新版 ODA Drawings原 TeighaAPI 基本兼容升级成本主要是重编译和重新验证或者在上游约定统一转成低版本再交付。前者是正路后者只能作为过渡手段。4.4 坐标精度陷阱为什么会出现 2.1616e16 这类坐标CAD 里画直线显示 2.1616e16是一个被问烂的问题根源在 double 的精度。double 只有大约 15 到 17 位有效数字当坐标值到了 10 的 16 次方量级最后一个毫米位早就被舍入吃掉了。出现这种情况通常有两种来源图纸内容距离原点极远或者有人把投影坐标高斯坐标、经纬度直接当成模型空间坐标塞了进来。批量处理时我建议写入前先检查包围盒如果坐标绝对值超过 1e8就整体平移到原点附近再保存// 把模型空间所有实体平移到原点附近避免天文坐标 Vector3d offset new Vector3d(500000, 400000, 0); // 原始基准点 foreach (ObjectId id in ms) { Entity e (Entity)tr.GetObject(id, OpenMode.ForWrite); e.TransformBy(Matrix3d.Displacement(offset.Negate())); }平移前要记录基准点下游读取时再加回来。另一个高频问题是单位换算GIS 系统的数据常以米为单位写进以毫米为单位的 CAD 里要把坐标乘 1000否则一张 10 米见方的图在 CAD 里只有 0.01 个单位肉眼几乎看不见。这也是 DWG 与 SHP 互转工具里最常见的图没了的原因——不是数据丢了是单位和数量级错了。5. 用一个回读校验闭环收尾写出的文件先自己读一遍5.1 回读校验实体数、图层、包围盒三重对比写出来的文件在交给下游之前先用同一套库重新打开一次做三重校验。这套逻辑我每个批量任务都会加public static void Verify(string file) { using (Database db new Database(false, true)) { db.ReadDwgFile(file, FileShare.Read, true, ); using (Transaction tr db.TransactionManager.StartTransaction()) { BlockTable bt (BlockTable)tr.GetObject(db.BlockTableId, OpenMode.ForRead); BlockTableRecord ms (BlockTableRecord)tr.GetObject( bt[BlockTableRecord.ModelSpace], OpenMode.ForRead); Dictionarystring, int groups new Dictionarystring, int(); Extents3d ext new Extents3d(); foreach (ObjectId id in ms) { Entity e (Entity)tr.GetObject(id, OpenMode.ForRead); string typeName e.GetType().Name; if (groups.ContainsKey(typeName)) groups[typeName]; else groups[typeName] 1; // GeometricExtents 是便宜的包围盒不触发完整几何解析 ext.AddExtents(e.GeometricExtents); } // 1) 实体分布2) 包围盒范围3) 图层集合 Console.WriteLine(实体分布: string.Join(, , groups)); Console.WriteLine($包围盒 min({ext.MinPoint.X},{ext.MinPoint.Y}) $max({ext.MaxPoint.X},{ext.MaxPoint.Y})); tr.Commit(); } } }GeometricExtents是校验坐标范围最快的入口比逐个解析几何便宜一个量级。对比时和写入前记录的期望分布、期望包围盒对账任何一项对不上就拒绝交付。这样能兜住 4.4 里说的量级错误也能兜住实体创建时漏掉AddNewlyCreatedDBObject导致的实体数少了问题。5.2 大批量时的三个实战习惯事务范围、本机库位、版本白名单批量跑几千个文件时我还会额外注意三件事。第一每个文件单独new Database处理完用using释放绝不在两个文件之间复用同一个 Database 实例ODA 库的全局状态比想象中多复用会导致句柄错乱。第二把dwg 如何提取图层信息这种高频需求做成独立工具函数先按图层名分组统计实体数如果某个图层的实体数异常比如 0直接在入队阶段拦截而不是等保存完才发现。第三维护一个版本白名单读取时捕获版本不支持的异常把文件路径和版本号写进失败队列让上游统一转档后重跑而不是在循环里反复重试同一个必然失败的文件。如果团队里有人习惯用 Python也可以通过 pythonnet 加载同一个托管程序集但事务和缓存行为不变建议先在 C# 里跑通回读校验再封装成 Python 可调的入口。回到开头那个批量任务先让回读校验在 10 个样本图纸上全绿再放开全量之后把图层名和句柄映射写进缓存批处理就不再每天重读同一批文件了。本文还有配套的精品资源点击获取
返回列表