TornadoFX編程指南,第5章,數(shù)據(jù)控件

譯自《Data Controls

數(shù)據(jù)控件

任何重要的應(yīng)用程序都會(huì)使用數(shù)據(jù),并為用戶提供查看,操作和修改數(shù)據(jù)的方法,這對(duì)于用戶界面開(kāi)發(fā)來(lái)說(shuō)不是一件小事。 幸運(yùn)的是,TornadoFX簡(jiǎn)化了許多JavaFX數(shù)據(jù)控件,如ListView , TableViewTreeViewTreeTableView 。 這些控件以純面向?qū)ο蟮姆绞皆O(shè)置起來(lái)可能會(huì)很麻煩。 但是使用構(gòu)建器,通過(guò)函數(shù)性聲明(functional declarations),我們可以以更加流暢的方式對(duì)所有這些控件進(jìn)行編碼。

ListView

ListView類(lèi)似于ComboBox,但它會(huì)顯示ScrollView的所有項(xiàng)目,并具有允許多選的選項(xiàng),如圖5.1所示。

listview<String> {
    items.add("Alpha")
    items.add("Beta")
    items.add("Gamma")
    items.add("Delta")
    items.add("Epsilon")
    selectionModel.selectionMode = SelectionMode.MULTIPLE
}
圖5.1

您還可以直接提供一個(gè)ObservableList的項(xiàng)列表,并省略類(lèi)型聲明,因?yàn)樗梢员煌茢唷?使用ObservableList也可以讓列表中的更改自動(dòng)反映在ListView中。

val greekLetters = listOf("Alpha","Beta",
        "Gamma","Delta","Epsilon").observable()

listview(greekLetters) {
    selectionModel.selectionMode = SelectionMode.MULTIPLE
}

像大多數(shù)數(shù)據(jù)控件一樣,請(qǐng)記住,默認(rèn)情況下, ListView將調(diào)用toString()來(lái)為你的領(lǐng)域類(lèi)(domain class)中的每個(gè)項(xiàng)目呈現(xiàn)文本。

自定義單元格格式化(Custom Cell formatting)

即使ListView的默認(rèn)外觀相當(dāng)無(wú)聊(因?yàn)樗{(diào)用toString()并將其呈現(xiàn)為文本),您還可以修改它,以便每個(gè)單元格都是您選擇的自定義Node。 通過(guò)調(diào)用cellCache(),TornadoFX提供了一種方便的方式來(lái)重載列表中每項(xiàng)返回的Node類(lèi)型(圖5.2)。

class MyView: View() {

    val persons = listOf(
            Person("John Marlow", LocalDate.of(1982,11,2)),
            Person("Samantha James", LocalDate.of(1973,2,4))
    ).observable()

    override val root = listview(persons) {
        cellFormat {
            graphic = cache {
                form {
                    fieldset {
                        field("Name") {
                            label(it.name)
                        }
                        field("Birthday") {
                            label(it.birthday.toString())
                        }
                        label("${it.age} years old") {
                            alignment = Pos.CENTER_RIGHT
                            style {
                                fontSize = 22.px
                                fontWeight = FontWeight.BOLD
                            }
                        }
                    }
                }
            }
        }
    }
}

class Person(val name: String, val birthday: LocalDate) {
    val age: Int get() = Period.between(birthday, LocalDate.now()).years
}
圖5.2 - ListView自定義單元格渲染

cellFormat()函數(shù)允許您在單元格從屏幕上進(jìn)入視圖時(shí)配置其text和/或graphic屬性。 單元格本身被重用(reused),但是每當(dāng)ListView要求單元格更新它的內(nèi)容時(shí), 就會(huì)調(diào)用cellFormat()函數(shù)。 在我們的例子中,我們只賦值給graphic ,但如果您只想更改字符串表示,您應(yīng)該賦值給text 。 同時(shí)賦值給textgraphic也是完全合法和正常的。 這些值將在列表單元格未顯示活動(dòng)項(xiàng)時(shí),被cellFormat函數(shù)自動(dòng)清除。

