Kotlin OpenJFX
最后修改于 2024 年 1 月 29 日
本文介绍了如何在 Kotlin 中创建 OpenJFX GUI 应用程序。
OpenJFX是用于 JDK 的下一代客户端应用程序平台,适用于桌面和嵌入式系统。
应用程序
Application
是 OpenJFX 程序的主要类。它的 start
方法是应用程序的主入口点;它是系统准备就绪后调用的第一个方法。
一个应用程序由一个 Stage
和一个 Scene
组成。Stage 是顶级容器,是应用程序的主窗口。Scene 是 Stage 的视觉内容容器。Scene 的内容组织在场景图中。
场景图
场景图 是一个节点的分层树,表示应用程序用户界面的所有视觉元素。场景图中的单个元素称为节点。每个节点都是一个分支节点或叶节点。分支节点可以包含其他节点——它们的子节点。叶节点不包含其他节点。树中的第一个节点称为根节点;根节点没有父节点。
节点的具体实现包括图形图元、控件、布局管理器、图像或媒体。可以通过修改节点属性来操作场景。通过这种方式,我们可以对节点进行动画处理、应用效果、进行变换或更改其不透明度。
设置 OpenJFX
我们可以使用 Gradle 设置 OpenJFX。
plugins { kotlin("jvm") version "1.7.10" id("org.openjfx.javafxplugin") version "0.0.13" }
我们添加 org.openjfx.javafxplugin
插件。
javafx { modules("javafx.controls") }
我们配置 OpenJFX 应用程序。
简单示例
以下是一个简单的 OpenJFX 应用程序。
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) }
该示例在屏幕上显示一个小窗口。
class SimpleEx : Application() {
每个 OpenJFX 程序都继承自 Application
。
override fun start(stage: Stage) {
Application 的 start
方法被重写。它是 OpenJFX 程序的主要入口点。它接收一个 Stage
作为其唯一参数。(Stage 是主应用程序窗口或区域。)
val root = StackPane()
StackPane
是用于组织节点的容器。它使用一个简单的布局管理器,将它的内容节点放置在一个从后到前的单一堆栈中。在我们的例子中,我们只想放置一个节点。
val scene = Scene(root, 400.0, 300.0)
Scene
是场景图中所有内容的容器。它将一个根节点作为其第一个参数。StackPane
是此场景图中的一个根节点。接下来的两个参数指定场景的宽度和高度。
stage.title = "Simple"
我们通过 title
属性设置窗口的标题。
stage.scene = scene
一个场景被添加到舞台。
stage.show()
show 方法在屏幕上显示窗口。
fun main() { Application.launch(SimpleEx::class.java) }
我们使用 Application.launch
运行应用程序。

按钮控件
在下面的示例中,我们有一个 Button
控件。当我们点击按钮时,应用程序将终止。当按下并释放按钮时,将发送一个 ActionEvent
。
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) }
一个按钮控件被放置在窗口的左上角。一个事件处理程序被添加到按钮中。
val hbox = HBox() hbox.padding = Insets(15.0, 0.0, 0.0, 15.0)
UI 元素的布局是通过布局管理器完成的。HBox
是一个简单的管理器,它在单行中布局控件。我们设置了一些填充,以便在我们的按钮和窗口边框之间留出一些空间。
val btn = Button("Quit")
创建了一个 Button
控件。我们提供它的文本。
btn.prefWidth = 80.0
我们增加了按钮的首选宽度,因为默认值太小了。
btn.onAction = EventHandler { Platform.exit() }
通过 onAction
属性,我们将一个事件处理程序插入到按钮中。当我们点击按钮时,提供的 lambda 表达式将被执行。Platform.exit
终止应用程序。
val root = StackPane() root.style = "-fx-font-size: 1.2em" root.children.add(hbox)
水平框被添加到 StackPane
。我们通过 style
属性更改窗格的样式。字体大小已增加。默认值有点太小了。

