Windows 消息机制浅析

1. Windows 的历史

中国人喜欢以史为鉴,而事实也确实是,如果你能知道一件事情的来龙去脉,往往可以更容易地理解事物为什么会表现为当前这样的现状。所以,我的介绍性开场白通常会以一段历史开始。不过,我不会以精确到年月日的那种方式详细讲述,而是选取几个对我们的编程生涯有重要影响的关键点。

Windows 是真正的图形化界面操作系统的普及者,无论任何人,争夺什么第一个实现的GUI、第一个商业化的GUI之类的虚名,都替代不了 Windows 的历史功绩,让最普通的用户能够容易地操纵PC。

第一个声名大噪的版本是 Windows 3.0(也有人认为应该是它的更加健康强壮的弟弟 Windows 3.1),从那个时候开始,我们就和本文中以下的几个关键角色有了不尽的情缘:

上面代码中的这三个相关函数,会在后文中提到。

第二个大红大紫的版本则非 Windows 95 莫属。这个版本的主要变化在于,无论如何,它是一个大众化的所谓 32 位系统了。之所以要加上“所谓的”三个字,是因为这个系统是个混血儿,在 3 位代码中混杂有大量的从之前的 Windows3.x 上移植过来的 16 位代码。

此时间稍后,另一支潜力股的关键进化过程结束,Windows NT 4.0 隆重登场,这个分支的操作系统是全 32 位的,成为了 Windows 95 系列的掘墓者,也是我们现在所使用几乎所有的 Windows 桌面系统(Windows2000/XP/2003/Vista/2008)的前辈。但是,这个版本由于对系统硬件的要求甚高(在当时),所以没有引起普通用户的广泛关注。

下一个里程碑就是 Windows 2000 了,微软实现了 Windows9x/Me 分支和 Windows NT 分支的合并。紧接着,Windows XP 现身。从有关消息方面来考察,Windows2000 做了微小的改进,在此之前,我们在很多情况下需要创建一个正常的、隐藏的、完整的窗口来处理消息,而 Windows 2000 引入了一种特殊类型的窗口用于此类需求。道理上来讲,应该会减少一些资源占用。

此后经过五六年的时间,Windows Vista 诞生。事实上,从 Windows2000 开始,Windows 家族的编程模型,尤其是对原生态代码(native code)而言,已经基本没有太大的变化。通常只是增加了新的API或者用户控件,或者现有控件增加了新的功能或者风格。尽管 Windows Vista 中有很多的变化,但是对于我们今天要讲到的主题,影响不大。最主要的一个影响,是消息的发送方和接收方之间有了等级限制,不像之前可以随意互相进行消息传递,这是出于安全性的考虑。

2. Windows 的宏观构造

从最原始的版本开始,有三个比较大的功能块占据了 Windows 系统的绝大部分,这三个块,就是赫赫有名的 Kernel、GDI、User。从 Windows 95 起,另两个在先前不太起眼的部分也迅速崛起,那就是大名鼎鼎的 Registry 和 Shell。

这几个大块的分工是这样的:Kernel,望文生义,负责内核部分,这是任何一个可以称之为操作系统的东西的基石,主要职责有:内存管理、任务调度、外设管理等;GDI,则是对可以进行图形化操纵的设备的操作接口,对外提供的主要功能是在设备上:提供坐标系统,绘制点、线、形状,进行填充,文本绘制,管理画笔、画刷、字体等绘图对象;User,则是前两者的粘合剂,使系统能够通过图形化操作方式和使用者(也就是 User)进行交互,把零散的 GDI 对象有机地组织起来,抽象为窗口,用以接受用户的输入,进行相应的运算(广义上的,并不是局限于算数运算),并最终将结果呈现给用户。当然,User 部分通常是指可以实现上述的功能的基础构造,真正的实现部分需要大量的额外工作,这也是 Shell 部分的主要工作。而 Registry,则是提供给用户一种与物理存储无关的统一的数据访问方式。

很容易就可以看出,消息功能,这种被我们一直以窗口间通讯最为自然的方式所使用的机制,应该隶属于 User 部分。

