I was hacking on a application today and wanted a dot grid in the background. Canvas is well suited to this as you don’t need 1000s of nodes for every dot on the grid. Canvas by default has a fixed size so I wrapped it in a Pane to make it Resizable. I thought this might be a generally useful example to show how to create a resizable canvas node. Also if you ever need a grid, it could be applied to a normal line grid just as easily. So here is a little test app with a single node of my DotGrid.

 

Dot Gird Test App
Test Application

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.stage.Stage;

/**
* Test app for DotGrid
*/
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.stage.Stage;

/**
 * Test app for DotGrid
 */
public class DotGridTestApp extends Application {

    @Override public void start(Stage stage) throws Exception {
        stage.setScene(new Scene(new DotGrid(), Color.WHITE));
        stage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

Dot Grid Class


import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;

/**
 * A node that draws a triangle grid of dots using canvas
 */
public class DotGrid extends Pane {
    private static final double SPACING_X = 25;
    private static final double SPACING_Y = 20;
    private static final double RADIUS = 1.5;
    private Canvas canvas = new Canvas();

    public DotGrid() {
        getChildren().add(canvas);
    }

    @Override protected void layoutChildren() {
        final int top = (int)snappedTopInset();
        final int right = (int)snappedRightInset();
        final int bottom = (int)snappedBottomInset();
        final int left = (int)snappedLeftInset();
        final int w = (int)getWidth() - left - right;
        final int h = (int)getHeight() - top - bottom;
        canvas.setLayoutX(left);
        canvas.setLayoutY(top);
        if (w != canvas.getWidth() || h != canvas.getHeight()) {
            canvas.setWidth(w);
            canvas.setHeight(h);
            GraphicsContext g = canvas.getGraphicsContext2D();
            g.clearRect(0, 0, w, h);
            g.setFill(Color.gray(0,0.2));

            for (int x = 0; x < w; x += SPACING_X) {
                for (int y = 0; y < h; y += SPACING_Y) {
                    double offsetY = (y%(2*SPACING_Y)) == 0 ? SPACING_X /2 : 0;
                    g.fillOval(x-RADIUS+offsetY,y-RADIUS,RADIUS+RADIUS,RADIUS+RADIUS);
                }
            }
        }
    }
}

There really is not much too it, when the layout is applied to Pane check if size has changed and redraw the canvas. There are a couple of optimizations you could do here if performance was a issue.

  • When the Pane gets bigger don’t clear, just draw any new dots needed. If you were animating the size growing then this might be a good win.
  • Draw the dot once into a WritableImage using another Canvas or Circle node and snapshot(). Then draw that image for each dot. That way turn out to be a performance win, but maybe not. If you had more complex dots, like company logos then this would probably be a good gain.

In this case I have not set any preferred size for the DotGrid so it will be whatever size its parent wants it to be. If you need it to be a minimum size you could call setPrefSize() or setMinSize() in the constructor.