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的消息
其中双击信息许哟啊在创建窗口实例的时候指明风格使用
1 | wndclass.style=CS_DBLCLKS | ... |
带有CS_DBLCLKS风格的窗口才可以接收WM_LBUTTONDBLCLK这种双击消息
对于鼠标消息(hwnd,message,wParam,lParam)
lParam
包含鼠标的位置信息,低字表示x坐标,高字表示y坐标
1 | x=LOWORD(lParam); |
wParam
包含了鼠标哪个键,还有此时Ctrl和Shift的状态.
令wParam和宏定义按位与即可测试相应状态
1 |
MOUSEMOVE的速度
windows不会给鼠标经过的每个像素点都产生一个WM_MOUSEMOVE消息,这取决于应用程序处理WM_MOUSEMOVE的速度,当应用程序的消息队列中还有WM_MOUSEMOVE的消息时就不能接收第二个WM_MOUSEMOVE消息
书上在此给出了一个例子.
凡是WM_MOUSEMOVE捕获的点都会被计入点集,点集中任意两个点连一条线
分析一下其过程函数WndProc
变量定义
1 | static POINT pt[MAXPOINTS];//存储已经捕获的点集 |
鼠标信息处理
1 | case WM_LBUTTONDOWN: |
绘图消息
1 | case WM_PAINT: |
窗口销毁消息
1 | case WM_DESTROY: |
效果
双击
只有创建窗口实例时,风格上允许双击的窗口才可以接受双击信息
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
非客户区鼠标消息
非客户区的消息在WM_前缀之后又加了一个NC前缀(not client)
参数意义
lParam
低字为x坐标,高字为y坐标,此处的坐标是相对于整个屏幕的坐标
而客户区的鼠标信息中lParam携带的坐标是相对于客户区左上角的坐标
屏幕坐标和客户区坐标的互换:
1 | BOOL ScreenToClient( |
白刀子进,红刀子出
使用pt带着原坐标进去,带着转换坐标出来
wParam
非客户区鼠标移动或者单击的位置(不是坐标),一个标识符
击中测试
WM_NCHITTEST
关于鼠标的最后一个消息类型
这个消息优先级高于所有客户区和非客户区的鼠标消息
lParam
鼠标位置的屏幕坐标
wParam
没有用到
这条消息应该被直接传递给DefWindowProc,操作系统负责将屏幕坐标翻译为客户区坐标之后,产生一个客户区鼠标消息发送给应用程序
那么如果捕获该消息并且不让他传递给DefWindowProc,就阻断了所有鼠标消息.所有本窗口的鼠标动作将失效
什么是击中测试
在文件浏览器中双击某个文件时,文件浏览器会进入该目录或者打开该文件.
可是文件浏览器是怎么知道应该打开哪个文件的呢?
他需要获取鼠标位置然后判断这个位置落在哪里
考虑实现一个简单的文件浏览器,以列表形式列出当前目录下的所有文件和子文件夹
每个文件占一行,放不下就用滚动条
客户区的点击动作就需要将纵坐标换算为行数,再根据卷动情况判断是指向的哪一行
根据确定的行号作为下标查文件名表,查到之后打开该文件,如果是文件夹则打开该文件夹
击中测试例程
书上在此给出了一个击中测试的例程,分析其过程函数WndProc
变量定义
1 |
|
尺寸变化消息
1 | case WM_SIZE: |
尽量用整个客户区打印所有方格
左键单击消息
1 | case WM_LBUTTONDOWN: |
绘图消息
1 | case WM_PAINT: |
键盘模仿鼠标
使用方向键移动鼠标光标.使用Enter确认按下
这样即使计算机没有连接鼠标也能使用键盘模拟鼠标动作
比如windows桌面上如果有一个图标是高亮的,那么右方向键会使同行右侧的图标高亮,可以用隐藏光标,然后捕获位置进行击中测试,决定高亮哪个图标.
显示计数
书上扯了大半天,实际上就说了一个有用的东西
1 | int ShowCursor( |
当bShow为False则不显示鼠标光标
当bShow为True则显示鼠标光标
指针位置
不管有没有接鼠标,鼠标指针都是存在的,一般开机时位于屏幕正中间.即使不使用鼠标移动指针位置,也可以使用键盘做到
1 | BOOL GetCursorPos( |
两个函数中涉及到的坐标都是屏幕坐标,如果需要客户区坐标,可以使用坐标转换函数
1 | BOOL ScreenToClient( |
使用GetCursorPos并且用ScreenToClient转换得到的指针位置和鼠标消息中的指针位置不同
前者是啥时候调用函数啥时候取得指针位置,后者指针位置是产生该条消息时指针的位置
击中测试
变量定义
1 |
|
窗口焦点信息
1 | case WM_SETFOCUS://获得焦点 |
当窗口获得焦点的时候显示光标,失去焦点的时候隐藏光标
尺寸调整信息
1 | case WM_SIZE://客户区重新计算块大小 |
当客户区尺寸发生变化的时候调整区块的大小
使得5*5个区块尽量占满整个客户区
虚拟键信息
1 | case WM_KEYDOWN://虚拟键按下 |
使用子窗口
每个子窗口都有自己的句柄,客户区,窗口过程函数.
多个子窗口将整个客户区划分成几个小的矩形区域
对于子窗口的鼠标消息,lParam参数包含相对于该子窗口左上角的坐标
本来我们的程序中使用了一个fState[DIVISIONS][DIVISIONS]
二维数组保存每个区块的状态,整个程序就一个窗口过程,它遍历打印每个窗口的状态.
现在使用子窗口,使得每个区块成为一个子窗口,每个子窗口自己处理发生在自己身上的鼠标键盘动作
注册父窗口类
1 | static TCHAR szAppName[] = TEXT("Checker4");//将要作为父窗口类名 |
注册子窗口类
1 | wndclass.lpfnWndProc = ChildWndProc;//修改一下wndclass的部分信息,填写子窗口类信息 |
此处注册子窗口类,但是并不在winmain函数中创建实例,而是当父窗口起来之后,在其WM_CREATE消息处理中创建25个szChildClass指向的子窗口实例
创建父窗口实例,显示父窗口消息循环
1 | hwnd = CreateWindow(szAppName, TEXT("Checker4 Mouse Hit-Test Demo"), |
父窗口过程WndProc
变量定义
1 | static HWND hwndChild[DIVISIONS][DIVISIONS];//子窗口句柄数组,hwndChild[x][y]为第x行第y列子窗口的句柄 |
区分HWND句柄和HINSTANCE句柄
整个win32程序只有一个引用程序句柄HINSTANCE,
着一个程序可以有很多个窗口,每个窗口都有一个独一无二的窗口句柄HWND
窗口创建消息
1 | case WM_CREATE://主窗口创建消息,此时为创建子窗口的最佳时机 |
CreateWindow
创建父窗口实例时也使用了该函数,返回值是一个窗口实例的句柄
创建并显示窗口三个过程:
RegisterClass注册窗口类,该窗口类的类名作为句柄
CreateWindow创建窗口实例,可以使用刚才的窗口类名填充窗口的基本信息
ShowWindow显示窗口类
1 | HWND CreateWindowA( |
调整尺寸消息
1 | case WM_SIZE: |
MoveWindow
对于子窗口来说,该函数中指定的坐标都是相对于父窗口客户区左上角的
对于非子窗口来说,该函数中的坐标是屏幕坐标
1 | BOOL MoveWindow( |
左键按下消息
1 | case WM_LBUTTONDOWN: |
理论上25个子窗口尽量铺满父窗口的客户区,但是父窗口客户区的最右边和最下边可能有留白,因此当鼠标点击到这些地方的时候父窗口就会接到WM_LBUTTONDOWN消息
父窗口对这种消息的处理是发出一条蜂鸣声实际上是一个wav波形文件,还有一些系统定义好了的蜂鸣声,作用不大不展开了
获得焦点消息
1 | case WM_SETFOCUS://父窗口获取焦点之后通知它上次获得焦点的子窗口继续获得焦点 |
父窗口获得焦点之后,应该把最后一次获得焦点的子窗口作为焦点窗口
但是处理本消息时并没有体现获得idFocus,原因是该全局变量idFocus在WM_KEYDOWN中更新,显然没有按下键盘,通过鼠标点选也可以获得焦点,获得焦点的时候就需要指定让子窗口获得焦点
而现在父窗口掌握着25个子窗口句柄,应该怎么把焦点交给其中之一的子窗口呢?
1 | HWND GetDlgItem( |
这个函数的作用是,返回父窗口的一个子窗口的句柄
这里nIDDlgItem这个值是父窗口注册子窗口是在CreateWindow函数的hMenu上指定的,其中第x行第y列的子窗口是这样索引的
1 | (HMENU)(y << 8 | x) |
既然要获取子窗口的句柄,父窗口不是实例化子窗口时就维护了一个子窗口句柄数组吗?
为啥还要额外维护一个值托管这个句柄呢?
只有一个子窗口句柄数组不能知道最近获得焦点的子窗口是谁,因此idFocus就起到了一个寄存器的作用
虚拟键消息
1 | case WM_KEYDOWN: |
这里计算x,y坐标的方式根HMENU参数的定义方式相反,互为逆运算
1 | x = idFocus & 0xFF;//x保存原来的焦点位置 |
WM_KEYDOWN
消息执行完毕后立刻设置当前子窗口为焦点窗口
窗口销毁消息
1 | case WM_DESTROY: |
子窗口过程ChildWndProc
子窗口创建消息
1 | case WM_CREATE: |
修改窗口的一个属性
1 | LONG SetWindowLongA( |
例子中修改的是下标为0的属性,对应的宏定义是DWL_MSGRESULT(0)
设置对话框过程的返回值,设置成了0.
实际上子窗口也不需要把这个值返回给父窗口看,它自己就可以决定把自己绘制成什么颜色
因此只是借用了一个线程的窗口属性来放置自己应该是按下还是起来的状态
子窗口键鼠消息
由于父窗口中会主动将焦点下放到子窗口,因此焦点子窗口可获取键鼠的输入.
父窗口只能等子窗口吃完了然后吃剩下的
1 | case WM_KEYDOWN: |
这里子窗口就处理里两个虚拟键消息,空格和回车,其他的消息通过
1 | SendMessage(GetParent(hwnd), message, wParam, lParam); |
转发给父窗口
空格,回车,左键单击一视同仁,首先本子窗口的属性置反
然后设置本窗口为焦点窗口
然后使窗口无效,导致重绘
子窗口获取/失去焦点消息
1 | case WM_SETFOCUS: |
GetWindowLong 获取窗口属性
由于窗口属性都是LONG类型的值,因此该函数取名"GetWindowLong"
1 | LONG GetWindowLongA( |
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中获得焦点的子窗口会多绘制一些提示信息,那么当它失去焦点的时候就得擦除提示信息了
子窗口绘图消息
1 | case WM_PAINT: |
捕获鼠标
书上举了一个例子,一个绘图程序,假如要绘制一个矩形,鼠标左键按下之后确定矩形一个点,不松开拖着鼠标移动则实时绘制矩形边框,(好像这个边框叫做橡皮线)
当鼠标左键松开时确定整个矩形,此时填充矩形表明完成绘制
例程
绘制边框橡皮线函数
1 | void DrawBoxOutline(HWND hwnd, POINT ptBeg, POINT ptEnd)//正常情况下客户区内松开左键时调用,填充矩形 |
ptBeg和ptEnd都是相对于hwnd的客户区而言的,意思是在hwnd中绘制一个左上角ptBeg到右下角的ptEnd矩形边框
SetROP2
1 | int SetROP2( |
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 | case WM_LBUTTONDOWN: |
左键按下时就已经开始绘图了,首先绘制一个点,即左键按下时的鼠标位置,
fBlocking变量用来记录绘图过程中有没有被Esc打断过.
SetCapture(hwnd)意思是从调用该函数开始,所有鼠标动作,包括不在本客户区的鼠标动作,全都被本程序捕获.这种状态需要到ReleaseCapture解出
Set/ReleaseCapture
1 | SetCapture(hwnd);//此函数执行之后所有的鼠标动作将被hwnd指向的窗口捕获 |
鼠标移动消息
1 | case WM_MOUSEMOVE: |
由于左键起来之后会进行结算,因此绘图时的鼠标移动是压着左键移动的
首先判断了fBlocking的状态,如果被Esc打断则直接退出,不做处理
这里调用了两次DrawBoxOutline
,其作用是:
由于SetROP2(hdc, R2_NOT)
这个设置,会导致相邻两次绘图使用的颜色相反
如果本次使用黑色,那么下一次就使用白色
这样交替绘制的意义是:刚用黑色绘制出边框,接着逻辑上擦除它,但是在屏幕上不显示擦除,这就是压着黑笔停下时仍然能看到过期的客户区上有边框线.当画笔再次移动时,先前的边框已经被逻辑擦除,此时再画新线保证只有新线,不会有重影.这就实现了橡皮线的效果
这里两次调用DrawBoxOutline
,头一次的ptEnd没有被修改,也就是上一次绘图使用的ptEnd,那么重绘这个矩形,相当于擦除了上一次的绘制
左键起来消息
1 | case WM_LBUTTONUP: |
首先判断fBlocking状态,如果中途被Esc打断过则不做处理
调用一次DrawBoxOutline
擦除最后一次WM_MOUSEMOVE
留下的边框.但是最后一次WM_MOUSEMOVE和WM_LBUTTONUP的到达时间非常接近,拉不开差距,因此这个函数调用与否意义不大,除非电脑很卡
释放对鼠标的捕获状态,程序对于客户区以外的鼠标动作不再处理
鼠标样式还原为斜向箭头
重置中断标志,设置绘图标志有效,提醒WM_PAINT应该绘制填充矩形了
Esc中断消息
1 | case WM_CHAR: |
对于该消息,例子采用的是处理字符消息,而不是处理虚拟键消息
如果出现Esc消息并且fBlocking表明正在绘图,那么设置fBlocking绘图无效
此后由于WM_MOUSEMOVE需要判断fBlocking正在绘图才继续绘制边框,因此Esc出现之后矩形边框橡皮线立刻消失
然而这种绘制失效的状态需要等到左键起来才能完全恢复
WM_LBUTTONUP也会先判断fBlocking是否还有效.无效则啥也不处理
此时所有标志都恢复原状(fValidBox压根没有被改变过,fBlocking被Esc重置为假)
绘图消息
1 | case WM_PAINT: |
fValidBox是LBUTTONUP准备好的绘制标志,如果绘画中途没有Esc中断则LBUTTONUP消息处理中,会把fValidBox置有效,提醒WM_PAINT应该画图了
修改画刷为黑色画刷然后填充最后确定的矩形
如果fBlocking为有效说明仍然没有确定矩形的另一个点,此时WM_PAINT也打印矩形边框橡皮线,然而是多次一句,因为WM_MOUSEMOVE已经把这件事干了.
窗口销毁消息
1 | case WM_DESTROY: |
鼠标滚动
鼠标滚动消息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 | static int iDeltaPerLine, iAccumDelta; |
iDeltaPerLine即单位行增量
iAccumDelta表示先前的滚动效果的累加,可以理解为初始滚动量
ulScrollLines存放灵敏度系数
滚动消息
1 | case WM_MOUSEWHEEL: |