譯自《Basic Controls》
基本控件
TornadoFX最令人興奮的功能之一就是Type-Safe Builders。 配置(Configuring)和布置(laying out)復雜UI的控件可能是冗長而困難的,代碼可能很快變得混亂而難以維護。 幸運的是,您可以使用由Groovy開創(chuàng)的強大的閉包模式(powerful closure pattern) ,以純粹和簡單的Kotlin代碼來創(chuàng)建結構化的UI布局。
雖然我們稍后會學習如何應用FXML,但是您可能會發(fā)現(xiàn)構建器(builders)是一個表達力強勁的方法,可以在一小段時間內創(chuàng)建復雜的UI。 沒有配置文件或編譯器的魔術,構建器使用純Kotlin代碼完成。 接下來的幾個章節(jié)將把構建器分為不同類別的控件。 一路上,您將逐漸通過將這些構建器集成在一起來構建更復雜的UI。
但首先,讓我們來看看構建器如何實際工作。
構建器如何工作
Kotlin的標準庫提供了一些有用的“塊(block)”函數(shù),目標是任何類型T。 有with()函數(shù)(with() function) ,它允許你編寫一個item的代碼,好像你正好在它的類中一樣。
class MyView : View() {
override val root = VBox()
init {
with(root) {
this += Button("Press Me")
}
}
}
在上面的例子中,with()函數(shù)接受root作為參數(shù)。 以下的閉包參數(shù)通過將root引用為this來直接操作root,這被安全地解釋為VBox 。 通過調用它的plusAssign()擴展運算符(extended operator)將一個Button添加到VBox 。
或者,Kotlin中的每個類型都有一個apply()函數(shù)(apply() function) 。 這與with()是幾乎相同的功能,但它實際上是一個擴展的高階函數(shù)(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!") }
}
}
}
}
上面表達了構建器工作的基本概念,并且正在進行三項任務:
- 創(chuàng)建一個
Button -
Button被修改 - 該
Button被添加到它的“父級(parent)”,它是一個VBox
當聲明任何Node,這三個步驟是如此常見,以至于TornadoFX使用策略性放置的擴展函數(shù)(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()函數(shù)調用的呢?為什么我們使用一個名為button()的函數(shù)而不是實際的Button呢? 我們不會太深入如何做到這一點,如果你好奇,你可以隨時挖掘源代碼(source code ) 。
但本質上, VBox (或任何可定位的組件)具有稱為button()的擴展函數(shù)。 它接受一個文本參數(shù)和一個可選的閉包,目標是它將實例化的Button。 當調用此函數(shù)時,將創(chuàng)建一個帶指定文本的Button,對其應用閉包,將其添加到在其上調用的VBox ,然后將其返回。
為進一步提高效率,您可以重載(override )View的root ,并為其賦值一個構建器函數(shù)(builder function),從而可以避免需要任何init()和with()塊。
class MyView : View() {
override val root = vbox {
button("Press Me") {
textFill = Color.RED
action { println("Button pressed!") }
}
}
}
當您將控件嵌套到其他控件中時,構建器模式變得特別強大。 使用這些構建器擴展函數(shù),您可以輕松地將多個HBox實例嵌入到一個VBox ,并創(chuàng)建一個結構清晰的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等控件的引用,則可以將它們保存到變量或屬性中,因為函數(shù)返回生成的控件。 建議您使用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}")
}
}
}
}
請注意,非構建器擴展函數(shù)和屬性也已添加到不同的控件中。useMaxWidth是Node的擴展屬性,它將Node設置為占用允許的最大寬度。 在接下來的幾章中,我們將會看到更多這些有用的擴展。
在接下來的章節(jié)中,我們將介紹每個JavaFX控件的每個相應的構建器。 利用上述理念,您可以從頭到尾或者作為參考來閱讀以后的章節(jié)。
基本控件的構建器
本章的其余部分將介紹常見的JavaFX控件(如Button, Label和TextField構建器。 下一章將介紹數(shù)據(jù)驅動控件(如ListView, TableView和TreeTableView構建器)。
Button
對于任何Pane,您可以調用其button()擴展函數(shù)向其添加一個Button。 您可以選擇傳遞text參數(shù)和Button.() -> Unit的lambda來修改其屬性。
在Pane中,這將添加一個帶有紅色文本的Button,并在每次點擊時打印 “Button pressed!” (圖4.2)
button("Press Me") {
textFill = Color.RED
action {
println("Button pressed!")
}
}

Label
您可以調用label()擴展函數(shù)將Label添加到給定的Pane。 或者,您可以提供一個文本(String或Property<String>),一個圖形 (類型為Node或ObjectProperty<Node>)和Label.() -> Unit的lambda來修改其屬性(圖4.3)。
label("Lorem ipsum", circle(10, 10, 5)) {
textFill = Color.BLUE
}

TextField
對于任何Pane,您可以通過調用textfield()擴展函數(shù)來添加一個TextField(圖4.4)。
textfield()

您可以選擇提供初始文本(initial text)以及閉包(closure)以操縱TextField。 例如,我們可以在其textProperty()添加一個監(jiān)聽器,并在每次更改時打印其值(圖4.5)。
textfield("Input something") {
textProperty().addListener { obs, old, new ->
println("You typed: " + new)
}
}

PasswordField
如果您需要一個TextField來獲取敏感信息,可能需要考慮使用PasswordField。 它將顯示匿名字符以防止窺視。 您還可以提供初始密碼作為參數(shù),以及代碼塊來操作它(圖4.7)。
passwordfield("my_password") {
requestFocus()
}

CheckBox
您可以創(chuàng)建一個CheckBox以快速創(chuàng)建一個真/假(true/false)狀態(tài)控件,并可選擇使用塊來操作它(圖4.8)。
checkbox("Admin Mode") {
action { println(isSelected) }
}
請注意,動作塊( action block)被包含在checkbox內,從而您可以訪問它的isSelected屬性。 如果您不需要訪問CheckBox的屬性,您可以這樣寫:checkbox("Admin Mode").action {} 。

您還可以提供一個Property<Boolean>,這將會綁定到其選擇狀態(tài) 。
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<String> {
items = texasCities
}

如果將values聲明為參數(shù),則不需要指定通用類型(generic type)。
val texasCities = FXCollections.observableArrayList("Austin",
"Dallas","Midland","San Antonio","Fort Worth")
combobox(values = texasCities)
您還可以指定要綁定到所選值的Property<T> 。
val texasCities = FXCollections.observableArrayList("Austin",
"Dallas","Midland","San Antonio","Fort Worth")
val selectedCity = SimpleStringProperty()
combobox(selectedCity, texasCities)
ToggleButton
ToggleButton是一個按照它的選擇狀態(tài)來表示真/假(true/false)狀態(tài)的按鈕(圖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()函數(shù)。 這將確保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具有相同的功能,但具有不同的視覺風格。 當它被選中時,它會填充一個環(huán)形控件(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<LocalDate>作為綁定到其值的參數(shù)。
val dateProperty = SimpleObjectProperty<LocalDate>()
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)

這是一個更加動態(tài)的例子,模擬一個過程在短時間內的進展。
progressbar() {
thread {
for (i in 1..100) {
Platform.runLater { progress = i.toDouble() / 100.0 }
Thread.sleep(100)
}
}
}
您還可以傳遞一個將progress綁定到其值的Property<Double>,以及一個操作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<Double>,和/或一個塊作為可選參數(shù)(圖4.19)。
val completion = SimpleObjectProperty(0.0)
progressindicator(completion)
ImageView
您可以使用imageview()嵌入圖像。
imageview("tornado.jpg")

像大多數(shù)其他控件一樣,您可以使用塊來修改其屬性(圖4.20)。
imageview("tornado.jpg") {
scaleX = .50
scaleY = .50
}

ScrollPane
您可以將控件嵌入到ScrollPane,使其可滾動。 當可用區(qū)域變得小于控件時,滾動條將顯示出來,以導航該控件的區(qū)域。
例如,您可以在ScrollPane包裝一個ImageView(圖4.21)。
scrollpane {
imageview("tornado.jpg")
}

請記住,許多控件(如TableView和TreeTableView已經有滾動條,因此將它們包裝在ScrollPane是不必要的(圖4.22)。
Hyperlink
您可以創(chuàng)建Hyperlink控件來模擬典型的到文件,網站的超鏈接的行為,或者簡單地執(zhí)行操作。
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)
}
}

