3

I have created an FXML-based custom control, which in turn references another FXML-based custom control. They all work just fine when I load them in eclipse, but when I try to import them into SceneBuilder, the outer control (the one containing the other one) does not get imported properly.

Here is a dramatically simplified example:

Widget.java:

package example;

import java.util.logging.Level;
import java.util.logging.Logger;

import javafx.fxml.FXMLLoader;
import javafx.scene.layout.Pane;

public class Widget extends Pane{

    public Widget() {
        
        Logger logger = Logger.getLogger(Widget.class.getName());

        try {
            FXMLLoader loader= new FXMLLoader(Widget.class.getResource("Widget.fxml"));
            Pane pane = loader.load();
            this.getChildren().add(pane);
        }
        catch(Exception e) {
            logger.log(Level.SEVERE, "Failed to load Widget", e);
        }
    }
}

Widget.fxml:

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

<?import javafx.scene.layout.FlowPane?>
<?import javafx.scene.control.Label?>
<?import example.SubWidget?>

<FlowPane  xmlns:fx="http://javafx.com/fxml/1" >
   <children>
       <Label text="Widget"/>
      <SubWidget></SubWidget>
   </children>
</FlowPane>

SubWidget.java:

package example;

import java.util.logging.Level;
import java.util.logging.Logger;

import javafx.fxml.FXMLLoader;
import javafx.scene.layout.Pane;

public class SubWidget extends Pane{

    public SubWidget() {
        
        Logger logger = Logger.getLogger(SubWidget.class.getName());

        try {
            FXMLLoader loader= new FXMLLoader(SubWidget.class.getResource("SubWidget.fxml"));
            Pane pane = loader.load();
       
            this.getChildren().add(pane);
        }
        catch(Exception e) {
            logger.log(Level.SEVERE, "Failed to load SubWidget", e);
        }
    }
}

SubWidget.fxml:

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

<?import javafx.scene.layout.FlowPane?>
<?import javafx.scene.control.Label?>

<FlowPane  xmlns:fx="http://javafx.com/fxml/1" >
   <children>
       <Label text="Subwidget"/>
   </children>
</FlowPane>

I export these to a jar and attempt to import them via SceneBuilder's jar import functionality. When I do so, SubWidget gets imported and looks just fine. Widget will also show up as a control, but it will be completely empty.

When I check the log files I see that it is due to the FXML loader not being able to find the SubWidget class file:

Oct 29, 2020 4:28:30 PM example.Widget <init>
SEVERE: Failed to load Widget
javafx.fxml.LoadException: 
file:/C:/Users/nate/AppData/Roaming/Scene%20Builder/Library/TestFxControl.jar!/example/Widget.fxml

    at javafx.fxml.FXMLLoader.constructLoadException(FXMLLoader.java:2601)
    at javafx.fxml.FXMLLoader.importClass(FXMLLoader.java:2848)
    at javafx.fxml.FXMLLoader.processImport(FXMLLoader.java:2692)
    at javafx.fxml.FXMLLoader.processProcessingInstruction(FXMLLoader.java:2661)
    at javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2517)
    at javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2441)
    at javafx.fxml.FXMLLoader.load(FXMLLoader.java:2409)
    at example.Widget.<init>(Widget.java:27)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(Unknown Source)
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(Unknown Source)
    at java.lang.reflect.Constructor.newInstance(Unknown Source)
    at java.lang.Class.newInstance(Unknown Source)
    at sun.reflect.misc.ReflectUtil.newInstance(Unknown Source)
    at javafx.fxml.FXMLLoader$InstanceDeclarationElement.constructValue(FXMLLoader.java:1009)
    at javafx.fxml.FXMLLoader$ValueElement.processStartElement(FXMLLoader.java:746)
    at javafx.fxml.FXMLLoader.processStartElement(FXMLLoader.java:2707)
    at javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2527)
    at javafx.fxml.FXMLLoader.load(FXMLLoader.java:2425)
    at com.oracle.javafx.scenebuilder.kit.library.util.JarExplorer.instantiateWithFXMLLoader(JarExplorer.java:110)
    at com.oracle.javafx.scenebuilder.kit.library.util.JarExplorer.exploreEntry(JarExplorer.java:160)
    at com.oracle.javafx.scenebuilder.kit.library.util.JarExplorer.explore(JarExplorer.java:70)
    at com.oracle.javafx.scenebuilder.kit.library.user.LibraryFolderWatcher.exploreAndUpdateLibrary(LibraryFolderWatcher.java:325)
    at com.oracle.javafx.scenebuilder.kit.library.user.LibraryFolderWatcher.runDiscovery(LibraryFolderWatcher.java:138)
    at com.oracle.javafx.scenebuilder.kit.library.user.LibraryFolderWatcher.run(LibraryFolderWatcher.java:92)
    at java.lang.Thread.run(Unknown Source)
