This the multi-page printable view of this section. Click here to print.
JavaFX
- 1: JavaFX Layouts
- 2: JavaFX Property
- 3: JavaFX Style
- 4: JavaFX Controls
- 5: JavaFX Responsive
- 6: JavaFX TextEditor
- 7: JavaFX Background Task
- 8: JavaFX Animation
- 9: JavaFX Events
- 10: JavaFX Refresh Scene
- 11: JavaFX TableView
- 12: JavaFX REST
- 13: JavaFX New Window
- 14: JavaFX Button Events
- 15: JavaFX Canvas Vs Pane
- 16: JavaFX GridPane Nodes
- 17: JavaFX Triangles
- 18: JavaFX Canvas
- 19: JavaFX Charts
- 20: JavaFX Drag Shapes
- 21: JavaFX TableView Filter
- 22: JavaFX TableView Style
- 23: JavaFX TableView SVG
- 24: JavaFX All Buttons
1 - JavaFX Layouts
JavaFX 中的布局容器提供了各种内置功能,用于设置组件节点的位置、大小、对齐。
一共提供了 9 种默认的布局容器,包括基类 Pane
,该类仅用于设置位置。
- Pane
- AnchorPane
- BorderPane
- GridPane
- StackPane
- TilePane
- HBox
- VBox
另外一些布局容器用于支持用户控制查看节点的视窗,而非布局:
- ScrollPane:支持滚动或平移窗格。
- TabPane:通过选择 Tab 来查看不同的窗格。
- SplitPane:通过移动分隔符来改变两个可见窗格的比例。
布局的选择取决于两个关键因素:
- 你想要你的节点所处的位置(布局类型)
- 布局的方式将控制其子元素的位置、大小和对齐方式
选择布局之后,基于布局功能设置节点的位置、大小、对齐。
布局类型
可以将布局分为 3 个大类:块(block)、网格(grid)、行列(row/column)。
Block 布局
块布局是最简单的,只有一组规则来定义其子元素的布局。我称它们为“块”布局,因为它们不会将节点划分为网格(网格布局)或行列(行/列布局)。
也就是说,从布局逻辑的角度来看,窗格是单个块。
- StackPane:节点位置的设置由 对齐 设置来控制。
- AnchorPane:阶段位置的设置由 ”绝对位置“ 设置来控制。
- 绝对位置表示可以同时控制位置和大小,StackPane 则仅能控制位置。
Grid 布局
网格布局是最复杂的,可以非常详细地控制节点的位置。
- TilePane:最简单,但也提供了最少的控制能力。
- BorderPane:支持对定位的粗略控制,但在默认情况下也保证了确定行为。这对于默认窗口行为用例来说是一个很好的平衡。
- GridPane:支持非常精细的控制节点位置,但使用起来可能很困难和复杂。
Row/Column 布局
以顺序方式排列节点,横向或纵向。
其中每种布局都可以控制序列中节点之间的间距。对于 FlowPane,用户还可以在多行和多列设置中控制行或列之间的空间。
选择布局
基于控制子元素的方式来选择需要的布局。
窗格用于以各种方式控制子元素的位置、大小和对齐。这将影响面板调整大小时它如何移动和调整节点的大小。
值得记住的是,窗口经常被用户调整大小,比你默认设定的的要小或大。
以下是各种布局如何控制子节点:
Pane | Layout Positions Children | Resizes Children | Aligns Children | Has sub-regions | Children can overlap |
---|---|---|---|---|---|
AnchorPane | ✔ | ✔ | ✔ | ||
StackPane | ✔ | ✔ | |||
BorderPane | ✔ | ✔ | ✔ | ✔ | |
GridPane | ✔ | ✔ | (virtual) | ||
TilePane | ✔ | ✔ | (virtual) | ||
FlowPane | ✔ | ✔ | |||
HBox | ✔ | ✔ | ✔ | ||
VBox | ✔ | ✔ | ✔ |
使用布局的一个常见问题是试图调整控件的大小。如果你在调整控件的布局时遇到了麻烦,比如按钮,可以查看下拉菜单来修复它。
深入布局
Pane
类是所有布局的基类,不会对其子布局应用任何布局行为。因此,当您需要绝对定位一个节点,但又不希望有任何额外的调整大小行为或对齐时,Pane 就够用了。
每个布局窗格扩展了 Pane 类,并在窗格中定义的核心行为之上提供了附加的功能层。布局窗格没有任何魔法:它们通过根据一组特定于它们试图创建的布局的规则手动计算每个子节点的 layoutX 和 layoutty 属性来实现它们想要的布局行为。
AnchorPane
- 区域是一个单独的块,任何节点都可以在上面定位
- 在节点上设置的锚点将被应用,覆盖任何现有的最大或最小属性值
- 重叠节点从 observableelist 子节点的开始呈现到结束,这意味着在默认情况下,后添加的项最后呈现
应用方法
除了使用 layoutX 和 layoutY 进行绝对定位之外,AnchorPane 还有四个方法来定义它的子组件的布局。
setTopAnchor(Double double)
setBottomAnchor(Double value)
setRightAnchor(Double value)
setLeftAnchor(Double value)
这些方法定义了子元素的边缘相对于 AnchorPane 的指定边缘的绝对位置。
这些方法可以同时用于设置位置和大小。
要更改重叠节点的呈现顺序,请使用 getChildren()
获取子节点列表,并更改顺序。
设置位置
在一个节点上设置一个或两个锚点将会影响到根据 AnchorPane 的布局边界来定位该节点。
将锚点设置为 0 将把节点的指定一侧粘到 AnchorPane 的同一侧。
注意:锚设置方法接受双精度值,可能为负值,但可能没有您想象的效果。
如果我尝试设置一个负的左锚,我可能期望节点看起来像是从 AnchorPane 的左边缘滑出。相反,AnchorPane 尝试调整自己的大小以适应节点。
如果是为上部的节点设置一个负锚点,看起来仍然像粘在面板的左边。而其他的节点看起来只是向右移动了。
如果想要成功实现从一个 AnchorPane 的左边滑出的预期效果,你需要将滑动和剪切结合起来(sliding with clipping)。
设置大小
在一个节点上设置三个锚,将会有拉伸两个相反的锚之间的节点的效果。
这可能非常有用,因为它会覆盖节点上的任何最大或最小宽度或高度。通过使用所有四个锚点,可以在两个方向上拉伸节点。
StackPane
StackPane 是一个简单的布局窗格,类似于 AnchorPane。这两个窗格都是相对简单的布局,支持呈现相互重叠的节点。但是,AnchorPane 专注于绝对定位,而 StackPane 专注于基于对齐的节点定位。
应用方法
为了充分利用 StackPane,我通常先对对齐进行排序,然后再进行定位,所以在本例中,我将先进行对齐,然后再进行定位。
StackPane 也不会调整节点的大小。与锚窗格不同的是,锚窗格可以通过在子节点边界上强制绝对定位来拉伸节点,而 StackPane 专注于对齐,这意味着它不会调整任何节点的大小。
设置对齐
默认情况下,所有节点都在 StackPane 的中心对齐。
要更改节点的默认对齐方式,需要在相关的 StackPane 上使用 setAlignment(Pos value)
。
为了使节点准确地位于您想要的位置,您可能需要使用 translateX 和 translateY 属性来修改对象的位置。事实上,当我开始使用 StackPanes 时,我经常将侦听器挂接到可以调整节点翻译属性的 StackPane 宽度上。
停! 用边距(margin)代替。
要调整节点位置,可以使用静态方法 StackPane.setMargin(Node child, Insets insets)
,引用要对齐的节点和需要的对齐方式。
设置位置
StackPane 不支持节点的绝对定位,任何 layoutX 和 layoutY 的值都会被 StackPane 忽略。通过逐个节点设置边距,或更改节点的 translateX 和 translateY 值,可以对节点的位置进行微调。
Margins:边距
我认为使用 StackPane 最安全的方法是使用边距,而不是手动修改对象的 translate 属性。在本例中,您可以通过调用静态方法 StackPane.setMargin(Node node, Insets margin)
,在每个节点周围手动创建一个透明的边距。
手动修改
您可以使用 translateX 和 translateY 属性来调整子节点的位置。这对于添加或删除节点的动画或过渡特别有用。
这些可能非常有用,但请记住,它们是在计算完 StackPane 的布局边界之后应用的,所以带有修改的 translate 属性的节点可能会在 StackPane 的边界之外呈现。
注意:当一个 translate 属性被修改时,它将在所有对齐和插入之后被应用,所以在计算修改这些属性的数量时,请记住要考虑这些因素。
BorderPane
BorderPane 被设计为具有固定高度的顶部和底部区域,以及固定宽度的左右区域。其效果是,随着 BorderPane 改变尺寸,中心的大小也会变化。
应用方式
其应用方式包含三个部分:位置、大小、对齐。
设置位置
与我们迄今为止看到的窗格不同,这些窗格使用 getChildren()
方法访问节点的子节点,这与 BorderPane 不兼容。
由于设计意图以特定的方式托管子节点,我们需要告诉 BorderPane 要将节点添加到哪个区域。在 BorderPane 中设置节点的五种方法是:
- Centre:
setCenter(Node node)
- Left:
setLeft(Node node)
- Right:
setRight(Node node)
- Top:
setTop(Node node)
- Bottom:
setBottom(Node node)
在 BorderPane 的每个区域中只能存储一个节点,因此多次调用这些方法将删除原始节点,并用最近的赋值替换它。
设置大小
BorderPane 的大小调整行为是它特别适合于标准窗口布局的原因之一。它的设计是为了适应固定高度但可变宽度的菜单栏和固定宽度但可变高度的导航窗口。
由于这种确切的设计意图,BorderPane 对节点大小的影响取决于节点在 BorderPane 中的位置。以下是关于 BorderPane 如何调整其子节点大小的细节:
Top、Bottom
- Height:顶部和底部窗格中的节点固定在它们的首选高度。
- Width:顶部和底部区域节点的宽度根据 BorderPane 的宽度在其可调整大小的范围内进行调整。
- 如果要求 BorderPane 缩小到顶部/底部节点的最小宽度以下,它们仍然忠实地以最小大小呈现。
Left、Right
- Height:左右区域节点的高度根据 BorderPane 的高度在其可调整大小的范围内进行调整。计算结果为 BorderPane 的高度减去顶部和底部区域的高度。
- 如果要求 BorderPane 缩小到左/右节点的最小高度以下,它们仍然忠实地以最小大小呈现。
- Width:左右窗格中的节点固定在它们的首选宽度上。
Center
- Height:中心区域的宽度计算为 BorderPane 的宽度减去左右节点的首选宽度之和。
- Width:中心区域的高度计算为边框窗格的高度减去顶部和底部节点的首选高度之和。
如果要求 BorderPane 收缩到中心节点的最小高度或宽度以下,它仍将忠实地呈现在其最小大小。
设置对齐
在以下情况下,BorderPane 将在其区域的节点周围创建空间:
- BoderPane 的宽度大于顶部或底部节点的最大宽度。
- BorderPane 的高度大于左右节点的最大高度。
- 中间剩余空间大于中心节点的最大尺寸。
如果任何 BorderPane 区域的大小大于其包含节点的最大大小,默认情况下它将按照以下规则进行定位:
- Top/Bottom/Left/Right:Top-Left 对齐。(记住,对于 Top 和 Bottom,区域的大小为 preheight,所以垂直对齐不会被注意到,左/右和水平对齐也是如此。
- Center:中心对齐。
可以使用静态方法 BorderPane.setAlignment(Node node, Pos alignment)
设置 BorderPane 区域内任何节点的对齐方式。或者 FXML 文件中使用 BorderPane.alignment="<alignment>"
。
GridPane
GridPane 布局为网格布局中的节点提供了最好的控制级别。这包括为对齐和调整大小设置特定于列和行属性的功能,以及特定于节点的跨行和跨列行为。
应用方式
GridPane 是 JavaFX 最复杂的默认布局,有多层的位置和大小控制。
几乎所有这些都涉及到创建 RowConstraints 或 ColumnConstraints 对象。涉及的方法为:getColumnConstraints().add(ColumnConstraints constraint)
或 getRowConstraints().add(RowConstraints constraint)
。
约束按添加到 GridPane 的顺序设置到列和行。向 GridPane 中添加空约束(如 new ColumnConstraints()
)将产生不设置任何约束的效果。如果您想要跳过行或列,这是有用的。
设置位置
在 GridPane 中定位内容可以通过四种方式实现:
设置放置节点的单元格
更改列或行的大小
将节点设置为跨多个列或行
更改列和行之间的填充
1. 设置放置节点的单元格
在最粗糙的层次上,通过在网格中设置节点的位置来实现节点的定位。这可以通过同时设置行位置、列位置或两者来实现。
//set both positions simultaneously
GridPane.setConstraints(myNode, 0, 1);
//set column position
GridPane.setColumnIndex(myNode, 0);
//set row position
GridPane.setRowIndex(myNode, 1);
2. 更改列或行的大小
默认情况下,列和行大小将根据内容的首选宽度和高度计算。可以通过创建 ColumnConstraints 和 RowConstraints 对象重写此行为,并将它们应用到 GridPane。
使用 RowConstraints 对象,可以使用 setPrefHeight(Double value)
指定行的高度为像素尺寸,或者使用setPercentHeight(Double value)
指定的网格窗格总高度的百分比。
ColumnConstraints 对象也有类似的方法。
3. 将节点设置为跨多个列或行
可以通过指示 GridPane 将节点设置为跨多列或多行来更改节点在网格中的位置。
通过静态方法来设置:GridPane.setRowSpan(Node child, Integer value)
、GridPane.setColumnSpan(Node child, Integer value)
。
4. 更改列和行之间的填充
最后,可以通过更改 GridPane 的行和列之间的填充来调整节点的位置。
GridPane 边缘的普通填充可以使用 setHgap(double value)
和 setVgap(double value)
,其他地方则可以使用 setPadding(Insets value)
来设置。
设置大小
可以通过多种方式成功地调整节点和列的大小。这两种方法都需要创建 RowConstraints 和 ColumnConstraints 对象。
扩展行和列以填充未占用的空间
垂直和水平地将节点扩展到行或列中的可用空间
默认情况下,GridPane 将以节点的首选大小容纳节点,并且不会将节点拉伸以填充列。它还将把所有列和行相等地展开到可用空间中,因此这对于调优位置行为很有用。
1. 扩展行和列以填充未占用的空间
可以通过创建一个 RowConstraints 对象并调用 setVgrow(true)
来指示一行填充任何未被占用的空间。
同样,可以通过创建 ColumnConstraints 对象并调用 setHgrow(true)
来指示列填充任何未占用的空间。
空间通过以下逻辑分布在行和列之间:
- 总是在所有具有
Priority.ALWAYS
优先级的行或列之间均匀地分配空间。 - 如果没有列/行具有
Priority.ALWAYS
优先级。如果仍然存在剩余空间,则将剩余空间分配到带有Priority.SOMETIMES
优先级的行或列之间。
2. 垂直和水平地将节点扩展到行或列中的可用空间
通过创建一个 RowConstraints 对象(或修改一个现有的对象),并调用 setFillHeight (true)
,可以指示一行将所有节点垂直增长到未占用的空间。
同样以类似的方式,可以通过创建 ColumnConstraints 对象(或修改现有的对象)并调用 setFillWidth (true)
来指示列填充任何未被占用的水平空间。
设置对齐
最后,可以指示行内的节点垂直对齐(以及列内的节点水平对齐)。这些是使用 HPos 和 VPos 枚举设置的。
最后,可以通过创建 RowConstraints 对象(或修改现有对象)并调用 setValignment (VPos value)
来指示一行垂直对齐节点。
通过创建 ColumnConstraints 对象(或修改现有对象)并调用 setHalignment (HPos value)
,可以指示列水平对齐节点。
TilePane
TilePane 通过在一维中添加 tiles(节点在其中呈现的虚拟区域),然后将 tiles 包装到其他行或列来计算其布局。tiles 在整个窗格中大小一致。
应用方式
可以通过调整 tile 之间的填充或设置特定于节点的边距来改变 tile 中节点的位置、大小和对齐方式。
设置位置
节点之间的距离可以通过设置包含节点的 TilePane 实例的 hgap 和 vgap 属性来调整。
Tile 的大小也可以作为一种改变节点位置的方法,尽管用户应该知道这也会改变节点的大小(在其可调整大小的范围内)。
Tile 的大小可以通过调用 setPrefTileWidth(double value)
和 setPrefTileHeight(double value)
来改变。这些大小不能保证,如果 TilePane 的大小减小到不能容纳这些瓷砖的程度,那么它们的大小就会减小。
设置大小
与许多其他布局不同,TilePane 在默认情况下会尝试在其可调整大小范围内拉伸小于 tile 大小的 tile 实例。
如果 tileprefWidth
和 tilePrefHeight
属性小于节点的首选尺寸,它也会尝试在可调整大小的范围内缩小较大的节点。
设置对齐
如果默认的 tile 大小大于任何节点的最大大小,节点将根据它设置的对齐方式在 tile 中分布。
使用静态方法 TilePane.setAlignment(Pos value)
在每个节点的基础上设置对齐。默认的对齐方式是 Pos.CENTER
,但如果您使用静态方法 TilePane.getAlignment(Node node)
在没有显式设置的节点上,它将返回 null(对于其他窗格也是如此)。
FlowPane
FlowPane 的设计是为了给你的布局一个响应式的感觉,改变行或列的长度,以适应流窗格本身的大小。对于外观相似但需要重新洗牌的物品来说,这是非常棒的。
应用方式
默认情况下,FlowPane 会将节点填充到行中,然后在必要时生成额外的行。对于生成内容列而不是行的流窗格,创建一个流窗格并调用 setOrientation(Orientation.VERTICAL)
。
流窗格不会尝试调整节点的大小,所以这里没有调整大小的部分。
设置位置
因为 FlowPane 可以用来将项目打包成行或列,所以它在内部将它们称为“runs”。如果方向是水平的,则运行是水平的。如果它是垂直的,runs 就是垂直的。
FlowPanes 的基本规则是:在下一个节点超过最大允许长度之前尽可能长地运行一次,然后将该节点作为下一次运行的第一项。
你可以通过调用 setMargin(Insets Insets)
来设置 FlowPane 的边距,在内容周围创建一个空间,它不会试图填充节点。它使用 Insets 对象,该对象可以分别或同时指定顶部、底部、左侧和右侧的边距。
您可以使用 setAlignment(Pos value)
来确定 FlowPane 将分配的剩余空间的位置。上面的图像显示了一个垂直的流窗格,对齐方式为 Pos.TOP_LEFT
,这意味着空间分布在底部和右侧。
Run 的长度
您可以通过调用 setPrefWrapLength(Double value)
来控制 run 的长度,无论您设置的方向如何,它都可以工作。
垂直与水平间隙
当设置间隙时,FlowPane 不区分方向,所以它们是绝对的。这意味着需要使用 setVgap(Double value)
设置垂直元素之间的间隙,使用 setHgap(Double value)
设置水平元素之间的间隙。
如果您希望功能区分运行和运行中的项目,那么开始切换方向时可能会感到困惑。
run 中元素的间隙
对于水平 FlowPane(默认值),使用 setVgap()
,对于垂直流窗格,使用 setHgap()
。
run 之间的间隙
对于水平 FlowPane(默认值),使用 setHgap()
,对于垂直流窗格,使用 setVgap()
。
设置对齐
如果 FlowPane 中包含不同大小的节点,则流窗格将运行以容纳该运行中的最大成员。这意味着如果它是一个垂直的流窗格,它将使运行足够宽的最宽成员。如果它是一个水平的 FlowPane,它将使运行足够高的最高成员。
令人沮丧的是,FlowPane 要求您分别设置列(垂直方向)和行(水平方向)的对齐方式。
- 水平
FlowPane
带有行:setRowValignment(VPos value).
VPos 枚举值为TOP
,BOTTOM
,BASELINE
andCENTER
。 - 垂直
FlowPane
带有列:setColumnHalignment(HPos value).
HPos 枚举值为LEFT
,CENTER
andRIGHT
。
这是一个垂直方向的 FlowPane,使用 HPos.LEFT
设置列对齐。
HBox
HBox 从左到右水平定位节点。可以设置节点大小和对齐首选项,而元素的顺序可以通过改变 ObservableList 子元素的顺序来改变。
应用方式
与FlowPane不同,HBox 只有一组水平节点。然而,她们对孩子的体型有更多的控制权。
设置位置
子节点的水平顺序是从左到右的,与 ObservableList 中项目的顺序相同。默认情况下,节点的顺序是添加子节点的顺序。
设置大小
可以在水平方向和垂直方向上对 HBox 进行调整。
垂直方向
默认情况下,HBox 将垂直调整其子节点的最大允许高度(由 HBox 本身的高度及其最大高度属性限制)。这个功能可以通过调用 setFillHeight(false)
来关闭。
这是为 HBox 实例设置的,并将应用于该 HBox 中的所有节点。
水平方向
节点可以请求按节点逐个调整大小,以填充任何额外的水平空间。
节点可以通过调用静态方法 Hbox.setHgrow(Node node, Priority value)
来请求填充 HBox 内部任何剩余的水平空间。
Priority 枚举值为 SOMETIMES、ALWAYS 和 NEVER 。空间的分布方式如下:
- 总是在所有具有优先级的节点之间均匀分配空间。
- 如果没有节点具有优先级。如果总是有剩余空间,则在节点之间优先分配剩余空间。
- 根据 HBox 的对齐方式分配剩余空间。
设置对齐
你可以通过调用 setAlignment(Pos value)
来设置 HBox 的对齐方式。
VBox
VBox 从上到下垂直地定位节点。我将简单介绍一下,但如果您想了解更详细的功能指南,可以参考 HBox 的部分,内部逻辑是相同的,但是是水平的而不是垂直的。
应用方式
VBox 有一组垂直节点。就像 HBox 一样,比垂直的 FlowPane 对其子节点的大小有更多的控制。
设置大小
可以水平和垂直地对VBox进行调整。
水平设置
要停止 VBox 水平拉伸其子元素,使用 setFillWidth(false)
。这是为 VBox 实例设置的,并将应用于该 VBox 中的所有节点。
垂直设置
通过通过方法 Vbox.setHgrow(Node node, Priority value)
请求节点伸展以适应 VBox 中任何剩余的垂直空间。空间的分布规则与 HBox 相同(但垂直分布……)
设置对齐
可以通过方法 setAlignment(Pos value)
设置对齐方式。
总结
JavaFX 有 9 个预定义的容器,提供了定义图表、图像和控件等节点的位置、大小和对齐方式的功能。JavaFX 的布局可以实现各种各样的功能和位置控制。
有三种基本的布局类型:网格、行列和块。它们定义了布局“思考”节点定位的方式。先画出容器的草图,然后用它来决定你需要的布局类型,这通常是一个很好的开始。
一旦你选择了你想让你的面板看起来的基本方式(网格,行列或块),我建议你选择你的布局基于他们如何响应被调整大小。除非你明确地将它们设置为固定大小,否则用户总是会发现一些漏洞,使窗口变得非常大(或非常小)。
2 - JavaFX Property
使用 JavaFX 的好处之一是它对 Property 和 Binding 的强大而全面的支持。除此之外,Property 还允许你连接场景,这样当你修改它后面的数据时,视图就会自动更新。
属性是可写或只读的可观察对象。在 JavaFX 中有 30 种类型的 Property 对象,包括 StringProperty、SimpleListProperty 和 ReadOnlyObjectProperty。每个属性都包装了一个现有的 Java 对象,添加了监听和绑定的功能。
以 SimpleDoubleProperty
为例,它集成了 JavaFX 中 binding、property 和 value 的各种重要的 package,每个 package 都增加了JavaFX 属性所展示的最终功能的一个方面。
除了属性功能之外,JavaFX 还通过 Binding 和 Expression 对象提供绑定值的功能。
Binding 是一种强制对象之间关系的机制,其中一个或多个可观察对象被用来更新另一个对象的值。Binding 可以是一个方向,也可以是两个方向,可以直接从属性(Fluent API)或使用 Bindings 实用程序类(Bindings API)创建。
如果需要额外的自定义或性能,也可以手动创建自定义绑定对象。这被称为低级 API。
主要内容
Property 和 Binding 是一组接口和类,旨在大大简化开发人员的工作。也就是说,在 Bindings 类中有 61个 属性和 249 个方法,因此很难管理。
本文将介绍:
- 什么是属性,哪些属性是可用的
- 如何观察一个属性值
- 什么是约束?
- 如何绑定属性
- 中间绑定技术(Fluent API 和 Bindings API)
- 高阶绑定技术(低级 API)
JavaFX 早期的许多问题,比如当你改变某些东西时,场景不能自动更新,这是因为场景与属性的错误连接。JavaFX 场景旨在基于属性和事件进行更新。
如果您想加深对 JavaFX 如何工作的理解,也可以查看关于事件的全面指南。这两篇文章结合起来将为 JavaFX 的后台工作提供一个真正坚实的基础。
什么是 Property
如果你像我一样,并且没有计算机科学背景,那么属性一开始似乎很吓人。不过,引擎盖下面并没有什么魔法。大多数 JavaFX Property 对象扩展了两个关键接口:ReadOnlyProperty<T>
和 WriteableValue<T>
。
Some of them don’t, though. JavaFX has 10 read-only properties, which extend ReadOnlyProperty<T>
, but don’t extend WriteableValue<T>
.
JavaFX 有 10 个只读属性,扩展了 ReadOnlyProperty<T>
,但没有扩展 WriteableValue<T>
。
创建 Property
JavaFX 提供了十个内置类,使创建属性变得非常容易。它们实现了所有必需的功能,从监听到绑定。
- SimpleBooleanProperty
- SimpleDoubleProperty
- SimpleFloatProperty
- SimpleIntegerProperty
- SimpleListProperty
- SimpleLongProperty
- SimpleMapProperty
- SimpleObjectProperty
- SimpleSetProperty
- SimpleStringProperty
您可以定义任何简单的属性对象,可以带有或不带初始值。如果没有定义默认值,它们将默认为属性包装对象的默认值——0、false、 空字符串("")或一个空集合。
SimpleIntegerProperty()
SimpleIntegerProperty(int initialValue)
它们也可以用一个名称和一个 JavaFX 称为属性“bean”的对象来创建。这并没有以任何方式封装属性,而是创建了一个符号链接到表示属性“所有者”的对象。
SimpleIntegerProperty(Object bean, String name)
SimpleIntegerProperty(Object bean, String name, int initialValue)
参数 name 和 bean 都不会改变属性的行为,但它可以作为有用的查找方式。如果您将相同的侦听器附加到多个属性(特别是通过编程生成的属性),那么这很有用。然后,一旦发生更改,您可以使用 bean 和 name 参数来检查刚刚更改的属性。
所有的 JavaFX 属性都有方法来实现以下功能:
- 侦听对属性值的更改
- 将属性捆绑在一起(绑定),以便它们自动更新
- 获取和设置(如果可写)属性值
如何观察属性
正如我们刚才从上面看到的,JavaFX Property 对象是不同实现接口的大杂烩。这在这里很重要,因为这意味着它们提供了两种侦听更改的方式:失效和更改。
失效侦听器:JavaFX 中的每个属性都扩展了 Observable 接口,这意味着它们都提供了注册侦听器的功能,当属性失效时,这些侦听器就会被触发。如果你不熟悉“invalidates”,它是一种将属性标记为潜在更改而不强制其重新计算属性值的方法。
对于具有复杂或昂贵计算的属性,这可能是一个有用的工具,但我发现它们使用得不如更改侦听器多。
更改监听器:在此之上,JavaFX 属性扩展了 ObservableValue<T>
,这意味着您可以注册监听器,该监听器只在对象实际更改时触发。与无效侦听器相比,我更经常使用这些侦听器。
更改侦听器允许我们听到更改,并提前提供可执行代码,这些代码将基于 Property 的新旧值执行。
监听更改
您可以通过调用 addListener()
方法在属性上注册一个监听器,提供一个 InvalidationListener
(不太常见)或 ChangeListener
(更常见)。
要添加更改侦听器,我们可以通过实现 ChangeListener 接口来完整地定义它——这是一个具有一个方法:changed()
的函数接口。
DoubleProperty altitude = new SimpleDoubleProperty(35000);
ChangeListener<Number> changeListener = new ChangeListener<Number>() {
@Override
public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
if (newValue.doubleValue() < 15000) {
deployParachute();
}
}
};
altitude.addListener(changeListener);
所有的数值属性(double、float、int 和 long)都需要使用 Number 类型参数化的更改侦听器。当然,因为它们是一个功能性接口,我们也可以使用自 Java 8 以来的 lambdas 内联创建它们。
altitude.addListener((observable, oldValue, newValue) -> {
if (newValue.doubleValue() < 15000) {
deployParachute();
}
});
每当属性的值更改时,更改侦听器将触发一次。
什么是 Binding
Binding 是一种将对象连接在一起的方法,强制执行一个对象至少依赖另一个对象的关系。属性本身可以本机绑定,也可以通过创建 Expression 和 Binding 对象来绑定。
Expression 和 Binding 都是可观察对象,它们也依赖于至少一个其他可观察对象的值(但可能更多)。这使您能够创建具有多个计算的表达式链:一种将字符串或数字转换组合在一起的超简单方法。
Expression 和 Binding 在本文的中级和高级部分中。现在,我们只将两个属性绑定在一起,而不需要任何额外的类。
如何绑定属性
在幕后,绑定是侦听更改的特定用例。所有 JavaFX 的绑定 api 都有样板代码,用来监听(至少)一个属性的更改,并使用任何更改来更新该绑定的值。
更改侦听器让我们提前提供可执行代码,而绑定让我们方便地将两个属性串在一起,而不必担心实现更新特定值的问题。
最简单和最常用的方法是附加到 Property 对象本身的方法:bind()
和 bindBidirectional()
。它们代表了单向和双向绑定的最简单的选项。
单向绑定
当您在目标属性上调用 bind()
方法时,您将第二个属性作为参数传递给它——绑定源。
StringProperty sourceProperty = new SimpleStringProperty("First Value");
StringProperty targetProperty = new SimpleStringProperty("Second Value");
targetProperty.bind(sourceProperty);
目标存储对新源属性的引用,并侦听更改。当源值更改时,当检测到更改时,它会自动更新目标(本身)。
在本例中,targetProperty 将跟踪 sourceProperty 的值。关于此方法的额外位的几点注意事项:
如果属性当前已经被绑定,则当前绑定将被删除,新绑定将替换它。
如果提供了 null 参数,则该方法抛出一个 NullPointerExeption。
该方法立即复制它正在侦听的属性的值,因此目标属性的当前值将丢失。
双向绑定
当然,可以将两个方向的属性连接在一起,将它们的值连接在一起,这样它们的值总是相同的。为此,我们调用bindBidirectional()
,再次将源属性作为参数传递。
StringProperty sourceProperty = new SimpleStringProperty("First Value");
StringProperty targetProperty = new SimpleStringProperty("Second Value");
targetProperty.bindBidirectional(sourceProperty);
如果属性的值不同,那么方法调用的顺序对于确定绑定的起始值很重要。
应用于 targetProperty 的方法在对 sourceProperty 进行交互绑定之前立即更新 targetProperty 的值。这意味着在双向绑定之后,两个属性都将把属性的值作为参数传递(源属性)。
除了被限制为碳复制方法的基本绑定之外,JavaFX 还支持更复杂的绑定:创建一个属性的多个或复杂操作来更新另一个属性。
如果将一个属性绑定到另一个属性不能涵盖您的用例,请不要担心——我将在下面几节中介绍更复杂的绑定。
中间绑定技术
有三种方法可以操作任何属性,并将操作后的值用于绑定:
- Fluent API – 类似
myProperty.bind(otherProperty).multiply(2)
的方法 - Bindings API – 类似
Bindings.add(myProperty, otherProperty)
的方法 - Low-Level API – 创建
DoubleBinding
这样的自定义 Binding 对象
其中两个提供了 cookie-cutter 方法,用于在预定义的实现中绑定属性。我总是发现这些包含了属性绑定的大部分用例,因为它们给了您巨大的灵活性。
低级 API(创建 Binding 对象)可以有更高的性能,但可能会变得复杂得多。在此基础上,我将它分成单独的部分,我将在本文的末尾详细介绍。
Fluent API
Fluent API 依赖于“表达式”对象的创建,该对象类似于属性(它们是可观察值),具有额外的方便方法来支持额外的操作。
属性也可以绑定到表达式,这意味着任何操作的输出都可以用于更新属性,如上所述。这种功能——被观察到并依赖于对象的值——创造了链接的可能性。
在字符串的情况下,我们可以使用它来创建字符串链,它们连接在一起。一旦 sourceProperty 更新,targetProperty 将通过表达式自动更新。
StringProperty sourceProperty = new SimpleStringProperty("It doesn't matter how slowly you go");
StringExpression expression = sourceProperty.concat(" as long as you don't stop");
StringProperty targetProperty = new SimpleStringProperty("");
targetProperty.bind(expression);
System.out.println(targetProperty.get());
//Output: It doesn't matter how slowly you go as long as you don't stop
您可以内联完成所有这些工作,使复杂的代码相对简洁。在本例中,我们将在调用 bind 方法的同时创建 StringExpression。
targetProperty.bind(sourceProperty.concat(" as long as you don't stop"));
System.out.println(targetProperty.get());
//Output: It doesn't matter how slowly you go as long as you don't stop
这可能有点令人困惑,但不要忘记 targetProperty 实际上是绑定到由 concat()
方法创建的 StringExpression 的。绑定到 sourceProperty 的是匿名表达式。
这带来了惊人的好处。API 很丰富,这种风格生成的可读代码涵盖了您需要的大多数操作。
我们也可以使用 Fluent API 来处理数字。
在处理数字时,我们可以链式操作来创建简单、可读的代码,表示我们试图复制的公式。要把角度转换成弧度,你要乘以 π 再除以 180。代码具有很高的可读性。
DoubleProperty degrees = new SimpleDoubleProperty(180);
DoubleProperty radians = new SimpleDoubleProperty();
radians.bind(
degrees.multiply(Math.PI).divide(180)
);
但是,就性能而言,每个表达式都是链中的一个链接,在初始属性的每次更改时都需要更新。在这个例子中,我们将角度转换为弧度,我们创建了两个可观察值来更新弧度属性。
对于复杂的转换,或者在需要进行大量绑定的情况下,可以考虑使用 Bindings API(如果它提供了所需的灵活性),或者低级 API。
Bindings API
JavaFX 中的 Bindings 类是一个实用程序类,包含 249 个用于属性绑定的方法。它允许你在各种类型的可观察对象之间创建绑定。根据绑定的不同,可以将属性与值结合使用,比如字符串和数字。
有 10 种通用的绑定策略,我将其分为两个主要领域,我将其称为“值操作”和“集合操作”。有些桶装不下,所以我们有了一个不雅的桶,叫做“other”。
Values
- Mathematics (+, – ÷, ×)
- Selecting the maximum or minimum
- Value comparison (=, !=, <, >, <=, >=)
- String formatting
Collections
- Binding two collections (lists, maps, sets)
- Binding values to objects at a certain position in a collection
- Binding to collection size
- Whether a collection is empty
Other bindings
- Multiple-object bindings
- Boolean operators (and, not or) – (and when!)
- Selecting values
由于方法的数量庞大,我将深入探讨如何使用我认为(1)经常使用的方法或(2)难以理解的方法。本质上,我会试着用有用的解释增加价值,同时尽量不让你看到 RSI 滚动。
操作值
Bindings API 支持四种简单的数学操作:加法、减法、乘法和除法。它提供了单独的方法来使用浮点型、双精度型、整型和长值,以及两个 ObservableNumberValue 对象(例如 DoubleProperty)之间的方法。
Mathematics
DoubleBinding add(double op1, ObservableNumberValue op2)
NumberBinding add(float op1, ObservableNumberValue op2)
NumberBinding add(int op1, ObservableNumberValue op2)
NumberBinding add(long op1, ObservableNumberValue op2)
DoubleBinding add(ObservableNumberValue op1, double op2)
NumberBinding add(ObservableNumberValue op1, float op2)
NumberBinding add(ObservableNumberValue op1, int op2)
NumberBinding add(ObservableNumberValue op1, long op2)
NumberBinding add(ObservableNumberValue op1, ObservableNumberValue op2)
对于每个数值选项都有相同的方法。为了方便,API 交换了第一个和第二个参数。对于加法和乘法来说,顺序无关紧要,但是对于减法来说,顺序决定了哪个参数要从另一个参数中减去。
DoubleBinding subtract(double op1, ObservableNumberValue op2)
DoubleBinding subtract(ObservableNumberValue op1, double op2)
NumberBinding subtract(ObservableNumberValue op1, ObservableNumberValue op2)
在每种情况下,从第一个参数中减去第二个参数。
DoubleBinding multiply(double op1, ObservableNumberValue op2)
DoubleBinding multiply(ObservableNumberValue op1, double op2)
NumberBinding multiply(ObservableNumberValue op1, ObservableNumberValue op2)
最后,有了划分,秩序又变得重要起来。绑定的值计算为第一个参数除以第二个参数。
DoubleBinding divide(double op1, ObservableNumberValue op2)
DoubleBinding divide(ObservableNumberValue op1, double op2)
NumberBinding divide(ObservableNumberValue op1, ObservableNumberValue op2)
总的来说,这为相对简单的操作生成了大量的方法——36 个实用方法用于 4 个基本操作。
这可能看起来有点过火,但它确实做到了它应该做的。JavaFX 的创建者为每个基本的数学运算提供了实现,这样您就不必做这些跑腿工作了。
选择最大或最小
和往常一样,JavaFX 在定义方便的方法方面做了大量的跑腿工作。下面是 max()
方法,它们在可观察值和非可观察值之间进行交换和更改。
max(double op1, ObservableNumberValue op2)
max(ObservableNumberValue op1, double op2)
max(ObservableNumberValue op1, ObservableNumberValue op2)
事实上,当你需要求两个数的最小值时,这是完全一样的。绑定总是等于两个的最小值——至少其中一个是可观察的。
min(double op1, ObservableNumberValue op2)
min(ObservableNumberValue op1, double op2)
min(ObservableNumberValue op1, ObservableNumberValue op2)
比较值(=, !=, <, >, <=, >=)
当列表已满,选择了足够多的道具,或者当他们在游戏中获胜或失败时,价值比较可以自动提醒用户。
取负和取反
在最简单的情况下,JavaFX 提供了计算负数(减去 1 乘以数字)和的方法:
negate(ObservableNumberValue value)
not(ObservableBooleanValue op)
不等于
在复杂性方面,JavaFX 还提供了两种不同类型对象之间的比较。绑定的值将始终报告这两个对象是否相等。
notEqual(double op1, ObservableNumberValue op2, double epsilon)
notEqual(Object op1, ObservableObjectValue<?> op2)
notEqual(ObservableNumberValue op1, double op2, double epsilon)
notEqual(ObservableNumberValue op1, ObservableNumberValue op2)
notEqual(ObservableNumberValue op1, ObservableNumberValue op2, double epsilon)
notEqual(ObservableObjectValue<?> op1, Object op2)
notEqual(ObservableObjectValue<?> op1, ObservableObjectValue<?> op2)
它还为比较字符串提供了很多支持。特别值得注意的是 notEqualIgnoreCase()
方法。在比较之前,我花了很长时间将所有的字符串转换成小写。
notEqual(ObservableStringValue op1, ObservableStringValue op2)
notEqual(ObservableStringValue op1, String op2)
notEqual(String op1, ObservableStringValue op2)
notEqualIgnoreCase(ObservableStringValue op1, ObservableStringValue op2)
notEqualIgnoreCase(ObservableStringValue op1, String op2)
notEqualIgnoreCase(String op1, ObservableStringValue op2)
正如标题所建议的,我们的值比较 bucket 还包含比较小于和大于的数值的方法。查看文档了解所有的细节——我暂时让您休息一下,然后继续讨论集合。
操作集合
Bindings API 提供了四种不同的绑定到集合的方式:复制、索引绑定、大小绑定和空绑定。只有第一个集合将集合的内容复制到目标集合中。其他三个提取值—单个变量—基于集合状态的一个方面。
绑定两个集合
要将两个集合绑定在一起,您可以调用 Bindings.bindContent()
或 Bindings.bindContentBidirectional()
。
在第一种情况下,你将跟踪一个可观察集合——一个 ObservableList, ObservableSet 或 ObservableMap——并在同一类型的非可观察集合中创建一个 carbon-copy。
bindContent(List<E> list1, ObservableList<? extends E> list2)
bindContent(Map<K,V> map1, ObservableMap<? extends K,? extends V> map2)
bindContent(Set<E> set1, ObservableSet<? extends E> set2)
在这种情况下,非可观察对象集合的内容会立即被可观察对象的内容覆盖。
如果你想把两个可观察集合绑定在一起,使用 Bindings.bindContentBidirectional()
方法,它接受两个相同类型的集合作为参数。
bindContentBidirectional(ObservableList<E> list1, ObservableList<E> list2)
bindContentBidirectional(ObservableMap<K,V> map1, ObservableMap<K,V> map2)
bindContentBidirectional(ObservableSet<E> set1, ObservableSet<E> set2)
在这些情况下,第一个集合将被擦除,并作为绑定过程的一部分使用第二个列表的内容。
Bindings API 不支持任何更复杂的集合绑定——比如相互映射、范围受限的列表复制。如果需要这些,则需要创建自己的绑定。为此,请查看下面的低级API部分。
绑定集合——相关的值
除此之外,绑定的选项仅限于绑定与集合相关的值——特定索引处的值、集合的大小或集合是否为空。
将值绑定到集合中特定位置的对象
根据集合中指定索引处的单个值绑定变量是非常容易的。在每种情况下,你都需要提供一个可观察对象集合,以及该对象的索引。
索引的值也可以作为可观察值传入,这意味着随着它的变化,绑定的值也会发生变化。
每种对象类型都有一个方法专门化,它为请求的值生成正确类型的绑定对象。布尔值、浮点值、双精度值、整型值、长值和字符串值都通过单独的方法支持。
记住:绝对值得记住的是,这些方法不会像我们上面看到的那样把你的参数绑定在一起。每个 valueAt 方法都返回一个 Binding 对象,其中包含您的值。
绑定到大小或空置的
基于大小或集合是否为空的绑定要容易得多。绑定 API 提供了对 ObservableList, ObservableArray, ObservableSet 和 ObservableMap对象的支持,以及 ObservableStringValue。
查看 isEmpty()
和各种 size()
方法的文档,查看所有选项。
创建绑定对象
我将在这里介绍的 Bindings API 的最后一部分是绑定多个自定义对象,并提供一个函数来计算该值。
createBooleanBinding(Callable<Boolean> func, Observable... dependencies)
createDoubleBinding(Callable<Double> func, Observable... dependencies)
createFloatBinding(Callable<Float> func, Observable... dependencies)
createIntegerBinding(Callable<Integer> func, Observable... dependencies)
createLongBinding(Callable<Long> func, Observable... dependencies)
createObjectBinding(Callable<T> func, Observable... dependencies)
createStringBinding(Callable<String> func, Observable... dependencies)
这几乎和低级 API 一样好,但它有一些限制:
请注意,它依赖于虚方法调用(您正在传递一个函数对象,而不是创建一个类方法)。这意味着如果你经常使用它,它可能会比低层 API 慢。
不过,这是一个相对简单的API——只需传入每个依赖项,然后使用提供的函数对它们进行转换。为了清晰起见,我将函数定义为 lambda,但您也可以手动创建 Callable<Double>
并覆盖 call()
方法。
IntegerProperty cost = new SimpleIntegerProperty(15);
DoubleProperty multiplier = new SimpleDoubleProperty(25);
double flatRate = 4;
DoubleBinding totalCost = Bindings.createDoubleBinding(
() -> cost.get() * multiplier.get() + flatRate,
cost, multiplier
);
如果这不能满足您的需求,您可以通过扩展 JavaFX 的 Bindings 类中可用的对象来创建完全定制的绑定。这样可以更快。所以如果你需要,这里是如何做的。
高阶绑定——低级 API Binding
在 JavaFX 中创建绑定的最可定制的方法是自己手动创建一个 binding 对象。这样做的好处是,您可以精确地定义所需的计算,而不必创建 Expression 对象链,这可能会降低性能。
当然,您也可以进行复杂的计算并使用 Bindings API 绑定多个对象,但正如我们前面看到的那样,这样做的效率并不高。低级 API 的好处是,在计算值时需要执行的任何计算都是在自定义 Binding 类中定义的。
如果你想知道为什么这可能会有更好的性能,那就是类函数更有可能被编译器“内联”。这意味着,如果您需要一个绑定的值重复且快速地为计算机,那么您的代码可能会执行得更快。
什么是低级 API
低级 API, 10 个抽象绑定类的集合,旨在实现所有棘手的绑定(例如,添加和删除侦听器)。这使您可以集中精力指定应该如何计算绑定的值。
每个类都接受可观察值(比如属性)并将它们转换为输出。就像 Fluent 和 Bindings API 一样,低级 API 支持布尔值、字符串、数字、集合和对象。
创建低级 Binding
创建低级绑定可以像定义抽象内部类(与其他代码一起定义的类)一样简单。因为抽象绑定类只有一个抽象方法,所以您只需要在定义方法时重写 computeValue()
。
在定义绑定时,官方建议在绑定创建期间使用初始化块绑定源属性。老实说,与创建构造函数相比,我不喜欢这样做,但这可能只是因为初始化块看起来有点陌生。
总体效果是完全相同的——编译器将代码从初始化块复制到每个构造函数中。如果您要创建一个将要使用多次的具体类,则构造函数方法更合适。
//Inside your binding object at the top
{
super.bind(cost, itemCount);
}
然后,剩下的工作就是定义 computeValue()
方法。在本例中,它非常简单,但是您可以将计算变得任意复杂。
DoubleProperty cost = new SimpleDoubleProperty(25);
IntegerProperty itemCount = new SimpleIntegerProperty(15);
DoubleBinding totalCost = new DoubleBinding() {
{
super.bind(cost, itemCount);
}
@Override
protected double computeValue() {
return itemCount.get() * cost.get();
}
};
由此可见,totalCost 绑定的值将始终反映 cost 和 itemCount 属性的乘积。
如果您希望能够传递 totalCost 对象并在稍后检索依赖项,您可以添加额外的功能来覆盖默认的 getDependencies()
方法。
给低级 API 添加功能
在可定制的 value 方法之上,可以通过覆盖 getDependencies()
和 dispose()
方法来扩展低级 API 中的每个类。
- getDependencies():如果您需要存储它们并在以后取回它们,可以返回所有的依赖项(见下面的警告!)
- dispose():可以注销绑定所有依赖项的侦听器。您通常不需要这样做,除非您特别在没有弱监听器(默认)的情况下实现绑定。
重写 getDependencies()
如果您希望能够将绑定对象传递给另一个类,或者存储它并在以后检索依赖项,那么重写 getDependencies()
是很有用的。
DoubleBinding totalCost = new DoubleBinding() {
{
super.bind(cost, itemCount);
}
@Override
protected double computeValue() {
return itemCount.get() * cost.get();
}
@Override
public ObservableList<?> getDependencies() {
return FXCollections.observableList(Arrays.asList(cost, itemCount));
}
};
在匆忙重写此方法之前,值得记住的是,低级API的所有默认实现都使用弱侦听器。这意味着:
如果你使用默认实现的低级 API,你需要保持对你的可观察对象的强引用,否则它们将被垃圾收集,引用将丢失。
也就是说,如果您已经用强侦听器实现了绑定,那么您还需要重写 dispose()
方法。这将防止内存泄漏的发生,如果绑定被使用并被遗忘(至少被您……)后没有从被观察的对象中注销。
重写 dispose()
重写 dispose()
方法就像系统地注销我们开始时绑定的每个可观察对象一样简单。要在一次调用中完成此操作,可以调用 unbind()
,传入每个值。
@Override
public void dispose() {
unbind(cost, itemCount);
}
如果您有一个更复杂的自定义实现,您可能需要一次查找和取消注册一个可观察对象。
总结
JavaFX 中的属性可以是只读的,也可以是可写的,但是始终是可观察的。每个属性都实现了 javafx.bean 的功能。绑定,javafx.beans.value 和 javafx.beans.property 包。
每个属性都可以使用 InvalidationListener 或 ChangeListener 对象来观察。这两个都可以通过调用 addListener()
方法来访问,因为每个属性都有一个 addListener()
方法来进行无效和更改。
属性监听的一个扩展是属性绑定,该功能允许用户将属性连接在一起,以便根据一个或多个更改自动更新属性。
在此基础上,JavaFX 支持通过 Expression 对象和 bindings 对象扩展绑定。通过 Fluent 和 Bindings api 访问这些内容是最简单的。但是,如果您绝对需要性能或定制,那么低级 API 允许您自己创建完全定制的绑定。