dustland

dustball in dustland

win32程序设计-chapter7 鼠标

windows SDK chapter 7 mouse

鼠标的基本信息

是否在线

1
fMouse=GetSystemMetrics(SM_MOUSEPRESENT);

如果鼠标都不在线(和鼠标共用接口的输入设备也有可能被作为鼠标),则该函数返回0.

如果至少有一个鼠标在线则函数返回非零值

单键双键

1
cButtons = GetSystemMetrics(SM_CMOUSEBUTTONS);

但是在我的笔记本电脑上,这个值是8,而我的鼠标就四个键

可能是触摸板加上了两指三指四指动作,等等各种使用方法?

鼠标样式

鼠标样式最常见的就是斜向箭头,当程序忙的时候可能变成沙漏或者左箭头右沙漏

当绘图的时候鼠标可能会变成十字

实际上这个图标就是一个小的位图结构,点击鼠标时有效的位置只有一个像素点,叫做"焦点"

斜向箭头的焦点在顶点上.十字的焦点在十字路口

设置鼠标样式

在实例化窗口对象的时候就指定过一次鼠标样式了

1
wndclass.hCursor  = LoadCursor(NULL, IDC_ARROW);

IDC_ARROW就是最常见的斜向箭头样式

Value Meaning
IDC_APPSTARTINGMAKEINTRESOURCE(32650) Standard arrow and small hourglass
IDC_ARROWMAKEINTRESOURCE(32512) Standard arrow
IDC_CROSSMAKEINTRESOURCE(32515) Crosshair
IDC_HANDMAKEINTRESOURCE(32649) Hand
IDC_HELPMAKEINTRESOURCE(32651) Arrow and question mark
IDC_IBEAMMAKEINTRESOURCE(32513) I-beam
IDC_ICONMAKEINTRESOURCE(32641) Obsolete for applications marked version 4.0 or later.
IDC_NOMAKEINTRESOURCE(32648) Slashed circle
IDC_SIZEMAKEINTRESOURCE(32640) Obsolete for applications marked version 4.0 or later. Use IDC_SIZEALL.
IDC_SIZEALLMAKEINTRESOURCE(32646) Four-pointed arrow pointing north, south, east, and west
IDC_SIZENESWMAKEINTRESOURCE(32643) Double-pointed arrow pointing northeast and southwest
IDC_SIZENSMAKEINTRESOURCE(32645) Double-pointed arrow pointing north and south
IDC_SIZENWSEMAKEINTRESOURCE(32642) Double-pointed arrow pointing northwest and southeast
IDC_SIZEWEMAKEINTRESOURCE(32644) Double-pointed arrow pointing west and east
IDC_UPARROWMAKEINTRESOURCE(32516) Vertical arrow
IDC_WAITMAKEINTRESOURCE(32514) Hourglass

然而在我的win11笔记本上,除了斜向箭头和转圈,其他鼠标样式都加载不出来,或者都加载成转圈或者斜向箭头

客户区鼠标消息

windows只把键盘消息发往具有输入焦点的窗口,但是鼠标不同

只要鼠标经过某个窗口,windows就会对齐发送一个WM_MOUSEMOVE的消息

image-20220813092214535

其中双击信息许哟啊在创建窗口实例的时候指明风格使用

1
wndclass.style=CS_DBLCLKS | ...

带有CS_DBLCLKS风格的窗口才可以接收WM_LBUTTONDBLCLK这种双击消息

对于鼠标消息(hwnd,message,wParam,lParam)

lParam

包含鼠标的位置信息,低字表示x坐标,高字表示y坐标

1
2
x=LOWORD(lParam);
y=HIWORD(lParam);

wParam

包含了鼠标哪个键,还有此时Ctrl和Shift的状态.

令wParam和宏定义按位与即可测试相应状态

1
2
3
4
5
6
7
8
9
#ifndef NOKEYSTATES
#define MK_LBUTTON 0x0001
#define MK_RBUTTON 0x0002
#define MK_SHIFT 0x0004
#define MK_CONTROL 0x0008
#define MK_MBUTTON 0x0010
#define MK_XBUTTON1 0x0020
#define MK_XBUTTON2 0x0040
#endif

MOUSEMOVE的速度

windows不会给鼠标经过的每个像素点都产生一个WM_MOUSEMOVE消息,这取决于应用程序处理WM_MOUSEMOVE的速度,当应用程序的消息队列中还有WM_MOUSEMOVE的消息时就不能接收第二个WM_MOUSEMOVE消息

书上在此给出了一个例子.

凡是WM_MOUSEMOVE捕获的点都会被计入点集,点集中任意两个点连一条线

分析一下其过程函数WndProc

变量定义

