【问题标题】:KeyBinding multiple keys pressed not workingKeyBinding多个键按下不起作用
【发布时间】:2021-04-18 22:43:19
【问题描述】:

所以我有一个绘制的矩形,我想用箭头键移动它,包括对角线移动,或者更确切地说,允许同时按下多个键(换句话说,移动类似于 2D 中的玩家移动游戏)。我试图用一个不起作用的 KeyListener 来做到这一点。所以我决定转向 KeyBindings,我从这个网站上找到了一个例子:https://coderanch.com/t/606742/java/key-bindings(Rob Camick 的帖子)

我直接复制了代码,它按预期工作,除了它正在移动一个图标而不是像我想要的那样绘制一个矩形。我试图修改代码,以便它可以移动一个绘制的矩形,但只是失败了。这是我最近的尝试,也是最小的可重复性:

import java.awt.event.*;
import java.awt.*;
import javax.swing.*;
 
public class Test extends JPanel implements ActionListener
{
    public JPanel component = this;
    public int x = 100;
    public int y = 100;
    private Timer timer;
    private int keysPressed;
    private InputMap inputMap;
 
    public void addAction(String name, int keyDeltaX, int keyDeltaY, int keyCode)
    {
        new NavigationAction(name, keyDeltaX, keyDeltaY, keyCode);
    }
 
    //  Move the component to its new location
 
    private void handleKeyEvent(boolean pressed, int deltaX, int deltaY)
    {
        keysPressed += pressed ? 1 : -1;

        x += deltaX;
        y += deltaY;
 
        //  Start the Timer when the first key is pressed
        
        if (keysPressed == 1)
            timer.start();
 
        //  Stop the Timer when all keys have been released
 
        if (keysPressed == 0)
            timer.stop();
    }
 
    //  Invoked when the Timer fires
 
    public void actionPerformed(ActionEvent e)
    {
        repaint();
    }
 
    class NavigationAction extends AbstractAction implements ActionListener
    {
        private int keyDeltaX;
        private int keyDeltaY;
 
        private KeyStroke pressedKeyStroke;
        private boolean listeningForKeyPressed;
 
        public NavigationAction(String name, int keyDeltaX, int keyDeltaY, int keyCode)
        {
            super(name);
 
            this.keyDeltaX = keyDeltaX;
            this.keyDeltaY = keyDeltaY;
            
            pressedKeyStroke = KeyStroke.getKeyStroke(keyCode, 0, false);
            KeyStroke releasedKeyStroke = KeyStroke.getKeyStroke(keyCode, 0, true);
 
            inputMap.put(pressedKeyStroke, getValue(Action.NAME));
            inputMap.put(releasedKeyStroke, getValue(Action.NAME));
            component.getActionMap().put(getValue(Action.NAME), this);
            listeningForKeyPressed = true;
        }
 
        public void actionPerformed(ActionEvent e)
        {
            //  While the key is held down multiple keyPress events are generated,
            //  we only care about the first event generated
            if (listeningForKeyPressed)
            {
                handleKeyEvent(true, keyDeltaX, keyDeltaY);
                inputMap.remove(pressedKeyStroke);
                listeningForKeyPressed = false;
            }
            else // listening for key released
            {
                handleKeyEvent(false, -keyDeltaX, -keyDeltaY);
                inputMap.put(pressedKeyStroke, getValue(Action.NAME));
                listeningForKeyPressed = true;
            }
        }
    }
    
    public void paintComponent(Graphics tool) {
        super.paintComponent(tool);
        System.out.println(x + ", " + y);
        tool.drawRect(x, y, 50, 50);
    }
    
    public Test() {}
 
    public static void main(String[] args)
    {
        new Test().create();
    }
    
    public void create() {
        JFrame frame = new JFrame();
        frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
        frame.setSize(600, 600);
        frame.setLocationRelativeTo( null );
        
        frame.getContentPane().add(component);
        inputMap = component.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
        
        timer = new Timer(24, this);
        timer.setInitialDelay( 0 );
        
        addAction("Left", -3,  0, KeyEvent.VK_LEFT);
        addAction("Right", 3,  0, KeyEvent.VK_RIGHT);
        addAction("Up",    0, -3, KeyEvent.VK_UP);
        addAction("Down",  0,  3, KeyEvent.VK_DOWN);
        
        frame.setVisible(true);
    }
 
}

