T
T
TornadoFX Guide
Search…
⌃K

7. Layouts and Menus

Complex UI's require many controls. It is likely these controls need to be grouped, positioned, and sized with set policies. Fortunately TornadoFX streamlines many layouts that come with JavaFX, as well as features its own proprietary Form layout.
TornadoFX also has type-safe builders to create menus in a highly structured, declarative way. Menus can be especially cumbersome to build using conventional JavaFX code, and Kotlin really shines in this department.

Builders for Layouts

Layouts group controls and set policies about their sizing and positioning behavior. Technically, layouts themselves are controls so therefore you can nest layouts inside layouts. This is critical for building complex UI's, and TornadoFX makes maintenance of UI code easier by visibly showing the nested relationships.

VBox

A VBox stacks controls vertically in the order they are declared inside its block (Figure 7.1).
vbox {
button("Button 1").setOnAction {
println("Button 1 Pressed")
}
button("Button 2").setOnAction {
println("Button 2 Pressed")
}
}
Figure 7.1
You can also call vboxConstraints() within a child's block to change the margin and vertical growing behaviors of the VBox.
vbox {
button("Button 1") {
vboxConstraints {
marginBottom = 20.0
vGrow = Priority.ALWAYS
}
}
button("Button 2")
}
You can use a shorthand extension property for vGrow without calling vboxConstraints().
vbox {
button("Button 1") {
vGrow = Priority.ALWAYS
}
button("Button 2")
}

HBox

HBox behaves almost identically to VBox, but it stacks all controls horizontally left-to-right in the order declared in its block.
hbox {
button("Button 1").setOnAction {
println("Button 1 Pressed")
}
button("Button 2").setOnAction {
println("Button 2 Pressed")
}
}
Figure 7.2
You can also call hboxconstraints() within the a child's block to change the margin and horizontal growing behaviors of the HBox.
hbox {
button("Button 1") {
hboxConstraints {
marginRight = 20.0
hGrow = Priority.ALWAYS
}
}
button("Button 2")
}
You can use a shorthand extension property for hGrow without calling hboxConstraints().
hbox {
button("Button 1") {
hGrow = Priority.ALWAYS
}
button("Button 2")
}

FlowPane

The FlowPane lays out controls left-to-right and wraps to the next line on the boundary. For example, say you added 100 buttons to a FlowPane (Figure 7.3). You will notice it simply lays out buttons from left-to-right, and when it runs out of room it moves to the "next line".
flowpane {
for (i in 1..100) {
button(i.toString()) {
setOnAction { println("You pressed button $i") }
}
}
}
Figure 7.3
Notice also when you resize the window, the FlowLayout will re-wrap the buttons so they all can fit (Figure 7.4)
Figure 7.4
The FlowLayout is not used often because it is often simplistic for handling a large number of controls, but it comes in handy for certain situations and can be used inside other layouts.

BorderPane

The BorderPane is a highly useful layout that divides controls into 5 regions: top, left, bottom, right, and center. Many UI's can easily be built using two or more of these regions to hold controls (Figure 7.5).
borderpane {
top = label("TOP") {
useMaxWidth = true
style {
backgroundColor += Color.RED
}
}
​
bottom = label("BOTTOM") {
useMaxWidth = true
style {
backgroundColor += Color.BLUE
}
}
​
left = label("LEFT") {
useMaxWidth = true
style {
backgroundColor += Color.GREEN
}
}
​
right = label("RIGHT") {
useMaxWidth = true
style {
backgroundColor += Color.PURPLE
}
}
​
center = label("CENTER") {
useMaxWidth = true
style {
backgroundColor += Color.YELLOW
}
}
}
FIGURE 7.5
You will notice that the top and bottom regions take up the entire horizontal space, while left, center, and right must share the available horizontal space. But center is entitled to any extra available space (vertically and horizontally), making it ideal to hold large controls like TableView. For instance, you may vertically stack some buttons in the left region and put a TableView in the center region (Figure 7.6).
borderpane {
left = vbox {
button("REFRESH")
button("COMMIT")
}
​
center = tableview<Person> {
items = listOf(
Person("Joe Thompson", 33),
Person("Sam Smith", 29),
Person("Nancy Reams", 41)
).observable()
​
column("NAME",Person::name)
column("AGE",Person::age)
}
}
Figure 7.6
BorderPane is a layout you will likely want to use often because it simplifies many complex UI's. The top region is commonly used to hold a MenuBar and the bottom region often holds a status bar of some kind. You have already seen center hold the focal control such as a TableView, and left and right hold side panels with any peripheral controls (like Buttons or Toolbars) not appropriate for the MenuBar. We will learn about Menus later in this section.