1
2
3
4
5
static POINT pt[MAXPOINTS];//存储已经捕获的点集
static int iCount;//记录已经捕获的点数量
HDC hdc;//设备环境句柄
int i, j;//循环变量
PAINTSTRUCT ps;//绘图结构

鼠标信息处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
case WM_LBUTTONDOWN:
iCount = 0;//左键按下时清零重新记录
InvalidateRect(hwnd, NULL, TRUE);//清零后重绘
return 0;

case WM_MOUSEMOVE:
if (wParam & MK_LBUTTON && iCount < MAXPOINTS)//如果是左键按下的移动状态并且目前点集未满则捕获新点
{
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);//左键松开,立刻通知处理WM_PAINT函数,重绘点集
return 0;

绘图消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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);//从pt[i]到pt[j]连线
LineTo(hdc, pt[j].x, pt[j].y);
}
ShowCursor(FALSE);
SetCursor(LoadCursor(NULL, IDC_ARROW));//绘图完毕,光标从忙状态换成指针状态
EndPaint(hwnd, &ps);
return 0;

窗口销毁消息

1
2
3
4
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}

效果

左鼠标移速快,右鼠标移速慢

双击

只有创建窗口实例时,风格上允许双击的窗口才可以接受双击信息

1
wndclass.style = CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS;

对于没有双击风格的窗口,双击动作造成的消息:

WM_LBUTTONDOWN 左键第一次按下

WM_LBUTTONUP 左键第一次起来

WM_LBUTTONDOWN 左键第二次按下

WM_LBUTTONUP 左键起来

对于有双击风格的窗口,双击造成的信息:

WM_LBUTTONDOWN

WM_LBUTTONUP

WM_LBUTTONDBLCLK 第二次按下被替换为WM_LBUTTONDBLCLK

WM_LBUTTONUP

非客户区鼠标消息

image-20220813105023942

非客户区的消息在WM_前缀之后又加了一个NC前缀(not client)

参数意义

lParam

低字为x坐标,高字为y坐标,此处的坐标是相对于整个屏幕的坐标

而客户区的鼠标信息中lParam携带的坐标是相对于客户区左上角的坐标

image-20220813105412718

屏幕坐标和客户区坐标的互换:

1
2
3
4
5
6
7
8
BOOL ScreenToClient(
[in] HWND hWnd,
LPPOINT lpPoint
);
BOOL ClientToScreen(
[in] HWND hWnd,
[in, out] LPPOINT lpPoint
);

白刀子进,红刀子出

使用pt带着原坐标进去,带着转换坐标出来

wParam

非客户区鼠标移动或者单击的位置(不是坐标),一个标识符

击中测试

WM_NCHITTEST

关于鼠标的最后一个消息类型

这个消息优先级高于所有客户区和非客户区的鼠标消息

lParam

鼠标位置的屏幕坐标

wParam

没有用到

这条消息应该被直接传递给DefWindowProc,操作系统负责将屏幕坐标翻译为客户区坐标之后,产生一个客户区鼠标消息发送给应用程序

那么如果捕获该消息并且不让他传递给DefWindowProc,就阻断了所有鼠标消息.所有本窗口的鼠标动作将失效

什么是击中测试

在文件浏览器中双击某个文件时,文件浏览器会进入该目录或者打开该文件.

可是文件浏览器是怎么知道应该打开哪个文件的呢?

他需要获取鼠标位置然后判断这个位置落在哪里

image-20220814083834575

考虑实现一个简单的文件浏览器,以列表形式列出当前目录下的所有文件和子文件夹

每个文件占一行,放不下就用滚动条

客户区的点击动作就需要将纵坐标换算为行数,再根据卷动情况判断是指向的哪一行

根据确定的行号作为下标查文件名表,查到之后打开该文件,如果是文件夹则打开该文件夹

击中测试例程

书上在此给出了一个击中测试的例程,分析其过程函数WndProc

变量定义

1
2
3
4
5
6
7
#define DIVISIONS 5//整个客户区分成5*5=25个矩形区域
...
static BOOL fState[DIVISIONS][DIVISIONS];//状态数组,fState[x][y]记录第x行低y列的格子状态
static int cxBlock, cyBlock;//一个格子的宽度和高度
HDC hdc;
int x, y;//临时变量,作为下标遍历每个格子
PAINTSTRUCT ps;

尺寸变化消息

1
2
3
4
case WM_SIZE:
cxBlock = LOWORD(lParam) / DIVISIONS;//lParam携带的是当前客户区大小,cxBlocks计算的是平均每个矩形的宽度
cyBlock = HIWORD(lParam) / DIVISIONS;//平均每个矩形的高度
return 0;

尽量用整个客户区打印所有方格

