摘录于《Windows程序(第5版,珍藏版).CHarles.Petzold 著》P230
前面曾讨论了 Windows 资源管理器是如何响应鼠标单击和双击消息的。显然,程序(更确切地说,是 Windows 资源管理器所采用的列表视图控件)必须首先准确地判断用户鼠标所指的文件或目录位置。
这就是“击中测试”。正如 DefWindowProc 在处理 WM_NCHITTEST 消息时必须做击中测试一样,窗口过程经常也必须在其客户区内做击中测试。概况地说,击中测试包含了对传递到窗口过程的 x 和 y 坐标的一些运算,其中 x 和 y 的值包含在鼠标消息的参数 lParam 中。
7.4.1 一个假想的例子
下面是一个例子。假设程序需要显示几列按字母顺序排列的文件。通常,你会使用列表视图控件,因为它已实现了所有需要的“击中测试”功能。但是,假设由于某种原因不能使用它,那么久需要自己实现这些功能。假定文件名都保存在一个有序的字符串指针数组中,数组名为 szFileNames。
假设文件列表从客户区的顶部开始,其中客户区的大小为 cxClient 像素宽和 cyClient 像素高。每列的宽度为 cxColWidth 像素;字符的高度为 cyChar 像素。那么每列可存放的文件数目为:
iNumInCol = cyClient / cyChar;
当程序接收到一个鼠标单击消息时,从参数 lParam 中可以得到坐标值 cxMouse 和 cyMouse。然后,利用下面这个公式,可以计算得到用户所指的文件名位于那一列:
iColumn = cxMouse / cxColWidth;
文件名相对于列表顶部的位置为:
iFromTop = cyMouse / cyChar;
现在,计算文件名在数组 szFileNames 中的索引号:
iIndex = iColumn * iNumInCol + iFromTop;
如果 iIndex 大于数组中文件的个数,那么用户单击的位置是显示设备的空白区域。
在很多情况下,击中测试比这个实例更加复杂。当显示一个由很多小图形组成的图像时,必须确定其中每个图形的显示坐标。在击中测试计算中,则必须反过来根据坐标来找到相应的图形。对于一个采用可变字号的字处理程序来说,这种计算会变得相当繁琐,因为用户必须反过来查找字符在字符串中的位置。
7.4.2 一个简单的程序
CHECKER1 程序展示了一些简单的击中测试。该程序将客户区划分成 25 个矩形,构成一个 5 * 5 的数组。如果在其中一个矩形内单击鼠标,就用 X 形填充该矩形。再次单击,则 X 形消失。
/*--------------------------------------------------------
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 ("This 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, LPARAM lParam)
{
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) ;
}
CHECKER1 程序的显示结果如图 7-5 所示。所有 25 个矩形具有相同的宽度和高度,这些宽度和高度分别被保存在 cxBlock 和 cyBlock 中。无论何时改变客户区的大小,程序都将重新计算 cxBlock 和 cyBlock。WM_LBUTTONDOWN 处理逻辑利用鼠标的坐标来判断哪个矩形被单击,然后在 fState 数组中计算出那个矩形的新状态,最后强制使该矩形失效,从而产生 WM_PAINT 消息。
图 7-5 CHECKER1 程序的显示结果
如果客户区的宽度或高度不能被 5 整除,客户区的左边或底部就会出现一个小长条区域,不被矩形覆盖。这里实现了一个错误处理,也就是在这片区域进行鼠标单击时,CHECKER1 程序会调用 MessageBeep 函数进行响应。
当 CHECKER1 程序接收到 WM_PAINT 消息时,它会重新绘制整个客户区,利用 GDI 中的 Rectangle 函数画出所有的矩形。若 fState 值为真,则 CHECKER1 程序调用函数 MoveToEx 和 函数 LineTo 画出两条直线。在处理 WM_PAINT 消息的过程中,CHECKER1 程序不会检查每个矩形区是否是无效矩形,但是实际上它能够做到。检查无效性的一种方法是对循环内每个矩形块创建一个 RECT 结构(采用与 WM_LBUTTONDOWN 逻辑中相同的公式),然后利用 IntersectRect 函数检查该矩形是否与无效矩形(以 ps.rcPaint 形式存在)相交。
7.4.3 使用键盘模仿鼠标操作
使用 CHECKER1 程序需要用到鼠标。很快我们会给程序增加一个键盘接口,正如我们在第 6 章所做的一样。不过,对于使用鼠标指针来实现指向的程序而言,增加一个键盘接口还必须考虑如何显示和移动鼠标的指针。
即使没有安装鼠标,Windows 仍然能够显示鼠标的指针。Windows 为这种指针保留了一个“显示计数” (display count)。若安装了鼠标,则显示计数初始值为 0;否则,显示计数初始值为 -1。只有当显示计数为非负时,系统才显示鼠标指针。为了增加显示计数,可以调用以下语句:
showCursor (TRUE);
而减少显示计数则可以调用以下语句:
showCursor (FALSE);
在调用 ShowCursor 函数之前,不需要检查是否已安装鼠标。如果想显示鼠标指针,不管鼠标是否存在,都可以简单地调用 ShowCursor 函数来增加显示计数。在增加一次显示计数后,如果没有没有安装鼠标,那么再减少显示计数就会隐藏指针;如果鼠标存在,减少显示计数仍然保留指针显示。
即便没有安装鼠标,Windows 也能够保留当前鼠标指针的位置。如果在没有安装鼠标的情况下显示鼠标指针,那么这个指针可能会出现在屏幕的任意位置并保持不动,直到用户明确地移动它。为了获取指针的位置,可以调用以下语句:
GetCursorPos (&pt);
其中 pt 是一个 POINT 结构。函数向 POINT 字段填充鼠标的 x 和 y 坐标。利用下面这个函数,可以设置指针的位置:
SetCursorPos (x, y);
在前面两个语句中,x 和 y 坐标都是屏幕坐标,而不是客户区坐标。(这是很明显的,因为这两个函数都没有 hwnd 参数。)前面已经讲过,
调用 ScreenToClient 和 ClientToScreen 可以在屏幕坐标与客户区坐标之间转换。
在处理鼠标消息的过程中,如果调用 GetCursorPos 并将坐标转换成客户区坐标,那么这些坐标值会与鼠标消息的参数 lParam 中所包含的坐标值略微有些不同。GetCursorPos 函数返回的坐标值是指鼠标的当前位置,而参数 lParam 包含的坐标是指产生消息的那一刻的鼠标位置。
你可能需要编写一段键盘处理逻辑,用键盘方向键来移动鼠标指针,并用空格键或 Enter 键来模拟鼠标按钮。此时,你“不应该”每按一次键,鼠标指针只移动一个像素,因为这需要长时间按住方向键,才能够移动鼠标。
在为鼠标指针增加键盘逻辑时,为了仍能将指针放置在准确的像素位置,可以这样处理按键消息:按住方向键不懂时,刚开始鼠标指针只是缓慢地移动,然后再加速移动。回忆一下,在 WM_KEYDOWN 消息中,参数 lParam 指出了键盘按键消息是否是重复击键的结果。下面这个程序出色地利用这个信息。
7.4.4 在 CHECKER 中增加键盘接口
除了包含一个键盘接口,CHECKER2 程序的内容与 CHECKER1 程序完全相同。利用←、→、↑ 和 ↓ 四个方向键可以在 25 个矩形之间移动鼠标指针。Home 键把鼠标指针移动到左上角的矩形;End 键使鼠标指针落到右下角的矩形。空格键和回车键都可以切换 X 形标记。
/*--------------------------------------------------------
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 ("This 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。这些 x 和 y 的值表示了矩形在 5 * 5 数组中的位置。当按下某个键时,鼠标指针可能在客户区也可能不在客户区内,因此 x 和 y 必须包含在 min 和 max 的宏处理之中,保证它们的范围处于 0 和 4 之间。
对于方向键,CHECKER2 程序相应的增加或减少 x 和 y 的值。若按下回车键或空格键,CHECKER2 程序调用 SendMessage 给自己发送一个 WM_LBUTTONDOWN 消息。这种方法类似于第 6 章 SYSMETS 程序为窗口滚动条增加一个键盘接口所使用的方法。最后,WM_KEYDOWN 处理逻辑计算得到指向矩形中心的客户区坐标,并调用 ClientToScreen 将其转换成屏幕坐标,最后调用 SetCursorPos 设置指针的位置。
7.4.5 在击中测试中使用子窗口
一些程序(如 Windows 画图程序)将客户区划分成几个更小的逻辑区域。画图程序的左边区域是用图标表示的工具箱;底部区域是颜色盒。在画图程序中单击这两部分区域时,在确定用户实际选择的工具或颜色之前,必须首先考虑小区域在整个客户区中的位置。
实际上,不这么做也可以。画图程序可以简单地利用“子窗口”的方式来绘制这些小区域并进行击中测试。子窗口将整个客户区划分成几个更小的矩形区域。每个子窗口都有属于自己的句柄、窗口过程和客户区。每个子窗口过程只接收与自身窗口有关的鼠标消息。鼠标消息的参数 lParam 中包含的坐标是相对于子窗口客户区左上角的,而不是相对于“父”窗口(即画图的主程序窗口)的客户区。
通过这种方式使用子窗口有助于程序的结构化和模式化。如果子窗口使用不同的窗口类,那么每个子窗口都有自己的窗口过程。不同的窗口类还可以定义不同的背景颜色和不同的默认鼠标指针。在第 9 章中,我们将介绍“子窗口控件”,其中的每个控件都是一个预先定义好的窗口,包括滚动条、按钮和编辑框。而现在,我们将在 CHECKER 程序中介绍子窗口的应用。
7.4.6 CHECKER 程序中的子窗口
该版本的 CHECKER 程序创建了 25 个子窗口,用于处理鼠标单击操作。该程序没有包含键盘接口,但是也可以增加。
/*--------------------------------------------------------
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 ("This 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 个子窗口的窗口过程。 两个窗口过程都必须被定义成 CALLBLCK 函数。
在调用 RegisterClass 函数注册窗口类时,用户会定义一个窗口类结构,而每个窗口过程都与某个特定的窗口类结构有关。为此,CHECKER3 程序需要用到两个窗口类。第一个窗口类表示主窗口,名为“Checker3”。第二个窗口类名为“Checker3_Child”。不过,用户不一定需要为窗口选择如此有含义的名称。
CHECKER3 程序在 WinMain 函数中注册了这两个窗口类。在注册完常规窗口类之后,为了注册 Checker3_Child 类,CHECKER3 程序简单地重用了 wndclass 结构的大部分字段。但是,对子窗口来说,其中四个字段的值不同:
- lpfnWndProc 字段被设置成 ChildWndProc,表示子窗口类的窗口过程。
- cbWndExtra 字段被设置成 4 个字节,或 更准确地说是一个长整形变量的大小(sizeof(long))。这个字段通知 Windows 在内部结构中给基于这个窗口类的每个窗口预留 4 个字节的额外空间。用户可以利用这些空间为每个窗口保存不同的信息。
- hIcon 字段被设置成 NULL,因为类似 CHECKER3 程序中的子窗口不需要图标。
- pszClassName 字段被设置成 “Checker3_Child”,即子窗口类的名称。
在 WinMain 函数中,CreateWindow 函数创建了基于 Checker3 类的主窗口。这是常规的处理。但是,当 WndProc 接收到一个 WM_CREATE 消息时,它会重复调用 CreateWindow 函数 25 次,创建 25 个基于 Checker3_Child 类的子窗口。下表对 WinMain 中 CreateWindow 函数所调用的参数与 WndProc 中用于创建 25 个子窗口的 CreateWindow 函数所调用的参数进行了比较。
参 数 | 主 窗 口 | 子 窗 口 |
---|---|---|
窗口类 | “Checker3” | “Checker3_Child” |
窗口标题 | “Checker3...” | NULL |
窗口风格 | WS_OVERLAPPEDWINDOW | WS_CHILDWINDOW | WS_VISIBLE |
水平位置 | CW_USEDEFAULT | 0 |
垂直位置 | CW_USEDEFAULT | 0 |
宽度 | CW_USEDEFAULT | 0 |
高度 | CW_USEDEFAULT | 0 |
父窗口句柄 | NULL | hwnd |
菜单句柄/子 ID | NULL | (HMENU) (y << 8 | x) |
实例句柄 | hInstance | (HINSTANCE) GetWindowLong(hwnd, GWL_HINSTANCE) |
额外参数 | NULL | NULL |
通常,子窗口需要用到窗口位置和窗口大小参数,但是在 CHECKER3 程序中,子窗口的位置和大小在后面的 WndProc 函数中才定义。对主窗口来说,父窗口句柄为 NULL,因为它本身就是父类。而在调用 CreateWindow 创建子窗口时,需要用到相应的父窗口句柄。
主窗口没有菜单,因此菜单句柄为 NULL。对子窗口来说,该参数称为“子 ID” (child ID) 或“子窗口 ID” (child windows ID)。这是一个用来唯一标识子窗口的数值。在处理对话框的子窗口控件时,子 ID 显得更加重要。在第 11 章我们将看到这一点。在 CHECKER3 程序中,根据每个窗口在主窗口 5 * 5 数组中的位置 x 和 y,我简单地将子 ID 设定成一个 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 消息,则表示该窗口被击中,而这正是程序所需要的。
7.4.6 子窗口和键盘
下一步是为 CHECKER3 程序增加一个键盘接口。但在这里,可以采用一种更合适的方法。在 CHECKER2 程序中,按下空格键,鼠标指针的位置住处哪个矩形得到选中标记。当处理子窗口时,我们可以从对话框的运行机制得到一些启发:在对话框中,每个子窗口会利用闪烁的插入符号或矩形虚拟框来表示自己是否具有输入焦点(因此可以用键盘来切换不同的子窗口)。
/*--------------------------------------------------------
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 ("This 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())
{
// 下面注释代码可行
// // ShowCursor(FALSE);
// POINT pt;
// pt.x = rect.right / 2;
// pt.y = rect.bottom / 2;
// ClientToScreen(hwnd, &pt);
// SetCursorPos(pt.x, pt.y);
// // ShowCursor(TRUE);
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);
}
回顾一下,当调用 CreateWindow 创建窗口时,每个子窗口都定义了一个唯一的“子窗口 ID” 数值。在 CHECKER3 程序中,这个 ID 数值由矩形的 x 和 y 坐标组合而成。为了获取一个具体子窗口的子窗口 ID,程序调用如下:
idChild = GetWindowLong(hwndChild, GWL_ID);
下面这个函数功能一样:
idChild = GetDlgCtrlId (hwndChild);
正如函数名称所示,它主要与对话框和控件窗口一起使用。如果知道父窗口的句柄和子窗口 ID,还可能得到子窗口的句柄:
hwndChild = GetDlgItem (hwndParent, idChild);
在 CHECKER4 程序中,全局变量 idFocus 用于保存当前具有输入焦点的窗口的子 ID 数值。先前曾提到,在子窗口上单击鼠标时,子窗口不能自动地获得输入焦点。因此,在 CHECKER4 程序中,父窗口调用下面的这个函数来处理 WM_SETFOCUS 消息,从而给其中一个子窗口设置输入焦点:
setFocus (GetDlgItem (hwnd, idFocus));
窗口过程 ChildWndProc 同时处理 WM_SETFOCUS 消息和 WM_KILLFOCUS 消息。处理 WM_SETFOCUS 消息时,它将接收输入焦点的子窗口 ID 保存在全局变量 idFocus 中。对这两个消息,程序都将使窗口无效,并产生 WM_PAINT 消息。如果 WM_PAINT 消息所绘制的子窗口带有输入焦点,程序就会利用 PS_DASH 画笔绘制一个矩形,表示该窗口带有输入焦点。
窗口过程 ChildWndProc 还要处理 WM_KEYDOWN 消息。对于除了空格键和回车键之外的其他任何键,WM_KEYDOWN 消息都会被传递到主窗口。而对空格键和回车键,窗口过程会执行与 WM_LBUTTONDOWN 消息一样的处理。
对鼠标移动键的处理交给了主窗口来完成。与 CHECKER2 中的方法类似,此程序获取具有输入焦点的子窗口的 x 和 y 坐标,再根据按下的具体方向键改变这些坐标值。然后调用 SetFocus 将输入焦点设置一个新的子窗口。