资讯详情

C++ MFC 二维绘图源码实战:直线、圆与多边形填充算法详解

📅 2026/10/23 14:15:57 | 华诺云谱 👁 阅读
C++ MFC 二维绘图源码实战:直线、圆与多边形填充算法详解
简介这份资源是面向计算机图形学初学者与C Windows开发者的MFC二维绘图工具完整源代码帮助读者通过一个可运行的实例掌握图形界面编程与几何图形绘制。项目基于MFC的消息驱动模型涵盖自定义CView视图类、鼠标与键盘消息映射、CDC绘图接口、CBrush与CPen颜色线条管理、CFont文本输出、CFile与CFileDialog文件读写以及状态栏、菜单和内存设备上下文等核心知识点并配有异常捕获与ASSERT调试示例。压缩包共39个文件约1.94MB包含7个h头文件、6个cpp源文件及obj、ico、rc、vcproj、sln等工程与资源文件覆盖从源码到可执行程序的完整结构。目前已有699人学习下载适合作为课程设计、图形学实验或MFC入门练手项目读者可据此理解绘图工具从界面搭建到图形编辑、保存的完整实现思路。1. 从一份 C MFC 二维绘图源码说起它到底能解决什么问题很多做计算机图形学课程设计或者 Windows 桌面工具开发的人都会卡在同一个地方算法原理都懂直线插补、圆生成、多边形填充、二维变换这些概念背得滚瓜烂熟但真要交出一个能跑、能画、能交互的完整程序就不知道从哪下手了。这份 C MFC 源代码二维绘图工具就是冲着这个痛点来的——它把计算机图形学里最核心的二维图元生成与变换算法用 MFC 的单文档/多文档框架封装成了一个可编译、可运行、可继续扩展的工程。它适合三类人一是正在做图形学课设、需要一份能跑通的参考实现来对照自己代码的在校开发者二是想用 MFC 快速搭一个绘图类工具原型、不想从 CDC 绘图消息映射开始啃的 Windows 客户端开发者三是想通过读源码理解光栅化算法怎么落到像素上的自学者。源码里通常包含直线、圆、椭圆、多边形填充、二维平移旋转缩放这些模块配合鼠标交互完成绘制。下面我按“先看懂结构、再动手编译、然后逐个算法拆解、最后避坑和进阶”的顺序把这份资源拆开讲。2. 工程结构与编译环境把源码跑起来要过的几道关2.1 先认清 MFC 工程的骨架拿到一份 MFC 绘图源码别急着点编译。先看目录结构典型的 MFC 单文档工程会有这么几类文件解决方案文件.sln、项目文件.vcxproj、应用程序类App、主框架窗口类MainFrm、文档类Doc、视图类View以及资源文件.rc和资源头文件resource.h。绘图逻辑绝大多数落在 View 类的OnDraw和各个鼠标消息处理函数里因为视图类才是真正持有CDC*绘图设备上下文的地方。文档类和视图类的分工要理清文档类负责存数据比如你画了哪些图元、每个图元的参数视图类负责把数据画出来。很多初学者把图元数据直接塞在视图类里小工程能跑但一旦要做撤销重做、序列化保存就会很痛苦。这份源码如果结构规范图元数据应该在 Doc 类里以某种容器常见是CArray或std::vector保存View 类只负责遍历绘制。2.2 编译环境的版本匹配MFC 对 Visual Studio 版本和工具集比较敏感。源码如果是用某个版本的 VS 创建的换一个版本打开时项目属性里的“平台工具集”和“Windows SDK 版本”很可能对不上直接报“找不到 vXXX 工具集”或者一堆afxwin.h找不到的错误。处理办法是右键项目 → 属性 → 常规把平台工具集改成你本机装了的版本Windows SDK 版本也选本机存在的。另外要确认安装 VS 时勾选了“使用 C 的桌面开发”里的 MFC 组件这个组件默认不一定装。检查方法是在 VS Installer 里看单个组件搜“MFC”把对应版本勾上。# 如果不想开 VS可以用命令行确认 MFC 头文件是否存在 # 以 VS2022 为例路径随版本变化按实际安装调整 dir C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\*\atlmfc\include\afxwin.h这条命令的作用是确认afxwin.h这个 MFC 核心头文件在不在。如果返回“找不到文件”说明 MFC 组件没装编译必然失败。参数上注意*是通配工具集版本号因为 MSVC 目录名带具体版本用通配能避免手写错。2.3 字符集与预编译头MFC 工程常见两个编译坑字符集和预编译头。字符集分 Unicode 和多字节老源码很多是多字节字符集写的用char*和strcpy这类函数如果项目属性设成了 Unicode就会报一堆const char*不能转LPCWSTR的错误。解决方式是把项目属性 → 高级 → 字符集改成“使用多字节字符集”或者反过来把源码里的字符串都加_T()宏适配。预编译头pch.h老版本叫stdafx.h必须放在每个 .cpp 文件的第一行包含否则报“预编译头文件来自编译器的早期版本”或者直接找不到。如果源码里某个 cpp 忘了加补上即可。提示改字符集和工具集之后最好执行一次“生成 → 清理解决方案”再重新生成避免旧的中间文件干扰。3. 核心算法拆解直线、圆与多边形的像素级实现3.1 DDA 与 Bresenham 直线算法二维绘图工具里画直线看着简单但落到像素上就是另一回事。屏幕像素是离散的你给两个端点中间那些点怎么选就是直线生成算法要解决的。源码里一般会实现两种DDA 和 Bresenham。DDA 的思路是增量法按 x 或 y 方向步进每步算一次浮点坐标再取整。实现直观但涉及浮点运算。Bresenham 全程整数运算效率更高是实际工程里更常用的。下面这段是 Bresenham 的典型实现我按源码里常见的写法整理// Bresenham 直线算法假设 |dx| |dy|即斜率绝对值不超过 1 void DrawLineBresenham(CDC* pDC, int x0, int y0, int x1, int y1, COLORREF color) { int dx abs(x1 - x0); int dy abs(y1 - y0); int sx (x0 x1) ? 1 : -1; // x 步进方向 int sy (y0 y1) ? 1 : -1; // y 步进方向 int err dx - dy; // 误差项初值 while (true) { pDC-SetPixelV(x0, y0, color); // 画当前像素 if (x0 x1 y0 y1) break; int e2 2 * err; if (e2 -dy) { err - dy; x0 sx; } // x 方向走一步 if (e2 dx) { err dx; y0 sy; } // y 方向走一步 } }逻辑说明err是决策变量代表当前像素位置到理想直线的偏差。e2 2*err是为了避免浮点比较把判断放大两倍。两个if分别决定 x 和 y 是否步进这样能覆盖所有八分圆方向不用像教科书那样分八种情况写。参数上x0,y0是起点x1,y1是终点color是像素颜色。注意SetPixelV比SetPixel快因为它不返回实际绘制颜色绘图工具里优先用它。3.2 中点圆算法与椭圆生成圆生成常用中点圆算法本质是 Bresenham 思想在圆上的推广。利用八对称性只算八分之一圆弧其余七个对称点直接映射效率翻八倍。核心是决策变量判断下一个像素取正右方还是右下方。// 中点圆算法只画八分之一再对称 void DrawCircleMidpoint(CDC* pDC, int xc, int yc, int r, COLORREF color) { int x 0, y r; int d 1 - r; // 决策变量初值 while (x y) { // 八对称画点 pDC-SetPixelV(xc x, yc y, color); pDC-SetPixelV(xc - x, yc y, color); pDC-SetPixelV(xc x, yc - y, color); pDC-SetPixelV(xc - x, yc - y, color); pDC-SetPixelV(xc y, yc x, color); pDC-SetPixelV(xc - y, yc x, color); pDC-SetPixelV(xc y, yc - x, color); pDC-SetPixelV(xc - y, yc - x, color); if (d 0) { d 2 * x 3; } // 取正右方点 else { d 2 * (x - y) 5; y--; } // 取右下方点 x; } }逻辑说明d是决策变量d 0说明中点在圆内选正右方像素否则选右下方。d的更新公式2x3和2(x-y)5是推导后的整数形式避免每次开方。参数xc,yc是圆心r是半径。椭圆生成思路类似但对称性只有四对称决策变量推导更复杂源码里如果实现了椭圆通常单独一个函数参数多两个半轴长。3.3 多边形填充扫描线算法的边界处理多边形填充是绘图工具里最容易出 bug 的部分。扫描线算法的思路是对每条扫描线求它与多边形各边的交点排序后两两配对配对区间内的像素填充。听起来简单但顶点相交、水平边、奇偶性判断这几个地方全是坑。// 扫描线填充核心片段求交点并排序 struct Edge { double x; int ymax; double dxdy; }; // 活性边表 AET按 x 排序 std::sort(aet.begin(), aet.end(), [](const Edge a, const Edge b) { return a.x b.x; }); // 两两配对填充 for (size_t i 0; i 1 aet.size(); i 2) { int xStart (int)ceil(aet[i].x); int xEnd (int)floor(aet[i 1].x); for (int x xStart; x xEnd; x) pDC-SetPixelV(x, y, color); }逻辑说明aet是活性边表存当前扫描线相交的边每条边记录交点 x、边的最大 yymax和斜率倒数dxdy。排序后相邻两个交点构成一个填充区间。参数上ceil和floor是为了处理交点非整数的情况保证填充不越界。顶点相交的处理原则是如果顶点是局部极值点算两个交点如果不是算一个。水平边直接跳过不参与交点计算否则会多算。注意扫描线算法对自相交多边形和带孔多边形需要额外处理源码如果只支持简单凸/凹多边形遇到复杂图形会填充异常这是算法本身的边界不是代码 bug。4. 二维变换与交互鼠标消息怎么驱动图形4.1 平移、旋转、缩放的矩阵表示二维变换用齐次坐标的 3x3 矩阵统一表示平移、旋转、缩放都能写成矩阵乘法。源码里如果做了变换功能通常会有一个变换矩阵类或者直接用数组存。旋转要绕某点转标准做法是“平移到原点 → 旋转 → 平移回去”三步矩阵连乘。// 二维点绕 (cx,cy) 旋转 angle 弧度 void RotatePoint(double x, double y, double cx, double cy, double angle) { double rad angle * 3.14159265358979 / 180.0; // 角度转弧度 double cosA cos(rad), sinA sin(rad); double tx x - cx, ty y - cy; // 平移到原点 x cx tx * cosA - ty * sinA; // 旋转后平移回去 y cy tx * sinA ty * cosA; }逻辑说明先把点相对旋转中心平移旋转完再加回中心坐标。参数angle是角度制内部转弧度。cosA和sinA提前算好避免重复计算。如果要对整个图形变换需要对每个顶点都调用一次变换完重绘。4.2 鼠标消息映射与交互流程MFC 里鼠标交互靠消息映射。在 View 类的消息映射表里加ON_WM_LBUTTONDOWN、ON_WM_MOUSEMOVE、ON_WM_LBUTTONUP然后实现对应的OnLButtonDown、OnMouseMove、OnLButtonUp函数。典型流程是按下左键记录起点移动时如果处于绘制状态就画橡皮筋预览抬起时确定终点并把图元存入文档。void CMyView::OnLButtonDown(UINT nFlags, CPoint point) { m_bDrawing true; // 进入绘制状态 m_ptStart point; // 记录起点 m_ptEnd point; CView::OnLButtonDown(nFlags, point); } void CMyView::OnMouseMove(UINT nFlags, CPoint point) { if (m_bDrawing) { m_ptEnd point; // 更新终点 Invalidate(); // 触发重绘画预览 } CView::OnMouseMove(nFlags, point); }逻辑说明m_bDrawing是绘制状态标志m_ptStart和m_ptEnd存起点终点。Invalidate()会触发OnDraw重绘在OnDraw里根据当前状态决定画不画预览线。参数point是客户区坐标注意如果用了滚动条要转成逻辑坐标否则滚动后坐标会偏。4.3 双缓冲消除闪烁MFC 直接OnDraw里画图拖动时闪烁会很严重。标准解法是双缓冲先在内存 DC 里画好整幅图再一次性 BitBlt 到屏幕 DC。void CMyView::OnDraw(CDC* pDC) { CRect rect; GetClientRect(rect); CDC memDC; memDC.CreateCompatibleDC(pDC); // 创建兼容内存 DC CBitmap bmp; bmp.CreateCompatibleBitmap(pDC, rect.Width(), rect.Height()); memDC.SelectObject(bmp); // 选入位图 memDC.FillSolidRect(rect, RGB(255,255,255)); // 背景 // 在这里用 memDC 画所有图元而不是 pDC DrawAllShapes(memDC); pDC-BitBlt(0, 0, rect.Width(), rect.Height(), memDC, 0, 0, SRCCOPY); }逻辑说明memDC是内存设备上下文bmp是兼容位图。所有绘制操作先落到memDC最后BitBlt一次性拷贝到屏幕。参数SRCCOPY表示直接拷贝源像素。这样屏幕只刷新一次闪烁基本消除。注意bmp要选入memDC后再画否则画到默认的 1x1 位图上什么都看不到。5. 避坑与排查编译、绘图、交互里的血泪经验5.1 编译报错“无法打开 afxwin.h”现象一编译就报找不到afxwin.h或afxext.h。原因VS 安装时没勾 MFC 组件或者项目工具集和本机 MFC 版本不匹配。解决打开 VS Installer修改安装在单个组件里搜 MFC勾选对应版本然后确认项目属性里的平台工具集和本机一致。5.2 画出来的线是断的或者位置偏移现象直线画出来中间有断点或者整体偏移几个像素。原因一是用了SetPixel但坐标算错二是客户区坐标和逻辑坐标没转换三是OnDraw里没考虑滚动偏移。解决检查坐标转换用DPtoLP和LPtoDP做设备坐标和逻辑坐标互转确认OnDraw里pDC的原点设置正确。5.3 拖动绘制时闪烁严重现象鼠标拖动预览线时屏幕狂闪。原因每次Invalidate都直接往屏幕 DC 画没有双缓冲。解决按 4.3 节改成内存 DC 双缓冲所有绘制先落内存位图再 BitBlt。如果还闪检查是不是在OnEraseBkgnd里做了重绘可以返回 TRUE 屏蔽背景擦除。5.4 多边形填充出现漏填或过填现象凹多边形填充时某些区域没填上或者填到了多边形外面。原因顶点相交处理不对或者水平边没跳过导致交点个数是奇数。解决按 3.3 节的规则处理顶点——局部极值算两个交点非极值算一个水平边直接跳过。另外交点排序后要检查配对数量奇数说明有边没处理对。5.5 内存 DC 用完没释放导致资源泄漏现象程序跑久了变卡或者反复重绘后报 GDI 资源耗尽。原因每次OnDraw都CreateCompatibleDC和CreateCompatibleBitmap但没DeleteDC和DeleteObject。解决把内存 DC 和位图做成 View 类的成员变量只在窗口大小变化时重建OnDraw里复用或者在函数结束前确保memDC.DeleteDC()和bmp.DeleteObject()被调用。提示GDI 对象泄漏在任务管理器里看不出来要用任务管理器的“GDI 对象”列或者专门的工具查数字持续上涨就是泄漏。6. 进阶玩法把绘图工具改成算法演示器源码跑通之后别急着交差。我一般会做一件事把它改成一个算法对比演示器同一个图形用不同算法画直观看出差异。具体做法是加一个算法选择下拉框或者菜单项在OnDraw里根据当前选中的算法分支调用不同的绘制函数。比如画圆可以同时用中点圆算法和直接用EllipseAPI对比像素级差异。再进一步可以加一个“显示像素网格”的开关把每个像素画成小方格这样 Bresenham 的决策过程一目了然。实现上就是在OnDraw里先画网格线再画图形网格间距按缩放比例算。这个功能对理解光栅化特别有用也是课设答辩时的加分项。还有一个实用技巧是加撤销重做。文档类里维护一个操作栈每次添加图元就压栈撤销就弹栈并重绘。MFC 的CDocument有Serialize函数配合CArchive还能把图元存成文件下次打开直接恢复。这样绘图工具就从“能画”升级到“能用”了。// 简单的撤销栈存图元列表的快照 std::vectorstd::vectorShape m_undoStack; void PushUndo(const std::vectorShape shapes) { m_undoStack.push_back(shapes); // 压入当前状态快照 if (m_undoStack.size() 50) // 限制栈深度防止内存涨太快 m_undoStack.erase(m_undoStack.begin()); } void Undo() { if (!m_undoStack.empty()) { m_shapes m_undoStack.back(); // 恢复上一个状态 m_undoStack.pop_back(); Invalidate(); } }逻辑说明m_undoStack存的是图元列表的快照每次操作前压入当前状态。PushUndo里限制栈深度 50避免长时间使用内存无限增长。Undo弹出最后一个快照恢复并重绘。参数上快照方式简单但费内存图元特别多时可以改成存操作记录而不是全量快照。从那以后我每次拿到一份图形学源码都强制自己先跑通、再改一个算法、最后加一个交互功能三步走完才算真正吃透。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