[转]理解消息处理过程

来源:互联网 发布:分词 python 编辑:程序博客网 时间:2024/05/17 07:53
作者:cat

这是我写的一个简单的测试,我将使用到的窗口句柄、消息号和函数调用堆栈复制如下:

//按钮响应事件的实现如下:
procedure TForm1.Button1Click(Sender: TObject);
begin
  ShowMessage('a');  //在这里设置断点,得到的调用堆栈在后面列出
end;

//调用堆栈
TForm1.Button1Click($D637BC)
TControl.Click
TButton.Click
TButton.CNCommand((48401, 1638, 0, 2950758, 0))
TControl.WndProc((48401, 1638, 2950758, 0, 1638, 0, 1638, 45, 0, 0))
TWinControl.WndProc((48401, 1638, 2950758, 0, 1638, 0, 1638, 45, 0, 0))
TButtonControl.WndProc((48401, 1638, 2950758, 0, 1638, 0, 1638, 45, 0, 0))
TControl.Perform(48401,1638,2950758)
DoControlMsg(2950758,(no value))
TWinControl.WMCommand((273, 1638, 0, 2950758, 0))
TCustomForm.WMCommand((273, 1638, 0, 2950758, 0))
TControl.WndProc((273, 1638, 2950758, 0, 1638, 0, 1638, 45, 0, 0))
TWinControl.WndProc((273, 1638, 2950758, 0, 1638, 0, 1638, 45, 0, 0))
TCustomForm.WndProc((273, 1638, 2950758, 0, 1638, 0, 1638, 45, 0, 0))
TWinControl.MainWndProc((273, 1638, 2950758, 0, 1638, 0, 1638, 45, 0, 0))
StdWndProc(2622946,273,1638,2950758)
TWinControl.DefaultHandler((no value))
TControl.WMLButtonUp((514, 0, 41, 12, (41, 12), 0))
TControl.WndProc((514, 0, 786473, 0, 0, 0, 41, 12, 0, 0))
TWinControl.WndProc((514, 0, 786473, 0, 0, 0, 41, 12, 0, 0))
TButtonControl.WndProc((514, 0, 786473, 0, 0, 0, 41, 12, 0, 0))
TWinControl.MainWndProc((514, 0, 786473, 0, 0, 0, 41, 12, 0, 0))
StdWndProc(2950758,514,0,786473)
TApplication.ProcessMessage((2950758, 514, 0, 786473, 21866171, (348, 206)))
TApplication.HandleMessage
TApplication.Run
Project1

//涉及到的消息
514 : WM_LBUTTONUP
273 : WM_COMMAND
4840: CN_COMMAND = CN_BASE + WM_COMMAND

//涉及到的窗口的句柄
ApplicationHandle:3801610
MainFormHandle   :2622946
CurrFormHandle   :2622946
Button1Handle    :2950758

//简单对上面的堆栈解释一下
应用程序如果没有消息,会停在TApplication类的Idle过程中(这个过程有一个WaitMessage)
(注意:在下面的描述中,为了叙述方便,所有被提到的过程和函数 都统一被称为函数)
当应用程序的消息队列有消息的时候,会再次执行TApplication的Run中的那个消息循环,
调用TApplication的HandleMessage函数,这个函数中,会调用TApplication的ProcessMessage,
在ProcessMessage中,通过PeekMessage取出消息,然后进行相关的处理。
现在,再来说说按钮(Button1)的Click涉及的的消息:(在阅读下面的内容时,请参照我前面给的调用堆栈)
第一个消息 WM_LBUTTONUP :在ProcessMessage中,取得这个消息的相关信息,我们从堆栈中可以看到,
这个消息对应的窗口句柄是Button1的Handle,取得这个消息之后,
消息被DispatchMessage函数分配出去,这个函数实际上就是去调用了消息句柄中
指定窗口(这里是Button1)在创建的时候所给定的窗口过程。因此进入到StdWndProc中,
在这里,经过堆栈的变换后,调用了事先准备好的类(Button1)中的窗口过程MainWndProc,
然后进入WindowProc函数,因为TButton类重载了WindowProc,因此,首先进入的是TButton
的WindowProc,然后层层向上,最后进入到TControl的WindowProc,因为从
TButton到TControl的WindowProc中都没有对WM_LBUTTONUP消息进行处理,因此最终调用了
TControl的Dispatch,因为子类没有重载过Dispatch函数,因此最终执行的是TObject的
Dispatch,在这里,消息被映射到了消息处理函数(也就是通过Message关键字定义的对应函数)
因为TButton类的继承架构中,只有TControl定义了对WM_LBUTTONUP消息的响应函数,因此,
执行流程转入到了TControl的WMLButtonUp函数中,在这个函数的第一句有一个inherited,
这一句表示要先去执行一下DefaultHandler虚函数,而这个虚函数在TButton的继承体系中,
TWinControl重载了这个虚方法,因此执行被转移到了TWinControl的DefaultHandler中,
注意,到这里为止,消息依然是 WM_LBUTTONUP 消息,TWinControl的DefaultHandler函数
经过一系列的处理,会执行其中的:
  Result := CallWindowProc(FDefWndProc, FHandle, Msg, WParam, LParam)
