首页 > 解决方案 > 为什么 TableCell 的 isSelected() 方法在 JavaFX8 TableRow 中向前和向后移动时会给出不同的结果?

问题描述

InvalidationListener有一套selectedProperty()定制的TableCell。我使用它来根据数据模型中的布尔值更改行的可编辑性和背景颜色。

当我单击一个单元格然后单击同一行中的前一个单元格并将其跟踪到isSelected()设置行颜色时的值时,我正在调试行颜色的神秘消失。

从我所见,在连续前进时从 to 变化,但isSelected()false向后移动时从to变化。truetruefalse

为什么它会这样?它报告的更改不应该保持一致吗?

我尝试使用selectedProperty.get()instead ofisSelected()和 aChangeListener而不是 an InvalidationListener,但得到了相同的结果。如果我使用键盘而不是鼠标导航,也会发生同样的事情。

这是一个演示该问题的 MVCE。它基于用户 Slaw 在此处的回答How to set a TableRow's background color based on it is selected and/or a value in data model, in a JavaFX8 TableView? 和用户 kleopatra 在这里TreeTableView 的回答:设置行不可编辑

要重现该行为,请单击一行的第二个或第三个单元格。它将根据下面显示的矩阵改变颜色。然后单击同一行中的前一个单元格。行颜色应该消失并恢复为默认值。isSelected()和的值selectedProperty()将输出到控制台。

在此处输入图像描述

我正在使用 JavaFX8 (JDK1.8.0_181)、NetBeans 8.2 和 Scene Builder 8.3。

Test45_Listeners.java

package test45_listeners;

import java.util.Arrays;
import javafx.application.Application;
import static javafx.application.Application.launch;
import javafx.beans.Observable;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;

public class Test45_Listeners extends Application {

    private final ObservableList<TestModel> olTestModel 
            = FXCollections.observableArrayList(testmodel -> new Observable[] {});

    private Parent createContent() {

        //Initialise the TableView and data
        createDummyData(100);

        TableView<TestModel> table = new TableView<>();

        TableColumn<TestModel, String> colField1 = new TableColumn<>("field1");
        colField1.setCellValueFactory(features -> features.getValue().field1Property());
        colField1.setCellFactory(col -> TestTextCell.createStringTextCell(TestModel::lockedProperty));

        TableColumn<TestModel, String> colField2 = new TableColumn<>("field2");
        colField2.setCellValueFactory(features -> features.getValue().field2Property());
        colField2.setCellFactory(col -> TestTextCell.createStringTextCell(TestModel::lockedProperty));

        TableColumn<TestModel, String> colField3 = new TableColumn<>("field3");
        colField3.setCellValueFactory(features -> features.getValue().field3Property());
        colField3.setCellFactory(col -> TestTextCell.createStringTextCell(TestModel::lockedProperty));

        table.setItems(olTestModel);
        table.getColumns().addAll(Arrays.asList(colField1, colField2, colField3));
        table.setEditable(true);
        table.getSelectionModel().setCellSelectionEnabled(true);
        table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);

        //Set a row factory to set the background colour of any LOCKED row to be yellow
        table.setRowFactory(tv -> {
            TableRow<TestModel> row = new TableRow<TestModel>() {
                @Override
                public void updateItem(TestModel item, boolean empty) {
                    super.updateItem(item, empty);
                    boolean locked = false;
                    if ( getItem() != null ) {
                        locked = getItem().lockedProperty().get();
                        setEditable( ! locked );
                    }

                    if ( !isEmpty() && locked ) {
                        setStyle("-fx-background-color: yellow;");
                    } else {
                        setStyle(null);
                    }

                }
            };
            return row;
        });

        BorderPane content = new BorderPane(table);
        return content;
    }

    private void createDummyData(int count) {

        for ( int i=0; i<count; i++ ) {
            boolean locked = Math.random() >= 0.5;
            olTestModel.add(new TestModel(locked, (locked ? "row LOCKED" : "row NOT locked"),
                    Integer.toString(i), "a"+Integer.toString(i)));
        }

    }

    private class TestModel {

        private final BooleanProperty locked;
        private final StringProperty field1;
        private final StringProperty field2;
        private final StringProperty field3;

        public TestModel(

            boolean locked,
            String field1,
            String field2,
            String field3
        ) {
            this.locked = new SimpleBooleanProperty(locked);
            this.field1 = new SimpleStringProperty(field1);
            this.field2 = new SimpleStringProperty(field2);
            this.field3 = new SimpleStringProperty(field3);
        }

        public boolean getLocked() {return locked.get();}
        public void setLocked(boolean locked) {this.locked.set(locked);}
        public BooleanProperty lockedProperty() {return locked;}

        public String getField1() {return field1.get();}
        public void setField1(String field1) {this.field1.set(field1);}
        public StringProperty field1Property() {return field1;}

        public String getField2() {return field2.get();}
        public void setField2(String field2) {this.field2.set(field2);}
        public StringProperty field2Property() {return field2;}

        public String getField3() {return field3.get();}
        public void setField3(String field3) {this.field3.set(field3);}
        public StringProperty field3Property() {return field3;}

    }

    @Override
    public void start(Stage stage) throws Exception {
        stage.setScene(new Scene(createContent()));
        stage.setTitle("Test");
        stage.setWidth(350);
        stage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }

}

