How to create custom components in JavaFX 2.0 using FXML?
Asked Answered
A

4

44

I can't seem to find any material on the subject. To give a more concrete example, let's say I want to create a simple component that combines a checkbox and a label. Then, populate a ListView with instances of this custom component.

UPDATE: see my answer for complete code

UPDATE 2: For an up-to-date tutorial, please, consult the official documentation. There was a lot of new stuff that was added in 2.2. Finally, the Introduction to FXML covers pretty much everything you need to know about FXML.

UPDATE 3: Hendrik Ebbers made an extremely helpful blog post about custom UI controls.

Allomorphism answered 8/12, 2011 at 17:8 Comment(5)
The custom control example in your 'official documentation' link is broken. In there are two methods that are not part of the API.Rameau
@danLeon first, it's not MY "official documentation". it's THE "official documentation" written by Oracle employees who are working on JavaFX. second, the code I'm linking to contains a working example of how to create custom components in JavaFX 2.2. most likely the version you have is older, hence missing methods. Here's a highlight from that page: "Before you start, ensure that the version of NetBeans IDE that you are using supports JavaFX 2.2"Allomorphism
You right! My IDE was under JavaFx 2.1, Thanks for comment. Now over 2.2, I deleted any previous java version in my computer.Rameau
Hi @Andrey, thanks for mentioning my post. You are doing very interesting stuff here. Never played with fxml and custom components before but it's a great way to outsource the layout. I think the outcome of this will be a new "custom controls" post in my series.Levileviable
@HendrikEbbers that would be awesome. I'm looking forward to it.Allomorphism
A
41

Update: For an up-to-date tutorial, please, consult the official documentation. There was a lot of new stuff that was added in 2.2. Also, the Introduction to FXML covers pretty much everything you need to know about FXML. Finally, Hendrik Ebbers made an extremely helpful blog post about custom UI controls.


After a few days of looking around the API and reading through some docs (Intro to FXML, Getting started with FXML Property binding, Future of FXML) I've come up with a fairly sensible solution. The least straight-forward piece of information I learned from this little experiment was that the instance of a controller (declared with fx:controller in FXML) is held by the FXMLLoader that loaded the FXML file... Worst of all, this important fact is only mentioned in one place in all the docs I saw:

a controller is generally only visible to the FXML loader that creates it

So, remember, in order to programmatically (from Java code) obtain a reference to the instance of a controller that was declared in FXML with fx:controller use FXMLLoader.getController() (refer to the implementation of the ChoiceCell class below for a complete example).

Another thing to note is that Property.bindBiderctional() will set the value of the calling property to the value of the property passed in as the argument. Given two boolean properties target (originally set to false) and source (initially set to true) calling target.bindBidirectional(source) will set the value of target to true. Obviously, any subsequent changes to either property will change the other property's value (target.set(false) will cause the value of source to be set to false):

BooleanProperty target = new SimpleBooleanProperty();//value is false
BooleanProperty source = new SimpleBooleanProperty(true);//value is true
target.bindBidirectional(source);//target.get() will now return true
target.set(false);//both values are now false
source.set(true);//both values are now true

Anyway, here is the complete code that demonstrates how FXML and Java can work together (as well as a few other useful things)

Package structure:

com.example.javafx.choice
  ChoiceCell.java
  ChoiceController.java
  ChoiceModel.java
  ChoiceView.fxml
com.example.javafx.mvc
  FxmlMvcPatternDemo.java
  MainController.java
  MainView.fxml
  MainView.properties

FxmlMvcPatternDemo.java

package com.example.javafx.mvc;

import java.util.ResourceBundle;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class FxmlMvcPatternDemo extends Application
{
    public static void main(String[] args) throws ClassNotFoundException
    {
        Application.launch(FxmlMvcPatternDemo.class, args);
    }

    @Override
    public void start(Stage stage) throws Exception
    {
        Parent root = FXMLLoader.load
        (
            FxmlMvcPatternDemo.class.getResource("MainView.fxml"),
            ResourceBundle.getBundle(FxmlMvcPatternDemo.class.getPackage().getName()+".MainView")/*properties file*/
        );

        stage.setScene(new Scene(root));
        stage.show();
    }
}

MainView.fxml

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

<?import java.lang.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<VBox
    xmlns:fx="http://javafx.com/fxml"
    fx:controller="com.example.javafx.mvc.MainController"

    prefWidth="300"
    prefHeight="400"
    fillWidth="false"
