JavaFX ScrollPane and FlowPane properties to resize the FlowPane if columns overfill the ScrollPane width

231 Views Asked by At

I have a FlowPane wrapped in a ScrollPane. FlowPane orientation is Vertical, so it will wrap the controls. But I want to set the FlowPane to resize vertically if the columns size is greater than the width of ScrollPane. I've tried a lot of settings, both on ScrollPane and FlowPane but none of them helped me with my wish. As an image of how I want to do is something like this: (red contur is ScrollPane, green is FlowPane)

Containers, after the flow pane is populated, with ScrollPane's width more than two columns of controls: populated

How it works right now, after resizing:

enter image description here

How I want to do after resizing the ScrollPane:

after_resizing

Can this be achieved? What settings I must do to both ScrollPane and FlowPane?


Edit:

Minimal reproduction code:

hello-view.fxml:

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ScrollPane?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.FlowPane?>
<?import javafx.scene.text.Font?>

<AnchorPane prefHeight="424.0" prefWidth="457.0" xmlns="http://javafx.com/javafx/19" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.example.demo.HelloController">
   <children>
      <AnchorPane layoutX="14.0" layoutY="14.0" prefHeight="399.0" prefWidth="430.0" style="-fx-border-color: #555555;" AnchorPane.bottomAnchor="14.0" AnchorPane.leftAnchor="14.0" AnchorPane.rightAnchor="14.0" AnchorPane.topAnchor="14.0">
         <children>
            <ScrollPane fitToHeight="true" fitToWidth="true" focusTraversable="false" hbarPolicy="NEVER" layoutX="14.0" layoutY="13.0" maxWidth="1.7976931348623157E308" prefHeight="377.0" prefWidth="404.0" style="-fx-border-color: red; -fx-border-width: 2;" AnchorPane.bottomAnchor="7.0" AnchorPane.leftAnchor="13.0" AnchorPane.rightAnchor="12.0" AnchorPane.topAnchor="12.0">
               <content>
                  <FlowPane maxWidth="1.7976931348623157E308" orientation="VERTICAL" prefHeight="363.0" prefWidth="397.0" rowValignment="TOP" style="-fx-border-color: green; -fx-border-width: 2;">
                     <children>
                        <Button mnemonicParsing="false" prefHeight="121.0" prefWidth="140.0" text="1">
                           <FlowPane.margin>
                              <Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
                           </FlowPane.margin>
                           <font>
                              <Font name="System Bold" size="24.0" />
                           </font>
                        </Button>
                        <Button layoutX="12.0" layoutY="12.0" mnemonicParsing="false" prefHeight="121.0" prefWidth="140.0" text="2">
                           <FlowPane.margin>
                              <Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
                           </FlowPane.margin>
                           <font>
                              <Font name="System Bold" size="24.0" />
                           </font>
                        </Button>
                        <Button layoutX="10.0" layoutY="135.0" mnemonicParsing="false" prefHeight="121.0" prefWidth="140.0" text="3">
                           <FlowPane.margin>
                              <Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
                           </FlowPane.margin>
                           <font>
                              <Font name="System Bold" size="24.0" />
                           </font>
                        </Button>
                        <Button layoutX="154.0" layoutY="10.0" mnemonicParsing="false" prefHeight="121.0" prefWidth="140.0" text="4">
                           <FlowPane.margin>
                              <Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
                           </FlowPane.margin>
                           <font>
                              <Font name="System Bold" size="24.0" />
                           </font>
                        </Button>
                     </children>
                  </FlowPane>
               </content>
            </ScrollPane>
         </children>
      </AnchorPane>
   </children>
</AnchorPane>

HelloController.java:

package com.example.demo;

import javafx.fxml.FXML;
import javafx.scene.control.Label;

public class HelloController {
    @FXML
    private Label welcomeText;

    @FXML
    protected void onHelloButtonClick() {
        welcomeText.setText("Welcome to JavaFX Application!");
    }
}

HelloApplication.java

package com.example.demo;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.stage.Stage;

import java.io.IOException;

public class HelloApplication extends Application {
    @Override
    public void start(Stage stage) throws IOException {
        FXMLLoader fxmlLoader = new FXMLLoader(HelloApplication.class.getResource("hello-view.fxml"));
        Scene scene = new Scene(fxmlLoader.load(), 320, 240);
        stage.setTitle("Hello!");
        stage.setScene(scene);
        stage.show();
    }

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

HelloApplication.java and HelloController.java are default demo files from starting project in JavaFX.

Conclusion: Is there a combination of properties for ScrollPane and FlowPane to be able to resize the FlowPane vertically and stop resizing in the right when the control inside tries to move to create a new column (this happens when ScrollPane resize vertically)? I don't want to create those invisible columns in the right, instead grows the FlowPane vertically! Mention: this could happens in two way

