这样,如果 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 前在堆栈中设置参数:
            push LPARAM
            push WPARAM
            push UINT
            push HWND
            push (eip.Next)             ; 把Windows 回调前下一条语句的地址
                                        ; 保存在堆栈中
            jmp FObjectInstance.Code    ; 调用 TWinControl.FObjectInstance
  
FObjectInstance.Code 只有一句 call 指令:
call ObjectInstance.offset  
            push eip.Next
            jmp InstanceBlock.Code      ; 调用 InstanceBlock.Code
  
InstanceBlock.Code:
            pop ecx                     ; 将 eip.Next 的值存入 ecx, 用于
                                        ; 取 @MainWndProc 和 Self
            jmp StdWndProc              ; 跳转至 StdWndProc
  
StdWndProc 的汇编代码:
function StdWndProc(Window: HWND; Message, WParam: Longint;
  LParam: Longint): Longint; stdcall; assembler;
asm
            push ebp
            mov ebp, esp
        XOR     EAX,EAX
            xor eax, eax
        PUSH    EAX
            push eax                    ; 设置 Message.Result := 0
        PUSH    LParam                  ; 为什么 Borland 不从上面的堆栈中直接
            push dword ptr [ebp+$14]    ; 获取这些参数而要重新 push 一遍?
        PUSH    WParam                  ; 因为 TMessage 的 Result 是
            push dword ptr [ebp+$10]    ; 记录的最后一个字段,而回调函数的 HWND
        PUSH    Message                 ; 是第一个参数,没有办法兼容。
            push dword ptr [ebp+$0c]
        MOV     EDX,ESP
            mov edx, esp                ; 设置 Message 在堆栈中的地址为
                                        ; MainWndProc 的参数
        MOV     EAX,[ECX].Longint[4]
            mov eax, [ecx+$04]          ; 设置 Self 为 MainWndProc 的隐含参数
        CALL    [ECX].Pointer
            call dword ptr [ecx]        : 呼叫 TWinControl.MainWndProc(Self,
                                        ; @Message)
        ADD     ESP,12
            add esp, $0c
        POP     EAX
            pop eax
end;
            pop ebp
            ret $0010
            mov eax, 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: TWndMethod read FWindowProc 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 直接返回
  
这段代码的意思是:如果当前组件正处于拖放状态,则丢弃所有键盘消息。

转载于:https://blog.51cto.com/rosehacker/788736

Delphi 的消息机制浅探二相关推荐

  1. Delphi 的消息机制浅探三

    再看一段:     WM_MOUSEFIRST..WM_MOUSELAST:       if IsControlMouseMsg(TWMMouse(Message)) then       begi ...

  2. Delphi的对象机制浅探[转载]

    Delphi的对象机制浅探 savetime2k@yahoo.com 2004-1-3 前几天开始阅读 VCL 源代码,可是几个基类的继承代码把我看得头大.在大富翁请教了几位仁兄后,我还是对Delph ...

  3. JS闭包中未使用的引用变量回收机制浅探

    缘起与群里贴出的一段sizzle代码: 最后的那段指定为null是否有必要? ---------------------------------------忧郁的分割线------------ siz ...

  4. Android:Android消息机制整理

    文章目录 前言 一.Android消息机制的构成 二.为什么只允许主线程对UI进行更新操作 三.消息机制具体分析 ThreadLocal原理分析 MessageQueue Looper Handler ...

  5. win32mfc————win32消息机制

    文章目录 文章目录 文章目录 前言 一.`消息机制`是什么 二.Windows消息机制 1.产生消息 2.消息传递 3.消息处理 三.基本消息 1.窗口创建消息 2.窗口销毁 3.窗口激活 重要消息 ...

  6. WPF的消息机制(二)- WPF内部的5个窗口之隐藏消息窗口

    原文:WPF的消息机制(二)- WPF内部的5个窗口之隐藏消息窗口 版权声明:本文为博主原创文章,未经博主允许不得转载. https://blog.csdn.net/powertoolsteam/ar ...

  7. Windows消息机制学习笔记(二)—— 窗口与线程

    Windows消息机制学习笔记(二)-- 窗口与线程 要点回顾 消息从哪里来? 实验一:Spy++捕获消息 实验二:消息捕获 消息到哪里去? 窗口在哪? 实验:分析CreateWindowExW 窗口 ...

  8. 【腾讯Bugly干货分享】经典随机Crash之二:Android消息机制

    为什么80%的码农都做不了架构师?>>>    本文作者:鲁可--腾讯SNG专项测试组 测试工程师 背景 承上经典随机Crash之一:线程安全 问题的模型 好几次灰度top1.top ...

  9. Handler消息机制(二):一个线程有几个Handler

    在消息机制里面,有一个非常重要的东西,那就是Looper,Looper的作用主要是从消息队列里面取出消息交给Handler处理,不过不仅限于此,在这里面还有很多东西值得我们去源码看一看: 1.从Loo ...

最新文章

  1. Sqlserver中查询存储过程是否包含某些文本
  2. Python小游戏(小蜜蜂)
  3. 【Linux】一步一步学Linux——sudo命令(105)
  4. 小米登录协议分析_小米回应小米11充电头兼容问题
  5. Java集合之EnumSet
  6. 水泵怎么做_泳池设备日常怎么维护和保养?
  7. 技术动态 | 大规模中文概念图谱CN-Probase正式发布
  8. Oracle运行set autotrace on报错SP2-0618、SP2-0611
  9. goland 方法注释_goland 设置注释模板的过程图文详解
  10. linux系统如何管理文件
  11. websocket 带头部信息请求 header_BeetleX之Websocket协议分析详解
  12. git操作本地仓库基本使用教程
  13. 产品经理/总监 面试题及答案
  14. 【马克思主义基本原理】--第一章--【世界的物质性及发展规律】
  15. 在尘世间做最好的自己
  16. git远程代码回滚_git 远程分支回滚
  17. 饥荒控制台输入没用_饥荒控制台使用教程
  18. 机器学习中的K-means算法原理与R语言实例
  19. 学生信息管理系统之查:查询成绩信息流程
  20. 安装与破解IntelliJ IDEA2017

热门文章

  1. 华为最新全系列交换机命令手册、配置指南下载
  2. 遍历Page的Controls集合
  3. vue 写门户网站_你不得不知道的Vue项目技巧
  4. Android UI开发第四十一篇——墨迹天气3.0引导界面及动画实现
  5. VS2010 asp.net web site项目使用log4net
  6. OSPF 224.0.0.5(AllSPFRouters)和224.0.0.6(AllDRouters)的区别
  7. 在Android软件开发教学过程中应当注意的事项总结
  8. jquery选择器详解
  9. fg jobs bg
  10. 【转】UINavigationController 直接返回到第一级目录