Javafx listview disable horizontal scrolling
Asked Answered
D

1

6

I'm trying to avoid horizontal scrolling in ListView. The ListView instance holds list of HBox items, each item has a different width.

So far I'm using such a cell factory:

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);
        }
    }
}

Unfortunately it is not enough. Usually after adding several items ListView's horizontal scrollbar appears. Even if it seems to be unnecessary.

How can I assure, that ListViewCell will not exceed it's parent width and horizontal scrollbar will not appear?

Dachshund answered 1/10, 2016 at 19:47 Comment(3)
I do not want to hide the horizontal scrollbar. I want to eliminate possibility, that it will be needed.Dachshund
i simply use row.prefWidthProperty().bind(listviwe.widthProperty().divide(1.1)); row.setMaxWidth(Control.USE_PREF_SIZE);Passmore
avoid horizontal scrolling in ListView why exactly? The ListView instance holds list of HBox items that's wrong, never-ever keep Nodes as data in a virtualized control: while we often get away with it (except in ComboBox), problems are looming due to cell re-use.Kinelski
A
0

There is a lot at play here that make customizing ListView horizontal scrollbar behavior difficult to deal with. In addition to that, common misunderstandings on how ListView works can cause other problems.

The main issue to address is that the width of the ListCells will not automatically adapt when the vertical scrollbar becomes visible. Therefore, the moment it is, suddenly the contents are too wide to fit between the left edge of the ListView and the left edge of the vertical scrollbar, triggering a horizontal scrollbar. There is also the default padding of a ListCell as well as the border widths of the ListView itself to consider when determining the proper binding to set.

The following class that extends 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());
            }
        }
    }

Regarding your code, your bindings could cause problems. A ListCell's updateItem() method is not only called when the ListCell is created. A ListView can contain a pretty large list of data, so to improve the performance only the ListCells scrolled into view (and possibly a few before and after) need their graphic rendered. The updateItem() method handles this. In your code, a Region is being created over and over again and each and every one of them is being bound to the width of your ListView. Instead, the ListCell itself should be bound.

The following class extends ListCell and the method to bind the HBox is called in the constructor:

    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);
            }
        }
    }

The scrollbarThickness parameter of WidthBoundList constructor has been set to 12. The sumBorderSides parameter has been set to 2 because my WidthBoundList has a one pixel border on the right and left. The bindPrefWidth parameter has been set to true to prevent the horizontal scroller from showing at all (labels have ellipses, any non-text nodes that you might add to the hbox will simply be clipped). Set bindPrefWidth to false to allow a horizontal scrollbar, and with these proper bindings it should only show when needed. An implementation:

    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();
    }
Aristotelianism answered 12/11, 2022 at 7:22 Comment(6)
some comments: a) don't keep a reference to a looked-up node: they are under the control of the skin and might change during the life-time - actually, better not do any lookup if you can get along without: here you would implement a custom skin - which in current versions has api to access the scrollbars - to do the work b) don't create nodes in updateItem, doing so defeats the whole point of having re-usable cells at allKinelski
Good points. I've updated so that HBox creation is at the time of ListCell creation. I'm going to leave out the custom skin implementation since this question was asked over 6 years ago. It would definitely be cleaner for an application, but I think this more clearly demonstrates what the problem is and sufficiently answers this ancient question.Aristotelianism
wrong direction - data != nodeKinelski
Please clarify. "The ListView instance holds a list of HBox items" in the question.Aristotelianism
it's wrong to hold nodes as items in the list - and wrong to replicate such an error in an answer. And no need to so for solving the no-horizontal-sb problem.Kinelski
Heard and understood.Aristotelianism

© 2022 - 2024 — McMap. All rights reserved.