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


173

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

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

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

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


3
这是一个网页演示,该网页自动打印所有按下的键的列表:stackoverflow.com/a/13651016/975097
Anderson Green

Answers:


327

注意:现已弃用 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。当您放开时A,该keyup事件将触发,并导致相同的逻辑来确定map[65](A)的相反结果,该结果现在为false,但是由于map[66](B)仍处于“按下”状态(尚未触发键入事件),它仍然是真的

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

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

您现在可以做两件事:

A)可以创建Key logger(示例)作为参考,以便以后您想快速找出一个或多个键代码时使用。假设您已经定义了一个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]

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

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

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

就这么简单:)

笔记

跟踪键码

通常,对代码进行文档记录是一种很好的做法,尤其是诸如键代码之类的东西(如 // 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 false或区分之前,请先了解这种区别。e.preventDefault()

event.keyCode 不推荐使用

用户SeanVieiraevent.keyCode已弃用的评论中指出。

在那里,他给出了一个极好的选择:event.key,它返回所按下键的字符串表示形式,例如"a"for A"Shift"forShift

我继续做饭,准备了一种检查所说弦的工具

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
});

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

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

还有attachEvent("onevent", callback)Internet Explorer的非标准实现,但这已被弃用,甚至与JavaScript不相关(它与称为JScript的深奥语言有关)。您的最大利益就是尽可能避免使用多语种代码。

帮手班

为了解决混乱/投诉,我编写了一个进行此类抽象的“类”(pastebin link):

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(假设它是一个textarea),并为key组合设置一个观察点Ctrl+5。当Ctrl5都关闭时,将调用您传入的回调函数(在本例中为添加"FIVE "到textarea 的函数)。回调与名称相关联print_5,因此只需将其删除即可:

input_txt.unwatch("print_5");

input_txttxt元素分离:

input_txt.detach();

这样,垃圾收集可以捡起对象(input_txt),如果将其丢弃,则不会留下旧的僵尸事件侦听器。

为了更全面,这里是C / Java样式的类API的快速参考,因此您知道它们返回的内容以及期望的参数。

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我已经玩了一段时间的声明式样式编程,这种方式现在是我个人最喜欢的方式:小提琴手pastebin

通常,它可以与您实际想要的情况(ctrl,alt,shift)一起使用,但是如果您需要同时打,例如,a+w将这些方法“组合”为一个结构并不难。多键查找。


我希望这个详尽解释的答案迷你博客对您有所帮助:)


我刚刚对这个答案做了很大的更新!键盘记录程序示例更加连贯,我更新了格式,以便“注释”部分更易于阅读,并且我添加了关于return falsevspreventDefault()
Braden Best

当您按住文档中的键时,然后单击URL框,然后放开键,该怎么办。永远不会触发keyup,但是key已打开,从而导致列表不正确。反之亦然:按下/按住URL框,从不触发keydown,然后将焦点放在文档上,keydown状态不在列表中。基本上,只要文档重新获得焦点,您就永远无法确定关键状态。
user3015682

3
注意:keyCode不推荐使用-如果您切换到key该位置,则可以得到按键的实际字符表示,这可能很好。
肖恩·维埃拉

1
@SeanVieira然后,您也可以在C中做一些奇怪的事情。例如,您是否知道myString[5]与相同5[myString],并且甚至不会给出编译警告(即使使用-Wall -pedantic)?这是因为pointer[offset]符号使用了指针,添加了偏移量,然后取消了对结果的引用,使其myString[5]与相同*(myString + 5)
Braden Best

1
@inorganik是指助手类吗?要点可以像回购协议一样使用吗?为一小段代码进行整个回购将很乏味。当然,我要提出要点。我今晚要拍。午夜山区时间-ish
Braden Best

30

您应该使用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中工作时同样有效。


但正如我认为的那样,这里有一个错误。如果按住一个按钮,然后切换到另一选项卡(或松散焦点),而在重新聚焦于scrit时仍然按住该按钮,则即使没有按下该按钮,也会显示该按钮已被按下。:D
XCS

3
@Cristy:然后您还可以添加一个onblur事件处理程序,该事件处理程序将从数组中删除所有按下的键。一旦失去焦点,就必须再次按所有键。不幸的是,没有JS等效于GetKeyboardState
Martijn

1
在Mac(Chrome)上粘贴时遇到问题。它成功地按下了按键91(命令),按下了按键86(v),但随后只按下了91,而按下了86。按键列表:向上:91,向下:86。这似乎仅在第二次放开命令键时发生-如果我先放开它,则会正确注册这两个键。
詹姆斯·阿尔迪

