web-dev-qa-db-ja.com

キーリスナーの代わりにキーバインディングを使用する方法

画面上のオブジェクトがユーザーキー入力に反応する方法として、コード(ゲームなど)で KeyListener sを使用しています。ここに私のコードがあります:

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

応答性に問題があります:

  • オブジェクトを機能させるには、オブジェクトをクリックする必要があります。
  • キーの1つを押すことで得られる反応は、私が望んでいた方法ではありません。反応が速すぎたり反応しすぎたりします。

なぜこれが起こり、どうすれば修正できますか?

16
user1803551

この回答では、教育目的でキーリスナーの代わりにキーバインディングを使用する方法について説明します。そうではない

  • Javaでゲームを書く方法
  • コードの書き方がどのように見えるべきか(例:可視性)
  • キーバインディングを実装するための最も効率的な(パフォーマンスまたはコードに関して)方法

それは

  • キーリスナーに問題がある人への回答として投稿するもの

回答; キーバインディングのスイングチュートリアル を読んでください。

マニュアルを読みたくありません。すでに持っている美しいコードの代わりにキーバインディングを使用したい理由を教えてください!

さて、Swingチュートリアル

  • キーバインディングでは、コンポーネントをクリックする必要はありません(フォーカスを与えるため):
    • ユーザーの観点から予期しない動作を削除します。
    • 2つのオブジェクトがある場合、オブジェクトを同時に移動することはできません。1つのオブジェクトのみが特定の時間にフォーカスを取得できるためです(異なるキーにバインドしても)。
  • キーバインディングは、保守と操作が簡単です:
    • ユーザーアクションの無効化、再バインド、再割り当てははるかに簡単です。
    • コードは読みやすいです。

OK、あなたは私にそれを試してみると納得させた。どのように機能しますか?

チュートリアルには、それに関する良いセクションがあります。キーバインディングには、2つのオブジェクトInputMapおよびActionMapが含まれます。 InputMapはユーザー入力をアクション名にマップし、ActionMapはアクション名をActionにマップします。ユーザーがキーを押すと、入力マップでキーが検索され、アクション名が検索されます。次に、アクションマップでアクション名が検索され、アクションが実行されます。

面倒に見えます。ユーザー入力をアクションに直接バインドし、アクション名を削除してみませんか?次に、2つではなく1つのマップのみが必要です。

良い質問!これは、キーバインディングを管理しやすくするものの1つであることがわかります(無効化、再バインドなど)。

これの完全な動作コードを教えてほしい

いいえ(Swingチュートリアルには 実例 があります)。

最悪だな! 嫌いです!

単一のキーバインディングを作成する方法は次のとおりです。

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

異なるフォーカス状態に反応する3つのInputMapsがあることに注意してください。

myComponent.getInputMap(JComponent.WHEN_FOCUSED);
myComponent.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
myComponent.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
  • WHEN_FOCUSEDは、引数が指定されていないときに使用されるものでもあり、コンポーネントにフォーカスがあるときに使用されます。これは、キーリスナーの場合に似ています。
  • WHEN_ANCESTOR_OF_FOCUSED_COMPONENTは、アクションを受け取るために登録されているコンポーネント内にフォーカスされたコンポーネントがある場合に使用されます。宇宙船内に多くの乗組員がいて、いずれかの乗組員が焦点を合わせている間も宇宙船に入力を継続させたい場合は、これを使用します。
  • WHEN_IN_FOCUSED_WINDOWは、アクションを受信するために登録されたコンポーネントがフォーカスされたコンポーネント内にある場合に使用されます。フォーカスされたウィンドウに多くの戦車があり、それらすべてが同時に入力を受信するようにする場合は、これを使用します。

質問で提示されたコードは、両方のオブジェクトが同時に制御されると仮定すると、次のようになります。

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

入力マップをアクションマップから分離すると、再利用可能なコードとバインディングのより良い制御が可能になることがわかります。さらに、機能が必要な場合は、アクションを直接制御することもできます。例えば:

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

詳細については、 アクションチュートリアル を参照してください。

4つのキー(方向)に1つのアクション、移動、1つのキーに1つのアクション、発射を使用したことがわかります。各キーに独自のアクションを与えたり、すべてのキーに同じアクションを与えたり、アクション内で何をすべきかを整理したりしないのはなぜですか(ムーブの場合のように)?

いい視点ね。技術的には両方を行うことができますが、何が理にかなっており、何が簡単な管理と再利用可能なコードを可能にするかを考える必要があります。ここでは、移動はすべての方向で類似しており、発射は異なると想定したため、このアプローチを選択しました。

多くのKeyStrokesが使用されていますが、それらは何ですか? KeyEventのようなものですか?

はい、同様の機能がありますが、ここでの使用により適しています。情報と作成方法については、 [〜#〜] api [〜#〜] を参照してください。


質問?改善点?提案?コメントを残す。より良い答えがありますか?投稿してください。

54
user1803551

注:これはnot回答ではなく、コメントが多すぎるコードです:-)

GetKeyStroke(String)を介してkeyStrokesを取得するのが正しい方法ですが、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_".

最後の行は正確な名前である必要があります。これは大文字小文字の問題です。ダウンキーの場合、正確なキーコード名はVK_DOWN、したがって、パラメータは「DOWN」(「Down」または大文字/小文字の他のバリエーションではない)でなければなりません

完全に直感的ではありません(読んでください:自分で少し掘り下げなければなりませんでした)が、修飾キーにKeyStrokeを取得しています。適切なスペルを使用しても、以下は機能しません。

KeyStroke control = getKeyStroke("CONTROL"); 

Awtイベントキューのさらに下の方に、単一の修飾キーのkeyEventが作成され、修飾キーとして使用されます。コントロールキーにバインドするには、ストロークが必要です。

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

ここでは、数百行のコードを読むだけで数行の長さのトリックを学ぶ必要のない簡単な方法を紹介します。

新しいJLabelを宣言し、それをJFrameに追加します(他のコンポーネントではテストしませんでした)

_private static JLabel listener= new JLabel(); 
_

キーが機能するためには、これに焦点を合わせる必要があります。

コンストラクター内:

_add(listener);
_

この方法を使用します。

古い方法:

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

KeyStringは適切に記述する必要があります。タイプセーフではないため、各ボタンのkeyString(公式の用語ではない)とは何かを知るには、公式の list を調べる必要があります。

新しい方法

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

この新しいメソッドでは、単に_KeyEvent.VK_WHATEVER_を使用して設定できます

例の呼び出し:

_  setKeyBinding(KeyEvent.VK_CONTROL, new AbstractAction() {

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

        }
    });
_

AbstractActionの匿名クラスを送信(またはサブクラスを使用)します。そのpublic void actionPerformed(ActionEvent e)をオーバーライドして、キーにしたいことを何でもさせます。

問題:

VK_ALT_GRAPHで実行できませんでした。

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

何らかの理由で機能しない.

0
WVrock