web-dev-qa-db-fra.com

Comment utiliser les raccourcis clavier à la place des écouteurs clés

J'utilise KeyListener s dans mon code (jeu ou autre) pour permettre à mes objets à l'écran de réagir à la saisie par la clé d'utilisateur. Voici mon code:

public class MyGame extends JFrame {

    static int up = KeyEvent.VK_UP;
    static int right = KeyEvent.VK_RIGHT;
    static int down = KeyEvent.VK_DOWN;
    static int left = KeyEvent.VK_LEFT;
    static int fire = KeyEvent.VK_Q;

    public MyGame() {

//      Do all the layout management and what not...
        JLabel obj1 = new JLabel();
        JLabel obj2 = new JLabel();
        obj1.addKeyListener(new MyKeyListener());
        obj2.addKeyListener(new MyKeyListener());
        add(obj1);
        add(obj2);
//      Do other GUI things...
    }

    static void move(int direction, Object source) {

        // do something
    }

    static void fire(Object source) {

        // do something
    }

    static void rebindKey(int newKey, String oldKey) {

//      Depends on your GUI implementation.
//      Detecting the new key by a KeyListener is the way to go this time.
        if (oldKey.equals("up"))
            up = newKey;
        if (oldKey.equals("down"))
            down = newKey;
//      ...
    }

    public static void main(String[] args) {

        new MyGame();
    }

    private static class MyKeyListener extends KeyAdapter {

        @Override
        public void keyPressed(KeyEvent e) {

            Object source = e.getSource();
            int action = e.getExtendedKeyCode();

/* Will not work if you want to allow rebinding keys since case variables must be constants.
            switch (action) {
                case up:
                    move(1, source);
                case right:
                    move(2, source);
                case down:
                    move(3, source);
                case left:
                    move(4, source);
                case fire:
                    fire(source);
                ...
            }
*/
            if (action == up)
                move(1, source);
            else if (action == right)
                move(2, source);
            else if (action == down)
                move(3, source);
            else if (action == left)
                move(4, source);
            else if (action == fire)
                fire(source);
        }
    }
}

J'ai des problèmes de réactivité:

  • J'ai besoin de cliquer sur l'objet pour que cela fonctionne.
  • La réponse que j'obtiens en appuyant sur l'une des touches n'est pas la façon dont je voulais que cela fonctionne - trop réactif ou trop peu réactif.

Pourquoi cela se produit-il et comment résoudre ce problème?

16
user1803551

