Windows 消息以及消息处理算法--线程和消息队列详解

Windows以消息驱动的方式,使得线程能够通过处理消息来响应外界。

Windows 为每个需要接受消息和处理消息的线程建立消息队列(包括发送消息队列,登记消息队列,输入消息队列,响应消息队列),其中发送消息队列保存其他线程通过SendMessage发送给该线程建立窗口的消息,登记消息队列保存通过PostMessage发送给该线程或者该线程建立窗口的消息,输入消息队列保存系统的输入(包括键盘,鼠标输入),响应消息队列包含该线程调用SendMessage给指定窗口的窗口函数处理完后通知该线程的信息。

线程和消息队列关系:

RIT+SHIQ构成了系统的硬件输入模型的核心

系统为每个线程维护一个消息队列,还维护一个全局的消息队列,称为系统硬件输入队列(SHIQ:SystemHanrwareInputQueue),用于存储系统中硬件出发的消息。(如鼠标、键盘等引发的消息)在系统初始化的时候会建立一个特殊的线程------原始输入线程(RIT:RawInputThread).

系统为线程建立消息队列,实际上就是分配一个THREADINFO结构的数据,使其与线程关联。

在THREADINFO结构中包含有登记消息队列的指针、虚拟输入队列指针、发送队列指针、应答消息队列指针、退出代码、唤醒标记和局部输入状态变量等信息。

Windows 消息以及消息处理算法--线程和消息队列详解

*   虚拟输入队列指针(Virtualized-input):接收接盘的等虚拟输入信息队列                                (就是一个指针数组)

    *   登记消息队列指针(Posted-Message):使用PostMessage函数发送的消息,将存放于此                      (就是一个指针数组)

    *   发送消息队列指针(Send-Message):SendMessage函数发送的消息存放位置                                (就是一个指针数组)

    *   应答消息队列指针(Reply-Message):使用SendMessage函数发送信息后,返回的信息存放于此

    *   nExitCode:确定线程退出状态,是一个int型,不同数值说明线程处于不同状态

    *   唤醒标志:判断是否处于唤醒状态

    *   局部输入状态变量:不详

PostMessage(HWND hWnd,UINT Msg,WPARAM wParam,LPARAM lParam):(SendMessage和PostMessage的原理差不多,只是SendMessage会等待窗口过程处理完了以后才返回)

在调用该函数时,系统首先确定是哪个线程创建了hWnd参数标识的窗口,然后系统分配一块内存区域,将消息信息(该函数的实参)存储在这块区域中,并将该区域的首地址添加到线程的登记消息队列中。

 

整体流程:(以鼠标消息为例WM_MOUSEMOVE)

1.用户移动鼠标产生事件,系统通过设备驱动程序将消息(系统先将鼠标事件封装成消息,即MSG结构体)放入SHIQ(就是把一个指向MSG结构体的32位地址放入虚拟输入队列指针中),此时RIT会唤醒,RIT通过当前鼠标光标之下的窗口,获取创建窗口的线程ID(通过GetWindowThreadProcessId函数实现),然后将鼠标消息放入到线程的虚拟输入队列中(就是把一个指向MSG结构体的32位地址放入虚拟输入队列指针中)

2.应用程序从虚拟输入队列指针中取消息,并将其回传给操作系统

3.由操作系统调用窗口过程(通过hWnd找到所属的窗口类,在窗口类中找到窗口过程的地址)

伪算法:

Windows通过QS_SENDMESSAGE、QS_POSTMESSAGE、QS_QUIT、QS_INPUT、QS_PAINT、QS_TIMER表示是否有发送消息、登记消息、退出消息、输入消息、重绘消息、定时消息。

消息的优先级是QS_SENDMESSAGE > QS_POSTMESSAGE > QS_QUIT > QS_INPUT > QS_PAINT > QS_TIMER。

Windows处理消息的方式大概是这样的:

消息循环伪算法:

BOOL bRet = FALSE;
MSG msg;
while ((bRet = GetMessage(&msg, NULL, 0, 0))) {
         if (bRet == -1) break; // On Error exit the loop
         TranslateMessage(&msg); //转换消息
         DispatchMessage(&msg); //发送消息,其实就是调用指定窗口的窗口函数
}
 
