TForm1.Button1Click($9637C0) TControl.Click TButton.Click TButton.CNCommand((48401, 660, 0, 524948, 0)) TControl.WndProc((48401, 660, 524948, 0, 660, 0, 660, 8, 0, 0)) TWinControl.WndProc((48401, 660, 524948, 0, 660, 0, 660, 8, 0, 0)) TButtonControl.WndProc((48401, 660, 524948, 0, 660, 0, 660, 8, 0, 0)) TControl.Perform(48401,660,524948) DoControlMsg(524948,(no value)) TWinControl.WMCommand((273, 660, 0, 524948, 0)) TCustomForm.WMCommand((273, 660, 0, 524948, 0)) TControl.WndProc((273, 660, 524948, 0, 660, 0, 660, 8, 0, 0)) TWinControl.WndProc((273, 660, 524948, 0, 660, 0, 660, 8, 0, 0)) TCustomForm.WndProc((273, 660, 524948, 0, 660, 0, 660, 8, 0, 0)) TWinControl.MainWndProc((273, 660, 524948, 0, 660, 0, 660, 8, 0, 0)) StdWndProc(918056,273,660,524948) TWinControl.DefaultHandler((no value)) TControl.WMLButtonUp((514, 0, 48, 13, (48, 13), 0)) TControl.WndProc((514, 0, 852016, 0, 0, 0, 48, 13, 0, 0)) TWinControl.WndProc((514, 0, 852016, 0, 0, 0, 48, 13, 0, 0)) TButtonControl.WndProc((514, 0, 852016, 0, 0, 0, 48, 13, 0, 0)) TWinControl.MainWndProc((514, 0, 852016, 0, 0, 0, 48, 13, 0, 0)) StdWndProc(524948,514,0,852016) TApplication.HandleMessage TApplication.Run Project1 |
一个 Button 被点击,在 TButton 内部会发生两个消息:WM_LBUTTONDOWN/WM_LBUTTONUP, TButton 没有处理 WM_LBUTTONUP(问题:为什么只响应 WM_LBUTTONUP,这两个消息只应该发生在 Windows 原生控件内,除非 TButton subclass 了 "Button",这部分代码我没看),只是交给 TWinControl.DefaultHandler,随后 TButton 又将生成的 WM_COMMAND 消息发送给它的 Parent,即 TForm,经过一系列消息传递, WM_COMMAND 在 TWinControl.WMCommand 中被处理,通过 DoControlMsg 将 WM_COMMAND 加工成 CN_COMMAND,再利用 TControl.Perform 将 CN_COMMAND 传回 TButton,又通过一系列的消息传递到 TButton 中的 Dispatch,通过查询动态方法表找到 Handler -- TButton.CNCommand,它又调用虚方法 TButton.Click,继而调用 TControl.Click,在这个方法中会调用 FOnClick,而 FOnClick 特性值的内容就是当程序员使用对象查看器撰写 TButton 的 OnClick 事件处理函数时 Delphi 便会自动指定给 TButton 的 OnClick 特性,例子中 OnClick 被指定为 TForm1.Button1Click,因此 TForm1.Button1Click 最终被调用。
2. TForm
新建一个 Application,为 Form1 的 OnMouseDown 事件随便写一点代码,在这个方法上设断点,F9 运行,看看 Call Stack
TForm1.FormMouseDown(???,???,[ssLeft],346,212) TControl.MouseDown(mbLeft,[ssLeft],346,212) TControl.DoMouseDown((513, 1, 346, 212, (346, 212), 0),mbLeft,[]) TControl.WMLButtonDown((513, 1, 346, 212, (346, 212), 0)) TControl.WndProc((513, 1, 13893978, 0, 1, 0, 346, 212, 0, 0)) TWinControl.WndProc((513, 1, 13893978, 0, 1, 0, 346, 212, 0, 0)) TCustomForm.WndProc((513, 1, 13893978, 0, 1, 0, 346, 212, 0, 0)) TWinControl.MainWndProc((513, 1, 13893978, 0, 1, 0, 346, 212, 0, 0)) StdWndProc(2687598,513,1,13893978) TApplication.HandleMessage TApplication.Run Project1 |
鼠标在 Form 上点击,产生两个消息 WM_LBUTTONDOWN/WM_LBUTTONUP,但我们只截获 WM_LBUTTONDOWN。产生的 WM_LBUTTONDOWN 经过一系列的消息传递到达 TObject.Dispatch,通过查询动态方法表在 TForm 的父类 TControl 中找到了 Handler -- TControl.WMLButtonDown,在 TControl.WMLButtonDown 中又经过 TControl.DoMouseDown、TControl.MouseDown 一系列方法调用,最终调用到 FOnMouseDown,FOnMouseDown 被赋值为 TForm1.FormMouseDown,调用 FOnMouseDown 即调用 TForm1.FormMouseDown。
讲了一大堆消息实现过程,那么在实际中到底有哪些应用?
四,消息的实际应用
如果你是共享软件作者,经常会为你的软件被 Crack 掉所烦恼,你能做的就是要加强你的软件的 Anti-Crack 功能,今天就交你一招。
如果你用过 Delphi 的专用反汇编工具 DEDE,那么你肯定知道像 Button1Click 这种 Event Handler 的方法入口地址 极容易被定位,其原理是根据TForm 的 RTTI 信息获取的(通过分析 dfm 资源文件就可以获得地址),其实 VCL 窗体只有 published 过的类成员才会生成 RTTI 信息。知道这个关键点加上对 VCL 消息机制的深入了解你就可以防止这一切发生。
1. Anti-Crack
新建一个 Application,在 Form1 上放两个 Button,命名为 btnRegister、btnCancel,双击这两个按钮,分别生成TForm1.btnCancelClick、TForm1.btnRegisterClick 两个 Event Handler 骨架代码,然后在对象查看器中取消 btnRegister.OnClick 与 TForm1.btnRegisterClick 的关联, 随后将 TForm1.btnCancelClick 的声明放入 TForms1 声明的 private 区段。再按照下面的代码 内容加入其他部分:
unit Unit1; |
这个方法的本质就是截获 TForm1 的 WM_COMMAND 消息并自己处理,请自行分析代码,我就不多说了。编译完后你可以用 DEDE 反汇编一下,看看还能不能那么容易地找到 TForm1.btnRegisterClick 的入口地址。
结束语
VCL 消息机制你理解了吗?是不是感到特别复杂?一个消息往往要经过10几个方法才能传到 Event Handler,别看消息传递经过这么漫长的路途,但是 VCL 消息机制的效率还是非常高的,因为很多关键的代码都是用汇编直接写成的,每一个中途站花费的时间也非常少,因此需要处理的消息还是能很快地到达目的地。
我最开始学 Windows 编程是从 SDK 开始学起的,那时候会写了基本的 Windows 程序,一段时间内总认为会 SDK 比会用 Delphi 牛X,现在想起来真傻,比起直来直去的 SDK 编程,VCL 消息机制要复杂得多得多,看完 VCL 源码后最大感受就是觉得以前跟没学过编程似的,但不可否认的是,只有在你掌握了 OOP/ASM/SDK 这些基础知识后,你才有看懂 VCL 源码的资本,这些基础知识你都掌握了吗?
参考文献
1. 李维.《深入核心 -- VCL架构剖析》第四、五章,2004.1
2. savetime."Delphi 的消息机制浅探", Jan 2004
3. cheka."VCL窗口函数注册机制研究手记,兼与MFC比较", 2001