TestTextCell.java

package test45_listeners;

import java.util.function.Function;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.beans.value.WeakChangeListener;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.TableCell;
import javafx.scene.control.TextField;
import javafx.util.StringConverter;
import javafx.util.converter.DefaultStringConverter;

public class TestTextCell<S, T> extends TableCell<S, T> {

    public final TextField textField = new TextField();
    public final StringConverter<T> converter;

    private BooleanProperty isLockedProperty;

    private final InvalidationListener strongListener = (Observable observable) -> {
        updateStyle();
    };
    private final WeakInvalidationListener weakListener = new WeakInvalidationListener(strongListener);
/*
    public ChangeListener<Boolean> strongListener = (ObservableValue<? extends Boolean> observable, Boolean wasFocused, Boolean isNowFocused) -> {
        updateStyle();
    };
    public final WeakChangeListener<Boolean> weakListener = new WeakChangeListener<Boolean>(strongListener);
*/
    //********************************************************************************************************************* 
    public TestTextCell(StringConverter<T> converter, Function<S, BooleanProperty> methodGetLockedProperty) {

        this.converter = converter;
        setGraphic(textField);
        setContentDisplay(ContentDisplay.TEXT_ONLY);

        itemProperty().addListener((obx, oldItem, newItem) -> {
            if (newItem == null) {
                setText(null);
            } else {
                setText(converter.toString(newItem));
                if ( methodGetLockedProperty != null ) {
                    S datamodel = getTableView().getItems().get(getIndex());
                    isLockedProperty = methodGetLockedProperty.apply(datamodel);
                } else {
                    isLockedProperty = new SimpleBooleanProperty(false);
                }
            }
        });

        //Add the invalidation listener
        selectedProperty().addListener(strongListener);

    }

    //*******************************************************************************************************************    
    public static <S> TestTextCell<S, String> createStringTextCell(Function<S, BooleanProperty> methodGetLockedProperty) {
        return new TestTextCell<S, String>(new DefaultStringConverter(), methodGetLockedProperty);
    }

    //*******************************************************************************************************************    
    @Override
    protected void updateItem(T item, boolean empty) {

        T oldItem = (T) getItem();
        if (oldItem != null) {
            selectedProperty().removeListener(weakListener);
        }

        super.updateItem(item, empty);

        if (item != null) {
            selectedProperty().addListener(weakListener);

            if ( getTableRow() != null ) {
                if (getGraphic() != null) {
                    getGraphic().disableProperty().bind(
                        Bindings.not(getTableRow().editableProperty())
                    );
                }
            }
        }

    }

    @Override
    public void startEdit() {
        if ( ! isLockedProperty.get() ) {
            super.startEdit();
            if (getGraphic() != null) {
                textField.setText(converter.toString(getItem()));
                setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
                getGraphic().requestFocus();
            }
        }
    }

    @Override
    public void cancelEdit() {
        super.cancelEdit();
        setContentDisplay(ContentDisplay.TEXT_ONLY);
    }

    //*******************************************************************************************************************    
    private void updateStyle() {

        System.out.println("in updateStyle(), isLockedProperty = " + isLockedProperty.get() 
                + ", isSelected() = " + isSelected() + ", selectedProperty.get() = " + selectedProperty().get());
        if ( getTableRow() != null ) {
            if ( isLockedProperty.get() && isSelected() ) {
//            if ( isLockedProperty.get() && selectedProperty().get() ) {
                getTableRow().setStyle("-fx-background-color: pink;");
            } else if ( isLockedProperty.get() && ! isSelected()) {
//            } else if ( isLockedProperty.get() && ! selectedProperty().get() ) {
                getTableRow().setStyle("-fx-background-color: yellow;");
            } else if ( ! isLockedProperty.get() && isSelected() ) {
//            } else if ( ! isLockedProperty.get() && selectedProperty().get() ) {
                getTableRow().setStyle("-fx-background-color: #b6e1fc;");
            } else if ( ! isLockedProperty.get() && ! isSelected() ) {
//            } else if ( ! isLockedProperty.get() && ! selectedProperty().get() ) {
                getTableRow().setStyle(null);
            } else {
                throw new AssertionError("how did I get here?");
            }
        }

    }

}

标签: javafxtableviewjavafx-8listener

解决方案


除了评论中所有好的和有价值的建议外,要回答“为什么”的实际问题 :: TableCell 的 isSelected() 方法确实工作正常,并且您的代码中存在错误计算所需逻辑的问题。

为了证明这一点,我希望您将 updateStyle() 方法中的打印语句更新到下面

System.out.println(getItem() + " :: in updateStyle(), isLockedProperty = " + isLockedProperty.get() + ", isSelected() = " + isSelected());

让我们考虑第一行:

在此处输入图像描述