請(qǐng)注意,每當(dāng)列表單元被要求更新時(shí),就將新節(jié)點(diǎn)分配給graphic屬性可能是昂貴的。 對(duì)于許多用例可能會(huì)很好,但是對(duì)于重節(jié)點(diǎn)圖(heavy node graphs)或使用綁定到單元內(nèi)的ui組件的節(jié)點(diǎn)圖(node graphs where you utilize binding towards the ui components inside the cell),應(yīng)該緩存結(jié)果節(jié)點(diǎn)(resulting node),以便每個(gè)節(jié)點(diǎn)只創(chuàng)建一次節(jié)點(diǎn)圖(node graph)。 這在上面的例子中使用cache包裝器完成。

如果為空才賦值(Assign If Null)

如果您有想要重新創(chuàng)建列表單元格的graphic屬性的原因,則可以使用assignIfNull幫助器,如果該屬性尚未包含值,則將為任何給定屬性分配一個(gè)值。 這將確保您在已分配graphic屬性的單元格上調(diào)用updateItem()時(shí)避免創(chuàng)建新節(jié)點(diǎn)。

cellFormat {
    graphicProperty().assignIfNull {
        label("Hello")
    }
}

ListCellFragment

ListCellFragment是一個(gè)特殊的片段Fragment,可以幫助您管理ListView單元格。 它擴(kuò)展了Fragment,并包含一些額外的ListView特定字段和幫助器。 您從不手動(dòng)實(shí)例化這些片段,而是指示ListView根據(jù)需要?jiǎng)?chuàng)建它們。 ListCellListCellFragment實(shí)例之間有一對(duì)一的關(guān)聯(lián)。 一個(gè)ListCellFragment實(shí)例在其生命周期中,將被用于表示幾個(gè)不同的項(xiàng)。

為了理解這是如何工作的,讓我們考慮一個(gè)手動(dòng)實(shí)現(xiàn)的ListCell,基本上這就是你將如何在vanilla JavaFX中做到這一點(diǎn)的。 當(dāng)ListCell應(yīng)該表示一個(gè)新的項(xiàng),沒(méi)有項(xiàng)或只是同一項(xiàng)的更新時(shí),將調(diào)用updateItem()函數(shù)。 當(dāng)您使用ListCellFragment時(shí),您不需要實(shí)現(xiàn)類(lèi)似于updateItem()的內(nèi)容,但它內(nèi)部的itemProperty將會(huì)自動(dòng)更新以表示新的項(xiàng)。 您可以監(jiān)聽(tīng)對(duì)itemProperty的更改,或者更好地將其直接綁定到ViewModel,以便您的UI可以直接綁定到ViewModel,因此不再需要關(guān)心基礎(chǔ)項(xiàng)的更改。

讓我們使用ListCellFragmentcellFormat示例中重新創(chuàng)建表單。 我們需要一個(gè)ViewModel,我們稱(chēng)之為PersonModel。 有關(guān)ViewModel的完整說(shuō)明,請(qǐng)參閱“編輯模型和驗(yàn)證(Editing Models and Validation)”一章。 現(xiàn)在,假設(shè)ViewModel作為底層Person的代理,并且可以更改Person,而ViewModel中的可觀察值保持不變。 當(dāng)我們創(chuàng)建了我們的PersonCellFragment,我們需要配置ListView來(lái)使用它:

listview(personlist) {
    cellFragment(PersonCellFragment::class)
}

現(xiàn)在是ListCellFragment本身。

class PersonListFragment : ListCellFragment<Person>() {
    val person = PersonModel().bindTo(this)

    override val root = form {
        fieldset {
            field("Name") {
                label(person.name)
            }
            field("Birthday") {
                label(person.birthday)
            }
            label(stringBinding(person.age) { "$value years old" }) {
                alignment = Pos.CENTER_RIGHT
                style {
                    fontSize = 22.px
                    fontWeight = FontWeight.BOLD
                }
            }
        }
    }
}

因?yàn)榇薋ragment將被重用以表示不同的列表項(xiàng),最簡(jiǎn)單的方法是將ui元素綁定到ViewModel的屬性。