GetMessage伪算法如下:
BOOL GetMessage(MSG *lpMsg, HWND hWnd , UINT wMsgFilterMin, UINT wMsgFilterMax)
{
         //查看QS_SENDMESSAGE标志,如果有的话循环处理,直到没有消息位置
         DWORD dwRetVal = 0;
         ThreadInfo threadInfo;
 
FLAG_SENDPROCLOOP:
         GetThreadInfo(GetCurrentThreadId(), &threadInfo);
         while (threadInfo.QS_SENDMESSAGE == QS_SIGNALSET) {
                   //从发送消息队列中获取消息
                   dwReturnVal = GetMsgFromQueue(QUEUE_SEND, lpMsg, hWnd,wMsgFilterMin, wMsgFilterMax);
                   //判断是否取到消息,有则调用窗口函数,无则复为QS_SENDMESSAGE标志
                   If (dwReturnVal == GETMESSAGE_HASMESSAGE) {
                            //调用指定窗口的窗口函数
                            CallWindowProc(hWnd, &threadInfo, lpMsg);
                   }
                   else {
                            QS_SENDMESSAGE = QS_SIGNALRESET;
                            break;
                   }
         }
         //在继续处理之前再次检查发送消息队列
         if (threadInfo.QS_SENDMESSAGE == QS_SIGNALSET) goto FLAG_SENDPROCLOOP;
         //检查发送消息队列, 如果有消息则取发送消息
         //判断是否还有发送消息,没有了则复位QS_POSTMESSAGE标志
         if (threadInfo.QS_POSTMESSAGE == QS_SIGNALSET) {
                   dwReturnVal = GetMsgFromQueue(QUEUE_POST, lpMsg, hWnd, wMsgFilterMin, wMsgFilterMax);
                   if (dwReturnVal == GETMESSAGE_LASTMESSAGE)
                            threadInfo.QS_POSTMESSAGE = QS_SIGNALRESET;
                  
                   return TRUE;
         }       
 
         //如果退出标志被置位
         if (threadInfo.QS_QUIT == QS_SIGNALSET) {
                   threadInfo.QS_QUIT = QS_SIGNALRESET;
                   FillMessage(lpMsg, MESSAGE_QUIT);
                   return FALSE;
         }
 
         //检查输入消息队列
         if (threadInfo.QS_INPUT == QS_SIGNALSET) {
                   DWORD dwRetVal = GetMessageFromQueue(QUEUE_INPUT, lpMsg, hWnd, wMsgFilterMin, wMsgFilterMax);
                   //检查是否有键盘,鼠标消息
                   if (Test(dwRetVal, QS_KEY) == QS_LASTMOUSEKEYMESSAGE)
                            threadInfo.QS_KEY = QS_SIGNALRESET;
                   if (Test(dwRetVal, QS_MOUSEBUTTON) == QS_LASTMOUSEMESSAGE)
                            threadInfo.QS_MOUSEBUTTON = QS_SIGNALRESET;
 
                   return TRUE;
         }
 
         //测试QS_PAINT
         if (threadInfo.QS_PAINT == QS_SIGNALSET) {
                   //填充MSG,如果没有窗口过程确认窗口,则复位QS_PAINT标志
                   //...
                   //返回TRUE
                   threadInfo.QS_PAINT = QS_SIGNALRESET;
                   return TRUE;
         }
 
         if (threadInfo.QS_TIMER == QS_SIGNALSET) {
                   //填充MSG,如果没有定时器报时,则复位QS_TIMER标志
                   //...
                   //返回TRUE
                   return TRUE;
         }
 
         //等待有消息到达
         dwRetVal = MsgWaitForMultipleObjectsEx(...);
         if (...)
                   goto FLAG_SENDPROCLOOP;
 
         //等待失败
         return FALSE;
}

上面要注意的是各种消息被处理的优先级顺序,在发送队列中有发送消息时,GetMessage不返回,直到将发送队列中消息处理完毕为止,然后复位QS_SENDMESSAGE,没有发送消息时,GetMessage才查看登记消息,如果没有登记消息,则依着优先级从高到低的顺序依次处理各种消息。 如果此过程中发现了优先级低的消息,则GetMessage填充一个MSG,然后返回。如果是QS_QUIT被置位,则GetMessage返回FALSE,否则返回TRUE。 当GetMessage返回FALSE时,消息循环也就结束了。看消息循环可知,当消息循环再次调用GetMessage时,依然按照优先级顺序依次处理各种消息。请注意SendMessage发送到目标线程消息队列的消息在目标线程调用GetMessage时被处理掉,直到没有发送消息为止GetMessage才回去查询其他消息,如果有消息GetMessage取到消息返回,否则GetMessage使得线程陷入IDLE状态,被挂起,当有消息到达线程时GetMessage被唤醒,获取消息返回。

上一篇:7款个性化jQuery/HTML5地图插件


下一篇:[转]CSS Display(显示) 与 Visibility(可见性)