之前实现过一个需求:JavaFX渲染子表格。当时采用的方法是 点击某一行展开对应的子表格,这种方法同一时间只能展开一个子表格。其实最终想要的效果是在表格渲染完毕后,所有的子表格全部默认展开。
初步尝试
JavaFx
的中文资料比较少,查阅了 StackoverFlow
相关问题,仍未找到解决办法。只能自行实现,经过多番尝试,初步实现了这个效果,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| tableView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); tableView.setPrefHeight(750.0);
tableView.setRowFactory(tv -> new TableRow<Map<String, Object>>() { Node detailsPane;
{ ObservableList<Node> children = this.getChildren(); TableRow<Map<String, Object>> that = this; this.selectedProperty().addListener(new ChangeListener<Boolean>() { @Override public void changed(ObservableValue<? extends Boolean> obs, Boolean wasSelected, Boolean isNowSelected) { if (isNowSelected) { if (detailsPane != null && children != null && !children.contains(detailsPane)) { children.add(detailsPane); } that.selectedProperty().removeListener(this); } } }); }
@Override protected void updateItem(Map<String, Object> item, boolean empty) { super.updateItem(item, empty); if (!empty) { List<Map<String, Object>> subTableItems = childSensorMap.get(item.get("sensor_code").toString()); if (CollectionUtils.isNotEmpty(subTableItems)) { detailsPane = constructSubTable(FXCollections.observableArrayList(subTableItems)); Platform.runLater(() -> { tv.getSelectionModel().select(item); }); } } }
@Override protected double computePrefHeight(double width) { if (detailsPane != null) { return super.computePrefHeight(width) + detailsPane.prefHeight(60); } else { return super.computePrefHeight(width); } }
@Override protected void layoutChildren() { super.layoutChildren(); if (isSelected() && detailsPane != null) { double width = getWidth(); double paneHeight = detailsPane.prefHeight(width); detailsPane.resizeRelocate(0, getHeight() - paneHeight, width, paneHeight); } } });
|
解释一下上面的代码:仍然采用了之前的点击事件触发子表格展开的方式,在其中重写了 updateItem()
方法,在这个方法中手动的触发点击事件,也就是 tv.getSelectionModel().select(item);
这段代码。另外要注意的是子表格的行高度计算问题,有子表格的行和没有子表格的行需要分别计算。
但是,测试发现这个实现仍然存在问题:当数据较多时,表格数据不能在一页内展示完,这时滚动下拉框,会发现子表格渲染出现异常。 如下:

对代码进行 Debug
,发现是 updateItem()
引起的:每次在滚动下拉框时,都会触发这个方法,导致表格行重新渲染。
在 Turning off updateItem rendering in TableCell once scrolled or data visited 这篇讨论中,找到了一条评论:

