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事件的呢,这其中也涉及到很多内容,留待以后再学习了。

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