键盘对于大家来说可能再也熟悉不过了,它和鼠标是现在最常用的电脑输入设备。虽然在现在的图形界面操作系统下使用鼠标比使用键盘更方便、更广泛,但是鼠标还是一时半会儿取代不了它的老前辈——键盘的地位,尤其是在打字方面。这一回我们就从编程的角度重新认识一下键盘吧。
键盘基础
我们用前面的知识分析个例子吧。比如我们在打字时按下了键盘上的一个按键,即用户触发了一个事件,有事件产生,系统自然要将其包装成相应的消息并交由相关程序来处理。简而言之,Windows程序获得键盘输入的方式:键盘输入以消息的形式传递给程序的窗口过程。
如果要说的再详细一点,可以这么叙述:
当用户按下某个键时,
1.键盘会检测到这个动作,并通过键盘控制器把扫描码(scan code)传送到计算机;
键盘扫描码跟具体的硬件有关的,不同厂商对同一个键的扫描码有可能不同。
2.计算机接收到扫描码后,将其交给键盘驱动程序;
3.键盘驱动程序把这个扫描码转换为键盘虚拟码;
虚拟码与具体硬件无关,不同厂商的键盘,同一个键的虚拟码总是相同的。
3.然后,键盘驱动程序把该键盘操作的扫描码和虚拟码以及其它信息传递给操作系统;
4.操作系统将获得的信息封装在一个键盘消息中,并把该键盘消息插入到消息列队。
5.通过Windows的消息系统,该键盘消息被送到某个窗口中;
6.窗口所在的应用程序接收到消息后,可以了解到有关键盘操作的信息,然后决定作出一定的响应。
注意,4中产生的这些消息并不保存在应用程序的消息队列中。实际上,Windows在系统消息队列中保存这些消息。系统消息队列是独立的消息队列,它由Windows维护,用于初步保存用户从键盘和鼠标输入的信息。只有当Windows应用程序处理完前一个用户输入消息时,Windows才会从系统消息队列中取出下一个消息,并将其放入响应应用程序的消息队列中。概括的来讲,此过程分为两步:首先在系统消息队列中保存消息,然后将它们放入应用程序的消息队列。
与所有的个人计算机硬件一样,键盘必须由在Windows下运行的所有应用程序共享。有些应用程序可能有多个窗口,键盘又必须由该应用程序内的所有窗口共享。当按下键盘上的键时,只会有一个窗口的窗口过程接收键盘消息,并且此消息结构体中hwnd字段就指出了接收此消息窗口的句柄。我们可以说接收特定键盘事件的窗口具有输入焦点。
应用程序从Windows接受的关于键盘事件的消息可以分为击键消息和字符消息。我想大家从名字就应该对这两种消息有了一种感官上的认识了:前者应该与按下(或松开)键盘上的按键这一动作有关,后者应该与按键上印的字符有关。先有个模糊的印象,接下来我们就要详细了解这两种消息了。
击键消息
先认识一下它们的名字吧:WM_KEYDOWN、WM_KEYUP、WM_SYSKEYDOWN和WM_SYSKEYUP。一般我喜欢先从名字中揣测新知识的含义,不妨你也先猜一下它们的具体含义。
或许正如你猜的,当你按下一个键时,Windows把WM_KEYDOWN或者WM_SYSKEYDOWN消息放入消息队列;当你释放一个键时,Windows把WM_KEYUP或者WM_SYSKEYUP消息放入消息队列。通常“down(按下)”和“up(放开)”消息是成对出现的。不过,如果你按住一个键使得自动重复功能生效,那么当该键最后被释放时,Windows会给窗口过程发送一系列WM_KEYDOWN(或者WM_SYSKEYDOWN)消息和一个WM_KEYUP(或者WM_SYSKEYUP)消息。
WM_SYSKEYDOWN和WM_SYSKEYUP中的“SYS”代表“系统”,它表示该击键对Windows系统比对Windows应用程序更加重要。WM_SYSKEYDOWN和WM_SYSKEYUP消息经常由与Alt相组合的击键产生,这些击键激活程序菜单或者系统菜单上的选项,或者用于切换活动窗口等系统功能(Alt-Tab或者Alt-Esc),也可以用作系统菜单加速键(Alt键与一个功能键相结合,例如Alt-F4用于关闭应用程序)。程序通常忽略WM_SYSKEYUP和WM_SYSKEYDOWN消息,而是将它们传送到DefWindowProc来处理。由于Windows要处理所有Alt键的功能,所以你无需拦截这些消息。当然您想在自己的窗口过程中加上拦截系统按键的程序代码,也不是不行。
但是,请再考虑一下,几乎所有会影响用户程序窗口的消息都会先通过用户窗口过程。只有用户把消息传送到DefWindowProc,Windows系统才会对消息进行默认处理。例如,如果你将下面几行语句:
case WM_SYSKEYDOWN:
case WM_SYSKEYUP:
case WM_SYSCHAR:
return 0;
加入到一个窗口过程中,那么当你的程序主窗口拥有输入焦点时,就可以有效地阻止所有Alt键操作,其中包括Alt-Tab、Alt-Esc以及菜单操作。看到了吧,这些消息无法送达DefWindowProc,Windows系统就无法进行相关处理。虽然我怀疑你会这么做,但是,我相信你会感到窗口过程的强大功能。
WM_KEYDOWN和WM_KEYUP消息通常是在按下或者释放不带Alt键的键时产生的,你的程序可以使用或者忽略这些消息,Windows本身并不处理这些消息。
对以上四个击键消息中,wParam是虚拟键代码,表示按下或释放的键,而lParam则包含属于按键的其它数据(我们暂且不用理会它)。wParam、lParam还记得吗,如果记不得的话,再回去复习一下第四回中消息结构体的讲解。
下面我们就来详细的认识一下什么是虚拟键码。
虚拟键码(虚拟码与具体硬件无关,不同厂商的键盘,同一个键的虚拟码总是相同的)保存在WM_KEYDOWN、WM_KEYUP、WM_SYSKEYDOWN和WM_SYSKEYUP消息wParam参数中。简单地讲,此代码标识按下或释放的键。
我们使用的大多数虚拟键码的名称都以VK_开头。下表列出了这些名称和数值(十进制和十六进制),以及与虚拟键相对应的IBM兼容机种键盘上的键。下表也标出了Windows执行时是否需要这些键。下表还按数字顺序列出了虚拟键码。
十进制 |
十六进制 |
WINUSER.H标识符 |
必需? |
IBM兼容键盘 |
1 |
01 |
VK_LBUTTON |
鼠标左键 |
|
2 |
02 |
VK_RBUTTON |
鼠标右键 |
|
3 |
03 |
VK_CANCEL |
ˇ |
Ctrl-Break |
4 |
04 |
VK_MBUTTON |
鼠标中键 |
前四个虚拟键码中有三个指的是鼠标键。你永远都不会从键盘消息中获得这些鼠标键代码。在下一回可以看到,我们能够从鼠标消息中获得它们。VK_CANCEL代码是一个虚拟键码,它包括同时按下两个键(Ctrl-Break)。Windows应用程序通常不使用此键。
十进制 |
十六进制 |
WINUSER.H标识符 |
必需? |
IBM兼容键盘 |
8 |
08 |
VK_BACK |
ˇ |
Backspace |
9 |
09 |
VK_TAB |
ˇ |
Tab |
12 |
0C |
VK_CLEAR |
Num Lock关闭时的数字键盘5 |
|
13 |
0D |
VK_RETURN |
ˇ |
Enter (或者另一个) |
16 |
10 |
VK_SHIFT |
ˇ |
Shift (或者另一个) |
17 |
11 |
VK_CONTROL |
ˇ |
Ctrl (或者另一个) |
18 |
12 |
VK_MENU |
ˇ |
Alt (或者另一个) |
19 |
13 |
VK_PAUSE |
Pause |
|
20 |
14 |
VK_CAPITAL |
ˇ |
Caps Lock |
27 |
1B |
VK_ESCAPE |
ˇ |
Esc |
32 |
20 |
VK_SPACE |
ˇ |
Spacebar |
表中的键--Backspace、Tab、Enter、Escape和Spacebar-通常用于Windows程序。不过,Windows一般用字符消息(而不是键盘消息)来处理这些键。另外,Windows程序通常不需要监视Shift、Ctrl或Alt键的状态。
十进制 |
十六进制 |
WINUSER.H标识符 |
必需? |
IBM兼容键盘 |
33 |
21 |
VK_PRIOR |
ˇ |
Page Up |
34 |
22 |
VK_NEXT |
ˇ |
Page Down |
35 |
23 |
VK_END |
ˇ |
End |
36 |
24 |
VK_HOME |
ˇ |
Home |
37 |
25 |
VK_LEFT |
ˇ |
左箭头 |
38 |
26 |
VK_UP |
ˇ |
上箭头 |
39 |
27 |
VK_RIGHT |
ˇ |
右箭头 |
40 |
28 |
VK_DOWN |
ˇ |
下箭头 |
41 |
29 |
VK_SELECT |
||
42 |
2A |
VK_PRINT |
||
43 |
2B |
VK_EXECUTE |
||
44 |
2C |
VK_SNAPSHOT |
Print Screen |
|
45 |
2D |
VK_INSERT |
ˇ |
Insert |
46 |
2E |
VK_DELETE |
ˇ |
Delete |
47 |
2F |
VK_HELP |
上表列出的前八个码可能是与VK_INSERT和VK_DELETE一起最常用的虚拟键码。
Print Screen键在平时都被Windows应用程序所忽略。VK_SELECT、VK_PRINT、VK_EXECUTE和VK_HELP只在我们很少见的键盘上出现,也不用去理会。
十进制 |
十六进制 |
WINUSER.H标识符 |
必需? |
IBM兼容键盘 |
48-57 |
30-39 |
无 |
ˇ |
主键盘上的0到9 |
65-90 |
41-5A |
无 |
ˇ |
A到Z |
Windows也包括在主键盘上的字母和数字键的虚拟键码(数字键盘将单独处理)。注意,数字和字母的虚拟键码是ASCII码。Windows程序几乎从不使用这些虚拟键码;实际上,程序使用的是ASCII码字符的字符消息。
十进制 |
十六进制 |
WINUSER.H标识符 |
必需? |
IBM兼容键盘 |
91 |
5B |
VK_LWIN |
左Windows键 |
|
92 |
5C |
VK_RWIN |
右Windows键 |
|
93 |
5D |
VK_APPS |
Applications键 |
上表所示的代码是由Microsoft Natural Keyboard及其兼容键盘产生的:Windows用VK_LWIN和VK_RWIN键打开“开始”菜单。应用程序能够通过显示帮助信息或者当成快捷方式键看待来处理application键。
十进制 |
十六进制 |
WINUSER.H标识符 |
必需? |
IBM兼容键盘 |
96-105 |
60-69 |
VK_NUMPAD0到VK_ NUMPAD9 |
NumLock打开时数字键盘上的0到9 |
|
106 |
6A |
VK_MULTIPLY |
数字键盘上的* |
|
107 |
6B |
VK_ADD |
数字键盘上的+ |
|
108 |
6C |
VK_SEPARATOR |
||
109 |
6D |
VK_SUBTRACT |
数字键盘上的- |
|
110 |
6E |
VK_DECIMAL |
数字键盘上的. |
|
111 |
6F |
VK_DIVIDE |
数字键盘上的/ |
上表所示的代码用于数字键盘上的键(如果有的话)。
十进制 |
十六进制 |
WINUSER.H标识符 |
必需? |
IBM兼容键盘 |
112-121 |
70-79 |
VK_F1到VK_F10 |
ˇ |
功能键F1到F10 |
122-135 |
7A-87 |
VK_F11到VK_F24 |
功能键F11到F24 |
|
144 |
90 |
VK_NUMLOCK |
Num Lock |
|
145 |
91 |
VK_SCROLL |
Scroll Lock |
最后,虽然多数的键盘都有12个功能键,但Windows只需要10个,而数字标识却有24个。另外,程序通常用功能键作为键盘加速键,这样,它们通常不处理上表所示的按键。另外,还定义了一些其它虚拟键码,但它们只用于非标准键盘上的键,或者通常在大型主机终端机上使用的键。这里就不在列表介绍了。
如果程序能够获得每个按键的信息,这当然很理想,但是大多数Windows程序忽略了几乎所有的按键,而只处理部分的按键消息。WM_SYSKEYDOWN和WM_SYSKEYUP消息是由Windows系统函数使用的,你不必为此费心,就算你要处理WM_KEYDOWN消息,通常也可以忽略WM_KEYUP消息。
Windows程序通常为不产生字符的击键使用WM_KEYDOWN消息。对于光标移动键、功能键、Insert和Delete键,WM_KEYDOWN消息是最有用的。不过, Insert、Delete和功能键经常作为菜单加速键。因为Windows能把菜单加速键翻译为菜单命令消息,所以你就不必自己来处理按键。可以归纳如下:多数情况下,你将只为光标移动键(有时也为Insert和Delete键)处理WM_KEYDOWN消息。在使用这些键的时候,你可以通过GetKeyState来检查Shift键和Ctrl键的状态。例如,Windows程序经常使用Shift与光标键的组合键来扩大文字处理文档里选中的范围。Ctrl键常用于修改光标键的意义。例如,Ctrl与右箭头键相组合可以表示光标右移一个词。
字符消息
while (GetMessage (&msg, NULL, 0, 0))
{
TranslateMessage (&msg) ;
DispatchMessage (&msg) ;
}
大家对这个还有印象吧,这是WinMain中典型的消息循环。GetMessage函数用队列中的下一个消息填入msg结构的字段。DispatchMessage以此消息为参数调用适当的窗口过程。
在这两个函数之间是TranslateMessage函数,它将击键消息转换为字符消息。如果消息为WM_KEYDOWN或者WM_SYSKEYDOWN,并且击键与换挡状态①相组合产生一个字符,则TranslateMessage把字符消息放入消息队列中。此字符消息将是GetMessage从消息队列中得到的击键消息之后的下一个消息。
字符消息可以分为四类:WM_CHAR和WM_DEADCHAR消息是从WM_KEYDOWN得到的;而WM_SYSCHAR和WM_SYSDEADCHAR消息是从WM_SYSKEYDOWN消息得到的。在大多数情况下,Windows程序会忽略除WM_CHAR之外的任何消息。所以我们后面涉及的仅仅是WM_CHAR消息。
伴随四个字符消息的lParam参数与产生字符代码消息的按键消息之lParam参数相同。不过,参数wParam不是虚拟键码。实际上,它是ANSI或Unicode字符代码。
因为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) |
如果用户按住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) |
组合使用Ctrl键与字母键会产生从0x01(Ctrl-A)到0x1A(Ctrl-Z)的ASCII控制代码,其中的某些控制代码也可以由下表列出的键产生:
按键 |
字符代码 |
产生方法 |
ANSI C控制字符 |
Backspace |
0x08 |
Ctrl-H |
|
Tab |
0x09 |
Ctrl-I |
|
Ctrl-Enter |
0x0A |
Ctrl-J |
|
Enter |
0x0D |
Ctrl-M |
|
Esc |
0x1B |
Ctrl-[ |
最右列给出了在ANSI C中定义的控制字符,它们用于描述这些键的字符代码。有时Windows程序将Ctrl与字母键的组合用作菜单快捷键,此时,不会将字母键转换成字符消息。
处理击键和字符消息的基本规则是:如果需要读取输入到窗口的键盘字符,那么您可以处理WM_CHAR消息。如果需要读取光标键、功能键、Delete、Insert、Shift、Ctrl以及Alt键,那么您可以处理WM_KEYDOWN消息。
但是Tab键怎么办?Enter、Backspace和Escape键又怎么办?传统上,这些键都产生表6-13列出的ASCII控制字符。但是在Windows中,它们也产生虚拟键码。这些键应该在处理WM_CHAR或者在处理WM_KEYDOWN期间处理吗?
Petzold先生将Tab、Enter、Backspace和Escape键处理成控制字符,而不是虚拟键。通常这样处理WM_CHAR:
case WM_CHAR:
[other program lines]
switch (wParam)
{
case '': // backspace
[other program lines]
break ;
case ' ': // tab
[other program lines]
break ;
case ' ': // linefeed
[other program lines]
break ;
case ' ': // carriage return
[other program lines]
break ;
default: // character codes
[other program lines]
break ;
}
return 0 ;
WM_DEADCHAR和WM_SYSDEADCHAR是“死键”消息,Windows程序基本忽略它们,我们就不用了解了。