【问题讨论】:

  • 我从这个网站上找到了一个例子:coderanch.com/t/606742/java/key-bindings - 这不是我在你上一个问题中提供给你的链接:stackoverflow.com/questions/67066490/…
  • @camickr 是的,这不是您提供的链接,但是,我确实尝试使用您的链接并使用它。我觉得没有必要同时发布这两个链接,而且由于您的链接看起来像是您写的,我不希望在您允许发布它时遇到任何麻烦,而且我也对另一个链接做了更多的工作.对此感到抱歉。但我想我有我的解决方案,有人在下面发布。无论如何,感谢您提供该代码,它确实让我深入了解了所有这些键绑定内容。
  • 我提供的链接将包含最新的建议,并且是我推荐的方法。你为什么要去寻找另一个链接?即使您从我这里找到了另一个链接,为什么还要在此处发布该代码,因为那不是我推荐的代码? 我在另一个链接上也做了更多的工作 - 那么与我推荐的链接相比,为什么你会花更多的时间在另一个链接上?提供我提供的链接是有原因的。
  • @camickr 所以我真的无法理解你推荐的那个,我感谢你推荐那个。我去寻找其他代码的原因是因为我有点放弃尝试修改您的代码。那时我发现了你的其他旧代码,而且,从我的立场来看,更清楚。所以,因为我更好地理解了其他代码,所以我能够更好地修改它。而且,由于我对其进行了更多修改,因此发布旧代码而不是您建议的代码似乎更合适。我希望我没有以任何方式冒犯您,我只是想让阅读此问题的每个人都清楚。

标签: java swing key-bindings


【解决方案1】:

好的,我对您提供的代码的一个问题是您承担了泄漏责任。 NavigationAction 不负责注册键绑定或修改 UI 的状态。它应该只负责生成状态已更改回负责处理程序的通知。

这可以通过使用某种模型来实现,该模型保持某种状态,由NavigationAction 更新并由 UI 读取,或者,正如我选择的那样,通过委托回调。

另一个问题是,当用户释放密钥时会发生什么?另一个问题是,你如何处理重复按键的延迟(即当你按住键时,第一个按键事件和重复按键事件之间有一个小的延迟)?

我们可以通过简单的心态改变来处理这些事情。与其使用NavigationAction 来确定应该发生什么,不如使用它来告诉我们的UI“何时”发生了某事。在这种情况下,方向键被按下或释放。

我们不是直接对击键做出反应,而是从状态模型中“添加”或“删除”方向,并允许 Swing Timer 相应地更新 UI 的状态,这通常会更快更流畅。

它还具有解耦逻辑的副作用,因为您不再需要关心状态“如何”更改,只需关心它是什么,然后您就可以决定如何对其做出反应。

这种方法通常(以某种形式)在游戏引擎中使用。

import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.util.HashSet;
import java.util.Set;
import javax.swing.AbstractAction;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.KeyStroke;
import javax.swing.Timer;

public class Test {

    public static void main(String[] args) {
        new Test();
    }