>
    <children>
        <Label text="%title" />
        <ListView fx:id="choicesView" />
        <Button text="Force Change" onAction="#handleForceChange" />
    </children>
</VBox>

MainView.properties

title=JavaFX 2.0 FXML MVC demo

MainController.java

package com.example.javafx.mvc;

import com.example.javafx.choice.ChoiceCell;
import com.example.javafx.choice.ChoiceModel;
import java.net.URL;
import java.util.ResourceBundle;
import javafx.collections.FXCollections;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.util.Callback;

public class MainController implements Initializable
{
    @FXML
    private ListView<ChoiceModel> choicesView;

    @Override
    public void initialize(URL url, ResourceBundle rb)
    {
        choicesView.setCellFactory(new Callback<ListView<ChoiceModel>, ListCell<ChoiceModel>>()
        {
            public ListCell<ChoiceModel> call(ListView<ChoiceModel> p)
            {
                return new ChoiceCell();
            }
        });
        choicesView.setItems(FXCollections.observableArrayList
        (
            new ChoiceModel("Tiger", true),
            new ChoiceModel("Shark", false),
            new ChoiceModel("Bear", false),
            new ChoiceModel("Wolf", true)
        ));
    }

    @FXML
    private void handleForceChange(ActionEvent event)
    {
        if(choicesView != null && choicesView.getItems().size() > 0)
        {
            boolean isSelected = choicesView.getItems().get(0).isSelected();
            choicesView.getItems().get(0).setSelected(!isSelected);
        }
    }
}

ChoiceView.fxml

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

<?import java.lang.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<HBox
    xmlns:fx="http://javafx.com/fxml"

    fx:controller="com.example.javafx.choice.ChoiceController"
>
    <children>
        <CheckBox fx:id="isSelectedView" />
        <Label fx:id="labelView" />
    </children>
</HBox>

ChoiceController.java

package com.example.javafx.choice;

import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;

public class ChoiceController
{
    private final ChangeListener<String> LABEL_CHANGE_LISTENER = new ChangeListener<String>()
    {
        public void changed(ObservableValue<? extends String> property, String oldValue, String newValue)
        {
            updateLabelView(newValue);
        }
    };

    private final ChangeListener<Boolean> IS_SELECTED_CHANGE_LISTENER = new ChangeListener<Boolean>()
    {
        public void changed(ObservableValue<? extends Boolean> property, Boolean oldValue, Boolean newValue)
        {
            updateIsSelectedView(newValue);
        }
    };

    @FXML
    private Label labelView;

    @FXML
    private CheckBox isSelectedView;

    private ChoiceModel model;

    public ChoiceModel getModel()
    {
        return model;
    }

    public void setModel(ChoiceModel model)
    {
        if(this.model != null)
            removeModelListeners();
        this.model = model;
        setupModelListeners();
        updateView();
    }

    private void removeModelListeners()
    {
        model.labelProperty().removeListener(LABEL_CHANGE_LISTENER);
        model.isSelectedProperty().removeListener(IS_SELECTED_CHANGE_LISTENER);
        isSelectedView.selectedProperty().unbindBidirectional(model.isSelectedProperty())
    }

    private void setupModelListeners()
    {
        model.labelProperty().addListener(LABEL_CHANGE_LISTENER);
        model.isSelectedProperty().addListener(IS_SELECTED_CHANGE_LISTENER);
        isSelectedView.selectedProperty().bindBidirectional(model.isSelectedProperty());
    }

    private void updateView()
    {
        updateLabelView();
        updateIsSelectedView();
    }

    private void updateLabelView(){ updateLabelView(model.getLabel()); }
    private void updateLabelView(String newValue)
    {
        labelView.setText(newValue);
    }

    private void updateIsSelectedView(){ updateIsSelectedView(model.isSelected()); }
    private void updateIsSelectedView(boolean newValue)
    {
        isSelectedView.setSelected(newValue);
    }
}

ChoiceModel.java

package com.example.javafx.choice;

import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;

public class ChoiceModel
{
    private final StringProperty label;
    private final BooleanProperty isSelected;

    public ChoiceModel()
    {
        this(null, false);
    }

    public ChoiceModel(String label)
    {
        this(label, false);
    }

    public ChoiceModel(String label, boolean isSelected)
    {
        this.label = new SimpleStringProperty(label);
        this.isSelected = new SimpleBooleanProperty(isSelected);
    }

