Sai*_*dem 5 javafx javafx-8 javafx-18
最近,我将应用程序从 JavaFX 8 更新到 JavaFX 18。迁移后,我发现了一些与TreeView. 如果我理解正确的话,在场景脉冲结束时,所有节点(实际上是父节点)都将完全渲染,并且变为isNeedsLayoutfalse,直到发生下一次更改。
这在 JavaFX 8 中按预期工作。而在 JavaFX 18 中,对于某些节点,即使在脉冲完成后isNeedsLayout标志仍然存在。true这是一个错误吗?还是故意这么实施的?
在下面的演示中,我尝试TreeView在脉冲完成后打印所有节点( 的子节点)状态。我可以清楚地看到两个 JavaFX 版本之间的输出差异。
谁能告诉我,如何确保所有节点都正确渲染/布局。
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import javafx.util.Duration;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class TreeViewLayoutIssue extends Application {
int k = 1;
@Override
public void start(Stage primaryStage) throws Exception {
final TreeView<String> fxTree = new TreeView<>();
fxTree.setCellFactory(t -> new TreeCell<String>() {
Label lbl = new Label();
@Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
setText(null);
if (item != null) {
lbl.setText(item);
setGraphic(lbl);
} else {
setGraphic(null);
}
}
@Override
protected void layoutChildren() {
super.layoutChildren();
if (getItem() != null) {
System.out.println("Layouting ::> " + getItem());
}
}
});
fxTree.setShowRoot(false);
StackPane root = new StackPane(fxTree);
root.setPadding(new Insets(15));
final Scene scene = new Scene(root, 250, 250);
scene.getStylesheets().add(this.getClass().getResource("treeview.css").toExternalForm());
primaryStage.setTitle("TreeView FX18");
primaryStage.setScene(scene);
primaryStage.show();
addData(fxTree);
final Timeline timeline = new Timeline(new KeyFrame(Duration.millis(2000), e -> {
System.out.println("\nIteration #" + k++);
printNeedsLayout(fxTree);
System.out.println("-----------------------------------------------------------------------------");
}));
timeline.setCycleCount(3);
timeline.play();
}
private void printNeedsLayout(final Parent parent) {
System.out.println(" " + parent + " isNeedsLayout: " + parent.isNeedsLayout());
for (final Node n : parent.getChildrenUnmodifiable()) {
if (n instanceof Parent) {
printNeedsLayout((Parent) n);
}
}
}
private void addData(TreeView<String> fxTree) {
final TreeItem<String> rootNode = new TreeItem<>("");
fxTree.setRoot(rootNode);
final TreeItem<String> grp1Node = new TreeItem<>("Group 1");
final TreeItem<String> grp2Node = new TreeItem<>("Group 2");
rootNode.getChildren().addAll(grp1Node, grp2Node);
final TreeItem<String> subNode = new TreeItem<>("Team");
grp1Node.getChildren().addAll(subNode);
final List<TreeItem<String>> groups = Stream.of("Red", "Green", "Yellow", "Blue").map(TreeItem::new).collect(Collectors.toList());
groups.forEach(itm -> subNode.getChildren().add(itm));
grp1Node.setExpanded(true);
grp2Node.setExpanded(true);
subNode.setExpanded(true);
}
}
Run Code Online (Sandbox Code Playgroud)
树视图.css
.virtual-flow .clipped-container .sheet .tree-cell .tree-disclosure-node > .arrow {
-fx-background-color: #77797a;
}
.virtual-flow .clipped-container .sheet .tree-cell .tree-disclosure-node {
-fx-padding: 5px 6px 3px 8px; /* default is 4px 6px 4px 8px */
}
.virtual-flow .clipped-container .sheet .tree-cell:expanded > .tree-disclosure-node {
-fx-padding: 7px 6px 1px 8px; /* default is 4px 6px 4px 8px */
}
.virtual-flow .clipped-container .sheet .tree-cell:selected > .tree-disclosure-node > .arrow {
-fx-background-color: #f7f7f7;
}
Run Code Online (Sandbox Code Playgroud)
输出如下:
首次选择节点时,在 FX18 中观察到文本略有变化。而FX8则没有问题。
输出 (JavaFX 8):所有节点的 isNeedsLayout 均为 false。
TreeView@7abc5bb4[styleClass=tree-view] isNeedsLayout: false
VirtualFlow[id=virtual-flow, styleClass=virtual-flow] isNeedsLayout: false
VirtualFlow$ClippedContainer@187e8c80[styleClass=clipped-container] isNeedsLayout: false
Group@57d7468f[styleClass=sheet] isNeedsLayout: false
TreeViewLayoutIssue$1@11a5c9a9[styleClass=cell indexed-cell tree-cell]'Group 1' isNeedsLayout: false
StackPane@7c0429[styleClass=tree-disclosure-node] isNeedsLayout: false
StackPane@731322a7[styleClass=arrow] isNeedsLayout: false
TreeViewLayoutIssue$1@198e401a[styleClass=cell indexed-cell tree-cell]'Team' isNeedsLayout: false
StackPane@e2e9b3[styleClass=tree-disclosure-node] isNeedsLayout: false
StackPane@18615368[styleClass=arrow] isNeedsLayout: false
TreeViewLayoutIssue$1@1632f423[styleClass=cell indexed-cell tree-cell]'Red' isNeedsLayout: false
TreeViewLayoutIssue$1@1f706faa[styleClass=cell indexed-cell tree-cell]'Green' isNeedsLayout: false
TreeViewLayoutIssue$1@2fc143cc[styleClass=cell indexed-cell tree-cell]'Yellow' isNeedsLayout: false
TreeViewLayoutIssue$1@6cd95e88[styleClass=cell indexed-cell tree-cell]'Blue' isNeedsLayout: false
TreeViewLayoutIssue$1@1d2b9016[styleClass=cell indexed-cell tree-cell]'Group 2' isNeedsLayout: false
TreeViewLayoutIssue$1@7ed38c68[styleClass=cell indexed-cell tree-cell]'null' isNeedsLayout: false
TreeViewLayoutIssue$1@4a73e93a[styleClass=cell indexed-cell tree-cell]'null' isNeedsLayout: false
TreeViewLayoutIssue$1@2a65b6dd[styleClass=cell indexed-cell tree-cell]'null' isNeedsLayout: false
Group@2c481877 isNeedsLayout: false
TreeViewLayoutIssue$1@1621d92f[styleClass=cell indexed-cell tree-cell]'null' isNeedsLayout: false
VirtualScrollBar@9a98f02[styleClass=scroll-bar] isNeedsLayout: false
StackPane@343dbe88[styleClass=track-background] isNeedsLayout: false
ScrollBarSkin$2@4727ac31[styleClass=increment-button] isNeedsLayout: false
Region@6d0e770[styleClass=increment-arrow] isNeedsLayout: false
ScrollBarSkin$3@201819d7[styleClass=decrement-button] isNeedsLayout: false
Region@38d26811[styleClass=decrement-arrow] isNeedsLayout: false
StackPane@2e0a5131[styleClass=track] isNeedsLayout: false
ScrollBarSkin$1@771a6386[styleClass=thumb] isNeedsLayout: false
VirtualScrollBar@3177bd8a[styleClass=scroll-bar] isNeedsLayout: false
StackPane@39ae89a6[styleClass=track-background] isNeedsLayout: false
ScrollBarSkin$2@3393f75c[styleClass=increment-button] isNeedsLayout: false
Region@20002045[styleClass=increment-arrow] isNeedsLayout: false
ScrollBarSkin$3@1230eb63[styleClass=decrement-button] isNeedsLayout: false
Region@5dcf6e46[styleClass=decrement-arrow] isNeedsLayout: false
StackPane@342b0a3d[styleClass=track] isNeedsLayout: false
ScrollBarSkin$1@798546a7[styleClass=thumb] isNeedsLayout: false
StackPane@7094a28f[styleClass=corner] isNeedsLayout: false
Run Code Online (Sandbox Code Playgroud)
输出(JavaFX 18):某些节点的 isNeedsLayout 仍然为 true。
TreeView@6d663ddb[styleClass=tree-view] isNeedsLayout: true
VirtualFlow[id=virtual-flow, styleClass=virtual-flow] isNeedsLayout: false
VirtualFlow$ClippedContainer@25755334[styleClass=clipped-container] isNeedsLayout: false
Group@5ec62b29[styleClass=sheet] isNeedsLayout: true
TreeViewLayoutIssue$1@61b62840[styleClass=cell indexed-cell tree-cell]'null' isNeedsLayout: false
TreeViewLayoutIssue$1@1995039d[styleClass=cell indexed-cell tree-cell]'null' isNeedsLayout: false
TreeViewLayoutIssue$1@5401ae6f[styleClass=cell indexed-cell tree-cell]'null' isNeedsLayout: false
TreeViewLayoutIssue$1@2d51271e[styleClass=cell indexed-cell tree-cell]'Group 2' isNeedsLayout: true
TreeViewLayoutIssue$1@500b9275[styleClass=cell indexed-cell tree-cell]'Blue' isNeedsLayout: true
TreeViewLayoutIssue$1@342e3899[styleClass=cell indexed-cell tree-cell]'Yellow' isNeedsLayout: true
TreeViewLayoutIssue$1@68c87749[styleClass=cell indexed-cell tree-cell]'Green' isNeedsLayout: true
TreeViewLayoutIssue$1@18ed5cce[styleClass=cell indexed-cell tree-cell]'Red' isNeedsLayout: true
TreeViewLayoutIssue$1@2f10a091[styleClass=cell indexed-cell tree-cell]'Team' isNeedsLayout: true
StackPane@16cb362[styleClass=tree-disclosure-node] isNeedsLayout: true
StackPane@7b6a2594[styleClass=arrow] isNeedsLayout: false
TreeViewLayoutIssue$1@70c30d4[styleClass=cell indexed-cell tree-cell]'Group 1' isNeedsLayout: true
StackPane@698dbc70[styleClass=tree-disclosure-node] isNeedsLayout: true
StackPane@45cafbcd[styleClass=arrow] isNeedsLayout: false
Group@461a2cd9 isNeedsLayout: false
VirtualScrollBar@13f48448[styleClass=scroll-bar] isNeedsLayout: false
StackPane@1dea4632[styleClass=track-background] isNeedsLayout: false
ScrollBarSkin$2@6acd8e11[styleClass=increment-button] isNeedsLayout: false
Region@5ab4b98c[styleClass=increment-arrow] isNeedsLayout: false
ScrollBarSkin$3@36602364[styleClass=decrement-button] isNeedsLayout: false
Region@178b3dbd[styleClass=decrement-arrow] isNeedsLayout: false
StackPane@d25f45e[styleClass=track] isNeedsLayout: false
ScrollBarSkin$1@4231e81e[styleClass=thumb] isNeedsLayout: false
VirtualScrollBar@6662e7e5[styleClass=scroll-bar] isNeedsLayout: false
StackPane@63f40cf6[styleClass=track-background] isNeedsLayout: false
ScrollBarSkin$2@36c6ae74[styleClass=increment-button] isNeedsLayout: false
Region@261de839[styleClass=increment-arrow] isNeedsLayout: false
ScrollBarSkin$3@7f53c4b5[styleClass=decrement-button] isNeedsLayout: false
Region@6c2c3d1f[styleClass=decrement-arrow] isNeedsLayout: false
StackPane@721aca85[styleClass=track] isNeedsLayout: false
ScrollBarSkin$1@314afb8c[styleClass=thumb] isNeedsLayout: false
StackPane@5183e9a5[styleClass=corner] isNeedsLayout: false
Run Code Online (Sandbox Code Playgroud)
关于文本转变的一项观察:
如果我在显示舞台后将数据添加到treeView,则单元格的布局与在显示舞台前添加数据时的单元格布局方向相反。
案例#1:在显示舞台之前添加数据时的单元格布局:
Layouting ::> Group 1
Layouting ::> Team
Layouting ::> Red
Layouting ::> Green
Layouting ::> Yellow
Layouting ::> Blue
Layouting ::> Group 2
Run Code Online (Sandbox Code Playgroud)
案例#2:显示舞台后添加数据时的单元格布局:
Layouting ::> Group 2
Layouting ::> Blue
Layouting ::> Yellow
Layouting ::> Green
Layouting ::> Red
Layouting ::> Team
Layouting ::> Group 1
Run Code Online (Sandbox Code Playgroud)
我可以看到这是 中的另一个问题TreeView。简单解释一下这个问题:
TreeCellSkin内部维护一个静态 map( maxDisclosureWidthMap) 来跟踪TreeView.TreeCellSkin!! 中的硬编码值)。为了证明这一点,如果我更新我的 css 以使披露节点具有更大的左填充(例如50px)。在下面的 gif 中,您可以看到“团队”上方的单元格布局正确。当我选择树视图(强制布局请求)时,其他单元格使用正确的公开节点宽度并移动文本。
.virtual-flow .clipped-container .sheet .tree-cell .tree-disclosure-node {
-fx-padding: 5px 6px 3px 50px; /* default is 4px 6px 4px 8px */
}
.virtual-flow .clipped-container .sheet .tree-cell:expanded > .tree-disclosure-node {
-fx-padding: 7px 6px 1px 50px; /* default is 4px 6px 4px 8px */
}
Run Code Online (Sandbox Code Playgroud)
我的最终解决方案: [过时] [参见下面的另一个解决方案]
考虑到上述所有问题(不一致的 isNeedsLayout、文本移位等),我想出了以下解决方案。
这个想法是开始不断AnimationTimer检查是否有任何子节点isNeedsLayout为真。如果为 true,则通过调用该layout()方法强制进行布局。在树视图初始化后添加以下代码后,所有问题都得到解决。
// Code to add after initializing TreeView
new AnimationTimer() {
@Override
public void handle(final long now) {
forceLayout(fxTree);
}
}.start();
private void forceLayout(final Parent parent) {
for (final Node n : parent.getChildrenUnmodifiable()) {
if (n instanceof final Parent p) {
forceLayout(p);
}
}
if (parent.isNeedsLayout()) {
parent.layout();
}
}
Run Code Online (Sandbox Code Playgroud)
现在我的下一个最大的担心是:它会降低性能吗?
[更新]替代解决方案:
虽然@kleopatra提供的答案解决了这个问题,但打开窗口时我仍然可以看到快速的布局跳转。这是非常明显的行为,因为我们在未来的某个未知时间(Platform.runLater)请求布局。
为了消除这种影响,我需要确保在同一脉冲中纠正布局。因此,每当披露节点宽度发生变化时,我都会在脉冲结束时提出forceLayout的解决方案(我之前的解决方案) (@kleopatra的解决方案)
新的解决方案是:
以下是使用新解决方案的示例的完整工作代码(使用相同的treeview.css):
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.control.skin.TreeCellSkin;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import javafx.util.Duration;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class TreeViewLayoutIssue extends Application {
int k = 1;
/**
* Utility class to hack around JDK-8288665: broken layout of
* nested TreeCells.
*/
public class DisclosureNodeHack {
/**
* Key for max disclosure node width
*/
public static final String DISCLOSURE_NODE_WIDTH = "disclosureNodeWidth";
public static class HackedTreeCell<String> extends TreeCell<String> {
Label lbl = new Label();
@Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
setText(null);
if (item != null) {
lbl.setText((java.lang.String) item);
setGraphic(lbl);
} else {
setGraphic(null);
}
}
@Override
protected void layoutChildren() {
super.layoutChildren();
if (getItem() != null) {
System.out.println("Laid-out TreeCell ::> " + getItem());
}
}
@Override
protected Skin<?> createDefaultSkin() {
return new DisclosureNodeHack.HackedTreeCellSkin<>(this);
}
public final HackedTreeView<String> getHackedTreeView() {
return (HackedTreeView<String>) getTreeView();
}
}
/**
* Custom skin that puts the width of the disclosure node in the
* Tree's properties.
*/
public static class HackedTreeCellSkin<T> extends TreeCellSkin<T> {
HackedTreeCell<T> cell;
public HackedTreeCellSkin(HackedTreeCell<T> control) {
super(control);
cell = control;
}
@Override
protected void layoutChildren(double x, double y, double w, double h) {
super.layoutChildren(x, y, w, h);
if (getSkinnable().getTreeItem() == null || getSkinnable().getTreeView() == null) return;
Node disclosure = getSkinnable().lookup(".tree-disclosure-node");
if (disclosure instanceof Region) {
double width = ((Region) disclosure).getWidth();
Object prevWidth = getSkinnable().getTreeView().getProperties().get(DISCLOSURE_NODE_WIDTH);
getSkinnable().getTreeView().getProperties().put(DISCLOSURE_NODE_WIDTH, width);
if (prevWidth == null || ((Double) prevWidth).doubleValue() != width) {
cell.getHackedTreeView().installListener();
}
}
}
}
public static class HackedTreeView<T> extends TreeView<T> {
private Runnable listener = new Runnable() {
@Override
public void run() {
System.out.println("------ Forcing Layout ------");
forceLayout(HackedTreeView.this);
getScene().removePostLayoutPulseListener(this);
}
};
public HackedTreeView() {
setCellFactory(t -> new DisclosureNodeHack.HackedTreeCell());
}
private void forceLayout(final Parent parent) {
for (final Node n : parent.getChildrenUnmodifiable()) {
if (n instanceof final Parent p) {
forceLayout(p);
}
}
if (parent.isNeedsLayout()) {
parent.layout();
}
}
public final void installListener() {
getScene().removePostLayoutPulseListener(listener);
getScene().addPostLayoutPulseListener(listener);
}
}
private DisclosureNodeHack() {
}
}
@Override
public void start(Stage primaryStage) throws Exception {
final DisclosureNodeHack.HackedTreeView<String> fxTree = new DisclosureNodeHack.HackedTreeView<>();
fxTree.setShowRoot(false);
StackPane root = new StackPane(fxTree);
root.setPadding(new Insets(15));
final Scene scene = new Scene(root, 250, 250);
scene.getStylesheets().add(this.getClass().getResource("treeview.css").toExternalForm());
primaryStage.setTitle("TreeView FX18");
primaryStage.setScene(scene);
primaryStage.show();
addData(fxTree);
final Timeline timeline = new Timeline(new KeyFrame(Duration.millis(2000), e -> {
System.out.println("\nIteration #" + k++);
printNeedsLayout(fxTree);
System.out.println("-----------------------------------------------------------------------------");
}));
timeline.setCycleCount(1);
timeline.play();
}
private void printNeedsLayout(final Parent parent) {
System.out.println(" " + parent + " isNeedsLayout: " + parent.isNeedsLayout());
for (final Node n : parent.getChildrenUnmodifiable()) {
if (n instanceof Parent) {
printNeedsLayout((Parent) n);
}
}
}
private void addData(TreeView<String> fxTree) {
final TreeItem<String> rootNode = new TreeItem<>("");
fxTree.setRoot(rootNode);
final TreeItem<String> grp1Node = new TreeItem<>("Group 1");
final TreeItem<String> grp2Node = new TreeItem<>("Group 2");
rootNode.getChildren().addAll(grp1Node, grp2Node);
final TreeItem<String> subNode = new TreeItem<>("Team");
grp1Node.getChildren().addAll(subNode);
final List<TreeItem<String>> groups = Stream.of("Red", "Green", "Yellow", "Blue").map(TreeItem::new).collect(Collectors.toList());
groups.forEach(itm -> subNode.getChildren().add(itm));
grp1Node.setExpanded(true);
grp2Node.setExpanded(true);
subNode.setExpanded(true);
}
}
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
544 次 |
| 最近记录: |