JavaFX : Adapt component height to its children height? - javafx

I'm trying to develop a desktop app using javaFX (2.2), and I'm experiencing some difficulties to really get it to behave like I would like it to. Being a new user of the framework, I might not be using it as it should be...
What I want is a kind of dashbord, with several JavaFX or custom controls inside a ScrollPane. I currently have several views that look like this :
<AnchorPane id="userContent" xmlns:fx="http://javafx.com/fxml" fx:controller="squalp.view.OrgViewController">
<children>
<ScrollPane fx:id="scrollPane" styleClass="transparent" hbarPolicy="NEVER" vbarPolicy="AS_NEEDED" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
<content>
<VBox fx:id="vBox" styleClass="transparent" alignment="TOP_LEFT" focusTraversable="true">
<children>
<Label text="Label" />
<Button fx:id="bTest" text="Test" onAction="#test"/>
<TreeView fx:id="orgTree" focusTraversable="true" />
</children>
</VBox>
</content>
</ScrollPane>
</children>
</AnchorPane>
As long as I keep using simple controls, no problem, items are correctly placed one after another in th VBox.
But when using TreeViews or TableViews, I can only get my controls to have fixed height, meaning extra space on the bottom, or new scroll bars, in addition to the main one. What I would like to achieve is to adapt my controls to their current content height.
Qt's QML, for exemple, has a childrenHeight property, and it's kind of what I'm looking for. I figure this content height must be calculated somehow, since components scrollbars do adapt to the content.
I had to use a workaround for tableviews :
table.setPrefHeight(list.size() * rowHeight + headerHeight);
but I couldn't find a way to track displayed items on a treeview.
Has anyone an idea of what I could be doing to handle this issue ? Or I am really not using the framework as I should ?
Thanks in advance.
EDIT : Visual description of the problem :
Case 1
Here my treeview's height is set to a default value. There are not enough items to fill the treeview : if I want to put another component below, there'll be a gap; if my scene is not big enough, the ScrollPane's scrollbar appears, even though all the displayable content is visible.
Case 2
If if expand my treeview's items, there are now too many of them. They are not all visible at once, and the treeview's scrollbar appears, even though I already have a scrollable view with the scrollpane.
What I would like is my TreeView to adapt to its content height (reduced height if items are collapsed, bigger height if they are expanded), in order to be able to put several components one after another and to scroll through them all using the ScrollPane's scrollbar.
Since the TreeView component has itself a scrollbar, its visible content's height must be calculated at some point (since scrollbars basically use ratios based on contentHeight/componentHeight). This information doesn't seem to be available in the public API, but I wonder if, developping a custom class that expands TreeView, there could be a way to bind my component to its "visible height".

There maybe a proper way to achieve your goal, but I can suggest the approach similar to tableview's.
orgTree.prefHeightProperty().bind(orgTree.impl_treeItemCountProperty()
.multiply(treeRowHeight)); // treeRowHeight maybe 24.
Note that this is only a workaround where the deprecated and internal use-only method is used, which maybe removed in future releases.

I was looking for way to make TreeView auto-resize to fit its content, e.g. when a node is expanded or collapsed. Here's what I eventually do. Note the Platform.runLater() is required, thus simple property binding would not work here. Also note the cellHeight is the height of a tree node. It's beyond the scope of the question here how to get that.
treeView.expandedItemCountProperty().addListener((obs, oldV, newV) -> {
Platform.runLater(() -> {
treeView.setPrefHeight(newV.intValue() * cellHeight);
});
});

You can use the binding property of the view to the childs height property. By this, whenever the height of the Parent changes, childs height will automatically change ! This is not automatic height adaption, like you want, but a forced adaption, which will help you !
Width property is also available
Basically this is a snippet that will help you :
pane.heightProperty().addListener(new ChangeListener<Number>() {
#Override
public void changed(ObservableValue<? extends Number> arg0,
Number oldValue, Number newValue) {
table.setPrefHeight(newValue - 200);/*The difference of height between
parent and child to be maintained*/
}
});

