Adding a Custom JavaFX Component to Scene Builder 2.0 (Part 2)

In a previous post I demonstrated how to add a custom component to Scene Builder 2.0 which was basically a short cut for copying & pasting FXML from the custom component into the FXML of the UI control which contained the component.

In this post I will demonstrate how to add a component as what I will call a ‘customized’ FXML component. Where the component will be encapsulated in its own custom FXML tag.

So to start off with, let’s say we have a label which contains a commodity name, image, and change in price. We’d like to have a collection of these commodity image labels on the main UI. Scene Builder is used to design the custom label (below):

enter image description here

Once the component is laid out, be sure that the “Use fxroot:construct” option is selected under the “Document” -> “Controller” accordion item on the left pane of Scene Builder.

enter image description here

In the project, for the custom component you should have the CommodityImageLabel.fxml file (the component just created in Scene Builder), and then create a CommodityImageLabel.java class to act as the component’s controller. (More on this in just a second)

enter image description here

First, a look below at the FXML code that was generated by Scene Builder

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

<?import java.lang.*?>
<?import java.net.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.image.*?>
<?import javafx.scene.effect.*?>
<?import javafx.geometry.*?>
<?import javafx.scene.text.*?>

<fx:root styleClass="glass-pane" type="AnchorPane" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
   <children>
      <AnchorPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="110.0" prefWidth="675.0" styleClass="glass-pane" stylesheets="@zoi.css">
          <children>
              <Label fx:id="percentChangeLabel" layoutX="480.0" layoutY="14.0" text="- 0.23%" AnchorPane.rightAnchor="14.0">
                  <styleClass>
                      <String fx:value="label-text" />
                      <String fx:value="header" />
                  </styleClass>
              </Label>
              <Label fx:id="commodityLabel" layoutX="90.0" layoutY="28.0" text="Wheat">
                  <font>
                      <Font name="System Bold" size="50.0" />
                  </font>
                  <padding>
                      <Insets left="25.0" right="25.0" />
                  </padding>
                  <styleClass>
                      <String fx:value="label-text" />
                      <String fx:value="header" />
                  </styleClass>
              </Label>
              <Label fx:id="directionLabel" layoutX="120.0" layoutY="3.0" styleClass="label-direction-text" text="Long">
                  <opaqueInsets>
                      <Insets bottom="20.0" />
                  </opaqueInsets>
              </Label>
                <ImageView fx:id="commodityImageView" fitHeight="75.0" fitWidth="75.0" layoutX="14.0" layoutY="18.0" pickOnBounds="true" preserveRatio="true" AnchorPane.bottomAnchor="15.0" AnchorPane.leftAnchor="15.0" AnchorPane.topAnchor="15.0">
               <image>
                  <Image url="@../images/ZW.png" />
               </image></ImageView>
          </children>
      </AnchorPane>
   </children>
</fx:root>

Ok, on to the contorller class for the component.

/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package com.zoicapital.commoditylabelwidget;

import java.io.IOException;
import java.text.DecimalFormat;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.Label;
import javafx.scene.image.ImageView;
import javafx.scene.layout.AnchorPane;

/**
 *
 * @author robbob
 */
public class CommodityImageLabel extends AnchorPane {

    @FXML
    protected Label directionLabel;

    @FXML
    protected Label commodityLabel;

    @FXML
    protected Label percentChangeLabel;

    @FXML
    private ImageView commodityImageView;

    protected DecimalFormat decimalFormat = new DecimalFormat("#0.00");
    protected String currentStyle = "";

    public CommodityImageLabel() {
        FXMLLoader fxmlLoader = new FXMLLoader(
                getClass().getResource("/fxml/CommodityImageLabel.fxml"));

        fxmlLoader.setRoot(this);
        fxmlLoader.setController(this);

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

    }

