双缓存之BeginPaint&GetDC

本文探讨了Windows编程中BeginPaint与GetDC的区别,解释了如何避免绘图闪烁问题,包括使用双缓冲技术的具体步骤。

这是个windows编程问题。


第一种情况显示出来的字很正常。

********************************************************************************
    case WM_PAINT :
         gdc = BeginPaint (hwnd, &ps);
         TextOut (gdc, 0, 0, s, strlen (s));
         EndPaint (hwnd, &ps);
    break ;

*******************************************************************************

第二种情况显示的字不停闪烁。

*******************************************************************************
   case WM_PAINT :
           gdc = GetDC (hwnd);
           TextOut (gdc, 0, 0, s, strlen (s));
           ReleaseDC (hwnd, gdc);
    break ;

*******************************************************************************

请教两种函数的作用?

    BeginPaint() EndPaint() 可以删除消息队列中的WM_PAINT 消息,并使无效区域有效。
GetDC()ReleaseDC() 并不删除也不能使无效区域有效,因此当程序跳出 WM_PAINT 时 ,无效区域仍然存在。系统就回不断发送WM_PAINT 消息,于是程序不断处理WM_PAINT 消息。

    相当于BeginPaint EndPaint 会告诉GDI内部,这个窗口需要重画的地方已经重画了,这样WM_PAINT 处理完返回给系统后,系统不会再重发WM_PAINT ,而GetDC 没有告诉系统这个窗口需要重画的地方已经画过,在你把程序返回给系统后,系统一直以为通知你的重画命令你还没有乖乖的执行或者执行出错,所以在消息空闲时,它还会不断地发WM_PAINT 催促你画,导致程序卡死。

【无效区域】

    无效区域就是指需要重画的区域,无效的意思是:当前内容是旧的,过时的。
假设A是新弹出的一个对话框或被激活的现有对话框,A对话框置于原来的活动对话框B前面,造成对话框B的部分或全部被覆盖,当对话框A移开或关闭后,使对话框B原来被覆盖的地方重新可见。那部分被覆盖的地方就称为无效区域。
    只有当一个窗口消息空闲时,系统才会抽空检查一下这个窗口的无效区域是否为非空(WM_PAINT 的优先级是最低的。这也就是为什么系统很忙时窗口和桌面往往会出现变白、刷新不了、留拖拽痕迹等现象的原因),如果非空,系统就发送WM_PAINT 。所以一定要用BeginPaint EndPaint 把无效区域设为空,否则WM_PAINT 将一直被发送。

为什么WINDOWS要提出无效区域的概念?

     这是为了加速。
     因为BeginPaint EndPaint 用到的设备描述符只会在当前的无效区域内绘画,在有效区域内的绘画会自动被过滤,大家都知道,WIN GDI的绘画速度是比较慢的,所以能节省一个象素就节省一个,不用吝啬,这样可以有效加快绘画速度。
    可见BeginPaint EndPaint 是比较“被动”的,只在窗口新建时和被摧残时才重画。
GetDC 用于主动绘制,只要你指到哪,它就打到哪。它不加判断就都画上去,无效区域跟它没关系。对话框没被覆盖没被摧残,它很健康,系统没要求它重画,但开发者有些情况下需要它主动重画:比如一个定时换外观的窗口,这时候就要在WM_TIMER 处理代码用GetDC 。这时候再用 BeginPaint EndPaint 的话,会因为无效区域为空,所有绘画操作都将被过滤掉。

***********************************************************************************************************************************

