首页 | 新闻 | 新品 | 文库 | 方案 | 视频 | 下载 | 商城 | 开发板 | 数据中心 | 座谈新版 | 培训 | 工具 | 博客 | 论坛 | 百科 | GEC | 活动 | 主题月 | 电子展
返回列表 回复 发帖

在基于 Web 的 VNC 应用程序中支持多种键盘布局(4)

在基于 Web 的 VNC 应用程序中支持多种键盘布局(4)

有效的实现浏览器支持状况截至编写本文时,  都支持                    KeyboardEvent.code 属性。Microsoft Edge   将此 API 列入 “考虑” 范围。对于任何不支持 KeyboardEvent.code                    的浏览器,VNC Web 客户端必须禁用 QEMU VNC 扩展,退而使用默认的 RFB KeyEvent                    消息,或者找到另一种方法来使用可用的 KeyboardEvent 属性获取不依赖于布局的实体键。

扩展的 QEMU KeyEvent 消息已在多个桌面 VNC 客户端中良好地建立和实现。既然                KeyboardEvent.code 属性使恢复按下的实体键成为可能,那么 VNC Web                客户端就没有理由不采用相同方式实现该扩展。我为 noVNC 项目实现的解决方案可供任何基于 Web 的 VNC 客户端使用。
忽略 keypressed 事件我在解决方案中选择了忽略 keypressed 事件。这些事件仅在一个或多个 keypressed                事件生成一个可读字符(一个键符)时触发。检测到来自支持 QEMU VNC 扩展的客户端的连接时,QEMU VNC                服务器会(在大多数时候,我稍后将讨论)忽略消息的 keysym 字段,仅依靠 keycode                字段在虚拟机中模拟 XT 扫描码。
代码实现我设计的完整、有效的实现可在 GitHub 上获得。

在这里,我将重点介绍一些特别值得注意的细节。
如何将 KeyboardEvent.code 转换为                xt_scancodeKeyboardEvent.code 提供了键的物理位置,但未使用可直接用在 RFB                消息中的格式。以下是该属性的可能值的一个示例:
'Esc' key:  xt_scancode 0x0001 keyboardevent.code = "Escape"
Spacebar: xt_scancode 0x0039 keyboardevent.code = "Space"
'F1' key: xt_scancode 0x003B keyboardevent.code = "F1"




我的实现使用了这篇有关 KeyboardEvent.code 的 Mozilla Developer Network   中提供的表,创建一个将 KeyboardEvent.code 值转换为相应的                xt_scancode 的哈希表,例如:
XT_scancode["Escape"] = 0x0001;
XT_scancode["Space"] = 0x0039;
XT_scancode["F1"] = 0x003B;




创建 QEMU RFB KeyEvent 消息将 buff 视为一个大小为 12 的字节数组:
buff[offset] = 255; // msg-type
buff[offset + 1] = 0; // sub msg-type

buff[offset + 2] = (down >> 8);
buff[offset + 3] = down;

buff[offset + 4] = (keysym >> 24);
buff[offset + 5] = (keysym >> 16);
buff[offset + 6] = (keysym >> 8);
buff[offset + 7] = keysym;

var RFBkeycode = getRFBkeycode(keycode)

buff[offset + 8] = (RFBkeycode >> 24);
buff[offset + 9] = (RFBkeycode >> 16);
buff[offset + 10] = (RFBkeycode >> 8);
buff[offset + 11] = RFBkeycode;




数据结构与   类似,这绝非偶然。在本代码中,keycode 是从                keyboardevent.code 值转换得到的                xt_scancode,keysym 是一个 0 字段(大部分情况下如此)。
getRFBkeycode() 函数将 XT_scancode 转换为 QEMU VNC                扩展定义的格式:
function getRFBkeycode(xt_scancode) {
    var upperByte = (keycode >> 8);
    var lowerByte = (keycode & 0x00ff);
    if (upperByte === 0xe0 && lowerByte < 0x7f) {
        lowerByte = lowerByte | 0x80;
        return lowerByte;
    }
    return xt_scancode
}




