windows消息机制详解-3

1. 引言

Windows 在操作系统平台占有绝对统治地位,基于Windows 的编程和开发越来越广泛。
Dos 是过程驱动的,而Windows 是事件驱动的[6],这种差别的存在使得很多Dos 程序员不能
习惯Windows 的程序开发。而很多Windows 程序开发人员也只是对消息运行机制一知半解,
想要掌握Windows 编程的核心,必须深刻理解消息机制。事件驱动围绕着消息的产生与处
理展开,事件驱动是靠消息循环机制来实现的。也可以理解为消息是一种报告有关事件发生
的通知,消息是Windows 操作系统的灵魂,掌握了消息运行机制就掌握了Windows 编程的
神兵利器。本文将首先阐述Windows 的编程原理,继而对Windows 的消息运行机制进行分
析,并讲述对消息的处理。MFC 是一个广为使用的编程类库,对Windows 的消息机制进行
了良好的封装,所以,在第二部分将着重讨论MFC 的消息映射,最后结合编程实际,通过
对MFC 消息映射的分析,非常巧妙的加以应用,以帮助解决实际问题

2. Windows 消息运行机制
在介绍Windows 消息运行机制之前,首先介绍一下消息的概念。
2.1 消息的概念和表示
消息(Message)指的就是Windows 操作系统发给应用程序的一个通告[5],它告诉应用
程序某个特定的事件发生了。比如,用户单击鼠标或按键都会引发Windows 系统发送相应
的消息。最终处理消息的是应用程序的窗口函数,如果程序不负责处理的话系统将会作出默
认处理。
从数据结构[4]的角度来说,消息是一个结构体,它包含了消息的类型标识符以及其他的
一些附加信息。
系统定义的结构体MSG[1]用于表示消息,MSG 具有如下定义形式:
typedef struct tagMSG
{
HWND hwnd;
UINT message;
WPARAM wParam;
LPARAM lParam;
DWORD time;
POINT pt;
}MSG;

其中hwnd 是窗口的句柄,这个参数将决定由哪个窗口过程函数对消息进行处理;message
是一个消息常量,用来表示消息的类型;wParam 和lParam 都是32 位的附加信息,具体表
示什么内容,要视消息的类型而定;time 是消息发送的时间;pt 是消息发送时鼠标所在的位
置。

2.2 Windows 编程原理
Windows 是一消息(Message)驱动式系统,Windows 消息提供了应用程序与应用程序
之间、应用程序与Windows 系统之间进行通讯的手段。应用程序要实现的功能由消息来触
发,并靠对消息的响应和处理来完成。Windows 系统中有两种消息队列,一种是系统消息队
列,另一种是应用程序消息队列。计算机的所有输入设备由 Windows 监控,当一个事件发
生时,Windows 先将输入的消息放入系统消息队列中,然后再将输入的消息拷贝到相应的应
用程序队列中,应用程序中的消息循环从它的消息队列中检索每一个消息并发送给相应的窗
口函数中。一个事件的发生,到达处理它的窗口函数必须经历上述过程。
所谓消息就是描述事件发生的信息,Windows 程序是事件驱动的,用这一方法编写程序
避免了死板的操作模式,因为Windows 程序的执行顺序将取决于事件的发生顺序,具有不
可预知性。Windows 操作系统,计算机硬件,应用程序之间具有如图1 所示的关系

 

 

箭头1 说明操作系统能够操纵输入输出设备,例如让打印机打印;箭头2 说明操作系统
能够感知输入输出设备的状态变化,如鼠标单击,按键按下等,这就是操作系统和计算机硬
件之间的交互关系,应用程序开发者并不需要知道他们之间是如何做到的,我们需要了解的
操作系统与应用程序之间如何交互。箭头3 是应用程序通知操作系统执行某个具体的操作,
这是通过调用操作系统的API 来实现的;操作系统能够感知硬件的状态变化,但是并不决
定如何处理,而是把这种变化转交给应用程序,由应用程序决定如何处理,向上的箭头4
说明了这种转交情况,操作系统通过把每个事件都包装成一个称为消息结构体MSG 来实现
这个过程,也就是消息响应,要理解消息响应,首先需要了解消息的概念和表示。

