Sai Dandem
Sai Dandem

Reputation: 9989

Horizontal ScrollBar is visible in TableView with constrained resize policy

The horizontal ScrollBar of the TableView(with constrained resize policy) keeps flashing when the TableView is resized(shrinking). I believe this is a long lasting issue as I can find the open ticket for this issue as JDK-8089009 and other reference issues JDK-8115476 & JDK-8089280.

The purpose of me asking this question now is to see if anyone has a solution or workaround to fix this existing issue.

Below is the demo code as provided in JDK-8089009 where the issue is reproducible with the latest version (18+) of JavaFX.

enter image description here

import javafx.application.Application;
import javafx.geometry.Orientation;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.SplitPane;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class HorizontalConstrainedTableScrolling extends Application {
    @Override
    public void start(final Stage primaryStage) throws Exception {
        final TableView<Object> left = new TableView<>();
        final TableColumn<Object, String> leftColumn = new TableColumn<>();
        left.getColumns().add(leftColumn);
        left.getItems().add(new Object());
        left.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);

        final TableView<Object> right = new TableView<>();
        final TableColumn<Object, String> rightColumn = new TableColumn<>();
        right.getColumns().add(rightColumn);
        right.getItems().add(new Object());

        final SplitPane splitPane = new SplitPane();
        splitPane.setOrientation(Orientation.HORIZONTAL);
        splitPane.getItems().addAll(left, right);

        Label osLabel = new Label(System.getProperty("os.name"));
        Label jvmLabel = new Label(
                System.getProperty("java.version") +
                        "-" + System.getProperty("java.vm.version") +
                        " (" + System.getProperty("os.arch") + ")"
        );

        primaryStage.setScene(new Scene(new BorderPane(splitPane, null, null, new VBox(osLabel, jvmLabel), null)));
        primaryStage.setWidth(600);
        primaryStage.setHeight(400);
        primaryStage.setTitle("TableView in SplitPane");
        primaryStage.show();
    }
}

[Update]:

Below is the usecase I generally encounter for using the constrained resize policy.

The requirement is , usually one column is strechable while all the other columns have some min/max widths so that they cannot go beyond those sizes. All columns should fit in the tableView provided if they have enough space, if the space is less than they can fit, then the scroll bar should appear.

Below is the demo demonstrating the example (with the scroll bar issue):

enter image description here

import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.stage.Stage;

public class ConstrainedResizePolicyDemo extends Application {
    @Override
    public void start(Stage primaryStage) throws Exception {
        TableColumn<Person, String> fnCol = new TableColumn<>("First Name");
        fnCol.setMinWidth(100);
        fnCol.setCellValueFactory(param -> param.getValue().firstNameProperty());

        TableColumn<Person, String> act1Col = new TableColumn<>("Act1");
        act1Col.setMinWidth(50);
        act1Col.setMaxWidth(50);
        act1Col.setResizable(false);
        act1Col.setCellValueFactory(param -> param.getValue().act1Property());

        TableColumn<Person, String> priceCol = new TableColumn<>("Price");
        priceCol.setMinWidth(100);
        priceCol.setMaxWidth(150);
        priceCol.setCellValueFactory(param -> param.getValue().priceProperty());

        TableColumn<Person, String> act2Col = new TableColumn<>("Act2");
        act2Col.setMinWidth(50);
        act2Col.setMaxWidth(50);
        act2Col.setResizable(false);
        act2Col.setCellValueFactory(param -> param.getValue().act2Property());

        TableColumn<Person, String> totalCol = new TableColumn<>("Total");
        totalCol.setMinWidth(100);
        totalCol.setMaxWidth(150);
        totalCol.setCellValueFactory(param -> param.getValue().totalProperty());

        TableColumn<Person, String> act3Col = new TableColumn<>("Act3");
        act3Col.setMinWidth(50);
        act3Col.setMaxWidth(50);
        act3Col.setResizable(false);
        act3Col.setCellValueFactory(param -> param.getValue().act3Property());

        TableColumn<Person, String> bidCol = new TableColumn<>("Bid");
        bidCol.setMinWidth(100);
        bidCol.setMaxWidth(150);
        bidCol.setCellValueFactory(param -> param.getValue().bidProperty());

        ObservableList<Person> persons = FXCollections.observableArrayList();
        persons.add(new Person("Harry", "A", "200.00", "B", "210.00", "C", "300.00"));
        persons.add(new Person("Kingston", "D", "260.00", "E", "610.00", "F", "700.00"));

        TableView<Person> tableView = new TableView<>();
        tableView.getColumns().addAll(fnCol, act1Col, priceCol, act2Col, totalCol, act3Col, bidCol);
        tableView.setItems(persons);
        tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);