eg:

  1. PAINTSTRUCT ps;
  2. HDC hdc = Beginpaint ( hwnd, &ps) ;
  3. HDC hdcMem = CreateCompatibleDC ( hdc) ; //Create a DC that matches the device
  4. HANDLE hBmp = LoadImage ( g_hInst_MainWnd, MAKEINTRESOURCE ( IDB_MAINWND) ,
  5.                             IMAGE_BITMAP , 0 , 0 , 0 ) ;
  6. HGDIOBJ hOldSel = SelectObject ( hdcMem, hBmp) ; //Select the bitmap into to the compatible
  7.                                    //device context
  8. BITMAP bmp; //Get the bitmap dimensions from the bitmap
  9. GetObject ( hBmp, sizeof ( BIMAP) , &bmp) ;
  10. RECT rc;        //Get the window area
  11. GetClientRect( hwnd, &rc) ;
  12. //Copy the bitmap image from the memory DC to the screen DC
  13. BitBlt ( hdc, rc.left , rc.top , bmp.bmWidth , bmp.bmHeight , hdcMem, 0 , 0 , SRCCOPY ) ;
  14. SelectObject ( hdcMem, hOldSel) ; //Restore original bitmap selection and destroy
  15.                       //the memory DC
  16. DeleteDC ( hdcMem) ;
  17. EndPaint ( hWnd, &ps) ;
  18. return 0 ;

下面是更加详细的介绍

//========================================================================
//TITLE:
//     EVC绘制位图--BeginPaint()与GetDC()的区别
//AUTHOR:
//     norains
//DATE:
//     Tuesday   29-August-2006
//========================================================================

1.BeginPaint()和GetDC()
         在EVC中绘制位图比较方便,有不少现成的函数可供调用,我们所要注意的只是BeginPaint() GetDC() 的使用即可.
         因为代码比较简单,所以不做更多解释.

         这是消息循环函数:
         LRESULT CALLBACK MainWndProc (HWND hWnd, UINT wMsg , WPARAM wParam ,
                                                                     LPARAM lParam )

         {
             ......
           
              switch (wMsg)
              {
                 case WM_PAINT :
                         OnPaintMainWnd (hWnd,wMsg,wParam,lParam);
                         break;
               
                 ......               
           
             }
             return DefWindowProc (hWnd,wMsg,wParam,lParam);
           
             ......
           
         }
       
         响应WM_PAINT消息的函数,在这里进行位图的绘制:
         LRESULT OnPaintMainWnd(HWND hWnd,UINT wMsg,WPARAM wParam,LPARAM lParam)
         {
                PAINTSTRUCT ps;
                HDC hdc = BeginPaint(hWnd,&ps);
                 HDC hdcMem = CreateCompatibleDC(hdc);//Create a DC that matches the device             
                HANDLE hBmp= LoadImage(g_hInst_MainWnd,MAKEINTRESOURCE
                                                         (IDB_MAINWND),IMAGE_BITMAP,0,0,0); //Load the bitmap
                   //Select the bitmap into to the compatible device context
                HGDIOBJ hOldSel = SelectObject(hdcMem,hBmp);
                 BITMAP bmp;   //Get the bitmap dimensions from the bitmap
                GetObject(hBmp,sizeof(BITMAP),&bmp);
                RECT rc;          //Get the window area
                GetClientRect(hWnd,&rc);
                   //Copy the bitmap image from the memory DC to the screen DC
                BitBlt(hdc,rc.left,rc.top,bmp.bmWidth,bmp.bmHeight,hdcMem,0,0,SRCCOPY);
                SelectObject(hdcMem,hOldSel);//Restore original bitmap selection and destroy the memory
                                                                   //DC
   
                DeleteDC(hdcMem);
                EndPaint(hWnd,&ps);
               return 0;
         }

         我们都知道BeginPaint ()和EndPain t()需要配套使用,并且这两个函数也只能用在WM_PAINT 消息的相应函数当中.如果我们在 WM_PAINT 的响应函数中将以上两个绘制函数相应替换为GetDC ()和ReleaseDC ()会有什么结果呢?
         即:
        HDC hdc = BeginPaint (hWnd, &ps);     -->    HDC hdc = GetDC (hWnd);
         EndPaint (hWnd, &ps);                 -->    ReleaseDC (hWnd, hdc);

       
         编译并运行程序,我们发现窗口一片空白,好像没有绘制位图.但其实不尽然,我们采用单步调试,可以发现其实位图已经绘制出来,只不过又被背景颜色抹掉了. 由此可知,如果需要使用GetDC(),我们对消息循环函数必须要加上对WM_ERASEBKGND的处理:
         LRESULT CALLBACK MainWndProc (HWND hWnd, UINT wMsg , WPARAM wParam ,
                                                                     LPARAM lParam )
         {
             switch (wMsg )
             {
                 case WM_PAINT :
                         OnPaintMainWnd (hWnd, wMsg, wParam, lParam );
                         break ;
                 case WM_ERASEBKGND :   
                         return 0;   
                 .......         
             }
             return DefWindowProc (hWnd, wMsg, wParam, lParam );
         }
         只要系统不对WM_ERASEBKGND进行默认处理,我们用GetDC()替代BeginPaint()就可以正常使用.
       
         至此我们可以看出BeginPaint(),EndPaint()和GetDC(),ReleaseDC()的区别.前一对只能用在WM_PAINT响应 函数中,并且绘制背景时不会被抹掉;后一对随处可用,但如果用在WM_PAINT响应函数中,那么接下来将会被WM_ERASEBKGND消息的响应函数 的背景绘制给抹掉.

