通过参数将集合传递/绑定到FXML中的自定义组件(从vbox扩展)

时间:2020-03-02 13:00:42

标签: java javafx fxml

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

@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}” < / strong> ----不幸的是,这无法按我预期的那样工作,因为fxml文件在控制器初始化之前加载,因此 todoTasks 始终为 null 。我还在TodoItemsVBox中使用一个arg构造函数尝试了@NamedArg-甚至失败,并出现以下异常:“无法绑定到未类型化的对象。”

有人可以提出一种解决方案,该方法如何将控制器中定义的对象集合通过其参数传递到自定义组件中?

1 个答案:

答案 0 :(得分:1)

您所拥有的代码有两个问题:

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

这是可以正常运行的应用程序的最低版本。

基本的TodoItem.java:

public class TodoItem {

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

将列表作为属性公开的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>

最后是应用程序类:

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

}

确实,控制器无处存储数据;您应该有一个单独的模型类来做到这一点,该类在控制器和视图之间共享。在这里这样做相当简单。您只需要对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}"/>

最后用组装程序

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(视图和控制器)分离了,如果要访问UI的另一部分(将使用另一个FXML和另一个控制器)。