1. 鼠标的基础知识:
1) 一般Windows程序会为程序窗口中的每个按钮都分配一个键盘快捷键,但是鼠标的便利还是无法逾越的;
2) 双键鼠标的右键:一般用于打开一些特殊的菜单(即上下文菜单)或执行一些特殊的拖动,而上下文菜单就是指在普通菜单栏之外的窗口中的菜单;
3) 查看鼠标是否接上或者鼠标上一共有几个键:
GetSystemMetrics( SM_MOUSEPRESENT ); // 接上为TRUE否则为FALSE
GetSystemMetrics( SM_CMOUSEBUTTONS ); // 返回鼠标上键的个数,如果鼠标没接上则返回0
4) 鼠标指针及热点:
i) 鼠标指针指图像显示器上显示的一个鼠标的位图小图标;
ii) 热点是指该小图标上的一个但像素精度的点,真正指示了鼠标的位置;
iii) IDI_ARROW的热点是箭头顶点,IDI_CROSS的热点是十字中点;
5) 对鼠标的三种操作:
i) 单击:按下按钮然后松开;
ii) 双击:连续两次快速单击(中间时间间隔要小,该时间可以设定);
iii) 拖动:按下按钮不放,并移动鼠标;
2. 客户区鼠标消息:
1) Windows定义了21种鼠标消息,但是只有10种和客户区有关,其余11种应用程序经常忽略都交由DefWindowProc自动处理;
2) 鼠标消息和键盘消息的一个不同之处在于,键盘消息只有输入焦点才能接受,但是当鼠标经过窗口或者在窗口内击中即使该窗口不是焦点也会收到鼠标消息;
3) 罗列10种客户区鼠标消息,第一种是WM_MOUSEMOVE,当鼠标在窗口中移动时就会受到该消息(不管其是不是焦点窗口):
!!!注意:只有当wndclass.style里加了CS_DBLCLKS选项时才可以接受双键消息!
4) lParam:LOWORD中存放x坐标,HIWORD中存放y坐标;
5) wParam:存放此时鼠标的附加状态,以MK_为前缀的位掩码表示(检测的时候用位与&),表示Mouse Key,即鼠标键,有:
MK_LBUTTON: 按下了左键
MK_RBUTTON:按下了右键
MK_MBUTTON:按下了中键
MK_SHIFT:按下了Shift键
MK_CONTROL:按下了Ctrl键
6) 利用鼠标击键改变当前活动窗口:通过鼠标左击可以将非活动窗口变为活动窗口,即当窗口收到WM_LBUTTONDOWN消息时就可以安全的保证该窗口是活动窗口了;
介绍一种特殊情况,先在一个窗口左击,按下后拖到另一个窗口再释放,则第一个窗口会收到WM_LBUTTONDOWN消息但收不到WM_LBUTTONUP消息,但第二个窗口刚好相反,但是第一个窗口仍然是活动窗口(因为第一个窗口收到了WM_LBUTTONDOWN消息但是第二个没有);
7) 两个特殊的例外:
i) 捕获鼠标:即鼠标位于窗口之外也能捕获其鼠标消息(捕获技术后面会将);
ii) 模态对话框:当一个模块对话框被启动后其它任何程序都不能接受鼠标消息,必须现处理模态对话框并关闭后才能恢复正常;
8) 延时:系统不会为每个鼠标经过的像素都产生WM_MOUSEMOVE消息,具体产生多少个这样的消息取决于过程处理的速度,接下来的程序Connect将展示这一特性,程序为鼠标一次拖动所经过的所有位置两两之间连上线,你会发现如果拖动的慢几乎整个凸包(由鼠标经过的位置形成)多会被填充上黑色,但是如果拖动的快就会看出明显的连线痕迹,因为拖动慢则就能及时处理鼠标消息导致鼠标经过的每个像素都来得及处理,而拖动快则过程来不及处理每个经过的像素;
事实上Windows这样处理WM_MOUSEMOVE消息,消息队列中只能有一个WM_MOUSEMOVE消息,只要该消息没被处理就不会再向队列中假如WM_MOUSEMOVE消息了,所以能处理多少个WM_MOUSEMOVE消息取决于处理速度:
// connect.c
#include
// 追踪轨迹的最大点个数
// 拖动时间太长,轨迹太长的太耗内存
#define CMAXPOINTS 1000
LRESULT CALLBACK WndProc( HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam );
int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCmdLine, int nCmdShow )
{
static TCHAR szAppName[] = TEXT("connect");
WNDCLASS wndclass;
HWND hWnd;
MSG msg;
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;
RegisterClass( &wndclass );
hWnd = CreateWindow(
szAppName, TEXT("Connect-the-Points Mouse demo"),
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT,
NULL, NULL, hInstance, NULL
);
ShowWindow( hWnd, nCmdShow );
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 POINT pt[CMAXPOINTS];
static int iCount; // 轨迹上的点数
HDC hdc;
PAINTSTRUCT ps;
int i, j;
switch ( message )
{
case WM_LBUTTONDOWN: // 按下左键就代表一次重新开始
iCount = 0;
InvalidateRect( hWnd, NULL, TRUE ); // 队列重刷而不是立即重刷
return 0;
case WM_MOUSEMOVE: // 这里规定必须是鼠标左键拖动才行
if ( wParam & MK_LBUTTON && iCount < CMAXPOINTS )
{
pt[iCount].x = LOWORD( lParam );
pt[iCount].y = HIWORD( lParam );
hdc = GetDC( hWnd );
// 将轨迹上的点通过对像素点着色记录下来
// 最后一个参数是COLORREF,0就是RGB( 0, 0, 0 )黑色
SetPixel( hdc, pt[iCount].x, pt[iCount].y, 0 );
ReleaseDC( hWnd, hdc );
iCount++;
}
return 0;
case WM_LBUTTONUP: // 释放后对轨迹上的点两两连线
InvalidateRect( hWnd, NULL, FALSE ); // 背景不刷白,留下轨迹点
return 0;
case WM_PAINT: // 连线在重刷中进行
// 当鼠标左键按下时iCount = 0,并且Invalidate为TRUE,因此一片空白,相当于重新开始
hdc = BeginPaint( hWnd, &ps );
SetCursor( LoadCursor( NULL, IDC_WAIT ) ); // 连线较慢设成等待图标
ShowCursor( TRUE ); // 显示计数加1,显示计数开始默认为0,只有非负时才能显示鼠标
// 因此现在显示计数为1了
for ( i = 0; i < iCount; i++ )
for ( j = i + 1; j < iCount; j++ )
{
MoveToEx( hdc, pt[i].x, pt[i].y, NULL );
LineTo( hdc, pt[j].x, pt[j].y );
}
SetCursor( LoadCursor( NULL, IDC_CROSS ) ); // 连线完毕后将图标设回来
ShowCursor( FALSE ); // 把显示计数减到0
EndPaint( hWnd, &ps );
return 0;
case WM_DESTROY:
PostQuitMessage( 0 );
return 0;
}
return DefWindowProc( hWnd, message, wParam, lParam );
}
有时需要监控按下鼠标键时Shift和Ctrl是否也被同时按下:
if ( wParam & MK_SHIFT )
{
if ( wParam & MK_CONTROL )
{
[Shift + Ctrl]
}
else
{
[Shift]
}
}
else
{
if ( wParam & MK_CONTROL )
{
[Ctrl]
}
else
{
[No Key Down]
}
}
1) 双击的两个标准:
i) 位置标准:两次单击的位置间隔不超过一个平均系统字体宽度;
ii) 时间标准:两次单击间隔必须规定在一个特定时长范围内(该时长可以在控制面板中定义);
2) 开启双击功能,必须在wndclass.style中加上CS_DBLCLKS选项才行,这样才能接受WM_DBLCLK消息;
3) 接受双击和连续两次单击的区别:
i) 连续两次单击:
WM_LBUTTONDOWN
WM_LBUTTONUP
WM_LBUTTONDOWN
WM_LBUTTONUP
ii) 双击消息:
WM_LBUTTONDOWN
WM_LBUTTONUP
WM_LBUTTONDBLCLK
WM_LBUTTONUP
可以看到仅仅是将第二个WM_LBUTTONDOWN替换成了WM_LBUTTONDBLCLK,这样更符合逻辑,一般第一次单击执行和普通单击同样的功能,而第二个双击消息执行一些特殊功能,比如双击打开一个文件夹时第一次单击的作用就是选中(此时Windows会反向高亮显示该文件夹),第二次双击消息则打开该文件,这种逻辑非常简洁有效!
5. 非客户区鼠标消息:
1) 是指窗口内部,并且是客户区以外的区域中的鼠标消息,包括标题栏、菜单栏、滚条等等;
2) 一般非客户去的鼠标消息和系统键盘消息一样都交由DefWindowProc来处理;
3) 和客户区鼠标消息一样,非客户区鼠标消息也对应着10种消息,移动消息是WM_NCMOUSEMOVE,其中NC指的是NoneClient:
4) lParam和非客户区的相同,而wParm不再是鼠标键标记,而是位置信息,表示鼠标落在了那片区域中,比如标题栏、滚条、菜单按钮上等等,前缀为HT,即Hit Test,击中测试的意思,击中测试就是指测试击中的位置在哪个区域中,比如HTSYSMENU,就表示击中了系统菜单按钮的意思,由于击中的在非客户区,因此lParam中的坐标不在是客户区中的坐标,而是整个屏幕的坐标了!可以利用一下两个函数进行客户区和屏幕坐标之间的转换:
ScreenToClient( hwnd, &pt )和ClientToScreen( hwnd, &pt ),两个函数都会将原来pt中的坐标抹去换成新坐标,并且不备份原坐标!
当击中位置在客户区左上方时转换而成的客户区坐标为负的!
6. 击中测试以及消息产生消息机制:
1) 击中测试的概念:鼠标按下 -> 确定坐标 -> 根据坐标判断鼠标落在哪个区域
结束上述过程后就可以真正产生响应的精确的鼠标消息了,比如判断好了鼠标落在客户区,在可以产生WM_LBUTTONDOWN消息了,如果判断好了鼠标落在系统菜单上则可以产生WM_NCLBUTTONDOWN消息并且wParam为HTSYSMENU;
2) 事实上Windows是严格遵照上述过程的,这就要引出最后一种鼠标消息——WM_NCHTTEST消息,Windows由该消息产生其它所有的鼠标消息,这就是鼠标击中测试消息,用来完成1)中讲述的击中测试的过程,在产生任何一个精确的鼠标消息之前都是先产生WM_NCHTTEST消息的,该消息用于测试击中区域,因此wParam没用,只有lParam中的坐标有用,该坐标交由Windows来判断落在哪个区域(一般应用程序不响应都交由DefWindowProc来处理),比如判断好了落在客户区,则DefWindowProc返回HTCLIENT(表示击中了客户区),之后便自动产生一条WM_LBUTTONDOWN的消息插入消息队列中,如果判断好了落在系统菜单按钮上,则DefWindowProc返回HTSYSMENU,并产生一个WM_NCLBUTTONDOWN消息,并将该返回值作为消息的wParam,再将该消息插入到消息队列中去,因此击中消息是优先级最高的鼠标消息,用来产生所有客户区和非客户区的鼠标消息,这就是Windows其中一个消息产生消息的案例;
3) DefWindowProc处理WM_NCHTTEST消息的返回值可以是所有非客户区鼠标消息的wParam值也可以是HTCLIENT(击中客户区)、HTNOWHERE(不在任何窗口)、HTTRANSPARENT(击中了被另一个窗口遮住的窗口)、HTERROR(使DefWindowProc产生一个警示声),当击中客户区后Windows会将WM_NCHTTEST的屏幕坐标转化成客户区坐标产生客户区鼠标消息;
WM_NCHTTEST的坐标必定是屏幕坐标(以为逻辑上它属于非客户区鼠标消息)!
4) 屏蔽所有鼠标消息:
很简单,只要在应用程序中响应WM_WMNCHTTEST消息,并直接返回即可:
case WM_NCHTTEST: return ( LRESULT )HTNOWHERE;
这样不管是客户区还是非客户区都接收不到鼠标消息了,什么菜单、滚条、关闭按钮都将无效!
7. 一个简单的击中测试程序Checker:
将客户区划分成一个5×5的矩形区域,如果鼠标击中某个小区域,就在该区域中画叉,再次击中则叉消失:
// checker1.c
#include
// 分成5×5的区域
#define CDIVISIONS 5
BOOL fState[CDIVISIONS][CDIVISIONS]; // 记录各个区域的状态,有叉为1,空位0
int cxBlock, cyBlock; // 记录区域的坐标
LRESULT CALLBACK WndProc( HWND hWnd, UINT message, WPARAM wParam, LPARAM lPram );
void DrawCross( HDC hdc, int x, int y ); // 画叉
int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd )
{
static TCHAR szAppName[] = TEXT("checker1");
WNDCLASS wndclass;
HWND hWnd;
MSG msg;
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;
RegisterClass( &wndclass );
hWnd = CreateWindow(
szAppName, TEXT("Checker1"),
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT,
NULL, NULL, hInstance, NULL
);
ShowWindow( hWnd, nShowCmd );
UpdateWindow( hWnd );
while ( GetMessage( &msg, NULL, 0, 0 ) )
{
TranslateMessage( &msg );
DispatchMessage( &msg );
}
return msg.wParam;
}
void DrawCross( HDC hdc, int x, int y )
{
if ( !fState[x][y] ) // 白笔覆盖黑笔抹掉叉,不要费时非资源地重画
{
SelectObject( hdc, GetStockObject( WHITE_PEN ) );
}
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 );
if ( !fState[x][y] ) // 抹掉后要恢复黑笔,否则会影响其他区域的绘制
{
SelectObject( hdc, GetStockObject( BLACK_PEN ) );
}
}
LRESULT CALLBACK WndProc( HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam )
{
HDC hdc;
PAINTSTRUCT ps;
int x, y;
switch ( message )
{
case WM_SIZE:
cxBlock = LOWORD( lParam ) / CDIVISIONS;
cyBlock = HIWORD( lParam ) / CDIVISIONS;
return 0;
case WM_LBUTTONDOWN:
x = LOWORD( lParam ) / cxBlock;
y = HIWORD( lParam ) / cyBlock;
if ( x < CDIVISIONS && y < CDIVISIONS ) // 击中测试,确定落在哪个区域中
{
fState[x][y] ^= 1; // 状态改变
hdc = GetDC( hWnd );
DrawCross( hdc, x, y );
ReleaseDC( hWnd, hdc );
}
else
MessageBeep( 0 ); // 击中了客户区以内但又没落在小格子中则蜂鸣警报
// 即击中了边界区域
return 0;
case WM_PAINT:
hdc = BeginPaint( hWnd, &ps );
for ( x = 0; x < CDIVISIONS; x++ ) // 重绘了话需要绘制网格,并还原所有网格的状态
for ( y = 0; y < CDIVISIONS; y++ )
{
Rectangle( hdc, x * cxBlock, y * cyBlock, ( x + 1 ) * cxBlock, ( y + 1 ) * cyBlock );
DrawCross( hdc, x, y );
}
EndPaint( hWnd, &ps );
return 0;
case WM_DESTROY:
PostQuitMessage( 0 );
return 0;
}
return DefWindowProc( hWnd, message, wParam, lParam );
}
8. 加入键盘接口的Checker:
在checker1的基础上加入键盘接口,可以直接使用方向键控制鼠标,当使用方向键时鼠标只能落在小格子的中心,空格键和回车键可以模拟鼠标左击的功能;
当然,在使用方向键的时候需要获取鼠标的实际位置,并以该位置为参考坐标进行移动,此时需要用到GetCursorPos函数,参数为POINT指针,但是其没有hwnd参数,因此获得的坐标肯定跟窗口无关(即屏幕坐标),需要使用ScreenToClient函数转化成客户区坐标,同样当用键盘移动鼠标之后也需要显示鼠标的新位置,此时需要SetCursorPos函数,其有两个参数,分别为鼠标的x和y坐标,同样也没有hwnd参数,因此改坐标为屏幕坐标,所以需要使用ClientToScreen函数将客户区坐标转化成屏幕坐标再设置;
!!GetCursorPos函数是具有时效性的,即得到的是调用该函数时鼠标的坐标,而鼠标消息中的lParam存储的是产生该消息那一刻的坐标,这两个概念是不一样的,使用的时候一定要注意!
// checker2.c
#include
#define CDIVISIONS 5
LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam );
int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd )
{
static TCHAR szAppName[] = TEXT("checker2");
WNDCLASS wndclass;
HWND hwnd;
MSG msg;
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;
RegisterClass( &wndclass );
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, nShowCmd );
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[CDIVISIONS][CDIVISIONS];
static int cxBlock, cyBlock;
HDC hdc;
PAINTSTRUCT ps;
POINT point;
RECT rect;
int x, y;
switch ( message )
{
case WM_SIZE:
cxBlock = LOWORD( lParam ) / CDIVISIONS;
cyBlock = HIWORD( lParam ) / CDIVISIONS;
return 0;
case WM_SETFOCUS: // 考虑到没有鼠标接入电脑的情形
// 如果没有鼠标接入电脑则显示计数值初始为-1
// 所以在这种情况下也要支持鼠标的话就需要将显示计数值增加到非负才行
ShowCursor( TRUE );
return 0;
case WM_KILLFOCUS:
ShowCursor( FALSE );
return 0;
case WM_LBUTTONDOWN:
x = LOWORD( lParam ) / cxBlock;
y = HIWORD( lParam ) / cyBlock;
if ( x < CDIVISIONS && y < CDIVISIONS )
{
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_KEYDOWN: // 用键盘模拟鼠标的操作
GetCursorPos( &point );
ScreenToClient( hwnd, &point ); // 获取此时鼠标的位置并转化成客户区坐标
// 这样就有了一个操作的起始位置,这样接下来的操作都是相对该起始位置的
x = max( 0, min( CDIVISIONS - 1, point.x / cxBlock ) ); // 计算此时鼠标所在的方块区域
y = max( 0, min( CDIVISIONS - 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 = CDIVISIONS - 1;
break;
case VK_RETURN: // 回车和空格模拟鼠标左击
case VK_SPACE:
SendMessage( hwnd, WM_LBUTTONDOWN, MK_LBUTTON, MAKELONG( x * cxBlock, y * cyBlock ) );
// MAKELONG可以做一个32位值,低16位是第一个参数,高16位是第二个参数
break;
}
// 越界的要跑到另一头
x = ( x + CDIVISIONS ) % CDIVISIONS;
y = ( y + CDIVISIONS ) % CDIVISIONS;
// 通过键盘移动鼠标则把坐标都规整到格子的中心位置
point.x = x * cxBlock + cxBlock / 2;
point.y = y * cyBlock + cyBlock / 2;
ClientToScreen( hwnd, &point ); // 转化成屏幕坐标再调整鼠标位置
SetCursorPos( point.x, point.y );
return 0;
case WM_PAINT:
hdc = BeginPaint( hwnd, &ps );
for ( x = 0; x < CDIVISIONS; x++ )
for ( y = 0; y < CDIVISIONS; 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 );
}