javajavafxscenebuilderopenjfx

JavaFX custom component usage in SceneBuilder


I'm creating a small personal project using Java 20, JavaFX 20 and Maven. I'm having trouble creating reusable components and manipulating them through the main scene's controller.

First, I followed the steps listed in the official documentation. After that, I went to SceneBuilder and imported my custom component's FXML file in SceneBuilder (Click on the small engine icon where it says "Library" -> JAR/FXML Manager -> Add Library/FXML from file system) and added it to the scene like you would with any default component. I then gave my custom component a fx:id and added it to my scene's controller class so I can to stuff with it, but I get the following error.

Exception in Application start method
java.lang.reflect.InvocationTargetException
    at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:119)
    at java.base/java.lang.reflect.Method.invoke(Method.java:578)
    at javafx.graphics@20/com.sun.javafx.application.LauncherImpl.launchApplicationWithArgs(LauncherImpl.java:464)
    at javafx.graphics@20/com.sun.javafx.application.LauncherImpl.launchApplication(LauncherImpl.java:363)
    at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
    at java.base/java.lang.reflect.Method.invoke(Method.java:578)
    at java.base/sun.launcher.LauncherHelper$FXHelper.main(LauncherHelper.java:1081)
Caused by: java.lang.RuntimeException: Exception in Application start method
    at javafx.graphics@20/com.sun.javafx.application.LauncherImpl.launchApplication1(LauncherImpl.java:893)
    at javafx.graphics@20/com.sun.javafx.application.LauncherImpl.lambda$launchApplication$2(LauncherImpl.java:195)
    at java.base/java.lang.Thread.run(Thread.java:1623)
Caused by: javafx.fxml.LoadException: 
/C:/Users/user/Desktop/eclipse-workspace/Project 3/target/classes/app/views/fxml/Menu.fxml:43

    at javafx.fxml@20/javafx.fxml.FXMLLoader.constructLoadException(FXMLLoader.java:2722)
    at javafx.fxml@20/javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2700)
    at javafx.fxml@20/javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2563)
    at javafx.fxml@20/javafx.fxml.FXMLLoader.load(FXMLLoader.java:2531)
    at app/app.Main.loadFXML(Main.java:29)
    at app/app.Main.start(Main.java:17)
    at javafx.graphics@20/com.sun.javafx.application.LauncherImpl.lambda$launchApplication1$9(LauncherImpl.java:839)
    at javafx.graphics@20/com.sun.javafx.application.PlatformImpl.lambda$runAndWait$12(PlatformImpl.java:483)
    at javafx.graphics@20/com.sun.javafx.application.PlatformImpl.lambda$runLater$10(PlatformImpl.java:456)
    at java.base/java.security.AccessController.doPrivileged(AccessController.java:400)
    at javafx.graphics@20/com.sun.javafx.application.PlatformImpl.lambda$runLater$11(PlatformImpl.java:455)
    at javafx.graphics@20/com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:95)
    at javafx.graphics@20/com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
    at javafx.graphics@20/com.sun.glass.ui.win.WinApplication.lambda$runLoop$3(WinApplication.java:185)
    ... 1 more
Caused by: java.lang.IllegalArgumentException: Can not set app.components.Custom field app.controllers.Menu.cc to javafx.scene.layout.VBox
    at java.base/jdk.internal.reflect.FieldAccessorImpl.throwSetIllegalArgumentException(FieldAccessorImpl.java:228)
    at java.base/jdk.internal.reflect.FieldAccessorImpl.throwSetIllegalArgumentException(FieldAccessorImpl.java:232)
    at java.base/jdk.internal.reflect.MethodHandleObjectFieldAccessorImpl.set(MethodHandleObjectFieldAccessorImpl.java:115)
    at java.base/java.lang.reflect.Field.set(Field.java:834)
    at javafx.fxml@20/javafx.fxml.FXMLLoader.injectFields(FXMLLoader.java:1175)
    at javafx.fxml@20/javafx.fxml.FXMLLoader$ValueElement.processValue(FXMLLoader.java:870)
    at javafx.fxml@20/javafx.fxml.FXMLLoader$ValueElement.processStartElement(FXMLLoader.java:764)
    at javafx.fxml@20/javafx.fxml.FXMLLoader.processStartElement(FXMLLoader.java:2853)
    at javafx.fxml@20/javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2649)
    ... 13 more
Exception running application app.Main

A weird thing I noticed is that when I add the component to the main scene, it shows up as a VBox and not a Custom even though when I drag it in the "Hierarchy" tab it says the component's name is Custom, not VBox.

Here are the files related Custom.java

package app.components;

import java.io.IOException;

import app.Main;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;

public class Custom extends VBox {
    
    @FXML private Button plusBtn;
    @FXML private Button minusBtn;
    @FXML private Label label;
    
