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



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

@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/strong>总是null。我还在TodoItemsVBox中尝试了带有一个arg构造函数的@NamedArg,它甚至失败了,异常为:"无法绑定到非类型化对象。">

有人能提出一个解决方案吗?如何通过控制器中定义的对象集合的参数将其传递到自定义组件中?

代码有两个问题:

  1. 对于FXML表达式绑定,您需要公开类中的属性,而不仅仅是值本身。这适用于ObservableLists以及常规值。因此,您的TodoItemsVBox类需要公开一个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 ;
}
}

将列表公开为属性的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和另一个控制器(中的相同数据,这就变得至关重要。

最新更新