How to load a FXML file onto another FXML file with different controllers

104 Views Asked by At

I'm currently working on my first FXML project and also trying to implement the MVC format. My project is to build a calculator and my way of proceeding is by having a base fxml file with a menu that when the onAction of the menuItems are called, they load a FXML file onto the anchorPane that is in my base FXML file. The base.fxml file has now controller, I only included a MenuBar with its respective controller. What I ultimately want is a way to load a the fxml file and use it's functions in another fxml file (base.fxml). When I click on any menuItem, it gives a very long error ending in a NullPointerException at the line of the FXMLLoader.

This is the onAction called when a menuItem is pressed (the only thing that changes is the file called depending on the type of calculator the user wants) :

public void modeStandard() throws IOException {
        Parent root = FXMLLoader.load(Objects.requireNonNull(getClass().getResource("standard.fxml")));
        anchorPane.getChildren().clear();
        anchorPane.getChildren().add(root);
    }

The anchorPane has an id that is used in base.fxml (shown at the end). All my onActions are called at their respective locations and the controllers are also assigned correctly

For reference, this is my base.fxml :

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

<?import javafx.scene.control.SplitPane?>
<?import javafx.scene.control.TableColumn?>
<?import javafx.scene.control.TableView?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.VBox?>

<VBox maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/21" xmlns:fx="http://javafx.com/fxml/1" >
    <children>
        <fx:include source="MenuBar.fxml" fx:id="menuBar"/>
        <SplitPane dividerPositions="0.6546822742474916" prefHeight="429.0" prefWidth="600.0">
            <items>
                <AnchorPane fx:id="anchorPane" />
                <AnchorPane minHeight="0.0" minWidth="0.0" prefHeight="160.0" prefWidth="100.0">
                    <children>
                        <TableView prefHeight="371.0" prefWidth="200.0">
                            <columns>
                                <TableColumn prefWidth="198.0" text="Résultats précédents" />
                            </columns>
                        </TableView>
                    </children>
                </AnchorPane>
            </items>
        </SplitPane>
    </children>
</VBox>

And this is menuBar.fxml, which is included :

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

<?import javafx.scene.control.Menu?>
<?import javafx.scene.control.MenuBar?>
<?import javafx.scene.control.MenuItem?>

<MenuBar prefHeight="0.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/21" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.example.demo.modele.MenuBarController">
<menus>
    <Menu mnemonicParsing="false" text="Standard">
        <items>
            <MenuItem  onAction="#modeStandard" mnemonicParsing="false" text="Calculatrice standard" />
        </items>
    </Menu>
    <Menu mnemonicParsing="false" text="Scientifique">
        <items>
            <MenuItem onAction="#modeScientifique" mnemonicParsing="false" text="Calculatrice scientifique" />
        </items>
    </Menu>
    <Menu mnemonicParsing="false" text="Graphique">
        <items>
            <MenuItem onAction="#modeGraphique" mnemonicParsing="false" text="Calculatrice graphique" />
        </items>
    </Menu>
    <Menu mnemonicParsing="false" text="Programmeur">
        <items>
            <MenuItem onAction="#modeProgrammeur" mnemonicParsing="false" text="Calculatrice programmeur" />
        </items>
    </Menu>
    <Menu mnemonicParsing="false" text="Conversion">
         <items>
            <MenuItem onAction="#appelerConvertisseur" mnemonicParsing="false" text="Appeller convertisseur" />
         </items>
    </Menu>
</menus>
</MenuBar>

The MenuBarController is basically the first function I've shown ( modeStandard() ) multiplied 4 times for the different modes and the anchorPane and menuBar so I can id them :

@FXML
    private AnchorPane anchorPane;
@FXML
    private MenuBar menuBar;

And finally this is an example of what im trying to load onto the anchorPane in base.fxml :

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

