如何使用 JavaScript 检测是否一次按下多个键?

IT技术 javascript keydown
2021-01-31 03:27:23

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

  • 当我按下SPACE字符跳跃。
  • 当我按下字符向右移动时。

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

我使用该keydown函数来按下键。如何检查是否同时按下了多个键?

6个回答

注意: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 */
}

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

只是为了解释一下,假设您按下AB,每个都会触发一个keydown事件,事件设置map[e.keyCode]为 的值e.type == keydown,该事件的计算结果为truefalse现在两者map[65]map[66]都设置为true当你放开 时Akeyup事件触发,导致相同的逻辑来确定map[65](A)的相反结果,现在是false,但由于map[66](B) 仍然“向下”(它没有触发 keyup 事件),它仍然如此

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>

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

alert(element); // [Object HTMLDivElement]

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

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

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)之类的东西,这样您就可以记住它们是什么。

您还应该按照与文档相同的顺序放置密钥代码 ( CTRL+ENTER => map[17] && map[13], NOT 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"

问题:“即使我没有按下按键,这个组合键也会一直激活”

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

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

陷阱:浏览器默认值

这是我发现的一件烦人的事情,解决方案包括:

问题:由于浏览器通常对按键组合具有默认操作(例如CtrlD激活书签窗口,或CtrlShiftC在 maxthon 上激活 Skynote),您可能还想添加return falseafter map = [],这样您网站的用户就不会在出现“重复文件”时感到沮丧功能,被放置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已弃用。

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

我继续编写了一个用于检查所述字符串工具

element.onevent 对比 element.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
});

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

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

还有attachEvent("onevent", callback)来自 Internet Explorer 的非标准实现,但这已经过时了,甚至不属于 JavaScript(它属于一种名为JScript的深奥语言)。尽可能避免多语言代码符合您的最大利益。

一个帮手类

为了解决混淆/投诉,我编写了一个“类”来进行这种抽象(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");

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

input_txt.unwatch("print_5");

input_txttxt元素分离

input_txt.detach();

这样,垃圾收集可以捡起对象 ( input_txt),如果它被扔掉,你就不会剩下一个旧的僵尸事件侦听器。

为全面起见,这里是类 API 的快速参考,以 C/Java 风格呈现,因此您知道它们返回什么以及它们期望什么参数。

Boolean  key_down (String key);

true如果key关闭则返回,否则返回false。

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

true如果所有键key1 .. keyN都按下,则返回,否则返回false。

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 的请求,我创建了一个gist

更新 2018-07-21我一直在玩声明式编程一段时间,现在这种方式是我个人的最爱:fiddlepastebin

通常,它适用于您实际想要的情况(ctrl、alt、shift),但是如果您需要同时点击a+w,那么将这些方法“组合”成一个多键查找。


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

当您在文档处于焦点时按下/按住一个键,然后单击 URL 框,然后松开该键会怎样。keyup 永远不会被触发,但 key 是 up 的,导致列表不正确。反之亦然:在 URL 框中按下/按住键,从不触发 keydown,然后将焦点放在文档上,并且 keydown 状态不在列表中。基本上,每当文档重新获得焦点时,您就永远无法确定关键状态。
2021-03-15 03:27:23
@SeanVieira 再说一次,你也可以用 C 做一些奇怪的事情。例如,您是否知道这myString[5]与 相同5[myString],并且它甚至不会给您编译警告(即使使用-Wall -pedantic)?这是因为pointer[offset]表示法采用指针,加上偏移量,然后解引用结果,myString[5]*(myString + 5).
2021-03-22 03:27:23
@inorganik 你指的是辅助类吗?要点可以像 repos 一样使用吗?为一小段代码制作一个完整的 repo 会很乏味。当然,我会做一个要点。今晚我会拍摄。午夜山时间-ish
2021-04-07 03:27:23
我刚刚对这个答案进行了重大更新!键盘记录器示例更加连贯,我更新了格式,以便“注释”部分更易于阅读,并且我添加了一个关于return falsevs的新注释preventDefault()
2021-04-09 03:27:23
注意:keyCode不推荐使用 - 如果您切换到key那么您将获得键的实际字符表示,这可能很好。
2021-04-09 03:27:23
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");

    } 

}