namebirthday屬性直接綁定到字段內(nèi)的標(biāo)簽。 最后一個(gè)標(biāo)簽中的age字符串需要使用stringBinding()構(gòu)造,以確保在該項(xiàng)更改時(shí)會(huì)更新。

雖然這可能看起來(lái)比cellFormat()的例子稍微增加了一些工作,但是這種方法可以利用Fragment類(lèi)提供的所有內(nèi)容。 它還強(qiáng)制您在構(gòu)建器層次結(jié)構(gòu)之外定義單元格節(jié)點(diǎn)圖(cell node graph),從而提高了重構(gòu)的可能性并實(shí)現(xiàn)了代碼重用。

額外的助手和編輯支持

ListCellFragment還有一些其他幫助屬性。 它們包括cellProperty,它將在底層單元格更改時(shí)更新,而editProperty將告訴您底層列表單元格是否處于編輯模式。 還有編輯助手函數(shù)叫做startEditcommitEdit,cancelEdit加上一個(gè)onEdit回調(diào)。 ListCellFragment使得利用ListView的現(xiàn)有編輯功能變得微不足道。 TodoMVC演示應(yīng)用程序中可以看到一個(gè)完整的例子。

TableView

可能在TornadoFX中最重要的構(gòu)建器之一是TableView。 如果您已經(jīng)與JavaFX合作,您可能已經(jīng)體驗(yàn)過(guò)面向?qū)ο蟮姆绞綐?gòu)建TableView。 但是TornadoFX使用擴(kuò)展函數(shù)提供了一個(gè)函數(shù)性的聲明構(gòu)造模式,大大簡(jiǎn)化了TableView的編碼。

假設(shè)您有領(lǐng)域類(lèi)型,例如Person。

class Person(val id: Int, val name: String, val birthday: LocalDate) {
    val age: Int get() = Period.between(birthday, LocalDate.now()).years
}

拿幾個(gè)Person實(shí)例,把它們放在一個(gè)ObservableList 。

private val persons = listOf(
        Person(1,"Samantha Stuart",LocalDate.of(1981,12,4)),
        Person(2,"Tom Marks",LocalDate.of(2001,1,23)),
        Person(3,"Stuart Gills",LocalDate.of(1989,5,23)),
        Person(3,"Nicole Williams",LocalDate.of(1998,8,11))
).observable()

您可以使用一個(gè)函數(shù)性結(jié)構(gòu)快速聲明一個(gè)TableView,其所有列,并將items屬性指定為ObservableList<Person>(圖5.3)。

tableview(persons) {
    column("ID",Person::id)
    column("Name", Person::name)
    column("Birthday", Person::birthday)
    column("Age",Person::age)
}
圖5.3

column()函數(shù)是TableView擴(kuò)展函數(shù),它接受header名稱(chēng),使用反射語(yǔ)法的映射屬性(mapped property using reflection syntax)。 然后,TornadoFX將采用每個(gè)映射來(lái)渲染給定列中每個(gè)單元格的值。

如果要對(duì)TableView的列大小調(diào)整策略(resize policies)進(jìn)行細(xì)粒度控制,有關(guān)SmartResize策略的更多信息,請(qǐng)參閱附錄A2。

使用“Property”屬性

如果您遵循JavaFX Property約定設(shè)置您的領(lǐng)域類(lèi)(domain class),它將自動(dòng)支持值編輯。

您可以以常規(guī)方式創(chuàng)建這些Property對(duì)象,也可以使用TornadoFX的property委托來(lái)自動(dòng)創(chuàng)建這些Property聲明,如下所示。

class Person(id: Int, name: String, birthday: LocalDate) {
    var id by property(id)
    fun idProperty() = getProperty(Person::id)

    var name by property(name)
    fun nameProperty() = getProperty(Person::name)

    var birthday by property(birthday)
    fun birthdayProperty() = getProperty(Person::birthday)

    val age: Int get() = Period.between(birthday, LocalDate.now()).years
}