    public void setPercentChange(double percentChange) {

        StringBuilder builder = new StringBuilder();
        String percentChangeString = decimalFormat.format(Math.abs(percentChange) * 100.0);
        String style;
        if ("0.00".equals(percentChangeString)) {
            style = Style.LABEL_PLAIN;
        } else {
            if (percentChange < 0) {
                builder.append("-");
                style = Style.LABEL_RED;
            } else {
                builder.append("+");
                style = Style.LABEL_GREEN;
            }
        }

        builder.append(percentChangeString);
        builder.append("%");

        percentChangeLabel.setText(builder.toString());

        if (!style.equals(currentStyle)) {
            getStyleClass().remove(currentStyle);
            currentStyle = style;
            getStyleClass().add(currentStyle);
        }

    }

}

 

First off, notice that it extends AnchorPane, and also that the constructor loads the component’s FXML and then tells the loader that ‘this’ will be acting as the component’s controller.
That’s it for creating the custom component. Build it, jar it up, and its ready to be imported into Scene Builder and used in an application.


Using the Custom Component in Scene Builder

The new application will need to include the previously created .jar file as a dependency, in this case the “CommodityLabelWidget.jar” file.

enter image description here

The DemoScreen.fxml component for this application currently looks as follows.

enter image description here

In order to get the CommodityImageLabel into Scene Builder’s pallete, select the small “gear” icon in the Library header of Scene Builder, and select the “Import JAR/FXML File…” item.
enter image description here

Next, browse to the .jar file that contains your custom component.
enter image description here

Once the .jar is selected, the import dialog should display a list of all the custom components in the .jar file. In this case the CommodityImageLabel is the only component available.

enter image description here

At this point the custom component is added to the Scene Builder pallette, and can be dragged on to the main UI.

enter image description here

Screen shot showing 2 CommodityImageLabels placed on the main application UI.

enter image description here

Now, in order to be able to access these custom component’s from your main UI’s controller class, enter an fx:id for the components in Scene Builder’s “Code” section for each component. Scene Builder may complain that it isn’t able to find an injectable field with the name you just entered, but don’t worry, you’ll be adding it to the UI’s controller shortly.
Save the .FXML file in Scene Builder and then head back over to the IDE.

enter image description here

Below is the FXML file generated by Scene Builder. Take note of the CommodityImageLabel “custom” FXML tags, also note that the controller for this class is “DemoScreenController” which is shown next.

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

<?import com.zoicapital.commoditylabelwidget.*?>
<?import javafx.scene.text.*?>
<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>


<AnchorPane id="AnchorPane" prefHeight="632.0" prefWidth="889.0" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.zoicapital.demolabelproject.DemoScreenController">
   <children>
      <Label layoutX="72.0" layoutY="48.0" text="My Commodities">
         <font>
            <Font name="System Bold" size="36.0" />
         </font>
      </Label>
      <Separator layoutX="72.0" layoutY="109.0" prefHeight="4.0" prefWidth="289.0" />
      <CommodityImageLabel fx:id="commodityLabel1" layoutX="72.0" layoutY="144.0" />
      <CommodityImageLabel fx:id="commodityLabel2" layoutX="72.0" layoutY="268.0" />
   </children>
</AnchorPane>

In order to access the custom label components, be sure to name the fields the same as the fx:id controller name that you specified in Scene Builder, and to annotate the fields with the @FXML annotation.
Once this is complete you will have access to the CommodityImageLabel’s underlying controller classes, and can manipulate the custom component.

/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */

package com.zoicapital.demolabelproject;

import com.zoicapital.commoditylabelwidget.CommodityImageLabel;
import java.net.URL;
import java.util.ResourceBundle;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;

/**
 * FXML Controller class
 *
 * @author robbob
 */
public class DemoScreenController implements Initializable {


    @FXML
    protected CommodityImageLabel commodityLabel1;

    @FXML
    protected CommodityImageLabel commodityLabel2;

    /**
     * Initializes the controller class.
     */
    @Override
    public void initialize(URL url, ResourceBundle rb) {
        commodityLabel1.setPercentChange(50.0);
        commodityLabel2.setPercentChange(-4.55);
    }    

}

The unfortunate wrinkle to this is that Scene Builder does not automatically pick up changes to the custom components. Meaning if you were to make a modification to the CommodityImageLabel component, you would need to manually re-import the .jar file in to Scene Builder in order to see the changes. This should hopefully be something that is addressed in a future version of Scene Builder.