2
看起来,当您一次按三个或更多键时,它将停止向下检测任何其他键,直到您将其抬起为止。(使用Firefox 22测试)
Qvcool

1
@JamesAlday同样的问题。显然,它仅影响Mac上的Meta(OS)密钥。请参阅此处的问题3:bitspushedaround.com/…–
唐·

20
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");

    } 

}

1
这就是我想要的,最好的答案
Randall Coding,

7

我用这种方式(必须检查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;
    }
});

5

对于谁需要完整的示例代码。向右向左添加

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);

3

使keydown甚至调用多个功能,每个功能检查一个特定的键并做出适当的响应。

document.keydown = function (key) {

    checkKey("x");
    checkKey("y");
};

2

我会尝试在添加keypress Event处理程序keydown。例如:

window.onkeydown = function() {
    // evaluate key and call respective handler
    window.onkeypress = function() {
       // evaluate key and call respective handler
    }
}

window.onkeyup = function() {
    window.onkeypress = void(0) ;
}

这只是为了说明一种模式。我在这里不做详细介绍(尤其是不针对特定于浏览器的level2 + Event注册)。

请发回是否有帮助。


1
这是行不通的:按键做了很多的键是不的keydown触发和KEYUP 触发。另外,并非所有浏览器都会重复触发按键事件。
Martijn

Quirksmode说错了:quirksmode.org/dom/events/keys.html。但是我不会争论,因为我没有测试我的建议。
FK82 2011年

从该页面引用:“当用户按下特殊键(例如箭头键)时,浏览器不应触发按键事件”。至于重复,它将Opera和Konqueror列为未正确执行的操作。
Martijn

2

如果按下的键之一是Alt / Crtl / Shift,则可以使用以下方法:

document.body.addEventListener('keydown', keysDown(actions) );

function actions() {
   // do stuff here
}

// simultaneous pressing Alt + R
function keysDown (cb) {
  return function (zEvent) {
    if (zEvent.altKey &&  zEvent.code === "KeyR" ) {
      return cb()
    }
  }
}

2
    $(document).ready(function () {
        // using ascii 17 for ctrl, 18 for alt and 83 for "S"
        // ctr+alt+S
        var map = { 17: false, 18: false, 83: false };
        $(document).keyup(function (e) {
            if (e.keyCode in map) {
                map[e.keyCode] = true;
                if (map[17] && map[18] && map[83]) {
                    // Write your own code here, what  you want to do
                    map[17] = false;
                    map[18] = false;
                    map[83] = false;
                }
            }
            else {
                // if u press any other key apart from that "map" will reset.
                map[17] = false;
                map[18] = false;
                map[83] = false;
            }
        });

    });

感谢您的贡献。请尽量不要只是发布代码,添加一些解释。
蒂姆·鲁特

2

这不是通用方法,但在某些情况下很有用。对于CTRL+ somethingShift+ somethingCTRL+ Shift+ 等组合非常有用something等。

示例:当您想使用CTRL+ 打印页面时P,总是CTRL先按下第一个键P。与CTRL+ SCTRL+ U和其他组合相同。

document.addEventListener('keydown',function(e){
      
    //SHIFT + something
    if(e.shiftKey){
        switch(e.code){

            case 'KeyS':
                console.log('Shift + S');
                break;

        }
    }

    //CTRL + SHIFT + something
    if(e.ctrlKey && e.shiftKey){
        switch(e.code){

            case 'KeyS':
                console.log('CTRL + Shift + S');
                break;

        }
    }

});


1
case 65: //A
jp = 1;
setTimeout("jp = 0;", 100);

if(pj > 0) {
ABFunction();
pj = 0;
}
break;

case 66: //B
pj = 1;
setTimeout("pj = 0;", 100);

if(jp > 0) {
ABFunction();
jp = 0;
}
break;

我知道这不是最好的方法。


-1
Easiest, and most Effective Method

//check key press
    function loop(){
        //>>key<< can be any string representing a letter eg: "a", "b", "ctrl",
        if(map[*key*]==true){
         //do something
        }
        //multiple keys
        if(map["x"]==true&&map["ctrl"]==true){
         console.log("x, and ctrl are being held down together")
        }
    }

//>>>variable which will hold all key information<<
    var map={}

//Key Event Listeners
    window.addEventListener("keydown", btnd, true);
    window.addEventListener("keyup", btnu, true);

    //Handle button down
      function btnd(e) {
      map[e.key] = true;
      }

    //Handle Button up
      function btnu(e) {
      map[e.key] = false;
      }

//>>>If you want to see the state of every Key on the Keybaord<<<
    setInterval(() => {
                for (var x in map) {
                    log += "|" + x + "=" + map[x];
                }
                console.log(log);
                log = "";
            }, 300);
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.