键盘有几种不同的输入类型:

  • 字符输入,用户用来编辑文本。
  • 键盘快捷键,执行一些程序的快捷操作,例如:CTRL+O 打开文件。
  • 系统命令,执行一些系统操作,例如:ALT+TAB 选择窗口。

在研究键盘输入前,需要知道按键和字符输入不是一回事,例如,按下A键的结果可能会得到下面几种字符:

  • a
  • A
  • á

另外,如果按住 ALT 键,再按下A键,系统不会认为这是字符输入操作,而会把它当做一条系统命令去执行。

1. 虚拟键盘码

当你按下一个键的时候,硬件会生成一个扫描码(scan code),驱动程序会将扫描码转换为虚拟键盘码(virtual-key codes),虚拟键盘码是独立于设备的。任何键盘上按下相同的键会产生一样的虚拟键盘码。

一般情况下,虚拟键盘码和 ASCII 码或者其他字符编码完全不同。原因显而易见,因为同一个键可能生成不同的字符,并且一些键不是会产生字符,而是执行某些功能。

下面这些虚拟键盘码会映射为 ASCII 码:

  • 0 到 9 keys = ASCII ‘0’ – ‘9’ (0x30 – 0x39)
  • A 到 Z keys = ASCII ‘A’ – ‘Z’ (0x41 – 0x5A)

这种映射关系并不一定是好事,因为这很容易让人误以为虚拟键盘码和字符有着某种关系。

在 WinUser.h 头文件中定义了大量的虚拟键盘码的常量。例如:向左的方向键对应着 VK_LEFT(0x25)的虚拟键盘码。这里可以查看完整的 虚拟键盘码。虚拟键盘码中没有定义 ASCII 相关的常量信息,例如 A 键的 ASCII 码是 0x41,如果想要使用该键,直接使用这个值即可,Windows 没有定义类似 VK_A 这样的常量。

2. 按键按下和按键释放消息

当你按下一个键的时候,键盘焦点窗口将会收到下列消息之一:

  • WM_SYSKEYDOWN
  • WM_KEYDOWN

WM_SYSKEYDOWN 消息代表一个系统键(system key),按下系统键意味着需要执行一些系统命令,下面是两种典型的系统键:

  • ALT + 任何键
  • F10

按下 F10 键默认会激活 Windows 窗口 的菜单栏。而执行 ALT-key 组合件会执行系统命令。例如,ALT + TAB 可以选择一个新窗口。假如一个窗口存在菜单栏,按下 ALT 键可以激活菜单项,ALT 具体和哪些键组合完全在于应用程序菜单栏的快捷键是什么,如果某些组合键不存在,应用程序将会忽略该命令。

剩余的按键都被归为非系统键(nonsystem key),如果按下则会产生 WM_KEYDOWN 消息。

当释放一个按键的时候,系统会发送对应的两条消息:

  • WM_KEYUP
  • WM_SYSKEYUP

如果你长时间按下某个键,将会开启键盘的重复特性,这回导致系统发送多个 key-down 消息,一个 key-up 消息。

目前这四种消息中,参数 wParam 会包含按键的虚拟键盘码。参数 lParam 包含32位的标记位组合信息。大部分情况下都不需要考虑 lParam 的值,但有一个标记位可能会被用到,在第30位的位置,标记着按键上一次的状态信息,如果为1则代表开启了键盘重复特性而产生的 key-down 消息。

系统键一般用于执行一些操作系统自带的功能。如果应用程序拦截了 WM_SYSKEYDOWN 消息,则操作系统默认的功能将不会被执行。

3. 字符消息

TranslateMessage 函数会将按键转换为对应的字符,每个产生一个字符该函数都会向消息队列中放入一个 WM_CHAR 消息或者 WM_SYSCHAR 消息。wParam 参数将会包含这个 UTF-16 编码的字符。

就像你猜测的一样,WM_CHAR 消息产生自 WM_KEYDOWN 消息,而 WM_SYSCHAR 消息产生自 WM_SYSKEYDOWN 消息。例如,假设按下 SHIFT 键然后按下 A 键,如果一个标准的键盘布局,你将会收到下面的消息序列:

WM_KEYDOWN: SHIFT → WM_KEYDOWN: A → WM_CHAR: 'A'

如果你按下 ALT + P 组合件,则会生成:

WM_SYSKEYDOWN: VK_MENU → WM_SYSKEYDOWN: 0x50 → WM_SYSCHAR: 'p' → WM_SYSKEYUP: 0x50 → WM_KEYUP: VK_MENU

由于历史原因,ALT 键的名字被称为 VK_MENU。

WM_SYSCHAR 消息代表一个系统字符,像 WM_SYSKEYDOWN 消息一样,一般情况下应该直接将该消息投递到 DefWindowProc 函数中处理,如果你拦截该消息,可能会导致一些系统命令失效。特别注意的是不要将 WM_SYSCHAR 视为用户输入的普通字符。

