experimenting with javafx and java 8, part 2

As I continued to work a bit with the example application from yesterday, I started adding more code to the ResizeableCavas class so it would draw more stuff. In particular I wanted a grid drawn underneath the target as shown above. That’s when the aversion to too many lines of code in a given method or class kicked in, and I reorganized the application to break up functionality into something more manageable and reusable.

Thus, the creation of a CanvasLayer interface and two implementations using the interface, and some simple extensions to add those layers to the ResizeableCanvas. New code to follow.

package borderpaneexample;

import javafx.scene.canvas.GraphicsContext;

/**
 *
 * @author wbeebe
 */
public interface CanvasLayer {
    void draw(GraphicsContext gc, double width, double height);
}

And the two implementations.

package borderpaneexample;

import javafx.scene.canvas.GraphicsContext;
import javafx.scene.paint.Color;

/**
 *
 * @author wbeebe
 */
public class GridLayer implements CanvasLayer {

    @Override
    public void draw(GraphicsContext gc, double width, double height) {
        gc.clearRect(0, 0, width, height);
        // lighter than LIGHTGRAY
        gc.setStroke(Color.rgb(230, 230, 230));

        // Create all the vertical lines.
        //
        for (double i = 0; i <= width; i += 10) {
            gc.setLineWidth((i % 100) == 0 ? 2 : 1);
            gc.strokeLine(i, 0, i, height-2);
        }

        // Create all the horizontal lines.
        //
        for (double j = 0; j <= height; j += 10) {
            gc.setLineWidth((j % 100) == 0 ? 2 : 1);
            gc.strokeLine(0, j, width-2, j);
        }
    }
}
package borderpaneexample;

import javafx.scene.canvas.GraphicsContext;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;

/**
 *
 * @author wbeebe
 */
public class TargetLayer implements CanvasLayer {

    @Override
    public void draw(GraphicsContext gc, double width, double height) {
        gc.fillOval((width - 40)/2.0, (height - 40)/2.0, 40, 40);

        Font font = gc.getFont();
        gc.fillText("Width: " + Double.toString(width), width/2 + 2, font.getSize());

        // Some interesting graphics context manipulation. Move to the left
        // edge and print the height, rotated 90 degrees parallel to the
        // left edge.
        //
        gc.save();
        gc.translate(font.getSize(), height/2);
        gc.rotate(-90);
        gc.fillText("Height: " + Double.toString(height), 2, 0);
        gc.restore();

        gc.setStroke(Color.RED);
        gc.setLineWidth(2);
        gc.strokeLine(0, 0, width, height);
        gc.strokeLine(0, height, width, 0);
        gc.strokeLine(width/2.0, 0, width/2.0, height);
        gc.strokeLine(0, height/2.0, width, height/2.0);
    }
}

And now the reworked ResizeableCanvas. A new method was implemented to add CanvasLayers to be drawn. All changes to support a list of CanvasLayers are highlighted.

package borderpaneexample;

import java.util.ArrayList;
import java.util.List;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.layout.Region;

/**
 *
 * @author wbeebe
 */

public class ResizeableCanvas extends Canvas {
    List<CanvasLayer>canvasLayerList;
    /**
     * ResizeableCanvas provides a resizeable Canvas.
     * 
     * Resizeable Canvas requires that it be wrapped in a Region.
 
 To use, follow these steps:
 1) Instantiate a wrap around Region instance, such as Pane;
 2) Instantiate a ResizeableCanvas with the Region class instance;
 3) Call the Region's getChildren().add(...) method to add the
    ResizeableCanvas instance to the Region instance.
 
 This is especially effective, and required, if you want a resizable
 Canvas in the center of a BorderPane and want to use any of the other
 BorderPane regions.
 
 Internally the Resizable Canvas is bound to the wrapper Region's
 width and heigh property; when the wrapper Region is resized, then
 the Resiable Canvas' resizeDraw() method is called, producing the effect of
 resizing through redrawing.
 
 For this to work, isResizable() must be overridden to return true,
 prefWidth() overridden to return the current width, and
 prefHeight() overridden to return the current height.
     * 
     * @param region 
     */
    public ResizeableCanvas(Region region) {
        this.canvasLayerList = new ArrayList<>();
        widthProperty().bind(region.widthProperty());
        heightProperty().bind(region.heightProperty());
        widthProperty().addListener(event -> resizeDraw() );
        heightProperty().addListener(event -> resizeDraw() );
    }

