TornadoFX编程指南,第4章,基本控件

译自《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!") }
            }
        }
    }
}

上面表达了构建器工作的基本概念,并且正在进行三项任务:

  1. 创建一个Button
  2. Button被修改
  3. 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 )Viewroot ,并为其赋值一个构建器函数(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
         }
    }
}
图4.1

另外请注意,我们将在稍后了解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}")
            }
        }
    }

}

请注意,非构建器扩展函数和属性也已添加到不同的控件中。useMaxWidthNode的扩展属性,它将Node设置为占用允许的最大宽度。 在接下来的几章中,我们将会看到更多这些有用的扩展。

在接下来的章节中,我们将介绍每个JavaFX控件的每个相应的构建器。 利用上述理念,您可以从头到尾或者作为参考来阅读以后的章节。

基本控件的构建器

本章的其余部分将介绍常见的JavaFX控件(如ButtonLabelTextField构建器。 下一章将介绍数据驱动控件(如ListViewTableViewTreeTableView构建器)。

Button

对于任何Pane,您可以调用其button()扩展函数向其添加一个Button。 您可以选择传递text参数和Button.() -> Unitlambda来修改其属性。

Pane中,这将添加一个带有红色文本的Button,并在每次点击时打印 “Button pressed!” (图4.2)

button("Press Me") {
    textFill = Color.RED
    action {
        println("Button pressed!")
    }
}
图4.2

Label

您可以调用label()扩展函数将Label添加到给定的Pane。 或者,您可以提供一个文本(StringProperty<String>),一个图形 (类型为NodeObjectProperty<Node>)和Label.() -> Unit的lambda来修改其属性(图4.3)。

label("Lorem ipsum", circle(10, 10, 5)) {
    textFill = Color.BLUE
}
图4.3

TextField

对于任何Pane,您可以通过调用textfield()扩展函数来添加一个TextField(图4.4)。

textfield()
图4.4

您可以选择提供初始文本(initial text)以及闭包(closure)以操纵TextField。 例如,我们可以在其textProperty()添加一个监听器,并在每次更改时打印其值(图4.5)。

textfield("Input something") {
    textProperty().addListener { obs, old, new ->
        println("You typed: " + new)
    }
}
图4.6

PasswordField

如果您需要一个TextField来获取敏感信息,可能需要考虑使用PasswordField。 它将显示匿名字符以防止窥视。 您还可以提供初始密码作为参数,以及代码块来操作它(图4.7)。

passwordfield("my_password") {
    requestFocus()
}
图4.7

CheckBox

您可以创建一个CheckBox以快速创建一个真/假(true/false)状态控件,并可选择使用块来操作它(图4.8)。

checkbox("Admin Mode") {
    action { println(isSelected) }
}

请注意,动作块( action block)被包含在checkbox内,从而您可以访问它的isSelected属性。 如果您不需要访问CheckBox的属性,您可以这样写:checkbox("Admin Mode").action {}

图4.9

您还可以提供一个Property<Boolean>,这将会绑定到其选择状态 。

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
}
图4.10

如果将values声明为参数,则不需要指定通用类型(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是一个按照它的选择状态来表示真/假(true/false)状态的按钮(图4.11)。

togglebutton("OFF") {
    action {
        text = if (isSelected) "ON" else "OFF" 
    }
}

也许一个控制按钮文本的更加自然的方式(idomatic way)是使用绑定到textPropertyStringBinding:

togglebutton {
    val stateText = selectedProperty().stringBinding {
        if (it == true) "ON" else "OFF"
    }
    textProperty().bind(stateText)
}
图4.11

您可以选择将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)
    }
}
图4.12

RadioButton

RadioButtonToggleButton具有相同的功能,但具有不同的视觉风格。 当它被选中时,它会填充一个环形控件(circular control)(图4.13)。

radiobutton("Power User Mode") {
    action {
        println("Power User Mode: $isSelected")
    }
}
图4.13

也可以像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)
    }
}
图4.14

DatePicker

DatePicker声明起来是很简单的。 它允许您从弹出的日历控件(popout calendar control)中选择日期。 您可以选择提供一个块来操作它(图4.15)。

