譯自《Layouts and Menus》
布局和菜單
復雜的UI需要很多控件。 這些控件可能需要使用設置策略(set policies),進行分組,定位并調(diào)整大小。 幸運的是,TornadoFX簡化了JavaFX自帶的許多布局(layouts),并且具有自己的專有Form布局。
TornadoFX還具有類型安全的構(gòu)建器(type-safe builders),以高度結(jié)構(gòu)化,聲明性的方式創(chuàng)建菜單。 使用常規(guī)JavaFX代碼構(gòu)建菜單尤其繁瑣,而Kotlin在這個部分真的很出色。
布局構(gòu)建器(Builders for Layouts)
布局(Layouts)將控制分組,并設置有關(guān)其大小和定位行為的策略(policies)。 在技??術(shù)上,布局(layouts)本身就是控件,因此您可以在布局中嵌套布局。 這對于構(gòu)建復雜的UI來說至關(guān)重要,而TornadoFX可以通過明顯地顯示嵌套關(guān)系來簡化UI代碼的維護。
VBox
VBox按照控件在其塊中聲明的順序垂直堆疊控件(圖7.1)。
vbox {
button("Button 1").setOnAction {
println("Button 1 Pressed")
}
button("Button 2").setOnAction {
println("Button 2 Pressed")
}
}

您還可以在子控件的塊中調(diào)用vboxConstraints()來更改VBox的邊距(margin)和垂直增長(vertical growing)行為。
vbox {
button("Button 1") {
vboxConstraints {
marginBottom = 20.0
vGrow = Priority.ALWAYS
}
}
button("Button 2")
}
您可以用vGrow速記擴展屬性(shorthand extension property),而無需調(diào)用vboxConstraints()。
vbox {
button("Button 1") {
vGrow = Priority.ALWAYS
}
button("Button 2")
}
HBox
HBox行為幾乎與VBox相同,但是按照其塊中聲明的順序從左到右水平堆疊所有控件。
hbox {
button("Button 1").setOnAction {
println("Button 1 Pressed")
}
button("Button 2").setOnAction {
println("Button 2 Pressed")
}
}

您還可以在子控件的塊內(nèi)調(diào)用hboxConstraints()來更改HBox的邊距(margin)和橫向增長(horizontal growing behaviors)行為。
hbox {
button("Button 1") {
hboxConstraints {
marginRight = 20.0
hGrow = Priority.ALWAYS
}
}
button("Button 2")
}
您可以使用hGrow縮寫擴展屬性(shorthand extension property),而不調(diào)用hboxConstraints() 。
hbox {
button("Button 1") {
hGrow = Priority.ALWAYS
}
button("Button 2")
}
FlowPane
FlowPane控件從左至右布局控件,并在到達邊界時將其轉(zhuǎn)到下一行。 例如,假設您添加了100個按鈕到FlowPane (圖7.3)。你會注意到它只是從左到右布置按鈕,當它耗盡空間時,它移動到“下一行”。
flowpane {
for (i in 1..100) {
button(i.toString()) {
setOnAction { println("You pressed button $i") }
}
}
}

請注意,當您調(diào)整窗口大小時, FlowLayout將重新布局按鈕,以使它們都可以適合(圖7.4)

FlowLayout不經(jīng)常使用,因為處理大量控件通常是簡單的,但它可以在某些情況下派上用場,也可以在其他布局中使用。
BorderPane
BorderPane是一個非常有用的布局,將控件分為5個區(qū)域: top , left , bottom , right和center 。 可以使用這些區(qū)域的兩個或更多來來保存控件,很容易地構(gòu)建許多UI(圖7.5)。
borderpane {
top = label("TOP") {
useMaxWidth = true
style {
backgroundColor = Color.RED
}
}
bottom = label("BOTTOM") {
useMaxWidth = true
style {
backgroundColor = Color.BLUE
}
}
left = label("LEFT") {
useMaxWidth = true
style {
backgroundColor = Color.GREEN
}
}
right = label("RIGHT") {
useMaxWidth = true
style {
backgroundColor = Color.PURPLE
}
}
center = label("CENTER") {
useMaxWidth = true
style {
backgroundColor = Color.YELLOW
}
}
}

