elaspog
elaspog

Reputation: 1699

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?

Upvotes: 0

Views: 1498

Answers (1)

James_D
James_D

Reputation: 209339

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);
}

Upvotes: 1

Related Questions