2.3 Windows 消息循环
消息循环[1]是Windows 应用程序存在的根本,应用程序通过消息循环获取各种消息,并
通过相应的窗口过程函数,对消息加以处理;正是这个消息循环使得一个应用程序能够响应
外部的各种事件,所以消息循环往往是一个Windows 应用程序的核心部分。
Windows 的消息机制如图2 所示:

 

 

Windows 操作系统为每个线程维持一个消息队列,当事件产生时,操作系统感知这一事
件的发生,并包装成消息发送到消息队列,应用程序通过GetMessage()函数取得消息并存于
一个消息结构体中,然后通过一个TranslateMessage()和DispatchMessage()解释和分发消息,
下面的代码描述了Windows 的消息循环。
while(GetMessage (&msg, NULL, 0, 0))
{
TranslateMessage (&msg) ;
DispatchMessage (&msg) ;
}
TranslateMessage(&msg)对于大多数消息而言不起作用,但是有些消息,比如键盘按键按
下和弹起(分别对于KeyDown 和KeyUp 消息),却需要通过它解释,产生一个WM_CHAR
消息。DispatchMessage(&msg)负责把消息分发到消息结构体中对应的窗口,交由窗口过程
函数处理。GetMessage()在取得WM_QUIT 之前的返回值都为TRUE,也就是说只有获取到
WM_QUIT 消息才返回FALSE,才能跳出消息循环。

2.4 消息的处理
取得的消息将交由窗口处理函数进行处理,对于每个窗口类Windows 为我们预备了一个
默认的窗口过程处理函数DefWindowProc(),这样做的好处是,我们可以着眼于我们感兴趣
的消息,把其他不感兴趣的消息传递给默认窗口过程函数进行处理。每一个窗口类都有一个
窗口过程函数,此函数是一个回调函数,它是由Windows 操作系统负责调用的,而应用程
序本身不能调用它。以switch 语句开始,对于每条感兴趣的消息都以一个case 引出。
LRESULT CALLBACK WndProc
(
HWND hwnd,
UINT message,
WPARAM wParam,
LPARAM lParam
)
{

switch(uMsgId)
{

case WM_TIMER://对WM_TIMER 定时器消息的处理过程
return 0;
case WM_LBUTTONDOWN://对鼠标左键单击消息的处理过程
reurn 0;
. …
default:
return DefWindowProc(hwnd,uMsgId,wParam,lParam);
}
}
对于每条已经处理过的消息都必须返回0,否则消息将不停的重试下去;对于不感兴趣
的消息,交给DefWindowProc()函数进行处理,并需要返回其处理值。


3. MFC 的消息映射
MFC 是Windows 下编程的微软基础类库,封装了大部分Windows API 和Windows 控件,
提供了一套消息映射和命令响应机制,方便了应用程序的开发。MFC 只是通过对Windows
消息映射的进行封装,使得添加消息响应变得更为简单,但深究起来,与Windows 消息机
制有一样的底层实现。


3.1 MFC 消息映射的实现
在MFC 的框架结构下,“消息映射”是通过巧妙的宏定义,形成一张消息映射表格来进
行的。这样一旦消息发生,Framework 就可以根据消息映射表格来进行消息映射和命令传递。
首先在需要进行消息处理的类的头文件(.H)里,都会含有DECLARE_MESSAGE_MAP()
宏,声明该类拥有消息映射表格。
然后在类应用程序文件(.CPP)实现这一表格
BEGIN_MESSAGE_MAP(CInheritClass, CBaseClass)
//{{AFX_MSG_MAP(CInheritClass)
ON_COMMAND(ID_EDIT_COPY,OnEditCopy)
………
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
这里主要进行消息映射的实现,把它和消息处理函数联系在一起。其中出现三个宏,第
一个宏是BEGIN_MESSAGE_MAP 有两个参数,分别是拥有消息表格的类,及其父类。第
二个宏是ON_COMMAND , 指定命令消息的处理函数名称。第三个是
END_MESSAGE_MAP()作为结尾符号。
DECLARE_MESSAGE_MAP 宏定义里包含了MFC 定义的两个新的数据结构;
AFX_MSGMAP_ENTRY 和AFX_MSGMAP;其中AFX_MSGMAP_ENTRY 结构包含了
一个消息的所有相关信息,而AFX_MSGMAP 主要作用有两个,一是用来得到基类的消息映
射入口地址。二是得到本身的消息映射入口地址。
实际上,MFC 把所有的消息一条条填入到AFX_MSGMAP_ENTRY 结构中去,形成一
个数组,该数组存放了所有的消息和与它们相关的参数。同时通过AFX_MSGMAP 能得到
该数组的首地址,同时得到基类的消息映射入口地址。当本身对该消息不响应的时候,就可
以上溯到基类的消息映射表寻找对应的消息响应。
MFC 通过钩子函数_AfxCbtFilterHook()截获消息,并在此函数中把窗口过程函数设置为

