2

I am trying to wrap my head around Scroll- and Tilepanes atm, and I have come upon an issue I just cant solve without a dirty hack.

I have a horizontal TilePane that has 8 Tiles, and I set it to have 4 columns, resulting in 2 rows with 4 tiles. That TilePane I put in an HBox, since if I put it in a StackPane it would stretch the size of the tilepane making my colum setting void. A bit weird that setting the prefColumns/Rows recalculates the size of the TilePane, rather than trying to set the actual amounts of columns/rows, feels more like a dirty hack.

Anyway, putting the HBox directly into the ScrollPane would not work either, since the Scrollbars would not appear even after the 2nd row of tiles would get cut off. Setting that HBox again in a Stackpane which I then put in a ScrollPane does the trick. Atleast until I resize the width of the window to be so small the tilepane has to align the tiles anew and a 3rd or more rows appear.

Here is the basic programm:

public class Main extends Application {

    @Override
    public void start(Stage stage) {

        TilePane tilePane = new TilePane();
        tilePane.setPadding(new Insets(5));
        tilePane.setVgap(4);
        tilePane.setHgap(4);
        tilePane.setPrefColumns(4);
        tilePane.setStyle("-fx-background-color: lightblue;");

        HBox tiles[] = new HBox[8];
        for (int i = 0; i < 8; i++) {
            tiles[i] = new HBox(new Label("This is node #" + i));
            tiles[i].setStyle("-fx-border-color: black;");
            tiles[i].setPadding(new Insets(50));
            tilePane.getChildren().add(tiles[i]);
        }

        HBox hbox = new HBox();
        hbox.setAlignment(Pos.CENTER);
        hbox.setStyle("-fx-background-color: blue;");
        hbox.getChildren().add(tilePane); 

        StackPane stack = new StackPane();
        stack.getChildren().add(hbox);

        ScrollPane sp = new ScrollPane();
        sp.setFitToHeight(true);
        sp.setFitToWidth(true);
        sp.setContent(stack);

        stage.setScene(new Scene(sp, 800, 600));
        stage.show();
    }

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

I managed to achieve my wanted behaviour, but its more of a really dirty hack. I added a listener to the height and width of my HBox containing the TilePane and assumed that when the height changes its because the width got so small that a column was removed and a new row added. To be able to do that I put the HBox in a VBox so that it would not grow withe the height of the ScrollPane. For the width I simply calculated if there is space to display another colum (up to 4), to do it.

Here are the changes:

public class Main extends Application {

    private boolean notFirstPassHeight;
    private boolean notFirstPassWidth;

    @Override
    public void start(Stage stage) {

        TilePane tilePane = new TilePane();
        tilePane.setPadding(new Insets(5));
        tilePane.setVgap(4);
        tilePane.setHgap(4);
        tilePane.setPrefColumns(4);
        tilePane.setStyle("-fx-background-color: lightblue;");
        // I took the value from ScenicView
        tilePane.prefTileWidthProperty().set(182);

        HBox tiles[] = new HBox[8];
        for (int i = 0; i < 8; i++) {
            tiles[i] = new HBox(new Label("This is node #" + i));
            tiles[i].setStyle("-fx-border-color: black;");
            tiles[i].setPadding(new Insets(50));
            tilePane.getChildren().add(tiles[i]);
        }

        ScrollPane sp = new ScrollPane();
        sp.setFitToHeight(true);
        sp.setFitToWidth(true);

        StackPane stack = new StackPane();

        VBox vbox = new VBox();
        vbox.setStyle("-fx-background-color: red");

        HBox hbox = new HBox();
        hbox.setAlignment(Pos.CENTER);
        hbox.setStyle("-fx-background-color: blue;");
        hbox.getChildren().add(tilePane);

        notFirstPassHeight = false;
        notFirstPassWidth = false;
        hbox.heightProperty().addListener((observable, oldValue, newValue) -> {
            if (oldValue.doubleValue() < newValue.doubleValue() && notFirstPassHeight) {
                tilePane.setPrefColumns(tilePane.getPrefColumns() - 1);
                stack.requestLayout();
            }
            notFirstPassHeight = true;
        });
        hbox.widthProperty().addListener((observable, oldValue, newValue) -> {
             if (oldValue.doubleValue() < newValue.doubleValue() && notFirstPassWidth && tilePane.getPrefColumns() <= 3
                     && (newValue.doubleValue() / (tilePane.getPrefColumns() + 1)) > tilePane.getPrefTileWidth()) {
                 tilePane.setPrefColumns(tilePane.getPrefColumns() + 1);
                 stack.requestLayout();
             }
             notFirstPassWidth = true;
        });

        vbox.getChildren().add(hbox);

        stack.getChildren().add(vbox);

        sp.setContent(stack);
        stage.setScene(new Scene(sp, 800, 600));
        stage.show();
    }

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

}

However this approach requires me to

1.Know the Width of the Tiles in the Tilepane.

2.Consider Padding and Gap between tiles for my calculation to be accurate, which I dont do in my example.

And its just not a good approach at any rate if you ask me. Too complicated a process for such a basic thing. There has to be a way better and simple way to accomplish complete resizability and the wanted behaviour with TilePanes in a ScrollPane.

Community
  • 1
  • 1
  • I want the functionality to be that what it does in my "dirty hack" (2nd) example. To have 2 rows ala 4 columns if I say have it at full screen, and if the application gets resized to dynamically add rows and reduce columns as it the width gets smaller and smaller. Basically I want the application to prioritize growing in height and display vertical scrollbars and only really show scrollbars when there is no more room left to resize itself (down to 1 column). Hope I could explain myself, my 2nd example does what I want it to do, I just dont like my limited approach. – 404KnowledgeNotFound Aug 17 '16 at 13:35
  • Ah, OK, then `GridPane` is not what you want. Let me experiment... – James_D Aug 17 '16 at 13:36

1 Answers1

4

Setting the preferred number of columns and/or rows in the TilePane determines the calculation for the prefWidth and prefHeight values for that tile pane. If you want to force a maximum number of columns, you just need to make the maxWidth equal to the computed prefWidth: you can do this with

tilePane.setMaxWidth(Region.USE_PREF_SIZE);

This means that (as long as the tile pane is placed in something that manages layout), it will never be wider than the pref width, which is computed to allow the preferred number of columns. It may, of course, be smaller than that. (Note you could use the same trick with setMinWidth if you needed a minimum number of columns, rather than a maximum number of columns.)

The scroll pane's fitToHeight and fitToWidth properties will, when true, attempt to resize the height (respectively width) of the content to be equal to the height (width) of the scroll pane's viewport. These operations will take precedence over the preferred height (width) of the content, but will attempt to respect the minimum height (width).

Consequently, it's usually a mistake to call both setFitToWidth(true) and setFitToHeight(true), as this will almost always turn off scrolling completely (just forcing the content to be the same size as the scroll pane's viewport).

