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
一看,就center
和bottom
。
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()
}
}
)
}
这就能猜到,界面必然就是bind
到names
,更改数据就能更新界面。
核心列表的实现
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>() {//...}
}
这里知识点好几个:
- 接口脱皮直接变函数;
- 函数参数写到括号外;
- 匿名函数
- 对象的匿名子类单例对象
这里的大括号内部是单例对象的声明(定义)部分,里面的this
是ListCell<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就是滑……很滑很滑……