引子

本文所談及的技術內容都來自於Internet的公開信息。由CKER在閒暇之際整理後,貼出來以飴網友,姑且妄稱原創。
每次在國外網站上找到精彩文章的時候,心中都會暗自嘆息為什麼在中文網站難以覓得這類文章呢?其實原因大家都明白。
時至今日,學習Windows編程的兄弟們都知道消息機制的重要性。所以理解消息機制也成了不可或缺的功課。
大家都知道,Borland的C++ Builder以及Delphi的核心是VCL。作為Win32平台上的開發工具,封裝Windows的消息機制當然也是必不可少的。
那麼,在C++ Builder中處理消息的方法有哪些呢?它們之間的區別又在哪裡?如果您很清楚這些,呵呵,對不起啦,請關掉這個窗口。
如果不清楚那就和我一起深入VCL的源碼看個究竟吧。『注:BCB只有Professional和Enterprise版本才帶有VCL源碼。當然,大夥的版本都有源碼的。我沒猜錯吧 :-)<CKER用的是BCB5>

方法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.
file://
#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::Dispatch(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:
                Form1::Dispatch(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()方法。反而提供了一個新的方法就是xycleo提到的WndProc()。一起來看看Borland的工程師們是怎麼寫的吧。

procedure TControl.WndProc(var Message: TMessage);
varForm: TCustomForm;
begin//由擁有control的窗體來處理設計期間的消息if (CSDesigning in ComponentState) thenbegin Form := GetParentForm(Self); if (Form <> nil) and (Form.Designer <> nil) andForm.Designer.IsDesignMsg(Self, Message) then Exit;end//如果需要,鍵盤消息交由擁有control的窗體來處理else if (Message.Msg >= WM_KEYFIRST) and (Message.Msg <= WM_KEYLAST) thenbegin 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) thenbegin if not (csDoubleClicks in ControlStyle) thencase Message.Msg ofWM_LBUTTONDBLCLK, WM_RBUTTONDBLCLK, WM_MBUTTONDBLCLK: Dec(Message.Msg, WM_LBUTTONDBLCLK - WM_LBUTTONDOWN);end; case Message.Msg ofWM_MOUSEMOVE: Application.HintMouseMessage(Self, Message);WM_LBUTTONDOWN, WM_LBUTTONDBLCLK:begin if FDragMode = dmAutomatic then beginBeginAutoDrag;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 doSendDockNotification(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);varForm: TCustomForm;KeyState: TKeyboardState;WheelMsg: TCMMouseWheel;begincase Message.Msg of WM_SETFOCUS:beginForm := GetParentForm(Self);if (Form <> nil) and not Form.SetFocusedControl(Self) then Exit;end; WM_KILLFOCUS:if csFocusing in ControlState then Exit; WM_NCHITTEST:begininherited 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)) thenbeginif 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) thenCaptureControl.Perform(WM_CANCELMODE, 0, 0);else with Mouse doif WheelPresent and (RegWheelMessage <> 0) and(Message.Msg = RegWheelMessage) thenbeginGetKeyboardState(KeyState);with WheelMsg dobegin 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;beginif GetCapture = Handle thenbegin Control := nil; if (CaptureControl <> nil) and (CaptureControl.Parent = Self) thenControl := CaptureControl;end else Control := ControlAtPos(SmallPointToPoint(Message.Pos), False);Result := False;if Control <> nil thenbegin 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。RH指出的來自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 <vcl.h>#pragma hdrstopUSERES("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;beginFRunning := True;try AddExitProc(DoneApplication); if FMainForm <> nil then begin
// 設置主窗口的顯示屬性case CmdShow ofSW_SHOWMINNOACTIVE: FMainForm.FWindowState := wsMinimized;SW_SHOWMAXIMIZED: MainForm.WindowState := wsMaximized;end;if FShowMainForm thenif FMainForm.FWindowState = wsMinimized then Minimize else FMainForm.Visible := True;
// 看見了吧,這裡有個循環,直到Terminated屬性為真退出。Terminated什麼意思,就是取消,結束repeatHandleMessageuntil Terminated; end;finally FRunning := False;end;end;

消息處理的具體實現不在Run方法中,很顯然關鍵在HandleMessage方法,看看這函數名字-消息處理。只有跟進HandleMessage瞧瞧嘍。

procedure TApplication.HandleMessage;varMsg: TMsg;beginif not ProcessMessage(Msg) then Idle(Msg);end;

咳,這裡也不是案發現場。程序先將消息交給ProcessMessage方法處理。如果沒什麼要處理的,就轉入Application.Idle方法『程序在空閒時調用的方法』。
呼呼,再跟進ProcessMessage方法吧。

function TApplication.ProcessMessage(var Msg: TMsg): Boolean;varHandled: Boolean;beginResult := False;if PeekMessage(Msg, 0, 0, 0, PM_REMOVE) thenbegin Result := True; if Msg.Message <> WM_QUIT then beginHandled := False;if Assigned(FOnMessage) then FOnMessage(Msg, Handled);if not IsHintMsg(Msg) and not Handled and not IsMDIMsg(Msg) andnot IsKeyMsg(Msg) and not IsDlgMsg(Msg) thenbeginTranslateMessage(Msg);DispatchMessage(Msg);end; end elseFTerminate := True;end;end;

哎呀呀,終於有眉目了。ProcessMessage採用了一套標準的Windows API 函數 PeekMessage .... TranslateMessage;DispatchMessage。前一篇帖子RH
跟了幾貼說
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方法來處理消息。但是太晚了
一點。我想沒有人願意最後一個處理消息吧...:-)』

寫到這裡似乎可以結束了。但如果您看過上一篇的話,一定會注意到RH的提到的Application->HookMainWindow方法。這又是怎麼一回事呢?

如果您打算使用Application->OnMessage來捕獲所有發送至您的應用程序的消息的話,您大概要失望了。原因已經講過,它無法捕獲使用
SendMessage直接發送給窗口的消息,因為這不通過消息隊列。您也許會說我可以直接重載TApplication的WndProc方法。呵呵,不可以。因為
TApplication的WndProc方法被Borland申明為靜態的,從而無法重載。顯而易見,這麼做的原因很可能是Borland擔心其所帶來的副作用。那該
如何是好呢?
查看TApplication的WndProc的pascal源碼可以看到:

procedure TApplication.WndProc(var Message: TMessage);... // 節約篇幅,此處與主題無關代碼略去

begintry Message.Result := 0; for I := 0 to FWindowHooks.Count - 1 doif 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);
}
//---------------------------------------------------------------------------


這樣將本文中的兩種方法相結合,您就可以自如的處理到達您的應用程序的各種消息了。
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 dascan 的頭像
    dascan

    阿達の設計手札

    dascan 發表在 痞客邦 留言(0) 人氣()