如何在Java中使用JavaFX的TableView与Java记录(records)?

4

Records是Java 16的一个新功能。它在JEP 395: Records中定义。

假设你有一个像这样的记录。

public record Person(String last, String first, int age)
{
    public Person()
    {
        this("", "", 0);
    }
}

这是一个最终类。它自动生成了getter方法:first()、last()和age()。

现在这里有一个JavaFX中的TableView。

/**************************************************
*   Author: Morrison
*   Date:  10 Nov 202021
**************************************************/

import javafx.application.Application;
import javafx.application.Platform;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.scene.control.TableView;
import javafx.scene.control.TableColumn;
import javafx.scene.control.cell.PropertyValueFactory;

public class TV extends Application
{
    public TV()
    {
    }

    @Override
    public void init()
    {
    }

    @Override
    public void start(Stage primary)
    {
        BorderPane root = new BorderPane();
        TableView<Person> table = new TableView<>();
        root.setCenter(table);

        TableColumn<Person, String> lastColumn = new TableColumn<>("Last");
        lastColumn.setCellValueFactory(new PropertyValueFactory<Person,
            String>("last"));

        TableColumn<Person, String> firstColumn = new TableColumn<>("First");
        firstColumn.setCellValueFactory(new PropertyValueFactory<Person,
            String>("first"));

        TableColumn<Person, Integer> ageColumn = new TableColumn<>("Age");
        ageColumn.setCellValueFactory(new PropertyValueFactory<Person,
            Integer>("age"));

        table.getColumns().add(lastColumn);
        table.getColumns().add(firstColumn);
        table.getColumns().add(ageColumn);
        table.getItems().add(new Person("Smith", "Justin", 41));
        table.getItems().add(new Person("Smith", "Sheila", 42));
        table.getItems().add(new Person("Morrison", "Paul", 58));
        table.getItems().add(new Person("Tyx", "Kylee", 40));
        table.getItems().add(new Person("Lincoln", "Abraham", 200));
        Scene s = new Scene(root, 500, 500);
        primary.setTitle("TableView Demo");
        primary.setScene(s);
        primary.show();
    }

    @Override
    public void stop()
    {
    }
}

问题出在这里。

TableColumn<Person, String> lastColumn = new TableColumn<>("Last");
        lastColumn.setCellValueFactory(new PropertyValueFactory<Person,
            String>("last"));
        

TableView期望有名为getFirstgetLastgetAge的方法来完成其工作。除了这种可怕的方法,还有其他解决办法吗?
public record Person(String last, String first, int age)
{
    public Person()
    {
        this("", "", 0);
    }
    public String getLast()
    {
        return last;
    }
    public String getFirst()
    {
        return first;
    }
    public int getAge()
    {
        return age;
    }
}

1
这是基本功能;查看任何使用Java 8或更高版本编写的“TableView”合理教程即可。只需为每个列实现“cellValueFactory”。 - James_D
1个回答

9

解决方案

使用一个lambda单元格值工厂,而不是PropertyValueFactory

关于这两者之间的区别的一些解释,请参见:

为什么这样做有效

正如您所指出的问题,记录访问器不遵循标准的JavaBean属性命名约定,这是PropertyValueFactory所期望的。例如,记录使用first()而不是getFirst()作为访问器,这使其与PropertyValueFactory不兼容。

如果你应用了一个“做这种可怕的事情”的变通方法,即向记录添加额外的get方法,只是为了能够使用PropertyValueFactoryTableView进行交互?-> 绝对不要这样做,有更好的方法 :-)

修复它所需要的是定义自己的自定义单元格工厂,而不是使用PropertyValueFactory

最好使用lambda表达式来完成这个任务(或者对于非常复杂的单元格值工厂,可以使用自定义类)。使用lambda单元格工厂具有类型安全性和编译时检查的优势,而PropertyValueFactory则没有(有关更多信息,请参阅前面引用的答案)。

使用lambda定义而不是PropertyValueFactories的示例

SimpleStringProperty

一个使用lambda单元格工厂定义记录String字段的示例用法:

TableColumn<Person, String> lastColumn = new TableColumn<>("Last");
lastColumn.setCellValueFactory(
        p -> new SimpleStringProperty(p.getValue().last())
);

我们可以明确指定那个p变量的类型,以便清晰明了:一个对TableColumn.CellDataFeatures对象的引用。
lastColumn.setCellValueFactory(
        ( TableColumn.CellDataFeatures < Person, String > p ) -> new SimpleStringProperty (p.getValue().last())
);

将记录字段包装在属性或绑定中是必要的,因为单元格值工厂实现期望以可观察值作为输入。

ReadOnlyStringWrapper

您可以尝试使用ReadOnlyStringWrapper代替SimpleStringProperty,像这样:

lastColumn.setCellValueFactory(
        p -> new ReadOnlyStringWrapper(p.getValue().last()).getReadOnlyProperty()
);