datepicker {
    value = LocalDate.now()
}
图4.15

您还可以提供Property<LocalDate>作为绑定到其值的参数。

val dateProperty = SimpleObjectProperty<LocalDate>()

datepicker(dateProperty) {
    value = LocalDate.now()
}

TextArea

TextArea允许您输入多行自由格式文本(multiline freeform text)。 声明时,您可以选择提供初始文本value以及处理程序块(图4.16)。

textarea("Type memo here") {
    selectAll()
}
图4.16

ProgressBar

ProgressBar可视化完成一个过程趋近完成的进度。 您可以选择提供小于或等于1.0的初始Double值,表示完成百分比(图4.17)。

progressbar(0.5)
图4.17

这是一个更加动态的例子,模拟一个过程在短时间内的进展。

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)
        }
    }
}
图4.18

就像ProgressBar一样,您可以提供一个Property<Double>,和/或一个块作为可选参数(图4.19)。

val completion = SimpleObjectProperty(0.0)
progressindicator(completion)

ImageView

您可以使用imageview()嵌入图像。

imageview("tornado.jpg")
图4.19

像大多数其他控件一样,您可以使用块来修改其属性(图4.20)。

imageview("tornado.jpg") {
    scaleX = .50
    scaleY = .50
}
图4.20

ScrollPane

您可以将控件嵌入到ScrollPane,使其可滚动。 当可用区域变得小于控件时,滚动条将显示出来,以导航该控件的区域。

例如,您可以在ScrollPane包装一个ImageView(图4.21)。

scrollpane {
    imageview("tornado.jpg")
}
图4.21

请记住,许多控件(如TableViewTreeTableView已经有滚动条,因此将它们包装在ScrollPane是不必要的(图4.22)。

Hyperlink

您可以创建Hyperlink控件来模拟典型的到文件,网站的超链接的行为,或者简单地执行操作。

hyperlink("Open File").action { println("Opening file...") }
图4.22

Text

您可以使用格式化的属性(formatted properties)添加一个简单的Text。 该控件比Label简单且原始(simpler and rawer),并且可以使用\n字符分隔段落(图4.23)。

text("Veni\nVidi\nVici") {
    fill = Color.PURPLE
    font = Font(20.0)
}
图4.23

TextFlow

如果您需要连接使用不同格式的多条文本,则TextFlow控件可能会有所帮助(图4.24)。

textflow {
    text("Tornado") {
        fill = Color.PURPLE
        font = Font(20.0)
    }
    text("FX") {
        fill = Color.ORANGE
        font = Font(28.0)
    }
}
图4.24

您可以使用标准构建器函数,将任何Node添加到textflow,包括图像。

Tooltips

在任何Node上,您都可以通过tooltip()函数指定Tooltip`(图4.25)。

button("Commit") {
    tooltip("Writes input to the database")
}
图4.25

像大多数其他构建器一样,您可以提供一个闭包来自定义Tooltip本身。

button("Commit") {
    tooltip("Writes input to the database") {
        font = Font.font("Verdana")
    }
}

还有许多其他构建器控件,TornadoFX的维护者已经努力为每个JavaFX控件创建一个构建器。 如果您需要不在这里的内容,请使用Google查看是否包含在JavaFX中。 如果JavaFX中有一个控件可用,那么在TornadoFX中就有一个名称相同的构建器。

总结

在本章中,我们了解到TornadoFX构建器以及它们如何通过使用Kotlin扩展函数工作。 我们还涵盖了基本控件的构建器,如ButtonTextFieldImageView。 在接下来的章节中,我们将了解桌面,布局,菜单,图表和其他控件的构建器。 正如您将看到的,将所有这些构建器结合在一起创建了一种强大的方法,以非常结构化和最小的代码来表达复杂的UI。

这些并不是TornadoFX API中唯一的控件构建器,本指南尽可能地跟上。 始终检查GitHub上TornadoFX以查看可用的最新构建器和功能,如果您看到有任何缺失,请提交问题。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,761评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,953评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,998评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,248评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,130评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,145评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,550评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,236评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,510评论 1 291
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,601评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,376评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,247评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,613评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,911评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,191评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,532评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,739评论 2 335

推荐阅读更多精彩内容