Delphi的消息机制探索-第一部分:流程概述

dreammonster 2011-08-28 01:43:10
加精
第一部分:流程概述

这是一个很大的题目,本人才疏学浅难免有纰漏之处,还请大家多多见谅!

虽然这篇文章是讨论Delphi的消息机制,但是如果不知道Windows的程序是如何运转的,就根本无从下手,所以先费点口沫,让我们回顾一下Windows的消息机制。

让我们先从一张图开始:


这张图大概说明了Window的消息处理流程,首先,桌面上或许有很多窗口,但是对于操作系统来说,如果某个窗口被鼠标点击了一下,系统可以分辨出是那一个窗口被点击,根据该窗口的句柄(每个窗口的唯一标识符)就可以找到这个窗口是由那一个程序创建的。

在windows系统中,时刻都会产生许多事件,因此,为了让应用程序处理各种事件,必须创建一种可以让应用程序有效处理事件的机制,因此为每个程序维护一个独立的消息列表是操作系统责无旁贷的义务,系统会把所有关于该程序的事件全部发送到这个队列当中,而用户程序在一个循环中不停的去这个队列里取消息并进行处理,这样就基本构成了一个消息的循环体系。

但是,这里需要注意的是,第一,该消息队列是由操作系统维护的,用户程序不用太担心怎么样去维护它,第二,系统并不是把消息投递到队列就完事了,它必须要知道用户程序是怎么处理处理该消息的,这样它才能维护该程序的消息队列。

基于这样的原因,应用程序并不是直接处理从消息队列取得的消息,(注意打叉的箭头),而是请求windows调用自己设定好的处理程序。看上去有些奇怪,为什么要绕这个弯呢?其实除了上面说的原因,还有其它的理由,因为系统并不是把所有的消息都投递到消息队列中去的,有些需要及时处理的消息(比如设置光标)等都被直接投递到消息处理程序,还有,如果程序里使用了Sendmessage函数,这个函数也会把消息直接投递到(非跨进程)消息处理程序中去。如下图:
[img= ][/img]

所以,当我们向系统申请创建一个窗口的时候,在我们填的申请表格里就要填上该消息处理模块的地址,系统将根据创建好的窗口的句柄和我们申请时填写的消息处理模块的地址进行调用,这样被操作系统调用的我们程序里的消息处理模块就被称为回调函数。

让我们进距离观察一下,下面就是windows的回调函数原型:

LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
无论是用什么语言编程,只要你递交给系统的回调函数和这个模型兼容,系统就能准确的把消息投递过来,在机器眼里,无论是什么函数,它都看成是一个32位的地址指针,假如我们自己的的回调函数如下:

function MyProc(Window: HWnd; AMessage: UINT; WParam : WPARAM;

LParam: LPARAM): LRESULT; stdcall; export;

Begin

Somecode..

End;

当我们把它提交给系统的时候,它就是一个普通的地址指针,将来系统需要投递消息的时候就会用一条call指令直接跳转到这个地址,但是,在跳到这个地址之前,系统会按照回调函数的约定把参数按照顺序压入堆栈,比如按照stdcall从右至左的约定,下面是模拟的汇编指令:

Push LPARAM

Push WPARAM

Push AMessage

Push HWnd

Call MyProc

当程序转到Myproc中执行的时候,按照约定它就能知道上面的参数位于堆栈的什么位置,从而正确的进行处理。

因此,回调函数就是一种约定,我按什么顺序要投递什么消息,你按什么顺序来取,只要双方都遵守这个约定就可以了。

如此,事情看上去并不是很复杂,申请窗口->提交回调函数地址->创建窗口->返回窗口句柄,我们按照这个顺序就能得到了一个窗口,并且当有消息的时候系统会自动把消息投递过来,我们只要在回调函数中写上怎么处理消息的程序就可以了。

我们看一下另一种情形:

如图,假如窗口window_b中有一个button,当用户在这个button上点击之后会出现什么情况呢?




由于button也是一个窗口(child类别),因此它也有一个有效的窗口句柄,但是这类控件有自己内部的回调函数,这是由系统提供的,当按钮被点击之后,系统将把消息提交给按钮的内部回调函数,内部的回调函数负责重新绘制按钮(比如按下去和弹上来的样子),这样就不必让我们操心按钮的绘制问题了,另外,系统还会给“拥有”这个按钮的窗口(父窗口)发送一条wm_command消息,这条消息的内容包括该按钮的ID、消息码和该按钮的句柄,父窗口可以在自己回调函数中处理该类事件,如

