再看一段:
    WM_MOUSEFIRST..WM_MOUSELAST:
      if IsControlMouseMsg(TWMMouse(Message)) then
      begin
        { Check HandleAllocated because IsControlMouseMsg might have freed the
          window if user code executed something like Parent := nil. }
        if (Message.Result = 0) and HandleAllocated then
          DefWindowProc(Handle, Message.Msg, Message.wParam, Message.lParam);
          // DefWindowProc 是 Win32 API 中缺省处理消息的函数
        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 主要实现的操作是:
    响应与 Form Designer 的交互(在设计期间)
    在控件不支持双击的情况下把鼠标双击事件转换成单击
    判断鼠标移动时是否需要显示提示窗口(HintWindow)
    判断控件是否设置为 AutoDrag,如果是则执行控件的拖放处理
    调用 TControl.MouseWheelHandler 实现鼠标滚轮消息
    使用 TObject.Dispatch 调用 DMT 消息处理方法
  
TControl.WndProc 相对比较简单,在此只随便谈谈第二条。你是否有过这样的使用经验:在你快速双击某个软件的 Button 时,只形成一次 Click 事件。所以如果你需要设计一个不管用户用多快的速度点击,都能生成同样点击次数 Click 事件的按钮时,就需要参考 TControl.WndProc 处理鼠标消息的过程了。
  
TControl.WndProc 最后一行代码是 Dispatch(Message),也就是说如果某个消息没有被 TControl 以后的任何类处理,消息会被 Dispatch 处理。
  
TObject.Dispatch 是 Delphi VCL 消息体系中非常关键的方法。
  
===============================================================================
⊙ 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×0000FFFF              RegisterWindowMessage 生成的消息范围
  0×00010000 至 0xFFFFFFFF              Microsoft 保留的消息,只由系统使用
  
  ( WM_USER = 0×00000400,  WM_APP = 0×00008000 )
  
发现问题的答案了吗?原来应用程序真正可用的消息只有 0×00000000 0×0000FFFF,也就是消息 ID 只有低位 2 字节是有效的。(Borland 真是牛啊,连这也能想出来。)
  
由于 Intel CPU 的内存存放规则是高位字节存放在高地址,低位字节存放在低地址,所以 Dispatch 的 Message 参数的第一个内存字节就是 LoWord(Message.Msg)。下图是 Message参数的内存存放方式描述:
  
        |        | + Memory
        |——–|
        | HiWord |
        |——–|
        | LoWord | = $C000,调用 DefaultHandler (注意这里)
    PUSH    EAX            ; 保存对象的指针
    MOV     EAX,[EAX]      ; 找到对象的 VMT 指针
    CALL    GetDynaMethod  ; 调用对象的动态方法; 如果找到了动态方法 ZF = 0
                           ; 没找到 ZF = 1
                           ; 注:GetDynaMethod 是 System.pas 中的获得动态方法地
                           ; 址的汇编函数
    POP     EAX            ; 恢复 EAX 为对象的指针
    JE      @@default      ; 如果没找到相关的动态方法,调用 DefaultHandler    
    MOV     ECX,ESI        ; 把找到的动态方法指针存入 ECX
    POP     ESI            ; 恢复 ESI
    JMP     ECX            ; 调用对象的动态方法
  
@@default:
    POP     ESI            ; 恢复 ESI
    MOV     ECX,[EAX]      ; 把对象的 VMT 指针存入 ECX,以调用 DefaultHandler
    JMP     DWORD PTR [ECX] + VMTOFFSET TObject.DefaultHandler
end;
  
TObject.Dispatch 的执行过程是:
    把 MsgID 存入 SI,作为动态方法的索引值
    如果 SI >= $C000,则调用 DefaultHandler(也就是所有 RegisterWindowMessage
        生成的消息ID 会直接被发送到 DefaultHandler 中,后面会讲一个实例)
    检查是否有相对应的动态方法
    找到了动态方法,则执行该方法
    没找到动态方法,则调用 DefaultHandler
  
原来以 message 关键字定义的对象方法就是动态方法,随便从 TWinControl 中抓几个消息处理函数出来:
  
    procedure WMSize(var Message: TWMSize); message WM_SIZE;
    procedure WMMove(var Message: TWMMove); message WM_MOVE;
  