您需要為每個(gè)屬性創(chuàng)建xxxProperty()函數(shù),以便在使用反射時(shí)支持JavaFX的命名約定。 這可以很容易地通過(guò)中繼他們的調(diào)用getProperty()來(lái)檢索給定字段的Property來(lái)完成。 有關(guān)這些屬性委托如何工作的詳細(xì)信息,請(qǐng)參閱附錄A1。

現(xiàn)在在TableView ,您可以使其可編輯,映射到屬性,并應(yīng)用適當(dāng)?shù)膯卧窬庉嫻S(cell-editing factories)來(lái)使值可編輯。

override val root = tableview(persons) {
    isEditable = true
    column("ID",Person::idProperty).useTextField(IntegerStringConverter())
    column("Name", Person::nameProperty).useTextField(DefaultStringConverter())
    column("Birthday", Person::birthdayProperty).useTextField(LocalDateStringConverter())
    column("Age",Person::age)
}

為了允許編輯和渲染,TornadoFX提供了一些列表的默認(rèn)單元格工廠,可以通過(guò)擴(kuò)展函數(shù)輕松調(diào)用。

擴(kuò)展函數(shù) 描述
useTextField() 使用標(biāo)準(zhǔn)TextField和其提供的StringConverter來(lái)編輯值
useComboBox() 通過(guò)ComboBox編輯具有指定的ObservableList<T>單元格的值
useChoiceBox() 使用ChoiceBox接受對(duì)單元格的值的改變
useCheckBox() Boolean值的列渲染可編輯的CheckBox
useProgressBar() Double值列的單元格渲染為的ProgressBar

Property語(yǔ)法替代品

如果你不關(guān)心在一個(gè)函數(shù)中暴露Property (這在實(shí)際使用中是常見(jiàn)的),你可以這樣表達(dá)你的類(lèi):

class Person(id: Int, name: String, birthday: LocalDate) {
    val idProperty = SimpleIntegerProperty(id)
    var id by idProperty

    val nameProperty = SimpleStringProperty(name)
    var name by nameProperty

    val birthdayProperty = SimpleObjectProperty(birthday)
    var birthday by birthdayProperty

    val age: Int get() = Period.between(birthday, LocalDate.now()).years
}

此替代模式將Property作為字段成員公開(kāi)而不是函數(shù)。 如果您喜歡上述語(yǔ)法,但又希望保留該函數(shù),則可以將該屬性設(shè)置為private并如下添加函數(shù):

private val nameProperty = SimpleStringProperty(name)
fun nameProperty() = nameProperty
var name by nameProperty

從這些模式中選擇都是一個(gè)品味的問(wèn)題,您可以使用任何符合您的需求或最佳選擇的版本。

您還可以使用TornadoFX插件將普通屬性轉(zhuǎn)換為JavaFX屬性。 請(qǐng)參閱第13章了解如何做到這一點(diǎn)。

使用cellFormat()

還有其他適用于TableView的擴(kuò)展函數(shù),可以幫助聲明TableView的流程。 例如,您可以在給定列上調(diào)用cellFormat()函數(shù)來(lái)應(yīng)用格式規(guī)則,例如突出顯示“Age”值小于18的單元(圖5.4)。

tableview(persons) {
    column("ID", Person::id)
    column("Name", Person::name)
    column("Birthday", Person::birthday)
    column("Age", Person::age).cellFormat {
        text = it.toString()
        style {
            if (it < 18) {
                backgroundColor += c("#8b0000")
                textFill = Color.WHITE
            } else {
                backgroundColor += Color.WHITE
                textFill = Color.BLACK            
            }
        }
     }
}
圖5.4

函數(shù)性地聲明列值

如果需要將列的值映射到非屬性( non-property)(例如函數(shù)),則可以使用非反射方式(non-reflection means)來(lái)提取該列的值。

假設(shè)你有一個(gè) WeeklyReport類(lèi)型,它有一個(gè)getTotal()函數(shù)接受DayOfWeek參數(shù)(星期一,星期二...星期日的枚舉)。

