简介:面向C#开发者与CAD图形处理学习者的DXF解析与显示源码合集,围绕在C# 2010环境中读取DXF交换格式、构建对象模型并完成图形渲染这一核心问题展开,适合具备一定C#基础、希望理解文件解析与GDI+/WPF绘图的中级读者参考。压缩包共180个文件、约2.78MB,主体为90个cs源码文件,配合28个dxf样例、4个csproj工程文件、2个sln解决方案及dll、exe、xaml、resx等运行与界面资源,覆盖解析器、实体类库、示例数据和可执行程序等层次。已有2610人学习,说明其在同类资料中具有一定参考度。读者可从中获得从DXF分段识别、实体对象建模到界面展示的完整工程脉络,并借助现成样例对照调试解析逻辑、观察渲染结果,对实体分类、坐标转换、性能取舍与异常处理形成直观认识,也可作为课程设计或小型CAD查看工具的参考样板。
1. 从一张必须嵌进业务系统的图纸说起:这份 C# dxf 源码能解决什么
前阵子接手一个售后系统改造,客户要求设备布置图直接在系统里打开,不能让人再装一遍 CAD。他们给过来的全是 .dxf 文件,我从网上翻了一圈 C# dxf 源码,能跑通的不多:有的只认 LINE 和 CIRCLE,圆弧方向画反,块引用全堆在原点,图层颜色一律黑色。这份 C# 2010 版的全套资料,价值不在于它用了多新的技术,而在于把「读组码 → 建实体模型 → GDI+ 画到窗口 → 缩放平移」这条链路完整走通了,还带了块表、图层、ACI 颜色这些真正会让新手翻车的地方。适合两类人:需要在 .NET Framework 4.0 工控机或老项目里内嵌简易图纸查看器的;想拿它当 DXF 格式入门教材、自己改出数据提取工具的。
2. 项目骨架与技术选型:VS2010 + WinForms + GDI+ 的三层拆法
2.1 为什么锁定 .NET Framework 4.0 加 GDI+,而不是 WPF
先说服自己再动手,这决定了后面所有代码长什么样。目标机器是客户现场那种装了老版系统的工控机,有的连 .NET 4.5 都没有,只保证 4.0;换成 WPF 意味着要额外确认 Windows 版本、XAML 渲染层、字体回退,部署成本立刻上去。GDI+ 虽然老旧,但绘制几万个线段完全够用,而且System.Drawing就在框架里,编译出来是一个带图标的小 exe,拷过去双击就跑。
另一个现实原因是坐标系。DXF 是严格的右手笛卡尔坐标系,Y 轴向上;GDI+ 的屏幕坐标 Y 轴向下。WPF 可以用RenderTransform优雅地翻转,但翻转之后文字也跟着颠倒,得再给文字做一次反向变换。GDI+ 虽然也得手工换算,可你被迫在代码里把「世界坐标 → 屏幕坐标」写清楚,调试时能一行行打日志,反而更好定位。这个取舍我认,图纸查看器不是做视觉特效,可控性优先。
项目本身的类型是经典的 WinForms 单窗口应用:一个MainForm承载PictureBox或自绘Control,左边挂一个图层勾选列表,底部状态栏显示鼠标处的世界坐标。没有引入任何第三方 DXF 库,全部自己按组码解析,好处是源码完全可控,坏处是你得自己啃格式细节,这也正是这份资料值得拆的地方。
2.2 目录结构与各文件职责
源码的组织方式偏教学向,按「模型 / 解析 / 渲染 / 界面」四层切分,文件不多但职责清楚。整理成下面这样看会直观很多:
DxfViewer/ ├─ DxfViewer.sln ├─ DxfViewer/ │ ├─ Program.cs // 入口,Application.Run(MainForm) │ ├─ MainForm.cs // 窗口、菜单、图层列表、鼠标缩放 │ ├─ MainForm.Designer.cs // VS2010 设计器生成 │ ├─ Model/ │ │ ├─ DxfEntity.cs // 实体基类:Layer、Color、Handle、Draw() │ │ ├─ DxfLine.cs // LINE │ │ ├─ DxfCircle.cs // CIRCLE │ │ ├─ DxfArc.cs // ARC │ │ ├─ DxfPolyline.cs // LWPOLYLINE / POLYLINE + VERTEX │ │ ├─ DxfText.cs // TEXT / MTEXT │ │ ├─ DxfInsert.cs // INSERT 块引用 │ │ ├─ DxfBlock.cs // BLOCK 定义,含基点 │ │ └─ DxfLayer.cs // 图层名、ACI 颜色、开关状态 │ ├─ Parser/ │ │ ├─ DxfReader.cs // 组码流读取器,逐行产出 (code, value) │ │ ├─ DxfDocument.cs // 一次解析的整体结果:图层表、块表、实体表 │ │ ├─ SectionParser.cs // SECTION 分发 │ │ └─ EntityFactory.cs // 按实体类型名创建对象 │ ├─ Render/ │ │ ├─ Viewport.cs // 缩放、平移、世界坐标↔屏幕坐标 │ │ ├─ DxfRenderer.cs // 遍历实体并绘制 │ │ └─ AciColorTable.cs // 256 色调色板 │ └─ Properties/ └─ Sample/ // 几份测试用 dxf这个结构不是必须照抄,但它把「解析」和「绘制」彻底分开了,后面做二次开发时你能只改渲染层而不动解析代码。实际动手做的时候,我建议先只建DxfReader和DxfLine、DxfCircle两类,让窗口能画出一条线,再把其余实体一个个补进去。一次性把八种实体全写完,调试时会分不清是解析错了还是画错了。
提示:
Model里的实体类不要持有Graphics对象,也不要在解析阶段计算屏幕坐标。实体只保存世界坐标,绘制时由Viewport统一换算,这条边界一旦破了,缩放功能就没法做对。
2.3 DXF 的组码模型:先看懂文件再写解析器
DXF 本质是「一组成对的文本行」:第一行是组码(整数),第二行是值,组码决定了这个值该被理解成字符串、双精度浮点还是整数。看一段真实片段比看十页文档管用:
0 SECTION 2 ENTITIES 0 LINE 8 设备层 10 100.0 20 200.0 30 0.0 11 400.0 21 200.0 31 0.0 0 CIRCLE 8 设备层 10 250.0 20 200.0 30 0.0 40 50.0 0 ENDSEC读法很直白:0是结构标记,后面跟SECTION表示开新段,2跟段名;段内每个0开始一个新实体,8是图层名,10/20/30是第一个点的 XYZ,11/21/31是第二个点,40对 CIRCLE 是半径、对 TEXT 是字高。0 ENDSEC收尾。
真正需要记牢的组码没几个:0结构、2名称、5句柄、8图层、62颜色、6线型、10/20/30和11/21/31和12/22/32点、40数值、41/42/43XYZ 缩放、50/51角度、70标志位、90顶点数、42(在 LWPOLYLINE 顶点里)是凸度、210/220/230拉伸方向。其余组码遇到不认识的就跳过,不要报错——DXF 里存在大量扩展数据(组码 1000 以上)和注释(组码 999),一概不理。
有一个容易忽略的层次:TABLES段里的LAYER表定义了每个图层的颜色和线型,BLOCKS段里定义了块几何,ENTITIES段里是真正的模型空间实体。三者顺序是固定出现的,所以解析器可以先读 TABLES 建图层字典,再读 BLOCKS 建块字典,最后读 ENTITIES 时遇到INSERT就能直接把块几何拿过来。反过来先读 ENTITIES 也能做,只是块引用得挂起等后面再展开,徒增复杂度。
3. 解析内核拆解:从组码流到实体对象的一条链路
3.1 组码读取器:把逐行文本变成 (code, value) 对
整个解析的地基就这一百来行。核心是把StreamReader.ReadLine的两行一组读出来,转成组码和字符串值,其余全交给上层。写成这样:
public class DxfReader : IDisposable { private readonly StreamReader _sr; private int _lineNo = 0; public DxfReader(string path) { // detectEncodingFromByteOrderMarks: 让 StreamReader 自己吃掉 UTF-8 BOM _sr = new StreamReader(path, Encoding.Default, true); } public bool EndOfFile { get { return _sr.Peek() < 0; } } public int Code { get; private set; } public string Value { get; private set; } /// <summary> /// 读下一对组码/值,返回 false 表示文件结束 /// </summary> public bool Read() { string codeLine = _sr.ReadLine(); if (codeLine == null) return false; _lineNo++; // 空行和 999 注释行直接跳过,不要抛异常 while (codeLine.Trim().Length == 0) { codeLine = _sr.ReadLine(); if (codeLine == null) return false; _lineNo++; } Code = int.Parse(codeLine.Trim()); // 组码行一定是整数 Value = _sr.ReadLine(); // 值行不做 Trim!文字里的空格是内容 if (Value == null) return false; _lineNo++; if (Code == 999) return Read(); // 注释组码,递归读下一对 return true; } public double ReadDouble() { return double.Parse(Value, CultureInfo.InvariantCulture); } public int ReadInt() { return int.Parse(Value.Trim()); } public void Dispose() { _sr.Dispose(); } }逻辑上有三个点值得说。第一,Encoding.Default在中文系统上是 GB2312,AutoCAD 2004 及以前的 ANSI 版 DXF 正好能对上;如果图纸是 2007 以后的 UTF-8 版本,靠第二个参数detectEncodingFromByteOrderMarks: true从 BOM 自动识别,没有 BOM 时再走后面的兜底策略。第二,值行千万不能Trim(),TEXT 实体的文字内容前后可能本来就有空格,一 Trim 就改了图面。第三,Code == 999是注释,直接递归跳过,比在上层到处判断要干净。
CultureInfo.InvariantCulture这一处是老手才会注意的细节。DXF 里的浮点用小数点,如果程序跑在把小数点当千分位的区域设置下,double.Parse("1.5")可能得到 15,这种玄学问题排查起来能耗掉一整天。
3.2 段分发与表结构:SECTION / TABLE / BLOCK 的处理顺序
有了读对的能力,下一层是一个状态机。整个文件的外层只有三种状态:不在任何段里、在某个段里、在段内的某张表里。用switch (reader.Code)加switch (reader.Value)组合处理就够:
public DxfDocument Parse(string path) { var doc = new DxfDocument(); using (var reader = new DxfReader(path)) { string section = null; while (reader.Read()) { if (reader.Code == 0) { switch (reader.Value) { case "SECTION": // 下一对组码是 2,段名 reader.Read(); section = reader.Value; continue; case "ENDSEC": section = null; continue; } // 段内的实体/表结构分发 if (section == "TABLES") ParseTable(reader, doc); else if (section == "BLOCKS") ParseBlock(reader, doc); else if (section == "ENTITIES") { var ent = EntityFactory.Create(reader.Value); if (ent != null) { ent.Read(reader); doc.Entities.Add(ent); } } } } } return doc; }ParseTable里只关心LAYER表:遇到0 TABLE再读一个2拿到表名,如果是LAYER就一直读到0 ENDTAB,中间每个0 LAYER建一个DxfLayer,用2取名、62取 ACI 颜色、6取线型。注意62为负值表示图层是关闭的,颜色要取绝对值,关闭状态单独记一位。
ParseBlock稍微绕一点:0 BLOCK之后先用2拿块名、10/20/30拿块基点,然后块内的实体同样是0 LINE、0 CIRCLE这种结构,可以直接丢给同一个EntityFactory处理,直到遇到0 ENDBLK结束。也就是说实体解析函数必须能被 ENTITIES 段和 BLOCKS 段复用,这一点在设计EntityFactory时就要考虑到,别把「当前在哪个段」写进实体类里。
注意:
LAYER表可能在文件里出现多次(同名覆盖),遇到重复图层名要用后出现的覆盖前面的,不要用Dictionary.Add直接抛异常。
3.3 实体工厂:LINE、CIRCLE、ARC、LWPOLYLINE、TEXT、INSERT 怎么落地
实体解析的差异部分用虚方法Read(DxfReader)各自实现,公共部分(图层、颜色、句柄)在基类里收口。工厂只负责按名字 new 对象:
public static class EntityFactory { public static DxfEntity Create(string type) { switch (type) { case "LINE": return new DxfLine(); case "CIRCLE": return new DxfCircle(); case "ARC": return new DxfArc(); case "LWPOLYLINE": return new DxfPolyline(); case "POLYLINE": return new DxfPolyline(); // 旧式,另走 VERTEX 分支 case "TEXT": case "MTEXT": return new DxfText(); case "INSERT": return new DxfInsert(); default: return null; // 不认识的实体一律忽略 } } }基类里读公共属性,注意62缺省时是 BYLAYER(256),要留给渲染阶段去图层表里查真实颜色:
public abstract class DxfEntity { public string Layer = "0"; public int Color = 256; // 256 = BYLAYER, 0 = BYBLOCK public string Handle = ""; public abstract void Read(DxfReader r); protected void ReadCommon(DxfReader r) { switch (r.Code) { case 8: Layer = r.Value; break; case 62: Color = r.ReadInt(); break; case 5: Handle = r.Value; break; } } }LWPOLYLINE是整份源码里最值得看的一个,因为它同时考了「重复组码」和「凸度转圆弧」两件事。读法是循环:90拿顶点数,70拿标志(bit 1 表示闭合),然后每遇到一个10就开始一个新顶点,紧跟的20是 Y,可选的42是这一段的凸度。凸度bulge = tan(θ/4),θ 是该段圆弧的圆心角,正值为逆时针。0 就是直线段。渲染时凸度非零的段要按「两端点 + 凸度」反算圆心和起止角,这一段公式写错的话图面看起来会很怪,但程序不报错,属于典型的「不崩但画错」。
INSERT只保存块名、插入点、XYZ 缩放和旋转角,真正的几何在渲染时到块字典里去取。这样同一份块定义被引用一百次也只存一份,内存和加载速度都会好很多。
3.4 块引用与变换矩阵:INSERT 的平移旋转缩放
块引用的坐标变换是新手最容易栽的地方。正确顺序是:块内实体坐标先减去块基点(把块基点挪到原点),再依次应用缩放、旋转,最后平移到插入点。顺序错了图就飞了。写成矩阵:
/// <summary> /// 构造 INSERT 的变换矩阵:先平移到原点(减基点) → 缩放 → 旋转 → 平移到插入点 /// </summary> public static Matrix BuildInsertMatrix(DxfInsert ins, DxfBlock block) { var m = new Matrix(); m.Translate(-block.BasePoint.X, -block.BasePoint.Y); // 1. 基点归零 m.Scale((float)ins.ScaleX, (float)ins.ScaleY); // 2. 缩放 m.Rotate((float)(ins.Rotation * Math.PI / 180.0)); // 3. 旋转,DXF 存的是角度 m.Translate((float)ins.InsertPoint.X, (float)ins.InsertPoint.Y); // 4. 平移到插入点 return m; }这里有三个参数坑。Matrix.Rotate要的是弧度,DXF 的50组码存的是角度,必须乘 π/180,忘了这一步图纸会旋转得莫名其妙。41/42/43缺省值都是 1,不是 0,解析时字段初始化就要设成 1,否则缩放为 0 之后整块几何会塌成一个点,界面上什么都看不到。Matrix在 GDI+ 里是行向量左乘,Translate之后再Scale效果是「先缩放后平移」,跟数学课本上的列向量写法相反,这一点实测一次就记住了。
块还可能嵌套,块 A 里引用块 B。稳妥做法是加一个递归深度上限(比如 16 层),超过就停并记日志,否则一旦图纸里存在循环引用(有些导出工具真能造出来),解析线程会直接栈溢出,整个窗口卡死。
4. 显示层实现:世界坐标到屏幕像素的映射与双缓冲重绘
4.1 视图变换:缩放、平移与 Y 轴翻转
视口只需要三个字段:缩放比例、屏幕上的偏移、以及窗口客户区尺寸。世界坐标转屏幕写成:
public PointF WorldToScreen(double wx, double wy) { // 先缩放到像素,再翻转 Y(DXF 向上、屏幕向下),最后加平移量 float sx = (float)(wx * Scale) + OffsetX; float sy = (float)(-wy * Scale) + OffsetY; return new PointF(sx, sy); } public PointF ScreenToWorld(float px, float py) { double wx = (px - OffsetX) / Scale; double wy = -(py - OffsetY) / Scale; // 注意负号 return new PointF((float)wx, (float)wy); }PageUnit保持Pixel,不要改成毫米或英寸,否则 DXF 的图纸单位会被 GDI+ 二次解释,越算越乱。Scale的初值不要拍脑袋写 1,正确做法是先用HEADER段的$EXTMIN/$EXTMAX或者遍历全部实体求包围盒,算出「窗口宽 / 图形宽」和「窗口高 / 图形高」取小值,再乘 0.9 留边距,这样打开任何一份图纸都能一屏看全,用户第一眼的体验差别很大。
鼠标滚轮缩放的锚点是鼠标位置,不是窗口中心。实现思路是:缩放前把鼠标点转成世界坐标w0,改完Scale后用同一个屏幕点反算回世界坐标w1,再把Offset加上(w1 - w0) * Scale,视觉上就是以鼠标为中心放大,不这么做的话每次放大图都往窗口中心跑,用户得反复平移,很难用。
4.2 绘制器:每个实体一个 Draw 方法
渲染层只认世界坐标,绘制前统一做一次变换。直线的绘制最简单,圆弧需要做角度转换:
public override void Draw(Graphics g, Viewport vp, Color color) { // DXF 的角是「度、逆时针、X 轴正向为 0」;GDI+ DrawArc 的角是「度、顺时针」 float start = (float)(-EndAngle); // 取负实现转向 float sweep = (float)(EndAngle - StartAngle); // 逆时针弧的扫过角 if (sweep <= 0) sweep += 360f; // 处理跨 0 度的情况 var c = vp.WorldToScreen(Center.X, Center.Y); float r = (float)(Radius * vp.Scale); // 半径太小就退化成一个点,省掉一次无意义的 DrawArc if (r < 0.5f) { g.FillRectangle(new SolidBrush(color), c.X, c.Y, 1, 1); return; } using (var pen = new Pen(color, 1f)) g.DrawArc(pen, c.X - r, c.Y - r, r * 2, r * 2, start, sweep); }关键是那个start = -EndAngle。DXF 里圆弧的起点角是50、终点角是51,逆时针从起点扫到终点;GDI+ 的DrawArc是从起始角开始顺时针扫。取负号相当于把镜像做掉,扫过角保持正值即可。跨 0 度的情况(比如起点 350°、终点 10°)必须靠sweep += 360补回来,否则会画出一段反向的弧,图面上看着像多了一条线,其实是圆弧绕远路走了。
线的颜色不要每个实体都new Pen,几万个实体这么写会疯狂产生 GC 压力。稳妥做法是在DxfRenderer里维护一个Dictionary<int, Pen>,按 ACI 颜色缓存画笔,绘制结束统一Dispose。
4.3 ACI 颜色与图层开关:BYLAYER 到底怎么解析
DXF 的62组码是 ACI 颜色索引,1~255 对应一张固定调色板,最常用的几个是 1 红、2 黄、3 绿、4 青、5 蓝、6 洋红、7 白/黑(随背景)、8 灰、9 浅灰。完整的 256 色调色板在AciColorTable里一次性初始化成数组,索引取值即可。
真正的规则在于继承关系。实体的62是 256(BYLAYER)时,颜色来自所属图层;是 0(BYBLOCK)时,颜色来自外层 INSERT 的颜色。图层表里62为负表示图层关闭,取绝对值当颜色用,同时这个图层上的实体不绘制。图层冻结(70组码的 bit 1)和锁定(bit 4)处理方式不同,冻结是不绘制,锁定是照常绘制但不允许编辑,查看器里只需要处理冻结。
我的习惯是把「实体颜色解析」独立成一个小函数,输入实体和文档,输出最终 ARGB 值,这样图层开关、BYLAYER、BYBLOCK 三个判断集中在一处,调起来快:
public static Color Resolve(DxfEntity e, DxfDocument doc, Color inherited) { int aci = e.Color; if (aci == 0) return inherited; // BYBLOCK,用外层传入色 if (aci == 256) // BYLAYER,查图层表 { DxfLayer layer; aci = doc.Layers.TryGetValue(e.Layer, out layer) ? layer.Color : 7; } if (aci < 0) aci = -aci; return AciColorTable.Get(aci); }4.4 大图重绘:双缓冲与视口裁剪
图纸一大,重绘就会闪。WinForms 自绘控件里加两行:
public DxfCanvas() { SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.UserPaint, true); UpdateStyles(); }这三项组合的效果是:绘制全部走OnPaint,先画到后台缓冲区再一次性贴到屏幕,闪烁基本消失。别在OnPaint里调Refresh(),会形成死循环。
第二个性能点是视口裁剪。几万个实体里,屏幕外的那部分完全不用画。给每个实体缓存一个世界坐标包围盒,OnPaint里先用视口反算当前可见的世界矩形做个粗筛,只对相交的实体调Draw。实测一份三万实体的图纸,加裁剪之后平移的帧率能从十几帧回到流畅。粗筛精度要求不高,宁可多画几个也不要把该画的筛掉,块引用展开后的实体包围盒要按变换后的坐标重算。
5. 避坑与排查:五个真实卡过我的问题
5.1 一打开就抛「输入字符串的格式不正确」
现象:int.Parse在DxfReader.Read()里炸掉,堆栈指向组码行。原因通常有三种:文件是二进制 DXF(开头有AutoCAD Binary DXF标记,后面全是字节流,不是文本);文件被别的工具转存过,带了 BOM 或行尾混用\r\n和\n;组码行前后有空行。解决办法是先读前 22 个字节判断是不是二进制,是就直接提示不支持并给出转换建议;文本模式则用Trim()后再解析,并把空行跳过,同时加一个_lineNo字段,异常信息里把行号带出来,不然几万行的文件根本定位不到。
5.2 中文图层名和文字全是乱码
现象:图元能画出来,但图层列表里全是问号。原因在编码:AutoCAD 2004 及更早的 DXF 用 ANSI 代码页(简体中文是 GBK),2007 之后默认 UTF-8。HEADER段的$ACADVER和$DWGCODEPAGE能告诉你答案,AC1021及以上是 UTF-8,AC1018及以下是 GBK。工程上的做法是先用Encoding.Default读,如果发现HEADER里声明的是ANSI_936就保持不动,声明为 UTF-8 就重新以new UTF8Encoding(false)打开重读一遍。别指望一个编码吃遍所有文件。
5.3 块引用全部堆在原点,或者整体偏移了一大截
现象:图纸里明明有几十个设备符号,打开后全叠在 (0,0)。原因是没有应用块基点,或者矩阵顺序写反了。BLOCK定义里的10/20/30是基点,块内几何是相对这个基点画的,正确顺序是「减基点 → 缩放 → 旋转 → 加插入点」。如果发现所有块整体偏移了一个固定量,八成是把基点加了两次或者顺序颠倒。排查方法是找一份只含单个 INSERT 的最小图纸,把矩阵每一步的中间结果打出来,对照HEADER里的$EXTMIN看是否吻合。
5.4 圆弧画反了,或者明明 90 度的弧画成了 270 度
现象:圆弧的位置对,但开口方向反了,或者绕着远路画了一圈。原因有两个:GDI+ 的DrawArc角度方向与 DXF 相反,需要start = -endAngle;跨 0 度时扫过角为负,要加 360 修正。另外51小于50的情况在 DXF 里是合法的(表示跨过 0 度),不要在解析阶段就做归一化,留到绘制阶段统一处理,否则会丢失原始信息。
5.5 平移时画面抖、拖影,大图直接卡住
现象:鼠标一拖,图面残影一串,重新打开大文件要等十几秒。原因通常是没开双缓冲、每次Paint都重新解析文件、以及没有视口裁剪。前两个是设计问题:解析只做一次,结果放在DxfDocument里常驻内存;绘制走OnPaint加双缓冲。第三个是性能优化,加包围盒粗筛即可。还有一种情况是Pen每帧 new 上千个,GC 跟不上,按颜色缓存画笔能立刻缓解。
提示:如果图纸里出现 210/220/230 拉伸方向不是 (0,0,1) 的圆或弧,需要走 OCS(对象坐标系)换算,先按任意轴算法求出变换矩阵再绘制,否则圆会变成偏心椭圆。二维图纸里这种情况少见,但机械类图纸里确实存在。
6. 二次开发:把只读查看器改成能用的业务组件
拿到这份源码后,多数人不会停在「能打开看」这一步,而是要嵌进自己的系统。我一般从三个方向改,按投入产出排序。
第一是实体拾取。查看器只能看不能选,业务上就没法用。做法是在Viewport里加一个HitTest(Point screenPt, double tolerancePx),把屏幕点反算成世界坐标,然后遍历可见实体算点到线、点到圆的距离,返回最近的那个。包围盒先做粗筛,精度用tolerancePx / Scale换算成世界单位,这样缩放级别不同也能保持手感一致。选中之后高亮绘制,记得把选中状态和原始实体分开存,别直接改实体颜色。
第二是数据提取。图纸里真正值钱的是信息而不是像素,比如统计某个图层上所有线段的长度、把所有INSERT的块名和插入点导成表格、把TEXT内容当设备编号读出来。这块只需要在DxfDocument上写 LINQ 查询,完全不用碰渲染层:
// 统计"设备层"上所有直线的总长度,单位与图纸一致 var total = doc.Entities .OfType<DxfLine>() .Where(l => l.Layer == "设备层") .Sum(l => Math.Sqrt(Math.Pow(l.End.X - l.Start.X, 2) + Math.Pow(l.End.Y - l.Start.Y, 2)));第三是导出。客户经常要「把当前视图截个图发给别人」,Render到Bitmap再存 PNG 就行,注意导出时用离屏的Graphics,不要截窗口,否则会把工具栏和滚动条一起截进去。分辨率可以按当前Scale乘 2 或 3 做超采样,文字边缘会干净很多。
改造完怎么验证?我的方法是拿一份自己用 CAD 画的标准图形做基准:一个已知边长的矩形、一个已知半径的圆、一个块引用两次的符号、一段带中文的标注文字。每次改完解析或渲染,打开这份基准图,用状态栏的坐标读数去对关键点,长度误差控制在 0.5% 以内、文字不乱码、块位置正确,就可以认为没有回归。这套流程比盯着复杂图纸看要可靠得多。
血泪经验是:任何一次对DxfReader的改动,都要拿二进制 DXF、GBK 编码的旧版 DXF、UTF-8 的新版 DXF 三份文件各跑一遍再提交,我曾经为了兼容一个客户的文件改了编码处理,结果把原本正常的十几份图纸全读成了乱码,回滚加排查花了大半天。从那以后我每次动解析层都强制走一遍这三份回归样本,希望帮到你。
本文还有配套的精品资源,点击获取