JavaFX17 学习手册(一)
一、入门指南
在本章中,您将学习:
-
JavaFX 是什么
-
JavaFX 的历史
-
如何设置 Eclipse IDE 以使用 JavaFX 应用程序,以及如何编写您的第一个 JavaFX 应用程序
-
如何向 JavaFX 应用程序传递参数
-
如何启动 JavaFX 应用程序
-
JavaFX 应用程序的生命周期
-
如何终止 JavaFX 应用程序
JavaFX 是什么?
JavaFX 是一个基于 Java 的开源框架,用于开发富客户端应用程序。它可以与市场上的其他框架(如 Adobe AIR 和 Microsoft Blazor)相媲美。在 Java 平台的图形用户界面(GUI)开发技术领域,JavaFX 也被视为 Swing 的继承者。JavaFX 库作为公共 Java 应用程序编程接口(API)提供。JavaFX 包含几个特性,这些特性使它成为开发富客户端应用程序的首选:
-
JavaFX 是用 Java 编写的,这使您能够利用所有 Java 特性,如多线程、泛型和 lambda 表达式。您可以使用自己选择的任何 Java IDE(如 NetBeans 或 Eclipse)来创作、编译、运行、调试和打包您的 JavaFX 应用程序。
-
JavaFX 通过其库支持数据绑定。
-
JavaFX 代码也可以使用任何 Java 虚拟机(JVM)支持的脚本语言编写,比如 Kotlin、Groovy 和 Scala。
-
JavaFX 提供了两种构建用户界面(UI)的方法:使用 Java 代码和使用 FXML。FXML 是一种基于 XML 的可脚本化标记语言,用于以声明方式定义 UI。Gluon 公司提供了一个名为 Scene Builder 的工具,这是一个 FXML 的可视化编辑器。
-
JavaFX 提供了丰富的多媒体支持,如播放音频和视频。它利用了平台上可用的编解码器。
-
JavaFX 允许您在应用程序中嵌入 web 内容。
-
JavaFX 为应用效果和动画提供了现成的支持,这对开发游戏应用程序非常重要。您可以通过编写几行代码来实现复杂的动画。
JavaFX API 背后有许多组件,可以利用 Java 本地库和可用的硬件和软件。JavaFX 组件如图 1-1 所示。
图 1-1
JavaFX 平台的组件
JavaFX 中的 GUI 被构造成一个场景图。场景图是视觉元素的集合,称为节点,以分层方式排列。使用公共 JavaFX API 构建场景图。场景图中的节点可以处理用户输入和用户手势。它们可以有效果、转换和状态。场景图中的节点类型包括简单的 UI 控件,如按钮、文本字段、二维(2D)和三维(3D)形状、图像、媒体(音频和视频)、web 内容和图表。
Prism 是一个硬件加速的图形管道,用于渲染场景图形。如果硬件加速渲染在平台上不可用,则使用 Java 2D 作为后备渲染机制。例如,在使用 Java 2D 进行渲染之前,它将尝试在 Windows 上使用 DirectX,在 Mac、Linux 和嵌入式平台上使用 OpenGL。
Glass Windowing Toolkit 使用本地操作系统提供图形和窗口服务,如 windows 和计时器。该工具包还负责管理事件队列。在 JavaFX 中,事件队列由一个名为 JavaFX 应用线程的操作系统级线程管理。所有用户输入事件都在 JavaFX 应用程序线程上调度。JavaFX 要求只能在 JavaFX 应用程序线程上修改实时场景图形。
Prism 使用一个单独的线程,而不是 JavaFX 应用程序线程来进行渲染。它通过在处理下一帧的同时渲染一帧来加速处理过程。当场景图形被修改时,例如,通过在文本字段中输入一些文本,Prism 需要重新渲染场景图形。使用称为脉冲事件的事件来实现场景图形与 Prism 的同步。当场景图形被修改并且需要重新渲染时,一个脉冲事件在 JavaFX 应用程序线程上排队。脉冲事件表示场景图形与 Prism 中的渲染层不同步,应该渲染 Prism 级别的最新帧。脉冲事件被限制在每秒最大 60 帧。
媒体引擎负责在 JavaFX 中提供媒体支持,例如,回放音频和视频。它利用了平台上可用的编解码器。媒体引擎使用单独的线程处理媒体帧,并使用 JavaFX 应用程序线程将帧与场景图形同步。媒体引擎基于 GStreamer ,这是一个开源的多媒体框架。
web 引擎负责处理嵌入在场景图中的 web 内容(HTML)。Prism 负责呈现 web 内容。web 引擎基于 WebKit ,这是一个开源的 web 浏览器引擎。支持 HTML5、级联样式表(CSS)、JavaScript 和文档对象模型(DOM)。
Quantum toolkit 是对 Prism、Glass、media engine 和 web engine 等底层组件的抽象。它还有助于低层组件之间的协调。
Note
在本书中,假设您已经掌握了 Java 编程语言的中级知识,包括 lambda 表达式和新的 Time API(从 Java 8 开始)。
JavaFX 的历史
JavaFX 最初是由 Chris Oliver 在 SeeBeyond 开发的,它被称为 F3 (Form Follows Function)。F3 是一种易于开发 GUI 应用程序的 Java 脚本语言。它提供了声明性语法、静态类型、类型推断、数据绑定、动画、2D 图形和 Swing 组件。SeeBeyond 被 Sun Microsystems 收购,F3 于 2007 年更名为 JavaFX。甲骨文在 2010 年收购了太阳微系统公司。甲骨文随后在 2013 年开源了 JavaFX。
JavaFX 的第一个版本发布于 2008 年第四季度。版本号从 2.2 跃升到 8.0。从 Java 8 开始,Java SE 和 JavaFX 的版本号将是相同的。Java SE 和 JavaFX 的主要版本也将同时发布。JavaFX 的当前版本是 17.0 版。从 Java SE 11 开始,JavaFX 不再是 Java SE 运行时库的一部分。在 Java 11 中,您需要下载并包含 JavaFX 库来编译和运行您的 JavaFX 程序。表 1-1 包含 JavaFX 的发布列表。
表 1-1
JavaFX 版本
|出厂日期
|
版本
|
评论
| | --- | --- | --- | | 2008 年第四季度 | JavaFX 1.0 | 这是 JavaFX 的最初版本。它使用一种称为 JavaFX Script 的声明语言来编写 JavaFX 代码。 | | Q1,2009 年 | JavaFX 1.1 | 引入了对 JavaFX Mobile 的支持。 | | Q2,2009 年 | JavaFX 1.2 | – | | Q2,2010 年 | JavaFX 1.3 | – | | 2010 年第三季度 | JavaFX 1.3.1 | – | | 2011 年第四季度 | java fx 2.0 | 不再支持 JavaFX 脚本。它使用 Java 语言编写 JavaFX 代码。对 JavaFX Mobile 的支持已取消。 | | 2012 年,Q2 | JavaFX 2.1 | 引入了对 Mac OS 桌面版的支持。 | | 2012 年第三季度 | JavaFX 2.2 | – | | 2014 年,Q1 | JavaFX 8.0 | JavaFX 版本从 2.2 跳到了 8.0。JavaFX 和 Java SE 版本将从 Java 8 开始匹配。 | | 2015 年,Q2 | JavaFX 9.0 | 公开的一些内部 API,JEP253。 | | 2018 年第三季度 | JavaFX 11.0.3 | JavaFX 不再是 Oracle Java JDK 的一部分。JavaFX 现在是一个可下载的开源模块,由 Gluon 公司提供。作为端口增加了对手持设备和其他嵌入式设备的支持。 | | 2019 年,Q1 | JavaFX 12.0.1 | 错误修复和一些增强。 | | 2019 年第三季度 | JavaFX 13.0 | 错误修复和一些增强。 | | Q1,2020 年 | JavaFX 14.0 | 在 WebView 中支持 HTTP/2。更多的错误修复和一些增强。 | | 2020 年第三季度 | JavaFX 15.0 | 提高稳定性(内存管理)。更多的错误修复和一些增强。 | | Q1,2021 年 | JavaFX 16.0 | JavaFX 模块必须从模块路径加载,而不是从类路径加载(编译器警告)。更多的错误修复和一些增强。 | | 2021 年第四季度 | JavaFX 17.0.1 | 小的改进和错误修复。 |
发行说明显示了更多详细信息。你可以在 https://github.com/openjdk/jfx/tree/master/doc-files 看到它们。
系统需求
您需要在计算机上安装以下软件:
-
Java 开发工具包 17,来自 Oracle,或者 OpenJDK。
-
Eclipse IDE 2021-06 或更高版本。
-
适用于您平台的 JavaFX 17 SDK,下载并解压缩到您选择的文件夹中。前往
https://openjfx.io/获取文档和下载链接。
Caution
如果您使用 Oracle 的 JDK,您需要输入一个付费程序,以防您将 JDK 用于商业项目。如果不希望这样,可以考虑使用 OpenJDK。
没有必要使用 Eclipse IDE 来编译和运行本书中的程序。您可以使用任何其他 IDE,例如 NetBeans、JDeveloper 或 IntelliJ IDEA。如果您愿意的话,您可以不使用任何 IDE,只使用命令行,也许还可以使用 Ant、Maven 或 Grails 之类的构建工具。
JavaFX 发行版还提供了一组预打包的 jmod 文件。然而,在编写这个版本时,由于类路径问题,还不能使用它们。如果你愿意,你可以试试 JMODs。我们将在本书中使用 SDK,它由一堆打包成 jar 文件的 Java 模块以及特定于平台的本地库组成。
JavaFX 运行时库
在 PC 上下载并解压 JavaFX SDK 发行版后,您会发现 Java 模块 jar(Java FX . base、javafx.graphics 等。)以及特定于平台的本机库(。所以文件,。dll 文件等。)在 lib 文件夹中。为了编译和运行 JavaFX 应用程序,您必须引用这个 lib 文件夹中的 jar,并且所有特定于平台的本地库必须位于同一个文件夹中。
如果您使用 IDE 和/或 JavaFX 工具,可能会包含 JavaFX 模块和库,并为您进行配置。如果您是这种情况,请注意正确的 JavaFX 版本。最可靠的开发设置,尽管可能不是最方便的,是使用 Eclipse 编辑和编译 Java 文件,自己提供 JavaFX 库,并且而不是使用 IDE 的任何特殊 JavaFX 特性。
JavaFX 源代码
有经验的开发人员有时更喜欢查看 JavaFX 库的源代码,以了解幕后是如何实现的。
JavaFX SDK 包括源代码—注意 lib 文件夹中的 src.zip 文件。这也是您可以从 Eclipse 内部使用的文件,用于将 JavaFX 源代码附加到 JavaFX API 库——然后您可以使用 F3 键来轻松导航到 JavaFX API 类源代码。
您的第一个 JavaFX 应用程序
让我们编写您的第一个 JavaFX 应用程序。它应该在一个窗口中显示文本“Hello JavaFX”。我将采用一种循序渐进的方法来解释如何开发第一个应用程序。我将添加尽可能少的代码行,然后解释代码做什么以及为什么需要它。
开始一个 Eclipse 项目
让我们首先看看如何建立一个 Eclipse 项目来开发 JavaFX 应用程序。
Note
如果您使用不同的 IDE,为了了解如何设置 JavaFX 项目,您必须查阅 IDE 的文档。
打开 Eclipse,使用您喜欢的任何工作空间。接下来,安装来自 JDK 17 的 JRE,如果你还没有这样做的话。为此,请转到首选项并注册 JRE,如图 1-2 所示。
图 1-2
在 Eclipse 中注册 JRE
启动一个名为 HelloFX 的新 Java 项目。确保新建项目向导创建了一个 module-info.java 文件。有一个复选框。在项目首选项中,添加 JavaFX 安装的 lib 文件夹中的所有模块 JARs 参见图 1-3 。
图 1-3
在 Eclipse 中添加 JavaFX 模块
确保将 jar 添加到 Modulepath 部分,而不是 Classpath 部分。
作为最后一个准备步骤,在 src 文件夹中创建一个包 com . jdojo . intro——在这里,我们添加应用程序类。
设置模块信息
为了让 JavaFX 在模块化环境中正确工作,请记住我们在 src 文件夹中添加了一个 module-info.java 文件,这相当于说“我们使用模块化环境”,我们需要在该文件中添加几个条目。打开它,将其内容更改为
module JavaFXBook {
requires javafx.graphics;
requires javafx.controls;
requires java.desktop;
requires javafx.swing;
requires javafx.media;
requires javafx.web;
requires javafx.fxml;
requires jdk.jsobject;
opens com.jdojo.intro to javafx.graphics, javafx.base;
}
在第一行中,使用您在创建项目时输入的任何内容作为模块名称。
创建 HelloJavaFX 类
一个JavaFX application是一个必须从javafx.application包中的Application class继承的类。您将把您的类命名为HelloFXApp,它将被存储在com.jdojo.intro包中。清单 1-1 显示了HelloFXApp类的初始代码。请注意,HelloFXApp类此时不会编译。您将在下一节中修复它。
// HelloFXApp.java
package com.jdojo.intro;
import javafx.application.Application;
public class HelloFXApp extends Application {
public static void main(String[] args) {
Application.launch(args);
}
// Application logic goes here
}
Listing 1-1Inheriting Your JavaFX Application Class from the javafx.application.Application Class
该程序包括一个包声明、一个导入语句和一个类声明。代码中没有类似 JavaFX 的内容。它看起来像任何其他 Java 应用程序。然而,通过从Application类继承HelloFXApp类,您已经满足了 JavaFX 应用程序的需求之一。
覆盖 start() 方法
如果您尝试编译HelloFXApp类,将会导致以下编译时错误: *HelloFXApp 不是抽象的,不会覆盖应用程序中的抽象方法 start(Stage)。*该错误表明Application类包含一个抽象的start(Stage stage)方法,该方法没有在HelloFXApp类中被覆盖。作为 Java 开发人员,您知道下一步该做什么:要么将HelloFXApp类声明为抽象类,要么为start()方法提供一个实现。这里,让我们为start()方法提供一个实现。Application类中的start()方法声明如下:
public abstract void start(Stage stage) throws java.lang.Exception
清单 1-2 显示了覆盖start()方法的HelloFXApp类的修改代码。
// HelloFXApp.java
package com.jdojo.intro;
import javafx.application.Application;
import javafx.stage.Stage;
public class HelloFXApp extends Application {
public static void main(String[] args) {
Application.launch(args);
}
@Override
public void start(Stage stage) {
// The logic for starting the application goes here
}
}
Listing 1-2Overriding the start() Method in Your JavaFX Application Class
在修订后的代码中,您加入了两件事:
-
您已经添加了一个另外的
import语句来从javafx.stage包中导入Stage类。 -
您已经实现了
start()方法。该方法的throws子句被删除,这符合 Java 中覆盖方法的规则。
start()方法是 JavaFX 应用程序的入口点。它由 JavaFX 应用程序启动器调用。注意,start()方法被传递了一个Stage类的实例,这个实例被称为应用程序的初级阶段。您可以根据需要在应用程序中创建更多阶段。但是,主阶段总是由 JavaFX 运行时为您创建的。
Tip
每个 JavaFX 应用程序类都必须从Application类继承,并为start(Stage stage)方法提供实现。
展示舞台
类似于现实世界中的舞台,JavaFX 舞台用于显示场景。场景具有视觉效果,如文本、形状、图像、控件、动画和效果,用户可以与这些视觉效果进行交互,所有基于 GUI 的应用程序都是如此。
在 JavaFX 中,主舞台是场景的容器。根据应用程序的运行环境,stage 的外观会有所不同。您不需要根据环境采取任何行动,因为 JavaFX 运行时会为您处理所有细节。由于该应用程序作为桌面应用程序运行,主舞台将是一个带有标题栏和显示场景区域的窗口。
由应用程序启动器创建的初级阶段没有场景。在下一节中,您将为您的舞台创建一个场景。
您必须展示舞台才能看到场景中包含的视觉效果。使用show()方法显示阶段。或者,您可以使用setTitle()方法为舞台设置一个标题。清单 1-3 中显示了HelloFXApp类的修订代码。
// HelloFXApp.java
package com.jdojo.intro;
import javafx.application.Application;
import javafx.stage.Stage;
public class HelloFXApp extends Application {
public static void main(String[] args) {
Application.launch(args);
}
@Override
public void start(Stage stage) {
// Set a title for the stage
stage.setTitle("Hello JavaFX Application");
// Show the stage
stage.show();
}
}
Listing 1-3Showing the Primary Stage in Your JavaFX Application Class
启动应用程序
现在您已经准备好运行您的第一个 JavaFX 应用程序了。您的 IDE 可能已经动态编译了您的类。Eclipse 就是这样工作的。
使用 Eclipse IDE 中的启动器运行HelloFXApp类。在类名上单击鼠标右键,然后调用作为➤ Java 应用程序运行。在命令行上,为了编译和运行,您必须添加所有的 modulepath 条目,这超出了本章的介绍范围。
如果成功启动,应用程序将显示一个带有标题栏的窗口,如图 1-4 所示。
图 1-4
没有场景的 JavaFX 应用程序
窗口的主要区域是空的。这是舞台将显示其场景的内容区域。因为您还没有舞台场景,所以您会看到一个空白区域。标题栏显示您在start()方法中设置的标题。
您可以使用窗口标题栏中的关闭菜单选项关闭应用程序。在 Windows 中使用 Alt + F4 关闭窗口。您可以使用平台提供的任何其他选项来关闭窗口。
Tip
直到所有窗口都关闭或者应用程序使用Platform.exit()方法退出,类Application的launch()方法才返回。Platform级在javafx.application包里。
您还没有在 JavaFX 中看到任何令人兴奋的东西!你需要等待,直到你在下一部分创建一个场景。
向舞台添加场景
在javafx.scene包中的Scene类的一个实例代表一个场景。舞台包含一个场景,场景包含视觉内容。
场景的内容以树状层次排列。在层次结构的顶端是根节点和 ?? 节点。根节点可能包含子节点,子节点又可能包含它们的子节点,依此类推。必须有根节点才能创建场景。您将使用一个VBox作为根节点。VBox代表垂直框,将其子项垂直排列成一列。下面的语句创建了一个VBox:
VBox root = new VBox();
Tip
从javafx.scene.Parent类继承的任何节点都可以用作场景的根节点。几个节点,称为布局窗格或容器,如VBox、HBox、Pane、FlowPane、GridPane或TilePane,可以用作根节点。Group是一个特殊的容器,将它的子容器组合在一起。
可以有子节点的节点提供了一个返回其子节点的ObservableList的getChildren()方法。要向节点添加子节点,只需将子节点添加到ObservableList中。下面的代码片段将一个Text节点添加到一个VBox中:
// Create a VBox node
VBox root = new VBox();
// Create a Text node
Text msg = new Text("Hello JavaFX");
// Add the Text node to the VBox as a child node
root.getChildren().add(msg);
Scene类包含几个构造器。您将使用允许您指定场景的根节点和大小的那个。以下语句创建一个以VBox为根节点的场景,宽度为 300 像素,高度为 50 像素:
// Create a scene
Scene scene = new Scene(root, 300, 50);
您需要通过调用Stage类的setScene()方法将场景设置为舞台:
// Set the scene to the stage
stage.setScene(scene);
就这样。您已经用一个场景完成了您的第一个 JavaFX 程序。清单 1-4 包含完整的程序。程序显示如图 1-5 所示的窗口。
图 1-5
JavaFX 应用程序的场景有一个Text节点
// HelloFXAppWithAScene.java
package com.jdojo.intro;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.stage.Stage;
public class HelloFXAppWithAScene extends Application {
public static void main(String[] args) {
Application.launch(args);
}
@Override
public void start(Stage stage) {
Text msg = new Text("Hello JavaFX");
VBox root = new VBox();
root.getChildren().add(msg);
Scene scene = new Scene(root, 300, 50);
stage.setScene(scene);
stage.setTitle(
"Hello JavaFX Application with a Scene");
stage.show();
}
}
Listing 1-4A JavaFX Application with a Scene Having a Text Node
改进 HelloFX 应用程序
JavaFX 能够做的事情比您到目前为止看到的要多得多。让我们增强第一个程序,并添加更多的用户界面元素,如按钮和文本字段。这一次,用户将能够与应用程序进行交互。使用Button类的实例创建一个按钮,如下所示:
// Create a button with "Exit" text
Button exitBtn = new Button("Exit");
当一个按钮被点击时,一个ActionEvent被触发。您可以添加一个ActionEvent处理程序来处理该事件。使用setOnAction()方法为按钮设置一个ActionEvent处理程序。下面的语句为按钮设置了一个ActionEvent处理程序。处理程序终止应用程序。您可以使用 lambda 表达式或匿名类来设置ActionEvent处理程序。以下代码片段展示了这两种方法:
// Using a lambda expression
exitBtn.setOnAction(e -> Platform.exit());
// Using an anonymous class
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
...
exitBtn.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent e) {
Platform.exit();
}
});
清单 1-5 中的程序展示了如何给场景添加更多的节点。该程序使用Label类的setStyle()方法将Label的填充颜色设置为蓝色。稍后我将讨论在 JavaFX 中使用 CSS。
// ImprovedHelloFXApp.java
package com.jdojo.intro;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class ImprovedHelloFXApp extends Application {
public static void main(String[] args) {
Application.launch(args);
}
@Override
public void start(Stage stage) {
Label nameLbl = new Label("Enter your name:");
TextField nameFld = new TextField();
Label msg = new Label();
msg.setStyle("-fx-text-fill: blue;");
// Create buttons
Button sayHelloBtn = new Button("Say Hello");
Button exitBtn = new Button("Exit");
// Add the event handler for the Say Hello button
sayHelloBtn.setOnAction(e -> {
String name = nameFld.getText();
if (name.trim().length() > 0) {
msg.setText("Hello " + name);
} else {
msg.setText("Hello there");
}
});
// Add the event handler for the Exit button
exitBtn.setOnAction(e -> Platform.exit());
// Create the root node
VBox root = new VBox();
// Set the vertical spacing between children to 5px
root.setSpacing(5);
// Add children to the root node
root.getChildren().addAll(nameLbl, nameFld, msg,
sayHelloBtn, exitBtn);
Scene scene = new Scene(root, 350, 150);
stage.setScene(scene);
stage.setTitle("Improved Hello JavaFX Application");
stage.show();
}
}
Listing 1-5Interacting with Users in a JavaFX Application
改进后的HelloFX程序显示如图 1-6 所示的窗口。该窗口包含两个标签、一个文本字段和两个按钮。一个VBox被用作场景的根节点。在文本栏中输入名称,然后点按“问好”按钮以查看问候信息。在不输入姓名的情况下点击“问好”按钮会显示消息Hello there。应用程序在Label控件中显示一条消息。单击退出按钮退出应用程序。
图 1-6
一个 JavaFX 应用程序,它的场景中有一些控件
向 JavaFX 应用程序传递参数
与 Java 应用程序一样,您可以在命令行上或者通过 IDE 中的一些启动配置将参数传递给 JavaFX 应用程序。
Parameters类是Application类的静态内部类,它封装了传递给 JavaFX 应用程序的参数。它将参数分为三类:
-
命名参数
-
未命名参数
-
原始参数(命名和未命名参数的组合)
您需要使用Parameters类的以下三个方法来访问三种类型的参数:
-
Map<String, String> getNamed() -
List<String> getUnnamed() -
List<String> getRaw()
参数可以是命名的,也可以是未命名的。命名参数由(名称,值)对组成。未命名的参数由单个值组成。getNamed()方法返回一个包含名称参数的键值对的Map<String, String>。getUnnamed()方法返回一个List<String>,其中每个元素都是一个未命名的参数值。
您只能将命名和未命名的参数传递给 JavaFX 应用程序。不传递原始类型参数。JavaFX 运行时通过Parameters类的getRaw()方法将所有已命名和未命名的参数作为List<String>传递给应用程序。下面的讨论将使这三种方法的返回值之间的区别变得清晰。
Application类的getParameters()方法返回Application.Parameters类的引用。对Parameters类的引用可以在Application类的init()方法和随后执行的代码中找到。参数在应用程序的构造器中不可用,因为它在init()方法之前被调用。调用构造器中的getParameters()方法返回null。
清单 1-6 中的程序读取传递给应用程序的所有类型的参数,并将它们显示在一个TextArea中。一个TextArea是显示多行文本的 UI 节点。
// FXParamApp.java
package com.jdojo.intro;
import java.util.List;
import java.util.Map;
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.TextArea;
import javafx.stage.Stage;
public class FXParamApp extends Application {
public static void main(String[] args) {
Application.launch(args);
}
@Override
public void start(Stage stage) {
// Get application parameters
Parameters p = this.getParameters();
Map<String, String> namedParams = p.getNamed();
List<String> unnamedParams = p.getUnnamed();
List<String> rawParams = p.getRaw();
String paramStr = "Named Parameters: " + namedParams +
"\n" +
"Unnamed Parameters: " + unnamedParams + "\n" +
"Raw Parameters: " + rawParams;
TextArea ta = new TextArea(paramStr);
Group root = new Group(ta);
stage.setScene(new Scene(root));
stage.setTitle("Application Parameters");
stage.show();
}
}
Listing 1-6Accessing Parameters Passed to a JavaFX Application
让我们看几个将参数传递给FXParamApp类的例子。当您运行FXParamApp类时,以下情况中提到的输出显示在窗口的TextArea控件中。
案例 1
使用以下命令将该类作为独立应用程序运行:
java [options] com.jdojo.stage.FXParamApp Anna Lola
前面的命令没有传递命名参数和两个未命名参数:Anna和Lola。原始参数列表将包含两个未命名的参数。输出将如下所示:
Named Parameters: {}
Unnamed Parameters: [Anna, Lola]
Raw Parameters: [Anna, Lola]
案例 2
要从命令行传递一个命名的参数,您需要在参数前面加两个连字符(--)。也就是说,应该在表单中输入命名参数
--key=value
使用以下命令将该类作为独立应用程序运行:
java [options] com.jdojo.stage.FXParamApp \
Anna Lola --width=200 --height=100
前面的命令传递两个命名参数:width=200和height=100。它传递两个未命名的参数:Anna和Lola。原始参数列表将包含四个元素:两个命名参数和两个未命名参数。原始参数列表中的命名参数值前面有两个连字符。输出将如下所示:
Named Parameters: {height=100, width=200}
Unnamed Parameters: [Anna, Lola]
Raw Parameters: [Anna, Lola, --width=200, --height=100]
启动 JavaFX 应用程序
前面,我谈到了在开发第一个 JavaFX 应用程序时启动 JavaFX 应用程序的主题。本节提供了关于启动 JavaFX 应用程序的更多细节。
每个 JavaFX 应用程序类都继承自Application类。Application级在javafx.application包里。它包含一个静态的launch()方法。它的唯一目的是启动 JavaFX 应用程序。它是一个重载方法,有以下两种变体:
-
static void launch(Class<? extends Application> appClass, String... args) -
static void launch(String... args)
注意,您不需要创建 JavaFX 应用程序类的对象来启动它。当调用launch()方法时,JavaFX 运行时创建应用程序类的一个对象。
Tip
您的 JavaFX 应用程序类必须有一个no-args构造器;否则,当试图启动它时,将会引发运行时异常。
launch()方法的第一个变体很清楚。您将应用程序类的类引用作为第一个参数传递,并且launch()方法将创建该类的一个对象。第二个参数由传递给应用程序的命令行参数组成。下面的代码片段展示了如何使用launch()方法的第一个变体:
public class MyJavaFXApp extends Application {
public static void main(String[] args) {
Application.launch(MyJavaFXApp.class, args);
}
// More code goes here
}
传递给launch()方法的类引用不必与调用该方法的类相同。例如,下面的代码片段从MyAppLauncher类启动MyJavaFXApp应用程序类,它没有扩展Application类:
public class MyAppLauncher {
public static void main(String[] args) {
Application.launch(MyJavaFXApp.class, args);
}
// More code goes here
}
launch()方法的第二个变体只有一个参数,即传递给应用程序的命令行参数。它使用哪个 JavaFX 应用程序类来启动应用程序?它试图根据调用者找到应用程序类名。它检查调用它的代码的类名。如果该方法作为从Application类直接或间接继承的类的代码的一部分被调用,则该类用于启动 JavaFX 应用程序。否则,将引发运行时异常。让我们看一些例子来说明这个规则。
在下面的代码片段中,launch()方法检测到它是从MyJavaFXApp类的main()方法中调用的。MyJavaFXApp类继承自Application类。因此,MyJavaFXApp类被用作应用程序类:
public class MyJavaFXApp extends Application {
public static void main(String[] args) {
Application.launch(args);
}
// More code goes here
}
在下面的代码片段中,从Test类的main()方法调用了launch()方法。Test不从Application类继承。因此,会引发运行时异常,如代码下面的输出所示:
public class Test {
public static void main(String[] args) {
Application.launch(args);
}
// More code goes here
}
Exception in thread "main" java.lang.RuntimeException: Error: class Test is not a subclass of javafx.application.Application
at javafx.application.Application.launch(Application.java:308)
at Test.main(Test.java)
在下面的代码片段中,launch()方法检测到它是从MyJavaFXApp$1类的run()方法中调用的。注意,MyJavaFXApp$1类是编译器生成的匿名内部类,是Object类的子类,而不是Application类,它实现了Runnable接口。因为对launch()方法的调用包含在MyJavaFXApp$1类中,而MyJavaFXApp$1类不是Application类的子类,所以会抛出一个运行时异常,如下面代码的输出所示:
public class MyJavaFXApp extends Application {
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
public void run() {
Application.launch(args);
}
});
t.start();
}
// More code goes here
}
Exception in thread "Thread-0" java.lang.RuntimeException: Error: class MyJavaFXApp$1 is not a subclass of javafx.application.Application
at javafx.application.Application.launch(Application.java:211)
at MyJavaFXApp$1.run(MyJavaFXApp.java)
at java.lang.Thread.run(Thread.java:722)
现在您已经知道了如何启动 JavaFX 应用程序,是时候学习启动 JavaFX 应用程序的最佳实践了:将main()方法中的代码限制为只有一条启动应用程序的语句,如以下代码所示:
public class MyJavaFXApp extends Application {
public static void main(String[] args) {
Application.launch(args);
// Do not add any more code in this method
}
// More code goes here
}
Tip
Application类的launch()方法只能调用一次;否则,将引发运行时异常。对launch()方法的调用会一直阻塞,直到应用程序终止。
JavaFX 应用程序的生命周期
JavaFX 运行时创建几个线程。在应用程序的不同阶段,线程用于执行不同的任务。在这一节中,我将只解释那些在生命周期中用来调用Application类的方法的线程。JavaFX 运行时在其他线程中创建了两个线程:
-
Java FX-启动器
-
JavaFX 应用程序线程
Application类的launch()方法创建这些线程。在 JavaFX 应用程序的生命周期中,JavaFX 运行时按顺序调用指定 JavaFX Application类的以下方法:
-
no-args构造器 -
init()法 -
start()法 -
stop()法
JavaFX 运行时在 JavaFX 应用程序线程上创建指定的Application类的对象。JavaFX 启动器线程调用指定的Application类的init()方法。Application类中的init()方法实现为空。您可以在应用程序类中重写此方法。不允许在 JavaFX 启动器线程上创建Stage或Scene。它们必须在 JavaFX 应用程序线程上创建。因此,不能在init()方法中创建Stage或Scene。试图这样做将引发运行时异常。创建 UI 控件是很好的,例如按钮或形状。
JavaFX 应用程序线程调用指定的Application类的start(Stage stage)方法。注意,Application类中的start()方法被声明为abstract,您必须在您的应用程序类中覆盖这个方法。
此时,launch()方法等待 JavaFX 应用程序完成。当应用完成时,JavaFX 应用线程调用指定的Application类的stop()方法。在Application类中,stop()方法的默认实现是空的。当应用程序停止时,您必须在您的application类中覆盖这个方法来执行您的逻辑。
清单 1-7 中的代码展示了 JavaFX 应用程序的生命周期。它显示一个空的舞台。当显示 stage 时,您将看到输出的前三行。您需要关闭阶段才能看到输出的最后一行。
// FXLifeCycleApp.java
package com.jdojo.intro;
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class FXLifeCycleApp extends Application {
public FXLifeCycleApp() {
String name = Thread.currentThread().getName();
System.out.println("FXLifeCycleApp() constructor: " +
name);
}
public static void main(String[] args) {
Application.launch(args);
}
@Override
public void init() {
String name = Thread.currentThread().getName();
System.out.println("init() method: " + name);
}
@Override
public void start(Stage stage) {
String name = Thread.currentThread().getName();
System.out.println("start() method: " + name);
Scene scene = new Scene(new Group(), 200, 200);
stage.setScene(scene);
stage.setTitle("JavaFX Application Life Cycle");
stage.show();
}
@Override
public void stop() {
String name = Thread.currentThread().getName();
System.out.println("stop() method: " + name);
}
}
FXLifeCycleApp() constructor: JavaFX Application Thread
init() method: JavaFX-Launcher
start() method: JavaFX Application Thread
stop() method: JavaFX Application Thread
Listing 1-7The Life Cycle of a JavaFX Application
终止 JavaFX 应用程序
JavaFX 应用程序可以显式或隐式终止。您可以通过调用Platform.exit()方法显式终止 JavaFX 应用程序。当这个方法被调用时,在start()方法之后或者从该方法内部,调用Application类的stop()方法,然后 JavaFX 应用程序线程被终止。此时,如果只有守护线程在运行,JVM 将退出。如果从构造器或Application类的init()方法调用该方法,则stop()方法可能不会被调用。
当最后一个窗口关闭时,JavaFX 应用程序可以隐式终止。使用Platform类的静态setImplicitExit(boolean implicitExit)方法可以打开和关闭这种行为。将true传递给这个方法可以打开这个行为。将false传递给这个方法可以关闭这个行为。默认情况下,此行为是打开的。这就是为什么在迄今为止的大多数例子中,当你关闭窗口时,应用程序会被终止。当这个行为打开时,在终止 JavaFX 应用程序线程之前,调用Application类的stop()方法。终止 JavaFX 应用程序线程并不总是会终止 JVM。如果所有正在运行的非守护线程都终止了,JVM 也会终止。如果 JavaFX 应用程序的隐式终止行为被关闭,您必须调用Platform类的exit()方法来终止应用程序。
摘要
JavaFX 是一个开源的基于 Java 的 GUI 框架,用于开发富客户端应用程序。它是 Swing 在 Java 平台 GUI 开发技术领域的继承者。
JavaFX 中的 GUI 分阶段显示。stage 是Stage类的一个实例。舞台是桌面应用程序中的一个窗口。一个舞台包含一个场景。场景包含一组以树状结构排列的节点(图形)。
JavaFX 应用程序继承自Application类。JavaFX 运行时创建称为初级阶段的第一个阶段,并调用应用程序类的start()方法,传递初级阶段的引用。开发人员需要向舞台添加一个场景,并在start()方法中使舞台可见。
您可以使用Application类的launch()方法启动 JavaFX 应用程序。
在 JavaFX 应用程序的生命周期中,JavaFX 运行时以特定的顺序调用 JavaFX Application类的预定义方法。首先,调用该类的no-args构造器,然后调用init()和start()方法。当应用程序终止时,会调用 stop()方法。您可以通过调用Platform.exit()方法来终止 JavaFX 应用程序。
下一章将向您介绍 JavaFX 中的属性和绑定。
二、属性和绑定
在本章中,您将学习:
-
JavaFX 中的属性是什么
-
如何创建属性对象并使用它
-
JavaFX 中属性的类层次结构
-
如何处理属性对象中的失效和更改事件
-
JavaFX 中的绑定是什么,以及如何使用单向和双向绑定
-
关于 JavaFX 中的高级和低级绑定 API
本章讨论 Java 和 JavaFX 中的属性和绑定支持。如果您有使用 JavaBeans API 进行属性和绑定的经验,可以跳过前面几节,这几节讨论了 Java 中的属性和绑定支持,从“理解 JavaFX 中的属性”一节开始。
本章的例子在com.jdojo.binding包中。为了让它们工作,您必须在module-info.java文件中添加相应的一行:
...
opens com.jdojo.binding to javafx.graphics, javafx.base;
...
什么是财产?
一个 Java 类可以包含两类成员:字段和方法。字段代表对象的状态,它们被声明为私有的。公共方法被称为访问器或获取器和设置器,用于读取和修改私有字段。简单地说,对于所有或部分私有字段,具有公共访问器的 Java 类被称为 Java bean ,访问器定义了 bean 的属性。Java bean 的属性允许用户定制其状态、行为或两者。
Java beans 是可观察的。它们支持属性更改通知。当 Java bean 的公共属性发生变化时,会向所有感兴趣的侦听器发送通知。
本质上,Java beans 定义了可重用的组件,这些组件可以由构建器工具组装起来以创建 Java 应用程序。这为第三方开发 JavaBean 并使其可供他人重用打开了大门。
属性可以是只读、只写或读/写。只读属性有 getter,但没有 setter。只写属性有 setter,但没有 getter。读/写属性有一个 getter 和一个 setter。
Java IDEs 和其他构建工具(例如,GUI 布局构建器)使用自省来获取 bean 的属性列表,并允许您在设计时操作这些属性。Java bean 可以是可视的,也可以是不可视的。bean 的属性可以在构建工具中使用,也可以以编程方式使用。
JavaBeans API 提供了一个类库,通过java.beans包和命名约定来创建和使用 JavaBeans。下面是一个具有读/写name属性的Person bean 的例子。getName()方法(getter)返回name字段的值。setName()方法(setter)设置name字段的值:
// Person.java
package com.jdojo.binding;
public class Person {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
按照惯例,getter 和 setter 方法的名称是通过将属性名称的第一个字母大写,分别附加到单词 get 和 set 来构造的。getter 方法不应该带任何参数,它的返回类型应该与字段的类型相同。setter 方法应该带一个参数,参数的类型应该和字段的类型相同,其返回类型应该是void。
以下代码片段以编程方式操作Person bean 的name属性:
Person p = new Person();
p.setName("John Jacobs");
String name = p.getName();
一些面向对象的编程语言,例如 C#,提供了第三种类型的类成员,称为属性。属性用于从类外部读取、写入和计算私有字段的值。C#允许您声明一个带有Name属性的Person类,如下所示:
// C# version of the Person class
public class Person {
private string name;
public string Name {
get { return name; }
set { name = value; }
}
}
在 C#中,以下代码片段使用Name属性操作name私有字段;它相当于前面显示的 Java 版本的代码:
Person p = new Person();
p.Name = "John Jacobs";
string name = p.Name;
如果属性的访问器执行返回和设置字段值的例行工作,C#提供了一种紧凑的格式来定义这样的属性。在这种情况下,您甚至不需要声明私有字段。您可以用 C#重写Person类,如下所示:
// C# version of the Person class using the compact format
public class Person {
public string Name { get; set; }
}
那么,什么是财产呢?一个属性是一个类的公共可访问属性,影响它的状态、行为或两者。即使属性是可公开访问的,它的使用(读/写)也会调用隐藏实际实现的方法来访问数据。属性是可观察的,所以当它的值改变时,感兴趣的人会得到通知。
Tip
本质上,属性定义了对象的公共状态,可以读取、写入和观察对象的变化。与其他编程语言(如 C#)不同,Java 中的属性在语言级别不受支持。Java 对属性的支持来自 JavaBeans API 和设计模式。关于 Java 中属性的更多细节,请参考 JavaBeans 规范,可以从 www.oracle.com/java/technologies/javase/javabeans-spec.html 下载。
除了简单的属性,比如Person bean 的name属性,Java 还支持索引、绑定和约束属性。索引属性是使用索引访问的值的数组。索引属性是使用数组数据类型实现的。当绑定属性发生更改时,它会向所有侦听器发送通知。受约束的属性是侦听器可以否决更改的绑定属性。
什么是绑定?
在编程中,术语绑定被用在许多不同的上下文中。在这里,我想在数据绑定的上下文中定义它。数据绑定定义了程序中数据元素(通常是变量)之间的关系,以保持它们的同步。在 GUI 应用程序中,数据绑定经常用于将数据模型中的元素与相应的 UI 元素同步。
考虑以下语句,假设 x、y 和 z 是数值变量:
x = y + z;
前面的语句定义了 x、y 和 z 之间的绑定。当执行该语句时,x 的值与 y 和 z 的总和同步。绑定还具有时间因子。在前面的语句中,x 的值绑定到 y 和 z 的和,并且在语句执行时有效。在执行前面的语句之前和之后,x 的值可能不是 y 和 z 的和。
有时,希望绑定保持一段时间。考虑以下使用listPrice、discounts和taxes定义绑定的语句:
soldPrice = listPrice - discounts + taxes;
在这种情况下,您希望保持绑定永远有效,这样无论何时listPrice、discounts或taxes发生变化,销售价格都会被正确计算。
在前面的绑定中,listPrice、discounts、taxes被称为依赖,也就是说soldPrice被绑定到listPrice、discounts、taxes。
为了使绑定正常工作,有必要在依赖关系发生变化时通知绑定。支持绑定的编程语言提供了一种用依赖关系注册侦听器的机制。当依赖关系变得无效或改变时,所有侦听器都会得到通知。当绑定接收到这样的通知时,它可以将其自身与其依赖项同步。
绑定可以是急切绑定或懒惰绑定。在急切绑定中,绑定变量在其依赖关系更改后会立即重新计算。在惰性绑定中,当绑定变量的依赖关系改变时,不会重新计算绑定变量。而是在下次读取时重新计算。与急切绑定相比,惰性绑定的性能更好。
绑定可以是单向或双向。单向绑定只在一个方向起作用;依赖关系中的更改会传播到绑定变量。双向绑定在两个方向上都起作用。在双向绑定中,绑定变量和依赖项保持它们的值相互同步。通常,双向绑定只在两个变量之间定义。例如,双向绑定 x = y 和 y = x 声明 x 和 y 的值总是相同的。
从数学上讲,不可能唯一地定义多个变量之间的双向绑定。在前面的示例中,销售价格绑定是单向绑定。如果您想使它成为一个双向绑定,那么当销售价格发生变化时,不可能唯一地计算标价、折扣和税的值。在另一个方向有无限多的可能性。
具有 GUI 的应用程序为用户提供 UI 部件,例如文本字段、复选框和按钮,以操作数据。UI 小部件中显示的数据必须与底层数据模型同步,反之亦然。在这种情况下,需要双向绑定来保持 UI 和数据模型同步。
了解 JavaBeans 中的绑定支持
在我讨论 JavaFX 属性和绑定之前,让我们先简单了解一下 JavaBeans API 中的绑定支持。如果您以前使用过 JavaBeans API,您可以跳过这一节。
从早期版本开始,Java 就支持 bean 属性的绑定。清单 2-1 显示了一个具有两个属性name和salary的Employee bean。
// Employee.java
package com.jdojo.binding;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
public class Employee {
private String name;
private double salary;
private PropertyChangeSupport pcs = new PropertyChangeSupport(this);
public Employee() {
this.name = "John Doe";
this.salary = 1000.0;
}
public Employee(String name, double salary) {
this.name = name;
this.salary = salary;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getSalary() {
return salary;
}
public void setSalary(double newSalary) {
double oldSalary = this.salary;
this.salary = newSalary;
// Notify the registered listeners about the change
pcs.firePropertyChange("salary", oldSalary, newSalary);
}
public void addPropertyChangeListener(
PropertyChangeListener listener) {
pcs.addPropertyChangeListener(listener);
}
public void removePropertyChangeListener(
PropertyChangeListener listener) {
pcs.removePropertyChangeListener(listener);
}
@Override
public String toString() {
return "name = " + name + ", salary = " + salary;
}
}
Listing 2-1An Employee Java Bean with Two Properties Named name and salary
Employee bean 的两个属性都是读/写的。salary属性也是一个绑定属性。它的设置器在薪水变化时生成属性变化通知。
感兴趣的监听器可以使用addPropertyChangeListener()和removePropertyChangeListener()方法注册或取消注册变更通知。PropertyChangeSupport类是 JavaBeans API 的一部分,它简化了属性更改监听器的注册和删除以及属性更改通知的触发。
任何对基于工资变化的同步值感兴趣的一方都需要向Employee bean 注册,并在收到变化通知时采取必要的行动。
清单 2-2 展示了如何为一个Employee bean 注册工资变化通知。下面的输出显示工资变化通知只触发了两次,而setSalary()方法被调用了三次。这是真的,因为对setSalary()方法的第二次调用使用了与第一次调用相同的工资金额,而PropertyChangeSupport类足够聪明,能够检测到这一点。该示例还展示了如何使用 JavaBeans API 绑定变量。员工的税款是根据纳税百分比计算的。在 JavaBeans API 中,属性更改通知用于绑定变量。
// EmployeeTest.java
package com.jdojo.binding;
import java.beans.PropertyChangeEvent;
public class EmployeeTest {
public static void main(String[] args) {
final Employee e1 = new Employee("John Jacobs", 2000.0);
// Compute the tax
computeTax(e1.getSalary());
// Add a property change listener to e1
e1.addPropertyChangeListener(
EmployeeTest::handlePropertyChange);
// Change the salary
e1.setSalary(3000.00);
e1.setSalary(3000.00); // No change notification is sent.
e1.setSalary(6000.00);
}
public static void handlePropertyChange(PropertyChangeEvent e) {
String propertyName = e.getPropertyName();
if ("salary".equals(propertyName)) {
System.out.print("Salary has changed. ");
System.out.print("Old:" + e.getOldValue());
System.out.println(", New:" +
e.getNewValue());
computeTax((Double)e.getNewValue());
}
}
public static void computeTax(double salary) {
final double TAX_PERCENT = 20.0;
double tax = salary * TAX_PERCENT/100.0;
System.out.println("Salary:" + salary + ", Tax:" + tax);
}
}
Salary:2000.0, Tax:400.0
Salary has changed. Old:2000.0, New:3000.0
Salary:3000.0, Tax:600.0
Salary has changed. Old:3000.0, New:6000.0
Salary:6000.0, Tax:1200.0
Listing 2-2An EmployeeTest Class That Tests the Employee Bean for Salary Changes
了解 JavaFX 中的属性
JavaFX 通过属性和绑定API 支持属性、事件和绑定。JavaFX 中的属性支持是 JavaBeans 属性的巨大飞跃。
JavaFX 中的所有属性都是可观察的。可以观察到它们的失效和值的变化。可以有读/写或只读属性。所有读/写属性都支持绑定。
在 JavaFX 中,属性可以表示一个值或一组值。本章介绍代表单个值的属性。我将在第三章中介绍代表一组值的属性。
在 JavaFX 中,属性是对象。每种属性都有一个属性类层次结构。例如,IntegerProperty、DoubleProperty和StringProperty类分别代表int、double和String类型的属性。这些类是abstract。它们有两种类型的实现类:一种表示读/写属性,另一种表示只读属性的包装。例如,SimpleDoubleProperty和ReadOnlyDoubleWrapper类是具体的类,它们的对象分别用作读/写和只读双精度属性。
以下是如何创建初始值为 100 的IntegerProperty的示例:
IntegerProperty counter = new SimpleIntegerProperty(100);
属性类提供了两对 getter 和 setter 方法:get() / set()和getValue() / setValue()。get()和set()方法分别获取和设置属性的值。对于基本类型属性,它们使用基本类型值。比如对于IntegerProperty,get()方法的返回类型和set()方法的参数类型都是int。getValue()和setValue()方法处理一个对象类型;例如,对于IntegerProperty,它们的返回类型和参数类型是Integer。
Tip
对于引用类型属性,比如StringProperty和ObjectProperty<T>,两对 getter 和 setter 都使用一个对象类型。也就是说,StringProperty的get()和getValue()方法都返回一个String,而set()和setValue()方法都带有一个String参数。对于基元类型的自动装箱,使用哪个版本的 getter 和 setter 并不重要。getValue()和setValue()方法的存在是为了帮助你根据对象类型编写通用代码。
下面的代码片段使用了一个IntegerProperty及其get()和set()方法。counter属性是读/写属性,因为它是SimpleIntegerProperty类的对象:
IntegerProperty counter = new SimpleIntegerProperty(1);
int counterValue = counter.get();
System.out.println("Counter:" + counterValue);
counter.set(2);
counterValue = counter.get();
System.out.println("Counter:" + counterValue);
Counter:1
Counter:2
使用只读属性有点棘手。一个ReadOnlyXXXWrapper类包装了XXX类型的两个属性:一个只读,一个读/写。两种属性都是同步的。它的getReadOnlyProperty()方法返回一个ReadOnlyXXXProperty对象。
下面的代码片段展示了如何创建一个只读的Integer属性。属性是读/写的,而属性是只读的。当idWrapper中的值改变时,id中的值自动改变:
ReadOnlyIntegerWrapper idWrapper = new ReadOnlyIntegerWrapper(100);
ReadOnlyIntegerProperty id = idWrapper.getReadOnlyProperty();
System.out.println("idWrapper:" + idWrapper.get());
System.out.println("id:" + id.get());
// Change the value
idWrapper.set(101);
System.out.println("idWrapper:" + idWrapper.get());
System.out.println("id:" + id.get());
idWrapper:100
id:100
idWrapper:101
id:101
Tip
通常,包装属性用作类的私有实例变量。类别可以在内部变更属性。它的一个方法返回包装类的只读属性对象,因此同一个属性对于外界是只读的。
可以使用代表单个值的七种类型的属性。这些属性的基类被命名为XXXProperty,只读基类被命名为ReadOnlyXXXProperty,包装类被命名为ReadOnlyXXXWrapper。每种类型的XXX值列于表 2-1 中。
表 2-1
包装单个值的属性类列表
|类型
|
XXX 值
|
| --- | --- |
| int | Integer |
| long | Long |
| float | Float |
| double | Double |
| boolean | Boolean |
| String | String |
| Object | Object |
属性对象包装了三条信息:
-
包含它的 bean 的引用
-
一个名字
-
一种价值观
创建属性对象时,可以提供前面三条信息的全部,也可以不提供。像SimpleXXXProperty和ReadOnlyXXXWrapper这样命名的具体属性类提供了四个构造器,让您提供这三条信息的组合。下面是SimpleIntegerProperty类的构造器:
SimpleIntegerProperty()
SimpleIntegerProperty(int initialValue)
SimpleIntegerProperty(Object bean, String name)
SimpleIntegerProperty(Object bean, String name, int initialValue)
初始值的默认值取决于属性的类型。对于数值类型是零,对于布尔类型是false,对于引用类型是null。
属性对象可以是 bean 的一部分,也可以是独立的对象。指定的bean是对包含该属性的 bean 对象的引用。对于独立的属性对象,可以是null。其默认值为null。
属性的名字就是它的名字。如果未提供,则默认为空字符串。
下面的代码片段创建一个属性对象作为 bean 的一部分,并设置所有三个值。SimpleStringProperty类的构造器的第一个参数是this,它是Person bean 的引用,第二个参数—"name"—是属性的名称,第三个参数—"Li"—是属性的值:
public class Person {
private StringProperty name = new SimpleStringProperty(
this, "name", "Li");
// More code goes here...
}
每个属性类都有分别返回 bean 引用和属性名的getBean()和getName()方法。
在 JavaFX Beans 中使用属性
在上一节中,您看到了 JavaFX 属性作为独立对象的使用。在本节中,您将在类中使用它们来定义属性。让我们创建一个具有三个属性的Book类:ISBN、title和price,将使用 JavaFX 属性类对其进行建模。
在 JavaFX 中,不将类的属性声明为基本类型之一。相反,您使用 JavaFX 属性类之一。Book类的title属性将声明如下。照常宣布private:
public class Book {
private StringProperty title = new SimpleStringProperty(this,
"title", "Unknown");
}
您为属性声明了一个公共 getter,按照惯例,它被命名为XXXProperty,其中XXX是属性的名称。这个 getter 返回属性的引用。对于我们的title属性,getter 将被命名为titleProperty,如下所示:
public class Book {
private StringProperty title = new SimpleStringProperty(this,
"title", "Unknown");
public final StringProperty titleProperty() {
return title;
}
}
前面的Book类声明可以很好地处理title属性,如下面设置和获取书名的代码片段所示:
Book b = new Book();
b.titleProperty().set("Harnessing JavaFX 17.0");
String title = b.titleProperty().get();
根据 JavaFX 设计模式,而不是任何技术要求,JavaFX 属性有一个 getter 和 setter,类似于 JavaBeans 中的 getter 和 setter。getter 的返回类型和 setter 的参数类型与属性值的类型相同。比如对于StringProperty和IntegerProperty,分别会是String和int。title属性的getTitle()和setTitle()方法声明如下:
public class Book {
private StringProperty title = new SimpleStringProperty(this,
"title", "Unknown");
public final StringProperty titleProperty() {
return title;
}
public final String getTitle() {
return title.get();
}
public final void setTitle(String title) {
this.title.set(title);
}
}
注意,getTitle()和setTitle()方法在内部使用title属性对象来获取和设置标题值。
Tip
按照惯例,类的属性的 getters 和 setters 被声明为final。添加了使用 JavaBeans 命名约定的额外的 getters 和 setters,以使该类能够与使用旧 JavaBeans 命名约定来标识类属性的旧工具和框架进行互操作。
以下代码片段显示了对Book类的只读ISBN属性的声明:
public class Book {
private ReadOnlyStringWrapper ISBN =
new ReadOnlyStringWrapper(this, "ISBN", "Unknown");
public final String getISBN() {
return ISBN.get();
}
public final ReadOnlyStringProperty ISBNProperty() {
return ISBN.getReadOnlyProperty();
}
// More code goes here...
}
关于只读ISBN属性的声明,请注意以下几点:
-
它使用了
ReadOnlyStringWrapper类而不是SimpleStringProperty类。 -
属性值没有设置器。你可以声明一个;但是,必须是私人的。
-
属性值的 getter 与读/写属性的 getter 工作方式相同。
-
ISBNProperty()方法使用ReadOnlyStringProperty作为返回类型,而不是ReadOnlyStringWrapper。它从包装对象获取属性对象的只读版本,并返回该版本。
对于Book类的用户,它的ISBN属性是只读的。但是,它可以在内部进行更改,并且该更改将自动反映在 property 对象的只读版本中。
清单 2-3 显示了Book类的完整代码。
// Book.java
package com.jdojo.binding;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
public class Book {
private StringProperty title = new SimpleStringProperty(this,
"title", "Unknown");
private DoubleProperty price = new SimpleDoubleProperty(this,
"price", 0.0);
private ReadOnlyStringWrapper ISBN = new ReadOnlyStringWrapper(this,
"ISBN", "Unknown");
public Book() {
}
public Book(String title, double price, String ISBN) {
this.title.set(title);
this.price.set(price);
this.ISBN.set(ISBN);
}
public final String getTitle() {
return title.get();
}
public final void setTitle(String title) {
this.title.set(title);
}
public final StringProperty titleProperty() {
return title;
}
public final double getprice() {
return price.get();
}
public final void setPrice(double price) {
this.price.set(price);
}
public final DoubleProperty priceProperty() {
return price;
}
public final String getISBN() {
return ISBN.get();
}
public final ReadOnlyStringProperty ISBNProperty() {
return ISBN.getReadOnlyProperty();
}
}
Listing 2-3A Book Class with Two Read/Write and a Read-Only Properties
清单 2-4 测试了Book类的属性。它创建一个Book对象,打印细节,更改一些属性,然后再次打印细节。注意printDetails()方法的ReadOnlyProperty参数类型的使用。所有的属性类都直接或间接地实现了ReadOnlyProperty接口。
属性实现类的toString()方法返回一个格式良好的字符串,该字符串包含属性的所有相关信息。我没有使用 property 对象的toString()方法,因为我想向您展示 JavaFX 属性的不同方法的用法。
// BookPropertyTest.java
package com.jdojo.binding;
import javafx.beans.property.ReadOnlyProperty;
public class BookPropertyTest {
public static void main(String[] args) {
Book book = new Book("Harnessing JavaFX", 9.99,
"0123456789");
System.out.println("After creating the Book object...");
// Print Property details
printDetails(book.titleProperty());
printDetails(book.priceProperty());
printDetails(book.ISBNProperty());
// Change the book's properties
book.setTitle("Harnessing JavaFX 17.0");
book.setPrice(9.49);
System.out.println(
"\nAfter changing the Book properties...");
// Print Property details
printDetails(book.titleProperty());
printDetails(book.priceProperty());
printDetails(book.ISBNProperty());
}
public static void printDetails(ReadOnlyProperty<?> p) {
String name = p.getName();
Object value = p.getValue();
Object bean = p.getBean();
String beanClassName = (bean == null)?
"null":bean.getClass().getSimpleName();
String propClassName = p.getClass().getSimpleName();
System.out.print(propClassName);
System.out.print("[Name:" + name);
System.out.print(", Bean Class:" + beanClassName);
System.out.println(", Value:" + value + "]");
}
}
After creating the Book object...
SimpleStringProperty[Name:title, Bean Class:Book, Value:Harnessing JavaFX]
SimpleDoubleProperty[Name:price, Bean Class:Book, Value:9.99]
ReadOnlyPropertyImpl[Name:ISBN, Bean Class:Book, Value:0123456789]
After changing the Book properties...
SimpleStringProperty[Name:title, Bean Class:Book, Value:Harnessing JavaFX 17.0]
SimpleDoubleProperty[Name:price, Bean Class:Book, Value:9.49]
ReadOnlyPropertyImpl[Name:ISBN, Bean Class:Book, Value:0123456789]
Listing 2-4A Test Class to Test Properties of the Book Class
了解属性类层次结构
在开始使用 JavaFX 属性和绑定 API 之前,理解它们的一些核心类和接口非常重要。图 2-1 显示了 properties API 核心接口的类图。你不需要在你的程序中直接使用这些接口。这些接口的专用版本和实现它们的类是存在的,并且可以直接使用。
图 2-1
JavaFX 属性 API 中核心接口的类图
JavaFX 属性 API 中的类和接口分布在不同的包中。那些包是javafx.beans、javafx.beans.binding、javafx.beans.property和javafx.beans.value。
Observable接口位于属性 API 的顶部。一个Observable包装内容,可以观察到它的内容失效。Observable接口有两个方法来支持这一点。它的addListener()方法允许您添加一个InvalidationListener。当Observable的内容无效时,调用InvalidationListener的invalidated()方法。可以使用removeListener()方法移除InvalidationListener。
Tip
所有 JavaFX 属性都是可观察的。
只有当其内容的状态从有效变为无效时,Observable才会生成无效事件。也就是说,一行中的多个失效应该只生成一个失效事件。JavaFX 中的属性类遵循这个原则。
Tip
一个Observable产生一个失效事件并不一定意味着它的内容发生了变化。意思就是它的内容因为某种原因是无效的。例如,对一个ObservableList进行排序可能会生成一个无效事件。排序不会改变列表的内容;它只是对内容进行了重新排序。
ObservableValue接口继承自Observable接口。一个ObservableValue包装了一个值,可以观察到它的变化。它有一个getValue()方法,返回它包装的值。它生成失效事件和变更事件。当ObservableValue中的值不再有效时,生成失效事件。值更改时会生成更改事件。您可以将一个ChangeListener注册到一个ObservableValue。每当ChangeListener的值发生变化时,就会调用changed()方法。changed()方法接收三个参数:对ObservableValue的引用、旧值和新值。
一个ObservableValue可以缓慢或急切地重新计算它的值。在惰性策略中,当它的值变得无效时,它不知道该值是否已经改变,直到该值被重新计算;下次读取该值时会重新计算。例如,使用一个ObservableValue的getValue()方法会使它重新计算它的值,如果这个值是无效的并且它使用了一个懒惰策略。在 eager 策略中,一旦值变得无效,就会重新计算。
为了生成无效事件,一个ObservableValue可以使用惰性或急切评估。懒惰评估更有效率。然而,生成变更事件会迫使一个ObservableValue立即重新计算它的值(一个急切的评估),因为它必须将新值传递给注册的变更监听器。
ReadOnlyProperty接口增加了getBean()和getName()方法。清单 2-4 展示了它们的用法。getBean()方法返回包含属性对象的 bean 的引用。getName()方法返回属性的名称。只读属性实现此接口。
一个WritableValue包装了一个值,可以分别使用它的getValue()和setValue()方法读取和设置该值。读/写属性实现此接口。
Property接口继承自ReadOnlyProperty和WritableValue接口。它添加了以下五种方法来支持绑定:
-
void bind(ObservableValue<? extends T> observable) -
void unbind() -
void bindBidirectional(Property<T> other) -
void unbindBidirectional(Property<T> other) -
boolean isBound()
bind()方法在这个Property和指定的ObservableValue之间添加一个单向绑定。如果存在的话,unbind()方法删除这个Property的单向绑定。
bindBidirectional()方法在这个Property和指定的Property之间创建一个双向绑定。unbindBidirectional()方法移除双向绑定。
注意bind()和bindBidirectional()方法的参数类型的不同。同一类型的Property和ObservableValue之间可以创建单向绑定,只要它们通过继承相关。但是,只能在同一类型的两个属性之间创建双向绑定。
如果Property被绑定,isBound()方法返回true。否则返回false。
Tip
所有读/写 JavaFX 属性都支持绑定。
图 2-2 显示了 JavaFX 中 integer 属性的部分类图。该图让您了解 JavaFX 属性 API 的复杂性。您不需要学习属性 API 中的所有类。在您的应用程序中,您将只使用其中的几个。
图 2-2
整数属性的类图
处理属性失效事件
当属性值的状态第一次从有效变为无效时,属性会生成一个无效事件。JavaFX 中的属性使用惰性计算。当无效属性再次变为无效时,不会生成失效事件。无效属性在重新计算时变得有效,例如,通过调用其get()或getValue()方法。
清单 2-5 提供了程序来演示何时为属性生成失效事件。这个程序包含了足够的注释来帮助你理解它的逻辑。一开始,它创建一个名为counter的IntegerProperty:
IntegerProperty counter = new SimpleIntegerProperty(100);
一个InvalidationListener被添加到counter属性:
counter.addListener(InvalidationTest::invalidated);
当您创建属性对象时,它是有效的。当您将counter属性更改为 101 时,它会触发一个失效事件。此时,counter属性变得无效。当您将它的值更改为 102 时,它不会触发无效事件,因为它已经无效了。当您使用get()方法读取counter值时,它再次变得有效。现在您为counter设置了相同的值 102,它不会触发一个无效事件,因为该值并没有真正改变。counter属性仍然有效。最后,您将它的值改为一个不同的值,果然,一个无效事件被触发。
Tip
您并不局限于在一个属性中只添加一个失效侦听器。您可以根据需要添加任意数量的失效侦听器。一旦你完成了一个无效监听器,确保通过调用Observable接口的removeListener()方法来移除它;否则,可能会导致内存泄漏。
// InvalidationTest.java
// Listing part of the example sources download for the book
Before changing the counter value-1
Counter is invalid.
After changing the counter value-1
Before changing the counter value-2
After changing the counter value-2
Counter value = 102
Before changing the counter value-3
After changing the counter value-3
Before changing the counter value-4
Counter is invalid.
After changing the counter value-4
Listing 2-5Testing Invalidation Events for Properties
处理属性更改事件
您可以注册一个ChangeListener来接收关于属性更改事件的通知。每次属性值更改时,都会触发属性更改事件。一个ChangeListener的changed()方法接收三个值:属性对象的引用、旧值和新值。
让我们运行一个类似的测试用例来测试属性变更事件,就像上一节中对失效事件所做的那样。清单 2-6 中的程序演示了为属性生成的变更事件。
// ChangeTest.java
package com.jdojo.binding;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.value.ObservableValue;
public class ChangeTest {
public static void main(String[] args) {
IntegerProperty counter = new SimpleIntegerProperty(100);
// Add a change listener to the counter property
counter.addListener(ChangeTest::changed);
System.out.println("\nBefore changing the counter value-1");
counter.set(101);
System.out.println("After changing the counter value-1");
System.out.println("\nBefore changing the counter value-2");
counter.set(102);
System.out.println("After changing the counter value-2");
// Try to set the same value
System.out.println("\nBefore changing the counter value-3");
counter.set(102); // No change event is fired.
System.out.println("After changing the counter value-3");
// Try to set a different value
System.out.println("\nBefore changing the counter value-4");
counter.set(103);
System.out.println("After changing the counter value-4");
}
public static void changed(ObservableValue<? extends Number> prop,
Number oldValue,
Number newValue) {
System.out.print("Counter changed: ");
System.out.println("Old = " + oldValue +
", new = " + newValue);
}
}
Before changing the counter value-1
Counter changed: Old = 100, new = 101
After changing the counter value-1
Before changing the counter value-2
Counter changed: Old = 101, new = 102
After changing the counter value-2
Before changing the counter value-3
After changing the counter value-3
Before changing the counter value-4
Counter changed: Old = 102, new = 103
After changing the counter value-4
Listing 2-6Testing Change Events for Properties
一开始,程序创建一个名为counter的IntegerProperty:
IntegerProperty counter = new SimpleIntegerProperty(100);
加个ChangeListener有个小技巧。IntegerPropertyBase类中的addListener()方法声明如下:
void addListener(ChangeListener<? super Number> listener)
这意味着如果你使用泛型,那么一个IntegerProperty的ChangeListener必须按照Number类或者Number类的超类来编写。向counter属性添加ChangeListener的三种方法如下所示:
// Method-1: Using generics and the Number class
counter.addListener(new ChangeListener<Number>() {
@Override
public void changed(ObservableValue<? extends Number> prop,
Number oldValue,
Number newValue) {
System.out.print("Counter changed: ");
System.out.println("Old = " + oldValue +
", new = " + newValue);
}});
// Method-2: Using generics and the Object class
counter.addListener( new ChangeListener<Object>() {
@Override
public void changed(ObservableValue<? extends Object> prop,
Object oldValue,
Object newValue) {
System.out.print("Counter changed: ");
System.out.println("Old = " + oldValue +
", new = " + newValue);
}});
// Method-3: Not using generics. It may generate compile-time warnings.
counter.addListener(new ChangeListener() {
@Override
public void changed(ObservableValue prop,
Object oldValue,
Object newValue) {
System.out.print("Counter changed: ");
System.out.println("Old = " + oldValue +
", new = " + newValue);
}});
清单 2-6 使用了第一种方法,它利用了泛型;如您所见,ChangeTest类中的changed()方法的签名与method-1中的changed()方法签名相匹配。我使用了一个带有方法引用的 lambda 表达式来添加一个ChangeListener,如下所示:
counter.addListener(ChangeTest::changed);
前面的输出显示,当属性值更改时,将触发属性更改事件。用相同的值调用set()方法不会触发属性更改事件。
与生成失效事件不同,属性使用对其值的急切评估来生成更改事件,因为它必须将新值传递给属性更改侦听器。下一节讨论属性对象如何评估它的值,如果它既有无效侦听器又有更改侦听器的话。
处理失效和变更事件
当您必须决定是使用失效侦听器还是更改侦听器时,您需要考虑性能。通常,失效侦听器比更改侦听器性能更好。原因是双重的:
-
失效侦听器使得延迟计算值成为可能。
-
一行中的多个无效仅触发一个无效事件。
但是,使用哪个监听器取决于当前的情况。一个经验法则是,如果您在失效事件处理程序中读取属性的值,您应该使用一个更改侦听器。当您读取失效侦听器中的属性值时,它会触发该值的重新计算,这是在触发更改事件之前自动完成的。如果不需要读取属性的值,请使用失效侦听器。
清单 2-7 有一个程序向IntegerProperty添加一个无效监听器和一个变更监听器。这个程序是清单 2-5 和 2-6 的组合。下面的输出显示,当属性值改变时,失效和改变这两个事件总是被触发。这是因为更改事件会在更改后立即使属性有效,并且值的下一次更改会触发一个无效事件,当然还有一个更改事件。
// ChangeAndInvalidationTest.java
// Listing part of the example sources download for the book
Before changing the counter value-1
Counter is invalid.
Counter changed: old = 100, new = 101
After changing the counter value-1
Before changing the counter value-2
Counter is invalid.
Counter changed: old = 101, new = 102
After changing the counter value-2
Before changing the counter value-3
After changing the counter value-3
Before changing the counter value-4
Counter is invalid.
Counter changed: old = 102, new = 103
After changing the counter value-4
Listing 2-7Testing Invalidation and Change Events for Properties Together
在 JavaFX 中使用绑定
在 JavaFX 中,绑定是一个计算结果为值的表达式。它由一个或多个被称为其依赖性的可观察值组成。绑定观察其依赖关系的变化,并自动重新计算其值。JavaFX 对所有绑定都使用惰性求值。当绑定最初被定义或者当它的依赖关系改变时,它的值被标记为无效。无效绑定的值在下次被请求时计算,通常使用它的get()或getValue()方法。JavaFX 中的所有属性类都内置了对绑定的支持。
让我们看一个 JavaFX 中绑定的简单例子。考虑以下表示两个整数 x 和 y 之和的表达式:
x + y
表达式 x + y 表示一个绑定,它有两个依赖项:x 和 y
sum = x + y
为了在 JavaFX 中实现前面的逻辑,需要创建两个IntegerProperty变量:x和y:
IntegerProperty x = new SimpleIntegerProperty(100);
IntegerProperty y = new SimpleIntegerProperty(200);
以下语句创建了一个名为sum的绑定,表示x和y的总和:
NumberBinding sum = x.add(y);
一个绑定有一个isValid()方法,如果它有效,则返回true;否则,它返回false。您可以使用方法intValue()、longValue()、floatValue()和doubleValue()分别获得NumberBinding的值,如int、long、float和double。
清单 2-8 中的程序展示了如何基于前面的讨论创建和使用绑定。当 sum 绑定被创建时,它是无效的,并且它不知道它的值。从输出中可以明显看出这一点。一旦您使用sum.initValue()方法请求了它的值,它就会计算它的值并将自己标记为有效。当您更改它的一个依赖项时,它将变得无效,直到您再次请求它的值。
// BindingTest.java
package com.jdojo.binding;
import javafx.beans.binding.NumberBinding;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
public class BindingTest {
public static void main(String[] args) {
IntegerProperty x = new SimpleIntegerProperty(100);
IntegerProperty y = new SimpleIntegerProperty(200);
// Create a binding: sum = x + y
NumberBinding sum = x.add(y);
System.out.println("After creating sum");
System.out.println("sum.isValid(): " + sum.isValid());
// Let us get the value of sum, so it computes its value and
// becomes valid
int value = sum.intValue();
System.out.println("\nAfter requesting value");
System.out.println("sum.isValid(): " + sum.isValid());
System.out.println("sum = " + value);
// Change the value of x
x.set(250);
System.out.println("\nAfter changing x");
System.out.println("sum.isValid(): " + sum.isValid());
// Get the value of sum again
value = sum.intValue();
System.out.println("\nAfter requesting value");
System.out.println("sum.isValid(): " + sum.isValid());
System.out.println("sum = " + value);
}
}
After creating sum
sum.isValid(): false
After requesting value
sum.isValid(): true
sum = 300
After changing x
sum.isValid(): false
After requesting value
sum.isValid(): true
sum = 450
Listing 2-8Using a Simple Binding
一个绑定在内部将失效侦听器添加到它的所有依赖项中(清单 2-9 )。当它的任何依赖项无效时,它会将自己标记为无效。无效的绑定并不意味着它的值已经改变。这意味着下次请求值时,它需要重新计算它的值。
在 JavaFX 中,还可以将属性绑定到绑定。回想一下,绑定是一个自动与其依赖项同步的表达式。使用此定义,绑定属性是其值基于表达式计算的属性,当依赖关系更改时,该属性会自动同步。假设您有三个属性,x、y 和 z,如下所示:
IntegerProperty x = new SimpleIntegerProperty(10);
IntegerProperty y = new SimpleIntegerProperty(20);
IntegerProperty z = new SimpleIntegerProperty(60);
您可以使用Property接口的bind()方法将属性z绑定到表达式x + y,如下所示:
z.bind(x.add(y));
注意,你不能写z.bind(x + y),因为+操作符不知道如何将两个IntegerProperty对象的值相加。您需要使用绑定 API 来创建绑定表达式,就像您在前面的语句中所做的那样。我将很快介绍绑定 API 的细节。
现在,当x、y或两者都改变时,z属性无效。下次请求z的值时,它会重新计算表达式x.add(y)来获得它的值。
您可以使用Property接口的unbind()方法来解除绑定属性。对未绑定或从未绑定的属性调用unbind()方法没有任何效果。您可以按如下方式解除z属性的绑定:
z.unbind();
解除绑定后,属性表现为普通属性,独立保持其值。解除属性绑定会断开属性与其依赖项之间的链接。
// BoundProperty.java
package com.jdojo.binding;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
public class BoundProperty {
public static void main(String[] args) {
IntegerProperty x = new SimpleIntegerProperty(10);
IntegerProperty y = new SimpleIntegerProperty(20);
IntegerProperty z = new SimpleIntegerProperty(60);
z.bind(x.add(y));
System.out.println("After binding z: Bound = " + z.isBound() +
", z = " + z.get());
// Change x and y
x.set(15);
y.set(19);
System.out.println("After changing x and y: Bound = " +
z.isBound() + ", z = " + z.get());
// Unbind z
z.unbind();
// Will not affect the value of z as it is not bound to
// x and y anymore
x.set(100);
y.set(200);
System.out.println("After unbinding z: Bound = " +
z.isBound() + ", z = " + z.get());
}
}
After binding z: Bound = true, z = 30
After changing x and y: Bound = true, z = 34
After unbinding z: Bound = false, z = 34
Listing 2-9Binding a Property
单向和双向绑定
绑定有一个方向,即传播更改的方向。JavaFX 支持两种类型的属性绑定:单向绑定和双向绑定。单向绑定只在一个方向起作用;依赖项中的更改会传播到绑定属性,反之亦然。双向绑定在两个方向上都起作用;依赖项的更改反映在属性中,反之亦然。
接口Property的bind()方法在属性和ObservableValue之间创建了一个单向绑定,这可能是一个复杂的表达式。bindBidirectional()方法在一个属性和同类型的另一个属性之间创建一个双向绑定。
假设 x,y,z 是IntegerProperty的三个实例。考虑以下绑定:
z = x + y
在 JavaFX 中,上述绑定只能表示为单向绑定,如下所示:
z.bind(x.add(y));
假设您能够在前一种情况下使用双向绑定。如果你能将z的值改为 100,你将如何反过来计算x和y的值?因为z是100,所以x和y有无限多种可能的组合,例如,(99,1),(98,2),(101,–1),(200,–100),等等。将绑定属性的更改传播到其依赖项是不可能得到可预测的结果的。这就是将属性绑定到表达式只允许作为单向绑定的原因。
单向绑定有一个限制。一旦属性具有单向绑定,就不能直接更改属性的值;它的值必须根据绑定自动计算。在直接更改其值之前,必须先解除绑定。以下代码片段显示了这种情况:
IntegerProperty x = new SimpleIntegerProperty(10);
IntegerProperty y = new SimpleIntegerProperty(20);
IntegerProperty z = new SimpleIntegerProperty(60);
z.bind(x.add(y));
z.set(7878); // Will throw a RuntimeException
要直接更改z的值,您可以键入以下内容:
z.unbind(); // Unbind z first
z.set(7878); // OK
单向绑定还有另一个限制。一个属性一次只能有一个单向绑定。考虑属性z的以下两个单向绑定。假设x、y、z、a和b是IntegerProperty的五个实例:
z = x + y
z = a + b
如果x、y、a和b是四个不同的属性,那么前面显示的z的绑定是不可能的。想想x = 1、y = 2、a = 3、b = 4。能定义一下z的值吗?会是 3 还是 7?这就是一个属性一次只能有一个单向绑定的原因。
重新绑定已经具有单向绑定的属性会解除以前的绑定。例如,下面的代码片段就很好:
IntegerProperty x = new SimpleIntegerProperty(1);
IntegerProperty y = new SimpleIntegerProperty(2);
IntegerProperty a = new SimpleIntegerProperty(3);
IntegerProperty b = new SimpleIntegerProperty(4);
IntegerProperty z = new SimpleIntegerProperty(0);
z.bind(x.add(y));
System.out.println("z = " + z.get());
z.bind(a.add(b)); // Will unbind the previous binding
System.out.println("z = " + z.get());
z = 3
z = 7
双向绑定在两个方向上都起作用。它有一些限制。它只能在相同类型的属性之间创建。也就是说,双向绑定只能是类型x = y和y = x,其中x和y属于同一类型。
双向绑定消除了单向绑定的一些限制。一个属性可以同时有多个双向绑定。双向绑定属性也可以独立更改;该更改反映在绑定到该属性的所有属性中。也就是说,使用双向绑定,以下绑定是可能的:
x = y
x = z
在前一种情况下,x、y和z的值将总是同步的。也就是说,在建立绑定后,所有三个属性将具有相同的值。您也可以在x、y和z之间建立双向绑定,如下所示:
x = z
z = y
现在出现了一个问题。前面的两个双向绑定最终会在x、y和z中具有相同的值吗?答案是否定的。最后一个双向绑定中右侧操作数的值(例如,请参见前面的表达式)是所有参与属性包含的值。我来阐述一下这一点。假设x为 1,y为 2,z为 3,则有如下双向绑定:
x = y
x = z
第一次绑定x = y,将设置x的值等于y的值。此时,x和y将为 2。第二个绑定x = z,将设置x的值等于z的值。也就是x和z会是 3。然而,x已经有了到y的双向绑定,这也将把x的新值 3 传播到y。因此,这三个属性的值将与z的值相同。清单 2-10 中的程序展示了如何使用双向绑定。
// BidirectionalBinding.java
package com.jdojo.binding;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
public class BidirectionalBinding {
public static void main(String[] args) {
IntegerProperty x = new SimpleIntegerProperty(1);
IntegerProperty y = new SimpleIntegerProperty(2);
IntegerProperty z = new SimpleIntegerProperty(3);
System.out.println("Before binding:");
System.out.println("x=" + x.get() + ", y=" + y.get() +
", z=" + z.get());
x.bindBidirectional(y);
System.out.println("After binding-1:");
System.out.println("x=" + x.get() + ", y=" + y.get() +
", z=" + z.get());
x.bindBidirectional(z);
System.out.println("After binding-2:");
System.out.println("x=" + x.get() + ", y=" + y.get() +
", z=" + z.get());
System.out.println("After changing z:");
z.set(19);
System.out.println("x=" + x.get() + ", y=" + y.get() +
", z=" + z.get());
// Remove bindings
x.unbindBidirectional(y);
x.unbindBidirectional(z);
System.out.println(
"After unbinding and changing them separately:");
x.set(100);
y.set(200);
z.set(300);
System.out.println("x=" + x.get() + ", y=" + y.get() +
", z=" + z.get());
}
}
Before binding:
x=1, y=2, z=3
After binding-1:
x=2, y=2, z=3
After binding-2:
x=3, y=3, z=3
After changing z:
x=19, y=19, z=19
After unbinding and changing them separately:
x=100, y=200, z=300
Listing 2-10Using Bidirectional Bindings
与单向绑定不同,创建双向绑定时,不会移除以前的绑定,因为一个属性可以有多个双向绑定。您必须使用unbindBidirectional()方法移除所有双向绑定,为属性的每个双向绑定调用一次该方法,如下所示:
// Create bidirectional bindings
x.bindBidirectional(y);
x.bindBidirectional(z);
// Remove bidirectional bindings
x.unbindBidirectional(y);
x.unbindBidirectional(z);
了解绑定 API
前几节简单快速地介绍了 JavaFX 中的绑定。现在是时候深入挖掘并详细理解绑定 API 了。绑定 API 分为两类:
-
高级绑定 API
-
低级绑定 API
高级绑定 API 允许您使用 JavaFX 类库定义绑定。对于大多数用例,您可以使用高级绑定 API。
有时,现有的 API 不足以定义绑定。在这些情况下,使用低级绑定 API。在低级绑定 API 中,从现有的绑定类派生一个绑定类,并编写自己的逻辑来定义绑定。
高级绑定 API
高级绑定 API 由两部分组成:Fluent API 和Bindings类。您可以只使用 Fluent API、只使用Bindings类或者结合使用两者来定义绑定。我们来看两部分,先分开再合起来。
使用 Fluent API
Fluent API 由不同接口和类中的几个方法组成。这个 API 被称为 Fluent ,因为方法名、它们的参数和返回类型已经被设计成允许流畅地编写代码。与使用非流畅 API 编写的代码相比,使用流畅 API 编写的代码可读性更好。设计一个流畅的 API 需要更多的时间。流畅的 API 对开发者更友好,对设计者不友好。fluent API 的一个特性是方法链接;您可以将单独的方法调用合并到一个语句中。考虑下面的代码片段来添加三个属性x、y和z。使用非流畅 API 的代码可能如下所示:
x.add(y);
x.add(z);
使用 Fluent API,前面的代码可能如下所示,这使读者更好地理解作者的意图:
x.add(y).add(z);
图 2-3 显示了IntegerBinding和IntegerProperty类的类图。图中省略了一些属于IntegerProperty类层次的接口和类。long、float和double类型的类图类似。
图 2-3
IntegerBinding和IntegerProperty的部分类图
从ObservableNumberValue和Binding接口到IntegerBinding类的类和接口是int数据类型的流畅绑定 API 的一部分。起初,看起来好像有很多课要学。大多数类和接口存在于属性和绑定 API 中,以避免原始值的装箱和拆箱。要学习流畅的绑定 API,需要重点关注XXXExpression和XXXBinding类和接口。XXXExpression类拥有用于创建绑定表达式的方法。
绑定接口
Binding接口的一个实例表示一个值,该值是从一个或多个称为依赖关系的源中导出的。它有以下四种方法:
-
public void dispose() -
public ObservableList<?> getDependencies() -
public void invalidate() -
public boolean isValid()
方法dispose()的实现是可选的,它向一个Binding表明它将不再被使用,因此它可以删除对其他对象的引用。绑定 API 在内部使用弱失效侦听器,因此不需要调用此方法。
方法getDependencies()的实现是可选的,它返回不可修改的依赖关系ObservableList。它仅用于调试目的。不应在生产代码中使用此方法。
对invalidate()方法的调用会使Binding无效。如果一个Binding有效,isValid()方法返回true。否则返回false。
数字绑定接口
NumberBinding接口是一个标记接口,其实例包装了一个int、long、float或double类型的数值。由DoubleBinding、FloatBinding、IntegerBinding和LongBinding类实现。
可观察的界面
ObservableNumberValue接口的一个实例包装了一个int、long、float或double类型的数值。它提供了以下四种获取值的方法:
-
double doubleValue() -
float floatValue() -
int intValue() -
long longValue()
您使用了清单 2-8 中提供的intValue()方法从NumberBinding实例中获取int值。您使用的代码应该是
IntegerProperty x = new SimpleIntegerProperty(100);
IntegerProperty y = new SimpleIntegerProperty(200);
// Create a binding: sum = x + y
NumberBinding sum = x.add(y);
int value = sum.intValue(); // Get the int value
ObservableIntegerValue 接口
ObservableIntegerValue接口定义了一个返回特定类型的int值的get()方法。
数字表达式接口
NumberExpression接口包含几个使用流畅风格创建绑定的便利方法。它有超过 50 个方法,其中大多数都是重载的。这些方法返回一个Binding类型,比如NumberBinding、BooleanBinding等等。表 2-2 列出了NumberExpression界面中的方法。大多数方法都是重载的。该表没有显示方法参数。
表 2-2
NumberExpression界面中方法的总结
方法名称
|
返回类型
|
描述
|
| --- | --- | --- |
| add()``subtract()``multiply()``divide() | NumberBinding | 这些方法创建一个新的NumberBinding,它是NumberExpression的和、差、积和除,以及一个数值或一个ObservableNumberValue。 |
| greaterThan()``greaterThanOrEqualTo()``isEqualTo()``isNotEqualTo()``lessThan()``lessThanOrEqualTo() | BooleanBinding | 这些方法创建一个新的BooleanBinding,存储NumberExpression和一个数值或ObservableNumberValue的比较结果。方法名足够清楚,可以告诉我们它们执行哪种比较。 |
| negate() | NumberBinding | 它创建了一个新的NumberBinding,它是对NumberExpression的否定。 |
| asString() | StringBinding | 它创建了一个StringBinding,将NumberExpression的值保存为一个String对象。此方法还支持基于区域设置的字符串格式。 |
在使用算术表达式定义绑定时,NumberExpression接口中的方法允许混合类型(int、long、float和double)。当该接口中方法的返回类型为NumberBinding时,实际返回的类型为IntegerBinding、LongBinding、FloatBinding或DoubleBinding。算术表达式的绑定类型由与 Java 编程语言相同的规则决定。表达式的结果取决于操作数的类型。规则如下:
-
如果操作数之一是
double,则结果是double。 -
如果操作数中没有一个是
double,而其中一个是float,那么结果就是一个float。 -
如果操作数都不是
double或float,并且其中一个是long,则结果是long。 -
否则,结果是一个
int。
考虑以下代码片段:
IntegerProperty x = new SimpleIntegerProperty(1);
IntegerProperty y = new SimpleIntegerProperty(2);
NumberBinding sum = x.add(y);
int value = sum.intValue();
数字表达式x.add(y)只涉及int操作数(x和y属于int类型)。因此,根据前面的规则,它的结果是一个int值,并且它返回一个IntegerBinding对象。因为NumberExpression中的add()方法将返回类型指定为NumberBinding,所以使用了一个NumberBinding类型来存储结果。您必须从ObservableNumberValue接口使用intValue()方法。您可以重写前面的代码片段,如下所示:
IntegerProperty x = new SimpleIntegerProperty(1);
IntegerProperty y = new SimpleIntegerProperty(2);
// Casting to IntegerBinding is safe
IntegerBinding sum = (IntegerBinding)x.add(y);
int value = sum.get();
NumberExpressionBase类是NumberExpression接口的一个实现。IntegerExpression类扩展了NumberExpressionBase类。它重写其超类中的方法,以提供特定于类型的返回类型。
清单 2-11 中的程序创建了一个DoubleBinding来计算圆的面积。它还创建了一个DoubleProperty并将其绑定到同一个表达式来计算面积。您可以选择是使用Binding对象还是绑定属性对象。这个程序向你展示了这两种方法。
// CircleArea.java
package com.jdojo.binding;
import javafx.beans.binding.DoubleBinding;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
public class CircleArea {
public static void main(String[] args) {
DoubleProperty radius = new SimpleDoubleProperty(7.0);
// Create a binding for computing area of the circle
DoubleBinding area =
radius.multiply(radius).multiply(Math.PI);
System.out.println("Radius = " + radius.get() +
", Area = " + area.get());
// Change the radius
radius.set(14.0);
System.out.println("Radius = " + radius.get() +
", Area = " + area.get());
// Create a DoubleProperty and bind it to an expression
// that computes the area of the circle
DoubleProperty area2 = new SimpleDoubleProperty();
area2.bind(radius.multiply(radius).multiply(Math.PI));
System.out.println("Radius = " + radius.get() +
", Area2 = " + area2.get());
}
}
Radius = 7.0, Area = 153.93804002589985
Radius = 14.0, Area = 615.7521601035994
Radius = 14.0, Area2 = 615.7521601035994
Listing 2-11Computing the Area of a Circle from Its Radius Using a Fluent Binding API
字符串绑定类
包含绑定 API 中支持String类型绑定的类的类图如图 2-4 所示。
图 2-4
StringBinding的部分类图
ObservableStringValue接口声明了一个返回类型为String的get()方法。StringExpression类中的方法允许您使用流畅的风格创建绑定。提供了一些方法来将一个对象连接到StringExpression,比较两个字符串,检查null,等等。它有两种方法获取它的值:getValue()和getValueSafe()。两者都返回当前值。然而,当当前值为null.时,后者返回空的String
清单 2-12 中的程序展示了如何使用StringBinding和StringExpression类。StringExpression类中的concat()方法接受一个Object类型作为参数。如果参数是ObservableValue,当参数改变时StringExpression自动更新。注意asString()方法在radius和area属性上的使用。对一个NumberExpression的asString()方法返回一个StringBinding。
// StringExpressionTest.java
package com.jdojo.binding;
import java.util.Locale;
import javafx.beans.binding.StringExpression;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
public class StringExpressionTest {
public static void main(String[] args) {
DoubleProperty radius = new SimpleDoubleProperty(7.0);
DoubleProperty area = new SimpleDoubleProperty(0);
StringProperty initStr = new SimpleStringProperty(
"Radius = ");
// Bind area to an expression that computes the area of
// the circle
area.bind(radius.multiply(radius).multiply(Math.PI));
// Create a string expression to describe the circle
StringExpression desc = initStr.concat(radius.asString())
.concat(", Area = ")
.concat(area.asString(Locale.US, "%.2f"));
System.out.println(desc.getValue());
// Change the radius
radius.set(14.0);
System.out.println(desc.getValue());
}
}
Radius = 7.0, Area = 153.94
Radius = 14.0, Area = 615.75
Listing 2-12Using StringBinding and StringExpression
对象表达式和对象绑定类
现在是时候让ObjectExpression和ObjectBinding类创建任何类型对象的绑定了。他们的类图与StringExpression和StringBinding类非常相似。ObjectExpression类有比较对象是否相等和检查空值的方法。清单 2-13 中的程序展示了如何使用ObjectBinding类。
// ObjectBindingTest.java
package com.jdojo.binding;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
public class ObjectBindingTest {
public static void main(String[] args) {
Book b1 = new Book("J1", 90, "1234567890");
Book b2 = new Book("J2", 80, "0123456789");
ObjectProperty<Book> book1 = new SimpleObjectProperty<>(b1);
ObjectProperty<Book> book2 = new SimpleObjectProperty<>(b2);
// Create a binding that computes if book1 and book2 are equal
BooleanBinding isEqual = book1.isEqualTo(book2);
System.out.println(isEqual.get());
book2.set(b1);
System.out.println(isEqual.get());
}
}
false
true
Listing 2-13Using the ObjectBinding Class
BooleanExpression 和 BooleanBinding 类
BooleanExpression类包含诸如and()、or()和not()之类的方法,允许您在表达式中使用布尔逻辑运算符。它的isEqualTo()和isNotEqualTo()方法可以让你比较一个BooleanExpression和另一个ObservableBooleanValue。一个BooleanExpression的结果是true或false。
清单 2-14 中的程序展示了如何使用BooleanExpression类。它使用流畅的风格创建一个布尔表达式x > y && y <> z。注意,greaterThan()和isNotEqualTo()方法是在NumberExpression接口中定义的。该程序只使用来自BooleanExpression类的and()方法。
// BooelanExpressionTest.java
package com.jdojo.binding;
import javafx.beans.binding.BooleanExpression;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
public class BooelanExpressionTest {
public static void main(String[] args) {
IntegerProperty x = new SimpleIntegerProperty(1);
IntegerProperty y = new SimpleIntegerProperty(2);
IntegerProperty z = new SimpleIntegerProperty(3);
// Create a boolean expression for x > y && y <> z
BooleanExpression condition =
x.greaterThan(y).and(y.isNotEqualTo(z));
System.out.println(condition.get());
// Make the condition true by setting x to 3
x.set(3);
System.out.println(condition.get());
}
}
false
true
Listing 2-14Using BooleanExpression and BooleanBinding
在表达式中使用三元运算
Java 编程语言提供了一个三元运算符(condition?value1:value2),用于执行形式为 when-then-otherwise 的三元运算。JavaFX 绑定 API 为此提供了一个When类。使用When类的一般语法如下所示:
new When(condition).then(value1).otherwise(value2)
condition必须是一个ObservableBooleanValue。当condition计算结果为true时,它返回value1。否则返回value2。value1和value2的类型必须相同。值可以是常量或ObservableValue的实例。
让我们使用一个三元运算,根据一个IntegerProperty的值是偶数还是奇数,分别返回一个String even或odd。Fluent API 没有计算模数的方法。你必须自己做这件事。对整数执行除以 2 的整数除法,并将结果乘以 2。如果你得到同样的数字,这个数字是偶数。否则,数字是奇数。例如,使用整数除法,(7/2)*2 得到 6,而不是 7。清单 2-15 提供了完整的程序。
// TernaryTest.java
package com.jdojo.binding;
import javafx.beans.binding.When;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.binding.StringBinding;
public class TernaryTest {
public static void main(String[] args) {
IntegerProperty num = new SimpleIntegerProperty(10);
StringBinding desc =
new When(num.divide(2).multiply(2).isEqualTo(num))
.then("even")
.otherwise("odd");
System.out.println(num.get() + " is " + desc.get());
num.set(19);
System.out.println(num.get() + " is " + desc.get());
}
}
10 is even
19 is odd
Listing 2-15Using the When Class to Perform a Ternary Operation
使用绑定实用程序类
Bindings类是一个助手类,用于创建简单的绑定。它由 150 多个静态方法组成。他们中的大多数都超载了几个变种。我不会一一列举或讨论。请参考在线 JavaFX API 文档以获得完整的方法列表。表 2-3 列出了Bindings类的方法及其描述。它排除了属于集合绑定的方法。
表 2-3
Bindings类中方法的总结
方法名称
|
描述
|
| --- | --- |
| add()``subtract()``multiply()``divide() | 它们通过对它的两个参数应用算术运算来创建一个绑定。至少有一个参数必须是ObservableNumberValue。如果参数之一是一个double,它的返回类型是DoubleBinding;否则,其返回类型为NumberBinding。 |
| and() | 它通过对它的两个参数应用布尔运算and来创建一个BooleanBinding。 |
| bindBidirectional()``unbindBidirectional() | 它们创建和删除两个属性之间的双向绑定。 |
| concat() | 它返回一个保存其参数串联值的StringExpression。它需要一个varargs参数。 |
| convert() | 它返回一个包装其参数的StringExpression。 |
| createXXXBinding() | 它允许您创建一个XXX类型的定制绑定,其中XXX可以是Boolean、Double、Float、Integer、String和Object。 |
| equal()``notEqual()``equalIgnoreCase()``notEqualIgnoreCase() | 他们创建了一个BooleanBinding,包装了两个参数相等或不相等的比较结果。这些方法的一些变体允许传递公差值。如果两个参数在公差范围内,则认为它们相等。通常,容差值用于比较浮点数。这些方法的忽略大小写变量只对String类型有效。 |
| format() | 它创建一个StringExpression,保存根据指定格式String格式化的多个对象的值。 |
| greaterThan()``greaterThanOrEqual()``lessThan()``lessThanOrEqual() | 他们创建一个BooleanBinding来包装比较参数的结果。 |
| isNotNull``isNull | 他们创建一个BooleanBinding来包装与null进行比较的结果。 |
| max()``min() | 它们创建一个绑定,保存该方法的两个参数的最大值和最小值。其中一个参数必须是ObservableNumberValue。 |
| negate() | 它创建一个NumberBinding来保存一个ObservableNumberValue的否定。 |
| not() | 它创建一个BooleanBinding来保存一个ObservableBooleanValue的逆。 |
| or() | 它创建一个BooleanBinding,保存对它的两个ObservableBooleanValue参数应用条件or操作的结果。 |
| selectXXX() | 它创建一个绑定来选择嵌套属性。嵌套属性可以是类型a.b.c。绑定的值将是c。像a.b.c这样的表达式中涉及的类和属性必须是公共的。如果表达式的任何部分不可访问,因为它们不是公共的或者它们不存在,类型的默认值,例如,null表示Object type,空的String表示String type,0 表示数值类型,而false表示布尔类型,就是绑定的值。(后面我会讨论一个使用select()方法的例子。) |
| when() | 它创建了一个将条件作为参数的When类的实例。 |
我们使用 Fluent API 的大多数例子也可以使用Bindings类编写。清单 2-16 中的程序类似于清单 2-12 中的程序。它使用了Bindings类,而不是 Fluent API。它使用multiply()方法计算面积,使用format()方法格式化结果。做同一件事可能有几种方法。为了格式化结果,您还可以使用Bindings.concat()方法,如下所示:
// BindingsClassTest.java
package com.jdojo.binding;
import java.util.Locale;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.StringExpression;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
public class BindingsClassTest {
public static void main(String[] args) {
DoubleProperty radius = new SimpleDoubleProperty(7.0);
DoubleProperty area = new SimpleDoubleProperty(0.0);
// Bind area to an expression that computes the area of
// the circle
area.bind(Bindings.multiply(
Bindings.multiply(radius, radius), Math.PI));
// Create a string expression to describe the circle
StringExpression desc = Bindings.format(Locale.US,
"Radius = %.2f, Area = %.2f", radius, area);
System.out.println(desc.get());
// Change the radius
radius.set(14.0);
System.out.println(desc.getValue());
}
}
Radius = 7.00, Area = 153.94
Radius = 14.00, Area = 615.75
Listing 2-16Using the Bindings Class
StringExpression desc = Bindings.concat("Radius = ",
radius.asString(Locale.US, "%.2f"),
", Area = ", area.asString(Locale.US, "%.2f"));
让我们看一个使用Bindings类的selectXXX()方法的例子。它用于为嵌套属性创建绑定。在嵌套层次结构中,所有的类和属性都必须是公共的。假设您有一个拥有zip属性的Address类和一个拥有addr属性的Person类。这些类别分别显示在清单 2-17 和 2-18 中。
// Person.java
package com.jdojo.binding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
public class Person {
private ObjectProperty<Address> addr =
new SimpleObjectProperty(new Address());
public ObjectProperty<Address> addrProperty() {
return addr;
}
}
Listing 2-18A Person Class
// Address.java
package com.jdojo.binding;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
public class Address {
private StringProperty zip = new SimpleStringProperty("36106");
public StringProperty zipProperty() {
return zip;
}
}
Listing 2-17An Address Class
假设您创建了一个Person类的ObjectProperty,如下所示:
ObjectProperty<Person> p = new SimpleObjectProperty(new Person());
使用Bindings.selectString()方法,您可以为Person对象的addr属性的zip属性创建一个StringBinding,如下所示:
// Bind p.addr.zip
StringBinding zipBinding = Bindings.selectString(p, "addr", "zip");
前面的语句为StringProperty zip获取一个绑定,它是对象p的addr属性的嵌套属性。selectXXX()方法中的一个属性可能有多层嵌套。你可以有一个selectXXX()的称呼
StringBinding xyzBinding = Bindings.selectString(x, "a", "b", "c", "d");
Note
JavaFX API 文档指出,如果任何属性参数不可访问,Bindings.selectString()将返回空的String。然而,运行时返回null。
清单 2-19 展示了selectString()方法的使用。程序打印两次zip属性的值:一次是默认值,一次是更改后的值。最后,它试图绑定一个不存在的属性p.addr.state。绑定到不存在的属性会导致异常。
// BindNestedProperty.java
// Listing part of the example sources download for the book
36106
35217
null
Aug. 21, 2021 10:41:56 AM com.sun.javafx.binding.SelectBinding$SelectBindingHelper getObservableValue
WARNING: Exception while evaluating select-binding [addr, state]
java.lang.NoSuchMethodException: com.jdojo.binding.BindNestedProperty$Address.getState()
at java.base/java.lang.Class.getMethod(Class.java:2195)
...
at JavaFXBook/
com.jdojo.binding.BindNestedProperty.main(BindNestedProperty.java:57)
Listing 2-19Using the selectXXX() Method of the Bindings Class
结合 Fluent API 和绑定类
在使用高级绑定 API 时,可以在同一个绑定表达式中使用 fluent 和Bindings类 API。以下代码片段展示了这种方法:
DoubleProperty radius = new SimpleDoubleProperty(7.0);
DoubleProperty area = new SimpleDoubleProperty(0);
// Combine the Fluent API and Bindings class API
area.bind(Bindings.multiply(Math.PI, radius.multiply(radius)));
使用低级绑定 API
高级绑定 API 并不适合所有情况。例如,它没有提供计算一个Observable数的平方根的方法。如果高级绑定 API 变得太麻烦而无法使用,或者它没有提供您需要的东西,您可以使用低级绑定 API。它以增加几行代码为代价,为您提供了强大的功能和灵活性。低级 API 允许您使用 Java 编程语言的全部潜力来定义绑定。
使用低级绑定 API 包括以下三个步骤:
-
创建一个扩展其中一个绑定类的类。例如,如果你想创建一个
DoubleBinding,你需要扩展DoubleBinding类。 -
调用超类的
bind()方法绑定所有依赖关系。注意,所有绑定类都有一个bind()方法实现。您需要调用此方法,将所有依赖项作为参数传递。它的参数类型是一个Observable类型的varargs。 -
重写超类的
computeValue()方法来编写绑定的逻辑。它计算绑定的当前值。它的返回类型与绑定的类型相同,例如,DoubleBinding的返回类型是double,而StringBinding的返回类型是String,依此类推。
此外,您可以重写绑定类的一些方法,为您的绑定提供更多功能。当绑定被释放时,您可以重写dispose()方法来执行额外的操作。可以覆盖getDependencies()方法来返回绑定的依赖列表。如果想在绑定无效时执行额外的操作,就需要重写onInvalidating()方法。
考虑计算圆的面积的问题。以下代码片段使用低级 API 来完成此任务:
final DoubleProperty radius = new SimpleDoubleProperty(7.0);
DoubleProperty area = new SimpleDoubleProperty(0);
DoubleBinding areaBinding = new DoubleBinding() {
{
this.bind(radius);
}
@Override
protected double computeValue() {
double r = radius.get();
double area = Math.PI * r * r;
return area;
}
};
area.bind(areaBinding); // Bind the area property to the areaBinding
前面的代码片段创建了一个匿名类,它扩展了DoubleBinding类。它调用bind()方法,传递对radius属性的引用。匿名类没有构造器,所以你必须使用实例初始化器来调用bind()方法。computeValue()方法计算并返回圆的面积。属性radius已经被声明为final,因为它正在匿名类中使用。
清单 2-20 中的程序展示了如何使用低级绑定 API。它覆盖了区域绑定的computeValue()方法。对于描述绑定,它也覆盖了dispose()、getDependencies()和onInvalidating()方法。
// LowLevelBinding.java
// Listing part of the example sources download for the book
Radius = 7.00, Area = 153.94
Description is invalid.
Radius = 14.00, Area = 615.75
Listing 2-20Using the Low-Level Binding API to Compute the Area of a Circle
使用绑定使圆居中
让我们看一个使用绑定的 JavaFX GUI 应用程序的例子。您将创建一个带有圆形的屏幕,即使在调整屏幕大小时,它也将位于屏幕的中心。圆的周长将接触屏幕的较近的边。如果屏幕的宽度和高度相同,圆的周长将接触屏幕的所有四个边。
试图在没有绑定的情况下开发具有中心圆的屏幕是一项单调乏味的任务。javafx.scene.shape包中的Circle类代表一个圆。它有三个属性——centerX、centerY、radius——DoubleProperty类型。centerX和centerY属性定义了圆心的(x,y)坐标。radius属性定义了圆的半径。默认情况下,圆用黑色填充。
创建一个圆,将centerX、centerY和radius设置为默认值 0.0,如下所示:
Circle c = new Circle();
接下来,将圆添加到一个组中,并以该组作为其根节点创建一个场景,如下所示:
Group root = new Group(c);
Scene scene = new Scene(root, 150, 150);
以下绑定将根据场景的大小来定位和调整圆的大小:
c.centerXProperty().bind(scene.widthProperty().divide(2));
c.centerYProperty().bind(scene.heightProperty().divide(2));
c.radiusProperty().bind(Bindings.min(scene.widthProperty(),
scene.heightProperty()).divide(2));
前两个绑定将圆的centerX和centerY分别绑定到场景的宽度和高度的中间。第三个绑定将圆的radius绑定到场景最小宽度和高度的一半(见divide(2))。就这样!当应用程序运行时,绑定 API 具有保持圆圈居中的魔力。
清单 2-21 有完整的程序。图 2-5 显示程序初始运行时的画面。图 2-6 显示屏幕水平拉伸时的屏幕。尝试垂直拉伸屏幕,您会注意到圆周仅接触屏幕的左侧和右侧。
图 2-6
CenteredCircle程序的屏幕水平伸展时的屏幕
图 2-5
最初运行CenteredCircle程序时的屏幕
// CenteredCircle.java
// Listing part of the example sources download for the book
Listing 2-21Using the Binding API to Keep a Circle Centered in a Scene
摘要
一个 Java 类可能包含两种类型的成员:字段和方法。字段表示其对象的状态,它们被声明为私有的。公共方法,也称为访问器,或者 getters 和 setters,用于读取和修改私有字段。对于所有或部分私有字段具有公共访问器的 Java 类称为 Java bean,访问器定义了 bean 的属性。Java bean 的属性允许用户定制其状态、行为或两者。
JavaFX 通过属性和绑定 API 支持属性、事件和绑定。JavaFX 中的属性支持是 JavaBeans 属性的巨大飞跃。JavaFX 中的所有属性都是可观察的。可以观察到它们的失效和值的变化。您可以拥有读/写或只读属性。所有读/写属性都支持绑定。在 JavaFX 中,属性可以表示一个值或一组值。
当属性值的状态第一次从有效变为无效时,属性会生成一个无效事件。JavaFX 中的属性使用惰性计算。当无效属性再次变为无效时,不会生成失效事件。无效的属性在重新计算后变得有效。
在 JavaFX 中,绑定是一个计算结果为值的表达式。它由一个或多个被称为依赖关系的可观察值组成。绑定观察其依赖关系的变化,并自动重新计算其值。JavaFX 对所有绑定都使用惰性求值。当绑定最初被定义或者当它的依赖关系改变时,它的值被标记为无效。无效绑定的值在下次请求时计算。JavaFX 中的所有属性类都内置了对绑定的支持。
绑定有一个方向,即传播更改的方向。JavaFX 支持两种类型的属性绑定:单向绑定和双向绑定。单向绑定只在一个方向起作用;依赖项中的更改会传播到绑定属性,反之亦然。双向绑定在两个方向上都起作用;依赖项的更改反映在属性中,反之亦然。
JavaFX 中的绑定 API 分为两类:高级绑定 API 和低级绑定 API。高级绑定 API 允许您使用 JavaFX 类库定义绑定。对于大多数用例,您可以使用高级绑定 API。有时,现有的 API 不足以定义绑定。在这些情况下,使用低级绑定 API。在低级绑定 API 中,从现有的绑定类派生一个绑定类,并编写自己的逻辑来定义绑定。
下一章将向您介绍 JavaFX 中的可观察集合。