2.绘图闪烁问题        
     有时候我们大量绘制屏幕时,可能会出现屏幕闪烁问题,这时候可以采用双缓冲的做法.步骤首先是创建一个内存DC,然后往内存DC中绘图,最后把内存DC的内容复制到显示DC中,完成绘制.具体过程并不复杂,结合代码来说明一下.
    
PS:这段代码也是响应WM_PAINT 消息的.
   
    
PAINTSTRUCT ps;
     HDC hdc;
           
     hdc = BeginPaint (hWnd, &ps); //获取屏幕显示DC  
     HDC hdcMem = CreateCompatibleDC(hdc); //创建内存DC
            //创建一个bmp内存空间
     HBITMAP hBmp = CreateCompatibleBitmap(hdc,SCREEN_WIDTH,SCREEN_HEIGHT);
     HGDIOBJ hOldSel = SelectObject(hdcMem,hBmp);    //将bmp内存空间分配给内存DC
           //这是使用者需要绘制的画面,全部往内存DC绘制
     Rectangle(hdcMem,0,0,SCREEN_WIDTH,SCREEN_HEIGHT);

     DrawMenuButton(hdcMem);
         //将内存DC的内容复制到屏幕显示DC中,完成显示
     BitBlt(hdc,0,0,SCREEN_WIDTH,SCREEN_HEIGHT,hdcMem,0,0,SRCCOPY);     
     SelectObject(hdcMem,hOldSel);    //清除资源  
     DeleteDC(hdcMem);   
     EndPaint(hWnd,&ps
);