左键单击消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
case WM_LBUTTONDOWN:
x = LOWORD(lParam) / cxBlock;//鼠标的横坐标落在哪一列
y = HIWORD(lParam) / cyBlock;//鼠标的纵坐标落在哪一行
if (x < DIVISIONS && y < DIVISIONS) {//x,y都在合法范围之内
fState[x][y] ^= 1;//修改fState[x][y]的状态,1变0,0变1
rect.left = x * cxBlock;
rect.top = y * cyBlock;
rect.right = (x + 1) * cxBlock;
rect.bottom = (y + 1) * cyBlock;
//InvalidateRect(hwnd, &rect, TRUE);//rect对应区域失效
InvalidateRect(hwnd, NULL, TRUE);//全区域失效重绘
}

else {
MessageBeep(0);
}
return 0;

绘图消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
case WM_PAINT:
hdc = BeginPaint(hwnd, &ps);
//HBRUSH hBrushBlack=(HBRUSH)
for (x = 0; x < DIVISIONS; ++x) {//遍历每个矩形区域
for (y = 0; y < DIVISIONS; ++y) {
if (fState[x][y]) {//1则表示这个格子是按下的状态,刷成灰色
SelectObject(hdc, (HBRUSH)GetStockObject(LTGRAY_BRUSH));
}
else {//0则表示这个格子没有按下,刷成白色
SelectObject(hdc, (HBRUSH)GetStockObject(WHITE_BRUSH));
}
Rectangle(hdc, x * cxBlock, y * cyBlock, (x + 1) * cxBlock, (y + 1) * cyBlock);//每个区域绘制矩形

}

}
EndPaint(hwnd, &ps);
return 0;

键盘模仿鼠标

使用方向键移动鼠标光标.使用Enter确认按下

这样即使计算机没有连接鼠标也能使用键盘模拟鼠标动作

比如windows桌面上如果有一个图标是高亮的,那么右方向键会使同行右侧的图标高亮,可以用隐藏光标,然后捕获位置进行击中测试,决定高亮哪个图标.

显示计数

书上扯了大半天,实际上就说了一个有用的东西

1
2
3
int ShowCursor(
[in] BOOL bShow
);

当bShow为False则不显示鼠标光标

当bShow为True则显示鼠标光标

指针位置

不管有没有接鼠标,鼠标指针都是存在的,一般开机时位于屏幕正中间.即使不使用鼠标移动指针位置,也可以使用键盘做到

1
2
3
4
5
6
7
BOOL GetCursorPos(
[out] LPPOINT lpPoint//lpPoint承接返回值,指针位置的坐标结构体
);
BOOL SetCursorPos(
[in] int X,//设置指针位置(X,Y)
[in] int Y
);

两个函数中涉及到的坐标都是屏幕坐标,如果需要客户区坐标,可以使用坐标转换函数

1
2
3
4
5
6
7
8
BOOL ScreenToClient(
[in] HWND hWnd,
LPPOINT lpPoint
);
BOOL ClientToScreen(
[in] HWND hWnd,
[in, out] LPPOINT lpPoint
);

使用GetCursorPos并且用ScreenToClient转换得到的指针位置和鼠标消息中的指针位置不同

前者是啥时候调用函数啥时候取得指针位置,后者指针位置是产生该条消息时指针的位置

击中测试

变量定义

1
2
3
4
5
6
7
8
9
#define DIVISIONS 5//客户区划分为5*5=25个区域
...
static int fState[DIVISIONS][DIVISIONS];//记录每个格子的状态
static int cxBlock, cyBlock;//每个格子的宽度和高度
HDC hdc;
int x, y;//临时变量,用来遍历fState
PAINTSTRUCT ps;//BeginPaint和EndPaint需要使用
POINT point;记录鼠标位置

窗口焦点信息

1
2
3
4
5
6
case WM_SETFOCUS://获得焦点
ShowCursor(TRUE);
return 0;
case WM_KILLFOCUS://失去焦点
ShowCursor(FALSE);
return 0;

当窗口获得焦点的时候显示光标,失去焦点的时候隐藏光标

尺寸调整信息

1
2
3
4
case WM_SIZE://客户区重新计算块大小
cxBlock = LOWORD(lParam) / DIVISIONS;
cyBlock = HIWORD(lParam) / DIVISIONS;
return 0;

当客户区尺寸发生变化的时候调整区块的大小

使得5*5个区块尽量占满整个客户区

虚拟键信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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;//y的单位是列,上键按下之后纵坐标应该上移一个格子的高度
break;
case VK_DOWN:
++y;
break;
case VK_LEFT:
--x;
break;
case VK_HOME://Home键,光标回到左上角
x = y = 0;
break;
case VK_RIGHT:
++x;
break;
case VK_END:
x = y = DIVISIONS - 1;//End键,光标跳到右下格
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;

