| 引子:本文所談及的技術內容都來自于Internet的公開信息。由筆者在閑暇之際整理 后,貼出來以飴網友,姑且妄稱原創。每次在國外網站上找到精彩文章的時候,心中都 會暗自嘆息,為什么在中文網站難以覓得這類文章呢?其實原因大家都明白。 時至今日,學習Windows編程的兄弟們都知道消息機制的重要性。所以理解消息機制也 成了不可或缺的功課。大家都知道,Borland的C++ Builder以及Delphi的核心是VCL。 作為Win32平臺上的開發工具,封裝Windows的消息機制當然也是必不可少的。那么,在 C++ Builder中處理消息的方法有哪些呢?它們之間的區別又在哪里?如果您很清楚這 些,呵呵,對不起啦,請關掉這個窗口。如果不清楚那就和我一起深入VCL的源碼看個 究竟吧。 方法1:使用消息映射(Message Map)重載TObject的Dispatch虛成員函數 這個方法大家用的很多。形式如下 BEGIN_MESSAGE_MAP VCL_MESSAGE_HANDLER( … … ) END_MESSAGE_MAP( … ) 但這幾句話實在太突兀,C++標準中沒有這樣的定義。不用講,這顯然又是宏定義。它 們到底怎么來的呢?CKER第一次見到它們的時候,百思不得其解。嘿嘿,不深入VCL, 怎么可能理解? 在/Borland/CBuilder5/Include/Vcl找到sysmac.h,其中有如下的預編譯宏定義: #define BEGIN_MESSAGE_MAP virtual void __fastcall Dispatch(void *Message) / { / switch (((PMessage)Message)->Msg) / { #define VCL_MESSAGE_HANDLER(msg,type,meth) / case msg: / meth(*((type *)Message)); / break; // NOTE: ATL defines a MESSAGE_HANDLER macro which conflicts with VCL's macro. The // VCL macro has been renamed to VCL_MESSAGE_HANDLER. If you are not using ATL, // MESSAGE_HANDLER is defined as in previous versions of BCB. #if !defined(USING_ATL) && !defined(USING_ATLVCL) && !defined(INC_ATL_HEADERS) #define MESSAGE_HANDLER VCL_MESSAGE_HANDLER #endif // ATL_COMPAT #define END_MESSAGE_MAP(base) default: / base: ispatch(Message); / break; / } / } 這樣對如下的例子: BEGIN_MESSAGE_MAP VCL_MESSAGE_HANDLER(WM_PAINT,TMessage,OnPaint) END_MESSAGE_MAP(TForm1) 在預編譯時,就被展開成如下的代碼 virtual void __fastcall Dispatch(void *Message) { switch (((PMessage)Message)->Msg) { case WM_PAINT: OnPaint(*((TMessage *)Message)); //消息響應句柄,也就是響應消息的成員函數,在Form1中定義 break; default: TForm1: ispatch(Message); break; } } 這樣就很順眼了,對吧。對這種方法有兩點要解釋一下: 1.virtual void __fastcall Dispatch(void *Message) 這個虛方法的定義最早可以在TObject的定義中找到。打開BCB的幫助,查找TForm的方 法(Method),你會發現這里很清楚的寫著Dispatch方法繼承自TObject。如果您關心 VCL的繼承機制的話,您會發現TObject是所有VCL對象的基類。TObject的抽象凝聚了 Borland的工程師們的心血。如果有興趣。您應該好好查看一下TObject的定義。 很顯然,所有TObject的子類都可以重載基類的Dispatch方法,來實現自己的消息調 用。如果Dispatch方法找不到此消息的定義,會將此消息交由TObject:DefaultHandler 方法來處理。抽象基類TObject的DefaultHandler方法實際上是空的。同樣要由繼承子 類重載實現它們自己的消息處理過程。 2.很多時候,我見到的第二行是這樣寫的: MESSAGE_HANDLER(WM_PAINT,TMessage,OnPaint) 在這里,您可以很清楚地看到幾行注解,意思是ATL中同樣包含了一個MESSAGE_HANDLER 的宏定義,這與VCL發生了沖突。為了解決這個問題,Borland改用 VCL_MESSAGE_HANDLER。當您沒有使用ATL的時候,MESSAGE_HANDLER將轉換成 VCL_MESSAGE_HANDLER。但如果用了ATL就會有問題。所以我建議您始終使用 VCL_MESSAGE_HANDLER的寫法,以免出現問題。 ? 方法2:重載TControl的WndProc方法 還是先談談VCL的繼承策略。VCL中的繼承鏈的頂部是TObject基類。一切的VCL組件和對 象都繼承自TObject。 打開BCB幫助查看TControl的繼承關系: TObject->TPersistent->TComponent->TControl 原來TControl是從TPersistent類的子類TComponent類繼承而來的。TPersistent抽象基 類具有使用流stream來存取類的屬性的能力。 TComponent類則是所有VCL組件的父類。 這就是所有的VCL組件包括您的自定義組件可以使用dfm文件存取屬性的原因(當然要是 TPersistent的子類,我想您很少需要直接從TObject類來派生您的自定義組件吧)。 TControl類的重要性并不亞于它的父類們。在BCB的繼承關系中,TControl類的是所有 VCL可視化組件的父類。實際上就是控件的意思吧。所謂可視化是指您可以在運行期間 看到和操縱的控件。這類控件所具有的一些基本屬性和方法都在TControl類中進行定 義。 TControl的實現在/Borland/CBuilder5/Source/Vcl/control.pas中可以找到。(可能 會有朋友問你怎么知道在那里?使用BCB提供的Search -> Find in files很容易找到。 或者使用第三方插件的grep功能。) 好了,進入VCL的源碼吧。說到這里免不了要抱怨一下Borland。哎,為什么要用pascal 實現這一切……:-( TControl繼承但并沒有重寫TObject的Dispatch方法。反而提供了一個新的方法 WndProc。一起來看看Borland的工程師們是怎么寫的吧。 procedure TControl.WndProc(var Message: TMessage); var Form: TCustomForm; begin //由擁有control的窗體來處理設計期間的消息 if (csDesigning in ComponentState) then begin Form := GetParentForm(Self); if (Form <> nil) and (Form.Designer <> nil) and Form.Designer.IsDesignMsg(Self, Message) then Exit; end //如果需要,鍵盤消息交由擁有control的窗體來處理 else if (Message.Msg >= WM_KEYFIRST) and (Message.Msg <= WM_KEYLAST) then begin Form := GetParentForm(Self); if (Form <> nil) and Form.WantChildKey(Self, Message) then Exit; end //處理鼠標消息 else if (Message.Msg >= WM_MOUSEFIRST) and (Message.Msg <= WM_MOUSELAST) then begin if not (csDoubleClicks in ControlStyle) then case Message.Msg of WM_LBUTTONDBLCLK, WM_RBUTTONDBLCLK, WM_MBUTTONDBLCLK: Dec(Message.Msg, WM_LBUTTONDBLCLK - WM_LBUTTONDOWN); end; case Message.Msg of WM_MOUSEMOVE: Application.HintMouseMessage(Self, Message); WM_LBUTTONDOWN, WM_LBUTTONDBLCLK: begin if FDragMode = dmAutomatic then begin BeginAutoDrag; Exit; end; Include(FControlState, csLButtonDown); end; WM_LBUTTONUP: Exclude(FControlState, csLButtonDown); end; end // 下面一行有點特別。如果您仔細的話會看到這個消息是CM_VISIBLECHANGED. // 而不是我們熟悉的WM_開頭的標準Windows消息. // 盡管Borland沒有在它的幫助中提到有這一類的CM消息存在。但很顯然這是BCB的 // 自定義消息。呵呵,如果您對此有興趣可以在VCL源碼中查找相關的內容。一定會有 不小的收獲。 else if Message.Msg = CM_VISIBLECHANGED then with Message do SendDockNotification(Msg, WParam, LParam); // 最后調用dispatch方法。 Dispatch(Message); end; 看完這段代碼,你會發現TControl類實際上只處理了鼠標消息,沒有處理的消息最后都 轉入Dispatch()來處理。 但這里需要強調指出的是TControl自己并沒有獲得焦點Focus的能力。TControl的子類 TWinControl才具有這樣的能力。我憑什么這樣講?呵呵,還是打開BCB的幫助。很多朋 友抱怨BCB的幫助實在不如VC的MSDN。毋庸諱言,的確差遠了。而且這個幫助還經常有 問題。但有總比沒有好啊。 言歸正傳,在幫助的The TWinControl Branch 分支下,您可以看到關于TWinControl類 的簡介。指出TWinControl類是所有窗體類控件的基類。所謂窗體類控件指的是這樣一 類控件: 1. 可以在程序運行時取得焦點的控件。 2. 其他的控件可以顯示數據,但只有窗體類控件才能和用戶發生鍵盤交互。 3. 窗體類控件能夠包含其他控件(容器)。 4. 包含其他控件的控件又稱做父控件。只有窗體類控件才能夠作為其他控件的父控 件。 5. 窗體類控件擁有句柄。 除了能夠接受焦點之外,TWinControl的一切都跟TControl沒什么分別。這一點意味著 TWinControl可以對許多的標準事件作出響應,Windows也必須為它分配一個句柄。并且 與這個主題相關的最重要的是,這里提到是由BCB負責來對控件進行重畫以及消息處 理。這就是說,TWinControl封裝了這一切。 似乎扯的太遠了。但我要提出來的問題是TControl類的WndProc方法中處理了鼠標消 息。但這個消息只有它的子類TWinControl才能夠得到啊!? 這怎么可以呢……Borland是如何實現這一切的呢?這個問題實在很奧妙。為了看個究 竟,再次深入VCL吧。 還是在control.pas中,TWinControl繼承了TControl的WndProc方法。源碼如下: procedure TWinControl.WndProc(var Message: TMessage); var Form: TCustomForm; KeyState: TKeyboardState; WheelMsg: TCMMouseWheel; begin case Message.Msg of WM_SETFOCUS: begin Form := GetParentForm(Self); if (Form <> nil) and not Form.SetFocusedControl(Self) then Exit; end; WM_KILLFOCUS: if csFocusing in ControlState then Exit; WM_NCHITTEST: begin inherited WndProc(Message); if (Message.Result = HTTRANSPARENT) and (ControlAtPos(ScreenToClient( SmallPointToPoint(TWMNCHitTest(Message).Pos)), False) <> nil) then Message.Result := HTCLIENT; Exit; end; WM_MOUSEFIRST..WM_MOUSELAST: //下面這一句話指出,鼠標消息實際上轉入IsControlMouseMsg方法來處理了。 if IsControlMouseMsg(TWMMouse(Message)) then begin if Message.Result = 0 then DefWindowProc(Handle, Message.Msg, Message.wParam, Message.lParam); Exit; end; WM_KEYFIRST..WM_KEYLAST: if Dragging then Exit; WM_CANCELMODE: if (GetCapture = Handle) and (CaptureControl <> nil) and (CaptureControl.Parent = Self) then CaptureControl.Perform(WM_CANCELMODE, 0, 0); else with Mouse do if WheelPresent and (RegWheelMessage <> 0) and (Message.Msg = RegWheelMessage) then begin GetKeyboardState(KeyState); with WheelMsg do begin Msg := Message.Msg; ShiftState := KeyboardStateToShiftState(KeyState); WheelDelta := Message.WParam; Pos := TSmallPoint(Message.LParam); end; MouseWheelHandler(TMessage(WheelMsg)); Exit; end; end; inherited WndProc(Message); end; 鼠標消息是由IsControlMouseMsg方法來處理的。只有再跟到IsControlMouseMsg去看看 啦。源碼如下: function TWinControl.IsControlMouseMsg(var Message: TWMMouse): Boolean; var //TControl出現啦 Control: TControl; P: TPoint; begin if GetCapture = Handle then begin Control := nil; if (CaptureControl <> nil) and (CaptureControl.Parent = Self) then Control := CaptureControl; end else Control := ControlAtPos(SmallPointToPoint(Message.Pos), False); Result := False; if Control <> nil then begin P.X := Message.XPos - Control.Left; P.Y := Message.YPos - Control.Top; file://TControl的Perform方法將消息交由WndProc處理。 Message.Result := Control.Perform(Message.Msg, Message.Keys, Longint(PointToSmallPoint(P))); Result := True; end; end; 原來如此,TWinControl最后還是將鼠標消息交給TControl的WndProc來處理了。這里出 現的Perform方法在BCB的幫助里可以查到,是TControl類中開始出現的方法。它的作用 就是將指定的消息傳遞給TControl的WndProc過程。 結論就是TControl類的WndProc方法的消息是由TWinControl類在其重載的WndProc方法 中調用IsControlMouseMsg方法后使用Peform方法傳遞得到的。 由于這個原因,BCB和Delphi中的TControl類及其所有的派生類都有一個先天的而且是 必須的限制。那就是所有的TControl類及其派生類的Owner必須是TWinControl類或者 TWinControl的派生類。Owner屬性最早可以在TComponent中找到,一個組件或者控件是 由它的Owner擁有并負責釋放其內存的。這就是說,當Owner從內存中釋放的時候,它所 擁有的所有控件占用的內存也都被釋放了。Owner最好的例子就是Form。Owner同時也負 責消息的分派,當Owner接收到消息的時候,它負責將應該傳遞給其所擁有的控件的消 息傳遞給它們。這樣這些控件就能夠取得處理消息的能力。TImage就是個例子:你可以 發現Borland并沒有讓TImage重載TControl的WndProc方法,所以TImage也只有處理鼠標 消息的能力,而這種能力正是來自TControl的。 唧唧崴崴的說了一大堆。終于可以說處理消息的第二種方法就是重載TControl的 WndProc方法了。例程如下: void __fastcall TForm1::WndProc(TMessage &Message) { switch (Message.Msg) { case WM_CLOSE: OnCLOSE(Message); // 處理WM_CLOSE消息的方法 break; } TForm::WndProc(Message); } 乍看起來,這和上次講的重載Dispatch方法好象差不多。但實際上還是有差別的。差別 就在先后次序上,從前面TControl的WndProc可以看到,消息是先交給WndProc來處理, 最后才調用Dispatch方法的啦。 這樣,重載WndProc方法可以比重載Dispatch方法更早一點點得到消息并處理消息。 好了,這次就說到這里。在您的應用程序里還有沒有比這更早得到消息的辦法呢?有, 下次再說。 ? 方法3 自TApplication的方法 不用我多廢話,大家都知道TApplication在BCB中的重要性。在BCB的幫助中指出: TApplication、TScreen和TForm構成了所有BCB風格的Win32 GUI程序的脊梁,他們控制 著您程序的行為。TApplication類提供的屬性和方法封裝了標準Windows程序的行為。 TApplication表現了在Windows操作系統中創建、運行、支持和銷毀應用程序的基本原 理。因此,TApplication大大簡化了開發者和Windows環境之間的接口。這正是BCB的 RAD特性。 TApplication封裝的標準Windows行為大致包括如下幾部分: 1> Windows 消息處理 2> 上下文關聯的在線幫助 3> 菜單的快捷鍵和鍵盤事件處理 4> 異常處理 5> 管理由操作系統定義的程序基礎部分,如:MainWindow 主窗口、 WindowClass 窗 口類 等。 一般情況下,BCB會為每個程序自動生成一個TApplication類的實例。這部分源碼可以 在yourproject.cpp文件中見到(這里假定您的工程名稱就叫yourproject.bpr)。 當然TApplication是不可見的,他總是在您的Form背后默默的控制著您的程序的行為。 但也不是找不到蛛絲馬跡。如果您新建一個程序(New Application),然后不作任何改 動,編譯運行的話,你會發現程序窗體的Caption是Form1,但在Windows的狀態條上的 Caption確寫著project1的字樣。這就是TApplication存在的證據。當然,這只是一種 臆測,實戰的方法應該打開BCB附帶的WinSight來查看系統的進程。您可以清楚的看到 TApplication類的存在,他的大小是0(隱藏的嘛),然后才是TForm1類。 好了,既然TApplication封裝了消息處理的內容。我們就研究一下TApplication的實際 動作吧。實際上消息到達BCB程序時,最先得到它們的就是TApplication對象。經由 TApplication之后,才傳遞給Form的。以前的方法都是重載TForm的方法,顯然要比本 文所提到的方法要晚一些收到消息。對您來說,是不是希望在第一時間收到消息并處理 它們呢? 要清楚的知道TApplication的處理機制還是深入VCL源碼。首先看一看最最普通的一段 代碼吧。 #include #pragma hdrstop USERES("Project1.res"); USEFORM("Unit1.cpp", Form1); //-------------------------------------------------------------- WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) { try { // 初始化Application Application->Initialize(); // 創建主窗口,并顯示 Application->CreateForm(__classid(TForm1), &Form1); // 進入消息循環,直到程序退出 Application->Run(); } catch (Exception &exception) { Application->ShowException(&exception); } return 0; } 短短的幾行代碼就可以讓您的BCB程序自如運行。因為一切都已經被VCL在后臺封裝好 了。Application->Run()方法進入程序的消息循環,直到程序退出。一起跟進VCL源碼 看個究竟吧。 TApplication的定義在forms.pas中。 procedure TApplication.Run; begin FRunning := True; try AddExitProc(DoneApplication); if FMainForm <> nil then begin // 設置主窗口的顯示屬性 case CmdShow of SW_SHOWMINNOACTIVE: FMainForm.FWindowState := wsMinimized; SW_SHOWMAXIMIZED: MainForm.WindowState := wsMaximized; end; if FShowMainForm then if FMainForm.FWindowState = wsMinimized then Minimize else FMainForm.Visible := True; // 看見了吧,這里有個循環,直到Terminated屬性為真退出。Terminated什么意思, 就是取消,結束 repeat HandleMessage until Terminated; end; finally FRunning := False; end; end; 消息處理的具體實現不在Run方法中,很顯然關鍵在HandleMessage方法,看看這函數名 字-消息處理。只有跟進HandleMessage瞧瞧嘍。 procedure TApplication.HandleMessage; var Msg: TMsg; begin if not ProcessMessage(Msg) then Idle(Msg); end; 咳,這里也不是案發現場。程序先將消息交給ProcessMessage方法處理。如果沒什么要 處理的,就轉入Application.Idle方法“程序在空閑時調用的方法”。 呼呼,再跟進ProcessMessage方法吧。 function TApplication.ProcessMessage(var Msg: TMsg): Boolean; var Handled: Boolean; begin Result := False; if PeekMessage(Msg, 0, 0, 0, PM_REMOVE) then begin Result := True; if Msg.Message <> WM_QUIT then begin Handled := False; if Assigned(FOnMessage) then FOnMessage(Msg, Handled); if not IsHintMsg(Msg) and not Handled and not IsMDIMsg(Msg) and not IsKeyMsg(Msg) and not IsDlgMsg(Msg) then begin TranslateMessage(Msg); DispatchMessage(Msg); end; end else FTerminate := True; end; end; 哎呀呀,終于有眉目了。ProcessMessage采用了一套標準的Windows API 函數 PeekMessage .... TranslateMessage;DispatchMessage。 有人說:Application->OnMessage = MyOnMessage; //不能響應SendMessage的消息, 但是可以響應PostMessage發送的消息,也就是消息隊列里的消息 SendMessage和PostMessage最主要的區別在于發送的消息有沒有通過消息隊列。 原因就在這里。ProcessMessage使用了PeekMessage(Msg, 0, 0, 0, PM_REMOVE) 從消 息隊列中提取消息。然后先檢查是不是退出消息。不是的話,檢查是否存在OnMessage 方法。如果存在就轉入OnMessage處理消息。最后才將消息分發出去。 這樣重載Application的OnMessage方法要比前兩種方法更早得到消息,可以說是最快速 的方法了吧。舉個例子: void __fastcall TForm1::MyOnMessage(tagMSG &Msg, bool &Handled) { TMessage Message; switch (Msg.message) { case WM_KEYDOWN: Message.Msg = Msg.message; Message.WParam = Msg.wParam; Message.LParam = Msg.lParam; MessageDlg("You Pressed Key!", mtWarning, TMsgDlgButtons() << mbOK, 0); Handled = true; break; } } void __fastcall TForm1::FormCreate(TObject *Sender) { Application->OnMessage = MyOnMessage; } 現在可以簡短的總結一下VCL的消息機制了。 標準的BCB程序使用Application->Run()進入消息循環,在Application的 ProcessMessage方法中,使用PeekMessage方法從消息隊列中提取消息,并將此消息從 消息隊列中移除。然后ProcessMessage 方法檢查是否存在Application->OnMessage方 法。存在則轉入此方法處理消息。之后再將處理過的消息分發給程序中的各個對象。至 此,WndProc方法收到消息,并進行處理。如果有無法處理的交給重載的Dispatch方法 來處理。要是還不能處理的話,再交給父類的Dispatch方法處理。最后Dispatch方法實 際上將消息轉入DefaultHandler方法來處理。 嘿嘿,實際上,你一樣可以重載DefaultHandler方法來處理消息。但是太晚了一點。我 想沒有人愿意最后一個處理消息吧...:-) 寫到這里似乎可以結束了。但如果您看過上一篇的話,一定會注意到 Application->HookMainWindow方法。這又是怎么一回事呢? 如果您打算使用Application->OnMessage來捕獲所有發送至您的應用程序的消息的話, 您大概要失望了。原因已經講過,它無法捕獲使用SendMessage直接發送給窗口的消 息,因為這不通過消息隊列。您也許會說我可以直接重載TApplication的WndProc方 法。呵呵,不可以。因為TApplication的WndProc方法被Borland申明為靜態的,從而無 法重載。顯而易見,這么做的原因很可能是Borland擔心其所帶來的副作用。那該如何 是好呢? 查看TApplication的WndProc的pascal源碼可以看到: procedure TApplication.WndProc(var Message: TMessage); ... // 節約篇幅,此處與主題無關代碼略去 begin try Message.Result := 0; for I := 0 to FWindowHooks.Count - 1 do if TWindowHook(FWindowHooks[I]^)(Message) then Exit; ... // 節約篇幅,此處與主題無關代碼略去 WndProc方法一開始先調用HookMainWindow掛鉤的自定義消息處理方法,然后再調用缺 省過程處理消息。這樣使用HookMainWindow就可以在WndProc中間接加入自己的消息處 理方法。使用這個方法響應SendMessage發送來的消息很管用。最后提醒一下,使用 HookMainWindow掛鉤之后一定要對應的調用UnhookMainWindow卸載鉤子程序。給個例子 : void __fastcall TForm1::FormCreate(TObject *Sender) { Application->HookMainWindow(AppHookFunc); } bool __fastcall TForm1::AppHookFunc(TMessage &Message) { bool Handled ; switch (Message.Msg) { case WM_CLOSE: mrYes == MessageDlg("Really Close??", mtWarning, TMsgDlgButtons() << mbYes << mbNo, 0) ? Handled = false : Handled = true ; break; } return Handled; } void __fastcall TForm1::FormDestroy(TObject *Sender) { Application->UnhookMainWindow(AppHookFunc); } void __fastcall TForm1::Button1Click(TObject *Sender) { SendMessage(Application->Handle,WM_CLOSE,0,0); } 這樣,將本文中的兩種方法相結合,您就可以自如的處理到達您的應用程序的各種消息 了。 |