201 votes

Comment détecter si plusieurs touches sont pressées en même temps en utilisant JavaScript ?

J'essaie de développer un moteur de jeu JavaScript et j'ai rencontré ce problème :

  • Quand j'appuie sur SPACE le personnage saute.
  • Quand j'appuie sur le personnage se déplace vers la droite.

Le problème est que lorsque j'appuie sur la droite puis sur l'espace, le personnage saute puis s'arrête de bouger.

J'utilise le keydown pour obtenir la touche pressée. Comment puis-je vérifier si plusieurs touches sont enfoncées en même temps ?

3 votes

Voici une démonstration d'une page Web qui imprime automatiquement une liste de toutes les touches enfoncées : stackoverflow.com/a/13651016/975097

376voto

B1KMusic Points 1088

Note : keyCode est maintenant déprécié.

La détection des frappes multiples est facile si vous comprenez le concept.

La façon dont je le fais est la suivante :

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

Ce code est très simple : Puisque l'ordinateur ne laisse passer qu'une seule touche à la fois, un tableau est créé pour garder la trace de plusieurs touches. Le tableau peut ensuite être utilisé pour vérifier une ou plusieurs touches à la fois.

Juste pour expliquer, disons que vous appuyez sur A y B chacun tire un keydown événement qui fixe map[e.keyCode] à la valeur de e.type == keydown qui donne l'une des deux valeurs suivantes vrai o faux . Maintenant, les deux map[65] y map[66] sont fixés à true . Quand vous laissez partir A le keyup se déclenche, ce qui amène la même logique à déterminer le résultat opposé pour les éléments suivants map[65] (A), qui est maintenant faux mais comme map[66] (B) est toujours "en bas" (il n'a pas déclenché d'événement keyup), il demeure vrai .

El map Le tableau, à travers les deux événements, ressemble à ceci :

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

Il y a deux choses que vous pouvez faire maintenant :

A) Un enregistreur de clés ( exemple ) peut être créé comme référence pour plus tard lorsque vous voulez comprendre rapidement un ou plusieurs codes de clé. En supposant que vous ayez défini un élément html et que vous l'ayez pointé avec la variable element .

element.innerHTML = '';
var i, l = map.length;
for(i = 0; i < l; i ++){
    if(map[i]){
        element.innerHTML += '<hr>' + i;
    }
}

Remarque : vous pouvez facilement saisir un élément par son nom. id attribut.

<div id="element"></div>

Cela crée un élément html qui peut être facilement référencé en javascript avec element

alert(element); // [Object HTMLDivElement]

Vous n'avez même pas besoin d'utiliser document.getElementById() o $() pour le saisir. Mais pour des raisons de compatibilité, l'utilisation de la méthode jQuery $() est plus largement recommandé.

Assurez-vous simplement que le script vient après le corps du HTML. Conseil d'optimisation : La plupart des sites web de renom mettent la balise script. après la balise body pour l'optimisation. En effet, la balise script bloque le chargement d'autres éléments jusqu'à ce que son script ait fini de se télécharger. Le fait de la placer avant le contenu permet à ce dernier de se charger avant.