在一个快速测试中,它起作用了。对于不可变记录来说,这可能是一个更好的方法,但我还没有进行彻底的测试以确保,所以为了安全起见,在这个示例的其余部分中我使用了简单的读写属性。
对于其他类型的数据:
同样地,对于一个int字段:
TableColumn<Person, Integer> ageColumn = new TableColumn<>("Age");
ageColumn.setCellValueFactory(
        p -> new SimpleIntegerProperty(p.getValue().age()).asObject()
);

需要在lambda的末尾加上asObject()的原因在这里解释了,如果你感兴趣的话(但这只是JavaFX框架对Java泛型使用的一个奇怪方面,不值得花太多时间去研究,只需添加asObject()调用然后继续即可): 同样地,如果你的记录包含其他对象(或其他记录),那么你可以为SimpleObjectProperty<MyType>定义一个单元格值工厂。
注意:这种用于lambda单元工厂定义的方法和上面定义的模式也适用于标准(非记录)类。对于记录来说,这里没有什么特别之处。唯一需要注意的是,在lambda中的getValue()调用之后要使用正确的访问器名称。例如,使用first()而不是通常在类上定义以支持标准Java Bean命名模式的getFirst()调用。真正好的地方在于,如果你定义了错误的访问器名称,你将会得到一个编译器错误,并且在尝试运行代码之前就知道了确切的问题和位置。
示例代码
基于问题中的代码的完整可执行示例。

example

Person.java

public record Person(String last, String first, int age) {}

RecordTableViewer.java

import javafx.application.Application;
import javafx.beans.property.*;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.stage.Stage;

public class RecordTableViewer extends Application {
    @Override
    public void start(Stage stage) {
        TableView<Person> table = new TableView<>();

        TableColumn<Person, String> lastColumn = new TableColumn<>("Last");
        lastColumn.setCellValueFactory(
                p -> new SimpleStringProperty(p.getValue().last())
        );

        TableColumn<Person, String> firstColumn = new TableColumn<>("First");
        firstColumn.setCellValueFactory(
                p -> new SimpleStringProperty(p.getValue().first())
        );

        TableColumn<Person, Integer> ageColumn = new TableColumn<>("Age");
        ageColumn.setCellValueFactory(
                p -> new SimpleIntegerProperty(p.getValue().age()).asObject()
        );

        //noinspection unchecked
        table.getColumns().addAll(lastColumn, firstColumn, ageColumn);

        table.getItems().addAll(
                new Person("Smith", "Justin", 41),
                new Person("Smith", "Sheila", 42),
                new Person("Morrison", "Paul", 58),
                new Person("Tyx", "Kylee", 40),
                new Person("Lincoln", "Abraham", 200)
        );

        stage.setScene(new Scene(table, 200, 200));
        stage.show();
    }
}

PropertyValueFactory

记录 (record) 是否应该对 PropertyValueFactory 进行 "修复"?

记录字段访问器遵循自己的访问命名约定,fieldname(),就像 Java Beans 一样,getFieldname()

可以提出一个增强请求,要求核心框架中的 PropertyValueFactory 更改其实现方式,以便它也能识别记录访问器的命名标准。

然而,我认为更新 PropertyValueFactory 以识别记录字段访问器不是一个好主意。

更好的解决方案是不针对记录支持更新 PropertyValueFactory,而只允许类型安全的自定义单元格值方法,这在本答案中有详细说明。

我之所以这样认为,是因为 kleopatra 在评论中给出的解释:

自定义 valueFactory 绝对是正确的方法 :) 即使对某些人来说,实现与 PropertyValueFactory 等效可能看起来很有吸引力 - 但这是个坏主意,考虑到 "表中数据不显示" 类型问题的纷至沓来,都是由于拼写错误 ..


cell -> cell.getValue()::first 等等,如果我没有犯错的话。请添加一些代码,否则您的答案最好作为评论给出。 - Joop Eggen
4
实际上,这应该是data -> new SimpleStringProperty(data.getValue().first()),因为它需要返回一个ObservableValue - Slaw
编辑以添加完整示例,并为不同记录字段类型提供样本 Lambda 定义。 - jewelsea
1
一个自定义的valueFactory绝对是正确的选择 :) 即使实现一个类似于PropertyValueFactory的等效方法可能看起来很有吸引力,但是考虑到由于拼写错误而导致的“表格中数据未显示”类型问题的数量之多,这将是一个坏主意。 - kleopatra
1
这很酷,感谢您的回答。我喜欢我们可以使用新的Record类编程方式来简化显示TableView。虽然如果我们可以将记录类直接传递到TableView中并自动生成列,然后通过getItems()方法添加记录,那就更好了。也许可以编写这样一个类,利用反射从记录中提取相关细节以自动生成列... - Michael Sims
@MichaelSims 是的,看起来是可行的。如果完成了,希望这种方法能避免使用 TableView 的初学者遇到的 PropertyValueFactory 问题。 - jewelsea

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接