Kotlin OpenJFX
last modified January 29, 2024
This article shows how to create OpenJFX GUI applications in Kotlin.
OpenJFX is the next generation client application platform for desktop and embedded systems for use with the JDK.
Application
Application is the main class of an OpenJFX program. Its
start method is the main entry point of the application; it is the
first method to be called after the system is ready.
An application consists of a Stage and a Scene. Stage
is the top-level container, the main window of the application. Scene is the
container for the visual content of the Stage. The Scene's content is organized
in a Scene graph.
Scene graph
Scene graph is a hierarchical tree of nodes that represents all of the visual elements of the application's user interface. A single element in a scene graph is called a node. Each node is a branch node or a leaf node. Branch nodes can contain other nodes—their children. Leaf nodes do not contain other nodes. The first node in the tree is called the root node; a root node does not have a parent.
Concrete implementations of nodes include graphics primitives, controls, layout managers, images, or media. It is possible to manipulate the scene by modifying node properties. This way we can animate the nodes, apply effects, do transformations, or change their opacity.
Setting up OpenJFX
We can set up an OpenJFX with Gradle.
plugins {
kotlin("jvm") version "1.7.10"
id("org.openjfx.javafxplugin") version "0.0.13"
}
We add the org.openjfx.javafxplugin plugin.
javafx {
modules("javafx.controls")
}
We configure the OpenJFX application.
Simple example
The following is a simple OpenJFX application.
package com.zetcode
import javafx.application.Application
import javafx.scene.Scene
import javafx.scene.layout.StackPane
import javafx.stage.Stage
class SimpleEx : Application() {
override fun start(stage: Stage) {
val root = StackPane()
val scene = Scene(root, 400.0, 300.0)
stage.title = "Simple"
stage.scene = scene
stage.show()
}
}
fun main() {
Application.launch(SimpleEx::class.java)
}
The example shows a small window on the screen.
class SimpleEx : Application() {
Each OpenJFX program inherits from Application.
override fun start(stage: Stage) {
The Application's start method is overridden. It is the main entry
point to the OpenJFX program. It receives a Stage as its only
parameter. (Stage is the main application window or area.)
val root = StackPane()
StackPane is a container used for organizing nodes. It uses a
simple layout manager that places its content nodes in a back-to-front single
stack. In our case, we only want to place a single node.
val scene = Scene(root, 400.0, 300.0)
Scene is the container for all content in a scene graph. It takes a
root node as its first parameter. The StackPane is a root node in
this scene graph. The next two parameters specify the width and the height of
the scene.
stage.title = "Simple"
We set the title of the window via the title property.
stage.scene = scene
A scene is added to the stage.
stage.show()
The show method shows the window on the screen.
fun main() {
Application.launch(SimpleEx::class.java)
}
We run the application with Application.launch.
Button control
In the following example, we have a Button control. When we click
on the button, the application terminates. When a button is pressed and
released, an ActionEvent is sent.
package com.zetcode
import javafx.application.Application
import javafx.application.Platform
import javafx.event.EventHandler
import javafx.geometry.Insets
import javafx.scene.Scene
import javafx.scene.control.Button
import javafx.scene.layout.HBox
import javafx.scene.layout.StackPane
import javafx.stage.Stage
class QuitButton : Application() {
override fun start(stage: Stage) {
val hbox = HBox()
hbox.padding = Insets(15.0, 0.0, 0.0, 15.0)
val btn = Button("Quit")
btn.prefWidth = 80.0
btn.onAction = EventHandler { Platform.exit() }
hbox.children.add(btn)
val root = StackPane()
root.style = "-fx-font-size: 1.2em"
root.children.add(hbox)
val scene = Scene(root, 400.0, 300.0)
stage.title = "Quit button"
stage.scene = scene
stage.show()
}
}
fun main() {
Application.launch(QuitButton::class.java)
}
A button control is placed in the upper-left corner of the window. An event handler is added to the button.
val hbox = HBox() hbox.padding = Insets(15.0, 0.0, 0.0, 15.0)
The layout of UI elements is done with layout managers. The HBox is
a simple manager which lays out controls in a single horizontal line. We set
some padding in order to have some space between our button and the window
borders.
val btn = Button("Quit")
A Button control is created. We provide its text.
btn.prefWidth = 80.0
We increase the preferred width of the button, since the default is too small.
btn.onAction = EventHandler { Platform.exit() }
Via the onAction property, we plug an event handler to the button.
When we click on the button, the provided lambda expression is executed.
The Platform.exit terminates the application.
val root = StackPane() root.style = "-fx-font-size: 1.2em" root.children.add(hbox)
The horizontal box is added to the StackPane. We change the style
of the pane via the style property. The font size is increased.
The default is a bit too small.
Label control
Label control is used to display text or images.
package com.zetcode
import javafx.application.Application
import javafx.geometry.Insets
import javafx.geometry.Pos
import javafx.scene.Scene
import javafx.scene.control.Label
import javafx.scene.image.Image
import javafx.scene.image.ImageView
import javafx.scene.layout.*
import javafx.scene.paint.Color
import javafx.scene.text.Font
import javafx.scene.text.FontPosture
import javafx.scene.text.FontWeight
import javafx.stage.Stage
class Labels : Application() {
override fun start(stage: Stage) {
val vbox = VBox(20.0)
vbox.alignment = Pos.CENTER
vbox.padding = Insets(20.0)
val title = Label("Sid the sloth")
title.font = Font.font("arial", FontWeight.BOLD,
FontPosture.ITALIC, 22.0)
val img = ImageView(Image("/images/sid.png"))
val lbi = Label()
lbi.graphic = img
vbox.children.addAll(title, lbi)
val root = StackPane()
val bf = BackgroundFill(
Color.valueOf("#358ae6"), CornerRadii.EMPTY, Insets.EMPTY
)
root.background = Background(bf)
root.children.add(vbox)
val scene = Scene(root)
stage.title = "Labels"
stage.scene = scene
stage.show()
}
}
fun main() {
Application.launch(Labels::class.java)
}
In this program, we use labels to display a title and a PNG image.
val vbox = VBox(20.0) vbox.alignment = Pos.CENTER vbox.padding = Insets(20.0)
The two controls are placed in a vertical box.
val title = Label("Sid the sloth")
title.font = Font.font("arial", FontWeight.BOLD,
FontPosture.ITALIC, 22.0)
This label displays text. We set a new font via the font property.
val img = ImageView(Image("/images/sid.png"))
val lbi = Label()
lbi.graphic = img
This label displays an image. In addition to Label, we also use
the ImageView and Image. The ImageView is
set via the graphic property.
val bf = BackgroundFill(
Color.valueOf("#358ae6"), CornerRadii.EMPTY, Insets.EMPTY
)
root.background = Background(bf)
For a better contrast, we change the background colour of the window.
CheckBox control
CheckBox is a tri-state selection control box showing a checkmark
or tick mark when checked. The control has two states by default: checked and
unchecked. The setAllowIndeterminate enables the third state:
indeterminate.
package com.zetcode
import javafx.application.Application
import javafx.geometry.Insets
import javafx.scene.Scene
import javafx.scene.control.CheckBox
import javafx.scene.layout.HBox
import javafx.stage.Stage
class CheckBoxEx : Application() {
override fun start(stage: Stage) {
val root = HBox()
root.padding = Insets(10.0, 0.0, 0.0, 10.0)
val cbox = CheckBox("Show title")
cbox.isSelected = true
cbox.selectedProperty().addListener { _, _, newVal ->
if (newVal) {
stage.title = "CheckBox"
} else {
stage.title = ""
}
}
root.children.add(cbox)
val scene = Scene(root, 400.0, 250.0)
stage.title = "CheckBox"
stage.scene = scene
stage.show()
}
}
fun main() {
Application.launch(CheckBoxEx::class.java)
}
The example shows or hides the title of the window depending whether the check box is selected.
val cbox = CheckBox("Show title")
A CheckBox control is created. The specified text is its label.
cbox.isSelected = true
Since the title of the window is visible by default, we select the control with
the isSelected property.
cbox.selectedProperty().addListener { _, _, newVal ->
if (newVal) {
stage.title = "CheckBox"
} else {
stage.title = ""
}
}
We add a listener to the selected property. If the CheckBox is
selected, the newVal contains true value. Depending on the value,
we change the title of the window via the title property.
Slider control
Slider is a component that lets the user graphically select a a
continuous or discrete range of valid numeric valus by sliding a knob within a
bounded interval.
Slider can optionally show tick marks for the range of its values.
The tick marks are set with the showTickMarks property.
package com.zetcode
import javafx.application.Application
import javafx.geometry.Insets
import javafx.scene.Scene
import javafx.scene.control.Label
import javafx.scene.control.Slider
import javafx.scene.layout.HBox
import javafx.stage.Stage
class SliderEx : Application() {
override fun start(stage: Stage) {
val root = HBox()
root.padding = Insets(10.0)
root.spacing = 40.0
val slider = Slider(0.0, 100.0, 0.0)
slider.setMinSize(290.0, -1.0)
slider.isShowTickMarks = true
val label = Label("0")
slider.valueProperty().addListener { _, _, newVal ->
label.text = "${newVal.toInt()}"
}
root.children.addAll(slider, label)
val scene = Scene(root, 400.0, 250.0)
stage.title = "Slider"
stage.scene = scene
stage.show()
}
}
fun main() {
Application.launch(SliderEx::class.java)
}
A selected value from a slider is displayed in the adjacent label component.
val slider = Slider(0.0, 100.0, 0.0)
A Slider is created with the minimum, maximum, and current values
as parameters.
slider.setMinSize(290.0, -1.0)
We set the minimum size of the control with setMinSize.
slider.isShowTickMarks = true
The isShowTickMarks property determines whether tick marks are
painted on the slider.
slider.valueProperty().addListener { _, _, newVal ->
label.text = "${newVal.toInt()}"
}
We plug a listener for value property modifications. In the lambda, we set the current value of the slider to the label component.
ComboBox control
ComboBox is a component that combines a button or editable field
and a drop-down list. The user can select a value from the drop-down list, which
appears at the user's request.
If we make the combo box editable, then the combo box includes an editable field
into which the user can type a value. A combo box is made editable via
isEditable property.
package com.zetcode
import javafx.application.Application
import javafx.collections.FXCollections
import javafx.geometry.Insets
import javafx.geometry.Pos
import javafx.scene.Scene
import javafx.scene.control.ComboBox
import javafx.scene.control.Label
import javafx.scene.layout.HBox
import javafx.scene.layout.StackPane
import javafx.stage.Stage
class ComboBoxEx : Application() {
private lateinit var label: Label;
override fun start(stage: Stage) {
val hbox = HBox(40.0)
hbox.padding = Insets(20.0)
hbox.alignment = Pos.BASELINE_LEFT
val distros = listOf(
"Ubuntu", "Redhat", "Arch",
"Debian", "Mint"
)
val combo = ComboBox(
FXCollections.observableList(distros)
)
combo.valueProperty().addListener { _, _, newVal ->
label.text = "$newVal"
}
combo.setMinSize(150.0, -1.0)
label = Label()
hbox.children.addAll(combo, label)
val root = StackPane()
root.style = "-fx-font-size: 1.5em"
root.children.add(hbox)
val scene = Scene(root, 400.0, 350.0)
stage.title = "ComboBox"
stage.scene = scene
stage.show()
}
}
fun main() {
Application.launch(ComboBoxEx::class.java)
}
The program contains a combo box and a label. The combo box contains a list of Linux distribution names. The selected item from the combo box is shown in the adjacent label.
val distros = listOf(
"Ubuntu", "Redhat", "Arch",
"Debian", "Mint"
)
val combo = ComboBox(
FXCollections.observableList(distros)
)
A ComboBox is created. The observable list allows listeners to
track changes when they occur.
hbox.alignment = Pos.BASELINE_LEFT
With the baseline option, the label's text is aligned with the combo's text vertically.
combo.valueProperty().addListener { _, _, newVal ->
label.text = "$newVal"
}
We add a listener to the value property of the combo box. The new value is set to the label.
Moving window
The following example shows the position of the application window on the screen.
package com.zetcode
import javafx.application.Application
import javafx.geometry.Insets
import javafx.scene.Scene
import javafx.scene.control.Label
import javafx.scene.layout.VBox
import javafx.stage.Stage
class MoveWindowEx : Application() {
private val lblX: Label = Label("")
private val lblY: Label = Label("")
override fun start(stage: Stage) {
val vbox = VBox(10.0)
vbox.padding = Insets(10.0);
vbox.children.addAll(lblX, lblY);
stage.xProperty().addListener { _, _, newVal -> lblX.text = "x: $newVal" }
stage.yProperty().addListener { _, _, newVal -> lblY.text = "x: $newVal" }
val scene = Scene(vbox, 450.0, 250.0)
vbox.style = "-fx-font-size: 1.2em"
stage.title = "Move window"
stage.scene = scene
stage.show()
}
}
fun main() {
Application.launch(MoveWindowEx::class.java)
}
The example shows the current window coordinates in two label controls. To get
the window position, we listen for changes of the xProperty and
yProperty of the stage.
private val lblX: Label = Label("")
private val lblY: Label = Label("")
These two labels show the x and y coordinates of the top-left corner of the application window.
stage.xProperty().addListener { _, _, newVal -> lblX.text = "x: $newVal" }
The xProperty stores the horizontal location of the stage on the
screen. We add a listener to listen for changes of the property. Each time the
property is modified, we retrieve the new value and update the label.
Shapes
In the following example, we paint shapes on the canvas. Canvas is
an image that can be drawn on using a set of graphics commands provided by a
GraphicsContext.
package com.zetcode
import javafx.application.Application
import javafx.scene.Scene
import javafx.scene.canvas.Canvas
import javafx.scene.canvas.GraphicsContext
import javafx.scene.layout.Pane
import javafx.scene.paint.Color
import javafx.scene.shape.ArcType
import javafx.stage.Stage
class Shapes : Application() {
override fun start(stage: Stage) {
val root = Pane()
val canvas = Canvas(500.0, 500.0)
drawShapes(canvas.graphicsContext2D)
root.children.add(canvas)
val scene = Scene(root, 450.0, 350.0, Color.WHITESMOKE)
stage.title = "Shapes"
stage.scene = scene
stage.show()
}
private fun drawShapes(gc: GraphicsContext) {
gc.fill = Color.GRAY
gc.fillOval(30.0, 30.0, 80.0, 80.0)
gc.fillOval(150.0, 30.0, 120.0, 80.0)
gc.fillRect(320.0, 30.0, 100.0, 100.0)
gc.fillRoundRect(30.0, 180.0, 100.0, 100.0, 20.0, 20.0)
gc.fillArc(150.0, 180.0, 100.0, 100.0, 45.0, 180.0, ArcType.OPEN)
gc.fillPolygon(doubleArrayOf(290.0, 380.0, 290.0),
doubleArrayOf(140.0, 300.0, 300.0), 3)
}
}
fun main() {
Application.launch(Shapes::class.java)
}
The example paints six different shapes using the graphics context's fill methods.
private fun drawShapes(gc: GraphicsContext) {
The GraphicsContext is an interface through which we paint on the
canvas.
gc.fill = Color.GRAY
The shapes are painted in gray colour.
gc.fillOval(30.0, 30.0, 80.0, 80.0) gc.fillOval(150.0, 30.0, 120.0, 80.0)
The fillOval method paints a circle and an ellipse. The first two
parameters are the x and y coordinates. The third and the fourth parameter are
the width and height of the oval.
gc.fillRect(320.0, 30.0, 100.0, 100.0)
The fillRect fills a rectangle using the current fill paint.
gc.fillRoundRect(30.0, 180.0, 100.0, 100.0, 20.0, 20.0)
The fillRoundRect paints a rectangle, whose corners are rounded.
The last two parameters of the method are the arc width and arc height of the
rectangle corners.
gc.fillArc(150.0, 180.0, 100.0, 100.0, 45.0, 180.0, ArcType.OPEN)
The fillArc method fills an arc using the current fill paint. The
last three parameters are the starting angle, the angular extend, and the
closure type.
gc.fillPolygon(doubleArrayOf(290.0, 380.0, 290.0),
doubleArrayOf(140.0, 300.0, 300.0), 3)
The fillPolygon method fills a polygon with the given points using
the currently set fill paint. In our case, it paints a right-angled triangle.
The first parameter is an array containing the x coordinates of the polygon
points, the second parameter is an array containing the y coordinates of the
polygon points. The last parameter is the number of points that form a polygon.
Bézier curve
A Bézier curve is a cubic line. It can be painted on the canvas with
bezierCurveTo.
package com.zetcode
import javafx.application.Application
import javafx.scene.Scene
import javafx.scene.canvas.Canvas
import javafx.scene.canvas.GraphicsContext
import javafx.scene.layout.Pane
import javafx.scene.paint.Color
import javafx.stage.Stage
class BezierCurve : Application() {
override fun start(stage: Stage) {
val root = Pane()
val canvas = Canvas(500.0, 500.0)
drawCurve(canvas.graphicsContext2D)
root.children.add(canvas)
val scene = Scene(root, 450.0, 350.0, Color.WHITESMOKE)
stage.title = "Bézier curve"
stage.scene = scene
stage.show()
}
private fun drawCurve(gc: GraphicsContext) {
gc.stroke = Color.valueOf("#16567d")
gc.lineWidth = 1.5
gc.beginPath();
gc.moveTo(40.0, 40.0);
gc.bezierCurveTo(80.0, 240.0, 280.0, 90.0, 350.0, 300.0)
gc.stroke()
}
}
fun main() {
Application.launch(BezierCurve::class.java)
}
The program draws a single Bézier curve.
gc.stroke = Color.valueOf("#16567d")
gc.lineWidth = 1.5
The line will be of blueish colour having width 1.5 units.
gc.beginPath(); gc.moveTo(40.0, 40.0); gc.bezierCurveTo(80.0, 240.0, 280.0, 90.0, 350.0, 300.0)
We create a path. First, we move to a point with moveTo. Then
we draw the curve with bezierCurveTo.
gc.stroke()
The path is painted with stroke function.
Reflection
Reflection is an effect that renders a reflected version of the
obejct below the actual content.
package com.zetcode
import javafx.application.Application
import javafx.scene.Scene
import javafx.scene.effect.Reflection
import javafx.scene.layout.StackPane
import javafx.scene.paint.Color
import javafx.scene.text.Font
import javafx.scene.text.FontWeight
import javafx.scene.text.Text
import javafx.stage.Stage
class ReflectionEx : Application() {
override fun start(stage: Stage) {
val root = StackPane()
val text = Text()
text.text = "ZetCode";
text.fill = Color.STEELBLUE;
text.font = Font.font("Serif", FontWeight.BOLD, 60.0);
val ref = Reflection()
text.effect = ref;
root.children.add(text);
val scene = Scene(root, 400.0, 250.0)
stage.title = "Reflection"
stage.scene = scene
stage.show()
}
}
fun main() {
Application.launch(ReflectionEx::class.java)
}
In the program, we show a reflected text.
val text = Text()
text.text = "ZetCode";
text.fill = Color.STEELBLUE;
text.font = Font.font("Serif", FontWeight.BOLD, 60.0);
A Text shape is created. We define its colour and font.
val ref = Reflection() text.effect = ref;
A reflection effect with default values is applied on the Text.
PathTransition animation
PathTransition creates an animation along a path. The animation is
performed by updating the translateX and
translateY variables of the node. Note that we must use a node that
supports absolute positioning of elements.
package com.zetcode
import javafx.animation.PathTransition
import javafx.application.Application
import javafx.scene.Scene
import javafx.scene.layout.Pane
import javafx.scene.paint.Color
import javafx.scene.shape.Circle
import javafx.scene.shape.CubicCurveTo
import javafx.scene.shape.MoveTo
import javafx.scene.shape.Path
import javafx.stage.Stage
import javafx.util.Duration
class PathTransitionEx : Application() {
override fun start(stage: Stage) {
val root = Pane()
val path = Path()
path.elements.add(MoveTo(20.0, 120.0))
path.elements.add(
CubicCurveTo(
180.0, 60.0,
250.0, 340.0, 420.0, 240.0
)
)
val circle = Circle(20.0, 120.0, 10.0)
circle.fill = Color.CADETBLUE
val ptr = PathTransition()
ptr.duration = Duration.seconds(6.0)
ptr.delay = Duration.seconds(2.0)
ptr.path = path
ptr.node = circle
ptr.cycleCount = 2
ptr.isAutoReverse = true
ptr.play()
root.children.addAll(path, circle)
val scene = Scene(root, 450.0, 350.0)
stage.title = "Path transition"
stage.scene = scene
stage.show()
}
}
fun main() {
Application.launch(PathTransitionEx::class.java)
}
The program uses a PathTransition to move a circle along a path.
The animation starts after an initial delay of 2 seconds. It consists of two
cycles. The animation is reveresed; that is, the circle goes from the starting
point to the ending point and then it returns back.
val root = Pane()
We use the Pane as our root node. It supports absolute positioning
that is needed for animation.
val path = Path()
path.elements.add(MoveTo(20.0, 120.0))
path.elements.add(
CubicCurveTo(
180.0, 60.0,
250.0, 340.0, 420.0, 240.0
)
)
Here we define a Path along which the animated object will be
moving.
var circle = new Circle(20, 120, 10); circle.setFill(Color.CADETBLUE);
We move a circle object in our animation.
val ptr = PathTransition()
A PathTransition object is created.
ptr.duration = Duration.seconds(6.0)
The duration property sets the duration of the animation.
ptr.delay = Duration.seconds(2.0)
The delay property sets the initial delay of the animation.
ptr.path = path ptr.node = circle
We set the path and the target node of the animation.
ptr.cycleCount = 2
Our animation has two cycles; it is set with the cycleCount
property.
ptr.isAutoReverse = true
With the isAutoReverse property, we reverse the direction of the
animation; the circle moves back to the starting position.
ptr.play()
Finally, the play method the plays the animation.
PieChart
A pie chart is a circular chart which is divided into slices to illustrate
numerical proportion. It can be created with PieChart.
package com.zetcode
import javafx.application.Application
import javafx.collections.FXCollections
import javafx.collections.ObservableList
import javafx.scene.Scene
import javafx.scene.chart.PieChart
import javafx.scene.layout.HBox
import javafx.stage.Stage
class PieChartEx : Application() {
override fun start(stage: Stage) {
val root = HBox()
val scene = Scene(root, 450.0, 330.0)
val pieChartData: ObservableList<PieChart.Data> =
FXCollections.observableArrayList(
PieChart.Data("Apache", 52.0),
PieChart.Data("Nginx", 31.0),
PieChart.Data("IIS", 12.0),
PieChart.Data("LiteSpeed", 2.0),
PieChart.Data("Google server", 1.0),
PieChart.Data("Others", 2.0)
)
val pieChart = PieChart(pieChartData)
pieChart.title = "Web servers market share (2016)"
root.children.add(pieChart)
stage.title = "PieChart"
stage.scene = scene
stage.show()
}
}
fun main() {
Application.launch(PieChartEx::class.java)
}
In the program, a pie chart is created to show the market share of web servers.
val pieChartData: ObservableList<PieChart.Data> =
FXCollections.observableArrayList(
PieChart.Data("Apache", 52.0),
PieChart.Data("Nginx", 31.0),
PieChart.Data("IIS", 12.0),
PieChart.Data("LiteSpeed", 2.0),
PieChart.Data("Google server", 1.0),
PieChart.Data("Others", 2.0)
)
Pie chart data items are created with the PieChart.Data.
val pieChart = PieChart(pieChartData)
A pie chart is created with the PieChart class.
Source
This article was an introduction to UI development in Kotlin and OpenJFX.
Author
List all Kotlin tutorials.