您应该使用keydown事件来跟踪按下的键,并且应该使用keyup事件来跟踪何时释放键。

看这个例子:http : //jsfiddle.net/vor0nwe/mkHsU/

(更新:我在这里复制代码,以防 jsfiddle.net 保释 :) HTML:

<ul id="log">
    <li>List of keys:</li>
</ul>

...和 ​​Javascript(使用 jQuery):

var log = $('#log')[0],
    pressedKeys = [];

$(document.body).keydown(function (evt) {
    var li = pressedKeys[evt.keyCode];
    if (!li) {
        li = log.appendChild(document.createElement('li'));
        pressedKeys[evt.keyCode] = li;
    }
    $(li).text('Down: ' + evt.keyCode);
    $(li).removeClass('key-up');
});

$(document.body).keyup(function (evt) {
    var li = pressedKeys[evt.keyCode];
    if (!li) {
       li = log.appendChild(document.createElement('li'));
    }
    $(li).text('Up: ' + evt.keyCode);
    $(li).addClass('key-up');
});

在那个例子中,我使用一个数组来跟踪按下了哪些键。在实际应用程序中,您可能希望在delete每个元素的关联键被释放后访问它们。

请注意,虽然我在本示例中使用 jQuery 使事情变得简单,但该概念在使用“原始”Javascript 时同样适用。

@Cristy:那么您还可以添加一个onblur事件处理程序,它从数组中删除所有按下的键。一旦您失去焦点,就必须再次按下所有键。不幸的是,没有等效于GetKeyboardState.
2021-03-16 03:27:23
@JamesAlday 同样的问题。它显然只影响 Mac 上的 Meta (OS) 键。请参阅此处的问题 #3:bitspushedaround.com/...
2021-03-24 03:27:23
但正如我所认为的那样,有一个错误。如果您按住一个按钮,然后切换到另一个选项卡(或松散焦点),同时在您重新聚焦在脚本上时仍按住该按钮,它会显示该按钮被按下,即使它没有被按下。:D
2021-03-25 03:27:23
在 Mac (Chrome) 上粘贴有问题。它成功地获得了 keydown 91(命令),keydown 86(v),但随后只输入了 91,留下了 86。按键列表:向上:91,向下:86。这似乎只有在第二次松开命令键时才会发生 - 如果我先松开它,它会在两者上正确注册 keyup。
2021-03-29 03:27:23
看起来,当您一次按下三个或更多键时,它会停止检测任何按下的键,直到您抬起一个键。(使用 Firefox 22 测试)
2021-04-02 03:27:23

谁需要完整的示例代码。右+左添加

var keyPressed = {};
document.addEventListener('keydown', function(e) {

   keyPressed[e.key + e.location] = true;

    if(keyPressed.Shift1 == true && keyPressed.Control1 == true){
        // Left shift+CONTROL pressed!
        keyPressed = {}; // reset key map
    }
    if(keyPressed.Shift2 == true && keyPressed.Control2 == true){
        // Right shift+CONTROL pressed!
        keyPressed = {};
    }

}, false);

document.addEventListener('keyup', function(e) {
   keyPressed[e.key + e.location] = false;

   keyPressed = {};
}, false);
这是我的赢家。简洁,功能齐全。如果您需要知道您按下的键的名称,只需在 keydown 函数的末尾记录 keyPressed 对象。谢谢你,朋友!
2021-03-25 03:27:23

我使用这种方式(必须检查 Shift + Ctrl 按下的位置):

// create some object to save all pressed keys
var keys = {
    shift: false,
    ctrl: false
};

$(document.body).keydown(function(event) {
// save status of the button 'pressed' == 'true'
    if (event.keyCode == 16) {
        keys["shift"] = true;
    } else if (event.keyCode == 17) {
        keys["ctrl"] = true;
    }
    if (keys["shift"] && keys["ctrl"]) {
        $("#convert").trigger("click"); // or do anything else
    }
});

$(document.body).keyup(function(event) {
    // reset status of the button 'released' == 'false'
    if (event.keyCode == 16) {
        keys["shift"] = false;
    } else if (event.keyCode == 17) {
        keys["ctrl"] = false;
    }
});