KeyBinding 多个键按下不工作

KeyBinding multiple keys pressed not working

所以我有一个绘制的矩形,我想用箭头键移动,包括对角线移动,或者更确切地说允许同时按下多个键(换句话说,类似于玩家移动的移动2D 游戏)。我曾尝试使用无法正常工作的 KeyListener 来执行此操作。所以我决定转向 KeyBindings,我从这个网站找到了一个例子:https://coderanch.com/t/606742/java/key-bindings (Rob Camick's post)

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

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

好的,所以我对您提供的代码存在的问题之一是您负有泄漏责任。 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);
            }
        }

    }
}

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

However, I am encountering a problem: it seems that removeNotify() is never called. I wanted to verify this and copied this post and pasted it into a new java file. I then added print statements in the add and remove notify methods, and only the print statement in the addNotify() method was printed. Also, if you add a print statement in the actionPerformed method, it prints repeatedly never ending

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 将在删除父容器时通过层次结构调用。