到现在终于明白 WM_SIZE、WM_PAINT 方法的处理过程了吧。不但是 Windows 消息,连 Delphi 自己定义的消息也是以同样的方式处理的:
  
    procedure CMEnabledChanged(var Message: TMessage); message CM_ENABLEDCHANGED;
    procedure CMFontChanged(var Message: TMessage); message CM_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 是应用程序启动时自动使用 RegisterWindowMessage API 注册的 Windows 系统级消息ID,也就是说这个消息到达 Dispatch 后会无条件地传递给 DefaultHandler(见 Dispatch 的分析)。TWinControl.DefaultHandler 发现这个消息就把 Self 指针设置为返回值。在 Controls.pas 中有个函数 ObjectFromHWnd 使用窗口句柄获得 TWinControl 的句柄,就是使用这个消息实现的。不过这个消息是由 Delphi 内部使用,不能被应用程序使用。(思考:每次应用程序启动都会调用 RegisterWindowMessage,如果电脑长期不停机,那么 0xC000 – 0xFFFF 之间的消息 ID 是否会被耗尽?)
  
另外,TWinControl.DefaultHandler 在 TWinControl.FHandle 不为 0 的情况下,使用 CallWindowProc API 调用 TWndControl.FDefWndProc 窗口过程。FDefWndProc 是个指针,它是从哪里初始化的呢?跟踪一下,发现它是在 TWinControl.CreateWnd 中被设置为如下值:
  
    FDefWndProc := Params.WindowClass.lpfnWndProc;
  
还记得前面讨论的窗口创建过程吗?TWinControl.CreateWnd 函数首先调用 TWinControl.CreateParams 获得待创建的窗口类的参数。CreateParams 把 WndClass.lpfnWndProc 设置为 Windows 的默认回调函数 DefWindowProc API。但 CreateParams 是个虚函数,可以被 TWinControl 的继承类重载,因此程序员可以指定一个自己设计的窗口过程。
  