您會注意到top和bottom區(qū)域占據(jù)整個水平空間,而left , center , right必須共享可用的水平空間。 但center有權(quán)獲得任何額外的可用空間(垂直和水平),使其成為像TableView這樣的大型控件的理想選擇。 例如,您可以在left區(qū)域中垂直堆疊一些按鈕,并將TableView放在center區(qū)域(圖7.6)。
borderpane {
left = vbox {
button("REFRESH")
button("COMMIT")
}
center = tableview<Person> {
items = listOf(
Person("Joe Thompson", 33),
Person("Sam Smith", 29),
Person("Nancy Reams", 41)
).observable()
column("NAME",Person::name)
column("AGE",Person::age)
}
}

BorderPane是您可能想要經(jīng)常使用的布局,因為它簡化了許多復雜的UI。top區(qū)域通常用于保存MenuBar , bottom區(qū)域通常保持某種狀態(tài)欄。 您已經(jīng)看到center保持焦點控制,如TableView , left和right保持側(cè)面板與任何不適合放在MenuBar中的外圍控件(如按鈕或工具欄) 。 本節(jié)稍后將介紹菜單。
表單生成器
TornadoFX有一個有用的Form控件來處理大量的用戶輸入。 擁有多個輸入字段以獲取用戶信息是常見的,JavaFX沒有內(nèi)置的解決方案來簡化此操作。 為了解決這個問題,TornadoFX有一個構(gòu)建器來聲明具有任意數(shù)量字段的Form (圖7.7)。
form {
fieldset("Personal Info") {
field("First Name") {
textfield()
}
field("Last Name") {
textfield()
}
field("Birthday") {
datepicker()
}
}
fieldset("Contact") {
field("Phone") {
textfield()
}
field("Email") {
textfield()
}
}
button("Commit") {
action { println("Wrote to database!")}
}
}


是不是很棒? 您可以為每個字段指定一個或多個控件, Form將為您呈現(xiàn)分組和標簽。
您也可以選擇在輸入字段之上布置標簽:
fieldset("FieldSet", labelPosition = VERTICAL)
每個field都包含一個內(nèi)有標簽的容器,另一個容器用于在其中添加的輸入字段。 默認情況下,輸入字段的容器是HBox ,這意味著單個字段中的多個輸入將彼此水平相鄰布置。 您可以指定一個字段的orientation參數(shù),使其在多個輸入之間相互上下排列。 垂直取向的另一個用例是允許輸入隨著垂直方向的擴展而增長。 這對于在表單中顯示TextAreas非常方便:
form {
fieldset("Feedback Form", labelPosition = VERTICAL) {
field("Comment", VERTICAL) {
textarea {
prefRowCount = 5
vgrow = Priority.ALWAYS
}
}
buttonbar {
button("Send")
}
}
}

上面的示例還使用buttonbar構(gòu)建器創(chuàng)建一個沒有標簽的特殊字段,同時保留標簽縮進,使按鈕在輸入框下排列。
您將每個輸入綁定到一個模型(model),您可以將控件布局的渲染留給Form。 因此,如果可能,您可能希望在GridPane上使用它,接下來我們將介紹。
在Form內(nèi)嵌套布局(Nesting layouts inside a Form)
您可以使用您選擇的任何布局容器來包裝fieldets和fields,以創(chuàng)建復雜的表單布局。
form {
hbox(20) {
fieldset("Left FieldSet") {
hbox(20) {
vbox {
field("Field l1a") { textfield() }
field("Field l2a") { textfield() }
}
vbox {
field("Field l1b") { textfield() }
field("Field l2b") { textfield() }
}
}
}
fieldset("Right FieldSet") {
hbox(20) {
vbox {
field("Field r1a") { textfield() }
field("Field r2a") { textfield() }
}
vbox {
field("Field r1b") { textfield() }
field("Field r2b") { textfield() }
}
}
}
}
}

