看李维的内幕或者看savetime大虾的东西
http://www.delphibbs.com/delphibbs/dispq.asp?lid=2403549
标题: Delphi 的消息机制浅探
-------------------------转自网络
savetime2k@yahoo.com 2004.1.9
我从去年 12 月上旬开始等待李维的《Inside VCL》。我当时的计划是,在这本书的指导下深入学习 Delphi。到了 12 月底,书还没有出来,我不愿再等,开始阅读 VCL 源代码。在读完 TObject、TPersistant 和 TComponent 的代码之后,我发现还是不清楚 Delphi 对象到底是怎样被创建的。于是我查看 Delphi 生成的汇编代码,终于理解了对象创建的整个过程(这里要特别感谢 book523 的帮助)。
此后我就开始学习 Delphi VCL 的消息处理机制。自从我写下《Delphi的对象机制浅探》,至今正好一个星期,我也基本上把 Delphi VCL 的消息处理框架读完了。我的学习方法就是阅读源代码,一开始比较艰苦,后来线索逐渐清晰起来。在此把自己对 Delphi VCL 消息机制的理解记录下来,便于今后的复习,也给初学 Delphi 或没有时间阅读 VCL 源代码的朋友参考(毕竟没有几个程序员像我这样有时间
。由于学习时间较短,一定会有错误,请大家指正。
我在分析 VCL 消息机制的过程中,基本上只考查了三个类 TObject、TControl 和 TWinControl。虽然我没有阅读上层类(如 TForm)的代码,但我认为这些都是实现的细节。我相信 VCL 消息系统中最关键的东西都在这三个类中。纲举而目张,掌握基础类的消息处理方法之后再读其他类的消息处理过程就容易得多了。
要想读懂本文,最低配置为:
了解 Win32 消息循环和窗口过程
基本了解 TObject、TControl 和 TWinControl 实现的内容
熟悉 Delphi 对象的重载与多态
推荐配置为:
熟悉 Win32 SDK 编程
熟悉 Delphi 的对象机制
熟悉 Delphi 内嵌汇编语言
推荐阅读:
《Delphi 的原子世界》
http://www.codelphi.com/
《VCL窗口函数注册机制研究手记,兼与MFC比较》
http://www.delphibbs.com/delphibbs/dispq.asp?lid=584889
《Delphi的对象机制浅探》
http://www.delphibbs.com/delphibbs/dispq.asp?LID=2390131
本文排版格式为:
正文由窗口自动换行;所有代码以 80 字符为边界;中英文字符以空格符分隔。
(作者保留对本文的所有权利,未经作者同意请勿在在任何公共媒体转载。)
目 录
===============================================================================
⊙ 一个 GUI Application 的执行过程:消息循环的建立
⊙ TWinControl.Create、注册窗口过程和创建窗口
⊙ 补充知识:TWndMethod 概述
⊙ VCL 的消息处理从 TWinControl.MainWndProc 开始
⊙ TWinControl.WndProc
⊙ TControl.WndProc
⊙ TObject.Dispatch
⊙ TWinControl.DefaultHandler
⊙ TControl.Perform 和 TWinControl.Broadcast
⊙ TWinControl.WMPaint
⊙ 以 TWinControl 为例描述消息传递的路径
===============================================================================
正 文
===============================================================================
⊙ 一个 GUI Application 的执行过程:消息循环的建立
===============================================================================
通常一个 Win32 GUI 应用程序是围绕着消息循环的处理而运行的。在一个标准的 C 语言 Win32 GUI 程序中,主程序段都会出现以下代码:
while (GetMessage(&msg, NULL, 0, 0)) // GetMessage 第二个参数为 NULL,
// 表示接收所有应用程序产生的窗口消息
{
TranslateMessage(&msg)
// 转换消息中的字符集
DispatchMessage(&msg)
// 把 msg 参数传递给 lpfnWndProc
}
lpfnWndProc 是 Win32 API 定义的回调函数的地址,其原型如下:
int __stdcall WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
Windows 回调函数(callback function) 也通常被称为窗口过程(window procedure),本文随意使用这两个名称,代表同样的意义。
应用程序使用 GetMessage 不断检查应用程序的消息队列中是否有消息到达。如果发现了消息,则调用 TranslateMessage。TranslateMessage 主要是做字符消息本地化的工作,不是关键的函数。然后调用 DispatchMessage(&msg)。DispatchMessage(&msg) 使用 msg 为参数调用已创建的窗口的回调函数(WndClass.lpfnWndProc)。lpfnWndProc 是由用户设计的消息处理方法。
当 GetMessage 在应用程序的消息队列中发现一条 WM_QUIT 消息时,GetMessage 返回 False,消息循环才告结束,通常应用程序在这时清理资源后也结束运行。
使用最原始的 Win32 API 编写的应用程序的执行过程是很容易理解的,但是用 Delphi VCL 组件封装消息系统,并不是容易的事。首先,Delphi 是一种面向对象的程序设计语言,不但要把 Win32 的消息处理过程封装在对象的各个继承类中,让应用程序的使用者方便地调用,也要让 VCL 组件的开发者有拓展消息处理的空间。其次,Delphi 的对象模型中所有的类方法都是对象相关的(也就是传递了一个隐含的参数 Self),所以 Delphi 对象的方法不能直接被 Windows 回调。Delphi VCL 必须用其他的方法让 Windows 回调到对象的消息处理函数。
让我们跟踪一个标准的 Delphi Application 的执行过程,查看 Delphi 是如何开始一个消息循环的。
program Project1;
begin
Application.Initialize;
Application.CreateForm(TForm1, Form1);
Application.Run;
end.
在 Project1 的 Application.Initialize 之前,Delphi 编译器会自动插入一行代码:
SysInit._InitExe。_InitExe 主要是初始化 HInstance 和模块信息表等。然后 _InitExe 调用 System._StartExe。System._StartExe 调用 System.InitUnit;System.InitUnit 调用项目中所有被包含单元的 Initialization 段的代码;其中有 Controls.Initialization 段,这个段比较关键。在这段代码中建立了 Mouse、Screen 和 Application 三个关键的全局对象。
Application.Create 调用 Application.CreateHandle。Application.CreateHandle 建立一个窗口,并设置 Application.WndProc 为回调函数(这里使用了 MakeObjectInstance 方法,后面再谈)。Application.WndProc 主要处理一些应用程序级别的消息。
我第一次跟踪应用程序的执行时没有发现 Application 对象的创建过程,原来在 SysInit._InitExe 中被隐含调用了。如果你想跟踪这个过程,不要设置断点,直接按 F7 就发现了。
然后才到了 Project1 的第 1 句: Application.Initialize;
这个函数只有一句代码:
if InitProc <> nil then TProcedure(InitProc);
也就是说如果用户想在应用程序的执行前运行一个特定的过程,可以设置 InitProc 指向该过程。(为什么用户不在 Application.Initialize 之前或在单元的 Initliazation 段中直接运行这个特定的过程呢?一个可能的答案是:如果元件设计者希望在应用程序的代码执行之前执行一个过程,并且这个过程必须在其他单元的 Initialization 执行完成之后执行[比如说 Application 对象必须创建],则只能使用这个过程指针来实现。)
然后是 Project1 的第 2 句: Application.CreateForm(TForm1, Form1);
这句的主要作用是创建 TForm1 对象,然后把 Application.MainForm 设置为 TForm1。
最后是 Project1 的第 3 句: Application.Run;
TApplication.Run 调用 TApplication.HandleMessage 处理消息。Application.HandleMessage 的代码也只有一行:
if not ProcessMessage(Msg) then Idle(Msg);
TApplication.ProcessMessage 才真正开始建立消息循环。ProcessMessage 使用 PeekMessage API 代替 GetMessage 获取消息队列中的消息。使用 PeekMessage 的好处是 PeekMessage 发现消息队列中没有消息时会立即返回,这样就为 HandleMessage 函数执行 Idle(Msg) 提供了依据。
ProcessMessage 在处理消息循环的时候还特别处理了 HintMsg、MDIMsg、KeyMsg、DlgMsg 等特殊消息,所以在 Delphi 中很少再看到纯 Win32 SDK 编程中的要区分 Dialog Window、MDI Window 的处理,这些都被封装到 TForm 中去了(其实 Win32 SDK 中的 Dialog 也是只是 Microsoft 专门写了一个窗口过程和一组函数方便用户界面的设计,其内部运作过程与一个普通窗口无异)。
function TApplication.ProcessMessage(var Msg: TMsg): Boolean;
var
Handled: Boolean;
begin
Result := False;
if PeekMessage(Msg, 0, 0, 0, PM_REMOVE) then // 从消息队列获取消息
begin
Result := True;
if Msg.Message <> WM_QUIT then
begin
Handled := False
// Handled 表示 Application.OnMessage 是否已经处理过
// 当前消息。
// 如果用户设置了Application.OnMessage 事件句柄,
// 则先调用 Application.OnMessage
if Assigned(FOnMessage) then FOnMessage(Msg, Handled)
if not IsHintMsg(Msg) and not Handled and not IsMDIMsg(Msg) and
not IsKeyMsg(Msg) and not IsDlgMsg(Msg) then
// 思考:not Handled 为什么不放在最前?
begin
TranslateMessage(Msg)
// 处理字符转换
DispatchMessage(Msg)
// 调用 WndClass.lpfnWndProc
end;
end
else
FTerminate := True
// 收到 WM_QUIT 时应用程序终止
// (这里只是设置一个终止标记)
end
end;
从上面的代码来看,Delphi 应用程序的消息循环机制与标准 Win32 C 语言应用程序差不多。只是 Delphi 为了方便用户的使用设置了很多扩展空间,其副作用是消息处理会比纯 C Win32 API 调用效率要低一些。
===============================================================================
⊙ TWinControl.Create、注册窗口过程和创建窗口
===============================================================================
上面简单讨论了一个 Application 的建立到形成消息循环的过程,现在的问题是 Delphi 控件是如何封装创建窗口这一过程的。因为只有建立了窗口,消息循环才有意义。
让我们先回顾 Delphi VCL中几个主要类的继承架框:
TObject 所有对象的基类
TPersistent 所有具有流特性对象的基类
TComponent 所有能放在 Delphi Form Designer 上的对象的基类
TControl 所有可视的对象的基类
TWinControl 所有具有窗口句柄的对象基类
Delphi 是从 TWinControl 开始实现窗口相关的元件。所谓窗口,对于程序设计者来说,就是一个窗口句柄 HWND。TWinControl 有一个 FHandle 私有成员代表当前对象的窗口句柄,通过 TWinControl.Handle 属性来访问。
我第一次跟踪 TWinControl.Create 过程时,竟然没有发现 CreateWindow API 被调用,说明 TWinControl 并不是在对象创建时就建立 Windows 窗口。如果用户使用 TWinControl.Create(Application) 以后,立即使用 Handle 访问窗口会出现什么情况呢?
答案在 TWinControl.GetHandle 中,Handle 是一个只读的窗口句柄:
property TWinControl.Handle: HWnd read GetHandle;
TWinControl.GetHandle 代码的内容是:一旦用户要访问 FHandle 成员,TWinControl.HandleNeeded 就会被调用。HandleNeeded 首先判断 TWinControl.FHandle 是否是等于 0 (还记得吗?任何对象调用构造函数以后所有对象成员的内存都被清零)。如果 FHandle 不等于 0,则直接返回 FHandle;如果 FHandle 等于 0,则说明窗口还没有被创建,这时 HandleNeeded 自动调用 TWinControl.CreateHandle 来创建一个 Handle。但 CreateHandle 只是个包装函数,它首先调用 TWinControl.CreateWnd 来创建窗口,然后生成一些维护 VCL Control 运行的参数(我还没细看)。CreateWnd 是一个重要的过程,它先调用 TWinControl.CreateParams 设置创建窗口的参数。(CreateParams 是个虚方法,也就是说程序员可以重载这个函数,定义待建窗口的属性。) CreateWnd 然后调用 TWinControl.CreateWindowHandle。CreateWindowHandle 才是真正调用 CreateWindowEx API 创建窗口的函数。
够麻烦吧,我们可以抱怨 Borland 为什么把事情弄得这么复杂,但最终希望 Borland 这样设计自有它的道理。上面的讨论可以总结为 TWinControl 为了为了减少系统资源的占用尽量推迟建立窗口,只在某个方法需要调用到控件的窗口句柄时才真正创建窗口。这通常发生在窗口需要显示的时候。一个窗口是否需要显示常常发生在对 Parent 属性 (在TControl 中定义) 赋值的时候。设置 Parent 属性时,TControl.SetParent 方法会调用 TWinControl.RemoveControl 和 TWinControl.InsertControl 方法。InsertControl 调用 TWinControl.UpdateControlState。UpdateControlState 检查 TWinControl.Showing 属性来判断是否要调用 TWinControl.UpdateShowing。UpdateShowing 必须要有一个窗口句柄,因此调用 TWinControl.CreateHandle 来创建窗口。
不过上面说的这些,只是繁杂而不艰深,还有很多关键的代码没有谈到呢。
你可能发现有一个关键的东西被遗漏了,对,那就是窗口的回调函数。由于 Delphi 建立一个窗口的回调过程太复杂了(并且是非常精巧的设计),只好单独拿出来讨论。
cheka 的《VCL窗口函数注册机制研究手记,兼与MFC比较》一文中对 VCL 的窗口回调实现进行了深入的分析,请参考:http://www.delphibbs.com/delphibbs/dispq.asp?lid=584889
我在此简单介绍回调函数在 VCL 中的实现:
TWinControl.Create 的代码中,第一句是 inherited,第二句是
FObjectInstance := Classes.MakeObjectInstance(MainWndProc);
我想这段代码可能吓倒过很多人,如果没有 cheka 的分析,很多人难以理解。但是你不一定真的要阅读 MakeObjectInstance 的实现过程,你只要知道:
MakeObjectInstance 在内存中生成了一小段汇编代码,这段代码的内容就是一个标准的窗口过程。这段汇编代码中同时存储了两个参数,一个是 MainWndProc 的地址,一个是 Self (对象的地址)。这段汇编代码的功能就是使用 Self 参数调用 TWinControl.MainWndProc 函数。
MakeObjectInstance 返回后,这段代码的地址存入了 TWinControl.FObjectInstance 私有成员中。
这样,TWinControl.FObjectInstance 就可以当作标准的窗口过程来用。你可能认为 TWinControl 会直接把 TWinControl.FObjectInstance 注册为窗口类的回调函数(使用 RegisterClass API),但这样做是不对的。因为一个 FObjectInstance 的汇编代码内置了对象相关的参数(对象的地址 Self),所以不能用它作为公共的回调函数注册。TWinControl.CreateWnd 调用 CreateParams 获得要注册的窗口类的资料,然后使用 Controls.pas 中的静态函数 InitWndProc 作为窗口回调函数进行窗口类的注册。InitWndProc 的参数符合 Windows 回调函数的标准。InitWndProc 第一次被回调时就把新建窗口(注意不是窗口类)的回调函数替换为对象的 TWinControl.FObjectInstance (这是一种 Windows subclassing 技术),并且使用 SetProp 把对象的地址保存在新建窗口的属性表中,供 Delphi 的辅助函数读取(比如 Controls.pas 中的 FindControl 函数)。
总之,TWinControl.FObjectInstance 最终是被注册为窗口回调函数了。
这样,如果 TWinControl 对象所创建的窗口收到消息后(形象的说法),会被 Windows 回调 TWinControl.FObjectInstance,而 FObjectInstance 会呼叫该对象的 TWinControl.MainWndProc 函数。就这样 VCL 完成了对象的消息处理过程与 Windows 要求的回调函数格式差异的转换。注意,在转换过程中,Windows 回调时传递进来的第一个参数 HWND 被抛弃了。因此 Delphi 的组件必须使用 TWinControl.Handle (或 protected 中的 WindowHandle) 来得到这个参数。Windows 回调函数需要传回的返回值也被替换为 TMessage 结构中的最后一个字段 Result。
为了使大家更清楚窗口被回调的过程,我把从 DispatchMessage 开始到 TWinControl.MainWndProc 被调用的汇编代码(你可以把从 FObjectInstance.Code 开始至最后一行的代码看成是一个标准的窗口回调函数):
DispatchMessage(&Msg) // Application.Run 呼叫 DispatchMessage 通知
// Windows 准备回调
Windows 准备回调 TWinControl.FObjectInstance 前在堆栈中设置参数:
push LPARAM
push WPARAM
push UINT
push HWND
push (eip.Next)
把Windows 回调前下一条语句的地址
保存在堆栈中
jmp FObjectInstance.Code
调用 TWinControl.FObjectInstance
FObjectInstance.Code 只有一句 call 指令:
call ObjectInstance.offset
push eip.Next
jmp InstanceBlock.Code
调用 InstanceBlock.Code
InstanceBlock.Code:
pop ecx
将 eip.Next 的值存入 ecx, 用于
取 @MainWndProc 和 Self
jmp StdWndProc
跳转至 StdWndProc
StdWndProc 的汇编代码:
function StdWndProc(Window: HWND
Message, WParam: Longint;
LParam: Longint): Longint
stdcall
assembler;
asm
push ebp
mov ebp, esp
XOR EAX,EAX
xor eax, eax
PUSH EAX
push eax
设置 Message.Result := 0
PUSH LParam
为什么 Borland 不从上面的堆栈中直接
push dword ptr [ebp+$14]
获取这些参数而要重新 push 一遍?
PUSH WParam
因为 TMessage 的 Result 是
push dword ptr [ebp+$10]
记录的最后一个字段,而回调函数的 HWND
PUSH Message
是第一个参数,没有办法兼容。
push dword ptr [ebp+$0c]
MOV EDX,ESP
mov edx, esp
设置 Message 在堆栈中的地址为
MainWndProc 的参数
MOV EAX,[ECX].Longint[4]
mov eax, [ecx+$04]
设置 Self 为 MainWndProc 的隐含参数
CALL [ECX].Pointer
call dword ptr [ecx] : 呼叫 TWinControl.MainWndProc(Self,
@Message)
ADD ESP,12
add esp, $0c
POP EAX
pop eax
end;
pop ebp
ret $0010
mov eax, eax
看不懂上面的汇编代码,不影响对下文讨论的理解。
===============================================================================
⊙ 补充知识:TWndMethod 概述
===============================================================================
写这段基础知识是因为我在阅读 MakeObjectInstance(MainWndProc) 这句时不知道究竟传递了什么东西给 MakeObjectInstance。弄清楚了 TWndMethod 类型的含义还可以理解后面 VCL 消息系统中的一个小技巧。
TWndMethod = procedure(var Message: TMessage) of object;
这句类型声明的意思是:TWndMethod 是一种过程类型,它指向一个接收 TMessage 类型参数的过程,但它不是一般的静态过程,它是对象相关(object related)的。TWndMethod 在内存中存储为一个指向过程的指针和一个对象的指针,所以占用8个字节。TWndMethod类型的变量必须使用已实例化的对象来赋值。举个例子:
var
SomeMethod: TWndMethod;
begin
SomeMethod := Form1.MainWndProc
// 正确。这时 SomeMethod 包含 MainWndProc
// 和 Form1 的指针,可以用 SomeMethod(Msg)
// 来执行。
SomeMethod := TForm.MainWndProc
// 错误!不能用类引用。
end;
如果把 TWndMethod变量赋值给虚方法会怎样?举例:
var
SomeMethod: TWndMethod;
begin
SomeMethod := Form1.WndProc
// TForm.WndProc 是虚方法
end;
这时,编译器实现为 SomeMethod 指向 Form1 对象虚方法表中的 WndProc 过程的地址和 Form1 对象的地址。也就是说编译器正确地处理了虚方法的赋值。调用 SomeMethod(Message) 就等于调用 Form1.WndProc(Message)。
在可能被赋值的情况下,对象方法最好不要设计为有返回值的函数(function),而要设计为过程(procedure)。原因很简单,把一个有返回值的对象方法赋值给 TWndMethod 变量,会造成编译时的二义性。
===============================================================================
⊙ VCL 的消息处理从 TWinControl.MainWndProc 开始
===============================================================================
通过对 Application.Run、TWinControl.Create、TWinControl.Handle 和 TWinControl.CreateWnd 的讨论,我们现在可以把焦点转向 VCL 内部的消息处理过程。VCL 控件的消息源头就是 TWinControl.MainWndProc 函数。(如果不能理解这一点,请重新阅读上面的讨论。)
让我们先看一下 MainWndProc 函数的代码(异常处理的语句被我删除):
procedure TWinControl.MainWndProc(var Message: TMessage);
begin
WindowProc(Message);
end;
TWinControl.MainWndProc 以引用(也就是隐含传地址)的方式接受一个 TMessage 类型的参数,TMessage 的定义如下(其中的WParam、LParam、Result 各有 HiWord 和 LoWord 的联合字段,被我删除了,免得代码太长):
TMessage = packed record
Msg: Cardinal;
WParam: Longint;
LParam: Longint;
Result: Longint);
end;
TMessage 中并没有窗口句柄,因为这个句柄已经在窗口创建之后保存在 TWinControl.Handle 之中。TMessage.Msg 是消息的 ID 号,这个消息可以是 Windows 标准消息、用户定义的消息或 VCL 定义的 Control 消息等。WParam 和 LParam 与标准 Windows 回调函数中 wParam 和 lParam 的意义相同,Result 相当于标准 Windows 回调函数的返回值。
注意 MainWndProc 不是虚函数,所以它不能被 TWinControl 的继承类重载。(思考:为什么 Borland 不将 MainWndProc 设计为虚函数呢?)
MainWndProc 中建立两层异常处理,用于释放消息处理过程中发生异常时的资源泄漏,并调用默认的异常处理过程。被异常处理包围着的是 WindowProc(Message)。WindowProc 是 TControl(而不是 TWinControl) 的一个属性(property):
property WindowProc: TWndMethod read FWindowProc write FWindowProc;
WindowProc 的类型是 TWndMethod,所以它是一个对象相关的消息处理函数指针(请参考前面 TWndMethod 的介绍)。在 TControl.Create 中 FWindowProc 被赋值为 WndProc。
WndProc 是 TControl 的一个函数,参数与 TWinControl.MainWndProc 相同:
procedure TControl.WndProc(var Message: TMessage)
virtual;
原来 MainWndProc 只是个代理函数,最终处理消息的是 TControl.WndProc 函数。
那么 Borland 为什么要用一个 FWindowProc 来存储这个 WndProc 函数,而不直接调用 WndProc 呢?我猜想可能是基于效率的考虑。还记得上面 TWndMethod 的讨论吗?一个 TWndMethod 变量可以被赋值为一个虚函数,编译器对此操作的实现是通过对象指针访问到了对象的虚函数表,并把虚函数表项中的函数地址传回。由于 WndProc 是一个调用频率非常高的函数(可能要用“百次/秒”或“千次/秒”来计算),所以如果每次调用 WndProc 都要访问虚函数表将会浪费大量时间,因此在 TControl 的构造函数中就把 WndProc 的真正地址存储在 WindowProc 中,以后调用 WindowProc 将就转换为静态函数的调用,以加快处理速度。
===============================================================================
⊙ TWinControl.WndProc
===============================================================================
转了层层弯,到现在我们才刚进入 VCL 消息系统处理开始的地方:WndProc 函数。如前所述,TWinControl.MainWndProc 接收到消息后并没有处理消息,而是把消息传递给 WindowProc 处理。由于 WindowProc 总是指向当前对象的 WndProc 函数的地址,我们可以简单地认为 WndProc 函数是 VCL 中第一个处理消息的函数,调用 WindowProc 只是效率问题。
WndProc 函数是个虚函数,在 TControl 中开始定义,在 TWinControl 中被重载。Borland 将 WndProc 设计为虚函数就是为了各继承类能够接管消息处理,并把未处理的消息或加工过的消息传递到上一层类中处理。
这里将消息处理的传递过程和对象的构造函数稍加对比:
对象的构造函数通常会在第一行代码中使用 inherited 语句调用父类的构造函数以初始化父类定义的成员变量,父类也会在构造函数开头调用祖父类的构造函数,如此递归,因此一个 TWinControl 对象的创建过程是 TComponent.Create -> TControl.Create -> TWinControl.Create。
而消息处理函数 WndProc 则是先处理自己想要的消息,然后看情况是否要递交到父类的 WndProc 中处理。所以消息的处理过程是 TWinControl.WndProc -> TControl.WndProc。
因此,如果要分析消息的处理过程,应该从子类的 WndProc 过程开始,然后才是父类的 WndProc 过程。由于 TWinControl 是第一个支持窗口创建的类,所以它的 WndProc 是很重要的,它实现了最基本的 VCL 消息处理。
TWinControl.WndProc 主要是预处理一些键盘、鼠标、窗口焦点消息,对于不必响应的消息,TWinControl.WndProc 直接返回,否则把消息传递至 TControl.WndProc 处理。
从 TWinControl.WndProc 摘抄一段看看:
WM_KEYFIRST..WM_KEYLAST:
if Dragging then Exit
// 注意:使用 Exit 直接返回
这段代码的意思是:如果当前组件正处于拖放状态,则丢弃所有键盘消息。
再看一段:
WM_MOUSEFIRST..WM_MOUSELAST:
if IsControlMouseMsg(TWMMouse(Message)) then
begin
{ Check HandleAllocated because IsControlMouseMsg might have freed the
window if user code executed something like Parent := nil. }
if (Message.Result = 0) and HandleAllocated then
DefWindowProc(Handle, Message.Msg, Message.wParam, Message.lParam);
// DefWindowProc 是 Win32 API 中缺省处理消息的函数
Exit;
end;
这里的 IsControlMouseMsg 很关键。让我们回忆一下:TControl 类的对象并没有创建 Windows 窗口,它是怎样接收到鼠标和重绘等消息的呢?原来这些消息就是由它的 Parent 窗口发送的。
在上面的代码中,TWinControl.IsControlMouseMsg 判断鼠标地址是否落在 TControl 类控件上,如果不是就返回否值。TWinControl 再调用 TControl.WndProc,TControl.WndProc 又调用了 TObject.Dispatch 方法,这是后话。
如果当前鼠标地址落在窗口上的 TControl 类控件上,则根据 TControl 对象的相对位置重新生成了鼠标消息,再调用 TControl.Perform 方法把加工过的鼠标消息直接发到 TControl.WndProc 处理。TControl.Perform 方法以后再谈。
如果 TWinControl 的继承类重载 WndProc 处鼠标消息,但不使用 inherited 把消息传递给父类处理,则会使从 TControl 继承下来的对象不能收到鼠标消息。现在我们来做个试验,下面 Form1 上的 TSpeedButton 等非窗口控件不会发生 OnClick 等鼠标事件。
procedure TForm1.WndProc(var Message: TMessage)
override;
begin
case Message.Msg of
WM_MOUSEFIRST..WM_MOUSELAST:
begin
DefWindowProc(Handle, Message.Msg, Message.WParam, Message.LParam);
Exit
// 直接退出
end;
else
inherited;
end;
end;
TWinControl.WndProc 的最后一行代码是:
inherited WndProc(Message);
也就是调用 TControl.WndProc。让我们来看看 TControl.WndProc 做了些什么。
===============================================================================
⊙ TControl.WndProc
===============================================================================
TControl.WndProc 主要实现的操作是:
响应与 Form Designer 的交互(在设计期间)
在控件不支持双击的情况下把鼠标双击事件转换成单击
判断鼠标移动时是否需要显示提示窗口(HintWindow)
判断控件是否设置为 AutoDrag,如果是则执行控件的拖放处理
调用 TControl.MouseWheelHandler 实现鼠标滚轮消息
使用 TObject.Dispatch 调用 DMT 消息处理方法
TControl.WndProc 相对比较简单,在此只随便谈谈第二条。你是否有过这样的使用经验:在你快速双击某个软件的 Button 时,只形成一次 Click 事件。所以如果你需要设计一个不管用户用多快的速度点击,都能生成同样点击次数 Click 事件的按钮时,就需要参考 TControl.WndProc 处理鼠标消息的过程了。
TControl.WndProc 最后一行代码是 Dispatch(Message),也就是说如果某个消息没有被 TControl 以后的任何类处理,消息会被 Dispatch 处理。
TObject.Dispatch 是 Delphi VCL 消息体系中非常关键的方法。
===============================================================================
⊙ TObject.Dispatch
===============================================================================
TObject.Dispatch 是个虚函数,它的声明如下:
procedure TObject.Dispatch(var Message)
virtual;
请注意它的参数虽然与 MainWndProc 和 WndProc 的参数相似,但它没有规定参数的类型。这就是说,Dispatch 可以接受任何形式的参数。
Delphi 的文档指出:Message参数的前 2 个字节是 Message 的 ID(下文简称为 MsgID),通过 MsgID 搜索对象的消息处理方法。
这段话并没有为我们理解 Dispatch 方法提供更多的帮助,看来我们必须通过阅读源代码来分析这个函数的运作过程。
TObject.Dispatch 虽然是个虚方法,但却没有被 TPersistent、TComponent、TControl、TWinControl、TForm 等后续类重载( TCommonDialog 调用了 TObject.Dispatch,但对于整个 VCL 消息系统并不重要),并且只由 TControl.WndProc 调用过。所以可以简单地认为如果消息没有在 WndProc 中被处理,则被 TObject.Dispatch 处理。
我们很容易查觉到一个很重要的问题:MsgID 是 2 个字节,而 TMessage.Msg 是 4 个字节,如果 TControl.WndProc 把 TMessage 消息传递给 Dispatch 方法,是不是会形成错误的消息呢?
要解释这个问题,必须先了解 Windows 消息的规则。由于 Windows 操作系统的所有窗口都使用消息传递事件和信息,Microsoft 必须制定窗口消息的格式。如果每个程序员都随意定义消息 ID 值肯定会产生混乱。Microsoft 把窗口消息分为五个区段:
0x00000000 至 WM_USER - 1 标准视窗消息,以 WM_ 为前缀
WM_USER 至 WM_APP - 1 用户自定义窗口类的消息
WM_APP 至 0x0000BFFF 应用程序级的消息
0x0000C000 至 0x0000FFFF RegisterWindowMessage 生成的消息范围
0x00010000 至 0xFFFFFFFF Microsoft 保留的消息,只由系统使用
( WM_USER = 0x00000400, WM_APP = 0x00008000 )
发现问题的答案了吗?原来应用程序真正可用的消息只有 0x00000000 至 0x0000FFFF,也就是消息 ID 只有低位 2 字节是有效的。(Borland 真是牛啊,连这也能想出来。)
由于 Intel CPU 的内存存放规则是高位字节存放在高地址,低位字节存放在低地址,所以 Dispatch 的 Message 参数的第一个内存字节就是 LoWord(Message.Msg)。下图是 Message参数的内存存放方式描述:
| | + Memory
|--------|
| HiWord |
|--------|
| LoWord | <-- [EDX]
|--------|
| |
|--------|
| |
|--------| - Memory
[ 图示:Integer 类型的 MsgID 在内存中的分配(见 Dispatch 汇编代码) ]
(为了简单起见,我用 Word 为内存单位而不是 Byte,希望不至于更难看懂)
现在可以开始阅读 TObject.Dispatch 的汇编代码了(不懂汇编没关系,后面会介绍具体的功能):
procedure TObject.Dispatch(var Message)
virtual
asm
PUSH ESI
保存 ESI
MOV SI,[EDX]
把 MsgID 移入 SI (2 bytes)
如果 MsgID 是Integer 类型,[EDX] = LoWord(MsgID),
见上图
OR SI,SI
JE @@default
如果 SI = 0,调用 DefaultHanlder
CMP SI,0C000H
JAE @@default
如果 SI >= $C000,调用 DefaultHandler (注意这里)
PUSH EAX
保存对象的指针
MOV EAX,[EAX]
找到对象的 VMT 指针
CALL GetDynaMethod
调用对象的动态方法
如果找到了动态方法 ZF = 0 ,
没找到 ZF = 1
注:GetDynaMethod 是 System.pas 中的获得动态方法地
址的汇编函数
POP EAX
恢复 EAX 为对象的指针
JE @@default
如果没找到相关的动态方法,调用 DefaultHandler
MOV ECX,ESI
把找到的动态方法指针存入 ECX
POP ESI
恢复 ESI
JMP ECX
调用对象的动态方法
@@default:
POP ESI
恢复 ESI
MOV ECX,[EAX]
把对象的 VMT 指针存入 ECX,以调用 DefaultHandler
JMP DWORD PTR [ECX] + VMTOFFSET TObject.DefaultHandler
end;
TObject.Dispatch 的执行过程是:
把 MsgID 存入 SI,作为动态方法的索引值
如果 SI >= $C000,则调用 DefaultHandler(也就是所有 RegisterWindowMessage
生成的消息ID 会直接被发送到 DefaultHandler 中,后面会讲一个实例)
检查是否有相对应的动态方法
找到了动态方法,则执行该方法
没找到动态方法,则调用 DefaultHandler
原来以 message 关键字定义的对象方法就是动态方法,随便从 TWinControl 中抓几个消息处理函数出来:
procedure WMSize(var Message: TWMSize)
message WM_SIZE;
procedure WMMove(var Message: TWMMove)
message WM_MOVE;
到现在终于明白 WM_SIZE、WM_PAINT 方法的处理过程了吧。不但是 Windows 消息,连 Delphi 自己定义的消息也是以同样的方式处理的:
procedure CMEnabledChanged(var Message: TMessage)
message CM_ENABLEDCHANGED;
procedure CMFontChanged(var Message: TMessage)
message CM_FONTCHANGED;
所以如果你自己针对某个控件定义了一个消息,你也可以用 message 关键字定义处理该方法的函数,VCL 的消息系统会自动调用到你定义的函数。
由于 Dispatch 的参数只以最前 2 个字节为索引,并且自 MainWndProc 到 WndProc 到 Dispatch 都是以引用(传递地址)的方式来传递消息内容,你可以将消息的结构设置为任何结构,甚至可以只有 MsgID —— 只要你在处理消息的函数中正确地访问这些参数就行。
最关键的 Dispatch 方法告一段落,现在让我们看看 DefaultHandler 做了些什么?
===============================================================================
⊙ TWinControl.DefaultHandler
===============================================================================
DispatchHandler 是从 TObject 就开始存在的,它的声明如下:
procedure TObject.DefaultHandler(var Message)
virtual;
从名字也可以看出该函数的大概目的:最终的消息处理函数。在 TObject 的定义中 DefaultHandler 并没有代码,DefaultHandler 是在需要处理消息的类(TControl)之后被重载的。
从上面的讨论中已经知道 DefaultHandler 是由 TObject.Dispatch 调用的,所以 DefaultHandler 和 Dispatch 的参数类型一样都是无类型的 var Message。
由于 DefaultHandler 是个虚方法,所以执行流程是从子类到父类。在 TWinControl 和 TControl 的 DefaultHandler 中,仍然遵从 WndProc 的执行规则,也就是 TWinControl 没处理的消息,再使用 inherited 调用 TControl.DefaultHandler 来处理。
在 TWinControl.DefaultHandler 中先是处理了一些不太重要的Windows 消息,如WM_CONTEXTMENU、WM_CTLCOLORMSGBOX等。然后做了两件比较重要的工作:1、处理 RM_GetObjectInstance 消息;2、对所有未处理的窗口消息调用 TWinControl.FDefWndProc。
下面分别讨论。
RM_GetObjectInstance 是应用程序启动时自动使用 RegisterWindowMessage API 注册的 Windows 系统级消息ID,也就是说这个消息到达 Dispatch 后会无条件地传递给 DefaultHandler(见 Dispatch 的分析)。TWinControl.DefaultHandler 发现这个消息就把 Self 指针设置为返回值。在 Controls.pas 中有个函数 ObjectFromHWnd 使用窗口句柄获得 TWinControl 的句柄,就是使用这个消息实现的。不过这个消息是由 Delphi 内部使用,不能被应用程序使用。(思考:每次应用程序启动都会调用 RegisterWindowMessage,如果电脑长期不停机,那么 0xC000 - 0xFFFF 之间的消息 ID 是否会被耗尽?)
另外,TWinControl.DefaultHandler 在 TWinControl.FHandle 不为 0 的情况下,使用 CallWindowProc API 调用 TWndControl.FDefWndProc 窗口过程。FDefWndProc 是个指针,它是从哪里初始化的呢?跟踪一下,发现它是在 TWinControl.CreateWnd 中被设置为如下值:
FDefWndProc := Params.WindowClass.lpfnWndProc;
还记得前面讨论的窗口创建过程吗?TWinControl.CreateWnd 函数首先调用 TWinControl.CreateParams 获得待创建的窗口类的参数。CreateParams 把 WndClass.lpfnWndProc 设置为 Windows 的默认回调函数 DefWindowProc API。但 CreateParams 是个虚函数,可以被 TWinControl 的继承类重载,因此程序员可以指定一个自己设计的窗口过程。
所以 TWinControl.DefaultHandler 中调用 FDefWndProc 的意图很明显,就是可以在 Win32 API 的层次上支持消息的处理(比如可以从 C 语言写的 DLL 中导入窗口过程给 VCL 控件),给程序员提供充足的弹性空间。
TWinControl.DefaultHandler 最后一行调用了 inherited,把消息传递给 TControl 来处理。
TControl.DefaultHandler 只处理了三个消息 WM_GETTEXT、WM_GETTEXTLENGTH、WM_SETTEXT。为什么要处理这个几个看似不重要的消息呢?原因是:Windows 系统中每个窗口都有一个 WindowText 属性,而 VCL 的 TControl 为了模拟成窗口也存储了一份保存在 FText 成员中,所以 TControl 在此接管这几个消息。
TControl.DefaultHandler 并没有调用 inherited,其实也没有必要调用,因为 TControl 的祖先类都没有实现 DefaultHandler 函数。可以认为 DefaultHandler 的执行到此为止。
VCL 的消息流程至此为止。
===============================================================================
⊙ TControl.Perform 和 TWinControl.Broadcast
===============================================================================
现在介绍 VCL 消息系统中两个十分简单但调用频率很高的函数。
TControl.Perform 用于直接把消息送往控件的消息处理函数 WndProc。Perform 方法不是虚方法,它把参数重新组装成一个 TMessage 类型,然后调用 WindowProc(还记得 WindowProc 的作用吗?),并返回 Message.Result 给用户。它的调用格式如下:
function TControl.Perform(Msg: Cardinal
WParam, LParam: Longint): Longint;
Perform 经常用于通知控件某些事件发生,或得到消息处理的结果,如下例:
Perform(CM_ENABLEDCHANGED, 0, 0);
Text := Perform(WM_GETTEXTLENGTH, 0, 0);
TWinControl.Broadcast 用于把消息广播给每一个子控件。它调用 TWinControl.Controls[] 数组中的所有对象的 WindowsProc 过程。
procedure TWinControl.Broadcast(var Message);
注意 Broadcast 的参数是无类型的。虽然如此,在 Broadcast 函数体中会把消息转换为 TMessage 类型,也就是说 Broadcast 的参数必须是 TMessage 类型。那么为什么要设计为无类型的消息呢?原因是 TMessage 有很多变体(Msg 和 Result 字段不会变,WParam 和 LParam 可设计为其它数据类型),将 Broadcast 设计为无类型参数可以使程序员不用在调用前强制转换参数,但调用时必须知道这一点。比如以下字符消息的变体,是和 TMessage 兼容的:
TWMKey = packed record
Msg: Cardinal;
CharCode: Word;
Unused: Word;
KeyData: Longint;
Result: Longint;
end;
===============================================================================
⊙ TWinControl.WMPaint
===============================================================================
上面在讨论 TWinControl.WndProc 时提到,TControl 类控件的鼠标和重绘消息是从 Parent TWinControl 中产生的。但我们只发现了鼠标消息的产生,那么重绘消息是从哪里产生出来的呢?答案是TWinControl.WMPaint:
procedure TWinControl.WMPaint(var Message: TWMPaint)
message WM_PAINT;
在 TWinControl.WMPaint 中建立了双缓冲重绘机制,但我们目前不关心这个,只看最关键的代码:
if not (csCustomPaint in ControlState) and (ControlCount = 0) then
inherited // 注意 inherited 的实现
else
PaintHandler(Message)
这段代码的意思是,如果控件不支持自绘制并且不包含 TControl 就调用 inherited。
inherited 是什么呢?由于 TWinControl.WMPaint 的父类 TControl 没有实现这个消息句柄,Delphi 生成的汇编代码竟然是:call Self.DefaultHandler。(TWinControl.DefaultHandler 只是简单地调用 TWinControl.FDefWndProc。)
如果条件为否,那么将调用 TWinControl.PaintHandler(不是虚函数)。PaintHandler 调用 BeginPaint API 获得窗口设备环境,再使用该设备环境句柄为参数调用 TWinControl.PaintWindow。在 TWinControl 中 PaintWindow 只是简单地把消息传递给 DefaultHandler。PaintWindow 是个虚函数,可以在继承类中被改写,以实现自己需要的绘制内容。PaintHandler 还调用了 TWinControl.PaintControls 方法。PaintControls 使用 Perform 发送 WM_PAINT 消息给 TWinControl 控件包含的所有 TControl 控件。
这样,TControl 控件才获得了重绘的消息。
让我们设计一个 TWinControl 的继承类作为练习:
TMyWinControl = class(TWinControl)
protected
procedure PaintWindow(DC: HDC)
override;
public
constructor Create(AOwner: TComponent)
override;
end;
constructor TMyWinControl.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
ControlState := ControlState + [csCustomPaint];
// 必须通知 WMPaint 需要画自己
end;
procedure TMyWinControl.PaintWindow(DC: HDC);
var
Rect: TRect;
begin
Windows.GetClientRect(Handle, Rect);
FillRect(DC, Rect, COLOR_BTNSHADOW + 1);
SetBkMode(DC, TRANSPARENT);
DrawText(DC, 'Hello, TMyWinControl', -1, Rect, DT_SINGLELINE or DT_VCENTER
or DT_CENTER);
end;
上面实现的 TMyWinControl 简单地重载 PaintWindow 消息,它可以包含 TControl 对象,并能正确地把它们画出来。如果你确定该控件不需要包含 TControl 对象,你也可以直接重载 WMPaint 消息,这就像用 C 语言写普通的 WM_PAINT 处理函数一样。
===============================================================================
⊙ 以 TWinControl 为例描述消息传递的路径
===============================================================================
下图描述一条消息到达后消息处理函数的调用路径,每一层表示函数被上层函数调用。
TWinControl.FObjectInstance
|-TWinControl.MainWndProc
|-TWinControl.WindowProc
|-TWinControl.WndProc
|-TControl.WndProc
|-TObject.Dispatch
|-Call DMT messages
|-TWinControl.DefaultHandler
|-TControl.DefaultHandler
注:
如前文所述,上图中的 WindowProc 是个指针,所以它在编译器级实际上等于 WndProc,而不是调用 WndProc,图中为了防止与消息分枝混淆特意区分成两层。
TObject.Dispatch 有两条通路,如果当前控件以 message 关键字实现了消息处理函数,则呼叫该函数,否则调用 DefaultHandler。
有些消息处理函数可能在中途就已经返回了,有些消息处理函数可能会被递归调用。
===============================================================================
结束语
VCL 的消息机制就讨论到这里。希望我们通过本文的讨论理清了 VCL 处理消息的框架,今后我们将使用这些最基础的知识开始探索 Delphi 程序设计的旅程。
===============================================================================
yczjs (2004-01-10 0:20:00)
好文章
elphi园地已经转载
请楼主同意
http://www.delphifans.com/article_view.asp?id=254
xzh2000 (2004-01-10 0:31:00)
http://www.delphibbs.com/delphibbs/dispq.asp?lid=2403560
写这么多,辛苦了,帮我看看这个小问题
savetime (2004-01-10 4:19:00)
原来是站长驾到,有失远迎。我的版权声明主要是担心文中的错误误导了初学者,所以先放到大富翁里请大家检查。既然你已经贴上去了,我只好祈求读者自己小心了。
PS.你的园地怎么不长金币?我到现在还是 0 块,能不能送几块给我以备不时需。大富翁每天还长 5 分呢。
kk2000 (2004-01-10 8:42:00)
savetime: 我已经报到!现在还来不及看。帮你顶先!
savetime (2004-01-10 12:51:00)
文中有关 _InitExe 的讨论有误,已修改。
blyb (2004-01-10 13:29:00)
好文章,收藏。
^nEWnEW^ (2004-01-10 14:51:00)
收藏
cc112233 (2004-01-10 16:19:00)
文章写得通俗易懂,但讲的内容绝不肤浅,而且叙述的非常细致,我喜欢读这种文章。
谢谢你写这么好的东西供我们分享。
lichdr (2004-01-10 16:28:00)
收藏了。
現在收藏了很多帖子,以後有空慢慢看。
JamesBit (2004-01-10 20:14:00)
好文, 支持.(我没灌水啊~~~~~~~~~~~~~)
C0026 (2004-01-11 1:24:00)
好文章,我也在等李维的那本VCL出来,不如你去代他写吧
wjh_wy (2004-01-11 13:38:00)
好文章,楼主真是利害,佩服。
book523 (2004-01-12 12:54:00)
楼主真厉害啊,
和李维那本书讲的差不多啊。
但是好像有一点遗漏了,
Vcl中的消息机制是由窗体来代理的,
窗体是怎么找到他的子控件,
并执行相应子控件的消息处理代码呢?
savetime (2004-01-12 9:44:00)
to book523,
>> 那窗体是如何找到他的子控件,并调用相应的消息处理代码呢?
是啊,我写的时候一路顺流而下,就没有单独叙述这点。不过,读者应该可以在 TWinControl.WndProc、TControl.Perform、TWinControl.Broadcast、TWinControl.WMPaint 的讨论中找到答案:
TControl.Perform 用于直接把消息送往控件的消息处理函数 WndProc。
TWinControl.Broadcast 用于把消息广播给每一个子控件。它调用 TWinControl.Controls[] 数组中的所有对象的 WindowsProc 过程。
book523 (2004-01-12 13:06:00)
那窗体是如何找到他的子控件,并调用相应的消息处理代码呢?
很多消息是通过DoControlMsg过程来实现的。
看TWinControl的好的消息处理过程都调用这个过程,我们看一下这个过程:
function DoControlMsg(ControlHandle: HWnd
var Message): Boolean;
var
Control: TWinControl;
begin
DoControlMsg := False;
Control := FindControl(ControlHandle)
//根据HWnd找到相应的控件
if Control <> nil then
with TMessage(Message) do
begin
//Msg + CN_BASE把windows消息转换成Vcl内部的控件消息,然后执行控件Perform方法,即调用控件的WndProc过程,这样窗体就把消息给了控件,然后由控件处理自己的消息了。
Result := Control.Perform(Msg + CN_BASE, WParam, LParam)
DoControlMsg := True;
end;
end
book523 (2004-01-12 13:16:00)
还有一些消息是通过
TWinControl.Broadcast过程广播给子控件的,
子控件接到消息后,根据消息的信息决定是否是
自己要处理的消息。
TControl.Perform 只是调用控件自己的消息处理过程wndproc,
并不能把消息传递给子控件。
procedure TWinControl.Broadcast(var Message);
var
I: Integer;
begin
for I := 0 to ControlCount - 1 do
begin
Controls
.WindowProc(TMessage(Message));
if TMessage(Message).Result <> 0 then Exit;
end;
end
savetime (2004-01-12 13:54:00)
to book523,
每次你都能发现我的漏洞 。你要是不说,我还真的不知道有 DoControlMsg 这个函数。我下午要出去办事,回来以后再细细品味一下。
book523 (2004-01-12 14:23:00)
这也不能说是漏洞,
只是你漏了这一点。
有了你前面的分析,
再找出这个过程就比较容易了。
savetime (2004-01-12 16:09:00)
to book523,
大概看了一下,请检查我的结论:
DoControlMsg 的用处是:将子窗口控件(TWinControl)发送给 Parent TWinControl.Handle 的消息重新改变消息 ID 后发再发送给子窗口控件(TWinControl)处理。
VCL 建立 DoControlMsg 的原因是:
Windows 中预建了一些窗口类如:Button, ListBox, ScrollBar,以这些窗口类建立子窗口后,会发送消息给父窗口以表示自己发生的事件。比如 Button 被鼠标点击时,会向父窗口发送 WM_COMMAND 消息,这时消息是被父窗口接收到的。TWinControl 就接管这些由“子窗口发送给父窗口的消息”,并转换为 Delphi 自定义的消息,最后调用 子窗口控件(TWinControl)的 WndProc 来处理。此举完成 Win32 子窗口的消息处理至 Delphi VCL 子控件消息处理的转换。
DoControlMsg 的 Control 不是 TControl,而是 TWinControl。
从 TControl 继承的非窗口类仍然是从 TWinControl.WndProc 接收到鼠标消息,从 TWinControl.WMPaint 接收到重绘消息。它们之间的链接点是 TWinControl.Controls[] 数组。
book523 (2004-01-12 19:06:00)
看看call Stack就知道了。
新建一个button,写句代码:
showmessage('fdas');
设个断点,运行
View-debug-call Stack
如下内容:
TForm1.Button1Click($CE3798)
TControl.Click
TButton.Click
TButton.CNCommand((48401, 970, 0, 263114, 0))
TControl.WndProc((48401, 970, 263114, 0, 970, 0, 970, 4, 0, 0))
TWinControl.WndProc((48401, 970, 263114, 0, 970, 0, 970, 4, 0, 0))
TButtonControl.WndProc((48401, 970, 263114, 0, 970, 0, 970, 4, 0, 0))
TControl.Perform(48401,970,263114)
DoControlMsg(263114,(no value))
TWinControl.WMCommand((273, 970, 0, 263114, 0))
TCustomForm.WMCommand((273, 970, 0, 263114, 0))
TControl.WndProc((273, 970, 263114, 0, 970, 0, 970, 4, 0, 0))
TWinControl.WndProc((273, 970, 263114, 0, 970, 0, 970, 4, 0, 0))
TCustomForm.WndProc((273, 970, 263114, 0, 970, 0, 970, 4, 0, 0))
TWinControl.MainWndProc((273, 970, 263114, 0, 970, 0, 970, 4, 0, 0))
StdWndProc(263108,273,970,263114)
TWinControl.DefaultHandler((no value))
TControl.WMLButtonUp((514, 0, 45, 8, (45, 8), 0))
TControl.WndProc((514, 0, 524333, 0, 0, 0, 45, 8, 0, 0))
TWinControl.WndProc((514, 0, 524333, 0, 0, 0, 45, 8, 0, 0))
TButtonControl.WndProc((514, 0, 524333, 0, 0, 0, 45, 8, 0, 0))
TWinControl.MainWndProc((514, 0, 524333, 0, 0, 0, 45, 8, 0, 0))
StdWndProc(263114,514,0,524333)
TApplication.HandleMessage
TApplication.Run
看看经过了好多次处理,最后才到达TForm1.Button1Click($CE3798)啊。
book523 (2004-01-12 19:09:00)
savetime 厉害啊,
《inside Vcl》
拿到没?
savetime (2004-01-12 21:49:00)
to book523,
书店通知我近期出版社很忙,拿不到书,只好再等等。
李维的书上有没有说 TObject.Dispatch 2byte 的消息与 Windows 4bytes 消息的关系?我在文章中写的是猜的。我还是觉得有点不妥,如果微软发送一个 0x00010000 至 0xFFFFFFFF 之间的消息给应用程序会出现什么情况呢?当然这种可能性比较小,Borland 应该是有把握才样做的。
liqsz (2004-01-13 8:30:00)
study
pyzfl (2004-01-13 9:04:00)
好贴,有时间详细研习,希望不要删除啊。[^]
book523 (2004-01-13 9:21:00)
这一点书上没有提到,
我想你想的应该正确的吧。
估计这是windows的一个规范吧,
微软也不能违反这个规范啊。
Vcl中的消息Msg全是用两个字节定义的啊。
savetime (2004-01-13 13:57:00)
趁人少把分散了,发现错误后别忘了提醒我。
savetime (2004-01-14 22:10:00)
这两天在读 RTTI 和 Streaming / Persistent。RTTI 倒还容易理解,Persistent 则毫无进展 。
谁能介绍 TFiler.Read / ReadRootComponent / ReadSignature / ReadComponent 这些函数的作用?
或者指点一下 Persistent 机制的流程。
即使是一点点的帮助也是非常欢迎的。book523,你可一定要帮我啊。
book523 (2004-01-15 11:07:00)
给你转贴一文,希望对你有帮助。
Delphi的组件读写机制
一、流式对象(Stream)和读写对象(Filer)的介绍
在面向对象程序设计中,对象式数据管理占有很重要的地位。在Delphi中,对对象式数据管理的支持方式是其一大特色。
Delphi是一个面向对象的可视化设计与面向对象的语言相结合的集成开发环境。Delphi的核心是组件。组件是对象的一种。Delphi应用程序完全是由组件来构造的,因此开发高性能的Delphi应用程序必然会涉及对象式数据管理技术。
对象式数据管理包括两方面的内容:
● 用对象来管理数据
● 对各类数据对象(包括对象和组件)的管理
Delphi将对象式数据管理类归结为Stream对象(Stream)和Filer对象(Filer),并将它们应用于可视组件类库(VCL)的方方面面。它们提供了丰富的在内存、外存和Windows资源中管理对象的功能,
Stream对象,又称流式对象,是TStream、THandleStream、TFileStream、TMemoryStream、TResourceStream和TBlobStream等的统称。它们分别代表了在各种媒介上存储数据的能力,它们将各种数据类型(包括对象和组件) 在内存、外存和数据库字段中的管理操作抽象为对象方法,并且充分利用了面向对象技术的优点,应用程序可以相当容易地在各种Stream对象中拷贝数据。
读写对象(Filer)包括TFiler对象、TReader对象和TWriter对象。TFiler对象是文件读写的基础对象,在应用程序中使用的主要是TReader和TWriter。TReader和TWriter对象都直接从TFiler对象继承。TFiler对象定义了Filer对象的基本属性和方法。
Filer对象主要完成两大功能:
● 存取窗体文件和窗体文件中的组件
● 提供数据缓冲,加快数据读写操作
为了对流式对象和读写对象有一个感性的认识,先来看一个例子。
a)写文件
procedure TFomr1.WriteData (Sender: TObject)
r;
Var
FileStream:TFilestream;
Mywriter:TWriter;
i: integer
Begin
FileStream:=TFilestream.create(‘c:Test.txt’,fmopenwrite);//创建文件流对象
Mywriter:=TWriter.create(FileStream,1024); //把Mywriter和FileStream联系起来
Mywriter. writelistbegin
//写入列表开始标志
For i:=0 to Memo1.lines.count-1 do
Mywriter.writestring(memo1.lines[i]); //保存Memo组件中文本信息到文件中
Mywriter.writelistend
//写入列表结束标志
FileStream.seek(0,sofrombeginning); //文件流对象指针移到流起始位置
Mywriter.free
//释放Mywriter对象
FileStream.free
//释放FileStream对象
End;
b)读文件
procedure TForm1.ReadData(Sender: TObject);
Var
FileStream:TFilestream;
Myreader:TReader;
Begin
FileStream:=TFilestream.create(‘c:Test.txt’,fmopenread);
Myreader:=TRreader.create(FileStream,1024)
//把Myreader和FileStream联系起来
Myreader.readlistbegin
//把写入的列表开始标志读出来
Memo1.lines.clear
//清除Memo1组件的文本内容
While not myreader.endoflist do //注意TReader的一个方法:endoflist
Begin
Memo1.lines.add(myreader.readstring)
//把读出的字符串加到Memo1组件中
End;
Myreader.readlistend; //把写入的列表结束标志读出来
Myreader.free; //释放Myreader对象
FileStream.free; //释放FileStream对象
End;
上面两个过程,一个为写过程,另一个为读过程。写过程通过TWriter,利用TFilestream把一个Memo中的内容(文本信息)存为一个保存在磁盘上的二进制文件。读过程刚好和写过程相反,通过TReader,利用TFilestream把二进制文件中的内容转换为文本信息并显示在Memo中。运行程序可以看到,读过程忠实的把写过程所保存的信息进行了还原。
下图描述了数据对象(包括对象和组件)、流式对象和读写对象之间的关系。
图(一)
值得注意的是,读写对象如TFiler对象、TReader对象和TWriter对象等很少由应用程序编写者进行直接的调用,它通常用来读写组件的状态,它在读写组件机制中扮演着非常重要的角色。
对于流式对象Stream,很多参考资料上都有很详细的介绍,而TFiler对象、TReader对象和TWriter对象特别是组件读写机制的参考资料则很少见,本文将通过对VCL原代码的跟踪而对组件读写机制进行剖析。
二、读写对象(Filer)与组件读写机制
Filer对象主要用于存取Delphi的窗体文件和窗体文件中的组件,所以要清楚地理解Filer对象就要清楚Delphi 窗体文件(DFM文件)的结构。
DFM文件是用于Delphi存储窗体的。窗体是Delphi可视化程序设计的核心。窗体对应Delphi应用程序中的窗口,窗体中的可视组件对应窗口中的界面元素,非可视组件如TTimer和TOpenDialog,对应Delphi应用程序的某项功能。Delphi应用程序的设计实际上是以窗体的设计为中心。因此,DFM文件在Delphi应用设计中也占很重要的位置。窗体中的所有元素包括窗体自身的属性都包含在DFM文件中。
在Delphi应用程序窗口中,界面元素是按拥有关系相互联系的,因此树状结构是最自然的表达形式;相应地,窗体中的组件也是按树状结构组织;对应在DFM文件中,也要表达这种关系。DFM文件在物理上,是以文本方式存储的(在Delphi2.0版本以前是存储为二进制文件的),在逻辑上则是以树状结构安排各组件的关系。从该文本中可以看清窗体的树状结构。下面是DFM文件的内容:
object Form1: TForm1
Left = 197
Top = 124
……
PixelsPerInch = 96
TextHeight = 13
object Button1: TButton
Left = 272
……
Caption = ‘Button1‘
TabOrder = 0
end
object Panel1: TPanel
Left = 120
……
Caption = ‘Panel1‘
TabOrder = 1
object CheckBox1: TCheckBox
Left = 104
……
Caption = ‘CheckBox1‘
TabOrder = 0
end
end
end
这个DFM文件就是TWriter通过流式对象Stream来生成的,当然这里还有一个二进制文件到文本信息文件的转换过程,这个转换过程不是本文要研究的对象,所以忽略这样的一个过程。
在程序开始运行的时候,TReader通过流式对象Stream来读取窗体及组件,因为Delphi在编译程序的时候,利用编译指令{$R *.dfm}已经把DFM文件信息编译到可执行文件中,因此TReader读取的内容实际上是被编译到可执行文件中的有关窗体和组件的信息。
TReader和TWriter不仅能够读取和写入Object Pascal中绝大部分标准数据类型,而且能够读写List、Variant等高级类型,甚至能够读写Perperties和Component。不过,TReader、TWriter自身实际上提供的功能很有限,大部分实际的工作是由TStream这个非常强大的类来完成的。也就是说TReader、TWriter实际上只是一个工具,它只是负责怎么去读写组件,至于具体的读写操作是由TStream来完成的。
由于TFiler是TReader和TWriter的公共祖先类,因为要了解TReader和TWriter,还是先从TFiler开始。
先来看一下TFiler类的定义:
TFiler = class(TObject)
private
FStream: TStream;
FBuffer: Pointer;
FBufSize: Integer;
FBufPos: Integer;
FBufEnd: Integer;
FRoot: TComponent;
FLookupRoot: TComponent;
FAncestor: TPersistent;
FIgnoreChildren: Boolean;
protected
procedure SetRoot(value: TComponent)
virtual;
public
constructor Create(Stream: TStream
BufSize: Integer);
destructor Destroy
override;
procedure DefineProperty(const Name: string;
ReadData: TReaderProc
WriteData: TWriterProc;
HasData: Boolean)
virtual
abstract;
procedure DefineBinaryProperty(const Name: string;
ReadData, WriteData: TStreamProc;
HasData: Boolean)
virtual
abstract;
procedure FlushBuffer
virtual
abstract;
property Root: TComponent read FRoot write SetRoot;
property LookupRoot: TComponent read FLookupRoot;
property Ancestor: TPersistent read FAncestor write FAncestor;
property IgnoreChildren: Boolean read FIgnoreChildren write FIgnoreChildren;
end;
TFiler对象是TReader和TWriter的抽象类,定义了用于组件存储的基本属性和方法。它定义了Root属性,Root指明了所读或写的组件的根对象,它的Create方法将Stream对象作为传入参数以建立与Stream对象的联系, Filer对象的具体读写操作都是由Stream对象完成。因此,只要是Stream对象所能访问的媒介都能由Filer对象存取组件。
TFiler 对象还提供了两个定义属性的public方法:DefineProperty和DefineBinaryProperty,这两个方法使对象能读写不在组件published部分定义的属性。下面重点介绍一下这两个方法。
Defineproperty ( )方法用于使标准数据类型持久化,诸如字符串、整数、布尔、字符、浮点和枚举。
在Defineproperty方法中。Name参数用于指定应写入DFM文件的属性的名称,该属性不在类的published部分定义。
ReadData和WriteData参数指定在存取对象时读和写所需数据的方法。ReadData参数和WriteData参数的类型分别是TReaderProc和TWriterProc。这两个类型是这样声明的:
TReaderProc = procedure(Reader: TReader) of object;
TWriterProc = procedure(Writer: TWriter) of object;
HasData参数在运行时决定了属性是否有数据要存储。
DefineBinaryProperty方法和Defineproperty有很多的相同之处,它用来存储二进制数据,如声音和图象等。
下面来说明一下这两个方法的用途。
我们在窗体上放一个非可视化组件如TTimer,重新打开窗体时我们发现TTimer还是在原来的地方,但TTimer没有Left和Top属性啊,那么它的位置信息保存在哪里呢?
打开该窗体的DFM文件,可以看到有类似如下的几行内容:
object Timer1: TTimer
Left = 184
Top = 149
end
Delphi的流系统只能保存published数据,但TTimer并没有published的Left和Top属性,那么这些数据是怎么被保存下来的呢?
TTimer是TComponent的派生类,在TComponent类中我们发现有这样的一个函数:
procedure TComponent.DefineProperties(Filer: TFiler);
var
Ancestor: TComponent;
Info: Longint;
begin
Info := 0;
Ancestor := TComponent(Filer.Ancestor);
if Ancestor <> nil then Info := Ancestor.FDesignInfo;
Filer.DefineProperty(‘Left‘, ReadLeft, WriteLeft,
LongRec(FDesignInfo).Lo <> LongRec(Info).Lo);
Filer.DefineProperty(‘Top‘, ReadTop, WriteTop,
LongRec(FDesignInfo).Hi <> LongRec(Info).Hi);
end;
TComponent的DefineProperties是覆盖了它的祖先类TPersistent的方法,在TPersistent类中该方法为空的虚方法。
在DefineProperties方法中,我们可以看出,有一个Filer对象作为它的参数,当定义属性时,它引用了Ancestor属性,如果该属性非空,对象应当只读写与从Ancestor继承的不同的属性的值。它调用TFiler的DefineProperty方法,并定义了ReadLeft,WriteLeft,ReadTop,WriteTop方法来读写Left和Top属性。
因此,凡是从TComponent派生的组件,即使它没有Left和Top属性,在流化到DFM文件中,都会存在这样的两个属性。
在查找资料的过程中,发现很少有资料涉及到组件读写机制的。由于组件的写过程是在设计阶段由Delphi的IDE来完成的,因此无法跟踪它的运行过程。所以笔者是通过在程序运行过程中跟踪VCL原代码来了解组件的读机制的,又通过读机制和TWriter来分析组件的写机制。所以下文将按照这一思维过程来讲述组件读写机制,先讲TReader,而后是TWriter。TReader
先来看Delphi的工程文件,会发现类似这样的几行代码:
begin
Application.Initialize;
Application.CreateForm(TForm1, Form1);
Application.Run;
end.
这是Delphi程序的入口。简单的说一下这几行代码的意义:Application.Initialize对开始运行的应用程序进行一些必要的初始化工作,Application.CreateForm(TForm1, Form1)创建必要的窗体,Application.Run程序开始运行,进入消息循环。
现在我们最关心的是创建窗体这一句。窗体以及窗体上的组件是怎么创建出来的呢?在前面已经提到过:窗体中的所有组件包括窗体自身的属性都包含在DFM文件中,而Delphi在编译程序的时候,利用编译指令{$R *.dfm}已经把DFM文件信息编译到可执行文件中。因此,可以断定创建窗体的时候需要去读取DFM信息,用什么去读呢,当然是TReader了!
通过对程序的一步步的跟踪,可以发现程序在创建窗体的过程中调用了TReader的ReadRootComponent方法。该方法的作用是读出根组件及其所拥有的全部组件。来看一下该方法的实现:
function TReader.ReadRootComponent(Root: TComponent): TComponent;
……
begin
ReadSignature;
Result := nil;
GlobalNameSpace.BeginWrite
// Loading from stream adds to name space
try
try
ReadPrefix(Flags, I);
if Root = nil then
begin
Result := TComponentClass(FindClass(ReadStr)).Create(nil);
Result.Name := ReadStr;
end else
begin
Result := Root;
ReadStr
{ Ignore class name }
if csDesigning in Result.ComponentState then
ReadStr else
begin
Include(Result.FComponentState, csLoading);
Include(Result.FComponentState, csReading);
Result.Name := FindUniqueName(ReadStr);
end;
end;
FRoot := Result;
FFinder := TClassFinder.Create(TPersistentClass(Result.ClassType), True);
try
FLookupRoot := Result;
G := GlobalLoaded;
if G <> nil then
FLoaded := G else
FLoaded := TList.Create;
try
if FLoaded.IndexOf(FRoot) < 0 then
FLoaded.Add(FRoot);
FOwner := FRoot;
Include(FRoot.FComponentState, csLoading);
Include(FRoot.FComponentState, csReading);
FRoot.ReadState(Self);
Exclude(FRoot.FComponentState, csReading);
if G = nil then
for I := 0 to FLoaded.Count - 1 do TComponent(FLoaded).Loaded;
finally
if G = nil then FLoaded.Free;
FLoaded := nil;
end;
finally
FFinder.Free;
end;
……
finally
GlobalNameSpace.EndWrite;
end;
end;
ReadRootComponent首先调用ReadSignature读取Filer对象标签(’TPF0’)。载入对象之前检测标签,能防止疏忽大意,导致读取无效或过时的数据。
再看一下ReadPrefix(Flags, I)这一句,ReadPrefix方法的功能与ReadSignature的很相象,只不过它是读取流中组件前面的标志(PreFix)。当一个Write对象将组件写入流中时,它在组件前面预写了两个值,第一个值是指明组件是否是从祖先窗体中继承的窗体和它在窗体中的位置是否重要的标志;第二个值指明它在祖先窗体创建次序。
然后,如果Root参数为nil,则用ReadStr读出的类名创建新组件,并从流中读出组件的Name属性;否则,忽略类名,并判断Name属性的唯一性。
FRoot.ReadState(Self);
这是很关键的一句,ReadState方法读取根组件的属性和其拥有的组件。这个ReadState方法虽然是TComponent的方法,但进一步的跟踪就可以发现,它实际上最终还是定位到了TReader的ReadDataInner方法,该方法的实现如下:
procedure TReader.ReadDataInner(Instance: TComponent);
var
OldParent, OldOwner: TComponent;
begin
while not EndOfList do ReadProperty(Instance);
ReadListEnd;
OldParent := Parent;
OldOwner := Owner;
Parent := Instance.GetChildParent;
try
Owner := Instance.GetChildOwner;
if not Assigned(Owner) then Owner := Root;
while not EndOfList do ReadComponent(nil);
ReadListEnd;
finally
Parent := OldParent;
Owner := OldOwner;
end;
end;
其中有这样的这一行代码:
while not EndOfList do ReadProperty(Instance);
这是用来读取根组件的属性的,对于属性,前面提到过,既有组件本身的published属性,也有非published属性,例如TTimer的Left和Top。对于这两种不同的属性,应该有两种不同的读方法,为了验证这个想法,我们来看一下ReadProperty方法的实现。
procedure TReader.ReadProperty(AInstance: TPersistent);
……
begin
……
PropInfo := GetPropInfo(Instance.ClassInfo, FPropName);
if PropInfo <> nil then ReadPropvalue(Instance, PropInfo) else
begin
{ Cannot reliably recover from an error in a defined property }
FCanHandleExcepts := False;
Instance.DefineProperties(Self);
FCanHandleExcepts := True;
if FPropName <> ‘‘ then
PropertyError(FPropName);
end;
……
end;
为了节省篇幅,省略了一些代码,这里说明一下:FPropName是从文件读取到的属性名。
PropInfo := GetPropInfo(Instance.ClassInfo, FPropName);
这一句代码是获得published属性FPropName的信息。从接下来的代码中可以看到,如果属性信息不为空,就通过ReadPropvalue方法读取属性值,而ReadPropvalue方法是通过RTTI函数来读取属性值的,这里不再详细介绍。如果属性信息为空,说明属性FPropName为非published的,它就必须通过另外一种机制去读取。这就是前面提到的DefineProperties方法,如下:
Instance.DefineProperties(Self);
该方法实际上调用的是TReader的DefineProperty方法:
procedure TReader.DefineProperty(const Name: string;
ReadData: TReaderProc
WriteData: TWriterProc
HasData: Boolean);
begin
if SameText(Name, FPropName) and Assigned(ReadData) then
begin
ReadData(Self);
FPropName := ‘‘;
end;
end;
它先去比较读取的属性名是否和预设的属性名相同,如果相同并且读方法ReadData不为空时就调用ReadData方法读取属性值。
好了,根组件已经读上来了,接下来应该是读该根组件所拥有的组件了。再来看方法:
procedure TReader.ReadDataInner(Instance: TComponent);
该方法后面有一句这样的代码:
while not EndOfList do ReadComponent(nil);
这正是用来读取子组件的。子组件的读取机制是和上面所介绍的根组件的读取一样的,这是一个树的深度遍历。
到这里为止,组件的读机制已经介绍完了。
再来看组件的写机制。当我们在窗体上添加一个组件时,它的相关的属性就会保存在DFM文件中,这个过程就是由TWriter来完成的。
&#61656
TWriter
TWriter 对象是可实例化的往流中写数据的Filer对象。TWriter对象直接从TFiler继承而来,除了覆盖从TFiler继承的方法外,还增加了大量的关于写各种数据类型(如Integer、String和Component等)的方法。
TWriter对象提供了许多往流中写各种类型数据的方法, TWrite对象往流中写数据是依据不同的数据采取不同的格式的。 因此要掌握TWriter对象的实现和应用方法,必须了解Writer对象存储数据的格式。
首先要说明的是,每个Filer对象的流中都包含有Filer对象标签。该标签占四个字节其值为“TPF0”。Filer对象为WriteSignature和ReadSignature方法存取该标签。该标签主要用于Reader对象读数据(组件等)时,指导读操作。
其次,Writer对象在存储数据前都要留一个字节的标志位,以指出后面存放的是什么类型的数据。该字节为TvalueType类型的值。TvalueType是枚举类型,占一个字节空间,其定义如下:
TvalueType = (VaNull, VaList, VaInt8, VaInt16, VaInt32, VaEntended, VaString, VaIdent,
VaFalse, VaTrue, VaBinary, VaSet, VaLString, VaNil, VaCollection)
因此,对Writer对象的每一个写数据方法,在实现上,都要先写标志位再写相应的数据;而Reader对象的每一个读数据方法都要先读标志位进行判断,如果符合就读数据,否则产生一个读数据无效的异常事件。VaList标志有着特殊的用途,它是用来标识后面将有一连串类型相同的项目,而标识连续项目结束的标志是VaNull。因此,在Writer对象写连续若干个相同项目时,先用WriteListBegin写入VaList标志,写完数据项目后,再写出VaNull标志;而读这些数据时,以ReadListBegin开始,ReadListEnd结束,中间用EndofList函数判断是否有VaNull标志。
来看一下TWriter的一个非常重要的方法WriteData:
procedure TWriter.WriteData(Instance: TComponent);
……
begin
……
WritePrefix(Flags, FChildPos);
if UseQualifiedNames then
WriteStr(GetTypeData(PTypeInfo(Instance.ClassType.ClassInfo)).UnitName + ‘.‘ + Instance.ClassName)
else
WriteStr(Instance.ClassName);
WriteStr(Instance.Name);
PropertiesPosition := Position;
if (FAncestorList <> nil) and (FAncestorPos < FAncestorList.Count) then
begin
if Ancestor <> nil then Inc(FAncestorPos);
Inc(FChildPos);
end;
WriteProperties(Instance);
WriteListEnd;
……
end;
从WriteData方法中我们可以看出生成DFM文件信息的概貌。先写入组件前面的标志(PreFix),然后写入类名、实例名。紧接着有这样的一条语句:
WriteProperties(Instance);
这是用来写组件的属性的。前面提到过,在DFM文件中,既有published属性,又有非published属性,这两种属性的写入方法应该是不一样的。来看WriteProperties的实现:
procedure TWriter.WriteProperties(Instance: TPersistent);
……
begin
Count := GetTypeData(Instance.ClassInfo)^.PropCount;
if Count > 0 then
begin
GetMem(PropList, Count * SizeOf(Pointer));
try
GetPropInfos(Instance.ClassInfo, PropList);
for I := 0 to Count - 1 do
begin
PropInfo := PropList^;
if PropInfo = nil then
Break;
if IsStoredProp(Instance, PropInfo) then
WriteProperty(Instance, PropInfo);
end;
finally
FreeMem(PropList, Count * SizeOf(Pointer));
end;
end;
Instance.DefineProperties(Self);
end;
请看下面的代码:
if IsStoredProp(Instance, PropInfo) then
WriteProperty(Instance, PropInfo);
函数IsStoredProp通过存储限定符来判断该属性是否需要保存,如需保存,就调用WriteProperty来保存属性,而WriteProperty是通过一系列的RTTI函数来实现的。
Published属性保存完后就要保存非published属性了,这是通过这句代码完成的:
Instance.DefineProperties(Self);
DefineProperties的实现前面已经讲过了,TTimer的Left、Top属性就是通过它来保存的。
好,到目前为止还存在这样的一个疑问:根组件所拥有的子组件是怎么保存的?再来看WriteData方法(该方法在前面提到过):
procedure TWriter.WriteData(Instance: TComponent);
……
begin
……
if not IgnoreChildren then
try
if (FAncestor <> nil) and (FAncestor is TComponent) then
begin
if (FAncestor is TComponent) and (csInline in TComponent(FAncestor).ComponentState) then
FRootAncestor := TComponent(FAncestor);
FAncestorList := TList.Create;
TComponent(FAncestor).GetChildren(AddAncestor, FRootAncestor);
end;
if csInline in Instance.ComponentState then
FRoot := Instance;
Instance.GetChildren(WriteComponent, FRoot);
finally
FAncestorList.Free;
end;
end;
IgnoreChildren属性使一个Writer对象存储组件时可以不存储该组件拥有的子组件。如果IgnoreChildren属性为True,则Writer对象存储组件时不存它拥有的子组件。否则就要存储子组件。
Instance.GetChildren(WriteComponent, FRoot);
这是写子组件的最关键的一句,它把WriteComponent方法作为回调函数,按照深度优先遍历树的原则,如果根组件FRoot存在子组件,则用WriteComponent来保存它的子组件。这样我们在DFM文件中看到的是树状的组件结构。