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.
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.
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
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")
}
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.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.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.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

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.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
}
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

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")
}
}
}
}
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.
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

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

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 alsoRadioMenuItem
andCheckMenuItem
variants ofMenuItem
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.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:
|
iconPosition | yes | Side | LEFT | Configures the icon position of the ListMenu . Possible positions:
|
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-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-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. |
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. |
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
.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.