如何在 JavaFx 中的单元格的 updateItem 方法中获取以前的 table 单元格值?

How to get previous table cell value inside cell's updateItem method in JavaFx?

我想要一个带有自定义 graphicTableCell 在值变化时进行动画处理,其中动画类型取决于变化的性质,因此我需要知道之前的值与当前的比较。

这是您的典型自定义 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));