GridPane
如果你想對控件的布局進行細致的管理, GridPane會給你很多的。 當然,它需要更多的配置和代碼樣板。 在繼續(xù)使用GridPane之前,您可能需要考慮使用為您抽象了布局配置的Form或其他布局。
使用GridPane的一種方法是聲明每row的內(nèi)容。 對于任何給定的Node您可以調(diào)用其gridpaneConstraints來配置該Node的各種GridPane行為,例如margin和columnSpan (圖7.10)
gridpane {
row {
button("North") {
useMaxWidth = true
gridpaneConstraints {
marginBottom = 10.0
columnSpan = 2
}
}
}
row {
button("West")
button("East")
}
row {
button("South") {
useMaxWidth = true
gridpaneConstraints {
marginTop = 10.0
columnSpan = 2
}
}
}
}

請注意,在每行之間,如果在其gridpaneConstraints內(nèi)分別為“North”和“South”按鈕的marginBottom和marginTop聲明了每行之間的距離為10.0 。
或者,您可以顯式指定每個Node的列/行索引位置,而不是聲明每row的控件。 這將完成我們之前建立的精確布局,但是使用列/行索引來規(guī)范。 它有點冗長,但它可以更加明確地控制控件的位置。
gridpane {
button("North") {
useMaxWidth = true
gridpaneConstraints {
columnRowIndex(0,0)
marginBottom = 10.0
columnSpan = 2
}
}
button("West").gridpaneConstraints {
columnRowIndex(0,1)
}
button("East").gridpaneConstraints {
columnRowIndex(1,1)
}
button("South") {
useMaxWidth = true
gridpaneConstraints {
columnRowIndex(0,2)
marginTop = 10.0
columnSpan = 2
}
}
}
這些都是您可以在給定Node上修改的gridpaneConstraints屬性。 一些表示為可以賦值的簡單屬性,而其他屬性可以通過函數(shù)賦值。
| 屬性 | 描述 |
|---|---|
| columnIndex:Int | 給定控件的列索引 |
| rowIndex:Int | 給定控件的行索引 |
| columnRowIndex(columnIndex:Int,rowIndex:Int) | 指定行和列索引 |
| columnSpan:Int | 控件占用的列數(shù) |
| rowSpan:Int | 控制占用的行數(shù) |
| hGrow:Priority | 水平增長優(yōu)先 |
| vGrow:Priority | 垂直成長優(yōu)先 |
| vhGrow:Priority | 為vGrow和hGrow指定相同的優(yōu)先級 |
| fillHeight:Boolean | 設置Node是否填充其區(qū)域的高度 |
| fillWidth:Boolean | 設置Node是否填充其區(qū)域的寬度 |
| fillHeightWidth:Boolean | 設置Node是否填充高度和寬度的區(qū)域 |
| hAlignment:HPos | 水平對齊政策 |
| vAlignment:VPos | 垂直對齊策略 |
| margin:Int |
Node所有四邊的邊距 |
| marginBottom:Int |
Node底部的邊距 |
| marginTop:Int |
Node頂端的邊距 |
| marginLeft:Int |
Node左側(cè)的左邊距 |
| marginRight:Int |
Node右側(cè)的右邊距 |
| marginLeftRight:Int |
Node的右邊距和左邊距 |
| marginTopBottom:Int |
Node的頂部和底部邊距 |
另外,如果需要配置ColumnConstraints,可以在GridPane本身的GridPane Node上調(diào)用gridpaneColumnConstraints ,也可以調(diào)用constraintsForColumn(columnIndex)。
gridpane {
row {
button("Left") {
gridpaneColumnConstraints {
percentWidth = 25.0
}
}
button("Middle")
button("Right")
}
constraintsForColumn(1).percentWidth = 50.0
}
StackPane
一個StackPane是一個布局,您將不太經(jīng)常使用。 對于您添加的每個控件,它將逐字地堆疊在一起(literally stack them ),而不是像VBox,但是字面上覆蓋它們(literally overlay them)。
例如,您可以創(chuàng)建一個“BOTTOM” Button并在其頂部放置一個“TOP” Button 。 您聲明控件的順序?qū)⒁韵嗤捻樞驈牡撞康巾敳刻砑铀鼈儯▓D7.10)。
class MyView: View() {
override val root = stackpane {
button("BOTTOM") {
useMaxHeight = true
useMaxWidth = true
style {
backgroundColor += Color.AQUAMARINE
fontSize = 40.0.px
}
}
button("TOP") {
style {
backgroundColor += Color.WHITE
}
}
}
}

