1. 程式人生 > >深度分析WM_PAINT和WM_ERASEBKGND消息

深度分析WM_PAINT和WM_ERASEBKGND消息

erase ble title 填充 switch 生成 article () outb

做windows開發這麽久了,一直以來對WM_PAINT和WM_ERASEBKGND消息總是感覺理解的不準確,每次要自繪一個窗口都因為知其然不知其所以然,偶然發現一篇文章,詳細透徹地分了這個兩個消息的用途和設計初衷,這篇文章也是我見過最深入也是最準確關於WM_PAINT和WM_ERASEBKGND消息的,文中每一句話都值得咀嚼。先轉載如下:

一直以來,對於WM_PAINT和WM_ERASEBKGND消息不是很清楚,從書上和網上找了很多資料,大體上有以下幾點說法:
1>WM_PAINT先產生,WM_ERASEBKGND後產生

2.WM_PAINT產生後,在調用BeginPaint時

hdc = BeginPaint(hWnd, &ps);
如果ps.fErase為true,則BeginPaint會產生WM_ERASEBKGND消息

3.BeginPaint函數用來擦除窗口背景

4.WM_ERASEBKGND用來繪制背景

經過調試、分析,發現上面的說法並不正確。以下是一些測試代碼,代碼後面附上一些分析。最後總結出幾點,可以解釋程序中出現的所有關於窗口重繪的問題。
如有不正確的地方,大家可以指正。

為了說明問題,在此不說WM_NCPAINT消息(非客戶區消息),只說WM_ERASEBKGND消息和客戶區的WM_PAINT消息

//此段代碼摘自vc6應用程序向導自動生成的代碼,並添加了一些測試代碼

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
int wmId, wmEvent;
PAINTSTRUCT ps;
HDC hdc;
TCHAR szHello[MAX_LOADSTRING];
LoadString(hInst, IDS_HELLO, szHello, MAX_LOADSTRING);

switch (message)
{
case WM_COMMAND:
wmId = LOWORD(wParam);
wmEvent = HIWORD(wParam);
// Parse the menu selections:
switch (wmId)
{
case IDM_ABOUT:
DialogBox(hInst, (LPCTSTR)IDD_ABOUTBOX, hWnd, (DLGPROC)About);
break;
case IDM_EXIT:
DestroyWindow(hWnd);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
break;
case WM_ERASEBKGND: //如果處理了這個消息,則默認消息處理函數不會調用,背景就不會繪制
{
static int iCount=0;
char ch[MAX_PATH];
sprintf(ch,"%d ---------WM_ERASEBKGND\n",iCount); //這個函數需要包含#include<stdio.h>
OutputDebugString(ch); //調試時便於觀察
iCount++;
break;
}
case WM_PAINT:
{
OutputDebugString(" -------------WM_PAINT\n");
hdc = BeginPaint(hWnd, &ps); //使無效區域變得有效,並填充ps結構
// TODO: Add any drawing code here...

//繪制一個藍色橢圓,ps.rcPaint保存了客戶區矩形
HBRUSH hbrush=::CreateSolidBrush(RGB(0,0,255));
::SelectObject(hdc,hbrush);
::Ellipse(hdc,ps.rcPaint.left,ps.rcPaint.top,ps.rcPaint.right,ps.rcPaint.bottom);
::DeleteObject(hbrush);

EndPaint(hWnd, &ps);
break;
}
case WM_LBUTTONDOWN: //調用DefWindowProc擦除客戶區背景
{
HDC hdc;
hdc=::GetDC(hWnd);
WPARAM w=(WPARAM)hdc;
LPARAM l=0;
DefWindowProc(hWnd, WM_ERASEBKGND, w, l);
}
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}

先說一下程序運行時發現的一些現象:
1.
上面的代碼:如果添加了WM_ERASEBKGND消息,裏面什麽也不做,如下
case WM_ERASEBKGND:

break;

則當程序運行時,如果收到WM_ERASEBKGND消息,則這個switch-case結構中就不會執行默認消息處理函數DefWindowProc,運行時發現,窗口的背景就沒有了,即背景為空。
這說明了窗口背景僅僅是由默認的消息處理函數DefWindowProc繪制的。
(註:註冊窗口類時,背景設置為白色wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);)


