Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to create custom components in JavaFX 2.0 using FXML?

Tags:

fxml

javafx-2

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.

like image 839
Andrey Avatar asked Dec 08 '11 17:12

Andrey


People also ask

How do I add a custom component to SceneBuilder?

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.

How do controllers work in JavaFX using FXML?

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.

How do I add FXML to another FXML?

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.


2 Answers

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);             }         }     } } 
like image 132
Andrey Avatar answered Sep 22 '22 06:09

Andrey


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.

like image 29
Daniel De León Avatar answered Sep 23 '22 06:09

Daniel De León