_ {Cette réponse explique et montre comment utiliser des liaisons de touches au lieu d'écouteurs de touches à des fins pédagogiques. Ce n'est pas le cas

  • Comment écrire un jeu en Java.
  • À quoi devrait ressembler une bonne écriture de code (visibilité, par exemple).
  • Le moyen le plus efficace (en termes de performances ou de code) d'implémenter des raccourcis clavier.

Il est

  • _ {Ce que je publierais comme réponse à quiconque a des problèmes d'auditeurs clés}.

Réponse; Lisez le didacticiel Swing sur les raccourcis clavier .

Je ne veux pas lire les manuels, dites-moi pourquoi je voudrais utiliser des raccourcis clavier au lieu du beau code que j'ai déjà!

Eh bien, le tutoriel Swing explique que

  • Les raccourcis clavier ne vous obligent pas à cliquer sur le composant (pour le mettre en évidence):
    • Supprime le comportement inattendu du point de vue de l'utilisateur.
    • Si vous avez 2 objets, ils ne peuvent pas se déplacer simultanément car un seul des objets peut avoir le focus à un moment donné (même si vous les liez à des clés différentes).
  • Les raccourcis clavier sont plus faciles à maintenir et à manipuler:
    • Désactiver, relier, réaffecter des actions de l'utilisateur est beaucoup plus facile.
    • Le code est plus facile à lire.

OK, vous m'avez convaincu d'essayer. Comment ça marche?

Le tutorial contient une bonne section à ce sujet. Les raccourcis clavier impliquent 2 objets InputMap et ActionMap. InputMap mappe une entrée utilisateur sur un nom d'action, ActionMap mappe un nom d'action sur Action. Lorsque l'utilisateur appuie sur une touche, la mappe d'entrée est recherchée pour la clé et trouve un nom d'action, puis la mappe d'action est recherchée pour le nom de l'action et exécute l'action.

Ça a l'air lourd. Pourquoi ne pas lier directement l'entrée de l'utilisateur à l'action et supprimer le nom de l'action? Ensuite, vous n’avez besoin que d’une seule carte et non de deux.

Bonne question! Vous verrez que c'est l'une des choses qui rendent les liaisons de clé plus faciles à gérer (désactiver, relier, etc.).

Je veux que vous me donniez un code de travail complet.

Non (le tutoriel Swing a des exemples de travail ).

Tu crains! Je te deteste!

Voici comment créer une liaison de clé unique:

myComponent.getInputMap().put("userInput", "myAction");
myComponent.getActionMap().put("myAction", action);

Notez qu'il y a 3 InputMaps réagissant à différents états de focus:

myComponent.getInputMap(JComponent.WHEN_FOCUSED);
myComponent.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
myComponent.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
  • WHEN_FOCUSED, qui est également celui utilisé lorsqu'aucun argument n'est fourni, est utilisé lorsque le composant a le focus. Ceci est similaire au cas d'écoute de clé.
  • WHEN_ANCESTOR_OF_FOCUSED_COMPONENT est utilisé lorsqu'un composant ciblé est à l'intérieur d'un composant qui est enregistré pour recevoir l'action. Si vous avez plusieurs membres d'équipage à l'intérieur d'un vaisseau spatial et que vous voulez que le vaisseau spatial continue à recevoir des informations pendant que l'un des membres de l'équipage a le focus, utilisez ceci.
  • WHEN_IN_FOCUSED_WINDOW est utilisé lorsqu'un composant enregistré pour recevoir l'action se trouve à l'intérieur d'un composant ciblé. Si vous avez plusieurs réservoirs dans une fenêtre ciblée et que vous souhaitez que tous reçoivent une entrée en même temps, utilisez ceci.

Le code présenté dans la question ressemblera à quelque chose comme ceci en supposant que les deux objets doivent être contrôlés en même temps:

public class MyGame extends JFrame {

    private static final int IFW = JComponent.WHEN_IN_FOCUSED_WINDOW;
    private static final String MOVE_UP = "move up";
    private static final String MOVE_DOWN = "move down";
    private static final String FIRE = "move fire";

    static JLabel obj1 = new JLabel();
    static JLabel obj2 = new JLabel();

    public MyGame() {

//      Do all the layout management and what not...

        obj1.getInputMap(IFW).put(KeyStroke.getKeyStroke("UP"), MOVE_UP);
        obj1.getInputMap(IFW).put(KeyStroke.getKeyStroke("DOWN"), MOVE_DOWN);
//      ...
        obj1.getInputMap(IFW).put(KeyStroke.getKeyStroke("control CONTROL"), FIRE);
        obj2.getInputMap(IFW).put(KeyStroke.getKeyStroke("W"), MOVE_UP);
        obj2.getInputMap(IFW).put(KeyStroke.getKeyStroke("S"), MOVE_DOWN);
//      ...
        obj2.getInputMap(IFW).put(KeyStroke.getKeyStroke("T"), FIRE);

        obj1.getActionMap().put(MOVE_UP, new MoveAction(1, 1));
        obj1.getActionMap().put(MOVE_DOWN, new MoveAction(2, 1));
//      ...
        obj1.getActionMap().put(FIRE, new FireAction(1));
        obj2.getActionMap().put(MOVE_UP, new MoveAction(1, 2));
        obj2.getActionMap().put(MOVE_DOWN, new MoveAction(2, 2));
//      ...
        obj2.getActionMap().put(FIRE, new FireAction(2));

//      In practice you would probably create your own objects instead of the JLabels.
//      Then you can create a convenience method obj.inputMapPut(String ks, String a)
//      equivalent to obj.getInputMap(IFW).put(KeyStroke.getKeyStroke(ks), a);
//      and something similar for the action map.

        add(obj1);
        add(obj2);
//      Do other GUI things...
    }

    static void rebindKey(KeyEvent ke, String oldKey) {

//      Depends on your GUI implementation.
//      Detecting the new key by a KeyListener is the way to go this time.
        obj1.getInputMap(IFW).remove(KeyStroke.getKeyStroke(oldKey));
//      Removing can also be done by assigning the action name "none".
        obj1.getInputMap(IFW).put(KeyStroke.getKeyStrokeForEvent(ke),
                 obj1.getInputMap(IFW).get(KeyStroke.getKeyStroke(oldKey)));
//      You can drop the remove action if you want a secondary key for the action.
    }

    public static void main(String[] args) {

        new MyGame();
    }

    private class MoveAction extends AbstractAction {

        int direction;
        int player;

        MoveAction(int direction, int player) {

            this.direction = direction;
            this.player = player;
        }

        @Override
        public void actionPerformed(ActionEvent e) {

            // Same as the move method in the question code.
            // Player can be detected by e.getSource() instead and call its own move method.
        }
    }

    private class FireAction extends AbstractAction {

        int player;

        FireAction(int player) {

            this.player = player;
        }

        @Override
        public void actionPerformed(ActionEvent e) {

            // Same as the fire method in the question code.
            // Player can be detected by e.getSource() instead, and call its own fire method.
            // If so then remove the constructor.
        }
    }
}

Vous pouvez constater que la séparation de la mappe d'entrée de la mappe d'action permet un code réutilisable et un meilleur contrôle des liaisons. En outre, vous pouvez également contrôler une action directement si vous avez besoin de cette fonctionnalité. Par exemple:

FireAction p1Fire = new FireAction(1);
p1Fire.setEnabled(false); // Disable the action (for both players in this case).

Voir le didacticiel Action pour plus d'informations.

Je vois que vous avez utilisé 1 action, déplacer, pour 4 touches (directions) et 1 action, feu, pour 1 touche. Pourquoi ne pas donner à chaque touche sa propre action, ou donner à toutes les touches la même action et déterminer ce qu'il faut faire dans l'action (comme dans le cas d'un mouvement)?

Bon point. Techniquement, vous pouvez faire les deux, mais vous devez penser à ce qui a du sens et à ce qui permet une gestion facile et un code réutilisable. Ici, j'ai supposé que le déplacement était similaire dans toutes les directions et que le tir était différent, j'ai donc choisi cette approche.

Je vois beaucoup de KeyStrokes utilisés, quels sont-ils? Sont-ils comme un KeyEvent?

Oui, ils ont une fonction similaire, mais sont plus appropriés pour une utilisation ici. Voir leur API pour plus d’informations et pour savoir comment les créer.


_ {Questions? Améliorations? Suggestions? Laisser un commentaire . Vous avez une meilleure réponse? Postez-le.} _

52
user1803551

Note: ceci est pas une réponse, juste un commentaire avec trop de code :-)

Obtenir keyStrokes via getKeyStroke (String) est la méthode correcte - mais nécessite une lecture attentive du document api:

modifiers := shift | control | ctrl | meta | alt | altGraph
typedID := typed <typedKey>
typedKey := string of length 1 giving Unicode character.
pressedReleasedID := (pressed | released) key
key := KeyEvent key code name, i.e. the name following "VK_".

Il est préférable que la dernière ligne soit nom exact , c’est le cas: pour la touche bas le nom exact du code est VK_DOWN, le paramètre doit donc être "DOWN" (pas "Down" ou toute autre variante de haut/minuscules)

Pas tout à fait intuitif (lire: j'ai dû creuser un peu moi-même), c'est obtenir un KeyStroke à une touche de modificateur. Même avec une orthographe correcte, les éléments suivants ne fonctionneront pas: 

KeyStroke control = getKeyStroke("CONTROL"); 

Plus profondément dans la file d'attente d'événements awt, un événement keyEvent pour une seule touche de modificateur est créé avec lui-même en tant que modificateur. Pour vous lier à la touche de contrôle, vous avez besoin du trait: 

KeyStroke control = getKeyStroke("ctrl CONTROL"); 
6
kleopatra

Voici un moyen facile qui ne vous obligerait pas à lire des centaines de lignes de code, mais apprenez un tour de quelques lignes. 

déclarer un nouveau JLabel et l'ajouter à votre JFrame (je ne l'ai pas testé dans d'autres composants)

private static JLabel listener= new JLabel(); 

L'accent doit rester sur ce point pour que les clés fonctionnent.

En constructeur: 

add(listener);

Utilisez cette méthode:

ANCIENNE METHODE:

 private void setKeyBinding(String keyString, AbstractAction action) {
        listener.getInputMap().put(KeyStroke.getKeyStroke(keyString), keyString);
        listener.getActionMap().put(keyString, action);
    }

KeyString doit être écrit correctement. Ce n'est pas typé et vous devez consulter la liste officielle list pour savoir quel est le keyString (ce n'est pas un terme officiel) pour chaque bouton. 

NOUVELLE METHODE

private void setKeyBinding(int keyCode, AbstractAction action) {
    int modifier = 0;
    switch (keyCode) {
        case KeyEvent.VK_CONTROL:
            modifier = InputEvent.CTRL_DOWN_MASK;
            break;
        case KeyEvent.VK_SHIFT:
            modifier = InputEvent.SHIFT_DOWN_MASK;
            break;
        case KeyEvent.VK_ALT:
            modifier = InputEvent.ALT_DOWN_MASK;
            break;

    }

    listener.getInputMap().put(KeyStroke.getKeyStroke(keyCode, modifier), keyCode);
    listener.getActionMap().put(keyCode, action);
}

Dans cette nouvelle méthode, vous pouvez simplement le définir en utilisant KeyEvent.VK_WHATEVER

EXEMPLE D'APPEL:

  setKeyBinding(KeyEvent.VK_CONTROL, new AbstractAction() {

        @Override
        public void actionPerformed(ActionEvent e) {
            System.out.println("ctrl pressed");

        }
    });

Envoyez une classe anonyme (ou utilisez une sous-classe) de AbstractAction. Remplacez sa public void actionPerformed(ActionEvent e) et faites-la faire ce que vous voulez que la clé fasse.

PROBLEME: 

Je ne pouvais pas le faire fonctionner pour VK_ALT_GRAPH.

 case KeyEvent.VK_ALT_GRAPH:
            modifier = InputEvent.ALT_GRAPH_DOWN_MASK;
            break;

ne le fait pas fonctionner pour moi pour une raison quelconque.

0
WVrock