Related

JavaFX: HBox.setHgrow refuses to do its job

Taking this code, shouldn't the word "Label" be on the right side of the application window?
To be more specific about my problem: Why doesn't the HBox.setHgrow give the label all the horizontal space available?
package com.example.javafxwindow
import javafx.application.Application
import javafx.geometry.Pos
import javafx.scene.Scene
import javafx.scene.control.Label
import javafx.scene.layout.HBox
import javafx.scene.layout.Priority
import javafx.stage.Stage
class HelloApplication : Application()
{
override fun start(stage: Stage)
{
val label = Label("Label")
label.alignment = Pos.CENTER_RIGHT
HBox.setHgrow(label, Priority.ALWAYS)
val hbox = HBox(label)
val scene = Scene(hbox, 400.0, 400.0)
stage.scene = scene
stage.show()
}
}
fun main()
{
Application.launch(HelloApplication::class.java)
}
This is, slowly but surely, driving me nuts.
I don't know what to do else. Maybe I'm blind by now ... where's the error?
I would really appreciate any help.
Allowing a label to grow beyond its preferred size
The label is bounded by its maxWidth which defaults to its prefWidth.
You want to make the maxWidth unbounded, so call:
label.setMaxWidth(Integer.MAX_VALUE)
Different controls have different defaults for maxWidth. For buttons and labels the maxWidth default to their prefWidth. Unless you reset the maxWidth these controls won't grow; even if you provide hints that you might think would make them do so.
Quoting the Oracle JavaFX layout sizing tutorial:
UI controls also provide default minimum and maximum sizes that are based on the typical usage of the control. For example, the maximum size of a Button object defaults to its preferred size because you don't usually want buttons to grow arbitrarily large. However, the maximum size of a ScrollPane object is unbounded because typically you do want them to grow to fill their spaces.
It would be nice if this detail of information was also in java doc of each control, so you would know which controls default their max size to the pref size. There is some information on this in the HBox java doc.
Spring alternative
An alternative is to insert a spring pane into the hbox before the label and set the hgrow on the spring pane instead. That will push the label all the way to the right. For info on that approach see:
How to create toolbar with left, center and right sections in javaFX?
Alternative layout
As noted by DaveB in the comments:
Perhaps, if you simply want the Label over to the right, using AnchorPane instead of HBox would be better. Just anchor the Label to the right side. Choosing the correct layout container can often be the key to retaining your sanity.
Example FXML
I don't write Kotlin, so I will provide a layout example with FXML instead and you can translate it to Kotlin if you wish.
The example sets the maxWidth of a label to a large number so that the label can grow to fill the available area, with the text in the label right aligned.
<HBox prefHeight="50.0" prefWidth="150.0" xmlns="http://javafx.com/javafx/18" xmlns:fx="http://javafx.com/fxml/1">
<children>
<Label alignment="CENTER_RIGHT" maxWidth="1.7976931348623157E308" text="Label" HBox.hgrow="ALWAYS" />
</children>
</HBox>
FAQ
That worked. But I can see that's going to be hard to remember next time.
Just my opinion, others have different opinions:
It will probably be easier to remember than you may think. Once something like this bites you, it is difficult to forget.
If something is not resizing how you expect, just try setting the max width (and/or max height).
Use scene builder for prototyping such things, removing any default sizing it sets for layouts and providing sizing hints only where needed.
Scene Builder is quicker and easier for layout than writing things in code (for many, but not all tasks), plus the result is easier to maintain.
Do you happen to know why label.prefWidth=Double.MAX_VALUE did not work?
It only "produced" a "..." text instead of "Label" and it remained on the right side of the application window.
It doesn't make a lot of sense to prefer something to be an infinite size, you could never see it at the preferred size.
It probably confused the layout algorithm. I'm guessing it sized the label to the preferred size, then needed to round it up to snap it for pixel alignment or add to the margin. This would cause a double overflow, making the preferred size negative, which doesn't make sense. So it would try to size to the minimum size instead. When there is not enough space to display a value, a label will elide the text (replacing characters with ...), which is what it did here. The default minimum size will be just enough to display the elided text.
If you also set minSize to 100 and the prefSize to Double.MAX_VALUE, the label will show all of the text aligned within the minSize, which indicates that it is the minimum size that is being respected in such a case.
Anyway, don't set the prefWidth to Double.MAX_VALUE. Instead, set the maxWidth to some large number so that the field can grow to fit it (e.g. Integer.MAX_VALUE).

