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/

  • 相关阅读:
    LeetCode OJ 107. Binary Tree Level Order Traversal II
    LeetCode OJ 116. Populating Next Right Pointers in Each Node
    LeetCode OJ 108. Convert Sorted Array to Binary Search Tree
    LeetCode OJ 105. Construct Binary Tree from Preorder and Inorder Traversal
    LeetCode OJ 98. Validate Binary Search Tree
    老程序员解Bug的通用套路
    转载 四年努力,梦归阿里,和大家聊聊成长感悟
    转载面试感悟----一名3年工作经验的程序员应该具备的技能
    Web Service和Servlet的区别
    关于spring xml文件中的xmlns,xsi:schemaLocation
  • 原文地址:https://www.cnblogs.com/findumars/p/4006617.html
Copyright © 2011-2022 走看看