Kotlin编写JavaFX的顺滑之数据控件(一)列表视图ListView

效果先行

一个列表视图控件,可以实现展示一系列项,每一个项对应ListView界面中的一行。这个控件很简单,但是也很好用。当采用Kotlin编写的时候,就更好用了。

  • 增加项
  • 清除项
  • 选择项
  • 原位编辑
    列表试图

再给代码

data class Kid(
    var firstname: String = "",
    var middleName: String = "",
    var lastname: String = "",
    var age: Int = 0
)

class DefaultFocusInScope {
    private val _ff = SimpleBooleanProperty(false)

    /**
     * Set n be the focused node in the scope, when focused, call func
     * @param n Node
     * @param func Function0<Unit>
     */
    fun focusNode(n: Node, func: () -> Unit) {
        _ff.addListener { _, _, _ ->
            Platform.runLater {
                n.requestFocus()
                func()
            }
        }
    }

    /**
     * set focus to the default focused node in this scope.
     */
    fun focus() {
        _ff.value = !(_ff.value)
    }
}

fun randString(len: Int = 10, from: Int = 97, until: Int = 122) = (1..len).map {
    Random.nextInt(from, until).toChar()
}.toTypedArray().joinToString(separator = "") { "$it" }


fun listViewDemo(): Parent {
    val names = SimpleObjectProperty(FXCollections.observableArrayList<Kid>())

    return BorderPane().apply {
        padding = Insets(5.0)
        center = ListView<Kid>().apply {
            isEditable = true
            itemsProperty().bindBidirectional(names)
            setCellFactory {
                object : ListCell<Kid>() {
                    val thisKid = SimpleObjectProperty(Kid())
                    override fun updateItem(item: Kid?, empty: Boolean) {
                        super.updateItem(item, empty)
                        if (!empty && item != null) {
                            graphic = HBox().apply {
                                thisKid.value = item
                                children.add(Label(item.firstname).apply {
                                    HBox.setHgrow(this, Priority.ALWAYS)
                                    maxWidth = Double.MAX_VALUE
                                })
                                children.add(Separator(Orientation.VERTICAL))
                                children.add(Label(item.middleName).apply { prefWidth = 32.0 })
                                children.add(Separator(Orientation.VERTICAL))
                                children.add(Label(item.lastname).apply { prefWidth = 56.0 })
                                children.add(Separator(Orientation.VERTICAL))
                                children.add(Label("${item.age}"))
                            }
                        } else {
                            text = null
                            graphic = null
                        }
                    }

                    override fun startEdit() {
                        val af = DefaultFocusInScope()
                        super.startEdit()
                        graphic = HBox().apply {
                            with(thisKid.value) {
                                children.add(TextField(firstname).apply {
                                    HBox.setHgrow(this, Priority.ALWAYS)
                                    maxWidth = Double.MAX_VALUE
                                    textProperty().addListener { _ ->
                                        firstname = text
                                    }
                                    af.focusNode(this) { end() }
                                })
                                children.add(TextField(middleName).apply {
                                    prefWidth = 48.0
                                    textProperty().addListener { _ ->
                                        middleName = text
                                    }
                                })
                                children.add(TextField(lastname).apply {
                                    textProperty().addListener { _ ->
                                        lastname = text
                                    }
                                    prefWidth = 60.0
                                })
                                children.add(TextField("${age}").apply {
                                    prefWidth = 48.0
                                    textProperty().addListener { _ ->
                                        text.toIntOrNull()?.let { age = it }
                                    }
                                })
                                children.add(Button("↵").apply {
                                    setOnAction { commitEdit(thisKid.value) }
                                    isDefaultButton = true
                                })
                            }
                        }
                        af.focus()
                    }
                }
            }
        }
        bottom = HBox().apply {
            alignment = Pos.CENTER
            children.addAll(
                Button("Add item").apply {
                    setOnAction {
                        names.value.add(
                            Kid(
                                randString(12),
                                randString(2),
                                randString(5),
                                Random.nextInt(1, 10)
                            )
                        )
                    }
                },
                Button("Clear").apply {
                    setOnAction {
                        names.value.clear()
                    }
                }
            )
        }
    }
}

class ListViewTutorialApplication : Application() {
    override fun start(stage: Stage) {
        val scene = Scene(listViewDemo(), 800.0, 600.0)
        stage.title = "ListView"
        stage.scene = scene
        stage.show()
    }
}

fun main() {
    Application.launch(ListViewTutorialApplication::class.java)
}

代码解释

程序入口

这个部分不用解释,直接引导我们去看listViewDemo()函数。

class ListViewTutorialApplication : Application() {
    override fun start(stage: Stage) {
        val scene = Scene(listViewDemo(), 800.0, 600.0)
        stage.title = "ListView"
        stage.scene = scene
        stage.show()
    }
}

fun main() {
    Application.launch(ListViewTutorialApplication::class.java)
}

根节点

一眼看过去,简单,根节点是一个BorderPane() 。这个函数还声明一个对象属性,对象属性里面是一个可观察的列表。数据项是Kid

fun listViewDemo(): Parent {
    val names = SimpleObjectProperty(FXCollections.observableArrayList<Kid>())
    return BorderPane()
}

既然是BorderPane,下面就看五个葫芦娃

  • center
  • top
  • left
  • right
  • bottom

一看,就centerbottom

bottom节点

