JavaFX FXML从控制器绑定到嵌套属性



来自WPF和XAML,回到JavaFX,我在绑定方面遇到了相当困难。 通常,如果我用 Java 编码模型类的要点和绑定属性,则 GUI 元素确实有效。但是有这么多样板代码,真的很累。

过去,我一直避免使用FXML,并且总是用纯Java构建我的GUI。现在我想看看我错过了什么。已经熟悉 XAML 提供了更容易进入 FXML 的途径,但我在绑定方面遇到了问题。有一篇来自具有类似背景的人的帖子: 如何在 JavaFX 2 中的 FXML 中进行绑定?

对于"问题"本身: 同样,来自 WPF,从代码到 GUI 获取简单的字符串绑定似乎有很多开销和样板,我想知道是否有更简单、更现代或更方便的绑定属性的方法。

除了旧帖子和最近的答案(截至 2020 年)之外,我没有找到太多关于绑定和 FXML 的内容,说明 FXML 的状态在这段时间之后仍然不是很好。

在这一点上,我不再确定该怎么做。为如此多的属性编写大量样板代码以获得适当的绑定......直接更新 GUI 组件以减少代码实际上可能更可行(例如,通过从模型传递数据在标签本身上设置文本属性)?

用示例更新(很抱歉一开始没有包括这个); 请注意,我正在使用 Lombok 生成带有注释的 getter 和 setter,以减少样板代码的数量:

简单模型类:

public class Model {
@Getter @Setter
private StringProperty someString  = new SimpleStringProperty("test");
}

简化的控制器:

public class Controller {
@FXML
public Label myLabel;
@FXML
public Label myOtherLabel;

@Getter @Setter
private StringProperty someString = new SimpleStringProperty("test");
@Getter @Setter
private Model model = new Model();
public void init() {
// if I wouldn't use binding within FXML I would do the following:
myLabel.textProperty().bind(someString);
myOtherLabel.textProperty().bind(model.getSomeString());
}
public void onButtonClick(ActionEvent e) {
someString.set("clicked");
model.getSomeString().set("clicked");
}
}

简化的 FXML 视图:

<VBox>
<Text>Some text</Text>
<Label fx:id="myLabel" text="${controller.someString}"/>
<Label fx:id="myOtherLabel" text="${controller.model.someString}"/>
</VBox>

更正我最初的陈述: 我实际上错过了一个细节,所以我想我想我弄清楚了为什么它不起作用。看起来我总是需要四个项目才能进行一次绑定工作:

// the actual property holding the data
private StringProperty someString = new SimpleStringProperty("Test");
// a method to return the above property
public StringProperty someStringProperty() {
return someString;
}
// a getter for the actual text of the property
public String getSomeString() {
return someString.get();
}
// a setter for the property string (optional)
public void setSomeString(String text) {
someString.set(text);
}

我所做的只是生成一些需要在GUI上显示的数据。仅为 FXML 中的绑定为每个属性手动写入多达四个成员似乎很多。 替代方法是在代码中绑定我希望显示的每个属性的someProperty.bind(someOtherProperty)。同时,我可以写someLabel.setText(someString);,我就完成了。

在代码中创建绑定(如上所示)也有一个缺点;如果我想换出(或重新生成)模型,绑定不会更新。我将不得不重用模型的一个实例并设置它的内容,以免破坏绑定。

举另一个例子,我来自哪里,可能有助于理解我为什么提出这个问题。在 WPF/XAML 中,您只有以下内容:

public class Model
{
public string SomeString { get; set; }
}
public partial class View ...
{
public Model DataModel { get; set; }
}

在 XAML 文件中:

<Label Text = "{Binding DataModel.SomeString}" />

。然后将模型对象设置为数据上下文,完成。像这样比较这两个框架可能是不公平的,这并不是要抱怨和像"WPF 要好得多"。再一次,只是想解释我来自哪里。

TLDR;我应该咬紧牙关,去找大量的样板文件来有一个很好的FXML绑定,还是用"原始"的方式直接设置GUI内容,省去所有额外的代码?

就目前而言,就我个人而言,放弃 FXML 并纯粹用 Java 构建 GUI 似乎更可行,因为它似乎会大大减少代码量,因为即使您在 FXML 中定义 GUI,您可能也必须加倍重新定义控制器中的控件才能访问它们。

只是一些评论的摘要,我认为这是对问题的完整回答。

JavaFX 属性模式通过添加附加的"属性访问器"方法来扩展标准 Java Bean 模式。其中标准 Java Bean 模式通过两种方法定义类型为T的属性x

public T getX() ; // makes x readable
public void setX(T x) ; // makes x writable

JavaFX 属性模式添加了一个附加方法

public Property<T> xProperty() ; 

它允许访问可观察属性。合同是

xProperty().get() == getX()

并且影响

setX(x) 

xProperty().set(x)

是相同的。