如果我从左到右选择单元格,输出如下:

// When field1 is selected
row LOCKED :: in updateStyle(), isLockedProperty = true, isSelected() = true
row LOCKED :: in updateStyle(), isLockedProperty = true, isSelected() = true

// When field2 is selected
row LOCKED :: in updateStyle(), isLockedProperty = true, isSelected() = false
row LOCKED :: in updateStyle(), isLockedProperty = true, isSelected() = false
0 :: in updateStyle(), isLockedProperty = true, isSelected() = true
0 :: in updateStyle(), isLockedProperty = true, isSelected() = true

// When field3 is selected
0 :: in updateStyle(), isLockedProperty = true, isSelected() = false
0 :: in updateStyle(), isLockedProperty = true, isSelected() = false
a0 :: in updateStyle(), isLockedProperty = true, isSelected() = true
a0 :: in updateStyle(), isLockedProperty = true, isSelected() = true

暂时不要担心每个选择的双重打印(因为您同时设置了强侦听器和弱侦听器)。但是从输出中我们了解到,每次我们选择一个单元格时,较早的单元格的选定值都会设置为 false,这是正确的。这非常适合您的样式更新,因为“真”总是在“假”之后。

现在尝试从右到左选择单元格,输出如下:

// When field3 is selected
a0 :: in updateStyle(), isLockedProperty = false, isSelected() = true
a0 :: in updateStyle(), isLockedProperty = false, isSelected() = true

// When field2 is selected
0 :: in updateStyle(), isLockedProperty = false, isSelected() = true
0 :: in updateStyle(), isLockedProperty = false, isSelected() = true
a0 :: in updateStyle(), isLockedProperty = false, isSelected() = false
a0 :: in updateStyle(), isLockedProperty = false, isSelected() = false

// When field1 is selected
row NOT locked :: in updateStyle(), isLockedProperty = false, isSelected() = true
row NOT locked :: in updateStyle(), isLockedProperty = false, isSelected() = true
0 :: in updateStyle(), isLockedProperty = false, isSelected() = false
0 :: in updateStyle(), isLockedProperty = false, isSelected() = false

从输出中可以清楚地看出,“真”出现在“假”之前。换句话说,JavaFX 内部可能总是从左到右按顺序更新单元格选择

这是您的代码失败的地方。当您将选择从右向左更改时,首先调用左侧单元格更新,然后调用右侧单元格更新。并且由于未选择右侧单元格,因此您永远看不到所需的选定行样式。

可能的解决方案::

我再次提到,请务必考虑评论中的所有建议。由于您打算放弃这个想法,我只想让您知道这仍然是一个可行的实现。可能还有其他更好的方法,但这是一种可能的解决方案。

从上面的分析中,很明显,依靠单元格选择来获得你想要的行为并不是一个恰当的解决方案。我建议在行工厂本身中执行所有行样式。

当然为此,您需要在表项中添加一个新的 BooleanProperty,以让您知道该行是否被选中。

请在您当前的代码中进行以下更改:

1)注释掉TestTextCell.java中的updateStyle()方法,去掉所有的监听器。

2) 在 TestModel 和适当的 getter 和 setter 中添加一个新属性。

private final BooleanProperty selected = new SimpleBooleanProperty();

3)添加一个tableView的selectedItem监听器来更新模型中的item选择。

table.getSelectionModel().selectedItemProperty().addListener((obs, oldItem, newItem) -> {
    if (oldItem != null) {
        oldItem.setSelected(false);
    }
    if (newItem != null) {
        newItem.setSelected(true);
    }
});

4)将您的行工厂实现更新为以下:

//Set a row factory to set the background colour of any LOCKED row to be yellow
table.setRowFactory(tv -> {
    TableRow<TestModel> row = new TableRow<TestModel>() {

        private final ChangeListener<Boolean> listener = (o, v, newValue) -> updateStyle();

        {
            itemProperty().addListener((obs, oldItem, newItem) -> {
                if (oldItem != null) {
                    oldItem.selectedProperty().removeListener(listener);
                }
                if (newItem != null) {
                    newItem.selectedProperty().addListener(listener);
                }
            });
        }

        @Override
        public void updateItem(TestModel item, boolean empty) {
            super.updateItem(item, empty);
            if (getItem() != null) {
                setEditable(!getItem().getLocked());
            } else {
                setEditable(false);
            }
            updateStyle();
        }

        private void updateStyle(){
            if (getItem() != null) {
                boolean isLocked = getItem().getLocked();
                boolean isSelected = getItem().isSelected();
                if (isLocked) {
                    if (isSelected) {
                        setStyle("-fx-background-color: pink;");
                    } else {
                        setStyle("-fx-background-color: yellow;");
                    }
                } else {
                    if (isSelected) {
                        setStyle("-fx-background-color: #b6e1fc;");
                    } else {
                        setStyle("-fx-background-color: transparent;");
                    }
                }
            } else {
                setStyle("-fx-background-color: transparent;");
            }
        }
    };
    return row;
});

我真的很欢迎对我的理解进行任何更正。


推荐阅读