TornadoFX Guide

User Manual: Pdf

Open the PDF directly: View PDF PDF.
Page Count: 276

DownloadTornadoFX Guide Tornadofx-guide
Open PDF In BrowserView PDF
Table of Contents
Introduction

1.1

1. Why TornadoFX?

1.2

2. Setting Up

1.3

3. Components

1.4

4. Basic Controls

1.5

5. Data Controls

1.6

6. Type Safe CSS

1.7

7. Layouts and Menus

1.8

8. Charts

1.9

9. Shapes and Animation

1.10

10. FXML

1.11

11. Editing Models and Validation

1.12

12. OSGi

1.13

13. TornadoFX IDEA Plugin

1.14

14. Scopes

1.15

15. EventBus

1.16

16. Workspaces

1.17

17. Internationalization

1.18

18. Config Settings and State

1.19

19. JSON and REST

1.20

20. Dependency Injection

1.21

21. Wizard

1.22

Appendix A - Supplementary Topics

1.23

Appendix B - Tools and Utilities

1.24

1

Introduction

TornadoFX Guide
This is a continual effort to fully document the TornadoFX framework in the format of a book.

2

1. Why TornadoFX?

Introduction
User interfaces are becoming increasingly critical to the success of consumer and business
applications. With the rise of consumer mobile apps and web applications, business users
are increasingly holding enterprise applications to a higher standard of quality. They want
rich, feature-packed user interfaces that provide immediate insight and navigate complex
screens intuitively. More importantly, they want the application to adapt quickly to business
changes on a frequent basis. For the developer, this means the application must not only be
maintainable but also evolvable. TornadoFX seeks to assist all these objectives and greatly
streamline the coding of JavaFX UI's.
While much of the enterprise IT world is pushing HTML5 and cloud-based applications,
many businesses are still using desktop UI frameworks like JavaFX. While it does not
distribute to large audiences as easily as web applications, JavaFX works well for "in-house"
business applications. Its high-performance with large datasets (and the fact it is native
Java) make it a practical choice for applications used behind the corporate firewall.
JavaFX, like many UI frameworks, can quickly become verbose and difficult to maintain.
Fortunately, the release of Kotlin has created an opportunity to rethink how JavaFX
applications are built.
An interesting product that is in development is JPro, a web-based JavaFX container
that uses no plugins. It can work with TornadoFX and JavaFX, but is still in closed beta
at the time of writing. You can follow the project and wait for its availability here:
https://jpro.io/

Why TornadoFX?
In February 2016, JetBrains released Kotlin, a new JVM language that emphasizes
pragmatism over convention. Kotlin works at a higher level of abstraction and provides
practical language features not available in Java. One of the more important features of
Kotlin is its 100% interoperability with existing Java libraries and codebases, including
JavaFX.
While JavaFX can be used with Kotlin in the same manner as Java, some believed Kotlin
had language features that could streamline and simplify JavaFX development. Well before
Kotlin's beta, Eugen Kiss prototyped JavaFX "builders" with KotlinFX. In January 2016,
Edvin Syse rebooted the initiative and released TornadoFX.

3

1. Why TornadoFX?

TornadoFX seeks to greatly minimize the amount of code needed to build JavaFX
applications. It not only includes type-safe builders to quickly lay out controls and user
interfaces, but also features dependency injection, delegated properties, control extension
functions, and other practical features enabled by Kotlin. TornadoFX is a fine showcase of
how Kotlin can simplify codebases, and it tackles the verbosity of UI code with elegance and
simplicity. It can work in conjunction with other popular JavaFX libraries such as ControlsFX
and JFXtras. It works especially well with reactive frameworks such as ReactFX as well as
RxJava and friends (including RxJavaFX, RxKotlin, and RxKotlinFX).

Reader Requirements
This book expects readers to have some knowledge of Kotlin and have spent some time
getting acquainted with it. There will be some coverage of Kotlin language features but only
to a certain extent. If you have not done so already, read the JetBrains Kotlin Reference and
spend a good few hours studying it.
It definitely helps to be familiar with JavaFX but it is not a requirement. Perhaps you started
studying JavaFX but found the development experience to be tedious, and you are checking
out TornadoFX hoping it provides a better way to build user interfaces. If this describes your
experience and you are learning Kotlin, then you will probably benefit from this guide.

A Motivational Example
If you have worked with JavaFX before, you might have created a
Say you have a given domain type

Person

TableView

at some point.

. TornadoFX allows you to much more concisely

create the JavaBeans-like convention used for the JavaFX binding.
class Person(id: Int, name: String, birthday: LocalDate) {
val idProperty = SimpleIntegerProperty(id)
var id by idProperty
val nameProperty = SimpleStringProperty(name)
var name by nameProperty
val birthdayProperty = SimpleObjectProperty(birthday)
var birthday by birthdayProperty
val age: Int get() = Period.between(birthday, LocalDate.now()).years
}

You can then build an entire "

View

" containing a

TableView

with a small code footprint.

4

1. Why TornadoFX?

class MyView : View() {
private val persons = 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()
override val root = tableview(persons) {
column("ID", Person::idProperty)
column("Name", Person::nameProperty)
column("Birthday", Person::birthdayProperty)
column("Age", Person::age)
}
}

RENDERED OUTPUT:

Half of that code was just initializing sample data! If you hone in on just the part declaring the
TableView

with four columns (shown below), you will see it took a simple functional

construct to build a

TableView

. It will automatically support edits to the fields as well.

tableview(persons) {
column("ID", Person::idProperty)
column("Name", Person::nameProperty)
column("Birthday", Person::birthdayProperty)
column("Age", Person::age)
}

As shown below, we can use the

cellFormat()

extension function on a

create conditional formatting for "Age" values that are less than

18

TableColumn

, and

.

5

1. Why TornadoFX?

tableview {
items = persons
column("ID", Person::idProperty)
column("Name", Person::nameProperty)
column("Birthday", Person::birthdayProperty)
column("Age", Person::age).cellFormat {
text = it.toString()
style {
if (it < 18) {
backgroundColor += c("#8b0000")
textFill = Color.WHITE
}
}
}
}

RENDERED OUTPUT:

These declarations are pure Kotlin code, and TornadoFX is packed with expressive power
for dozens of cases like this. This allows you to focus on creating solutions rather than
engineering UI code. Your JavaFX applications will not only be turned around more quickly,
but also be maintainable and evolvable.

6

2. Setting Up

Setting Up
To use TornadoFX, there are several options to set up the dependency for your project.
Mainstream build automation tools like Gradle and Maven are supported and should have no
issues in getting set up.
Please note that TornadoFX is a Kotlin library, and therefore your project needs to be
configured to use Kotlin. For Gradle and Maven configurations, please refer to the Kotlin
Gradle Setup and Kotlin Maven Setup guides. Make sure your development environment or
IDE is equipped to work with Kotlin and has the proper plugins and compilers.
This guide will use Intellij IDEA to walk through certain examples. IDEA is the IDE of choice
to work with Kotlin, although Eclipse has a plugin as well.

Gradle
For Gradle, you can set up the dependency directly from Maven Central. Provide the desired
version number for the

x.y.z

placeholder.

repositories {
mavenCentral()
}
// Minimum jvmTarget of 1.8 needed since Kotlin 1.1
compileKotlin {
kotlinOptions.jvmTarget= 1.8
}
dependencies {
compile 'no.tornado:tornadofx:x.y.z'
}

Maven
To import TornadoFX with Maven, add the following dependency to your POM file. Provide
the desired version number for the

x.y.z

placeholder.

Goes into kotlin-maven-plugin block:

7

2. Setting Up


1.8


Then this goes into

dependencies

block:


no.tornado
tornadofx
x.y.z


