之前有學MFC的同學告訴我覺得Windows的控件重繪難以理解,就算重繪成功了還是有些地方不明白,我覺得可能很多人都有這樣的問題,在這里我從Windows窗體的最基本原理來講解,如果你有類似的疑惑希望這篇文章可以幫你解惑。
1.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,
- CW_USEDEFAULT,
- CW_USEDEFAULT,
- CW_USEDEFAULT,
- NULL,
- 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);
- return (0);
- case WM_DESTROY:
- PostQuitMessage (0);
- return (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來顯示
- break;
模板創建的模態對話框對話框過程函數
- 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 (TRUE);
- }
- return (FALSE);
- }
- return (FALSE);
- }
模板創建的非模態對話框的對話框過程函數
- BOOL CALLBACK CreateProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
- {
- switch(message)
- {
- 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);
- CreateWindow(TEXT("button"),
- TEXT("取消"),
- WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON,
- 10, 100,
- 100, 50,
- hDlg,
- (HMENU)IDCANCEL,
- GetWindowLong(hDlg, GWL_HINSTANCE),
- NULL);
- }
- return (TRUE);
- case WM_CLOSE:
- DestroyWindow(hDlg);
- hDlgModeless = NULL;//注意及時將指針置0防止窗口銷毀后消窗口分發消息
- return (TRUE);
- case WM_COMMAND:
- switch (LOWORD(wParam))
- {
- case IDCANCEL:
- {
- SendMessage(hDlg, WM_CLOSE, 0, 0);
- }
- return (TRUE);
- case IDOK:
- {
- }
- return (TRUE);
- }
- return (FALSE);
- }
創建效果
模態對話框
非模態對話框
二者起到了相同的作用,動態創建比模板創建要靈活的多,這個深入學習請自行查找相關資料。上例中需要注意的模態對話框和非模態對話框,前者的消息不流經主窗口消息泵,后者的消息要先流經主窗口消息泵。
2.控件重繪(WM_DRAWITEM)






- BOOL CALLBACK SelfDrawProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
- {
- LPDRAWITEMSTRUCT pdis;
- HDC hdc;
- 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;
- switch(message)
- {
- case WM_INITDIALOG:
- {
- 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);
- }
- return (TRUE);
- 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 (TRUE);
- }
- return (FALSE);
- //自繪制按鈕
- 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)
- {
- hMemDc = CreateCompatibleDC(hdc);
- SelectObject(hMemDc, pdis->CtlID == IDOK ? hBitmapOK_D : hBitmapCANCEL_D);
- BitBlt(hdc, 0, 0, bm.bmWidth, bm.bmHeight, hMemDc, 0, 0, SRCCOPY);
- DeleteDC(hMemDc);
- }
- RestoreDC(hdc, -1);
- }
- return (TRUE);
- }
- return (FALSE);
- }

3.控件重繪(控件子類化)
子類化是借鑒C++的面向對象中的繼承和重載的思想,基本意思就是如果子類對消息處理了的話對應C++的重載,這時候父類就沒辦法再處理這個消息,除非人為的將消息傳遞給父類,所有的消息先流經子類再到父類,當然這一過程需要子類的配合,具體意思我們用代碼來說明。- WNDPROC btnOkOldProc, btnCancelOldProc;
- BOOL CALLBACK SubclassProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
- {
- static HWND hwndOk;
- static HWND hwndCanel;
- switch(message)
- {
- case WM_INITDIALOG:
- {
- hwndOk = GetDlgItem(hDlg, IDOK);
- hwndCanel = GetDlgItem(hDlg, IDCANCEL);
- //窗口子類化
- btnOkOldProc = SetWindowLong(hwndOk, GWL_WNDPROC, (LONG)BtnProc);
- btnCancelOldProc = SetWindowLong(hwndCanel, GWL_WNDPROC, (LONG)BtnProc);
- }
- return (TRUE);
- 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 (TRUE);
- }
- return (FALSE);
- }
- return (FALSE);
- }
- typedef enum tagBUTTONSTATE
- {
- BTNSTATE_DEFAULT=0,
- BTNSTATE_FOCUS,
- BTNSTATE_SELECTED
- }BUTTONSTATE;
- LRESULT CALLBACK BtnProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
- {
- HDC hdc;
- HDC hMemDc;
- PAINTSTRUCT ps;
- static int id;
- 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 BITMAP bm;
- 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;
- }
- switch (message)
- {
- case WM_CREATE:
- //注意這個消息不會進入
- return (0);
- case WM_PAINT:
- hdc = BeginPaint (hwnd, &ps);
- 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);
- EndPaint (hwnd, &ps);
- return (0);
- case WM_SETFOCUS:
- if (id==IDOK)
- {
- btnOkState = BTNSTATE_FOCUS;
- }
- else
- {
- btnCancelState = BTNSTATE_FOCUS;
- }
- return (0);
- case WM_KILLFOCUS:
- if (id==IDOK)
- {
- btnOkState = BTNSTATE_DEFAULT;
- }
- else
- {
- btnCancelState = BTNSTATE_DEFAULT;
- }
- return (0);
- case WM_KEYDOWN:
- if (wParam == VK_SPACE)
- {
- if (id==IDOK)
- {
- btnOkState = BTNSTATE_SELECTED;
- }
- else
- {
- btnCancelState = BTNSTATE_SELECTED;
- }
- InvalidateRect(hwnd, NULL, TRUE);
- }
- return (0);
- case WM_KEYUP:
- if (wParam == VK_SPACE)
- {
- if (id==IDOK)
- {
- btnOkState = BTNSTATE_FOCUS;
- }
- else
- {
- btnCancelState = BTNSTATE_FOCUS;
- }
- InvalidateRect(hwnd, NULL, TRUE);
- }
- return (0);
- case WM_LBUTTONDOWN:
- SetCapture(hwnd);
- if (id==IDOK)
- {
- btnOkState = BTNSTATE_SELECTED;
- }
- else
- {
- btnCancelState = BTNSTATE_SELECTED;
- }
- InvalidateRect(hwnd, NULL, TRUE);
- return (0);
- case WM_LBUTTONUP:
- if (id==IDOK)
- {
- btnOkState = BTNSTATE_FOCUS;
- }
- else
- {
- btnCancelState = BTNSTATE_FOCUS;
- }
- InvalidateRect(hwnd, NULL, TRUE);
- ReleaseCapture();
- return (0);
- case WM_DESTROY:
- DestroyWindow(hwnd);
- return (0);
- }
- return CallWindowProc (id == IDOK ? btnOkOldProc : btnCancelOldProc, hwnd, message, wParam, lParam);
- }
按鈕原有的內建窗體過程函數的時候按鈕已經創建完成,所以如果我們在BtnProc的WM_CREATE設置斷點,程序是不會進入的。和WM_DRAWITEM一樣,我們需要按鈕的不同狀態時繪制,因為我們采用自己的BtnProc過程函數,所以我們只能自己來維護按鈕的狀態,在WM_PAINT函數中根據不同狀態繪制不同樣式的按鈕,在其他消息中處理按鈕的按下或彈起、獲得焦點、或失去焦點等狀態轉變。

http://blog.csdn.net/wenzhou1219/article/details/17652181