Form Builder

TornadoFX has a helpful Form control to handle a large number of user inputs. Having several input fields to take user information is common and JavaFX does not have a built-in solution to streamline this. To remedy this, TornadoFX has a builder to declare a Form with any number of fields (Figure 7.7).
form {
fieldset("Personal Info") {
field("First Name") {
textfield()
}
field("Last Name") {
textfield()
}
field("Birthday") {
datepicker()
}
}
fieldset("Contact") {
field("Phone") {
textfield()
}
field("Email") {
textfield()
}
}
button("Commit") {
action { println("Wrote to database!")}
}
}
Figure 7.7
Awesome right? You can specify one or more controls for each of the fields, and the Form will render the groupings and labels for you.
You can choose to lay out the label above the inputs as well:
fieldset("FieldSet", labelPosition = VERTICAL)
Each field represents a container with the label and another container for the input fields you add inside it. The input container is by default an HBox, meaning that multiple inputs within a single field will be laid out next to each other. You can specify the orientation parameter to a field to make it lay out multiple inputs below each other. Another use case for Vertical orientation is to allow an input to grow as the form expands vertically. This is handy for displaying TextAreas in Forms:
form {
fieldset("Feedback Form", labelPosition = VERTICAL) {
field("Comment", VERTICAL) {
textarea {
prefRowCount = 5
vgrow = Priority.ALWAYS
}
}
buttonbar {
button("Send")
}
}
}
Figure 7.8
The example above also uses the buttonbar builder to create a special field with no label while retaining the label indent so the buttons line up under the inputs.
You bind each input to a model, and you can leave the rendering of the control layouts to the Form. For this reason you will likely want to use this over the GridPane if possible, which we will cover next.

Nesting layouts inside a Form

You can wrap both fieldsets and fields with any layout container of your choosing to create complex form layouts.
form {
hbox(20) {
fieldset("Left FieldSet") {
hbox(20) {
vbox {
field("Field l1a") { textfield() }
field("Field l2a") { textfield() }
}
vbox {
field("Field l1b") { textfield() }
field("Field l2b") { textfield() }
}
}
}
fieldset("Right FieldSet") {
hbox(20) {
vbox {
field("Field r1a") { textfield() }
field("Field r2a") { textfield() }
}
vbox {
field("Field r1b") { textfield() }
field("Field r2b") { textfield() }
}
}
}
}
}
The HBoxes are configured with a spacing of 20 pixels, using the parameter for the hbox builder. It can also be specified as hbox(spacing = 20) for clarity.
Figure 7.9

Using Input Filters

As a part of the TextInputControl, filterInput is a convenient way to restrict user input in form fields. filterInput accepts the changes to form fields and compares them against your filter. If the filter evaluates to true, the input is accepted. In the following example, a textfield has it's input restricted to integers between 0 and 10.
val FirstTenFilter: (TextFormatter.Change) -> Boolean = { change ->
!change.isAdded || change.controlNewText.let {
it.isInt() && it.toInt() in 0..10
}
}
​
textfield {
filterInput(FirstTenFilter)
}
The code above checks if change was triggered by adding new text with isAdded or evaluates the new text against an a function to determine if the text entered, it, isInt() and is within the range of 0-10. If any of the checks return false, the user input will be rejected and they will not be able to input those characters.

GridPane

