1

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.

Vali Maties
  • 94
  • 1
  • 8
  • 1
    [mcve] please .. – kleopatra Nov 11 '22 at 15:17
  • Ok, I will try to substract this problem from my app and I will edit soon the question, sorry for that. – Vali Maties Nov 11 '22 at 17:49
  • 1
    I've added the minimal code for reproduction – Vali Maties Nov 11 '22 at 21:27
  • Your problem (or one of them) is the [orientation](https://openjfx.io/javadoc/17/javafx.graphics/javafx/scene/layout/FlowPane.html#orientationProperty) property of the FlowPane, which you have set to VERTICAL, which means "A vertical flowpane lays out children top to bottom, wrapping at the flowpane's height.", but the height has no meaning because it is in a scrollpane. Instead it will wrap based on [prefWrapLength](https://openjfx.io/javadoc/17/javafx.graphics/javafx/scene/layout/FlowPane.html#prefWrapLengthProperty), set that to infinity and it will never wrap. – jewelsea Nov 11 '22 at 23:56
  • I could be wrong, but I think you might need to create a custom layout pane (i.e. write your layout extending Pane or Region) to achieve the exactly the layout behaviour you want. – jewelsea Nov 11 '22 at 23:59
  • 1
    Off topic, but try not to use so many hard-coded values (pref width/height) and manual layout like AnchorPane. For instance your sample layout could be create using StackPanes with inset padding rather than AnchorPanes with hardcoded sizes, layout co-ordinates and anchors. – jewelsea Nov 12 '22 at 00:01
  • Thanks @jewelsea, yes indeed, seems to be a weird orientation, but I've saw this tyof orientation in a software made by a mondial hardware/software brand. It's weird, but in the same time it has a logic regarding to parent's ScrollBars visibility. Think about it, if the horizontal scrollbar is set to `Never`, but user wants to fill the `FlowPane` in vertical orientation (like in my situation), then there is no logic to resize the `FlowPane` to the right. In the next comment I'll post how I've did it finally, but it works only for my case, with same controls size. – Vali Maties Nov 12 '22 at 07:39
  • I've wrapped the `FlowPane` in an `AnchorPane` and I've set it to anchor to all locations (top, bottom, left and right). The `AnchorPane` then is wrapped in the `ScrollPane`. I've added Listners to `ScrollPane`'s widthProperty and heightProperty and calculate the height of the AnchorPane depending on number of controls of FlowPane and how many columns and row could be creating on scrollpane widht. I don't know if I have to post this as an answer as long as this is limited to same size controls in `FlowPane`. – Vali Maties Nov 12 '22 at 07:53
  • You don’t have to post answer, but you could, it [is encouraged](https://stackoverflow.com/help/self-answer). I do think this is a case where a custom layout pane would be preferred, but that is tricky if you have never done it before and there is no official documentation on how to do it. The approach you have taken while maybe not optimal, seems reasonable. – jewelsea Nov 12 '22 at 09:04
  • hmm .. not near my IDE right now, will try later, but for now a couple of comments: the most important __never-ever__ hard-code sizing constraints, doing so will effectively prevent a layout from doing its job (no calculations at all) plus a thingy I don't quite understand: fitTo for both width and height: doesn't that disable the scrollbars always? – kleopatra Nov 12 '22 at 10:34
  • No @kleopatra, even if fitWidth and fitHeight are set, the `FlowPane` will resize if the content does not fit with the current orientation and bounds. Make test to see it is like that :) – Vali Maties Nov 12 '22 at 10:44

1 Answers1

-1

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

Vali Maties
  • 94
  • 1
  • 8
  • sry but that's a -1 for hard-coding sizing constraints: whatever the problem, hard-coded constraints are __not__ the solution! – kleopatra Nov 12 '22 at 11:07
  • also: all deeply nested layouts (particularly if one of the collaborators is an AnchorPane) are bound to pose problems, as are bindings to parent sizes. A very quick check: flowPane with horizontal orientation, containing enough nodes (f.i. buttons) as content of a scrollPane with fitToWidth (_not_ fitToHeight), contained in a borderPane, no hard-coded sizing constraints anywhere seem to behave as you want it: comes up with two column (at default scene width), on decreasing width switching to single column and vertical scrollbar – kleopatra Nov 12 '22 at 11:18
  • Probably, but till someone (maybe even you, as long as you downvoted) came with a solution, with exactly how you wrote "minimal reproduction code", I will use something else. Till then, this solution works, and works very well. And, BTW, those nested AnchorPanes are there because contains other controls too, as I said it was extracted from my app :) – Vali Maties Nov 12 '22 at 11:55