Search code examples
javajavafxfxml

Modifying JavaFX Scene Loaded via FXML File?


I'm attempting to modify a JavaFX scene, loaded via a FXML file, in the Java code of the application I'm running.

I have the following test FXML file, created with SceneBuilder:

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

<?import javafx.scene.control.Tab?>
<?import javafx.scene.control.TabPane?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.shape.Line?>


<BorderPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="720.0" prefWidth="1280.0" xmlns="" xmlns:fx="">
   <top>
      <TabPane prefHeight="100.0" prefWidth="200.0" tabClosingPolicy="UNAVAILABLE" BorderPane.alignment="CENTER">
        <tabs>
          <Tab text="Untitled Tab 1">
            <content>
              <AnchorPane prefHeight="100.0" prefWidth="200.0" />
            </content>
          </Tab>
          <Tab text="Untitled Tab 2">
            <content>
              <AnchorPane prefHeight="100.0" prefWidth="200.0" />
            </content>
          </Tab>
        </tabs>
      </TabPane>
   </top>
   <center>
   </center>
</BorderPane>

Which is just a BorderPane with two empty tabs in the Top section, and an empty Center section. I wish to be able to draw some shapes in the Center section. I took the following example code for drawing some random shapes in a Canvas window, and made sure the example code works by-itself:

enter image description here

I then wanted to insert that same exact drawn Canvas, with the exact same shapes, into the Center section of my BorderPane declared in the FXML file shown above.

However, I'm a bit confused as to how I'm supposed to properly reference the various children of the scene loaded by the FXML loader. If I try the following code in my start function:

public void start(Stage stage) throws IOException 
{
    stage.setTitle("Drawing Operations Test");
    Scene scene = new Scene(loadFXML("primary"), 1280, 720);
        
    Canvas canvas = new Canvas(300, 250);
    GraphicsContext gc = canvas.getGraphicsContext2D();
    drawShapes(gc);
    scene.getRoot().getChildrenUnmodifiable().get(0).getChildren().add(canvas);
        
    stage.setScene(scene);
    stage.show();
}

Where drawShapes is the function that draws the shapes on the Canvas, the line:

scene.getRoot().getChildrenUnmodifiable().get(0).getChildren().add(canvas);

Is listed as erroneous, because the class-function getChildren cannot be found in the Node class.

Now, my initial assumption for the cause of that error was because I'm supposed to typecast the Node object into the respective class that I'm wanting to add canvas to, in my case the BorderPane class. However, I wanted to clarify exactly what the getChildren class-function will return me here.

Does getChildren return the BorderPane object, as that's the first object that's declared in the FXML file? Or is that element parented under something else? What would be the proper way of adding my Canvas to the Center section of my BorderPane?

Thanks for reading my post, any guidance is appreciated.


Solution

  • scene.getRoot()
    

    will return the root of the scene, which you set via the call to the Scene constructor:

    Scene scene = new Scene(loadFXML("primary"), 1280, 720);
    

    In other words, scene.getRoot() is the object returned from load.FXML(...). You haven't shown us that method definition, so we cannot know for sure, but it is reasonable to assume it returns the result of calling FXMLLoader.load(...), which would be the BorderPane (the root element, in the XML sense, of the FXML file).

    So you could cast the result of scene.getRoot() to a BorderPane and then call setCenter(...) (or, obviously better, just keep a reference to the result of loadFXML(...) in the first place).

    The code

    scene.getRoot().getChildrenUnmodifiable()
    

    will return a list of the child nodes of the BorderPane, i.e. a list containing just the TabPane, and so

    scene.getRoot().getChildrenUnmodifiable().get(0)
    

    is a reference to the TabPane, and

    scene.getRoot().getChildrenUnmodifiable().get(0).getChildren()
    

    (with appropriate casts added) is a list of the child nodes of the TabPane. The content of that list is basically an implementation detail of the TabPane and its skin.


    None of this is the way the API designers intended this to be used, though. Instead, define a controller class for the FXML, and inject the BorderPane into it:

    <?xml version="1.0" encoding="UTF-8"?>
    
    <?import javafx.scene.control.Tab?>
    <?import javafx.scene.control.TabPane?>
    <?import javafx.scene.layout.AnchorPane?>
    <?import javafx.scene.layout.BorderPane?>
    <?import javafx.scene.shape.Line?>
    
    
    <BorderPane 
        maxHeight="-Infinity" maxWidth="-Infinity" 
        minHeight="-Infinity" minWidth="-Infinity" 
        prefHeight="720.0" prefWidth="1280.0" 
        xmlns="" xmlns:fx=""
        fx:controller="my.package.MyController"
        fx:id="borderPane" 
    >
    
        <!-- ... -->
    
    </BorderPane>
    

    Then define the controller, and modify the border pane in the initialize() method (if you want to add the canvas immediately), or in an event handler (if you want to modify the scene graph in response to user actions):

    package my.package;
    
    // imports ...
    
    public class MyController {
    
        @FXML
        private BorderPane borderPane;
    
        @FXML
        private void initialize() {
            
            Canvas canvas = new Canvas(300, 250);
            GraphicsContext gc = canvas.getGraphicsContext2D();
            drawShapes(gc);
            borderPane.setCenter(canvas);
    
        }
    
        private void drawShapes(GraphicsContext gc) {
            // ...
        }
    }