对于 Windows Mobile 系统来说,底层的实现上与桌面系统大相径庭,例如,它本身并没有 kernel32.dll、gdi32.dll、user32.dll 这几个众所周知的系统库,而是有一个多合一的 coredll.dll,而且内核被实现为一个更接近于正常进程的 nk.exe 进程,而不是桌面系统下的那个抽象的执行体。尽管如此,但是在逻辑上,我们依然可以将之与桌面系统同等看待。

3. Windows 的消息概念

在我们的通常认识上,消息事实就是一个数值。我们检查一下消息相关的各个回调函数的原型就会发现,表示消息的那个参数的数据类型是 UINT,也就是无符号的整数类型。不过,我们通常也会发现,消息往往还附带有两个其他类型的数据,一个是 WPARAM 类型的,一个是 LPARAM 类型的,如果算上消息的目标窗口的句柄,那么,一个消息以及相关信息才能够说是比较完整。为什么说是比较呢?看一下 MSG 这个结构的定义就会发现,其实还有另外两个我们不太经常使用的数据,是与一条消息有关系的。 MSG 的完整声明如下:

前四项正是我们已经提及过的,而后两项,一个表示消息发生时的时间,一个表示此消息发生时的按屏幕坐标表示的鼠标光标的位置。

从这个结构也可以看出,我们经常所说的消息,更多是指代表了一个确定的消息的数值。

我们可能还会听到有这样的称呼:命令消息、通知消息、反射消息等等。首先需要声明的一点是,这并不是对 Windows 系统中的消息的科学分类,而是在某些特定场景下的通俗称谓。命令消息,一般特指 WM_COMMAND 消息,此消息通常由控件或者菜单发出,表示用户执行/发出了一个命令。通知消息,一般特指 WM_NOTIFY 消息,此消息通常由公用控件(Common Controls)发出,表示一些事件发生了,需要处理。反射消息,一般用于对 Windows API 的封装类或者类库中。这是一类消息的总称,它们的处理需要经过一种被称为“反射”的机制。这一机制的具体方式下一节中会有描述。

Windows 的消息分类不好分(如果非要划分的话,可以分为系统定义的消息和应用程序定义的消息),不过有一个区段划分。从 0x00000x03FF,为系统定义的消息,常见的 WM_PAINTWM_CREATE 等均在其中;从 0x04000x7FFF,专用于用户自定义的消息,可以使用 WM_USER + x 的形式自行定义,其中 WM_USER 的值就是 0x0400,x 取一个整数;从 0x80000xBFFF,从 Windows 95 开始,也用作用户自定义的消息范围,可以使用 WM_APP + x 的形式自行定义。根据微软的建议, WM_APP 类消息用于程序之间的消息通信,而 WM_USER 类消息则最好用于某个特定的窗口类。微软自己遵循这一惯例,所以,公用控件的消息,如 TVM_DELETEITEM,基本都是 WM_USER 类属。从 0xC000 开始,到 0xFFFF,这个区段的消息值保留给 RegisterWindowMessage 这个 API,此 API 可以接受一个字符串,把它变换成一个唯一的消息值。在桌面系统上,最常见的源字符串,可能就是“TaskbarCreated”了,由它对应的消息会发送到所有的顶级窗口,通知任务栏刚刚被创建(可能是由于资源管理崩溃后重新启动导致的)。

由上也可以看出,Windows 的消息值是一个 16 位的数字,这是 16 系统时代留给我们的痕迹。另外的一个痕迹是 WPARAMLPARAM 这两个数据类型,在 16 位时代, WPARAM 是 16 位的,其名字的意思是 word parameter, LPARAM 是 32 位的,其名字的意思是 long parameter。

4. Windows 的消息机制

4.1. 消息队列

说到消息机制,可能连最初级的 Windows 程序员都会对消息队列(Message Queue)这个名词耳熟(不过不见得能详)。对于这样一个基本概念,Windows 操作系统提供的针对消息队列的 API 却少的可怜( GetQueueStatusGetInputStateGetMessageExtraInfoSetMessageExtraInfo),而且,这些 API 的出镜率也相当的低,甚至有不少经验丰富的程序员也从来没有使用过它们。在 Windows Mobile 上,这些 API 干脆付诸阙如,不过有一个同样极少使用的 GetMessageQueueReadyTimeStamp 函数在充门面。

