JavaFX supports automatic self-adjusting layout that resizes controls along with the containing window, but this may require some non-obvious changes to the controls’ default properties. The problem I recently ran into was automatically resizing a ListView
within a VBox
. This article describes the undesirable default behavior and its correction, based on JavaFX 2.2 in JDK 7. It might also prove useful for similar issues involving other containers.
Demonstration
I wrote a tiny JavaFX program, ListViewSizing.java, to demonstrate the problem and the various stages of its solution. The following screenshots were taken on Windows 8.1 at 120 DPI (125%). The layout issues exist independently of DPI settings, but I found they appear most obviously and conveniently at 120 DPI. Since we’re interested in window resizing, I took two screenshots of the same eight window (Stage
) configurations, with all windows in the second screenshot twice the height of those in the first.
Click on each image to show a full-size version in a new window
What are we looking at? From left to right, we start with the unsatisfactory default property values (A1/B1), then change two different properties (A2-3/B2-3), until we achieve our desired layout behavior (A4/B4). Here’s the structure of each window:
- The (invisible) root node is a
VBox
with two children. - The top child is a simple
Text
in A1-4, and the sameText
wrapped in aTabPane
in B1-4. - The bottom child is a
ListView
which should fill the entire rest of the window. - The “Height” shown at the top is the actual height of the
ListView
, i.e.getHeight
. - The “prefHeight” is its preferred height used in layout calculations, i.e.
getPrefHeight
. - And “Vgrow” is the grow priority of the
ListView
within theVBox
, i.e.VBox.getVgrow
.
In the following analysis, I’ll use “short” and “tall” to reference the windows as shown in the top and bottom screenshot, respectively, e.g. “A1 (short)” versus “A1 (tall)”.
Analysis
In the first column, only A1 (short) is correct. The default property values cause various layout failures in every other case.
- A1 (tall) – the
ListView
stops growing at 400 pixels, leaving empty space. - B1 (short) – the
ListView
hides theTabPane
entirely! - B1 (tall) – the
ListView
still partially overlaps theTabPane
.
Obviously, the ListView
’s default preferred height (USE_COMPUTED_SIZE
) doesn’t work too well. But merely setting it to zero is no solution, either. As the second column shows, the ListView
simply disappears in this case, with an actual height of two pixels.
In the third column, we leave the preferred height alone and instead set the VBox
grow priority to ALWAYS
. This fixes one case, namely A3 (tall) where the ListView
now correctly stretches to the bottom of the window. The faulty B3 cases with the hidden or overlapped TabPane
remain unchanged, though.
The fourth column combines a preferred height of zero with a grow priority of ALWAYS
– and now everything works. The plain Text
variant in A4 appears as before, but the TabPane
in B4 is finally properly cleared at both window sizes. In the tall versions, the actual ListView
height difference between B1/3 and B4 is small – 396 versus 392 pixels – but that was large enough to cause an ugly overlap.
Bonus Bug. You might try to encourage a ListView
to occupy the maximum available size by setting its preferred size to Double.MAX_VALUE
. Not only does this not work – it will lock up the application! JavaFX apparently actually tries to allocate a ListView
of infinite height…
Causes
First, where does the maximum height of 400 pixels in A1 (tall) come from? Digging through the JDK library jfxrt.jar
with Emmanuel Dupuy’s Java Decompiler, I found the method computePrefHeight
in class com.sun.javafx.scene.control.skin.ListViewSkin
which simply returns the constant value 400. The companion method computePrefWidth
takes that method’s result and multiplies it with 0.618033987. These values are clearly arbitrary but I’m not sure how to improve on them, since a ListView
by its very nature is not tied to some fixed or natural size. I think a ListView
should never supply a default preferred size in the first place.
The ListView
overlapping the TabPane
in the B rows is a tougher question. In the short samples, why does a largely empty ListView
completely hide a TabPane
with fixed text content? That looks like an error in the JavaFX layout algorithm to me. In the tall samples, the mysterious height of 396 pixels is just halfway between the correct height of 392 pixels and the hard-coded preferred height of 400 pixels. Was the layout algorithm trying to find a compromise between correct and preferred size? At any rate, the TabPane
evidently doesn’t push back hard enough in the internal layout calculations.
Summary
Whatever the causes, the lessons for ListView
users are clear enough.
- The “computed” default value for the preferred size is useless. Either specify your own fixed size if you have one, or set the preferred size to zero. Otherwise it may cause undesirable interference with layout calculations.
-
To correctly have a
ListView
grow with its container, look for the corresponding property on the container itself. If none exists you will have to directly bind theListView
’s size properties to the container size.
Once you know what to do it’s simple enough. I do wish we’d get the obviously desirable and sensible behavior by default: grow to all available space not otherwise occupied, but no further. It would be great if that could be fixed in a future JavaFX version.
I am encountering the same problem, but with a twist. I have a VBox, which contains in order, a Label, a ListView, another Label, another ListView. Ideally, each listview would only grow to be as long as needed to fit its items. I have tried:
Which results in two equally sized ListViews, each filling about half the vertical space, minus the label space. This is surprising to me, since I expected the entire VBox to be no bigger than the contents, but the ListViews expand vertically to fill the space.
I then tried:
Hoping the bottom ListView would expand to fill any whitespace. However, this squashes the top ListView to nothing. Setting a prefHeight=0 on both has no effect. Setting a prefHeight to any other value of course means it’s no longer dynamically sized.
Any idea on how to make this work, or do I need to abandon ListView ?
Vaughn, none of your code samples made it through. All I see in the edit view is whitespace. Please try plain text only.
Regarding the overlapping vbox children, I suspect the likely reason is that the combined pref-height of the vbox children would be too big to fit. In this case the vbox layouting must somehow distribute the available space to its children, shrinking them over their pref-height. Unlike vgrow there is no vshrink priority. This is a known gap. See
[#RT-37309] [VBox, HBox, GridPane]: hshrink/vshrink layout hint similar to vgrow
https://javafx-jira.kenai.com/browse/RT-37309
Good observation. Yes, a vshrink policy for children falling under preferred size would be useful. I voted for your proposal, maybe someone will take note.
Great blog entry. This saved my day :-) I ran into the overlapping problem and searched the whole day for a solution.
Thanks, always nice to hear people found my posts useful!
Good analysis! Had the same issues with TreeView (which has the same strange fixed 400px height – welcome c&p code ;)). I fixed it by setting prefHeight to a high value like 1000.
Ha! Someone must have defined 400px as DEFAULT_CONTAINER_HEIGHT in the JavaFX source code. Thanks for the TreeView tip!