twitter: @RobTerpilowski

Advertisements

12 thoughts on “Adding a Custom JavaFX Component to Scene Builder 2.0 (Part 2)

  1. I was searching for a clean approach to create custom components. Your blog helped me a lot to get there. The problem comes with the two possibilities to define a controller of a fxml-document. Providing a controller in the fxml via fx:controller allows the designer to use all the features of the JavaFX Scene Builder. While importing such a file in the builder creates only a copy of its content, like you mentioned in:
    https://rterp.wordpress.com/2014/05/21/adding-custom-javafx-components-to-scene-builder-2-0/

    Assigning the controller to the FXML with the FXMLLoader in the constructor of the controller, combined with all the other stuff you’ve explained above results a nice encapsulated custom component. The only drawback is you will not see fx.ids or event handler names in the scene builder, because there is no controller associated.

    To solve the problem I parsed the fxml file, replaced the fx:controller string before loading the fxml document with the FXMLLoader.load(InputStream) method. In my other attempt I’ve written my own FXMLLoader to handle the “fx:controller=” attribute in a different way when a controller was already specified.

    • thanks for your sharing, but i still don’t know how to solve “Controller value already specified” when i click button in other scene………..

  2. Pingback: JavaFX links of the week, August 4 // JavaFX News, Demos and Insight // FX Experience

  3. For various reasons I knew I wanted to do this, but didn’t know where to start until I read your blog post here. Your post was crucial to my solution.

    But I wanted to go one step further. From what I can tell, your technique only allows one to build new, custom JavaFX components by compositing existing, stock JavaFX components. I wanted to be able create new, SceneBuilder-recognizable JavaFX components that are based on my custom subclasses of ANY JavaFX component.

    I’m happy to say that I accomplished this today, and, except for one required, rather kludgy, code segment in the custom JavaFX class, the solution seems elegant to me.

    Here’s my revised version of the fxml file. … (The same file, with no changes, will work for ANY custom JavaFX class I write as long as it subclasses Node or any of its descendants.)

    That’s it for the fxml file!

    And here’s a sample class that loads that fxml file and subclasses TextField so it can enforce number-only entry in a JavaFX TextField …

    package numberTextField;

    import java.io.*;
    import javafx.fxml.*;
    import java.util.regex.*;
    import javafx.beans.value.*;
    import javafx.scene.control.*;
    import javafx.util.converter.*;

    public class NumberTextField extends TextField implements ChangeListener
    /*
    This class only allows numbers to be entered into the text field.
    In addition, the entered number is forced to be >= mMin and <= mMax.

    We employ a kludge here to get around leaking "this" in our constructor.
    (Leaking "this" in a constructor can cause concurrency problems – the
    recipient of a "this" injected from within a constructor could start
    using the object referenced by "this" before it has finished constructing
    itself.) We override the layoutChildren() method of our super class,
    because it is guaranteed to be called early on, and we want that guaranteed
    early call so the class can load itself from its fxml file outside the
    constructor. (See, for example, the "fxmlLoader.setRoot(this);" line below.)

    We don't use a factory design pattern because we want this class's jar to
    appear as a custom component in SceneBuilder and SceneBuilder requires a public
    default constructor. So we work around this impasse by using a boolean
    flag ("mHasBeenInitialized") that is checked in our layoutChildren() method
    and if false we call the init() method that, among other things, loads the
    class's fxml file.
    */
    {
    // class variables
    private static final String cMyFXMLPath = "NumberTextField.fxml";
    private static final long cDefaultMin = 0;
    private static final long cDefaultMax = 100;
    private static final int cWidth = 40;
    private static final int cHeight = 40;
    private static final Pattern cNumberPattern = Pattern.compile( "^(-?0|-?[1-9]\\d*)(\\.\\d+)?(E\\d+)?$" );
    private static final NumberStringConverter cConverter = new NumberStringConverter();
    // member variables
    private boolean mHasBeenInitialized = false; // See above
    private final long mMin;
    private final long mMax;

    public NumberTextField()
    // SceneBuilder requires a default constructor.
    {
    this(cDefaultMin, cDefaultMax);
    }

    public NumberTextField(long min,
    long max)
    {
    super();
    mMin = min;
    mMax = max;
    }

    private void init() // See note above.
    {
    FXMLLoader fxmlLoader;

    if (mHasBeenInitialized)
    return;

    mHasBeenInitialized = true;
    fxmlLoader = new FXMLLoader(getClass().getResource(cMyFXMLPath));
    fxmlLoader.setRoot(this);
    fxmlLoader.setController(this);
    try
    {
    fxmlLoader.load();
    }
    catch (IOException exception)
    {
    throw new RuntimeException(exception);
    }
    textProperty().addListener(this);
    }

    @Override protected void layoutChildren()
    {
    if (!mHasBeenInitialized) // See note above.
    init();
    super.layoutChildren();
    }

    @Override public void changed(ObservableValue observable,
    String oldValue,
    String newValue)
    // This is the code that prevents anything but numbers from
    // being entered into the text field. In addition, the entered
    // number is forced to be >= mMin and mMax)
    setText(cConverter.toString(mMax));
    else if (value.longValue() “Show Preview in Window” and try typing or pasting anything into the NumberTextField other than numbers. It blocks all non-number input.

    This is a really powerful, generalizable technique for creating FXML-based JavaFX components.

    Thank you so much for providing me the gateway to this!

  4. For various reasons I knew I wanted to do this, but didn’t know where to start until I read your blog post here. Your post was crucial to my solution.

    But I wanted to go one step further. From what I can tell, your technique only allows one to build new, custom JavaFX components by compositing existing, stock JavaFX components. I wanted to be able create new, SceneBuilder-recognizable JavaFX components that are based on my custom subclasses of ANY JavaFX component.

    I’m happy to say that I accomplished this today, and, except for one required, rather kludgy, code segment in the custom JavaFX class, the solution seems elegant to me.

    Here’s my revised version of the fxml file. … (The same file, with no changes, will work for ANY custom JavaFX class I write as long as it subclasses Node or any of its descendants.)

    	<?xml version="1.0" encoding="UTF-8"?>
    	<?import javafx.scene.*?>
    	<!--
    	The use of 'fx:root' and 'type="Node"' below is the 'magic'
    	that allows our custom component to inherit from, and therefore
    	customize, ANY JavaFX Node it wants to!
    	-->
    	<fx:root type="Node" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1"/>
    	

    That’s it for the fxml file!

    And here’s a sample class that enforces only number entry in a JavaFX TextField …

    	package numberTextField;
    
    	import java.io.*;
    	import javafx.fxml.*;
    	import java.util.regex.*;
    	import javafx.beans.value.*;
    	import javafx.scene.control.*;
    	import javafx.util.converter.*;
    
    
    	public class NumberTextField extends TextField implements ChangeListener<String>
    		/*
    		This class prevents anything but numbers from being entered into the text
    		field. In addition, the entered number is forced to be >= mMin and <= mMax.
    
    		We employ a kludge here to get around leaking "this" in our constructor.
    		(Leaking "this" in a constructor can cause concurrency problems - the
    		recipient of a "this" injected from within a constructor could start
    		using the object referenced by "this" before it has finished constructing
    		itself.) We override the layoutChildren() method of our super class,
    		because it is guaranteed to be called early on, and we want that guaranteed
    		early call so the class can load itself from its fxml file outside the
    		constructor. (See, for example, the "fxmlLoader.setRoot(this);" line below.)
    
    		We don't use a factory design pattern because we want this class's jar to
    		appear as a custom component in SceneBuilder and SceneBuilder requires a public
    		default constructor. So we work around this impasse by using a boolean
    		flag ("mHasBeenInitialized") that is checked in our layoutChildren() method
    		and if false we call the init() method that, among other things, loads the
    		class's fxml file.
    		*/
    		{
    		// class variables
    		private static final String			cMyFXMLPath = "NumberTextField.fxml";
    		private static final long			cDefaultMin = 0;
    		private static final long			cDefaultMax = 100;
    		private static final int			cWidth = 40;
    		private static final int			cHeight = 40;
    		private static final Pattern		cNumberPattern = Pattern.compile( "^(-?0|-?[1-9]\\d*)(\\.\\d+)?(E\\d+)?$" );
    		private static final NumberStringConverter	cConverter = new NumberStringConverter();
    		// member variables
    		private boolean				mHasBeenInitialized = false;    // See above
    		private final long				mMin;
    		private final long				mMax;
    
    
    		public NumberTextField()
    		// SceneBuilder requires a default constructor.
    		{
    		this(cDefaultMin, cDefaultMax);
    		}
    
    
    		public NumberTextField(long min,
    				long max)
    		{
    		super();
    		mMin = min;
    		mMax = max;
    		}
    
    
    		private void init()	    // See note above.
    		{
    		FXMLLoader	fxmlLoader;
    	
    		if (mHasBeenInitialized)
    			return;
    	
    		mHasBeenInitialized = true;
    		fxmlLoader = new FXMLLoader(getClass().getResource(cMyFXMLPath));
    		fxmlLoader.setRoot(this);
    		fxmlLoader.setController(this);
    		try
    			{
    			fxmlLoader.load();
    			}
    		catch (IOException exception)
    			{
    			throw new RuntimeException(exception);
    			}
    		textProperty().addListener(this);
    		}
    
    
    		@Override protected void layoutChildren()
    		{
    		if (!mHasBeenInitialized)	// See note above.
    			init();
    		super.layoutChildren();
    		}
    
    
    		@Override public void changed(ObservableValue<? extends String> observable,
    					  String oldValue,
    					  String newValue)
    		// This is the code that prevents anything but numbers from
    		// being entered into the text field. In addition, the entered
    		// number is forced to be >= mMin and <= mMax.
    		{
    		Number	    value;
    
    		if (cNumberPattern.matcher(newValue).matches())
    			{
    			value = cConverter.fromString(newValue);
    			if (value.longValue() > mMax)
    			setText(cConverter.toString(mMax));
    			else if (value.longValue() < mMin)
    			setText(cConverter.toString(mMin));
    			}
    		else if (!newValue.isEmpty())
    			{
    			setText(oldValue);
    			}
    		}
    		}
    	

    When you build this jar and import it into SceneBuilder your subclass of the stock JavaFX TextField is available for all the same SceneBuilder manipulations that the stock TextField supports. To prove this, just drag the NumberTextField control to the compositing pane and try editing its properties. It behaves just like the stock TextField. Then invoke the menu item “Preview” -> “Show Preview in Window”, and try typing or pasting anything into the NumberTextField other than numbers. It blocks all non-number input.

    This is a really powerful, generalizable technique for creating FXML-based JavaFX components.

    Thank you so much for providing me the gateway to this!

    • Thanks for the feedback Dean!

      Also, thank you for sharing your work, this looks like a really elegant solution for importing ‘home-grown’ JavaFX custom components.

  5. Thank you, it really helped me creating my first custom control (it’s a status bar that i use on all my screens in my app to show infos).
    I have added an ObjectProperty, so that i can change my status icon via FXML an not only in code:

    Sample:
    private ObjectProperty icon;

    public ObjectProperty iconProperty() {
    if (icon == null) {
    icon = new ObjectPropertyBase() {
    @Override
    public Object getBean() {
    return StatusBar.this;
    }

    @Override
    public String getName() {
    return “icon”;
    }

    @Override
    public void setValue(Image image) {
    imgView.setImage(image);
    }

    @Override
    public Image getValue() {
    return imgView.getImage();
    }
    };
    }
    return icon;
    }

    public void setIcon(Image icon) {
    iconProperty().setValue(icon);
    }

    public Image getIcon() {
    return icon == null ? null : icon.getValue();
    }

    Now i can change/set the Image in FXML:

    That’s really nice!

    One question: Is it possible to set/change the custom control icon in scenebuilder? All custom controls have the ? icon, an custom icon would be nice.

    Greetings,
    Tom

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s