user1802693 user1802693 - 7 months ago 53
Java Question

JavaFX resizable canvas problems

I've a problem what's partially solved, but I am curious about another (better) solutions..
I want a canvas what fills the entire window and resizes itself if the window is resized.

Option 1 (partial solution):

According to this: https://dlemmermann.wordpress.com/2014/04/10/javafx-tip-1-resizable-canvas/
and this: How to make canvas Resizable in javaFX?
I've created (copied) the class:

public class ResizableCanvas extends Canvas {

public ResizableCanvas() {
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();
}
}


and I placed the following content to my ViewField.fxml file:

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.canvas.Canvas?>
<?import javafx.scene.layout.StackPane?>
<?import blahblahblah.ResizableCanvas?>

<StackPane fx:id="pane" minHeight="500.0" minWidth="500.0" style="-fx-background-color: white; -fx-border-color: black;" xmlns="http://javafx.com/javafx/8.0.65" xmlns:fx="http://javafx.com/fxml/1" fx:controller="blahblahblah.ViewFieldController">
<children>
<ResizableCanvas fx:id="resizableCanvas" />
</children>
</StackPane>


My controller (attached to this fxml file) is ViewFieldController.java:

import javafx.beans.property.ReadOnlyDoubleProperty;
import javafx.fxml.FXML;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;

public class ViewFieldController {

private Stage dialogStage;

@FXML
private StackPane pane;

@FXML
private ResizableCanvas resizableCanvas;

@FXML
private void initialize() {

GraphicsContext gc = resizableCanvas.getGraphicsContext2D();

ReadOnlyDoubleProperty widthProperty = pane.widthProperty();
ReadOnlyDoubleProperty heightProperty = pane.heightProperty();
resizableCanvas.widthProperty().bind(widthProperty);
resizableCanvas.heightProperty().bind(heightProperty);

drawArea(gc);
}

public void setDialogStage(Stage dialogStage) {
this.dialogStage = dialogStage;
}

private void drawArea(GraphicsContext gc) {

gc.setStroke(Color.BLACK);
gc.setLineWidth(1);
gc.strokeLine(0, 0, resizableCanvas.getWidth(), resizableCanvas.getHeight());
gc.strokeLine(0, resizableCanvas.getHeight(), resizableCanvas.getWidth(), 0);
}

}


The windows is loaded from the Main controller MainApp.java:

public class MainApp extends Application {

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

public void showViewField() {
try {
FXMLLoader loader = new FXMLLoader();
loader.setLocation(MainApp.class.getResource("view/ViewField.fxml"));
StackPane pane = (StackPane) loader.load();

Stage stage = new Stage();
stage.setTitle("View Field");
Scene scene = new Scene(pane);
stage.setScene(scene);

ViewFieldController controller = loader.getController();
controller.setDialogStage(stage);

stage.show();

} catch (IOException e) {
e.printStackTrace();
}
}

}


This solution works ("big X" appears on the canvas, thanks to drawArea() method).
The problem with this solution that after placing the ResisableCanvas class into the FXML file I can't use the SceneBuilder any more on this file, because I get the following exception:

java.io.IOException: javafx.fxml.LoadException:
...

at com.oracle.javafx.scenebuilder.kit.fxom.FXOMLoader.load(FXOMLoader.java:92)
at com.oracle.javafx.scenebuilder.kit.fxom.FXOMDocument.<init>(FXOMDocument.java:82)
at com.oracle.javafx.scenebuilder.kit.fxom.FXOMDocument.<init>(FXOMDocument.java:97)
at com.oracle.javafx.scenebuilder.kit.editor.EditorController.updateFxomDocument(EditorController.java:2384)
at com.oracle.javafx.scenebuilder.kit.editor.EditorController.setFxmlTextAndLocation(EditorController.java:664)
at com.oracle.javafx.scenebuilder.app.DocumentWindowController.loadFromFile(DocumentWindowController.java:381)
at com.oracle.javafx.scenebuilder.app.SceneBuilderApp.performOpenFiles(SceneBuilderApp.java:554)
at com.oracle.javafx.scenebuilder.app.SceneBuilderApp.handleOpenFilesAction(SceneBuilderApp.java:424)
at com.oracle.javafx.scenebuilder.app.SceneBuilderApp.handleLaunch(SceneBuilderApp.java:403)
at com.oracle.javafx.scenebuilder.app.AppPlatform.requestStartGeneric(AppPlatform.java:139)
at com.oracle.javafx.scenebuilder.app.AppPlatform.requestStart(AppPlatform.java:106)
at com.oracle.javafx.scenebuilder.app.SceneBuilderApp.start(SceneBuilderApp.java:353)
at com.sun.javafx.application.LauncherImpl.lambda$launchApplication1$163(LauncherImpl.java:863)
at com.sun.javafx.application.PlatformImpl.lambda$runAndWait$176(PlatformImpl.java:326)
at com.sun.javafx.application.PlatformImpl.lambda$null$174(PlatformImpl.java:295)
at java.security.AccessController.doPrivileged(Native Method)
at com.sun.javafx.application.PlatformImpl.lambda$runLater$175(PlatformImpl.java:294)
at com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:95)
at com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
at com.sun.glass.ui.win.WinApplication.lambda$null$149(WinApplication.java:191)
at java.lang.Thread.run(Thread.java:745)
Caused by: javafx.fxml.LoadException:
...

