• 全部
  • 语言基础/算法/系统设计
  • 数据库相关
  • 图形处理/多媒体
  • 网络通信/分布式开发
  • VCL组件开发及应用
  • Windows SDK/API
  • 问答

由VCL代码理解VCL的消息机制

ehom 2003-04-01 04:01:28
Delphi,一个非常优秀的开发工具,拥有强大的可视化开发环境、面向组件的快速开发模式、优秀的VCL类库、快速的代码编译器、强大的数据库和WEB开发能力、还有众多的第三方控件支持...(此处省略x千字,既然大家都知道了,不浪费口水了 ^_^)

说到VCL的优秀就不能不提到其对Windows消息及API的较全面和完美的封装,正因为如此开发者在大多数情况下甚至不需理会Windows消息处理的细节,而只需要写几行事件驱动代码即可!

但如果做为开发人员你还是想对此做些了解的话,那么就继续,通过VCL代码本身来体会VCL中的消息处理机制。

(以下代码取自Delphi 6)

说到VCL中的消息处理就不能不提到TApplication,Windows会为每一个当前运行的程序建立一个消息队列,用来完成用户与程序的交互,正是通过Application完成了对Windows消息的集中处理!

首先通过Application.Run进入消息循环进行消息的处理,其中调用了HandleMessage。

procedure TApplication.HandleMessage;
var
Msg: TMsg;
begin
if not ProcessMessage(Msg) then Idle(Msg);//这里先调用ProcessMessage处理,返回值为False调用Idle,就是在空闲时,即消息队列中无消息等待处理时调用Idle。
end;

function TApplication.ProcessMessage(var Msg: TMsg): Boolean;
var
Handled: Boolean;
begin
Result := False;
if PeekMessage(Msg, 0, 0, 0, PM_REMOVE) then//查询消息队列中有无消息等待处理,参数PM_REMOVE使消息在处理完后会被删除。
begin
Result := True;
if Msg.Message <> WM_QUIT then//如果是WM_QUIT,终止进程,否则执行下面的代码
begin
Handled := False;
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
begin
TranslateMessage(Msg);//将记录Msg传递给Windows进行转换
DispatchMessage(Msg);//将记录Msg回传给Windows
end;
end
else
FTerminate := True;
end;
end;

然后程序中的各个VCL对象又是如何接收到Windows消息的呢?这还要从窗体的创建开始!

首先找到TWinControl.CreateWnd中的
Windows.RegisterClass(WindowClass)//调用RegisterClass注册一个窗体类

向上看
WindowClass.lpfnWndProc := @InitWndProc;//这里指定了窗口的消息处理函数的指针为@InitWndProc!

再找到function InitWndProc(HWindow: HWnd; Message, WParam, LParam: Longint): Longint;

发现了
CreationControl.FHandle := HWindow;
SetWindowLong(HWindow, GWL_WNDPROC,Longint(CreationControl.FObjectInstance));
没有?

原来InitWndProc初次被调用时候,又使用API函数SetWindowLong指定处理消息的窗口过程为FObjectInstance。

回到TWinControl.Create
FObjectInstance := Classes.MakeObjectInstance(MainWndProc);
找到关键所在了,也许有些朋友对MakeObjectInstance这个函数很熟了,它的作用就是将一个成员过程转换为标准过程。

绕了个圈子?为什么呢?很简单,因为窗体成员过程包括一隐含参数传递Self指针,所以需要转化为标准过程。

const
InstanceCount = 313;//这个不难理解吧?314*13+10=4092,再大的话,记录TInstanceBlock的大小就超过了下面定义的PageSize

type
PObjectInstance = ^TObjectInstance;
TObjectInstance = packed record
Code: Byte;
Offset: Integer;
case Integer of
0: (Next: PObjectInstance);
1: (Method: TWndMethod);
end;

