7.2 客戶區滑鼠訊息
摘錄於《Windows程式(第5版,珍藏版).CHarles.Petzold 著》P223
第 6 章已經講到,Windows 只把鍵盤訊息傳送到當前具有輸入焦點的視窗。滑鼠訊息則不同:當滑鼠經過視窗或在視窗內被單擊,則即使該視窗是非活動視窗或不帶輸入焦點,視窗過程還是會收到滑鼠訊息。Windows 定義了 21 種滑鼠訊息。不過,其中 11 種訊息與客戶區無關,稱為“非客戶區訊息”。Windows 應用程式經常忽略這類訊息。
當滑鼠移經視窗客戶區時,視窗過程接收 WM_MOUSEMOVE 訊息。在視窗客戶區內按下或釋放滑鼠按鈕時,視窗過程接收如下表所示的訊息:
按 鈕 | 按 下 | 釋 放 | 第二次按下按鈕 |
---|---|---|---|
左鍵 | WM_LBUTTONDOWN | WM_LBUTTONUP | WM_LBUTTONDBLCLK |
中鍵 | WM_MBOTTONDOWN | WM_MBUTTONUP | WM_MBUTTONDBLCLK |
右鍵 | WM_RBUTTONDOWN | WM_RBUTTONUP | WM_RBUTTONDBLCLK |
視窗過程只對三鍵滑鼠接收 MBUTTON 訊息;只對雙鍵滑鼠接收 RBUTTON 訊息。而只有當視窗類被定義成接收滑鼠雙擊時,視窗過程才接收 DBLCLK(雙擊)訊息。
對所有這些訊息來說,引數 lParam 包含了滑鼠的位置資訊
x = LOWORD (lParam);
y = HIWORD (lParam);
引數 wParam 表示滑鼠按鈕、Shift 鍵和 Ctrl 鍵的狀態。可以利用 WINUSER.H 標頭檔案中定義的位掩碼來測試引數 wParam。字首 MK 代表“滑鼠鍵”(mouse key)。
MK_LBUTTON 按下左鍵 MK_MBUTTON 按下中鍵 MK_RBUTTON 按下右鍵 MK_SHIFT 按下 Shift 鍵 MK_CONTROL 按下 Ctrl 鍵
例如,當接收到 WM_LBUTTONDOWN 訊息時,若
wParam & MK_SHIFT
的值為 TRUE(非零),則表示按下左鍵的同時按下了 Shift 鍵。
滑鼠移經視窗的客戶區時,Windows 系統不會為滑鼠經過的每個畫素位置都產生 WM_MOUSEMOVE 訊息。程式收到的 WM_MOUSEMOVE 訊息個數取決於滑鼠硬體和視窗過程處理滑鼠移動訊息的速度。換言之,如果訊息佇列裡還有未處理的 WM_MOUSEMOVE 訊息,Windows 就不會重複向訊息佇列中新增該訊息。試驗下面這個 CONNECT 程式,可以對 WM_MOUSEMOVE 訊息的產生速度有一個全面的瞭解。
若在非活動視窗的客戶區內按下滑鼠左鍵,Windows 會將該視窗變為活動視窗,並向視窗過程傳送 WM_LBUTTONDOWN 訊息。當視窗過程接收到 WM_LBUTTONDOWN 訊息時,程式就能夠安全地保證該視窗是活動視窗。但是,在事先沒有接收 WM_LBUTTONDOWN 訊息的情況下,視窗過程仍然可以接收 WM_LBUTTONUP 訊息。比如,當用戶在其他視窗內按下滑鼠,再移動到使用者視窗,然後釋放,此時就會發生這種情況。類似地,當移動滑鼠到另一個視窗再釋放時,前一個視窗過程在接收 WM_LBUTTONDOWN 訊息後,就接收不到相應的 WM_LBUTTONUP 訊息。
前面這些規則有兩個例外:
- 即使滑鼠位於視窗的客戶區之外,視窗過程也有辦法“捕獲滑鼠”,並且繼續接收滑鼠訊息。本章會在後面講述如何捕獲滑鼠。
- 若正在顯示一個系統模式訊息框或系統模式對話方塊,則其他任何程式都不能接收滑鼠訊息。當系統模式訊息框或對話方塊處於活動狀態時,它們會阻止系統切換到另一個視窗。例如,關閉 Windows 時彈出的訊息框就是一個系統模式訊息框。
7.2.1 簡單的滑鼠處理示例
為了使使用者對 Windows 系統向程式傳送滑鼠訊息的機制有一個全面的瞭解,CONNECT 程式進行了一些簡單的滑鼠處理。
/*--------------------------------------------------------
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 ("This 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;
int 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-2 所示。)
CONNECT 程式的操作方法如下:將滑鼠指標移到客戶區,按下左鍵,略微移動滑鼠,再釋放左鍵。在按下左鍵時快速移動滑鼠,就可以得到一條經過多個點的曲線。
CONNECT 程式利用了三個在第 5 章討論過的 GDI 函式:SetPixel函式在按下左鍵時為每個 WM_MOUSEMOVE 訊息繪製一個黑色畫素點。(在高解析度顯示裝置中,人眼幾乎看不見一個單獨的畫素點。)繪製直線則需要用到 MoveToEx 函式和 LineTo 函式。
在使用者釋放左鍵時,如果滑鼠指標已經移出客戶區,CONNECT 程式就不會連線這些點,因為程式沒有接收到 WM_LBUTTONUP 訊息。此時如果再將滑鼠移入客戶區,並按下左鍵,CONNECT 程式就是清空客戶區。如果想在客戶區外釋放滑鼠,並繼續設計圖形,就可以在客戶區外按下滑鼠的左鍵,再將滑鼠移入客戶區。
CONNECT 程式最多能夠儲存 1000 個點。假設點的數目為 P,那麼在 CONNECT 程式中,所畫線條的數目等於 P * (P - 1) / 2。對 1000 個點來說,幾乎需要畫 500 000 條直線。取決於具體的硬體,這可能要耗費大約 1 分鐘的時間。Windows 98 是一個搶佔式多工環境,因此在這段時間裡,使用者可以切換到其他程式。但是,當 CONNECT 程式處於忙碌狀態時,使用者不能對 CONNECT 程式做其他的任何操作(比如移動視窗或調整大小)。在第 20 章中,我們將會討論如何處理類似的這種問題。
CONNECT 程式需要耗費一定的時候來繪製直線,因此,滑鼠指標會變成沙漏形,並在處理 WM_PAINT 訊息時回到原來的形狀。這就需要兩次呼叫 SetCursor 函式來切換兩個備用指標。CONNECT 程式還呼叫了兩次 ShowCursor 函式,其中第一次呼叫時引數為 TRUE,第二次呼叫時引數為 FLASE。
有時,“跟蹤”(tracking) 一次常被用來指代程式處理滑鼠移動的方式。然而,跟蹤並不意味著程式的視窗過程要使用一個迴圈來不停地主動監視滑鼠在顯示裝置上的運動。相反,視窗過程只會被動地處理每個到達的滑鼠訊息,然後迅速退出並將控制返還給 Windows 系統。
7.2.2 處理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;
}
// 表示使用者還按下了 Shift 鍵來模擬滑鼠右鍵,所以交給下面的 WM_RBUTTONDOWN 程式碼處理
case WM_RBUTTONDOWN:
[這裡處理右鍵]
return 0;
利用虛擬鍵程式碼 VK_LBUTTON、VK_RBUTTON、BK_MBUTTON、VK_SHIFT 和 VK_CONTROL,Windows 的 GetKeyState 函式也能夠返回滑鼠按鈕或 Shift 等鍵的狀態。當 GetKeyState 函式返回一個負值時,表示已按下了滑鼠按鈕或相應的 Shift 鍵或 Ctrl 鍵。GetKeyState 函式返回的是當前滑鼠或鍵盤的狀態,因此,狀態資訊與正被處理的訊息是完全同步的。對於鍵盤中未被按下的鍵,不能使用 GetKeyState 函式;同樣,在沒有按下滑鼠時,也不能使用 GetKeyState 函式。所以,請不要像下面這樣做:
while (GetKeyState (VK_LBUTTON) >= 0); // 這是錯誤的程式碼!
在訊息的處理過程中,當呼叫 GetKeyState 函式時,只有在已經按下左鍵後,GetKeyState 函式才會報告左鍵被按下的狀態。
7.2.3 滑鼠雙擊
滑鼠雙擊是指連續兩次快速地單擊。為了達到雙擊的效果,兩次單擊不僅要在物理位置上十分靠近(預設情況下,是一個平均系統字型字元寬度、半個字元高度的區域內)。還必須發生在特定的時間間隔之內。這個間隔稱為“雙擊速度”。在控制面板中,可以改變這個時間間隔。
如果想讓視窗過程接收滑鼠雙擊訊息,那麼在呼叫 RegisterClass 初始化視窗類結構時,必須在視窗風格欄位中包含識別符號 CS_DBLCLKS:
wndclass.style = CS_HREDRAW | CS_VREDRAW | 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_LBUTTONDOWN 訊息被簡單地替換成 WM_LBUTTONDBLCLK 訊息。
如果雙擊的第一次單擊與滑鼠單擊所執行的功能一致,那麼處理雙擊訊息就要容易得多。這是,第二次單擊(WM_LBUTTONDBLCLK 訊息)只需在地刺單擊之後執行一些其他操作。例如,考察對 Windows 資源管理器檔案列表的滑鼠操作。滑鼠單擊選中檔案,此時 Windows 資源管理器反相顯示該檔案。滑鼠雙擊時執行下面兩步:第一次單擊會選中檔案,正如滑鼠單擊;第二次單擊指示 Windows 資源管理器開啟檔案。這是多麼簡潔的邏輯處理操作。如果雙擊中第一次單擊與滑鼠所執行的功能不一致,那麼滑鼠的處理邏輯就要複雜得多。