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.