摘录于《Windows程序(第5版,珍藏版).CHarles.Petzold 著》P176
当用户按下一个键时,Windows 将 WM_KEYDOWN 或 WM_SYSKEWDOWN 消息放入具有输入焦点的窗口的消息队列中。当该键被释放时,Windows 把 WM_KEYUP 或 WM_SYSKEYUP 消息放入相应的消息队列中。
键按下 | 键释放 | |
---|---|---|
非系统键击 | WM_KEYDOWN | WM_KEYUP |
系统键击 | WM_SYSKEYDOWN | WM_SYSKEYUP |
通常键按下消息和键释放消息是成对出现的。但是如果你按下一个键不放时,则被认为发送了一次连续按键(自动重复)行为,Windows 将发送给窗口过程一连串的 WM_KEYDOWN(或 WM_SYSKEYDOWN)消息。当此键最终被释放时,Windows 发送给窗口过程一个 WM_KEYUP(或 WM_SYSKEYUP)消息。像所有的队列消息一样,击键消息是被可实时追踪的。你能通过调用 GetMessageTime 函数,得到键被按下或释放的相对时间。
6.2.1 系统键击和非系统键击
WM_SYSKEYDOWN 和 WM_SYSKEYUP 中的 “SYS”代表系统,它表明该击键对 Windows 比对 Windows 应用程序更加重要。当输入键和 Alt 键组合时通常产生的是 WM_SYSKEYDOWN 和 WM_SYSKEYUP 消息。这些按键调用程序菜单或系统菜单选项,被用来实现系统功能如转换活动窗口(Alt-Tab 键 Alt-Esc 键),或作为系统菜单快捷键(Alt 键和功能键的组合,如 Alt-F4 是用于关闭一个应用程序)。应用程序通常忽略 WM_SYSKEYUP 和 WM_SYSKEYDOWN 消息,将它们交付给 DefWindowProc 函数完成默认处理。因为Windows 关注所有的 Alt 键功能逻辑,应用程序就不必处理这些消息。你的窗口过程最终会接收到的是与击键产生结果相关的消息(如一个菜单被选中)。如果你在窗口过程中编码去捕获这些系统击键消息,则在处理完毕后,仍然需要发送这些消息给 DefWindowProc 函数,以便不影响 Windows 对它的处理。
但是再仔细考虑一下。几乎所有影响程序窗口的消息都将先经过窗口过程。仅当应用程序传递给 DefWindowProc 函数时,Windows 才会处理这些消息。例如,如果你在窗口过程中增加下面几行:
那么在你的程序主窗口具有输入焦点时,就可以有效地阻止所有 Alt 键的操作。这些操作包含 Alt-Tab 键、Alt-Esc 键和菜单操作。虽然你不一定想做这些,但我相信你能感觉到窗口过程内含的强大功能。
不与 Alt 组合时按下和释放键会产生 WM_KEYDOWN 和 WM_KEYUP 消息。应用程序可以使用或者丢弃这些击键消息。Windows 也不处理它们。
对所有四类击键消息,wParam 是虚拟键代码,用于标识哪个键被按下或被释放,而 lParam 包含属于本次击键的一些其他数据。
6.2.2 虚拟键代码
虚拟键代码
存储在 WM_KEYDOWN、WM_KEYUP、WM_SYSKEYDOWN 和 WM_SYSKEYUP 消息的
wParam 参数中
。
此代码确定哪个键被按下或被释放
。
啊哈,“虚拟”这个词无处不在,难道你不喜欢它吗?它原本是指存在于意念中而不是现实世界中的某物,但也只有使用 DOS 汇编语言编写应用程序的经验丰富的程序员才能领悟到,为什么对 Windows 键盘处理过程如此重要的键代码是虚拟的而不是真实的。
对早期的程序员,真实的键码是由自然键盘硬件产生的。Windows 文件称它们为扫描码。在 IBM 兼容键盘上,扫描码 16 为 Q 键,17 为 W 键,18 为 E 键,19 为 R 键,20 为 T 键,21 为 Y 键等。你会发现,扫描码基于键盘的自然布局。Windows 的程序开发者认为这些扫描码与键盘太相关了,所以他们通过定义所谓的虚拟键代码,试图使用与设备无关的方式来处理键盘。一些虚拟键代码没有用在 IBM 兼容键盘上,但可能能在其他制造厂商的键盘上找到,或者可能会用在未来的键盘上。
你经常使用的大多数虚拟键代码命名是以 VK_ 开头的,它定义在 WINUSER.H 头文件中。下面这些表中列出了这些虚拟键代码的名称和数值(用十进制和十六进制)以及对应于虚拟键的 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 应用程序通常不使用此键。
以下表中的一些键,如退格键、Tab 键、回车键、Esc 键和空格键,经常被用于 Windows 程序中。但是 Windows 程序通常使用字符消息(而不是击键消息)来处理这些键。
十进制 | 十六进制 | WINUSER.H | 是否必需 | IBM 兼容键盘 |
---|---|---|---|---|
8 | 08 | VK_BACK | √ | 退格键 |
9 | 09 | VK_TAB | √ | Tab 键 |
12 | 0C | VK_CLEAR | 数字锁定键关闭时的数字键 5 | |
13 | 0D | VK_RETURN | √ | 回车键(任意一个) |
16 | 10 | VK_SHIFT | √ | Shift 键(任意一个) |
17 | 11 | VK_CONTROL | √ | Ctrl 键(任意一个) |
18 | 12 | VK_MENU | √ | Alt 键(任意一个) |
19 | 13 | VK_PAUSE | Pause 键 | |
20 | 14 | VK_CAPITAL | √ | 大写锁定键 |
27 | 1B | VK_ESCAPE | √ | Esc 键 |
32 | 20 | VK_SPACE | √ | 空格键 |
下表中列出的前八个代码以及 VK_INSERT、VK_DELETE 码可能是最常使用的虚拟键代码:
十进制 | 十六进制 | 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 | CK_SNAPSHOT | Print Screen 键 | |
45 | 2D | VK_INSERT | √ | Insert 键 |
46 | 2E | VK_DELETE | √ | Del 键 |
47 | 2F | VK_HELP |
Windows 也包含了主键盘上的字母键和数字键的虚拟键代码(数字键盘被单独处理)。
十进制 | 十六进制 | WINUSER.H 中的标识符 | 是否必需 | IBM 兼容键盘 |
---|---|---|---|---|
48—57 | 30—39 | 无 | √ | 主键盘上的 0 到 9 |
65—90 | 41—5A | 无 | √ | A 到 Z |
下面的键是由微软 Natural Keyboard 键盘及其兼容键盘产生的。
十进制 | 十六进制 | WINUSER.H 中的标识符 | 是否必需 | IBM 兼容键盘 |
---|---|---|---|---|
91 | 5B | VK_LWIN | 左 Windows 键 | |
92 | 5C | VK_RWIN | 右 Windows 键 | |
93 | 5D | VK_APPS | Application 键 |
下面的代码是和数字小键盘中的键相对应的代码(如果存在的话):
十进制 | 十六进制 | WINUSER.H 中的标识符 | 是否必需 | IBM 兼容键盘 |
---|---|---|---|---|
96—105 | 60—69 | VK_NUMPAD0 到 VK_NUMPAD9 | 数字锁定键打开时数字 小键盘的 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 | 数字键区的/ |
最后,尽管大部分键盘都有 12 个功能键,Windows 则仅需要 10 个,但它却有 24 个数字标识符。此外,程序通常把功能键用作键盘快捷键,所以它们通常不处理下表的击键:
十进制 | 十六进制 | 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 | 数字锁定键 | |
145 | 91 | VK_SCROLL | Scroll Lock 键 |
虽然还定义了其他一些虚拟键代码,但它们被保留为非标准键盘上的键或者主机终端上的键。
6.2.3 lparam 信息
如前所述,在四个击键消息中(WM_KEYDOWN、WM_KEYUP、WM_SYSKEYDOWN、WM_SYSKEYUP),wParam 消息参数包含了虚拟键代码,lParam 消息参数包含了帮助理解击键的其他有用消息。32 位的 lParam 消息被分成了 6 个字段,如图 6-1 所示。
重复计数
重复计数是消息所表示的击键的数目。大多数情况下,它被设置为 1。但是,如果你按下一个键不放,且窗口过程不足够快,跟不上输入速率(该项可在控制面板的【键盘】应用程序中设置)来处理击键消息,Windows 就会把一些 WM_KEYDOWN 和 WM_SYSKEYDOWN 消息合并成一个单独的消息,并相应增加重复计数字段。WM_KEYUP 和 WM_SYSKEYUP 消息的重复计数总是为 1。
重复计数大于 1 表明此时连续击键的速度快于程序的处理能力,所以你可能想在处理键盘消息的时候忽略重复计数。由于额外的击键堆积,几乎每一个人都有过字处理文档或电子表格不停滚屏的经历。当程序要花费一段时间来处理每一个击键时,应用程序可以忽略重复计数来解决此问题。但是在其他情况下,你也许需要使用重复计数。你可能需要在这两种情况下执行程序,找到最合适的一种。
OEM 扫描码
OEM 扫描码是键盘硬件产生的代码。这对中年的汇编语言程序员来说是相当熟悉的,他们从 PC 兼容机的 ROM BIOS 服务中获得这些值(OEM 指的是个人计算机的原始设备制造厂商(Original Equipment Manufacturer),在这里是指“IBM 标准”)。我们不在需要这种东西了。
Windows 程序几乎可以做到忽略 OEM 扫描码,除非是它要依赖于键盘上键的分布
。
扩展键标记
如果击键结果来自于 IBM 加强型键盘的附加键,则扩展键标记为 1。(IBM 加强型键盘有 101 或 102 个键。键盘上部是功能键。光标移动键与数字小键盘分离,但数字小键盘保留有光标移动键的功能。)键盘右侧的 Alt 和 Ctrl 键、分离于数字小键盘的光标移动键(包含 Insert 键和 Delete 键)、数字小键盘的斜线和回车键,以及 Num Lock 键的这一标记位均设置为 1。
Windows 程序通常忽略扩展键标记
。
内容代码
如果在击键的同时也按下了 Alt 键,则内容代码为 1。WM_SYSKEYUP 和 WM_SYSKEYDOWN 消息的此位始终为 1,而 WM_KEYUP 和 WM_KEYDOWN 消息的此位始终为0。有两种情况例外。
- 如果活动窗口最小化了,则它不具有输入焦点。所有的击键将产生 WM_SYSKEYUP 和 WM_SYSKEYDOWN 消息。如果 Alt 键未被按下,内容代码字段将被置为 0。Windows 处理 WM_SYSKEYUP 和 WM_SYSKEYDOWN 消息,使最小化的活动窗口不处理这些击键。
- 在某些非英语的键盘上,一些字符是通过 Shift 键、Ctrl 键或 Alt 键同另一个键的组合产生的。在这些情况下,内容代码被设置为 1,但消息并不是系统击键消息。
键的先前状态
如果键以前是处于释放状态的,则键的先前状态为 0。而如果键以前是按下的,则键的先前状态为 1
。WM_KEYUP 和 WM_SYSKEYUP 消息的此字段总是为 1。但 WM_KEYDOWN 和 WM_SYSKEYDOWN 消息的此字段可能为 0 或 1。该位为 1 表明,消息为重复击键产生的第二个或后续发出的消息。
转换状态
如果键正在被按下,转换状态为 0;如果键正在被释放,转换状态为 1 。WM_KEYDOWN 和 WM_SYSKEYDOWN 消息的此字段设置为 0,而 WM_KEYUP 和 WM_SYSKEYUP 消息的此字段设置为 1。
6.2.4 转义状态
当处理击键消息时,你可能需要知道是否有转义键(Shift 键、Ctrl 键和 Alt 键)或切换键(Caps Lock 键、Num Lock 键和 Scroll Lock 键)被按下。你能通过调用 GetKeyState 函数获得此信息。例如:
如果 Shift 键被按下,则 iState 变量为负(即高位置 1)。如果 Caps Lock 键打开,则从
返回的值是最低位置为 1。此位与键盘上的小灯保持一致。
通常你会使用虚拟键代码 VK_SHIFT、VK_CONTROL 和 VK_MENU(你也许还记得指 Alt 键)来调用 GetKeyState 函数。你也能用 GetKeyState 函数通过标识符 VK_LSHIFT、VK_RSHIFT、VK_LCONTROL、VK_RCONTROL、VK_LMENU 或 VK_RMENU 来确定是左侧还是右侧的 Shift 键、Ctrl 键或 Alt 键被按下。这些标识符仅在 GetKeyState 函数和 GetAsyncKeyState 函数中使用。
你也能使用虚拟键代码 VK_LBUTTON、VK_RBUTTON 和 VK_MBUTTON 来得到鼠标按钮的状态。但是,大多数需要监视鼠标按钮和击键的 Windows 程序通常使用另一种方法,即当 Windows 程序接收到鼠标消息时,才检查击键。实际上,转义状态信息被包含在鼠标消息中,我们将在下一章介绍。
请注意 GetKeyState函数的用法。它并非实时的检查键盘状态。更准确地说,它反映了到目前为止的键盘状态,并包含了正在被处理的当前消息。大多数情况下,这正是你想要的。如果你需要确定用户是否按下了 Shift+Tab 键,可在处理 Tab 键的 WM_KEYDOWN 消息时,调用含 VK_SHIFT 参数的 GetKeyState 函数。如果 GetKeyState 函数的返回值是负的,你就知道在按下 Tab 键之前按下了 Shift 键。并且在你处理 Tab 键时,Shift 键是否已被释放没有什么影响。你只要知道在 Tab 键按下的时候,Shift 键是按下的。
GetKeyState 函数无法让你获得独立于标准键盘消息的键盘消息。例如,你也许感到有必要暂停窗口过程的处理,直到用户按下 F1 功能键:
这种做法是错误的!这一定会中止你的程序(当然,除非在执行该语句之前,你从消息队列中获得了 F1 功能键的 WM_KEYDOWN 消息)。如果你确实需要了解某个键的当前实时状态,可以使用 GetAsyncKeyState 函数。
6.2.5 使用击键消息
Windows 程序能够获得程序运行时的每一个击键信息。这当然是有用的。但是,大部分的 Windows 程序几乎忽略所有的击键消息,只处理少数的一些击键消息。Windows 系统函数处理 WM_SYSKEYDOWN 和 WM_SYSKEYUP 消息,应用程序不必关心它们。如果应用程序处理 WM_KEYDOWN 消息,通常可忽略 WM_KEYUP 消息。
Windows 程序通常为不产生字符的击键使用 WM_KEYDOWN 消息。尽管你认为可以通过使用击键消息和转义信息,把击键消息转换为字符,但也不要这么做。你将会在非英语键盘上遇到问题。例如,如果你获得 wParam 参数等于 0x33 的 WM_KEYDOWN 消息,你知道用户按下了数字键 3。到目前为止,一切都还不错。如果你使用 GetKeyState 函数,且发现 Shift 键被按下,你也许会认为用户在在输入“#”。未必如此,例如英国用户就是在输入另一种符号,看起来像£。
对光标移动键、功能键、Insert 键和 Delete 键,WM_KEYDOWN 消息是最有用的。但是,Insert 键、Delete 键与功能键,经常被用作菜单快捷键。因为 Windows 会把菜单快捷键转换为菜单命令消息,所以应用程序不比自己处理这些击键。
Windows 之前的 MS-DOS 应用程序曾经大量地使用功能键与 Shift 键、Ctrl 键和 Alt 键的组合。你能在 Windows 程序中做类似的事情(的确,Microsoft Word 大量地使用了功能键作为快捷命令方式),但不推荐这么做。如果你确实想使用功能键,这些功能键应该重复菜单命令。Windows 的目标之一就是提供不需要记忆或查询复杂命令表的用户界面。
因此,总结如下:大部分时间,你仅需要处理光标移动键的 WM_KEYDOWN 消息,有时处理 Insert 键和 Delete 键的 WM_KEYDOWN 消息。当使用这些键时,可以通过 GetKeyState 函数检查 Shift 键和 Ctrl 键的状态。例如,Windows 程序经常使用 Shift 键和光标键的组合来扩大字处理文档中的选中范围。Ctrl 键常用于改变光标键的意义。例如,Ctrl 键和右箭头键的组合用于将光标右移一个单词。
决定如何在你的应用程序中使用键盘的一种最好方法是,调查在当前流行的 Windows 程序中如何使用键盘。如果你不喜欢那些定义,也可以自由地做一些不同的事情。但是记住这样做不利于用户快速学习你的程序。
6.2.6 为 SYSMETS 加上键盘处理功能
第 4 章 SYSMETS 程序的 3 个版本都是在不了解键盘的情况下写的。我们只能通过在滚动条上使用鼠标来滚动文本。现在我们知道怎样处理击键消息,就来给程序添加键盘接口。显然,这里的功能是处理光标移动键。垂直滚动中我们们会大量使用这些键(Home、End、Page Up、Page Down、上箭头和下箭头)。左箭头键和右箭头键用于不太重要的水平滚动。
创建键盘接口的一个简单方法是在窗口过程中增加 WM_KEYDOWN 逻辑,它类似于或从本质上复制了所有的 WM_VSCROLL 和 WM_HSCROLL 逻辑。但是,这是不明智的。因为不管任何时候我们想修改滚动条逻辑,就不得不在 WM_KEYDOWN 消息上做同样的改变。
简单地把每一个 WM_KEYDOWN 消息转换为等同的 WM_VSCROLL 或 WM_HSCROLL 消息,是不是会更好吗?然后我们可以通过给窗口过程发送假冒的消息欺骗 WndProc 函数,使它认为收到了滚动条消息。
Windows 允许你这样做。函数命名为 SendMessage,它携带了传送给窗口过程的参数:
当你调用 SendMessage 函数时,Windows 调用窗口句柄是 hwnd 的窗口过程,同时把四个函数变量传递给它。当窗口过程处理完此消息,Windows 把控制权交还给紧跟着 SendMessage 调用的下一条语句。 你向它发送消息的窗口过程可以是同一个窗口过程,也可以是同一程序的其他窗口过程,或者甚至是另一个应用程序的窗口过程 。
下面将说明在 SYSMETS 程序中,我们怎样使用 SendMessage 函数处理 WM_KEYDOWN 消息:
依次类推,大概意思已经清楚了。我们的目的是给滚动条增加键盘接口,并且也已经这么做了。实际上我们通过给窗口过程发送滚动条消息,实现了用光标移动键重复滚动条逻辑。现在你明白为什么我要在 SYSMETS3 程序的 WM_VSCROLL 消息中包含 SB_TOP 和 SB_BOTTOM 处理过程了吧。那时它没有用,现在它被用来处理 Home 和 End 键。