JavaFX17 现代 Java 客户端权威指南(一)

原文:The Definitive Guide to Modern Java Clients with JavaFX 17

协议:CC BY-NC-SA 4.0

一、客户端 Java 入门

作者斯蒂芬·陈

客户端技术是构建任何用户交互界面的基础。因为他们是用户看到的应用程序的第一部分,他们也给你的观众留下了最大的影响。因此,重要的是用户界面看起来不错,并且易于使用和直观。

无论是桌面、移动、平板还是嵌入式设备,Java 客户端技术都为构建现代用户体验提供了一个简单而优雅的解决方案。因为 Java 语言是跨平台的,所以这减少了为多屏幕和多种形式构建和维护应用程序的工作量。此外,作为最广泛使用的编程语言之一,任何人都可以帮助维护您的代码,使其成为未来的坚实基础。

在这一章中,我们将展示 Java 客户端技术的一些实例,并指导您构建自己的跨平台客户端,以展示实现这一点是多么容易。

Java 客户端技术的应用

几十年来,Java 客户端技术已经被用于各种应用程序,从商业应用程序到开发工具甚至游戏。此外,现在 Java 运行在移动和嵌入式平台上,您也可以在手机、平板电脑和 Raspberry Pi 设备上找到 Java 应用程序。通常很难判断您是否正在使用 Java 应用程序,因为它与所需的 Java 库打包在一起,所以看起来就像任何其他本机应用程序一样。

我们将探索几个不同的 Java 客户机应用程序,您可能使用过也可能没有使用过,以便让您了解这项技术的潜力。

商业中的 Java 客户端

Java 客户端技术是企业内部应用程序的主要部分。这是因为它非常擅长构建高度定制的应用程序,这些应用程序具有复杂的控件,如图形、树、表格和甘特图。通过一次构建应用程序并利用 Java 的跨平台能力,企业节省了初始实现成本和维护成本。

Java 客户端技术在行业中的常见用例是高速交易、列车监控和调度、供应链管理、医学成像和库存管理。MINT systems 开发了一个培训和资源管理系统(TRMS ),已被众多商业航空公司采用,如阿联酋航空、捷蓝航空、Azul Linhas Aéreas Brasileiras、联邦快递、汉莎航空集团和 Avianca-Taca 集团。 1

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1-1

MINT 航空公司培训与资源管理软件系统 2

图 1-1 显示了薄荷 TRMS 的一个更复杂的用户界面屏幕。它利用了树、表、带和甘特图,这些都是使用最新的 Java 客户端技术 JavaFX 实现的。JavaFX 是一个用户界面工具包,它提供了构建现代应用程序所需的所有布局、控件和图表。这展示了一个非常复杂的视图,在任何其他跨平台技术中实现它都是一个挑战。

要了解如何使用预构建的 JavaFX 控件轻松构建复杂应用程序的更多信息,请查看第四章“JavaFX 控件深度剖析”

游戏和 3D

Java 客户端技术也非常适合构建游戏。有史以来最受欢迎的游戏之一是由一个人使用 Java 技术开发的。Markus Persson(又名 Notch)在 2009 年发布了《我的世界》的开发版本。所有最初的开发都是在他的业余时间完成的,直到 alpha 版本赚了足够的钱,他可以创办自己的公司 Mojang,全职专注于游戏。它现在是世界上票房第二高的视频游戏,每月有 9100 万用户。 4

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1-2

由@tingsterchin 5 创建的来自 Tingsterland 的《我的世界》服务器示例

《我的世界》的成功很大程度上是通过大型的修改社区,他们构建插件来改变游戏的行为并增强游戏,使其远远超出了原始游戏的限制。图 1-2 显示了一个年轻开发人员创建的客户《我的世界》服务器的例子。Java 通过动态类加载和安全的沙箱模型为构建可扩展的应用程序提供了一个很好的平台。全球还拥有 1200 万 Java 开发人员, 6 不乏开发专业知识和人才。

《我的世界》完全是用 Java 构建的,使用了 Swing 和 Java 2D 等客户端技术以及一个名为 LWJGL 的 Java 游戏库。Java 和这些库提供的高度抽象使得 Notch 有可能在短时间内开发《我的世界》,并支持各种平台,而无需庞大的开发团队。

JavaFX 中内置的 3D 支持是一个更容易上手的 3D 库。你可以在第 8“Java FX 3D”一章中找到更多关于 3D 图形的信息。

移动会议应用

Java 客户端技术不仅仅适用于桌面。使用 Gluon 开发的移动 JavaFX 技术, 7 你可以在手机、平板电脑和嵌入式设备上运行你的 Java 客户端,比如 Raspberry Pi。现有的 JavaFX 应用程序可以直接移植到移动设备上,只需对控件的样式稍作修改,就可以在不同的屏幕尺寸上运行。对于处理特定于移动设备的 API,Gluon 提供了 Charm Down,它提供了与硬件特性的跨平台集成。

JavaFX mobile 的一个很好的例子是 Devoxx 会议应用程序。这最初是为旧金山的 JavaOne 大会构建的,并为开源社区做出了贡献。Devoxx 会议选择了它,并做了大量的工作,将其扩展为一个通用的会议应用程序,为每年在世界各地举行的几十个 Devoxx 和 Voxxed 会议提供服务。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1-3

Devoxx 会议移动应用。从左至右:会议选择、演讲人列表、会场导航 8

图 1-3 显示了会议应用程序中的几个不同屏幕,用于选择活动、展示演讲者和导航到会场。Devoxx conference family 的创始人斯蒂芬·让桑(Stephan Schmidt)表示:“JavaFX 移动技术帮助我们将多个原生应用简化为一个在 iOS 和 Android 设备上得到良好支持的跨平台应用。这对与会者来说是更好的体验,也更容易保持更新。”

在本章的后面,我们有一个简单的移动示例来展示使用这项技术有多简单,在第十一章“iOS 和 Android 的本地移动应用”中有更全面的指导

客户端 Java 的现代方法

虽然客户机 Java 技术已经存在了很长时间,但是开发生态系统一直处于不断的变化之中。在移动、云计算和应用程序分发方面已经取得了显著的进步,这些进步影响了您构建和分发客户端应用程序的方式。这本书旨在通过指导你设计和实现最佳实践,使你成为一名成功的现代应用程序开发人员。

我们将在此描述并在本书其余部分强调的三个具体最佳实践如下:

  1. 先瞄准手机。

  2. 为云而构建。

  3. 包装你的平台。

首先锁定手机

自 iPhone 和 Android 分别于 2007 年和 2008 年问世以来,智能手机的使用率一直在稳步上升。如图 1-4 所示,截至 2021 年,移动智能手机和平板电脑的 web 流量已超过桌面,占所有 web 请求的 54.8%。因此,对于成功的应用程序来说,移动不仅仅是一个选项,而是一个必需的界面。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1-4

自 2009 年以来移动使用占全球网络流量的百分比 9

智能手机已经发展到拥有处理能力、内存、存储和分辨率来运行传统上被认为是纯桌面的完整应用程序的地步。在许多使用情况下,带有蓝牙键盘的平板电脑可以很容易地用作台式机的替代品。此外,智能手机和平板电脑都内置无线互联网,这使得即使在没有宽带的情况下也可以使用它们。

因此,越来越多的“智能手机依赖”用户只能通过手机上网,却没有可用于台式机或笔记本电脑连接的宽带。如图 1-5 所示,28%的美国千禧一代(18-29 岁)依赖智能手机。只有当您的应用程序有移动版本时,这些用户才能使用您的应用程序!

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1-5

皮尤研究中心 10 按年龄段划分的依赖智能手机的美国公民

如前所述,JavaFX 拥有强大的移动功能,并通过 OpenJDK 的贡献者 Gluon 得到了增强。通过使用 JavaFX mobile,您可以一次编写一个应用程序代码库,然后面向多个屏幕,包括智能手机、平板电脑和桌面。这使您的应用程序比那些不允许用户将工作随身携带的纯桌面应用程序具有巨大的竞争优势。在第十一章“iOS 和 Android 的原生移动应用”中了解更多信息!

为云而构建

应用程序后端的模式已经从内部转移到了云。这是因为最终用户对他们如何与数据交互的期望发生了变化。历史上,用户会在本地拥有和管理他们的数据。随着可用的高速连接、可访问的加密和安全性以及每个用户多个屏幕的出现,这种期望已经改变。现在,用户希望数据始终在线和可用,以便可以从任何设备上使用,并轻松共享和协作。

一个很好的例子是 eteoBoard,这是一个由德国 Saxonia Systems AG 创建的数字协作 scrum 板。它旨在通过创建跨多个位置的扩展项目团队室来解决分布式团队的问题。如图 1-6 所示,这是通过使用大型监视器上的电话会议设备和显示在由 JavaFX 技术驱动的大型触摸屏监视器上的电子项目板来实现的。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1-6

用于管理项目积压 11 的电子白板示例

eteoBoard 应用程序使用 SynchronizeFX 12 在多个客户端之间实时同步用户界面状态。所有的项目数据从 Atlassian 吉拉或微软 Team Foundation Server 加载和存储,两者都是基于云的敏捷生命周期管理包,具有 REST 接口。从最终用户的角度来看,所有这些都是透明的,他们可以获得当前项目数据的最新视图,因此他们可以关注团队的进展。

这表明用户期望数据总是在线和可用的,这使得客户端应用程序需要与云后端紧密集成。有关如何在客户端应用程序中利用云的更多信息,请查看第 9“Java FX、Web 和云基础设施”一章中的“为云构建”一节

打包您的平台

台式电脑甚至移动设备已经发展到硬盘和网络传输的问题对于用户体验来说是次要的。如图 1-7 所示,十大移动应用的大小稳步增长,平均超过 1 GB。这意味着像让所有应用程序共享 Java 运行时这样的小优化不值得额外的步骤、复杂性和失败场景。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1-7

由 Sensor Tower 13 收集的 2013 年以来十大 iOS 应用的文件大小

不推荐使用像 Applets 和 Web Start 这样的传统技术来分发应用程序。由于它们依赖于共享的 Java 运行时,错误配置的系统很容易导致最终用户无法运行您的应用程序。更糟糕的是,如果不保持更新,这些技术会带来安全问题。因此,这些已被弃用,不应使用。 14

相反,您应该将应用程序需要的所有东西打包成一个发行版。这包括运行应用程序所需的类文件、库,甚至 Java 运行时。虽然这看起来包含了很多内容,但它平均只额外花费 20-30mb,并保证您的用户将拥有一个可用的 Java 运行时,该运行时已经过您正在使用的应用程序的特定版本的测试。

Java 14 重新引入了一个名为 jpackage 的工具,它负责将 Java 运行时与您的应用程序捆绑在一起进行分发。你可以在第十章“打包桌面应用”中找到更多关于这个和其他打包解决方案的信息此外,第十一章“iOS 和 Android 的本地移动应用”以此为基础,介绍了如何打包您的移动应用并在 iOS 和 Android 设备上的应用商店中发布。

设置您的环境

为了开始客户端编程,我们将使用 JavaFX 技术制作一个小的示例应用程序。为此,我们需要一个现代的 Java 版本以及 JavaFX 库。我们的建议是始终使用最新版本的 OpenJDK,因为它提供了最新的安全补丁,并且由 Oracle 免费支持。此外,如果您遵循打包应用程序的最佳实践,那么最终用户安装的 Java 版本并不重要,因为您将把最新的 Java 运行时与您的应用程序捆绑在一起。

可以从 http://jdk.java.net 下载 OpenJDK。只需从图 1-8 所示的页面中选择最新的“准备使用”版本,在撰写本文时,该版本是 Java 开发工具包(JDK) 17。版本每 6 个月增加一次,所以你的版本可能会有所不同。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1-8

jdk.java.net 上的 OpenJDK 下载站点

OpenJDK 没有安装程序,但是从命令行安装它非常容易。以下是每个操作系统如何轻松做到这一点的说明。

macOS JDK 安装

打开一个终端,转到下载 OpenJDK 的文件夹。您可以使用以下命令对其进行解压缩:

tar xf openjdk-17_osx-x64_bin.tar.gz

确保用正确的文件名替换您下载的 OpenJDK 版本。然后,您需要将它移动到 JDK 文件夹中,以便它能够被识别:

sudo mv jdk-17.jdk /Library/Java/JavaVirtualMachines/

同样,用正确的文件夹名替换您解压缩的 OpenJDK 版本,并输入您的管理员密码,因为这是一个受保护的文件夹。

最后,为了测试您的新 Java 安装是否被识别,运行java命令:

java -version

对于您安装的 OpenJDK 版本,您应该会看到如下输出:

openjdk version "17" 2021-09-16
OpenJDK Runtime Environment (build 17+??-????)
OpenJDK 64-Bit Server VM (build 17+??-????, mixed mode, sharing)

Windows JDK 安装

Windows JDK 以 zip 文件的形式出现。要安装它,将其解压缩到合适的文件位置,如C:/Program Files/Java/.如果您之前没有安装 JDK,该文件夹可能不存在,但可以用管理员权限创建。对该文件夹的复制操作也需要管理员权限,Windows 会提示您进行确认。

接下来,您需要创建 JAVA_HOME 环境变量,许多工具都希望设置这个变量。为此,请打开“系统属性”对话框,您可以在其中编辑环境变量。该对话框在现代 Windows 操作系统中隐藏得相当好,但是可以通过按下Windows+R并输入sysdm.cpl经由运行对话框可靠地访问,如图 1-9 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1-9

使用运行对话框调出系统属性

一旦系统属性对话框打开,选择高级选项卡,该对话框应出现在图 1-10 所示的屏幕上。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1-10

显示“高级”选项卡的“系统属性”对话框

在此选项卡上,单击“环境变量…”按钮。这将弹出如图 1-11 所示的对话框,允许您创建和编辑环境变量。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1-11

环境变量对话框

在环境变量对话框中,我们将创建一个新变量并修改路径。要创建新变量,请单击“系统变量”下的“新建…”按钮将这个变量命名为“JAVA_HOME”,并给它一个值,这个值就是新解压的 OpenJDK 发行版的位置。为了避免输入错误,您可以使用“浏览目录…”按钮来选择您之前创建的文件夹。确切的值将根据您的 JDK 版本而有所不同,但我的是“C:\Program Files\Java\jdk-17”。

接下来,修改Path环境变量以包含对JDK bin 文件夹的引用。为此,选择“Path”变量,可在系统变量下找到,点击“编辑…”将出现如图 1-12 所示的对话框。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1-12

Windows 路径环境变量编辑对话框

点击“新建”按钮,输入“%JAVA_HOME%\bin”作为数值。如果您使用的是早期版本的 Windows,您可能只有一个简单的文本字段,其中各个路径元素用分号分隔。如果是这种情况,只需转到字段的末尾,输入 JDK bin 值,并在前面加一个分号。如果您以前安装过 Java,您可能需要找到它的路径条目并删除它,这样它就不会覆盖您的新安装。完成后,单击“确定”并退出对话框。

现在,您可以通过打开命令提示符并键入 Java version 命令来测试 OpenJDK 安装是否正常工作:

java -version

如果安装正确,您应该得到如下所示的输出,指示您成功安装的 OpenJDK 的版本:

openjdk version "17" 2021-09-16
OpenJDK Runtime Environment (build 17+??-????)
OpenJDK 64-Bit Server VM (build 17+??-????, mixed mode, sharing)

Linux JDK 安装

如果你在 Linux 上,安装 OpenJDK 轻而易举。大多数 Linux 发行版都预装了 OpenJDK,可以通过为您的托管包运行适当的命令来轻松更新它。

以下是针对不同 Linux 发行版的命令示例(根据最新的 OpenJDK 版本进行适当修改):

  • Ubuntu,Debian: sudo apt-get install openjdk-17-jdk

  • Red Hat,Oracle Linux: sudo yum install java-17-openjdk-devel

如果您新安装的 Java 发行版没有被选作缺省版本,您可以使用update-alternatives命令修改缺省的 Java 版本,如果它没有出现在列表中,您可以添加新的 OpenJDK 发行版。

JavaFX 安装

JavaFX 不再作为 OpenJDK 的标准部分,所以必须单独下载和配置。JavaFX SDK 由官方 OpenJFX 贡献者 Gluon 构建和打包。他们的 builds 可以从 https://gluonhq.com/products/javafx/ 下载,如图 1-13 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1-13

由 Gluon 提供的 JavaFX 下载站点

为您之前安装的 OpenJDK 版本选择匹配的 JavaFX 版本,在撰写本文时是 JavaFX 17。您可能需要向下滚动“长期支持”构建部分,并在页面的更下方找到“最新版本”。为您选择的平台下载 SDK。

下载完成后,将 JavaFX SDK 解压缩到您选择的目录中。出于本书的目的,我们将假设您将它留在了下载文件夹中,但是您可以随意将它移动到一个更永久的位置。

现在您的 Java 和 JavaFX 安装已经准备好了,您可以创建一个现代化的客户端应用程序了。在下一节中,我们将带您完成第一个客户机应用程序的步骤。

您的第一个现代 Java 客户端

我们将指导您使用 JavaFX 技术创建您的第一个客户端应用程序,Java FX 技术是可用的最现代的用户界面平台。有一套丰富的 Java 开发工具也可以用 JavaFX 构建 ui,所以对于第一个教程,您甚至不需要编写一行代码。然而,这将使您对现代客户端技术的可能性有所了解,并为后续章节中介绍的 UI 概念打下基础。

用 IntelliJ IDEA 编写客户端应用程序

可以用您选择的任何 IDE 编写 JavaFX 应用程序,包括 IntelliJ、NetBeans、Eclipse 或 Visual Studio 代码。然而,我们将使用 IntelliJ Community Edition 介绍客户端开发,这是 Java 编码的行业标准,对客户端开发完全免费。

要下载 IntelliJ 的最新版本,请前往 https://www.jetbrains.com/idea/ 并点击“下载”这将把你带到图 1-14 所示的下载页面,在这里你可以选择免费下载开源社区版。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1-14

IntelliJ 社区版下载

下载后,安装 IntelliJ 并启动它。您将看到一些配置 IntelliJ 的选项。你可以随意定制,但默认值应该没问题。创建一个新项目,你会得到如图 1-15 所示的对话框。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1-15

IntelliJ 中的新项目创建对话框

它应该会自动获取您之前配置的系统 JDK。在这里,您可以在左侧窗格中选择 JavaFX,它将为您提供一个 Java FX 应用程序模板。点击“下一步”,选择一个项目名称,如“HelloModernWorld”,然后点击“完成”这将在如图 1-16 所示的新窗口中打开您的项目。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1-16

你好,现代世界项目已创建

标准项目创建一个最小的 JavaFX 应用程序,其中包含一个主类、一个控制器和一个用于用户界面的 FXML 文件。这个项目还不能运行,因为它找不到我们之前下载的 JavaFX 库,这从红色突出显示中可以明显看出。为了解决这个问题,我们需要在 JavaFX 库上添加一个模块依赖。

在“文件”菜单中,选择“项目结构…”以打开项目设置对话框。在这里,选择左侧的“模块”并选择“依赖关系”选项卡,以进入图 1-17 所示的屏幕。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1-17

IntelliJ 中的项目结构对话框

要添加 JavaFX 库,请单击窗格底部的“+”符号并选择“JARs or directories”然后导航到您先前下载并解压缩的 OpenJFX JDK,并选择“lib”文件夹。单击“OK”,这将完成依赖关系,修复语法突出显示。

您可以尝试通过进入“运行”菜单并选择“运行‘Main’”来运行应用程序,但是 JavaFX 的配置并不完整,您仍然会得到如图 1-18 所示的错误。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1-18

缺少 JavaFX 运行时组件时出现运行错误

要添加缺少的运行时组件,请打开配置对话框,方法是转到“运行”菜单并选择“编辑配置…”这将打开运行/调试配置对话框,您可以在其中输入传递给 Java 虚拟机的 VM 选项。

要指定要使用的 JavaFX 运行时库,请按如下方式传入 VM 选项:

--module-path /Users/schin/Downloads/javafx-sdk-17/lib --add-modules javafx.controls,javafx.fxml

确保将模块路径修改为用户的正确位置(必须是完全限定的)、JavaFX SDK 的正确版本以及特定于平台的路径分隔符。确保正确的一个简单方法是右键单击该字段并选择“Insert Path ”,这样您将得到一个标准的文件系统选择器,IntelliJ 将为您的操作系统创建正确的路径格式。

还要注意,我们只指定了javafx.controlsjavafx.fxml模块。如果您的应用程序需要额外的模块,请确保在这个逗号分隔的列表中指定它们。然而,如果模块被模块间的依赖关系自动包含,例如所有其他模块都依赖的javafx.base,那么您通常可以排除这些模块。

