JavaFX9-高级教程-全-

JavaFX9 高级教程(全)

原文:Pro JavaFX 9

协议:CC BY-NC-SA 4.0

一、开始使用 JavaFX

Don't ask what the world needs. Ask what makes you energetic and then do it. Because the world needs people who are alive. Howard Thurman

在 2007 年 5 月的 JavaOne 大会上,Sun Microsystems 宣布了一个名为 JavaFX 的新产品系列。它的既定目标包括在手机、电视、嵌入式汽车系统和浏览器等消费设备上开发和部署内容丰富的应用程序。Sun 公司的软件工程师 Josh Marinacci 在一次 Java Posse 采访中非常恰当地做了如下陈述:“JavaFX 是一种重新发明客户机 Java 和修复过去错误的代码。”他指的是 Java Swing 和 Java 2D 有很多功能,但也非常复杂。此外,自从 Swing 和 Java 2D 问世以来,技术已经有了很大的发展。今天的客户端系统(桌面以及移动和嵌入式设备)都配备了强大的图形处理器 GPU。JavaFX 利用了 GPU 提供的新特性和性能提升。通过使用 FXML,JavaFX 允许我们用声明式编程风格简单而优雅地表达用户界面(ui)。它还利用了 Java 的全部功能,因为您可以实例化和使用当今存在的数百万个 Java 类。添加一些特性,比如将 UI 绑定到模型中的属性,并更改侦听器以减少对 setter 方法的需求,这样就有了一个组合,有助于将 Java 恢复到客户端 Internet 应用程序。

在本章中,我们将帮助您快速开发 JavaFX 应用程序。在向您介绍了 JavaFX 的简史之后,我们将向您展示如何获得所需的工具。我们还探索了一些优秀的 JavaFX 资源,并带您完成编译和运行 JavaFX 应用程序的过程。在这个过程中,当我们一起浏览应用程序代码时,您会学到很多关于 JavaFX 应用程序编程接口(API)的知识。

JavaFX 简史

JavaFX 最初是 Chris Oliver 的创意,当时他在一家名为 SeeBeyond 的公司工作。他们需要更丰富的用户界面,所以 Chris 为此创造了一种他称为 F3(形式服从功能)的语言。在“令人难以置信的酷创新”一文中(引用于本章末尾的“参考资料”部分),Chris 引用如下:“当涉及到将人们集成到业务流程中时,您需要图形用户界面来与他们交互,因此在企业应用程序空间中有一个图形用例,SeeBeyond 对拥有更丰富的用户界面感兴趣。”

SeeBeyond 被 Sun 收购,他随后将 F3 的名称改为 JavaFX,并在 JavaOne 2007 上宣布了这一消息。Chris Oliver 在收购期间加入 Sun,继续领导 JavaFX 的开发。

JavaFX Script 的第一个版本是一种解释型语言,被认为是后来出现的编译型 JavaFX Script 语言的原型。解释的 JavaFX 脚本非常健壮,2007 年下半年出版了两本基于该版本的 JavaFX 书籍。一个是用日语写的,另一个是用英语写的(Java FX Script:Jim Weaver 的富互联网/客户端应用程序的动态 Java 脚本(Apress,2007))。

当开发人员正在试验 JavaFX 并提供改进反馈时,Sun 的 JavaFX Script 编译器团队正忙于创建该语言的编译版本。这包括一组新的运行时 API 库。JavaFX Script 编译器项目在 2007 年 12 月初达到了一个临界点,在一篇名为“祝贺 JavaFX Script 编译器团队—大象进门了”的博客文章中对此进行了纪念。这句话来自 JavaFX Script 编译器项目负责人 Tom Ball 的一篇博客文章,其中包含以下摘录。当我最近被问到 JavaFX Script 编译器团队将于何时发布我们的第一个里程碑版本时,我想到了一个大象的比喻。“我不能给你一个准确的日期,”我说。“这就像把一头大象推进一扇门;在临界质量超过阈值之前,你不知道什么时候会结束。不过,一旦你跨过了这个门槛,剩下的事情就会很快发生,而且可以更准确地预测。”

图 1-1 显示了作者之一 Jim Weaver 为该帖子编写的 JavaFX 应用程序的截屏,表明该项目实际上已经达到了 Tom Ball 提到的临界质量。

A323806_4_En_1_Fig1_HTML.jpg

图 1-1。

Screenshot for the “Elephant Is Through the Door” program

JavaFX 在 2008 年继续取得很大进展:

  • NetBeans JavaFX 插件已于 2008 年 3 月推出编译版。
  • 许多 JavaFX 运行时库(主要集中在 JavaFX 的 UI 方面)由一个团队重写,该团队包括来自 Java Swing 团队的一些非常有才华的开发人员。
  • 2008 年 7 月,JavaFX Preview 软件开发工具包(SDK)发布,在 JavaOne 2008 上,Sun 宣布 JavaFX 1.0 SDK 将于 2008 年秋季发布。
  • 2008 年 12 月 4 日,JavaFX 1.0 SDK 发布。这一事件增加了开发人员和 IT 经理对 JavaFX 的采用率,因为它代表了一个稳定的代码库。
  • 2009 年 4 月,甲骨文和 Sun 宣布甲骨文将收购 Sun。JavaFX 1.2 SDK 在 JavaOne 大会 2009 上发布。
  • 2010 年 1 月,甲骨文完成了对太阳的收购。JavaFX 1.3 SDK 于 2010 年 4 月发布,JavaFX 1.3.1 是 1.3 版本的最后一个版本。

在 JavaOne 大会 2010 上,宣布了 JavaFX 2.0。JavaFX 2.0 路线图由 Oracle 发布,包括如下内容。

  • 放弃 JavaFX 脚本语言,转而使用 Java 和 JavaFX 2.0 API。这使得 JavaFX 成为主流,因为它可以被运行在 Java 虚拟机(JVM)上的任何语言(例如 Java、Groovy 和 JRuby)使用。因此,现有的开发人员不需要学习新的语言,但是他们可以使用现有的技能并开始开发 JavaFX 应用程序。
  • 在 JavaFX 2.0 API 中提供 JavaFX Script 的引人注目的特性,包括绑定到表达式。
  • 在 JavaFX 1.3 中已经可用的组件的基础上,提供一组越来越丰富的 UI 组件。
  • 提供一个 Web 组件,用于将 HTML 和 JavaScript 内容嵌入 JavaFX 应用程序。
  • 启用 JavaFX 与 Swing 的互操作性。
  • 从头开始重写媒体堆栈。

JavaFX 2.0 是在 JavaOne 2011 上发布的,由于之前阐述的创新特性,其采用率大大增加。

JavaFX 8 标志着另一个重要的里程碑。JavaFX 现在是 Java 平台标准版不可或缺的一部分。

  • 这清楚地表明 JavaFX 被认为是足够成熟的,它是客户机上 Java 的未来。
  • 这极大地有利于开发人员,因为他们不必下载两个 SDK 和工具套件。
  • Java 8 中的新技术,特别是 lambda 表达式、流 API 和默认接口方法,在 JavaFX 中非常有用。
  • 添加了许多新功能,包括本机 3D 支持、打印 API 和一些新控件,包括日期选择器。
  • 自 JavaFX 8 发布以来,JavaFX 平台遵循与 Java 平台标准版相同的版本和发布过程。因此,当 Java 9 发布时,JavaFX 9 也发布了。
  • Java 9 的主要焦点是模块化。Java 平台标准版已经变得越来越大,并不是所有的应用程序都要求所有的类都可用。通过模块化 Java 平台,可以更容易地创建 Java 平台的子集,这些子集组合了许多足以运行特定应用程序的模块。这种模块化的努力是巨大的,花了许多年才完成。Java Platform,Standard Edition 的所有部分都被重构为模块,包括 JavaFX 9 平台 API。
  • 模块化的结果之一是现在不再允许代码依赖于另一个模块的内部 API。这具有深远的影响。在 JavaFX 9 之前,通常通过实现未记录的内部 API 来创建控件。这些 API 是公共的,因为它们在不同的包中被其他 JavaFX 类内部使用。因此,开发人员也可以使用它们。
  • 由于这些内部 API 现在位于默认情况下不公开该功能的模块中,所以想要创建自定义控件的开发人员需要一种新的方法。因此,JavaFX 团队不仅要将所有 JavaFX 公共 API 迁移到多个模块中,还必须为以前通过内部 API 访问的功能提供公共 API。

在 Java 9 中,JavaFX 平台提供了以下模块:

  • javafx.base
  • javafx.controls
  • javafx.fxml
  • javafx.graphics
  • javafx.jmx
  • javafx.media
  • javafx.swing
  • javafx.swt
  • javafx.web
  • jdk .打包程序
  • jdk.packager.services

既然您已经上了 JavaFX 的必修历史课,让我们向您展示一些示例、工具和其他资源在哪里,从而离编写代码更近一步。

准备您的 JavaFX 之旅

所需工具

因为 JavaFX 是 Java 9 的一部分,所以不必下载单独的 JavaFX SDK。整个 JavaFX API 和实现是 Java 9 SE SDK 的一部分,可以从 www.oracle.com/technetwork/java/javase/downloads/index.html 下载。

该 SDK 包含开发、运行和打包 JavaFX 应用程序所需的一切。您可以使用 Java 9 SE SDK 中包含的命令行工具来编译 JavaFX 应用程序。

然而,为了提高生产率,大多数开发人员更喜欢集成开发环境(IDE)。根据定义,支持 Java 9 的 IDE 也支持 JavaFX 9。因此,您可以使用自己喜欢的 IDE 开发 JavaFX 应用程序。在本书中,我们主要使用 NetBeans IDE,但也可以使用其他 IDE,如 IntelliJ 或 Eclipse。NetBeans IDE 可以从 https://netbeans.org/downloads 下载。

许多 JavaFX 开发人员,尤其是从事用户界面工作的开发人员,更喜欢使用 WYSIWYG 工具来创建界面。Scene Builder 是一个独立的工具,允许您设计 JavaFX 界面,而不是对其进行编码。我们将在第四章中讨论场景构建器。尽管 Scene Builder 生成 FXML(我们在第三章 3 中也会讨论 FXML ),它可以在任何 IDE 中使用,但是 NetBeans 提供了与 Scene Builder 的紧密集成。场景构建工具可以在 http://gluonhq.com/products/scene-builder/ 下载。

JavaFX,社区

JavaFX 不是一个闭源项目,是在一个秘密的掩体中开发的。相反,JavaFX 是以开放的精神开发的,它有开放的源代码库、开放的邮件列表和开放活跃的知识共享社区。

源代码在 OpenJFX 项目中开发,该项目是开发 Java SE 的 OpenJDK 项目的子项目。如果你想检查源代码或架构,或者如果你想阅读邮件列表上的技术讨论,看看 http://openjdk.java.net/projects/openjfx

开发人员社区非常活跃,无论是在 OpenJFX 还是在特定于应用程序的领域。许多 JavaFX 开发人员定期在博客上介绍他们的 JavaFX 活动,许多与 JavaFX 相关的非 Oracle 产品和项目也由该社区创建和维护。

此外,JavaFX 工程师和开发人员维护的博客是 JavaFX 最新技术信息的重要资源。例如,Oracle JavaFX 工程师 Jonathan Giles 在 http://fxexperience.com 让开发人员了解 JavaFX 的最新创新。本章末尾的“参考资料”部分包含了本书作者用来参与 JavaFX 开发人员社区的博客的 URL。

JavaFX 社区的两个重要特征是它自己的创造力和分享的愿望。有许多开源项目为 JavaFX 平台带来了附加值。由于 JavaFX 平台工程师和外部 JavaFX 开发人员之间的良好合作,这些开源项目非常适合官方 JavaFX 平台。

下面列出了一些最有趣的尝试:

  • Gluon 允许你使用 Java 和 JavaFX 创建 iOS 和 Android 应用。因此,您的 JavaFX 应用程序可用于为 Android 设备和 iPhone 或 iPad 创建应用程序。

JavaFX 的这个移动端口将在第 12 章中详细讨论。

  • ControlsFX 是一个致力于向 JavaFX 平台添加高质量控件和附加组件的项目。
  • JFXtras.org 是另一个致力于向 JavaFX 平台添加高质量控件和插件的项目。

值得一提的是,JavaFX 团队正在密切关注 JFXtras.org 和 ControlsFX 的工作,在其中一个项目中产生的想法可能会成为 JavaFX 的下一个版本。

花几分钟时间探索这些网站。接下来,我们指出一些有价值的资源。

使用官方规格

在开发 JavaFX 应用程序时,访问 API Javadoc 文档非常有用,可在 http://download.java.net/jdk9/jfxdocs/index.html 获得,如图 1-2 所示。

A323806_4_En_1_Fig2_HTML.jpg

图 1-2。

JavaFX SDK API Javadoc

例如,图 1-2 中的 API 文档显示了如何使用位于javafx.scene.shape包中的Rectangle类。向下滚动这个网页会显示属性、构造器、方法和其他关于Rectangle类的有用信息。顺便说一下,这个 API 文档可以在您下载的 Java 8 SE SDK 中找到,但是我们希望您也知道如何在线找到它。

除了 Javadoc 之外,手头有级联样式表(CSS)样式参考也非常有用。本文档解释了可以应用于特定 JavaFX 元素的所有样式类。你可以在 http://download.java.net/jdk9/jfxdocs/javafx/scene/doc-files/cssref.html 找到这份文件。

风景

您已经下载了 Scene Builder,该工具允许您通过设计而不是编写代码来创建 ui。我们预计将会有更多由公司和个人开发的工具来帮助您创建 JavaFX 应用程序。ScenicView 是首批免费提供的工具之一,在调试 JavaFX 应用程序时非常有用,它最初由 Oracle 的 Amy Fowler 创建,后来由 Jonathan Giles 维护。您可以在 http://scenic-view.org/ 下载 ScenicView。

ScenicView 特别有用,因为它提供了一个方便的 UI,允许开发人员在运行时检查节点的属性(即维度、翻译、CSS)。

包装和分销

用于向最终用户交付软件的技术总是在变化。过去,交付 Java 应用程序的首选方式是通过 Java 网络启动协议(JNLP)。这样,小应用程序和独立应用程序都可以安装在客户机上。然而,这种技术有许多问题。这个想法只有在最终用户安装了能够执行应用程序的 JVM 的情况下才行得通。这并不总是正确的。即使在桌面领域,系统可以预装 JVM 交付,也存在版本和安全性问题。事实上,一些应用程序是针对特定版本的 JVM 硬编码的。尽管 JVM 中的漏洞在大多数情况下可以很快得到修复,但这仍然要求最终用户总是安装最新版本的 JVM,这可能非常令人沮丧。

最重要的是,浏览器制造商越来越不愿意支持替代的嵌入式平台。总之,依赖浏览器和本地预装的 JVM 并不能提供最佳的最终用户体验。

客户端软件行业正越来越多地转向所谓的应用商店。在这个概念中,可以下载和安装自包含的应用程序。它们不依赖于预先安装的执行环境。这些原则起源于移动领域,苹果的 AppStore 和安卓的 Play Store 在这个领域处于领先地位。尤其是在这些市场,一键安装比本地下载、拆包、手动配置、噩梦多有巨大优势。

在 Java 术语中,自包含应用程序意味着应用程序与能够运行该应用程序的 JVM 捆绑在一起。在过去,这种想法经常被拒绝,因为它使应用程序包太大。然而,随着内存和存储容量的增加,以及通过互联网发送字节的成本的降低,这个缺点变得越来越不相关。

目前有许多正在开发的技术可以帮助您将应用程序与正确的 JVM 版本捆绑在一起并打包。

将 Java 应用程序与 Java 虚拟机运行时捆绑在一起的标准技术是 JavaPackager,它是在 OpenJFX 项目区域内开发的。JavaFXPackager 包含一个用于创建自包含包的 API。NetBeans 使用这个工具,只需点击几下,就可以用它来生成自包含的包。

现在您已经安装了工具,我们将向您展示如何创建一个简单的 JavaFX 程序,然后我们将详细地浏览它。我们为你选择的第一个节目叫做“Hello Earthrise”,它展示了比典型的“Hello World”节目更多的特点。

开发您的第一个 JavaFX 程序:Hello Earthrise

1968 年在平安夜,阿波罗 8 号的机组人员历史上第一次进入月球轨道。他们是第一批见证“地球升起”的人类,拍摄了如图 1-3 所示的壮丽照片。这个图片是在程序启动时从这本书的网站上动态加载的,所以你需要连接到互联网才能查看它。

A323806_4_En_1_Fig3_HTML.jpg

图 1-3。

The Hello Earthrise program

除了演示如何通过互联网动态加载图像,这个例子还展示了如何在 JavaFX 中使用动画。现在是你编译和运行程序的时候了。我们向您展示了两种方法:从命令行和使用 NetBeans。

从命令行编译和运行

我们通常使用 IDE 来构建和运行 JavaFX 程序,但是为了揭开这个过程的神秘面纱,我们首先使用命令行工具。

Note

对于这个练习,就像书中的大多数其他练习一样,您需要源代码。如果您不想在文本编辑器中键入源代码,可以从代码下载站点获得本书中所有示例的源代码。有关该站点的位置,请参见本章末尾的“参考资料”部分。

假设您已经下载了这本书的源代码并将其解压缩到一个目录中,请按照本练习中的说明,按照指示执行所有步骤。我们在练习之后剖析源代码。

Compiling and Running the Hello Earthrise Program from the Command Line

在本练习中,您将使用 javac 和 java 命令行工具来编译和运行程序。从机器上的命令行提示符:

  1. 导航到Chapter01/Hello目录。

  2. 执行以下命令编译HelloEarthRiseMain.java文件。

    javac -d . HelloEarthRiseMain.java
    
    
  3. 因为在这个命令中使用了–d选项,所以生成的类文件被放在与源文件中的包语句相匹配的目录中。这些目录的根目录由为–d选项给出的参数指定,在本例中是当前目录。

  4. 要运行该程序,请执行以下命令。请注意,我们使用将要执行的类的完全限定名,这需要指定路径名和类名的节点,它们都用句点分隔。

    java projavafx.helloearthrise.ui.HelloEarthRiseMain
    
    

程序应该如图 1-4 所示,文本缓慢向上滚动,让人想起星球大战的开场抓取。

祝贺您完成探索 JavaFX 的第一个练习!

了解 Hello Earthrise 计划

现在您已经运行了应用程序,让我们一起浏览一下程序清单。Hello Earthrise 应用程序的代码如清单 1-1 所示。

package projavafx.helloearthrise.ui;

import javafx.animation.Interpolator;
import javafx.animation.Timeline;
import javafx.animation.TranslateTransition;
import javafx.application.Application;
import javafx.geometry.VPos;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.scene.text.Text;
import javafx.scene.text.TextAlignment;
import javafx.stage.Stage;
import javafx.util.Duration;

/**
 * Main class for the "Hello World" style example
 */
public class HelloEarthRiseMain extends Application {

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        Application.launch(args);
    }

    @Override
    public void start(Stage stage) {

        String message
                = "Earthrise at Christmas: "
                + "[Forty] years ago this Christmas, a turbulent world "
                + "looked to the heavens for a unique view of our home "
                + "planet. This photo of Earthrise over the lunar horizon "
                + "was taken by the Apollo 8 crew in December 1968, showing "
                + "Earth for the first time as it appears from deep space. "
                + "Astronauts Frank Borman, Jim Lovell and William Anders "
                + "had become the first humans to leave Earth orbit, "
                + "entering lunar orbit on Christmas Eve. In a historic live "
                + "broadcast that night, the crew took turns reading from "
                + "the Book of Genesis, closing with a holiday wish from "
                + "Commander Borman: \"We close with good night, good luck, "
                + "a Merry Christmas, and God bless all of you    all of "
                + "you on the good Earth.\"";

        // Reference to the Text
        Text textRef = new Text(message);
        textRef.setLayoutY(100);
        textRef.setTextOrigin(VPos.TOP);
        textRef.setTextAlignment(TextAlignment.JUSTIFY);
        textRef.setWrappingWidth(400);
        textRef.setFill(Color.rgb(187, 195, 107));
        textRef.setFont(Font.font("SansSerif", FontWeight.BOLD, 24));

        // Provides the animated scrolling behavior for the text
        TranslateTransition transTransition = new TranslateTransition(new Duration(75000), textRef);
        transTransition.setToY(-820);
        transTransition.setInterpolator(Interpolator.LINEAR);
        transTransition.setCycleCount(Timeline.INDEFINITE);

        // Create an ImageView containing the Image
        Image image = new Image ("http://projavafx.com/img/earthrise.jpg");
        ImageView imageView = new ImageView(image);

        // Create a Group containing the text
        Group textGroup = new Group(textRef);
        textGroup.setLayoutX(50);
        textGroup.setLayoutY(180);
        textGroup.setClip(new Rectangle(430, 85));

        // Combine ImageView and Group
        Group root = new Group(imageView, textGroup);
        Scene scene = new Scene(root, 516, 387);

        stage.setScene(scene);
        stage.setTitle("Hello Earthrise");
        stage.show();

        // Start the text animation
        transTransition.play();
    }
}

Listing 1-1.The HelloEarthRiseMain.java Program

现在您已经看到了代码,让我们更详细地看看它的构造和概念。

建筑商怎么了?

如果您之前使用的是 JavaFX 2,那么您可能对所谓的构建器模式很熟悉。构建器提供了一种声明式的编程风格。不是在类实例上调用set()方法来指定它的字段,构建器模式使用一个Builder类的实例来定义目标类应该如何组成。

构建器在 JavaFX 中非常受欢迎。然而,事实证明,将它们保留在平台中存在重大的技术障碍。因此,决定逐步淘汰建筑商。在 Java 8 中,Builder类仍然是可用的,但是它们已经过时了。在 Java 9 中,Builder类已经被完全移除。

JavaFX 客户端架构师 Richard Bair 在 http://mail.openjdk.java.net/pipermail/openjfx-dev/2013-March/006725.html 的邮件列表条目中可以找到关于Builder类不再受欢迎的原因的更多信息。这个条目的底部包含了一个非常重要的声明:“我相信 FXML 或 lambda 或替代语言都提供了其他途径来实现与构建者相同的目标,但没有字节码或类的额外成本。”

这就是我们将在本书中展示的内容。在本章快结束时,我们在代码中展示了 lambda 表达式的第一个例子。在第三章中,我们展示了 Scene Builder 和 FXML 如何允许你使用一种声明性的方式来定义一个 UI。

在当前示例中,我们以编程方式定义了 UI 的不同组件,并将它们粘合在一起。在第三章中,我们使用基于声明性 FXML 的方法展示了相同的例子。

JavaFX 应用程序

让我们看看第一个例子中的类声明:

public class HelloEarthRiseMain extends Application

该声明声明我们的应用程序扩展了javafx.application.Application类。这个类有一个我们应该实现的抽象方法:

public void start(Stage stage) {}

这个方法将被执行 JavaFX 应用程序的环境调用。

根据环境的不同,JavaFX 应用程序将以不同的方式启动。作为一名开发人员,您不必担心您的应用程序是如何启动的,以及在哪里连接到物理屏幕。您必须实现“start”方法,并使用提供的Stage参数来创建您的 UI,这将在下一段中讨论。

在我们的命令行示例中,我们通过执行 application 类的 main 方法来启动应用程序。main 方法的实现非常简单:

public static void main(String[] args) {
    Application.launch(args);
}

这个 main 方法中唯一的指令是调用应用程序的静态启动方法,这将启动应用程序。

Tip

JavaFX 应用程序总是需要扩展javafx.application.Application类。

一个舞台和一个场景

一个Stage包含一个 JavaFX 应用的 UI,无论它是部署在桌面上、嵌入式系统上还是其他设备上。例如,在桌面上,Stage有自己的顶层窗口,通常包括边框和标题栏。

初始阶段由 JavaFX 运行时创建,并通过start()方法传递给您,如前一段所述。Stage类有一组属性和方法。这些属性和方法中的一些,如清单中的代码片段所示,如下所示。

  • 包含用户界面中图形节点的场景
  • 出现在窗口标题栏中的标题(部署在桌面上时)
  • Stage的可见度
stage.setScene(scene);
stage.setTitle("Hello Earthrise");
stage.show();

A Scene是 JavaFX 场景图中的顶部容器。一个Scene保存显示在Stage上的图形元素。一个Scene中的每个元素都是一个图形节点,它是任何一个扩展了javafx.scene.Node的类。场景图是Scene的分层表示。场景图中的元素可能包含子元素,它们都是Node类的实例。

Scene类包含许多属性,比如它的宽度和高度。一个Scene也有一个名为root的属性,它保存显示在Scene中的图形元素,在本例中是一个包含一个ImageView实例(显示图像)和一个Group实例的Group实例。嵌套在后一个Group中的是一个Text实例(这是一个图形元素,通常称为图形节点,或简称为节点)。

注意,Sceneroot属性包含了Group类的一个实例。root属性可以包含javafx.scene.Node的任何子类的一个实例,并且通常包含一个能够保存自己的一组Node实例的实例。看一看 JavaFX API 文档,我们在“使用官方规范”一节中向您展示了如何访问该文档,并检查Node类以查看可用于任何图形节点的属性和方法。另外,看看javafx.scene.image包中的ImageView类和javafx.scene包中的Group类。在这两种情况下,它们都继承自Node类。

Tip

在阅读本书时,我们再怎么强调手边有 JavaFX API 文档的重要性也不为过。当提到类、变量和函数时,查看文档以获得更多信息是一个好主意。此外,这个习惯有助于您更加熟悉 API 中可用的内容。

显示图像

如下面的代码所示,显示图像需要结合使用一个ImageView实例和一个Image实例。

Image image = new Image ("http://projavafx.com/img/earthrise.jpg");
ImageView imageView = new ImageView(image);

Image实例识别图像资源,并从分配给其 URL 变量的 URL 中加载它。这两个类都位于javafx.scene.image包中。

显示文本

在本例中,我们创建了一个文本节点,如下所示:

Text textRef = new Text(message);

如果您查阅 JavaFX API 文档,您会注意到包含在包javafx.scene.text中的Text实例扩展了一个Shape,后者扩展了一个Node。因此,Text实例也是Node,并且Node上的所有属性也适用于Text。此外,Text实例可以像使用其他节点一样在场景图中使用。

从示例中可以看出,Text实例包含许多可以修改的属性。大多数属性都是不言自明的,但是在操作对象时参考 JavaFX API 文档总是有用的。

因为 JavaFX 中的所有图形元素都直接或间接地扩展了Node类,并且因为Node类已经包含了许多有用的属性,所以特定图形元素(如Text)上的属性数量可能相当多。

在我们的示例中,我们设置了有限数量的属性,下面将简要介绍这些属性。

textRef.setLayoutY(100)方法将 100 像素的垂直平移应用于Text内容。fill方法用于指定文本的颜色。

当您查看 API 文档中的javafx.scene.text包时,请看一下Font类的 font 函数,它用于定义字体系列、粗细和Text的大小。

属性指定文本如何与其区域对齐。

再次参考 JavaFX API 文档,注意 VPos enum(在javafx.geometry包中)有作为常量的字段,例如 BASELINE、BOTTOM 和 TOP。这些控制文本相对于显示的Text垂直位置的原点:

  • 顶部原点,正如我们在前面的代码片段中使用的,将文本的顶部(包括升序)放置在布局位置,相对于Text所在的坐标空间。
  • 底部原点将文本的底部放置在布局位置,包括下行字母(例如,位于小写的 g 中)。
  • 基线原点将文本的基线(不包括下行)放置在布局位置。这是一个Text实例的textOrigin属性的默认值。

wrappingWidth属性使您能够指定文本将在多少像素处换行。

textAlignment属性使您能够控制文本如何对齐。在我们的例子中,TextAlignment.JUSTIFY将文本左右对齐,扩展单词之间的空间来实现这一点。

我们正在显示的文本足够长,足以包裹并绘制在地球上,因此我们需要定义一个矩形区域,在这个区域之外,文本是看不见的。

Tip

我们建议您修改一些值,重新编译该示例,然后再次运行它。这将帮助您理解不同属性的工作原理。或者,通过使用ScenicView,您可以在运行时检查和修改不同的属性。

将图形节点作为一个组使用

JavaFX 的一个强大的图形特性是创建场景图的能力,场景图由图形节点树组成。然后,您可以为位于层次结构中的Group的属性赋值,包含在Group中的节点将受到影响。在我们当前的清单 1-1 的例子中,我们使用了一个Group来包含一个Text节点,并在Group中裁剪一个特定的矩形区域,这样当文本向上移动时,它就不会出现在月球或地球上。下面是相关的代码片段:

Group textGroup = new Group(textRef);
textGroup.setLayoutX(50);
textGroup.setLayoutY(180);
textGroup.setClip(new Rectangle(430, 85));

请注意,Group位于其默认位置的右侧 50 像素和下方 180 像素处。这是由于分配给Group实例的layoutXlayoutY变量的值。因为这个Group直接包含在Scene中,所以它的左上角的位置是从Scene的左上角向右 50 像素,向下 180 像素。看一下图 1-4 来看看这个例子,当你阅读其余的解释时。

A323806_4_En_1_Fig4_HTML.jpg

图 1-4。

The Scene, Group, Text, and clip illustrated

一个Group实例包含了Node子类的实例,通过children()方法将它们的集合分配给自己。在前面的代码片段中,Group包含一个Text实例,该实例的layoutY属性被赋值。因为这个Text包含在一个Group中,所以它假定了Group的二维空间(也称为坐标空间),其中Text节点的原点(0,0)与Group的左上角重合。将值 100 赋给layoutY属性会导致Text位于Group顶部下方 100 个像素处,而Group正好位于剪辑区域底部的下方,因此在动画开始之前,剪辑区域不会出现在视图中。因为没有给layoutX变量赋值,所以它的值是 0(默认值)。

刚刚描述的GrouplayoutXlayoutY属性是我们之前陈述的例子,即包含在Group中的节点将受到分配给Group属性的值的影响。另一个例子是将一个Group实例的不透明度属性设置为 0.5,这会导致该Group中包含的所有节点变成半透明的。如果 JavaFX API 文档很方便,可以看看javafx.scene.Group类中可用的属性。然后查看javafx.scene.Node类属性中可用的属性,在这里您可以找到由Group类继承的layoutXlayoutY和不透明度变量。

剪裁图形区域

为了定义一个裁剪区域,我们为 clip 属性分配一个Node子类来定义裁剪形状,在本例中是一个宽 430 像素、高 85 像素的Rectangle。除了防止Text遮住月亮之外,当Text因为动画而向上滚动时,剪辑区域还防止Text遮住地球。

动画文本,使其向上滚动

当调用HelloEarthriseMain程序时,Text开始缓慢向上滚动。为了实现这个动画,我们使用了位于javafx.animation包中的TranslateTransition类,如清单 1-1 中的代码片段所示。

TranslateTransition transTransition = new TranslateTransition(new Duration(75000), textRef);
transTransition.setToY(-820);
transTransition.setInterpolator(Interpolator.LINEAR);
transTransition.setCycleCount(Timeline.INDEFINITE);
...code omitted...
// Start the text animation
transTransition.play();

javafx.animation包包含了制作节点动画的便利类。这个TranslateTransition实例在 75 秒的时间内将textRef变量引用的Text节点从其原始的 100 像素的 Y 位置转换为–820 像素的 Y 位置。Interpolator.LINEAR常量被赋予插值器属性,这使得动画以线性方式进行。查看一下javafx.animation包中Interpolator类的 API 文档,会发现还有其他形式的插值可用,其中一种是 EASE_OUT,它会在指定持续时间的末尾减慢动画的速度。

Note

在这种情况下,插值是在给定起始值、结束值和持续时间的情况下计算任意时间点的值的过程。

前面代码片段中的最后一行开始执行之前在程序中创建的TranslateTransition实例的 play 方法。这使得Text开始向上滚动。由于分配给cycleCount变量的值,这个转换将无限重复。

现在,您已经使用命令行工具编译并运行了此示例,并且我们已经一起浏览了代码,是时候开始使用 NetBeans IDE 来使开发和部署过程变得更快更容易了。

用 NetBeans 构建和运行程序

假设您已经下载了本书的源代码并将其解压缩到一个目录中,请按照本练习中的说明在 NetBeans 中构建并运行 Hello Earthrise 程序。如果您还没有下载 Java SDK 和 NetBeans,请从本章末尾“参考资料”一节中列出的站点下载。

Building and Running Hello Earthrise with Netbeans

要构建并运行 Hello Earthrise 程序,请执行以下步骤。

  1. 启动 NetBeans。

  2. Choose File ➤ New Project from the menu bar. The first window of the New Project Wizard will appear. Select the JavaFX category, and you will see wizard shown in Figure 1-5.

    A323806_4_En_1_Fig5_HTML.jpg

    图 1-5。

    New Project Wizard

  3. Choose JavaFX Application in the Projects pane, and then click Next. The next page in the New Project Wizard, shown in Figure 1-6, should appear.

    A323806_4_En_1_Fig6_HTML.jpg

    图 1-6。

    The next page of the New Project Wizard

  4. 在这个屏幕上,键入项目名称(我们使用 HelloEarthRise)并单击 Browse。

  5. 直接在文本框中输入项目位置,或者点击 Browse 导航到所需的目录(我们使用了/home/johan/NetBeansProjects)来选择项目位置。

  6. 选择创建应用程序类复选框,并将提供的包/类名更改为projavafx.helloearthrise.ui.HelloEarthRiseMain

  7. 单击完成。现在应该已经创建了 HelloEarthRise 项目,该项目具有由 NetBeans 创建的默认主类。如果您想要运行此默认程序,请在“项目”窗格中右键单击 HelloEarthRise 项目,然后从快捷菜单中选择“运行项目”。

  8. 将清单 1-1 中的代码输入 HelloEarthRiseMain.java 代码窗口。你可以输入它,或者从本书源代码下载的Chapter01/HelloEarthRise/src/projavafx/helloearthrise/ui目录下的HelloEarthRiseMain.java文件中剪切并粘贴它。

  9. 在“项目”窗格中右键单击 HelloEarthRise 项目,然后从快捷菜单中选择“运行项目”。

HelloEarthRise 程序应该开始执行,如本章前面的图 1-3 所示。

至此,您已经从命令行和使用 NetBeans 构建并运行了“Hello Earthrise”程序应用程序。在离开这个例子之前,我们向您展示了实现滚动Text节点的另一种方法。在javafx.scene.control包中有一个名为ScrollPane的类,其目的是提供一个节点的可滚动视图,该视图通常比视图大。此外,用户可以在可滚动区域内拖动正在查看的节点。图 1-7 显示了使用ScrollPane控件修改后的 Hello Earthrise 程序。

A323806_4_En_1_Fig7_HTML.jpg

图 1-7。

Using the ScrollPane control to provide a scrollable view of the Text node

请注意,移动光标是可见的,表示用户可以在裁剪区域周围拖动节点。注意图 1-7 中的截图是运行在 macOS X 上的程序,移动光标在其他平台上有不同的外观。清单 1-2 包含了这个例子的相关代码部分,名为HelloScrollPaneMain.java

...code omitted...
    // Create a ScrollPane containing the text
        ScrollPane scrollPane = new ScrollPane();
        scrollPane.setLayoutX(50);
        scrollPane.setLayoutY(180);
        scrollPane.setPrefWidth(400);
        scrollPane.setPrefHeight(85);
        scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
        scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
        scrollPane.setPannable(true);
        scrollPane.setContent(textRef);
        scrollPane.setStyle("-fx-background-color: transparent;");

        // Combine ImageView and ScrollPane
        Group root = new Group(imageView, scrollPane);
        Scene scene = new Scene(root, 516, 387);

Listing 1-2.The HelloScrollPaneMain.java Program

现在您已经学习了 JavaFX 应用程序开发的一些基础知识,让我们研究另一个示例应用程序来帮助您学习更多的 JavaFX 概念和构造。

开发您的第二个 JavaFX 程序:“更多牛铃!”

如果你熟悉周六夜现场电视节目,你可能看过“更多牛铃”的小品,在这个小品中,克里斯托弗·沃肯扮演的角色在一次蓝牡蛎邪教录制会上不断要求“更多牛铃”。下面的 JavaFX 示例程序在一个假想的应用程序的上下文中介绍了 JavaFX 的一些简单而强大的概念,该应用程序允许您选择音乐流派并控制音量。当然,“牛铃金属”,简称“牛铃”,是可用的流派之一。图 1-8 显示了这个应用程序的截图,它有一种复古的 iPhone 应用程序外观。

A323806_4_En_1_Fig8_HTML.jpg

图 1-8。

The Audio Configuration “More Cowbell” program

构建和运行音频配置程序

在本章的前面,我们向您展示了如何在 NetBeans 中创建新的 JavaFX 项目。对于本例(以及本书中的其他示例),我们利用了本书的代码下载包包含每个示例的 NetBeans 和 Eclipse 项目文件这一事实。按照本练习中的说明构建并运行音频配置应用程序。

Building and Running the Audio Configuration Program Using Netbeans

要使用 NetBeans 构建和执行该程序,请执行以下步骤。

  1. From the File menu, select the Open Project menu item. In the Open Project dialog box, navigate to the Chapter01 directory where you extracted the book’s code download bundle, as shown in Figure 1-9.

    A323806_4_En_1_Fig9_HTML.jpg

    图 1-9。

    The Chapter 01 directory in the Open Project dialog box

  2. 在左侧面板中选择 AudioConfig 项目,然后点按“打开项目”。

  3. 按照前面讨论的方式运行项目。

应用程序应如图 1-8 所示。

音频配置程序的行为

运行应用程序时,请注意调整音量滑块会改变显示的相关分贝(dB)级别。此外,选择静音复选框会禁用滑块,选择各种风格会改变音量滑块。这种行为是由以下代码中显示的概念实现的,例如:

  • 绑定到包含模型的类
  • 使用更改监听器
  • 创建可观察列表

了解音频配置程序

音频配置程序包含两个源代码文件,如清单 1-3 和清单 1-4 所示:

  • 清单 1-3 中的AudioConfigMain.java文件包含了主类,并以您在清单 1-1 中的 Hello Earthrise 示例中所熟悉的方式来表达 UI。
  • 清单 1-4 中的AudioConfigModel.java文件包含了这个程序的模型,它保存了 UI 绑定到的应用程序的状态。

看一看清单 1-3 中的AudioConfigMain.java源代码,之后我们一起检查它,重点关注前一个例子中没有涉及的概念。

package projavafx.audioconfig.ui;

import javafx.application.Application;
import javafx.geometry.VPos;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ChoiceBox;
import javafx.scene.control.Slider;
import javafx.scene.paint.Color;
import javafx.scene.paint.CycleMethod;
import javafx.scene.paint.LinearGradient;
import javafx.scene.paint.Stop;
import javafx.scene.shape.Line;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.scene.text.Text;
import javafx.stage.Stage;
import projavafx.audioconfig.model.AudioConfigModel;

public class AudioConfigMain extends Application {

    // A reference to the model
    AudioConfigModel acModel = new AudioConfigModel();

    Text textDb;
    Slider slider;
    CheckBox mutingCheckBox;
    ChoiceBox genreChoiceBox;
    Color color = Color.color(0.66, 0.67, 0.69);

    public static void main(String[] args) {
        Application.launch(args);
    }

    @Override
    public void start(Stage stage) {
        Text title = new Text(65,12, "Audio Configuration");
        title.setTextOrigin(VPos.TOP);
        title.setFill(Color.WHITE);
        title.setFont(Font.font("SansSerif", FontWeight.BOLD, 20));

        Text textDb = new Text();
        textDb.setLayoutX(18);
        textDb.setLayoutY(69);
        textDb.setTextOrigin(VPos.TOP);
        textDb.setFill(Color.web("#131021"));
        textDb.setFont(Font.font("SansSerif", FontWeight.BOLD, 18));

        Text mutingText = new Text(18, 113, "Muting");
        mutingText.setTextOrigin(VPos.TOP);
        mutingText.setFont(Font.font("SanSerif", FontWeight.BOLD, 18));
        mutingText.setFill(Color.web("#131021"));

        Text genreText = new Text(18,154,"Genre");
        genreText.setTextOrigin(VPos.TOP);
        genreText.setFill(Color.web("#131021"));
        genreText.setFont(Font.font("SanSerif", FontWeight.BOLD, 18));

        slider = new Slider();
        slider.setLayoutX(135);
        slider.setLayoutY(69);
        slider.setPrefWidth(162);
        slider.setMin(acModel.minDecibels);
        slider.setMax(acModel.maxDecibels);

        mutingCheckBox = new CheckBox();
        mutingCheckBox.setLayoutX(280);
        mutingCheckBox.setLayoutY(113);

        genreChoiceBox = new ChoiceBox();
        genreChoiceBox.setLayoutX(204);
        genreChoiceBox.setLayoutY(154);
        genreChoiceBox.setPrefWidth(93);
        genreChoiceBox.setItems(acModel.genres);
        Stop[] stops = new Stop[]{new Stop(0, Color.web("0xAEBBCC")), new Stop(1, Color.web("0x6D84A3"))};

        LinearGradient linearGradient = new LinearGradient(0, 0, 0, 1, true, CycleMethod.NO_CYCLE, stops);
        Rectangle rectangle = new Rectangle(0, 0, 320, 45);
        rectangle.setFill(linearGradient);

        Rectangle rectangle2 = new Rectangle(0, 43, 320, 300);
        rectangle2.setFill(Color.rgb(199, 206, 213));

        Rectangle rectangle3 = new Rectangle(8, 54, 300, 130);
        rectangle3.setArcHeight(20);
        rectangle3.setArcWidth(20);
        rectangle3.setFill(Color.WHITE);
        rectangle3.setStroke(color);

        Line line1 = new Line(9, 97, 309, 97);
        line1.setStroke(color);

        Line line2 = new Line(9, 141, 309, 141);
        line2.setFill(color);

        Group group = new Group(rectangle, title, rectangle2, rectangle3,
                textDb,
                slider,
                line1,
                mutingText,
                mutingCheckBox, line2, genreText,
                genreChoiceBox);
        Scene scene = new Scene(group, 320, 343);

        textDb.textProperty().bind(acModel.selectedDBs.asString().concat(" dB"));
        slider.valueProperty().bindBidirectional(acModel.selectedDBs);
        slider.disableProperty().bind(acModel.muting);
        mutingCheckBox.selectedProperty().bindBidirectional(acModel.muting);
        acModel.genreSelectionModel = genreChoiceBox.getSelectionModel();
        acModel.addListenerToGenreSelectionModel();
        acModel.genreSelectionModel.selectFirst();

        stage.setScene(scene);
        stage.setTitle("Audio Configuration");
        stage.show();
    }
}

Listing 1-3.The AudioConfigMain.java Program

现在您已经看到了这个应用程序中的主类,让我们来看一下新概念。

装订的魔力

JavaFX 最强大的方面之一是绑定,它使应用程序的 UI 能够轻松地与应用程序的状态或模型保持同步。JavaFX 应用程序的模型通常保存在一个或多个类中,在本例中是AudioConfigModel类。请看下面的代码片段,摘自清单 1-3 ,其中我们创建了这个模型类的一个实例。

  AudioConfigModel acModel = new AudioConfigModel();

在这个 UI 的场景中有几个图形节点实例(回想一下,场景由一系列节点组成)。跳过其中的几个,我们来看下面代码片段中显示的图形节点,这些节点有一个属性绑定到模型中的selectedDBs属性。

textDb = new Text();
... code omitted
slider = new Slider();
...code omitted...
textDb.textProperty().bind(acModel.selectedDBs.asString().concat(" dB"));
slider.valueProperty().bindBidirectional(acModel.selectedDBs);

如这段代码所示,Text对象的 text 属性被绑定到一个表达式。bind函数包含一个表达式(包含selectedDBs属性),该表达式被求值并成为文本属性的值。查看图 1-9 (或检查正在运行的应用程序)以查看滑块左侧显示的Text节点的内容值。

还要注意代码中的Slider节点的value属性也绑定到了模型中的selectedDBs属性,但是它使用了bindBidirectional()方法。这导致绑定是双向的,所以在这种情况下,当滑块移动时,模型中的selectedDBs属性会改变。相反,当selectedDBs属性改变时(作为改变类型的结果),滑块移动。

继续移动滑块来演示代码片段中绑定表达式的效果。滑块左侧显示的分贝数应随着滑块的调整而变化。

在清单 1-3 中还有其他绑定属性,我们在遍历模型类时会指出。在离开 UI 之前,我们在这个例子中指出一些与颜色相关的概念。

颜色和渐变

清单 1-3 中的以下代码片段包含了一个定义颜色渐变模式和颜色的例子。

Stop[] stops = new Stop[]{new Stop(0, Color.web("0xAEBBCC")), new Stop(1, Color.web("0x6D84A3"))};
LinearGradient linearGradient = new LinearGradient(0, 0, 0, 1, true, CycleMethod.NO_CYCLE, stops);
Rectangle rectangle = new Rectangle(0, 0, 320, 45);
rectangle.setFill(linearGradient);

如果 JavaFX API 文档很方便,首先看一下javafx.scene.shape.Rectangle类,注意它继承了一个名为fill的属性,该属性的类型为javafx.scene.paint.Paint。查看Paint类的 JavaFX API 文档,您会看到ColorImagePatternLinearGradientRadialGradient类是Paint的子类。这意味着可以为任何形状的填充指定颜色、图案或渐变。

要创建一个LinearGradient,如代码所示,您需要定义至少两个停靠点,它们定义了位置和该位置的颜色。在此示例中,第一个停止点的偏移值为 0.0,第二个停止点的偏移值为 1.0。这些是单位正方形两端的值,结果是梯度将跨越整个节点(在这种情况下是一个Rectangle)。LinearGradient的方向由它的startXstartYendXendY值控制,我们通过构造器传递这些值。在这种情况下,方向只是垂直的,因为startY值是 0.0,endY值是 1.0,而startXendX值都是 0.0。

注意,在清单 1-1 中的 Hello Earthrise 示例中,名为Color.WHITE的常量用于表示白色。在前面的代码片段中,Color类的 web 函数用于根据十六进制值定义颜色。

音频配置示例的模型类

看看清单 1-4 中AudioConfig Model类的源代码。

package projavafx.audioconfig.model;

import javafx.beans.Observable;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.control.SingleSelectionModel;

/**
 * The model class that the AudioConfigMain class uses
 */
public class AudioConfigModel {
  /**
   * The minimum audio volume in decibels
   */
  public double minDecibels = 0.0;

  /**
   * The maximum audio volume in decibels
   */
  public double maxDecibels = 160.0;

  /**
   * The selected audio volume in decibels
   */
  public IntegerProperty selectedDBs = new SimpleIntegerProperty(0);

  /**
   * Indicates whether audio is muted
   */
  public BooleanProperty muting = new SimpleBooleanProperty(false);

  /**
   * List of some musical genres
   */
  public ObservableList genres = FXCollections.observableArrayList(
    "Chamber",
    "Country",
    "Cowbell",
    "Metal",
    "Polka",
    "Rock"
  );

  /**
   * A reference to the selection model used by the Slider
   */
  public SingleSelectionModel genreSelectionModel;

  /**
   * Adds a change listener to the selection model of the ChoiceBox, and contains
   * code that executes when the selection in the ChoiceBox changes.
   */
public void addListenerToGenreSelectionModel() {
    genreSelectionModel.selectedIndexProperty().addListener((Observable o) -> {
        int selectedIndex = genreSelectionModel.selectedIndexProperty().getValue();
        switch(selectedIndex) {
            case 0: selectedDBs.setValue(80);
            break;
            case 1: selectedDBs.setValue(100);
            break;
            case 2: selectedDBs.setValue(150);
            break;
            case 3: selectedDBs.setValue(140);
            break;
            case 4: selectedDBs.setValue(120);
            break;
            case 5: selectedDBs.setValue(130);
        }
    });

  }
}

Listing 1-4.The Source Code for AudioConfigModel.java

使用失效侦听器和 Lambda 表达式

在“绑定的魔力”一节中,我们展示了如何使用属性绑定来动态改变参数。还有另一种更低级但也更灵活的方法来实现这一点,使用ChangeListenersInvalidationListeners。这些概念将在第四章中详细讨论。

在我们的例子中,我们给genreSelectionModelselectedIndexProperty添加了一个InvalidationListener。当selectedIndexProperty的值改变时,当我们还没有检索它时,添加的InvalidationListener上的invalidated(Observable)方法将被调用。在这个方法的实现中,我们检索了selectedIndexProperty的值,并根据它的值修改了selectedDBs属性的值。这是通过以下代码实现的:

public void addListenerToGenreSelectionModel() {
    genreSelectionModel.selectedIndexProperty().addListener((Observable o) -> {
        int selectedIndex = genreSelectionModel.selectedIndexProperty().getValue();
        switch(selectedIndex) {
            case 0: selectedDBs.setValue(80);
            break;
            case 1: selectedDBs.setValue(100);
            break;
            case 2: selectedDBs.setValue(150);
            break;
            case 3: selectedDBs.setValue(140);
            break;
            case 4: selectedDBs.setValue(120);
            break;
            case 5: selectedDBs.setValue(130);
        }
    });

  }

注意,我们在这里使用的是 lambda 表达式,而不是创建一个新的InvalidationListener实例并实现它的单个抽象方法 invalidated。

Tip

JavaFX 8 的主要增强之一是它使用了 Java 8。因此,具有单一抽象方法的抽象类可以很容易地被 lambda 表达式替换,这明显增强了代码的可读性。

是什么原因导致genreSelectionModelselectedIndexProperty发生变化?为了找到这个问题的答案,我们必须重新查看清单 1-3 中的一些代码。在下面的代码片段中,ChoiceBoxsetItems方法用于用包含流派的条目填充ChoiceBox

genreChoiceBox = new ChoiceBox();
genreChoiceBox.setLayoutX(204);
genreChoiceBox.setLayoutY(154);
genreChoiceBox.setPrefWidth(93);
genreChoiceBox.setItems(acModel.genres);

清单 1-4 中的模型代码片段包含了ComboBox项绑定到的集合:

/**
 * List of some musical genres
 */
public ObservableList genres = FXCollections.observableArrayList(
  "Chamber",
  "Country",
  "Cowbell",
  "Metal",
  "Polka",
  "Rock"
);

当用户在ChoiceBox中选择不同的项目时,invalidationListener被调用。再次查看invalidationListener中的代码,您会看到selectedDBs属性的值发生了变化,您可能还记得,它是双向绑定到滑块的。这就是当您在组合框中选择一个流派时滑块移动的原因。继续运行音频配置程序来测试这一点。

Note

ChoiceBoxitems属性与一个ObservableList相关联会导致ChoiceBox中的项目在底层集合中的元素被修改时自动更新。

调查 JavaFX 特性

我们通过调查 JavaFX 的许多特性来结束这一章,其中一些是对您的回顾。我们通过描述 Java SDK API 中几个更常用的包和类来做到这一点。

javafx.stage包包含以下内容:

  • Stage类,它是任何 JavaFX 应用程序的 UI 容器层次结构的顶层,不管它部署在哪里(例如,桌面、浏览器或手机)。
  • Screen类,代表运行 JavaFX 程序的机器上的显示器。这使您能够获得有关屏幕的信息,如尺寸和分辨率。

javafx.scene包包含一些您经常使用的类:

  • Scene类是 JavaFX 应用程序 UI 包含层次结构的第二层。它包括应用程序中包含的所有 UI 元素。这些元素被称为图形节点,或简称为节点。
  • Node类是 JavaFX 中所有图形节点的基类。文本、图像、媒体、形状和控件(如文本框和按钮)等 UI 元素都是Node的子类。花点时间看一下Node类中的变量和函数,以了解提供给所有子类的功能,包括边界计算和鼠标键盘事件处理。
  • Group类是Node类的子类。其目的包括将节点分组到单个坐标空间中,并允许将变换(例如旋转)应用于整个组。此外,被改变的组属性(例如,不透明度)适用于该组中包含的所有节点。

有几个包以javafx.scene开头,包含各种类型的Node的子类。例如:

  • javafx.scene.image包包含了ImageImageView类,它们使得图像能够在Scene中显示。ImageView类是Node的子类。
  • javafx.scene.shape包包含几个绘制形状的类,如CircleRectangleLinePolygonArc。形状的基类名为Shape,包含一个名为fill的属性,该属性使您能够指定填充形状的颜色、图案或渐变。
  • javafx.scene.text包包含用于在场景中绘制文本的Text类。Font类使你能够指定字体名称和文本大小。
  • javafx.scene.media包中有允许你播放媒体的类。MediaView类是显示媒体的Node的子类。
  • 这个javafx.scene.chart包有帮助你轻松创建面积图、条形图、气泡图、折线图、饼图和散点图的类。这个包中对应的 UI 类有AreaChartBarChartBubbleChartLineChartPieChartScatterChart

下面是 JavaFX 8 API 中的一些其他包。

  • javafx.scene.control包包含了几个 UI 控件,每个都能够通过 CSS 来设置皮肤和样式。
  • javafx.scene.transform包使您能够变换节点(缩放、旋转、平移、剪切和仿射)。
  • javafx.scene.input包包含了像MouseEventKeyEvent这样的类,它们从一个事件处理函数(比如Node类的onMouseClicked事件)中提供关于这些事件的信息。
  • javafx.scene.layout包包含多个布局容器,包括HBoxVBoxBorderPaneFlowPaneStackPaneTilePane
  • javafx.scene.effect包包含ReflectionGlowShadowBoxBlurLighting等简单易用的效果。
  • javafx.scene.web包包含了在 JavaFX 应用程序中轻松嵌入 web 浏览器的类。
  • javafx.animation包包含基于时间的插值,通常用于动画和普通过渡的便利类。
  • javafx.beansjavafx.beans.bindingjavafx.beans.propertyjavafx.beans.value包包含实现属性和绑定的类。
  • javafx.fxml包包含实现 FXML 这种非常强大的工具的类,FXML 是一种用 XML 表示 JavaFX UIs 的标记语言。
  • javafx.util包包含实用程序类,如 HelloEarthRise 示例中使用的Duration类。
  • javafx.print包包含打印 JavaFX 应用程序(部分)布局的实用程序。
  • javafx.embed.swing包包含 Swing 应用程序中嵌入式 JavaFX 应用程序所需的功能。
  • javafx.embed.swt包包含在 SWT 应用程序中嵌入 JavaFX 应用程序所需的功能。

根据这些信息,再次查看 JavaFX API 文档,以便更深入地了解如何使用它的功能。

摘要

恭喜你!在本章中,您学习了很多关于 JavaFX 的知识,包括

  • JavaFX 是富客户端 Java,是软件开发行业所需要的。
  • 自从 Java 9 发布以来,JavaFX APIs 被分成许多遵循 Java 9 约定和规则的模块。
  • JavaFX 历史上的一些高潮。
  • 在哪里可以找到 JavaFX 资源,包括 Java SDK、NetBeans、Scene Builder、ScenicView 和 API 文档。
  • 如何从命令行编译和运行 JavaFX 程序。
  • 如何使用 NetBeans 构建和运行 JavaFX 程序。
  • 如何使用 JavaFX API 中的几个类?
  • 如何在 JavaFX 中创建一个类,并将其用作包含 JavaFX 应用程序状态的模型。
  • 如何使用属性绑定使用户界面与模型保持同步?

我们还查看了许多可用的 API 包和类,您了解了如何利用它们的功能。既然您已经开始使用 JavaFX,那么您可以在第二章中开始研究 JavaFX 的细节。

资源

有关 JavaFX 的一些背景信息,可以参考以下资源。

二、在 JavaFX 中创建用户界面

生活是没有橡皮擦的绘画艺术。—约翰·w·加德纳

第一章讲述了开发和执行 JavaFX 程序的基础知识,帮助您快速使用 JavaFX。现在我们将讲述在 JavaFX 中创建 UI 的许多细节,这些细节在第一章中被忽略了。议程上的第一项是让您熟悉 JavaFX 用来表达 UI 的剧场隐喻,并涵盖我们称之为以节点为中心的 UI 的意义。

用户界面的编程式创建与声明式创建

JavaFX 平台为创建 UI 提供了两种互补的方式。在本章中,我们将讨论如何使用 Java API 来创建和填充 UI。对于习惯于编写代码来利用 API 的 Java 开发人员来说,这是一种方便的方式。

设计者经常使用图形工具来声明而不是编程 UI。JavaFX 平台定义了 FXML,这是一种基于 XML 的标记语言,可用于以声明方式描述 UI。此外,Gluon 提供了一个名为 Scene Builder 的图形工具,该工具能够处理 FXML 文件。场景生成器的使用在第四章中演示。

请注意,部分 UI 可以使用 API 创建,而其他部分可以使用 Scene Builder 创建。FXML APIs 提供了两种方法之间的桥梁和集成粘合剂。

以节点为中心的用户界面简介

在 JavaFX 中创建 UI 就像创建一部戏剧,因为它通常由以下非常简单的步骤组成:

  1. 创造一个你的程序可以表演的舞台。您的阶段的实现将取决于它所部署的平台(例如,台式机、平板电脑或嵌入式系统)。
  2. 创建一个场景,其中演员和道具(节点)将在视觉上相互交流,并与观众(您的程序的用户)交流。像戏剧行业中任何优秀的布景设计师一样,优秀的 JavaFX 开发人员努力使他们的场景在视觉上吸引人。为此,与平面设计师合作完成你的“戏剧”通常是个好主意
  3. 在场景中创建节点。这些节点是javafx.scene.Node类的子类,包括 UI 控件、形状、文本(一种形状)、图像、媒体播放器、嵌入式浏览器和您创建的自定义 UI 组件。节点也可以是其他节点的容器,通常提供跨平台的布局功能。场景具有包含节点的有向图的场景图。通过更改一组非常丰富的Node属性的值,可以以多种方式操作单个节点和节点组(例如,移动、缩放和设置不透明度)。
  4. 创建表示场景中节点模型的变量和类。正如在第一章中所讨论的,JavaFX 的一个非常强大的方面是绑定,它使应用程序的 UI 能够很容易地与应用程序的状态或模型保持同步。注本章中的大多数例子都是用来演示 UI 概念的小程序。由于这个原因,许多例子中的模型由出现在主程序中的变量组成,而不是包含在单独的 Java 类中(例如第一章中的AudioConfigModel类)。
  5. 创建事件处理程序,比如onMousePressed,允许用户与你的程序交互。通常,这些事件处理程序操纵模型中的实例变量。许多这样的处理程序需要实现一个抽象方法,因此提供了一个使用 lambda 表达式的绝佳机会。
  6. 创建为场景添加动画的时间轴和过渡。例如,您可能希望书籍列表的缩略图在场景中平滑移动,或者希望 UI 中的某个页面淡入视图。你可能只是想让一个乒乓球在场景中移动,从墙壁和球拍上反弹回来;这将在本章后面的“节点冲突检测的原理”一节中演示。

让我们从第 1 步开始,在这一步中,我们检查阶段的功能。

搭建舞台

您的舞台的外观和功能将取决于它所部署的平台。例如,如果部署在移动设备或带有触摸屏的嵌入式设备中,您的舞台可能是整个触摸屏。部署在 X11 系统中的 JavaFX 程序的舞台将是一个窗口。

了解舞台类

对于任何具有图形用户界面的 JavaFX 程序来说,Stage类都是顶级容器。它有几个属性和方法,例如,允许它被定位、调整大小、给定标题、变得不可见或给定某种程度的不透明度。我们所知道的学习一个类的能力的两个最好的方法是研究 JavaFX API 文档和检查(和编写)使用它的程序。在本节中,我们要求您两者都做,从查看 API 文档开始。

JavaFX API 文档和其他 Java API 文档一样,可以在 http://download.java.net/java/jdk9/docs/api/overview-summary 在线获得。在浏览器中打开index.html文件,导航到 javafx.graphics 模块中的javafx.stage包,并选择Stage类。该页面应包含属性、构造器和方法的表格,包括图 2-1 摘录中显示的部分。

A323806_4_En_2_Fig1_HTML.jpg

图 2-1。

A portion of the Stage class documentation in the JavaFX API

继续浏览Stage类中每个属性和方法的文档,记住点击链接以显示更详细的信息。当您完成时,请回来,我们将向您展示一个程序,它演示了在Stage类中可用的许多属性和方法。

使用 Stage 类:StageCoach 示例

图 2-2 显示了一个谦逊的、故意不合适的 StageCoach 示例程序的截图。

A323806_4_En_2_Fig2_HTML.jpg

图 2-2。

A screenshot of the StageCoach example

StageCoach 程序旨在指导您使用Stage类和相关类,如StageStyleScreen。此外,我们用这个程序向您展示如何将参数传递到程序中。在浏览程序的行为之前,先打开项目。遵循第一章中构建和执行音频配置项目的说明。项目文件位于 Chapter02 目录中,该目录隶属于您提取本书的代码下载包的位置。

Examining the Behavior of the Stagecoach Program

当程序启动时,其外观应该类似于图 2-2 中的截图。要全面检查其行为,请执行以下步骤。注意,出于教学目的,UI 上的属性和方法名称对应于Stage实例中的属性和方法。

请注意,StageCoach 程序的窗口最初显示在屏幕顶部附近,其水平位置在屏幕中央。拖动程序的窗口,观察 UI 顶部附近的 x 和 y 值会动态更新,以反映它在屏幕上的位置。

调整程序窗口的大小,观察宽度和高度值的变化,以反映Stage的宽度和高度。请注意,这个大小包括窗口的装饰(标题栏和边框)。

单击该程序(或以其他方式使其成为焦点),注意聚焦的值为 true。使窗口失去焦点,可能是通过单击屏幕上的其他地方,注意焦点值变为 false。

清除 resizable 复选框,然后注意 resizable 值变为 false。然后尝试调整窗口大小,注意这是不允许的。再次选中可调整大小复选框,使窗口可调整大小。

选择全屏复选框。请注意,程序占据了整个屏幕,窗口装饰不可见。清除“全屏”复选框,将程序恢复到原来的大小。

编辑标题标签旁边的文本字段中的文本,注意窗口标题栏中的文本已更改以反映新值。

拖动窗口以部分覆盖另一个窗口,然后单击后退()。请注意,这会将程序放在另一个窗口的后面,因此会导致 z 顺序发生变化。

当程序窗口的一部分在另一个窗口后面,但 toFront()按钮可见时,点按该按钮。请注意,该程序的窗口位于另一个窗口的前面。

单击 close(),注意程序已退出。

再次调用程序,传入字符串"undecorated"。如果从 NetBeans 调用,请使用项目属性对话框来传递此参数,如图 2-3 所示。"undecorated"字符串作为不带值的参数传递。

A323806_4_En_2_Fig3_HTML.jpg

图 2-3。

Using NetBeans’ Project Properties dialog box to pass an argument into the program

注意,这次程序出现时没有任何窗口装饰,但是程序的白色背景包括了窗口的背景。图 2-4 截图中的黑色轮廓是桌面背景的一部分。

通过单击 close()再次退出程序,然后再次运行程序,将字符串"transparent"作为参数传入。注意程序以圆角矩形的形状出现,如图 2-5 所示。

A323806_4_En_2_Fig4_HTML.jpg

图 2-4。

The StageCoach program after being invoked with the undecorated argument

注意你可能已经注意到图 2-4 和 2-5 中的截图有负的y值。这是因为在拍摄屏幕截图时,应用程序位于辅助显示器上,逻辑上位于主显示器之上。

A323806_4_En_2_Fig5_HTML.jpg

图 2-5。

The StageCoach program after being invoked with the transparent argument

单击应用程序的用户界面,在屏幕上拖动它,完成后单击关闭()。祝贺你坚持这个 13 步练习!进行这个练习可以让你对它背后的代码有所了解,现在我们一起来看一看。

了解公共马车项目

在我们指出新的相关概念之前,请看一下清单 2-1 中 StageCoach 程序的代码。

package projavafx.stagecoach.ui;
import java.util.List;
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.geometry.Rectangle2D;
import javafx.geometry.VPos;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Text;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.stage.WindowEvent;

public class StageCoachMain extends Application {

    StringProperty title = new SimpleStringProperty();

    Text textStageX;
    Text textStageY;
    Text textStageW;
    Text textStageH;
    Text textStageF;
    CheckBox checkBoxResizable;
    CheckBox checkBoxFullScreen;

    double dragAnchorX;
    double dragAnchorY;

    public static void main(String[] args) {
        Application.launch(args);
    }

    @Override
    public void start(Stage stage) {
        StageStyle stageStyle = StageStyle.DECORATED;
        List<String> unnamedParams = getParameters().getUnnamed();
        if (unnamedParams.size() > 0) {
            String stageStyleParam = unnamedParams.get(0);
            if (stageStyleParam.equalsIgnoreCase("transparent")) {
                stageStyle = StageStyle.TRANSPARENT;
            } else if (stageStyleParam.equalsIgnoreCase("undecorated")) {
                stageStyle = StageStyle.UNDECORATED;
            } else if (stageStyleParam.equalsIgnoreCase("utility")) {
                stageStyle = StageStyle.UTILITY;
            }
        }
        final Stage stageRef = stage;
        Group rootGroup;
        TextField titleTextField;
        Button toBackButton = new Button("toBack()");
        toBackButton.setOnAction(e -> stageRef.toBack());
        Button toFrontButton = new Button("toFront()");
        toFrontButton.setOnAction(e -> stageRef.toFront());
        Button closeButton = new Button("close()");
        closeButton.setOnAction(e -> stageRef.close());
        Rectangle blue = new Rectangle(250, 350, Color.SKYBLUE);
        blue.setArcHeight(50);
        blue.setArcWidth(50);
        textStageX = new Text();
        textStageX.setTextOrigin(VPos.TOP);
        textStageY = new Text();
        textStageY.setTextOrigin(VPos.TOP);
        textStageH = new Text();
        textStageH.setTextOrigin(VPos.TOP);
        textStageW = new Text();
        textStageW.setTextOrigin(VPos.TOP);
        textStageF = new Text();
        textStageF.setTextOrigin(VPos.TOP);
        checkBoxResizable = new CheckBox("resizable");
        checkBoxResizable.setDisable(stageStyle == StageStyle.TRANSPARENT
                || stageStyle == StageStyle.UNDECORATED);
        checkBoxFullScreen = new CheckBox("fullScreen");
        titleTextField = new TextField("Stage Coach");
        Label titleLabel = new Label("title");
        HBox titleBox = new HBox(titleLabel, titleTextField);
        VBox contentBox = new VBox(
                textStageX, textStageY, textStageW, textStageH, textStageF,
                checkBoxResizable, checkBoxFullScreen,
                titleBox, toBackButton, toFrontButton, closeButton);
        contentBox.setLayoutX(30);
        contentBox.setLayoutY(20);
        contentBox.setSpacing(10);
        rootGroup = new Group(blue, contentBox);

        Scene scene = new Scene(rootGroup, 270, 370);
        scene.setFill(Color.TRANSPARENT);

        //when mouse button is pressed, save the initial position of screen
        rootGroup.setOnMousePressed((MouseEvent me) -> {
            dragAnchorX = me.getScreenX() - stageRef.getX();
            dragAnchorY = me.getScreenY() - stageRef.getY();
        });

        //when screen is dragged, translate it accordingly
        rootGroup.setOnMouseDragged((MouseEvent me) -> {
            stageRef.setX(me.getScreenX() - dragAnchorX);
            stageRef.setY(me.getScreenY() - dragAnchorY);
        });

        textStageX.textProperty().bind(new SimpleStringProperty("x: ")
                .concat(stageRef.xProperty().asString()));
        textStageY.textProperty().bind(new SimpleStringProperty("y: ")
                .concat(stageRef.yProperty().asString()));
        textStageW.textProperty().bind(new SimpleStringProperty("width: ")
                .concat(stageRef.widthProperty().asString()));
        textStageH.textProperty().bind(new SimpleStringProperty("height: ")
                .concat(stageRef.heightProperty().asString()));
        textStageF.textProperty().bind(new SimpleStringProperty("focused: ")
                .concat(stageRef.focusedProperty().asString()));
        stage.setResizable(true);
        checkBoxResizable.selectedProperty()
                .bindBidirectional(stage.resizableProperty());
        checkBoxFullScreen.selectedProperty().addListener((ov, oldValue, newValue) -> {
            stageRef.setFullScreen(checkBoxFullScreen.selectedProperty().getValue());
        });
        title.bind(titleTextField.textProperty());

        stage.setScene(scene);
        stage.titleProperty().bind(title);
        stage.initStyle(stageStyle);
        stage.setOnCloseRequest((WindowEvent we) -> {
            System.out.println("Stage is closing");
        });
        stage.show();
        Rectangle2D primScreenBounds = Screen.getPrimary().getVisualBounds();
        stage.setX((primScreenBounds.getWidth() - stage.getWidth()) / 2);
        stage.setY((primScreenBounds.getHeight() - stage.getHeight()) / 4);
    }
}

Listing 2-1.
StageCoachMain.java

获取程序参数

这个程序引入的第一个新概念是读取传递给 JavaFX 程序的参数的能力。javafx.application包包含一个名为Application的类,该类包含与应用程序生命周期相关的方法,如launch()init()start()stop()Application类中的另一个方法是getParameters(),它允许应用程序访问命令行上传递的参数,以及 JNLP 文件中指定的未命名参数和<name,value>对。为了方便起见,下面是清单 2-1 中的相关代码片段:

StageStyle stageStyle = StageStyle.DECORATED;
List<String> unnamedParams = getParameters().getUnnamed();
if (unnamedParams.size() > 0) {
  String stageStyleParam = unnamedParams.get(0);
  if (stageStyleParam.equalsIgnoreCase("transparent")) {
    stageStyle = StageStyle.TRANSPARENT;
  }
  else if (stageStyleParam.equalsIgnoreCase("undecorated")) {
    stageStyle = StageStyle.UNDECORATED;
  }
  else if (stageStyleParam.equalsIgnoreCase("utility")) {
    stageStyle = StageStyle.UTILITY;
  }
}
...code omitted...
stage.initStyle(stageStyle);

设定舞台风格

我们使用前面描述的getParameters()方法来获得一个参数,告诉我们Stage实例的舞台样式应该是它的默认样式(StageStyle.DECORATED)、StageStyle.UNDECORATED还是StageStyle.TRANSPARENT。在前面的练习中,您已经看到了每种方法的效果,特别是在图 2-2 、 2-4 和 2-5 中。

控制阶段是否可调整大小

如清单 2-1 中的摘录所示,为了使这个应用程序的窗口最初可调整大小,我们调用了Stage实例的setResizable()方法。为了保持Stage的 resizable 属性和 resizable 复选框的状态同步,复选框被双向绑定到Stage实例的 resizable 属性。

stage.setResizable(true);
checkBoxResizable.selectedProperty()
        .bindBidirectional(stage.resizableProperty());

Tip

无法显式设置绑定的属性。在代码段之前的代码中,在下一行中绑定 resizable 属性之前,使用setResizable()方法设置该属性。

使舞台全屏显示

通过将Stage实例的fullScreen属性设置为 true,可以使Stage以全屏模式显示。如清单 2-1 中的代码片段所示,为了保持StagefullScreen属性和全屏复选框的状态同步,每当checkBox的 selected 属性改变时,就会更新Stage实例的fullScreen属性。

checkBoxFullScreen.selectedProperty().addListener((ov, oldValue, newValue) -> {
    stageRef.setFullScreen(checkBoxFullScreen.selectedProperty().getValue());
});

注意,全屏模式对某些平台没有影响。例如,在移动设备上,JavaFX 应用程序将默认为全屏模式,而 JavaFX 在移动设备上的发行版不允许非全屏选项,因为这在设备上的移动应用程序世界中没有意义。

在舞台的边界上工作

Stage的边界由它的xywidthheight属性表示,这些属性的值可以随意更改。清单 2-1 中的以下代码片段演示了这一点,其中Stage被放置在顶部附近,并且在Stage被初始化后在主屏幕上水平居中。

Rectangle2D primScreenBounds = Screen.getPrimary().getVisualBounds();
stage.setX((primScreenBounds.getWidth() - stage.getWidth()) / 2);
stage.setY((primScreenBounds.getHeight() - stage.getHeight()) / 4);

我们使用javafx.stage包的Screen类来获取主屏幕的尺寸,以便计算出所需的位置。

Note

我们有意使图 2-2 中的Stage大于其中包含的Scene以说明以下要点。Stage的宽度和高度包括其装饰(标题栏和边框),在不同的平台上有所不同。因此,通常最好控制Scene的宽度和高度(我们稍后会向您展示如何控制),并让Stage符合该尺寸。

绘制圆角矩形

正如第一章所指出的,通过指定拐角的arcWidtharcHeight,可以在Rectangle上设置圆角。清单 2-1 中的以下代码片段绘制了天蓝色圆角矩形,该矩形成为图 2-5 中透明窗口示例的背景。

Rectangle blue = new Rectangle(250, 350, Color.SKYBLUE);
blue.setArcHeight(50);
blue.setArcWidth(50);

在这个代码片段中,我们使用了三个参数的构造器Rectangle,其中前两个参数指定了Rectangle的宽度和高度。第三个参数定义了Rectangle的填充颜色。

从这段代码中可以看出,使用arcWidth(double v)arcHeight(double v)方法很容易创建圆角矩形,其中参数v定义了圆弧的直径。

标题栏不可用时在桌面上拖动舞台

可以使用标题栏在桌面上拖动Stage,但是当StageStyleUNDECORATEDTRANSPARENT时,标题栏不可用。为了允许在这种情况下拖动,我们添加了清单 2-1 中的代码片段。

//when mouse button is pressed, save the initial position of screen
rootGroup.setOnMousePressed((MouseEvent me) -> {
    dragAnchorX = me.getScreenX() - stageRef.getX();
    dragAnchorY = me.getScreenY() - stageRef.getY();
});

//when screen is dragged, translate it accordingly
rootGroup.setOnMouseDragged((MouseEvent me) -> {
    stageRef.setX(me.getScreenX() - dragAnchorX);
    stageRef.setY(me.getScreenY() - dragAnchorY);
});

事件处理程序将在本章稍后介绍,但是作为预览,当鼠标被拖动时,提供给onMouseDragged()方法的 lambda 表达式将被调用。因此,xy属性的值会根据鼠标被拖动的像素数而改变,这将随着鼠标被拖动而移动Stage

使用 UI 布局容器

当开发将在跨平台环境中部署或国际化的应用程序时,最好使用布局容器。使用布局容器的一个优点是,当节点大小改变时,它们彼此之间的可视关系是可预测的。另一个优点是,您不必计算放在 UI 中的每个节点的位置。

清单 2-1 中的以下代码片段显示了位于javafx.scene.layout包中的VBox布局类如何用于在一列中排列TextCheckBoxHBoxButton节点。这个代码片段还显示了布局容器可能是嵌套的,如名为titleBoxHBox所示,它水平排列LabelTextField节点。请注意,为了清楚地显示布局嵌套,此代码片段中省略了几行代码:

HBox titleBox = new HBox(titleLabel, titleTextField);
VBox contentBox = new VBox(
        textStageX, textStageY, textStageW, textStageH, textStageF,
        checkBoxResizable, checkBoxFullScreen,
        titleBox, toBackButton, toFrontButton, closeButton);

布局类VBox类似于第一章中 Hello Earthrise 示例中讨论的Group类,因为它包含一个节点集合。与Group类不同,VBox类垂直排列其包含的节点,按照 spacing 属性中指定的像素数来分隔它们。

确定舞台是否在焦点上

要知道您的 JavaFX 应用程序是否是当前处于焦点中的应用程序(例如,按下的键被传送到应用程序),只需查询Stage实例的focused属性。清单 2-1 中的以下片段演示了这一点。

textStageF.textProperty().bind(new SimpleStringProperty("focused: ")
        .concat(stageRef.focusedProperty().asString()));

控制舞台的 Z 顺序

如果您希望 JavaFX 应用程序出现在屏幕上其他窗口的顶部或后面,您可以分别使用toFront()toBack()方法。清单 2-1 中的以下片段展示了这是如何实现的。

Button toBackButton = new Button("toBack()");
toBackButton.setOnAction(e -> stageRef.toBack());
Button toFrontButton = new Button("toFront()");
toFrontButton.setOnAction(e -> stageRef.toFront());

再次注意使用 lambda 表达式如何增强代码的可读性。从代码片段的第一行可以清楚地看到,创建了一个名为toBackButtonButton,按钮上显示了一个文本"toBack()"。第二行定义了当在按钮上执行一个动作时(即点击按钮),stage 被发送到后面。

如果不使用 lambda 表达式,第二行将被对匿名内部类的调用所替换,如下所示:

toBackButton.setOnAction(new EventHandler<javafx.event.ActionEvent>() {
  @Override public void handle(javafx.event.ActionEvent e) {
    stageRef.toBack();
  }
})

这种方法不仅需要更多的代码,而且不允许 Java 运行时优化调用,可读性也差得多。

关闭载物台并检测其关闭时间

如清单 2-1 中的代码片段所示,您可以用它的close()方法以编程方式关闭Stage。当stageStyle未装饰或透明时,这很重要,因为窗口系统提供的关闭按钮不存在。

Button closeButton = new Button("close()");
closeButton.setOnAction(e -> stageRef.close());

顺便说一下,您可以通过使用清单 2-1 中的代码片段所示的onCloseRequest事件处理程序来检测何时有关闭Stage的外部请求。

stage.setOnCloseRequest((WindowEvent we) -> {
        System.out.println("Stage is closing");
});

要看到这一点,在没有任何参数的情况下运行应用程序,使其具有前面显示的图 2-2 的外观,然后单击窗口装饰上的关闭按钮。

Tip

只有当有外部请求关闭窗口时,才会调用onCloseRequest事件处理程序。这就是为什么当您单击标有“close()”的按钮时,本例中没有出现“Stage is closing”消息。

大吵大闹

继续我们创建 JavaFX 应用程序的剧场隐喻,我们现在讨论在Stage上放置一个Scene。如你所知,Scene是演员和道具(节点)与观众(你的节目的使用者)进行视觉互动的地方。

使用场景类:OnTheScene 示例

Stage类一样,我们将使用一个虚构的示例应用程序来演示和教授Scene类中可用功能的细节。OnTheScene 程序截图见图 2-6 。

A323806_4_En_2_Fig6_HTML.jpg

图 2-6。

The OnTheScene program when first invoked

继续运行 OnTheScene 程序,按照下面的练习中的指示测试它的速度。接下来我们将对代码进行演练,以便您可以将行为与其背后的代码关联起来。

Examining the Behavior of the Onthescene Program

当 OnTheScene 程序启动时,其外观应该类似于图 2-6 中的截图。要全面检查其行为,请执行以下步骤。请注意,UI 上的属性和方法名称对应于SceneStageCursor类中的属性和方法,以及级联样式表(CSS)文件名。

  1. 拖动应用程序,注意虽然Stage xy值是相对于屏幕的,但是Scenexy值是相对于Stage(包括装饰)外部的左上角的。同样,Scene的宽度和高度是Stage内部的尺寸(不包括装饰)。如前所述,最好显式地设置Scene的宽度和高度(或者通过假设所包含节点的大小来隐式地设置它们),而不是设置修饰过的Stage的宽度和高度。

  2. 调整程序窗口的大小,观察宽度和高度值的变化,以反映Scene的宽度和高度。另请注意,当您更改窗口的高度时,场景中大部分内容的位置也会发生变化。

  3. 单击 lookup()超链接,注意字符串“场景高度:XXX。x "打印在控制台中,其中 XXX。x 是Scene的高度。

  4. 将鼠标悬停在选择框下拉列表上,注意它会变得稍微大一些。单击选择框并在列表中选择一种光标样式,注意光标会变为该样式。选择“无”时要小心,因为光标可能会消失,你需要使用键盘(或移动鼠标时的心灵力量)来使其可见。

  5. 拖动左侧的滑块,注意到Scene的填充颜色发生了变化,并且Scene顶部的字符串反映了当前填充颜色的红绿蓝(RGB)和不透明度值。

  6. Notice the appearance and content of the text on the Scene. Then click changeOfScene.css, noticing that the color and font and content characteristics for some of the text on the Scene changes as shown in the screenshot in Figure 2-7.

    A323806_4_En_2_Fig7_HTML.jpg

    图 2-7。

    The OnTheScene program with the changeOfScene CSS style sheet applied

  7. 单击 OnTheScene.css,注意颜色和字体特征恢复到它们以前的状态。

既然您已经研究了这个演示了Scene特性的示例程序,那么让我们浏览一下代码吧!

了解 OnTheScene 程序

在我们指出新的和相关的概念之前,先看看清单 2-2 中的 OnTheScene 程序的代码。

import javafx.application.Application;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.geometry.VPos;
import javafx.scene.Cursor;
import javafx.scene.Scene;
import javafx.scene.control.ChoiceBox;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.Label;
import javafx.scene.control.RadioButton;
import javafx.scene.control.Slider;
import javafx.scene.control.ToggleGroup;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.scene.text.Text;
import javafx.stage.Stage;

public class OnTheSceneMain extends Application {

    DoubleProperty fillVals = new SimpleDoubleProperty(255.0);

    Scene sceneRef;

    ObservableList cursors = FXCollections.observableArrayList(
            Cursor.DEFAULT,
            Cursor.CROSSHAIR,
            Cursor.WAIT,
            Cursor.TEXT,
            Cursor.HAND,
            Cursor.MOVE,
            Cursor.N_RESIZE,
            Cursor.NE_RESIZE,
            Cursor.E_RESIZE,
            Cursor.SE_RESIZE,
            Cursor.S_RESIZE,
            Cursor.SW_RESIZE,
            Cursor.W_RESIZE,
            Cursor.NW_RESIZE,
            Cursor.NONE
    );

    public static void main(String[] args) {
        Application.launch(args);
    }

    @Override
    public void start(Stage stage) {
        Slider sliderRef;
        ChoiceBox choiceBoxRef;
        Text textSceneX;
        Text textSceneY;
        Text textSceneW;
        Text textSceneH;
        Label labelStageX;
        Label labelStageY;
        Label labelStageW;
        Label labelStageH;

        final ToggleGroup toggleGrp = new ToggleGroup();
        sliderRef = new Slider(0, 255, 255);
        sliderRef.setOrientation(Orientation.VERTICAL);
        choiceBoxRef = new ChoiceBox(cursors);
        HBox hbox = new HBox(sliderRef, choiceBoxRef);
        hbox.setSpacing(10);
        textSceneX = new Text();
        textSceneX.getStyleClass().add("emphasized-text");
        textSceneY = new Text();
        textSceneY.getStyleClass().add("emphasized-text");
        textSceneW = new Text();
        textSceneW.getStyleClass().add("emphasized-text");
        textSceneH = new Text();
        textSceneH.getStyleClass().add("emphasized-text");
        textSceneH.setId("sceneHeightText");
        Hyperlink hyperlink = new Hyperlink("lookup");
        hyperlink.setOnAction((javafx.event.ActionEvent e) -> {
            System.out.println("sceneRef:" + sceneRef);
            Text textRef = (Text) sceneRef.lookup("#sceneHeightText");
            System.out.println(textRef.getText());
        });
        RadioButton radio1 = new RadioButton("onTheScene.css");
        radio1.setSelected(true);
        radio1.setToggleGroup(toggleGrp);
        RadioButton radio2 = new RadioButton("changeOfScene.css");
        radio2.setToggleGroup(toggleGrp);
        labelStageX = new Label();
        labelStageX.setId("stageX");
        labelStageY = new Label();
        labelStageY.setId("stageY");
        labelStageW = new Label();
        labelStageH = new Label();

        FlowPane sceneRoot = new FlowPane(Orientation.VERTICAL, 20, 10, hbox,
                textSceneX, textSceneY, textSceneW, textSceneH, hyperlink,
                radio1, radio2,
                labelStageX, labelStageY,
                labelStageW,
                labelStageH);
        sceneRoot.setPadding(new Insets(0, 20, 40, 0));
        sceneRoot.setColumnHalignment(HPos.LEFT);
        sceneRoot.setLayoutX(20);
        sceneRoot.setLayoutY(40);

        sceneRef = new Scene(sceneRoot, 600, 250);

        sceneRef.getStylesheets().add("onTheScene.css");
        stage.setScene(sceneRef);

        choiceBoxRef.getSelectionModel().selectFirst();

        // Setup various property binding
        textSceneX.textProperty().bind(new SimpleStringProperty("Scene x: ")
                .concat(sceneRef.xProperty().asString()));
        textSceneY.textProperty().bind(new SimpleStringProperty("Scene y: ")
                .concat(sceneRef.yProperty().asString()));
        textSceneW.textProperty().bind(new SimpleStringProperty("Scene width: ")
                .concat(sceneRef.widthProperty().asString()));
        textSceneH.textProperty().bind(new SimpleStringProperty("Scene height: ")
                .concat(sceneRef.heightProperty().asString()));
        labelStageX.textProperty().bind(new SimpleStringProperty("Stage x: ")
                .concat(sceneRef.getWindow().xProperty().asString()));
        labelStageY.textProperty().bind(new SimpleStringProperty("Stage y: ")
                .concat(sceneRef.getWindow().yProperty().asString()));
        labelStageW.textProperty().bind(new SimpleStringProperty("Stage width: ")
                .concat(sceneRef.getWindow().widthProperty().asString()));
        labelStageH.textProperty().bind(new SimpleStringProperty("Stage height: ")
                .concat(sceneRef.getWindow().heightProperty().asString()));
        sceneRef.cursorProperty().bind(choiceBoxRef.getSelectionModel()
                .selectedItemProperty());
        fillVals.bind(sliderRef.valueProperty());

        // When fillVals changes, use that value as the RGB to fill the scene
        fillVals.addListener((ov, oldValue, newValue) -> {
            Double fillValue = fillVals.getValue() / 256.0;
            sceneRef.setFill(new Color(fillValue, fillValue, fillValue, 1.0));
        });

        // When the selected radio button changes, set the appropriate style sheet
        toggleGrp.selectedToggleProperty().addListener((ov, oldValue, newValue) -> {
            String radioButtonText = ((RadioButton) toggleGrp.getSelectedToggle())
                    .getText();
            sceneRef.getStylesheets().clear();
            sceneRef.getStylesheets().addAll(radioButtonText);
        });

        stage.setTitle("On the Scene");
        stage.show();

        // Define an unmanaged node that will display Text
        Text addedTextRef = new Text(0, -30, "");
        addedTextRef.setTextOrigin(VPos.TOP);
        addedTextRef.setFill(Color.BLUE);
        addedTextRef.setFont(Font.font("Sans Serif", FontWeight.BOLD, 16));
        addedTextRef.setManaged(false);

        // Bind the text of the added Text node to the fill property of the Scene
        addedTextRef.textProperty().bind(new SimpleStringProperty("Scene fill: ").
                concat(sceneRef.fillProperty()));

        // Add to the Text node to the FlowPane
        ((FlowPane) sceneRef.getRoot()).getChildren().add(addedTextRef);
    }
}

Listing 2-2.
OnTheSceneMain.java

为场景设置光标

可以为给定节点、整个场景或两者设置光标。要实现后者,将Scene实例的 cursor 属性设置为Cursor类中的一个常量值,如清单 2-2 中的代码片段所示。

sceneRef.cursorProperty().bind(choiceBoxRef.getSelectionModel()
        .selectedItemProperty());

通过查看 JavaFX API 文档中的javafx.scene.Cursor类可以看到这些光标值;我们在清单 2-2 中创建了这些常量的集合。

绘制场景的背景

Scene类有一个javafx.scene.paint .Paint类型的填充属性。查看 JavaFX API 会发现Paint的已知子类是ColorImagePatternLinearGradientRadialGradient。因此,Scene的背景可以填充纯色、图案和渐变。如果不设置Scene的 fill 属性,将使用默认颜色(白色)。

Tip

其中一个Color常量是Color.TRANSPARENT,所以如果需要,你可以让Scene的背景完全透明。事实上,图 2-5 中 StageCoach 截图中圆角矩形后面的Scene不是白色的原因是它的 fill 属性被设置为Color.TRANSPARENT(见清单 2-1 )。

为了在 OnTheScene 示例中设置 fill 属性,我们使用 RGB 公式来创建颜色,而不是使用Color类中的常量之一(例如Color.BLUE)。查看 JavaFX API 文档中的javafx.scene.paint.Color类,向下滚动常量,如ALICEBLUEWHITESMOKE,查看构造器和方法。我们使用了一个Color类的构造器,为它设置了 fill 属性,如清单 2-2 中的代码片段所示。

sceneRef.setFill(new Color(fillValue, fillValue, fillValue, 1.0));

当您移动绑定了fillVals属性的滑块时,Color()构造器的每个参数都被设置为一个从 0 到 255 的值,如清单 2-2 中的代码片段所示。

fillVals.bind(sliderRef.valueProperty());

用节点普及场景

如第一章所述,您可以通过实例化节点并将它们添加到可以包含其他节点的容器节点(例如GroupVBox)来填充节点Scene。这些功能使您能够构建包含节点的复杂场景图。在这里的例子中,Scene的根属性包含一个Flow布局容器,这使得它的内容垂直或水平流动,根据需要换行。我们示例中的Flow容器包含一个HBox(包含一个Slider和一个ChoiceBox)和几个其他节点(类TextHyperlinkRadioButton的实例)。

通过 ID 查找场景节点

可以在节点的id属性中为Scene中的每个节点分配一个 ID。例如,在清单 2-2 的以下代码片段中,Text节点的id属性被赋予了String "sceneHeightText"。当调用超链接控件中的 action 事件处理程序时,使用Scene实例的lookup()方法获取对id"sceneHeightText"的节点的引用。然后,事件处理程序将Text节点的内容打印到控制台。

Note

超链接控件本质上是一个具有超链接文本外观的按钮。它有一个动作事件处理程序,您可以在其中放置打开浏览器页面或任何其他所需功能的代码。

textSceneH = new Text();
textSceneH.getStyleClass().add("emphasized-text");
textSceneH.setId("sceneHeightText");
Hyperlink hyperlink = new Hyperlink("lookup");
hyperlink.setOnAction((javafx.event.ActionEvent e) -> {
    System.out.println("sceneRef:" + sceneRef);
    Text textRef = (Text) sceneRef.lookup("#sceneHeightText");
    System.out.println(textRef.getText());
});

仔细检查动作事件处理程序可以发现,lookup()方法返回了一个Node,但是这个代码片段中返回的实际对象类型是一个Text对象。因为我们需要访问不在Node类中的Text类(文本)的属性,所以有必要强迫编译器相信在运行时该对象将是Text类的实例。

从现场进入舞台

为了从Scene中获得对Stage实例的引用,我们使用了Scene类中名为window的属性。该属性的访问器方法出现在清单 2-2 的以下代码片段中,用于获取屏幕上Stage的 x 和 y 坐标。

labelStageX.textProperty().bind(new SimpleStringProperty("Stage x: ")
        .concat(sceneRef.getWindow().xProperty().asString()));
labelStageY.textProperty().bind(new SimpleStringProperty("Stage y: ")
        .concat(sceneRef.getWindow().yProperty().asString()));

将节点插入场景的内容序列

有时向 UI 容器类的子类动态添加一个节点是很有用的。下面清单 2-2 中的代码片段演示了如何通过向FlowPane实例的子实例动态添加一个Text节点来实现这一点:

// Define an unmanaged node that will display Text
Text addedTextRef = new Text(0, -30, "");
addedTextRef.setTextOrigin(VPos.TOP);
addedTextRef.setFill(Color.BLUE);
addedTextRef.setFont(Font.font("Sans Serif", FontWeight.BOLD, 16));
addedTextRef.setManaged(false);

// Bind the text of the added Text node to the fill property of the Scene
addedTextRef.textProperty().bind(new SimpleStringProperty("Scene fill: ").
        concat(sceneRef.fillProperty()));

// Add the Text node to the FlowPane
((FlowPane) sceneRef.getRoot()).getChildren().add(addedTextRef);

这个特定的Text节点是图 2-6 和 2-7 中所示的Scene顶部的节点,其中显示了Scene的 fill 属性的值。注意,在这个例子中,addedTextRef实例的managed属性被设置为 false,所以它的位置不受FlowPane的控制。默认情况下,节点是“托管的”,这意味着它们的父节点(该节点添加到的容器)负责节点的布局。通过将managed属性设置为 false,假设开发人员负责布局节点。

CSS 样式化场景中的节点

JavaFX 的一个非常强大的方面是能够使用 CSS 动态地样式化Scene中的节点。在上一个练习的步骤 6 中,当您单击 changeOfScene.css 将 UI 的外观从图 2-6 中看到的更改为图 2-7 中显示的时,您使用了该功能。此外,在本练习的第 7 步中,当您选择 onTheScene.css 单选按钮时,UI 的外观变回图 2-6 所示的样子。清单 2-2 中的相关代码片段如下所示:

sceneRef.getStylesheets().add("onTheScene.css");
...code omitted...
// When the selected radio button changes, set the appropriate stylesheet
        toggleGrp.selectedToggleProperty().addListener((ov, oldValue, newValue) -> {
        String radioButtonText = ((RadioButton) toggleGrp.getSelectedToggle())
                .getText();
        sceneRef.getStylesheets().clear();
        sceneRef.getStylesheets().addAll("/"+radioButtonText);
});

在这个代码片段中,Scenestylesheets属性被初始化为onTheScene.css文件的位置,在本例中是根目录。片段中还显示了当点击适当的按钮时,CSS 文件被分配给SceneRadioButton实例的文本等于样式表的名称,因此我们可以很容易地为场景设置相应的样式表。查看清单 2-3 以查看与图 2-6 中的截图相对应的样式表。这个样式表中的一些 CSS 选择器表示其id属性为"stageX""stageY"的节点。这个样式表中还有一个选择器,它表示styleClass属性为"emphasized-text"的节点。此外,在这个样式表中有一个选择器,它通过将控件的驼色名称替换为小写连字符名称(选择框)来映射到 ChoiceBox UI 控件。该样式表中的属性以“-fx”开头,并对应于它们所关联的节点类型。该样式表中的值(例如,黑色、斜体和 14pt)表示为标准 CSS 值。

#stageX, #stageY {
  -fx-padding: 1;
  -fx-border-color: black;
  -fx-border-style: dashed;
  -fx-border-width: 2;
  -fx-border-radius: 5;
}

.emphasized-text {
  -fx-font-size: 14pt;
  -fx-font-weight: normal;
  -fx-font-style: italic;
}

.choice-box:hover {
    -fx-scale-x: 1.1;
    -fx-scale-y: 1.1;
}

.radio-button .radio  {
   -fx-background-color: -fx-shadow-highlight-color, -fx-outer-border,
                         -fx-inner-border, -fx-body-color;
   -fx-background-insets: 0 0 -1 0,  0,  1,  2;
   -fx-background-radius: 1.0em;
   -fx-padding: 0.333333em;
}

.radio-button:focused .radio {
    -fx-background-color: -fx-focus-color, -fx-outer-border,
                          -fx-inner-border, -fx-body-color;
    -fx-background-radius: 1.0em;
    -fx-background-insets: -1.4, 0, 1, 2;
}

Listing 2-3.
onTheScene.css

清单 2-4 是图 2-7 中截图对应的样式表。有关 CSS 样式表的更多信息,请参阅本章末尾的“参考资料”部分。

#stageX, #stageY {
  -fx-padding: 3;
  -fx-border-color: blue;
  -fx-stroke-dash-array: 12 2 4 2;
  -fx-border-width: 4;
  -fx-border-radius: 5;
}

.emphasized-text {
  -fx-font-size: 14pt;
  -fx-font-weight: bold;
  -fx-font-style: normal;
}

.radio-button *.radio  {
    -fx-padding: 10;
    -fx-background-color: red, yellow;
    -fx-background-insets: 0, 5;
    -fx-background-radius: 30, 20;
}

.radio-button:focused *.radio {
    -fx-background-color: blue, red, yellow;
    -fx-background-insets: -5, 0, 5;
    -fx-background-radius: 40, 30, 20;
}

Listing 2-4.

changeOfScene.css

现在,您已经有了一些使用StageScene类、几个Node子类和 CSS 样式的经验,我们将向您展示如何处理 JavaFX 程序运行时可能发生的事件。

处理输入事件

到目前为止,我们已经展示了几个事件处理的例子。例如,我们使用onAction事件处理程序在点击按钮时执行代码。我们还使用了Stage类的onCloseRequest事件处理程序,在外部请求Stage关闭时执行代码。在本节中,我们将探索 JavaFX 中更多可用的事件处理程序。

调查鼠标、键盘、触摸和手势事件和处理程序

JavaFX 程序中发生的大多数事件都与用户操作输入设备(如鼠标、键盘或多点触摸屏)有关。为了查看可用的事件处理程序及其关联的事件对象,我们再看一下 JavaFX API 文档。首先,导航到javafx.scene.Node类,查找以字母“on”开头的属性。这些属性表示 JavaFX 中所有节点通用的事件处理程序。以下是 JavaFX 8 API 中这些事件处理程序的列表:

  • 关键事件处理程序:onKeyPressedonKeyReleasedonKeyTyped
  • 鼠标事件处理程序:onMouseClickedonMouseDragEnteredonMouseDragExitedonMouseDraggedonMouseDragOveronMouseDragReleasedonMouseEnteredonMouseExitedonMouseMovedonMousePressedonMouseReleased
  • 拖放处理程序:onDragDetectedonDragDoneonDragDroppedonDragEnteredonDragExitedonDragOver
  • 触摸处理者:onTouchMovedonTouchPressedonTouchReleasedonTouchStationary
  • 手势处理程序:onRotateonRotationFinishedonRotationStartedonScrollonScrollStartedonScrollFinishedonSwipeLeftonSwipeRightonSwipeUponSwipeDownonZoomonZoomStartedonZoomFinished

其中的每一个都是一个属性,它定义了当特定的输入事件发生时要调用的方法。对于关键事件处理程序,如 JavaFX API 文档所示,该方法的参数是一个javafx.scene.input.KeyEvent实例。鼠标事件处理程序的方法参数是一个javafx.scene.input.MouseEvent。Touch handlers 消耗一个javafx.scene.input.TouchEvent实例,当一个手势事件发生时,handle 事件的方法参数是一个javax.scene.input.GestureInput实例。

了解 KeyEvent 类

查看一下KeyEvent类的 JavaFX API 文档,您会看到它包含了几个方法,其中一个常用的是getCode()getCode()方法返回一个KeyCode实例,代表按下时导致事件的按键。查看 JavaFX API 文档中的javafx.scene.input.KeyCode类可以发现,存在大量的常量来表示国际键盘上的按键。另一种确定按下了哪个键的方法是调用getCharacter()方法,该方法返回一个字符串,该字符串表示与按下的键相关联的 Unicode 字符。

通过分别调用isAltDown()isControlDown()isMetaDown()isShiftDown()方法,KeyEvent类还使您能够查看 Alt、Ctrl、Meta 和/或 Shift 键在事件发生时是否被按下。

了解 MouseEvent 类

看一看 JavaFX API 文档中的MouseEvent类,您会看到比KeyEvent中可用的方法多得多。和KeyEvent一样,MouseEvent也有isAltDown()isControlDown()isMetaDown()isShiftDown()方法,以及 source 字段,它是对事件起源的对象的引用。此外,它有几个方法可以精确定位鼠标事件发生的各个坐标空间,所有坐标空间都以像素表示:

  • getX()getY()返回鼠标事件相对于发生鼠标事件的节点原点的水平和垂直位置。
  • getSceneX()getSceneY()返回鼠标事件相对于Scene的水平和垂直位置。
  • getScreenX()getScreenY()返回鼠标事件相对于屏幕的水平和垂直位置。

以下是其他一些常用的方法:

  • 如果检测到拖动事件,返回 true。
  • getButton()isPrimaryButtonDown()isSecondaryButtonDown()isMiddleButtonDown()getClickCount()包含关于点击了什么按钮以及点击了多少次的信息。

在本章的稍后部分,你将获得一些在 ZenPong 示例程序中创建按键和鼠标事件处理程序的经验。为了继续为 ZenPong 的例子做准备,我们现在给你看一下如何为场景中的节点制作动画。

了解 TouchEvent 类

随着越来越多的设备配备了触摸屏,对触摸事件的内置支持使 JavaFX 成为创建利用多点触摸功能的应用程序的一流平台,这意味着该平台能够在一组事件中跟踪多个触摸点。

TouchEvent类提供了getTouchPoint()方法,该方法返回一个特定的触摸点。这个TouchPoint上的方法类似于MouseEvent上的方法,比如可以通过调用getX()getY(),或者getSceneX()getSceneY(),或者getScreenX()getScreenY()来检索相对和绝对位置。

TouchEvent类还允许开发人员获得属于同一组的其他接触点的信息。通过调用getEventSetId(),得到TouchEvent实例集合的唯一标识符,通过调用getTouchPoints(),可以得到集合中所有接触点的列表,?? 返回一个TouchPoint实例的列表。

了解 GestureEvent 类

除了处理多点触摸事件,JavaFX 还支持手势事件的创建和调度。手势越来越多地用于智能手机、平板电脑、触摸屏和其他输入设备。它们提供了一种执行操作的直观方式,例如,让用户滑动他或她的手指。GestureEvent类目前有四个子类,每个子类代表一个特定的手势:RotateEventScrollEventSwipeEventZoomEvent。所有这些事件都有类似于检索动作位置的MouseEvent的方法——getX()getY()getSceneX()getSceneY()以及getScreenX()getScreenY()方法。

特定的子类都允许检索更详细的事件类型。例如,SwipeEvent可以是向右或向左、向上或向下的滑动。这个信息是通过调用GestureEvent上的getEventType()方法获得的。

在场景中设置节点动画

JavaFX 的优势之一是可以轻松地创建图形丰富的 ui。这种丰富性的一部分是能够动画显示位于Scene中的节点。在其核心,动画节点涉及改变其属性值在一段时间内。制作节点动画的示例包括。

  • 当鼠标进入其边界时逐渐增大节点的大小,当鼠标退出其边界时逐渐减小节点的大小。请注意,这需要缩放节点,这被称为变换。
  • 逐渐增加或减少节点的不透明度,以分别提供淡入或淡出效果。
  • 逐渐改变节点中改变其位置的属性值,使其从一个位置移动到另一个位置。例如,这在创建诸如 Pong 之类的游戏时非常有用。一个相关的功能是检测一个节点何时与另一个节点发生冲突。

制作节点动画需要使用位于javafx.animation包中的Timeline类。根据动画的要求和个人偏好,使用两种通用技术之一:

  • 直接创建一个Timeline类的实例,并提供在特定时间点指定值和动作的关键帧。
  • 使用javafx.animation.Transition子类来定义特定的过渡并将其与节点相关联。转换的示例包括使节点在一段时间内沿着定义的路径移动,以及在一段时间内旋转节点。这些转换类中的每一个都扩展了Timeline类。

我们现在介绍这些技术,展示每种技术的例子,从列出的第一种开始。

为动画使用时间轴

看看 JavaFX API 文档中的javafx.animation包,您会看到直接创建时间线时使用的三个类:TimelineKeyFrameInterpolator。仔细阅读这些类的文档,然后回来,这样我们可以向您展示一些使用它们的例子。

Tip

对于您遇到的任何新的包、类、属性和方法,请记住查阅 JavaFX API 文档。

节拍器 1 示例

我们使用一个简单的节拍器示例来演示如何创建时间轴。

如图 2-8 中的截图所示,Metronome1 程序有一个钟摆和四个按钮,用于开始、暂停、恢复和停止动画。本例中的钟摆是一个Line节点,我们将通过在一秒钟的时间内插入其startX属性来激活该节点。继续做下面的练习,并以这个例子为例。

A323806_4_En_2_Fig8_HTML.jpg

图 2-8。

The Metronome1 program Examining the Behavior of the Metronome1 Program

当 Metronome1 程序启动时,其外观应该类似于图 2-8 中的截图。要全面检查其行为,请执行以下步骤。

  1. 注意,在场景中的四个按钮中,只有 Start 按钮处于启用状态。
  2. 单击开始。请注意,线的顶端来回移动,每个方向移动一秒钟。此外,请注意,Start 和 Resume 按钮被禁用,Pause 和 Stop 按钮被启用。
  3. 单击暂停,注意动画暂停。此外,请注意启动和暂停按钮被禁用,恢复和停止按钮被启用。
  4. 单击“继续”,注意动画会从暂停的地方继续播放。
  5. 单击 Stop,注意动画停止,按钮状态与程序第一次启动时相同(参见步骤 1)。
  6. 再次单击 Start,注意该行在开始动画之前跳回到它的起始点(而不是像在步骤 4 中那样简单地恢复)。
  7. 单击停止。

现在,您已经体验了 Metronome1 程序的行为,让我们浏览一下它背后的代码。

了解 Metronome1 程序

在我们讨论相关概念之前,先看看清单 2-5 中的 Metronome1 程序的代码。

package projavafx.metronome1.ui;

import javafx.animation.Animation;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Line;
import javafx.stage.Stage;
import javafx.util.Duration;

public class Metronome1Main extends Application {

    DoubleProperty startXVal = new SimpleDoubleProperty(100.0);

    Button startButton;
    Button pauseButton;
    Button resumeButton;
    Button stopButton;
    Line line;
    Timeline anim;

    public static void main(String[] args) {
        Application.launch(args);
    }

    @Override
    public void start(Stage stage) {
        anim = new Timeline(
                new KeyFrame(new Duration(0.0), new KeyValue(startXVal, 100.)),
                new KeyFrame(new Duration(1000.0), new KeyValue(startXVal, 300., Interpolator.LINEAR))
        );
        anim.setAutoReverse(true);
        anim.setCycleCount(Animation.INDEFINITE);
        line = new Line(0, 50, 200, 400);
        line.setStrokeWidth(4);
        line.setStroke(Color.BLUE);
        startButton = new Button("start");
        startButton.setOnAction(e -> anim.playFromStart());
        pauseButton = new Button("pause");
        pauseButton.setOnAction(e -> anim.pause());
        resumeButton = new Button("resume");
        resumeButton.setOnAction(e -> anim.play());
        stopButton = new Button("stop");
        stopButton.setOnAction(e -> anim.stop());
        HBox commands = new HBox(10,
                startButton,
                pauseButton,
                resumeButton,
                stopButton);
        commands.setLayoutX(60);
        commands.setLayoutY(420);
        Group group = new Group(line, commands);
        Scene scene = new Scene(group, 400, 500);

        line.startXProperty().bind(startXVal);
        startButton.disableProperty().bind(anim.statusProperty()
                .isNotEqualTo(Animation.Status.STOPPED));
        pauseButton.disableProperty().bind(anim.statusProperty()
                .isNotEqualTo(Animation.Status.RUNNING));
        resumeButton.disableProperty().bind(anim.statusProperty()
                .isNotEqualTo(Animation.Status.PAUSED));
        stopButton.disableProperty().bind(anim.statusProperty()
                .isEqualTo(Animation.Status.STOPPED));

        stage.setScene(scene);
        stage.setTitle("Metronome 1");
        stage.show();
    }
}

Listing 2-5.
Metronome1Main.java

了解时间轴类

Timeline类的主要目的是提供在给定的时间段内以渐进的方式改变属性值的能力。请看清单 2-5 中的以下代码片段,看看时间轴是如何创建的,以及它的一些常用属性。

DoubleProperty startXVal = new SimpleDoubleProperty(100.0);

  ...code omitted...

Timeline anim = new Timeline(
                new KeyFrame(new Duration(0.0), new KeyValue(startXVal, 100.)),
                new KeyFrame(new Duration(1000.0), new KeyValue(startXVal, 300., Interpolator.LINEAR))
        );
anim.setAutoReverse(true);
        anim.setCycleCount(Animation.INDEFINITE);

  ...code omitted...

line = new Line(0, 50, 200, 400);
        line.setStrokeWidth(4);
        line.setStroke(Color.BLUE);

  ...code omitted...

    line.startXProperty().bind(startXVal);

Note

在 JavaFX 2 中,建议使用 builder 模式来创建Nodes。因此,创建一个Line将按如下方式完成:

line = LineBuilder.create()
.startY(50)
.endX(200)
.endY(400)
.strokeWidth(4)
.stroke(Color.BLUE)
.build();

这种方法的优点是很清楚第二行中的参数“50”是什么意思:该行在垂直位置的起始坐标是 50。同样的可读性可以通过调用 setter 方法来实现,例如

line.setStartY(50);

然而,在实践中,许多参数是通过Node的构造器传递的。对于一个Line实例,第二个参数是startY参数。这种方法减少了代码行,但是开发人员应该注意构造器中参数的顺序和含义。我们再次强烈建议在编写 JavaFX 应用程序时使用 Javadoc。

将关键帧插入时间轴

我们的时间轴包含两个KeyFrame实例的集合。使用KeyValue构造器,其中一个实例在时间轴开始时将 100 赋给startXVal属性,另一个实例在时间轴运行一秒后将 300 赋给startXVal属性。因为LinestartX属性被绑定到startXVal属性的值,所以最终结果是该行的顶部在一秒钟内水平移动了 200 个像素。

在时间轴的第二个KeyFrame中,KeyValue构造器被传递了第三个参数,该参数指定从 100 到 300 的插值将在一秒的持续时间内以线性方式发生。其他Interpolation常量包括EASE_INEASE_OUTEASE_BOTH。这些分别导致KeyFrame中的插值在开始、结束或两者都较慢。

下面是本例中使用的从Animation类继承的其他Timeline属性:

  • 我们将其初始化为真。这会导致时间轴在到达最后一个KeyFrame时自动反转。反向时,插值在一秒钟内从 300 到 100。
  • cycleCount,我们将其初始化为Animation.INDEFINITE。这导致时间轴无限重复,直到被Timeline类的stop()方法停止。

说到Timeline类的方法,现在是向您展示如何控制时间轴并监控其状态的好时机。

控制和监控时间线

正如您在使用 Metronome1 程序时观察到的,单击按钮会导致动画开始、暂停、恢复和停止。这进而会影响动画的状态(运行、暂停或停止)。这些状态以启用或禁用的形式反映在按钮中。清单 2-5 中的以下片段显示了如何开始、暂停、恢复和停止时间线,以及如何判断时间线是正在运行还是暂停。

startButton = new Button("start");
startButton.setOnAction(e -> anim.playFromStart());
pauseButton = new Button("pause");
pauseButton.setOnAction(e -> anim.pause());
resumeButton = new Button("resume");
resumeButton.setOnAction(e -> anim.play());
stopButton = new Button("stop");
stopButton.setOnAction(e -> anim.stop());

...code omitted...

startButton.disableProperty().bind(anim.statusProperty()
        .isNotEqualTo(Animation.Status.STOPPED));
pauseButton.disableProperty().bind(anim.statusProperty()
        .isNotEqualTo(Animation.Status.RUNNING));
resumeButton.disableProperty().bind(anim.statusProperty()
        .isNotEqualTo(Animation.Status.PAUSED));
stopButton.disableProperty().bind(anim.statusProperty()
        .isEqualTo(Animation.Status.STOPPED));

如开始按钮的动作事件处理程序所示,调用了Timeline实例的playFrom Start()方法,该方法从头开始播放时间轴。此外,那个Buttondisable属性被绑定到一个表达式,该表达式评估时间线的状态属性是否不等于Animation.Status.STOPPED。这将导致按钮在时间线未停止时被禁用(在这种情况下,时间线必须处于运行或暂停状态)。

当用户单击暂停按钮时,动作事件处理程序调用时间轴的pause()方法,暂停动画。那个Buttondisable属性被绑定到一个表达式,该表达式评估时间轴是否没有运行。

仅当时间线未暂停时,“继续”按钮才会被禁用。为了从暂停的地方恢复时间轴,动作事件处理程序调用时间轴的play()方法。

最后,当时间轴停止时,“停止”按钮被禁用。为了停止时间轴,动作事件处理程序调用时间轴的stop()方法。

既然您已经知道了如何通过创建Timeline类和KeyFrame实例来制作节点动画,那么是时候学习如何使用过渡类来制作节点动画了。

为动画使用过渡类

使用TimeLine允许非常灵活的动画。JavaFX 支持许多现成的通用动画,有助于从一种状态转换到另一种状态。javafx.animation包包含几个类,它们的目的是提供方便的方法来完成这些常用的动画任务。TimeLineTransition(所有具体转换的抽象根类)都扩展了Animation类。

表 2-1 包含该包中的过渡类列表。

表 2-1。

Transition Classes in the javafx.animation Package for Animating Nodes

| 过渡类名 | 描述 | | --- | --- | | `TranslateTransition` | 在给定时间段内将节点从一个位置平移(移动)到另一个位置。这在第一章的 Hello Earthrise 示例程序中使用。 | | `PathTransition` | 沿指定路径移动节点。 | | `RotateTransition` | 在给定时间段内旋转节点。 | | `ScaleTransition` | 在给定时间段内缩放(增大或减小)节点。 | | `FadeTransition` | 在给定时间段内淡化(增加或减少不透明度)节点。 | | `FillTransition` | 在给定时间内更改形状的填充。 | | `StrokeTransition` | 在给定时间段内更改形状的笔触颜色。 | | `PauseTransition` | 在动作持续时间结束时执行动作;主要设计用于`SequentialTransition`中,作为等待一段时间的手段。 | | `SequentialTransition` | 允许您定义一系列按顺序执行的转换。 | | `ParallelTransition` | 允许您定义一系列并行执行的转换。 |

让我们来看看节拍器主题的一个变体,其中我们使用TranslateTransition为动画创建了一个节拍器。

节拍器过渡示例

当使用过渡类时,我们对动画采取了与直接使用Timeline类不同的方法:

  • 在基于时间轴的 Metronome1 程序中,我们将一个节点的属性(具体来说就是,startX)绑定到模型中的属性(startXVal),然后使用时间轴在模型中插入属性的值。
  • 然而,当使用转换类时,我们给Transition子类的属性赋值,其中一个是节点。最终结果是节点本身受到影响,而不仅仅是节点的绑定属性受到影响。

当我们浏览 MetronomeTransition 示例时,这两种方法之间的区别就变得很明显了。图 2-9 显示了该程序第一次被调用时的屏幕截图。

A323806_4_En_2_Fig9_HTML.jpg

图 2-9。

The MetronomeTransition program

这个例子和上一个(Metronome1)例子的第一个显著区别是,我们将让一个Circle节点来回移动,而不是让一行的一端来回移动。

节拍器过渡程序的行为

继续运行程序,并使用 Metronome1 执行与上一个练习中相同的步骤。除了前面指出的视觉差异之外,所有东西都应该功能相同。

了解节拍器转换程序

在我们指出相关概念之前,先看一下清单 2-6 中节拍器转换程序的代码。

package projavafx.metronometransition.ui;

import javafx.animation.Animation;
import javafx.animation.Interpolator;
import javafx.animation.TranslateTransition;
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
import javafx.util.Duration;

public class MetronomeTransitionMain extends Application {

    Button startButton;
    Button pauseButton;
    Button resumeButton;
    Button stopButton;
    Circle circle;

    public static void main(String[] args) {
        Application.launch(args);
    }

    @Override
    public void start(Stage stage) {
        circle = new Circle(100, 50, 4, Color.BLUE);
        TranslateTransition anim = new TranslateTransition(new Duration(1000.0), circle);
        anim.setFromX(0);
        anim.setToX(200);
        anim.setAutoReverse(true);
        anim.setCycleCount(Animation.INDEFINITE);
        anim.setInterpolator(Interpolator.LINEAR);
        startButton = new Button("start");
        startButton.setOnAction(e -> anim.playFromStart());
        pauseButton = new Button("pause");
        pauseButton.setOnAction(e -> anim.pause());
        resumeButton = new Button("resume");
        resumeButton.setOnAction(e -> anim.play());
        stopButton = new Button("stop");
        stopButton.setOnAction(e -> anim.stop());
        HBox commands = new HBox(10, startButton,
                pauseButton,
                resumeButton,
                stopButton);
        commands.setLayoutX(60);
        commands.setLayoutY(420);
        Group group = new Group(circle, commands);
        Scene scene = new Scene(group, 400, 500);
        startButton.disableProperty().bind(anim.statusProperty()
                .isNotEqualTo(Animation.Status.STOPPED));
        pauseButton.disableProperty().bind(anim.statusProperty()
                .isNotEqualTo(Animation.Status.RUNNING));
        resumeButton.disableProperty().bind(anim.statusProperty()
                .isNotEqualTo(Animation.Status.PAUSED));
        stopButton.disableProperty().bind(anim.statusProperty()
                .isEqualTo(Animation.Status.STOPPED));

        stage.setScene(scene);
        stage.setTitle("Metronome using TranslateTransition");
        stage.show();
    }
}

Listing 2-6.

MetronomeTransitionMain.fx

使用 translalaternsition 类

如清单 2-6 中的代码片段所示,为了创建一个TranslateTransition,我们提供了一些值,这些值让人想起我们在前面的例子中创建时间轴时使用的值。例如,我们将autoReverse设置为真,将cycleCount设置为Animation.INDEFINITE。同样,就像为时间轴创建KeyFrame一样,我们在这里也提供了持续时间和插值类型。

此外,我们为特定于某个TranslateTransition的属性提供一些值,即fromXtoX。在请求的持续时间内对这些值进行插值,并将其分配给由过渡控制的节点的layoutX属性(在本例中为圆形)。如果我们还想引起垂直移动,给fromYtoY赋值会导致它们之间的插值被赋给layoutY属性。

提供toXtoY值的另一种方法是向byXbyY属性提供值,这使您能够指定在每个方向上行进的距离,而不是起点和终点。此外,如果不为fromX提供值,插值将从节点的layoutX属性的当前值开始。同样适用于fromY(如果未提供,插值将从layoutY的值开始)。

circle = new Circle(100, 50, 4, Color.BLUE);
TranslateTransition anim = new TranslateTransition(new Duration(1000.0), circle);
anim.setFromX(0);
anim.setToX(200);
anim.setAutoReverse(true);
anim.setCycleCount(Animation.INDEFINITE);
anim.setInterpolator(Interpolator.LINEAR);

控制和监控过渡

与表 2-1 中的所有类一样,TranslateTransition类扩展了javafx.animation.Transition类,后者又扩展了Animation类。因为Timeline类扩展了Animation类,通过比较清单 2-5 和 2-6 可以看出,本例中按钮的所有代码都与上例中的相同。事实上,开始、暂停、恢复和停止动画所需的功能是在Animation类本身上定义的,并且由Translation类和Timeline类继承。

MetronomePathTransition 示例

如表 2-1 所示,PathTransition是一个转换类,使你能够沿着一个定义的几何路径移动一个节点。图 2-10 显示了一个名为 MetronomePathTransition 的节拍器示例版本的屏幕截图,演示了如何使用PathTransition类。

A323806_4_En_2_Fig10_HTML.jpg

图 2-10。

The MetronomePathTransition program

MetronomePathTransition 程序的行为

继续运行程序,再次执行与节拍器 1 练习相同的步骤。除了节点是一个椭圆而不是一个圆,并且节点沿着一条弧线的路径移动之外,一切都应该与 MetronomeTransition 示例中的功能相同。

了解 MetronomePathTransition 程序

清单 2-7 包含了 MetronomePathTransition 程序的代码片段,突出了与前面的(MetronomeTransition)程序的不同之处。看一下代码,然后我们回顾相关的概念。

package projavafx.metronomepathtransition.ui;

...imports omitted...

public class MetronomePathTransitionMain extends Application {

    Button startButton;
    Button pauseButton;
    Button resumeButton;
    Button stopButton;
    Ellipse ellipse;

    Path path;

    public static void main(String[] args) {
        Application.launch(args);
    }

    @Override
    public void start(Stage stage) {
        ellipse = new Ellipse(100, 50, 4, 8);
        ellipse.setFill(Color.BLUE);
        path = new Path(
                new MoveTo(100, 50),
                new ArcTo(350, 350, 0, 300, 50, false, true)
        );
        PathTransition anim = new PathTransition(new Duration(1000.0), path, ellipse);
        anim.setOrientation(OrientationType.ORTHOGONAL_TO_TANGENT);
        anim.setInterpolator(Interpolator.LINEAR);
        anim.setAutoReverse(true);
        anim.setCycleCount(Timeline.INDEFINITE);
        startButton = new Button("start");
        startButton.setOnAction(e -> anim.playFromStart());
        pauseButton = new Button("pause");
        pauseButton.setOnAction(e -> anim.pause());
        resumeButton = new Button("resume");
        resumeButton.setOnAction(e -> anim.play());
        stopButton = new Button("stop");
        stopButton.setOnAction(e -> anim.stop());
        HBox commands = new HBox(10, startButton,
                pauseButton,
                resumeButton,
                stopButton);
        commands.setLayoutX(60);
        commands.setLayoutY(420);
        Group group = new Group(ellipse, commands);
        Scene scene = new Scene(group, 400, 500);

        startButton.disableProperty().bind(anim.statusProperty()
                .isNotEqualTo(Animation.Status.STOPPED));
        pauseButton.disableProperty().bind(anim.statusProperty()
                .isNotEqualTo(Animation.Status.RUNNING));
        resumeButton.disableProperty().bind(anim.statusProperty()
                .isNotEqualTo(Animation.Status.PAUSED));
        stopButton.disableProperty().bind(anim.statusProperty()
                .isEqualTo(Animation.Status.STOPPED));

        stage.setScene(scene);
        stage.setTitle("Metronome using PathTransition");
        stage.show();
    }
}

Listing 2-7.Portions of 
MetronomePathTransitionMain.java

使用 PathTransition 类

如清单 2-7 所示,定义一个PathTransition包括向 path 属性提供一个类型为Path的实例,该属性表示节点将要行进的几何路径。这里我们创建了一个Path实例,它定义了一个弧线,起点在 x 轴上 100 像素,y 轴上 50 像素,终点在 x 轴上 300 像素,y 轴上 50 像素,水平和垂直半径为 350 像素。这是通过创建一个包含MoveToArcTo路径元素的Path来实现的。查看 JavaFX API 文档中的javafx.scene.shape包,了解关于用于创建路径的PathElement类及其子类的更多信息。

Tip

除了sweepFlag之外,ArcTo类中的属性相当直观。如果sweepFlag为真,连接圆弧中心和圆弧本身的线扫过的角度越来越大;否则,它会以递减的角度扫描。

PathTransition类的另一个属性是 orientation,它控制节点的方向是保持不变还是在沿着路径移动时保持垂直于路径的切线。清单 2-7 使用OrientationType.ORTHOGONAL_TO_TANGENT常量来完成后者,因为前者是默认的。

画一个椭圆

如清单 2-7 所示,绘制一个Ellipse与绘制一个Circle相似,不同的是需要一个额外的半径(radiusXradiusY而不仅仅是radius)。

现在,您已经学习了如何通过创建时间轴和过渡来制作节点动画,我们将创建一个非常简单的 Pong 风格的游戏,它需要制作一个乒乓球动画。在这个过程中,你学会了如何在游戏中检测球何时击中了球拍或墙壁。

节点冲突检测之禅

设置节点动画时,有时需要知道节点何时与另一个节点发生碰撞。为了展示这种能力,我们的同事克里斯·赖特开发了一款简单的乒乓风格游戏,我们称之为 ZenPong。原来我们让他只用一个桨搭建游戏,这就带来了著名的禅宗公案(哲学谜语)“一只手拍手的声音是什么?”铭记在心。Chris 在开发游戏中获得了如此多的乐趣,以至于他偷偷加入了第二个球拍,但我们仍然称这个例子为 ZenPong。图 2-11 显示了第一次调用时这个非常简单的游戏形式。

A323806_4_En_2_Fig11_HTML.jpg

图 2-11。

The initial state of the ZenPong game

按照接下来的练习中的说明来尝试这个游戏,记住你控制两个桨(除非你能让一个同事分享你的键盘来玩)。

Examining the Behavior of the Zenpong Game

程序启动时,其外观应该类似于图 2-11 中的截图。要全面检查其行为,请执行以下步骤。

  1. 在点按“开始”之前,将每个桨垂直拖到其他位置。一个游戏欺骗是向上拖动左桨和向下拖动右桨,这将使他们在发球后处于良好的位置来回应球。

  2. 练习使用 A 键向上移动左拨片,Z 键向下移动左拨片,L 键向上移动右拨片,逗号(,)键向下移动右拨片。

  3. Click Start to begin playing the game. Notice that the Start button disappears and the ball begins moving at a 45° angle, bouncing off paddles and the top and bottom walls . The screen should look similar to Figure 2-12.

    A323806_4_En_2_Fig12_HTML.jpg

    图 2-12。

    The ZenPong game in action

  4. 如果球击中左墙或右墙,你的一只手就输掉了比赛。请注意游戏重置,再次看起来像图 2-11 中的截图。

现在您已经体验了 ZenPong 程序的行为,让我们回顾一下它背后的代码。

了解 zenping 计划

在我们强调其中演示的一些概念之前,先检查清单 2-8 中 ZenPong 程序的代码。

package projavafx.zenpong.ui;

...imports omitted...

public class ZenPongMain extends Application {

    /**
     * The center points of the moving ball
     */
    DoubleProperty centerX = new SimpleDoubleProperty();
    DoubleProperty centerY = new SimpleDoubleProperty();

    /**
     * The Y coordinate of the left paddle
     */
    DoubleProperty leftPaddleY = new SimpleDoubleProperty();

    /**
     * The Y coordinate of the right paddle
     */
    DoubleProperty rightPaddleY = new SimpleDoubleProperty();

    /**
     * The drag anchor for left and right paddles
     */
    double leftPaddleDragAnchorY;
    double rightPaddleDragAnchorY;

    /**
     * The initial translateY property for the left and right paddles
     */
    double initLeftPaddleTranslateY;
    double initRightPaddleTranslateY;

    /**
     * The moving ball
     */
    Circle ball;

    /**
     * The Group containing all of the walls, paddles, and ball. This also
     * allows us to requestFocus for KeyEvents on the Group
     */
    Group pongComponents;

    /**
     * The left and right paddles
     */
    Rectangle leftPaddle;
    Rectangle rightPaddle;

    /**
     * The walls
     */
    Rectangle topWall;
    Rectangle rightWall;
    Rectangle leftWall;
    Rectangle bottomWall;

    Button startButton;

    /**
     * Controls whether the startButton is visible
     */
    BooleanProperty startVisible = new SimpleBooleanProperty(true);

    /**
     * The animation of the ball
     */
    Timeline pongAnimation;

    /**
     * Controls whether the ball is moving right
     */
    boolean movingRight = true;

    /**
     * Controls whether the ball is moving down
     */
    boolean movingDown = true;

    /**
     * Sets the initial starting positions of the ball and paddles
     */
    void initialize() {
        centerX.setValue(250);
        centerY.setValue(250);
        leftPaddleY.setValue(235);
        rightPaddleY.setValue(235);
        startVisible.set(true);
        pongComponents.requestFocus();
    }

    /**
     * Checks whether or not the ball has collided with either the paddles,
     * topWall, or bottomWall. If the ball hits the wall behind the paddles, the
     * game is over.
     */
    void checkForCollision() {
        if (ball.intersects(rightWall.getBoundsInLocal())
                || ball.intersects(leftWall.getBoundsInLocal())) {
            pongAnimation.stop();
            initialize();
        } else if (ball.intersects(bottomWall.getBoundsInLocal())
                || ball.intersects(topWall.getBoundsInLocal())) {
            movingDown = !movingDown;
        } else if (ball.intersects(leftPaddle.getBoundsInParent()) && !movingRight) {
            movingRight = !movingRight;
        } else if (ball.intersects(rightPaddle.getBoundsInParent()) && movingRight) {
            movingRight = !movingRight;
        }
    }

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        Application.launch(args);
    }

    @Override
    public void start(Stage stage) {
        pongAnimation = new Timeline(
                new KeyFrame(new Duration(10.0), t -> {
                    checkForCollision();
                    int horzPixels = movingRight ? 1 : -1;
                    int vertPixels = movingDown ? 1 : -1;
                    centerX.setValue(centerX.getValue() + horzPixels);
                    centerY.setValue(centerY.getValue() + vertPixels);
                })
        );
        pongAnimation.setCycleCount(Timeline.INDEFINITE);
        ball = new Circle(0, 0, 5, Color.WHITE);
        topWall = new Rectangle(0, 0, 500, 1);
        leftWall = new Rectangle(0, 0, 1, 500);
        rightWall = new Rectangle(500, 0, 1, 500);
        bottomWall = new Rectangle(0, 500, 500, 1);
        leftPaddle = new Rectangle(20, 0, 10, 30);
        leftPaddle.setFill(Color.LIGHTBLUE);
        leftPaddle.setCursor(Cursor.HAND);
        leftPaddle.setOnMousePressed(me -> {
            initLeftPaddleTranslateY = leftPaddle.getTranslateY();
            leftPaddleDragAnchorY = me.getSceneY();
        });
        leftPaddle.setOnMouseDragged(me -> {
            double dragY = me.getSceneY() - leftPaddleDragAnchorY;
            leftPaddleY.setValue(initLeftPaddleTranslateY + dragY);
        });
        rightPaddle = new Rectangle(470, 0, 10, 30);
        rightPaddle.setFill(Color.LIGHTBLUE);
        rightPaddle.setCursor(Cursor.CLOSED_HAND);
        rightPaddle.setOnMousePressed(me -> {
            initRightPaddleTranslateY = rightPaddle.getTranslateY();
            rightPaddleDragAnchorY = me.getSceneY();
        });
        rightPaddle.setOnMouseDragged(me -> {
            double dragY = me.getSceneY() - rightPaddleDragAnchorY;
            rightPaddleY.setValue(initRightPaddleTranslateY + dragY);
        });
        startButton = new Button("Start!");
        startButton.setLayoutX(225);
        startButton.setLayoutY(470);
        startButton.setOnAction(e -> {
            startVisible.set(false);
            pongAnimation.playFromStart();
            pongComponents.requestFocus();
        });
        pongComponents = new Group(ball,
                topWall,
                leftWall,
                rightWall,
                bottomWall,
                leftPaddle,
                rightPaddle,
                startButton);
        pongComponents.setFocusTraversable(true);
        pongComponents.setOnKeyPressed(k -> {
            if (k.getCode() == KeyCode.SPACE
                    && pongAnimation.statusProperty()
                    .equals(Animation.Status.STOPPED)) {
                rightPaddleY.setValue(rightPaddleY.getValue() - 6);
            } else if (k.getCode() == KeyCode.L
                    && !rightPaddle.getBoundsInParent().intersects(topWall.getBoundsInLocal())) {
                rightPaddleY.setValue(rightPaddleY.getValue() - 6);
            } else if (k.getCode() == KeyCode.COMMA
                    && !rightPaddle.getBoundsInParent().intersects(bottomWall.getBoundsInLocal())) {
                rightPaddleY.setValue(rightPaddleY.getValue() + 6);
            } else if (k.getCode() == KeyCode.A
                    && !leftPaddle.getBoundsInParent().intersects(topWall.getBoundsInLocal())) {
                leftPaddleY.setValue(leftPaddleY.getValue() - 6);
            } else if (k.getCode() == KeyCode.Z
                    && !leftPaddle.getBoundsInParent().intersects(bottomWall.getBoundsInLocal())) {
                leftPaddleY.setValue(leftPaddleY.getValue() + 6);
            }
        });
        Scene scene = new Scene(pongComponents, 500, 500);
        scene.setFill(Color.GRAY);

        ball.centerXProperty().bind(centerX);
        ball.centerYProperty().bind(centerY);
        leftPaddle.translateYProperty().bind(leftPaddleY);
        rightPaddle.translateYProperty().bind(rightPaddleY);
        startButton.visibleProperty().bind(startVisible);

        stage.setScene(scene);
        initialize();
        stage.setTitle("ZenPong Example");
        stage.show();
    }
}

Listing 2-8.
ZenPongMain.java

使用关键帧动作事件处理程序

我们在时间线中使用了与本章前面的 Metronome1 程序中演示的不同的技术(见图 2-8 和清单 2-5 )。我们不是在一段时间内插入两个值,而是在时间轴中使用KeyFrame实例的动作事件处理程序。请看清单 2-8 中的以下代码片段,了解这项技术的使用情况。

pongAnimation = new Timeline(
                new KeyFrame(new Duration(10.0), t -> {
                    checkForCollision();
                    int horzPixels = movingRight ? 1 : -1;
                    int vertPixels = movingDown ? 1 : -1;
                    centerX.setValue(centerX.getValue() + horzPixels);
                    centerY.setValue(centerY.getValue() + vertPixels);
                })
);
pongAnimation.setCycleCount(Timeline.INDEFINITE);

如代码片段所示,我们只使用了一个KeyFrame,而且它的时间非常短(10 毫秒)。当一个KeyFrame有一个动作事件处理程序时,该处理程序中的代码——在本例中也是一个 lambda 表达式——在到达那个KeyFrame的时间时被执行。因为这个时间轴的cycleCount是不确定的,所以动作事件处理程序会每隔 10 毫秒执行一次。该事件处理程序中的代码做两件事:

  • 调用一个名为checkForCollision()的方法,这个方法是在这个程序中定义的,其目的是查看球是否与任一球拍或任何墙壁发生碰撞
  • 考虑到球已经移动的方向,更新球的位置绑定到的模型中的属性

使用 Node intersects()方法检测碰撞

看看清单 2-8 中下面的代码片段中的checkForCollision()方法,看看我们如何通过检测两个节点何时相交(共享任何相同的像素)来检查冲突。

void checkForCollision() {
  if (ball.intersects(rightWall.getBoundsInLocal()) ||
      ball.intersects(leftWall.getBoundsInLocal())) {
    pongAnimation.stop();
    initialize();
  }
  else if (ball.intersects(bottomWall.getBoundsInLocal()) ||
           ball.intersects(topWall.getBoundsInLocal())) {
    movingDown = !movingDown;
  }
  else if (ball.intersects(leftPaddle.getBoundsInParent()) && !movingRight) {
    movingRight = !movingRight;
  }
  else if (ball.intersects(rightPaddle.getBoundsInParent()) && movingRight) {
    movingRight = !movingRight;
  }
}

这里显示的Node类的intersects()方法接受位于javafx.geometry包中的Bounds类型的参数。它表示节点的矩形边界,例如,前面代码片段中显示的leftPaddle节点。请注意,为了获得包含它的Group中左挡板的位置,我们使用了leftPaddle (a Rectangle)从Node类继承的boundsInParent属性。

上述代码片段中 intersect 方法调用的最终结果如下。

  • 如果球与rightWallleftWall的边界相交,则pongAnimation Timeline停止,并为下一局初始化游戏。注意,rightWallleft Wall节点是位于Scene左右两侧的一个像素宽的矩形。看一下清单 2-8 看看它们是在哪里定义的。
  • 如果球与bottomWalltopWall的边界相交,球的垂直方向将通过否定程序的布尔movingDown变量来改变。
  • 如果球与leftPaddlerightPaddle的边界相交,球的水平方向将通过否定程序的布尔movingRight变量来改变。

Tip

有关boundsInParent及其相关属性layoutBoundsboundsInLocal的更多信息,请参见 JavaFX API 文档中javafx.scene.Node类开头的“边界矩形”讨论。例如,通常使用表达式myNode.getLayoutBounds().getWidth()myNode.getLayoutBounds().getHeight()来找出节点的宽度或高度。

拖动节点

正如您之前所经历的,ZenPong 应用程序的拨片可能会被鼠标拖动。清单 2-8 中的以下代码片段展示了如何在 ZenPong 中实现这一功能来拖动右球拍。

  DoubleProperty rightPaddleY = new SimpleDoubleProperty();
  ...code omitted...
  double rightPaddleDragStartY;
  double rightPaddleDragAnchorY;
  ...code omitted...
  void initialize() {
...code omitted...
    rightPaddleY.setValue(235);

  }
  ...code omitted...

rightPaddle = new Rectangle(470, 0, 10, 30);
rightPaddle.setFill(Color.LIGHTBLUE);
rightPaddle.setCursor(Cursor.CLOSED_HAND);
rightPaddle.setOnMousePressed(me -> {
    initRightPaddleTranslateY = rightPaddle.getTranslateY();
    rightPaddleDragAnchorY = me.getSceneY();
});
rightPaddle.setOnMouseDragged(me -> {
    double dragY = me.getSceneY() - rightPaddleDragAnchorY;
    rightPaddleY.setValue(initRightPaddleTranslateY + dragY);
});

...code omitted...

rightPaddle.translateYProperty().bind(rightPaddleY);

请注意,在这个 ZenPong 示例中,我们只垂直拖动桨,而不是水平拖动。因此,代码片段只处理 y 轴上的拖动。在初始位置创建 paddle 后,我们为MousePressedMouseDragged事件注册事件处理程序。后者操纵rightPaddleY属性,该属性用于沿 y 轴平移桨。属性和绑定将在第三章中详细解释。

为节点提供键盘输入焦点

对于接收按键事件的节点,它必须拥有键盘焦点。这在 ZenPong 示例中是通过做这两件事来实现的,如清单 2-8 中的代码片段所示:

  • 将 true 赋给Group节点的focusTraversable属性。这允许节点接受键盘焦点。
  • 调用Group节点的requestFocus()方法(由pongComponents变量引用)。这要求节点获得焦点。

Tip

您不能直接设置Stage的聚焦属性的值。查阅 API 文档还会发现,您不能设置一个Node(例如,我们现在正在讨论的Group)的聚焦属性的值。然而,正如在刚刚提到的第二点中所讨论的,您可以在节点上调用requestFocus(),如果被授予权限(并且focusTraversable为真),就会将聚焦属性设置为真。顺便说一下,Stage没有requestFocus()方法,但是它有一个toFront()方法,这应该会给它键盘焦点。

...code omitted...

pongComponents.setFocusTraversable(true);
pongComponents.setOnKeyPressed(k -> {
    if (k.getCode() == KeyCode.SPACE
            && pongAnimation.statusProperty()
            .equals(Animation.Status.STOPPED)) {
        rightPaddleY.setValue(rightPaddleY.getValue() - 6);
    } else if (k.getCode() == KeyCode.L
            && !rightPaddle.getBoundsInParent().intersects(topWall.getBoundsInLocal())) {
        rightPaddleY.setValue(rightPaddleY.getValue() - 6);
    } else if (k.getCode() == KeyCode.COMMA
            && !rightPaddle.getBoundsInParent().intersects(bottomWall.getBoundsInLocal())) {
        rightPaddleY.setValue(rightPaddleY.getValue() + 6);
    } else if (k.getCode() == KeyCode.A
            && !leftPaddle.getBoundsInParent().intersects(topWall.getBoundsInLocal())) {
        leftPaddleY.setValue(leftPaddleY.getValue() - 6);
    } else if (k.getCode() == KeyCode.Z
            && !leftPaddle.getBoundsInParent().intersects(bottomWall.getBoundsInLocal())) {
        leftPaddleY.setValue(leftPaddleY.getValue() + 6);
    }
});

现在节点有了焦点,当用户与键盘交互时,将调用适当的事件处理程序。在这个例子中,我们感兴趣的是某些键被按下的时间,这将在下面讨论。

使用 onKeyPressed 事件处理程序

当用户按键时,提供给onKeyPressed方法的 lambda 表达式被调用,传递一个包含事件信息的KeyEvent实例。这个表达式的方法体,如清单 2-8 中的代码片段所示,将KeyEvent实例的getCode()方法与代表箭头键的KeyCode常量进行比较,以确定哪个键被按下了。

摘要

恭喜你!在本章中,您已经学习了很多关于在 JavaFX 中创建 ui 的知识,包括以下内容。

  • 在 JavaFX 中创建一个 UI,我们大致基于创建一部戏剧的隐喻,通常包括创建一个舞台、一个场景、节点、一个模型和事件处理程序,并制作一些节点的动画
  • 关于使用Stage类的大多数属性和方法的细节,包括如何创建一个没有窗口装饰的透明的Stage
  • 如何使用HBoxVBox布局容器分别水平和垂直组织节点
  • 关于使用Scene类的许多属性和方法的细节
  • 如何通过将一个或多个样式表与Scene相关联来创建 CSS 样式并将其应用于程序中的节点
  • 如何处理键盘和鼠标输入事件
  • 如何使用Timeline类和过渡类来制作场景中节点的动画
  • 如何检测场景中的节点何时发生碰撞

在第三章中,我们将讨论创建用户界面的另一种方法,这次是使用场景构建器。然后,在第四章中,我们将更深入地探讨属性和绑定领域。

资源

有关创建 JavaFX UIs 的更多信息,可以参考以下资源。

三、属性和绑定

The sky is full of vigor and perseverance. Correspondingly, superior people keep their vitality constantly. -I ching

在第 1 和 2 章中,我们向您介绍了 JavaFX 9 平台,它是 Oracle JDK 9 的一部分。您可以用自己喜欢的 IDE 来设置开发环境:Eclipse、NetBeans 或 IntelliJ IDEA。您编写并运行了您的第一个 JavaFX GUI 程序。您学习了 JavaFX 的基本构建块:类StageScene,以及进入SceneNode。毫无疑问,您已经注意到使用用户定义的模型类来表示应用程序状态,并通过属性和绑定将该状态传递给 UI。

在本章中,我们将向您介绍 JavaFX 属性和绑定框架。在回顾了一点历史并展示了一个展示 JavaFX Property的各种使用方式的激励性示例之后,我们将介绍框架的关键概念:ObservableObservableValueWritableValueReadOnlyPropertyPropertyBinding。我们向您展示了框架的这些基本接口所提供的功能。然后,我们向您展示如何将Property对象绑定在一起,如何利用属性和其他绑定来构建Binding对象——使用Bindings实用程序类中的工厂方法、fluent 接口 API,或者通过直接扩展实现Binding接口的抽象类来降低级别——以及如何使用它们来轻松地将程序一部分中的更改传播到程序的其他部分,而无需过多的编码。然后我们介绍 JavaFX Beans 命名约定,它是原始 JavaBeans 命名约定的扩展,使将数据组织到封装的组件中变得有条不紊。我们通过展示如何将旧式 JavaBeans 属性改编成 JavaFX 属性来结束本章。

因为 JavaFX 属性和绑定框架是 JavaFX 平台的非可视部分,所以本章中的示例程序本质上也是非可视的。我们处理BooleanIntegerLongFloatDoubleStringObject类型属性和绑定,因为这些是 JavaFX 绑定框架专门处理的类型。您的 GUI 构建乐趣将在下一章和后续章节中继续。

JavaFX 绑定的先驱

在 Java 生命的早期,人们就认识到需要将 Java 组件的属性直接暴露给客户机代码,允许它们观察和操作这些属性,并在它们的值改变时采取行动。Java 1.1 中的 JavaBeans 框架通过现在熟悉的 getter 和 setter 约定提供了对属性的支持。它还通过其PropertyChangeEventPropertyChangeListener机制支持属性变化的传播。尽管 JavaBeans 框架在许多 Swing 应用程序中使用,但它的使用相当麻烦,需要相当多的样板代码。几年来,人们创建了几个高级数据绑定框架,取得了不同程度的成功。JavaFX 属性和绑定框架中 JavaBeans 的继承主要在于定义 JavaFX 组件时的 JavaFX Beans getter、setter 和属性 getter 命名约定。在讲述了 JavaFX 属性和绑定框架的关键概念和接口之后,我们将在本章的后面讨论 JavaFX Beans getter、setter 和属性 getter 命名约定。

JavaFX 属性和绑定框架的另一个继承来自 JavaFX Script 语言,它是 JavaFX 1.x 平台的一部分。尽管 JavaFX 平台不赞成使用 JavaFX Script 语言,而是支持基于 Java 的 API,但这种转变的目标之一是保留 JavaFX Script 的bind关键字的大部分功能,它的表达能力让许多 JavaFX 爱好者感到高兴。例如,JavaFX 脚本支持绑定到复杂表达式:

var a = 1;
var b = 10;
var m = 4;
def c = bind for (x in [a..b] where x < m) { x * x };

每当abm的值改变时,该代码将自动重新计算c的值。

尽管 JavaFX 属性和绑定框架不支持 JavaFX 脚本的所有绑定结构,但它支持许多有用表达式的绑定。在介绍了框架的关键概念和接口之后,我们将更多地讨论如何构建复合绑定表达式。

激励人心的例子

让我们从清单 3-1 中的一个例子开始,它通过使用几个SimpleIntegerProperty类的实例展示了Property接口的功能。

import javafx.beans.InvalidationListener;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;

public class MotivatingExample {
    private static IntegerProperty intProperty;

    public static void main(String[] args) {
        createProperty();
        addAndRemoveInvalidationListener();
        addAndRemoveChangeListener();
        bindAndUnbindOnePropertyToAnother();
    }

    private static void createProperty() {
        System.out.println();
        intProperty = new SimpleIntegerProperty(1024);
        System.out.println("intProperty = " + intProperty);
        System.out.println("intProperty.get() = " + intProperty.get());
        System.out.println("intProperty.getValue() = " + intProperty.getValue().intValue());
    }

    private static void addAndRemoveInvalidationListener() {
        System.out.println();
        final InvalidationListener invalidationListener = observable ->
            System.out.println("The observable has been invalidated: " + observable + ".");

        intProperty.addListener(invalidationListener);
        System.out.println("Added invalidation listener.");

        System.out.println("Calling intProperty.set(2048).");
        intProperty.set(2048);

        System.out.println("Calling intProperty.setValue(3072).");
        intProperty.setValue(Integer.valueOf(3072));

        intProperty.removeListener(invalidationListener);
        System.out.println("Removed invalidation listener.");

        System.out.println("Calling intProperty.set(4096).");
        intProperty.set(4096);
    }

    private static void addAndRemoveChangeListener() {
        System.out.println();
        final ChangeListener changeListener = (ObservableValue observableValue, Object oldValue, Object newValue) ->
            System.out.println("The observableValue has changed: oldValue = " + oldValue + ", newValue = " + newValue);
        intProperty.addListener(changeListener);
        System.out.println("Added change listener.");

        System.out.println("Calling intProperty.set(5120).");
        intProperty.set(5120);

        intProperty.removeListener(changeListener);
        System.out.println("Removed change listener.");

        System.out.println("Calling intProperty.set(6144).");
        intProperty.set(6144);
    }

    private static void bindAndUnbindOnePropertyToAnother() {
        System.out.println();
        IntegerProperty otherProperty = new SimpleIntegerProperty(0);
        System.out.println("otherProperty.get() = " + otherProperty.get());

        System.out.println("Binding otherProperty to intProperty.");
        otherProperty.bind(intProperty);
        System.out.println("otherProperty.get() = " + otherProperty.get());

        System.out.println("Calling intProperty.set(7168).");
        intProperty.set(7168);
        System.out.println("otherProperty.get() = " + otherProperty.get());

        System.out.println("Unbinding otherProperty from intProperty.");
        otherProperty.unbind();
        System.out.println("otherProperty.get() = " + otherProperty.get());

        System.out.println("Calling intProperty.set(8192).");
        intProperty.set(8192);
        System.out.println("otherProperty.get() = " + otherProperty.get());
    }
}

Listing 3-1.
MotivatingExample.java

在这个例子中,我们创建了一个名为intPropertySimpleIntegerProperty对象,初始值为1024。然后我们通过一系列不同的整数更新它的值,同时我们添加然后移除一个InvalidationListener,添加然后移除一个ChangeListener,最后,创建另一个名为otherPropertySimpleIntegerProperty,将其绑定到,然后从intProperty解除绑定。我们利用 Java 8 lambda 语法来定义我们的侦听器。示例程序使用了大量的println调用来展示程序内部发生的事情。

当我们运行清单 3-1 中的程序时,以下输出被打印到控制台:

intProperty = IntegerProperty [value: 1024]
intProperty.get() = 1024
intProperty.getValue() = 1024

Added invalidation listener.
Calling intProperty.set(2048).
The observable has been invalidated: IntegerProperty [value: 2048].
Calling intProperty.setValue(3072).
The observable has been invalidated: IntegerProperty [value: 3072].
Removed invalidation listener.
Calling intProperty.set(4096).

Added change listener.
Calling intProperty.set(5120).
The observableValue has changed: oldValue = 4096, newValue = 5120
Removed change listener.
Calling intProperty.set(6144).

otherProperty.get() = 0
Binding otherProperty to intProperty.
otherProperty.get() = 6144
Calling intProperty.set(7168).
otherProperty.get() = 7168
Unbinding otherProperty from intProperty.
otherProperty.get() = 7168
Calling intProperty.set(8192).
otherProperty.get() = 7168

通过将输出行与程序源代码相关联(或者通过在您喜欢的 IDE 的调试器中单步调试代码),我们可以得出以下结论。

  • 一个SimpleIntegerProperty对象,比如intPropertyotherProperty持有一个int值。该值可以用get()set()getValue()setValue()方法操作。get()set()方法使用原语int类型执行它们的操作。getValue()setValue()方法使用Integer包装器类型。
  • 您可以在intProperty中添加和删除InvalidationListener对象。
  • 您可以在intProperty中添加和删除ChangeListener对象。
  • 另一个Property对象如otherProperty可以将自己绑定到intProperty。当这种情况发生时,otherProperty接收intProperty的值。
  • 当在intProperty上设置一个新值时,连接到它的任何对象都会得到通知。如果对象被移除,则不会发送通知。
  • 当被通知时,InvalidationListener对象仅被告知哪个对象正在发出通知,并且该对象仅被称为Observable
  • 当被通知时,除了发送通知的对象之外,ChangeListener对象还被告知另外两条信息——oldValuenewValue。发送对象被称为ObservableValue
  • 在绑定属性如otherProperty的情况下,我们无法从输出中得知intProperty中值的变化何时或如何通知它。然而,我们可以推断它一定知道这个变化,因为当我们向otherProperty请求它的值时,我们得到了intProperty的最新值。

Note

尽管这个激励示例使用了一个Integer属性,但是类似的示例也可以使用基于BooleanLongFloatDoubleStringObject类型的属性。在 JavaFX 属性和绑定框架中,当接口为具体类型扩展或实现时,它们总是为BooleanIntegerLongFloatDoubleStringObject类型完成。

这个例子让我们注意到 JavaFX 属性和绑定框架的一些关键接口和概念:包括Observable和相关联的InvalidationListener接口、ObservableValue和相关联的ChangeListener接口、get()set()getValue()setValue()方法,它们允许我们直接操作SimpleIntegerProperty对象的值,以及bind()方法,它们允许我们通过从属于另一个SimpleIntegerProperty对象来放弃对SimpleIntegerProperty对象的值的直接操作。

在下一节中,我们将更详细地向您展示 JavaFX 属性和绑定框架的这些以及其他一些关键接口和概念。

理解关键接口和概念

图 3-1 是一个 UML 图,显示了 JavaFX 属性和绑定框架的关键接口。它包括一些您在上一节中看到的界面,以及一些您还没有看到的界面。

A323806_4_En_3_Fig1_HTML.jpg

图 3-1。

Key interfaces of the JavaFX properties and bindings framework Note

我们没有向您展示 UML 图中接口的完全限定名。这些接口分布在四个包中:javafx.beansjavafx.beans.bindingjavafx.beans.propertyjavafx.beans.value。通过查看 JavaFX API 文档或您喜欢的 IDE 的“find class”特性,您可以很容易地确定哪个接口属于哪个包。

理解可观察界面

层次结构的根是Observable接口。您可以将InvalidationListener对象注册到一个Observable对象来接收失效事件。在上一节的激励示例中,您已经看到了从一种Observable对象,即SimpleIntegerProperty对象intProperty触发的失效事件。当调用set()setValue()方法将底层值从一个int更改为另一个int时,它被触发。

Note

如果您连续多次使用相同的值调用 setter,JavaFX 属性和绑定框架中的Property接口的任何实现只会触发一次失效事件。

另一个引发失效事件的地方是来自Binding对象。你还没有看到一个Binding对象的例子,但是在本章的后半部分有大量的Binding对象。现在我们只注意到一个Binding对象可能会变得无效,例如,当它的invalidate()方法被调用时,或者如我们在本章后面所展示的,当它的一个依赖项触发一个无效事件时。

Note

如果一个失效事件连续几次失效,那么 JavaFX properties and bindings 框架中的任何一个Binding接口实现都只会触发一次。

了解 ObservableValue 接口

层次结构中的下一个是ObservableValue接口。它只是一个有值的Observable。它的getValue()方法返回它的值。我们在激励示例中对SimpleIntegerProperty对象调用的getValue()方法可以被认为是来自这个接口。您可以将ChangeListener对象注册到一个ObservableValue对象来接收变更事件。

在上一节的激励示例中,您看到了变更事件被激发。当 change 事件触发时,ChangeListener接收到另外两条信息:ObservableValue对象的旧值和新值。

Note

如果您连续多次使用相同的值调用 setter,JavaFX 属性和绑定框架中的任何ObservableValue接口实现只会触发一次 change 事件。

无效事件和变更事件之间的区别在于 JavaFX 属性和绑定框架可以支持惰性评估。我们通过查看激励示例中的三行代码来展示一个例子:

otherProperty.bind(intProperty);
intProperty.set(7168);
System.out.println("otherProperty.get() = " + otherProperty.get());

当调用intProperty.set(7168)时,它向otherProperty触发一个无效事件。在收到这个无效事件时,otherProperty简单地记录下它的值不再有效的事实。它不会通过查询intProperty来立即重新计算其值。当otherProperty.get()被调用时,重新计算被执行。想象一下,如果我们多次调用intProperty.set(),而不是像前面的代码那样只调用intProperty.set()一次;otherProperty仍然只重新计算一次它的值。

Note

ObservableValue接口不是Observable的唯一直接子接口。在javafx.collections包中还有另外四个Observable的直接子接口:ObservableListObservableMapObservableSetObservableArray,对应的ListChangeListenerMapChangeListenerSetChangeListenerArrayChangeListener作为回调机制。这些 JavaFX 可观察集合将在第七章中介绍。

了解可写值接口

这可能是整个章节中最简单的部分,因为WritableValue界面确实像它看起来那样简单。它的目的是将getValue()setValue()方法注入到这个接口的实现中。JavaFX 属性和绑定框架中WritableValue的所有实现类也实现了ObservableValue;所以你可以做一个论证,WritableValue的值只是为了提供setValue()方法。

你已经看到了激励例子中的setValue()方法。

了解 ReadOnlyProperty 接口

ReadOnlyProperty接口在其实现中注入了两个方法。getBean()方法应该返回包含ReadOnlyRropertyObject,如果不包含在Object中,则返回 null。如果ReadOnlyProperty没有名字,那么getName()方法应该返回ReadOnlyProperty的名字或者空字符串。

包含对象和名称提供了关于ReadOnlyProperty的上下文信息。属性的上下文信息在无效事件的传播或值的重新计算中不起任何直接作用。但是,如果提供的话,会在一些外围计算中考虑到。

在我们的激励示例中,intProperty是在没有任何上下文信息的情况下构建的。如果我们使用完整的构造器给它命名,

intProperty = new SimpleIntegerProperty(null, "intProperty", 1024);

输出将包含属性名:

intProperty = IntegerProperty [name: intProperty, value: 1024]

了解属性接口

现在我们来到关键接口层次的底部。到目前为止,Property接口拥有我们已经研究过的所有四个接口作为它的超接口:ObservableObservableValueReadOnlyPropertyWritableValue。因此,它继承了这些接口的所有方法。它还提供了自己的五种方法:

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

在上一节的激励示例中,您已经看到了两种有效的方法:bind()unbind()

调用bind()会在Property对象和ObservableValue参数之间创建一个单向绑定或依赖关系。一旦他们进入这种关系,调用Property对象上的set()setValue()方法将导致抛出一个RuntimeException。调用Property对象上的get()getValue()方法将返回ObservableValue对象的值。当然,改变ObservableValue对象的值会使Property对象失效。调用unbind()释放Property对象可能有的任何现有单向绑定。如果单向绑定生效,isBound()方法返回true;否则,返回false

调用bindBidirectional()会在Property调用者和Property参数之间创建一个双向绑定。注意,与接受ObservableValue参数的bind()方法不同,bindBidirectional()方法接受Property参数。只有两个Property对象可以双向绑定在一起。一旦它们进入这种关系,在任一个Property对象上调用set()setValue()方法将导致两个对象的值都被更新。调用unbindBidirectional()释放调用者和参数可能有的任何现有双向绑定。清单 3-2 中的程序展示了一个简单的双向绑定。

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-2.
BidirectionalBindingExample.java

在这个例子中,我们创建了两个名为prop1prop2SimpleStringProperty对象,在它们之间创建了一个双向绑定,然后在两个属性上分别名为set()get()

当我们运行清单 3-2 中的程序时,以下输出被打印到控制台:

Constructing two StringProperty objects.
Calling bindBidirectional.
prop1.isBound() = false
prop2.isBound() = false
Calling prop1.set("prop1 says: Hi!")
prop2.get() returned:
prop1 says: Hi!
Calling prop2.set(prop2.get() + "\nprop2 says: Bye!")
prop1.get() returned:
prop1 says: Hi!
prop2 says: Bye!

Caution

每个Property对象一次最多可以有一个活动的单向绑定。它可以有任意多的双向绑定。isBound()方法只适用于单向绑定。当单向绑定已经生效时,用不同的ObservableValue参数第二次调用bind()将会解除现有的绑定并用新的替换它。

了解绑定接口

Binding接口定义了四种揭示接口意图的方法。一个Binding对象是一个ObservableValue,它的有效性可以用isValid()方法查询,用invalidate()方法设置。它有一个依赖列表,可以用getDependencies()方法获得。最后一个dispose()方法发信号通知绑定将不再被使用,它所使用的资源可以被清理。

从这个对Binding接口的简短描述中,我们可以推断出它代表了一个具有多个依赖关系的单向绑定。我们想象,每一个依赖项都可以是一个ObservableValueBinding注册到这个 ?? 来接收无效事件。当调用get()getValue()方法时,如果绑定无效,则重新计算其值。

JavaFX 属性和绑定框架不提供任何实现Binding接口的具体类。但是,它提供了多种方法来轻松创建自己的Binding对象:可以在框架中扩展抽象基类;您可以在实用程序类Bindings中使用一组静态方法,从现有的常规 Java 值(即不可观察的值)、属性和绑定中创建新的绑定;您还可以使用各种属性和绑定类中提供的一组方法,并形成一个流畅的接口 API 来创建新的绑定。我们将在本章后面的“创建绑定”一节中介绍实用程序方法和 fluent 接口 API。现在,我们通过扩展DoubleBinding抽象类向您展示第一个绑定示例。清单 3-3 中的程序使用绑定来计算一个矩形的面积。

import javafx.beans.binding.DoubleBinding;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;

public class RectangleAreaExample {
    public static void main(String[] args) {
        System.out.println("Constructing x with initial value of 2.0.");
        final DoubleProperty x = new SimpleDoubleProperty(null, "x", 2.0);
        System.out.println("Constructing y with initial value of 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() {
            private double value;

            {
                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-3.
RectangleAreaExample.java

在匿名内部类中,我们调用超类DoubleBinding中受保护的bind()方法,通知超类我们想要监听来自DoubleProperty对象xy的失效事件。我们最终在超类DoubleBinding中实现了受保护的抽象computeValue()方法,以便在需要重新计算时进行实际计算。

当我们运行清单 3-3 中的程序时,以下输出被打印到控制台:

Constructing x with initial value of 2.0.
Constructing y with initial value of 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()只被调用一次。

Caution

DoubleBinding抽象类包含一个空的默认实现dispose()和一个返回空列表的默认实现getDependencies()。为了使这个例子成为一个正确的Binding实现,我们应该覆盖这两个方法来正确地运行。

现在您已经牢牢掌握了 JavaFX 属性和绑定框架的关键接口和概念,我们将向您展示这些通用接口如何专用于特定类型的接口,以及如何在特定类型的抽象和具体类中实现。

键接口的特定类型专门化

我们在上一节中没有强调这个事实,因为我们相信省略它不会影响那里的解释,但是除了ObservableInvalidationListener,其余的接口都是带有类型参数<T>的通用接口。在本节中,我们将研究这些通用接口是如何专用于感兴趣的特定类型的:BooleanIntegerLongFloatDoubleStringObject。我们还研究了框架的一些抽象和具体类,并探索了每个类的典型使用场景。

Note

这些接口的专门化也存在于ListMapSet中。它们是为处理可观察集合而设计的。我们将在第七章中讨论可观测集合。

特定类型接口的通用主题

尽管通用接口的专门化方式并不完全相同,但存在一个共同的主题:

  • Boolean型直接专门化。
  • IntegerLongFloatDouble类型通过Number超类型特殊化。
  • String型通过Object型专门化。

这个主题存在于所有关键接口的特定于类型的专门化中。例如,我们检查ObservableValue<T>接口的子接口:

  • ObservableBooleanValue扩展了ObservableValue<Boolean>,它提供了一个额外的方法。
    • boolean get();
  • ObservableNumberValue扩展了ObservableValue<Number>,它提供了四个额外的方法。
    • int intValue();
    • long longValue();
    • float floatValue();
    • double doubleValue();
  • ObservableObjectValue<T>扩展了ObservableValue<T>,它提供了一个额外的方法。
    • T get();
  • ObservableIntegerValueObservableLongValueObservableFloatValueObservableDoubleValue扩展了ObservableNumberValue,并且每个都提供了一个额外的get()方法来返回适当的原始类型值。
  • ObservableStringValue扩展了ObservableObjectValue<String>并继承了其返回Stringget()方法。

注意,我们在示例中使用的get()方法是在特定于类型的ObservableValue子接口中定义的。类似的检查揭示了我们在示例中使用的set()方法是在特定于类型的WritableValue子接口中定义的。

这种派生层次结构的一个实际结果是,任何数字属性都可以在任何其他数字属性或绑定上调用bind()。实际上,bind()方法对任何数字属性的签名如下:

void bind(ObservableValue<? extends Number>  observable);

任何数值属性和绑定都可以赋给泛型参数类型。清单 3-4 中的程序显示,任何不同特定类型的数字属性都可以相互绑定。

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-4.
NumericPropertiesExample.java

在本例中,我们创建了四个数字属性,并将它们绑定到一个大小递减的链中,以演示绑定是否按预期工作。然后,我们颠倒了链的顺序,将 double 属性的值设置为一个会溢出 integer 属性的数字,以强调这样一个事实:即使您可以将不同大小的数值属性绑定在一起,但是当依赖属性的值超出绑定属性的范围时,将应用普通的 Java 数值转换。

当我们运行清单 3-4 中的程序时,以下内容被打印到控制台:

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

常用类别

我们现在给出四个包javafx.beansjavafx.beans.bindingjavafx.beans.propertyjavafx.beans.value的内容的调查。在本节中,SimpleIntegerProperty系列的类是指在BooleanIntegerLongFloatDoubleStringObject类型上外推的类。所以说的话也适用于SimpleBooleanProperty,以此类推。

  • JavaFX 属性和绑定框架中最常用的类是SimpleIntegerProperty系列的类。它们提供了Property接口的所有功能,包括惰性评估。到目前为止,本章的所有例子都使用了它们。
  • JavaFX 属性和绑定框架中的另一组具体类是ReadOnlyIntegerWrapper系列的类。这些类实现了Property接口,但也有一个getReadOnlyProperty()方法,该方法返回一个与主Property同步的ReadOnlyProperty。当你需要一个完整的Property来实现一个组件,但是你只想把一个ReadOnlyProperty交给组件的客户端时,它们非常方便使用。
  • 抽象类的IntegerPropertyBase系列可以被扩展以提供完整的Property类的实现,尽管实际上SimpleIntegerProperty系列的类更容易使用。在IntegerPropertyBase系列类中唯一的抽象方法是getBean()getName()
  • 可以扩展抽象类的ReadOnlyIntegerPropertyBase系列来提供ReadOnlyProperty类的实现。这很少是必要的。在ReadOnlyIntegerPropertyBase系列的类中仅有的抽象方法是get()getBean()getName()
  • 在调用addListener()之前,WeakInvalidationListenerWeakChangeListener类可以用来包装InvalidationListenerChangeListener实例。它们保存包装的侦听器实例的弱引用。只要您持有对您这边的包装侦听器的引用,弱引用将保持活动状态,并且您将接收事件。当您使用完包装的侦听器并从您的一端取消引用它时,弱引用将符合垃圾收集的条件,然后再进行垃圾收集。所有 JavaFX 属性和绑定框架Observable对象都知道如何在弱引用被垃圾收集后清理弱侦听器。当侦听器在使用后没有被删除时,这可以防止内存泄漏。WeakInvalidationListenerWeakListener类实现了WeakListener接口,如果包装的监听器实例被垃圾收集,其wasGarbageCollected()方法将返回true

这涵盖了驻留在javafx.beansjavafx.beans.propertyjavafx.beans.value包中的所有 JavaFX 属性和绑定 API,以及javafx.beans.binding包中的一些 API,但不是全部。javafx.beans.property.adapters包提供了旧式 JavaBeans 属性和 JavaFX 属性之间的适配器。我们将在“使 JavaBeans 属性适应 JavaFX 属性”一节中介绍这些适配器。javafx.beans.binding包的其余类是 API,帮助您从现有的属性和绑定中创建新的绑定。这是下一节的重点。

创建绑定

现在,我们将注意力转向从现有的属性和绑定中创建新的绑定。在本章前面的“理解关键接口和概念”一节中,您已经了解到绑定是一个可观察的值,它有一系列依赖项,这些依赖项也是可观察的值。

JavaFX 属性和绑定框架提供了三种创建新绑定的方法:

  • 扩展了IntegerBinding系列的抽象类。
  • 使用绑定——在实用程序类Bindings中创建静态方法。
  • 使用IntegerExpression系列抽象类提供的 fluent 接口 API。

您在“理解绑定接口”一节中看到了直接扩展方法。接下来我们将探索Bindings实用程序类。

了解绑定实用程序类

Bindings类包含 236 个工厂方法,这些方法利用现有的可观察值和常规值进行新的绑定。考虑到可观察值和常规 Java(不可观察)值都可以用于构建新的绑定,大多数方法都被重载。至少有一个参数必须是可观察值。下面是九个重载的add()方法的签名:

public static NumberBinding add(ObservableNumberValue n1, ObservableNumberValue n2)
public static DoubleBinding add(ObservableNumberValue n, double d)
public static DoubleBinding add(double d, ObservableNumberValue n)
public static NumberBinding add(ObservableNumberValue n, float f)
public static NumberBinding add(float f, ObservableNumberValue n)
public static NumberBinding add(ObservableNumberValue n, long l)
public static NumberBinding add(long l, ObservableNumberValue n)
public static NumberBinding add(ObservableNumberValue n, int i)
public static NumberBinding add(int i, ObservableNumberValue n)

当调用add()方法时,它返回一个NumberBinding,其依赖项包括所有可观察值参数,其值是其两个参数值的和。类似的重载方法也存在于subtract()multiply()divide()中。

Note

从上一节回忆起,ObservableIntegerValueObservableLongValueObservableFloatValueObservableDoubleValueObservableNumberValue的子类。所以刚才说的四种算术方法,可以取这些可观测数值的任意组合,也可以取任何不可观测值。

清单 3-5 中的程序使用Bindings中的算术方法计算笛卡尔平面中顶点为(x1, y1)(x2, y2)(x3, y3)的三角形的面积,使用以下公式:

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

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 sum3 = Bindings.add(sum2, x3y1);
        final NumberBinding diff1 = Bindings.subtract(sum3, 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-5.
TriangleAreaExample.java

我们用IntegerProperty来表示坐标。构建NumberBinding area使用了Bindings的所有四种算术工厂方法。因为我们从IntegerProperty对象开始,即使来自Bindings的算术工厂方法的返回类型是NumberBinding,实际返回的对象,直到determinant,都是IntegerBinding对象。我们在divide()调用中使用了2.0D而不仅仅是2来强制划分为double划分,而不是int划分。我们构建的所有属性和绑定形成一个树形结构,以area为根,中间绑定为内部节点,属性x1y1x2y2x3y3为叶。如果我们使用正则算术表达式的语法来解析面积公式的数学表达式,这个树类似于我们将得到的解析树。

当我们运行清单 3-5 中的程序时,以下输出被打印到控制台:

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类还有以下工厂方法。

  • 逻辑运算符:andornot
  • 数字运算符:minmaxnegate
  • 对象运算符:isNullisNotNull
  • 字符串运算符:lengthisEmptyisNotEmpty
  • 关系运算符:
    • equal
    • equalIgnoreCase
    • greaterThan
    • greaterThanOrEqual
    • lessThan
    • lessThanOrEqual
    • notEqual
    • notEqualIgnoreCase
  • 创建运算符:
    • createBooleanBinding
    • createIntegerBinding
    • createLongBinding
    • createFloatBinding
    • createDoubleBinding
    • createStringBinding
    • createObjectBinding
  • 选择运算符:
    • select
    • selectBoolean
    • selectInteger
    • selectLong
    • selectFloat
    • selectDouble
    • selectString

除了创建操作符和选择操作符,前面的操作符都执行您认为它们会执行的操作。对象运算符仅对可观察的字符串值和可观察的对象值有意义。字符串运算符仅对可观察的字符串值有意义。除了IgnoreCase以外的所有关系运算符都适用于数值。在比较floatdouble值时,数值的equalnotEqual操作符有第三个double公差参数。equalnotEqual操作符也适用于boolean、字符串和对象值。对于字符串和对象值,equalnotEqual操作符使用equals()方法比较它们的值。

创建操作符提供了一种无需直接扩展抽象基类就能创建绑定的便捷方式。它接受一个Callable和任意数量的依赖项作为参数。清单 3-3 中的区域双重绑定可以使用 lambda 表达式作为Callable重写,如下所示:

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

选择操作符对所谓的 Java FX bean 进行操作,Java bean 是根据 Java FX bean 规范构造的 Java 类。我们将在本章后面的“理解 Java FX bean 约定”一节中讨论 Java FX bean。

Bindings中有许多处理可观察集合的方法。我们将在第七章中介绍它们。

这涵盖了Bindings中返回绑定对象的所有方法。Bindings中有 18 个方法不返回绑定对象。各种bindBidirectional()unbindBidirectional()方法创建双向绑定。事实上,各种属性类中的bindBidirectional()unbindBidirectional()方法简单地调用了Bindings类中相应的方法。bindContent()unbindContent()方法将一个普通集合绑定到一个可观察的集合。convert()concat()和一对重载的format()方法返回StringExpression对象。最后,when()方法返回一个When对象。

WhenStringExpression类是创建绑定的 fluent 接口 API 的一部分,我们将在下一小节中介绍。

了解 Fluent 接口 API

如果你问,“为什么有人会给一个方法命名为when()?”以及“When类会封装什么样的信息?”欢迎加入俱乐部。当您没有注意到的时候,面向对象编程社区发明了一种全新的 API 设计方法,它完全无视几十年来面向对象实践的原则。这种新方法不是封装数据和将业务逻辑分布到相关的域对象中,而是产生一种 API 风格,它鼓励方法链接,并使用一种方法的返回类型来确定哪种方法可用于火车的下一节车厢。选择方法名称不是为了传达完整的意思,而是为了让整个方法链读起来像一个流畅的句子。这种风格被称为流畅的界面 API。

Note

你可以在 Martin Fowler 的网站上找到关于 fluent 接口的更全面的阐述,在本章的最后引用。

用于创建绑定的 fluent 接口 API 在IntegerExpression系列的类中定义。IntegerExpressionIntegerPropertyIntegerBinding的超类,使得IntegerExpression的方法在IntegerPropertyIntegerBinding类中也可用。四个数值表达式类共享一个公共的超接口NumberExpression,所有的方法都在这里定义。特定于类型的表达式类覆盖了一些产生NumberBinding的方法,以返回更合适的绑定类型。

下面列出了可用于七种属性和绑定的方法:

  • 对于BooleanPropertyBooleanBinding
    • BooleanBinding and(ObservableBooleanValue b)
    • BooleanBinding or(ObservableBooleanValue b)
    • BooleanBinding not()
    • BooleanBinding isEqualTo(ObservableBooleanValue b)
    • BooleanBinding isNotEqualTo(ObservableBooleanValue b)
    • StringBinding asString()
  • 适用于所有数字属性和绑定
    • BooleanBinding isEqualTo(ObservableNumberValue m)
    • BooleanBinding isEqualTo(ObservableNumberValue m, double err)
    • BooleanBinding isEqualTo(double d, double err)
    • BooleanBinding isEqualTo(float f, double err)
    • BooleanBinding isEqualTo(long l)
    • BooleanBinding isEqualTo(long l, double err)
    • BooleanBinding isEqualTo(int i)
    • BooleanBinding isEqualTo(int i, double err)
    • BooleanBinding isNotEqualTo(ObservableNumberValue m)
    • BooleanBinding isNotEqualTo(ObservableNumberValue m, double err)
    • BooleanBinding isNotEqualTo(double d, double err)
    • BooleanBinding isNotEqualTo(float f, double err)
    • BooleanBinding isNotEqualTo(long l)
    • BooleanBinding isNotEqualTo(long l, double err)
    • BooleanBinding isNotEqualTo(int i)
    • BooleanBinding isNotEqualTo(int i, double err)
    • BooleanBinding greaterThan(ObservableNumberValue m)
    • BooleanBinding greaterThan(double d)
    • BooleanBinding greaterThan(float f)
    • BooleanBinding greaterThan(long l)
    • BooleanBinding greaterThan(int i)
    • BooleanBinding lessThan(ObservableNumberValue m)
    • BooleanBinding lessThan(double d)
    • BooleanBinding lessThan(float f)
    • BooleanBinding lessThan(long l)
    • BooleanBinding lessThan(int i)
    • BooleanBinding greaterThanOrEqualTo(ObservableNumberValue m)
    • BooleanBinding greaterThanOrEqualTo(double d)
    • BooleanBinding greaterThanOrEqualTo(float f)
    • BooleanBinding greaterThanOrEqualTo(long l)
    • BooleanBinding greaterThanOrEqualTo(int i)
    • BooleanBinding lessThanOrEqualTo(ObservableNumberValue m)
    • BooleanBinding lessThanOrEqualTo(double d)
    • BooleanBinding lessThanOrEqualTo(float f)
    • BooleanBinding lessThanOrEqualTo(long l)
    • BooleanBinding lessThanOrEqualTo(int i)
    • StringBinding asString()
    • StringBinding asString(String str)
    • StringBinding asString(Locale locale, String str)
  • 对于IntegerPropertyIntegerBinding
    • IntegerBinding negate()
    • NumberBinding add(ObservableNumberValue n)
    • DoubleBinding add(double d)
    • FloatBinding add(float f)
    • LongBinding add(long l)
    • IntegerBinding add(int i)
    • NumberBinding subtract(ObservableNumberValue n)
    • DoubleBinding subtract(double d)
    • FloatBinding subtract(float f)
    • LongBinding subtract(long l)
    • IntegerBinding subtract(int i)
    • NumberBinding multiply(ObservableNumberValue n)
    • DoubleBinding multiply(double d)
    • FloatBinding multiply(float f)
    • LongBinding multiply(long l)
    • IntegerBinding multiply(int i)
    • NumberBinding divide(ObservableNumberValue n)
    • DoubleBinding divide(double d)
    • FloatBinding divide(float f)
    • LongBinding divide(long l)
    • IntegerBinding divide(int i)
  • 对于LongPropertyLongBinding
    • LongBinding negate()
    • NumberBinding add(ObservableNumberValue n)
    • DoubleBinding add(double d)
    • FloatBinding add(float f)
    • LongBinding add(long l)
    • LongBinding add(int i)
    • NumberBinding subtract(ObservableNumberValue n)
    • DoubleBinding subtract(double d)
    • FloatBinding subtract(float f)
    • LongBinding subtract(long l)
    • LongBinding subtract(int i)
    • NumberBinding multiply(ObservableNumberValue n)
    • DoubleBinding multiply(double d)
    • FloatBinding multiply(float f)
    • LongBinding multiply(long l)
    • LongBinding multiply(int i)
    • NumberBinding divide(ObservableNumberValue n)
    • DoubleBinding divide(double d)
    • FloatBinding divide(float f)
    • LongBinding divide(long l)
    • LongBinding divide(int i)
  • 对于FloatPropertyFloatBinding
    • FloatBinding negate()
    • NumberBinding add(ObservableNumberValue n)
    • DoubleBinding add(double d)
    • FloatBinding add(float g)
    • FloatBinding add(long l)
    • FloatBinding add(int i)
    • NumberBinding subtract(ObservableNumberValue n)
    • DoubleBinding subtract(double d)
    • FloatBinding subtract(float g)
    • FloatBinding subtract(long l)
    • FloatBinding subtract(int i)
    • NumberBinding multiply(ObservableNumberValue n)
    • DoubleBinding multiply(double d)
    • FloatBinding multiply(float g)
    • FloatBinding multiply(long l)
    • FloatBinding multiply(int i)
    • NumberBinding divide(ObservableNumberValue n)
    • DoubleBinding divide(double d)
    • FloatBinding divide(float g)
    • FloatBinding divide(long l)
    • FloatBinding divide(int i)
  • 对于DoublePropertyDoubleBinding
    • DoubleBinding negate()
    • DoubleBinding add(ObservableNumberValue n)
    • DoubleBinding add(double d)
    • DoubleBinding add(float f)
    • DoubleBinding add(long l)
    • DoubleBinding add(int i)
    • DoubleBinding subtract(ObservableNumberValue n)
    • DoubleBinding subtract(double d)
    • DoubleBinding subtract(float f)
    • DoubleBinding subtract(long l)
    • DoubleBinding subtract(int i)
    • DoubleBinding multiply(ObservableNumberValue n)
    • DoubleBinding multiply(double d)
    • DoubleBinding multiply(float f)
    • DoubleBinding multiply(long l)
    • DoubleBinding multiply(int i)
    • DoubleBinding divide(ObservableNumberValue n)
    • DoubleBinding divide(double d)
    • DoubleBinding divide(float f)
    • DoubleBinding divide(long l)
    • DoubleBinding divide(int i)
  • 对于StringPropertyStringBinding
    • StringExpression concat(Object obj)
    • BooleanBinding isEqualTo(ObservableStringValue str)
    • BooleanBinding isEqualTo(String str)
    • BooleanBinding isNotEqualTo(ObservableStringValue str)
    • BooleanBinding isNotEqualTo(String str)
    • BooleanBinding isEqualToIgnoreCase(ObservableStringValue str)
    • BooleanBinding isEqualToIgnoreCase(String str)
    • BooleanBinding isNotEqualToIgnoreCase(ObservableStringValue str)
    • BooleanBinding isNotEqualToIgnoreCase(String str)
    • BooleanBinding greaterThan(ObservableStringValue str)
    • BooleanBinding greaterThan(String str)
    • BooleanBinding lessThan(ObservableStringValue str)
    • BooleanBinding lessThan(String str)
    • BooleanBinding greaterThanOrEqualTo(ObservableStringValue str)
    • BooleanBinding greaterThanOrEqualTo(String str)
    • BooleanBinding lessThanOrEqualTo(ObservableStringValue str)
    • BooleanBinding lessThanOrEqualTo(String str)
    • BooleanBinding isNull()
    • BooleanBinding isNotNull()
    • IntegerBinding length()
    • BooleanExpression isEmpty()
    • BooleanExpression isNotEmpty()
  • 对于ObjectPropertyObjectBinding
    • BooleanBinding isEqualTo(ObservableObjectValue<?> obj)
    • BooleanBinding isEqualTo(Object obj)
    • BooleanBinding isNotEqualTo(ObservableObjectValue<?> obj)
    • BooleanBinding isNotEqualTo(Object obj)
    • BooleanBinding isNull()
    • BooleanBinding isNotNull()

使用这些方法,您可以创建无限多种绑定,方法是从属性开始,调用适合该属性类型的方法之一来获取绑定,调用适合该绑定类型的方法之一来获取另一个绑定,依此类推。这里值得指出的一个事实是,特定于类型的数值表达式的所有方法都是在返回类型为NumberBindingNumberExpression基本接口中定义的,并且在具有相同参数签名但返回类型更特定的特定于类型的表达式类中被覆盖。这种用相同的参数签名但更具体的返回类型覆盖子类中的方法的方式被称为协变返回类型覆盖,并且自 Java 5 以来一直是 Java 语言的一个特性。这一事实的结果之一是,用 fluent 接口 API 构建的数字绑定比用Bindings类中的工厂方法构建的绑定有更多特定的类型。

有时有必要将特定于类型的表达式转换为保存相同类型值的对象表达式。这可以通过特定于类型的表达式类中的asObject()方法来完成。可以使用 expressions 类中的静态方法进行转换。对于IntegerExpression,这些静态方法如下:

static IntegerExpression integerExpression(ObservableIntegerValue value)
static <T extends java.lang.Number> IntegerExpression integerExpression(ObservableValue<T> value)

清单 3-6 中的程序是对清单 3-5 中三角形区域示例的修改,它使用了流畅的接口 API,而不是调用Bindings类中的工厂方法。

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-6.
TriangleAreaFluentExample.java

请注意清单 3-5 中用于构建区域绑定的 13 行代码和 12 个中间变量是如何减少到清单 3-6 中不使用中间变量的 7 行代码的。我们还使用了Bindings.format()方法来构建一个名为outputStringExpression对象。有两个带签名的重载Bindings.format()方法:

StringExpression format(Locale locale, String format, Object... args)
StringExpression format(String format, Object... args)

它们的工作方式与相应的String.format()方法类似,它们根据格式规范formatLocale locale或者默认的Locale对值args进行格式化。如果args中的任何一个是ObservableValue,其变化将反映在StringExpression中。

当我们运行清单 3-6 中的程序时,以下输出被打印到控制台:

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类的神秘面纱,以及它在构建本质上是 if/then/else 表达式的绑定中所扮演的角色。When类有一个接受ObservableBooleanValue参数的构造器:

public When(ObservableBooleanValue b)

它有以下 11 个重载的then()方法。

When.NumberConditionBuilder then(ObservableNumberValue n)
When.NumberConditionBuilder then(double d)
When.NumberConditionBuilder then(float f)
When.NumberConditionBuilder then(long l)
When.NumberConditionBuilder then(int i)
When.BooleanConditionBuilder then(ObservableBooleanValue b)
When.BooleanConditionBuilder then(boolean b)
When.StringConditionBuilder then(ObservableStringValue str)
When.StringConditionBuilder then(String str)
When.ObjectConditionBuilder<T> then(ObservableObjectValue<T> obj)
When.ObjectConditionBuilder<T> then(T obj)

then()方法返回的对象类型取决于参数的类型。如果参数是数值类型,无论是可观察的还是不可观察的,返回类型都是嵌套类When.NumberConditionBuilder。同样,对于布尔参数,返回类型是When.BooleanConditionBuilder;对于字符串参数,When.StringConditionBuilder;而对于对象论证,When.ObjectConditionBuilder

这些条件构建器又有下面的otherwise()方法。

  • 对于When.NumberConditionBuilder
    • NumberBinding otherwise(ObservableNumberValue n)
    • DoubleBinding otherwise(double d)
    • NumberBinding otherwise(float f)
    • NumberBinding otherwise(long l)
    • NumberBinding otherwise(int i)
  • 对于When.BooleanConditionBuilder
    • BooleanBinding otherwise(ObservableBooleanValue b)
    • BooleanBinding otherwise(boolean b)
  • 对于When.StringConditionBuilder
    • StringBinding otherwise(ObservableStringValue str)
    • StringBinding otherwise(String str)
  • 对于When.ObjectConditionBuilder
    • ObjectBinding<T> otherwise(ObservableObjectValue<T>  obj)
    • ObjectBinding<T> otherwise(T obj)

这些方法签名的最终效果是,您可以通过以下方式构建一个类似于 if/then/else 表达式的绑定:

new When(b).then(x).otherwise(y)

b是一个ObservableBooleanValuexy是类似的类型,可以是可观测的,也可以是不可观测的。最终的绑定将是类似于xy的类型。

清单 3-7 中的程序使用来自When类的 fluent 接口 API 来计算给定边abc的三角形的面积。回想一下,要形成三角形,三条边必须满足以下条件:

a + b > c, b + c > a, c + a > b.

当满足上述条件时,可以使用 Heron 公式计算三角形的面积:

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

其中s是半参数:

s = (a + b + c) / 2.

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-7.
HeronsFormulaExample.java

由于DoubleExpression中没有现成的绑定方法来计算平方根,我们为areaSquared创建了一个DoubleBindingWhen()的构造器参数是由abc三个条件构建的BooleanBindingthen()方法的参数是计算三角形面积平方的DoubleBinding。因为then()参数是数字,所以otherwise()参数也必须是数字。我们选择使用0.0D来表示遇到了无效的三角形。

Note

除了使用When()构造器,还可以使用Bindings实用程序类中的工厂方法when()来创建When对象。

当我们运行清单 3-7 中的程序时,以下输出被打印到控制台:

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.

如果清单 3-7 中定义的绑定让您有点晕头转向,您并不孤单。我们选择这个例子只是为了说明由When类提供的 fluent 接口 API 的使用。事实上,我们在“理解绑定接口”一节中首次介绍的直接子类化方法可能更适合这个例子。

清单 3-8 中的程序通过使用直接扩展方法解决了与清单 3-7 相同的问题。

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-8.
HeronsFormulaDirectExtensionExample.java

对于复杂的表达式和超出可用运算符范围的表达式,首选直接扩展方法。

现在,您已经掌握了javafx.beansjavafx.beans.bindingjavafx.beans.propertyjavafx.beans.value包中的所有 API,您已经准备好超越 JavaFX 属性和绑定框架的细节,并学习如何将这些属性组织成称为 JavaFX Beans 的更大的组件。

了解 JavaFX Beans 约定

JavaFX 引入了 JavaFX Beans 的概念,这是一组为 Java 对象提供属性支持的约定。在本节中,我们将讨论指定 JavaFX Beans 属性的命名约定、实现 JavaFX Beans 属性的几种方法,以及选择绑定的使用。

JavaFX Beans 规范

多年来,Java 一直使用 JavaBeans API 来表示对象的属性。JavaBeans 属性由一对 getter 和 setter 方法表示。属性更改通过激发 setter 代码中的属性更改事件传播到属性更改侦听器。

JavaFX 引入了 JavaFX Beans 规范,该规范通过 JavaFX 属性和绑定框架中的属性类的帮助,为 Java 对象添加了属性支持。

Caution

财产这个词在这里有两种不同的含义。当我们说“JavaFX Beans 属性”时,应该理解为是指类似于 JavaBeans 属性的更高层次的概念。当我们说“JavaFX 属性和绑定框架属性”时,应该理解为是指PropertyReadOnlyProperty接口的各种实现,比如IntegerPropertyStringProperty等等。JavaFX Beans 属性是使用 JavaFX 属性和绑定框架属性指定的。

像它们的 JavaBeans 对应物一样,JavaFX Beans 属性是由 Java 类中的一组方法指定的。要在 Java 类中定义 JavaFX Beans 属性,需要提供三个方法:getter、setter 和属性 getter。对于类型为double的名为height的属性,有三种方法:

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

getter 和 setter 方法的名称遵循 JavaBeans 约定。它们是通过将“get”和“set”与首字母大写的属性名称连接起来获得的。对于boolean类型属性,getter 名称也可以以is开头。属性 getter 的名称是通过将属性的名称与“Property”连接起来获得的。要定义一个只读的 JavaFX Beans 属性,您可以移除 setter 方法,或者将其更改为私有方法,并将属性 getter 的返回类型更改为ReadOnlyProperty

该规范仅涉及 JavaFX Beans 属性的接口,并没有强加任何实现约束。根据 JavaFX Bean 可能拥有的属性数量以及这些属性的使用模式,有几种实现策略。毫不奇怪,它们都使用 JavaFX 属性和绑定框架属性作为 JavaFX Beans 属性值的后备存储。我们将在接下来的两个小节中向您展示这些策略。

理解急切实例化的属性策略

热切实例化属性策略是实现 JavaFX Beans 属性的最简单方式。对于要在对象中定义的每个 JavaFX Beans 属性,您需要在类中引入一个私有字段,该字段属于适当的 JavaFX 属性和绑定框架属性类型。这些私有字段在 bean 构建时被实例化。getter 和 setter 方法简单地调用私有字段的get()set()方法。属性 getter 只是返回私有字段本身。

清单 3-9 中的程序定义了一个具有int属性iString属性strColor属性color的 JavaFX Bean。

import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
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-9.
JavaFXBeanModelExample.java

这是一个简单的 Java 类。在这个实现中,我们只想指出两件事。首先,按照惯例,getter 和 setter 方法被声明为final。第二,当私有字段被初始化时,我们用完整的上下文信息调用简单的属性构造器,并把this作为第一个参数提供给它们。在本章之前的所有例子中,我们使用null作为简单属性构造器的第一个参数,因为这些属性不是更高级 JavaFX Bean 对象的一部分。

清单 3-10 中的程序定义了一个视图类,它监视清单 3-9 中定义的 JavaFX Bean 的一个实例。它通过连接更改监听器来观察 bean 的istrcolor属性的更改,这些监听器将任何更改打印到控制台。

import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.paint.Color;

public class JavaFXBeanViewExample {
    private JavaFXBeanModelExample model;

    public JavaFXBeanViewExample(JavaFXBeanModelExample model) {
        this.model = model;
        hookupChangeListeners();
    }

    private void hookupChangeListeners() {
        model.iProperty().addListener(new ChangeListener<Number>() {
            @Override
            public void changed(ObservableValue<? extends Number> observableValue, Number
            oldValue, Number newValue) {
                System.out.println("Property i changed: old value = " + oldValue + ", new
                value = " + newValue);
            }
        });

        model.strProperty().addListener(new ChangeListener<String>() {
            @Override
            public void changed(ObservableValue<? extends String> observableValue, String
            oldValue, String newValue) {
                System.out.println("Property str changed: old value = " + oldValue + ", new
                value = " + newValue);
            }
        });

        model.colorProperty().addListener(new ChangeListener<Color>() {
            @Override
            public void changed(ObservableValue<? extends Color> observableValue, Color
            oldValue, Color newValue) {
                System.out.println("Property color changed: old value = " + oldValue + ",
                new value = " + newValue);
            }
        });
    }
}

Listing 3-10.
JavaFXBeanViewExample.java

清单 3-11 中的程序定义了一个可以修改模型对象的控制器。

import javafx.scene.paint.Color;

public class JavaFXBeanControllerExample {
    private JavaFXBeanModelExample model;
    private JavaFXBeanViewExample view;

    public JavaFXBeanControllerExample(JavaFXBeanModelExample model, JavaFXBeanViewExampleÉ
 view) {
        this.model = model;
        this.view = view;
    }

    public void incrementIPropertyOnModel() {
        model.setI(model.getI() + 1);
    }

    public void changeStrPropertyOnModel() {
        final String str = model.getStr();
        if (str.equals("Hello")) {
            model.setStr("World");
        } else {
            model.setStr("Hello");
        }
    }

    public void switchColorPropertyOnModel() {
        final Color color = model.getColor();
        if (color.equals(Color.BLACK)) {
            model.setColor(Color.WHITE);
        } else {
            model.setColor(Color.BLACK);
        }
    }
}

Listing 3-11.
JavaFXBeanControllerExample.java

请注意,这不是一个成熟的控制器,它对视图对象的引用不做任何事情。清单 3-12 中的程序提供了一个主程序,它以典型的模型-视图-控制器模式组装并测试驱动清单 3-9 到 3-11 中的类。

public class JavaFXBeanMainExample {
    public static void main(String[] args) {
        JavaFXBeanModelExample model = new JavaFXBeanModelExample();
        JavaFXBeanViewExample view = new JavaFXBeanViewExample(model);
        JavaFXBeanControllerExample controller = new JavaFXBeanControllerExample(model, view);

        controller.incrementIPropertyOnModel();
        controller.changeStrPropertyOnModel();
        controller.switchColorPropertyOnModel();
        controller.incrementIPropertyOnModel();
        controller.changeStrPropertyOnModel();
        controller.switchColorPropertyOnModel();
    }
}

Listing 3-12.
JavaFXbeanMainExample.java

当我们运行清单 3-9 到 3-12 中的程序时,以下输出被打印到控制台:

Property i changed: old value = 0, new value = 1
Property str changed: old value = Hello, new value = World
Property color changed: old value = 0x000000ff, new value = 0xffffffff
Property i changed: old value = 1, new value = 2
Property str changed: old value = World, new value = Hello
Property color changed: old value = 0xffffffff, new value = 0x000000ff

理解延迟实例化属性策略

如果您的 JavaFX Bean 有许多属性,那么在 Bean 创建时预先实例化所有的 properties 对象可能是一种过于繁重的方法。如果只有少数属性被实际使用,那么所有 properties 对象的内存都被浪费了。在这种情况下,您可以使用几个延迟实例化的属性策略之一。两种典型的策略是半懒惰实例化策略和全懒惰实例化策略。

在半懒惰策略中,只有在使用不同于默认值的值调用 setter 时,或者在调用属性 getter 时,属性对象才会被实例化。清单 3-13 中的程序说明了这个策略是如何实现的。

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-13.
JavaFXBeanModelHalfLazyExample.java

在这种策略中,客户端代码可以多次调用 getter,而无需实例化属性对象。如果 property 对象为空,getter 只返回默认值。一旦用一个不同于默认值的值调用 setter,它将调用属性 getter,该属性 getter 惰性地实例化属性对象。如果客户端代码直接调用属性 getter,属性对象也会被实例化。

在全懒策略中,只有在调用属性 getter 时,属性对象才会被实例化。只有当属性对象已经被实例化时,getter 和 setter 才会检查它;否则,它们会通过一个单独的字段。

清单 3-14 中的程序展示了一个全懒惰属性的例子。

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", _str);
        }
        return str;
    }
}

Listing 3-14.
JavaFXBeanModelFullLazyExample.java

Caution

全惰性实例化策略会产生额外的字段开销,以稍微延长对属性实例化的需求。类似地,半懒惰和全懒惰实例化策略都要付出实现复杂性和运行时性能的代价,以获得潜在的运行时内存占用减少的好处。这是软件工程中一个经典的权衡情况。您选择哪种策略将取决于您的应用环境。我们的建议是,只有在需要的时候才引入优化。

使用选择绑定

正如您在“理解绑定实用程序类”一节中看到的,Bindings实用程序类包含七个选择操作符。这些运算符的方法签名是:

  • select(Object root, String… steps)
  • selectBoolean(Object root, String… steps)
  • selectDouble(Object root, String… steps)
  • selectFloat(Object root, String… steps)
  • selectInteger(Object root, String… steps)
  • selectLong(Object root, String… steps)
  • selectString(Object root, String… steps)

这些选择操作符允许您创建观察深度嵌套的 JavaFX Beans 属性的绑定。假设您有一个具有属性的 JavaFX bean,其类型是具有属性的 JavaFX bean,其类型是具有属性的 JavaFX bean,依此类推。假设你正在通过一个ObjectProperty观察这个属性链的根。然后,您可以创建一个绑定来观察深度嵌套的 JavaFX Beans 属性,方法是调用一个 select 方法,该方法的类型与深度嵌套的 JavaFX Beans 属性的类型相匹配,并将ObjectProperty作为根,将到达深度嵌套的 JavaFX Beans 属性的后续 JavaFX Beans 属性名称作为其余参数。

Note

还有另一组选择方法,将一个ObservableValue作为第一个参数。它们是在 JavaFX 2.0 中引入的。以Object作为第一个参数的 select 方法集允许我们使用任何 Java 对象,而不仅仅是 JavaFX Beans,作为选择绑定的根。

在下面的例子中,我们使用了javafx.scene.effect包中的几个类——LightingLight——来说明选择操作符是如何工作的。在本书后面的章节中,我们会教你如何将光照应用到 JavaFX 场景图中。目前,我们感兴趣的是,Lighting是一个 JavaFX bean,它有一个名为light的属性,其类型为Light,而Light也是一个 JavaFX bean,它有一个名为color的属性,其类型为Color(在javafx.scene.paint中)。

清单 3-15 中的程序说明了如何观察灯光的颜色。

import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
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<>(new Lighting());
        final ObjectBinding<Color> selectBinding = Bindings.select(root, "light", "color");
        selectBinding.addListener(new ChangeListener<Color>() {
            @Override
            public void changed(ObservableValue<? extends Color> observableValue, Color
                oldValue, Color 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-15.
SelectBindingExample.java

在这个例子中,root是观察Lighting物体的ObjectProperty。绑定colorBinding观察Lighting对象的light属性的color属性,即root的值。然后,我们创建了一些LightLighting对象,并以各种方式更改了它们的配置。

当我们运行清单 3-15 中的程序时,以下输出被打印到控制台:

firstLight is black.
secondLight is white.
firstLighting has firstLight.
secondLighting has secondLight.
Making root observe firstLighting.
    The color changed:
        old color = 0xffffffff,
        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

不出所料,root观察到的物体配置的每一个变化都会触发一个变化事件,colorBinding的值总是反映当前Lighting物体在root中的光线颜色。

Caution

如果提供的属性名与 JavaFX bean 中的任何属性名都不匹配,JavaFX 属性和绑定框架不会发出任何警告。它将只包含类型的默认值:null表示对象类型,零表示数值类型,false表示boolean类型,空字符串表示字符串类型。

使 JavaBeans 属性适应 JavaFX 属性

自 JavaBeans 规范发布以来的许多年里,为各种项目、产品和库编写了许多 JavaBeans。为了更好地帮助 Java 开发人员利用这些 JavaBean,在javafx.beans.properties.adapter包中提供了一组适配器,通过在 JavaBeans 属性之外创建一个 JavaFX 属性,使它们在 JavaFX 世界中有用。

在本节中,我们首先通过一个简单的例子简要回顾 JavaBeans 规范对属性、绑定属性和约束属性的定义。然后,我们向您展示如何使用适配器从 JavaBeans 属性创建 JavaFX 属性。

了解 JavaBeans 属性

JavaBeans 属性是使用熟悉的 getter 和 setter 命名约定定义的。如果只提供了 getter,则属性是“只读”的,如果同时提供了 getter 和 setter,则属性是“读/写”的。JavaBeans 事件由事件对象、事件监听器接口和 JavaBean 上的监听器注册方法组成。JavaBeans 属性可以使用两种特殊的事件:当 JavaBeans 属性改变时,可以触发一个PropertyChange事件;当 JavaBeans 属性改变时,也可以触发一个VetoableChange事件;而如果监听器抛出一个PropertyVetoException,属性更改应该不会生效。setter 触发PropertyChange事件的属性称为绑定属性。其 setter 触发VetoableChange事件的属性称为受约束属性。助手类PropertyChangeSupportVetoableChangeSupport允许在 JavaBean 类中轻松定义绑定属性和约束属性。

清单 3-16 用三个属性定义了一个 JavaBeanPerson:nameaddressphoneNumberaddress属性是一个绑定属性,phoneNumber属性是一个约束属性。

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-16.
Person.java

了解 JavaFX 属性适配器

javafx.beans.property.adapter包中的接口和类可以用来轻松地将 JavaBeans 属性适配到 JavaFX 属性。ReadOnlyJavaBeanProperty接口是ReadOnlyProperty的子接口,增加了两个方法:

void dispose()
void fireValueChangedEvent()

JavaBeanProperty接口扩展了ReadOnlyJavaBeanPropertyProperty接口。这两个接口中的每一个都有针对BooleanIntegerLongFloatDoubleObjectString类型的具体类专门化。这些类没有公共构造器。相反,提供了生成器类来创建这些类型的实例。我们在下面的示例代码中使用了JavaBeanStringProperty类。相同的模式适用于所有其他 JavaFX 属性适配器。JavaBeanStringPropertyBuilder支持以下方法:

public static JavaBeanStringPropertyBuilder create()
public JavaBeanStringProperty build()
public JavaBeanStringPropertyBuilder name(java.lang.String)
public JavaBeanStringPropertyBuilder bean(java.lang.Object)
public JavaBeanStringPropertyBuilder beanClass(java.lang.Class<?>)
public JavaBeanStringPropertyBuilder getter(java.lang.String)
public JavaBeanStringPropertyBuilder setter(java.lang.String)
public JavaBeanStringPropertyBuilder getter(java.lang.reflect.Method)
public JavaBeanStringPropertyBuilder setter(java.lang.reflect.Method)

要使用构建器,首先调用它的静态方法create()。然后调用返回构建器本身的方法链。最后,调用build()方法来创建属性。大多数情况下,调用bean()name()方法来指定 JavaBean 实例和属性名就足够了。getter()setter()方法可以用来指定一个不遵循命名约定的 getter 和 setter。beanClass()方法可以用来指定 JavaBean 类。在构建器上预先设置 JavaBean 类允许您更有效地为同一个 JavaBean 类的多个实例的同一个 JavaBeans 属性创建适配器。

Note

尽管 JavaFX 场景、控件等类的构建器已经被弃用,但是javafx.beans.property.adapter包中的构建器并没有被弃用。它们是生成 JavaBeans 属性适配器所必需的。

清单 3-17 中的程序说明了将Person类的三个 JavaBeans 属性改编成JavaBeanStringProperty对象。

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("Stephen Chin");
        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-17.
JavaBeanPropertiesExamples.java

adaptJavaBeanProperty()方法中,我们实例化了一个Person bean,并将其name JavaBeans 属性改编为 JavaFX JavaBeanStringProperty。为了帮助你理解什么时候一个ChangeEvent被传递到nameProperty,我们给它添加了一个ChangeListener(以 lambda 表达式的形式)。因为name不是一个绑定属性,当我们调用person.setName()时,nameProperty并不知道这个变化。为了通知nameProperty这个变化,我们调用它的fireValueChangedEvent()方法。当我们调用nameProperty.get()时,我们得到我们在person bean 上设置的名称。相反,在我们调用nameProperty.set()之后,对person.getName()的调用将返回我们在nameProperty上设置的内容。

adaptBoundProperty()方法中,我们实例化了一个Person bean,并将其address JavaBeans 属性改编为 JavaFX JavaBeanStringProperty。为了帮助你理解什么时候一个ChangeEvent被传递到addressProperty,我们给它添加了一个ChangeListener(以 lambda 表达式的形式)。因为address是一个绑定属性,所以addressProperty被注册为person bean 的PropertyChangeListener;因此,当我们调用person.setAddress()时,会立即通知addressProperty,而无需我们调用fireValuechangedEvent()方法。

adaptConstrainedProperty()方法中,我们实例化了一个Person bean,并将其phoneNumber JavaBeans 属性改编为JavaBeanStringProperty。我们再次添加了一个ChangeListener。因为phoneNumber是一个受约束的属性,phoneNumberProperty能够否决person.setPhoneNumber()调用。当这种情况发生时,person.setPhoneNumber()调用抛出一个PropertyVetoException。如果它本身绑定到另一个 JavaFX 属性,那么phoneNumberProperty将否决这样的更改。我们调用person.setPhoneNumber()两次,一次是在将phoneNumberProperty绑定到另一个 JavaFX 属性之前,一次是在绑定phoneNumberProperty之后。第一个调用成功地改变了phoneNumberProperty的值,第二个调用抛出了一个PropertyVetoException

当我们运行清单 3-17 中的程序时,以下输出被打印到控制台:

Setting name on the JavaBeans property
Calling fireValueChange
JavaFX property StringProperty [bean: Person@776ec8df, name: name, value: Stephen Chin] changed:
        oldValue = null, newValue = Stephen Chin
nameProperty.get() = Stephen Chin
Setting value on the JavaFX property
JavaFX property StringProperty [bean: Person@776ec8df, name: name, value: Johan Vos] changed:
        oldValue = Stephen Chin, newValue = Johan Vos
person.getName() = Johan Vos

Setting address on the JavaBeans property
JavaFX property StringProperty [bean: Person@41629346, name: address, value: 12345 main Street] changed:
        oldValue = null, newValue = 12345 main Street

Setting phoneNumber on the JavaBeans property
JavaFX property StringProperty [bean: Person@6d311334, name: phoneNumber, value: 800-555-1212] changed:
        oldValue = null, newValue = 800-555-1212
Bind phoneNumberProperty to another property
JavaFX property StringProperty [bean: Person@6d311334, 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 Beans 规范。你现在应该明白下面的重要原则。

  • JavaFX 属性和绑定是框架的基础。
  • 它们符合框架的关键接口。
  • 它们触发两种事件:无效事件和变更事件。
  • 框架提供的所有属性和绑定都延迟重新计算它们的值——只有在请求值时。为了迫使他们急切地重新评估,需要附上一个ChangeListener
  • 从现有的属性和绑定中以三种方式之一创建新的绑定:使用Bindings实用程序类的工厂方法,使用 fluent 接口 API,或者直接扩展抽象类的IntegerBinding系列。
  • JavaFX Beans 规范使用三种方法来定义属性:getter、setter 和属性 getter。
  • JavaFX Beans 属性可以通过急切、半懒惰和全懒惰策略来实现。
  • 旧式 JavaBeans 属性可以很容易地适应 JavaFX 属性。

资源

以下是关于属性和绑定的有用资源。

四、使用场景构建器创建用户界面

给我一根足够长的杠杆和一个支点,我可以撬动地球。—阿基米德

在第二章中,您了解了以编程方式和声明方式创建 JavaFX UI 的两种方式,以及如何使用 JavaFX APIs 以编程方式创建 UI。您熟悉 JavaFX UI 的剧场隐喻,其中的Stage代表 Windows、Mac 或 Linux 程序中的窗口,或者移动设备中的触摸屏,它包含的SceneNode代表 UI 的内容。在本章中,我们将讨论 JavaFX 中 UI 故事的另一面:UI 的声明性创建。

这种 UI 设计方法的核心是 FXML 文件。它是一种 XML 文件格式,专门用于保存 UI 元素的信息。它包含 UI 元素的“是什么”,但不包含“如何”这就是为什么这种创建 JavaFX UIs 的方法被称为声明性的。FXML 的核心是一种 Java 对象序列化格式,可以用于以某种方式编写的任何 Java 类,包括所有旧式的 JavaBeans。然而,实际上,它仅用于指定 JavaFX UIs。

除了在文本编辑器或您喜欢的 Java 集成开发环境(ide)中直接编辑之外,FXML 文件还可以通过一个名为 JavaFX Scene Builder 的图形工具进行操作。JavaFX 场景构建器 1.0 于 2012 年 8 月发布,JavaFX 场景构建器 1.1 于 2013 年 9 月发布。1.0 和 1.1 都支持 JavaFX 2。JavaFX Scene Builder 2.0 于 2014 年 5 月发布,可与 JavaFX 8 配合使用。JavaFX Scene Builder 2.0 代码库以开源形式发布,虽然 Oracle JavaFX 团队仍在为其做出贡献,但 Scene Builder 的开发和发布现在由 Gluon 协调。

Gluon 合并了来自 Oracle、Gluon 工程师和社区贡献者的贡献,并维护一个公共代码库和一个问题跟踪器。此外,Gluon 为 Windows、Mac 和 Linux 创建了二进制版本。Scene Builder 上的所有信息,包括如何下载和安装,现在都在 http://gluonhq.com/products/scene-builder/ 维护。

JavaFX Scene Builder 是一个完全图形化的工具,允许您从可用容器、控件和其他可视节点的调色板中绘制 UI 屏幕,并通过在屏幕上直接操作和通过属性编辑器修改其属性来对其进行布局。

JavaFX 运行时使用FXMLLoader类将 FXML 文件加载到 JavaFX 应用程序中。加载 FXML 文件的结果总是一个 Java 对象,通常是一个容器Node,如GroupPane。这个对象可以作为一个Scene的根,或者作为一个节点附加到一个更大的以编程方式创建的场景图中。对于 JavaFX 应用程序的其余部分,从 FXML 文件加载的节点与以编程方式构造的节点没有什么不同。

我们以螺旋上升的方式介绍了 FXML 文件的内容和格式,它们在运行时是如何加载的,以及它们在设计时是如何形成的。我们从一个完整的例子开始这一章,这个例子展示了如何使用 FXML 完成第二章的清单 2-1 中的 StageCoach 程序。然后,我们详细介绍 FXML 加载工具。然后,我们展示一系列手工制作的小 FXML 文件,突出 FXML 文件格式的所有特性。一旦您理解了 FXML 文件格式,我们将向您展示如何使用 JavaFX Scene Builder 创建这些 FXML 文件,涵盖 JavaFX Scene Builder 2.0 的所有功能。

Note

你需要从 http://gluonhq.com/products/scene-builder/ 下载并安装 Gluon 的开源 JavaFX Scene Builder 9.0 来浏览本章的例子。我们还强烈建议配置您最喜欢的 IDE,以使用 JavaFX Scene Builder 9.0 来编辑 FXML 文件。NetBeans 和 IntelliJ IDEA 捆绑了 JavaFX 支持。Eclipse 用户可以安装 e(fx)clipse 插件。配置完成后,您可以在 IDE 中右键单击项目中的任何 FXML 文件,然后选择“使用场景生成器编辑”上下文菜单项。当然,您也可以使用 IDE 的 XML 文件编辑功能将 FXML 文件编辑为 XML 文件。

使用 FXML 设置舞台

将第二章中的 StageCoach 程序从使用以编程方式创建的 UI 转换为使用以声明方式创建的 UI 的过程非常简单。

使用 JavaFX Scene Builder 以图形方式创建用户界面

我们首先用 JavaFX Scene Builder 创建了一个表示场景根节点的 FXML 文件。图 4-1 显示了创建该 UI 时的屏幕截图。

A323806_4_En_4_Fig1_HTML.jpg

图 4-1。

StageCoach.fxml being created in JavaFX Scene Builder

我们将在本章的后半部分详细介绍如何使用 JavaFX Scene Builder。现在,只需观察工具的主要功能区域。中间是内容面板,显示正在处理的 UI 的外观。左侧是顶部的“库”面板,它包括可在内容面板中使用的所有可能的节点,这些节点被划分为简单的子集,如容器、控件、形状、图表等;下面是“文档”面板,它将内容面板中正在处理的场景图形显示为称为层次结构的树结构,以及为 UI 中的各种控件提供事件处理程序代码的控制器。右侧是检查器区域,其中包含允许您操作当前选定控件的属性、布局和代码连接的子区域。

了解 FXML 文件

清单 4-1 显示了 JavaFX Scene Builder 从我们创建的 UI 中保存的 FXML 文件。

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.Button?>
<?import javafx.scene.control.CheckBox?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.Group?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.shape.Rectangle?>
<?import javafx.scene.text.Text?>
<Group fx:id="rootGroup"
       onMouseDragged="#mouseDraggedHandler"
       onMousePressed="#mousePressedHandler"

       xmlns:fx="http://javafx.com/fxml/1"
       fx:controller="projavafx.stagecoach.ui.StageCoachController">
    <children>
        <Rectangle fx:id="blue"
                   arcHeight="50.0"
                   arcWidth="50.0"
                   fill="SKYBLUE"
                   height="350.0"
                   strokeType="INSIDE"
                   width="250.0"/>
        <VBox fx:id="contentBox"
              layoutX="30.0"
              layoutY="20.0"
              spacing="10.0">
            <children>
                <Text fx:id="textStageX"
                      strokeType="OUTSIDE"
                      strokeWidth="0.0"
                      text="x:"
                      textOrigin="TOP"/>
                <Text fx:id="textStageY"
                      layoutX="10.0"
                      layoutY="23.0"
                      strokeType="OUTSIDE"
                      strokeWidth="0.0"
                      text="y:"
                      textOrigin="TOP"/>
                <Text fx:id="textStageH"
                      layoutX="10.0"
                      layoutY="50.0"
                      strokeType="OUTSIDE"
                      strokeWidth="0.0"
                      text="height:"
                      textOrigin="TOP"/>
                <Text fx:id="textStageW"
                      layoutX="10.0"
                      layoutY="77.0"
                      strokeType="OUTSIDE"
                      strokeWidth="0.0"
                      text="width:"
                      textOrigin="TOP"/>
                <Text fx:id="textStageF"
                      layoutX="10.0"
                      layoutY="104.0"
                      strokeType="OUTSIDE"
                      strokeWidth="0.0"
                      text="focused:"
                      textOrigin="TOP"/>
                <CheckBox fx:id="checkBoxResizable"
                          mnemonicParsing="false"
                          text="resizable"/>
                <CheckBox fx:id="checkBoxFullScreen"
                          mnemonicParsing="false"
                          text="fullScreen"/>
                <HBox fx:id="titleBox">
                    <children>
                        <Label fx:id="titleLabel"
                               text="title"/>
                        <TextField fx:id="titleTextField"
                                   text="Stage Coach"/>
                    </children>
                </HBox>
                <Button fx:id="toBackButton"
                        mnemonicParsing="false"
                        onAction="#toBackEventHandler"
                        text="toBack()"/>
                <Button fx:id="toFrontButton"
                        mnemonicParsing="false"
                        onAction="#toFrontEventHandler"
                        text="toFront()"/>
                <Button fx:id="closeButton"
                        mnemonicParsing="false"
                        onAction="#closeEventHandler"
                        text="close()"/>
            </children>
        </VBox>
    </children>
</Group>

Listing 4-1.
StageCoach.fxml

Note

JavaFX Scene Builder 创建的 FXML 文件具有较长的行。我们重新格式化了 FXML 文件,以适应书的页面。

这个 FXML 文件的大部分可以直观地理解:它表示一个包含两个孩子的Group,一个Rectangle和一个VBoxVBox依次持有五个Text节点、两个CheckBox es、一个HBox和三个ButtonHBox持有一个Label和一个TextField。这些节点的各种属性被设置为一些合理的值;例如三个Button上的text设置为"toBack()""toFront()""close()"

这个 FXML 文件中的一些结构需要更多的解释。文件顶部的 XML 处理指令

<?import javafx.scene.control.Button?>
<?import javafx.scene.control.CheckBox?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.Group?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.shape.Rectangle?>
<?import javafx.scene.text.Text?>

通知这个文件的消费者,或者在设计时通知 JavaFX Scene Builder,或者在运行时通知FXMLLoader,以导入提到的 Java 类。这些与 Java 源文件中的导入指令具有相同的效果。

为顶级元素Group提供了两个名称空间声明。JavaFX Scene Builder 将这些名称空间放在它创建的每个 FXML 文件中:

xmlns:fx="http://javafx.com/fxml/1"

Caution

FXML 文件没有根据任何 XML 架构进行验证。FXMLLoader、JavaFX Scene Builder 和 Java IDEs(如 NetBeans、Eclipse 和 IntelliJ IDEA)使用此处指定的名称空间来在编辑 FXML 文件时提供帮助。实际的前缀,即第一个名称空间的空字符串和第二个名称空间的“fx”不应该改变。

这个 FXML 文件包含两种带有fx前缀的属性,fx: controllerfx:idfx:controller属性出现在顶层元素Group上。它通知 JavaFX 运行时,在当前 FXML 文件中设计的 UI 将与一个称为其控制器的 Java 类一起工作:

fx:controller="projavafx.stagecoach.ui.StageCoachController"

前面的属性声明StageCoach.fxml将与 Java 类projavafx.stagecoach.ui.StageCoachController一起工作。fx:id属性可以出现在代表 JavaFX Node的每个元素中。fx:id的值是控制器中一个字段的名称,表示 FXML 文件加载后的NodeStageCoach.fxml文件声明了下面的fx:ids(只显示了带有fx:id属性的行):

<Group fx:id="rootGroup"
        <Rectangle fx:id="blue"
        <VBox fx:id="contentBox"
                <Text fx:id="textStageX"
                <Text fx:id="textStageY"
                <Text fx:id="textStageH"
                <Text fx:id="textStageW"
                <Text fx:id="textStageF"
                <CheckBox fx:id="checkBoxResizable"
                <CheckBox fx:id="checkBoxFullScreen"
                <HBox fx:id="titleBox">
                        <Label fx:id="titleLabel"
                        <TextField fx:id="titleTextField"
                <Button fx:id="toBackButton"
                <Button fx:id="toFrontButton"
                <Button fx:id="closeButton"

因此,在FXMLLoader完成加载 FXML 文件之后,可以在 Java 代码中访问和操作 FXML 文件中的顶层Group节点,作为StageCoachController类的rootGroup字段。在这个 FXML 文件中,我们为我们创建的所有节点分配了一个fx:id。这样做只是为了说明的目的。如果没有理由以编程方式操作节点,比如静态标签,那么可以省略控制器中的fx:id属性和相应的字段。

提供对 FXML 文件中节点的编程访问是控制器扮演的一个角色。控制器扮演的另一个角色是提供处理用户输入和来自 FXML 文件中节点的交互事件的方法。这些事件处理程序由名称以on开头的属性指定,例如onMouseDraggedonMousePressedonAction。它们对应于Node类或其子类中的setOnMouseDragged()setOnMousePressed()setOnAction()方法。要将事件处理程序设置为控制器中的一个方法,请使用带有“#”字符的方法名称作为onMouseDraggedonMousePressedonAction属性的值。StageCoach.fxml文件声明了以下事件处理程序(只显示了带有事件处理程序的行):

<Group fx:id="rootGroup"
       onMouseDragged="#mouseDraggedHandler"
       onMousePressed="#mousePressedHandler"
                <Button fx:id="toBackButton"
                        onAction="#toBackEventHandler"
                <Button fx:id="toFrontButton"
                        onAction="#toFrontEventHandler"
                <Button fx:id="closeButton"
                        onAction="#closeEventHandler"

控制器类中的事件处理器方法通常应该符合EventHandler<T>接口中单个方法的签名

void handle(T event)

其中T是适当的事件对象,MouseEvent用于onMouseDraggedonMousePressed事件处理程序,ActionEvent用于onAction事件处理程序。不带任何参数的方法也可以设置为事件处理程序方法。如果不打算使用事件对象,可以使用这样的方法。

现在我们已经理解了 FXML 文件,接下来我们继续学习控制器类。

了解控制器

清单 4-2 显示了使用我们在上一小节中创建的 FXML 文件的控制器类。

package projavafx.stagecoach.ui;

import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Text;
import javafx.stage.Stage;
import javafx.stage.StageStyle;

public class StageCoachController {
    @FXML
    private Rectangle blue;

    @FXML
    private VBox contentBox;

    @FXML
    private Text textStageX;

    @FXML
    private Text textStageY;

    @FXML
    private Text textStageH;

    @FXML
    private Text textStageW;

    @FXML
    private Text textStageF;

    @FXML
    private CheckBox checkBoxResizable;

    @FXML
    private CheckBox checkBoxFullScreen;

    @FXML
    private HBox titleBox;

    @FXML
    private Label titleLabel;

    @FXML
    private TextField titleTextField;

    @FXML
    private Button toBackButton;

    @FXML
    private Button toFrontButton;

    @FXML
    private Button closeButton;

    private Stage stage;
    private StringProperty title = new SimpleStringProperty();
    private double dragAnchorX;
    private double dragAnchorY;

    public void setStage(Stage stage) {
        this.stage = stage;
    }

    public void setupBinding(StageStyle stageStyle) {
        checkBoxResizable.setDisable(stageStyle == StageStyle.TRANSPARENT
            || stageStyle == StageStyle.UNDECORATED);
        textStageX.textProperty().bind(new SimpleStringProperty("x: ")
            .concat(stage.xProperty().asString()));
        textStageY.textProperty().bind(new SimpleStringProperty("y: ")
            .concat(stage.yProperty().asString()));
        textStageW.textProperty().bind(new SimpleStringProperty("width: ")
            .concat(stage.widthProperty().asString()));
        textStageH.textProperty().bind(new SimpleStringProperty("height: ")
            .concat(stage.heightProperty().asString()));
        textStageF.textProperty().bind(new SimpleStringProperty("focused: ")
            .concat(stage.focusedProperty().asString()));
        stage.setResizable(true);
        checkBoxResizable.selectedProperty()
            .bindBidirectional(stage.resizableProperty());
        checkBoxFullScreen.selectedProperty().addListener((ov, oldValue, newValue) ->
            stage.setFullScreen(checkBoxFullScreen.selectedProperty().getValue()));
        title.bind(titleTextField.textProperty());
        stage.titleProperty().bind(title);
        stage.initStyle(stageStyle);
    }

    @FXML
    public void toBackEventHandler(ActionEvent e) {
        stage.toBack();
    }

    @FXML
    public void toFrontEventHandler(ActionEvent e) {
        stage.toFront();
    }

    @FXML
    public void closeEventHandler(ActionEvent e) {
        stage.close();
    }

    @FXML
    public void mousePressedHandler(MouseEvent me) {
        dragAnchorX = me.getScreenX() - stage.getX();
        dragAnchorY = me.getScreenY() - stage.getY();
    }

    @FXML
    public void mouseDraggedHandler(MouseEvent me) {
        stage.setX(me.getScreenX() - dragAnchorX);
        stage.setY(me.getScreenY() - dragAnchorY);
    }
}

Listing 4-2.
StageCoachController.java

这个类是从第二章的StageCoachMain类中提取出来的,这是我们指定为 FXML 文件StageCoach.fxml.的控制器类的类。实际上,它包含了类型和名称与 FXML 文件中的fx:id相匹配的字段。它还包括名称和签名与 FXML 文件中各种节点的事件处理程序相匹配的方法。

唯一需要解释的是@FXML注释。属于javafx.fxml套餐。这是一个带有运行时保留的标记注释,可以应用于字段和方法。当应用于字段时,@FXML注释告诉 JavaFX Scene Builder 该字段的名称可以用作 FXML 文件中适当类型元素的fx:id。当应用于一个方法时,@FXML注释告诉 JavaFX Scene Builder 该方法的名称可以用作适当类型的事件处理程序属性的值。不管修饰符是什么,用@FXML标注的字段和方法都可以被 FXML 加载工具访问。因此,将所有的@FXML注释字段从public更改为private是安全的,不会对 FXML 加载过程产生负面影响。

StageCoachController类包含 FXML 文件中声明的所有fx:id的匹配字段。它还包括 FXML 文件中的事件处理程序属性指向的五个事件处理程序方法。所有这些字段和方法都用@FXML进行了注释。

StageCoachController还包括一些没有用@FXML注释标注的字段和方法。这些字段和方法出现在类中是为了其他目的。例如,stage字段、setStage()setupBindings()方法直接在 Java 代码中使用。

了解 FXMLLoader

现在我们已经了解了 FXML 文件和使用 FXML 文件的控制器类,我们将注意力转向运行时 FXML 文件的加载。javafx.fxml包中的FXMLLoader类完成了加载 FXML 文件的大部分工作。在我们的例子中,FXML 文件的加载是在StageCoachMain类中完成的。清单 4-3 显示了StageCoachMain类。

package projavafx.stagecoach.ui;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.geometry.Rectangle2D;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.stage.StageStyle;

import java.io.IOException;
import java.util.List;

public class StageCoachMain extends Application {
    @Override
    public void start(Stage stage) throws IOException {
        final StageStyle stageStyle = configStageStyle();

        FXMLLoader fxmlLoader = new FXMLLoader(StageCoachMain.class
            .getResource("/projavafx/stagecoach/ui/StageCoach.fxml"));
        Group rootGroup = fxmlLoader.load();

        final StageCoachController controller = fxmlLoader.getController();
        controller.setStage(stage);
        controller.setupBinding(stageStyle);

        Scene scene = new Scene(rootGroup, 250, 350);
        scene.setFill(Color.TRANSPARENT);

        stage.setScene(scene);
        stage.setOnCloseRequest(we -> System.out.println("Stage is closing"));
        stage.show();
        Rectangle2D primScreenBounds = Screen.getPrimary().getVisualBounds();
        stage.setX((primScreenBounds.getWidth() - stage.getWidth()) / 2);
        stage.setY((primScreenBounds.getHeight() - stage.getHeight()) / 4);
    }

    public static void main(String[] args) {
        launch(args);
    }

    private StageStyle configStageStyle() {
        StageStyle stageStyle = StageStyle.DECORATED;
        List<String> unnamedParams = getParameters().getUnnamed();
        if (unnamedParams.size() > 0) {
            String stageStyleParam = unnamedParams.get(0);
            if (stageStyleParam.equalsIgnoreCase("transparent")) {
                stageStyle = StageStyle.TRANSPARENT;
            } else if (stageStyleParam.equalsIgnoreCase("undecorated")) {
                stageStyle = StageStyle.UNDECORATED;
            } else if (stageStyleParam.equalsIgnoreCase("utility")) {
                stageStyle = StageStyle.UTILITY;
            }
        }
        return stageStyle;
    }
}

Listing 4-3.
StageCoachMain.java

在查看FXMLLoader代码之前,让我指出,对于这个例子,我们选择将StageCoach.fxml文件与StageCoachMain.javaStageCoachController.java文件放在一起。它们都位于projavafx/stagecoach/ui目录中。当我们编译源文件时,这种关系被保留下来。因此,当我们运行这个程序时,FXML 文件作为资源/projavafx/stagecoach/ui/StageCoach.fxml出现在类路径中。图 4-2 展示了我们例子中的文件布局。

A323806_4_En_4_Fig2_HTML.jpg

图 4-2。

The file layout of the StageCoach example

FXML 文件的加载由以下代码片段执行:

FXMLLoader fxmlLoader = new FXMLLoader(StageCoachMain.class
    .getResource("/projavafx/stagecoach/ui/StageCoach.fxml"));
Group rootGroup = fxmlLoader.load();

final StageCoachController controller = fxmlLoader.getController();

这里我们使用FXMLLoader类的单参数构造器构造一个fxmlLoader对象,并传入一个由StageCoachMainClass对象上的getResource()调用返回的URL对象。这个 URL 对象是一个 jar URL 或一个文件 URL,这取决于您是否从 jar 运行这个程序。然后我们在fxmlLoader对象上调用load()方法。这个方法读取 FXML 文件,解析它,实例化它指定的所有节点,并根据它指定的包含关系将它们连接起来。因为控制器是在 FXML 文件中指定的,所以该方法还实例化了一个StageCoachController实例,并根据fx:id将节点分配给控制器实例的字段,这一步通常称为将 FXML 节点注入控制器。事件处理程序也被连接起来。load()方法返回 FXML 文件中的顶层对象,在我们的例子中是一个Group。该返回的Group对象被分配给rootGroup变量,并在后续代码中使用,使用方式与第二章中以编程方式创建的rootGroup相同。然后我们调用getController()方法来获取控制器,控制器的节点字段已经被FXMLLoader注入。该控制器被分配给controller变量,并在后续代码中使用,就像我们刚刚以编程方式创建了它并分配了它的节点字段一样。

既然我们已经完成了将 Stage 蔻驰程序从编程式 UI 创建切换到声明式 UI 创建,我们就可以运行它了。它的行为就像第二章一样。图 4-3 显示了使用transparent命令行参数运行的程序。

A323806_4_En_4_Fig3_HTML.jpg

图 4-3。

The Stage Coach program run with transparent command-line argument

在这一节中,我们谈到了 FXML 设计时和运行时工具的所有方面。然而,我们只描述了每个设施的一部分,仅仅足以让我们的示例程序运行。在本章的其余部分,我们将详细研究每一个工具。

了解 FXML 加载工具

FXML 文件加载工具由两个类组成,一个接口、一个异常和javafx.fxml包中的一个注释。FXMLLoader是完成大部分工作的类,例如读取和解析 FXML 文件,识别 FXML 文件中的处理指令,并以必要的动作做出响应,识别 FXML 文件的每个元素和属性,并将对象创建任务委托给一组构建器,必要时创建控制器对象,并将节点和其他对象注入控制器。JavaFXBuilderFactory负责创建构建器,以响应FXMLLoader对特定类的构建器的请求。控制器类可以实现Initializable接口来接收来自FXMLLoader的信息,就像以前版本的 JavaFX 一样;然而,这个功能已经被注入方法所取代,所以我们不讨论它。如果 FXML 文件包含错误,使得FXMLLoader无法构建 FXML 文件中指定的所有对象,则会抛出LoadException@FXML注释可以在控制器类中使用,将某些字段标记为注入目标,将某些方法标记为事件处理程序候选。

了解 FXMLLoader 类

FXMLLoader类有以下公共构造器:

  • FXMLLoader()
  • FXMLLoader(URL location)
  • FXMLLoader(URL location, ResourceBundle resources)
  • FXMLLoader(URL location, ResourceBundle resources, BuilderFactory builderFactory)
  • FXMLLoader(URL location, ResourceBundle resources, BuilderFactory BuilderFactory builderFactory, Callback<Class<?>, Object> controllerFactory)
  • FXMLLoader(Charset charset)
  • FXMLLoader(URL location, ResourceBundle resources, BuilderFactory BuilderFactory builderFactory, Callback<Class<?> controllerFactory, Object>, Charset charset)
  • FXMLLoader(URL location, ResourceBundle resources, BuilderFactory BuilderFactory builderFactory, Callback<Class<?>, Object> controllerFactory, Charset charset, LinkedList<FXMLLoader> loaders)

参数较少的构造器委托给参数较多的构造器,缺少的参数用默认值填充。location参数是要加载的 FXML 文件的URL。默认为nullresources参数是与 FXML 文件一起使用的资源包。如果在 FXML 文件中使用国际化字符串,这是必要的。默认为nullbuilderFactory参数是生成器工厂,FXMLLoader用它来获得它创建的各种对象的生成器。它默认为JavaFXBuilderFactory的一个实例。这个构建器工厂了解所有可能出现在 FXML 文件中的标准 JavaFX 类型,所以很少使用定制的构建器工厂。controllerFactory是一个javafx.util.CallBack,当提供控制器的类时,它能够返回控制器的实例。默认为null,在这种情况下FXMLLoader将通过调用控制器类的无参数构造器,通过反射实例化控制器。因此,只有当控制器不能以这种方式构建时,才需要提供一个controllerFactory。解析 FXML 时使用charset。它默认为 UTF-8。loaders参数是一个FXMLLoader列表,默认为空列表。

FXMLLoader类有下面的 getter 和 setter 方法来改变FXMLLoader的状态:

  • URL getLocation()
  • void setLocation(URL location)
  • ResourceBundle getResources()
  • void setResources(ResourceBundle resources)
  • ObservableMap<String, Object> getNamespace()
  • <T> T getRoot()
  • void setRoot(Object root)
  • <T> T getController()
  • void setController(Object controller)
  • BuilderFactory getBuilderFactory()
  • void setBuilderFactory(BuilderFactory builderFactory)
  • Callback<Class<?>, Object> getControllerFactory()
  • void setControllerFactory(Callback<Class<?>, Object> controllerFactory)
  • Charset getCharset()
  • void setCharset(Charset charset)
  • ClassLoader getClassLoader()
  • void setClassLoader(ClassLoader classLoader)

从这个列表中可以看出,locationresourcesbuilderFactorycontrollerFactorycharset也可以在FXMLLoader构造完成后进行设置。另外,我们可以获取并设置rootcontrollerclassLoader,获取FXMLLoadernamespace。只有当 FXML 文件使用fx:root作为其根元素时,root才相关,在这种情况下,必须在加载 FXML 文件之前调用setRoot()。我们将在下一节更详细地介绍fx:root的用法。只有当 FXML 文件的顶层元素中不存在fx:controller属性时,才需要在加载 FXML 文件之前设置controllerclassLoadernamespace主要由FXMLLoader内部使用,通常不会被用户代码调用。

FXML 文件的实际加载发生在调用其中一个load()方法的时候。FXMLLoader类有以下加载方法:

  • <T> T load() throws IOException
  • <T> T load(InputStream input) throws IOException
  • static <T> T load(URL location) throws IOException
  • static <T> T load(URL location, ResourceBundle resources) throws IOException
  • static <T> T load(URL location, ResourceBundle resources, BuilderFactory builderFactory) throws IOException
  • static <T> T load(URL location, ResourceBundle resources, BuilderFactory builderFactory, Callback<Class<?>, Object> controllerFactory) throws IOException
  • static <T> T load(URL location, ResourceBundle resources, BuilderFactory builderFactory, Callback<Class<?>, Object> controllerFactory, Charset charset) throws IOException

不带参数的load()方法可以在已经初始化了所有必要字段的FXMLLoader实例上调用。采用InputStream参数的load()方法将从指定的input加载 FXML。所有静态的load()方法都是方便的方法,它们将使用提供的参数实例化一个FXMLLoader,然后调用它的一个非静态的load()方法。

在我们的下一个例子中,我们故意没有在 FXML 文件中指定fx:controller。我们还向控制器类添加了一个单参数构造器。FXML 文件、控制器类和主类如清单 4-4 、 4-5 和 4-6 所示。

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.web.WebView?>
<VBox maxHeight="-Infinity"
      maxWidth="-Infinity"
      minHeight="-Infinity"
      minWidth="-Infinity"
      prefHeight="400.0"
      prefWidth="600.0"
      spacing="10.0"

      xmlns:fx="http://javafx.com/fxml/1">
    <children>
        <HBox spacing="10.0">
            <children>
                <TextField fx:id="address"
                           onAction="#actionHandler"
                           HBox.hgrow="ALWAYS">
                    <padding>
                        <Insets bottom="4.0" left="4.0" right="4.0" top="4.0"/>
                    </padding>
                </TextField>
                <Button fx:id="loadButton"
                        mnemonicParsing="false"
                        onAction="#actionHandler"
                        text="Load"/>
            </children>
        </HBox>
        <WebView fx:id="webView"
                 prefHeight="200.0"
                 prefWidth="200.0"
                 VBox.vgrow="ALWAYS"/>
    </children>
    <padding>
        <Insets bottom="10.0" left="10.0" right="10.0" top="10.0"/>
    </padding>
</VBox>

Listing 4-4.
FXMLLoaderExample.fxml

import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.TextField;
import javafx.scene.web.WebView;

public class FXMLLoaderExampleController {
    @FXML
    private TextField address;

    @FXML
    private WebView webView;

    @FXML
    private Button loadButton;

    private String name;

    public FXMLLoaderExampleController(String name) {
        this.name = name;
    }

    @FXML
    public void actionHandler() {
        webView.getEngine().load(address.getText());
    }
}

Listing 4-5.
FXMLLoaderExampleController.

java

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class FXMLLoaderExampleMain extends Application {
    @Override
    public void start(Stage primaryStage) throws Exception {
        FXMLLoader fxmlLoader = new FXMLLoader();
        fxmlLoader.setLocation(
            FXMLLoaderExampleMain.class.getResource("/FXMLLoaderExample.fxml"));
        fxmlLoader.setController(
            new FXMLLoaderExampleController("FXMLLoaderExampleController"));
        final VBox vBox = fxmlLoader.load();
        Scene scene = new Scene(vBox, 600, 400);
        primaryStage.setTitle("FXMLLoader Example");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

Listing 4-6.
FXMLLoaderExampleMain.

java

因为我们没有在 FXML 文件的顶层元素中指定fx:controller属性,所以我们需要在加载 FXML 文件之前在fxmlLoader上设置控制器:

FXMLLoader fxmlLoader = new FXMLLoader();
fxmlLoader.setLocation(
    FXMLLoaderExampleMain.class.getResource("/FXMLLoaderExample.fxml"));
fxmlLoader.setController(
    new FXMLLoaderExampleController("FXMLLoaderExampleController"));
final VBox vBox = fxmlLoader.load();

如果没有设置控制器,将抛出一个LoaderException,并显示消息“没有指定控制器”这是因为我们指定了控制器方法actionHandler作为文本字段和按钮的动作事件处理程序。FXMLLoader需要控制器来满足 FXML 文件中的这些规范。如果没有指定事件处理程序,FXML 文件将会成功加载,因为不需要控制器。

这个程序是一个非常原始的网络浏览器,有一个地址栏TextField,一个加载栏Button,和一个WebView。图 4-4 显示了工作中的 FXMLLoaderExample 程序。

A323806_4_En_4_Fig4_HTML.jpg

图 4-4。

The FXMLLoaderExample program

我们的下一个示例 ControllerFactoryExample 与 FXMLLoaderExample 几乎相同,只有两处不同,所以我们在这里没有展示完整的代码。您可以在代码下载包中找到它。不像在FXMLLoaderExample中,我们在 FXML 文件中指定了fx:controller。这迫使我们删除主类中的setController()调用,因为否则我们会得到一个LoadException消息“控制器值已经指定”但是,因为我们的控制器没有默认的构造器,FXMLLoader会抛出一个因无法实例化控制器而导致的LoadException。这个异常可以通过我们在fxmlLoader上设置的简单控制器工厂来纠正:

fxmlLoader.setControllerFactory(
    clazz -> new ControllerFactoryExampleController("ExampleController"));

这里我们使用了一个简单的 lambda 表达式来实现函数接口Callback<Class<?>, Object>,它只有一个方法:

Object call(Class<?>)

在我们的实现中,我们简单地返回一个ControllerFactoryExampleController的实例。

理解@FXML 注释

我们已经看到了@FXML注释的两种用法。它可以应用于 FXML 文件的控制器中的字段,这些字段的名称和类型与要注入节点的 FXML 元素的fx:id属性和元素名称相匹配。它可以应用于不带参数或者只带一个类型为javafx.event.Event或其子类型的参数的 void 方法,使它们有资格用作 FXML 文件中元素的事件处理程序。

FXMLLoader将把它的locationresources注入控制器,如果它有接收它们的字段的话:

@FXML
private URL location;

@FXML
private ResourceBundle resources;

FXMLLoader还将调用带有以下签名的@FXML带注释的初始化方法:

@FXML
public void initialize() {
    // ...
}

清单 4-7 、 4-8 和 4-9 中的 FXMLInjectionExample 说明了这些特性是如何工作的。在这个例子中,我们将四个Label放在 FXML 文件的一个VBox中。我们将两个Label注入控制器。我们还在控制器类中指定了locationresources注入字段。最后,在initialize()方法中,我们将两个注入的Label的文本设置为locationresource的字符串表示。

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Font?>
<VBox alignment="CENTER_LEFT"
      maxHeight="-Infinity"
      maxWidth="-Infinity"
      minHeight="-Infinity"
      minWidth="-Infinity"
      prefHeight="150.0"
      prefWidth="700.0"
      spacing="10.0"

      xmlns:fx="http://javafx.com/fxml/1"
      fx:controller="FXMLInjectionExampleController">
    <children>
        <Label text="Location:">
            <font>
                <Font name="System Bold" size="14.0"/>
            </font>
        </Label>
        <Label fx:id="locationLabel" text="[location]"/>
        <Label text="Resources:">
            <font>
                <Font name="System Bold" size="14.0"/>
            </font>
        </Label>
        <Label fx:id="resourcesLabel" text="[resources]"/>
    </children>
    <opaqueInsets>
        <Insets/>
    </opaqueInsets>
    <padding>
        <Insets bottom="10.0" left="10.0" right="10.0" top="10.0"/>
    </padding>
</VBox>

Listing 4-7.
FXMLInjectionExample.fxml

import javafx.fxml.FXML;
import javafx.scene.control.Label;

import java.net.URL;
import java.util.ResourceBundle;

public class FXMLInjectionExampleController {
    @FXML
    private Label resourcesLabel;

    @FXML
    private Label locationLabel;

    @FXML
    private URL location;

    @FXML
    private ResourceBundle resources;

    @FXML
    public void initialize() {
        locationLabel.setText(location.toString());
        resourcesLabel.setText(resources.getBaseBundleName());
    }
}

Listing 4-8.
FXMLInjectionExampleController.

java

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

import java.util.ResourceBundle;

public class FXMLInjectionExampleMain extends Application {
    @Override
    public void start(Stage primaryStage) throws Exception {
        FXMLLoader fxmlLoader = new FXMLLoader();
        fxmlLoader.setLocation(
            FXMLInjectionExampleMain.class.getResource("/FXMLInjectionExample.fxml"));
        fxmlLoader.setResources(ResourceBundle.getBundle("FXMLInjectionExample"));
        VBox vBox = fxmlLoader.load();
        Scene scene = new Scene(vBox);
        primaryStage.setTitle("FXML Injection Example");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

Listing 4-9.
FXMLInjectionExampleMain.

java

注意,我们还创建了一个空的FXMLInjectionExample.properties文件,用作资源包来说明资源字段到控制器的注入。我们将在下一节解释如何使用带有 FXML 文件的资源包。当 FXMLInjectionExample 在我们的机器上运行时,屏幕上会显示图 4-5 中的 FXML 注入示例窗口。

A323806_4_En_4_Fig5_HTML.jpg

图 4-5。

The FXMLInjection program

@FXML注释也可用于包含的 FXML 文件控制器注入,以及标记javafx.event.EventHandler类型的控制器属性,用作 FXML 文件中的事件处理程序。在下一节讨论 FXML 文件的相关特性时,我们将详细介绍它们。

探索 FXML 文件的功能

在本节中,我们将介绍 FXML 文件格式的特性。因为FXMLLoader的主要目标是将 FXML 文件反序列化为 Java 对象,所以它提供了有助于简化 FXML 文件编写的工具也就不足为奇了。

FXML 格式的反序列化能力

因为我们在这一节中讨论的特性与反序列化通用 Java 对象有更多的关系,所以我们将离开 GUI 世界,使用普通的 Java 类。我们在讨论中使用清单 4-10 中定义的 JavaBean。这是一个虚构的类,旨在说明不同的 FXML 特性。

package projavafx.fxmlbasicfeatures;

import javafx.scene.paint.Color;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class FXMLBasicFeaturesBean {
    private String name;
    private String address;
    private boolean flag;
    private int count;
    private Color foreground;
    private Color background;
    private Double price;
    private Double discount;
    private List<Integer> sizes;
    private Map<String, Double> profits;
    private Long inventory;
    private List<String> products = new ArrayList<String>();
    private Map<String, String> abbreviations = new HashMap<>();

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    public boolean isFlag() {
        return flag;
    }

    public void setFlag(boolean flag) {
        this.flag = flag;
    }

    public int getCount() {
        return count;
    }

    public void setCount(int count) {
        this.count = count;
    }

    public Color getForeground() {
        return foreground;
    }

    public void setForeground(Color foreground) {
        this.foreground = foreground;
    }

    public Color getBackground() {
        return background;
    }

    public void setBackground(Color background) {
        this.background = background;
    }

    public Double getPrice() {
        return price;
    }

    public void setPrice(Double price) {
        this.price = price;
    }

    public Double getDiscount() {
        return discount;
    }

    public void setDiscount(Double discount) {
        this.discount = discount;
    }

    public List<Integer> getSizes() {
        return sizes;
    }

    public void setSizes(List<Integer> sizes) {
        this.sizes = sizes;
    }

    public Map<String, Double> getProfits() {
        return profits;
    }

    public void setProfits(Map<String, Double> profits) {
        this.profits = profits;
    }

    public Long getInventory() {
        return inventory;
    }

    public void setInventory(Long inventory) {
        this.inventory = inventory;
    }

    public List<String> getProducts() {
        return products;
    }

    public Map<String, String> getAbbreviations() {
        return abbreviations;
    }

    @Override
    public String toString() {
        return "FXMLBasicFeaturesBean{" +
            "name='" + name + '\'' +
            ",\n\taddress='" + address + '\'' +
            ",\n\tflag=" + flag +
            ",\n\tcount=" + count +
            ",\n\tforeground=" + foreground +
            ",\n\tbackground=" + background +
            ",\n\tprice=" + price +
            ",\n\tdiscount=" + discount +
            ",\n\tsizes=" + sizes +
            ",\n\tprofits=" + profits +
            ",\n\tinventory=" + inventory +
            ",\n\tproducts=" + products +
            ",\n\tabbreviations=" + abbreviations +
            '}';
    }
}

Listing 4-10.
FXMLBasicFeaturesBean.java

清单 4-11 中的 FXML 文件被加载并打印到清单 4-12 中程序的控制台上。

<?import javafx.scene.paint.Color?>
<?import projavafx.fxmlbasicfeatures.FXMLBasicFeaturesBean?>
<?import projavafx.fxmlbasicfeatures.Utilities?>
<?import java.lang.Double?>
<?import java.lang.Integer?>
<?import java.lang.Long?>
<?import java.util.HashMap?>
<?import java.lang.String?>
<FXMLBasicFeaturesBean name="John Smith"
                       flag="true"
                       count="12345"
                       xmlns:fx="http://javafx.com/fxml/1">
    <address>12345 Main St.</address>
    <foreground>#ff8800</foreground>
    <background>
        <Color red="0.0" green="1.0" blue="0.5"/>
    </background>
    <price>
        <Double fx:value="3.1415926"/>
    </price>
    <discount>
        <Utilities fx:constant="TEN_PCT"/>
    </discount>
    <sizes>
        <Utilities fx:factory="createList">
            <Integer fx:value="1"/>
            <Integer fx:value="2"/>
            <Integer fx:value="3"/>
        </Utilities>
    </sizes>
    <profits>
        <HashMap q1="1000" q2="1100" q3="1200" a4="1300"/>
    </profits>
    <fx:define>
        <Long fx:id="inv" fx:value="9765625"/>
    </fx:define>
    <inventory>
        <fx:reference source="inv"/>
    </inventory>
    <products>
        <String fx:value="widget"/>
        <String fx:value="gadget"/>
        <String fx:value="models"/>
    </products>
    <abbreviations CA="California" NY="New York" FL="Florida" MO="Missouri"/>

</FXMLBasicFeaturesBean>

Listing 4-11.
FXMLBasicFeatures.fxml

package projavafx.fxmlbasicfeatures;

import javafx.fxml.FXMLLoader;

import java.io.IOException;

public class FXMLBasicFeaturesMain {
    public static void main(String[] args) throws IOException {
        FXMLBasicFeaturesBean bean = FXMLLoader.load(
            FXMLBasicFeaturesMain.class.getResource(
                "/projavafx/fxmlbasicfeatures/FXMLBasicFeatures.fxml")
        );
        System.out.println("bean = " + bean);
    }
}

Listing 4-12.
FXMLBasicFeaturesMain.java

我们使用了一个小的工具类,它包含一些常量和一个创建List<Integer>的工厂方法,如清单 4-13 所示。

package projavafx.fxmlbasicfeatures;

import java.util.ArrayList;
import java.util.List;

public class Utilities {
    public static final Double TEN_PCT = 0.1d;
    public static final Double TWENTY_PCT = 0.2d;
    public static final Double THIRTY_PCT = 0.3d;

    public static List<Integer> createList() {
        return  new ArrayList<>();
    }
}

Listing 4-13.
Utilities.java

正在 FXML 文件中创建FXMLBasicFeaturesBean对象;FXML 文件的顶层元素是FXMLBasicFeaturesBean这一事实表明了这一点。nameaddress字段说明了可以将字段设置为属性或子元素:

<FXMLBasicFeaturesBean name="John Smith"
                       flag="true"
                       count="12345"
                       xmlns:fx="http://javafx.com/fxml/1">
    <address>12345 Main St.</address>

foregroundbackground字段说明了设置javafx.scene.paint.Color子元素的两种方式,要么通过十六进制字符串,要么使用Color元素(记住Color是一个没有默认构造器的不可变对象):

<foreground>#ff8800</foreground>
<background>
    <Color red="0.0" green="1.0" blue="0.5"/>
</background>

price字段说明了一种构造Double对象的方法。fx:value属性调用Double上的valueOf(String)方法。这适用于任何具有工厂方法valueOf(String)的 Java 类:

<price>
    <Double fx:value="3.1415926"/>
</price>

discount字段说明了如何使用 Java 类中定义的常量。属性访问其元素类型的常量(public static final)字段。下面将折扣字段设置为Utilities.TEN_PCT,即0.1:

<discount>
    <Utilities fx:constant="TEN_PCT"/>
</discount>

sizes字段说明了使用工厂方法创建对象。属性在其元素的类型上调用指定的工厂方法。在我们的例子中,它调用Utilities.createList()来创建一个Integer列表,然后用三个Integer填充它。注意sizes是一个读写属性。稍后您将看到一个如何填充只读列表属性的示例。

<sizes>
    <Utilities fx:factory="createList">
        <Integer fx:value="1"/>
        <Integer fx:value="2"/>
        <Integer fx:value="3"/>
    </Utilities>
</sizes>

profits字段说明了如何填充读写映射。这里,我们将利润字段设置为一个用键/值对创建的HashMap:

<profits>
    <HashMap q1="1000" q2="1100" q3="1200" a4="1300"/>
</profits>

inventory字段说明了如何在一个地方定义一个对象并在另一个地方引用它。元素创建了一个具有fx:id属性的独立对象。fx:reference元素创建了一个对别处定义的对象的引用,它的source属性指向一个先前定义的对象的fx:id:

<fx:define>
    <Long fx:id="inv" fx:value="9765625"/>
</fx:define>
<inventory>
    <fx:reference source="inv"/>
</inventory>

products字段说明了如何填充只读列表。FXML 的以下片段相当于调用bean.getProducts().addAll("widget", "gadget", "models"):

<products>
    <String fx:value="widget"/>
    <String fx:value="gadget"/>
    <String fx:value="models"/>
</products>

abbreviations字段说明了如何填充只读地图:

<abbreviations CA="California" NY="New York" FL="Florida" MO="Missouri"/>

当 FXMLBasicFeaturesMain 程序运行时,以下输出将按预期打印到控制台:

bean = FXMLBasicFeaturesBean{name='John Smith',
        address='12345 Main St.',
        flag=true,
        count=12345,
        foreground=0xff8800ff,
        background=0x00ff80ff,
        price=3.1415926,
        discount=0.1,
        sizes=[1, 2, 3],
        profits={q1=1000, q2=1100, q3=1200, a4=1300},
        inventory=9765625,
        products=[widget, gadget, models],
        abbreviations={MO=Missouri, FL=Florida, NY=New York, CA=California}}

了解默认和静态属性

许多 JavaFX 类都有一个默认属性。默认属性是用类上的@DefaultProperty注释指定的。@DefaultProperty注释属于javafx.beans包。例如,javafx.scene.Group类的默认属性是它的children属性。在 FXML 文件中,当通过子元素指定默认属性时,默认属性本身的开始和结束标记可以省略。作为一个例子,下面的代码片段,您可以在清单 4-1 中看到。

<HBox fx:id="titleBox">
    <children>
        <Label fx:id="titleLabel"
               text="title"/>
        <TextField fx:id="titleTextField"
                   text="Stage Coach"/>
    </children>
</HBox>

可以简化为

<HBox fx:id="titleBox">
    <Label fx:id="titleLabel"
           text="title"/>
    <TextField fx:id="titleTextField"
               text="Stage Coach"/>
</HBox>

静态属性是在对象上设置的属性,不是通过调用对象本身的 setter 方法,而是通过调用不同类的静态方法,将对象和属性值作为参数传递。许多 JavaFX 的容器Node都有这样的静态方法。这些方法在将一个Node添加到容器之前被调用,以影响某些结果。静态属性在 FXML 文件中表示为内部对象(作为静态方法的第一个参数传入的对象)的属性,其名称包括类名和静态方法名,用点分隔。您可以在清单 4-4 中找到一个静态属性的例子:

<WebView fx:id="webView"
         prefHeight="200.0"
         prefWidth="200.0"
         VBox.vgrow="ALWAYS"/>

这里我们将一个WebView添加到一个VBoxVBox.vgrow属性表明FXMLLoader需要在将webView添加到VBox之前调用下面的。

VBox.vgrow(webView, Priority.ALWAYS)

静态属性除了作为属性出现之外,还可以作为子元素出现。

了解属性解析和绑定

正如你在本章前面所看到的,对象属性可以表示为属性和子元素。有时,将属性建模为子元素或属性同样有效。然而,FXMLLoader将对属性进行额外的处理,使得使用属性更有吸引力。处理属性时,FXMLLoader会执行三种属性值解析和表达式绑定。

当属性的值以@字符开始时,FXMLLoader会将该值视为相对于当前文件的位置。这被称为位置解析。当一个属性的值以一个%字符开始时,FXMLLoader会将该值视为资源包中的一个键,并用特定于地区的值替换该键。这称为资源解析。当一个属性的值以一个$字符开始时,FXMLLoader会将该值视为一个变量名,并将被引用变量的值替换为该属性的值。这被称为可变分辨率。

当一个属性的值以${开始,以}结束,并且如果该属性表示一个 JavaFX 属性,FXMLLoader将把该值视为一个绑定表达式,并将 JavaFX 属性绑定到包含的表达式。这叫做表达式绑定。您将在第三章中了解 JavaFX 属性和绑定。现在简单地理解一下,当一个属性被绑定到一个表达式时,每次表达式改变值时,这种改变都会反映在属性中。支持的表达式包括字符串、布尔、数值、一元运算符(减)和!(求反)、算术运算符(+*/%)、逻辑运算符(&&||)和关系运算符(>>=<<===!=)。

清单 4-14 到 4-19 中显示的 ResolutionAndBindingExample 说明了位置解析、资源解析、变量解析以及表达式绑定的使用。

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Font?>
<?import java.util.Date?>
<VBox id="vbox" alignment="CENTER_LEFT" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity"
      minWidth="-Infinity" prefHeight="200.0" prefWidth="700.0" spacing="10.0"
      stylesheets="@ResolutionAndBindingExample.css" 
      xmlns:fx="http://javafx.com/fxml/1" fx:controller="ResolutionAndBindingController">
    <children>
        <Label text="%location">
            <font>
                <Font name="System Bold" size="14.0"/>
            </font>
        </Label>
        <Label fx:id="locationLabel" text="[location]"/>
        <Label text="%resources">
            <font>
                <Font name="System Bold" size="14.0"/>
            </font>
        </Label>
        <Label fx:id="resourcesLabel" text="[resources]"/>
        <Label text="%currentDate">
            <font>
                <Font name="System Bold" size="14.0"/>
            </font>
        </Label>
        <HBox alignment="BASELINE_LEFT" spacing="10.0">
            <children>
                <fx:define>
                    <Date fx:id="capturedDate"/>
                </fx:define>
                <Label fx:id="currentDateLabel" text="$capturedDate"/>
                <TextField fx:id="textField"/>
                <Label text="${textField.text}"/>
            </children>
        </HBox>
    </children>
    <opaqueInsets>
        <Insets/>
    </opaqueInsets>
    <padding>
        <Insets bottom="10.0" left="10.0" right="10.0" top="10.0"/>
    </padding>
</VBox>

Listing 4-14.
ResolutionAndBindingExample.fxml

import javafx.fxml.FXML;
import javafx.scene.control.Label;

import java.net.URL;
import java.util.ResourceBundle;

public class ResolutionAndBindingController {
    @FXML
    private Label resourcesLabel;

    @FXML
    private Label locationLabel;

    @FXML
    private Label currentDateLabel;

    @FXML
    private URL location;

    @FXML
    private ResourceBundle resources;

    @FXML
    public void initialize() {
        locationLabel.setText(location.toString());
        resourcesLabel.setText(resources.getBaseBundleName() +
            " (" + resources.getLocale().getCountry() +
            ", " + resources.getLocale().getLanguage() + ")");
    }
}

Listing 4-15.
ResolutionAndBindingController.

java

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

import java.util.ResourceBundle;

public class ResolutionAndBindingExample extends Application {
    @Override
    public void start(Stage primaryStage) throws Exception {
        FXMLLoader fxmlLoader = new FXMLLoader();
        fxmlLoader.setLocation(
            ResolutionAndBindingExample.class.getResource(
                "/ResolutionAndBindingExample.fxml"));
        fxmlLoader.setResources(
            ResourceBundle.getBundle(
                "ResolutionAndBindingExample"));
        VBox vBox = fxmlLoader.load();
        Scene scene = new Scene(vBox);
        primaryStage.setTitle("Resolution and Binding Example");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

Listing 4-16.

ResolutionAndBindingExample.java

location=Location:
resources=Resources:
currentDate=CurrentDate:
Listing 4-17.
ResourceAndBindingExample.

properties

location=Emplacement:
resources=Resources:
currentDate=Date du jour:
Listing 4-18.
ResolutionAndBindingExample_fr_FR.properties

#vbox {
    -fx-background-color: azure ;
}
Listing 4-19.
ResolutionAndBindingExample.css

FXML 文件中使用位置解析来指定 CSS 文件的位置。stylesheet属性被设置为位置“@ResolutionAndBindingExample.css”:

<VBox id="vbox" alignment="CENTER_LEFT" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity"
      minWidth="-Infinity" prefHeight="200.0" prefWidth="700.0" spacing="10.0"
      stylesheets="@ResolutionAndBindingExample.css" 
      xmlns:fx="http://javafx.com/fxml/1" fx:controller="ResolutionAndBindingController">

样式表将VBox的背景色设置为天蓝色。资源解析用于设置程序中三个标签的文本:

<Label text="%location">
<Label text="%resources">
<Label text="%currentDate">

在加载 FXML 文件之前,这些标签将从提供给FXMLLoader的资源包中获取文本。提供了属性文件的默认区域设置和法语区域设置翻译。可变解析发生在定义的java.util.Date实例和Label之间:

<fx:define>
    <Date fx:id="capturedDate"/>
</fx:define>
<Label fx:id="currentDateLabel" text="$capturedDate"/>

定义的Date被赋予了capturedDatefx:id,标签使用变量作为其文本。最后,表达式绑定发生在TextFieldLabel之间:

<TextField fx:id="textField"/>
<Label text="${textField.text}"/>

TextField被赋予了textFieldfx:id,标签被绑定到表达式textField.text,结果标签模仿了文本字段中输入的内容。当使用法语语言环境运行 ResolutionAndBindingExample 时,将显示如图 4-6 所示的解析和绑定示例窗口。

A323806_4_En_4_Fig6_HTML.jpg

图 4-6。

The ResolutionAndBindingExample program

使用多个 FXML 文件

因为加载一个 FXML 文件的结果是一个可以在一个Scene中使用的 JavaFX Node,所以对于任何一个Scene,您并不局限于只使用一个 FXML 文件。例如,您可以将场景分成两个或更多部分,并用各自的 FXML 文件来表示每个部分。然后,您可以在每个部分的 FXML 文件上调用FXMLLoaderload()方法之一,并以编程方式将结果节点组装到您的场景中。

FXML 文件格式支持另一种将单独准备的 FXML 文件组合在一起的机制。一个 FXML 文件可以包含另一个带有fx:include元素的 FXML 文件。元素支持三个属性:source属性保存包含的 FXML 文件的位置;resources属性保存被包含的 FXML 文件使用的资源包的位置;而charset属性保存包含的 FXML 文件的字符集。如果source属性以“/”字符开头,则解释为类路径中的路径;否则,它被解释为相对于包含 FXML 文件的位置。resourcecharset属性是可选的。如果未指定它们,则使用它们用于加载包含 FXML 文件的值。用于加载包含 FXML 文件的构建器工厂和控制器工厂也用于加载包含 FXML 文件。

可以为一个fx:include元素指定一个fx:id。当指定了一个fx:id时,可以指定包含的 FXML 文件的控制器中的一个相应字段,并且FXMLLoader将把加载包含的 FXML 文件的结果注入这个字段。此外,如果被包含的 FXML 文件在其根元素中指定了fx:controller,则该被包含的 FXML 文件的控制器也可以被注入到被包含的 FXML 文件的控制器中,只要在被包含的文件的控制器中有适当命名和类型化的字段可用于接收被注入的被包含的 FXML 文件的控制器。在本节的示例应用程序中,我们使用两个 FXML 文件来表示应用程序的 UI。包含的 FXML 文件包含如下行:

<BorderPane maxHeight="-Infinity"
            ...
            fx:controller="IncludeExampleTreeController">
        <fx:include fx:id="details"
                    source="IncludeExampleDetail.fxml" />

包含的 FXML 有如下几行:

<VBox maxHeight="-Infinity"
      ...
      fx:controller="IncludeExampleDetailController">

因此,加载包含的 FXML 文件将产生一个类型为VBox的根元素和一个类型为IncludeExampleDetailController的控制器。包含 FXML 文件的控制器,IncludeExampleTreeController有如下字段:

@FXML
private VBox details;

@FXML
private IncludeExampleDetailController detailsController;

当包含 FXML 文件被加载时,这些字段将保存包含 FXML 文件的加载根和控制器。

本节示例的完整源代码如清单 4-20 到 4-25 所示。

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TreeTableColumn?>
<?import javafx.scene.control.TreeTableView?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Font?>
<BorderPane maxHeight="-Infinity"
            maxWidth="-Infinity"
            minHeight="-Infinity"
            minWidth="-Infinity"
            prefHeight="400.0"
            prefWidth="600.0"

            xmlns:fx="http://javafx.com/fxml/1"
            fx:controller="IncludeExampleTreeController">
    <top>
        <Label text="Product Details"
               BorderPane.alignment="CENTER">
            <font>
                <Font name="System Bold Italic" size="36.0"/>
            </font>
        </Label>
    </top>
    <left>
        <VBox spacing="10.0">
            <children>
                <Label text="List of Products:">
                    <font>
                        <Font name="System Bold" size="12.0"/>
                    </font>
                </Label>
                <TreeTableView fx:id="treeTableView"
                               prefHeight="200.0"
                               prefWidth="200.0"
                               BorderPane.alignment="CENTER"
                               VBox.vgrow="ALWAYS">
                    <columns>
                        <TreeTableColumn fx:id="category"
                                         editable="false"
                                         prefWidth="125.0"
                                         text="Category"/>
                        <TreeTableColumn fx:id="name"
                                         editable="false"
                                         prefWidth="75.0"
                                         text="Name"/>
                    </columns>
                </TreeTableView>
            </children>
            <BorderPane.margin>
                <Insets/>
            </BorderPane.margin>
        </VBox>
    </left>
    <center>
        <fx:include fx:id="details"
                    source="IncludeExampleDetail.fxml"/>
    </center>
    <padding>
        <Insets bottom="10.0" left="10.0" right="10.0" top="10.0"/>
    </padding>
</BorderPane>

Listing 4-20.
IncludeExampleTree.fxml

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextArea?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Font?>
<VBox maxHeight="-Infinity"
      maxWidth="-Infinity"
      minHeight="-Infinity"
      minWidth="-Infinity"
      prefHeight="346.0"
      prefWidth="384.0"
      spacing="10.0"

      xmlns:fx="http://javafx.com/fxml/1"
      fx:controller="IncludeExampleDetailController">
    <children>
        <Label text="Category:">
            <font>
                <Font name="System Bold" size="12.0"/>
            </font>
        </Label>
        <Label fx:id="category" text="[Category]"/>
        <Label text="Name:">
            <font>
                <Font name="System Bold" size="12.0"/>
            </font>
        </Label>
        <Label fx:id="name" text="[Name]"/>
        <Label text="Description:">
            <font>
                <Font name="System Bold" size="12.0"/>
            </font>
        </Label>
        <TextArea fx:id="description"
                  prefHeight="200.0"
                  prefWidth="200.0"
                  VBox.vgrow="ALWAYS"/>
    </children>
    <padding>
        <Insets bottom="10.0" left="20.0" right="10.0" top="30.0"/>
    </padding>
</VBox>

Listing 4-21.
IncludeExampleDetail.fxml

import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.fxml.FXML;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeTableColumn;
import javafx.scene.control.TreeTableView;
import javafx.scene.layout.VBox;

public class IncludeExampleTreeController {
    @FXML
    private TreeTableView<Product> treeTableView;

    @FXML
    private TreeTableColumn<Product, String> category;

    @FXML
    private TreeTableColumn<Product, String> name;

    @FXML
    private VBox details;

    @FXML
    private IncludeExampleDetailController detailsController;

    @FXML
    public void initialize() {
        Product[] products = new Product[101];
        for (int i = 0; i <= 100; i++) {
            products[i] = new Product();
            products[i].setCategory("Category" + (i / 10));
            products[i].setName("Name" + i);
            products[i].setDescription("Description" + i);
        }
        TreeItem<Product> root = new TreeItem<>(products[100]);
        root.setExpanded(true);
        for (int i = 0; i < 10; i++) {
            TreeItem<Product> firstLevel =
                new TreeItem<>(products[i * 10]);
            firstLevel.setExpanded(true);
            for (int j = 1; j < 10; j++) {
                TreeItem<Product> secondLevel =
                    new TreeItem<>(products[i * 10 + j]);
                secondLevel.setExpanded(true);
                firstLevel.getChildren().add(secondLevel);
            }
            root.getChildren().add(firstLevel);
        }

        category.setCellValueFactory(param ->
            new ReadOnlyStringWrapper(param.getValue().getValue().getCategory()));
        name.setCellValueFactory(param ->
            new ReadOnlyStringWrapper(param.getValue().getValue().getName()));

        treeTableView.setRoot(root);

        treeTableView.getSelectionModel().selectedItemProperty()
            .addListener((observable, oldValue, newValue) -> {
                Product product = null;
                if (newValue != null) {
                    product = newValue.getValue();
                }
                detailsController.setProduct(product);
            });
    }
}

Listing 4-22.
IncludeExampleTreeController.java

import javafx.beans.value.ChangeListener;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.control.TextArea;

public class IncludeExampleDetailController {
    @FXML
    private Label category;

    @FXML
    private Label name;

    @FXML
    private TextArea description;

    private Product product;
    private ChangeListener<String> listener;

    public void setProduct(Product product) {
        if (this.product != null) {
            unhookListener();
        }
        this.product = product;
        hookTo(product);
    }

    private void unhookListener() {
        description.textProperty().removeListener(listener);
    }

    private void hookTo(Product product) {
        if (product == null) {
            category.setText("");
            name.setText("");
            description.setText("");
            listener = null;
        } else {
            category.setText(product.getCategory());
            name.setText(product.getName());
            description.setText(product.getDescription());
            listener = (observable, oldValue, newValue) ->
                product.setDescription(newValue);
            description.textProperty().addListener(listener);
        }
    }
}

Listing 4-23.
IncludeExampleDetailController.java

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;

public class IncludeExample extends Application {
    @Override
    public void start(Stage primaryStage) throws Exception {
        FXMLLoader fxmlLoader = new FXMLLoader();
        fxmlLoader.setLocation(
            IncludeExample.class.getResource("IncludeExampleTree.fxml"));
        final BorderPane borderPane = fxmlLoader.load();
        Scene scene = new Scene(borderPane, 600, 400);
        primaryStage.setTitle("Include Example");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

Listing 4-24.
IncludeExample.java

public class Product {
    private String category;
    private String name;
    private String description;

    public String getCategory() {
        return category;
    }

    public void setCategory(String category) {
        this.category = category;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

Listing 4-25.
Product.java

在这个 IncludeExample 程序中,我们在两个 FXML 文件中构建 UI,每个文件都有自己的控制器支持。UI 的特点是左边有一个TreeTableView,右边有一些Label和一个TextAreaTreeTableView加载有虚拟Product数据。当左边的一行TreeTableView被选中时,相应的Product会显示在右边。您可以使用右侧的TextArea编辑Product的描述字段。当您从左侧的旧行导航到新行时,右侧的Product会反映这一变化。但是,您对先前显示的Product所做的所有更改都会保留在模型中。当您导航回已修改的Product时,您的更改将再次显示。第六章中更详细地介绍了TreeTableView类。

我们使用了一个附加在TextFieldtextProperty上的ChangeListener<String>来同步TextField中的文本和Product中的description。JavaFX 属性和更改侦听器是 JavaFX 属性和绑定 API 的一部分。我们将在下一章讨论这个 API。

当 IncludeExample 运行时,显示如图 4-7 所示的 Include Example 窗口。

A323806_4_En_4_Fig7_HTML.jpg

图 4-7。

The IncludeExample program

使用 fx:root 创建定制组件

元素允许我们将一个 FXML 文件附加到另一个 FXML 文件中。类似地,fx:root元素允许我们将 FXML 文件附加到代码中提供的Node上。fx:root元素必须是 FXML 文件中的顶级元素。必须为它提供一个type属性,该属性决定了需要在代码中创建的Node的类型,以便加载这个 FXML 文件。

最简单的形式是,您可以从

<SomeType ...

<fx:root type="some.package.SomeType" ...

在加载 FXML 文件之前,在代码中实例化SomeType并将其设置为FXMLLoader中的根,如下所示:

SomeType someType = new SomeType();
fxmlLoader.setRoot(someType);
fxmlLoader.load();

下一个例子更进一步。它定义了一个扩展 FXML 文件的fx:root类型的类,并作为 FXML 文件的根和控制器。它在其构造器中加载 FXML 文件,并使用initialize()方法在 FXML 文件中构建的节点之间建立所需的关系。然后,可以像使用本地 JavaFX 节点一样使用该类。以这种方式构造的类称为自定义组件。

我们在这里定义的定制组件是一个简单的复合定制组件,这意味着它由几个节点组成,这些节点共同满足一些业务需求。我们在这个例子中创建的定制组件叫做ProdId。它旨在帮助产品 ID 的数据输入,产品 ID 必须具有“A-123456”的形式,其中破折号前只有一个字符,并且必须是“A”或“B”或“c”。破折号后最多可以有六个字符。该程序如清单 4-26 至 4-28 所示。

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextField?>
<fx:root type="javafx.scene.layout.HBox"
         alignment="BASELINE_LEFT"
         maxHeight="-Infinity"
         maxWidth="-Infinity"
         minHeight="-Infinity"
         minWidth="-Infinity"

         xmlns:fx="http://javafx.com/fxml/1">
    <children>
        <TextField fx:id="prefix" prefColumnCount="1"/>
        <Label text="-"/>
        <TextField fx:id="prodCode" prefColumnCount="6"/>
    </children>
</fx:root>

Listing 4-26.
ProdId.fxml

package projavafx.customcomponent;

import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.TextField;
import javafx.scene.layout.HBox;

import java.io.IOException;

public class ProdId extends HBox {

    @FXML
    private TextField prefix;

    @FXML
    private TextField prodCode;

    private StringProperty prodId = new SimpleStringProperty();

    public ProdId() throws IOException {
        FXMLLoader fxmlLoader = new FXMLLoader(ProdId.class.getResource("ProdId.fxml"));
        fxmlLoader.setRoot(this);
        fxmlLoader.setController(this);
        fxmlLoader.load();
    }

    @FXML
    public void initialize() {
        prefix.textProperty().addListener((observable, oldValue, newValue) -> {
            switch (newValue) {
                case "A":
                case "B":
                case "C":
                    prodCode.requestFocus();
                    break;
                default:
                    prefix.setText("");
            }
        });
        prodCode.textProperty().addListener((observable, oldValue, newValue) -> {
            if (newValue.length() > 6) {
                prodCode.setText(newValue.substring(0, 6));
            } else if (newValue.length() == 0) {
                prefix.requestFocus();
            }
        });
        prodId.bind(prefix.textProperty().concat("-").concat(prodCode.textProperty()));
    }

    public String getProdId() {
        return prodId.get();
    }

    public StringProperty prodIdProperty() {
        return prodId;
    }

    public void setProdId(String prodId) {
        this.prodId.set(prodId);
    }
}

Listing 4-27.

ProdId.java

package projavafx.customcomponent;

import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
import javafx.stage.Stage;

public class CustomComponent extends Application {
    @Override
    public void start(Stage primaryStage) throws Exception {
        VBox vBox = new VBox(10);
        vBox.setPadding(new Insets(10, 10, 10, 10));
        vBox.setAlignment(Pos.BASELINE_CENTER);

        final Label prodIdLabel = new Label("Enter Product Id:");
        final ProdId prodId = new ProdId();

        final Label label = new Label();
        label.setFont(Font.font(48));
        label.textProperty().bind(prodId.prodIdProperty());

        HBox hBox = new HBox(10);
        hBox.setPadding(new Insets(10, 10, 10, 10));
        hBox.setAlignment(Pos.BASELINE_LEFT);
        hBox.getChildren().addAll(prodIdLabel, prodId);

        vBox.getChildren().addAll(hBox, label);
        Scene scene = new Scene(vBox);
        primaryStage.setTitle("Custom Component Example");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

Listing 4-28.
CustomComponent.

java

注意,在主程序CustomComponent类中,我们没有加载任何 FXML 文件。我们简单地实例化了ProdId,并继续使用它,就像它是一个本地 JavaFX 节点一样。FXML 文件只是将两个TextField和一个Label放在一个HBox类型fx:root中。没有设置fx:controller,因为我们想在ProdId类的构造器中设置它。除了两个注入的TextField之外,我们还有另一个名为prodIdStringProperty字段,为此我们定义了一个 getter getProdId(),一个 setter setProdId()和一个 property getter prodIdProperty()

private StringProperty prodId = new SimpleStringProperty();

public String getProdId() {
    return prodId.get();
}

public StringProperty prodIdProperty() {
    return prodId;
}

public void setProdId(String prodId) {
    this.prodId.set(prodId);
}

验证需求和便利功能在initialize()方法中,当FXMLLoader完成加载 FXML 文件时,将调用该方法。我们将ChangeListener连接到两个TextFieldtextProperty,只允许有效的变更发生。当prefix填充了正确的数据时,我们也将光标移动到prodCode。同样,当我们从prodCode字段后退时,光标会自然地跳到prefix文本字段。

@FXML
public void initialize() {
    prefix.textProperty().addListener((observable, oldValue, newValue) -> {
        switch (newValue) {
            case "A":
            case "B":
            case "C":
                prodCode.requestFocus();
                break;
            default:
                prefix.setText("");
        }
    });
    prodCode.textProperty().addListener((observable, oldValue, newValue) -> {
        if (newValue.length() > 6) {
            prodCode.setText(newValue.substring(0, 6));
        } else if (newValue.length() == 0) {
            prefix.requestFocus();
        }
    });
    prodId.bind(prefix.textProperty().concat("-").concat(prodCode.textProperty()));
}

当 CustomComponent 程序运行时,显示如图 4-8 所示的自定义组件示例窗口。

A323806_4_En_4_Fig8_HTML.jpg

图 4-8。

The CustomComponent program

使用脚本或控制器属性的事件处理

在上一节中,我们向您介绍了如何使用控制器的方法作为 FXML 文件中节点的事件处理程序。JavaFX 允许另外两种方式在 FXML 文件中设置事件处理程序。一种方法是使用脚本。可以使用任何基于 JSR-223 兼容javax.script的脚本引擎。必须在 FXML 文件的顶部指定用于编写脚本的语言。要使用 Oracle JDK 8 附带的 Nashorn JavaScript 引擎,以下处理指令必须出现在 FXML 文件的顶部:

<?language javascript?>

元素用于引入脚本。支持内联脚本和外部文件脚本。以下是一个内联脚本:

<fx:script>
    function actionHandler(event) {
        webView.getEngine().load(address.getText());
    }
</fx:script>

外部脚本采用以下形式:

<fx:script source="myscript.js"/>

FXML 文件中具有fx:id的任何节点都可以通过它们的fx:id名称从脚本环境中访问。如果 FXML 文件有一个控制器,那么这个控制器就是一个名为controller的变量。在fx:script部分中声明的变量也可以用作 FXML 文件其余部分的属性中的变量。要使用fx:script部分定义的actionHandler(event)函数作为事件处理程序,可以指定如下:

<TextField fx:id="address"
           onAction="actionHandler(event)"

Caution

如果您的事件处理程序不需要检查事件对象,您可以使用不带参数的函数,或者使用带一个参数作为事件处理程序属性值的函数,比如onAction。如果你调用一个只有一个参数的函数,那么你必须将系统提供的事件变量传递给它。

清单 4-29 和 4-30 中的脚本示例说明了使用脚本的事件处理。

<?xml version="1.0" encoding="UTF-8"?>

<?language javascript?>

<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.web.WebView?>
<VBox maxHeight="-Infinity"
      maxWidth="-Infinity"
      minHeight="-Infinity"
      minWidth="-Infinity"
      prefHeight="400.0"
      prefWidth="600.0"
      spacing="10.0"

      xmlns:fx="http://javafx.com/fxml/1">
    <fx:script>
        function actionHandler(event) {
            webView.getEngine().load(address.getText());
        }
         </fx:script>
    <children>
        <HBox spacing="10.0">
            <children>
                <TextField fx:id="address"
                           onAction="actionHandler(event)"
                           HBox.hgrow="ALWAYS">
                    <padding>
                        <Insets bottom="4.0" left="4.0" right="4.0" top="4.0"/>
                    </padding>
                </TextField>
                <Button fx:id="loadButton"
                        mnemonicParsing="false"
                        onAction="actionHandler(event)"
                        text="Load"/>
            </children>
        </HBox>
        <WebView fx:id="webView"
                 prefHeight="200.0"
                 prefWidth="200.0"
                 VBox.vgrow="ALWAYS"/>
    </children>
    <padding>
        <Insets bottom="10.0" left="10.0" right="10.0" top="10.0"/>
    </padding>
</VBox>

Listing 4-29.
ScriptingExample.

fxml

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class ScriptingExample extends Application {
    @Override
    public void start(Stage primaryStage) throws Exception {
        FXMLLoader fxmlLoader = new FXMLLoader();
        fxmlLoader.setLocation(
            ScriptingExample.class.getResource("/ScriptingExample.fxml"));
        final VBox vBox = fxmlLoader.load();
        Scene scene = new Scene(vBox, 600, 400);
        primaryStage.setTitle("Scripting Example");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

Listing 4-30.
ScriptingExample.

java

运行 ScriptingExample 时,将显示与图 4-4 非常相似的脚本示例窗口。

您还可以使用变量语法指定事件处理程序:

<TextField fx:id="address"
           onAction="$controller.actionHandler"

这将把控制器的actionHandler属性设置为onActionEvent的事件处理程序。在控制器中,actionHandler属性应该有正确的事件处理程序类型。对于onAction事件,属性应该如下所示:

@FXML
public EventHandler<ActionEvent> getActionHandler() {
    return event -> {
        // handle the event
    };
}

现在我们已经对 FXML 文件格式有了透彻的理解,我们可以有效地利用 GUI 编辑的便利来创建 FXML 文件。

使用 JavaFX 场景构建器

在前面的章节中,您学习了 FXML 文件格式的基础知识。在尝试使用和理解 JavaFX Scene Builder 工具时,这些知识会非常有用。在本章的最后一节,我们将深入探讨 JavaFX Scene Builder 的用法。

因为设计 UI 是非常主观的,有时是艺术的努力,所以很大程度上取决于手边的应用程序以及 UI 和用户体验团队的设计敏感度。我们并不假装知道做 UI 设计的最好方法。因此,在本节中,我们将向您介绍 JavaFX Scene Builder 2.0 本身,向您指出 UI 设计器的各个部分,并讨论如何转动旋钮和切换齿轮以实现所需的结果。

JavaFX 场景构建器概述

当您启动 JavaFX Scene Builder 时,屏幕看起来如图 4-9 所示。

A323806_4_En_4_Fig9_HTML.jpg

图 4-9。

The JavaFX Scene Builder program

首次启动时,JavaFX Scene Builder UI 顶部有一个菜单栏,屏幕左侧有两个名为 Library 和 Document 的折叠容器,屏幕中间有一个内容面板,屏幕右侧有一个名为 Inspector 的折叠容器。

了解菜单栏和菜单项

JavaFX Scene Builder 中有九个菜单。让我们一个一个地检查它们。

文件菜单如图 4-10 所示。

A323806_4_En_4_Fig10_HTML.jpg

图 4-10。

The File menu

新的、打开、保存、另存为、恢复到已保存、在资源管理器(或 Finder,或桌面)中显示、关闭窗口和退出菜单项做了你认为它们应该做的事情。“从模板新建”菜单项从现有模板创建新的 FXML 文件。模板列表如图 4-11 所示。

A323806_4_En_4_Fig11_HTML.jpg

图 4-11。

The templates

“导入”菜单项允许您将另一个 FXML 文件的内容复制到当前 FXML 文件中。它还允许您将图像和媒体文件添加到当前 FXML 文件中。这样导入的文件被包装在一个ImageViewMediaView节点中。“包含”菜单项允许您将一个fx:include元素添加到当前的 FXML 文件中。“关闭窗口”菜单项关闭当前窗口中正在编辑的 FXML 文件。“首选项”菜单项允许您设置某些控制 JavaFX Scene Builder 外观的首选项。“退出”菜单项允许您完全退出 JavaFX Scene Builder 应用程序。在关闭应用程序之前,它会要求您保存任何未保存的文件。

编辑菜单如图 4-12 所示。

A323806_4_En_4_Fig12_HTML.jpg

图 4-12。

The Edit menu

撤消、重做、剪切、复制、粘贴、粘贴到、复制、删除、全选、不选、选择父项、选择下一个和选择上一个菜单项都执行它们正常的功能。“根据所选内容裁剪文档”菜单项删除所有未选中的内容。

视图菜单如图 4-13 所示。

A323806_4_En_4_Fig13_HTML.jpg

图 4-13。

The View menu

内容菜单项将焦点放在屏幕中间的内容面板上。“属性”、“布局”和“代码”菜单项将焦点放在屏幕右侧检查器面板中的属性、布局或代码部分。“隐藏库”命令隐藏屏幕左侧顶部的“库”面板。一旦库被隐藏,菜单项将改变为显示库。“隐藏文档”菜单项对屏幕左侧底部的“文档”面板执行相同的操作。“显示 CSS 分析器”菜单项显示 CSS 分析器,它最初是不显示的。“隐藏左面板”和“隐藏右面板”菜单项隐藏左面板(库面板和文档面板)或右面板(检查器面板)。“显示轮廓”菜单项显示项目的轮廓。“显示样本数据”菜单项将显示TreeViewTableViewTreeTableView节点的样本数据,以帮助您可视化工作中的节点。示例数据不与 FXML 文件一起保存。“禁用对齐参考线”菜单项禁用在内容面板的容器中移动节点时显示的对齐参考线。这些对齐准则帮助您将节点定位在屏幕上的正确位置。“缩放”菜单项允许您更改内容面板的放大率。Show Sample Controller Skeleton 菜单项将打开一个对话框,显示基于在文档面板中进行的控制器设置和为 FXML 文件中的节点声明的fx:id的框架控制器类声明。

图 4-14 显示了带有 CSS 分析器的 JavaFX 场景构建器屏幕。

A323806_4_En_4_Fig14_HTML.jpg

图 4-14。

The JavaFX Scene Builder screen with the CSS Analyzer shown

插入菜单如图 4-15 所示。

A323806_4_En_4_Fig15_HTML.jpg

图 4-15。

The Insert menu

“插入”菜单包含子菜单和菜单项,允许您将不同种类的节点插入到内容面板中。子菜单及其菜单项表示与“库”面板中相同的层次结构。它们包括容器、控件、菜单、杂项、形状、图表和 3D 类别。我们将在后续章节中更详细地介绍这些节点。

修改菜单如图 4-16 所示。

A323806_4_En_4_Fig16_HTML.jpg

图 4-16。

The Modify menu

“适合父节点”菜单项将扩展所选节点以填充一个AnchorPane容器,并将节点锚定到父节点的所有边上。使用计算尺寸菜单项会将所选元素的尺寸调整为USE_COMPUTED_SIZE。GridPane 子菜单包含与GridPane容器一起工作的项目。“设置效果”子菜单包含可以在当前节点上设置的每个效果的项目。添加弹出控件允许您向选定的节点添加一个ContextMenu或一个Tooltip。场景大小子菜单允许您将场景的大小更改为一些常见的大小,包括 320×240 (QVGA)、640×480 (VGA)、1280×800 和 1920×1080。

排列菜单如图 4-17 所示。

A323806_4_En_4_Fig17_HTML.jpg

图 4-17。

The Arrange menu

“置于顶层”、“置于底层”、“置于顶层”和“置于底层”菜单项将选定节点移动到重叠节点的 z 顺序的前面、后面、上面或下面。“打包”子菜单包含每种容器类型的项目,并允许您将一组选定节点打包到容器中。例如,您可以选择将两个相邻的Label包装成一个HBox。“展开”菜单项从选定节点中移除容器。

预览菜单如图 4-18 所示。

A323806_4_En_4_Fig18_HTML.jpg

图 4-18。

The Preview menu

“在窗口中显示预览”菜单项允许您在活动窗口中预览场景,以查看它在现实生活中的效果。这是最有用的菜单项,因为你会多次使用它。JavaFX 主题子菜单包含各种主题,您可以使用这些主题预览场景。“场景样式表”子菜单包含允许您添加、移除或编辑在预览期间应用到场景中的样式表的项目。“国际化”子菜单包含允许您添加、移除或编辑在预览期间使用的资源包的项目。预览尺寸子菜单包含预览期间首选屏幕尺寸的项目。

“窗口”菜单允许您在同时编辑的多个 FXML 文件之间切换。

“帮助”菜单显示 JavaFX Scene Builder 的联机帮助和“关于”框。

了解库面板

“库”面板位于左侧面板的顶部,可以使用“查看➤”“隐藏库”菜单项隐藏。它包含可以用来构建 UI 的容器和节点。图 4-19 显示了打开容器抽屉的库面板,显示了一些容器。你可以点击其他抽屉,看看里面装的是什么。图 4-20 显示了控件抽屉打开的库面板,显示了一些控件。

“库”面板顶部有一个搜索框。你可以在搜索框中输入一个容器或控件的名称,或者其他抽屉的名称。当您键入时,“库”面板会将其显示从折叠排列更改为单个列表,其中包含名称与搜索框中输入的名称相匹配的所有节点。这使您可以通过名称快速找到一个节点,而不必逐一查看抽屉。图 4-21 显示了搜索模式下的库面板。要退出搜索模式,只需单击搜索框右端的 x 标记。

找到容器或节点后,可以将其拖动到内容面板,拖动到文档面板中的层次树,或者双击它。将容器带到内容面板,然后用控件和其他节点填充容器,这就是在 JavaFX Scene Builder 中构建 UI 的方式。

A323806_4_En_4_Fig20_HTML.jpg

图 4-20。

The Library panel with its Controls drawer open

A323806_4_En_4_Fig19_HTML.jpg

图 4-19。

The Library panel with its Containers drawer open

搜索框的右侧是一个菜单按钮,其中包含几个菜单项和一个改变“库”面板行为的子菜单。图 4-22 显示了该菜单按钮的可用内容。“以列表形式查看”菜单项将“库”面板从在几个部分中显示其节点更改为一起显示其节点,而不显示部分。“按节查看”将“库”面板从在一个列表中显示其节点更改为在几个节中显示其节点。“导入 JAR/FXML 文件”菜单项允许您将外部 JAR 文件或 FXML 文件作为自定义组件导入 JavaFX Scene Builder。“导入选择”菜单项允许您将当前选定的节点作为自定义组件导入到 JavaFX Scene Builder 中。“自定资源库文件夹”子菜单包含两个菜单项。“在资源管理器中显示”菜单项打开操作系统的文件资源管理器(或 Finder)中保存自定义组件的文件夹,允许您删除任何导入的自定义库。“显示 jar 分析报告”菜单项显示一个报告,该报告显示 JavaFX Scene Builder 对导入的 JAR 文件的评估。

A323806_4_En_4_Fig22_HTML.jpg

图 4-22。

The Library panel with its menu open

A323806_4_En_4_Fig21_HTML.jpg

图 4-21。

The Library panel in search mode

为了说明如何将自定义组件导入 JavaFX Scene Builder,我们将上一节的 custom component 示例中的类文件和 FXML 文件打包到一个CustomComponent.jar文件中。然后我们调用 Import JAR/FXML File 菜单项,导航到目录,并选择要导入的CustomComponent.jar文件。我们一点击文件选择对话框中的打开按钮,JavaFX Scene Builder 就会打开导入对话框,如图 4-23 所示。

A323806_4_En_4_Fig23_HTML.jpg

图 4-23。

The Import dialog for importing CustomComponent.jar

我们可以通过单击左侧列表中的定制组件名称来检查 jar 文件中包含的每个定制组件。关于所选定制组件的信息,包括定制组件的可视化表示,显示在屏幕的右侧。我们可以通过选择组件名称旁边的复选框来选择要导入的自定义组件。然后,我们单击 Import Component 按钮完成导入过程。导入后,ProdId 自定义组件将显示在“库”面板的“自定义”部分,并且可以添加到构建的任何其他 ui 中。

了解文档面板

文档面板位于左侧面板的底部,可以使用“查看➤”“隐藏文档”菜单项隐藏。它包含两个部分。“层次结构”部分显示添加到内容面板的所有节点的树视图,按包含关系组织。因为内容面板中节点的布局可能会使从内容面板中选择节点变得棘手,所以在“文档”面板的“层次结构”部分进行选择可能会更容易。

图 4-24 显示了清单 4-4 中 FXMLLoaderExample 中 FXML 文件的文档面板的层次结构部分。您可以看到选择 WebView 节点后展开的节点树。

A323806_4_En_4_Fig24_HTML.jpg

图 4-24。

The Hierarchy section of the Document panel for FXMLLoaderExample.fxml

控制器部分显示关于 FXML 文件控制器的信息。图 4-25 显示了清单 4-7 中 FXMLInjectionExample 中 FXML 文件的文档面板控制器部分。您可以在此部分设置控制器类的名称。在本节中,您还可以选择使用 FXML 文件的fx:root结构。您还会看到一个带有已经设置好的fx:id的节点列表,您可以通过单击 Assigned fx:id 表中的行来选择节点。

A323806_4_En_4_Fig25_HTML.jpg

图 4-25。

The Controller section of the Document panel for FXMLInjectionExample.fxml

文档面板的右上角有一个菜单按钮。它包含一个层次显示子菜单,该子菜单有三个菜单项,如图 4-26 所示。Info 菜单项使 Hierarchy 部分显示每个节点及其一般信息,通常也显示在同一节点的内容面板中。fx:id 菜单项使 Hierarchy 部分显示每个节点及其 fx:id(如果已设置)。“节点 ID”菜单项使“层次”部分显示每个节点及其节点 Id(如果已设置)。CSS 使用节点 ID 来查找节点并操作节点的样式。

A323806_4_En_4_Fig26_HTML.jpg

图 4-26。

The Document panel with its menu open

了解内容面板

内容面板是组成 UI 的地方。首先将一个容器拖动到内容面板。然后,将其他节点拖到内容面板上,并定位到容器节点上。当您拖动节点时,当您拖动的节点到达特定的对齐和间距位置时,会出现红色的指引线。在这些指导方针的帮助下,您应该能够创建视觉上令人愉悦的 ui。

在内容面板上方,有一个面包屑条,显示内容区域中所选节点的路径。这使您可以轻松导航到当前选定节点的包含节点。出现这种情况时,JavaFX Scene Builder 会在该栏中显示警告和错误消息。

JavaFX Scene Builder 的一个便利功能是,当您选择了几个节点时,您可以通过右键单击选定的节点,选择“包裹”子菜单,然后选择其中一种容器类型来进入上下文菜单。也可以通过这种方式展开节点,移除包含该节点的任何容器。

图 4-27 显示了清单 4-20 中的IncludeExampleTree.fxml文件正在 JavaFX 场景构建器中编辑。

A323806_4_En_4_Fig27_HTML.jpg

图 4-27。

The IncludeExampleTree.fxml file being edited in JavaFX Scene Builder

了解检查器面板

检查器面板位于右侧面板中,可以使用“查看➤”“隐藏右侧面板”菜单项隐藏。它包含属性、布局和代码部分。“属性”部分列出了内容面板中选定节点的所有常规属性。您可以通过更改此处显示的值来设置属性。您还可以通过调用属性右侧的小菜单按钮将属性改回默认值。您可以在 ID 属性编辑器的属性部分设置节点 ID。图 4-28 显示了检查器面板的属性部分。

A323806_4_En_4_Fig28_HTML.jpg

图 4-28。

The Properties section of the Inspector panel

布局部分列出了当前选定节点的所有与布局相关的属性。图 4-29 显示了检查器面板的布局剖面。

A323806_4_En_4_Fig29_HTML.jpg

图 4-29。

The Layout section of the Inspector panel

代码部分列出了内容面板中选定节点可能拥有的所有事件处理程序。它还允许您设置所选节点的fx:id。您可以在代码部分以任何方式连接事件处理程序,但是提供事件处理程序最方便的方式是将它们设置为控制器中正确签名的方法。图 4-30 显示了检查面板的代码部分。

A323806_4_En_4_Fig30_HTML.jpg

图 4-30。

The Code section of the Inspector panel

摘要

在本章中,您学习了在 JavaFX 中创建 UI 的声明式方法。您学习了以下重要工具和信息:

  • FXML 文件是声明性 UI 信息的载体,是 JavaFX 项目的核心资产。
  • FXML 文件由FXMLLoader加载到 JavaFX 应用程序中。加载的结果是一个可以合并到一个Scene中的节点。
  • FXML 文件可以有一个配套的控制器类,它在运行时代表 FXML 文件中声明的节点执行编程功能,如事件处理。
  • FXML 文件可以在您喜欢的 Java IDEs 中使用智能建议和补全功能轻松编辑。
  • FXML 文件也可以在 Gluon Scene Builder 9.0 中编辑,这是一个用于编辑 FXML 文件的开源工具。
  • JavaFX Scene Builder 是指定 JavaFX UIs 的高效工具。您可以将容器、控件和其他 JavaFX 节点添加到 FXML 文件的内容中。
  • 您可以设置一个控制器,并定义场景中各个节点的fx:ids
  • 通过操作“文档”面板的“层次”部分中的容器、控件和其他节点,可以组织 FXML 文件中的层次信息。
  • 通过使用“检查器”面板中的“属性”、“布局”和“代码”部分,可以操作 FXML 文件中节点的属性。
  • 您可以在内容面板中直观地编写您的 UI。
  • 你可以用 CSS 分析器分析用户界面的 CSS。

资源

五、集合和并发

当你知道一件事时,坚持你知道它;当你不知道一件事时,允许你不知道它——这就是知识。—孔子

在第四章对 JavaFX UI 控件进行了快速探索之后,我们在本章中重新将注意力集中在 JavaFX 的一些底层功能上。

回想一下,在第四章中,您学习了Observable接口及其一个子接口ObservableValue。在这一章中,我们研究了ObservableObservableListObservableMapObservableSetObservableArray的其他四个子接口,从而完成了Observable系列接口和类的故事。

然后我们将介绍 JavaFX 中的并发性。我们解释 JavaFX 线程模型,指出 JavaFX 应用程序中最重要的线程。我们看一下您必须遵循的规则,以确保您的 JavaFX 应用程序能够响应用户输入,并且不会被执行时间过长的事件处理程序锁定。我们还向您展示了如何使用javafx.concurrent框架将长期运行的例程卸载到后台线程。

我们用一些例子来结束本章,这些例子展示了如何使用JFXPanel将 JavaFX 场景图嵌入到 Swing 应用程序中,如何使用FXCanvas将 JavaFX 场景图嵌入到 SWT 应用程序中,注意如何使 JavaFX 事件线程与 Swing 事件调度线程配合良好,以及如何使用SwingNode将 Swing 组件嵌入到 Java FX 场景中。

理解可观察的集合和数组

正如我们在第四章中看到的,Observable接口有五个直接子接口——ObservableValue接口、ObservableList接口、ObservableMap接口、ObservableSet接口和ObservableArray接口。我们了解到ObservableValue接口在 JavaFX 属性和绑定框架中起着核心作用。

ObservableListObservableMapObservableSetObservableArray接口位于javafx.collections包中,被称为 JavaFX 可观察集合和数组。除了扩展Observable接口之外,ObservableListObservableMapObservableSet al分别扩展java.util.Listjava.util.Mapjava.util.Set接口,使它们成为 Java 集合框架眼中的真正集合。您可以在这些接口的对象上调用您熟悉的所有 Java 集合框架方法,并期望得到完全相同的结果。除了普通的 Java 集合框架之外,JavaFX observable 集合和数组还提供了对注册侦听器的通知。因为它们是Observable的,所以您可以向 JavaFX 可观察集合对象注册InvalidationListener的,并在可观察集合和数组的内容无效时得到通知。

每个 JavaFX observable 集合和数组接口都支持一个 change 事件,该事件传达了更详细的变更信息。在接下来的小节中,我们将研究 JavaFX 可观察集合和数组以及它们支持的变更事件。

理解观察列表

图 5-1 是显示ObservableList和支持接口的 UML 图。

A323806_4_En_5_Fig1_HTML.jpg

图 5-1。

Key interfaces that support the JavaFX observable list

为了避免混乱,我们从图 5-1 中省略了java.util.List接口。java.util.List接口是ObservableList的另一个超级接口。在ObservableList接口上有以下两种方法可以让你注册和注销ListChangeListener:

  • addListener(ListChangeListener<? super E> listener)
  • removeListener(ListChangeListener<? super E> listener)

以下关于ObservableList的额外方法使得使用界面更加容易:

  • addAll(E... elements)
  • setAll(E... elements)
  • setAll(Collection<? extends E> col)
  • removeAll(E... elements)
  • retainAll(E... elements)
  • remove(int from, int to)
  • filtered(Predicate<E>)
  • sorted(Comparator<E>)
  • sorted()

The filtered()和两个sorted()方法返回一个包装了ObservableListFilteredListSortedList。当最初的ObservableList发生变异时,包装器FilteredListSortedList会反映这些变化。

ListChangeListener接口只有一个方法:onChange(ListChangeListener.Change<? extends E> change)。当ObservableList的内容被操作时,这个方法被调用。注意,这个方法的参数类型是嵌套类Change,它在ListChangeListener接口中声明。我们将在下一小节中向您展示如何使用ListChangeListener.Change类。现在,我们来看看清单 5-1 中的一个简单例子,它展示了当一个ObservableList被操作时无效和列表改变事件的触发。

package com.projavafx.collections;

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 5-1.
ObservableListExample.java

与 Java 集合框架不同,在 Java 集合框架中,公共 API 既包含接口,如ListMapSet,也包含可以实例化的接口的具体实现,如ArrayListHashMapHashSet,JavaFX 可观察集合框架仅提供接口ObservableListObservableMapObservableSetObservableArray。具体的实现类不是公共的,但是您必须使用实用程序类FXCollections来获得 JavaFX 可观察集合和数组的对象。在清单 5-1 中,我们通过调用FXCollections上的工厂方法来获得一个ObservableList<String>对象:

        ObservableList<String> strings = FXCollections.observableArrayList();

然后,我们将一个InvalidationListener和一个ListChangeListener与可观察列表挂钩。因为两个侦听器都有一个单参数方法,并且是使用addListener()添加的,所以我们必须在 lambda 表达式中指定参数类型。失效侦听器只是在每次被调用时打印出一条消息。列表更改监听器打印出可观察列表的内容。程序的其余部分只是以各种方式操纵可观察列表的内容:通过调用java.util.List接口上的方法,通过调用添加到ObservableList中的一些新的便利方法,以及通过调用从可观察列表中获得的Iterator上的remove()方法。

当我们运行清单 5-1 中的程序时,以下输出被打印到控制台:

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]

事实上,我们在代码中进行的每个更改可观察列表内容的调用都会触发失效侦听器和列表更改侦听器上的回调。

如果失效侦听器或列表更改侦听器的实例已经作为侦听器添加到可观察列表中,则所有后续的将该实例作为参数的addListener()调用都将被忽略。当然,您可以向一个可观察列表中添加任意数量的不同失效侦听器和列表更改侦听器。

处理 ListChangeListener 中的更改事件

在这一节中,我们将仔细研究一下ListChangeListener.Change类,并讨论onChange()回调方法应该如何处理列表更改事件。

正如我们在上一节中看到的,对于通过调用FXCollections.observableArrayList()获得的ObservableList,每个 mutator 调用——也就是说,对更改可观察列表内容的单个方法的每个调用——都会生成一个发送给每个注册观察者的列表更改事件。事件对象是一个实现了ListChangeListener.Change接口的类的实例,可以认为它代表了一个或多个离散的变化,每个变化都属于四种类型之一:添加元素、删除元素、替换元素或置换元素。ListChangeListener.Change类提供了以下方法,允许您获得关于变更的详细信息:

  • boolean next()
  • void reset()
  • boolean wasAdded()
  • boolean wasRemoved()
  • boolean wasReplaced()
  • boolean wasPermutated()
  • int getFrom()
  • int getTo()
  • int getAddedSize()
  • List<E> getAddedSublist()
  • int getRemovedSize()
  • List<E> getRemoved()
  • int getPermutation(int i)
  • ObservableList<E> getList()

next()reset()方法控制遍历事件对象中所有离散变化的游标。在进入ListChangeListeneronChange()方法时,光标位于第一次离散变化之前。您必须调用next()方法来将光标移动到第一个离散变更处。对next()方法的后续调用会将光标移动到剩余的离散变更上。如果到达下一个离散变化,返回值将是true。如果光标已经在最后一个离散变化上,返回值将是false。一旦光标定位在有效的离散变更上,就可以调用wasAdded()wasRemoved()wasReplaced()wasPermutated()方法来确定离散变更所代表的变更类型。

Caution

wasAdded()wasRemoved()wasReplaced()wasPermutated()方法不是正交的。只有当离散变更既是添加又是删除时,它才是替换。测试离散变化类型的正确顺序是,首先确定它是排列还是替换,然后确定它是添加还是删除。

一旦您确定了离散变化的种类,您就可以调用其他方法来获得更多的信息。对于加法,getFrom()方法返回添加了新元素的可观察列表中的索引;getTo()方法返回元素的索引,该元素比添加的元素的末尾多一个;getAddedSize()方法返回添加的元素数量;而getAddedSublist()方法返回一个包含添加元素的List<E>。对于移除,getFrom()getTo()方法都返回可观察列表中移除元素的索引;getRemovedSize()方法返回被移除的元素的数量;而getRemoved()方法返回一个包含被移除元素的List<E>。对于替换,既要检查与添加相关的方法,也要检查与移除相关的方法,因为替换可以被视为移除,然后在相同的索引处添加。对于排列,getPermutation(int i)方法返回排列后可观察列表中元素的索引,该元素在排列前在可观察列表中的索引是i。在所有情况下,getList()方法总是返回底层的可观察列表。

在清单 5-2 所示的例子中,我们在将一个ListChangeListener附加到一个ObservableList之后执行各种列表操作。名为MyListenerListChangeListener的实现包括一个用于ListChangeListener.Change对象的漂亮的打印机,当一个事件被触发时打印出列表改变事件对象。

package com.projavafx.collections;

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 permutationStringBuilder = new StringBuilder("[");
                    for (int k = change.getFrom(); k < change.getTo(); k++) {
                        permutationStringBuilder.append(k)
                            .append("->")
                            .append(change.getPermutation(k));
                        if (k < change.getTo() - 1) {
                            permutationStringBuilder.append(", ");
                        }
                    }
                    permutationStringBuilder.append("]");
                    String permutation = permutationStringBuilder.toString();
                    sb.append("\t\tPermutation: ").append(permutation).append("\n");
                }
            }
            return sb.toString();
        }
    }
}

Listing 5-2.
ListChangeEventExample.java

在前面的例子中,我们触发了可观察列表中的四种离散变化。因为ObservableList上没有方法会触发置换事件,所以我们使用了FXCollections类中的sort()实用方法来实现置换。在后面的章节中,我们将详细介绍FXCollections。我们触发了两次替换事件,一次是用set(),一次是用setAll()setAll()的好处在于它在一个操作中有效地执行了一个clear()和一个addAll(),并且只生成一个变更事件。

当我们运行清单 5-2 中的程序时,以下输出被打印到控制台:

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]

在除了removeAll()调用之外的所有调用中,列表更改事件对象只包含一个离散的更改。removeAll()调用生成包含两个离散变化的列表变化事件的原因是,我们希望删除的三个元素落在列表中两个不相交的范围内。

在我们关心列表变更事件的大多数用例中,您不一定需要区分离散变更的种类。有时你只是想对所有添加和删除的元素做些什么。在这种情况下,您的ListChangeListener方法可以像下面这样简单。

@Override
public void onChanged(Change<? extends Foo> change) {
    while (change.next()) {
        for (Foo foo : change.getAddedSubList()) {
            // starting up
        }
        for (Foo foo : change.getRemoved()) {
            // cleaning up
        }
    }
}

理解可观察地图

虽然在 JavaFX observable collections 框架层次结构中,ObservableMap看起来相当于ObservableList,但它实际上没有ObservableList复杂。图 5-2 是显示ObservableMap和支持接口的 UML 图。

A323806_4_En_5_Fig2_HTML.jpg

图 5-2。

Key interfaces that support the JavaFX observable map

为了避免混乱,我们从图 5-2 中省略了java.util.Map接口。java.util.Map接口是ObservableMap的另一个超级接口。ObservableMap界面上的以下方法允许您注册和注销MapChangeListeners:

  • addListener(MapChangeListener<? super K, ? super V> listener)
  • addListener(MapChangeListener<? super K, ? super V> listener)

ObservableMap上没有额外的方便方法。

MapChangeListener接口只有一个方法:onChange(MapChangeListener.Change<? extends K, ? extends V> change)。当ObservableMap的内容被操作时,这个方法被调用。注意,这个方法的参数类型是嵌套类Change,它在MapChangeListener接口中声明。与ListChangeListener.Change类不同的是,MapChangeListener.Change类适用于报告地图中单个键的变化。如果ObservableMap上的方法调用影响了多个键,那么将触发与受影响的键数量一样多的地图改变事件。

MapChangeListener.Change类为您提供了以下方法来检查对键所做的更改。

  • 如果为键添加了新值,则boolean wasAdded()返回true
  • 如果从键中删除了旧值,则boolean wasRemoved()返回true
  • K getKey()返回受影响的键。
  • V getValueAdded()返回为键添加的值。
  • V getValueRemoved()返回为键删除的值。(注意,使用现有键的put()调用将导致旧值被删除。)
  • ObservableMap<K, V> getMap()

在清单 5-3 的例子中,我们在将一个MapChangeListener附加到一个ObservableMap之后执行各种地图操作。名为MyListenerMapChangeListener的实现包括一个用于MapChangeListener.Change对象的漂亮的打印机,当一个事件被触发时打印出地图改变事件对象。

package com.projavafx.collections;

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);

        final 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 5-3.
MapChangeEventExample.

java

当我们运行清单 5-3 中的程序时,以下输出被打印到控制台:

Calling put("First", 1):
        map = {First=1}
        Change event data:
                Was added: true
                Was removed: false
                Key: First
                Value added: 1
                Value removed: null

Calling put("First", 100):
        map = {First=100}
        Change event data:
                Was added: true
                Was removed: true
                Key: First
                Value added: 100
                Value removed: 1
Calling putAll(anotherMap):
        map = {Second=2, First=100}
        Change event data:
                Was added: true
                Was removed: false
                Key: Second
                Value added: 2
                Value removed: null

        map = {Second=2, Third=3, First=100}
        Change event data:
                Was added: true
                Was removed: false
                Key: Third
                Value added: 3
                Value removed: null
Calling remove on entryIterator:
        map = {Third=3, First=100}
        Change event data:
                Was added: false
                Was removed: true
                Key: Second
                Value added: null
                Value removed: 2

Calling remove on valueIterator:
        map = {First=100}
        Change event data:
                Was added: false
                Was removed: true
                Key: Third
                Value added: null
                Value removed: 3

在前面的例子中,注意到putAll()调用生成了两个 map change 事件,因为另一个 map 包含两个键。

了解可观察集

ObservableSet接口类似于ObservableMap接口,因为它的SetChangeListener.Change对象跟踪单个元素。图 5-3 是显示ObservableSet和支持接口的 UML 图。

A323806_4_En_5_Fig3_HTML.jpg

图 5-3。

Key interfaces that support the JavaFX observable set

为了避免混乱,我们从图 5-3 中省略了java.util.Set接口。java.util.Set接口是ObservableSet的另一个超级接口。ObservableSet界面上的以下方法允许您注册和注销SetChangeListeners:

  • addListener(SetChangeListener<? super E> listener)
  • addListener(SetChangeListener<? super E> listener)

ObservableSet上没有额外的方便方法。

SetChangeListener接口只有一个方法:onChange(SetChangeListener.Change<? extends E> change)。当ObservableSet的内容被操作时,这个方法被调用。注意,这个方法的参数类型是嵌套类Change,它在SetChangeListener接口中声明。SetChangeListener.Change类适用于报告集合中单个元素的变化。如果对ObservableSet的方法调用影响了多个元素,那么将触发与受影响元素数量一样多的 set change 事件。

SetChangeListener.Change类为您提供了以下方法来检查对元素所做的更改。

  • 如果一个新元素被添加到集合中,boolean wasAdded()返回true
  • 如果从集合中删除了一个元素,则boolean wasRemoved()返回true
  • E getElementAdded()返回添加到集合中的元素。
  • E getElementRemoved()返回从集合中删除的元素。
  • ObservableSet<E> getSet()

在清单 5-4 的例子中,我们在将一个SetChangeListener附加到一个ObservableSet之后执行各种设置操作。名为MyListenerSetChangeListener的实现包括一个用于SetChangeListener.Change对象的漂亮的打印机,当一个事件被触发时打印出设置的变更事件对象。

package com.projavafx.collections;

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 5-4.
SetChangeEventExample.

java

当我们运行清单 5-4 中的程序时,以下输出被打印到控制台:

Calling add("First"):
        set = [First]
        Change event data:
                Was added: true
                Was removed: false
                Element added: First
                Element removed: null

Calling addAll(Arrays.asList("Second", "Third")):
        set = [Second, First]
        Change event data:
                Was added: true
                Was removed: false
                Element added: Second
                Element removed: null

        set = [Second, First, Third]
        Change event data:
                Was added: true
                Was removed: false
                Element added: Third
                Element removed: null

Calling remove("Third"):
        set = [Second, First]
        Change event data:
                Was added: false
                Was removed: true
                Element added: null
                Element removed: Third

在前面的例子中,注意到addAll()调用生成了两个集合变更事件,因为添加到可观察集合的列表包含两个元素。

了解可观察阵列

ObservableArray接口是在需要观察原始 int 或 float 值列表的情况下引入的,但是每次在列表中添加或删除原始值时,装箱和取消装箱的开销出于性能原因是不可接受的。预计ObservableArray及其两个子接口ObservableIntegerArrayObservableFloatArray的实现将使用原始数组作为其内容的后备存储。JavaFX 3D API 使用了ObservableArrayObservableIntegerArrayObservableFloatArray。图 5-4 是显示ObservableArray和支持接口的 UML 图。

A323806_4_En_5_Fig4_HTML.jpg

图 5-4。

Key interfaces that support the JavaFX observable array

ObservableListObservableMapObservableSet接口不同,ObservableArray接口不实现任何 Java 集合框架接口。ObservableArray界面上的以下方法允许您注册和注销ArrayChangeListeners:

  • addListener(ArrayChangeListener<T> listener)
  • removeListener(ArrayChangeListener<T> listener)

ObservableArray上的以下附加方法让您可以控制底层的原始数组:

  • resize(int size)
  • ensureCapacity(int capacity)
  • trimToSize()
  • clear()
  • size()

这些方法处理一个ObservableArray的容量和大小。容量是基础基元数组的长度。大小是实际包含应用程序数据的元素的数量。容量总是大于或等于大小。如果当前底层基元数组的长度小于所需的新容量,则ensureCapacity()方法分配新的底层基元数组。resize()方法改变ObservableArray的大小。如果新大小大于当前容量,则容量会增加。如果新大小大于当前大小,则附加元素用零填充。如果新的大小小于当前的大小,resize()实际上并不收缩数组,而是用零填充“丢失”的元素。trimToSize()方法用一个长度与ObservableArray的大小相同的数组替换底层的原始数组。clear()方法将ObservableArray的大小调整为零。size()方法返回ObservableArray的当前大小。

ArrayChangeListener接口只有一个方法:onChanged(T observableArray, boolean sizeChanged, int from, int to)。注意,ArrayChangeListener没有像在ListChangeListenerMapChangeListenerSetChangeListener中那样将Change对象传递给onChange()方法,而是直接将变化的特征作为参数传递。第一个参数是ObservableArray本身。如果可观察数组的大小已经改变,则sizeChanged参数将是truefromto参数标记被改变元素的范围,包括在from端,不包括在to端。

ObservableIntegerArrayObservableFloatArray接口具有以特定于类型的方式操作数据的方法。我们列出ObservableIntegerArray的方法(ObservableFloatArray的方法类似):

  • copyTo(int srcIndex, int[] dest, int destIndex, int length)
  • copyTo(int srcIndex, ObservableIntegerArray dest, int destIndex, int length)
  • int get(int index)
  • addAll(int... elements)
  • addAll(ObservableIntegerArray src)
  • addAll(int[] src, int srcIndex, int length)
  • addAll(ObservableIntegerArray src, int srcIndex, int length)
  • setAll(int... elements)
  • setAll(int[] src, int srcIndex, int length)
  • setAll(ObservableIntegerArray src)
  • setAll(ObservableIntegerArray src, int srcIndex, int length)
  • set(int destIndex, int[] src, int srcIndex, int length)
  • set(int destIndex, ObservableIntegerArray src, int srcIndex, int length)
  • set(int index, int value)
  • int[] toArray(int[] dest)
  • int[] toArray(int srcIndex, int[] dest, int length)

addAll()方法追加到ObservableIntegerArray中。setAll()方法取代了ObservableIntegerArray的内容。这两组方法的源可以是一个包含int的 vararg 数组、ObservableIntegerArray、一个包含起始索引和长度的int数组,或者一个包含起始索引和长度的ObservableIntegerArrayget()方法返回指定索引处的值。set()方法从第一个参数中指定的索引开始,用新值替换ObservableIntegerArray的一部分。替换数据可以是单个int值、具有起始索引和长度的int数组,或者具有起始索引和长度的ObservableIntegerArray。如果原始ObservableIntegerArray中没有足够的空间来容纳替换数据,则抛出ArrayIndexOutOfBoundsExceptioncopyTo()方法将从指定srcIndex开始的ObservableIntegerArray的一部分复制到从指定destIndex开始的目的int数组或ObservableIntegerArray中。length参数规定了复制部分的长度。如果源ObservableIntegerArray中没有足够的元素来形成指定长度的一部分,或者如果目标中没有足够的空间来容纳复制的部分,则会抛出ArrayIndexOutOfBoundsExceptiontoArray()方法将ObservableIntegerArray的内容复制到一个int数组中。如果dest参数不为空且有足够空间,则填充并返回;否则,分配、填充并返回一个新的int数组。在指定了srcIndexlength的表单中,如果ObservableIntegerArray中没有足够的元素,就会抛出ArrayIndexOutOfBoundsException

在清单 5-5 所示的例子中,我们在将ArrayChangeListener附加到ObservableIntegerArray之后执行各种数组操作。当事件被触发时,我们打印出传递给onChange()方法的参数。

package com.projavafx.collections;

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 5-5.
ArrayChangeEventExample.java

当我们运行清单 5-5 中的程序时,以下输出被打印到控制台:

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]

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

FXCollections类在 Java FX observable collections and arrays 框架中扮演的角色类似于java.util.Collections类在 Java collections 框架中扮演的角色。FXCollections类包含以下ObservableList的工厂方法:

  • ObservableList<E> observableList(List<E> list)
  • ObservableList<E> observableList(List<E> list, Callback<E, Observable[]> extractor);
  • ObservableList<E> observableArrayList()
  • ObservableList<E> observableArrayList(Callback<E, Observable[]> extractor);
  • ObservableList<E> observableArrayList(E... items)
  • ObservableList<E> observableArrayList(Collection<? extends E> col)
  • ObservableList<E> concat(ObservableList<E>... lists)
  • ObservableList<E> unmodifiableObservableList(ObservableList<E> list)
  • ObservableList<E> checkedObservableList(ObservableList<E> list, Class<E> type)
  • ObservableList<E> synchronizedObservableList(ObservableList<E> list)
  • ObservableList<E> emptyObservableList()
  • ObservableList<E> singletonObservableList(E e)

它包含以下ObservableMap的工厂方法:

  • ObservableMap<K, V> observableMap(Map<K, V> map)
  • ObservableMap<K, V> unmodifiableObservableMap(ObservableMap<K, V> map)
  • ObservableMap<K, V> checkedObservableMap(ObservableMap<K, V> map, Class<K> keyType, Class<V> valType)
  • ObservableMap<K, V> synchronizedObservableMap(ObservableMap<K, V> map)
  • ObservableMap<K, V> emptyObservableMap();
  • ObservableMap<K, V> observableHashMap()

它包含以下ObservableSet的工厂方法:

  • ObservableSet<E> observableSet(Set<E> set)
  • ObservableSet<E> observableSet(E...)
  • ObservableSet<E> unmodifiableObservableSet(ObservableSet<E> set)
  • ObservableSet<E> checkedObservableSet(ObservableSet<E> set, Class<E> type)
  • ObservableSet<E> synchronizedObservableSet(ObservableSet<E>)
  • ObservableSet<E> emptyObservableSet()

它包含以下ObservableIntegerArrayObservableFloatArray的工厂方法:

  • ObservableIntegerArray observableIntegerArray()
  • ObservableIntegerArray observableIntegerArray(int...)
  • ObservableIntegerArray observableIntegerArray(ObservableIntegerArray)
  • ObservableFloatArray observableFloatArray()
  • ObservableFloatArray observableFloatArray(float...)
  • ObservableFloatArray observableFloatArray(ObservableFloatArray)

它还包含九个实用方法,与java.util.Collections中同名的方法类似。它们都作用于ObservableList物体。它们与它们的java.util.Collections对手的不同之处在于,当它们作用于ObservableList时,注意只生成一个列表改变事件,而它们的java.util.Collections对手会生成不止一个列表改变事件。

  • void copy(ObservableList<? super T> dest, java.util.List<? extends T> src)
  • void fill(ObservableList<? super T> list, T obj)
  • boolean replaceAll(ObservableList<T> list, T oldVal, T newVal)
  • void reverse(ObservableList list)
  • void rotate(ObservableList list, int distance)
  • void shuffle(ObservableList<?> list)
  • void shuffle(ObservableList list, java.util.Random rnd)
  • void sort(ObservableList<T> list)
  • void sort(ObservableList<T> list, java.util.Comparator<? super T> c)

我们在清单 5-6 中展示了这些实用方法的效果。

package com.projavafx.collections;

import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;

import java.util.Arrays;
import java.util.Comparator;
import java.util.Random;

public class FXCollectionsExample {
    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 copy: ");
        FXCollections.copy(strings, Arrays.asList("Four", "Five"));

        System.out.println("Calling replaceAll: ");
        FXCollections.replaceAll(strings, "Two", "Two_1");

        System.out.println("Calling reverse: ");
        FXCollections.reverse(strings);

        System.out.println("Calling rotate(strings, 2): ");
        FXCollections.rotate(strings, 2);

        System.out.println("Calling shuffle(strings): ");
        FXCollections.shuffle(strings);

        System.out.println("Calling shuffle(strings, new Random(0L)): ");
        FXCollections.shuffle(strings, new Random(0L));

        System.out.println("Calling sort(strings): ");
        FXCollections.sort(strings);

        System.out.println("Calling sort(strings, c) with custom comparator: ");
        FXCollections.sort(strings, new Comparator<String>() {
            @Override
            public int compare(String lhs, String rhs) {
                // Reverse the order
                return rhs.compareTo(lhs);
            }
        });

        System.out.println("Calling fill(strings, \"Ten\"): ");
        FXCollections.fill(strings, "Ten");
    }

    // We omitted the nested class MyListener, which is the same as in Listing 5-2
}

Listing 5-6.
FXCollectionsExample.java

当我们运行清单 5-6 中的程序时,以下输出被打印到控制台:

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 copy:
        list = [Four, Five, Two, Three]
        Change event data:
                cursor = 0
                Kind of change: replaced
                Affected range: [0, 4]
                Added size: 4
                Added sublist: [Four, Five, Two, Three]
                Removed size: 4
                Removed: [Zero, One, Two, Three]

Calling replaceAll:
        list = [Four, Five, Two_1, Three]
        Change event data:
                cursor = 0
                Kind of change: replaced
                Affected range: [0, 4]
                Added size: 4
                Added sublist: [Four, Five, Two_1, Three]
                Removed size: 4
                Removed: [Four, Five, Two, Three]

Calling reverse:
        list = [Three, Two_1, Five, Four]
        Change event data:
                cursor = 0
                Kind of change: replaced
                Affected range: [0, 4]
                Added size: 4
                Added sublist: [Three, Two_1, Five, Four]
                Removed size: 4
                Removed: [Four, Five, Two_1, Three]

Calling rotate(strings, 2):
        list = [Five, Four, Three, Two_1]
        Change event data:
                cursor = 0
                Kind of change: replaced
                Affected range: [0, 4]
                Added size: 4
                Added sublist: [Five, Four, Three, Two_1]
                Removed size: 4
                Removed: [Three, Two_1, Five, Four]

Calling shuffle(strings):
        list = [Three, Four, Two_1, Five]
        Change event data:
                cursor = 0
                Kind of change: replaced
                Affected range: [0, 4]
                Added size: 4
                Added sublist: [Three, Four, Two_1, Five]
                Removed size: 4
                Removed: [Five, Four, Three, Two_1]

Calling shuffle(strings, new Random(0L)):
        list = [Five, Three, Four, Two_1]
        Change event data:
                cursor = 0
                Kind of change: replaced
                Affected range: [0, 4]
                Added size: 4
                Added sublist: [Five, Three, Four, Two_1]
                Removed size: 4
                Removed: [Three, Four, Two_1, Five]
Calling sort(strings):
        list = [Five, Four, Three, Two_1]
        Change event data:
                cursor = 0
                Kind of change: permutated
                Affected range: [0, 4]
                Permutation: [0->0, 1->2, 2->1, 3->3]
Calling sort(strings, c) with custom comparator:
        list = [Two_1, Three, Four, Five]
        Change event data:
                cursor = 0
                Kind of change: permutated
                Affected range: [0, 4]
                Permutation: [0->3, 1->2, 2->1, 3->0]

Calling fill(strings, "Ten"):
        list = [Ten, Ten, Ten, Ten]
        Change event data:
                cursor = 0
                Kind of change: replaced
                Affected range: [0, 4]
                Added size: 4
                Added sublist: [Ten, Ten, Ten, Ten]
                Removed size: 4
                Removed: [Two_1, Three, Four, Five]

请注意,每次调用FXCollections中的实用程序方法都会生成一个列表更改事件。

使用 JavaFX 并发框架

众所周知,现在几乎所有的 GUI 平台都使用单线程事件调度模型。JavaFX 也不例外,事实上 JavaFX 中的所有 UI 事件都是在 JavaFX 应用程序线程中处理的。然而,随着近年来多核台式机变得越来越普遍(例如,本章是在四核 PC 上编写的),JavaFX 的设计人员很自然地通过利用 Java 编程语言的出色并发支持来利用硬件的全部功能。

在本节中,我们将研究所有 JavaFX 应用程序中存在的重要线程。我们解释它们在 JavaFX 应用程序的整体方案中扮演的角色。然后我们将注意力转向 JavaFX 应用程序线程,解释为什么在 JavaFX 应用程序线程中执行长时间运行的代码会使您的应用程序看起来挂起。最后,我们来看一下javafx.concurrent框架,并向您展示如何使用它在 JavaFX 应用程序线程之外的工作线程中执行长期运行的代码,并将结果反馈给 JavaFX 应用程序线程以更新 GUI 状态。

Note

如果您熟悉 Swing 编程,JavaFX 应用程序线程类似于 Swing 的事件调度器线程(EDT),通常名为 AWT-EventQueue-0。

识别 JavaFX 应用程序中的线程

清单 5-7 中的程序创建了一个简单的 JavaFX GUI,带有一个ListView、一个TextArea和一个Button,并用应用程序所有活动线程的名称填充 ListView。当您从ListView中选择一个项目时,该线程的堆栈跟踪会显示在TextArea中。线程和堆栈跟踪的原始列表是在应用程序启动时填充的。您可以通过单击 update 按钮来更新线程和堆栈跟踪的列表。

package com.projavafx.collections;

import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ListView;
import javafx.scene.control.TextArea;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

import java.util.Map;

public class JavaFXThreadsExample extends Application
    implements EventHandler<ActionEvent>, ChangeListener<Number> {

    private Model model;
    private View view;

    public static void main(String[] args) {
        launch(args);
    }

    public JavaFXThreadsExample() {
        model = new Model();
    }

    @Override
    public void start(Stage stage) throws Exception {
        view = new View(model);
        hookupEvents();
        stage.setTitle("JavaFX Threads Information");
        stage.setScene(view.scene);
        stage.show();
    }

    private void hookupEvents() {
        view.updateButton.setOnAction(this);
        view.threadNames.getSelectionModel().selectedIndexProperty().addListener(this);
    }

    @Override
    public void changed(ObservableValue<? extends Number> observableValue,
                        Number oldValue, Number newValue) {
        int index = (Integer) newValue;
        if (index >= 0) {
            view.stackTrace.setText(model.stackTraces.get(index));
        }
    }

    @Override
    public void handle(ActionEvent actionEvent) {
        model.update();
    }

    public static class Model {
        public ObservableList<String> threadNames;
        public ObservableList<String> stackTraces;

        public Model() {
            threadNames = FXCollections.observableArrayList();
            stackTraces = FXCollections.observableArrayList();
            update();
        }

        public void update() {
            threadNames.clear();
            stackTraces.clear();
            final Map<Thread, StackTraceElement[]> map = Thread.getAllStackTraces();
            for (Map.Entry<Thread, StackTraceElement[]> entry : map.entrySet()) {
                threadNames.add("\"" + entry.getKey().getName() + "\"");
                stackTraces.add(formatStackTrace(entry.getValue()));
            }
        }

        private String formatStackTrace(StackTraceElement[] value) {
            StringBuilder sb = new StringBuilder("StackTrace: \n");
            for (StackTraceElement stackTraceElement : value) {
                sb.append("    at ").append(stackTraceElement.toString()).append("\n");
            }
            return sb.toString();
        }
    }

    private static class View {
        public ListView<String> threadNames;
        public TextArea stackTrace;
        public Button updateButton;
        public Scene scene;

        private View(Model model) {
            threadNames = new ListView<>(model.threadNames);
            stackTrace = new TextArea();
            updateButton = new Button("Update");
            VBox vBox = new VBox(10, threadNames, stackTrace, updateButton);
            vBox.setPadding(new Insets(10, 10, 10, 10));
            scene = new Scene(vBox, 440, 640);
        }
    }
}

Listing 5-7.
JavaFXThreadsExample.

java

这是一个非常小的 JavaFX GUI 应用程序。在让您运行该程序之前,我们指出了该程序的几个特点。首先,记住main()方法:

public static void main(String[] args) {
    launch(args);
}

这个方法你已经见过几次了。这种风格化的main()方法总是出现在扩展了javafx.application.Application类的类中。有一个重载版本的Application.launch()方法,它将一个Class对象作为第一个参数,可以从其他类调用:

launch(Class<? Extends Application> appClass, String[] args)

因此,您可以将main()方法移到另一个类中:

public class Main {
    public static void main(String[] args) {
        Application.launch(JavaFXThreadsExample.class, args);
    }
}

达到同样的效果。

接下来,请注意嵌套类Model在其update()方法中构建了它的数据模型,该模型由所有活动线程的列表和每个线程的堆栈跟踪组成:

public void update() {
    threadNames.clear();
    stackTraces.clear();
    final Map<Thread, StackTraceElement[]> map = Thread.getAllStackTraces();
    for (Map.Entry<Thread, StackTraceElement[]> entry : map.entrySet()) {
        threadNames.add("\"" + entry.getKey().getName() + "\"");
        stackTraces.add(formatStackTrace(entry.getValue()));
    }
}

该方法在Model的构造器中调用一次,从JavaFXThreadsExample的构造器调用,从更新按钮的事件处理程序调用一次。

当我们运行清单 5-7 中的程序时,屏幕上会显示图 5-5 中的 GUI。您可以通过单击列表中的每个线程名称并在文本区域中查看该线程的堆栈跟踪来浏览 JavaFX 程序中的线程。以下是一些有趣的观察结果:

  • main”线程的调用堆栈包括对com.sun.javafx.application.LauncherImpl.launchApplication()的调用。
  • JavaFX-Launcher”线程的调用堆栈包括对com.sun.javafx.application.PlatformImpl.runAndWait()的调用。这将代码(包括构造器的调用)放在 JavaFX 应用程序线程上。
  • JavaFX Application Thread”线程的调用栈包括 Windows 机器上的本机com.sun.glass.ui.win.WinApplication._runLoop()方法,以及 Mac 或 Linux 机器上的类似方法。
  • QuantumRenderer-0”线程的调用栈包含了com.sun.javafx.tk.quantum.QuantumRenderer$PipelineRunnable.run()方法。

现在,当您单击 Update 按钮并检查“JavaFX Application Thread”线程的调用堆栈时,您会发现 Update 按钮的事件处理程序是在 JavaFX 应用程序线程上执行的。

A323806_4_En_5_Fig5_HTML.jpg

图 5-5。

The JavaFXThreadsExample program

这个小实验揭示了 JavaFX 运行时系统的一些架构元素。尽管部分信息包括由com.sun层次结构中的类适当表示的实现细节(因此不能在普通 JavaFX 应用程序的代码中使用),但是了解一些内部工作原理是有益的。

Caution

在接下来的讨论中,我们提到了包中名称以com.sun开头的 Java 类。这些类是 JavaFX 运行时系统的实现细节,不打算在普通的 JavaFX 应用程序中使用。它们可能会在 JavaFX 的未来版本中发生变化。

javafx.application.Application类为 JavaFX 应用程序提供生命周期支持。除了我们在本节前面提到的两个静态launch()方法,它还提供了以下生命周期方法。

  • public void init() throws Exception
  • public abstract void start(Stage stage) throws Exception
  • public void stop() throws Exception

在“JavaFX-Launcher”线程中调用了init()方法。在 JavaFX 应用程序线程中调用构造器、start()stop()方法。JavaFX 应用程序线程是com.sun.glass包层次结构中玻璃窗口工具包的一部分。JavaFX 事件在 JavaFX 应用程序线程上处理。所有实时场景操作都必须在 JavaFX 应用程序线程中执行。未附加到实时场景的节点可以在其他线程中创建和操纵,直到它们附加到实时场景。

Note

Glass Windowing Toolkit 在 JavaFX 应用程序中的作用类似于 AWT 在 Swing 应用程序中的作用。它从本机平台提供绘图表面和输入事件。在 AWT 中,EDT 不同于本机平台的 UI 线程,它们之间必须进行通信,与 AWT 不同,Glass Windowing Toolkit 中的 JavaFX 应用程序线程直接使用本机平台的 UI 线程。

"QuantumRenderer-0"线程的所有者是位于com.sun.javafx.tk.quantum包层次结构中的 Quantum 工具包。该线程负责使用com.sun.prism包层次结构中的 Prism 图形引擎渲染 JavaFX 场景图。如果 JavaFX 支持图形硬件,Prism 将使用完全加速的渲染路径。如果 JavaFX 不支持图形硬件,Prism 将退回到 Java2D 渲染路径。Quantum Toolkit 还负责协调事件线程和渲染线程的活动。它使用pulse事件进行协调。

Note

脉冲事件是放在 JavaFX 应用程序线程队列中的事件。当它被处理时,它沿着渲染层同步场景图形元素的状态。如果场景图形的状态发生变化(通过运行动画或直接修改场景图形),则会安排脉冲事件。脉冲事件被限制在每秒 60 帧。

如果 JavaFXThreadsExample 程序包含媒体播放,那么另一个名为“JFXMedia Player EventQueueThread”的线程就会出现在列表中。这个线程负责通过使用 JavaFX 应用程序线程来同步场景图中的最新帧。

修复无响应的用户界面

事件处理程序在 JavaFX 应用程序线程上执行,因此,如果一个事件处理程序花费太长时间来完成它的工作,整个 UI 将变得无响应,因为任何后续的用户操作将简单地排队,直到长时间运行的事件处理程序完成后才会被处理。

我们在清单 5-8 中对此进行了说明。

package com.projavafx.collections;

import javafx.application.Application;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;

public class UnresponsiveUIExample extends Application {
    private Model model;
    private View view;

    public static void main(String[] args) {
        launch(args);
    }

    public UnresponsiveUIExample() {
        model = new Model();
    }

    @Override
    public void start(Stage stage) throws Exception {
        view = new View(model);
        hookupEvents();
        stage.setTitle("Unresponsive UI Example");
        stage.setScene(view.scene);
        stage.show();
    }

    private void hookupEvents() {
        view.changeFillButton.setOnAction(actionEvent -> {
            final Paint fillPaint = model.getFillPaint();
            if (fillPaint.equals(Color.LIGHTGRAY)) {
                model.setFillPaint(Color.GRAY);
            } else {
                model.setFillPaint(Color.LIGHTGRAY);
            }
            // Bad code, this will cause the UI to be unresponsive
            try {
                Thread.sleep(Long.MAX_VALUE);
            } catch (InterruptedException e) {
                //  TODO properly handle interruption
            }
        });

        view.changeStrokeButton.setOnAction(actionEvent -> {
            final Paint strokePaint = model.getStrokePaint();
            if (strokePaint.equals(Color.DARKGRAY)) {
                model.setStrokePaint(Color.BLACK);
            } else {
                model.setStrokePaint(Color.DARKGRAY);
            }
        });
    }

    private static class Model {
        private ObjectProperty<Paint> fillPaint = new SimpleObjectProperty<>();
        private ObjectProperty<Paint> strokePaint = new SimpleObjectProperty<>();

        private Model() {
            fillPaint.set(Color.LIGHTGRAY);
            strokePaint.set(Color.DARKGRAY);
        }

        final public Paint getFillPaint() {
            return fillPaint.get();
        }

        final public void setFillPaint(Paint value) {
            this.fillPaint.set(value);
        }

        final public Paint getStrokePaint() {
            return strokePaint.get();
        }

        final public void setStrokePaint(Paint value) {
            this.strokePaint.set(value);
        }

        final public ObjectProperty<Paint> fillPaintProperty() {
            return fillPaint;
        }

        final public ObjectProperty<Paint> strokePaintProperty() {
            return strokePaint;
        }
    }

    private static class View {
        public Rectangle rectangle;
        public Button changeFillButton;
        public Button changeStrokeButton;
        public HBox buttonHBox;
        public Scene scene;

        private View(Model model) {
            rectangle = new Rectangle(200, 200);
            rectangle.setStrokeWidth(10);
            rectangle.fillProperty().bind(model.fillPaintProperty());
            rectangle.strokeProperty().bind(model.strokePaintProperty());

            changeFillButton = new Button("Change Fill");
            changeStrokeButton = new Button("Change Stroke");

            buttonHBox = new HBox(10, changeFillButton, changeStrokeButton);
            buttonHBox.setPadding(new Insets(10, 10, 10, 10));
            buttonHBox.setAlignment(Pos.CENTER);

            BorderPane root = new BorderPane(rectangle, null, null, buttonHBox, null);
            root.setPadding(new Insets(10, 10, 10, 10));

            scene = new Scene(root);
        }
    }
}

Listing 5-8.
UnresponsiveUIExample.

java

这个类建立了一个简单的 UI,在一个BorderPane的中心有一个带有明显的Color.DARKGRAY笔划和Color.LIGHTGRAY填充的矩形,在底部有两个标记为“改变填充”和“改变笔划”的按钮。更改填充按钮应该在Color.LIGHTGRAYColor.GRAY之间切换矩形的填充。改变笔划按钮应该在Color.DARKGRAYColor.BLACK之间切换矩形的笔划。当我们运行清单 5-8 中的程序时,屏幕上会显示图 5-6 中的 GUI。

A323806_4_En_5_Fig6_HTML.jpg

图 5-6。

The UnresponsiveUIExample program

但是,这个程序在“更改填充”按钮的事件处理程序中有一个错误:

@Override
public void handle(ActionEvent actionEvent) {
    final Paint fillPaint = model.getFillPaint();
    if (fillPaint.equals(Color.LIGHTGRAY)) {
        model.setFillPaint(Color.GRAY);
    } else {
        model.setFillPaint(Color.LIGHTGRAY);
    }
    // Bad code, this will cause the UI to be unresponsive
    try {
        Thread.sleep(Long.MAX_VALUE);
    } catch (InterruptedException e) {
        // TODO properly handle interruption
    }
}

Thread.sleep(Long.MAX_VALUE)模拟需要很长时间执行的代码。在实际应用中,这可能是一个数据库调用、一个 web 服务调用或一段复杂的代码。因此,如果您单击“更改填充”按钮,在矩形中看不到颜色变化。更糟糕的是,整个 UI 似乎被锁定了:更改填充和更改描边按钮停止工作;操作系统提供的关闭窗口按钮将不会产生预期的效果。操作系统也可能将该程序标记为不响应,停止该程序的唯一方法是使用操作系统的强制终止功能。

要解决这样的问题,我们需要将长时间运行的代码卸载到工作线程,并将长时间计算的结果传递回 JavaFX 应用程序线程,以更新 UI 的状态,以便用户可以看到结果。根据您学习 Java 的时间,您对将代码卸载到工作线程的第一个问题的回答可能会有所不同。如果你是一名资深的 Java 程序员,你的本能反应可能是实例化一个Runnable,将其包装在一个Thread中,并对其调用start()。如果你在 Java 5 之后开始使用 Java,并且学习了java.util.concurrent类的层次结构,你的反应可能是站起来一个java.util.concurrent.ExecutorService并向它提交java.util.concurrent.FutureTask s。JavaFX 在javafx.concurrent包中包含了一个基于后一种方法的工作线程框架。

我们将在接下来的几节中研究这个框架中的接口和类,但在此之前,我们使用RunnableThread方法将计算卸载到一个工作线程。我们在这里的目的是强调第二个问题的答案,即如何使代码从一个工作线程运行在 JavaFX 应用程序线程上。完整的修正程序可以在ResponsiveUIExample.java中找到。以下是“更改填充”按钮的事件处理程序的新代码:

view.changeFillButton.setOnAction(actionEvent -> {
    final Paint fillPaint = model.getFillPaint();
    if (fillPaint.equals(Color.LIGHTGRAY)) {
        model.setFillPaint(Color.GRAY);
    } else {
        model.setFillPaint(Color.LIGHTGRAY);
    }
    Runnable task = () -> {
        try {
            Thread.sleep(3000);
            Platform.runLater(() -> {
                final Rectangle rect = view.rectangle;
                double newArcSize =
                    rect.getArcHeight() < 20 ? 30 : 0;
                rect.setArcWidth(newArcSize);
                rect.setArcHeight(newArcSize);
            });
        } catch (InterruptedException e) {
            // TODO properly handle interruption
        }
    };
    new Thread(task).start();
});

我们已经用在工作线程中执行的代码取代了长睡眠。休眠三秒钟后,工作线程调用javafx.application.Platform类的runLater()方法,传递给它另一个切换矩形圆角的Runnable。因为长时间运行的计算是在工作线程中完成的,所以事件处理程序不会阻塞 JavaFX 应用程序线程。填充的变化现在立即反映在用户界面中。因为Platform.runLater()调用导致Runnable在 JavaFX 应用程序线程上执行,所以圆角的变化在三秒钟后反映在 UI 中。我们必须在 JavaFX 应用程序线程上执行Runnable的原因是它修改了现场场景的状态。

Platform类包括以下其他有用的实用方法:

  • 如果在 JavaFX 应用程序线程上执行,public static boolean isFxApplicationThread()返回true,否则返回false
  • public static boolean isSupported(ConditionalFeature)测试执行环境是否支持ConditionalFeature。可测试的ConditionalFeatures包括GRAPHICSCONTROLSMEDIAWEBSWTSWINGFXMLSCENE3DEFFECTSHAPE_CLIPINPUT_METHODTRANSPARENT_WINDOWUNIFIED_WINDOWTWO_LEVEL_FOCUSVIRTUAL_KEYBOARDINPUT_TOUCHINPUT_MULTITOUCHINPUT_POINTER
  • 如果在应用程序的start()方法被调用之后调用public static void exit(),则在 JavaFX 应用程序线程和其他 JavaFX 平台线程被关闭之前,导致应用程序的stop()方法在 JavaFX 应用程序线程上执行。如果应用程序的start()方法还没有被调用,应用程序的stop()方法可能不会被调用。
  • public static boolean isImplicitExit()public static void setImplicitExit(boolean)测试并设置隐式退出标志。当此标志为真时,JavaFX 运行时将在最后一个应用程序窗口关闭时关闭;否则,您必须显式调用Platform.exit()来关闭 JavaFX 运行时。该标志的默认值为true

Note

如果你熟悉 Swing 编程,你应该会看到 JavaFX 的Platform.runLater()和 Swing 的EventQueue.invokeLater(),或者说SwingUtilities.invokeLater()的相似之处。

既然我们已经用RunnableThreadPlatform.runLater()解决了我们的问题,是时候看看我们如何使用 JavaFX 的内置工人线程框架以一种更加灵活和优雅的方式解决问题了。

了解 javafx.concurrent 框架

javafx.concurrent包中的 JavaFX worker 线程框架将 Java 5 中引入的 Java 并发框架的多功能性和灵活性与 JavaFX 属性和绑定框架的便利性相结合,以产生一个易于使用的工具集,该工具集能够识别 JavaFX 应用程序线程规则,并且非常易于使用。它由一个接口Worker和实现该接口的三个抽象基类Task<V>Service<V>ScheduledService<V>组成。

了解工作接口

Worker接口指定了一个 JavaFX bean,它有九个只读属性、一个名为cancel()的方法、一个状态模型和状态转换规则。一个Worker代表一个工作单元,它运行在一个或多个后台线程中,但是 JavaFX 应用程序线程可以安全地观察到它的一些内部状态。九个只读属性如下。

  • title是表示任务标题的String属性。
  • message是一个String属性,表示随着任务的进行,更详细的消息。
  • running是一个只有当Worker处于Worker.State.SCHEDULEDWorker.State.RUNNING状态时才为真的boolean属性。
  • state是一个代表任务的Worker.StateObject属性。
  • totalWork是一个double属性,表示任务的总工作量。当工作总量未知时,其值为–1.0
  • workDone是一个double属性,表示任务中到目前为止已经完成的工作量。它的值是–1.0或介于0totalWork之间的一个数。
  • progress是一个double属性,表示任务中到目前为止已经完成的总工作量的百分比。其值为–1.0workDonetotalWork之比。
  • value是表示任务输出的Object属性。只有当任务已经成功完成,即已经达到Worker.State.SUCCEEDED状态时,它的值才是非空的。
  • exception是一个Object属性,表示任务的实现已经抛出给 JavaFX worker 线程框架的一个Throwable。只有当任务处于Worker.State.FAILED状态时,它的值才是非空的。

上述属性旨在从 JavaFX 应用程序线程中访问。将场景图属性绑定到它们是安全的,因为这些属性的失效事件和更改事件是在 JavaFX 应用程序线程上触发的。通过在许多 GUI 应用程序中看到的虚构的任务进度消息框来考虑这些属性是有帮助的。它们通常有一个标题,一个进度条,指示已经完成的工作的百分比,还有一条消息告诉用户已经处理了多少项,还有多少项需要处理。所有这些属性都是由 JavaFX worker threading framework 本身或任务的实际实现来设置的。

runningstatevalueexception属性由框架控制,无需用户干预即可在 JavaFX 应用程序线程中观察到它们。当框架想要更改这些属性时,它会确保更改是在 JavaFX 应用程序线程上完成的。任务的实现代码可以通过调用框架提供的受保护方法来更新titlemessagetotalWorkworkDoneprogress属性,这些方法保证在 JavaFX 应用程序线程上完成更改。

Worker.State是一个嵌套枚举,它定义了Worker的以下六种状态:

  • READY (initial state)
  • SCHEDULED (transitional state)
  • RUNNING (transitional state)
  • SUCCEEDED (terminal state)
  • CANCELLED (terminal state)
  • FAILED (terminal state)

如果还没有处于SUCCEEDEDFAILED状态,cancel()方法将把Worker转换到CANCELLED状态。

现在您已经熟悉了Worker接口的属性和状态,您可以继续学习 JavaFX worker threading framework 中实现该接口的三个抽象类,Task<V>Service<V>以及ScheduledService<V>

理解任务抽象类

Task<V>抽象类是用于一次性任务的Worker接口的实现。一旦它的状态前进到SUCCEEDEDFAILEDCANCELLED,它将永远停留在终止状态。Task<V>抽象类扩展了FutureTask<V>类,因此支持RunnableFuture<V>RunnableFuture<V>接口以及Worker接口。Future<V>RunnableFuture<V>FutureTask<V>接口和类是java.util.concurrent包的一部分。由于这种继承,一个Task<V>对象可以以适合其父类的各种方式使用。然而,对于典型的 JavaFX 用法,只使用Task<V>类本身的方法就足够了,可以在该类的 Javadoc 中找到这些方法的列表。以下是这些方法的列表,不包括上一节中讨论的只读属性:

  • protected abstract V call() throws Exception
  • public final boolean cancel()
  • public boolean cancel(boolean mayInterruptIfRunning)
  • protected void updateTitle(String title)
  • protected void updateMessage(String message)
  • protected void updateProgress(long workDone, long totalWork)
  • protected void updateProgress(double workDone, double totalWork)
  • protected void updateValue(V)

Task<V>抽象类实现了javafx.event.EventTarget接口。它支持的事件由WorkerStateEvent类表示。五个Worker.State中的每一个都有一个WorkerStateEvent,当Task<V>转换到一个状态时,事件被触发。在Task<V>中有五个类型为EventHandler<WorkerStateEvent>的对象属性和五个受保护的方法。触发相应的事件时,将调用这些事件处理程序和受保护的方法:

  • onScheduled属性
  • onRunning属性
  • onSucceeded属性
  • onCancelled属性
  • onFailed属性
  • protected void scheduled()
  • protected void running()
  • protected void succeeded()
  • protected void cancelled()
  • protected void failed()

Task<V>的扩展必须覆盖受保护的抽象call()方法来执行实际的工作。call()方法的实现可以调用受保护的方法updateTitle()updateMessage()updateProgress()updateValue()向 JavaFX 应用线程发布其内部状态。实现完全控制任务的标题和消息。对于需要两个 longs 的updateProgress()调用,workDonetotalWork必须要么都是–1,表示进度不确定,要么满足关系workDone >= 0workDone <= totalWork,导致progress值在0.01.0之间(0%100%)。

Caution

如果workDone > totalWork,或者其中一个是<–1,那么updateProgress() API 将抛出一个异常。但是,它允许你传入(0, 0),导致一个NaN的进程。

这两个cancel()方法可以从任何线程调用,如果任务还没有处于SUCCEEDEDFAILED状态,它们会将任务转移到CANCELLED状态。如果在任务运行之前调用了任何一个cancel()方法,它将进入CANCELLED状态,并且永远不会运行。两个cancel()方法的不同之处仅在于任务处于RUNNING状态,并且仅在于它们对正在运行的线程的处理。如果cancel(true)被调用,线程将接收一个中断。为了使该中断具有使任务快速完成处理的预期效果,必须对call()方法的实现进行编码,以检测该中断并跳过任何进一步的处理。无参数的cancel()方法简单地转发到cancel(true)

清单 5-9 展示了一个Task的创建,启动它,并从一个显示所有九个属性的简单 GUI 中观察任务的属性。

package com.projavafx.collections;

import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.concurrent.Task;
import javafx.concurrent.Worker;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressBar;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;

import java.util.concurrent.atomic.AtomicBoolean;

public class WorkerAndTaskExample extends Application {
    private Model model;
    private View view;

    public static void main(String[] args) {
        launch(args);
    }

    public WorkerAndTaskExample() {
        model = new Model();
    }

    @Override
    public void start(Stage stage) throws Exception {
        view = new View(model);
        hookupEvents();
        stage.setTitle("Worker and Task Example");
        stage.setScene(view.scene);
        stage.show();
    }

    private void hookupEvents() {
        view.startButton.setOnAction(actionEvent -> {
            new Thread((Runnable) model.worker).start();
        });
        view.cancelButton.setOnAction(actionEvent -> {
            model.worker.cancel();
        });
        view.exceptionButton.setOnAction(actionEvent -> {
            model.shouldThrow.getAndSet(true);
        });
    }

    private static class Model {
        public Worker<String> worker;
        public AtomicBoolean shouldThrow = new AtomicBoolean(false);

        private Model() {
            worker = new Task<String>() {
                @Override
                protected String call() throws Exception {
                    updateTitle("Example Task");
                    updateMessage("Starting...");
                    final int total = 250;
                    updateProgress(0, total);
                    for (int i = 1; i <= total; i++) {
                        if (isCancelled()) {
                            updateValue("Canceled at " + System.currentTimeMillis());
                            return null; // ignored
                        }
                        try {
                            Thread.sleep(20);
                        } catch (InterruptedException e) {
                                updateValue("Canceled at " + System.currentTimeMillis());
                                return null; // ignored                        }
                        if (shouldThrow.get()) {
                            throw new RuntimeException("Exception thrown at " + System.currentTimeMillis());
                        }
                        updateTitle("Example Task (" + i + ")");
                        updateMessage("Processed " + i + " of " + total + " items.");
                        updateProgress(i, total);
                    }
                    return "Completed at " + System.currentTimeMillis();
                }

                @Override
                protected void scheduled() {
                    System.out.println("The task is scheduled.");
                }

                @Override
                protected void running() {
                    System.out.println("The task is running.");
                }
            };
            ((Task<String>) worker).setOnSucceeded(event -> {
                System.out.println("The task succeeded.");
            });
            ((Task<String>) worker).setOnCancelled(event -> {
                System.out.println("The task is canceled.");
            });
            ((Task<String>) worker).setOnFailed(event -> {
                System.out.println("The task failed.");
            });
        }
    }

    private static class View {
        public ProgressBar progressBar;

        public Label title;
        public Label message;
        public Label running;
        public Label state;
        public Label totalWork;
        public Label workDone;
        public Label progress;
        public Label value;
        public Label exception;

        public Button startButton;
        public Button cancelButton;
        public Button exceptionButton;

        public Scene scene;

        private View(final Model model) {
            progressBar = new ProgressBar();
            progressBar.setMinWidth(250);

            title = new Label();
            message = new Label();
            running = new Label();
            state = new Label();
            totalWork = new Label();
            workDone = new Label();
            progress = new Label();
            value = new Label();
            exception = new Label();

            startButton = new Button("Start");
            cancelButton = new Button("Cancel");
            exceptionButton = new Button("Exception");

            final ReadOnlyObjectProperty<Worker.State> stateProperty =
                model.worker.stateProperty();

            progressBar.progressProperty().bind(model.worker.progressProperty());

            title.textProperty().bind(
                model.worker.titleProperty());
            message.textProperty().bind(
                model.worker.messageProperty());
            running.textProperty().bind(
                Bindings.format("%s", model.worker.runningProperty()));
            state.textProperty().bind(
                Bindings.format("%s", stateProperty));
            totalWork.textProperty().bind(
                model.worker.totalWorkProperty().asString());
            workDone.textProperty().bind(
                model.worker.workDoneProperty().asString());
            progress.textProperty().bind(
                Bindings.format("%5.2f%%", model.worker.progressProperty().multiply(100)));
            value.textProperty().bind(
                model.worker.valueProperty());
            exception.textProperty().bind(Bindings.createStringBinding(() -> {
                final Throwable exception = model.worker.getException();
                if (exception == null) return "";
                return exception.getMessage();
            }, model.worker.exceptionProperty()));

            startButton.disableProperty().bind(
                stateProperty.isNotEqualTo(Worker.State.READY));
            cancelButton.disableProperty().bind(
                stateProperty.isNotEqualTo(Worker.State.RUNNING));
            exceptionButton.disableProperty().bind(
                stateProperty.isNotEqualTo(Worker.State.RUNNING));

            HBox topPane = new HBox(10, progressBar);
            topPane.setAlignment(Pos.CENTER);
            topPane.setPadding(new Insets(10, 10, 10, 10));

            ColumnConstraints constraints1 = new ColumnConstraints();
            constraints1.setHalignment(HPos.CENTER);
            constraints1.setMinWidth(65);

            ColumnConstraints constraints2 = new ColumnConstraints();
            constraints2.setHalignment(HPos.LEFT);
            constraints2.setMinWidth(200);

            GridPane centerPane = new GridPane();
            centerPane.setHgap(10);
            centerPane.setVgap(10);
            centerPane.setPadding(new Insets(10, 10, 10, 10));
            centerPane.getColumnConstraints()
                .addAll(constraints1, constraints2);

            centerPane.add(new Label("Title:"), 0, 0);
            centerPane.add(new Label("Message:"), 0, 1);
            centerPane.add(new Label("Running:"), 0, 2);
            centerPane.add(new Label("State:"), 0, 3);
            centerPane.add(new Label("Total Work:"), 0, 4);
            centerPane.add(new Label("Work Done:"), 0, 5);
            centerPane.add(new Label("Progress:"), 0, 6);
            centerPane.add(new Label("Value:"), 0, 7);
            centerPane.add(new Label("Exception:"), 0, 8);

            centerPane.add(title, 1, 0);
            centerPane.add(message, 1, 1);
            centerPane.add(running, 1, 2);
            centerPane.add(state, 1, 3);
            centerPane.add(totalWork, 1, 4);
            centerPane.add(workDone, 1, 5);
            centerPane.add(progress, 1, 6);
            centerPane.add(value, 1, 7);
            centerPane.add(exception, 1, 8);

            HBox buttonPane = new HBox(10,
                startButton, cancelButton, exceptionButton);
            buttonPane.setPadding(new Insets(10, 10, 10, 10));
            buttonPane.setAlignment(Pos.CENTER);

            BorderPane root = new BorderPane(centerPane,
                topPane, null, buttonPane, null);
            scene = new Scene(root);
        }
    }
}

Listing 5-9.
WorkerAndTaskExample.

java

这个程序的Model嵌套类包含一个Worker类型的worker字段和一个AtomicBoolean类型的shouldThrow字段。字段worker被初始化为一个匿名子类Task<String>的实例,该实例通过以每项 20 毫秒的速度模拟 250 项的处理来实现其call()方法。它在调用开始时和循环的每次迭代中更新任务的属性。它在两个地方处理取消。它在每次迭代的顶部检查isCancelled()标志,并且还检查Thread.sleep()调用的InterruptedException处理程序中的isCancelled()标志。如果任务被取消,它调用updateValue(),并从loop中出来,迅速返回。返回值被框架忽略。shouldThrow字段由View控制,通知任务应该抛出异常。

这个程序的View嵌套类创建了一个简单的 UI,在顶部有一个ProgressBar,在中心有一组Labels,显示工作人员的各种属性,在底部有三个按钮。Label的内容与worker的各种属性绑定在一起。按钮的disable属性也被绑定到worker的 state 属性,以便在任何时候只有相关的按钮被启用。例如,“开始”按钮在程序启动时是启用的,但在按下该按钮并开始执行任务后就被禁用了。同样,只有当任务正在运行时,才会启用“取消”和“异常”按钮。

当我们运行清单 5-9 中的程序时,屏幕上会显示图 5-7 中的 GUI。

A323806_4_En_5_Fig7_HTML.jpg

图 5-7。

The WorkerAndTaskExample program after starting up

请注意,进度条处于不确定的状态。标题、消息、值和异常的值为空。跑步的价值是假的。状态的值为就绪,总工作量和已完成工作量的值为–1.0,进度显示为–100%。“开始”按钮被启用,而“取消”和“例外”按钮被禁用。

单击 Start 按钮后,任务开始执行,GUI 会随着任务的进行自动反映属性值。图 5-8 是这个阶段的应用截图。请注意,进度条处于确定状态,反映了任务的进度。Title 和 Message 的值反映了在任务中的call()方法的实现中为这些属性设置了什么。跑步的价值是真实的。State 的值是 RUNNING,而 Total Work、Work Done 和 Progress 的值反映了正在执行的任务的当前状态:250 项中的 156 项已完成。值和例外字段为空,因为任务中既没有值也没有例外。开始按钮现在被禁用。Cancel 和 Exception 按钮已启用,这表示我们可能会尝试取消任务,或者在此时强制从任务中抛出一个异常。

A323806_4_En_5_Fig8_HTML.jpg

图 5-8。

The WorkerAndTaskExample program while a task is in progress

当任务正常完成时,我们到达图 5-9 中的截图。请注意,进度条位于 100.00%。“标题”、“消息”、“总工时”、“已完成工时”和“进度”域的值都反映了任务已经处理完所有 250 个项目的事实。运行值为假。状态为 SUCCEEDED,Value 字段现在包含来自call()方法的返回值。

A323806_4_En_5_Fig9_HTML.jpg

图 5-9。

The WorkerAndTaskExample program after the task succeeded

如果我们没有让任务正常完成,而是点击取消按钮,任务将立即完成,并显示图 5-10 中的屏幕截图。请注意,State 字段的值现在已被取消。值字段包含当任务被取消时我们传递给updateValue()方法的字符串。当我们检测到任务被取消时,我们有两个退出方法体的选择。在清单 5-7 的程序中,我们选择更新值并从方法返回。我们也可以通过抛出一个RuntimeException来选择退出方法体。如果我们做了这个选择,屏幕截图将会有一个空的值字段,但是有一个非空的异常字段。无论如何,工人的状态都是CANCELLED

Caution

当您响应取消而正常返回时,Task的当前实现中的一个错误导致一个IllegalStateException被记录为Task的异常。这将在下一版本中修复。

A323806_4_En_5_Fig10_HTML.jpg

图 5-10。

The WorkerAndTaskExample program after the task has been cancelled

最后一个截图,图 5-11 ,展示了在任务执行时点击异常按钮会发生什么。我们通过从 JavaFX 应用程序中设置一个AtomicBoolean标志来模拟任务中的异常,然后任务在worker线程中拾取这个标志并抛出异常。请注意,“状态”字段的值为“现在失败”。值字段为空,因为任务未成功完成。异常字段填充了我们抛出的RuntimeException的消息。

A323806_4_En_5_Fig11_HTML.jpg

图 5-11。

The WorkerAndTaskExample program after the task threw an exception

我们在Task的匿名扩展中重写了scheduled()running()方法,并在Model类中为成功、取消和失败的事件设置了事件处理程序。当您处理这些场景时,应该会看到这些事件被记录到控制台中。

Note

Task<V>类定义了一次性任务,这些任务只执行一次,不会再次运行。每次运行任务后,您必须重启WorkerAndTaskExample程序。

理解服务抽象类

Service<V>抽象类是打算被重用的Worker接口的实现。它扩展了Worker的状态模型,允许其状态被重置为Worker.State.READYService<V>抽象类不扩展任何类,并且实现了WorkerEventTarget接口。除了Worker接口的九个只读属性外,Service<V>还有一个类型为Executor的读写属性,称为executor。它也有事件处理器属性和受保护的事件回调方法,就像Task类一样。以下是其余Service<V>方法的列表:

  • protected abstract Task<V> createTask()
  • public void start()
  • public void reset()
  • public void restart()
  • public boolean cancel()

Service<V>的扩展必须覆盖受保护的抽象createTask()方法,以生成新创建的Task。只有当Service<V>对象处于Worker.State.READY状态时,才能调用start()方法。它调用createTask()来获得一个新生成的Task,并向executor属性请求一个Executor。如果没有设置executor属性,它会创建自己的Executor。它将Service<V>对象的九个Worker属性绑定到Task的属性。然后将Task转换到Worker.State.SCHEDULED状态,并在Executor上执行Task。只有当Service<V>的状态不是Worker.State.SCHEDULEDWorker.State.RUNNING时,才能调用reset()方法。它只是将九个Service<V>属性从底层Task的属性中解除绑定,并将它们的值重置为新的启动值:state属性为Worker.State.READY,其他属性为null""false–1restart()方法简单地取消当前正在执行的Task,如果有的话,然后执行一个reset(),接着执行一个start()cancel()方法将取消当前正在执行的Task,如果有的话;否则,它将把Service<V>转换到Worker.State.CANCELLED状态。

清单 5-10 展示了使用Service<V>抽象类的匿名子类的一个实例在它自己的Executor中重复执行Task s。

package com.projavafx.collections;

import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import javafx.concurrent.Worker;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressBar;
import javafx.scene.control.TextField;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;

import java.util.concurrent.atomic.AtomicBoolean;

public class ServiceExample extends Application {
    private Model model;
    private View view;

    public static void main(String[] args) {
        launch(args);
    }

    public ServiceExample() {
        model = new Model();
    }

    @Override
    public void start(Stage stage) throws Exception {
        view = new View(model);
        hookupEvents();
        stage.setTitle("Service Example");
        stage.setScene(view.scene);
        stage.show();
    }

    private void hookupEvents() {
        view.startButton.setOnAction(actionEvent -> {
            model.shouldThrow.getAndSet(false);
            ((Service) model.worker).restart();
        });
        view.cancelButton.setOnAction(actionEvent -> {
            model.worker.cancel();
        });
        view.exceptionButton.setOnAction(actionEvent -> {
            model.shouldThrow.getAndSet(true);
        });
    }

    private static class Model {
        public Worker<String> worker;
        public AtomicBoolean shouldThrow = new AtomicBoolean(false);
        public IntegerProperty numberOfItems = new SimpleIntegerProperty(250);

        private Model() {
            worker = new Service<String>() {
                @Override
                protected Task createTask() {
                    return new Task<String>() {
                        @Override
                        protected String call() throws Exception {
                            updateTitle("Example Service");
                            updateMessage("Starting...");
                            final int total = numberOfItems.get();
                            updateProgress(0, total);
                            for (int i = 1; i <= total; i++) {
                                if (isCancelled()) {
                                    updateValue("Canceled at " + System.currentTimeMillis());
                                    return null; // ignored
                                }
                                try {
                                    Thread.sleep(20);
                                } catch (InterruptedException e) {
                                    if (isCancelled()) {
                                        updateValue("Canceled at " + System.currentTimeMillis());
                                        return null; // ignored
                                    }                                }
                                if (shouldThrow.get()) {
                                    throw new RuntimeException("Exception thrown at " + System.currentTimeMillis());
                                }
                                updateTitle("Example Service (" + i + ")");
                                updateMessage("Processed " + i + " of " + total + " items.");
                                updateProgress(i, total);
                            }
                            return "Completed at " + System.currentTimeMillis();
                        }
                    };
                }
            };
        }
    }

    private static class View {
        public ProgressBar progressBar;

        public Label title;
        public Label message;
        public Label running;
        public Label state;
        public Label totalWork;
        public Label workDone;
        public Label progress;
        public Label value;
        public Label exception;

        public TextField numberOfItems;
        public Button startButton;
        public Button cancelButton;
        public Button exceptionButton;

        public Scene scene;

        private View(final Model model) {
            progressBar = new ProgressBar();
            progressBar.setMinWidth(250);

            title = new Label();
            message = new Label();
            running = new Label();
            state = new Label();
            totalWork = new Label();
            workDone = new Label();
            progress = new Label();
            value = new Label();
            exception = new Label();
            numberOfItems = new TextField();
            numberOfItems.setMaxWidth(40);

            startButton = new Button("Start");
            cancelButton = new Button("Cancel");
            exceptionButton = new Button("Exception");

            final ReadOnlyObjectProperty<Worker.State> stateProperty =
                model.worker.stateProperty();

            progressBar.progressProperty().bind(model.worker.progressProperty());

            title.textProperty().bind(
                model.worker.titleProperty());
            message.textProperty().bind(
                model.worker.messageProperty());
            running.textProperty().bind(
                Bindings.format("%s", model.worker.runningProperty()));
            state.textProperty().bind(
                Bindings.format("%s", stateProperty));
            totalWork.textProperty().bind(
                model.worker.totalWorkProperty().asString());
            workDone.textProperty().bind(
                model.worker.workDoneProperty().asString());
            progress.textProperty().bind(
                Bindings.format("%5.2f%%", model.worker.progressProperty().multiply(100)));
            value.textProperty().bind(
                model.worker.valueProperty());
            exception.textProperty().bind(Bindings.createStringBinding(() -> {
                final Throwable exception = model.worker.getException();
                if (exception == null) return "";
                return exception.getMessage();
            }, model.worker.exceptionProperty()));

            model.numberOfItems.bind(Bindings.createIntegerBinding(() -> {
                final String text = numberOfItems.getText();
                int n = 250;
                try {
                    n = Integer.parseInt(text);
                } catch (NumberFormatException e) {
                }
                return n;
            }, numberOfItems.textProperty()));

            startButton.disableProperty().bind(
                stateProperty.isEqualTo(Worker.State.RUNNING));
            cancelButton.disableProperty().bind(
                stateProperty.isNotEqualTo(Worker.State.RUNNING));
            exceptionButton.disableProperty().bind(
                stateProperty.isNotEqualTo(Worker.State.RUNNING));

            HBox topPane = new HBox(10, progressBar);
            topPane.setPadding(new Insets(10, 10, 10, 10));
            topPane.setAlignment(Pos.CENTER);

            ColumnConstraints constraints1 = new ColumnConstraints();
            constraints1.setHalignment(HPos.RIGHT);
            constraints1.setMinWidth(65);
            ColumnConstraints constraints2 = new ColumnConstraints();
            constraints2.setHalignment(HPos.LEFT);
            constraints2.setMinWidth(200);

            GridPane centerPane = new GridPane();
            centerPane.setHgap(10);
            centerPane.setVgap(10);
            centerPane.setPadding(new Insets(10, 10, 10, 10));
            centerPane.getColumnConstraints().addAll(constraints1, constraints2);
            centerPane.add(new Label("Title:"), 0, 0);
            centerPane.add(new Label("Message:"), 0, 1);
            centerPane.add(new Label("Running:"), 0, 2);
            centerPane.add(new Label("State:"), 0, 3);
            centerPane.add(new Label("Total Work:"), 0, 4);
            centerPane.add(new Label("Work Done:"), 0, 5);
            centerPane.add(new Label("Progress:"), 0, 6);
            centerPane.add(new Label("Value:"), 0, 7);
            centerPane.add(new Label("Exception:"), 0, 8);

            centerPane.add(title, 1, 0);
            centerPane.add(message, 1, 1);
            centerPane.add(running, 1, 2);
            centerPane.add(state, 1, 3);
            centerPane.add(totalWork, 1, 4);
            centerPane.add(workDone, 1, 5);
            centerPane.add(progress, 1, 6);
            centerPane.add(value, 1, 7);
            centerPane.add(exception, 1, 8);

            HBox buttonPane = new HBox(10,
                new Label("Process"), numberOfItems, new Label("items"),
                startButton, cancelButton, exceptionButton);
            buttonPane.setPadding(new Insets(10, 10, 10, 10));
            buttonPane.setAlignment(Pos.CENTER);

            BorderPane root = new BorderPane(centerPane, topPane, null, buttonPane, null);
            scene = new Scene(root);
        }
    }
}

Listing 5-10.
ServiceExample.java

前面的程序是从我们在前一节中学习的WorkerAndTaskExample类派生而来的。这个程序的Model嵌套类包含一个Worker类型的worker字段、AtomicBoolean类型的shouldThrow字段和IntegerProperty类型的numberOfItems字段。worker字段被初始化为Service<String>的匿名子类的一个实例,该实例实现其createTask()方法以返回一个带有call()方法的Task<String>,该方法的实现几乎与上一节中的Task<String>实现完全相同,只是它并不总是处理 250 个项目,而是从Model类的numberOfItems属性中选取要处理的项目数。

这个程序的View嵌套类创建了一个与前一节几乎相同的 UI,但是在按钮面板中添加了一些额外的控件。添加到按钮面板的控件之一是名为numberOfItemsTextFieldmodelnumberOfItems IntegerProperty被绑定到用viewnumberOfItems字段的textProperty()创建的IntegerBinding。这有效地控制了每个新创建的Task将处理的项目数量。仅当服务处于Worker.State.RUNNING状态时,启动按钮才被禁用。因此,您可以在任务完成后单击开始按钮。

开始按钮的动作处理程序现在将shouldThrow标志重置为false,并调用服务的restart()

图 5-12 至 5-16 中的截图是在与WorkerAndTaskExample程序的图 5-7 至 5-11 中的截图类似的情况下用ServiceExample程序拍摄的。

A323806_4_En_5_Fig16_HTML.jpg

图 5-16。

The ServiceExample program after the task threw an exception

A323806_4_En_5_Fig15_HTML.jpg

图 5-15。

The ServiceExample program after the task has been cancelled

A323806_4_En_5_Fig14_HTML.jpg

图 5-14。

The ServiceExample program after the task succeeded

A323806_4_En_5_Fig13_HTML.jpg

图 5-13。

The ServiceExample program while a task is in progress

A323806_4_En_5_Fig12_HTML.jpg

图 5-12。

The ServiceExample program after starting up

正如您从前面的屏幕截图中看到的,输入到文本字段中的数字确实会影响每次运行服务时处理的项目数量,屏幕截图中 UI 中反映的消息就是证明。

Caution

因为使用 JavaFX worker threading framework 启动的任务在后台线程中执行,所以不要访问任务代码中的任何实时场景非常重要。

了解 ScheduledService 抽象类

ScheduledService<V>抽象类扩展了Service<V>抽象类,并提供由服务创建的任务的重复执行。ScheduledService<V>类通过以下属性控制其任务的重复方式:

  • delay
  • period
  • backOffStrategy
  • restartOnFailure
  • maximumFailureCount
  • currentFailureCount
  • cumulativePeriod
  • maximumCumulativePeriod
  • lastValue

delay属性控制在调度服务上的start()调用之后任务开始运行之前必须经过的时间。period属性控制任务运行一次后,下一次运行开始前必须经过的时间。period测量一次运行开始和下一次运行开始之间的差异。delayperiodDuration类型的对象属性。如果任务执行过程中没有出现故障情况,则ScheduledService将无限期重复该任务。

如果在任务执行过程中出现故障,那么接下来会发生什么由restartOnFailure属性控制。如果该属性为falseScheduledService将保持在FAILED状态,不会再发生任何事情。如果restartOnFailure属性是true,那么ScheduledService将再次运行任务。失败任务的重新运行由backOffStrategymaximumFailureCountmaximumCumulativePeriod属性控制。后退策略只是一个 lambda 表达式,它将ScheduledService作为一个参数,并返回一个Duration,它被设置为cumulativePeriod属性的值。自上次任务运行失败开始后经过的时间达到cumulativePeriod时,任务将开始重新运行。除了cumulativePeriod之外,ScheduledService还跟踪currentFailureCount属性,它是当前失败运行序列中连续失败运行的次数。如果任务重新运行成功,ScheduledService将返回到其在period时间间隔内运行任务的正常行为;否则(即,如果重新运行再次失败),ScheduledService将向backOffStrategy请求新的cumulativePeriod,并再次重新运行。如果currentFailureCount达到maximumFailureCount,或者cumulativePeriod大于或等于maximumCumulativePeriod,则ScheduledService将进入故障状态,不会再发生任何情况。

提供了三种后退策略。它们是ScheduledService中的常量。线性增长的LINEAR_BACKOFF_STRATEGY回报越来越长的DurationsEXPONENTIAL_BACKOFF_STRATEGY回报越来越长Durations呈指数增长。LOGARITHMIC_BACKOFF_STRATEGY返回以对数方式增长的更长的Durations。你可以很容易地定义自己的后退策略。

你可以通过调用reset()start()方法来重置和重启一个ScheduledService

将 JavaFX 与其他 GUI 工具包混合使用

在研究了 JavaFX 运行时的线程范例和从 JavaFX 应用程序线程执行代码的方法之后,我们现在来看看如何使 JavaFX 与其他 GUI 工具包共存。JavaFX 提供了将 JavaFX 与 Swing 或 SWT 混合使用的类和框架。您可以在 Swing 应用程序中嵌入 JavaFX 场景。您可以在 SWT 应用程序中嵌入 JavaFX 场景。并且可以在 JavaFX 应用程序中嵌入 Swing 组件。

在 Swing 应用程序中嵌入 JavaFX 场景

JavaFX 支持通过类的javafx.embed.swing包将 JavaFX 场景嵌入到 Swing 应用程序中。这是一个非常小的包,包括一个用于将 JavaFX 场景嵌入到 Swing 中的公共类—JFXPanel—和另一个用于将 Swing 组件嵌入到 JavaFX 应用程序中的类— SwingNodeJFXPanel类扩展了javax.swing.JComponent,因此可以像其他 Swing 组件一样放在 Swing 程序中。JFXPanel还可以托管一个 JavaFX 场景,这样就可以将 JavaFX 场景添加到 Swing 程序中。

然而,这个嵌入了 JavaFX 场景的 Swing 程序既需要 Swing 运行时来使其 Swing 部分正常工作,也需要 JavaFX 运行时来使 JavaFX 部分正常工作。因此,它同时具有 Swing 事件调度线程(EDT)和 JavaFX 应用程序线程。JFXPanel类在 Swing 和 JavaFX 之间进行所有用户事件的双向翻译。

正如 JavaFX 的规则要求对现场场景的所有访问都在 JavaFX 应用程序线程中完成一样,Swing 的规则要求对 Swing GUIs 的所有访问都在 EDT 中完成。如果您想从 JavaFX 事件处理程序改变 Swing 组件,或者相反,您仍然需要跳转线程。正如我们前面看到的,在 JavaFX 应用程序线程上执行一段代码的正确方法是使用Platform.runLater()。在 Swing EDT 上执行一段代码的正确方法是使用EventQueue.invokeLater()

在本节中,我们将一个纯 Swing 程序转换成一个 Swing 和 JavaFX 混合程序。我们从清单 5-11 中的 Swing 程序开始,它与ResponsiveUIExample程序非常相似。

package com.projavafx.collections;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

public class NoJavaFXSceneInSwingExample {
    public static void main(final String[] args) {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                swingMain(args);
            }
        });
    }
    private static void swingMain(String[] args) {
        Model model = new Model();
        View view = new View(model);
        Controller controller = new Controller(model, view);
        controller.mainLoop();
    }

    private static class Model {
        public Color fillColor = Color.LIGHT_GRAY;
        public Color strokeColor = Color.DARK_GRAY;
    }

    private static class View {
        public JFrame frame;
        public JComponent canvas;
        public JButton changeFillButton;
        public JButton changeStrokeButton;

        private View(final Model model) {
            frame = new JFrame("No JavaFX in Swing Example");
            canvas = new JComponent() {
                @Override
                public void paint(Graphics g) {
                    g.setColor(model.strokeColor);
                    g.fillRect(0, 0, 200, 200);
                    g.setColor(model.fillColor);
                    g.fillRect(10, 10, 180, 180);
                }

                @Override
                public Dimension getPreferredSize() {
                    return new Dimension(200, 200);
                }
            };
            FlowLayout canvasPanelLayout = new FlowLayout(FlowLayout.CENTER, 10, 10);
            JPanel canvasPanel = new JPanel(canvasPanelLayout);
            canvasPanel.add(canvas);

            changeFillButton = new JButton("Change Fill");
            changeStrokeButton = new JButton("Change Stroke");
            FlowLayout buttonPanelLayout = new FlowLayout(FlowLayout.CENTER, 10, 10);
            JPanel buttonPanel = new JPanel(buttonPanelLayout);
            buttonPanel.add(changeFillButton);
            buttonPanel.add(changeStrokeButton);

            frame.add(canvasPanel, BorderLayout.CENTER);
            frame.add(buttonPanel, BorderLayout.SOUTH);
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.setLocationByPlatform(true)

;
            frame.pack();
        }
    }
    private static class Controller {
        private View view;

        private Controller(final Model model, final View view) {
            this.view = view;
            this.view.changeFillButton.addActionListener(new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    if (model.fillColor.equals(Color.LIGHT_GRAY)) {
                        model.fillColor = Color.GRAY;
                    } else {
                        model.fillColor = Color.LIGHT_GRAY;
                    }
                    view.canvas.repaint();
                }
            });
            this.view.changeStrokeButton.addActionListener(new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    if (model.strokeColor.equals(Color.DARK_GRAY)) {
                        model.strokeColor = Color.BLACK;
                    } else {
                        model.strokeColor = Color.DARK_GRAY;
                    }
                    view.canvas.repaint();
                }
            });
        }

        public void mainLoop() {
            view.frame.setVisible(true);
        }
    }
}

Listing 5-11.
NoJavaFXSceneInSwingExample.java

运行清单 5-11 中的程序时,显示图 5-17 中的 UI。它是一个包含三个 Swing 组件的JFrame,一个包含被覆盖的paint()getPreferredSize()方法的JComponent,使它看起来像我们在前面的程序中看到的矩形,以及两个将改变矩形的填充和笔划的JButtons

A323806_4_En_5_Fig17_HTML.jpg

图 5-17。

The NoJavaFXSceneInSwingExample program

由于NoJavaFXSceneInSwingExample中的自定义绘制JComponent很难长期维护,我们用 JavaFX Rectangle替换了它。这是通过用等效的JFXPanel代码替换 Swing 代码来实现的。这是 Swing 代码:

canvas = new JComponent() {
    @Override
    public void paint(Graphics g) {
        g.setColor(model.strokeColor);
        g.fillRect(0, 0, 200, 200);
        g.setColor(model.fillColor);
        g.fillRect(10, 10, 180, 180);
    }

    @Override
    public Dimension getPreferredSize() {
        return new Dimension(200, 200);
    }
};

这里是JFXPanel代码:

canvas = new JFXPanel();
canvas.setPreferredSize(new Dimension(210, 210));
Platform.runLater(new Runnable() {
    @Override
    public void run() {
        final Rectangle rectangle = new Rectangle(200, 200);
        rectangle.setStrokeWidth(10);
        rectangle.fillProperty().bind(model.fillProperty());
        rectangle.strokeProperty().bind(model.strokeProperty());
        final VBox vBox = new VBox(rectangle);
        final Scene scene = new Scene(vBox);
        canvas.setScene(scene);
    }
});

JFXPanel构造器引导 JavaFX 运行时系统。我们为JFXPanel设置了首选大小,以便它在 Swing 容器中正确布局。然后,我们在 JavaFX 应用程序线程上构建场景图,并将其绑定到模型,我们将其更改为 JavaFX bean。需要进行的另一组更改是在两个JButtonActionListener中。修改model会触发对 JavaFX 矩形的更改,因此需要在 JavaFX 应用程序线程上运行以下代码:

this.view.changeFillButton.addActionListener(e -> {
    Platform.runLater(() -> {
        final Paint fillPaint = model.getFill();
        if (fillPaint.equals(Color.LIGHTGRAY)) {
            model.setFill(Color.GRAY);
        } else {
            model.setFill(Color.LIGHTGRAY);
        }
    });
});

清单 5-12 显示了完整的 Swing JavaFX 混合程序。

package com.projavafx.collections;

import javafx.application.Platform;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.embed.swing.JFXPanel;
import javafx.scene.Scene;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.shape.Rectangle;

import javax.swing.*;
import java.awt.*;

public class JavaFXSceneInSwingExample {
    public static void main(final String[] args) {
        EventQueue.invokeLater(() -> {
            swingMain(args);
        });
    }

    private static void swingMain(String[] args) {

        Model model = new Model();
        View view = new View(model);
        Controller controller = new Controller(model, view);
        controller.mainLoop();
    }

    private static class Model {
        private ObjectProperty<Color> fill = new SimpleObjectProperty<>(Color.LIGHTGRAY);
        private ObjectProperty<Color> stroke = new SimpleObjectProperty<>(Color.DARKGRAY);

        public final Color getFill() {
            return fill.get();
        }

        public final void setFill(Color value) {
            this.fill.set(value);
        }

        public final Color getStroke() {
            return stroke.get();
        }

        public final void setStroke(Color value) {
            this.stroke.set(value);
        }

        public final ObjectProperty<Color> fillProperty() {
            return fill;
        }

        public final ObjectProperty<Color> strokeProperty() {
            return stroke;
        }
    }

    private static class View {
        public JFrame frame;
        public JFXPanel canvas;
        public JButton changeFillButton;
        public JButton changeStrokeButton;

        private View(final Model model) {
            frame = new JFrame("JavaFX in Swing Example");
            canvas = new JFXPanel();
            canvas.setPreferredSize(new Dimension(210, 210));
            Platform.runLater(new Runnable() {

                @Override
                public void run() {
                    final Rectangle rectangle = new Rectangle(200, 200);
                    rectangle.setStrokeWidth(10);
                    rectangle.fillProperty().bind(model.fillProperty());
                    rectangle.strokeProperty().bind(model.strokeProperty());
                    final VBox vBox = new VBox(rectangle);
                    final Scene scene = new Scene(vBox);
                    canvas.setScene(scene);
                }
            });
            FlowLayout canvasPanelLayout = new FlowLayout(FlowLayout.CENTER, 10, 10);
            JPanel canvasPanel = new JPanel(canvasPanelLayout);
            canvasPanel.add(canvas);

            changeFillButton = new JButton("Change Fill");
            changeStrokeButton = new JButton("Change Stroke");
            FlowLayout buttonPanelLayout = new FlowLayout(FlowLayout.CENTER, 10, 10);
            JPanel buttonPanel = new JPanel(buttonPanelLayout);
            buttonPanel.add(changeFillButton);
            buttonPanel.add(changeStrokeButton);

            frame.add(canvasPanel, BorderLayout.CENTER);
            frame.add(buttonPanel, BorderLayout.SOUTH);
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.setLocationByPlatform(true);
            frame.pack();
        }
    }

    private static class Controller {
        private View view;

        private Controller(final Model model, final View view) {
            this.view = view;
            this.view.changeFillButton.addActionListener(e -> {
                Platform.runLater(() -> {
                    final Paint fillPaint = model.getFill();
                    if (fillPaint.equals(Color.LIGHTGRAY)) {
                        model.setFill(Color.GRAY);
                    } else {
                        model.setFill(Color.LIGHTGRAY);
                    }
                });
            });
            this.view.changeStrokeButton.addActionListener(e -> {
                Platform.runLater(() -> {
                    final Paint strokePaint = model.getStroke();
                    if (strokePaint.equals(Color.DARKGRAY)) {
                        model.setStroke(Color.BLACK);
                    } else {
                        model.setStroke(Color.DARKGRAY);
                    }
                });
            });
        }

        public void mainLoop() {
            view.frame.setVisible(true);

        }
    }
}

Listing 5-12.
JavaFXSceneInSwingExample.java

运行清单 5-12 中的程序时,显示图 5-18 中的 GUI。从截图上看不出来,但是JFrame中间的矩形是 JavaFX 矩形。

A323806_4_En_5_Fig18_HTML.jpg

图 5-18。

The JavaFXSceneInSwingExample program

在 SWT 应用程序中嵌入 JavaFX 场景

JavaFX 能够通过类的javafx.embed.swt包将 JavaFX 场景嵌入到 SWT 应用程序中。它包含两个公共类,FXCanvasSWTFXUtilsFXCanvas类扩展了org.eclipse.swt.widgets.Canvas,可以像任何其他 SWT 小部件一样放在 SWT 程序中。FXCanvas还可以托管一个 JavaFX 场景,并且可以将 JavaFX 场景添加到一个 SWT 程序中。

因为 SWT 和 JavaFX 都使用本地平台的 UI 线程作为自己的事件调度线程,所以 SWT UI 线程(在这里实例化一个Display对象,启动主循环,并且必须创建和访问所有其他 UI 小部件)和 JavaFX 应用程序线程是同一个线程。因此,在您的 SWT 和 JavaFX 事件处理程序中没有必要使用Platform.runLater()或其 SWT 等价物display.asyncExec()

清单 5-13 中的 SWT 程序是清单 5-11 中 Swing 程序的 SWT 端口。

Note

您需要将包含 SWT 类的 jar 文件添加到您的类路径中,以编译清单 5-13 和 5-14 中的程序。在我们的开发机器上,SWT jar 位于%ECLIPSE_HOME%\plugins\ org.eclipse.swt.win32.win32.x86_64_3.102.1.v20140206-1358.jar,其中%ECLIPSE_HOME%是 Eclipse(开普勒 SR2)安装目录。

import org.eclipse.swt.SWT;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseMoveListener;
import org.eclipse.swt.events.MouseTrackAdapter;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.RGB;
import org.eclipse.swt.layout.RowData;
import org.eclipse.swt.layout.RowLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Shell;

public class NoJavaFXSceneInSWTExample {
    public static void main(final String[] args) {
        Model model = new Model();
        View view = new View(model);
        Controller controller = new Controller(model, view);
        controller.mainLoop();
    }

    private static class Model {
        public static final RGB LIGHT_GRAY = new RGB(0xd3, 0xd3, 0xd3);
        public static final RGB GRAY = new RGB(0x80, 0x80, 0x80);
        public static final RGB DARK_GRAY = new RGB(0xa9, 0xa9, 0xa9);
        public static final RGB BLACK = new RGB(0x0, 0x0, 0x0);
        public RGB fillColor = LIGHT_GRAY;
        public RGB strokeColor = DARK_GRAY;

    }

    private static class View {
        public Display display;
        public Shell frame;
        public Canvas canvas;
        public Button changeFillButton;
        public Button changeStrokeButton;
        public Label mouseLocation;
        public boolean mouseInCanvas;

        private View(final Model model) {
            this.display = new Display();
            frame = new Shell(display);
            frame.setText("No JavaFX in SWT Example");
            RowLayout frameLayout = new RowLayout(SWT.VERTICAL);
            frameLayout.spacing = 10;
            frameLayout.center = true;
            frame.setLayout(frameLayout);

            Composite canvasPanel = new Composite(frame, SWT.NONE);
            RowLayout canvasPanelLayout = new RowLayout(SWT.VERTICAL);
            canvasPanelLayout.spacing = 10;
            canvasPanel.setLayout(canvasPanelLayout);

            canvas = new Canvas(canvasPanel, SWT.NONE);
            canvas.setLayoutData(new RowData(200, 200));
            canvas.addPaintListener(new PaintListener() {
                @Override
                public void paintControl(PaintEvent paintEvent) {
                    final GC gc = paintEvent.gc;
                    final Color strokeColor = new Color(display, model.strokeColor);
                    gc.setBackground(strokeColor);
                    gc.fillRectangle(0, 0, 200, 200);
                    final Color fillColor = new Color(display, model.fillColor);
                    gc.setBackground(fillColor);
                    gc.fillRectangle(10, 10, 180, 180);
                    strokeColor.dispose();
                    fillColor.dispose();
                }
            });

            Composite buttonPanel = new Composite(frame, SWT.NONE);
            RowLayout buttonPanelLayout = new RowLayout(SWT.HORIZONTAL);
            buttonPanelLayout.spacing = 10;
            buttonPanelLayout.center = true;
            buttonPanel.setLayout(buttonPanelLayout);

            changeFillButton = new Button(buttonPanel, SWT.NONE);
            changeFillButton.setText("Change Fill");
            changeStrokeButton = new Button(buttonPanel, SWT.NONE);
            changeStrokeButton.setText("Change Stroke");
            mouseLocation = new Label(buttonPanel, SWT.NONE);
            mouseLocation.setLayoutData(new RowData(50, 15));

            frame.pack();
        }
    }

    private static class Controller {
        private View view;

        private Controller(final Model model, final View view) {
            this.view = view;
            view.changeFillButton.addSelectionListener(new SelectionAdapter() {
                @Override
                public void widgetSelected(SelectionEvent e) {
                    if (model.fillColor.equals(model.LIGHT_GRAY)) {
                        model.fillColor = model.GRAY;
                    } else {
                        model.fillColor = model.LIGHT_GRAY;
                    }
                    view.canvas.redraw();
                }
            });
            view.changeStrokeButton.addSelectionListener(new SelectionAdapter() {
                @Override
                public void widgetSelected(SelectionEvent e) {
                    if (model.strokeColor.equals(model.DARK_GRAY)) {
                        model.strokeColor = model.BLACK;
                    } else {
                        model.strokeColor = model.DARK_GRAY;
                    }
                    view.canvas.redraw();
                }
            });
            view.canvas.addMouseMoveListener(new MouseMoveListener() {
                @Override
                public void mouseMove(MouseEvent mouseEvent) {
                    if (view.mouseInCanvas) {
                        view.mouseLocation.setText("(" + mouseEvent.x + ", " + mouseEvent.y + ")");
                    }
                }
            });
            this.view.canvas.addMouseTrackListener(new MouseTrackAdapter() {
                @Override
                public void mouseEnter(MouseEvent e) {
                    view.mouseInCanvas = true;
                }

                @Override
                public void mouseExit(MouseEvent e) {
                    view.mouseInCanvas = false;
                    view.mouseLocation.setText("");

                }
            });

        }

        public void mainLoop() {
            view.frame.open();
            while (!view.frame.isDisposed()) {
                if (!view.display.readAndDispatch()) {
                    view.display.sleep();
                }
            }
            view.display.dispose();
        }
    }
}

Listing 5-13.
NoJavaFXSceneInSWTExample.java

运行清单 5-13 中的程序时,显示图 5-19 中的 UI。它是一个 SWT Shell包含四个 SWT 小部件,一个Canvas带有一个PaintListener使它看起来像我们前面看到的矩形,两个Button将改变矩形的填充和笔画,还有一个Label小部件当鼠标在矩形内时将显示鼠标指针的位置。

A323806_4_En_5_Fig19_HTML.jpg

图 5-19。

The NoJavaFXSceneInSWTExample program

正如我们在 Swing 示例中所做的那样,我们用 JavaFX Rectangle替换了程序NoJavaFXSceneInSWTExample中自定义绘制的Canvas小部件。这是通过用等效的 FXCanvas 码替换 SWT 码来实现的。以下是 SWT 电码:

canvas = new Canvas(canvasPanel, SWT.NONE);
canvas.setLayoutData(new RowData(200, 200));
canvas.addPaintListener(new PaintListener() {
    @Override
    public void paintControl(PaintEvent paintEvent) {
        final GC gc = paintEvent.gc;
        final Color strokeColor = new Color(display, model.strokeColor);
        gc.setBackground(strokeColor);
        gc.fillRectangle(0, 0, 200, 200);
        final Color fillColor = new Color(display, model.fillColor);
        gc.setBackground(fillColor);
        gc.fillRectangle(10, 10, 180, 180);
        strokeColor.dispose();
        fillColor.dispose();
    }
});

这是 FXCanvas 代码:

canvas = new FXCanvas(canvasPanel, SWT.NONE);
rectangle = new Rectangle(200, 200);
rectangle.setStrokeWidth(10);
VBox vBox = new VBox(rectangle);
Scene scene = new Scene(vBox, 210, 210);
canvas.setScene(scene);
rectangle.fillProperty().bind(model.fillProperty());
rectangle.strokeProperty().bind(model.strokeProperty());

我们还将模型更改为 JavaFX bean。事件侦听器以自然的方式更改。完整的 SWT JavaFX 混合程序如清单 5-14 所示。

package com.projavafx.collections;

import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.embed.swt.FXCanvas;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.shape.Rectangle;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.layout.RowData;
import org.eclipse.swt.layout.RowLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Shell;

public class JavaFXSceneInSWTExample {
    public static void main(final String[] args) {
        Model model = new Model();
        View view = new View(model);
        Controller controller = new Controller(model, view);
        controller.mainLoop();
    }

    private static class Model {
        private ObjectProperty<Color> fill = new SimpleObjectProperty<>(Color.LIGHTGRAY);
        private ObjectProperty<Color> stroke = new SimpleObjectProperty<>(Color.DARKGRAY);

        public Color getFill() {
            return fill.get();
        }

        public void setFill(Color value) {
            this.fill.set(value);
        }

        public Color getStroke() {
            return stroke.get();
        }

        public void setStroke(Color value) {
            this.stroke.set(value);
        }

        public ObjectProperty<Color> fillProperty() {
            return fill;
        }

        public ObjectProperty<Color> strokeProperty() {
            return stroke;
        }
    }

    private static class View {

        public Display display;
        public Shell frame;
        public FXCanvas canvas;
        public Button changeFillButton;
        public Button changeStrokeButton;
        public Label mouseLocation;
        public boolean mouseInCanvas;
        public Rectangle rectangle;

        private View(final Model model) {
            this.display = new Display();
            frame = new Shell(display);
            frame.setText("JavaFX in SWT Example");
            RowLayout frameLayout = new RowLayout(SWT.VERTICAL);
            frameLayout.spacing = 10;
            frameLayout.center = true;
            frame.setLayout(frameLayout);

            Composite canvasPanel = new Composite(frame, SWT.NONE);
            RowLayout canvasPanelLayout = new RowLayout(SWT.VERTICAL);
            canvasPanelLayout.spacing = 10;
            canvasPanel.setLayout(canvasPanelLayout);
            canvas = new FXCanvas(canvasPanel, SWT.NONE);
            rectangle = new Rectangle(200, 200);
            rectangle.setStrokeWidth(10);
            VBox vBox = new VBox(rectangle);
            Scene scene = new Scene(vBox, 210, 210);
            canvas.setScene(scene);
            rectangle.fillProperty().bind(model.fillProperty());
            rectangle.strokeProperty().bind(model.strokeProperty());

            Composite buttonPanel = new Composite(frame, SWT.NONE);
            RowLayout buttonPanelLayout = new RowLayout(SWT.HORIZONTAL);
            buttonPanelLayout.spacing = 10;
            buttonPanelLayout.center = true;
            buttonPanel.setLayout(buttonPanelLayout);

            changeFillButton = new Button(buttonPanel, SWT.NONE);
            changeFillButton.setText("Change Fill");
            changeStrokeButton = new Button(buttonPanel, SWT.NONE);
            changeStrokeButton.setText("Change Stroke");
            mouseLocation = new Label(buttonPanel, SWT.NONE);
            mouseLocation.setLayoutData(new RowData(50, 15));

            frame.pack();

        }
    }

    private static class Controller {
        private View view;

        private Controller(final Model model, final View view) {
            this.view = view;
            view.changeFillButton.addSelectionListener(new SelectionAdapter() {
                @Override
                public void widgetSelected(SelectionEvent e) {
                    final Paint fillPaint = model.getFill();
                    if (fillPaint.equals(Color.LIGHTGRAY)) {
                        model.setFill(Color.GRAY);
                    } else {
                        model.setFill(Color.LIGHTGRAY);
                    }
                }
            });
            view.changeStrokeButton.addSelectionListener(new SelectionAdapter() {
                @Override
                public void widgetSelected(SelectionEvent e) {
                    final Paint strokePaint = model.getStroke();
                    if (strokePaint.equals(Color.DARKGRAY)) {
                        model.setStroke(Color.BLACK);
                    } else {
                        model.setStroke(Color.DARKGRAY);
                    }
                }
            });
            view.rectangle.setOnMouseEntered(new EventHandler<MouseEvent>() {
                @Override
                public void handle(MouseEvent mouseEvent) {
                    view.mouseInCanvas = true;
                }
            });
            view.rectangle.setOnMouseExited(new EventHandler<MouseEvent>() {
                @Override
                public void handle(final MouseEvent mouseEvent) {
                    view.mouseInCanvas = false;
                    view.mouseLocation.setText("");
                }
            });
            view.rectangle.setOnMouseMoved(new EventHandler<MouseEvent>() {
                @Override
                public void handle(final MouseEvent mouseEvent) {
                    if (view.mouseInCanvas) {
                        view.mouseLocation.setText("(" + (int) mouseEvent.getSceneX() + ", " + (int) mouseEvent.getSceneY() + ")");
                    }
                }

            });
        }

        public void mainLoop() {
            view.frame.open();
            while (!view.frame.isDisposed()) {
                if (!view.display.readAndDispatch()) view.display.sleep();
            }
            view.display.dispose();
        }
    }
}

Listing 5-14.
JavaFXSceneInSWTExample.java

运行清单 5-14 中的程序时,显示图 5-20 中的 GUI。SWT 外壳中心的矩形是一个 JavaFX 矩形。

A323806_4_En_5_Fig20_HTML.jpg

图 5-20。

The JavaFXSceneInSWTExample program

在 JavaFX 应用程序中嵌入 Swing 组件

javafx.embed.swing包中的SwingNode类是一个 JavaFX Node,它可以托管 Swing JComponent,因此允许您在 JavaFX 应用程序中嵌入一个 Swing 组件。这为遗留 Swing 应用程序提供了一种渐进的方式来迁移到 JavaFX。除了默认的构造器,为应用程序开发人员设计的唯一其他公共方法是一对用于嵌入式JComponent的 getter 和 setter 方法:

  • public void setContent(JComponent)
  • public JComponent getContent()

当包含一个JComponentSwingNode被附加到一个现场 JavaFX 场景时,SwingNode类负责将所有 JavaFX 输入和焦点事件转发到嵌入的JComponent。JavaFX 应用程序中只允许嵌入轻量级 Swing 组件。就像在 Swing 应用程序中嵌入 JavaFX 场景的情况一样,在具有嵌入式 Swing 组件的 JavaFX 应用程序中存在两个事件调度线程,即 JavaFX 应用程序线程和 Swing EDT,值得我们注意。特别是,应该满足在 JavaFX 应用程序线程中执行 JavaFX 实时场景操作的要求和在 Swing EDT 中执行 Swing UI 操作的要求。实际上,这意味着如果您想在 JavaFX 事件处理程序中操作 Swing UI,您应该用EventQueue.invokeLater();将其发送到 Swing EDT,如果您想在 Swing 事件侦听器方法中操作 JavaFX 节点,您应该用Platform.runLater()将其发送到 JavaFX 应用程序线程。

在本节中,我们将本章中使用的示例程序转换成一个嵌入了定制 Swing 组件的 JavaFX 应用程序。自定义 Swing 组件是JComponent的一个简单子类,它用两种不同的颜色绘制一个带有粗边框的矩形。

private static class MyRectangle extends JComponent {
    private final Model model;

    public MyRectangle(Model model) {
        this.model = model;
    }

    @Override
    public void paint(Graphics g) {
        g.setColor(model.getStrokeColor());
        g.fillRect(0, 0, 200, 200);
        g.setColor(model.getFillColor());
        g.fillRect(10, 10, 180, 180);
    }

    @Override
    public Dimension getMaximumSize() {
        return new Dimension(200, 200);
    }
}

当前的SwingNode实现缺乏很好地响应 JavaFX 容器节点布局请求的能力。我们对其进行了扩展,从而改进了应用程序的布局:

private static class MySwingNode extends SwingNode {
    @Override
    public double minWidth(double height) {
        return 250;
    }

    @Override
    public double minHeight(double width) {
        return 200;
    }
}

当点击两个 JavaFX 按钮时,我们调用代码来改变通过EventQueue.invokeLater()表示为 lambda 表达式的Runnable中 Swing 组件MyRectangle的状态:

view.changeFillButton.setOnAction(actionEvent -> {
    EventQueue.invokeLater(() -> {
        final java.awt.Color fillColor = model.getFillColor();
        if (fillColor.equals(java.awt.Color.LIGHT_GRAY)) {
            model.setFillColor(java.awt.Color.GRAY);
        } else {
            model.setFillColor(java.awt.Color.LIGHT_GRAY);
        }
        view.canvas.repaint();
    });
});

Note

repaint()方法实际上是罕见的可以从任何线程调用的 Swing UI 方法之一,而不仅仅是 Swing EDT。我们使用的EventQueue.invokeLater()仅用于说明目的。

当鼠标悬停在MyRectangle上时,我们通过更新LabeltextProperty绑定到的Model类中名为mouseLocationStringProperty来更新 JavaFX 标签:

canvas.addMouseMotionListener(new MouseMotionListener() {
    @Override
    public void mouseDragged(MouseEvent e) {
    }

    @Override
    public void mouseMoved(MouseEvent e) {
        Platform.runLater(() -> {
            model.setMouseLocation("(" + e.getX() + ", " + e.getY() + ")");
        });
    }
});
swingNode.setContent(canvas);

JavaFX 示例应用程序中完整的 Swing 组件如清单 5-15 所示。

package com.projavafx.collections;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.embed.swing.SwingNode;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;

public class SwingComponentInJavaFXExample extends Application {
    private Model model;
    private View view;

    public static void main(String[] args) {
        launch(args);
    }

    public SwingComponentInJavaFXExample() {
        model = new Model();
    }

    @Override
    public void start(Stage stage) throws Exception {
        view = new View(model);
        hookupEvents();
        stage.setTitle("Swing in JavaFX Example");
        stage.setScene(view.scene)

;
        stage.show();
    }

    private void hookupEvents() {
        view.changeFillButton.setOnAction(actionEvent -> {
            EventQueue.invokeLater(() -> {
                final java.awt.Color fillColor = model.getFillColor();
                if (fillColor.equals(java.awt.Color.LIGHT_GRAY)) {
                    model.setFillColor(java.awt.Color.GRAY);
                } else {
                    model.setFillColor(java.awt.Color.LIGHT_GRAY);
                }
                view.canvas.repaint();
            });
        });

        view.changeStrokeButton.setOnAction(actionEvent -> {
            EventQueue.invokeLater(() -> {
                final java.awt.Color strokeColor = model.getStrokeColor();
                if (strokeColor.equals(java.awt.Color.GRAY)) {
                    model.setStrokeColor(java.awt.Color.BLACK);
                } else {
                    model.setStrokeColor(java.awt.Color.GRAY);
                }
                view.canvas.repaint();
            });
        });
    }

    private static class Model {
        private java.awt.Color fillColor;
        private java.awt.Color strokeColor;
        final private StringProperty mouseLocation = new SimpleStringProperty(this, "mouseLocation", "");

        private Model() {
            fillColor = java.awt.Color.LIGHT_GRAY;
            strokeColor = java.awt.Color.GRAY;
        }

        public java.awt.Color getFillColor() {
            return fillColor;
        }

        public void setFillColor(java.awt.Color fillColor) {
            this.fillColor = fillColor;
        }

        public java.awt.Color getStrokeColor() {
            return strokeColor;
        }

        public void setStrokeColor(java.awt.Color strokeColor) {
            this.strokeColor = strokeColor;
        }

        public final void setMouseLocation(String mouseLocation) {
            this.mouseLocation.set(mouseLocation);
        }

        public final StringProperty mouseLocationProperty() {
            return mouseLocation;

        }
    }

    private static class View {
        public JComponent canvas;
        public Button changeFillButton;
        public Button changeStrokeButton;
        public Label mouseLocation;
        public HBox buttonHBox;
        public Scene scene;

        private View(Model model) {
            SwingNode swingNode = new MySwingNode();

            EventQueue.invokeLater(() -> {
                canvas = new MyRectangle(model);
                canvas.addMouseListener(new MouseAdapter() {
                    @Override
                    public void mouseExited(MouseEvent e) {
                        Platform.runLater(() -> {
                            model.setMouseLocation("");
                        });
                    }
                });
                canvas.addMouseMotionListener(new MouseMotionListener() {
                    @Override
                    public void mouseDragged(MouseEvent e) {
                    }

                    @Override
                    public void mouseMoved(MouseEvent e) {
                        Platform.runLater(() -> {
                            model.setMouseLocation("(" + e.getX() + ", " + e.getY() + ")");
                        });
                    }
                });
                swingNode.setContent(canvas);
            });

            changeFillButton = new Button("Change Fill");
            changeStrokeButton = new Button("Change Stroke");
            mouseLocation = new Label("(100, 100)");
            mouseLocation.setPrefSize(60, 15);
            mouseLocation.textProperty().bind(model.mouseLocationProperty());

            buttonHBox = new HBox(10, changeFillButton, changeStrokeButton, mouseLocation);
            buttonHBox.setPadding(new Insets(10, 0, 10, 0));
            buttonHBox.setAlignment(Pos.CENTER);

            VBox root = new VBox(10, swingNode, buttonHBox);
            root.setPadding(new Insets(10, 10, 10, 10));

            scene = new Scene(root);

        }
    }

    private static class MySwingNode extends SwingNode {
        @Override
        public double minWidth(double height) {
            return 250;
        }

        @Override
        public double minHeight(double width) {
            return 200;
        }
    }

    private static class MyRectangle extends JComponent {
        private final Model model;

        public MyRectangle(Model model) {
            this.model = model;
        }

        @Override
        public void paint(Graphics g) {
            g.setColor(model.getStrokeColor());
            g.fillRect(0, 0, 200, 200);
            g.setColor(model.getFillColor());
            g.fillRect(10, 10, 180, 180);
        }

        @Override
        public Dimension getMaximumSize() {
            return new Dimension(200, 200);
        }
    }
}

Listing 5-15.
SwingComponentInJavaFXExample.java

运行清单 5-15 中的程序时,显示图 5-21 中的 GUI。JavaFX 应用程序中间的矩形是一个 Swing JComponent

A323806_4_En_5_Fig21_HTML.jpg

图 5-21。

The SwingComponentInJavaFXExample program Tip

如果您想知道是否有办法将 SWT 小部件嵌入 JavaFX 应用程序,答案是否定的。原因是 SWT 小部件是重量级组件,因此更难集成到 JavaFX 这样的轻量级 GUI 工具包中。

摘要

在本章中,我们查看了 JavaFX 可观察集合、JavaFX 工作线程框架、在 Swing 和 SWT 应用程序中嵌入 JavaFX 场景,以及在 JavaFX 应用程序中嵌入 Swing 组件,以帮助您理解以下原则和技术。

  • JavaFX 支持可观察的集合和数组:ObservableListObservableMapObservableSetObservableArray,子接口ObservableIntegerArrayObservableFloatArray
  • ObservableList激发Change事件到ListChangeListener. ListChangeListener.Change可能包含一个或多个离散的变化。
  • ObservableMap通过MapChangeListener. MapChangeListener.Change触发Change事件只代表一个键的变化。
  • ObservableSet通过SetChangeListener. SetChangeListener.Change引发Change事件只代表一个元素的变化。
  • ObservableArray及其子接口通过ArrayChangeListener触发变化事件。
  • FXCollections类包含创建可观察集合和数组的工厂方法,以及处理它们的实用方法。
  • JavaFX 应用程序中的主要事件处理线程是 JavaFX 应用程序线程。对实时场景的所有访问都必须通过 JavaFX 应用程序线程来完成。
  • prism 渲染线程和媒体事件线程等其他重要线程与 JavaFX 应用程序线程协作,使图形渲染和媒体回放成为可能。
  • JavaFX 应用程序线程上长时间运行的计算会使 JavaFX GUIs 无响应。它们应该被外包给后台线程或工作线程。
  • Worker接口定义了九个可以在 JavaFX 应用程序线程上观察到的属性。它还定义了一个cancel()方法。
  • Task<V>定义一次性任务,用于将工作卸载到后台或工作线程,并将结果或异常传达给 JavaFX 应用程序线程。
  • Service<V>为创建和运行后台任务定义了一个可重用的机制。
  • 定义了一种可重复使用的机制,用于以循环方式创建和运行后台任务。
  • JFXPanel类是一个JComponent,它可以将 JavaFX 场景放入 Swing 应用程序中。
  • 在 Swing JavaFX 混合程序中,使用 Swing 事件监听器中的Platform.runLater()来访问 JavaFX 场景,使用 JavaFX 事件处理程序中的EventQueue.invokeLater()SwingUtilities.invokeLater()来访问 Swing 小部件。
  • FXCanvas类是一个 SWT 小部件,可以将 JavaFX 场景放到 SWT 应用程序中。
  • 在 SWT JavaFX 混合程序中,SWT UI 线程和 JavaFX 应用程序线程是同一个线程。
  • SwingNode类是一个 JavaFX Node,它可以将一个 Swing 组件放入 JavaFX 应用程序中。

资源

以下是理解本章内容的一些有用资源:

六、在 JavaFX 中创建图表

任何足够先进的技术都和魔法没什么区别。—亚瑟·C·克拉克

在许多商业应用中,报告是一个重要的方面。JavaFX 平台包含一个用于创建图表的 API。因为一个Chart基本上就是一个Node,所以将图表与 JavaFX 应用程序的其他部分集成起来非常简单。因此,报告是典型 JavaFX 业务应用程序不可或缺的一部分。

设计一个 API 通常是许多需求之间的折衷。两个最常见的需求是“简单易用”和“易于扩展”JavaFX Chart API 满足了这两个要求。图表 API 包含许多方法,允许开发人员更改图表的外观和感觉以及数据,使其成为一个灵活的 API,可以很容易地进行扩展。不过,这些设置的默认值非常合理,只需几行代码就可以轻松地将图表与自定义应用程序集成在一起。

JavaFX 9 中的 JavaFX Chart API 有八个具体的实现,可供开发人员使用。除此之外,开发人员可以通过扩展一个抽象类来添加他们自己的实现。

JavaFX 图表 API 的结构

存在不同类型的图表,并且有多种方法对它们进行分类。JavaFX 图表 API 区分双轴图表和不带轴的图表。JavaFX 9 版本包含一个无轴图表的实现,即PieChart。双轴图有很多,都是抽象XYChart类的扩展,如图 6-1 。

A323806_4_En_6_Fig1_HTML.jpg

图 6-1。

Overview of the charts in the JavaFX Chart API

抽象的Chart类定义了所有图表的设置。基本上,图表由三部分组成:标题、图例和内容。图表的内容对于每个实现都是特定的,但是图例和标题概念在各个实现中是相似的。因此,Chart类有许多带有相应 getter 和 setter 方法的属性,这些方法允许操作这些概念。Chart类的 Javadoc 提到了以下属性。

BooleanProperty animated
ObjectProperty<Node> legend
BooleanProperty legendVisible
ObjectProperty<Side> legendSide
StringProperty title
ObjectProperty<Side> titleSide

在接下来的示例中,我们使用了其中的一些属性,但是我们也展示了即使没有为这些属性设置值,Chart API 也允许您创建漂亮的图表。

因为Chart扩展了RegionParentNode,所以这些类上可用的所有属性和方法也可以在Chart上使用。好处之一是,用于向 JavaFX Node添加样式信息的 CSS 样式技术也适用于 JavaFX 图表。

JavaFX CSS 参考指南,可在 http://download.java.net/jdk8/jfxdocs/ javafx/scene/doc-files/cssref.html获得,包含可由设计者和开发者改变的 CSS 属性的概述。默认情况下,JavaFX 9 运行时附带的 modena 样式表用于显示 JavaFX 图表。有关在 JavaFX 图表中使用 CSS 样式的更多信息,请参考位于 http://docs.oracle.com/javase/8/javafx/user-interface-tutorial/css-styles.htm 的 Oracle 图表教程。

使用 JavaFX 饼图

PieChart以典型的饼图结构呈现信息,其中切片的大小与数据的值成比例。在深入细节之前,我们展示一个小应用程序来呈现一个PieChart

简单的例子

我们的例子显示了许多编程语言的“市场份额”,基于 2017 年 4 月的 TIOBE 指数。TIOBE 编程社区指数可在 https://www.tiobe.com/tiobe-index 获得,它提供了基于搜索引擎流量的编程语言受欢迎程度的指示。2017 年 4 月排名截图如图 6-2 。

A323806_4_En_6_Fig2_HTML.jpg

图 6-2。

Screenshot of the TIOBE index in April 2017, taken from www.tiobe.com/tiobe-index Note

https://www.tiobe.com/tiobe-index/programming-languages-definition/ 中描述了 TIOBE 使用的算法。这些数字的科学价值超出了我们示例的范围。

清单 6-1 包含了这个例子的代码。

package com.projavafx.charts ;

import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.chart.PieChart;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

public class ChartApp1 extends Application {

    @Override
    public void start(Stage primaryStage) {
        PieChart pieChart = new PieChart();
        pieChart.setData(getChartData());

        primaryStage.setTitle("PieChart");
        StackPane root = new StackPane();
        root.getChildren().add(pieChart);
        primaryStage.setScene(new Scene(root, 400, 250));
        primaryStage.show();
    }

    private ObservableList<PieChart.Data> getChartData() {
        ObservableList<PieChart.Data> answer = FXCollections.observableArrayList();
                answer.addAll(new PieChart.Data("java", 15.57),
                new PieChart.Data("C", 6.97),
                new PieChart.Data("C++", 4.55),
                new PieChart.Data("C#", 3.58),
                new PieChart.Data("Python", 3.45),
                new PieChart.Data("PHP", 3.38),
                new PieChart.Data("Visual Basic .NET", 3.25));
        return answer;
    }
}

Listing 6-1.
Rendering

the TIOBE Index in a PieChart

运行该示例的结果如图 6-3 所示。

A323806_4_En_6_Fig3_HTML.jpg

图 6-3。

Rendering the TIOBE index in a PieChart

只需有限的代码,我们就可以在PieChart中呈现数据。在我们修改这个例子之前,我们解释一下不同的部分。

设置应用程序、舞台和场景所需的代码包含在第一章中。一个PieChart扩展了一个Node,所以我们可以很容易地将它添加到场景图中。start 方法中的前两行代码创建了PieChart,并向其中添加了所需的数据:

PieChart pieChart = new PieChart();
pieChart.setData(getChartData());

类型为ObservableList<PieChart.Data>的数据是从getChartData()方法中获得的,对于我们的例子,它包含静态数据。正如getChartData()方法的返回类型所指定的,返回的数据是PieChart.Data的一个ObservableList

PieChart.Data的一个实例是PieChart的一个嵌套类,它包含了绘制一片饼图所需的信息。PieChart.Data有一个接受切片名称和值的构造器:

PieChart.Data(String name, double value)

我们使用这个构造器来创建包含编程语言名称及其在 TIOBE 索引中的分数的数据元素。

new PieChart.Data("java", 15.57)

然后,我们将这些元素添加到我们需要返回的 ObservableList <piechart.data>中。</piechart.data>

一些修改

虽然这个简单例子的结果看起来已经很好了,但是我们可以调整代码和渲染。首先,该示例使用两行代码来创建PieChart并用数据填充它:

PieChart pieChart = new PieChart();
pieChart.setData(getChartData());

因为PieChart也有一个参数构造器,前面的代码片段可以替换如下。

PieChart pieChart = new PieChart(getChartData());

除了在抽象类Chart上定义的属性之外,PieChart还有以下属性。

BooleanProperty clockwise
ObjectProperty<ObservableList<PieChart.Data>> data
DoubleProperty labelLineLength
BooleanProperty labelsVisible
DoubleProperty startAngle

我们在上一节中讨论了数据属性。其他一些属性将在下一段代码中演示。清单 6-2 包含了start()方法的修改版本。

public void start(Stage primaryStage) {
  PieChart pieChart = new PieChart();
  pieChart.setData(getChartData());
  pieChart.setTitle("Tiobe index");
  pieChart.setLegendSide(Side.LEFT);
  pieChart.setClockwise(false);
  pieChart.setLabelsVisible(false);

  primaryStage.setTitle("PieChart");

  StackPane root = new StackPane();
  root.getChildren().add(pieChart);
  primaryStage.setScene(new Scene(root, 400, 250));
  primaryStage.show();
}

Listing 6-2.
Modified Version

of the PieChart Example

因为我们在新代码中使用了Side.LEFT字段,所以我们也必须在应用程序中导入Side类。这是通过在代码的导入块中添加以下行来实现的。

import javafx.geometry.Side

运行这个修改后的版本会产生如图 6-4 所示的修改后的输出。

A323806_4_En_6_Fig4_HTML.jpg

图 6-4。

The output of the modified PieChart example

更改几行代码会导致输出看起来非常不同。我们更详细地回顾一下我们所做的更改。首先,我们向图表添加了一个标题。这是通过调用完成的

pieChart.setTitle("Tiobe index");

我们也可以使用titleProperty:

pieChart.titleProperty().set("Tiobe index");

这两种方法产生相同的输出。

Note

即将到来的修改也可以使用相同的模式来完成。我们只用 setter 方法来记录这种方法,但是用基于属性的方法来代替它是很容易的。

我们修改后的示例中的下一行代码更改了图例的位置:

pieChart.setLegendSide(Side.LEFT);

当未指定legendSide时,图例显示在默认位置,即图表下方。titlelegendSide都是属于抽象Chart类的属性。因此,它们可以设置在任何图表上。我们修改后的示例中的下一行修改了一个特定于PieChart的属性:

pieChart.setClockwise(false);

默认情况下,PieChart中的切片是顺时针绘制的。通过将该属性设置为 false,切片将逆时针呈现。我们还禁止在PieChart中显示标签。标签仍显示在图例中,但它们不再指向单个切片。这是通过以下代码行实现的:

pieChart.setLabelsVisible(false);

到目前为止,所有布局更改都是以编程方式完成的。使用 CSS 样式表来设计一般的应用程序,特别是图表,也是可能的,并且经常被推荐。

我们从 Java 代码中删除了布局更改,并添加了一个包含一些布局说明的样式表。清单 6-3 显示了start()方法的修改代码,清单 6-4 包含了我们添加的样式表。

public void start(Stage primaryStage) {
    PieChart pieChart = new PieChart();
    pieChart.setData(getChartData());
     pieChart.titleProperty().set("Tiobe index");

    primaryStage.setTitle("PieChart");
    StackPane root = new StackPane();
    root.getChildren().add(pieChart);
    Scene scene = new Scene (root, 400, 250);
    scene.getStylesheets().add("/chartappstyle.css");
    primaryStage.setScene(scene);
    primaryStage.show();
}

Listing 6-3.Remove Programmatic Layout Instructions

.chart {
    -fx-clockwise: false;
    -fx-pie-label-visible: true;
    -fx-label-line-length: 5;
    -fx-start-angle: 90;
    -fx-legend-side: right;
}

.chart-pie-label {
    -fx-font-size:9px;

}
.chart-content {
    -fx-padding:1;
}

.default-color0.chart-pie {
    -fx-pie-color:blue;
}

.chart-legend {
    -fx-background-color: #f0e68c;
    -fx-border-color: #696969;
    -fx-border-width:1;
}

Listing 6-4.Style Sheet

for PieChart Example

运行这段代码会产生如图 6-5 所示的输出。

A323806_4_En_6_Fig5_HTML.jpg

图 6-5。

Using CSS to style the PieChart

我们现在回顾一下我们所做的更改。在我们详细讨论各个更改之前,我们将展示如何在应用程序中包含 CSS。这是通过向场景添加样式表来实现的,如下所示。

scene.getStylesheets().add("/chartappstyle.css");

当应用程序运行时,包含样式表的文件chartappstyle.css必须在类路径中。

在清单 6-2 中,我们使用

pieChart.setClockwise(false)

我们从清单 6-3 的代码中删除了那一行,取而代之的是在样式表的chart类上定义了-fx-clockwise属性:

.chart {
    -fx-clockwise: false;
    -fx-pie-label-visible: true;
    -fx-label-line-length: 5;
    -fx-start-angle: 90;
    -fx-legend-side: right;
}

在同一个.chart类定义中,我们通过将- fx-pie-label-visible属性设置为 true 来使饼图上的标签可见,并将每个标签的线长度指定为 5。

此外,我们将整个饼图旋转 90 度,这是通过定义-fx-start-angle属性实现的。标签现在在样式表中定义了,我们通过省略下面一行从代码中删除了相应的定义。

pieChart.setLabelsVisible(false)

为了确保图例出现在图表的右侧,我们指定了-fx-legend-side属性。

默认情况下,PieChart使用在 caspian 样式表中定义的默认颜色。第一个切片用default-color0填充,第二个切片用default-color1填充,依此类推。更改不同切片颜色的最简单方法是覆盖默认颜色的定义。在我们的样式表中,这是通过

.default-color0.chart-pie {
    -fx-pie-color: blue;
}

可以对其他切片进行同样的操作。

如果在没有 CSS 的其他部分的情况下运行该示例,您会注意到图表本身相当小,并且标签的大小占用了太多的空间。因此,我们将标签的字体大小修改如下:

.chart-pie-label {
    -fx-font-size:9px;
}

此外,我们减少了图表区域的填充:

.chart-content {
    -fx-padding:1;
}

最后,我们改变背景和图例的笔画。这是通过如下重写chart-legend类来实现的。

.chart-legend {
   -fx-background-color: #f0e68c;
    -fx-border-color: #696969;
    -fx-border-width:1;
}

同样,我们建议读者参考http://docs.oracle.com/javase/9/javafx/user-interface-tutorial/css-styles.htm【TODO:FINAL LINK】了解更多关于使用 CSS 和 JavaFX 图表的信息。

使用 xy 图表

XYChart类是一个抽象类,有七个直接已知的子类。这些类和PieChart类的区别在于XYChart有两个轴和一个可选的alternativeColumnalternativeRow。这转化为下面的XYChart附加属性列表。

BooleanProperty alternativeColumnFillVisible
BooleanProperty alternativeRowFillVisible
ObjectProperty<ObservableList<XYChart.Series<X,Y>>> data
BooleanProperty horizontalGridLinesVisible
BooleanProperty horizontalZeroLineVisible
BooleanProperty verticalGridLinesVisible
BooleanProperty verticalZeroLineVisible

XYChart中的数据按顺序排列。这些系列如何呈现是特定于XYChart子类的实现的。一般来说,一个系列中的单个元素包含许多对。下面的例子使用了三种编程语言在未来市场份额的假设预测。我们从 2017 年的 Java、C 和 C++的 TIOBE 指数开始,并在 2020 年之前的每一年向它们添加随机值(在–2 和+2 之间)。Java 的结果(year,number)对构成了 Java 系列,这同样适用于 C 和 C++。因此,我们有三个系列,每个系列包含 10 双鞋。

PieChartXYChart的主要区别在于XYChart中有一个 x 轴和一个 y 轴。当创建一个XYChart时,这些轴是必需的,这可以从下面的构造器中观察到。

XYChart (Axis<X> xAxis, Axis<Y> yAxis)

Axis类是一个抽象类,用两个子类扩展了Region(因此也扩展了ParentNode):CategoryAxisValueAxisCategoryAxis用于呈现String格式的标签,这可以从类定义中观察到:

public class CategoryAxis extends Axis<java.lang.String>

ValueAxis用于呈现代表Number的数据条目。它本身是一个抽象类,定义如下。

public abstract class ValueAxis <T extends java.lang.Number> extends Axis<T>

ValueAxis类有一个具体的子类,即NumberAxis:

public final class NumberAxis extends ValueAxis<java.lang.Number>

通过这些例子,这些Axis类之间的差异将变得清晰。我们现在展示一些不同的XYChart实现的例子,从ScatterChart开始。所有XYChart共有的一些特征也在ScatterChart部分进行了解释。

Note

因为Axis类扩展了Region,它们允许应用与任何其他Regions相同的 CSS 元素。这允许高度定制的Axis实例。

使用散点图

ScatterChart类的一个实例用于呈现数据,其中每个数据项被表示为二维区域中的一个符号。如前一节所述,我们将呈现一个包含三个数据系列的图表,代表 Java、C 和 C++的 TIOBE 指数的假设发展。我们首先展示一个简单实现的代码,并将其提炼为更有用的内容。

一个简单的实现

清单 6-5 显示了我们的应用程序使用ScatterChart的第一个实现。

package com.projavafx ;

import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.ScatterChart;
import javafx.scene.chart.XYChart;
import javafx.scene.chart.XYChart.Series;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

public class ChartApp3 extends Application {

    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage primaryStage) {
        NumberAxis xAxis = new NumberAxis();
        NumberAxis yAxis = new NumberAxis();
        ScatterChart scatterChart = new ScatterChart(xAxis, yAxis);
        scatterChart.setData(getChartData());
        primaryStage.setTitle("ScatterChart");

        StackPane root = new StackPane();
        root.getChildren().add(scatterChart);
        primaryStage.setScene(new Scene(root, 400, 250));
        primaryStage.show();
    }

    private ObservableList<XYChart.Series<Integer, Double>> getChartData() {
        double javaValue = 15.57;
        double cValue = 6.97;
        double cppValue = 4.55;
        ObservableList<XYChart.Series<Integer, Double>> answer = FXCollections.observableArrayList();
        Series<Integer, Double> java = new Series<>();
        Series<Integer, Double> c = new Series<>();
        Series<Integer, Double> cpp = new Series<>();
        for (int i = 2017; i < 2027; i++) {
            java.getData().add(new XYChart.Data(i, javaValue));
            javaValue = javaValue + 4 * Math.random() - 2;
            c.getData().add(new XYChart.Data(i, cValue));
            cValue = cValue + Math.random() - .5;
            cpp.getData().add(new XYChart.Data(i, cppValue));
            cppValue = cppValue + 4 * Math.random() - 2;
        }
        answer.addAll(java, c, cpp);
        return answer;
    }
}

Listing 6-5.First Implementation of Rendering Data in a ScatterChart

执行该应用程序会产生一个类似于图 6-6 所示的图形。

A323806_4_En_6_Fig6_HTML.jpg

图 6-6。

The result of the naive implementation of the ScatterChart

虽然图表显示了所需的信息,但可读性不强。我们添加了一些增强功能,但是首先让我们更深入地看看代码的不同部分。

PieChart示例类似,我们创建了一个单独的方法来获取数据。其中一个原因是,在现实世界的应用程序中,不太可能有静态数据。通过将数据检索隔离在一个单独的方法中,改变获取数据的方式变得更加容易。

单个数据点由一个实例XYChart.Data<IntegerDouble>定义,用构造器XYChart.Data(Integer i, Double d)创建,其中参数定义如下。

i: Integer, representing a specific year (between 2017 and 2026)
d: Double, representing the hypothetical TIOBE index for the particular series in the year specified by I

局部变量javaValuecValuecppValue用于记录不同编程语言的分数。它们用 2017 年的实际值初始化。每一年,个人得分会以–2 到+2 之间的随机值递增或递减。数据点堆叠成一系列。在我们的例子中,我们有三个系列,每个系列包含 10 个XYChart.Data<IntegerDouble>的实例。这些系列属于XYChart.Series<Integer, Double>类型。

通过调用将数据条目添加到相应的序列中

java.getData().add (...)
c.getData().add(...)

cpp.getData().add(...)

最后,所有序列都被添加到ObservableList<XYChart.Series<Integer, Double>>并返回。

应用程序的start()方法包含创建和呈现ScatterChart以及用从getChartData方法获得的数据填充它所需的功能。

Note

如前所述,我们可以在这里使用不同的模式与PieChart。我们在示例中使用了 JavaBeans 模式,但是我们也可以使用属性。

为了创建一个ScatterChart,我们需要创建一个xAxis和一个yAxis。在我们的第一个简单实现中,我们为此使用了两个NumberAxis实例:

NumberAxis xAxis = new NumberAxis();
NumberAxis yAxis = new NumberAxis();

除了调用下面的ScatterChart构造器,这个方法与PieChart的情况没有什么不同。

ScatterChart scatterChart = new ScatterChart(xAxis, yAxis);

改进简单的实现

查看图 6-5 时,首先观察到的一个现象是,一个系列中的所有数据图几乎都呈现在彼此的顶部。原因很清楚:x-Axis从 0 开始,到 2250 结束。默认情况下,NumberAxis会自动确定其范围。我们可以通过将autoRanging属性设置为 false,并为lowerBoundupperBound提供值来否决这种行为。如果我们用下面的代码片段替换原始示例中的xAxis的构造器,

NumberAxis xAxis = new NumberAxis();
xAxis.setAutoRanging(false);
xAxis.setLowerBound(2017);
xAxis.setUpperBound(2027);

结果输出将类似于图 6-7 所示。

A323806_4_En_6_Fig7_HTML.jpg

图 6-7。

Defining the behavior of the xAxis

接下来,我们希望向图表添加一个标题,并且希望图例节点中的符号附近有名称。向图表添加标题与向PieChart添加标题没有什么不同,这是通过代码实现的:

scatterChart.setTitle("Speculations");

通过向三个XYChart.Series实例添加名称,我们向图例节点中的符号添加标签。getChartData方法的相关部分变成

Series<Integer, Double> java = new Series<>();
Series<Integer, Double> c = new Series<>();
Series<Integer, Double> cpp = new Series<>();
java.setName("java");
c.setName("C");
cpp.setName("C++");

在应用两个更改后再次运行应用程序会产生类似于图 6-8 所示的输出。

A323806_4_En_6_Fig8_HTML.jpg

图 6-8。

ScatterChart with a title and named symbols

到目前为止,我们用一个NumberAxis来表示xAxis。因为年可以被表示为Number实例,这是可行的。但是,因为我们不对年份进行任何数值运算,并且因为连续数据条目之间的距离总是一年,所以我们也可以使用一个String值来表示这些信息。

我们现在修改代码,用一个CategoryAxis代替xAxis。将xAxisNumberAxis更改为CategoryAxis也意味着getChartData()方法应该返回ObservableList<XYChart.Series<String, Double>>的一个实例,这意味着单个Series中的不同元素应该具有类型XYChart.Data<String, Double>

在清单 6-6 中,原始代码被修改为使用CategoryAxis

package projavafx ;

import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.chart.CategoryAxis;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.ScatterChart;
import javafx.scene.chart.XYChart;
import javafx.scene.chart.XYChart.Series;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

public class ChartApp7 extends Application {

    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage primaryStage) {
        CategoryAxis xAxis = new CategoryAxis();
        NumberAxis yAxis = new NumberAxis();
        ScatterChart scatterChart = new ScatterChart(xAxis, yAxis);
        scatterChart.setData(getChartData());
        scatterChart.setTitle("speculations");
        primaryStage.setTitle("ScatterChart example");

        StackPane root = new StackPane();
        root.getChildren().add(scatterChart);
        primaryStage.setScene(new Scene(root, 400, 250));
        primaryStage.show();
    }

    private ObservableList<XYChart.Series<String, Double>> getChartData() {
        double javaValue = 15.57;
        double cValue = 6.97;
        double cppValue = 4.55;
        ObservableList<XYChart.Series<String, Double>> answer = FXCollections.observableArrayList();
        Series<String, Double> java = new Series<>();
        Series<String, Double> c = new Series<>();
        Series<String, Double> cpp = new Series<>();
        java.setName("java");
        c.setName("C");
        cpp.setName("C++");

        for (int i = 2017; i < 2027; i++) {
            java.getData().add(new XYChart.Data(Integer.toString(i), javaValue));
            javaValue = javaValue + 4 * Math.random() - .2;
            c.getData().add(new XYChart.Data(Integer.toString(i), cValue));
            cValue = cValue + 4 * Math.random() - 2;
            cpp.getData().add(new XYChart.Data(Integer.toString(i), cppValue));
            cppValue = cppValue + 4 * Math.random() - 2;
        }
        answer.addAll(java, c, cpp);
        return answer;
    }
}

Listing 6-6.Using CategoryAxis Instead of 
NumberAxis

for the xAxis

运行修改后的应用程序会产生类似于图 6-9 的输出。

A323806_4_En_6_Fig9_HTML.jpg

图 6-9。

Using a ScatterChart with a CategoryAxis on the xAxis

使用折线图

上一节中的示例导致数据条目由单个点或符号表示。通常,最好用一条线将点连接起来,因为这有助于观察趋势。JavaFX LineChart非常适合这一点。

用于LineChart的 API 与用于ScatterChart的 API 有许多共同的方法。事实上,我们可以重用清单 6-6 中的大部分代码,只需用LineChart替换ScatterChart的出现,用javafx.scene.chart.LineChart替换javafx.scene.chart.ScatterChart的导入。数据保持不变,所以我们只在清单 6-7 中显示新的start()方法。

public void start(Stage primaryStage) {
    CategoryAxis xAxis = new CategoryAxis();
    NumberAxis yAxis = new NumberAxis();
    LineChart lineChart = new LineChart(xAxis, yAxis);
    lineChart.setData(getChartData());
    lineChart.setTitle("speculations");
    primaryStage.setTitle("LineChart example");

    StackPane root = new StackPane();
    root.getChildren().add(lineChart);
    primaryStage.setScene(new Scene(root, 400, 250));
    primaryStage.show();
}

Listing 6-7.Using a LineChart Instead of a ScatterChart

运行该应用程序会产生如图 6-10 所示的输出。

A323806_4_En_6_Fig10_HTML.jpg

图 6-10。

Using a LineChart for displaying trends

对于ScatterChart可用的大多数功能对于LineChart也是可用的。使用LineChart可以改变图例的位置,添加或删除标题,以及使用NumberAxis代替CategoryAxis

使用条形图

一个BarChart能够呈现与一个ScatterChart和一个LineChart相同的数据,但是看起来不同。在BarChart中,重点通常是显示给定类别的不同系列之间的相对差异。在我们的例子中,这意味着我们关注 Java、C 和 C++的值之间的差异。

同样,我们不需要修改返回数据的方法。的确,一个BarChart需要一个CategoryAxis作为它的xAxis,我们已经修改了getChartData()方法来返回一个包含XYChart.Series<String, double>ObservableList。从清单 6-6 开始,我们仅将出现的ScatterChart更改为BarChart,并获得清单 6-8 。

public void start(Stage primaryStage) {
    CategoryAxis xAxis = new CategoryAxis();
    NumberAxis yAxis = new NumberAxis();
    BarChart barChart = new BarChart(xAxis, yAxis);
    barChart.setData(getChartData());
    barChart.setTitle("speculations");
    primaryStage.setTitle("BarChart example");

    StackPane root = new StackPane();
    root.getChildren().add(barChart);
    primaryStage.setScene(new Scene(root, 400, 250));
    primaryStage.show();
}

Listing 6-8.Using a BarChart Instead of a ScatterChart

一旦我们用javafx.scene.chart.BarChart的导入替换了javafx.scene.chart.ScatterChart的导入,我们就可以构建并运行应用程序了。结果是一个类似于图 6-11 所示的BarChart

A323806_4_En_6_Fig11_HTML.jpg

图 6-11。

Using BarChart for highlighting differences between the values

虽然结果确实显示了各年数值之间的差异,但并不十分清楚,因为条形相当小。总场景宽度为 400 像素,没有太多空间来渲染大条形。但是,条形图 API 包含定义条形之间的内部间距和类别之间的间距的方法。在我们的例子中,我们希望条之间的间隙更小,例如一个像素。这是通过调用

barChart.setBarGap(1);

将这一行代码添加到 start 方法并重新运行应用程序会产生如图 6-12 所示的输出。

A323806_4_En_6_Fig12_HTML.jpg

图 6-12。

Setting the gap between bars to one pixel

显然,这一行代码导致了可读性的巨大差异。

使用堆叠条形图

JavaFX 2.1 中增加了StackedBarChart。与BarChart一样,StackedBarChart在条形中显示数据,但是StackedBarChart不是将同一类别的条形一个接一个地显示,而是将同一类别的条形一个接一个地显示。这通常使得检查总数更容易。

通常,类别与数据系列中的常用键值相对应。因此,在我们的示例中,不同的年份(2017 年、2018 年、……2026 年)可以被视为类别。我们可以将这些类别添加到xAxis,如下所示:

IntStream.range(2017,2026).forEach(t -> xAxis.getCategories().add(String.valueOf(t)));

除此之外,惟一的代码变化是在代码和导入语句中用StackedBarChart替换了BarChart。这导致了清单 6-9 中的代码片段。

public void start(Stage primaryStage) {
    CategoryAxis xAxis = new CategoryAxis();
    IntStream.range(2017,2026).forEach(t -> xAxis.getCategories().add(String.valueOf(t)));
    NumberAxis yAxis = new NumberAxis();
    StackedBarChart stackedBarChart = new StackedBarChart(xAxis, yAxis, getChartData());
    stackedBarChart.setTitle("speculations");
    primaryStage.setTitle("StackedBarChart example");

    StackPane root = new StackPane();
    root.getChildren().add(stackedBarChart);
    Scene scene = new Scene(root, 400, 250);
    primaryStage.setScene(scene);
    primaryStage.show();
}

Listing 6-9.Using a StackedBarChart Instead of a ScatterChart

现在运行应用程序会产生如图 6-13 所示的输出。

A323806_4_En_6_Fig13_HTML.jpg

图 6-13。

Rendering stacked bar chart plots using StackedBarChart

使用面积图

在某些情况下,填充点连线下方的区域是有意义的。虽然与在LineChart的情况下呈现相同的数据,但结果看起来不同。清单 6-10 包含了修改后的start()方法,它使用了一个AreaChart来代替原来的ScatterChart。和之前的修改一样,我们没有改变getChartData()方法。

public void start(Stage primaryStage) {
    CategoryAxis xAxis = new CategoryAxis();
    NumberAxis yAxis = new NumberAxis();
    AreaChart areaChart = new AreaChart(xAxis, yAxis);
    areaChart.setData(getChartData());
    areaChart.setTitle("speculations");
    primaryStage.setTitle("AreaChart example");
    StackPane root = new StackPane();
    root.getChildren().add(areaChart);
    primaryStage.setScene(new Scene(root, 400, 250));
    primaryStage.show();
}
Listing 6-10.Using an AreaChart Instead of a ScatterChart

运行该应用程序会产生如图 6-14 所示的输出。

A323806_4_En_6_Fig14_HTML.jpg

图 6-14。

Rendering area plots using AreaChart

使用堆叠面积图

StackedAreaChart对于AreaChart就像StackedBarChart对于BarChart一样。StackedAreaChart不是显示单个区域,而是总是显示特定类别中值的总和。

AreaChart更改为StackedAreaChart只需要更改一行代码和适当的导入语句。

AreaChart areaChart = new AreaChart(xAxis, yAxis);

必须替换为

StackedAreaChart areaChart = new StackedAreaChart(xAxis, yAxis);

应用此更改并运行应用程序会产生类似图 6-15 中的图表。

A323806_4_En_6_Fig15_HTML.jpg

图 6-15。

Rendering area plots using AreaChart

使用气泡图

XYChart的最后一个实现很特殊。BubbleChart不包含已经不在XYChart类上的属性,但是它是当前 JavaFX Chart API 中唯一使用XYChart.Data类上的附加参数的直接实现。

我们首先修改清单 6-6 中的代码,使用BubbleChart代替ScatterChart。因为默认情况下,当xAxis上的跨度与yAxis上的跨度相差很大时,气泡会被拉伸,所以我们不用年,而是用一年的十分之一作为xAxis上的值。这样,我们在xAxis (10 年)上的跨度为 100 个单位,而在yAxis上的跨度约为 30 个单位。这或多或少也是我们图表的宽度和高度之比。因此,气泡相对来说是圆形的。

清单 6-11 包含了呈现一个BubbleChart的代码。

package com.projavafx.charts;

import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.chart.*;
import javafx.scene.chart.XYChart.Series;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import javafx.util.StringConverter;

public class ChartApp14 extends Application {

    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage primaryStage) {
        NumberAxis xAxis = new NumberAxis();
        NumberAxis yAxis = new NumberAxis();
        yAxis.setAutoRanging(false);
        yAxis.setLowerBound(0);
        yAxis.setUpperBound(30);
        xAxis.setAutoRanging(false);
        xAxis.setAutoRanging(false);
        xAxis.setLowerBound(20170);
        xAxis.setUpperBound(20261);
        xAxis.setTickUnit(10);
        xAxis.setTickLabelFormatter(new StringConverter<Number>() {

            @Override
            public String toString(Number n) {
                return String.valueOf(n.intValue() / 10);
            }

            @Override
            public Number fromString(String s) {
                return Integer.valueOf(s) * 10;
            }
        });
        BubbleChart bubbleChart = new BubbleChart(xAxis, yAxis);
        bubbleChart.setData(getChartData());
        bubbleChart.setTitle("Speculations");
        primaryStage.setTitle("BubbleChart example");

        StackPane root = new StackPane();
        root.getChildren().add(bubbleChart);
        primaryStage.setScene(new Scene(root, 400, 250));

        primaryStage.show();
    }

    private ObservableList<XYChart.Series<Integer, Double>> getChartData() {
        double javaValue = 15.57;
        double cValue = 6.97;
        double cppValue = 4.55;
        ObservableList<XYChart.Series<Integer, Double>> answer = FXCollections.observableArrayList();
        Series<Integer, Double> java = new Series<>();
        Series<Integer, Double> c = new Series<>();
        Series<Integer, Double> cpp = new Series<>();
        java.setName("java");
        c.setName("C");
        cpp.setName("C++");
        for (int i = 20170; i < 20260; i = i + 10) {
            double diff = Math.random();
            java.getData().add(new XYChart.Data(i, javaValue));
            javaValue = Math.max(javaValue + 2 * diff - 1, 0);
            diff = Math.random();
            c.getData().add(new XYChart.Data(i, cValue));
            cValue = Math.max(cValue + 2 * diff - 1, 0);
            diff = Math.random();
            cpp.getData().add(new XYChart.Data(i, cppValue));
            cppValue = Math.max(cppValue + 2 * diff - 1, 0);
        }
        answer.addAll(java, c, cpp);
        return answer;
    }

}

Listing 6-11.Using the BubbleChart

xAxis的范围从 201670 年到 20261 年,但是我们当然希望在轴上显示年份。这可以通过调用

xAxis.setTickLabelFormatter(new StringConverter<Number>() {
 ...
}

我们提供的StringConverter将我们使用的数字(如 20210)转换成Strings(如 2021),反之亦然。这样做,我们能够使用任何我们想要的量来计算气泡,并且仍然有一个格式化标签的好方法。运行该示例会产生如图 6-16 所示的图表。

A323806_4_En_6_Fig16_HTML.jpg

图 6-16。

Using a BubbleChart with fixed radius

直到现在,我们都没有利用XYChart.Data的三参数构造器。除了我们已经熟悉的双参数构造器之外,

XYChart.Data (X xValue, Y yValue)

XYChart.Data也有一个三参数的构造器:

XYChart.Data (X xValue, Y yValue, Object extraValue)

extraValue参数可以是任何类型。这允许开发人员实现他们自己的XYChart子类,利用可以包含在单个数据元素中的附加信息。BubbleChart实现使用这个extraValue来决定应该渲染多大的气泡。

我们现在修改getChartData()方法来使用三参数构造器。xValueyValue参数仍然与前面的清单中的相同,但是我们现在添加了第三个参数,表示即将到来的趋势。这个参数越大,下一年的涨幅就越大。参数越小,下一年跌幅越大。修改后的getChartData()方法如清单 6-12 所示。

private ObservableList<XYChart.Series<Integer, Double>> getChartData() {
    double javaValue = 15.57;
    double cValue = 6.97;
    double cppValue = 4.55;
    ObservableList<XYChart.Series<Integer, Double>> answer = FXCollections.observableArrayList();
    Series<Integer, Double> java = new Series<>();
    Series<Integer, Double> c = new Series<>();
    Series<Integer, Double> cpp = new Series<>();
    java.setName("java");
    c.setName("C");
    cpp.setName("C++");
    for (int i = 20170; i < 20270; i =  i+10) {
        double diff = Math.random();
        java.getData().add(new XYChart.Data(i, javaValue, 2*diff));
        javaValue = Math.max(javaValue + 2*diff - 1,0);
        diff = Math.random();
        c.getData().add(new XYChart.Data(i, cValue,2* diff));
        cValue = Math.max(cValue + 2*diff - 1,0);
        diff = Math.random();
        cpp.getData().add(new XYChart.Data(i, cppValue, 2*diff));
        cppValue = Math.max(cppValue + 2*diff - 1,0);
    }
    answer.addAll(java, c, cpp);
    return answer;
}
Listing 6-12.Using a Three-Argument Constructor

for XYChart.Data Instances

将该方法与清单 6-11 中的start()方法相结合,会产生如图 6-17 所示的输出。

A323806_4_En_6_Fig17_HTML.jpg

图 6-17。

Adding variations in the size of the Bubbles

摘要

JavaFX Chart API 为不同的图表类型提供了许多现成的实现。每一个实现都有不同的目的,开发人员可以选择最合适的Chart

通过应用 CSS 规则或使用特定于Chart的方法或属性,可以修改Chart并为特定的应用程序进行调优。

如果您需要一个更加定制化的Chart,您可以扩展抽象的Chart类并利用该类上的现有属性,或者如果您的图表需要两个轴,您可以扩展抽象的XYChart类。

资源

有关 JavaFX Chart API 的更多信息,请参考以下资源:

七、连接到企业服务

专家是在一个狭窄的领域里犯了所有可能犯的错误的人。—尼尔斯·玻尔

客户端应用程序可能非常令人兴奋,但通常它们不会生活在孤立的环境中。典型的客户端应用程序以某种方式与其他应用程序、后端组件和云环境交换数据和功能。这对客户端应用程序的开发提出了新的要求。

到目前为止,我们已经解释了如何使用 JavaFX 平台来呈现信息和交互操作数据。在本章中,我们简要概述了将 JavaFX 应用程序与企业系统集成的可用选项,然后继续介绍该过程的一些具体示例。

我们的示例旨在演示 JavaFX 应用程序如何轻松地访问 REST 资源,然后将响应(来自 JSON 或 XML 格式)转换为 JavaFX 控件可以理解的格式。作为我们的示例外部数据源,堆栈交换 API 是理想的,因为它们是公开可用的,易于理解,并且在互联网上广泛使用。

前端和后端平台

JavaFX 通常被认为是一个前端平台。虽然这种说法没有公平对待 JavaFX 平台中与 UI 无关的 API,但大多数 JavaFX 应用程序确实关注“内容”的丰富和交互式可视化。

Java 的一大优点是,一种语言可以在多种设备、桌面和服务器上使用。创建 JavaFX 核心的 Java 语言也是 Java 平台企业版(Java EE)的基础核心。

Java 平台是企业应用程序的头号开发平台。JavaFX 平台提供了丰富的交互式 UI,与运行在 Java 平台上的企业应用程序相结合,创造了巨大的可能性。为此,JavaFX 应用程序和 Java 企业应用程序必须交换数据。交换数据可以以多种方式发生,并且取决于需求(从前端以及从后端);一种方式可能比另一种方式更合适。

基本上,有两种不同的方法:

A323806_4_En_7_Fig2_HTML.jpg

图 7-2。

JavaFX application communicates with enterprise components on a remote server

A323806_4_En_7_Fig1_HTML.jpg

图 7-1。

JavaFX and enterprise components on a single system

  • JavaFX 应用程序可以利用这样一个事实,即它运行在与典型企业应用程序相同的基础设施上,并且可以与这些企业组件深度集成。如图 7-1 所示。
  • JavaFX 应用程序运行在一个相对简单的 Java 平台上,使用 Java 企业组件已经支持的标准协议与企业服务器交换数据。如图 7-2 所示。

第一种方法已被提及并简要涉及,但本章的重点是第二种方法,即 JavaFX 客户端与远程服务器上的 Java 企业组件进行通信。

没有所谓的最佳方法,因为它实际上取决于环境和用例。一个典型的 Java 客户端应用程序运行在比今天的后端服务器和云环境功能更少的硬件上,在这种情况下,不推荐第一种方法。然而,很明显,在一些情况下,资源(CPU、集群、可伸缩性)在客户端系统上是广泛可用的,在这种情况下,这种方法肯定是可以考虑的。

这里还应该强调的是,只要使用标准的、定义良好的协议(例如 SOAP/REST),就很有可能将 JavaFX 应用程序连接到非 Java 后端应用程序。客户机和服务器之间的分离确实允许在客户机和服务器上使用不同的编程语言。

在同一环境中合并 JavaFX 和 Java 企业模块

JavaFX 9 构建在 Java 平台标准版之上。因此,这个平台提供的所有功能都可以在 JavaFX 9 中使用。两个最流行的 Java 企业框架——Java 平台企业版和 Spring 框架——也构建在 Java 平台标准版之上。因此,JavaFX 应用程序可以与使用 Java Platform,Enterprise Edition 的应用程序或使用 Spring Framework 构建的应用程序生活在同一个环境中。

JavaFX 开发人员因此可以使用他或她喜欢的企业工具来创建应用程序。这样做有许多好处。企业组件提供的工具允许开发人员专注于特定的领域层,同时保护他们免受数据库资源和事务的影响。

Java 是企业环境中的一个流行平台,许多公司、组织和个人已经开发了许多企业组件和库。

Java 平台企业版是由通过 Java 社区进程(JCP)计划标准化的规范定义的。对于不同的组成部分,单独的 Java 规范请求(JSR)被归档。

这些单独的 JSR 中的大多数都是由许多公司实现的,并且这些实现通常被组合到一个产品中。典型的企业框架实现一个或多个 JSR,并且它们可能包含额外的特定于产品的功能。

在这些规范中最流行的实现中,有 Tomcat/TomEE、Hibernate、JBoss/WildFly、GlassFish、Payara、WebLogic 和 WebSphere。许多产品实现了所有的 JSR,这些产品被称为 Java 平台企业版的实现,通常被称为 Java EE 平台或应用服务器。

另一个流行的 Java 企业框架 Spring Framework 包含了 Java 平台企业版中定义的许多 JSR 的实现,并添加了更多特定的组件和 API。

从技术上讲,JavaFX 平台中没有阻止使用 Java 企业组件的限制。很有可能在客户机系统上运行 Java 企业应用服务器,或者在同一个客户机系统上执行 Spring 框架应用程序。利用 Java 企业应用服务器或 Spring 框架的应用程序也可以包含 JavaFX 代码。

然而,企业开发在许多方面不同于客户端开发:

  • 企业基础设施正在向云转移。特定任务(例如,存储、邮件等。)外包给“云”中提供特定功能的组件。企业服务器通常位于云环境中,允许与云组件进行快速无缝的交互。
  • 就资源需求而言,企业系统侧重于计算资源(CPU、缓存和内存),而台式计算机和笔记本电脑侧重于视觉资源(例如,图形硬件加速)。
  • 启动时间在服务器中几乎不成问题,但在许多桌面应用程序中却至关重要。此外,服务器应该是 24/7 全天候运行的,而大多数客户机并不是这样。
  • 部署和生命周期管理通常特定于服务器产品或客户端产品。升级服务器或服务器软件通常是一个乏味的过程。必须尽量减少停机时间,因为客户端应用程序可能会打开与服务器的连接。部署客户端应用程序可以通过多种方式进行,例如通过独立、自包含的应用程序或 Java 网络启动协议(JNLP)。
  • 企业开发使用了许多在客户端开发中有用的模式(例如,控制反转、基于容器的初始化),但是这些模式通常需要与传统客户端不同的架构。

使用 JavaFX 调用远程(Web)服务

企业组件通常通过 web 资源来访问。一些规范清楚地描述了基于 web 的框架应该如何与企业组件交互以呈现信息。然而,还有其他规范允许从非 web 资源访问企业组件(用 Java 或其他语言编写)。因为那些规范允许企业开发和任何其他开发之间的解耦,所以它们已经被许多涉众定义了。

1998 年,简单对象访问协议(SOAP)由微软发明,随后被用作 Java 应用程序和。NET 应用程序。SOAP 是基于 XML 的,当前的版本 1.2 是 2003 年 W3C 推荐的。Java 提供了许多工具,允许开发人员与 SOAP 交换数据。

尽管 SOAP 功能强大且可读性相对较好,但通常被认为相当冗长。随着 mashups 和提供特定功能的简单服务的兴起,出现了一种新的架构风格:表述性状态转移(REST)。REST 允许服务器和客户端开发人员以一种松散耦合且更加简化的方式交换数据,其中协议可以是 XML、JSON、Atom 或任何其他格式。

在下一节中,我们将展示一些使用 REST APIs 在 JavaFX 客户端应用程序和服务器或云组件之间建立通信的例子。虽然这种方法将显示数据是如何传输的,但它仍然需要一些锅炉板代码。在本节之后,我们将讨论一些框架,这些框架使得与企业组件的连接对 JavaFX 开发人员来说更加透明。

休息

在互联网上可以找到大量关于 REST 和基于 REST 的 web 服务的资源和文档。基于 REST 的 web 服务公开了许多可以使用 HTTP 协议访问的 URIs。通常,不同的 HTTP request方法(get、post、put、delete)用于表示对资源的不同操作。

基于 REST 的 web 服务可以使用标准的 HTTP 技术来访问,Java 平台附带了许多 API(主要在java.iojava.net中),方便了对基于 REST 的 web 服务的访问。

JavaFX 在 Java 平台(Standard Edition 9)之上编写的主要优势之一是能够在 JavaFX 应用程序中使用所有这些 API。这就是我们在本章第一个例子中所做的。我们展示了如何使用 Java APIs 来消费基于 REST 的 web 服务,以及如何将结果集成到 JavaFX 应用程序中。

接下来,我们将展示如何利用 JavaFX APIs 来避免常见的缺陷(例如,无响应的应用程序、无动态更新等)。).最后,我们简要概述了使 JavaFX 开发人员能够轻松访问基于 REST 的 web 服务的第三方库。

设置应用程序

首先,我们为样本创建框架。我们将使用 Stack Exchange 提供的 API。Stack Exchange 网络是一个论坛集群,每个论坛位于一个特定的域中,在这里,问题和答案以这样一种方式组合在一起,即来自最受信任的用户的“最佳”答案会出现在顶部。Java 开发人员可能对 Stack Overflow 很熟悉,它是 Stack Exchange 中的第一个站点,提供了大量与 IT 相关的问题和答案。

Stack Exchange 提供的 REST APIs 在 https://api.stackexchange.com 有很好的描述。我们的目标不是探索 Stack Exchange 和相应 API 提供的所有可能性,因此感兴趣的读者可以参考网站上的文档。

在本章的示例中,我们希望将问题的作者、问题的标题以及提出问题的日期可视化。

最初,我们用带有 getters 和 setters 的 Java 对象来表示一个问题。这显示在清单 7-1 中。

package projavafx;

public class Question {

    private String owner;
    private String question;
    private long timestamp;

    public Question () {
    }
    public Question (String o, String q, long t) {
        this.owner = o;
        this.question = q;
        this.timestamp = t;
    }

    public String getOwner() {
        return owner;
    }

    public void setOwner(String owner) {
        this.owner = owner;
    }

    public String getQuestion() {
        return question;
    }

    public void setQuestion(String question) {
        this.question = question;
    }

    public long getTimestamp() {
        return timestamp;
    }

    public void setTimestamp(long timestamp) {
        this.timestamp = timestamp;
    }

}

Listing 7-1.

Question Class

我们的Question类有两个构造器。在下面的一个例子中需要零参数构造器,我们稍后再回到这个例子。在其他示例中,为了方便起见,使用了带三个参数的构造器。

在清单 7-2 中,我们展示了如何显示问题。在第一个例子中,问题不是通过栈交换 API 获得的,但是它们在这个例子中是硬编码的。

package projavafx;

import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.ListView;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

public class StackOverflowApp1 extends Application {

    public static void main(String[] args) {
        launch(args);

    }

    @Override
    public void start(Stage primaryStage) {
        ListView<Question> listView = new ListView<>();
        listView.setItems(getObservableList());
        StackPane root = new StackPane();
        root.getChildren().add(listView);

        Scene scene = new Scene(root, 500, 300);
        primaryStage.setTitle("StackOverflow List");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    ObservableList<Question> getObservableList() {
        ObservableList<Question> answer = FXCollections.observableArrayList();
        long now = System.currentTimeMillis();
        long yesterday = now - 1000 * 60 * 60 * 24;
        Question q1 = new Question("James", "How can I call a REST service?", now);
        Question q2 = new Question("Stephen", "Does JavaFX work on Android?", yesterday);
        answer.addAll(q1, q2);
        return answer;
    }

}

Listing 7-2.Framework for Rendering Questions in a 
ListView

如果您已经阅读了前面的章节,这段代码并没有包含任何新的内容。我们创建一个ListView,将其添加到一个StackPane,创建一个Scene,并渲染Stage

用包含Question s 的ObservableList填充ListView,这个ObservableList是通过调用getObservableList()方法获得的。在下面的示例中,我们修改了这个方法,并展示了如何从堆栈交换 API 中检索Question s。

Note

getObservableList返回一个ObservableListListView自动观察这个ObservableList。因此,ObservableList的变化会立即呈现在ListView控件中。在后面的示例中,我们将利用这一功能。

运行该示例会产生如图 7-3 所示的窗口。

A323806_4_En_7_Fig3_HTML.jpg

图 7-3。

The result of the first example

结果窗口包含一个有两个条目的ListView。这些条目对应于清单 7-2 底部的getObservableList()方法中创建的两个问题。

窗口中显示的有关问题的信息不是很有用。事实上,我们告诉ListView它应该显示一些Question的实例,但是我们没有告诉它们应该如何显示。后者可以通过指定一个CellFactory来实现。在这一章中,我们的目标不是创建一个花哨的 UI;相反,我们希望展示如何检索数据并在 UI 中呈现这些数据。因此,我们简要地展示了开发人员如何通过使用CellFactory概念来改变数据的可视化。关于我们在示例中使用的 UI 控件的概述(ListViewTableView,请参考第六章。

在清单 7-3 中,我们创建了一个QuestionCell类,它扩展了ListCell并定义了如何布局一个单元格。

package projavafx;

import java.text.SimpleDateFormat;
import java.util.Date;
import javafx.scene.control.ListCell;

public class QuestionCell extends ListCell<Question> {

    static final SimpleDateFormat sdf = new SimpleDateFormat ("dd-MM-YY");
    @Override
    protected void updateItem(Question question, boolean empty){
        super.updateItem(question, empty);
        if (empty) {
            setText("");
        } else {
            StringBuilder sb= new StringBuilder();
            sb.append("[").append(sdf.format(new Date(question.getTimestamp()))).append("]")
                    .append(" ").append(question.getOwner()+": "+question.getQuestion());
            setText(sb.toString());
        }
    }
}

Listing 7-3.Define 
QuestionCell

当一个单元格项需要更新时,我们告诉它显示一些包含方括号中的时间戳的文本,后面是作者和问题的标题。接下来,ListView需要被告知它应该呈现QuestionCell s。我们通过调用ListView.setCellFactory()方法来做到这一点,并提供一个 lambda 表达式,该表达式在被调用时创建一个新的QuestionCell。在清单 7-4 中,我们展示了我们的StackOverflowApplication的启动方法的修改版本。

public void start(Stage primaryStage) {
      ListView<Question> listView = new ListView<>();
      listView.setItems(getObservableList());
      listView.setCellFactory(l -> new QuestionCell());
      StackPane root = new StackPane();
      root.getChildren().add(listView);

      Scene scene = new Scene(root, 500, 300);

      primaryStage.setTitle("StackOverflow List");
      primaryStage.setScene(scene);
      primaryStage.show();
  }

Listing 7-4.Use CellFactory on the ListView

如果我们现在运行应用程序,输出如图 7-4 所示。

A323806_4_En_7_Fig4_HTML.jpg

图 7-4。

The result of adding a QuestionCell

对于ListView条目中的每个问题,现在的输出是我们所期望的。我们可以用CellFactories做更多的事情(例如,我们可以使用图形而不仅仅是文本),但是这超出了本章的范围。

我们现在用通过堆栈交换 API 获得的真实信息替换硬编码的问题。

使用堆栈交换 API

栈交换网( http://stackexchange.com )允许第三方开发者使用基于 REST 的接口浏览访问问题和答案。Stack Exchange 维护了许多基于 REST 的 API,但是在我们的例子中,我们仅限于 Stack Exchange Search API。关于此 API 的更多信息,请访问 http://api.stackexchange.com/docs

资源 URL——REST 服务的端点——非常简单:

http://api.stackexchange.com/2.2/search

这里可以提供许多查询参数。我们将只使用两个参数,感兴趣的读者可以参考栈交换文档,了解关于其他参数的信息。

  • site:指定您想要搜索的域,在我们的例子中是“stackoverflow"
  • tagged:分号分隔的标签列表。我们希望搜索所有标有javafx"的问题。

组合这两个参数会导致以下 REST 调用:

http://api.stackexchange.com/2.2/search?tagged=javafx&site=stackoverflow

当在浏览器中执行这个 REST 调用,或者使用命令工具(例如 cURL)时,结果类似于清单 7-5 中的 JSON-text。

{
  "items": [
    {
      "tags": [
        "java",
        "sorting",
        "javafx",
        "tableview"
      ],
      "owner": {
        "reputation": 132,
        "user_id": 578518,
        "user_type": "registered",
        "accept_rate": 84,
        "profile_image": "https://www.gravatar.com/avatar/bdbee99c377a7063b24e09e7121fb1ab?s=128&d=identicon&r=PG",
        "display_name": "Rps",
        "link": "http://stackoverflow.com/users/578518/rps"
      },
      "is_answered": false,

      "view_count": 7,
      "answer_count": 1,
      "score": 0,
      "last_activity_date": 1397845222,
      "creation_date": 1397844823,
      "last_edit_date": 1397845143,
      "question_id": 23159737,
      "link": "http://stackoverflow.com/questions/23159737/javafx-tableview-ordered-by-date",
      "title": "javafx Tableview ordered by date"
    },

...
,"has_more":true
,"quota_max":300,
"quota_remaining":290
}

Listing 7-5.
JSON Response

Obtained from the Stack Exchange Search API

堆栈交换 API 只提供基于 JSON 的响应。许多 web 服务以 XML 的形式提供信息,还有一些同时提供 JSON 和 XML。因为我们还想展示如何处理 XML 响应,所以我们为 Stack Exchange REST 服务创建了自己的基于 XML 的输出。这个 XML 响应不是调用外部 REST 端点,而是通过读取本地文件获得的。

我们自定义的 XML 响应如清单 7-6 所示。

<?xml version="1.0" encoding="UTF-8"?>

<items>

  <item>

    <tags>

      <tag>java</tag>

      <tag>sorting</tag>

      <tag>javafx</tag>

      <tag>tableview</tag>

    </tags>

    <owner>Rps</owner>

    <creation_date>1397844823</creation_date>

    <title>javafx Tableview ordered by date</title>

  <item>

</items>

Listing 7-6.Artificial XML Response

Obtained from the Stack Exchange Search API

尽管 JSON 响应中的数据包含与 XML 响应中的数据相同的信息,但是格式当然是非常不同的。JSON 和 XML 都在互联网上广泛使用,大量 web 服务以这两种格式提供响应。

根据用例和开发人员的不同,一种格式可能比另一种格式更受青睐。一般来说,JavaFX 应用程序应该能够使用这两种格式,因为它们必须与第三方数据连接,JavaFX 开发人员不能总是影响后端使用的数据格式。

Note

许多应用程序允许多种格式,通过指定 HTTP“Accept”头,客户端可以在不同的格式之间进行选择。

在下一个例子中,我们将展示如何检索和解析栈交换搜索 API 中使用的 JSON 响应。

JSON 响应格式

JSON 是互联网上非常流行的格式,尤其是在用 JavaScript 解析输入数据的 web 应用程序中。JSON 数据相当紧凑,或多或少具有可读性。

Java 中有许多工具可以读写 JSON 数据。截至 2013 年 6 月,当 Java 企业版 7 发布时,Java 中有一个描述如何读写 JSON 数据的标准规范。本 Java 规范定义为 JSR 353,更多信息可在 www.jcp.org/en/jsr/detail?id=353 .获取

JSR 353 only defines a specification, and an implementation is still needed to do the actual work. In our examples, we will use JSONP, which is the Reference Implementation of JSR 353\. This Reference Implementation can be found at https://jsonp.java.net/ 。不过,我们鼓励读者尝试他们最喜欢的 JSR 353 实现。

尽管 JSR 353 是 Java 企业版的一部分,但是参考实现也可以在 Java 标准版环境中工作。没有外部依赖性。

我们现在用通过 Stack Exchange REST API 获得的真题替换包含两个假题的硬编码列表。我们保留现有的代码,但是我们修改了清单 7-7 中所示的getObservableList()方法。

ObservableList<Question> getObservableList() throws IOException {
    String url = "http://api.stackexchange.com/2.2/search?tagged=javafx&site=stackoverflow";
    URL host = new URL(url);
    JsonReader jr = Json.createReader(new GZIPInputStream(host.openConnection().getInputStream()));

    JsonObject jsonObject = jr.readObject();
    JsonArray jsonArray = jsonObject.getJsonArray("items");
    ObservableList<Question> answer = FXCollections.observableArrayList();

    jsonArray.iterator().forEachRemaining((JsonValue e) -> {
        JsonObject obj = (JsonObject) e;
        JsonString name = obj.getJsonObject("owner").getJsonString("display_name");
        JsonString quest = obj.getJsonString("title");
        JsonNumber jsonNumber = obj.getJsonNumber("creation_date");
        Question q = new Question(name.getString(), quest.getString(), jsonNumber.longValue() * 1000);
        answer.add(q);
    });
    return answer;
}

Listing 7-7.Obtain Questions via the Stack Exchange REST API

, JSON Format, and Parse the JSON

在深入研究代码之前,我们在图 7-5 中展示了修改后的应用程序的结果。

A323806_4_En_7_Fig5_HTML.jpg

图 7-5。

The result of the StackOverflowApplication retrieving JSON data

清单 7-7 中的代码可以分为四个部分:

  1. 调用 REST 端点。
  2. 获取原始 JSON 数据。
  3. 将每个项目转换成一个问题。
  4. Question添加到结果中。

调用 REST 端点非常简单:

String url = "http://api.stackexchange.com/2.2/search?tagged=javafx&site=stackoverflow";
URL host = new URL(url);
JsonReader jr = Json.createReader(new GZIPInputStream(host.openConnection().getInputStream()));

首先,我们创建一个指向所需位置的 URL 对象。接下来,我们打开到该位置的连接。因为 Stack Exchange 以压缩数据的形式发送数据,所以我们使用从连接中获得的InputStream打开一个GZIPInputStream。我们将这个GZIPInputStream作为Json.createReader()方法中的InputStream参数。

我们现在有了一个 JSON 阅读器,它使用我们想要的数据。从这个 JSON 阅读器手动提取 Java 对象需要针对特定情况的特定代码。

Note

我们也可以使用 JSON 解析器来代替 JSON 阅读器。我们并不打算提供详尽的 JSON 解析指南。我们只是试图展示如何将 JSON 数据转换成我们特定用例的 Java 对象。你可以很容易地在网上找到许多关于 JSON 的教程。

在清单 7-5 中,我们观察到问题在一个名为 items 的数组中,从左边的方括号(``)开始。我们可以使用下面的语句获得这个 JSON 数组:

JsonArray jsonArray = jsonObject.getJsonArray("items");

接下来,我们需要迭代所有这些元素。对于我们遇到的每个项目,我们希望创建一个Question实例。

遍历数组元素可以使用

jsonArray.iterator().forEachRemaining((JsonValue e) -> {
    ...
}

为了创建一个Question实例,我们需要获得问题的作者姓名、标题和创建日期。Java JSON API 为此提供了一种标准的方法:

JsonObject obj = (JsonObject) e;
JsonString name = obj.getJsonObject("owner").getJsonString("display_name");
JsonString quest = obj.getJsonString("title");
JsonNumber jsonNumber = obj.getJsonNumber("creation_date");

最后,我们需要基于这些信息创建一个Question实例,并将其添加到我们将返回的ObservableList实例中:

Question q = new Question(name.getString(), quest.getString(), jsonNumber.longValue() * 1000);
answer.add(q);

这个例子表明,检索和读取从 REST 端点获得的 JSON 数据,并将结果转换成一个ListView是非常容易的。在下一节中,我们将演示 XML 响应的类似过程。

XML 响应格式

XML 格式在 Java 平台中被广泛使用。因此,Java 中基于 XML 的操作的标准化在几年前就开始了。Java 平台标准版内置了许多 XML 工具,我们可以在 JavaFX 中使用这些 API 和工具,而无需任何外部依赖。在本节中,我们首先使用一个 DOM 处理器来解析我们人工构建的 XML 响应。接下来,我们使用 JAXB 标准来自动获取 Java 对象。

将我们的应用程序从 JSON 输入更改为 XML 输入只需要更改getObservableList方法。新的实现如清单 [7-8 所示。

ObservableList<Question> getObservableList() throws IOException, ParserConfigurationException, SAXException {
        ObservableList<Question> answer = FXCollections.observableArrayList();
        InputStream inputStream = this.getClass().getResourceAsStream("/stackoverflow.xml");
        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
        DocumentBuilder db = dbf.newDocumentBuilder();
        Document doc = db.parse(inputStream);
        NodeList questionNodes = doc.getElementsByTagName("item");
        int count = questionNodes.getLength();
        for (int i = 0; i < count; i++) {
            Question question = new Question();
            Element questionNode = (Element) questionNodes.item(i);

            NodeList childNodes = questionNode.getChildNodes();
            int cnt2 = childNodes.getLength();
            for (int j = 0; j < cnt2; j++) {
                Node me = childNodes.item(j);
                String nodeName = me.getNodeName();
                if ("creation_date".equals(nodeName)) {
                    question.setTimestamp(Long.parseLong(me.getTextContent()));
                }
                if ("owner".equals(nodeName)) {
                    question.setOwner(me.getTextContent());
                }
                if ("title".equals(nodeName)) {
                    question.setQuestion(me.getTextContent());
                }
            }
            answer.add(question);
        }
        return answer;

    }

Listing 7-8.Obtaining Questions from the XML-Based Response

同样,本节的目标不是给出 DOM APIs 的全面概述。互联网上有大量的资源提供关于 XML 的信息,特别是关于 DOM 的信息。

为了能够编译清单 7-8 中的代码,必须添加以下导入语句。

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

在我们详细讨论代码之前,我们在图 7-6 中展示了这个例子的输出。

A323806_4_En_7_Fig6_HTML.jpg

图 7-6。

The result of the question application using XML response

清单 7-8 中的代码与清单 7-7 中的代码有一些相似之处。在这两种情况下,我们处理文本格式(JSON 或 XML)的可用数据,并将数据转换成问题实例。在清单 7-8 中,DOM 方法用于检查收到的响应。

使用下面的代码获得了一个org.w3c.dom.Document实例。

InputStream inputStream = this.getClass().getResourceAsStream("/stackoverflow.xml");
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();
Document doc = db.parse(inputStream);

在这种情况下,我们基于一个InputStream创建一个DocumentInputStream是从人为创建的文件中获得的。我们还可以从一个URLConnection创建一个InputStream,并将这个InputStream传递给db.parse()方法。更简单的是,DocumentBuilder.parse方法还接受一个包含 REST 端点 URL 的String参数。

这表明,尽管我们在这种情况下使用的是包含问题的静态文件,但在使用真正的 REST 端点时,我们可以轻松地使用相同的代码。

现在可以查询结果Document。从清单 7-6 中显示的 XML 响应中,我们了解到各个问题都包含在名为“item”的 XML 元素中。我们使用下面的代码来获取这些 XML 元素的列表。

NodeList questionNodes = doc.getElementsByTagName("item");

然后我们遍历这个列表,通过检查各个 XML 元素中的childNodes来获得特定于问题的字段。最后,我们将产生的问题添加到名为 answer 的Question对象的ObservableList中。

这种方法相当简单,但是我们仍然需要做一些手工的 XML 解析。尽管这考虑到了灵活性,但是随着数据结构复杂性的增加,解析变得更加困难和容易出错。

幸运的是,Java 标准版 API 包含将 XML 直接转换成 Java 对象的工具。这些 API 的规范由 JAXB 标准定义,可以在javax.xml.bind包中获得。将 XML 数据转换成 Java 对象的过程称为解组。

我们现在修改我们的示例,并混合使用 DOM 解析和 JAXB 解组。同样,我们只改变了getObservableList()方法。修改后的实现如清单 7-9 所示。

ObservableList<Question> getObservableList() throws IOException, ParserConfigurationException, SAXException {
    ObservableList<Question> answer = FXCollections.observableArrayList();
     InputStream inputStream = this.getClass().getResourceAsStream("/stackoverflow.xml");
    DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
    DocumentBuilder db = dbf.newDocumentBuilder();
    Document doc = db.parse(inputStream);
    NodeList questionNodes = doc.getElementsByTagName("item");
    int count = questionNodes.getLength();
    for (int i = 0; i < count; i++) {
        Element questionNode = (Element) questionNodes.item(i);
        DOMSource source = new DOMSource(questionNode);
        final Question question = (Question) JAXB.unmarshal(source, Question.class);

        answer.add(question);
    }
    return answer;

}

Listing 7-9.Combining XML Parsing and JAXB

这种方法与清单 7-8 中使用的方法的唯一区别是对单个问题的解析。我们使用 JAXB 中的 unmarshal 方法,而不是使用 DOM 解析来获取各个问题的特定字段。JAXB 规范允许大量的灵活性和配置,而JAXB.unmarshal方法只是一种方便的方法。但是,在很多情况下,这种方法就足够了。JAXB.unmarshal方法有两个参数:输入源和作为转换结果的类。

我们希望将 XML 源转换成我们的Question类的实例,但是 JAXB 框架如何知道如何映射字段呢?在许多情况下,映射很简单,不需要修改现有的代码,但是在其他情况下,映射稍微复杂一些。很好,有一个完整的带注释的包,我们可以用它来帮助 JAXB 确定 XML 和 Java 对象之间的转换。

为了让清单 7-9 中的代码工作,我们对Question类做了一些小的修改。清单 7-10 显示了Question类的新代码。

package projavafx;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;

@XmlAccessorType(XmlAccessType.FIELD)
public class Question {

    private String owner;
    @XmlElement(name = "title")
    private String question;
    @XmlElement(name = "creation_date")
    private long timestamp;

    public Question(String o, String q, long t) {
        this.owner = o;
        this.question = q;
        this.timestamp = t;
    }

    public Question() {
    }

    /**
     * @return the owner
     */
    public String getOwner() {
        return owner;
    }

    /**
     * @param owner the owner to set
     */
    public void setOwner(String owner) {
        this.owner = owner;
    }

    /**
     * @return the question
     */
    public String getQuestion() {
        return question;
    }

    /**
     * @param question the question to set
     */
    public void setQuestion(String question) {
        this.question = question;
    }

    /**
     * @return the timestamp
     */
    public long getTimestamp() {
        return timestamp;
    }

    /**
     * @param timestamp the timestamp to set
     */
    public void setTimestamp(long timestamp) {
        this.timestamp = timestamp;
    }

}

Listing 7-10.
Question Class with JAXB Annotations

我们向原始的Question类添加了三个注释。首先,我们用

@XmlAccessorType(XmlAccessType.FIELD)

这个注释告诉 JAXB 框架将 XML 数据映射到这个类的字段上,而不是映射到这个类的JavaBean属性(getter/setter 方法)上。第二个和第三个注释被添加到question字段和timeStamp字段:

@XmlElement(name = "title")
private String question;
@XmlElement(name = "creation_date")
private long timestamp;

这表明question字段对应于名为“title”的 XML 元素,时间戳字段对应于名为"creation_date"的 XML 元素。事实上,如果我们查看清单 7-6 ,它显示问题在名为“标题”的元素中,时间戳在名为“creation_date"的元素中。我们必须指示 JAXB 运行时用我们的时间戳字段映射这个元素,这就是我们对@XmlElement注释所做的。

使用 JAXB 注释可以很容易地将 XML 问题元素转换成单独的Question实例,但是在我们的主类中仍然有一些手工的 XML 处理。但是,我们可以完全去掉手动的XMLParsing,将整个 XML 响应转换成一个 Java 对象。这样做,getObservableList()方法变得非常简单,如清单 7-11 所示。

ObservableList<Question> getObservableList() {
    InputStream inputStream = this.getClass().getResourceAsStream("/stackoverflow.xml");
    QuestionResponse response = JAXB.unmarshal(inputStream, QuestionResponse.class);
    return FXCollections.observableArrayList(response.getItem());
}
Listing 7-11.Parsing Incoming XML Data Using JAXB

在这个例子中,我们使用 JAXB 将 XML 响应转换成一个QuestionResponse实例,然后通过这个QuestionResponse实例获得问题。注意,按照方法签名的要求,我们将问题从常规的List对象转换为ObservableList对象。稍后,我们将展示一个无需进行额外转换的示例。

QuestionResponse类有两个目标:将 XML 响应映射到 Java 对象上,并使问题项作为Question实例的List可用。这是通过清单 7-12 中的代码实现的。

package projavafx;

import java.util.List;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement(name="items")
@XmlAccessorType(XmlAccessType.FIELD)
public class QuestionResponse {

    private List<Question> questions;

    public List<Question> getQuestions() {
        return questions;
    }

    public void setQuestions(List<Question> questions) {
        this.questions = questions;
    }
}

Listing 7-12.

QuestionResponse Class

, Enabling Conversion Between XML Response and Java Objects

QuestionResponse类本身有两个注释。我们已经讨论了以下内容:

@XmlAccessorType(XmlAccessType.FIELD)

这个注释表明这个类对应于 XML 结构中的一个名为"items"的根对象。

@XmlRootElement(name="items")

这确实对应于我们在清单 7-6 中创建的 XML 响应的语法。

前面的示例展示了如何使用 Java 2 平台标准版中的现有技术从 web 服务获取数据,并将这些数据注入 JavaFX 控件中。我们现在修改示例代码,以利用 JavaFX 平台的一些特定特性。

异步处理

到目前为止,示例的一个主要问题是它们在数据检索和解析过程中阻塞了 UI。在许多现实情况下,这是不可接受的。由于网络或服务器问题,对外部 web 服务的调用可能会比预期时间长。即使外部调用很快,暂时没有响应的 UI 也会降低应用程序的整体质量。

幸运的是,JavaFX 平台允许并发和异步任务。任务、工人和服务的概念在第七章中讨论过。在这一节中,我们将展示如何在访问 web 服务时利用javafx.concurrent包。我们还利用了这样一个事实,即ListView监视包含其项目的ObservableList

基本思想是,当创建ListView时,我们立即返回一个空的ObservableList,同时在后台线程中检索数据。一旦我们检索并解析了数据,我们将它添加到ObservableList,结果将立即在ListView中可见。

清单 7-13 显示了这个例子的主类。我们从清单 7-7 中的代码开始,在那里我们使用一个对栈交换 API 的 REST 请求获得了 JSON 格式的问题。不过,经过一些小的修改,我们也可以使用 XML 响应。

package projavafx;

import java.io.IOException;
import javafx.application.Application;
import static javafx.application.Application.launch;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.concurrent.Service;
import javafx.concurrent.Worker;
import javafx.scene.Scene;
import javafx.scene.control.ListView;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

public class StackOverflow4 extends Application {

    @Override
    public void start(Stage primaryStage) throws IOException {
        ListView<Question> listView = new ListView<>();
        listView.setItems(getObservableList());
        listView.setCellFactory(l -> new QuestionCell());
        StackPane root = new StackPane();
        root.getChildren().add(listView);

        Scene scene = new Scene(root, 500, 300);

        primaryStage.setTitle("StackOverflow List");
        primaryStage.setScene(scene);
        primaryStage.show();
        System.out.println (« Done with the setup ») ;
    }

    ObservableList<Question> getObservableList() throws IOException {
        String url = "http://api.stackexchange.com/2.2/search?order=desc&sort=activity&tagged=javafx&site=stackoverflow";
        Service<ObservableList<Question>> service = new QuestionRetrievalService(url);

        ObservableList<Question> answer = FXCollections.observableArrayList();
        service.stateProperty().addListener(new InvalidationListener() {

            @Override
            public void invalidated(Observable observable) {
                System.out.println("value is now "+service.getState());
                if (service.getState().equals(Worker.State.SUCCEEDED)) {
                    answer.addAll(service.getValue());
                }
            }
        });
        System.out.println("START SERVICE = "+service.getTitle());
        service.start();
        return answer;
    }

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        launch(args);
    }
}

Listing 7-13.Use a Background Thread for Retrieving Question 
ListView

main 方法与前面的例子没有什么不同,除了添加了一个System.out日志消息,它将在我们完成设置时打印一条消息。

getObservableList方法将首先创建一个ObservableList的实例,该实例在方法完成时返回。最初,这个实例将是一个空列表。在这个方法中,创建了一个QuestionRetrievalService的实例,并在构造器中传递了 REST 端点的位置。扩展了javafx.concurrent.ServiceQuestionRetrievalService被启动,我们监听服务的State的变化。当服务的状态变为State.SUCCEEDED时,我们将检索到的问题添加到ObservableList中。请注意,在QuestionRetrievalService实例的每个状态变化时,我们都会向System.out记录一条消息。

我们现在仔细看看QuestionRetrievalService以理解它如何启动一个新线程,以及它如何确保使用 JavaFX 线程将检索到的问题添加到ListView控件中。QuestionRetrievalService的代码如清单 7-14 所示。

package projavafx;

import java.net.URL;
import java.util.zip.GZIPInputStream;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import javax.json.Json;
import javax.json.JsonArray;
import javax.json.JsonNumber;
import javax.json.JsonObject;
import javax.json.JsonReader;
import javax.json.JsonString;
import javax.json.JsonValue;

public class QuestionRetrievalService extends Service<ObservableList<Question>> {

    private String loc;

    public QuestionRetrievalService(String loc) {
        this.loc = loc;
    }

    @Override
    protected Task<ObservableList<Question>> createTask() {
        return new Task<ObservableList<Question>>() {

            @Override
            protected ObservableList<Question> call() throws Exception {
                URL host = new URL(loc);
                JsonReader jr = Json.createReader(new GZIPInputStream(host.openConnection().getInputStream()));

                JsonObject jsonObject = jr.readObject();
                JsonArray jsonArray = jsonObject.getJsonArray("items");
                ObservableList<Question> answer = FXCollections.observableArrayList();

                jsonArray.iterator().forEachRemaining((JsonValue e) -> {
                    JsonObject obj = (JsonObject) e;
                    JsonString name = obj.getJsonObject("owner").getJsonString("display_name");
                    JsonString quest = obj.getJsonString("title");
                    JsonNumber jsonNumber = obj.getJsonNumber("creation_date");
                    Question q = new Question(name.getString(), quest.getString(), jsonNumber.longValue() * 1000);
                    System.out.println("Adding question "+q);
                    answer.add(q);
                });
                return answer;
            }
        };
    }

}

Listing 7-14.

QuestionRetrievalService

QuestionRetrievalService扩展了Service,因此必须实现一个createTask方法。当Service启动时,该任务在一个单独的线程中执行。QuestionRetrievalService上的createTask方法创建一个新的Task并返回它。这种方法的特征,

Task<ObservableList<Question>> createTask(),

确保Task创建一个ObservableList问题。泛型类型参数ObservableList<Question>Service声明中的类型参数相同。因此,ServicegetValue()方法也将返回Question s 的ObservableList

事实上,下面的代码片段说明了questionRetrievalService.getValue()应该返回一个ObservableList<Question>

ObservableList<Question> answer = FXCollections.observableArrayList();
...
    if (now == State.SUCCEEDED) {
        answer.addAll(service.getValue());
    }

我们在QuestionRetrievalService中创建的Task实例必须实现 call 方法。这个方法实际上在做前面例子中的getObservableList方法正在做的事情:检索数据并解析它们。

虽然Service(由createTask创建的Task)中的实际工作是在后台线程中完成的,但是Service上的所有方法,包括getValue()调用,都应该从 JavaFX 线程中访问。内部实现确保对Service中可用属性的所有更改都在 JavaFX 应用程序线程上执行。

运行该示例会给出与运行上一个示例完全相同的视觉输出。然而,为了清楚起见,我们添加了一些System.out消息。如果我们运行该示例,可以在控制台上看到以下消息。

State of service is READY
State of service is SCHEDULED
Done with the setup
State of service is RUNNING
Adding question projavafx.Question@482fb3d5
...
Adding question projavafx.Question@2d622bf7
State of service is SUCCEEDED

这表明getObservableList方法在问题被获取并添加到列表之前返回。

Note

理论上,您可能会注意到一种不同的行为,因为后台线程可能会在其他初始化完成之前完成。然而,在实践中,当涉及网络调用时,这种行为是不太可能的。

将 Web 服务数据转换为TableView

到目前为止,我们所有的例子都显示了 a ListView中的问题。ListView是一个简单而强大的 JavaFX 控件,然而,在某些情况下还有其他控件更适合呈现信息。

我们也可以在一个TableView中显示Question数据,这就是我们在本节中所做的。数据的检索和解析与上一个示例中的一样。然而,我们现在使用一个TableView来呈现数据,并且我们必须定义我们想要看到的列。对于每一列,我们必须指定数据的来源。清单 7-15 中的代码显示了示例中使用的启动方法。

@Override
public void start(Stage primaryStage) throws IOException {
    TableView<Question> tableView = new TableView<>();
    tableView.setItems(getObservableList());
    TableColumn<Question, String> dateColumn = new TableColumn<>("Date");
    TableColumn<Question, String> ownerColumn = new TableColumn<>("Owner");
    TableColumn<Question, String> questionColumn = new TableColumn<>("Question");
    dateColumn.setCellValueFactory((CellDataFeatures<Question, String> cdf) -> {
        Question q = cdf.getValue();
        return new SimpleStringProperty(getTimeStampString(q.getTimestamp()));
    });
    ownerColumn.setCellValueFactory((CellDataFeatures<Question, String> cdf) -> {
        Question q = cdf.getValue();
        return new SimpleStringProperty(q.getOwner());
    });
    questionColumn.setCellValueFactory((CellDataFeatures<Question, String> cdf) -> {
        Question q = cdf.getValue();
        return new SimpleStringProperty(q.getQuestion());
    });
    questionColumn.setPrefWidth(350);
    tableView.getColumns().addAll(dateColumn, ownerColumn, questionColumn);
    StackPane root = new StackPane();
    root.getChildren().add(tableView);

    Scene scene = new Scene(root, 500, 300);

    primaryStage.setTitle("StackOverflow Table");
    primaryStage.setScene(scene);
    primaryStage.show();
}

Listing 7-15.The Start Method

in the Application Rendering Questions in a 
TableView

显然,这个例子比显示ListView的例子需要更多的代码。设置一个表稍微复杂一些,因为涉及到不同的列。设置ListView的内容和设置TableView的内容没有太大区别。这是通过做来实现的

tableView.setItems(getObservableList());

其中的getObservableList()方法与上一个例子中的实现相同。注意,我们也可以使用方便的构造器

TableView<Question> tableView = new TableView<>(getObservableList());

当使用一个TableView时,我们必须定义若干个TableColumns。这是在下面的代码片段中完成的。

TableColumn<Question, String> dateColumn = new TableColumn<>("Date");
TableColumn<Question, String> ownerColumn = new TableColumn<>("Owner");
TableColumn<Question, String> questionColumn = new TableColumn<>("Question");

使用TableColumn构造器,我们创建一个标题为“Date”的TableColumn,一个标题为“Owner”,第三个标题为“Question”Generics <Question, String>表示一行中的每个条目代表一个Question,指定列中的单个单元格属于String类型。

接下来,我们创建的TableColumn的实例需要知道它们应该呈现什么数据。这是使用CellFactories完成的,如下面的代码片段所示。

dateColumn.setCellValueFactory((CellDataFeatures<Question, String> cdf) -> {
    Question q = cdf.getValue();
    return new SimpleStringProperty(getTimeStampString(q.getTimestamp()));
});

setCellValueFactory方法的详细描述超出了本章的范围。我们鼓励读者在处理表格的时候看看TableViewTableColumn类的 Javadoc。Javadoc 解释说,我们必须用 call 方法指定一个Callback类,该方法返回一个包含特定单元格内容的ObservableValue。幸运的是,我们可以为此使用 lambda 表达式。

我们在这一行中显示的问题可以通过在这个 lambda 表达式中作为单个参数传递的CellDataFeatures实例获得。因为我们想要显示时间戳,所以我们返回一个SimpleStringProperty,它的内容被设置为指定的Question的时间戳。

同样的技术必须用于另一个TableColumns(包含所有者和适用的Question对象中包含的问题)。

最后,我们必须将列添加到TableView:

tableView.getColumns().addAll(dateColumn, ownerColumn, questionColumn);

运行此示例会产生如图 7-7 所示的可视化输出。

A323806_4_En_7_Fig7_HTML.jpg

图 7-7。

Using a TableView for rendering questions

对于一个简单的表,这个示例需要大量的样板代码,但幸运的是,JavaFX 平台包含了一种减少代码量的方法。为每一列手动设置CellValueFactory实例很麻烦,但是我们可以使用另一种方法来完成,通过使用 JavaFX 属性。清单 7-16 包含了主类的 start 方法的修改版本,其中我们利用了 JavaFX 属性的概念。

@Override
public void start(Stage primaryStage) throws IOException {
    TableView<Question> tableView = new TableView<>();
    tableView.setItems(getObservableList());
    TableColumn<Question, String> dateColumn = new TableColumn<>("Date");
    TableColumn<Question, String> ownerColumn = new TableColumn<>("Owner");
    TableColumn<Question, String> questionColumn = new TableColumn<>("Question");
    dateColumn.setCellValueFactory(new PropertyValueFactory<>("timestampString"));
    ownerColumn.setCellValueFactory(new PropertyValueFactory<>("owner"));
    questionColumn.setCellValueFactory(new PropertyValueFactory<>("question"));
    questionColumn.setPrefWidth(350);
    tableView.getColumns().addAll(dateColumn, ownerColumn, questionColumn);
    StackPane root = new StackPane();
    root.getChildren().add(tableView);

    Scene scene = new Scene(root, 500, 300);

    primaryStage.setTitle("StackOverflow Table");
    primaryStage.setScene(scene);
    primaryStage.show();
}

Listing 7-16.Rendering Data in Columns Based on JavaFX Properties

这段代码显然比前一个示例中的代码要短。我们实际上替换了

dateColumn.setCellValueFactory((CellDataFeatures<Question, String> cdf) -> {
    Question q = cdf.getValue();
    return new SimpleStringProperty(getTimeStampString(q.getTimestamp()));
});

经过

dateColumn.setCellValueFactory(new PropertyValueFactory<>("timestampString"));

这同样适用于ownerColumnquestionColumn

我们使用javafx.scene.control.cell.PropertyValueFactory<S,T>(String name)的实例来定义哪些特定数据应该呈现在哪个单元格中。

PropertyValueFactory搜索具有指定名称的 JavaFX 属性,并在被调用时返回该属性的ObservableValue。如果找不到具有这样名称的属性,Javadoc 会这样说:

In this example, the "firstName" string is used as a reference to the firstNameProperty () method assumed in the Person class type (the class type of TableView items list). In addition, the method must return a Property instance. If you find a way to meet these requirements, then fill TableCell with this observable value. In addition, TableView will automatically add an observer to the return value, so that TableView will observe any triggered changes, which will cause the cell to be updated immediately. If there is no method matching this pattern, there is failure support for trying to call the get < attribute > () or the is < attribute > () (that is, getFirstName() or isFirstName() in the previous example). If there is a method that matches the pattern, the value returned from this method is wrapped in a ReadOnlyObjectWrapper and returned to TableCell. However, in this case, this means that TableCell will not be able to observe the change of ObservableValue (this is the case with the first method).

由此可见,JavaFX 属性是在TableView中呈现信息的首选方式。到目前为止,我们使用带有 JavaBean getter 和 setter 方法的 POJO Question类作为显示在ListViewTableView中的值对象。

尽管前面的例子在不使用 JavaFX 属性的情况下也能工作,如 Javadoc 所述,我们现在修改Question类,使用 JavaFX 属性来表示所有者信息。timeStamp和文本字段也可以修改为使用 JavaFX 属性,但是混合示例表明 Javadoc 中描述的失败场景确实有效。修改后的Question级如清单 7-17 所示。

package projavafx;

import java.text.SimpleDateFormat;
import java.util.Date;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;

@XmlAccessorType(XmlAccessType.PROPERTY)
public class Question {

    static final SimpleDateFormat sdf = new SimpleDateFormat ("dd-MM-YY");

    private StringProperty ownerProperty = new SimpleStringProperty();
    private String question;
    private long timestamp;

    public Question (String o, String q, long t) {
        this.ownerProperty.set(o);
        this.question = q;
        this.timestamp = t;
    }

    public String getOwner() {
        return ownerProperty.get();
    }

    public void setOwner(String owner) {
        this.ownerProperty.set(owner);
    }

    public String getQuestion() {
        return question;
    }

    public void setQuestion(String question) {
        this.question = question;
    }

    public long getTimestamp() {
        return timestamp;
    }

    public void setTimestamp(long timestamp) {
        this.timestamp = timestamp;
    }

    public String getTimestampString() {
        return sdf.format(new Date(timestamp));
    }

}

Listing 7-17.Implementation of Question Class Using JavaFX Properties

for the Author Field

关于这个实现,有一些事情需要注意。ownerProperty遵循标准的 JavaFX 约定,如第三章所述。

除了引入 JavaFX 属性之外,Question类的实现还有另一个重大变化。该类现在用

@XmlAccessorType(XmlAccessType.PROPERTY)

这样做的原因是,在这样做的时候,setter 方法将被JAXB.unmarshal方法调用,当它用一些特定的信息创建一个Question的实例时。既然我们正在使用 JavaFX 属性而不是基本类型,这是必需的。JAXB 框架可以很容易地将 XML 元素“owner”的值赋给 owner String字段,但是默认情况下它不能将值赋给 JavaFX Property对象。

通过使用XmlAccessType.PROPERTY,JAXB 框架将调用setOwner(String v)方法,向setOwner方法提供 XML 元素的值。此方法的实现

ownerProperty.set(owner);

然后将更新随后被TableColumnTableView使用的 JavaFX 属性。

Question实现中的另一个重要变化是我们添加了一个方法

String getTimestampString()

该方法将以人类可读的格式返回时间戳。您可能已经注意到,在清单 7-16 中,我们将dateColumnCellValueFactory设置为指向"timestampString"而不是"timeStamp"PropertyValueFactory:

dateColumn.setCellValueFactory(new PropertyValueFactory<>("timestampString"));

这样做的原因是getTimestamp()方法返回一个 long 类型,而我们更喜欢以一种可读性更好的格式来显示时间戳。通过添加一个getTimestampString()方法并将CellValueFactory指向该方法,该列中单元格的内容将成为可读的时间指示。

到目前为止,我们在本章中展示的例子表明,Java 平台标准版已经包含了许多在访问 web 服务时非常有用的 API。我们还展示了如何使用 JavaFX 并发框架、ObservableList模式、JavaFX 属性和PropertyValueFactory类来增强调用 web 服务和在 JavaFX 控件中呈现数据之间的流程。

虽然示例中没有涉及火箭科学,但是额外的需求会使事情变得更加复杂,并且需要更多的样板代码。幸运的是,JavaFX 社区中已经出现了许多倡议,目标是让我们的生活变得更容易。

使用外部库

到目前为止,我们所有的例子都不需要任何额外的外部库。Java Platform,Standard Edition 和 JavaFX platform 提供了一个很好的环境,可以用来访问 web 服务。在本节中,我们使用两个外部库,并展示它们如何使访问 web 服务变得更容易。

胶子连接

本书之前的版本提到 DataFX 是一个外部库,它提供了一个 JavaFX API 来连接移除端点。DataFX 数据服务的开发已经并入了 Gluon Connect 的开发。

Gluon Connect 是一个开源的、BSD 许可的框架,由 Gluon 管理。胶子连接产品在 http://gluonhq.com/products/mobile/connect 描述,代码在 Bitbucket ( https://bitucket.org/gluon-oss/gluon-connect )提供。

根据 Bitbucket 的说法:“Gluon Connect 是一个客户端库,它简化了将任何来源和格式的数据绑定到 JavaFX UI 控件的过程。它的工作原理是从数据源中检索数据,并将数据从特定格式转换成 JavaFX 可观察列表和可观察对象,这些列表和对象可以直接在 JavaFX UI 控件中使用。它旨在允许开发人员轻松添加对自定义数据源和数据格式的支持。”

Gluon Connect 的一个主要优势是它也支持移动平台。我们将在下一章讨论移动设备上的 JavaFX。

在下一个例子中,我们将 Gluon Connect 与我们的栈交换例子集成在一起。同样,唯一的变化是在getObservableList方法中,但是为了清楚起见,我们在清单 7-18 中显示了整个主类。

import java.io.IOException;
import javafx.application.Application;
import static javafx.application.Application.launch;
import javafx.collections.ObservableList;
import javafx.concurrent.Worker;
import javafx.scene.Scene;
import javafx.scene.control.ListView;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import org.datafx.provider.ListDataProvider;
import org.datafx.provider.ListDataProviderBuilder;
import org.datafx.io.RestSource;
import org.datafx.io.RestSourceBuilder;
import org.datafx.io.converter.InputStreamConverter;
import org.datafx.io.converter.JsonConverter;

public class StackOverflowGluonConnect extends Application {

    @Override
    public void start(Stage primaryStage) throws IOException {
        ListView<Question> listView = new ListView<>();
        listView.setItems(getObservableList());
        listView.setCellFactory(l -> new QuestionCell());
        StackPane root = new StackPane();
        root.getChildren().add(listView);

        Scene scene = new Scene(root, 500, 300);

        primaryStage.setTitle("StackOverflow List");
        primaryStage.setScene(scene);
        primaryStage.show();
        System.out.println ("Done with the setup");
    }

    ObservableList<Question> getObservableList() throws IOException {
        InputStreamConverter converter = new JsonConverter("item", Question.class);

        RestSource restSource = RestSourceBuilder.create()
                .converter(converter)
                .host("http://api.stackexchange.com")
                .path("2.2").path("search")
                .queryParam("order", "desc")
                .queryParam("sort", "activity")
                .queryParam("tagged", "javafx")
                .queryParam("site", "stackoverflow").build();
        ListDataProvider<Question> ldp = ListDataProviderBuilder.create()
                .dataReader(restSource)
                .build();
        Worker<ObservableList<Question>> retrieve = ldp.retrieve();
        return retrieve.getValue();
    }

public static void main(String[] args) {
        launch(args);
    }
}

Listing 7-18.Obtaining Questions Using Gluon Connect

相关的部分,getObservableList方法的实现非常简单。

我们首先使用构建器模式创建一个RestClient。我们通过添加请求方法、主机名和路径来提供关于 REST 端点的信息。通过为查询中需要使用的每个查询参数调用queryParam()方法来提供查询参数。

接下来,我们调用 DataProvider 上的一个便利方法,并要求它检索属于我们刚刚创建的 RestClient 的列表。我们为列表中的不同实体提供目标类。这样,DataProvider 将尝试将传入的数据映射到我们指定的目标。Gluon Connect 将识别 JSON 和 XML 格式的数据,并且可以处理压缩的输入流。

后者在堆栈溢出示例中是必需的,因为堆栈溢出端点以压缩格式返回其数据。手动检查数据是否被压缩需要大量样板代码。

调用DataProvider.retrieveList的结果是GluonObservableList的一个实例,一个扩展标准 JavaFX ObservableList的类。GluonObservableListObservableList增加了一些属性,使其更适合远程数据检索的特定领域。例如,有一个状态属性可以具有下列值之一:

  • 准备好的
  • 运转
  • 不成功的
  • 成功
  • 离开的

如果数据检索失败,state 属性将设置为 FAILED。另一个属性 exception 属性将包含包装的异常,该异常指示数据检索失败的原因。

调用DataProvider.retrieveList方法启动一个异步服务。我们可以立即将结果对象返回给我们的可视控件,而不是等待结果。Gluon Connect 框架将在读取和解析输入数据的同时更新结果对象。对于大块数据,这非常有用,因为这种方法允许开发人员在其他部分仍在输入或仍在处理时呈现部分数据。

Gluon Connect 是一个开源框架,在业务友好的 BSD 许可下获得许可,因此您可以在开源和专有(商业)软件中自由使用它。它已经包含了许多增强功能,删除了样板代码,使它更适合客户端到服务器的通信。

使用 Gluon Connect,您可以轻松地从各种来源检索数据,包括文件和 REST 端点。

胶子也为此提供了商业扩展。位于 https://gluonhq.com/products/cloudlink 的 Gluon CloudLink 允许开发人员在 JavaFX 客户端应用程序和许多云提供商和后端服务之间同步数据和功能。Gluon CloudLink 中的数据服务组件允许内容的实时双向同步。这样,Java 企业开发人员可以在一些后端代码中修改 Java 类,结果将立即在 JavaFX 客户端的 UI 组件中可见。

Gluon Connect 包含对 Gluon CloudLink 的支持,在数据来自 Gluon CloudLink 的情况下,您在前面的示例中使用的 API 可以重用。

JAX-RS 啊

Java 企业版 7 的发布包括 JAX-RS 2.0 的发布。该规范不仅定义了 Java 开发人员如何提供 REST 端点,还定义了 Java 代码如何消费 REST 端点。

在下一个例子中,我们修改清单 7-14 的QuestionRetrievalService来使用 JAX-RS API。这显示在清单 7-19 中。

package projavafx.jerseystackoverflow;

import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.MediaType;

public class QuestionRetrievalService extends Service<ObservableList<Question>> {

    private String loc;
    private String path;
    private String search;

    public QuestionRetrievalService(String loc, String path, String search) {
        this.loc = loc;
        this.path = path;
        this.search = search;
    }

    @Override
    protected Task<ObservableList<Question>> createTask() {
        return new Task<ObservableList<Question>>() {
            @Override
            protected ObservableList<Question> call() throws Exception {
                Client client = ClientBuilder.newClient();
                WebTarget target = client.target(loc).path(path).queryParam("tagged", search).queryParam("site", "stackoverflow");
                QuestionResponse response = target.request(MediaType.APPLICATION_JSON).get(QuestionResponse.class);
                return FXCollections.observableArrayList(response.getItem());

            }
        };
    }

}

Listing 7-19.Using JAX-RS for Retrieving Questions

为了展示 JAX-RS 的一个很好的工具,我们稍微修改了一下QuestionRetrievalService的构造器,以接受三个参数:

public QuestionRetrievalService(String host, String path, String search);

这是因为 JAX-RS 允许我们使用Builder模式来构建 REST 资源,允许区分主机名、路径、查询参数和其他内容。

因此,我们必须对清单 7-13 稍作修改:

String url = "http://api.stackexchange.com/2.2/search?order=desc&sort=activity&tagged=javafx&site=stackoverflow";
Service<ObservableList<Question>> service = new QuestionRetrievalService(url);

被替换为

String url = "http://api.stackexchange.com/";
String path = "2.2/search";
String search = "javafx";
Service<ObservableList<Question>> service = new QuestionRetrievalService(url, path, search);

主机名、路径和搜索参数用于创建 JAX-RS WebTarget:

Client client = ClientBuilder.newClient();
WebTarget target = client.target(loc).path(path).queryParam("tagged", search)
        .queryParam("site", "stackoverflow");

在这个WebResource上,我们可以调用 request 方法来执行请求,后面是get(Class clazz)方法,并提供一个类参数。REST 调用的结果将被解析成所提供的类的一个实例,这也是我们在清单 7-11 的例子中使用 JAXB 所做的。

QuestionResponse response = target.request(MediaType.APPLICATION_JSON).get(QuestionResponse.class);

响应现在包含一个问题列表,我们可以使用与清单 7-4 中完全相同的代码来呈现问题。

摘要

在本章中,我们简要解释了集成 JavaFX 应用程序和企业应用程序的两个选项。我们展示了许多通过 web 服务检索数据的技术,还展示了如何在典型的 JavaFX 控件(如ListViewTableView)中呈现数据。

我们使用了第三方工具来简化检索、解析和呈现数据的过程。我们演示了一些与远程 web 服务相关的特定于 JavaFX 的问题(例如,更新 UI 应该发生在 JavaFX 应用程序线程上)。

重要的是要认识到 JavaFX 客户端应用程序和 web 服务之间的解耦允许很大程度的自由度。处理 web 服务有不同的工具和技术,我们鼓励开发人员在 JavaFX 应用程序中使用他们喜欢的工具。

posted @ 2024-08-06 16:34  绝不原创的飞龙  阅读(42)  评论(0编辑  收藏  举报