abstract class WeeklyReport(val startDate: LocalDate) {
    abstract fun getTotal(dayOfWeek: DayOfWeek): BigDecimal
}

假設(shè)你想為每個(gè)DayOfWeek創(chuàng)建一個(gè)列。 您無(wú)法映射到屬性,但您可以顯式映射每個(gè)WeeklyReport項(xiàng)以提取該DayOfWeek的每個(gè)值。

tableview<WeeklyReport> {
    for (dayOfWeek in DayOfWeek.values()) {
        column<WeeklyReport, BigDecimal>(dayOfWeek.toString()) {
            ReadOnlyObjectWrapper(it.value.getTotal(dayOfWeek))
        }
    }
}

這更接近于JavaFX TableColumn的傳統(tǒng)setCellValueFactory()

行擴(kuò)展器(Row Expanders)

稍后我們將了解TreeTableView ,它具有 “parent” 和 “child” 行的概念,但是使用該控件的約束是父和子必須具有相同的列。 幸運(yùn)的是,TornadoFX帶有一個(gè)非常棒的實(shí)用程序,不僅可以顯示給定行的“子表(child table)”,而且可以顯示任何類(lèi)型的Node控件。

假設(shè)我們有兩種領(lǐng)域類(lèi)型: RegionBranch 。 Region是地理區(qū)域,它包含一個(gè)或多個(gè)Branch項(xiàng),它們是特定的業(yè)務(wù)運(yùn)營(yíng)地點(diǎn)(倉(cāng)庫(kù),配送中心等)。 以下是這些類(lèi)型和某些給定實(shí)例的聲明。

class Region(val id: Int, val name: String, val country: String, val branches: ObservableList<Branch>)

class Branch(val id: Int, val facilityCode: String, val city: String, val stateProvince: String)

val regions = listOf(
        Region(1,"Pacific Northwest", "USA",listOf(
                Branch(1,"D","Seattle","WA"),
                Branch(2,"W","Portland","OR")
        ).observable()),
        Region(2,"Alberta", "Canada",listOf(
                Branch(3,"W","Calgary","AB")
        ).observable()),
        Region(3,"Midwest", "USA", listOf(
                Branch(4,"D","Chicago","IL"),
                Branch(5,"D","Frankfort","KY"),
                Branch(6, "W","Indianapolis", "IN")
        ).observable())
).observable()

我們可以創(chuàng)建一個(gè)TableView ,其中每一行都定義了一個(gè)rowExpander()函數(shù),我們可以隨意創(chuàng)建任意一個(gè)Node控件,該Node是根據(jù)特定行的項(xiàng)目構(gòu)建的。 在這種情況下,我們可以為給定Region嵌套另一個(gè)TableView ,以顯示屬于它的所有Branch項(xiàng)。 它將有一個(gè)“+”按鈕列來(lái)展開(kāi)并顯示此擴(kuò)展控件(圖5.5)。

圖5.5

有一些可配置性選項(xiàng),例如“雙擊展開(kāi)”行為和訪問(wèn)expanderColumn (帶有“+”按鈕的列)以驅(qū)動(dòng)填充(drive a padding)(圖5.6)。

override val root = tableview(regions) {
        column("ID",Region::id)
        column("Name", Region::name)
        column("Country", Region::country)
        rowExpander(expandOnDoubleClick = true) {
            paddingLeft = expanderColumn.width
            tableview(it.branches) {
                column("ID",Branch::id)
                column("Facility Code",Branch::facilityCode)
                column("City",Branch::city)
                column("State/Province",Branch::stateProvince)
            }
        }
    }
圖5.6

rowExpander()函數(shù)不必返回TableView而是返回任何類(lèi)型的Node ,包括Forms和其他簡(jiǎn)單或復(fù)雜的控件。

訪問(wèn)擴(kuò)展器列(expander column)

您可能想要在實(shí)際的擴(kuò)展器列(expander column)上操作或調(diào)用函數(shù)。 如果您使用雙擊來(lái)激活擴(kuò)展,您可能不想在表中顯示展開(kāi)列。 首先我們需要引用擴(kuò)展器:

