wake-up-neo.com

So verwenden Sie Key Bindings anstelle von Key Listeners

Ich verwende KeyListener s in meinem Code (Spiel oder auf andere Weise), um meine Objekte auf dem Bildschirm auf Benutzereingaben zu reagieren. Hier ist mein 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);
        }
    }
}

Ich habe Probleme mit der Reaktionsfähigkeit:

  • Ich muss auf das Objekt klicken, damit es funktioniert.
  • Die Antwort, die ich bekomme, wenn ich eine der Tasten drücke, ist nicht so, wie ich wollte, dass sie funktioniert - zu ansprechend oder zu unreagierend.

Warum passiert das und wie kann ich das beheben?

16
user1803551

In dieser Antwort wird erläutert und veranschaulicht, wie Tastenzuweisungen anstelle von Schlüsselhörern für Bildungszwecke verwendet werden. Es ist nicht

  • Wie schreibe ich ein Spiel in Java?
  • Wie soll das Schreiben von Code aussehen (z. B. Sichtbarkeit).
  • Die effizienteste (leistungs- oder codeweise) Art, Schlüsselbindungen zu implementieren.

Es ist

  • Was ich als Antwort an jeden posten würde, der Probleme mit den wichtigsten Zuhörern hat.

Antworten; Lesen Sie das Swing-Tutorial zu Tastenkombinationen .

Ich möchte keine Handbücher lesen, sagen Sie mir, warum ich anstelle des schönen Codes, den ich bereits habe, Schlüsselbindungen verwenden möchte!

Nun, das Swing-Tutorial erklärt das

  • Bei Tastenzuordnungen müssen Sie nicht auf die Komponente klicken (um sie zu aktivieren):
    • Entfernt unerwartetes Verhalten aus Sicht des Benutzers.
    • Wenn Sie 2 Objekte haben, können sie sich nicht gleichzeitig bewegen, da nur 1 Objekt zu einer bestimmten Zeit den Fokus haben kann (selbst wenn Sie sie an andere Tasten binden).
  • Schlüsselbindungen sind einfacher zu verwalten und zu bearbeiten:
    • Das Deaktivieren, erneutes Binden und erneutes Zuweisen von Benutzeraktionen ist viel einfacher.
    • Der Code ist einfacher zu lesen.

OK, Sie haben mich davon überzeugt, es auszuprobieren. Wie funktioniert es?

Das Tutorial hat einen guten Abschnitt darüber. Schlüsselbindungen umfassen 2 Objekte InputMap und ActionMap. InputMap ordnet eine Benutzereingabe einem Aktionsnamen zu, ActionMap ordnet einen Aktionsnamen einem Action zu. Wenn der Benutzer eine Taste drückt, wird die Eingabezuordnung nach der Taste durchsucht und ein Aktionsname gefunden. Dann wird die Aktionszuordnung nach dem Aktionsnamen gesucht und führt die Aktion aus.

Sieht umständlich aus. Warum binden Sie die Benutzereingabe nicht direkt an die Aktion und entfernen den Aktionsnamen? Dann brauchen Sie nur eine Karte und nicht zwei.

Gute Frage! Sie werden sehen, dass dies eine der Faktoren ist, die die Tastenkombinationen leichter handhabbar machen (deaktivieren, neu binden usw.).

Ich möchte, dass Sie mir einen vollständigen Code dazu geben.

Nein (das Swing-Tutorial enthält Arbeitsbeispiele ).

Du gehtst mir auf die Nerven! Ich hasse dich!

So stellen Sie eine einzelne Schlüsselbindung her:

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

Beachten Sie, dass es 3 InputMaps gibt, die auf verschiedene Fokuszustände reagieren:

myComponent.getInputMap(JComponent.WHEN_FOCUSED);
myComponent.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
myComponent.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
  • WHEN_FOCUSED, der auch verwendet wird, wenn kein Argument angegeben wird, wird verwendet, wenn die Komponente den Fokus hat. Dies ist ähnlich wie bei dem Key Listener.
  • WHEN_ANCESTOR_OF_FOCUSED_COMPONENT wird verwendet, wenn sich eine fokussierte Komponente in einer Komponente befindet, die zum Empfangen der Aktion registriert ist. Wenn sich viele Besatzungsmitglieder in einem Raumschiff befinden und Sie möchten, dass das Raumschiff weiterhin Eingaben erhält, während eines der Besatzungsmitglieder den Fokus hat, verwenden Sie diese Option.
  • WHEN_IN_FOCUSED_WINDOW wird verwendet, wenn sich eine Komponente, die zum Empfangen der Aktion registriert ist, in einer fokussierten Komponente befindet. Wenn sich viele Tanks in einem fokussierten Fenster befinden und alle gleichzeitig Eingaben erhalten sollen, verwenden Sie diese Option.

