Windows开发中一个重要的概念就是消息。能搞清楚消息的传递和处理,相信可以使我们对Windows程序有更深的理解。
先把消息划分为3类:发送消息(Incomingsent message)、投递消息(Post message)、输入消息(Input message)。其中发送消息是非队列消息,而后两种是队列消息。在线程的消息队列中并不包括非队列消息,而只有队列消息才会在线程的消息队列中。
由上面的分类也可以知道为什么不能通过PostMessage函数来模拟输入动作。因为投递消息将进入到投递消息队列,而输入消息则是进入到输入消息队列中,而且这两个消息队列是在不同的时间进行处理的。
一、发出消息
SendMessage函数族(SendNotifyMessage,SendMessageCallback和SendMessageTimeout)中,从消息的处理方式上来看,所有函数的行为都是一样的:如果发送者和接收者在同一个线程中,那么接收者的窗口过程将被直接调用;如果发送者和接收者在不同的线程中,那么这个消息将被添加到接收者的发送消息队列中,并且将“唤醒”接收者来处理消息。
PostMessage和PostThreadMessage这两个函数将消息添加到接收者的而投递消息队列中,并且唤醒接收者。
用户输入(包括键盘输入、鼠标输入,或者通过SendInput函数产生的输入)所产生的消息被添加到输入消息队列中,并且同样将唤醒接收者。
如下图:
接收者被唤醒意味着,如果接收者被GetMessage、WaitMessage、MsgWaitMultiplieObjects或者其他类似的函数阻塞了,那么接收线程将解除阻塞,这样它能够处理新的消息,就好像添加到队列的消息正是接收线程正在等待的消息。
二、接收消息
在消息的接收端,有三种方法用来接收消息。虽然各种方法之间略有不同,但这些方法都遵循相同的基本规则:
1. 发送消息将被直接递交到窗口过程中。
2. 接收投递消息(通过GetMessage或者PeekMessage函数)。
3. 接收输入消息(通过GetMessage或者PeekMessage函数)。
在消息的处理过程中需要递交消息,这个函数可能像下面这样:
LRESULT DeliverMessage(HWND hWnd,UINT uMsg,WPARAM wParam,LPARAM lParam) { //获取对应的窗口过程 WNDPROC lpfnWndProc = (WNDPROC)GetWindowLongPtr(hWnd,GWLP_WNDPROC); //调用窗口过程处理消息 return CallWindowProc(lpfnWndProc,hWnd,uMsg,wParam,lParam); }
消息的处理原则之一就是要递交发送消息。这个函数的处理过程像下面这样:
void DeliverIncomingSentMessages() { while(有一个发送消息){ MSG msg = 这个消息; 从发送消息队列中删除这个消息; if(这是一个特殊的伪消息){ 处理这个伪消息; }else{ DeliverMessage(msg.hWnd, msg.message, msg.wParam, msg.lParam); 把返回值交给发送者; } } }
在前面这些函数的基础上,我们可以开始分析接收消息的函数了。
PeekMessage的伪码像下面展示的那样:
BOOL PeekMessage(LPMSG pmsg,HWND hwnd,UINT wMsgFilterMin,UINT wMsgFilterMax,UINT flags) { DeliverIncomingSentMessages(); if(有某个投递消息满足过滤条件){ *pmsg = 这个消息; if(flags & PM_REMOVE) 从投递消息队列中删除该消息; return TRUE; } if(有某个输入消息满足过滤条件){ *pmsg = 这个消息; if(flags & PM_REMOVE) 从输入消息队列中删除这个消息; return TRUE; } return FALSE; }
PeekMessage函数首先会递交所有等待处理的发送消息(如果有的话)。在完成这个操作之后,函数将根据设置的过滤条件(wMsgFilterMin和参数wMsgFilterMax分别用于设置被检索消息的最小值和最大值)在投递消息队列中查找相应的消息,如果没有找到,将在输入消息队列中根据过滤条件继续查找。如果找到了一个消息,那么接收该消息(不是递交)并把该消息从对应的消息队列中删除(如果flags参数中包含PM_REMOVE标志)。如果不存在满足过滤条件的消息,那么PeekMessage函数将返回FALSE。
由上可见,在处理各类消息时所采用的策略是不同的。我们不可能过滤掉发送消息,这些消息将肯定会被处理。只能对投递消息和输入消息进行过滤。发送消息在PeekMessage函数内部被派发,而投递消息和输入消息将通过pmsg参数返回给调用者,并且由调用者来决定对这个消息进行什么样的处理(通常的处理是“派发这个消息”,不过也可以不这样做)。
GetMessage函数的处理流程与PeekMessage函数类似,只是GetMessage只有在获得了一个投递消息或者输入消息时才会返回。如果没有获得这样的消息,GetMessage将一直等下去,直到来了一个投递消息或输入消息:
BOOL GetMessage(LPMSG pmsg,HWND hwnd,UINT wMsgFilterMin,UINT wMsgFilterMax) { while(!PeekMessage(pmsg,hwnd,wMsgFilterMin,wMsgFilterMax,PM_REMOVE)){ WaitMessage(); } return pmsg->message != WM_QUIT; }
还有一种情况是线程间发送消息的派发。
SendToAnotherThread(…) { 将消息添加到接收者的发送消息队列中; //这两个函数将等待接收者响应 if(Function == SendMessage || Function == SendMessageTimeOut){ while(!收到了消息 && !超时){ DeliverIncomingSentMessage(); 等待新的消息或者超时(如果是SendMessageTimeOut); } } }
消息被添加到目标窗口所在线程的发送消息队列中;在调用了SendMessage和SendMessageTimeout的情况下,发送线程在派发发送消息后将等待目标窗口返回一个结果(或者消息超时)。
综上所述,只有在三种情况下才能派发发送消息:1.在PeekMessage中;2.在GetMessage中;3.线程间的SendMessage。
三、消息生命周期
1.发送消息
前面已经说到过,线程如果向自己的某个窗口发送消息,则接收窗口的窗口过程将被直接调用(这个消息既不进入消息队列,也不会进入消息泵)。
线程间发送消息,则被发送的消息将加入到目标窗口的发送消息队列中。这个消息将在发送消息队列中等待,直到目标窗口所在的线程通过执行PeekMessage,GetMessage或者线程间的SendMessage。当接收消息的线程从窗口过程中返回时,消息的处理结果将被返回到发送线程中。在得到返回结果后,发送线程将继续执行。
如果使用了ReplyMessage函数,那么消息生命周期可能会发生变化。如果接收线程在处理发送消息时调用了ReplyMessage,那么传递给ReplyMessage函数的参数值将被返回给发送线程,就好像接收线程从窗口过程中返回一样。因此,发送线程和接收线程将同时执行。发送线程执行是因为这个线程将不再需要等待消息的结果,而接收线程执行是因为这个线程仍然没有从窗口过程中返回(窗口过程的最终返回值将被忽略,因为接收线程已经向发送线程返回了一个值,并且程序也无法再回到那个时刻来改变那个已经返回的值)。
2.投递消息
投递消息被添加到目标窗口所在的线程的投递消息队列中。这个消息将在投递消息队列中持续等待,直到接收线程调用GetMessage或者PeekMessage函数将消息赋值到程序所提供的MSG结构中。接下来发生的动作将取决于接收这个消息的对象。
虽然,从理论上来说,程序可以对投递消息做任何操作。但是最可能的情况是在主消息泵中得到该消息,主消息泵的代码可能这样:
while (GetMessage(&msg, NULL, 0,0)){ if(!TranslateAccelerator(hwnd,hacc,&msg)){ TranslateMessage(&msg); DispatchMessage(&msg); } }
如果是上面的情况,那么这个消息将首先与加速键表进行比较,如果找到了一个匹配的加速键,将会递交一个WM_COMMAND(或者WM_SYSCOMMAND)消息到hwnd窗口中。在这种情况下,消息的处理过程就结束了,因此这个消息将永远都不会被投递到窗口过程中。
如果没有找到匹配的加速键,这个消息将通常由DispatchMessage函数递交到窗口过程,但也有例外的情况:
○ 如果是线程消息,那么就没有相应的目标窗口。因此,线程消息不会被派发。
○ 如果是WM_TIMER消息,并且消息的lParam参数不为空,那么lParam将被视为一个TIMEPROC回调函数,因此将直接调用这个函数。
3.生成的投递消息
前面分的三类消息并不能涵盖所有的消息,还有一些,我们姑且称之为“特殊类型的消息”。
WM_MOUSEMOVE消息就是其中的一个特殊消息。当鼠标移动的时候,这个消息并不是被作为输入消息添加到输入消息队列中,而是设置一个“鼠标已经移动”的标志。在窗口管理器查找输入消息时,如果发现设置了“鼠标已经移动”这个标志,那么窗口管理器将清除这个标志,并且动态地生成一个WM_MOUSEMOVE消息,然后再把这个消息添加到输入消息的队列中(或者与一个现有的WM_MOUSEMOVE消息合并在一起)。
其它属于这种“动态生成的”特殊消息分别是:WM_PAINT、WM_TIMER、和WM_QUIT。其中前两种消息甚至可以在消息查找过程完成之后再来生成,当没有发现可用的输入消息并且在消息过滤器中明确指出需要这种类型的消息时,才会生成这两种消息(生成WM_QUIT消息的时间甚至比生成窗口绘制消息和计时器消息的时间更晚,因为只有在投递消息队列为空的情况下才会生成这个消息。此外,WM_QUIT消息会忽略消息过滤的设置;也就是说,只要这个消息出现,就肯定会被处理)。
综合考虑前面说的各种情况,将PeekMessage写成下面这种更为复杂的形式:
BOOL PeekMessage(LPMSG pmsg, HWND hwnd, UINT wMsgFilterMin, UINT wMsgFilterMax, UINT flags) { DeliverIncomingSentMessage(); if(有某个投递消息满足过滤条件){ *pmsg = 这个消息; if(flags & PM_REMOVE) 将这个消息从投递消息队列中删除: return TURE; } //WM_QUIT消息将忽略过滤设置 if(有一个WM_QUIT消息正在等待处理,并且没有投递消息了){ 清除”有WM_QUIT消息正在等待处理”的标志; *pmsg = WM_QUIT消息; return TRUE; } if(鼠标移动了并且输入消息满足过滤条件){ 清除”鼠标已经移动”的标志; 将WM_MOUSEMOVE消息添加到输入消息队列中; } if(有某个输入消息满足过滤条件){ *pmsg = 这个消息; if(flags & PM_REMOVE) 从输入消息队列中删除这个消息; return TURE; } if(窗口需要进行绘制并且WM_PAINT消息满足过滤条件){ *pmsg = WM_PAINT消息; return TRUE; } if(计时器被触发了,并且WM_TIMER消息满足过滤条件){ 将一个WM_TIMER消息添加到投递消息队列中; *pmsg = 这个消息; if(flags & PM_REMOVE) 从投递消息队列中删除这个消息; return TRUE; } return FALSE; }
WM_MOUSEMOVE和WM_TIMER消息是非常有意思的:当需要生成这些消息时,它们才成为队列中正真的消息。如果程序在调用PeekMessage函数时使用了PM_NOREMOVE标志,那么这会导致一些微妙的问题。这些消息将停留在消息队列中,随后的鼠标移动消息将会和这个驻留的鼠标移动消息合并在一起。