2.
如上面的代碼,因為有WM_ERASEBKGND消息,則程序運行時窗口背景為NULL,
但是如果添加了WM_LBUTTONDOWN消息,從裏面調用默認窗口消息處理函數,如下
case WM_LBUTTONDOWN:
{
HDC hdc;
hdc=::GetDC(hWnd);
WPARAM w=(WPARAM)hdc; //變量w作為WM_ERASEBKGND消息的wParam參數,保存了設備環境句柄
LPARAM l=0;
DefWindowProc(hWnd, WM_ERASEBKGND, w, l); //調用默認消息處理函數DefWindowProc
}
break;
程序運行時,如果用鼠標單擊一下窗口客戶區,則窗口的背景就會顯示!這進一步說明了窗口的背景色是由默認消息處理函數DefWindowProc繪制的。

3.
以上代碼,因為添加了WM_ERASEBKGND消息,所以窗口背景是空。
雖然在WM_PAINT消息中有
hdc = BeginPaint(hWnd, &ps);函數的調用,但是窗口背景仍然是空,這說明了BeginPaint函數並不會擦除背景(即用默認畫刷繪制窗口背景)。

BeginPaint函數只做了兩件事情:
1》使窗口無效區域變得有效,從而使Windows不再發送WM_PAINT消息(直到窗口大小改變等,使窗口再次變得無效)。
(如果窗口一直無效,則Windows會不停地發送WM_PAINT消息)

1》填充PAINTSTRUCT結構。填充這個結構的目的,是讓程序員可以根據ps變量中的標誌值進行某些操作

4.
調試的時候,發現:當窗口改變大小,或者其它操作使窗口變得無效時,WM_ERASEBKGND消息總是先於WM_PAINT消息發出,而且如果產生WM_ERASEBKGND消息,
則最後一個WM_ERASEBKGND的下一條消息一定是WM_PAINT消息(WM_ERASEBKGND可能會連續產生幾次)。WM_ERASEBKGND消息和WM_PAINT消息之間沒有其它消息


--------------------------------------------
以下是一些總結

1.窗口背景的擦除(即繪制)

窗口的背景色是由默認的消息處理函數DefWindowProc擦除的(即這個函數使用註冊窗口類時使用的背景刷擦除窗口背景)。
什麽時候繪制?在窗口函數收到WM_ERASEBKGND消息,DefWindowProc函數以WM_ERASEBKGND為參數,才會繪制窗口背景
(註:當WM_ERASEBKGND消息產生後,窗口一定有一部分變得無效)

2.窗口的無效:

當拖動窗口的一個頂點改變了窗口的大小、窗口由最小化恢復到最大化、窗口的一部分被其它窗口遮住又重新顯示、調用MoveWindow函數改變了窗口大小、窗口移動到桌面之外的

部分被拖回重新顯示時,窗口就會變得無效。 無效區域是整個客戶區,因此默認窗口處理函數DefWindowProc會擦除整個客戶區。
(註:拖動窗口標題欄移動窗口,只要窗口沒有移動到屏幕之外,那麽這兩個消息都不產生)

當窗口無效時,Windows會給窗口發出WM_ERASEBKGND消息和WM_PAINT消息,而且WM_ERASEBKGND先發出一次或者幾次,緊接著是WM_PAINT


例外:InvalidateRect函數的調用會使窗口變得無效,並產生WM_ERASEBKGND消息和WM_PAINT消息,而WM_ERASEBKGND是否產生取決於參數bErase

void InvalidateRect (
LPCRECT lpRect,
BOOL bErase = TRUE );
當參數bErase為true時,WM_ERASEBKGND消息產生,當bErase為false時WM_ERASEBKGND消息不產生


3.消息的處理過程
當窗口無效時,

先發出WM_ERASEBKGND消息若幹次-----------再發出WM_PAINT消息,WM_ERASEBKGND和WM_PAINT之間沒有其它消息
WM_ERASEBKGND消息的後面一定是WM_PAINT

1》WM_ERASEBKGND消息的處理:
上面的代碼,如果沒有添加WM_ERASEBKGND,則默認的消息處理函數DefWindowProc會被調用,此時的DefWindowProc會擦除窗口背景(即繪制背景),並且ps.fErase會為FALSE

如果添加了WM_ERASEBKGND消息,DefWindowProc就不會被調用,則無法擦除窗口背景,並且ps.fErase會為true

