窗口是什么?大家每天在使用Windows,屏幕上的一個個方塊就是一個個窗口!那么,窗口為什么是這個樣子呢?窗口就是程序嗎?
回想DOS時代的計算屏幕,在1990年Windows 3.0推出之前,計算機的屏幕一直使用文本模式,黑洞洞的底色上漂浮著白色的小字。對DOS程序來說,屏幕是唯一的,上面有光標表示輸入字符的位置,程序運行后往屏幕輸出一些信息,退出時輸出的信息就留在了屏幕上,然后是第二個程序重復這個過程,當屏幕被寫滿的時候,整個屏幕上卷一行,最上面一行被去掉,然后程序在最底下新空出來的一行上繼續輸出。
對于一個單任務的操作系統,這種方式是很合理的,因為平時使用傳真機或打字機就是用上卷的方式來容納新的內容的。但是如果是多任務呢?兩個程序同時往屏幕上寫東西或者兩個人同時往打字機上打字,那么誰都看不懂混在一起的是什么。DOS下的TSR(內存駐留)程序是多個程序同時使用一個屏幕的例子,但實質上這并不是多任務,而是TSR將別的程序暫時掛起,掛起的程序不可能在TSR執行期間再向屏幕輸出內容,TSR在輸出自己的內容之前必須保存屏幕上顯示的內容,并在退出的時候把屏幕恢復原來的樣子,否則掛起的程序并不知道屏幕已經被改變,在這個過程中,DOS不會去干預中間發生的一切。
Windows是多任務的操作系統,可以同時運行多個程序,同樣,各個程序在屏幕上的顯示不能互相干擾,而且,多個程序可以看成是“同時”運行的,在后臺的程序也可能隨時向屏幕輸出內容,這中間的高度是由Windows完成的。Windows采用的方法是給程序一塊矩形的屏幕空間,這就是窗口。應用程序通過Windows向屬于自己的窗口顯示信息,Windows判斷該窗口是不是被別的窗口擋住,并把沒有擋住的部分輸出到屏幕上,這樣屏幕上顯示的東西就不會互相覆蓋而亂套。對于應用程序來說,它只需認為窗口就是自己擁有的顯示空間就可以了。
窗口和程序的關系
既然不同窗口的內容就是不同程序的輸出,那么一個窗口就是一個程序嗎?反過來,一個程序就是一個窗口嗎?
答案是否定的,一個窗口不一定就是一個程序,它可能只是一個程序的一部分。一個程序可以建立多個頂層窗口,如Windows的桌面和任務欄都是頂層窗口,但它們都屬于“文件管理器”進程,所以并不是一個窗口就是一個程序的代表。Windows的窗口采用層次結構,一個窗口中可以建立多個子窗口,如窗口中的狀態欄,工具欄,對話框中的按鈕,文本輸入框與復選框等都是子窗口。子窗口中還可以建立下一級子窗口,如Word工具欄上的字體選擇框。
反過來,運行的程序并非一定就是窗口,比如悄悄在后臺運行的木馬程序就不會顯示一個窗口向用戶報告它在干什么。在Windows NT下用“任務管理器”查看,進程的數量比屏幕上的窗口多得多,意味著很多的運行程序并沒有顯示窗口。如果一個程序不想和用戶交互,它可以選擇不建立窗口。
所以本章的標題“第一個窗口程序”,指學習編寫第一個以標準的窗口為界面的程序,而不是泛指Windows程序。如果要寫的Win32程序不是以窗口為界面的,如控制臺程序等,就不一定采用本章中提及的以消息驅動的程序結構。
雖然以窗口為界面的程序并不是所有Windows程序的必須選擇,但絕大部分的應用程序是以這種方式出現的,從操作系統的名稱“Windows”就可以看出這一點,了解窗口程序就是相當于在了解Windows工作方式的基礎。
窗口界面
大部分的窗口看上去都是大同小異。
典型的窗口,即Windows附帶的寫字板程序窗口。
窗口一般由屏幕上的矩形區域組成,不同的窗口可能包括一些相同的組成部分,如標題欄、菜單、工具欄、邊框和狀態欄等,每個部分都有自己固定的行為模式:
·窗口邊框:窗口的外沿就是窗口邊框,用鼠標按住邊框并拖動可以調整窗口的大小。
·標題欄:窗口的最上面是標題欄,用鼠標按住標題欄拖動可以移動窗口,雙擊標題欄則將窗口最大化或從最大化的狀態恢復。通過標題欄的顏色可以區分窗口是不是活動窗口,同時標題欄列出了應用程序的名稱。
·菜單:標題欄下面是菜單,單擊菜單會彈出各種功能選擇。
·工具欄:菜單的下面是工具欄,工具欄上用圖標的方式列出最常用的功能,相當于菜單的快捷方式。
·圖標和最小化、最大化與關閉按鈕:圖標位于標題欄的左邊,三個控制按鈕則位于標題欄的右邊。單擊圖標會彈出一個系統菜單,雙擊圖標則相當于按下了關閉按鈕。最小化、最大化按鈕用來控制窗口的大小。
·狀態欄:位于窗口的最下面,用來顯示一些狀態信息。
·客戶區:窗口中間用來工作或輸出的區域,叫做窗口的客戶區,把窗口看做是一張白紙的話,客戶區就是白紙中真正用來寫東西的區域,程序在這里和用戶進行交互。
·滾動條:如果客戶區太小不足以顯示全部內容,右邊或底部可能還有滾動條,拖動它可以滾動窗口的客戶區,以便看到其他的內容。
雖然大部分的窗口看上去都差不多,但并不是每個窗口都有這些東西,也許有的窗口就沒有圖標和最小化、最大化框,有的沒有工具欄或狀態欄,有的沒有標題欄,而有的就干脆是個奇怪的形狀,如Office幫助中助手,那些小狗小貓都些不折不扣的窗口,Windows的桌面和桌面下面的任務欄也都是窗口,就連屏幕保護的黑屏幕也是一個大小為整個屏幕、沒有標題欄和邊框的窗口!
一致的窗口形狀和行為模式為Windows用戶提供了一致的用戶界面,幾乎所有的窗口程序都在菜單的第一欄設置有關文件的操作和退出功能、最后一欄設置程序的幫助,相同的功能在工具欄上的圖標也是大同小蒸發量的,用戶可以不再像在DOS下那樣,對不同的程序需要學習不同的界面,用戶自從學會使用第一個軟件起,就基本學會了所有Windows軟件的使用模式,而且可以通過相似的菜單、工具欄等來發掘程序的新功能。窗口的菜單和客戶區則是最個性化的東西,菜單隨程序的不同而不同,而客戶區則是窗口程序的輸出區域,不同的程序在客戶區內顯示了不同的內容。
窗口程序是怎么工作的
窗口程序的運行模式
對程序員來說,要了解的不僅是用戶可以看到的部分,還必須了解窗口底下的東西,了解用怎樣的程序結構來實現窗口的行為模式。
DOS程序員熟悉的是順序化的、按過程驅動的程序設計方法。程序有明顯的開始、明顯的過程和明顯的結束,由程序運行的階段來決定用戶該做什么。而窗口程序是事件驅動的(再次提醒:這里是“窗口程序”,而不是“Windows程序”,因為和窗口有關的程序者事件驅動的,其他的Windows程序可能并不這樣工作,如控制臺程序的結構還是同DOS程序一樣是順序化的,但與窗口相關的Windows程序占了絕大多數,所以大部分的書籍中講到Windows程序就認為是事件驅動的程序),用戶可能隨時發出各種消息,如操作的過程中覺得窗口不夠大了,就馬上會拖動邊框,程序必須馬上調整客戶區的內容以適應新的窗口大小;用戶覺得想先干別的事情,可能會把窗口最小化,關閉按鈕也有可能隨時被按下,這意味著程序要隨時可以處理退出的請求。如果非要規定干活的時候不能移動窗口與調整大小,那么這些窗口就會呆在桌面上一動不動。
窗口程序在結構上和DOS程序有很大的不同,窗口程序實現大部分功能的代碼應該呆在同一個模塊——消息處理模塊,這個模塊可以隨時應付所有類型的消息,只有這樣才能隨時響應用戶的各種操作。
下面是地地道道的Win32匯編窗口程序:
FirstWindow源代碼。
.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 dd ?
hWinMain dd ?
.const
szClassName db 'MyClass',0
szCaptionMain db 'My first Window!',0
szText db 'Win32 Assembly, Simple and powerful!',0
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 代碼段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
.code
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 窗口過程
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_ProcWinMain proc uses ebx edi esi, hWnd, uMsg, wParam, lParam
local @stPs:PAINTSTRUCT
local @stRect:RECT
local @hDc
mov eax,uMsg
;*************************************************************************
.if eax == WM_PAINT
invoke BeginPaint,hWnd,addr @stPs
mov @hDc,eax
invoke GetClientRect,hWnd,addr @stRect
invoke DrawText,@hDc,addr szText,-1, \
addr @stRect, \
DT_SINGLELINE or DT_CENTER or DT_VCENTER
invoke EndPaint, hWnd, addr @stPs
;***************************************************************************
.elseif eax == WM_CLOSE
invoke DestroyWindow,hWinMain
invoke PostQuitMessage,NULL
;***************************************************************************
.else
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.endif
;***************************************************************************
xor eax,eax
ret
_ProcWinMain endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_WinMain proc
local @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 + 1
mov @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_SHOWNORMAL
invoke UpdateWindow,hWinMain
;**************************************************************************
; 消息循環
;**************************************************************************
.while TRUE
invoke GetMessage, addr @stMsg, NULL, 0, 0
.break .if eax == 0
invoke TranslateMessage, addr @stMsg
invoke DispatchMessage, addr @stMsg
.endw
ret
_WinMain endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
start:
call _WinMain
invoke ExitProcess, NULL
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
end start
編譯運行后,窗口出來了,對于這個窗口,用戶可以拖動邊框去改變大小、按標題欄上的按鈕來最大化和最小化,當光標到邊框的時候,會自動變成箭頭,總之,這個窗口包括了一個典型窗口的所有特征。
接下來開始分析源代碼,看了這三頁多的源代碼,第一個感覺是什么?是不是想撤退了?90%的人有同樣的感覺,別急,過了這一關,Win32匯編的入門就成功了一半了,所以千萬要挺住!有個振奮人心的消息是,這個程序是大部分窗口程序的模板,以后要寫一個新的程序,把它拷貝過來再往中間添磚加瓦就是了,功夫一點都不白費。
先靜下心來分析一下程序的結構,首先是注釋,模式定義,include,.data數據段,.code代碼段。
程序入口是start,然后執行了一下_WinMain子程序,完成后就是程序退出的函數ExitProcess,再看_WinMain的結構,前面是順序下來的幾個API調用:
GetMoudleHandle à RelZeroMemory à LoadCursor à RegisterClassEx à CreateWindowEx à ShowWindow à UpdateWindow
從名稱上就能看出它們的用途,很明顯,窗口是在CreateWindowEx處建立的,ShowWindow則是把窗口顯示在屏幕上,這些代碼是窗口的建立過程。
接下來,就是一個由3個API組成的循環了:
GetMessage à TranslateMessage à DispatchMessage
很明顯,這是和消息有關的循環,因為API名稱中都帶有Message字樣,如果退出這個循環,程序也就結束了,這個循環叫做消息循環。
設置_WinMain子程序并不是必須的,可以把_WinMain的所有代碼放到主程序中,沒有任何影響,之所以這樣只是為將這里使用的變量定義成局部變量,這樣可以方便移植。
看了程序的流程,似乎沒有什么地方涉及窗口的行為,如改變大小和移動位置的處理等。
再看源程序,除了_WinMain,還有一個子程序_ProcWinMain,但除了在WNDCLASSEX結構的賦值中提到過它,好像就沒有什么地方要用到這個子程序,起碼在自己編寫的源代碼中沒有任何一個地方調用過它。
再看ProWinMain,它是一個分支結構處理的子程序,功能是把參數uMsg取出來,根據不同的uMsg執行不同的代碼,完了以后就退出了,中間也沒有任何東西和主程序有關聯。
第一個窗口程序就是由這么兩個似乎是風馬牛不相及的部分組成的,但它確實能工作,對于寫慣了DOS匯編的程序員來說,這似乎不可理解。下面來看看這么一個陌生而奇怪的程序是如何工作的。
窗口程序的運行過程
在屏幕上顯示一個窗口的過程一般有以下步驟,這就是主程序的結構流程:
1)得到應用程序的句柄(GetMoudleHandle)。
2)注冊窗口類(RegisterClassEx)。在注冊之前,要先填寫RegisterClassEx的參數WNDCLASSEX結構。
3)建立窗口(CreateWindowEx)。
4)顯示窗口(ShowWindow)。
5)刷新窗口客戶區(UpdateWindow)。
6)進入無限的消息獲取和處理的循環。首先獲取消息(GetMessage),如果有消息到達,則將消息分派到回調函數處理(DispatchMessage),如果消息是WM_QUIT,則退出循環。
程序的另一半_ProcWinMain子程序是用來處理消息的,它就是窗口的回調函數(CallBack),也叫做窗口過程,之所以是回調函數是因為它是由Windows而不是我們自己調用的,我們調用DispatchMessage,而DispatchMessage再回過來調用窗口過程。
所有的用戶操作都是通過消息來傳給應用程序的,如用戶按鍵,鼠標移動,選擇了菜單和拖動了窗口等,應用程序中由窗口過程接收消息并處理,在例子程序中就是_ProcWinMain。窗口過程構造了一個分支結構,對應不同的消息執行不同的代碼,所以一個應用程序中幾乎所有的功能代碼都集中在窗口過程里。
Windows在系統內部有一個系統消息隊列,當輸入設備有所動作的時候,如用戶按動了鍵盤、移動了鼠標,按下或放開了鼠標等,Windows都會產生相應的記錄放在系統消息隊列里,每個記錄中包含消息的類型、發生的位置(如鼠標在什么坐標移動)和發生的時間等信息。
同時,Windows為每個程序(嚴格地說是每個線程)維護一個消息隊列,Windows檢查系統消息隊列里消息的發生位置,當位置位于某個應用程序的窗口范圍內的時候,就把這個消息派送到應用程序的消息隊列里。
當應用程序還沒有來取消息的時候,消息就暫時保留在消息隊里,當程序中的消息循環執行到GetMessage的時候,控制權轉移到GetMessage所在的USER32.DLL中,USER32.DLL從程序消息隊列中取出一條消息,然后把這條消息返回應用程序。
應用程序可以對這條消息進行預處理,如可以用TranslateMessage把基于鍵盤掃描碼的按鍵消息轉換成基于ASCII碼的鍵盤消息,以后也會用到TranslateAccelerator把鍵盤快捷鍵轉換成命令消息,但這個步驟不是必需的。
然后應用程序將處理這個消息,但方法不是自己直接調用窗口過程來完成,而是通過DispatchMessage間接調用窗口過程,Dispatch的英文含義是“分派”,之所以是“分派”,是因為一個程序可能建有不止一個窗口,不同的窗口消息必須分派給相應的窗口過程。當控制權轉移到USER32.DLL中的DispatchMessage時,DispatchMessage找出消息對應窗口的窗口過程,然后把消息的具體信息當做參數來調用它,窗口過程根據消息找到對應的分支去處理,然后返回,這時控制權回到DispatchMessage,最后DispatchMessage函數返回應用程序。這樣,一個循環就結束了,程序又開始新一輪的GetMessage。
窗口程序 à GetMessage(USER32.DLL) –> TranslateMessage(USER32.DLL) –> DispatchMessage(USER32.DLL) à 窗口過程 à USER32.DLL à 窗口程序
為什么要由Windows來調用窗口過程,程序取了消自以后自己處理不是更簡便嗎?事實上并非如此,如果程序自己處理消息的“分派”,就必須自己維護本程序所屬窗口的列表,當程序建立的窗口不止一個的時候,這個工作就變得復雜起來;另一個原因是:別的程序也可能用SendMessage通過Windows直接調用你的窗口過程;第三個原因:Windows并不是把所有的消息都放進消息隊列,有的消息是直接調用窗口過程處理的,如WM_SETCURSOR等實時性很強的消息,所以窗口過程必須開放給Windows。
應用程序之間也可以互發消息,PostMessage是把一個消息放到其他程序的消息隊列中,目標程序收到了這個條消息就把它放入該程序的消息隊列去處理;而SendMessage則越過消息隊列直接調用目標程序的窗口過程,窗口過程返回以后才從SendMessage返回。
窗口過程是由Windows回調的,Windows又是怎么知道往哪里回調呢?答案是我們在調用RegisterClassEx函數的時候告訴了Windows。