简体   繁体   中英

Java KeyAdapter vs. Swing Key Bindings?

I have a java swing program that I was previously controlling with a the KeyAdapter class. For several reasons, I have decided to switch over to using swing's built in key binding system (using InputMap and ActionMap ) instead. While switching, I have run into some confusing behaviors.

In order to test these systems, I have a simple JPanel:

public class Board extends JPanel {

    private final int WIDTH = 500;
    private final int HEIGHT = 500;
    
    private boolean eventTest = false;

    public Board() {
        initBoard();
        initKeyBindings();
    }

    // initialization
    // -----------------------------------------------------------------------------------------
    private void initBoard() {
        setPreferredSize(new Dimension(WIDTH, HEIGHT));
        setFocusable(true);
    }

    private void initKeyBindings() {

        getInputMap().put((KeyStroke.getKeyStroke(KeyEvent.VK_SHIFT, 0), "Shift Pressed");

        getActionMap().put("Shift Pressed", new AbstractAction() {
            public void actionPerformed(ActionEvent e) {
                eventTest = true;
            }
        });

    }

    // drawing
    // -----------------------------------------------------------------------------------------
    @Override
    protected void paintComponent(Graphics g) {
        // paint background
        super.paintComponent(g);

        g.setColor(Color.black);
        g.drawString("Test: " + eventTest, 10, 10);
        eventTest = false;
    }

Also in my program, I have a loop calling the repaint() method 10 times per second, so that I can see eventTest get updated. I am expecting this system to display eventTest as true on a frame where the shift key becomes pressed, and false otherwise. I also have tested other keys by changing the relevant key codes.

When I want to test the KeyAdapter, I add this block to the initBoard() method, and comment out initKeyBindings() in the constructor:

this.addKeyListener(new KeyAdapter() {
    @Override
    public void keyPressed(KeyEvent e) {
        if (e.getKeyCode() == KeyEvent.VK_SHIFT) {
            eventTest = true;
        }
    }
});

When using the KeyAdapter class, this works as expected. However, when I switch over to using key bindings, it becomes confusing. For some reason, eventTest is only displayed as true when I press down both shift keys. If I hold either shift key down, event test becomes true on the frame when I press the other, and then returns to false. I would like it to do this when one shift key is pressed, without having to hold the other one.

Additionally, when I set it to trigger on right arrow presses instead, a slightly different behavior happens. In both the KeyAdapter and key bindings modes, what happens is that eventTest becomes true on the frame I press the right arrow, returns to false for a short time, and then becomes true for as long as I hold the arrow. From reading the documentation online, it appears that this is caused by an OS dependent behavior (I am running Ubuntu 18.04) to continue sending out KeyPressed events while a key is held down. What I am confused about is why this behavior would be different for the shift key than for the right arrow. If possible, I would like to find a way to make eventTest true only on the first frame a key is pressed.

Any ideas as to what is causing this? Thanks!

I have found at least a partial answer.

For the issue where I had to hold down both shift keys to generate a key pressed event when using key bindings, there is a simple fix. All that needs to be done is to change the what is added to the InputMap from:

getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_SHIFT, 0), "pressed");

to

getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_SHIFT, KeyEvent.SHIFT_DOWN_MASK), "pressed");

I am not completely sure why the input map counts pressing a single shift key as as KeyEvent with a key code of VK_SHIFT AND the SHIFT_DOWN_MASK , but that appears to be what it is doing. It would make more intuitive sense to me if the mask was applied only for if there is already one shift key pressed and the user attempts to press the other one, but interestingly, this binding no longer detects events for if one shift key is held and the other is pressed. Weird.

The problems with other keys have slightly less clean solutions. As to the question of why shift behaves differently than other keys. I believe this is an intentional design built into the OS. For example, if the user presses and holds the right arrow (or many other keys, such as the every text character key), it is reasonable to assume that they want to repeat the action that is tied to that key. Ie if a user is typing, and presses and holds "a", they likely want to input multiple "a" characters in quick succession into the text document. However, auto-repeating the shift key in a similar manner is not (in most cases) useful to the user. Therefore, it makes sense that no repeated events for the shift key are generated. I don't have any sources to back this up, it is just a hypothesis, but it makes sense to me.

In order to remove these extra events, there doesn't seem to be a good solution. One thing that works, but is sloppy, is to store a list of all keys currently pressed, and then have your action map check if the key is pressed before executing its action. Another approach would be to use timers and ignore events that occur to close in time to one another (see this post for more details). Both of these implementations require more memory usage and code for every key you wish to track, so they are not ideal.

A slightly better solution (IMO) can be achieved using KeyAdapter instead of Key Bindings. The key to this solution lies in the fact that pressing down one key while another is held will interrupt the stream of auto-repeat events, and it will not resume again for the original key (even if the second key is released). Because of this, we really only have to track the last key pressed in order to accurately filter out all auto-repeat events, because that is the only key that could be sending those events.

The code would look something like this:

addKeyListener(new KeyAdapter() {
    @Override
    public void keyPressed(KeyEvent e) {
        int keyCode = e.getKeyCode();
        
        if (keyCode != lastKeyPressed && keyCode != KeyEvent.VK_UNDEFINED) {
            // do some action
            lastKeyPressed = keyCode;
        }
    }

    @Override
    public void keyReleased(KeyEvent e) {
        // do some action
        lastKeyPressed = -1; // indicates that it is not possible for any key 
                             // to send auto-repeat events currently
    }
});

This solution of course looses some of the flexibility provided by swing's key binding system, but that has an easier workaround. You can create your own map of int to Action (or really any other type that is convenient to describe what you want to do), and instead of adding key bindings to InputMap s and ActionMap s, you put them in there. Next, instead of putting the direct code for the action you want to do inside of the KeyAdapter , put something like myMap.get(e.getKeyCode()).actionPerformed(); . This allows you to add, remove, and change key bindings by performing the corresponding operation on the map.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM