3

I created this custom component:

public class IconButton extends Button {

    @FXML private ImageView imageView;

    private IconButtonState state;
    private String fullIconUrl;
    private String outlineIconUrl;

    public IconButton(@NamedArg("fullIconUrl") String fullIconUrl,
                      @NamedArg("outlineIconUrl") String outlineIconUrl) {
        URL url = getClass().getResource(View.ICON_BUTTON.getFileName());
        FXMLLoader loader = new FXMLLoader(url);

        loader.setRoot(this);
        loader.setController(this);

        state = IconButtonState.NOT_INITIALIZED;

        this.fullIconUrl = fullIconUrl;
        this.outlineIconUrl = outlineIconUrl;

        try {
            loader.load();
        } catch (IOException exception) {
            exception.printStackTrace();
            throw new RuntimeException(exception);
        }
    }

    @FXML
    public void initialize() {
        this.state = IconButtonState.ACTIVE;

        String url = buildUrl(fullIconUrl);
        Image image = new Image(url);
        imageView.setImage(image);
    }
}
<!-- icon-button.fxml -->
<?xml version="1.0" encoding="UTF-8"?>

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

<fx:root mnemonicParsing="false" prefHeight="65.0" prefWidth="98.0" style="-fx-background-color: transparent;" type="Button" xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1">
   <graphic>
      <ImageView fx:id="imageView" fitHeight="64.0" fitWidth="48.0" pickOnBounds="true" preserveRatio="true">
      </ImageView>
   </graphic>
</fx:root>

Then, I instantiated my IconButton component in another fxml file like this:

<!-- home.fxml -->
<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.*?>
<?import javafx.scene.layout.*?>
<?import agill.deshopp.components.*?>

<BorderPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="768.0" prefWidth="1024.0" xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1">
    <top>
        <HBox styleClass="app-bar" stylesheets="@../css/home.css">
         <padding>
            <Insets bottom="15.0" top="15.0" />
         </padding>
         <children>
             <IconButton fullIconUrl="form-full" outlineIconUrl="form-outline"></IconButton>
             <IconButton fullIconUrl="message-full" outlineIconUrl="message-outline"></IconButton>
             <IconButton fullIconUrl="chart-full" outlineIconUrl="chart-outline"></IconButton>
             <Region HBox.hgrow="ALWAYS"></Region>
             <IconButton fullIconUrl="settings-full" outlineIconUrl="settings-outline"></IconButton>
         </children>
        </HBox>
    </top>
   <center>
      <Pane BorderPane.alignment="CENTER" />
   </center>
</BorderPane>

The code runs fine and the screen renders as expected. However, I can't open the file in SceneBuilder. It prompts me with this exception:

java.lang.RuntimeException: Cannot create instance of agill.deshopp.components.IconButton with given set of properties: [fullIconUrl, outlineIconUrl]

javafx.fxml.LoadException: 
/home/allan/IdeaProjects/california/src/main/resources/agill/deshopp/fxml/home.fxml:14

How do I fix this?

Allan Juan
  • 2,048
  • 1
  • 18
  • 42
  • 3
    You never instantiate `fullIconUrl` or `outlineIconUrl`. Presumably if you read the complete stack trace, you'll see a null pointer exception at the line `this.fullIconUrl.set(fullIconUrl);` – James_D Jul 07 '20 at 21:10
  • 2
    The moral here is that you should always [read the complete stack trace](https://stackoverflow.com/questions/3988788/what-is-a-stack-trace-and-how-can-i-use-it-to-debug-my-application-errors) – James_D Jul 07 '20 at 21:16
  • Hm... it would make sense if it was a NullPointerException, but there wasn't any on the StackTrace. Anyways I initialized both StringProperties as `new SimpleStringProperty()` before setting values to them, but it still doesn't work. It's hard for me to digest the stack trace because most of it are lib calls – Allan Juan Jul 07 '20 at 21:26
  • Not that I don't read it, but, for example, on the stack trace I just posted. I can't take much value from it, all I can understand is "hey man you tried to create a new scene but it raised runtime exception my bad" – Allan Juan Jul 07 '20 at 21:28
  • 1
    Hmm. All of this happens by reflection, of course, so it’s possible for the root cause of the exception to get “lost”. I’ll reopen when I’m back at the computer, since you still get the issue when you initialize those fields. (No option to reopen on the phone app.) – James_D Jul 07 '20 at 21:37
  • Hey, I figured it out! I followed your advice. I was able to get a better stack trace by calling `exception.printStackTrace()` instead of just throwing the exception. I found out `imageView` was evaluating as null in the initialize method because I forgot to put an `fx:id` in the fxml file :) Do you think I should delete this? – Allan Juan Jul 07 '20 at 21:54
  • I'm going to edit the question, because now SceneBuilder is showing a problem with this same file. – Allan Juan Jul 07 '20 at 22:19
  • This is a wild guess based on information I _think_ I remember reading once, but what happens if you call `loader.setClassLoader(getClass().getClassLoader())` before calling `loader.load()`? – Slaw Jul 08 '20 at 13:47
  • Hmm... I tried this but nothing changed. The program kept with the same behaviour. I get this error when trying to open the file in Intellij's embbeded SceneBuilder, but when I open it in actual SceneBuilder, the screen renders, but I get "selection contains undefined reference" when I select the IconButtons in home.fxml – Allan Juan Jul 08 '20 at 17:04
  • Please post [mre] – c0der Jul 11 '20 at 05:16