at javafx.fxml.FXMLLoader.constructLoadException(FXMLLoader.java:2601)
at javafx.fxml.FXMLLoader.importClass(FXMLLoader.java:2848)
at javafx.fxml.FXMLLoader.processImport(FXMLLoader.java:2692)
at javafx.fxml.FXMLLoader.processProcessingInstruction(FXMLLoader.java:2661)
at javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2517)
at javafx.fxml.FXMLLoader.load(FXMLLoader.java:2425)
at com.oracle.javafx.scenebuilder.kit.fxom.FXOMLoader.load(FXOMLoader.java:89)
... 20 more
Caused by: java.lang.ClassNotFoundException: blahblahblah.ResizableCanvas
at java.lang.ClassLoader.findClass(ClassLoader.java:530)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at javafx.fxml.FXMLLoader.loadTypeForPackage(FXMLLoader.java:2916)
at javafx.fxml.FXMLLoader.loadType(FXMLLoader.java:2905)
at javafx.fxml.FXMLLoader.importClass(FXMLLoader.java:2846)
... 25 more


Option 2 (partial solution):

My second solution uses the same ResizableCanvas class, but there is no FXML file or controller class.

public class MainApp extends Application {

//...

public void showViewField2() {

ResizableCanvas canvas = new ResizableCanvas();
StackPane pane = new StackPane();
pane.getChildren().add(canvas);

Stage stage = new Stage();
stage.setTitle("View Field");
Scene scene = new Scene(pane);
stage.setScene(scene);

canvas.widthProperty().bind(pane.widthProperty());
canvas.heightProperty().bind(pane.heightProperty());

stage.show();
}

}


This solution works too (big X is present on the screen), but the same problem appears here:
I can't open the FXML file in ScreenBuilder but for different reason (there is no FXML file at all).

Option 3 (my original solution, not working):

I've replaced the ResizableCanvas class in ViewField.fxml file with an original Canvas class:

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.canvas.Canvas?>
<?import javafx.scene.layout.StackPane?>

<StackPane fx:id="pane" minHeight="500.0" minWidth="500.0" style="-fx-background-color: white; -fx-border-color: black;" xmlns="http://javafx.com/javafx/8.0.65" xmlns:fx="http://javafx.com/fxml/1" fx:controller="blahblahblah.ViewFieldController">
<children>
<Canvas fx:id="regularCanvas" />
</children>
</StackPane>


My ViewFieldController.java looks like this:

import javafx.beans.property.ReadOnlyDoubleProperty;
import javafx.fxml.FXML;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;

public class ViewFieldController {

private Stage dialogStage;

@FXML
private StackPane pane;

@FXML
private Canvas regularCanvas;

@FXML
private void initialize() {

GraphicsContext gc = regularCanvas.getGraphicsContext2D();

ReadOnlyDoubleProperty widthProperty = pane.widthProperty();
ReadOnlyDoubleProperty heightProperty = pane.heightProperty();
regularCanvas.widthProperty().bind(widthProperty);
regularCanvas.heightProperty().bind(heightProperty);

drawArea(gc);
}

public void setDialogStage(Stage dialogStage) {
this.dialogStage = dialogStage;
}

private void drawArea(GraphicsContext gc) {

gc.setStroke(Color.BLACK);
gc.setLineWidth(1);
gc.strokeLine(0, 0, regularCanvas.getWidth(), regularCanvas.getHeight());
gc.strokeLine(0, regularCanvas.getHeight(), regularCanvas.getWidth(), 0);
}

}


But the screen is empty here. There is no "big X" drawn on to the canvas.
If I resize the window, I dont't know if the canvas is resized or not, but it's possible to load the FXML file into the SceneBuilder for further work..

It's not better if I give some height and width to the canvas (result is the same).

<StackPane fx:id="pane" minHeight="500.0" minWidth="500.0" style="-fx-background-color: white; -fx-border-color: black;" xmlns="http://javafx.com/javafx/8.0.65" xmlns:fx="http://javafx.com/fxml/1" fx:controller="blahblahblah.ViewFieldController">
<children>
<Canvas fx:id="regularCanvas" height="300.0" width="300.0" />
</children>
</StackPane>


My questions are:


  1. Why the "big X" does not appeared int the 3rd case?

  2. What modifications should I do to make the 3rd case work?

  3. Is there any other solution what solves my problems?

  4. Why did I get the exception in 1st case?

  5. Why is it impossible to "Fit to parent" the canvas in SceneBuilder?

  6. Why is the "resisable" option disabled in SceneBuilder?


Answer

You can make option 1 work with Scene Builder by bundling the custom canvas into a jar file and importing it to the Scene Builder library. See this question, for example.

Option 3 does not work because you only ever call drawArea() from the initialize() method. At that point, the canvas is not part of a Scene, and certainly not displayed in a window; consequently is hasn't undergone layout and so has zero width and height. You can fix this option by adding listeners to the width and height and redrawing when they change, as you do in option 1:

@FXML
private void initialize() {

    GraphicsContext gc = regularCanvas.getGraphicsContext2D();

    ReadOnlyDoubleProperty widthProperty = pane.widthProperty();
    ReadOnlyDoubleProperty heightProperty = pane.heightProperty();
    regularCanvas.widthProperty().bind(widthProperty);
    regularCanvas.heightProperty().bind(heightProperty);

    regularCanvas.widthProperty().addListener((obs, oldWidth, newWidth) -> drawArea(gc));
    regularCanvas.heightProperty().addListener((obs, oldHeight, newHeight) -> drawArea(gc));

    drawArea(gc);
}