如何使用带有 java 条记录的 JavaFX TableView?
How do you use a JavaFX TableView with java records?
Java 记录是 Java15 的新功能。假设你有这样一条记录。
public record Person(String last, String first, int age)
{
public Person()
{
this("", "", 0);
}
}
这是决赛 class。它自动生成 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 需要名称为 getFirst
、getLast
和 getAge
的方法来完成它的工作。除了做这件可怕的事情,还有其他解决方法吗?
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;
}
}
解决方案
使用 lambda 单元值工厂而不是 PropertyValueFactory
。
有关两者之间差异的一些解释,请参阅:
为什么这样有效
正如您所注意到的,问题在于记录访问器不遵循标准 java bean 属性 命名约定,这正是 PropertyValueFactory
所期望的。例如,记录使用 first()
而不是 getFirst()
作为访问器,这使得它与 PropertyValueFactory
.
不兼容
您是否应该应用“做这件可怕的事情”的变通方法,即向记录添加额外的获取方法,以便您可以使用 PropertyValueFactory
与 TableView
交互? -> 绝对不是,还有更好的方法:-)
需要修复的是定义您自己的自定义细胞工厂,而不是使用 PropertyValueFactory
。
最好使用 lambda(或自定义 class 用于真正复杂的单元值工厂)。使用 lambda 单元工厂具有 PropertyValueFactory
所没有的类型安全和编译时检查的优势(有关更多信息,请参阅先前引用的答案)。
定义 lambda 而非 PropertyValueFactories 的示例
记录 String
字段的 lambda 单元工厂定义的示例用法:
TableColumn<Person, String> lastColumn = new TableColumn<>("Last");
lastColumn.setCellValueFactory(
p -> new SimpleStringProperty(p.getValue().last())
);
有必要将记录字段包装在 属性 或绑定中,因为单元格值工厂实现需要一个可观察值作为输入。
您可以使用 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()
);
此处解释了将 asObject()
放在 lambda 末尾的必要性,以防您感到好奇(但这只是 java 泛型用法的一个奇怪方面 JavaFX 框架,不值得花很多时间研究,只需添加 asObject()
调用并继续 IMO):
- Why does asObject allow use of SimpleLongProperty with setCellValueProperty?
同样,如果您的记录包含其他对象(或其他记录),那么您可以为 SimpleObjectProperty<MyType>
.
定义单元格值工厂
注意: 这种用于 lambda 细胞工厂定义的方法和上面定义的模式也适用于标准(非记录)classes。这里没有什么特别的记录。唯一需要注意的是在 lambda 中调用 getValue()
之后注意使用正确的访问器名称。例如,使用 first()
而不是通常在 class 上定义的标准 getFirst()
调用来支持标准 Java Bean 命名模式。真正伟大的事情是,如果你定义了错误的访问器名称,你会得到一个编译器错误,并且在你尝试 运行 代码之前就知道确切的问题和位置。
示例代码
基于问题中代码的完整可执行示例。
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 是否应该为记录“固定”?
记录字段访问器遵循自己的访问命名约定,fieldname()
,就像 Java Beans 所做的那样,getFieldname()
.
可能会提出增强请求 PropertyValueFactory
以更改其在核心框架中的实现,以便它也可以识别记录访问器命名标准。
但是,我认为更新 PropertyValueFactory
以识别记录字段访问器不是一个好主意。
更好的解决方案是不更新 PropertyValueFactory 以支持记录,只允许使用此答案中概述的类型安全自定义单元格值方法。
我相信这是因为 kleopatra 在评论中提供的解释:
a custom valueFactory is definitely the way to go :) Even if it might appear attractive to some to implement an equivalent to PropertyValueFactory - but: that would be a bad idea, looking at the sheer number of questions of type "data not showing in table" due to typos ..
Java 记录是 Java15 的新功能。假设你有这样一条记录。
public record Person(String last, String first, int age)
{
public Person()
{
this("", "", 0);
}
}
这是决赛 class。它自动生成 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 需要名称为 getFirst
、getLast
和 getAge
的方法来完成它的工作。除了做这件可怕的事情,还有其他解决方法吗?
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;
}
}
解决方案
使用 lambda 单元值工厂而不是 PropertyValueFactory
。
有关两者之间差异的一些解释,请参阅:
为什么这样有效
正如您所注意到的,问题在于记录访问器不遵循标准 java bean 属性 命名约定,这正是 PropertyValueFactory
所期望的。例如,记录使用 first()
而不是 getFirst()
作为访问器,这使得它与 PropertyValueFactory
.
您是否应该应用“做这件可怕的事情”的变通方法,即向记录添加额外的获取方法,以便您可以使用 PropertyValueFactory
与 TableView
交互? -> 绝对不是,还有更好的方法:-)
需要修复的是定义您自己的自定义细胞工厂,而不是使用 PropertyValueFactory
。
最好使用 lambda(或自定义 class 用于真正复杂的单元值工厂)。使用 lambda 单元工厂具有 PropertyValueFactory
所没有的类型安全和编译时检查的优势(有关更多信息,请参阅先前引用的答案)。
定义 lambda 而非 PropertyValueFactories 的示例
记录 String
字段的 lambda 单元工厂定义的示例用法:
TableColumn<Person, String> lastColumn = new TableColumn<>("Last");
lastColumn.setCellValueFactory(
p -> new SimpleStringProperty(p.getValue().last())
);
有必要将记录字段包装在 属性 或绑定中,因为单元格值工厂实现需要一个可观察值作为输入。
您可以使用 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()
);
此处解释了将 asObject()
放在 lambda 末尾的必要性,以防您感到好奇(但这只是 java 泛型用法的一个奇怪方面 JavaFX 框架,不值得花很多时间研究,只需添加 asObject()
调用并继续 IMO):
- Why does asObject allow use of SimpleLongProperty with setCellValueProperty?
同样,如果您的记录包含其他对象(或其他记录),那么您可以为 SimpleObjectProperty<MyType>
.
注意: 这种用于 lambda 细胞工厂定义的方法和上面定义的模式也适用于标准(非记录)classes。这里没有什么特别的记录。唯一需要注意的是在 lambda 中调用 getValue()
之后注意使用正确的访问器名称。例如,使用 first()
而不是通常在 class 上定义的标准 getFirst()
调用来支持标准 Java Bean 命名模式。真正伟大的事情是,如果你定义了错误的访问器名称,你会得到一个编译器错误,并且在你尝试 运行 代码之前就知道确切的问题和位置。
示例代码
基于问题中代码的完整可执行示例。
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 是否应该为记录“固定”?
记录字段访问器遵循自己的访问命名约定,fieldname()
,就像 Java Beans 所做的那样,getFieldname()
.
可能会提出增强请求 PropertyValueFactory
以更改其在核心框架中的实现,以便它也可以识别记录访问器命名标准。
但是,我认为更新 PropertyValueFactory
以识别记录字段访问器不是一个好主意。
更好的解决方案是不更新 PropertyValueFactory 以支持记录,只允许使用此答案中概述的类型安全自定义单元格值方法。
我相信这是因为 kleopatra 在评论中提供的解释:
a custom valueFactory is definitely the way to go :) Even if it might appear attractive to some to implement an equivalent to PropertyValueFactory - but: that would be a bad idea, looking at the sheer number of questions of type "data not showing in table" due to typos ..