Windows核心编程 第27章 硬件输入模型和局部输入状态
第27章?硬件輸入模型和局部輸入狀態
? ? 這章說的是按鍵和鼠標事件是如何進入系統并發送給適當的窗口過程的。微軟設計輸入模型的一個主要目標就是為了保證一個線程的動作不要對其他線程的動作產生不好的影響。
27.1?原始輸入線程
? ? 當系統初始化時,要建立一個特殊的線程,即原始輸入線程(raw?input?thread,R?I?T)。此外,系統還要建立一個隊列,稱為系統硬件輸入隊列(System?hardware?input?queue,?SHIQ)。R?I?T和S?H?I?Q構成系統硬件輸入模型的核心。
?
? ? R?I?T怎么才能知道要向哪一個線程的虛擬輸入隊列里增加硬件輸入消息?對鼠標消息,R?I?T只是確定是哪一個窗口在鼠標光標之下。利用這個窗口,?R?I?T調用G?e?t?Wi?n?d?o?w?T?h?r?e?a?dP?r?o?c?e?s?s?I?d來確定是哪個線程建立了這個窗口。返回的線程?I?D指出哪一個線程應該得到這個鼠標消息。
對按鍵硬件事件的處理稍有不同。在任何給定的時刻,只有一個線程同?R?I?T“連接”。這個線程稱為前景線程(foreground?thread),因為它建立了正在與用戶交互的窗口,并且這個線程的窗口相對于其他線程所建立的窗口來說處在畫面中的前景。
? ? 當一個用戶在系統上登錄時,?Windows?Explorer進程讓一個線程建立相應的任務欄(t?a?s?k?b?a?r)和桌面。這個線程連接到R?I?T。如果你又要產生C?a?l?c?u?l?a?t?o?r,那么就又有一個線程來建立一個窗口,并且這個線程變成連接到?R?I?T的線程。注意現在Windows?Explorer的線程不再與R?I?T連接,因為在一個時刻只能有一個線程同R?I?T連接。當一個按鍵消息進入S?H?I?Q時,R?I?T就被喚醒,將這個事件轉換成適當的按鍵消息,并將消息放入與R?I?T連接的線程的虛擬輸入隊列。
? ? 不同的線程是如何連接到R?I?T的呢?我們已經說過,當產生一個進程時,這個進程的線程可以建立一個窗口。這個窗口處于前景,其建立窗口的線程同?R?I?T相連接。另外,R?I?T還要負責處理特殊的鍵組合,如A?l?t?+?Ta?b、A?l?t?+?E?s?c和C?t?r?l?+?A?l?t?+?D?e?l等。因為R?I?T在內部處理這些鍵組合,就可以保證用戶總能用鍵盤激活窗口。應用程序不能夠攔截和廢棄這些鍵組合。當用戶按動了某個特殊的鍵組合時,R?I?T激活選定的窗口,并將窗口的線程連接到R?I?T。Wi?n?d?o?w?s也提供激活窗口的功能,使窗口的線程連接到R?I?T。
? ? 從上面的圖中可以看到如何保護線程,避免相互影響的。如果?R?I?T向窗口?B?1?或窗口?B?2?發送一個消息,消息到達線程?B的虛擬輸入隊列。在處理消息時,線程?B在與五個內核對象同步時可能會進入死循環或死鎖。如果發生這種情況,線程仍然同?R?I?T連接在一起,并且可能有更多的消息要增加到線程的虛擬輸入隊列中。
這種情況下,用戶會發現窗口?B?1和B?2都沒有反應,可能想切換到窗口?A?1?。為了做這種切換,用戶按A?l?t?+?Ta?b。因為是R?I?T處理A?l?t?+?Ta?b按鍵組合,所以用戶總能切換到另外的窗口,不會有什么問題。在選定窗口?A?1?之后,線程?A就連接到R?I?T。這個時候,用戶就可以對窗口?A?1?進入輸入,盡管線程及其窗口都沒有響應。
27.2?局部輸入狀態
??哪一個窗口有鼠標捕獲。
??鼠標光標的形狀。
??鼠標光標的可見性。
????由于每個線程都有自己的輸入狀態變量,每個線程都有不同的焦點窗口、鼠標捕獲窗口等概念。從一個線程的角度來看,或者它的某個窗口擁有鍵盤焦點,或者系統中沒有窗口擁有鍵盤焦點;或者它的某個窗口擁有鼠標捕獲,或者系統中沒有窗口擁有鼠標捕獲,等等。
27.2.1?鍵盤輸入與焦點
???R?I?T使用戶的鍵盤輸入流向一個線程的虛擬輸入隊列,而不是流向一個窗口。R?I?T將鍵盤事件放入線程的虛擬輸入隊列時不用涉及具體的窗口。當這個線程調用G?e?t?M?e?s?s?a?g?e時,鍵盤事件從隊列中移出并分派給當前有輸入焦點的窗口。(由該線程所建立)。下圖說明了這個處理過程。
? ? 線程1當前正在從R?I?T接收輸入,用窗口A、窗口B或窗口C的句柄作參數調用S?e?t?F?o?c?u?s會引起焦點改變。失去焦點的窗口除去它的焦點矩形或隱藏它的插入符號,獲得焦點的窗口畫出焦點矩形或顯示它的插入符號。
? ? 假定線程1仍然從R?I?T接收輸入,并用窗口?E的句柄作為參數調用?S?e?t?F?o?c?u?s。這種情況下,系統阻止執行這個調用,因為想要設置焦點的窗口不使用當前連接?R?I?T的虛擬輸入隊列。在線的線程不一樣,那么,對于建立失去焦點窗口的線程,要更新它的局部輸入狀態變量,說明它沒有窗口擁有焦點。這時調用G?e?t?F?o?c?u?s將返回N?U?L?L,這會使線程知道當前沒有窗口擁有焦點。
????函數S?e?t?A?c?t?i?v?e?Wi?n?d?o?w激活系統中一個最高層(?t?o?p?-?l?e?v?e?l)的窗口,并對這個窗口設定焦點:
HWND?WINAPI?SetActiveWindow(__in?HWND?hWnd);
同S?e?t?F?o?c?u?s函數一樣,如果調用線程沒有創建作為函數參數的窗口,則這個函數什么也不做。
與S?e?t?A?c?t?i?v?e?Wi?n?d?o?w相配合的函數是G?e?t?A?c?t?i?v?e?Wi?n?d?o?w函數:
HANDLE?GetActiveWindow();
? ? 這個函數的功能同G?e?t?F?o?c?u?s函數差不多,不同之處是它返回由調用線程的局部輸入狀態變量所指出的活動窗口的句柄。當活動窗口屬于另外的線程時,?G?e?t?A?c?t?i?v?e?Wi?n?d?o?w返回N?U?L?L。
? ? 其他可以改變窗口的?Z序(Z?-?o?r?d?e?r)、活動狀態和焦點狀態的函數還包括?B?r?i?n?g?Wi?n?d?o?w?ToTo?p和S?e?t?Wi?n?d?o?w?P?o?s:
?
BOOL?WINAPI?BringWindowToTop(__in?HWND?hWnd);
?
BOOL?WINAPI?SetWindowPos(
????_In_?HWND?hWnd,
????_In_opt_?HWND?hWndInsertAfter,
????_In_?int?X,
????_In_?int?Y,
????_In_?int?cx,
????_In_?int?cy,
????_In_?UINT?uFlags);
? ? 這兩個函數功能相同(實際上,?B?r?i?n?g?Wi?n?d?o?w?To?To?p函數在內部調用?S?e?t?Wi?n?d?o?w?P?o?s,以H?W?N?D?_?TO?P作為第二個參數)。如果調用這兩個函數的線程沒有連接到?R?I?T,則函數什么也不做。如果調用這些函數的線程同?R?I?T相連接,系統就會激活相應的窗口。注意即使調用線程不是建立這個窗口的線程,也同樣有效。這意味著,這個窗口變成活動的,并且建立這個窗口的線程被連接到R?I?T。這也引起調用線程和新連接到R?I?T的線程的局部輸入狀態變量被更新。
? ? 有時候,一個線程想讓它的窗口成為屏幕的前景。例如,有可能會利用?Microsoft?Qutlook
安排一個會議。在會議開始前的半小時,?O?u?t?l?o?o?k彈出一個對話框提醒用戶會議將要開始。如果Q?u?t?l?o?o?k的線程沒有連接到R?I?T,這個對話框就會藏在其他窗口的后面,有可能看不見它。
? ? 為了制止這種現象,微軟對?S?e?t?F?o?r?e?g?r?o?u?n?d?Wi?n?d?o?w函數增加了更多的智能。特別規定,僅當調用一個函數的線程已經連接到?R?I?T或者當前與R?I?T相連接的線程在一定的時間內(這個時間量由S?y?s?t?e?m?P?a?r?a?m?e?t?e?r?s?I?n?f?o函數和S?P?I?_?S?E?T?F?O?R?E?G?R?O?U?N?D?_?L?O?C?K?T?I?M?E?O?U?T值來控制)沒有收到任何輸入,這個函數才有效。另外,如果有一個菜單是活動的,這個函數就失效。
? ? 如果不允許S?e?t?F?o?r?e?g?r?o?u?n?d?Wi?n?d?o?w將窗口移到前景,它會閃爍該窗口的標題欄和任務條上該窗口的按鈕。用戶看到任務條按鈕閃爍,就知道該窗口想得到用戶的注意。用戶應該手工激活這個窗口,看一看要報告什么信息。還可以用S?y?s?t?e?m?P?a?r?a?m?e?t?e?r?s?I?n?f?o函數和S?P?I?_?S?E?T?F?O?R?E?G?R?O?U?N?D?-F?L?A?S?H?C?O?U?N?T值來控制閃爍。
? ? 由于這些新的內容,系統又提供了另外一些函數。如果調用?A?l?l?o?w?S?e?t?F?o?r?e?g?r?o?u?n?d?Wi?n?d?o?w的線程能夠成功調用S?e?t?F?o?r?e?g?r?o?u?n?d?Wi?n?d?o?w,第一個函數(見下面所列)可使指定進程的一個線程成功調 ? ?用S?e?t?F?o?r?e?g?r?o?u?n?d?Wi?n?d?o?w。為了使任何進程都可以在你的線程的窗口上彈出一個窗口,指定A?S?F?W?_?A?N?Y?(定義為-1?)作為d?w?P?r?o?c?e?s?s?I?d參數:
? ? 此外,線程可以鎖定?S?e?t?F?o?r?e?g?r?o?u?n?d?Wi?n?d?o?w函數,使它總是失效的。方法是調用?L?o?c?kS?e?t?F?o?r?e?g?r?o?u?n?d?Wi?n?d?o?w。
BOOL?LockSetForegroundWindow(UINT?uLockCode);
? ? 對u?L?o?c?k?C?o?d?e參數可以指定L?S?F?W?_?L?O?C?K或者L?S?F?W?_?U?N?L?O?C?K。當一個菜單被激活時,系統在內部調用這個函數,這樣一個試圖跳到前景的窗口就不能關閉這個菜單。?Wi?n?d?o?w?sE?x?p?l?o?r?e?r在顯示S?t?a?r?t菜單時,需要明確地調用這些函數,因為?S?t?a?r?t菜單不是一個內置菜單。當用戶按了A?l?t鍵或者將一個窗口拉到前景時,系統自動解鎖?S?e?t?F?o?r?e?g?r?o?u?n?d?Wi?n?d?o?w函數。這可以防止一個程序一直對S?e?t?F?o?r?e?g?r?o?u?n?d?Wi?n?d?o?w函數封鎖。
? ? 關于鍵盤管理和局部輸入狀態,其他的內容是同步鍵狀態數組。每個線程的局部輸入狀態變量都包含一個同步鍵狀態數組,但所有的線程要共享一個同步鍵狀態數組。這些數組反映了在任何給定時刻鍵盤所有鍵的狀態。利用?G?e?t?A?s?y?n?c?K?e?y?S?t?a?t?e函數可以確定用戶當前是否按下了鍵盤上的一個鍵:
SHORT?WINAPI?GetAsyncKeyState(__in?int?vKey);
? ? 參數n?Vi?r?t?K?e?y指出要檢查鍵的虛鍵代碼。結果的高位指出該鍵當前是否被按下(是為?1,否為0)。筆者在處理一個消息時,常用這個函數來檢查用戶是否釋放了鼠標主按鈕。為函數參數賦一個虛鍵值V?K?_?L?B?U?T?TO?N,并等待返回值的高位成為0。注意,如果調用函數的線程不是建立的窗口上,鼠標光標就可見了。
鼠標光標管理的另一個方面是使用C?l?i?p?C?u?r?s?o?r函數將鼠標光標剪貼到一個矩形區域。
BOOL?ClipCursor(CONST?RECT?*prc);
? ? 這個函數使鼠標被限制在一個由p?r?c參數指定的矩形區域內。當一個程序調用?C?l?i?p?C?u?r?s?o?r函數時,系統該做些什么呢?允許剪貼鼠標光標可能會對其他線程產生不利影響,而不允許剪貼鼠標光標又會影響調用線程。微軟實現了一種折衷的方案。當一個線程調用這個函數時,系統將鼠標光標剪貼到指定的矩形區域。但是,如果同步激活事件發生(當用戶點擊了其他程序的窗口,調用了S?e?t?F?o?r?e?g?r?o?u?n?d?Wi?n?d?o?w,或按了C?t?r?l?+?E?s?c組合鍵),系統停止剪貼鼠標光標的移動,允許鼠標光標在整個屏幕上自由移動。
27.3?將虛擬輸入隊列同局部輸入狀態掛接在一起
?????從上面的討論我們可以看出這個輸入模型是強壯的,因為每個線程都有自己的局部輸入狀態環境,并且在必要時每個線程可以連接到?R?I?T或從R?I?T斷開。有時候,我們可能想讓兩個或多個線程共享一組局部輸入狀態變量及一個虛擬輸入隊列。
可以利用A?t?t?a?c?h?T?h?r?e?a?d?I?n?p?u?t函數來強制兩個或多個線程共享同一個虛擬輸入隊列和一組局部輸入狀態變量:
BOOL?WINAPI?AttachThreadInput(
????__in?DWORD?idAttach,
__in?DWORD?idAttachT);
? ? 函數的第一個參數i?d?A?t?t?a?c?h,是一個線程的I?D,該線程所包含的虛擬輸入隊列(以及局部輸入狀態變量)是你不想再使用的。第二個參數?i?d?A?t?t?a?c?h?To,是另一個線程的I?D,這個線程所包含的虛擬輸入隊列(和局部輸入狀態變量)是想讓兩個線程共享的。第三個參數?f?A?t?t?a?c?h,當想讓共享發生時,被設置為?T?R?U?E,當想把兩個線程的虛擬輸入隊列和局部輸入狀態變量分開時,設定為FA?L?S?E??梢酝ㄟ^多次調用A?t?t?a?c?h?T?h?r?e?a?d?I?n?p?u?t函數讓多個線程共享同一個虛擬輸入隊列和局部輸入狀態變量。
? ? 我們再考慮前面的例子,假定線程?A調用A?t?t?a?c?h?T?h?r?e?a?d?I?n?p?u?t,傳遞線程?A的I?D作為第一個參數,線程B的I?D作為第二個參數,T?R?U?E作為最后一個參數:
線程?A的虛擬輸入隊列將不再接收輸入事件,除非再一次調用A?t?t?a?c?h?T?h?r?e?a?d?I?n?p?u?t并傳遞FA?L?S?E作為最后一個參數,將兩個線程的輸入隊列分開。
? ? 當將兩個線程的輸入都掛接在一起時,就使線程共享單一的虛擬輸入隊列和同一組局部輸入狀態變量。但線程仍然使用自己的登記消息隊列、發送消息隊列、應答消息隊列和喚醒標志(見第2?6章的討論)。
? ? 如果讓所有的線程都共享一個輸入隊列,就會嚴重削弱系統的強壯性。如果某一個線程接收一個按鍵消息并且掛起,其他的線程就不能接收任何輸入了。所以應該盡量避免使用A?t?t?a?c?h?T?h?r?e?a?d?I?n?p?u?t函數。在某些情況下,系統隱式地將兩個線程掛接在一起。第一種情況是當一個線程安裝一個日志記錄掛鉤(journal?record?hook)或日志播放掛鉤(journal?playback?hook)的時候。當掛鉤被卸載時,系統自動恢復所有線程,這樣線程就可以使用掛鉤安裝前它們所使用的相同輸入隊列。
? ? 當一個線程安裝一個日志記錄掛鉤時,它是讓系統將用戶輸入的所有硬件事件都通知它。這個線程通常將這些信息保存或記錄在一個文件上。因用戶的輸入必須按進入的次序來記錄,所以系統中每個線程要共享一個虛擬輸入隊列,使所有的輸入處理同步。
? ? 還有一些情況,系統會代替你隱式地調用?A?t?t?a?c?h?T?h?r?e?a?d?I?n?p?u?t。假定你的程序建立了兩個線程。第一個線程建立了一個對話框。在這個對話框建立之后,第二個線程調用?G?r?e?a?t?Wi?n?d?o?w,使用W?S?_?C?H?I?L?D風格,并向這個子窗口的雙親傳遞對話框的句柄。系統用子窗口的線程調用A?t?t?a?c?h?T?h?r?e?a?d?I?n?p?u?t,讓子窗口的線程使用對話框線程所使用的輸入隊列。這樣就使對話框的所有子窗口之間對輸入強制同步。
總結
以上是生活随笔為你收集整理的Windows核心编程 第27章 硬件输入模型和局部输入状态的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Windows核心编程 第26章 窗口消
- 下一篇: WindowsPE 第七章 资源表