experimenting with javafx and java 8

I’ve been working a bit with JavaFX for a little while now, looking to achieve complex graphical applications with more power and greater programmatic ease than I ever could with the Java Foundation Classes (AWT, Swing, and Java 2D). If nothing else, the ability to tool the look and feel through CSS (Cascading Style Sheets) is a huge advance over pure Java programming, But I digress…

I’ve had one application that uses JavaFX Canvas (javafx.scene.canvas.Canvas) for some complex rendering, but it hasn’t worked the way I wanted it to. So I backed up and created a simple JavaFX lab to work with and better understand how to use a JavaFX Canvas in a BorderPane. I did finally come to understand how to use the various components together. I’m sharing this in the hope that it’ll help anyone with similar needs and looking for a solution.


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.StackPane;
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;");

        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 main class is where the application is constructed and launched. The key to making this work is to create a class derived from Canvas.

package borderpaneexample;

import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;

/**
 *
 * @author wbeebe
 */

public class ResizeableCanvas extends Canvas {
    private double width, height;
    
    /**
     * 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) {
        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() {
        width = getWidth();
        height = getHeight();
        GraphicsContext gc = getGraphicsContext2D();

        gc.clearRect(0, 0, width, 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);
    }

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

The exposition for how this works is in the comments of ResizeableCanvas. But if you want to see where it was added, then check out highlighted lines 46-48 in the main class. All of this was done in NetBeans 8.2 as the editor/IDE, and Java 8 update 152. It’s going to be a while yet before I move to Java 9; all the tools I need just aren’t there yet.