JavaFX ListView禁用水平滚动

6
我将尝试在ListView中避免水平滚动。ListView实例保存了HBox项目列表,每个项目的宽度不同。
目前,我正在使用这样的单元格工厂:
public class ListViewCell extends ListCell<Data>
{
    @Override
    public void updateItem(Data data, boolean empty)
    {
        super.updateItem(data, empty);
        if(empty || data == null){
            setGraphic(null);
            setText(null);
        }
        if(data != null)
        {
            Region region = createRow(data);
            region.prefWidthProperty().bind(mListView.widthProperty().subtract(20));
            region.maxWidthProperty().bind(mListView.widthProperty().subtract(20));
            setGraphic(region);
        }
    }
}

很遗憾,这还不够。通常在添加多个项目后,ListView会出现水平滚动条。即使看起来是不必要的。

我该如何保证ListViewCell不超过其父宽度,且水平滚动条不会出现?


5
我不想隐藏水平滚动条。我想消除需要使用它的可能性。 - Dejwi
1
我只是简单地使用了以下代码:row.prefWidthProperty().bind(listviwe.widthProperty().divide(1.1)); row.setMaxWidth(Control.USE_PREF_SIZE); - Door D Uthpala
避免ListView中的水平滚动,为什么呢?ListView实例保存了HBox项目列表,这是错误的。在虚拟化控件中永远不要将节点作为数据保留:尽管我们经常可以摆脱它(除了ComboBox),但由于单元格重用,问题正在逐渐浮现。 - kleopatra
1个回答

0

这里有很多因素参与进来,使得自定义ListView水平滚动条行为变得难以处理。除此之外,对于ListView如何工作的常见误解也会导致其他问题。

需要解决的主要问题是:当垂直滚动条出现时,ListCells的宽度不会自动适应。因此,一旦出现垂直滚动条,内容就会突然变得太宽,无法适应在ListView的左边缘和垂直滚动条的左边缘之间,从而触发水平滚动条。还需考虑ListCell的默认填充以及ListView本身的边框宽度,以确定正确的绑定设置。

下面是扩展ListView的类:

    public class WidthBoundList extends ListView {
        private final BooleanProperty vbarVisibleProperty = new SimpleBooleanProperty(false);
        private final boolean bindPrefWidth;
        private final double scrollbarThickness;
        private final double sumBorderSides;
        
        public WidthBoundList(double scrollbarThickness, double sumBorderSides, boolean bindPrefWidth) {
            this.scrollbarThickness = scrollbarThickness;
            this.sumBorderSides = sumBorderSides;
            this.bindPrefWidth = bindPrefWidth;
            Platform.runLater(()->{
                findScroller();
            });
        }
        
        private void findScroller() {
            if (!this.getChildren().isEmpty()) {
                VirtualFlow flow = (VirtualFlow)this.getChildren().get(0);
                if (flow != null) {
                    List<Node> flowChildren = flow.getChildrenUnmodifiable();
                    int len = flowChildren .size();
                    for (int i = 0; i < len; i++) {
                        Node n = flowChildren .get(i);
                        if (n.getClass().equals(VirtualScrollBar.class)) {
                            final ScrollBar bar = (ScrollBar) n;
                            if (bar.getOrientation().equals(Orientation.VERTICAL)) {
                                vbarVisibleProperty.bind(bar.visibleProperty());
                                bar.setPrefWidth(scrollbarThickness);
                                bar.setMinWidth(scrollbarThickness);
                                bar.setMaxWidth(scrollbarThickness);
                            } else if (bar.getOrientation().equals(Orientation.HORIZONTAL)) {
                                bar.setPrefHeight(scrollbarThickness);
                                bar.setMinHeight(scrollbarThickness);
                                bar.setMaxHeight(scrollbarThickness);
                            }
                        }
                    }
                } else {
                    Platform.runLater(()->{
                        findScroller();
                    });
                }
            } else {
                Platform.runLater(()->{
                    findScroller();
                });
            }
        }
        
        public void bindWidthScrollCondition(Region node) {
            node.maxWidthProperty().unbind();
            node.prefWidthProperty().unbind();
            node.maxWidthProperty().bind(
                Bindings.when(vbarVisibleProperty)
                        .then(this.widthProperty().subtract(scrollbarThickness).subtract(sumBorderSides))
                        .otherwise(this.widthProperty().subtract(sumBorderSides))
            );
            if (bindPrefWidth) { 
                node.prefWidthProperty().bind(node.maxWidthProperty());
            }
        }
    }