单击“确定”关闭此对话框并再次运行您的应用程序。这一次,应用程序应该编译和执行无误;然而,由于它只是一个存根,您将得到一个如图 1-19 所示的空窗口。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1-19

空 Hello World 应用程序

在下一节中,我们将向您展示如何修改这个应用程序来构建一个快速的 Hello Modern World 应用程序。

使用场景构建器快速开发应用程序

Scene Builder 是快速构建现代客户端应用程序的绝佳工具。当您构建应用程序时,它提供了所见即所得的可视化表示,为布局和控件提供了方便的调色板,为添加到场景图的组件提供了属性编辑器。它还直接操作作为中间格式的 FXML 文件,以声明方式指定用户界面,而不会影响 Java 代码。

IntelliJ 模板已经包含了样板代码,可以根据 FXML 文件创建新的应用程序,因此我们只需在 Scene Builder 中编辑 FXML 文件来修改用户界面。

Scene Builder 也是由 Gluon 构建、打包和分发的,所以我们可以从获得 JavaFX 的同一个网站下载它。进入 https://gluonhq.com/products/scene-builder/ ,你会看到一个类似于图 1-20 的下载页面,在这里你可以获得适合你的操作系统的场景构建器的正确版本。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1-20

场景生成器下载页面由 Gluon 提供

安装并运行 Scene Builder 后,将显示一个欢迎对话框,您可以在其中打开现有项目。导航到 HelloModernWorld 项目文件夹,选择位于src/sample目录中的sample.fxml文件。

这将打开基本的场景生成器用户界面,并显示如图 1-21 所示的空项目。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1-21

在场景构建器中打开的示例项目

对于这个例子,我们将添加几个组件来展示 JavaFX 的图像和文本功能。首先,单击左侧窗格中的“Controls”并将一个新的“ImageView”拖动到中间窗格中,以将其添加到场景图中,这将添加图像控件,如图 1-22 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1-22

添加到场景图的 ImageView 控件

作为背景,我们将使用来自美国宇航局的知识共享许可图像,显示空间站上用于测量太空海风的 RapidScat 仪器的一部分。您可以从以下 URL 下载 1024 像素版本:

http://bit.ly/RapidScat1024

将该文件放在与 FXML 文件相同的文件夹中,然后通过更新右窗格中的图像属性在 Scene Builder 中选择它。要打开文件选择对话框,请单击文本输入字段旁边的“…”按钮。

要增加图像的大小,请单击右侧字段中的“布局”并删除“适合宽度”和“适合高度”值。这将改变布局,以自动缩放到我们导入的图像的大小,而不是如图 1-23 所示限制它。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1-23

显示 RapidScat 仪器 15 作为背景的图像视图

接下来,点击左边的“容器”并拖动一个AnchorPane作为左下方“层次”窗格中GridPane的子节点。这将允许我们添加额外的控件,我们可以拖动场景生成器和位置自由。

在 AnchorPane 下,您可以添加一些不同的控件来编写示例应用程序的文本。我推荐Label控件,它可以在左上角窗格的控件类别下找到。为了创建如图 1-24 所示的布局,我添加了三个标签并修改了字体、大小和颜色,如下所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1-24

Hello Modern World 应用程序上的文本覆盖

  • 你好世界:字体,快递;尺寸,96px;颜色,白色

  • Java:字体,系统;尺寸,77px;颜色,#FFA518

  • FX:字体,系统;尺寸,77px;颜色,#5382A1

最后,为了让文本突出一点,你可以添加一个视觉效果。在“层次”面板中选择 AnchorPane 元素,进入“修改”菜单,选择“设置效果”子菜单。您可以选择不同的效果应用于场景图中的任何元素。选择“绽放”效果,会得到鲜明的视觉风格。

转到“文件”菜单并选择“保存”,保存对 FXML 文件的更改这将自动更新项目中的文件,并允许您立即运行应用程序并查看更改。

切换回 IntelliJ IDEA。在运行项目之前,我们将对 Main.java 类进行一些更新:

  • 删除Scene上的尺寸约束。只需删除构造器中指定固定大小的第二个和第三个参数。

  • 更改窗口标题。只需更新setTitle调用,将窗口命名为“Hello Modern World ”,以匹配项目名称。

更新后的 Main.java 代码如清单 1-1 所示。

public class Main extends Application {
    @Override
    public void start(Stage primaryStage) throws Exception{
        Parent root = FXMLLoader.load(getClass().getResource("sample.fxml"));
        primaryStage.setTitle("Hello Modern World");
        primaryStage.setScene(new Scene(root));
        primaryStage.show();
    }
    public static void main(String[] args) {
        launch(args);
    }
}

Listing 1-1Main class for Hello Modern World

现在尝试再次运行您的项目。通过对 FXML 文件和 Main.java 类的更新,您应该会看到一个现代的 Hello World 示例,如图 1-25 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1-25

已完成 Hello Modern World 应用程序

现代客户端开发之路

本章通过真实世界中使用的应用程序示例,为理解 Java 客户端编程的最新发展奠定了基础,您可能甚至没有意识到这些应用程序是用这种非常强大的编程语言编写的。此外,您还了解了为什么您应该采用移动优先的方法来构建现代客户端应用程序,为云构建并为最终用户打包您的应用程序。您还能够成功地在 JavaFX 中构建您的第一个现代客户端应用程序,Java FX 是 Java 客户端框架的最新版本。

在接下来的章节中,我们将更详细地讨论这些主题,详细阐述如何为多个平台打包您的应用程序,构建您的应用程序以与 REST 和其他云架构集成,并展示 Java 客户端框架的最新功能。我们希望你和我们一样渴望学习如何构建现代化的 ui,来书写和分享这些知识。

MINT 软件系统-欧洲航空培训研讨会(EATS)。检索自 www.eats-event.com/mint/ 。2019 年 7 月 19 日。

2

亚历山大·卡萨尔。20 个 JavaFX 实际应用程序。检索自 https://jaxenter.com/20-javafx-real-world-applications-123653.html 。2016 年 2 月 11 日。

3

维基百科。《我的世界》。检索自 https://en.wikipedia.org/wiki/Minecraft 。2019 年 8 月。

4

吉尔伯特本。“《我的世界》”仍然是世界上最大的游戏之一,每月有超过 9100 万人玩。检索自 www.businessinsider.com/minecraft-has-74-million-monthly-players-2018-1 。2018 年 10 月。

5

运行于 https://tingsterland.com/ 的《我的世界》服务器截图

6

Oracle 通过最新的 Java 版本提高开发人员的工作效率。检索自 www.prnewswire.com/news-releases/oracle-makes-developers-more-productive-with-latest-java-release-300814269.html 。2019 年 3 月。

7

胶子官网: https://gluonhq.com/

8

Devoxx iOS 会议应用的截图。官方发布会网址: https://devoxx.com/

9

2009 年至 2021 年全球手机网页的百分比。检索自 https://gs.statcounter.com/platform-market-share/desktop-mobile-tablet/worldwide/#yearly-2009-2021 。2021 年 4 月。

10

移动概况介绍。检索自 www.pewinternet.org/fact-sheet/mobile/ 。2021 年 6 月。

11

ETEO–一个团队–一个办公室。宣传视频: www.youtube.com/watch?v=mX1SvXeUetQ

12

SynchronizeFX 的开源回购: https://github.com/saxsys/SynchronizeFX

13

iPhone 顶级应用的规模在四年内增长了 1000%。检索自 https://sensortower.com/blog/ios-app-size-growth 。2017 年 6 月。

14

Karakun 已经启动了一个为 Java 11+复兴 Java Web Start 的项目,如果你在 Web 部署方面投入很大,这是一个不错的选择: https://openwebstart.com/

15

美国宇航局。专用灵巧机械手(SPDM),携带 RapidScat 仪器组件。检索自 https://commons.wikimedia.org/wiki/File:ISS-RapidScat_nadir_adapter_removed_from_CRS-4_Dragon_trunk_ (ISS041E049097).jpg。2014 年 9 月。

二、JavaFX 基础知识

盖尔·安德森和保罗·安德松写的

在您的系统上安装了 Java SDK 和 JavaFX 之后,让我们创建一些应用程序并探索 JavaFX 的基础。首先,我们将描述 JavaFX 应用程序的基本结构,以及使 JavaFX 成为现代客户机的强大选择的一些特性。我们将向您展示如何创建具有吸引力和响应性的 ui。我们将看看 FXML,它是一种基于 XML 的标记语言,允许您定义和配置 UI。我们还将介绍 Scene Builder,这是一个用于设计和配置 JavaFX UI 的独立拖放工具。

为了进一步改进或完全重新设计您的 UI,JavaFX 使用级联样式表(CSS)。我们将向您展示几种在 JavaFX 中使用 CSS 的方法。

JavaFX 属性提供了强大的绑定机制。我们将介绍 JavaFX 属性和绑定。我们将展示为什么 JavaFX observables 和 binding 有助于创建比庞大的侦听器更不容易出错的紧凑代码。我们还将探索几个布局控件,并向您展示将动画融入 UI 是多么容易。

我们将用一个示例应用程序来结束本章,该应用程序使用 JavaFX 集合、一个可编辑的表单和用于典型数据库 CRUD 操作的按钮来实现一个主从 UI。

在这一章中,我们将介绍后续章节更深入讨论的主题。这是为了让您体验 JavaFX 的潜力,并为在本书中进一步探索 JavaFX 提供基础知识。开始吧!

JavaFX 舞台和场景图

JavaFX 应用程序由 JavaFX 平台控制,JavaFX 平台是构建应用程序对象和 Java FX 应用程序线程的运行时系统。要构建 JavaFX 应用程序,必须扩展 JavaFX 应用程序类。JavaFX 运行时系统控制应用程序的生命周期,并调用应用程序的start()方法。

JavaFX 用了一个剧场的比喻:顶层容器就是舞台,由平台为你构建。在桌面应用程序中,舞台就是窗口。它的外观取决于主机系统,并因 macOS、Windows 和 Linux 平台而异。通常,窗口用调整大小、最小化和退出应用程序的控件来修饰。也可以建造无装饰的窗户。您也可以为其他环境专门化应用程序类。例如,使用 Gluon 移动应用框架,您的程序扩展了移动应用,这是一个专门为移动设备编写的应用类。

JavaFX 是单线程的

您必须始终在 JavaFX 应用程序线程上构造和修改舞台及其场景对象。注意 JavaFX(像 Swing 一样)是单线程 UI 模型。对于 JavaFX 开发人员来说,这是一个非常简单的限制。当您创建 UI 元素、响应事件处理程序、使用动画管理动态内容或在场景图中进行更改时,工作会继续在 JavaFX 应用程序线程上执行。

但是,为了保持 UI 的响应性,您应该将长时间运行的工作分配给单独线程中的后台任务。在这种情况下,修改 UI 的工作必须与在后台线程上执行的工作分开。幸运的是,JavaFX 有一个开发良好的并发 API,可以帮助开发人员将长期运行的任务分配给一个或多个单独的线程。这使得 UI 线程能够响应用户事件。这些主题将在第十三章“机器学习和 JavaFX”中探讨。

分层节点结构

继续剧院的比喻,舞台拥有一个场景。场景由 JavaFX 元素组成,比如根元素,它是顶层场景元素,包含所谓的场景图。

场景图是一个严格的层次结构,它由可视化应用程序的元素组成。这些元素被称为节点。一个节点只有一个父节点(根节点除外),并且可以包含其他节点。或者节点可以是没有子节点的叶节点。必须将节点添加到场景图中,以便参与该场景的渲染。此外,一个节点只能添加到一个场景中一次,除非先将其移除,然后再添加到其他地方。

父节点通常通过根据布局规则和您配置的任何约束在场景中排列子节点来管理子节点。JavaFX 对 2D 图形使用二维坐标系,原点在场景的左上角,如图 2-1 所示。x 轴上的坐标值向右增加,y 轴值随着场景向下移动而增加。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2-1

JavaFX 2D 坐标系

JavaFX 还支持 3D 图形,并用 z 轴值表示第三维,提供深度。请参见第八章“JavaFX 3D”,以“深入”了解 JavaFX 的三维功能。

除了相对于父对象的局部坐标系之外,JavaFX 还有一个绝对坐标系。在每种情况下,坐标系的原点都是父坐标系的左上角。通常,布局控件隐藏了场景中组件放置的复杂性,并为您管理其子组件的放置。组件放置基于特定的布局控件以及您对它的配置方式。

也可以嵌套布局控件。例如,可以将多个 VBox 控件放在一个 HBox 中,或将一个 AnchorPane 放在 SplitPane 控件的一个窗格中。其他父节点是更复杂的可视节点,如 TextField、TextArea 和 Button。这些节点有受管理的子部分。例如,按钮包括带标签的文本部分和可选图形。此图形可以是任何节点类型,但通常是图像或图标。

回想一下,叶节点没有子节点。示例包括形状(如矩形、椭圆形、直线、路径和文本)和 ImageView,即用于呈现图像的节点。

一个简单的形状示例

图 2-2 显示了一个名为 MyShapes 的简单 JavaFX 应用程序,它在应用程序窗口的中心显示一个椭圆和一个文本元素。此窗口的外观因底层平台而异。调整窗口大小时,可见元素将在调整后的空间中保持居中。尽管这是一个简单的程序,但是关于 JavaFX 呈现、布局特性和节点还有很多东西需要学习。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2-2

MyShapes 应用程序

清单 2-1 显示了 MyShapes 程序的源代码。类 MyShapes 是主类,它扩展了应用程序。JavaFX 运行时系统实例化 MyShapes 和初级 Stage,并将其传递给被覆盖的start()方法。运行时系统为您调用start()方法。

package org.modernclient;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Ellipse;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.stage.Stage;
public class MyShapes extends Application {
    @Override
    public void start(Stage stage) throws Exception {
        // Create an Ellipse and set fill color
        Ellipse ellipse = new Ellipse(110, 70);
        ellipse.setFill(Color.LIGHTBLUE);
        // Create a Text shape with font and size
        Text text = new Text("My Shapes");
        text.setFont(new Font("Arial Bold", 24));
        StackPane stackPane = new StackPane();
        stackPane.getChildren().addAll(ellipse, text);
        Scene scene = new Scene(stackPane, 350, 230,
                       Color.LIGHTYELLOW);
        stage.setTitle("MyShapes with JavaFX");
        stage.setScene(scene);
        stage.show();
    }
    public static void main(String[] args) {
        launch(args);
    }
}

Listing 2-1MyShapes.java

请注意引用了javafx.applicationjavafx.scenejavafx.stage中的包的导入语句。

Note

确保为任何导入语句指定正确的包。一些 JavaFX 类,比如 Rectangle,与它们的 AWT 或 Swing 对应物具有相同的类名。所有 JavaFX 类都是包javafx的一部分。

这个程序创建几个节点,并将它们添加到 StackPane 布局容器中。该程序还创建场景,配置舞台,并显示舞台。让我们详细看看这些步骤。

首先,我们创建一个椭圆形状,以像素为单位提供宽度和高度。因为椭圆扩展了形状,所以我们也可以配置任何形状属性。这包括“填充”,它允许您指定内部绘制值。

颜色

形状的填充属性可以是 JavaFX 颜色、线性渐变、径向渐变或图像。我们简单讨论一下色彩。您可以用几种方法在 JavaFX 中指定颜色。这里,我们将椭圆填充属性设置为Color.LIGHTBLUE。JavaFX 颜色类中目前有 147 种预定义的颜色,按字母顺序从ALICEBLUEYELLOWGREEN命名。但是,您也可以使用十六进制或十进制的 web RGB 值来指定颜色。您可以选择提供透明度的 alpha 值。完全不透明为 1,完全透明为 0。例如,0.5 的透明度显示了颜色,但也让背景色显示出来。

以下是设定形状填充颜色的几个示例:

ellipse.setFill(Color.LIGHTBLUE);              // Light blue, fully opaque
ellipse.setFill(Color.web("#ADD8E6"));         // Light blue, fully opaque
ellipse.setFill(Color.web("#ADD8E680"));       // Light blue, .5 opaque
ellipse.setFill(Color.web("0xADD8E6"));        // Light blue, fully opaque
ellipse.setFill(Color.web("0xADD8E680"));      // Light blue, .5 opaque
ellipse.setFill(Color.rgb(173, 216, 230));     // Light blue, fully opaque
ellipse.setFill(Color.rgb(173, 216, 230, .5)); // Light blue, .5 opaque

第五章,“掌握视觉和 CSS 设计”,向您展示了使用 CSS 和 JavaFX 指定颜色、渐变和图像的附加选项。

值得注意的是,您可以插入颜色的值,这就是 JavaFX 构建渐变的方式。我们将告诉你如何创建一个线性渐变。

文本是一种形状

接下来,我们创建一个文本对象。文本也是具有附加属性的形状,如字体、文本对齐方式、文本和环绕宽度。构造器提供文本,setFont()方法设置它的字体。

JavaFX 坐标系

注意,我们创建了椭圆和文本节点,但是它们还没有出现在我们的场景图中。在我们将它们添加到场景中之前,我们必须将这些节点放在某种布局容器中。布局控件在管理场景图时非常重要。这些控件不仅可以为您安排组件,还可以响应事件,例如调整大小、添加或移除元素,以及对场景图中一个或多个节点大小的任何更改。

为了向您展示布局控件的重要性,让我们用一个组替换清单 2-1 中的 StackPane,并手动指定位置。组是管理其子节点的父节点,但不提供任何布局功能。这里我们创建一个组,并用构造器添加椭圆和文本元素。然后我们指定组作为场景的根节点:

        Group group = new Group(ellipse, text);
         ...
        Scene scene = new Scene(group, 350, 230, Color.LIGHTYELLOW);

组为其子对象使用默认对齐设置,并将所有内容放置在原点(0,0),即场景的左上角。对于文本,默认位置是文本元素的左下边缘。在这种情况下,唯一可见的部分将是延伸到下边缘下方的字母(“我的形状”的小写字母“y”和“p”)。椭圆将以组原点(0,0)为中心,因此只有右下象限可见。

这种安排显然不是我们想要的。要解决这个问题,让我们手动将 350 × 230 场景中的形状居中,如下所示:

        Group group = new Group(ellipse, text);
        // Manually placing components is tedious and error-prone
        ellipse.setCenterX(175);
        ellipse.setCenterY(115);
        text.setX(175-(text.getLayoutBounds().getWidth()/2));
        text.setY(115+(text.getLayoutBounds().getHeight()/2));
        ...
        Scene scene = new Scene(group, 350, 230, Color.LIGHTYELLOW);

现在,形状将会很好地在场景中居中。但这仍不理想。当窗口调整大小时,这些形状将停留在场景中的这些坐标上(除非您编写代码来检测和响应窗口调整)。你不想这么做。而是使用 JavaFX 布局控件!

布局控件

现在让我们稍微绕道来讨论一些常见的布局控件。要管理场景的节点,可以使用一个或多个控件。每个控件都是为特定的布局配置而设计的。此外,您可以嵌套布局控件来管理节点组,并指定布局应该如何响应事件,如调整托管节点的大小或对其进行更改。您可以指定对齐设置以及边距控制和填充。

有几种方法可以将节点添加到布局容器中。您可以使用布局容器的构造器添加子节点。您还可以将方法getChildren().add()用于单个节点,将方法getChildren().addAll()用于多个节点。此外,一些布局控件有专门的方法来添加节点。现在让我们看看几个常用的布局控件,向您展示 JavaFX 如何为您构建一个场景。

面板

一个方便简单的布局容器是 StackPane,我们在清单 2-1 中使用了它。此布局控件按照您添加节点的顺序从后向前堆叠其子控件。注意,我们首先添加椭圆,使它出现在文本节点的后面。在相反的顺序中,椭圆会遮蔽文本元素。

默认情况下,StackPane 将其所有子节点居中。您可以为子级提供不同的对齐方式,或者将对齐方式应用于 StackPane 中的特定节点。例如,

        // align the text only
        stackPane.setAlignment(text, Pos.BOTTOM_CENTER);

将文本节点沿 StackPane 的下边缘居中。现在,当您调整窗口大小时,椭圆保持居中,文本保持锚定在窗口的下边缘。若要指定所有托管节点与下边缘的对齐方式,请使用

        // align all managed nodes
        stackPane.setAlignment(Pos.BOTTOM_CENTER);

虽然椭圆和文本都出现在窗口的底部,但它们不会相对于彼此居中,因为它们将在各自的底部边缘对齐。

锚板

