【问题标题】:How to reserve space for the VirtualFlow scrollbars?如何为 VirtualFlow 滚动条预留空间?
【发布时间】:2021-01-20 13:23:22
【问题描述】:

VirtualFlow 的当前实现仅在视图 rect 小于控件大小时才显示滚动条。我所说的控件是指 ListView、TreeView 和任何标准的虚拟化控件。问题是垂直滚动条的出现会导致控件宽度的重新计算,即它会将单元格内容略微移动到左侧。这是明显明显且非常不舒服的运动。

我需要预先为垂直滚动条预留一些空间,但是没有一个控件提供 API 来操作 VirtualFlow 滚动条的行为,这是非常不幸的 API 设计。更不用说大多数实现都将滚动条放在组件的顶部,因此只是重叠了它的一小部分。

问题是,“实现这一目标的最佳方法是什么?”。填充无济于事,JavaFX 没有边距支持。我可以将控制(例如 ListView)放在 ScrollPane 中,但我敢打赌 VirtualFlow 在这种情况下不会继续重用单元格,所以这不是一个解决方案。

示例:

展开和折叠node2,它会移动lbRight 的内容。

public class Launcher extends Application {

    @Override
    public void start(Stage primaryStage) throws Exception {
        TreeItem<UUID> root = new TreeItem<>(UUID.randomUUID());

        TreeView<UUID> tree = new TreeView<>(root);
        tree.setCellFactory(list -> new CustomCell());

        TreeItem<UUID> node0 = new TreeItem<>(UUID.randomUUID());
        TreeItem<UUID> node1 = new TreeItem<>(UUID.randomUUID());
        TreeItem<UUID> node2 = new TreeItem<>(UUID.randomUUID());
        IntStream.range(0, 100)
                .mapToObj(index -> new TreeItem<>(UUID.randomUUID()))
                .forEach(node2.getChildren()::add);

        root.getChildren().setAll(node0, node1, node2);
        root.setExpanded(true);
        node2.setExpanded(true);

        BorderPane pane = new BorderPane();
        pane.setCenter(tree);

        Scene scene = new Scene(pane, 600, 600);
        primaryStage.setTitle("Demo");
        primaryStage.setScene(scene);
        primaryStage.setOnCloseRequest(t -> Platform.exit());
        primaryStage.show();
    }

    static class CustomCell extends TreeCell<UUID> {

        public HBox hBox;
        public Label lbLeft;
        public Label lbRight;

        public CustomCell() {
            hBox = new HBox();
            lbLeft = new Label();
            lbRight = new Label();
            lbRight.setStyle("-fx-padding: 0 20 0 0");

            Region spacer = new Region();
            HBox.setHgrow(spacer, Priority.ALWAYS);

            hBox.getChildren().setAll(lbLeft, spacer, lbRight);
        }

        @Override
        protected void updateItem(UUID uuid, boolean empty) {
            super.updateItem(uuid, empty);

            if (empty) {
                setGraphic(null);
                return;
            }

            String s = uuid.toString();
            lbLeft.setText(s.substring(0, 6));
            lbRight.setText(s.substring(6, 12));
            setGraphic(hBox);
        }
    }
}

【问题讨论】:

  • 在自定义要求未得到满足时一如既往:自行实现 :) 在这里,您需要使用自定义虚拟流的自定义皮肤。无论如何,第一步是确切地指定你想要什么,清楚地考虑约束。
  • 垂直滚动条的出现会导致控件宽度的重新计算 真的吗?举个例子来说明你的意思怎么样?
  • @kleopatra 这几乎不是定制的。这是任何其他主要 UI 工具包默认实现的唯一合理行为¯\_(ツ)_/¯ 请注意 Qt 或 GTK 小部件如何显示滚动条。它总是在顶部。至于你的建议,嗯……谢谢……但实施起来非常棘手。
  • 其他框架做什么并不重要,是吗 ;) 我的意思是 custom 就像 fx 在 not-provided 中一样 - 他们有一个像他们那样实施的理由。保持领先(因此可能隐藏单元格内容)并不是天生就更好,只是不同的 IMO。是的,我知道要更改 VirtualFlow 并不简单,我去过那里 ;) 无论如何,您仍然没有指定确切您想要什么。否则,此问题在本网站范围内无法回答。
  • .. 按原样,这听起来更像是在抱怨您认为缺少什么:提供 API 来操作 VirtualFlow 滚动条行为,这是非常不幸的 API 设计 ..好吧,也许或不......要走的路:指定你想要的api并自己实现它和/或提交一个增强问题,贡献你实现的东西:)

标签: java javafx scroll scrollbar


【解决方案1】:

反应

你不能仅仅扩展 VirtualFlow 并覆盖一个方法

如果该方法被 package/-private 访问深深地隐藏了,那肯定是正确的(但即便如此:javafx 是开源的,checkout-edit-compile-distribute 也是一个选项 :)。在这种情况下,我们可能会采用如下所述的覆盖公共 api(未经正式测试!)。

VirtualFlow 是单元格和滚动条的“布局”:特别是,它必须处理所有内容的大小/定位,而滚动条不可见。关于如何做到这一点有多种选择:

  • 调整单元格宽度以始终填充视口,当垂直滚动条隐藏/可见时增加/减少
  • 保持单元格宽度不变,以便滚动条始终留有空间,无论是否可见
  • 保持单元格宽度不变,这样滚动条就不会留下空间,将其放置在单元格顶部
  • 其他 ??

默认 VirtualFlow 实现第一个,没有切换到任何其他选项的选项。 (可能是 RFE 的候选人,请随时报告 :)。

