由VCL代码理解VCL的消息机制.docx
- 文档编号:4194149
- 上传时间:2022-11-28
- 格式:DOCX
- 页数:26
- 大小:25.85KB
由VCL代码理解VCL的消息机制.docx
《由VCL代码理解VCL的消息机制.docx》由会员分享,可在线阅读,更多相关《由VCL代码理解VCL的消息机制.docx(26页珍藏版)》请在冰豆网上搜索。
由VCL代码理解VCL的消息机制
Delphi,一个非常优秀的开发工具,拥有强大的可视化开发环境、面向组件的快速开发模式、优秀的VCL类库、快速的代码编译器、强大的数据库和WEB开发能力、还有众多的第三方控件支持...(此处省略x千字,既然大家都知道了,不浪费口水了^_^)
说到VCL的优秀就不能不提到其对Windows消息及API的较全面和完美的封装,正因为如此开发者在大多数情况下甚至不需理会Windows消息处理的细节,而只需要写几行事件驱动代码即可!
但如果做为开发人员你还是想对此做些了解的话,那么就继续,通过VCL代码本身来体会VCL中的消息处理机制。
(以下代码取自Delphi6)
说到VCL中的消息处理就不能不提到TApplication,Windows会为每一个当前运行的程序建立一个消息队列,用来完成用户与程序的交互,正是通过Application完成了对Windows消息的集中处理!
首先通过Application.Run进入消息循环进行消息的处理,其中调用了HandleMessage。
procedureTApplication.HandleMessage;
var
Msg:
TMsg;
begin
ifnotProcessMessage(Msg)thenIdle(Msg);//这里先调用ProcessMessage处理,返回值为False调用Idle,就是在空闲时,即消息队列中无消息等待处理时调用Idle。
end;
functionTApplication.ProcessMessage(varMsg:
TMsg):
Boolean;
var
Handled:
Boolean;
begin
Result:
=False;
ifPeekMessage(Msg,0,0,0,PM_REMOVE)then//查询消息队列中有无消息等待处理,参数PM_REMOVE使消息在处理完后会被删除。
begin
Result:
=True;
ifMsg.Message<>WM_QUITthen//如果是WM_QUIT,终止进程,否则执行下面的代码
begin
Handled:
=False;
ifAssigned(FOnMessage)thenFOnMessage(Msg,Handled);
ifnotIsHintMsg(Msg)andnotHandledandnotIsMDIMsg(Msg)and
notIsKeyMsg(Msg)andnotIsDlgMsg(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!
再找到functionInitWndProc(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=packedrecord
Code:
Byte;
Offset:
Integer;
caseIntegerof
0:
(Next:
PObjectInstance);
1:
(Method:
TWndMethod);
end;
type
PInstanceBlock=^TInstanceBlock;
TInstanceBlock=packedrecord
Next:
PInstanceBlock;
Code:
array[1..2]ofByte;
WndProcPtr:
Pointer;
Instances:
array[0..InstanceCount]ofTObjectInstance;
end;
var
InstBlockList:
PInstanceBlock;
InstFreeList:
PObjectInstance;
functionStdWndProc(Window:
HWND;Message,WParam:
Longint;LParam:
Longint):
Longint;stdcall;assembler;
asm
XOREAX,EAX
PUSHEAX
PUSHLParam
PUSHWParam
PUSHMessage
MOVEDX,ESP;将堆栈中构造的记录TMessage指针赋给EDX
MOVEAX,[ECX].Longint[4];传递Self指针给EAX,类中的Self指针也就是指向VMT入口地址
CALL[ECX].Pointer;调用MainWndProc方法
ADDESP,12
POPEAX
end;
functionCalcJmpOffset(Src,Dest:
Pointer):
Longint;
begin
Result:
=Longint(Dest)-(Longint(Src)+5);
end;
functionMakeObjectInstance(Method:
TWndMethod):
Pointer;
const
BlockCode:
array[1..2]ofByte=(
$59,{POPECX}
$E9);{JMPStdWndProc}
PageSize=4096;
var
Block:
PInstanceBlock;
Instance:
PObjectInstance;
begin
ifInstFreeList=nilthen
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;{CALLNEARPTROffset}
Instance^.Offset:
=CalcJmpOffset(Instance,@Block^.Code);
Instance^.Next:
=InstFreeList;
InstFreeList:
=Instance;
Inc(Longint(Instance),SizeOf(TObjectInstance));
untilLongint(Instance)-Longint(Block)>=SizeOf(TInstanceBlock);
InstBlockList:
=Block;
end;
Result:
=InstFreeList;
Instance:
=InstFreeList;
InstFreeList:
=Instance^.Next;
Instance^.Method:
=Method;
end;
(注:
上面出现的那些16进制代码其实就是些16进制的机器代码$59=PopECX$E8=Call$E9=Jmp)
以上代码看起来有点乱,但综合起来看也很好理解!
MakeObjectInstance实际上就是构建了一个Block链表
其结构看看记录TInstanceBlock的结构可知其结构如下:
Next//下一页指针
Code//PopECX和Jmp
WndProcPtr//和StdWndProc间的地址偏移
Instances//接下来是314个Instance链表
Instance链表通过记录TObjectInstance也很好理解其内容
Code//Call
Offset//地址偏移
Method//指向对象方法的指针(结合TMethod很好理解TWndMethod这类对象方法指针指向数据的结构)
好现在来把这个流程回顾一遍,Windows回调的是什么呢?
其实是转到并执行一段动态生成的代码:
先是执行Calloffset,根据偏移量转去执行PopECX,当然由于在Call这之前会将下一条指令入栈,所以这里弹出的就是指向对象方法的指针。
接下来就是执行jmp[StdWndProc],其中将堆栈中构造的记录TMessage指针赋给了EDX,而根据上面的解释结合TMethod去理解,很容易理解
MOVEAX,[ECX].Longint[4];传递Self指针给EAX,类中的Self指针也就是指向VMT入口地址
CALL[ECX].Pointer;调用MainWndProc方法
现在终于豁然开朗了,Windows消息就是这样被传递到了TWinControl.MainWndProc,相比MFC中的回调全局函数AfxWndProc来根据窗体句柄检索对应的对象指针的方法效率要高的多!
VCL比MFC优秀的又一佐证!
^_^
现在终于找到了VCL接收消息的方法MainWndProc
procedureTWinControl.MainWndProc(varMessage:
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
procedureTControl.WndProc(varMessage:
TMessage);
var
Form:
TCustomForm;
KeyState:
TKeyboardState;
WheelMsg:
TCMMouseWheel;
begin
...
//省略以上的消息相关处理代码,研究某些特定消息时可自行查看
...
Dispatch(Message);//调用Dispatch处理
end;
接下来,先不急着查看Dispatch中的相应代码。
想想看,忘了什么?
上面只是继承于TWinControl的有句柄的控件,那继承于TGraphicControl的没有句柄的控件是如何获得并处理消息的?
下面以鼠标消息为例:
TWinControl.WndProc中有下面的代码:
caseMessage.Msgof
...
WM_MOUSEFIRST..WM_MOUSELAST:
//注1:
下面再解释这段
ifIsControlMouseMsg(TWMMouse(Message))then
begin
{CheckHandleAllocatedbecauseIsControlMouseMsgmighthavefreedthe
windowifusercodeexecutedsomethinglikeParent:
=nil.}
if(Message.Result=0)andHandleAllocatedthen
DefWindowProc(Handle,Message.Msg,Message.wParam,Message.lParam);
Exit;
end;
...
end;
inheritedWndProc(Message);//执行祖先类的WndProc方法
functionTWinControl.IsControlMouseMsg(varMessage:
TWMMouse):
Boolean;
var
Control:
TControl;
P:
TPoint;
begin
ifGetCapture=Handlethen
begin
Control:
=nil;
if(CaptureControl<>nil)and(CaptureControl.Parent=Self)then
Control:
=CaptureControl;
endelse
Control:
=ControlAtPos(SmallPointToPoint(Message.Pos),False);//这里通过ControlAtPos获得了鼠标所在控件
Result:
=False;
ifControl<>nilthen
begin
P.X:
=Message.XPos-Control.Left;
P.Y:
=Message.YPos-Control.Top;
Message.Result:
=Control.Perform(Message.Msg,Message.Keys,Longint(PointToSmallPoint(P)));//调用Perform方法发送消息给对应的实例
Result:
=True;
end;
end;
propertyWindowProc:
TWndMethodreadFWindowProcwriteFWindowProc;
functionTControl.Perform(Msg:
Cardinal;WParam,LParam:
Longint):
Longint;
var
Message:
TMessage;
begin
Message.Msg:
=Msg;
Message.WParam:
=WParam;
Message.LParam:
=LParam;
Message.Result:
=0;
ifSelf<>nilthenWindowProc(Message);//由于TControl创建实例时已经将FWindowProc指向WndProc,所以这里实际也就是调用WndProc
Result:
=Message.Result;
end;
VCL中就是这样将消息分发给了那些继承于TGraphicControl的没有句柄的图形控件。
上面说的都是Windows消息(WindowsMessages),似乎还应该说说两条经常用到的VCL中自定义消息:
CM_MOUSEENTER,CM_MOUSELEAVE(CM=ShortofControlMessage)
它们是如何被处理的呢?
还是看上面的(ifnotProcessMessage(Msg)thenIdle(Msg);),这两条不是Windows消息,所以会触发Idle
procedureTApplication.Idle(constMsg:
TMsg);
var
Control:
TControl;
Done:
Boolean;
begin
Control:
=DoMouseIdle;//调用DoMouseIdle方法
...
end;
functionTApplication.DoMouseIdle:
TControl;
var
CaptureControl:
TControl;
P:
TPoint;
begin
GetCursorPos(P);
Result:
=FindDragTarget(P,True);//获取当前鼠标所停留在的控件
if(Result<>nil)and(csDesigninginResult.ComponentState)then
Result:
=nil;
CaptureControl:
=GetCaptureControl;
ifFMouseControl<>Resultthen//判断以前记录的鼠标指针所指向的控件和现在所指向的控件是否相同
begin
if((FMouseControl<>nil)and(CaptureControl=nil))or
((CaptureControl<>nil)and(FMouseControl=CaptureControl))then
FMouseControl.Perform(CM_MOUSELEAVE,0,0);//发送消息CM_MOUSELEAVE给以前记录的鼠标指针所指向的控件
FMouseControl:
=Result;//记录当前鼠标指针所指向的控件
if((FMouseControl<>nil)and(CaptureControl=nil))or
((CaptureControl<>nil)and(FMouseControl=CaptureControl))then
FMouseControl.Perform(CM_MOUSEENTER,0,0);//发送消息CM_MOUSEENTER给鼠标指针现在所在的控件
end;
end;
functionFindDragTarget(constPos:
TPoint;AllowDisabled:
Boolean):
TControl;
var
Window:
TWinControl;
Control:
TControl;
begin
Result:
=nil;
Window:
=FindVCLWindow(Pos);//这里返回的是TWinControl,是一个有句柄的控件
ifWindow<>nilthen
begin
Result:
=Window;
Control:
=Window.ControlAtPos(Window.ScreenToClient(Pos),AllowDisabled);//鼠标所指向处可能还存在一继承于TGraphicControl的图形控件,而上面返回的只是其容器控件
ifControl<>nilthenResult:
=Control;//如果存在就返回用ControlAtPos所得到的控件
end;
end;
于是又转到了上面的TControl.Perform
现在所有的问题又都集中到了Dispatch的身上,消息是如何触发事件的处理方法的呢?
首先看条消息处理方法的申明:
procedureCMMouseEnter(varMessage:
TMessage);messageCM_MOUSEENTER;
这实际可以认为是申明了一个动态方法,调用Dispatch实际上就是通过消息号在DMT(动态方法表)中找到相应的动态方法指针,然后执行
//上面已经提到了,寄存器EAX中是类的Self指针,即VMT入口地址,寄存器EDX中是指向记录Message的指针
procedureTObject.Dispatch(varMessage);
asm
PUSHESI
MOVSI,[EDX];消息号,也就是记录TMessage中Msg的值,对应CM_MOUSEENTER就是$B013(45075)
ORSI,SI
JE@@default
CMPSI,0C000H
JAE@@default
PUSHEAX
MOVEAX,[EAX];VMT入口地址
CALLGetDynaMethod;调用GetDynaMethod查找
POPEAX
JE@@default;在GetDynaMethod中如果找到会将标志位寄存器的值置为0,如果是1,表示未找到,执行跳转
MOVECX,ESI;传递指针给ECX
POPESI
JMPECX;跳转到ECX所指向的位置,也就完成了通过消息号调用CMMouseEnter的过程
@@default:
POPESI
MOVECX,[EAX]
JMPdwordptr[ECX].vmtDefaultHandler;如果此构件和它的祖先类中都没有对应此消息的处理句柄,调用Defaulthandler方法
end;
procedureGetDynaMethod;
{functionGetDynaMethod(vmt:
TClass;selector:
Smallint):
Pointer;}
asm
{->EAXvmtofclass}
{SIdy
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- VCL 代码 理解 消息 机制