您可以使用標準構建器函數(shù),將任何Node添加到textflow,包括圖像。
Tooltips
在任何Node上,您都可以通過tooltip()函數(shù)指定Tooltip`(圖4.25)。
button("Commit") {
tooltip("Writes input to the database")
}

像大多數(shù)其他構建器一樣,您可以提供一個閉包來自定義Tooltip本身。
button("Commit") {
tooltip("Writes input to the database") {
font = Font.font("Verdana")
}
}
還有許多其他構建器控件,TornadoFX的維護者已經努力為每個JavaFX控件創(chuàng)建一個構建器。 如果您需要不在這里的內容,請使用Google查看是否包含在JavaFX中。 如果JavaFX中有一個控件可用,那么在TornadoFX中就有一個名稱相同的構建器。
總結
在本章中,我們了解到TornadoFX構建器以及它們如何通過使用Kotlin擴展函數(shù)工作。 我們還涵蓋了基本控件的構建器,如Button,TextField和ImageView。 在接下來的章節(jié)中,我們將了解桌面,布局,菜單,圖表和其他控件的構建器。 正如您將看到的,將所有這些構建器結合在一起創(chuàng)建了一種強大的方法,以非常結構化和最小的代碼來表達復雜的UI。
這些并不是TornadoFX API中唯一的控件構建器,本指南盡可能地跟上。 始終檢查GitHub上TornadoFX以查看可用的最新構建器和功能,如果您看到有任何缺失,請?zhí)峤粏栴}。