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

时间:2014-03-30 06:57:38

标签: java swing key-bindings keyevent key-events

我在我的代码(游戏或其他方面)中使用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);
        }
    }
}

我的响应能力有问题:

  • 我需要点击对象才能使用。
  • 我按下其中一个键所得到的反应并不是我希望它如何工作 - 过于敏感或反应迟钝。

为什么会发生这种情况?如何解决这个问题?

4 个答案:

答案 0 :(得分:52)

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

  • 如何用Java编写游戏。
  • 代码编写应该如何(例如,可见性)。
  • 实现密钥绑定的最有效(以性能或代码方式)方式。

  • 对于遇到关键听众问题的人,我会发布什么作为答案

答案;阅读Swing tutorial on key bindings

  

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

嗯, Swing教程解释了

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

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

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

  

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

好问题!您将看到这是使键绑定更易于管理的事情之一(禁用,重新绑定等)。

  

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

否( Swing教程working examples)。

  

你很糟糕!我恨你!

以下是如何进行单键绑定:

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

您可以看到将输入映射与操作映射分开可以获得可重用的代码并更好地控制绑定。此外,如果需要该功能,还可以直接控制Action。例如:

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

有关详细信息,请参阅Action tutorial

  

我看到你使用1个动作,移动,4个键(方向)和1个动作,火,1个键。为什么不给每个键都有自己的动作,或者给所有键做同样的动作并在动作中做出如何处理(如移动情况)?

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

  

我看到很多KeyStroke被使用了,那是什么?它们是KeyEvent吗?

是的,他们有类似的功能,但更适合在这里使用。有关信息以及如何创建信息,请参阅他们的API


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

答案 1 :(得分:6)

注意:这是答案,只是代码太多的评论: - )

通过getKeyStroke(String)获取keyStrokes是正确的方法 - 但需要仔细阅读api doc:

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”(不是“向下”或大写/小写字母的任何其他变体)

不完全直观(阅读:我必须自己挖掘一下)正在获得一个修改键的KeyStroke。即使拼写正确,以下内容也不起作用:

KeyStroke control = getKeyStroke("CONTROL"); 

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

KeyStroke control = getKeyStroke("ctrl CONTROL"); 

答案 2 :(得分:0)

这是一个简便的方法,不需要学习几百行代码,只需学习几行长的技巧即可。

声明一个新的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必须正确编写。它不是类型安全的,您必须向官方list咨询以了解什么是每个按钮的keyString(这不是官方术语)。

新方法

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;

由于某种原因无法使其正常工作。

答案 3 :(得分:0)

这是一个如何使键绑定起作用的示例。

  

(在JFrame内的extends子类中,该子类由构造函数调用)

// Create key bindings for controls
private void createKeyBindings(JPanel p) {
    InputMap im = p.getInputMap(JPanel.WHEN_IN_FOCUSED_WINDOW);
    ActionMap am = p.getActionMap();
    im.put(KeyStroke.getKeyStroke("W"), MoveAction.Action.MOVE_UP);
    im.put(KeyStroke.getKeyStroke("S"), MoveAction.Action.MOVE_DOWN);
    im.put(KeyStroke.getKeyStroke("A"), MoveAction.Action.MOVE_LEFT);
    im.put(KeyStroke.getKeyStroke("D"), MoveAction.Action.MOVE_RIGHT);
    am.put(MoveAction.Action.MOVE_UP, new MoveAction(this, MoveAction.Action.MOVE_UP));
    am.put(MoveAction.Action.MOVE_DOWN, new MoveAction(this, MoveAction.Action.MOVE_DOWN));
    am.put(MoveAction.Action.MOVE_LEFT, new MoveAction(this, MoveAction.Action.MOVE_LEFT));
    am.put(MoveAction.Action.MOVE_RIGHT, new MoveAction(this, MoveAction.Action.MOVE_RIGHT));
}
  

用于处理上面创建的那些键绑定的单独类(其中WindowextendsJFrame的类)

// Handles the key bindings
class MoveAction extends AbstractAction {

    enum Action {
        MOVE_UP, MOVE_DOWN, MOVE_LEFT, MOVE_RIGHT;
    }


    private static final long serialVersionUID = /* Some ID */;

    Window window;
    Action action;

    public MoveAction(Window window, Action action) {
        this.window = window;
        this.action = action;
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        switch (action) {
        case MOVE_UP:
            /* ... */
            break;
        case MOVE_DOWN:
            /* ... */
            break;
        case MOVE_LEFT:
            /* ... */
            break;
        case MOVE_RIGHT:
            /* ... */
            break;
        }
    }
}
相关问题