微軟視窗操作系统是以事件驅動做為程式設計的基礎。程式的執行緒会从作業系統获取訊息。應用程式會不斷循环呼叫GetMessage函式(或是PeekMessage函式)來接收這些訊息,這個循环稱之為“事件迴圈”。基本上事件迴圈的程式碼如下所示(C語言 / C++程式語言):
MSG msg; //用于存储一条消息BOOL bRet;//从UI线程消息队列中取出一条消息while( (bRet = GetMessage( &msg, NULL, 0, 0 )) != 0){ if (bRet == -1) { //错误处理代码,通常是直接退出程序 } else { TranslateMessage(&msg); //按键消息转换为字符消息 DispatchMessage(&msg); //分发消息给相应的窗体 }}
雖然在程序上並沒有很嚴格的規定與要求,但是一般來說,它的事件迴圈通常會呼叫TranslateMessage函式與DispatchMessage函式,這兩個函式會傳遞訊息給回呼函式,以及调用相应視窗的消息处理函数。
現在的繪圖介面架構程式設計,例如Visual Basic與Qt基本上是不會要求應用程式直接拥有視窗程式的訊息迴圈,但是會以鍵盤與滑鼠的按鍵動作來作為事件的處理機制。在這些架構底下,訊息迴圈的痕迹還是可以被找到的。
注意:在上述的原始碼裡,尤其在while迴圈大於零的條件。即使GetMessage函式的傳回值型態是英文字大寫的BOOL,但是在Win32視窗程式裡,它是被定義成int整數型態,它有兩個值,TRUE是整數的1,FALSE是整數的0。整數 -1代表error(例如第二个参数为输出的窗口句柄但取不到值的时候),整數0值当GetMessage获取到WM_QUIT訊息。假如有其他訊息,那麼非零值會當成傳回值(有訊息的傳回值通常是正值,但是有些程式設計的說明文件不一定會說明的很詳細[1][2])。
16位Windows系统为非抢先单线程模式,应用程序没有发送消息队列,向窗口发送一个消息总是按同步方式执行,也即发送程序要在接受消息的窗口完全处理完消息之后才能继续运行。这通常是一个所期望的特性。但是,如果接收消息的窗口花很长的时间来处理消息或者出现挂起,则发送程序就不能再执行。这意味着系统是不强壮的。[3]如果应用程序消息队列(只用于存放投寄的消息)为空,由于没有虚拟输入消息队列,SendMessage或PeekMessage函数访问系统事件队列查取可用的鼠标或键盘输入消息。如果系统队列中没有需要处理的事件,SendMessage或PeekMessage函数扫描所有窗口以处理需要修改重绘的区域。如果没有需要重绘的区域,则交出CPU控制权。恢复CPU控制权时,查看是否有定时器过期。至此如果没有消息可返回,SendMessage进入睡眠,直至被输入事件唤醒;PeekMessage如果没有设置PM_NOYIELD标记,则会让出CPU控制权,但不会让线程休眠,重新获得CPU后PeekMessage将控制权返回到线程,并返回一个空值指出这个线程没有要处理的消息了。
本文主要关注Win32系统的消息处理机制。
Windows系统规定,窗口和钩子(hook)这两种User对象分别由建立窗口和安装钩子的线程所拥有,一旦该线程结束,操作系统会自动删除窗口或卸载钩子。而其他的User对象(图标icon、光标cursor、窗口类WndClass、菜单、加速键表等)则归进程所有,进程结束时操作系统会自动删除这些对象。
建立窗口的线程必须就是处理窗口所有消息的线程,即UI线程(User Interface Thread)创建了窗体及窗体上的各种控件,系统为UI线程分配一个消息队列用于窗口消息的派送(dispatch)。为了使窗口处置这些消息,线程必须有它自己的“消息循环”。只有当一个线程调用Windows API中的GDI(Graphics Device Interface)和User函数时,操作系统才会将其看成是一个UI线程,并为它分配一些另外的资源,创建一套线程消息队列;否则,操作系统把非UI线程视作普通工作线程(Workhorse),不会为它创建消息队列。因此,调用PostThreadMessage前,这个线程必须是UI线程从而有投寄消息的队列,通常可在该线程中调用一次PeekMessage函数以达到这个目的。
如果一个UI线程结束运行,操作系统会自动回收它所创建的所有窗体。
窗体过程(Window Procedure)是一个函数,每个窗体有一个窗体过程,负责处理该窗体的所有消息。
UI控件也是独立的“Window”,拥有自己的“窗体过程”。
Windows操作系统的内核空间中有一个系统消息队列(system message queue),在内核空间中还为每个UI线程分配各自的线程消息队列(Thread message queue)。在发生输入事件之后,Windows操作系统的输入设备驱动程序将输入事件转换为一个「消息」投寄到系统消息队列;操作系统的一个专门线程从系统消息队列取出消息,分发到各个UI线程的输入消息队列中。
每个UI线程的线程信息块TIB分配一个THREADINFO的结构,该结构包含一族成员变量,包括:[4]
TranslateMessage
把按键消息转化为字符消息,如WM_KEYDOWN转化为WM_CHAR,然后放入线程的虚拟输入消息队列中,成为下一个待处理的键盘消息。应用程序的每个UI线程中有一段称之为“消息循环”的代码,通过GetMessage系统调用(或是PeekMessage系统调用)访问系统空间中的对应的UI线程的消息队列,并依照下述次序处理:
需要注意的是,GetMessage如果在应用程序消息队列未获取消息,则GetMessage调用不返回,该线程挂起,CPU使用权交给操作系统。即GetMessage为阻塞调用。
由此可见,Windows的事件驱动模式,并不是操作系统把消息主动分发给应用程序;而是由应用程序的每个UI线程通过“消息循环”代码从UI线程消息队列获取消息。
TranslateMessage
处理后该函数在线程消息队列投寄(post)相应的字符消息,wParam参数是ASCII或Unicode的character code;这取决于RegisterClass函数是A或W版;IsWindowUnicode函数判断窗口过程会接受哪种编码。产生字符消息的按键有:任何字符键、回退键(BACKSPACE)、回车键(carriage return)、ESC、SHIFT + ENTER(linefeed换行)、TAB。因为TranslateMessage函数从WM_KEYDOWN和WM_SYSKEYDOWN消息产生了字符消息,所以字符消息是夹在按键消息之间传递给窗口消息处理程序的。如果使用者按住一个键不放,会自动重复产生一系列的WM_KEYDOWN消息;对每条WM_KEYDOWN消息,都会得到一条字符消息。如果某些WM_KEYDOWN消息的重复计数大于1,那么相应的WM_CHAR消息将具有同样的重复计数。TranslateMessage
函数处理“死键”(dead key)的WM_KEYUP消息,向具有输入焦点的窗口投寄(post)出WM_DEADCHAR消息。死键是产生附加符号的按键。例如在德语键盘,锐音符被按下、释放后,再按下A,将获得字母á的WM_CHAR。如果在死键之后跟有不能带此附件符号的字母(例如锐音符后跟「s」),那么将接收到两条WM_CHAR消息:前一个消息的wParam等于附加符号本身的ASCII码(与传递到WM_DEADCHAR消息的wParam值相同),第二个消息的wParam等于字母的ASCII代码。键盘输入时需要明确插入符位置,相关API函数为:CreateCaret、SetCaretPos、ShowCaret、HideCaret、DestroyCaret、GetCaretPos、GetCaretBlinkTime、SetCaretBlinkTime。
3个获得键状态的函数:GetKeyState、GetAsyncKeyState、GetKeyboardState。
对于自定义的控件,当单击子窗口时,父窗口会得到焦点。但对于标准子窗口控件,单击时会自动获得焦点(子窗口过程在WM_LBUTTONDOWN中实现了SetFocus(hwnd))。如果一个子窗口拥有输入焦点,鼠标单击另一个兄弟子窗口,则兄弟子窗口获得输入焦点。
Windows API函数SendMessage是个同步调用,即它发出的Windows消息没被处理完之前这个函数就不返回。但这个函数不是阻塞的。分两种情形:[5]
异步发送或投寄消息的函数,如PostMessage、SendMessageCallback、SendNotifyMessage,消息参数中不能使用指针,否则函数调用失败。
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;}