VCL的命令消息(一)窗口控件的命令消息

VCL的命令消息

Windows中的消息有三種:標準的窗口消息,命令消息,控件通知消息,再加上我們自定義的消息,所以Windows程序我們要處理四種消息,幸運的我們常用的開發工具都帶了自己的Framwork,Visual C++中用的是MFC,Delphi和BCB用的VCL,這些Framwork都有一套自己的消息處理機制,它們處理前面三種系統已經定義的消息,所以我們要做的就是處理自己定義的消息。命令消息的消息ID是WM_COMMAND,其他以WM_開頭的消息是標準窗口消息。VCL封裝了Windows的消息機制,它可以處所有Windows預定義的消息。

這裏要分析一下Delphi的VCL處理命令消息的方式。當用戶點擊菜單、加速鍵、工具欄,或者子窗口控件觸發某些事件時,都會產生WM_COMMAND消息,WM_COMMAND消息是由菜單、加速鍵、工具欄和子窗口控件發送給父窗口的。

WM_COMMAND消息中有兩個參 數,wparam、lparam,定義如下:

       wParam 高兩個字節 通知碼

       wParam 低兩字節 命令ID

       lParam 發送命令消息的子窗體句柄。 

對於菜單 和加速鍵來說,lParam爲0,只有控件此項才非0。命令ID也就是資源腳本中定義的菜單項的命令ID或者加速鍵的命令ID;菜單的通知碼爲0;加速鍵 的通知碼爲1。

       先看下子控件是如何發送WM_COMMAND消息給父窗口的的,以TButton爲例,這裏要分析一下用戶點擊TButton控件時消息是如何產生,路由,轉發,被處理而最終調用TButton.OnClick事件處理函數的。  用鼠標左鍵點擊TButton控件是會產生兩消息WM_LBUTTONDOWN和WM_LBUTTONUP,OnClick是在鬆開鼠標後調用的,也就是WM_LBUTTONUP被處理後觸發的。我們就來看看VCL處理WM_LBUTTONUP的過程。

       VCL封裝的窗口控件的真正的窗口函數中WndProc,這個函數是在控件類TControl中定義的,是一個虛函數。TButton並沒有覆蓋此函數,它的父類覆蓋的此窗口函數,

procedure TButtonControl.WndProc(var Message: TMessage);

begin

  case Message.Msg of

    WM_LBUTTONDOWN, WM_LBUTTONDBLCLK:

      if not (csDesigning in ComponentState) and not Focused then

      begin

        FClicksDisabled := True;

        Windows.SetFocus(Handle);

        FClicksDisabled := False;

        if not Focused then Exit;

      end;

    CN_COMMAND:

      if FClicksDisabled then Exit;

  end;

  inherited WndProc(Message);

end;

TButtonControl.WndProc並沒有處理WM_LBUTTONUP消息而是調用TWinControl.WndProc來處理,這裏只摘寫部分代碼如下

procedure TWinControl.WndProc(var Message: TMessage);

……

WM_MOUSEFIRST..WM_MOUSELAST:

      if IsControlMouseMsg(TWMMouse(Message)) then

      begin

        { Check HandleAllocated because IsControlMouseMsg might have freed the

          window if user code executed something like Parent := nil. }

        if (Message.Result = 0) and HandleAllocated then

          DefWindowProc(Handle, Message.Msg, Message.wParam, Message.lParam);

        Exit;

      end;

……

可以看出對於鼠標消息,窗口函數要調用IsControlMouseMsg,作用是判斷是不是當前控件的子控件的鼠標消息,可以比較關鍵,又有點不好理解,下面簡單的說明:

只有窗口控件能捕獲鼠標,非窗口控件是不能捕獲的,也就是隻有從TwinControl類繼承的子控件才能獲得鼠標焦點,而直接從TControl類或是從TGraphicsControl類繼承的控件是非窗口控件沒有窗口句柄Handle而不能獲得焦點,如TLable是從TGraphicsControl繼承下來的,沒有窗口句柄,它就不能捕獲鼠標。而我們點擊TLable時,捕獲鼠標的是TLable的父窗口即TLable.Parent。能作爲業父窗口的只能是TWinControl和它的子類。如果TLable在TForm上,點擊TLable時獲得焦點的就是TForm,然後TForm調用 IsControlMouseMsg判斷我們實際要點的是不是TForm上的其他子控件(這裏的TLable),再做相應的處理。我們看IsControlMouseMsg的源代碼就知道了。