AnchorPane 根据配置的定位点管理其子节点,即使容器调整大小时也是如此。您可以为组件指定距窗格边缘的偏移。在这里,我们向 AnchorPane 添加一个标签,并以 10 像素的偏移量将其锚定到窗格的左下方:

        AnchorPane anchorPane = new AnchorPane();
        Label label = new Label("My Label");
        anchorPane.getChildren().add(label);
        AnchorPane.setLeftAnchor(label, 10.0);
        AnchorPane.setBottomAnchor(label, 10.0);

AnchorPane 通常用作顶级布局管理器来控制边距,即使在调整窗口大小时也是如此。

组件

GridPane 允许您将子节点放在大小灵活的二维网格中。组件可以跨越行和/或列,但是给定行中所有组件的行大小是一致的。同样,对于给定的列,列的宽度是一致的。GridPane 有专门的方法将节点添加到由列号和行号指定的特定单元格中。可选参数允许您指定列和行的跨度值。例如,这里的第一个标签放在对应于第 0 列和第 0 行的单元格中。第二个标签放在对应于第 1 列和第 0 行的单元格中,它跨越两列(第二列和第三列)。我们还必须提供一个行跨度值(这里设置为 1):

        GridPane gridPane = new GridPane();
        gridPane.add(new Label("Label1"), 0, 0);
        gridPane.add(new Label("Label2 is very long"), 1, 0, 2, 1);

GridPane 对于在容纳各种大小的列或行的表单中布局组件很有用。GridPane 还允许节点跨越多列或多行。我们在我们的主-细节 UI 示例中使用 GridPane(参见本章后面的“将它们放在一起”)。

FlowPane 和 TilePane

FlowPane 以水平流或垂直流的方式管理其子节点。默认方向是水平的。可以用构造器或者使用方法setOrientation()指定流向。这里,我们用构造器指定一个垂直方向:

        FlowPane flowpane = new FlowPane(Orientation.VERTICAL);

FlowPane 根据可配置的边界包装子节点。如果调整包含流窗格的窗格的大小,布局将根据需要调整流。单元的大小取决于节点的大小,除非所有节点的大小都相同,否则它不会是统一的网格。这种布局对于大小可变的节点很方便,例如 ImageView 节点或形状。TilePane 类似于 FlowPane,只是 TilePane 使用大小相等的单元格。

BorderPane

BorderPane 对于具有离散部分的桌面应用程序来说很方便,包括一个顶部工具栏(顶部)、一个底部状态栏(底部)、一个中心工作区(中心)和两个侧边区域(左右)。这五个部分中的任何一个都可以是空的。下面是一个边框窗格的示例,中间是一个矩形,顶部是一个标签:

        BorderPane borderPane = new BorderPane();
        Label colorLabel = new Label("Color: Lightblue");
        colorLabel.setFont(new Font("Verdana", 18));
        borderPane.setTop(colorLabel);
        Rectangle rectangle = new Rectangle(100, 50, Color.LIGHTBLUE);
        borderPane.setCenter(rectangle);
        borderPane.setAlignment(colorLabel, Pos.CENTER);
        borderPane.setMargin(colorLabel, new Insets(20,10,5,10));

请注意,默认情况下,BorderPane 对中心区域使用居中对齐,对顶部使用左对齐。为了保持顶部区域标签居中,我们用Pos.CENTER配置它的对齐方式。我们还用 BorderPane 静态方法setMargin()设置标签周围的边距。Insets 构造器接受四个值,分别对应于上、右、下和左边缘。类似的对齐和边距配置也适用于其他布局组件。

分屏

SplitPane 将布局空间划分为多个水平或垂直配置的区域。分隔线是可移动的,通常在 SplitPane 的每个区域中使用其他布局控件。我们在我们的主-细节 UI 示例中使用 SplitPane(参见本章后面的“将它们放在一起”)。

HBox、VBox 和按钮栏

HBox 和 VBox 布局控件为子节点提供单一水平或垂直位置。您可以将 HBox 节点嵌套在 VBox 中以获得类似网格的效果,或者将 VBox 节点嵌套在 HBox 组件中。ButtonBar 便于在水平容器中放置一排大小相等的按钮。

有关这些和其他布局控件的详细信息,请参见第四章“JavaFX 控件深入研究”

大吵大闹

回到清单 2-1 ,场景持有场景图,由它的根节点定义。首先,我们构建场景并提供stackPane作为根节点。然后,我们以像素为单位指定它的宽度和高度,并为背景提供一个可选的 fill 参数(Color.LIGHTYELLOW)。

剩下的就是配置舞台了。我们提供一个标题,设置场景,展示舞台。JavaFX 运行时渲染我们的场景,如图 2-2 所示。

图 2-3 显示了我们的 MyShapes 应用程序的场景图的层次视图。根节点是 StackPane,它包含两个子节点 Ellipse 和 Text。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2-3

MyShapes 场景图

增强 MyShapes 应用程序

与旧的 UI 工具包相比,JavaFX 的优势之一是可以轻松地将效果、渐变和动画应用到场景图中的节点。我们将反复讨论场景图节点的概念,因为这是 JavaFX 运行时高效呈现应用程序可视部分的方式。现在让我们对 MyShapes 进行一些修改,向您展示其中的一些功能。因为 JavaFX 能够插值颜色,所以您可以使用颜色来定义渐变。渐变赋予形状深度,可以是径向的,也可以是线性的。让我们给你看一个线性梯度。

线性梯度

线性渐变需要两种或两种以上的颜色,称为色标。渐变色标由一种颜色和一个介于 0 和 1 之间的偏移量组成。此偏移量指定沿渐变放置颜色的位置。渐变计算从一个色标到下一个色标的比例阴影。

在我们的例子中,我们将使用三个色标:Color.DODGERBLUEColor.LIGHTBLUEColor.GREEN。第一个停靠点的偏移量为 0,第二个偏移量为. 5,第三个偏移量为 1.0,如下所示:

        Stop[] stops = new Stop[] { new Stop(0, Color.DODGERBLUE),
                new Stop(0.5, Color.LIGHTBLUE),
                new Stop(1.0, Color.LIGHTGREEN)};

LinearGradient 构造器指定 x 轴范围,后跟 y 轴范围。下面的线性渐变具有恒定的 x 轴,但其 y 轴是变化的。这被称为垂直梯度。(我们在程序 MyShapes2 中使用这个垂直渐变,如图 2-4 所示。)

        // startX=0, startY=0, endX=0, endY=1
        LinearGradient gradient = new LinearGradient(0, 0, 0, 1, true,
                CycleMethod.NO_CYCLE, stops);

Boolean true 表示渐变贯穿形状(其中 0 和 1 与形状成比例),而NO_CYCLE表示图案不重复。布尔值 false 表示渐变的 x 和 y 值相对于父级的本地坐标系。

要制作水平渐变,请指定 x 轴的范围,并使 y 轴保持不变,如下所示:

        // startX=0, startY=0, endX=1, endY=0
        LinearGradient gradient = new LinearGradient(0, 0, 1, 0, true,
                CycleMethod.NO_CYCLE, stops);

其他组合允许您指定对角线渐变或反向渐变,其中颜色以相反的顺序出现。

阴影

接下来,让我们添加一个投影效果的椭圆。您可以指定投影的颜色、半径以及 x 和 y 偏移。半径越大,阴影越大。偏移量表示相对于形状外边缘的阴影位置。这里,我们指定半径为 30 个像素,在形状的右下方偏移 10 个像素:

        ellipse.setEffect(new DropShadow(30, 10, 10, Color.GRAY));

这些偏移模拟从场景左上角发出的光源。当偏移为 0 时,阴影包围整个形状,就好像光源直接照射在场景上方。

反射

反射效果镜像组件,并淡入透明,这取决于您如何配置其顶部和底部不透明度、分数和偏移。让我们给文本节点添加一个反射效果。我们将使用 0.8 作为分数,这样反射将是反射分量的十分之八。偏移以像素为单位指定反射从下边缘以下多远开始。我们指定 1 个像素(默认值为 0)。倒影从完全不透明(顶部不透明度)开始,并过渡到完全透明(底部不透明度),除非您修改顶部和底部不透明度值:

        Reflection r = new Reflection();
        r.setFraction(.8);
        r.setTopOffset(1.0);
        text.setEffect(r);

图 2-4 显示了在窗口中运行的增强的 MyShapes 程序。您会看到应用于椭圆的线性渐变填充、椭圆上的投影以及应用于文本的反射效果。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2-4

增强型 MyShapes 应用程序(MyShapes2)

配置操作

现在是时候让我们的应用程序做点什么了。JavaFX 用鼠标、手势、触摸或按键定义了各种类型的标准输入事件。这些输入事件类型都有特定的处理程序来处理它们。

现在让我们把事情简单化。我们将向您展示如何编写一个事件处理程序来处理单个鼠标点击事件。我们将创建处理程序,并将其附加到场景图中的一个节点上。程序的行为将根据哪个节点获取处理程序而有所不同。我们可以在文本、椭圆或 StackPane 节点上配置鼠标单击处理程序。

下面是将动作事件处理程序添加到文本节点的代码:

        text.setOnMouseClicked(mouseEvent -> {
            System.out.println(mouseEvent.getSource().getClass()
                 + " clicked.");
        });

当用户在文本中单击时,程序显示该行

        class javafx.scene.text.Text clicked.

如果用户在背景区域(堆栈窗格)或椭圆内单击,则不会发生任何事情。如果我们将同一个侦听器附加到椭圆而不是文本,我们会看到这条线

        class javafx.scene.shape.Ellipse clicked.

请注意,因为文本对象出现在堆栈窗格中的椭圆前面,所以单击文本对象不会调用事件处理程序。即使这些场景图节点出现在彼此的顶部,它们在层次中也是单独的节点。也就是说,一个不在另一个里面;相反,它们都是由堆栈窗格管理的不同叶节点。在这种情况下,如果希望两个节点都响应鼠标单击,可以将鼠标事件处理程序附加到两个节点上。或者您可以只将一个事件处理程序附加到 StackPane 节点。然后,在窗口内的任何地方单击鼠标都会触发处理程序,输出如下:

        class javafx.scene.layout.StackPane clicked.

让我们做一些更令人兴奋的事情,将动画应用到 MyShapes 程序中。

动画

当您使用内置的转换 API 时,JavaFX 使动画变得非常容易。每个 JavaFX 转换类型控制一个或多个节点(或形状)属性。例如,FadeTransition 控制节点的不透明度,随时间改变属性。要逐渐淡出某些东西,您可以将其不透明度从完全不透明(1)更改为完全透明(0)。TranslateTransition 通过修改节点的 translateX 和 translateY 属性(如果在 3D 中工作,则为 translateZ)来移动节点。

您可以使用 ParallelTransition 并行播放多个过渡,或者使用 SequentialTransition 顺序播放多个过渡。要控制两个连续转换之间的时序,使用 PauseTransition 或使用转换方法setDelay()配置转换开始前的延迟。您还可以使用 transition action 事件处理程序属性onFinished来定义转换完成时的动作。

过渡从方法play()playFromStart()开始。方法play()在当前时间开始转换;方法playFromStart()总是从时间 0 开始。其他方法还有stop()pause()。您可以用getStatus()查询一个转换的状态,它会返回一个动画。状态枚举值:RUNNINGPAUSEDSTOPPED

所有过渡都支持通用属性durationautoReversecycleCountonFinishedcurrentTime,以及nodeshape(针对特定形状的过渡)。

现在让我们为 MyShapes 程序定义一个 RotateTransition。当用户在窗口内单击时,旋转开始。图 2-5 显示了旋转过渡期间运行的程序。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2-5

具有 RotateTransition 的 MyShapes 应用程序(MyShapes2)

清单 2-2 展示了 MyShapes 程序的start()方法中的动画代码。

public class MyShapes extends Application {
    @Override
    public void start(Stage stage) throws Exception {
         ...
        // Define RotateTransition
        RotateTransition rotate = new RotateTransition(
                       Duration.millis(2500), stackPane);
        rotate.setToAngle(360);
        rotate.setFromAngle(0);
        rotate.setInterpolator(Interpolator.LINEAR);
        // configure mouse click handler
        stackPane.setOnMouseClicked(mouseEvent -> {
            if (rotate.getStatus().equals(Animation.Status.RUNNING)) {
                rotate.pause();
            } else {
                rotate.play();
            }
        });
        ...
    }
}

Listing 2-2Using RotateTransition

RotateTransition 构造器指定 2500 毫秒的持续时间,并将转换应用于 StackPane 节点。旋转动画从角度 0 开始,线性地进行到角度 360,提供一次完整的旋转。当用户单击 StackPane 布局控件内的任意位置时,动画开始。

在这个例子中有一些有趣的事情需要注意。首先,因为我们在 StackPane 节点上定义了转换,所以旋转应用于 StackPane 的所有子节点。这意味着不仅椭圆和文本形状会旋转,投影和反射效果也会旋转。

其次,事件处理程序检查转换状态。如果动画正在进行(运行),事件处理程序会暂停过渡。如果它没有运行,它会用play()启动它。因为play()在过渡的当前时间开始,所以pause()后跟play()从暂停的地方恢复过渡。

JavaFX 属性

通过操纵节点的属性来控制节点。JavaFX 属性类似于常规的 JavaBean 属性。它们有 setters 和 getters,通常保存值,并遵循相同的命名约定。但是 JavaFX 属性更强大,因为它们是可观察的。在这一节中,我们将介绍 JavaFX 属性、侦听器和绑定的概念,它们帮助您配置和控制场景图中的节点。

您已经看到了如何通过操纵与节点相关的属性来配置场景图形中的节点。例如,椭圆中的 fill 属性提供了形状的内部颜色。同样,高度和宽度属性定义了椭圆的大小。font 属性定义文本的字体,它的 text 属性保存单词“我的形状”

因为 JavaFX 属性是可观察的,所以您可以定义在属性值更改或无效时得到通知的侦听器。此外,您可以使用内置的绑定机制将一个或多个属性的值链接到另一个属性。您可以指定单向绑定或双向绑定。您甚至可以定义自己的 JavaFX 属性,并将它们作为模型对象或控制对象的一部分包含在您的程序中。

为了使用绑定表达式或将监听器附加到 JavaFX 属性,您必须通过属性的属性 getter 来访问属性。按照惯例,属性 getter 是小写字母的属性名称,后跟大写字母“p”的单词 property。例如,fill 属性的属性 getter 是fillProperty() ,,节点的 opacity 属性的属性 getter 是opacityProperty()。使用任何属性 getter,您都可以访问属性元数据(比如用属性 getter 方法getName()访问它的名称,用属性 getter 方法getValue()访问它的值)。让我们首先向您展示属性侦听器。

属性侦听器

应用于对象属性(不是集合)的 JavaFX 属性侦听器有两种类型:失效侦听器和更改侦听器。当属性值不再有效时,将触发失效侦听器。对于这个例子和后面的例子,我们将讨论 MyShapesProperties 程序,它基于前面的 MyShapes 应用程序。在这个新程序中,我们添加了第二个文本对象,放在旋转 StackPane 下面的 VBox 布局控件中。图 2-6 显示了使用顶级 VBox 更新后的场景图。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2-6

我的形状属性场景图

失效侦听器

失效侦听器有一个方法,您可以用 lambda 表达式覆盖它。让我们先向您展示非 lambda 表达式,这样您就可以看到完整的方法定义。当您单击 StackPane 时,鼠标单击处理程序会像以前一样旋转 StackPane 控件。第二个 Text 对象显示 RotationTransition 动画的状态,该动画由只读 status 属性管理。你会看到RUNNINGPAUSEDSTOPPED。图 2-7 显示动画暂停。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2-7

带有失效侦听器的 MyShapesProperties 应用程序

失效侦听器包括一个允许您访问该属性的可观察对象。因为可观察对象是非泛型的,所以必须应用适当的类型转换来访问属性值。这里有一种方法可以在附加到动画 status 属性的监听器中访问该属性的值。注意,我们用属性 getter 方法statusProperty()附加了监听器:

        rotate.statusProperty().addListener(new InvalidationListener() {
            @Override
            public  void invalidated(Observable observable) {
                text2.setText("Animation status: " +
                    ((ObservableObjectValue<Animation.Status>)observable)
                    .getValue());
            }
        });

这里我们用 lambda 表达式实现了同一个监听器:

        rotate.statusProperty().addListener(observable -> {
            text2.setText("Animation status: " +
                ((ObservableObjectValue<Animation.Status>)observable)
                .getValue());
        });

因为我们只访问 status 属性值,所以可以用方法getStatus()绕过 observable,返回一个 enum。这避免了转换表达式:

        rotate.statusProperty().addListener(observable -> {
            text2.setText("Animation status: " + rotate.getStatus());
        });

更改听众

当您需要访问一个可观察对象的前一个值以及它的当前值时,请使用更改侦听器。变更监听器提供可观察值和新旧值。更改监听器的成本可能会更高,因为它们必须跟踪更多的信息。下面是一个非 lambda 版本的更改监听器,它显示旧值和新值。请注意,您不必强制转换这些参数,因为更改侦听器是通用的:

        rotate.statusProperty().addListener(
                    new ChangeListener<Animation.Status>() {
            @Override
            public void changed(
                ObservableValue<? extends Animation.Status> observableValue,
                  Animation.Status oldValue, Animation.Status newValue) {
                    text2.setText("Was " + oldValue + ", Now " + newValue);
                }
        });

下面是具有更紧凑的 lambda 表达式的版本:

        rotate.statusProperty().addListener(
                    (observableValue, oldValue, newValue) -> {
            text2.setText("Was " + oldValue + ", Now " + newValue);
        });

图 2-8 显示了运行的 MyShapesProperties,其中一个更改监听器连接到动画的 status 属性。现在我们可以显示以前和当前的值。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2-8

带有更改监听器的 MyShapesProperties 应用程序

有约束力的

JavaFX 绑定是一种灵活的、API 丰富的机制,让您在许多情况下避免编写侦听器。使用绑定将 JavaFX 属性的值链接到一个或多个其他 JavaFX 属性。属性绑定可以是单向或双向的。当属性是同一类型时,单向的bind()方法可能就是您所需要的。然而,当属性具有不同的类型或者您想要基于多个属性计算一个值时,那么您将需要 fluent 和 bindings APIs。您还可以使用自定义绑定创建自己的绑定方法。

单向绑定

最简单的绑定形式是将一个属性的值链接到另一个属性的值。这里,我们将text2的 rotate 属性绑定到stackPane的 rotate 属性:

        text2.rotateProperty().bind(stackPane.rotateProperty());

这意味着对stackPane旋转的任何改变都会立即更新text2的旋转属性。当在 MyShapesProperties 程序中设置此绑定时,StackPane 内的任何单击都会启动旋转过渡。这使得堆叠板和text2组件一起旋转。StackPane 旋转是因为我们启动了为该节点定义的 RotateTransition。由于绑定表达式,text2节点旋转。

请注意,在绑定属性时,除非先解除属性绑定,否则无法显式设置其值。

双向绑定

双向绑定提供了两个属性之间的双向关系。当一个属性更新时,另一个属性也会更新。下面是一个包含两个文本属性的示例:

        text2.textProperty().bindBidirectional(text.textProperty());

两个文本控件最初都显示“我的形状”当用户在stackPane内单击并且stackPane旋转时,由于改变监听器,两个文本属性现在都将包含动画状态。

双向绑定不是完全对称的;两个属性的初始值都采用在对bindBidirectional()的调用中传递的属性值。与bind()不同,在使用双向绑定时,可以显式设置任一属性。

流畅 API 和绑定 API

当不止一个属性需要参与绑定时,或者当需要执行某种计算或转换时,fluent 和 bindings APIs 可以帮助您构造绑定表达式。例如,以下绑定表达式显示 StackPane 从 0 度到 360 度旋转时的旋转角度。text 属性是一个字符串,rotate 属性是一个 double。绑定方法asString()将 double 转换为 string,将数字格式化为小数点右边的一个数字:

        text2.textProperty().bind(stackPane.rotateProperty()
               .asString("%.1f"));

对于一个更复杂的例子,让我们根据动画是否正在运行来更新text2的 stroke 属性(它的颜色)。这里我们基于三元表达式构建了一个与When的绑定。这会在动画运行时将笔划颜色设置为绿色,在动画停止或暂停时设置为红色:

        text2.strokeProperty().bind(new When(rotate.statusProperty()
               .isEqualTo(Animation.Status.RUNNING))
               .then(Color.GREEN).otherwise(Color.RED));

text2 text 属性是在 change listener 中设置的,它附加到我们前面展示的动画状态属性。

图 2-9 显示了复杂绑定表达式附加到text2 strokeProperty的应用程序 MyShapesProperties。由于动画正在运行,stroke 属性被设置为Color.GREEN

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2-9

带有 fluent 和绑定 API 的 MyShapesProperties 应用程序

