Windows訊息機制:
眾所周知,Windows系統是靠訊息來驅動的。一個應用程式內部的運行不是靠順序,而是靠事件的觸發來控制,而各個事件與組件間的通訊就是靠訊息來完成的。
Windows是一個多任務的作業系統,在同一時刻,系統中有著多個應用程式的執行個體在運行。在這樣的一個作業系統中,不可能像過去的DOS那樣,由一個應用程式來享用所有的系統資源,這些資源是由Windows統一管理的。那麼,系統如何協調各個應用執行個體的運行,如何為各個應用執行個體分配CPU時間呢?如何相應使用者的輸入呢?事實上,Windows時刻監視著使用者的一舉一動,並分析使用者的動作與哪一個應用程式相關,然後,將使用者的動作以訊息的形式發送到系統中的訊息佇列,這個訊息佇列就是應用程式正常啟動並執行基礎,也是一條紐帶,將應用程式中各個部分串連成一個整體來完成特定的任務。直到應用程式終止它會不停的檢測系統的訊息佇列,對隊列中未處理的訊息進行分析,根據訊息所包含的內容採取適當的動作來響應使用者所作的操作。
舉個簡單的例子:
假設我們的應用程式的某個表單有個File菜單,那麼,在運行該應用程式的時候,如果使用者單擊了File菜單,這個動作將被Windows (而不是應用程式本身!)所捕獲,Windows經過分析得知這個動作應該由上面所說的那個應用程式去處理,因此,Windows就發送了個叫做WM_COMMAND的訊息給應用程式,該訊息所包含的資訊告訴應用程式:“使用者單擊了File菜單”,應用程式得知這一訊息之後,採取相應的動作來響應它,在VB中預設會去執行File Click事件。這個過程被稱為訊息處理。Windows為每一個應用程式(確切地說是每一個線程)維護了相應的訊息佇列,應用程式的任務就是不停的從它的訊息佇列中擷取訊息,分析訊息和處理訊息,直到一條接到叫做WM_QUIT訊息為止,這個過程通常是由一種叫做訊息迴圈的程式結構來實現的。
Do While GetMessage(wMsg, 0&, 0&, 0&)
Call TranslateMessage(wMsg)’翻譯訊息
Call DispatchMessage(wMsg)’ 撤去訊息
Loop
其中wMsg就是一個訊息結構,它可以這樣定義:
Public Type Msg
hwnd As Long
message As Long
wParam As Long
lParam As Long
time As Long
pt As POINTAPI
End Type
參數1:hwnd是訊息要發送到的那個視窗的控制代碼,這個視窗就是咱們用CreateWindows函數建立的那一個。如果是在一個有多個視窗的應用程式中,用這個參數就可決定讓哪個視窗接收訊息。
參數2:message是一個數字,它唯一標識了一種訊息類型。每種訊息類型都在Windows檔案中定義了,這些常量都以WM_開始後面帶一些描述了訊息特性的名稱。比如說當應用程式退出時,Windows就嚮應用程式發送一條WM_QUIT訊息。
參數3:一個32位的訊息參數,這個值的確切意義取決於訊息本身。
參數4:同上。
參數5:訊息放入訊息佇列中的時間,在這個域中寫入的並不是日期,而是從Windows啟動後所測量的時間值。Windows用這個域來使用訊息保持正確的順序。
參數6:訊息放入訊息佇列時的滑鼠座標.
訊息迴圈以GetMessage調用開始,它從訊息佇列中取出一個訊息:
GetMessage(&msg,NULL,0,0),第一個參數是要接收訊息的MSG結構的地址,第二個參數表示視窗控制代碼,NULL則表示要擷取該應用程式建立的所有視窗的訊息;第三,四參數指定訊息範圍。後面三個參數被設定為預設值,這就是說你打算接收發送到屬於這個應用程式的任何一個視窗的所有訊息。在接收到除WM_QUIT之外的任何一個訊息後,GetMessage()都返回TRUE。如果GetMessage收到一個WM_QUIT訊息,則返回FALSE,如收到其他訊息,則返回TRUE。因此,在接收到WM_QUIT之前,帶有GetMessage()的訊息迴圈可以一直迴圈下去。只有當收到的訊息是WM_QUIT時,GetMessage才返回FALSE,結束訊息迴圈,從而終止應用程式。 均為NULL時就表示擷取所有訊息。
訊息用GetMessage讀入後(注意這個訊息可不是WM_QUIT訊息),它首先要經過函數TranslateMessage()進行翻譯,這個函數會轉換成一些鍵盤訊息,它檢索匹配的WM_KEYDOWN和WM_KEYUP訊息,並為視窗產生相應的ASCII字元訊息(WM_CHAR),它包含指定鍵的ANSI字元.但對大多數訊息來說它並不起什麼作用,所以現在沒有必要考慮它。
下一個函數調用DispatchMessage()要求Windows將訊息傳送給在MSG結構中為視窗所指定的視窗過程。我們在講到登記視窗類別時曾提到過,登記視窗類別時,我們曾指定Windows把函數WindosProc作為咱們這個視窗的視窗過程(就是指處理這個訊息的東東)。就是說,Windows會調用函數WindowsProc()來處理這個訊息。在WindowProc()處理完訊息後,代碼又迴圈到開始去接收另一個訊息,這樣就完成了一個訊息迴圈。
因此,從某種角度上來看,Windows應用程式是由一系列的訊息處理代碼來實現的。這和傳統的過程式編程方法很不一樣,編程者只能夠預測使用者所利用應用程式使用介面物件所進行的操作以及為這些操作編寫處理代碼,卻不可以這些操作在什麼時候發生或者是以什麼順序來發生,也就是說,我們不可能知道什麼訊息會在什麼時候以什麼順序來臨。那麼windows是如何解決這個問題的呢?windows採用一種叫做回呼函數(callback function)的特殊函數,這個函數由Windows直接調用。實際上每個視窗類別都必須有一個回呼函數。在Windows中訊息迴圈和視窗類別的回呼函數已經都被封裝起來,我們一般情況不會接觸,如果我們想重新註冊視窗過程函數WindowProc(就是這個回呼函數),我們必須使用子類(Subclass)的技術。
(這部分說的可能比較多,而且都是開發Windows應用程式的基礎部分(基礎的東西雖然難度不大,但是非常重要)。但是因為對於部分VB程式員來說可能接觸不多,因此說的多了點。)
子類(Subclass):
按照上文提到的因為對於正常的VB通常不能直接處理Windows系統的訊息,但是我們可以通過子類的方法截獲Windows訊息並且自訂其處理方法(而如果想截獲其他應用程式的訊息就需要使用鉤子(Hook)技術)。
應用程式可以用過SetWindowLong API 函數為具有視窗控制代碼(hWnd)的表單、控制項或其他對象安裝新的訊息處理(Message handler)過程函數WindowProc。這個新的WindowProc過程必須被定義在模組(.BAS)檔案中。
Private Sub Form_Load()
OldWindowProc = SetWindowLong( _
hwnd, GWL_WNDPROC, _
AddressOf NewWindowProc)
End Sub
現在,如果表單收到Windows訊息,系統將調用新的WindowProc 過程(NewWindowProc),這個新的視窗過程函數將檢查當前的訊息行為是否被指定,如果沒有指定具體的行為,將被傳遞給源視窗過程函數WindowProc,有源WindowProc進行預設的處理。這個過程是非常重要的,否則因為當前視窗可能會因為訊息遺失,造成不能進行重繪、更新等其他視窗預設的標準行為。而且新的視窗過程必須返回源過程函數返回的結果。
下面用一個實際代碼例子示範處理WM_SYSCOMMAND訊息的過程:
WM_SYSCOMMAND: 當使用者選擇“視窗菜單”的一條命令是觸發。
Public Function NewWindowProc( _
ByVal hwnd As Long, ByVal msg As Long, _
ByVal wParam As Long, lParam As WINDOWPOS) As Long
Const WM_SYSCOMMAND = &H112
Const SC_SIZE = &HF000&
' 檢查是否是WM_SYSCOMMAND訊息
If msg = WM_SYSCOMMAND Then
' 如果收到的訊息是WM_SYSCOMMAND ,進一步檢查命令參數是否是SC_SIZE, 如果是就忽略它,不進行任何處理。
If (wParam And &HFFF0) = SC_SIZE Then Exit Function
End If
'*其餘的訊息傳遞給源視窗過程函數*非常重要
NewWindowProc = CallWindowProc( _
OldWindowProc, hwnd, msg, wParam, _
lParam)
End Function
上面的過程函數首先檢查收到的訊息是否是WM_SYSCOMMAND訊息,如果是,那麼再進一步檢查參數(wParam)是否是SC_SIZE命令。如果是表示表單想要調整大小。但是我們自訂的視窗過程函數已經對它進行了處理,因此這個訊息將不會被傳遞到源視窗過程函數。而我們自訂的這個視窗過程沒有處理的訊息將全部進一步傳遞給源視窗過程函數(它的地址儲存在OldWindowProc中)。
需要注意的是,當我們卸載我們子類的對象前,我們必須恢複它的視窗過程函數。
Private Sub Form_Unload(Cancel As Integer)
SetWindowLong hwnd, GWL_WNDPROC, OldWindowProc
End Sub
因為我們卸載一個視窗對象,系統會發送WM_NCDESTROY訊息給對象,因此我們可以通過檢測這個訊息來自動回復對象的源視窗過程。
Public Function NewWindowProc( ByVal hwnd As Long, ByVal msg As Long, _
ByVal wParam As Long, lParam As WINDOWPOS) As Long
Const WM_NCDESTROY = &H82
Const WM_SYSCOMMAND = &H112
Const SC_SIZE = &HF000&
' 如果組件被銷毀,恢複源視窗過程處理函數
If msg = WM_NCDESTROY Then
SetWindowLong hwnd, GWL_WNDPROC,OldWindowProc
End If
If msg = WM_SYSCOMMAND Then
If (wParam And &HFFF0) = SC_SIZE Then Exit Function
End If
NewWindowProc = CallWindowProc( _
OldWindowProc, hwnd, msg, wParam, _
lParam)
End Function
需要注意的一點是,這種方式很容易造成VB IDE的崩潰。不要在偵錯模式中途暫停或終於應用程式,因為這樣可能不能恢複源視窗過程函數,造成無法處理正常的訊息,變得異常或IDE崩潰,因此切記調試前一定存檔。