AfxWindProc,而原来的窗口过程函数被保存在成员变量m_pfnSuper 中。
在MFC 框架下,通过下面的步骤来对消息进行映射[7]。
1 函数AfxWndProc 接收Windows 操作系统发送的消息。
2 函数AfxWndProc 调用函数AfxCallWndProc 进行消息处理,这里一个进步是把对句柄的
操作转换成对CWnd 对象的操作。
3 函数AfxCallWndProc 调用CWnd 类的方法WindowProc 进行消息处理。
4 WindowProc 调用OnWndMsg 进行正式的消息处理,即把消息派送到相关的方法中去处理。
5 如果OnWndMsg 方法没有对消息进行处理的话,就调用DefWindowProc 对消息进行处理。
这就是MFC 对消息调用过程的巧妙封装。


3.2 MFC 消息分类


1 命令消息(WM_COMMAND)
比如菜单项的选择,工具栏按钮点击等发出该消息。所有派生自CCmdTarget 的类都有
能力接收WM_COMMAND 消息。


2 标准消息(WM_XXX)
比如窗口创建,窗口销毁等。所有派生自CWnd 的类才有资格接收标准消息。


3 通告消息(WM_NOTIFY)
这是有控件向父窗口发送的消息,标示控件本身状态的变化。比如下拉列表框选项的改
变CBN_SELCHANGE 和树形控件的TVN_SELCHANGED 消息都是通告消息。
Window 9x 版及以后的新控件通告消息不再通过WM_COMMAND 传送,而是通过
WM_NOTIFY 传送, 但是老控件的通告消息, 比如CBN_SELCHANGE 还是通过
WM_COMMAND 消息发送。


4 自定义消息
利用MFC 编程,可以使用自定义消息。使用自定义消息需要遵循一定的步骤[2]并需要
自己编写消息响应函数


4. MFC 消息的灵活运用
在此,我们给出一个示例程序,演示对MFC 消息的灵活运用,通过此例的剖析,将加
深我们对MFC 消息的理解。


4.1 示例功能描述
本示例程序将演示这样一种效果:
对话框上有一个CTabCtrl 控件,一个CComboBox 控件,两个按钮Button1 和Button2。
CTabCtrl 控件有两个标签页Tab1 和Tab2;CComboxBox 有两个选项:选项1 和选项2;通
过按钮(Button1 和Button2)单击,分别发送CTabCtrl 控件的TCN_SELCHANGE 消息和
下拉列表框的CBN_SELCHANGE 消息,在各自的消息响应函数中只是简单的对控件选项做
切换和给出提示信息。
单击Button1 将选中标签页Tab1 和下拉列表框的选项1,并弹出提示信息;单击Button2
将选中标签页Tab2 和下拉列表框的选项2,并弹出提示信息。


4.2 程序设计思路
TCN_SELCHANGE 消息和CBN_SELCHANGE 消息都属于通告消息,此消息由子控件

发送给父窗口,在MSDN 中查询发现TCN_SELCHANGE 消息是以WM_NOTIFY 消息的形
式发送,在MSDN 中查询WM_NOTIFY 消息:
idCtrl = (int) wParam;
pnmh = (LPNMHDR) lParam;
也就是说,WPARAM 参数传递发送此消息的控件标识,LAPAM 参数一个指向NMHDR
结构体的指针。NMHDR 结构体定义如下:
typedef struct tagNMHDR
{
HWND hwndFrom;
UINT idFrom;
UINT code;
}
NMHDR; 其中hwndFrom 标识发送消息控件的句柄,idFrom 是发送消息控件的ID,code
则是消息码,如果要发送TCN_SELCHANGE 消息,则以TCN_SELCHANGE 填充。
查询MSDN 发现, 由CComboBox 控件发送的CBN_SELCHANGE 消息以
WM_COMMAND 消息发送,WPARAM 的高字节传递CComboBox 控件的ID,低字节发送
消息码CBN_SELCHANGE,而LPARAM 则传送发送此消息的控件句柄。
所以我们可以通过在按钮控件的单击响应函数里分别发送WM_NOTIFY 和
WM_COMMAND 消息来引起TCN_SELCHANGE 和CBN_SELCHANGE 消息响应函数的调
用,分别在两控件消息响应函数中实现选项改变和消息提示即可,遵照这种思路,我们就可
以实现我们想要的功能。