关于您的代码,您的绑定可能会引起问题。 ListCell的updateItem()方法不仅在创建ListCell时调用。 ListView可以包含相当大的数据列表,因此为了提高性能,只有滚动到视图中的ListCells(以及可能在其前后的一些)需要渲染其图形。 updateItem()方法处理此操作。 在您的代码中,Region一遍又一遍地被创建,并且每个Region都被绑定到ListView的宽度上。 相反,应该绑定ListCell本身。
以下类扩展了ListCell,并在构造函数中调用了绑定HBox的方法:
    public class BoundListCell extends ListCell<String> {
        private final HBox  hbox;
        private final Label label;
        
        public BoundListCell(WidthBoundList widthBoundList) {
            this.setPadding(Insets.EMPTY);
            hbox = new HBox();
            label = new Label();
            hbox.setPadding(new Insets(2, 4, 2, 4));
            hbox.getChildren().add(label);
            widthBoundList.bindWidthScrollCondition(this);
        }
        
        @Override
        public void updateItem(String data, boolean empty) {
            super.updateItem(data, empty);
            if (empty || data == null) {
                label.setText("");
                setGraphic(null);
                setText(null);
            } else {
                label.setText(data);
                setGraphic(hbox);
            }
        }
    }

WidthBoundList构造函数的scrollbarThickness参数已设置为12。由于我的WidthBoundList在右侧和左侧有一个像素的边框,因此将sumBorderSides参数设置为2。将bindPrefWidth参数设置为true可防止水平滚动条完全显示(标签具有省略号,您可能添加到hbox中的任何非文本节点都将被简单地剪切)。将bindPrefWidth设置为false以允许水平滚动条,并且使用这些适当的绑定应仅在需要时显示。实现如下:

    private final WidthBoundList myListView = new WidthBoundList(12, 2, true);
    
    public static void main(final String... a) {
        Application.launch(a);
    }
    
    @Override
    public void start(final Stage primaryStage) throws Exception {
        myListView.setCellFactory(c -> new BoundListCell(myListView));
        
        VBox vBox = new VBox();
        vBox.setFillWidth(true);
        vBox.setAlignment(Pos.CENTER);
        vBox.setSpacing(5);

        Button button = new Button("APPEND");
        button.setOnAction((e)->{
            myListView.getItems().add("THIS IS LIST ITEM NUMBER " + myListView.getItems().size());
        });
        
        vBox.getChildren().addAll(myListView, button);
        
        myListView.maxWidthProperty().bind(vBox.widthProperty().subtract(20));
        myListView.prefHeightProperty().bind(vBox.heightProperty().subtract(20));
        
        primaryStage.setScene(new Scene(vBox, 200, 400));
        primaryStage.show();
    }

一些注释:a)不要保留对查找节点的引用:它们受皮肤控制,可能在生命周期内发生更改 - 实际上,最好不要进行任何查找,如果可以不使用就不要使用:在这里,您将实现自定义皮肤 - 在当前版本中具有访问滚动条的API - 来完成工作b)不要在updateItem中创建节点,这样做会破坏拥有可重复使用单元格的整个目的 - kleopatra
好的观点。我已经更新了,使HBox的创建在ListCell创建时进行。我将省略自定义皮肤实现,因为这个问题是6年前提出的。这对于应用程序来说肯定更清洁,但我认为这更清楚地展示了问题,并足以回答这个古老的问题。 - AtomicUs5000
错误方向 - 数据!= 节点 - kleopatra
请澄清问题中的“ListView实例保存了HBox项目列表”的含义。 - AtomicUs5000
把节点作为列表中的项目是错误的,也是在回答中复制这样的错误。没有必要如此解决“无水平滚动条”的问题。 - kleopatra
知道了,明白了。 - AtomicUs5000

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接