标签控件
Label
控件用于显示文本或图像。
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) }
在此程序中,我们使用标签来显示标题和 PNG 图像。
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)
此标签显示文本。我们通过 font
属性设置一个新字体。
val img = ImageView(Image("/images/sid.png")) val lbi = Label() lbi.graphic = img
此标签显示一个图像。除了 Label
,我们还使用 ImageView
和 Image
。ImageView
通过 graphic
属性设置。
val bf = BackgroundFill( Color.valueOf("#358ae6"), CornerRadii.EMPTY, Insets.EMPTY ) root.background = Background(bf)
为了更好的对比度,我们更改了窗口的背景颜色。

复选框控件
CheckBox
是一个三态选择控件框,在选中时显示复选标记或勾号。该控件默认有两种状态:选中和未选中。setAllowIndeterminate
启用第三种状态:不确定。
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) }
该示例根据是否选中复选框来显示或隐藏窗口的标题。
val cbox = CheckBox("Show title")
创建了一个 CheckBox
控件。指定的文本是它的标签。
cbox.isSelected = true
由于窗口的标题默认可见,我们使用 isSelected
属性选择该控件。
cbox.selectedProperty().addListener { _, _, newVal -> if (newVal) { stage.title = "CheckBox" } else { stage.title = "" } }
我们向已选属性添加了一个监听器。如果选中了 CheckBox
,则 newVal
包含 true 值。根据该值,我们通过 title
属性更改窗口的标题。

滑块控件
Slider
是一个组件,允许用户通过在有界区间内滑动旋钮来以图形方式选择有效数值的连续或离散范围。
Slider
可以选择性地为它的值范围显示刻度线。刻度线使用 showTickMarks
属性设置。
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) }
从滑块中选择的值显示在相邻的标签组件中。
val slider = Slider(0.0, 100.0, 0.0)
Slider
是使用最小值、最大值和当前值作为参数创建的。
slider.setMinSize(290.0, -1.0)
我们使用 setMinSize
设置控件的最小尺寸。
slider.isShowTickMarks = true
isShowTickMarks
属性决定是否在滑块上绘制刻度线。
slider.valueProperty().addListener { _, _, newVal -> label.text = "${newVal.toInt()}" }
我们为值属性的修改插入一个监听器。在 lambda 中,我们设置滑块的当前值到标签组件。

组合框控件
ComboBox
是一个组件,它结合了一个按钮或可编辑字段和一个下拉列表。用户可以从下拉列表中选择一个值,该下拉列表应用户请求出现。
如果我们将组合框设为可编辑,则组合框包含一个可编辑字段,用户可以在其中键入一个值。组合框通过 isEditable
属性设置为可编辑。
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) }
该程序包含一个组合框和一个标签。组合框包含一个 Linux 发行版名称列表。从组合框中选择的条目显示在相邻的标签中。
val distros = listOf( "Ubuntu", "Redhat", "Arch", "Debian", "Mint" ) val combo = ComboBox( FXCollections.observableList(distros) )
创建了一个 ComboBox
。可观察列表允许监听器在发生更改时跟踪更改。
hbox.alignment = Pos.BASELINE_LEFT
使用基线选项,标签的文本与组合框的文本垂直对齐。
combo.valueProperty().addListener { _, _, newVal -> label.text = "$newVal" }
我们向组合框的值属性添加了一个监听器。新值被设置为标签。

移动窗口
以下示例显示应用程序窗口在屏幕上的位置。
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) }
该示例在两个标签控件中显示当前窗口坐标。要获取窗口位置,我们监听舞台的 xProperty
和 yProperty
的变化。
private val lblX: Label = Label("") private val lblY: Label = Label("")
这两个标签显示应用程序窗口左上角的 x 和 y 坐标。
stage.xProperty().addListener { _, _, newVal -> lblX.text = "x: $newVal" }
xProperty
存储舞台在屏幕上的水平位置。我们添加一个监听器来监听属性的变化。每次修改属性时,我们检索新值并更新标签。

