JavaFX七巧板游戏:布局控件

用于布局的控件

JavaFX七巧板游戏:布局入门到放弃一文把用于布局的窗格(Pane)拉了一遍,里面提到有一些控件,也有布局的作用。

明显,控件与窗格一样,是Region的子类;与窗格不同的是,控件有自己的UI元素。

下图是控件的子类结构,在右边的框中的类,就是用于布局的控件:

  • ToolBar
  • ButtonBar
  • SplitPane
  • Pagination
  • Accordion
  • ScrollPane(为啥打个 × \times ×呢,因为开头我觉得可以不用显式调用这个,后来发现自己太年轻了)
  • TabPane
  • TitledPane
    控件类图
    这里,我想用一个小界面演示程序,把这几个类展示一下。

代码在:gitcode库,可以直接git clone之后,用IDEA或者Gradle run运行。

程序的变态样子

因为这个程序仅作演示用,所有点变态……我自己都被自己吓坏了……

略显变态的一点东西

源代码

整个程序的源代码如下。我们从后至前(看程序就应该从后至前,从抽象到具体……从无聊到有趣,千万不要按顺序读……)简单讲讲,要搞清楚用户界面,还是应该运行起来,点一点,再次强调gitcode库

package org.cardc

import javafx.application.Application
import javafx.geometry.Insets
import javafx.geometry.Side
import javafx.scene.Scene
import javafx.scene.control.*
import javafx.scene.layout.BorderPane
import javafx.scene.layout.Pane
import javafx.scene.layout.Priority
import javafx.scene.layout.VBox
import javafx.scene.paint.Color
import javafx.scene.shape.Circle
import javafx.stage.Stage
import kotlin.math.sqrt
import kotlin.reflect.KVisibility

fun getPane(color: String) = Pane().apply {
    style = "-fx-background-color: $color;"
    setPrefSize(200.0, 200.0)
    children.addAll(
        Circle(100.0, 50.0, 50.0, Color.RED),
        Circle(50.0, 50 * (1 + sqrt(3.0)), 50.0, Color.YELLOW),
        Circle(150.0, 50 * (1 + sqrt(3.0)), 50.0, Color.BLUE)
    )
}

fun javafxColors() =
    Color::class.members.filter { it.isFinal && it.visibility == KVisibility.PUBLIC && it.parameters.isEmpty() }
        .map { it.name.lowercase() }

val colors = javafxColors().sorted().reversed().toTypedArray()

fun panes() = colors.map { getPane(it) }.toTypedArray()

fun buttonData() = ButtonBar.ButtonData.values().sorted().map {
    Button(it.name).apply {
        ButtonBar.setButtonData(this, it)
    }
}.toTypedArray()

fun toolBarDemo() = ToolBar(
    *buttonData()
)


fun buttonBarDemo() = ButtonBar().apply {
    buttons.addAll(buttonData())
}

fun splitPaneDemo() = SplitPane().apply {
    val panes = panes()
    items.addAll(panes[0], panes[1], panes[2])
    setDividerPositions(0.3, 0.8)
}

fun paginationDemo() = Pagination(panes().size, 0).apply {
    setPageFactory {
        panes()[it]
    }
}

fun accordionDemo() = Accordion().apply {
    panes.addAll(panes().mapIndexed { i, p ->
        TitledPane("TitledPane: ${colors[i]}", p)
    })
}

fun tabPaneDemo() = TabPane().apply {
    side = Side.BOTTOM
    tabClosingPolicy = TabPane.TabClosingPolicy.UNAVAILABLE
    tabs.addAll(panes().mapIndexed { i, p ->
        Tab("Tab: ${colors[i]}", p)
    })
}

class ControlsLearningApplication : Application() {
    override fun start(primaryStage: Stage) {
        primaryStage.scene = Scene(
            BorderPane().apply {
                padding = Insets(2.0)
                top = toolBarDemo()
                center = VBox().apply {
                    padding = Insets(10.0)
                    children.addAll(splitPaneDemo().apply {
                        VBox.setVgrow(this, Priority.ALWAYS)
                    }, tabPaneDemo())

                }
                bottom = buttonBarDemo().apply { padding = Insets(10.0) }
                left = paginationDemo().apply { padding= Insets(10.0) }
                right = ScrollPane(accordionDemo()).apply { padding = Insets(10.0) }
            }, 1440.0, 900.0
        )
        primaryStage.title = "Controls that layout children..."
        primaryStage.show()
    }
}

fun main() {
    Application.launch(ControlsLearningApplication::class.java)
}

程序界面主体

程序主体,入口函数main,调用ControlsLearningApplication。这个Application的子类重载了start方法,构造一个Scene,这个剧场的根节点是一个BorderPane(参考:JavaFX七巧板游戏:布局入门到放弃)。

class ControlsLearningApplication : Application() {
    override fun start(primaryStage: Stage) {
        primaryStage.scene = Scene(
            BorderPane().apply {
                padding = Insets(2.0)
                top = toolBarDemo()
                center = VBox().apply {
                    padding = Insets(10.0)
                    children.addAll(splitPaneDemo().apply {
                        VBox.setVgrow(this, Priority.ALWAYS)
                    }, tabPaneDemo())

                }
                bottom = buttonBarDemo().apply { padding = Insets(10.0) }
                left = paginationDemo().apply { padding= Insets(10.0) }
                right = ScrollPane(accordionDemo()).apply { padding = Insets(10.0) }
            }, 1440.0, 900.0
        )
        primaryStage.title = "Controls that layout children..."
        primaryStage.show()
    }
}