有关 JavaFX 属性和绑定的其他示例,请参见第三章“属性和绑定”

使用 FXML

您已经看到了 JavaFX APIs 如何创建场景图节点并为您配置它们。MyShapes 和 MyShapesProperties 程序仅使用 JavaFX 代码来构建和配置这些对象。另一种方法是用 FXML 声明场景图节点,FXML 是一种基于 XML 的标记符号。FXML 允许您以声明的格式描述和配置场景图形。这种方法有几个优点:

  • FXML 标记结构是分层的,因此它反映了场景图的结构。

  • FXML 描述了您的视图,并支持模型-视图-控制器(MVC)架构,为大型应用程序提供了更好的结构。

  • FXML 减少了创建和配置场景图节点所需编写的 JavaFX 代码。

  • 您可以使用 Scene Builder 设计您的用户界面。这个拖放工具是一个独立的应用程序,提供场景的可视化渲染。Scene Builder 会为您生成 FXML 标记。

  • 您还可以使用文本编辑器和 IDE 编辑器编辑 FXML 标记。

FXML 影响你程序的结构。主应用程序类现在调用 FXMLLoader。这个加载器解析您的 FXML 标记,创建 JavaFX 对象,并将场景图插入到根节点的场景中。可以有多个 FXML 文件,通常每个文件都有一个对应的 JavaFX 控制器类。该控制器类可能包括事件处理程序或其他动态更新场景的语句。控制器还包括管理特定视图的业务逻辑。

让我们回到我们的 MyShapes 示例(现在称为 MyShapesFXML ),使用 FXML 文件作为视图,使用 CSS 作为样式。图 2-10 显示了我们程序中的文件,这些文件是为了与构建工具或 ide 一起使用而排列的。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2-10

带有 FXML 和 CSS 的 MyShapesFXML

JavaFX 源代码出现在 java 子目录下。资源子目录包含 FXML 和 CSS 文件(这里是 Scene.fxmlStyles.css )。

这个程序包括一个旋转堆栈面板、VBox 控件和第二个文本对象。清单 2-3 显示了描述我们的场景图的 FXML 代码:一个包含 StackPane 和 Text 元素的顶级 VBox。堆叠面板包括椭圆和文本形状。

<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.effect.DropShadow?>
<?import javafx.scene.effect.Reflection?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.paint.LinearGradient?>
<?import javafx.scene.paint.Stop?>
<?import javafx.scene.shape.Ellipse?>
<?import javafx.scene.text.Font?>
<?import javafx.scene.text.Text?>
<VBox alignment="CENTER" prefHeight="350.0" prefWidth="350.0" spacing="50.0"
 xmlns:="http://javafx.com/javafx/10.0.1" xmlns:fx=http://javafx.com/fxml/1
 fx:controller="org.modernclient.FXMLController">
    <children>
        <StackPane fx:id="stackPane" onMouseClicked="#handleMouseClick"
                               prefHeight="150.0" prefWidth="200.0">
            <children>
                <Ellipse radiusX="110.0" radiusY="70.0">
                    <fill>
                        <LinearGradient endX="0.5" endY="1.0" startX="0.5">
                            <stops>
                                <Stop color="DODGERBLUE" />
                                <Stop color="LIGHTBLUE" offset="0.5" />
                                <Stop color="LIGHTGREEN" offset="1.0" />
                            </stops>
                        </LinearGradient>
                    </fill>
                    <effect>
                        <DropShadow color="GREY" offsetX="5.0"
                                                 offsetY="5.0" />
                    </effect>
                </Ellipse>
                <Text text="My Shapes">
                    <font>
                        <Font name="Arial Bold" size="24.0" />
                    </font>
                    <effect>
                        <Reflection fraction="0.8" topOffset="1.0" />
                    </effect>
                </Text>
            </children>
        </StackPane>
        <Text fx:id="text2" text="Animation Status: ">
            <font>
                <Font name="Arial Bold" size="18.0" />
            </font>
        </Text>
    </children>
</VBox>

Listing 2-3Scene.fxml

顶层容器包括 JavaFX 控制器类的名称和属性fx:controller。VBox 指定其对齐方式、首选大小和间距,后跟其子元素:StackPane 和 Text。在这里,我们用首选的大小配置 StackPane。一个特殊的属性fx:id指定了对应于这个节点的变量名。在 JavaFX 控制器类中,您现在会看到这个变量名用 StackPane 的@FXML进行了注释。这是您访问在 FXML 文件中声明的控制器类中的对象的方式。

此外,StackPane 指定了一个名为#handleMouseClickonMouseClicked事件处理程序。这个事件处理程序在 JavaFX 控制器类中也用@FXML进行了注释。

这里,StackPane 子节点 Ellipse 和 Text 是在子 FXML 节点中声明的。两者都没有关联的fx:id属性,因为控制器类不需要访问这些对象。您还可以看到线性渐变、投影和反射效果配置。

注意,带有fx:idtext2”的文本对象出现在 StackPane 定义之后。这使得第二个文本对象出现在 VBox 的 StackPane 下。我们还指定了一个fx:id属性来从 JavaFX 控制器访问这个节点。

控制器类别

现在让我们向您展示控制器类。您会注意到代码更加紧凑,因为对象实例化和配置代码不再由 Java 语句完成。所有这些现在都在 FXML 标记中指定。清单 2-4 显示了 FXMLController.java 的控制器代码。

package org.modernclient;
import javafx.animation.Animation;
import javafx.animation.Interpolator;
import javafx.animation.RotateTransition;
import javafx.beans.binding.When;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.text.Text;
import javafx.util.Duration;
import java.net.URL;
import java.util.ResourceBundle;
public class FXMLController implements Initializable {
    @FXML
    private StackPane stackPane;
    @FXML
    private Text text2;
    private RotateTransition rotate;
    @Override
    public void initialize(URL url, ResourceBundle rb) {
        rotate = new RotateTransition(Duration.millis(2500), stackPane);
        rotate.setToAngle(360);
        rotate.setFromAngle(0);
        rotate.setInterpolator(Interpolator.LINEAR);
        rotate.statusProperty().addListener(
                           (observableValue, oldValue, newValue) -> {
            text2.setText("Was " + oldValue + ", Now " + newValue);
        });
        text2.strokeProperty().bind(new When(rotate.statusProperty()
                 .isEqualTo(Animation.Status.RUNNING))
                 .then(Color.GREEN).otherwise(Color.RED));
    }
    @FXML
    private void handleMouseClick(MouseEvent mouseEvent) {
        if (rotate.getStatus().equals(Animation.Status.RUNNING)) {
            rotate.pause();
        } else {
            rotate.play();
        }
    }
}

Listing 2-4FXMLController.java

控制器类实现 Initializable 并覆盖运行时为您调用的方法initialize(),。重要的是,私有类字段stackPanetext2@FXML标注。@FXML注释将控制器类中的变量名与 FXML 文件中描述的对象相关联。控制器类中没有创建这些对象的代码,因为 FXMLLoader 会为您完成这些工作。

initialize()方法在这里做了三件事。首先,它创建并配置 RotateTransition,并将其应用于stackPane节点。其次,它向转换的 status 属性添加了一个更改侦听器。第三,text2 stroke 属性的绑定表达式根据旋转过渡的状态指定其颜色。

带有handleMouseClick()@FXML注释表示 FXML 文件配置了事件处理程序。此鼠标单击事件处理程序启动和停止旋转过渡的动画。

JavaFX 应用程序类

主应用程序类 MyShapesFXML 现在变得非常简单。它的工作是调用 FXMLLoader,解析 FXML ( Scene.fxml ),构建场景图,并返回场景图根。您所要做的就是像以前一样构建场景对象并配置舞台,如清单 2-5 所示。

package org.modernclient;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
public class MyShapesFXML extends Application {
    @Override
    public void start(Stage stage) throws Exception {
        Parent root = FXMLLoader.load(getClass()
                           .getResource("/fxml/Scene.fxml"));
        Scene scene = new Scene(root, Color.LIGHTYELLOW);
        scene.getStylesheets().add(getClass()
            .getResource("/styles/Styles.css").toExternalForm());
        stage.setTitle("MyShapesApp with JavaFX");
        stage.setScene(scene);
        stage.show();
    }
    public static void main(String[] args) {
        launch(args);
    }
}

Listing 2-5MyShapesFXML.java

添加 CSS

现在让我们向您展示如何将您自己的风格与 CSS 结合起来。JavaFX 的一个优点是它能够用 CSS 样式化节点。JavaFX 附带了一个默认样式表, Modena.css 。您可以扩充这些默认样式,或者用新样式替换它们。我们在文件 Styles.css 中找到的示例 CSS 文件是一个单独的样式类(mytext),它将其字体样式设置为斜体,如清单 2-6 所示。

.mytext {
    -fx-font-style: italic;
}

Listing 2-6Styles.css

要使用这个样式表,您必须首先在应用程序的start()方法或 FXML 文件中加载文件。清单 2-5 展示了如何在 MyShapesFXML.java 加载样式表。一旦文件被添加到可用的样式表中,您就可以将样式类应用到一个节点。例如,要将单独定义的样式类应用于特定节点,请使用

      text2.getStyleClass().add("mytext");

这里,“mytext”是样式类,text2是我们程序中的第二个文本对象。

或者,您可以在 FXML 文件中指定样式表。这种方法的优点是,现在在 Scene Builder 中可以使用样式。下面是修改后的 Scene.fxml 文件,它加载这个定制的 CSS 文件并将定制的 CSS 样式类应用到text2文本节点:

...
<VBox alignment="CENTER" prefHeight="350.0" prefWidth="350.0" spacing="50.0"
         stylesheets="@../styles/Styles.css"
         xmlns:="http://javafx.com/javafx/10.0.1"
         xmlns:fx="http://javafx.com/fxml/1"
         fx:controller="org.modernclient.FXMLController">
    <children>

<StackPane fx:id="stackPane" onMouseClicked="#handleMouseClick" prefHeight="150.0" prefWidth="200.0">
           ... code removed ...
        </StackPane>
        <Text fx:id="text2" styleClass="mytext" text="Animation Status: ">
            <font>
                <Font name="Arial Bold" size="18.0" />
            </font>
        </Text>
    </children>
</VBox>

有关如何在 JavaFX 应用程序中使用 CSS 的深入讨论,请参见第五章“掌握可视化和 CSS 设计”。

使用场景构建器

Scene Builder 最初由 Oracle 开发,现在是开源的。可以从胶子这里下载: https://gluonhq.com/products/scene-builder/ 。Scene Builder 是一个独立的拖放工具,用于创建 JavaFX UIs。图 2-11 显示了主场景构建器窗口,文件 Scene.fxml 来自 MyShapesFXML 程序。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2-11

带有场景构建器的 FXML 文件

左上角的窗口显示了 JavaFX 组件库。这个资源库包括容器、控制、形状、3D 等等。从该窗口中,选择组件并将其放到中间可视视图中的场景上,或者放到左下区域中显示的文档窗口上。

“文档”窗口显示场景图形层次。您可以选择组件并在树中移动它们。右侧窗口是一个检查器窗口,允许您配置每个组件,包括其属性、布局设置和代码。在图 2-11 中,StackPane 在文档层次结构窗口中被选中,并出现在中央可视视图中。在检查器窗口中,OnMouseClicked 属性被设置为#handleMouseClick,这是 JavaFX 控制器类中相应方法的名称。

在构建真实世界中基于表单的 ui 时,Scene Builder 尤其有用。您可以可视化场景层次,并轻松配置布局和对齐设置。

把这一切放在一起

现在是时候构建一个更有趣的 JavaFX 应用程序了,它实现了一个主从视图。当我们向您展示这个应用程序时,我们将解释几个 JavaFX 特性,它们有助于您控制 UI 并保持数据和应用程序的一致性。

首先,我们使用场景构建器来构建和配置 UI。我们的示例包括一个 Person 模型类和一个保存数据的底层 ObservableList。该程序允许用户进行更改,但我们不保存任何数据。JavaFX 有管理数据集合的 ObservableLists,您可以编写侦听器和绑定表达式来响应任何数据更改。该程序使用事件处理程序和绑定表达式的组合来保持应用程序状态的一致性。

主从用户界面

对于 UI,我们在左侧窗口(主视图)中使用一个 JavaFX ListView 控件,在右侧窗口(详细视图)中使用一个表单。在 Scene Builder 中,我们选择一个 AnchorPane 作为顶级组件,并选择场景图根。SplitPane 布局窗格将应用程序视图分为两个部分,每个部分都将 AnchorPane 作为其主容器。图 2-12 显示了正在运行的个人 UI 应用程序。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2-12

个人 UI 应用程序

ListView 控件允许您选择人员对象。在这里,第一个人被选中,这个人的详细信息出现在右边的表单控件中。

表单控件具有以下布局:

  • 该表单包含一个 GridPane(两列四行),其中包含 Person 的firstnamelastname字段的文本字段。

  • TextArea 保存 Person 的notes字段。第一列中的标签标记这些控件中的每一个。

  • GridPane 的底行由一个 ButtonBar 组成,它跨越两列,默认情况下在右侧对齐。按钮栏将其所有按钮的大小调整为最宽按钮标签的宽度,以便按钮具有统一的大小。

  • 这些按钮允许您执行新建(创建人员并将该人员添加到列表中)、更新(编辑所选人员)和删除(从列表中删除所选人员)。

  • 绑定表达式查询应用程序的状态,并启用或禁用按钮。图 2-12 显示了在删除按钮启用的情况下,新建和更新按钮被禁用。

图 2-13 显示了我们的人 UI 应用场景图的层次视图。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2-13

人员 UI 场景图层次结构

图 2-14 显示了应用程序的文件结构。Person.java 的包含人模型代码,SampleData.java 的提供初始化应用程序的数据。FXMLController.java是 JavaFX 控制器类,PersonUI.java持有主应用程序类。在资源下,FXML 文件 Scene.fxml 描述了 UI。****

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2-14

人员 UI 应用程序文件结构

模型

让我们从清单 2-7 中显示的 Person 类开始。这是我们在这个应用程序中使用的“模型”。

我们的 Person 类有三个字段:firstnamelastnamenotes。这些字段被实现为 JavaFX 属性,使它们可以被观察到。我们遵循前面描述的命名约定来实现 getter、setter 和属性 getter。幸运的是,JavaFX 提供了方便的类来帮助您创建属性。这里我们使用SimpleStringProperty()将每个字段构造为 JavaFX 字符串属性。

package org.modernclient.model;
import javafx.beans.Observable;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.util.Callback;
import java.util.Objects;
public class Person {
    private final StringProperty firstname = new SimpleStringProperty(
           this, "firstname", "");
    private final StringProperty lastname = new SimpleStringProperty(
           this, "lastname", "");
    private final StringProperty notes = new SimpleStringProperty(
           this, "notes", "sample notes");
    public Person() {
    }
    public Person(String firstname, String lastname, String notes) {
        this.firstname.set(firstname);
        this.lastname.set(lastname);
        this.notes.set(notes);
    }
    public String getFirstname() {
        return firstname.get();
    }
    public StringProperty firstnameProperty() {
        return firstname;
    }
    public void setFirstname(String firstname) {
        this.firstname.set(firstname);
    }
    public String getLastname() {
        return lastname.get();
    }
    public StringProperty lastnameProperty() {
        return lastname;
    }
    public void setLastname(String lastname) {
        this.lastname.set(lastname);
    }
    public String getNotes() {
        return notes.get();
    }
    public StringProperty notesProperty() {
        return notes;
    }
    public void setNotes(String notes) {
        this.notes.set(notes);
    }
    @Override
    public String toString() {
        return firstname.get() + " " + lastname.get();
    }
    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;
        Person person = (Person) obj;
        return Objects.equals(firstname, person.firstname) &&
                Objects.equals(lastname, person.lastname) &&
                Objects.equals(notes, person.notes);
    }
    @Override
    public int hashCode() {
        return Objects.hash(firstname, lastname, notes);
    }
}

Listing 2-7model.Person.java

可观察列表

使用 JavaFX 集合时,通常会使用 ObservableLists 来检测侦听器的列表变化。此外,显示数据列表的 JavaFX 控件需要可观察的列表。这些控件自动更新 UI 以响应列表修改。当我们带您浏览我们的示例程序时,我们将解释其中的一些复杂性。

实现 ListView 选择

ListView 控件在可观察的列表中显示项目,并允许您选择一个或多个项目。要在右视图的表单字段中显示一个选定的人,您需要为selectedItemProperty使用一个更改监听器。每当用户从 ListView 中选择不同的项目或取消选择选定的项目时,都会调用此更改侦听器。您可以使用鼠标以及箭头键、Home(第一个项目)和 End(最后一个项目)进行选择。在 Mac 上,使用 Fn+左箭头键表示 Home,使用 Fn+右箭头键表示 End。对于取消选择(在 Mac 上是 command+click,在 Linux 或 Windows 上是 Ctrl+click),新值为 null,我们清除所有的表单控件字段。清单 2-8 显示了 ListView 选择更改监听器。

listView.getSelectionModel().selectedItemProperty().addListener(
        personChangeListener = (observable, oldValue, newValue) -> {
            // newValue can be null if nothing is selected
            selectedPerson = newValue;
            modifiedProperty.set(false);
            if (newValue != null) {
                // Populate controls with selected Person
                firstnameTextField.setText(selectedPerson.getFirstname());
                lastnameTextField.setText(selectedPerson.getLastname());
                notesTextArea.setText(selectedPerson.getNotes());
            } else {
                firstnameTextField.setText("");
                lastnameTextField.setText("");
                notesTextArea.setText("");
            }
        });

Listing 2-8ListView selection change listener

布尔属性modifiedProperty跟踪用户是否改变了表单中三个文本控件中的任何一个。我们在每次 ListView 选择后重置该标志,并在绑定表达式中使用该属性来控制更新按钮的 disable 属性。

使用多重选择

默认情况下,ListView 控件实现单项选择,因此最多只能选择一项。ListView 还提供多重选择,您可以通过配置选择模式来启用它,如下所示:

    listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);

有了这个设置,每次用户用Ctrl+clickcommand+click向选择中添加另一个项目时,selectedItemProperty监听器就会被新的选择调用。getSelectedItems()方法返回当前选择的所有项目,newValue参数是最近选择的值。例如,以下更改监听器收集多个选定的项目并打印它们:

listView.getSelectionModel().selectedItemProperty().addListener(
        personChangeListener = (observable, oldValue, newValue) -> {
      ObservableList<Person> selectedItems =
                      listView.getSelectionModel().getSelectedItems();
      // Do something with selectedItems
      System.out.println(selectedItems);
 });

我们的 Person UI 应用程序对 ListView 使用单选模式。

列表视图和排序

假设您想先按姓氏再按名字对姓名列表进行排序。JavaFX 有几种方法对列表进行排序。由于我们需要对名称进行排序,我们将把底层的 ObservableArrayList 包装在一个 SortedList 中。为了在 ListView 中保持列表的排序,我们用排序后的列表调用 ListView 的setItems()方法。比较器指定排序。首先,我们比较每个人的姓氏进行排序,然后根据需要比较名字。为了设置排序,setComparator()方法使用一个匿名类,或者更简洁地说,一个 lambda 表达式:

// Use a sorted list; sort by lastname; then by firstname
SortedList<Person> sortedList = new SortedList(personList);
sortedList.setComparator((p1, p2) -> {
    int result = p1.getLastname().compareToIgnoreCase(p2.getLastname());
    if (result == 0) {
        result = p1.getFirstname().compareToIgnoreCase(p2.getFirstname());
    }
    return result;
});
listView.setItems(sortedList);

注意,比较器参数p1p2被推断为 Person 类型,因为 SortedList 是泛型的。

有关 ListView 控件的更深入的讨论,包括使用单元格和单元格工厂的高级编辑和显示功能,请参见第四章“JavaFX 控件深入探讨”

人员 UI 应用程序操作

我们的 Person UI 应用程序实现了三个操作:删除(从底层列表中删除选定的 Person 对象)、新建(创建一个 Person 对象并将其添加到底层列表中)和更新(对选定的 Person 对象进行更改并更新底层列表)。让我们详细检查一下每个操作,着眼于了解更多关于 JavaFX 特性的知识,这些特性可以帮助您构建这种类型的应用程序。

删除某人

控制器类包括一个删除按钮的动作事件处理程序。下面是定义删除按钮的 FXML 片段:

        <Button fx:id="removeButton" mnemonicParsing="false"
                     onAction="#removeButtonAction" text="Delete" />

fx:id属性命名按钮,以便 JavaFX 控制器类可以访问它。onAction属性对应于控制器代码中的 ActionEvent 处理程序。我们在这个应用程序中没有使用键盘快捷键,所以我们将属性mnemonicParsing设置为 false。