<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<AnchorPane fx:controller="com.example.demo.modele.Standard" xmlns="http://javafx.com/javafx/21" xmlns:fx="http://javafx.com/fxml/1">
    <children>
        <VBox alignment="CENTER" layoutX="6.0" prefHeight="371.0" prefWidth="405.0">
            <children>
                <Label alignment="CENTER" text="0" translateX="80.0" />
                <GridPane alignment="CENTER">
                    <columnConstraints>
                        <ColumnConstraints hgrow="SOMETIMES" maxWidth="66.0" minWidth="10.0" prefWidth="68.0" />
                        <ColumnConstraints hgrow="SOMETIMES" maxWidth="66.0" minWidth="10.0" prefWidth="68.0" />
                        <ColumnConstraints hgrow="SOMETIMES" maxWidth="66.0" minWidth="66.0" prefWidth="66.0" />
                    </columnConstraints>
                    <rowConstraints>
                        <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
                        <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
                        <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
                        <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
                        <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
                        <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
                        <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
                        <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
                    </rowConstraints>
                    <children>
                        <Button alignment="BASELINE_CENTER" mnemonicParsing="false" prefHeight="26.0" prefWidth="66.0" text="+" />
                        <Button mnemonicParsing="false" prefHeight="26.0" prefWidth="66.0" text="-" GridPane.columnIndex="1" />
                        <Button mnemonicParsing="false" prefHeight="27.0" prefWidth="66.0" text="x" GridPane.rowIndex="1" />
                        <Button mnemonicParsing="false" prefHeight="27.0" prefWidth="66.0" text="÷" GridPane.columnIndex="1" GridPane.rowIndex="1" />
                        <Button mnemonicParsing="false" prefHeight="26.0" prefWidth="66.0" text="^2" GridPane.rowIndex="2" />
                        <Button mnemonicParsing="false" prefHeight="26.0" prefWidth="66.0" text="√" GridPane.columnIndex="1" GridPane.rowIndex="2" />
                        <Button mnemonicParsing="false" prefHeight="26.0" prefWidth="66.0" text="^-1" GridPane.columnIndex="2" />
                        <Button mnemonicParsing="false" prefHeight="16.0" prefWidth="66.0" text="-x" GridPane.columnIndex="2" GridPane.rowIndex="1" />
                        <Button mnemonicParsing="false" prefHeight="27.0" prefWidth="66.0" text="AC" GridPane.columnIndex="2" GridPane.rowIndex="2" />
                        <Button mnemonicParsing="false" prefWidth="66.0" text="7" GridPane.rowIndex="3" />
                        <Button mnemonicParsing="false" prefWidth="66.0" text="8" GridPane.columnIndex="1" GridPane.rowIndex="3" />
                        <Button mnemonicParsing="false" prefWidth="66.0" text="9" GridPane.columnIndex="2" GridPane.rowIndex="3" />
                        <Button mnemonicParsing="false" prefWidth="66.0" text="4" GridPane.rowIndex="4" />
                        <Button mnemonicParsing="false" prefWidth="66.0" text="5" GridPane.columnIndex="1" GridPane.rowIndex="4" />
                        <Button mnemonicParsing="false" prefWidth="66.0" text="6" GridPane.columnIndex="2" GridPane.rowIndex="4" />
                        <Button mnemonicParsing="false" prefWidth="66.0" text="DEL" GridPane.rowIndex="6" />
                        <Button mnemonicParsing="false" prefWidth="66.0" text="2" GridPane.columnIndex="1" GridPane.rowIndex="5" />
                        <Button mnemonicParsing="false" prefWidth="66.0" text="1" GridPane.rowIndex="5" />
                        <Button mnemonicParsing="false" prefWidth="66.0" text="0" GridPane.columnIndex="1" GridPane.rowIndex="6" />
                        <Button mnemonicParsing="false" prefWidth="66.0" text="3" GridPane.columnIndex="2" GridPane.rowIndex="5" />
                        <Button mnemonicParsing="false" prefWidth="66.0" text="." GridPane.columnIndex="2" GridPane.rowIndex="6" />
                    </children>
                </GridPane>
                <Button   onAction="#notationSansPriorite" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" mnemonicParsing="false" prefWidth="200.0" text="=" translateY="-30.0"/>
            </children>
        </VBox>
    </children>