这一切,都归功于在 API 层极好的封装性,减少了开始接触这个平台时需要了解的概念。但是,对于我们这样既想知其然,又想知其所以然的群体,还是有必要对消息队列有充分的了解。

4.1.1. 系统消息队列

这是一个系统唯一的队列,输入设备(键盘、鼠标或者其他)的驱动程序会把用户的操作输入转化成消息放置于系统队列中,然后系统会把此消息转到目标窗口所在线程的消息队列中等待处理。

4.1.2. 线程消息队列(应用程序消息队列)

应用程序消息队列这个名称是历史遗留,在 32 位(以及之后的 64 位)系统中,正确的名称应该是线程消息队列。每一个 GUI 线程都会维护这样一个线程消息队列。(这个队列只有在线程调用 User 或者 GDI 函数时才会创建,默认并不创建)。然后线程消息队列中的消息会被本线程的消息循环(有时也被称为消息泵)派送到相应的窗口过程(也叫窗口回调函数)处理。

4.2. 消息的生命期

4.2.1. 消息的产生

消息产生的源头有两个,一个是系统,一个是应用程序。系统产生的消息又可以大致分为两类,一类是由输入设备导致的,例如 WM_MOUSEMOVE,一类是 User 部分(或者是系统内的其他部分通过 User 部分)为了实现自身的正常行为或者管理功能而主动生成的,如 WM_WINDOWPOSCHANGED

产生的方式也有两种,一种称为发送(Send),另一种称为投递(Post,也有译作张贴的),对应于大家极为熟悉的两个 API, SendMessagePostMessage。系统产生的消息,虽然我们看不到代码,不过我们还是可以粗略地划拨一下,基本上所有的输入类消息,都是以投递的方式抵达应用的,而其他的消息,则大部分是采取了发送方式。

至于应用程序,可以随意选用适合自己的消息产生方式。

4.2.2. 消息的处理

在绝大部分情况下,消息总是有一个目标窗口的,因此,消息也绝大部分是被某个窗口所处理的。处理消息的地方,就是这个窗口的回调函数。

窗口的回调函数,之所以被称作“回调”,就是因为这个函数一般并不是由用户(程序员)主动调用它的,而是系统认为在恰当的时候对它进行调用。那么,这个“恰当的时候”是什么时候呢?根据消息产生的方式,“恰当的时候”也有两个时机。

第一个时机是, DispatchMessage 函数被调用时,另一个时机是 SendMessage 函数被调用时。

我们正常情况下以系统处理对一个顶级窗口的关闭按钮的鼠标左键点击事件为例来说明。

这个点击事件完成的标志性消息是 WM_NCLBUTTONUP,表示在一个窗口的非客户区的鼠标左键释放动作,另外,这个鼠标消息的其他数据中会表明,发生这个动作的位置是在关闭按钮上( HTCLOSE)。这是一个鼠标输入事件,从前文可以知道,它会被系统投递到消息队列中。

于是,在消息循环中 GetMessage 的某次执行结束后,这个消息被取到了 MSG 结构里。从文章开头的消息循环代码可知,这个消息接下来会被 TranslateMessage 函数做必要的(事实上是“可能的”)翻译,然后交给 DispatchMessage 来全权处理。

DispatchMessage 拿到了 MSG 结构,开始自己的一套办事流程。

首先,检查消息指定的目标窗口句柄。看系统内(实际上是本线程内)是不是确实存在这样一个窗口,如果没有,那说明这个消息已经不会有需要对它负责的人选了,那么这个消息就会被丢弃。

如果有,它就会直接调用目标窗口的回调函数。终于看到,我们写的回调函数出场了,这就是“恰当的时机”之一。当然,为了叙述清晰,此处省略了系统做的一些其他处理。

这样,对于系统来说,一条投递消息就处理完成,转而继续 GetMessage

不过对于我们上面的例子,事情还没有完。

