如何使用JavaScript检测是否同时按下多个键?

2022-08-30 01:22:28

我正在尝试开发一个JavaScript游戏引擎,我遇到了这个问题:

  • 当我按下角色跳跃时。SPACE
  • 当我按下角色时,角色向右移动。→

问题是,当我向右按然后按空格键时,字符会跳跃,然后停止移动。

我使用该功能按下键。如何检查是否同时按下多个键?keydown


答案 1

注意:keyCode 现已弃用。

如果您了解这个概念,多次击键检测很容易

我这样做的方式是这样的:

var map = {}; // You could also use an array
onkeydown = onkeyup = function(e){
    e = e || event; // to deal with IE
    map[e.keyCode] = e.type == 'keydown';
    /* insert conditional here */
}

此代码非常简单:由于计算机一次只传递一个击键,因此会创建一个数组来跟踪多个键。然后,可以使用该数组一次检查一个或多个键。

只是为了解释一下,假设您按 和 ,每个事件都会触发一个设置为 的值的事件,该事件的计算结果为 truefalse。现在两者都设置为 。当您放开 时,事件将触发,导致相同的逻辑确定 (A) 的相反结果,现在该结果为 false,但由于 (B) 仍处于“关闭”状态(它没有触发 keyup 事件),因此它仍为 trueABkeydownmap[e.keyCode]e.type == keydownmap[65]map[66]trueAkeyupmap[65]map[66]

通过这两个事件的数组如下所示:map

// keydown A 
// keydown B
[
    65:true,
    66:true
]
// keyup A
// keydown B
[
    65:false,
    66:true
]

您现在可以执行两项操作:

A)可以创建一个键盘记录器(示例)作为参考,以便以后当您想要快速找出一个或多个键代码时。假设您已经定义了一个 html 元素,并使用变量 指向它。element

element.innerHTML = '';
var i, l = map.length;
for(i = 0; i < l; i ++){
    if(map[i]){
        element.innerHTML += '<hr>' + i;
    }
}

注意:您可以按属性轻松抓取元素。id

<div id="element"></div>

这创建了一个html元素,可以在javascript中轻松引用element

alert(element); // [Object HTMLDivElement]

您甚至不必使用或抓住它。但为了兼容起见,更广泛地推荐使用jQuery。document.getElementById()$()$()

只需确保脚本标记位于 HTML 正文之后即可。优化提示:大多数大牌网站将脚本标签放在正文标签之后进行优化。这是因为脚本标记会阻止加载更多元素,直到其脚本下载完成。将其放在内容之前可以事先加载内容。

B(这是你的兴趣所在)您可以在原来的位置一次检查一个或多个键,以此示例为例:/*insert conditional here*/

if(map[17] && map[16] && map[65]){ // CTRL+SHIFT+A
    alert('Control Shift A');
}else if(map[17] && map[16] && map[66]){ // CTRL+SHIFT+B
    alert('Control Shift B');
}else if(map[17] && map[16] && map[67]){ // CTRL+SHIFT+C
    alert('Control Shift C');
}

编辑:这不是最易读的片段。可读性很重要,因此您可以尝试这样的事情,以使其更容易看到:

function test_key(selkey){
    var alias = {
        "ctrl":  17,
        "shift": 16,
        "A":     65,
        /* ... */
    };

    return key[selkey] || key[alias[selkey]];
}

function test_keys(){
    var keylist = arguments;

    for(var i = 0; i < keylist.length; i++)
        if(!test_key(keylist[i]))
            return false;

    return true;
}

用法:

test_keys(13, 16, 65)
test_keys('ctrl', 'shift', 'A')
test_key(65)
test_key('A')

这样更好吗?

if(test_keys('ctrl', 'shift')){
    if(test_key('A')){
        alert('Control Shift A');
    } else if(test_key('B')){
        alert('Control Shift B');
    } else if(test_key('C')){
        alert('Control Shift C');
    }
}

(编辑结束)


此示例检查 、 和CtrlShiftACtrlShiftBCtrlShiftC

就这么简单 :)

笔记

跟踪键代码

作为一般规则,最好记录代码,特别是像Key代码(如)这样的东西,这样你就可以记住它们是什么。 // CTRL+ENTER