If you want to micromanage the layout of your controls, the GridPane will give you plenty of that. Of course it requires more configuration and code boilerplate. Before proceeding to use a GridPane, you might want to consider using Form or other layouts that abstract layout configuration for you.
One way to use GridPane is to declare the contents of each row. For any given Node you can call its gridpaneConstraints to configure various GridPane behaviors for that Node, such as margin and columnSpan (Figure 7.10)
gridpane {
row {
button("North") {
useMaxWidth = true
gridpaneConstraints {
marginBottom = 10.0
columnSpan = 2
}
}
}
row {
button("West")
button("East")
}
row {
button("South") {
useMaxWidth = true
gridpaneConstraints {
marginTop = 10.0
columnSpan = 2
}
}
}
}
Figure 7.11
Notice how there is a margin of 10.0 between each row, which was declared for the marginBottom and marginTop of the "North" and "South" buttons respectively inside their gridpaneConstraints.
Alternatively, you can explicitly specify the column/row index positions for each Node rather than declaring each row of controls. This will accomplish the exact layout we built previously, but with column/row index specifications instead. It is a bit more verbose, but it gives you more explicit control over the positions of controls.
gridpane {
button("North") {
useMaxWidth = true
gridpaneConstraints {
columnRowIndex(0,0)
marginBottom = 10.0
columnSpan = 2
}
}
button("West").gridpaneConstraints {
columnRowIndex(0,1)
}
button("East").gridpaneConstraints {
columnRowIndex(1,1)
}
​
button("South") {
useMaxWidth = true
gridpaneConstraints {
columnRowIndex(0,2)
marginTop = 10.0
columnSpan = 2
}
}
}
These are all the gridpaneConstraints attributes you can modify on a given Node. Some are expressed as simple properties that can be assigned while others are assignable through functions.
Attribute
Description
columnIndex: Int
The column index for the given control
rowIndex: Int
The row index for the given control
columnRowIndex(columnIndex: Int, rowIndex: Int)
Specifes the row and column index
columnSpan: Int
The number of columns the control occupies
rowSpan: Int
The number of rows the control occupies
hGrow: Priority
The horizonal grow priority
vGrow: Priority
The vertical grow priority
vhGrow: Priority
Specifies the same priority for vGrow and hGrow
fillHeight: Boolean
Sets whether the Node fills the height of its area
fillWidth: Boolean
Sets whether the Node filles the width of its area
fillHeightWidth: Boolean
Sets whether the Node fills its area for both height and width
hAlignment: HPos
The horizonal alignment policy
vAlignment: VPos
The vertical alignment policy
margin: Int
The margin for all four sides of the Node
marginBottom: Int
The margin for the bottom side of the Node
marginTop: Int
The margin for the top side of the Node
marginLeft: Int
The left margin for the left side of the Node
marginRight: Int
The right margin for the right side of the Node
marginLeftRight: Int
The right and left margins for the Node
marginTopBottom: Int
The top and bottom marins for a Node
Additionally, if you need to configure ColumnConstraints, you can call gridpaneColumnConstraints on any child Node, or constraintsForColumn(columnIndex) on the GridPane itself.
gridpane {
row {
button("Left") {
gridpaneColumnConstraints {
percentWidth = 25.0
}
}
​
button("Middle")
button("Right")
}
constraintsForColumn(1).percentWidth = 50.0
}

StackPane

A StackPane is a layout you will use less often. For each control you add, it will literally stack them on top of each other not like a VBox, but literally overlay them.
For instance, you can create a "BOTTOM" Button and put a "TOP" Button on top of it. The order you declare controls will add them from bottom-to-top in that same order (Figure 7.10).
class MyView: View() {
​
override val root = stackpane {
button("BOTTOM") {
useMaxHeight = true
useMaxWidth = true
style {
backgroundColor += Color.AQUAMARINE
fontSize = 40.0.px
}
}
​
button("TOP") {
style {
backgroundColor += Color.WHITE
}
}
}
}
Figure 7.11

TabPane

A TabPane creates a UI with different screens separated by "tabs". This allows switching between different screens quickly and easily by clicking on the corresponding tab (Figure 7.11). You can declare a tabpane() and then declare as many tab() instances as you need. For each tab() function you will build your hierarchy of nodes inside, starting with the container node.
tabpane {
tab("Screen 1") {
vbox {
button("Button 1")
button("Button 2")
}
}
tab("Screen 2") {
hbox {
button("Button 3")
button("Button 4")
}
}
}
Figure 7.12
TabPane is an effective tool to separate screens and organize a high number of controls. The syntax is somewhat succinct enough to declare complex controls like TableView right inside the tab() block (Figure 7.13).
tabpane {
tab("Screen 1") {
vbox {
button("Button 1")
button("Button 2")
}
}
tab("Screen 2") {
tableview<Person> {
items = listOf(
Person(1,"Samantha Stuart",LocalDate.of(1981,12,4)),
Person(2,"Tom Marks",LocalDate.of(2001,1,23)),
Person(3,"Stuart Gills",LocalDate.of(1989,5,23)),
Person(3,"Nicole Williams",LocalDate.of(1998,8,11))
).observable()
​
column("ID",Person::id)
column("Name", Person::name)
column("Birthday", Person::birthday)
column("Age",Person::age)
}
}
}
Figure 7.13
Like many builders, the TabPane has several properties that can adjust the behavior of its tabs. For instance, you can call tabClosingPolicy to get rid of the "X" buttons on the tabs so they cannot be closed.
class MyView: View() {
override val root = tabpane {
tabClosingPolicy = TabPane.TabClosingPolicy.UNAVAILABLE
​
tab("Screen 1") {
vbox {
button("Button 1")
button("Button 2")
}
}
tab("Screen 2") {
hbox {
button("Button 3")
button("Button 4")
}
}
}
}