B (c'est là que se situe votre intérêt) Vous pouvez vérifier une ou plusieurs clés à la fois dans les cas suivants /*insert conditional here*/ était, prenez cet exemple :

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

Editar : Ce n'est pas le snippet le plus lisible. La lisibilité est importante, vous pouvez donc essayer quelque chose comme ceci pour le rendre plus facile à lire :

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

Utilisation :

test_keys(13, 16, 65)
test_keys('ctrl', 'shift', 'A')
test_key(65)
test_key('A')

Est-ce que c'est mieux ?

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

(fin du montage)


Cet exemple vérifie que CtrlShiftA , CtrlShiftB y CtrlShiftC

C'est aussi simple que cela :)

Notes

Suivi des KeyCodes

En règle générale, il est bon de documenter le code, en particulier les éléments tels que les codes clés (tels que // CTRL+ENTER ) pour que vous puissiez vous souvenir de ce qu'ils étaient.

Vous devez également placer les codes clés dans le même ordre que celui de la documentation ( CTRL+ENTER => map[17] && map[13] , PAS map[13] && map[17] ). De cette façon, vous ne serez jamais désorienté lorsque vous devrez revenir en arrière et modifier le code.

Un problème avec les chaînes if-else

Si vous vérifiez des combinaisons de montants différents (par exemple CtrlShiftAltEnter y CtrlEnter ), mettre des combos plus petits après les combinaisons plus importantes, ou bien les petites combinaisons remplaceront les grandes combinaisons si elles sont suffisamment similaires. Exemple :

// 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 : "Cette combinaison de touches continue à s'activer même si je n'appuie pas sur les touches".

Lorsqu'il s'agit d'alertes ou de tout ce qui détourne l'attention de la fenêtre principale, il peut être utile d'inclure les éléments suivants map = [] pour réinitialiser le tableau après la réalisation de la condition. Cela est dû au fait que certaines choses, comme alert() En effet, si l'événement "keyup" n'est pas déclenché, la fenêtre principale n'est pas mise en évidence. Par exemple :

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

Gaffe : les paramètres par défaut du navigateur

Voici une chose ennuyeuse que j'ai trouvée, avec la solution incluse :

Problème : étant donné que le navigateur a généralement des actions par défaut sur les combinaisons de touches (telles que CtrlD active la fenêtre des signets, ou CtrlShiftC active skynote sur maxthon), vous pourriez aussi ajouter return false après map = [] Ainsi, les utilisateurs de votre site ne seront pas frustrés lorsque la fonction "Fichier dupliqué", qui est mise en place sur le site de la Commission européenne, sera désactivée. CtrlD pour mettre la page en signet.

if(map[17] && map[68]){ // CTRL+D
    alert('The bookmark window didn\'t pop up!');
    map = {};
    return false;
}

Sans return false la fenêtre Signets serait apparaissent, au grand dam de l'utilisateur.

La déclaration de retour (nouveau)

Ok, donc vous ne voulez pas toujours quitter la fonction à ce moment-là. C'est pourquoi le event.preventDefault() La fonction est là. Ce qu'elle fait, c'est mettre un drapeau interne qui dit à l'interpréteur de no permettre au navigateur d'exécuter son action par défaut. Après cela, l'exécution de la fonction se poursuit (alors que return quittera immédiatement la fonction).

Comprenez bien cette distinction avant de décider si vous devez utiliser return false o e.preventDefault()

event.keyCode est déprécié

Utilisateur SeanVieira a souligné dans les commentaires que event.keyCode est déprécié.

Là, il a donné une excellente alternative : event.key qui renvoie une représentation sous forme de chaîne de caractères de la touche enfoncée, comme suit "a" para A ou "Shift" para Shift .

J'ai pris les devants et j'ai préparé un outil pour examiner lesdites cordes.

element.onevent vs element.addEventListener

Manipulateurs enregistrés auprès de addEventListener peuvent être empilés, et sont appelés dans l'ordre d'enregistrement, tandis que le réglage .onevent directement est plutôt agressive et remplace tout ce que vous possédiez précédemment.

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

El .onevent semble tout remplacer et le comportement de la propriété ev.preventDefault() y return false; peuvent être assez imprévisibles.

Dans les deux cas, les gestionnaires enregistrés via addEventlistener semblent plus faciles à écrire et à raisonner.

Il existe également attachEvent("onevent", callback) à partir de l'implémentation non standard d'Internet Explorer, mais cela est plus que déprécié et ne concerne même pas JavaScript (il s'agit d'un langage ésotérique appelé JScript ). Il serait dans votre intérêt d'éviter autant que possible le code polyglotte.

Une classe d'aide

Pour répondre à la confusion/aux plaintes, j'ai écrit une "classe" qui fait cette abstraction ( lien 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();
}

Cette classe ne fait pas tout et ne gère pas tous les cas d'utilisation imaginables. Je ne suis pas un spécialiste des bibliothèques. Mais pour une utilisation interactive générale, elle devrait convenir.

Pour utiliser cette classe, créez une instance et faites-la pointer vers l'élément auquel vous souhaitez associer la saisie clavier :

var input_txt = Input(document.getElementById("txt"));

input_txt.watch("print_5", function(){
    txt.value += "FIVE ";
}, "Control", "5");

Ce que cela va faire, c'est attacher un nouveau récepteur d'entrée à l'élément avec #txt (supposons qu'il s'agit d'un textarea), et définir un point de surveillance pour la combinaison de touches Ctrl+5 . Lorsque les deux Ctrl y 5 sont désactivées, la fonction de rappel que vous avez transmise (dans ce cas, une fonction qui ajoute des données à la base de données de l'entreprise) est activée. "FIVE " à la zone de texte) sera appelée. La callback est associée au nom print_5 donc pour le supprimer, il suffit d'utiliser :

input_txt.unwatch("print_5");

Pour détacher input_txt de la txt élément :

input_txt.detach();

De cette façon, le ramassage des ordures peut récupérer l'objet ( input_txt ), s'il est jeté, et vous n'aurez plus un vieil écouteur d'événements zombie.

Par souci d'exhaustivité, voici une référence rapide aux API des classes, présentées en style C/Java afin que vous sachiez ce qu'elles retournent et quels arguments elles attendent.

Boolean  key_down (String key);

Renvoie à true si key est en panne, faux sinon.

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

Renvoie à true si toutes les clés key1 .. keyN sont en panne, faux sinon.

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

Crée un "point de surveillance" tel que le fait d'appuyer sur toutes les keyN déclenchera le callback

void     unwatch (String name);

Supprime ledit point de surveillance via son nom

void     clear (void);

Efface le cache des "clés". Equivalent à map = {} au-dessus de

void     detach (void);

Détache le ev_kdown y ev_kup de l'élément parent, ce qui permet de se débarrasser en toute sécurité de l'instance

Mise à jour 2017-12-02 En réponse à une demande de publier ceci sur github, j'ai créé une Gist .

Mise à jour 2018-07-21 Je joue avec la programmation de style déclaratif depuis un certain temps, et cette méthode est maintenant ma préférée : violon , pastebin

En règle générale, cela fonctionne avec les cas que vous souhaitez (ctrl, alt, shift), mais si vous avez besoin de frapper, par exemple, a+w en même temps, il ne serait pas trop difficile de "combiner" les approches dans une recherche à touches multiples.


J'espère que ce réponse expliquée en détail Le mini-blog a été utile :)

0 votes

Je viens de faire une grosse mise à jour de cette réponse ! L'exemple du keylogger est plus cohérent, j'ai mis à jour la mise en forme pour que la section "notes" soit plus facile à lire, et j'ai ajouté une nouvelle note à propos de return false vs preventDefault()

0 votes

Qu'en est-il lorsque vous appuyez sur une touche ou la maintenez enfoncée avec le document en focus, puis que vous cliquez sur la boîte d'URL, puis que vous relâchez la touche. keyup n'est jamais déclenché, pourtant la touche est en haut, ce qui fait que la liste est incorrecte. Et vice-versa : vous appuyez sur une touche ou la maintenez enfoncée dans la boîte URL, la touche bas n'est jamais activée, puis vous mettez le focus sur le document et l'état de la touche bas n'apparaît pas dans la liste. En fait, chaque fois que le document reprend le focus, vous ne pouvez jamais être sûr de l'état de la touche.

0 votes

@user3015682 Cela revient à avoir une certaine confiance dans l'intelligence de vos utilisateurs. Il n'y a rien que vous puissiez faire à propos du focus, à part vider le tableau keys_down[] à chaque fois qu'une fonction est activée. Même Flash n'a pas de solution pour cela. En général, le retrait du focus n'est pas intentionnel. Mais, si votre utilisateur veut intentionnellement fait l'imbécile avec les clés, et arrive à casser quelque chose, c'est de sa faute. Le bon sens veut qu'ils ne recommencent pas, parce qu'ils veulent que l'application ou la page Web fonctionne correctement, ce qui explique pourquoi ils l'utilisent.

31voto

Martijn Points 6412

Vous devez utiliser le touchedown pour garder la trace des touches pressées, y vous devez utiliser le keyup pour garder la trace du moment où les touches sont relâchées.

Voir cet exemple : http://jsfiddle.net/vor0nwe/mkHsU/

(Mise à jour : je reproduis le code ici, au cas où jsfiddle.net échouerait :) Le HTML :

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

...et le Javascript (en utilisant 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');
});