4.3 程序实现步骤
启动VC++6.0,新建基于对话框的应用程序MsgTest.
在对话框上添加1 个CTabCtrl 控件,一个CComboBox 控件,2 个按钮Button1 和Button2;
给IDC_TAB1 和IDC_COMBO1 分别关联控件成员变量m_tab1 和m_cb1;为两按钮分
别添加按钮单击响应函数。
在对话框的OnInitDlg()函数中为CTabCtrl 控件添加两个标签页,Tab1 和Tab2;为
ComboBox 添加选项1 和2;代码如下:
m_tab1.InsertItem(0,"Tab1");
m_tab1.InsertItem(1,"Tab2");
m_cb1.AddString("选项1");
m_cb1.AddString("选项2");
用ClassWizard 为CTabCtrl 添加消息响应TCN_SELCHANGE,为CComboBox 添加消息
响应CBN_SELCHANGE。在OnSelchangeTab1()函数中添加代码
int nIndex=m_tab1.GetCurSel();
CString str;
str.Format("%d",nIndex+1);
MessageBox("Tab"+str+" selected!");
在OnSelchangeCombo1()函数中添加代码:
int nIndex=m_cb1.GetCurSel();
CString str;
str.Format("%d",nIndex+1);

MessageBox("ComboBox 选项"+str+" selected!");
在按钮1 的响应函数OnButton1()中添加代码:
m_tab1.SetCurSel(0);
NMHDR nmhdr;
nmhdr.code=TCN_SELCHANGE;
nmhdr.hwndFrom=GetDlgItem(IDC_TAB1)->m_hWnd;
nmhdr.idFrom=IDC_TAB1;
SendMessage(WM_NOTIFY,(WPARAM)IDC_TAB1,(LPARAM)&nmhdr);
m_cb1.SetCurSel(0);
WPARAM wParam=0;
WPARAM lParam=0;
wParam=IDC_COMBO1;
wParam= wParam | (CBN_SELCHANGE<<16);
lParam=(WPARAM)(GetDlgItem(IDC_COMBO1)->m_hWnd);
SendMessage(WM_COMMAND, wParam, lParam);
在按钮2 的响应函数OnButton2()中添加类似代码,只需要把m_tab1.SetCurSel(0)和
m_cb1.SetCurSel(0)分别改成m_tab1.SetCurSel(1)和m_cb1.SetCurSel(1)。
通过SendMessage() 函数向控件的父窗口也就是对话框窗口发送相应的消息,
TCN_SELCHANGE 是以WM_NOTIFY 消息的形式发送,参数WPARAM 标识发送
TCN_SELCHANGE 消息的控件ID,LPARAM 是一个NMHDR 结构体的指针,此结构体的
成员code 标识发送什么通告消息,此处是TCN_SELCHANGE,hwndFrom 是发送消息的控
件句柄, 程序中用GetDlgItem()->m_hWdn 获得, idFrom 是发送消息的控件ID 。
CBN_SELCHANGE 以WM_COMMAND 消息的形式发送,同样的,通过查阅MSDN,可以
对此消息的两个参数进行赋值,以保证消息的正确发送。
通过上面的5 个步骤,我们的程序就编写完成了,单击Button1,可以发现,CTabCtrl
切换到了Tab1 标签页,CComboBox 选择了“选项1”,并弹出消息对话框。由此可见确实引
起了消息响应函数的调用,完成了预定的功能。
通过查阅MSDN,可以得到其他消息的发送和包装形式,我们可以方便的加以利用,完
成更为复杂的功能,可以说,掌握了Windows 的消息机制,就掌握了Windows 编程的核心。
5. 总结
Windows 消息机制是Windows 编程的本质和核心,对Windows 消息机制的理解能提高
我们Windows 程序开发的能力。本文首先阐述Windows 的消息机制,然后讲解了MFC 的
消息映射,消息分类,最后通过示例程序,讲解如何借助MSDN,灵活运用消息编程,解
决实际问题。本文对Windows 下的程序开发具有一定的参考和借鉴意义。

