譯自《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
, TableView
, TreeView
和TreeTableView
。 這些控件以純面向?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
}
您還可以直接提供一個(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
}
cellFormat()
函數(shù)允許您在單元格從屏幕上進(jìn)入視圖時(shí)配置其text
和/或graphic
屬性。 單元格本身被重用(reused),但是每當(dāng)ListView
要求單元格更新它的內(nèi)容時(shí), 就會(huì)調(diào)用cellFormat()
函數(shù)。 在我們的例子中,我們只賦值給graphic
,但如果您只想更改字符串表示,您應(yīng)該賦值給text
。 同時(shí)賦值給text
和graphic
也是完全合法和正常的。 這些值將在列表單元格未顯示活動(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)建它們。 ListCell
和ListCellFragment
實(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)的更改。
讓我們使用ListCellFragment
從cellFormat
示例中重新創(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
的屬性。
name
和birthday
屬性直接綁定到字段內(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ù)叫做startEdit
,commitEdit
,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)
}
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
}
}
}
}
函數(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)型: Region
和Branch
。 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)。
有一些可配置性選項(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)
}
}
}
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 }
}
}
我們來(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
,我們提供屬于該department
的Person
對(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)使用如本章前面所述的TableView
和rowExpander()
。
假設(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ò)將TableView
和TreeView
所需的組件合并在一起來(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)方式與TableView
和ListView
相同。 您提供一個(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)
}
}
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())
}
}
}
這次給網(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)與TableView
, TreeView
以及本章中已經(jīng)看到的其他數(shù)據(jù)控件一起工作得很好。 使用構(gòu)建器模式,您可以快速且函數(shù)性地聲明數(shù)據(jù)的顯示方式。
在第7章中,我們將在布局中嵌入控件,輕松創(chuàng)建更復(fù)雜的UI。