使用子窗口

每个子窗口都有自己的句柄,客户区,窗口过程函数.

多个子窗口将整个客户区划分成几个小的矩形区域

对于子窗口的鼠标消息,lParam参数包含相对于该子窗口左上角的坐标

本来我们的程序中使用了一个fState[DIVISIONS][DIVISIONS]二维数组保存每个区块的状态,整个程序就一个窗口过程,它遍历打印每个窗口的状态.

现在使用子窗口,使得每个区块成为一个子窗口,每个子窗口自己处理发生在自己身上的鼠标键盘动作

注册父窗口类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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("Program requires Windows NT!"),
szAppName,
MB_ICONERROR);
return 0;
}

注册子窗口类

1
2
3
4
5
6
wndclass.lpfnWndProc = ChildWndProc;//修改一下wndclass的部分信息,填写子窗口类信息
wndclass.cbWndExtra = sizeof(long);
wndclass.hIcon = NULL;
wndclass.lpszClassName = szChildClass;//此处绑定了szChildClass字符串作为子窗口类的索引值

RegisterClass(&wndclass);//注册子窗口类

此处注册子窗口类,但是并不在winmain函数中创建实例,而是当父窗口起来之后,在其WM_CREATE消息处理中创建25个szChildClass指向的子窗口实例

创建父窗口实例,显示父窗口消息循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;

父窗口过程WndProc

变量定义

1
2
static HWND hwndChild[DIVISIONS][DIVISIONS];//子窗口句柄数组,hwndChild[x][y]为第x行第y列子窗口的句柄
int cxBlock, cyBlock, x, y;//cxBlock每个子窗口的尺寸,x遍历子窗口数组使用的下标

区分HWND句柄和HINSTANCE句柄

整个win32程序只有一个引用程序句柄HINSTANCE,

着一个程序可以有很多个窗口,每个窗口都有一个独一无二的窗口句柄HWND

窗口创建消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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),//子窗口的菜单句柄,作用是给父窗口提供索引,在GetDlgItem 获取子窗口句柄时有重要作用
(HINSTANCE) GetWindowLong(hwnd, GWL_HINSTANCE),//从windows那里获取一个应用此程序实例句柄
NULL//通过WM_CREATE的lParam参数传递给子窗口的值的指针
);
return 0;
CreateWindow

创建父窗口实例时也使用了该函数,返回值是一个窗口实例的句柄

创建并显示窗口三个过程:

RegisterClass注册窗口类,该窗口类的类名作为句柄

CreateWindow创建窗口实例,可以使用刚才的窗口类名填充窗口的基本信息

ShowWindow显示窗口类

1
2
3
4
5
6
7
8
9
10
11
12
13
HWND CreateWindowA(
[in, optional] lpClassName,//子窗口使用的类名
[in, optional] lpWindowName,//子窗口名
[in] dwStyle,//子窗口风格
[in] x,//子窗口位置(相对于父窗口的客户区)
[in] y,
[in] nWidth,//子窗口的尺寸
[in] nHeight,
[in, optional] hWndParent,//父窗口的句柄
[in, optional] hMenu,//子窗口标识符
[in, optional] hInstance,//程序实例的句柄
[in, optional] lpParam//父窗口要传递给子窗口WM_CREATE消息,lParam参数的信息
);

调整尺寸消息

1
2
3
4
5
6
7
8
9
10
11
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);//大小是cxBlock宽,cyBlock高
return 0;

MoveWindow

对于子窗口来说,该函数中指定的坐标都是相对于父窗口客户区左上角的

对于非子窗口来说,该函数中的坐标是屏幕坐标

1
2
3
4
5
6
7
8
BOOL MoveWindow(
[in] HWND hWnd,//需要移动位置的窗口句柄,一般用于父窗口过程移动子窗口
[in] int X,
[in] int Y,
[in] int nWidth,//移动顺便设置尺寸
[in] int nHeight,
[in] BOOL bRepaint//是否重绘,TRUE则hWnd指向的窗口收到WM_PAINT消息
);

左键按下消息

1
2
3
case WM_LBUTTONDOWN:
MessageBeep(0);
return 0;

理论上25个子窗口尽量铺满父窗口的客户区,但是父窗口客户区的最右边和最下边可能有留白,因此当鼠标点击到这些地方的时候父窗口就会接到WM_LBUTTONDOWN消息

父窗口对这种消息的处理是发出一条蜂鸣声实际上是一个wav波形文件,还有一些系统定义好了的蜂鸣声,作用不大不展开了

获得焦点消息