Dans cet exemple, j'utilise un tableau pour garder la trace des touches sur lesquelles on appuie. Dans une application réelle, vous pourriez vouloir delete chaque élément une fois que la clé qui lui est associée a été libérée.

Notez que si j'ai utilisé jQuery pour me faciliter la tâche dans cet exemple, le concept fonctionne tout aussi bien lorsque vous travaillez en Javascript "brut".

0 votes

Mais comme je l'ai pensé, il y a un bug. Si vous maintenez un bouton enfoncé, puis passez à un autre onglet (ou perdez le focus) tout en maintenant le bouton enfoncé, lorsque vous refaites le focus sur le texte, le bouton est affiché même s'il ne l'est pas. :D

4 votes

@Cristy : alors vous pourriez aussi ajouter un onblur qui supprime toutes les touches enfoncées du tableau. Une fois que vous avez perdu le focus, il serait logique de devoir appuyer à nouveau sur toutes les touches. Malheureusement, il n'y a pas d'équivalent JS à GetKeyboardState .

0 votes

Merci, ce "mode" à touches multiples a résolu plus de bugs que prévu :D

8voto

Array Points 88

J'ai utilisé cette méthode (j'ai dû vérifier où sont les touches 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;
    }
});

0 votes

Comment faire cela dans un composant angulaire ?

0 votes

Je n'ai pas d'expérience avec angular pour répondre à votre question @ScipioAfricanus

2voto

FK82 Points 1464

J'essaierais d'ajouter un keypress Event le gestionnaire sur keydown . Par exemple

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

Il s'agit simplement d'illustrer un modèle ; je n'entrerai pas dans les détails ici (surtout pas dans le niveau spécifique du navigateur2+). Event enregistrement).

Dites-nous si cela vous aide ou non.

1 votes

Cela ne fonctionnerait pas : keypress ne se déclenche pas sur un grand nombre de touches que keydown et keyup. faire déclencher. De même, tous les navigateurs ne déclenchent pas de manière répétée les événements de type "keydown".

0 votes

Quirksmode dit que tu as tort : quirksmode.org/dom/events/keyes.html . Mais je ne le contesterai pas puisque je n'ai pas testé ma proposition.

0 votes

Citation de cette page : "Lorsque l'utilisateur appuie sur des touches spéciales, telles que les touches fléchées, le navigateur ne doit PAS déclencher d'événements liés à l'appui sur une touche". . En ce qui concerne les répétitions, Opera et Konqueror sont cités comme ne faisant pas cela correctement.

1voto

Anonymous Points 11
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;

Pas la meilleure façon, je sais.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X