  1. when resize form from the bottom, and the controls from the bottom of the FlowPane will move to the top, and the FlowPane will resize automatically to the right and put the controls in the hidden area of FlowPane, (and)
  2. When you resize form horizontally and there is no more space to move the controls from the right column to the next row, so the FlowPane will not stay anchored to right and to try to create an "invisible" row (or as many as it takes to move needed controls down).

I hope I make it clear as possible.

1

There are 1 best solutions below

3
On

This answer I found it myself and it works only for FlowPane containing controls with the same size.

Purpose: Allow user to populate a FlowPane in Vertical orientation, with parent ScrollPane's scrollbars set only to vertical scrollbar visible (when needed) and the FlowPane width to fit ScrollPane's width always.

Starting from a demo project in JavaFX, this is the fxml file which contains definition for the main-view (hello-view.fxml in this case).

hello-view.fxml file:

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ScrollPane?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.FlowPane?>
<?import javafx.scene.text.Font?>

<AnchorPane prefHeight="424.0" prefWidth="457.0" xmlns="http://javafx.com/javafx/19" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.example.demo.HelloController">
   <children>
      <AnchorPane layoutX="14.0" layoutY="14.0" prefHeight="399.0" prefWidth="430.0" style="-fx-border-color: #555555;" AnchorPane.bottomAnchor="14.0" AnchorPane.leftAnchor="14.0" AnchorPane.rightAnchor="14.0" AnchorPane.topAnchor="14.0">
         <children>
            <ScrollPane fx:id="scrollPane" fitToWidth="true" focusTraversable="false" hbarPolicy="NEVER" layoutX="14.0" layoutY="13.0" maxHeight="1.7976931348623157E308" maxWidth="-Infinity" prefHeight="377.0" prefWidth="404.0" style="-fx-border-color: red; -fx-border-width: 0;" AnchorPane.bottomAnchor="7.0" AnchorPane.leftAnchor="13.0" AnchorPane.rightAnchor="12.0" AnchorPane.topAnchor="12.0">
               <content>
                  <AnchorPane fx:id="ancFlow">
                     <children>
                        <FlowPane fx:id="flowPane" orientation="VERTICAL" prefHeight="368.0" prefWidth="396.0" prefWrapLength="10.0" rowValignment="TOP" style="-fx-border-color: green; -fx-border-width: 0;" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
                           <children>
                              <Button maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" mnemonicParsing="false" prefHeight="121.0" prefWidth="140.0" text="1">
                                 <FlowPane.margin>
                                    <Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
                                 </FlowPane.margin>
                                 <font>
                                    <Font name="System Bold" size="24.0" />
                                 </font>
                              </Button>
                              <Button layoutX="12.0" layoutY="12.0" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" mnemonicParsing="false" prefHeight="121.0" prefWidth="140.0" text="2">
                                 <FlowPane.margin>
                                    <Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
                                 </FlowPane.margin>
                                 <font>
                                    <Font name="System Bold" size="24.0" />
                                 </font>
                              </Button>
                              <Button layoutX="10.0" layoutY="135.0" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" mnemonicParsing="false" prefHeight="121.0" prefWidth="140.0" text="3">
                                 <FlowPane.margin>
                                    <Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
                                 </FlowPane.margin>
                                 <font>
                                    <Font name="System Bold" size="24.0" />
                                 </font>
                              </Button>
                              <Button layoutX="154.0" layoutY="10.0" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" mnemonicParsing="false" prefHeight="121.0" prefWidth="140.0" text="4">
                                 <FlowPane.margin>
                                    <Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
                                 </FlowPane.margin>
                                 <font>
                                    <Font name="System Bold" size="24.0" />
                                 </font>
                              </Button>
                           </children>
                        </FlowPane>
                     </children>
                  </AnchorPane>
               </content>
            </ScrollPane>
         </children>
      </AnchorPane>
   </children>
</AnchorPane>

HelloController.java file:

package com.example.demo;

import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.FlowPane;

import java.net.URL;
import java.util.ResourceBundle;

public class HelloController implements Initializable {
    private final long buttonWidth = 144;
    private final long buttonHeight = 125;
    private double columns;
    private double rows;
    @FXML
    private AnchorPane ancFlow;

