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;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;public class GridLayer implements CanvasLayer {@Overridepublic void draw(GraphicsContext gc, double width, double height) {gc.clearRect(0, 0, width, height);// lighter than LIGHTGRAYgc.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;public class TargetLayer implements CanvasLayer {@Overridepublic 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;public class ResizeableCanvas extends Canvas {List<CanvasLayer>canvasLayerList;public ResizeableCanvas(Region region) {this.canvasLayerList = new ArrayList<>();widthProperty().bind(region.widthProperty());heightProperty().bind(region.heightProperty());widthProperty().addListener(event -> resizeDraw() );heightProperty().addListener(event -> resizeDraw() );}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);}@Overridepublic boolean isResizable() { return true;} @Overridepublic double prefWidth(double height) { return getWidth();} @Overridepublic 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;public class BorderPaneExample extends Application {@Overridepublic 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();}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.
You must be logged in to post a comment.