Der in der Frage dargestellte Code sieht in etwa so aus, wenn beide Objekte gleichzeitig gesteuert werden:

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

Sie sehen, dass das Trennen der Eingabezuordnung von der Aktionszuordnung wiederverwendbaren Code und eine bessere Kontrolle der Bindungen ermöglicht. Darüber hinaus können Sie eine Aktion auch direkt steuern, wenn Sie die Funktionalität benötigen. Zum Beispiel:

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

Weitere Informationen finden Sie im Action-Tutorial .

Ich sehe, dass Sie 1 Aktion, Bewegung für 4 Tasten (Richtungen) und 1 Aktion, Feuer, für 1 Taste verwendet haben. Warum geben Sie jeder Taste keine eigene Aktion oder geben Sie allen Tasten dieselbe Aktion und sortieren Sie, was zu tun ist innerhalb der Aktion ausführen (wie in dem Verschiebungsfall)?

Guter Punkt. Technisch können Sie beides tun, aber Sie müssen sich überlegen, was sinnvoll ist und was eine einfache Verwaltung und wiederverwendbaren Code ermöglicht. Ich nahm an, dass das Bewegen für alle Richtungen ähnlich ist und das Schießen anders ist, also habe ich diesen Ansatz gewählt.

Ich sehe viele KeyStrokes, was sind die? Sind sie wie ein KeyEvent?

Ja, sie haben eine ähnliche Funktion, sind aber für die Verwendung hier besser geeignet. Sehen Sie sich ihre API für Informationen und wie Sie sie erstellen können.


Fragen? Verbesserungen? Vorschläge? Hinterlasse einen Kommentar . Hast du eine bessere Antwort? Veröffentliche es.

52
user1803551

Hinweis: Dies ist keine Antwort, nur ein Kommentar mit zu viel Code :-)

KeyStrokes über getKeyStroke (String) zu beziehen, ist der richtige Weg - aber das api doc muss sorgfältig gelesen werden:

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_".

Die letzte Zeile sollte besser genauer Name sein, das ist der Fall: Für die Abwärts-Taste lautet der genaue Tastencode VK_DOWN, daher muss der Parameter "DOWN" sein (nicht "Down" oder eine andere Variation von Kleinbuchstaben)

Nicht ganz intuitiv (gelesen: musste selbst ein bisschen Dig graben) bringt einen KeyStroke auf einen Modifier-Key. Selbst mit der richtigen Schreibweise funktioniert Folgendes nicht: 

KeyStroke control = getKeyStroke("CONTROL"); 

Weiter unten in der awt-Ereigniswarteschlange wird ein keyEvent für einen einzelnen Modifiziererschlüssel mit sich selbst als Modifizierer erstellt. Um an die Steuertaste zu binden, benötigen Sie den Strich: 

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

Hier ist eine einfache Methode, bei der Sie nicht hunderte von Codezeilen lesen müssen, sondern nur ein paar Zeilen lang lernen. 

deklarieren Sie ein neues JLabel und fügen Sie es Ihrem JFrame hinzu (ich habe es nicht in anderen Komponenten getestet)

private static JLabel listener= new JLabel(); 

Der Fokus muss auf diesem Punkt bleiben, damit die Tasten funktionieren.

Im Konstruktor: 

add(listener);

Verwenden Sie diese Methode:

ALTE METHODE:

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

KeyString muss richtig geschrieben werden. Es ist nicht typensicher und Sie müssen die offizielle list konsultieren, um zu erfahren, was der keyString (es ist kein offizieller Begriff) für jede Schaltfläche ist. 

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

In dieser neuen Methode können Sie es einfach mit KeyEvent.VK_WHATEVER einstellen.

BEISPIEL ANRUFE:

  setKeyBinding(KeyEvent.VK_CONTROL, new AbstractAction() {

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

        }
    });

Senden Sie eine anonyme Klasse (oder verwenden Sie eine Unterklasse) von AbstractAction. Überschreiben Sie public void actionPerformed(ActionEvent e) und machen Sie, was immer Sie wollen, damit der Schlüssel funktioniert.

PROBLEM: 

Ich konnte es nicht für VK_ALT_GRAPH zum Laufen bringen.

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

macht es aus irgendeinem Grund nicht für mich.

0
WVrock