fun main() {
    Application.launch(ControlsLearningApplication::class.java)
}

所以主界面的五个部分(我是讲真话的好程序员:只会布局三板斧:BorderPane, VBox, HBox):

  • top:toolBarDemo
  • bottom: buttonBarDemo
  • left: paginationDemo
  • right: ScrollPane包一个accordionDemo
  • center: VBoxsplitPaneDemotabPaneDemo

包含了前面提到的,用于界面布局的所有控件。

ToolBarButtonBar

这两货都是Bar,最终体现在UI上,就是一条。但是这两的目的和布局方式都不一样。

ToolBar是一个水平或者垂直展示items的控件。最常见的items包括Button,ToggleButtonSeparator,但是也不仅仅包括这三类,只要是Node都可以加入。

如果items的数目过多,就会显示一个额外的按钮可以找到被隐藏的部分。

ButtonBar本质上是一个HBox,这个控件试图解决的是操作系统特定的按钮顺序问题。通过buttonsNode对象放到这个控件上,可以通过setButtonData(Node, ButtonData)来设定标记,按钮之间的相对顺序就会被自动排列……这个功能我没看到有多大用处……

fun toolBarDemo() = ToolBar(
    *buttonData()
)
fun buttonBarDemo() = ButtonBar().apply {
    buttons.addAll(buttonData())
}

这两个函数非常简单,一个调用构造函数,把一个数组展开作为参数;另外一个调用buttons.addAll来增加。这样就需要看看buttonData函数:

fun buttonData() = ButtonBar.ButtonData.values().sorted().map {
    Button(it.name).apply {
        ButtonBar.setButtonData(this, it)
    }
}.toTypedArray()

这个函数把,ButtonData所有的值遍历排序,用名字作为按钮的标签,把标识设好,最后把按钮列表转换成按钮数组。这是Kotlin的典型操作方式,不要自己去用循环,直接用函数参数来做。

从界面上看,ToolBar中的按钮大小不一,ButtonBar的按钮默认尺寸相同,但是如果窗口宽度变小后,前者显示一个扩展按钮,后者的按钮直接被遮挡。

PaginationTabPane


fun paginationDemo() = Pagination(panes().size, 0).apply {
    setPageFactory {
        panes()[it]
    }
}
fun tabPaneDemo() = TabPane().apply {
    side = Side.BOTTOM
    tabClosingPolicy = TabPane.TabClosingPolicy.UNAVAILABLE //是否可以关闭标签。
    tabs.addAll(panes().mapIndexed { i, p ->
        Tab("Tab: ${colors[i]}", p)
    })
}

这两个东西很像,都是提供一个导航来访问一系列页面。前者页面的编号是数字,自动生成一个前后导航的UI元素;后者更好理解,就是标签,自己设置标签名称。

Pagination的机制是采用工厂方法,对应每一个标签编号,选择对应的内容。

TabPane采用tabs属性管理,可以增加和删除标签,甚至可以绑定属性,自动更新标签,这个实际上是一个很重的控件,能够实现很复杂的功能。

Pagination
上面是Pagination,下面是TabPane
TabPane

SplitPane

这个控件是一个分栏的布局形式。

fun splitPaneDemo() = SplitPane().apply {
    val panes = panes()
    items.addAll(panes[0], panes[1], panes[2])
    setDividerPositions(0.3, 0.8)
}

增加items,设置初始的分界位置,0.0到1.0之间。比较简单。

Accordion

这是一个折叠标签控件。

fun accordionDemo() = Accordion().apply {
    panes.addAll(panes().mapIndexed { i, p ->
        TitledPane("TitledPane: ${colors[i]}", p)
    })
}

这里可以看到,panes必须放TitledPane,这个TitledPane实际上一个Labeled控件……标签的名字显示在折叠标签中,每次只能打开一个。

折叠标签

ScrollPane

这个控件多半是自动的……。我们这个程序的中就把Accordion放在一个`ScrollPane``中。它会自动在需要的时候增加滚动条。

right = ScrollPane(accordionDemo()).apply { padding = Insets(10.0) }

panes函数、colors及其他

往上看程序,最后就剩下几个函数,功能都比较简单,但是使用了kotlin之后就特别美好……

// 设置背景颜色,绘制三原色的三个圆形
fun getPane(color: String) = Pane().apply {
    style = "-fx-background-color: $color;"
    setPrefSize(200.0, 200.0)
    children.addAll(
        Circle(100.0, 50.0, 50.0, Color.RED),
        Circle(50.0, 50 * (1 + sqrt(3.0)), 50.0, Color.YELLOW),
        Circle(150.0, 50 * (1 + sqrt(3.0)), 50.0, Color.BLUE)
    )
}
// 把JavaFX定义的所有有名字的颜色找出来
fun javafxColors() =
    Color::class.members.filter { it.isFinal && it.visibility == KVisibility.PUBLIC && it.parameters.isEmpty() }
        .map { it.name.lowercase() }
// 排序,转换为数组
val colors = javafxColors().sorted().reversed().toTypedArray()
// 每一个颜色产生一个Pane
fun panes() = colors.map { getPane(it) }.toTypedArray()

尾声

基本上,用于布局的控件就这么几个。结合JavaFX七巧板游戏:布局入门到放弃中介绍的用于布局的窗格,JavaFX的七巧板游戏就是这么多内容,组合来组合去,就能构成任意复杂的界面。

posted @ 2022-07-06 17:16  大福是小强  阅读(27)  评论(0编辑  收藏  举报  来源