Loading AI tools
来自维基百科,自由的百科全书
微軟視窗操作系統是以事件驅動做為程式設計的基礎。程式的執行緒會從作業系統獲取訊息。應用程式會不斷循環呼叫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;
}
Seamless Wikipedia browsing. On steroids.
Every time you click a link to Wikipedia, Wiktionary or Wikiquote in your browser's search results, it will show the modern Wikiwand interface.
Wikiwand extension is a five stars, simple, with minimum permission required to keep your browsing private, safe and transparent.