所以 TWinControl.DefaultHandler 中调用 FDefWndProc 的意图很明显,就是可以在 Win32 API 的层次上支持消息的处理(比如可以从 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 类控件的鼠标和重绘消息是从 Parent TWinControl 中产生的。但我们只发现了鼠标消息的产生,那么重绘消息是从哪里产生出来的呢?答案是TWinControl.WMPaint:
  
    procedure TWinControl.WMPaint(var Message: TWMPaint); message WM_PAINT;
  
在 TWinControl.WMPaint 中建立了双缓冲重绘机制,但我们目前不关心这个,只看最关键的代码:
  
    if not (csCustomPaint in ControlState) and (ControlCount = 0) then
      inherited                 // 注意 inherited 的实现
    else
      PaintHandler(Message);    
  
这段代码的意思是,如果控件不支持自绘制并且不包含 TControl 就调用 inherited
inherited 是什么呢?由于 TWinControl.WMPaint 的父类 TControl 没有实现这个消息句柄,Delphi 生成的汇编代码竟然是:call Self.DefaultHandler。(TWinControl.DefaultHandler 只是简单地调用 TWinControl.FDefWndProc。)
  
如果条件为否,那么将调用 TWinControl.PaintHandler(不是虚函数)。PaintHandler 调用 BeginPaint API 获得窗口设备环境,再使用该设备环境句柄为参数调用 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
                      |-Call DMT messages
                      |-TWinControl.DefaultHandler
                          |-TControl.DefaultHandler
  
注:
如前文所述,上图中的 WindowProc 是个指针,所以它在编译器级实际上等于 WndProc,而不是调用 WndProc,图中为了防止与消息分枝混淆特意区分成两层。
TObject.Dispatch 有两条通路,如果当前控件以 message 关键字实现了消息处理函数,则呼叫该函数,否则调用 DefaultHandler。
有些消息处理函数可能在中途就已经返回了,有些消息处理函数可能会被递归调用。
  
===============================================================================
结束语
VCL 的消息机制就讨论到这里。希望我们通过本文的讨论理清了 VCL 处理消息的框架,今后我们将使用这些最基础的知识开始探索 Delphi 程序设计的旅程。
===============================================================================

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

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

  1. Delphi 的消息机制浅探二

    这样,如果 TWinControl 对象所创建的窗口收到消息后(形象的说法),会被 Windows 回调 TWinControl.FObjectInstance,而 FObjectInstance 会 ...

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

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

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

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

  4. handler消息机制

    MessageQueue代码:http://grepcode.com/file_/repository.grepcode.com/java/ext/com.google.android/android ...

  5. Android Handler消息机制源码分析

    一,前言 众多周知, Android 只允许在主线程中更新UI,因此主线程也称为UI线程(ActivityThread). 如此设计原因有二: (1) 由于UI操作的方法都不是线程安全的,如果多个线程 ...

  6. UVM 中的消息机制

    UVM消息属性 UVM 中的消息有三种属性,分别是:严重度(severity).冗余度(verbosity).以及消息的关联行为,此外还有消息的标签 ID.UVM 的消息机制基于该三种属性和标签 ID ...

  7. WPF的消息机制(三)- WPF内部的5个窗口之处理激活和关闭的消息窗口以及系统资源通知窗口...

    目录 WPF的消息机制(一)-让应用程序动起来 WPF的消息机制(二)-WPF内部的5个窗口 (1)隐藏消息窗口 (2)处理激活和关闭的消息窗口和系统资源通知窗口 (3)用于用户交互的可见窗口 (4) ...

  8. Windows消息机制学习笔记(三)—— 消息的接收与分发

    Windows消息机制学习笔记(三)-- 消息的接收与分发 要点回顾 消息循环 消息队列 消息的接收 GetMessage 实验1:理解GetMessage 第一步:编译并运行程序A 第二步:编译并运 ...

  9. android handler的机制和原理_一文搞懂handler:彻底明白Android消息机制的原理及源码

    提起Android消息机制,想必都不陌生.其中包含三个部分:Handler,MessageQueue以及Looper,三者共同协作,完成消息机制的运行.本篇文章将由浅入深解析Android消息机制的运 ...

最新文章

  1. Java架构技术揭秘:Redis+Nginx+Dubbo精选+面试题+精选视频(送)
  2. 干货|为什么Kafka不支持读写分离
  3. WCF系列之.net(3.0/3.5)Rest使用示例
  4. android自定义控件 jar,Android Studio引用自定义的framework.jar包
  5. 为什么有的人手机通知栏显示的是4G+而有的是HD?
  6. H3C 模拟器 pc与sw直连 登录web
  7. jQuery获取select onChange的值
  8. 新型恶意软件—— Grizzly攻击Telegram
  9. 推荐记录片系列:Ultimate Factories系列和MegaStructures系列
  10. Atitit NER实体命名识别(Name Entity Recognition 目录 1.1. a. NER实体命名识别(Name Entity Recognition)   1 2. NER抽
  11. javascript技术教程蔡敏_JavaScript基础与实践教程 (王萍萍,赵俊莉,孙强) pdf扫描版...
  12. Windows11安装JDK
  13. 操作系统漏洞检测与利用
  14. WPS 文件忘记保存退出找回
  15. Mac下使用Eclipse读java源码
  16. 【Elastic知识简报】standard analyzer和standard tokenizer有什么区别?
  17. 初中计算机期末质量分析,信息技术期末质量分析
  18. hibernate中各种不一样的查询hql,hqc(一)
  19. Shiro权限控制+整合shiro
  20. ESP8266-Arduino编程实例-磁簧开关传感器驱动

热门文章

  1. SublimeLinter插件对PHP语法检测不起作用的解决办法
  2. 关于外部存储器件对存储数据的管理。
  3. Git: 生成ssh公钥
  4. 贪心算法(Greedy Algorithm)最小生成树 克鲁斯卡尔算法(Kruskal#39;s algorithm)
  5. 使用NSRunloop等待异步任务完成
  6. Linux学习笔记--对文本的操作及正则表达式
  7. 【LeetCode】53.最大子序和
  8. dbeaver连接mysql 驱动jar_Jmeter(七) 从入门到精通 建立数据库测试计划实战lt;MySQL数据库gt;(详解教程)...
  9. 苹果6换屏多钱_手机换屏维修的猫腻,附换屏须知
  10. 鸿蒙应用开发在线体验官网,华为鸿蒙 HarmonyOS 应用开发在线体验网站上线