属性访问器提供的其他功能是客户端绑定或观察属性的能力:

someOtherProperty.bind(xProperty())
xProperty().addListener(...)

通常,该模式仅实现为

private final ObjectProperty<T> x = new SimpleObjectProperty<>(initialValue);
public ObjectProperty<T> xProperty() {
return x ;
}
public final T getX() {
return xProperty().get();
}
public final void setX(T value) {
xProperty().set(value);
}

这里需要注意的是,属性类型是T,但字段类型不同(它是ObjectProperty<T>)。与 Java Bean 模式一样,字段的名称也可以与属性的名称不同。

龙目岛不适合所有这些,因为它假定字段名称和类型都定义了属性名称和类型。所以使用

@Getter @Setter
private ObjectProperty<T> x = new SimpleObjectProperty<>();

将生成"错误"的方法:

public ObjectProperty<T> getX();
public void setX(ObjectProperty<T> x);

请注意,您基本上永远不希望替换ObjectProperty本身;若要更改属性值,您应该在现有属性实例上调用set()。调用 Lombok 定义的setX(...)方法将导致之前注册的任何侦听器无法收到数据后续更改的通知。

您可以执行以下操作:

@Getter
private final ObjectProperty<T> x = new SimpleObjectProperty<>();

客户可以很好地使用它;他们必须这样做。

getX().get()

要检索值,

getX().set(x)

进行设置,以及

someProperty.bind(getX())
getX().addListener(...)

等。这有效,但不符合正常的命名模式。

FXML 表达式绑定使用反射和BindingAPI 绑定到属性(和属性的属性)等。

表达式${controller.model.value}本质上翻译为Bindings.select(controller, "model", "value"),所以

<Label fx:id="label" text="${controller.model.value}"/>

相当于做

label.textProperty().bind(Bindings.select(this, "model", "value"));

在控制器中。

反过来,Bindings.select使用反射来标识要绑定到的 JavaFX 属性。在执行此操作时,它假定根据 JavaFX 属性模式命名方法。因此,尝试将 FXML 表达式绑定与龙目岛结合使用将不起作用。

下面是一个完整工作应用程序的示例,它将标签的文本绑定到模型中由控制器持有的属性。

hello-view.fxml

<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>
<VBox alignment="CENTER" spacing="20.0" xmlns:fx="http://javafx.com/fxml"
fx:controller="org.jamesd.examples.binding.HelloController">
<HBox spacing="5">
<Label text="Current value:"/>
<Label text="${controller.model.value}"/>
</HBox>
<HBox spacing="5">
<Label text="Update value:"/>
<TextField fx:id="newValueTF" onAction="#updateValue"/>
</HBox>
<Button text="Reset model" onAction="#resetModel"/>
</VBox>

您好控制器.java

package org.jamesd.examples.binding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
public class HelloController {
private ObjectProperty<Model> model = new SimpleObjectProperty<>(new Model());

public Model getModel() {
return model.get();
}
public ObjectProperty<Model> modelProperty() {
return model;
}
public void setModel(Model model) {
this.model.set(model);
}
@FXML
private TextField newValueTF ;
@FXML
private void updateValue() {
getModel().setValue(newValueTF.getText());
}
@FXML
private void resetModel() {
setModel(new Model());
}
}

型号.java

package org.jamesd.examples.binding;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
public class Model {
private final StringProperty value = new SimpleStringProperty("Test");
public String getValue() {
return value.get();
}
public StringProperty valueProperty() {
return value;
}
public void setValue(String value) {
this.value.set(value);
}
}

和应用程序类:

package org.jamesd.examples.binding;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.stage.Stage;
import java.io.IOException;
public class HelloApplication extends Application {
@Override
public void start(Stage stage) throws IOException {
FXMLLoader fxmlLoader = new FXMLLoader(HelloApplication.class.getResource("hello-view.fxml"));
Scene scene = new Scene(fxmlLoader.load());
stage.setTitle("Hello!");
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch();
}
}

请注意,这允许绑定在替换控制器中的模型实例后保留。这也可以在控制器中的 Java 代码中实现,使用

label.textProperty().bind(Bindings.select(model, "value"));

或者,最好在 JavaFX 19 或更高版本中:

label.textProperty().bind(model.flatMap(Model::valueProperty));

最后,虽然 JavaFX 属性模式很详细,但大多数 IDE 都可以自动生成代码。例如,从

public class Model {
private final StringProperty value = new SimpleStringProperty("Test");
}

从菜单中选择"代码","生成..."和"吸气者/二传手"。然后选择value属性(您可以根据需要选择任意数量的属性,并在一次拍摄中为多个属性执行此操作)和"确定",将为您生成三种方法。

在 Eclipse 中,安装 E(fx)clipse 插件。然后右键单击代码,选择"源"、"生成 JavaFX getter 和 setter",然后按照向导选择适当的属性。

最新更新