val expander = rowExpander(true) { ... }

如果要隱藏?cái)U(kuò)展器列,只需調(diào)用expander.isVisible = false 。 您還可以通過(guò)調(diào)用expander.toggleExpanded(rowIndex)以編程方式切換任何行的展開(kāi)狀態(tài)。

TreeView

TreeView包含元素,其中每個(gè)元素又可能包含子元素。 通常,有箭頭標(biāo)志允許您擴(kuò)展父元素以查看其子元素。 例如,我們可以在部門(mén)名稱(chēng)下嵌套員工。

傳統(tǒng)上在JavaFX中,填充這些元素是相當(dāng)麻煩和冗長(zhǎng)的。 幸運(yùn)的是TornadoFX比較簡(jiǎn)單。

假設(shè)你有一個(gè)簡(jiǎn)單的類(lèi)型Person和一個(gè)包含幾個(gè)實(shí)例的ObservableList。

data class Person(val name: String, val department: String)

val persons = listOf(
        Person("Mary Hanes","Marketing"),
        Person("Steve Folley","Customer Service"),
        Person("John Ramsy","IT Help Desk"),
        Person("Erlick Foyes","Customer Service"),
        Person("Erin James","Marketing"),
        Person("Jacob Mays","IT Help Desk"),
        Person("Larry Cable","Customer Service")
        )

使用treeview()構(gòu)建器創(chuàng)建TreeView可以在功能上完成圖5.7。

// Create Person objects for the departments
// with the department name as Person.name

val departments = persons
    .map { it.department }
    .distinct().map { Person(it, "") }

treeview<Person> {
    // Create root item
    root = TreeItem(Person("Departments", ""))

    // Make sure the text in each TreeItem is the name of the Person
    cellFormat { text = it.name }

    // Generate items. Children of the root item will contain departments
    populate { parent ->
        if (parent == root) departments else persons.filter { it.department == parent.value.name }
    }
}
圖5.7

我們來(lái)分解這個(gè)過(guò)程:

val departments = persons
    .map { it.department }
    .distinct().map { Person(it, "") }

首先我們收集來(lái)自persons列表的所有departments的清單。 但是,之后我們將每個(gè)department字符串放在一個(gè)Person對(duì)象中,因?yàn)?code>TreeView只接受Person元素。 雖然這不是很直觀,但這是TreeView的約束和設(shè)計(jì)。 我們必須讓每個(gè)department是一個(gè)Person讓其能被接受。

treeview<Person> {
    // Create root item
    root = TreeItem(Person("Departments", ""))

接下來(lái),我們?yōu)?code>TreeView指定最高層級(jí)的root,所有部門(mén)將被嵌套其下,我們給它一個(gè)名為 “Departments” 的占位符Person

  cellFormat { text = it.name }

然后我們指定cellFormat()來(lái)渲染每個(gè)單元格上每個(gè)Person (包括部門(mén))的name

   populate { parent ->
        if (parent == root) departments else persons.filter { it.department == parent.value.name }
    }

最后,我們調(diào)用populate()函數(shù),并提供一個(gè)指示如何向每個(gè)parent提供子級(jí)的塊。 如果parent確實(shí)是root ,那么我們返回departments 。 否則, parent是一個(gè)department,我們提供屬于該departmentPerson對(duì)象的列表。

數(shù)據(jù)驅(qū)動(dòng)TreeView

如果從populate返回的子列表是ObservableList,則該列表的任何更改將自動(dòng)反映在TreeView中。 將為任何出現(xiàn)的新子項(xiàng)調(diào)用填充函數(shù),刪除的項(xiàng)也將導(dǎo)致與之關(guān)聯(lián)的TreeItems被刪除。

具有不同的類(lèi)型的TreeView

使上一個(gè)例子中的每個(gè)實(shí)體都是一個(gè)Person不一定是直觀的。 我們讓每個(gè)部門(mén)都是一個(gè)Person,也是root “部門(mén)”。 對(duì)于更復(fù)雜的TreeView<T> ,其中T是未知的,可以是任意數(shù)量的類(lèi)型,最好用星型投影(star projection)來(lái)使用T型。