据我所知,这些单元格在滚动 TableView 时会被重用,因此并非每个表格项都有自己的 TableCell,只有那些可见的才有。滚动时应调用 TableCell 上的 updateItem。
这就解释了上面的问题:JavaFx
单元格数据的重用机制,导致了子表格渲染异常!
进一步优化
首先尝试了废弃上面的 updateItem()
方法,将子表格的创建放在点击操作中进行,但是测试之后发现这样做存在一个问题:只能渲染当前页的子表格,超出当前页的数据将无法渲染子表格。代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| tableView.setRowFactory(tv -> new TableRow<Map<String, Object>>() { Node detailsPane;
{ ObservableList<Node> children = this.getChildren(); TableRow<Map<String, Object>> that = this; this.selectedProperty().addListener(new ChangeListener<Boolean>() { @Override public void changed(ObservableValue<? extends Boolean> obs, Boolean wasSelected, Boolean isNowSelected) { if (isNowSelected) { if (detailsPane != null && children != null && !children.contains(detailsPane)) { children.add(detailsPane); } that.selectedProperty().removeListener(this); } } }); Platform.runLater(() -> { if (getItem() != null) { List<Map<String, Object>> subTableItems = childSensorMap.get(getItem().get("sensor_code").toString()); if (CollectionUtils.isNotEmpty(subTableItems)) { detailsPane = constructSubTable(FXCollections.observableArrayList(subTableItems)); tv.getSelectionModel().select(getIndex()); } } }); }
@Override protected double computePrefHeight(double width) { if (detailsPane != null) { return super.computePrefHeight(width) + detailsPane.prefHeight(60); } else { return super.computePrefHeight(width); } }
@Override protected void layoutChildren() { super.layoutChildren(); if (isSelected() && detailsPane != null) { double width = getWidth(); double paneHeight = detailsPane.prefHeight(width); detailsPane.resizeRelocate(0, getHeight() - paneHeight, width, paneHeight); } } });
|
最终方案
第二种方法的问题没法解决,因此只能回退回第一版,在第一版的基础上继续改进,只需要解决表格行复用机制带来的问题即可。首先优化点击事件,优化点如下:
- 每次滚动都会触发
updateItem()
,在 updateItem()
方法中定义触发表格行点击事件的代码。这样每次滚动时,就会触发表格行点击事件。
- 区分当前事件时用户点击的还是滚动触发的,只处理滚动触发的。
- 维护一个全局变量Map,里面存放表格行和子表格的映射。为了避免子表格被复用,在每次点击事件触发时,从
Map
中寻找当前行对应的子表格。并判断已经渲染的子表格与寻找到的是否相等,如果相等,则无需再处理。如果不相等,则移除已渲染的子表格,并重新将 Map
中拿到的子表格渲染上去。
最终代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
| private Map<String, Map<Integer, Node>> subTableMap;
tableView.setRowFactory(tv -> new TableRow<Map<String, Object>>() { boolean isUserTrigger = true;
{ ObservableList<Node> children = this.getChildren(); this.selectedProperty().addListener((obs, wasSelected, isNowSelected) -> { if (isNowSelected && !isUserTrigger) { Node detailPane = getCurrentSubTableMap().get(getIndex()); Node node = children.get(children.size() - 1); boolean hasSubTable = false; if (node instanceof VBox) { if (node != detailPane) { children.remove(children.size() - 1); } else { hasSubTable = true; } } if (!hasSubTable && detailPane != null) { children.add(detailPane); } isUserTrigger = true; } }); }
@Override protected void updateItem(Map<String, Object> item, boolean empty) { super.updateItem(item, empty); if (!empty) { List<Map<String, Object>> subTableItems = childSensorMap.get(item.get("sensor_code").toString()); if (CollectionUtils.isNotEmpty(subTableItems) && !getCurrentSubTableMap().containsKey(getIndex())) { getCurrentSubTableMap().put(getIndex(), constructSubTable(FXCollections.observableArrayList(subTableItems))); } Platform.runLater(() -> { isUserTrigger = false; tv.getSelectionModel().clearSelection(getIndex()); tv.getSelectionModel().select(getIndex()); }); } }
@Override protected double computePrefHeight(double width) { if (getCurrentSubTableMap().containsKey(getIndex())) { return super.computePrefHeight(width) + getCurrentSubTableMap().get(getIndex()).prefHeight(60); } else { return super.computePrefHeight(width); } }
@Override protected void layoutChildren() { super.layoutChildren(); if (getCurrentSubTableMap().containsKey(getIndex())) { double width = getWidth(); double paneHeight = getCurrentSubTableMap().get(getIndex()).prefHeight(width); getCurrentSubTableMap().get(getIndex()).resizeRelocate(0, getHeight() - paneHeight, width, paneHeight); } } });
private Map<Integer, Node> getCurrentSubTableMap() { return subTableMap.get(currentProductModel + 'A' + proModelStoveMap.get(currentProductModel).getId()); }
|
参考文档
Turning off updateItem rendering in TableCell once scrolled or data visited