7. 滑鼠

Post date: 2012/3/23 上午 05:37:26

7. 滑鼠

滑鼠是有一個或多個鍵的定位設備。雖然也可以使用諸如觸摸畫面和光筆之類的輸入設備,但是只有滑鼠以及常用在膝上型電腦上的軌跡球等才是滲透了PC市場的唯一輸入設備。

情況並非總是如此。當然,Windows的早期開發人員認為他們不應該要求使用者為了執行其產品而必須買隻滑鼠。因此,他們將滑鼠作為一種選擇性的附加設備,而為Windows中的所有操作以及applet提供一種鍵盤介面(例如,查看Windows小算盤程式的線上說明資訊,可以看到每個按鈕都提供了一個同等功效的鍵盤操作方式)。第三方軟體發展人員使用鍵盤介面來提供與滑鼠操作相同的功能,這本書以前的版本也是這麼做的。

理論上來說,現在的Windows需要滑鼠。至少,一些訊息方塊是這樣講的。當然,您也可以拔下滑鼠,而且Windows仍然可以執行良好(只有訊息方塊會提示您沒有連接滑鼠)。試圖不用滑鼠來使用Windows就像用腳趾來彈鋼琴一樣(至少在最初的一段時間裏是這樣),但您依然可以這樣做。正因為如此,我還是喜歡為滑鼠功能提供鍵盤操作。打字員尤其喜歡讓他們的手保持在鍵盤上,並且我認為每個人都有在雜亂的桌上找不到滑鼠,或者滑鼠移動不靈敏的經驗。使用鍵盤通常不需要花費更多的精力和努力,並且為喜歡使用鍵盤的人提供更多的功能。

我們通常認為,鍵盤便於輸入和操作文字資料,而滑鼠則便於畫圖和操作圖形物件。實際上,本章大多數的範例程式都畫了一些圖形,並且用到了我們在 第五章 所學到的知識。

滑鼠基礎

Windows 98能支援單鍵、雙鍵或者三鍵滑鼠,也可以使用搖桿或者光筆來模擬單鍵滑鼠。早期,由於許多使用者都有單鍵滑鼠,所以Windows應用程式總是避免使用雙鍵或三鍵滑鼠。不過,由於雙鍵滑鼠已經成為事實上的標準,因此不使用第二個鍵的傳統已經不再合理了。當然,第二個滑鼠按鍵是用於啟動一個「快顯功能表」,亦即出現在普通功能表列之外的視窗中功能表,或者用於特殊的拖曳操作(拖曳將在後面加以解釋)。然而,程式不能依賴雙鍵滑鼠。

理論上,您可以用我們的老朋友GetSystemMetrics函式來確認滑鼠是否存在:

如果已經安裝了滑鼠,fMouse將傳回TRUE(非0);如果沒有安裝,則傳回0。然而,在Windows 98中,不論滑鼠是否安裝,此函式都將傳回TRUE 。在Microsoft Windows NT中,它可以正常工作。

要確定所安裝滑鼠其上按鍵的個數,可使用

如果沒有安裝滑鼠,那麼函式將傳回0。然而,在Windows 98下,如果沒有安裝滑鼠,此函式將傳回2。

習慣用左手的使用者可以使用Windows的「控制台」來切換滑鼠按鍵。雖然應用程式可以通過在GetSystemMetrics中使用SM_SWAPBUTTON參數來確定是否進行了這種切換,但通常沒有這個必要。由食指觸發的鍵被認為是左鍵,即使事實上是位於滑鼠的右邊。不過,在一個教育訓練程式中,您可能想在螢幕上畫一個滑鼠,在這種情況下,您可能想知道滑鼠按鍵是否被切換過了。

您可以在「控制台」中設定滑鼠的其他參數,例如雙擊速度。從Windows應用程式,通過使用SystemParametersInfo函式可以設定或獲得此項資訊。

一些簡單的定義

當Windows使用者移動滑鼠時,Windows在顯示器上移動一個稱為「滑鼠游標」的小點陣圖。滑鼠游標有一個指向顯示器上精確位置的單圖素「熱點」。當我提到滑鼠游標在螢幕上的位置時,指的是熱點的位置。

Windows支援幾種預先定義的滑鼠游標,程式可以使用這些游標。最常見的是稱為IDC_ARROW的斜箭頭(在WINUSER.H中定義)。熱點在箭頭的頂端。IDC_CROSS游標(在本章後面的BLOKOUT程式中有用到)的熱點在十字交叉線的中心。 IDC_WAIT游標是一個沙漏,通常用於指示程式正在執行。程式寫作者也可以設計自己的游標。我們將在 第十章 學習設計方法。在定義視窗類別結構時指定特定視窗的內定游標,例如:

下面是一些描述滑鼠按鍵動作的術語:

fMouse = GetSystemMetrics (SM_MOUSEPRESENT) ;

cButtons = GetSystemMetrics (SM_CMOUSEBUTTONS) ;

wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ;

  • Clicking 按下並放開一個滑鼠按鍵。
  • Double-clicking 快速按下並放開滑鼠按鍵兩次。
  • Dragging 按住滑鼠按鍵並移動滑鼠。

對三鍵滑鼠來說,三個鍵分別稱為左鍵、中鍵、右鍵。在Windows表頭檔案中定義的與滑鼠有關的識別字使用縮寫LBUTTON、MBUTTON和RBUTTON。雙鍵滑鼠只有左鍵與右鍵,單鍵滑鼠只有一個左鍵。

滑鼠(Mouse)的複數

現在,為了展現我的勇氣,我將面對輸入裝置最難辯的爭論話題:什麼是「mouse」的複數。雖然每個人都知道多隻齧齒動物稱為mice,似乎沒有人對該如何稱呼多個輸入裝置有最後的答案。不管「mice」或「mouse」聽起來都不對勁。我慣常參考的《American Heritage Dictionary of the English Language》第三版則隻字未提。

《Wired style:Principles of English Usage in the Digital Age》(HardWired, 1996)指出「mouse」比較好,以避免與齧齒動物搞混。在1964發明滑鼠的Doug Engelbart對此爭議也幫不上忙。我曾經問過他mouse的複數是什麼,他說我不知道。

最後,高權威的Microsoft Manual of Style for Technical Publications告訴我們「避免使用複數mice。假如你必須提到多隻mouse,使用mouse devices」。這聽起來像是在逃避問題,但當一切聽起來都不對勁時,它確實是個明智的忠告了。事實上,大部分需要mouse複數的句子都能重新修改來避開。例如,試著說"People use the almost as much as keyboard",而不是"Pople use mice almost as much as keyboards"。

前一章 中您已經看到,Windows只把鍵盤訊息發送給擁有輸入焦點的視窗。滑鼠訊息與此不同:只要滑鼠跨越視窗或者在某視窗中按下滑鼠按鍵,那麼視窗訊息處理程式就會收到滑鼠訊息,而不管該視窗是否活動或者是否擁有輸入焦點。Windows為滑鼠定義了21種訊息,不過,其中有11個訊息和顯示區域無關(下面稱之為「非顯示區域」訊息),Windows程式經常忽略這些訊息。

當滑鼠移過視窗的顯示區域時,視窗訊息處理程式收到WM_MOUSEMOVE訊息。當在視窗的顯示區域中按下或者釋放一個滑鼠按鍵時,視窗訊息處理程式會接收到下面這些訊息:

顯示區域滑鼠訊息

表7-1

只有對三鍵滑鼠,視窗訊息處理程式才會收到MBUTTON訊息;只有對雙鍵或者三鍵滑鼠,才會接收到RBUTTON訊息。只有當定義的視窗類別能接收DBLCLK(雙擊)訊息,視窗訊息處理程式才能接收到這些訊息(請參見 本章中「雙擊滑鼠按鍵」一節 )。

對於所有這些訊息來說,其lParam值均含有滑鼠的位置:低字組為x座標,高字組為y座標,這兩個座標是相對於視窗顯示區域左上角的位置。您可以用LOWORD和HIWORD巨集來提取這些值:

wParam的值指示滑鼠按鍵以及Shift和Ctrl鍵的狀態。您可以使用表頭檔案WINUSER.H中定義的位元遮罩來測試wParam。MK字首代表「滑鼠按鍵」。

x = LOWORD (lParam) ; y = HIWORD (lParam) ;

例如,如果收到了WM_LBUTTONDOWN訊息,而且值

是TRUE(非0),您就知道當左鍵按下時也按下了Shift鍵。

當您把滑鼠移過視窗的顯示區域時,Windows並不為滑鼠的每個可能的圖素位置都產生一個WM_MOUSEMOVE訊息。您的程式接收到WM_MOUSEMOVE訊息的次數,依賴於滑鼠硬體,以及您的視窗訊息處理程式在處理滑鼠移動訊息時的速度。換句話說,Windows不能用未處理的WM_MOUSEMOVE訊息來填入訊息佇列。當您執行下面將描述的CONNECT程式時,您將會更了解WM_MOUSEMOVE訊息處理的速率。

如果您在非活動視窗的顯示區域中按下滑鼠左鍵,Windows將把活動視窗改為在其中按下滑鼠按鍵的視窗,然後把WM_LBUTTONDOWN訊息送到該視窗訊息處理程式。當視窗訊息處理程式得到WM_LBUTTONDOWN訊息時,您的程式就可以安全地假定該視窗是活動化的了。不過,您的視窗訊息處理程式可能在未接收到WM_LBUTTONDOWN訊息的情況下先接收到了WM_LBUTTONUP的訊息。如果在一個視窗中按下滑鼠按鍵,然後移動到使用者視窗釋放它,就會出現這種情況。類似的情況,當滑鼠按鍵在另一個視窗中被釋放時,視窗訊息處理程式只能接收到WM_LBUTTONDOWN訊息,而沒有相應的WM_LBUTTONUP訊息。

