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.
In SceneBuilder, activate the drop-down button next to "Library" in the top of the left pane: Choose "Import JAR/FXML File..." Select the Jar file created from step 1. Make sure the class you need access to in SceneBuilder ( PaneWithSelectionListener ) is checked.
Specifying Controller Class in FXML Notice the fx:controller attribute in the root element (the VBox element). This attribute contains the name of the controller class. An instance of this class is created when the FXML file is loaded. For this to work, the controller class must have a no-argument constructor.
The <fx:include> tag can be used to include one fxml file into another. The controller of the included fxml can be injected into the controller of the including file just as any other object created by the FXMLLoader . This is done by adding the fx:id attribute to the <fx:include> element.
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); } } } }
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With