简介:本资源是一份面向C++初学者与Windows桌面开发入门者的MFC图形绘制实践项目,聚焦于使用MFC框架在视图中动态绘制直线、矩形和椭圆三大基础图形,解决GUI程序中GDI绘图逻辑实现与用户交互响应的核心问题。压缩包共51个文件,包含8个头文件(.h)定义类结构、7个源码文件(.cpp)实现OnDraw()重写与鼠标消息处理、2个图标(.ico)及位图(.bmp)资源、1个可执行程序(.exe)供直接运行验证,另有调试符号(.pdb)、工程配置(.dsw/.dsp)等完整VC6开发环境产物,总大小3.69MB。已有396人学习下载。读者可获得一套可编译运行的MFC绘图示例工程,涵盖CDC设备上下文调用、CRect坐标封装、LineTo/DrawRect/Ellipse绘图函数实操,以及WM_LBUTTONDOWN消息映射等关键机制,目录结构规范,便于理解MFC文档/视图架构与图形编程全流程。
1. 这不是画图软件,而是一把“让程序自己动笔”的钥匙:MFC 绘制直线、矩形、椭圆到底是什么?
你有没有试过——点开一个窗口,里面什么都没有,只有个空白背景;你拖动鼠标,松手一瞬,一条线就稳稳画出来了?或者按住左键拉一下,一个带边框的矩形立刻出现;再换种方式,它又变成一个光滑的椭圆……这不是 Photoshop,也不是画图板,而是你的程序在“亲手”画画。
这就是 MFC(Microsoft Foundation Classes)绘图最直观的入门场景:用 C++ 写几行代码,就能控制 Windows 窗口里的每一个像素点,画直线、画矩形、画椭圆——不靠第三方库,不调外部工具,全靠系统原生支持的 GDI(图形设备接口)能力。它不是炫技,而是很多桌面软件底层的真实逻辑:按钮的圆角、进度条的填充、图表的坐标轴、甚至游戏里简单的 UI 框架,背后都可能藏着这几类基础图形的绘制逻辑。
适合谁?零基础但学过一点点 C++ 的新手(比如写过“Hello World”、知道 if 和 for 是啥)、刚接触 Windows 编程的学生、想从“控制台黑窗口”跨到“有界面能交互”的开发者。你不需要懂驱动、不用碰注册表、更不用研究 DirectX——就从画一条线开始,把“程序会动”这件事,真真切切地跑在自己电脑上。
2. 先搞懂三件事:为什么是 MFC?为什么是 CDC?为什么非得在 OnPaint 里画?
MFC 不是编程语言,也不是新发明的框架,它是微软上世纪 90 年代起封装 Windows API 的一套 C++ 类库。你可以把它理解成“Windows 系统操作说明书的翻译官”:原始 API 调用像读文言文(CreateWindow、GetDC、SelectObject…),而 MFC 把它翻译成更接近日常逻辑的 C++ 表达(CWnd::OnPaint()、CDC::MoveTo()、CDC::LineTo())。对新手来说,它屏蔽了大量句柄管理、消息循环、资源释放等“玄学细节”,让你专注在“我想画什么”这个核心问题上。
2.1 什么是 CDC?它不是“疾控中心”,而是你程序的“画笔+画布+颜料盒”
CDC 全称是Class Device Context(设备上下文类),它是 MFC 中所有绘图操作的唯一入口。别被名字吓住——它本质上就是你程序和屏幕(或打印机、内存位图)之间的一张“工作台”。
- 你不能直接对屏幕像素下命令,就像你不能直接对一张白纸说“变红”;
- 你必须先申请一块“临时画布”(CDC 对象),再在这块画布上选笔(CPen)、选刷子(CBrush)、选字体(CFont);
- 所有画线、画矩形、画椭圆的动作,都必须通过这个 CDC 对象来执行。
提示:MFC 中最常见的 CDC 获取方式是
CPaintDC dc(this)—— 它专用于响应窗口重绘(OnPaint),自动处理 BeginPaint/EndPaint,安全且省心;而CClientDC dc(this)则可用于任意时刻在客户区绘图(比如鼠标拖拽时实时预览),但需手动注意使用时机,否则容易翻车。
2.2 为什么非得在 OnPaint 函数里画?——Windows 的“重绘契约”
Windows 系统有个铁律:窗口内容不持久。最小化再还原、被别的窗口遮住又露出来、甚至快速拖动窗口边缘……这些操作都会导致窗口某部分“丢失图像”,系统就会发一个 WM_PAINT 消息,要求你“重新画一遍”。
MFC 把这个消息自动映射为CxxxView::OnPaint()或CxxxDialog::OnPaint()函数。你在这里写的绘图代码,就是履行这份“重绘契约”的唯一机会。
如果你在 OnLButtonDown 里直接用 CDC 画了一条线,然后窗口被挡住再弹回来——那条线就消失了。因为 OnPaint 没重画它。
所以记住一句血泪经验:所有需要稳定显示的图形,必须进 OnPaint;所有想“即时反馈”的操作(如拖拽画线),得一边在 OnMouseMove 里计算位置,一边在 OnPaint 里根据当前状态重绘全部内容。
2.3 直线 / 矩形 / 椭圆:三个函数,三种“描述方式”
它们看着像兄弟,实则逻辑不同,新手最容易混淆参数顺序和坐标含义:
| 图形 | 核心函数 | 参数含义(关键!) | 白话理解 |
|---|---|---|---|
| 直线 | dc.MoveTo(x1, y1); dc.LineTo(x2, y2); | MoveTo 是“抬笔移到起点”,LineTo 是“落笔画到终点”;两点决定唯一线段 | 就像用尺子比着画:先定起点,再拉到终点 |
| 矩形 | dc.Rectangle(left, top, right, bottom); | 四个值是左、上、右、下的坐标(不是宽高!);right 和 bottom 不包含在矩形内 | 想画宽100高60的矩形?写Rectangle(10, 20, 110, 80),不是(10,20,100,60) |
| 椭圆 | dc.Ellipse(left, top, right, bottom); | 和 Rectangle 完全一样!用外接矩形定义椭圆大小与位置;圆是特例(right−left == bottom−top) | 想画圆?确保左右差 = 上下差;想画横椭圆?让 right−left 更大些 |
注意:所有坐标都是相对于窗口客户区左上角(0,0)的像素点。客户区 = 去掉标题栏、边框、菜单栏后的白色区域。用
GetClientRect(&rect)可准确获取它的尺寸。
3. 第一个可运行的例子:从新建项目到屏幕上画出第一根线(含完整步骤+逐行解释)
我们不做“Hello World”,做“Hello Line”——用 Visual Studio 创建一个最简 MFC 单文档程序,让它启动就画一条斜线。这是你和 MFC 绘图世界的第一次握手。
3.1 新建项目:选对模板,避开 90% 的环境坑
- 打开 Visual Studio(推荐 VS 2019 或更新版本,社区版完全免费)
- 【创建新项目】→ 搜索 “MFC” → 选择“MFC 应用程序”(不是“MFC 类库”或“MFC DLL”)
- 项目名填
MyFirstDraw,位置选好,点击【创建】 - 在向导中:
- 【应用程序类型】选单文档(最简单,自带视图类)
- 【复合控件】取消勾选(新手不需要 ActiveX)
- 【高级功能】全取消(不勾选打印、OLE、数据库等)
- 【生成选项】勾选“使用 Unicode 库”(现代 Windows 标准)
- 点击【完成】,等待项目生成完毕
提示:如果看不到 “MFC 应用程序” 模板,请打开 VS 安装器,检查是否安装了“使用 C++ 的桌面开发” 工作负载,并确认其中“MFC 和 ATL 支持”已勾选。这是新手第一步最常见的卡点,不是你电脑问题,是 VS 默认没装全。
3.2 找到绘图入口:定位并修改 OnPaint 函数
项目生成后,解决方案资源管理器里展开Source Files→ 找到MyFirstDrawView.cpp(注意是 View 文件,不是 Doc 或 Frame)。
滚动到文件末尾附近,找到这个函数:
void CMyFirstDrawView::OnPaint() { CPaintDC dc(this); // 构造 CPaintDC,自动处理 BeginPaint/EndPaint // TODO: 在此处添加绘图代码 }这就是我们要动手的地方。删掉// TODO...这行,替换成以下代码:
void CMyFirstDrawView::OnPaint() { CPaintDC dc(this); // ① 获取安全的绘图上下文 // ② 设置画笔:红色、实线、1像素宽 CPen pen(PS_SOLID, 1, RGB(255, 0, 0)); CPen* pOldPen = dc.SelectObject(&pen); // ③ 选入画笔,并保存旧画笔 // ④ 画一条从 (50,50) 到 (200,150) 的直线 dc.MoveTo(50, 50); dc.LineTo(200, 150); // ⑤ 恢复旧画笔(重要!防止后续绘图异常) dc.SelectObject(pOldPen); }3.3 代码逐行白话解释:每一行都在干什么?
CPaintDC dc(this);:向系统申请一块“本次重绘专用”的画布。它内部自动调用了BeginPaint,并在对象析构时自动EndPaint,安全可靠。CPen pen(PS_SOLID, 1, RGB(255, 0, 0));:创建一支“红色实线笔”。PS_SOLID是线型(实线),1是线宽(像素),RGB(255,0,0)是纯红(红255、绿0、蓝0)。dc.SelectObject(&pen);:把这支新笔“装进”画布的笔槽里。返回值pOldPen是原来那支“默认黑笔”的指针,必须存着!dc.MoveTo(50, 50);:画笔抬起,移动到客户区坐标 (50,50)(距左边界50像素、距上边界50像素)。dc.LineTo(200, 150);:画笔落下,从 (50,50) 画直线到 (200,150)。dc.SelectObject(pOldPen);:用完新笔,必须把原来的笔“装回去”。否则下次 OnPaint 时,画布里还留着这支红笔,可能导致其他图形也变红,甚至崩溃。这是新手最常漏的一步,后果是“画着画着,整个窗口文字都变红了”。
3.4 编译运行:见证第一根线诞生
- 按
Ctrl + F5(不调试运行)或点击绿色三角形 ▶️ - 程序启动,弹出一个标准 Windows 窗口(标题是 MyFirstDraw)
- 你会看到窗口客户区中央偏左的位置,一条清晰的红色斜线!
- 最小化再还原,线还在;拖动窗口大小,线依然稳定显示——因为你遵守了 OnPaint 契约。
验证成功的关键信号:线的颜色、位置、长度和你代码里写的完全一致;窗口重绘(如遮挡后恢复)不丢线;没有报错、没有崩溃、没有乱码。
4. 加点料:在同一窗口里画矩形和椭圆(并学会控制颜色与填充)
光有线太单调。我们接着在同一个 OnPaint 函数里,追加一个蓝色矩形和一个绿色椭圆。重点不是多画两个图形,而是掌握“如何换笔、换刷子、理解坐标系”。
4.1 补充绘图代码:紧接在画线代码之后
在刚才dc.SelectObject(pOldPen);后面,空一行,加入以下代码:
// ======== 画蓝色矩形 ======== CRect rect; GetClientRect(&rect); // ① 获取客户区实际大小(防硬编码坐标出错) int cx = rect.Width(); // 宽度 int cy = rect.Height(); // 高度 // ② 创建蓝色画笔(边框)和浅灰刷子(填充) CPen bluePen(PS_SOLID, 2, RGB(0, 0, 255)); CBrush grayBrush(RGB(220, 220, 220)); // ③ 选入新笔新刷,画矩形(注意:left/top/right/bottom!) CPen* pOldPen2 = dc.SelectObject(&bluePen); CBrush* pOldBrush = dc.SelectObject(&grayBrush); dc.Rectangle(cx/2 - 80, cy/2 - 40, cx/2 + 80, cy/2 + 40); // 居中,宽160,高80 // ④ 恢复旧笔旧刷 dc.SelectObject(pOldPen2); dc.SelectObject(pOldBrush); // ======== 画绿色椭圆 ======== // ⑤ 复用上面的矩形区域,画内切椭圆(即同一外接矩形) CPen greenPen(PS_SOLID, 1, RGB(0, 180, 0)); CPen* pOldPen3 = dc.SelectObject(&greenPen); dc.Ellipse(cx/2 - 80, cy/2 - 40, cx/2 + 80, cy/2 + 40); // 坐标同上,自动变椭圆 dc.SelectObject(pOldPen3);4.2 关键参数说明:为什么这样写?错一点就变形
GetClientRect(&rect);:绝对不要硬写Rectangle(100,100,300,200)!窗口大小可变,硬编码会导致图形跑出窗口或挤在一起。用GetClientRect动态获取,再用Width()/Height()算比例,才是稳健做法。cx/2 - 80:意思是“从客户区水平中心往左移80像素”,作为矩形左边界。同理cx/2 + 80是右边界 → 宽度 = 160。Rectangle(...)和Ellipse(...)用完全相同的四个坐标:这保证了椭圆刚好内切于矩形,视觉上是“套娃”关系。想单独调椭圆?改后两个数(bottom变大,椭圆就拉长)。CBrush控制填充色,CPen控制边框。如果只想要空心矩形,就不SelectObject刷子;如果只要纯色块无边框,就SelectObject一个CBrush并把CPen设为NULL_PEN(但初学者先别碰这个)。
4.3 运行效果与验证要点
- 启动程序,你应该看到:
✅ 一条红色斜线(原功能保留)
✅ 一个居中、蓝色边框、浅灰色填充的矩形(宽160,高80)
✅ 一个同样居中、绿色边框、刚好内切于矩形的椭圆 - 拉大窗口:三个图形同比例放大,位置保持居中(因用了
cx/2,cy/2) - 缩小窗口:图形自动缩小,不溢出、不重叠、不消失
如果椭圆没显示?检查
Ellipse四个参数是否和Rectangle完全一致;如果矩形没填充?确认CBrush是否SelectObject成功,且没被后续SelectObject覆盖;如果颜色不对?检查RGB三参数顺序(红、绿、蓝),别写成RGB(0,255,0)当成红。
5. 新手必踩的 5 个坑:现象、原因、当场解决法(亲测有效)
刚上手时,90% 的“程序没反应”“画不出来”“颜色错乱”“一动就崩”,其实就集中在下面这几个点。我第一次写的时候,光调Rectangle坐标就花了两小时——不是代码错,是理解错了。
5.1 现象:OnPaint 里画了线,但窗口一片空白
原因:CPaintDC dc(this);这行代码被误删、注释,或写成了CClientDC(后者在 OnPaint 里用会崩溃)。
解决:严格核对 OnPaint 函数第一行,必须是CPaintDC dc(this);,且不能有任何//注释。CClientDC只能在 OnLButtonDown 等非重绘函数里用。
5.2 现象:画出来的矩形/椭圆严重变形(比如又高又瘦,或扁平如饼)
原因:把Rectangle(left, top, right, bottom)误解为Rectangle(x, y, width, height),写了Rectangle(100, 100, 100, 50),结果 right=100 < left=100,系统当作无效矩形跳过。
解决:牢记公式:right = left + width,bottom = top + height。写之前心算一遍:right > left且bottom > top必须成立。建议永远用GetClientRect动态算。
5.3 现象:画完线后,窗口标题栏、菜单栏、滚动条全变成红色
原因:dc.SelectObject(&pen);后忘了dc.SelectObject(pOldPen);恢复原笔。GDI 对象是全局共享的,不恢复会导致系统级绘图异常。
解决:每一对SelectObject(newObj)后,必须紧跟SelectObject(oldObj)。养成习惯:写完SelectObject,立刻敲回车,手写dc.SelectObject(pOldXXX);,再填变量名。
5.4 现象:鼠标一动,图形就闪烁、抖动、反复重绘
原因:在OnMouseMove或OnLButtonDown里直接调用Invalidate()强制刷新,但 OnPaint 里没清屏,旧图形残留叠加。
解决:在OnPaint开头加一行dc.FillSolidRect(&rect, RGB(255,255,255));(用白色填满客户区),或更规范地:CRect rect; GetClientRect(&rect); dc.FillRect(&rect, &brushWhite);。先清屏,再画新图。
5.5 现象:编译报错error C2664: 'CPen::CPen' : cannot convert parameter 3 from 'int' to 'COLORREF'
原因:CPen构造函数第三个参数要COLORREF类型,但你写了CPen(PS_SOLID, 1, 0xFF0000)这样的十六进制数,而RGB()宏才是正确转换方式。
解决:永远用RGB(r,g,b),不要手写0xRRGGBB。RGB(255,0,0)正确,0xFF0000错误(虽然值一样,但类型不匹配)。
提示:VS 编译错误信息里,双击错误行能直接跳转到出错代码。遇到报错,先看第1行提示,90% 是拼写、分号、括号、类型不匹配——不是算法问题。
6. 下一步学什么?三条清晰路径,从“画线”走向“能做东西”
你现在能画线、画框、画椭圆,但这只是 MFC 绘图的“字母表”。接下来怎么走,不靠猜,我按真实学习曲线给你拆成三步,每一步都有明确目标、推荐练习、避坑提醒。
6.1 路径一:让图形“活起来”——掌握鼠标交互与实时绘图(1~2 天)
目标:实现“鼠标按下开始画,拖动实时预览,松手确定图形”。这是所有绘图软件的基础。
关键知识点:
OnLButtonDown:记录起点m_ptStartOnMouseMove:用CClientDC实时画橡皮筋线(先画旧线擦除,再画新线)OnLButtonUp:用Invalidate()触发OnPaint,在OnPaint中根据m_ptStart和m_ptEnd画最终图形
避坑提醒:CClientDC不能在OnPaint里用;橡皮筋线必须用SetROP2(R2_NOT)实现“画两次即擦除”;所有鼠标点坐标要用ClientToScreen/ScreenToClient转换(避免窗口有菜单栏时坐标偏移)。
我的教训:第一次做橡皮筋线,没清旧线,拖动时满屏乱线。后来发现R2_NOT是 GDI 的“异或模式”,画一次变色,再画一次变回,本质是“画两次等于没画”,这才是橡皮筋原理。从那以后我每次写交互绘图,都强制在草稿纸上画出OnLButtonDown → OnMouseMove → OnLButtonUp的数据流,标清楚哪些变量存哪、何时更新、何时重绘。
6.2 路径二:让图形“记得住”——引入图形对象与容器管理(3~5 天)
目标:画完的线/矩形/椭圆能保存下来,关闭再打开还在,还能选中、移动、删除。
关键知识点:
- 定义基类
CGraphObject,派生CLine,CRectObj,CEllipseObj,每个存自己的坐标、颜色、线宽 - 用
CArray<CGraphObject*, CGraphObject*> m_objects;存所有图形 OnPaint中遍历m_objects,调用各自Draw(CDC*)方法OnLButtonDown时遍历m_objects,用PtInRect或距离公式判断是否点中
避坑提醒:MFC 的CArray存的是指针,new出的对象必须delete,否则内存泄漏;OnPaint里遍历绘图时,千万别在循环里delete对象(会崩),标记后统一清理;保存/加载用CArchive,序列化时每个类必须加DECLARE_SERIAL和IMPLEMENT_SERIAL。
我的教训:曾因忘记delete导致程序跑10分钟内存涨到2GB。后来养成了固定习惯:每次new后立刻写// TODO: delete in destructor,并在类析构函数里补上for(auto obj : m_objects) delete obj; m_objects.RemoveAll();。
6.3 路径三:让图形“更专业”——进阶 GDI 技巧与性能优化(1 周+)
目标:支持抗锯齿文字、透明填充、双缓冲防闪烁、打印输出、导出 BMP/PNG。
关键知识点:
CDC::SetStretchBltMode(COLORONCOLOR)防缩放失真CDC::SetBkMode(TRANSPARENT)让文字背景透明- 双缓冲:
CDC memDC; CBitmap bmp; bmp.CreateCompatibleBitmap(...); memDC.CreateCompatibleDC(&dc); memDC.SelectObject(&bmp);先画到内存位图,再BitBlt到屏幕 - 打印:
CPrintDialog+CDC::StartDoc+CDC::StartPage,复用OnDraw逻辑(MFC 文档/视图架构优势)
避坑提醒:双缓冲的CBitmap必须和屏幕兼容(CreateCompatibleBitmap),不能CreateBitmap(100,100,...);BitBlt的源 DC 是memDC,目标 DC 是dc,方向别反;打印时CDC的映射模式(SetMapMode)默认是MM_TEXT,坐标是像素,但打印机 DPI 高,需用SetWindowExt/SetViewportExt缩放。
我的教训:第一次做双缓冲,CreateCompatibleBitmap传了错误的dc,结果位图是黑白的。后来发现必须用GetDC()获取的CDC*,而不是CPaintDC。从那以后我每次用 GDI 资源,都强制在变量名前加前缀:m_dcMem(成员)、m_bmpBuf(缓冲位图)、m_penDash(虚线笔),一眼看出生命周期和用途。
希望帮到你。
本文还有配套的精品资源,点击获取