资讯详情

MFC绘图核心机制:CDC、GDI对象与直线矩形椭圆绘制全解析

📅 2026/10/14 16:17:48 | 华诺云谱 👁 阅读
MFC绘图核心机制:CDC、GDI对象与直线矩形椭圆绘制全解析
简介这份基于微软基础类库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与CWindowDCMFC把所有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鼠标消息、定时器消息否客户区CPaintDCWM_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在栈上析构时它已经被选入DCGDI对象的销毁行为和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画出来只有线框没有填充。原因很简单——画刷没有选入DCGDI默认用白色画刷填充矩形内部白画刷在白色窗口背景上看不出来。要填充颜色必须显式创建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_PAINTOnDraw重跑一遍你在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绘图项目也有用。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

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

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

你可能需要的服务

订阅华诺云谱资讯周报

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

↑