yj2000
yj2000

Reputation: 63

How to move a rectangle using keys in java

I have been trying to find out how to move a rectangle with the arrow keys but there seems to be a problem.
I am using a KeyListener to detect all of the key inputs.
I do not know how to use a KeyBinding therefore I do not wish for the solution to have it.
I am planning to learn it right after mastering KeyListener. Please give me suggestions on how to fix it.

package expo;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JFrame;
import javax.swing.JPanel;

public class Expo extends JPanel implements KeyListener{
    int x = 0; 
    int y = 0;

    @Override
    public void keyTyped(KeyEvent e) {
        //System.out.println("Key Typed");
    }

    @Override
    public void keyPressed(KeyEvent e) {

        if(e.getKeyCode() == KeyEvent.VK_DOWN){
            System.out.println("Key Pressed" + e.getKeyCode());
            y = y + 2;       
        }
    }

    @Override
    public void keyReleased(KeyEvent e) {
        //System.out.println("Key Released");
    }

    public void paint(Graphics g){
        g.setColor(Color.BLUE);
        g.drawRect(x ,y,100,100);

        repaint();
    }

    public static void main(String[] args) throws InterruptedException {

        Expo expo = new Expo();
        JFrame f = new JFrame();

        f.setVisible(true);
        f.setSize(500,500);
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.addKeyListener(expo);

        f.add(expo);
        f.repaint();           
    }
}

Upvotes: 1

Views: 6090

Answers (2)

MadProgrammer
MadProgrammer

Reputation: 347194

  1. Use the key bindings API over KeyListener, it solves the focus related issues that KeyListener suffers from. How to Use Key Bindings
  2. Don't break the paint chain. If you override one of the paint methods, you must call it's super implementation.
  3. Avoid overriding paint, it's normally to high in the paint process and because of the way painting works, doesn't always get called when a child component is painted, which can cause some interesting issues. Convention recommends using paintComponent instead. See Painting in AWT and Swing and Performing Custom Painting for more details
  4. Try calling JFrame#setVisible last, after you have established the UI, you will find it causes less issues

As an example...

import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.KeyStroke;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;

public class Expo extends JPanel {

    int x = 0;
    int y = 0;

    public Expo() {
        bindKeyStrokeTo(WHEN_IN_FOCUSED_WINDOW, "down", KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0), new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                y += 2;
                if (y + 100 > getHeight()) {
                    y = getHeight() - 100;
                }
                repaint();
            }
        });
        bindKeyStrokeTo(WHEN_IN_FOCUSED_WINDOW, "up", KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0), new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                y -= 2;
                if (y < 0) {
                    y = 0;
                }
                repaint();
            }
        });
    }

    public void bindKeyStrokeTo(int condition, String name, KeyStroke keyStroke, Action action) {
        InputMap im = getInputMap(condition);
        ActionMap am = getActionMap();

        im.put(keyStroke, name);
        am.put(name, action);
    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        g.setColor(Color.BLUE);
        g.drawRect(x, y, 100, 100);
    }

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

    public static void main(String[] args) throws InterruptedException {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                    ex.printStackTrace();
                }

                JFrame frame = new JFrame("Testing");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.add(new Expo());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

}

That's all cool and everything, but now, when you hold down the key, the rectangle moves, pauses and then begins to move steadily!?

This is actually quite normal. Instead, what you can do, is establish an "update loop" which constantly monitors the state of a set of flags, makes decisions about what to do when those flags are set and updates the UI.

So, what this does, is sets up a Swing Timer which ticks every 40 milliseconds, checks the state of the current "vertical key state", updates the y position accordingly and schedules a repaint, this allows for a much more smoother movement as we're not reliant on the repeating key stroke.

This also demonstrates the power of the key bindings API, as it establishes a single Action to handle both the up and down movements and the associated key releases...neat

import java.awt.Color;
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 javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.KeyStroke;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;

public class Expo extends JPanel {

    int x = 0;
    int y = 0;

    public enum VerticalKey {

        UP, DOWN, NONE;
    }

    public enum HorizontalKey {

        LEFT, RIGHT, NONE;
    }

    private VerticalKey verticalKeyState = VerticalKey.NONE;
    private HorizontalKey horizontalKeyState = HorizontalKey.NONE;

    public Expo() {
        bindKeyStrokeTo(WHEN_IN_FOCUSED_WINDOW, "pressed.down", KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0), new VerticalAction(VerticalKey.DOWN));
        bindKeyStrokeTo(WHEN_IN_FOCUSED_WINDOW, "released.down", KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0, true), new VerticalAction(VerticalKey.NONE));
        bindKeyStrokeTo(WHEN_IN_FOCUSED_WINDOW, "pressed.up", KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0), new VerticalAction(VerticalKey.UP));
        bindKeyStrokeTo(WHEN_IN_FOCUSED_WINDOW, "released.up", KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0, true), new VerticalAction(VerticalKey.NONE));

        Timer timer = new Timer(40, new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                switch (verticalKeyState) {
                    case UP:
                        y -= 2;
                        break;
                    case DOWN:
                        y += 2;
                        break;
                }
                if (y + 100 > getHeight()) {
                    y = getHeight() - 100;
                } else if (y < 0) {
                    y = 0;
                }

                repaint();
            }
        });
        timer.start();
    }

    public void bindKeyStrokeTo(int condition, String name, KeyStroke keyStroke, Action action) {
        InputMap im = getInputMap(condition);
        ActionMap am = getActionMap();

        im.put(keyStroke, name);
        am.put(name, action);
    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        g.setColor(Color.BLUE);
        g.drawRect(x, y, 100, 100);
    }

    public void setVerticalKeyState(VerticalKey verticalKeyState) {
        this.verticalKeyState = verticalKeyState;
        System.out.println(verticalKeyState);
    }

    public void setHorizontalKeyState(HorizontalKey horizontalKeyState) {
        this.horizontalKeyState = horizontalKeyState;
    }

    public class VerticalAction extends AbstractAction {

        private VerticalKey verticalKey;

        public VerticalAction(VerticalKey verticalKeys) {
            this.verticalKey = verticalKeys;
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            setVerticalKeyState(verticalKey);
        }

    }

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

    public static void main(String[] args) throws InterruptedException {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                    ex.printStackTrace();
                }

                JFrame frame = new JFrame("Testing");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.add(new Expo());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

}

Upvotes: 1

user4668606
user4668606

Reputation:

Just change these two pieces of code:

@Override
public void keyPressed(KeyEvent e) {

    if(e.getKeyCode() == KeyEvent.VK_DOWN){
        System.out.println("Key Pressed" + e.getKeyCode());
        y = y + 2;

        repaint();//add this line to update the UI
    }
}

and

public void paint(Graphics g){
    super.paint(g);//you should always call the super-method first

    g.setColor(Color.BLUE);
    g.drawRect(x ,y,100,100);
}

This would fix the issue. Though i'd recommend to override paintComponent instead of paint (read the section about "The Paint Methods" in this article: Painting in AWT and Swing). Basically the changes are i made change the following:
In keyPressed this line: repaint(); will update the UI after the square moved. Actually the square was moved before aswell, but the changes won't show until the UI is updated. In paint this line: super.paint(g); makes the panel do it's default painting first, which includes clearing the complete panel. And i've removed the repaint(); call, since it's completely useless.

NOTE: if you use paintComponent instead of paint, you'll have to change the first call from super.paint(g) to super.paintComponent(g) to avoid a stackoverflow.

Upvotes: 0

Related Questions