Case Message of : WM_COMMAND

Begin

//处理

End;

可以想象,如果一个程序有数个窗口,每个窗口又有数个控件,那么该程序会产生许多消息,而要是没有什么好的机制,我们只能在窗口的回调函数中用case来判断并进行处理。

无疑,对于复杂的程序来说,这是一件极其枯燥和效率低下的办法,



现在让我们换一种思路来考虑一下,如果我们采用面向对象的思路,我们可以解决几个重要的问题:

1、代码重用:如果每个窗口(宝库控件)都是一个对象,那么所有从同一类继承下来的对象都拥有一致消息处理方式,而不是为每个窗口都去写一个回调函数。

如下图,窗口现在以对象的方式进行管理,那么只要在该窗口类写好了回调函数,所有从该类派生的窗口对象都有了一致的处理方式,我们只需要在构造该窗口的时候把该对象的回调函数传给系统就可以了,这样系统会把消息传递给每一个对象的回调函数去处理。




2、灵活性:由于OOP强大的重载机制,我们可以轻易的改写各个对象的回调函数,并且调用inherited继承既有的处理方式。

3、封装性:纯手工的创建和维护窗口是一件很繁琐且容易出错的工作,如果在类一层上对许多繁琐的细节进行封装,那么在使用上会大大便利并且不容易出错误。

4、消息细节化:windows会产生很多消息,可以利用面向对象的方式把它们分门别类,然后在类的不同层次进行单独处理,下图模拟一个鼠标点击事件说明这个流程。




1)、用户鼠标点击窗口被系统捕获。2)、系统将这个消息发送到该程序的消息队列。3)、该程序的消息循环取出该消息。4)消息循环请求系统处理该消息。5)系统则根据该窗口注册的回调函数地址把该事件和相关参数送到该地址,这个地址被delphi做了处理,它们先跳到一个特殊的程序,这个程序要做一些必要的处理。6)、然后该消息被送到注册了该窗口的对象的类方法MainWndProc中。7)、该对象方法把消息转发到类的最底层的WndProc中去处理。8)、如果该层的WndProc不能处理这个消息它会inherited上层的WndProc继续处理。9、10)、如果上层的各WndProc也不能处理的话,该消息最后被Tobject的Dispatch进行分发。11、12)、Dispatch直接根据该类的VMT表找到该类的动态方法表。13、从动态方法表中查找该事件的处理方法。如果在该类中找不到就到该类的父类去找,最终在Tcontrol类的动态方法表中找到了实现WMLButtonDown动态方法,然后交由该方法进行处理。

由此可见,只要我们在类的任意一层指定了相应消息的处理方法,我们就可以方便的把消息分门别类进行细分处理。

尤其方便的一点是:系统传递给回调函数的消息并没有明确的划分类型,除了窗口句柄和消息ID之外,剩下的两个参数Wparam和Lparam需要根据消息ID手动进行分解,但是采用上面的方法,我们可以直接按照消息的含义把消息映射成相应的类型,如上面的TWMLButtonDown就是TWMMouse类型的结构体,消息到了这里被自动拆分成相应的信息。



TWMMouse = packed record

Msg: Cardinal;

Keys: Longint;

case Integer of

0: (

XPos: Smallint;

YPos: Smallint);

1: (

Pos: TSmallPoint;

Result: Longint);

end;

5、和用户事件模型对接:有了上面一系列的处理,最终我们可以很方便的和用户处理程序进行对接了,下图说明了对接原理。



1、首先声明一个该事件的处理方法的方法类型(TmouseEvent)。2、然后在该类声明一个该原型的方法指针变量(FonMousDown)并发布。3、用户写一个与该方法类型兼容的方法(FormMouseDown)。4、然后让FonMouseDown指向自己(FonMouseDown= FormMouseDown)。(其实这和上面我们说的windows的回调函数是一个道理)。5)最后在该消息的动态处理方法中使用 if Assigned(FOnMouseDown) then FOnMouseDown(Self, Button, Shift, X, Y);就调用了用户的处理程序。



控件消息的处理流程如下:

