Amnon Aashkenazy
Amnon Aashkenazy

Reputation: 17

Drawing on JavaFX canvas does nothing

I am trying to draw an oval in a canvas but fillOval does nothing. There are no exceptions or errors. While debugging it seems to get to the code and run it but nothing appears on the screen.

code (only the relevant package): https://github.com/amnon3234/PTM_Project/tree/master/PTM_Project_Final/src/view

specific parts: My canvas:

public class HeightMapDisplayer extends Canvas {

    public HeightMapDisplayer() {
        GraphicsContext gc = this.getGraphicsContext2D();
        gc.setFill(Color.RED);
        gc.fillOval(10,10,40,40);
    }
}

My controller:

public class MainWindowController implements Initializable {
    @FXML
    HeightMapDisplayer _heightMap;

    // Constructor , Initialize data
    public MainWindowController() {
        _heightMap = new HeightMapDisplayer();
    }
}

My XML:

<BorderPane xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1" fx:controller="view.MainWindowController">
   <left>
    ...
               <HeightMapDisplayer fx:id="heightMap" height="400.0" width="400.0" />
    ...
</BorderPane>

Result:

Result

Upvotes: 0

Views: 959

Answers (1)

James_D
James_D

Reputation: 209339

The problem is that you are trying to draw on the canvas before you set the width and height. Because your Canvas subclass implicitly invokes the default Canvas constructor, which sets the width and height to zero, then in the constructor you perform the drawing. Since this drawing happens outside the bounds of the canvas, nothing appears. The width and height attributes in the FXML cause the setWidth() and setHeight() methods to be invoked, but of course this happens after the constructor call is complete, by which time it is too late.

A "quick fix" is to allow the width and height to be passed to your constructor, so you can pass them on to the Canvas constructor:

public class HeightMapDisplayer extends Canvas {

    public HeightMapDisplayer(
            @NamedArg("width") double width,
            @NamedArg("height") double height) {

        super(width, height);
        GraphicsContext gc = this.getGraphicsContext2D();
        gc.setFill(Color.RED);
        gc.fillOval(10,10,40,40);
    }
}

However, it is generally a bad idea to subclass JavaFX node classes (other than those specifically written with the intention of subclassing them, such as Cells). A much better approach is to use a MVC approach: define a model class encapsulating the data you want to represent on the canvas, define a controller to do the drawing, and just use a plain Canvas.

E.g. the (mock) model class might look like

public class HeightMap {

    // data you need here...
    private int offset ;
    private int size ;

    public int getOffset() {
        return offset;
    }
    public void setOffset(int offset) {
        this.offset = offset;
    }
    public int getSize() {
        return size;
    }
    public void setSize(int size) {
        this.size = size;
    }

}

with a controller class

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

public class HeightMapController {

    private final Canvas view ;
    private HeightMap model ;

    public HeightMapController(Canvas view) {
        this.view = view ;
    }

    public void setModel(HeightMap model) {
        this.model = model ;
        GraphicsContext gc = view.getGraphicsContext2D();
        gc.setFill(Color.RED);
        gc.fillOval(model.getOffset(), model.getOffset(), model.getSize(), model.getSize());
    }

}

Here's an updated MainWindowController:

import java.net.URL;
import java.util.ResourceBundle;

import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.canvas.Canvas;

public class MainWindowController implements Initializable {
    @FXML
    private Canvas heightMap;

    private HeightMapController heightMapController ;


    @Override
    public void initialize(URL location, ResourceBundle resources) {
        heightMapController = new HeightMapController(heightMap);
    }

    public void setHeightMapModel(HeightMap heightMapModel) {
        heightMapController.setModel(heightMapModel);
    }
}

and FXML file:

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

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

<BorderPane xmlns="http://javafx.com/javafx/10.0.2-internal"
    xmlns:fx="http://javafx.com/fxml/1"
    fx:controller="org.jamesd.examples.canvas.MainWindowController">
    <left>
        <Canvas fx:id="heightMap"
            height="400" width="400" />
    </left>
</BorderPane>

and finally the code that assembles everything:

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

import java.io.IOException;

/**
 * JavaFX App
 */
public class App extends Application {


    @Override
    public void start(Stage stage) throws IOException {
        FXMLLoader loader = new FXMLLoader(getClass().getResource("Main.fxml"));
        Scene scene = new Scene(loader.load());
        MainWindowController controller = loader.getController();
        HeightMap heightMap = new HeightMap();
        heightMap.setOffset(10);
        heightMap.setSize(40);
        controller.setHeightMapModel(heightMap);
        stage.setScene(scene);
        stage.show();
    }

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

}

There are other techniques that can make this a bit cleaner, e.g. using a controller factory to instantiate controllers with the model already present, or using a dependency injection framework to achieve the same thing, but this should give you the idea of the correct structure.

Upvotes: 1

Related Questions