這些規則有兩個例外:

wparam & MK_SHIFT

  • 視窗訊息處理程式可以「攔截滑鼠」並且連續地接收滑鼠訊息,即使此時滑鼠在該視窗顯示區域之外。您將在本章的後面學習如何攔截滑鼠。
  • 如果正在顯示一個系統模態訊息方塊或者系統模態對話方塊,那麼其他程式就不能接收滑鼠訊息。當系統模態訊息方塊或者對話方塊活動時,禁止切換到其他視窗或者程式。一個顯示系統模態訊息方塊的例子,是當您關閉Windows時。

簡單的滑鼠處理:一個例子

程式7-1中所示的CONNECT程式能作一些簡單的滑鼠處理,使您對Windows如何向您的程式發送滑鼠訊息有一些體會。

程式7-1 CONNECT CONNECT.C /*-------------------------------------------------------------------------- CONNECT.C -- Connect-the-Dots Mouse Demo Program (c) Charles Petzold, 1998 ---------------------------------------------------------------------------*/ #include <windows.h> #define MAXPOINTS 1000 LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("Connect") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) { MessageBox (NULL, TEXT ("Program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow ( szAppName, TEXT ("Connect-the-Points Mouse Demo"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } LRESULT CALLBACK WndProc ( HWND hwnd, UINT message, WPARAM wParam,LPARAM lParam) { static POINT pt[MAXPOINTS] ; static int iCount ; HDC hdc ; in i, j ; PAINTSTRUCT ps ; switch (message) { case WM_LBUTTONDOWN: iCount = 0 ; InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; case WM_MOUSEMOVE: if (wParam & MK_LBUTTON && iCount < 1000) { pt[iCount ].x = LOWORD (lParam) ; pt[iCount++].y = HIWORD (lParam) ; hdc = GetDC (hwnd) ; SetPixel (hdc, LOWORD (lParam), HIWORD (lParam), 0) ; ReleaseDC (hwnd, hdc) ; } return 0 ; case WM_LBUTTONUP: InvalidateRect (hwnd, NULL, FALSE) ; return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; SetCursor (LoadCursor (NULL, IDC_WAIT)) ; ShowCursor (TRUE) ; for (i = 0 ; i < iCount - 1 ; i++) for (j = i + 1 ; j < iCount ; j++) { MoveToEx (hdc, pt[i].x, pt[i].y, NULL) ; LineTo (hdc, pt[j].x, pt[j].y) ; } ShowCursor (FALSE) ; SetCursor (LoadCursor (NULL, IDC_ARROW)) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }

CONNECT處理三個滑鼠訊息:

  • WM_LBUTTONDOWN CONNECT 清除顯示區域。
  • WM_MOUSEMOVE 如果按下左鍵,那麼CONNECT就在顯示區域中的滑鼠位置處繪製一個黑點,並保存該座標。
  • WM_LBUTTONUP CONNECT 把顯示區域中繪製的點與其他每個點連接起來。有時會產生一個漂亮的圖形,有時則會是黑鴉鴉的一團糟(見圖7-1)。

圖7-1 CONNECT的螢幕顯示

CONNECT的使用方法:把滑鼠游標移動到顯示區域中,按下左鍵,移動一下位置,釋放左鍵。對幾個構成曲線的點,CONNECT能處理得很好,方法是按住左鍵,快速移動滑鼠,這樣就可以繪製出該曲線圖案。

CONNECT使用了三個簡單的圖形裝置介面(GDI)函式,我在 第五章 討論過這些函式。當滑鼠左鍵按下時,SetPixel為每個WM_MOUSEMOVE訊息繪製一個黑圖素(對於高解析度的顯示器,圖素幾乎看不見)。畫直線需要MoveToEx和LineTo函式。

如果您在釋放滑鼠按鍵之前把滑鼠游標移到顯示區域之外,那麼CONNECT就不會連接這些點,因為它沒有收到WM_LBUTTONUP訊息。如果您把滑鼠移回顯示區域內並按下左鍵,那麼CONNECT將清除顯示區域。如果想在顯示區域外釋放左鍵後還繼續進行畫圖,那麼可以在顯示區域外按下滑鼠再移回顯示區域中。

CONNECT最多可以保存1000個點。設點數為P,則CONNECT畫的線數就等於P × (P - 1) / 2。如果有1000個點,則要繪製50萬條直線,大約需要幾分鐘才能畫完(時間的長短取決於您的硬體設備)。由於Windows 98是一種優先權式多工環境,因此您可以在這一段時間切換到別的程式中。但是,當程式正在忙的時候,您將無法對CONNECT程式做任何事(諸如移動或者縮放等)。在 第二十章 中,我們將討論解決這一問題的方法。

因為CONNECT可能會花一些時間來繪製直線,因此在處理WM_PAINT訊息時它將切換到沙漏游標,然後再恢復原狀。這要求使用兩個現有游標來呼叫SetCursor。CONNECT還呼叫兩次ShowCursor,一次用TRUE參數,另一次用FALSE參數。我將在本章的後面,「 使用鍵盤模擬滑鼠 」一節中更詳細地討論這些呼叫。

有時,我們使用「跟蹤」這個詞代表程式處理滑鼠移動的方法。但是,跟蹤並不意味著,程式在視窗訊息處理程式中的某個迴圈裏,不斷跟隨滑鼠在顯示器上的運動。實際上,視窗訊息處理程式處理每條滑鼠訊息,然後迅速退出。

處理Shift鍵

當CONNECT接收到一個WM_MOUSEMOVE訊息時,它把wParam和MK_LBUTTON進行位元與(AND)運算,來確定是否按下了左鍵。wParam也可以用於確定Shift鍵的狀態。例如,如果處理必須依賴於Shift和Ctrl鍵的狀態,那麼您可以使用如下所示的方法:

