zoukankan      html  css  js  c++  java
  • 点击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/

  • 相关阅读:
    vue打包配置发布路径
    qt编程参考资料
    qt下载地址
    树结构遍历节点名字提取,这里提取的是el-tree数据结构,封装成函数
    set实现数组去重后是对象,这里转化为数组
    js中的async await
    微信小程序调微信支付
    Http权威指南(TCP连接)
    Http权威指南(报文)
    Http权威指南(概述篇总结)
  • 原文地址:https://www.cnblogs.com/findumars/p/4006617.html
Copyright © 2011-2022 走看看