Pass/bind 通过参数在 FXML 中自定义组件(从 vbox 扩展)的集合

Pass/bind a collection to a custom component (extended from vbox) in FXML via parameters

在我的应用程序中,我声明了一个自定义组件,如下所示:

@DefaultProperty("todoItems")
public class TodoItemsVBox extends VBox {
    private ObservableList<TodoItem> todoItems;

    // Setter/Getter omitted
}

现在在 fxml 的某处我想像这样使用 TodoItemsVBox 组件:

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ScrollPane?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>

<BorderPane prefHeight="600" prefWidth="500.0" xmlns="http://javafx.com/javafx/11.0.1" 
xmlns:fx="http://javafx.com/fxml/1" 
fx:controller="com.todolist.controller.TodoListController"
        stylesheets="@../css/app.css">
<top>
    <HBox spacing="10.0">
        <TextField fx:id="input" layoutX="35.0" layoutY="64.0" prefWidth="431.0" promptText="Enter todo task" HBox.hgrow="ALWAYS" onAction="#addTask"/>
        <Button layoutX="216.0" layoutY="107.0" mnemonicParsing="false" onAction="#addTask" prefHeight="27.0" prefWidth="70.0" text="Add" HBox.hgrow="ALWAYS" />
        <padding>
            <Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
        </padding>
    </HBox>
</top>
<center>
    <ScrollPane fitToHeight="true" fitToWidth="true" prefHeight="200.0" prefWidth="200.0" BorderPane.alignment="CENTER">
        <TodoItemsVBox fx:id="todoItemsVBox" todoItems="${todoTasks}"/>
    </ScrollPane>
</center>

...所以我们可以看到 fxml 有它的控制器 TodoListController

public class TodoListController implements {
    private final ObservableList<TodoItem> todoTasks = FXCollections.observableArrayList(/*Fill in the collection somehow - for now doesn't matter*/);

    @FXML
    private TodoItemsVBox todoItemsVBox;

    // Setter/Getter omitted
}

所以,我想做的是:通过这样的构造将 todoTasks 传递到 FXML 中定义的 TodoItemsVBox 中:todoItems="${todoTasks}" ---- 不幸的是这并没有像我预期的那样工作,因为 fxml 文件在控制器初始化之前加载所以 todoTasks 总是 。我还在 TodoItemsVBox 中使用一个 arg 构造函数尝试了 @NamedArg - 它甚至失败并出现异常:“无法绑定到未类型化对象。”

有人可以提出一个解决方案,如何将在控制器中定义的一组对象通过它的参数传递到自定义组件中吗?

您的代码存在两个问题:

  1. 对于 FXML 表达式绑定,您需要公开 class 中的属性,而不仅仅是值本身。这适用于 ObservableLists 以及常规值。所以你的 TodoItemsVBox class 需要公开一个 ListProperty todoItemsProperty()
  2. FXML 表达式绑定(即 ${todoTasks})引用 FXMLLoadernamespace,而不是控制器。控制器会自动注入命名空间(使用键 "controller"),因此,鉴于任务列表存储在您的控制器中(这不一定是个好主意),您可以在此处使用 ${controller.todoTasks}

这是您的应用程序的最小完整版本。

基础TodoItem.java:

public class TodoItem {

    private final String name ;
    public TodoItem(String name) {
        this.name = name ;
    }
    public String getName() {
        return name ;
    }
}

A TodoItemsVBox 将列表公开为 属性:

import javafx.beans.property.ListProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.collections.ListChangeListener.Change;
import javafx.collections.ObservableList;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;

public class TodoItemsVBox extends VBox {

    private ListProperty<TodoItem> todoItems = new SimpleListProperty<>();

    public TodoItemsVBox() {
        // not efficient but works for demo:
        todoItems.addListener((Change<? extends TodoItem> c) -> rebuildView());
    }

    private void rebuildView() {
        getChildren().clear();
        todoItems.stream()
            .map(TodoItem::getName)
            .map(Label::new)
            .forEach(getChildren()::add);
    }