type
PInstanceBlock = ^TInstanceBlock;
TInstanceBlock = packed record
Next: PInstanceBlock;
Code: array[1..2] of Byte;
WndProcPtr: Pointer;
Instances: array[0..InstanceCount] of TObjectInstance;
end;

var
InstBlockList: PInstanceBlock;
InstFreeList: PObjectInstance;

function StdWndProc(Window: HWND; Message, WParam: Longint;
LParam: Longint): Longint; stdcall; assembler;
asm
XOR EAX,EAX
PUSH EAX
PUSH LParam
PUSH WParam
PUSH Message
MOV EDX,ESP ;将堆栈中构造的记录TMessage指针赋给EDX
MOV EAX,[ECX].Longint[4] ;传递Self指针给EAX,类中的Self指针也就是指向VMT入口地址
CALL [ECX].Pointer ;调用MainWndProc方法
ADD ESP,12
POP EAX
end;

function CalcJmpOffset(Src, Dest: Pointer): Longint;
begin
Result := Longint(Dest) - (Longint(Src) + 5);
end;

function MakeObjectInstance(Method: TWndMethod): Pointer;
const
BlockCode: array[1..2] of Byte = (
$59, { POP ECX }
$E9); { JMP StdWndProc }
PageSize = 4096;
var
Block: PInstanceBlock;
Instance: PObjectInstance;
begin
if InstFreeList = nil then
begin
Block := VirtualAlloc(nil, PageSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);//分配虚拟内存,并指定这块内存为可读写并可执行
Block^.Next := InstBlockList;
Move(BlockCode, Block^.Code, SizeOf(BlockCode));
Block^.WndProcPtr := Pointer(CalcJmpOffset(@Block^.Code[2], @StdWndProc));
Instance := @Block^.Instances;
repeat
Instance^.Code := $E8; { CALL NEAR PTR Offset }
Instance^.Offset := CalcJmpOffset(Instance, @Block^.Code);
Instance^.Next := InstFreeList;
InstFreeList := Instance;
Inc(Longint(Instance), SizeOf(TObjectInstance));
until Longint(Instance) - Longint(Block) >= SizeOf(TInstanceBlock);
InstBlockList := Block;
end;
Result := InstFreeList;
Instance := InstFreeList;
InstFreeList := Instance^.Next;
Instance^.Method := Method;
end;

(注:上面出现的那些16进制代码其实就是些16进制的机器代码 $59=Pop ECX $E8=Call $E9=Jmp)

以上代码看起来有点乱,但综合起来看也很好理解!MakeObjectInstance实际上就是构建了一个Block链表

其结构看看记录TInstanceBlock的结构可知其结构如下:

Next//下一页指针
Code//Pop ECX和Jmp
WndProcPtr//和StdWndProc间的地址偏移
Instances//接下来是314个Instance链表

Instance链表通过记录TObjectInstance也很好理解其内容

Code//Call
Offset//地址偏移
Method//指向对象方法的指针(结合TMethod很好理解TWndMethod这类对象方法指针指向数据的结构)

好现在来把这个流程回顾一遍,Windows回调的是什么呢?其实是转到并执行一段动态生成的代码:先是执行Call offset ,根据偏移量转去执行Pop ECX,当然由于在Call这之前会将下一条指令入栈,所以这里弹出的就是指向对象方法的指针。接下来就是执行jmp [StdWndProc],其中将堆栈中构造的记录TMessage指针赋给了EDX,而根据上面的解释结合TMethod去理解,很容易理解
MOV EAX,[ECX].Longint[4] ;传递Self指针给EAX,类中的Self指针也就是指向VMT入口地址
CALL [ECX].Pointer ;调用MainWndProc方法

现在终于豁然开朗了,Windows消息就是这样被传递到了TWinControl.MainWndProc,相比MFC中的回调全局函数AfxWndProc来根据窗体句柄检索对应的对象指针的方法效率要高的多!VCL比MFC优秀的又一佐证! ^_^

现在终于找到了VCL接收消息的方法MainWndProc

