Як визначити, чи натискають відразу кілька клавіш за допомогою JavaScript?


173

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

  • Коли я натискаю, SPACEсимвол скаче.
  • Коли я натискаю, символ рухається вправо.

Проблема полягає в тому, що коли я натискаю праворуч, а потім натискаю пробіл, персонаж стрибає, а потім перестає рухатися.

Я використовую keydownфункцію для натискання клавіші. Як я можу перевірити наявність декількох клавіш одночасно?


3
Ось демонстрація веб-сторінки, яка автоматично роздруковує список усіх натиснутих клавіш: stackoverflow.com/a/13651016/975097
Anderson Green

Відповіді:


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) все ще "вниз" (це не викликало події клавіатури), це залишається правдою .

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. Порада з оптимізації : Більшість веб-сайтів із великими іменами ставлять тег сценарію після тегу 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');
    }
}

(кінець редагування)


Цей приклад перевіряє наявність CtrlShiftA, CtrlShiftBіCtrlShiftC

Це так просто, як це :)

Примітки

Відстеження KeyCodes

Як правило, добре документувати код, особливо такі речі, як Ключові коди (наприклад // 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"

Gotcha: "Цей комбінація клавіш продовжує активуватися, хоча я не натискаю клавіші"

Якщо ви маєте справу з сповіщеннями або будь-чим, що фокусується в головному вікні, ви можете включити 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

Gotcha: Параметри браузера

Ось ось яку дратівливу річ я знайшов, включаючи рішення:

Проблема: Оскільки браузер зазвичай виконує дії за замовчуванням на ключових комбо (наприклад, CtrlDактивує вікно закладок або CtrlShiftCактивує знімок на макстоні), ви можете також додати 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);

Повертається, trueякщо keyвниз, помилка помилка.

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

Повертається, trueякщо всі клавіші key1 .. keyNвідключені, інакше помилково.

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 Я деякий час грав із програмою декларативної стилістики, і цей спосіб зараз є моїм улюбленим: скрипка , пастбін

Як правило, він буде працювати з справами, які ви хотіли б реально (ctrl, alt, shift), але якщо вам потрібно натиснути, скажімо, a+wв той же час, "не поєднати" підходи в пошук декількома ключами.


Я сподіваюся, що цей ґрунтовно пояснений відповідь міні-блогу був корисним :)


Я щойно зробив велике оновлення цієї відповіді! Приклад кейлоггера є більш узгодженим, я оновив форматування, щоб розділ «замітки» було легше читати, і я додав нову примітку про return falsevspreventDefault()
Braden Best

Що робити, коли ви натискаєте / утримуєте клавішу з документом у фокусі, потім ви клацаєте поле URL-адреси, а потім ви відпускаєте ключ. клавіша ніколи не запускається, але ключ працює, що призводить до неправильного переліку. Також навпаки: натисніть / утримуйте клавішу у вікні URL-адреси, клавіша ніколи не запускається, потім зосередьте увагу на документі, а статус клавіші відсутній у списку. Зазвичай, коли документ знову набуває фокус, ви ніколи не можете бути впевнені в статусі ключа.
користувач3015682

3
Примітка: keyCodeзастаріло - якщо ви переходите до keyцього, ви отримуєте фактичне представлення символу ключа, який може бути приємним.
Шон Віейра

1
@SeanVieira Знову ж таки, ви можете робити якісь дивні речі і в C. Наприклад, чи знаєте ви, що myString[5]це те саме 5[myString], що навіть не дасть вам попередження про компіляцію (навіть з -Wall -pedantic)? Це тому, що pointer[offset]нотація бере вказівник, додає зміщення, а потім відмінює результат, роблячи myString[5]те саме, що *(myString + 5).
Бреден Кращий

1
@inorganik Ви маєте на увазі клас помічників? Чи можна використовувати гейсти як репости? Було б нудно зробити цілий репо для маленького фрагмента коду. Звичайно, я зроблю суть. Я буду стріляти на сьогоднішній день. Опівночна гора Time -ish
Бреден Кращий

30

Ви повинні використовувати подію клавіш, щоб слідкувати за натисканням клавіш, а також слід використовувати подію клавіатури, щоб відстежувати, коли клавіші відпущені.

Дивіться цей приклад: 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.
Мартійн

1
Виникла проблема із вставкою на Mac (Chrome). Він успішно отримує клавіш 91 (команда), клавіш 86 (v), але потім клавішує лише 91, залишаючи 86 вниз. Список клавіш: Вгору: 91, Вниз: 86. Здається, це відбувається лише тоді, коли відпустити командну клавішу вдруге - якщо я відпущу її першою, вона правильно зареєструє клавіатуру на обох.
Джеймс Олдай

2
Здається, що коли ви натискаєте відразу три чи більше клавіш, він перестає виявляти більше клавіш вниз, поки не піднімете одну. (Тестовано з Firefox 22)
Qvcool

1
@JamesAlday Та сама проблема. Це, мабуть, впливає лише на клавішу Meta (OS) на Mac. Дивіться випуск №3 тут: bitspushedaround.com/…
Дон


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

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реєстрація).

Повідомте, будь ласка, допомагає це чи ні.


1
Це не буде працювати: натискання не спрацьовує на багато клавіш , що KeyDown і KeyUp зробити курок. Крім того, не всі браузери неодноразово викликають події клавіатури.
Martijn

Quirksmode каже, що ви помилилися: quirksmode.org/dom/events/keys.html . Але я не заперечую це, оскільки не перевіряв свою пропозицію.
FK82

Цитується на цій сторінці: "Коли користувач натискає спеціальні клавіші, такі як клавіші зі стрілками, браузер НЕ повинен запускати події натискання клавіш" . Що стосується повторів, у ньому вказано, що 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+ 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);
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.