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.tornadotornadofxx.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:
Since the FXML file automatically gets bound to our
the
#functionName
View,
we can reference functions via
syntax. Note that we do not add parenthesis to the function call, and you
cannot pass parameters directly. You can however add a parameter of type
javafx.event.ActionEvent
to the
increment
function if you want inspect the source
Node
of the action or check what kind of action triggered the button. For this example we do not
need it, so we leave the
increment
function without parameters.
150
10. FXML
FXML file locations
By default, build tools like Maven and Gradle will ignore any extra resources you put into
your source root folders, so if you put your FXML files there they won't be available at
runtime unless you specifically tell your build tool to include them. This could still be
problematic because IDEA might not pick up your custom resource location from the build
file, once again resulting in failure at runtime. For that resource, we recommend that you
place your FXML files in
src/main/resources
your packages, or put them all in a
views
the FXML location parameter to the
fxml
and either follow the same folder structure as
folder or similar. The latter requires you to add
delegate, and might be messy if you have a large
number of Views, so going with the default is a good idea.
Accessing Nodes with the
Using just FXML, we have wired the
still need to bind the
an identifier for the
counter
Label
Button
value to the
to call
text
fxid
delegate
increment()
property of the
, so in our FXML file we add the
every time it is called. We
Label
. To do this, we need
attribute to it.
fx:id
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