Note

当助记键分析为真时,可以指定一个键盘快捷键来激活带标签的控件,例如 Alt+F 来打开文件菜单。通过在标签中的目标字母前面加上下划线字符来定义键盘快捷键。

您不能直接更新 SortedList,但是可以将更改应用到它的底层列表(ObservableList personList)。每当您添加或删除项目时,SortedList 总是保持其元素排序。

下面是控制器类中的事件处理程序:

        @FXML
        private void removeButtonAction(ActionEvent actionEvent) {
            personList.remove(selectedPerson);
        }

这个处理程序从后台可观察数组列表中删除选中的 Person 对象。ListView 控件的选择改变监听器设置selectedPerson,如清单 2-8 所示。

注意,我们在这里不必检查selectedPerson是否为空。为什么不呢?您将看到,当selectedItemProperty为空时,我们禁用了删除按钮。这意味着当用户取消选择 ListView 控件中的元素时,永远不会调用 Delete 按钮的 action 事件处理程序。下面是控制删除按钮的禁用属性的绑定表达式:

        removeButton.disableProperty().bind(
           listView.getSelectionModel().selectedItemProperty().isNull());

这个优雅的语句使事件处理程序更加紧凑,因此不容易出错。按钮disableProperty和选择模型selectedItemProperty都是 JavaFX 可观察的。因此,您可以在绑定表达式中使用它们。当bind()参数的值改变时,调用bind()的属性自动更新。

添加一个人

New 按钮将一个人添加到列表中,并随后更新 ListView 控件。新项目总是被排序,因为当元素被添加到包装列表时,列表会重新排序。下面是定义新按钮的 FXML。类似于删除按钮,我们定义了fx:idonAction属性:

        <Button fx:id="createButton" mnemonicParsing="false"
                 onAction="#createButtonAction" text="New" />

什么情况下应该禁用新建按钮?

  • 单击“新建”时,不应选择列表视图中的任何项目。因此,如果selectedItemProperty不为空,我们禁用新按钮。请注意,您可以使用 command+单击或 Ctrl+单击来取消选择选定的项目。

  • 如果名字或姓氏字段为空,我们就不应该创建新的人员。因此,如果这两个字段中有一个为空,我们将禁用“新建”按钮。但是,我们允许注释字段为空。

以下是实现这些限制的绑定表达式:

        createButton.disableProperty().bind(
            listView.getSelectionModel().selectedItemProperty().isNotNull()
                .or(firstnameTextField.textProperty().isEmpty()
                .or(lastnameTextField.textProperty().isEmpty())));

现在让我们向您展示新的按钮事件处理程序:

        @FXML
        private void createButtonAction(ActionEvent actionEvent) {
            Person person = new Person(firstnameTextField.getText(),
                    lastnameTextField.getText(), notesTextArea.getText());
            personList.add(person);
            // and select it
            listView.getSelectionModel().select(person);
        }

首先,我们使用表单的文本控件创建一个新的 Person 对象,并将这个人添加到包装列表中(ObservableList personList)。为了让这个人的数据立即可见和可编辑,我们选择了新添加的人。

更新某人

人员的更新不像其他操作那样简单。在我们深入研究原因的细节之前,让我们先来看看 Update 按钮的 FXML 代码,它与其他按钮类似:

        <Button fx:id="updateButton" mnemonicParsing="false"
                 onAction="#updateButtonAction" text="Update" />

默认情况下,排序列表不响应变化的单个数组元素。例如,如果人“Ethan Nieto”更改为“Ethan Abraham”,列表将不会像添加或删除项目时那样重新排序。有两种方法可以解决这个问题。首先是删除该项,然后用新值重新添加。

第二种方法是为底层对象定义一个提取器。提取器定义了发生变化时应该观察的属性。通常,不会观察到对单个列表元素的更改。提取器标志返回的可观察对象更新列表 ChangeListener 中的更改。因此,要使 ListView 控件在更改单个元素后显示正确排序的列表,您需要定义一个带有提取器的 ObservableList。

提取器的好处是您只包括影响排序的属性。在我们的例子中,属性firstnamelastname影响列表的顺序。这些属性应该放在提取器中。

提取器是模型类中的静态回调方法。这是我们的 Person 类的提取器:

    public class Person {
     ...
         public static Callback<Person, Observable[]> extractor =
             p-> new Observable[] {
                p.lastnameProperty(), p.firstnameProperty()
             };
    }

现在控制器类可以使用这个提取器来声明一个名为personList的 ObservableList,如下所示:

    private final ObservableList<Person> personList =
              FXCollections.observableArrayList(Person.extractor);

设置提取器后,排序后的列表会检测到firstnamePropertylastnameProperty的变化,并根据需要重新排序。

接下来,我们定义何时启用更新按钮。在我们的应用程序中,如果没有选择任何项目,或者如果 firstname 或 lastname 文本字段为空,则应该禁用 Update 按钮。最后,如果用户尚未对表单的文本组件进行更改,我们将禁用 Update。我们用一个名为modifiedProperty的 JavaFX 布尔属性跟踪这些变化,这个属性是用 JavaFX 布尔属性助手类 SimpleBooleanProperty 创建的。我们在 JavaFX 控制器类中将该布尔值初始化为 false,如下所示:

    private final BooleanProperty modifiedProperty =
        new SimpleBooleanProperty(false);

我们在 ListView 选择更改监听器中将这个布尔属性重置为 false(清单 2-8 )。当在可以改变的三个字段中的任何一个发生击键时,modifiedProperty被设置为 true:名字、姓氏和注释控件。下面是击键事件处理程序,当在这三个控件的焦点内检测到击键时,将调用该处理程序:

    @FXML
    private void handleKeyAction(KeyEvent keyEvent) {
        modifiedProperty.set(true);
    }

当然,FXML 标记必须为所有三个文本控件配置属性onKeyReleased来调用击键事件处理程序。下面是firstname文本字段的 FXML,它将handleKeyAction事件处理程序链接到该控件的按键释放事件:

    <TextField fx:id="firstnameTextField" onKeyReleased="#handleKeyAction"
        prefWidth="248.0"
        GridPane.columnIndex="1"
        GridPane.hgrow="ALWAYS" />

下面是更新按钮的绑定表达式,如果selectedItemProperty为空、modifiedProperty为假或者文本控件为空,则该表达式被禁用:

    updateButton.disableProperty().bind(
        listView.getSelectionModel().selectedItemProperty().isNull()
            .or(modifiedProperty.not())
            .or(firstnameTextField.textProperty().isEmpty()
            .or(lastnameTextField.textProperty().isEmpty())));

现在让我们向您展示更新按钮的动作事件处理程序。当用户在 ListView 控件中选择一项并对任何文本字段进行至少一次更改后单击 Update 按钮时,将调用此处理程序。

但是还有一件家务要做。在开始用表单控件的值更新所选项之前,我们必须删除selectedItemProperty上的监听器。为什么呢?回想一下,对firstnamelastname属性的更改将动态地影响列表,并可能对其进行重新排序。此外,这可能会改变 ListView 对当前所选项的想法,并调用 ChangeListener。为了防止这种情况,我们在更新过程中删除了侦听器,并在更新完成后重新添加侦听器。在更新过程中,选定的项目保持不变(即使列表重新排序)。因此,我们清除了modifiedProperty标志以确保更新按钮被禁用:

@FXML
private void updateButtonAction(ActionEvent actionEvent) {
    Person p = listView.getSelectionModel().getSelectedItem();
    listView.getSelectionModel().selectedItemProperty()
                 .removeListener(personChangeListener);
    p.setFirstname(firstnameTextField.getText());
    p.setLastname(lastnameTextField.getText());
    p.setNotes(notesTextArea.getText());
    listView.getSelectionModel().selectedItemProperty()
                 .addListener(personChangeListener);
    modifiedProperty.set(false);
}

有记录的人员用户界面

Java 16 中令人兴奋的新特性之一是记录。记录允许您对保存不可变数据和描述状态的类进行建模,通常只需要一行代码。让我们重构 Person UI 示例,将 Java 记录用于 Person 模型类。我们这样做有几个原因。

  • 随着应用程序利用新的 Java 特性,使用 JavaFX 的现代 Java 客户机将继续发展。毕竟,JavaFX 是用 Java APIs 实现的,当然可以利用新特性。

  • 我们的 UI 示例是记录的一个很好的候选,因为使用 Person 记录而不是 class 是一种简单的方法。

  • 我们最初用 JavaFX 属性实现了 Person,这些属性是可观察和可变的。但是,在我们的应用环境中,这种可变性是必要的,甚至是可取的吗?

  • Java 记录有助于提高代码的可读性,因为通常只有一行代码定义了模型类的状态。

个人记录

我们用它的名字和它的不可变组件来声明一个记录;每个组件都有名称和类型。这些组件是生成的类中的最终实例字段。Java 为字段生成访问器方法,为方法equals()hashCode()toString()生成构造器和默认实现。

下面是新的 Person 类,它比清单 2-7 中显示的非记录版本要短得多!

public record Person (String firstname, String lastname, String notes) {
    @Override
    public String toString() {
        return firstname + " " + lastname;
    }
}

注意,我们提供了自己的 toString()实现来替换自动生成的toString(),因为 ListView 使用它来显示每个 Person 对象。生成的访问器方法是firstname()lastname()notes(),以匹配记录头中声明的元素。我们更新了我们的应用程序,使用这些名称来代替传统的 getter 形式。这影响了selectedItemProperty改变监听器和排序列表比较器。

不需要对 createButtonAction 或removeButtonAction事件处理程序进行任何更改。创建我们的人员对象示例列表(SampleData.java)的代码也没有变化。

但是,记录确实需要对 updateButtonAction 事件处理程序进行更改。因为 Person 对象现在是不可变的,所以我们不能更新它的字段。因此,要更新一个人,我们必须创建一个新的人对象,删除旧的人对象,并将新的人对象添加到支持列表中。排序后的列表会自动用新数据更新。下面是新的updateButtonAction事件处理程序:

   @FXML
    private void updateButtonAction(ActionEvent actionEvent) {
        Person person = new Person(firstnameTextField.getText(), lastnameTextField.getText(),
                    notesTextArea.getText());
        personList.remove(listView.getSelectionModel().getSelectedItem());
        personList.add(person);
        listView.getSelectionModel().select(person);
        modifiedProperty.set(false);
    }

通过删除和添加人员,更新过程变得更加简单。不再需要检测更改的提取器,也不需要在更新期间临时删除 selectedItemProperty 更改侦听器。

通过将 Person 限制为不可变的容器,我们极大地简化了 Person 和程序的可读性。然而,JavaFX 属性和绑定仍然是维护 UI 状态的理想特性。

要点总结

这一章涵盖了很多领域。让我们回顾一下要点:

  • JavaFX 是一个现代的 UI 工具包,可以在桌面、移动和嵌入式环境中高效运行。

  • JavaFX 使用了一个剧院的比喻。运行时系统创建主阶段并调用应用程序的start()方法。

  • 创建一个层次场景图,并在场景中安装根节点。

  • JavaFX 运行时系统在 JavaFX 应用程序线程上执行所有 UI 更新和场景图形修改。任何长时间运行的工作都应该被放到单独线程中的后台任务中,以保持 UI 的响应性。JavaFX 有一个开发良好的并发库,可以帮助您将 UI 代码与后台代码分开。

  • JavaFX 支持 2D 和 3D 图形。2D 图形中的原点是场景的左上角。

  • JavaFX 包括一组丰富的布局控件,允许您在场景中排列组件。您可以嵌套布局控件并指定调整大小的标准。

  • JavaFX 将场景图定义为节点的层次集合。节点由其属性来描述。

  • JavaFX 属性是可观察的。您可以附加侦听器,并使用富绑定 API 将属性相互链接,并检测更改。

  • JavaFX 允许您定义称为过渡的高级动画。

  • 场景图的分层性质意味着父节点可以将渲染工作委托给其子节点。

  • JavaFX 支持各种各样的事件,让您对用户输入和场景图的变化做出反应。

  • 虽然可以完全用 Java 编写 JavaFX 应用程序,但更好的方法是用 FXML 编写可视化描述,FXML 是一种用于指定 UI 内容的标记语言。FXML 有助于将可视代码与模型和控制器代码分开。

  • 每个 FXML 文件通常描述一个场景并配置一个控制器。

  • 场景构建器是一个方便的拖放工具,用于定义和配置场景中的组件。

  • 您可以使用在 FXML 或 Java 代码中加载的 CSS 文件自定义 JavaFX 控件和表单的样式。

  • 可观察列表允许您监听列表中的变化。

  • JavaFX 有几个显示数据列表的控件。您可以使用 ObservableLists 配置这些控件,以便控件可以自动检测到更改。

  • 通过在模型类中定义一个提取器,可以对单个元素的变化做出反应。提取器指定应该发出变更事件信号的属性。

  • 用 SortedList 包装 ObservableList 可以保持列表的排序。比较器允许您自定义排序标准。

  • 考虑用 Java 记录实现不可变的模型类。记录是从 Java 16 开始的一个新的 Java 特性。

三、属性和绑定

作者:韦琪高

天道之徒具有生命力和毅力。

与此相对应的

上位者不停地保持自己的生命力。

—易经

前两章向您介绍了客户端 Java 的概况和 JavaFX 的基础知识。在本章中,我们将深入探讨绑定和属性框架,这是 JavaFX 的一部分,它与声明性 UI 语言 FXML 和可视化 UI 设计器场景生成器一起,使 JavaFX 桌面和移动客户端应用程序编写起来优雅而愉快。

javafx.base模块是 JavaFX 属性和绑定框架的主页。它导出以下包:

  • javafx.beans

  • javafx.beans.binding

  • javafx.beans.property

  • javafx.beans.property.adapter

  • javafx.beans.value

  • javafx.collections

  • javafx.collections.transformation

  • javafx.event

  • javafx.util

  • javafx.util.converter

我们将关注javafx.beansjavafx.collections包及其子包。

关键概念

属性和绑定 API 的核心是一组接口,它们赋予我们讨论的两个核心概念以生命:属性绑定。图 3-1 显示了这些接口。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-1

JavaFX 属性和绑定框架的关键接口

可观察和无效监听器

通过Observable接口,您可以将InvalidationListener注册到PropertyBinding,这样当PropertyBinding变为无效时,您将会收到通知。如果调用其set()setValue()方法时使用的值不同于其当前保存的值,则Property会失效。当Bindinginvalidate()方法被调用或其依赖项失效时,该Binding将失效。InvalidationListener中的回调方法具有以下签名,使您可以访问对Observable对象的引用:

void invalidated(Observable observable);

Note

如果连续多次使用相同的值调用 setters,JavaFX 中的属性只会触发一次失效事件。

ObservableValue 和 ChangeListener

ObservableValue接口允许您用PropertyBinding注册ChangeListener s,这样当PropertyBinding的值从一个值变为另一个值时,您会收到通知。通知以回调方法的形式出现在ChangeListener中,带有以下签名,允许您访问其值已更改的属性或绑定的引用,以及旧值和新值:

void changed(ObservableValue<? extends T> observable,
             T oldValue, T newValue)

Note

InvalidationListenerChangeListener的弱版本,以及本章后面介绍的一些其他监听器,有助于避免内存泄漏。

可写值和只读属性

WritableValue接口向一个Property提供了setValue()方法。ReadOnlyProperty接口向一个Property注入两个方法:一个getBean()方法返回属性的持有者,一个getName()方法返回属性的描述性名称。如果属性不是更大对象的一部分,或者描述性名称不重要,这两种方法都可能返回 null。

JavaFX 属性

随着所有预备工作的结束,我们终于可以看看Property界面了。它提供了五种方法:

void bind(ObservableValue<? extends T> observable);
void unbind();
boolean isBound();
void bindBidirectional(Property<T> other);
void unbindBidirectional(Property<T> other);

bind()方法在PropertyObservableValue之间建立了一个单向绑定unbind()方法释放绑定。并且isBound()方法报告单向绑定是否有效。一旦生效,单向绑定将建立前者对后者的依赖关系Property上的set()setValue()方法会抛出一个RuntimeExceptionget()getValue()方法会返回ObservableValue的值。

bindBidirectional()方法在两个Property对象之间建立了一个双向绑定unbindBidirectional()方法释放它。一旦生效,在任一属性上调用set()setValue()将导致两个对象的值都被更新。

Caution

每个Property一次最多可以有一个活动的单向绑定。它可以有任意多的双向绑定。isBound()方法只适用于单向绑定。用不同的ObservableValue第二次调用bind()会解除之前的绑定并用新的替换它。

总结一下我们到目前为止所讨论的内容,我们看到一个Property可以保存一个值,当它的值改变时可以通知其他人,并且可以绑定到其他人以反映绑定对象的值。清单 3-1 展示了一个运行这些功能的愚蠢程序。

package org.modernclients.propertiesandbindings;
import javafx.beans.InvalidationListener;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.value.ChangeListener;
public class PropertiesExample {
    private static IntegerProperty i1;
    public static void main(String[] args) {
        createProperty();
        addAndRemoveInvalidationListener();
        addAndRemoveChangeListener();
        bindAndUnbindOnePropertyToAnother();
    }
    private static void createProperty() {
        System.out.println();
        i1 = new SimpleIntegerProperty(1024);
        System.out.println("i1 = " + i1);
        System.out.println("i1.get() = " + i1.get());
        System.out.println("i1.getValue() = "
                + i1.getValue());
    }
    private static void addAndRemoveInvalidationListener() {
        System.out.println();
        final InvalidationListener invalidationListener =
                observable -> {
                    System.out.println(
                            "The observable has been " +
                                    "invalidated: " +
                                    observable + ".");
                };
        i1.addListener(invalidationListener);
        System.out.println("Added invalidation listener.");
        System.out.println("Calling i1.set(2048).");
        i1.set(2048);
        System.out.println("Calling i1.setValue(3072).");
        i1.setValue(3072);
        i1.removeListener(invalidationListener);
        System.out.println("Removed invalidation listener.");
        System.out.println("Calling i1.set(4096).");
        i1.set(4096);
    }
    private static void addAndRemoveChangeListener() {
        System.out.println();
        final ChangeListener<Number> changeListener =
                (observableValue,
                 oldValue,
                 newValue) -> {
                    System.out.println(
                            "The observableValue has " +
                                    "changed: oldValue = " +
                                    oldValue +
                                    ", newValue = " +
                                    newValue);
                };
        i1.addListener(changeListener);
        System.out.println("Added change listener.");
        System.out.println("Calling i1.set(5120).");
        i1.set(5120);
        i1.removeListener(changeListener);
        System.out.println("Removed change listener.");
        System.out.println("Calling i1.set(6144).");
        i1.set(6144);
    }

    private static void bindAndUnbindOnePropertyToAnother() {
        System.out.println();
        IntegerProperty i2 = new SimpleIntegerProperty(0);
        System.out.println("i2.get() = " + i2.get());
        System.out.println("Binding i2 to i1.");
        i2.bind(i1);
        System.out.println("i2.get() = " + i2.get());
        System.out.println("Calling i1.set(7168).");
        i1.set(7168);
        System.out.println("i2.get() = " + i2.get());
        System.out.println("Unbinding i2 from i1.");
        i2.unbind();
        System.out.println("i2.get() = " + i2.get());
        System.out.println("Calling i1.set(8192).");
        i1.set(8192);
        System.out.println("i2.get() = " + i2.get());
    }
}

Listing 3-1PropertiesExample.java

Note

本节的源代码可以在本书随附的源代码包的第三章中找到。它被组织成一个带有子项目的 Gradle 项目,每个子项目对应一个示例。

这个程序是不言自明的,你几乎可以在脑海中想象它是如何执行的。我们在程序中使用了一个抽象类IntegerProperty及其具体实现SimpleIntegerProperty。它保存一个原始的int值。

清单 3-2 展示了工作中的双向绑定。

package org.modernclients.propertiesandbindings;

import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
public class BidirectionalBindingExample {
    public static void main(String[] args) {
        System.out.println("Constructing two StringProperty" +
                " objects.");
        StringProperty prop1 = new SimpleStringProperty("");
        StringProperty prop2 = new SimpleStringProperty("");
        System.out.println("Calling bindBidirectional.");
        prop2.bindBidirectional(prop1);
        System.out.println("prop1.isBound() = " +
                prop1.isBound());
        System.out.println("prop2.isBound() = " +
                prop2.isBound());
        System.out.println("Calling prop1.set(\"prop1" +
                " says: Hi!\")");
        prop1.set("prop1 says: Hi!");
        System.out.println("prop2.get() returned:");
        System.out.println(prop2.get());
        System.out.println("Calling prop2.set(prop2.get()" +
                " + \"\\nprop2 says: Bye!\")");
        prop2.set(prop2.get() + "\nprop2 says: Bye!");
        System.out.println("prop1.get() returned:");
        System.out.println(prop1.get());
    }
}

