Delphi消息机制浅探.docx
《Delphi消息机制浅探.docx》由会员分享,可在线阅读,更多相关《Delphi消息机制浅探.docx(35页珍藏版)》请在冰豆网上搜索。
Delphi消息机制浅探
自网上转载
我从去年12月上旬开始等待李维的《InsideVCL》。
我当时的计划是,在这本书的指导下深入学习Delphi。
到了12月底,书还没有出来,我不愿再等,开始阅读VCL源代码。
在读完TObject、TPersistant和TComponent的代码之后,我发现还是不清楚Delphi对象到底是怎样被创建的。
于是我查看Delphi生成的汇编代码,终于理解了对象创建的整个过程(这里要特别感谢book523的帮助)。
此后我就开始学习DelphiVCL的消息处理机制。
自从我写下《Delphi的对象机制浅探》,至今正好一个星期,我也基本上把DelphiVCL的消息处理框架读完了。
我的学习方法就是阅读源代码,一开始比较艰苦,后来线索逐渐清晰起来。
在此把自己对DelphiVCL消息机制的理解记录下来,便于今后的复习,也给初学Delphi或没有时间阅读VCL源代码的朋友参考(毕竟没有几个程序员像我这样有时间)。
由于学习时间较短,一定会有错误,请大家指正。
我在分析VCL消息机制的过程中,基本上只考查了三个类TObject、TControl和TWinControl。
虽然我没有阅读上层类(如TForm)的代码,但我认为这些都是实现的细节。
我相信VCL消息系统中最关键的东西都在这三个类中。
纲举而目张,掌握基础类的消息处理方法之后再读其他类的消息处理过程就容易得多了。
要想读懂本文,最低配置为:
了解Win32消息循环和窗口过程
基本了解TObject、TControl和TWinControl实现的内容
熟悉Delphi对象的重载与多态
推荐配置为:
熟悉Win32SDK编程
熟悉Delphi的对象机制
熟悉Delphi内嵌汇编语言
推荐阅读:
《Delphi的原子世界》
《VCL窗口函数注册机制研究手记,兼与MFC比较》
《Delphi的对象机制浅探》
本文排版格式为:
正文由窗口自动换行;所有代码以80字符为边界;中英文字符以空格符分隔。
(作者保留对本文的所有权利,未经作者同意请勿在在任何公共媒体转载。
)
目录
===============================================================================
⊙一个GUIApplication的执行过程:
消息循环的建立
⊙TWinControl.Create、注册窗口过程和创建窗口
⊙补充知识:
TWndMethod概述
⊙VCL的消息处理从TWinControl.MainWndProc开始
⊙TWinControl.WndProc
⊙TControl.WndProc
⊙TObject.Dispatch
⊙TWinControl.DefaultHandler
⊙TControl.Perform和TWinControl.Broadcast
⊙TWinControl.WMPaint
⊙以TWinControl为例描述消息传递的路径
===============================================================================
正文
===============================================================================
⊙一个GUIApplication的执行过程:
消息循环的建立
===============================================================================
通常一个Win32GUI应用程序是围绕着消息循环的处理而运行的。
在一个标准的C语言Win32GUI程序中,主程序段都会出现以下代码:
while(GetMessage(&msg,NULL,0,0))//GetMessage第二个参数为NULL,
//表示接收所有应用程序产生的窗口消息
{
TranslateMessage(&msg);//转换消息中的字符集
DispatchMessage(&msg);//把msg参数传递给lpfnWndProc
}
lpfnWndProc是Win32API定义的回调函数的地址,其原型如下:
int__stdcallWndProc(HWNDhWnd,UINTuMsg,WPARAMwParam,LPARAMlParam);
Windows回调函数(callbackfunction)也通常被称为窗口过程(windowprocedure),本文随意使用这两个名称,代表同样的意义。
应用程序使用GetMessage不断检查应用程序的消息队列中是否有消息到达。
如果发现了消息,则调用TranslateMessage。
TranslateMessage主要是做字符消息本地化的工作,不是关键的函数。
然后调用DispatchMessage(&msg)。
DispatchMessage(&msg)使用msg为参数调用已创建的窗口的回调函数(WndClass.lpfnWndProc)。
lpfnWndProc是由用户设计的消息处理方法。
当GetMessage在应用程序的消息队列中发现一条WM_QUIT消息时,GetMessage返回False,消息循环才告结束,通常应用程序在这时清理资源后也结束运行。
使用最原始的Win32API编写的应用程序的执行过程是很容易理解的,但是用DelphiVCL组件封装消息系统,并不是容易的事。
首先,Delphi是一种面向对象的程序设计语言,不但要把Win32的消息处理过程封装在对象的各个继承类中,让应用程序的使用者方便地调用,也要让VCL组件的开发者有拓展消息处理的空间。
其次,Delphi的对象模型中所有的类方法都是对象相关的(也就是传递了一个隐含的参数Self),所以Delphi对象的方法不能直接被Windows回调。
DelphiVCL必须用其他的方法让Windows回调到对象的消息处理函数。
让我们跟踪一个标准的DelphiApplication的执行过程,查看Delphi是如何开始一个消息循环的。
programProject1;
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;
这个函数只有一句代码:
ifInitProc<>nilthenTProcedure(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的代码也只有一行:
ifnotProcessMessage(Msg)thenIdle(Msg);
TApplication.ProcessMessage才真正开始建立消息循环。
ProcessMessage使用PeekMessageAPI代替GetMessage获取消息队列中的消息。
使用PeekMessage的好处是PeekMessage发现消息队列中没有消息时会立即返回,这样就为HandleMessage函数执行Idle(Msg)提供了依据。
ProcessMessage在处理消息循环的时候还特别处理了HintMsg、MDIMsg、KeyMsg、DlgMsg等特殊消息,所以在Delphi中很少再看到纯Win32SDK编程中的要区分DialogWindow、MDIWindow的处理,这些都被封装到TForm中去了(其实Win32SDK中的Dialog也是只是Microsoft专门写了一个窗口过程和一组函数方便用户界面的设计,其内部运作过程与一个普通窗口无异)。
functionTApplication.ProcessMessage(varMsg:
TMsg):
Boolean;
var
Handled:
Boolean;
begin
Result:
=False;
ifPeekMessage(Msg,0,0,0,PM_REMOVE)then//从消息队列获取消息
begin
Result:
=True;
ifMsg.MessageWM_QUITthen
begin
Handled:
=False;//Handled表示Application.OnMessage是否已//经处理过当前消息。
//如果用户设置了Application.OnMessage事件
//句柄,则先调用Application.OnMessage
ifAssigned(FOnMessage)thenFOnMessage(Msg,Handled);
ifnotIsHintMsg(Msg)andnotHandledandnotIsMDIMsg(Msg)and
notIsKeyMsg(Msg)andnotIsDlgMsg(Msg)then
//思考:
notHandled为什么不放在最前?
begin
TranslateMessage(Msg);//处理字符转换
DispatchMessage(Msg);//调用WndClass.lpfnWndProc
end;
end
else
FTerminate:
=True;//收到WM_QUIT时应用程序终止
//(这里只是设置一个终止标记)
end;
end;
从上面的代码来看,Delphi应用程序的消息循环机制与标准Win32C语言应用程序差不多。
只是Delphi为了方便用户的使用设置了很多扩展空间,其副作用是消息处理会比纯CWin32API调用效率要低一些。
===============================================================================
⊙TWinControl.Create、注册窗口过程和创建窗口
===============================================================================
上面简单讨论了一个Application的建立到形成消息循环的过程,现在的问题是Delphi控件是如何封装创建窗口这一过程的。
因为只有建立了窗口,消息循环才有意义。
让我们先回顾DelphiVCL中几个主要类的继承架框:
TObject所有对象的基类
TPersistent所有具有流特性对象的基类
TComponent所有能放在DelphiFormDesigner上的对象的基类
TControl所有可视的对象的基类
TWinControl所有具有窗口句柄的对象基类
Delphi是从TWinControl开始实现窗口相关的元件。
所谓窗口,对于程序设计者来说,就是一个窗口句柄HWND。
TWinControl有一个FHandle私有成员代表当前对象的窗口句柄,通过TWinControl.Handle属性来访问。
我第一次跟踪TWinControl.Create过程时,竟然没有发现CreateWindowAPI被调用,说明TWinControl并不是在对象创建时就建立Windows窗口。
如果用户使用TWinControl.Create(Application)以后,立即使用Handle访问窗口会出现什么情况呢?
答案在TWinControl.GetHandle中,Handle是一个只读的窗口句柄:
propertyTWinControl.Handle:
HWndreadGetHandle;
TWinControl.GetHandle代码的内容是:
一旦用户要访问FHandle成员,TWinControl.HandleNeeded就会被调用。
HandleNeeded首先判断TWinControl.FHandle是否是等于0(还记得吗?
任何对象调用构造函数以后所有对象成员的内存都被清零)。
如果FHandle不等于0,则直接返回FHandle;如果FHandle等于0,则说明窗口还没有被创建,这时HandleNeeded自动调用TWinControl.CreateHandle来创建一个Handle。
但CreateHandle只是个包装函数,它首先调用TWinControl.CreateWnd来创建窗口,然后生成一些维护VCLControl运行的参数(我还没细看)。
CreateWnd是一个重要的过程,它先调用TWinControl.CreateParams设置创建窗口的参数。
(CreateParams是个虚方法,也就是说程序员可以重载这个函数,定义待建窗口的属性。
)CreateWnd然后调用TWinControl.CreateWindowHandle。
CreateWindowHandle才是真正调用CreateWindowExAPI创建窗口的函数。
够麻烦吧,我们可以抱怨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的窗口回调实现进行了深入的分析,请参考:
我在此简单介绍回调函数在VCL中的实现:
TWinControl.Create的代码中,第一句是inherited,第二句是
FObjectInstance:
=Classes.MakeObjectInstance(MainWndProc);
我想这段代码可能吓倒过很多人,如果没有cheka的分析,很多人难以理解。
但是你不一定真的要阅读MakeObjectInstance的实现过程,你只要知道:
MakeObjectInstance在内存中生成了一小段汇编代码,这段代码的内容就是一个标准的窗口过程。
这段汇编代码中同时存储了两个参数,一个是MainWndProc的地址,一个是Self(对象的地址)。
这段汇编代码的功能就是使用Self参数调用TWinControl.MainWndProc函数。
MakeObjectInstance返回后,这段代码的地址存入了TWinControl.FObjectInstance私有成员中。
这样,TWinControl.FObjectInstance就可以当作标准的窗口过程来用。
你可能认为TWinControl会直接把TWinControl.FObjectInstance注册为窗口类的回调函数(使用RegisterClassAPI),但这样做是不对的。
因为一个FObjectInstance的汇编代码内置了对象相关的参数(对象的地址Self),所以不能用它作为公共的回调函数注册。
TWinControl.CreateWnd调用CreateParams获得要注册的窗口类的资料,然后使用Controls.pas中的静态函数InitWndProc作为窗口回调函数进行窗口类的注册。
InitWndProc的参数符合Windows回调函数的标准。
InitWndProc第一次被回调时就把新建窗口(注意不是窗口类)的回调函数替换为对象的TWinControl.FObjectInstance(这是一种Windowssubclassing技术),并且使用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前在堆栈中设置参数:
pushLPARAM
pushWPARAM
pushUINT
pushHWND
push(eip.Next);把Windows回调前下一条语句的地址
;保存在堆栈中
jmpFObjectInstance.Code;调用TWinControl.FObjectInstance
FObjectInstance.Code只有一句call指令:
callObjectInstance.offset
pusheip.Next
jmpInstanceBlock.Code;调用InstanceBlock.Code
InstanceBlock.Code:
popecx;将eip.Next的值存入ecx,用于
;取@MainWndProc和Self
jmpStdWndProc;跳转至StdWndProc
StdWndProc的汇编代码:
functionS