使用 setRowFactory 设置行样式对可见行不起作用 (JavaFX 11)

Using setRowFactory to style rows doesn't work on visible rows (JavaFX 11)

我有一个从 ObservableList 更新的 TableView。它有两列。加载文件时,将填充列表并更新 table(最初仅填充第一列)。验证列表中的项目后,第二列将填充成功或失败标志。使用 setRowFactory,我将行的背景样式更新为绿色表示成功或红色表示失败。某些项目未经过验证并带有“”样式。 table 在总共几千行中大约有十几行可见。我遇到的问题是可见行在滚动出视图然后再次返回之前不会更新它们的背景样式。

我已经能够通过使用 table 的 refresh() 方法来克服这个问题,但这会导致另一个问题。第一列是 editable 以允许在重新验证之前更正数据。如果使用 refresh() 方法,那么它会破坏编辑单元格的能力。文本字段仍然出现,但已禁用(没有焦点边框,也无法突出显示或编辑其内容)。

如果我省略了 refresh() 方法编辑就可以了。包括 refresh() 和 table 无需滚动即可正确显示,但编辑已损坏。

所以我可以有 editable 单元格或正确显示的行,但不能两者都有。除了这个问题,代码工作正常。我已经阅读了无数示例和 TableView 问题以及相关的解决方案,但我尝试过的都没有解决问题。在我的努力中,我可以看到重写的 updateItem 方法仅在行再次变得可见后重新绘制时才会被调用。我的想法是我需要另一种机制来设置 validationResponse 更改中的行的样式,但这就是我卡住的地方。

所以我的问题是如何让可见的 table 行在不滚动的情况下更新它们的样式,同时不破坏单元格编辑?谢谢!!

编辑:

下面是可重现的代码示例。单击第一个按钮以使用初始数据填充 table。单击第二个按钮以模拟验证。第二列将使用验证响应进行更新,但在行滚动出视图然后返回视图之前样式不会生效。此时第一列是editable。如果您取消注释 tblGCode.refresh() 行并重新 运行 测试,样式将立即应用而不滚动,但编辑第一列中的单元格不再有效。

主要class:

public class TableViewTest extends Application {

    private final ObservableList<GCodeItem> gcodeItems = FXCollections.observableArrayList(
        item -> new Observable[]{item.validatedProperty(), item.errorDescriptionProperty()});
    private final TableView tblGCode = new TableView();

    @Override
    public void start(Stage stage) {

        TableColumn<GCodeItem, String> colGCode = new TableColumn<>("GCode");
        colGCode.setCellValueFactory(new PropertyValueFactory<>("gcode"));
        TableColumn<GCodeItem, String> colStatus = new TableColumn<>("Status");
        colStatus.setCellValueFactory(new PropertyValueFactory<>("validationResponse"));

        // Set first column to be editable
        tblGCode.setEditable(true);
        colGCode.setEditable(true);
        colGCode.setCellFactory(TextFieldTableCell.forTableColumn());
        colGCode.setOnEditCommit((TableColumn.CellEditEvent<GCodeItem, String> t) -> {
            ((GCodeItem) t.getTableView().getItems().get(t.getTablePosition().getRow())).setGcode(t.getNewValue());
        });

        // Set row factory
        tblGCode.setRowFactory(tbl -> new TableRow<GCodeItem>() {
            private final Tooltip tip = new Tooltip();
            {
                tip.setShowDelay(new Duration(250));
            }

            @Override
            protected void updateItem(GCodeItem item, boolean empty) {
                super.updateItem(item, empty);

                if(item == null || empty) {
                    setStyle("");
                    setTooltip(null);
                } else {
                    if(item.isValidated()) {
                        if(item.hasError()) {
                            setStyle("-fx-background-color: #ffcccc"); // red
                            tip.setText(item.getErrorDescription());
                            setTooltip(tip);
                        } else {
                            setStyle("-fx-background-color: #ccffdd"); // green
                            setTooltip(null);
                        }
                    } else {
                        setStyle("");                                
                        setTooltip(null);
                    }
                }
                //tblGCode.refresh(); // this works to give desired styling, but breaks editing
            }
        });

        tblGCode.getColumns().setAll(colGCode, colStatus);
        tblGCode.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);

        // buttons to simulate issue
        Button btnPopulate = new Button("1. Populate Table");
        btnPopulate.setOnAction(eh -> populateTable());
        Button btnValidate = new Button("2. Validate Table");
        btnValidate.setOnAction(eh -> simulateValidation());