    public ListProperty<TodoItem> todoItemsProperty() {
        return todoItems ;
    }

    public ObservableList<TodoItem> getTodoItems() {
        return todoItemsProperty().get() ;
    }

}

一个简单的控制器:

import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.scene.control.TextField;

public class TodoListController  {
    private ObservableList<TodoItem> todoTasks = FXCollections.observableArrayList();

    // not actually needed...
    @FXML
    private TodoItemsVBox todoItemsVBox;

    @FXML
    private TextField input ;


    public ObservableList<TodoItem> getTodoTasks() {
        return todoTasks;
    }


    @FXML
    private void addTask() {
        todoTasks.add(new TodoItem(input.getText()));
    }
}

FXML 文件(TodoList.fxml):

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ScrollPane?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>

<?import org.jamesd.examples.TodoItemsVBox ?>

<BorderPane prefHeight="600" prefWidth="500.0" xmlns="http://javafx.com/javafx/11.0.1" 
xmlns:fx="http://javafx.com/fxml/1" 
fx:controller="com.todolist.controller.TodoListController"
>

<top>
    <HBox spacing="10.0">
        <TextField fx:id="input" promptText="Enter todo task" HBox.hgrow="ALWAYS" onAction="#addTask"/>
        <Button onAction="#addTask" text="Add" HBox.hgrow="ALWAYS" />
        <padding>
            <Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
        </padding>
    </HBox>
</top>
<center>
    <ScrollPane fitToWidth="true" prefHeight="200.0" prefWidth="200.0" BorderPane.alignment="CENTER">
        <TodoItemsVBox fx:id="todoItemsVBox" todoItems="${controller.todoTasks}"/>
    </ScrollPane>
</center>
</BorderPane>

最后是申请 class:

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class TodoApp extends Application {

    @Override
    public void start(Stage primaryStage) throws Exception {
        FXMLLoader loader = new FXMLLoader(getClass().getResource("TodoList.fxml"));
        Scene scene = new Scene(loader.load());
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public static void main(String[] args) {
        Application.launch(args);
    }

}

真的,控制器不是存储数据的地方;您应该有一个单独的模型 class 来执行此操作,该模型在控制器和视图之间共享。在这里做起来相当简单;您只需要对 FXMLLoader 做更多的工作(即将模型放入命名空间,并手动创建和设置控制器)。

例如:

import javafx.collections.FXCollections;
import javafx.collections.ObservableList;

public class TodoModel {

    private ObservableList<TodoItem> todoTasks = FXCollections.observableArrayList();

    public ObservableList<TodoItem> getTodoTasks() {
        return todoTasks;
    }
}

然后你的控制器变成:

import javafx.fxml.FXML;
import javafx.scene.control.TextField;

public class TodoListController  {

    // not actually needed...
    @FXML
    private TodoItemsVBox todoItemsVBox;

    @FXML
    private TextField input ;

    private TodoModel model ;

    public TodoModel getModel() {
        return model;
    }

    public void setModel(TodoModel model) {
        this.model = model;
    }

    @FXML
    private void addTask() {
        model.getTodoTasks().add(new TodoItem(input.getText()));
    }
}

修改 FXML 以使用

<TodoItemsVBox fx:id="todoItemsVBox" todoItems="${model.todoTasks}"/>

最后 assemble 应用

public void start(Stage primaryStage) throws Exception {

    TodoModel model = new TodoModel();

    FXMLLoader loader = new FXMLLoader(getClass().getResource("TodoList.fxml"));
    loader.getNamespace().put("model", model);
    Scene scene = new Scene(loader.load());

    TodoListController controller = loader.getController();
    controller.setModel(model);

    primaryStage.setScene(scene);
    primaryStage.show();
}

这种方法的优点是您的数据现在与 UI(视图和控制器)分开,如果您想访问 [=70] 的另一部分中的相同数据,这就变得必不可少=](这将使用另一个 FXML 和另一个控制器)。