function TWinControl.IsControlMouseMsg(var Message: TWMMouse): Boolean;

var

  Control: TControl;

  P: TPoint;

begin

  if GetCapture = Handle then

  begin

    if (CaptureControl <> nil) and (CaptureControl.Parent = Self) then

      Control := CaptureControl

    else

      Control := nil;

  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;

    Message.Result := Control.Perform(Message.Msg, Message.Keys, Longint(PointToSmallPoint(P)));

    Result := True;

  end;

end;

GetCapture返回當前捕獲鼠標的窗口的句柄,由於系統先發送WM_LBUTTONDOWN消息,這裏的TBUTTON控件已經獲得了鼠標,GetCapture返回的是TBUTTON.Handle。

CaptureControl永遠指向一個無句柄的控件,即非窗口控件(非TwinControl類對象)或者nil(表示當前沒有控件應該應該響應鼠標事件或者窗口控件獲得了鼠標焦點)。因爲TButton不是容器窗口,沒有子控件所以CaptureControl=nil,函數的返回值是False。

IsControlMouseMsg返回False,TWinControl.WndProc因此又調用TControl.WndProc,TControl.WndProc也沒有處理完WM_LBUTTONUP最後調用TObject.Dispatch(Message);

Dispatch在類的動態方法表中查找處理WM_LBUTTONUP消息的消息函數,從TButton類形如依次是TButtonControl->TWinControl->TControl最終在TControl類中找到了WM_LBUTTONUP的消息處理函數TControl.WMLButtonUp,代碼如下:

procedure TControl.WMLButtonUp(var Message: TWMLButtonUp);

begin

  inherited;

  if csCaptureMouse in ControlStyle then MouseCapture := False;

  if csClicked in ControlState then

  begin

    Exclude(FControlState, csClicked);

    if PtInRect(ClientRect, SmallPointToPoint(Message.Pos)) then Click;

  end;

  DoMouseUp(Message, mbLeft);

end;

這裏我們關注的是inherited這個關鍵字,出現在消息處理函數裏inherited在Delphi有幫助手冊中說明如下:

The inherited statement searches backward through the class hierarchy and invokes the first message method with the same ID as the current method, automatically passing the message record to it. If no ancestor class implements a message method for the given ID, inherited calls the DefaultHandler method originally defined in TObject.做軟件是經常碰到English的,大家翻譯它應該不難,大意是:inherited在當前類的父類中沿着類的繼承架構向上查找處理同一消息ID(這裏爲WM_LBUTTONUP)的消息處理函數,如果找到了就把Message消息結構參數傳遞來調用它,如果祖先類都沒有實現處理該消息的消息處理函數就調用TObject類的DefaultHandler方法TObject.DefaultHandler是個空的虛方法,它是VCL的類中最後可以處理消息的地方,TControl和TWinControl類都覆蓋了這個方法。TWinControl.DefaultHandler中最重要的一行是

Result := CallWindowProc(FDefWndProc, FHandle, Msg, WParam, LParam);

就是調用系統默認的窗口函數(FDefWndProc是在創建窗口控件是保存的系統默認的窗口過程)也就是TButton的WM_LBUTTONUP消息最後是交給操作系統的默認窗口函數來處理的,FHandle參數是TButton.Handle, Msg是WM_LBUTTONUP, FDefWndProc是 DefWindowProc。操作系統處理WM_LBUTTONUP消息的方式則是把它轉化爲WM_COMMAND消息發送給TButton的父窗口即TForm。消息傳遞的路徑如下:

procedure TCustomForm.WndProc(var Message: TMessage);

procedure TWinControl.WndProc(var Message: TMessage);

procedure TControl.WndProc(var Message: TMessage);

procedure TObject.Dispatch(var Message);

procedure TCustomForm.WMCommand(var Message: TWMCommand);

