Как определить, нажаты ли несколько клавиш одновременно с использованием JavaScript?

173

Я пытаюсь разработать игровой движок JavaScript, и я столкнулся с этой проблемой:

  • Когда я нажимаю, SPACEперсонаж прыгает.
  • Когда я нажимаю, персонаж движется вправо.

Проблема в том, что когда я нажимаю вправо, а затем нажимаю пробел, персонаж прыгает, а затем перестает двигаться.

Я использую keydownфункцию, чтобы нажать клавишу. Как я могу проверить, нажаты ли несколько клавиш одновременно?

XCS
источник
3
Вот демонстрация веб-страницы, которая автоматически печатает список всех нажатых клавиш: stackoverflow.com/a/13651016/975097
Андерсон Грин

Ответы:

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 */
}

Этот код очень прост: поскольку компьютер одновременно выполняет только одно нажатие клавиши, создается массив для отслеживания нескольких клавиш. Затем этот массив можно использовать для проверки одного или нескольких ключей одновременно.

Просто чтобы объяснить, скажем, вы нажимаете Aи B, каждый запускает keydownсобытие, которое устанавливает map[e.keyCode]значение e.type == keydown, которое оценивается как истинное или ложное . Теперь оба map[65]и map[66]настроены на true. Когда вы отпускаете A, keyupсобытие запускается, в результате чего та же логика определяет противоположный результат для map[65](A), который теперь является ложным , но поскольку 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>

Это создает элемент HTML, на который можно легко ссылаться в JavaScript с помощью element

alert(element); // [Object HTMLDivElement]

Вы даже не должны использовать document.getElementById()или $()захватить это. Но для совместимости $()рекомендуется использовать jQuery .

Просто убедитесь, что тег script идет после тела HTML. Совет по оптимизации : большинство известных сайтов ставят тег script после тега 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');
    }
}

(конец редактирования)


Этот пример проверяет наличие CtrlShiftA, CtrlShiftBиCtrlShiftC

Это так же просто, как это :)

Ноты

Отслеживание ключевых кодов

Как правило, рекомендуется документировать код, особенно такие, как коды клавиш (например // CTRL+ENTER), чтобы вы могли помнить, что они были.

Вы также должны поместить коды клавиш в том же порядке, что и документация ( CTRL+ENTER => map[17] && map[13], НЕ map[13] && map[17]). Таким образом, вы никогда не запутаетесь, когда вам нужно вернуться и отредактировать код.

Гоча с цепями if-else

Если вы проверяете комбинации разных сумм (например, CtrlShiftAltEnterи CtrlEnter), поместите меньшие комбинации после больших, иначе меньшие комбинации будут заменять большие комбинации, если они достаточно похожи. Пример:

// 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активирует skynote на maxthon), вы также можете добавить return falseпосле 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 устарела

Пользователь SeanVieira указал в комментариях, чтоevent.keyCode устарел.

Там он дал отличную альтернативу:, event.keyкоторая возвращает строковое представление нажатой клавиши, например, "a"для Aили "Shift"для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 ). Было бы в ваших интересах избегать как можно большего количества полиглотов.

Вспомогательный класс

Чтобы устранить путаницу / жалобы, я написал «класс», который делает эту абстракцию ( ссылка для вставки ):

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. Когда оба Ctrlи 5выключены, "FIVE "будет вызвана функция обратного вызова (в данном случае это функция, которая добавляет текстовую область). Обратный вызов связан с именем print_5, поэтому, чтобы удалить его, вы просто используете:

input_txt.unwatch("print_5");

Чтобы отсоединить input_txtот txtэлемента:

input_txt.detach();

Таким образом, сборщик мусора может забрать объект ( input_txt), если он будет выброшен, и у вас не останется старый прослушиватель событий зомби.

Для краткости приведу краткую ссылку на API класса, представленный в стиле C / Java, чтобы вы знали, что они возвращают и какие аргументы они ожидают.

Boolean  key_down (String key);

Возвращает trueif keydown, 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_kdownи ev_kupслушатели от родительского элемента, что позволяет безопасно избавиться от экземпляра

Обновление 2017-12-02 В ответ на запрос опубликовать это на github я создал суть .

Обновление 2018-07-21 Некоторое время я играл с декларативным стилевым программированием, и теперь этот путь мой любимый: fiddle , pastebin.

Как правило, он будет работать с теми случаями, которые вы реально хотите (ctrl, alt, shift), но если вам нужно нажать, скажем, a+wв то же время, не составит труда объединить подходы в мульти-ключ-поиска.


Надеюсь, этот подробный мини-блог с ответом был полезен :)