Listing 3-2BidirectionalBindingExample.java

创建绑定

在上一节中,我们探讨了 JavaFX 属性和绑定框架的关键接口。我们还学习了关于Property物体的基础知识。在这一节中,我们拿起框架的另一半并检查Binding s。

JavaFX 绑定

Binding接口提供了四种方法:

boolean isValid();
void invalidate();
ObservableList<?> getDependencies();
void dispose();

一个Binding有效性可以用isValid()方法查询,用invalidate()方法设置。它有一个可以通过getDependencies()方法获得的依赖项的列表。最后,dispose()方法发出信号表示Binding将不再被使用,它所使用的资源可以被清理。

因此,Binding表示具有多个依赖关系的单向绑定。每个依赖都可以向Binding发送失效事件,使其失效。当通过get()getValue()调用查询Binding的值时,如果它被无效,它的值将根据依赖关系的值重新计算。该值将被缓存并用于后续的值查询,直到Binding再次失效。这种懒惰的值评估是 JavaFX 属性和绑定框架高效的原因。附加一个ChangeListener强制急切评估。

由于一个绑定可以用作另一个绑定的依赖项,因此可以构建复杂的绑定树。这是 JavaFX 属性和绑定框架强大功能的另一个来源。

Caution

与任何复杂的结构一样,必须小心避免性能下降和行为错误,尤其是在高负载的情况下。

与属性不同,框架不提供具体的绑定类。因此,所有绑定都是自定义绑定,有几种方法可以创建它们:

  • 扩展一个抽象基类,比如DoubleBinding

  • 在实用程序类Bindings中使用工厂方法

  • 在属性和绑定类中使用 fluent API 方法

通过直接扩展创建绑定

清单 3-3 展示了一个程序,它通过直接扩展DoubleBinding来创建一个绑定,并使用它来计算矩形的面积。

package org.modernclients.propertiesandbindings;
import javafx.beans.binding.DoubleBinding;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
public class DirectExtensionExample {
    public static void main(String[] args) {
        System.out.println("Constructing x with value 2.0.");
        final DoubleProperty x =
                new SimpleDoubleProperty(null, "x", 2.0);
        System.out.println("Constructing y with value 3.0.");
        final DoubleProperty y =
                new SimpleDoubleProperty(null, "y", 3.0);
        System.out.println("Creating binding area" +
                " with dependencies x and y.");
        DoubleBinding area = new DoubleBinding() {
            {
                super.bind(x, y);
            }
            @Override
            protected double computeValue() {
                System.out.println("computeValue()" +
                        " is called.");
                return x.get() * y.get();
            }
        };
        System.out.println("area.get() = " + area.get());
        System.out.println("area.get() = " + area.get());
        System.out.println("Setting x to 5");
        x.set(5);
        System.out.println("Setting y to 7");
        y.set(7);
        System.out.println("area.get() = " + area.get());
    }
}

Listing 3-3DirectExtensionExample.java

这里,我们通过覆盖其唯一的抽象方法computeValue()来扩展DoubleBinding类,以计算边长为xy的矩形的面积。我们还调用超类的bind()方法来使属性xy成为我们的依赖。

运行该程序会将以下内容打印到控制台:

Constructing x with value 2.0.
Constructing y with value 3.0.
Creating binding area with dependencies x and y.
computeValue() is called.
area.get() = 6.0
area.get() = 6.0
Setting x to 5
Setting y to 7
computeValue() is called.
area.get() = 35.0

注意,当我们连续两次调用area.get()时,computeValue()只被调用一次。

特定于类型的专门化

在我们进入下一个创建绑定的方法之前,我们需要向您提供一些关于键接口的一般性质及其特定于类型的专门化的细节。

本章前面的例子包括像IntegerPropertyStringPropertyDoubleBinding这样的类。这些是通用类型Property<T>Bindings<T>的专门类。由于 Java 的原始类型和引用类型二分法,直接使用泛型类型,比如Property<Integer>,同时处理原始值会导致装箱和拆箱效率低下。为了减少这种成本,泛型类型的特定于类型的专门化被构造为基元booleanintlongfloatdouble值,以这样的方式,当它们的get()set()方法被调用时,以及当进行内部计算时,基元类型从不被装箱和取消装箱。出于一致性原因,也为StringObject引用类型构建了类似的专门化。这说明了BooleanPropertyIntegerPropertyLongPropertyFloatPropertyDoublePropertyStringPropertyObjectProperty类的存在。

Caution

不要被名称IntegerProperty所迷惑,以为它是一个Integer对象的容器。真的不是。它是原始int价值观的容器。其他基于原语的类也是如此。

这些特定于类型的专门化的另一个方面是使用Number作为类型参数来派生数字原语类型的专门化。一个实际的结果是,任何数字属性都可以在任何其他数字属性或绑定上调用bind()。清单 3-4 显示了一个说明这一点的程序。

package org.modernclients.propertiesandbindings;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.FloatProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.LongProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleFloatProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleLongProperty;
public class NumericPropertiesExample {
    public static void main(String[] args) {
        IntegerProperty i =
                new SimpleIntegerProperty(null, "i", 1024);
        LongProperty l =
                new SimpleLongProperty(null, "l", 0L);
        FloatProperty f =
                new SimpleFloatProperty(null, "f", 0.0F);
        DoubleProperty d =
                new SimpleDoubleProperty(null, "d", 0.0);
        System.out.println("Constructed numerical" +
                " properties i, l, f, d.");
        System.out.println("i.get() = " + i.get());
        System.out.println("l.get() = " + l.get());
        System.out.println("f.get() = " + f.get());
        System.out.println("d.get() = " + d.get());
        l.bind(i);
        f.bind(l);
        d.bind(f);
        System.out.println("Bound l to i, f to l, d to f.");
        System.out.println("i.get() = " + i.get());
        System.out.println("l.get() = " + l.get());
        System.out.println("f.get() = " + f.get());
        System.out.println("d.get() = " + d.get());
        System.out.println("Calling i.set(2048).");
        i.set(2048);
        System.out.println("i.get() = " + i.get());
        System.out.println("l.get() = " + l.get());
        System.out.println("f.get() = " + f.get());
        System.out.println("d.get() = " + d.get());
        d.unbind();
        f.unbind();
        l.unbind();
        System.out.println("Unbound l to i, f to l, d to f.");
        f.bind(d);
        l.bind(f);
        i.bind(l);
        System.out.println("Bound f to d, l to f, i to l.");
        System.out.println("Calling d.set(10000000000L).");
        d.set(10000000000L);
        System.out.println("d.get() = " + d.get());
        System.out.println("f.get() = " + f.get());
        System.out.println("l.get() = " + l.get());
        System.out.println("i.get() = " + i.get());
    }
}

Listing 3-4NumericPropertiesExample.java

运行此应用程序会产生以下输出:

Constructed numerical properties i, l, f, d.
i.get() = 1024
l.get() = 0
f.get() = 0.0
d.get() = 0.0
Bound l to i, f to l, d to f.
i.get() = 1024
l.get() = 1024
f.get() = 1024.0
d.get() = 1024.0
Calling i.set(2048).
i.get() = 2048
l.get() = 2048
f.get() = 2048.0
d.get() = 2048.0
Unbound l to i, f to l, d to f.
Bound f to d, l to f, i to l.
Calling d.set(10000000000L).
d.get() = 1.0E10
f.get() = 1.0E10
l.get() = 10000000000
i.get() = 1410065408

绑定中的工厂方法

Bindings类包含 200 多个工厂方法,这些方法用现有的可观察值和常规值进行新的绑定。这些方法被重载以考虑参数类型的无数组合。

add()subtract()multiply()divide()方法显而易见,用两个数值创建一个新的数值绑定,其中至少有一个是可观察的值。清单 3-5 中的程序演示了它们的用法。它使用以下公式计算带有顶点(x1y1x2y2x3y3)的三角形的面积

area = (x1*y2 + x2*y3 + x3*y1 – x1*y3 – x2*y1 – x3*y2)/2

package org.modernclients.propertiesandbindings;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.NumberBinding;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
public class TriangleAreaExample {
    public static void main(String[] args) {
        IntegerProperty x1 = new SimpleIntegerProperty(0);
        IntegerProperty y1 = new SimpleIntegerProperty(0);
        IntegerProperty x2 = new SimpleIntegerProperty(0);
        IntegerProperty y2 = new SimpleIntegerProperty(0);
        IntegerProperty x3 = new SimpleIntegerProperty(0);
        IntegerProperty y3 = new SimpleIntegerProperty(0);
        final NumberBinding x1y2 = Bindings.multiply(x1, y2);
        final NumberBinding x2y3 = Bindings.multiply(x2, y3);
        final NumberBinding x3y1 = Bindings.multiply(x3, y1);
        final NumberBinding x1y3 = Bindings.multiply(x1, y3);
        final NumberBinding x2y1 = Bindings.multiply(x2, y1);
        final NumberBinding x3y2 = Bindings.multiply(x3, y2);
        final NumberBinding sum1 = Bindings.add(x1y2, x2y3);
        final NumberBinding sum2 = Bindings.add(sum1, x3y1);
        final NumberBinding diff1 =
                Bindings.subtract(sum2, x1y3);
        final NumberBinding diff2 =
                Bindings.subtract(diff1, x2y1);
        final NumberBinding determinant =
                Bindings.subtract(diff2, x3y2);
        final NumberBinding area =
                Bindings.divide(determinant, 2.0D);
        x1.set(0); y1.set(0);
        x2.set(6); y2.set(0);
        x3.set(4); y3.set(3);
        printResult(x1, y1, x2, y2, x3, y3, area);
        x1.set(1); y1.set(0);
        x2.set(2); y2.set(2);
        x3.set(0); y3.set(1);
        printResult(x1, y1, x2, y2, x3, y3, area);
    }
    private static void printResult(IntegerProperty x1,
                                    IntegerProperty y1,
                                    IntegerProperty x2,
                                    IntegerProperty y2,
                                    IntegerProperty x3,
                                    IntegerProperty y3,
                                    NumberBinding area) {
        System.out.println("For A(" +
                x1.get() + "," + y1.get() + "), B(" +
                x2.get() + "," + y2.get() + "), C(" +
                x3.get() + "," + y3.get() +
                "), the area of triangle ABC is " +
                area.getValue());
    }
}

Listing 3-5TriangleAreaExample.java

运行该程序会将以下内容打印到控制台:

For A(0,0), B(6,0), C(4,3), the area of triangle ABC is 9.0
For A(1,0), B(2,2), C(0,1), the area of triangle ABC is 1.5

Bindings中的其他工厂方法包括逻辑运算符and()or()not();数字运算符min()max()negate();空测试操作符isNull()isNotNull();字符串运算符length()isEmpty()isNotEmpty();以及关系运算符equal()equalIgnoreCase()greaterThan()graterThanOrEqual()lessThan()lessThanOrEqual()notEqual()notEqualIgnoreCase()。那些方法的名字是自我描述的,它们都做你认为它们做的事情。例如,为了确保只有在选择了收件人并且输入的金额大于零时,才启用“汇款”按钮,我们可以编写

sendBtn.disableProperty().bind(Bindings.not(
    Bindings.and(recipientSelected,
        Bindings.greaterThan(amount, 0.0))));

有一组名为createDoubleBinding()等的工厂方法,允许您从一个Callable和一组依赖项创建一个绑定。我们在清单 3-3 中创建的DoubleBinding可以简化为

DoubleBinding area = Bindings.createDoubleBinding(() -> {
      return x.get() * y.get();
}, x, y);

可以使用convert()concat()和几个重载的format()方法将非字符串可观察值转换为可观察字符串值,将几个可观察字符串值连接在一起,并将可观察数值或日期值格式化为可观察字符串值。要在Label中显示温度值,我们可以使用以下绑定:

tempLbl.textProperty().bind(Bindings.format("%2.1f \u00b0C", temperature));

随着 temperature 属性值的变化,温度的格式化字符串表示形式也随之变化。比如temperature为 37.5 时,标签会显示 37.5 C。

有一组名为select()selectInteger()等的工厂方法作用于JavaFX Bean,这些 Java 类符合 Java FX Bean 约定。还有一些方法对可观察集合起作用,这些可观察集合不包含单个值,而是包含一个ListMapSet或一个元素数组。我们将在本章后面的章节中介绍它们。

使用 Fluent API 创建绑定

流畅的 API 由一组协调的类组成,这些类的方法被设计成以这样一种方式链接在一起,即当大声读出方法链时,这些方法链以类似散文的句子描述它们做了什么。用于创建绑定的 fluent API 包含在IntegerExpression系列的类中。这些表达式类是属性类和绑定类的超类。因此,流畅的 API 方法很容易从熟悉的属性和绑定类中获得。通过浏览表达式类的 Javadocs,您可以对这些方法有所了解。总的来说,它们反映了Bindings类所能提供的。以下是使用 fluent API 构建的几个绑定示例:

recipientSelected.and(amount.greaterThan(0.0)).not()
temperature.asString("%2.1f \u00b0C")

它们相当于我们在上一节中使用来自Bindings类的工厂方法构建的绑定。

这里值得指出的一个事实是,特定于类型的数值表达式的所有方法都是在返回类型为NumberBindingNumberExpression基本接口中定义的,并且在具有相同参数签名但返回类型更特定的特定于类型的表达式类中被覆盖。这被称为协变返回类型覆盖,并且从 Java 5 开始就是 Java 语言的一个特性。这一事实的结果之一是,用 fluent API 构建的数字绑定比用Bindings类中的工厂方法构建的绑定有更多的特定类型。

清单 3-6 显示了清单 3-5 中三角形区域示例的流畅 API 版本。

package org.modernclients.propertiesandbindings;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.NumberBinding;
import javafx.beans.binding.StringExpression;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
public class TriangleAreaFluentExample {
    public static void main(String[] args) {
        IntegerProperty x1 = new SimpleIntegerProperty(0);
        IntegerProperty y1 = new SimpleIntegerProperty(0);
        IntegerProperty x2 = new SimpleIntegerProperty(0);
        IntegerProperty y2 = new SimpleIntegerProperty(0);
        IntegerProperty x3 = new SimpleIntegerProperty(0);
        IntegerProperty y3 = new SimpleIntegerProperty(0);
        final NumberBinding area = x1.multiply(y2)
                .add(x2.multiply(y3))
                .add(x3.multiply(y1))
                .subtract(x1.multiply(y3))
                .subtract(x2.multiply(y1))
                .subtract(x3.multiply(y2))
                .divide(2.0D);
        StringExpression output = Bindings.format(
                "For A(%d,%d), B(%d,%d), C(%d,%d)," +
                        " the area of triangle ABC is %3.1f",
                x1, y1, x2, y2, x3, y3, area);
        x1.set(0); y1.set(0);
        x2.set(6); y2.set(0);
        x3.set(4); y3.set(3);
        System.out.println(output.get());
        x1.set(1); y1.set(0);
        x2.set(2); y2.set(2);
        x3.set(0); y3.set(1);
        System.out.println(output.get());
    }
}

Listing 3-6TriangleAreaFluentExample.java

运行该程序会将以下内容打印到控制台:

For A(0,0), B(6,0), C(4,3), the area of triangle ABC is 9.0
For A(1,0), B(2,2), C(0,1), the area of triangle ABC is 1.5

When允许你在一个流畅的 API 中表达 if/then/else 逻辑。您可以使用构造器或Bindings类中的when()工厂方法构造这个类的对象,并传入一个ObservableBooleanValue。在When对象上重载的then()方法返回一个嵌套的条件构建器类的对象,该类又重载了返回一个绑定对象的otherwise()方法。这允许您通过以下方式建立绑定:

new When(condition).then(result).otherwise(alternative)

这里,condition是一个ObservableBooleanValueresultalternative是类似的类型,可以是可观测的,也可以是不可观测的。最终绑定的类型类似于resultalternative的类型。

清单 3-7 展示了这个 API 的使用示例。这里,我们使用 Heron 公式计算边长分别为abc的三角形的面积

area = sqrt(s * (s – a) * (s – b) * (s – c))

其中s = (a + b + c) / 2是半参数。回想一下,在三角形中,任何两条边的和都大于第三条边。

package org.modernclients.propertiesandbindings;
import javafx.beans.binding.DoubleBinding;
import javafx.beans.binding.When;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
public class HeronsFormulaExample {
    public static void main(String[] args) {
        DoubleProperty a = new SimpleDoubleProperty(0);
        DoubleProperty b = new SimpleDoubleProperty(0);
        DoubleProperty c = new SimpleDoubleProperty(0);
        DoubleBinding s = a.add(b).add(c).divide(2.0d);
        final DoubleBinding areaSquared = new When(
                a.add(b).greaterThan(c)
                        .and(b.add(c).greaterThan(a))
                        .and(c.add(a).greaterThan(b)))
                .then(s.multiply(s.subtract(a))
                        .multiply(s.subtract(b))
                        .multiply(s.subtract(c)))
                .otherwise(0.0D);
        a.set(3);
        b.set(4);
        c.set(5);
        System.out.printf("Given sides a = %1.0f," +
                        " b = %1.0f, and c = %1.0f," +
                        " the area of the triangle is" +
                        " %3.2f\n", a.get(), b.get(), c.get(),
                Math.sqrt(areaSquared.get()));
        a.set(2);
        b.set(2);
        c.set(2);
        System.out.printf("Given sides a = %1.0f," +
                        " b = %1.0f, and c = %1.0f," +
                        " the area of the triangle is" +
                        " %3.2f\n", a.get(), b.get(), c.get(),
                Math.sqrt(areaSquared.get()));
    }
}

Listing 3-7HeronsFormulaExample.java

运行该程序会将以下内容打印到控制台:

Given sides a = 3, b = 4, and c = 5, the area of the triangle is 6.00
Given sides a = 2, b = 2, and c = 2, the area of the triangle is 1.73

应该注意的是,fluent API 有其局限性。随着关系变得更加复杂或者超出了可用的运算符,直接扩展方法是首选。清单 3-8 展示了这样一个程序,它解决了与清单 3-7 相同的问题。

package org.modernclients.propertiesandbindings;
import javafx.beans.binding.DoubleBinding;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
public class HeronsFormulaDirectExtensionExample {
    public static void main(String[] args) {
        final DoubleProperty a = new SimpleDoubleProperty(0);
        final DoubleProperty b = new SimpleDoubleProperty(0);
        final DoubleProperty c = new SimpleDoubleProperty(0);
        DoubleBinding area = new DoubleBinding() {
            {
                super.bind(a, b, c);
            }
            @Override
            protected double computeValue() {
                double a0 = a.get();
                double b0 = b.get();
                double c0 = c.get();
                if ((a0 + b0 > c0) && (b0 + c0 > a0) &&
                        (c0 + a0 > b0)) {
                    double s = (a0 + b0 + c0) / 2.0D;
                    return Math.sqrt(s * (s - a0) *
                            (s - b0) * (s - c0));
                } else {
                    return 0.0D;
                }
            }
        };
        a.set(3);
        b.set(4);
        c.set(5);
        System.out.printf("Given sides a = %1.0f," +
                        " b = %1.0f, and c = %1.0f," +
                        " the area of the triangle" +
                        " is %3.2f\n", a.get(), b.get(),
                c.get(), area.get());
        a.set(2);
        b.set(2);
        c.set(2);
        System.out.printf("Given sides a = %1.0f," +
                        " b = %1.0f, and c = %1.0f," +
                        " the area of the triangle" +
                        " is %3.2f\n", a.get(), b.get(),
                c.get(), area.get());
    }
}

Listing 3-8HeronsFormulaDirectExtensionExample.java

可观察的集合

JavaFX 在包javafx.collectionsjavafx.collections.transformation中提供了对可观察集合的支持。

他们引入了另外四个Observable的子接口,与我们在本章前面章节学习的ObservableValue接口一起。分别是ObservableListObservableMapObservableSetObservableArray。observable list、map 和 set 还分别扩展了ListMapSet Java 集合的框架接口,因此可以像普通集合一样使用。因为它们只保存装箱的原始值,所以不需要特定于类型的专门化。另一方面,可观察数组在内部保存一个数组,并具有针对intfloat类型的特定于类型的专门化。它们在 JavaFX 3D API 中使用。

这些接口的主要目的是允许您注册和取消注册变更监听器。除此之外,ObservableList接口还有额外的方法,以更有效的方式操作可观察列表。ObservableMapObservableSet接口没有附加的方法。带有ObservableIntegerArrayObservableFloatArray子接口的ObservableArray接口拥有操纵可观察数组的方法。

