如何使用FXML在JavaFX 2.0中创建自定义组件?

44

我似乎找不到任何关于这个主题的资料。为了举一个更具体的例子,假设我想创建一个简单的组件,将复选框和标签组合在一起。然后,使用此自定义组件的实例填充 ListView。

更新: 请查看我的答案以获取完整的代码

更新2: 有关最新教程,请参阅官方文档. 2.2中添加了许多新东西. 最后,FXML简介涵盖了您需要了解的几乎所有内容。

更新3: Hendrik Ebbers发布了一篇非常有帮助的博客文章,介绍如何创建自定义UI控件。


您的“官方文档”链接中的自定义控件示例已经失效。其中有两种方法不是API的一部分。 - Daniel De León
@danLeon 首先,这不是我的“官方文档”,而是由正在开发JavaFX的Oracle员工编写的“官方文档”。其次,我链接的代码包含了如何在JavaFX 2.2中创建自定义组件的工作示例。很可能你使用的版本较旧,因此缺少方法。以下是该页面的亮点:“开始之前,请确保您使用的NetBeans IDE版本支持JavaFX 2.2”。 - Andrey
没错!我的IDE是在JavaFx 2.1下运行的,谢谢您的评论。现在已经升级到2.2以上版本,并删除了电脑中的任何旧版Java。 - Daniel De León
1
嗨@Andrey,感谢提到我的帖子。你在这里做的事情非常有趣。以前从未玩过fxml和自定义组件,但这是一种很好的外包布局的方式。我认为这将成为我系列中的新“自定义控件”帖子的结果。 - Hendrik Ebbers
@HendrikEbbers 那将会很棒。我期待着它。 - Andrey
4个回答

41
更新:如需最新的教程,请参阅官方文档。在2.2版本中增加了许多新内容。此外,FXML介绍基本涵盖了关于FXML的所有知识。最后,Hendrik Ebbers撰写了一篇非常有用的有关自定义UI控件的博客文章

经过几天的查看API和阅读一些文档(FXML介绍, 开始使用FXML, 属性绑定, FXML的未来),我得出了一个相当合理的解决方案。 从这个小实验中学到的最不直观的信息是,在FXML中声明的控制器实例(使用fx:controller)是由加载FXML文件的FXMLLoader持有的...最糟糕的是,这个重要的事实在我看过的所有文档中只有一个地方提到:

控制器通常只对创建它的FXML loader可见。

因此,请记住,要以编程方式(从Java代码)获取在FXML中声明为fx:controller的控制器实例的引用,请使用FXMLLoader.getController()(请参考下面ChoiceCell类的实现示例)。

需要注意的是,Property.bindBidirectional()会将调用属性的值设置为传入参数属性的值。给定两个布尔型属性 target(初始为false)和source(初始为true),调用target.bindBidirectional(source)会将target的值设置为true。显然,随后对任一属性的更改都会导致另一个属性的值发生变化(例如target.set(false)会将source的值设置为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

这是一个演示FXML和Java如何协作的完整代码(以及其他一些有用的东西)。

包结构:

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();
    }
}

主视图界面.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>

主视图属性

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

->

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);
            }
        }
    }
}

1
很高兴能帮忙。但请确保查看我在更新中提供的链接。2.2版本添加了许多新内容。 - Andrey

9
对于JavaFx 2.1版本,您可以通过以下方式创建自定义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.*?>
<?import customcontrolexample.myCommponent.*?>

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

组件代码:
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>

这段代码需要检查是否存在内存泄漏。


2
快速的答案是使用<fx:include>标签,不过你需要在Controller类中设置ChoiceModel。
<VBox
  xmlns:fx="http://javafx.com/fxml"

  fx:controller="fxmltestinclude.ChoiceDemo"
>
  <children>
    **<fx:include source="Choice.fxml" />**
    <ListView fx:id="choices" />
  </children>
</VBox>

此外,请查看此文档 http://fxexperience.com/wp-content/uploads/2011/08/Introducing-FXML.pdf - JimClarke
好的,但是我该如何使用Choice.fxml来渲染“choices”列表中的每个项目? - Andrey

2

关于自定义组件的FXML章节 给了我正确的提示。我的意图是将标签、滑块和文本框组合成一个自定义的LabeledValueSlider组件。

使用示例: 请参见rc-dukes自动驾驶RC汽车Java FX应用程序的资源/fx

    <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个标记值滑块

FXML文件

<?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>

组件源代码 请参见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);
    }
  }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接