[推薦]深入VCL 理解BCB的消息機制(全) |
|
axsoft
版主 發表:681 回覆:1056 積分:969 註冊:2002-03-13 發送簡訊給我 |
深入VCL 理解BCB的消息機制2002-07-08‧ ‧ ‧‧C builder資源中心 資料來源: http://www.yesky.com/20020702/1618597.shtml http://www.yesky.com/20020702/1618598.shtml http://www.yesky.com/20020702/1618598_1.shtml 時至今日,學習Windows編程的兄弟們都知道消息機制的重要性。所以理解消息機制也成了不可或缺的功課。 大家都知道,Borland的C Builder以及Delphi的核心是VCL。作為Win32平台上的開發工具,封裝Windows的消息機制當然也是必不可少的。 那麼,在C Builder中處理消息的方法有哪些呢?它們之間的區別又在哪里?如果您很清楚這些,呵呵,對不起啦,請關掉這個窗口。 如果不清楚那就和我一起深入VCL的源碼看個究竟吧。『注:BCB只有Professional和Enterprise版本才帶有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. 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); 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方法更早一點點得到消息並處理消息。 時間就是金錢---[ 發問前請先找找舊文章] |
本站聲明 |
1. 本論壇為無營利行為之開放平台,所有文章都是由網友自行張貼,如牽涉到法律糾紛一切與本站無關。 2. 假如網友發表之內容涉及侵權,而損及您的利益,請立即通知版主刪除。 3. 請勿批評中華民國元首及政府或批評各政黨,是藍是綠本站無權干涉,但這裡不是政治性論壇! |