【问题标题】:How to use Key Bindings instead of Key Listeners如何使用键绑定而不是键侦听器
【发布时间】:2026-01-06 02:00:02
【问题描述】:

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

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

我的响应能力有问题:

  • 我需要单击对象才能使其工作。
  • 按下其中一个键得到的响应不是我希望的那样 - 响应太快或太迟钝。

为什么会发生这种情况,我该如何解决?

【问题讨论】:

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


    【解决方案1】:

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

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

    单独的类来处理上面创建的那些键绑定(其中Windowextends 来自JFrame 的类)

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

    【讨论】:

      【解决方案2】:

      这是一个简单的方法,不需要您阅读数百行代码,只需学习几行长的技巧。

      声明一个新的 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;
      

      由于某种原因,它对我不起作用。

      【讨论】:

      • 1) 仅在 JLabel 拥有焦点时才有效(大多数其他组件都会拥有它); 2)“靠你自己”或咨询official documentation
      • 操作员正在编写游戏,所以我认为 JLabel 不可能失去焦点。但是他们不能通过listener.requestFocus(); 再次获得关注吗?我本人现在正在检查文档,并将很快更新我的答案
      • 请再次检查我的答案,如果我犯了任何错误,请通知我。我不想误导任何人。我已经尽我所能对其进行了测试,到目前为止它正在运行。
      • (1) 不一定是游戏(问题说明“游戏或其他”)。 (2)有2个JLabels,怎么可能两个都有焦点? (3) 用requestFocus() 获得焦点是很糟糕的,因为那样你就不能让 anything 获得焦点(比如菜单)。 (3) Alt Graph 并非在所有键盘上都存在。 (4) 我建议您先测试所有内容,然后再发布,而不是发布猜测(“需要确认”)。如果有问题,请搜索或询问您是否找不到答案,而不是在答案中发布问题。
      • alt 图形存在于我的键盘中,但按下时不会触发。
      【解决方案3】:

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

      • 如何用 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 个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.
              }
          }
      }
      

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

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

      请参阅Action tutorial 了解更多信息。

      我看到您使用 1 个动作,移动,用于 4 个键(方向)和 1 个动作,火,用于 1 个键。为什么不给每个键一个单独的动作,或者给所有的键一个相同的动作,然后在动作中理清要做什么(比如在移动的情况下)?

      好点子。从技术上讲,您可以两者兼得,但您必须考虑什么是有意义的,什么是易于管理和可重用代码的。这里我假设所有方向的移动都是相似的,射击是不同的,所以我选择了这种方法。

      我看到很多KeyStrokes 被使用,它们是什么?他们像KeyEvent吗?

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


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

      【讨论】:

      • 很好地解释了键绑定。 ... code will look something like this assuming both objects are to be controlled at the same time: - 我不认为这里提供的代码将支持同时移动两个对象。 KeyBinding 只在最后一个按键被按下时调用。如果按住两个键,则只会调用最后一个键的操作。如果我错了,我很想看到一个可行的例子。 Have a better answer? - 查看 Motion Using the Keyboard 中的 KeyBoard Animation 示例。
      【解决方案4】:

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

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

      最后一行最好是exact name,大​​小写很重要:对于向下键,确切的键代码名称是VK_DOWN,所以参数必须是“DOWN”(而不是“Down” " 或任何其他大小写字母变体)

      不完全直观(阅读:必须自己挖掘一点)正在将 KeyStroke 设置为修饰键。即使拼写正确,以下内容也不起作用:

      KeyStroke control = getKeyStroke("CONTROL"); 
      

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

      KeyStroke control = getKeyStroke("ctrl CONTROL"); 
      

      【讨论】:

        最近更新 更多