提问者:小点点

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


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

  • 当我按空格时,字符跳转。
  • 当我按时,字符向右移动。

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

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


共3个答案

匿名用户

注意: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的值,该值的计算结果为true或false。 现在map[65]map[66]都设置为true。 当您放弃A时,keyup事件将触发,导致相同的逻辑为Map[65](A)确定相反的结果,该结果现在为false,但由于Map[66](B)仍然是“down”(它没有触发keyup事件),因此它仍然为true。

通过这两个事件,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]

您甚至不必使用document.getElementById()$()来获取它。 但出于兼容性的考虑,更广泛地推荐使用jQuery的$()

只需确保脚本标记位于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

就是这么简单:)

作为一般规则,记录代码是很好的做法,特别是像键代码(如//ctrl+enter)这样的事情,以便您能够记住它们是什么。

您还应该按照与文档相同的顺序放置关键代码(ctrl+enter=>map[17]&map[13],而不是map[13]&map[17])。 这样,当您需要回去编辑代码时,您就不会感到困惑了。

如果检查不同数量的组合(如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),您可能还想在MAP=[]之后添加RETURN FALSE,这样当“重复文件”功能放在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()之前,先了解这种区别

用户SeanVieira在评论中指出,不推荐使用event.keycode

在这里,他给出了一个很好的替代方案:event.key,它返回所按键的字符串表示形式,例如a“a”,或者shift“shift”

我继续做了一个工具来检查这些字符串。

使用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注册的处理程序似乎更容易编写和推理。

还有来自Internet Explorer非标准实现的AttachEvent(“onEvent”,callback),但这是不推荐使用的,甚至不适合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+5设置一个观察点。 当ctrl5都关闭时,将调用您传入的回调函数(在本例中,是一个将“five”添加到textarea的函数)。 回调与名称print_5相关联,因此要删除它,只需使用:

input_txt.unwatch("print_5");

要将input_txttxt元素分离,请执行以下操作:

input_txt.detach();

通过这种方式,垃圾收集可以捡起对象(input_txt),如果它被扔掉,那么您就不会有一个旧的僵尸事件侦听器遗留下来。

为了彻底起见,这里有一个对类API的快速引用,以C/Java风格呈现,这样您就知道它们返回了什么以及它们期望的参数。

Boolean  key_down (String key);

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

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

返回true,如果所有键key1.. keyn为down,否则为false。

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

创建“观察点”,以便按下所有键n将触发回调

void     unwatch (String name);

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

void     clear (void);

清除“Keys Down”缓存。 相当于上面的map={}

void     detach (void);

ev_kdownev_kup侦听器与父元素分离,从而可以安全地摆脱实例

更新2017-12-02作为对将此发布到github的请求的响应,我创建了一个Gist。

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

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

我希望这个解释透彻的答案小博客对我有帮助:)

匿名用户

您应该使用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时,这个概念也同样适用。

匿名用户

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

    } 

}