问题的来源:在李维的《深入浅出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/