FXCollections 中的工厂和实用程序方法

FXCollections实用程序类包含创建可观察集合和数组的工厂方法。它们类似于java.util.Collections中的工厂方法,除了它们返回可观察的集合和数组。它们是创建系统提供的可观察集合和数组的唯一方法。

FXCollections实用程序类还提供了一些方法来操作它创建的ObservableList对象。这些方法包括copy()fill()replaceAll()reverse()rotate()shuffle()sort()方法。它们执行与它们的java.util.Collections对等物相同的功能,除了它们注意最小化生成的列表改变通知的数量。

清单 3-9 显示了FXCollections方法的用法。

package org.modernclients.propertiesandbindings;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.MapChangeListener;
import javafx.collections.ObservableFloatArray;
import javafx.collections.ObservableList;
import javafx.collections.ObservableMap;
import javafx.collections.ObservableSet;
import javafx.collections.SetChangeListener;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Random;
public class FXCollectionsExample {
    public static void main(String[] args) {
        ObservableList<String> list =
                FXCollections.observableArrayList();
        ObservableMap<String, String> map =
                FXCollections.observableHashMap();
        ObservableSet<Integer> set =
                FXCollections.observableSet();
        ObservableFloatArray array =
                FXCollections.observableFloatArray();
        list.addListener((ListChangeListener<String>) c -> {
            System.out.println("\tlist = " +
                    c.getList());
        });
        map.addListener((MapChangeListener<String, String>) c -> {
            System.out.println("\tmap = " +
                    c.getMap());
        });
        set.addListener((SetChangeListener<Integer>) c -> {
            System.out.println("\tset = " +
                    c.getSet());
        });
        array.addListener((observableArray,
                           sizeChanged, from, to) -> {
            System.out.println("\tarray = " +
                    observableArray);
        });
        manipulateList(list);
        manipulateMap(map);
        manipulateSet(set);
        manipulateArray(array);
    }

    private static void manipulateList(
            ObservableList<String> list) {
        System.out.println("Calling list.addAll(\"Zero\"," +
                " \"One\", \"Two\", \"Three\"):");
        list.addAll("Zero", "One", "Two", "Three");
        System.out.println("Calling copy(list," +
                " Arrays.asList(\"Four\", \"Five\")):");
        FXCollections.copy(list,
                Arrays.asList("Four", "Five"));
        System.out.println("Calling replaceAll(list," +
                " \"Two\", \"Two_1\"):");
        FXCollections.replaceAll(list, "Two", "Two_1");
        System.out.println("Calling reverse(list):");
        FXCollections.reverse(list);
        System.out.println("Calling rotate(list, 2):");
        FXCollections.rotate(list, 2);
        System.out.println("Calling shuffle(list):");
        FXCollections.shuffle(list);
        System.out.println("Calling shuffle(list," +
                " new Random(0L)):");
        FXCollections.shuffle(list, new Random(0L));
        System.out.println("Calling sort(list):");
        FXCollections.sort(list);
        System.out.println("Calling sort(list, c)" +
                " with custom comparator: ");
        FXCollections.sort(list, new Comparator<String>() {
            @Override
            public int compare(String lhs, String rhs) {
                // Reverse the order
                return rhs.compareTo(lhs);
            }
        });
        System.out.println("Calling fill(list," +
                " \"Ten\"): ");
        FXCollections.fill(list, "Ten");
    }
    private static void manipulateMap(
            ObservableMap<String, String> map) {
        System.out.println("Calling map.put(\"Key\"," +
                " \"Value\"):");
        map.put("Key", "Value");
    }
    private static void manipulateSet(
            ObservableSet<Integer> set) {
        System.out.println("Calling set.add(1024):");
        set.add(1024);
    }

    private static void manipulateArray(
            ObservableFloatArray array) {
        System.out.println("Calling  array.addAll(3.14159f," +
                " 2.71828f):");
        array.addAll(3.14159f, 2.71828f);
    }
}

Listing 3-9FXCollectionsExample.java

在这里,我们使用FXCollections工厂方法创建了一个可观察列表、一个可观察映射、一个可观察集合和一个可观察数组,给它们附加了监听器,并以某种方式操纵它们,包括对列表使用FXCollections实用方法,对数组使用ObservableFloatArray方法。

运行该程序会将以下内容打印到控制台:

Calling list.addAll("Zero", "One", "Two", "Three"):
        list = [Zero, One, Two, Three]
Calling copy(list, Arrays.asList("Four", "Five")):
        list = [Four, Five, Two, Three]
Calling replaceAll(list, "Two", "Two_1"):
        list = [Four, Five, Two_1, Three]
Calling reverse(list):
        list = [Three, Two_1, Five, Four]
Calling rotate(list, 2):
        list = [Five, Four, Three, Two_1]
Calling shuffle(list):
        list = [Five, Four, Two_1, Three]
Calling shuffle(list, new Random(0L)):
        list = [Three, Five, Four, Two_1]
Calling sort(list):
        list = [Five, Four, Three, Two_1]
Calling sort(list, c) with custom comparator:
        list = [Two_1, Three, Four, Five]
Calling fill(list, "Ten"):
        list = [Ten, Ten, Ten, Ten]
Calling map.put("Key", "Value"):
        map = {Key=Value}
Calling set.add(1024):
        set = [1024]
Calling  array.addAll(3.14159f, 2.71828f):
        array = [3.14159, 2.71828]

更改可观察集合的侦听器

ObservableListObservableMapObservableSetObservableArray接口提供了addListener()removeListener()方法来注册和取消注册侦听器,以便在底层集合或数组发生变化时得到通知。对应的ListChangeListenerMapChangeListenerSetChangeListener接口都有一个onChanged()回调方法,其参数是一个嵌套的Change类。并且ArrayChangeListener接口有一个带显式参数的onChanged()回调方法。

清单 3-10 显示了一个程序,其中一个ObservableList<String>被操纵,相应的Change对象在一个作为 lambda 实现的附加ListChangeListener中被查询。

package org.modernclient.propertiesandbindings;

import javafx.beans.Observable;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;

import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import static javafx.collections.ListChangeListener.Change;
public class ObservableListExample {
    public static void main(String[] args) {
        ObservableList<String> strings =
                FXCollections.observableArrayList();
        strings.addListener((Observable observable) -> {
            System.out.println("\tlist invalidated");
        });
        strings.addListener((Change<? extends String> change) -> {
            System.out.println("\tstrings = " +
                    change.getList());
        });
        System.out.println("Calling add(\"First\"): ");
        strings.add("First");
        System.out.println("Calling add(0, \"Zeroth\"): ");
        strings.add(0, "Zeroth");
        System.out.println("Calling addAll(\"Second\"," +
                " \"Third\"): ");
        strings.addAll("Second", "Third");
        System.out.println("Calling set(1," +
                " \"New First\"): ");
        strings.set(1, "New First");
        final List<String> list =
                Arrays.asList("Second_1", "Second_2");
        System.out.println("Calling addAll(3, list): ");
        strings.addAll(3, list);
        System.out.println("Calling remove(2, 4): ");
        strings.remove(2, 4);
        final Iterator<String> iterator =
                strings.iterator();
        while (iterator.hasNext()) {
            final String next = iterator.next();
            if (next.contains("t")) {
                System.out.println("Calling remove()" +
                        " on iterator: ");
                iterator.remove();
            }
        }
        System.out.println("Calling removeAll(" +
                "\"Third\", \"Fourth\"): ");
        strings.removeAll("Third", "Fourth");
    }
}

Listing 3-10ObservableListExample.java

运行该程序会将以下内容打印到控制台:

Calling add("First"):
        list invalidated
        strings = [First]
Calling add(0, "Zeroth"):
        list invalidated
        strings = [Zeroth, First]
Calling addAll("Second", "Third"):
        list invalidated
        strings = [Zeroth, First, Second, Third]
Calling set(1, "New First"):
        list invalidated
        strings = [Zeroth, New First, Second, Third]
Calling addAll(3, list):
        list invalidated
        strings = [Zeroth, New First, Second, Second_1, Second_2, Third]
Calling remove(2, 4):
        list invalidated
        strings = [Zeroth, New First, Second_2, Third]
Calling remove() on iterator:
        list invalidated
        strings = [New First, Second_2, Third]
Calling remove() on iterator:
        list invalidated

        strings = [Second_2, Third]
Calling removeAll("Third", "Fourth"):
        list invalidated
        strings = [Second_2]

ListChangeListener 中的更改事件

在上一节中,我们只查询了ListChangeListener.Change对象的list属性,该属性引用了被观察的列表。这个对象保存了更多关于底层列表变化的信息。它代表一个或多个离散的变更,每个变更都可以是添加元素、删除元素、替换元素或置换元素。变更界面为您提供了查询变更各个方面的方法。

next()reset()方法控制遍历离散变化的游标。当调用onChanged()时,光标位于第一个离散变化之前。一旦光标位于一个有效的离散变更上,wasAdded()wasRemoved()wasReplaced()wasPermuted()方法会告诉您这是哪种离散变更。

一旦知道了光标所在的离散变化,就可以调用其他方法来获得有关离散变化的更多细节。对于添加的元素,您可以获得from(含)和to(不含)索引、addedSizeaddedSubList。对于删除的元素,您可以获得删除元素的fromto(同from)索引、removedSizeremoved列表。对于被替换的元素,可以认为是先删除后添加,应检查与添加和删除相关的信息。对于元素置换,getPermutation(int i)方法将 before 索引映射到 after 索引。

清单 3-11 显示了一个带有漂亮的打印实现ListChangeListener的程序,当一个变更事件被触发时,它打印出Change对象的细节。

package org.modernclients.propertiesandbindings;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
public class ListChangeEventExample {
    public static void main(String[] args) {
        ObservableList<String> strings =
                FXCollections.observableArrayList();
        strings.addListener(new MyListener());
        System.out.println("Calling addAll(\"Zero\"," +
                " \"One\", \"Two\", \"Three\"): ");
        strings.addAll("Zero", "One", "Two", "Three");
        System.out.println("Calling" +
                " FXCollections.sort(strings): ");
        FXCollections.sort(strings);
        System.out.println("Calling set(1, \"Three_1\"): ");
        strings.set(1, "Three_1");
        System.out.println("Calling setAll(\"One_1\"," +
                " \"Three_1\", \"Two_1\", \"Zero_1\"): ");
        strings.setAll("One_1", "Three_1", "Two_1", "Zero_1");
        System.out.println("Calling removeAll(\"One_1\"," +
                " \"Two_1\", \"Zero_1\"): ");
        strings.removeAll("One_1", "Two_1", "Zero_1");
    }
    private static class MyListener implements
            ListChangeListener<String> {
        @Override
        public void onChanged(
                Change<? extends String> change) {
            System.out.println("\tlist = " +
                    change.getList());
            System.out.println(prettyPrint(change));
        }
        private String prettyPrint(
                Change<? extends String> change) {
            StringBuilder sb =
                    new StringBuilder("\tChange event data:\n");
            int i = 0;
            while (change.next()) {
                sb.append("\t\tcursor = ")
                        .append(i++)
                        .append("\n");
                final String kind =
                        change.wasPermutated() ? "permutated" :
                        change.wasReplaced() ? "replaced" :
                        change.wasRemoved() ? "removed" :
                        change.wasAdded() ? "added" :
                        "none";
                sb.append("\t\tKind of change: ")
                        .append(kind)
                        .append("\n");
                sb.append("\t\tAffected range: [")
                        .append(change.getFrom())
                        .append(", ")
                        .append(change.getTo())
                        .append("]\n");
                if (kind.equals("added") ||
                        kind.equals("replaced")) {
                    sb.append("\t\tAdded size: ")
                            .append(change.getAddedSize())
                            .append("\n");
                    sb.append("\t\tAdded sublist: ")
                            .append(change.getAddedSubList())
                            .append("\n");
                }

                if (kind.equals("removed") ||
                        kind.equals("replaced")) {
                    sb.append("\t\tRemoved size: ")
                            .append(change.getRemovedSize())
                            .append("\n");
                    sb.append("\t\tRemoved: ")
                            .append(change.getRemoved())
                            .append("\n");
                }
                if (kind.equals("permutated")) {
                    StringBuilder permutationSB =
                            new StringBuilder("[");
                    int from = change.getFrom();
                    int to = change.getTo();
                    for (int k = from; k < to; k++) {
                        int permutation =
                                change.getPermutation(k);
                        permutationSB.append(k)
                                .append("->")
                                .append(permutation);
                        if (k < change.getTo() - 1) {
                            permutationSB.append(", ");
                        }
                    }
                    permutationSB.append("]");
                    String permutation =
                            permutationSB.toString();
                    sb.append("\t\tPermutation: ")
                            .append(permutation).append("\n");
                }
            }
            return sb.toString();
        }
    }
}

Listing 3-11ListChangeEventExample.java

运行该程序会将以下内容打印到控制台:

Calling addAll("Zero", "One", "Two", "Three"):
        list = [Zero, One, Two, Three]
        Change event data:
                cursor = 0
                Kind of change: added
                Affected range: [0, 4]
                Added size: 4
                Added sublist: [Zero, One, Two, Three]
Calling FXCollections.sort(strings):
        list = [One, Three, Two, Zero]
        Change event data:
                cursor = 0
                Kind of change: permutated
                Affected range: [0, 4]
                Permutation: [0->3, 1->0, 2->2, 3->1]
Calling set(1, "Three_1"):
        list = [One, Three_1, Two, Zero]
        Change event data:
                cursor = 0
                Kind of change: replaced
                Affected range: [1, 2]
                Added size: 1
                Added sublist: [Three_1]
                Removed size: 1
                Removed: [Three]
Calling setAll("One_1", "Three_1", "Two_1", "Zero_1"):
        list = [One_1, Three_1, Two_1, Zero_1]
        Change event data:
                cursor = 0

                Kind of change: replaced
                Affected range: [0, 4]
                Added size: 4
                Added sublist: [One_1, Three_1, Two_1, Zero_1]
                Removed size: 4
                Removed: [One, Three_1, Two, Zero]
Calling removeAll("One_1", "Two_1", "Zero_1"):
        list = [Three_1]
        Change event data:
                cursor = 0
                Kind of change: removed
                Affected range: [0, 0]
                Removed size: 1
                Removed: [One_1]
                cursor = 1
                Kind of change: removed
                Affected range: [1, 1]
                Removed size: 2
                Removed: [Two_1, Zero_1]

MapChangeListener 中的更改事件

MapChangeListener.Change事件比其对应的可观察列表要简单得多,因为它只反映一个键的变化。因此,没有next()也没有reset()方法是必要的。如果多个键受到影响,将触发多个更改事件。

wasAdded()wasRemoved()方法指示是否添加或移除一个键。你总能找到受变化影响的key。而如果加了一个键,就可以在valueAdded得到;如果一把钥匙被拿掉,你就可以拿到valueRemoved

清单 3-12 显示了一个操纵可观察地图并记录生成的变更事件的程序。

package org.modernclients.propertiesandbindings;
import javafx.collections.FXCollections;
import javafx.collections.MapChangeListener;
import javafx.collections.ObservableMap;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
public class MapChangeEventExample {
    public static void main(String[] args) {
        ObservableMap<String, Integer> map =
                FXCollections.observableHashMap();
        map.addListener(new MyListener());
        System.out.println("Calling put(\"First\", 1): ");
        map.put("First", 1);
        System.out.println("Calling put(\"First\", 100): ");
        map.put("First", 100);
        Map<String, Integer> anotherMap = new HashMap<>();
        anotherMap.put("Second", 2);
        anotherMap.put("Third", 3);
        System.out.println("Calling putAll(anotherMap): ");
        map.putAll(anotherMap);
        Iterator<Map.Entry<String, Integer>> entryIterator =
                map.entrySet().iterator();
        while (entryIterator.hasNext()) {
            final Map.Entry<String, Integer> next =
                    entryIterator.next();
            if (next.getKey().equals("Second")) {
                System.out.println("Calling remove on" +
                        " entryIterator: ");
                entryIterator.remove();
            }
        }
        final Iterator<Integer> valueIterator =
                map.values().iterator();
        while (valueIterator.hasNext()) {
            final Integer next = valueIterator.next();
            if (next == 3) {
                System.out.println("Calling remove on" +
                        " valueIterator: ");
                valueIterator.remove();
            }
        }
    }

    private static class MyListener implements
            MapChangeListener<String, Integer> {
        @Override
        public void onChanged(
                Change<? extends String, ? extends Integer>
                        change) {
            System.out.println("\tmap = " + change.getMap());
            System.out.println(prettyPrint(change));
        }

        private String prettyPrint(
                Change<? extends String, ? extends Integer>
                        change) {
            StringBuilder sb =
                    new StringBuilder("\tChange event" +
                            " data:\n");
            sb.append("\t\tWas added: ")
                    .append(change.wasAdded())
                    .append("\n");
            sb.append("\t\tWas removed: ")
                    .append(change.wasRemoved())
                    .append("\n");
            sb.append("\t\tKey: ")
                    .append(change.getKey())
                    .append("\n");
            sb.append("\t\tValue added: ")
                    .append(change.getValueAdded())
                    .append("\n");
            sb.append("\t\tValue removed: ")
                    .append(change.getValueRemoved())
                    .append("\n");
            return sb.toString();
        }
    }
}

Listing 3-12MapChangeEventExample.java

SetChangeListener 中的更改事件

SetChangeListener.Change事件甚至比可观察地图的事件更简单,因为当可观察集合被修改时不涉及任何值。

清单 3-13 显示了一个操纵一个可观察集合并记录生成的变更事件的程序。

package org.modernclients.propertiesandbindings;
import javafx.collections.FXCollections;
import javafx.collections.ObservableSet;
import javafx.collections.SetChangeListener;
import java.util.Arrays;
public class SetChangeEventExample {
    public static void main(String[] args) {
        ObservableSet<String> set =
                FXCollections.observableSet();
        set.addListener(new MyListener());
        System.out.println("Calling add(\"First\"): ");
        set.add("First");
        System.out.println("Calling addAll(" +
                "Arrays.asList(\"Second\", \"Third\")): ");
        set.addAll(Arrays.asList("Second", "Third"));
        System.out.println("Calling remove(" +
                "\"Third\"): ");
        set.remove("Third");
    }
    private static class MyListener
            implements SetChangeListener<String> {
        @Override
        public void onChanged(Change<? extends String>
                                      change) {
            System.out.println("\tset = " +
                    change.getSet());
            System.out.println(prettyPrint(change));
        }
        private String prettyPrint(
                Change<? extends String> change) {
            StringBuilder sb =
                    new StringBuilder("\tChange" +
                            " event data:\n");
            sb.append("\t\tWas added: ")
                    .append(change.wasAdded())
                    .append("\n");
            sb.append("\t\tWas removed: ")
                    .append(change.wasRemoved())
                    .append("\n");
            sb.append("\t\tElement added: ")
                    .append(change.getElementAdded())
                    .append("\n");
            sb.append("\t\tElement removed: ")
                    .append(change.getElementRemoved())
                    .append("\n");
            return sb.toString();
        }
    }
}

Listing 3-13SetChangeEventExample.java

更改 ArrayChangeListener 中的事件

ArrayChangeListener中的onChanged()方法具有以下签名:

public void onChanged(T observableArray,
    boolean sizeChanged, int from, int to);

正如许多管理器类的数组一样,ObservableArray有一个容量和一个大小。容量是底层支持数组的长度,大小是包含应用程序数据的元素的数量。大小总是小于或等于容量。ensureCapacity()方法将容量设置为指定的值,并在必要时重新分配底层数组。resize()方法改变大小。如果新容量大于旧容量,则容量会增加。如果新的大小大于旧的大小,多余的元素用零填充。如果新的大小小于旧的大小,后备数组不会收缩,但丢失的元素会用零填充。trimToSize()方法将容量缩小到大小。clear()方法将可观察数组的大小调整为零。size()方法返回可观察数组的当前大小。

ObservableArrayObservableIntegerArray,ObservableFloatArray的特定类型专门化重载了以特定类型方式操作底层数组的方法。get()方法获取指定索引处的值。set()方法在指定的索引处设置一个值或一组值。addAll()方法将附加元素添加到可观察数组中。setAll()方法替换可观察数组中的元素。toArray()方法返回一个填充了可观察数组内容的原始数组。get()set()方法可能会抛出ArrayIndexOutOfBoundsException

清单 3-14 显示了一个操纵ObservableIntegerArray并显示变更通知的程序。