    public Test() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                JFrame frame = new JFrame();
                frame.add(new TestPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public interface KeyActionHandler {

        public void keyActionPerformed(Direction direction, boolean pressed);
    }

    public enum Direction {
        LEFT, RIGHT, DOWN, UP;
    }

    class NavigationAction extends AbstractAction implements ActionListener {

        private KeyActionHandler keyActionHandler;
        private Direction direction;
        private Boolean pressed;

        public NavigationAction(Direction direction, boolean pressed, KeyActionHandler keyActionHandler) {
            this.direction = direction;
            this.pressed = pressed;
            this.keyActionHandler = keyActionHandler;
        }

        public void actionPerformed(ActionEvent e) {
            keyActionHandler.keyActionPerformed(direction, pressed);
        }
    }

    public class TestPane extends JPanel implements ActionListener, KeyActionHandler {

        private int xDelta = 0;
        private int yDelta = 0;

        public int x = 100;
        public int y = 100;
        private Timer timer;

        public TestPane() {
            timer = new Timer(24, this);

            InputMap inputMap = getInputMap(WHEN_IN_FOCUSED_WINDOW);
            ActionMap actionMap = getActionMap();

            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0, false), "Pressed-Left");
            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0, false), "Pressed-Right");
            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0, false), "Pressed-Up");
            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0, false), "Pressed-Down");

            actionMap.put("Pressed-Left", new NavigationAction(Direction.LEFT, true, this));
            actionMap.put("Pressed-Right", new NavigationAction(Direction.RIGHT, true, this));
            actionMap.put("Pressed-Up", new NavigationAction(Direction.UP, true, this));
            actionMap.put("Pressed-Down", new NavigationAction(Direction.DOWN, true, this));

            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0, true), "Released-Left");
            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0, true), "Released-Right");
            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0, true), "Released-Up");
            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0, true), "Released-Down");

            actionMap.put("Released-Left", new NavigationAction(Direction.LEFT, false, this));
            actionMap.put("Released-Right", new NavigationAction(Direction.RIGHT, false, this));
            actionMap.put("Released-Up", new NavigationAction(Direction.UP, false, this));
            actionMap.put("Released-Down", new NavigationAction(Direction.DOWN, false, this));
        }

        @Override
        public Dimension getPreferredSize() {
            return new Dimension(400, 400);
        }

        @Override
        public void addNotify() {
            super.addNotify();
            timer.start();

        }

        @Override
        public void removeNotify() {
            super.removeNotify();
            timer.stop();
        }

        private Set<Direction> directions = new HashSet<>();

        //  Invoked when the Timer fires
        @Override
        public void actionPerformed(ActionEvent e) {

            if (directions.contains(Direction.LEFT)) {
                x -= 3;
            } else if (directions.contains(Direction.RIGHT)) {
                x += 3;
            }

            if (directions.contains(Direction.UP)) {
                y -= 3;
            } else if (directions.contains(Direction.DOWN)) {
                y += 3;
            }

            repaint();
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            g.drawRect(x, y, 50, 50);
        }

        @Override
        public void keyActionPerformed(Direction direction, boolean pressed) {            
            if (pressed) {
                directions.add(direction);
            } else {
                directions.remove(direction);
            }
        }

    }
}

这只是解决同一问题的另一种方法;)

但是,我遇到了一个问题:似乎永远不会调用 removeNotify()。我想验证这一点并复制了这篇文章并将其粘贴到一个新的 java 文件中。然后我在 add 和 remove notify 方法中添加了 print 语句,并且只打印了 addNotify() 方法中的 print 语句。另外,如果在 actionPerformed 方法中添加 print 语句,它会重复打印,永不结束

removeNotify 仅在组件或父容器被移除时调用。

import java.awt.Container;
import java.awt.Dimension;
import java.awt.GridBagLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

public class Test {

    public static void main(String[] args) {
        new Test();
    }

    public Test() {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                JFrame frame = new JFrame();
                frame.add(new TestPane());
                frame.pack();
                frame.setVisible(true);
            }
        });
    }

    public class TestPane extends JPanel {

        public TestPane() {
            setLayout(new GridBagLayout());
            JButton btn = new JButton("Remove");
            btn.addActionListener(new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent evt) {
                    Container parent = getParent();
                    parent.remove(TestPane.this);
                    parent.revalidate();
                    parent.repaint();
                }
            });
            add(btn);
        }

        @Override
        public Dimension getPreferredSize() {
            return new Dimension(200, 200);
        }

        @Override
        public void addNotify() {
            super.addNotify();
            System.out.println("Add");
        }

        @Override
        public void removeNotify() {
            super.removeNotify();
            System.out.println("Remove");
        }


    }
}

我验证了当父容器被移除时,removeNotify 将通过层次结构被调用。

【讨论】:

  • 哇,谢谢。我的代码不好的原因是我对这方面了解不多,抄了别人的代码。但是,我复制的代码有效,(您可以从网站复制并自己运行)。所以我想我可以尝试根据自己的喜好修改它,但我想我应该坚持你发布的内容。再次感谢。
  • @LuckyBandit74 这不是“坏”,只是我不会这样做,我想让你知道我为什么要这样修改它;)
  • 谢谢。我正在开发一个两人坦克(战斗)游戏,我需要 WASD 键独立于箭头键工作。我能够使用您的代码和想法来完成移动和射击功能。
  • @MadProgrammer 嘿,如果你还记得这个问题,我在触发多个键时遇到了麻烦,你帮助了这篇文章。但是,我遇到了一个问题:似乎永远不会调用 removeNotify() 。我想验证这一点并复制了这篇文章并将其粘贴到一个新的 java 文件中。然后我在 add 和 remove notify 方法中添加了 print 语句,并且只打印了 addNotify() 方法中的 print 语句。此外,如果您在 actionPerformed 方法中添加打印语句,它会重复打印,永无止境。如果你能弄清楚问题,那真的很有帮助
  • removeNotify 仅在组件本身从父容器中移除时调用
猜你喜欢
  • 2022-01-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-09-17
相关资源
最近更新 更多