语句,之所以要把这句完整地写出来,是因为这里面的FDefWndProc是一个函数指针,
他指向Windows默认的窗口过程,通过这句,程序的执行流程被转入了Windows的
user32.dll中,去执行DefWindowProcA API,在这里怎么处理的 WM_LBUTTONUP 消息我不清楚
(因为我没有 user32.dll 的源代码,呵呵),但是 通过观察调用堆栈,有一点是肯定的,
这个 WM_LBUTTONUP 消息在那里被转换为 WM_COMMAND 消息,并用调用了Button1的
父窗体的窗口过程,这一点从调用堆栈的下面两个调用中可以看出来
  StdWndProc(2622946,273,1638,2950758)
  TWinControl.DefaultHandler((no value))
StdWndProc中的那个窗口句柄2622946,就是Button1所在的那个窗口的窗口句柄。
(这里需要说明的是,这个父窗口不一定就是一个Form,如果你把Button放在了Panel1上,
那么这个父窗口就是Panel1而不是Form,这也是为什么有时候在Form中接收不到
WM_COMMAND消息的原因)
StdWndProc(2622946,273,1638,2950758)的结果,也导致了Button1所在的那个窗口的
窗口过程TWinControl.MainWndProc被调用(注意,在这里面,窗口的句柄已经被隐去了,
因为已经进入到StdWndProc函数中指定的那个句柄2622946所对应的类的窗口过程当中了,
因此在StdWndProc中的那个句柄2622946已经没有意义了)。需要注意的是,这时候的消息,
已经不是刚刚的那个 WM_LBUTTONUP 消息了,而是 WM_COMMAND 消息了。
TWinControl.MainWndProc在中又调用了WindowProc虚方法,进入到TCustomForm.WndProc中,
然后在TCustomForm.WndProc中又通过inherited WndProc(Message);进入到了
TWinControl.WndProc,然后又通过TWinControl.WndProc的inherited WndProc(Message)
进入到了TControl.WndProc中,然后,这里面有调用了
Dispatch(Message)方法,消息 WM_COMMAND 被分派给了WM_COMMAND的响应函数中,
也就是TCustomForm.WMCommand函数,然后在通过这个函数中的inherited;
执行流程到了TWinControl.WMCommand中,在这里,调用了DoControlMsg函数,
  这个函数根据给定的句柄调用指定句柄对应的窗口类的Perform函数,
TWinControl.WMCommand中调用DoControlMsg函数的时候,所给的窗口句柄就是Button1的句柄,
这个句柄在 WM_COMMAND 消息中携带(参见TWMCommand结构)。
在DoControlMsg中,通过FindControl函数,根据句柄找到了具体的窗体控件
(在这里,就是Button1)。然后,通过如下语句产生了一个CN类型的消息
  Result := Control.Perform(Msg + CN_BASE, WParam, LParam)
(CN_COMMAND=CN_BASE + WM_COMMAND,因此,这里产生的是CN_COMMAND消息)
执行流程进入了TControl.Perform函数中,注意,这时候已经在TButton类中了。
在TControl.Performz中,调用了WindowProc(Message)虚方法,进入到了
TButtonControl.WndProc中,然后是TWinControl.WndProc和TControl.WndProc中,
在TControl.WndProc中,通过inherited WndProc(Message),将CN_COMMAND消息
映射到了消息处理函数中,即TButton.CNCommand,然后通过Click方法进入到
TButton.Click中,然后是TControl.Click,在这里,因为我们在窗体中指定了
OnClick事件,所以FOnClick不为空,并指向了我们的响应事件函数
TForm1.Button1Click。至此,按钮的单击事件响应到了我们定义的函数中。