也就是說TForm,TCustomForm,TWinControl,TControl的窗口函數都沒處理WM_COMMAND消息,最後TControl.WndProc調用TObject.Dispatch在類的動態方法表中查找消息處理函數,因而在TCustomForm類的找到

procedure WMCommand(var Message: TWMCommand); message WM_COMMAND;它的實現如下:

procedure TCustomForm.WMCommand(var Message: TWMCommand);

begin

  with Message do

    if (Ctl <> 0) or (Menu = nil) or not Menu.DispatchCommand(ItemID) then

      inherited;

end;

看TWMCommand的定義,

  TWMCommand = packed record

    Msg: Cardinal;

    ItemID: Word;

    NotifyCode: Word;

    Ctl: HWND;

    Result: Longint;

  end;

Msg表示消息ID,ItemID表示子控件ID,NotifyCode表示通知代碼,Ctl表示子控件句柄,這裏的子控件就是將WM_COMMAND消息轉發到該窗口的子窗口控件,也就TButton。所以這裏的 Ctl <> 0爲True,就執行inherited調用父類的消息處理函數:

procedure TWinControl.WMCommand(var Message: TWMCommand);

begin

  if not DoControlMsg(Message.Ctl, Message) then inherited;

end;

DoControlMsg是一個單元內部的全局函數,它先根據窗口句柄找到對應的窗口類對象,然後調用該窗口控件的Perform方法將父窗口的WM_COMMAND消息又轉化爲相應的子窗口控件的CN_COMMAND消息。

function DoControlMsg(ControlHandle: HWnd; var Message): Boolean;

var

  Control: TWinControl;

begin

  DoControlMsg := False;

  Control := FindControl(ControlHandle);

  if Control <> nil then

    with TMessage(Message) do

    begin

      Result := Control.Perform(Msg + CN_BASE, WParam, LParam);

      DoControlMsg := True;

    end;

end;

在Controls單元有如下定義

Const CN_COMMAND           = CN_BASE + WM_COMMAND;

Perform方法會直接調用TButton子窗口的窗口函數,它又按如下路徑執行

TButtonControl.WndProc(它的ClicksDisabled一般都爲False)

TWinControl.WndPro

TControl.WndProc

TObject.Dispatch

TButton.CNCommand

下面是兩相關的重要函數TButton.CNCommand,TButton.Click,TControl.Click

procedure TButton.CNCommand(var Message: TWMCommand);

begin

  if Message.NotifyCode = BN_CLICKED then Click;

end;

 

procedure TButton.Click;

var

  Form: TCustomForm;

begin

  Form := GetParentForm(Self);

  if Form <> nil then Form.ModalResult := ModalResult;

  inherited Click;

end;

 

procedure TControl.Click;

begin

  { Call OnClick if assigned and not equal to associated action's OnExecute.

    If associated action's OnExecute assigned then call it, otherwise, call

    OnClick. }

  if Assigned(FOnClick) and (Action <> nil) and (@FOnClick <> @Action.OnExecute) then

    FOnClick(Self)

  else if not (csDesigning in ComponentState) and (ActionLink <> nil) then

    ActionLink.Execute(Self)

  else if Assigned(FOnClick) then

    FOnClick(Self);

end;

最後是在TControl.Click中調用我們寫的響應OnClick事件的方法或關聯的Action.OnExecute事件方法。下面是單擊Button時調用棧,調用順序是從下往上。

TForm1.btn1Click($18D3B2C)

TControl.Click

TButton.Click

TButton.CNCommand((48401, 752, 0, 6816496, 0))

TControl.WndProc((48401, 752, 6816496, 0, 752, 0, 752, 104, 0, 0))

TWinControl.WndProc((48401, 752, 6816496, 0, 752, 0, 752, 104, 0, 0))

TButtonControl.WndProc((48401, 752, 6816496, 0, 752, 0, 752, 104, 0, 0))

TControl.Perform(48401,752,6816496)

DoControlMsg(6816496,(no value))

TWinControl.WMCommand((273, 752, 0, 6816496, 0))

TCustomForm.WMCommand((273, 752, 0, 6816496, 0))