形状
在下面的示例中,我们在画布上绘制形状。Canvas
是一种图像,可以使用 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) }
该示例使用图形上下文的填充方法绘制六个不同的形状。
private fun drawShapes(gc: GraphicsContext) {
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)
fillOval
方法绘制一个圆和一个椭圆。前两个参数是 x 和 y 坐标。第三个和第四个参数是椭圆的宽度和高度。
gc.fillRect(320.0, 30.0, 100.0, 100.0)
fillRect
使用当前的填充笔刷填充一个矩形。
gc.fillRoundRect(30.0, 180.0, 100.0, 100.0, 20.0, 20.0)
fillRoundRect
绘制一个矩形,其角是圆角的。该方法的最后两个参数是矩形角的弧宽和弧高。
gc.fillArc(150.0, 180.0, 100.0, 100.0, 45.0, 180.0, ArcType.OPEN)
fillArc
方法使用当前的填充笔刷填充一个弧形。最后三个参数是起始角度、角度延伸和闭合类型。
gc.fillPolygon(doubleArrayOf(290.0, 380.0, 290.0), doubleArrayOf(140.0, 300.0, 300.0), 3)
fillPolygon
方法使用当前设置的填充笔刷填充具有给定点的多边形。在我们的例子中,它绘制一个直角三角形。第一个参数是一个包含多边形点 x 坐标的数组,第二个参数是一个包含多边形点 y 坐标的数组。最后一个参数是构成多边形的点的数量。

贝塞尔曲线
一个 贝塞尔曲线 是一条三次线。它可以用 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) }
该程序绘制一条贝塞尔曲线。
gc.stroke = Color.valueOf("#16567d") gc.lineWidth = 1.5
该线将是蓝色的,宽度为 1.5 个单位。
gc.beginPath(); gc.moveTo(40.0, 40.0); gc.bezierCurveTo(80.0, 240.0, 280.0, 90.0, 350.0, 300.0)
我们创建一个路径。首先,我们使用 moveTo
移动到一个点。然后我们使用 bezierCurveTo
绘制曲线。
gc.stroke()
路径使用 stroke
函数绘制。

反射
Reflection
是一种效果,它在实际内容下方渲染对象的反射版本。
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) }
在程序中,我们显示一个反射文本。
val text = Text() text.text = "ZetCode"; text.fill = Color.STEELBLUE; text.font = Font.font("Serif", FontWeight.BOLD, 60.0);
创建了一个 Text
形状。我们定义它的颜色和字体。
val ref = Reflection() text.effect = ref;
一个具有默认值的反射效果被应用于 Text
。

PathTransition 动画
PathTransition
沿着路径创建一个动画。动画通过更新节点的 translateX
和 translateY
变量来执行。请注意,我们必须使用一个支持元素绝对定位的节点。
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) }
该程序使用 PathTransition
沿路径移动一个圆。动画在初始延迟 2 秒后开始。它由两个周期组成。动画被反转;也就是说,圆从起点移动到终点,然后返回。
val root = Pane()
我们使用 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 ) )
在这里,我们定义一个 Path
,动画对象将沿着该路径移动。
var circle = new Circle(20, 120, 10); circle.setFill(Color.CADETBLUE);
我们在动画中移动一个圆对象。
val ptr = PathTransition()
创建了一个 PathTransition 对象。
ptr.duration = Duration.seconds(6.0)
duration
属性设置动画的持续时间。
ptr.delay = Duration.seconds(2.0)
delay
属性设置动画的初始延迟。
ptr.path = path ptr.node = circle
我们设置动画的路径和目标节点。
ptr.cycleCount = 2
我们的动画有两个周期;它使用 cycleCount
属性设置。
ptr.isAutoReverse = true
使用 isAutoReverse
属性,我们反转动画的方向;圆圈移动回起始位置。
ptr.play()
最后,play
方法播放动画。

饼图
饼图是一个圆形图表,它被分成切片以说明数字比例。可以使用 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) }
在该程序中,创建一个饼图以显示 Web 服务器的市场份额。
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) )
饼图数据项使用 PieChart.Data
创建。
val pieChart = PieChart(pieChartData)
使用 PieChart 类创建饼图。

来源
本文介绍了使用 Kotlin 和 OpenJFX 进行 UI 开发。
作者
列出 所有 Kotlin 教程。