JavaFX:来自 FXML 和 Item 模板的集合绑定

JavaFX: Collection binding from FXML and Item template

由于缺少教程和以下问题的示例不完整,我决定写这道题。如果这个问题的答案成为解决类似问题的工作示例,我将很高兴。

基于:


TASK

让我们使用 JavaFX 技术制作一个 GUI 应用程序(将 FXML 作为一部分用于制作图形视图的技术)显示 用户集合 并且对于每个用户还显示 his/her 汽车集合 例如。我们还使用 JavaFX 属性bindig 机制将模型(数据)与 GUI 同步。

IMPLEMENTATION

我开始为 usercar 创建 类。

User.java

package example;

import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;

public class User {
    public StringProperty firstName;
    public StringProperty lastName;

    private ObservableList<Car> cars;

    public User(String firstName, String lastName, Car[] cars) {
        this.firstName = new SimpleStringProperty(firstName);
        this.lastName  = new SimpleStringProperty(lastName);
        this.cars      = FXCollections.observableArrayList(cars);
    }

    public String getFirstName() {
        return firstName.get();
    }

    public StringProperty firstNameProperty() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName.set(firstName);
    }

    public String getLastName() {
        return lastName.get();
    }

    public StringProperty lastNameProperty() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName.set(lastName);
    }

    public ObservableList<Car> getCars() {
        return cars;
    }
}

Car.java

package example;

import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;

public class Car {
    public StringProperty modelName;
    public StringProperty manufacturer;

    public Car(String modelName, String manufacturer) {
        this.modelName    = new SimpleStringProperty(modelName);
        this.manufacturer = new SimpleStringProperty(manufacturer);
    }

    public String getModelName() {
        return modelName.get();
    }

    public StringProperty modelNameProperty() {
        return modelName;
    }

    public void setModelName(String modelName) {
        this.modelName.set(modelName);
    }

    public String getManufacturer() {
        return manufacturer.get();
    }

    public StringProperty manufacturerProperty() {
        return manufacturer;
    }

    public void setManufacturer(String manufacturer) {
        this.manufacturer.set(manufacturer);
    }
}

比起我为 FXML GUI 视图 准备 Controller 和用户样本集合。

Controller.java

package example;

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

public class Controller {
    private ObservableList<User> users = FXCollections.observableArrayList(
            new User("John", "Smith",  new Car[] {
                    new Car("LaFerrari", "Ferrari"),
                    new Car("FG X Falcon", "Ford")
            }),
            new User("Ariel", "England", new Car[] {
                    new Car("ATS", "Cadillac"),
                    new Car("Camaro", "Chevrolet"),
                    new Car("458 MM Speciale", "Ferrari")
            }),
            new User("Owen", "Finley", new Car[] {
                    new Car("Corsa", "Chevrolet"),
            })
    );
} 

最后,我还包含了生成的 Main.java.

package example;

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

public class Main extends Application {

    @Override
    public void start(Stage primaryStage) throws Exception{
        Parent root = FXMLLoader.load(getClass().getResource("sample.fxml"));
        primaryStage.setTitle("Users -- example application");
        primaryStage.setScene(new Scene(root, 300, 275));
        primaryStage.show();
    }

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

QUESTION

挑战是使用 JavaFX 绑定 制作 FXML GUI 视图 并显示来自相应 Controller 的准备好的用户集合。可能使用 ListView.

我还想在 FXML 中指定 ListView 项目 design/look 而 不在代码中 – 因为它是 GUI 设计的一部分。 .NET XAML ItemTemplate 的适当 JavaFX FXML 替代品。 ListCell 呢?


这个伪代码的某种方式:

users.fxml

<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.control.ListView?>
<?import javafx.scene.control.Label?>

<GridPane fx:controller="example.Controller"
          xmlns:fx="http://javafx.com/fxml" alignment="center" hgap="10" vgap="10">
    <ListView items="${users}">
        <ListView.ItemTemplate>
            <VBox>
                <Label Text="${firstName}" />
                <Label Text="${lastName}" Style="-fx-background-color: yellow" />

                <ListView items="${cars}">
                    <ListView.ItemTemplate>
                        <HBox>
                            <Label Text="${manufacturer}" />
                            <Label Text=": " />
                            <Label Text="${modelName}" />
                        </HBox>
                    </ListView.ItemTemplate>
                </ListView>
            </VBox>
        </ListView.ItemTemplate>
    </ListView>
</GridPane>

我总是对以下形式的问题持怀疑态度:"I am familiar with technology A, I am learning technology B and want to use it exactly the same way I use technology A"。每种技术(工具包、库、语言等等……)都有自己的预期用途和习语,最好按照预期的方式使用技术。任何给定的技术总会有您喜欢和不喜欢的地方:如果后者胜过前者,那就不要使用它。

JavaFX 的真正设计是在控制器中进行绑定,而不是在 FXML 中进行绑定,因此没有内置的模板机制。所以我可能不会真的推荐这种方法。

也就是说,您可以通过一点创造力和一点妥协来实现类似于您正在尝试做的事情。特别是,此解决方案涉及:

