Windows 是一个消息驱动的操作系统。一个消息由一个消息名称(UINT 类型)和两个参数
(WPARAM,LPARAM)构成。当用户进行了输入或是窗口的状态发生改变时,系统会发送消息到某
一个窗口。例如当菜单选中之后会有WM_COMMAND 消息发送,WPARAM 的高字中
(HIWORD(wParam))是命令的ID 号,对菜单来说就是菜单ID。当然用户也可以定义自己的消息名称,
也可以利用自定义消息来发送通知和传送数据。
一个消息必须由一个窗口接收。在窗口的消息处理函数中可以对消息进行分析,对自己感兴趣的
消息进行处理。例如希望对菜单选择进行处理,那么可以定义对WM_COMMAND 进行处理的代码,
如果希望在窗口中进行图形输出就必须对WM_PAINT 进行处理。
事实上为了应付那些没有被响应的消息,Windows 为窗口编写了默认的窗口消息处理函数,这个
窗口过程将负责处理那些程序中没有处理的消息。正因为有了这个默认窗口过程,程序员才可以利用
Windows 的窗口进行开发而不必过多关注窗口各种消息的处理。例如窗口在被拖动时会有很多消息发
送,而程序员都可以不予理睬让系统自己去处理。
说到消息就不能不说窗口句柄,系统通过窗口句柄在整个操作系统中唯一标识一个窗口。发送消
息时必须指定一个窗口句柄表明该消息由哪个窗口接收。而每个窗口都会有自己的窗口消息处理函数,
所以用户的输入就会被正确的处理。
下面这段伪代码演示如何在窗口过程中处理消息:
LONG windowProc(HWND hWnd,UINT uMessageType,WPARAM w,LPARAM l) { switch(uMessageType) { //使用SWITCH 语句将各种消息分开 case WM_PAINT: //处理绘制消息 Redraw(); break; case WM_TIMER: //处理定时器消息 OnTimer(); break; case WM_LBUTTONDOWN: //处理鼠标左键按下的消息 OnLButtonDown (); break; default: defaultWndProc(); //缺省的其他消息处理函数 break; } }
在Windows 操作系统中维护着一个或多个消息队列,所有产生的消息都被放入到队列中。系统在
队列中每次取出一条消息,根据消息的接收句柄而将该消息发送给拥有该窗口的消息循环。每一个运
行的程序都有自己的消息循环,在循环中得到属于自己的消息并根据接收窗口的句柄调用相应的窗口
过程。而在没有消息时消息循环就将控制权交给系统,从而使Windows 可以同时进行多个任务。下面
的伪代码演示了消息循环的用法:
while (1) { id =GetMessage (); if (id == WM_QUIT) break ; DispatchMessage (); }
初看这段代码容易给人一种错觉,这是一个忙等待(busy waiting)的消息循环,因为采用了while(1)
的循环方式,而忙等待是个非常糟糕的东西。
而实际上绝大部分时间里这个程序是在阻塞状态, 因为当程序没有收到消息通知时
GetMessage 就不会返回,所以也就不会占用系统的CPU 时间。GetMessage 函数的阻塞调用是这
段代码的关键部分。
上面这段代码的意思是调用GetMessage 函数从消息队列中取出消息,然后调用DispatchMessage
将消息发送给窗口消息处理程序。
在16 位的系统中只有一个消息队列,所以系统必须等待当前任务处理消息后才可以发送下一消息
到相应程序,如果一个程序陷入死循环或是耗时操作时,系统就会得不到控制权。这种多任务系统也
称为协同式的多任务系统。Windows 3.X 就是这种系统。而32 位的系统中每一个运行的程序都会有一
个消息队列,所以系统可以在多个消息队列中转换而不必等待当前程序完成消息处理就可以得到控制
权。这种多任务系统就称为抢先式的多任务系统,Windows 95/NT 就是这种系统。
消息能够被分为队列化的和非队列化的。队列化的消息是由Windows 放入程序消息队列中的。在
程序的消息循环中,重新传回并分配给窗口消息处理程序。非队列化的消息在Windows 调用窗口时直
接送给窗口消息处理程序。也就是说,队列化的消息被发送给消息队列,而非队列化的消息则发送给
窗口消息处理程序。任何情况下,窗口消息处理程序都将获得窗口所有的消息——包括队列化的和非
队列化的。窗口消息处理程序是窗口的消息中心。
队列化消息基本上是使用者输入的结果,以击键(如WM_KEYDOWN 和WM_KEYUP 消息)、
击键产生的字符(WM_CHAR)、鼠标移动(WM_MOUSEMOVE)和鼠标按钮(WM_LBUTTONDOWN)
的形式给出。队列化消息还包含时钟消息(WM_TIMER)、更新消息(WM_PAINT)和退出消息
(WM_QUIT)。
在许多情况下,非队列化消息来自调用特定的Windows 函数。例如,当WinMain 调用CreateWindow
时,Windows 将建立窗口并在处理中给窗口消息处理程序发送一个WM_CREATE 消息。当WinMain
调用ShowWindow 时,Windows 将给窗口消息处理程序发送WM_SIZE 和WM_SHOWWINDOW消息。
虽然Windows 程序可以多线程执行,但消息队列只为窗口消息处理程序在该执行中的窗口处理消
息。换句话说,消息循环和窗口消息处理程序不是并发执行的。当一个消息循环从其消息队列中接收
一个消息,然后调用DispatchMessage 将消息发送给窗口消息处理程序时,直到窗口消息处理程序将
控制传回给Windows,DispatchMessage 才能结束执行。
当然,窗口消息处理程序能调用给窗口消息处理程序发送另一个消息的函数。这时,窗口消息处
理程序必须在函数调用传回之前完成对第二个消息的处理。那时窗口消息处理程序将处理最初的消息。
例如,当窗口过程调用UpdateWindow 时,Windows 将调用窗口消息处理程序来处理WM_PAINT 消息。
窗口消息处理程序处理WM_PAINT 消息结束以后,UpdateWindow 调用将把控制传回给窗口消息处理
程序。
在工程中经常还会有这样的需求,即要求程序在空闲的时候执行某种额外的操作或运算。实际上
在Windows 中有很多闲置时间,在这个时间内,所有消息队列为空,Windows 只停在一个小循环中等
待键盘或者鼠标输入。能否在闲置时间内获得控制,从而做某种操作或运算,并且只在有消息加入程
序的消息队列之后才释放控制呢?这就是PeekMessage 函数的目的之一。下面是PeekMessage 调用的
一个例子:
PeekMessage (&msg, NULL, 0, 0, PM_REMOVE) ;
前面的4 个参数(一个指向MSG 结构的指针、一个窗口句柄、两个值指示消息范围)与GetMessage
的参数相同。将第二、三、四个参数设定为NULL 或0 时,表明想让PeekMessage 传回程序中所有窗
口的所有消息。如果要将消息从消息队列中删除,则将PeekMessage 的最后一个参数设定为
PM_REMOVE。如果不希望删除消息,那么可以将这个参数设定为PM_NOREMOVE。
GetMessage 不将控制传回给程序,它一直处于阻塞状态,直到从程序的消息队列中取得消息,但
是PeekMessage 总是立刻传回,而不论一个消息是否出现。当消息队列中有一个消息时,PeekMessage
的传回值为TRUE,并且将按通常方式处理消息。当队列中没有消息时,PeekMessage 传回FALSE。
可以将如下所示的消息循环:
while (1) { id =GetMessage (); if (id == WM_QUIT) break ; DispatchMessage (); }
替换为下面的循环:
while (TRUE) { if (PeekMessage (&msg, NULL, 0, 0, PM_REMOVE)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } else { // 完成其他额外的工作 } } return msg.wParam ;
如果PeekMessage 的传回值为TRUE,则消息按通常方式进行处理。如果传回值为FALSE,则在
将控制传回给Windows 之前,还可以做一点工作。
需要注意的是,不能用PeekMessage 从消息队列中删除WM_PAINT 消息,实际上GetMessage 并
不从消息队列中删除WM_PAINT 消息。从队列中删除WM_PAINT 消息的唯一方法是令窗口显示区域
的失效区域变得有效,这可以用ValidateRect 和ValidateRgn 或者BeginPaint 和EndPaint 对来完成。如
果在使用PeekMessage 从队列中取出WM_PAINT 消息后,同平常一样处理它,那么就不会有问题了。
所不能作的是使用如下所示的程序代码来清除消息队列中的所有消息:
while (PeekMessage (&msg, NULL, 0, 0, PM_REMOVE)) ;
这行代码从消息队列中删除WM_PAINT 之外的所有消息。如果队列中有一个WM_PAINT 消息,
程序就会永远地陷在while 循环中。