回炉重造之重读Windows核心编程-027-硬件输入模型和局部输入状态
第27章 硬件輸入模型和局部輸入狀態
現在起開始討論系統的硬件輸入模型。重點將考察按鍵和鼠標事件是如何進入系統并發送給適當的窗口過程的。微軟設計輸入模型的一個主要目標就是為了保證一個線程的動作不要對其他線程的動作產生不好的影響。健壯的系統,不會使一個掛起的線程妨礙其他的線程接收硬件的輸入。
27.1 原始輸入線程
當系統初始化時,要建立一個特殊的線程(rawinputthread,RIT),以及一個隊列,稱為系統硬件輸入隊列(Systemhardwareinputqueue,SHIQ)。RIT和SHIQ就構成了系統硬件輸入模型的核心。
那么RIT怎么才能知道要向哪一個線程的虛擬輸入隊列里增加硬件輸入消息?對鼠標消息,RIT只是確定是哪一個窗口在鼠標光標之下。利用這個窗口,RIT調用GetWindowThreadProcessId來確定是哪個線程建立了這個窗口。返回的線程ID指出哪一個線程應該得到這個鼠標消息。
對按鍵硬件事件的處理稍有不同。在任何給定的時刻,只有一個線程同RIT“連接”。這個線程稱為前景線程(foregroundthread),因為它建立了正在與用戶交互的窗口,并且這個線程的窗口相對于其他線程所建立的窗口來說處在畫面中的前景。
當一個用戶在系統上登錄時,WindowsExplorer進程讓一個線程建立相應的任務欄(taskbar)和桌面。這個線程連接到RIT。如果你又要產生Calculator,那么就又有一個線程來建立一個窗口,并且這個線程變成連接到RIT的線程。注意現在WindowsExplorer的線程不再與RIT連接,因為在一個時刻只能有一個線程同RIT連接。當一個按鍵消息進入SHIQ時,RIT就被喚醒,將這個事件轉換成適當的按鍵消息,并將消息放入與RIT連接的線程的虛擬輸入隊列。
不同的線程是如何連接到RIT的呢?我們已經說過,當產生一個進程時,這個進程的線程可以建立一個窗口。這個窗口處于前景,其建立窗口的線程同RIT相連接。另外,RIT還要負責處理特殊的鍵組合,如Alt+Tab、Alt+Esc和Ctrl+Alt+Del等。因為RIT在內部處理這些鍵組合,就可以保證用戶總能用鍵盤激活窗口。應用程序不能夠攔截和廢棄這些鍵組合。當用戶按動了某個特殊的鍵組合時,RIT激活選定的窗口,并將窗口的線程連接到RIT。Windows也提供激活窗口的功能,使窗口的線程連接到RIT。這些功能在本章后面討論。
如果RIT向窗口B1或窗口B2發送一個消息,消息到達線程B的虛擬輸入隊列。在處理消息時,線程B在與五個內核對象同步時可能會進入死循環或死鎖。如果發生這種情況,線程仍然同RIT連接在一起,并且可能有更多的消息要增加到線程的虛擬輸入隊列中。
這種情況下,用戶會發現窗口B1和B2都沒有反應,可能想切換到窗口A1。為了做這種切換,用戶按Alt+Tab。因為是RIT處理Alt+Tab按鍵組合,所以用戶總能切換到另外的窗口,不會有什么問題。在選定窗口A1之后,線程A就連接到RIT。這個時候,用戶就可以對窗口A1進入輸入,盡管線程及其窗口都沒有響應。
27.2 局部輸入狀態
由于每個線程都有自己的輸入狀態變量,每個線程都有不同的焦點窗口、鼠標捕獲窗口等概念。從一個線程的角度來看,或者它的某個窗口擁有鍵盤焦點,或者系統中沒有窗口擁有鍵盤焦點;或者它的某個窗口擁有鼠標捕獲,或者系統中沒有窗口擁有鼠標捕獲,等等。讀者會想到,這種隔離應該有一些細節,對此我們將在后面討論。
27.2.1 鍵盤輸入與焦點
已經知道,RIT使用戶的鍵盤輸入流向一個線程的虛擬輸入隊列,而不是流向一個窗口。RIT將鍵盤事件放入線程的虛擬輸入隊列時不用涉及具體的窗口。當這個線程調用GetMessage時,鍵盤事件從隊列中移出并分派給當前有輸入焦點的窗口。
線程1當前正在從RIT接收輸入,用窗口A、窗口B或窗口C的句柄作參數調用SetFocus會引起焦點改變。失去焦點的窗口除去它的焦點矩形或隱藏它的插入符號,獲得焦點的窗口畫出焦點矩形或顯示它的插入符號。
假定線程1仍然從RIT接收輸入,并用窗口E的句柄作為參數調用SetFocus。這種情況下,系統阻止執行這個調用,因為想要設置焦點的窗口不使用當前連接RIT的虛擬輸入隊列。線程不一樣,那么,對于建立失去焦點窗口的線程,要更新它的局部輸入狀態變量,說明它沒有窗口擁有焦點。這時調用GetFocus將返回NULL,這會使線程知道當前沒有窗口擁有焦點。
函數SetActiveWindow激活系統中一個最高層(top-level)的窗口,并對這個窗口設定焦點:
HWND SetActiveWindow(HWND hwnd);
同SetFocus函數一樣,如果調用線程沒有創建作為函數參數的窗口,則這個函數什么也不做。
與SetActiveWindow相配合的函數是GetActiveWindow函數:
HWND GetActiveWindow(HWND hwnd);
這個函數的功能同GetFocus函數差不多,不同之處是它返回由調用線程的局部輸入狀態變量所指出的活動窗口的句柄。當活動窗口屬于另外的線程時,GetActiveWindow返回NULL。
其他可以改變窗口的Z序(Z-order)、活動狀態和焦點狀態的函數還包括BringWindowToTop和SetWindowPos:
BOOL BringWindowToTop(HWND hwnd);
BOOL SetWindowsPos(
HWND hwnd,
HWND hwndInsertAfter,
int x,int y,
int cx,int cy,
UINT fuFlags);
這兩個函數功能相同(實際上,BringWindowToTop函數在內部調用SetWindowPos,以HWND_TOP作為第二個參數)。如果調用這兩個函數的線程沒有連接到RIT,則函數什么也不做。如果調用這些函數的線程同RIT相連接,系統就會激活相應的窗口。注意即使調用線程不是建立這個窗口的線程,也同樣有效。這意味著,這個窗口變成活動的,并且建立這個窗口的線程被連接到RIT。這也引起調用線程和新連接到RIT的線程的局部輸入狀態變量被更新。
有時候,一個線程想讓它的窗口成為屏幕的前景。例如,有可能會利用MicrosoftQutlook安排一個會議。在會議開始前的半小時,Outlook彈出一個對話框提醒用戶會議將要開始。如果Qutlook的線程沒有連接到RIT,這個對話框就會藏在其他窗口的后面,有可能看不見它。因為了制止這種現象,微軟對SetForegroundWindow函數增加了更多的智能。特別規定,僅當調用一個函數的線程已經連接到RIT或者當前與RIT相連接的線程在一定的時間內(這個時間量由SystemParametersInfo函數和SPI_SETFOREGROUND_LOCKTIMEOUT值來控制)沒有收到任何輸入,這個函數才有效。另外,如果有一個菜單是活動的,這個函數就失效。
如果不允許SetForegroundWindow將窗口移到前景,它會閃爍該窗口的標題欄和任務條上該窗口的按鈕。用戶看到任務條按鈕閃爍,就知道該窗口想得到用戶的注意。用戶應該手工激活這個窗口,看一看要報告什么信息。還可以用SystemParametersInfo函數和SPI_SETFOREGROUND-FLASHCOUNT值來控制閃爍。
由于這些新的內容,系統又提供了另外一些函數。如果調用AllowSetForegroundWindow的線程能夠成功調用SetForegroundWindow,第一個函數(見下面所列)可使指定進程的一個線程成功調用SetForegroundWindow。為了使任何進程都可以在你的線程的窗口上彈出一個窗口,指定ASFW_ANY(定義為-1)作為dwProcessId參數:
BOOL AllowSetForgroundWindow(DWORD dwProcessId);
此外,線程可以鎖定SetForegroundWindow函數,使它總是失效的。方法是調用LockSetForegroundWindow。
BOOL LockSetForgroundWindow(UINT uLockCode);
對uLockCode參數可以指定LSFW_LOCK或者LSFW_UNLOCK。當一個菜單被激活時,系統在內部調用這個函數,這樣一個試圖跳到前景的窗口就不能關閉這個菜單。WindowsExplorer在顯示Start菜單時,需要明確地調用這些函數,因為Start菜單不是一個內置菜單。當用戶按了Alt鍵或者將一個窗口拉到前景時,系統自動解鎖SetForegroundWindow函數。這可以防止一個程序一直對SetForegroundWindow函數封鎖。
關于鍵盤管理和局部輸入狀態,其他的內容是同步鍵狀態數組。每個線程的局部輸入狀態變量都包含一個同步鍵狀態數組,但所有的線程要共享一個同步鍵狀態數組。這些數組反映了在任何給定時刻鍵盤所有鍵的狀態。利用GetAsyncKeyState函數可以確定用戶當前是否按下了鍵盤上的一個鍵:
SHORT GetAsyncKeyState(int nVirKey);
參數nVirtKey指出要檢查鍵的虛鍵代碼。結果的高位指出該鍵當前是否被按下(是為1,否為0)。筆者在處理一個消息時,常用這個函數來檢查用戶是否釋放了鼠標主按鈕。為函數參數賦一個虛鍵值VK_LBUTTON,并等待返回值的高位成為0。注意,如果調用函數的線程不是建立的窗口上,鼠標光標就可見了。
鼠標光標管理的另一個方面是使用ClipCursor函數將鼠標光標剪貼到一個矩形區域。
BOOL ClipCursor(CONSTRECT* prc);
這個函數使鼠標被限制在一個由prc參數指定的矩形區域內。當一個程序調用ClipCursor函數時,系統該做些什么呢?允許剪貼鼠標光標可能會對其他線程產生不利影響,而不允許剪貼鼠標光標又會影響調用線程。微軟實現了一種折衷的方案。當一個線程調用這個函數時,系統將鼠標光標剪貼到指定的矩形區域。但是,如果同步激活事件發生(當用戶點擊了其他程序的窗口,調用了SetForegroundWindow,或按了Ctrl+Esc組合鍵),系統停止剪貼鼠標光標的移動,允許鼠標光標在整個屏幕上自由移動。
現在我們再討論鼠標捕獲。當一個窗口“捕獲”鼠標(通過調用SetCapture)時,它要求所有的鼠標消息從RIT發到調用線程的虛擬輸入隊列,并且所有的鼠標消息從虛擬輸入隊列發到設置捕獲的窗口。在程序調用ReleaseCapture之前,要一直繼續這種鼠標消息的捕獲。
鼠標的捕獲必須同系統的強壯性折衷,也只能是一種折衷。當一個程序調用SetCapture時,RIT將所有鼠標消息放入線程的虛擬輸入隊列。SetCapture還要為調用SetCapture的線程設置局部輸入狀態變量。
通常一個程序在用戶按一個鼠標按鈕時調用SetCapture。但是即使鼠標按鈕沒有被按下,也沒有理由說一個線程不能調用SetCapture。如果當一個鼠標按下時調用SetCapture,捕獲在全系統范圍內執行。但當系統檢測出沒有鼠標按鈕按下時,RIT不再將鼠標消息只發往線程的虛擬輸入隊列,而是將鼠標消息發往與鼠標光標所在的窗口相聯系的輸入隊列。這是不做鼠標捕獲時的正常行為。
但是,最初調用SetCapture的線程仍然認為鼠標捕獲有效。因此,每當鼠標處于有捕獲設置的線程所建立的窗口時,鼠標消息將發往這個線程的捕獲窗口。換言之,當用戶釋放了所有的鼠標按鈕時,鼠標捕獲不再在全系統范圍內執行,而是在一個線程的局部范圍內執行。
此外,如果用戶想激活一個其他線程所建立的窗口,系統自動向設置捕獲的線程發送鼠標按鈕按下和鼠標按鈕放開的消息。然后系統更新線程的局部輸入狀態變量,指出該線程不再具有鼠標捕獲。很明顯,通過這種實現方式,微軟希望鼠標點擊和拖動是使用鼠標捕獲的最常見理由。
27.3 將虛擬輸入隊列同局部輸入狀態掛接在一起
從上面的討論我們可以看出這個輸入模型是強壯的,因為每個線程都有自己的局部輸入狀態環境,并且在必要時每個線程可以連接到RIT或從RIT斷開。有時候,我們可能想讓兩個或多個線程共享一組局部輸入狀態變量及一個虛擬輸入隊列。
可以利用AttachThreadInput函數來強制兩個或多個線程共享同一個虛擬輸入隊列和一組局部輸入狀態變量:
BOOL AttachThreadInput(
DWORD idAttach,
DWORD idAttachTo,
BOOL fAttach);
函數的第一個參數idAttach,是一個線程的ID,該線程所包含的虛擬輸入隊列(以及局部輸入狀態變量)是你不想再使用的。第二個參數idAttachTo,是另一個線程的ID,這個線程所包含的虛擬輸入隊列(和局部輸入狀態變量)是想讓兩個線程共享的。第三個參數fAttach,當想讓共享發生時,被設置為TRUE,當想把兩個線程的虛擬輸入隊列和局部輸入狀態變量分開時,設定為FALSE。可以通過多次調用AttachThreadInput函數讓多個線程共享同一個虛擬輸入隊列和局部輸入狀態變量。
我們再考慮前面的例子,假定線程A調用AttachThreadInput,傳遞線程A的ID作為第一個參數,線程B的ID作為第二個參數,TRUE作為最后一個參數:
SHORT GetKeyState(int nvirKey);
現在每個發往窗口A1、窗口B1或窗口B2的硬件輸入事件都將添加到線程B的虛擬輸入隊列中。線程A的虛擬輸入隊列將不再接收輸入事件,除非再一次調用AttachThreadInput并傳遞FALSE作為最后一個參數,將兩個線程的輸入隊列分開。
當將兩個線程的輸入都掛接在一起時,就使線程共享單一的虛擬輸入隊列和同一組局部輸入狀態變量。但線程仍然使用自己的登記消息隊列、發送消息隊列、應答消息隊列和喚醒標志(見第26章的討論)。
如果讓所有的線程都共享一個輸入隊列,就會嚴重削弱系統的強壯性。如果某一個線程接收一個按鍵消息并且掛起,其他的線程就不能接收任何輸入了。所以應該盡量避免使用AttachThreadInput函數。
在某些情況下,系統隱式地將兩個線程掛接在一起。第一種情況是當一個線程安裝一個日志記錄掛鉤(journalrecordhook)或日志播放掛鉤(journalplaybackhook)的時候。當掛鉤被卸載時,系統自動恢復所有線程,這樣線程就可以使用掛鉤安裝前它們所使用的相同輸入隊列。
當一個線程安裝一個日志記錄掛鉤時,它是讓系統將用戶輸入的所有硬件事件都通知它。這個線程通常將這些信息保存或記錄在一個文件上。因用戶的輸入必須按進入的次序來記錄,所以系統中每個線程要共享一個虛擬輸入隊列,使所有的輸入處理同步。
還有一些情況,系統會代替你隱式地調用AttachThreadInput。假定你的程序建立了兩個線程。第一個線程建立了一個對話框。在這個對話框建立之后,第二個線程調用GreatWindow,使用WS_CHILD風格,并向這個子窗口的雙親傳遞對話框的句柄。系統用子窗口的線程調用AttachThreadInput,讓子窗口的線程使用對話框線程所使用的輸入隊列。這樣就使對話框的所有子窗口之間對輸入強制同步。
27.3.1 LISLab示例程序
LISLab程序(“27LISLab.exe”)清單列在清單27-1上。這是一個實驗室,可以用它來實驗局部輸入狀態。這個程序的源代碼和資源文件在本書所附光盤的27-LISLab目錄下。
為了用局部輸入狀態做實驗,需要兩個線程作為實驗品。LISLab進程有一個線程,這里選擇Notepad的線程作為另一個。
如果當LISLab啟動時Notepad沒有在運行,LISLab將啟動Notepad。在LISLab初始化之后,就會見到圖27-4所示的對話框。
這個窗口的左上角是Windows編組框。
有窗口擁有焦點并且沒有窗口是活動的。
然后可以通過改變窗口的焦點來進行實驗。首先在LocalInputStateLab對話框右上角的Function組合框內選擇SetFocus。然后鍵入延遲時間(以秒計),即在調用SetFocus之前你想讓LISLab等待的時間。對這個實驗,你很可能會指定延遲為0s。后面將簡單介紹如何使用Delay字段。
下一步選擇一個窗口,作為調用SetFocus時的參數。用LocalInputStateLab對話框左邊的NotepadWindowsAndSelf列表框選擇一個窗口。對這個實驗,選擇列表框中的[Notepad]Untitled-Notepad。現在已經為調用SetFocus做好準備。只需點擊Delay按鈕,觀察Windows編組框會發生什么變化。什么也沒發生。系統沒有執行改變焦點的動作。
如果真想讓SetFocus將焦點改變到Notepad,就點擊AttachToNotepad按鈕。點擊這個按鈕使LISLab調用下面的函數:
這個調用告訴LISLab的線程去使用Note-pad所使用的虛擬輸入隊列。另外,LISLab的線程也要與Notepad共享局部輸入狀態變量。
AttachThreadInput(GetWindowThreadProcessId(g_hwndNotepad, NULL), GetCurrentThreadId(),TRUE);
如果在點擊了AttachToNotepad按鈕之后,點擊Notepad窗口,LISLab的對話框變成下圖所示的樣子。現在注意,由于兩個線程的輸入隊列是掛接在一起的,LISLab可以服從Notepad所做的窗口焦點改變。圖27-5所示的對話框顯示Edit控制框當前具有焦點。如果我們顯示Notepad中的FileOpen對話框,LISLab將繼續更新它的顯示屏內容,告訴我們哪一個
Note-pad窗口具有焦點,哪個窗口是活動的等等。
現在注意,由于兩個線程的輸入隊列是掛接在一起的,LISLab可以服從Notepad所做的窗口焦點改變。圖27-5所示的對話框顯示Edit控制框當前具有焦點。如果我們顯示Notepad中的FileOpen對話框,LISLab將繼續更新它的顯示屏內容,告訴我們哪一個Note-pad窗口具有焦點,哪個窗口是活動的等等。
現在我們再回到LISLab,點擊Delay按鈕,讓SetFocus給Notepad焦點。這一次,對SetFocus的調用成功,因兩個線程的輸入隊列是接在一起的。
讀者可以繼續實驗,通過在Function組合框中選擇不同的函數,分別對SetActiveWindow
不過,RIT仍然同Notepad的線程相“連接”。
關于窗口和焦點還要說明一點:SetFocus函數和SetActiveWindow函數都返回原來擁有焦點或原來活動的窗口的句柄。有關這個窗口的信息顯示在LISLab對話框的PrevWnd字段里。而且,LISLab在調用SetForegroundWindow之前,要先調用GetForegroundWindow來取得原來處于前景的窗口的句柄。這些信息也顯示在PrevWnd字段。
現在我們對鼠標光標的內容進行實驗。每當你在LISLab的對話框上移動鼠標(但沒有在它的任何子窗口上移動),鼠標被顯示成一個垂直箭頭。當鼠標消息發送到這個對話框,消息要添加到MouseMessageReceived列表框中。這樣你就可以知道何時對話框在接收鼠標消息。如果你將鼠標移出對話框或移到某個子窗口之上,就會發現鼠標消息不再添加到MouseMessageReceived列表框中。
現在將鼠標移往對話框的右部,移在文本ClickRightMouseButtonToSetCapture之上,然后點擊并按住鼠標右鍵。這時,LISLab調用SetCapture并傳遞LISLab對話框的句柄作為參數。注意LISLab更新Windows編組框來反映它擁有鼠標捕獲。
不要釋放鼠標右鍵,在LISLab的子窗口上移動鼠標,并觀察鼠標消息被添加到列表框里。注意,如果你將鼠標移出LISLab的對話框,LISLab會繼續得知鼠標消息。不論你在屏幕上什么位置移動鼠標,鼠標光標都保持垂直箭頭形狀。
現在我們看一看系統的其他表現。釋放鼠標右鍵,看會發生什么。在LISLab對話框的上部所反映的捕獲窗口繼續顯示LISLab依然認為自己擁有鼠標捕獲。如果你將鼠標移出LISLab的對話框,鼠標光標就不再保持垂直箭頭的形狀,鼠標消息也不再向MouseMssagesReceived列表框中添加,你會看到鼠標捕獲依然有效,因為所有窗口都使用同一組局部輸入狀態變量。
當完成對鼠標捕獲的實驗時,可以使用下面兩種辦法將其關閉:
在LocalInputStateLab對話框的任何地方雙擊鼠標右鍵,讓LISLab安排ReleaseCapture的調用。
點擊一個由LISLab的線程之外的線程所建立的窗口。這樣系統會自動向LISLab的對話框發送鼠標按鈕彈起和鼠標按鈕按下的消息。
不論使用哪種辦法,要觀察Windows編組框中的Capture字段是如何變化以反映沒有窗口擁有鼠標捕獲。
點擊Hide或ShowCursor按鈕,使LISLab執行下面的代碼:
ShowCursor(FALSE);//或者ShowCursor(TRUE);
當你隱藏了鼠標光標時,如果在LISLab的對話框上移動鼠標時,不會出現鼠標光標。但在這個對話框之外移動鼠標時,鼠標光標又會出現。使用Show按鈕來抵消Hide按鈕的效果。注意隱藏光標的效果是要積累的,也就是,如果5次點擊Hide按鈕,必須也5次點擊Show按鈕,才能使光標可見。
最后一個實驗是使用InfiniteLoop按鈕。當點擊這個按鈕時,LISLab執行下面的代碼:
SetCursor(LoadCursor(NULL,IDC_NO));
for(;;)
;
第一行代碼將鼠標光標改變成一個缺口圓(slashedcircle),第二行代碼執行一個死循環。在點擊InfiniteLoop按鈕之后,LISLab停止響應任何輸入。如果在LISLab的對話框上移動鼠標,鼠標光標依然是缺口圓。如果將鼠標移出對話框,光標要改變,以反映它所處窗口的光標。可以用鼠標操作這些其他的窗口。
如果將鼠標移回到LISLab的對話框,系統看到LISLab沒有響應,就自動將光標改回最近的形狀——缺口圓。可以看到執行死循環的線程對用戶是不方便的,但可以因此使用其他窗口。
注意,如果把一個窗口移到掛起的LocalInputStateLab對話框,然后再把它移開,系統要發送一個WM_PAINT消息。但系統發現這個線程沒有響應。系統為這個沒有響應的程序重畫窗口。當然,系統不能正確地重畫這個窗口,因為系統不知道這個程序是干什么的。所以系統只是抹掉窗口的背景,并重畫框架。
現在還有一個問題,如果屏幕上有一個窗口對我們做的任何事情(按鍵或擊鼠標鈕)都沒有響應。我們如何清除這個窗口?在Windows2000里,可以用鼠標右擊Taskbar上的程序按鈕,或顯示圖27-7的TaskManager窗口。然后只要在窗口里選擇我們想要結束的程序,在這里是LocalInputStateLab,再點擊EndTask按鈕。系統將試圖用一種溫和的方式(通過發送一個WM_CLOSE消息)來結束LISLab,但發現該程序沒有反應。
在Windows2000中會顯示圖的對話框。
選擇EndTask(在Windows98里)或EndNow(在Windows2000里)使系統強制性地將LISLab從系統中清除。Cancel按鈕是告訴系統你改變了主意,不再想結束這個程序。這里,我們選擇EndTask或EndNow,從系統中清除LISLab。
這個實驗的主要目的是為了說明系統的強壯性。一個程序不可能使操作系統處于這樣一個狀態——使其他程序不可用。還要注意在結束處理中,Windows98和Windows2000都可以自動釋放線程所分配的資源,不會造成內存遺漏。
27.3.2 LISWatch 示例程序
LISWatch程序(“27LISWatch.exe”)的源程序清單列在清單27-2中。這是一個有用的實用程序,用來監控活動窗口、焦點窗口和鼠標捕獲窗口。在本書所附光盤的27-LISWatch目錄下是這個程序的源代碼和資源文件。
當運行LISWatch,會顯示圖27-10的對話框。當這個對話框接收到一個WM_INITDIALOG消息時,它調用SetTimer來設置一個計時器,每秒鐘激發兩次。當收到WM_TIMER消息,對話框的內容就更新以反映哪個窗口是活動的、哪個窗口擁有焦點、哪個窗口捕獲了鼠標。在這個對話框
以調用GetFocus、GetActiveWindow和GetCapture,所有這些函數都返回有效的窗口句柄。幫助函數CalcWndText構造一個字符串,包含每個窗口的類名和窗口標題。然后每個窗口的串在LISWatch的對話框中被更新。最后,Dlg_OnTimer在返回之前,再一次調用AttachThreadInput,但這次是將最后一個參數設定為FALSE,這樣兩個線程的局部輸入狀態就彼此斷開。
前面解釋了LISWatch的基本內容。然而,我們對LISWatch增加了其他一些特性,在這里解釋一下。當啟動LISWatch之后,它要監控系統中任何地方發生的窗口活動的變化。這就是對話框頂部的“全系統范圍(System-wide)”的意思。LISWatch還可以讓你只限于觀察一個線程的局部輸入狀態的變化。利用這種特性,LISWatch可以向你報告一個線程的確切情況。
為了讓LISWatch監控一個線程的局部輸入狀態,所要做的只是在LISWatch的窗口上按下鼠標左鍵,在另外一個線程建立的窗口上拖動鼠標光標,然后釋放鼠標按鈕。在釋放鼠標按鈕之后,LISWatch將全局變量g-dwThreadIdAttachTo設置成所選線程的ID。這個線程ID將替換LISWatch的對話框頂部的“System-wide”。當這個全局變量不是零,Dlg_OnTimer會略微改變它的行為。不是總將它的局部輸入狀態同前景線程的局部輸入掛接在一起,而是將LISWatch本身同所選擇的線程掛接在一起。用這種方式,LISWatch調用GetActiveWindow、GetFocus和GetCapture來反映所選擇線程的局部輸入狀態的情況。
我們來做一個實驗。運行Calculator,再用LISWatch來選擇它的窗口。當激活Calculator的窗口時,LISWatch更新它的顯示內容,見圖27-11的對話框。
這里,Calculator的線程ID是0x000004ec。當前設定LISWatch來監控這一個線程的局部輸入狀態變化。如果點擊Calculator的任何單選按鈕或復選框,LISWatch都可以顯示焦點的變化,因為所有這些窗口都是由線程0x000004ec建立的。
如果現在再激活一個由另外的程序(這里的例子是Notepad)建立的窗口,LISWatch的對話框是下面的樣子(見圖27-12)。
總結
以上是生活随笔為你收集整理的回炉重造之重读Windows核心编程-027-硬件输入模型和局部输入状态的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: VS快捷键大全
- 下一篇: 设置Jexus开机启动