    public Custom() {
        FXMLLoader loader = new FXMLLoader(Main.class.getResource("components/Custom.fxml"));
        loader.setRoot(this);
        loader.setController(this);
        try {
            loader.load();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
    
    public void newText(String text) {
        label.setText(text);
    }
}

Custom.fxml

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

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

<VBox alignment="CENTER" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" xmlns="http://javafx.com/javafx/19" xmlns:fx="http://javafx.com/fxml/1">
   <children>
      <Button fx:id="plusBtn" mnemonicParsing="false" text="+" />
      <Label fx:id="label" text="Label" />
      <Button fx:id="minusBtn" mnemonicParsing="false" text="-" />
   </children>
</VBox>

At the moment, my main scene is just an empty StackPane with my custom component in the center to which I gave "cc" as the fx:id.

Menu.java

package app.controllers;

import app.components.Custom;

public class Menu {

    @FXML
    private Custom cc;

        public void initialize() {
        cc.newText("Test");
    }
}

module-info.java

module app {
    requires javafx.controls;
    requires javafx.fxml;
    requires javafx.media;
    requires javafx.graphics;
    requires javafx.base;
    
    opens app to javafx.fxml;
    opens app.controllers to javafx.fxml;
    
    exports app;
}

The problem is that when I add (drag and drop from Custom section to the StackPane) my component, it shows up as VBox and not Custom. Here's a screenshot, it might make what I mean clearer:

image

I want the component to show up just as Custom, not VBox, because SceneBuilder tells me that it doesn't find an injectable field for 'cc' even though I have the field in my controller class.


Solution

  • This answer is long, but there is a lot going on so it is what it is.

    These steps worked for me. If followed exactly, it will likely work for you. If you deviate from the steps in any way, there are no guarantees it will work, nor that I will support you much with it.

    I'm just going to note what to do without a lot of explanation.

    The basic approach is to:

    1. Create one module with the custom control(s) needed.
    2. Import the module with the custom controls into SceneBuilder.
    3. Use SceneBuilder to design your app using your custom controls.
    4. Create a new project for the app which uses the FXML generated by SceneBuilder. The new project depends on the custom control module for functionality.
    5. Build and run the application project.

    Idea can create and work with multi-module maven projects, so the multiple modules can be in a single project (which might be a reasonable approach for this), but that setup is more complex and not core to solving your problem, so I did not document it here.

    Step 1: Create a JavaFX Project for your Custom Component

    In Idea -> New Project -> JavaFX Project -> (use Maven) -> group name "com.example" artifact name "custom-component".

    Replace the generated java source files, fxml and pom.xml with those below.

    src/main/java/com/example/customcomponent/CustomComponent.java

    package com.example.customcomponent;
    
    import javafx.fxml.FXML;
    import javafx.fxml.FXMLLoader;
    import javafx.scene.control.Button;
    import javafx.scene.control.Label;
    import javafx.scene.layout.VBox;
    
    import java.io.IOException;
    
    public class CustomComponent extends VBox {
        
        @FXML private Button plusBtn;
        @FXML private Button minusBtn;
        @FXML private Label label;
        
        public CustomComponent() {
            FXMLLoader loader = new FXMLLoader(
                    CustomComponent.class.getResource(
                            "custom-component.fxml"
                    )
            );
            loader.setRoot(this);
            loader.setController(this);
            try {
                loader.load();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
        
        public void newText(String text) {
            label.setText(text);
        }
    }
    

    src/main/java/module-info.java

    module com.example.customcomponent {
        requires javafx.controls;
        requires javafx.fxml;
    
        opens com.example.customcomponent to javafx.fxml;
        exports com.example.customcomponent;
    }
    

    src/main/resources/com/example/customcomponent/custom-component.fxml

    <?xml version="1.0" encoding="UTF-8"?>
    
    <?import javafx.scene.control.Button?>
    <?import javafx.scene.control.Label?>
    <fx:root type="javafx.scene.layout.VBox" alignment="CENTER" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" xmlns="http://javafx.com/javafx/19" xmlns:fx="http://javafx.com/fxml/1">
        <children>
            <Button fx:id="plusBtn" mnemonicParsing="false" text="+" />
            <Label fx:id="label" text="Label" />
            <Button fx:id="minusBtn" mnemonicParsing="false" text="-" />
        </children>
    </fx:root>
    

    pom.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
    
        <groupId>com.example</groupId>
        <artifactId>custom-component</artifactId>
        <version>1.0-SNAPSHOT</version>
        <name>custom-component</name>
    
        <properties>
            <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        </properties>
    
        <dependencies>
            <dependency>
                <groupId>org.openjfx</groupId>
                <artifactId>javafx-controls</artifactId>
                <version>19</version>
            </dependency>
            <dependency>
                <groupId>org.openjfx</groupId>
                <artifactId>javafx-fxml</artifactId>
                <version>19</version>
            </dependency>
        </dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>3.11.0</version>
                    <configuration>
                        <source>17</source>
                        <target>17</target>
                    </configuration>
                </plugin>
            </plugins>
        </build>
    </project>
    

    Reimport the maven project after changing it.

    Go to the maven window and do maven -> install.

    Step 2: Import the Custom Component into SceneBuilder

    Download and install SceneBuilder 19.

    Create a new (Empty) project in SceneBuilder.

    Click on the cog icon next to the library search field.

    JAR/FXML Manager

    Select "JAR/FXML Manager".

    Choose "Manually add Library from repository".

    Enter Group ID: "com.example", Artifact ID: "custom-component", and press TAB.

    Select Version: "1.0-SNAPSHOT (local)" from the drop down.

    Manually add Library from repository

    Choose "Add JAR".

    The "Import" dialog will show "CustomComponent" with a tick next to it and a preview image of the component.

    Import dialog

    Keep the default sizing settings for the new component, and choose "Import Component".

    The newly installed library will be listed in the "Library Manager" dialog.

    Installed library

    Close the "Library Manager" dialog.

    Step 3: Design your Application UI using the Custom Component

    Go to the library search field, type "StackPane".

    Drag the StackPane into the scene you are creating.

    Go to the library search field, type "CustomComponent".

    Drag the CustomComponent into the center of StackPane.

    Click on the CustomComponent, then click on the "Code" panel and enter an "fx:id" as: "customComponent".

    Click on the controller pane and enter the controller class name "com.example.customcomponentdemo.HelloController".

    Save your FXML file as "hello-view.xml", it will look like this:

    hello-view in SceneBuilder

    <?xml version="1.0" encoding="UTF-8"?>
    
    <?import com.example.customcomponent.CustomComponent?>
    <?import javafx.scene.layout.StackPane?>
    
    
    <StackPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/19" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.example.customcomponentdemo.HelloController">
       <children>
          <CustomComponent fx:id="customComponent" />
       </children>
    </StackPane>
    

    Step 4: Create a JavaFX Project for your Application using the Custom Component

    In Idea -> new JavaFX project (maven) -> group id: "com.example", artifact id: "custom-component-demo" -> replace the generated files as below:

    pom.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
    
        <groupId>com.example</groupId>
        <artifactId>custom-component-demo</artifactId>
        <version>1.0-SNAPSHOT</version>
        <name>custom-component-demo</name>
    
        <properties>
            <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        </properties>
    
        <dependencies>
            <dependency>
                <groupId>org.openjfx</groupId>
                <artifactId>javafx-controls</artifactId>
                <version>20</version>
            </dependency>
            <dependency>
                <groupId>org.openjfx</groupId>
                <artifactId>javafx-fxml</artifactId>
                <version>20</version>
            </dependency>
            <dependency>
                <groupId>com.example</groupId>
                <artifactId>custom-component</artifactId>
                <version>1.0-SNAPSHOT</version>
            </dependency>
        </dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>3.11.0</version>
                    <configuration>
                        <source>20</source>
                        <target>20</target>
                    </configuration>
                </plugin>
            </plugins>
        </build>
    </project>
    

    (reimport maven project after editing the pom.xml)

    src/main/java/module-info.java

    module com.example.customcomponentdemo {
        requires javafx.controls;
        requires javafx.fxml;
        requires com.example.customcomponent;
    
        opens com.example.customcomponentdemo to javafx.fxml;
        exports com.example.customcomponentdemo;
    }
    

    src/main/resources/com/example/customcomponentdemo/hello-view.fxml

    Use the file which you saved from SceneBuilder.

    src/main/java/com/example/customcomponentdemo/HelloApplication.java

    Unchanged from generated code:

    package com.example.customcomponentdemo;
    
    import javafx.application.Application;
    import javafx.fxml.FXMLLoader;
    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 {
            FXMLLoader fxmlLoader = new FXMLLoader(HelloApplication.class.getResource("hello-view.fxml"));
            Scene scene = new Scene(fxmlLoader.load(), 320, 240);
            stage.setTitle("Hello!");
            stage.setScene(scene);
            stage.show();
        }
    
        public static void main(String[] args) {
            launch();
        }
    }
    

    src/main/java/com/example/customcomponentdemo/HelloController.java

    package com.example.customcomponentdemo;
    
    import com.example.customcomponent.CustomComponent;
    import javafx.fxml.FXML;
    
    public class HelloController {
        @FXML
        private CustomComponent customComponent;
    
        @FXML
        private void initialize() {
            customComponent.newText("xyzzy");
        }
    }
    

    Step 5: Run your Application

    Double click on the HelloApplication.java file and right click to build the app and run it.

    The app will show your custom component with the text of the component initialized by the app controller.

    App with customized custom component

    Important note on versions

    Look carefully at the Java source and target versions in the custom component project file, it lists 17, not 20. Also the JavaFX version is 19. These are set to match the version of SceneBuilder used. SceneBuilder 19 can only understand JavaFX 19 components (and lower) and runs on JDK 17, so can only understand JAR files compiled to Java 17 (or lower). If you try using a higher target JDK for building your custom component than SceneBuilder understands, then it won't find your component in the JAR file to import it.

    As you can see for the execution project you don't have the same restrictions, so it can freely run on JavaFX 20 and JDK 20 with no issue.

    Related