我们都清楚,对于 WM_NCLBUTTONUP 这样一条消息,通常我们是无暇去做额外处理的(正事还忙不过来呢……)。所以,我们一般都会把它扔到那个著名的垃圾堆里,没错, DefWindowProc。尽管如此,我们还是可以看出, DefWindowProc 其实已经成了我们的回调函数的一个组成部分,唯一的差别在,这个函数不是我们自己写的而已。

DefWindowProc 对这个消息的处理也是相当轻松,它基本上没有做什么实质性的事情,而是生成了另外一个消息, WM_SYSCOMMAND,同时在 wParam 里指定为 SC_CLOSE。这一次,消息没有被投递到消息队列里,而是直接 Send 出来的。

于是, SendMessage 的艰难历程开始。

第一步, SendMessage 的方向和 DispatchMessage 几乎一模一样,检查句柄。

第二步,事情就来了,它需要检查目标窗口和自己在不在一个线程内。如果在,那就比较好办,按照 DispatchMessage 趟出来的老路走:调用目标窗口的回调函数。这,就是“恰当的时机”之二。

可是要是不在一个线程内,那就麻烦了。道理很简单,别的线程有自己的运行轨迹,没有办法去让它立即就来处理这个消息。

现在, SendMessage 该怎么处理手里的这个烫手山芋呢?(作者注:写到此处时,很有写上“欲知后事如何,且听下回分解”的冲动)

微软的架构师做了个非常聪明的选择:不干涉其他线程的内政。我不会生拉硬拽让你来处理我的消息,我会把消息投递给你(这个投递是内部操作,从外面看,这条消息应该一直被认为是发送过去的),然后 —— 我等着。

这下,球踢到了目标线程那边。目标线程一点也不含糊,既然消息来到了我的队列里,那我的 GetMessage 会按照既定的流程走,不过,和上文 WM_NCLBUTTONUP 的经历有所不同。鉴于这条消息是外来客,而且是 Send 方式,于是它以优先于线程内部的其他消息进行处理(毕竟友邦在等着啊),处理完毕之后,把结果返回给消息的源线程。可以参见下文中对 GetMessage 函数的叙述。

在我们的现在讨论的这个例子里,由于 SendMessage(WM_SYSCOMMAND) 是属于本线程内的,所以就会递归调用回窗口的回调函数里。此后的处理,还是另外的几个消息被衍生出来,如 WM_CLOSEWM_DESTROY。这个例子仅仅出于概念性的展示,而不是完全精确可靠的,而且,在 Windows Mobile 上,干脆就没有非客户区的概念。

这就是系统内所有消息的处理方式。

不过稍等, PostThreadMessage 投递到消息队列里的消息怎么办?答案是:你自己看着办。最好的处理位置,就是在消息循环中的 TranslateMessage 调用之前。

另外一个需要稍做注解的问题是消息的返回值问题,这个问题有些微妙。对于大多数的消息,返回值都没有什么意义。对于另外的一些消息,返回值意义重大。我相信有很多人对 WM_ERASEBKGND 消息的返回值会有印象,该消息的返回值直接影响到系统是不是要进行缺省的绘制窗口背景操作。所以,处理完一条消息究竟应该返回什么,查一下文档会更稳妥一些。

这才算是功德圆满了。

4.2.3. 消息的优先级

上一节中其实已经暗示了这一点,来自于其他线程的发送的消息优先级会高一点点。

不过还需要注意,还有那么几个优先级比正常的消息低一点点的。它们是: WM_PAINTWM_TIMERWM_QUIT。只有在队列中没有其他消息的时候,这几个消息才会被处理,多个 WM_PAINT 消息还会被合并以提高效率(内幕揭示: WM_PAINT 其实也是一个标志位,所以看上去是被“合并了”)。

其他所有消息则以先进先出(FIFO)的方式被处理。

4.2.4. 没有处理的消息呢?

有人会问出这个问题的。事实上,这差不多就是一个伪命题,基本不存在没有处理的消息。从 4.2.2 节的叙述也可以看出,消息总会流到某一个处理分支里去。