您还应该按照与文档相同的顺序放置密钥代码(,NOT )。这样,当您需要返回并编辑代码时,您就不会感到困惑。CTRL+ENTER => map[17] && map[13]map[13] && map[17]

一个陷阱与 if-else 链

如果检查不同数量的连击(如和),则在较大的连击之后放置较小的连击,否则较小的连击将覆盖较大的连击(如果它们足够相似)。例:CtrlShiftAltEnterCtrlEnter

// Correct:
if(map[17] && map[16] && map[13]){ // CTRL+SHIFT+ENTER
    alert('Whoa, mr. power user');
}else if(map[17] && map[13]){ // CTRL+ENTER
    alert('You found me');
}else if(map[13]){ // ENTER
    alert('You pressed Enter. You win the prize!')
}

// Incorrect:
if(map[17] && map[13]){ // CTRL+ENTER
    alert('You found me');
}else if(map[17] && map[16] && map[13]){ // CTRL+SHIFT+ENTER
    alert('Whoa, mr. power user');
}else if(map[13]){ // ENTER
    alert('You pressed Enter. You win the prize!');
}
// What will go wrong: When trying to do CTRL+SHIFT+ENTER, it will
// detect CTRL+ENTER first, and override CTRL+SHIFT+ENTER.
// Removing the else's is not a proper solution, either
// as it will cause it to alert BOTH "Mr. Power user" AND "You Found Me"

Gotcha:“即使我没有按下键,此按键组合也会继续激活”

在处理警报或从主窗口获取焦点的任何内容时,您可能希望包括在条件完成后重置数组。这是因为有些事情,比如 ,将焦点从主窗口移开,导致 'keyup' 事件不触发。例如:map = []alert()

if(map[17] && map[13]){ // CTRL+ENTER
    alert('Oh noes, a bug!');
}
// When you Press any key after executing this, it will alert again, even though you 
// are clearly NOT pressing CTRL+ENTER
// The fix would look like this:

if(map[17] && map[13]){ // CTRL+ENTER
    alert('Take that, bug!');
    map = {};
}
// The bug no longer happens since the array is cleared

陷阱:浏览器默认值

这是我发现的一个令人讨厌的事情,包括解决方案:

问题:由于浏览器通常对关键组合具有默认操作(例如激活书签窗口,或在maxthon上激活skynote),您可能还想在之后添加,因此当您网站的用户在放置“复制文件”功能时不会感到沮丧,而是将页面添加为书签。CtrlDCtrlShiftCreturn falsemap = []CtrlD

if(map[17] && map[68]){ // CTRL+D
    alert('The bookmark window didn\'t pop up!');
    map = {};
    return false;
}

如果没有 ,书签窗口弹出,令用户感到沮丧。return false

退货声明(新增)

好吧,所以你并不总是想在那个时候退出函数。这就是该功能存在的原因。它所做的是设置一个内部标志,告诉解释器不允许浏览器运行其默认操作。之后,函数的执行将继续(而将立即退出函数)。event.preventDefault()return

在决定是否使用或return falsee.preventDefault()

event.keyCode已弃用

用户SeanVieira在评论中指出了已弃用的内容。event.keyCode

在那里,他给出了一个很好的替代方案:,它返回被按下的键的字符串表示,如 for 或 for 。event.key"a"A"Shift"Shift

我继续准备了一个检查所述字符串的工具。

element.oneventelement.addEventListener

注册到 的处理程序可以堆叠,并按注册顺序调用,而直接设置则相当激进,会覆盖您以前拥有的任何内容。addEventListener.onevent

document.body.onkeydown = function(ev){
    // do some stuff
    ev.preventDefault(); // cancels default actions
    return false; // cancels this function as well as default actions
}

document.body.addEventListener("keydown", function(ev){
    // do some stuff
    ev.preventDefault() // cancels default actions
    return false; // cancels this function only
});

该属性似乎覆盖了所有内容和行为,并且可能相当不可预测。.oneventev.preventDefault()return false;

在任何一种情况下,通过注册的处理程序似乎更容易编写和推理。addEventlistener

还有来自Internet Explorer的非标准实现,但这已经不推荐使用,甚至与JavaScript无关(它与一种名为JScript的深奥语言有关)。尽可能避免使用多语言代码符合您的最佳利益。attachEvent("onevent", callback)