TControl.WndProc((273, 752, 6816496, 0, 752, 0, 752, 104, 0, 0))

TWinControl.WndProc((273, 752, 6816496, 0, 752, 0, 752, 104, 0, 0))

TCustomForm.WndProc((273, 752, 6816496, 0, 752, 0, 752, 104, 0, 0))

TWinControl.MainWndProc((273, 752, 6816496, 0, 752, 0, 752, 104, 0, 0))

StdWndProc(1181622,273,752,6816496)

TWinControl.DefaultHandler((no value))

TControl.WMLButtonUp((514, 0, 39, 9, (39, 9), 0))

TControl.WndProc((514, 0, 589863, 0, 0, 0, 39, 9, 0, 0))

TWinControl.WndProc((514, 0, 589863, 0, 0, 0, 39, 9, 0, 0))

TButtonControl.WndProc((514, 0, 589863, 0, 0, 0, 39, 9, 0, 0))

TWinControl.MainWndProc((514, 0, 589863, 0, 0, 0, 39, 9, 0, 0))

StdWndProc(6816496,514,0,589863)

TApplication.ProcessMessage((6816496, 514, 0, 589863, 15108306, (491, 303)))

TApplication.HandleMessage

TApplication.Run

Project1

其中相關消息ID和窗口句柄的十進制值如下:

WM_LBUTTONDOWN=513

WM_LBUTTONUP=514

WM_COMMAND=273

CN_COMMAND = CN_BASE + WM_COMMAND=48401

Form1.Handle=1181622

Btn1.Handle=6816496

從前面的分析和上面的調用棧順序可以看出對於子控件的WM_LBUTTONUP消息VCL先是將它交給操作系統處理,操作系統把這個消息又轉化成它的父窗口的命令消息WM_COMMAND,TWinControl又把命令消息轉化爲VCL內部的消息CN_COMMAND轉發給先前給它發送命令消息的子控件,最後消息在子控件類(這裏爲TButton)中被最終處理產生OnClick事件並結束消息的傳遞。

至於爲什麼WM_LBUTTONUP會由操作系統轉化成父窗口的WM_COMMAND消息,恕小人孤陋寡聞,不得其解。不過我猜測應該與MicroSoft有關。因爲大多數的窗口控件都是Windows提供的,它如何響應這樣窗口消息當然是Windows自身最清楚,在Windows平臺上VCL當然要遵守它MS提供的遊戲規則,所以讓操作系統來處理消息,除非是VCL內部使用的消息和用戶自定義消息,很多消息都是交由操作系統來處理的。

另外Visual C++的MFC中也是這樣,Button的WM_LBUTTONUP會轉化成它的父窗口的WM_COMMAND消息,由父窗口來處理。對於WM_COMMAND消息MFC是這樣處理的,如果是視類CView及其子類首先在自身和父類的消息映射表中查找相關的處理函數,找到就處理結束,如果都沒有就按照文檔類(CDocument)=》框架窗口類(CFrameWnd)=》應用程序類(CWinApp)的順序來處理,在文檔類(CDocument)中WM_COMMAND又是按照先自身然後文檔管理器的順序來處理,直到這些類中有一個OnCmdMsg方法處理了該命令消息或都沒有處理命令消息。MFC不會再把WM_COMMAND消息轉化發送回子窗口處理,這是它和VCL最主要的區別。MFC用的典型的責任鏈設計模式。

本文是心按鈕TButton爲例來分析的,全文的脈絡如上面列出的調用棧一樣已經很清析了。這些的也都是我學習VCL框架源代碼的一點心得。但是TButton是標準的窗口控件,它是窗口句柄,可以響應標準窗口消息,WM_LBUTTONUP就窗口消息的一種。但VCL中還很重要的一個類繼承分枝-圖形控件類TGraphicControl,它同TWinContol類一樣也繼承自TControl類,但它是沒有窗口句柄的,不能接收處理Window消息,只能處理一些VCL內部定義的消息。如TLable就是TGraphicControl的子類的,它沒有窗口句柄,不能接受WM_LBUTTONUP,但它又是如何在單擊時產生OnClick事件的呢,這其中也涉及到很多內容,留待以後再學習了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章