TabPane
TabPane創(chuàng)建一個用“tab”分隔的不同屏幕的UI。 這允許通過點擊相應的選項卡快速輕松地切換不同的屏幕(圖7.11)。 您可以聲明一個tabpane(),然后根據(jù)需要聲明盡可能多的tab()實例。 對于每個tab()函數(shù),通過Tab的名稱和父Node控件來填充它。
tabpane {
tab("Screen 1", VBox()) {
button("Button 1")
button("Button 2")
}
tab("Screen 2", HBox()) {
button("Button 3")
button("Button 4")
}
}

TabePane是分隔屏幕并組織大量控件的有效工具。 語法有些簡潔,足以在tab()塊中聲明像TableView這樣的復雜控件(圖7.13)。
tabpane {
tab("Screen 1", VBox()) {
button("Button 1")
button("Button 2")
}
tab("Screen 2", HBox()) {
tableview<Person> {
items = 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()
column("ID",Person::id)
column("Name", Person::name)
column("Birthday", Person::birthday)
column("Age",Person::age)
}
}
}

像許多構(gòu)建器一樣, TabPane有幾個屬性可以調(diào)整其選項卡的行為。 例如,您可以調(diào)用tabClosingPolicy來去掉選項卡上的“X”按鈕,從而無法關(guān)閉。
class MyView: View() {
override val root = tabpane {
tabClosingPolicy = TabPane.TabClosingPolicy.UNAVAILABLE
tab("Screen 1", VBox()) {
button("Button 1")
button("Button 2")
}
tab("Screen 2", HBox()) {
button("Button 3")
button("Button 4")
}
}
}
菜單構(gòu)建器
以嚴格面向?qū)ο蟮姆绞綐?gòu)建菜單可能很麻煩。 但是使用類型安全的構(gòu)建器,Kotlin的函數(shù)結(jié)構(gòu)可以直觀地聲明嵌套的菜單層次結(jié)構(gòu)。
MenuBar,Menu和MenuItem
使用導航菜單在用戶界面上保留大量命令并不常見。 例如, BorderPane的top區(qū)域通常是MenuBar所在的地方。 在那里可以輕松添加菜單和子菜單(圖7.12)。
menubar {
menu("File") {
menu("Connect") {
item("Facebook")
item("Twitter")
}
item("Save")
item("Quit")
}
menu("Edit") {
item("Copy")
item("Paste")
}
}

您還可以選擇提供鍵盤快捷鍵,圖形以及每個item()的action函數(shù)參數(shù),以指定選定操作時的動作(圖7.14)。
menubar {
menu("File") {
menu("Connect") {
item("Facebook", graphic = fbIcon).action { println("Connecting Facebook!") }
item("Twitter", graphic = twIcon).action { println("Connecting Twitter!") }
}
item("Save","Shortcut+S").action {
println("Saving!")
}
menu("Quit","Shortcut+Q").action {
println("Quitting!")
}
}
menu("Edit") {
item("Copy","Shortcut+C").action {
println("Copying!")
}
item("Paste","Shortcut+V").action {
println("Pasting!")
}
}
}
分隔線(Separators)
您可以在Menu的兩個items之間聲明一個separator()來創(chuàng)建一個分隔線。 這有助于給Menu分組命令并將它們分開(圖7.15)。
menu("File") {
menu("Connect") {
item("Facebook")
item("Twitter")
}
separator()
item("Save","Shortcut+S") {
println("Saving!")
}
item("Quit","Shortcut+Q") {
println("Quitting!")
}
}

