点击TButton后的执行OnClick和OnMouseDown两个事件的过程(其实是通过WM_COMMAND执行程序员的代码)

问题的来源:在李维的《深入浅出VCL》一书中提到了点击TButton会触发WM_COMMAND消息,正是它真正执行了程序员的代码。也许是我比较笨,没有理解他说的含义。但是后来经过追踪代码和仔细分析,终于明白了整个过程。结论是,自己对Win32的不够了解,其实触发按钮就是靠这个WM_COMMAND消息,而且VC里也是这样做的。

现象:有没有发现TButton既有OnClick,又有OnMouseDown,它们之间是什么区别和联系是什么呢?普通的按钮点击到底是哪个事件执行了程序员的代码,又是如何执行的呢?且看我的分析过程:

type
  TForm1 = class(TForm)
    Button1: TButton;
    Button2: TButton;
    procedure Button1Click(Sender: TObject);
    procedure Button1MouseDown(Sender: TObject; Button: TMouseButton;
      Shift: TShiftState; X, Y: Integer);
    procedure Button2Click(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
    m_tag: integer;
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.Button1Click(Sender: TObject);
begin
  tag:=100;
end;

procedure TForm1.Button1MouseDown(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
begin
  m_tag:=200;
end;

procedure TForm1.Button2Click(Sender: TObject);
begin
  ShowMessage(intToStr(tag));
  ShowMessage(intToStr(m_tag));
end;

点击Button1后,再点击Button2,发现tag和m_tag两个值都被赋值了。看来用鼠标点击Button是一箭双雕啊,会同时触发OnClick和OnMouseDown事件。至于这两个事件哪个会先执行,则要看产生消息的先后顺序。至于到底谁先谁后,我想了好多办法:用SPY++观察不行,因为Button1和Form1是两个不同的句柄;在Application.Run里观察消息也不行,因为实在Application运行以后是太多消息了,没法调试。也许修改VCL源代码并同时加上case WM_COMMAND和case WM_LBUTTONDOWN后看先截住谁。不过后来我想了一个好办法,就是在这两个事件里加上记录时间的选项,这样简单方便,实在是不用什么高深技术。通过代码测试,我发现还是OnClick会被先执行:

  TForm1 = class(TForm)
    Button1: TButton;
    Button2: TButton;
    procedure Button1Click(Sender: TObject);
    procedure Button1MouseDown(Sender: TObject; Button: TMouseButton;
      Shift: TShiftState; X, Y: Integer);
    procedure Button2Click(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
    m_tag: integer;
    m_time_click: TTime;
    m_time_mousedown: TTime;
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.Button1Click(Sender: TObject);
begin
  tag:=100;
  m_time_click:=now;
end;

procedure TForm1.Button1MouseDown(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
begin
  m_tag:=200;
  m_time_mousedown:=now;
end;

procedure TForm1.Button2Click(Sender: TObject);
begin
  ShowMessage(intToStr(tag));
  ShowMessage(intToStr(m_tag));
  if m_time_click>m_time_mousedown then ShowMessage('m_time_click is first');
end;

 

于是接下去自然应该分析OnClick的执行过程。不过说实话,正面分析有点难,但我还是硬着头皮上吧(其实我本人是知道答案后反推整个过程的)。如下:

1.程序员改写的OnClick事件,那么我们可以发现OnClick是TControl的事件:
property OnClick: TNotifyEvent read FOnClick write FOnClick stored IsOnClickStored;
同时可以了解一下什么是TNotifyEvent?按住Ctrl点击鼠标就可以找到它的定义:
TNotifyEvent = procedure(Sender: TObject) of object;
就是说是一个函数指针。从这个原理上来猜,就是VCL框架让这个函数指针指向了程序员定义的那个函数,才使得程序员定义的函数自动被融入到VCL框架内得以正确执行。

2.然后在TControl里搜索,是谁在调用。发现有不少地方都调用了FOnClick事件。不过我们这个例子里,没有什么action在起作用,所以只能是TControl.Click函数在调用它,代码如下:

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;

再看它的定义,发现是一个动态函数:

procedure TControl.Click; dynamic;
于是下一步就该研究,是谁调用Click函数了。

题外话:为什么要搜索Controls单元?因为它第一次定义了OnClick事件,所以嫌疑最大。如果它那里只定义不处理,那也有是可能的。不过我的整个例子只涉及到TForm和TButton,除了这两个类本身要研究,还有就是它们的父类要研究,那样就缩小研究范围、一共只有几个类了,一定可以找到OnClick事件的来龙去脉。好在我们在TControl里就发现它了,那样就变得更简单了。关于这点完全是我自己的心得,别的文章可以把原理讲的更透彻,但是对于类似我这样的白痴产生更源头上的问题,只有我这样有相同的疑惑和经历才会讲到这一点。其实关于VCL我还有大量的疑惑没有解决(当然是在仔细研读了VCL代码基础上产生的大量问题,很多都是细节里的细节),如果哪位高手愿意与我探讨,我将不甚感激。

3. 很明显,对于一个动态函数,一旦其子类有覆盖函数,那么就会执行子类的覆盖函数。没有就拉倒,变得更简单了。搜索TButton及其父类TButtonControl,我们果然在TButton里发现了它的覆盖函数:procedure Click; override; 即:

procedure TButton.Click;
var
Form: TCustomForm;
begin
Form := GetParentForm(Self);
if Form <> nil then Form.ModalResult := ModalResult;
inherited Click;
end;

有趣的是,我们发现这个覆盖函数仅仅改写父窗体的状态,它本身并不真正执行程序员事件,还是要inherited Click;也就是TControl.Click来执行程序员的事件。猜测这么做是因为放在TControl里可以让图形控件也拥有Click的能力。

4. 我们继续搜索,发现在TControl.WMLButtonUp函数里调用Click;函数(注意,到这步分析错了,大家不要往下看了,稍后纠正)

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 // API
Click; // 类函数
end;
DoMouseUp(Message, mbLeft);
end;

正是它响应了WM_LBUTTONUP消息。注意啊,OnClick事件只有放开鼠标的时候才执行,否则是不会执行的;而且因为PtInRect函数判断的关系,按下按以后,鼠标是不能移到Button1范围之外,否则也不会执行Click函数,不信可以试试。

5. 至此就可以明白,只要鼠标点击Button1,鼠标就会产生WM_LBUTTONUP消息并发送给Button1,VCL内建的消息循环必然会找到TControl.WMLButtonUp从而执行Click函数。但是它会先执行TButton.Click;函数,这个Click函数做了两件事情:先通知祖先Form(一定是Form,而不是别的窗口,而且也不必是直接的父窗口,可以是间接的。所以Form里放一个TPanel,TPanel里放一个TButton也还是可以找到这个Form,这样间接的按钮照样通过改变ModalResult照样关闭一个Form)的ModalResult属性状态被改变了,然后执行要inherited Click;也就是TControl.Click;这个函数里面有这句:if Assigned(FOnClick) then FOnClick(Self); 如果FOnClick有值了,或者说不再是一个空指针了,那么它就会调用函数指针FOnClick执行的那个函数,而且还是带一个Self参数的函数。那么FOnClick是否有值了如何判断呢?简单呀,程序员双击Button1后,在Unit1.dfm里以下内容:

object Button1: TButton
Left = 256
Top = 72
Width = 75
Height = 25
Caption = 'Button1'
TabOrder = 0
OnClick = Button1Click // 这里,函数指针连接了程序员的函数!
OnMouseDown = Button1MouseDown
end

也就是说OnClick已经指向了程序员定义的函数(其实是IDE为自动产生,程序员填写执行内容的函数。通过手动赋值OnClick = Func1就可以指向另一个指定函数一点问题没有)。

OnClick的分析过程到此结束。OnMouseDown的分析过程,且听下回分解(我会继续编辑此文,而不是另开博文)。

--------------------------------------------------------------------------

有兴趣的还可以参考一下这篇文章,图文并茂,挺好的:
http://ymg97526.blog.163.com/blog/static/173658160201131021911946/

posted @ 2014-10-05 01:24  findumars  Views(3137)  Comments(0Edit  收藏  举报