如何使用键绑定而不是键侦听器

2022-09-02 14:18:55

我在代码(游戏或其他方式)中使用 KeyListener作为屏幕上的对象对用户键输入做出反应的方式。这是我的代码:

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

此答案解释并演示了如何出于教育目的使用键绑定而不是键侦听器。它不是

  • 如何用Java编写游戏。
  • 代码编写应该是什么样子(例如可见性)。
  • 实现密钥绑定的最有效(性能或代码方面)方法。

是的

  • 我会发布给任何与关键听众有麻烦的人的答案

答案;阅读有关键绑定的 Swing 教程

我不想阅读手册,告诉我为什么我要使用键绑定而不是我已经拥有的漂亮代码!

好吧,Swing教程解释了

  • 键绑定不需要您单击组件(以使其获得焦点):
    • 从用户的角度删除意外行为。
    • 如果您有 2 个对象,它们不能同时移动,因为在给定时间只有 1 个对象可以具有焦点(即使您将它们绑定到不同的键)。
  • 键绑定更易于维护和操作:
    • 禁用、重新绑定、重新分配用户操作要容易得多。
    • 代码更易于阅读。

好吧,你说服我尝试一下。它是如何工作的?

本教程有一个很好的部分。键绑定涉及 2 个对象和 . 将用户输入映射到操作名称,将操作名称映射到 .当用户按下某个键时,在输入映射中搜索该键并找到操作名称,然后在操作映射中搜索操作名称并执行操作。InputMapActionMapInputMapActionMapAction

看起来很麻烦。为什么不将用户输入直接绑定到操作并删除操作名称?那么你只需要一张地图,而不是两张。

问得好!您将看到,这是使密钥绑定更易于管理(禁用、重新绑定等)的原因之一。

我希望你给我一个完整的工作代码。

否(Swing 教程工作示例)。

你糟透了!我恨你!

下面介绍如何进行单个键绑定:

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

请注意,有3秒对不同的焦点状态做出反应:InputMap

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

有关详细信息,请参阅操作教程

我看到你用了1个动作,移动,4个键(方向)和1个动作,开火,1个键。为什么不为每个键提供自己的操作,或者为所有键提供相同的操作并整理出在操作中要执行的操作(如在移动案例中)?

好点。从技术上讲,你可以同时做这两件事,但你必须考虑什么是有意义的,什么是允许轻松管理和可重用代码的。在这里,我假设移动对于所有方向都是相似的,而射击是不同的,所以我选择了这种方法。

我看到很多使用的关键行程,这些是什么?它们像KeyEvent吗?

是的,它们具有类似的功能,但更适合在此处使用。有关信息以及如何创建它们的信息,请参阅其 API


问题?改进?建议?发表评论。有更好的答案吗?发布它。


答案 2

注意:这不是一个答案,只是一个代码太多的注释:-)

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

最后一行最好是确切的名称,这是情况:对于向下键,确切的键代码名称是,因此参数必须是“DOWN”(而不是“Down”或任何其他大写/小写字母的变体)VK_DOWN

不完全直观(阅读:不得不自己挖掘一下)是将KeyStroke转换为修饰键。即使拼写正确,以下内容也不起作用:

KeyStroke control = getKeyStroke("CONTROL"); 

在 awt 事件队列的更深处,将创建单个修饰键的 keyEvent,其自身作为修饰符。要绑定到控制键,您需要笔划:

KeyStroke control = getKeyStroke("ctrl CONTROL");