news 2026/11/4 9:24:20

MFC绘图入门:用CDC在OnPaint中画直线矩形椭圆

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MFC绘图入门:用CDC在OnPaint中画直线矩形椭圆

简介:本资源是一份面向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% 的环境坑

  1. 打开 Visual Studio(推荐 VS 2019 或更新版本,社区版完全免费)
  2. 【创建新项目】→ 搜索 “MFC” → 选择“MFC 应用程序”(不是“MFC 类库”或“MFC DLL”)
  3. 项目名填MyFirstDraw,位置选好,点击【创建】
  4. 在向导中:
    • 【应用程序类型】选单文档(最简单,自带视图类)
    • 【复合控件】取消勾选(新手不需要 ActiveX)
    • 【高级功能】全取消(不勾选打印、OLE、数据库等)
    • 【生成选项】勾选“使用 Unicode 库”(现代 Windows 标准)
  5. 点击【完成】,等待项目生成完毕

提示:如果看不到 “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_ptStart
  • OnMouseMove:用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(虚线笔),一眼看出生命周期和用途。

希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/4 9:23:30

电商长序列建模实战:20K步行为压缩与低延迟工程方案

1. TM20K不是新模型代号&#xff0c;而是电商广告推荐中序列建模的实战缩影 你点开一篇论文或技术分享&#xff0c;看到“TM20K”这个代号&#xff0c;第一反应可能是&#xff1a;又一个新SOTA模型&#xff1f;带Transformer、加了MoE、用了强化学习微调&#xff1f;但实际在一…

作者头像 李华
网站建设 2026/11/4 9:23:12

网络安全学习资源与合规实战指南

1. 网络安全学习资源全景指南在数字化浪潮席卷全球的今天&#xff0c;网络安全已成为每个互联网用户都需要关注的重要议题。作为一名从业十余年的安全研究员&#xff0c;我经常收到这样的咨询&#xff1a;"哪里能学到靠谱的网络安全知识&#xff1f;"、"如何避免…

作者头像 李华
网站建设 2026/11/4 9:22:41

LLM输出JSON不可靠?四轮实测对比约束方案

1. 为什么不能相信 LLM 的“自由发挥”1.1 一个让人血压升高的 JSON 输出我最近在做一个小型智能客服项目&#xff0c;需求本身不复杂&#xff1a;让大模型读一段中文采购记录&#xff0c;把关键字段抽成 JSON 交给后端入库。最开始我天真地以为&#xff0c;只要在提示词里把字…

作者头像 李华
网站建设 2026/11/4 9:21:56

AI产品化落地三大信号:编程独立、法律合规、支付闭环

2026年9月23日这天&#xff0c;AI圈的消息密度比平时高了不少。早上打开信息流&#xff0c;刷到的三条大事件放在一起还挺有意思&#xff1a;Kimi Code Desktop独立化了&#xff0c;OpenAI正式放出了面向法律场景的AI平台&#xff0c;Agentic Commerce则在支付环节跑通了商业闭…

作者头像 李华
网站建设 2026/11/4 9:21:25

纯C实现哈夫曼压缩:零依赖、位级控制、可调试的工业级教学工具

简介&#xff1a;本资源是面向高校数据结构课程设计的哈夫曼文件压缩实践项目&#xff0c;适用于掌握二叉树、链表等基础数据结构后进行综合应用训练的学习者。项目完整实现基于字符频次统计构建Huffman树、生成最优前缀编码、完成ASCII文本文件压缩的核心流程&#xff0c;涵盖…

作者头像 李华
网站建设 2026/11/4 9:21:16

Electron主进程+FastAPI流式Agent架构设计

1. 这不是“Electron FastAPI”简单拼接&#xff0c;而是一套面向真实AI对话场景的流式Agent架构设计你搜“Electron 与 FastAPI 如何完成流式 Agent 对话”&#xff0c;大概率是刚跑通一个基础聊天界面&#xff0c;发现输入后要等十几秒才吐出整段回复&#xff0c;滚动条卡在…

作者头像 李华