
2.9 鼠标
2.9.1 鼠标概述
现在鼠标已是电脑的标配了,以前的鼠标都是PS/2接口的鼠标,现在大都是即插即用的USB鼠标和无线鼠标。鼠标可以说是使用频率最高的设备。常见的鼠标是双键鼠标,中间带有一个滚轮,通过滚轮我们可以迅速地进行翻页。鼠标的属性可以在控制面板里的“鼠标”项下面查看或设置,比如设置左手使用方式还是右手使用方式、设置鼠标双击的速度和鼠标指针移动的速度、指针的图标等等。
在Visual C++程序开发中,系统提供了一个Win32函数GetSystemMetrics来判断鼠标的一些属性,比如判断鼠标是否存在:
BOOL bMouseExist = GetSystemMetrics(SM_MOUSEPRESENT); if(bMouseExist) AfxMessageBox(_T("鼠标存在")); else AfxMessageBox(_T("没有检测到鼠标"));
再比如,判断鼠标上键的个数:
int cn = GetSystemMetrics(SM_CMOUSEBUTTONS);
2.9.2 鼠标消息
用户操作鼠标都会产生鼠标事件,比如按下鼠标的左键或右键、移动鼠标,鼠标事件发生后,系统会向鼠标指针(下面简称鼠标)所在的窗口发送相应的鼠标消息,比如鼠标左键按下消息、鼠标移动消息等。根据鼠标所在窗口的位置不同,鼠标消息通常分为客户区消息和非客户区消息。顾名思义,如果在窗口客户区内发生的鼠标消息就称为鼠标的客户区消息,如果在非客户区内产生的消息就是非客户区消息。
1.鼠标的客户区消息
关于窗口的客户区的概念我们在前面章节已经讲述,这里不再赘述。鼠标的客户区消息一个很重要的用途就是用鼠标绘制图形的时候,都是在窗口的客户区上进行,最典型的应用就是Windows自带的画图程序,它就是在窗口客户区上让用户用鼠标画出各种图形。要实现这一功能,就要对鼠标的客户区消息处理有相当的了解。常见的鼠标客户区消息见下表。

由上表可见客户区内的鼠标消息分的很细,像我们常说的单击鼠标左键,通常包括鼠标左键按下和释放的两个过程。
要处理客户区内的鼠标消息,还需要知道消息发生时的鼠标的一些信息,比如鼠标的坐标位置。客户区鼠标的消息处理函数的参数就是起这样的作用。比如,按下鼠标左键的消息处理函数:
void CWnd::OnLButtonDown(UINT nFlags, CPoint point);
其中参数nFlags表示不同的虚拟键是否被按下。这个参数可以是下列值之一:
● MK_CONTROL:如果CTRL键被按下,则设置此位。
● MK_LBUTTON:如果鼠标左键被按下,则设置此位。
● MK_MBUTTON:如果鼠标中键被按下,则设置此位。
● MK_RBUTTON:如果鼠标右键被按下,则设置此位。
● MK_SHIFT:如果SHIFT键被按下,则设置此位。
参数point表示鼠标相对于当前用户鼠标光标的窗口客户区左上角的坐标。
值得注意的是,鼠标的坐标分为客户区坐标(简称客户坐标,基于客户区原点)和屏幕坐标(基于屏幕左上角)。这两个坐标可以相互转换。了解鼠标坐标对于用鼠标画图形相当重要。屏幕坐标到客户区坐标的转换函数是CWnd::ScreenToClient,该函数声明如下:
void ScreenToClient( LPPOINT lpPoint ) const; void ScreenToClient( LPRECT lpRect ) const;
其中参数lpPoint指向一个CPoint对象或POINT结构变量,它包含了要转换的屏幕点坐标。lpRect指向一个CRect对象或RECT结构变量,它包含了要转换的屏幕矩形坐标。执行完成后,客户坐标由lpPoint或lpRect获得。
客户区坐标到屏幕坐标的转换函数是CWnd::ClientToScreen,该函数声明如下:
void ClientToScreen( LPPOINT lpPoint ) const; void ClientToScreen( LPRECT lpRect ) const;
其中参数lpPoint指向一个POINT结构变量或CPoint对象,它包含了要转换的客户区点坐标。lpRect指向一个RECT结构变量或CRect对象,它包含了要转换的客户区的矩形坐标。执行完成后,屏幕坐标由lpPoint或lpRect获得。
下面我们来看2个简单例子,当鼠标左键按下、释放和双击的时候,跳出一个信息框。第二个例子我们在鼠标移动的时候,实时显示它的客户坐标和屏幕坐标。
【例2.44】 鼠标在客户区内按下左键和双击右键
(1)打开Visual C++ 2013,新建一个单文档工程,工程名是Test。
(2)切换到类视图,单击类CTestView,然后在属性视图里选择“消息”页,找到消息WM_LBUTTONDOWN,然后添加消息处理函数,如图2-96所示。