上下文菜單(ContextMenu)
JavaFX中的大多數(shù)控件都有一個contextMenu屬性,您可以在其中指定ContextMenu實例。 這是一個在右鍵單擊控件時彈出的Menu。
一個ContextMenu有函數(shù)可以添加Menu和MenuItem實例,就像MenuBar一樣 。 例如,將一個ContextMenu添加到TableView<Person>是有幫助的,并提供要在表格記錄上完成的命令(圖7.16)。 有一個名為contextmenu的構(gòu)建器將構(gòu)建一個ContextMenu并將其賦值給控件的contextMenu屬性。
tableview(persons) {
column("ID", Person::id)
column("Name", Person::name)
column("Birthday", Person::birthday)
column("Age", Person::age)
contextmenu {
item("Send Email").action {
selectedItem?.apply { println("Sending Email to $name") }
}
item("Change Status").action {
selectedItem?.apply { println("Changing Status for $name") }
}
}
}

注意還有可用的
RadioMenuItem和CheckMenuItem這些MenuItem變體。
當菜單被選為op塊參數(shù)時,menuitem構(gòu)建器采取動作來執(zhí)行。 不幸的是,這破壞了其他構(gòu)建器,其中op塊對構(gòu)建器創(chuàng)建的元素進行操作。 因此,引入item構(gòu)建器作為替代,您可以在item本身上操作,因此您必須調(diào)用setOnAction來賦值動作。menuitem構(gòu)建器沒有被棄用,因為它以比item構(gòu)建器更簡潔的方式解決了常見情況。
ListMenu
TornadoFX帶有一個列表菜單(ListMenu),其行為和看起來更像是一個典型的基于ul/li的HTML5菜單。


以下代碼示例顯示如何使用構(gòu)建器模式的ListMenu:
listmenu(theme = "blue") {
item(text = "Contacts", graphic = Styles.contactsIcon()) {
// Marks this item as active.
activeItem = this
whenSelected { /* Do some action */ }
}
item(text = "Projects", graphic = Styles.projectsIcon())
item(text = "Settings", graphic = Styles.settingsIcon())
}
以下屬性可用于配置ListMenu :

Css屬性(Css Properties)

偽類(Pseudo Classes)

看看ListMenu的默認樣式表。
項目(Item)
item構(gòu)建器允許以非常方便的方式為ListMenu創(chuàng)建items。 支持以下語法:
item("SomeText", graphic = SomeNode, tag = SomeObject) {
// Marks this item as active.
activeItem = this
// Do some action when selected
whenSelected { /* Action */ }
}

填充父容器(Filling the parent container)
useMaxWidth屬性可用于水平填充父容器。 useMaxHeight屬性將垂直填充父容器。 這些屬性實際上適用于所有節(jié)點,但對ListMenu特別有用。
Squeezebox
JavaFX具有手風琴(Accordion)控件,可讓您將一組TilePanes組合在一起,形成手風琴控件(accordion of controls)。 JavaFX手風琴(Accordion)只允許您一次打開單個手風琴折疊(a single accordion fold),并且還有一些其他缺點。 為了解決這個問題,TornadoFX附帶了SqueezeBox組件,其行為看起來非常類似于手風琴(Accordion),同時提供了一些增強功能。
squeezebox {
fold("Customer Editor", expanded = true) {
form {
fieldset("Customer Details") {
field("Name") { textfield() }
field("Password") { textfield() }
}
}
}
fold("Some other editor", expanded = true) {
stackpane {
label("Nothing here")
}
}
}

一個
Squeezebox顯示兩個折疊,兩者都默認擴展。
您可以通過將multiselect = false傳遞給構(gòu)建器構(gòu)造函數(shù),使SqueezeBox僅允許在任何給定時間展開單個折疊。
您可以選擇通過單擊標題窗格右側(cè)的十字架(clicking a cross in the right corner of the title pane)而允許折疊成為可關(guān)閉的(allow folds to be closable)。 您可以通過將closeable = true傳遞給fold構(gòu)建器,從而以每折為單位啟用關(guān)閉按鈕(enable the close buttons on a per fold basis)。
squeezebox {
fold("Customer Editor", expanded = true, closeable = true) {
form {
fieldset("Customer Details") {
field("Name") { textfield() }
field("Password") { textfield() }
}
}
}
fold("Some other editor", closeable = true) {
stackpane {
label("Nothing here")
}
}
}