Binding properties in FXML with numerical operators causing "Can't resolve symbol" error

I am trying to bind the layoutX property of a label to half the width of a pane using FXML in JavaFX 12. When I use the binding expression on the property width with the / operator I receive the error Can't resolve symbol width / 2. How would i fix this?
Originally I had my form set in a VBox where I could set the alignment. However I am pretty used to setting positions by myself instead of giving control of the layout to the VBox. So I am trying to set the position myself.
I am able to achieve the desired result inside my java class with: label.layoutXProperty.bind(pane.widthProperty().divide(2)).
I am trying to bind the property in FXML to keep my classes smaller. Currently I have the following code:
<Pane fx:controller="jotGenerator.JotController"
xmlns:fx="http://javafx.com/fxml"
fx:id="contents">
<padding><Insets topRightBottomLeft="50"/></padding>
<Label text="JOT" layoutX="${contents.width / 2}"/>
</Pane>
The issue appears on this line <Label text="JOT" layoutX="${contents.width / 2}"/>
I am expecting this to bind the horizontal layout position to the half the width of the pane. However i receive an error in IntelliJ saying Can't resolve symbol width / 2
Thanks for any help!
Ah don't worry, turns out the error comes up in IntelliJ but the FXML loader can find the property anyway. You can just ignore the message I suppose.

Set width of sap.m.MenuButton

I have a MenuButton https://openui5.hana.ondemand.com/explored.html#/entity/sap.m.MenuButton/samples
in a bar with long text property value.
I would like to set width of the MenuButton in order to show all the text.
If I set width property, I can set (reduce) length (2px, 5px, 15px...) but increasing over a threshold the button remain to a fixed max size.
It's kind of an overkill but if you want to avoid using custom CSS classes, you could wrap the MenuButton with a flexbox control (with renderType="Bare") and override max-width (or set min-width) by using FlexItemData as a layoutData.
<HBox renderType="Bare">
<MenuButton text="Hello World" width="600px">
<layoutData>
<FlexItemData maxWidth="100%"/>
</layoutData>
</MenuButton>
</HBox>
PS: The max-width of sap.m.MenuButton was indeed not documented. After filing an issue, the API reference now mentions that it has a max-width.
As per visual design this width can be maximum of 12rem (192px)

Is there a way to insert children at specific position?

thats a part of my fxml:
<VBox>
<children>
<Button/>
<Button/>
</children>
</VBox>
I want to add a new child to vBox. But i want to add it at a specific position, for example between the two buttons "at position 2".
The child list of a pane is an ObservableList, which is just a subinterface of a plain old java.util.List. So you have access to all the usual list methods. Assuming you have a reference to the VBox in your controller, call it vbox, you can just do
vbox.getChildren().add(1, myNewButton);

What's the difference (in FXML) between layout.Pane and control.Control?