</AnchorPane>

Here is the error :

Exception in thread "JavaFX Application Thread" java.lang.RuntimeException: java.lang.reflect.InvocationTargetException
    at javafx.fxml@19/javafx.fxml.FXMLLoader$MethodHandler.invoke(FXMLLoader.java:1857)
    at javafx.fxml@19/javafx.fxml.FXMLLoader$ControllerMethodEventHandler.handle(FXMLLoader.java:1724)
    at javafx.base@19/com.sun.javafx.event.CompositeEventHandler.dispatchBubblingEvent(CompositeEventHandler.java:86)
    at javafx.base@19/com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:234)
    at javafx.base@19/com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:191)
    at javafx.base@19/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:58)
    at javafx.base@19/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
    at javafx.base@19/com.sun.javafx.event.EventUtil.fireEventImpl(EventUtil.java:74)
    at javafx.base@19/com.sun.javafx.event.EventUtil.fireEvent(EventUtil.java:49)
    at javafx.base@19/javafx.event.Event.fireEvent(Event.java:198)
    at javafx.controls@19/javafx.scene.control.MenuItem.fire(MenuItem.java:459)
    at javafx.controls@19/com.sun.javafx.scene.control.ContextMenuContent$MenuItemContainer.doSelect(ContextMenuContent.java:1385)
    at javafx.controls@19/com.sun.javafx.scene.control.ContextMenuContent$MenuItemContainer.lambda$createChildren$12(ContextMenuContent.java:1338)
    at javafx.base@19/com.sun.javafx.event.CompositeEventHandler$NormalEventHandlerRecord.handleBubblingEvent(CompositeEventHandler.java:247)
    at javafx.base@19/com.sun.javafx.event.CompositeEventHandler.dispatchBubblingEvent(CompositeEventHandler.java:80)
    at javafx.base@19/com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:234)
    at javafx.base@19/com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:191)
    at javafx.base@19/com.sun.javafx.event.CompositeEventDispatcher.dispatchBubblingEvent(CompositeEventDispatcher.java:59)
    at javafx.base@19/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:58)
    at javafx.base@19/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
    at javafx.base@19/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
    at javafx.base@19/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
    at javafx.base@19/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
    at javafx.base@19/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
    at javafx.base@19/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
    at javafx.base@19/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
    at javafx.base@19/com.sun.javafx.event.EventUtil.fireEventImpl(EventUtil.java:74)
    at javafx.base@19/com.sun.javafx.event.EventUtil.fireEvent(EventUtil.java:54)
    at javafx.base@19/javafx.event.Event.fireEvent(Event.java:198)
    at javafx.graphics@19/javafx.scene.Scene$MouseHandler.process(Scene.java:3894)
    at javafx.graphics@19/javafx.scene.Scene.processMouseEvent(Scene.java:1887)
    at javafx.graphics@19/javafx.scene.Scene$ScenePeerListener.mouseEvent(Scene.java:2620)
    at javafx.graphics@19/com.sun.javafx.tk.quantum.GlassViewEventHandler$MouseEventNotification.run(GlassViewEventHandler.java:411)
    at javafx.graphics@19/com.sun.javafx.tk.quantum.GlassViewEventHandler$MouseEventNotification.run(GlassViewEventHandler.java:301)
    at java.base/java.security.AccessController.doPrivileged(AccessController.java:399)
    at javafx.graphics@19/com.sun.javafx.tk.quantum.GlassViewEventHandler.lambda$handleMouseEvent$2(GlassViewEventHandler.java:450)
    at javafx.graphics@19/com.sun.javafx.tk.quantum.QuantumToolkit.runWithoutRenderLock(QuantumToolkit.java:424)
    at javafx.graphics@19/com.sun.javafx.tk.quantum.GlassViewEventHandler.handleMouseEvent(GlassViewEventHandler.java:449)
    at javafx.graphics@19/com.sun.glass.ui.View.handleMouseEvent(View.java:551)
    at javafx.graphics@19/com.sun.glass.ui.View.notifyMouse(View.java:937)
    at javafx.graphics@19/com.sun.glass.ui.mac.MacView.notifyMouse(MacView.java:127)
