Lightweight JavaFX Framework for Kotlin
- Supports both MVC, MVP and their derivatives
- Dependency injection
- Type safe GUI builders
- Type safe CSS builders
- First class FXML support
- Async task execution
- EventBus with thread targeting
- Hot reload of Views and Stylesheets
- OSGi support
- REST client with automatic JSON conversion
- Zero config, no XML, no annotations
TornadoFX requires Kotlin 1.1.1. Make sure you update your IDE plugins to minimum:
- Kotlin Plugin: 1.1.1-release
- TornadoFX Plugin: 1.7.2
After updating IntelliJ IDEA, make sure your Kotlin target version is 1.1 (Project Settings -> Modules -> Kotlin -> Language Version / API Version)
You also need a full rebuild of your code. If you run into trouble, try to clean caches and restart IDEA (File -> Invalidate caches / Restart).
- Screencasts
- Book (EAP) We are gradually migrating all information from the Wiki into the Guide
- Wiki
- Slack
- User Forum
- Dev Forum
- Stack Overflow
- Documentation
- IntelliJ IDEA Plugin
- Example Application
- Maven QuickStart Archetype
- Changelog
mvn archetype:generate -DarchetypeGroupId=no.tornado \
-DarchetypeArtifactId=tornadofx-quickstart-archetype \
-DarchetypeVersion=1.7.2<dependency>
<groupId>no.tornado</groupId>
<artifactId>tornadofx</artifactId>
<version>1.7.2</version>
</dependency>compile 'no.tornado:tornadofx:1.7.2'Configure your build environment to use snapshots if you want to try out the latest features:
<repositories>
<repository>
<id>snapshots-repo</id>
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
<releases><enabled>false</enabled></releases>
<snapshots><enabled>true</enabled></snapshots>
</repository>
</repositories>Snapshots are published every day at GMT 16:00 if there has been any changes.
TornadoFX is now built against Kotlin 1.1.1 and compiled with jvmTarget 1.8, which means that your code must do the same. Update your build system to configure the jvmTarget accordingly.
For Maven, you add the following configuration block to kotlin-maven-plugin:
<configuration>
<jvmTarget>1.8</jvmTarget>
</configuration>For Gradle, it means configuring the kotlinOptions of the Kotlin compilation task:
compileKotlin {
kotlinOptions.jvmTarget= "1.8"
}Failing to do so will yield errors about the compiler not being able to inline certain calls.
Create a View
class HelloWorld : View() {
override val root = hbox {
label("Hello world")
}
}Load the root node from HelloWorld.fxml and inject controls by fx:id
class HelloWorld : View() {
override val root: HBox by fxml()
val myLabel: Label by fxid()
init {
myLabel.text = "Hello world"
}
}Start your application and show the primary View and add a type safe stylesheet
class HelloWorldApp : App(HelloWorld::class, Styles::class)
class Styles : Stylesheet() {
init {
label {
fontSize = 20.px
fontWeight = FontWeight.BOLD
backgroundColor += c("#cecece")
}
}
}Start app and load a type safe stylesheet
Use Type Safe Builders to quickly create complex user interfaces
class MyView : View() {
private val persons = FXCollections.observableArrayList(
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))
)
override val root = tableview(persons) {
column("ID", Person::id)
column("Name", Person::name)
column("Birthday", Person::birthday)
column("Age", Person::age)
columnResizePolicy = SmartResize.POLICY
}
}RENDERED UI
Create a Customer model object that can be converted to and from JSON and exposes both a JavaFX Property and getter/setter pairs:
import tornadofx.getValue
import tornadofx.setValue
class Customer : JsonModel {
val idProperty = SimpleIntegerProperty()
var id by idProperty
val nameProperty = SimpleStringProperty()
var name by nameProperty
override fun updateModel(json: JsonObject) {
with(json) {
id = int("id")
name = string("name")
}
}
override fun toJSON(json: JsonBuilder) {
with(json) {
add("id", id)
add("name", name)
}
}
}Create a controller which downloads a JSON list of customers with the REST api:
class HelloWorldController : Controller() {
val api : Rest by inject()
fun loadCustomers(): ObservableList<Customer> =
api.get("customers").list().toModel()
}Configure the REST API with a base URI and Basic Authentication:
with (api) {
baseURI = "http://contoso.com/api"
setBasicAuth("user", "secret")
}Load customers in the background and update a TableView on the UI thread:
runAsync {
controller.loadCustomers()
} ui {
customerTable.items = it
}Load customers and apply to table declaratively:
customerTable.asyncItems { controller.loadCustomers() }Define a type safe CSS stylesheet:
class Styles : Stylesheet() {
companion object {
// Define css classes
val heading by cssclass()
// Define colors
val mainColor = c("#bdbd22")
}
init {
heading {
textFill = mainColor
fontSize = 20.px
fontWeight = BOLD
}
button {
padding = box(10.px, 20.px)
fontWeight = BOLD
}
val flat = mixin {
backgroundInsets += box(0.px)
borderColor += box(Color.DARKGRAY)
}
s(button, textInput) {
+flat
}
}
}Create an HBox with a Label and a TextField with type safe builders:
hbox {
label("Hello world") {
addClass(heading)
}
textfield {
promptText = "Enter your name"
}
}Get and set per component configuration settings:
// set prefWidth from setting or default to 200.0
node.prefWidth(config.double("width", 200.0))
// set username and age, then save
with (config) {
set("username", "john")
set("age", 30)
save()
}Create a Fragment instead of a View. A Fragment is not a Singleton like View is, so you will
create a new instance and you can reuse the Fragment in multiple ui locations simultaneously.
class MyFragment : Fragment() {
override val root = hbox {
}
}Open it in a Modal Window:
find(MyFragment::class).openModal()Lookup and embed a View inside another Pane in one go
add(MyFragment::class)Inject a View and embed inside another Pane
val myView: MyView by inject()
init {
root.add(myFragment)
}Swap a View for another (change Scene root or embedded View)
button("Go to next page") {
action {
replaceWith(PageTwo::class, ViewTransition.Slide(0.3.seconds, Direction.LEFT)
}
}Open a View in an internal window over the current scene graph
button("Open") {
action {
openInternalWindow(MyOtherView::class)
}
}