        Scene scene = new Scene(tableView, 600, 150);
        primaryStage.setScene(scene);
        primaryStage.setTitle("Constrained Resize Policy TableView");
        primaryStage.show();
    }

    class Person {
        private StringProperty firstName = new SimpleStringProperty();
        private StringProperty act1 = new SimpleStringProperty();
        private StringProperty price = new SimpleStringProperty();
        private StringProperty act2 = new SimpleStringProperty();
        private StringProperty total = new SimpleStringProperty();
        private StringProperty act3 = new SimpleStringProperty();
        private StringProperty bid = new SimpleStringProperty();

        public Person(String fn, String act1, String price, String act2, String total, String act3, String bid) {
            setFirstName(fn);
            setAct1(act1);
            setPrice(price);
            setAct2(act2);
            setTotal(total);
            setAct3(act3);
            setBid(bid);
        }

        public StringProperty firstNameProperty() {
            return firstName;
        }

        public void setFirstName(String firstName) {
            this.firstName.set(firstName);
        }

        public StringProperty act1Property() {
            return act1;
        }

        public void setAct1(String act1) {
            this.act1.set(act1);
        }

        public StringProperty priceProperty() {
            return price;
        }

        public void setPrice(String price) {
            this.price.set(price);
        }

        public StringProperty act2Property() {
            return act2;
        }

        public void setAct2(String act2) {
            this.act2.set(act2);
        }

        public StringProperty totalProperty() {
            return total;
        }

        public void setTotal(String total) {
            this.total.set(total);
        }

        public StringProperty act3Property() {
            return act3;
        }

        public void setAct3(String act3) {
            this.act3.set(act3);
        }

        public StringProperty bidProperty() {
            return bid;
        }

        public void setBid(String bid) {
            this.bid.set(bid);
        }
    }
}

Upvotes: 4

Views: 752

Answers (2)

AtomicUs5000
AtomicUs5000

Reputation: 378

I use a workaround that has worked for me since JavaFX 8 and still does in JavaFX 19, but using the workaround means that you have follow some rules. In order for this to work:

  • any TableColumn that has maxWidth set must not be resizable
  • resizable columns must be next to each other or resizing them will sometimes show the hbar
  • you should then disable the column reordering for all columns so the user cannot rearrange them in a way where this workaround does not work. Once that is order, you just need a binding and a listener. The binding takes care the scrollbar when resizing the table and listener takes care of the scrollbar when resizing the columns.

The following works for your first example code. No listener is needed because there is only the one column:

leftColumn.prefWidthProperty().bind(left.widthProperty().subtract(1));