那么,我本人倾向于提问者在问这样一个问题:如果窗口回调函数没有处理某个消息,那这个消息最终怎么样了?其实这还是取决于回调函数实现者的意志。如果你只是简单地返回,那事实上也是进行了处理,只不过,处理的方式是“什么都没做”而已;如果你把消息传递给 DefWindowProc,那么它会处理自己感兴趣的若干消息,对于别的消息,它也一概不管,直接返回。

4.3. 消息死锁

假设有线程 A 和 B,现在有以下步骤:

1) 线程 A SendMessage 给线程 B,A 等待消息在线程 B 中处理后返回
2) 线程 B 收到了线程 A 发来的消息,并进行处理,在处理过程中,B 也向线程 A SendMessage,然后等待从 A 返回。

此时线程 A 正等待从线程 B 返回,无法处理B发来的消息,从而导致了线程 A 和 B 相互等待,形成死锁。

以此类推,多个线程也可以形成环形死锁。

可以使用 SendNotifyMessageSendMessageTimeout 来避免出现此类死锁。

(作者注:对两个线程互相 SendMessage 曾经专门写程序进行过验证,结果却没有死锁,不知道是不是新一些的 Windows 系统作了特殊的处理。请大家自行验证。)

4.4. 模态(Modal)

这个词汇曾给我带来极大的困惑,我曾经做过不少的努力,想弄清楚为什么当初系统的构建者使用“模态”这个词汇来表达这样一种情景,但是最后失败了。我不得不接受这个词,并运用它。直到数天前,我找到了一个对模态的简要介绍,如果有兴趣,各位可以自己去看:http://www.usabilityfirst.com/glossary/main.cgi?function=display_term&term_id=320。(我曾做过的另外一个努力是想知道为什么 Handle 会被翻译为“句柄”,或者,是谁首先这样翻译的,迄今无解)。Windows 中的模态有好几个场景,比较典型的有:

显示了一个对话框
显示出一个菜单
操作滚动条
移动窗口
改变窗口大小

把我的体会归纳起来,那就是:如果进入了一个模态场景,那么,除了这个模态本身的明确目标,其余操作被一概禁止。概念上可以理解为,模态,是一种独占模式、一种强制模式,一种霸道模式。

在 Windows 里,模态的实现其实很简单,只不过就是包含了自己的消息循环而已,说穿了毫无悬念可言,但是如果不明白这个内幕的话,就会觉得很神秘。那么,根据此结论,我们就可以做一些有趣(或者有意义)的事情了,看一下以下代码,预测一下 TestModal 的执行结果:

答案见本大节末尾。

需要提醒的是,模态是用户界面里相当重要而普遍的一个概念,不仅存在于 Windows 环境下,也存在于其他的用户界面系统中,例如 Symbian。

4.5. 与消息处理有关的钩子(Hook)

很多人都或多或少地听说过或者接触过钩子。钩子在处理事务的正常流程之外,额外给予了我们一种监听或者控制的方式(注意:在 Windows Mobile 系统下,钩子并不被正式支持)。

(TODO: 细化,不过由于这个内容针对桌面系统更多,所以暂时可以略过)

4.6. 所谓的反射(Reflection)

上文也已经提到,反射通常会在对 Windows API 的封装类或者类库中出现,这是由于 Windows SDK 的 API 是以 C 的风格暴露给使用者的,与 C++ 语言的主要用类编程的风格有一些需要啮合的地方。

举例来说,一个 Button,在 SDK 中是一个已经定型的控件,基本上实现了自包容,要扩展它的功能的话(例如,绘制不同的外观),系统把接口(广义上的接口,即一种交互上的契约)制定为发给 Button 的属主(通常就是父窗口)的两条消息( WM_MEASUREITEMWM_DRAWITEM)。其道理在于,使用 Button 控件的父窗口,往往是用户自己实现的,处理起来更方便,而不需要对 Button 自身做什么手脚。

但是,这种交互方式在 C++ 的世界里是相当忌讳的。C++ 的自包容单位是对象,那么一个 Button 对象的封装类,假定是 CButton,不能自己处理自己的绘制问题,这是不太符合法则的(尽管不是不可以)。