NumLock 的独特情况:键符发挥作用的时刻我提到过键符基本上被忽略。在至少一种情况下,QEMU VNC                服务器考虑键符:当使用数字键盘 (Numpad) 中的键时。
在我的解决方案的第一个实现中(忽略 QEMU KeyEvent 消息的键符字段),出现了一种奇怪的行为:当按下任何多用途数字键盘键时,比如                0、1、2、3、4、6、7、8、9 或小数点(en_US 布局中的句点),即使虚拟机和客户端上的 NumLock 状态为                ON,QEMU VNC 服务器也会:
  • 将虚拟机的 NumLock 状态更改为 OFF(如果它为 ON)
  • 按键
例如,在客户端和虚拟机上的 NumLock 状态为 ON 时按数字键盘键 8,会将虚拟机中的 NumLock 状态更改为                OFF,然后执行向上箭头键的操作。在 NumLock 状态为 OFF 时按数字键盘键 8                的行为才是符合预期的。
此问题可通过可靠方式利用客户端和虚拟机的 NumLock 状态来解决。但远程 QEMU VNC 服务器不可能知道客户端键盘的 NumLock                状态。服务器可以看到何时按下/释放 NumLock 键,但无从了解当前的 NumLock 状态,因为 QEMU VNC                KeyEvent 消息未传递该信息。
经过在桌面 VNC 客户端上广泛测试后,我认识到在这些环境中发送了键符。尽管键码不会基于 NumLock                状态而发生更改,但键符会受到影响。结论是,QEMU VNC 服务器使用键符字段来猜测客户端的 NumLock                状态,并采取相应行动来尝试同步虚拟机状态。在实现中,发送的键符为 0 时,服务器将此解释为 “客户端的 NumLock 状态为                OFF”,强制将客户端 NumLock 状态更改为 OFF,然后发送按下的键码。
因为如果不发送键符,会默认为 NumLock 状态为 OFF,所以解决方案是仅在 NumLock 状态为                ON 时发送键符。
发送数字键盘的键符生成键符的键盘事件是 keypressed 事件,我的解决方案忽略了该事件。那么如何将键符应用于 QEMU                KeyEvent 消息?
幸运的是,确定键符不是一定需要 keypressed                事件。数字键盘在所有布局中都是标准的(否则,如果没有键盘布局图,QEMU VNC 服务器就无法猜测 NumLock                状态)。所以,数字键盘键的键符值可预先确定。
这就留下了一个问题,如果不使用 keypress 事件,如何区分数字键 7 用作 Home 键的情况和用作数字 7                的情况。我的实现使用了 KeyboardEvent.keyCode 属性(在 keydown                事件上设置)来进行区分,如下面的代码片段所示。
下面的函数接收一个键盘事件 evt,并将 KeyboardEvent.code                值与属于数字键盘的值相比较:
function isNumPadMultiKey(evt) {
    var numPadCodes = ["Numpad0", "Numpad1", "Numpad2",
        "Numpad3", "Numpad4", "Numpad5", "Numpad6",
        "Numpad7", "Numpad8", "Numpad9", "NumpadDecimal"];
    return (numPadCodes.indexOf(evt.code) !== -1);
}




