Thursday, September 2, 2010

Interactive Keyboard Input In Java: KeyListeners

In a console application, you can get keyboard input using the Scanner class, as described in Keyboard Input for Console Apps. In an graphical app, though, you can use one of the classes built to accept text input (e.g. TextArea or JTextField) or add code to your application to respond directly to the keyboard.

Keyboard input in the Java GUI made simple.
Playing with Today's Program

There are two basic ways of doing this. One is to set up Key Bindings, which maps keystrokes to actions in your application similar to accelerator keys or menu keyboard equivalents. The other is to use a Key Listener, similar to the Mouse Listener, which I detailed in Simple Mouse Interaction.

In this example we're going to use Key Listeners. There is less overhead to setting up a KeyListener when you just need to use a few keys. Key Bindings require more overhead to set up, but when you want to bind actions to a lot of different keystrokes, and manage the actions bound to particular keystrokes at a higher level, Key Bindings are better to use than a simple KeyListener.

As its name implies, a KeyListener is an Event Listener. If you're not sure what that is, read my article on Listeners or follow the prior link to Oracle/Sun's description.

Here's a program that demonstrates simple keyboard interaction. It's based on the MousePanel program I presented in Simple Mouse Interaction. It acts as a sort of "Etch-a-Sketch". You can download the KeyPanel program source from my Java code site.

// Import the basic necessary classes.
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

public class KeyPanel extends JPanel implements KeyListener{

public KeyPanel(){
super();
pointX=0;
pointY=0;
oldX=0;
oldY=0;
addKeyListener(this);
}

int pointX, pointY, oldX, oldY;
boolean erase;

public void paintComponent(Graphics g){
// Erase the board if it's been requested.
if (erase) {
g.clearRect(0, 0 , getBounds().width, getBounds().height);
erase = false; // We're done, turn off this flag now.
}

// Draw gray where the pointer was..
g.setColor(Color.GRAY);
g.fillRect(oldX-2, oldY-2, 4, 4);
// Draw "Cursor" at current location in black.
g.setColor(Color.BLACK);
g.fillRect(pointX-2,pointY-2, 4, 4);
}

public void keyPressed(KeyEvent key){

// Copy the last clicked location into the 'old' variables.
oldX=pointX;
oldY=pointY;
// Move the current point depending on which key was pressed.
if (key.getKeyCode() == key.VK_DOWN){
pointY=pointY+5;
if (pointY > getBounds().height){
pointY=getBounds().height;
}
}
if (key.getKeyCode() == key.VK_UP){
pointY=pointY-5;
if (pointY < 0){pointY=0;}
}
if (key.getKeyCode() == key.VK_LEFT){
pointX=pointX-5;
if (pointX < 0){pointX=0;}
}
if (key.getKeyCode() == key.VK_RIGHT){
pointX=pointX+5;
if (pointX > getBounds().width){
pointX=getBounds().width;
}
}

// Set a flag to erase the screen if Space is pressed.
if (key.getKeyCode() == key.VK_SPACE){
erase = true;
}


// Tell the panel that we need to redraw things.
repaint();
}

/* The following methods have to be here to comply
with the MouseListener interface, but we don't
use them, so their code blocks are empty. */
public void keyTyped(KeyEvent key){ }
public void keyReleased(KeyEvent key){ }

public static void main(String arg[]){
JFrame frame = new JFrame("Use Arrows to Draw, Space to Erase.");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(640,480);

KeyPanel panel = new KeyPanel();
frame.setContentPane(panel);
frame.setVisible(true);

// We *must* do this to see KeyEvents.
panel.setFocusable(true);


// Initialize the drawing pointer.
panel.oldX=panel.getBounds().width/2;
panel.oldY=panel.getBounds().height/2;
panel.pointX=panel.oldX;
panel.pointY=panel.oldY;

}
}

Using this technique with the Simple Video Game Kernel would be similar. The VGKernel would extend KeyListener, register itself, and implement the KeyListener methods. But in those methods, rather than performing the operations that result from the keypress, as in this program, you would want to simply set a flag to show that the key has been pressed. Then, in your core game logic you would test to see whether the key has been pressed, and perform the appropriate actions.

That way the actions are performed at the appropriate time in your game, and not just whenever the key happens to get pressed. Reacting to a key when it is pressed is appropriate for a turn-based game, but not for a real-time game. In a real-time game the action happens according to the timing of the TimerTask that drives the game, which is why we just note that a key has been pressed, and wait until the TimerTask occurs to actually conduct the action related to that key. This would be similar to what we do with the space key here, which sets a flag to tell paintComponent() to erase the screen.

Give this program a try, see if you can extend it to allow the user to select colors to draw with or change the size of the drawing pen.