Programming windows 5th Edition Chapter 3 視窗和訊息
1. 本章講述了一個最簡單的帶視窗的windows程式HelloWin。
2. 視窗類別別。在建立視窗(調用CreateWindow)之前,需要先註冊視窗類別別(RegisterClass)。所謂視窗類別別表示的是視窗大體應該遵循 的共性,比如按鈕視窗,他們的類別是一樣的,比如游標,背景,最關鍵的是訊息處理函數,這些東西都應該是一樣的,至於每個按鈕視窗不同的地方,比如大小, 位置等,放在CreateWindow中由我們來定義。所以視窗類別別就是抽象了視窗的一些共性的東西,多個視窗在CreateWindow的時候,可以共 享一個視窗類別別,也就是只需執行一次RegisterClass。視窗類別別中最重要的就是定義了訊息CALLBACK函數了,也就是訊息處理函數了。看到 這裡有人會擔心了,既然視窗類別別中定義了訊息處理函數,那麼,如果一堆視窗建立的時候都是用的一個視窗類別別的話,那這些視窗的訊息處理函數就都是一樣的 嘍?--沒錯,OK,那既然一樣,我怎麼對這些視窗的同一個訊息做不同的處理呢?--很簡單,在訊息處理函數中,有一個參數hWnd,用來標識了視窗的句 柄(也就是CreateWindow返回的東西),用這個就可以區分不同的視窗了。
3. 來看本章HelloWin的代碼:
-
Code: Select all
-
/*------------------------------------------------------------
HELLOWIN.C -- Displays "Hello, Windows!" in client area
Eric Zhang 2007
------------------------------------------------------------*/#include <windows.h>
LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
PSTR szCmdLine, int iCmdShow)
{
static TCHAR szAppName[] = TEXT ("HelloWin") ;
HWND hwnd ;
MSG msg ;
WNDCLASS wndclass ;
wndclass.style = CS_HREDRAW | CS_VREDRAW ;
wndclass.lpfnWndProc = WndProc ;
wndclass.cbClsExtra = 0 ;
wndclass.cbWndExtra = 0 ;
wndclass.hInstance = hInstance ;
wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ;
wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ;
wndclass.hbrBackground = (HBRUSH) GetStockObject (BLACK_BRUSH) ;
wndclass.lpszMenuName = NULL ;
wndclass.lpszClassName = szAppName ;
if (!RegisterClass (&wndclass))
{
MessageBox (NULL, TEXT ("This program requires Windows NT!"),
szAppName, MB_ICONERROR) ;
return 0 ;
}
hwnd = CreateWindow (szAppName, // window class name
TEXT ("The Hello Program"), // window caption
WS_OVERLAPPEDWINDOW, // window style
CW_USEDEFAULT, // initial x position
CW_USEDEFAULT, // initial y position
CW_USEDEFAULT, // initial x size
CW_USEDEFAULT, // initial y size
NULL, // parent window handle
NULL, // window menu handle
hInstance, // program instance handle
NULL) ; // creation parameters
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 ;
RECT rect ;
switch (message)
{
case WM_CREATE:
PlaySound (TEXT("hellowin.wav"), NULL, SND_FILENAME | SND_ASYNC) ;
return 0 ;
case WM_PAINT:
hdc = BeginPaint (hwnd, &ps) ;
GetClientRect (hwnd, &rect) ;
DrawText (hdc, TEXT ("Hello, Windows!"), -1, &rect,
DT_SINGLELINE | DT_CENTER | DT_VCENTER) ;
EndPaint (hwnd, &ps) ;
return 0 ;
case WM_DESTROY:
PostQuitMessage (0) ;
return 0 ;
}
return DefWindowProc (hwnd, message, wParam, lParam) ;
}
4. HelloWin很簡單,就是建立了一個視窗,在中央位置寫上文字,另外在視窗顯示的時候會播放一個wav檔案。註:為了播放這個wav檔案,代碼中調用了PlaySound函數,要使用這個函數在Project中記得將WinMM.lib檔案連結進去。
5. 現在來看代碼。首先代碼中有很多常量,比如CS_HREDRAW, WM_PAINT等,這些常量不用記憶,需要的時候查手冊即可,這些常量都有特定的意義和一些約定俗成的書寫格式:
附件1
除此以外,代碼中還有一些其他的資料類型,如下:
附件2 附件3
附 件3中列的都是控制代碼,只是指向不同東西的控制代碼而已。在windows中,控制代碼使用非常普遍,就好像是Linux編程中的檔案描述符一樣。windows中 的控制代碼就是一個32位的整數,用來代表一個對象而已。很多windows函數都需要控制代碼,這樣windows函數才知道我們操作的對象是誰。
6. 匈牙利標記法。代碼中的變數名稱基本上都使用了匈牙利標記法,其實第一章中的程式開始就已經開始使用了,這裡做了一個總結,以後寫代碼可以參考:
附件4
7. 現在來看代碼。首先定義了一個函數原型,也就是我們的訊息處理函數的原型。LRESULT就是long型;CALLBACK和WINAPI一樣,就是 __stdcall,加上CALLBACK,windows將來調用我們的訊息處理函數的時候,免得發生call convention不一致的問題,所以一般都要加這個CALLBACK;WndProc的四個參數中,第一個參數是視窗控制代碼,第二個參數是訊息代碼,第 三個參數和第四個參數是兩個parameter,wParam在32位windows下,就是UINT -- unsigned int,lParam就是long型,在16位windows下,wParam原來是WORD -- unsigned short類型。所以,按照匈牙利標記法,wParam應該寫成uiParam,但由於原來16位windows下是wParam,所以就沒有修改保留了 下來。
8. 然後的代碼就是註冊視窗類別別了。註冊視窗類別別調用RegisterClass,這個函數只需要一個參數,就是視窗類別別structure WNDCLASS。由於這個WNDCLASS中含有兩個字串成員變數,所以回想起第二章的內容,自然這個WNDCLASS就有WNDCLASSA和 WNDCLASSW兩個版本了。而這個WNDCLASS中的字串自然就是 T 類型的字串了。OK,下面來看這個Structure中的內容:
style -- 指出視窗的風格。代碼中寫的是CS_HREDRAW|CS_VREDRAW,這表示視窗在橫向或縱向發生變化的時候,重繪視窗。後面會看到,這就是為什麼 視窗大小改變後,文字依然會顯示在視窗的正中,就是因為這裡的設定,視窗重繪,觸發WM_PAINT訊息。在WINUSER.H中可以找到全部的CS打頭 的(表示視窗類別別樣式)的常量。注意觀察這些常量的數值,其實他們每個數值都將位元中的某一位置成了1,所以,他們之間可以互相用 或 符號串連起來。
lpfnWndProc -- 這是最重要的了,指定訊息處理函數,這是一個函數指標。
cbClsExtra, cbWndExtra -- 預留的空間,如果需要,程式可以自訂這部分內容。
hInstance -- 就是我們這個程式的執行個體控制代碼,WinMain的第一個參數
hIcon -- 指定視窗表徵圖,該表徵圖出現在視窗標題列的最左邊和工作列中
hCursor -- 指定滑鼠的游標。這裡,LoadIcon和LoadCursor,如果第一個參數是NULL,就表示使用windows預定義好的那些表徵圖和滑鼠游標。具 體看這兩個函數的MSDN;如果我們要使用自己的表徵圖或游標,那麼,第一個參數要設定成hInstance,也就是我們這個程式的執行個體控制代碼。
hbrBackground -- 視窗的背景。hbr表示handle to a brush。windows中的brush表示用來填充一個地區的著色樣式。Windows有幾個標準的brush,他們稱為Stock Brush(庫存畫刷)。所以我們用GetStockObject得到了一個白色畫刷。
lpszMenuName -- 指定應用程式菜單
lpszClassName -- 視窗類別別名稱。這裡的內容要和CreateWindow中的第一個參數一樣,如果我們建立的這個window想使用這個視窗類別別的話。
9. OK,一切具備,調用RegisterClass了,這裡代碼做了一個出錯處理判斷,這是應該的,因為RegisterClass也有A/W兩個版本,如 果我們定義了UNICODE條件編譯變數,那麼將來被調用的將是RegisterClassW,而win98雖然有這個函數,但是一調這個函數win98 會立刻返回錯誤(win9x只有很小一部分支援Unicode,比如MessageBoxW是可以用的)。
BTW: 對於出錯的處理,很多時候我們會調用GetLastError函數,這個函數能返回上次操作失敗的錯誤碼,這些錯誤碼對應的含義可以在MSDN的 /Platform SDK/Windows Base Services/Debugging and Error Handling/Error Codes/System Errors - Numerical Order中找到
10. 然後就看到CreateWindow函數了。代碼中有注釋,就不一項一項說了。只有第二個參數,CW_OVERLAPPEDWINDOW,這個常量是一批常量的合集(定義在WINUSER.H中):
-
Code: Select all
-
#define WS_OVERLAPPEDWINDOW (WS_OVERLAPPED | \
WS_CAPTION | \
WS_SYSMENU | \
WS_THICKFRAME | \
WS_MINIMIZEBOX | \
WS_MAXIMIZEBOX)
用來表示視窗的表現形態。
11. 然後代碼調用了ShowWindow,用來顯示視窗,然後又調了UpdateWindow,這個函數會觸發WM_PAINT訊息,用來初始化視窗中的圖形和文字。
其 實我自己試了一下,發現不調用UpdateWindow也照樣有WM_PAINT訊息的產生,視窗也照樣能正常顯示。原來我以為這裡調用 UpdateWindow只是為了規範,可是後來通過下一章的學習,發現這裡調用UpdateWindow是有道理的。道理就在於 UpdateWindow是產生WM_PAINT訊息,但是不同的是,這次產生的WM_PAINT訊息是非入隊訊息,也就是說,調用這個函數 後,windows會立馬調用我們的訊息處理函數,直到這個訊息處理完畢,UpdateWindow才返回。如果不使用這個函數,一般情況下產生的 WM_PAINT訊息只會排在訊息佇列中,而且windows約定WM_PAINT訊息是一個優先順序低的訊息,所以,這樣會導致介面更新不及時--簡言 之,當我們需要視窗介面立即更新的時候,請使用UpdateWindow,在很多地方都可以使用。
12. 然後就進入訊息迴圈了,用GetMessage從訊息佇列中取出訊息,注意:windows中不是什麼訊息都會進訊息佇列的,有些訊息是不進訊息佇列的, 此時windows會直接調用訊息處理函數,這些訊息叫非入隊訊息。不過我們不需要關心這些複雜的問題,我們只需要知道--任何訊息都會在我們的訊息處理 函數中被處理。GetMessage中MSG的結構是這樣的:
-
Code: Select all
-
typedef struct tagMSG
{
HWND hwnd ;
UINT message ;
WPARAM wParam ;
LPARAM lParam ;
DWORD time ;
POINT pt ;
} MSG, * PMSG ;
GetMessage函數的參數含義可以看MSDN,第二個參數為NULL表示接受本進程建立的所有視窗的訊息;第三個參數和第四個參數表示過濾 訊息的min和max值,也就是說,訊息編號在這個區間內的才會被GetMessage抓下來,如果這兩個值都是0,那就是沒有訊息過濾。 GetMessage函數會一直返回非0的數,除非取到了WM_QUIT訊息。
MSG結構中,hwnd是訊息發生視窗控制代碼,message是訊息編號,wParam,lParam是訊息參數,可以在這裡取到訊息的詳細內容,time是發生時間,PT是發生該訊息的時候滑鼠所在的座標。
13. TranslateMessage是把msg傳給windows,進行一些鍵盤轉換。這一點會在第六章深入討論;DispatchMessage是把 msg傳回給windows,然後windows就會調用我們的訊息處理函數(WndProc),所以對於入隊訊息,是由我們程式手動 GetMessage,然後再派發的,非入隊訊息不通過GetMessage,直接由windows調用訊息處理函數。
14. 對於訊息迴圈和訊息處理。有幾點需要重視:
1. DispatchMessage將訊息派發出去後,會一直等到這個訊息被處理完(訊息處理函數返回了),本函數才會返回,GetMessage才會被執行去取下一條訊息
2. 在訊息處理函數中,也是一樣,如果在訊息處理過程中觸發了其他的訊息,那麼觸發訊息的代碼也會block,一直到被觸發出來的新訊息被處理完為止。
3. 訊息處理函數必須return 0,除非是return DefWindowProc。
15. 下面的代碼就是訊息處理函數了,有這麼一些要點:
WM_CREATE訊息會在CreateWindow執行過程中產生
對於WM_PAINT訊息的處理,基本上都是從BeginPaint開始,以EndPaint結束。因為BeginPaint會返回一個hdc, 有了這個hdc,我們才能在視窗上寫字,繪圖。而且BeginPaint會填充一個PAINTSTRUCTURE,能告訴我們哪些地方需要重繪 (invalid)。在BeginPaint中,如果顯示地區背景沒有被刪除,則由windows來刪除,windows根據註冊視窗類別別中的背景來擦除 顯示地區。如果我們的訊息處理函數不處理WM_PAINT,那麼DefWindowProc只是簡單的調用BeginPaint和EndPaint來使顯 示地區重新生效。
WM_DESTROY訊息相應中我們調用了PostQuitMessage函數,該函數 會發出WM_QUIT訊息,GetMessage碰到這個訊息會返回0,從而WinMain中的訊息迴圈結束。參數0會被設定到該訊息的wParam,所 以我們在WinMain中return了msg.wParam
有時候,DefWindowProc處理完訊息後會產生其它的訊息。例如,假設使用者執行HELLOWIN,並且使用者最終單擊了 Close按鈕,或者假設用鍵盤或滑鼠從系統功能表中選擇了 Close, DefWindowProc處理這一鍵盤或者滑鼠輸入,在檢測到使用者選擇了Close選項之後,它給視窗訊息處理常式發送一條 WM_SYSCOMMAND訊息。WndProc將這個訊息傳給DefWindowProc。DefWindowProc給視窗訊息處理常式發送一條 WM_CLOSE訊息來響應之。WndProc再次將它傳給DefWindowProc。DestroyWindow呼叫DestroyWindow來響 應這條WM_CLOSE訊息。DestroyWindow導致Windows給視窗訊息處理常式發送一條WM_DESTROY訊息。WndProc再呼叫 PostQuitMessage,將一條WM_QUIT訊息放入訊息佇列中,以此來響應此訊息。這個訊息導致WinMain中的訊息迴圈終止,然後程式結 束。