2》WM_PAINT的處理
在這個消息中如果調用了hdc = BeginPaint(hWnd, &ps);函數,則此函數只做了兩件事:填充ps結構、使窗口重新變得有效

另外DefWindowProc函數也會使窗口變得有效

關於ps.fErase;
這個參數和窗口函數WndProc的返回值有關:
當窗口函數WndProc返回true;則產生WM_PAINT消息時,ps.fErase就為false;表明系統擦除了背景
當窗口函數WndProc返回false;則產生WM_PAINT消息時,ps.fErase就為true;表明系統沒有擦除背景

設想一下,當上面的代碼中添加了WM_ERASEBKGND消息並在其中直接返回true(這表明系統已經繪制了窗口背景),則ps.fErase就為false
case WM_ERASEBKGND:
return true; //窗口函數WndProc返回true;

註意返回的真或者假只是讓程序員可以看見ps.fErase,並作出自己的代碼,與窗口的顯示即背景沒有關系
有些人說當ps.fErase==true,BeginPaint函數會發送一個WM_ERASEBKGND消息,其實BeginPaint並未發出WM_ERASEBKGND消息

4.自己繪制背景或者系統繪制背景。
如果程序員不想系統擦除背景,而自己想繪制背景,怎麽辦呢?方法是在WM_ERASEBKGND消息處理中添加自己的繪制代碼。
對於WM_ERASEBKGND消息,wParam參數保存了用於繪制的設備環境,lParam不使用。

如上面的示例代碼,當添加了WM_ERASEBKGND消息,則switch---case中就不會調用DefWindowProc函數繪制背景。這時,程序員自己就可以添加繪制代碼
而在基於MFC的程序中,是這樣處理自繪代碼的:

BOOL CCeDlg::OnEraseBkgnd(CDC* pDC) //這個函數就是WM_ERASEBKGND的消息處理函數
{
// TODO: Add your message handler code here and/or call default
//添加自繪代碼
...
return TRUE; //返回真,代表著窗口函數的返回值。以便於程序員在WM_PAINT消息中作出相應處理(如果需要)。這裏返回時就不會調用下面的默認處理

//下面將調用系統默認的消息處理函數DefWindowProc進行背景的默認繪制。
return CDialog::OnEraseBkgnd(pDC); //不執行自動生成的這個函數
}

執行這個函數時,提示用戶繪制背景,如果用戶沒有繪制背景,則return CDialog::OnEraseBkgnd(pDC);調用默認的窗口處理函數進行背景的擦除


BOOL CCeDlg::OnEraseBkgnd(CDC* pDC)
{
// TODO: Add your message handler code here and/or call default
//添加自繪背景代碼
CBitmap m_bitmap;
BITMAP m_bmInfo;
m_bitmap.LoadBitmap(IDB_BITMAP1);
m_bitmap.GetObject(sizeof(m_bmInfo),&m_bmInfo);

CDC memDC;
memDC.CreateCompatibleDC(pDC);
memDC.SelectObject(&m_bitmap);

GetClientRect(m_rect);
pDC->StretchBlt(0,0,m_rect.Width(),m_rect.Height(),

&memDC,0,0,m_bmInfo.bmWidth,m_bmInfo.bmHeight,SRCCOPY); //內存拷貝函數。繪制背景
memDC.DeleteDC();

return true;//返回真,代表著窗口函數的返回值。以便於程序員在WM_PAINT消息中作出相應處理(如果需要)。這裏返回時就不會調用下面的默認處理

//下面將調用系統默認的消息處理函數DefWindowProc進行背景的默認繪制。
return CDialog::OnEraseBkgnd(pDC); //不執行自動生成的這個函數
}

5.WM_ERASEBKGND消息和WM_PAINT消息的另外一種含義:背景色與前景色

可以這樣理解WM_ERASEBKGND消息和WM_PAINT消息:

WM_ERASEBKGND消息用於通知系統或者程序員繪制背景色
WM_PAINT消息用於通知程序員繪制前景色,比如在WM_PAINT中調用TextOut函數輸出文本

非常感謝原作者的工作,文章地址:http://blog.csdn.net/sdeeds/article/details/6859530

深度分析WM_PAINT和WM_ERASEBKGND消息