绑定按键事件

RegisterKeyNotify()是用来将特定按键输入事件和事件处理函数绑定的函数,如图3.2所示.

本节示例代码的目录为key-notify (日文版为020_simple_text_input_ex_register_key_notify)。

struct EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL {
    ...
    unsigned long long (*RegisterKeyNotify)(
        struct EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL *This,
        struct EFI_KEY_DATA *KeyData,
            /* 要绑定的按键事件 */
        unsigned long long (*KeyNotificationFunction)(
            struct EFI_KEY_DATA *KeyData),
            /* 指向要绑定的函数的指针 */
        void **NotifyHandle
            /* 返回的句柄,它的值唯一,在取消绑定时要用到 */
        );
    ...
};

图3.2: RegisterKeyNotify()相关的定义(位于efi.h中)

而这个函数的第二个参数类型struct EFI_KEY_DATA如图3.3所示。

struct EFI_KEY_DATA {
    /* 要绑定的按键 */
    struct EFI_INPUT_KEY {
        unsigned short ScanCode;
            /* 扫描码,用于Unicode范围外的按键 */
        unsigned short UnicodeChar;
            /* Unicode码,用于Unicode范围内的按键 */
    } Key;
    /* 要绑定的输入设备状态 */
    struct EFI_KEY_STATE {
        unsigned int KeyShiftState;
            /* Ctrl、Alt、Shift等键的状态(是否按下) */
        unsigned char KeyToggleState;
            /* 切换键状态,如NumLock、CapsLock是否被按下 */
    } KeyState;
};

图3.3: struct EFI_KEY_DATA的定义

EFI_KEY_DATA表示的按键事件不是单纯的某个按键被按下,而是“在某一特定状态下某个按键被按下”,因此它支持组合键。关于这部分更详细的内容,参见标准文档"11.2 Simple Text Input Ex Protocol(P.414)"。接下来的代码中,我们不考虑按下时键盘的状态,也就是说只会用到其中的EFI_INPUT_KEY这一部分,所以我们把EFI_KEY_STATE中的值都设为0。

图3.4展示了一个使用RegisterKeyNotify()的例子。

#include "efi.h"
#include "common.h"

unsigned char is_exit = FALSE;

unsigned long long key_notice(
    struct EFI_KEY_DATA *KeyData __attribute__ ((unused)))
{
    is_exit = TRUE;

    return EFI_SUCCESS;
}

void efi_main(void *ImageHandle __attribute__ ((unused)),
          struct EFI_SYSTEM_TABLE *SystemTable)
{
    unsigned long long status;
    struct EFI_KEY_DATA key_data = {{0, L'q'}, {0, 0}};
    void *notify_handle;

    efi_init(SystemTable);
    ST->ConOut->ClearScreen(ST->ConOut);

    puts(L"Waiting for the 'q' key input...\r\n");

    status = STIEP->RegisterKeyNotify(STIEP, &key_data, key_notice, &notify_handle);
    assert(status, L"RegisterKeyNotify");

    while (!is_exit);

    puts(L"exit.\r\n");

    while (TRUE);
}

图3.4: 使用RegisterKeyNotify()的例子

上面这段代码实现了按下"q"键来退出efi_main()中的一个循环的功能。这里要处理的事件是“按下q键”,因此我们先将key_data.Key.UnicodeChar设置为"q",并将key_data中的其它属性设为0。然后,我们使用RegisterKeyNotify()函数,将“按下q键”这一事件与事件处理函数key_notice()绑定。在key_notice()函数中,我们把全局变量is_exit设置为TRUE,这样就实现了按下q键跳出efi_main()while (!is_exit);这个循环的功能,如图3.5和图3.6所示。

等待q键输入时的情况

图3.5: 图3.4的程序等待q键输入时的情况

按下q键后的情况

图3.6: 按下q键后的情况

补充: 获取Unicode范围外的按键输入

对于Unicode范围外的按键输入(Esc键、方向键、功能键等),需要设置EFI_KEY_DATA.ScanCode为这个按键对应的扫描码。标准文档"Table 88. EFI Scan Codes for EFI_SIMPLE_TEXT_INPUT_PROTOCOL(P.410)"和"Table 89. EFI Scan Codes for EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL(P.411)"记录了各个按键对应的扫描码。表3.1是对它们的汇总。

扫描码按键扫描码按键
0x00Null0x15F11
0x010x16F12
0x020x68F13
0x030x69F14
0x040x6AF15
0x05Home0x6BF16
0x06End0x6CF17
0x07Insert0x6DF18
0x08Delete0x6EF19
0x09Page Up0x6FF20
0x0aPage Down0x70F21
0x0bF10x71F22
0x0cF20x72F23
0x0dF30x73F24
0x0eF40x7F静音
0x0fF50x80音量+
0x10F60x81音量-
0x11F70x100亮度+
0x12F80x101亮度-
0x13F90x102待机
0x14F100x103休眠
0x17Esc0x104屏幕开关
0x105恢复
0x106弹出
0x8000-0xFFFFOEM保留键

表3.1: 各按键对应的扫描码

查表3.1可知,如果要绑定Esc键,我们需要将EFI_KEY_DATA.Key.ScanCode设置为0x17。因此,如果要把图3.4中的程序改成“按下Esc键来退出循环”的话,我们只需要像图3.7这样修改第18行的变量key_data即可。

- struct EFI_KEY_DATA key_data = {{0, L'q'}, {0, 0}};
+ struct EFI_KEY_DATA esc_key_data = {{0x17, 0}, {0, 0}};

图3.7: 将q键改为Esc键所需要进行的修改