procedure TWinControl.MainWndProc(var Message: TMessage);
begin
try
try
WindowProc(Message);//由于TControl创建实例时已经将FWindowProc指向WndProc,所以这里实际也就是调用WndProc
finally
FreeDeviceContexts;
FreeMemoryContexts;//调用FreeDeviceContexts和FreeMemoryContexts是为了保证VCL线程安全
end;
except
Application.HandleException(Self);
end;
end;

这里也不能忽略了TWinControl.WndProc

procedure TControl.WndProc(var Message: TMessage);
var
Form: TCustomForm;
KeyState: TKeyboardState;
WheelMsg: TCMMouseWheel;
begin
...
//省略以上的消息相关处理代码,研究某些特定消息时可自行查看
...
Dispatch(Message);//调用Dispatch处理
end;
...全文
115 点赞 收藏 82
写回复
82 条回复
切换为时间正序
当前发帖距今超过3年,不再开放新的回复
发表回复
rgwfeng2 2003-05-15
Mark
回复
nhconch 2003-04-23
旧东西了,还炒什么
回复
lizongqi 2003-04-22
收藏ing...
回复
redwh 2003-04-22
深奥。学习ING
回复
nyf1220 2003-04-20
收藏~~
回复
chenchangfu 2003-04-18
好东西,改变了你也改变了我
好好好!学习了,但看不懂,我是菜鸟呀
^~^
回复
originaldenny 2003-04-17
to brightlight(蓝色闪电)
"delphi把什么东西都封装的那么好我们如何才能学到更深入的东西呢?"
____下个企业版delphi然后自己去看源代码,你就会了解如何从TObject对象逐渐派生到各种各样的组件
回复
lm008 2003-04-17
我是菜鸟...只想知道如何动态的在窗体上添加控件
先感谢高手了
回复
随风奔跑 2003-04-16
mark
学习
回复
fangchangjiang 2003-04-16
不错不错
回复
FrameSniper 2003-04-15
碰到他们就问问题,想问什么问什么?

想机枪一样把问题射向他们,然后自己不去思考.....

呵呵,先去看书了......
回复
lihongyue 2003-04-14
谢谢,虽然现在看不懂,但会去仔细琢磨。
回复
Nizvoo 2003-04-14
嘻嘻,年年如此。我是碌碌无为的过,年年看人剖析这些东西。呵呵。受益,看来不变的是我。
回复
torble 2003-04-12
感谢
回复
ehom 2003-04-11
以上说的是VCL的消息机制,而非窗体的创建过程,代码由Window回调,所以无法设置断点跟踪

了解程序窗建窗体的整个过程很容易

打开菜单Project->Options,选中Compiler->Debugging->Use Debug DCUs

按F7单步执行,直接在工程文件中的
Application.CreateForm(TMainForm, MainForm);
上设置断点也可
回复
jokerx 2003-04-11
有个问题,想问一下。
在调试的时候,我怎样才能对Application.CreateForm(TMainForm, MainForm);进行调试呢?
怎样设置BreakPoint?才能让我看到FORM的创建过程呢?
这个问题困扰我很长时间了:(,还忘各位给出答案。谢谢了。
回复
wangbing2000 2003-04-10
我好 景仰 学习
回复
jokerx 2003-04-10
学习!学习!学习!学习!学习!学习!学习!学习!学习!学习!学习!学习!学习!学习!学习!学习!学习!学习!学习!学习!学习!学习!学习!学习!学习!学习!学习!学习!学习!学习!学习!学习!学习!学习!学习!学习!

真的要好好学习。。。我期望的水平就是这样啊。。。景仰ING 。。。。
回复
qsfsea 2003-04-09
gz
回复
EISA 2003-04-09
收藏~~研究~~~
回复
发帖
Delphi
创建于2007-08-02

4873

社区成员

Delphi 开发及应用
申请成为版主
帖子事件
创建了帖子
2003-04-01 04:01
社区公告
暂无公告