    @FXML
    private ScrollPane scrollPane;

    @FXML
    private FlowPane flowPane;

    @Override
    public void initialize(URL url, ResourceBundle resourceBundle) {
        // next line was used to see how ancPane is resizing
        //ancFlow.setBackground(new Background(new BackgroundFill(Color.rgb(220, 120, 120), new CornerRadii(0), new Insets(0))));
    }

    public void resizeFlowPaneParent(){
        scrollPane.heightProperty().addListener(new ChangeListener<Number>() {
            @Override
            public void changed(ObservableValue<? extends Number> observableValue, Number oldValue, Number newValue) {
                if(scrollPane.getHeight() < buttonHeight) return;
                double verticalPadding = scrollPane.getPadding().getTop() + scrollPane.getPadding().getTop();
                ancFlow.setPrefWidth(scrollPane.getWidth() - (scrollPane.getWidth() - scrollPane.getViewportBounds().getWidth()));
                int controls = flowPane.getChildren().size();
                rows = Math.floorDiv(newValue.longValue() , buttonHeight + (long)verticalPadding);
                if((long)rows == 0) return;
                columns = Math.ceilDiv(controls, (long)(rows));
                double matchColumns = Math.floorDiv((long)(scrollPane.getWidth()- (scrollPane.getWidth() - scrollPane.getViewportBounds().getWidth())), buttonWidth);
                if (columns <= matchColumns) {
                    if(rows * buttonHeight - (long)verticalPadding <= scrollPane.getHeight() - verticalPadding || (rows * columns * buttonHeight ) > controls * buttonHeight )
                    {
                        ancFlow.setPrefHeight(scrollPane.getHeight()-verticalPadding);
                    }
                    else
                    {
                        ancFlow.setPrefHeight(rows * buttonHeight);
                    }
                }
                else
                {
                    double matchRows = Math.ceilDiv(controls, (long)matchColumns);
                    ancFlow.setPrefHeight(matchRows * buttonHeight);
                }
            }
        });
        scrollPane.widthProperty().addListener(new ChangeListener<Number>() {
            @Override
            public void changed(ObservableValue<? extends Number> observableValue, Number oldValue, Number newValue) {
                if(newValue.longValue() - (newValue.longValue() - scrollPane.getViewportBounds().getWidth()) < buttonWidth) return;
                double verticalPadding = scrollPane.getPadding().getTop() + scrollPane.getPadding().getTop();
                ancFlow.setPrefWidth(newValue.longValue() - (newValue.longValue() - scrollPane.getViewportBounds().getWidth()));
                int controls = flowPane.getChildren().size();
                columns = Math.floorDiv(newValue.longValue() - (long)(newValue.longValue() - scrollPane.getViewportBounds().getWidth()), buttonWidth);
                rows = Math.ceilDiv(controls, (long)columns);
                if(rows * buttonHeight < scrollPane.getHeight())
                {
                    ancFlow.setPrefHeight(scrollPane.getHeight()-verticalPadding);
                }
                else
                {
                    ancFlow.setPrefHeight(rows * buttonHeight);
                }
            }
        });
    }
}

HelloApplication.java file:

package com.example.demo;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.stage.Stage;

import java.io.IOException;

public class HelloApplication extends Application {
    @Override
    public void start(Stage stage) throws IOException {
        FXMLLoader fxmlLoader = new FXMLLoader(HelloApplication.class.getResource("hello-view.fxml"));
        Scene scene = new Scene(fxmlLoader.load(), 320, 240);
        stage.setTitle("Hello!");
        stage.setScene(scene);
        stage.show();
        ((HelloController)fxmlLoader.getController()).resizeFlowPaneParent();
        stage.setWidth(stage.getWidth() + 1);
        stage.setHeight(stage.getHeight() + 1);
    }

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

The approach was to wrap the FlowPane in a AnchorPane. FlowPane is anchored in AnchorPane to all bounds. The AnchorPane is wrapped in ScrollPane.

In controller I've created a public method in which I added listners to widthProperty() and heightProperty() of ScrollPane to be able to calculate the rows needed for resizing the AnchorPane, based on how many items are in FlowPane and how many columns could be made with the ScrollPane's width. This method was added because size of controls must be read after the scene is displayed, which I've done it in HelloApplication.java main class. In this way listners are added after all items are displayed, and it will be calculated correctly.

Demo: enter image description here