为了消除这一不和谐音,就有人提出了反射机制。其核心就在于,对于本该子控件自己处理的事件所对应的消息(如前面的 WM_DRAWITEM),父窗口即使收到,也不进行直接处理,而是把这个消息重新发回给子控件本身。

这样带来一个问题,当 Button 收到一个 WM_DRAWITEM 消息时,弄不清楚究竟是自己的子窗口发来的(虽说往 Button 上建立子窗口不常见,但不是不可以),还是父窗口把原本是自己的消息反射回来了。所以,最后微软给出一个解决办法,就是反射消息的时候,把消息的值上加一个固定的附加值,这个值就是 OCM__BASE。尽管最初只是微软自己在这样做,这个值也完全可以各取各的,但是后来别的类/类库的编制者几乎都无一例外地和微软保持了一致。

当控件收到消息之后,先把这个附加值减掉,就可以知道是哪一条消息被反射回来了,然后再作相应的处理。

4.4 节小测试的答案:一个消息框显示大概 1 秒钟的时间,然后自动消失。有的人根据这一表现,写出了自己的超时候自动关闭的消息框。如果各位有兴趣,可以自己尝试也实现一下。(提示:需要考虑一下用户先于定时器触发就手动关闭了消息框的情况)

5. Windows 的消息本质

一个特殊的事件同步机制,使用多种常规线程间同步机制实现。

6. Windows 的消息操纵

注意:以下讨论中用浅绿色标注的函数,表示在 Windows Mobile 平台上是没有的。

在使用消息的过程中,这两个函数的使用率是最高的。初学者有时会搞不清楚这两个发送消息的函数的使用场景,容易误用。所以放在这里一起说。其实上面已经对 SendMessage 做了很多的介绍,所以在这儿的重点会放在 PostMessage 上。相较 SendMessage 而言, PostMessage 的工作要轻松许多,只要找到知道那个的窗口句柄所在的线程,把消息放到该线程的消息队列里就可以了,完全不理会这条消息最终的命运,是不是被正确处理了。

这一点,从 PostMessageSendMessage 的返回值的不同也有体现。 PostMessage 函数的返回值是 BOOL 类型,体现的是投递操作是否成功。投递操作是有可能失败的,尽管我们不愿意同时也确实很少看到。例如,目标线程的消息队列已经满(在 16 位时代出现概率较高),或者更糟糕,目标线程根本就没有消息队列。

当然, PostMessage 也要检查窗口句柄的合法性,不过和 SendMessage 不同的一点是,它允许窗口句柄是 NULL。在此情况下,对它的调用就等价于调用 PostThreadMessage 向自身所在线程投递一条消息。

从上面的描述可以很容易地看出, PostMessageSendMessage 的本质区别在于前者发出的消息是异步处理的,而后者发出的消息是同步处理的。理解这一点非常重要。

从上面的这个结果推演,还可以得到另外一个有时会很有用的推论。在本线程之内,如果你在处理某个窗口消息的时候,希望在处理之后开展另一项以此消息为前提的工作,那么可以向本窗口 Post 一条消息,来作为该后续工作的触发机制。

检查线程的消息队列,如果有消息就取出该消息到一个传入的 MSG 结构中并返回,没有消息,就等待。等待时线程处于休眠状态,CPU 被分配给系统内的其他线程使用。

需要注意的是,由其它线程 Send 过来的消息,会在这里就地处理(即调用相应的窗口回调函数),而不会返回给调用者。

这个消息的来龙去脉在上文中已经有较为详细的叙述,故此略去。

这个函数在本质上与消息机制的关系不大,绝大多数的消息循环中都出现它的身影是因为绝大多数的程序员都不知道这个函数真正是干什么的,仅仅是出于惯例或者初学时教科书上给出的范例。这个函数的作用主要和输入有关,它会把 WM_KEYDOWNWM_KEYUP 这样的消息恰当地、适时地翻译出新的消息来,如 WM_CHAR。如果你确信某个线程根本不会有用户输入方面的需求,基本上可以安全地将之从循环中移除。

可以和它相提并论的就是列出的 TranslateAccelerator 函数,这个函数会把用户输入根据指定的加速键(Accelerator)表翻译为适当的命令消息。