        var scene = new Scene(new VBox(tblGCode, btnPopulate, btnValidate), 640, 320);
        stage.setScene(scene);
        stage.show();
    }

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

    private void populateTable() {
        // simulates updating of ObservableList with first couple of dozen lines of a file
        gcodeItems.add(new GCodeItem("(1001)"));
        gcodeItems.add(new GCodeItem("(T4  D=0.25 CR=0 - ZMIN=-0.4824 - flat end mill)"));
        gcodeItems.add(new GCodeItem("G90 G94"));
        gcodeItems.add(new GCodeItem("G17"));
        gcodeItems.add(new GCodeItem("G20"));
        gcodeItems.add(new GCodeItem("G28 G91 Z0"));
        gcodeItems.add(new GCodeItem("G90"));
        gcodeItems.add(new GCodeItem(""));
        gcodeItems.add(new GCodeItem("(Face1)"));
        gcodeItems.add(new GCodeItem("T4 M6"));
        gcodeItems.add(new GCodeItem("S5000 M3"));
        gcodeItems.add(new GCodeItem("G54"));
        gcodeItems.add(new GCodeItem("M8"));
        gcodeItems.add(new GCodeItem("G0 X1.3842 Y-1.1452"));
        gcodeItems.add(new GCodeItem("Z0.6"));
        gcodeItems.add(new GCodeItem("Z0.2"));
        gcodeItems.add(new GCodeItem("G1 Z0.015 F20"));
        gcodeItems.add(new GCodeItem("G18 G3 X1.3592 Z-0.01 I-0.025 K0"));
        gcodeItems.add(new GCodeItem("G1 X1.2492"));
        gcodeItems.add(new GCodeItem("X-1.2492 F40"));
        gcodeItems.add(new GCodeItem("X-1.25"));
        gcodeItems.add(new GCodeItem("G17 G2 X-1.25 Y-0.9178 I0 J0.1137"));
        gcodeItems.add(new GCodeItem("G1 X1.25"));
        gcodeItems.add(new GCodeItem("G3 X1.25 Y-0.6904 I0 J0.1137"));

        // Add list to table
        tblGCode.setItems(gcodeItems);
    }

    private void simulateValidation() {
        // sets validationResponse on certain rows (not every row is validated)
        gcodeItems.get(2).setValidationResponse("ok");
        gcodeItems.get(3).setValidationResponse("ok");
        gcodeItems.get(4).setValidationResponse("ok");
        gcodeItems.get(5).setValidationResponse("ok");
        gcodeItems.get(6).setValidationResponse("ok");
        gcodeItems.get(9).setValidationResponse("error:20");
        gcodeItems.get(10).setValidationResponse("ok");
        gcodeItems.get(11).setValidationResponse("ok");
        gcodeItems.get(12).setValidationResponse("ok");
        gcodeItems.get(13).setValidationResponse("ok");
        gcodeItems.get(14).setValidationResponse("ok");
        gcodeItems.get(15).setValidationResponse("ok");
        gcodeItems.get(16).setValidationResponse("ok");
        gcodeItems.get(17).setValidationResponse("ok");
        gcodeItems.get(18).setValidationResponse("ok");
        gcodeItems.get(19).setValidationResponse("ok");
        gcodeItems.get(20).setValidationResponse("ok");
        gcodeItems.get(21).setValidationResponse("ok");
        gcodeItems.get(22).setValidationResponse("ok");
        gcodeItems.get(23).setValidationResponse("ok");
    }
}

GCodeItem 型号:

public class GCodeItem {

    private final SimpleStringProperty gcode;
    private final SimpleStringProperty validationResponse;
    private ReadOnlyBooleanWrapper validated;
    private ReadOnlyBooleanWrapper hasError;
    private ReadOnlyIntegerWrapper errorNumber;
    private ReadOnlyStringWrapper errorDescription;

    public GCodeItem(String gcode) {
        this.gcode = new SimpleStringProperty(gcode);
        this.validationResponse = new SimpleStringProperty("");
        this.validated = new ReadOnlyBooleanWrapper();
        this.hasError = new ReadOnlyBooleanWrapper();
        this.errorNumber = new ReadOnlyIntegerWrapper();
        this.errorDescription = new ReadOnlyStringWrapper();

        validated.bind(Bindings.createBooleanBinding(
            () -> ! "".equals(getValidationResponse()),
            validationResponse
        ));

        hasError.bind(Bindings.createBooleanBinding(
            () -> ! ("ok".equals(getValidationResponse()) ||
                    "".equals(getValidationResponse())),
            validationResponse
        ));

        errorNumber.bind(Bindings.createIntegerBinding(
            () -> {
                String vResp = getValidationResponse();
                if ("ok".equals(vResp)) {
                    return 0;
                } else {
                    // should handle potential exceptions here...
                    if(vResp.contains(":")) {
                        int en = Integer.parseInt(vResp.split(":")[1]);
                        return en ;
                    } else {
                        return 0;
                    }
                }
            }, validationResponse
        ));

        errorDescription.bind(Bindings.createStringBinding(
            () -> {
                int en = getErrorNumber() ;
                return GrblDictionary.getErrorDescription(en);
            }, errorNumber
        ));
    }

