如何在 JavaFx 中的单元格的 updateItem 方法中获取以前的 table 单元格值?
How to get previous table cell value inside cell's updateItem method in JavaFx?
我想要一个带有自定义 graphic
的 TableCell
在值变化时进行动画处理,其中动画类型取决于变化的性质,因此我需要知道之前的值与当前的比较。
这是您的典型自定义 table 单元格(Kotlin 代码):
class MyTableCell<S, T> : TableCell<S, T>() {
override fun updateItem(item: T?, empty: Boolean) {
if (empty || field == null) {
text = null
graphic = null
} else {
// need to get the old value here
}
}
我看到 javafx/scene/control/TableCell.java
中的 super 方法确实知道旧值并使用它与当前值进行比较,但重写仅获取 newValue
:
private void updateItem(int oldIndex) {
...
final T oldValue = getItem();
...
final T newValue = currentObservableValue == null ? null : currentObservableValue.getValue();
...
if (oldIndex == index) {
if (!isItemChanged(oldValue, newValue)) {
...
}
...
}
...
updateItem(newValue, false); // sadly, `oldValue` is not passed
我只能想到一个丑陋的解决方法,所以我想知道是否有一些惯用的方法来获取旧的单元格值?
这是一个示例应用程序:
import javafx.application.Application
import javafx.beans.property.SimpleDoubleProperty
import javafx.collections.FXCollections
import javafx.scene.Scene
import javafx.scene.control.*
import javafx.scene.control.cell.PropertyValueFactory
import javafx.stage.Stage
import kotlinx.coroutines.experimental.delay
import kotlinx.coroutines.experimental.launch
import tornadofx.*
class Foo {
val barProperty = SimpleDoubleProperty()
var bar: Double
get() = barProperty.get()
set(value) = barProperty.set(value)
}
class FooApp: Application() {
override fun start(primaryStage: Stage) {
val foos = FXCollections.observableArrayList(
Foo().apply { bar = 42.0 }
)
val table = TableView<Foo>(foos)
val barColumn = TableColumn<Foo, Double>("Bar")
barColumn.cellValueFactory = PropertyValueFactory<Foo, Double>("bar")
barColumn.setCellFactory {
FooTableCell<Foo, Double> { "%.2f".format(it) }
}
table.columns.add(barColumn)
val scene = Scene(table, 400.0, 200.0)
primaryStage.scene = scene
primaryStage.title = "Table Cell"
primaryStage.show()
launch {
while (isActive) {
delay(500)
val oldFoo = foos[0]
// Replacing the old Foo instance with a new one,
// updating the value of the `bar` field:
foos[0] = Foo().apply {
bar = oldFoo.bar - 1.0 + Math.random() * 2.0
}
// because a change to a field cannot be detected by an observable list
// and so does not propagates to the table. This won't result in
// a visible change:
// foos[0].bar = foos[0].bar - 1.0 + Math.random() * 2.0
}
}
}
}
class FooTableCell<S, T>(private val format: (T) -> String) : TableCell<S, T>() {
init {
contentDisplay = ContentDisplay.GRAPHIC_ONLY
itemProperty().addListener(ChangeListener { obs, oldItem, newItem ->
if (newItem != null && oldItem != null && newItem != oldItem) {
// This is never true.
println("!!! Old: $oldItem, New: $newItem")
} else {
println("Change listener:\nOld: $oldItem, New: $newItem\n")
}
})
}
override fun updateItem(item: T?, empty: Boolean) {
val oldItem = this.item
super.updateItem(item, empty)
if (item != null && oldItem != null && item != oldItem) {
// This is never true.
println("!!! Old: $oldItem, New: $item")
} else {
println("updateItem:\nOld: $oldItem, New: $item\n")
}
if (empty || item == null) {
graphic = null
text = null
} else if (tableRow != null) {
val cell = this
graphic = Label().apply {
textProperty().bindBidirectional(cell.textProperty())
}
text = format(item)
}
}
}
fun main(args: Array<String>) {
Application.launch(FooApp::class.java, *args)
}
item
属性的实际值被updateItem()
方法的默认实现改变了,所以在调用默认实现之前获取值即可:
public class MyTableCell<S,T> extends TableCell<S,T> {
@Override
protected void updateItem(T item, boolean empty) {
T oldItem = getItem();
super.updateItem(item, empty) ;
// ...
}
}
或者,您可以只使用 itemProperty()
:
注册一个更改侦听器
public class MyTableCell<S,T> extends TableCell<S,T> {
public MyTableCell() {
itemProperty().addListener((obs, oldItem, newItem) -> {
// do animation here...
});
}
@Override
protected void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
// other functionality here...
}
}
这是演示这两种技术的 SSCCE:
import java.util.Random;
import java.util.function.Function;
import javafx.application.Application;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
public class TableCellWithChange extends Application {
public static class ChangeAwareCell<S,T> extends TableCell<S,T> {
public ChangeAwareCell() {
itemProperty().addListener((obs, oldItem, newItem) -> {
System.out.printf("In listener, value for %s changed from %s to %s%n", getTableRow().getItem(), oldItem, newItem);
});
}
@Override
protected void updateItem(T item, boolean empty) {
T oldItem = getItem();
super.updateItem(item, empty);
if (empty) {
setText(null);
} else {
setText(item.toString());
System.out.printf("Change in %s from %s to %s %n", getTableView().getItems().get(getIndex()), oldItem, item);
}
}
}
@Override
public void start(Stage primaryStage) {
TableView<Item> table = new TableView<>();
TableColumn<Item, String> itemCol = column("Item", Item::nameProperty);
table.getColumns().add(itemCol);
TableColumn<Item, Number> valueCol = column("Value", Item:: valueProperty);
table.getColumns().add(valueCol);
valueCol.setCellFactory(tc -> new ChangeAwareCell<>());
TableColumn<Item, Void> changeCol = new TableColumn<>();
changeCol.setCellFactory(tc -> new TableCell<>() {
private Button incButton = new Button("^");
private Button decButton = new Button("v");
private HBox graphic = new HBox(2, incButton, decButton);
{
incButton.setOnAction(e -> {
Item item = getTableRow().getItem();
item.setValue(item.getValue()+1);
});
decButton.setOnAction(e -> {
Item item = getTableRow().getItem();
item.setValue(item.getValue()-1);
});
}
@Override
protected void updateItem(Void item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setGraphic(null);
} else {
setGraphic(graphic);
}
}
});
table.getColumns().add(changeCol);
Random rng = new Random();
for (int i = 1 ; i <= 20 ; i++) {
table.getItems().add(new Item("Item "+i, rng.nextInt(100)));
}
Scene scene = new Scene(table);
primaryStage.setScene(scene);
primaryStage.show();
}
private <S,T> TableColumn<S,T> column(String text, Function<S, ObservableValue<T>> property) {
TableColumn<S,T> col = new TableColumn<>(text);
col.setCellValueFactory(cellData -> property.apply(cellData.getValue()));
col.setPrefWidth(150);
return col ;
}
public static class Item {
private final StringProperty name = new SimpleStringProperty();
private final IntegerProperty value = new SimpleIntegerProperty();
public Item(String name, int value) {
setName(name);
setValue(value);
}
@Override
public String toString() {
return getName();
}
public final StringProperty nameProperty() {
return this.name;
}
public final String getName() {
return this.nameProperty().get();
}
public final void setName(final String name) {
this.nameProperty().set(name);
}
public final IntegerProperty valueProperty() {
return this.value;
}
public final int getValue() {
return this.valueProperty().get();
}
public final void setValue(final int value) {
this.valueProperty().set(value);
}
}
public static void main(String[] args) {
launch(args);
}
}
当然,这些项目也会发生变化,例如当用户在 table 周围滚动时,或者单元格以其他方式重复使用时;所以这可能不是你想要的。您可能希望改为向模型中的适当 属性 添加一个侦听器。执行此操作的最简单方法可能是在单元格中存储对来自模型的实际 属性 的引用,并在更新单元格时更新该引用:
public static class ChangeAwareCell<S,T> extends TableCell<S,T> {
private Function<S, ObservableValue<T>> property ;
private ObservableValue<T> lastObservableValue ;
private ChangeListener<T> listener = (obs, oldValue, newValue) -> valueChanged(oldValue, newValue);
public ChangeAwareCell(Function<S, ObservableValue<T>> property) {
this.property = property ;
}
private void valueChanged(T oldValue, T newValue) {
System.out.printf("Value changed from %s to %s %n", oldValue, newValue);
}
@Override
protected void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if (lastObservableValue != null) {
lastObservableValue.removeListener(listener);
}
if (empty) {
setText(null);
} else {
lastObservableValue = property.apply(getTableRow().getItem());
lastObservableValue.addListener(listener);
setText(item.toString());
}
}
}
当然还有相应的改动:
valueCol.setCellFactory(tc -> new ChangeAwareCell<>(Item::valueProperty));
我想要一个带有自定义 graphic
的 TableCell
在值变化时进行动画处理,其中动画类型取决于变化的性质,因此我需要知道之前的值与当前的比较。
这是您的典型自定义 table 单元格(Kotlin 代码):
class MyTableCell<S, T> : TableCell<S, T>() {
override fun updateItem(item: T?, empty: Boolean) {
if (empty || field == null) {
text = null
graphic = null
} else {
// need to get the old value here
}
}
我看到 javafx/scene/control/TableCell.java
中的 super 方法确实知道旧值并使用它与当前值进行比较,但重写仅获取 newValue
:
private void updateItem(int oldIndex) {
...
final T oldValue = getItem();
...
final T newValue = currentObservableValue == null ? null : currentObservableValue.getValue();
...
if (oldIndex == index) {
if (!isItemChanged(oldValue, newValue)) {
...
}
...
}
...
updateItem(newValue, false); // sadly, `oldValue` is not passed
我只能想到一个丑陋的解决方法,所以我想知道是否有一些惯用的方法来获取旧的单元格值?
这是一个示例应用程序:
import javafx.application.Application
import javafx.beans.property.SimpleDoubleProperty
import javafx.collections.FXCollections
import javafx.scene.Scene
import javafx.scene.control.*
import javafx.scene.control.cell.PropertyValueFactory
import javafx.stage.Stage
import kotlinx.coroutines.experimental.delay
import kotlinx.coroutines.experimental.launch
import tornadofx.*
class Foo {
val barProperty = SimpleDoubleProperty()
var bar: Double
get() = barProperty.get()
set(value) = barProperty.set(value)
}
class FooApp: Application() {
override fun start(primaryStage: Stage) {
val foos = FXCollections.observableArrayList(
Foo().apply { bar = 42.0 }
)
val table = TableView<Foo>(foos)
val barColumn = TableColumn<Foo, Double>("Bar")
barColumn.cellValueFactory = PropertyValueFactory<Foo, Double>("bar")
barColumn.setCellFactory {
FooTableCell<Foo, Double> { "%.2f".format(it) }
}
table.columns.add(barColumn)
val scene = Scene(table, 400.0, 200.0)
primaryStage.scene = scene
primaryStage.title = "Table Cell"
primaryStage.show()
launch {
while (isActive) {
delay(500)
val oldFoo = foos[0]
// Replacing the old Foo instance with a new one,
// updating the value of the `bar` field:
foos[0] = Foo().apply {
bar = oldFoo.bar - 1.0 + Math.random() * 2.0
}
// because a change to a field cannot be detected by an observable list
// and so does not propagates to the table. This won't result in
// a visible change:
// foos[0].bar = foos[0].bar - 1.0 + Math.random() * 2.0
}
}
}
}
class FooTableCell<S, T>(private val format: (T) -> String) : TableCell<S, T>() {
init {
contentDisplay = ContentDisplay.GRAPHIC_ONLY
itemProperty().addListener(ChangeListener { obs, oldItem, newItem ->
if (newItem != null && oldItem != null && newItem != oldItem) {
// This is never true.
println("!!! Old: $oldItem, New: $newItem")
} else {
println("Change listener:\nOld: $oldItem, New: $newItem\n")
}
})
}
override fun updateItem(item: T?, empty: Boolean) {
val oldItem = this.item
super.updateItem(item, empty)
if (item != null && oldItem != null && item != oldItem) {
// This is never true.
println("!!! Old: $oldItem, New: $item")
} else {
println("updateItem:\nOld: $oldItem, New: $item\n")
}
if (empty || item == null) {
graphic = null
text = null
} else if (tableRow != null) {
val cell = this
graphic = Label().apply {
textProperty().bindBidirectional(cell.textProperty())
}
text = format(item)
}
}
}
fun main(args: Array<String>) {
Application.launch(FooApp::class.java, *args)
}
item
属性的实际值被updateItem()
方法的默认实现改变了,所以在调用默认实现之前获取值即可:
public class MyTableCell<S,T> extends TableCell<S,T> {
@Override
protected void updateItem(T item, boolean empty) {
T oldItem = getItem();
super.updateItem(item, empty) ;
// ...
}
}
或者,您可以只使用 itemProperty()
:
public class MyTableCell<S,T> extends TableCell<S,T> {
public MyTableCell() {
itemProperty().addListener((obs, oldItem, newItem) -> {
// do animation here...
});
}
@Override
protected void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
// other functionality here...
}
}
这是演示这两种技术的 SSCCE:
import java.util.Random;
import java.util.function.Function;
import javafx.application.Application;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
public class TableCellWithChange extends Application {
public static class ChangeAwareCell<S,T> extends TableCell<S,T> {
public ChangeAwareCell() {
itemProperty().addListener((obs, oldItem, newItem) -> {
System.out.printf("In listener, value for %s changed from %s to %s%n", getTableRow().getItem(), oldItem, newItem);
});
}
@Override
protected void updateItem(T item, boolean empty) {
T oldItem = getItem();
super.updateItem(item, empty);
if (empty) {
setText(null);
} else {
setText(item.toString());
System.out.printf("Change in %s from %s to %s %n", getTableView().getItems().get(getIndex()), oldItem, item);
}
}
}
@Override
public void start(Stage primaryStage) {
TableView<Item> table = new TableView<>();
TableColumn<Item, String> itemCol = column("Item", Item::nameProperty);
table.getColumns().add(itemCol);
TableColumn<Item, Number> valueCol = column("Value", Item:: valueProperty);
table.getColumns().add(valueCol);
valueCol.setCellFactory(tc -> new ChangeAwareCell<>());
TableColumn<Item, Void> changeCol = new TableColumn<>();
changeCol.setCellFactory(tc -> new TableCell<>() {
private Button incButton = new Button("^");
private Button decButton = new Button("v");
private HBox graphic = new HBox(2, incButton, decButton);
{
incButton.setOnAction(e -> {
Item item = getTableRow().getItem();
item.setValue(item.getValue()+1);
});
decButton.setOnAction(e -> {
Item item = getTableRow().getItem();
item.setValue(item.getValue()-1);
});
}
@Override
protected void updateItem(Void item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setGraphic(null);
} else {
setGraphic(graphic);
}
}
});
table.getColumns().add(changeCol);
Random rng = new Random();
for (int i = 1 ; i <= 20 ; i++) {
table.getItems().add(new Item("Item "+i, rng.nextInt(100)));
}
Scene scene = new Scene(table);
primaryStage.setScene(scene);
primaryStage.show();
}
private <S,T> TableColumn<S,T> column(String text, Function<S, ObservableValue<T>> property) {
TableColumn<S,T> col = new TableColumn<>(text);
col.setCellValueFactory(cellData -> property.apply(cellData.getValue()));
col.setPrefWidth(150);
return col ;
}
public static class Item {
private final StringProperty name = new SimpleStringProperty();
private final IntegerProperty value = new SimpleIntegerProperty();
public Item(String name, int value) {
setName(name);
setValue(value);
}
@Override
public String toString() {
return getName();
}
public final StringProperty nameProperty() {
return this.name;
}
public final String getName() {
return this.nameProperty().get();
}
public final void setName(final String name) {
this.nameProperty().set(name);
}
public final IntegerProperty valueProperty() {
return this.value;
}
public final int getValue() {
return this.valueProperty().get();
}
public final void setValue(final int value) {
this.valueProperty().set(value);
}
}
public static void main(String[] args) {
launch(args);
}
}
当然,这些项目也会发生变化,例如当用户在 table 周围滚动时,或者单元格以其他方式重复使用时;所以这可能不是你想要的。您可能希望改为向模型中的适当 属性 添加一个侦听器。执行此操作的最简单方法可能是在单元格中存储对来自模型的实际 属性 的引用,并在更新单元格时更新该引用:
public static class ChangeAwareCell<S,T> extends TableCell<S,T> {
private Function<S, ObservableValue<T>> property ;
private ObservableValue<T> lastObservableValue ;
private ChangeListener<T> listener = (obs, oldValue, newValue) -> valueChanged(oldValue, newValue);
public ChangeAwareCell(Function<S, ObservableValue<T>> property) {
this.property = property ;
}
private void valueChanged(T oldValue, T newValue) {
System.out.printf("Value changed from %s to %s %n", oldValue, newValue);
}
@Override
protected void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if (lastObservableValue != null) {
lastObservableValue.removeListener(listener);
}
if (empty) {
setText(null);
} else {
lastObservableValue = property.apply(getTableRow().getItem());
lastObservableValue.addListener(listener);
setText(item.toString());
}
}
}
当然还有相应的改动:
valueCol.setCellFactory(tc -> new ChangeAwareCell<>(Item::valueProperty));