帮助程序类

为了解决混淆/抱怨,我写了一个“类”来做这个抽象(pastebin链接):

function Input(el){
    var parent = el,
        map = {},
        intervals = {};
    
    function ev_kdown(ev)
    {
        map[ev.key] = true;
        ev.preventDefault();
        return;
    }
    
    function ev_kup(ev)
    {
        map[ev.key] = false;
        ev.preventDefault();
        return;
    }
    
    function key_down(key)
    {
        return map[key];
    }

    function keys_down_array(array)
    {
        for(var i = 0; i < array.length; i++)
            if(!key_down(array[i]))
                return false;

        return true;
    }
    
    function keys_down_arguments()
    {
        return keys_down_array(Array.from(arguments));
    }
    
    function clear()
    {
        map = {};
    }
    
    function watch_loop(keylist, callback)
    {
        return function(){
            if(keys_down_array(keylist))
                callback();
        }
    }

    function watch(name, callback)
    {
        var keylist = Array.from(arguments).splice(2);

        intervals[name] = setInterval(watch_loop(keylist, callback), 1000/24);
    }

    function unwatch(name)
    {
        clearInterval(intervals[name]);
        delete intervals[name];
    }

    function detach()
    {
        parent.removeEventListener("keydown", ev_kdown);
        parent.removeEventListener("keyup", ev_kup);
    }
    
    function attach()
    {
        parent.addEventListener("keydown", ev_kdown);
        parent.addEventListener("keyup", ev_kup);
    }
    
    function Input()
    {
        attach();

        return {
            key_down: key_down,
            keys_down: keys_down_arguments,
            watch: watch,
            unwatch: unwatch,
            clear: clear,
            detach: detach
        };
    }
    
    return Input();
}

这个类不会做所有的事情,它不会处理所有可以想象的用例。我不是图书馆的人。但对于一般的交互式使用,它应该没问题。

若要使用此类,请创建一个实例并将其指向要与键盘输入关联的元素:

var input_txt = Input(document.getElementById("txt"));

input_txt.watch("print_5", function(){
    txt.value += "FIVE ";
}, "Control", "5");

这将做的是将一个新的输入侦听器附加到元素 with (让我们假设它是一个文本区域),并为键组合设置一个观察点。当两者都关闭时,将调用传入的回调函数(在本例中为添加到文本区域的函数)。回调与名称相关联,因此要将其删除,您只需使用:#txtCtrl+5Ctrl5"FIVE "print_5

input_txt.unwatch("print_5");

要与元素分离:input_txttxt

input_txt.detach();

这样,垃圾回收可以拾取对象 (),如果它被扔掉,并且你不会剩下旧的僵尸事件侦听器。input_txt

为了彻底起见,这里是该类API的快速参考,以C / Java风格呈现,因此您知道它们返回的内容以及它们期望的参数。

Boolean  key_down (String key);

如果为 down,则返回,否则返回 false。truekey

Boolean  keys_down (String key1, String key2, ...);

如果所有键都已关闭,则返回,否则为 false。truekey1 .. keyN

void     watch (String name, Function callback, String key1, String key2, ...);

创建一个“观察点”,以便全部按 将触发回调keyN

void     unwatch (String name);

通过其名称删除所述观察点

void     clear (void);

擦除“键向下”缓存。相当于以上map = {}

void     detach (void);

将 和 侦听器与父元素分离,从而可以安全地删除实例ev_kdownev_kup

更新 2017-12-02为了响应将此内容发布到github的请求,我创建了一个要点

更新 2018-07-21我玩声明式编程已经有一段时间了,这种方式现在是我个人最喜欢的:小提琴pastebin。

通常,它可以处理您实际想要的情况(ctrl,alt,shift),但是如果您需要同时点击,例如,将这些方法“组合”到多键查找中并不太困难。a+w


我希望这个彻底解释的答案迷你博客对:)


答案 2
document.onkeydown = keydown; 

function keydown (evt) { 

    if (!evt) evt = event; 

    if (evt.ctrlKey && evt.altKey && evt.keyCode === 115) {

        alert("CTRL+ALT+F4"); 

    } else if (evt.shiftKey && evt.keyCode === 9) { 

        alert("Shift+TAB");

    } 

}