4.1 開始了解窗口
4.1.1 窗口是什么窗口是什么?
大家每天在使用Windows,屏幕上的一個個方塊就是一個個窗口!那么,窗口為什么是這個樣子呢?窗口就是程序嗎?
1.使用窗口的原因
回想一下DOS時代的計算機屏幕,在1990年Windows 3.0推出之前,計算機的屏幕一直使用文本模式,黑洞洞的底色上漂浮著白色的小字,性能不高的圖形模式只用于簡單的游戲和一些圖形軟件。對DOS程序來說,屏幕是唯一的,上面有個光標表示輸入字符的位置,程序運行后往屏幕輸出一些信息,退出時輸出的信息就留在了屏幕上,然后是第二個程序重復這個過程,當屏幕被寫滿的時候,整個屏幕上卷一行,最上面一行被去掉,然后程序在最底下新空出來的一行上繼續輸出。
對于一個單任務的操作系統來說,這種方式是很合理的,因為平時使用的傳真機或打字機就是用上卷的方式來容納新的內容的。但是如果是多任務呢?兩個程序同時往屏幕上輸出字符或者兩個人同時往打字機上打字,那么誰都看不懂混在一起的是什么。DOS下的TSR(內存駐留)程序是多個程序同時使用一個屏幕的例子,但實質上這并不是多任務,而是TSR將別的程序暫時掛起,掛起的程序不可能在TSR執行期間再向屏幕輸出內容,TSR在輸出自己的內容之前必須保存屏幕上顯示的內容,并在退出的時候把屏幕恢復為原來的樣子,否則掛起的程序并不知道屏幕已經被改變,在這個過程中,DOS不會去干預中間發生的一切。
Windows是多任務的操作系統,可以同時運行多個程序,同樣,各個程序在屏幕上的顯示不能互相干擾,而且,多個程序可以看成是“同時”運行的,在后臺的程序也可能隨時向屏幕輸出內容,這中間的調度是由Windows完成的。Windows采用的方法是給程序一塊矩形的屏幕空間,這就是窗口。應用程序通過Windows向屬于自己的窗口顯示信息,Windows判斷該窗口是不是被別的窗口擋住,并把沒有擋住的部分輸出到屏幕上,這樣屏幕上顯示的窗口就不會互相覆蓋而亂套。對于應用程序來說,它只需認為窗口就是自己擁有的顯示空間就可以了。
2.窗口和程序的關系
既然不同窗口的內容就是不同程序的輸出,那么一個窗口就是一個程序嗎?反過來,一個程序就是一個窗口嗎?
答案是否定的,一個窗口不一定就是一個程序,它可能只是一個程序的一部分。一個程序可以建立多個頂層窗口,如Windows的桌面和任務欄都是頂層窗口,但它們都屬于“文件管理器”進程,所以并不是一個窗口就是一個程序的代表。Windows的窗口采用層次結構,一個窗口中可以建立多個子窗口,如窗口中的狀態欄、工具欄,對話框中的按鈕、文本輸入框與復選框等都是子窗口。子窗口中還可以再建立下一級子窗口,如Word工具欄上的字體選擇框。
反過來,運行的程序并非一定就是窗口,比如悄悄在后臺運行的木馬程序就不會顯示一個窗口向用戶報告它在干非法勾當。在Windows NT下用“任務管理器”查看,進程的數量比屏幕上的窗口多得多,意味著很多的運行程序并沒有顯示窗口。如果一個程序不想和用戶交互,它可以選擇不建立窗口。
所以本章的標題“第一個窗口程序”指學習編寫第一個以標準的窗口為界面的程序,而不是泛指Windows程序。如果要寫的Win32程序不是以窗口為界面的(如控制臺程序等),就不一定采用本章中提及的以消息驅動的程序結構。
雖然以窗口為界面的程序并不是所有Windows程序的必然選擇,但絕大部分的應用程序是以這種方式出現的,從操作系統的名稱“Windows”就可以看出這一點,了解窗口程序就是相當于在了解Windows工作方式的基礎。
控制臺方式也是Windows程序的另一種常用界面,考慮到初學者剛剛接觸Windows程序的體系架構,將控制臺界面編程的內容插在本章或者后續章節中容易引起初學者對窗口程序架構理解上的混淆,所以本書將控制臺編程單獨放在附錄A中(以電子版方式放在隨書光盤中),有興趣的讀者可以在學完資源、圖形編程、界面編程等內容后再單獨閱讀這個章節。
4.1.2 窗口界面
大部分的窗口看上去都是大同小異的,先來看一個典型的窗口——Windows附帶的寫字板,該程序的界面如圖4.1所示,我們將用它來說明窗口的各個組成部分。
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?圖4.1 一個典型的窗口
窗口一般由屏幕上的矩形區域組成,不同的窗口可能包括一些相同的組成部分,如標題欄、菜單、工具欄、邊框和狀態欄等,每個部分都有自己固定的行為模式:
● 窗口邊框——窗口的外沿就是窗口邊框,用鼠標按住邊框并拖動可以調整窗口的大小。
● 標題欄——窗口的最上面是標題欄,用鼠標按住標題欄拖動可移動窗口,雙擊標題欄則將窗口最大化或從最大化的狀態恢復。通過標題欄的顏色可以區分窗口是不是活動窗口,同時標題欄列出了應用程序的名稱。
● 菜單——標題欄下面是菜單,單擊菜單會彈出各種功能選擇。
● 工具欄——菜單的下面是工具欄,工具欄上用圖標的方式列出最常用的功能,相當于菜單的快捷方式。
● 圖標和“最小化”、“最大化”與“關閉”按鈕——圖標位于標題欄的左邊,三個控制按鈕則位于標題欄的右邊。單擊圖標會彈出一個系統菜單,雙擊圖標則相當于按下了“關閉”按鈕。“最小化”、“最大化”按鈕用來控制窗口的大小。
● 狀態欄——狀態欄位于窗口的最下面,用來顯示一些狀態信息。
● 客戶區——窗口中間用來工作或輸出的區域叫做窗口的客戶區,把窗口看做是一張白紙的話,客戶區就是白紙中真正用來書寫的部分,程序在這里和用戶進行交互。
● 滾動條——如果客戶區太小不足于顯示全部內容,則右邊或底部可能還有滾動條,拖動它可以滾動窗口的客戶區,以便看到其他的內容。
雖然大部分窗口看上去都差不多,但并不是每個窗口都有這些組成部分,也許有的窗口就沒有圖標和最小化、最大化框,有的沒有工具欄或狀態欄,有的沒有標題欄,而有的就干脆是個奇怪的形狀,如Office幫助中的助手,那些小狗小貓都是些不折不扣的窗口,本書第7章中的BmpClock例子就是類似的不規則窗口的例子,另外,Windows的桌面和桌面下面的任務欄也都是窗口,就連屏幕保護程序的黑屏幕也是一個大小為整個屏幕、沒有標題欄和邊框的窗口!
一致的窗口形狀和行為模式為Windows用戶提供了一致的用戶界面,幾乎所有的窗口程序都在菜單的第一欄設置有關文件的操作和退出功能、最后一欄設置程序的幫助,相同的功能在工具欄上的圖標也是大同小異的,用戶可以不再像在DOS下那樣,對不同的程序需要學習不同的界面,用戶自從學會使用第一個軟件起,就基本學會了所有Windows軟件的使用模式,而且可以通過相似的菜單、工具欄等來發掘程序的新功能。窗口的菜單和客戶區是最個性化的部分,菜單隨程序功能的不同而不同,而客戶區則是窗口程序的輸出區域,不同的程序在客戶區內顯示了不同的內容。
4.1.3 窗口程序是怎么工作的
1.窗口程序的運行模式
對程序員來說,不僅要了解用戶可以看到的部分,還必須了解隱藏在窗口底下的細節,了解用怎樣的程序結構來實現窗口的行為模式。
DOS程序員熟悉的是順序化的、按過程驅動的程序設計方法,這種程序有明顯的開始、明顯的過程和明顯的結束,由程序運行的階段來決定用戶該做什么。
而窗口程序是事件驅動的,用戶可能隨時發出各種消息,如操作的過程中覺得窗口不夠大了,就馬上拖動邊框,程序必須馬上調整客戶區的內容以適應新的窗口大小;用戶覺得想先干別的事情,可能會把窗口最小化,“關閉”按鈕也有可能隨時被按下,這意味著程序要隨時可以處理退出的請求。如果非要規定干活的時候不能移動窗口與調整大小,那么這些窗口就會呆在桌面上一動不動。
再次提醒:這里是“窗口程序”而不是“Windows程序”,因為和窗口有關的程序才是事件驅動的,其他的Windows可能并不這樣工作,如控制臺程序的結構還是同DOS程序一樣是順序化的,但與窗口相關的Windows程序占了絕大多數,所以大部分書籍中講到Windows程序就認為是事件驅動的程序。
先通過一個簡單的例子來說明兩種程序設計方式的不同,以DOS下的文件比較命令comp為例,程序運行時先提示輸入第一個文件名,然后是輸入第二個文件名,程序比較后退出,同時把結果輸出在屏幕上。假如有一個窗口版的comp程序,那么運行時會在屏幕上出現一個對話框,上面有兩個文本框用來輸入兩個文件名,還會有個“比較”按鈕,按下后開始比較文件,用戶可以隨時按下“關閉”按鈕來退出程序。
兩種程序的運行會有相當大的不同,如圖4.2所示,DOS程序必須按照順序運行,當運行到輸入第二個文件名時,用戶不可能回到第一步修改第一個文件名,這時候用戶也不能退出(除非用戶強制用Ctrl+C鍵,但這不是程序的本意);而在窗口程序中用戶可以隨意選擇先輸入哪個文件名,同時也可以對窗口進行各種操作,當用戶做任何一個操作的時候,相當于發出了一個消息,這些消息沒有任何順序關系,程序中必須隨時準備處理不同的消息。
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?圖4.2 不同的程序結構模式
這就決定了窗口程序必定在結構上和DOS程序有很大的不同,窗口程序實現大部分功能的代碼應該呆在同一個模塊——圖中的“消息處理”模塊中,這個模塊可以隨時應付所有類型的消息,只有這樣才能隨時響應用戶的各種操作。
下面先來看一個地地道道的Win32匯編窗口程序。
2.FirstWindow源代碼
讀者可以在所帶光盤的Chapter04\FirstWindow目錄中找到源代碼,目錄里面有兩個文件,它們是匯編源文件FirstWindow.asm和nmake工具使用的makefile,匯編源程序如下:
【學習筆記】
;FirstWindow.asm 窗口程序的模板代碼
; 使用 nmake 或下列命令進行編譯和鏈接:
; ml /c /coff FirstWindow.asm
; Link /subsystem:windows FirstWindow.obj
.386
.model flat,stdcall
option casemap:none;include 文件定義
;-------------------------------------------------------------
include windows.inc
include gdi32.inc
includelib gdi32.lib
include user32.inc
includelib user32.lib
include kernel32.inc
includelib kernel32.lib
;--------------------------------------------------------------;數據段
;-------------------------------------------------------------
.data? ;未初始化
hInstance dword ?
hWinMain dword ?.const ;常量數據
szClassName byte 'MyClass',0
szCaptionMain byte 'My first Window!', 0
szText byte 'Win32 Assembly, Simple and powerful!', 0
;-----------------------------------------------------------------;代碼段
;-------------------------------------------------------------
.code
;窗口過程
;-------------------------------------------------------------
_ProcWinMain proc uses ebx edi esi, hWnd, uMsg, wParam, lParamlocal @stPs:PAINTSTRUCTlocal @stRect:RECTlocal @hDcmov eax, uMsg;-----------------------------------------------------------.if eax == WM_PAINTinvoke BeginPaint, hWnd, addr @stRectmov @hDc, eax invoke GetClientRect, hWnd, addr @stRect invoke DrawText, @hDc, addr szText, -1, \addr @stRect, \DT_SINGLELINE or DT_CENTER or DT_VCENTERinvoke EndPaint, hWnd, addr @stPs ;-------------------------------------------------------------.elseif eax == WM_CLOSEinvoke DestroyWindow, hWinMain invoke PostQuitMessage, NULL;--------------------------------------------------------------.else invoke DefWindowProc, hWnd, uMsg, wParam, lParamret.endif;--------------------------------------------------------------xor eax, eax ret
_ProcWinMain endp
;-------------------------------------------------------------------_WinMain proclocal @stWndClass:WNDCLASSEXlocal @stMsg:MSGinvoke GetModuleHandle, NULL mov hInstance, eax invoke RtlZeroMemory, addr @stWndClass, sizeof @stWndClass ;注冊窗口類;------------------------------------------------------------------invoke LoadCursor, 0, IDC_ARROWmov @stWndClass.hCursor, eax push hInstance pop @stWndClass.hInstancemov @stWndClass.cbSize, sizeof WNDCLASSEX mov @stWndClass.style, CS_HREDRAW or CS_VREDRAWmov @stWndClass.lpfnWndProc, offset _ProcWinMain mov @stWndClass.hbrBackground, COLOR_WINDOW + 1mov @stWndClass.lpszClassName, offset szClassName invoke RegisterClassEx, addr @stWndClass ;建立并顯示窗口;--------------------------------------------------------------------invoke CreateWindowEx, WS_EX_CLIENTEDGE, offset szClassName, \offset szCaptionMain, WS_OVERLAPPEDWINDOW, \100, 100, 600, 400, \NULL, NULL, hInstance, NULL mov hWinMain, eax invoke ShowWindow, hWinMain, SW_SHOWNORMALinvoke UpdateWindow, hWinMain ;消息循環;------------------------------------------------------------------------.while TRUE invoke GetMessage, addr @stMsg, NULL, 0, 0.break .if eax == 0invoke TranslateMessage, addr @stMsg invoke DispatchMessage, addr @stMsg .endwret
_WinMain endp ;----------------------------------------------------------------------------
start:call _WinMain invoke ExitProcess, 0
end start
讓我們打開一個DOS窗口,切換到FirstWindow所在的目錄,運行環境設置的批處理文件var.bat,再鍵入nmake編譯出FirstWindow.exe,這個程序只有2560字節,運行后窗口出來了,如圖4.3所示。對于這個窗口,用戶可以拖動邊框去改變大小、按標題欄上的按鈕來最大化和最小化,當光標移到邊框的時候,會自動變成雙箭頭……總之,這個窗口包括了一個典型窗口的所有特征。
windws XP 環境下【編譯:ml /c /coff FirstWindow.asm 鏈接:Link /subsystem:windows FirstWindow.obj】
運行如下:
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 圖4.3 FirstWindow的運行結果
接下來開始分析源代碼,看了這近三頁的源代碼,第一個感覺是什么?是不是想撤退了?筆者剛開始編Win32程序的時候就是這種感覺,可能90%的人有同樣的感覺,別急,過了這一關,Win32匯編的入門就成功了一半,所以千萬要挺住!有個振奮人心的消息是,這個程序是大部分窗口程序的模板,以后要寫一個新的程序,把它復制過來再往中間添磚加瓦就是了,工夫一點都不白費。
先靜下心來分析一下程序的結構,還看得懂,很好!其實源程序的結構在第3章里已經了解過了,首先是注釋……模式定義……include…… .data數據段,都沒有問題,這些已經占去了近40行了,好了,終于是關鍵的代碼段了,統計一下,只剩80行代碼了。
分析一下程序的結構,發現入口是start,然后執行了一個_WinMain子程序,完成后就是程序退出的函數ExitProcess,再看_WinMain的結構,前面是順序下來的幾個API:
GetModuleHandle → RtlZeroMemory → LoadCursor → RegisterClassEx → CreateWindowEx → ShowWindow → UpdateWindow
從名稱上就能看出它們的用途,很明顯,窗口是在CreateWindowEx處建立的,ShowWindow則是把窗口顯示在屏幕上,這些代碼是窗口的建立過程。
接下來,就是一個由3個API組成的循環了:
GetMessage → TranslateMessage → DispatchMessage
很明顯,這是與消息有關的循環,因為API名稱中都帶有Message字樣,如果退出這個循環,程序也就結束了,這個循環叫做消息循環。設置_WinMain子程序并不是必需的,可以把_WinMain的所有代碼放到主程序中,沒有任何影響,之所以這樣,只是為了將這里使用的變量定義成局部變量,這樣可以方便移植。
看了程序的流程,似乎沒有什么地方涉及窗口的行為,如改變大小和移動位置的處理等。再看源程序,除了_WinMain,還有一個子程序_ProcWinMain,但除了在WNDCLASSEX結構的賦值中提到過它,好像就沒有什么地方要用到這個子程序,起碼在自己編寫的源代碼中沒有任何一個地方調用過它。
再看_ProcWinMain,它是一個分支結構處理的子程序,功能是把參數uMsg取出來,根據不同的uMsg執行不同的代碼,完了以后就退出了,中間也沒有任何代碼和主程序有關聯。
第一個窗口程序就是由這么兩個似乎是風馬牛不相及的部分組成的,但它確實能工作,對于寫慣了DOS匯編的程序員來說,這似乎不可理解。下面來看看這么一個陌生而奇怪的程序是如何工作的。
3.窗口程序的運行過程
在屏幕上顯示一個窗口的過程一般有以下步驟,這就是主程序的結構流程:
(1)得到應用程序的句柄(GetModuleHandle)。
(2)注冊窗口類(RegisterClassEx)。在注冊之前,要先填寫RegisterClassEx的參數WNDCLASSEX結構。
(3)建立窗口(CreateWindowEx)。
(4)顯示窗口(ShowWindow)。
(5)刷新窗口客戶區(UpdateWindow)。
(6)進入無限的消息獲取和處理的循環。首先獲取消息(GetMessage),如果有消息到達,則將消息分派到回調函數處理(DispatchMessage),如果消息是WM_QUIT,則退出循環。
程序的另一半_ProcWinMain子程序是用來處理消息的,它就是窗口的回調函數(Callback),也叫做窗口過程,之所以是回調函數,是因為它是由Windows而不是我們自己調用的,我們調用DispatchMessage,而DispatchMessage在自己的內部回過來調用窗口過程。
所有的用戶操作都是通過消息來傳給應用程序的,如用戶按鍵、鼠標移動、選擇了菜單和拖動了窗口等,應用程序中由窗口過程接收消息并處理,在例子程序中就是_ProcWinMain。由于窗口過程構造了一個分支結構,對應不同的消息執行不同的代碼,所以一個應用程序中幾乎所有的功能代碼都集中在窗口過程里。
窗口程序運行中消息傳輸的流程可以由圖4.4來表示。
先來看看Windows對消息的處理。Windows在系統內部有一個系統消息隊列,當輸入設備有所動作的時候,如用戶按動了鍵盤、移動了鼠標、按下或放開了鼠標等,Windows都會產生相應的記錄放在系統消息隊列里,如圖4.4中的箭頭a和b所示,每個記錄中包含消息的類型、發生的位置(如鼠標在什么坐標移動)和發生的時間等信息。
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?圖4.4 窗口程序的運行過程
同時,Windows為每個程序(嚴格地說是每個線程)維護一個消息隊列,Windows檢查系統消息隊列里消息的發生位置,當位置位于某個應用程序的窗口范圍內的時候,就把這個消息派送到應用程序的消息隊列里,如圖4.4中的箭頭c所示。
當應用程序還沒有來取消息的時候,消息就暫時保留在消息隊列里,當程序中的消息循環執行到GetMessage的時候,控制權轉移到GetMessage所在的USER32.DLL中(箭頭1),USER32.DLL從程序消息隊列中取出一條消息(箭頭2),然后把這條消息返回應用程序(箭頭3)。
應用程序可以對這條消息進行預處理,如可以用TranslateMessage把基于鍵盤掃描碼的按鍵消息轉換成基于ASCII碼的鍵盤消息,以后也會用到TranslateAccelerator把鍵盤快捷鍵轉換成命令消息,但這個步驟不是必需的。
然后應用程序將處理這條消息,但方法不是自己直接調用窗口過程來完成的,而是通過DispatchMessage間接調用窗口過程,Dispatch的英文含義是“分派”,之所以是“分派”,是因為一個程序可能建有不止一個窗口,不同的窗口消息必須分派給相應的窗口過程。當控制權轉移到USER32.DLL中的DispatchMessage時,DispatchMessage找出消息對應窗口的窗口過程,然后把消息的具體信息當做參數來調用它(箭頭5),窗口過程根據消息找到對應的分支去處理,然后返回(箭頭6),這時控制權回到DispatchMessage,最后DispatchMessage函數返回應用程序(箭頭7)。這樣,一個循環就結束了,程序又開始新一輪的GetMessage。
有個很常見的問題:為什么要由Windows來調用窗口過程,程序取了消息以后自己處理不是更簡便嗎?事實上并非如此,如果程序自己處理消息的“分派”,就必須自己維護本程序所屬窗口的列表,當程序建立的窗口不止一個的時候,這個工作就變得復雜起來;另一個原因是:別的程序也可能用SendMessage通過Windows直接調用你的窗口過程;第三個原因:Windows并不是把所有的消息都放進消息隊列,有的消息是直接調用窗口過程處理的,如WM_SETCURSOR等實時性很強的消息,所以窗口過程必須開放給Windows。
應用程序之間也可以互發消息,PostMessage是把一個消息放到其他程序的消息隊列中,如圖4.4中箭頭d所示,目標程序收到了這條消息就把它放入該程序的消息隊列去處理;而SendMessage則越過消息隊列直接調用目標程序的窗口過程(如圖4.4中箭頭I所示),窗口過程返回以后才從SendMessage返回(如圖4.4中箭頭II所示)。
窗口過程是由Windows回調的,Windows又是怎么知道往哪里回調的呢?答案是我們在調用RegisterClassEx函數的時候已經把窗口過程的地址告訴了Windows。
4.2 分析窗口程序
了解了消息驅動體系的工作流程以后,讓我們來分析如何用Win32匯編實現這一切,本節和下一節將詳細分析FirstWindow源程序。
4.2.1 模塊和句柄
1.模塊的概念
一個模塊代表的是一個運行中的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 eaxmov hUserDllHandle,eax.endif…
如果使用參數NULL調用GetModuleHandle,那么得到的是調用者本模塊的句柄,我們的源程序中就是這樣使用的:
invoke GetModuleHandle, NULL
mov hInstance, eax
可以注意到,把返回的句柄放到了取名為hInstance的變量里而并不是放在hModule中,為什么是hInstance呢?Instance是“實例”,它的概念來自于Win16,Win16中不同運行程序的地址空間并非是完全隔離的,一個可執行文件運行后形成“模塊”,多次加載同一個可執行文件時,這個“模塊”是公用的,為了區分多次加載的“拷貝”,就把每個“拷貝”叫做實例,每個實例均用不同的“實例句柄”(hInstance)值來標識它們。
但在Win32中,程序運行時是隔離的,每個實例都使用自己私有的4 GB空間,都認為自己是唯一的,不存在一個模塊的多個實例的問題,實際上在Win32中,實例句柄就是模塊句柄,但很多API原型中用到模塊句柄的時候使用的名稱還是沿用hInstance,所以我們還是把變量名稱取為hInstance。
在C語言的編程中,hInstance通過WinMain由系統傳入,WinMain的原型是:
WinMain(hInstance, hPrevInstance, lpzCmdParam, nCmdShow),程序不用自己去獲得hInstance,這個過程由C的初始化代碼代勞了,但在Win32匯編中hInstance必須自己獲取,如果不了解hModule就是hInstance的話,就無法得知如何得到hInstance,因為并沒有一個GetInstanceHandle之類的API函數。
2.句柄是什么
隨著分析的深入,句柄(handle)一詞也出現得頻繁起來,“句柄”是什么呢?句柄只是一個數值而已,它的值對程序來說是沒有意義的,它只是Windows用來表示各種資源的編號而已,可見只有Windows才知道怎么使用它來引用各種資源。
下面舉例說明。屏幕上已經有10個窗口,Windows把它們從1到10編號,應用程序又建立了一個窗口,現在Windows把它編號為11,然后把11當做窗口句柄返回給應用程序,應用程序并不知道11代表的是什么,但在操作窗口的時候,把11當做句柄傳給Windows,Windows自然可以根據這個數值查出是哪個窗口。當該窗口關閉的時候,11這個編號作廢。第二次運行的時候,如果屏幕上現有5個窗口,那么現在句柄可能就是6了,所以,應用程序并不用關心句柄的具體數值是多少。打個比方,可以把句柄當做是商場中寄放書包時營業員給的紙條,紙條上的標記用戶并不知道是什么意思,但把它交還給營業員的時候,她自然會找到正確的書包。
Windows中幾乎所有的東西都是用句柄來標識的,文件句柄、窗口句柄、線程句柄和模塊句柄等,同樣道理,不必關心它們的值究竟是多少,拿來用就是了!
4.2.2 創建窗口
在創建窗口之前,先要談到“類”。“類”的概念讀者都不陌生,主要是為了把一組物體的相同屬性歸納整理起來封裝在一起,以便重復使用,在“類”已定義的屬性基礎上加上其他個性化的屬性,就形成了各式各樣的個體。
Windows中創建窗口同樣使用這樣的層次結構。首先定義一個窗口類,然后在窗口類的基礎上添加其他的屬性建立窗口。不用一步到位的辦法是因為很多窗口的基本屬性和行為都是一樣的,如按鈕、文本輸入框和選擇框等,對這些特殊的窗口Windows都預定義了對應的類,使用時直接使用對應的類名建立窗口就可以了。只有用戶自定義的窗口才需要先定義自己的類,再建立窗口。這樣可以節省資源。
1.注冊窗口類
建立窗口類的方法是在系統中注冊,注冊窗口類的API函數是RegisterClassEx,最后的“Ex”是擴展的意思,因為它是Win16中RegisterClass的擴展。一個窗口類定義了窗口的一些主要屬性,如:圖標、光標、背景色、菜單和負責處理該窗口所屬消息的函數。這些屬性并不是分成多個參數傳遞過去的,而是定義在一個WNDCLASSEX結構中,再把結構的地址當參數一次性傳遞給RegisterClassEx,WNDCLASSEX是WNDCLASS結構的擴展。
WNDCLASSEX的結構定義為:
WNDCLASSEX STRUCTcbsize 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
在FirstWindow程序中,注冊窗口類的代碼是:
local @stWndClass:WNDCLASSEX;定義一個WNDCLASSEX結構…invoke RtlZeroMemory,addr @stWndClass,sizeof @stWndClassinvoke LoadCursor,0,IDC_ARROWmov @stWndClass.hCursor,eaxpush hInstancepop @stWndClass.hInstancemov @stWndClass.cbSize,sizeof WNDCLASSEXmov @stWndClass.style,CS_HREDRAW or CS_VREDRAWmov @stWndClass.lpfnWndProc,offset _ProcWinMainmov @stWndClass.hbrBackground,COLOR_WINDOW + 1mov @stWndClass.lpszClassName,offset szClassNameinvoke RegisterClassEx,addr @stWndClass
程序定義了一個WNDCLASSEX結構的變量@stWndClass,用RtlZeroMemory將它填為全零(局部變量初始化的重要性在第3章中已經強調過),再填寫結構的各個字段,這樣,沒有賦值的部分就保持為0,結構各字段的含義如下:
● hIcon——圖標句柄,指定顯示在窗口標題欄左上角的圖標。Windows已經預定義了一些圖標,同樣,程序也可以使用在資源文件中定義的圖標,這些圖標的句柄可以用LoadIcon函數獲得。因為例子程序沒有用到圖標,所以Windows給窗口顯示了一個默認的圖標。
● hCursor——光標句柄,指定了鼠標在窗口中的光標形狀。同樣,Windows也預定義了一些光標,可以用LoadCursor獲取它們的句柄,IDC_ARROW是Windows預定義的箭頭光標,如果想使用自定義的光標,也可以自己在資源文件中定義。
● lpszMenuName——指定窗口上顯示的默認菜單,它指向一個字符串,描述資源文件中菜單的名稱,如果資源文件中菜單是用數值定義的,那么這里使用菜單資源的數值。窗口中的菜單也可以在建立窗口函數CreateWindowEx的參數中指定。如果在兩個地方都沒有指定,那么建立的窗口上就沒有菜單。
● hInstance——指定要注冊的窗口類屬于哪個模塊,模塊句柄在程序開始的地方已經用GetModuleHandle函數獲得。
● cbSize——指定WNDCLASSEX結構的長度,用sizeof偽操作來獲取。很多Win32API參數中的結構都有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_WINDOW等,使用顏色值的時候,Windows規定必須在顏色值上加1,所以程序中的指令是:
mov @stWndClass.hbrBackground,COLOR_WINDOW + 1
● lpszClassName——指定程序員要建立的類命名,以便以后用這個名稱來引用它。這個字段是一個字符串指針,在程序里,它指向“MyClass”字符串。
● cbWndExtra和cbClsExtra——分別是在Windows內部保存的窗口結構和類結構中給程序員預留的空間大小,用來存放自定義數據,它們的單位是字節。不使用自定義數據的話,這兩個字段就是0。
● lpfnWndProc——最重要的參數,它指定了基于這個類建立的窗口的窗口過程地址。通過這個參數,Windows就知道了在DispatchMessage函數中把窗口消息發到哪里去,一個窗口過程可以為多個窗口服務,只要這些窗口是基于同一個窗口類建立的。Windows中不同應用程序中的按鈕和文本框的行為都是一樣的,就是因為它們是基于相同的Windows預定義類建立的,它們背后的窗口過程其實是同一段代碼。
結構中的style表示窗口的風格,Windows已經有一些預定義的值,它們是以CS(Class Style的縮寫)開始的標識符,如表4.1所示。
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 表4.1 一些窗口類的style預定義值
可以看到,這些預定義值實際上是在使用不重復的數據位,所以可以組合起來使用,同時使用不同的預定義值并不會引起混淆。
對于不同二進制位組合的計算,“加”和“或”的結果是一樣的,在FirstWindow程序中用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了。
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”中指定的窗口過程中去,當然,這里也可以是Windows預定義的類名,如編輯框就是“EDIT”。
● lpWindowName——指向表示窗口名稱的字符串,該名稱會顯示在標題欄上。如果該參數空白,則標題欄上什么都沒有。
● hMenu——窗口上要出現的菜單的句柄。在注冊窗口類的時候也定義了一個菜單,那是窗口的默認菜單,意思是如果這里沒有定義菜單(用參數NULL)而注冊窗口類時定義了菜單,則使用窗口類中定義的菜單;如果這里指定了菜單句柄,則不管窗口類中有沒有定義都將使用這里定義的菜單;如果兩個地方都沒有定義菜單句柄,則窗口上沒有菜單。另外,當建立的窗口是子窗口時(dwStyle中指定了WS_CHILD),這個參數是另一個含義,這時hMenu參數指定的是子窗口的ID號(這樣可以節省一個參數的位置,因為子窗口不會有菜單)。
● lpParam——這是一個指針,指向一個欲傳給窗口的參數,這個參數在WM_CREATE消息中可以被獲取,一般情況下用不到這個字段。
● hInstance——模塊句柄,和注冊窗口類時一樣,指定了窗口所屬的程序模塊。
● hWndParent——窗口所屬的父窗口,對于普通窗口(相對于子窗口),這里的“父子”關系只是從屬關系,主要用來在父窗口銷毀時一同將其“子”窗口銷毀,并不會把窗口位置限制在父窗口的客戶區范圍內,但如果要建立的是真正的子窗口(dwStyle中指定了WS_CHILD的時候),這時窗口位置會被限制在父窗口的客戶區范圍內,同時窗口的坐標(x,y)也是以父窗口的左上角為基準的。
● x,y——指定窗口左上角位置,單位是像素。默認時可指定為CW_USEDEFAULT,這樣Windows會自動為窗口指定最合適的位置,當建立子窗口時,位置是以父窗口的左上角為基準的,否則,以屏幕左上角為基準。
● nWidth,nHeight—窗口的寬度和高度,也就是窗口的大小,同樣是以像素為單位的。默認時可指定為CW_USEDEFAULT,這樣Windows會自動為窗口指定最合適的大小。
窗口的兩個參數dwStyle和dwExStyle決定了窗口的外形和行為,dwStyle是從Win16開始就有的屬性,表4.2列出了一些常見的dwStyle定義,它們是一些以WS(Windows Style的縮寫)為開頭的預定義值。
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 表4.2 窗口風格的預定義值
為了容易理解,Windows也為一些定義取了一些別名,同時,由于窗口的風格往往是幾種風格的組合,所以Windows也預定義了一些組合值,如表4.3所示。
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?表4.3 等效的窗口風格預定義值
dwExStyle是Win32中擴展的,它們是一些以WS_EX_開頭的預定義值,主要定義了一些特殊的風格,表4.4給出了一些最常用的特殊風格。
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?表4.4 窗口擴展風格的預定義值
用預定義的組合值WS_EX_PALETTEWINDOW可以很方便地構成浮在其他窗口前面的工具欄。
下面看幾種不同的窗口外形,如圖4.5所示,窗口1是WS_OVERLAPPED類型的窗口,只有一個邊框,沒有控制按鈕和圖標;窗口2同時指定WS_MAXIMIZEBOX,WS_MINIMIZEBOX和WS_SYSMENU,在窗口1的基礎上多了控制按鈕和圖標;窗口3是WS_POPUPWINDOW風格的,這是一個沒有標題和控制按鈕的彈出式窗口,常見的軟件裝入時的版權窗口就是這種風格;前面3個窗口都不能通過拖動邊框改變大小,而窗口4指定了WS_THICKFRAME風格,可以改變大小,它的邊框顯得厚了一點;窗口5的風格是WS_OVERLAPPEDWINDOW,是最常見的屬性組合;窗口6在窗口5的基礎上指定了WS_EX_CLIENTEDGE,它的客戶區顯得有立體感;窗口7是個工具欄,指定的是WS_EX_TOOLWINDOW風格,可以看到它的標題欄要小得多;窗口8指定了WS_HSCROLL和WS_VSCROLL風格,窗口中多了垂直和水平滾動條。
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?圖4.5 不同風格的窗口
FirstWindow程序中建立窗口的相關代碼是這樣的:
invoke CreateWindowEx,WS_EX_CLIENTEDGE,\offset szClassName,offset szCaptionMain,\WS_OVERLAPPEDWINDOW,\100,100,600,400,\NULL,NULL,hInstance,NULLmov hWinMain,eaxinvoke ShowWindow,hWinMain,SW_SHOWNORMALinvoke UpdateWindow,hWinMain…
建立窗口以后,eax中傳回來的是窗口句柄,要把它保存起來以備后用,這時候,窗口雖已建立,但還沒有在屏幕上顯示出來,要用ShowWindow把它顯示出來,ShowWindow也可以用在別的地方,主要用來控制窗口的顯示狀態(顯示或隱藏),大小控制(最大化、最小化或原始大小)和是否激活(當前窗口還是背后的窗口),它用窗口句柄做第一個參數,第二個參數則是顯示的方式。表4.5給出了顯示方式預定義值。
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?表4.5 ShowWindow函數顯示方式的定義
窗口顯示以后,用UpdateWindow繪制客戶區,它實際上就是向窗口發送了一條WM_PAINT消息。到此為止,一個頂層窗口就正常建立并顯示了。
CreateWindowEx也可以用來建立子窗口,Windows中有很多預定義的子窗口類,如按鈕和文本框的類名分別是“Button”和“Edit”。要建立一個按鈕,只要把lpClassName指向“Button”字符串就可以了。下面舉例說明建立一個按鈕的方法,代碼如下:
.dataszButton db 'button',0szButtonText 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消息中加入建立按鈕的代碼,執行一下,窗口中就出現了一個按鈕,如圖4.6所示。建立按鈕的時候,lpWindowName參數就是按鈕上的文字,風格則一定要指定WS_CHILD,建立的按鈕才會在我們的主窗口上,WS_VISIBLE也要同時指定,否則按鈕不會顯示出來,hMenu參數在這里用來表示子窗口ID,將它設置為1,在建立多個子窗口的時候,ID應該有所區別。這個例子的源程序可以在所附帶光盤的Chapter04\FirstWindow-1目錄中找到。
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?圖4.6 用CreateWindowEx建立的按鈕
【學習筆記】
;FirstWindow1.asm 在窗口模板程序上添加一個按鈕
; 使用 nmake 或下列命令進行編譯和鏈接:
; ml /c /coff FirstWindow1.asm
; Link /subsystem:windows FirstWindow1.obj
.386
.model flat,stdcall
option casemap:none;include 文件定義
;-------------------------------------------------------------
include windows.inc
include gdi32.inc
includelib gdi32.lib
include user32.inc
includelib user32.lib
include kernel32.inc
includelib kernel32.lib
;--------------------------------------------------------------;數據段
;-------------------------------------------------------------
.data? ;未初始化
hInstance dword ?
hWinMain dword ?.const ;常量數據
szClassName byte 'MyClass',0
szCaptionMain byte 'My first Window!', 0
szText byte 'Win32 Assembly, Simple and powerful!', 0
szButton byte 'button', 0
szButtonText byte '&OK', 0
;-----------------------------------------------------------------;代碼段
;-------------------------------------------------------------
.code
;窗口過程
;-------------------------------------------------------------
_ProcWinMain proc uses ebx edi esi, hWnd, uMsg, wParam, lParamlocal @stPs:PAINTSTRUCTlocal @stRect:RECTlocal @hDcmov eax, uMsg;-----------------------------------------------------------.if eax == WM_PAINTinvoke BeginPaint, hWnd, addr @stRectmov @hDc, eax invoke GetClientRect, hWnd, addr @stRect invoke DrawText, @hDc, addr szText, -1, \addr @stRect, \DT_SINGLELINE or DT_CENTER or DT_VCENTERinvoke EndPaint, hWnd, addr @stPs ;-------------------------------------------------------------;添加一個按鈕.elseif eax == WM_CREATEinvoke CreateWindowEx, NULL, \offset szButton, offset szButtonText, \WS_CHILD or WS_VISIBLE, \10, 10, 65, 22, \hWnd, 1, hInstance, NULL ;-------------------------------------------------------------.elseif eax == WM_CLOSEinvoke DestroyWindow, hWinMain invoke PostQuitMessage, NULL;--------------------------------------------------------------.else invoke DefWindowProc, hWnd, uMsg, wParam, lParamret.endif;--------------------------------------------------------------xor eax, eax ret
_ProcWinMain endp
;-------------------------------------------------------------------_WinMain proclocal @stWndClass:WNDCLASSEXlocal @stMsg:MSGinvoke GetModuleHandle, NULL mov hInstance, eax invoke RtlZeroMemory, addr @stWndClass, sizeof @stWndClass ;注冊窗口類;------------------------------------------------------------------invoke LoadCursor, 0, IDC_ARROWmov @stWndClass.hCursor, eax push hInstance pop @stWndClass.hInstancemov @stWndClass.cbSize, sizeof WNDCLASSEX mov @stWndClass.style, CS_HREDRAW or CS_VREDRAWmov @stWndClass.lpfnWndProc, offset _ProcWinMain mov @stWndClass.hbrBackground, COLOR_WINDOW + 1mov @stWndClass.lpszClassName, offset szClassName invoke RegisterClassEx, addr @stWndClass ;建立并顯示窗口;--------------------------------------------------------------------invoke CreateWindowEx, WS_EX_CLIENTEDGE, offset szClassName, \offset szCaptionMain, WS_OVERLAPPEDWINDOW, \100, 100, 600, 400, \NULL, NULL, hInstance, NULL mov hWinMain, eax invoke ShowWindow, hWinMain, SW_SHOWNORMALinvoke UpdateWindow, hWinMain ;消息循環;------------------------------------------------------------------------.while TRUE invoke GetMessage, addr @stMsg, NULL, 0, 0.break .if eax == 0invoke TranslateMessage, addr @stMsg invoke DispatchMessage, addr @stMsg .endwret
_WinMain endp ;----------------------------------------------------------------------------
start:call _WinMain invoke ExitProcess, 0
end start
Window XP環境下編譯運行如下:
4.2.3 消息循環
1.消息循環的一般形式
程序中的以下代碼就是通常的消息循環:
.while TRUEinvoke GetMessage,addr @stMsg,NULL,0,0.break .if eax == 0invoke TranslateMessage,addr @stMsginvoke DispatchMessage,addr @stMsg.endw
消息循環中的幾個函數要用到一個MSG結構,用來做消息傳遞:
MSG STRUCThwnd 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,wMsgFilterMin,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函數才返回,然后開始新一輪消息循環。
2.其他形式的消息循環
GetMessage函數是程序空閑的時候主動將控制權交還給Windows的一種方式,Windows是一個搶占式的多任務系統,任務之間每20 ms切換一次,試想一下,如果窗口程序在主窗口中采用死循環等待,消息由Windows直接發送到窗口過程,那么程序會是下列這種樣子:
invoke CreateWindow,…
invoke ShowWindow,…
invoke UpdateWindow,…
.while dwQuitFlag == 0 ;要退出時在窗口過程中設置dwQuitFlag
.endw
invoke ExitProcess,…
但這樣一來,即使程序在空閑狀態,輪到自己的20 ms時間片的時候,CPU時間就會全部消耗在 .while循環中,使用GetMessage的時候,輪到應用程序時間片的時候,如果消息隊列里還沒有消息,那么程序還是停留在GetMessage內部,這時就可以由Windows當家做主沒收這20 ms的時間片,這樣保證了CPU資源的合理應用。
如果應用程序想把所有時間充分利用回來,消息隊列里沒有消息的時候不讓GetMessage在Windows內部等待,拱手交出屬于自己的CPU時間,那么消息循環可以是下列這種樣子:
.while TRUEinvoke PeekMessage,addr @stMsg,NULL,0,0,PM_REMOVE.if eax.break .if @stMsg.message == WM_QUITinvoke TranslateMessage,addr @stMsginvoke DispatchMessage,addr @stMsg.else<做其他工作>.endif.endw
PeekMessage是一個類似于GetMessage的函數,區別在于當消息隊列里有消息的時候,PeekMessage取回消息,并在eax中返回非零值,沒有消息的時候它會直接返回,并在eax中返回零。所以在返回非零值的時候,程序檢查消息是否是WM_QUIT,是則結束消息循環,不是則用標準流程處理消息;返回零的時候,表示是空閑時間,程序就可以做其他工作了,但插入做其他工作的代碼執行時間不能過長,以不超過10 ms為好,否則會影響正常的消息處理,使窗口的反應看起來很遲鈍。如果必須處理很長時間的工作,那么應該將它分成很多小部分處理,以便有足夠的頻率用PeekMessage來檢查消息。
PeekMessage的前面4個參數和GetMessage是相同的,增加的最后一個參數表示在取回消息以后,對消息隊列中的消息是否保留。當這個參數是PM_REMOVE時,消息被取回的同時也被從消息隊列里刪除,而用PM_NOREMOVE的時候,被取回的消息不會從消息隊列中刪除,函數相當于“偷看”了這條消息。例子程序中用了PM_REMOVE,否則每次看到的都是隊列中的第一條消息。
4.2.4 窗口過程
窗口過程是給Windows回調用的,它必須遵循規定的格式。對窗口過程的子程序名并沒有規定,對Windows來說,窗口過程的地址才是唯一需要的,例子程序中的子程序名是_ProcWinMain,讀者可以改用任何名稱。窗口過程子程序的參數格式為:
WindowProc proc hwnd,uMsg,wParam,lParam
第一個參數是窗口句柄,由于一個窗口過程可能為多個基于同一個窗口類的窗口服務,所以Windows回調的時候必須指出要操作的窗口,否則窗口過程不知道要去處理哪個窗口,FirstWindow程序只建立了一個窗口,所以每次傳遞過來的hwnd和用CreateWindowEx函數返回的窗口句柄是一樣的;第二個參數是消息標識,后面兩個參數是消息的兩個參數。這4個參數和消息循環中MSG結構中的前4個字段是一樣的。
1.窗口過程的結構
窗口過程一般有如下的結構:
WindowProc proc uses ebx edi esi hWnd,uMsg,wParam,lParammov eax,uMsg.if eax == WM_XXX<處理WM_XXX消息>.elseif eax == WM_YYY<處理WM_YYY消息>.elseif eax == WM_CLOSEinvoke DestroyWindow,hWinMaininvoke PostQuitMessage,NULL.elseinvoke DefWindowProc,hWnd,uMsg,wParam,lParamret.endifxor eax,eaxretWindowProc 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之間,用戶可以自定義一些消息,通過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清0后返回,如果在處理某個消息的時候需要返回不同的值,可以在分支中將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,程序就會莫名其妙地退出了。
2.收到消息的順序
窗口過程收到消息是有一定順序的,收到第一條消息并不是從消息循環開始以后,而是在CreateWindowEx中就開始了,顯示和刷新窗口的函數ShowWindow和UpdateWindow也向窗口過程發送消息,這一點并不奇怪,因為Windows在CreateWindowEx前調用RegisterClassEx的時候就已經得到窗口過程的地址了。并且在建立窗口的過程中需要窗口過程的配合。表4.6和表4.7分別列出了調用CreateWindowEx和ShowWindow的時候窗口過程收到的消息
表4.6 調用CreateWindowEx時窗口過程收到的消息 | |
消息發生 | 說明 |
WM_GETMINMAXINFO | 獲取窗口大小,以便初始化 |
WM_NCCREATE | 非客戶區開始建立 |
WM_NCCALCSIZE | 計算客戶區大小 |
WM_CREATE | 窗口建立 |
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?表4.7 調用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為止。表4.8列出了DestroyWindow向窗口過程發送的消息。
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 表4.8 調用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消息也不用處理。
3.消息的默認處理——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后面直接使用的這句ret非常重要,如果丟失了這一句,那么相當于處理大多數消息時沒有返回正確的值,窗口將不會正常工作。
表4.9中列出了DefWindowProc中對一些消息的處理方法,如果與用戶期望的不同,就必須在窗口過程中自己處理。
表4.9 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以結束消息循環。
附錄B(以電子版方式放在隨書光盤中)中的內容以幾個實驗的方式,演示了窗口從建立、進行各種操作到摧毀的過程中收到的各種消息,讀者可以自行閱讀并進行實驗,以加深對窗口程序工作原理的認識。
4.3 窗口間的通信
4.3.1 窗口間的消息互發
在介紹消息循環的時候,已經知道在不同應用程序之間的窗口中可以互發消息(如圖4.4所示),方法是通過SendMessage或者PostMessage函數,這兩個函數的使用語法是相同的:
invoke PostMessage,hWnd,Msg,wParam,lParam
invoke SendMessage,hWnd,Msg,wParam,lParam。
對于不同的Msg,wParam和lParam的含義是不同的,如對于WM_SETTEXT是:
wParam = 0; ;未定義,必須為0
lParam = (LPARAM)(LPCTSTR)lpsz ;要設置的字符串地址
想一想就會發現一個問題:Windows中不同應用程序的地址空間是隔離的(如圖1.6所示),假設程序1要用SendMessage調用程序2所屬窗口的窗口過程,但程序2窗口過程的代碼并不在程序1的地址空間中,那么SendMessage如何調用它呢?其實很簡單,當程序1調用SendMessage函數的時候,Windows會先保存wParam和lParam參數并等待,等輪到程序2的時間片的時候再去調用它的窗口過程,并把保存的wParam和lParam參數發給它,等窗口過程返回的時候,Windows記下返回值并等待,再等輪到程序1的時間片的時候把返回值當做SendMessage的返回值傳給程序1,這樣程序1看上去就像自己直接在調用程序2的窗口過程一樣。
但又一個問題出現了:Windows在做“牽線紅娘”的時候傳遞了wParam和lParam,以及返回值,如果參數指向一個字符串呢,比如說上面的WM_SETTEXT消息中的lParam指向一個字符串,假設程序1中lParam指向字符串的地址為xxxxxxxx,把這個地址傳給程序2的時候,程序2不可能訪問到程序1的地址空間,在程序2中xxxxxxxx指向的可能是其他內容,也可能是不可訪問的,這又該如何處理呢?
寫一個源程序實驗一下,用一個程序向另一個程序的窗口發送WM_SETTEXT消息,然后在另一個程序中將接收到的WM_SETTEXT消息的參數顯示出來。先來打造接收程序,首先復制一份FirstWindow的代碼,然后在窗口過程的分支中加上以下代碼:
.elseif eax == WM_SETTEXTinvoke wsprintf,addr szBuffer,addr szReceive,lParam,lParaminvoke MessageBox,hWnd,offset szBuffer,addr szCaptionMain,MB_OK
同時在數據段中加上下列定義:
szCaptionMain db 'Receive Message',0
szReceive db 'Receive WM_SETTEXT message',0dh,0ahdb 'param: %08x',0dh,0ahdb 'text: "%s"',0dh,0ah,0
在這里,要提及Win32 API中一個很常用的函數wsprintf,這是一個字符串格式化函數,可以將數值按指定格式翻譯成字符串,類似于C語言中的printf函數,它的原型是這樣的:
int wsprintf(LPTSTR lpOut, // 輸出緩沖區地址LPCTSTR lpFmt, // 格式化串地址... // 變量列表);
變量列表的數目由格式化字符串規定,wsprintf處理格式化字符串,遇到普通的字符則直接復制到輸出,遇到%字符則代表有一個變量,%后面不同的字母表示不同的輸出格式,如%d表示輸出為整數,%x表示輸出為十六進制,%s表示輸出字符串等。
%符號和表示格式的d,x和s等字母間可以用數字來指定輸出時占用的位長,這時輸出的位長不夠時函數會用空格填齊。另外,表示位長的數字前可以加0來表示填齊時用“0”而非空格,如%08x表示輸出為8位前面用0填齊的十六進制數。wsprintf是Win32API中唯一一個參數數量不定的函數,使用wsprintf函數的時候,參數的數量取決于格式化字符串中用%號指定的數量,變量列表的數目和格式化串中的%格式一定要一一對應,比如,例子中szReceive中有兩個%號定義,那么后面就要額外跟兩個參數:
invoke wsprintf,addr szBuffer,addr szReceive,lParam,lParam
這條語句將lParam的數值,以及lParam的字符串按照szReceive格式化串定義的格式轉換,并將結果存放到szBuffer中,然后程序將szBuffer中的內容在一個消息框中顯示出來:
invoke MessageBox,hWnd,offset szBuffer,addr szCaptionMain,MB_OK
【學習筆記】
完整的接收程序如下:
;Receive.asm 從一個程序向另一個窗口程序發送消息 之 [消息接收程序]
; 使用 nmake 或下列命令進行編譯和鏈接:
; ml /c /coff Receive.asm
; Link /subsystem:windows Receive.obj
.386
.model flat,stdcall
option casemap:none
;include 文件定義
include C:/masm32/include/windows.inc
include C:/masm32/include/gdi32.inc
includelib C:/masm32/lib/gdi32.lib
include C:/masm32/include/user32.inc
includelib C:/masm32/lib/user32.lib
include C:/masm32/include/kernel32.inc
includelib C:/masm32/lib/kernel32.lib;數據段
.data? ;未初始化的數據
hInstance dword ?
hWinMain dword ?
szBuffer byte 512 dup(?)
.const ;常量數據
szClassName byte 'MyClass', 0
szCaptionMain byte 'Receive Message', 0
szReceive byte 'Receive WM_SETTEXT message', 0dh, 0ahbyte 'param:%08x', 0dh, 0ah byte 'text: "%s"', 0dh, 0ah,0; 代碼段
.code
;窗口過程
_ProcWinMain proc uses ebx edi esi, hWnd, uMsg, wParam, lParammov eax, uMsg .if eax == WM_CLOSEinvoke DestroyWindow, hWinMaininvoke PostQuitMessage, NULL ;收到 WM_SETTEXT 消息則將消息字符串和字符串地址顯示出來.elseif eax == WM_SETTEXT invoke wsprintf, addr szBuffer, addr szReceive, lParam, lParam invoke MessageBox, hWnd, offset szBuffer, addr szCaptionMain, MB_OK .else invoke DefWindowProc, hWnd, uMsg, wParam, lParam ret .endifxor eax, eax ret
_ProcWinMain endp _WinMain proc local @stWndClass:WNDCLASSEXlocal @stMsg:MSG invoke GetModuleHandle, NULL mov hInstance, eax 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 + 1mov @stWndClass.lpszClassName, offset szClassNameinvoke RegisterClassEx, addr @stWndClass ;建立并顯示窗口invoke CreateWindowEx, WS_EX_CLIENTEDGE, offset szClassName, offset szCaptionMain, \WS_OVERLAPPEDWINDOW, \50, 50, 200, 150, \NULL, NULL, hInstance, NULL mov hWinMain, eax invoke ShowWindow, hWinMain, SW_SHOWNORMALinvoke UpdateWindow, hWinMain ;消息循環.while TRUEinvoke GetMessage, addr @stMsg, NULL, 0, 0.break .if eax == 0invoke TranslateMessage, addr @stMsg invoke DispatchMessage, addr @stMsg .endw ret
_WinMain endp main proc call _WinMain invoke ExitProcess, 0
main endp
end main
接收程序寫好了,現在來寫一個發送程序,如下所示:
;Send.asm 從一個程序向另一個窗口程序發送消息 之 [發送程序]
; 使用 nmake 或下列命令進行編譯和鏈接:
; ml /c /coff Send.asm
; Link /subsystem:windows Send.obj
.386
.model flat,stdcall
option casemap:none
;include 文件定義
include C:/masm32/include/windows.inc
include C:/masm32/include/gdi32.inc
includelib C:/masm32/lib/gdi32.lib
include C:/masm32/include/user32.inc
includelib C:/masm32/lib/user32.lib
include C:/masm32/include/kernel32.inc
includelib C:/masm32/lib/kernel32.lib;數據段
.data? ;未初始化的數據
hWnd dword ?
szBuffer byte 256 dup(?)
.const ;常量數據
szCaption byte 'SendMessage', 0
szStart byte 'Press OK to start SendMessage, param: %08x!', 0
szReturn byte 'SendMessage returned!', 0
szDestClass byte 'MyClass', 0
szText byte 'Text send to other windows', 0
szNotFound byte 'Receive Message Window not found!', 0; 代碼段
.code
main proc invoke FindWindow, addr szDestClass, NULL.if eaxmov hWnd, eax invoke wsprintf, addr szBuffer, addr szStart, addr szText invoke MessageBox, NULL, offset szBuffer, \offset szCaption, MB_OKinvoke SendMessage, hWnd, WM_SETTEXT, 0, addr szTextinvoke MessageBox, NULL, offset szReturn, \offset szCaption, MB_OK .elseinvoke MessageBox, NULL, offset szNotFound, \offset szCaption, MB_OK .endifinvoke ExitProcess, 0
main endp
end main
分別編譯兩個程序,生成Receive.exe和Send.exe.
在這個程序中首先用FindWindow函數找到接收窗口的窗口句柄,FindWindow函數的使用方法是:
invoke FindWindow,lpClassName,lpWindowName.if eaxmov hWin,eax.endif
兩個參數都指向字符串,lpClassName指向需要尋找的窗口的窗口類,lpWindowName指向需要尋找窗口的窗口標題,如果目標窗口存在的話,函數的返回值是找到的窗口句柄,否則函數返回0。
用接收窗口的窗口類當做參數尋找窗口,如果沒有找到,顯示“Receive Message Window not found”,找到的話則把“Text send to other windows”字符串的地址當做WM_SETTEXT消息的參數用SendMessage發送給接收窗口。兩個程序的源代碼可以在所附帶光盤的Chapter04\SendMessage目錄中找到。
好!現在發送開始,首先執行Receive.exe,窗口出來了,然后執行Send.exe,屏幕上出現一個對話框:Press OK to start SendMessage, param: 00402072,表示在Send程序中字符串的地址是00402072h,現在單擊“確定”按鈕執行SendMessage函數,單擊后對話框消失,但接收程序顯示出了一個對話框,內容為:
Receive WM_SETTEXT message
param: 0012ff1c (注:該地址在具體執行的時候可能有所不同)
text: "Text send to other windows"
點擊【確定】按鈕
可見字符串是正確地傳了過來,但地址卻不是發送程序的00402072h,這是怎么回事呢?
其實Windows在處理SendMessage的時候要檢查消息的類型,并對不同的消息做不同的處理,當消息的參數是一個普通的32位數時,僅僅將該數值傳遞給目標窗口過程;而當消息的參數是一個指針的時候,Windows對指針指向的內容進行了一些處理,以便數據能夠正常地傳遞到目標進程中。
Windows首先創建一塊共享內存,并將WM_SETTEXT消息lParam指向的字符串復制到該內存中,然后再發送消息到其他進程,并將共享內存在目標進程中的地址發送給目標窗口過程,目標窗口過程處理完消息后,函數返回,共享內存被釋放。共享內存使用的是第10章介紹的內存映射文件技術,相當于用圖1.6的方法將同一塊物理內存映射到不同進程的不同線性地址上去。
雖然當消息傳遞到目標窗口過程的時候lParam的取值會有所變化,但在WM_SETTEXT消息中,lParam的數值是多少并不重要,重要的是它指向的字符串是否正確。
最后,單擊Receive程序中的“確定”按鈕,Send程序馬上會彈出一個消息框并顯示:SendMessage returned,這是SendMessage函數告訴我們:我回來了!
在用戶自定義的消息中(WM_USER等),不要在消息參數中傳遞指針,這只會引發非法訪問內存,因為Windows不知道用戶的意圖,它只會把lParam和wParam當兩個普通的數值傳遞,而不會幫用戶把指針指向的內容復制到一塊共享內存中。
4.3.2 在窗口間傳遞數據
在WM_SETTEXT這一類的消息中,Windows可以將參數所指的字符串傳遞到目標窗口過程中,但是這些消息都有它們的本職工作,并且傳遞的數據也只限于以0結尾的字符串。為了能夠在不同進程的窗口間自由地拷貝任意類型的數據,Windows提供了一個特殊的窗口消息——WM_COPYDATA。
WM_COPYDATA消息用一個COPYDATASTRUCT結構來描述要拷貝的數據的長度和位置:
COPYDATASTRUCT STRUCTdwData DWORD ? ; 附加字段cbData DWORD ? ; 數據長度lpData DWORD ? ; 數據位置指針
COPYDATASTRUCT ENDS
其中的dwData字段是一個備用的字段,可以存放任何值,例如,讀者有可能向另外的進程發送數據的同時用一個數字來說明數據的類型,那么就可以把這個字段用上去;cbData字段規定了發送的字節數,lpData字段是指向待發送數據的指針。填充好數據結構后,用SendMessage函數就可以將數據發送給目標窗口過程:
.data
stCopyData COPYDATASTRUCT <>
.code...invoke SendMessage,hDestWnd,WM_COPYDATA,hWnd,addr stCopyData
例句中的hDestWnd為目標窗口句柄;wParam指定為hWnd,是當前窗口的句柄;lParam指向已經填充完畢的COPYDATASTRUCT結構。
Windows收到WM_COPYDATA消息后,會根據cbData字段的長度創建一塊共享內存,并把lpData所指的數據拷貝到共享內存中,然后定位該共享內存在目標進程中的地址,把該地址作為新的地址添加到COPYDATASTRUCT結構的lpData字段中,最后將經過處理的COPYDATASTRUCT結構發送給目標窗口過程,目標窗口過程就可以根據結構中的字段來定位數據了。目標窗口過程返回后,Windows釋放掉共享內存,SendMessage函數返回。
光盤Chapter04\SendMessage-1目錄中的源代碼演示了WM_COPYDATA消息的使用方法,讀者可自行對比該例子和上一個例子的區別。
【學習筆記】
;Receive.asm 從一個程序向另一個窗口程序發送消息 [之消息接收程序]
; 使用 nmake 或下列命令進行編譯和鏈接:
; ml /c /coff Receive.asm
; Link /subsystem:windows Receive.obj
.386
.model flat,stdcall
option casemap:none
;include 文件定義
include C:/masm32/include/windows.inc
include C:/masm32/include/gdi32.inc
includelib C:/masm32/lib/gdi32.lib
include C:/masm32/include/user32.inc
includelib C:/masm32/lib/user32.lib
include C:/masm32/include/kernel32.inc
includelib C:/masm32/lib/kernel32.lib ;數據段
.data?
hInstance dword ?
hWinMain dword ?
szBuffer byte 512 dup(?)
.const
szClassName byte 'MyClass', 0
szCaptionMain byte 'Receive Message', 0
szReceive byte 'Receive WM_COPYDATA message', 0dh, 0ahbyte 'length:%08x', 0dh, 0ah byte 'text address: %08x', 0dh, 0ah byte 'text: "%s"', 0dh, 0ah, 0;代碼段
.code
;窗口過程
_ProcWinMain proc uses ebx edi esi, hWnd, uMsg, wParam, lParam mov eax, uMsg .if eax == WM_CLOSEinvoke DestroyWindow, hWinMain invoke PostQuitMessage, NULL ;收到 WM_COPYDATA 消息將消息附帶的數據長度和字符串數據顯示出來 .elseif eax == WM_COPYDATA mov eax, lParam assume eax:ptr COPYDATASTRUCT invoke wsprintf, addr szBuffer, addr szReceive, \[eax].cbData, [eax].lpData, [eax].lpDatainvoke MessageBox, hWnd, offset szBuffer, addr szCaptionMain, MB_OK assume eax:nothing.else invoke DefWindowProc, hWnd, uMsg, wParam, lParam ret.endifxor eax, eax ret
_ProcWinMain endp _WinMain proclocal @stWndClass:WNDCLASSEX local @stMsg:MSG invoke GetModuleHandle, NULL mov hInstance, eax 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 + 1mov @stWndClass.lpszClassName, offset szClassName invoke RegisterClassEx, addr @stWndClass ;建立并顯示窗口invoke CreateWindowEx, WS_EX_CLIENTEDGE or WS_EX_TOPMOST, offset szClassName, offset szCaptionMain, \WS_OVERLAPPEDWINDOW, \50, 50, 200, 150, \NULL, NULL, hInstance, NULL mov hWinMain, eax invoke ShowWindow, hWinMain, SW_SHOWNORMAL invoke UpdateWindow, hWinMain ;消息循環.while TRUE invoke GetMessage, addr @stMsg, NULL, 0, 0.break .if eax == 0invoke TranslateMessage, addr @stMsg invoke DispatchMessage, addr @stMsg .endwret
_WinMain endp main proc call _WinMain invoke ExitProcess, 0
main endp
end main
發送方源碼:
;Send.asm 從一個程序向另一個窗口程序發送消息 之 [發送程序]
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;使用 nmake 或下列命令進行編譯和鏈接:
;ml /c /coff Send.asm
;Link /subsystem:windows Send.obj
.386
.model flat,stdcall
option casemap:none
;include 文件定義
include C:/masm32/include/windows.inc
include C:/masm32/include/user32.inc
includelib C:/masm32/lib/user32.lib
include C:/masm32/include/kernel32.inc
includelib C:/masm32/lib/kernel32.lib ;數據段
.data
hWnd dword ?
szBuffer byte 512 dup(?)
stCopyData COPYDATASTRUCT <>
szCaption byte 'SendMessage', 0
szStart byte 'Press OK to start SendMessage, text address:%08x!', 0
szReturn byte 'SendMessage returned!', 0
szDestClass byte 'MyClass', 0 ;目標窗口的窗口類
szText byte 'Text send to other windows', 0
szNotFound byte 'Receive Message Window not found!', 0;代碼段
.code
main proc invoke FindWindow, addr szDestClass, NULL .if eax mov hWnd, eax ;找到目標窗口則發送消息invoke wsprintf, addr szBuffer, addr szStart, addr szText invoke MessageBox, NULL, offset szBuffer, offset szCaption, MB_OK mov stCopyData.cbData, sizeof szText mov stCopyData.lpData, offset szText invoke SendMessage, hWnd, WM_COPYDATA, 0, addr stCopyData invoke MessageBox, NULL, offset szReturn, offset szCaption, MB_OK .else invoke MessageBox, NULL, offset szNotFound, offset szCaption, MB_OK .endifinvoke ExitProcess, 0
main endp
end main
編譯運行,先運行Receive.exe,再運行Send.exe
點擊【確定】按鈕
4.3.3 SendMessage和PostMessage函數的區別
從邏輯上看,SendMessage函數相當于直接調用其他窗口的窗口過程來處理某個消息,并等待窗口過程的返回,在函數返回后,目標窗口過程必定已經處理了該消息。PostMessage函數則將消息放入目標窗口的消息隊列中并直接返回,函數返回后,目標窗口過程可能還沒有處理到該消息。
對于普通的消息來說,兩個函數除了在處理速度上有所區別外,其他的表現都一模一樣,但是對于WM_SETTEXT,WM_COPYDATA等在參數中用到指針的消息來說,兩者就有所不同了。讀者可以嘗試將前面例子中的SendMessage函數改為PostMessage函數,就會發現Receive程序根本不會接收到WM_SETTEXT或者WM_COPYDATA消息。事實上,當消息參數中用到指針時,用PostMessage函數來發送消息都不會成功,PostMessage的參考文檔中明確地說明,該函數不能用于任何參數中用到指針的消息。