消息泵也就是消息(处理)循环 (Message Loop),每个基于事件驱动编写出来的 Windows 程序都应该有一个。 消息循环(Message Loop)是程序的心脏,保证程序的正常运行,它的形状大概如下面的结构。
while (true)
{
// 内部处理
}
可见,它应该是不断循环的一段代码, 打破它的循环可以有条件的使用 break 。
消息(处理)循环的首要任务当然就是检测消息队列中的消息了,你有两个选择,就是使用 PeekMessage() 或 GetMessage() 函数。不过,这两个函数是有区别的,下面讲解一下。
记得 Sleep() 函数吗?上一篇教程里面用来让程序暂停执行特定的一段时间,在你沉睡的时候不会给 CPU 带来任何运行的负担,这比用空循环延时更科学。
GetMessage() 就是这样的原理,你执行 GetMessage() 的时候,如果有消息在消息队列里,它取得 MSG 并返回真。但如果没有消息呢?它就先小睡一会儿。什么时候被唤醒? 当然是系统向你的消息队列发送消息的时候,所以什么时候被返回,作为程序设计者的你根本无法预知!
那么说来,它应该不会返回 0 的了吧?不对,一个特殊情况,如果它取得的消息的代号是 WM_QUIT (WM_QUIT == 18),它返回的就是 0 ,那时候你就可以使用 break 了。
PeekMessage() 就不同了,它从来不偷懒! 队列中有消息, 它返回真 ;没有消息,它返回假。它的逻辑就那么简单!
WaitMessage() 是特意为 PeekMessage() 准备的,一般来说,当 消息循环里面为空的时候就会调用 WaitMessage() 。它有什么功用?就像它的名字一样,等待消息。嗯,睡着等!(这样可以减轻 CPU 运算负担)
当你拿回来的消息代号等于 WM_QUIT (WM_QUIT == 18),你一样可以使用 break 。
以下两个例子功能其实是一样的,你会选择哪一个做你的消息循环呢?
MSG msg; // 定义消息载体
PostQuitMessage(0); // 发送 WM_QUIT 使打断循环
// 使用 GetMessage() 的例子
while (true)
{
if ( ! GetMessage( &msg, NULL, 0, 0 ) ) break;
// 其它处理
}
PostQuitMessage(0); // 发送 WM_QUIT 使打断循环
// 使用 PeekMessage() 的例子
while (true)
{
if ( PeekMessage( &msg, NULL, 0, 0, PM_REMOVE ) )
{
if ( msg.message == WM_QUIT ) break;
// 其它处理
}
else WaitMessage();
}
我肯定会选择第二个,因为我可以更加自主地开发。 ( 一个提示:游戏程序程序通常会把它的核心运算部分做成函数,用来代替 WaitMessage() 函数 )
以上例子描述的是一个消息循环的基本框架,不过,相信你也知道,不会就那么简单的。 不过,也不会太复杂,只要加多两个函数就行了,那就是 TranslateMessage() 和 DispatchMessage() 。
Translate 是转化的意思,整个函数有什么用呢?对了,是把 虚拟键码 转化为 字符码 ( MSDN 原文:translates virtual-key messages into character messages )。 虚拟键码 (virtual-key) 是什么?其实在之前检测全局键盘里面就用过,那是对键盘上不同键钮的编码,用来区分每一个键钮。 那么字符码 (character) 实际上是 ASCii 码的一个子集。 实现方式是,当认定消息需要转化为 字符码 消息,该函数就会向自己的消息队列里 Post 一个 WM_CHAR 的消息 (WM_CHAR == 258),因为是使用 Post 方式,所发送的消息会插队到最前面。 要转化的 虚拟键码 从原消息的 wParam 中取得,转化后的 字符码 则放在 WM_CHAR 消息的 wParam 中。
举个例子,当 A 键被按下的时候会发出 WM_KEYDOWN 消息,并且附带一个虚拟键码帮助我们知道被按下的是哪一个键。但是,A 被按下的情况不止一种,根据不同的其他因素,可能是 大写 的 "A" ,也可能是 小写 的 "a",完全视乎当时是否打开大写输入 (Caps Lock) 和有没有同时按下 Shift 键。 TranslateMessage() 会自动识别这些情况,保证在 WM_CHAR 消息的 字符码 是希望得到的效果。
再举个例子: 如果 键 "7" 被按下,同时还有 Shift 键,那么转化后的 WM_CHAR 消息的 wParam 中就不是 "7" 的 ASCii 码 而是 "&" 的 ASCii 码了。
没有对应 ASCii 码的 虚拟键码 (比如 VK_LSHIFT) 只是被直接复制到 WM_CHAR 消息的 wParam 中去而已。
( 想不到一个简单的函数也要用这么多篇幅讲解,不多说了,查查 MSDN 就知道了 )
为了验证以上说的,我修改了上一篇中关于检测系统消息的循环部分。
long i = 19, k = 0 ; // i 为循环系数初始值, k 用来纪录接受了多少条消息
while (i) // 循环系数 i 为 0 则退出
{
if ( PeekMessage( &msg, NULL, 0, 0, PM_REMOVE ) )
{
wsprintf( Temp , "检测到 第 %ld 号 消息 \n "
" 第一参数 wParam = %ld ;"
" 第二参数 lParam = %ld ;\n "
" (系统时间) msg.time = %ld ;"
" (涉及的句柄) msg.hwnd = %ld \n\n",
msg.message, msg.wParam, msg.lParam, msg.time, msg.hwnd );
lstrcat( Result, Temp ); k++; // 追加到结果字符串后面
if ( TranslateMessage( &msg ) ) // 新加入 TranslateMessage() 函数
{
wsprintf( Temp , "\n注意! 第 %ld 号 消息被 Translate 或和"
" WM_KEYDOWN、WM_KEYUP、WM_SYSKEYDOWN、WM_SYSKEYUP 相关."
" \n\n\n", msg.message );
lstrcat( Result, Temp ); // 追加到结果字符串
}
} // End of if ( PeekMessage(...
i--; // 循环系数减少 1,为 0 则退出
}
通过感性认识,我认为 TranslateMessage() 实际上不过在适当的时候重新发送 WM_CHAR 而已! 不过,如果没有它,很多窗口都不工作了。我也不知道是什么原因,姑且留着它吧。
那么接下来就是 DispatchMessage() 函数了,查看 MSDN 的解释说,它是分派消息到窗口的消息处理函数中去进行处理的。
这个不难解释,不过要先说明一下窗口的消息处理函数 ( window procedure ), 一个程序可以同时创建多个窗口,这些窗口对消息有着不同的处理方法,所以就要给窗口定义自己的消息处理的函数 (window procedure)。
但是,系统发送给这些窗口的消息都统一发送到同一个 消息队列 中,幸亏消息结构中有 msg.hwnd 指出该条消息与哪一个窗口相关, DispatchMessage() 函数就是依照这个保证消息分派处理自动化而且不会出错!
在本例中创建的 "EDIT" 类的窗口,它的消息处理函数已经被预先定义了,以后会介绍自己定义的方法,是要调用 RegisterClassEx() 函数来实现的。暂时卖个关子,不想再搞混乱你了。
下面给出优化的消息泵的源程序:
// File Name: WinMain.cpp
#define WIN32_LEAN_AND_MEAN // Say No to MFC !!
#include <windows.h>
char Temp[77] = "Hello world";
char Title[] = "Sample __CopyRight - `海风 ";
// Name: WinMain() 主程序入口
// ------ ---------- ----------- ---------
int WINAPI WinMain( HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine, int nCmdShow )
{
//*
// 下面用 CreateWindowEx() 函数创建一个临时窗口 , 使用 hWnd 暂时保存句柄
HWND hWnd=CreateWindowEx( WS_EX_TOPMOST | WS_EX_TOOLWINDOW ,
"Edit","1231",
WS_OVERLAPPEDWINDOW,
120,120,
280,180,
NULL,NULL,hInstance,NULL);
ShowWindow( hWnd, SW_SHOW ); // 让这个窗口可以被看见
MessageBox( NULL, "创建了测试窗口,按确定继续测试!", Title, MB_OK | MB_TOPMOST );
// */
MSG msg; // 定义了一个装载消息的结构
// 优化的消息循环
while (true)
{
if ( PeekMessage( &msg, NULL, 0, 0, PM_REMOVE ) )
{
if ( msg.message == WM_QUIT ) break;
// 一旦按 Shift 键就发送退出消息
if ( msg.message == WM_KEYDOWN && msg.wParam == VK_SHIFT ) PostQuitMessage( true );
TranslateMessage(&msg);
DispatchMessage(&msg);
}
else WaitMessage();
} // End of while (true)
DestroyWindow( hWnd ); // 退出程序前要销毁窗口
MessageBox( NULL, "按 确定 结束测试", Title, MB_OK | MB_TOPMOST );
ExitProcess(0);
return NULL;
}
这个程序不太长,内容是最常用到的东西!
也许你还不知道,其实 MessageBox() 函数里面就包含了一个消息泵,如果调用过该函数,你的消息队列就可能被清空了!
…… `海风 2002年10月17日 pm 6:01
——————————————————————————
此时此刻,走廊传来小童的嬉闹声 " 呔! 看招! 黄狗射尿,万丈穿心 ! ... "
我忍不住笑出声来 ... 现在的小孩真厉害!
现在正听的歌: 浪客剑心结尾曲 - It's gonna rain