当程序在无响应(卡死)后恢复时,绘图子窗口中的图形消失,这是一个典型的 **GDI 绘图内容未持久化** 问题。 --- ### 回答问题: #### ✅ 问题原因: Windows 的图形系统是基于“重绘机制”的。所有通过 GDI 函数(如 `TextOut`、`Rectangle`、`LineTo` 等)绘制的内容都只存在于显存或设备上下文中,并不会自动保存。一旦窗口被遮挡、最小化再还原,或者系统需要重绘窗口区域时,Windows 会发送 `WM_PAINT` 消息要求应用程序重新绘制客户区。 如果你的绘图代码写在了 `WM_LBUTTONDOWN`、`WM_TIMER` 或其他非 `WM_PAINT` 处理中(即“临时绘图”),那么: - 当窗口重绘时,这些绘图操作不会重复执行; - 原有图形就被清除 → **导致图形丢失**。 特别是在程序“无响应”期间: - 系统无法处理消息队列; - 所有屏幕更新停滞; - 恢复后 Windows 强制触发 `WM_PAINT` 请求重绘; - 若你的绘图逻辑不在 `WM_PAINT` 中,图形就无法恢复。 --- ### ✅ 正确解决方案: 你必须将所有绘图操作放在两个地方之一: #### 方法一:在 `WM_PAINT` 消息中绘图(推荐) ```c LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { static POINT ptStart = {0}, ptEnd = {0}; static BOOL bDrawing = FALSE; switch (msg) { case WM_LBUTTONDOWN: ptStart.x = LOWORD(lParam); ptStart.y = HIWORD(lParam); bDrawing = TRUE; return 0; case WM_MOUSEMOVE: if (wParam & MK_LBUTTON && bDrawing) { // 不在此处永久绘图!仅用于实时反馈可用 InvalidateRect HDC hdc = GetDC(hwnd); SelectPen(hdc, CreatePen(PS_SOLID, 1, RGB(0,0,0))); MoveToEx(hdc, ptStart.x, ptStart.y, NULL); LineTo(hdc, LOWORD(lParam), HIWORD(lParam)); DeleteObject(SelectObject(hdc, GetStockObject(BLACK_PEN))); ReleaseDC(hwnd, hdc); ptStart = MAKEPOINT(lParam); // 更新起点 } return 0; case WM_LBUTTONUP: if (bDrawing) { ptEnd.x = LOWORD(lParam); ptEnd.y = HIWORD(lParam); bDrawing = FALSE; InvalidateRect(hwnd, NULL, TRUE); // 触发重绘 } return 0; case WM_PAINT: { PAINTSTRUCT ps; HDC hdc = BeginPaint(hwnd, &ps); // 在这里重新绘制所有内容 if (!bDrawing) { HPEN hPen = CreatePen(PS_SOLID, 1, RGB(0,0,255)); SelectObject(hdc, hPen); MoveToEx(hdc, ptStart.x, ptStart.y, NULL); LineTo(hdc, ptEnd.x, ptEnd.y); DeleteObject(hPen); } EndPaint(hwnd, &ps); break; } case WM_DESTROY: PostQuitMessage(0); return 0; } return DefWindowProc(hwnd, msg, wParam, lParam); } ``` > ⚠️ 关键点:不要依赖一次性绘图。所有图形状态要记录下来(如坐标、颜色等),并在 `WM_PAINT` 中根据这些数据重新绘制。 --- #### 方法二:使用内存双缓冲 + 位图保存图像 如果图形复杂,可以创建一个内存设备上下文和兼容位图,把用户的绘图先画到内存位图上,然后每次 `WM_PAINT` 都把这个位图贴上去。 ```c HBITMAP hBitmap = NULL; HDC hMemDC = NULL; int bitmapW = 800, bitmapH = 600; // 初始化(在窗口创建后) void InitBackBuffer(HDC hdc, int w, int h) { if (hMemDC) DeleteDC(hMemDC); if (hBitmap) DeleteObject(hBitmap); hMemDC = CreateCompatibleDC(hdc); hBitmap = CreateCompatibleBitmap(hdc, w, h); SelectObject(hMemDC, hBitmap); PatBlt(hMemDC, 0, 0, w, h, WHITENESS); // 白色背景 } // 绘图示例:画线并保存到位图 void DrawLineToMemory(int x1, int y1, int x2, int y2) { HPEN hPen = CreatePen(PS_SOLID, 1, RGB(0,0,0)); SelectObject(hMemDC, hPen); MoveToEx(hMemDC, x1, y1, NULL); LineTo(hMemDC, x2, y2); DeleteObject(hPen); } // WM_PAINT 处理 case WM_PAINT: { PAINTSTRUCT ps; HDC hdc = BeginPaint(hwnd, &ps); BitBlt(hdc, 0, 0, bitmapW, bitmapH, hMemDC, 0, 0, SRCCOPY); EndPaint(hwnd, &ps); break; } ``` 这样即使窗口重绘,图形也从内存位图中恢复,不会丢失。 --- ### 总结: | 错误做法 | 正确做法 | |--------|---------| | 直接在鼠标消息里用 `LineTo` 画图 | 记录绘图数据,在 `WM_PAINT` 中重绘 | | 使用 `GetDC()` 后直接绘图不保存 | 使用内存 DC 和位图缓存图像 | | 忽略 `InvalidateRect()` 和 `BeginPaint` 机制 | 遵循标准 Windows 绘图流程 | ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值