第四章:键盘、鼠标基础
一:键盘基础
Windows程序获得键盘输入的方式:键盘输入以消息的形式传递给程序的窗口过程。
Windows用8种不同的消息来传递不同的键盘事件,但所有的键盘事件不是都需要我们进行处理,我们只对需要进行处理的事件进行处理,而不需要处理的事件可以将其忽略掉,交给Windows进行默认的处理。
1:焦点
键盘必须对Windows下运行的所有应用程序共享,有些应用程序可能有多个窗口,键盘必须由该应用程序内的所有窗口共享.
而当用户按下键盘时,只有一个程序接受键盘消息,而此时接受特定键盘事件的窗口具有输入焦点.
2:击键和字符
应用程序从Windows接收的关于键盘事件的讯息可以分为按键和字符两类,这与您看待键盘的两种方式一致。
首先,您可以将键盘看作是键的集合。键盘只有唯一的A键,按下该键是一次按键,释放该键也是一次按键。但是键盘也是能产生可显示字符或控制字符的输入设备。根据Ctrl、 Shift和Caps Lock键的状态,A键能产生几个字符。通常情况下,此字符为小写a。如果按下Shift键或者打开了Caps Lock,则该字符就变成大写A。如果按下了Ctrl,则该字符为Ctrl-A(它在ASCII中有意义,但在Windows中可能是某事件的键盘加速键)。在一些键盘上,A按键之前可能有「死字符键(dead-character key)」或者Shift、Ctrl或者Alt的不同组合,这些组合可以产生带有音调标记的小写或者大写,例如,à、á、â、Ä、或 Å。
对产生可显示字符的按键组合,Windows不仅给程序发送按键讯息,而且还发送字符讯息。有些键不产生字元,这些键包括shift键、功能键、游标移动键和特殊字符键如Insert和Delete。对于这些键,Windows只产生按键讯息。
二:按键消息
您按下一个键时,Windows把WM_KEYDOWN或者WM_SYSKEYDOWN讯息放入有输入焦点的窗口的讯息队列;当您释放一个键时,Windows把WM_KEYUP或者WM_SYSKEYUP讯息放入讯息队列中, 通常「down(按下)」和「up(放开)」讯息是成对出现的。
1: 系统键与非系统键
WM_SYSKEYDOWN和WM_SYSKEYUP中的「SYS」代表「系统」,它表示该按键对Windows比对Windows应用程式更加重要。WM_SYSKEYDOWN和WM_SYSKEYUP讯息经常由与Alt相组合的按键产生,这些按键启动程式功能表或者系统功能表上的选项,或者用於切换活动视窗等系统功能(Alt-Tab或者Alt-Esc),也可以用作系统功能表加速键(Alt键与一个功能键相结合,例如Alt-F4用於关闭应用程式)。
WM_KEYDOWN和WM_KEYUP讯息通常是在按下或者释放不带Alt键的键时产生的。
对所有四类按键讯息,wParam是虚拟键代码,表示按下或释放的键,而lParam则包含属於按键的其他资料。
2: 虚拟键码
虚拟键码保存在WM_KEYDOWN、WM_KEYUP、WM_SYSKEYDOWN和WM_SYSKEYUP讯息的wParam参数中。此代码标识按下或释放的键。
常用虚拟键码列表:
VK_CANCEL Ctrl-Break
VK_BACK Backspace
VK_TAB Tab
VK_CLEAR Num Lock关闭时的数字键盘5
VK_RETURN Enter
VK_SHIFT Shift
VK_CONTROL Ctrl
VK_PAUSE Pause
VK_ALT Alt
VK_CAPITAL Caps Lock
VK_ESCAPE Esc
VK_SPACE Spacebar
VK_ PRIOR Page Up
VK_ NEXT Page Down
VK_ END End
VK_ HOME Home
VK_ LEFT 左箭头
VK_ UP 上箭头
VK_ RIGHT 右箭头
VK_ DOWN 下箭头
VK_ VK_SNAPSHOT Print Screen
VK_ INSERT Insert
VK_ DELETE Delete
VK_F1到VK_F10 功能键F1到F10
VK_ NUMLOCK Num Lock
VK_ SCROLL Scroll Lock
3: 换档状态
在处理按键讯息时,您可能需要知道是否按下了位移键(Shift、Ctrl和Alt)或开关键(Caps Lock、Num Lock和Scroll Lock)。通过呼叫GetKeyState函式,您就能获得此资讯。例如:
iState = GetKeyState (VK_SHIFT) ;
如果按下了Shift,则iState值为负(即设定了最高位置位元)。如果Caps Lock键打开,则从iState = GetKeyState (VK_CAPITAL) ;传回的值低位元被设为1。此位元与键盘上的小灯保持一致。
4: 按键消息示例
case WM_KEYDOWN:
switch (wParam)
{
case VK_HOME:
MessageBox(NULL,"Home Key be press", "Info",MB_OK);
break ;
case VK_END:
MessageBox(NULL,"End Key be press", "Info",MB_OK);
break ;
case VK_PRIOR:
MessageBox(NULL,"Prior Key be press", "Info",MB_OK);
Break
}
三: 字符消息
字符讯息可以分为四类
| 字 符 | 死 字 符 |
非系统字符 | WM_CHAR | WM_DEADCHAR |
系统字符 | WM_SYSCHAR | WM_SYSDEADCHAR |
WM_CHAR和WM_DEADCHAR讯息是从WM_KEYDOWN得到的;而WM_SYSCHAR和WM_SYSDEADCHAR讯息是从WM_SYSKEYDOWN讯息得到的。
在大多数情况下,Windows程序会忽略除WM_CHAR之外的任何讯息。伴随四个字符讯息的lParam参数与产生字元代码讯息的按键讯息之lParam参数相同。不过,参数wParam不是虚拟键码。实际上,它是ANSI或Unicode字符代码。
这些字符讯息是我们将文字传递给窗口讯息处理程序时遇到的第一个讯息。它们不是唯一的讯息,其它讯息伴随以0结尾的整个字符串。
1: 讯息顺序
因为TranslateMessage函式从WM_KEYDOWN和WM_SYSKEYDOWN讯息产生了字符讯息,所以字符讯息是夹在按键讯息之间传递给窗口讯息处理程序的。例如,如果Caps Lock未打开,而使用者按下再释放A键,则窗口讯息处理程序将接收到三个讯息:
讯 息 | 按 键 或 者 代 码 |
WM_KEYDOWN | 「A」的虚拟键码(0x41) |
WM_CHAR | 「a」的字符代码(0x61) |
WM_KEYUP | 「A」的虚拟键码(0x41) |
如果您按下Shift键,再按下A键,然后释放A键,再释放Shift键,就会输入大写的A,而窗口讯息处理程序会接收到五个讯息:
讯 息 | 按 键 或 者 代 码 |
WM_KEYDOWN | 虚拟键码VK_SHIFT (0x10) |
WM_KEYDOWN | 「A」的虚拟键码(0x41) |
WM_CHAR | 「A」的字符代码(0x41) |
WM_KEYUP | 「A」的虚拟键码(0x41) |
WM_KEYUP | 虚拟键码VK_SHIFT(0x10) |
Shift键本身不产生字元讯息。
如果使用者按住A键,以使自动重复产生一系列的按键,那么对每条WM_KEYDOWN讯息,都会得到一条字符讯息:
讯 息 | 按 键 或 者 代 码 |
WM_KEYDOWN | 「A」的虚拟键码(0x41) |
WM_CHAR | 「a」的字符代码(0x61) |
WM_KEYDOWN | 「A」的虚拟键码(0x41) |
WM_CHAR | 「a」的字符代码(0x61) |
WM_KEYDOWN | 「A」的虚拟键码(0x41) |
WM_CHAR | 「a」的字符代码(0x61) |
WM_KEYDOWN | 「A」的虚拟键码(0x41) |
WM_CHAR | 「a」的字符代码(0x61) |
WM_KEYUP | 「A」的虚拟键码(0x41) |
如果某些WM_KEYDOWN讯息的重复计数大于1,那么相应的WM_CHAR讯息将具有同样的重复计数。
有时Windows程序将Ctrl与字母键的组合用作菜单加速键,此时,不会将字母键转换成字符讯息。
2: 处理控制字符
处理按键和字符讯息的基本规则是:如果需要读取输入到窗口的键盘字符,那么您可以处理WM_CHAR讯息。如果需要读取光标键、功能键、Delete、Insert、Shift、Ctrl以及Alt键,那么您可以处理WM_KEYDOWN讯息。
但是Tab、Enter、Backspace和Escape键,传统上,这些键都产生ASCII控制字符。但是在Windows中,它们也产生虚拟键码。下面给出一种方法处理:将Tab、Enter、Backspace和Escape键处理成控制字符,而不是虚拟键。通常这样处理WM_CHAR:
case WM_CHAR:
switch (wParam)
{
case '\b': // backspace
…
break ;
case '\t': // tab
…
break ;
case '\n': // linefeed
…
break ;
case '\r': // carriage return
…
break ;
default: // character codes
…
break ;
}
return 0 ;
3: 死字符讯息
Windows程序经常忽略WM_DEADCHAR和WM_SYSDEADCHAR讯息,但您应该明确地知道死字符是什么,以及它们工作的方式。
在某些非U.S.英语键盘上,有些键用于给字母加上音调。因为它们本身不产生字元,所以称之为「死键」。例如,使用德语键盘时,对于U.S.键盘上的+/=键,德语键盘的对应位置就是一个死键,未按下Shift键时它用于标识锐音,按下Shift键时则用于标识抑音。
当使用者按下这个死键时,窗口讯息处理程序接收到一个wParam等于音调本身的ASCII或者Unicode代码的WM_DEADCHAR讯息。当使用者再按下可以带有此音调的字母键(例如A键)时,窗口讯息处理程序会接收到WM_CHAR讯息,其中wParam等于带有音调的字母「a」的ANSI代码。
因此,使用者程序不需要处理WM_DEADCHAR讯息,原因是WM_CHAR讯息已含有程序所需要的所有信息。Windows的做法甚至还设计了内部错误处理。如果在死键之后跟有不能带此音调符号的字母(例如「s」),那么窗口讯息处理程序将在一行接收到两条WM_CHAR讯息-前一个讯息的wParam等于音调符号本身的ASCII代码(与传递到WM_DEADCHAR讯息的wParam值相同),第二个讯息的wParam等于字母s的ASCII代码。
四:鼠标基础
当Windows使用者移动鼠标时,Windows在显示器上移动一个称为「鼠标光标」的小位图。鼠标光标有一个指向显示器上精确位置的单图素「热点」。当我提到鼠标光标在屏幕上的位置时,指的是热点的位置。
Windows支持几种预先定义的鼠标光标,程序可以使用这些光标。最常见的是称为IDC_ARROW的斜箭头(在WINUSER.H中定义)。热点在箭头的顶端。IDC_CROSS光标的热点在十字交叉线的中心。 IDC_WAIT光标是一个沙漏,通常用于指示程序正在执行。程序写作者也可以设计自己的光标,在定义窗口类别结构时指定特定窗口的内定光标,例如:
wndclass.hCursor = LoadCursor (NULL, IDC_ARROW);
1:客户区鼠标消息
Windows只把键盘讯息发送给拥有输入焦点的窗口。鼠标讯息与此不同:只要鼠标跨越窗口或者在某窗口中按下鼠标按键,那么窗口讯息处理程序就会收到鼠标讯息,而不管该窗口是否活动或者是否拥有输入焦点。Windows为鼠标定义了21种讯息,不过,其中有11个讯息和显示区域无关(下面称之为「非显示区域」讯息),Windows程序经常忽略这些讯息。
当鼠标移过窗口的显示区域时,窗口讯息处理程序收到WM_MOUSEMOVE讯息。当在窗口的显示区域中按下或者释放一个鼠标按键时,窗口讯息处理程序会接收到下面这些讯息:
键 | 按 下 | 释 放 | 按下(双键) |
左 | WM_LBUTTONDOWN | WM_LBUTTONUP | WM_LBUTTONDBLCLK |
中 | WM_MBUTTONDOWN | WM_MBUTTONUP | WM_MBUTTONDBLCLK |
右 | WM_RBUTTONDOWN | WM_RBUTTONUP | WM_RBUTTONDBLCLK |
只有对三键鼠标,窗口讯息处理程序才会收到MBUTTON讯息;只有对双键或者三键鼠标,才会接收到RBUTTON讯息。只有当定义的窗口类别能接收DBLCLK(双击)讯息,窗口讯息处理程序才能接收到这些讯息。
对于所有这些讯息来说,其lParam值均含有鼠标的位置:低字组为x坐标,高字组为y坐标,这两个坐标是相对于窗口显示区域左上角的位置。您可以用LOWORD和HIWORD宏来提取这些值:
x = LOWORD (lParam) ;
y = HIWORD (lParam) ;
wParam的值指示鼠标按键以及Shift和Ctrl键的状态。您可以使用表头档案WINUSER.H中定义的位屏蔽来测试wParam。MK前缀代表「鼠标按键」。
MK_LBUTTON | 按下左键 |
MK_MBUTTON | 按下中键 |
MK_RBUTTON | 按下右键 |
MK_SHIFT | 按下Shift键 |
MK_CONTROL | 按下Ctrl键 |
例如,如果收到了WM_LBUTTONDOWN讯息,而且值wparam & MK_SHIFT是TRUE(非0),您就知道当左键按下时也按下了Shift键。
当您把鼠标移过窗口的显示区域时,Windows并不为鼠标的每个可能的图素位置都产生一个WM_MOUSEMOVE讯息。您的程序接收到WM_MOUSEMOVE讯息的次数,依赖于鼠标硬件,以及您的窗口讯息处理程序在处理鼠标移动讯息时的速度。换句话说,Windows不能用未处理的WM_MOUSEMOVE讯息来填入讯息队列。
如果您在非活动窗口的显示区域中按下鼠标左键,Windows将把活动窗口改为在其中按下鼠标按键的窗口,然后把WM_LBUTTONDOWN讯息送到该窗口讯息处理程序。当窗口讯息处理程序得到WM_LBUTTONDOWN讯息时,您的程序就可以安全地假定该窗口是活动化的了。不过,您的窗口讯息处理程序可能在未接收到WM_LBUTTONDOWN讯息的情况下先接收到了WM_LBUTTONUP的讯息。如果在一个窗口中按下鼠标按键,然后移动到使用者窗口释放它,就会出现这种情况。类似的情况,当鼠标按键在另一个窗口中被释放时,窗口讯息处理程序只能接收到WM_LBUTTONDOWN讯息,而没有相应的WM_LBUTTONUP讯息。
这些规则有两个例外:
窗口讯息处理程序可以「拦截鼠标」并且连续地接收鼠标讯息,即使此时鼠标在该窗口显示区域之外。
如果正在显示一个系统模态消息框或者系统模态对话框,那么其它程序就不能接收鼠标讯息。当系统模态消息框或者对话框活动时,禁止切换到其它窗口或者程序。
一个例子:
switch (message)
{
case WM_LBUTTONDOWN:
iCount = 0 ;
InvalidateRect (hwnd, NULL, TRUE) ;
return 0 ;
case WM_MOUSEMOVE:
if (wParam & MK_LBUTTON && iCount < 1000)
{
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) ;
return 0 ;
}
2:非客户区鼠标消息
在视窗的显示区域内移动或按下滑鼠按键时,将产生10种讯息。如果滑鼠在视窗的显示区域之外但还在视窗内,Windows就给视窗讯息处理程式发送一条「非显示区域」滑鼠讯息。视窗非显示区域包括标题列、功能表和视窗卷动列。
通常,您不需要处理非显示区域滑鼠讯息,而是将这些讯息传给DefWindowProc,从而使Windows执行系统功能。就这方面来说,非显示区域滑鼠讯息类似於系统键盘讯息WM_SYSKEYDOWN、WM_SYSKEYUP和WM_SYSCHAR。
非显示区域滑鼠讯息几乎完全与显示区域滑鼠讯息相对应。讯息中含有字母「NC」以表示是非显示区域讯息。如果滑鼠在视窗的非显示区域中移动,那么视窗讯息处理程式会接收到WM_NCMOUSEMOVE讯息。滑鼠按键产生下表所示的讯息。
键 | 按 下 | 释 放 | 按下(双击) |
左 | WM_NCLBUTTONDOWN | WM_NCLBUTTONUP | WM_NCLBUTTONDBLCLK |
中 | WM_NCMBUTTONDOWN | WM_NCMBUTTONUP | WM_NCMBUTTONDBLCLK |
右 | WM_NCRBUTTONDOWN | WM_NCRBUTTONUP | WM_NCRBUTTONDBLCLK |
对非显示区域滑鼠讯息,wParam和lParam参数与显示区域滑鼠讯息的wParam和lParam参数不同。wParam参数指明移动或者按滑鼠按键的非显示区域。它设定为WINUSER.H中定义的以HT开头的识别字之一。
lParam参数的低位元word为x座标,高位元word为y座标,但是,它们是萤幕座标,而不是像显示区域滑鼠讯息那样指的是显示区域座标。对萤幕座标,显示器左上角的x和y的值为0。当往右移时x的值增加,往下移时y的值增加。
您可以用两个Windows函式将萤幕座标转换为显示区域座标或者反之:
ScreenToClient (hwnd, &pt) ;
ClientToScreen (hwnd, &pt) ;
这里pt是POINT结构。这两个函式转换了保存在结构中的值,而且没有保留以前的值。注意,如果萤幕座标点在视窗显示区域的上面或者左边,显示区域座标x或y值就是负值。