    public String getLabel(){ return label.get(); }
    public void setLabel(String label){ this.label.set(label); }
    public StringProperty labelProperty(){ return label; }

    public boolean isSelected(){ return isSelected.get(); }
    public void setSelected(boolean isSelected){ this.isSelected.set(isSelected); }
    public BooleanProperty isSelectedProperty(){ return isSelected; }
}

ChoiceCell.java

package com.example.javafx.choice;

import java.io.IOException;
import java.net.URL;
import javafx.fxml.FXMLLoader;
import javafx.fxml.JavaFXBuilderFactory;
import javafx.scene.Node;
import javafx.scene.control.ListCell;

public class ChoiceCell extends ListCell<ChoiceModel>
{
    @Override
    protected void updateItem(ChoiceModel model, boolean bln)
    {
        super.updateItem(model, bln);

        if(model != null)
        {
            URL location = ChoiceController.class.getResource("ChoiceView.fxml");

            FXMLLoader fxmlLoader = new FXMLLoader();
            fxmlLoader.setLocation(location);
            fxmlLoader.setBuilderFactory(new JavaFXBuilderFactory());

            try
            {
                Node root = (Node)fxmlLoader.load(location.openStream());
                ChoiceController controller = (ChoiceController)fxmlLoader.getController();
                controller.setModel(model);
                setGraphic(root);
            }
            catch(IOException ioe)
            {
                throw new IllegalStateException(ioe);
            }
        }
    }
}
Allomorphism answered 13/12, 2011 at 14:22 Comment(1)
glad to help. however, make sure you take a look at the links I provided in the update. a lot of new stuff was added in 2.2Allomorphism
A
10

For JavaFx 2.1, You can create a custom FXML control component by this way:

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

<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import customcontrolexample.myCommponent.*?>

<VBox xmlns:fx="http://javafx.com/fxml" fx:controller="customcontrolexample.FXML1Controller">
    <children>
        <MyComponent welcome="1234"/>
    </children>
</VBox>

Component code:

MyComponent.java

package customcontrolexample.myCommponent;

import java.io.IOException;
import javafx.beans.property.StringProperty;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.layout.Pane;
import javafx.util.Callback;

public class MyComponent extends Pane {

    private Node view;
    private MyComponentController controller;

    public MyComponent() {
        FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("myComponent.fxml"));
        fxmlLoader.setControllerFactory(new Callback<Class<?>, Object>() {
            @Override
            public Object call(Class<?> param) {
                return controller = new MyComponentController();
            }
        });
        try {
            view = (Node) fxmlLoader.load();

        } catch (IOException ex) {
        }
        getChildren().add(view);
    }

    public void setWelcome(String str) {
        controller.textField.setText(str);
    }

    public String getWelcome() {
        return controller.textField.getText();
    }

    public StringProperty welcomeProperty() {
        return controller.textField.textProperty();
    }
}

MyComponentController.java

package customcontrolexample.myCommponent;

import java.net.URL;
import java.util.ResourceBundle;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.TextField;

public class MyComponentController implements Initializable {

    int i = 0;
    @FXML
    TextField textField;

    @FXML
    protected void doSomething() {
        textField.setText("The button was clicked #" + ++i);
    }

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        textField.setText("Just click the button!");
    }
}

myComponent.fxml

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

<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<VBox xmlns:fx="http://javafx.com/fxml" fx:controller="customcontrolexample.myCommponent.MyComponentController">
  <children>
    <TextField fx:id="textField" prefWidth="200.0" />
    <Button mnemonicParsing="false" onAction="#doSomething" text="B" />
  </children>
</VBox>

This code needs to check if there is no memory leak.

Almonry answered 1/10, 2012 at 21:5 Comment(0)
O
2

Quick answer is <fx:include> tag, however, you would need to set the ChoiceModel in the Controller class.

<VBox
  xmlns:fx="http://javafx.com/fxml"

  fx:controller="fxmltestinclude.ChoiceDemo"
>
  <children>
    **<fx:include source="Choice.fxml" />**
    <ListView fx:id="choices" />
  </children>
</VBox>
Oyster answered 8/12, 2011 at 21:55 Comment(2)
Also, check out this document fxexperience.com/wp-content/uploads/2011/08/…Oyster
ok, but how would I use Choice.fxml to render each item in the 'choices' list?Allomorphism
N
2

The introduction to fxml chapter on custom components

