摘录于《Windows程序(第5版,珍藏版).CHarles.Petzold 著》P286
我们将通过如图9-1 所示的 BTNLOOK 程序去了解按钮窗口类。BTNLOOK 创建了十个子窗口按钮控件,对应于 10 种标准的按钮样式。
/*--------------------------------------------------------
BTNLOOK.C -- Button Look Program
(c) Charles Petzold, 1998
--------------------------------------------------------*/
#include <windows.h>
struct
{
int iStyle;
TCHAR * szText;
}
button[] =
{
BS_PUSHBUTTON, TEXT ("PUSHBUTTON"),
BS_DEFPUSHBUTTON, TEXT ("DEFPUSHBUTTON"),
BS_CHECKBOX, TEXT ("CHECKBOX"),
BS_AUTOCHECKBOX, TEXT ("AUTOCHECKBOX"),
BS_RADIOBUTTON, TEXT ("RADIOBUTTON"),
BS_3STATE, TEXT ("3STATE"),
BS_AUTO3STATE, TEXT ("AUTO3STATE"),
BS_GROUPBOX, TEXT ("GROUPBOX"),
BS_AUTORADIOBUTTON, TEXT ("AUTORADIO"),
BS_OWNERDRAW, TEXT ("OWNERDRAW")
};
#define NUM (sizeof button / sizeof button[0])
LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
PSTR szCmdLine, int iCmdShow)
{
static TCHAR szAppName[] = TEXT ("BtnLook") ;
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 ("Button Look"),
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 hwndButton[NUM];
static RECT rect;
static TCHAR szTop[] = TEXT ("message wParam lParam"),
szUnd[] = TEXT ("_______ ______ ______"),
szFormat[] = TEXT ("%-16s%04X-%04X %04X-%04X"),
szBuffer[50];
static int cxChar, cyChar;
HDC hdc;
PAINTSTRUCT ps;
int i;
switch (message)
{
case WM_CREATE:
cxChar = LOWORD(GetDialogBaseUnits());
cyChar = HIWORD(GetDialogBaseUnits());
for (i = 0; i < NUM; ++ i)
hwndButton[i] = CreateWindow (TEXT("button"),
button[i].szText,
WS_CHILD | WS_VISIBLE | button[i].iStyle,
cxChar, cyChar * (1 + 2 * i),
20 * cxChar, 7 * cyChar / 4,
hwnd, (HMENU) i,
((LPCREATESTRUCT) lParam)->hInstance, NULL);
return 0;
case WM_SIZE:
rect.left = 24 * cxChar;
rect.top = 2 * cyChar;
rect.right = LOWORD(lParam);
rect.bottom = HIWORD(lParam);
return 0;
case WM_PAINT:
InvalidateRect(hwnd, &rect, TRUE);
hdc = BeginPaint(hwnd, &ps);
SelectObject(hdc, GetStockObject(SYSTEM_FIXED_FONT));
SetBkMode(hdc, TRANSPARENT);
TextOut(hdc, 24 * cxChar, cyChar, szTop, lstrlen(szTop));
TextOut(hdc, 24 * cxChar, cyChar, szUnd, lstrlen(szUnd));
EndPaint(hwnd, &ps);
return 0;
case WM_DRAWITEM:
case WM_COMMAND:
ScrollWindow(hwnd, 0, -cyChar, &rect, &rect);
hdc = GetDC(hwnd);
SelectObject(hdc, GetStockObject(SYSTEM_FIXED_FONT));
TextOut(hdc, 24 * cxChar, cyChar * (rect.bottom / cyChar - 1),
szBuffer,
wsprintf(szBuffer, szFormat,
message == WM_DRAWITEM ? TEXT ("WM_DRAWITEM") :
TEXT ("WM_COMMAND"),
HIWORD(wParam), LOWORD(wParam),
HIWORD(lParam), LOWORD(lParam)));
ReleaseDC(hwnd, hdc);
ValidateRect(hwnd, &rect);
break;
case WM_DESTROY:
PostQuitMessage (0) ;
return 0 ;
}
return DefWindowProc (hwnd, message, wParam, lParam) ;
}
在单击每个按钮时,该按钮会向其父窗口过程发送 WM_COMMAND 消息,这个窗口过程就是我们所熟悉的 WndProc。BTNLOOK 的 WndProc 函数将在客户区的右侧显示这个信息的 wParam 和 lParam 参数,如图 9-2 所示。
BS_OWNERDRAW 样式的按钮在这个窗口中只显示了背景阴影,这是因为这种按钮需要由程序自己来复制按钮的绘制。当 WM_DRAWITEM 消息的 lParam 参数是一个指向 DRAWITEMSTRUCT结构类型的指针时,它表明按钮需要绘制。这些消息也会显示在 BTNLOOK 程序中。我将在本章的随后部分详细讨论这种需要自绘制的按钮。
图 9-2 BTNLOOK 的显示
9.1.1 创建子窗口
BTNLOOK 定义了一个名为 Button 的数据结构,它存有 10 种不同类型的按钮样式和相应的描述文本字符串。这宗按钮窗口样式都以字母 BS 开始,它是 button style 的缩写。在 WndProc 的 WM_CREATE 消息处理中,10 个按钮子窗口将在一个 for 循环中产生。CreateWindow 函数调用使用了一下的参数:
类名 | TEXT("button") |
窗口文本 | button[i].szText |
窗口样式 | WS_CHILD | WS_VISIBLE | button[i].iStyle |
x 坐标 | cxChar |
y 坐标 | cyChar * (1 + 2 * i) |
宽度 | 20 * xChar |
高度 | 7 * yChar / 4 |
父窗口 | hwnd |
子窗口 ID | (HMENU) i |
案列句柄 | ((LPCREATESTRUCT)lParam)->hInstance |
额外参数 | NULL |
类名参数是预定义的名称。窗口的样式包括 WS_CHILD, WS_VISIBLE 和定义在按钮结构中的 10 种按钮样式之一(BS_PUSHBUTTON, BS_DEFPUSHBUTTON 等等)。窗口文本参数是要和每个按钮一起显示的文本。对于普通窗口,这个参数表示显示在标题栏中的文本。这里我只是用该文本来标识按钮样式。
x 和 y 位置参数指定子窗口左上角的位置,它是相对于其父窗口客户区的左上角的位置。宽度和高度参数指定每个子窗口的宽度和高度。请注意,我使用GetDialogBaseUnits 函数来获取字符的默认字体的宽度和高度。对话框使用这个函数获取字体尺寸。该函数返回一个 32 位值,它的低位字和高位字分别是子窗口的宽度和高度。虽然 GetDialogBaseUnits 与 GetTextMetrics 返回类似的数据,但是 GetDialogBaseUnits 更易于使用,它更利于保持与对话框中的控件的更多的一致性。
在子窗口中,每个子窗口的 ID 参数应是唯一的。此 ID 帮助你的窗口过程确认 WM_COMMAND 消息是从哪个子窗口发来的。请注意,该子窗口 ID 作为 CreateWinow 函数的参数,它的位置原本是用于指定该程序的菜单的,所以它必须转换成 HMENU 类型。
CreateWindow 函数的实例句柄参数看起来有点奇怪,但我们利用了这样一个事实,即在WM_CREATE 消息里 lParam 实际上时一个指向 CREATESTRUCT 结构类型的指针,hInstance 则是这个结构的一个成员。因此,我们把 lParam 转换为一个指向 CREATESTRUCT 结构的指针,并从中获得 hInstance 句柄。
(有些 Windows 程序使用全局变量 hInst,来让窗口过程获得 WinMain 的实例句柄。在 WinMain 函数中,只需要在主窗口产生之前,简单地如下设置:
hInst = hInstance;
在第 7 章的 CHECKER3 程序中,我们
用 GetWindowLong 来获得实例句柄:
GetWindowLong (hwnd, GWL_HINSTANCE);
可以使用以上如何一种方法。)
在 CreateWindow 调用之后,我们不需要对这些子窗口做任何更多的事情。按钮窗口过程在 Windows 中维护所有的按钮和处理重绘任务。(唯一的例外是 BS_OWNERDRAW 样式的按钮,此按钮样式需要程序来绘制按钮。)在该程序终止时,Windows 在销毁父窗口的同时销毁这些子窗口。
9.1.2 子窗口传递信息给父窗口
运行 BTNLOOK 时,会看到不同的按钮类型显示在左侧客户区。正如我前面提到的,在用鼠标单击一个按钮时,子窗口控件发送 WM_COMMAND 消息给其父窗口。BTNLOOK 俘获 WM_COMMAND 消息并显示 wParam 与 lParam 的值。这几个值的含义如下:
LOWORD(wParam) | 子窗口 ID |
HIWORD(wParam) | 通知码 |
lParam | 子窗口句柄 |
子窗口 ID 是新建子窗口时传递给 CreateWindow 的值。在 BTNLOOK 中,这些 ID 的值是 0 到 9,代表显示在客户区的 10 个按钮。子窗口句柄是 Windows 从 CreateWindow 函数调用返回的值。
通知码进一步给出每条消息的意思。按钮通知码的可能值定义在 Windows 头文件中,如下表所示:
按钮通知码标识符 | 值 |
---|---|
BN_CLICKED | 0 |
BN_PAINT | 1 |
BN_HILITE 或 BN_PUSHED | 2 |
BN_UNHILITE 或 BN_UNPUSHED | 3 |
BN_DISABLE | 4 |
BN_DOUBLECLICKED 或 BN_DBLCLK | 5 |
BN_SETFOCUS | 6 |
BN_KILLFOCUS | 7 |
你会发现,在用鼠标单击按钮时,按钮文本的周围会出现一圈虚线。这表明该按钮拥有输入焦点。所有的键盘输入消息会送到这个子窗口按钮控件,而不是主窗口。按钮控件一旦获得输入焦点,便会忽略所有的按键操作,但空格键除外,此时的空格键具有和单击鼠标相同的效果。
9.1.3 父窗口传递信息给子窗口
虽然在 BTNLOOK 中没有演示,但是窗口过程也可以发送消息给子窗口控件。这些消息包括前缀为 WM 的许多窗口消息。此外,在 WINUSER.H 中定义了 8 个专用于按钮的消息;每个消息都以字母 BM 开头,以表示按钮消息。这些按钮消息如下表所示:
按钮消息 | 值 |
---|---|
BM_GETCHECK | 0x00F0 |
BM_SETCHECK | 0x00F1 |
BM_GETSTATE | 0x00F2 |
BM_SETSTATE | 0x00F3 |
BM_SETSTYLE | 0x00F4 |
BM_CLICK | 0x00F5 |
BM_GETTIMAGE | 0x00F6 |
BM_SETIMAGE | 0x00F7 |
父窗口发送 BM_GETCHECK 和 BM_SETCHECK 消息给子窗口控件,以获取和设置复选框和单选按钮的选择状态。在用鼠标或空格键单击窗口时,BM_GETSTATE 和 BM_SETSTATE 消息将反映一个窗口的状态是正常的还是被单击了。在我们介绍每一种类型的按钮时,会了解这些消息的工作机制。BM_SETSTYLE 消息允许在创建按钮后改变按钮样式。
每个子窗口有一个窗口句柄和唯一的 ID。知道其中一个就可以得到另一个。如果知道子窗口句柄,则通过以下函数调用,获得其 ID:
id = GetWindowLong (hwndChild, GWL_ID);
此函数(和 SetWindowLong 函数一起)在第 7 章的 CHECKER3 程序中被用来维护一个特定区域内的数据,这个区域是窗口类被注册时产生的。创建子窗口的时候,Windows 会保留这个区域,只有用 GWL_ID 标识符才能访问。也可以用另一个函数,如下所示:
id = GetDlgCtrlId (hwndChild);
虽然函数名称中的“Dlg”会使人联想到对话框,但它的确是一个通用的函数。
在知道子窗口 ID 和父窗口句柄后,可以得到子窗口句柄:
hwndChild = GetDlgItem (hwndParent, id);
9.1.4 按钮
在 BTNLOOK 中显示的前两个按钮是“按键”按钮(push button)。此类按钮是一种带有文本的矩形,这些文本是在 CreateWindow 调用的窗口文本参数中提供的。而 CreateWindow 或 MoveWindow 调用中指定的宽度和高度则确定了矩形的大小。文本显示在矩形的中心。
按键按钮控件主要用于立即启动某些行动而不必保留任何类型的开/关指示。有两种类型的按键按钮控件,它们的窗口样式分别是 BS_PUSHBUTTON 和 BS_DEFPUSHBUTTON。BS_DEFPUSHBUTTON 的 “DEF” 表示 “默认值”。在被用于设计对话框时,BS_PUSHBUTTON 控件和 BS_DEFPUSHBUTTON 控件功能是完全不同的。但在被用作子窗口控件时,两种类型按钮的表现基本相同,虽然 BS_DEFPUSHBUTTON 会有一个较重的轮廓。
按键按钮的最佳视觉高度是字符高度的 7/4,这是我们在 BTNLOOK 中使用的高度。这种按钮的宽度至少需要容纳文本的宽度,外加两个额外的字符宽度。
当鼠标指针显示在按钮内部时,按下鼠标按钮将会导致按钮用三维样式的阴影重绘自己,彷佛它被按下似的。当释放鼠标,按钮会恢复原貌,并发出一个通知码为 BN_CLICKED 的 WM_COMMAND 消息到父窗口。像其他按钮类型一样,当一个按钮有输入焦点时,文本会被虚线包围。按下和释放空格键的效果同按下和释放鼠标一样。
通过给 Windows 发送一个 BM_SETSTATE 消息,可以模拟按键按钮的状态变化。下面的语句将导致按钮看上去被按住一样:
SendMessage (hwndButton, BM_SETSTATE, 1, 0);
调用下面的函数则会让按钮回到正常状态:
SendMessage (hwndButton, BM_SETSTATE, 0, 0);
窗口句柄 hwndButton 是 CreateWindow 调用的返回值。
也可以给按键按钮发送一个 BM_GETSTATE 消息。子窗口控件返回当前按钮的状态:如果按键是按下的,则返回 TRUE;如果没有被按下,则返回 FALSE。然而大多数应用程序不需要此信息。而因为这种按钮不保留任何开/关信息,所以 BM_SETCHECK 和 BM_GETCHECK 消息没有被用到。
9.1.5 复选框
复选框(check box)是一个带文本的正方形框,文本通常出现在复选框的右侧。(如果在创建按钮时包括 BS_LEFTTEXT 样式,则文本会出现在按钮左侧;还可以组合使用 BS_RIGHT 样式使文本右对齐。)复选框通常用在应用程序中,以允许用户选择多个选项。复选框的常见只能是作为切换开关:单击方框即可选中此功能选项(出现一个选中标记),再单击方框即可取消此功能选项(选中标记消失)。
复选框最常见的两类样式是 BS_CHECKBOX 和 BS_AUTOCHECKBOX。在使用 BS_CHECKBOX 样式时,必须自己给控件发送一个 BM_SETCHECK 消息来设置其选中标记。wParam 参数设置为 1 会创建一个选中标记,设置为 0 则清除标记。可以向控件发送 BM_GETCHECK 信息来获取复选框当前的被选状态。在处理来自控件的 WM_COMMAND 消息时,可使用下面的代码切换选中标记:
SendMessage ((HWND)lParam, BM_SETCHECK, (WPARAM)
!SendMessage ((HWND)lParam, BM_GETCHECK, 0, 0), 0);
请注意,在第二行中,SendMessage 函数前面有个操作符!。lParam 值是 WM_COMMAND 消息传递给窗口过程的子窗口句柄。以后需要知道按钮状态的时候,可以发送另一条 BM_GETCHECK 消息。或者,可以把当前的选择状态保存在窗口过程的静态变量中。也可以发送一条 BM_SETCHECK 信息,借此将 BS_CHECKBOX 复选框初始化为选中状态:
SendMessage (hwndButton, BM_SETSTATE, 1, 0);
对于 BS_AUTOCHECKBOX 样式,按钮控件本身负责切换选定和取消标记。窗口过程可以忽略 WM_COMMAND 消息。在需要当前按钮的状态时,可以向控件发送 BM_GETCHECK 消息:
iCheck = (int) SendMessage (hwndButton, BM_GETCHECK, 0, 0);
如果复选框被选中,iCheck 的值就为 TRUE 或非零。如果没有被选中,则为 FALSE 或 0。
其他两个复选框样式分别为 BS_3STATE 和 BS_AUTO3STATE。正如其名称所表明的,这些样式可以显示第三种状态——一个灰色的复选框。这发送在向控件发送 BM_SETCHECK 消息(wParam 被设为 2) 时。灰色复选框告诉用户用户,选择是不确定的或无关紧要的。
CreateWindow 函数调用指定了控件矩形的大小,复选框会在矩形的左边缘对齐,并在矩形的顶端和底部居中。单击矩形内的任何地方都会导致 WM_COMMAND 消息被发送到父窗口。复选框的最低高度是一个字符的高度。最小宽度是现有字符数再加 2 个字符的宽度。
9.1.6 单选按钮
单选按钮的名称来源于汽车收音机的一排选择按钮,它曾风靡一时。汽车收音机的每个按钮设定为不同的电台,在任意时刻只有一个按钮可以被按下。在对话框中,一组单选按钮依据约定用于相互排斥的选项。不同于复选框,单选按钮没有状态切换,也就是说,如果单击按钮第二次,其状态仍保持不变。
单选按钮很像一个复选框,但它包含一个小圆圈,而不是一个方框。圆圈内有一个大黑点表明该单选按钮已被选中。单选按钮的窗口样式为 BS_RADIOBUTTON 或 BS_AUTORADIOBUTTON,但后者指用于对话框。
在收到来自单选按钮的 WM_COMMAND 消息时,应通过向它发送一条 wParam 等于 1 的 BM_SETCHECK 消息来显示其选中标记:
SendMessage (hwndButton, BM_SETCHECK, 1, 0);
而对于其他在同一组中的所有单选按钮,则可以通过向它们发送 wParam 设置为 0 的 BM_SETCHECK 消息来取消其选中标记:
SendMessage (hwndButton, BM_SETCHECK, 0, 0);
9.1.7 组合框
带有 BS_GROUPBOX 样式的组合框,是一个古怪的按钮类。它既不处理鼠标或键盘输入也不发送消息 WM_COMMAND 到父窗口。组合框是一种矩形外框,窗口文本显示在矩形顶部。组合框常常被用来包容其他按钮控件。
9.1.8 改变按钮文本
可以调用 SetWindowText 来改变按钮中的文本:
SetWindowText (hwnd, pszString);
这里的 hwnd 是要被改变文本的窗口的句柄,pszString 是一个指向以零结尾的字符串的指针。对于普通窗口来说,该文本就是指窗口标题栏中的文本。而对于按钮控件来说,该文本是和按钮一起显示的文本。
也可以获得一个窗口的当前文本,如下所示:
iLength = GetWindowText (hwnd, pszBuffer, iMaxLength);
这里的 iMaxLength 指定了 pszBuffer 所指向的缓冲区所能接收的最大字符串长度。函数会返回被复制的字符串长度。可以事先调用如下函数,使程序对可接收的特定文本长度有所准备:
iLength = GetWindowTextLength (hwnd);
9.1.9 可见的按钮和启用的按钮
要接收鼠标和键盘输入,子窗口必须是可见的(显示)并且是启用的。如果一个子窗口是可见的,但没有启用,那么文本在窗口中的显示是灰色的,而不是黑色的。
如果在创建子窗口时在窗口类中没有包括 WS_VISIBLE,子窗口将不会显示,除非调用 ShowWindow:
ShowWindow (hwndChild, SW_SHOWNORMAL);
但如果在窗口类中包含了 WS_VISIBLE,就无需调用 ShowWindow。不过,可以调用以下 ShowWindow 来隐藏这个子窗口:
ShowWindow (hwndChild, SW_HIDE);
可以调用以下函数来判断窗口是否可见:
isWindowVisible (hwndChild);
还可以启用或者禁用一个子窗口。在默认情况下,窗口是处于启用状态的。可以调用下面的函数来禁用子窗口:
EnableWindow (hwndChild, FALSE);
对于按钮控件,此函数可以把按钮的文本字符串变成灰色。按钮不再响应鼠标或键盘输入。这
是表明按钮选项目前无法使用的最佳方法。
通过如下调用可以重新启用子窗口:
EnableWindow (hwndChild, TRUE);
可以调用以下函数了解子窗口是否已被启用:
isWindowEnabled (hwndChild);
9.1.10 按钮和输入焦点
正如我在本前面所指出的,用鼠标单击时,按键按钮、复选框、单选按钮、自绘按钮得到输入焦点。如果按钮上的文本被虚线包围,则表明它已经获得输入焦点。子窗口控件得到输入焦点后,父窗口便会失去输入焦点。之后所有的键盘输入将送到控件子窗口而不是其父窗口。然而,子窗口控件只对空格键做出响应,空格键现在的功能类似于鼠标。这种情况下引发一个明显的问题:程序已经失去了对键盘处理的控制。让我们看看如何解决这个问题。
正如我在第 6 章讨论的,在 Windows 把输入焦点从一个窗口(如父窗口)切换到另一个窗口(如子窗口控件)时,它首先会向将要失去输入焦点的窗口发送一条消息 WM_KILLFOCUS。相应的 wParam 参数是将要获得输入焦点的窗口的句柄。Windows 然后向要接收输入焦点的窗口发送 WM_SETFOCUS 消息,用 wParam 指定失去输入焦点的窗口的句柄。(在这两种情况下,wParam 都可能是 NULL,表明没有窗口具有或正在接收输入焦点。)
父窗口可以通过对 WM_KILLFOCUS 消息的处理来阻止子窗口控件获得输入焦点。假定数组 hwndChild 包含所有子窗口的窗口句柄。(这些句柄都是在调用 CreateWindow 创建窗口时保存在数组中的。)NUM 是子窗口的数目。
case WM_KILLFOCUS:
for (i = 0; i < NUM; ++ i)
if (hwndChild[i] == (HWND) wParam)
{
SetFocus (hwnd);
break;
}
return 0;
在此代码中,当父窗口检测到它将失去输入焦点并将焦点转给它的一个子窗口控件时,它调用 SetFocus 来重新获得本身的输入焦点。
下面有一个更简单(但不太明显)的方法来实现我们的想法:
case WM_KILLFOCUS:
if (hwnd == GetParent((HWND) wParam))
SetFocus(hwnd);
return 0;
但这两种方法都有一个缺陷,就是他们会使按钮不再回应空格键,因为按钮从未得到输入焦点。一个更好的办法是让按钮获得输入焦点,而且还能让用户用 Tab 键从一个按钮移动到另外一个按钮。这咋听起来好像不可能,但我会再本章后面的 COOLORS1 程序中指出如何用“
窗口子类”这一技术来实现它。