Kotlin编写JavaFX的顺滑之数据控件(二)表视图TableView基础应用

利用TableView显示数据

TableView显示一个二维表格,它有两个基本组成结构。一个就是行,一个就是列,其中每一行代表着数据集合中的一个数据(其类型为T),而每一列代表着数据的一个特征(特征目前都当作基本类型,如StringIntDouble,也可以是enum class)。 列是TableView的内部状态,而其对外部的操作接口是行,对应的一个T的列表,可以增加、删除和变更。

TableView就是这样一个行和列构成的2维数据的抽象。在使用TableView时,要完成以下几个步骤:

  1. 定义行对应的数据结构,构成一个ObservableList<<T>
  2. 定义所有要显示的列,并逐一添加到columns,添加顺序为从左往右的显示顺序。
  3. 将数据集合赋予给TableViewitems
  4. TableView的实例添加到UI的容器中。
  5. 构造变更数据集合的UI和功能对表格进行操作。

当不提供原位编辑功能时,TableView的使用是最符合其原始的抽象概念,因此整个实现方式非常自然。

效果

利用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)
}

代码详解

首先,程序除了引用部分,分为四个部分:

  1. 程序入口;
  2. 数据模型;
  3. 视图模型;
  4. 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)

提供的接口包括:

  1. 获取数据集合;
    val classmates: List<Person> get() = persons.toList()
  1. 获取表格视图;
    val tableView
        get() = TableView<Person>().apply {
            columns.addAll(personColumns)
            itemsProperty().bindBidirectional(personsProperty)
        }

这里,构造一个新的UI单元,设置其列,并将数据集合双向绑定在一个列表的对象属性上,这里也可以直接设置items,

items.addAll(persons)

现在这个做法,只是为了方面后面增加表格操作的功能:

  • 增加数据;
  • 删除数据;
  • 整个替换数据;
  1. 增加一个数据;
    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显示一个数据集合,步骤如下:

  1. 定义行对应的数据结构,构成一个ObservableList<<T>
  2. 定义所有要显示的列,并逐一添加到columns,添加顺序为从左往右的显示顺序。
  3. 将数据集合赋予给TableViewitems
  4. TableView的实例添加到UI的容器中。
  5. 构造变更数据集合的UI和功能对表格进行操作。

需要的代码数量极低,显示中还自动包括各列排序等功能,非常好用。

posted @ 2023-02-13 13:08  大福是小强  阅读(73)  评论(0编辑  收藏  举报  来源