  1. 将单元格的定义 "templates" 移动到不同的 FXML 文件,并且
  2. 写一个(可重用)Java class 将所有内容连接在一起。

这可能不是最好或最有效的方法,但它应该能为您提供一些帮助。

我首先只是将数据重构为 DataAccessor class,并在 FXML 中对其进行实例化,并将其注入到控制器中。这是在 FXML 中访问 items 的便捷方式,但如果它冒犯了您的 MVC/MVP 敏感度,还有其他方法可以做到这一点 :)

package example;

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

public class DataAccessor {
    private ObservableList<User> users = FXCollections.observableArrayList(
            new User("John", "Smith",  new Car[]{
                    new Car("LaFerrari", "Ferrari"),
                    new Car("FG X Falcon", "Ford")
            }),
            new User("Ariel", "England",new Car[]{
                    new Car("ATS", "Cadillac"),
                    new Car("Camaro", "Chevrolet"),
                    new Car("458 MM Speciale", "Ferrari")
            }),
            new User("Owen", "Finley", new Car[]{
                    new Car("Corsa", "Chevrolet")
            })
    );

    public ObservableList<User> getUsers() {
        return users ;
    }
}

package example;
import javafx.fxml.FXML;

public class Controller {

    @FXML
    private DataAccessor dataAccessor ;


} 

基本思想是定义一个通用的单元工厂实现,该实现创建其图形 属性 从指定的 FXML 文件加载的单元:

package example;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;

import javafx.beans.NamedArg;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.util.Callback;

public class FXMLListCellFactory implements Callback<ListView<Object>, ListCell<Object>> {

    private final URL fxmlSource ;

    public FXMLListCellFactory(@NamedArg("fxmlSource") String fxmlSource) throws MalformedURLException {
        this.fxmlSource = new URL(fxmlSource) ;
    }

    @Override
    public ListCell<Object> call(ListView<Object> lv) {
        return new ListCell<Object>() {
            @Override
            protected void updateItem(Object item, boolean empty) {
                super.updateItem(item, empty);
                if (item == null || empty) {
                    setGraphic(null);
                } else {
                    try {
                        FXMLLoader loader = new FXMLLoader(fxmlSource);
                        loader.getNamespace().put("item", item);
                        setGraphic(loader.load());
                    } catch (IOException e) {
                        e.printStackTrace();
                        setGraphic(null);
                    }
                }
            }
        };
    }

}

现在您可以创建一个使用它的 FXML 文件。这个版本有一个 "master-detail" UI(用户列表,select 一个用户,第二个列表显示他们的汽车列表)。

sample.fxml:

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

<?import javafx.scene.layout.HBox?>

<?import example.DataAccessor?>
<?import example.FXMLListCellFactory?>
<?import javafx.scene.control.ListView?>

<HBox xmlns:fx="http://javafx.com/fxml/1" fx:controller="example.Controller" spacing="10">

    <fx:define>
        <DataAccessor fx:id="dataAccessor" />
    </fx:define>

    <ListView fx:id="userList" items="${dataAccessor.users}">
        <cellFactory>
            <FXMLListCellFactory fxmlSource="@userListCell.fxml"/>
        </cellFactory>
    </ListView>

    <ListView fx:id="carList" items="${userList.selectionModel.selectedItem.cars}">
        <cellFactory>
            <FXMLListCellFactory fxmlSource="@carListCell.fxml"/>
        </cellFactory>
    </ListView>
</HBox>

这引用了另外两个 FXML 文件,每个列表视图一个:

userListCell.fxml:

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

<?import javafx.scene.layout.HBox?>
<?import javafx.scene.control.Label?>
<?import javafx.beans.property.SimpleObjectProperty?>

<HBox xmlns:fx="http://javafx.com/fxml/1" spacing="5">
    <Label text="${item.firstName}"/>
    <Label text="${item.lastName}"/> 
</HBox>

和carListCell.fxml:

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

<?import javafx.scene.layout.HBox?>
<?import javafx.beans.property.SimpleObjectProperty?>
<?import javafx.scene.control.Label?>

<HBox xmlns:fx="http://javafx.com/fxml/1">
    <Label text="${item.manufacturer+' '+item.modelName}"/>
</HBox>

Main 和模型 class 与您的问题完全相同。

可能有一种方法可以做到这一点,而无需将单元格图形的 FMXL 分解到单独的文件中,例如说服(以某种方式...)FXMLLoader 将内容解析为文字字符串并将其传递给单元工厂实现;然后在单元工厂实现中将字符串转换为流并使用 FXMLLoader.load(...) 方法获取流。不过,我会把它留给 reader 作为练习。

最后请注意,在单元格的 updateItem(...) 方法中加载和解析 FXML 文件并不是一种特别有效的方法;我找不到快速解决这个问题的方法,尽管它也有可能。