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:
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.
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:
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.
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.
Download and install SceneBuilder 19.
Create a new (Empty) project in SceneBuilder.
Click on the cog icon next to the library search field.
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.
Choose "Add JAR".
The "Import" dialog will show "CustomComponent" with a tick next to it and a preview image of the component.
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.
Close the "Library Manager" dialog.
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:
<?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>
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");
}
}
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.
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.
Especially see the section titled "Component Pre-requisites" in the linked related answer, which some code requirements for the custom components you create (not all of which are intuitive).
Load custom components in scenebuilder 17
The above is for importing 3rd party libraries into SceneBuilder which is a bit simpler than creating libraries yourself as was demonstrated in this answer.