使用星形投影(star projection),您可以安全地填充嵌入到TreeView的多個(gè)類(lèi)型。

例如,您可以創(chuàng)建一個(gè)Department類(lèi)型并利用cellFormat()來(lái)利用渲染的類(lèi)型檢查(utilize type-checking for rendering)。 然后,您可以使用一個(gè)將遍歷每個(gè)元素的populate()函數(shù),并為每個(gè)元素指定子元素(如果有)。

data class Department(val name: String)

// Create Department objects for the departments by getting distinct values from Person.department
val departments = persons.map { it.department }.distinct().map { Department(it) }

// Type safe way of extracting the correct TreeItem text
cellFormat {
    text = when (it) {
        is String -> it
        is Department -> it.name
        is Person -> it.name
        else -> throw IllegalArgumentException("Invalid value type")
    }
}

// Generate items. Children of the root item will contain departments, children of departments are filtered
populate { parent ->
    val value = parent.value
    if (parent == root) departments
    else if (value is Department) persons.filter { it.department == value.name }
    else null
}

TreeTableView

TreeTableView操作和功能與TreeView類(lèi)似,但它具有多個(gè)列,因?yàn)樗且粋€(gè)表。 請(qǐng)注意, TreeTableView中的列對(duì)于每個(gè)父元素和子元素都是相同的。 如果您希望父子之間的列不同,請(qǐng)使用如本章前面所述的TableViewrowExpander()

假設(shè)您有一個(gè)Person類(lèi),可選地具有employees參數(shù),如果沒(méi)有人向該Person報(bào)告,則該參數(shù)默認(rèn)為空List<Person>

class Person(val name: String,
  val department: String,
  val email: String,
  val employees: List<Person> = emptyList())

然后你有一個(gè)ObservableList<Person>持有這個(gè)類(lèi)的實(shí)例。

val persons = listOf(
        Person("Mary Hanes", "IT Administration", "mary.hanes@contoso.com", listOf(
            Person("Jacob Mays", "IT Help Desk", "jacob.mays@contoso.com"),
            Person("John Ramsy", "IT Help Desk", "john.ramsy@contoso.com"))),
        Person("Erin James", "Human Resources", "erin.james@contoso.com", listOf(
            Person("Erlick Foyes", "Customer Service", "erlick.foyes@contoso.com"),
            Person("Steve Folley", "Customer Service", "steve.folley@contoso.com"),
            Person("Larry Cable", "Customer Service", "larry.cable@contoso.com")))
).observable()

您可以通過(guò)將TableViewTreeView所需的組件合并在一起來(lái)創(chuàng)建TreeTableView 。 您將需要調(diào)用populate()函數(shù)并設(shè)置根TreeItem

val treeTableView = TreeTableView<Person>().apply {
    column("Name", Person::nameProperty)
    column("Department", Person::departmentProperty)
    column("Email", Person::emailProperty)

    /// Create the root item that holds all top level employees
    root = TreeItem(Person("Employees by leader", "", "", persons))

    // Always return employees under the current person
    populate { it.value.employees }

    // Expand the two first levels
    root.isExpanded = true
    root.children.forEach { it.isExpanded = true }

    // Resize to display all elements on the first two levels
    resizeColumnsToFitContent()
}

還可以使用更多的像Map這樣的臨時(shí)后備存儲(chǔ)。 這樣會(huì)看起來(lái)像這樣:

val tableData = mapOf(
    "Fruit" to arrayOf("apple", "pear", "Banana"),
    "Veggies" to arrayOf("beans", "cauliflower", "cale"),
    "Meat" to arrayOf("poultry", "pork", "beef")
)

treetableview<String>(TreeItem("Items")) {
    column<String, String>("Type", { it.value.valueProperty() })
    populate {
        if (it.value == "Items") tableData.keys
        else tableData[it.value]?.asList()
    }
}

數(shù)據(jù)網(wǎng)格