消息的响应,确切说,是和线程有关系,系统为每个使用了窗口的线程创建了一套消息队列,
用来响应消息,当操作系统投递一个消息的时候,首先根据消息中包含的窗口句柄找到
窗口对应的线程,然后把这个消息投递到这个线程的消息队列中,然后操作系统再在
适当的时候唤醒这个等待处理消息的线程,操作系统的工作基本就结束了。而被唤醒的线程,
大多数都会实现一个消息循环:检索出当前消息,处理消息,分派消息。

至于消息如何分派,那时每个线程的事情,而Windows呢,也提供了一个默认的窗口过程,
用来实现根据窗口的句柄找到该句柄对应的窗口过程,然后调用一下这个窗口过程。

这是一般的消息处理的过程。
消息队列只和线程有关系,至于这个线程中包含多少个窗体,以及如何分派得到的消息,
那时线程自己需要做的事情,和Windows关系不大,和消息队列关系也不大,呵呵。
2007-1-9 17:07 cat
上面有一个地方需要更正:就是在
在TControl.WndProc中,通过inherited WndProc(Message),将CN_COMMAND消息
映射到了消息处理函数中
应该是:
在TControl.WndProc中,通过Dispatch(Message),将CN_COMMAND消息
映射到了消息处理函数中


2007-1-10 08:32 cat
在上面的论述中,有两个问题:
1、鼠标点击按钮,为什么上面只有 WM_LBUTTONUP 消息,WM_LBUTTONDOWN 消息呢?
2、既然消息需要经过消息循环才能被取出并响应,那什么上面提到的 WM_COMMAND 消息
没有遵循这个规则,而直接就触发了对应的窗口过程呢?

解释第一个疑问其实很简单,因为我们的断点设置在 OnClick 事件中, Click 事件是因为
CN_COMMAND 消息而被调用的,而 CN_COMMAND 消息是由 WM_COMMAND 消息产生的,
从上面的分析看,WM_COMMAND 消息是因为 WM_LBUTTONUP 而产生的,这就好比一个消息链,
一个接着一个。而 WM_LBUTTONDOWN 消息在 WM_LBUTTONUP 消息之前就已经响应完毕了,
响应完 WM_LBUTTONDOWN 消息之后,又回到了消息循环中,在消息循环中,再次取出
WM_LBUTTONUP 消息进行处理,因此,我们在上面的调用堆栈中,只看到了 WM_LBUTTONUP
消息,而没有看到 WM_LBUTTONDOWN 消息(它已经在之前的消息循环中响应完毕了)、

对于第二个问题,解释起来稍微麻烦些,我们知道,投递消息的函数常用的有两种:
PostMessage 和 SendMessage,PostMessage 函数投递了消息之后马上就返回,而不关心
消息是否被响应,而 SendMessage 函数产生的消息,必须要在消息响应完毕之后才会返回。
而实际上,PostMessage 仅仅是将消息加入到目标线程的消息队列之后就返回了,至于消息
是否会被响应,那取决于目标线程是否有机会将消息取出,但是这个问题 PostMessage 不关心,
只要将消息加入到目标线程的消息队列,PostMessage 函数就认为完事了。
而 SendMessage 采取的方式是直接触发目标线程对应的窗口函数,并且直到窗口函数
响应结束之后才会返回,如果使用 SendMessage 函数投递消息的线程和目标线程不是
同一个线程,操作系统会将投递消息的线程挂起,直到目标线程的窗口过程处理完这个消息
之后才会唤醒投递消息的线程继续执行(这仅仅是一个简单的描述,实际情况要比这个复杂,
但是大体上是这个流程)。因此,上面提到的,在 Button1 的 WM_LBUTTONUP 消息被系统
默认的窗口过程处理之后,直接产生了一个 WM_COMMAND 消息,并且直接就被投递到了
Button1 父窗口的窗口过程上,我个人分析,Windows 内部估计使用的是 SendMessage
 
原创粉丝点击