javafxfxmljavafx-11

How to consistently dictate size for nested JavaFX controls?


I'm working on developing my programming skills and am building a UI application in JavaFX because Java is my strongest language.

Unfortunately I've found building the UI tedious at best using FXML. I decided to make my window a fixed size, thinking that a non-responsive UI would be easier to build for my first time.

Adding elements is easy, but getting them to be properly sized has proven very difficult, especially when layouts are nested (IE a TiledPane within an HBox). I think I'm misunderstanding how children inherit properties from their parent, or vice versa.

In my application, I've nested frequently because the data served to the view can take several seconds to load, and I didn't want to burden users with frequent load screens.

For example, this is the FXML for a screen which displays data based on which button you press. My window is 1600x900. I'd like the elements in the second HBox to fill the rest of the screen, but nothing I seem to adjust causes them to grow. There are other elements I'd like to resize but I think that will be easier once I understand whatever principle I'm missing.

<VBox fx:id="detachmentView" visible="false">
                <HBox alignment="CENTER">
                    <Button fx:id="detachmentRules" text="Rules" styleClass="medium_button" onAction="#detachmentRulesButton"/>
                    <Button fx:id="detachmentStratagems" text="Stratagems" styleClass="medium_button" onAction="#detachmentStratagemButton"/>
                    <Button fx:id="detachmentEnhancements" text="Enhancements" styleClass="medium_button" onAction="#detachmentEnhancementButton"/>
                </HBox>
                <Separator/>
                <HBox>
                    <HBox maxWidth="666">
                        <VBox>
                            <Label text="Detachment Name:" styleClass="detachment_header"/>
                            <ListView fx:id="detachmentList" onMouseClicked="#detachmentListSelect" styleClass="detachment_list" prefWidth="333"/>
                        </VBox>
                        <VBox fx:id="stratagemHeader" visible="false" managed="false">
                            <Label text="Stratagem Name:"  styleClass="detachment_header"/>
                            <ListView fx:id="stratagemList" onMouseClicked="#stratagemListSelect" styleClass="detachment_list" prefWidth="333"/>
                        </VBox>
                        <VBox fx:id="enhancementHeader" visible="false" managed="false">
                            <Label text="Enhancement Name:" styleClass="detachment_header"/>
                            <ListView fx:id="enhancementList" onMouseClicked="#enhancementListSelect" styleClass="detachment_list" prefWidth="333"/>
                        </VBox>
                    </HBox>
                    <StackPane>
                        <StackPane fx:id="detachmentRulesView">
                            <VBox>
                                <Label fx:id="detachmentLabel" styleClass="detachment_header_big"/>
                                <VBox fx:id="dynamicVBox"/>
                            </VBox>
                        </StackPane>
                        <StackPane fx:id="detachmentStratagemView" visible="false">
                            <BorderPane>
                                <center>
                                    <VBox>
                                        <Label fx:id="stratagemID" styleClass="detachment_header"/>
                                        <Label fx:id="stratagemType" styleClass="detachment_header"/>
                                        <Label fx:id="stratagemCost" styleClass="detachment_header"/>
                                        <Label fx:id="stratagemPhase" styleClass="detachment_header"/>
                                        <WebView fx:id="stratagemRules" style="-fx-pref-height: 250;"/>
                                        <TextArea fx:id="stratagemLegend" wrapText="true" editable="false"/>
                                    </VBox>
                                </center>
                            </BorderPane>
                        </StackPane>
                        <StackPane fx:id="enhancementView" visible="false">
                            <BorderPane>
                                <center>
                                    <VBox>
                                        <Label fx:id="enhancementName" styleClass="detachment_header"/>
                                        <Label fx:id="enhancementCost" styleClass="detachment_header"/>
                                        <WebView fx:id="enhancementRules" style="-fx-pref-height: 250;"/>
                                        <TextArea fx:id="enhancementLegend" wrapText="true" editable="false"/>
                                    </VBox>
                                </center>
                            </BorderPane>
                        </StackPane>
                    </StackPane>
                </HBox>
            </VBox>

It yields a result like this: FXML ELement Sizing

I have read the JavaFX documentation, several websites dedicated to JavaFX, this forum, and ChatGPT. I've tried adding various modifiers like prefHeight, maxHeight, minHeight, VBox.vgrow on many different parents and children for the relevant elements to no avail. I've used these elsewhere successfully so I'm not sure what I'm doing wrong.

It is also entirely possible that this is a crappy way to build a UI and I'll need to refactor.


