Doombringer
Doombringer

Reputation: 664

JavaFX - Scene Builder 16 not loading Custom Control properly (no errors)

Following this tutorial, I made a custom control.

CustomControl:

package com.oof.bruh.controls;

import com.oof.bruh.controllers.CustomController;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.layout.*;

public class CustomControl extends VBox {

    CustomController controller;

    public CustomControl() {
        super();

        try {
            FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/custom_control.fxml"));
            // Creating new controller
            controller = new CustomController();
            // Hooking up the controller to the 'custom_control.fxml'
            loader.setController(controller);
            // Create a redundant Node in which to load the fxml
            Node node = loader.load();
            this.getStylesheets().add(this.getClass().getResource("/css/style.css").toExternalForm());

            this.getChildren().add(node);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

CustomController

package com.oof.bruh.controllers;

import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Label;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import org.kordamp.ikonli.javafx.FontIcon;

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

public class CustomController implements Initializable {

    @FXML
    VBox container;
    @FXML
    HBox titleContainer;
    @FXML
    Label title;
    @FXML
    FontIcon btnClock;

    @Override
    public void initialize(URL url, ResourceBundle resourceBundle) {

        btnClock.setOnMouseClicked(new EventHandler<MouseEvent>() {
            @Override
            public void handle(MouseEvent event) {
            
                System.out.println("Clock clicked");
            }
        });
    }

    public VBox getContainer() {
        return container;
    }

    public void setContainer(VBox container) {
        this.container = container;
    }

    public HBox getTitleContainer() {
        return titleContainer;
    }

    public void setTitleContainer(HBox titleContainer) {
        this.titleContainer = titleContainer;
    }

    public Label getTitle() {
        return title;
    }

    public void setTitle(Label title) {
        this.title = title;
    }

    public FontIcon getBtnClock() {
        return btnClock;
    }

    public void setBtnClock(FontIcon btnClock) {
        this.btnClock = btnClock;
    }
}

FXML Contents

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

<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.Pane?>
<?import javafx.scene.layout.VBox?>
<?import org.kordamp.ikonli.javafx.FontIcon?>

<VBox fx:id="container" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefWidth="218.0" spacing="2.0" xmlns="http://javafx.com/javafx/16" xmlns:fx="http://javafx.com/fxml/1">
   <children>
      <HBox fx:id="titleContainer">
         <children>
            <Label fx:id="title" text="&lt;title&gt;" />
            <Pane HBox.hgrow="ALWAYS" />
            <FontIcon fx:id="btnClock" iconLiteral="mdi2c-clock-outline" />
         </children>
      </HBox>
   </children>
   <padding>
      <Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
   </padding>
</VBox>

Gradle Dependencies

dependencies {
    implementation "org.openjfx:javafx-base:11.0.2:${platform}"
    implementation "org.openjfx:javafx-controls:11.0.2:${platform}"
    implementation "org.openjfx:javafx-graphics:11.0.2:${platform}"
    implementation "org.openjfx:javafx-fxml:11.0.2:${platform}"
    implementation "org.kordamp.ikonli:ikonli-javafx:12.2.0"
    implementation "org.kordamp.ikonli:ikonli-materialdesign2-pack:12.2.0"
}

Problem

When I export the .jar and import it into the Scene Builder from the JAR/FXML Manager option, it detects it, but there's nothing in the preview, which from what I've come to learn, means something is not getting loaded properly.

I checked the Custom Library Folder -> Show JAR Analysis Report. There's nothing there.

What happens when I add it anyway and drag it into the scene?

  1. If added to a Container (e.g. Pane), its width and height are 0 (zero). Even if made bigger manually, there are no "sub-components" to be found.
  2. If added directly, it has the default layout settings for VBox, but still nothing showing.

If I run the file using Gradle, everything works as expected.

Expected result

To be able to add multiple custom FXML made components coupled in a .jar.

Things I've tried

  1. If I take the custom_control.fxml from the Gradle generated build folder and add it in the Scene Builder the same way as the .jar, it adds it with no issues. Not only does it have the proper layout dimensions, but I can even select each "sub-component" individually.
  2. Creating everything programmatically, generating the .jar and importing it into the Scene Builder, loads the control fine, but I've no access to each "sub-component".
  3. I took the <fx:root> route, as explained in this answer here and many other similar ones. When I run it via Gradle, it would work, but it still wouldn't show up in Scene Builder.
  4. Adding loader.setClassLoader(getClass().getClassLoader()); as mentioned by "Brad Turek" in this question, even though I didn't get any ClassNotFoundException errors.
  5. Not adding the CustomController in CustomControl, but instead putting it in the custom_control.fxml file directly. Once again, it would run fine using Gradle, but when added to the Scene Builder, there's nothing in the preview section.
  6. Restarting Scene Builder multiple times.
  7. I followed this tutorial, which seems to be outdated, thinking perhaps adding a Skin would help somehow. Unfortunately, trying to extend BehaviorBase or using setSkinClassName(CustomControlSkin.class.getName());, seems to lead to Cannot find symbol 'XXXXXXXX'. Eventually I stumbled upon this GitHub example of a DateTimePicker which implemented a Skin, after also reading UI Controls Architecture. Even though I managed to make a Skin for it, it would STILL not show up properly in Scene Builder.
  8. I followed the instructions in the extensive answer here, making sure to cover the "Component Pre-requisites" section, and here but to no avail, i.e. the same issue persists, runs fine using Gradle -> Nothing in the component preview section in Scene Builder.
  9. Used the old System.out.println("...") way after each line in the CustomControl thinking it might show up in JAR Analysis Report, but it didn't.

Possible issue

I'm thinking something might not be getting loaded along the way, but with no errors, I find it hard to pin point the exact place.

Upvotes: 1

Views: 872

Answers (1)

Doombringer
Doombringer

Reputation: 664

As suggested by José Pereda, I ran Scene Builder via terminal, thanks to which I was greeted by the following error:

> com.oof.bruh.controls.CustomControl - OK

javafx.fxml.LoadException: 
file:/path/to/my/project/files/bruh/build/libs/bruh-1.0.jar!/fxml/custom_control.fxml
at javafx.fxml/javafx.fxml.FXMLLoader.constructLoadException(Unknown Source)
    at javafx.fxml/javafx.fxml.FXMLLoader.importClass(Unknown Source)
    at javafx.fxml/javafx.fxml.FXMLLoader.processImport(Unknown Source)
    at javafx.fxml/javafx.fxml.FXMLLoader.processProcessingInstruction(Unknown Source)
    at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(Unknown Source)
    at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(Unknown Source)
    at javafx.fxml/javafx.fxml.FXMLLoader.load(Unknown Source)
    at com.oof.bruh.controls.CustomControl.<init>(CustomControl.java:22)
    .
    .
    .
    .
    .
Caused by: java.lang.ClassNotFoundException: org.kordamp.ikonli.javafx.FontIcon
    at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(Unknown Source)
    at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(Unknown Source)
    at java.base/java.lang.ClassLoader.loadClass(Unknown Source)
    at javafx.fxml/javafx.fxml.FXMLLoader.loadTypeForPackage(Unknown Source)
    at javafx.fxml/javafx.fxml.FXMLLoader.loadType(Unknown Source)
    ... 127 more

This line at com.oof.bruh.controls.CustomControl.<init>(CustomControl.java:22) being the Node node = loader.load();.

After seeing this error, I implemented solution #4 mentioned in the Things I've tried section.

package com.oof.bruh.controls;

import com.oof.bruh.controllers.CustomController;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.layout.*;

public class CustomControl extends VBox {

    CustomController controller;

    public CustomControl() {
        super();

        try {
            FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/custom_control.fxml"));
            // Creating new controller
            controller = new CustomController();
            // Hooking up the controller to the 'custom_control.fxml'
            loader.setController(controller);
            loader.setClassLoader(getClass().getClassLoader());
            // Create a redundant Node in which to load the fxml
            Node node = loader.load();
            this.getStylesheets().add(this.getClass().getResource("/css/style.css").toExternalForm());

            this.getChildren().add(node);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

I created the .jar but subsequently, I got java.util.zip.ZipException: invalid LOC header (bad signature) error.

Thanks to the answer here, I decided to do a simple Gradle clean -> build.

Then, when I added the new .jar to Scene Builder, the custom control finally showed up! Unfortunately, I don't have access to the "sub-components". I'm not sure if it's an expected behavior or another separate issue. Either way, I'll do some research first.

Why didn't this solution work earlier?

My thinking is because, many of the times, I wasn't doing "clean -> build" every time I made a change to my files, but instead directly used the jar option in Gradle.

There for the .jar file might've been corrupted and I might've been getting the ZipException without knowing it before even getting the chance to get the ClassNotFoundException error.

Perhaps, at one of the times when I had the loader.setClassLoader(getClass().getClassLoader()); implemented, I might've been getting the ZipException, so even if I wouldn't have gotten ClassNotFoundException, since the .jar was in a bad condition, the Scene Builder wouldn't have been able to use its contents.

Things to remember

  1. Always do Gradle clean -> build when any of the generated files are needed for something after any changes to those files.
  2. If Scene Builder is used and no errors are shown anywhere, always start it from the command line or terminal to see for possible errors.

Upvotes: 3

Related Questions