一、引言
要想熟練掌握Windows應用程序的開發,首先需要理解Windows平台下程序運行的內部機制,然而在.NET平台下,創建一個Windows桌面程序,只需要簡單地選擇Windows窗體應用程序就可以了,微軟幫我們做了非常好的封裝,以至於對於很多.NET開發人員至今也不清楚Windows 平台下程序運行的內部機制,所以本專題將深入剖析下Windows 程序的內部運行機制。
二、Windows平台下幾個基礎概念
有朋友會問,理解了程序運行的內部機制有什么用,因為在我們實際開發中用得微軟提供的模板來進行編程?對於這個疑問,我的回答是——理解了Windows平台下程序的運行內部機制可以使我們更有自信地寫代碼,因為我們知道模板后台幫我們封裝的內容,並且理解這點也是打好了基礎,基礎打好了,學習新的知識也就快了。
2.1 窗口與句柄
窗口是Windows應用程序中非常重要的一個元素,一個Windows應用程序至少要有一個窗口,稱為主窗口,窗口是我們看到的一塊矩形區域,它是與用戶進行交互的接口,利用窗口可以接受用戶的輸入以及對用戶輸入的響應。例如我們看到的QQ登陸界面就是一個窗口。窗口又可分為客戶區和非客戶區,如下圖所示,其中,客戶區通常用來顯示控件或文字,標題欄、系統菜單,菜單欄、最小化框和最大化框、可調邊框都稱為窗口的非客戶區,它們主要由Windows系統進行管理,我們創建的應用程序主要負責客戶區的外觀顯示和操作,窗口也可以有一個父窗口,並且,對話框和消息框都是屬於窗口。
在Windows應用程序中,句柄是用來唯一標識窗口的,我們想對某個窗體進行操作時,必須首先獲得該窗口的句柄,句柄還包括圖標句柄(如上圖中Form1前小圖標),光標句柄(即移到窗體時顯示的光標),和畫刷句柄(上圖中客戶區中顏色就是通過指定窗口類的背景畫刷句柄進行設置的,關於窗口類的結構會在下面介紹)。對於句柄的理解,大家簡單理解為用來標識窗體,它的類型為struct,這點可以通過在VS中通過F12查看HWND的定義。
2.2 消息與消息隊列
Windows 操作系統是基於事件驅動的一種操作系統,所以在Windows平台下所有應用程序也是基於事件驅動機制,即是基於消息的。例如,當用戶在窗口中按下鼠標左鍵時,操作系統會知曉這一事件,於是將事件封裝成一個消息,傳遞到應用程序的消息隊列中,,然后應用程序從消息隊列中取出消息並進行響應。在這個處理過程中,操作系統會調用應用程序中專門負責消息處理的函數,該函數稱為窗口過程。
1. 消息
消息是由MSG結構體表示的,MSG的結構體定義如下(也可以參考MSDN:http://msdn.microsoft.com/en-us/library/windows/desktop/ms644958(v=vs.85).aspx):
// MSG
typedef struct tagMSG { HWND hwnd; UINT message; WPARAM wParam; LPARAM lParam; DWORD time; POINT pt; } MSG, *PMSG, *LPMSG;
該結構體中各參數的含義如下:
- hwnd——表示消息所屬的窗口,我們通常開發的窗口應用程序中,一個消息都是與某個窗口相關聯的。
- message——指定消息的標識符,在Windows中,消息是由一個數值來表示的,不同消息對應於不同的數值,但是由於數值不便於記憶,所以Windows將消息對應的數值定義為宏,為了使開發人員明白定義的宏為一個消息時,把消息宏都定義以WM(Windows Message的縮寫)為前綴。例如 WM_CHAR表示字符消息,WM_LBUTTONDOWN表示鼠標左鍵按下消息。要想知道每個宏對應的數組,可以在VS中用F12 來查看宏對應的數值。
- wParam和lParam——用於指定消息的附加信息。關於每個消息的附件信息可以參考MSDN中相關信息的說明文檔,這里列出WM_CHAR消息的說明文檔:http://msdn.microsoft.com/en-us/library/windows/desktop/ms646276(v=vs.85).aspx
- time——表示消息被傳送到消息隊列的時間
- pt——表示當消息被傳送時光標在屏幕中的位置
2. 消息隊列
每一個Windows應用程序開始執行后,系統都會為該程序創建一個消息隊列(從而得出消息隊列是由系統創建的)來存放該程序創建過程中的窗口消息。當用戶在窗口中發送一個消息時,系統會將該消息推送到消息隊列中,而應用程序的過程函數則通過一個消息循環不斷地從消息隊列中取出消息,並進行響應。這種消息機制,就是Windows程序運行的機制。
在Windows程序中,消息又可分為“進隊消息”和“不進隊消息”。進隊的消息由系統放入到應用程序的消息隊列中,然后由應用程序取出並發送給窗口過程處理。不進隊的消息由系統直接調用窗口過程進行處理。
三、動手實現第一個Windows桌面程序
下面,讓我們手動要完成一個完整的Win32桌面程序,該程序實現的功能就是簡單地創建一個窗體,並在窗體中響應鍵盤及鼠標消息,實現該程序的步驟可分為:
- WinMain函數的定義
- 創建一個窗口
- 進行消息循環,從消息隊列中獲得一個消息
- 編寫窗口過程函數,用於處理消息。
3.1 WinMain函數的定義
當Windows啟動一個桌面程序時,它調用的就是該程序的WinMain函數,該函數是Windows桌面程序的入口函數,與控制台中的main函數的作用相同。WinMain函數的聲明如下所示(也可以參考MSDN:http://msdn.microsoft.com/en-us/library/windows/desktop/ms633559(v=vs.85).aspx):
int WINAPI WinMain( _In_ HINSTANCE hInstance, _In_ HINSTANCE hPrevInstance, _In_ LPSTR lpCmdLine, _In_ int nCmdShow );
上面我列出的定義與MSDN略有不同,MSDN中定義使用的是CALLBACK,而我上面列出的是WINAPI,其實兩者都是一樣的,可以在VS中通過F12查看宏的定義可以發現:
#define CALLBACK __stdcall #define WINAPI __stdcall
它們都是代表_stdcall,_stdcall是一種函數調用方式,__stdcall 調用約定用來調用 Win32 API 函數,更多介紹可以參考MSDN:http://msdn.microsoft.com/zh-cn/library/zxk0tw93(v=vs.120).aspx。
WinMain函數的4個參數是由系統調用WinMain函數時,傳遞給應用程序應用程序的,它們具體的含義為:
- hInstance——表示該程序當前運行實例的句柄,當程序在Windows平台下運行時,該值唯一標識着運行中的實例,這里需要朱注意:一個應用程序可以運行多個實例,每運行一個實例,系統都會為該實例分配一個句柄值,並通過hInstance參數傳遞給WinMain函數(從這句話可以得出,WinMain函數並不是程序調用的第一個函數,你在VS的調用堆棧中可以發現,在WinMain函數之前還有:WinMainCRTStartup()和_tmainCRTStartup()函數)。這里的句柄應該與窗口句柄區分開來,hInstance參數代表的是應用程序實例的句柄,而一個應用程序可以有多個窗口,每個窗口都對應一個句柄;
- hPrevInstance——表示當前實例的前一個實例的句柄。在Win32環境下,它的值總是為NULL;
- lpCmdLine——表示一個以空終止的字符串,指定傳遞給應用程序的命令行參數,例如:你雙擊一個Word文件,此時此時將該文件的路徑作為命令行參數傳遞給Word應用程序,安裝的Word應用程序得到該文件的路徑后,就在窗口中打開文件的內容。我們可以在VS中通過屬性——>調試——>命令參數來編輯想輸入的命令參數;
- nCmdShow——指定窗口應該如何顯示,如最大化、最小化等。如.NET中Form的WindowState屬性。
3.2 創建一個窗口
創建一個完整的窗口,需要經過下面4個步驟:
- 設計一個窗口類
- 注冊窗口類
- 創建窗口類
- 顯示及更新窗口。
上面四個步驟,仔細想想你也知道的,我們想創建一個窗口,首先應該設計下它長什么樣子吧(第一步),設計完成之后,總要讓系統知道已經設計完了窗口了吧,所以我們要通過注冊窗口類的方式來通知系統(第二步),成功注冊之后,系統已經知道存在這樣一個窗口了,接下來就應該創建窗口類的一個實例了(第三步),最后就是把創建完的窗口顯示和再加修飾下(第四步)。這四步完全來源我們生活,例如,上司找你做一個東西出來,你首先要在腦海中構想它的樣子(設計,第一步),設計完之后,要讓老板知道你設計完成了就應該告知老板你設計完了(第二步),老板知道之后,老板覺得可以就命令工廠把模型做出來(第三步),最后就是拿給客戶看(第四步)。下面我們按照這4步來完成一個窗口的創建。
3.2.1 設計一個窗口類
Windows已經為我們定義好了一個窗口類,它的具體定義如下,你也可以自我查看MSDN:http://msdn.microsoft.com/en-us/library/windows/desktop/ms633576(v=vs.85).aspx。
typedef struct tagWNDCLASS { UINT style; // 窗口的樣式,如CS_HREDRAW表示當窗口水平向上的寬度發生變化時,將重繪整個窗口 WNDPROC lpfnWndProc; // 一個函數指針,指向窗口過程函數,用於對事件進行響應,該函數是回調函數,可以對照委托回調進行理解 int cbClsExtra; // 類的附加內存,用於存儲類的附加信息,一般把該參數設置為0 int cbWndExtra; // 窗口的附加內存,一般也設置為0 HINSTANCE hInstance; // 包含窗口的應用程序實例句柄 HICON hIcon; // 窗口類的圖標句柄,如果設置為NULL,那么系統會提供一個默認的圖標 HCURSOR hCursor; // 窗口類的光標 HBRUSH hbrBackground;// 窗口類的背景畫刷句柄, LPCTSTR lpszMenuName; // 指定菜單資源的名字 LPCTSTR lpszClassName; // 指定窗口類的名字 } WNDCLASS, *PWNDCLASS;
在程序中,我們創建一個窗口類對象,然后為該對象指定其屬性來完成窗口類的設計。
3.2.2 注冊窗口類
設計完窗口類之后,我們需要使用RegisterClass(CONST WNDCLASS *lpWndClass)函數來完成窗口類的注冊,注冊成功之后,我們才可以創建該類型的窗口。
3.2.3 創建窗口
注冊窗口類之后,即已經告知系統,我們已經存在這樣的一個窗口類,下面可以使用CreateWindow()函數來創建該類型的一個窗口,CreateWindow函數的定義如下:
HWND WINAPI CreateWindow( _In_opt_ LPCTSTR lpClassName,// 注冊窗口類的名字 _In_opt_ LPCTSTR lpWindowName, // 窗口名,如果窗口樣式指定了標題欄,那么窗口名將顯示在標題欄上 _In_ DWORD dwStyle, // 指定創建窗口的樣式 _In_ int x,// 窗口左上角x坐標 _In_ int y, // 窗口左上角y坐標 _In_ int nWidth,// 窗口寬度 _In_ int nHeight,// 窗口高度 _In_opt_ HWND hWndParent,// 窗口的父窗口句柄 _In_opt_ HMENU hMenu,// 窗口菜單句柄 _In_opt_ HINSTANCE hInstance,// 包含窗口的應用程序實例 _In_opt_ LPVOID lpParam // 作為WM_CREATE消息的附加參數lParam傳入給窗口,WM_CREATE有兩個參數:wParam和lParam參數,更多內容參考MSDN:http://msdn.microsoft.com/en-us/library/ms632619(v=vs.85).aspx );
3.2.4 顯示及更新窗口
創建窗口后,最好一步需要做的就是將窗口展示給用戶看,我們可以通過ShowWindow()和UpdateWindow()函數來完成,ShowWindow()函數來設置窗口的特殊狀態,調用完ShowWindow()之后,接下來調用UpdateWindow()函數來刷新窗口。即把設置好的窗口繪制在桌面上。調用UpdateWindow()函數之后將發送一個WM_PAINT消息給窗口過程函數來進行處理,從而來刷新窗口,注意,該WM_PAINT消息是沒有放到前面介紹的消息隊列中,屬於不入隊的消息。
3.3 進行消息循環,從消息隊列中獲得一個消息
接下面,我們需要實現一個消息循環函數,來完成不斷從消息隊列中取出消息,並交給窗口過程函數進行處理。我們可以通過Windows API 中GetMessage()函數來完成這個過程,下面是該函數的原型:
BOOL WINAPI GetMessage(
_Out_ LPMSG lpMsg, // 輸出參數,指向消息的結構體指針
_In_opt_ HWND hWnd, // 指定接收從哪個窗口的消息
_In_ UINT wMsgFilterMin,// 獲取消息的最小值,通常設置為0
_In_ UINT wMsgFilterMax// 獲取消息的最大值。如果wMsgFilterMin和wMsgFilterMax都設置為0時,表示接收所有消息
);
GetMessage()函數除了接收WM_QUIT消息(接收WM_QUIT消息返回0)外,接收其他函數都返回非零值,如果出現錯誤則返回-1。如參數hWnd為無效句柄時(即傳遞NULL),此時發送錯誤返回為-1.
3.4 編寫窗口過程函數,用於處理消息
前面都涉及到對消息的處理,下面就完成最后一步——窗口過程函數的實現,該函數為回調函數,窗口過程函數的聲明如下,(也可以參考MSDNhttp://msdn.microsoft.com/en-us/library/windows/desktop/ms633573(v=vs.85).aspx):
LRESULT CALLBACK WindowProc(
_In_ HWND hwnd,// 處理消息的窗口句柄
_In_ UINT uMsg,// 消息代碼
_In_ WPARAM wParam,
_In_ LPARAM lParam // wParam和lParam是消息的附加參數
);
3.5 完整的實現代碼
有了上面的實現思路之后,那么實現該程序將再簡單不過了,同時,大家可以根據下面代碼來對比理解下上面介紹的理論,具體實現代碼如下(這里需要指明一點,如果不小心把回調函數的實現的名字輸入錯誤時,將出現如下圖所示的錯誤):
// 手動實現一個Windows 程序 #include <Windows.h> #include <stdio.h> // 定義窗口過程函數,這里可以設置為你想要的名字 LRESULT CALLBACK WinProc( HWND hwnd, // 窗口句柄 UINT uMsg,// 消息代碼 WPARAM wParam, // 第一個消息參數 LPARAM lParam // 第二個消息參數 ); int WINAPI WinMain( HINSTANCE hInstance, // 當前運行實例的句柄 HINSTANCE hPrevInstance, // 當前實例的前一個實例句柄 LPSTR lpCmdLine,// 指定傳遞給應用程序的命令行參數 int nCmdShow // 指定程序的窗口如何顯示,例如最大化、最小化等 ) { // 1. 設計一個窗口類 WNDCLASS wndclass; wndclass.cbClsExtra=0; wndclass.cbWndExtra=0; wndclass.hbrBackground=(HBRUSH)GetStockObject(GRAY_BRUSH);// 指定背景畫刷 wndclass.hCursor =LoadCursor(NULL,IDC_CROSS);// 指定窗口類的光標句柄 wndclass.hIcon=LoadIcon(NULL,IDI_ERROR); wndclass.hInstance=hInstance; wndclass.lpfnWndProc=WinProc; wndclass.lpszClassName=L"learninghard2013"; // 設置窗口類的名稱 wndclass.lpszMenuName=NULL;// 設計窗口類創建的窗口沒有默認的菜單 wndclass.style=CS_HREDRAW|CS_VREDRAW;// 設置窗口樣式為寬度和高度變化時,將重新繪制整個窗口 // 2. 注冊窗口類 RegisterClass(&wndclass); // 3. 創建窗口,定義一個變量來保存成功創建窗口后返回的句柄 HWND hwnd; hwnd =CreateWindow(L"learninghard2013",L"手動實現窗口應用程序",WS_OVERLAPPEDWINDOW,100,100,600,400,NULL,NULL,hInstance,NULL); // 4. 顯示和刷新窗口 ShowWindow(hwnd,SW_SHOWNORMAL); UpdateWindow(hwnd); // 定義消息結構體,開始消息循環 MSG msg; BOOL breturn; // GetMessage接受到WM_QUIT消息時返回為0,即為假 while((breturn=GetMessage(&msg,hwnd,0,0))!=0) { if(breturn==-1) { // 出錯時退出 return -1; } else { // 接受到消息不為WM_QUIT消息的情況 // 將虛擬鍵消息轉化為字符消息,字符消息被傳遞到調用線程的消息隊列中,當下一次調用GetMessage函數被取出 TranslateMessage(&msg); // 分發一個消息到窗口過程,由窗口過程函數對消息進行處理 DispatchMessage(&msg); } } return msg.wParam; } // 實現窗口過程函數 LRESULT CALLBACK WinProc(HWND hwnd, UINT uMsg, WPARAM wParam,LPARAM lParam) { switch(uMsg) { case WM_CHAR: WCHAR szChar[20]; swprintf(szChar,L"字符代碼是 %d",wParam); MessageBox(hwnd,szChar,L"字符",0); break; case WM_LBUTTONDOWN: MessageBox(hwnd,L"鼠標點擊",L"消息",0); swprintf(szChar,L"消息附加信息 %d",wParam); MessageBox(hwnd,szChar,L"消息",0); HDC hdc; hdc =GetDC(hwnd); TextOut(hdc,0,50,L"LearningHard實現",wcslen(L"LearningHard實現")); ReleaseDC(hwnd,hdc); break; case WM_PAINT: HDC hDC; PAINTSTRUCT ps; hDC =BeginPaint(hwnd,&ps); // BeiginPaint只能在WM_PAINT消息時調用 TextOut(hDC,0,0,L"http://www.cnblogs.com/zhili/",wcslen(L"http://www.cnblogs.com/zhili/")); EndPaint(hwnd,&ps); break; case WM_CLOSE: if(IDYES==MessageBox(hwnd,L"是否真的結束",L"消息窗口",MB_YESNO)) { DestroyWindow(hwnd); } break; case WM_DESTROY: PostQuitMessage(0); break; default: // 調用默認的窗口過程 return DefWindowProc(hwnd,uMsg,wParam,lParam); } return 0; }
輸入上面的代碼在VS中,按下Ctrl+F5按鈕運行程序,你將看到下面的窗口(你可以測試在窗口點擊的效果和鍵盤按下效果):
四、小結
本專題介紹了Windows程序運行的內部機制,了解了本專題的內容,相信對.NET中WinForm應用程序背后實現的原理將不再陌生了。這里再總結下純手動創建Windows 桌面程序的步驟:
- 查看MSDN查找WinMain的聲明並編寫應用程序中的WinMain函數
- 設計窗口類,查看WNDCLASS
- 注冊窗口類,設計RegisterClass()函數
- 創建窗口,設計CreateWindow()函數
- 顯示並更新窗口,設計ShowWindow()和UpdateWindow()函數
- 實現消息循環,從消息隊列中取出消息交給窗口過程函數處理,設計GetMessage()函數
- 實現窗口過程函數,查看WindowProc函數。
本專題所有源代碼下載:Windows程序運行的內部機制源碼