-
Notifications
You must be signed in to change notification settings - Fork 272
Components
Wiki ▸ Documentation ▸ Components
The visual parts of a Tornado FX application is comprised of UI Components called View
and Fragment
. They behave exactly the same with one crucial difference: View
is a singleton, so there will be only one instance of any given View
in your application, while Fragment
behaves like a prototype object, meaning that a new instance will be created every time you look one up.
Note: For all other purposes they are the same, so for brevity we will simply refer to UI Components as views from now on.
A View
will contain your view controller logic, as well as the actual hierarchy of Java FX nodes that comprises the user interface. You can choose to build your UI with Kotlin or FXML.
class CounterView : View() {
override val root = BorderPane()
val counter = SimpleIntegerProperty()
init {
title = "Counter"
with (root) {
style {
padding = box(20.px)
}
center {
vbox(10.0) {
alignment = Pos.CENTER
label() {
bind(counter)
style { fontSize = 25.px }
}
button("Click to increment").setOnAction {
increment()
}
}
}
}
}
fun increment() {
counter.value += 1
}
}
A Counter app with type safe inline styles, binding and an action
Instead of building your UI in Kotlin directly, you can also pull in the root node from an FXML file. You can access the root node created from FXML directly in init
.
class CounterView : View() {
override val root : BorderPane by fxml()
val counter = SimpleIntegerProperty()
val counterInfo: Label by fxid()
init {
title = "Counter"
counterInfo.bind(counter)
}
fun increment() {
counter.value += 1
}
}
The view is loaded from FXML and the Label is injected with the
fxid()
delegate.
It is also possible to use the @FXML annotation instead of fxid()
, but it is not a best practice. If you want to use it, the syntax would be @FXML lateinit var label: Label
. It is more verbose, and you get a var
instead of a val
, so use the fxid()
delegate unless you have a very good reason to avoid it.
The corresponding CounterView.fxml
would look like this:
<BorderPane xmlns="http://javafx.com/javafx/null" xmlns:fx="http://javafx.com/fxml/1">
<padding>
<Insets top="20" right="20" bottom="20" left="20"/>
</padding>
<center>
<VBox alignment="CENTER" spacing="10">
<Label fx:id="counterInfo">
<font>
<Font size="20"/>
</font>
</Label>
<Button text="Click to increment" onAction="#increment"/>
</VBox>
</center>
</BorderPane>
You can override the default convention and place the FXML anywhere you like by specifying it's location:
override val root: HBox by fxml("/views/view.fxml")
All views have a title property. The title property of the primary view is automatically bound to
the primary stage. The same goes for Fragments - if you open a Fragment in a modal using the openModal()
function, the title of the modal is bound to the Fragment title.
A View can also contain other views. You do this by adding the root node of a subview somewhere in the node hierarchy of the master view. The views themselves are not automatically linked, but you easily add a reference property to them if you need to. When you embed views, you can either look them up via the find
method, or inject them in the parent view.
class MasterView : View() {
override val root = BorderPane()
val detail: DetailView by inject()
init {
// Enable communication between the views
detail.master = this
// Assign the DetailView root node to the center property of the BorderPane
root.center = detail.root
// Find the HeaderView and assign it to the BorderPane top (alternative approach)
root.top = find(HeaderView::class)
}
}
A Master view with two embedded views. The DetailView has access to the MasterView via the master property.
It is important to note that the master
property of DetailView is not a framework feature - it is simply a property you might add to enable communication between views. You can alternatively communicate with events if you don't like the hard coupling between views.
When you add a view as a child node of another Pane
, you can use this shorthand syntax to extract the root node:
override val root = HBox()
val subview: MySubView by inject()
init {
root += subview
}
A subview added using the shorthand syntax to avoid refering to the actual root node inside the view
Note that the +=
syntax can be used to add both views and arbitrary nodes to any Pane
that can contain child nodes. It is actually just an extension function that basically just does pane.children.add(node)
for you.
When a user interface will only be used in one place at a time, a View is the better choice. For popups or other short lived objects, you might consider Fragments
instead. A complex view might contain both other Views and Fragments
. The Fragment
class also has a convenient openModal
and a corresponding closeModal
function that will open the fragment in a modal window. The openModal
function takes optional parameters to configure stageStyle
and modality
plus other options.
To have the Views reload automatically every time the Stage gains focus, start the app with the program parameter --live-views
or call reloadViewsOnFocus()
in the init
block of your App
class. See built in startup parameters for more information.
You can preserve state across reloads by returning a state object of your choosing in the pack
method of your View, and recieve this state object in the reloaded View in the unpack
method of your View. Example:
class LoginScreen : View() {
override val root: Parent by fxml()
val person = Person()
val username: TextField by fxid()
val password: TextField by fxid()
init {
title = "Login"
username.bind(person.usernameProperty())
password.bind(person.passwordProperty())
}
override fun pack() = person
override fun unpack(state: Any?) {
state as Person
person.username = state.username
person.password = state.password
}
}
A View that knows how to transfer state to the new instance when it is reloaded
To enter fullscreen you need to get a hold of the current stage
and call stage.isFullScreen = true
. The primary stage is the active stage unless you opened a modal window via view.openModal()
or manually created a stage. The primary stage is available in the variable FX.primaryStage
. To open the application in fullscreen on startup you should override start
in your app class:
class MyApp : App(MyView::class) {
override fun start(stage: Stage) {
super.start(stage)
stage.isFullScreen = true
}
}
In the following example we toggle fullscreen mode in a modal window via a button:
button("Toggle fullscreen") {
setOnAction {
with (modalStage) { isFullScreen = !isFullScreen }
}
}
Business logic is contained in a Controller
. All controllers are singletons, and can be injected into both other controllers and views.
Note: From now on, components refers to any "Controller
, View
or Fragment
". They all extend the Component
base class
Controllers might perform long running tasks, and should not run on the Java FX UI thread. Calling code on the right thread can be tedius and error prone, but Tornado FX does all the heavy lifting, leaving you to focus on your business and view logic.
The examples below will use the included Rest
controller. Please see the [Rest Client](Rest Client Documentation) for further details.
The framework adds no restrictions or assumptions as to how you use your controllers. They are simply singleton objects that you can access from other controllers and views. However, some patterns have proven extremely useful, so we'll present them here.
class CustomerController : Controller() {
val api : Rest by inject()
fun listCustomers(): ObservableList<Customer> =
api.get("customers").list().toModel()
}
Controller that can load a JSON list of customers and convert them to a
Customer
model object
To access this controller from a view, you can inject it or look it up with the find
function. The listCustomers
function might take a long time to perform, and should not run on the JavaFX UI Thread. You need to run the call itself in a background thread, and update the UI on the JavaFX UI Thread when the call completes. This can easily be achived with the background
helper function:
background {
customerController.listCustomers()
} ui {
customerTable.items = it
}
See Async Task Execution for more information.
Next: FXML