摘录于《Windows程序(第5版,珍藏版).CHarles.Petzold 著》P306
当滚动条问题首次出现在第 4 章时,我讨论过“窗口滚动条”和“滚动条控件”之间的某些差别。SYSMETS 程序使用窗口滚动条,它们显示在窗口的右侧和底部。你可以用标识符 WS_VSCROLL 或 WS_HSCROLL(或同时使用这两者),在创建窗口时为窗口添加窗口滚动条。现在,我们已准备好去创建一些滚动条控件,它们是子窗口,可以出现在父窗口客户区的任何地方。可以使用预定义的“滚动条”窗口类和两种滚动条样式 SBS_VERT 和 SBS_HORZ 之一来创建子窗口滚动条控件。
不同于按钮控件(编辑控件以及列表控件将在后面讨论),滚动条控件不发送 WM_COMMAND 消息到父窗口。它们就像窗口滚动条一样发送 WM_VSCROLL 和 WM_HSCROLL 消息。在处理滚动条消息时,可以用 lParam 参数区分窗口滚动条和滚动条控件。如果 lParam 参数等于 0,就说明它是窗口滚动条;如果等于滚动条窗口句柄,就说明它是滚动条控件。wParam 参数的高位字和低位字部分对于窗口滚动条和滚动条的含义是一样的。
与窗口滚动条有一个固定的宽度不同,Windows 会通过 CreateWindow 调用(或之后的 MoveWindow 调用)中指定的矩形尺寸来调整滚动条控件的尺寸。你可以产生长而窄的滚动条或短而粗的滚动条控件。
如果想创建和窗口滚动条具有相同尺寸的滚动条控件,则可以使用 GetSystemMetrics 获得水平滚动条的高度:
GetSystemMetrics (SM_CYHSCROLL);
或者获得垂直滚动条的宽度。
GetSystemMetrics (SM_CXVSCROLL);
滚动条窗口样式标识符 SBS_LEFTALIGH,SBS_RIGHTALIGN,SBS_TOPALIGN 和 SBS_BOTTOMALIGN 都为滚动条提供标准尺寸。不过它们只适用于对话框中的滚动条。可以用于用于窗口滚动条的同样的函数来设置滚动条控件的范围和位置:
SetScrollRange (hwndScroll, SB_CTL, iMin, iMax, bRedraw);
SetScrollPos (hwndScroll, SB_CTL, iPos, bRedraw);
SetScrollInfo (hwndScroll, SB_CTL, &si, bRedraw);
区别在于,窗口滚动条会使用主窗口的句柄作为第一个参数,SB_VERT 或 SB_HORZ 作为第二个参数。
更神奇的是,名为 COLOR_SCROLLBAR 的系统颜色不再对滚动条起作用。滚动条两端的按钮及滑块的颜色将基于 COLOR_BTNFACE,COLOR_BTNHILIGHT,COLOR_BTNSHADOW,COLOR_BTNTEXT(给小箭头用),COLOR_DKSHADOW 以及 COLOR_BTNHIGHLIGHT。在两端按钮之间的大片区域则基于 COLOR_BTNFACE 和 COLOR_BTNHIGHLIGHT 的某种组合。
如果你俘获了 WM_CTLCOLORSCROLLBAR 消息,就可以从这个消息返回一个画刷来修改原来的颜色。下面来尝试一下。
9.4.1 COLORS1 程序
为了了解滚动条和静态子窗口的使用,同时要为更为深入地探索着色原理,我们将以 COLORS1 程序为例。COLORS1 在左边客户区展示了三个滚动条,颜色分别是“红”、“绿”和“蓝“。在滚动这些滚动条时,客户区的右边会出现对应的三种颜色的组合。三种颜色的值是由滚动条决定的。
/*--------------------------------------------------------
Colors1.C -- Colors Using Scroll Bars
(c) Charles Petzold, 1998
--------------------------------------------------------*/
#include <windows.h>
LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;
LRESULT CALLBACK ScrollProc (HWND, UINT, WPARAM, LPARAM) ;
int idFocus;
WNDPROC OldScroll[3];
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
PSTR szCmdLine, int iCmdShow)
{
static TCHAR szAppName[] = TEXT ("Colors1") ;
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 = CreateSolidBrush(0);
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 ("Color Scroll"),
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 COLORREF crPrim[3] = { RGB(255, 0, 0), RGB (0, 255, 0),
RGB(0, 0, 255) };
static HBRUSH hBrush[3], hBrushStatic;
static HWND hwndScroll[3], hwndLabel[3], hwndValue[3], hwndRect;
static int color[3], cyChar;
static RECT rcColor;
static TCHAR * szColorLabel[] = { TEXT("Red"), TEXT("Green"),
TEXT("Blue") };
HINSTANCE hInstance;
int i, cxClient, cyClient;
TCHAR szBuffer[10];
switch (message)
{
case WM_CREATE:
hInstance = (HINSTANCE) GetWindowLong(hwnd, GWL_HINSTANCE);
// Create the white-rectangle window against which the
// Scroll bars will be positioned. The child window ID is 9.
hwndRect = CreateWindow(TEXT("static"), NULL,
WS_CHILD | WS_VISIBLE | SS_WHITERECT,
0, 0, 0, 0,
hwnd, (HMENU) 9, hInstance, NULL);
for (i = 0; i < 3; ++ i)
{
// The three scroll bars have IDs 0, 1, and 2, with
// scroll bar ranges from 0 through 255.
hwndScroll[i] = CreateWindow(TEXT("scrollbar"), NULL,
WS_CHILD | WS_VISIBLE |
WS_TABSTOP | SBS_VERT,
0, 0, 0, 0,
hwnd, (HMENU) i, hInstance, NULL);
SetScrollRange(hwndScroll[i], SB_CTL, 0, 255, FALSE);
SetScrollPos (hwndScroll[i], SB_CTL, 0, FALSE);
// The three color-name labels have IDs 3, 4, and 5,
// and text strings "Red", "Green", and "Blue".
hwndLabel[i] = CreateWindow(TEXT("static"), szColorLabel[i],
WS_CHILD | WS_VISIBLE | SS_CENTER,
0, 0, 0, 0,
hwnd, (HMENU) (i + 3),
hInstance, NULL);
// The three color-value text fields have IDs 6, 7,
// and 8, and initial text strings of "0".
hwndValue[i] = CreateWindow(TEXT("static"), TEXT("0"),
WS_CHILD | WS_VISIBLE | SS_CENTER,
0, 0, 0, 0,
hwnd, (HMENU) (i + 6),
hInstance, NULL);
OldScroll[i] = (WNDPROC) SetWindowLong (hwndScroll[i],
GWL_WNDPROC, (LONG) ScrollProc);
hBrush[i] = CreateSolidBrush (crPrim[i]);
}
hBrushStatic = CreateSolidBrush(
GetSysColor(COLOR_BTNHIGHLIGHT));
cyChar = HIWORD(GetDialogBaseUnits());
return 0;
case WM_SIZE:
cxClient = LOWORD(lParam);
cyClient = HIWORD(lParam);
SetRect(&rcColor, cxClient / 2, 0, cxClient, cyClient);
MoveWindow(hwndRect, 0, 0, cxClient / 2, cyClient, TRUE);
for (i = 0; i < 3; ++ i)
{
MoveWindow(hwndScroll[i],
(2 * i + 1) * cxClient / 14, 2 * cyChar,
cxClient / 14, cyClient - 4 * cyChar, TRUE);
MoveWindow(hwndLabel[i],
(4 * i + 1) * cxClient / 28, cyChar / 2,
cxClient / 7, cyChar, TRUE);
MoveWindow(hwndValue[i],
(4 * i + 1) * cxClient / 28,
cyClient - 3 * cyChar / 2,
cxClient / 7, cyChar, TRUE);
}
SetFocus(hwnd);
return 0;
case WM_SETFOCUS:
SetFocus(hwndScroll[idFocus]);
return 0;
case WM_VSCROLL:
i = GetWindowLong((HWND)lParam, GWL_ID);
switch (LOWORD(wParam))
{
case SB_PAGEDOWN:
color[i] += 15;
// fall through
case SB_LINEDOWN:
color[i] = min(255, color[i] + 1);
break;
case SB_PAGEUP:
color[i] -= 15;
// fall through
case SB_LINEUP:
color[i] = max (0, color[i] - 1);
break;
case SB_TOP:
color[i] = 0;
break;
case SB_BOTTOM:
color[i] = 255;
break;
case SB_THUMBPOSITION:
case SB_THUMBTRACK:
color[i] = HIWORD(wParam);
break;
default:
break;
}
SetScrollPos(hwndScroll[i], SB_CTL, color[i], TRUE);
wsprintf(szBuffer, TEXT ("%i"), color[i]);
SetWindowText(hwndValue[i], szBuffer);
DeleteObject((HBRUSH) SetClassLong(hwnd, GCLP_HBRBACKGROUND, (LONG)
CreateSolidBrush(RGB(color[0], color[1], color[2]))));
InvalidateRect(hwnd, &rcColor, TRUE);
return 0;
case WM_CTLCOLORSCROLLBAR:
i = GetWindowLong((HWND)lParam, GWL_ID);
return (LRESULT) hBrush[i];
case WM_CTLCOLORSTATIC:
i = GetWindowLong ((HWND)lParam, GWL_ID);
if (i >= 3 && i <= 8) // static text controls
{
SetTextColor((HDC)wParam, crPrim[i % 3]);
SetBkColor((HDC)wParam, GetSysColor(COLOR_BTNHIGHLIGHT));
return (LRESULT) hBrushStatic;
}
break;
case WM_SYSCOLORCHANGE:
DeleteObject(hBrushStatic);
hBrushStatic = CreateSolidBrush(GetSysColor (COLOR_BTNHIGHLIGHT));
return 0;
case WM_DESTROY:
DeleteObject((HBRUSH)
SetClassLong(hwnd, GCL_HBRBACKGROUND, (LONG)
GetStockObject(WHITE_BRUSH)));
for (i = 0; i < 3; ++ i)
DeleteObject(hBrush[i]);
DeleteObject(hBrushStatic);
PostQuitMessage (0) ;
return 0 ;
}
return DefWindowProc (hwnd, message, wParam, lParam) ;
}
LRESULT CALLBACK ScrollProc(HWND hwnd, UINT message,
WPARAM wParam, LPARAM lParam)
{
int id = GetWindowLong(hwnd, GWL_ID);
switch(message)
{
case WM_KEYDOWN:
if (wParam == VK_TAB)
SetFocus(GetDlgItem(GetParent(hwnd),
(id + (GetKeyState(VK_SHIFT) < 0 ? 2 : 1)) % 3));
break;
case WM_SETFOCUS:
idFocus = id;
break;
}
return CallWindowProc (OldScroll[id], hwnd, message, wParam, lParam);
}
COLORS1 让它的子窗口干活。该程序使用了 10 个子窗口控件:3 个滚动条、6 个静态文本窗口和 1 个静态矩形。COLORS1 俘获 WM_CTLCOLORSCROLLBAR 消息来设定红、绿、蓝三个滚动条内部的颜色, 俘获WM_CTLCOLORSTATIC 消息来设定静态文本的颜色。
你可以使用鼠标或键盘来滚动滚动条。可以使用 CLOLORS1 作为开发工具,来试验颜色,并为自己的 Windows 程序选择漂亮的颜色(或者,如果你愿意,也可以用难看的颜色)。COLORS1 的显示结果如图 9-6 所示。
图 9-6 COLORS1 的显示
COLORS1 不处理 WM_PAINT 消息。几乎所有 COLORS1 的工作都是由子窗口完成的。
显示在客户区右边的颜色实际上时窗口的背景颜色。样式为 SS_WHITERECT 的静态子窗口屏蔽了左边的一半客户区。三个滚动条子窗口控件用的是 SBS_CENTER 样式。这些滚动条都放在 SS_WHITERECT 子窗口之上。另外 6 个样式为 SS_CENTER(文本居中)的静态子窗口标签和颜色值。在 WinMain 函数中,COLORS1 使用 CreateWindow 建立了一个普通股的层叠窗口和 10 个子窗口。SS_WHITERECT 和 SS_CENTER 静态窗口使用“静态”窗口类;三个滚动条使用“滚动条”窗口类。
CreateWindow 最初把 x 坐标、y 坐标、宽度和高度参数的值设为 0,因为位置和大小取决于客户区的大小,而后者是未知的。当 COLORS1 收到 WM_SIZE 消息时,它的窗口过程会使用 MoveWindow 调整所有 10 个子窗口的尺寸。因此只要调整了 COLORS1 窗口的大小,滚动条就会产生相应比例的变化。
WndProc 窗口过程收到 WM_VSCROLL 消息之后,lParam 参数的高位字代表子窗口的句柄。我们可以用 GetWindowLong 来获得窗口 ID:
i = GetWindowLong ((HWND) lParam, GWL_ID);
为方便起见,我们将三个滚动条的 ID 值分别设置为 0、1、2,因此 WndProc 能分辨出是哪一个滚动条产生的消息。
由于在新建窗口时,子窗口的句柄被保存在数组中,所以 WndProc 可以处理相应的滚动条消息,并调用 SetScrollPos 为相应的滚动条设置新的值:
SetScrollPos (hwndScroll[i], SB_CTL, color[i], TRUE);
WndProc 还会改变滚动条下方子窗口的文本信息:
wsprintf (szBuffer, TEXT("%i"), color[i]);
SetWindowText (hwndValue[i], szBuffer);
9.4.2 自动键盘接口
滚动条控件还可以处理击键消息,但前提是它们拥有输入焦点。下表显示了键盘光标键是如何转换为滚动条消息的。
光标键 | 滚动条消息 wParam 的值 |
---|---|
Home | SB_TOP |
End | SB_BOTTOM |
Page Up | SB_PAGEUP |
Page Down | SB_PAGEDOWN |
← 或 ↑ | SB_LINEUP |
→ 或 ↓ | SB_LINEDOWN |
事实上,SB_TOP 和 SB_BOTTOM 这两个滚动条消息只能通过键盘产生。如果你想让滚动条控件在鼠标单击滚动条时取得输入焦点,必须在 CreateWindow 函数的窗口类参数中加入WS_TABSTOP 标识符。当滚动条获得输入焦点时,滚动条滑块上会显示一个闪烁的灰色块。
为了给滚动条提供完整的键盘接口,需要作更多的工作。首先,WndProc 窗口过程必须专门提供滚动条所需要的输入焦点。为此,它会处理 WM_SETFOCUS 消息,这是父窗口得到输入焦点时接收到的消息。WndProc 只需把输入焦点给其中一个滚动条:
SetFocus (hwndScroll[idFocus]);
这里的 idFocus 是一个全局变量。
但还需要一些方法通过键盘把输入焦点从一个滚动条移到另一个,最好是用 Tab 键。这样做会有一定难度,因为一旦滚动条获得输入焦点,它就会处理所有的按键信息。但是滚动条关心的只有光标键,它会忽视 Tab 键。解决问题这个问题的出路在于使用“窗口子类”这一技术。我们把这个概念加入 COLORS1 中,通过 Tab 键来实现滚动条之间的切换。
9.4.3 窗口子类
滚动条控件的窗口过程在 Windows 内部。但是你可以调用 GetWindowLong 来获取这个窗口过程的地址,使用 GWL_WNDPROC 标识符作为参数即可。此外,还
可以通过用 SetWindowLong,为滚动条设置一个新的窗口过程。这种技术被称为“窗口子类”,它的功能非常强大。它可以让你连接到已有的窗口过程,在你的程序中处理一些消息,并将其他消息传递给旧的窗口过程。
在 COLORS1 中,能初步处理滚动条消息的窗口过程名为 ScrollProc,它列在 COLORS1.C 文件的结束部分。由于 COLORS1 中的 ScrollProc 是一个被 Windows 调用的函数,因此它必须被定义成 CALLBACK 类型。
对这三个滚动条中的每一个,COLORS1 都使用 SetWindowLong 来设置新的滚动条窗口过程地址,同时获得现有滚动条窗口过程的地址:
OldScroll[i] = (WNDPROC) SetWindowLong (hwndScroll[i],GWL_WNDPROC,
(LONG) ScrollProc);
现在,ScrollProc 获得了 Windows 发送给 COLORS1 程序中三个滚动条的所有消息(但当然不包括在其他程序中的滚动条)。当它收到 Tab 或 Shift-Tab 击键信息后,此 ScrollProc 窗口过程只需更改输入焦点到下一个(或前一个)滚动条即可。它使用 CallWindowProc 来调用旧的滚动条窗口过程。
9.4.4 背景着色
在 COLORS1 定义它的窗口类时,给它的客户区指定了一个黑色的背景画刷:
wndclass.hbrBackground = CreateSolidBrush(0);
当你更改 COLORS1 的滚动条设置时,程序必须创建一个新的画刷,并把新画刷句柄存入窗口类的结构中。
正如我们使用 GetWindowLong 和 SetWindowLong 来获取和设置滚动条窗口过程一样,我们可以使用 GetClassWord 和 SetClassWord 来获取和设置这个画刷的句柄。
你可以创建新的画刷,把句柄存入到窗口类结构中,然后删除旧的画刷:
DeleteObject((HBRUSH)
SetClassLong(hwnd, GCLP_HBRBACKGROUND, (LONG)
CreateSolidBrush(RGB(color[0], color[1], color[2]))));
下一次 Windows 重新给窗口背景着色的时候,Windows 将使用这个新画刷。如果要强制 Windows 擦除背景,我们可以让客户区的右半部分失效:
InvalidateRect(hwnd, &rcColor, TRUE);
函数的第三个参数输入 TRUE(非零值),表明我们希望在窗口重绘之前擦除背景。
InvalidateRect 函数会让 Windows 把 WM_PAINT 消息放在窗口过程的消息队列中。由于 WM_PAINT 消息属于低优先级消息,因此如果你仍然在移动鼠标或光标键,此消息将不会立即被处理。另一种情况是,如果你希望窗口在颜色修改后立即更新,则可以在 InvalidateRect 函数被调用后调用下面的函数:
UpdateWindow (hwnd);
不过,这个函数可能会拖慢键盘和鼠标处理速度。
COLORS1 的 WndProc 函数不直接处理 WM_PAINT 消息,而是将它传递给 DefWindowProc 函数。WM_PAINT 消息在 Windows 中的默认处理方式仅仅是调用 BeginPaint 和 EndPaint 来使窗口有效。因为我们在 InvalidateRect 中指定旧背景应该被删除,所以调用 BeginPaint 会使 Windows 产生一条 WM_ERASEBKGND(擦除背景)消息。WndProc 也会忽略此消息。Windows 会对该消息进行处理,它使用窗口类指定的画刷来擦除这个客户区的背景。
通常情况下,在终止程序前进行清理总是一个不错的好主意,因此在处理 WM_DESTROY 消息时,DeleteObject 被再次调用:
DeleteObject((HBRUSH)
SetClassLong(hwnd, GCL_HBRBACKGROUND,
(LONG)GetStockObject(WHITE_BRUSH)));
9.4.5 给滚动条和静态文本着色
在 COLORS1 中,三个滚动条内部和六个文本框总文本的颜色分别被着色为红色、绿色和蓝色。滚动条的着色是通过处理 WM_CTLCOLORSCROLLBAR 消息来完成的。
在 WndProc 中,为三个画刷句柄定义了一个静态数组:
static HBRUSH hBrush[3];
在处理 WM_CREATE 的过程中,创建了这三个画刷:
for (i = 0; i < 3; ++ i)
hBrush[i] = CreateSolidBrush(crPrim[i]);
在这里 crPrim 数组中包含三个基色的 RGB 值。在处理 WM_CTLCOLORSCROLLBAR 时,窗口过程返回三个画刷中的一个:
case WM_CTLCOLORSCROLLBAR:
i = GetWindowLong((HWND)lParam, GWL_ID);
return (LRESULT) hBrush[i];
这些画刷必须在处理 WM_DESTROY 消息期间被销毁:
for (i = 0; i < 3; ++ i)
DeleteObject(hBrush[i]);
类似地,静态文本框中文本的着色是通过处理 WM_CTLCOLORSTATIC 消息和调用 SetTextColor 函数完成的。文本的背景通过调用 SetBkColor 设置为系统颜色 COLOR_BTNHIGHLIGHT。这回导致文本的背景与滚动条后面的静态矩形控件具有相同的颜色。对于静态文本控件,这种文本的背景颜色只应用于字符串中每个字符背后的矩形框,而不是控件窗口的整个宽度。要做到这一点,窗口过程必须返回一个 COLOR_BTNHIGHTLIGHT 颜色的画刷句柄。这个被称为 hBrushStatic 的画刷时在 WM_CREATE 处理期间产生的,并在 WM_DESTROY 消息处理期间被销毁。
程序在 WM_CREATE 消息处理期间产生 COLOR_BTNHIGHLIGHT 颜色的画刷,并在程序生命周期中使用它。但这样我们会碰到一个小问题。如果 COLOR_BTNHIGHLIGHT 颜色在程序运行过程中被改变了,静态矩形的颜色会改变,文本背景颜色会改变,但整个文本窗口控件的背景仍将保持原有的 COLOR_BTNHIGHLIGHT 颜色。
为解决这一问题,COLORS1 在处理 WM_SYSCOLORCHANGE 消息时简单地用新颜色重新创建 hBrushStatic。