1
2
3
case WM_SETFOCUS://父窗口获取焦点之后通知它上次获得焦点的子窗口继续获得焦点
SetFocus(GetDlgItem(hwnd, idFocus));//通知哪一个子窗口获得焦点,全局变量idFocus在WM_KEYDOWN被设置
return 0;

父窗口获得焦点之后,应该把最后一次获得焦点的子窗口作为焦点窗口

但是处理本消息时并没有体现获得idFocus,原因是该全局变量idFocus在WM_KEYDOWN中更新,显然没有按下键盘,通过鼠标点选也可以获得焦点,获得焦点的时候就需要指定让子窗口获得焦点

而现在父窗口掌握着25个子窗口句柄,应该怎么把焦点交给其中之一的子窗口呢?

1
2
3
4
HWND GetDlgItem(
[in, optional] HWND hDlg,//父窗口句柄
[in] int nIDDlgItem//子窗口的索引值
);

这个函数的作用是,返回父窗口的一个子窗口的句柄

这里nIDDlgItem这个值是父窗口注册子窗口是在CreateWindow函数的hMenu上指定的,其中第x行第y列的子窗口是这样索引的

1
(HMENU)(y << 8 | x)

既然要获取子窗口的句柄,父窗口不是实例化子窗口时就维护了一个子窗口句柄数组吗?

为啥还要额外维护一个值托管这个句柄呢?

只有一个子窗口句柄数组不能知道最近获得焦点的子窗口是谁,因此idFocus就起到了一个寄存器的作用

虚拟键消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
case WM_KEYDOWN:
x = idFocus & 0xFF;//x保存原来的焦点位置(子窗口下标)
y = idFocus >> 8;//y保存原来的焦点位置
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;
x = (x + DIVISIONS) % DIVISIONS;
y = (y + DIVISIONS) % DIVISIONS;
idFocus = y << 8 | x;//调整新焦点
SetFocus(GetDlgItem(hwnd, idFocus));//设置新焦点
return 0;

这里计算x,y坐标的方式根HMENU参数的定义方式相反,互为逆运算

1
2
3
4
5
		x = idFocus & 0xFF;//x保存原来的焦点位置
y = idFocus >> 8;//y保存原来的焦点位置
...

idFocus = y << 8 | x;//调整新焦点

WM_KEYDOWN消息执行完毕后立刻设置当前子窗口为焦点窗口

窗口销毁消息

1
2
3
4
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}

子窗口过程ChildWndProc

子窗口创建消息

1
2
3
case WM_CREATE:
SetWindowLong(hwnd, 0, 0); // on/off flag ,刚创建时本窗口的开关状态置0
return 0;

修改窗口的一个属性

1
2
3
4
5
LONG SetWindowLongA(
[in] HWND hWnd,//窗口句柄
[in] int nIndex,//指定要修改的窗口属性
[in] LONG dwNewLong//该窗口属性的新值
);

例子中修改的是下标为0的属性,对应的宏定义是DWL_MSGRESULT(0)

设置对话框过程的返回值,设置成了0.

实际上子窗口也不需要把这个值返回给父窗口看,它自己就可以决定把自己绘制成什么颜色

因此只是借用了一个线程的窗口属性来放置自己应该是按下还是起来的状态

子窗口键鼠消息

由于父窗口中会主动将焦点下放到子窗口,因此焦点子窗口可获取键鼠的输入.

父窗口只能等子窗口吃完了然后吃剩下的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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

这里子窗口就处理里两个虚拟键消息,空格和回车,其他的消息通过

1
SendMessage(GetParent(hwnd), message, wParam, lParam);

转发给父窗口

空格,回车,左键单击一视同仁,首先本子窗口的属性置反

然后设置本窗口为焦点窗口

然后使窗口无效,导致重绘

子窗口获取/失去焦点消息

1
2
3
4
5
6
7
case WM_SETFOCUS:
idFocus = GetWindowLong(hwnd, GWL_ID);
// Fall through
case WM_KILLFOCUS:
InvalidateRect(hwnd, NULL, TRUE);//失效重绘,因为子窗口获得焦点的时候会有绘制方框提示,因此失去焦点时应当不再提示
return 0;

GetWindowLong 获取窗口属性

由于窗口属性都是LONG类型的值,因此该函数取名"GetWindowLong"