窥探线程的消息队列。无论队列中有没有消息,这个函数都立即返回。它的参数列表与 GetMessage 基本一致,只是多了一个标志参数。这个标志参数指定了如果队列中如果有消息的话, PeekMessage 的行为。如果该标志中含有 PM_REMOVE,则 PeekMessage 会把新消息返回到 MSG 结构中,正如 GetMessage 的行为那样。如果标志中指定了 PM_NOREMOVE,则不会取出任何消息。

这个函数的作用是等待一条消息的到来。等待期间线程处于休眠状态,一旦有新消息到来,则立即返回。

了解了 PeekMessageWaitMessage 之后,理论上,我们可以写出自己的 GetMessage 了。

这个函数很有意思,它的行为属于看人下菜碟型。如果目标线程就是自身所处线程,那么它就是 SendMessage;而一旦发现目标线程是其他线程,那它就类似于 PostMessage,不等待目标窗口处理完成。不过,仅仅是类似,因为它发出的消息仍然会被目标线程认为是 Send 过来的。

这个函数可以说是 SendMessage 函数家族(相对 PostMessage 而言)之中最强大的函数。它在标准的 SendMessage 函数的功能前提下,加入了许多额外的控制选项以及一个超时设定。例如,它可以指定,如果发现目标窗口已经失去响应的话,那么就立即返回;也可以指定如果目标窗口的响应时间超过了指定的超时时限的话也返回,而不是无限等待下去。而且我们知道, SendMessage 是会固执地等待下去的。(内幕揭示: SendMessage 其实就是对 SendMessageTimeout 的一个浅封装)

SendMessageTimeout 不同,这个函数在另外一个方向上对标准的 SendMessage 进行了扩展。它的行为与 SendNotifyMessage 类似,只不过允许在对方处理完消息之后,指定一个本线程内的后续处理函数。仔细观察可以发现, SendNotifyMessage 其实是本函数的一个特例。

对这个函数的使用场景较少,实际上,作者几乎从来没有见到必须使用它的情况。网上有一些对此函数的讨论和测试代码,但很少有实用价值。(恐怕这也是 Windows Mobile 没有实现此函数的原因之一。)

这个函数的名字具有迷惑性。事实上,它本身并不会投递任何消息,而是偷偷在系统内部置了一个标志,当调用 GetMessage 时会检测此标志位。若此标志位被置位,而且队列中已经没有别的符合条件的投递消息,则 GetMessage 返回 FALSE,用以终止消息循环。

不过,有人会有这样的疑惑。我们知道, PostMessage 当窗口句柄为 NULL 的时候,就相当于 PostThreadMessage(GetCurrentThreadId(), ),那么,为什么不用 PostMessage(NULL, WM_QUIT, 0, 0),而要引入这么一个单独的 API 呢?有的人给出的原因是,这个 API 出现在 Windows 的 16 位时代,当时还没有线程的概念。这个答案仔细推敲的话,其实似是而非,因为完全可以把进程的执行看作是一个线程。真正的原因,可能从前文能得到一些思考线索,尤其注意“队列中已经没有别的符合条件的投递消息”这个叙述。

跨线程投递消息。我们知道,消息队列是属于线程的,所以,可以不指定目标窗口而只指定目标线程就投递消息。投递到目标线程的消息通常会被 GetMessage 取出,但是,由于没有指定目标窗口,所以不会被派发到任何一个窗口回调函数中。

请注意上文中的通常二字。这是因为在一般的情况下,我们是按照 GetMessage(&msg, NULL, 0, 0) 这样的形式对 GetMessage 进行调用的,但是,第二个参数是一个窗口句柄,如果指定了一个合法的窗口句柄,那么 GetMessage 就只会取出与该窗口有关的投递消息。如果这样的调用放在线程的主消息循环中,就可能会造成消息积压(这和你在本线程中究竟创建了多少个窗口有关)。所幸的是,迄今我还没有见到过有谁这样使用 GetMessage

我们一般所接触到的消息都是发送给窗口的,其实, 消息的接收者可以是多种多样的,它可以是应用程序(application)、可安装驱动程序(installable driver)、网络驱动程序(networkdriver)、系统级设备驱动程序(system-leveldevice driver)等,用 BroadcastSystemMessage 这个 API 可以对以上系统组件发送消息。

