简介:这份基于微软基础类库(MFC)的绘图工程示例,面向具备C++语言基础的开发者,以直线、矩形和椭圆三种基本图形的绘制为主线,系统讲解视图类中绘图函数的重写、设备上下文的使用以及鼠标消息的映射处理,既可帮助初学者快速掌握Windows界面绘制原理,也能作为后续开发图形编辑器的基础模板。压缩包共五十一个文件,包含八份头文件、七份源文件、七份编译目标文件和七份浏览信息文件,同时带有可执行程序、调试符号、图标及位图等资源,总体积约三点六九兆字节,并附有可编译的工程文件,能够直接在Visual C++ 6.0环境中打开运行。目前已有三百九十三人学习该资源,内容不仅提供了移动光标、画线、绘制矩形和椭圆等基础函数的调用示例,也涵盖颜色选择、线条样式、画刷及画笔定制等进阶细节;借助鼠标交互示例,读者可以理解如何响应左键按下消息并根据坐标动态生成图形,进而在此基础上添加撤销、保存等扩展功能。
1. 为什么直线、矩形、椭圆能暴露MFC绘制的全部秘密
用MFC在窗口上画一条直线,只需要MoveTo和LineTo两个调用,但同一份代码在不同窗口里表现能差出三倍差距:有的图形一闪就没,有的窗口一缩放内容消失,有的画笔颜色怎么设都不对。原因不在直线函数本身,而在MFC的绘制不是“画完就存进窗口”,而是由设备上下文、GDI对象、无效区域三个零件组成的协作机制。这篇文章拿直线、矩形、椭圆三种最基础的图形当钥匙,把CDC生命周期、画笔选入、坐标换算和重绘驱动一次讲透。适合刚把MFC向导跑通、想手动绘制界面的从业者,也适合被OnDraw和OnPaint绕晕、想搞清楚绘制代码到底该放哪儿的熟手。
2. 设备上下文与GDI对象:绘图代码真正的执行环境
2.1 CDC的三种常见用法:CClientDC、CPaintDC与CWindowDC
MFC把所有GDI绘制动作封装进CDC类。CDC是一个设备上下文,它绑定到一段具体的输出区域,你调用Rectangle、Ellipse、LineTo时,真正作用的对象是这个DC关联的窗口或内存位图,而不是某个抽象的“画布”。这也是MFC绘图和纯逻辑代码最大的差异:同一个绘制指令,在不同DC上输出位置、坐标系、甚至最终颜色都可能不一样。
实际项目里最常见的三种获取DC的方式是CClientDC、CPaintDC和CWindowDC。CClientDC适合不需要响应WM_PAINT的场景。比如我在WM_LBUTTONUP里画一个临时标记点,直接用CClientDC dc(this)获取客户区DC,画完就释放,这是一次性绘制的标准做法。它的绘制范围是客户区,标题栏、边框不在范围内,好处是坐标体系简单,坏处是你画出来的内容没有任何持久性,窗口一旦重绘就消失。
CPaintDC则只能在WM_PAINT消息处理函数里使用。BeginPaint和EndPaint除了创建DC,还会把无效区域标记为已处理。如果你在WM_PAINT里改用CClientDC或GetDC而不用CPaintDC,窗口的无效区域永远不会被清除,系统会认为你还没画完,于是WM_PAINT消息会不停产生,CPU占用直接飙升,这是刚接触MFC的人最容易犯的隐藏错误。
CWindowDC允许你绘制包括非客户区在内的整个窗口区域,一般用在自定义标题栏或边框绘制。绘制区域变大,坐标换算就变复杂,客户区原点与窗口原点不同,鼠标消息里拿到的坐标不能直接用于CWindowDC。
| DC类型 | 典型场景 | 是否清除无效区域 | 绘制范围 |
|---|---|---|---|
| CClientDC | 鼠标消息、定时器消息 | 否 | 客户区 |
| CPaintDC | WM_PAINT内部 | 是 | 客户区 |
| CWindowDC | 自定义边框、非客户区 | 否 | 整个窗口 |
一个可以落地的选择标准是:如果绘制代码会因窗口暴露或尺寸变化被系统反复调用,请用CPaintDC;如果只是在某个用户交互事件里做一次性输出,用CClientDC。用混了,轻则图形不持久,重则消息风暴导致界面假死。CWindowDC这类用法建议只在确实需要覆盖非客户区时才碰,否则窗口的常规绘制流程会被你搅乱。
2.2 画笔选入与还原:第三步为什么不能省略
GDI对象(CPen、CBrush等)本身不会自动与DC绑定。你在构造函数里创建一支红色画笔,调用dc.SelectObject(&pen)把它选入,后续的直线、矩形、椭圆都会用这支画笔。关键还在选入之后:用完必须把原来的画笔选回去。
void DrawWithRedPen(CDC* pDC) { CPen redPen(PS_SOLID, 2, RGB(255, 0, 0)); // 1. 创建红色画笔 CPen* pOldPen = pDC->SelectObject(&redPen); // 2. 选入并保存旧画笔 // 3. 绘制一条直线,验证颜色与宽度生效 pDC->MoveTo(10, 10); pDC->LineTo(200, 10); pDC->SelectObject(pOldPen); // 4. 把旧画笔选回DC } // 5. redPen对象析构,释放GDI句柄这五步是GDI绘图的固定节奏。第二步的返回值是老画笔的指针,必须保存。第四步把老画笔选回DC,是为了让DC恢复到一个没有外部依赖的状态。如果函数在第三步提前return,第四步没执行,redPen在栈上析构时,它已经被选入DC,GDI对象的销毁行为和DC的引用产生冲突,轻则资源没有按时释放,重则句柄泄漏。
我见过某模块在绘图循环里每次都new CPen而不delete,任务管理器里GDI对象数肉眼可见地往上涨,最后窗口画不出任何内容。MFC里用CPen这类封装类创建的GDI对象,析构函数会自动调用DeleteObject,但前提是对象还在栈上或者确实被析构了。new出来的对象如果没人delete,析构永远不会执行,句柄就永远占着。常见做法是把画笔对象定义为局部变量,函数结束自然析构,再配合SelectObject还原,GDI资源管理基本不会出问题。如果绘制代码分支很多,手动还原容易漏,做一个简单的RAII包装类,把选入和还原放进构造析构,会可靠得多。
3. 直线绘制:从一条线到橡皮筋机制的三级跳
3.1 OnDraw与OnPaint的分工:绘制代码该写在哪一侧
MFC向导生成的View类里有两个容易搞混的函数:OnPaint和OnDraw。OnPaint是WM_PAINT消息的处理函数,OnDraw是CView在OnPaint内部调用的虚函数。为什么要绕一圈?因为打印和打印预览场景下,框架希望把绘制逻辑复用给打印机DC。你直接把代码写在OnPaint里,打印时这部分逻辑不会自动走到打印DC上。写在OnDraw里,框架的OnPaint默认实现了CPaintDC的构造、OnDraw调用和EndPaint配对,打印预览也会调用同一个OnDraw。
还有一个更实际的原因:CPaintDC必须在WM_PAINT消息里通过BeginPaint构造,EndPaint结束。你手写OnPaint时如果忘了把这两个资源配对,无效区域不会被清除,整个窗口就会陷入无休止的重绘。框架生成的OnPaint默认已经处理好了这层逻辑,你只需要在OnDraw里写实际的图形代码。加了消息映射后,最好检查一下向导生成的OnPaint实现,确认里面调用了OnDraw(&dc),这是我第一次自己改OnPaint时翻过的车,手工把函数体清空后,整个窗口再也不刷新了。
3.2 直线参数细节:宽度、虚线退化与坐标方向
从一个基础的OnDraw实现开始,看一下直线绘制需要关心的三个参数。
void CMyView::OnDraw(CDC* pDC) { // 1. 创建实线画笔,1像素宽,深蓝色 CPen penLine(PS_SOLID, 1, RGB(30, 80, 160)); CPen* pOldPen = pDC->SelectObject(&penLine); // 2. 从客户区坐标(20,30)画到(200,30) pDC->MoveTo(20, 30); pDC->LineTo(200, 30); // 3. 虚线画笔绘制第二条线 CPen penDash(PS_DASH, 1, RGB(160, 80, 30)); pDC->SelectObject(&penDash); pDC->MoveTo(20, 50); pDC->LineTo(200, 50); // 4. 还原画笔 pDC->SelectObject(pOldPen); }第一个参数是画笔宽度。PS_SOLID实线可以随意设置宽度,PS_DASH虚线在宽度大于1时,多数显示驱动会退化成实线,这是GDI的历史限制。要画宽虚线,可以用PS_GEOMETRIC画笔配合自定义样式,或者用实线分段绘制模拟。第二个参数是坐标单位。MoveTo和LineTo的坐标默认是逻辑单位,窗口没有设置映射模式时,逻辑单位等于像素,原点在客户区左上角,X向右为正,Y向下为正。一旦调用SetMapMode改成MM_HIMETRIC或MM_ISOTROPIC,坐标单位不再是像素,Y轴方向也可能翻转,这时候你画的图很可能“消失”,因为坐标被解释到了屏幕左上角之外的负区域。
第三个参数是绘制时机。OnDraw里的代码只在窗口需要重绘时执行。你从别处修改了数据、希望画面更新,需要调用Invalidate或InvalidateRect把窗口标记为无效,系统投递WM_PAINT消息,OnDraw才会重新跑一遍。直接调用UpdateWindow可以强制立即重绘,但前提是先Invalidate,否则无效区域为空,OnDraw不会触发。
3.3 SetROP2与临时直线:橡皮筋效果的技术地基
交互式绘图里常见的橡皮筋效果,画直线时显示一条跟随鼠标移动的临时线,鼠标不动它不动。要实现这个效果,光靠OnDraw全量重绘还不够,因为临时线需要随鼠标位置快速变化,如果每次都走完整的重绘流程,窗口会闪烁得厉害。
这里用到的关键技术是设置绘制模式SetROP2。默认模式是R2_COPYPEN,新像素直接覆盖旧像素。改成R2_XORPEN后,新写入的颜色会和背景做异或运算,同一个位置画两次,像素会恢复原样。利用这个特性,只需在旧位置把临时线画一次(等效擦除),再到新位置重新画一条,不需要触发OnDraw,也不会留下残影。
// 在OnMouseMove里处理橡皮筋直线 void CMyView::OnMouseMove(UINT nFlags, CPoint point) { if (nFlags & MK_LBUTTON) { CClientDC dc(this); dc.SetROP2(R2_XORPEN); // 异或模式,第二次画同一位置等于擦除 // 假设上一次鼠标位置保存在m_lastPoint // 先擦除旧直线 dc.MoveTo(m_startPoint); dc.LineTo(m_lastPoint); // 再画新直线 dc.MoveTo(m_startPoint); dc.LineTo(point); m_lastPoint = point; // 更新位置 } CView::OnMouseMove(nFlags, point); }这里有一个实践要点:R2_XORPEN模式下,画笔颜色和背景色的异或结果可能不是预期颜色,这是正常的,临时线呈现奇特色彩反而容易辨认。真正定稿的图形在鼠标抬起时用普通模式重新绘制一次,并把顶点存入数据容器。后面会看到这其实是完整画板的标准两步流程:临时绘制只做预览,数据持久化才是重点。
4. 矩形与椭圆:CRect约束与画刷填充逻辑
4.1 Rectangle绘制:填充画刷和边界覆盖问题
矩形绘制函数原型是Rectangle(LPCRECT)或者Rectangle(int left, int top, int right, int bottom)。MFC里CRect是RECT结构体的包装,提供了很多方便的函数,但如果不注意约束,画出来的矩形会和你想象的不一样。
先看核心代码:
void CMyView::OnDraw(CDC* pDC) { CRect rc1(50, 50, 250, 150); // 准备一个浅灰色画刷,填充矩形内部 CBrush brushFill(RGB(230, 230, 230)); CBrush* pOldBrush = pDC->SelectObject(&brushFill); // 未设置画笔时,矩形边框使用系统默认黑色 pDC->Rectangle(rc1); pDC->SelectObject(pOldBrush); }这里有一个经常让新手翻车的现象:Rectangle画出来只有线框,没有填充。原因很简单——画刷没有选入DC,GDI默认用白色画刷填充矩形内部,白画刷在白色窗口背景上看不出来。要填充颜色,必须显式创建CBrush并选入。反过来,如果你希望矩形只有边框、内部透明,需要用空画刷而不是白色画刷。可以创建CBrush brushEmpty((HBRUSH)GetStockObject(HOLLOW_BRUSH))再选入,或者用NULL_BRUSH库存画刷,这样内部区域不会覆盖已绘制的内容。
CRect最值得注意的函数是NormalizeRect。当用户用鼠标画矩形时,起点可能在左上、终点在右下,也可能方向相反,导致left大于right或top大于bottom。如果不做归一化,Rectangle会画出怪异结果或干脆不显示。NormalizeRect会在原地调整left/right和top/bottom,确保矩形方向正确。
CRect rcUser(clickStart, clickEnd); // 由鼠标两次点击确定 rcUser.NormalizeRect(); // 修正矩形方向 pDC->Rectangle(rcUser);矩形边界还有一个与画笔宽度相关的覆盖问题。GDI画矩形时,边框线的中心落在传入矩形边界上。也就是说,矩形区域是(50,50)到(250,150),画笔宽度是2像素时,最外层像素会出现在(49,49)到(251,151)附近。当多个矩形紧贴排列时,相邻矩形会互相覆盖1到2像素。如果对边界精度要求高,一般做法是把矩形尺寸减去画笔宽度,再做OffsetRect调整回左上角位置。
4.2 Ellipse绘制:外接矩形与CRect复用
Ellipse函数的参数同样是一个矩形区域,画的是与该矩形内切的椭圆。代码结构和Rectangle几乎一致:
void CMyView::OnDraw(CDC* pDC) { CRect rcElli(50, 50, 250, 150); CBrush brushBlue(RGB(120, 160, 220)); CBrush* pOldBrush = pDC->SelectObject(&brushBlue); CPen penBlue(PS_SOLID, 2, RGB(60, 80, 160)); CPen* pOldPen = pDC->SelectObject(&penBlue); pDC->Ellipse(rcElli); pDC->SelectObject(pOldPen); pDC->SelectObject(pOldBrush); }Ellipse的填充和边框行为与Rectangle一致:画刷控制内部,画笔控制轮廓。区别在于椭圆没有直角边界,边框覆盖问题肉眼不易察觉,但坐标计算完全一致——确定椭圆形状的参数就是传入的外接矩形。如果想画正圆,需要保证CRect的宽高相等。我在实际项目里经常看到有人直接用鼠标拖拽得到的矩形画椭圆,结果画出一堆扁椭圆,然后怀疑坐标换算有Bug。这种情况要先检查矩形宽高比,再决定是否强制等比缩放。
椭圆还有一个视觉细节:如果矩形区域的长宽比接近1,椭圆接近正圆;如果长宽比很大,就成了一个压扁的形状。需要画“等比例椭圆”时,可以根据客户区尺寸动态计算外接矩形,再交给Ellipse。比如要让椭圆的短边始终等于客户区较短边减去一定边距:
CRect rcClient; GetClientRect(&rcClient); int diameter = min(rcClient.Width(), rcClient.Height()) - 20; int cx = (rcClient.Width() - diameter) / 2; int cy = (rcClient.Height() - diameter) / 2; CRect rcCircle(cx, cy, cx + diameter, cy + diameter); pDC->Ellipse(rcCircle);GetClientRect返回的矩形,右下角坐标是(width, height)并不是(width-1, height-1)。GDI绘图对right和bottom的语义是“不包含”,实际绘制范围会比传入矩形略小。做精细边缘对齐时,可以把right和bottom各减1再传入。另外,当窗口尺寸变化时,框架会自动触发WM_PAINT,OnDraw重跑一遍,你在OnDraw里用GetClientRect动态计算矩形,椭圆就会跟随新尺寸变化。这种做法对直线、矩形、椭圆全适用,我会把所有几何区域统一放进OnDraw里的CRect计算逻辑中。
5. 常见问题排查:五类绘制崩溃的定位与修复
5.1 图形只在鼠标事件里出现,窗口一刷新就消失
现象:在WM_LBUTTONUP里调用CClientDC dc(this); dc.Rectangle(...),画出来的矩形确实出现了。但窗口最小化再恢复、或者被其他窗口遮挡后又暴露时,矩形不见了。
原因:CClientDC绘制的图形只存在于当时的窗口画面缓冲里。窗口系统在后台重新绘制时,只会执行WM_PAINT里的绘制代码,鼠标消息里画的内容不会被保留。这是MFC窗口绘制的核心机制:画面不是静态影像,而是每次重绘时由消息驱动的结果。
解决:把要画的图形数据存到View类成员变量里,例如一个CRect数组。在鼠标事件里向数组添加数据,然后调用Invalidate请求重绘,让OnDraw把所有已存储的数据重新画出来。
void CMyView::OnLButtonUp(UINT nFlags, CPoint point) { // 鼠标事件里只更新数据,不直接绘图 m_rectList.AddTail(m_draggingRect); Invalidate(); // 请求系统重绘 CView::OnLButtonUp(nFlags, point); } void CMyView::OnDraw(CDC* pDC) { POSITION pos = m_rectList.GetHeadPosition(); while (pos != NULL) { CRect rc = m_rectList.GetNext(pos); pDC->Rectangle(rc); } }这条经验是整个MFC绘图的支柱:数据存成员,绘制全走OnDraw。遵循它,绘制消失问题直接消除。
5.2 窗口缩放时画面剧烈闪烁
现象:拖动窗口边缘调整尺寸,绘制内容在变化过程中不停闪烁,甚至出现残影。这个问题在做画板类工具时尤为突出,因为图形数量多,每次重绘耗时明显。
原因:Windows在重绘窗口时先用背景色擦除客户区,再执行WM_PAINT。擦除和重绘之间有一个明显的“白底空窗期”,窗口尺寸连续变化时这个空窗反复出现,闪烁就是这么来的。
解决:最直接的办法是处理WM_ERASEBKGND消息,让它直接返回TRUE,屏蔽系统默认的背景擦除。
BOOL CMyView::OnEraseBkgnd(CDC* pDC) { // 返回TRUE表示背景擦除已完成,系统不再用白色覆盖客户区 return TRUE; }注意:屏蔽背景擦除后,OnDraw必须先自己用背景色填充整个客户区,否则图形移动时残影会留在窗口上。所以标准的做法是走双缓冲:先在内存DC里把所有图形画好,再一次BitBlt到屏幕。具体模板放到下一章。
5.3 GDI对象数只涨不落,最后界面彻底瘫痪
现象:程序运行一段时间后,任务管理器里GDI对象数不断增长,绘图变慢,窗口内控件消失或变形,严重时整个界面无法刷新。
原因:最常见的是每次绘图都用new创建CPen、CBrush,但没有调用DeleteObject或析构就丢弃了。另一个来源是SelectObject选入新对象后,没有还原旧对象,导致GDI句柄被DC持有,析构时机错乱。GDI对象表是系统级资源,一旦耗尽,所有窗口绘制都会失败。
解决:检查所有绘图函数,确认CPen、CBrush均为栈上局部变量。确认每次SelectObject的返回值被保存,函数结束前重新选入DC。用以下代码可以在你怀疑某个模块泄漏时,输出当前进程GDI对象数:
// 在绘制循环前后调用,观察数值是否持续增长 DWORD dwGdiCount = GetGuiResources(GetCurrentProcess(), GR_GDIOBJECTS); TRACE(_T("GDI对象数: %lu\n"), dwGdiCount);如果调试输出显示循环前后GDI对象数差值大于零,说明循环里存在未释放的GDI对象。逐个检查绘图函数,通常都能定位到漏了SelectObject还原的那一行。
5.4 画笔颜色设置了却显示为黑色
现象:CPen pen(PS_SOLID, 1, RGB(255, 0, 0)),SelectObject之后画直线,线是黑色的,红色完全不生效。
原因:常见的有三种情况。第一,画笔选入之后,某个调用把当前DC的文本颜色或画刷颜色改了,导致后续输出的颜色被覆盖。第二,多次绘图时每次都重新创建画笔,但实际选入DC的是旧画笔,新画笔没有生效。第三,在部分显示驱动上,画笔宽度为0或样式为PS_NULL时,会呈现奇怪的默认效果。以上都不符合时,再检查RGB宏的参数是不是被无意间交换了。
解决:用GetCurrentPen调试确认DC当前持有的画笔对象类型:
CPen redPen(PS_SOLID, 1, RGB(255, 0, 0)); CPen* pOld = pDC->SelectObject(&redPen); LOGPEN logPen; pDC->GetCurrentPen()->GetLogPen(&logPen); TRACE(_T("当前画笔颜色: R=%d G=%d B=%d\n"), GetRValue(logPen.lopnColor), GetGValue(logPen.lopnColor), GetBValue(logPen.lopnColor));如果TRACE输出的是255,0,0,说明DC状态没有问题,颜色改变发生在后续某个调用里。如果输出是黑色,说明选入的画笔并不是你想的那一支,需要检查变量作用域和生命周期。从那以后我遇到颜色不对,先看画笔选入是否真正执行,再看后续有没有重置DC状态的调用。
5.5 鼠标坐标和绘制位置错位
现象:鼠标点击客户区,期望在(100,100)处画一个点,结果点落在完全不同的偏移位置。这个现象在窗口有滚动条或缩放比例时特别明显。
原因:鼠标消息里拿到的point是客户区设备坐标。OnDraw里用GetClientRect、Invalidate相关操作时,如果你的View还设置了映射模式或滚动偏移,设备和逻辑坐标之间没有自动换算,坐标自然就对不上。
解决:在鼠标交互时,把原始point先做一次从设备坐标到逻辑坐标的换算,再存入成员变量。
void CMyView::OnLButtonDown(UINT nFlags, CPoint point) { CClientDC dc(this); OnPrepareDC(&dc); // 应用当前映射模式和滚动偏移 dc.DPtoLP(&point); // 设备坐标转逻辑坐标 m_startPoint = point; // 存储换算后的坐标 CView::OnLButtonDown(nFlags, point); }OnPrepareDC是这里的关键。它会把视图的滚动偏移和映射模式设置到DC上。对于没有滚动条、也没有改变映射模式的简单视图,DPtoLP通常不改变数值,但一旦窗口加了滚动或缩放,漏掉这一步坐标必然错位。我的排查习惯是先在接口函数里TRACE出point和转换后的m_startPoint,对比就知道是哪一步出了问题。
6. 进阶验证:合成画板与双缓冲输出模板
6.1 三种图形合成一个可交互画板
把直线、矩形、椭圆整合进一个程序,用枚举记录当前绘制模式,鼠标交互时按模式生成不同数据。这个思路让代码结构变得非常清晰:数据层只存形状的坐标和类型,绘制层按存储内容重新渲染。
enum DrawMode { MODE_LINE, MODE_RECT, MODE_ELLIPSE }; // 存储一个绘制对象 class CShapeItem { public: DrawMode mode; CPoint ptStart; CPoint ptEnd; }; void CMyView::OnDraw(CDC* pDC) { for (size_t i = 0; i < m_shapeList.size(); i++) { const CShapeItem& item = m_shapeList[i]; if (item.mode == MODE_LINE) { pDC->MoveTo(item.ptStart); pDC->LineTo(item.ptEnd); } else { CRect rc(item.ptStart, item.ptEnd); rc.NormalizeRect(); if (item.mode == MODE_RECT) pDC->Rectangle(rc); else pDC->Ellipse(rc); } } }构建画板时先不要急着做橡皮筋效果,等所有模式都能在鼠标抬起后画出正确形状,再引入R2_XORPEN临时预览。鼠标抬起那一瞬间要做两件事:把最终坐标存入m_shapeList,然后Invalidate触发全量重绘。这与第5章的“数据存成员,绘制全走OnDraw”是一个思路,只是数据从单个矩形升级成了一个容器。
6.2 双缓冲重绘:消除闪烁的完整模板
屏蔽WM_ERASEBKGND只是治标,双缓冲才是治本。把要绘制的内容先画到内存位图,再一次性拷贝到屏幕上,是MFC里一劳永逸解决闪烁的标准模板。
void CMyView::OnDraw(CDC* pDC) { CRect rcClient; GetClientRect(&rcClient); // 1. 创建内存兼容DC和位图 CDC memDC; memDC.CreateCompatibleDC(pDC); CBitmap memBitmap; memBitmap.CreateCompatibleBitmap(pDC, rcClient.Width(), rcClient.Height()); CBitmap* pOldBmp = memDC.SelectObject(&memBitmap); // 2. 用窗口背景色填充内存DC,避免残影 CBrush bgBrush(::GetSysColor(COLOR_WINDOW)); memDC.FillRect(rcClient, &bgBrush); // 3. 在内存DC上执行所有绘制逻辑 // 这一步把OnDraw里的实际绘制内容全部改为memDC.xxx() // 例如 memDC.Rectangle(rc)、memDC.MoveTo(pt)等 // 4. 一次性拷贝到屏幕 pDC->BitBlt(0, 0, rcClient.Width(), rcClient.Height(), &memDC, 0, 0, SRCCOPY); // 5. 还原位图选入 memDC.SelectObject(pOldBmp); }双缓冲里有三个地方要注意。第一,CreateCompatibleBitmap的参数是宽和高,用rcClient.Width()和Height(),不要直接传rcClient,否则按下拉框IDOK时某些驱动会创建失败。第二,FillRect必须用纯色画刷,不要用HOLLOW_BRUSH,否则背景残留。第三,内存DC的坐标系和窗口DC完全一致,前面的全部绘图逻辑可以原封不动搬到memDC上,这是选择兼容DC的好处。
完成整合后,我一般会跑一遍完整验证:快速拉大缩小窗口看是否还有闪烁;连续绘制100个形状,最小化再恢复,确认所有数据都保留;任务管理器里观察GDI对象数在一个绘制周期内不增长。第一次做这个画板时,我把数据存进临时变量而不是成员容器,结果每次重绘都只剩最后一个图形,窗口一遮挡就暴露了问题。从那以后我每次做和绘制相关的修改,都强制走一遍“数据入容器、OnDraw全量重绘、双缓冲输出”的流程,不再依赖窗口自动保留像素。希望这套思路对你手里的MFC绘图项目也有用。
本文还有配套的精品资源,点击获取