    /**
     * Basically shows how to use the GraphicsContext to resizeDraw into the Canvas.
     * Any graphic operation that can be supported by a Canvas can be performed
     * here on a resize event.
     */
    private void resizeDraw() {
        double width = getWidth();
        double height = getHeight();
        GraphicsContext gc = getGraphicsContext2D();

        canvasLayerList.forEach((canvasLayer) -> {
            canvasLayer.draw(gc, width, height);
        });
    }

    public void addLayer(CanvasLayer canvasLayer) {
        canvasLayerList.add(canvasLayer);
    }

    @Override
    public boolean isResizable() {
      return true;
    }
 
    @Override
    public double prefWidth(double height) {
      return getWidth();
    }
 
    @Override
    public double prefHeight(double width) {
      return getHeight();
    }
}

And finally the main class. Again, code for implementing the ResizeableCanvas and its changes are highlighted.

package borderpaneexample;

import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

/**
 *
 * @author wbeebe
 */
public class BorderPaneExample extends Application {
    
    @Override
    public void start(Stage primaryStage) {
        BorderPane borderPane = new BorderPane();

        Button rightButton = new Button("Right");
        rightButton.setOnAction((ActionEvent event) -> {
            System.out.println("Right button");
        });

        Button leftButton = new Button("Left");
        leftButton.setOnAction((ActionEvent event) -> {
            System.out.println("Left button");
        });

        Button topButton = new Button("Top");
        topButton.setOnAction((ActionEvent event) -> {
            System.out.println("Top button");
        });

        Button bottomButton = new Button("Bottom");
        bottomButton.setOnAction((ActionEvent event) -> {
            System.out.println("Bottom button");
        });
        
        Pane centerPane = new Pane();
        ResizeableCanvas resizeableCanvas = new ResizeableCanvas(centerPane);
        centerPane.getChildren().add(resizeableCanvas);
        centerPane.setStyle(
            "-fx-padding: 0;" +
            "-fx-border-style: solid inside;" +
            "-fx-border-width: 1;" +
            "-fx-border-insets: 0;" +
            "-fx-border-radius: 0;" +
            "-fx-border-color: #000;");
        resizeableCanvas.addLayer(new GridLayer());
        resizeableCanvas.addLayer(new TargetLayer());

        borderPane.setCenter(centerPane);
        borderPane.setRight(rightButton);
        BorderPane.setAlignment(rightButton, Pos.CENTER_RIGHT);
        VBox vbox = makeVBox();
        vbox.getChildren().add(leftButton);
        borderPane.setLeft(vbox);
        BorderPane.setAlignment(leftButton, Pos.CENTER_LEFT);
        HBox hbox = makeHBox();
        hbox.getChildren().add(topButton);
        borderPane.setTop(hbox);
        BorderPane.setAlignment(topButton, Pos.TOP_CENTER);
        borderPane.setBottom(bottomButton);
        BorderPane.setAlignment(bottomButton, Pos.BOTTOM_CENTER);
        borderPane.setStyle(
            "-fx-padding: 2;" +
            "-fx-border-style: solid inside;" +
            "-fx-border-width: 2;" +
            "-fx-border-insets: 2;" +
            "-fx-border-radius: 0;" +
            "-fx-border-color: #cccccc;");


        Scene scene = new Scene(borderPane, 800, 600);
        primaryStage.setTitle("BorderPane Example with Canvas");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        launch(args);
    }

    public HBox makeHBox() {
        HBox hbox = new HBox();
        hbox.setPadding(new Insets(5, 5, 5, 5));
        hbox.setSpacing(5);
        hbox.setStyle("-fx-background-color: #cccccc;");
        return hbox;
    }

    public VBox makeVBox() {
        VBox vbox = new VBox();
        vbox.setPadding(new Insets(5, 5, 5, 5));
        vbox.setSpacing(5);
        vbox.setStyle("-fx-background-color: #dddddd;");
        return vbox;
    }
}

The order that CanvasLayers are added is important, as that’s the order in which they are rendered on a call to ResizeableCanvas’ resizeDraw() method. It’s also important to realize that the lowest layer to render should have a call to GraphicsContext’s clearRect(…) method in order to clear the Canvas area before anything is redrawn. Else ResizeableCanvas rapidly turns into a mess every time the application is resized. Comment out the addition of GridLayer (line 55 above) so that only TargetLayer is added and watch what happens as you resize the application.

Not sure at this point what to do next. A map could easily be displayed in a layer, but ResizeableCanvas doesn’t know about a viewport into an arbitrarily larger context, or the ability to move around within it using sliders. Something else to contemplate.