gave me the right hint. My intention was to combine a label a slider and a textfield to one custom LabeledValueSlider component.

Usage example: see resources/fx of rc-dukes Self-Driving RC Car Java FX App

    <LabeledValueSlider fx:id='cannyThreshold1' text="Canny threshold 1" blockIncrement="1" max="2000" min="0" value="20" format="\%.0f"/>
    <LabeledValueSlider fx:id="cannyThreshold2" text="Canny threshold 2"  blockIncrement="1" max="2000" min="0" value="50" format="\%.0f"/>
    <LabeledValueSlider fx:id="lineDetectRho" text="LineDetect rho" blockIncrement="0.01" max="20" min="0" value="0.5" />
    <LabeledValueSlider fx:id="lineDetectTheta" text="LineDetect theta" blockIncrement="0.01" max="5" min="-5" value="0.5" />
    <LabeledValueSlider fx:id="lineDetectThreshold" text="LineDetect threshold" blockIncrement="1" max="200" min="0" value="20" format="\%.0f" />
    <LabeledValueSlider fx:id="lineDetectMinLineLength" text="LineDetect minLineLength"  blockIncrement="1" max="200" min="0" value="50" format="\%.0f"/>
    <LabeledValueSlider fx:id="lineDetectMaxLineGap" text="LineDetect maxLineGap" blockIncrement="1" max="500" min="0" value="50" format="\%.0f"/>

7 LabeledValueSliders

FXML file

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

<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.Slider?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.HBox?>

<fx:root type="javafx.scene.layout.HBox" xmlns:fx="http://javafx.com/fxml">

    <padding>
        <Insets left="10" right="10" />
    </padding>
    <Label fx:id='label' text="Label for Slider" minWidth="180"/>
    <Slider fx:id='slider' blockIncrement="1" max="100" min="0" value="50" />
    <TextField fx:id="textField" maxWidth="75"/>
</fx:root>

Component Source code see LabeledValueSlider.java

package org.rcdukes.app;

import java.io.IOException;
import java.net.URL;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.Slider;
import javafx.scene.control.TextField;
import javafx.scene.layout.HBox;

/**
 * a Slider with a Label and a value
 * 
 * @author wf
 *
 */
public class LabeledValueSlider extends HBox {
  public static boolean debug=true;
  protected static final Logger LOG = LoggerFactory
      .getLogger(LabeledValueSlider.class);
  @FXML
  private Label label;
  @FXML
  private Slider slider;
  @FXML
  private TextField textField;

  String format;


  public String getFormat() {
    return format;
  }

  public void setFormat(String format) {
    textField.textProperty().bind(slider.valueProperty().asString(format));
    this.format = format;
  }

  public double getBlockIncrement() {
    return slider.getBlockIncrement();
  }

  public void setBlockIncrement(double value) {
    slider.setBlockIncrement(value);
  }

  public double getMax() {
    return slider.getMax();
  }

  public void setMax(double value) {
    slider.setMax(value);
  }

  public double getMin() {
    return slider.getMin();
  }

  public void setMin(double value) {
    slider.setMin(value);
  }

  public double getValue() {
    return slider.getValue();
  }

  public void setValue(double value) {
    slider.setValue(value);
  }

  public String getText() {
    return label.getText();
  }

  public void setText(String pLabelText) {
    label.setText(pLabelText);
  }

  public URL  getResource(String path) {
    return getClass().getClassLoader().getResource(path);
  }

  /**
   * construct me
   * see https://docs.oracle.com/javase/9/docs/api/javafx/fxml/doc-files/introduction_to_fxml.html#custom_components
   */
  public LabeledValueSlider() {
    FXMLLoader fxmlLoader = new FXMLLoader(
        getResource("fx/labeledvalueslider.fxml"));
    try {
      // let's load the HBox - fxmlLoader doesn't know anything about us yet
      fxmlLoader.setController(this); 
      fxmlLoader.setRoot(this);
      Object loaded = fxmlLoader.load();
      Object root=fxmlLoader.getRoot();

      if (debug) {
        String msg=String.format("%s loaded for root %s", loaded.getClass().getName(),root.getClass().getName());
        LOG.info(msg);
      }

      textField.setAlignment(Pos.CENTER_RIGHT);
      if (format == null)
        setFormat("%.2f");
    } catch (IOException exception) {
      throw new RuntimeException(exception);
    }
  }
}
Natividad answered 23/2, 2020 at 9:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.