While working on FlexGanttFX I had to deal a lot with the JavaFX Canvas node. I am using it to render activities on a timeline. Each row in the Gantt chart is a Canvas node. The user has the option to resize each row individually. So I had to figure out the best way to resize a canvas, which out-of-the-box is not resizable. The listing below shows how this can be accomplished.
The main steps needed are:
- Create a subclass of Canvas.
- Override the isResizable() method and return true.
- Override the prefWidth() and prefHeight() methods. Return the values of Canvas.getWidth() and Canvas.getHeight().
- Add listeners to the width and height properties of Canvas in order to trigger a redraw when the size of the canvas changes.
- Bind the width and height properties of Canvas to the width and height properties of the parent pane.
import javafx.application.Application; import javafx.scene.Scene; import javafx.scene.canvas.Canvas; import javafx.scene.canvas.GraphicsContext; import javafx.scene.layout.StackPane; import javafx.scene.paint.Color; import javafx.stage.Stage; /** * Tip 1: A canvas resizing itself to the size of * the parent pane. */ public class Tip1ResizableCanvas extends Application { class ResizableCanvas extends Canvas { public ResizableCanvas() { // Redraw canvas when size changes. widthProperty().addListener(evt -> draw()); heightProperty().addListener(evt -> draw()); } private void draw() { double width = getWidth(); double height = getHeight(); GraphicsContext gc = getGraphicsContext2D(); gc.clearRect(0, 0, width, height); gc.setStroke(Color.RED); gc.strokeLine(0, 0, width, height); gc.strokeLine(0, height, width, 0); } @Override public boolean isResizable() { return true; } @Override public double prefWidth(double height) { return getWidth(); } @Override public double prefHeight(double width) { return getHeight(); } } @Override public void start(Stage stage) throws Exception { ResizableCanvas canvas = new ResizableCanvas(); StackPane stackPane = new StackPane(); stackPane.getChildren().add(canvas); // Bind canvas size to stack pane size. canvas.widthProperty().bind( stackPane.widthProperty()); canvas.heightProperty().bind( stackPane.heightProperty()); stage.setScene(new Scene(stackPane)); stage.setTitle("Tip 1: Resizable Canvas"); stage.show(); } public static void main(String[] args) { launch(args); } }
[…] posts this week, both of which are tips for fellow JavaFX developers. Firstly, he has posted about creating a resizable canvas in JavaFX, and secondly he has posted a tip about how to ensure lines are sharp / non-blurry when […]
[…] posts this week, both of which are tips for fellow JavaFX developers. Firstly, he has posted about creating a resizable canvas in JavaFX, and secondly he has posted a tip about how to ensure lines are sharp / non-blurry when […]
Nice blog! I wasn’t really aware of the isResizable method – recently I overrode StackPane.layoutChildren() to manually resize a child canvas, and I didn’t quite stop to think about why I had to do that.
If you do it that way then I think you will notice a little bit of a delay before the canvas content (the rendering / drawing) is actually updated.
Reblogged this on Dinesh Ram Kali..
That was of great help, thanks ! 😉
How I can create form with ResizableCanvas from fxml file?
If you use SceneBuilder you will need to import a JAR file that contains the resizable canvas control.
Nice summary, thanks!
In this case, it doesn’t matter, but prefHeight() and prefWidth() have the names of their parameters reversed.
The parameters of prefHeight() and prefWidth() are not reversed. The input for the calculation of the preferred height is the width of the control and vice versa.
If you use next scene graph all works properly
Scene
|_ StackPane
|_ Canvas
But if Canvas placed in any Node which itself placed in another Node and so on, then this solution does not work!
Scene
|_ StackPane mainPane
|_ StackPane canvasContainer
|_ Canvas
Canvas will grow, but never decreases. This problem is more actual, when you have many other controls in your scene graph.
Any other solutions?
I got it!
This http://fxexperience.com/2014/05/resizable-grid-using-canvas/ solution helped me.
The key point is that you have class which extends Pane (or any other resizeable Node class, e.g. StackPane or BorderPane) and also overrides method layoutChildren(). In this method you can write your own behavior on resizing child Canvas.
it only work,when stage.setScene(new Scene(stackPane)); if wrap stackPane with BorderPane, only when stage is becoming bigger, but not becoming smaller
Which position of the BorderPane did you use?
public class CanvasPane extends Pane{
private ResizableCanvas canvas;
public CanvasPane(ResizableCanvas canvas){
this.canvas = canvas;
this.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);
canvas.draw();
}
}
}
Nice post. Very cool.
Interestingly though, I find that I actually don’t need to extend Canvas at all (to override prefWidth, prefHeight, or isResizable). It simply works out of the box: I merely attach listeners to the width and height property and then pass the Canvas-instance directly to a draw-function.
Any thoughts?
It depends on your use case obviously. A canvas can of course be resized directly by calling setWidth() / setHeight() on it. But if you want to use one of the built-in layout panes like BorderPane then you need more than that because you will completely leave the layout / resize of the canvas to the BorderPane (e.g. when placed in the “center” location).