1
2
3
4
LONG GetWindowLongA(
[in] HWND hWnd,//指定要获取信息的窗口句柄
[in] int nIndex//指定要获取该窗口的哪个属性
);
Value Meaning
GWL_EXSTYLE-20 获取窗口实例的拓展风格,即CreateWindow函数指定的dwExStyle
GWL_HINSTANCE-6 获取应用程序句柄
GWL_HWNDPARENT-8 获取父窗口句柄
GWL_ID-12 获取本窗口的索引值,即CreateWindow函数指定的HMENU值
GWL_STYLE-16 获取窗口实例的风格,这个风格就是CreateWindow指定的dwStyle
GWL_USERDATA-21 Retrieves the user data associated with the window. This data is intended for use by the application that created the window. Its value is initially zero.
GWL_WNDPROC-4 Retrieves the address of the window procedure, or a handle representing the address of the window procedure. You must use the CallWindowProc function to call the window procedure.

The following values are also available when the hWnd parameter identifies a dialog box.

Value Meaning
DWL_DLGPROCDWLP_MSGRESULT + sizeof(LRESULT) Retrieves the address of the dialog box procedure, or a handle representing the address of the dialog box procedure. You must use the CallWindowProc function to call the dialog box procedure.
DWL_MSGRESULT0 Retrieves the return value of a message processed in the dialog box procedure.
DWL_USERDWLP_DLGPROC + sizeof(DLGPROC) Retrieves extra information private to the application, such as handles or pointers.

例子中获取的是子窗口的HMENU索引值

1
idFocus = GetWindowLong(hwnd, GWL_ID);

这句的意思就是子窗口将idFocus当前焦点窗口寄存器设置为子窗口自己

当子窗口失去焦点的时候需要通知失效重绘,其原因是WM_PAINT中获得焦点的子窗口会多绘制一些提示信息,那么当它失去焦点的时候就得擦除提示信息了

image-20220814172257251