DataGrid類(lèi)似于GridPane,因?yàn)樗造`活網(wǎng)格的形式顯示了行和列項(xiàng),但相似之處也在那里結(jié)束。 GridPane需要您將子節(jié)點(diǎn)添加到子列表中, DataGrid的數(shù)據(jù)驅(qū)動(dòng)方式與TableViewListView相同。 您提供一個(gè)子項(xiàng)列表,并告訴它如何將這些子項(xiàng)轉(zhuǎn)換為圖形表示(graphical representation)。

它支持一次選擇單個(gè)項(xiàng)目或多個(gè)項(xiàng)目,以便它可以用作例如圖形查看器或其他組件的顯示,您希望對(duì)底層數(shù)據(jù)進(jìn)行可視化表示。 使用方式接近ListView,但您可以在每個(gè)單元格內(nèi)創(chuàng)建任意場(chǎng)景圖形,因此可以輕松地為每個(gè)項(xiàng)可視化多個(gè)屬性。

val kittens = listOf("http://i.imgur.com/DuFZ6PQb.jpg", "http://i.imgur.com/o2QoeNnb.jpg") // more items here

datagrid(kittens) {
    cellCache {
         imageview(it)
    }
}
圖5.8

cellCache()函數(shù)接收列表中的每個(gè)項(xiàng),并且由于我們?cè)谑纠惺褂昧艘粋€(gè)Strings列表,所以我們只需將該字符串傳遞給imageview()構(gòu)建器,即在每個(gè)表格單元格內(nèi)創(chuàng)建一個(gè)ImageView。 調(diào)用cellCache()函數(shù)而不是cellFormat()函數(shù)是重要的,以避免每次DataGrid重繪時(shí)重新創(chuàng)建圖像。 它將重用(reuse)這些項(xiàng)。

讓我們創(chuàng)建一個(gè)涉及更多的場(chǎng)景圖,并且還可以更改每個(gè)單元格的默認(rèn)大?。?/p>

val numbers = (1..10).toList()

datagrid(numbers) {
    cellHeight = 75.0
    cellWidth = 75.0

    multiSelect = true

    cellCache {
        stackpane {
            circle(radius = 25.0) {
                fill = Color.FORESTGREEN
            }
            label(it.toString())
        }
    }
}
圖5.9

這次給網(wǎng)格提供了一個(gè)數(shù)字列表。 我們首先指定單元格高度和寬度為75個(gè)像素,是默認(rèn)大小的一半。 我們還可以配置多選,以便能夠選擇多個(gè)單一元素。 這是通過(guò)擴(kuò)展屬性編寫(xiě)selectionModel.selectionMode = SelectionMode.MULTIPLE的快捷方式。 我們創(chuàng)建一個(gè)StackPane,它將一個(gè)Label放在Circle頂部。

您可能會(huì)想知道為什么標(biāo)簽這么大和而本體還是默認(rèn)大小。 這是從默認(rèn)樣式表(default stylesheet)來(lái)的。 樣式表是進(jìn)一步定制的良好起點(diǎn)。 數(shù)據(jù)網(wǎng)格的所有屬性都可以在代碼和CSS中配置,樣式表列出了所有可能的樣式屬性。

號(hào)碼列表展示了如何支持多選。 當(dāng)選擇單元格時(shí),它接收叫做selected的CSS偽類(lèi)(CSS pseudo class)。 默認(rèn)情況下,它的有關(guān)選擇樣式的行為大體上與ListView行相似。 您可以訪問(wèn)數(shù)據(jù)網(wǎng)格的selectionModel以監(jiān)聽(tīng)選擇更改,查看選擇的項(xiàng)目等。

總結(jié)

函數(shù)性構(gòu)造(Functional constructs)與TableViewTreeView以及本章中已經(jīng)看到的其他數(shù)據(jù)控件一起工作得很好。 使用構(gòu)建器模式,您可以快速且函數(shù)性地聲明數(shù)據(jù)的顯示方式。

在第7章中,我們將在布局中嵌入控件,輕松創(chuàng)建更復(fù)雜的UI。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容