理解消息循环
为了编写任何即使是最简单的程序,了解windows程序的消息循环和整个消息发送结构是非常有必要的。既然我们已经尝试了一点消息处理的东西,我们应该对整个程序有更深入的理解,如果你没有理解消息是怎么发生的和它们运行的机制,那接下来的内容你会感到很蛋疼。
什么是消息?
一条消息是一个整数值,如果你查阅你的头文件(这是个好的查阅API的工作惯例)你会发现像下面的东西:
1 #define WM_INITDIALOG 0x0110 2 #define WM_COMMAND 0x0111 3 4 #define WM_LBUTTONDOWN 0x0201
还有很多类似的东西。在windows中消息几乎被运用在基本水平上的所有通信,如果你想要一个窗口控制(只是一个制定的窗口)做某些事情你需要发送给它一条消息;同样的如果另一个窗口想让你做某些事情,它会发送给你一条消息;如果某些像用户点击键盘、移动鼠标、点击按钮的事件发生,那么相应的消息会被系统发送到受影响的窗口,如果你是其中一个受影响的窗口,那么你需要处理消息并采取相应的行动。
每条窗口消息都可能有两个参数,wParam和lParam,最初wParam是16位而lParam是32位,但是在win32中它们都是32位。不是每条消息都需要用到这些参数,用到时的用法也不同,举个例子,WM_CLOSE消息就都没有用到这两个参数,这时我们可以忽略这两个采纳数;而WM_COMMAND就同时用到了这两个参数,wParam包含了两个值,HIWORD(wParam)是一条通知消息(如果可用的话),LOWORD(wParam)是一个发送消息的控制或菜单id。
lParam是控制发送消息或NULL(如果消息不是从控制中来的话)的HWND(窗口句柄)
HIWORD()和LOWORD()被windows宏定义,这两个值都是windows中的宏定义,一个指32位(0xFFFF0000)的高16位(FFFF),一个指32位的低16位(0000);在win32中一个WORD表示的是16位,而DWORD(double word)才表示32位
你可以通过PostMessage()或SendMessage()这两种方式发送消息。PostMessage()将消息插入到消息队列中然后马上返回,这意味着即使一次PostMessage()的调用被完成时,消息可能还没被处理;SendMessage()直接发送消息给窗口并且会不会立即返回,知道窗口完成了对消息的处理。如果我们想关闭一个窗口,我们可以通过PostMessage(hwnd, WM_CLOSE, 0, 0)发送一个WM_CLOSE的消息,这跟我们点击窗口右上方的X按钮有同样的效果,注意在WM_CLOSE消息的相应处理中,wParam和lParam都是0,这是因为刚才提到的在WM_CLOSE消息处理中没有用到这两个参数。
对话框
一旦你开始使用对话框盒子,为了跟它们交流你将需要发送消息给控制。你可以通过使用GetDlgItem()通过使用ID先获取控制的句柄然后使用SendMessage(),或者你可以使用SendDlgItemMessage(),这个方法组合了上面的两个步骤。你给它一个窗口句柄和孩子ID将会得到一个孩子句柄,然后向他发送消息。SendDlgItemMessage()和类似的APIGetDlgItemText()将会作用在所有的窗口上,而不仅仅是对话框。
什么是消息队列?
让我们试着想象当你忙着处理WM_PAINT的时候突然用户用键盘输入一堆东西,会发生什么事情?你绘图的时候被键盘打断应该抛弃吗?当然不是!很明显不管抛弃哪个都不是正确的做法,所以我们有了消息队列。新增POST过来的消息会被添加到消息队列中来,当消息被处理时消息就会从消息队列中被移除,这确保了你不会错过一条消息,如果你正在处理一条消息,那么其他消息就会在队列中等待直到你开始处理它们。
什么是一个消息环?
1 while(GetMessage(&Msg, NULL, 0, 0) > 0) 2 { 3 TranslateMessage(&Msg); 4 DispatchMessage(&Msg); 5 }
1、在消息队列里,消息环调用GetMessage()方法,如果你的消息队列空了,那么你的程序
将会停止然后等待消息,相当于挂起的状态
2、当一个事件发生时,会导致一条消息加入到消息队列中(比如系统注册一个鼠标点击)
GetMessage()返回一个正值表明有一个消息要处理,并且复制给我们传递给它的MSG数
据结构的成员,方法也可能会返回0如果消息是WM_QUIT,如果是发生错误的话方法会
返回负数。
3、我们得到消息(在Msg变量中)然后把它传递给TranslateMessage(),这会产生一点额外
的处理,将虚拟的键盘点击翻译成字符消息,这个步骤实际上是可选的,如果不需要时
它不会发生。
4、一旦上面的步骤完成,我们把消息传递给DispatchMessage(),DispatchMessage()会做的
工作就是拿到消息,检查它是哪个窗口的然后找到对应窗口的消息处理程序,接着调用
消息处理程序,并且将窗口的句柄,消息,wParam和lParam四个参数传递给它。
5、在你的消息处理程序中,你会检查参数消息,然后对它做任何你想做的事情,如果你没
有处理指定的消息,那么默认它会调用DefWindowProc()方法,这个消息处理的默认操作,
通常意味着什么都不做。
6、一旦你已经完成了消息处理,你的消息处理程序会返回,DisPatchMessage()返回,然后
我们又回到了环开始的地方。
这是窗口程序中一个非常重要的概念,你的消息处理程序不是神奇地由系统调用,实际上你是间接地通过DispatchMessage()调用。如果你想要的话,你可以在窗口句柄中使用GetWindowLong()直接调用窗口处理程序。
1 while(GetMessage(&Msg, NULL, 0, 0) > 0) 2 { 3 WNDPROC fWndProc = (WNDPROC)GetWindowLong(Msg.hwnd, GWL_WNDPROC); 4 fWndProc(Msg.hwnd, Msg.message, Msg.wParam, Msg.lParam); 5 }
我试着用前面的代码,但是并没有正常工作,因为有各种各样的问题比如Unicode/Ansi编码翻译等,调用定时器回调方法不会占用,这可能会打破我们目前的所有但微不足道的程序,所以你可以尝试,但在实际的代码中不要真的这么做: )
注意,我们使用GetWindowLong()来检索窗口相关的消息处理程序,为什么我们不直接调用WndProc()呢?嗯,我们的消息环是要对我们程序中所有窗口都适用的,包括有自己的窗口过程的按钮和列表框,所以我们应该保证调用了正确的窗口过程,因为多个窗口可以使用同一个窗口过程,所以第一个参数(窗口句柄)用来告诉窗口过程消息属于哪个窗口的。
正如你所看到的,你的应用程序的大多数时间都花费在执行这个消息循环,你可以愉悦地发送消息给可以处理它们的快乐的窗口。但是当你想退出程序时你会怎么做?因为我们使用了一个while()循环,如果GetMessage()返回了false,循环将会结束并且到达我们WinMain()的终点接着退出程序。这正是PostQuitMessage()完成的东西,它把WM_QUIT消息放入队列中而不是返回一个正值,GetMessage()会填充Msg数据结构的内容然后返回0,在这个时候Msg的wParam成员就包含了你传递给PostQuitMessage()的值,这里你可以忽略它,从WinMain()的返回值将会使用在进程结束的退出代码中。
重点:GetMessgae()将会返回-1如果出现错误的话,确保记住这个,否则在某些时候会让你很蛋疼。。。即使GetMessage()被定义用来返回一个布尔值,它也能够返回TRUE或FALSE之外的值,因为BOOL使用UINT(unsigned int)来定义的。下面的代码例子可能可以运行,但是不会正确地处理一下条件:
1 while(GetMessage(&Msg, NULL, 0, 0)) 2 3 while(GetMessage(&Msg, NULL, 0, 0) != 0) 4 5 while(GetMessage(&Msg, NULL, 0, 0) == TRUE)
上面都是错的!就像我刚刚提到的,它可能跟我第一个教程里使用的差不多,只要GetMessage()没有失败它会工作地很好,即使你的代码是正确的,这个也不是正确的,但是在很多情况下这个代码是错误的不能正常工作,当GetMessage()发生失败的时候!这里我们郑重说明和纠正,请原谅我错过了一些要点。
1 while(GetMessage(&Msg, NULL, 0, 0) > 0)
这个才是正确的!
我希望现在你对窗口消息循环有更好的理解,如果还不是非常理解的话,别害怕,一旦你使用它们一段时间之后情况会好很多。
PS.由于本人英文水平所限,只能翻译到这个程度了,有纰漏还望多多指出,附上本篇翻译的英文原版教程地址:http://www.winprog.org/tutorial/message_loop.html