图2-96
这就是可视化的方式添加按下鼠标左键的消息处理函数。此时会显示代码编辑窗口,并自动定位到函数OnLButtonDown处,在该函数中添加代码如下:
void CTestView::OnLButtonDown(UINT nFlags, CPoint point) { // TODO: 在此添加消息处理程序代码和/或调用默认值 AfxMessageBox(_T("鼠标左键被按下")); CView::OnLButtonDown(nFlags, point); }
再添加鼠标右键双击的消息处理函数,代码如下:
void CTestView::OnRButtonDblClk(UINT nFlags, CPoint point) { // TODO: 在此添加消息处理程序代码和/或调用默认值 AfxMessageBox(_T("鼠标右键被双击")); CView::OnRButtonDblClk(nFlags, point); }
(3)保存工程并运行,运行结果如图2-97所示。
【例2.45】 实时显示鼠标坐标
(1)打开Visual C++ 2013,新建一个单文档工程,工程名是Test。
(2)添加鼠标移动消息处理函数。切换到类视图,单击类CTestView,然后在属性视图里选择“消息”页,找到消息WM_MOUSEMOVE,然后添加消息处理函数,如图2-98所示。

图2-97

图2-98
这就是可视化的方式添加鼠标移动的消息处理函数。此时会显示代码编辑窗口,并自动定位到函数OnMouseMove处,在该函数中添加代码如下:
void CTestView::OnMouseMove(UINT nFlags, CPoint point) { // TODO: 在此添加消息处理程序代码和/或调用默认值 CClientDC dc(this); //定义当前视图窗口客户区的设备描述表 CString str; CPoint ptSrn(point); //定义屏幕坐标,并且把客户坐标point赋值给它 ClientToScreen(&ptSrn); //把客户坐标转换为屏幕坐标 str.Format(_T("鼠标客户坐标(%d, %d),鼠标屏幕坐标(%d, %d) "),point.x, point.y, ptSrn.x, ptSrn.y); //格式化字符串 dc.TextOut(0, 0, str); //在客户区原点处输出结果 CView::OnMouseMove(nFlags, point); }
OnMouseMove函数的参数point就是存放鼠标当前的客户坐标,然后我们定义一个坐标ptSrn,它将存放鼠标屏幕坐标,用point对ptSrn初始化后,就可以利用函数ClientToScreen来获得鼠标屏幕坐标。dc是我们定义的客户区设备描述表,有了它,就可以利用其成员函数TextOut来输出结果。
(3)保存工程并运行,运行结果如图2-99所示。

图2-99
2.鼠标的非客户区消息
非客户区通常指窗口的标题栏、菜单栏、边框和滚动条等区域。正常情况下,不必去处理鼠标的非客户区消息,让系统默认处理即可。但有时候为了实现某些特殊功能,比如当用户单击标题栏上的最大化按钮的时候,不让窗口最大化,这个时候就要处理非客户区消息了。但要注意,在单文档或多文档程序中,鼠标的非客户区消息通常针对框架窗口,因为标题栏、菜单栏等区域都属于框架窗口的非客户区,而视图窗口的客户区一般就包括自己的边框。鼠标的非客户区消息见下表。

可以发现,鼠标非客户区消息的形式就是比客户区消息多了NC。
通过添加鼠标非客户区的消息处理函数能对鼠标的非客户区消息进行处理,而处理消息时所需的信息,可以通过消息处理函数的参数获得,比如在框架窗口的非客户区上按下鼠标左键的消息处理函数:
void CMainFrame::OnNcLButtonDown(UINT nHitTest, CPoint point);
其中参数nHitTest表示鼠标的击中测试码。所谓击中测试就是确定鼠标的位置;参数point表示鼠标的屏幕坐标(注意是屏幕坐标,该坐标基于屏幕左脚上角(0,0))。
非客户区上鼠标常见的几种测试码如下表所示。

下面我们看个例子,让窗口的最大化和关闭按钮失效。
【例2.46】 让框架窗口的最大化和关闭按钮失效
(1)打开Visual C++ 2013,新建一个单文档工程,工程名是Test。
(2)切换到类视图,单击类CMainFrame,然后在属性视图里选择“消息”页,找到消息WM_NCLBUTTONDOWN来添加消息处理函数,并在函数中添加代码如下:
void CMainFrame::OnNcLButtonDown(UINT nHitTest, CPoint point) { // TODO: 在此添加消息处理程序代码和/或调用默认值 if (HTMAXBUTTON == nHitTest) { AfxMessageBox(_T("最大化已经不可用!")); return; } else if (HTCLOSE == nHitTest) { AfxMessageBox(_T("关闭按钮已经不可用,请用菜单文件|退出")); return; } CFrameWnd::OnNcLButtonDown(nHitTest, point); }
程序判断用户是否单击了最大化按钮和关闭按钮,如果是,则直接返回,不去做系统默认处理,即不再调用CFrameWnd::OnNcLButtonDown(nHitTest, point);,这样这两个按钮的系统默认功能就没有了。
(3)保存工程并运行,运行结果如图2-100所示。

图2-100
3.命中测试消息
喜欢刨根问底的朋友或许想知道,系统怎么知道鼠标消息是发生在客户区还是非客户区?答案是在这两类消息发送之前,系统会先发送WM_HITTEST消息,来判断当前鼠标事件发生在窗口的哪个部位?是客户区还是非客户区?我们有必要了解下消息WM_HITTEST。
当鼠标在窗口上移动或按键时,系统首先将发送命中测试消息WM_HITTEST给窗口。通过该消息,可以知道鼠标当前所在的窗口部位,也称命中哪个部位,该消息也叫命中测试消息。该消息的消息处理函数的返回值指明了窗口的部位,比如HTCAPTION表示鼠标在窗口的标题栏,HTCLIENT表示鼠标在窗口的客户区。
前面提到,在鼠标客户区消息或非客户区消息产生之前会先发送出WM_HITTEST消息,然后根据WM_HITTEST的消息处理函数的返回值来决定在哪个部位,继而再发送哪种类型的鼠标消息,我们来看一下鼠标键按下时的系统处理过程:
第一步,确定鼠标键单击的是哪个窗口。Windows会用表记录当前屏幕上各个窗口的区域坐标,当鼠标驱动程序通知Windows鼠标键按下了,Windows根据鼠标的坐标确定它单击的是哪个窗口。
第二步,确定鼠标键单击的是窗口的哪个部位。Windows会向鼠标键单击的窗口发送WM_NCHITTEST消息,来询问鼠标键单击的是窗口的哪个部位。(WM_NCHITTEST的消息响应函数的返回值会通知Windows)。通常来说,WM_NCHITTEST消息是系统来处理的,用户一般不会主动去处理它,也就是说,WM_NCHITTEST的消息响应函数通常采用的是Windows默认的处理函数。
第三步,根据鼠标键单击的部位给窗口发送相应的消息(例如客户区消息或非客户区消息)。例如:如果WM_NCHITTEST的消息处理函数的返回值是HTCLIENT,表示鼠标单击的是客户区,则Windows会向窗口发送WM_LBUTTONDOWN消息;如果WM_NCHITTEST的消息处理函数的返回值不是HTCLIENT(可能是HTCAPTION、HTCLOSE、HTMAXBUTTON等),即鼠标单击的是非客户区,Windows就会向窗口发送WM_NCLBUTTONDOWN消息。
下面我们来看一下WM_HITTEST的消息处理函数CWnd::OnNcHitTest,其声明如下:
afx_msg UINT OnNcHitTest( CPoint point );
其中,参数point包含了鼠标所在位置的屏幕坐标。返回值表示鼠标所在窗口的部位,也叫鼠标击中测试码,取值如下表所示。

【例2.47】 通过菜单栏来拖动窗口
(1)新建一个单文档工程。
(2)切换到类视图,为类CMainFrame添加WM_NCHITTEST的消息处理函数,代码如下:
LRESULT CMainFrame::OnNcHitTest(CPoint point) { // TODO: 在此添加消息处理程序代码和/或调用默认值 UINT nHitTest = CFrameWnd::OnNcHitTest(point); if (nHitTest == HTMENU) //如果用户单击了菜单,则准备返回标题栏 nHitTest = HTCAPTION; return nHitTest; }
(3)保存工程并运行,运行结果如图2-101所示。

图2-101