下方一个横向的布局,里面两个按钮,一个增加一个项,一个清除一个项。直接通过setOnAction函数来实现,清爽。

        bottom = HBox().apply {
            alignment = Pos.CENTER
            children.addAll(
                Button("Add item").apply {
                    setOnAction {
                        names.value.add(
                            Kid(
                                randString(12),
                                randString(2),
                                randString(5),
                                Random.nextInt(1, 10)
                            )
                        )
                    }
                },
                Button("Clear").apply {
                    setOnAction {
                        names.value.clear()
                    }
                }
            )
        }

这就能猜到,界面必然就是bindnames,更改数据就能更新界面。

核心列表的实现

        center = ListView<Kid>().apply {
            isEditable = true
            itemsProperty().bindBidirectional(names)
            // ......
        }

首先,ListView是一个模板类,有一个项的类型参数,这里的参数是胡乱定义的一个data class,因为我在送小孩上兴趣班……因此就叫Kid

这里设置列表可以编辑,也就是双击一个项就会进入编辑模式。接下来,把项与前面定义的SimpleObjectProperty(FXCollections.observableArrayList<Kid>())双向绑定,这个定义形式在数据控件中很常用,记住就行。

要实现这么fancy的效果,就调用一个函数:

public final void setCellFactory(Callback<ListView<T>, ListCell<T>> value)

其中那个Callback是一个接口,函数输入ListView<T>,输出ListCell<T>

public interface Callback<P,R> {
    public R call(P param);
}

这样的东西在Kotlin里面就变得极为简单,采用lambda函数来实现call函数就行。

setCellFactory { it:ListView<Kid> ->
                object : ListCell<Kid>() {//...}
}

这里知识点好几个:

  • 接口脱皮直接变函数;
  • 函数参数写到括号外;
  • 匿名函数
  • 对象的匿名子类单例对象

这里的大括号内部是单例对象的声明(定义)部分,里面的thisListCell<Kid>子类的定义部分,所以能够重载ListCell<Kid>的函数。

为了显示自定义的项,需要重载:override fun updateItem(item: Kid?, empty: Boolean),为了能够原位编辑,需要重载override fun startEdit()

显示项

这个函数非常简单,首先调用父类,然后对象不为空并且标志不为空,设置text和/或graphic,记得,设置成null要两个都要设。不然会有奇怪的显示。

override fun updateItem(item: Kid?, empty: Boolean) {
                        super.updateItem(item, empty)
                        if (!empty && item != null) {
                            graphic = HBox().apply {
                                thisKid.value = item
                                children.add(Label(item.firstname).apply {
                                    HBox.setHgrow(this, Priority.ALWAYS)
                                    maxWidth = Double.MAX_VALUE
                                })
                                children.add(Separator(Orientation.VERTICAL))
                                children.add(Label(item.middleName).apply { prefWidth = 32.0 })
                                children.add(Separator(Orientation.VERTICAL))
                                children.add(Label(item.lastname).apply { prefWidth = 56.0 })
                                children.add(Separator(Orientation.VERTICAL))
                                children.add(Label("${item.age}"))
                            }
                        } else {
                            text = null
                            graphic = null
                        }
                    }

编辑项

这里唯一需要注意的是,把graphic设成可以编辑的控件,最后设定一个事件来调用commitEdit函数,完成项编辑。

                    override fun startEdit() {
                        val af = DefaultFocusInScope()
                        super.startEdit()
                        graphic = HBox().apply {
                            with(thisKid.value) {
                                children.add(TextField(firstname).apply {
                                    HBox.setHgrow(this, Priority.ALWAYS)
                                    maxWidth = Double.MAX_VALUE
                                    textProperty().addListener { _ ->
                                        firstname = text
                                    }
                                    af.focusNode(this) { end() }
                                })
                                children.add(TextField(middleName).apply {
                                    prefWidth = 48.0
                                    textProperty().addListener { _ ->
                                        middleName = text
                                    }
                                })
                                children.add(TextField(lastname).apply {
                                    textProperty().addListener { _ ->
                                        lastname = text
                                    }
                                    prefWidth = 60.0
                                })
                                children.add(TextField("${age}").apply {
                                    prefWidth = 48.0
                                    textProperty().addListener { _ ->
                                        text.toIntOrNull()?.let { age = it }
                                    }
                                })
                                children.add(Button("↵").apply {
                                    setOnAction { commitEdit(thisKid.value) }
                                    isDefaultButton = true
                                })
                            }
                        }
                        af.focus()
                    }

其他无聊的部分

这个部分是设置焦点的问题,设置一个节点的某个子节点为默认的焦点,又不喜欢保存子节点的实例,所以编写一个类来干这个事情。

逻辑非常简单,一个简单的属性,这个属性变更时,把节点请求焦点的事件加到JavaFX事件队列中,还能调用一个自定义的函数,这用来实现清空选择并把光标放在文本框的末尾。

class DefaultFocusInScope {
    private val _ff = SimpleBooleanProperty(false)

    /**
     * Set n be the focused node in the scope, when focused, call func
     * @param n Node
     * @param func Function0<Unit>
     */
    fun focusNode(n: Node, func: () -> Unit) {
        _ff.addListener { _, _, _ ->
            Platform.runLater {
                n.requestFocus()
                func()
            }
        }
    }

    /**
     * set focus to the default focused node in this scope.
     */
    fun focus() {
        _ff.value = !(_ff.value)
    }
}

随机字符串,比较无聊。

fun randString(len: Int = 10, from: Int = 97, until: Int = 122) = (1..len).map {
    Random.nextInt(from, until).toChar()
}.toTypedArray().joinToString(separator = "") { "$it" }

结论

一句话,Kotlin就是滑……很滑很滑……

posted @ 2022-07-15 17:20  大福是小强  阅读(45)  评论(0编辑  收藏  举报  来源