Caused by: java.lang.reflect.InvocationTargetException
    at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:116)
    at java.base/java.lang.reflect.Method.invoke(Method.java:578)
    at com.sun.javafx.reflect.Trampoline.invoke(MethodUtil.java:77)
    at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
    at java.base/java.lang.reflect.Method.invoke(Method.java:578)
    at javafx.base@19/com.sun.javafx.reflect.MethodUtil.invoke(MethodUtil.java:275)
    at javafx.fxml@19/com.sun.javafx.fxml.MethodHelper.invoke(MethodHelper.java:84)
    at javafx.fxml@19/javafx.fxml.FXMLLoader$MethodHandler.invoke(FXMLLoader.java:1854)
    ... 40 more
Caused by: java.lang.NullPointerException
    at java.base/java.util.Objects.requireNonNull(Objects.java:233)
    at com.example.demo/com.example.demo.modele.MenuBarController.modeStandard(MenuBarController.java:50)
    at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
    ... 47 more

this is my best attempt at a minimal reproducible example. There are 2 controllers and 2 fxml files, respectively named BaseController with base.fxml and ButtonController with button.fxml .

Firstly is HelloApplication :

package com.example.test;

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

import java.io.IOException;

public class HelloApplication extends Application {
    @Override
    public void start(Stage stage) throws IOException {
        Parent root = FXMLLoader.load(getClass().getResource("base.fxml"));
        Scene scene = new Scene(root);
        stage.setScene(scene);
        stage.show();
    }

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

Shown here is BaseController :

package com.example.test;

import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.layout.VBox;

import java.io.IOException;

public class BaseController {

    @FXML
    private VBox vBoxBase;

    public void importButton() throws IOException {
        Parent root = FXMLLoader.load(getClass().getResource("button.fxml"));
        vBoxBase = (VBox) root;
    }
}

And here is base.fxml :

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

<?import javafx.scene.control.Menu?>
<?import javafx.scene.control.MenuBar?>
<?import javafx.scene.control.MenuItem?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.VBox?>

<AnchorPane fx:controller="com.example.test.BaseController" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/21" xmlns:fx="http://javafx.com/fxml/1">
   <children>
      <MenuBar layoutY="8.0">
        <menus>
          <Menu mnemonicParsing="false" text="Import">
            <items>
              <MenuItem onAction="#importButton" mnemonicParsing="false" text="Import button" />
            </items>
          </Menu>
        </menus>
      </MenuBar>
      <VBox fx:id="vBoxBase" layoutX="250.0" layoutY="100.0" prefHeight="200.0" prefWidth="100.0" />
   </children>
</AnchorPane>

Now here is ButtonController :

package com.example.test;

public class ButtonController {