Using UIComponents as tabs

You can also embed other UIComponents like Fragments and Views in your Tabs, simply by adding them with either the generic add function or the specialized tab function:
class MyView : View("My TabPane") {
override val root = tabpane {
tab<Screen1>()
tab<Screen2>()
}
}
​
class Screen1 : Fragment("Screen 1") {
override val root = vbox {
button("Button 1")
button("Button 2")
}
}
​
class Screen2 : Fragment("Screen 2") {
override val root = vbox {
button("Button 3")
button("Button 4")
}
}
This strategy promotes reuse and keeps your UI code cleaner.

Builders for Menus

Creating menus can be cumbersome to build in a strictly object-oriented way. But using type-safe builders, Kotlin's functional constructs make it intuitive to declare nested menu hierarchies.
It is not uncommon to use navigable menus to keep a large number of commands on a user interface organized. For instance, the top region of a BorderPane is typically where a MenuBar goes. There you can add menus and submenus easily (Figure 7.5).
menubar {
menu("File") {
menu("Connect") {
item("Facebook")
item("Twitter")
}
item("Save")
item("Quit")
}
menu("Edit") {
item("Copy")
item("Paste")
}
}
Figure 7.14
You can also optionally provide keyboard shortcuts, graphics, as well as an action function parameter for each item() to specify the action when it is selected (Figure 7.14).
menubar {
menu("File") {
menu("Connect") {
item("Facebook", graphic = fbIcon).action { println("Connecting Facebook!") }
item("Twitter", graphic = twIcon).action { println("Connecting Twitter!") }
}
item("Save","Shortcut+S").action {
println("Saving!")
}
item("Quit","Shortcut+Q").action {
println("Quitting!")
}
}
menu("Edit") {
item("Copy","Shortcut+C").action {
println("Copying!")
}
item("Paste","Shortcut+V").action {
println("Pasting!")
}
}
}
Figure 7.14

Separators

You can declare a separator() between two items in a Menu to create a divider line. This is helpful to group commands in a Menu and distinctly separate them (Figure 7.15).
menu("File") {
menu("Connect") {
item("Facebook")
item("Twitter")
}
separator()
item("Save","Shortcut+S").action {
println("Saving!")
}
item("Quit","Shortcut+Q").action {
println("Quitting!")
}
}
Figure 7.15

ContextMenu

Most controls in JavaFX have a contextMenu property where you can assign a ContextMenu instance. This is a Menu that pops up when the control is right-clicked.
A ContextMenu has functions to add Menu and MenuItem instances to it just like a MenuBar. It can be helpful to add a ContextMenu to a TableView<Person>, for example, and provide commands to be done on a table record (Figure 7.16). There is a builder called contextmenu that will build a ContextMenu and assign it to the contextMenu property of the control.
tableview(persons) {
column("ID", Person::idProperty)
column("Name", Person::nameProperty)
column("Birthday", Person::birthdayProperty)
column("Age", Person::ageProperty)
​
contextmenu {
item("Send Email").action {
selectedItem?.apply { println("Sending Email to $name") }
}
item("Change Status").action {
selectedItem?.apply { println("Changing Status for $name") }
}
}
}
Figure 7.16
Note there are also RadioMenuItem and CheckMenuItem variants of MenuItem available.
The menuitem builders take the action to perform when the menu is selected as the op block parameter. Unfortunately, this breaks with the other builders, where the op block operates on the element that the builder created. Therefore, the item builder was introduced as an alternative, where you operate on the item itself, so that you must call setOnAction to assign the action. The menuitem builder is not deprecated, as it solves the common case in a more concise way than the item builder.

