之前有學MFC的同學告訴我覺得Windows的控件重繪難以了解,就算重繪成功了還是有些地方不明白,我覺得可能很多人都有這樣的問題,在這裡我從Windows窗體的最基本原理來講解,如果你有類似的疑惑希望這篇文章可以幫你解惑。
首先,如果看過Win32 SDK程式設計的都知道Windows的三大核心系統:負責視窗對象産生和消息分發的USER子產品,負責圖像顯示繪制的GDI子產品,負責記憶體、程序、IO管理的KERNEL子產品。試想象一下如何在一個像素陣列上産生視窗對象,其實就是使用GDI繪制視窗,不停的以一定的頻率重新整理顯示在螢幕上,這就是圖形界面,如果由在DOS或Windows DOS模拟器下編寫圖形界面的經驗這個比較好了解。是以說其實USER子產品中的視窗産生是依靠GDI子產品的(包括菜單、滾動條等都是使用GDI來繪制的)。
那麼,下面我們就從USER子產品和GDI子產品來說說Windows 的窗體原理。
如果接觸過Win32 SDK程式設計的知道一個标準Windows窗體的産生過程:設計視窗類、注冊視窗類、建立視窗、顯示視窗、啟動消息循環泵循環擷取消息分發到窗體過程函數處理。為了保證部落格的連貫性,在這裡我貼上一個标準Windows窗體的産生代碼。
#include <windows.h>
LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam);
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow)
{
static TCHAR szAppName[] = TEXT ("視窗類名稱");
HWND hwnd;
MSG msg;
WNDCLASSEX wndclassex = {0};
//設計視窗類
wndclassex.cbSize = sizeof(WNDCLASSEX);
wndclassex.style = CS_HREDRAW | CS_VREDRAW;
wndclassex.lpfnWndProc = WndProc;
wndclassex.cbClsExtra = 0;
wndclassex.cbWndExtra = 0;
wndclassex.hInstance = hInstance;
wndclassex.hIcon = LoadIcon (NULL, IDI_APPLICATION);
wndclassex.hCursor = LoadCursor (NULL, IDC_ARROW);
wndclassex.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH);
wndclassex.lpszMenuName = NULL;
wndclassex.lpszClassName = szAppName;
wndclassex.hIconSm = wndclassex.hIcon;
//注冊視窗類
if (!RegisterClassEx (&wndclassex))
{
MessageBox (NULL, TEXT ("RegisterClassEx failed!"), szAppName, MB_ICONERROR);
return 0;
}
//産生視窗
hwnd = CreateWindowEx (WS_EX_OVERLAPPEDWINDOW,
szAppName,
TEXT ("視窗名稱"),
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,
NULL,
hInstance,
NULL);
//顯示視窗
ShowWindow (hwnd, iCmdShow);
UpdateWindow (hwnd);
//啟動消息循環泵循環擷取消息配置設定到窗體過程函數處理
while (GetMessage (&msg, NULL, 0, 0))
TranslateMessage (&msg);
DispatchMessage (&msg);
return msg.wParam;
}
//窗體過程函數
LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
HDC hdc;
PAINTSTRUCT ps;
switch (message)
case WM_CREATE:
return (0);
case WM_PAINT:
hdc = BeginPaint (hwnd, &ps);
EndPaint (hwnd, &ps);
case WM_DESTROY:
PostQuitMessage (0);
return DefWindowProc (hwnd, message, wParam, lParam);
需要明白的是,所有Windows的窗體及控件歸根結底都是使用CreateWindow或CreateWindowEx來建立的,他們都需要标準Windows窗體的産生過程。
普通的窗體好了解,主要需要弄清楚是對話框及控件的産生和消息分派處理流程。
對話框及其子控件的管理依靠Windows内建的對話框管理器,對話框管理器的工作包括:
1.根據我們在資源設計器中設計的對話框及子控件産生的.rc檔案來自動生成對話框和子控件(如果有手動編寫.rc檔案的經曆的話,知道編寫RC檔案其實就是指定視窗和子控件大小、類型、樣式等參數,對話框管理器将這些參數傳入CreateWindow函數産生窗體)
2.模态對話框直接顯示窗體,非模态對話框消息指明WS_VISIBLE屬性的話,需要調用ShowWindow來顯示窗體。
4.維護一個消息循環泵,對于模态對話框來說這個消息泵的消息不經過父視窗,是以表現為模态;對于非模态對話框這個消息泵消息經過主視窗,必須由主視窗傳給非模态對話框,表現為非模态。
3.維護一個内建的窗體過程函數,對于對話框來說會處理對話框的關閉打開及子視窗的焦點、tab等,對于子控件也是一樣,每個子控件會有自己類型的窗體過程函數,窗體過程函數處理子控件的獲得或失去焦點、按下或彈起、建立等表現樣式和行為。對于對話框來說,他會開放一個對話框過程函數,讓部分消息先通過對話框管理函數處理,如果對話框過程函數不處理才交給預設的内建過程函數處理,對于子控件來說,他們并沒有開放過程函數,而是由内建窗體函數将要處理的消息發給父視窗處理。
那麼對話框管理器完成了标準Windows窗體的産生中後半部分工作,至于設計視窗類和注冊視窗類這是由Windows自己預先做好了的,如常見的“button”、“listbox”、“edit”類等等。
一個簡要的示意圖如下

