Kotlin编写JavaFX的顺滑之数据控件(二)表视图TableView基础应用
利用TableView显示数据
TableView显示一个二维表格,它有两个基本组成结构。一个就是行,一个就是列,其中每一行代表着数据集合中的一个数据(其类型为T
),而每一列代表着数据的一个特征(特征目前都当作基本类型,如String
,Int
,Double
,也可以是enum class
)。 列是TableView的内部状态,而其对外部的操作接口是行,对应的一个T
的列表,可以增加、删除和变更。
TableView就是这样一个行和列构成的2维数据的抽象。在使用TableView
时,要完成以下几个步骤:
- 定义行对应的数据结构,构成一个
ObservableList<<T>
。 - 定义所有要显示的列,并逐一添加到
columns
,添加顺序为从左往右的显示顺序。 - 将数据集合赋予给
TableView
的items
。 - 将
TableView
的实例添加到UI的容器中。 - 构造变更数据集合的UI和功能对表格进行操作。
当不提供原位编辑功能时,TableView
的使用是最符合其原始的抽象概念,因此整个实现方式非常自然。
效果
操作的动态图:
全部代码
package org.cardc.tableviewtutor
import javafx.application.Application
import javafx.beans.property.SimpleIntegerProperty
import javafx.beans.property.SimpleObjectProperty
import javafx.beans.property.SimpleStringProperty
import javafx.collections.FXCollections
import javafx.collections.ObservableList
import javafx.geometry.Insets
import javafx.geometry.Pos
import javafx.scene.Parent
import javafx.scene.Scene
import javafx.scene.control.*
import javafx.scene.control.cell.PropertyValueFactory
import javafx.scene.layout.HBox
import javafx.scene.layout.Priority
import javafx.scene.layout.VBox
import javafx.scene.text.Font
import javafx.stage.Stage
/*
Data class
Initial classmates
*/
enum class Gender {
Female, Male
}
data class Person(var name: String = "", var age: Int = 35, var gender: Gender = Gender.Female)
val classmates = mutableListOf<Person>(
Person("Q. Chen", 43, Gender.Male),
Person("L. Li", 53, Gender.Male)
)
/*
View Model
All Observable data
*/
object KotlinClass {
val persons: ObservableList<Person> = FXCollections.observableList(classmates)
private val personsProperty = SimpleObjectProperty(persons)
val classmates: List<Person> get() = persons.toList()
private val personColumns
get() = listOf(TableColumn<Person, String>("Name").apply {
cellValueFactory = PropertyValueFactory<Person, String>("name")
}, TableColumn<Person, Double>("Age").apply {
cellValueFactory = PropertyValueFactory<Person, Double>("age")
}, TableColumn<Person, Gender>("Gender").apply {
cellValueFactory = PropertyValueFactory<Person, Gender>("gender")
})
val tableView
get() = TableView<Person>().apply {
columns.addAll(personColumns)
itemsProperty().bindBidirectional(personsProperty)
}
fun add(person: Person) {
persons.add(person)
}
}
/*
View
All UI elements
*/
class HelloApplication : Application() {
override fun start(stage: Stage) {
val scene = Scene(rootView(), 800.0, 600.0)
stage.title = "Kotlin Class"
stage.scene = scene
stage.show()
}
private fun rootView(): Parent {
return VBox(10.0).apply {
children.add(Label("Classmates").apply {
font = Font("Arial", 30.0)
})
children.add(KotlinClass.tableView.apply {
VBox.setVgrow(this, Priority.ALWAYS)
})
children.add(HBox(10.0).apply {
alignment = Pos.CENTER
VBox.setMargin(this, Insets(0.0, 5.0, 10.0, 5.0))
val name = SimpleStringProperty("")
val age = SimpleIntegerProperty(0)
val gender = SimpleObjectProperty<Gender>(Gender.Female)
children.addAll(
Label("Person: "),
TextField().apply {
textProperty().bindBidirectional(name)
HBox.setHgrow(this, Priority.ALWAYS)
},
Spinner<Int>(0, 120, 35).apply { age.bind(valueProperty()) },
ComboBox<Gender>().apply {
items.addAll(Gender.values())
valueProperty().bindBidirectional(gender)
},
Button("Add").apply {
setOnAction {
KotlinClass.add(
Person(name.value, age.value, gender.value)
)
}
},
)
})
}
}
}
/*
Main
*/
fun main() {
Application.launch(HelloApplication::class.java)
}
代码详解
首先,程序除了引用部分,分为四个部分:
- 程序入口;
- 数据模型;
- 视图模型;
- UI(视图)。
哪怕是再简单的程序,都应该把模型-视图模型-视图进行分离,参见Kotlin+JavaFX的顺滑。
入口
这个比较简单,不再赘述。
视图
也就是UI界面。
/*
View
All UI elements
*/
class HelloApplication : Application() {
override fun start(stage: Stage) {
val scene = Scene(rootView(), 800.0, 600.0)
stage.title = "Kotlin Class"
stage.scene = scene
stage.show()
}
private fun rootView(): Parent {
return VBox(10.0).apply {
children.add(Label("Classmates").apply {
font = Font("Arial", 30.0)
})
children.add(KotlinClass.tableView.apply {
VBox.setVgrow(this, Priority.ALWAYS)
})
children.add(HBox(10.0).apply {
alignment = Pos.CENTER
VBox.setMargin(this, Insets(0.0, 5.0, 10.0, 5.0))
val name = SimpleStringProperty("")
val age = SimpleIntegerProperty(0)
val gender = SimpleObjectProperty<Gender>(Gender.Female)
children.addAll(
Label("Person: "),
TextField().apply {
textProperty().bindBidirectional(name)
HBox.setHgrow(this, Priority.ALWAYS)
},
Spinner<Int>(0, 120, 35).apply { age.bind(valueProperty()) },
ComboBox<Gender>().apply {
items.addAll(Gender.values())
valueProperty().bindBidirectional(gender)
},
Button("Add").apply {
setOnAction {
KotlinClass.add(
Person(name.value, age.value, gender.value)
)
}
},
)
})
}
}
}
首先可以看到,视窗有VBox
组成,从上到下,有标签、TableView
、和一个HBox
。
标签设置了较大的字体;主角TableView
把所有的空间都占掉,这里通过ViewModel
的属性tableView
来访问;下面的工具栏对应增加一个数据的UI组件。
视图模型
所有UI元素和数据之间的访问和调用都由视图模型KotlinClass
来处理。视图模型内部维护的数据都是监视其变动的数据:
val persons: ObservableList<Person> = FXCollections.observableList(classmates)
private val personsProperty = SimpleObjectProperty(persons)
提供的接口包括:
- 获取数据集合;
val classmates: List<Person> get() = persons.toList()
- 获取表格视图;
val tableView
get() = TableView<Person>().apply {
columns.addAll(personColumns)
itemsProperty().bindBidirectional(personsProperty)
}
这里,构造一个新的UI单元,设置其列,并将数据集合双向绑定在一个列表的对象属性上,这里也可以直接设置items
,
items.addAll(persons)
现在这个做法,只是为了方面后面增加表格操作的功能:
- 增加数据;
- 删除数据;
- 整个替换数据;
- 增加一个数据;
fun add(person: Person) {
persons.add(person)
}
其中,构造所有的列是一个内部函数,只在获取表格视图中调用:
private val personColumns
get() = listOf(TableColumn<Person, String>("Name").apply {
cellValueFactory = PropertyValueFactory<Person, String>("name")
}, TableColumn<Person, Double>("Age").apply {
cellValueFactory = PropertyValueFactory<Person, Double>("age")
}, TableColumn<Person, Gender>("Gender").apply {
cellValueFactory = PropertyValueFactory<Person, Gender>("gender")
})
这里的表格列的定义采取了简化的方法,唯一值得逐一的是PropertyValueFactory
的两个模板类分别是,表格数据元素类型、列对应的数据类型,而构造函数的参数则是数据元素对应的属性的名称。
也就是说Person
类必须定义一个可以访问的属性,名称为name
,才能在构造列的时候正确获取单元的指。
阅读PropertyValueFactory
的源代码,按照property
的名称(字符串),通过反射的方法来从T
的对象中获得值。如果名称不对或者T
没有对应的属性,则抛出异常。
public PropertyValueFactory(@NamedArg("property") String property) {
this.property = property;
}
/** {@inheritDoc} */
@Override public ObservableValue<T> call(CellDataFeatures<S,T> param) {
return getCellDataReflectively(param.getValue());
}
private ObservableValue<T> getCellDataReflectively(S rowData) {
if (getProperty() == null || getProperty().isEmpty() || rowData == null) return null;
try {
// we attempt to cache the property reference here, as otherwise
// performance suffers when working in large data models. For
// a bit of reference, refer to RT-13937.
if (columnClass == null || previousProperty == null ||
! columnClass.equals(rowData.getClass()) ||
! previousProperty.equals(getProperty())) {
// create a new PropertyReference
this.columnClass = rowData.getClass();
this.previousProperty = getProperty();
this.propertyRef = new PropertyReference<T>(rowData.getClass(), getProperty());
}
if (propertyRef != null) {
if (propertyRef.hasProperty()) {
return propertyRef.getProperty(rowData);
} else {
T value = propertyRef.get(rowData);
return new ReadOnlyObjectWrapper<T>(value);
}
}
} catch (RuntimeException e) {
// log the warning and move on
final PlatformLogger logger = Logging.getControlsLogger();
if (logger.isLoggable(Level.WARNING)) {
logger.warning("Can not retrieve property '" + getProperty() +
"' in PropertyValueFactory: " + this +
" with provided class type: " + rowData.getClass(), e);
}
propertyRef = null;
}
return null;
}
}
数据模型
这里的简化数据模型非常直观,无需赘述:
enum class Gender {
Female, Male
}
data class Person(var name: String = "", var age: Int = 35, var gender: Gender = Gender.Female)
val classmates = mutableListOf<Person>(
Person("Q. Chen", 43, Gender.Male),
Person("L. Li", 53, Gender.Male)
)
展示了常用的字符串、数字和enum的用法。
总结
使用TableView
显示一个数据集合,步骤如下:
- 定义行对应的数据结构,构成一个
ObservableList<<T>
。 - 定义所有要显示的列,并逐一添加到
columns
,添加顺序为从左往右的显示顺序。 - 将数据集合赋予给
TableView
的items
。 - 将
TableView
的实例添加到UI的容器中。 - 构造变更数据集合的UI和功能对表格进行操作。
需要的代码数量极低,显示中还自动包括各列排序等功能,非常好用。