I'd like to create a grid as a background for my JavaFX application. My current solution is to paint a rectangle on a canvas, create an image pattern from it and set it as fill.
Question: Is there a better way to approach this, preferrably via CSS?
Current version:
public class BackgroundGrid extends Application {
double gridSize = 20;
#Override
public void start(Stage primaryStage) {
Scene scene = new Scene(new Group(), 800, 600);
primaryStage.setScene(scene);
primaryStage.show();
scene.setFill(createGridPattern());
}
public ImagePattern createGridPattern() {
double w = gridSize;
double h = gridSize;
Canvas canvas = new Canvas(w, h);
GraphicsContext gc = canvas.getGraphicsContext2D();
gc.setStroke(Color.BLACK);
gc.setFill(Color.LIGHTGRAY.deriveColor(1, 1, 1, 0.2));
gc.fillRect(0, 0, w, h);
gc.strokeRect(0, 0, w, h);
Image image = canvas.snapshot(new SnapshotParameters(), null);
ImagePattern pattern = new ImagePattern(image, 0, 0, w, h, false);
return pattern;
}
public static void main(String[] args) {
launch(args);
}
}
Thank you very much!
Edit: in order to get sharp grid lines, just use
gc.strokeRect(0.5, 0.5, w, h);
I think that wouldn't be doable in CSS, isn't it?
You can do it with CSS too. This is all you need:
.root {
-fx-background-color: #D3D3D333,
linear-gradient(from 0.5px 0.0px to 10.5px 0.0px, repeat, black 5%, transparent 5%),
linear-gradient(from 0.0px 0.5px to 0.0px 10.5px, repeat, black 5%, transparent 5%);
}
The 0.5px offset solves some buggy behavior when set from 0px to 10px, and some lines are rendered with two pixels instead of one:
Here is an answer reproduced from an old Oracle forum post.
GridPane based approach
A few methods (base upon a GridPane layout):
style borders of the individual cells (and ensure that they fill
their entire grid position) OR
style the background of the whole
grid leaving gaps between cells which fill their entire grid
position as is shown below OR
add new grid nodes with lines and then
style the added lines.
I chose method 2 (styling the grid background) for the code below. The sample uses inline CSS styles (cause I'm lazy), but it would work (and be better) with an external CSS stylesheet to style the grid.
import javafx.application.Application;
import javafx.geometry.HPos;
import javafx.geometry.VPos;
import javafx.scene.*;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.stage.Stage;
public class GridPaneStyle extends Application {
#Override
public void start(final Stage stage) {
// create a grid with some sample data.
GridPane grid = new GridPane();
grid.addRow(0, new Label("1"), new Label("2"), new Label("3"));
grid.addRow(1, new Label("A"), new Label("B"), new Label("C"));
// make all of the Controls and Panes inside the grid fill their grid cell,
// align them in the center and give them a filled background.
// you could also place each of them in their own centered StackPane with
// a styled background to achieve the same effect.
for (Node n : grid.getChildren()) {
if (n instanceof Control) {
Control control = (Control) n;
control.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
control.setStyle("-fx-background-color: cornsilk; -fx-alignment: center;");
}
if (n instanceof Pane) {
Pane pane = (Pane) n;
pane.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
pane.setStyle("-fx-background-color: cornsilk; -fx-alignment: center;");
}
}
// style the grid so that it has a background and gaps around the grid and between the
// grid cells so that the background will show through as grid lines.
grid.setStyle("-fx-background-color: palegreen; -fx-padding: 2; -fx-hgap: 2; -fx-vgap: 2;");
// turn layout pixel snapping off on the grid so that grid lines will be an even width.
grid.setSnapToPixel(false);
// set some constraints so that the grid will fill the available area.
ColumnConstraints oneThird = new ColumnConstraints();
oneThird.setPercentWidth(100 / 3.0);
oneThird.setHalignment(HPos.CENTER);
grid.getColumnConstraints().addAll(oneThird, oneThird, oneThird);
RowConstraints oneHalf = new RowConstraints();
oneHalf.setPercentHeight(100 / 2.0);
oneHalf.setValignment(VPos.CENTER);
grid.getRowConstraints().addAll(oneHalf, oneHalf);
// layout the scene in a stackpane with some padding so that the grid is centered
// and it is easy to see the outer grid lines.
StackPane layout = new StackPane();
layout.setStyle("-fx-background-color: whitesmoke; -fx-padding: 10;");
layout.getChildren().addAll(grid);
stage.setScene(new Scene(layout, 600, 400));
stage.show();
// can be uncommented to show the grid lines for debugging purposes, but not particularly useful for styling purposes.
//grid.setGridLinesVisible(true);
}
public static void main(String[] args) {
launch();
}
}
Alternate Canvas Based Approach
See also the FXExperience blog post Resizable Grid using Canvas.
The advice to get really sharp grid lines is not correct for all systems. On a Retina display you would have to use a line width of 0.5 and then an offset of 0.25 from the integer line coordinates. So in practice you would have to determine on what system your application is running and then use different line widths and offsets.
Related
I would like to create a JavaFX Node hierarchy such as:
Region parentRegion = new Region();
Circle circle = new Circle();
parentRegion.getChildren().add(circle);
But I don't want the children of the parent region to use the StyleSheet of the parent.
So for example if I have the following CSS:
.green {
-fx-fill: green;
}
And I have the following code:
Region parentRegion = new Region();
parentRegion.getStylesheets().add(<my CSS path>);
Circle circle = new Circle();
circle.getStyleClass().add("green");
parentRegion.getChildren().add(circle);
I would like the Circle to be black and not green.
Is it possible?
You can place the child in a SubScene.
SubScenes do not inherit the CSS styles of their parent nodes.
Example
In the example, there are two circles, one circle is in the root layout pane of the main scene, the other is in a SubScene (with a light blue background) which is also in the root layout pane of the main scene.
The layout pane of the main scene (a VBox) has styles defined so that all of the child circles inherit a green fill. However, the SubScene prevents the inheritance of CSS styles, so only the circle which is not in the SubScene is green.
colored-circles.css
.green {
-fx-fill: green;
}
Circle {
-fx-fill: inherit;
}
Note that, by default, -fx-fill CSS attribute values are not inherited by child nodes, hence the additional rule to have circles inherit the CSS attribute value for -fx-fill.
StyleInheritance.java
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.*;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
public class StyleInheritance extends Application {
public static final double R = 50;
#Override
public void start(Stage stage) {
Circle circleInMainScene = new Circle(R, R, R);
Circle circleInSubScene = new Circle(R, R, R);
SubScene subScene = new SubScene(
new Group(circleInSubScene),
circleInSubScene.getLayoutBounds().getWidth(),
circleInSubScene.getLayoutBounds().getHeight()
);
subScene.setFill(Color.LIGHTBLUE);
VBox layout = new VBox(10, circleInMainScene, subScene);
layout.setPadding(new Insets(10));
layout.getStyleClass().add("green");
Scene mainScene = new Scene(layout);
mainScene.getStylesheets().add(
StyleInheritance.class.getResource(
"colored-circles.css"
).toExternalForm()
);
stage.setScene(mainScene);
stage.show();
}
public static void main(String[] args) {
launch();
}
}
There is probably also some way of accomplishing what you want via CSS rules alone rather than using a SubScene, but using a SubScene appears to work and is what I came up with.
I want create a app using javafx. It looks like this:
I want to add the zoom function for the chart. When I click the button "Zoom in", the app will become fig2. However, I have no idea to achieve it. When I change the size of pane included the chart, it will change grid pane size, looks like this:
You do not want the zoom to be considered for the gridpane layout. In this can be achieved by applying transforms to the child of the gridpane you want to modify.
The following example demonstrates how to zoom a node while the mouse is hovering over it:
private static Region createRegion(String background) {
Region region = new Region();
region.setStyle("-fx-background-color:"+background);
region.setPrefSize(300, 100);
return region;
}
#Override
public void start(Stage primaryStage) {
GridPane gp = new GridPane();
// create background
gp.add(createRegion("green"), 0, 0);
gp.add(createRegion("dodgerblue"), 0, 1);
// create region to be zoomed
Region zoomRegion = createRegion("red");
GridPane.setFillWidth(zoomRegion, Boolean.FALSE);
GridPane.setFillHeight(zoomRegion, Boolean.FALSE);
zoomRegion.setPrefWidth(100);
Scale scale = new Scale();
zoomRegion.getTransforms().add(scale);
// keep pivot at bottom left corner
scale.pivotYProperty().bind(zoomRegion.heightProperty());
zoomRegion.hoverProperty().addListener((observable, oldValue, newValue) -> {
// adjust scale when hover state is changed
double scaleFactor = newValue ? 1.5 : 1;
scale.setX(scaleFactor);
scale.setY(scaleFactor);
});
gp.add(zoomRegion, 0, 1);
Scene scene = new Scene(gp);
primaryStage.setScene(scene);
primaryStage.show();
}
I am creating a board game in JavaFX using GridPane.
There are 7 different animations which could be placed in each grid (cell) of the grid.
Initially the grid looks like this
I tested adding a simple circle to it before programming my animation insertions. And it looks like this
The nodes added are SubScenes which include TimeLine animation. Each cell size is 40x40 and the SubScene size is also 40x40.
The subscenes when added, get on top of the gridpane border lines and it doesn't look good.
What can I do so that the nodes are added below the grid lines? i.e. the gridlines are on top of the nodes.
If it is not possible with GridPane, is there anything else I can use?
class which i execute for the game
class Game {
static GridPane grid;
public void start(final Stage stage) throws Exception {
int rows = 5;
int columns = 5;
stage.setTitle("Enjoy your game");
grid = new GridPane();
for(int i = 0; i < columns; i++) {
ColumnConstraints column = new ColumnConstraints(40);
grid.getColumnConstraints().add(column);
}
for(int i = 0; i < rows; i++) {
RowConstraints row = new RowConstraints(40);
grid.getRowConstraints().add(row);
}
grid.setOnMouseReleased(new EventHandler<MouseEvent> () {
public void handle(MouseEvent me) {
grid.add(Anims.getAnim(1), (int)((me.getSceneX() - (me.getSceneX() % 40)) / 40), (int)((me.getSceneY() - (me.getSceneY() % 40)) / 40)); //here the getAnim argument could be between 1-7
}
});
grid.setStyle("-fx-background-color: white; -fx-grid-lines-visible: true");
Scene scene = new Scene(grid, (columns * 40) + 100, (rows * 40) + 100, Color.WHITE);
stage.setScene(scene);
stage.show();
}
public static void main(final String[] arguments) {
Application.launch(arguments);
}
}
class which contains animations, here I am just creating a circle
public class Anims {
public static SubScene getAnim(final int number) throws Exception {
Circle circle = new Circle(20, 20f, 7);
circle.setFill(Color.RED);
Group group = new Group();
group.getChildren().add(circle);
SubScene scene = new SubScene(group, 40, 40);
scene.setFill(Color.WHITE);
return scene;
}
}
Don't use setGridLinesVisible(true): the documentation explicitly states this is for debug only.
Instead, place a pane in all the grid cells (even the empty ones), and style the pane so you see the borders. (This gives you the opportunity to control the borders very carefully, so you can avoid double borders, etc.) Then add the content to each pane. You can also register the mouse listeners with the pane, which means you don't have to do the ugly math to figure out which cell was clicked.
The recommended way to apply a border to any region is to use CSS and a "nested background" approach. In this approach, you draw two (or more) background fills on the region, with different insets, giving the appearance of a border. So for example:
-fx-background-fill: black, white ;
-fx-background-insets: 0, 1 ;
will first draw a black background with no insets, and then over that will draw a white background with insets of 1 pixel on all sides, giving the appearance of a black border of width 1 pixel. While this may seem counter-intuitive, the performance of this is (allegedly) better than specifying border directly. You can also specify a sequence of four values for the insets for each fill, which are interpreted as the insets on the top, right, bottom, and left, respectively. So
-fx-background-fill: black, white ;
-fx-background-insets: 0, 0 1 1 0 ;
has the effect of a black border on the right and bottom, etc.
I'm also not sure SubScene is what you really want, unless you are intending attaching different cameras to each cell. If you really need a subscene, make the fill transparent to avoid drawing over the edges of the cell. You could just add the Group directly to each cell (you could probably just add the circle, depending on exactly what you need...).
Something like:
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.RowConstraints;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
public class Game2 extends Application{
#Override
public void start(final Stage stage) throws Exception {
int rows = 5;
int columns = 5;
stage.setTitle("Enjoy your game");
GridPane grid = new GridPane();
grid.getStyleClass().add("game-grid");
for(int i = 0; i < columns; i++) {
ColumnConstraints column = new ColumnConstraints(40);
grid.getColumnConstraints().add(column);
}
for(int i = 0; i < rows; i++) {
RowConstraints row = new RowConstraints(40);
grid.getRowConstraints().add(row);
}
for (int i = 0; i < columns; i++) {
for (int j = 0; j < rows; j++) {
Pane pane = new Pane();
pane.setOnMouseReleased(e -> {
pane.getChildren().add(Anims.getAtoms(1));
});
pane.getStyleClass().add("game-grid-cell");
if (i == 0) {
pane.getStyleClass().add("first-column");
}
if (j == 0) {
pane.getStyleClass().add("first-row");
}
grid.add(pane, i, j);
}
}
Scene scene = new Scene(grid, (columns * 40) + 100, (rows * 40) + 100, Color.WHITE);
scene.getStylesheets().add("game.css");
stage.setScene(scene);
stage.show();
}
public static class Anims {
public static Node getAtoms(final int number) {
Circle circle = new Circle(20, 20f, 7);
circle.setFill(Color.RED);
Group group = new Group();
group.getChildren().add(circle);
// SubScene scene = new SubScene(group, 40, 40);
// scene.setFill(Color.TRANSPARENT);
return group;
}
}
public static void main(final String[] arguments) {
Application.launch(arguments);
}
}
and the css:
.game-grid {
-fx-background-color: white ;
-fx-padding: 10 ;
}
.game-grid-cell {
-fx-background-color: black, white ;
-fx-background-insets: 0, 0 1 1 0 ;
}
.game-grid-cell.first-row {
-fx-background-insets: 0, 1 1 1 0 ;
}
.game-grid-cell.first-column {
-fx-background-insets: 0, 0 1 1 1 ;
}
.game-grid-cell.first-row.first-column {
-fx-background-insets: 0, 1 ;
}
Simply add an H and V gap of one pixel width and let the grid pane's background color "shine" through:
.my-grid-pane {
-fx-background-color: lightgray;
-fx-vgap: 1;
-fx-hgap: 1;
-fx-padding: 1;
}
If the grid pane's background color spreads from outside more than one pixel (will happen if its parent is larger than itself), just wrap the grid in a Group!
I apologize for the response instead of the comment, not enough reputation.
Strangely, but #James_D 's response didn't help me; when the window was resized, the cell borders randomly changed their width, overlapping each other.
This answer helped solve the problem, so by slightly changing the code given by #James_D (only the .css file), we get:
.classes-grid {
-fx-background-color: white ;
-fx-padding: 10 ;
}
.classes-grid-cell {
-fx-border-color: dimgray;
-fx-border-width: 0 1 1 0;
-fx-background-color: transparent;
}
.classes-grid-cell.first-row {
-fx-border-width: 1 1 1 0 ;
}
.classes-grid-cell.first-column {
-fx-border-width: 0 1 1 1 ;
}
.classes-grid-cell.first-row.first-column {
-fx-border-width: 1 ;
}
Same idea with Mordechai's answer. But if you want to set these things by JavaFX code, not CSS stylesheet. Then you can do sth like this:
Set up the Hgap and Vgap: gridpane.setHgap(1) and gridpane.setVgap(1)
Set up the background color: gridpane.setBackground(new Background(new BackgroundFill(Color.rgb(0,0,0), new CornerRadii(2.5), new Insets(-1.0)))) (CornerRadii and Insets value depends on your choice, background color determined by rgb value)
I'm trying to make a card like the bootstrap CSS, but using JavaFX components. I want a rounded border but the background color of the top part (the header) is giving me problems.
The background overflows the border and looks quite ugly. I've googled a bit and found that an overflow:hidden on the background color should solve it. JavaFX css doesn't seem to have that though. Is there another way of solving this?
My solution so far:
As described in the JavaFX CSS Reference Guide, overflow is not supported.
JavaFX CSS does not support CSS layout properties such as float, position, overflow, and width. However, the CSS padding and margins properties are supported on some JavaFX scene graph objects. All other aspects of layout are handled programmatically in JavaFX code. In addition, CSS support for HTML-specific elements such as Tables are not supported since there is no equivalent construct in JavaFX.
However, to solve the rounded-background issue you can use -fx-background-radius along with -fx-border-radius. They should be the same value. You can find it here in the reference guide.
Here's an example of a bootstrap-like card that I think you are trying to make. You would use -fx-background-radius: <top-left> <top-right> <bottom-right> <bottom-left>; which would be -fx-background-radius: 10 10 0 0;
public class Card extends StackPane {
public BorderPane border = new BorderPane();
public StackPane header = new StackPane(), content = new StackPane();
public Card() {
setAlignment(Pos.CENTER);
getChildren().add(border);
border.setTop(header);
border.setCenter(content);
border.setStyle("-fx-border-color: cornflowerblue; -fx-border-radius: 10; ");
header.setStyle("-fx-background-color: derive(cornflowerblue, 70%); -fx-background-radius: 10 10 0 0; ");
header.setMinWidth(100);
header.setMinHeight(80);
content.setMinWidth(100);
content.setMinHeight(100);
}
public BorderPane getCard() {
return border;
}
public StackPane getHeader() {
return header;
}
public StackPane getContent() {
return content;
}
}
public void start(Stage stage) throws Exception {
Card card = new Card();
card.setPadding(new Insets(10,10,10,10));
GridPane grid = new GridPane();
grid.setVgap(10); grid.setHgap(10);
grid.setAlignment(Pos.CENTER);
grid.addRow(0, new Label("Username"), new TextField());
grid.addRow(1, new Label("Password"), new PasswordField());
grid.addRow(2, new Button("Submit"));
card.getContent().getChildren().add(grid);
Label title = new Label("Card Example");
title.setFont(Font.font("Tahoma", FontWeight.SEMI_BOLD, 36));
card.getHeader().getChildren().add(title);
StackPane stack = new StackPane();
stack.setAlignment(Pos.CENTER);
stack.getChildren().add(card);
Scene scene = new Scene(stack, 500, 300);
stage.setTitle("Boostrap-like Card Example");
stage.setScene(scene);
stage.show();
}
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.
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 ;
}