Delphi消息机制浅探_第1页
Delphi消息机制浅探_第2页
Delphi消息机制浅探_第3页
Delphi消息机制浅探_第4页
Delphi消息机制浅探_第5页
已阅读5页,还剩28页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

1、自网上转载我从去年 12 月上旬开始等待李维的 Inside VCL 。我当时的计划是,在这本 书的指导下深入学习 Delphi。到了 12 月底,书还没有出来,我不愿再等,开 始阅读 VCL 源代码。在读完 TObject、 TPersistant 和 TComponent 的代码之 后,我发现还是不清楚 Delphi 对象到底是怎样被创建的。于是我查看 Delphi 生成的汇编代码, 终于理解了对象创建的整个过程 (这里要特别感谢 book523 的 帮助 。此后我就开始学习 Delphi VCL 的消息处理机制。自从我写下 Delphi 的对象 机制浅探,至今正好一个星期,我也基本上把

2、Delphi VCL 的消息处理框架读 完了。 我的学习方法就是阅读源代码, 一开始比较艰苦, 后来线索逐渐清晰起来。 在此把自己对 Delphi VCL 消息机制的理解记录下来,便于今后的复习,也给初 学 Delphi 或没有时间阅读 VCL 源代码的朋友参考 (毕竟没有几个程序员像我 这样有时间 。由于学习时间较短,一定会有错误,请大家指正。我在分析 VCL 消息机制的过程中,基本上只考查了三个类 TObject、 TControl 和 TWinControl。 虽然我没有阅读上层类 (如 TForm的代码, 但我认为这些都是 实现的细节。我相信 VCL 消息系统中最关键的东西都在这三个类

3、中。纲举而目 张,掌握基础类的消息处理方法之后再读其他类的消息处理过程就容易得多了。要想读懂本文,最低配置为:了解 Win32 消息循环和窗口过程基本了解 TObject、 TControl 和 TWinControl 实现的内容熟悉 Delphi 对象的重载与多态推荐配置为:熟悉 Win32 SDK 编程熟悉 Delphi 的对象机制熟悉 Delphi 内嵌汇编语言推荐阅读: Delphi 的原子世界 VCL 窗口函数注册机制研究手记,兼与 MFC 比较 Delphi 的对象机制浅探本文排版格式为:正文由窗口自动换行; 所有代码以 80 字符为边界; 中英文字符以空格符分 隔。(作者保留对本

4、文的所有权利,未经作者同意请勿在在任何公共媒体转载。 目 录= = 一个 GUI Application 的执行过程:消息循环的建立 TWinControl.Create、注册窗口过程和创建窗口 补充知识:TWndMethod 概述 VCL 的消息处理从 TWinControl.MainWndProc 开始 TWinControl.WndProc TControl.WndProc TObject.Dispatch TWinControl.DefaultHandler TControl.Perform 和 TWinControl.Broadcast TWinControl.WMPaint 以 T

5、WinControl 为例描述消息传递的路径= =正 文= = 一个 GUI Application 的执行过程:消息循环的建立= =通常一个 Win32 GUI 应用程序是围绕着消息循环的处理而运行的。在一个标准 的 C 语言 Win32 GUI 程序中,主程序段都会出现以下代码:while (GetMessage(&msg, NULL, 0, 0 / GetMessage 第二个参数为 NULL, / 表示接收所有应用程序产生的窗 口消息TranslateMessage(&msg; / 转换消息中的字符集DispatchMessage(&msg; / 把 msg 参数传递给 lpfnWnd

6、Proc lpfnWndProc 是 Win32 API 定义的回调函数的地址,其原型如下:int _stdcall WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam;Windows 回 调 函 数 (callback function 也 通 常 被 称 为 窗 口 过 程 (window procedure ,本文随意使用这两个名称,代表同样的意义。应用程序使用 GetMessage 不断检查应用程序的消息队列中是否有消息到达。 如 果发现了消息, 则调用 TranslateMessage。 TranslateMessag

7、e 主要是做字符消息本地化的工作, 不是 关 键的函 数 。然后调用 DispatchMessage(&msg。 DispatchMessage(&msg 使 用 msg 为 参 数 调 用 已 创 建 的 窗 口 的 回 调 函 数 (WndClass.lpfnWndProc。 lpfnWndProc 是由用户设计的消息处理方法。当 GetMessage 在 应 用 程 序 的 消 息 队 列 中 发 现 一 条 WM_QUIT 消 息 时 , GetMessage 返回 False,消息循环才告结束,通常应用程序在这时清理资源后 也结束运行。使用最原始的 Win32 API 编写的应用程序

8、的执行过程是很容易理解的,但是用 Delphi VCL 组件封装消息系统,并不是容易的事。首先, Delphi 是一种面向对 象的程序设计语言,不但要把 Win32 的消息处理过程封装在对象的各个继承类 中,让应用程序的使用者方便地调用,也要让 VCL 组件的开发者有拓展消息处 理的空间。 其次, Delphi 的对象模型中所有的类方法都是对象相关的 (也就是传 递了一个隐含的参数 Self,所以 Delphi 对象的方法不能直接被 Windows 回 调。 Delphi VCL 必须用其他的方法让 Windows 回调到对象的消息处理函数。让我们跟踪一个标准的 Delphi Applicat

9、ion 的执行过程, 查看 Delphi 是如何 开始一个消息循环的。program Project1;beginApplication.Initialize;Application.CreateForm(TForm1, Form1;Application.Run;end.在 Project1 的 Application.Initialize 之前, Delphi 编译器会自动插入一 行代码:SysInit._InitExe。 _InitExe 主要是初始化 HInstance 和模块信息表等。 然后 _InitExe 调用 System._StartExe。 System._StartExe

10、 调用 System.InitUnit; System.InitUnit 调用项目中所有被包含单元的 Initialization 段的代码; 其 中有 Controls.Initialization 段,这个段比较关键。在这段代码中建立了 Mouse 、 Screen 和 Application 三个关键的全局对象。Application.Create 调用 Application.CreateHandle。 Application.CreateHandle 建立一个窗口, 并设置 Application.WndProc 为回 调函数 (这里使用了 MakeObjectInstance 方法

11、,后面再谈 。 Application.WndProc 主要处理一些应用程序级别的消息。我第一次跟踪应用程序的执行时没有发现 Application 对象的创建过程,原来 在 SysInit._InitExe 中被隐含调用了。 如果你想跟踪这个过程, 不要设置断点, 直接按 F7 就发现了。然后才到了 Project1 的第 1 句: Application.Initialize;这个函数只有一句代码:if InitProc nil then TProcedure(InitProc;也就是说如果用户想在应用程序的执行前运行一个特定的过程,可以设置 InitProc 指向该过程。 (为什么用户不

12、在 Application.Initialize 之前或在单 元的 Initliazation 段中直接运行这个特定的过程呢?一个可能的答案是:如 果元件设计者希望在应用程序的代码执行之前执行一个过程, 并且这个过程必须 在其他单元的 Initialization 执行完成之后执行 比如说 Application 对象 必须创建 ,则只能使用这个过程指针来实现。 然后是 Project1 的第 2 句: Application.CreateForm(TForm1, Form1; 这句的主要作用是创建 TForm1 对象,然后把 Application.MainForm 设置为 TForm1。最

13、后是 Project1 的第 3 句: Application.Run;TApplication.Run 调 用 TApplication.HandleMessage 处 理 消 息 。 Application.HandleMessage 的代码也只有一行:if not ProcessMessage(Msg then Idle(Msg;TApplication.ProcessMessage 才真正开始建立消息循环。 ProcessMessage 使 用 PeekMessage API 代 替 GetMessage 获 取 消 息 队 列 中 的 消 息 。 使 用 PeekMessage 的好

14、处是 PeekMessage 发现消息队列中没有消息时会立即返回, 这样就为 HandleMessage 函数执行 Idle(Msg 提供了依据。ProcessMessage 在处理消息循环的时候还特别处理了 HintMsg、 MDIMsg 、 KeyMsg 、 DlgMsg 等特殊消息,所以在 Delphi 中很少再看到纯 Win32 SDK 编程 中的要区分 Dialog Window 、 MDI Window 的处理,这些都被封装到 TForm 中去 了 (其实 Win32 SDK 中的 Dialog 也是只是 Microsoft 专门写了一个窗口过程 和一组函数方便用户界面的设计,其内

15、部运作过程与一个普通窗口无异 。function TApplication.ProcessMessage(var Msg: TMsg: Boolean;varHandled: Boolean;beginResult := False;if PeekMessage(Msg, 0, 0, 0, PM_REMOVE then / 从消息队列获取消息 beginResult := True;if Msg.Message WM_QUIT thenbeginHandled := False; / Handled 表示 Application.OnMessage 是否已 /经处理过当前消息。/ 如果用户设置

16、了 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 为什么不放在最前? beginTranslateMessage(Msg; / 处理字符转换DispatchMessage(Msg; /调用

17、 WndClass.lpfnWndProc end;endelseFTerminate := True; / 收到 WM_QUIT 时应用程序终止 / (这里只是设置一个终止标记 end;end;从上面的代码来看, Delphi 应用程序的消息循环机制与标准 Win32 C 语言应用 程序差不多。 只是 Delphi 为了方便用户的使用设置了很多扩展空间, 其副作用 是消息处理会比纯 C Win32 API 调用效率要低一些。= = TWinControl.Create、注册窗口过程和创建窗口= =上面简单讨论了一个 Application 的建立到形成消息循环的过程,现在的问题 是 Delp

18、hi 控件是如何封装创建窗口这一过程的。 因为只有建立了窗口, 消息循 环才有意义。让我们先回顾 Delphi VCL中几个主要类的继承架框:TObject 所有对象的基类TPersistent 所有具有流特性对象的基类TComponent 所有能放在 Delphi Form Designer 上的对象的基类 TControl 所有可视的对象的基类TWinControl 所有具有窗口句柄的对象基类Delphi 是从 TWinControl 开始实现窗口相关的元件。 所谓窗口, 对于程序设计 者来说, 就是一个窗口句柄 HWND。 TWinControl 有一个 FHandle 私有成员代表 当

19、前对象的窗口句柄,通过 TWinControl.Handle 属性来访问。我第一次跟踪 TWinControl.Create 过程时,竟然没有发现 CreateWindow API 被调用, 说明 TWinControl 并不是在对象创建时就建立 Windows 窗口。 如果用 户使用 TWinControl.Create(Application 以后,立即使用 Handle 访问窗口会出现什么情况呢?答案在 TWinControl.GetHandle 中, Handle 是一个只读的窗口句柄:property TWinControl.Handle: HWnd read GetHandle;T

20、WinControl.GetHandle 代码的内容是:一旦用户要访问 FHandle 成员, TWinControl.HandleNeeded 就 会 被 调 用 。 HandleNeeded 首 先 判 断 TWinControl.FHandle 是否是等于 0 (还记得吗?任何对象调用构造函数以后 所有对象成员的内存都被清零 。 如果 FHandle 不等于 0, 则直接返回 FHandle; 如果 FHandle 等于 0,则说明窗口还没有被创建,这时 HandleNeeded 自动调 用 TWinControl.CreateHandle 来创建一个 Handle。但 CreateHa

21、ndle 只是个 包装函数,它首先调用 TWinControl.CreateWnd 来创建窗口,然后生成一些维 护 VCL Control 运行的参数 (我还没细看 。 CreateWnd 是一个重要的过程,它 先调用 TWinControl.CreateParams 设置创建窗口的参数。 (CreateParams 是个 虚方法, 也就是说程序员可以重载这个函数, 定义待建窗口的属性。 CreateWnd 然后调用 TWinControl.CreateWindowHandle。 CreateWindowHandle 才是真正调 用 CreateWindowEx API 创建窗口的函数。够麻烦

22、吧,我们可以抱怨 Borland 为什么把事情弄得这么复杂,但最终希望 Borland 这样设计自有它的道理。上面的讨论可以总结为 TWinControl 为了为 了减少系统资源的占用尽量推迟建立窗口, 只在某个方法需要调用到控件的窗口 句柄时才真正创建窗口。 这通常发生在窗口需要显示的时候。 一个窗口是否需要 显示常常发生在对 Parent 属性 (在 TControl 中定义 赋值的时候。设置 Parent 属性时, TControl.SetParent 方法会调用 TWinControl.RemoveControl 和 TWinControl.InsertControl 方 法 。 In

23、sertControl 调 用 TWinControl.UpdateControlState 。 UpdateControlState 检 查 TWinControl.Showing 属性来判断是否要调用 TWinControl.UpdateShowing。 UpdateShowing 必须要有一个窗口句柄,因此调用 TWinControl.CreateHandle 来创建窗口。不过上面说的这些,只是繁杂而不艰深,还有很多关键的代码没有谈到呢。你可能发现有一个关键的东西被遗漏了,对,那就是窗口的回调函数。由于 Delphi 建立一个窗口的回调过程太复杂了 (并且是非常精巧的设计 ,只好单独 拿

24、出来讨论。cheka 的 VCL 窗口函数注册机制研究手记,兼与 MFC 比较一文中对 VCL 的 窗 口 回 调 实 现 进 行 了 深 入 的 分 析 , 请 参 考 : 我在此简单介绍回调函数在 VCL 中的实现:TWinControl.Create 的代码中,第一句是 inherited,第二句是FObjectInstance := Classes.MakeObjectInstance(MainWndProc;我想这段代码可能吓倒过很多人,如果没有 cheka 的分析,很多人难以理解。 但是你不一定真的要阅读 MakeObjectInstance 的实现过程,你只要知道:MakeObj

25、ectInstance 在内存中生成了一小段汇编代码,这段代码的内容就是一 个标准的窗口过程。 这段汇编代码中同时存储了两个参数, 一个是 MainWndProc 的地址,一个是 Self (对象的地址 。这段汇编代码的功能就是使用 Self 参数 调用 TWinControl.MainWndProc 函数。MakeObjectInstance 返 回 后 , 这 段 代 码 的 地 址 存 入 了 TWinControl.FObjectInstance 私有成员中。这样, TWinControl.FObjectInstance 就可以当作标准的窗口过程来用。 你可能 认为 TWinContr

26、ol 会直接把 TWinControl.FObjectInstance 注册为窗口类的 回 调 函 数 (使 用 RegisterClass API , 但 这 样 做 是 不 对 的 。 因 为 一 个 FObjectInstance 的汇编代码内置了对象相关的参数 (对象的地址 Self,所以 不 能 用 它 作 为 公 共 的 回 调 函 数 注 册 。 TWinControl.CreateWnd 调 用 CreateParams 获得要注册的窗口类的资料,然后使用 Controls.pas 中的静态 函数 InitWndProc 作为窗口回调函数进行窗口类的注册。 InitWndPro

27、c 的参数 符合 Windows 回调函数的标准。 InitWndProc 第一次被回调时就把新建窗口 (注 意不是窗口类 的回调函数替换为对象的 TWinControl.FObjectInstance (这是 一种 Windows subclassing 技术 ,并且使用 SetProp 把对象的地址保存在新 建窗口的属性表中,供 Delphi 的辅助函数读取 (比如 Controls.pas 中的 FindControl 函数 。总之, TWinControl.FObjectInstance 最终是被注册为窗口回调函数了。这样,如果 TWinControl 对象所创建的窗口收到消息后 (形

28、象的说法 ,会被 Windows 回调 TWinControl.FObjectInstance,而 FObjectInstance 会呼叫该 对象的 TWinControl.MainWndProc 函数。 就这样 VCL 完成了对象的消息处理过 程与 Windows 要求的回调函数格式差异的转换。 注意, 在转换过程中, Windows 回调时传递进来的第一个参数 HWND 被抛弃了。因此 Delphi 的组件必须使用 TWinControl.Handle (或 protected 中的 WindowHandle 来得到这个参数。 Windows 回调函数需要传回的返回值也被替换为 TMess

29、age 结构中的最后一个 字段 Result。为 了 使 大 家 更 清 楚 窗 口 被 回 调 的 过 程 , 我 把 从 DispatchMessage 开 始 到 TWinControl.MainWndProc 被 调 用 的 汇 编 代 码 (你 可 以 把 从 FObjectInstance.Code 开始至最后一行的代码看成是一个标准的窗口回调函 数 :DispatchMessage(&Msg / Application.Run 呼叫 DispatchMessage 通知/ Windows 准备回调Windows 准备回调 TWinControl.FObjectInstance 前

30、在堆栈中设置参数: push LPARAMpush WPARAMpush UINTpush HWNDpush (eip.Next ; 把 Windows 回调前下一条语句 的地址; 保存在堆栈中jmp FObjectInstance.Code ; 调 用 TWinControl.FObjectInstanceFObjectInstance.Code 只有一句 call 指令 :call ObjectInstance.offsetpush eip.Nextjmp InstanceBlock.Code ; 调用 InstanceBlock.CodeInstanceBlock.Code:pop ec

31、x ; 将 eip.Next 的值存入 ecx, 用于; 取 MainWndProc 和 Self jmp StdWndProc ; 跳转至 StdWndProcStdWndProc 的汇编代码 :function StdWndProc(Window: HWND; Message, WParam: Longint;LParam: Longint: Longint; stdcall; assembler;asmpush ebpmov ebp, espXOR EAX,EAXxor eax, eaxPUSH EAXpush eax ; 设置 Message.Result := 0 PUSH LPar

32、am ; 为什么 Borland 不从上面的堆 栈中直接push dword ptr ebp+$14 ; 获取这些参数而要重新 push 一 遍?PUSH WParam ; 因为 TMessage 的 Result 是 push dword ptr ebp+$10 ; 记录的最后一个字段,而回调函 数的 HWNDPUSH Message ; 是第一个参数,没有办法兼容。 push dword ptr ebp+$0cMOV EDX,ESPmov edx, esp ; 设置 Message 在堆栈中的地址 为; MainWndProc 的参数MOV EAX,ECX.Longint4mov eax,

33、 ecx+$04 ; 设置 Self 为 MainWndProc 的 隐含参数CALL ECX.Pointercall dword ptr ecx : 呼 叫 TWinControl.MainWndProc(Self,; MessageADD ESP,12add esp, $0cPOP EAXpop eaxend;pop ebpret $0010mov eax, eax看不懂上面的汇编代码,不影响对下文讨论的理解。= = 补充知识:TWndMethod 概述= =写这段基础知识是因为我在阅读 MakeObjectInstance(MainWndProc 这句时不 知道究竟传递了什么东西给 Ma

34、keObjectInstance。弄清楚了 TWndMethod 类型 的含义还可以理解后面 VCL 消息系统中的一个小技巧。TWndMethod = procedure(var Message: TMessage of object;这句类型声明的意 思 是:TWndMethod 是 一种过程 类型, 它指向一个 接收 TMessage 类型参数的过程,但它不是一般的静态过程,它是对象相关 (object related 的。 TWndMethod 在内存中存储为一个指向过程的指针和一个对象的指 针,所以占用 8个字节。 TWndMethod 类型的变量必须使用已实例化的对象来赋 值。举个例

35、子:varSomeMethod: TWndMethod;beginSomeMethod := Form1.MainWndProc; / 正确。这时 SomeMethod 包含 /MainWndProc和 Form1 的指针, /可以用 SomeMethod(Msg来执行。 SomeMethod := TForm.MainWndProc; / 错误!不能用类引用。end;如果把 TWndMethod变量赋值给虚方法会怎样?举例:varSomeMethod: TWndMethod;beginSomeMethod := Form1.WndProc; / TForm.WndProc 是虚方法end;这

36、时, 编译器实现为 SomeMethod 指向 Form1 对象虚方法表中的 WndProc 过程 的地址和 Form1 对象的地址。也就是说编译器正确地处理了虚方法的赋值。调 用 SomeMethod(Message 就等于调用 Form1.WndProc(Message。在可能被赋值的情况下,对象方法最好不要设计为有返回值的函数 (function, 而要设计为过程 (procedure。原因很简单,把一个有返回值的对象方法赋值给 TWndMethod 变量,会造成编译时的二义性。= = VCL 的消息处理从 TWinControl.MainWndProc 开始= =通 过 对 Appli

37、cation.Run 、 TWinControl.Create 、 TWinControl.Handle 和 TWinControl.CreateWnd 的讨论,我们现在可以把焦点转向 VCL 内部的消息处 理过程。 VCL 控件的消息源头就是 TWinControl.MainWndProc 函数。 (如果不能 理解这一点,请重新阅读上面的讨论。 让我们先看一下 MainWndProc 函数的代码 (异常处理的语句被我删除 :procedure TWinControl.MainWndProc(var Message: TMessage;beginWindowProc(Message;end;T

38、WinControl.MainWndProc 以 引 用 (也 就 是 隐 含 传 地 址 的 方 式 接 受 一 个 TMessage 类型的参数, TMessage 的定义如下 (其中的 WParam 、 LParam 、 Result 各有 HiWord 和 LoWord 的联合字段,被我删除了,免得代码太长 :TMessage = packed recordMsg: Cardinal;WParam: Longint;LParam: Longint;Result: Longint;end;TMessage 中并没有窗口句柄,因为这个句柄已经在窗口创建之后保存在 TWinControl.H

39、andle 之中。 TMessage.Msg 是消息的 ID 号,这个消息可以是 Windows 标准消息、 用户定义的消息或 VCL 定义的 Control 消息等。 WParam 和 LParam 与标准 Windows 回调函数中 wParam 和 lParam 的意义相同, Result 相当于标准 Windows 回调函数的返回值。注意 MainWndProc 不是虚函数, 所以它不能被 TWinControl 的继承类重载。 (思 考:为什么 Borland 不将 MainWndProc 设计为虚函数呢? MainWndProc 中建立两层异常处理, 用于释放消息处理过程中发生异常

40、时的资源 泄 漏 , 并 调 用 默 认 的 异 常 处 理 过 程 。 被 异 常 处 理 包 围 着 的 是 WindowProc(Message。 WindowProc 是 TControl(而不是 TWinControl 的一个 属性 (property:property WindowProc: TWndMethod read FWindowProc write FWindowProc;WindowProc 的类型是 TWndMethod, 所以它是一个对象相关的消息处理函数指针 (请参考前面 TWndMethod 的介绍 。 在 TControl.Create 中 FWindowPr

41、oc 被赋 值为 WndProc。WndProc 是 TControl 的一个函数,参数与 TWinControl.MainWndProc 相同: procedure TControl.WndProc(var Message: TMessage; virtual;原来 MainWndProc 只是个代理函数,最终处理消息的是 TControl.WndProc 函 数。那么 Borland 为什么要用一个 FWindowProc 来存储这个 WndProc 函数,而不 直接调用 WndProc 呢?我猜想可能是基于效率的考虑。 还记得上面 TWndMethod 的讨论吗?一个 TWndMetho

42、d 变量可以被赋值为一个虚函数, 编译器对此操作的 实现是通过对象指针访问到了对象的虚函数表, 并把虚函数表项中的函数地址传 回。由于 WndProc 是一个调用频率非常高的函数 (可能要用“百次 /秒”或“千 次 /秒”来计算 ,所以如果每次调用 WndProc 都要访问虚函数表将会浪费大量 时间, 因此 在 TControl 的构 造函 数中 就 把 WndProc 的真 正 地址存 储在 WindowProc 中,以后调用 WindowProc 将就转换为静态函数的调用,以加快处 理速度。= = TWinControl.WndProc= =转了层层弯,到现在我们才刚进入 VCL 消息系统

43、处理开始的地方:WndProc 函数。 如前所述, TWinControl.MainWndProc 接收到消息后并没有处理消息, 而是 把消息传递给 WindowProc 处理。由于 WindowProc 总是指向当前对象的 WndProc 函数的地址, 我们可以简单地认为 WndProc 函数是 VCL 中第一个处理 消息的函数,调用 WindowProc 只是效率问题。WndProc 函数是个虚函数,在 TControl 中开始定义,在 TWinControl 中被重 载。 Borland 将 WndProc 设计为虚函数就是为了各继承类能够接管消息处理, 并把未处理的消息或加工过的消息传

44、递到上一层类中处理。这里将消息处理的传递过程和对象的构造函数稍加对比:对象的构造函数通常会在第一行代码中使用 inherited 语句调用父类的构造函 数以初始化父类定义的成员变量, 父类也会在构造函数开头调用祖父类的构造函 数,如此递归,因此一个 TWinControl 对象的创建过程是 TComponent.Create - TControl.Create - TWinControl.Create。而消息处理函数 WndProc 则是先处理自己想要的消息,然后看情况是否要递交 到父类的 WndProc 中处理。所以消息的处理过程是 TWinControl.WndProc - TContro

45、l.WndProc 。因此,如果要分析消息的处理过程,应该从子类的 WndProc 过程开始,然后才 是父类的 WndProc 过程。由于 TWinControl 是第一个支持窗口创建的类,所 以它的 WndProc 是很重要的,它实现了最基本的 VCL 消息处理。TWinControl.WndProc 主要是预处理一些键盘、 鼠标、 窗口焦点消息, 对于不必 响 应 的 消 息 , TWinControl.WndProc 直 接 返 回 , 否 则 把 消 息 传 递 至 TControl.WndProc 处理。从 TWinControl.WndProc 摘抄一段看看:WM_KEYFIRST

46、.WM_KEYLAST:if Dragging then Exit; / 注意:使用 Exit 直接返回这段代码的意思是:如果当前组件正处于拖放状态,则丢弃所有键盘消息。再看一段:WM_MOUSEFIRST.WM_MOUSELAST:if IsControlMouseMsg(TWMMouse(Message thenbegin Check HandleAllocated because IsControlMouseMsg might have freed thewindow if user code executed something like Parent := nil. if (Mess

47、age.Result = 0 and HandleAllocated thenDefWindowProc(Handle, Message.Msg, Message.wParam,Message.lParam;/ DefWindowProc 是 Win32 API 中缺省处理消息的函数Exit;end;这里的 IsControlMouseMsg 很关键。 让我们回忆一下:TControl 类的对象并没 有创建 Windows 窗口,它是怎样接收到鼠标和重绘等消息的呢?原来这些消息 就是由它的 Parent 窗口发送的。在上面的代码中, TWinControl.IsControlMouseMsg

48、判断鼠标地址是否落在 TControl 类 控 件 上 , 如 果 不 是 就 返 回 否 值 。 TWinControl 再 调 用 TControl.WndProc , TControl.WndProc 又调用了 TObject.Dispatch 方法,这 是后话。如果当前鼠标地址落在窗口上的 TControl 类控件上, 则根据 TControl 对象的 相对位置重新生成了鼠标消息, 再调用 TControl.Perform 方法把加工过的鼠标 消息直接发到 TControl.WndProc 处理。 TControl.Perform 方法以后再谈。如果 TWinControl 的继承类重

49、载 WndProc 处鼠标消息,但不使用 inherited 把消息传递给父类处理,则会使从 TControl 继承下来的对象不能收到鼠标消 息。现在我们来做个试验,下面 Form1 上的 TSpeedButton 等非窗口控件不会 发生 OnClick 等鼠标事件。procedure TForm1.WndProc(var Message: TMessage; override;begincase Message.Msg ofWM_MOUSEFIRST.WM_MOUSELAST:beginDefWindowProc(Handle, Message.Msg, Message.WParam, Me

50、ssage.LParam;Exit; / 直接退出end;elseinherited;end;end;TWinControl.WndProc 的最后一行代码是:inherited WndProc(Message;也就是调用 TControl.WndProc。 让我们来看看 TControl.WndProc 做了些什么。 = TControl.WndProc= =TControl.WndProc 主要实现的操作是:响应与 Form Designer 的交互 (在设计期间 在控件不支持双击的情况下把鼠标双击事件转换成单击判断鼠标移动时是否需要显示提示窗口 (HintWindow判断控件是否设置为

51、AutoDrag,如果是则执行控件的拖放处理调用 TControl.MouseWheelHandler 实现鼠标滚轮消息使用 TObject.Dispatch 调用 DMT 消息处理方法TControl.WndProc 相对比较简单,在此只随便谈谈第二条。你是否有过这样的 使用经验:在你快速双击某个软件的 Button 时,只形成一次 Click 事件。所 以如果你需要设计一个不管用户用多快的速度点击,都能生成同样点击次数 Click 事件的按钮时,就需要参考 TControl.WndProc 处理鼠标消息的过程了。TControl.WndProc 最后一行代码是 Dispatch(Messa

52、ge,也就是说如果某个消 息没有被 TControl 以后的任何类处理,消息会被 Dispatch 处理。TObject.Dispatch 是 Delphi VCL 消息体系中非常关键的方法。= = TObject.Dispatch= =TObject.Dispatch 是个虚函数,它的声明如下:procedure TObject.Dispatch(var Message; virtual;请注意它的参数虽然与 MainWndProc 和 WndProc 的参数相似, 但它没有规定参 数的类型。这就是说, Dispatch 可以接受任何形式的参数。Delphi 的文档指出:Message 参数

53、的前 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 个字节,如果 TContr

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论