ListMenu

TornadoFX comes with a list menu that behaves and looks more like a typical ul/li based HTML5 menu.
The following code example shows how to use the ListMenu with the builder pattern:
listmenu(theme = "blue") {
item(text = "Contacts", graphic = Styles.contactsIcon()) {
// Marks this item as active.
activeItem = this
whenSelected { /* Do some action */ }
}
item(text = "Projects", graphic = Styles.projectsIcon())
item(text = "Settings", graphic = Styles.settingsIcon())
}
The following Attributes can be used to configure the ListMenu:
Attribute
Builder-Attribute
Type
Default
Description
orientation
yes
Orientation
VERTICAL
Configures the orientation of the ListMenu. Possible orientations:
  • VERTICAL
  • HORIZONTAL
iconPosition
yes
Side
LEFT
Configures the icon position of the ListMenu. Possible positions:
  • TOP
  • BOTTOM
  • LEFT
  • RIGHT
theme
yes
String
null
Currently supported themes blue, null. If null is set the default gray theme is used.
tag
yes
Any?
null
The Tag can be any object or null, it can be useful to identify the ListMenu
activeItem
no
ListMenuItem?
null
Represent's the current active ListMenuItem of the ListMenu. To select a ListMenu on creation, just assign the specific ListItem to this property (have a look at the contacts ListMenuItem in the code example above.)

Css Properties

Css-Class
Css-Property
Default
Description
.list-menu
-fx-graphic-fixed-size
2em
The graphic size.
.list-menu .list-item
-fx-cursor
hand
The cursor symbol.
.list-menu .list-item
-fx-padding
10
The padding for each item
.list-menu .list-item
-fx-background-color
-fx-shadow-highlight-color, -fx-outer-border, -fx-inner-border, -fx-body-color
The color of the item
.list-menu .list-item
-fx-background-insets
0 0 -0.5 0, 0, 0.5, 1.5
The insets of each item.
.list-menu .list-item .label
-fx-text-fill
-fx-text-base-color
The text color of each item.

Pseudo Classes

Pseudo-Class
Css-Property
Default
Description
.list-menu .list-item:active
-fx-background-color
-fx-focus-color, -fx-inner-border, -fx-body-color, -fx-faint-focus-color, -fx-body-color
The color will be set if the item is active.
.list-menu .list-item:active
-fx-background-insets
-0.2, 1, 2, -1.4, 2.6
Insets will be set if the item is active.
.list-menu .list-item:hover
-fx-color
-fx-hover-base
The hover color.
Have a look at the default Stylesheet for the ListMenu​

Item

The item builder allows to create items for the ListMenu in a very convenient way. The following syntax is supported:
item("SomeText", graphic = SomeNode, tag = SomeObject) {
// Marks this item as active.
activeItem = this
​
// Do some action when selected
whenSelected { /* Action */ }
}
Attribute
Builder-Attribute
Type
Default
Description
text
yes
String?
null
The text which should be set for the given item.
tag
yes
Any?
null
The Tag can be any object or null and can be useful to identify the ListItem
graphic
yes
Node?
null
The graphic can be any Node and will be displayed beside the given text.
Function
Description
whenSelected
A convince function, which will be called anytime the given ListMenuItem is selected.

Filling the parent container

The useMaxWidth property can be used to fill the parent container horizontally. The useMaxHeight property will fill the parent container vertically. These properties actually applies to all Nodes, but is especially useful for the ListMenu.

SqueezeBox

JavaFX has an Accordion control that lets you group a set of TilePanes together to form an accordion of controls. The JavaFX Accordion only lets you open a single accordion fold at a time, and it has some other shortcomings. To solve this, TornadoFX comes with the SqueezeBox component that behaves and looks very similar to the Accordion, while providing some enhancements.
squeezebox {
fold("Customer Editor", expanded = true) {
form {
fieldset("Customer Details") {
field("Name") { textfield() }
field("Password") { textfield() }
}
}
}
fold("Some other editor", expanded = true) {
stackpane {
label("Nothing here")
}
}
}
Figure 7.17
A Squeezebox showing two folds, both expanded by default
You can tell the SqueezeBox to only allow a single fold to be expanded at any given time by passing multiselect = false to the builder constructor.