⭐ 欢迎来到虫虫下载站! | 📦 资源下载 📁 资源专辑 ℹ️ 关于我们
⭐ 虫虫下载站

📄 delphi消息机制.txt

📁 Delphi的消息机制
💻 TXT
📖 第 1 页 / 共 4 页
字号:
 
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 直接返回

这段代码的意思是:如果当前组件正处于拖放状态,则丢弃所有键盘消息。

再看一段:
    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;

⌨️ 快捷键说明

复制代码 Ctrl + C
搜索代码 Ctrl + F
全屏模式 F11
切换主题 Ctrl + Shift + D
显示快捷键 ?
增大字号 Ctrl + =
减小字号 Ctrl + -