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