这个函数用于在处理某条消息时,检查消息是不是来自于其他线程的发送操作。它的使用场景也极其有限,除非你确实计划限制某些消息的来源和产生方式。

这个函数在 MSDN 中的解释非常简单,只有寥寥数语,几乎到了模糊不清的地步。从示例代码段来推测,其作用大概是:消息的接收线程(目标线程)在处理过程中可以通过调用此函数使得消息的发送线程(源线程)结束等待状态继续执行。

根据微软的文档,其官方建议是:在处理每个有可能来自于其他线程的消息的时候,如果某一步骤的处理会调用到导致线程移交控制的函数(原文如此:any function that causes the thread to yield control),都应该先调用 InSendMessage 类属的函数进行判断,如果返回 TRUE,则要立即使用 ReplyMessage 答复消息的源线程。

“会导致线程移交控制的函数”,MSDN 给出的例子是 DialogBox,这使得我做出自己的推测,这样的函数,至少包括会导致进入某种模态场景的函数。

至于“有可能来自于其他线程的消息”,在 Windows 世界里的现实状况是,几乎任何一个消息都会来自于其他线程。

我多年以来的观察可以断定,现实中有无数没有进行以上流程判断的代码都在运行,而且也几乎没有暴露出什么严重的不良后果。这使得我有理由猜测,微软也许已经把对此情况的处理隐含到了系统内部。更何况,Windows Mobile 中根本就没有 ReplyMessage 这个 API。

这两个函数用于访问当前处理的消息的另外两个信息,对应于 MSG 结构里的相应域。它们存在的原因是因为窗口回调/消息处理函数一般都不会传递这两个数据。

这是一个在讲到消息相关的内容时,十有八九会被人遗忘的 API。它属于传统的 ITC、IPC 和 Windows 特有的消息机制的交叉地带。不过,在 Windows 平台上,如果还没有了解并掌握这个函数,那一定不能称其为专家。

这个函数揭示了以下平时不太为人所注意的细节:

1、 消息和内核对象,有千丝万缕的联系
2、 消息和内核对象可以按照相似的方式去处理

如果说, SendMessageTimeout 是 Windows 平台下最强大的发送消息的机制,那么, MsgWaitForMultipleObjects[Ex] 就是最强大等待机制,它是 WaitMessageWaitFor 函数族的集大成者。根据我们上面使用 WaitMessagePeekMessage 结合使用可以取代 GetMessage 的论断,我们也可以这样说, MsgWaitForMultipleObjects[Ex] 是最强大的消息循环发动机。

仔细描述此函数会超出单纯的消息机制范畴,所以把深入学习它的工作遗留给各位自己去实践。

7. Windows 的消息辨析

7.1. SendMessage 和 PostMessage 的区别

请考虑有面试考官问及此问题时你如何组织回答。

7.2. SendMessage 发送的消息不进入消息队列吗

提示:请考虑跨线程的情况。

这个说法不完全正确。当 SendMessage 发送的消息跨越线程边界时,消息其实被加入到了目标线程的消息队列里。不过,在线程队列里,别的线程Send过来的消息会被优先处理。

7.3. PostMessage(WM_QUIT)PostQuitMessage() 的区别,可能会产生怎样的差异化执行效果

提示:请考虑发生以上某个调用时,消息队列里不为空的情况。

7.4. 文章开头的经典消息循环正确么?

提示:请注意 GetMessage 的返回值。

曾经有很长一段时间,连微软的例子也这样写。但是,这样写其实是不对的。原因很简单, GetMessage 不仅仅是取道消息返回 TRUE,取不到(遇到 WM_QUIT 消息)返回 FALSE 这么单纯,它还会出错。出错时返回 -1。这就了能使得经典循环在 GetMessage 发生错误时变成死循环。微软的建议是,当 GetMessage 返回 -1 时,跳出循环,结束程序。

注:本文乃是数年前的培训讲义,文中有某处不完整,迄今未补,读者自察之。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注