您的位置:首页 > 编程语言 > Delphi

Delphi 的消息机制浅探(转)

2013-08-15 18:41 204 查看
转自http://blog.sina.com.cn/s/blog_bb2b4f6201018n9i.html


我从去年
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的原子世界》

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
字符为边界;中英文字符以空格符分隔。


(作者保留对本文的所有权利,未经作者同意请勿在在任何公共媒体转载。)


目录

===============================================================================

⊙一个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回调函数(callback
function
)也通常被称为窗口过程(window
procedure
),本文随意使用这两个名称,代表同样的意义。


应用程序使用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是如何开始一个消息循环的。


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使用PeekMessageAPI代替GetMessage获取消息队列中的消息。使用PeekMessage的好处是PeekMessage发现消息队列中没有消息时会立即返回,这样就为HandleMessage
函数执行Idle(Msg)提供了依据。


ProcessMessage在处理消息循环的时候还特别处理了HintMsg、MDIMsg、KeyMsg、DlgMsg等特殊消息,所以在Delphi中很少再看到纯Win32SDK编程中的要区分DialogWindow、MDIWindow的处理,这些都被封装到TForm中去了(其实Win32SDK中的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
.
MessageWM_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

//思考: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是一个只读的窗口句柄:


property
TWinControl
.
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的窗口回调实现进行了深入的分析,请参考: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注册为窗口类的回调函数(使用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的汇编代码:

function
StdWndProc(Window:HWND;Message,WParam:
Longint
;

LParam:
Longint
):
Longint
;stdcall;assembler;

asm

pushebp

movebp,esp

XOR
EAX,EAX

xor
eax,eax

PUSHEAX

pusheax;
设置Message
.
Result:=
0

PUSHLParam;
为什么Borland不从上面的堆栈中直接

pushdwordptr[ebp+
$14
];获取这些参数而要重新push一遍?

PUSHWParam;
因为TMessage的Result是

pushdwordptr[ebp+
$10
];记录的最后一个字段,而回调函数的HWND

PUSHMessage;
是第一个参数,没有办法兼容。

pushdwordptr[ebp+
$0c
]

MOVEDX,ESP

movedx,esp;
设置Message在堆栈中的地址为

;
MainWndProc的参数

MOVEAX,[ECX].
Longint
[
4
]

moveax,[ecx+
$04
];
设置Self为MainWndProc的隐含参数

CALL[ECX].
Pointer

calldwordptr[ecx]:呼叫TWinControl
.
MainWndProc(Self,

;
@Message)

ADDESP,
12

addesp,
$0c

POPEAX

popeax

end
;

popebp

ret
$0010

moveax,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:TWndMethodreadFWindowProc
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

{CheckHandleAllocatedbecauseIsControlMouseMsgmighthavefreedthe

windowifusercodeexecutedsomethinglikeParent:=nil.}

if
(Message
.
Result=
0
)
and
HandleAllocated
then

DefWindowProc(Handle,Message
.
Msg,Message
.
wParam,Message
.
lParam);

//DefWindowProc是Win32API中缺省处理消息的函数

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主要实现的操作是:

响应与FormDesigner的交互(在设计期间)

在控件不支持双击的情况下把鼠标双击事件转换成单击

判断鼠标移动时是否需要显示提示窗口(HintWindow)

判断控件是否设置为AutoDrag,如果是则执行控件的拖放处理

调用TControl
.
MouseWheelHandler实现鼠标滚轮消息

使用TObject
.
Dispatch调用DMT消息处理方法


TControl
.
WndProc相对比较简单,在此只随便谈谈第二条。你是否有过这样的使用经验:在你快速双击某个软件的Button时,只形成一次Click事件。所以如果你需要设计一个不管用户用多快的速度点击,都能生成同样点击次数Click事件的按钮时,就需要参考TControl
.
WndProc
处理鼠标消息的过程了。


TControl
.
WndProc最后一行代码是Dispatch(Message),也就是说如果某个消息没有被TControl以后的任何类处理,消息会被Dispatch处理。


TObject
.
Dispatch是DelphiVCL消息体系中非常关键的方法。


===============================================================================

⊙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把窗口消息分为五个区段:


0
×
00000000
至WM_USER–
1
标准视窗消息,以
WM_为前缀

WM_USER至WM_APP-
1
用户自定义窗口类的消息

WM_APP至
0
×0000BFFF应用程序级的消息

0
×0000C000至
0
×0000FFFFRegisterWindowMessage
生成的消息范围

0
×
00010000
至0xFFFFFFFFMicrosoft保留的消息,只由系统使用


(WM_USER=
0
×
00000400
,WM_APP=
0
×
00008000
)


发现问题的答案了吗?原来应用程序真正可用的消息只有
0
×
00000000
0
×0000FFFF,也就是消息ID只有低位
2
字节是有效的。(Borland
真是牛啊,连这也能想出来。)


由于IntelCPU的内存存放规则是高位字节存放在高地址,低位字节存放在低地址,所以Dispatch的Message参数的第一个内存字节就是LoWord(Message
.
Msg)。下图是Message参数的内存存放方式描述:


||+Memory

|——–|

|HiWord|

|——–|

|LoWord|=
$C000
,调用DefaultHandler(注意这里)

PUSHEAX;保存对象的指针

MOVEAX,[EAX];找到对象的VMT指针

CALLGetDynaMethod;调用对象的动态方法;如果找到了动态方法ZF=
0

;没找到ZF=
1

;注:GetDynaMethod
是System
.
pas中的获得动态方法地

;址的汇编函数

POPEAX;恢复EAX为对象的指针

JE@@default;如果没找到相关的动态方法,调用DefaultHandler

MOVECX,ESI;把找到的动态方法指针存入ECX

POPESI;恢复ESI

JMPECX;调用对象的动态方法


@@default:

POPESI;恢复ESI

MOVECX,[EAX];把对象的VMT指针存入ECX,以调用DefaultHandler

JMPDWORDPTR[ECX]+VMTOFFSETTObject
.
DefaultHandler

end
;


TObject
.
Dispatch的执行过程是:

把MsgID存入SI,作为动态方法的索引值

如果SI>=
$C000
,则调用DefaultHandler(也就是所有RegisterWindowMessage

生成的消息ID会直接被发送到DefaultHandler中,后面会讲一个实例)

检查是否有相对应的动态方法

找到了动态方法,则执行该方法

没找到动态方法,则调用DefaultHandler


原来以message关键字定义的对象方法就是动态方法,随便从TWinControl中抓几个消息处理函数出来:


procedure
WMSize(
var
Message:TWMSize);messageWM_SIZE;

procedure
WMMove(
var
Message:TWMMove);messageWM_MOVE;


到现在终于明白WM_SIZE、WM_PAINT方法的处理过程了吧。不但是Windows消息,连Delphi自己定义的消息也是以同样的方式处理的:


procedure
CMEnabledChanged(
var
Message:TMessage);messageCM_ENABLEDCHANGED;

procedure
CMFontChanged(
var
Message:TMessage);messageCM_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是应用程序启动时自动使用RegisterWindowMessageAPI注册的Windows系统级消息ID,也就是说这个消息到达Dispatch后会无条件地传递给DefaultHandler(见Dispatch的分析)。TWinControl
.
DefaultHandler
发现这个消息就把Self指针设置为返回值。在Controls
.
pas中有个函数ObjectFromHWnd使用窗口句柄获得TWinControl的句柄,就是使用这个消息实现的。不过这个消息是由Delphi内部使用,不能被应用程序使用。(思考:每次应用程序启动都会调用RegisterWindowMessage,如果电脑长期不停机,那么0xC000–0xFFFF之间的消息ID是否会被耗尽?)


另外,TWinControl
.
DefaultHandler在TWinControl
.
FHandle不为
0
的情况下,使用CallWindowProcAPI调用TWndControl
.
FDefWndProc
窗口过程。FDefWndProc是个指针,它是从哪里初始化的呢?跟踪一下,发现它是在TWinControl
.
CreateWnd中被设置为如下值:


FDefWndProc:=Params
.
WindowClass
.
lpfnWndProc;


还记得前面讨论的窗口创建过程吗?TWinControl
.
CreateWnd函数首先调用TWinControl
.
CreateParams获得待创建的窗口类的参数。CreateParams把WndClass
.
lpfnWndProc
设置为Windows的默认回调函数DefWindowProcAPI。但CreateParams是个虚函数,可以被TWinControl的继承类重载,因此程序员可以指定一个自己设计的窗口过程。


所以TWinControl
.
DefaultHandler中调用FDefWndProc的意图很明显,就是可以在Win32API的层次上支持消息的处理(比如可以从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类控件的鼠标和重绘消息是从ParentTWinControl中产生的。但我们只发现了鼠标消息的产生,那么重绘消息是从哪里产生出来的呢?答案是TWinControl
.
WMPaint:


procedure
TWinControl
.
WMPaint(
var
Message:TWMPaint);messageWM_PAINT;


在TWinControl
.
WMPaint中建立了双缓冲重绘机制,但我们目前不关心这个,只看最关键的代码:


if
not
(csCustomPaint
in
ControlState)
and
(ControlCount=
0
)
then

inherited
//注意inherited的实现

else

PaintHandler(Message);


这段代码的意思是,如果控件不支持自绘制并且不包含TControl就调用
inherited

inherited
是什么呢?由于TWinControl
.
WMPaint的父类TControl没有实现这个消息句柄,Delphi生成的汇编代码竟然是:callSelf
.
DefaultHandler。(TWinControl
.
DefaultHandler
只是简单地调用TWinControl
.
FDefWndProc。)


如果条件为否,那么将调用TWinControl
.
PaintHandler(不是虚函数)。PaintHandler调用BeginPaintAPI获得窗口设备环境,再使用该设备环境句柄为参数调用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

|-CallDMTmessages

|-TWinControl
.
DefaultHandler

|-TControl
.
DefaultHandler


注:

如前文所述,上图中的WindowProc是个指针,所以它在编译器级实际上等于WndProc,而不是调用WndProc,图中为了防止与消息分枝混淆特意区分成两层。

TObject
.
Dispatch有两条通路,如果当前控件以message关键字实现了消息处理函数,则呼叫该函数,否则调用DefaultHandler。

有些消息处理函数可能在中途就已经返回了,有些消息处理函数可能会被递归调用。


===============================================================================

结束语

VCL的消息机制就讨论到这里。希望我们通过本文的讨论理清了VCL处理消息的框架,今后我们将使用这些最基础的知识开始探索Delphi程序设计的旅程。

===============================================================================
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: