摘录于《Windows程序(第5版,珍藏版).CHarles.Petzold 著》P255
设计一个更好的捕鼠器,人们自然会不辞辛苦来到你的家门前。”我的母亲曾经这样教导我,不知不觉中阐释了 Emerson 的思想。当然,而今设计一个更好的“鼠标”也许更有价值。
微软的 IntelliMouse 系列鼠标增加了传统鼠标的功能,在两个按钮中间 添加了一个小滚轮。按下这个滚轮,这是它相当于鼠标的中键;中指滑动这个滚轮,这时它会产生一个特殊的消息,名为 WM_MOUSEWHEEL。程序利用鼠标滚轮,相应的滚动或缩放文档。最初,这听上去就像 一个毫无必要的噱头,但是我必须承认,我很快就习惯了使用鼠标滚轮来滚动浏览 Word 文档和 Internet Explorer。我不想专门去讨论如何使用鼠标滚轮,实际上,这里我只讲述如何给一个现有的程序(比如 SYSMETS4 程序)增加鼠标滚轮处理能力,以便在客户区内滚动数据。最后的 SYSMETS 程序如下所示。
/*----------------------------------------------------
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 ("This 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 horizontal 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_LINELEFT, 0);
break;
case VK_RIGHT:
SendMessage(hwnd, WM_HSCROLL, SB_LINERIGHT, 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) ;
}
发送给具有输入焦点的窗口(而不是鼠标指针下面的窗口)。和通常一样,参数 lParam 包含鼠标的位置信息;但是,这些做你是相对于屏幕左上角的坐标,而不是相对客户区的坐标。同样,参数 wParam 的低位字包含一系列标志(flag)。用于指明鼠标按钮、Shift 键和 Ctrl 键的状态。
新的信息出现在参数 wParam 的高位字。这是一个“增量” (delta)数值,当前它可能等于 120 或 -120,取决于滚轮向前滚动(也就是说,朝鼠标的前端,即带按钮和鼠标线的一端)还是向后滚动。数值 120 或 -120 表示文档分别向上或向下滚动三行。这里有一种构想:未来的鼠标滚动能够实现比当前鼠标更好的移动梯度,比如可以产生增量数值为 40 和 -40 的 WM_MOUSEWHEEL 消息。这些数值能够使文档像上或向下滚动一行。
为了保持程序的通用性,SYSMETS 程序在处理 WM_CREATE 消息和 WM_SETTINGCHANGE 消息时调用了参数为 SPI_GETWHEELSCROLLLINES 的 SystemParametersInfo 函数。这个参数表示了每个增量数值能够滚动多少行,其中增量数值用定义在WINUSER.H 头文件中的 WHEEL_DELTA来标识。WHEEL_DELTA 等于 120,且默认情况下 SystemParametersInfo 返回值为 3,因此,每滚动一行的增量是 40。SYSMETS 程序将这个值保存在 iDeltaPerLine 中。
在处理 WM_MOUSEWHEEL 消息的过程中,SYSMETS 程序给静态变量 iAccumDelta 增加了一个增量数值。然后,如果 iAccumDelta 大于等于 iDeltaPerLine(或小于等于 -iDeltaPerLine)时,SYMETS 程序就会利用 SB_LINEUP 或 SB_LINEDOWN 产生 WM_VSCROLL 消息。对于每个 WM_VSCROLL 消息,iAccumDelta 都会相应地减去(或加上)iDeltaPerLine 值。此代码允许增量值大于、小于或等于滚动一行所需的增量值。