Other Build Automation Solutions
For instructions on how to use TornadoFX with other build automation solutions, please refer
to the [TornadoFX page at the Central Repository]
([http://search.maven.org/#search|gav|1|g%3A"no.tornado]
(http://search.maven.org/#search|gav|1|g%3A"no.tornado)" AND a%3A"tornadofx")

Manual Import
To manually download and import the JAR file, go to the TornadoFX release page or the
Central Repository. Download the JAR file and configure it into your project.

Starting a TornadoFX Application
Newer versions of the JVM know how to start JavaFX applications without a

main()

method. A JavaFX application, and by extension a TornadoFX application, is any class that
extends

javafx.application.Application

javafx.application.Application

start the app by referencing
main()

. Since

tornadofx.App

extends

, TornadoFX apps are no different. Therefore you would

com.example.app.MyApp

, and you don't necessarily need a

function unless you need to supply command line arguments. In that case you

would add a package level main function to the

MyApp.kt

file:

fun main(args: Array) {
Application.launch(MyApp::class.java, *args)
}

8

2. Setting Up

This main function would be compiled to

com.example.app.MyAppKt

- notice the

Kt

at the

end. When you create a package
level main function, it will always have a class name of the fully qualified package, plus the
file name, appended with

Kt

.

In fact, TornadoFX also contains a helper to make it even nicer to launch applications from a
main class by accepting the app class as a generic type parameter:
fun main(args: Array) = launch(args)

9

3. Components

Components
JavaFX uses a theatrical analogy to organize an

Application

components. TornadoFX builds on this by providing
components. While the
Controller

, and

Stage

Fragment

, and

Scene

View

,

with

Stage

Controller

and

, and

are used by TornadoFX, the

Scene

Fragment

View

,

introduces new concepts that streamline development. Many of

these components are automatically maintained as singletons, and can communicate to
each other through simple dependency injections and other means.
You also have the option to utilize FXML which will be discussed much later. But first, lets
extend

App

to create an entry point that launches aTornadoFX application.

App and View Basics
To create a TornadoFX application, you must have at least one class that extends
App is the entry point to the application and specifies the initial
JavaFX

Application

View

, but you do not necessarily need to specify a

App

. An

. It does in fact extend

start()

or

main()

method.
But first, extend

App

to create your own implementation and specify the primary view as the

first constructor argument.
class MyApp: App(MyView::class)

A View contains display logic as well as a layout of Nodes, similar to the JavaFX
is automatically managed as a singleton. When you declare a
root

property which can be any

Node

property and assign it

VBox

. It

you must specify a

type, and that will hold the View's content.

In the same Kotlin file or in a new file, extend a class off of
root

View

Stage

or any other

Node

View

. Override the abstract

you choose.

class MyView: View() {
override val root = VBox()
}

However, we might want to populate this
initializer block, let's add a JavaFX
+=

VBox

Button

operators to add children, such as a

acting as the

and a

Button

Label

and

root

control. Using the

. You can use the "plus assign"

Label

10

3. Components

class MyView: View() {
override val root = VBox()
init {
root += Button("Press Me")
root += Label("")
}
}

While it is pretty clear what's going on from looking at this code, TornadoFX provides a
builder syntax that will streamline your UI code further andmake it much easier to reason
about the resulting UI just by looking at the code. We will gradually move into builder syntax,
and finally cover builders in full in the next chapter.
While we introduce you to new concepts, you might sometimes see code that is not using
best practices. We do this to introduce you gradually to concepts and give you a broader
understanding of what is going on under the hood. Gradually we will introduce more
powerful constructs to solve the problem at hand in a better way.
Next we will see how to run this application.

Starting a TornadoFX Application
Newer versions of the JVM know how to start JavaFX applications without a

main()

method. A JavaFX application, and by extension a TornadoFX application, is any class that
extends

javafx.application.Application

javafx.application.Application

start the app by referencing
main()

. Since

tornadofx.App

extends

, TornadoFX apps are no different. Therefore you would

com.example.app.MyApp

, and you do not necessarily need a

function unless you need to supply command line arguments. In that case you

would add a package level main function to the

MyApp.kt

file:

fun main(args: Array) {
Application.launch(MyApp::class.java, *args)
}

This main function would be compiled to

com.example.app.MyAppKt

. Notice the

Kt

at the

end. When you create a package level main function, it will always have a class name of the
fully qualified package, plus the file name, appended with
For launching and testing the

App

Kt

.

, we will use Intellij IDEA. Navigate to Run→Edit

Configurations (Figure 3.1).
Figure 3.1

11

3. Components

Click the green "+" sign and create a new Application configuration (Figure 3.2).
Figure 3.2

12

3. Components

Specify the name of your "Main class" which should be your

App

class. You will also need

to specify the module it resides in. Give the configuration a meaningful name such as
"Launcher". After that click "OK" (Figure 3.3).
Figure 3.3

You can run your TornadoFX application by selecting Run→Run 'Launcher' or whatever you
named the configuration (Figure 3.4).
Figure 3.4

13

3. Components

You should now see your application launch (Figure 3.5)
Figure 3.5

Congratulations! You have written your first (albeit simple) TornadoFX application. It may not
look like much right now, but as we cover more of TornadoFX's powerful features we will be
creating large, impressive user interfaces with little code in no time. But first let's understand
a little better what is happening between

App

and

View

.

Understanding Views
Let's dive a little deeper into how a
App

and

View

View

works and how it can be used. Take a look at the

classes we just built.

14

3. Components

class MyApp: App(MyView::class)
class MyView: View() {
override val root = VBox()
init {
with(root) {
this += Button("Press Me")
this += Label("Waiting")
}
}
}

A

View

contains a hierarchy of JavaFX Nodes and is injected by name wherever it is called.

In the next section we will learn how to leverage powerful builders to create these
hierarchies quickly. There is only one instance of

MyView

Node

maintained by TornadoFX,

effectively making it a singleton. TornadoFX also supports scopes, which can group together
a collection of
View

View

s,

Fragment

s and

Controller

s in separate instances, resulting in a

only being a singleton inside that scope. This is great for Multiple-Document Interface

applications and other advanced use cases. This is covered in a later chapter.

Using inject() and Embedding Views
You can also inject one or more Views into another
BottomView

the

TopView

them to the

into a
and

MasterView
BottomView

BorderPane

. Note we use the

View

. Below we embed a

inject()

TopView

and

delegate property to lazily inject

instances. Then we call each "child" View's

root

to assign

(Figure 3.6).

class MasterView: View() {
val topView: TopView by inject()
val bottomView: BottomView by inject()
override val root = borderpane {
top = topView.root
bottom = bottomView.root
}
}
class TopView: View() {
override val root = label("Top View")
}
class BottomView: View() {
override val root = label("Bottom View")
}

15

3. Components

Figure 3.6

If you need Views to communicate to each other, you can create a property in each of the
"child" Views that holds the "parent"

View

.

class MasterView : View() {
override val root = BorderPane()
val topView: TopView by inject()
val bottomView: BottomView by inject()
init {
with(root) {
top = topView.root
bottom = bottomView.root
}
topView.parent = this
bottomView.parent = this
}
}
class TopView: View() {
override val root = Label("Top View")
lateinit var parent: MasterView
}
class BottomView: View() {
override val root = Label("Bottom View")
lateinit var parent: MasterView
}

More typically you would use a

Controller

or a

ViewModel

to communicate between views,

and we will visit this topic later.

Injection Using find()
The

inject()

delegate will lazily assign a given component to a property. The first time that

component is called is when it will be retrieved. Alternatively, instead of using the
delegate you can use the

find()

function to retrieve a singleton instance of a

inject()

View

or

other components.

16

3. Components

class MasterView : View() {
override val root = BorderPane()
val topView = find(TopView::class)
val bottomView = find(BottomView::class)
init {
with(root) {
top = topView.root
bottom = bottomView.root
}
}
}
class TopView: View() {
override val root = Label("Top View")
}
class BottomView: View() {
override val root = Label("Bottom View")
}

You can use either

find()

or

inject()

, but using

inject()

delegates is the preferred

means to perform dependency injection.

Introduction to Builders
While we will cover builders more in depth in the next chapter, it is time to reveal that the
above example can be written in a much more concise and expressive syntax:
class MasterView : View() {
override val root = borderpane {
top(TopView::class)
bottom(BottomView::class)
}
}

Instead of injecting the
nodes to the

BorderPane

and

BottomView

and

bottom

TopView

s

top

and then assigning their respective root

property, we specify the

BorderPane

with the

builder syntax (all lower case) and then declaratively tell TornadoFX to pull in the two
subviews and assign them to the

top

and

bottom

properties automatically. Hopefully you

agree this is much more expressive, with a lot less boiler plate. This is one of the most
important principles TornadoFX tries to live by: Reduce boiler plate and increase readability.
The end result is often less code and less bugs.

17

3. Components

Controllers
In many cases, it is considered a good practice to separate a UI into three distinct parts:
1. Model - The business code layer that holds core logic and data
2. View - The visual display with various input and output controls
3. Controller - The "middleman" mediating events between the Model and the View
There are other flavors of MVC like MVVM and MVP, all of which can be leveraged in
TornadoFX.
While you could put all logic from the Model and Controller right into the view, it is often
cleaner to separate these three pieces distinctly to maximize reusability. One commonly
used pattern to accomplish this is the MVC pattern. In TornadoFX, a
injected to support a

View

can be

.

Here is a simple example. Create a simple
to a "database" when a

Controller

Button

View

with a

TextField

is clicked. We can inject a

whose value is written

Controller

that handles

interacting with the model that writes to the database. Since this example is simplified, there
will be no database but a printed message will serve as a placeholder (Figure 3.7).
class MyView : View() {
val controller: MyController by inject()
var inputField: TextField by singleAssign()
override val root = vbox {
label("Input")
inputField = textfield()
button("Commit") {
action {
controller.writeToDb(inputField.text)
inputField.clear()
}
}
}
}

class MyController: Controller() {
fun writeToDb(inputValue: String) {
println("Writing $inputValue to database!")
}
}

Figure 3.7

18

3. Components

When we build the UI, we make sure to add a reference to the
references from the

onClick

inputField

so that it can be

event handler of the "Commit" button later. When the

"Commit" button is clicked, you will see the Controller prints a line to the console.
Writing Alpha to database!

It is important to note that while the above works, and may even look pretty good, it is a
good practice to avoid referencing other UI elements directly. Your code will be much easier
to refactor if you bind your UI elements to properties and manipulate the properties instead.
We will introduce the

ViewModel

later, which provides even easier ways to deal with this

type of interaction.
You can also use Controllers to provide data to a

View

(Figure 3.8).

class MyView : View() {
val controller: MyController by inject()
override val root = vbox {
label("My items")
listview(controller.values)
}
}
class MyController: Controller() {
val values = FXCollections.observableArrayList("Alpha","Beta","Gamma","Delta")
}

Figure 3.8

19

3. Components

The

VBox

contains a

assigned to the

Label

values

and a

ListView

property of our

, and the

Controller

items

property of the

ListView

is

.

Whether they are reading or writing data, Controllers can have long-running tasks and
should not perform work on the JavaFX thread. You will learn how to easily offload work to a
worker thread using the

runAsync

construct later in this chapter.

Long running tasks
Whenever you call a function in a controller you need to determine if that function returns
immediately or if it performs potentially long-running tasks. If you call a function on the
JavaFX Application Thread, the UI will be unresponsive until the call completes.
Unresponsive UI's is a killer for user acceptance, so make sure that you run expensive
operations in the background. TornadoFX provides the
Code placed inside a

runAsync

runAsync

function to help with this.

block will run in the background. If the result of the

background call should update your UI, you must make sure that you apply the changes on
the JavaFX Application Thread. The

ui

block does exactly that.

20

3. Components

val textfield = textfield()
button("Update text") {
action {
runAsync {
myController.loadText()
} ui { loadedText ->
textfield.text = loadedText
}
}
}

When the button is clicked, the action inside the
ActionEvent

to

builder (which delegates the

method) is run. It makes a call out to

setAction

myController.loadText()

action

and applies the result to the text property of the textfield when it

returns. The UI stays responsive while the controller function runs.
Under the covers,

runAsync

to run your call inside the

creates JavaFX

Task

objects, and spins off a separate thread

Task

. You can assign this

to a variable and bind it to a UI

Task

to show progress while your operation is running.
In fact, this is so common that there is also an default ViewModel called
contains observable values for
the

runAsync

running

,

message

call with a specific instance of the

,

title

, and

TaskStatus

runAsync

called

progress

which

. You can supply

object, or use the default.

The TornadoFX sources includes an example usage of this in the
There is also a version of

TaskStatus

runAsyncWithProgress

AsyncProgressApp.kt

file.

which will cover the

current node with a progress indicator while the long running operation runs.

singleAssign() Property Delegate
In the example above we initialized the

inputField

property with the

singleAssign

delegate. If you want to guarantee that a value is only assigned once, you can use the
singleAssign()

delegate instead of the

lateinit

keyword from Kotlin. This will cause a

second assignment to throw an error, and it will also error when it is prematurely accessed
before it is assigned.
You can look up more about
guarantees a

var

singleAssign()

in detail in Appendix A1, but know for now it

can only be assigned once. It is also threadsafe and helps mitigate

issues with mutability.

Fragment

21

3. Components

Any

you create is a singleton, which means you typically use it in only one place at a

View

time. The reason for this is that the root node of the

View

can only have a single parent in a

JavaFX application. If you assign it another parent, it will disappear from its previous parent.
However, if you would like to create a piece of UI that is short-lived or can be used in
multiple places, consider using a

Fragment

. A Fragment is a special type of

that can

View

have multiple instances. They are particularly useful for popups or as pieces of a larger UI
(such as ListCells, which we look at via the
Both

and

View

Fragment

support

ListCellFragment

openModal()

,

later).

openWindow()

and

openInternalWindow()

that will open the root node in a separate Window.
class MyView : View() {
override val root = vbox {
button("Press Me") {
action {
find(MyFragment::class).openModal(stageStyle = StageStyle.UTILITY)
}
}
}
}
class MyFragment: Fragment() {
override val root = label("This is a popup")
}

You can pass optional arguments to

openModal()

as well to modify a few of its behaviors.

Optional Arguments for openModal()
Argument

Type

Description

stageStyle

StageStyle

Defines one of the possible enum styles for
Stage . Default: StageStyle.DECORATED

modality

Modality

Defines one of the possible enum modality
types for Stage . Default:
Modality.APPLICATION_MODAL

escapeClosesWindow

Boolean

Sets the
Default:

owner

Window

Specify the owner Window for this Stage`

block

Boolean

Block UI execution until the Window closes.
Default: false

ESC

key to call

closeModal()

.

true

InternalWindow
22

3. Components

While

openModal

opens in a new

Stage

,

openInternalWindow

opens over the current root

node, or any other node if you specify it:
button("Open editor") {
action {
openInternalWindow(Editor::class)
}
}

Figure 3.9

A good use case for the internal window is for single stage environments like JPro, or if you
want to customize the window trim to make the window appear more in line with the design
of your application. The Internal Window can be styled with CSS. Take a look at the
InternalWindow.Styles

class for more information about styleable properties.

The internal window API differs from modal/window in one important aspect. Since the
window opens over an existing node, you typically call

openInternalWindow()

from within the

View you want it to open on top of. You supply the View you want to show, and you can
optionally supply what node to open over via the

owner

parameter.

Optional Arguments for openInternalWindow()

23

3. Components

Argument

Type

Description

view

UIComponent

The component will be the content of the
new window

view

KClass

Alternatively, you can supply the class of the
view instead of an instance

icon

Node

Optional window icon

scope

Scope

If you specify the view class, you can also
specify the scope used to fetch the view

modal

Boolean

Defines if the covering node should be
disabled while the internal window is active.
Default: true

escapeClosesWindow

Boolean

Sets the

Node

Specify the owner Node for this window. The
window will by default cover the root node of
this view.`

owner

ESC

key to call

close()

. Default:

true

Closing modal windows
Any

Component

opened using

closed by calling

closeModal()

directly if needed using

openModal()

,

openWindow()

or

openInternalWindow()

. It is also possible to get to the

InternalWindow

findParentOfType(InternalWindow::class)

can be

instance

.

Replacing Views and Docking Events
With TornadoFX, is easy to swap your current
replaceWith()
View

View

with another

View

, and optionally add a transition. In the example below, a

will switch to the other view, which can be

MyView1

or

MyView2

using
Button

on each

(Figure 3.10).

24

3. Components

class MyView1: View() {
override val root = vbox {
button("Go to MyView2") {
action {
replaceWith(MyView2::class)
}
}
}
}
class MyView2: View() {
override val root = vbox {
button("Go to MyView1") {
action {
replaceWith(MyView1::class)
}
}
}
}

Figure 3.10

You also have the option to specify a spiffy animation for the transition between the two
Views.
replaceWith(MyView1::class, ViewTransition.Slide(0.3.seconds, Direction.LEFT)

This works by replacing the

root

Node

are two functions you can override on
connected to a parent (

onDock()

on given

View

View

with another

View

to leverage when a View's

), and when it is disconnected (

's

root

onUndock()

root
Node

. There
is

). You can

leverage these two events to connect and "clean up" whenever a

View

out. You will notice running the code below that whenever a

is swapped, it will undock

that previous

View

View

comes in or falls

and dock the new one. You can leverage these two events to manage

initialization and disposal tasks.

25

3. Components

class MyView1: View() {
override val root = vbox {
button("Go to MyView2") {
action {
replaceWith(MyView2::class)
}
}
}
override fun onDock() {
println("Docking MyView1!")
}
override fun onUndock() {
println("Undocking MyView1!")
}
}
class MyView2: View() {
override val root = vbox {
button("Go to MyView1") {
action {
replaceWith(MyView1::class)
}
}
}
override fun onDock() {
println("Docking MyView2!")
}
override fun onUndock() {
println("Undocking MyView2!")
}
}

Passing parameters to views
The best way to pass information between views is often an injected ViewModel. Even so, it
can still be convenient to be able to pass parameters to other components. The
inject

functions supports varargs of

Pair

find

and

which can be used for just this

purpose. Consider a customer list that opens a customer editor for the selected customer.
The action to edit a customer might look like this:
fun editCustomer(customer: Customer) {
find(mapOf(CustomerEditor::customer to customer).openWindow())
}

26

3. Components

The parameters are passed as a map, where the key is the property in the view and the
value is whatever you want the property to be. This gives you a type safe way of configuring
parameters for the target View.
Here we use the Kotlin
written as

to

syntax to create the parameter. This could also have been

Pair(CustomerEditor::customer, customer)

if you prefer. The editor can now

access the parameter like this:
class CustomerEditor : Fragment() {
val customer: Customer by param()
}

If you want to inspect the parameters instead of blindly relying on them to be available, you
can either declare them as nullable or consult the

params

map:

class CustomerEditor : Fragment() {
init {
val customer = params["customer"] as? Customer
if (customer != null) {
...
}
}
}

If you don't care about type safety you can also pass parameters as
customer)

mapOf("customer" to

, but then you miss out on automatic refactoring if you rename a property in the

target view.

Accessing the primary stage
View
Stage

has a property called

primaryStage

that allows you to manipulate properties of the

backing it, such as window size for example. Any

opened via

openModal

will also have a

modalStage

View

or

Fragment

that were

property available.

Accessing the scene
Some times it is necessary to get a hold of the current scene from within a
Fragment

. This can be achieved with

there is an even shorter way, just use

root.scene
scene

View

or

, or if you are within a type safe builder,

.

27

3. Components

Accessing resources
Lot's of JavaFX APIs takes resources as an

URL

or the

toExternalForm

of an URL. To

retrieve a resource url one would typically write something like:
val myAudioClip = AudioClip(MyView::class.java.getResource("mysound.wav").toExternalFo
rm())

Every

Component

has a

resources

object which can retrieve the external form url of a

resource like this:
val myAudiClip = AudioClip(resources["mysound.wav"])

If you need an actual

URL

it can be retrieved like this:

val myResourceURL = resources.url("mysound.wav")

The

resources

to the

helper also has several other helpful functions to help you turn files relative

Component

into an object of the type you need:

val myJsonObject = resources.json("myobject.json")
val myJsonArray = resources.jsonArray("myarray.json")
val myStream = resources.stream("somefile")

It's worth mentioning that the
InputStream

json

and

jsonArray

functions are also available on

objects.

Resources are relative to the
path, starting with a

/

Component

but you can also retrieve a resource by it's full

.

Shortcuts and key combinations for actions
You can fire actions when certain key combinations are typed. This is done with the
shortcut

function:

shortcut(KeyCombination.valueOf("Ctrl+Y")) {
doSomething()
}

28

3. Components

There is also a string version of the

shortcut

function that does the same but is less

verbose:
shortcut("Ctrl+Y")) {
doSomething()
}

You can also add shortcuts to button actions directly:
button("Save") {
action { doSave() }
shortcut("Ctrl+S")
}

Touch Support
JavaFX supports touch out of the box, and for now the only place we needed to improve it
was to handle shortpress and longpress in a more convenient way. It consists of two
functions similar to

action

, which can be configured on any

Node

:

shortpress { println("Activated on short press") }
longpress { println("Activated on long press") }

Both functions accepts a

consume

parameter which by default is

will prevent event bubbling for the press event. The
supports a
It is

threshold

700.millis

longpress

false

. Setting it to true

function additionally

parameter which is used to determine when a longpress has accured.

by default.

Summary
TornadoFX is filled with simple, streamlined, and powerful injection tools to manage Views
and Controllers. It also streamlines dialogs and other small UI pieces using

Fragment

. While

the applications we built so far are pretty simple, hopefully you appreciate the simplified
concepts TornadoFX introduces to JavaFX. In the next chapter we will cover what is
arguably the most powerful feature of TornadoFX: Type-Safe Builders.

29

4. Basic Controls

Basic Controls
One of the most exciting features of TornadoFX are the Type-Safe Builders. Configuring and
laying out controls for complex UI's can be verbose and difficult, and the code can quickly
become messy to maintain. Fortunately, you can use a powerful closure pattern pioneered
by Groovy to create structured UI layouts with pure and simple Kotlin code.
While we will learn how to apply FXML later, you may find builders to be an expressive,
robust way to create complex UI's in a fraction of the time. There are no configuration files or
compiler magic tricks, and builders are done with pure Kotlin code. The next several
chapters will divide the builders into separate categories of controls. Along the way, you will
gradually build more complex UI's by integrating these builders together.
But first, let's cover how builders actually work.

How Builders Work
Kotlin's standard library comes with a handful of helpful "block" functions to target items of
any type

T

. There is the with() function, which allows you to write code against an item as if

you were right inside of its class.
class MyView : View() {
override val root = VBox()
init {
with(root) {
this += Button("Press Me")
}
}
}

In the above example, the

with()

function accepts the

following closure argument manipulates
safely interpreted as a
plusAssign()

VBox

.A

Button

root

root

as an argument. The

directly by referring to it as

was added to the

VBox

this

, which is

by calling it's

extended operator.

Alternatively, every type in Kotlin has an apply() function. This is almost the same
functionality as

with()

but it is actually an extended higher-order function.

30

4. Basic Controls

class MyView : View() {
override val root = VBox()
init {
root.apply {
this += Button("Press Me")
}
}
}

Both

with()

and

apply()

accomplish a similar task. They safely interpret the type they are

targeting and allow manipulations to be done to it. However,
statement within the lambda, whereas
Therefore, if you call
helpful the

Button

apply()

on a

apply()

Button

with()

returns the last

does in fact return the item it was targeting.

to manipulate say, its font color and action, it is

returns itself so as to not break the declaration flow.

class MyView : View() {
override val root = VBox()
init {
with(root) {
this += Button("Press Me").apply {
textFill = Color.RED
action { println("Button pressed!") }
}
}
}
}

The basic concepts of how builders work are expressed above, and there are three tasks
being done:
1. A

Button

is created

2. The

Button

is modified

3. The

Button

is added to its "parent", which is a

When declaring any

Node

VBox

, these three steps are so common that TornadoFX streamlines

them for you using strategically placed extension functions, such as

button()

as shown

below.

31

4. Basic Controls

class MyView : View() {
override val root = VBox()
init {
with(root) {
button("Press Me") {
textFill = Color.RED
action { println("Button pressed!") }
}
}
}
}

While this looks much cleaner, you might be wondering: "How did we just get rid of the
and

+=

apply()

an actual

Button

function call? And why are we using a function called

button()

this

instead of

?"

We will not go too deep on how this is done, and you can always dig into the source code if
you are curious.
But essentially, the
button()

VBox

(or any targetable component) has an extension function called

.

It accepts a text argument and an optional closure targeting a
When this function is called, it will create a
to it, add it to the

VBox

Button

Button

it will instantiate.

with the specified text, apply the closure

it was called on, and then return it.

Taking this efficiency further, you can override the
function and avoid needing any

init

and

with()

root

in a

View

, but assign it a builder

blocks.

class MyView : View() {
override val root = vbox {
button("Press Me") {
textFill = Color.RED
action { println("Button pressed!") }
}
}
}

The builder pattern becomes especially powerful when you start nesting controls into other
controls. Using these builder extension functions, you can easily populate and nest multiple
HBox

instances into a

VBox

, and create UI code that is clearly structured (Figure 4.1).

32

4. Basic Controls

class MyView : View() {
override val root = vbox {
hbox {
label("First Name")
textfield()
}
hbox {
label("Last Name")
textfield()
}
button("LOGIN") {
useMaxWidth = true
}
}
}

Figure 4.1

Also note we will learn about TornadoFX's proprietary

Form

later, which will make

simple input UI's like this even simpler to code.
If you need to save references to controls such as the TextFields, you can save them to
variables or properties since the functions return the produced controls. It is recommend you
use the

singleAssign()

delegates to ensure the properties are only assigned once.

33

4. Basic Controls

class MyView : View() {
var firstNameField: TextField by singleAssign()
var lastNameField: TextField by singleAssign()
override val root = vbox {
hbox {
label("First Name")
firstNameField = textfield()
}
hbox {
label("Last Name")
lastNameField = textfield()
}
button("LOGIN") {
useMaxWidth = true
action {
println("Logging in as ${firstNameField.text} ${lastNameField.text}")
}
}
}
}

Note that non-builder extension functions and properties have been added to different
controls as well. The

useMaxWidth

is an extended property for

Node

, and it sets the

Node

to occupy the maximum width allowed. We will see more of these helpful extensions
throughout the next few chapters.
In the coming chapters, we will cover each corresponding builder for each JavaFX control.
With the concepts understood above, you can read about these next chapters start to finish
or as a reference.

Builders for Basic Controls
The rest of this chapter will cover builders for common JavaFX controls like
Label

, and

ListView

,

TextField
TableView

Button

,

. The next chapter will cover builders for data-driven controls like

, and

TreeTableView

.

Button
For any

Pane

, you can call its

optionally pass a

text

button()

argument and a

extension function to add a
Button.() -> Unit

Button

to it. You can

lambda to modify its properties.

34

4. Basic Controls

Within a

, this will add a

Pane

Button

with red text and print "Button pressed!" every time it

is clicked (Figure 4.2)
button("Press Me") {
textFill = Color.RED
action {
println("Button pressed!")
}
}

Figure 4.2

Label
You can call the

label()

extension function to add a

Optionally you can provide a text (of type
(of type

Node

or

ObjectProperty

String

) and a

or

Label

to a given

Property

Label.() -> Unit

Pane

.

), a graphic

lambda to modify its

properties (Figure 4.3).
label("Lorem ipsum", circle(10, 10, 5)) {
textFill = Color.BLUE
}

Figure 4.3

TextField
For any

Pane

you can add a

TextField

by calling its

textfield()

extension function

(Figure 4.4).
textfield()

Figure 4.4

35

4. Basic Controls

You can optionally provide initial text as well as a closure to manipulate the
example, we can add a listener to its

textProperty()

TextField

. For

and print its value every time it

changes (Figure 4.5).
textfield("Input something") {
textProperty().addListener { obs, old, new ->
println("You typed: " + new)
}
}

Figure 4.6

PasswordField
If you need a
PasswordField

TextField

to take sensitive information, you might want to consider a

instead. It will show anonymous characters to protect from prying eyes. You

can also provide an initial password as an argument and a block to manipulate it (Figure
4.7).
passwordfield("my_password") {
requestFocus()
}

Figure 4.7

CheckBox
You can create a

CheckBox

to quickly create a true/false state control and optionally

manipulate it with a block (Figure 4.8).

36

4. Basic Controls

checkbox("Admin Mode") {
action { println(isSelected) }
}

Notice that the action block is wrapped inside the checkbox so you can access
it's

isSelected

property. If you don't need access to the properties of the CheckBox you

could have written

checkbox("Admin Mode").action {}

.

Figure 4.9

You can also provide a

Property

that will bind to its selection state.

val booleanProperty = SimpleBooleanProperty()
checkbox("Admin Mode", booleanProperty).action { println(isSelected) }

ComboBox
A

ComboBox

is a drop down control that allows a fixed set of values to be selected from

(Figure 4.10).
val texasCities = FXCollections.observableArrayList("Austin",
"Dallas","Midland", "San Antonio","Fort Worth")
combobox {
items = texasCities
}

Figure 4.10

37

4. Basic Controls

You do not need to specify the generic type if you declare the

values

as an argument.

val texasCities = FXCollections.observableArrayList("Austin",
"Dallas","Midland","San Antonio","Fort Worth")
combobox(values = texasCities)

You can also specify a

Property

to be bound to the selected value.

val texasCities = FXCollections.observableArrayList("Austin",
"Dallas","Midland","San Antonio","Fort Worth")
val selectedCity = SimpleStringProperty()
combobox(selectedCity, texasCities)

ToggleButton
A

ToggleButton

is a button that expresses a true/false state depending on its selection state

(Figure 4.11).
togglebutton("OFF") {
action {
text = if (isSelected) "ON" else "OFF"
}
}

38

4. Basic Controls

Perhaps a more idomatic way to control the button text would be to use a StringBinding
bound to the

textProperty:

togglebutton {
val stateText = selectedProperty().stringBinding {
if (it == true) "ON" else "OFF"
}
textProperty().bind(stateText)
}

Figure 4.11

You can optionally pass a
ToggleButton

s in that

ToggleGroup

ToggleGroup

to the

togglebutton()

function. This will ensure all

can only have one selected at a time (Figure 4.12).

class MyView : View() {
private val toggleGroup = ToggleGroup()
override val root = hbox {
togglebutton("YES", toggleGroup)
togglebutton("NO", toggleGroup)
togglebutton("MAYBE", toggleGroup)
}
}

Figure 4.12

RadioButton
A

RadioButton

is the same functionality as a

ToggleButton

but with a different visual style.

When it is selected, it "fills" in a circular control (Figure 4.13).
radiobutton("Power User Mode") {
action {
println("Power User Mode: $isSelected")
}
}

39

4. Basic Controls

Figure 4.13

Also like the

ToggleButton

, you can set a

RadioButton

to be included in a

ToggleGroup

so

that only one item in that group can be selected at a time (Figure 4.14).
class MyView : View() {
private val toggleGroup = ToggleGroup()
override val root = vbox {
radiobutton("Employee", toggleGroup)
radiobutton("Contractor", toggleGroup)
radiobutton("Intern", toggleGroup)
}
}

Figure 4.14

DatePicker
The

DatePicker

is a simple to declare. It allows you to choose a date from a popout

calendar control. You can optionally provide a block to manipulate it (Figure 4.15).
datepicker {
value = LocalDate.now()
}

Figure 4.15

40

4. Basic Controls

You can also provide a

Property

as an argument to bind to its value.

val dateProperty = SimpleObjectProperty()
datepicker(dateProperty) {
value = LocalDate.now()
}

TextArea
The

TextArea

text

value

allows you input multiline freeform text. You can optionally provide the initial

as well as a block to manipulate it on declaration (Figure 4.16).

textarea("Type memo here") {
selectAll()
}

Figure 4.16

41

4. Basic Controls

ProgressBar
A

visualizes progress towards completion of a process. You can optionally

ProgressBar

provide an initial

Double

value less than or equal to 1.0 indicating percentage of completion

(Figure 4.17).
progressbar(0.5)

Figure 4.17

Here is a more dynamic example simulating progress over a short period of time.
progressbar() {
thread {
for (i in 1..100) {
Platform.runLater { progress = i.toDouble() / 100.0 }
Thread.sleep(100)
}
}
}

You can also pass a

Property

block to manipulate the

ProgressBar

that will bind the

progress

to its value as well as a

.

42

4. Basic Controls

progressbar(completion) {
progressProperty().addListener {
obsVal, old, new ->

print("VALUE: $new")

}
}

ProgressIndicator
A

ProgressIndicator

is functionally identical to a

ProgressBar

but uses a filling circle

instead of a bar (Figure 4.18).
progressindicator {
thread {
for (i in 1..100) {
Platform.runLater { progress = i.toDouble() / 100.0 }
Thread.sleep(100)
}
}
}

Figure 4.18

Just like the

ProgressBar

you can provide a

Property

and/or a block as optional

arguments (Figure 4.19).
val completion = SimpleObjectProperty(0.0)
progressindicator(completion)

ImageView
You can embed an image using

imageview()

.

imageview("tornado.jpg")

Figure 4.19

43

4. Basic Controls

Like most other controls, you can use a block to modify its attributes (Figure 4.20).
imageview("tornado.jpg") {
scaleX = .50
scaleY = .50
}

Figure 4.20

44

4. Basic Controls

ScrollPane
You can embed a control inside a

ScrollPane

to make it scrollable. When the available area

becomes smaller than the control, scrollbars will appear to navigate the control's area.
For instance, you can wrap an

ImageView

inside a

ScrollPane

(Figure 4.21).

scrollpane {
imageview("tornado.jpg")
}

Figure 4.21

45

4. Basic Controls

Keep in mind that many controls like

TableView

bars on them, so wrapping them in a

ScrollPane

and

TreeTableView

already have scroll

is not necessary (Figure 4.22).

Hyperlink
You can create a

Hyperlink

control to mimic the behavior of a typical hyperlink to a file, a

website, or simply perform an action.
hyperlink("Open File").action { println("Opening file...") }

Figure 4.22

Text
You can add a simple piece of
rawer than a

Label

Text

with formatted properties. This control is simpler and

, and paragraphs can be separated using

\n

characters (Figure 4.23).

46

4. Basic Controls

text("Veni\nVidi\nVici") {
fill = Color.PURPLE
font = Font(20.0)
}

Figure 4.23

TextFlow
If you need to concatenate multiple pieces of text with different formats, the

TextFlow

control can be helpful (Figure 4.24).
textflow {
text("Tornado") {
fill = Color.PURPLE
font = Font(20.0)
}
text("FX") {
fill = Color.ORANGE
font = Font(28.0)
}
}

Figure 4.24

You can add any

Node

to the

textflow

, including images, using the standard builder

functions.

Tooltips
Inside any

Node

you can specify a

Tooltip

via the

tooltip()

function (Figure 4.25).

47

4. Basic Controls

button("Commit") {
tooltip("Writes input to the database")
}

Figure 4.25

Like most other builders, you can provide a closure to customize the

Tooltip

itself.

button("Commit") {
tooltip("Writes input to the database") {
font = Font.font("Verdana")
}
}

There are many other builder controls, and the maintainers of TornadoFX have strived
to create a builder for every JavaFX control. If you need something that is not covered
here, use Google to see if its included in JavaFX. Chances are if a control is available
in JavaFX, there is a builder with the same name in TornadoFX.
SUMMARY
In this chapter we learned about TornadoFX builders and how they work simply by using
Kotlin extension functions. We also covered builders for basic controls like
TextField

and

ImageView

Button

,

. In the coming chapters we will learn about builders for tables,

layouts, menus, charts, and other controls. As you will see, combining all these builders
together creates a powerful way to express complex UI's with very structured and minimal
code.
These are not the only control builders in the TornadoFX API, and this guide does its best to
keep up. Always check the TornadoFX GitHub to see the latest builders and functionalities
available, and file an issue if you see any missing.

48

4. Basic Controls

49

5. Data Controls

Data Controls
Any significant application works with data, and providing a means for users to view,
manipulate, and modify data is not a trivial task for user interface development. Fortunately,
TornadoFX streamlines many JavaFX data controls such as
TreeView

, and

TreeTableView

ListView

,

TableView

,

. These controls can be cumbersome to set up in a purely

object-oriented way. But using builders through functional declarations, we can code all
these controls in a much more streamlined way.

ListView
A

ListView

is similar to a

ComboBox

but it displays all items within a

ScrollView

and has

the option of allowing multiple selections, as shown in Figure 5.1
listview {
items.add("Alpha")
items.add("Beta")
items.add("Gamma")
items.add("Delta")
items.add("Epsilon")
selectionModel.selectionMode = SelectionMode.MULTIPLE
}

Figure 5.1

You can also provide it an

ObservableList

of items up front and omit the type declaration

since it can be inferred. Using an ObservableList also has the benefit that changes to the list
will automatically be reflected in the ListView.

50

5. Data Controls

val greekLetters = listOf("Alpha","Beta",
"Gamma","Delta","Epsilon").observable()
listview(greekLetters) {
selectionModel.selectionMode = SelectionMode.MULTIPLE
}

Like most data controls, keep in mind that by default, the

ListView

will call

toString()

to

render the text for each item in your domain class. To render anything else, you will need to
create your own custom cell formatting.

Custom Cell Formatting in ListView
Even though the default look of a

ListView

is rather boring (because it calls

and renders it as text) you can modify it so that every cell is a custom
choosing. By calling
kind of

Node

cellCache()

Node

toString()

of your

, TornadoFX provides a convenient way to override what

is returned for each item in your list (Figure 5.2).

51

5. Data Controls

class MyView: View() {
val persons = listOf(
Person("John Marlow", LocalDate.of(1982,11,2)),
Person("Samantha James", LocalDate.of(1973,2,4))
).observable()
override val root = listview(persons) {
cellFormat {
graphic = cache {
form {
fieldset {
field("Name") {
label(it.name)
}
field("Birthday") {
label(it.birthday.toString())
}
label("${it.age} years old") {
alignment = Pos.CENTER_RIGHT
style {
fontSize = 22.px
fontWeight = FontWeight.BOLD
}
}
}
}
}
}
}
}
class Person(val name: String, val birthday: LocalDate) {
val age: Int get() = Period.between(birthday, LocalDate.now()).years
}

52

5. Data Controls

Figure 5.2 - A custom cell rendering for

The

cellFormat

ListView

function lets you configure the

text

and/or

graphic

property of the cell

whenever it comes into view on the screen. The cells themselves are reused, but whenever
the

ListView

asks the cell to update it's content, the

example we only assign to
you should assign it to
graphic

text

graphic

cellFormat

function is called. In our

, but if you just want to change the string representation

. It is completely legitimate to assign it to both

. The values will automatically be cleared by the

cellFormat

text

and

function when a

certain list cell is not showing an active item.
Note that assigning new nodes to the

graphic

property every time the list cell is asked to

update can be expensive. It might be fine for many use cases, but for heavy node graphs, or
node graphs where you utilize binding towards the ui components inside the cell, you should
cache the resulting node so the node graph will only be created once per node. This is done
using the

cache

wrapper in the above example.

53

5. Data Controls

Assign If Null
If you have a reason for wanting to recreate the graphic property for a list cell, you can use
the

assignIfNull

helper, which will assign a value to any given property if the property

doesn't already contain a value. This will make sure that you avoid creating new nodes if
updateItem

is called on a cell that already has a graphic property assigned.

cellFormat {
graphicProperty().assignIfNull {
label("Hello")
}
}

ListCellFragment
The

is a special fragment which can help you manage

ListCellFragment

extends

Fragment

, and includes some extra

ListView

them as needed. There is a one to one correlation between
instances. One

ListCellFragment

cells. It

specific fields and helpers. You

never instantiate these fragments manually, instead you instruct the
ListCellFragment

ListView

ListView

ListCell

to create

and

instance will over its lifecycle be used

to represent different items.
To understand how this works, let's consider a manually implemented
the way you would do in vanilla JavaFX. The
ListCell

you use a
but the

updateItem

ListCell

, essentially

function will be called when the

should represent a new item, no item, or just an update to the same item. When
ListCellFragment

itemProperty

, you do not need to implement something akin to

,

updateItem

inside it will update to represent the new item automatically. You can

listen to changes to the

itemProperty

way your UI can bind directly to the

, or better yet, bind it directly to a

ViewModel

ViewModel

. That

and no longer need to care about changes to

the underlying item.
Let's recreate the form from the
ViewModel

which we will call

cellFormat

PersonModel

chapter for a full explanation of the
acts as a proxy for an underlying
observable values in the
PersonCellFragment

(Please see the

ViewModel

Person

ViewModel

example using a

ListCellFragment

Editing Models and Validation

) For now, just imagine that the

, and that the

. We need a

Person

ViewModel

can be changed while the

remain the same. When we have created our

, we need to configure the

ListView

to use it:

listview(personlist) {
cellFragment(PersonCellFragment::class)
}

54

5. Data Controls

Now comes the

ListCellFragment

itself.

class PersonListFragment : ListCellFragment() {
val person = PersonModel().bindTo(this)
override val root = form {
fieldset {
field("Name") {
label(person.name)
}
field("Birthday") {
label(person.birthday)
}
label(stringBinding(person.age) { "$value years old" }) {
alignment = Pos.CENTER_RIGHT
style {
fontSize = 22.px
fontWeight = FontWeight.BOLD
}
}
}
}
}

Because this Fragment will be reused to represent different list items, the easiest approach
is to bind the ui elements to the ViewModel's properties.
The

name

and

birthday

properties are bound directly to the labels inside the fields. The

age string in the last label needs to be constructed using a

stringBinding

to make sure it

updates when the item changes.
While this might seem like slightly more work than the

cellFormat

example, this approach

makes it possible to leverage everything the Fragment class has to offer. It also forces you
to define the cell node graph outside of the builder hierarchy, which improves refactoring
possibilities and enables code reuse.

Additional helpers and editing support
The

ListCellFragment

cellProperty

also have some other helper properties. They include the

which will update whenever the underlying cell changes and the

editingProperty

, which will tell you if this the underlying list cell is in editing mode. There

are also editing helper functions called
onEdit

callback. The

capabilites of the

ListCellFragment

ListView

startEdit

,

commitEdit

,

cancelEdit

plus an

makes it trivial to utilize the existing editing

. A complete example can be seen in the TodoMVC demo

application.

55

5. Data Controls

TableView
Probably one of the most significant builders in TornadoFX is the one for
have worked with JavaFX, you might have experienced building a

TableView

TableView

. If you

in an object-

oriented way. But TornadoFX provides a functional declaration construct pattern using
extension functions that greatly simplifies the coding of a
Say you have a domain type, such as

Person

TableView

.

.

class Person(val id: Int, val name: String, val birthday: LocalDate) {
val age: Int get() = Period.between(birthday, LocalDate.now()).years
}

Take several instances of

Person

and put them in an

ObservableList

.

private val persons = 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()

You can quickly declare a
specify the

items

TableView

property to an

with all of its columns using a functional construct, and

ObservableList

(Figure 5.3).

tableview(persons) {
column("ID",Person::id)
column("Name", Person::name)
column("Birthday", Person::birthday)
column("Age",Person::age)
}

Figure 5.3

56

5. Data Controls

The

column()

functions are extension functions for

TableView

accepting a

header

name

and a mapped property using reflection syntax. TornadoFX will then take each mapping to
render a value for each cell in that given column.
If you want granular control over
for more information on

TableView

SmartResize

column resize policies, see Appendix A2

policies.

Using "Property" properties
If you follow the JavaFX

Property

conventions to set up your domain class, it will

automatically support value editing.
You can create these
property

Property

objects the conventional way, or you can use TornadoFX's

delegates to automatically create these

Property

declarations as shown below.

class Person(id: Int, name: String, birthday: LocalDate) {
var id by property(id)
fun idProperty() = getProperty(Person::id)
var name by property(name)
fun nameProperty() = getProperty(Person::name)
var birthday by property(birthday)
fun birthdayProperty() = getProperty(Person::birthday)
val age: Int get() = Period.between(birthday, LocalDate.now()).years
}

You need to create

xxxProperty()

functions for each property to support JavaFX's naming

convention when it uses reflection. This can easily be done by relaying their calls to
getProperty()

to retrieve the

Property

for a given field. See Appendix A1 for detailed

57

5. Data Controls

information on how these property delegates work.
Now on the

TableView

, you can make it editable, map to the properties, and apply the

appropriate cell-editing factories to make the values editable.
override val root = tableview(persons) {
isEditable = true
column("ID",Person::idProperty).useTextField(IntegerStringConverter())
column("Name", Person::nameProperty).useTextField(DefaultStringConverter())
column("Birthday", Person::birthdayProperty).useTextField(LocalDateStringConverter
())
column("Age",Person::age)
}

To allow editing and rendering, TornadoFX provides a few default cell factories you can
invoke on a column easily through extension functions.
Extension
Function

Description

useTextField()

Uses a standard

useComboBox()

Edits a cell value via a ComboBox with a specified
ObservableList of applicable values

useChoiceBox()

Accepts value changes to a cell with a

useCheckBox()

Renders an editable

useProgressBar()

Renders the cell as a

TextField

to edit values with a provided

StringConverter

CheckBox

for a

ProgressBar

ChoiceBox

Boolean

for a

value column

Double

value column

Property Syntax Alternatives
If you do not care about exposing the

Property

in a function (which is common in practial

usage) you can express your class like this:
class Person(id: Int, name: String, birthday: LocalDate) {
val idProperty = SimpleIntegerProperty(id)
var id by idProperty
val nameProperty = SimpleStringProperty(name)
var name by nameProperty
val birthdayProperty = SimpleObjectProperty(birthday)
var birthday by birthdayProperty
val age: Int get() = Period.between(birthday, LocalDate.now()).years
}

58

5. Data Controls

This alternative pattern exposes the

Property

as a field member instead of a function. If

you like the above syntax but want to keep the function, you can make the property
private

and add the function like this:

private val nameProperty = SimpleStringProperty(name)
fun nameProperty() = nameProperty
var name by nameProperty

Choosing from these patterns are all a matter of taste, and you can use whatever version
meets your needs or preferences best.
You can also convert plain properties to JavaFX properties using the TornadoFX Plugin.
Refer to Chapter 13 to learn how to do this.

Using cellFormat()
There are other extension functions applied to
declaring a

TableView

TableView

. For instance, you can call a

that can assist the flow of

cellFormat()

function on a given

column to apply formatting rules, such as highlighting "Age" values less than 18 (Figure 5.4).
tableview(persons) {
column("ID", Person::id)
column("Name", Person::name)
column("Birthday", Person::birthday)
column("Age", Person::age).cellFormat {
text = it.toString()
style {
if (it < 18) {
backgroundColor += c("#8b0000")
textFill = Color.WHITE
} else {
backgroundColor += Color.WHITE
textFill = Color.BLACK
}
}
}
}

Figure 5.4

59

5. Data Controls

Accessing nested properties
Let's assume our

Person

object has a

parent

property which is also of of type

Person

. To

create a column for the parent name, we have several options. Our first attempt is simply
extracting the name property manually:
column("Parent name", { it.value.parentProperty.value.nameProperty })

Notice how we can't simply reference the property, we need to access the value provided in
the callback to get to the actual instance and nest from there down to the nameProperty.
While this works, it has one major drawback. If the parent changes, the list won't be
updated. We can partially remedy this by defining the value for the property as the parent
itself, and formatting it's name:
column("Parent name", Person::parentProperty).cellFormat {
textProperty().bind(it.parentProperty.value.nameProperty)
}

It might still not update right away, even though it would eventually become consistent as the
TableView refreshes.
To create a binding that would reflect a change to the parent property immediately, consider
using a select binding: (more on bindings later)
column("Parent name", { it.value.parentProperty.select(Person::namePro
perty) })

60

5. Data Controls

Declaring Column Values Functionally
If you need to map a column's value to a non-property (such as a function), you can use a
non-reflection means to extract the values for that column.
Say you have a

WeeklyReport

type that has a

function accepting a

getTotal()

DayOfWeek

argument (an enum of Monday, Tuesday... Sunday).
abstract class WeeklyReport(val startDate: LocalDate) {
abstract fun getTotal(dayOfWeek: DayOfWeek): BigDecimal
}

Let's say you wanted to create a column for each
but you can map each
DayOfWeek

WeeklyReport

DayOfWeek

. You cannot map to properties,

item explicitly to extract each value for that

.

tableview {
for (dayOfWeek in DayOfWeek.values()) {
column(dayOfWeek.toString()) {
ReadOnlyObjectWrapper(it.value.getTotal(dayOfWeek))
}
}
}

This more closely resembles the traditional
TableColumn

setCellValueFactory()

for the JavaFX

.

Row Expanders
Later we will learn about the

TreeTableView

which has a notion of "parent" and "child" rows,

but the constraint with this control is the parent and child must have the same columns.
Fortunately, TornadoFX comes with an awesome utility to not only reveal a "child table" for a
given row, but any kind of

Node

Say we have two domain types:
and it contains one or more

control.
Region

Branch

and

Branch

.A

Region

is a geographical zone,

items which are specific business operation locations

(warehouses, distribution centers, etc). Here is a declaration of these types and some given
instances.

61

5. Data Controls

class Region(val id: Int, val name: String, val country: String, val branches: Observa
bleList)
class Branch(val id: Int, val facilityCode: String, val city: String, val stateProvince
: String)
val regions = listOf(
Region(1,"Pacific Northwest", "USA",listOf(
Branch(1,"D","Seattle","WA"),
Branch(2,"W","Portland","OR")
).observable()),
Region(2,"Alberta", "Canada",listOf(
Branch(3,"W","Calgary","AB")
).observable()),
Region(3,"Midwest", "USA", listOf(
Branch(4,"D","Chicago","IL"),
Branch(5,"D","Frankfort","KY"),
Branch(6, "W","Indianapolis", "IN")
).observable())
).observable()

We can create a

TableView

where each row has a

there we can arbitrarily create any
case, we can nest another

Node

TableView

rowExpander()

function defined, and

control built off that particular row's item. In this

for a given

Region

to show all the

Branch

items

belonging to it. It will have a "+" button column to expand and show this expanded control
(Figure 5.5).
Figure 5.5

62

5. Data Controls

There are a few configurability options, like "expand on double-click" behaviors and
accessing the

expanderColumn

(the column with the "+" button) to drive a padding (Figure

5.6).
override val root = tableview(regions) {
column("ID",Region::id)
column("Name", Region::name)
column("Country", Region::country)
rowExpander(expandOnDoubleClick = true) {
paddingLeft = expanderColumn.width
tableview(it.branches) {
column("ID",Branch::id)
column("Facility Code",Branch::facilityCode)
column("City",Branch::city)
column("State/Province",Branch::stateProvince)
}
}
}

Figure 5.6

63

5. Data Controls

The

rowExpander()

function does not have to return a

TableView

but any kind of

Node

,

including Forms and other simple or complex controls.

Accessing the expander column
You might want to manipulate or call functions on the actual expander column. If you
activate expand on double click, you might not want to show the expander column in the
table at all. First we need a reference to the expander:
val expander = rowExpander(true) { ... }

If you want to hide the expander column, just call

expander.isVisible = false

. You can also

programmatically toggle the expanded state of any column by calling
expander.toggleExpanded(rowIndex)

.

TreeView
The

TreeView

contains elements where each element may contain child elements. Typically

arrows allow you to expand a parent element to see its children. For instance, we can nest
employees under department names

64

5. Data Controls

Traditionally in JavaFX, populating these elements is rather cumbersome and verbose.
Fortunately TornadoFX makes it relatively simple.
Say you have a simple type

Person

and an

ObservableList

containing several instances.

data class Person(val name: String, val department: String)
val persons = listOf(
Person("Mary Hanes","Marketing"),
Person("Steve Folley","Customer Service"),
Person("John Ramsy","IT Help Desk"),
Person("Erlick Foyes","Customer Service"),
Person("Erin James","Marketing"),
Person("Jacob Mays","IT Help Desk"),
Person("Larry Cable","Customer Service")
)

Creating a

TreeView

with the

treeview()

builder can be done functionally Figure 5.7).

// Create Person objects for the departments
// with the department name as Person.name
val departments = persons
.map { it.department }
.distinct().map { Person(it, "") }
treeview {
// Create root item
root = TreeItem(Person("Departments", ""))
// Make sure the text in each TreeItem is the name of the Person
cellFormat { text = it.name }
// Generate items. Children of the root item will contain departments
populate { parent ->
if (parent == root) departments else persons.filter { it.department == parent.
value.name }
}
}

Figure 5.7

65

5. Data Controls

Let's break this down:
val departments = persons
.map { it.department }
.distinct().map { Person(it, "") }

First we gather a distinct list of all the
we put each
Person

department

String in a

departments
Person

derived from the

object since the

persons

TreeView

list. But then

only accepts

elements. While this is not very intuitive, this is the constraint and design of

TreeView

. We must make each

department

a

Person

for it to be accepted.

treeview {
// Create root item
root = TreeItem(Person("Departments", ""))

66

5. Data Controls

Next we specify the highest

root

for the

under, and we give it a placeholder

TreeView

that all departments will be nested

called "Departments".

Person

cellFormat { text = it.name }

Then we specify the

cellFormat()

to render the

of each

name

Person

(including

departments) on each cell.
populate { parent ->
if (parent == root) departments else persons.filter { it.department == parent.
value.name }
}

Finally, we call the
children to each
departments

populate()

. If the

parent

. Otherwise the

objects belonging to that

function and provide a block instructing how to provide
parent

parent

department

is indeed the

is a

department

root

, then we return the

and we provide a list of

Person

.

Data driven TreeView
If the child list you return from

populate

is an

ObservableList

, any changes to that list will

automatically be reflected in the TreeView. The populate function will be called for any new
children that appears, and removed items will result in removed TreeItems as well.

TreeView with Differing Types
It is not necessarily intuitive to make every entity in the previous example a
made each department a
TreeView

where

T

star projection for type

Person

as well as the

root

Person

. We

"Departments". For a more complex

is unknown and can be any number of types, it is better to leverage
T

.

Using star projection, you can safely populate multiple types nested into the

TreeView

For instance, you can create a

to utilize type-

Department

checking for rendering. Then you can use a

type and leverage
populate()

cellFormat()

.

function that will iterate over each

element, and you specify the children for each element (if any).

67

5. Data Controls

data class Department(val name: String)
// Create Department objects for the departments by getting distinct values from Perso
n.department
val departments = persons.map { it.department }.distinct().map { Department(it) }
// Type safe way of extracting the correct TreeItem text
cellFormat {
text = when (it) {
is String -> it
is Department -> it.name
is Person -> it.name
else -> throw IllegalArgumentException("Invalid value type")
}
}
// Generate items. Children of the root item will contain departments, children of dep
artments are filtered
populate { parent ->
val value = parent.value
if (parent == root) departments
else if (value is Department) persons.filter { it.department == value.name }
else null
}

TreeTableView
The

TreeTableView

operates and functions similarly to a

TreeView

columns since it is a table. Please note that the columns in a

, but it has multiple

TreeTableView

are the same

for each parent and child element. If you want the columns to be different between parent
and child, use a

TableView

Say you have a

Person

to an empty

with a

rowExpander()

as covered earlier in this chapter.

class that optionally has an

List

if nobody reports to that

employees
Person

parameter, which defaults

.

class Person(val name: String,
val department: String,
val email: String,
val employees: List = emptyList())

Then you have an

ObservableList

holding instances of this class.

68

5. Data Controls

val persons = listOf(
Person("Mary Hanes", "IT Administration", "mary.hanes@contoso.com", listOf(
Person("Jacob Mays", "IT Help Desk", "jacob.mays@contoso.com"),
Person("John Ramsy", "IT Help Desk", "john.ramsy@contoso.com"))),
Person("Erin James", "Human Resources", "erin.james@contoso.com", listOf(
Person("Erlick Foyes", "Customer Service", "erlick.foyes@contoso.com"),
Person("Steve Folley", "Customer Service", "steve.folley@contoso.com"),
Person("Larry Cable", "Customer Service", "larry.cable@contoso.com")))
).observable()

You can create a
TreeView
TreeItem

TreeTableView

by merging the components needed for a

together. You will need to call the

populate()

TableView

and

function as well as set the root

.

val treeTableView = TreeTableView().apply {
column("Name", Person::nameProperty)
column("Department", Person::departmentProperty)
column("Email", Person::emailProperty)
/// Create the root item that holds all top level employees
root = TreeItem(Person("Employees by leader", "", "", persons))
// Always return employees under the current person
populate { it.value.employees }
// Expand the two first levels
root.isExpanded = true
root.children.forEach { it.isExpanded = true }
// Resize to display all elements on the first two levels
resizeColumnsToFitContent()
}

It is also possible to work with more of an ad hoc backing store like a

Map

. That would look

something like this:

69

5. Data Controls

val tableData = mapOf(
"Fruit" to arrayOf("apple", "pear", "Banana"),
"Veggies" to arrayOf("beans", "cauliflower", "cale"),
"Meat" to arrayOf("poultry", "pork", "beef")
)
treetableview(TreeItem("Items")) {
column("Type", { it.value.valueProperty() })
populate {
if (it.value == "Items") tableData.keys
else tableData[it.value]?.asList()
}
}

DataGrid
A

DataGrid

is similar to the

GridPane

in that it displays items in a flexible grid of rows and

columns, but the similarities ends there. While the
the children list, the

DataGrid

GridPane

requires you to add Nodes to

is data driven in the same way as

TableView

and

.

ListView

You supply it with a list of items and tell it how to convert those children to a graphical
representation.
It supports selection of either a single item or multiple items at a time so it can be used as for
example the display of an image viewer or other components where you want a visual
representation of the underlying data. Usage wise it is close to a

ListView

, but you can

create an arbitrary scene graph inside each cell so it is easy to visualize multiple properties
for each item.
val kittens = listOf("http://i.imgur.com/DuFZ6PQb.jpg", "http://i.imgur.com/o2QoeNnb.j
pg") // more items here
datagrid(kittens) {
cellCache {
imageview(it)
}
}

Figure 5.8

70

5. Data Controls

The

cellCache

function receives each item in the list, and since we used a list of Strings in

our example, we simply pass that string to the

imageview()

inside each table cell. It is important to call the

cellCache

cellFormat

builder to create an

ImageView

function instead of the

function to avoid recreating the images every time the

DataGrid

redraws. It will

reuse the items.
Let's create a scene graph that is a little bit more involved, and also change the default size
of each cell:

71

5. Data Controls

val numbers = (1..10).toList()
datagrid(numbers) {
cellHeight = 75.0
cellWidth = 75.0
multiSelect = true
cellCache {
stackpane {
circle(radius = 25.0) {
fill = Color.FORESTGREEN
}
label(it.toString())
}
}
}

Figure 5.9

The grid is supplied with a list of numbers this time. We start by specifying a cell height and
width of 75 pixels, half of the default size. We also configure multi select to be able to select
more than a single element. This is a shortcut of writing
SelectionMode.MULTIPLE
Label

on top of a

selectionModel.selectionMode =

via an extension property. We create a

Circle

StackPane

that stacks a

.

72

5. Data Controls

You might wonder why the label got so big and bold by default. This is coming from the
default stylesheet. The stylesheet is a good starting point for further customization. All
properties of the data grid can be configured in code as well as in CSS, and the
stylesheet lists all possible style properties.
The number list showcased multiple selection. When a cell is selected, it receives the
CSS pseudo class of

selected

. By default it will behave mostly like a

with regards to selection styles. You can access the

selectionModel

ListView

row

of the data grid to

listen for selection changes, see what items are selected etc.

Summary
Functional constructs work well with data controls like

TableView

,

TreeView

, and others we

have seen in this chapter. Using the builder patterns, you can quickly and functionally
declare how data is displayed.
In Chapter 7, we will embed controls in layouts to create more complex UI's easily.

73

6. Type Safe CSS

Type-Safe CSS
While you can create plain text CSS style sheets in JavaFX, TornadoFX provides the option
to bring type-safety and compiled CSS to JavaFX. You can conveniently choose to create
styles in its own class, or do it inline within a control declaration.

Inline CSS
The quickest and easiest way to style a control on the fly is to call a given

Node

's inline

function. All the CSS properties available on a given control are available in a

style { }

type-safe manner, with compilation checks and auto-completion.
For example, you can style the borders on a

Button

(using the

box()

function), bold its

font, and rotate it (Figure 6.1).
button("Press Me") {
style {
fontWeight = FontWeight.EXTRA_BOLD
borderColor += box(
top = Color.RED,
right = Color.DARKGREEN,
left = Color.ORANGE,
bottom = Color.PURPLE
)
rotate = 45.deg
}
setOnAction { println("You pressed the button") }
}

Figure 6.1

This is especially helpful when you want to style a control without breaking the declaration
flow of the

Button

. However, keep in mind the

that control unless you pass

true

style { }

for its optional

append

will replace all styles applied to
argument.

74

6. Type Safe CSS

style(append = true) {
....
}

Some times you want to apply the same styles to many nodes in one go. The

style { }

function can also be applied to any Iterable that contains Nodes:
vbox {
label("First")
label("Second")
label("Third")
children.style {
fontWeight = FontWeight.BOLD
}
}

The

fontWeight

style is applied to all children of the vbox, in essence all the labels we

added.
When your styling complexity passes a certain threshold, you may want to consider using
Stylesheets which we will cover next.

Applying Style Classes with Stylesheets
If you want to organize, re-use, combine, and override styles you need to leverage a
Stylesheet

. Traditionally in JavaFX, a stylesheet is defined in a plain CSS text file included

in the project. However, TornadoFX allows creating stylesheets with pure Kotlin code. This
has the benefits of compilation checks, auto-completion, and other perks that come with
statically typed code.
To declare a

Stylesheet

, extend it onto your own class to hold your customized styles.

import tornadofx.*
class MyStyle: Stylesheet() {
}

Next, you will want to specify its

companion object

easily be retrieved. Declare a new

cssclass()

to hold class-level properties that can

-delegated property called

tackyButton

, and

define four colors we will use for its borders.

75

6. Type Safe CSS

import javafx.scene.paint.Color
import tornadofx.*
class MyStyle: Stylesheet() {
companion object {
val tackyButton by cssclass()
private val topColor = Color.RED
private val rightColor = Color.DARKGREEN
private val leftColor = Color.ORANGE
private val bottomColor = Color.PURPLE
}
}

Note also you can use the

c()

function to build colors quickly using RGB values or color

Strings.
private val topColor = c("#FF0000")
private val rightColor = c("#006400")
private val leftColor = c("#FFA500")
private val bottomColor = c("#800080")

Finally, declare an

init()

block to apply styling to the classes. Define your selection and

provide a block that manipulates its various properties. (For compound selections, call the
s()

function, which is an alias for the

define the

borderColor

degrees and

Number
20.px

function). Set

using the four colors and the

"Comic Sans MS", and increase the
properties for

select()

fontSize

box()

rotate

to 10 degrees,

function, make the font family

to 20 pixels. Note that there are extension

types to quickly yield the value in that unit, such as

10.deg

for 10

for 20 pixels.

76

6. Type Safe CSS

import javafx.scene.paint.Color
import tornadofx.*
class MyStyle: Stylesheet() {
companion object {
val tackyButton by cssclass()
private val topColor = Color.RED
private val rightColor = Color.DARKGREEN
private val leftColor = Color.ORANGE
private val bottomColor = Color.PURPLE
}
init {
tackyButton {
rotate = 10.deg
borderColor += box(topColor,rightColor,bottomColor,leftColor)
fontFamily = "Comic Sans MS"
fontSize = 20.px
}
}
}

Now you can apply the

style to buttons, labels, and other controls that support

tackyButton

these properties. While this styling can work with other controls like labels, we are going to
target buttons in this example.
First, load the

MyStyle

stylesheet into your application by including it as contructor

parameter.
class MyApp: App(MyView::class, MyStyle::class) {
init {
reloadStylesheetsOnFocus()
}
}

The

reloadStylesheetsOnFocus()

Stylesheets every time the
stylesheets

Stage

function call will instruct TornadoFX to reload the
gets focus. You can also pass the

--live-

argument to the application to accomplish this.

Important: For the reload to work, you must be running the JVM in debug mode and you
must instruct your IDE to recompile before you switch back to your app. Without these steps,
nothing will happen. This also applies to

reloadViewsOnFocus()

which is similar, but reloads

the whole view instead of just the stylesheet. This way, you can evolve your UI very rapidly
in a "code change, compile, refresh" manner.

77

6. Type Safe CSS

You can apply styles directly to a control by calling its
MyStyle.tackyButton

addClass()

function. Provide the

style to two buttons (Figure 6.2).

class MyView: View() {
override val root = vbox {
button("Press Me") {
addClass(MyStyle.tackyButton)
}
button("Press Me Too") {
addClass(MyStyle.tackyButton)
}
}
}

Figure 6.2

Intellij IDEA can perform a quickfix to import member variables, allowing
addClass(MyStyle.tackyButton)

You can use

removeClass()

to be shortened to

addClass(tackyButton)

if you prefer.

to remove the specified style as well.

Targeting Styles to a Type
One of the benefits of using pure Kotlin is you can tightly manipulate UI control behavior and
conditions using Kotlin code. For example, you can apply the style to any
iterating through a control's
applying the

addClass()

children

Button

by

, filtering for only children that are Buttons, and

to them.

class MyView: View() {
override val root = vbox {
button("Press Me")
button("Press Me Too")
children.asSequence()
.filter { it is Button }
.forEach { it.addClass(MyStyle.tackyButton) }
}
}

78

6. Type Safe CSS

Infact, manipulating classes on several nodes at once is so common that TornadoFX
provides a shortcut for it:
children.filter { it is Button }.addClass(MyStyle.tackyButton) }

You can also target all
button

in the

Button

Stylesheet

instances in your application by selecting and modifying the

. This will apply the style to all Buttons.

import javafx.scene.paint.Color
import tornadofx.*
class MyStyle: Stylesheet() {
companion object {
val tackyButton by cssclass()
private val topColor = Color.RED
private val rightColor = Color.DARKGREEN
private val leftColor = Color.ORANGE
private val bottomColor = Color.PURPLE
}
init {
button {
rotate = 10.deg
borderColor += box(topColor,rightColor,leftColor,bottomColor)
fontFamily = "Comic Sans MS"
fontSize = 20.px
}
}
}

import javafx.scene.layout.VBox
import tornadofx.*
class MyApp: App(MyView::class, MyStyle::class) {
init {
reloadStylesheetsOnFocus()
}
}

class MyView: View() {
override val root = vbox {
button("Press Me")
button("Press Me Too")
}
}

79

6. Type Safe CSS

Figure 6.3

Note also you can select multiple classes and control types to mix-and-match styles. For
example, you can set the font size of labels and buttons to 20 pixels, and create tacky
borders and fonts only for buttons (Figure 6.4).
class MyStyle: Stylesheet() {
companion object {
private val topColor = Color.RED
private val rightColor = Color.DARKGREEN
private val leftColor = Color.ORANGE
private val bottomColor = Color.PURPLE
}
init {
s(button, label) {
fontSize = 20.px
}
button {
rotate = 10.deg
borderColor += box(topColor,rightColor,leftColor,bottomColor)
fontFamily = "Comic Sans MS"
}
}
}

class MyApp: App(MyView::class, MyStyle::class) {
init {
reloadStylesheetsOnFocus()
}
}
class MyView: View() {
override val root = vbox {
label("Lorem Ipsum")
button("Press Me")
button("Press Me Too")
}
}

80

6. Type Safe CSS

Figure 6.4

Multi-Value CSS Properties
Some CSS properties accept multiple values, and TornadoFX Stylesheets can streamline
this with the

multi()

function. This allows you to specify multiple values via a

varargs

parameter and let TornadoFX take care of the rest. For instance, you can nest multiple
background colors and insets into a control (Figure 6.5).
label("Lore Ipsum") {
style {
fontSize = 30.px
backgroundColor = multi(Color.RED, Color.BLUE, Color.YELLOW)
backgroundInsets = multi(box(4.px), box(8.px), box(12.px))
}
}

Figure 6.5

The

multi()

function should work wherever multiple values are accepted. If you want to

only assign a single value to a property that accepts multiple values, you will need to use the
plusAssign()

operator to add it (Figure 6.6).

81

6. Type Safe CSS

label("Lore Ipsum") {
style {
fontSize = 30.px
backgroundColor += Color.RED
backgroundInsets += box(4.px)
}
}

Figure 6.6

Nesting Styles
Inside a selector block you can apply further styles targeting child controls.
For instance, define a CSS class called

critical

. Make it put an orange border around any

control it is applied to, and pad it by 5 pixels.
class MyStyle: Stylesheet() {
companion object {
val critical by cssclass()
}
init {
critical {
borderColor += box(Color.ORANGE)
padding = box(5.px)
}
}
}

But suppose when we applied

critical

to any control, such as an

HBox

, we want it to add

additional stylings to buttons inside that control. Nesting another selection will do the trick.

82

6. Type Safe CSS

class MyStyle: Stylesheet() {
companion object {
val critical by cssclass()
}
init {
critical {
borderColor += box(Color.ORANGE)
padding = box(5.px)
button {
backgroundColor += Color.RED
textFill = Color.WHITE
}
}
}
}

Now when you apply
defined style for

critical

button

to say, an

HBox

, all buttons inside that

HBox

will get that

(Figure 6.7)

class MyApp: App(MyView::class, MyStyle::class) {
init {
reloadStylesheetsOnFocus()
}
}
class MyView: View() {
override val root = hbox {
addClass(MyStyle.critical)
button("Warning!")
button("Danger!")
}
}

Figure 6.7

There is one critical thing to not confuse here. The orange border is only applied to the HBox
since the

critical

class was applied to it. The buttons do not get an orange border

because they are children to the

HBox

. While their style is defined by

not inherit the styles of their parent, only those defined for

button

critical

, they do

.

83

6. Type Safe CSS

If you want the buttons to get an orange border too, you need to apply the
directly to them. You will want to use the
also declared as

critical

and()

critical

class

to apply specific styles to buttons that are

.

class MyStyle: Stylesheet() {
companion object {
val critical by cssclass()
}
init {
critical {
borderColor += box(Color.ORANGE)
padding = box(5.px)
and(button) {
backgroundColor += Color.RED
textFill = Color.WHITE
}
}
}
}

class MyApp: App(MyView::class, MyStyle::class) {
init {
reloadStylesheetsOnFocus()
}
}
class MyView: View() {
override val root = hbox {
addClass(MyStyle.critical)
button("Warning!") {
addClass(MyStyle.critical)
}
button("Danger!") {
addClass(MyStyle.critical)
}
}
}

Figure 6.8

84

6. Type Safe CSS

Now you have orange borders around the

HBox

as well as the buttons. When nesting

styles, keep in mind that wrapping the selection with

and()

will cascade styles to children

controls or classes.

Mixins
There are times you may want to reuse a set of stylings and apply them to several controls
and selectors. This prevents you from having to redundantly define the same properties and
values. For instance, if you want to create a set of styling called

redAllTheThings

define it as a mixin as shown below. Then you can reuse it for a

redStyle

a

textInput

,a

label

, and a

passwordField

, you could

class, as well as

with additional style modifications (Figure

6.9).
Stylesheet

85

6. Type Safe CSS

import javafx.scene.paint.Color
import javafx.scene.text.FontWeight
import tornadofx.*
class Styles : Stylesheet() {
companion object {
val redStyle by cssclass().
}
init {
val redAllTheThings = mixin {
backgroundInsets += box(5.px)
borderColor += box(Color.RED)
textFill = Color.RED
}
redStyle {
+redAllTheThings
}
s(textInput, label) {
+redAllTheThings
fontWeight = FontWeight.BOLD
}
passwordField {
+redAllTheThings
backgroundColor += Color.YELLOW
}
}
}

App and View

86

6. Type Safe CSS

class MyApp: App(MyView::class, Styles::class)
class MyView : View("My View") {
override val root = vbox {
label("Enter your login")
form {
fieldset{
field("Username") {
textfield()
}
field("Password") {
passwordfield()
}
}
}
button("Go!") {
addClass(Styles.redStyle)
}
}
}

Figure 6.9

The stylesheet is applied to the application by adding it as a constructor parameter to the
App class. This is a vararg parameter, so you can send in a comma separated list of multiple
stylesheets. If you want to load stylesheets dynamically based on some condition, you can
call

importStylesheet(Styles::class

call to

importStylesheet

from anywhere. Any UIComponent opened after the

will get the stylesheet applied. You can also load normal text based

css stylesheets with this function:
importStylesheet("/mystyles.css")

Loading a text based css stylesheet
If you find you are repeating yourself setting the same CSS properties to the same values,
you might want to consider using mixins and reusing them wherever they are needed in a
Stylesheet

.

87

6. Type Safe CSS

Modifier Selections
TornadoFX also supports modifier selections by leveraging

and()

functions within a

selection. The most common case this is handy is styling for "selected" and cursor "hover"
contexts for a control.
If you wanted to create a UI that will make any
selected

in data controls such as

Cell

Button

ListView

red when it is hovered over, and any

red, you can define a

Stylesheet

like

this (Figure 6.10).
Stylesheet
import javafx.scene.paint.Color
import tornadofx.Stylesheet
class Styles : Stylesheet() {
init {
button {
and(hover) {
backgroundColor += Color.RED
}
}
cell {
and(selected) {
backgroundColor += Color.RED
}
}
}
}

App and View
import tornadofx.*
class MyApp: App(MyView::class, Styles::class)
class MyView : View("My View") {
val listItems = listOf("Alpha","Beta","Gamma").observable()
and
override val root = vbox {
button("Hover over me")
listview(listItems)
}
}

88

6. Type Safe CSS

Figure 6.10 - A cell is selected and the

Button

is being hovered over. Both are now red.

Whenever you need modifiers, use the

select()

function to make those contextual style

modifications.

Control-Specific Stylesheets
If you decide to create your own controls (often by extending an existing control, like
Button

), JavaFX allows you to pair a stylesheet with it. In this situation, it is advantageous

to load this

Stylesheet

DangerButton

only when this control is loaded. For instance, if you have a

class that extends

specifically for that

DangerButton

getUserAgentStyleSheet()
Stylesheet

Button

, you might consider creating a

Stylesheet

. To allow JavaFX to load it, you need to override the

function as shown below. This will convert your type-safe

into plain text CSS that JavaFX natively understands.

class DangerButton : Button("Danger!") {
init {
addClass(DangerButtonStyles.dangerButton)
}
override fun getUserAgentStylesheet() = DangerButtonStyles().base64URL.toExternalF
orm()
}
class DangerButtonStyles : Stylesheet() {
companion object {
val dangerButton by cssclass()
}
init {
dangerButton {
backgroundInsets += box(0.px)
fontWeight = FontWeight.BOLD
fontSize = 20.px
padding = box(10.px)
}
}
}

89

6. Type Safe CSS

The

DangerButtonStyles().base64URL.toExternalForm()

DangerButtonStyles

expression creates an instance of the

, and turns it into a URL containing the entire stylesheet that JavaFX

can consume.

Conclusion
TornadoFX does a great job executing a brilliant concept to make CSS type-safe, and it
further demonstrates the power of Kotlin DSL's. Configuration through static text files is slow
to express with, but type-safe CSS makes it fluent and quick especially with IDE autocompletion. Even if you are pragmatic about UI's and feel styling is superfluous, there will be
times you need to leverage conditional formatting and highlighting so rules "pop out" in a UI.
At minimum, get comfortable using the inline

style { }

block so you can quickly access

styling properties that cannot be accessed any other way (such as

TextWeight

).

90

7. Layouts and Menus

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()

growing behaviors of the

VBox

within a child's block to change the margin and vertical

.

91

7. Layouts and Menus

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()

horizontal growing behaviors of the

within the a child's block to change the margin and
HBox

.

92

7. Layouts and Menus

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

93

7. Layouts and Menus

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

bottom

,

right

is a highly useful layout that divides controls into 5 regions:
, and

center

top

,

left

,

. Many UI's can easily be built using two or more of these

regions to hold controls (Figure 7.5).

94

7. Layouts and Menus

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
left

,

center

, and

top

right

and

bottom

regions take up the entire horizontal space, while

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

region and put a

. For instance, you may vertically stack some buttons in the

TableView

in the

center

left

region (Figure 7.6).

95

7. Layouts and Menus

borderpane {
left = vbox {
button("REFRESH")
button("COMMIT")
}
center

= tableview {

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

UI's. The

is a layout you will likely want to use often because it simplifies many complex

top

region is commonly used to hold a

MenuBar

holds a status bar of some kind. You have already seen
as a

TableView

, and

left

and

right

and the

center

bottom

region often

hold the focal control such

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

96

7. Layouts and Menus

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

97

7. Layouts and Menus

98

7. Layouts and Menus

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

99

7. Layouts and Menus

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.

100

7. Layouts and Menus

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
builder. It can also be specified as

hbox(spacing = 20)

hbox

for clarity.

Figure 7.9

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.

101

7. Layouts and Menus

One way to use
can call its
such as

GridPane

is to declare the contents of each
to configure various

gridpaneConstraints

margin

and

row

GridPane

. For any given

Node

behaviors for that

Node

you
,

(Figure 7.10)

columnSpan

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
marginBottom

and

marginTop

gridpaneConstraints

10.0

between each row, which was declared for the

of the "North" and "South" buttons respectively inside their

.

Alternatively, you can explicitly specify the column/row index positions for each
than declaring each

row

Node

rather

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

102

7. Layouts and Menus

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.

103

7. Layouts and Menus

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

fillHeight: Boolean

Sets whether the
area

Node

fills the height of its

fillWidth: Boolean

Sets whether the
area

Node

filles the width of its

fillHeightWidth: Boolean

Sets whether the
height and width

Node

fills its area for both

hAlignment: HPos

The horizonal alignment policy

vAlignment: VPos

The vertical alignment policy

margin: Int

The margin for all four sides of the

marginBottom: Int

The margin for the bottom side of the

marginTop: Int

The margin for the top side of the

marginLeft: Int

The left margin for the left side of the

marginRight: Int

The right margin for the right side of the

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
gridpaneColumnConstraints

the

GridPane

vGrow

and

hGrow

ColumnConstraints

on any child

Node

, or

Node
Node

Node
Node
Node

, you can call

constraintsForColumn(columnIndex)

on

itself.

104

7. Layouts and Menus

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

105

7. Layouts and Menus

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
need. For each

tab()

tabpane()

and then declare as many

function pass in the name of the

Tab

tab()

instances as you

and the parent

Node

control

to populate it.
tabpane {
tab("Screen 1", VBox()) {
button("Button 1")
button("Button 2")
}
tab("Screen 2", HBox()) {
button("Button 3")
button("Button 4")
}
}

Figure 7.12

TabePane

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
inside the

tab()

TableView

right

block (Figure 7.13).

106

7. Layouts and Menus

tabpane {
tab("Screen 1", VBox()) {
button("Button 1")
button("Button 2")
}
tab("Screen 2", HBox()) {
tableview {
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

107

7. Layouts and Menus

Like many builders, the

has several properties that can adjust the behavior of its

TabPane

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

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.

MenuBar, Menu, and MenuItem
It is not uncommon to use navigable menus to keep a large number of commands on a user
interface organized. For instance, the
MenuBar

top

region of a

BorderPane

is typically where a

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

108

7. Layouts and Menus

Figure 7.14

You can also optionally provide keyboard shortcuts, graphics, as well as an
parameter for each

item()

action

function

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!")
}
menu("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

109

7. Layouts and Menus

Separators
You can declare a

separator()

between two items in a

is helpful to group commands in a

Menu

Menu

to create a divider line. This

and distinctly separate them (Figure 7.15).

menu("File") {
menu("Connect") {
item("Facebook")
item("Twitter")
}
separator()
item("Save","Shortcut+S") {
println("Saving!")
}
item("Quit","Shortcut+Q") {
println("Quitting!")
}
}

Figure 7.15

110

7. Layouts and Menus

ContextMenu
Most controls in JavaFX have a
instance. This is a
A

ContextMenu

MenuBar

Menu

contextMenu

property where you can assign a

ContextMenu

that pops up when the control is right-clicked.

has functions to add

. It can be helpful to add a

Menu

and

ContextMenu

MenuItem

to a

instances to it just like a

TableView

, 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::id)
column("Name", Person::name)
column("Birthday", Person::birthday)
column("Age", Person::age)
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

111

7. Layouts and Menus

Note there are also
The

RadioMenuItem

and

CheckMenuItem

variants of

MenuItem

available.

builders take the action to perform when the menu is selected as the op

menuitem

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

common case in a more concise way than the

builder is not deprecated, as it solves the

item

builder.

ListMenu
TornadoFX comes with a list menu that behaves and looks more like a typical

ul/li

based

HTML5 menu.

112

7. Layouts and Menus

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

:

113

7. Layouts and Menus

Attribute

orientation

iconPosition

theme

tag

BuilderAttribute

yes

yes

yes

yes

Type

Orientation

Side

String

Any?

Default

Description

VERTICAL

Configures the
orientation of the
ListMenu . Possible
orientations:
VERTICAL
HORIZONTAL

LEFT

Configures the icon
position of the ListMenu .
Possible positions:
TOP
BOTTOM
LEFT
RIGHT

null

Currently supported
themes blue , null . If
null is set the default
gray theme is used.

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

114

7. Layouts and Menus

Css-Class

CssProperty

Default

Description

-fx-graphicfixed-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

.list-menu

item

.list-menu
.list-item

-fxbackgroundcolor

-fx-shadow-highlight-color, -fx-outerborder, -fx-inner-border, -fx-bodycolor

The color of
the item

.list-menu
.list-item

-fxbackgroundinsets

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
PseudoClass

CssProperty

Default

Description

.list-menu
.listitem:active

-fxbackgroundcolor

-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
.listitem:active

-fxbackgroundinsets

-0.2, 1, 2, -1.4, 2.6

Insets will be set
if the item is
active.

.list-menu
.listitem: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:

115

7. Layouts and Menus

item("SomeText", graphic = SomeNode, tag = SomeObject) {
// Marks this item as active.
activeItem = this
// Do some action when selected
whenSelected { /* Action */ }
}

Attribute

BuilderAttribute

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
and can be useful to identify the

Type

Default

Description

null

ListItem

graphic

yes

Node?

The graphic can be any Node and
will be displayed beside the given
text .

null

Function

Description

whenSelected

A convince function, which will be called anytime the given
ListMenuItem is selected.

Filling the parent container
The

useMaxWidth

useMaxHeight

property can be used to fill the parent container horizontally. The

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.

116

7. Layouts and Menus

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.

You can optionally allow folds to be closable by clicking a cross in the right corner of the title
pane for the fold. You enable the close buttons on a per fold basis by passing
true

to the

fold

closeable =

builder.

117

7. Layouts and Menus

squeezebox {
fold("Customer Editor", expanded = true, closeable = true) {
form {
fieldset("Customer Details") {
field("Name") { textfield() }
field("Password") { textfield() }
}
}
}
fold("Some other editor", closeable = true) {
stackpane {
label("Nothing here")
}
}
}

Figure 7.18

This SqueezeBox has closeable folds
The

closeable

property can of course be combined with

expanded

.

Another important difference between the SqueezeBox and the Accordion is the way it
distributes overflowing space. The Accordion will extend vertically to fill its parent container
and push any folds below the currently opened ones all the way to the bottom. This creates
an unnatural looking view if the parent container is very large. The squeezebox probably
does what you want by default in this regard, but you can add

fillHeight = true

to get a

similar look as the Accordion.I

118

7. Layouts and Menus

You can style the SqueezeBox like you style a TitlePane. The close button has a css class
called

close-button

and the container has a css class called

squeeze-box

.

Drawer
The Drawer is a navigation component much like a TabPane, but it organizes each drawer
item in a vertically or horizontally placed button bar on either side of the parent container. It
resembles the tool drawers found in many popular business applications and IDEs. When an
item is selected, the content for the item is displayed next to or above/below the buttons in a
content area spanning the height or width of the control and the preferred width or height of
the content, depending on whether it is docked in a vertical or horizontal side of the parent.
In

multiselect

mode it will even let you open multiple drawer items simutaneously and

have them share the space between them. They will always open in the order of the
corresponding buttons.

119

7. Layouts and Menus

class DrawerView : View("TornadoFX Info Browser") {
override val root = drawer {
item("Screencasts", expanded = true) {
webview {
prefWidth = 470.0
engine.userAgent = iPhoneUserAgent
engine.load(TornadoFXScreencastsURI)
}
}
item("Links") {
listview(links) {
cellFormat { link ->
graphic = hyperlink(link.name) {
setOnAction {
hostServices.showDocument(link.uri)
}
}
}
}
}
item("People") {
tableview(people) {
column("Name", Person::name)
column("Nick", Person::nick)
}
}
}
class Link(val name: String, val uri: String)
class Person(val name: String, val nick: String)
// Sample data variables left out (iPhoneUserAgent, TornadoFXScreencastsURI, people
and links)
}

Figure 7.19

120

7. Layouts and Menus

The drawer can be configured to show the buttons on the right side, and you can choose to
support opening multiple drawer items simultaneously. When running in multiselect mode, a
header will appear above the content, which will help to distinguish the items in the content
area. You can control the header appearance with the boolean

showHeader

parameter. It will

default true when multiselect is enabled and false otherwise.

121

7. Layouts and Menus

drawer(side = Side.RIGHT, multiselect = true) {
// Everything else is identical
}

Figure 7.20

Drawer with buttons on the right side, multiselect mode and title panes
When the Drawer is added next to something, you can choose whether the content area of
the Drawer should displace the nodes next to it (default) or float over it. The
floatingContent

property is by default false, causing the Drawer to displace the content

122

7. Layouts and Menus

next to it.
You can control the size of the content area further using the
fixedContentSize

properties of

Drawer

. Depending on the

maxContentSize
dockingSide

and

, those properties

will constrain either the width or the height of the content area.
The

Workspace

rightDrawer

features built in support for the Drawer control. The

and

bottomDrawer

leftDrawer

,

properties of any Workspace will let you dock drawer items

into them. Read more about this in the Workspace chapter.

Converting observable list items and binding
to layouts
TODO

Summary
By now you should have the tools to quickly create complex UI's with layouts, tabbed panes,
as well as other controls to manage controls. Using these in conjunction with the data
controls, you should be able to turn around UI's in a fraction of the time.
When it comes to builders, you have reached the top of the peak and have everything you
need to be productive. All that is left to cover are charts and shapes, which we will cover in
the next two chapters.

123

8. Charts

Charts
JavaFX comes with a handy set of charts to quickly display data visualizations. While there
are more comprehensive charting libraries like JFreeChart and Orson Charts which work
fine with TornadoFX, the built-in JavaFX charts satisfy a majority of visualization needs.
They also have elegant animations when data is populated or changed.
TornadoFX comes with a few builders to streamline the declaration of charts using functional
constructs.

PieChart
The

PieChart

is a common visual aid to illustrate proportions of a whole. It is structurally

simpler than XY charts which we will learn about later. Inside a
call the

data()

piechart()

builder you can

function to pass multiple category-value pairs (Figure 8.1).

piechart("Desktop/Laptop OS Market Share") {
data("Windows", 77.62)
data("OS X", 9.52)
data("Other", 3.06)
data("Linux", 1.55)
data("Chrome OS", 0.55)
}

Figure 8.1

124

8. Charts

Note you can also provide an explicit

ObservableList

prepared in advance.

val items = listOf(
PieChart.Data("Windows", 77.62),
PieChart.Data("OS X", 9.52),
PieChart.Data("Other", 3.06),
PieChart.Data("Linux", 1.55),
PieChart.Data("Chrome OS", 0.55)
).observable()
piechart("Desktop/Laptop OS Market Share", items)

The block following

piechart

can be used to modify any of the attributes of the

just like any other control builder we covered. You can also leverage

for()

PieChart

loops,

Sequences, and other iterative tools within a block to add any number of data items.

125

8. Charts

val items = listOf(
PieChart.Data("Windows", 77.62),
PieChart.Data("OS X", 9.52),
PieChart.Data("Other", 3.06),
PieChart.Data("Linux", 1.55),
PieChart.Data("Chrome OS", 0.55)
).observable()
piechart("Desktop/Laptop OS Market Share") {
for (item in items) {
data.add(item)
}
}

Map-Based Data Sources
Sometimes you may want to build a chart using a
operator, you can construct a

to

Map

Map

as a datasource. Using the Kotlin

in a Kotlin-esque way and then pass it to the

data

function.
val items = mapOf(
"Windows" to 77.62,
"OS X" to 9.52,
"Other" to 3.06,
"Linux" to 1.55,
"Chrome OS" to 0.55
)
piechart("Desktop/Laptop OS Market Share") {
data(items)
}

XY Based Charts
Most charts often deal with one or more series of data points on an XY axis. The most
common are bar and line charts.

Bar Charts
You can represent one or more series of data points through a

BarChart

. This chart makes

it easy to compare different data points relative to their distance from the X or Y axis (Figure
8.2).

126

8. Charts

barchart("Unit Sales Q2 2016", CategoryAxis(), NumberAxis()) {
series("Product X") {
data("MAR", 10245)
data("APR", 23963)
data("MAY", 15038)
}
series("Product Y") {
data("MAR", 28443)
data("APR", 22845)
data("MAY", 19045)
}
}

Figure 8.2

Above, the

series()

and

data()

functions allow quick construction of data structures

backing the charts. On construction, you will need to construct the proper

Axis

type for

each X and Y axis. In this example, the months are not necessarily numeric but rather
Strings. Therefore they are best represented by a
numeric, are fit to use a

NumberAxis

CategoryAxis

. The units, already being

.

127

8. Charts

In the

series()

and

You can even call

data()

style()

blocks, you can customize further properties like colors.

to quickly apply type-safe CSS to the chart.

LineChart and AreaChart
A

LineChart

connects data points on an XY axis with lines, quickly visualizing upward and

downward trends between them (Figure 8.3)
linechart("Unit Sales Q2 2016", CategoryAxis(), NumberAxis()) {
series("Product X") {
data("MAR", 10245)
data("APR", 23963)
data("MAY", 15038)
}
series("Product Y") {
data("MAR", 28443)
data("APR", 22845)
data("MAY", 19045)
}
}

Figure 8.3

128

8. Charts

The backing data structure is not much different than a
series()

and

data()

BarChart

, and you use the

functions in the same manner.

You can also use a variant of

LineChart

called

AreaChart

, which will shade the area under

the lines a distinct color, as well as any overlaps (Figure 8.4).
Figure 8.4

129

8. Charts

Multiseries
You can streamline the declaration of more than one series using the
function, and call the

data()

functions with

varargs

multiseries()

values. We can consolidate our

previous example using this construct:
linechart("Unit Sales Q2 2016", CategoryAxis(), NumberAxis()) {
multiseries("Product X", "Product Y") {
data("MAR", 10245, 28443)
data("APR", 23963, 22845)
data("MAY", 15038, 19045)
}
}

This is just another convenience to reduce boilerplate and quickly declare your data
structure for a chart.

130

8. Charts

ScatterChart
A

ScatterChart

is the simplest representation of an XY data series. It plots the points

without bars or lines. It is often used to plot a large volume of data points in order to find
clusters. Here is a brief example of a

ScatterChart

plotting machine capacities by week for

two different product lines (Figure 8.5).
scatterchart("Machine Capacity by Product/Week", NumberAxis(), NumberAxis()) {
series("Product X") {
data(1,24)
data(2,22)
data(3,23)
data(4,19)
data(5,18)
}
series("Product Y") {
data(1,12)
data(2,15)
data(3,9)
data(4,11)
data(5,7)
}
}

Figure 8.5

131

8. Charts

BubbleChart
BubbleChart

is another XY chart similar to the

ScatterPlot

, but there is a third variable to

control the radius of each point. You can leverage this to show, for instance, output by week
with the bubble radii reflecting number of machines used (Figure 8.6).

132

8. Charts

bubblechart("Machine Capacity by Output/Week", NumberAxis(), NumberAxis()) {
series("Product X") {
data(1,24,1)
data(2,46,2)
data(3,23,1)
data(4,27,2)
data(5,18,1)
}
series("Product Y") {
data(1,12,1)
data(2,31,2)
data(3,9,1)
data(4,11,1)
data(5,15,2)
}
}

Figure 8.6

133

8. Charts

Summary
Charts are a an effective way to visualize data, and the builders in TornadoFX help create
them quickly. You can read more about JavaFX charts in Oracle's documentation. If you
need more advanced charting functionality, there are libraries like JFreeChart and Orson
Charts you can leverage and interop with TornadoFX, but this is beyond the scope of this
book.

134

9. Shapes and Animation

Shapes and Animation
JavaFX comes with nodes that represent almost any geometric shape as well as a

Path

node that provides facilities required for assembly and management of a geometric path (to
create custom shapes). JavaFX also has animation support to gradually change a

Node

property, creating a visual transition between two states. TornadoFX seeks to streamline all
these features through builder constructs.

Shape Basics
Every parameter to the shape builders are optional, and in most cases default to a value of
0.0

. This means that you only need to provide the parameters you care about. The

builders have positional parameters for most of the properties of each shape, and the rest
can be set in the functional block that follows. Therefore these are all valid ways to create a
rectangle:
rectangle {
width = 100.0
height = 100.0
}
rectangle(width = 100.0, height = 100.0)
rectangle(0.0, 0.0, 100.0, 100.0)

The form you choose is a matter of preference, but obviously consider the legibility of the
code you write. The examples in this chapter specify most of the properties inside the code
block for clarity, except when there is no code block support or the parameters are
reasonably self-explanatory.

Positioning within the Parent
Most of the shape builders give you the option to specify the location of the shape within the
parent. Whether or not this will have any effect depends on the parent node. An
not care about the

x

and

y

coordinates you specify unless you call

on the shape. However, a

Group

be created by wrapping a

StackPane

was created inside that

Group

HBox

will

setManaged(false)

control will. The screenshots in the following examples will
with padding around a

Group

, and finally the shape

as shown below.

135

9. Shapes and Animation

class MyView: View() {
override val root =

stackpane {

group {
//shapes will go here
}
}
}

Rectangle
Rectangle

defines a rectangle with an optional size and location in the parent. Rounded

corners can be specified with the

arcWidth

and

arcHeight

properties (Figure 9.1).

rectangle {
fill = Color.BLUE
width = 300.0
height = 150.0
arcWidth = 20.0
arcHeight = 20.0
}

Figure 9.1

136

9. Shapes and Animation

Arc
Arc

represents an arc object defined by a center, start angle, angular extent (length of the

arc in degrees), and an arc type (

OPEN

,

CHORD

, or

ROUND

) (Figure 9.2)

arc {
centerX = 200.0
centerY = 200.0
radiusX = 50.0
radiusY = 50.0
startAngle = 45.0
length = 250.0
type = ArcType.ROUND
}

Figure 9.2

Circle
Circle

represents a circle with the specified

radius

and

center

.

circle {
centerX = 100.0
centerY = 100.0
radius = 50.0
}

137

9. Shapes and Animation

CubicCurve
CubicCurve

represents a cubic Bézier parametric curve segment in (x,y) coordinate space.

Drawing a curve that intersects both the specified coordinates (
(

endX

,

enfY

controlY2

), using the specified points (

controlX1

,

startX

controlY1

,

) and (

startY

) and

controlX2

,

) as Bézier control points.

cubiccurve {
startX = 0.0
startY = 50.0
controlX1 = 25.0
controlY1 = 0.0
controlX2 = 75.0
controlY2 = 100.0
endX = 150.0
endY = 50.0
fill = Color.GREEN
}

138

9. Shapes and Animation

Ellipse
Ellipse

represents an ellipse with parameters specifying size and location.

ellipse {
centerX = 50.0
centerY = 50.0
radiusX = 100.0
radiusY = 50.0
fill = Color.CORAL
}

Line
Line is fairly straight forward. Supply start and end coordinates to draw a line between the
two points.
line {
startX = 50.0
startY = 50.0
endX = 150.0
endY = 100.0
}

139

9. Shapes and Animation

Polyline
A

is defined by an array of segment points.

Polyline

Polyline

is similar to

Polygon

,

except it is not automatically closed.
polyline(0.0, 0.0, 80.0, 40.0, 40.0, 80.0)

QuadCurve
The

Quadcurve

represents a quadratic Bézier parametric curve segment in (x,y) coordinate

space. Drawing a curve that intersects both the specified coordinates (
and (

endX

,

endY

), using the specified point (

controlX

,

controlY

startX

,

startY

)

) as Bézier control point.

140

9. Shapes and Animation

quadcurve {
startX = 0.0
startY = 150.0
endX = 150.0
endY = 150.0
controlX = 75.0
controlY = 0.0
fill = Color.BURLYWOOD
}

SVGPath
SVGPath

represents a shape that is constructed by parsing SVG path data from a String.

svgpath("M70,50 L90,50 L120,90 L150,50 L170,50 L210,90 L180,120 L170,110 L170,200 L70,
200 L70,110 L60,120 L30,90 L70,50") {
stroke = Color.DARKGREY
strokeWidth = 2.0
effect = DropShadow()
}

141

9. Shapes and Animation

Path
Path

represents a shape and provides facilities required for basic construction and

management of a geometric path. In other words, it helps you create a custom shape. The
following helper functions can be used to constuct the path:
moveTo(x, y)
hlineTo(x)
vlineTo(y)
quadqurveTo(controlX, controlY, x, y)
lineTo(x, y)
arcTo(radiusX, radiusY, xAxisRotation, x, y, largeArcFlag, sweepFlag)
closepath()

142

9. Shapes and Animation

path {
moveTo(0.0, 0.0)
hlineTo(70.0)
quadqurveTo {
x = 120.0
y = 60.0
controlX = 100.0
controlY = 0.0
}
lineTo(175.0, 55.0)
arcTo {
x = 50.0
y = 50.0
radiusX = 50.0
radiusY = 50.0
}
}

Animation
JavaFX has tools to animate any

Node

by gradually changing one or more of its properties.

There are three components you work with to create animations in JavaFX.
Timeline

- A sequence of

KeyFrame

- A "snapshot" specifying value changes on one or more writable properties

(via a

KeyValue

KeyValue

KeyFrame

items executed in a specified order

) on one or more Nodes

- A pairing of a

Node

property to a value that will be "transitioned" to

143

9. Shapes and Animation

A

is the basic building block of JavaFX animation. It specifies a property and the

KeyValue

"new value" it will gradually be transitioned to. So if you have a
rotateProperty()

of

0.0

, and you specify a

will incrementally move from

0.0

to

90.0

Rectangle

that changes it to

KeyValue

degrees. Put that

KeyValue

with a
90.0

inside a

degrees, it
KeyFrame

which will specify how long the animation between those two values will last. In this case
let's make it 5 seconds. Then finally put that

KeyFrame

in a

Timeline

. If you run the code

below, you will see a rectange gradually rotate from `0.0` to `90.0` degrees in 5 seconds
(Figure 9.1).
val rectangle = rectangle(width = 60.0,height = 40.0) {
padding = Insets(20.0)
}
timeline {
keyframe(Duration.seconds(5.0)) {
keyvalue(rectangle.rotateProperty(),90.0)
}
}

Figure 9.1

In a given

KeyFrame

, you can simultaneously manipulate other properties in that 5-second

window too. For instance we can transition the
arcHeightProperty()

while the

Rectangle

arcWidthProperty()

and

is rotating (Figure 9.2)

timeline {
keyframe(Duration.seconds(5.0)) {
keyvalue(rectangle.rotateProperty(),90.0)
keyvalue(rectangle.arcWidthProperty(),60.0)
keyvalue(rectangle.arcHeightProperty(),60.0)
}
}

Figure 9.2

144

9. Shapes and Animation

Interpolators
You can also specify an

Interpolator

instance, you can specify

which can add subtle effects to the animation. For

Interpolator.EASE_BOTH

to accelerate and decelerate the value

change at the beginning and end of the animation gracefully.
val rectangle = rectangle(width = 60.0, height = 40.0) {
padding = Insets(20.0)
}
timeline {
keyframe(5.seconds) {
keyvalue(rectangle.rotateProperty(), 180.0, interpolator = Interpolator.EASE_B
OTH)
}
}

Cycles and AutoReverse
You can modify other attributes of the
The

cycleCount

isAutoReverse

timeline()

such as

cycleCount

and

autoReverse

.

will repeat the animation a specified number of times, and setting the
to true will cause it to revert back with each cycle.

timeline {
keyframe(5.seconds) {
keyvalue(rectangle.rotateProperty(), 180.0, interpolator = Interpolator.EASE_B
OTH)
}
isAutoReverse = true
cycleCount = 3
}

To repeat the animation indefinitely, set the

cycleCount

to

Timeline.INDEFINITE

.

Shorthand Animation

145

9. Shapes and Animation

If you want to animate a single property, you can quickly animate it without declaring a
timeline()

,

keyframe()

propert and provide the

, and

keyset()

endValue

, the

. Call the

duration

animate()

extension function on that

, and optionally the

interoplator

. This is

much shorter and cleaner if you are animating just one property.
rectangle.rotateProperty().animate(endValue = 180.0, duration = 5.seconds)

Summary
In this chapter we covered builders for shape and animation. We did not cover JavaFX's
Canvas

as this is beyond the scope of the

TornadoFX

framework. It could easily take up

more than several chapters on its own. But the shapes and animation should allow you to do
basic custom graphics for a majority of tasks.
This concludes our coverage of TornadoFX builders for now. Next we will cover FXML for
those of us that have need to use it.

146

10. FXML

FXML and Internationalization
TornadoFX's type-safe builders provide a fast, easy, and declarative way to construct UI's.
This DSL approach is encouraged because it is more flexible, reliable, and simpler.
However, JavaFX also supports an XML-based structure called FXML that can also build a
UI layout. TornadoFX has tools to streamline FXML usage for those that need it.
If you are unfamiliar with FXML and are perfectly happy with type-safe builders, please feel
free to skip this chapter. If you need to work with FXML or feel you should learn it, please
read on. You can also take a look at the official FXML documentation to learn more.

Reasons for Considering FXML
While the developers of TornadoFX strongly encourage using type-safe builders, there are
situations and factors that might cause you to consider using FXML.

Separation of Concerns
With FXML it is easy to separate your UI logic code from the UI layout code. This separation
is just as achievable with type-safe builders by utilizing MVP or other separation pattern. But
some programmers find FXML forces them to maintain this separation and prefer it for that
reason.

WYSIWYG Editor
FXML files also can be edited and processed by Scene Builder, a visual layout tool that
allows building interfaces via drag-and-drop functionality. Edits in Scene Builder are
immediately rendered in a WYSIWYG ("What You See is What You Get") pane next to the
editor.
If you prefer making interfaces via drag-and-drop, or have trouble building UI's with pure
code, you might consider using FXML simply to leverage Scene Builder.
The Scene Builder tool was created by Oracle/Sun but is now maintained by Gluon, an
innovative company that invests heavily in JavaFX technology, especially for the mobile
market.

Compatibility with Existing Codebases

147

10. FXML

If you are converting an existing JavaFX application to TornadoFX, there is a strong chance
your UI was constructed with FXML. If you hesitate to transition legacy FXML to TornadoFX
buidlers, or would like to put that off as long as possible, TornadoFX can at least streamline
the processing of FXML.

How FXML works
The

root

property of a

View

represents the top level

Node

containing a hierarchy of

children Nodes, which makes up the user interface. When you work with FXML, you do not
instantiate this root node directly, but instead ask TornadoFX to load it from a corresponding
FXML file. By default, TornadoFX will look for a file with the same name as your view with
the

.fxml

file ending in the same package as your

View

class. You can also override the

FXML location with a parameter if you want to put all your FXML files in a single folder or
organize them some other way that does not directly correspond to your

View

location.

A Simple Example
Let's create a basic user interface that presents a

Label

functionality to this view so when the

Button

with the number of times the

has been clicked.

Create a file named

Button

CounterView.fxml

and a

is clicked, the

Button

Label

. We will add

will update its

text

with the following content:

148

10. FXML











You may notice above you have to import the types you use in FXML just like coding in Java or Kotlin. Intellij IDEA should have a plugin to support using ALT+ENTER to generate the import statements. If you load this file in Scene Builder you will see the following result (Figure 9.1). Figure 9.1 Next let's load this FXML into TornadoFX. Loading FXML into TornadoFX 149 10. FXML We have created an FXML file containing our UI structure, but now we need to load it into a TornadoFX root View for it to be usable. Logically, we can load this node of our . Define the following View View Node hierarchy into the class: class CounterView : View() { override val root : BorderPane by fxml() } Note that the root property is defined by the takes care of loading the corresponding placed CounterView delegate. The into the CounterView.fxml in a different location (such as CounterView.fxml where the fxml() /views/ fxml() root delegate property. If we ) that is different than file resides, we would add a parameter. class CounterView : View() { override val root : BorderPane by fxml("/views/CounterView.fxml") } We have laid out the UI, but it has no functionality yet. We need to define a variable that holds the number of times the Button has been clicked. Add a variable called counter and define a function that will increment its value: class CounterView : View() { override val root : BorderPane by fxml() val counter = SimpleIntegerProperty() fun increment() { counter.value += 1 } } We want the function to be called whenever the increment() FXML file, add the onAction Button is clicked. Back in the attribute to the button:

Source Exif Data:
File Type                       : PDF
File Type Extension             : pdf
MIME Type                       : application/pdf
PDF Version                     : 1.4
Linearized                      : No
Author                          : edvin
Create Date                     : 2017:09:11 23:44:17+00:00
Producer                        : calibre 2.57.1 [http://calibre-ebook.com]
Title                           : TornadoFX Guide
Description                     : This is a work-in-progress to fully document the TornadoFX framework in the format of a book.
Creator                         : edvin
Subject                         : 
Publisher                       : GitBook
Language                        : en
Metadata Date                   : 2017:09:11 23:44:17.847970+00:00
Timestamp                       : 2017:09:11 23:44:10.263711+00:00
Page Count                      : 276
EXIF Metadata provided by EXIF.tools

Navigation menu