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 root as an argument. The following closure argument manipulates root directly by referring to it as this, which is safely interpreted as a VBox. A Button was added to the VBox by calling it's plusAssign() 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.

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, with() returns the last statement within the lambda, whereas apply() does in fact return the item it was targeting. Therefore, if you call apply() on a Button to manipulate say, its font color and action, it is helpful the Button 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 VBox

When declaring any Node, these three steps are so common that TornadoFX streamlines them for you using strategically placed extension functions, such as button() as shown below.

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 this += and apply() function call? And why are we using a function called button() instead of an actual Button?"
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 VBox (or any targetable component) has an extension function called button().
It accepts a text argument and an optional closure targeting a Button it will instantiate.
When this function is called, it will create a Button with the specified text, apply the closure to it, add it to the VBox it was called on, and then return it.

Taking this efficiency further, you can override the root in a View, but assign it a builder function and avoid needing any init and with() 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).

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.

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 Button, Label, and TextField. The next chapter will cover builders for data-driven controls like ListView, TableView, and TreeTableView.

Button

For any Pane, you can call its button() extension function to add a Button to it. You can optionally pass a text argument and a Button.() -> Unit lambda to modify its properties.

Within a Pane, this will add a 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 Label to a given Pane.
Optionally you can provide a text (of type String or Property<String>), a graphic
(of typeNode or ObjectProperty<Node>) and a Label.() -> Unit 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

You can optionally provide initial text as well as a closure to manipulate the TextField. For example, we can add a listener to its textProperty() 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 TextField to take sensitive information, you might want to consider a PasswordField 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).

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

Notice that the action block is wrapped inside the checkbox so you can access it'sisSelected 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<Boolean> 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<String> {
    items = texasCities
}

Figure 4.10

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

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 ToggleGroup to the togglebutton() function. This will ensure all ToggleButtons in that ToggleGroup 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")
    }
}

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

You can also provide a Property<LocalDate> as an argument to bind to its value.

val dateProperty = SimpleObjectProperty<LocalDate>()

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

TextArea

The TextArea allows you input multiline freeform text. You can optionally provide the initial text value as well as a block to manipulate it on declaration (Figure 4.16).

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

Figure 4.16

ProgressBar

A ProgressBar visualizes progress towards completion of a process. You can optionally 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<Double> that will bind the progress to its value as well as a block to manipulate the ProgressBar.

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<Double> 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

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

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

Keep in mind that many controls like TableView and TreeTableView already have scroll bars on them, so wrapping them in a ScrollPane is not necessary (Figure 4.22).

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 Text with formatted properties. This control is simpler and rawer than a Label, and paragraphs can be separated using \n characters (Figure 4.23).

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

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 Button, TextField and ImageView. 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.

results matching ""

    No results matching ""