The following works for your second example code, including the reorderable and resizable changes. The binding and the listener can be applied to your choice of the TableColumns provided that your chosen column is resizable and does not have maxWidth set:

    @Override
    public void start(Stage primaryStage) throws Exception {
        TableColumn<Person, String> fnCol = new TableColumn<>("First Name");
        fnCol.setMinWidth(100);
        fnCol.setReorderable(false);
        fnCol.setCellValueFactory(param -> param.getValue().firstNameProperty());
        
        TableColumn<Person, String> priceCol = new TableColumn<>("Price");
        priceCol.setMinWidth(100);
        priceCol.setReorderable(false);
        priceCol.setCellValueFactory(param -> param.getValue().priceProperty());

        TableColumn<Person, String> totalCol = new TableColumn<>("Total");
        totalCol.setMinWidth(100);
        totalCol.setReorderable(false);
        totalCol.setCellValueFactory(param -> param.getValue().totalProperty());

        TableColumn<Person, String> bidCol = new TableColumn<>("Bid");
        bidCol.setMinWidth(100);
        bidCol.setReorderable(false);
        bidCol.setCellValueFactory(param -> param.getValue().bidProperty());

        TableColumn<Person, String> act1Col = new TableColumn<>("Act1");
        act1Col.setMinWidth(50);
        act1Col.setMaxWidth(50);
        act1Col.setResizable(false);
        act1Col.setReorderable(false);
        act1Col.setCellValueFactory(param -> param.getValue().act1Property());

        TableColumn<Person, String> act2Col = new TableColumn<>("Act2");
        act2Col.setMinWidth(50);
        act2Col.setMaxWidth(50);
        act2Col.setResizable(false);
        act2Col.setReorderable(false);
        act2Col.setCellValueFactory(param -> param.getValue().act2Property());

        TableColumn<Person, String> act3Col = new TableColumn<>("Act3");
        act3Col.setMinWidth(50);
        act3Col.setMaxWidth(50);
        act3Col.setResizable(false);
        act3Col.setReorderable(false);
        act3Col.setCellValueFactory(param -> param.getValue().act3Property());

        ObservableList<Person> persons = FXCollections.observableArrayList();
        persons.add(new Person("Harry", "A", "200.00", "B", "210.00", "C", "300.00"));
        persons.add(new Person("Kingston", "D", "260.00", "E", "610.00", "F", "700.00"));

        TableView<Person> tableView = new TableView<>();
        tableView.setPadding(Insets.EMPTY);
        tableView.getColumns().addAll(fnCol, priceCol, totalCol, bidCol, act1Col, act2Col, act3Col);
        tableView.setItems(persons);
        tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
        
        fnCol.widthProperty().addListener((obs, ov, nv)->{
            if (nv != null && !ov.equals(nv) && (double)nv > 0) {
                if ((double)nv > (double)ov) {
                    fnCol.prefWidthProperty().unbind();
                    fnCol.setPrefWidth(tableView.getWidth() - act1Col.getWidth() - priceCol.getWidth() - act2Col.getWidth() - totalCol.getWidth() - act3Col.getWidth() - bidCol.getWidth());
                    fnCol.prefWidthProperty().bind(
                        tableView.widthProperty()
                        .subtract(act1Col.widthProperty())
                        .subtract(priceCol.widthProperty())
                        .subtract(act2Col.widthProperty())
                        .subtract(totalCol.widthProperty())
                        .subtract(act3Col.widthProperty())
                        .subtract(bidCol.widthProperty())
                    );
                }
            }
        });
        fnCol.prefWidthProperty().bind(
            tableView.widthProperty()
            .subtract(act1Col.widthProperty())
            .subtract(priceCol.widthProperty())
            .subtract(act2Col.widthProperty())
            .subtract(totalCol.widthProperty())
            .subtract(act3Col.widthProperty())
            .subtract(bidCol.widthProperty())
        );

        Scene scene = new Scene(tableView, 600, 150);
        primaryStage.setScene(scene);
        primaryStage.setTitle("Constrained Resize Policy TableView");
        primaryStage.show();
    }

I'm assuming this works simply because of when precisely calculations are made of column widths and when horizontal scrollbar visibility is triggered and the order in which all these things happen.

Upvotes: 0

kleopatra
kleopatra

Reputation: 51535

As always with long-standing bugs, the only way out is a hack. The basic idea is to set the scrollBar's sizing constraints depending on the resize policy: either fixed to 0 for constrained- or the usual useComputed for unconstrained policy. Below is a utility method that implements the hack.

Notes

  • has to be called after the table is visible
  • has to be called whenever the policy changes at runtime
  • caveat: there still seems to be a slight visual artefact when resizing to very small values: looks like one (or both) of the arrows appear (not their background, just the arrow)

The code:

public static void updateScrollBar(final TableView<Object> table) {
    // lookup the horizontal scroll bar
    ScrollBar hbar = null;
    Set<Node> scrollBars = table.lookupAll(".scroll-bar");
    for (Node node : scrollBars) {
        ScrollBar bar = (ScrollBar) node;
        if (bar.getOrientation() == Orientation.HORIZONTAL) {
            hbar = bar;
            break;
        }
    }

    // choose sizing constraint as either 0 or useComputed, depending on policy
    Callback<?, ?> policy = table.getColumnResizePolicy();
    double pref = policy == CONSTRAINED_RESIZE_POLICY ? 0 : USE_COMPUTED_SIZE;

    // set all sizing constraints
    hbar.setPrefSize(pref, pref);
    hbar.setMaxSize(pref, pref);
    hbar.setMinSize(pref, pref);
}

Upvotes: 3

Related Questions