I am trying to learn how to create a UI. What I am currently experimenting with is a main screen on the left to house the current information for the user to see; and an Accordion on the right, for the user to select items which determine what is shown in the main screen. I am using Scene Builder to help me as I learn. When I drag a TitledPane (not empty) into my Accordion, it places an AnchorPane in there. This, unless I misunderstand something, does not allow for a list of selectable items in that TitledPane larger than the current height of the TitledPane. So I added another TitledPane (empty) into my Accordion, and then added a ScrollPane (not empty) to it. This again added an AnchorPane inside of the ScrollPane.
This is what my fxml document currently looks like (after the versioning, encoding and imports):
<VBox fx:id="testPane" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="testfxml.FXMLDocumentController">
<children>
<MenuBar>
<menus>
<Menu text="File">
<items>
<MenuItem fx:id="1" text="1" />
<MenuItem fx:id="2" text="2" />
<MenuItem fx:id="3" text="3" />
</items>
</Menu>
</menus>
</MenuBar>
<SplitPane dividerPositions="0.75" prefHeight="600" prefWidth="800">
<items>
<StackPane fx:id="mainscreen" />
<Accordion fx:id="test">
<panes>
<TitledPane text="Test1">
<content>
<AnchorPane fx:id="test1" />
</content>
</TitledPane>
<TitledPane text="Test2">
<content>
<ScrollPane>
<content>
<AnchorPane/>
</content>
</ScrollPane>
</content>
</TitledPane>
</panes>
</Accordion>
</items>
</SplitPane>
</children>
When I look at the api for javafx (https://docs.oracle.com/javase/8/javafx/api), AnchorPane shows this:
Class AnchorPane
java.lang.Object
javafx.scene.Node
javafx.scene.Parent
javafx.scene.layout.Region
javafx.scene.layout.Pane
javafx.scene.layout.AnchorPane
While ScrollPane shows this:
Class ScrollPane
java.lang.Object
javafx.scene.Node
javafx.scene.Parent
javafx.scene.layout.Region
javafx.scene.control.Control
javafx.scene.control.ScrollPane
I'm assuming the formatting, which to me implies inheritance, is significant. I noticed that down through "javafx.scene.layout.Region" is identical, but that they differ after that. One goes to layout.Pane, the other to control.Control.
I've read through the descriptions on the api and, perhaps because of my obvious lack of experience with coding, it didn't quite make sense to me - perhaps I'm trying to use one of them incorrectly?
What's the difference? Is an AnchorPane required?
You can put anything you like as the content of a TitledPane. SceneBuilder (which seems unduly fond of AnchorPanes, for some reason) puts an AnchorPane as the default content; however you can remove it, or just choose "TitledPane (empty)" and add your own content. For example, you can remove the AnchorPane (or start with an empty TitledPane) and drag a ScrollPane there instead.
It actually sounds like you really need a ListView as the content of your TitledPane. A ListView comes already equipped with as-needed scroll bar functionality.
To answer your actual question, the API documentation is indeed indicating the inheritance hierarchy.
JavaFX (like most UI libraries) defines a rich inheritance structure. Parent is the superclass of anything in the scene graph that contains other Nodes. The vast majority of classes that subclass Parent also subclass Region, which represents something that takes up a particular space in the layout. (The exceptions are Group, which behaves somewhat differently with respect to layout, and WebView, which probably should be a subclass of Control, but isn't...).
Control and Pane are both subclasses of Region (and hence of Parent). Pane represents a "container": something that is merely designed to hold other Nodes and manage their layout. AnchorPane is a subclass of Pane. Control, by contrast, represents a UI "widget" (something with which the user directly interacts), so Label, Button, ComboBox, etc, are all subclasses of Control.
Some controls are actually quite complex, and contain other Nodes. ScrollPane is an example of a control, because the user can interact with it (via its scroll bars); it also contains another Node, called its "content". In the same way that you can use any Node as the content for the TitledPane, you can use any node as the content for the ScrollPane. The ScrollPane's content is the node the user views and moves around via the ScrollPane's scroll bars. Again, you can use any Node as the ScrollPane's content; SceneBuilder just chooses an AnchorPane as the default (for no real reason other than it has to use something).

Resources