1)、用户鼠标点击窗口中的按钮被系统捕获,系统将自动绘制按钮2)、系统发送一个WM_COMMAND消息发送到该程序的消息队列,该消息的目的地是拥有该按钮的窗口(这里是主窗口对象)的回调函数地址,该消息中包含了该按钮的hwnd。3)、该程序的消息循环取出该消息。4)消息循环请求系统处理该消息。5)系统则根据该窗口注册的回调函数地址把该事件和相关参数送到该地址。6)、然后该消息被送到注册了该窗口的对象的类方法MainWndProc中。7)、该对象方法把消息转发到类的最底层的WndProc中去处理。8)、如果该层的WndProc不能处理这个消息它会inherited上层的WndProc继续处理。9、10)、如果上层的各WndProc也不能处理的话,该消息最后被Tobject的Dispatch进行分发。11、12)、从动态方法表中查找该事件的处理方法。如果在该类中找不到就到该类的父类去找。13)、这个方法是是由TcustomForm声明的WMSysCommand动态方法。14)、它如果不能处理就交给上层。15)、上层是TwinControl的WMSysCommand动态方法,它会做下面的工作。16)、由于这个消息里包含了该按钮的hwnd,它先通过这个句柄找到该按钮对象的self指针。17)、然后把这个消息转成Delphi自定义的控件消息CN_COMMAND。18)、调用该按钮对象的Perform方法将其转到该对象的MainWndPro回调函数。19)、该回调函数重复8-13步骤,最后在动态方法表中找到了处理方法。(如图)



正是采用了这样的流程,让我们才觉得window编程方便了许多,我想也正因为如此Delphi才受到这么多人的喜爱吧。

Delphi在背后做了许多繁重和有技巧的工作,大大的减轻了我们编写Windows程序的负担,同时它又没有隐瞒任何细节,如果愿意的话,任何使用Delphi的人都可以深入去了解这一切都是怎么实现的。我想这就是一种自信的魄力吧,同时也给了使用Delphi的人一个很好的学习机会,当你渐渐深入的时候,不觉会感叹Delphi的美丽和优雅,我时常想,创作Delphi的团体肯定充满了艺术气质,就如同Delphi这个名字一样,在它诞生那一刻,就注定了要光芒四射!
...全文
1139 40 打赏 收藏 转发到动态 举报
写回复
用AI写文章
40 条回复
切换为时间正序
请发表友善的回复…
发表回复
rainychan2009 2011-08-31
  • 打赏
  • 举报
回复
分享是好事
hui717 2011-08-30
  • 打赏
  • 举报
回复
写的不错,加油
lineuser 2011-08-30
  • 打赏
  • 举报
回复
mark and study
xmx2009 2011-08-30
  • 打赏
  • 举报
回复
将得很好呀,真是学习了,没想到那么深奥
woaifaye1988 2011-08-29
  • 打赏
  • 举报
回复
jiayoujiao
  • 打赏
  • 举报
回复
最近正在学delphi,谢谢分享!
  • 打赏
  • 举报
回复
有心了,不过这东西几乎每本书上都有
周药师 2011-08-29
  • 打赏
  • 举报
回复
感谢分享
linghengmao 2011-08-29
  • 打赏
  • 举报
回复
先頂再看!
普通网民 2011-08-29
  • 打赏
  • 举报
回复
乱七八糟。建议看看李维的那本Delphi。
xiaoxiangqing 2011-08-29
  • 打赏
  • 举报
回复
说得不错。
aydf1 2011-08-29
  • 打赏
  • 举报
回复
学习了
蓝色光芒 2011-08-29
  • 打赏
  • 举报
回复
一剑飘雪 2011-08-29
  • 打赏
  • 举报
回复
感谢分享
董小尾 2011-08-29
  • 打赏
  • 举报
回复
Delphi版 需要火起来
syh0816 2011-08-29
  • 打赏
  • 举报
回复
谢谢分享~~
xiaocongzhi 2011-08-29
  • 打赏
  • 举报
回复
李维那本Inside VCL 讲的比较详细!
babydog01 2011-08-29
  • 打赏
  • 举报
回复
[Quote=引用 17 楼 abung 的回复:]
领导开课,拉个小板凳,座听。。。
[/Quote]
aBung 2011-08-29
  • 打赏
  • 举报
回复
领导开课,拉个小板凳,座听。。。
zhan7505201 2011-08-29
  • 打赏
  • 举报
回复
学习。
加载更多回复(14)

16,748

社区成员

发帖
与我相关
我的任务
社区描述
Delphi 语言基础/算法/系统设计
社区管理员
  • 语言基础/算法/系统设计社区
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