资讯详情

资讯详情

C++手搓DXF显示程序:MFC解析组码实现图形绘制与缩放平移

简介这是一份面向C与MFC初学者及CAD图形开发者的DXF文件显示程序源码基于Visual Studio工程构建可加载并可视化DXF矢量图形帮助理解文件解析与界面绘制的完整链路。压缩包共195个文件约5.26MB以h头文件、cpp源文件、obj编译中间文件为主另含sln、dsw、vcproj等工程配置sample.dxf示例文件、ReadMe.md说明、ScreenShot.png界面截图及Bin可执行目录结构完整便于直接编译运行。程序覆盖DXF分段解析、MFC窗口与消息处理、CDC绘图与坐标转换、文件路径限制等关键知识点并附示例数据供测试。已有873人学习下载适合作为CAD相关软件开发或DXF数据处理项目的参考范例帮助读者快速掌握解析、渲染与工程配置的实践思路。1. 从一份 DXF 文件说起为什么我选择用 MFC 手搓一个显示程序前阵子帮一个做机械加工的朋友处理图纸他发来一堆.dxf文件问我能不能写个小工具直接打开看轮廓别每次都开笨重的 CAD。我第一反应是找现成库但试了几个要么依赖太重要么在 Windows 上编译各种报错。后来干脆用最朴素的路子C 直接解析 DXF 的组码MFC 负责画窗口。这就是这个「自制 DXF 显示 MFC 界面程序」的由来——它不追求渲染多华丽目标很明确把 DXF 里的直线、圆弧、圆、多段线读出来在 MFC 视图里按坐标画出来支持缩放和平移。适合谁适合正在学 C、想搞懂图形文件解析、又需要一个能跑起来的 MFC 练手项目的开发者。下面我按「能干什么 → 怎么跑 → 怎么改」的顺序把这份源码拆开讲。2. 项目定位与技术选型为什么是 MFC 原生解析而不是 Qt 或第三方库2.1 这个程序到底解决什么问题DXFDrawing Exchange Format是 CAD 领域通用的矢量图形交换格式本质是纯文本用「组码 值」成对描述实体。这个程序做的事就三件读文件、解析实体、画到窗口。它不处理三维、不处理图层样式、不做编辑只做「看」。听起来简单但真动手会发现坑不少DXF 有 ASCII 和二进制两种版本从 R12 到 2018 差异很大实体类型几十种。这个项目只覆盖最常见的 LINE、CIRCLE、ARC、LWPOLYLINE 四类够看大部分二维轮廓图。为什么不用 Qt因为目标环境是 Windows 桌面MFC 是 VS 自带不用配额外依赖编译出来就是单个 exe发给朋友直接能跑。为什么不用第三方 DXF 库一是学习目的二是很多库为了通用性把代码写得极重读源码反而费劲。自己解析组码几百行就能跑通改起来心里有数。2.2 技术栈与目录结构技术栈很干净C11、MFC基于对话框或 SDI 单文档、GDI 绘图、标准库文件流。没有 OpenGL没有 DirectX所有绘制走CDC的MoveTo、LineTo、Ellipse、Arc。项目结构大致如下DxfViewer/ ├── DxfViewer.sln ├── DxfViewer/ │ ├── DxfViewer.h/.cpp // 应用入口 │ ├── MainFrm.h/.cpp // 主框架窗口 │ ├── DxfViewerView.h/.cpp // 视图类负责绘制 │ ├── DxfViewerDoc.h/.cpp // 文档类持有实体数据 │ ├── DxfParser.h/.cpp // DXF 解析核心 │ ├── DxfEntity.h // 实体数据结构 │ └── res/ // 图标资源这个划分是 MFC 文档/视图架构的标准做法Doc存数据View负责显示Parser独立出来方便复用。我一般会把解析逻辑和界面彻底分开这样以后想换渲染方式比如换成 GDI 或 Direct2D只动 View 就行。2.3 实体数据结构设计解析出来的每个图形需要一个统一的结构来存。这里用基类 派生类的做法// DxfEntity.h enum class EntityType { LINE, CIRCLE, ARC, POLYLINE }; struct DxfEntity { EntityType type; COLORREF color; // 实体颜色默认黑 virtual ~DxfEntity() default; }; struct DxfLine : DxfEntity { double x1, y1, x2, y2; // 起点终点 }; struct DxfCircle : DxfEntity { double cx, cy, r; // 圆心半径 }; struct DxfArc : DxfEntity { double cx, cy, r; double startAngle, endAngle; // 弧度制 }; struct DxfPolyline : DxfEntity { std::vectorstd::pairdouble,double points; bool closed; };用enum class而不是裸枚举避免命名污染。坐标统一用double因为 DXF 里坐标精度可能到小数点后好几位用int会丢精度。颜色先留字段实际解析时如果组码 62 存在就赋值不存在就用默认。这个结构后面画图时直接switch(type)分发即可。3. 解析核心从组码到实体一步步把 DXF 读进内存3.1 DXF 文件格式速览DXF 是成对出现的一行组码整数一行值。比如0 LINE 8 0 10 100.0 20 50.0 11 200.0 21 150.00表示实体开始值是LINE8是图层名10/20是起点 X/Y11/21是终点 X/Y。解析就是循环读这两行根据组码决定当前在填哪个字段。关键点组码和值可能有多余空格读进来要trim文件可能以EOF结尾遇到就停。3.2 解析器主循环实现// DxfParser.cpp bool DxfParser::Parse(const std::string path, std::vectorstd::unique_ptrDxfEntity out) { std::ifstream fin(path); if (!fin) return false; std::string codeLine, valueLine; std::unique_ptrDxfEntity current; DxfPolyline* poly nullptr; while (std::getline(fin, codeLine) std::getline(fin, valueLine)) { int code std::stoi(Trim(codeLine)); std::string val Trim(valueLine); if (code 0) { // 遇到新实体先把上一个存起来 if (current) out.push_back(std::move(current)); if (val LINE) current std::make_uniqueDxfLine(); else if (val CIRCLE) current std::make_uniqueDxfCircle(); else if (val ARC) current std::make_uniqueDxfArc(); else if (val LWPOLYLINE) { auto p std::make_uniqueDxfPolyline(); poly p.get(); current std::move(p); } else if (val EOF) break; else current.reset(); // 不支持的实体先跳过 } else if (current) { FillEntity(current.get(), poly, code, val); } } if (current) out.push_back(std::move(current)); return true; }逻辑说明外层while每次读两行保证组码和值配对。code 0是实体分隔符先把上一个实体move进结果集再根据值创建新实体。LWPOLYLINE特殊因为它的顶点是多个10/20 组码连续出现需要额外指针poly记录当前多段线。EOF直接跳出。不认识的实体比如TEXT、DIMENSION就reset掉不解析也不报错保证程序不崩。参数说明path是文件路径out是输出实体列表。返回false只表示文件打不开解析过程中遇到未知实体不算失败。3.3 字段填充与多段线处理void DxfParser::FillEntity(DxfEntity* e, DxfPolyline* poly, int code, const std::string val) { double d 0.0; try { d std::stod(val); } catch (...) { return; } switch (e-type) { case EntityType::LINE: { auto* l static_castDxfLine*(e); if (code 10) l-x1 d; else if (code 20) l-y1 d; else if (code 11) l-x2 d; else if (code 21) l-y2 d; break; } case EntityType::CIRCLE: { auto* c static_castDxfCircle*(e); if (code 10) c-cx d; else if (code 20) c-cy d; else if (code 40) c-r d; break; } case EntityType::ARC: { auto* a static_castDxfArc*(e); if (code 10) a-cx d; else if (code 20) a-cy d; else if (code 40) a-r d; else if (code 50) a-startAngle d * 3.1415926 / 180.0; else if (code 51) a-endAngle d * 3.1415926 / 180.0; break; } case EntityType::POLYLINE: { if (code 10) poly-points.emplace_back(d, 0.0); else if (code 20 !poly-points.empty()) poly-points.back().second d; else if (code 70) poly-closed (static_castint(d) 1) ! 0; break; } } }这里注意 ARC 的角度DXF 里是度GDI 的Arc用的是弧度所以解析时直接转。多段线的70组码是标志位最低位为 1 表示闭合。10和20是成对出现的所以10先 push 一个点20再补 Y 值。如果文件里20出现在10之前极少见但存在这里会丢点稳妥做法是缓存上一个 X但多数 DXF 都按顺序来先这样处理够用。4. 绘制与交互把实体画到 MFC 视图并支持缩放平移4.1 视图类绘制入口MFC 里绘制走OnDraw但为了支持缩放平移我一般把绘制逻辑抽到Render函数OnDraw只负责调用。核心是维护一个「世界坐标 → 屏幕坐标」的变换screen (world - offset) * scale。// DxfViewerView.cpp void CDxfViewerView::OnDraw(CDC* pDC) { CDxfViewerDoc* pDoc GetDocument(); CRect rect; GetClientRect(rect); // 双缓冲避免闪烁 CDC memDC; memDC.CreateCompatibleDC(pDC); CBitmap bmp; bmp.CreateCompatibleBitmap(pDC, rect.Width(), rect.Height()); memDC.SelectObject(bmp); memDC.FillSolidRect(rect, RGB(255,255,255)); for (auto e : pDoc-m_entities) { DrawEntity(memDC, e.get()); } pDC-BitBlt(0, 0, rect.Width(), rect.Height(), memDC, 0, 0, SRCCOPY); }双缓冲是 MFC 绘图的老规矩不加的话缩放时闪得没法看。m_entities是文档类里存的实体列表解析完直接塞进去。4.2 坐标变换与实体绘制CPoint CDxfViewerView::WorldToScreen(double x, double y) { int sx static_castint((x - m_offsetX) * m_scale); int sy static_castint(m_clientHeight - (y - m_offsetY) * m_scale); return CPoint(sx, sy); } void CDxfViewerView::DrawEntity(CDC* pDC, DxfEntity* e) { CPen pen(PS_SOLID, 1, RGB(0,0,0)); CPen* old pDC-SelectObject(pen); switch (e-type) { case EntityType::LINE: { auto* l static_castDxfLine*(e); pDC-MoveTo(WorldToScreen(l-x1, l-y1)); pDC-LineTo(WorldToScreen(l-x2, l-y2)); break; } case EntityType::CIRCLE: { auto* c static_castDxfCircle*(e); CPoint center WorldToScreen(c-cx, c-cy); int r static_castint(c-r * m_scale); pDC-Ellipse(center.x - r, center.y - r, center.x r, center.y r); break; } case EntityType::ARC: { auto* a static_castDxfArc*(e); CPoint center WorldToScreen(a-cx, a-cy); int r static_castint(a-r * m_scale); // GDI Arc 角度是逆时针且 Y 轴向下需要转换 pDC-Arc(center.x - r, center.y - r, center.x r, center.y r, WorldToScreen(a-cx a-r*cos(a-startAngle), a-cy a-r*sin(a-startAngle)).x, WorldToScreen(a-cx a-r*cos(a-startAngle), a-cy a-r*sin(a-startAngle)).y, WorldToScreen(a-cx a-r*cos(a-endAngle), a-cy a-r*sin(a-endAngle)).x, WorldToScreen(a-cx a-r*cos(a-endAngle), a-cy a-r*sin(a-endAngle)).y); break; } case EntityType::POLYLINE: { auto* p static_castDxfPolyline*(e); if (p-points.empty()) break; pDC-MoveTo(WorldToScreen(p-points[0].first, p-points[0].second)); for (size_t i 1; i p-points.size(); i) pDC-LineTo(WorldToScreen(p-points[i].first, p-points[i].second)); if (p-closed) pDC-LineTo(WorldToScreen(p-points[0].first, p-points[0].second)); break; } } pDC-SelectObject(old); }WorldToScreen里 Y 轴翻转是因为 DXF 的 Y 向上屏幕 Y 向下。m_clientHeight在OnSize里更新。圆弧的绘制是这里最绕的GDI 的Arc需要外接矩形和起止点起止点用角度算出来再转屏幕坐标。注意 GDI 角度方向是逆时针而屏幕 Y 翻转后视觉上会变成顺时针实际测试时如果弧画反了把起止角度对调即可。4.3 鼠标滚轮缩放与拖拽平移BOOL CDxfViewerView::OnMouseWheel(UINT nFlags, short zDelta, CPoint pt) { double factor (zDelta 0) ? 1.1 : 0.9; // 以鼠标位置为中心缩放 CPoint worldPt ScreenToWorld(pt); m_scale * factor; CPoint newScreen WorldToScreen(worldPt.x, worldPt.y); m_offsetX (newScreen.x - pt.x) / m_scale; m_offsetY - (newScreen.y - pt.y) / m_scale; Invalidate(); return TRUE; }以鼠标为中心缩放体验比以原点缩放好得多。ScreenToWorld是WorldToScreen的逆运算。平移在OnMouseMove里处理按下左键记录起点移动时算差值加到m_offsetX/Y。这些交互代码不长但决定了程序好不好用建议一开始就加上。5. 避坑与常见问题解析和绘制时最容易翻车的几个点5.1 现象打开某些 DXF 后程序直接崩溃原因std::stod遇到空字符串或非数字会抛异常而 DXF 里有些组码的值是文本比如图层名如果没判断就转数字直接崩。解决所有stod/stoi都包在try-catch里或者先判断字符串是否只含数字和符号。我在FillEntity里已经加了try但Parse主循环里的stoi(codeLine)也要加因为有些文件开头有 BOM 或空行。5.2 现象图形画出来是上下颠倒的原因DXF 坐标系 Y 轴向上Windows 屏幕坐标 Y 轴向下忘了翻转。解决WorldToScreen里用m_clientHeight - y做翻转。如果发现整体镜像检查是不是漏了这一步或者m_clientHeight没在OnSize里更新导致用了旧值。5.3 现象圆弧方向画反或者画出来是个大饼原因GDIArc的角度参数和 DXF 的起止角定义不一致加上 Y 翻转后方向变化。解决先确认 DXF 里角度是度还是弧度标准是度转弧度然后测试一个 0 到 90 度的弧看画出来是四分之一圆还是补角。如果反了把起止点参数对调。另外Arc的外接矩形必须用int半径乘scale后如果小于 1 会画不出加个最小值判断。5.4 现象大图纸缩放后线条消失或错位原因int溢出。坐标值乘scale后可能超过int范围比如坐标 100000scale 100就是 10^7接近int上限。解决中间计算用double最后转int前做 clamp或者用long long。另外m_scale要限制范围比如 0.01 到 1000防止用户滚太快导致数值爆炸。5.5 现象多段线只画出一个点原因LWPOLYLINE的顶点组码是10/20交替但有些文件里20可能缺失或顺序错乱。解决在FillEntity里维护一个「上一个 X」缓存遇到20时如果当前点列表为空或最后一个点已有 Y 值就补一个新点。更稳的做法是解析完整个实体后检查点列表把 Y 为默认值的点过滤掉。6. 二次开发与改造从「能看」到「好用」的几个方向这个项目跑通之后能改的地方很多。我一般会按优先级来先加图层控制再加测量最后考虑导出。图层控制最简单解析时把组码8的值存到实体里视图里加个CListBox列出所有图层勾选控制显示。测量功能也不难鼠标点两个点算距离用WorldToScreen的逆变换拿到世界坐标勾股定理一算就行。如果想让渲染更顺滑可以把 GDI 换成 GDIGraphics类自带抗锯齿圆弧和圆会好看很多。改造时注意GDI 的DrawLine需要Pen和PointF坐标变换逻辑不变只是把CDC换成Graphics。下面是个 GDI 画线的示例// 在 View 里初始化 GDI Gdiplus::Graphics graphics(pDC-GetSafeHdc()); graphics.SetSmoothingMode(Gdiplus::SmoothingModeAntiAlias); Gdiplus::Pen pen(Gdiplus::Color(255, 0, 0, 0), 1.0f); graphics.DrawLine(pen, (float)sx1, (float)sy1, (float)sx2, (float)sy2);参数说明SmoothingModeAntiAlias开抗锯齿Pen的宽度用float颜色用 ARGB。注意 GDI 需要GdiplusStartup和GdiplusShutdown一般在App类里做。另一个实用改造是支持拖拽打开文件。在OnDropFiles里拿路径调Parse然后UpdateAllViews。这样用户不用点菜单直接把 DXF 拖进窗口就行。我还会加个「适应窗口」按钮根据所有实体的包围盒自动算scale和offset省得手动调。最后说个血泪经验改解析器时千万别只测一个文件。我当初用一张简单图纸调通了结果换了个带多段线的文件发现70组码没处理闭合画出来全是断的。从那以后我每次改完解析逻辑都强制走一遍「直线 圆 弧 闭合多段线 开放多段线」这五种实体的测试文件确认都画对才提交。希望这个项目能帮你少走点弯路拿到源码后先跑通再按自己的需求一点点加功能比一上来就大改要稳得多。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →