我有一个从 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/