应用程序经常定义一些键盘快捷键,例如 CTRL+O 组合键代表文件打开命令。你可以通过捕获 WM_KEYDOWN 消息来实现快捷键的逻辑,但是更好的解决方案是使用加速表(accelerator tables),或者称为快捷键表的方式来替代,这会带来很多好处:

  • 更少的代码逻辑。

  • 在一个文件中汇总所有的快捷键。

  • 支持本地化。

  • 快捷方式和菜单命令可以共享代码逻辑。

术语加速表是应用程序的一种资源,用来管理映射程序中使用的组合键,例如 CTRL+O 等应用程序命令。在开始学习如何使用加速表之前,我们将快速的了解一些关于资源的概念。

资源(resource)是一种二进制数据,它最终会嵌入到应用程序的二进制文件(EXE 或者 DLL)中。资源内部存储着应用程序在运行过程中所需要的数据,包括菜单、光标、图标、图像、字符串或者任何其它自定义的数据类型。应用程序在运行期间可以从二进制中加载这些资源,如果想要将资源打包成二进制,需要下面几个步骤:

  • 创建一个资源定义文件(.rc)。这个文件内部定义了资源的标识和类型,并且资源文件可能包含一些其它文件的引用。例如,一个图标资源在 .rc 文件中声明,但是图标文件本身并不存储在 .rc 文件中,而是独立存在的。

  • 使用微软提供的资源编译器(RC)编译资源定义源文件(.rc)为一个已经编译的资源文件(.res)。这个资源编译器包含在 Windows SDK 中。

  • 链接这个已编译的资源文件为二进制文件。

这个过程和源码编译的过程基本一致。如果你安装了 Visual Studio IDE 那么可以使用 IDE 中提供的资源编辑器来创建和修改资源,如果你的电脑中没有安装 Visual Studio IDE 也无需担心,因为资源定义文件(.rc)是一个文本文件,你可以使用任何编辑器编辑它,如果想了解关于资源文件的具体信息,可以参考 MSDN 的 相关文章

1. 定义一个加速表

一个加速表是一个键盘快捷键的列表,每个快捷键的定义如下:

  • 一个数字标识。每个数字标识对应一个应用程序命令,这些命令都可以绑定快捷键来执行。

  • 快捷键的 ASCII 字符或者虚拟键盘码。

  • 可选的辅助键:ALT、SHIFT 或 CTRL。

加速表本身也有一个标识符,这个标识符定义在应用程序资源列表中。下面让我们创建简单绘图程序的加速表。这个程序有两种模式,一种是绘画模式,另一种是选择模式。在绘画模式中,用户可以绘制一些形状。在选择模式中,用户可以选择已有的形状。对于这个程序,定义了如下的快捷键:

快捷键 命令
CTRL+M Toggle between modes.
F1 Switch to draw mode.
F2 Switch to selection mode.

首先定义这些命令的标识符。标识符的值是任意的,你可以在头文件中定义这些符号常量,例如:

#define IDR_ACCEL1                      101
#define ID_TOGGLE_MODE                40002
#define ID_DRAW_MODE                  40003
#define ID_SELECT_MODE                40004

例子中,IDR_ACCEL1 标识符为加速表的标识,下面的常量定义了应用程序的三种命令。按照惯例,一般资源文件的常量标识被定义在名字叫做 resource.h 的头文件中。下面是资源定义文件的内容:

#include "resource.h"

IDR_ACCEL1 ACCELERATORS
{
    0x4D,   ID_TOGGLE_MODE, VIRTKEY, CONTROL    // ctrl-M
    0x70,   ID_DRAW_MODE, VIRTKEY               // F1
    0x71,   ID_SELECT_MODE, VIRTKEY             // F2
}

加速表中的快捷键是定义在花括号中的。每条快捷键都包含以下内容:

  • 快捷键指定的虚拟键盘码或 ASCII 字符。

  • 应用程序命令。这些符号常量会在应用程序中使用,资源定义文件包含 resource.h 文件,其中定义了这些应用程序命令对应的常量。

  • VIRTKEY 关键字意味着第一项使用的是虚拟键盘码而不是 ASCII 字符。

  • 可选的辅助键:ALT、CONTROL 或 SHIFT。

如果你使用 ASCII 字符定义快捷键,需要注意 ASCII 的大小写是不同的。(例如,按下 ‘a’ 和 ‘A’ 可能是两个不同的快捷命令)。正是因为这种问题的存在,所以一般推荐使用虚拟键盘码替代 ASCII 码。

2. 加速表的加载

加速表资源必须在使用它之前加载,可以使用 LoadAccelerators 函数加载一个加速表。

HACCEL hAccel = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDR_ACCEL1));

应用程序应该在进入消息循环之前调用它。函数的第一个参数表示应用程序句柄(WinMain 函数的参数),第二个参数指定加速表的资源标识。该函数成功则会返回对应的资源句柄,如果函数失败,则会返回 NULL。

当你不再使用加速表的时候,可以使用 DestoryAcceleratorTable 销毁它。不管程序在运行期间是否销毁加速表,当程序退出之前都会自动销毁,所以真实情况是只有在替换加速表的时候才会调用该函数进行销毁操作。

3. 将按键翻译为命令

在程序中加速表被用来将按键翻译为 WM_COMMAND 消息命令,WM_COMMAND 消息处理函数中,wParam 参数包含命令的标识。例如,使用上面定义的加速表,CTRL+M 按键将被翻译为 WM_COMMAND 消息,wParam 参数中则会被指定为 ID_TOGGLE_MODE 的值。想要命令被正确翻译,你需要改变程序的消息循环:

MSG msg;
while (GetMessage(&msg, NULL, 0, 0))
{
    if (!TranslateAccelerator(win.Window(), hAccel, &msg))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
}

在这段代码中,消息循环内部实时调用 TranslateAccelerator 函数,该函数内部测试每个窗口消息,如果监听到有键盘按下的消息,则会使用加速表中的快捷键进行匹配,如果比配成功,则会发送 WM_COMMAND 消息通知应用程序执行对应的命令。 TransolateAccelerator 函数如果成功检测到一个快捷键命令,则会返回非0值,这意味着你需要跳过正常消息的处理流程,不对该消息做其它处理,而将其翻译为一个 WM_COMMAND 命令消息。如果返回0,则按照正常逻辑处理该消息。

对于前面举例的加速表,WM_COMMAND 消息的处理代码可能如下:

case WM_COMMAND:
    switch (LOWORD(wParam))
    {
    case ID_DRAW_MODE:
        SetMode(DrawMode);
        break;

    case ID_SELECT_MODE:
        SetMode(SelectMode);
        break;

    case ID_TOGGLE_MODE:
        if (mode == DrawMode)
        {
            SetMode(SelectMode);
        }
        else
        {
            SetMode(DrawMode);
        }
        break;
    }
    return 0;

代码中 SetMode 函数假设用来切换当前模式,至于具体的命令处理逻辑还需要看你的程序本身来决定。

求关注