我使用前面的函数来查看是否需要对某个指定的键盘事件进行任何特殊处理。
下面的函数接收一个键盘事件 evt,并将它的 keyboardevent.keyCode                属性与一个名为 numLockOnKeyCodes 的预定义值集相比较:
function getNumPadKeySym(evt) {
    var numLockOnKeySyms = {
        "Numpad0": 0xffb0, "Numpad1": 0xffb1, "Numpad2": 0xffb2,
        "Numpad3": 0xffb3, "Numpad4": 0xffb4, "Numpad5": 0xffb5,
        "Numpad6": 0xffb6, "Numpad7": 0xffb7, "Numpad8": 0xffb8,
        "Numpad9": 0xffb9, "NumpadDecimal": 0xffac
    };
    var numLockOnKeyCodes = [96, 97, 98, 99, 100, 101, 102,
        103, 104, 105, 108, 110];

    if (numLockOnKeyCodes.indexOf(evt.keyCode) !== -1) {
        return numLockOnKeySyms[evt.code];
    }
    return 0;




在 NumLock ON 状态下,numLockOnKeyCodes 值对应于数字键盘键 0 到                9 和小数点。如果 evt.keyCode 是这些值之一,那么该函数会返回                numLockOnKeySyms 提供的等效键符;否则,它会返回 0。
以下是在代码内调用这些函数的方式:
result.code = evt.code;
result.keysym = 0;

if (isNumPadMultiKey(evt)) {
    result.keysym = getNumPadKeySym(evt);
}




在此代码中,result 是在处理过程中传递的对象。这样,解决方案就可以确保正确处理 NumLock 键。
AltGR 和 Windows我在 Windows 10 上运行的所有支持的浏览器(Chrome、Firefox 和 Opera)中测试 noVNC                解决方案时出现了另一个异常:AltGR 修饰键在 Linux 虚拟机上未按预期工作。
通过调试代码,我发现, AltGR 键通过两条 KeyEvent 消息发送到 QEMU VNC                服务器,而不是一条消息。第一条消息是一个左 Ctrl 键;第二条消息是一个右 Alt 键 — 与您期望某人按下左 Ctrl 后立即按右                Alt 的效果相同。当客户端在 Linux PC 中运行时,发送 AltGR 键作为右 Alt。
  是有历史原因的。长话短说:旧的美国键盘没有 AltGR 键,Windows 最初使用左 Ctrl + 右 Alt 来模拟它。此解决方案适合没有                AltGR 键的键盘,但在使用有 AltGR 的键盘时可能带来误导。
一个解决方案是记录此行为,并强制用户删除此默认映射。另一个是我选择的解决方案 — 用于处理 noVNC 中的行为。我的代码包含对按左                Ctrl 后按右 Alt 的组合的特殊处理:
if (state.length > 0 && state[state.length-1].code == 'ControlLeft') {
     if (evt.code !== 'AltRight') {
         next({code: 'ControlLeft', type: 'keydown', keysym: 0});
     } else {
         state.pop();
     }
}
             (...)
            if (evt.code !== 'ControlLeft') {
next(evt);
            }




此代码告诉 noVNC:在 keydown 事件中,如果 KeyboardEvent.code                等于 ControlLeft,则不要立即转发该事件。等待第二个 keydown                事件,并验证它的代码是否等于 AltRight,这意味着浏览器收到了一个左 Ctrl + 右 Alt                的组合,这可能意味着在 Windows 浏览器中按下了 AltGR 键。在这种情况下,丢弃左 Ctrl,仅转发右 Alt,这是 Linux                中的默认行为。这种处理使 AltGR 键能按预期工作,甚至在 Windows 浏览器中也是如此。
此方法的缺点是,即使用户合理地按下了左 Ctrl + 右 Alt 的组合,也不会转发该组合。我将此视为可接受的缺点,因为左 Ctrl + 右 Alt                不是一种常用的组合键(左 Ctrl + 左 Alt 和右 Ctrl + 右 Alt 容易键入得多)。适用性影响极小,而且用户不需要在 Windows                重新配置键盘布局图。
弃用的属性我的实现的另一个已知缺陷,是一个用于处理 NumLock 问题的属性:
if (numLockOnKeyCodes.indexOf(evt.keyCode) !== -1) {




KeyboardEvent.keyCode(连同 which 和                charCode,可在 “ ” 部分看到)自 2015                年以来已被  。但是,当时在大部分浏览器中没有实现应在它们的位置使用的属性                KeyboardEvent.key(而且在编写本文时,所有 Safari 版本和 Chrome                移动版本仍不支持它)。所有这些弃用的属性被广泛用在 noVNC                和其他任何需要键盘控制的应用程序中。我不希望浏览器很快丢弃这些属性,但依靠一个弃用的属性不是推荐做法。我强烈建议受影响应用程序的开发人员将                keyCode、which 和 charCode 重构为新的                KeyboardEvent.key API。
返回列表