2 Answers2

1

The code posted in the question is not mre and can not be invoked.
However the following code is an mre. It runs with no exceptions.
Fxml files can be edited using ScreenBuilder.
Modify it to your needs to find out what's wrong in the code posted in the question.
Note that form-full throws exception so I used form_full and that the fxml assignment should include $ sign: fullIconUrl="$form_full".

package fx_tests.test;
import java.io.IOException;
import java.net.URL;
import javafx.beans.NamedArg;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.Button;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;

public class IconButton extends Button {

    @FXML private ImageView imageView;
    private final String fullIconUrl;

    public IconButton(@NamedArg("fullIconUrl") String fullIconUrl) {

        this.fullIconUrl = fullIconUrl;

        URL url = getClass().getResource("icon-button.fxml");
        FXMLLoader loader = new FXMLLoader(url);
        loader.setRoot(this);
        loader.setController(this);
    
        try {
            loader.load();
        } catch (IOException exception) {
            exception.printStackTrace();
        }
    }

    @FXML
    public void initialize() {
        Image image = new Image(fullIconUrl);
        imageView.setImage(image);
    }
}

Home.fxml:

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

<?import javafx.geometry.*?>
<?import javafx.scene.layout.*?>
<?import fx_tests.test.*?>

<BorderPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="168.0" 
prefWidth="124.0" xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1">
    <top>
         <HBox>
         <padding>
            <Insets bottom="15.0" top="15.0" />
         </padding>
         <children>
             <IconButton fullIconUrl="$form_full"></IconButton>
         </children>
        </HBox>
    </top>
   <center>
      <Pane BorderPane.alignment="CENTER" />
   </center>
</BorderPane>

icon-button.fxml:

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

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

<fx:root mnemonicParsing="false" prefHeight="65.0" prefWidth="98.0" style="-fx-background-color: transparent;" 
type="Button" xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1">
   <graphic>
      <ImageView fx:id="imageView" fitHeight="64.0" fitWidth="48.0" pickOnBounds="true" preserveRatio="true">
      </ImageView>
   </graphic>
</fx:root>

Test with:

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
    
public class Main extends Application {

    private static final String IMAGE = "https://www.shareicon.net/data/128x128/2015/03/28/14104_animal_256x256.png";
    @Override
    public void start(Stage currentStage) throws Exception {

        FXMLLoader loader = new FXMLLoader(getClass().getResource("Home.fxml"));
        loader.getNamespace().put("form_full", IMAGE);
        Parent root=loader.load();
        currentStage.setScene(new Scene(root));
        currentStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}
c0der
  • 18,467
  • 6
  • 33
  • 65
0

When you create a custom component in JavaFX, SceneBuilder need to know the definition of this component so it can load it. I have a few custom components of my own and had to export them as a jar, and import them into SceneBuilder. Here is an answer that gives instructions on how to do this:

Adding a custom component to SceneBuilder 2.0

One catch. SceneBuilder only supports up to Java 11. So your custom component must be built and exported to a jar with this version. I had problems with Java 14 and had to backport my specific JavaFX components to Java 11. Good luck!

NaderNader
  • 1,038
  • 1
  • 11
  • 16