    public final String getGcode() {
        return gcode.get();
    }
    public final void setGcode(String value) {
        gcode.set(value);
    }
    public SimpleStringProperty gcodeProperty() {
        return this.gcode;
    }

    public final String getValidationResponse() {
        return validationResponse.get();
    }
    public final void setValidationResponse(String value) {
        validationResponse.set(value);
    }
    public SimpleStringProperty validationResponseProperty() {
        return this.validationResponse;
    }

    public Boolean isValidated() {
        return validatedProperty().get();
    }
    public ReadOnlyBooleanProperty validatedProperty() {
        return validated.getReadOnlyProperty();
    }

    // ugly method name to conform to method naming pattern:
    public final boolean isHasError() {
        return hasErrorProperty().get();
    }
    // better method name:
    public final boolean hasError() {
        return isHasError();
    }
    public ReadOnlyBooleanProperty hasErrorProperty() {
        return hasError.getReadOnlyProperty();
    }

    public final int getErrorNumber() {
        return errorNumberProperty().get();
    }
    public ReadOnlyIntegerProperty errorNumberProperty() {
        return errorNumber.getReadOnlyProperty() ;
    }

    public final String getErrorDescription() {
        return errorDescriptionProperty().get();
    }
    public ReadOnlyStringProperty errorDescriptionProperty() {
        return errorDescription.getReadOnlyProperty();
    }
}

支持字典class(删节):

public class GrblDictionary {

    private static final Map<Integer, String> ERRORS = Map.ofEntries(
        entry(1, "G-code words consist of a letter and a value. Letter was not found."),
        entry(2, "Numeric value format is not valid or missing an expected value."),
        entry(17, "Laser mode requires PWM outentry."),
        entry(20, "Unsupported or invalid g-code command found in block."),
        entry(21, "More than one g-code command from same modal group found in block."),
        entry(22, "Feed rate has not yet been set or is undefined.")
    );

    public static String getErrorDescription(int errorNumber) {
        return ERRORS.containsKey(errorNumber) ? ERRORS.get(errorNumber) : "Unrecognized error number.";
    }
}

编辑#2:

如果我用 TableColumn.setCellFactory 替换 TableView.setRowFactory 代码,如下所示,我会得到想要的效果并且编辑仍然有效。这是一个明智的解决方案,还是我真的应该使用 setRowFactory 并让 setRowFactory 正确识别列表更改?在我的测试中,当行滚动查看时,似乎只调用了覆盖的 updateItem 方法。

colStatus.setCellFactory(tc -> new TableCell<GCodeItem, String>() {
    private final Tooltip tip = new Tooltip();
    {
        tip.setShowDelay(new Duration(250));
    }

    @Override
    protected void updateItem(String item, boolean empty) {
        super.updateItem(item, empty);

        TableRow<GCodeItem> row = this.getTableRow();
        GCodeItem rowItem = row.getItem();

        if(item == null || empty) {
            row.setStyle("");
            row.setTooltip(null);
        } else {
            if(rowItem.isValidated()) {
                if(rowItem.hasError()) {
                    row.setStyle("-fx-background-color: #ffcccc"); // red
                    tip.setText(rowItem.getErrorDescription());
                    row.setTooltip(tip);
                } else {
                    row.setStyle("-fx-background-color: #ccffdd"); // green
                    row.setTooltip(null);
                }
            } else {
                row.setStyle("");                                
                row.setTooltip(null);
            }
            setText(item);
        }
    }
});

编辑#3:

非常感谢 kleopatra 和 James_D 我现在有了解决方案。在行工厂中覆盖 isItemChanged() 解决了我的问题。

基本问题是当相关属性更改时,table 不会强制更新 table 行。像使用

一样使用 "extractor"
private final ObservableList<GCodeItem> gcodeItems = FXCollections.observableArrayList(
    item -> new Observable[]{item.validatedProperty(), item.errorDescriptionProperty()});

应该 工作,但似乎 table 在基础数据列表触发 updated 类型更改时不会强制行更新。 (我认为这是一个错误;JavaFX 团队可能根本不认为这是受支持的功能。)