那麼既然所有的窗體(包括對話框和控件)産生過程一樣,那麼我們就可以将對話框管理器的部分工作替換掉:
1.不使用對話框讀取.rc模闆的方式,直接将參數傳遞給CreateWindow函數來建立對話框和控件,這就是常見的動态建立控件原理。
2.設定控件自繪制如BS_OWNDRAW屬性,開放控件的WM_DRAWITEM消息給父視窗,由父視窗來繪制按鈕樣式,這就是常見的控件重繪原理。
3.替換掉内建的窗體函數,将消息傳到自定義的窗體過程函數處理,這就是常見的控件子類化原理。
下面,為了做示範,先用通用模闆建立的方式建立一個模态對話框和其子控件,然後模闆建立一個非模态對話框,在非模态對話框中使用動态建立的方式建立和模态對話框中模闆建立一樣的按鈕(當然位置和大小等可能不一樣,這裡隻是為了說明原理故筆者并沒有去管這些細節,如果你願意完全可以把它們做的一模一樣)。
代碼太長,這裡隻貼出部分代碼,詳細代碼請下載下傳示範檔案
主視窗消息泵
while (GetMessage (&msg, NULL, 0, 0))
{
//注意非模态對話框消息由主視窗分發
if (hDlgModeless == NULL || !IsDialogMessage(hDlgModeless, &msg))
TranslateMessage (&msg);
DispatchMessage (&msg);
}
主視窗菜單響應
case IDM_TEMPLATE:
DialogBox(GetWindowLong(hwnd, GWL_HINSTANCE),
IDD_TEMPLATE,
hwnd,
TemplateProc);
break;
case IDM_CREATE:
hDlgModeless = CreateDialog(GetWindowLong(hwnd, GWL_HINSTANCE),
MAKEINTRESOURCE(IDD_CREATE),
hwnd,
CreateProc);
ShowWindow(hDlgModeless, SW_NORMAL);//注意非模态對話框不指明WS_VISIBLE屬性必須顯示調用ShowWindow來顯示
模闆建立的模态對話框對話框過程函數
BOOL CALLBACK TemplateProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
switch(message)
case WM_CLOSE:
{
EndDialog(hDlg,0);
}
return (TRUE);
case WM_COMMAND:
switch (LOWORD(wParam))
case IDCANCEL:
{
SendMessage(hDlg, WM_CLOSE, 0, 0);
}
return (TRUE);
case IDOK:
return (FALSE);
return (FALSE);
模闆建立的非模态對話框的對話框過程函數
BOOL CALLBACK CreateProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
case WM_INITDIALOG:
//動态建立控件子視窗
CreateWindow(TEXT("button"),
TEXT("确定"),
WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON,
10, 10,
100, 50,
hDlg,
(HMENU)IDOK,
GetWindowLong(hDlg, GWL_HINSTANCE),
NULL);
TEXT("取消"),
WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON,
10, 100,
100, 50,
hDlg,
(HMENU)IDCANCEL,
GetWindowLong(hDlg, GWL_HINSTANCE),
NULL);
DestroyWindow(hDlg);
hDlgModeless = NULL;//注意及時将指針置0防止視窗銷毀後消視窗分發消息
建立效果
模态對話框
非模态對話框
二者起到了相同的作用,動态建立比模闆建立要靈活的多,這個深入學習請自行查找相關資料。上例中需要注意的模态對話框和非模态對話框,前者的消息不流經主視窗消息泵,後者的消息要先流經主視窗消息泵。
寫這篇博文的初衷就是講解控件重繪原理,自然不能少了這一内容,在剛剛提到了修改對話框管理器的行為的幾種方式,後兩種(開放WM_DRAWITEM消息和控件子類化)都是常用的控件重繪技巧,在這一節先講WM_DRAWITEM消息重繪,下一節講控件子類化重繪,都是以按鈕的重繪為例來講解。
WM_DRAWITEM顧名思義當控件需要重繪的時候發給主視窗的消息,一般在按鈕按下或彈起、獲得焦點或失去焦點、建立等時候會産生這一消息,預設是不開啟重繪消息的,如果使用模闆建立按鈕必須在按鈕屬性中設定OwnDraw屬性為True,如果動态建立按鈕必須加上BS_OWNDRAW這一屬性。
下面我要重繪兩個個按鈕,按鈕是模闆建立的,是預設的IDOK和IDCANCEL按鈕,希望達到的效果是
按鈕普通狀态分别為
按鈕獲得焦點分别為
按鈕按下狀态分别為
下面先貼出繪制部分代碼,再講解
BOOL CALLBACK SelfDrawProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
LPDRAWITEMSTRUCT pdis;
HDC hMemDc;
static HINSTANCE hInstance;
static HBITMAP hBitmapOK_D;
static HBITMAP hBitmapOK_U;
static HBITMAP hBitmapOK_F;
static HBITMAP hBitmapCANCEL_D;
static HBITMAP hBitmapCANCEL_U;
static HBITMAP hBitmapCANCEL_F;
static HWND hwndOk;
static HWND hwndCanel;
static BITMAP bm;
hInstance = GetWindowLong(hDlg, GWL_HINSTANCE);
hwndOk = GetDlgItem(hDlg, IDOK);
hwndCanel = GetDlgItem(hDlg, IDCANCEL);
hBitmapOK_D = LoadBitmap(hInstance, TEXT("image1d"));
hBitmapOK_U = LoadBitmap(hInstance, TEXT("image1u"));
hBitmapOK_F = LoadBitmap(hInstance, TEXT("image1f"));
hBitmapCANCEL_D = LoadBitmap(hInstance, TEXT("image2d"));
hBitmapCANCEL_U = LoadBitmap(hInstance, TEXT("image2u"));
hBitmapCANCEL_F = LoadBitmap(hInstance, TEXT("image2f"));
GetObject(hBitmapCANCEL_D, sizeof(BITMAP), (PTSTR)&bm);
//調整按鈕大小和最大圖檔一樣大
SetWindowPos(hwndOk, HWND_TOPMOST, 0, 0, bm.bmWidth, bm.bmHeight, SWP_NOZORDER | SWP_NOMOVE);
SetWindowPos(hwndCanel, HWND_TOPMOST, 0, 0, bm.bmWidth, bm.bmHeight, SWP_NOZORDER | SWP_NOMOVE);
//自繪制按鈕
case WM_DRAWITEM:
//獲得繪制結構體,包含繪制的按鈕DC和目前按鈕狀态等
pdis = (LPDRAWITEMSTRUCT)lParam;
if (pdis->CtlType == ODT_BUTTON)//隻繪制button類型
hdc = pdis->hDC;
SaveDC(hdc);//儲存DC,繪制完必須恢複預設
//繪制預設狀态
hMemDc = CreateCompatibleDC(hdc);
SelectObject(hMemDc, pdis->CtlID == IDOK ? hBitmapOK_U : hBitmapCANCEL_U);
BitBlt(hdc, 0, 0, bm.bmWidth, bm.bmHeight, hMemDc, 0, 0, SRCCOPY);
DeleteDC(hMemDc);
//繪制擷取焦點時狀态
if (pdis->itemState & ODS_FOCUS)
hMemDc = CreateCompatibleDC(hdc);
SelectObject(hMemDc, pdis->CtlID == IDOK ? hBitmapOK_F : hBitmapCANCEL_F);
BitBlt(hdc, 0, 0, bm.bmWidth, bm.bmHeight, hMemDc, 0, 0, SRCCOPY);
DeleteDC(hMemDc);
//繪制下壓狀态
if (pdis->itemState & ODS_SELECTED)
SelectObject(hMemDc, pdis->CtlID == IDOK ? hBitmapOK_D : hBitmapCANCEL_D);
RestoreDC(hdc, -1);
在WM_INITDIALOG函數中加載相關資源和設定按鈕大小
在WM_DRAWITEM完成主要繪制工作,獲得WM_DRAWITEM消息時獲得繪制的結構體,這個結構體包括目前要繪制的按鈕的ID、狀态等,我們主要的工作就是将對應狀态的按鈕貼上相應的位圖即可。
效果如下
WM_DRAWITEM消息控件重繪是最常用的重繪技巧,在網上常見的别人封裝好的自定義控件都是這樣的原理。
子類化是借鑒C++的面向對象中的繼承和重載的思想,基本意思就是如果子類對消息處理了的話對應C++的重載,這時候父類就沒辦法再處理這個消息,除非人為的将消息傳遞給父類,所有的消息先流經子類再到父類,當然這一過程需要子類的配合,具體意思我們用代碼來說明。
同樣是達到上一節WM_DRAWITEM繪制的按鈕效果
我們用控件子類化完成這一效果,貼出部分代碼,完整代碼請下載下傳示範檔案
對話框過程函數
WNDPROC btnOkOldProc, btnCancelOldProc;
BOOL CALLBACK SubclassProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
//視窗子類化
btnOkOldProc = SetWindowLong(hwndOk, GWL_WNDPROC, (LONG)BtnProc);
btnCancelOldProc = SetWindowLong(hwndCanel, GWL_WNDPROC, (LONG)BtnProc);
按鈕過程函數(子類)
typedef enum tagBUTTONSTATE
BTNSTATE_DEFAULT=0,
BTNSTATE_FOCUS,
BTNSTATE_SELECTED
}BUTTONSTATE;
LRESULT CALLBACK BtnProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
static int id;
static BOOL bOnce = TRUE;
static BUTTONSTATE btnOkState=BTNSTATE_FOCUS;
static BUTTONSTATE btnCancelState=BTNSTATE_DEFAULT;
id = GetWindowLong(hwnd, GWL_ID);
//初次進入函數加載資源,模拟WM_CREATE
if (TRUE == bOnce)
hInstance = GetWindowLong(hwnd, GWL_HINSTANCE);
hBitmapOK_D = LoadBitmap(hInstance, TEXT("image1d"));
hBitmapOK_U = LoadBitmap(hInstance, TEXT("image1u"));
hBitmapOK_F = LoadBitmap(hInstance, TEXT("image1f"));
hBitmapCANCEL_D = LoadBitmap(hInstance, TEXT("image2d"));
hBitmapCANCEL_U = LoadBitmap(hInstance, TEXT("image2u"));
hBitmapCANCEL_F = LoadBitmap(hInstance, TEXT("image2f"));
GetObject(hBitmapCANCEL_D, sizeof(BITMAP), (PTSTR)&bm);
SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, bm.bmWidth, bm.bmHeight, SWP_NOZORDER | SWP_NOMOVE);
bOnce = FALSE;
//注意這個消息不會進入
hMemDc = CreateCompatibleDC(hdc);
//繪制不同狀态下的按鈕樣式
if (btnOkState == BTNSTATE_DEFAULT && id == IDOK)
SelectObject(hMemDc, hBitmapOK_U);
if(btnCancelState == BTNSTATE_DEFAULT && id==IDCANCEL)
SelectObject(hMemDc, hBitmapCANCEL_U);
if (btnOkState == BTNSTATE_FOCUS && id==IDOK)
SelectObject(hMemDc, hBitmapOK_F);
if(btnCancelState == BTNSTATE_FOCUS && id==IDCANCEL)
SelectObject(hMemDc, hBitmapCANCEL_F);
if (btnOkState == BTNSTATE_SELECTED && id==IDOK)
SelectObject(hMemDc, hBitmapOK_D);
if(btnCancelState == BTNSTATE_SELECTED && id==IDCANCEL)
SelectObject(hMemDc, hBitmapCANCEL_D);
BitBlt(hdc, 0, 0, bm.bmWidth, bm.bmHeight, hMemDc, 0, 0, SRCCOPY);
DeleteDC(hMemDc);
case WM_SETFOCUS:
if (id==IDOK)
btnOkState = BTNSTATE_FOCUS;
else
btnCancelState = BTNSTATE_FOCUS;
case WM_KILLFOCUS:
btnOkState = BTNSTATE_DEFAULT;
btnCancelState = BTNSTATE_DEFAULT;
case WM_KEYDOWN:
if (wParam == VK_SPACE)
if (id==IDOK)
btnOkState = BTNSTATE_SELECTED;
else
btnCancelState = BTNSTATE_SELECTED;
InvalidateRect(hwnd, NULL, TRUE);
case WM_KEYUP:
btnOkState = BTNSTATE_FOCUS;
btnCancelState = BTNSTATE_FOCUS;
case WM_LBUTTONDOWN:
SetCapture(hwnd);
case WM_LBUTTONUP:
ReleaseCapture();
DestroyWindow(hwnd);
return CallWindowProc (id == IDOK ? btnOkOldProc : btnCancelOldProc, hwnd, message, wParam, lParam);
在以上代碼,我們在對話框的WM_INITDIALOG消息中強制換掉按鈕原有的内建窗體過程函數,使用我們自己的BtnProc過程函數。需要注意的是在我們換掉
按鈕原有的内建窗體過程函數的時候按鈕已經建立完成,是以如果我們在BtnProc的WM_CREATE設定斷點,程式是不會進入的。和WM_DRAWITEM一樣,我們需要按鈕的不同狀态時繪制,因為我們采用自己的BtnProc過程函數,是以我們隻能自己來維護按鈕的狀态,在WM_PAINT函數中根據不同狀态繪制不同樣式的按鈕,在其他消息中處理按鈕的按下或彈起、獲得焦點、或失去焦點等狀态轉變。
建立效果如下
我們基本上模拟了WM_DRAWITEM消息重繪效果:按Tab鍵切換焦點,按Space鍵按鈕按下彈起(當然隻是為了示範原理,會有一些Bug,你可以想辦法完善他們)。在上訴代碼中,我們在最後調用了原來的内建的窗體過程函數,我們處理了WM_PAINT、WM_KEYUP、WM_KEYDOWN等消息,這些消息都return (0)直接傳回了,即内建的窗體過程函數沒有機會處理這些消息,其他的子類沒有處理的消息都傳給原來内建的窗體過程函數處理了,如果我們想原來的内建窗體過程函數也處理WM_PAINT,那麼将return (0)改成break即可。這就是我上面提到的子類化的實作必須依靠子類化窗體函數的配合,我們也可以将所有的消息都在子類中處理不回傳給原來的内建視窗,但是這樣的工作量太大,一般是不會這樣做的。
另外,可以看到相比于WM_DRAWITEM消息重繪,子類化實作控件重繪工作量要大得多,當然這樣的靈活性要更大。實際上,微軟提供子類化的作用更多是為了重新定制子控件的行為,比如說要将一組相同按鈕按下時發送一個自定義消息,這時候就可以将這些按鈕的消息子類化都先流經一個子類化窗體過程函數,然後再調用内建的窗體過程函數。
總結來說,一般重繪控件樣式使用WM_DRAWITEM消息,重新定制控件行為使用窗體子類化。
本文轉自莫水千流部落格園部落格,原文連結:http://www.cnblogs.com/zhoug2020/p/6264732.html,如需轉載請自行聯系原作者