So here you want to make the max width of the tile pane respect the pref width, and fix the width of the tile pane to be the width of the scroll pane's viewport (so that when you shrink the width of the window, it shrinks the width of the viewport and creates more columns). This will add a vertical scrollbar if the number of rows grows large enough, and only add a horizontal scrollbar if the viewport shrinks horizontally below the minimum width of the tile pane (which is computed as the minimum of the preferred widths of all the nodes it contains).

I think the following version of your original code does essentially what you are looking for:

import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Region;
import javafx.scene.layout.TilePane;
import javafx.stage.Stage;

public class ScrollingTilePane extends Application {

    @Override
    public void start(Stage stage) {

        TilePane tilePane = new TilePane();
        tilePane.setPadding(new Insets(5));
        tilePane.setVgap(4);
        tilePane.setHgap(4);
        tilePane.setPrefColumns(4);
        tilePane.setStyle("-fx-background-color: lightblue;");

        // dont grow more than the preferred number of columns:
        tilePane.setMaxWidth(Region.USE_PREF_SIZE);

        HBox tiles[] = new HBox[8];
        for (int i = 0; i < 8; i++) {
            tiles[i] = new HBox(new Label("This is node #" + i));
            tiles[i].setStyle("-fx-border-color: black;");
            tiles[i].setPadding(new Insets(50));
            tilePane.getChildren().add(tiles[i]);
        }

        HBox hbox = new HBox();
        hbox.setAlignment(Pos.CENTER);
        hbox.setStyle("-fx-background-color: blue;");
        hbox.getChildren().add(tilePane); 

//        StackPane stack = new StackPane();
//        stack.getChildren().add(tilePane);
//        stack.setStyle("-fx-background-color: blue;");

        ScrollPane sp = new ScrollPane();
//        sp.setFitToHeight(true);
        sp.setFitToWidth(true);
        sp.setContent(hbox);

        stage.setScene(new Scene(sp, 800, 600));
        stage.show();
    }

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

Note that if you need to change the background color of the space outside the scroll pane's content, you can use the following in an external style sheet:

.scroll-pane .viewport {
  -fx-background-color: red ;
}
James_D
  • 201,275
  • 16
  • 291
  • 322
  • Thank you this works well, however in my real application I have also other screens in the ScrollPane and those seem to always be a few pixel to big, making the horizontal scrollbar never disappear. Its probably not a problem caused by this solution but maybe you have an idea as to why this happens? I noticed in ScenicView that layoutBounds and BoundsInParent are different, and the cause are Separators I have in the TableNavigators I have in the screen. I just dont know why they would do that tough. – 404KnowledgeNotFound Aug 18 '16 at 09:32
  • Same basic principles apply. It just sounds like you haven't figured out how layout works in JavaFX. If I have time later I will add a general description to the answer but read the [Javadocs for layout](https://docs.oracle.com/javase/8/javafx/api/javafx/scene/layout/package-summary.html). There are a couple of other resources I can try to dig out when I am back at my computer. – James_D Aug 18 '16 at 10:04
  • 1
    In short, though, a scroll pane will have a horizontal scroll bar if the preferred width (minimum width if fitToWidth is true) of the content is bigger than the width allocated to the scroll pane. – James_D Aug 18 '16 at 10:18
  • You are right, I didnt notice but the minWidth of my Header was bound to the Applications width, meaning 1920 at fullscreen, but the ScrollPanes Viewport for some reason only has 1918 avialable width at fullscreen. I changed the binding to prefWidth, works now. I guess I should avoid setting minWidth in the future if at all possible. Thank you for your time and effort :). – 404KnowledgeNotFound Aug 19 '16 at 08:05