子窗口绘图消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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))//通过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())//如果当前子窗口正在获得焦点,那么额外画出提示信息
{
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;

捕获鼠标

书上举了一个例子,一个绘图程序,假如要绘制一个矩形,鼠标左键按下之后确定矩形一个点,不松开拖着鼠标移动则实时绘制矩形边框,(好像这个边框叫做橡皮线)

当鼠标左键松开时确定整个矩形,此时填充矩形表明完成绘制

例程

绘制边框橡皮线函数

1
2
3
4
5
6
7
8
9
void DrawBoxOutline(HWND hwnd, POINT ptBeg, POINT ptEnd)//正常情况下客户区内松开左键时调用,填充矩形
{
HDC hdc;
hdc = GetDC(hwnd);
SetROP2(hdc, R2_NOT);//该函数的作用是,只要是绘图,当前背景色取反,原来是白板,取反得黑,交替使用绘图函数会呈现一白一黑
SelectObject(hdc, GetStockObject(NULL_BRUSH));//设置画笔无色,用ROP2反色下一次画出黑色
Rectangle(hdc, ptBeg.x, ptBeg.y, ptEnd.x, ptEnd.y);
ReleaseDC(hwnd, hdc);
}

ptBeg和ptEnd都是相对于hwnd的客户区而言的,意思是在hwnd中绘制一个左上角ptBeg到右下角的ptEnd矩形边框

SetROP2

1
2
3
4
int SetROP2(
[in] HDC hdc,
[in] int rop2//样式
);

The SetROP2 function sets the current foreground mix mode. GDI uses the foreground mix mode to combine pens and interiors of filled objects with the colors already on the screen. The foreground mix mode defines how colors from the brush or pen and the colors in the existing image are to be combined.

SetROP2函数设置当前前景的混合模式.

GDI使用前景混合模式,作用是将画笔画刷的行为和先前已有的颜色结合起来.也就是说本次绘画会影响之前的绘画,rop2参数指定怎么个影响方法,是将像素点的颜色异或还是按位与还是直接擦除先前颜色等等

前景和背景是相反的,背景在正文图层的下面,前景在正文图层的上面

rop2的可选值:

Mix mode Meaning
R2_BLACK 直接画黑,不考虑很多,不使用画笔颜色,相当于设置了一个寂寞
R2_COPYPEN 继续使用画笔颜色,相当于设置了一个寂寞
R2_MASKNOTPEN 原屏幕颜色和画笔反色的结合
R2_MASKPEN Pixel is a combination of the colors common to both the pen and the screen.
R2_MASKPENNOT Pixel is a combination of the colors common to both the pen and the inverse of the screen.
R2_MERGENOTPEN Pixel is a combination of the screen color and the inverse of the pen color.
R2_MERGEPEN Pixel is a combination of the pen color and the screen color.
R2_MERGEPENNOT Pixel is a combination of the pen color and the inverse of the screen color.
R2_NOP 啥也不改变,更是个寂寞
R2_NOT 和原屏幕颜色相反
R2_NOTCOPYPEN 笔的反色
R2_NOTMASKPEN Pixel is the inverse of the R2_MASKPEN color.
R2_NOTMERGEPEN Pixel is the inverse of the R2_MERGEPEN color.
R2_NOTXORPEN Pixel is the inverse of the R2_XORPEN color.
R2_WHITE Pixel is always 1.
R2_XORPEN Pixel is a combination of the colors in the pen and in the screen, but not in both.

左键按下消息

1
2
3
4
5
6
7
8
9
10
11
12
case WM_LBUTTONDOWN:
ptBeg.x = ptEnd.x = LOWORD(lParam);//ptBeg获取当前鼠标位置
ptBeg.y = ptEnd.y = HIWORD(lParam);

DrawBoxOutline(hwnd, ptBeg, ptEnd);//描矩形边.使用ptBeg,ptEnd指定主对角线的矩形

SetCursor(LoadCursor(NULL, IDC_CROSS));//左键按下后进入绘制状态,鼠标变成十字提示绘图

fBlocking = TRUE;//一个flag,表征鼠标是否一直按下,这个状态会被Esc键修改,表示终止绘图
SetCapture(hwnd);

return 0;

左键按下时就已经开始绘图了,首先绘制一个点,即左键按下时的鼠标位置,

fBlocking变量用来记录绘图过程中有没有被Esc打断过.

SetCapture(hwnd)意思是从调用该函数开始,所有鼠标动作,包括不在本客户区的鼠标动作,全都被本程序捕获.这种状态需要到ReleaseCapture解出

Set/ReleaseCapture
1
2
SetCapture(hwnd);//此函数执行之后所有的鼠标动作将被hwnd指向的窗口捕获
ReleaseCapture();//直到本函数执行之后鼠标动作才会恢复正常

鼠标移动消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
case WM_MOUSEMOVE:
if (fBlocking)
{
SetCursor(LoadCursor(NULL, IDC_CROSS));

DrawBoxOutline(hwnd, ptBeg, ptEnd);

ptEnd.x = LOWORD(lParam);
ptEnd.y = HIWORD(lParam);

DrawBoxOutline(hwnd, ptBeg, ptEnd);//这里矩形边框画了两次,是因为后来这一次会更新前面那一次,由于SetROP2已经设置了黑白交替绘画,
//当前一次是黑笔是,后一次是白笔就擦除了前面的绘制
//如此做到只能看到一个实时边框,否则只绘制一次会导致很多重影
}
return 0;

由于左键起来之后会进行结算,因此绘图时的鼠标移动是压着左键移动的

首先判断了fBlocking的状态,如果被Esc打断则直接退出,不做处理

这里调用了两次DrawBoxOutline,其作用是:

由于SetROP2(hdc, R2_NOT)这个设置,会导致相邻两次绘图使用的颜色相反

如果本次使用黑色,那么下一次就使用白色

这样交替绘制的意义是:刚用黑色绘制出边框,接着逻辑上擦除它,但是在屏幕上不显示擦除,这就是压着黑笔停下时仍然能看到过期的客户区上有边框线.当画笔再次移动时,先前的边框已经被逻辑擦除,此时再画新线保证只有新线,不会有重影.这就实现了橡皮线的效果

这里两次调用DrawBoxOutline,头一次的ptEnd没有被修改,也就是上一次绘图使用的ptEnd,那么重绘这个矩形,相当于擦除了上一次的绘制

左键起来消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
case WM_LBUTTONUP:
if (fBlocking)//如果fBlocking==1说明左键按下没有起来过并且没有被Esc中断过
{
DrawBoxOutline(hwnd, ptBeg, ptEnd);//这里只调用了一次,是因为这一次要固定边框位置了,直接描黑

ptBoxBeg = ptBeg;
ptBoxEnd.x = LOWORD(lParam);//准备好需要填充的矩形范围
ptBoxEnd.y = HIWORD(lParam);

ReleaseCapture();
SetCursor(LoadCursor(NULL, IDC_ARROW));//放下屠刀之后,鼠标变成了斜向箭头样式
fBlocking = FALSE;//关闭中断标志,为下一次绘图做准备
fValidBox = TRUE;//设置填充区域有效,可以填充,为WM_PAINT做准备

InvalidateRect(hwnd, NULL, TRUE);
}
return 0;

首先判断fBlocking状态,如果中途被Esc打断过则不做处理

调用一次DrawBoxOutline擦除最后一次WM_MOUSEMOVE留下的边框.但是最后一次WM_MOUSEMOVE和WM_LBUTTONUP的到达时间非常接近,拉不开差距,因此这个函数调用与否意义不大,除非电脑很卡

释放对鼠标的捕获状态,程序对于客户区以外的鼠标动作不再处理

鼠标样式还原为斜向箭头

重置中断标志,设置绘图标志有效,提醒WM_PAINT应该绘制填充矩形了

Esc中断消息

1
2
3
4
5
6
7
8
9
10
case WM_CHAR:
if (fBlocking & wParam == '\x1B') // i.e., Escape //按下Esc终止绘制矩形,即使左键正在按下
{
DrawBoxOutline(hwnd, ptBeg, ptEnd);

SetCursor(LoadCursor(NULL, IDC_ARROW));

fBlocking = FALSE;//Esc中止逻辑修改了一个fBlocking,就可以让程序知道左键按下起来之间有没有被中断过
}
return 0;

对于该消息,例子采用的是处理字符消息,而不是处理虚拟键消息

如果出现Esc消息并且fBlocking表明正在绘图,那么设置fBlocking绘图无效

此后由于WM_MOUSEMOVE需要判断fBlocking正在绘图才继续绘制边框,因此Esc出现之后矩形边框橡皮线立刻消失

然而这种绘制失效的状态需要等到左键起来才能完全恢复

WM_LBUTTONUP也会先判断fBlocking是否还有效.无效则啥也不处理

此时所有标志都恢复原状(fValidBox压根没有被改变过,fBlocking被Esc重置为假)

绘图消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
case WM_PAINT:
hdc = BeginPaint(hwnd, &ps);
if (fValidBox)//这个值只能通过鼠标左键起来修改,表明确实有过画图
{
SelectObject(hdc, GetStockObject(BLACK_BRUSH));
Rectangle(hdc, ptBoxBeg.x, ptBoxBeg.y,ptBoxEnd.x, ptBoxEnd.y);//填充
}

if (fBlocking)//fBlocking=1表明正在绘图,应当打印边框
{
SetROP2(hdc, R2_NOT);
SelectObject(hdc, GetStockObject(NULL_BRUSH));
Rectangle(hdc, ptBeg.x, ptBeg.y, ptEnd.x,ptEnd.y);
}

EndPaint(hwnd, &ps);
return 0;

fValidBox是LBUTTONUP准备好的绘制标志,如果绘画中途没有Esc中断则LBUTTONUP消息处理中,会把fValidBox置有效,提醒WM_PAINT应该画图了

修改画刷为黑色画刷然后填充最后确定的矩形

如果fBlocking为有效说明仍然没有确定矩形的另一个点,此时WM_PAINT也打印矩形边框橡皮线,然而是多次一句,因为WM_MOUSEMOVE已经把这件事干了.

窗口销毁消息

1
2
3
4
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}