Брэден Бест
источник
Я только что сделал большое обновление к этому ответу! Пример кейлоггера более последовательный, я обновил форматирование, чтобы раздел «заметки» было легче читать, и добавил новую заметку о return falsevspreventDefault()
Braden Best
Как насчет того, когда вы нажимаете / удерживаете клавишу, когда документ находится в фокусе, затем нажимаете на поле URL-адреса и отпускаете клавишу. keyup никогда не срабатывает, но ключ активен, что приводит к неправильному списку. Также наоборот: нажатие / удержание клавиши в поле URL, нажатие клавиши никогда не срабатывает, затем сфокусируйтесь на документе, а статус нажатия клавиши отсутствует в списке. В основном, когда документ возвращается в фокус, вы никогда не можете быть уверены в статусе ключа.
user3015682
3
NB: keyCodeустарело - если вы переключитесь на keyто, вы получите фактическое символьное представление ключа, которое может быть хорошим.
Шон Виейра
1
@ SeanVieira Опять же, вы можете делать некоторые странные вещи в C. Например, знаете ли вы, что myString[5]это то же самое 5[myString], и оно даже не даст вам предупреждение о компиляции (даже с -Wall -pedantic)? Это связано с тем, что pointer[offset]нотация берет указатель, добавляет смещение, а затем разыменовывает результат, делая myString[5]то же самое, что и *(myString + 5).
Брэден Бест
1
@inorganik вы имеете в виду класс помощника? Можно ли использовать гистос как репо? Было бы утомительно делать целый репозиторий для крошечного фрагмента кода. Конечно, я сделаю суть. Я буду стрелять сегодня вечером. Полуночная гора Время-иш
Брэден Бест
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.

Мартейн
источник
Но, как я думал, есть ошибка. Если вы удерживаете нажатой одну кнопку, затем переключаетесь на другую вкладку (или теряете фокус), продолжая удерживать кнопку при повторной фокусировке на скрите, это покажет, что кнопка нажата, даже если это не так. : D
XCS
3
@Cristy: тогда вы также можете добавить onblurобработчик событий, который удаляет все нажатые клавиши из массива. После того, как вы потеряли фокус, имеет смысл снова нажимать все клавиши. К сожалению, нет JS-эквивалента GetKeyboardState.
Мартейн
1
Возникла проблема со вставкой на Mac (Chrome). Он успешно получает 91 нажатие клавиши (команда), 86 нажатие клавиши (v), но затем только 91 нажимает, оставляя 86 вниз. Список клавиш: Вверх: 91, Вниз: 86. Это, кажется, происходит только при отпускании второй клавиши команды - если я отпущу ее сначала, она правильно зарегистрирует ключ на обоих.
Джеймс Алдай
2
Похоже, что когда вы нажимаете три или более клавиш одновременно, он перестает обнаруживать больше клавиш, пока вы не нажмете одну. (Протестировано с Firefox 22)
Qvcool
1
@JamesAlday Та же проблема. По-видимому, это влияет только на клавишу Meta (OS) на Mac. Смотрите выпуск № 3 здесь: bitsphedaround.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
Это было все, что я хотел, лучший ответ
Рэндалл Кодинг
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

Заставьте клавишу даже вызывать несколько функций, каждая из которых проверяет определенную клавишу и реагирует соответствующим образом.

document.keydown = function (key) {

    checkKey("x");
    checkKey("y");
};
AnonymousGuest
источник
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) ;
}

Это просто для иллюстрации шаблона; Я не буду вдаваться в подробности здесь (особенно не в браузер уровня 2 + Eventрегистрация).

Отпишитесь, пожалуйста, помогает ли это или нет.

FK82
источник
1
Это не будет работать: нажатие не срабатывает на много клавиш , что KeyDown и KeyUp сделать курок. Кроме того, не все браузеры постоянно вызывают события нажатия клавиш.
Мартейн
Quirksmode говорит, что вы не правы: quirksmode.org/dom/events/keys.html . Но я не буду спорить, так как я не проверял свое предложение.
FK82
Цитируется с этой страницы: «Когда пользователь нажимает специальные клавиши, такие как клавиши со стрелками, браузер НЕ должен запускать события нажатия клавиш» . Что касается повторов, в нем перечислены Opera и Konqueror, которые делают это неправильно.
Мартейн
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+ somethingили Shift+ somethingили CTRL+ Shift+ somethingи т. Д.

Пример: если вы хотите распечатать страницу с помощью CTRL+ P, всегда CTRLследует первая нажатая клавиша P. То же самое с CTRL+ S, CTRL+ 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);
flyingwizard
источник