深入研究代码会发现,单元格的最终大小是通过在布局代码末尾附近调用cell.resize(..)(如已经指出并利用in the self-answer)完成的。覆盖自定义单元格的调整大小是完全有效的,也是一个不错的选择.. 但不是唯一的,IMO。另一种方法是

  • 扩展 VirtualFlow 并覆盖 layoutChildren 以根据需要调整单元格宽度
  • 扩展 TreeViewSkin 以使用自定义流程

示例代码(需要 fx12++):

public static class XVirtualFlow<I extends IndexedCell> extends VirtualFlow<I> {

    @Override
    protected void layoutChildren() {
        super.layoutChildren();
        fitCellWidths();
    }

    /**
     * Resizes cell width to accomodate for invisible vbar.
     */
    private void fitCellWidths() {
        if (!isVertical() || getVbar().isVisible()) return;
        double width = getWidth() - getVbar().getWidth();
        for (I cell : getCells()) {
            cell.resize(width, cell.getHeight());
        }
    }
    
}

public static class XTreeViewSkin<T> extends TreeViewSkin<T>{

    public XTreeViewSkin(TreeView<T> control) {
        super(control);
    }

    @Override
    protected VirtualFlow<TreeCell<T>> createVirtualFlow() {
        return new XVirtualFlow<>();
    }
    
}

即时使用:

TreeView<UUID> tree = new TreeView<>(root) {

    @Override
    protected Skin<?> createDefaultSkin() {
        return new XTreeViewSkin<>(this);
    }
    
};

【讨论】:

  • Tnx 演示!这种方法的缺点是它调用单元格调整大小两次。此外,当您覆盖那些不应该被覆盖的功能时,您注定要手动向后移植任何进一步的性能更改。尽管 JavaFX 多年来没有得到积极开发,但它仍然是我想避免的技术债务。
【解决方案2】:

好的,这是基于@kleopatra cmets 和OpenJFX 代码探索的总结。不会有解决问题的代码,但也许它仍然会为某人腾出一些时间。

如前所述,管理虚拟化控件视口大小是VirtualFlow 的职责。所有的魔法都发生在layoutChildren()。首先是computes scrollbars visibility,然后是基于该知识的所有儿童的recalculates size。这是code 哪个causes the problem

由于所有实现细节都是私有的或包私有的,你不能只扩展VirtualFlow 并覆盖一两个方法,你必须复制粘贴并编辑整个类(删除一行,是的)。鉴于此,更改内部组件布局可能是更好的选择。

有时,我喜欢没有封装的语言。

更新:

我已经解决了这个问题。如果不调整 JavaFX 内部结构,就无法为垂直滚动条保留空间,但我们可以限制单元格宽度,因此它总是小于 TreeView(或列表视图)的宽度。这是一个简单的例子。

public class Launcher extends Application {

    public static final double SCENE_WIDTH = 500;

    @Override
    public void start(Stage primaryStage) {
        TreeItem<UUID> root = new TreeItem<>(UUID.randomUUID());

        TreeView<UUID> tree = new TreeView<>(root);
        tree.setCellFactory(list -> new CustomCell(SCENE_WIDTH));

        TreeItem<UUID> node0 = new TreeItem<>(UUID.randomUUID());
        TreeItem<UUID> node1 = new TreeItem<>(UUID.randomUUID());
        TreeItem<UUID> node2 = new TreeItem<>(UUID.randomUUID());
        IntStream.range(0, 100)
                .mapToObj(index -> new TreeItem<>(UUID.randomUUID()))
                .forEach(node2.getChildren()::add);

        root.getChildren().setAll(node0, node1, node2);
        root.setExpanded(true);
        node2.setExpanded(true);

        BorderPane pane = new BorderPane();
        pane.setCenter(tree);

        Scene scene = new Scene(pane, SCENE_WIDTH, 600);

        primaryStage.setTitle("Demo");
        primaryStage.setScene(scene);
        primaryStage.setOnCloseRequest(t -> Platform.exit());
        primaryStage.show();
    }

    static class CustomCell extends TreeCell<UUID> {

        public static final double RIGHT_PADDING = 40;

        /*
          this value depends on tree disclosure node width
          in my case it's enforced via CSS, so I always know exact
          value of this padding
        */
        public static final double INDENT_PADDING = 14;

        public HBox hBox;
        public Label lbLeft;
        public Label lbRight;

        public double maxWidth;

        public CustomCell(double maxWidth) {
            this.maxWidth = maxWidth;

            hBox = new HBox();
            lbLeft = new Label();
            lbRight = new Label();
            lbRight.setPadding(new Insets(0, RIGHT_PADDING, 0, 0));

            Region spacer = new Region();
            HBox.setHgrow(spacer, Priority.ALWAYS);

            hBox.getChildren().setAll(lbLeft, spacer, lbRight);
        }

        @Override
        protected void updateItem(UUID uuid, boolean empty) {
            super.updateItem(uuid, empty);

            if (empty) {
                setGraphic(null);
                return;
            }

            String s = uuid.toString();
            lbLeft.setText(s.substring(0, 6));
            lbRight.setText(s.substring(6, 12));

            setGraphic(hBox);
        }

        @Override
        public void resize(double width, double height) {
            // enforce item width
            double maxCellWidth = getTreeView().getWidth() - RIGHT_PADDING;
            double startLevel = getTreeView().isShowRoot() ? 0 : 1;
            double itemLevel = getTreeView().getTreeItemLevel(getTreeItem());
            if (itemLevel > startLevel) {
                maxCellWidth = maxCellWidth - ((itemLevel - startLevel) * INDENT_PADDING);
            }

            hBox.setPrefWidth(maxCellWidth);
            hBox.setMaxWidth(maxCellWidth);

            super.resize(width, height);
        }
    }
}

它远非完美,但它确实有效。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-03-24
    • 2021-10-14
    • 1970-01-01
    • 1970-01-01
    • 2014-11-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多