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
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);
}
}
}
}
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"/>
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);
}
}
}
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.
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>