Solution

  • Fixes for the immediate problem

    I'd like the elements in the second HBox to fill the rest of the screen

    First note that the HBox already fills the rest of the screen. You can see this if you color its background, e.g. with <HBox style='-fx-background-color:skyblue;'>

    The documentation for HBox tells you how the layout strategy for the HBox works:

    HBox will resize children (if resizable) to their preferred widths ... . If an hbox is resized larger than its preferred width, by default it will keep children to their preferred widths, leaving the extra space unused. If an application wishes to have one or more children be allocated that extra space it may optionally set an hgrow constraint on the child. See "Optional Layout Constraints" for details.

    The two child nodes of the HBox are another HBox and a StackPane. When there is more space than these need, the HBox will just size them to their preferred sizes and leave the rest of the space empty. You can allocate extra space to the child nodes by setting the hgrow constraint, but you need to specify which node(s) get the extra space. For example, to assign it only to the StackPane, you can add HBox.hgrow="ALWAYS" to the declaration of the StackPane:

    <VBox fx:id="detachmentView">
        <HBox alignment="CENTER">
            <!-- ... -->
        </HBox>
        <Separator />
        <HBox>
            <HBox maxWidth="666">
                <!-- ... -->
            </HBox>
            <StackPane HBox.hgrow="ALWAYS">
                <!-- ... -->
            </StackPane>
        </HBox>
    </VBox>
    

    You can achieve the same using a BorderPane:

    <VBox fx:id="detachmentView">
        <HBox alignment="CENTER">
            <!-- ... -->
        </HBox>
        <Separator />
        <BorderPane>
            <left>
                <HBox maxWidth="666">
                    <!-- ... -->
                </HBox>
            </left>
            <center>
                <StackPane>
                    <!-- ... -->
                </StackPane>
            </center>
        </BorderPane>
    </VBox>
    

    This works because the layout strategy for a BorderPane allocates extra space to the center. Again, referring to the documentation:

    The top and bottom children will be resized to their preferred heights and extend the width of the border pane. The left and right children will be resized to their preferred widths and extend the length between the top and bottom nodes. And the center node will be resized to fill the available space in the middle. Any of the positions may be null.

    In this case, the top, bottom, and right are all null, so they all take up zero space. The <HBox> in the left takes its preferred width and the rest of the space is allocated to the <StackPane>.

    How the layout system works

    The package documentation for javafx.scene.layout describes the general layout mechanism.

    The scene graph layout mechanism is driven automatically by the system once the application creates and displays a Scene. The scene graph detects dynamic node changes which affect layout (such as a change in size or content) and calls requestLayout(), which marks that branch as needing layout so that on the next pulse, a top-down layout pass is executed on that branch by invoking layout() on that branch's root. During that layout pass, the layoutChildren() callback method will be called on each parent to layout its children.

    In a standalone desktop application written in JavaFX, a window contains exactly one Scene. The size of the Scene is determined by the window; essentially filling all of the space in the window except for the "decorations" (title bar, window buttons, border).

    The Scene has exactly one root node, of type Parent, and the root is sized to fill the whole scene, regardless of the specific Parent subtype.

    When the scene is laid out, the root is assigned its size (the size of the scene), and then its layout() method is invoked, which in turn invokes its layoutChildren() method. Layout proceeds in a "top down" manner. This means the root node will query its child nodes for their preferred, minimum, and maximum sizes (if they are resizable), and allocate size and position to each of them based on the layout strategy, available space, preferred size of the child node, and any settings (such as the hgrow setting used above).

    Then for each child which is also a Parent instance, layout() (and consequently layoutChildren()) is invoked on each in turn.

    The upshot of all of this is that the root is allocated a size, from which it positions and sizes each immediate child node. And then having been allocated a size, each child node performs its layout.

    This is actually extremely well-suited to the "nested" design you describe, and is actually designed for this set up and for responsive UIs.

    The key to using this successfully is to be aware of all the pre-defined layout panes and roughly how they work. All of these are in the javafx.scene.layout package (whose documentation is linked above). There is also a tutorial.

    Typically, layout panes will attempt to resize their child nodes to their preferred sizes, and most of them will respect the minimum and maximum sizes whenever possible. The documentation for each layout pane describes how its min/pref/max sizes are computed and how it uses those of its child nodes to lay them out.

    Specific comments on your question and the code

    Unfortunately I've found building the UI tedious at best using FXML.

    There is no need to use FXML in a JavaFX application if you don't want. You can create the UI entirely in Java if you prefer. There are pros and cons to using FXML:

    Pros:

    Cons:

    I decided to make my window a fixed size, thinking that a non-responsive UI would be easier to build for my first time.

    This may not be true; as mentioned above the layout mechanism is specifically designed for responsive UIs, so by using a fixed window size you're effectively working against the API.

    Avoid any hard-coded sizes, such as maxWidth="666". Just occasionally you may have an element whose max size is set to its computed (preferred) size and you might set maxWidth="Infinity" to allow it to grow, if that is the desired effect. An example of this is shown here.

    Avoid using unnecessary panes. Your StackPane contains three StackPanes, each of which contains just one child node. This makes the three child StackPanes redundant. Consider replacing this:

    <VBox fx:id="detachmentView">
        <HBox alignment="CENTER">
            <!-- ... -->
        </HBox>
        <Separator />
        <HBox>
            <HBox maxWidth="666">
                <!-- ... -->
            </HBox>
            <StackPane HBox.hgrow="ALWAYS">
                <StackPane fx:id="detachmentRulesView">
                    <VBox>
                        <!-- -->
                    </VBox>
                </StackPane>
                <StackPane fx:id="detachmentStratagemView" >
                    <BorderPane>
                        <!-- -->
                    </BorderPane>
                </StackPane>
                <StackPane fx:id="enhancementView" >
                    <BorderPane>
                        <!-- -->
                    </BorderPane>
                </StackPane>
            </StackPane>
        </HBox>
    </VBox>
    

    with

    <VBox fx:id="detachmentView">
        <HBox alignment="CENTER">
            <!-- ... -->
        </HBox>
        <Separator />
        <HBox>
            <HBox maxWidth="666">
                <!-- ... -->
            </HBox>
            <StackPane HBox.hgrow="ALWAYS">
                <VBox fx:id="detachmentRulesView">
                    <!-- -->
                </VBox>
                <BorderPane fx:id="detachmentStratagemView">
                    <!-- -->
                </BorderPane>
                <BorderPane fx:id="enhancementView">
                    <!-- -->
                </BorderPane>
            </StackPane>
        </HBox>
    </VBox>