输入设备鼠标你得会
目前巿面上各种各样的鼠标琳琅满目,不过按外形可以分为两键鼠标、三键鼠标、滚轴鼠标、感应鼠标和五键鼠标等。滚轴鼠标和感应鼠标在笔记本计算机中应用很普逼。往不同方向转动鼠标中间的小圆球,或在感应板上移动手指,光标就会向相应的方向移动。当光标到达预定位置时,按一下鼠标或感应板,即可执行相应操作。
当用户移动鼠标时,系统在屏幕上显示一个称为鼠标光标的位图,鼠标光标中包含一个称为热点的单像素点,热点确定光标的位置。各种系统预定义光标的形状在讲解注册窗口类WNDCLASSEX结构的hCursor字段时说过。IDC_ARROW标准箭头光标的热点位于箭头的最上部,IDC_CROSS十字线光标的热点位于十字线的中心。除了系统预定义的光标形状,后面还会学习自定义光标。对于接收鼠标消息,不要求窗口必须处于活动状态或具有输入焦点。发生鼠标事件时,光标位置下面的窗口通常会接收到鼠标消息。
对于三键鼠标来说,3个按钮分别被称为左键、中键和右键。对鼠标按钮的操作包括单击、双击、移动和拖动。
- 单击∶按下鼠标按钮,然后松开。
- 双击∶连续两次快速单击鼠标按钮。
- 移动∶改变鼠标光标的位置。
- 拖动︰按下鼠标按钮不放,并移动鼠标光标。
Windows支持带有5个按钮的鼠标,五键鼠标除了左、中、右按钮外,还有XBUTTON1和XBUTTON2,浏览器中通常会使用这两个按钮实现网页的前进和后退功能。
要为习惯使用左手的用户配置鼠标,程序可以通过调用SwapMouseButton函数或SystemParameterslnfo函数(使用SPI_SETMOUSEBUTTONSWAP标志)来反转鼠标左键和右键,但请注意,鼠标是共享资源,因此反转鼠标左键和右键会影响其他所有程序。程序通常没有必要提供这个功能,因为控制面板已经为用户提供了习惯左手还是右手,以及后用单击锁定、双击速度、光标移动速度等丰富的功能,具体参见控制面板→鼠标(打开控制面板以后,查看方式选择小图标)
客户区鼠标消息
当用户移动、按下或释放鼠标按钮时,会生成鼠标输入事件。系统会将鼠标输入事件转换为消息,然后发送到线程的消息队列中。当用户在窗口的客户区内移动光标时,系统会不断发送一系列WM_MOUSEMOVE
消息;当用户在客户区内按下或释放鼠标按钮时,会发送下表所示的消息之一。
宏常量 | 含义 |
---|---|
WM_LBUTTONDOWN |
按下鼠标左键 |
WM_LBUTTONUP |
鼠标左键被释放 |
WM_LBUTTONDBLCLK |
双击鼠标左键 |
WM_MBUTTONDOWN |
按下鼠标中键 |
WM_MBUTTONUP |
鼠标中键被释放 |
WM_MBUTTONDBLCLK |
双击鼠标中键 |
WM_RBUTTONDOWN |
按下鼠标右键 |
WM_RBUTTONUP |
鼠标右键被释放 |
WM_RBUTTONDBLCLK |
双击鼠标右键 |
分别是左键、中键和右键的按下、释放和双击消息。鼠标按下和鼠标抬起消息是成对出现的,例如一次鼠标左键单击事件会生成WM_LBUTTONDOWN和WM_LBUTTONUP消息。
此外,程序可以通过调用TrackMouseEvent函数让系统发送另外两条消息。当鼠标光标悬停在客户区一段时间后发送WM_MOUSEHOVER消息,当鼠标光标离开客户区时发送WM_MOUSELEAVE消息∶
BOOL WINAPl TrackMouseEvent(_Inout_ LPTRACKMOUSEEVENT lpEventTrack); //TRACKMOUSEEVENT结构
参数lpEventTrack是一个指向TRACKMOUSEEVENT结构的指针,在WinUser.h头文件中定义如下︰
typedef struct tagTRACKMOUSEEVENT {
DWORD cbSize; //结构的大小
DWORD dwFlags; //标志位
HWND hwndTrack; //要跟踪的窗口的句柄
DWORD dwHoverTime; //悬停超时,以毫秒为单位
}TRACKMOUSEEVENT,*LPTRACKMOUSEEVENT;
- 参数dwFlags指定标志位,可以是下表所示的值的组合。
宏常量 | 含义 |
---|---|
TME_CANCEL |
取消先前的跟踪请求,还需要同时指定要取消的跟踪类型,例如,要取消悬停跟踪,需要指定为TME_CANCEL|TME_HOVER标志 |
TME_HOVER |
当鼠标光标悬停在客户区一段时间后发送WM_MOUSEHOVER消息,该消息只会触发一次。如果需要再次发送该消息,则必须重新调用TrackMouseEvent函数 |
TME_LEAVE |
当鼠标光标离开客户区时发送WM_MOUSELEAVE消息,该消息产生以后,所有由TrackMouseEvent函数设置的鼠标跟踪请求(悬停和离开)都会被取消。请注意,如果鼠标不在窗口的客户区内,会立即发送WM_MOUSELEAVE消息 |
TME_NONCLIENT |
当鼠标光标在非客户区悬停一段时间或离开时发送非客户区鼠标消息WM_NCMOUSEHOVER或WM_NCMOUSELEAVE消息,用法和前两个类似 |
- 参数dwHoverTime指定悬停超时(如果在dwFlags参数中指定了TME_HOVER),以毫秒为单位,可以设置为HOVER_DEFAULT,表示使用系统默认的悬停超时.
客户区鼠标消息的wParam参数包含鼠标事件发生时其他鼠标按钮以及Ctrl和Shift键的状态标志。wParam参数可以是下表所示的值的组合。
宏常量 | 含义 |
---|---|
MK_LBUTTON |
鼠标左键已按下 |
MK_MBUTTON |
鼠标中键已按下 |
MK_RBUTTON |
鼠标右键已按下 |
MK_CONTROL |
Ctrl键已按下 |
MK_SHIFT |
Shift键已按下 |
MK_XBUTTON1 |
第一个X按钮已按下 |
MK_XBUTTON2 |
第二个X按钮已按下 |
例如,当接收到WM_LBUTTONDOWN消息时,如果wParam & MK_SHIFT的值为TRUE(非需),则表示用户按下左键的同时还按下了Shift键。
客户区鼠标消息的IParam参数表示鼠标事件发生时光标热点的位置,lParam参数的低位字表示热
点的X坐标,高位字表示Y坐标,相对于客户区左上角。可以按如下方式获取光标热点的X和Y坐标
int xPos,yPos;
xPos = GET_X_LPARAM(IParam);
yPos = GET_Y_LPARAM(IParam);
GET_X_LPARAM和GET_Y_LPARAM宏在Windowsx.h头文件中定义如下∶
#define GET_×_LPARAM(Ip) ((int)(short)LoWORD(Ip))
#define GET_Y_LPARAM(Ip) ((int)(short)HIWORD(Ip))
鼠标光标的坐标有时候可能是负值,因此不能使用LOWORD或
HIWORD宏来提取光标位置的X坐标和Y坐标。LOWORD和HIWORD宏返回的是WORD类型,而WORD被定义为无符号短整型︰
typedef unsigned short WORD;
关于鼠标双击消息(WM_LBUTTONDBLCLK WM_MBUTTONDBLCLK WM_RBUTTONDBLCLK) ,在讲解
WNDCLASSEX结构的style字段时说过,必须指定CS_DBLCLKS样式才可以接收鼠标双击消息。鼠标双击消息通常会连续生成4条消息,例如,双击鼠标左键会生成以下消息序列∶
WM_LBUTTONDOWN
WM_LBUTTONUP
WM_LBUTTONDBLCLK
WM_LBUTTONUP
非客户区鼠标消息
窗口的非客户区包括标题栏、菜单栏、菜单、边框、滚动条、最小化按钮和最大化按钮等,当在窗口的非客户区内发生鼠标事件时,会生成非客户区鼠标消息,非客户区鼠标消息的名称包含字母NC。例如,当光标在非客户区中移动时会生成WM_NCMOUSEMOVE消息,当光标在非客户区时按下鼠标左键会生成WM_NCLBUTTONDOWN消息。
除此之外,通过调用TrackMouseEvent函数(dwFlags参数包含TME_NONCLIENT标志),当鼠标光标在非客户区悬停一段时间或离开时会发送WM_NCMOUSEHOVER或WM_NCMOUSELEAVE消息。非客户区鼠标消息由DefWindowProc函数执行默认处理,例如,当鼠标光标移动到窗口的边框时,DefWindowProc函数会处理非客户区鼠标消息,将光标更改为双向箭头。如果没有特殊需求,则程序不需要自己处理非客户区鼠标消息﹔如果实在需要处理,则处理完以后还应该转交给DefWindowProc函数。请看如下代码∶
case WM_NCLBUTTONDOWN:
case WM_NCLBUTTONUP:
case WM_NCLBUTTONDBLCLK:
return 0;
这么做的结果是,无法使用系统菜单,无法使用最小化、最大化、关闭按钮,无法打开程序的菜单,无法通过拖拉边框调整窗口大小等。
非客户区鼠标消息的wParam参数包含命中测试值。每当发生鼠标事件时(包括客户区和非客户区鼠标事件),系统都会先发送
WM_NCHITTEST消息,DefWindowProc函数会检查鼠标事件发生时的鼠标光标热点坐标以确定是发送客户区还是非客户区鼠标消息,随后还可能发送WM_SYsCOMMAND消息,DefWindowProc函数处理WM_NCHITTEST消息以后的返回值是指明光标热点位置的命中测试值。命中测试值可以是下表所示的值之一。
例如,如果鼠标事件发生时光标热点位于窗口的客户区中,则DefWindowProc函数将命中测试值HTCLIENT返回给窗口过程,窗口过程将HTCLIENT返回给系统,系统将光标热点的屏幕坐标转换为客户区坐标,然后发送相应的客户区鼠标消息
如果光标热点位于窗口的非客户区中,则DefWindowProc函数会返回其他命中测试值,窗口过程将其返回给系统,系统将命中测试值放在消息的wParam参数中,将光标热点的屏幕坐标放在lParam参数中,然后发送非客户区鼠标消息。也可能发送WM_SYSCOMMAND消息。例如,大家都知道双击程序窗口左上角的系统菜单图标可以关闭程序,双击事件首先产生WM_NCHITTEST消息,鼠标光标位于系统菜单图标之上,所以
DefWindowProc函数处理WM_NCHITTEST消息以后返回HTSYSMENU;系统发送WM_NCLBUTTONDBLCLK消息,其中wParam参数等于HTSYSMENU DefWindowProc函数处理
WM_NCLBUTTONDBLCLK消息,然后发送WM_SYSCOMMAND消息,其中wParam参数等于SC_CLOSE (当用户手动单击系统菜单中的"关闭"按钮时,也是产生WM_SYSCOMMAND消息;DefWindowProc函数处理WM_SYSCOMMAND消息,并发送WM_CLOSE消息;DefWindowProc处理WM_CLOSE消息,向窗口过程发送一个WM_DESTROY消息,DefWindowProc函数不会处理WM_DESTROY消息,这个消息需要我们自己处理.
非客户区鼠标消息的IParam参数包含鼠标光标热点的X坐标和Y坐标。与客户区鼠标消息不同,该坐标是屏幕坐标而不是客户区坐标,相对于屏幕左上角。可以使用GET_X_LPARAM (lParam)和
GET_Y_LPARAM (IParam)宏分别提取光标热点的X和Y坐标。通过调用ScreenToClient和ClientToScreen函数可以将屏幕坐标与客户区坐标相互转换。如果一个屏幕坐标点位于窗口客户区的左侧或上方,那么转换成客户区坐标后,X值或Y值会是负数。
X按钮消息
当用户按下或释放XBUTTON1或XBUTTON2按钮时会发送WM_XBUTTON*
或WM_NCXBUTTON*
消息,这些消息的wParam参数的高位字包含一个标志,指示用户按下或释放了哪个X按钮。
比如说,当用户在窗口的客户区中按下、释放或双击第一个或第二个X按钮时会发送WM_XBUTTONDOWN- WM_XBUTTONUP或WM_XBUTTONDBLCLK消息。
这些消息的wParam参数的低位字包含鼠标事件发生时其他鼠标按钮以及Ctrl和Shift键的状态标志,可用的标志和客户区鼠标消息的wParam参数相同。
wParam参数的高位字指明用户是按下、释放或双击了哪个X按钮。可以是下表所示的值之一。
宏常量 | 含义 |
---|---|
XBUTTON1 |
按下、释放或双击了第一个X按钮 |
XBUTTON2 |
按下、释放或双击了第二个X按钮 |
可以使用以下代码提取wParam参数中的信息∶
fwKeys = GET_KEYSTATE_WPARAM(wParam); //其他鼠标按钮以及Ctrl和Shift键的状态标志
fwButton = GET_XBUTTON_WPARAM(wParam); //按下、释放或双击了哪个X按钮
这些消息的IParam参数表示鼠标事件发生时光标热点的位置。lParam参数的低位字表示热点的X坐标,高位字表示Y坐标,相对于客户区左上角。可以使用GET_X_LPARAM (lParam)和GET_Y_LPARAM (IParam)宏分别提取光标热点的X和Y坐标。
当用户在窗口的非客户区中按下、释放或双击第一个或第二个X按钮时会发送WM_NCXBUTTONDOWN-WM_NCXBUTTONUP或WM_NCXBUTTONDBLCLK消息。
这些消息的wParam参数的低位字包含DefWindowProc函数在处理WM_NCHITTEST消息时返回的命中测试值,可用的命中测试值与非客户区鼠标消息的wParam参数相同;高位字表示按下了哪个X按钮,即XBUTTON1或XBUTTON2。
nHittest = GET_NCHITTEST_WPARAM(wParam); //命中测试值
fwButton = GET_XBUTTON_WPARAM(wParam); //按下、释放或双击了哪个X按钮
- 这些消息的IParam参数包含鼠标事件发生时光标热点的X坐标和Y坐标,坐标相对于屏幕的左上角。可以使用GET_X_LPARAM(IParam)和GET_Y_LPARAM(IParam)宏分别提取光标热点的X和Y坐标。
请注意,对于鼠标左键、中键或右键的客户区鼠标消息,窗口过程处理完以后应返回0﹔但是对于X按钮的客户区消息和非客户区消息处理完毕以后应返回TRUE。对于X按钮消息,DefWindowProc函数会执行默认处理,处理以后DefWindowProc函数会向窗口发送
WM_APPCOMMAND消息。DefWindowProc函数还会处理WM_APPCOMMAND消息,关于WM_APPCOMMAND消息参见MSDN.
鼠标光标函数
GetCursorPos函数可以获取鼠标光标的当前位置∶
BoOL WINAPI GetCursorPos(_Out_ LPPOINT lpPoint); //在lpPoint结构中返回鼠标光标位置,屏幕坐标
SetCursorPos函数可以将鼠标光标移动到指定的位置∶
BOOL WINAPI SetCursorPos(_In_ int x,_In_ int Y);//屏幕坐标
调用ShowCursor函数可以显示或隐藏鼠标光标︰
int WINAPI ShowCursor(_In_BOOL bShow);//设置为TRUE则显示计数加1,设置为FALSE,则显示计数减1
GetCursorlnfo函数可以获取鼠标光标的显示或隐藏状态、光标句柄和坐标∶
BOOL WINAPI GetCursorlnfo(_Inout_PCURSORINFO pci); //在这个CURSORINFO结构中返回鼠标光标信息
pci参数是一个指向CURSORINFO结构的指针,该结构在WinUser.h头文件中定义如下∶
typedef struct tagCURSORINFO
{
DWORD cbSize; //该结构的大小
DWORD flags; //光标状态标志,如果是0表示光标被隐藏,如果是CURSOR_SHOWING(1)表示正 在显示
HCURSOR hCursor;//光标句柄
POINT ptScreenPos; //接收光标坐标的POINT结构,屏幕坐标
}CURSORINFO,*PCURSORINFO,*LPCURSORINFO;
SetCursor函数可以设置光标的形状︰
HCURSOR WINAPI SetCursor(_In_opt_ HCURSOR hCursor); //光标的句柄
参数hCursor指定光标的句柄,这个句柄可以由LoadCursorCreateCursor或Loadlmage函数返回。如果hCursor参数设置为NULL,则从屏幕上删除光标。函数返回值是前一个光标的句柄。
例如调用SetCursor(LoadCursor(NULL,IDC_CROSS));
可以把光标形状设置为系统预定义光标十字线形状,关于创建自定义光标后面再讲。需要注意的是,鼠标光标是共享资源,仅当鼠标光标位于我们程序的客户区内时,应该调用SetCursorPos ShowCursor· SetCursor等函数改变鼠标光标,在光标离开客户区之前应该恢复为先前的状态。
ClipCursor函数可以将鼠标光标的活动范围限制在指定的矩形区域以内∶
BOOL WINAPl ClipCursor(_In_opt_const RECT *IpRect); //屏幕坐标,lpRect设置为NULL,则可以自由移动
某些加密程序为了防止用户调试自己的程序或者其他非法操作,经常限制用户的鼠标只能在自己程序的窗口范围内活动,例如下面的代码可以实现这个目的∶
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
RECT rect;
switch (uMsg)
{
case WM_CREATE:
SetTimer(hwnd, 1, 100, NULL);
return 0;
case WM_TIMER:
GetWindowRect(hwnd,&rect);
ClipCursor(&rect);
return 0;
case WM_DESTROY:
ClipCursor(NULL);
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
在WM_CREATE消息中调用SetTimer函数创建了一个计时器,每隔100 ms向窗口过程发送一个WM_TIMER消息。
鼠标捕获
本节实现一个鼠标绘制矩形的示例,用鼠标绘图需要跟踪鼠标光标的位置,跟踪光标通常涉及处理WM_LBUTTONDOWN
WM_MOUSEMOVE和WM_LBUTTONUP消息。
通过记录WM_LBUTTONDOWN消息的IParam参数中提供的光标位置来确定起点,鼠标移动时会不断产生一系列WM_MOUSEMOVE消息,通常也应该处理WM_MOUSEMOVE消息以实时反映所绘制图形的变化,最后,处理WM_LBUTTONUP消息结束绘制。DrawRectangle程序运行效果如下图所示:
#include <Windows.h>
#include <Windowsx.h>
// 函数声明
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
VOID DrawFrame(HWND hwnd, POINT ptLeftTop, POINT ptRightBottom);
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
WNDCLASSEX wndclass;
TCHAR szClassName[] = TEXT("MyWindow");
TCHAR szAppName[] = TEXT("DrawRectangle");
HWND hwnd;
MSG msg;
wndclass.cbSize = sizeof(WNDCLASSEX);
wndclass.style = CS_HREDRAW | CS_VREDRAW;
wndclass.lpfnWndProc = WindowProc;
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 = szClassName;
wndclass.hIconSm = NULL;
RegisterClassEx(&wndclass);
hwnd = CreateWindowEx(0, szClassName, szAppName, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, 400, 300, NULL, NULL, hInstance, NULL);
ShowWindow(hwnd, nCmdShow);
UpdateWindow(hwnd);
while (GetMessage(&msg, NULL, 0, 0) != 0)
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return msg.wParam;
}
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
HDC hdc;
PAINTSTRUCT ps;
static POINT ptLeftTop, ptRightBottom; // 矩形的左上角和右下角坐标
static BOOL bStarting;
switch (uMsg)
{
case WM_LBUTTONDOWN:
//SetCapture(hwnd);
SetCursor(LoadCursor(NULL, IDC_CROSS));
// 初始化新矩形的左上角和右下角坐标
ptLeftTop.x = ptRightBottom.x = GET_X_LPARAM(lParam);
ptLeftTop.y = ptRightBottom.y = GET_Y_LPARAM(lParam);
bStarting = TRUE;
return 0;
case WM_MOUSEMOVE:
if (bStarting)
{
SetCursor(LoadCursor(NULL, IDC_CROSS));
// 先擦除上一次WM_MOUSEMOVE消息所画的矩形,WM_MOUSEMOVE消息会不断产生
DrawFrame(hwnd, ptLeftTop, ptRightBottom);
// 新矩形的右下角坐标
ptRightBottom.x = GET_X_LPARAM(lParam);
ptRightBottom.y = GET_Y_LPARAM(lParam);
// 绘制新矩形
DrawFrame(hwnd, ptLeftTop, ptRightBottom);
}
return 0;
case WM_LBUTTONUP:
if (bStarting)
{
// 擦除WM_MOUSEMOVE消息中所画的最后一个矩形
DrawFrame(hwnd, ptLeftTop, ptRightBottom);
// 最终矩形的右下角坐标
ptRightBottom.x = GET_X_LPARAM(lParam);
ptRightBottom.y = GET_Y_LPARAM(lParam);
SetCursor(LoadCursor(NULL, IDC_ARROW));
bStarting = FALSE;
//ReleaseCapture();
// 绘制最终的矩形
InvalidateRect(hwnd, NULL, TRUE);
}
return 0;
case WM_PAINT:
hdc = BeginPaint(hwnd, &ps);
Rectangle(hdc, ptLeftTop.x, ptLeftTop.y, ptRightBottom.x, ptRightBottom.y);
EndPaint(hwnd, &ps);
return 0;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
VOID DrawFrame(HWND hwnd, POINT ptLeftTop, POINT ptRightBottom)
{
HDC hdc = GetDC(hwnd);
SelectObject(hdc, GetStockObject(NULL_BRUSH));
SetROP2(hdc, R2_NOT);
Rectangle(hdc, ptLeftTop.x, ptLeftTop.y, ptRightBottom.x, ptRightBottom.y);
SelectObject(hdc, GetStockObject(WHITE_BRUSH));
ReleaseDC(hwnd, hdc);
}
用户按下鼠标左键,鼠标光标的当前位置作为要绘制矩形的左上角和右下角坐标﹔然后不断拖动鼠标产生WM_MOUSEMOVE消息,将矩形的右下角坐标重设为鼠标光标的当前位置,然后画出矩形的边框;当用户释放鼠标时,绘制最终的矩形。
但是存在一个问题,在程序窗口的客户区内按下鼠标左键,然后移动鼠标到窗口以外时,程序将停止接收WM_MOUSEMOVE消息,现在释放鼠标,由于鼠标落在客户区以外,所以程序没有接收到
WM_BUTTONUP消息。再将鼠标移回程序窗口的客户区,窗口过程仍然会认为鼠标按钮处于按下的状态,程序现在不知道该如何运行了。
鼠标按下和鼠标抬起消息是成对出现的,这一点没错,但是,在一个窗口没有接收WM_LBUTTONDOWN消息的情况下,该窗口的窗口过程可能会接收到WM_LBUTTONUP消息,例如用户在其他窗口内按下鼠标,再移动到我们的程序窗口,然后释放,此时就会发生这种情况
如果用户在程序窗口按下鼠标,然后移动鼠标到另一个窗口再释放,则窗口过程会接收到WM_LBUTTONDOWN消息,但接收不到wM_LBUTTONUP消息。
把WM_LBUTTONDOWN消息中的SetCapture(hwnd);注释去掉,把WM_LBUTTONUP消息中的ReleaseCapture();注释也去掉,程序工作就会正常。下面解释这两个函数。
SetCapture函数为指定窗口设置鼠标捕获,然后该窗口可以接收所有鼠标消息,直到调用ReleaseCapture函数或为其他窗口设置了鼠标捕获∶
HWND WINAPI SetCapture(_ln_HWND hWnd); //为hWnd窗口捕获鼠标
函数返回值是先前捕获鼠标的窗口的句柄,一次只能有一个窗口来捕获鼠标。
不再需要鼠标捕获的时候必须调用ReleaseCapture函数释放鼠标捕获∶
BOOL WINAPI ReleaseCapture(void);
为指定窗口设置鼠标捕获以后,鼠标消息总是以客户区鼠标消息的形式出现。即使鼠标位于窗口的非客户区,参数lParam表示的鼠标光标也始终相对于客户区。当鼠标位于客户区之外的左方或上方时,X和Y坐标会是负值。不要随便为一个程序窗口设置鼠标捕获,通常只有当鼠标在客户区内被按下时,程序扌应该捕获鼠标﹔当释放鼠标按钮时,应该立即停止捕获。
还可以使用ClipCursor函数在绘制矩形期间将光标活动区域限制在客户区范围以内,例如在WM_LBUTTONDOWN消息中添加以下代码∶
...
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
HDC hdc;
PAINTSTRUCT ps;
static POINT ptLeftTop, ptRightBottom; // 矩形的左上角和右下角坐标
static BOOL bStarting;
RECT rectClient;
switch (uMsg)
{
case WM_LBUTTONDOWN:
// SetCapture(hwnd);
GetClientRect(hwnd,&rectClient); //获取客户区坐标
ClientToScreen(hwnd,LPPOINT(&rectClient));
ClientToScreen(hwnd,LPPOINT(&rectClient)+1);
ClipCursor(&rectClient);
SetCursor(LoadCursor(NULL, IDC_CROSS));
// 初始化新矩形的左上角和右下角坐标
ptLeftTop.x = ptRightBottom.x = GET_X_LPARAM(lParam);
ptLeftTop.y = ptRightBottom.y = GET_Y_LPARAM(lParam);
bStarting = TRUE;
return 0;
case WM_MOUSEMOVE:
if (bStarting)
{
SetCursor(LoadCursor(NULL, IDC_CROSS));
// 先擦除上一次WM_MOUSEMOVE消息所画的矩形,WM_MOUSEMOVE消息会不断产生
DrawFrame(hwnd, ptLeftTop, ptRightBottom);
// 新矩形的右下角坐标
ptRightBottom.x = GET_X_LPARAM(lParam);
ptRightBottom.y = GET_Y_LPARAM(lParam);
// 绘制新矩形
DrawFrame(hwnd, ptLeftTop, ptRightBottom);
}
return 0;
case WM_LBUTTONUP:
if (bStarting)
{
// 擦除WM_MOUSEMOVE消息中所画的最后一个矩形
DrawFrame(hwnd, ptLeftTop, ptRightBottom);
// 最终矩形的右下角坐标
ptRightBottom.x = GET_X_LPARAM(lParam);
ptRightBottom.y = GET_Y_LPARAM(lParam);
SetCursor(LoadCursor(NULL, IDC_ARROW));
bStarting = FALSE;
// ReleaseCapture();
// 绘制最终的矩形
InvalidateRect(hwnd, NULL, TRUE);
}
ClipCursor(NULL);
return 0;
case WM_PAINT:
hdc = BeginPaint(hwnd, &ps);
Rectangle(hdc, ptLeftTop.x, ptLeftTop.y, ptRightBottom.x, ptRightBottom.y);
EndPaint(hwnd, &ps);
return 0;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
不要忘了在WM_LBUTTONUP消息中添加以下代码还鼠标光标自由之身∶
ClipCursor(NULL);
这样的处理方式也很合理。
有一点需要注意,RECT结构有一个特性,就是RECT结构通常不包含右边缘和底边缘,大家可以测试一下,将光标活动区域限制在客户区范围内以后,最右侧和最底部是无法绘制到的。如果需要,则可以在获取客户区的矩形尺寸以后再为其right和bottom字段额外加1。有的函数处理方式则不同,例如FillRect函数是可以填充指定RECT结构的右边缘和底边缘的。
鼠标滚轮
按下或释放鼠标滚轮会发送WM_MBUTTONDOWN或WM_MBUTTONUP消息。
双击鼠标滚轮会发送WM_MBUTTONDBLCLK消息。另外,旋转滚轮会发送WM_MOUSEWHEEL消息。
需要注意的是,WM_MOUSEWHEEL消息是发送给具有输入焦点的窗口,而不是光标位置下面的窗口。
-
WM_MOUSEWHEEL消息的wParam参数的高位字表示本次滚轮旋转的距离,可能是正值或负值。正值表示滚轮向前旋转,远离用户 负值表示滚轮向后旋转,朝向用户。
-
WM_MOUSEWHEEL消息的wParam参数的低位字表示鼠标事件发生时其他鼠标按钮以及Ctrl和Shift键的状态标志,可用的标志和客户区鼠标消息的wParam参数相同。
可以使用以下代码获取wParam参数中的信息∶
fwKeys = GET_KEYSTATE_WPARAM(wparam);
iDistance = GET_WHEEL_DELTA_WPARAM(wParam);
- WM_MOUSEWHEEL消息的lParam参数包含鼠标事件发生时光标热点的X坐标和Y坐标,相对于屏幕左上角。可以使用GET_X_LPARAM(IParam)和GET_Y_LPARAM(IParam)宏分别提取光标热点的X和Y坐标。
还有一个鼠标滚轮水平滚动的WM_MOUSEHWHEEL消息,加了一个H表示Horizontal,其消息参数和WM_MOUSEWHEEL消息的完全相同,在此不再赘述。处理完这两个消息以后应该返回0。
程序可以通过调用SystemParameterslnfo函数获取一个WHEEL_DELTA
值可以滚动的行数∶
UINT uiScrollLines; //在uiScrollLines中返回一个WHEEL_DELTA值所滚动的行数
SystemParametersInfo(SPI_GETWHEELSCROLLLINES,0, &uiScrollLines,0);
滚动行数的默认值为3,用户可以通过控制面板修改默认值。uiScrollLines可能返回0,在这种情况下不应该滚动。
常量WHEEL_DELTA在WinUser.h头文件中定义如下∶
#define WHEEL_DELTA 120
就是说,滚动一行所需的增量为WHEEL_DELTA / uiScrollLines .
假设WM_MOUSEWHEEL消息中wParam参数的高位字值为192,正值表示滚轮向前旋转,远离用户,也就是向上滚动页面,此时应该滚动192/(WHEEL_DELTA/uiScrollLines),默认情况下就是192/40等于向上滚动4行。但是192/40还余32,为了提高用户体验,我们可以把这个32记录下来,加到下一次WM_MOUSEWHEEL消息中wParam参数的高位字上,当然也可以舍弃这个余下的32。
接下来,我们为SystemMetrics4程序添加鼠标滚轮垂直滚动条接口∶
#include <Windows.h>
#include <tchar.h>
#include "Metrics.h"
const int NUMLINES = sizeof(METRICS) / sizeof(METRICS[0]);
// 函数声明,窗口过程
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
WNDCLASSEX wndclass; // RegisterClassEx函数用的WNDCLASSEX结构
TCHAR szClassName[] = TEXT("MyWindow"); // RegisterClassEx函数注册的窗口类的名称
TCHAR szAppName[] = TEXT("GetSystemMetrics"); // 窗口标题
HWND hwnd; // CreateWindowEx函数创建的窗口的句柄
MSG msg; // 消息循环所用的消息结构体
wndclass.cbSize = sizeof(WNDCLASSEX);
wndclass.style = CS_HREDRAW | CS_VREDRAW;
wndclass.lpfnWndProc = WindowProc;
wndclass.cbClsExtra = 0;
wndclass.cbWndExtra = 0;
wndclass.hInstance = hInstance;
wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
wndclass.hbrBackground = (HBRUSH)(COLOR_3DFACE + 1);
wndclass.lpszMenuName = NULL;
wndclass.lpszClassName = szClassName;
wndclass.hIconSm = NULL;
RegisterClassEx(&wndclass);
hwnd = CreateWindowEx(0, szClassName, szAppName, WS_OVERLAPPEDWINDOW | WS_VSCROLL | WS_HSCROLL,
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL);
ShowWindow(hwnd, nCmdShow);
UpdateWindow(hwnd);
while (GetMessage(&msg, NULL, 0, 0) != 0)
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return msg.wParam;
}
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
HDC hdc;
PAINTSTRUCT ps;
TEXTMETRIC tm;
SCROLLINFO si;
HFONT hFont, hFontOld;
static int s_iCol1, s_iCol2, s_iCol3, s_iHeight;// 第1~3列字符串的最大宽度,字符串高度
static int s_cxClient, s_cyClient; // 客户区宽度、高度
static int s_cxChar; // 平均字符宽度,用于水平滚动条滚动单位
int iVertPos, iHorzPos; // 垂直、水平滚动条的当前位置
SIZE size = { 0 };
int x, y;
TCHAR szBuf[10];
int nPaintBeg, nPaintEnd; // 无效区域
//RECT rect;
UINT uiScrollLines;
static int iDistancePerLine; // 滚动一行所需距离
static int iDistanceScroll; // 本次处理WM_MOUSEWHEEL消息需要滚动多少距离
switch (uMsg)
{
case WM_CREATE:
hdc = GetDC(hwnd);
hFont = CreateFont(12, 0, 0, 0, 0, 0, 0, 0, GB2312_CHARSET, 0, 0, 0, 0, TEXT("宋体"));
hFontOld = (HFONT)SelectObject(hdc, hFont);
for (int i = 0; i < NUMLINES; i++)
{
GetTextExtentPoint32(hdc, METRICS[i].m_pLabel, _tcslen(METRICS[i].m_pLabel), &size);
if (size.cx > s_iCol1)
s_iCol1 = size.cx;
GetTextExtentPoint32(hdc, METRICS[i].m_pDesc, _tcslen(METRICS[i].m_pDesc), &size);
if (size.cx > s_iCol2)
s_iCol2 = size.cx;
GetTextExtentPoint32(hdc, szBuf,
wsprintf(szBuf, TEXT("%d"), GetSystemMetrics(METRICS[i].m_nIndex)), &size);
if (size.cx > s_iCol3)
s_iCol3 = size.cx;
}
s_iHeight = size.cy + 2;
GetTextMetrics(hdc, &tm);
s_cxChar = tm.tmAveCharWidth;
//GetClientRect(hwnd, &rect);
//rect.right = s_iCol1 + s_iCol2 + s_iCol3 + GetSystemMetrics(SM_CXVSCROLL);
//AdjustWindowRectEx(&rect, GetWindowLongPtr(hwnd, GWL_STYLE),
// GetMenu(hwnd) != NULL, GetWindowLongPtr(hwnd, GWL_EXSTYLE));
MoveWindow(hwnd, 0, 0, rect.right - rect.left, rect.bottom - rect.top, TRUE);
//SetWindowPos(hwnd, NULL, 0, 0, rect.right - rect.left, rect.bottom - rect.top,
// SWP_NOZORDER | SWP_NOMOVE);
SelectObject(hdc, hFontOld);
DeleteObject(hFont);
ReleaseDC(hwnd, hdc);
SystemParametersInfo(SPI_GETWHEELSCROLLLINES, 0, &uiScrollLines, 0);
if (uiScrollLines != 0)
iDistancePerLine = WHEEL_DELTA / uiScrollLines;
else
iDistancePerLine = 0;
return 0;
case WM_SIZE:
// 客户区宽度、高度
s_cxClient = LOWORD(lParam);
s_cyClient = HIWORD(lParam);
// 设置垂直滚动条的范围和页面大小
si.cbSize = sizeof(SCROLLINFO);
si.fMask = SIF_RANGE | SIF_PAGE;
si.nMin = 0;
si.nMax = NUMLINES - 1;
si.nPage = s_cyClient / s_iHeight;
SetScrollInfo(hwnd, SB_VERT, &si, TRUE);
// 设置水平滚动条的范围和页面大小
si.cbSize = sizeof(SCROLLINFO);
si.fMask = SIF_RANGE | SIF_PAGE;
si.nMin = 0;
si.nMax = (s_iCol1 + s_iCol2 + s_iCol3) / s_cxChar - 1;
si.nPage = s_cxClient / s_cxChar;
SetScrollInfo(hwnd, SB_HORZ, &si, TRUE);
return 0;
case WM_VSCROLL:
si.cbSize = sizeof(SCROLLINFO);
si.fMask = SIF_ALL;
GetScrollInfo(hwnd, SB_VERT, &si);
iVertPos = si.nPos;
switch (LOWORD(wParam))
{
case SB_LINEUP:
si.nPos -= 1;
break;
case SB_LINEDOWN:
si.nPos += 1;
break;
case SB_PAGEUP:
si.nPos -= si.nPage;
break;
case SB_PAGEDOWN:
si.nPos += si.nPage;
break;
case SB_THUMBTRACK:
si.nPos = si.nTrackPos;
break;
case SB_TOP:
si.nPos = 0;
break;
case SB_BOTTOM:
si.nPos = NUMLINES - 1;
break;
}
// 设置位置,然后获取位置,如果si.nPos越界,Windows不会设置
si.cbSize = sizeof(SCROLLINFO);
si.fMask = SIF_POS;
SetScrollInfo(hwnd, SB_VERT, &si, TRUE);
GetScrollInfo(hwnd, SB_VERT, &si);
// 如果Windows更新了滚动条位置,我们更新客户区
if (iVertPos != si.nPos)
{
ScrollWindow(hwnd, 0, s_iHeight * (iVertPos - si.nPos), NULL, NULL);
UpdateWindow(hwnd);
}
return 0;
case WM_HSCROLL:
si.cbSize = sizeof(SCROLLINFO);
si.fMask = SIF_ALL;
GetScrollInfo(hwnd, SB_HORZ, &si);
iHorzPos = si.nPos;
switch (LOWORD(wParam))
{
case SB_LINELEFT:
si.nPos -= 1;
break;
case SB_LINERIGHT:
si.nPos += 1;
break;
case SB_PAGELEFT:
si.nPos -= si.nPage;
break;
case SB_PAGERIGHT:
si.nPos += si.nPage;
break;
case SB_THUMBTRACK:
si.nPos = si.nTrackPos;
break;
}
// 设置位置,然后获取位置,如果si.nPos越界,Windows不会设置
si.cbSize = sizeof(SCROLLINFO);
si.fMask = SIF_POS;
SetScrollInfo(hwnd, SB_HORZ, &si, TRUE);
GetScrollInfo(hwnd, SB_HORZ, &si);
// 如果Windows更新了滚动条位置,我们更新客户区
if (iHorzPos != si.nPos)
{
ScrollWindow(hwnd, s_cxChar * (iHorzPos - si.nPos), 0, NULL, NULL);
UpdateWindow(hwnd);
}
return 0;
case WM_KEYDOWN:
switch (wParam)
{
case VK_UP: // 向上箭头键
SendMessage(hwnd, WM_VSCROLL, SB_LINEUP, 0);
break;
case VK_DOWN: // 向下箭头键
SendMessage(hwnd, WM_VSCROLL, SB_LINEDOWN, 0);
break;
case VK_PRIOR: // PageUp键
SendMessage(hwnd, WM_VSCROLL, SB_PAGEUP, 0);
break;
case VK_NEXT: // PageDown键
SendMessage(hwnd, WM_VSCROLL, SB_PAGEDOWN, 0);
break;
case VK_HOME: // Home键(或者Fn + PageUp键)
SendMessage(hwnd, WM_VSCROLL, SB_TOP, 0);
break;
case VK_END: // End键(或者Fn + PageDown键)
SendMessage(hwnd, WM_VSCROLL, SB_BOTTOM, 0);
break;
case VK_LEFT: // 左箭头键
SendMessage(hwnd, WM_HSCROLL, SB_LINELEFT, 0);
break;
case VK_RIGHT: // 右箭头键
SendMessage(hwnd, WM_HSCROLL, SB_LINERIGHT, 0);
break;
}
return 0;
case WM_MOUSEWHEEL:
if (iDistancePerLine == 0)
return 0;
iDistanceScroll += GET_WHEEL_DELTA_WPARAM(wParam);
// GET_WHEEL_DELTA_WPARAM(wParam)是正数,滚轮向前旋转,远离用户,向上滚动
while (iDistanceScroll >= iDistancePerLine)
{
SendMessage(hwnd, WM_VSCROLL, SB_LINEUP, 0);
iDistanceScroll -= iDistancePerLine;
}
// GET_WHEEL_DELTA_WPARAM(wParam)是负数,滚轮向后旋转,朝向用户,向下滚动
while (iDistanceScroll <= -iDistancePerLine)
{
SendMessage(hwnd, WM_VSCROLL, SB_LINEDOWN, 0);
iDistanceScroll += iDistancePerLine;
}
return 0;
case WM_PAINT:
hdc = BeginPaint(hwnd, &ps);
// 获取垂直滚动条、水平滚动条位置
si.cbSize = sizeof(SCROLLINFO);
si.fMask = SIF_POS | SIF_PAGE;
GetScrollInfo(hwnd, SB_VERT, &si);
iVertPos = si.nPos;
si.cbSize = sizeof(SCROLLINFO);
si.fMask = SIF_POS;
GetScrollInfo(hwnd, SB_HORZ, &si);
iHorzPos = si.nPos;
SetBkMode(hdc, TRANSPARENT);
hFont = CreateFont(12, 0, 0, 0, 0, 0, 0, 0, GB2312_CHARSET, 0, 0, 0, 0, TEXT("宋体"));
hFontOld = (HFONT)SelectObject(hdc, hFont);
// 获取无效区域
nPaintBeg = max(0, iVertPos + ps.rcPaint.top / s_iHeight);
nPaintEnd = min(NUMLINES - 1, iVertPos + ps.rcPaint.bottom / s_iHeight);
for (int i = nPaintBeg; i <= nPaintEnd; i++)
{
x = s_cxChar * (-iHorzPos);
y = s_iHeight * (i - iVertPos);
TextOut(hdc, x, y, METRICS[i].m_pLabel, _tcslen(METRICS[i].m_pLabel));
TextOut(hdc, x + s_iCol1, y, METRICS[i].m_pDesc, _tcslen(METRICS[i].m_pDesc));
TextOut(hdc, x + s_iCol1 + s_iCol2, y, szBuf,
wsprintf(szBuf, TEXT("%d"), GetSystemMetrics(METRICS[i].m_nIndex)));
}
SelectObject(hdc, hFontOld);
DeleteObject(hFont);
EndPaint(hwnd, &ps);
return 0;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
前面说过,滚动行数的默认值为3,用户可以通过控制面板修改其默认值。如果用户更改了系统参数,则系统会向所有顶级窗口广播
WM_SETTINGCHANGE消息。如果希望把程序做得更友好,则应该处理WM_SETTINGCHANGE消息,在该消息中再次调用SystemParametersInfo(SPI_GETWHEELSCROLLLINES,0, &uiScrollLines,0);重新计算滚动一行所需的增量。
模拟鼠标消息
有两种方法可以很方便地实现模拟鼠标消息,一是使用
SendMessage / PostMessage函数发送鼠标消息,二是使用
mouse_event模拟鼠标事件。mouse_event函数合成鼠标移动和鼠标按钮单击事件,原型如下∶
VOID mouse_event(
DWORD dwFlags, //控制鼠标移动和按钮单击的标志位
DWORD dx, //鼠标在X轴的绝对位置或者从上次鼠标事件产生以来移动的相对数量
DWORD dy,//鼠标在Y轴的绝对位置或者从上次鼠标事件产生以来移动的相对数量
DWORD dwData,//根据dwFlags参数的设置,具有不同含义
ULONG_PTR dwExtralnfo);//与鼠标事件关联的附加值,可调用GetMessageExtralnfo以获取此额外信息
微软建议使用SendInput函数代替mouse_event ,但是Sendlnput函数用起来确实有点复杂。
参数dwFlags是控制鼠标移动和按钮单击的标志位,该参数可以是下表所示的值的组合。
<
如果参数dwFlags指定了MOUSEEVENTF_ABSOLUTE标志,那么dx和dy参数为鼠标移动的绝对位置。dx和dy的值指定为0~65535,(0,,0)映射到屏幕的左上角,(65535,65535)映射到屏幕的右下角。例如,如果希望把鼠标光标移动到离屏幕左上角向右200像素,向下100像素处,那么在1920 1080分辨率的计算机上,应该这样调用mouse_event函数,我用按键1进行测试∶
case WM_CHAR:
switch (wParam)
{
case '1':
mouse_event(MOUSEEVENTF_ABSOLUTE| MOUSEEVENTF_MOVE,200*65535/1920,100*65535/1080,0,0);
break;
default:
break;
}
return 0;
如果参数dwFlags没有指定MOUSEEVENTF_ABSOLUTE标志,那么dx和dy表示相对于上次鼠标事件产生的位置的移动量,指定为正值表示鼠标向右(或下)移动,负值表示鼠标向左(或上)移动。但是,相对移动受指针移动速度和加速级别的设置影响,可以通过控制面板修改这些值,也可以通过指定SPI_SETMOUSESPEED或
SPI_SETMOUSE参数调用SystemParameterslnfo函数修改指针移动速度或加速级别。文章来源:https://www.toymoban.com/news/detail-833858.html
假设调用mouse_event(MOUSEEVENTF_MOVE,10,0,0,O);,不一定正好向右移动10像素。文章来源地址https://www.toymoban.com/news/detail-833858.html
到了这里,关于49_输入设备鼠标你得会的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!