Caused by: java.lang.ClassNotFoundException: example.SubWidget
    at java.net.URLClassLoader.findClass(Unknown Source)
    at java.lang.ClassLoader.loadClass(Unknown Source)
    at sun.misc.Launcher$AppClassLoader.loadClass(Unknown Source)
    at java.lang.ClassLoader.loadClass(Unknown Source)
    at javafx.fxml.FXMLLoader.loadTypeForPackage(FXMLLoader.java:2916)
    at javafx.fxml.FXMLLoader.loadType(FXMLLoader.java:2905)
    at javafx.fxml.FXMLLoader.importClass(FXMLLoader.java:2846)
    ... 24 more

It appears to me that the FXML loader is using a classloader which doesn't include other custom components, which is odd because if I bypassed the FXML in the Widget class and made it:

public class Widget extends FlowPane{

    public Widget() {
        
        this.getChildren().addAll(new Label("Widget"), new SubWidget());
    }
}

Then it loads just fine in SceneBuilder. So it appears that the ClassLoader that actually loads Widget and the one that the FXMLLoader uses are not the same.

This could possibly be related to this unanswered question, though they aren't exactly the same thing.

NateW
  • 908
  • 1
  • 8
  • 28
  • hmm .. afaics, this should work (and does, instantiating Widget in an app) .. – kleopatra Oct 30 '20 at 10:43
  • Are both custom controls in the same jar? – tbeernot Oct 30 '20 at 14:01
  • 1
    I'm glad to know that I'm not the only one who thinks this should work. And yes, it does work instantiating Widget in an app, but doesn't work loading the widget in SceneBuilder. I think that if I don't get any answers over the weekend, I'll report this as a bug to gluon – NateW Oct 30 '20 at 14:04
  • @tbeernot yes they are – NateW Oct 30 '20 at 14:04
  • I reported this as an issue with gluon: https://github.com/gluonhq/scenebuilder/issues/291 – NateW Nov 05 '20 at 23:05
  • I have the same problem. Did you manage to fix this? – Jawad El Fou Feb 12 '21 at 14:36
  • @JawadElFou I still haven't fixed it and there hasn't been any activity on the issue I reported. I think I'm going to try to fix it myself since its open source. Here's hoping someone at gluon will actually look at the pull request. – NateW Feb 14 '21 at 19:01

1 Answers1

1

I had the same issue and created a work around that is not pretty but works.

Create a jar that contains blank classes for the controls you can not load into Scene Builder, e.g:

package com.junglefinance.gui.controls;
import javafx.fxml.FXML;
import javafx.scene.control.TextField;
public class CategoryTextField extends TextField {
    @FXML private TextField textField;
    public CategoryTextField() {
    }
 }

Make sure the package matches the package of your target control.

Load this jar into Scene Builder. This will allow Scene Builder to generate your FXML ready to load into your program. When it is loaded it will pick up your target control rather than the dummy as the import is (in this case)

<?import com.junglefinance.gui.controls.CategoryTextField?>

Note the package.

  • 1
    Thanks. That should work. I sort of hate to accept this as the answer since its really more of a workaround than a solution, however, it looks like this is the best we're going to get without fixing scene builder. I'll accept it. – NateW Apr 07 '21 at 21:45
  • I've been struggling with the same issue. After putting the same search term into the SO search bar, I came across this, which I think offers a better solution: https://stackoverflow.com/a/50493549 – phanteh Feb 01 '23 at 20:56