javafx - 使用 setRowFactory 设置行样式不适用于可见行 (JavaFX 11)

标签 javafx tableview

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

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

如果我省略刷新()方法编辑工作就很好。包含刷新()并且表格可以正确显示而无需滚动,但编辑被破坏。

所以我可以拥有可编辑的单元格或正确显示的行,但不能同时拥有两者。除了这个问题之外,代码工作正常。我读过无数的示例和 TableView 问题以及相关的解决方案,但我尝试过的任何方法都无法解决问题。在我的努力中,我可以看到只有当行再次变得可见后重新绘制时才会调用重写的 updateItem 方法。我的想法是,我需要另一种机制来设置validationResponse更改上的行样式,但这就是我陷入困境的地方。

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

编辑:

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

主类:

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();
    }
}

支持字典类(删节):

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() 解决了我的问题。

最佳答案

安装条件行样式的位置是自定义 TableRow - 没有其他地方。与往常一样,包含的节点(例如此处的 tableCell)绝不能干扰其父级的状态,永远!

tableRow 中此类样式的基本问题是,row.updateItem(...) 在我们期望的时候没有被调用,特别是在之后属性的更新。有两个选项可以解决(除了确保通过使用提取器来通知表有关列中未显示的属性的更新,如James已经建议的那样)

一个快速的选择是通过覆盖 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
}

两者之间的选择取决于上下文和要求。见过其中一个或另一个无法正常工作的上下文,但没有明确指示何时/为何发生这种情况(太懒了,无法真正挖掘;)


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

  • 数据项既过于复杂(对于基本样式,不需要多个直接/间接交织的条件),又不够完整,无法真正演示需求(例如在编辑驱动错误条件的值后进行更新)<
  • 数据项公开属性(好事!) - 因此请使用这些属性(相对于 PropertyValueFactory,坏事!)
  • 使用可写属性,不需要自定义编辑提交处理程序
  • TableColumn 默认情况下是可编辑的,这使得 col.setEditable(true) 成为无操作。如果只有某些列可编辑,则其他列必须设置为 false

关于javafx - 使用 setRowFactory 设置行样式不适用于可见行 (JavaFX 11),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/60596504/

相关文章:

java - 如何将 JavaFX 节点导出为 SVG 图像?

ios - 自定义 UITableViewCell 未正确加载

ios - 如何通过快速点击 Collection View 的单元格来重新加载 TableView 中的数据

java - 绑定(bind)到标签时格式化整数

JavaFX Canvas : fill closed path composed of multiple geometries

java - 如何通过 CSS 在 JavaFX ContextMenu 中设置加速器显示文本的样式?

java - 与 ObservableMap 绑定(bind)的组合

ios - 如何将类型 'UIImage?' 的值分配给类型 'UIView?'

JavaFX TableView 未立即更新

ios - TableView 不使用 ReloadData() 进行更新