简介:面向C#开发者的DXF处理实用示例,基于netdxf开源库实现文件读取与坐标导出。压缩包共6个文件:DxfToolDemo.exe为可直接运行的主程序,netDxf.dll为读取DXF的核心类库,pdb与xml分别提供调试符号和API说明文档,蓝本-CAD.dxf是用于测试的样例图纸,config文件保存运行参数,整体仅319KB,轻量小巧,便于快速下载与部署。示例围绕加载DxfDocument对象、遍历Point/Line/Polyline等图元、提取三维坐标、生成CSV输出的完整链路展开,覆盖DXF解析中最常用的图元访问与坐标提取API,代码结构清晰,便于学习与二次开发。已有1764人学习下载,适合需要掌握CAD数据解析的初中级C#开发者;既可直接运行exe观察处理结果,也可结合pdb和xml文档深入理解内部调用细节,并在此基础上扩展图层、线型、块等复杂特性处理,减少从零排查netdxf接口的试错成本,是进一步开发CAD集成功能的高性价比起点。
1. 读 DXF 导出坐标:netdxf 把「解析文本」变成了「取属性」
很多工程问题是最后一步卡住的:图在 CAD 里画得清清楚楚,程序却拿不到坐标。来个甲方图纸,要求把所有孔心坐标按加工顺序导成表,你打开 DXF 文件一看,全是组码和数字,根本分不清哪行是坐标。用 netdxf(C#)框架做 DXF 文件读取与坐标导出,就是为了解决这类现场问题。它是一个免费的 C# 对象模型库,把 DXF 的 ENTITIES 段解析成强类型的 Line、LwPolyline、Circle、Insert 等对象,坐标直接以属性形式暴露出来。这篇笔记写给要做点位提取、测量放样、自动化下料的人,目标是让你半小时内跑通读取、展开块、导出 CSV,并把最常见的坑提前排掉。
2. 搭好 netdxf 读取环境:从 NuGet 包到第一行实体枚举
2.1 为什么是 netdxf:与手写解析、AutoCAD COM 的对比
DXF 本质是成对的组码(group code)和值,组码 10 后面跟 X 坐标,20 后面跟 Y 坐标,实体类型、图层、颜色都靠这些数字区分。手写解析器最麻烦的不是读数字,而是每个实体变体、每个 DXF 版本的细节差异:多段线顶点记录方式、块定义的嵌套变换、扩展数据对坐标的干扰,全部要自己消化。AutoCAD 的 COM 接口也能读,但它要求机器装完整 CAD 客户端,在服务器上做批处理非常不现实,版本一换接口就变。
常见做法是引入 netdxf 这类库,把文本解析封在内部,对外提供对象模型。下表是我在方案选型时常用的对比口径:
| 方案 | 优点 | 落地代价 |
|---|---|---|
| 手写 DXF 解析 | 零依赖、可控性强 | 要处理段结构、实体变体、版本差异,维护成本高 |
| AutoCAD COM | 功能全、和 CAD 一致 | 依赖客户端环境,批处理受限制,部署重 |
| netdxf | 开源、强类型、无外部依赖 | 需要理解实体模型和坐标系约定 |
我一般直接选 netdxf。它把 DXF 文档映射成 DxfDocument 对象,读坐标就从「解析文本」变成「取属性」,尤其适合把图纸里的点位批量抽出来交给下料、测量程序。
2.2 二十行代码加载 DXF 并枚举实体
先建一个控制台项目,在 NuGet 包管理器里搜索 netDxf 并引用,然后写最核心的加载逻辑。关键点:Load 不是稳妥返回,文件路径错、格式坏、版本不兼容都会抛异常,所以必须包 try-catch。
using netDxf; using netDxf.Entities; using netDxf.Header; string path = @"D:\drawings\part_001.dxf"; DxfDocument doc; try { doc = DxfDocument.Load(path); } catch (Exception ex) { Console.WriteLine("加载失败: " + ex.Message); return; } Console.WriteLine("DXF 版本: " + doc.DrawingVariables.AcadVersion); Console.WriteLine("图层数: " + doc.Layers.Count); Console.WriteLine("Line: " + doc.Entities.Lines.Count); Console.WriteLine("LwPolyline: " + doc.Entities.LwPolylines.Count); Console.WriteLine("Circle: " + doc.Entities.Circles.Count); Console.WriteLine("Insert块引用: " + doc.Entities.Inserts.Count);这段代码的作用是验证文件能不能读、图纸里大概有什么实体。DxfDocument.Load 是静态方法,一次调用就把 HEADER、TABLES、BLOCKS、ENTITIES 全部读进内存;doc.Entities 下的 Lines、LwPolylines、Circles、Inserts 是按类型拆好的只读集合,Count 直接告诉你每种实体的数量。DrawingVariables.AcadVersion 返回 DXF 版本枚举,用来判断是不是高版本文件。如果这里打印出来的数量和你用 CAD 里看到的数量一致,说明文件解析正常,可以继续往下走。
2.3 弄清楚实体结构再动手:Vector3、Layer 与模型空间
netdxf 里绝大部分坐标都是 netDxf.Vector3 结构,有 X、Y、Z 三个 double 属性。Line 有 StartPoint、EndPoint;Circle 有 Center、Radius;LwPolyline 的顶点是 Vertexes 列表,每个顶点有 Position 和 Bulge。实体对象上还有 Layer 属性,Layer.Name 就是图元所在图层名,点位提取时经常要靠图层过滤。
需要区分的是模型空间和布局。doc.Entities 聚合的是图纸里用于出图的主体实体,多数坐标提取任务只看模型空间,这也是默认情况;布局里的图框、标注块如果也被统计进去,数量会对不上。另一个容易忽略的点是:LwPolyline 的顶点 Position 是 Vector2(只有 X、Y),多段线的 Z 值存在 LwPolyline.Elevation 属性里,组装坐标时要把这两个值拼起来。理解这三件事,后面提取坐标才不会拿错字段。
3. 按实体类型提取坐标:Line、LwPolyline、Circle 与 Insert 块展开
3.1 三个高频实体的坐标提取代码
现场图纸里,点位来源主要就三种:直线端点、多段线顶点、圆心。加工件上的孔心基本是 Circle,轮廓基本是 Line 和 LwPolyline。先写一个通用的收集函数,把这三类实体全部抽成点:
using netDxf; using netDxf.Entities; public static List<Vector3> CollectPoints(DxfDocument doc) { var pts = new List<Vector3>(); // Line:取两个端点 foreach (var line in doc.Entities.Lines) { pts.Add(line.StartPoint); pts.Add(line.EndPoint); } // LwPolyline:取每个顶点,Z 用 Elevation foreach (var pline in doc.Entities.LwPolylines) { foreach (var v in pline.Vertexes) { pts.Add(new Vector3(v.Position.X, v.Position.Y, pline.Elevation)); } } // Circle:取圆心,孔位提取就靠它 foreach (var circle in doc.Entities.Circles) { pts.Add(circle.Center); } return pts; }逻辑不复杂,但有两个关键点。Line 的端点是 Vector3,直接用;LwPolyline 的顶点是 Vector2,必须把 pline.Elevation 拼成 Z,否则导出后所有点都贴在 Z=0 平面上,遇到带标高的路桥图会出错。Circle 在孔位加工场景里代表一个孔,中心点就是坐标。
下面是常见实体的坐标来源对照,写代码前先对一遍再动手:
| 实体类型 | 坐标来源 | 典型用途 |
|---|---|---|
| Line | StartPoint、EndPoint | 线段端点、求中点 |
| LwPolyline | Vertexes + Elevation | 轮廓点位、走刀路径 |
| Circle | Center | 孔心、圆心定位 |
| Arc | Center、StartAngle、EndAngle | 圆弧需离散取点 |
| Insert | Position + Block 内实体变换 | 图块内所有点位 |
3.2 Insert 块坐标变换:平移、旋转、缩放的执行顺序
坑最多的地方在块引用(Insert)。图纸里大量图元是块:螺栓孔、沉台、标准件,全都以块的形式插入。如果只遍历 doc.Entities.Lines,你会漏掉所有块里的线。netdxf 的 Insert 对象带一个 Block 属性,Block.Entities 里是块定义内部的实体,但它们的坐标是块定义坐标系,不是最终图纸坐标,必须先经过变换。
变换顺序是:先按 Scale 缩放,再绕插入点旋转,最后平移到 Insert.Position。注意 netdxf 的 Insert.Rotation 用的是弧度,不是角度,90 度要写成 Math.PI / 2。我常用的变换代码如下:
using netDxf; using netDxf.Entities; static Vector3 Transform(Vector3 p, Vector3 origin, double angle, Vector3 scale) { double x = p.X * scale.X; double y = p.Y * scale.Y; double cos = Math.Cos(angle); double sin = Math.Sin(angle); double rx = x * cos - y * sin; double ry = x * sin + y * cos; return new Vector3(rx + origin.X, ry + origin.Y, p.Z + origin.Z); } static void CollectFromInsert(Insert insert, List<Vector3> pts) { foreach (var entity in insert.Block.Entities) { if (entity is Line line) { pts.Add(Transform(line.StartPoint, insert.Position, insert.Rotation, insert.Scale)); pts.Add(Transform(line.EndPoint, insert.Position, insert.Rotation, insert.Scale)); } else if (entity is Circle circle) { pts.Add(Transform(circle.Center, insert.Position, insert.Rotation, insert.Scale)); } else if (entity is Insert nestedInsert) { // 嵌套块:递归处理 CollectFromInsert(nestedInsert, pts); } } }这段代码里的 Transform 只处理了等比例缩放。如果块的 X、Y 缩放比例不同,圆会变成椭圆,端点的变换仍然正确,但圆心的导出没问题,圆弧的离散要另算。实际工程中非均匀缩放的块不多,遇到时我一般会让 CAD 里先炸开再导出。另一个注意点:块里还能套块,所以 CollectFromInsert 里必须递归处理内层 Insert,漏掉递归会导致嵌套块里的孔位全部丢失。
3.3 把点表统一起来:容差去重与排序习惯
图纸里的点经常是重复的:一条封闭多段线首尾顶点重合,或者同一个圆心既被 Circle 画了一次、又被块里的点重复表达。直接导出会造成下游加工时重复走位。我会在导出前做一次容差去重,再按坐标排序,让点表稳定可预期:
pts = pts .Distinct(new Vector3Comparer(1e-4)) .OrderBy(p => p.X) .ThenBy(p => p.Y) .ToList();Vector3Comparer 是自己实现 IEqualityComparer 的比较器,判断两个点的距离是否小于 1e-4 毫米,小于就认为同一个点。这个容差不能设得太大,否则真正距离很近的孔会被误删;机械图纸用 0.01 毫米以内比较安全。排序规则按 X 再按 Y,导出后的点位顺序固定,方便和 CAD 里的人工核对。
4. 坐标导出 CSV:字段设计、数字精度与文件编码
4.1 先定好导出字段,再写代码
坐标导出看起来是写文件,实际上是个数据接口设计。测量人员要的字段、下料程序要的字段、领导看报告要的字段完全不一样。先设计字段再写代码,能避免后期反复改。我习惯的字段清单如下:
| 字段 | 示例 | 说明 |
|---|---|---|
| No | 1 | 点序号,从 1 开始 |
| X | 123.456 | 保留 3 位小数 |
| Y | 456.789 | 保留 3 位小数 |
| Z | 0.000 | 保留 3 位小数 |
| Entity | Circle | 来源实体类型,方便排错 |
| Layer | 孔位层 | 来源图层名 |
Entity 和 Layer 这两个字段不是给下游程序用的,是给人排错用的。导出的坐标如果少了一个孔,看到 Layer 列就知道是哪个图层没被统计到。
4.2 写 CSV 文件的完整代码与三个硬性参数
CSV 导出代码里有三个硬性参数:小数位数、区域设置、文件编码。区域设置是最大的坑,默认 ToString() 受系统区域影响,德语环境里小数点会变成逗号,CSV 直接炸开。编码方面,Excel 打开 CSV 要 UTF-8 BOM 才不会乱码,但很多测量软件只认 ANSI(GBK),我会按下游软件决定编码。下面是带 BOM 的 UTF-8 版本:
using System.Globalization; using System.Text; static void ExportCsv(string outPath, List<Vector3> pts) { var sb = new StringBuilder(); sb.AppendLine("No,X,Y,Z,Entity,Layer"); int idx = 1; foreach (var p in pts) { sb.AppendLine(string.Join(",", idx.ToString(CultureInfo.InvariantCulture), p.X.ToString("F3", CultureInfo.InvariantCulture), p.Y.ToString("F3", CultureInfo.InvariantCulture), p.Z.ToString("F3", CultureInfo.InvariantCulture))); idx++; } // UTF-8 with BOM,Excel 直接双击打开不乱码 File.WriteAllText(outPath, sb.ToString(), new UTF8Encoding(true)); }这里有两个动作必须形成习惯:所有数字格式化都显式传 CultureInfo.InvariantCulture,保证在任何操作系统的区域设置下输出一致;文件写入用 UTF8Encoding(true) 带 BOM,保证 Excel 识别中文表头。如果下游是实验室的旧测量软件,把编码换成 Encoding.GetEncoding("GBK") 更稳,但文件头不要 BOM。小数位数 F3 表示保留 3 位,普通机加工到 0.001 毫米足够,测绘需求再看情况改成 F6。
4.3 给测量软件用的 TXT 点文件与再入图验证
除了 CSV,现场很多全站仪、放样软件要的是纯文本点文件,格式通常是这样:
1,123.456,456.789,0.000 2,125.000,458.000,0.000不带表头、不带引号,逗号分隔。实现上把 4.2 的代码去掉表头和 Layer 列就行,注意换行符默认是 CRLF,测量软件大多兼容。还有一种更实用的验证方式:把导出的坐标重新写成一个小 DXF,叠加在原图上对比,肉眼就能看出有没有点错位。这个方法我在第 6 章会展开讲,它比反复数点数高效得多。
5. DXF 坐标导出避坑:五个反复出现的现场问题
坐标提取的报错大多不是程序 bug,而是图纸和现实对不上。下面按我实际遇到的高频现象整理,每条按「现象 → 原因 → 解决」说清楚。
5.1 枚举结果是 0:实体藏进了块或布局
现象:CAD 里打开图,满屏都是线、圆、多段线,程序跑完一个点都没输出,doc.Entities.Lines.Count 是 0。
原因:图元不在模型空间的裸实体里。常见两种情况:整张图被做成了一个块,唯一的裸实体就是几个 Insert;或者图元在布局(图纸空间)的视口里,模型空间其实是空的。
解决:先打印所有实体类型的 Count,确认到底哪些类型有数据;然后遍历 Inserts,用 3.2 的递归展开逻辑把所有块内部实体解放出来。布局里的实体则要换思路:让甲方在 CAD 里把布局内容复制到模型空间再交付,这比程序去解析布局视口省事得多。排查时记住一句话:Count 是 0 不代表图纸是空的,只代表实体不在你遍历的那一层。
5.2 坐标整体差一个量级:单位约定不是小事
现象:导出的坐标全部偏大或偏小 1000 倍,一个 100mm 长的零件,导出的是 0.1 或者 100000。
原因:DXF 文件本身不强制单位,毫米、米、英寸都可能。如果甲方用米制画图而程序按毫米解析,或者反过来,数值就差 1000 倍。净 dxf 读取的是文件里存的原始数值,它不做单位换算。
解决:导出前先问清楚图纸单位,或者用 CAD 里 DI 命令量一个已知尺寸来标定。我一般会在程序里留一个 UnitScale 参数,默认 1.0,一旦发现量级不对,直接乘系数重新导出,而不是改代码重新编译。这个排查动作能省掉大量来回沟通的时间。
5.3 多段线圆弧段丢点:Bulge 凸度必须还原
现象:LwPolyline 导出的顶点之间是一条直线,但 CAD 里这段明明是圆弧,加工出来的轮廓完全不对。
原因:LwPolyline 的顶点只记录了位置和凸度(Bulge)。Bulge 为 0 表示直线段,非 0 表示该顶点到下一个顶点之间是圆弧,圆弧的圆心角由 Bulge 决定。只取 Position 就丢掉了圆弧信息。
解决:检查顶点 Bulge,非 0 时按凸度插值补点。Bulge 和圆心角的关系是 bulge = tan(theta / 4),theta 是圆弧圆心角。插值时算出圆心和半径,在起点终点之间按一定段数生成弧上点。方向判断有一个土办法:先按数学逆时针生成,如果发现弧的方向和 CAD 相反,把符号取反再生成一次,现场调试时最快。
5.4 CSV 小数点变逗号:区域设置的经典翻车
现象:CSV 在别人电脑上打开,数字变成 123,456,列全部错位,Excel 把坐标当成两个字段。
原因:代码里直接用了 double.ToString(),没有指定区域。操作系统区域设置为德语、法语等地区时,小数点符号是逗号,和 CSV 的分隔符冲突。
解决:所有数值格式化必须显式传 CultureInfo.InvariantCulture,代码模板直接抄 4.2 节写法。这个坑出现频率极高,属于「改一次,一辈子受益」的血泪经验。顺带注意:CSV 文件在不同电脑间传递,最好统一用 UTF-8 BOM 编码,避免表头出现中文乱码。
5.5 Load 直接抛异常:文件占用与高版本 DXF
现象:DxfDocument.Load 抛异常,提示文件被占用,或者直接说不支持的 DXF 版本。
原因:源文件正被 AutoCAD 打开着,文件句柄被占用;或者图纸存成了太高版本的 DXF,netdxf 解析不了。
解决:程序读取前先把文件复制到临时目录,再对副本 Load。复制这一步成本极低,还能顺带解决路径中文、文件锁等问题。高版本格式则无解,只能让制图方另存为兼容版本,或者用 CAD 批量转存,这是所有 DXF 库的边界,不是 netdxf 的问题。
6. 把导出流程固化成小工具:回读成图和批处理命名
6.1 坐标回读成图,让肉眼抓坐标错位
导出 CSV 后,最有效的验证方式不是数点数,而是把坐标点重新写成 DXF 叠加到原图上比对。代码很短:
using netDxf; using netDxf.Entities; void WriteCheckFile(string outDxfPath, List<Vector3> pts) { var doc = new DxfDocument(); foreach (var p in pts) { // 画一个半径 1mm 的小圆做标记点 doc.Entities.Add(new Circle(p, 1.0)); } doc.Save(outDxfPath); }保存后把这个文件和原图用 CAD 打开叠在一起,点位对不上当场就能看出来。这个方法比任何日志都直观,我每次批量导出前都会先生成一张校验图,确认无误后再发 CSV 出去。半径 1mm 是按机械图纸精度设置的,如果图里最小孔小于 2mm,把半径改成 0.5。
6.2 批处理跑图纸:命令行参数与输出命名约定
单张图跑通后,真正省时间的是批处理。我一般把主程序做成命令行工具,输入一个图纸目录,自动遍历所有 DXF,输出文件名固定为「原文件名 + _pts.csv」:
DxfPointExporter.exe -input D:\drawings -output D:\results -scale 1.0程序里用 Directory.GetFiles 枚举 DXF,然后逐张 Load、提取、导出。输出命名用原文件名拼接后缀,后续出问题时能快速关联回源图纸。命令行里保留 scale 参数,单位不对时直接传 0.001 或 1000,不改代码就能救场。这个习惯是从一次半夜改单位系数重新编译的教训里养成的,从那以后凡是跟坐标系、单位相关的常量全部走参数化。希望帮到你。
本文还有配套的精品资源,点击获取