译自《Layouts and Menus》
布局和菜单
复杂的UI需要很多控件。 这些控件可能需要使用设置策略(set policies),进行分组,定位并调整大小。 幸运的是,TornadoFX简化了JavaFX自带的许多布局(layouts),并且具有自己的专有Form
布局。
TornadoFX还具有类型安全的构建器(type-safe builders),以高度结构化,声明性的方式创建菜单。 使用常规JavaFX代码构建菜单尤其繁琐,而Kotlin在这个部分真的很出色。
布局构建器(Builders for Layouts)
布局(Layouts)将控制分组,并设置有关其大小和定位行为的策略(policies)。 在技术上,布局(layouts)本身就是控件,因此您可以在布局中嵌套布局。 这对于构建复杂的UI来说至关重要,而TornadoFX可以通过明显地显示嵌套关系来简化UI代码的维护。
VBox
VBox按照控件在其块中声明的顺序垂直堆叠控件(图7.1)。
vbox {
button("Button 1").setOnAction {
println("Button 1 Pressed")
}
button("Button 2").setOnAction {
println("Button 2 Pressed")
}
}
您还可以在子控件的块中调用vboxConstraints()
来更改VBox
的边距(margin)和垂直增长(vertical growing)行为。
vbox {
button("Button 1") {
vboxConstraints {
marginBottom = 20.0
vGrow = Priority.ALWAYS
}
}
button("Button 2")
}
您可以用vGrow
速记扩展属性(shorthand extension property),而无需调用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")
}
}
您还可以在子控件的块内调用hboxConstraints()
来更改HBox
的边距(margin)和横向增长(horizontal growing behaviors)行为。
hbox {
button("Button 1") {
hboxConstraints {
marginRight = 20.0
hGrow = Priority.ALWAYS
}
}
button("Button 2")
}
您可以使用hGrow
缩写扩展属性(shorthand extension property),而不调用hboxConstraints()
。
hbox {
button("Button 1") {
hGrow = Priority.ALWAYS
}
button("Button 2")
}
FlowPane
FlowPane
控件从左至右布局控件,并在到达边界时将其转到下一行。 例如,假设您添加了100个按钮到FlowPane
(图7.3)。你会注意到它只是从左到右布置按钮,当它耗尽空间时,它移动到“下一行”。
flowpane {
for (i in 1..100) {
button(i.toString()) {
setOnAction { println("You pressed button $i") }
}
}
}
请注意,当您调整窗口大小时, FlowLayout
将重新布局按钮,以使它们都可以适合(图7.4)
FlowLayout
不经常使用,因为处理大量控件通常是简单的,但它可以在某些情况下派上用场,也可以在其他布局中使用。
BorderPane
BorderPane
是一个非常有用的布局,将控件分为5个区域: top
, left
, bottom
, right
和center
。 可以使用这些区域的两个或更多来来保存控件,很容易地构建许多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
区域占据整个水平空间,而left
, center
, right
必须共享可用的水平空间。 但center
有权获得任何额外的可用空间(垂直和水平),使其成为像TableView
这样的大型控件的理想选择。 例如,您可以在left
区域中垂直堆叠一些按钮,并将TableView
放在center区域(图7.6)。
borderpane {
left = vbox {
button("REFRESH")
button("COMMIT")
}
center = tableview {
items = listOf(
Person("Joe Thompson", 33),
Person("Sam Smith", 29),
Person("Nancy Reams", 41)
).observable()
column("NAME",Person::name)
column("AGE",Person::age)
}
}
BorderPane
是您可能想要经常使用的布局,因为它简化了许多复杂的UI。top
区域通常用于保存MenuBar
, bottom
区域通常保持某种状态栏。 您已经看到center
保持焦点控制,如TableView
, left
和right
保持侧面板与任何不适合放在MenuBar
中的外围控件(如按钮或工具栏) 。 本节稍后将介绍菜单。
表单生成器
TornadoFX有一个有用的Form
控件来处理大量的用户输入。 拥有多个输入字段以获取用户信息是常见的,JavaFX没有内置的解决方案来简化此操作。 为了解决这个问题,TornadoFX有一个构建器来声明具有任意数量字段的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
将为您呈现分组和标签。
您也可以选择在输入字段之上布置标签:
fieldset("FieldSet", labelPosition = VERTICAL)
每个field
都包含一个内有标签的容器,另一个容器用于在其中添加的输入字段。 默认情况下,输入字段的容器是HBox
,这意味着单个字段中的多个输入将彼此水平相邻布置。 您可以指定一个字段的orientation
参数,使其在多个输入之间相互上下排列。 垂直取向的另一个用例是允许输入随着垂直方向的扩展而增长。 这对于在表单中显示TextAreas
非常方便:
form {
fieldset("Feedback Form", labelPosition = VERTICAL) {
field("Comment", VERTICAL) {
textarea {
prefRowCount = 5
vgrow = Priority.ALWAYS
}
}
buttonbar {
button("Send")
}
}
}
上面的示例还使用buttonbar
构建器创建一个没有标签的特殊字段,同时保留标签缩进,使按钮在输入框下排列。
您将每个输入绑定到一个模型(model),您可以将控件布局的渲染留给Form
。 因此,如果可能,您可能希望在GridPane
上使用它,接下来我们将介绍。
在Form
内嵌套布局(Nesting layouts inside a Form)
您可以使用您选择的任何布局容器来包装fieldets
和fields
,以创建复杂的表单布局。
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
会给你很多的。 当然,它需要更多的配置和代码样板。 在继续使用GridPane
之前,您可能需要考虑使用为您抽象了布局配置的Form
或其他布局。
使用GridPane
的一种方法是声明每row
的内容。 对于任何给定的Node
您可以调用其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
内分别为“North”和“South”按钮的marginBottom
和marginTop
声明了每行之间的距离为10.0 。
或者,您可以显式指定每个Node
的列/行索引位置,而不是声明每row
的控件。 这将完成我们之前建立的精确布局,但是使用列/行索引来规范。 它有点冗长,但它可以更加明确地控制控件的位置。
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
属性。 一些表示为可以赋值的简单属性,而其他属性可以通过函数赋值。
属性 | 描述 |
---|---|
columnIndex:Int | 给定控件的列索引 |
rowIndex:Int | 给定控件的行索引 |
columnRowIndex(columnIndex:Int,rowIndex:Int) | 指定行和列索引 |
columnSpan:Int | 控件占用的列数 |
rowSpan:Int | 控制占用的行数 |
hGrow:Priority | 水平增长优先 |
vGrow:Priority | 垂直成长优先 |
vhGrow:Priority | 为vGrow和hGrow指定相同的优先级 |
fillHeight:Boolean | 设置Node 是否填充其区域的高度 |
fillWidth:Boolean | 设置Node 是否填充其区域的宽度 |
fillHeightWidth:Boolean | 设置Node 是否填充高度和宽度的区域 |
hAlignment:HPos | 水平对齐政策 |
vAlignment:VPos | 垂直对齐策略 |
margin:Int | Node 所有四边的边距 |
marginBottom:Int | Node 底部的边距 |
marginTop:Int | Node 顶端的边距 |
marginLeft:Int | Node 左侧的左边距 |
marginRight:Int | Node 右侧的右边距 |
marginLeftRight:Int | Node 的右边距和左边距 |
marginTopBottom:Int | Node 的顶部和底部边距 |
另外,如果需要配置ColumnConstraints
,可以在GridPane
本身的GridPane Node
上调用gridpaneColumnConstraints
,也可以调用constraintsForColumn(columnIndex)
。
gridpane {
row {
button("Left") {
gridpaneColumnConstraints {
percentWidth = 25.0
}
}
button("Middle")
button("Right")
}
constraintsForColumn(1).percentWidth = 50.0
}
StackPane
一个StackPane
是一个布局,您将不太经常使用。 对于您添加的每个控件,它将逐字地堆叠在一起(literally stack them ),而不是像VBox
,但是字面上覆盖它们(literally overlay them)。
例如,您可以创建一个“BOTTOM” Button
并在其顶部放置一个“TOP” Button
。 您声明控件的顺序将以相同的顺序从底部到顶部添加它们(图7.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
创建一个用“tab”分隔的不同屏幕的UI。 这允许通过点击相应的选项卡快速轻松地切换不同的屏幕(图7.11)。 您可以声明一个tabpane()
,然后根据需要声明尽可能多的tab()
实例。 对于每个tab()
函数,通过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 {
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)
}
}
}
像许多构建器一样, TabPane
有几个属性可以调整其选项卡的行为。 例如,您可以调用tabClosingPolicy
来去掉选项卡上的“X”按钮,从而无法关闭。
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")
}
}
}
菜单构建器
以严格面向对象的方式构建菜单可能很麻烦。 但是使用类型安全的构建器,Kotlin的函数结构可以直观地声明嵌套的菜单层次结构。
MenuBar,Menu和MenuItem
使用导航菜单在用户界面上保留大量命令并不常见。 例如, BorderPane
的top
区域通常是MenuBar
所在的地方。 在那里可以轻松添加菜单和子菜单(图7.12)。
menubar {
menu("File") {
menu("Connect") {
item("Facebook")
item("Twitter")
}
item("Save")
item("Quit")
}
menu("Edit") {
item("Copy")
item("Paste")
}
}
您还可以选择提供键盘快捷键,图形以及每个item()
的action
函数参数,以指定选定操作时的动作(图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()
来创建一个分隔线。 这有助于给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中的大多数控件都有一个contextMenu
属性,您可以在其中指定ContextMenu
实例。 这是一个在右键单击控件时弹出的Menu
。
一个ContextMenu
有函数可以添加Menu
和MenuItem
实例,就像MenuBar
一样 。 例如,将一个ContextMenu
添加到TableView
是有帮助的,并提供要在表格记录上完成的命令(图7.16)。 有一个名为contextmenu
的构建器将构建一个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
块参数时,menuitem
构建器采取动作来执行。 不幸的是,这破坏了其他构建器,其中op
块对构建器创建的元素进行操作。 因此,引入item
构建器作为替代,您可以在item
本身上操作,因此您必须调用setOnAction
来赋值动作。menuitem
构建器没有被弃用,因为它以比item
构建器更简洁的方式解决了常见情况。
ListMenu
TornadoFX带有一个列表菜单(ListMenu
),其行为和看起来更像是一个典型的基于ul/li
的HTML5菜单。
以下代码示例显示如何使用构建器模式的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
构建器允许以非常方便的方式为ListMenu
创建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
属性将垂直填充父容器。 这些属性实际上适用于所有节点,但对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
传递给构建器构造函数,使SqueezeBox
仅允许在任何给定时间展开单个折叠。
您可以选择通过单击标题窗格右侧的十字架(clicking a cross in the right corner of the title pane)而允许折叠成为可关闭的(allow folds to be closable)。 您可以通过将closeable = true
传递给fold构建器,从而以每折为单位启用关闭按钮(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
有可关闭的折叠(closeable folds)。
closeable
属性当然可以结合expanded
。
SqueezeBox
和Accordion
之间的另一个重要区别就是分配空间(distributes overflowing space)的方式。 手风琴(Accordion
)将垂直延伸以填充其父容器,并将当前打开的任何折叠推至底部。 如果父容器非常大,这将创建一个不自然的查看视图。 在这方面,挤压框(SqueezeBox
)可能默认就是您想要的,但您可以添加fillHeight = true
以获得类似于Accordion
的外观。
您可以像您一样创建一个TitlePane
样式一样来创建SqueezeBox
样式。 关闭按钮有一个名为close-button
的css
类,容器有一个名为squeeze-box
的css
类。
Drawer
抽屉(Drawer)是一个非常像TabPane
的导航组件,但它在父容器的任一侧的垂直或水平放置的按钮栏中组织每个抽屉项目。 它类似于许多流行的业务应用程序和IDE中发现的工具抽屉(tool drawers)。 当选择项目时,项目的内容将显示在跨越控件的高度或宽度的内容区域中的按钮旁边或上方/下方,以及内容的首选宽度或高度,具体取决于是否将其停靠在父级的垂直或水平方面。 在多重选择(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)
}
抽屉可以配置为显示右侧的按钮,您可以选择同时支持打开多个抽屉物品。 当以多重选择模式运行时,内容上方会出现一个标题,这将有助于区分内容区域中的项目。 您可以使用布尔的showHeader
参数控制标题外观。 当启用多重选择时,它将默认为true,否则为false。
drawer(side = Side.RIGHT, multiselect = true) {
// Everything else is identical
}
带有右侧按钮的抽屉,多选模式和标题窗格。
当抽屉被添加到某物的旁边时,您可以选择抽屉的内容区域是否应替换其旁边的节点(默认)或浮动。 floatingContent
属性默认为false,导致Drawer
替换其旁边的内容。
您可以使用Drawer
的maxContentSize
和fixedContentSize
属性进一步控制内容区域的大小。 根据dockingSide
,这些属性将限制内容区域的宽度或高度。
Workspace
功能内置支持抽屉控件。 任何Workspace
的leftDrawer
, rightDrawer
和bottomDrawer
属性将允许您将抽屉项目bottomDrawer
在其中。 在“工作区(Workspace
)”一章中了解更多信息。
转换可观察列表项并绑定到布局(Converting observable list items and binding to layouts)
TODO
总结
到目前为止,您应该拥有能力可以使用布局,标签窗格以及其他控件来管理控件。 将这些与数据控件结合使用,您应该可以在一小部分时间内转换UI。
当涉及到构建器时,您已经达到了顶峰(top of the peak),并拥有所需要的所有成效。 剩下的所有内容都是图表和形状(charts and shapes),我们将在接下来的两章中介绍。