NetBeans Project

The second maze drawing version

In the second version of the maze drawing program we will add the ability to place objects in the maze in addition to the walls.

The main new feature we are going to demonstrate in the code for this version is the ability to handle keyboard events. The user will be able to do a number of things in the application by pressing specific keys on the keyboard.

  1. The user can delete the last wall placed by pressing the backspace key.
  2. The user can switch from wall drawing mode to object placing mode by pressing the 'p' key on the keyboard.
  3. The user can change the color of the objects they place by pressing the 'r' key for red, the 'g' key for green, and the 'b' key for blue.

Adding treasure to the map

The objects we are going to be placing in the maze are simple rectangles that represent treasure. To implement these new objects we set up a Treasure class:

public class Treasure {

    private Rectangle rect;
    private Color color;
    
    private double snap(double x) {
        double n = Math.floor(x / 40);
        return n * 40.0;
    }
    
    public Treasure(double x, double y,Color c) {
        this.color = c;
        rect = new Rectangle();
        rect.setX(x-15.0);
        rect.setY(y-15.0);
        rect.setWidth(30.0);
        rect.setHeight(30.0);
        rect.setFill(color);
    }
    
    public Shape getShape() { return rect; }
    
    public void setLocation(double x, double y) {
        rect.setX(x-15.0);
        rect.setY(y-15.0);
    }
    
    public void snapToGrid(double x, double y) {
        rect.setX(snap(x)+ 5.0);
        rect.setY(snap(y)+ 5.0);
    }
    
    public void write(PrintWriter out) {
        String colorStr = "red";
        if(color == Color.BLUE)
            colorStr = "blue";
        else if(color == Color.GREEN)
            colorStr = "green";
        
        out.println(Math.round(rect.getX()) + " " + Math.round(rect.getY())
                + " " + colorStr);
    }
}

A Treasure object is essentially a container for a colored rectangle. Just like the Wall class, the Treasure class contains methods to support dragging the treasure object around on the screen and a method to write itself out to a text file.

Now that the MazePane needs to support drawing both wall segments and Treasure objects, we need to add some additional member variables to the MazePane class:

private ArrayList<Wall> walls;
private ArrayList<Treasure> treasure;
private Wall dragging;
private Treasure placing;
private Color color;
private boolean drawingWalls;

The dragging member variable gives us access to the current Wall that we are dragging when drawing a wall, while the placing member variable gives us access to the current Treasure object that we are dragging when placing treasure on the map. The color variable will allow us to remember what color should be used when placing new Treasure objects. The drawingWalls member variable will be true when we are in wall drawing mode and false when we are in treasure placing mode.

Adding support for key events

Since this application also needs to respond to key presses on the keyboard, we need to add an additional setOn method call in the constructor for the MazePane class:

this.setOnKeyPressed(e -> keyPress(e));

When the user presses a key on the keyboard the keyPress() method in the MazePane will be called.

public void keyPress(KeyEvent e) {
    switch (e.getCode()) {
        case R:
            color = Color.RED;
            break;
        case G:
            color = Color.GREEN;
            break;
        case B:
            color = Color.BLUE;
            break;
        case BACK_SPACE:
            Wall toRemove = walls.get(walls.size() - 1);
            walls.remove(toRemove);
            this.getChildren().remove(toRemove.getShape());
            break;
        case P:
            drawingWalls = !drawingWalls;
            break;
    }
}

A method that responds to key press events takes a single parameter of type javafx.scene.input.KeyEvent. This KeyEvent object carries with it information about the key press event. The getCode() method of the KeyEvent object returns a numeric code that identifies the key on the keyboard that the user pressed. We typically use a switch statement to set up different cases for the different keys we expect the user to press. The constants used in the switch statement are defined in a KeyCode enumeration. You can find a full list of available key codes here.

There is one last thing we will need to do to get key events working correctly in our application. The code that I showed above is sufficient to get the MazePane to respond to key events, but we also need to tell the system that the MazePane is interested in receiving key events in the first place. In JavaFX we do this by calling the requestFocus() method in our pane class. An unfortunate requirement that JavaFX has for us is that the requestFocus() call will only work correctly after the MazePane has been placed in a scene. This means that we can't call requestFocus() in the MazePane's constructor, since that constructor will get called before the MazePane is placed in a scene. We also can't call this method in the initialize() method of the application's controller, since that also will get called before the MazePane is installed in a scene. The only way around these restrictions is to put a special method in the controller class and then call that method at the right time. The method that we put in the controller class is

public void focusMaze() {
    maze.requestFocus();
}

and the code that calls it goes into the start() method in the App class:

@Override
public void start(Stage stage) throws IOException {
    FXMLLoader loader = new FXMLLoader(App.class.getResource("primary.fxml"));
    Scene scene = new Scene(loader.load(), 640, 480);
    PrimaryController controller = (PrimaryController) loader.getController();
    controller.focusMaze();
    stage.setScene(scene);
    scene.getRoot().setStyle("-fx-font-family: 'sans-serif'");
    stage.setTitle("Maze Drawing");
    stage.show();
}