package org.modernclients.propertiesandbindings;
import javafx.collections.FXCollections;
import javafx.collections.ObservableIntegerArray;
public class ArrayChangeEventExample {
    public static void main(String[] args) {
        final ObservableIntegerArray ints =
                FXCollections.observableIntegerArray(10, 20);
        ints.addListener((array,
                          sizeChanged, from, to) -> {
            StringBuilder sb =
                    new StringBuilder("\tObservable Array = ")
                            .append(array)
                            .append("\n")
                            .append("\t\tsizeChanged = ")
                            .append(sizeChanged).append("\n")
                            .append("\t\tfrom = ")
                            .append(from).append("\n")
                            .append("\t\tto = ")
                            .append(to)
                            .append("\n");
            System.out.println(sb.toString());
        });
        ints.ensureCapacity(20);
        System.out.println("Calling addAll(30, 40):");
        ints.addAll(30, 40);
        final int[] src = {50, 60, 70};
        System.out.println("Calling addAll(src, 1, 2):");
        ints.addAll(src, 1, 2);
        System.out.println("Calling set(0, src, 0, 1):");
        ints.set(0, src, 0, 1);
        System.out.println("Calling setAll(src):");
        ints.setAll(src);
        ints.trimToSize();
        final ObservableIntegerArray ints2 =
                FXCollections.observableIntegerArray();
        ints2.resize(ints.size());
        System.out.println("Calling copyTo(0, ints2," +
                " 0, ints.size()):");
        ints.copyTo(0, ints2, 0, ints.size());
        System.out.println("\tDestination = " + ints2);
    }
}

Listing 3-14ArrayChangeEventExample.java

此应用程序的输出如下所示:

Calling addAll(30, 40):
        Observable Array = [10, 20, 30, 40]
                sizeChanged = true
                from = 2
                to = 4
Calling addAll(src, 1, 2):
        Observable Array = [10, 20, 30, 40, 60, 70]
                sizeChanged = true
                from = 4
                to = 6
Calling set(0, src, 0, 1):
        Observable Array = [50, 20, 30, 40, 60, 70]
                sizeChanged = false
                from = 0
                to = 1
Calling setAll(src):
        Observable Array = [50, 60, 70]
                sizeChanged = true
                from = 0
                to = 3
Calling copyTo(0, ints2, 0, ints.size()):
        Destination = [50, 60, 70]

为可观察集合创建绑定

Bindings实用程序类包括从可观察集合中创建绑定的工厂方法。

重载方法valueAt()booleanValueAt()integerValueAt()longValueAt()floatValueAt()doubleValueAt()stringValueAt()从相同类型的可观察集合和适当类型的索引或键(可观察的或不可观察的)中创建适当类型的绑定。

例如,如果authorizations是一个代表Person对象授权状态的ObservableMap<Person, Boolean>,而user是一个ObjectProperty<Person>对象,那么booleanValueAt(authorizations, user)就是一个代表用户授权状态的BooleanBinding

重载的bindContent()方法将不可观察集合绑定到同类的可观察集合,确保不可观察集合与可观察集合具有相同的内容。unbindContent()方法删除了这样的内容绑定。重载的bindContentBidirectional()方法绑定两个同类的可观察集合,确保它们有相同的内容。unbindContentBidirectional()方法删除了这样的双向内容绑定。

JavaFX Beans

在前面的小节中,我们研究了单独的 JavaFX 属性和可观察集合。现在我们研究如何将它们组合成更大的单元,以形成更有意义的软件组件。

Java Beans 的概念几乎从一开始就存在。它引入了三个架构概念:属性事件方法。Java 中的方法很简单。事件是通过侦听器接口和事件对象提供的,JavaFX 控件仍在使用这些接口和对象。属性是使用现在非常熟悉的公共 getter 和 setter 方法提供的。

JavaFX 引入了 JavaFX Bean 概念,其中除了 getter 和 setter,JavaFX Bean 属性还有一个属性 getter 。对于类型为double的名为height的属性,有以下三种方法:

public final double getHeight();
public final void setHeight(double height);
public DoubleProperty heightProperty();

正如传统的属性通常使用相同类型的支持字段来实现一样,JavaFX Bean 属性通常使用适当的Property类型的支持字段来实现。由于这些属性是引用类型,对于具有许多属性的 JavaFX Bean,可能会创建许多额外的对象。根据使用模式,可以使用不同的策略来实现这些属性。

Note

只读 JavaFX Bean 属性可以用一个 getter 和一个返回 JavaFX 属性的只读版本的属性 getter 来定义。

急切实例化的属性

实现 JavaFX Bean 属性的最简单策略是急切实例化属性策略。每个属性都由在构造时实例化的适当属性类型支持。getter 和 setter 简单地调用后台属性的get()set()方法。属性 getter 返回支持属性本身。清单 3-15 显示了一个具有intStringColor属性的 JavaFX Bean。

package org.modernclients.propertiesandbindings;
import javafx.beans.property.*;
import javafx.scene.paint.Color;
public class JavaFXBeanModelExample {
    private IntegerProperty i =
            new SimpleIntegerProperty(this, "i", 0);
    private StringProperty str =
            new SimpleStringProperty(this, "str", "Hello");
    private ObjectProperty<Color> color =
            new SimpleObjectProperty<Color>(this, "color",
                    Color.BLACK);
    public final int getI() {
        return i.get();
    }
    public final void setI(int i) {
        this.i.set(i);
    }
    public IntegerProperty iProperty() {
        return i;
    }
    public final String getStr() {
        return str.get();
    }
    public final void setStr(String str) {
        this.str.set(str);
    }
    public StringProperty strProperty() {
        return str;
    }
    public final Color getColor() {
        return color.get();
    }
    public final void setColor(Color color) {
        this.color.set(color);
    }
    public ObjectProperty<Color> colorProperty() {
        return color;
    }
}

Listing 3-15JavaFXBeanModelExample.java

注意,我们使用了具有完整上下文的属性构造器,包括 bean、属性名和初始化属性的初始值。

半延迟实例化属性

如果 setter 和属性 getter 从未被调用,getter 将总是返回一个属性的默认值;你不需要一个属性实例来知道这一点。这是半懒惰实例化策略的基础。在这种策略中,只有在使用不同于默认值的值调用 setter 或调用属性 getter 时,属性才会被实例化。这种策略最适合具有许多属性的 JavaFX Beans,其中只有少数属性被设置。

清单 3-16 展示了这种策略的一个例子。

package org.modernclients.propertiesandbindings;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
public class JavaFXBeanModelHalfLazyExample {
    private static final String DEFAULT_STR = "Hello";
    private StringProperty str;
    public final String getStr() {
        if (str != null) {
            return str.get();
        } else {
            return DEFAULT_STR;
        }
    }
    public final void setStr(String str) {
        if ((this.str != null) ||
                !(str.equals(DEFAULT_STR))) {
            strProperty().set(str);
        }
    }
    public StringProperty strProperty() {
        if (str == null) {
            str = new SimpleStringProperty(this,
                    "str", DEFAULT_STR);
        }
        return str;
    }
}

Listing 3-16JavaFXBeanModelHalfLazyExample.java

完全延迟实例化的属性

深入思考半懒惰实例化策略,我们会问自己,“当调用 setter 时,我们真的需要实例化属性吗?”答案当然是否定的,如果我们有地方放它,就像过去一样。这就产生了全懒惰实例化策略。在这种策略中,只有在调用属性 getter 时,属性才会被实例化。只有当属性对象已经被实例化时,getter 和 setter 才会检查它;否则,它们会通过一个单独的支持字段。

清单 3-17 展示了这种策略的一个例子。

package org.modernclients.propertiesandbindings;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
public class JavaFXBeanModelFullLazyExample {
    private static final String DEFAULT_STR = "Hello";
    private StringProperty str;
    private String _str = DEFAULT_STR;
    public final String getStr() {
        if (str != null) {
            return str.get();
        } else {
            return _str;
        }
    }
    public final void setStr(String str) {
        if (this.str != null) {
            this.str.set(str);
        } else {
            _str = str;
        }
    }
    public StringProperty strProperty() {
        if (str == null) {
            str = new SimpleStringProperty(this,
                    "str", DEFAULT_STR);
        }
        return str;
    }
}

Listing 3-17JavaFXBeanModelFullLazyExample.java

选择绑定

现在我们已经理解了 JavaFX Bean 的概念,我们可以回到Bindings实用程序类,学习select()selectInteger()方法等等。他们有如下签名:

selectInteger(Object root, String... steps);

这些选择操作符允许您创建观察深度嵌套的 JavaFX Bean 属性的绑定。这里,root是作用域中的对象引用,每个step是手边对象的属性,指向下一个对象,依此类推。

最好用一个例子来说明这个概念。考虑类Lighting(在javafx.scene.effect中)。它有一个名为light的属性,类型为Light。并且Light有一个名为colorColor类型的属性(在javafx.scene.paint中)。清单 3-18 显示了一个构建选择绑定的程序,该绑定到达root对象的lightcolor

package org.modernclients.propertiesandbindings;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.effect.Light;
import javafx.scene.effect.Lighting;
import javafx.scene.paint.Color;
public class SelectBindingExample {
    public static void main(String[] args) {
        ObjectProperty<Lighting> root =
                new SimpleObjectProperty<>();
        final ObjectBinding<Color> colorBinding =
                Bindings.select(root, "light", "color");
        colorBinding.addListener((o, oldValue, newValue) ->
                System.out.println("\tThe color changed:\n" +
                        "\t\told color = " + oldValue +
                        ",\n\t\tnew color = " + newValue));
        System.out.println("firstLight is black.");
        Light firstLight = new Light.Point();
        firstLight.setColor(Color.BLACK);
        System.out.println("secondLight is white.");
        Light secondLight = new Light.Point();
        secondLight.setColor(Color.WHITE);
        System.out.println("firstLighting has firstLight.");
        Lighting firstLighting = new Lighting();
        firstLighting.setLight(firstLight);
        System.out.println("secondLighting has secondLight.");
        Lighting secondLighting = new Lighting();
        secondLighting.setLight(secondLight);
        System.out.println("Making root observe" +
                " firstLighting.");
        root.set(firstLighting);
        System.out.println("Making root observe" +
                " secondLighting.");
        root.set(secondLighting);
        System.out.println("Changing secondLighting's" +
                " light to firstLight");
        secondLighting.setLight(firstLight);
        System.out.println("Changing firstLight's" +
                " color to red");
        firstLight.setColor(Color.RED);
    }
}

Listing 3-18SelectBindingExample.java

运行该程序会将以下内容打印到控制台:

firstLight is black.
secondLight is white.
firstLighting has firstLight.
secondLighting has secondLight.
Making root observe firstLighting.
        The color changed:
                old color = null,
                new color = 0x000000ff
Making root observe secondLighting.
        The color changed:
                old color = 0x000000ff,
                new color = 0xffffffff
Changing secondLighting's light to firstLight
        The color changed:
                old color = 0xffffffff,
                new color = 0x000000ff
Changing firstLight's color to red
        The color changed:
                old color = 0x000000ff,
                new color = 0xff0000ff

改编 Java Beans

对于多年来编写的许多老式 Java Bean,JavaFX 在javafx.beans.property.adapter包中提供了一组适配器类,将 Java Bean 属性转换为 JavaFX 属性。

回想一下,如果在属性改变时触发了一个PropertyChange事件,那么 Java Bean 属性就是一个绑定属性。如果一个VetoableChange事件在被改变时被触发,那么它就是一个约束属性。如果一个注册的监听器抛出一个PropertyVetoException,这个改变不会生效。

清单 3-19 显示了一个具有普通属性name、绑定属性address和约束属性phoneNumberPerson bean。

package org.modernclients.propertiesandbindings;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.beans.PropertyVetoException;
import java.beans.VetoableChangeListener;
import java.beans.VetoableChangeSupport;
public class Person {
    private PropertyChangeSupport propertyChangeSupport;
    private VetoableChangeSupport vetoableChangeSupport;
    private String name;
    private String address;
    private String phoneNumber;
    public Person() {
        propertyChangeSupport =
                new PropertyChangeSupport(this);
        vetoableChangeSupport =
                new VetoableChangeSupport(this);
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getAddress() {
        return address;
    }
    public void setAddress(String address) {
        String oldAddress = this.address;
        this.address = address;
        propertyChangeSupport.firePropertyChange("address",
                oldAddress, this.address);
    }
    public String getPhoneNumber() {
        return phoneNumber;
    }
    public void setPhoneNumber(String phoneNumber)
            throws PropertyVetoException {
        String oldPhoneNumber = this.phoneNumber;
        vetoableChangeSupport.fireVetoableChange("phoneNumber",
                oldPhoneNumber, phoneNumber);
        this.phoneNumber = phoneNumber;
        propertyChangeSupport.firePropertyChange("phoneNumber",
                oldPhoneNumber, this.phoneNumber);
    }
    public void addPropertyChangeListener(PropertyChangeListener l) {
        propertyChangeSupport.addPropertyChangeListener(l);
    }
    public void removePropertyChangeListener(PropertyChangeListener l) {
        propertyChangeSupport.removePropertyChangeListener(l);
    }
    public PropertyChangeListener[] getPropertyChangeListeners() {
        return propertyChangeSupport.getPropertyChangeListeners();
    }
    public void addVetoableChangeListener(VetoableChangeListener l) {
        vetoableChangeSupport.addVetoableChangeListener(l);
    }
    public void removeVetoableChangeListener(VetoableChangeListener l) {
        vetoableChangeSupport.removeVetoableChangeListener(l);
    }
    public VetoableChangeListener[] getVetoableChangeListeners() {
        return vetoableChangeSupport.getVetoableChangeListeners();
    }
}

Listing 3-19Person.java

类型为String的 Java Bean 属性可以通过使用JavaBeanStringPropertyBuilder被改编成JavaBeanStringProperty:

JavaBeanStringPropertyBuilder.create()
        .bean(person)
        .name("name")
        .build();

这遵循熟悉的构建器模式:您调用静态的create()方法来获得构建器的实例,然后调用构建器实例上的bean()name()方法来配置构建器,告诉它要适应哪个 bean 和哪个属性。最后,您调用构建器上的build()方法来获得修改后的 JavaFX 属性。

builder 类有更多的方法可以用来处理更深奥的情况,例如,当 getter 或 setter 不遵循熟悉的命名约定,而是使用元数据指定时。

清单 3-20 显示了一个将Person类的三个 Java Bean 属性改编成JavaBeanStringProperty对象的程序。

package org.modernclients.propertiesandbindings;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.adapter.JavaBeanStringProperty;
import javafx.beans.property.adapter.JavaBeanStringPropertyBuilder;
import java.beans.PropertyVetoException;
public class JavaBeanPropertiesExample {
    public static void main(String[] args)
            throws NoSuchMethodException {
        adaptJavaBeansProperty();
        adaptBoundProperty();
        adaptConstrainedProperty();
    }
    private static void adaptJavaBeansProperty()
            throws NoSuchMethodException {
        Person person = new Person();
        JavaBeanStringProperty nameProperty =
                JavaBeanStringPropertyBuilder.create()
                        .bean(person)
                        .name("name")
                        .build();
        nameProperty.addListener((observable, oldValue, newValue) -> {
            System.out.println("JavaFX property " +
                    observable + " changed:");
            System.out.println("\toldValue = " +
                    oldValue + ", newValue = " + newValue);
        });
        System.out.println("Setting name on the" +
                " JavaBeans property");
        person.setName("Weiqi Gao");
        System.out.println("Calling fireValueChange");
        nameProperty.fireValueChangedEvent();
        System.out.println("nameProperty.get() = " +
                nameProperty.get());
        System.out.println("Setting value on the" +
                " JavaFX property");
        nameProperty.set("Johan Vos");
        System.out.println("person.getName() = " +
                person.getName());
    }
    private static void adaptBoundProperty()
            throws NoSuchMethodException {
        System.out.println();
        Person person = new Person();
        JavaBeanStringProperty addressProperty =
                JavaBeanStringPropertyBuilder.create()
                        .bean(person)
                        .name("address")
                        .build();
        addressProperty.addListener((observable, oldValue, newValue) -> {
            System.out.println("JavaFX property " +
                    observable + " changed:");
            System.out.println("\toldValue = " +
                    oldValue + ", newValue = " + newValue);
        });
        System.out.println("Setting address on the" +
                " JavaBeans property");
        person.setAddress("12345 main Street");
    }

    private static void adaptConstrainedProperty()
            throws NoSuchMethodException {
        System.out.println();
        Person person = new Person();
        JavaBeanStringProperty phoneNumberProperty =
                JavaBeanStringPropertyBuilder.create()
                        .bean(person)
                        .name("phoneNumber")
                        .build();
        phoneNumberProperty.addListener((observable,
                                         oldValue, newValue) -> {
            System.out.println("JavaFX property " +
                    observable + " changed:");
            System.out.println("\toldValue = " +
                    oldValue + ", newValue = " + newValue);
        });
        System.out.println("Setting phoneNumber on the" +
                " JavaBeans property");
        try {
            person.setPhoneNumber("800-555-1212");
        } catch (PropertyVetoException e) {
            System.out.println("A JavaBeans property" +
                    " change is vetoed.");
        }
        System.out.println("Bind phoneNumberProperty" +
                " to another property");
        SimpleStringProperty stringProperty =
                new SimpleStringProperty("866-555-1212");
        phoneNumberProperty.bind(stringProperty);
        System.out.println("Setting phoneNumber on the" +
                " JavaBeans property");
        try {
            person.setPhoneNumber("888-555-1212");
        } catch (PropertyVetoException e) {
            System.out.println("A JavaBeans property" +
                    " change is vetoed.");
        }
        System.out.println("person.getPhoneNumber() = " +
                person.getPhoneNumber());
    }
}

Listing 3-20JavaBeanPropertiesExample.java

注意,因为name不是一个绑定属性,所以调用person.setName()不会自动将新值传播给改编后的nameProperty。我们必须呼吁nameProperty上的fireValueChangedEvent()来实现这一点。对于绑定属性address,调用person.setAddress()会自动将新值传播到addressProperty。对于受约束的属性phoneNumber,在我们将适配的phoneNumberProperty绑定到另一个stringProperty之后,调用person.setPhoneNumber()会抛出一个PropertyVetoException,新值被拒绝。

运行该程序会将以下内容打印到控制台:

Setting name on the JavaBeans property
Calling fireValueChange
JavaFX property StringProperty [bean: org.modernclients.propertiesand
bindings.Person@5a8e6209, name: name, value: Weiqi Gao] changed:
        oldValue = null, newValue = Weiqi Gao
nameProperty.get() = Weiqi Gao
Setting value on the JavaFX property
JavaFX property StringProperty [bean: org.modernclients.propertiesand
bindings.Person@5a8e6209, name: name, value: Johan Vos] changed:
        oldValue = Weiqi Gao, newValue = Johan Vos
person.getName() = Johan Vos
Setting address on the JavaBeans property
JavaFX property StringProperty [bean: org.modernclients.propertiesand
bindings.Person@1f36e637, name: address, value: 12345 main Street] changed:
        oldValue = null, newValue = 12345 main Street
Setting phoneNumber on the JavaBeans property
JavaFX property StringProperty [bean: org.modernclients.propertiesand
bindings.Person@35d176f7, name: phoneNumber, value: 800-555-1212] changed:
        oldValue = null, newValue = 800-555-1212
Bind phoneNumberProperty to another property
JavaFX property StringProperty [bean: org.modernclients.propertiesand
bindings.Person@35d176f7, name: phoneNumber, value: 866-555-1212] changed:
        oldValue = 800-555-1212, newValue = 866-555-1212
Setting phoneNumber on the JavaBeans property
A JavaBeans property change is vetoed.
person.getPhoneNumber() = 866-555-1212

摘要

在本章中,您学习了 JavaFX 属性和绑定框架的基础知识。现在,您应该了解以下重要原则:

  • JavaFX 属性和绑定保存值并向附加的侦听器触发事件。

  • 当值无效时,将触发一个无效事件。并且在重新计算该值时触发一个 change 事件,这可能是延迟的。

  • 对于booleanintlongfloatdoubleStringObject类型,存在通用键接口的类型特定的专门化。对于原始类型,它们避免装箱和取消装箱。

  • ChangeListener附加到一个属性会强制进行急切评估。

  • 可以通过直接扩展、使用Bindings中的工厂方法或者使用 fluent API 来创建新的绑定。

  • 对于ListMapSetintfloat数组,存在可观察集合。它们的变化事件比可观察值更复杂。

  • 可观察的集合和数组是使用FXCollections实用程序类中的工厂方法创建的。

  • JavaFX Bean 属性由一个 getter、一个 setter 和一个属性 getter 定义。

  • JavaFX Bean 属性可以通过急切实例化、半懒惰实例化和全懒惰实例化策略来实现。

  • 旧式的 Java Bean 属性可以很容易地适应 JavaFX 属性。

资源

以下是使用本章内容的有用资源:

  • 16
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值