WM_CHAR 消息代表通常情况下的字符输入。这个字符类型是 wchar_t,代表一个 UTF-16 编码的字符。字符可以包含 ASCII 码之外的其它字符,特别是对于非美国标准的键盘。

用户还可以安装输入法编辑器(IME)来录入一些复杂的脚本。例如,使用日本键盘输入法键入片假名 カ (ka),你可能会收到下面的消息:

WM_KEYDOWN: VK_PROCESSKEY (the IME PROCESS key) → WM_KEYUP: 0x4B → WM_KEYDOWN: VK_PROCESSKEY → WM_KEYUP: 0x41 → WM_KEYDOWN: VK_PROCESSKEY → WM_CHAR: カ → WM_KEYUP: VK_RETURN

一些组合键被转换为 ASCII 控制字符。如,CTRL+A 被转换为 ASCII ctrl-A(SOH)字符(ASCII值为0x01)。对于文本输入,你应该在程序中过滤掉这些控制字符,并且在程序中最好使用 WM_KEYDOWN 消息作为应用程序的快捷键消息捕获,而不是通过 WM_CHAR 消息。更好的方法是在程序中使用快捷键表(accelerator table)处理快捷键的问题。

下面的代码用来调试各种键盘消息的响应情况,你可以尝试使用不同的键盘组合,研究消息生成的类型和顺序,有助于掌握本章的知识内容:

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    wchar_t msg[32];
    switch (uMsg)
    {
    case WM_SYSKEYDOWN:
        swprintf_s(msg, L"WM_SYSKEYDOWN: 0x%x\n", wParam);
        OutputDebugString(msg);
        break;

    case WM_SYSCHAR:
        swprintf_s(msg, L"WM_SYSCHAR: %c\n", (wchar_t)wParam);
        OutputDebugString(msg);
        break;

    case WM_SYSKEYUP:
        swprintf_s(msg, L"WM_SYSKEYUP: 0x%x\n", wParam);
        OutputDebugString(msg);
        break;

    case WM_KEYDOWN:
        swprintf_s(msg, L"WM_KEYDOWN: 0x%x\n", wParam);
        OutputDebugString(msg);
        break;

    case WM_KEYUP:
        swprintf_s(msg, L"WM_KEYUP: 0x%x\n", wParam);
        OutputDebugString(msg);
        break;

    case WM_CHAR:
        swprintf_s(msg, L"WM_CHAR: %c\n", (wchar_t)wParam);
        OutputDebugString(msg);
        break;

    /* Handle other messages (not shown) */

    }
    return DefWindowProc(m_hwnd, uMsg, wParam, lParam);
}

4. 各种键盘消息

一些键盘消息可以在大多数的程序中被忽略:

  • WM_DEADCHAR 消息用来发送一个组合键。例如一个发音符号,在西班牙语的键盘中,按下重音符 ‘ 和字符键 E 会产生字符 é。而 WM_DEADCHAR 消息就代表这个发送的重音符(é)。

  • WM_UNICHAR 消息已经被淘汰了。它可以使 ANSI 程序接收 Unicode 字符输入。

  • WM_IME_CHAR 该字符消息是当输入法转换一组按键操作为字符后发送。它是在 WM_CHAR 消息之外发送。

5. 键盘状态

键盘消息是事件驱动的,换句话说,只有在某些事情发生后你才能获取一个消息,例如当按下某个按键,你会收到刚刚按键对应的消息。这种消息的通知是异步的,为了任何时候都可以实时获取当前键盘状态,Windows 提供了 GetKeyState 函数。

例如,当你程序中需要响应鼠标左键单击和 ALT 键的组合时,你可能会想到用一个标记位来记录 ALT 键的状态,但是如果使用 GetKeyState 函数将会免去这些麻烦。当你收到 WM_LBUTTONDOWN 消息的时候,立刻调用 GetKeyState 函数获取按键状态:

if (GetKeyState(VK_MENU) & 0x8000))
{
    // ALT key is down.
}

GetKeyState 函数输入的参数是一个虚拟键盘码,它会返回一组标记位用来标记当前按键的状态(通常是两个标记)。0x8000 常量用来测试当前按键是否被按下。

一般键盘都会有两个 ALT 键,上面的例子测试是否有 ALT 键按下,而不管是哪一个键,你可以通过下面的方法专门检测具体的 ALT 键:

if (GetKeyState(VK_RMENU) & 0x8000))
{
    // ALT key is down.
}

GetKeyState 函数可以获取当前应用程序的虚拟键盘快照,而这个虚拟键盘的状态获取是基于消息队列中的内容。这也意味着,当你切换到另一个程序的时候,GetKeyState 函数将不再会检测到任何键盘事件,假如你真想获取当前物理按键的情况,可以使用 GetAsyncKeyState 函数,但是大多数情况下都推荐使用 GetKeyState 而不是 GetAsyncKeyState。