分析窗口程序
了解了消息驅動體系的工作流程以后,讓我們來分析如何用Win32匯編實現這一切。
模塊和句柄
模塊的概念
一個模塊代表的是一個運行中的exe文件或dll文件,用來代表這個文件中所有的代碼和資源,磁盤上的文件不是模塊,裝入內存后運行時就叫做模塊。
一個應用程序調用其他DLL中的API時,這些DLL文件被裝入內存,就產生了不同的模塊,為了區分地址空間中的不同模塊,每個模塊都有一個唯一的模塊句柄來標識。
很多API函數中都要用到程序的模塊句柄,以便利用程序中的各種資源,所以在程序中一開始就先取得模塊句柄并存放到一個全局變量中可以省去很多的麻煩,在Win32中,模塊句柄在數值上等于程序在內存中裝入的起始地址。
取模塊句柄使用的API函數是GetModuleHandle,它的使用方法是:
invoke GetModuleHandle, lpModuleName
lpModuleName參數是一個指向含有模塊名稱字符串的指針,可以用這個函數取得程序地址空間中各個模塊的句柄,例如,如果想得到User32.dll的句柄以便使用其中包含的圖標資源,那么可以如下使用:
szUserDll db ‘User32.dll’,0
invoke GetModuleHandle, addr szUserDll
.if eax
mov hUserDllHandle,eax
.endif
如果使用參數NULL,調用GetModuleHandle,那么得到的是調用者本模塊的句柄,如下所示:
invoke GetModuleHandl,NULL
mov hInstance,eax
可以注意到,把返回的句柄放到了hInstance變量里而并不是放在hModule中,為什么是hInstance呢?Instance是“實例”,它的概念來自于Win16,Win16中不同運行程序的地址空間并不是完全隔離的,一個可執行文件運行后形成“模塊”,多次加載同一個可執行文件時,這個“模塊”是公用的,為了區分多次加載的“拷貝”,就把每個“拷貝”叫做實例,每個實例均用不同的“實例句柄”(hInstance)值來標識它們。
但在Win32中,程序運行時是隔離的,每個實例都使用自己私有的4GB空間,都認為自己是唯一的,不存在一個模塊的多個實例的問題,實際上在Win32中,實例句柄就是模塊句柄,但很多API原型中用到模塊句柄的時候使用的名稱還是沿用hInstance,所以我們還是把變量名稱取為hInstance。
在C++語言的編程中,hInstance通過WinMain由系統傳入,WinMain的原型是:
WinMain(hInstance, hPrevInstance, lpszCmdParam, nCmdShow)
程序不用自己去獲得hInstance,但在Win32匯編中必須自己獲取,如果不了解hModule就是hInstance的話,就無法得知如何得到hInstance,因為并沒有一個類似于GetInstanceHandle之類的API函數。
句柄是什么
隨著分析的深入,句柄(handle)一詞也出現得頻繁了起來,“句柄”是什么呢?句柄只是一個數值而已,它的值對程序來說是沒有意義的,它只是Windows用來表示各種資源的編號而已,所以只有Windows才知道怎么使用它來引用各種資源。
舉例說明,屏幕上已經有10窗口,Windows把它們從1到10編號,應用程序又建立了一個窗口,現在Windows把它編號為11,然后把11當做窗口句柄返回給應用程序,應用程序并不知道11代表的是什么,但在操作窗口的時候,把11當做句柄傳給Windows,Windows自然可以根據這個數值查出是哪個窗口。當該窗口關閉的時候,11這個編號作廢。第二次運行的時候,如果屏幕上現有5個窗口,那么現在句柄可能就是6了,所以,應用程序并不用關心句柄的具體數值是多少。打個比方,可以把句柄當做是商場中寄放書包時營業員給的紙條,紙條上的標記用戶并不知道是什么意思,但把它交還給營業員的時候,她自然會找到正確的書包。
Windows中幾乎所有的東西都是用句柄來標識的,文件句柄、窗口句柄、線程句柄和模塊句柄等,同樣道理,不必關心它們的值究竟是多少,拿來用就是了!
創建窗口
在創建窗口之前,先要談到“類”。“類”的概念讀者都不陌生,主要是為了把一組物體的相同屬性歸納整理起來封裝在一起,以便重復使用,在“類”已定義的屬性基礎上加上其他個性化的屬性,就形成了各式各樣的個體。
Windows中創建窗口同樣使用這樣的層次結構。首先定義一個窗口類,然后在窗口類的基礎上添加其他的屬性建立窗口。不同一步到位的辦法是因為很多窗口的基本屬性和行為都是一樣的,如按鈕、文本輸入框和選擇框等,對這些東西Windows都預定義了對應的類,使用時直接使用對應的類名建立窗口就可以了。只有用戶自定義的窗口才需要先定義自己的類,再建立窗口。這樣可以節省資源。
注冊窗口類
建立窗口類的方法是在系統中注冊,注冊窗口類的API函數是RegisterClassEx,最后的Ex是擴展的意思,因為它是Win16中RegisterClass的擴展。一個窗口類定義了窗口的一些主要屬性,如:圖標、光標、背景色、菜單和負責處理該窗口所屬消息的函數。這些屬性并不是分成多個參數傳遞過去的,而是定義在一個WNDCLASSEX結構中,再把結構的地址當參數一次性傳遞給RegisterClassEx,WNDCLASSEX是WNDCLASS結構的擴展。
WNDCLASSEX的結構定義為:
WNDCLASSEX STRUCT
CbSize DWORD ? ;結構的字節數
Style DWORD ? ;類風格
LpfnWndProc DWORD ? ;窗口過程的地址
CbClsExtra DWORD ?
CbWndExtra DWORD ?
HInstance DWORD ? ;所屬的實例句柄
HIcon DWORD ? ;窗口圖標
HCursor DWORD ? ;窗口光標
HbrBackground DWORD ? ;背景色
LpszMenuName DWORD ? ;窗口菜單
LpszClassName DWORD ? ;類名字符串的地址
HIconSm DWORD ? ;上圖標
WNDCLASSEX ENDS
在Win32匯編源程序中,注冊窗口類的代碼如下:
local @stWndClass:WNDCLASSEX ;定義一個WNDCLASSEX結構
invoke RtlZeroMemory, addr @stWndClass, sizeof @stWndClass
invoke LoadCursor, 0, IDC_ARROW
mov @stWndClass.hCursor, eax
push hInstance
pop @stWndClass.hInstance
mov @stWndClass.cbSize, sizeof WNDCLASSEX
mov @stWndClass.style, CS_HREDRAW or CS_VREDRAW
mov @stWndClass.lpfnWndProc, offset _ProcWinMain
mov @stWndClass.hbrBackground, COLOR_WINDOW + 1
mov @stWndClass.lpszClassName, offset szClassName
invoke RegisterClassEx, addr @stWndClass
程序定義了一個WNDCLASSEX結構的變量@stWndClass,用RtlZeroMemory將它填為全零,再填寫結構的各個字段,這樣,沒有賦值的部分就保持為0,結構各字段的含義如下:
hIcon 圖標句柄,指定顯示在窗口標題欄左上角的圖標。Windows已經預定課外作業了一些圖標,同樣,程序也可以使用在資源文件中定義的圖標,這些圖標的句柄可以用LoadIcon函數獲得。例子程序沒有用到圖標,所以Windows給窗口顯示了一個默認的圖標。
hCursor 光標句柄,指定了鼠標在窗口中光標形狀。同樣,Windows也預定義了一些光標,可以用LoadCursor獲取它們的句柄,IDC_ARROW是Windows預定義的箭頭光標,如果想使用自定義的光標,也可以自己在資源文件中定義。
lpszMenuName 指定窗口上顯示的默認菜單,它指向一個字符串,描述資源文件中菜單的名稱,如果資源文件中菜單是用數值定義的,那么這里使用菜單資源的數值。窗口中的菜單也可以在建立窗口函數CreateWindowsEx的參數中指定。如果在兩個地方都沒有指定,那么建立的窗口上就沒有菜單。
hInstance 指定要注冊的窗口類屬于哪個模塊,模塊句柄在程序開始的地方已經用GetModuleHandle函數獲得。
cbSize 指定WNDCLASSEX結構的長度,用sizeof偽操作符來獲取。很多Win32 API參數中的結構都有cbSize字段,它主要是用來區分結構的版本,當以后新增了一個字段時,cbSize就相應增大,如果調用的時候cbSize還是老的長度,表示運行的是基于舊結構的程序,這樣可以防止使用無效的字段。
style 窗口風格。CS_HREDRAW和CS_VREDRAW表示窗口的寬度或高度改變時是否重畫窗口。比較重要的是CS_DBLCLKS風格,指定了它,Windows才會把在窗口中快速兩次單擊鼠標的行為翻譯為雙擊消息WM_LBUTTONDBLCLK發給窗口過程。
hbrBackground 窗口客戶區的背景色。前面的hbr表示它是一個刷子(Brush)的句柄,刷子一詞形象地表示了填充一個區域的著色模式。Windows預定義了一些刷子,如BLACK_BRUSH和WHITE_BRUSH等,可以用下列語句來得到它們的句柄:
invoke GetStockObject, WHITE_BRUSH
但在這里也可以使用顏色值,Windows已經預定義了一些顏色值,分別對應窗口各部分的顏色,如COLOR_BACKGROUND,COLOR_HIGHLIGHT,COLOR_MENU和COLOR_WINDOWS等,使用顏色值的時候,Windows規定必須在顏色值上加1,所以在程序中的指令是:
mov @stWndClass.hbrBackground, COLOR_WINDOWS + 1
lpszClassName指定程序員要建立的類命名,以便以后用這個名稱來引用它。這個字段是一個字符串指針,在程序里,它指向MyClass字符串中。
cbWndExtra和cbClsExtra分別是在Windows內部保存的窗口結構和類結構中給程序員預留的空間大小,用來存放自定義的數據,它們的單位是字節。不使用自定義數據的話,這兩個字段就是0。
lpfnWndProc 是最重要的參數,它指定了基于這個類建立的窗口的窗口過程地址。通過這個參數,Windows就知道了在DispatchMessage函數中把窗口消息發到哪里去,一個窗口過程可以為多個窗口服務,只要這些窗口是基于同一個窗口類建立的。Windows中不同應用程序中的按鈕和文本框的行為都是一樣的,就是因為它們是基于相同的Windows預定義類建立的,它們背后的窗口過程其實是同一段代碼。
結構中的style表示窗口的風格,Windows已經有一些預定義的值,它們是以CS_(Class Style的縮寫)開始的標識符,如下所示:
CS_VREDRAW 00000001H
CS_HREDRAW 00000002H
CS_KEYCVTWINDOWS 00000004H
CS_DBLCLKS 00000008H
CS_OWNDC 00000020H
CS_CLASSDC 00000040H
可以看到,這些預定義值實際上在使用不重復的數據位,所以可以組合起來使用,同時使用不同的預定義值并不會引起混淆。
注意:對于不同二進制位組合的計算,“加”和“或”的結果是一樣的,在FirstWindows程序中用CS_HREDRAW or CS_VREDRAW來代表兩個組合,若用CS_HREDRAW + CS_VREDRAW也并沒有什么不同,但強烈建議使用or,因為如果不小心指定了兩個同樣的風格時:CS_HREDRAW or CS_VREDRAW or CS_VREDRAW和原來的數值是一樣的,而CS_HREDRAW + CS_VREDRAW + CS_VREDRAW就不對了,因為1 or 1 = 1,而1 + 1就等于2了。
建立窗口
接下來的步驟是在已經注冊的窗口類的基礎上建立窗口,使用“類”的原因是定義窗口的“共性”,建立窗口時肯定還要指定窗口的很多“個性化”的參數,如WNDCLASSEX結構中沒有定義的外觀、標題、位置、大小和邊框類型等屬性,這些屬性是在建立窗口時才指定的。
和注冊窗口類時用一個結構傳遞所有參數不同,建立窗口時所有的屬性都是用單個參數的方式傳遞的,建立窗口的函數是CreateWindowEx(注意不要寫成CreateWindowsEx),同樣,它是Win16中的CreateWindow函數的擴展,主要表現在多了一個dwExStyle(擴展風格)參數,原因是Win32比Win16中多了很多種窗口風格,原來的一個風格參數已經不夠用了。
CreateWindowEx函數的使用方法是:
invoke CreateWindowEx, dwExStyle, lpClassName, lpWindowName, dwStyle, x, y, nWidth, nHeight, hWndParent, hMenu, hInstance, lpParam
12個參數很好理解:
lpClassName 建立窗口使用的類名字符串指針,在FirstWindow程序中指向MyClass字符串,表示使用MyClass類建立窗口,這正是我們自己注冊的類,這樣一來,這個窗口就有“MyClass”的所有屬性,并且消息將被發到“MyClass”中指定的窗口過程中去,當然,這里也可以是Window預定義的類名。
lpWindowName 指向表示窗口名稱的字符串,該名稱會顯示在標題欄上。如果該參數空白,則標題欄上什么都沒有。
hMenu 窗口上要出現的菜單的句柄。在注冊窗口類的時候也定義了一個菜單,那是窗口的默認菜單,意思是如果這里沒有定義菜單(用參數NULL)而注冊窗口類時定義了菜單,則使用窗口類中定義的菜單;如果這里指定了菜單句柄,則不管窗口類中有沒有定義都將使用這里定義的菜單;兩個地方都沒有定義菜單句柄,則窗口上沒有菜單。另外,當建立的窗口是子窗口時(dwStyle中指定了WS_CHILD),這個參數是另一個含義,這時hMenu參數指定的是子窗口的ID號,這樣可以節省一個參數的位置,因為反正子窗口不會有菜單。
hpParam 這是一個指針,指向一個欲傳給窗口的參數,這個參數在WM_CREATE消息中可以被獲取,一般情況下用不到這個字段。
hInstance 模塊句柄,和注冊窗口類時一樣,指定了窗口所屬的程序模塊。
hWndParent 窗口所屬的父窗口,對于普通窗口(相對于子窗口),這里的“父子”關系只是從屬關系,主要用來在父窗口銷毀時一同將其“子”窗口銷毀,并不會把窗口位置限制在父窗口的客戶區范圍內,但如果要建立的是真正的子窗口(dwStyle中指定了WS_CHILD的時候),這時窗口位置會被限制在父窗口的客戶區范圍內,同時窗口的坐標(x,y)也是以父窗口的左上角為基準的。
x,y 指定窗口左上角位置,單位是像素。默認時可指定為CW_USEDEFAULT,這樣Windows會自動為窗口指定最合適的位置,當建立子窗口時,位置是以父窗口的左上角為基準的,否則,以屏幕左上角為基準。
nWidth,hHeight 窗口的寬度和高度,也就是窗口的大小,同樣是以像素為單位的。默認時可指定為CW_USEDEFAULT,這樣Windows會自動為窗口指定最合適的大小。
窗口的兩個參數dwStyle和dwExStyle決定了窗口的外形和行為,dwStyle是從Win16開始就有的屬性,下表列出了一些常見的dwStyle定義,它們是一些以WS(Windows Style的縮寫)為開頭的預定義值。
預定義值
|
16進制值
|
含義
|
WS_OVERLAPPED
|
00000000h
|
普通的重疊式窗口
|
WS_POPUP
|
80000000h
|
彈出式窗口(沒有標題欄)
|
WS_CHILD
|
40000000h
|
子窗口
|
WS_MINIMIZE
|
20000000h
|
初始狀態是最小化的
|
WS_VISIBLE
|
10000000h
|
初始狀態是可見的
|
WS_DISABLED
|
08000000h
|
初始狀態是被禁止的
|
WS_MAXIMIZE
|
01000000h
|
初始狀態是最大化的
|
WS_BORDER
|
00800000h
|
單線條邊框
|
WS_DLGFRAME
|
00400000h
|
對話框類型的邊框
|
WS_VSCROLL
|
00200000h
|
帶垂直滾動條
|
WS_HSCROLL
|
00100000h
|
帶水平滾動條
|
WS_SYSMENU
|
00080000h
|
帶系統菜單(即帶標題欄左上角的圖標)
|
WS_THICKFRAME
|
00040000h
|
可以拖動調整大小的邊框
|
為了容易理解,Windows也為一些定義取了一些別名,同時,由于窗口的風格往往是幾種風格的組合,所以Windows也預定義了一些組合值,如下表所示:
預定義值
|
等效值
|
WS_CHILDWINDOW
|
WS_CHILD
|
WS_TILED
|
WS_OVERLAPPED
|
WS_ICONIC
|
WS_MINIMIZE
|
WS_SIZEBOX
|
WS_THICKFRAME
|
WS_OVERLAPPEDWINDOW
|
WS_OVERLAPPED or WS_CAPTION or WS_SYSMENU or WS_THICKFRAME or WS_MINIMIZEBOX or WS_MAXINIZEBOX
|
WS_TILEDWINDOW
|
WS_OVERLAPPEDWINDOW
|
WS_POPUPWINDOW
|
WS_POPUP or WS_BORDER or WS_SYSMENU
|
dwExStyle是Win32中擴展的,它們是一些以WS_EX_開頭的預定義值,主要定義了一些特殊的風格,如下表所示:
預定義值
|
16進制值
|
含義
|
WS_EX_POPMOST
|
00000008h
|
總在頂層的窗口
|
WS_EX_ACCEPTFILES
|
00000010h
|
允許窗口進行鼠標拖放操作
|
WS_EX_TOOLWINDOW
|
00000080h
|
工具窗口(很窄的標題欄)
|
WS_EX_WINDOWEDGE
|
00000100h
|
立體體的邊框
|
WS_EX_CLIENTEDGE
|
00000200h
|
客戶區立體邊框
|
WS_EX_OVERLAPPDWINDOW
|
|
WS_EX_WINDOWEDGE or WS_EX_CLIENTEDGE
|
WS_EX_PALETTEWINDOW
|
|
WS_EX_POPMOST
|
用預定義的組合值WS_EX_PALETTEWINDOW可以很方便地構成浮在其他窗口前面的工具欄。
建立窗口的相關代碼如下:
invoke CreateWindowEx, WS_EX_CLIENTEDGE, \
offset szClassName, offset szCaptionMain, \
WS_OVERLAPPPEDWINDOW, \
100, 100, 600, 400, \
NULL, NULL, hInstance, NULL
mov hWinMain,eax
invoke ShowWindow, hWinMain, SW_SHWONORMAL
invoke UpdateWindow, hWinMain
建立窗口以后,eax中傳回來的是窗口句柄,要把它保存起來,這時候,窗口雖已建立,但還沒有在屏幕上顯示出來,要用ShowWindow把它顯示出來,ShowWindow也可以用在另的地方,主要用來控制窗口的顯示狀態(顯示或隱藏),大小控制(最大化、最小化或原始大小)和是否激活(當前窗口還是背后的窗口),它用窗口句柄第一個參數,第二個參數則是顯示的方式。顯示方式有如下的預定義值:
預定義值
|
等效值
|
SW_HIDE
|
隱藏窗口,大小不變,激活狀態不變
|
SW_MAXIMIZE
|
最大化窗口,顯示狀態不變,激活狀態不變
|
SW_MINIMIZE
|
最小化窗口,顯示狀態不變,激活狀態不變
|
SW_RESTORE
|
從最大化或最小化恢復正常大小,顯示狀態不變,激活狀態不變
|
SW_SHOW
|
顯示并激活窗口,大小狀態不變
|
SW_SHOWMAXIMIZED
|
顯示并激活窗口,以最大化顯示
|
SW_SHOWMINIMIZED
|
顯示并激活窗口,以最小化顯示
|
SW_SHOWMINOACTIVE
|
顯示窗口并最小化,激活狀態不變
|
SW_SHOWNA
|
顯示窗口,大小狀態不變,激活狀態不變
|
SW_SHOWNOACTIVATE
|
顯示并從最大化或最小化恢復正常大小,激活狀態不變
|
SW_SHOWNORMAL
|
顯示并激活窗口,恢復正常大小(初始化時用這個參數)
|
窗口顯示以后,用UpdateWindow繪制客戶區,它實際上就是向窗口發送了一條WM_PAINT消息。到此為止,一個頂層窗口就正常建立并顯示了。
CreateWindowEx也可以用來建立子窗口,Windows中有很多預定義的子窗口類,如按鈕和文本框的類名分別是Button和Edit。要建立一個按鈕,只要把lpClassName指向Button字符串就可以了。例如:
.data
szButton db ‘button’,0
szButtonText db ‘&OK’,0
invoke CreateWindowEx, NULL, \
offset szButton, offset szButtonText, \
WS_CHILD or WS_VISIBLE, \
10, 10, 65, 22, \
hWnd, 1, hInstance, NULL
在FirstWindow的源程序中加入按鈕類的定義字符串“szButton”和按鈕文字字符串“szButtonText”,然后在窗口過程的WM_CREATE消息中加入建立按鈕的代碼,執行一下,窗口中就出現了一個按鈕。建立按鈕的時候,lpWindowName參數就是按鈕上的文字,風格則一定要指定WS_CHILD,建立的按鈕才會在我們的主窗口上,WS_VISIBLE也要同時指定,否則按鈕不會顯示出來,hMenu參數在這里用做表示子窗口ID,將它設置為1,在建立多個子窗口的時候,ID應該有所區別。
消息循環
消息循環的一般形式:
.while TRUE
invoke GetMessage, addr @stMsg, NULL, 0, 0
.break .if eax == 0
invoke TranslateMessage, addr @stMsg
invoke DispatchMessage, addr @stMsg
.endw
消息循環中的幾個函數要用到一個MSG結構,用來做消息傳遞:
MSG STRUCT
Hwnd DWORD ?
Message DWORD ?
WParam DWORD ?
LParam DWORD ?
Time DWORD ?
Pt POINT <>
MSG ENDS
各個字段的含義是:
hwnd 消息要發向的窗口句柄。
message 消息標識符,在頭文件中以WM_開頭的預定義值(意思為Windows Message)。
wParam 消息的參數之一。
lParam 消息的參數之二。
time 消息放入消息隊列的時間。
pt 這是一個POINT的數據結構,表示消息放入消息隊列時的鼠標坐標。
這個結構定義了消息的所有屬性,GetMessage函數就是從消息隊列中取出這樣一條消息來的:
invoke GetMessage, lpMsg, hWnd, wMsgPilterMin, wMsgFilterMax
函數的lpMsg指向一個MSG結構,函數會在這里返回取到的消息,hWnd參數指定要獲取哪個窗口的消息,例子中指定為NULL,表示獲取的是所有本程序所屬窗口的消息,wMsgFilterMin和wMsgFilterMax為0表示獲取所有編號的消息。
GetMessage函數從消息隊列里取得消息,填寫好MSG結構并返回,如果獲取的消息是WM_QUIT消息,那么eax中的返回值是0,否則eax返回非零值,所以用.break .if eax == 0來檢查返回值,如果消息隊列中有WM_QUIT則退出消息循環。
TranslateMessage將MSG結構傳給Windows進行一些鍵盤消息的轉換,當有鍵盤按下和放開時,Windows產生WM_KEYDOWN和WM_KEYUP或WM_SYSKEYDOWN和WM_SYSKEYUP消息,但這些消息的參數中包含的是按鍵的掃描碼,轉換成常用的ASCII碼要經過查表,很不方便,TranslateMessage遇到鍵盤消息則將掃描碼轉換成ASCII碼并在消息隊列中插入WM_CHAR或WM_SYSCHAR消息,參數就是轉換好的ASCII碼,如此一來,要處理鍵盤消息的話只要處理WM_CHAR消息就好了。遇到別的消息則TranslateMessage不做處理。
最后,由DispatchMessage將消息發送到窗口對應的窗口過程去處理。窗口過程返回后DispatchMessage函數才返回,然后開始新一輪消息循環。
其它形式的消息循環:
GetMessage函數是程序空閑的時候主動將控制權交還給Windows的一種方式,Windows是一個搶占式的多任務系統,任務之間每20ms切換一次,試想一下,如果窗口程序在主窗口中采用死循環等待,消息由Windows直接發送到窗口過程,那么程序會是下列這種樣子:
invoke CreateWindow,
invoke ShowWindow,
invoke UpdateWindow,
.while dwQuitFlag == 0 ;要退出時在窗口過程中設置dwQuitFlag
.endw
invoke ExitProcess,
但這樣一來,即使程序在空閑狀態,輪到自己的20ms時間片的時候,CPU時間就會全部消耗在.while循環中,使用GetMessage的時候,輪到應用程序時間片的時候,如果消息隊列里還沒有消息,那么程序還是停留在GetMessage內部,這時就可以由Windows當家作主沒收這20ms的時間片,如此保證了CPU資源的合理應用。
如果應用程序想把所有時間充分用回來,消息隊列里沒有消息的時候不讓GetMessage在Windows內部等待,拱手交出屬于自己的CPU時間,那么消息循環可以是下列這種樣子:
.while TRUE
invoke PeekMessage, addr @stMsg, NULL, 0, 0, PM)REMOVE
.if eax
.break .if @stMsg.message == WM_QUIT
invoke TranslateMessage, addr @stMsg
invoke DispatchMessage, addr @stMsg
.else
<做其他工作>
.endif
.endw
PeekMessage是一個類似于GetMessage的函數,區別在于當消息隊列里有消息的時候,PeekMessage取回消息,并在eax中返回非零值,沒有消息的時候它會直接返回,并在eax中返回零。所以在返回非零值的時候,程序檢查消息是否是WM_QUIT,是則結束消息循環,不是則用標準流程處理消息;返回零的時候,表示是空閑時間,程序就可以做其他工作了,但插入做其他工作的代碼執行時間不能過長,以不越過10ms為好,否則會影響正常的消息處理,使窗口的反應看起來很遲鈍。如果必須處理很長時間的工作,那么應該將它分成很多小部分處理,以便有足夠的頻率來用PeekMessage來檢查消息。PeekMessage的前面4個參數和GetMessage是相同的,增加了最后一個參數,PM_REMOVE表示取回消息的同時從消息隊列里刪除,否則用PM_NOREMOVE。
窗口過程
窗口過程是給Windows回調用的,它必須遵循規定的格式。對窗口過程的子程序名并沒有規定,對Windows來說,窗口過程的地址才是唯一需要的,例子程序中的子程序名是_ProcWinMain,讀者可以改用任何名稱。窗口過程子程序的參數格式為:
WindowProc proc hwnd, uMsg, wParam, lParam
第一個參數是窗口句柄,一個窗口過程可能為多個基于同一個窗口類的窗口服務,所以Windows回調的時候必須指出要操作的窗口,否則窗口過程不知道要去處理哪個窗口,FirstWindow程序只建立了一個窗口,所以每次傳遞過來的hwnd和用CreateWindowEx函數返回的窗口句柄是一樣的;
第二個參數是消息標識,后面兩個參數是消息的兩個參數。這4個參數和消息循環中MSG結構中的前4個字段是一樣的。
窗口過程的結構
窗口過程一般有如下結構:
WindowsProc proc uses ebx edi esi, hWnd, uMsg, wParam, lParam
mov eax,uMsg
.if eax == WM_XXX
<處理WM_XXX消息>
.elseif eax == WM_YYY
<處理WM_YYY消息>
.elseif eax == WM_CLOSE
invoke DestroyWindow, hWinMain
invoke PostQuitMessage, NULL
.else
invoke DefWindowProc, hWnd, uMsg, wParam, lParam
ret
.endif
mov eax,eax
ret
WindowProc endp
該過程主要是對uMsg參數中的消息編號構成一個分支結構,對于需要處理的消息分別處理。不感興趣的消息則交給DefWindowProc來處理。
要注意的是窗口過程中要注意保存ebx,edi,esi和ebp寄存器,高級語言中不用自己操心這一點,匯編中就要注意了,Windows內部將這4個寄存器當指針使用,如果返回時改變了它們的值,程序會馬上崩潰。proc后面的uses偽操作在子程序進入和退出時自動按插上push和pop寄存器指令,來保護這些寄存器的值。其實不僅是在窗口過程中是這樣,所有由應用程序提供給Windows的回調函數都必須遵循這個規定,如定時器回調函數等,所有Win32 API也遵循這個規定,所以調用API后,ebx,edi,esi和ebp寄存器的值總是不會被改變的,但ecx和edx的值就不一定了。
uMsg參數指定的消息有一定的范圍,Windows標準窗口中已經預定義的值在0~03ffh之間(1024個),用戶可以自定義一些消息,通過SendMessage等函數傳給窗口過程做自定義的處理工作,這時可以使用的值是從0400h開始的,WM_USER就定義為00000400h,當程序員定義多個用戶消息的時候,一般使用WM_USER+1,WM_USER+2,之類的定義方法。
wParam和lParam參數是消息所附帶的參數,它隨消息的不同而不同,對于不同的消息,它們的含義必須分別從手冊中查明:如WM_MOUSEMOVE消息中,wParam是標志,lParam是鼠標位置;而在WM_GETTEXT消息中,wParam是要獲取的字符數,lParam是緩沖地址;而對于WM_COPY消息來說,它不需要額外的信息,所以兩個參數都沒有定義。
處理了不同的消息,必須返回規定的值給Windows,返回值也需要分別從手冊中查明,比如處理WM_CREATE消息的時候,如果返回0表示成功;如果程序無法初始化,如申請內存失敗,那么可以返回-1,Windows就不會繼續窗口的創建過程。一些消息的返回值則沒有定義,但大部分的消息處理以后都以返回0表示成功,所以程序中把默認的返回語句放在最后,將eax清零后返回,如果在處理某個消息的時候需要返回不同的值,可以以分支中將eax賦值后直接用ret指令返回。對于DefWindowProc的返回值,我們不對它進行干涉,所以直接將eax不做修改地用ret返回。
WM_CLOSE消息是按下了窗口右上角的“關閉”按鈕后收到的,程序可以在這里處理和關閉窗口相關的事情,一般是相關資源的釋放工作,如釋放內存、保存工作和提示用戶是否保存工作等,如記事本程序在未保存的時候單擊“關閉”按鈕,會有提示框提示是否先保存文件,單擊“取消”按鈕的話,記事本不會關閉,這個步驟就是在WM_CLOSE消息處理中完成的。如果處理WM_CLOSE消息時直接返回,那么窗口不會關閉,因為這個消息只是Windows通知窗口用戶剛才單擊了“關閉”按鈕而已,窗口采用什么樣的行為是窗口的事。當窗口決定關閉的時候,需要程序自己調用DestroyWindow來摧毀窗口,并用PostQuitMessage向消息循環發送WM_QUIT消息來退出消息循環。調用PostQuitMessage時的參數是退出碼,就是GetMessage收到的WM_QUIT后MSG結構wParam字段中的東西,在這里使用NULL。
PostQuitMessage是初學者容易遺漏的函數,如果沒有這條語句,外觀上窗口是從屏幕上消失了,但主程序中的消息循環卻沒有收到WM_QUIT,結果還在那里打轉。常有人調試的時候丟了這條語句,結果再一次編譯的時候就收到錯誤:LINK fatal error LNK1104:cannot open file “xxx.exe”,表示exe文件現在不可寫。
Windows為什么不在窗口摧毀的時候自動發送一個WM_QUIT消息,而必須由用戶程序自己通過PostQuitMessage函數發送呢?其實很好理解:因為屏幕上可能不止一個窗口,Windows無法確定哪個窗口的關閉代表著程序結束。試想一下,用戶打開了一個輸入參數的小窗口,單擊“確定”按鈕后關閉并回到主窗口,Windows卻不分三七二十一自動發送了一個WM_QUIT,程序就會莫名其妙地退出了。
收到消息的順序
窗口過程收到消息是有一定順序的,收到第一條消息并不是從消息循環開始以后,而是在CreateWindowEx中就開始了,顯示和刷新窗口的函數ShowWindow和UpdateWindow也向窗口過程發送消息,這一點并不奇怪,因為Windows在CreateWindowEx前調用RegisterClassEx的時候就已經得到窗口過程的地址了。并且在建立窗口的過程中需要窗口過程的配合。
下面分別列出調用CreateWindowEx和ShowWindow的時候窗口過程收到的消息。
調用CreateWindowEx時窗口過程收到的消息
消息發生
|
說明
|
WM_GETMINMAXINFO
|
獲取窗口大小,以便初始化
|
WM_NCCREATE
|
非客戶區開始建立
|
WM_NCCALCSIZE
|
計算客戶區大小
|
WM_CREATE
|
窗口建立
|
調用ShowWindow時窗口過程收到的消息
消息發生
|
說明
|
WM_SHOWWINDOW
|
顯示窗口
|
WM_WINDOWPOSCHANGING
|
窗口位置準備改變
|
WM_ACTIVATEAPP
|
窗口準備激活
|
WM_NCACTIVATE
|
激活狀態改變
|
WM_GETTEXT
|
取窗口名稱(顯示標題欄用)
|
WM_ACTIVATE
|
窗口準備激活
|
WM_SETFOCUS
|
窗口獲得焦點
|
WM_NCPAINT
|
需要繪畫窗口邊框
|
WM_ERASEBKGND
|
需要擦除背景
|
WM_WINDOWPOSCHANGED
|
窗口益已經改變
|
WM_SIZE
|
窗口大小已經改變
|
WM_MOVE
|
窗口位置已經移動
|
然后程序執行UpdateWindow,這個函數向窗口過程發送一條WM_PAINT消息,接著,主程序開始進入消息循環,Windows根據各種因素給窗口過程發送相應的消息,一直到調用DestroyWindow為止。
調用DestroyWindow時窗口過程收到的消息
消息發生
|
說明
|
WM_NCACTIVATE
|
窗口激活狀態改變
|
WM_ACTIVATE
|
窗口準備非激活
|
WM_ACTIVATEAPP
|
窗口準備非激活
|
WM_KILLFOCUS
|
失去焦點
|
WM_DESTROY
|
窗口即將被摧毀
|
WM_NCDESTROY
|
窗口的非客戶區及所有子窗口已經被摧毀
|
在所有這些階段的消息中,大部分的消息都不需要程序自己關心,Windows只是盡義務通知窗口過程而已,窗口過程轉手就交給DefWindowProc去處理了。程序需要關心的消息有下面這些,可以根據需要選擇使用:
WM_CREATE 放置窗口初始化代碼,如建立各種子窗口(狀態欄和工具欄等)。
WM_SIZE 放置位置安排的代碼,因為建立的子窗口可能需要隨窗口大小的改變而移動位置。
WM_PAINT 如果需要自己繪制客戶區,則在這里安排代碼。
WM_CLOSE 向用戶確認是否退出,如果退出則摧毀窗口并發送WM_QUIT消息。
WM_DESTROY 窗口摧毀,在這里放置釋放資源等掃尾代碼。
在例子程序中,我們處理了WM_PAINT消息來繪制客戶區,功能就是在窗口中的中間寫上一行字:Win32 Assembly, Simple and powerful! 過程是先通過BeginPaint獲取窗口客戶區的“設備環境”句柄,然后通過GetClientRect獲取客戶區的大小,最后通過DrawText函數將字符串按照取得的屏幕大小居中寫到“設備環境”中,也就是窗口上。如果不需要顯示這個字符串,則連WM_PAINT消息也不用處理。
消息的默認處理:DefWindowProc
Windows預定義的消息范圍是0~03ffh,共1024個消息,查看一下頭文件Windows.inc,可以發現實際已定義的消息數目有幾百個,這些消息中的大部分對于窗口的運行來說都是必需的,如果窗口過程要處理每一種消息,那么窗口過程中的elseif語句就會綿延數千行,但是窗口的行為就是由處理這些消息的方法來表現的,不處理又不行,怎么辦呢?
實際上大部分窗口的行為都是差不多的,這意味著如果要窗口過程處理全部的消息,不同窗口的窗口過程代碼應該是大同上異的,那么可以用一個模塊來以默認的方式處理消息,Win32中的DefWindowProc函數實現的就是這個功能。
不要小看了這個DefWindowProc,正是它用默認的方式處理了幾百種消息,才使用戶能用區區百來行代碼寫出一個全功能的窗口。也正是所有的窗口都用DefWindowProc默認處理程序自己不處理的消息,才使它們的行為看上去大同小異,因為它們背后實際上是同一塊代碼在處理。
在窗口過程的分支語句中,用戶處理所有需要個性化處理的消息,對于表現行為是默認行為的消息,則在else分支中用DefWindowProc來處理,對于Windows來說,它并不關心消息在窗口過程中是程序用自己的代碼處理的還是用DefWindowProc處理的,它只看eax中的返回值來了解處理結果,所以不管消息是誰處理的,都必須在eax中返回正確的值。DefWindowProc返回時eax中就是它對消息的處理結果,程序只要直接把eax傳回給Windows就行了,所以在例子程序中,DefWindowProc后面直接用一句ret指令返回。
DefWindowProc中對一些消息的處理方法,如果和用戶期望的不同,就必須在窗口過程中自己處理。
DefWindowProc對一些消息的默認處理方式
消息
|
DefWindowProc的處理方式
|
WM_PAINT
|
發送WM_ERASEBKGND消息來擦除背景
|
WM_ERASEBKGND
|
用窗口類結構中的hbrBackground刷子來繪畫窗口背景
|
WM_CLOSE
|
調用DestroyWindow來摧毀窗口
|
WM_NCLBUTTONDBLCLK
|
這是非客戶區(如標題欄)鼠標雙擊消息,DefWindowProc測試鼠標的位置,然后再采取相應的措施,如標題欄雙擊將最大化和恢復窗口
|
WM_NCLBUTTONUP
|
這非客戶區鼠標標題釋放消息,同樣,DefWindowProc測試鼠標的位置然后再采取相應的措施,如鼠標在“關閉”按鈕的位置釋放將導致發送WM_CLOSE消息
|
WM_NCPAINT
|
非客戶區繪制消息,DefWindowProc將繪制邊框和客戶區
|
從這些默認的處理方法可以看出,想要一個窗口和別的窗口看起來不一樣,比如想要窗口看起來像蘋果機的窗口一樣,并且把關閉按鈕移到標題欄最左邊去,那么可以自己處理WM_NCPAINT消息,把非客戶區畫成蘋果機窗口的樣子,并把關閉按鈕畫到標題欄左邊去,并且自己處理WM_NCLBUTTONUP消息,當檢測到鼠標按下的位置在自己的關閉按鈕上的時候,則發送WM_CLOSE消息。對別的消息的處理思路也可以按這種方法類推。
另外,可以發現DefWindowProc對WM_CLOSE的默認處理是調用DestroyWindow摧毀窗口,DestroyWindow會引發一個WM_DESTROY消息,WM_CLOSE和WM_DESTROY的不同之處是:WM_CLOSE代表用戶有關閉的意向,窗口過程有權不“服從”,但收到WM_DESTROY的時候窗口已經在關閉過程中了,不管窗口過程愿不愿意,窗口的關閉已經是不可挽回的事了。
對于這兩個消息,窗口過程必須處理其中的一個,因為必須有個地方發送WM_QUIT消息來結束消息循環,例子程序中處理WM_CLOSE消息,在其中用DestroyWindow摧毀窗口,再調用PostQuitMessage結束消息循環;程序也可以不處理WM_CLOSE消息,讓DefWindowProc以默認處理的方式摧毀窗口,但這時候必須處理WM_DESTROY消息,在其中調用PostQuitMessage發送WM_QUIT以結束消息循環。