JavaFX9-高级教程-一-

282 阅读1小时+

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 的更多信息,可以参考以下资源。