相信API大家應該不陌生了,不過用純API編寫Windows窗口的人可能很少了,因爲他看上去似乎太麻煩了,爲了開發的效率,所以有了IDE編程工具,如VC、C#、Delphi等,他們都大大的爲我們的窗口程序開發提高了效率,你可以在不知道這些窗口組件是怎麼創建的就能很好的使用它,這就是現代化編程帶來的好處,同時對新人來講也是有一定弊處的,因爲你總是依賴這些表面的東西,而原理性的問題卻不清楚,碰到一些小問題就可能止步不前了,就像一個沒有異常處理功能的程序一樣,一個小小的異常問題就能讓你的程序崩潰終止,這可就阻礙我們想成爲一個優秀程序員的夢想了。所以爲了成爲優秀的程序員,我們有必要將這些IDE背後的黑手一一抓出來。所謂擒賊先擒王嘛!嘿嘿!
這篇算第一節吧!介紹一個純 API 打造的 Windows 程序流程。
再介紹前我們先來簡單瞭解下Windows消息機制,因爲如果不瞭解Windows消息處理機制,我們就無法深入Windows編程。
Windows消息機制處理的三大部分:
1:系統消息反應堆,是Windows內核消息機制的源動力,它支配着所有消息的有順傳遞,對於每個正在執行的Windows應用程序,系統會爲其創建一個應用程序的消息隊列,用來存放該程序可能創建的各種窗口的消息。
2:應用程序消息泵,是通過應用程序中一塊稱之爲“消息循環”的代碼,從對應的應用程序消息隊列池中取出消息,並把它們分發到相應的窗口函數中處理。
3:窗口消息處理器,是負責處理由消息泵發來的各種關於此窗口對象操作的消息(即窗口消息處理的回調函數,或叫窗口消息響應代碼)。
如果把系統消息反應堆比作是地下水資源,那應用程序消息泵就是我們的抽水機了,而窗口消息處理器就好比我們需要灌溉的地,然後由這些地種出各種各樣的植物來。當然上面的稱謂是經我包裝過的,這也許會對其它同學造成一定的打擊!但爲了能更生動的描述這個抽象的問題,還望各位大大們多多包含。嘻嘻!:)。
當然系統消息反應堆故不受我們的直接控制,他由系統管理,這個我們不必操心,我們只需要用CreateWindow/CreateWindowEx函數創建一個窗口,系統會自動爲我們創建一個對應的應用程序消息隊列,供我們的程序使用。如果你創建的窗口不是Windows預先註冊的(預先註冊的包括Buttons、Static 、Edit、 List Boxes等),那麼你還需要用RegisterClass/RegisterClassEx函數向Windows操作系統先註冊你的窗口類型。如果註冊成功,就會返回這個窗口類型的標識號,然後你就可以用標識號進行創建窗口,查找窗口和註銷窗口類型等等。如下代碼所示:
HINSTANCE hInstance=GetModuleHandleA(NULL);
RegisterMyClass(hInstance);
ATOM RegisterMyClass(HINSTANCE hInstance)
{
WNDCLASSEX winclass;
winclass.cbSize = sizeof(WNDCLASSEX); // 本結構的字節大小
winclass.style = CS_HREDRAW | CS_VREDRAW; // 窗口樣式
winclass.lpfnWndProc = WndProc; // 窗口處理消息的回調函數
winclass.cbClsExtra = 0; // 窗口類型的擴展
winclass.cbWndExtra = 0; // 窗口實例的擴展
winclass.hInstance = hInstance; // 窗口實例句柄
winclass.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_WINICO)); // 窗口圖標,如果爲NULL系統會默認一個給你
winclass.hCursor = LoadCursor(NULL, IDC_ARROW); // 窗口的光標,如果爲NULL系統會默認一個給你
winclass.hbrBackground = (HBRUSH)(COLOR_WINDOW+1); // 窗口背景顏色
winclass.lpszMenuName = NULL; // 窗口菜單名稱
winclass.lpszClassName = "FirstClass"; // 窗口類型的名稱
winclass.hIconSm = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_SMICO)); // 窗口小圖標,如果爲NULL系統會默認一個給你
return RegisterClassEx(&winclass); // 註冊前面描述窗口類型
}
注意hInstance是一個程序實例句柄(模塊句柄),他由GetModuleHandleA(NULL)取得當前應用程序的模塊句柄,它的作用一般用於在取模塊資源時提用(也就是說你要取的資源是基於哪裏的,可以這麼理解吧!)。註冊好之後就可以使用CreateWindow/CreateWindowEx函數實例化剛剛註冊的窗口類型了,當然如果失敗它則會返回的空值。如下
hWnd = CreateWindowEx(WS_EX_WINDOWEDGE|WS_EX_CLIENTEDGE, // 擴展的窗口類型
"FirstClass", // 已註冊的窗口類型名稱
"FirstWindows", // 窗口名稱
WS_OVERLAPPED|WS_MINIMIZEBOX|WS_MAXIMIZEBOX|WS_SYSMENU|WS_THICKFRAME|WS_CAPTION, //窗口樣式
100, // 窗口左上角X位置
100, // 窗口左上角Y位置
800, // 窗口的寬
600, // 窗口的高
NULL, // 窗口的父窗口句柄,即是否有父窗口
NULL, // 窗口的主菜單
hInstance, // 應用程序的實例句柄
NULL); // 傳送給窗口的自定義參數
正常情況下通過上面的操作就可以建立了一個窗口了,在調用CreateWindowEx成功時我們的WndProc窗口處理回調函數會馬上先後收到如下系統發來的消息。
msg=0x24 WM_GETMINMAXINFO // 將要改變窗口大小或位置
msg=0x81 WM_NCCREATE // 當窗口第一次被創建時,此消息在WM_CREATE消息發送前發送
msg=0x83 WM_NCCALCSIZE // 覈算窗口的客戶區域
msg=0x01 WM_CREATE // 應用程序創建一個窗口
應用程序消息泵則是我們窗口程序中用來提取消息隊列的一段消息循環代碼,他的作用就是取出系統投遞給我們的應用程序消息,以便我們的窗口程序作出適當的響應。他一般由如下代碼形式組成。
while(GetMessage (&msg, NULL, 0, 0)) // 或者是 PeekMessage
{
TranslateMessage (&msg) ; // 進行一些鍵盤轉換
DispatchMessage (&msg) ; // 將消息傳遞給相應的窗口消息處理函數
}
這就是一個典型的“消息循環”代碼(即應用程序消息泵),程序的消息由GetMessage或者PeekMessage函數從消息隊列中取得,然後通過DispatchMessage函數將消息分發到相應窗口對象的處理函數中處理(實際上DispatchMessage函數是將msg回傳給Windows,然後再由Windows將該消息發送給適當的窗口消息處理函數進行處理,即窗口消息處理器中處理。)。
這裏GetMessage函數的第一個參數指定要接收消息的MSG結構的地址,第二個參數表示窗口句柄,一般將其設置爲空,表示要獲取該應用程序創建的所有窗口的消息;第三、四參數用於指定消息範圍。後面三個參數被設置爲默認值,用於接收發送到屬於這個應用程序的任何一個窗口的所有消息。在接收到除WM_QUIT之外的任何一個消息後,GetMessage()返回TRUE;如果GetMessage收到一個WM_QUIT消息,則返回FALSE以退出消息循環,終止程序運行。因此,在接收到WM_QUIT之前,帶有GetMessage()的消息循環可以一直循環下去。另外PeekMessage函數只比GetMessage一個參數,它是用來指示取出消息時是否刪除應該消息。兩者最大的區別就是GetMessage是以阻塞方式運行,而PeekMessage是以非阻塞方式運行,後者在現代的編程用得最多,VC、DELPHI等都用這個,早期認爲PeekMessage比較浪費CPU資源,不過後面證明似乎沒什麼太大的區別,嘿嘿!。
而TranslateMessage函數只是用來把虛擬鍵消息轉換爲字符消息,其並不會修改原有的消息,當我們敲擊鍵盤上的某個字符鍵時,系統將產生WM_KEYDOWN和WM_KEYUP消息。這兩個消息的附加參數(wParam和lParam)包含的是虛擬鍵代碼和掃描碼等信息,而不我們想要ASCII碼字符,通過調用TranslateMessage這個函數後就可以將WM_KEYDOWN和WM_ KEYUP消息的組合轉換爲一條WM_CHAR消息(即將包含了ASCII碼的字符附加轉化後的消息的wParam參數上),然後它會將轉換後的新消息投遞到調用線程的消息隊列中(也就是我們的消息泵線程),最後在下一次調用GetMessage或PeekMessage函數時從該應用程序的消息隊列中讀出。
這裏值得注意的是前面用CreateWindowEx創建窗口時WndProc會收到窗口創建時的消息,而此消息並不是由應用程序消息泵發來的,而是CreateWindowEx告訴系統我將要創建窗口時,由系統消息反應堆直接發來的,並在窗口創建成功的第一時間用消息通知我們窗口已經創建完畢。
窗口消息處理器,也可以叫窗口消息處理回調函數,他是在CreateWindowEx創建窗口前RegisterClassEx註冊窗口類時就指定的消息處理回調函數,如前面的WndProc函數。典形的WndProc函數如下
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_DESTROY: // 窗口被銷燬時
MessageBox(hWnd, "程序將要退出了喲!", "提醒!", MB_OK);
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam); // 將不處理的消息傳給系統處理
}
return 0;
}
當窗口顯示之後,它就會源源不斷地接收到需要處理的用程序消息。如果WndProc函數不處理這個消息,就可以把它轉向DefWindowProc函數來處理,這時系統會用窗口默認消息處理函數。當你按下菜單,或者點擊窗口時,窗口需要運行這個消息處理函數,否則你可能就看到不窗口響應操作的效果了。
備註:此外在CreateWindowEx成功後,也可以用SetWindowLong函數爲窗口增加新的消息處理回調函數。消息處理回調函數的觸發順序總是後來先得(即新掛增的消息處理回調函數會第一個拿到消息處理的控制權。)。等待處理完後再將消息由DefWindowProc函數傳給那個窗口的下一個消息處理回調函數中處理(或者處理後就直接返回,終止消息再往下傳,也就是說排在它後面的其它處理回調函數就收不到此消息了。嘻嘻!很壞吧!)。
這裏介紹的是一個WINDOWS窗口的最基本框架,創建後只有一個白白的窗口,還有一個能響應退出響應的處理。
API創建窗口流程:
1:RegisterMyClass // 註冊窗口類
2:CreateWindowEx // 創建已經註冊的窗口(必須)
3:GetMessage/PeekMessage // 消息循環-->獲取消息(必須)
4:TranslateMessage // 消息循環-->轉換虛擬鍵消息(當你的程序不需要這些東西時,可略!)
5:DispatchMessage // 消息循環-->將消息傳遞給相應的窗口消息處理函數(必須)
6:DefWindowProc // 消息處理回調函數體內-->>將不處理的消息傳給系統處理(必須)