译自《Basic Controls》
基本控件
TornadoFX最令人兴奋的功能之一就是Type-Safe Builders。 配置(Configuring)和布置(laying out)复杂UI的控件可能是冗长而困难的,代码可能很快变得混乱而难以维护。 幸运的是,您可以使用由Groovy开创的强大的闭包模式(powerful closure pattern) ,以纯粹和简单的Kotlin代码来创建结构化的UI布局。
虽然我们稍后会学习如何应用FXML,但是您可能会发现构建器(builders)是一个表达力强劲的方法,可以在一小段时间内创建复杂的UI。 没有配置文件或编译器的魔术,构建器使用纯Kotlin代码完成。 接下来的几个章节将把构建器分为不同类别的控件。 一路上,您将逐渐通过将这些构建器集成在一起来构建更复杂的UI。
但首先,让我们来看看构建器如何实际工作。
构建器如何工作
Kotlin的标准库提供了一些有用的“块(block)”函数,目标是任何类型T
。 有with()函数(with() function) ,它允许你编写一个item的代码,好像你正好在它的类中一样。
class MyView : View() {
override val root = VBox()
init {
with(root) {
this += Button("Press Me")
}
}
}
在上面的例子中,with()
函数接受root
作为参数。 以下的闭包参数通过将root
引用为this
来直接操作root
,这被安全地解释为VBox
。 通过调用它的plusAssign()
扩展运算符(extended operator)将一个Button
添加到VBox
。
或者,Kotlin中的每个类型都有一个apply()
函数(apply() function) 。 这与with()
是几乎相同的功能,但它实际上是一个扩展的高阶函数(extended higher-order function)。
class MyView : View() {
override val root = VBox()
init {
root.apply {
this += Button("Press Me")
}
}
}
with()
和apply()
完成类似的任务。 他们安全地解释他们所针对的类型,并允许对其进行操作。 但是, with()
返回lambda中的最后一个语句,而apply()
实际上返回了它所针对的项目。 因此,如果您在Button
上调用apply()
来操作,例如其字体颜色和动作,那么Button
返回其自己是很有帮助的,以免破坏声明流程(declaration flow)。
class MyView : View() {
override val root = VBox()
init {
with(root) {
this += Button("Press Me").apply {
textFill = Color.RED
action { println("Button pressed!") }
}
}
}
}
上面表达了构建器工作的基本概念,并且正在进行三项任务:
- 创建一个
Button
-
Button
被修改 - 该
Button
被添加到它的“父级(parent)”,它是一个VBox
当声明任何Node
,这三个步骤是如此常见,以至于TornadoFX使用策略性放置的扩展函数(strategically placed extension functions)来简化它们,如下所示的button()
。
class MyView : View() {
override val root = VBox()
init {
with(root) {
button("Press Me") {
textFill = Color.RED
action { println("Button pressed!") }
}
}
}
}
虽然这看起来更干净,但您可能会想:“我们是如何摆脱this +=
和apply()
函数调用的呢?为什么我们使用一个名为button()
的函数而不是实际的Button
呢? 我们不会太深入如何做到这一点,如果你好奇,你可以随时挖掘源代码(source code ) 。
但本质上, VBox
(或任何可定位的组件)具有称为button()
的扩展函数。 它接受一个文本参数和一个可选的闭包,目标是它将实例化的Button
。 当调用此函数时,将创建一个带指定文本的Button
,对其应用闭包,将其添加到在其上调用的VBox
,然后将其返回。
为进一步提高效率,您可以重载(override )View
的root
,并为其赋值一个构建器函数(builder function),从而可以避免需要任何init()
和with()
块。
class MyView : View() {
override val root = vbox {
button("Press Me") {
textFill = Color.RED
action { println("Button pressed!") }
}
}
}
当您将控件嵌套到其他控件中时,构建器模式变得特别强大。 使用这些构建器扩展函数,您可以轻松地将多个HBox
实例嵌入到一个VBox
,并创建一个结构清晰的UI代码(图4.1)。
class MyView : View() {
override val root = vbox {
hbox {
label("First Name")
textfield()
}
hbox {
label("Last Name")
textfield()
}
button("LOGIN") {
useMaxWidth = true
}
}
}
另外请注意,我们将在稍后了解TornadoFX的专有Form,这将使像这样的简单输入UI的代码更简单。
如果需要保存对TextField等控件的引用,则可以将它们保存到变量或属性中,因为函数返回生成的控件。 建议您使用singleAssign()
代理来确保属性只赋值一次。
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}")
}
}
}
}
请注意,非构建器扩展函数和属性也已添加到不同的控件中。useMaxWidth
是Node
的扩展属性,它将Node
设置为占用允许的最大宽度。 在接下来的几章中,我们将会看到更多这些有用的扩展。
在接下来的章节中,我们将介绍每个JavaFX控件的每个相应的构建器。 利用上述理念,您可以从头到尾或者作为参考来阅读以后的章节。
基本控件的构建器
本章的其余部分将介绍常见的JavaFX控件(如Button
, Label
和TextField
构建器。 下一章将介绍数据驱动控件(如ListView
, TableView
和TreeTableView
构建器)。
Button
对于任何Pane
,您可以调用其button()
扩展函数向其添加一个Button
。 您可以选择传递text
参数和Button.() -> Unit
的lambda
来修改其属性。
在Pane
中,这将添加一个带有红色文本的Button
,并在每次点击时打印 “Button pressed!” (图4.2)
button("Press Me") {
textFill = Color.RED
action {
println("Button pressed!")
}
}
Label
您可以调用label()
扩展函数将Label
添加到给定的Pane
。 或者,您可以提供一个文本(String
或Property
),一个图形 (类型为Node
或ObjectProperty
)和Label.() -> Unit
的lambda来修改其属性(图4.3)。
label("Lorem ipsum", circle(10, 10, 5)) {
textFill = Color.BLUE
}
TextField
对于任何Pane
,您可以通过调用textfield()
扩展函数来添加一个TextField
(图4.4)。
textfield()
您可以选择提供初始文本(initial text)以及闭包(closure)以操纵TextField
。 例如,我们可以在其textProperty()
添加一个监听器,并在每次更改时打印其值(图4.5)。
textfield("Input something") {
textProperty().addListener { obs, old, new ->
println("You typed: " + new)
}
}
PasswordField
如果您需要一个TextField
来获取敏感信息,可能需要考虑使用PasswordField
。 它将显示匿名字符以防止窥视。 您还可以提供初始密码作为参数,以及代码块来操作它(图4.7)。
passwordfield("my_password") {
requestFocus()
}
CheckBox
您可以创建一个CheckBox
以快速创建一个真/假(true/false)状态控件,并可选择使用块来操作它(图4.8)。
checkbox("Admin Mode") {
action { println(isSelected) }
}
请注意,动作块( action block)被包含在checkbox
内,从而您可以访问它的isSelected
属性。 如果您不需要访问CheckBox
的属性,您可以这样写:checkbox("Admin Mode").action {}
。
您还可以提供一个Property
,这将会绑定到其选择状态 。
val booleanProperty = SimpleBooleanProperty()
checkbox("Admin Mode", booleanProperty).action { println(isSelected) }
ComboBox
ComboBox是一个下拉式控件,允许从中选择一组固定的值(图4.10)。
val texasCities = FXCollections.observableArrayList("Austin",
"Dallas","Midland", "San Antonio","Fort Worth")
combobox {
items = texasCities
}
如果将values声明为参数,则不需要指定通用类型(generic type)。
val texasCities = FXCollections.observableArrayList("Austin",
"Dallas","Midland","San Antonio","Fort Worth")
combobox(values = texasCities)
您还可以指定要绑定到所选值的Property
。
val texasCities = FXCollections.observableArrayList("Austin",
"Dallas","Midland","San Antonio","Fort Worth")
val selectedCity = SimpleStringProperty()
combobox(selectedCity, texasCities)
ToggleButton
ToggleButton
是一个按照它的选择状态来表示真/假(true/false)状态的按钮(图4.11)。
togglebutton("OFF") {
action {
text = if (isSelected) "ON" else "OFF"
}
}
也许一个控制按钮文本的更加自然的方式(idomatic way)是使用绑定到textProperty
的StringBinding
:
togglebutton {
val stateText = selectedProperty().stringBinding {
if (it == true) "ON" else "OFF"
}
textProperty().bind(stateText)
}
您可以选择将ToggleGroup
传递给togglebutton()
函数。 这将确保ToggleGroup
所有ToggleButton
只能一次选择一个(图4.12)。
class MyView : View() {
private val toggleGroup = ToggleGroup()
override val root = hbox {
togglebutton("YES", toggleGroup)
togglebutton("NO", toggleGroup)
togglebutton("MAYBE", toggleGroup)
}
}
RadioButton
RadioButton
与ToggleButton
具有相同的功能,但具有不同的视觉风格。 当它被选中时,它会填充一个环形控件(circular control)(图4.13)。
radiobutton("Power User Mode") {
action {
println("Power User Mode: $isSelected")
}
}
也可以像ToggleButton
一样,将RadioButton
设置为包含在ToggleGroup
内,以便一次只能选择该组中的一个项目(图4.14)。
class MyView : View() {
private val toggleGroup = ToggleGroup()
override val root = vbox {
radiobutton("Employee", toggleGroup)
radiobutton("Contractor", toggleGroup)
radiobutton("Intern", toggleGroup)
}
}
DatePicker
DatePicker
声明起来是很简单的。 它允许您从弹出的日历控件(popout calendar control)中选择日期。 您可以选择提供一个块来操作它(图4.15)。
datepicker {
value = LocalDate.now()
}
您还可以提供Property
作为绑定到其值的参数。
val dateProperty = SimpleObjectProperty()
datepicker(dateProperty) {
value = LocalDate.now()
}
TextArea
TextArea允许您输入多行自由格式文本(multiline freeform text)。 声明时,您可以选择提供初始文本value
以及处理程序块(图4.16)。
textarea("Type memo here") {
selectAll()
}
ProgressBar
ProgressBar
可视化完成一个过程趋近完成的进度。 您可以选择提供小于或等于1.0
的初始Double
值,表示完成百分比(图4.17)。
progressbar(0.5)
这是一个更加动态的例子,模拟一个过程在短时间内的进展。
progressbar() {
thread {
for (i in 1..100) {
Platform.runLater { progress = i.toDouble() / 100.0 }
Thread.sleep(100)
}
}
}
您还可以传递一个将progress
绑定到其值的Property
,以及一个操作ProgressBar
的块。
progressbar(completion) {
progressProperty().addListener {
obsVal, old, new -> print("VALUE: $new")
}
}
ProgressIndicator
ProgressIndicator
在功能上与ProgressBar
相同,但使用填充圆(filling circle)而不是进度条(图4.18)。
progressindicator {
thread {
for (i in 1..100) {
Platform.runLater { progress = i.toDouble() / 100.0 }
Thread.sleep(100)
}
}
}
就像ProgressBar
一样,您可以提供一个Property
,和/或一个块作为可选参数(图4.19)。
val completion = SimpleObjectProperty(0.0)
progressindicator(completion)
ImageView
您可以使用imageview()嵌入图像。
imageview("tornado.jpg")
像大多数其他控件一样,您可以使用块来修改其属性(图4.20)。
imageview("tornado.jpg") {
scaleX = .50
scaleY = .50
}
ScrollPane
您可以将控件嵌入到ScrollPane
,使其可滚动。 当可用区域变得小于控件时,滚动条将显示出来,以导航该控件的区域。
例如,您可以在ScrollPane
包装一个ImageView
(图4.21)。
scrollpane {
imageview("tornado.jpg")
}
请记住,许多控件(如TableView
和TreeTableView
已经有滚动条,因此将它们包装在ScrollPane
是不必要的(图4.22)。
Hyperlink
您可以创建Hyperlink
控件来模拟典型的到文件,网站的超链接的行为,或者简单地执行操作。
hyperlink("Open File").action { println("Opening file...") }
Text
您可以使用格式化的属性(formatted properties)添加一个简单的Text
。 该控件比Label
简单且原始(simpler and rawer),并且可以使用\n
字符分隔段落(图4.23)。
text("Veni\nVidi\nVici") {
fill = Color.PURPLE
font = Font(20.0)
}
TextFlow
如果您需要连接使用不同格式的多条文本,则TextFlow
控件可能会有所帮助(图4.24)。
textflow {
text("Tornado") {
fill = Color.PURPLE
font = Font(20.0)
}
text("FX") {
fill = Color.ORANGE
font = Font(28.0)
}
}
您可以使用标准构建器函数,将任何Node
添加到textflow
,包括图像。
Tooltips
在任何Node
上,您都可以通过tooltip()函数指定
Tooltip`(图4.25)。
button("Commit") {
tooltip("Writes input to the database")
}
像大多数其他构建器一样,您可以提供一个闭包来自定义Tooltip
本身。
button("Commit") {
tooltip("Writes input to the database") {
font = Font.font("Verdana")
}
}
还有许多其他构建器控件,TornadoFX的维护者已经努力为每个JavaFX控件创建一个构建器。 如果您需要不在这里的内容,请使用Google查看是否包含在JavaFX中。 如果JavaFX中有一个控件可用,那么在TornadoFX中就有一个名称相同的构建器。
总结
在本章中,我们了解到TornadoFX构建器以及它们如何通过使用Kotlin扩展函数工作。 我们还涵盖了基本控件的构建器,如Button
,TextField
和ImageView
。 在接下来的章节中,我们将了解桌面,布局,菜单,图表和其他控件的构建器。 正如您将看到的,将所有这些构建器结合在一起创建了一种强大的方法,以非常结构化和最小的代码来表达复杂的UI。
这些并不是TornadoFX API中唯一的控件构建器,本指南尽可能地跟上。 始终检查GitHub上TornadoFX以查看可用的最新构建器和功能,如果您看到有任何缺失,请提交问题。