    public void buttonFonction() {
        System.out.println("button pressed");
    }
}

And button.fxml finally :

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

<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.VBox?>


<VBox fx:controller="com.example.test.ButtonController" alignment="CENTER" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/21" xmlns:fx="http://javafx.com/fxml/1">
   <children>
      <Button onAction="#buttonFonction" mnemonicParsing="false" text="Imported Button" />
      <Label text="Imported label" />
   </children>
</VBox>

My goal is that button.fxml is imported onto vBoxBase when the MenuItem labeled "Import button" is pressed. I don't have any errors but when debugging, all values shown are null but the VBox has it's correct childset but these are also null. But, the onAction of the button is preserved.

P.S. this is my first post, if something is missing or you would like to see my project in it's entirety, I will gladly do anything in my power to help resolve this problem with you

1

There are 1 best solutions below

0
jewelsea On

Don't set FXML injected nodes to new values

In your BaseController you have:

@FXML
private VBox vBoxBase;

That will set the vBoxBase reference to the node with the corresponding fx:id which is created by the FXMLLoader. The node is part of the node graph returned by the loader.

Then later on you do this:

Parent root = FXMLLoader.load(getClass().getResource("button.fxml"));
vBoxBase = (VBox) root;

That will set the vBoxBase node to a different node from that which the FXMLLoader injected and which is not part of the node hierarchy returned by the original FXMLLoader invocation that created the vBoxBase, so the associated node you set is never attached to the scene graph and you can never see or interact with it.

What you probably want to do instead

Modify the child scene graph associated with your container node vBoxBase.

Parent root = FXMLLoader.load(getClass().getResource("button.fxml"));
vBoxBase.getChildren().add(root);

You are performing this on a menu item selection, so every time you select the menu item it will add a new value to the VBox.

However, maybe you only want a single item in the VBox on menu selection, in which case you could do:

Parent root = FXMLLoader.load(getClass().getResource("button.fxml"));
vBoxBase.getChildren().setAll(root);

If you didn't want a new node graph for the root created each time you select a menu item, you could load it in the initialize method and store the reference in an instance variable, and reference that when you use it.

private Parent buttonRoot;

public void initialize() {
    buttonRoot = FXMLLoader.load(getClass().getResource("button.fxml"));
}

public void importButton() throws IOException {
    vBoxBase.getChildren().setAll(buttonRoot);
}

If you want to use that pattern then FXML has a method for streamlining it, which I won't elaborate here, see nested controllers if interested.

If you only have a single item that you want to set, then (if you used a border pane for example) you could set it in a position in the border pane, possibly enclosing it in a stack pane for alignment, though I won't demonstrate that here.

General layout advice

  1. Don't hardcode sizes.
    • use layout hints and preferred sizes sparingly only when needed.
  2. Let the layout panes handle the layout for you.
    • use absolute positioning panes like AnchorPane sparingly (or not at all).
  3. Choose appropriate layout panes for your layout.
    • for example a traditional menu driven app could use a BorderPane instead of an AnchorPane as its root.

Example

The example applies the fix for adding a child of the vBoxBase mentioned earlier, as well as some of the general layout advice.

On execution, you get this output after the "Import button" menu item has been selected three times, to load the button and label FXML three times, and add it to the VBox used for the content of the application center panel.

example

BaseController.java

package com.example.test;

import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.layout.VBox;

import java.io.IOException;
import java.util.Objects;

public class BaseController {

    @FXML
    private VBox vBoxBase;

    public void importButton() throws IOException {
        Parent buttonControl = FXMLLoader.load(
                Objects.requireNonNull(
                        getClass().getResource(
                                "button.fxml"
                        )
                )
        );

        // various options for adding content.
        // try enabling them one at a time to test the differences.

        // adds a new button to the vbox
        // every time this method is called.
        vBoxBase.getChildren().add(buttonControl);

        // replaces existing content in the vbox with a new button
        // loaded from fxml every time this method is called.
        //vBoxBase.getChildren().setAll(buttonControl);
    }
}

base.fxml

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

<?import javafx.scene.control.Menu?>
<?import javafx.scene.control.MenuBar?>
<?import javafx.scene.control.MenuItem?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.VBox?>
<BorderPane fx:controller="com.example.test.BaseController" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/21" xmlns:fx="http://javafx.com/fxml/1">
    <top>
        <MenuBar>
            <menus>
                <Menu mnemonicParsing="false" text="Import">
                    <items>
                        <MenuItem onAction="#importButton" mnemonicParsing="false" text="Import button" />
                    </items>
                </Menu>
            </menus>
        </MenuBar>
    </top>
    <center>
        <VBox fx:id="vBoxBase"/>
    </center>
</BorderPane>

button.fxml

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

<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.VBox?>

<VBox fx:controller="com.example.test.ButtonController" xmlns="http://javafx.com/javafx/21" xmlns:fx="http://javafx.com/fxml/1">
    <children>
        <Button onAction="#buttonFonction" mnemonicParsing="false" text="Imported Button" />
        <Label text="Imported label" />
    </children>
</VBox>

The rest of your code is unchanged (though I would advise fixing spelling errors in code, those kinds of things are always annoying and can lead to bugs).