Search code examples
javafxresizescrollpane

How to get Scrollbars as needed with a TabPane as Content?


I have a BorderPane inside a Tabpane inside a ScrollPane. The ScrollPane.ScrollBarPolicy.AS_NEEDED does work if i remove the TabPane and put the BorderPane as Content of the ScrollPane. How do i get this to work with the TabPane?

Somehow the BorderPane is able to tell the ScrollPane when to display Scrollbars and the TabPane unable to do so. I looked through the avaible Methods for the Tabpane but couldn't find any for this resizing.

Working Example:

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.layout.*;
import javafx.stage.Stage;

public class FXApplication extends Application {

    private BorderPane border;
    private GridPane inner;
    private TabPane tabPane;

    @Override
    public void start(Stage primaryStage) {

        tabPane = new TabPane();
        Tab tab = new Tab("test");
        tabPane.getTabs().add(tab);

        border = new BorderPane();
        border.setCenter(innerGrid());
        tab.setContent(border);

        ScrollPane scp = new ScrollPane();
        scp.setFitToHeight(true);
        scp.setFitToWidth(true);
        scp.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
        scp.setHbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);

//        scp.setContent(border);  // this works
        scp.setContent(tabPane);   // this doesnt

        Scene s = new Scene(scp);
        primaryStage.setScene(s);
        primaryStage.show();
    }

    private GridPane innerGrid() {
        inner = new GridPane();

        for(int i=0; i<11 ;i++) {
            ColumnConstraints columnConstraints = new ColumnConstraints();
            columnConstraints.setHgrow(Priority.SOMETIMES);
            inner.getColumnConstraints().add(columnConstraints);

            RowConstraints rowConstraints = new RowConstraints();
            rowConstraints.setVgrow(Priority.SOMETIMES);
            inner.getRowConstraints().add(rowConstraints);
        }

        for(int i=0; i<100 ;i++) {
            inner.add(new Button("Button " + i), i/10, i%10);
        }

        return inner;
    }

    public static void main(String[] args) {
        FXApplication.launch(args);
    }

}

Solution

  • Astonishingly, the exact behavior of AS_NEEDED is unspecified. All we have is the ScrollPaneSkin to look at. The decision whether or not to show the (f.i.) horizontal bar happens in its private method determineHorizontalSBVisible()

    private boolean determineHorizontalSBVisible() {
        final ScrollPane sp = getSkinnable();
    
        if (Properties.IS_TOUCH_SUPPORTED) {
            return (tempVisibility && (nodeWidth > contentWidth));
        }
        else {
            // RT-17395: ScrollBarPolicy might be null. If so, treat it as "AS_NEEDED", which is the default
            ScrollBarPolicy hbarPolicy = sp.getHbarPolicy();
            return (ScrollBarPolicy.NEVER == hbarPolicy) ? false :
                   ((ScrollBarPolicy.ALWAYS == hbarPolicy) ? true :
                   ((sp.isFitToWidth() && scrollNode != null ? scrollNode.isResizable() : false) ?
                   (nodeWidth > contentWidth && scrollNode.minWidth(-1) > contentWidth) : (nodeWidth > contentWidth)));
        }
    }
    

    Here nodeWidth is the actual width of the content node - has been calculated previously, respecting the node's min/max widths - and contentWidth is the width available for laying out the content.

    Unreadable code (for me ;) In the case of resizable content and fitting into scrollPane's content area boils down to returning true if both content's actual and min width are greater than the available width.

    The minWidth makes the difference in your context: BorderPane has a min > 0, TabPane has a min == 0, so the method above always returns false.

    The other way round: to allow the hbar being visible with the TabPane it needs a min, f.i. by relating it to its pref:

    tabPane.setMinWidth(Region.USE_PREF_SIZE);