這個
SqueezeBox有可關(guān)閉的折疊(closeable folds)。
closeable屬性當然可以結(jié)合expanded。
SqueezeBox和Accordion之間的另一個重要區(qū)別就是分配空間(distributes overflowing space)的方式。 手風琴(Accordion)將垂直延伸以填充其父容器,并將當前打開的任何折疊推至底部。 如果父容器非常大,這將創(chuàng)建一個不自然的查看視圖。 在這方面,擠壓框(SqueezeBox)可能默認就是您想要的,但您可以添加fillHeight = true以獲得類似于Accordion的外觀。
您可以像您一樣創(chuàng)建一個TitlePane樣式一樣來創(chuàng)建SqueezeBox樣式。 關(guān)閉按鈕有一個名為close-button的css類,容器有一個名為squeeze-box的css類。
Drawer
抽屜(Drawer)是一個非常像TabPane的導航組件,但它在父容器的任一側(cè)的垂直或水平放置的按鈕欄中組織每個抽屜項目。 它類似于許多流行的業(yè)務應用程序和IDE中發(fā)現(xiàn)的工具抽屜(tool drawers)。 當選擇項目時,項目的內(nèi)容將顯示在跨越控件的高度或?qū)挾鹊膬?nèi)容區(qū)域中的按鈕旁邊或上方/下方,以及內(nèi)容的首選寬度或高度,具體取決于是否將其??吭诟讣壍拇怪被蛩椒矫妗?在多重選擇(multiselect)模式下,您甚至可以同時打開多個抽屜物品,讓它們共享它們之間的空間。 它們將始終按照相應按鈕的順序打開。
class DrawerView : View("TornadoFX Info Browser") {
override val root = drawer {
item("Screencasts", expanded = true) {
webview {
prefWidth = 470.0
engine.userAgent = iPhoneUserAgent
engine.load(TornadoFXScreencastsURI)
}
}
item("Links") {
listview(links) {
cellFormat { link ->
graphic = hyperlink(link.name) {
setOnAction {
hostServices.showDocument(link.uri)
}
}
}
}
}
item("People") {
tableview(people) {
column("Name", Person::name)
column("Nick", Person::nick)
}
}
}
class Link(val name: String, val uri: String)
class Person(val name: String, val nick: String)
// Sample data variables left out (iPhoneUserAgent, TornadoFXScreencastsURI, people and links)
}

抽屜可以配置為顯示右側(cè)的按鈕,您可以選擇同時支持打開多個抽屜物品。 當以多重選擇模式運行時,內(nèi)容上方會出現(xiàn)一個標題,這將有助于區(qū)分內(nèi)容區(qū)域中的項目。 您可以使用布爾的showHeader參數(shù)控制標題外觀。 當啟用多重選擇時,它將默認為true,否則為false。
drawer(side = Side.RIGHT, multiselect = true) {
// Everything else is identical
}

帶有右側(cè)按鈕的抽屜,多選模式和標題窗格。
當抽屜被添加到某物的旁邊時,您可以選擇抽屜的內(nèi)容區(qū)域是否應替換其旁邊的節(jié)點(默認)或浮動。 floatingContent屬性默認為false,導致Drawer替換其旁邊的內(nèi)容。
您可以使用Drawer的maxContentSize和fixedContentSize屬性進一步控制內(nèi)容區(qū)域的大小。 根據(jù)dockingSide,這些屬性將限制內(nèi)容區(qū)域的寬度或高度。
Workspace功能內(nèi)置支持抽屜控件。 任何Workspace的leftDrawer, rightDrawer和bottomDrawer屬性將允許您將抽屜項目bottomDrawer在其中。 在“工作區(qū)(Workspace)”一章中了解更多信息。
轉(zhuǎn)換可觀察列表項并綁定到布局(Converting observable list items and binding to layouts)
TODO
總結(jié)
到目前為止,您應該擁有能力可以使用布局,標簽窗格以及其他控件來管理控件。 將這些與數(shù)據(jù)控件結(jié)合使用,您應該可以在一小部分時間內(nèi)轉(zhuǎn)換UI。
當涉及到構(gòu)建器時,您已經(jīng)達到了頂峰(top of the peak),并擁有所需要的所有成效。 剩下的所有內(nèi)容都是圖表和形狀(charts and shapes),我們將在接下來的兩章中介紹。