if (wParam & MK_SHIFT) { if (wParam & MK_CONTROL) { //按下了Shift和Ctrl鍵 } else { //按下了Shift鍵 } { else { if (wParam & MK_CONTROL) { //按下了Ctrl鍵 } else { //Shift和Ctrl鍵均未按下 } }

如果您想在程式中同時使用左右鍵,同時如果您還希望只有單鍵滑鼠的使用者也能使用您的程式,那麼您可以這樣來寫作程式:Shift與左鍵的組合使用等效於右鍵。在這種情況下,對滑鼠按鍵的處理可以採用如下所示的方法:

case WM_LBUTTONDOWN: if (!(wParam & MK_SHIFT)) { //處理左鍵 return 0 ; } // Fall through case WM_RBUTTONDOWN: //處理右鍵 return 0 ;

Windows函式GetKeyState(在 第六章 中介紹過)可以使用虛擬鍵碼VK_LBUTTON、VK_RBUTTON、VK_MBUTTON、VK_SHIFT和VK_CONTROL來傳回滑鼠按鍵與Shift鍵的狀態。如果GetKeyState傳回負值,則說明已按下了滑鼠按鍵或者Shift鍵。因為GetKeyState傳回目前正在處理的滑鼠按鍵或者Shift鍵的狀態,所以全部狀態資訊與相應的訊息都是同步的。但是,正如不能把GetKeyState用於尚未按下的鍵一樣,您也不能為尚未按下的滑鼠按鍵呼叫GetKeyState。請不要這樣做:

只有在您呼叫GetKeyState期間處理訊息時,而左鍵已經按下,才會報告鍵已經按下的訊息。

雙擊滑鼠按鍵

雙擊滑鼠按鍵是指在短時間內單擊兩次。要確定為雙擊,則這兩次單擊必須發生在其相距的實際位置十分接近的狀況下(內定範圍是一個平均系統字體字元的寬,半個字元的高),並且發生在指定的時間間隔(稱為「雙擊速度」)內。您可以在「控制台」中改變時間間隔。

如果希望您的視窗訊息處理程式能夠收到雙按鍵的滑鼠訊息,那麼在呼叫RegisterClass初始化視窗類別結構時,必須在視窗風格中包含CS_DBLCLKS識別字:

如果在視窗風格中未包含CS_DBLCLKS,而使用者在短時間內雙擊了滑鼠按鍵,那麼視窗訊息處理程式會接收到下面這些訊息:

WM_LBUTTONDOWN

WM_LBUTTONUP

WM_LBUTTONDOWN

WM_LBUTTONUP

視窗訊息處理程式可能在這些鍵的訊息之前還收到了其他訊息。如果您想實作自己的雙擊處理,那麼您可以使用Windows函式GetMessageTime取得WM_LBUTTONDOWN訊息之間的相對時間。 第八章 將更詳細地討論這個函式。

如果您的視窗類別風格中包含了CS_DBLCLKS,那麼雙擊時視窗訊息處理程式將收到如下訊息:

WM_LBUTTONDOWN

WM_LBUTTONUP

WM_LBUTTONDBLCLK

WM_LBUTTONUP

WM_LBUTTONDBLCLK訊息簡單地替換了第二個WM_LBUTTONDOWN訊息。

如果雙擊中的第一次鍵操作完成單擊的功能,那麼雙擊這一訊息是很容易處理的。第二次按鍵(WM_LBUTTONDBLCLK訊息)則完成第一次按鍵以外的事情。例如,看看Windows Explorer中是如何用滑鼠來操作檔案列表的。按一次鍵將選中檔案,Windows Explorer用反白顯示列指出被選擇檔案的位置。雙擊則實作兩個功能:第一次是單擊那個選中檔案;第二次則指向Windows Explorer以打開該檔案。執行方式相當簡單,如果雙擊中的第一次按鍵不執行單擊功能,那麼滑鼠處理方式會變得非常複雜。

非顯示區域滑鼠訊息

在視窗的顯示區域內移動或按下滑鼠按鍵時,將產生10種訊息。如果滑鼠在視窗的顯示區域之外但還在視窗內,Windows就給視窗訊息處理程式發送一條「非顯示區域」滑鼠訊息。視窗非顯示區域包括標題列、功能表和視窗捲動列。

通常,您不需要處理非顯示區域滑鼠訊息,而是將這些訊息傳給DefWindowProc,從而使Windows執行系統功能。就這方面來說,非顯示區域滑鼠訊息類似於系統鍵盤訊息WM_SYSKEYDOWN、WM_SYSKEYUP和WM_SYSCHAR。

非顯示區域滑鼠訊息幾乎完全與顯示區域滑鼠訊息相對應。訊息中含有字母「NC」以表示是非顯示區域訊息。如果滑鼠在視窗的非顯示區域中移動,那麼視窗訊息處理程式會接收到WM_NCMOUSEMOVE訊息。滑鼠按鍵產生如表7-2所示的訊息。

while (GetKeyState (VK_LBUTTON) >= 0) ; // WRONG !!!

wndclass.style = CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS ;

表7-2

對非顯示區域滑鼠訊息,wParam和lParam參數與顯示區域滑鼠訊息的wParam和lParam參數不同。wParam參數指明移動或者按滑鼠按鍵的非顯示區域。它設定為WINUSER.H中定義的以HT開頭的識別字之一(HT表示「命中測試」)。

lParam參數的低位元word為x座標,高位元word為y座標,但是,它們是螢幕座標,而不是像顯示區域滑鼠訊息那樣指的是顯示區域座標。對螢幕座標,顯示器左上角的x和y的值為0。當往右移時x的值增加,往下移時y的值增加(見圖7-2)。

您可以用兩個Windows函式將螢幕座標轉換為顯示區域座標或者反之:

這裏pt是POINT結構。這兩個函式轉換了保存在結構中的值,而且沒有保留以前的值。注意,如果螢幕座標點在視窗顯示區域的上面或者左邊,顯示區域座標x或y值就是負值。

命中測試訊息

如果您數一下,就可以知道我們已經介紹了21個滑鼠訊息中的20個,最後一個訊息是WM_NCHITTEST,它代表「非顯示區域命中測試」。此訊息優先於所有其他的顯示區域和非顯示區域滑鼠訊息。lParam參數含有滑鼠位置的x和y螢幕座標,wParam參數沒有用。

Windows應用程式通常把這個訊息傳送給DefWindowProc,然後Windows用WM_NCHITTEST訊息產生與滑鼠位置相關的所有其他滑鼠訊息。對於非顯示區域滑鼠訊息,在處理WM_NCHITTEST時,從DefWindowProc傳回的值將成為滑鼠訊息中的wParam參數,這個值可以是任意非顯示區域滑鼠訊息的wParam值再加上以下內容:

ScreenToClient (hwnd, &pt) ; ClientToScreen (hwnd, &pt) ;

圖7-2 螢幕座標與客戶顯示區域座標

HTCLIENT

HTNOWHERE

HTTRANSPARENT

HTERROR

顯示區域

不在視窗中

視窗由另一個視窗覆蓋

使DefWindowProc產生警示用的嗶聲

如果DefWindowProc在其處理WM_NCHITTEST訊息後傳回HTCLIENT,那麼Windows將把螢幕座標轉換為顯示區域座標並產生顯示區域滑鼠訊息。

如果您還記得我們如何通過攔截WM_SYSKEYDOWN訊息來停用所有的系統鍵盤功能,那麼您可能會想我們可否通過攔截滑鼠訊息完成類似的事情。完全可以!只要您在視窗訊息處理程式中包含以下幾條敘述:

就可以有效地禁用您視窗中的所有顯示區域和非顯示區域滑鼠訊息。這樣一來,當滑鼠在您的視窗(包括系統功能表圖示、縮放按鈕以及關閉按鈕)中時,滑鼠按鍵將會失效。

從訊息產生訊息

Windows用WM_NCHITTEST訊息產生所有其他滑鼠訊息,這種由訊息引出其他訊息的想法在Windows中是很普遍的。讓我們來舉個例子。您知道,如果您在一個Windows程式的系統功能表圖示上雙擊一下,那麼程式將會終止。雙擊產生一系列的WM_NCHITTEST訊息。由於滑鼠定位在系統功能表圖示上,因此DefWindowProc將傳回HTSYSMENU的值,並且Windows把wParam等於HTSYSMENU的WM_NCLBUTTONDBLCLK訊息放在訊息佇列中。

視窗訊息處理程式通常把滑鼠訊息傳遞給DefWindowProc,當DefWindowProc接收到wParam參數等於HTSYSMENU的WM_NCLBUTTONDBLCLK訊息時,它就把wParam參數等於SC_CLOSE的WM_SYSCOMMAND訊息放入訊息佇列中(這個WM_SYSCOMMAND訊息是在使用者從系統功能表中選擇「Close」時產生的)。同樣地,視窗訊息處理程式也把這個訊息傳給DefWindowProc。DefWindowProc通過給視窗訊息處理程式發送WM_CLOSE訊息來處理該訊息。

如果一個程式在終止之前要求來自使用者的確認,那麼視窗訊息處理程式就需要攔截WM_CLOSE,否則,DefWindowProc呼叫DestroyWindow函式來處理WM_CLOSE。除了其他處理,DestroyWindow還給視窗訊息處理程式發送一個WM_DESTROY訊息。視窗訊息處理程式通常用下列程式碼來處理WM_DESTROY訊息:

case WM_NCHITTEST: return (LRESULT) HTNOWHERE ;

case WM_DESTROY: PostQuitMessage (0) ; return 0 ;

PostQuitMessage使得Windows把WM_QUIT訊息放入訊息佇列中,此訊息永遠不會到達視窗訊息處理程式,因為它使GetMessage傳回0,並終止訊息迴圈,從而也終止了程式。

程式中的命中測試

我在前面討論了Windows Explorer如何回應滑鼠的單擊和雙擊。顯然,程式(或者更精確的說,如同Windows Explorer般使用list view control)必須確定使用者滑鼠所指向的是哪一個檔案。

這叫做「命中測試」。正如DefWindowProc在處理WM_NCHITTEST訊息時做一些命中測試一樣,視窗訊息處理程式經常必須在顯示區域中進行一些命中測試。一般來說,命中測試中會使用x和y座標值,它們由傳到視窗訊息處理程式的滑鼠訊息的lParam參數給出。

一個假想的例子

有這樣一個例子。假設您的程式需要顯示幾列按字母排列的檔案。通常,您可以使用list view control,他會幫您由於要做全部的命中測試工作。但我們假設您由於某種原因而不能使用,這時就需要自己來做了。讓我們假定檔案名保存在稱為szFileNames的已排序字串指標陣列中。

讓我們也假定檔案列表開始於顯示區域的頂端,顯示區域為cxClient圖素寬, cyClient圖素高,每列為cxColWidth圖素寬,每個字元高度為cyChar圖素高。那麼每欄可填入的檔案數就是:

接收到一個滑鼠單擊訊息後,您就能從lParam獲得cxMouse和cyMouse座標。然後可以用下面的公式來計算使用者所指的是哪一列的檔案名:

相對於列頂端的檔案名位置為:

現在您就可以計算szFileNames陣列的下標:

如果iIndex超過了陣列中的檔案數,則表示使用者是在顯示器的空白區域內按滑鼠按鍵。

在許多情況下,命中測試要比本例更加複雜。在顯示一幅包含許多小圖形的圖像時,您必須決定要顯示的每個小圖形的座標。在命中計算中,您必須從座標找到物件。但這將在使用不確定字體大小的字處理程式中變得非常凌亂,因為您必須找到字元在字串中的位置。

範例程式

程式7-2所示的CHECKER1程式展示了一些簡單的命中測試,此程式把顯示區域分為5×5的25個矩形。如果您在某個矩形中按下滑鼠按鍵,那麼在該矩形中將出現一個「X」。如果您再按一次,那麼「X」將被刪除。

iNumInCol = cyClient / cyChar ;

iColumn = cxMouse / cxColWidth ;

iFromTop = cyMouse / cyChar ;

iIndex = iColumn * iNumInCol + iFromTop ;

程式7-2 CHECKER1 CHECKER1.C /*------------------------------------------------------------------------- CHECKER1.C -- Mouse Hit-Test Demo Program No. 1 (c) Charles Petzold, 1998 --------------------------------------------------------------------------*/ #include <windows.h> #define DIVISIONS 5 LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("Checker1") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) { MessageBox ( NULL, TEXT ("Program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow ( szAppName, TEXT ("Checker1 Mouse Hit-Test Demo"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } LRESULT CALLBACK WndProc ( HWND hwnd, UINT message, WPARAM wParam,LPARAMlParam) { static BOOL fState[DIVISIONS][DIVISIONS] ; static int cxBlock, cyBlock ; HDC hdc ; int x, y ; PAINTSTRUCT ps ; RECT rect ; switch (message) { case WM_SIZE : cxBlock = LOWORD (lParam) / DIVISIONS ; cyBlock = HIWORD (lParam) / DIVISIONS ; return 0 ; case WM_LBUTTONDOWN : x = LOWORD (lParam) / cxBlock ; y = HIWORD (lParam) / cyBlock ; if (x < DIVISIONS && y < DIVISIONS) { fState [x][y] ^= 1 ; rect.left = x * cxBlock ; rect.top = y * cyBlock ; rect.right = (x + 1) * cxBlock ; rect.bottom = (y + 1) * cyBlock ; InvalidateRect (hwnd, &rect, FALSE) ; } else MessageBeep (0) ; return 0 ; case WM_PAINT : hdc = BeginPaint (hwnd, &ps) ; for (x = 0 ; x < DIVISIONS ; x++) for (y = 0 ; y < DIVISIONS ; y++) { Rectangle (hdc, x * cxBlock, y * cyBlock, (x + 1) * cxBlock, (y + 1) * cyBlock) ; if (fState [x][y]) { MoveToEx (hdc, x * cxBlock, y * cyBlock, NULL) ; LineTo (hdc, (x+1) * cxBlock, (y+1) * cyBlock) ; MoveToEx (hdc, x * cxBlock, (y+1) * cyBlock, NULL) ; LineTo (hdc, (x+1) * cxBlock, y * cyBlock) ; } } EndPaint (hwnd,&ps); return 0 ; case WM_DESTROY : PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }

圖7-3是CHECKER1的顯示。程式畫的25個矩形的寬度和高度均相同。這些寬度和高度保存在cxBlock和cyBlock中,當顯示區域大小發生改變時,將重新對這些值進行計算。WM_LBUTTONDOWN處理過程使用滑鼠座標來確定在哪個矩形中按下了鍵,它在fState陣列中標誌目前矩形的狀態,並使該矩形區域失效,從而產生WM_PAINT訊息。

如果顯示區域的寬度和高度不能被5整除,那麼在顯示區域的左邊和下邊將有一小條區域不能被矩形所覆蓋。對於錯誤情況,CHECKER1通過呼叫MessageBeep回應此區域中的滑鼠按鍵操作。

當CHECKER1收到WM_PAINT訊息時,它通過GDI的Rectangle函式來重新繪製顯示區域。如果設定了fState值,那麼CHECKER1將使用MoveToEx和LineTo函式來繪製兩條直線。在處理WM_PAINT期間,CHECKER1在重新繪製之前並不檢查每個矩形區域的有效性,儘管它可以這樣做。檢查有效性的一種方法是在迴圈中為每個矩形塊建立RECT結構(使用與WM_LBUTTONDOWN處理程式中相同的公式),並使用IntersectRect函式檢查它是否與無效矩形(ps.rcPaint)相交。

CHECKER1只能在裝有滑鼠情況下才可執行。下面我們在程式中加入鍵盤介面,就如同 第六章 中對SYSMETS程式所做的那樣。不過,即使在一個使用滑鼠游標作為指向用途的程式中加入鍵盤介面,我們還是必須處理滑鼠游標的移動和顯示問題。

即使沒有安裝滑鼠,Windows仍然可以顯示一個滑鼠游標。Windows為這個游標保存了一個「顯示計數」。如果安裝了滑鼠,顯示計數會被初始化為0;否則,顯示計數會被初始化為-1。只有在顯示計數非負時才顯示滑鼠游標。要增加顯示計數,您可以呼叫:

要減少顯示計數,可以呼叫:

您在使用ShowCursor之前,不需要確定是否安裝了滑鼠。如果您想顯示滑鼠游標,而不管滑鼠存在與否,那麼只需呼叫ShowCursor來增加顯示計數。增加一次顯示計數之後,如果沒有安裝滑鼠則減少它以隱藏游標,如果安裝了滑鼠,則保留其顯示。

即使沒有安裝滑鼠,Windows也保留了滑鼠目前的位置。如果沒有安裝滑鼠,而您又顯示滑鼠游標,游標就可能出現在顯示器的任意位置,直到您確實移動了它。要獲得游標的位置,可以呼叫:

其中pt是POINT結構。函式使用滑鼠的x和y座標來填入POINT欄位。要設定游標位置,可以使用:

在這兩種情況下,x和y都是螢幕座標,而不是顯示區域座標(這是很明顯的,因為這些函式沒有要求hwnd參數)。前面已經提到過,呼叫ScreenToClient和ClientToScreen就能做到螢幕座標與客戶座標的相互轉換。

如果您在處理滑鼠訊息並轉換顯示區域座標時呼叫GetCursorPos ,這些座標可能與滑鼠訊息的lParam參數中的座標稍微有些不同。從GetCursorPos傳回的座標表示滑鼠目前的位置。lParam中的座標則是產生訊息時滑鼠的位置。

您或許想寫一個鍵盤處理程式:使用鍵盤方向鍵來移動滑鼠游標,使用Spacebar和Enter鍵來模擬滑鼠按鍵。您肯定不希望每次按鍵只是將滑鼠游標移動一個圖素,如果這樣做,當要把滑鼠游標從顯示器的一邊移動到另一邊時,會使用者在很長一段時間內都要按住同一個方向鍵。

如果您需要實作滑鼠游標的鍵盤介面,並保持游標的精確定位能力,那麼您可以採用下面的方式來處理按鍵訊息:當按下方向鍵時,一開始滑鼠游標移動較慢,但隨後會加快。您也許還記得WM_KEYDOWN訊息中的lParam參數標誌著按鍵訊息是否是重複活動的結果,這就是此參數的一個重要應用。

在CHECKER中加入鍵盤介面

程式7-3所示的CHECKER2程式,除了包括鍵盤介面外,和CHECKER1是一樣的,您可以使用左、右、上和下方向鍵在25個矩形之間移動游標。Home鍵把游標移動到矩形的左上角, End鍵把游標移動到矩形的右下角。Spacebar和Enter鍵都能切換X標記。

使用鍵盤模擬滑鼠

ShowCursor (TRUE) ;

ShowCursor (FALSE) ;

GetCursorPos (&pt) ;

SetCursorPos (x, y) ;

圖7-3 CHECKER1的螢幕顯示

程式7-3 CHECKER2 CHECKER2.C /*---------------------------------------------------------------------------- CHECKER2.C -- Mouse Hit-Test Demo Program No. 2 (c) Charles Petzold, 1998 ----------------------------------------------------------------------------*/ #include <windows.h> #define DIVISIONS 5 LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("Checker2") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) { MessageBox ( NULL, TEXT ("Program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow ( szAppName, TEXT ("Checker2 Mouse Hit-Test Demo"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while ( GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { static BOOL fState[DIVISIONS][DIVISIONS] ; static int cxBlock, cyBlock ; HDC hdc ; int x, y ; PAINTSTRUCT ps ; POINT point ; RECT rect ; switch (message) { case WM_SIZE : cxBlock = LOWORD (lParam) / DIVISIONS ; cyBlock = HIWORD (lParam) / DIVISIONS ; return 0 ; case WM_SETFOCUS : ShowCursor (TRUE) ; return 0 ; case WM_KILLFOCUS : ShowCursor (FALSE) ; return 0 ; case WM_KEYDOWN : GetCursorPos (&point) ; ScreenToClient (hwnd, &point) ; x = max (0, min (DIVISIONS - 1, point.x / cxBlock)) ; y = max (0, min (DIVISIONS - 1, point.y / cyBlock)) ; switch (wParam) { case VK_UP : y-- ; break ; case VK_DOWN : y++ ; break ; case VK_LEFT : x-- ; break ; case VK_RIGHT : x++ ; break ; case VK_HOME : x = y = 0 ; break ; case VK_END : x = y = DIVISIONS - 1 ; break ; case VK_RETURN : case VK_SPACE : SendMessage (hwnd, WM_LBUTTONDOWN, MK_LBUTTON, MAKELONG (x * cxBlock, y * cyBlock)) ; break ; } x = (x + DIVISIONS) % DIVISIONS ; y = (y + DIVISIONS) % DIVISIONS ; point.x = x * cxBlock + cxBlock / 2 ; point.y = y * cyBlock + cyBlock / 2 ; ClientToScreen (hwnd, &point) ; SetCursorPos (point.x, point.y) ; return 0 ; case WM_LBUTTONDOWN : x = LOWORD (lParam) / cxBlock ; y = HIWORD (lParam) / cyBlock ; if (x < DIVISIONS && y < DIVISIONS) { fState[x][y] ^= 1 ; rect.left = x * cxBlock ; rect.top = y * cyBlock ; rect.right = (x + 1) * cxBlock ; rect.bottom = (y + 1) * cyBlock ; InvalidateRect (hwnd, &rect, FALSE) ; } else MessageBeep (0) ; return 0 ; case WM_PAINT : hdc = BeginPaint (hwnd, &ps) ; for (x = 0 ; x < DIVISIONS ; x++) for (y = 0 ; y < DIVISIONS ; y++) { Rectangle (hdc, x * cxBlock, y * cyBlock, (x + 1) * cxBlock, (y + 1) * cyBlock) ; if (fState [x][y]) { MoveToEx (hdc, x *cxBlock, y *cyBlock, NULL) ; LineTo (hdc, (x+1)*cxBlock, (y+1)*cyBlock) ; MoveToEx (hdc, x *cxBlock, (y+1)*cyBlock, NULL) ; LineTo (hdc, (x+1)*cxBlock, y *cyBlock) ; } } EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY : PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }

CHECKER2中的WM_KEYDOWN的處理方式決定游標的位置(用GetCursorPos),把螢幕座標轉換為顯示區域座標(用ScreenToClient),並用矩形方塊的寬度和高度來除這個座標。這會產生指示矩形位置的x和y值(5×5陣列)。當按下一個鍵時,滑鼠游標可能在或不在顯示區域中,所以x和y必須經過min和max巨集處理以保證它們的範圍是0到4之間。

對方向鍵,CHECKER2近似地增加或減少x和y。如果是Enter鍵或Spacebar鍵,那麼CHECKER2使用SendMessage把WM_LBUTTONDOWN訊息發送給它自身。這種技術類似于在 第六章SYSMETS程式 中把鍵盤介面加到視窗捲動列時所使用的方法。WM_KEYDOWN的處理方式是通過計算指向矩形中心的顯示區域座標,再用ClientToScreen轉換成螢幕座標,然後用SetCursorPos設定游標位置來實作的。

將子視窗用於命中測試

有些程式(例如,Windows的「畫圖」程式),把顯示區域劃分為幾個小的邏輯區域。「畫圖」程式在其左邊有一個由圖示組成的工具功能表區,在底部有顏色功能表區。在這兩個區做命中測試的時候,「畫圖」必須在使用者選中功能表項之前記住功能表的位置。

不過,也可能不需要這麼做。實際上,畫風經由使用子視窗簡化了功能表的繪製和命中測試。子視窗把整個矩形區域劃分為幾個更小的矩形區,每個子視窗有自己的視窗代號、視窗訊息處理程式和顯示區域,每個視窗訊息處理程式接收只適用於它的子視窗的滑鼠訊息。滑鼠訊息中的lParam參數含有相當於該子視窗顯示區域左上角的座標,而不是其父視窗(那是「畫圖」的主應用程式視窗)顯示區域左上角的座標。

以這種方式使用子視窗有助於程式的結構化和模組化。如果子視窗使用不同的視窗類別,那麼每個子視窗都有它自己的視窗訊息處理程式。不同的視窗也可以定義不同的背景顏色和不同的內定游標。在第九章中,我將看到「子視窗控制項」-捲動列、按鈕和編輯方塊等預先定義的子視窗。現在,我們說明在CHECKER程式中是如何使用子視窗的。

CHECKER中的子視窗

程式7-4所示的CHECKER3程式,這一版本建立了25個處理滑鼠單擊的子視窗。它沒有鍵盤介面,但是可以按本章後面的 CHECKER4程式 範例的方法添加。

程式7-4 CHECKER3 CHECKER3.C /*--------------------------------------------------------------------------- CHECKER3.C -- Mouse Hit-Test Demo Program No. 3 (c) Charles Petzold, 1998 ----------------------------------------------------------------------------*/ #include <windows.h> #define DIVISIONS 5 LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; LRESULT CALLBACK ChildWndProc (HWND, UINT, WPARAM, LPARAM) ; TCHAR szChildClass[] = TEXT ("Checker3_Child") ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("Checker3") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) { MessageBox ( NULL, TEXT ("Program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } wndclass.lpfnWndProc = ChildWndProc ; wndclass.cbWndExtra = sizeof (long) ; wndclass.hIcon = NULL ; wndclass.lpszClassName = szChildClass ; RegisterClass (&wndclass) ; hwnd = CreateWindow ( szAppName, TEXT ("Checker3 Mouse Hit-Test Demo"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } LRESULT CALLBACK WndProc ( HWND hwnd, UINT message, WPARAM wParam,LPARAM lParam) { static HWND hwndChild[DIVISIONS][DIVISIONS] ; int cxBlock, cyBlock, x, y ; switch (message) { case WM_CREATE : for (x = 0 ; x < DIVISIONS ; x++) for (y = 0 ; y < DIVISIONS ; y++) hwndChild[x][y] = CreateWindow (szChildClass, NULL, WS_CHILDWINDOW | WS_VISIBLE, 0, 0, 0, 0, hwnd, (HMENU) (y << 8 | x), (HINSTANCE) GetWindowLong (hwnd, GWL_HINSTANCE), NULL) ; return 0 ; case WM_SIZE : cxBlock = LOWORD (lParam) / DIVISIONS ; cyBlock = HIWORD (lParam) / DIVISIONS ; for (x = 0 ; x < DIVISIONS ; x++) for (y = 0 ; y < DIVISIONS ; y++) MoveWindow ( hwndChild[x][y], x * cxBlock, y * cyBlock, cxBlock, cyBlock, TRUE) ; return 0 ; case WM_LBUTTONDOWN : MessageBeep (0) ; return 0 ; case WM_DESTROY : PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; } LRESULT CALLBACK ChildWndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { HDC hdc ; PAINTSTRUCT ps ; RECT rect ; switch (message) { case WM_CREATE : SetWindowLong (hwnd, 0, 0) ; // on/off flag return 0 ; case WM_LBUTTONDOWN : SetWindowLong (hwnd, 0, 1 ^ GetWindowLong (hwnd, 0)) ; InvalidateRect (hwnd, NULL, FALSE) ; return 0 ; case WM_PAINT : hdc = BeginPaint (hwnd, &ps) ; GetClientRect (hwnd, &rect) ; Rectangle (hdc, 0, 0, rect.right, rect.bottom) ; if (GetWindowLong (hwnd, 0)) { MoveToEx (hdc, 0, 0, NULL) ; LineTo (hdc, rect.right, rect.bottom) ; MoveToEx (hdc, 0, rect.bottom, NULL) ; LineTo (hdc, rect.right, 0) ; } EndPaint (hwnd, &ps) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }

CHECKER3有兩個視窗訊息處理程式WndProc和ChildWndProc。WndProc還是主(或父)視窗的視窗訊息處理程式。ChildWndProc是針對25個子視窗的視窗訊息處理程式。這兩個視窗訊息處理程式都必須定義為CALLBACK函式。

因為視窗訊息處理程式與特定的視窗類別結構相關聯,該視窗類別結構由Windows呼叫RegisterClass函式來註冊,CHECKER3需要兩個視窗類別。第一個視窗類別用於主視窗,名為「Checker3」。第二個視窗類別名為「Checker3_Child」。當然,您不必選擇像這樣有意義的名字。

CHECKER3在WinMain函式中註冊了這兩個視窗類別。註冊完常規的視窗類別之後,CHECKER3只是簡單地重新使用wndclass結構中的大多數的欄位來註冊Checker3_Child類別。無論如何,有四個欄位根據子視窗類別而設定為不同的值:

  • pfnWndProc欄位設定為ChildWndProc,子視窗類別的視窗訊息處理程式。
  • cbWndExtra欄位設定為4位元組,或者更確切地用sizeof (long)。該欄位告訴Windows在其為依據此視窗類別的視窗保留的內部結構中,預留了4位元組額外的空間。您能使用此空間來保存每個視窗的可能有所不同的資訊。
  • 因為像CHECKER3中的子視窗不需要圖示,所以hIcon欄位設定為NULL 。
  • pszClassName欄位設定為「Checker3_Child」,是類別的名稱。

通常,在WinMain中,CreateWindow呼叫建立依據Checker3類別的主視窗。然而,當WndProc收到WM_CREATE訊息後,它呼叫CreateWindow 25次以建立25個Checker3_Child類別的子視窗。表7-3是在WinMain中CreateWindow呼叫的參數,與在建立25個子視窗的WndProc中CreateWindow呼叫的參數間的比較。

表7-3

一般情況下,子視窗要求有關位置和大小的參數,但是在CHECKER3中的子視窗由WndProc確定位置和大小。對於主視窗,因為它本身就是父視窗,所以它的父視窗代號是NULL。當使用CreateWindow呼叫來建立一個子視窗時,就需要父視窗代號了。

主視窗沒有功能表,因此參數是NULL。對於子視窗,相同位置的參數稱為子ID(或子視窗ID)。這是唯一代表子視窗的數字。像我們在 第十一章 將看到的一樣,在處理對話方塊的子視窗控制項時,子ID顯得更為重要。對於CHECKER3來說,我只是簡單地將子ID設定為一個數值,該數值是每個子視窗在5×5的主視窗中的x和y位置的組合。

CreateWindow函式需要一個執行實體代號。在WinMain中,執行實體代號可以很容易地取得,因為它是WinMain的一個參數。在建立子視窗時, CHECKER3必須用GetWindowLong來從Windows為視窗保留的結構中取得hInstance值(相對於GetWindowLong,我也能將hInstance的值保存到整體變數,並直接使用它)。

每一個子視窗都在hwndChild陣列中保存了不同的視窗代號。當WndProc接收到一個WM_SIZE訊息後,它將為這25個子視窗呼叫MoveWindow。MoveWindow的參數表示子視窗左上角相對於父視窗顯示區域的座標、子視窗的寬度和高度以及子視窗是否需要重畫。

現在讓我們看一下ChildWndProc。此視窗訊息處理程式為所有這25個子視窗處理訊息。ChildWndProc的hwnd參數是子視窗接收訊息的代號。當ChildWndProc處理WM_CREATE訊息時(因為有25個子視窗,所以要發生25次),它用SetWindowWord在視窗結構保留的額外區域中儲存一個0值(通過在定義視窗類別時使用的cbWndExtra來保留的空間)。ChildWndProc用此值來恢復目前矩形的狀態(有X或沒有X)。在子視窗中單擊時,WM_LBUTTONDOWN處理常式簡單地修改這個整數值(從0到1,或從1到0),並使整個子視窗無效。此區域是被單擊的矩形。WM_PAINT的處理很簡單,因為它所繪製的矩形與顯示區域一樣大。

因為CHECKER3的C原始碼檔案和.EXE檔案比CHECKER1的大(更不用說程式的說明了),我不會試著告訴你說CHECKER3比CHECKER1更簡單。但請注意,我們沒有做任何的滑鼠命中測試!我們所要的,就是知道CHECKER3中是否有個子視窗得到了命中視窗的WM_LBUTTONDOWN訊息。

子視窗和鍵盤

為CHECKER3添加鍵盤介面就像CHECKER系列構想中的最後一步。但在這樣做的時候,可能有更適當的做法。在CHECKER2中,滑鼠游標的位置決定按下Spacebar鍵時哪個區域將獲得標記符號。當我們處理子視窗時,我們能從對話方塊功能中獲得提示。在對話方塊中,帶有閃爍的插入符號或點劃的矩形的子視窗表示它有輸入焦點(當然也可以用鍵盤進行定位)。

我們不需要把Windows內部已有的對話方塊處理方式重新寫過,我只是要告訴您大致上應該如何在應用程式中模擬對話方塊。研究過程中,您會發現這樣一件事:父視窗和子視窗可能要共用同鍵盤訊息處理。按下Spacebar鍵和Enter鍵時,子視窗將鎖定複選標記。按下方向鍵時,父視窗將在子視窗之間移動輸入焦點。實際上,當您在子視窗上單擊時,情況會有些複雜,這時是父視窗而不是子視窗獲得輸入焦點。

CHECKER4.C如程式7-5所示。

程式7-5 CHECKER4 CHECKER4.C /*--------------------------------------------------------------------------- CHECKER4.C -- Mouse Hit-Test Demo Program No. 4 (c) Charles Petzold, 1998 ---------------------------------------------------------------------------*/ #include <windows.h> #define DIVISIONS 5 LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; LRESULT CALLBACK ChildWndProc (HWND, UINT, WPARAM, LPARAM) ; int idFocus = 0 ; TCHAR szChildClass[] = TEXT ("Checker4_Child") ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("Checker4") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) { MessageBox (NULL, TEXT ("Program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } wndclass.lpfnWndProc = ChildWndProc ; wndclass.cbWndExtra = sizeof (long) ; wndclass.hIcon = NULL ; wndclass.lpszClassName = szChildClass ; RegisterClass (&wndclass) ; hwnd = CreateWindow ( szAppName, TEXT ("Checker4 Mouse Hit-Test Demo"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } LRESULT CALLBACK WndProc ( HWND hwnd, UINT message, WPARAM wParam,LPARAM lParam) { static HWND hwndChild[DIVISIONS][DIVISIONS] ; int cxBlock, cyBlock, x, y ; switch (message) { case WM_CREATE : for (x = 0 ; x < DIVISIONS ; x++) for (y = 0 ; y < DIVISIONS ; y++) hwndChild[x][y] = CreateWindow (szChildClass, NULL, WS_CHILDWINDOW | WS_VISIBLE, 0, 0, 0, 0, hwnd, (HMENU) (y << 8 | x), HINSTANCE) GetWindowLong (hwnd, GWL_HINSTANCE), NULL) ; return 0 ; case WM_SIZE : cxBlock = LOWORD (lParam) / DIVISIONS ; cyBlock = HIWORD (lParam) / DIVISIONS ; for (x = 0 ; x < DIVISIONS ; x++) for (y = 0 ; y < DIVISIONS ; y++) MoveWindow ( hwndChild[x][y], x * cxBlock, y * cyBlock, cxBlock, cyBlock, TRUE) ; return 0 ; case WM_LBUTTONDOWN : MessageBeep (0) ; return 0 ; // On set-focus message, set focus to child window case WM_SETFOCUS: SetFocus (GetDlgItem (hwnd, idFocus)) ; return 0 ; // On key-down message, possibly change the focus window case WM_KEYDOWN: x = idFocus & 0xFF ; y = idFocus >> 8 ; switch (wParam) { case VK_UP: y-- ; break ; case VK_DOWN: y++ ; break ; case VK_LEFT: x-- ; break ; case VK_RIGHT: x++ ; break ; case VK_HOME: x = y = 0 ; break ; case VK_END: x = y = DIVISIONS - 1 ; break ; default: return 0 ; } x = (x + DIVISIONS) % DIVISIONS ; y = (y + DIVISIONS) % DIVISIONS ; idFocus = y << 8 | x ; SetFocus (GetDlgItem (hwnd, idFocus)) ; return 0 ; case WM_DESTROY : PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; } LRESULT CALLBACK ChildWndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { HDC hdc ; PAINTSTRUCT ps ; RECT rect ; switch (message) { case WM_CREATE : SetWindowLong (hwnd, 0, 0) ; // on/off flag return 0 ; case WM_KEYDOWN: // Send most key presses to the parent window if (wParam != VK_RETURN && wParam != VK_SPACE) { SendMessage (GetParent (hwnd), message, wParam, lParam) ; return 0 ; } // For Return and Space, fall through to toggle the square case WM_LBUTTONDOWN : SetWindowLong (hwnd, 0, 1 ^ GetWindowLong (hwnd, 0)) ; SetFocus (hwnd) ; InvalidateRect (hwnd, NULL, FALSE) ; return 0 ; // For focus messages, invalidate the window for repaint case WM_SETFOCUS: idFocus = GetWindowLong (hwnd, GWL_ID) ; // Fall through case WM_KILLFOCUS: InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; case WM_PAINT : hdc = BeginPaint (hwnd, &ps) ; GetClientRect (hwnd, &rect) ; Rectangle (hdc, 0, 0, rect.right, rect.bottom) ; // Draw the "x" mark if (GetWindowLong (hwnd, 0)) { MoveToEx (hdc, 0, 0, NULL) ; LineTo (hdc, rect.right, rect.bottom) ; MoveToEx (hdc, 0, rect.bottom, NULL) ; LineTo (hdc, rect.right, 0) ; } // Draw the "focus" rectangle if (hwnd == GetFocus ()) { rect.left += rect.right / 10 ; rect.right -= rect.left ; rect.top += rect.bottom / 10 ; rect.bottom -= rect.top ; SelectObject (hdc, GetStockObject (NULL_BRUSH)) ; SelectObject (hdc, CreatePen (PS_DASH, 0, 0)) ; Rectangle (hdc, rect.left, rect.top, rect.right, rect.bottom) ; DeleteObject (SelectObject (hdc, GetStockObject (BLACK_PEN))) ; } EndPaint (hwnd, &ps) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }

您應該能回憶起每一個子視窗有唯一的子視窗ID,該ID在呼叫CreateWindow建立視窗時定義。在CHECKER3中,此ID是矩形的x和y位置的組合。一個程式可以通過下面的呼叫來獲得一個特定子視窗的子視窗ID:

下面的函式也有同樣的功能:

正如函式名稱所表示的,它主要用於對話方塊和控制視窗。如果您知道父視窗的代號和子視窗ID,此函式也可以獲得子視窗的代號:

在CHECKER4中,整體變數idFocus用於保存目前輸入焦點視窗的子視窗ID。我在前面說過,當您在子視窗上面單擊滑鼠時,它們不會自動獲得輸入焦點。因此,CHECKER4中的父視窗將通過呼叫下面的函式來處理WM_SETFOCUS訊息:

這樣設定一個子視窗為輸入焦點。

ChildWndProc處理WM_SETFOCUS和WM_KILLFOCUS訊息。對於WM_SETFOCUS,它將保存在整體變數idFocus中接收輸入焦點的子視窗ID。對於這兩種訊息,視窗是無效的,並產生一個WM_PAINT訊息。如果WM_PAINT訊息畫出了有輸入焦點的子視窗,則它將用PS_DASH畫筆的風格畫一個矩形以表示此視窗有輸入焦點。

ChildWndProc也處理WM_KEYDOWN訊息。對於除了Spacebar和Enter鍵以外的其他訊息,WM_KEYDOWN都將給父視窗發送訊息。另外,視窗訊息處理程式也處理類似WM_LBUTTONDOWN訊息的訊息。

處理方向移動鍵是父視窗的事情。在風格相似的CHECKER2中,此程式可獲得有輸入焦點的子視窗的x和y座標,並根據按下的特定方向鍵來改變它們。然後通過呼叫SetFocus將輸入焦點設定給新的子視窗。

攔截滑鼠

一個視窗訊息處理程式通常只在滑鼠游標位於視窗的顯示區域,或非顯示區域上時才接收滑鼠訊息。一個程式也可能需要在滑鼠位於視窗外時接收滑鼠訊息。如果是這樣,程式可以自行「攔截」滑鼠。別害怕,這麼做沒什麼大不了的。

設計矩形

為了說明攔截滑鼠的必要性,請讓我們看一下BLOKOUT1程式(如程式7-6所示)。此程式看起來達到了一定的功能,但它卻有十分嚴重的缺陷。

idChild = GetWindowLong (hwndChild, GWL_ID) ;

idChild = GetDlgCtrlID (hwndChild) ;

hwndChild = GetDlgItem (hwndParent, idChild) ;

SetFocus (GetDlgItem (hwnd, idFocus)) ;

程式7-6 BLOKOUT1 BLOKOUT1.C /*---------------------------------------------------------------------------- BLOKOUT1.C -- Mouse Button Demo Program (c) Charles Petzold, 1998 ----------------------------------------------------------------------------*/ #include <windows.h> LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("BlokOut1") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) { MessageBox ( NULL, TEXT ("Program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow (szAppName, TEXT ("Mouse Button Demo"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } void DrawBoxOutline (HWND hwnd, POINT ptBeg, POINT ptEnd) { HDC hdc ; hdc = GetDC (hwnd) ; SetROP2 (hdc, R2_NOT) ; SelectObject (hdc, GetStockObject (NULL_BRUSH)) ; Rectangle (hdc, ptBeg.x, ptBeg.y, ptEnd.x, ptEnd.y) ; ReleaseDC (hwnd, hdc) ; } LRESULT CALLBACK WndProc ( HWND hwnd, UINT message, WPARAM wParam,LPARAM lParam) { static BOOL fBlocking, fValidBox ; static POINT ptBeg, ptEnd, ptBoxBeg, ptBoxEnd ; HDC hdc ; PAINTSTRUCT ps ; switch (message) { case WM_LBUTTONDOWN : ptBeg.x = ptEnd.x = LOWORD (lParam) ; ptBeg.y = ptEnd.y = HIWORD (lParam) ; DrawBoxOutline (hwnd, ptBeg, ptEnd) ; SetCursor (LoadCursor (NULL, IDC_CROSS)) ; fBlocking = TRUE ; return 0 ; case WM_MOUSEMOVE : if (fBlocking) { SetCursor (LoadCursor (NULL, IDC_CROSS)) ; DrawBoxOutline (hwnd, ptBeg, ptEnd) ; ptEnd.x = LOWORD (lParam) ; ptEnd.y = HIWORD (lParam) ; DrawBoxOutline (hwnd, ptBeg, ptEnd) ; } return 0 ; case WM_LBUTTONUP : if (fBlocking) { DrawBoxOutline (hwnd, ptBeg, ptEnd) ; ptBoxBeg = ptBeg ; ptBoxEnd.x = LOWORD (lParam) ; ptBoxEnd.y = HIWORD (lParam) ; SetCursor (LoadCursor (NULL, IDC_ARROW)) ; fBlocking = FALSE ; fValidBox = TRUE ; InvalidateRect (hwnd, NULL, TRUE) ; } return 0 ; case WM_CHAR : if (fBlocking & wParam == '\x1B') // i.e., Escape { DrawBoxOutline (hwnd, ptBeg, ptEnd) ; SetCursor (LoadCursor (NULL, IDC_ARROW)) ; fBlocking = FALSE ; } return 0 ; case WM_PAINT : hdc = BeginPaint (hwnd, &ps) ; if (fValidBox) { SelectObject (hdc, GetStockObject (BLACK_BRUSH)) ; Rectangle ( hdc, ptBoxBeg.x, ptBoxBeg.y, ptBoxEnd.x, ptBoxEnd.y) ; } if (fBlocking) { SetROP2 (hdc, R2_NOT) ; SelectObject (hdc, GetStockObject (NULL_BRUSH)) ; Rectangle (hdc, ptBeg.x, ptBeg.y, ptEnd.x, ptEnd.y) ; } EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY : PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }

此程式展示了一些,它可以實作在Windows的「畫圖」程式中的東西。由按下滑鼠左鍵開始確定矩形的一角,然後拖動滑鼠。程式將畫一個矩形的輪廓,其相對位置是滑鼠目前的位置。當您釋放滑鼠後,程式將填入這個矩形。圖7-4顯示了一個已經畫完的矩形和另一個正在畫的矩形。

那麼,問題在哪裡呢?

請試一試下面的操作:在BLOKOUT1的顯示區域按下滑鼠的左鍵,然後將游標移出視窗。程式將停止接收WM_MOUSEMOVE訊息。現在釋放按鈕,BLOKOUT1將不再獲得WM_BUTTONUP訊息,因為游標在顯示區域以外。然後將游標移回BLOKOUT1的顯示區域,視窗訊息處理程式仍然認為按鈕處於按下狀態。

這樣做並不好,因為程式不知道發生了什麼事情。

攔截的解決方案

BLOKOUT1顯示了一些常見的程式功能,但它的程式碼顯然有缺陷。這種問題就是要使用滑鼠攔截來對付。如果使用者正在拖曳滑鼠,那麼當滑鼠短時間內被拖出視窗時應該沒有什麼大問題,程式應該仍然控制著滑鼠。

攔截滑鼠要比放置一個老鼠夾子容易一些,您只要呼叫:

在這個函式呼叫之後,Windows將所有滑鼠訊息發給視窗代號為hwnd的視窗訊息處理程式。之後收到滑鼠訊息都是以顯示區域訊息的型態出現,即使滑鼠正在視窗的非顯示區域。lParam參數將指示滑鼠在顯示區域座標中的位置。不過,當滑鼠位於顯示區域的左邊或者上方時,這些x和y座標可以是負的。當您想釋放滑鼠時,呼叫:

從而使處理恢復正常。

在32位元的Windows中,滑鼠攔截要比在以前的Windows版本中有多一些限制。特別是,如果滑鼠被攔截,而滑鼠按鍵目前並未被按下,並且滑鼠游標移到了另一個視窗上,那麼將不是由攔截滑鼠的那個視窗,而是由游標下面的視窗來接收滑鼠訊息。對於防止一個程式在攔截滑鼠之後不釋放它而引起整個系統的混亂,這是必要的。

換句話說,只有當滑鼠按鍵在您的顯示區域中被按下時才攔截滑鼠;當滑鼠按鍵被釋放時,才釋放滑鼠攔截。

BLOKOUT2程式

展示滑鼠攔截的BLOKOUT2程式如程式7-7所示。

SetCapture (hwnd) ;

ReleaseCapture () ;

圖7-4 BLOKOUT1的螢幕顯示

程式7-7 BLOKOUT2 BLOKOUT2.C /*---------------------------------------------------------------------------- BLOKOUT2.C -- Mouse Button & Capture Demo Program (c) Charles Petzold, 1998 ----------------------------------------------------------------------------*/ #include <windows.h> LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("BlokOut2") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) { MessageBox ( NULL, TEXT ("Program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow ( szAppName, TEXT ("Mouse Button & Capture Demo"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } void DrawBoxOutline (HWND hwnd, POINT ptBeg, POINT ptEnd) { HDC hdc ; hdc = GetDC (hwnd) ; SetROP2 (hdc, R2_NOT) ; SelectObject (hdc, GetStockObject (NULL_BRUSH)) ; Rectangle (hdc, ptBeg.x, ptBeg.y, ptEnd.x, ptEnd.y) ; ReleaseDC (hwnd, hdc) ; } LRESULT CALLBACK WndProc ( HWND hwnd, UINT message, WPARAM wParam,LPARAM lParam) { static BOOL fBlocking, fValidBox ; static POINT ptBeg, ptEnd, ptBoxBeg, ptBoxEnd ; HDC hdc ; PAINTSTRUCT ps ; switch (message) { case WM_LBUTTONDOWN : ptBeg.x = ptEnd.x = LOWORD (lParam) ; ptBeg.y = ptEnd.y = HIWORD (lParam) ; DrawBoxOutline (hwnd, ptBeg, ptEnd) ; SetCapture (hwnd) ; SetCursor (LoadCursor (NULL, IDC_CROSS)) ; fBlocking = TRUE ; return 0 ; case WM_MOUSEMOVE : if (fBlocking) { SetCursor (LoadCursor (NULL, IDC_CROSS)) ; DrawBoxOutline (hwnd, ptBeg, ptEnd) ; ptEnd.x = LOWORD (lParam) ; ptEnd.y = HIWORD (lParam) ; DrawBoxOutline (hwnd, ptBeg, ptEnd) ; } return 0 ; case WM_LBUTTONUP : if (fBlocking) { DrawBoxOutline (hwnd, ptBeg, ptEnd) ; ptBoxBeg = ptBeg ; ptBoxEnd.x = LOWORD (lParam) ; ptBoxEnd.y = HIWORD (lParam) ; ReleaseCapture () ; SetCursor (LoadCursor (NULL, IDC_ARROW)) ; fBlocking = FALSE ; fValidBox = TRUE ; InvalidateRect (hwnd, NULL, TRUE) ; } return 0 ; case WM_CHAR : if (fBlocking & wParam == '\x1B') // i.e., Escape { DrawBoxOutline (hwnd, ptBeg, ptEnd) ; ReleaseCapture () ; SetCursor (LoadCursor (NULL, IDC_ARROW)) ; fBlocking = FALSE ; } return 0 ; case WM_PAINT : hdc = BeginPaint (hwnd, &ps) ; if (fValidBox) { SelectObject (hdc, GetStockObject (BLACK_BRUSH)) ; Rectangle (hdc, ptBoxBeg.x, ptBoxBeg.y, ptBoxEnd.x, ptBoxEnd.y) ; } if (fBlocking) { SetROP2 (hdc, R2_NOT) ; SelectObject (hdc, GetStockObject (NULL_BRUSH)) ; Rectangle (hdc, ptBeg.x, ptBeg.y, ptEnd.x, ptEnd.y) ; } EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY : PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }

BLOKOUT2程式和BLOKOUT1程式一樣,只是多了三行新程式碼:在WM_LBUTTONDOWN訊息處理期間呼叫SetCapture,而在WM_LBUTTONDOWN和WM_CHAR訊息處理期間呼叫ReleaseCapture。檢查畫出視窗:使視窗小於螢幕大小,開始在顯示區域畫出一塊矩形,然後將滑鼠游標移出顯示區域的右邊或下邊,最後釋放滑鼠按鍵。程式將獲得整個矩形的座標。但是需要擴大視窗才能看清楚它。

攔截滑鼠並非只適用於那些古怪的應用程式。如果您需要滑鼠按鍵在顯示區域按下時都能夠追蹤WM_MOUSEMOVE訊息,並直到滑鼠按鍵被釋放為止,那麼您就應該攔截滑鼠。這樣將簡化您的程式,同時又符合使用者的期望。

滑鼠滑輪

與傳統的滑鼠相比,Microsoft IntelliMouse的特點是在兩個鍵之間多了一個小滑輪。您可以按下這個滑輪,這時它的功能相當於滑鼠按鍵的中鍵;或者您也可以用食指來轉動它,這會產生一條特殊的訊息,叫做WM_MOUSEWHEEL。使用滑鼠滑輪的程式通過滾動或放大文件來回應此訊息。它最初聽起來像一個不必要的隱藏機關,但我必須承認,我很快就習慣於使用滑鼠滑輪來滾動Microsoft Word和Microsoft Internet Explorer了。

我不想討論滑鼠滑輪的所有使用方法。實際上,我只是想告訴您如何在現有的程式(例如程式SYSMETS4)中添加滑鼠滑輪處理程式,以便在顯示區域中捲動資料。最終的SYSMETS程式如程式7-8所示。

程式7-8 SYSMETS4 SYSMETS.C /*---------------------------------------------------------------------------- SYSMETS.C -- Final System Metrics Display Program (c) Charles Petzold, 1998 ----------------------------------------------------------------------------*/ #include <windows.h> #include "sysmets.h" LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("SysMets") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) { MessageBox ( NULL, TEXT ("Program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow ( szAppName, TEXT ("Get System Metrics"), WS_OVERLAPPEDWINDOW | WS_VSCROLL | WS_HSCROLL, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } LRESULT CALLBACK WndProc ( HWND hwnd, UINT message, WPARAM wParam,LPARAM lParam) { static int cxChar, cxCaps, cyChar, cxClient, cyClient, iMaxWidth ; static int iDeltaPerLine, iAccumDelta ; // for mouse wheel logic HDC hdc ; int i, x, y, iVertPos, iHorzPos, iPaintBeg, iPaintEnd ; PAINTSTRUCT ps ; SCROLLINFO si ; TCHAR szBuffer[10] ; TEXTMETRIC tm ; ULONG ulScrollLines ; // for mouse wheel logic switch (message) { case WM_CREATE: hdc = GetDC (hwnd) ; GetTextMetrics (hdc, &tm) ; cxChar = tm.tmAveCharWidth ; cxCaps = (tm.tmPitchAndFamily & 1 ? 3 : 2) * cxChar / 2 ; cyChar = tm.tmHeight + tm.tmExternalLeading ; ReleaseDC (hwnd, hdc) ; // Save the width of the three columns iMaxWidth = 40 * cxChar + 22 * cxCaps ; // Fall through for mouse wheel information case WM_SETTINGCHANGE: SystemParametersInfo (SPI_GETWHEELSCROLLLINES, 0, &ulScrollLines, 0) ; // ulScrollLines usually equals 3 or 0 (for no scrolling) // WHEEL_DELTA equals 120, so iDeltaPerLine will be 40 if (ulScrollLines) iDeltaPerLine = WHEEL_DELTA / ulScrollLines ; else iDeltaPerLine = 0 ; return 0 ; case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; // Set vertical scroll bar range and page size si.cbSize = sizeof (si) ; si.fMask = SIF_RANGE | SIF_PAGE ; si.nMin = 0 ; si.nMax = NUMLINES - 1 ; si.nPage = cyClient / cyChar ; SetScrollInfo (hwnd, SB_VERT, &si, TRUE) ; // Set horizontal scroll bar range and page size si.cbSize = sizeof (si) ; si.fMask = SIF_RANGE | SIF_PAGE ; si.nMin = 0 ; si.nMax = 2 + iMaxWidth / cxChar ; si.nPage = cxClient / cxChar ; SetScrollInfo (hwnd, SB_HORZ, &si, TRUE) ; return 0 ; case WM_VSCROLL: // Get all the vertical scroll bar information si.cbSize = sizeof (si) ; si.fMask = SIF_ALL ; GetScrollInfo (hwnd, SB_VERT, &si) ; // Save the position for comparison later on iVertPos = si.nPos ; switch (LOWORD (wParam)) { case SB_TOP: si.nPos = si.nMin ; break ; case SB_BOTTOM: si.nPos = si.nMax ; break ; case SB_LINEUP: si.nPos -= 1 ; break ; case SB_LINEDOWN: si.nPos += 1 ; break ; case SB_PAGEUP: si.nPos -= si.nPage ; break ; case SB_PAGEDOWN: si.nPos += si.nPage ; break ; case SB_THUMBTRACK: si.nPos = si.nTrackPos ; break ; default: break ; } // Set the position and then retrieve it. Due to adjustments // by Windows it may not be the same as the value set. si.fMask = SIF_POS ; SetScrollInfo (hwnd, SB_VERT, &si, TRUE) ; GetScrollInfo (hwnd, SB_VERT, &si) ; // If the position has changed, scroll the window and update it if (si.nPos != iVertPos) { ScrollWindow ( hwnd, 0, cyChar * (iVertPos - si.nPos), NULL, NULL) ; UpdateWindow (hwnd) ; } return 0 ; case WM_HSCROLL: // Get all the vertical scroll bar information si.cbSize = sizeof (si) ; si.fMask = SIF_ALL ; // Save the position for comparison later on GetScrollInfo (hwnd, SB_HORZ, &si) ; iHorzPos = si.nPos ; switch (LOWORD (wParam)) { case SB_LINELEFT: si.nPos -= 1 ; break ; case SB_LINERIGHT: si.nPos += 1 ; break ; case SB_PAGELEFT: si.nPos -= si.nPage ; break ; case SB_PAGERIGHT: si.nPos += si.nPage ; break ; case SB_THUMBPOSITION: si.nPos = si.nTrackPos ; break ; default: break ; } // Set the position and then retrieve it. Due to adjustments // by Windows it may not be the same as the value set. si.fMask = SIF_POS ; SetScrollInfo (hwnd, SB_HORZ, &si, TRUE) ; GetScrollInfo (hwnd, SB_HORZ, &si) ; // If the position has changed, scroll the window if (si.nPos != iHorzPos) { ScrollWindow ( hwnd, cxChar * (iHorzPos - si.nPos), 0, NULL, NULL) ; } return 0 ; case WM_KEYDOWN : switch (wParam) { case VK_HOME : SendMessage (hwnd, WM_VSCROLL, SB_TOP, 0) ; break ; case VK_END : SendMessage (hwnd, WM_VSCROLL, SB_BOTTOM, 0) ; break ; case VK_PRIOR : SendMessage (hwnd, WM_VSCROLL, SB_PAGEUP, 0) ; break ; case VK_NEXT : SendMessage (hwnd, WM_VSCROLL, SB_PAGEDOWN, 0) ; break ; case VK_UP : SendMessage (hwnd, WM_VSCROLL, SB_LINEUP, 0) ; break ; case VK_DOWN : SendMessage (hwnd, WM_VSCROLL, SB_LINEDOWN, 0) ; break ; case VK_LEFT : SendMessage (hwnd, WM_HSCROLL, SB_PAGEUP, 0) ; break ; case VK_RIGHT : SendMessage (hwnd, WM_HSCROLL, SB_PAGEDOWN, 0) ; break ; } return 0 ; case WM_MOUSEWHEEL: if (iDeltaPerLine == 0) break ; iAccumDelta += (short) HIWORD (wParam) ; // 120 or -120 while (iAccumDelta >= iDeltaPerLine) { SendMessage (hwnd, WM_VSCROLL, SB_LINEUP, 0) ; iAccumDelta -= iDeltaPerLine ; } while (iAccumDelta <= -iDeltaPerLine) { SendMessage (hwnd, WM_VSCROLL, SB_LINEDOWN, 0) ; iAccumDelta += iDeltaPerLine ; } return 0 ; case WM_PAINT : hdc = BeginPaint (hwnd, &ps) ; // Get vertical scroll bar position si.cbSize = sizeof (si) ; si.fMask = SIF_POS ; GetScrollInfo (hwnd, SB_VERT, &si) ; iVertPos = si.nPos ; // Get horizontal scroll bar position GetScrollInfo (hwnd, SB_HORZ, &si) ; iHorzPos = si.nPos ; // Find painting limits iPaintBeg = max (0, iVertPos + ps.rcPaint.top / cyChar) ; iPaintEnd = min (NUMLINES - 1, iVertPos + ps.rcPaint.bottom / cyChar) ; for (i = iPaintBeg ; i <= iPaintEnd ; i++) { x = cxChar * (1 - iHorzPos) ; y = cyChar * (i - iVertPos) ; TextOut ( hdc, x, y, sysmetrics[i].szLabel, lstrlen (sysmetrics[i].szLabel)) ; TextOut ( hdc, x + 22 * cxCaps, y, sysmetrics[i].szDesc, lstrlen (sysmetrics[i].szDesc)) ; SetTextAlign (hdc, TA_RIGHT | TA_TOP) ; TextOut ( hdc, x + 22 * cxCaps + 40 * cxChar, y, szBuffer, wsprintf (szBuffer, TEXT ("%5d"), GetSystemMetrics (sysmetrics[i].iIndex))) ; SetTextAlign (hdc, TA_LEFT | TA_TOP) ; } EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY : PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }

轉動滑輪會導致Windows在有輸入焦點的視窗(不是滑鼠游標下面的視窗)產生WM_MOUSEWHEEL訊息。與平常一樣,lParam將獲得滑鼠的位置,當然座標是相對於螢幕左上角的,而不是顯示區域的。另外,wParam的低字組包含一系列的旗標,用於表示滑鼠按鍵、Shift與Ctrl鍵的狀態。

新的資訊保存在wParam的高字組。其中有一個「delta」值,該值目前可以是120或-120,這取決於滑輪的向前轉動(也就是說,向滑鼠的前面,即帶有按鈕與電纜的一端)還是向後轉動。值120或-120表示文件將分別向上或向下捲動三行。這裏的構想是,以後版本的滑鼠滑輪能有比現在的滑鼠產生更精確的移動速度資訊,並且用delta值,例如40和-40,來產生WM_MOUSEWHEEL訊息。這些值能使文件只向上或向下捲動一行。

為使程式能在一般化環境執行,SYSMETS將在WM_CREATE和WM_SETTINGCHANGE訊息處理時,以SPI_GETWHEELSCROLLLINES作為參數來呼叫SystemParametersInfo。此值說明WHEEL_DELTA的delta值將滾動多少行,WHEEL_DELTA在WINUSER.H中定義。WHEEL_DELTA等於120,並且,在內定情況下SystemParametersInfo傳回3,因此與捲動一行相聯繫的delta值就是40。SYSMETS將此值保存在iDeltaPerLine。

在WM_MOUSEWHEEL訊息處理期間,SYSMETS將delta值給靜態變數iAccumDelta。然後,如果iAccumDelta大於或等於iDeltaPerLine(或者是小於或等於-iDeltaPerLin),SYSMETS用SB_LINEUP或SB_LINEDOWN值產生WM_VSCROLL訊息。對於每一個WM_VSCROLL訊息,iAccumDelta由iDeltaPerLine增加(或減少)。此代碼允許delta值大於、小於或等於滾動一行所需要的delta值。

下面還有

還有一個引人注目的滑鼠問題:建立自訂滑鼠游標。我將在 第十章 ,與其他Windows資源一起討論此問題。