这里的一种方法是让 TableRow 向当前项目的 validationResponseProperty()(或任何其他所需的 属性)注册一个侦听器,并在它发生变化时更新该行。这里需要稍微注意一下,因为行显示的当前项目可能会发生变化(例如,滚动时或列表中的数据发生变化时),因此您需要观察 itemProperty() 并确保侦听器已注册到属性 在正确的项目中。这看起来像:

    // Set row factory
    tblGCode.setRowFactory(tbl -> new TableRow<GCodeItem>() {

        private final Tooltip tip = new Tooltip();

        private final ChangeListener<String> listener = (obs, oldValidationResponse, newValidationResponse) -> 
            updateStyleAndTooltip();

        {
            tip.setShowDelay(new Duration(250));
            itemProperty().addListener((obs, oldItem, newItem) -> {
                if (oldItem != null) {
                    oldItem.validationResponseProperty().removeListener(listener);
                }
                if (newItem != null) {
                    newItem.validationResponseProperty().addListener(listener);
                }
                updateStyleAndTooltip();
            });
        }

        @Override
        protected void updateItem(GCodeItem item, boolean empty) {
            super.updateItem(item, empty);
            updateStyleAndTooltip();

        }

        private void updateStyleAndTooltip() {
            GCodeItem item = getItem();
            if(item == null || isEmpty()) {
                setStyle("");
                setTooltip(null);
            } else {
                if(item.isValidated()) {
                    if(item.hasError()) {
                        setStyle("-fx-background-color: #ffcccc"); // red
                        tip.setText(item.getErrorDescription());
                        setTooltip(tip);
                    } else {
                        setStyle("-fx-background-color: #ccffdd"); // green
                        setTooltip(null);
                    }
                } else {
                    setStyle("");                                
                    setTooltip(null);
                }
            }   
        }
    });

请注意,现在您不再需要使用提取器创建的列表:

private final ObservableList<GCodeItem> gcodeItems = FXCollections.observableArrayList();

事实上,如果不将依赖属性实现为 JavaFX(绑定)属性(只要它们与其他数据保持一致),这确实可行;尽管我仍然认为您当前必须使用的版本是更好的实现。

顺便说一句,如果您使用 -fx-background 而不是 -fx-background-color,您的风格会更好。默认情况下,一行的背景色 (-fx-background-color) 设置为 -fx-background。但是,text 的颜色取决于 -fx-background:如果 -fx-background 是浅色,则使用深色文本,反之亦然。默认情况下,选择一行会发生变化 -fx-background,这会导致文本颜色发生变化,因此在您的实施中,您会注意到所选(已验证或错误)行中的文本难以阅读。简而言之,修改 -fx-background 会比修改 -fx-background-color.

更好地选择。

安装条件行样式的地方是自定义 TableRow - 别无他处。与往常一样,包含的节点 - 就像这里的 tableCells - 绝不能 干扰它们父级的状态,永远不会!。

在 table 行中使用这种样式的基本问题是 row.updateItem(...) 而不是 在我们预期的时候被调用,特别是在之后属性 的更新。有两个选项可以解决(除了确保 table 在更新列中未显示的属性时通过使用 已经建议的提取器完全得到通知)

一个快速的选择是始终无条件强制更新,方法是覆盖 isItemChanged:

@Override
protected boolean isItemChanged(GCodeItem oldItem,
        GCodeItem newItem) {
    return true;
}

另一种选择是更新 updateItem(...)updateIndex(...) 中的样式(后者总是在数据中有任何机会时被调用)

@Override
protected void updateIndex(int i) {
    super.updateIndex(i);
    doUpdateItem(getItem());
}

@Override
protected void updateItem(CustomItem item, boolean empty) {
    super.updateItem(item, empty);
    doUpdateItem(item);
}

protected void doUpdateItem(CustomItem item) {
    // actually do the update and styling
}

两者之间的选择取决于上下文和要求。已经看到其中一个或另一个不能正常工作的上下文,没有明确的指示 when/why 发生了(懒得真正挖掘 ;)


除此之外 - 对问题的一些评论随着时间的推移确实有了很大改善,但仍然不完全是 [MCVE]:

  • 数据项既过于复杂(对于基本样式,不需要几个 direct/indirect 交织在一起的条件)又不够完整,无法真正证明要求(例如在编辑导致错误的值后更新条件)
  • 数据项公开属性(好东西!)- 所以使用那些(与 PropertyValueFactory 相比,坏东西!)
  • 使用 writable 属性 不需要自定义编辑提交处理程序
  • TableColumn 默认为 editable,使 col.setEditable(true) 成为空操作。如果只有一些列应该editable,其他的必须设置为false