How to create custom components in JavaFX 2.0 using FXML?

后端 未结 4 670
小鲜肉
小鲜肉 2020-11-30 20:01

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

相关标签:
4条回答
  • 2020-11-30 20:15

    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);
                }
            }
        }
    }
    
    0 讨论(0)
  • 2020-11-30 20:15

    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);
        }
      }
    }
    
    0 讨论(0)
  • 2020-11-30 20:28

    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.

    0 讨论(0)
  • 2020-11-30 20:33

    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>
    
    0 讨论(0)
提交回复
热议问题