/********************************

1.       Windows 的历史

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

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

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

[cpp]  view plain copy

  1. while(GetMessage(&msg, NULL,0, 0))  
  2. {  
  3.                TranslateMessage(&msg);  
  4.                DispatchMessage (&msg);  
  5. }  
 
  1. while(GetMessage(&msg, NULL,0, 0))

  2. {

  3. TranslateMessage(&msg);

  4. DispatchMessage (&msg);

  5. }

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

第二个大红大紫的版本则非Windows 95莫属。这个版本的主要变化在于,无论如何,它是一个大众化的所谓32位系统了。之所以要加上“所谓的”三个字,是因为这个系统是个混血儿,在32位代码中混杂有大量的从之前的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 的完整声明如下:

[cpp]  view plain copy

  1. typedef struct {  
  2.     HWND hwnd;  
  3.     UINT message;  
  4.     WPARAM wParam;  
  5.     LPARAM lParam;  
  6.     DWORD time;  
  7.     POINT pt;  
  8. } MSG, *PMSG;  
 
  1. typedef struct {

  2. HWND hwnd;

  3. UINT message;

  4. WPARAM wParam;

  5. LPARAM lParam;

  6. DWORD time;

  7. POINT pt;

  8. } MSG, *PMSG;

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

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

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

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

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

4.       Windows 的消息机制

4.1.       消息队列

说到消息机制,可能连最初级的 Windows 程序员都会对消息队列(MessageQueue)这个名词耳熟(不过不见得能详)。对于这样一个基本概念,Windows 操作系统提供的针对消息队列的API 却少的可怜(GetQueueStatus、GetInputState、GetMessageExtraInfo、SetMessageExtraInfo),而且,这些 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,SendMessage 和 PostMessage。系统产生的消息,虽然我们看不到代码,不过我们还是可以粗略地划拨一下,基本上所有的输入类消息,都是以投递的方式抵达应用的,而其他的消息,则大部分是采取了发送方式。

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

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_CLOSE 和 WM_DESTROY。这个例子仅仅出于概念性的展示,而不是完全精确可靠的,而且,在 Windows Mobile 上,干脆就没有非客户区的概念。

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

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

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

这才算是功德圆满了。

4.2.3.      消息的优先级

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

不过还需要注意,还有那么几个优先级比正常的消息低一点点的。它们是:WM_PAINT、WM_TIMER、WM_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相互等待,形成死锁。

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

可以使用 SendNotifyMessage 或 SendMessageTimeout来避免出现此类死锁。

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

4.4.       模态(Modal)

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

显示了一个对话框

显示出一个菜单

操作滚动条

移动窗口

改变窗口大小

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

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

[cpp]  view plain copy

  1. void CALLBACK RequestQuit(HWNDhwnd, UINT uMsg, UINT idEvent, DWORD dwTime);  
  2. void TestModal()  
  3. {  
  4.                UINT uTimerId =SetTimer(NULL, 66, 1000, RequestQuit);  
  5.                MessageBox(NULL, NULL, NULL,MB_OK);  
  6.                KillTimer(NULL, uTimerId);  
  7. }  
  8.   
  9. void CALLBACK RequestQuit(HWND hwnd, UINT uMsg, UINT idEvent, DWORD dwTime)  
  10. {  
  11.                PostMessage(NULL, WM_QUIT,0, 0);  
  12. }  
 
  1. void CALLBACK RequestQuit(HWNDhwnd, UINT uMsg, UINT idEvent, DWORD dwTime);

  2. void TestModal()

  3. {

  4. UINT uTimerId =SetTimer(NULL, 66, 1000, RequestQuit);

  5. MessageBox(NULL, NULL, NULL,MB_OK);

  6. KillTimer(NULL, uTimerId);

  7. }

  8. void CALLBACK RequestQuit(HWND hwnd, UINT uMsg, UINT idEvent, DWORD dwTime)

  9. {

  10. PostMessage(NULL, WM_QUIT,0, 0);

  11. }

答案见本大节末尾。

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

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

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

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

4.6.       所谓的反射(Reflection)

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

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

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

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

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

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

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

5.       Windows 的消息本质

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

6.       Windows 的消息操纵

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

[cpp]  view plain copy

  1. SendMessage  
  2.   
  3. PostMessage  
 
  1. SendMessage

  2. PostMessage

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

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

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

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

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

 

[cpp]  view plain copy

  1. GetMessage  
GetMessage

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

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

[cpp]  view plain copy

  1. DispatchMessage  
DispatchMessage

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

[cpp]  view plain copy

  1. TranslateMessage(<SPAN style="COLOR: #33ff33">TranslateAccelerator</SPAN>)  
TranslateMessage(TranslateAccelerator)

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

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

[cpp]  view plain copy

  1. PeekMessage  
PeekMessage

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

[cpp]  view plain copy

  1. <SPAN style="COLOR: #33ff33">WaitMessage</SPAN>  
WaitMessage

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

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

[cpp]  view plain copy

  1. SendNotifyMessage  
SendNotifyMessage

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

SendMessageTimeout

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

[cpp]  view plain copy

  1. <SPAN style="COLOR: #33ff33">SendMessageCallback</SPAN>  
SendMessageCallback

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

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

[cpp]  view plain copy

  1. PostQuitMessage  
PostQuitMessage

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

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

[cpp]  view plain copy

  1. PostThreadMessage  
PostThreadMessage

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

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

[cpp]  view plain copy

  1. <SPAN style="COLOR: #33ff33">BroadcastSystemMessage[Ex]</SPAN>  
BroadcastSystemMessage[Ex]

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

[cpp]  view plain copy

  1. InSendMessage<SPAN style="COLOR: #33ff33">[Ex]</SPAN>  
InSendMessage[Ex]

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

[cpp]  view plain copy

  1. <SPAN style="COLOR: #33ff33">ReplyMessage</SPAN>  
ReplyMessage

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

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

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

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

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

[cpp]  view plain copy

  1. GetMessagePos  
  2.   
  3. <SPAN style="COLOR: #33ff33">GetMessageTime</SPAN>  
 
  1. GetMessagePos

  2. GetMessageTime

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

[cpp]  view plain copy

  1. MsgWaitForMultipleObjects[Ex]  
MsgWaitForMultipleObjects[Ex]

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

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

1、  消息和内核对象,有千丝万缕的联系

2、  消息和内核对象可以按照相似的方式去处理

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

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

7.       Windows 的消息辨析

7.1.       SendMessage和PostMessage的区别

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

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

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

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

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

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

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

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

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

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

Windows Features

>桌面窗口

    GetDesktopWindow:获取桌面窗口句柄

    SystemParameter(wAction=SPI_SETDESKWALLPAPER):设置桌面壁纸

>窗体结构

>客户区和非客户区

    客户区:应用程序中可以直接输出的区域

    非客户区:应用程序中的特殊区域(保护标题栏、菜单栏、系统按钮等)

>窗口创建消息:在这些消息中处理程序初始化的任务

    WM_NCCREATE:创建非客户区消息

    WM_CREATE:创建客户区消息

    WM_PARENTNOTIFY:创建子窗口后系统发送给父窗口

> 窗口类型

    1.OverlappedWindows:

        是一种顶层窗口

        有标题栏、边框、客户区(WS_OVERLAPPED),可选:窗口菜单、最小化和最大化按钮、滚动条(WS_OVERLAPPEDWINDOW)

        通常作为程序的主窗口

       

    2.Pop-UpWindows:弹出窗口

        是一种特殊的Overlapped窗口

        经常用来弹出对话框、消息窗口和临时窗口

        使用WS_POPUP样式创建窗口

    

    3.Child Windows:子窗口

        WS_CHILD样式

        局限于父窗口的显示区,必须为其指定一个父窗口

        通常用来切分父窗口

        子窗口显示默认是在父窗口的左上角,如果比父窗口大,则进行裁剪

        父窗口的消息影响其子窗口: 

            Destroyed:子窗口先销毁 

            Hidden:子窗口先隐藏

            Moved:随着父窗口移动,移动后得自绘

            Show:父窗口先显示

        裁剪:WS_CLIPCHILDREN样式使父窗口不能在子窗口之上绘制

        和父窗口关系:

            SetParent:指定一个父窗口,参数为NULL时,显示在桌面上,可以自由拖动

            GetParent:获得父窗口的句柄

            IsChild:判断一个窗口是不是另一个窗口的子窗口

            EnumChildWindow:枚举子窗口

            子窗口和父窗口的类型可以不一样

    消息:

            子窗口的消息直接发送给子窗口,如被禁止,则发送到父窗口

            EnableWindow:禁止或启用一个子窗口

            

     层叠窗口:WS_EX_LAYERED

    消息窗口:

        不可见、没有z自序,不能被枚举,不能收到广播消息

        使用HWND_MESSAGE句柄

        使用FindWindowEx并传入HWND_MEAAGE查找窗口

            

  窗口关系

>前台窗口与后台窗口

    和用户交互的窗口叫做前台窗口,创建的线程叫做前台线程

    其他线程创建的窗口叫做后台窗口,其他线程叫做后台线程

    前台窗口线程的优先级(9)高于其他线程的(7)

    设置前台窗口:点击、Alt+Tab、Alt+Esc

    获取前台窗口句柄:GetForegroundWindow()

    设置前台窗口:SetForegroundWindow(),设置的进程需要满足特定的条件

>Owned窗口

    窗口在其拥有者窗口之上(Z order)

    拥有者窗口销毁时被拥有者窗口也自动销毁

    拥有者窗口最小化时被拥有者窗口隐藏

    只有Overlapped和popup窗口可以称为拥有者窗口,子窗口不能称为拥有者窗口

    通过GetWindow及参数GW_OWNER获取窗口的拥有者窗口

>Z-Order

    顶层窗口在所有其它非顶层窗口之上

    BringWindowToTop、SetWindowPos、DeferWindowPos设置窗口在Z序的位置

窗口状态

>Aactive Windows:当前用户正在使用的窗口

    SetActiveWindows:切换激活窗口(本进程内?)

    SetWindowsPos、DeferWindowPos、SetWindowPlacement

    GetActiveWindow

    WM_ACTIVATEAPP:发送激活窗口切换时系统发送给两个窗口

>Disable Windows:

    Disable窗口不能收到鼠标和键盘消息

    EnableWindow:禁止或启用一个窗口

    IsWindowEnabled:判断一个窗口是否可用

    子窗口被disable的时候,父窗口可以接收子窗口的鼠标消息

>可见性

    如果一个窗口被隐藏等同于失效

    IsWindowVisible:判断一个窗口是否可见

>CloseWindow:最小化窗口

    SetWindowPlacement与ShowWindows功能类似,还可以改变最小化、最大化和恢复时默认的位置

    WM_QUERYOPEN:最大化或恢复窗口时,系统给窗口发送这个命令

    WM_GETMINMAXINOF:后去窗口的默认大小信息

Windows大小和位置

>WM_GETMINMAXINFO:获取窗口大小拖动时最大尺寸和最小尺寸

    WS_THICKFREAME:容许一个窗口大小被拖动

>WM_SYSCOMMAND:用户点击窗口菜单时由系统发送

    SC_CLOSE:关闭窗口,发送WM_CLOSE消息到窗口

    SC_MAXIMIZE:最大化窗口

    SC_MINIMIZE:最小化窗口

    SC_MOVE:移动窗口

    SC_RESTORE:恢复最大化或最小化之前的大小

    SC_SIZE:改变窗口大小

>改变大小或位置:

    SetWindowPlacement:设置窗口最大化、最小化,恢复的大小和位置,以及显示状态

    MoveWindow和SetWindowPos:设置窗口位置

    XXXDeferWindowPos:同时设置窗口的大小、位置,Z序和显示状态

    GetWindowRect:获取窗口矩形信息,都是相对于桌面的位置

    ScreenToClient和MapWindowPoints:进行坐标转化

    GetClientRect:获取客户区的矩形,相对与自身的

    TileWindows和 CascadeWindows: 将左右最大化窗口还原    

      WM_WINDOWPOSCHANGING:窗口大小和位置改变之后会收到此消息

    WM_NCCALSIZE:窗口创建时和大小改变时会收到此消息

>AnimateWindow:特殊显示或隐藏窗口

窗口销毁

>DestroyWindow:销毁窗口

   给自己和所有子窗口发送WM_DESTROY消息

>WM_CLOSE:在销毁窗口前,提供给用户一个确认的机会

>WM_DESTROY:给应用程序一次清理资源的机会

>PostQuitMessage:应用程序清理完资源时调用,退出主消息循环

>UpdateWindow:发送WM_PAINT消息

 

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:编程工作室 设计师:CSDN官方博客 返回首页
评论

打赏作者

luckyone906

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值