鼠标滚动

鼠标滚动消息WM_MOUSEWHEEL

当鼠标滚轮滚动时,该消息将被发往焦点窗口,

参数意义:

wParam

高字表明滚动增量

低字表明同时按下的虚拟键

Value Meaning
MK_CONTROL0x0008 The CTRL key is down.
MK_LBUTTON0x0001 The left mouse button is down.
MK_MBUTTON0x0010 The middle mouse button is down.
MK_RBUTTON0x0002 The right mouse button is down.
MK_SHIFT0x0004 The SHIFT key is down.
MK_XBUTTON10x0020 The first X button is down.
MK_XBUTTON20x0040 The second X button is down.

lParam

低字表明此时鼠标位置横坐标,屏幕坐标

高字表明纵坐标

滚动增量

衡量滚得狠不狠的参数,如果一下子转了好几圈显然增量很大,如果只发生了很少的转动,那么这点增量几乎不能导致程序卷动

在控制面板中我们可以设置滚动灵敏度,相同的滚动增量,假设都转一圈,可能灵敏度高的可以滚动一整页,灵敏度低的滚了三行,这是怎么实现的呢?

用滚动增量除以一个灵敏度系数,不妨给这个量起名灵敏后增量(我乱起的)

比如滚动增量为120,除以3得到40就是灵敏后增量.

在程序中我们可以设置一个单位行增量,意思是多少滚动增量可以导致程序卷动一个行,比如说设置为40

那么当灵敏度系数为3,那么初始时的120滚动增量就可以得到40 的灵敏后增量,刚好可以卷动一行

如果灵敏度系数为1,则灵敏后增量就是120.可以卷动三行

这就体现出不同灵敏度的区别了

加装滚动动作的Sysmets程序

滚动相关变量

1
2
static int iDeltaPerLine, iAccumDelta;
ULONG ulScrollLines;

iDeltaPerLine即单位行增量

iAccumDelta表示先前的滚动效果的累加,可以理解为初始滚动量

ulScrollLines存放灵敏度系数

滚动消息

1
2
3
4
5
6
7
8
9
10
11
12
case WM_MOUSEWHEEL:
if (iDeltaPerLine == 0)break;
iAccumDelta += (short)HIWORD(wParam);//累计滚动量
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;