【问题标题】:JavaFX: editable TableView without JavaFX-style propertiesJavaFX:没有 JavaFX 样式属性的可编辑 TableView
【发布时间】:2023-03-11 16:12:01
【问题描述】:

在阅读了许多涉及 setOnEditCommit 的可编辑 TableView 解决方案后,我今天对 Oracle 非常生气,这不是正确的做法。

以下是我在挖掘 JavaFX 源代码后发现的更好、更简单的解决方案:

【问题讨论】:

    标签: java javafx tableview


    【解决方案1】:

    我认为您的解决方案似乎比使用setOnEditCommit 复杂得多。例如(使用 Oracle 使用的常用联系表类型示例),给定一个标准的Person JavaBean:

    public class Person {
        private String firstName ;
        private String lastName ;
    
        public Person(String firstName, String lastName) {
            this.firstName = firstName;
            this.lastName = lastName;
        }
        public String getFirstName() {
            return firstName;
        }
        public void setFirstName(String firstName) {
            this.firstName = firstName;
        }
        public String getLastName() {
            return lastName;
        }
        public void setLastName(String lastName) {
            this.lastName = lastName;
        }
    
        @Override
        public String toString() {
            return firstName + " " + lastName ;
        }
    }
    

    此代码创建一个更新 Java bean 的可编辑表:

    import javafx.application.Application;
    import javafx.beans.property.ReadOnlyStringWrapper;
    import javafx.scene.Scene;
    import javafx.scene.control.Button;
    import javafx.scene.control.TableColumn;
    import javafx.scene.control.TableView;
    import javafx.scene.control.cell.TextFieldTableCell;
    import javafx.scene.layout.BorderPane;
    import javafx.scene.layout.HBox;
    import javafx.stage.Stage;
    
    
    public class Main extends Application {
        @Override
        public void start(Stage primaryStage) {
            BorderPane root = new BorderPane();
            TableView<Person> table = new TableView<>();
    
            TableColumn<Person, String> firstNameCol = new TableColumn<>("First Name");
    
            firstNameCol.setCellFactory(TextFieldTableCell.forTableColumn());
            firstNameCol.setOnEditCommit(
                    event -> event.getRowValue().setFirstName(event.getNewValue()));
            firstNameCol.setCellValueFactory(data -> new ReadOnlyStringWrapper(data.getValue().getFirstName()));
    
            TableColumn<Person, String> lastNameCol = new TableColumn<>("Last Name");
            lastNameCol.setCellFactory(TextFieldTableCell.forTableColumn());
            lastNameCol.setOnEditCommit(
                    event -> event.getRowValue().setLastName(event.getNewValue()));
            lastNameCol.setCellValueFactory(data -> new ReadOnlyStringWrapper(data.getValue().getLastName()));
    
            table.getColumns().addAll(firstNameCol, lastNameCol);
            table.setEditable(true);
    
            Button button = new Button("Show data");
            button.setOnAction(event -> table.getItems().forEach(System.out::println));
    
            HBox controls = new HBox(5, button);
    
            root.setCenter(table);
            root.setBottom(controls);
    
            table.getItems().addAll(
                    new Person("Jacob", "Smith"),
                    new Person("Isabella", "Johnson"),
                    new Person("Ethan", "Williams"),
                    new Person("Emma", "Jones"),
                    new Person("Michael", "Brown")
            );
    
            Scene scene = new Scene(root, 600, 400);
            primaryStage.setScene(scene);
            primaryStage.show();
        }
    
        public static void main(String[] args) {
            launch(args);
        }
    }
    

    此代码还避免了任何非公共 API 类。我同意构建器类更优雅一点,但它们一直是deprecated for good reason。

    此外,这也避免了反射,这与 WritablePropertyValueFactory 类(或 PropertyValueFactory API 类)不同,因此它的性能可能更好。

    【讨论】:

    • 反射可以通过在运行时创建访问器 JVM 代码来调用属性获取器和设置器来进一步自动优化以匹配本机性能。我的观点是告诉 API 如何做它自己可以发现的事情是错误的。
    • 您还可以尝试使用firstNameCol.setCellValueFactory( data -&gt; JavaBeanStringPropertyBuilder.create().bean(data.getValue()).name("firstName").build());,这也消除了“这是编辑完成后要执行的操作”代码。不过,我还没有考虑过这是否会造成内存泄漏。
    • .name() 部分闻起来像“魔术串”,尽可能避免。
    【解决方案2】:

    创建表格视图:

    BorderPaneBuilder.create()
        .top(ToolBarBuilder.create()
            .items(ButtonBuilder.create()
                .text("Add New Row")
                .onAction(e -> backendItemList.add(new MyItem("<NEW>", "<NEW>")))
                .build())
            .build())
        .center(
            TableViewBuilder.<CustomColumnNameMapping> create()
                .items(backendItemList)
                .editable(true)
                .columns(
                    TableColumnBuilder.<CustomColumnNameMapping, String> create()
                        .text("Column1 for property1")
                        .cellValueFactory(new WritablePropertyValueFactory<>("property1"))
                        .cellFactory(TextFieldTableCell.forTableColumn())
                        .editable(true)
                        .build(),
                    TableColumnBuilder.<CustomColumnNameMapping, String> create()
                        .text("Column2 for property2")
                        .cellValueFactory(new WritablePropertyValueFactory<>("property2"))
                        .cellFactory(TextFieldTableCell.forTableColumn())
                        .editable(true)
                        .build())
                .build())
        .build();
    

    WritablePropertyValueFactory.java:

    import javafx.beans.NamedArg;
    import javafx.beans.value.ObservableValue;
    import javafx.scene.control.TableColumn.CellDataFeatures;
    import javafx.scene.control.cell.PropertyValueFactory;
    import javafx.util.Callback;
    import sun.util.logging.PlatformLogger;
    import sun.util.logging.PlatformLogger.Level;
    
    import com.sun.javafx.property.PropertyReference;
    import com.sun.javafx.scene.control.Logging;
    
    // Original code from PropertyValueFactory
    // Replacing ReadOnlyObjectWrapper with new ReadableWritableObservableValue
    public class WritablePropertyValueFactory<S, T> implements Callback<CellDataFeatures<S, T>, ObservableValue<T>>
    {
        private final String property;
    
        private Class<?> columnClass;
        private String previousProperty;
        private PropertyReference<T> propertyRef;
    
        public WritablePropertyValueFactory(@NamedArg("property") String property)
        {
            this.property = property;
        }
    
        @Override
        @SuppressWarnings("unchecked")
        public ObservableValue<T> call(CellDataFeatures<S, T> param)
        {
            return getCellDataReflectively((T) param.getValue());
        }
    
        public final String getProperty()
        {
            return this.property;
        }
    
        private ObservableValue<T> getCellDataReflectively(T rowData)
        {
            if (getProperty() == null || getProperty().isEmpty() || rowData == null)
                return null;
            try
            {
                if (this.columnClass == null || this.previousProperty == null ||
                        !this.columnClass.equals(rowData.getClass()) ||
                        !this.previousProperty.equals(getProperty()))
                {
                    this.columnClass = rowData.getClass();
                    this.previousProperty = getProperty();
                    this.propertyRef = new PropertyReference<T>(rowData.getClass(), getProperty());
                }
                if (this.propertyRef.hasProperty())
                {
                    return this.propertyRef.getProperty(rowData);
                }
                else
                {
                    // Create ReadableWritableObservableValue instead of ReadOnlyObjectWrapper
                    return new ReadableWritableObservableValue<T>(
                            () -> this.propertyRef.get(rowData),
                            (value) -> this.propertyRef.set(rowData, value));
                }
            }
            catch (IllegalStateException e)
            {
                final PlatformLogger logger = Logging.getControlsLogger();
                if (logger.isLoggable(Level.WARNING))
                {
                    logger.finest("Can not retrieve property '" + getProperty() +
                            "' in PropertyValueFactory: " + this +
                            " with provided class type: " + rowData.getClass(), e);
                }
            }
            return null;
        }
    }
    

    ReadableWritableObservableValue.java

    import java.util.function.Consumer;
    import java.util.function.Supplier;
    
    import javafx.beans.InvalidationListener;
    import javafx.beans.value.ChangeListener;
    import javafx.beans.value.ObservableValue;
    import javafx.beans.value.WritableValue;
    
    public class ReadableWritableObservableValue<T> implements ObservableValue<T>, WritableValue<T>
    {
        protected final Supplier<T> getter;
        protected final Consumer<T> setter;
    
        public ReadableWritableObservableValue(Supplier<T> getter, Consumer<T> setter)
        {
            this.getter = getter;
            this.setter = setter;
        }
    
        @Override
        public void addListener(InvalidationListener listener)
        {
            // useless (no property to listen)
        }
    
        @Override
        public void removeListener(InvalidationListener listener)
        {
            // useless (no property to listen)
        }
    
        @Override
        public void addListener(ChangeListener<? super T> listener)
        {
            // useless (no property to listen)
        }
    
        @Override
        public void removeListener(ChangeListener<? super T> listener)
        {
            // useless (no property to listen)
        }
    
        @Override
        public T getValue()
        {
            return this.getter.get();
        }
    
        @Override
        public void setValue(T value)
        {
            this.setter.accept(value);
        }
    }
    

    PS:关键是从回调中返回一个WritableValue,见TableColumn#DEFAULT_EDIT_COMMIT_HANDLER。

    【讨论】:

    • 这段代码存在一些问题:您使用的是 JavaFX 8 中已弃用的类(构建器类)和不属于公共 API 的类(PropertyReference,例如例如),这很可能在未来的版本中被删除。
    • 我知道这一点,但由于后者的工具非常糟糕,他们仍然击败了 FXML。具有 WritableValue 的各种控件的行为也没有记录。但是,在最坏的情况下,它可以在运行时被普通的 JavaFX 绑定替换,调用它的代码不会发生任何变化。
    • 这似乎比setOnEditCommit 复杂得多:看我的回答。您对标准解决方案有何异议?
    • 创建必要的基础只是在开始时更加复杂。之后它会简单得多,因为您只需将列绑定到属性就可以了;该解决方案还可以使用通过全局注册表或注释指定的自定义转换器进行扩展。 标准 解决方案需要更多代码,这些代码可以而且应该通过反射和约定消除。 (并且也应该消除 cellFactory 调用,因为大多数时候可以从属性类型中检测到它)。
    • 不确定我真的买那个。简单性/复杂性有很多方面:例如,我的解决方案是编译时类型安全的。标准解决方案是为 JavaFX 风格的 bean 设计的(使用可观察的属性),并且在该用例中工作得非常干净,并且在此之外还有用于用例的机制。您的解决方案针对的是稍微不同的标准(使用 JavaBean 约定的模型),对于不适合该用例的模型(例如,表示属性、映射等的属性的列),您仍然需要相当多的代码。
    猜你喜欢
    • 1970-01-01
    • 2017-05-24
    • 1970-01-01
    • 1970-01-01
    • 2014-10-19
    • 2012-10-05
    • 2014-10-14
    • 2013-09-14
    • 2018-10-17
    相关资源
    最近更新 更多