JavaFX17-学习手册-全-

JavaFX17 学习手册(全)

原文:Learn JavaFX 17

协议:CC BY-NC-SA 4.0

一、入门指南

在本章中,您将学习:

  • JavaFX 是什么

  • JavaFX 的历史

  • 如何设置 Eclipse IDE 以使用 JavaFX 应用程序,以及如何编写您的第一个 JavaFX 应用程序

  • 如何向 JavaFX 应用程序传递参数

  • 如何启动 JavaFX 应用程序

  • JavaFX 应用程序的生命周期

  • 如何终止 JavaFX 应用程序

JavaFX 是什么?

JavaFX 是一个基于 Java 的开源框架,用于开发富客户端应用程序。它可以与市场上的其他框架(如 Adobe AIR 和 Microsoft Blazor)相媲美。在 Java *台的图形用户界面(GUI)开发技术领域,JavaFX 也被视为 Swing 的继承者。JavaFX 库作为公共 Java 应用程序编程接口(API)提供。JavaFX 包含几个特性,这些特性使它成为开发富客户端应用程序的首选:

  • JavaFX 是用 Java 编写的,这使您能够利用所有 Java 特性,如多线程、泛型和 lambda 表达式。您可以使用自己选择的任何 Java IDE(如 NetBeans 或 Eclipse)来创作、编译、运行、调试和打包您的 JavaFX 应用程序。

  • JavaFX 通过其库支持数据绑定。

  • JavaFX 代码也可以使用任何 Java 虚拟机(JVM)支持的脚本语言编写,比如 Kotlin、Groovy 和 Scala。

  • JavaFX 提供了两种构建用户界面(UI)的方法:使用 Java 代码和使用 FXML。FXML 是一种基于 XML 的可脚本化标记语言,用于以声明方式定义 UI。Gluon 公司提供了一个名为 Scene Builder 的工具,这是一个 FXML 的可视化编辑器。

  • JavaFX 提供了丰富的多媒体支持,如播放音频和视频。它利用了*台上可用的编解码器。

  • JavaFX 允许您在应用程序中嵌入 web 内容。

  • JavaFX 为应用效果和动画提供了现成的支持,这对开发游戏应用程序非常重要。您可以通过编写几行代码来实现复杂的动画。

JavaFX API 背后有许多组件,可以利用 Java 本地库和可用的硬件和软件。JavaFX 组件如图 1-1 所示。

img/336502_2_En_1_Fig1_HTML.png

图 1-1

JavaFX *台的组件

JavaFX 中的 GUI 被构造成一个场景图。场景图是视觉元素的集合,称为节点,以分层方式排列。使用公共 JavaFX API 构建场景图。场景图中的节点可以处理用户输入和用户手势。它们可以有效果、转换和状态。场景图中的节点类型包括简单的 UI 控件,如按钮、文本字段、二维(2D)和三维(3D)形状、图像、媒体(音频和视频)、web 内容和图表。

Prism 是一个硬件加速的图形管道,用于渲染场景图形。如果硬件加速渲染在*台上不可用,则使用 Java 2D 作为后备渲染机制。例如,在使用 Java 2D 进行渲染之前,它将尝试在 Windows 上使用 DirectX,在 Mac、Linux 和嵌入式*台上使用 OpenGL。

Glass Windowing Toolkit 使用本地操作系统提供图形和窗口服务,如 windows 和计时器。该工具包还负责管理事件队列。在 JavaFX 中,事件队列由一个名为 JavaFX 应用线程的操作系统级线程管理。所有用户输入事件都在 JavaFX 应用程序线程上调度。JavaFX 要求只能在 JavaFX 应用程序线程上修改实时场景图形。

Prism 使用一个单独的线程,而不是 JavaFX 应用程序线程来进行渲染。它通过在处理下一帧的同时渲染一帧来加速处理过程。当场景图形被修改时,例如,通过在文本字段中输入一些文本,Prism 需要重新渲染场景图形。使用称为脉冲事件的事件来实现场景图形与 Prism 的同步。当场景图形被修改并且需要重新渲染时,一个脉冲事件在 JavaFX 应用程序线程上排队。脉冲事件表示场景图形与 Prism 中的渲染层不同步,应该渲染 Prism 级别的最新帧。脉冲事件被限制在每秒最大 60 帧。

媒体引擎负责在 JavaFX 中提供媒体支持,例如,回放音频和视频。它利用了*台上可用的编解码器。媒体引擎使用单独的线程处理媒体帧,并使用 JavaFX 应用程序线程将帧与场景图形同步。媒体引擎基于 GStreamer ,这是一个开源的多媒体框架。

web 引擎负责处理嵌入在场景图中的 web 内容(HTML)。Prism 负责呈现 web 内容。web 引擎基于 WebKit ,这是一个开源的 web 浏览器引擎。支持 HTML5、级联样式表(CSS)、JavaScript 和文档对象模型(DOM)。

Quantum toolkit 是对 Prism、Glass、media engine 和 web engine 等底层组件的抽象。它还有助于低层组件之间的协调。

Note

在本书中,假设您已经掌握了 Java 编程语言的中级知识,包括 lambda 表达式和新的 Time API(从 Java 8 开始)。

JavaFX 的历史

JavaFX 最初是由 Chris Oliver 在 SeeBeyond 开发的,它被称为 F3 (Form Follows Function)。F3 是一种易于开发 GUI 应用程序的 Java 脚本语言。它提供了声明性语法、静态类型、类型推断、数据绑定、动画、2D 图形和 Swing 组件。SeeBeyond 被 Sun Microsystems 收购,F3 于 2007 年更名为 JavaFX。甲骨文在 2010 年收购了太阳微系统公司。甲骨文随后在 2013 年开源了 JavaFX。

JavaFX 的第一个版本发布于 2008 年第四季度。版本号从 2.2 跃升到 8.0。从 Java 8 开始,Java SE 和 JavaFX 的版本号将是相同的。Java SE 和 JavaFX 的主要版本也将同时发布。JavaFX 的当前版本是 17.0 版。从 Java SE 11 开始,JavaFX 不再是 Java SE 运行时库的一部分。在 Java 11 中,您需要下载并包含 JavaFX 库来编译和运行您的 JavaFX 程序。表 1-1 包含 JavaFX 的发布列表。

表 1-1

JavaFX 版本

|

出厂日期

|

版本

|

评论

2008 年第四季度 JavaFX 1.0 这是 JavaFX 的最初版本。它使用一种称为 JavaFX Script 的声明语言来编写 JavaFX 代码。
Q1,2009 年 JavaFX 1.1 引入了对 JavaFX Mobile 的支持。
Q2,2009 年 JavaFX 1.2
Q2,2010 年 JavaFX 1.3
2010 年第三季度 JavaFX 1.3.1
2011 年第四季度 java fx 2.0 不再支持 JavaFX 脚本。它使用 Java 语言编写 JavaFX 代码。对 JavaFX Mobile 的支持已取消。
2012 年,Q2 JavaFX 2.1 引入了对 Mac OS 桌面版的支持。
2012 年第三季度 JavaFX 2.2
2014 年,Q1 JavaFX 8.0 JavaFX 版本从 2.2 跳到了 8.0。JavaFX 和 Java SE 版本将从 Java 8 开始匹配。
2015 年,Q2 JavaFX 9.0 公开的一些内部 API,JEP253。
2018 年第三季度 JavaFX 11.0.3 JavaFX 不再是 Oracle Java JDK 的一部分。JavaFX 现在是一个可下载的开源模块,由 Gluon 公司提供。作为端口增加了对手持设备和其他嵌入式设备的支持。
2019 年,Q1 JavaFX 12.0.1 错误修复和一些增强。
2019 年第三季度 JavaFX 13.0 错误修复和一些增强。
Q1,2020 年 JavaFX 14.0 在 WebView 中支持 HTTP/2。更多的错误修复和一些增强。
2020 年第三季度 JavaFX 15.0 提高稳定性(内存管理)。更多的错误修复和一些增强。
Q1,2021 年 JavaFX 16.0 JavaFX 模块必须从模块路径加载,而不是从类路径加载(编译器警告)。更多的错误修复和一些增强。
2021 年第四季度 JavaFX 17.0.1 小的改进和错误修复。

发行说明显示了更多详细信息。你可以在 https://github.com/openjdk/jfx/tree/master/doc-files 看到它们。

系统需求

您需要在计算机上安装以下软件:

  • Java 开发工具包 17,来自 Oracle,或者 OpenJDK。

  • Eclipse IDE 2021-06 或更高版本。

  • 适用于您*台的 JavaFX 17 SDK,下载并解压缩到您选择的文件夹中。前往 https://openjfx.io/ 获取文档和下载链接。

Caution

如果您使用 Oracle 的 JDK,您需要输入一个付费程序,以防您将 JDK 用于商业项目。如果不希望这样,可以考虑使用 OpenJDK。

没有必要使用 Eclipse IDE 来编译和运行本书中的程序。您可以使用任何其他 IDE,例如 NetBeans、JDeveloper 或 IntelliJ IDEA。如果您愿意的话,您可以不使用任何 IDE,只使用命令行,也许还可以使用 Ant、Maven 或 Grails 之类的构建工具。

JavaFX 发行版还提供了一组预打包的 jmod 文件。然而,在编写这个版本时,由于类路径问题,还不能使用它们。如果你愿意,你可以试试 JMODs。我们将在本书中使用 SDK,它由一堆打包成 jar 文件的 Java 模块以及特定于*台的本地库组成。

JavaFX 运行时库

在 PC 上下载并解压 JavaFX SDK 发行版后,您会发现 Java 模块 jar(Java FX . base、javafx.graphics 等。)以及特定于*台的本机库(。所以文件,。dll 文件等。)在 lib 文件夹中。为了编译和运行 JavaFX 应用程序,您必须引用这个 lib 文件夹中的 jar,并且所有特定于*台的本地库必须位于同一个文件夹中。

如果您使用 IDE 和/或 JavaFX 工具,可能会包含 JavaFX 模块和库,并为您进行配置。如果您是这种情况,请注意正确的 JavaFX 版本。最可靠的开发设置,尽管可能不是最方便的,是使用 Eclipse 编辑和编译 Java 文件,自己提供 JavaFX 库,并且而不是使用 IDE 的任何特殊 JavaFX 特性。

JavaFX 源代码

有经验的开发人员有时更喜欢查看 JavaFX 库的源代码,以了解幕后是如何实现的。

JavaFX SDK 包括源代码—注意 lib 文件夹中的 src.zip 文件。这也是您可以从 Eclipse 内部使用的文件,用于将 JavaFX 源代码附加到 JavaFX API 库——然后您可以使用 F3 键来轻松导航到 JavaFX API 类源代码。

您的第一个 JavaFX 应用程序

让我们编写您的第一个 JavaFX 应用程序。它应该在一个窗口中显示文本“Hello JavaFX”。我将采用一种循序渐进的方法来解释如何开发第一个应用程序。我将添加尽可能少的代码行,然后解释代码做什么以及为什么需要它。

开始一个 Eclipse 项目

让我们首先看看如何建立一个 Eclipse 项目来开发 JavaFX 应用程序。

Note

如果您使用不同的 IDE,为了了解如何设置 JavaFX 项目,您必须查阅 IDE 的文档。

打开 Eclipse,使用您喜欢的任何工作空间。接下来,安装来自 JDK 17 的 JRE,如果你还没有这样做的话。为此,请转到首选项并注册 JRE,如图 1-2 所示。

img/336502_2_En_1_Fig2_HTML.jpg

图 1-2

在 Eclipse 中注册 JRE

启动一个名为 HelloFX 的新 Java 项目。确保新建项目向导创建了一个 module-info.java 文件。有一个复选框。在项目首选项中,添加 JavaFX 安装的 lib 文件夹中的所有模块 JARs 参见图 1-3 。

img/336502_2_En_1_Fig3_HTML.jpg

图 1-3

在 Eclipse 中添加 JavaFX 模块

确保将 jar 添加到 Modulepath 部分,而不是 Classpath 部分。

作为最后一个准备步骤,在 src 文件夹中创建一个包 com . jdojo . intro——在这里,我们添加应用程序类。

设置模块信息

为了让 JavaFX 在模块化环境中正确工作,请记住我们在 src 文件夹中添加了一个 module-info.java 文件,这相当于说“我们使用模块化环境”,我们需要在该文件中添加几个条目。打开它,将其内容更改为

module JavaFXBook {
      requires javafx.graphics;
      requires javafx.controls;
      requires java.desktop;
      requires javafx.swing;
      requires javafx.media;
      requires javafx.web;
      requires javafx.fxml;
      requires jdk.jsobject;

      opens com.jdojo.intro to javafx.graphics, javafx.base;
}

在第一行中,使用您在创建项目时输入的任何内容作为模块名称。

创建 HelloJavaFX

一个JavaFX application是一个必须从javafx.application包中的Application class继承的类。您将把您的类命名为HelloFXApp,它将被存储在com.jdojo.intro包中。清单 1-1 显示了HelloFXApp类的初始代码。请注意,HelloFXApp类此时不会编译。您将在下一节中修复它。

// HelloFXApp.java
package com.jdojo.intro;

import javafx.application.Application;

public class HelloFXApp extends Application {
      public static void main(String[] args) {
            Application.launch(args);
      }
      // Application logic goes here
}

Listing 1-1Inheriting Your JavaFX Application Class from the javafx.application.Application Class

该程序包括一个包声明、一个导入语句和一个类声明。代码中没有类似 JavaFX 的内容。它看起来像任何其他 Java 应用程序。然而,通过从Application类继承HelloFXApp类,您已经满足了 JavaFX 应用程序的需求之一。

覆盖 start() 方法

如果您尝试编译HelloFXApp类,将会导致以下编译时错误: *HelloFXApp 不是抽象的,不会覆盖应用程序中的抽象方法 start(Stage)。*该错误表明Application类包含一个抽象的start(Stage stage)方法,该方法没有在HelloFXApp类中被覆盖。作为 Java 开发人员,您知道下一步该做什么:要么将HelloFXApp类声明为抽象类,要么为start()方法提供一个实现。这里,让我们为start()方法提供一个实现。Application类中的start()方法声明如下:

public abstract void start(Stage stage) throws java.lang.Exception

清单 1-2 显示了覆盖start()方法的HelloFXApp类的修改代码。

// HelloFXApp.java
package com.jdojo.intro;

import javafx.application.Application;
import javafx.stage.Stage;

public class HelloFXApp extends Application {
      public static void main(String[] args) {
            Application.launch(args);
      }
      @Override
      public void start(Stage stage) {
               // The logic for starting the application goes here
      }
}

Listing 1-2Overriding the start() Method in Your JavaFX Application Class

在修订后的代码中,您加入了两件事:

  • 您已经添加了一个另外的import语句来从javafx.stage包中导入Stage类。

  • 您已经实现了start()方法。该方法的throws子句被删除,这符合 Java 中覆盖方法的规则。

start()方法是 JavaFX 应用程序的入口点。它由 JavaFX 应用程序启动器调用。注意,start()方法被传递了一个Stage类的实例,这个实例被称为应用程序的初级阶段。您可以根据需要在应用程序中创建更多阶段。但是,主阶段总是由 JavaFX 运行时为您创建的。

Tip

每个 JavaFX 应用程序类都必须从Application类继承,并为start(Stage stage)方法提供实现。

展示舞台

类似于现实世界中的舞台,JavaFX 舞台用于显示场景。场景具有视觉效果,如文本、形状、图像、控件、动画和效果,用户可以与这些视觉效果进行交互,所有基于 GUI 的应用程序都是如此。

在 JavaFX 中,主舞台是场景的容器。根据应用程序的运行环境,stage 的外观会有所不同。您不需要根据环境采取任何行动,因为 JavaFX 运行时会为您处理所有细节。由于该应用程序作为桌面应用程序运行,主舞台将是一个带有标题栏和显示场景区域的窗口。

由应用程序启动器创建的初级阶段没有场景。在下一节中,您将为您的舞台创建一个场景。

您必须展示舞台才能看到场景中包含的视觉效果。使用show()方法显示阶段。或者,您可以使用setTitle()方法为舞台设置一个标题。清单 1-3 中显示了HelloFXApp类的修订代码。

// HelloFXApp.java
package com.jdojo.intro;

import javafx.application.Application;
import javafx.stage.Stage;

public class HelloFXApp extends Application {
      public static void main(String[] args) {
            Application.launch(args);
      }
      @Override
      public void start(Stage stage) {
               // Set a title for the stage
               stage.setTitle("Hello JavaFX Application");

               // Show the stage
               stage.show();
       }
}

Listing 1-3Showing the Primary Stage in Your JavaFX Application Class

启动应用程序

现在您已经准备好运行您的第一个 JavaFX 应用程序了。您的 IDE 可能已经动态编译了您的类。Eclipse 就是这样工作的。

使用 Eclipse IDE 中的启动器运行HelloFXApp类。在类名上单击鼠标右键,然后调用作为➤ Java 应用程序运行。在命令行上,为了编译和运行,您必须添加所有的 modulepath 条目,这超出了本章的介绍范围。

如果成功启动,应用程序将显示一个带有标题栏的窗口,如图 1-4 所示。

img/336502_2_En_1_Fig4_HTML.jpg

图 1-4

没有场景的 JavaFX 应用程序

窗口的主要区域是空的。这是舞台将显示其场景的内容区域。因为您还没有舞台场景,所以您会看到一个空白区域。标题栏显示您在start()方法中设置的标题。

您可以使用窗口标题栏中的关闭菜单选项关闭应用程序。在 Windows 中使用 Alt + F4 关闭窗口。您可以使用*台提供的任何其他选项来关闭窗口。

Tip

直到所有窗口都关闭或者应用程序使用Platform.exit()方法退出,类Applicationlaunch()方法才返回。Platform级在javafx.application包里。

您还没有在 JavaFX 中看到任何令人兴奋的东西!你需要等待,直到你在下一部分创建一个场景。

向舞台添加场景

javafx.scene包中的Scene类的一个实例代表一个场景。舞台包含一个场景,场景包含视觉内容。

场景的内容以树状层次排列。在层次结构的顶端是根节点和 ?? 节点。根节点可能包含子节点,子节点又可能包含它们的子节点,依此类推。必须有根节点才能创建场景。您将使用一个VBox作为根节点。VBox代表垂直框,将其子项垂直排列成一列。下面的语句创建了一个VBox:

 VBox root = new VBox();

Tip

javafx.scene.Parent类继承的任何节点都可以用作场景的根节点。几个节点,称为布局窗格或容器,如VBoxHBoxPaneFlowPaneGridPaneTilePane,可以用作根节点。Group是一个特殊的容器,将它的子容器组合在一起。

可以有子节点的节点提供了一个返回其子节点的ObservableListgetChildren()方法。要向节点添加子节点,只需将子节点添加到ObservableList中。下面的代码片段将一个Text节点添加到一个VBox中:

 // Create a VBox node
VBox root = new VBox();

// Create a Text node
Text msg = new Text("Hello JavaFX");

// Add the Text node to the VBox as a child node
root.getChildren().add(msg);

Scene类包含几个构造器。您将使用允许您指定场景的根节点和大小的那个。以下语句创建一个以VBox为根节点的场景,宽度为 300 像素,高度为 50 像素:

 // Create a scene
Scene scene = new Scene(root, 300, 50);

您需要通过调用Stage类的setScene()方法将场景设置为舞台:

 // Set the scene to the stage
stage.setScene(scene);

就这样。您已经用一个场景完成了您的第一个 JavaFX 程序。清单 1-4 包含完整的程序。程序显示如图 1-5 所示的窗口。

img/336502_2_En_1_Fig5_HTML.jpg

图 1-5

JavaFX 应用程序的场景有一个Text节点

// HelloFXAppWithAScene.java
package com.jdojo.intro;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.stage.Stage;

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

       @Override
       public void start(Stage stage) {
               Text msg = new Text("Hello JavaFX");
               VBox root = new VBox();
               root.getChildren().add(msg);

               Scene scene = new Scene(root, 300, 50);
               stage.setScene(scene);
               stage.setTitle(
                   "Hello JavaFX Application with a Scene");
               stage.show();
       }
}

Listing 1-4A JavaFX Application with a Scene Having a Text Node

改进 HelloFX 应用程序

JavaFX 能够做的事情比您到目前为止看到的要多得多。让我们增强第一个程序,并添加更多的用户界面元素,如按钮和文本字段。这一次,用户将能够与应用程序进行交互。使用Button类的实例创建一个按钮,如下所示:

 // Create a button with "Exit" text
Button exitBtn = new Button("Exit");

当一个按钮被点击时,一个ActionEvent被触发。您可以添加一个ActionEvent处理程序来处理该事件。使用setOnAction()方法为按钮设置一个ActionEvent处理程序。下面的语句为按钮设置了一个ActionEvent处理程序。处理程序终止应用程序。您可以使用 lambda 表达式或匿名类来设置ActionEvent处理程序。以下代码片段展示了这两种方法:

// Using a lambda expression
exitBtn.setOnAction(e -> Platform.exit());

// Using an anonymous class
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
...
exitBtn.setOnAction(new EventHandler<ActionEvent>() {
       @Override
       public void handle(ActionEvent e) {
               Platform.exit();
       }
});

清单 1-5 中的程序展示了如何给场景添加更多的节点。该程序使用Label类的setStyle()方法将Label的填充颜色设置为蓝色。稍后我将讨论在 JavaFX 中使用 CSS。

// ImprovedHelloFXApp.java
package com.jdojo.intro;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

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

       @Override
       public void start(Stage stage) {
               Label nameLbl = new Label("Enter your name:");
               TextField nameFld = new TextField();

               Label msg = new Label();
               msg.setStyle("-fx-text-fill: blue;");

               // Create buttons
               Button sayHelloBtn = new Button("Say Hello");
               Button exitBtn = new Button("Exit");

               // Add the event handler for the Say Hello button
               sayHelloBtn.setOnAction(e -> {
                      String name = nameFld.getText();
                      if (name.trim().length() > 0) {
                             msg.setText("Hello " + name);
                      } else {
                             msg.setText("Hello there");
                      }

               });

               // Add the event handler for the Exit button
               exitBtn.setOnAction(e -> Platform.exit());

               // Create the root node
               VBox root = new VBox();

               // Set the vertical spacing between children to 5px
               root.setSpacing(5);

               // Add children to the root node
               root.getChildren().addAll(nameLbl, nameFld, msg,
                   sayHelloBtn, exitBtn);

               Scene scene = new Scene(root, 350, 150);
               stage.setScene(scene);
               stage.setTitle("Improved Hello JavaFX Application");
               stage.show();
       }
}

Listing 1-5Interacting with Users in a JavaFX Application

改进后的HelloFX程序显示如图 1-6 所示的窗口。该窗口包含两个标签、一个文本字段和两个按钮。一个VBox被用作场景的根节点。在文本栏中输入名称,然后点按“问好”按钮以查看问候信息。在不输入姓名的情况下点击“问好”按钮会显示消息Hello there。应用程序在Label控件中显示一条消息。单击退出按钮退出应用程序。

img/336502_2_En_1_Fig6_HTML.jpg

图 1-6

一个 JavaFX 应用程序,它的场景中有一些控件

向 JavaFX 应用程序传递参数

与 Java 应用程序一样,您可以在命令行上或者通过 IDE 中的一些启动配置将参数传递给 JavaFX 应用程序。

Parameters类是Application类的静态内部类,它封装了传递给 JavaFX 应用程序的参数。它将参数分为三类:

  • 命名参数

  • 未命名参数

  • 原始参数(命名和未命名参数的组合)

您需要使用Parameters类的以下三个方法来访问三种类型的参数:

  • Map<String, String> getNamed()

  • List<String> getUnnamed()

  • List<String> getRaw()

参数可以是命名的,也可以是未命名的。命名参数由(名称,值)对组成。未命名的参数由单个值组成。getNamed()方法返回一个包含名称参数的键值对的Map<String, String>getUnnamed()方法返回一个List<String>,其中每个元素都是一个未命名的参数值。

您只能将命名和未命名的参数传递给 JavaFX 应用程序。不传递原始类型参数。JavaFX 运行时通过Parameters类的getRaw()方法将所有已命名和未命名的参数作为List<String>传递给应用程序。下面的讨论将使这三种方法的返回值之间的区别变得清晰。

Application类的getParameters()方法返回Application.Parameters类的引用。对Parameters类的引用可以在Application类的init()方法和随后执行的代码中找到。参数在应用程序的构造器中不可用,因为它在init()方法之前被调用。调用构造器中的getParameters()方法返回null

清单 1-6 中的程序读取传递给应用程序的所有类型的参数,并将它们显示在一个TextArea中。一个TextArea是显示多行文本的 UI 节点。

// FXParamApp.java
package com.jdojo.intro;

import java.util.List;
import java.util.Map;
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.TextArea;
import javafx.stage.Stage;

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

       @Override
       public void start(Stage stage) {
               // Get application parameters
               Parameters p = this.getParameters();
               Map<String, String> namedParams = p.getNamed();
               List<String> unnamedParams = p.getUnnamed();
               List<String> rawParams = p.getRaw();

               String paramStr = "Named Parameters: " + namedParams +
                      "\n" +
                      "Unnamed Parameters: " + unnamedParams + "\n" +
                      "Raw Parameters: " + rawParams;

               TextArea ta = new TextArea(paramStr);
               Group root = new Group(ta);
               stage.setScene(new Scene(root));
               stage.setTitle("Application Parameters");
               stage.show();
       }
}

Listing 1-6Accessing Parameters Passed to a JavaFX Application

让我们看几个将参数传递给FXParamApp类的例子。当您运行FXParamApp类时,以下情况中提到的输出显示在窗口的TextArea控件中。

案例 1

使用以下命令将该类作为独立应用程序运行:

 java [options] com.jdojo.stage.FXParamApp Anna Lola

前面的命令没有传递命名参数和两个未命名参数:AnnaLola。原始参数列表将包含两个未命名的参数。输出将如下所示:

Named Parameters: {}
Unnamed Parameters: [Anna, Lola]
Raw Parameters: [Anna, Lola]

案例 2

要从命令行传递一个命名的参数,您需要在参数前面加两个连字符(--)。也就是说,应该在表单中输入命名参数

 --key=value

使用以下命令将该类作为独立应用程序运行:

java [options] com.jdojo.stage.FXParamApp \
    Anna Lola --width=200 --height=100

前面的命令传递两个命名参数:width=200height=100。它传递两个未命名的参数:AnnaLola。原始参数列表将包含四个元素:两个命名参数和两个未命名参数。原始参数列表中的命名参数值前面有两个连字符。输出将如下所示:

Named Parameters: {height=100, width=200}
Unnamed Parameters: [Anna, Lola]
Raw Parameters: [Anna, Lola, --width=200, --height=100]

启动 JavaFX 应用程序

前面,我谈到了在开发第一个 JavaFX 应用程序时启动 JavaFX 应用程序的主题。本节提供了关于启动 JavaFX 应用程序的更多细节。

每个 JavaFX 应用程序类都继承自Application类。Application级在javafx.application包里。它包含一个静态的launch()方法。它的唯一目的是启动 JavaFX 应用程序。它是一个重载方法,有以下两种变体:

  • static void launch(Class<? extends Application> appClass, String... args)

  • static void launch(String... args)

注意,您不需要创建 JavaFX 应用程序类的对象来启动它。当调用launch()方法时,JavaFX 运行时创建应用程序类的一个对象。

Tip

您的 JavaFX 应用程序类必须有一个no-args构造器;否则,当试图启动它时,将会引发运行时异常。

launch()方法的第一个变体很清楚。您将应用程序类的类引用作为第一个参数传递,并且launch()方法将创建该类的一个对象。第二个参数由传递给应用程序的命令行参数组成。下面的代码片段展示了如何使用launch()方法的第一个变体:

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

       // More code goes here
}

传递给launch()方法的类引用不必与调用该方法的类相同。例如,下面的代码片段从MyAppLauncher类启动MyJavaFXApp应用程序类,它没有扩展Application类:

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

       // More code goes here
}

launch()方法的第二个变体只有一个参数,即传递给应用程序的命令行参数。它使用哪个 JavaFX 应用程序类来启动应用程序?它试图根据调用者找到应用程序类名。它检查调用它的代码的类名。如果该方法作为从Application类直接或间接继承的类的代码的一部分被调用,则该类用于启动 JavaFX 应用程序。否则,将引发运行时异常。让我们看一些例子来说明这个规则。

在下面的代码片段中,launch()方法检测到它是从MyJavaFXApp类的main()方法中调用的。MyJavaFXApp类继承自Application类。因此,MyJavaFXApp类被用作应用程序类:

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

       // More code goes here
}

在下面的代码片段中,从Test类的main()方法调用了launch()方法。Test不从Application类继承。因此,会引发运行时异常,如代码下面的输出所示:

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

       // More code goes here
}
Exception in thread "main" java.lang.RuntimeException: Error: class Test is not a subclass of javafx.application.Application
       at javafx.application.Application.launch(Application.java:308)
       at Test.main(Test.java)

在下面的代码片段中,launch()方法检测到它是从MyJavaFXApp$1类的run()方法中调用的。注意,MyJavaFXApp$1类是编译器生成的匿名内部类,是Object类的子类,而不是Application类,它实现了Runnable接口。因为对launch()方法的调用包含在MyJavaFXApp$1类中,而MyJavaFXApp$1类不是Application类的子类,所以会抛出一个运行时异常,如下面代码的输出所示:

public class MyJavaFXApp extends Application {
       public static void main(String[] args) {
               Thread t = new Thread(new Runnable() {
                      public void run() {
                             Application.launch(args);
                      }
               });

               t.start();
       }

       // More code goes here
}

Exception in thread "Thread-0" java.lang.RuntimeException: Error: class MyJavaFXApp$1 is not a subclass of javafx.application.Application
       at javafx.application.Application.launch(Application.java:211)
       at MyJavaFXApp$1.run(MyJavaFXApp.java)
       at java.lang.Thread.run(Thread.java:722)

现在您已经知道了如何启动 JavaFX 应用程序,是时候学习启动 JavaFX 应用程序的最佳实践了:将main()方法中的代码限制为只有一条启动应用程序的语句,如以下代码所示:

 public class MyJavaFXApp extends Application {
       public static void main(String[] args) {
               Application.launch(args);

               // Do not add any more code in this method
       }

       // More code goes here
}

Tip

Application类的launch()方法只能调用一次;否则,将引发运行时异常。对launch()方法的调用会一直阻塞,直到应用程序终止。

JavaFX 应用程序的生命周期

JavaFX 运行时创建几个线程。在应用程序的不同阶段,线程用于执行不同的任务。在这一节中,我将只解释那些在生命周期中用来调用Application类的方法的线程。JavaFX 运行时在其他线程中创建了两个线程:

  • Java FX-启动器

  • JavaFX 应用程序线程

Application类的launch()方法创建这些线程。在 JavaFX 应用程序的生命周期中,JavaFX 运行时按顺序调用指定 JavaFX Application类的以下方法:

  • no-args构造器

  • init()

  • start()

  • stop()

JavaFX 运行时在 JavaFX 应用程序线程上创建指定的Application类的对象。JavaFX 启动器线程调用指定的Application类的init()方法。Application类中的init()方法实现为空。您可以在应用程序类中重写此方法。不允许在 JavaFX 启动器线程上创建StageScene。它们必须在 JavaFX 应用程序线程上创建。因此,不能在init()方法中创建StageScene。试图这样做将引发运行时异常。创建 UI 控件是很好的,例如按钮或形状。

JavaFX 应用程序线程调用指定的Application类的start(Stage stage)方法。注意,Application类中的start()方法被声明为abstract,您必须在您的应用程序类中覆盖这个方法。

此时,launch()方法等待 JavaFX 应用程序完成。当应用完成时,JavaFX 应用线程调用指定的Application类的stop()方法。在Application类中,stop()方法的默认实现是空的。当应用程序停止时,您必须在您的application类中覆盖这个方法来执行您的逻辑。

清单 1-7 中的代码展示了 JavaFX 应用程序的生命周期。它显示一个空的舞台。当显示 stage 时,您将看到输出的前三行。您需要关闭阶段才能看到输出的最后一行。

// FXLifeCycleApp.java
package com.jdojo.intro;

import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class FXLifeCycleApp extends Application {
       public FXLifeCycleApp() {
               String name = Thread.currentThread().getName();
               System.out.println("FXLifeCycleApp() constructor: " +
                   name);
       }

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

       @Override
       public void init() {
               String name = Thread.currentThread().getName();
               System.out.println("init() method: " + name);
       }

       @Override
       public void start(Stage stage) {
               String name = Thread.currentThread().getName();
               System.out.println("start() method: " + name);

               Scene scene = new Scene(new Group(), 200, 200);
               stage.setScene(scene);
               stage.setTitle("JavaFX Application Life Cycle");
               stage.show();
       }

       @Override
       public void stop() {
               String name = Thread.currentThread().getName();
               System.out.println("stop() method: " + name);
       }
}

FXLifeCycleApp() constructor: JavaFX Application Thread
init() method: JavaFX-Launcher
start() method: JavaFX Application Thread
stop() method: JavaFX Application Thread

Listing 1-7The Life Cycle of a JavaFX Application

终止 JavaFX 应用程序

JavaFX 应用程序可以显式或隐式终止。您可以通过调用Platform.exit()方法显式终止 JavaFX 应用程序。当这个方法被调用时,在start()方法之后或者从该方法内部,调用Application类的stop()方法,然后 JavaFX 应用程序线程被终止。此时,如果只有守护线程在运行,JVM 将退出。如果从构造器或Application类的init()方法调用该方法,则stop()方法可能不会被调用。

当最后一个窗口关闭时,JavaFX 应用程序可以隐式终止。使用Platform类的静态setImplicitExit(boolean implicitExit)方法可以打开和关闭这种行为。将true传递给这个方法可以打开这个行为。将false传递给这个方法可以关闭这个行为。默认情况下,此行为是打开的。这就是为什么在迄今为止的大多数例子中,当你关闭窗口时,应用程序会被终止。当这个行为打开时,在终止 JavaFX 应用程序线程之前,调用Application类的stop()方法。终止 JavaFX 应用程序线程并不总是会终止 JVM。如果所有正在运行的非守护线程都终止了,JVM 也会终止。如果 JavaFX 应用程序的隐式终止行为被关闭,您必须调用Platform类的exit()方法来终止应用程序。

摘要

JavaFX 是一个开源的基于 Java 的 GUI 框架,用于开发富客户端应用程序。它是 Swing 在 Java *台 GUI 开发技术领域的继承者。

JavaFX 中的 GUI 分阶段显示。stage 是Stage类的一个实例。舞台是桌面应用程序中的一个窗口。一个舞台包含一个场景。场景包含一组以树状结构排列的节点(图形)。

JavaFX 应用程序继承自Application类。JavaFX 运行时创建称为初级阶段的第一个阶段,并调用应用程序类的start()方法,传递初级阶段的引用。开发人员需要向舞台添加一个场景,并在start()方法中使舞台可见。

您可以使用Application类的launch()方法启动 JavaFX 应用程序。

在 JavaFX 应用程序的生命周期中,JavaFX 运行时以特定的顺序调用 JavaFX Application类的预定义方法。首先,调用该类的no-args构造器,然后调用init()start()方法。当应用程序终止时,会调用 stop()方法。您可以通过调用Platform.exit()方法来终止 JavaFX 应用程序。

下一章将向您介绍 JavaFX 中的属性和绑定。

二、属性和绑定

在本章中,您将学习:

  • JavaFX 中的属性是什么

  • 如何创建属性对象并使用它

  • JavaFX 中属性的类层次结构

  • 如何处理属性对象中的失效和更改事件

  • JavaFX 中的绑定是什么,以及如何使用单向和双向绑定

  • 关于 JavaFX 中的高级和低级绑定 API

本章讨论 Java 和 JavaFX 中的属性和绑定支持。如果您有使用 JavaBeans API 进行属性和绑定的经验,可以跳过前面几节,这几节讨论了 Java 中的属性和绑定支持,从“理解 JavaFX 中的属性”一节开始。

本章的例子在com.jdojo.binding包中。为了让它们工作,您必须在module-info.java文件中添加相应的一行:

...
opens com.jdojo.binding to javafx.graphics, javafx.base;
...

什么是财产?

一个 Java 类可以包含两类成员:字段方法。字段代表对象的状态,它们被声明为私有的。公共方法被称为访问器获取器设置器,用于读取和修改私有字段。简单地说,对于所有或部分私有字段,具有公共访问器的 Java 类被称为 Java bean ,访问器定义了 bean 的属性。Java bean 的属性允许用户定制其状态、行为或两者。

Java beans 是可观察的。它们支持属性更改通知。当 Java bean 的公共属性发生变化时,会向所有感兴趣的侦听器发送通知。

本质上,Java beans 定义了可重用的组件,这些组件可以由构建器工具组装起来以创建 Java 应用程序。这为第三方开发 JavaBean 并使其可供他人重用打开了大门。

属性可以是只读、只写或读/写。只读属性有 getter,但没有 setter。只写属性有 setter,但没有 getter。读/写属性有一个 getter 和一个 setter。

Java IDEs 和其他构建工具(例如,GUI 布局构建器)使用自省来获取 bean 的属性列表,并允许您在设计时操作这些属性。Java bean 可以是可视的,也可以是不可视的。bean 的属性可以在构建工具中使用,也可以以编程方式使用。

JavaBeans API 提供了一个类库,通过java.beans包和命名约定来创建和使用 JavaBeans。下面是一个具有读/写name属性的Person bean 的例子。getName()方法(getter)返回name字段的值。setName()方法(setter)设置name字段的值:

 // Person.java
package com.jdojo.binding;

public class Person {
        private String name;

        public String getName() {
                return name;
        }

        public void setName(String name) {
                this.name = name;
        }
}

按照惯例,getter 和 setter 方法的名称是通过将属性名称的第一个字母大写,分别附加到单词 getset 来构造的。getter 方法不应该带任何参数,它的返回类型应该与字段的类型相同。setter 方法应该带一个参数,参数的类型应该和字段的类型相同,其返回类型应该是void

以下代码片段以编程方式操作Person bean 的name属性:

Person p = new Person();
p.setName("John Jacobs");
String name = p.getName();

一些面向对象的编程语言,例如 C#,提供了第三种类型的类成员,称为属性。属性用于从类外部读取、写入和计算私有字段的值。C#允许您声明一个带有Name属性的Person类,如下所示:

// C# version of the Person class
public class Person {
        private string name;

        public string Name {
                get { return name; }
                set { name = value; }
        }
}

在 C#中,以下代码片段使用Name属性操作name私有字段;它相当于前面显示的 Java 版本的代码:

Person p = new Person();
p.Name = "John Jacobs";
string name = p.Name;

如果属性的访问器执行返回和设置字段值的例行工作,C#提供了一种紧凑的格式来定义这样的属性。在这种情况下,您甚至不需要声明私有字段。您可以用 C#重写Person类,如下所示:

// C# version of the Person class using the compact format
public class Person {
        public string Name { get; set; }
}

那么,什么是财产呢?一个属性是一个类的公共可访问属性,影响它的状态、行为或两者。即使属性是可公开访问的,它的使用(读/写)也会调用隐藏实际实现的方法来访问数据。属性是可观察的,所以当它的值改变时,感兴趣的人会得到通知。

Tip

本质上,属性定义了对象的公共状态,可以读取、写入和观察对象的变化。与其他编程语言(如 C#)不同,Java 中的属性在语言级别不受支持。Java 对属性的支持来自 JavaBeans API 和设计模式。关于 Java 中属性的更多细节,请参考 JavaBeans 规范,可以从 www.oracle.com/java/technologies/javase/javabeans-spec.html 下载。

除了简单的属性,比如Person bean 的name属性,Java 还支持索引绑定约束属性。索引属性是使用索引访问的值的数组。索引属性是使用数组数据类型实现的。当绑定属性发生更改时,它会向所有侦听器发送通知。受约束的属性是侦听器可以否决更改的绑定属性。

什么是绑定?

在编程中,术语绑定被用在许多不同的上下文中。在这里,我想在数据绑定的上下文中定义它。数据绑定定义了程序中数据元素(通常是变量)之间的关系,以保持它们的同步。在 GUI 应用程序中,数据绑定经常用于将数据模型中的元素与相应的 UI 元素同步。

考虑以下语句,假设 x、y 和 z 是数值变量:

x = y + z;

前面的语句定义了 x、y 和 z 之间的绑定。当执行该语句时,x 的值与 y 和 z 的总和同步。绑定还具有时间因子。在前面的语句中,x 的值绑定到 y 和 z 的和,并且在语句执行时有效。在执行前面的语句之前和之后,x 的值可能不是 y 和 z 的和。

有时,希望绑定保持一段时间。考虑以下使用listPricediscountstaxes定义绑定的语句:

soldPrice = listPrice - discounts + taxes;

在这种情况下,您希望保持绑定永远有效,这样无论何时listPricediscountstaxes发生变化,销售价格都会被正确计算。

在前面的绑定中,listPricediscountstaxes被称为依赖,也就是说soldPrice被绑定到listPricediscountstaxes

为了使绑定正常工作,有必要在依赖关系发生变化时通知绑定。支持绑定的编程语言提供了一种用依赖关系注册侦听器的机制。当依赖关系变得无效或改变时,所有侦听器都会得到通知。当绑定接收到这样的通知时,它可以将其自身与其依赖项同步。

绑定可以是急切绑定懒惰绑定。在急切绑定中,绑定变量在其依赖关系更改后会立即重新计算。在惰性绑定中,当绑定变量的依赖关系改变时,不会重新计算绑定变量。而是在下次读取时重新计算。与急切绑定相比,惰性绑定的性能更好。

绑定可以是单向双向。单向绑定只在一个方向起作用;依赖关系中的更改会传播到绑定变量。双向绑定在两个方向上都起作用。在双向绑定中,绑定变量和依赖项保持它们的值相互同步。通常,双向绑定只在两个变量之间定义。例如,双向绑定 x = y 和 y = x 声明 x 和 y 的值总是相同的。

从数学上讲,不可能唯一地定义多个变量之间的双向绑定。在前面的示例中,销售价格绑定是单向绑定。如果您想使它成为一个双向绑定,那么当销售价格发生变化时,不可能唯一地计算标价、折扣和税的值。在另一个方向有无限多的可能性。

具有 GUI 的应用程序为用户提供 UI 部件,例如文本字段、复选框和按钮,以操作数据。UI 小部件中显示的数据必须与底层数据模型同步,反之亦然。在这种情况下,需要双向绑定来保持 UI 和数据模型同步。

了解 JavaBeans 中的绑定支持

在我讨论 JavaFX 属性和绑定之前,让我们先简单了解一下 JavaBeans API 中的绑定支持。如果您以前使用过 JavaBeans API,您可以跳过这一节。

从早期版本开始,Java 就支持 bean 属性的绑定。清单 2-1 显示了一个具有两个属性namesalaryEmployee bean。

// Employee.java
package com.jdojo.binding;

import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;

public class Employee {
        private String name;
        private double salary;
        private PropertyChangeSupport pcs = new PropertyChangeSupport(this);

        public Employee() {
                this.name = "John Doe";
                this.salary = 1000.0;
        }

        public Employee(String name, double salary) {
                this.name = name;
                this.salary = salary;
        }

        public String getName() {
                return name;
        }

        public void setName(String name) {
                this.name = name;
        }

        public double getSalary() {
                return salary;
        }

        public void setSalary(double newSalary) {
                double oldSalary = this.salary;
                this.salary = newSalary;

                // Notify the registered listeners about the change
                pcs.firePropertyChange("salary", oldSalary, newSalary);
        }

        public void addPropertyChangeListener(
                         PropertyChangeListener listener) {
                pcs.addPropertyChangeListener(listener);
        }

        public void removePropertyChangeListener(
                         PropertyChangeListener listener) {
                pcs.removePropertyChangeListener(listener);
        }

        @Override
        public String toString() {
                return "name = " + name + ", salary = " + salary;
        }
}

Listing 2-1An Employee Java Bean with Two Properties Named name and salary

Employee bean 的两个属性都是读/写的。salary属性也是一个绑定属性。它的设置器在薪水变化时生成属性变化通知。

感兴趣的监听器可以使用addPropertyChangeListener()removePropertyChangeListener()方法注册或取消注册变更通知。PropertyChangeSupport类是 JavaBeans API 的一部分,它简化了属性更改监听器的注册和删除以及属性更改通知的触发。

任何对基于工资变化的同步值感兴趣的一方都需要向Employee bean 注册,并在收到变化通知时采取必要的行动。

清单 2-2 展示了如何为一个Employee bean 注册工资变化通知。下面的输出显示工资变化通知只触发了两次,而setSalary()方法被调用了三次。这是真的,因为对setSalary()方法的第二次调用使用了与第一次调用相同的工资金额,而PropertyChangeSupport类足够聪明,能够检测到这一点。该示例还展示了如何使用 JavaBeans API 绑定变量。员工的税款是根据纳税百分比计算的。在 JavaBeans API 中,属性更改通知用于绑定变量。

// EmployeeTest.java
package com.jdojo.binding;

import java.beans.PropertyChangeEvent;

public class EmployeeTest {
        public static void main(String[] args) {
                final Employee e1 = new Employee("John Jacobs", 2000.0);

                // Compute the tax
                computeTax(e1.getSalary());

                // Add a property change listener to e1
                e1.addPropertyChangeListener(
                         EmployeeTest::handlePropertyChange);

                // Change the salary
                e1.setSalary(3000.00);
                e1.setSalary(3000.00); // No change notification is sent.
                e1.setSalary(6000.00);
        }

        public static void handlePropertyChange(PropertyChangeEvent e) {
                String propertyName = e.getPropertyName();

                if ("salary".equals(propertyName)) {
                        System.out.print("Salary has changed. ");
                        System.out.print("Old:" + e.getOldValue());
                        System.out.println(", New:" +
                                    e.getNewValue());
                        computeTax((Double)e.getNewValue());
                }
        }

        public static void computeTax(double salary) {
                final double TAX_PERCENT = 20.0;
                double tax = salary * TAX_PERCENT/100.0;
                System.out.println("Salary:" + salary + ", Tax:" + tax);
        }
}
Salary:2000.0, Tax:400.0
Salary has changed. Old:2000.0, New:3000.0
Salary:3000.0, Tax:600.0
Salary has changed. Old:3000.0, New:6000.0
Salary:6000.0, Tax:1200.0

Listing 2-2An EmployeeTest Class That Tests the Employee Bean for Salary Changes

了解 JavaFX 中的属性

JavaFX 通过属性绑定API 支持属性、事件和绑定。JavaFX 中的属性支持是 JavaBeans 属性的巨大飞跃。

JavaFX 中的所有属性都是可观察的。可以观察到它们的失效和值的变化。可以有读/写或只读属性。所有读/写属性都支持绑定。

在 JavaFX 中,属性可以表示一个值或一组值。本章介绍代表单个值的属性。我将在第三章中介绍代表一组值的属性。

在 JavaFX 中,属性是对象。每种属性都有一个属性类层次结构。例如,IntegerPropertyDoublePropertyStringProperty类分别代表intdoubleString类型的属性。这些类是abstract。它们有两种类型的实现类:一种表示读/写属性,另一种表示只读属性的包装。例如,SimpleDoublePropertyReadOnlyDoubleWrapper类是具体的类,它们的对象分别用作读/写和只读双精度属性。

以下是如何创建初始值为 100 的IntegerProperty的示例:

IntegerProperty counter = new SimpleIntegerProperty(100);

属性类提供了两对 getter 和 setter 方法:get() / set()getValue() / setValue()get()set()方法分别获取和设置属性的值。对于基本类型属性,它们使用基本类型值。比如对于IntegerPropertyget()方法的返回类型和set()方法的参数类型都是intgetValue()setValue()方法处理一个对象类型;例如,对于IntegerProperty,它们的返回类型和参数类型是Integer

Tip

对于引用类型属性,比如StringPropertyObjectProperty<T>,两对 getter 和 setter 都使用一个对象类型。也就是说,StringPropertyget()getValue()方法都返回一个String,而set()setValue()方法都带有一个String参数。对于基元类型的自动装箱,使用哪个版本的 getter 和 setter 并不重要。getValue()setValue()方法的存在是为了帮助你根据对象类型编写通用代码。

下面的代码片段使用了一个IntegerProperty及其get()set()方法。counter属性是读/写属性,因为它是SimpleIntegerProperty类的对象:

IntegerProperty counter = new SimpleIntegerProperty(1);
int counterValue = counter.get();
System.out.println("Counter:" + counterValue);

counter.set(2);
counterValue = counter.get();
System.out.println("Counter:" + counterValue);
Counter:1
Counter:2

使用只读属性有点棘手。一个ReadOnlyXXXWrapper类包装了XXX类型的两个属性:一个只读,一个读/写。两种属性都是同步的。它的getReadOnlyProperty()方法返回一个ReadOnlyXXXProperty对象。

下面的代码片段展示了如何创建一个只读的Integer属性。属性是读/写的,而属性是只读的。当idWrapper中的值改变时,id中的值自动改变:

ReadOnlyIntegerWrapper idWrapper = new ReadOnlyIntegerWrapper(100);
ReadOnlyIntegerProperty id = idWrapper.getReadOnlyProperty();

System.out.println("idWrapper:" + idWrapper.get());
System.out.println("id:" + id.get());

// Change the value
idWrapper.set(101);

System.out.println("idWrapper:" + idWrapper.get());
System.out.println("id:" + id.get());
idWrapper:100
id:100
idWrapper:101
id:101

Tip

通常,包装属性用作类的私有实例变量。类别可以在内部变更属性。它的一个方法返回包装类的只读属性对象,因此同一个属性对于外界是只读的。

可以使用代表单个值的七种类型的属性。这些属性的基类被命名为XXXProperty,只读基类被命名为ReadOnlyXXXProperty,包装类被命名为ReadOnlyXXXWrapper。每种类型的XXX值列于表 2-1 中。

表 2-1

包装单个值的属性类列表

|

类型

|

XXX 值

int Integer
long Long
float Float
double Double
boolean Boolean
String String
Object Object

属性对象包装了三条信息:

  • 包含它的 bean 的引用

  • 一个名字

  • 一种价值观

创建属性对象时,可以提供前面三条信息的全部,也可以不提供。像SimpleXXXPropertyReadOnlyXXXWrapper这样命名的具体属性类提供了四个构造器,让您提供这三条信息的组合。下面是SimpleIntegerProperty类的构造器:

SimpleIntegerProperty()
SimpleIntegerProperty(int initialValue)
SimpleIntegerProperty(Object bean, String name)
SimpleIntegerProperty(Object bean, String name, int initialValue)

初始值的默认值取决于属性的类型。对于数值类型是零,对于布尔类型是false,对于引用类型是null

属性对象可以是 bean 的一部分,也可以是独立的对象。指定的bean是对包含该属性的 bean 对象的引用。对于独立的属性对象,可以是null。其默认值为null

属性的名字就是它的名字。如果未提供,则默认为空字符串。

下面的代码片段创建一个属性对象作为 bean 的一部分,并设置所有三个值。SimpleStringProperty类的构造器的第一个参数是this,它是Person bean 的引用,第二个参数—"name"—是属性的名称,第三个参数—"Li"—是属性的值:

public class Person {
        private StringProperty name = new SimpleStringProperty(
                this, "name", "Li");
           // More code goes here...
}

每个属性类都有分别返回 bean 引用和属性名的getBean()getName()方法。

在 JavaFX Beans 中使用属性

在上一节中,您看到了 JavaFX 属性作为独立对象的使用。在本节中,您将在类中使用它们来定义属性。让我们创建一个具有三个属性的Book类:ISBNtitleprice,将使用 JavaFX 属性类对其进行建模。

在 JavaFX 中,不将类的属性声明为基本类型之一。相反,您使用 JavaFX 属性类之一。Book类的title属性将声明如下。照常宣布private:

public class Book {
        private StringProperty title = new SimpleStringProperty(this,
               "title", "Unknown");
}

您为属性声明了一个公共 getter,按照惯例,它被命名为XXXProperty,其中XXX是属性的名称。这个 getter 返回属性的引用。对于我们的title属性,getter 将被命名为titleProperty,如下所示:

public class Book {
        private StringProperty title = new SimpleStringProperty(this,
               "title", "Unknown");

        public final StringProperty titleProperty() {
                return title;
        }
}

前面的Book类声明可以很好地处理title属性,如下面设置和获取书名的代码片段所示:

Book b = new Book();
b.titleProperty().set("Harnessing JavaFX 17.0");
String title = b.titleProperty().get();

根据 JavaFX 设计模式,而不是任何技术要求,JavaFX 属性有一个 getter 和 setter,类似于 JavaBeans 中的 getter 和 setter。getter 的返回类型和 setter 的参数类型与属性值的类型相同。比如对于StringPropertyIntegerProperty,分别会是Stringinttitle属性的getTitle()setTitle()方法声明如下:

public class Book {
        private StringProperty title = new SimpleStringProperty(this,
               "title", "Unknown");

        public final StringProperty titleProperty() {
                return title;
        }

        public final String getTitle() {
                return title.get();
        }

        public final void setTitle(String title) {
                this.title.set(title);
        }
}

注意,getTitle()setTitle()方法在内部使用title属性对象来获取和设置标题值。

Tip

按照惯例,类的属性的 getters 和 setters 被声明为final。添加了使用 JavaBeans 命名约定的额外的 getters 和 setters,以使该类能够与使用旧 JavaBeans 命名约定来标识类属性的旧工具和框架进行互操作。

以下代码片段显示了对Book类的只读ISBN属性的声明:

public class Book {
        private ReadOnlyStringWrapper ISBN =
               new ReadOnlyStringWrapper(this, "ISBN", "Unknown");

        public final String getISBN() {
                return ISBN.get();
        }

        public final ReadOnlyStringProperty ISBNProperty() {
                return ISBN.getReadOnlyProperty();
        }

        // More code goes here...
}

关于只读ISBN属性的声明,请注意以下几点:

  • 它使用了ReadOnlyStringWrapper类而不是SimpleStringProperty类。

  • 属性值没有设置器。你可以声明一个;但是,必须是私人的。

  • 属性值的 getter 与读/写属性的 getter 工作方式相同。

  • ISBNProperty()方法使用ReadOnlyStringProperty作为返回类型,而不是ReadOnlyStringWrapper。它从包装对象获取属性对象的只读版本,并返回该版本。

对于Book类的用户,它的ISBN属性是只读的。但是,它可以在内部进行更改,并且该更改将自动反映在 property 对象的只读版本中。

清单 2-3 显示了Book类的完整代码。

// Book.java
package com.jdojo.binding;

import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;

public class Book {
        private StringProperty title = new SimpleStringProperty(this,
               "title", "Unknown");
        private DoubleProperty price = new SimpleDoubleProperty(this,
                "price", 0.0);
        private ReadOnlyStringWrapper ISBN = new ReadOnlyStringWrapper(this,
                "ISBN", "Unknown");

        public Book() {
        }

        public Book(String title, double price, String ISBN) {
                this.title.set(title);
                this.price.set(price);
                this.ISBN.set(ISBN);
        }

        public final String getTitle() {
                return title.get();
        }

        public final void setTitle(String title) {
                this.title.set(title);
        }

        public final StringProperty titleProperty() {
                return title;
        }

        public final double getprice() {
                return price.get();
        }

        public final void setPrice(double price) {
                this.price.set(price);
        }

        public final DoubleProperty priceProperty() {
                return price;
        }

        public final String getISBN() {
                return ISBN.get();
        }

        public final ReadOnlyStringProperty ISBNProperty() {
                return ISBN.getReadOnlyProperty();
        }
}

Listing 2-3A Book Class with Two Read/Write and a Read-Only Properties

清单 2-4 测试了Book类的属性。它创建一个Book对象,打印细节,更改一些属性,然后再次打印细节。注意printDetails()方法的ReadOnlyProperty参数类型的使用。所有的属性类都直接或间接地实现了ReadOnlyProperty接口。

属性实现类的toString()方法返回一个格式良好的字符串,该字符串包含属性的所有相关信息。我没有使用 property 对象的toString()方法,因为我想向您展示 JavaFX 属性的不同方法的用法。

// BookPropertyTest.java
package com.jdojo.binding;

import javafx.beans.property.ReadOnlyProperty;

public class BookPropertyTest {
        public static void main(String[] args) {
                Book book = new Book("Harnessing JavaFX", 9.99,
                          "0123456789");

                System.out.println("After creating the Book object...");

                // Print Property details
                printDetails(book.titleProperty());
                printDetails(book.priceProperty());
                printDetails(book.ISBNProperty());

                // Change the book's properties
                book.setTitle("Harnessing JavaFX 17.0");
                book.setPrice(9.49);

                System.out.println(
                         "\nAfter changing the Book properties...");

                // Print Property details
                printDetails(book.titleProperty());
                printDetails(book.priceProperty());
                printDetails(book.ISBNProperty());
        }

        public static void printDetails(ReadOnlyProperty<?> p) {
                String name = p.getName();
                Object value = p.getValue();
                Object bean = p.getBean();
                String beanClassName = (bean == null)?
                          "null":bean.getClass().getSimpleName();
                String propClassName = p.getClass().getSimpleName();

                System.out.print(propClassName);
                System.out.print("[Name:" + name);
                System.out.print(", Bean Class:" + beanClassName);
                System.out.println(", Value:" + value + "]");
        }
}
After creating the Book object...
SimpleStringProperty[Name:title, Bean Class:Book, Value:Harnessing JavaFX]
SimpleDoubleProperty[Name:price, Bean Class:Book, Value:9.99]
ReadOnlyPropertyImpl[Name:ISBN, Bean Class:Book, Value:0123456789]

After changing the Book properties...
SimpleStringProperty[Name:title, Bean Class:Book, Value:Harnessing JavaFX 17.0]
SimpleDoubleProperty[Name:price, Bean Class:Book, Value:9.49]
ReadOnlyPropertyImpl[Name:ISBN, Bean Class:Book, Value:0123456789]

Listing 2-4A Test Class to Test Properties of the Book Class

了解属性类层次结构

在开始使用 JavaFX 属性和绑定 API 之前,理解它们的一些核心类和接口非常重要。图 2-1 显示了 properties API 核心接口的类图。你不需要在你的程序中直接使用这些接口。这些接口的专用版本和实现它们的类是存在的,并且可以直接使用。

img/336502_2_En_2_Fig1_HTML.jpg

图 2-1

JavaFX 属性 API 中核心接口的类图

JavaFX 属性 API 中的类和接口分布在不同的包中。那些包是javafx.beansjavafx.beans.bindingjavafx.beans.propertyjavafx.beans.value

Observable接口位于属性 API 的顶部。一个Observable包装内容,可以观察到它的内容失效。Observable接口有两个方法来支持这一点。它的addListener()方法允许您添加一个InvalidationListener。当Observable的内容无效时,调用InvalidationListenerinvalidated()方法。可以使用removeListener()方法移除InvalidationListener

Tip

所有 JavaFX 属性都是可观察的。

只有当其内容的状态从有效变为无效时,Observable才会生成无效事件。也就是说,一行中的多个失效应该只生成一个失效事件。JavaFX 中的属性类遵循这个原则。

Tip

一个Observable产生一个失效事件并不一定意味着它的内容发生了变化。意思就是它的内容因为某种原因是无效的。例如,对一个ObservableList进行排序可能会生成一个无效事件。排序不会改变列表的内容;它只是对内容进行了重新排序。

ObservableValue接口继承自Observable接口。一个ObservableValue包装了一个值,可以观察到它的变化。它有一个getValue()方法,返回它包装的值。它生成失效事件和变更事件。当ObservableValue中的值不再有效时,生成失效事件。值更改时会生成更改事件。您可以将一个ChangeListener注册到一个ObservableValue。每当ChangeListener的值发生变化时,就会调用changed()方法。changed()方法接收三个参数:对ObservableValue的引用、旧值和新值。

一个ObservableValue可以缓慢或急切地重新计算它的值。在惰性策略中,当它的值变得无效时,它不知道该值是否已经改变,直到该值被重新计算;下次读取该值时会重新计算。例如,使用一个ObservableValuegetValue()方法会使它重新计算它的值,如果这个值是无效的并且它使用了一个懒惰策略。在 eager 策略中,一旦值变得无效,就会重新计算。

为了生成无效事件,一个ObservableValue可以使用惰性或急切评估。懒惰评估更有效率。然而,生成变更事件会迫使一个ObservableValue立即重新计算它的值(一个急切的评估),因为它必须将新值传递给注册的变更监听器。

ReadOnlyProperty接口增加了getBean()getName()方法。清单 2-4 展示了它们的用法。getBean()方法返回包含属性对象的 bean 的引用。getName()方法返回属性的名称。只读属性实现此接口。

一个WritableValue包装了一个值,可以分别使用它的getValue()setValue()方法读取和设置该值。读/写属性实现此接口。

Property接口继承自ReadOnlyPropertyWritableValue接口。它添加了以下五种方法来支持绑定:

  • void bind(ObservableValue<? extends T> observable)

  • void unbind()

  • void bindBidirectional(Property<T> other)

  • void unbindBidirectional(Property<T> other)

  • boolean isBound()

bind()方法在这个Property和指定的ObservableValue之间添加一个单向绑定。如果存在的话,unbind()方法删除这个Property的单向绑定。

bindBidirectional()方法在这个Property和指定的Property之间创建一个双向绑定。unbindBidirectional()方法移除双向绑定。

注意bind()bindBidirectional()方法的参数类型的不同。同一类型的PropertyObservableValue之间可以创建单向绑定,只要它们通过继承相关。但是,只能在同一类型的两个属性之间创建双向绑定。

如果Property被绑定,isBound()方法返回true。否则返回false

Tip

所有读/写 JavaFX 属性都支持绑定。

图 2-2 显示了 JavaFX 中 integer 属性的部分类图。该图让您了解 JavaFX 属性 API 的复杂性。您不需要学习属性 API 中的所有类。在您的应用程序中,您将只使用其中的几个。

img/336502_2_En_2_Fig2_HTML.jpg

图 2-2

整数属性的类图

处理属性失效事件

当属性值的状态第一次从有效变为无效时,属性会生成一个无效事件。JavaFX 中的属性使用惰性计算。当无效属性再次变为无效时,不会生成失效事件。无效属性在重新计算时变得有效,例如,通过调用其get()getValue()方法。

清单 2-5 提供了程序来演示何时为属性生成失效事件。这个程序包含了足够的注释来帮助你理解它的逻辑。一开始,它创建一个名为counterIntegerProperty:

IntegerProperty counter = new SimpleIntegerProperty(100);

一个InvalidationListener被添加到counter属性:

counter.addListener(InvalidationTest::invalidated);

当您创建属性对象时,它是有效的。当您将counter属性更改为 101 时,它会触发一个失效事件。此时,counter属性变得无效。当您将它的值更改为 102 时,它不会触发无效事件,因为它已经无效了。当您使用get()方法读取counter值时,它再次变得有效。现在您为counter设置了相同的值 102,它不会触发一个无效事件,因为该值并没有真正改变。counter属性仍然有效。最后,您将它的值改为一个不同的值,果然,一个无效事件被触发。

Tip

您并不局限于在一个属性中只添加一个失效侦听器。您可以根据需要添加任意数量的失效侦听器。一旦你完成了一个无效监听器,确保通过调用Observable接口的removeListener()方法来移除它;否则,可能会导致内存泄漏。

// InvalidationTest.java
// Listing part of the example sources download for the book
Before changing the counter value-1
Counter is invalid.
After changing the counter value-1

Before changing the counter value-2
After changing the counter value-2
Counter value = 102

Before changing the counter value-3
After changing the counter value-3

Before changing the counter value-4
Counter is invalid.
After changing the counter value-4

Listing 2-5Testing Invalidation Events for Properties

处理属性更改事件

您可以注册一个ChangeListener来接收关于属性更改事件的通知。每次属性值更改时,都会触发属性更改事件。一个ChangeListenerchanged()方法接收三个值:属性对象的引用、旧值和新值。

让我们运行一个类似的测试用例来测试属性变更事件,就像上一节中对失效事件所做的那样。清单 2-6 中的程序演示了为属性生成的变更事件。

// ChangeTest.java
package com.jdojo.binding;

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

public class ChangeTest {
        public static void main(String[] args) {
            IntegerProperty counter = new SimpleIntegerProperty(100);

            // Add a change listener to the counter property
            counter.addListener(ChangeTest::changed);

            System.out.println("\nBefore changing the counter value-1");
            counter.set(101);
            System.out.println("After changing the counter value-1");

            System.out.println("\nBefore changing the counter value-2");
            counter.set(102);
            System.out.println("After changing the counter value-2");

            // Try to set the same value
            System.out.println("\nBefore changing the counter value-3");
            counter.set(102); // No change event is fired.
            System.out.println("After changing the counter value-3");

            // Try to set a different value
            System.out.println("\nBefore changing the counter value-4");
            counter.set(103);
            System.out.println("After changing the counter value-4");
        }

        public static void changed(ObservableValue<? extends Number> prop,
                                   Number oldValue,
                                   Number newValue) {
            System.out.print("Counter changed: ");
            System.out.println("Old = " + oldValue +
                    ", new = " + newValue);
        }
}
Before changing the counter value-1
Counter changed: Old = 100, new = 101
After changing the counter value-1

Before changing the counter value-2
Counter changed: Old = 101, new = 102
After changing the counter value-2

Before changing the counter value-3
After changing the counter value-3

Before changing the counter value-4
Counter changed: Old = 102, new = 103
After changing the counter value-4

Listing 2-6Testing Change Events for Properties

一开始,程序创建一个名为counterIntegerProperty:

IntegerProperty counter = new SimpleIntegerProperty(100);

加个ChangeListener有个小技巧。IntegerPropertyBase类中的addListener()方法声明如下:

void addListener(ChangeListener<? super Number> listener)

这意味着如果你使用泛型,那么一个IntegerPropertyChangeListener必须按照Number类或者Number类的超类来编写。向counter属性添加ChangeListener的三种方法如下所示:

// Method-1: Using generics and the Number class
counter.addListener(new ChangeListener<Number>() {
        @Override
        public void changed(ObservableValue<? extends Number> prop,
                            Number oldValue,
                            Number newValue) {
                System.out.print("Counter changed: ");
                System.out.println("Old = " + oldValue +
                         ", new = " + newValue);
        }});

// Method-2: Using generics and the Object class
counter.addListener( new ChangeListener<Object>() {
        @Override
        public void changed(ObservableValue<? extends Object> prop,
                            Object oldValue,
                            Object newValue) {
                System.out.print("Counter changed: ");
                System.out.println("Old = " + oldValue +
                         ", new = " + newValue);
        }});

// Method-3: Not using generics. It may generate compile-time warnings.
counter.addListener(new ChangeListener() {
        @Override
        public void changed(ObservableValue prop,
                            Object oldValue,
                            Object newValue) {
                System.out.print("Counter changed: ");
                System.out.println("Old = " + oldValue +
                         ", new = " + newValue);
        }});

清单 2-6 使用了第一种方法,它利用了泛型;如您所见,ChangeTest类中的changed()方法的签名与method-1中的changed()方法签名相匹配。我使用了一个带有方法引用的 lambda 表达式来添加一个ChangeListener,如下所示:

counter.addListener(ChangeTest::changed);

前面的输出显示,当属性值更改时,将触发属性更改事件。用相同的值调用set()方法不会触发属性更改事件。

与生成失效事件不同,属性使用对其值的急切评估来生成更改事件,因为它必须将新值传递给属性更改侦听器。下一节讨论属性对象如何评估它的值,如果它既有无效侦听器又有更改侦听器的话。

处理失效和变更事件

当您必须决定是使用失效侦听器还是更改侦听器时,您需要考虑性能。通常,失效侦听器比更改侦听器性能更好。原因是双重的:

  • 失效侦听器使得延迟计算值成为可能。

  • 一行中的多个无效仅触发一个无效事件。

但是,使用哪个监听器取决于当前的情况。一个经验法则是,如果您在失效事件处理程序中读取属性的值,您应该使用一个更改侦听器。当您读取失效侦听器中的属性值时,它会触发该值的重新计算,这是在触发更改事件之前自动完成的。如果不需要读取属性的值,请使用失效侦听器。

清单 2-7 有一个程序向IntegerProperty添加一个无效监听器和一个变更监听器。这个程序是清单 2-5 和 2-6 的组合。下面的输出显示,当属性值改变时,失效和改变这两个事件总是被触发。这是因为更改事件会在更改后立即使属性有效,并且值的下一次更改会触发一个无效事件,当然还有一个更改事件。

// ChangeAndInvalidationTest.java
// Listing part of the example sources download for the book

Before changing the counter value-1
Counter is invalid.
Counter changed: old = 100, new = 101
After changing the counter value-1

Before changing the counter value-2
Counter is invalid.
Counter changed: old = 101, new = 102
After changing the counter value-2

Before changing the counter value-3
After changing the counter value-3

Before changing the counter value-4
Counter is invalid.
Counter changed: old = 102, new = 103
After changing the counter value-4

Listing 2-7Testing Invalidation and Change Events for Properties Together

在 JavaFX 中使用绑定

在 JavaFX 中,绑定是一个计算结果为值的表达式。它由一个或多个被称为其依赖性的可观察值组成。绑定观察其依赖关系的变化,并自动重新计算其值。JavaFX 对所有绑定都使用惰性求值。当绑定最初被定义或者当它的依赖关系改变时,它的值被标记为无效。无效绑定的值在下次被请求时计算,通常使用它的get()getValue()方法。JavaFX 中的所有属性类都内置了对绑定的支持。

让我们看一个 JavaFX 中绑定的简单例子。考虑以下表示两个整数 x 和 y 之和的表达式:

x + y

表达式 x + y 表示一个绑定,它有两个依赖项:x 和 y

sum = x + y

为了在 JavaFX 中实现前面的逻辑,需要创建两个IntegerProperty变量:xy:

IntegerProperty x = new SimpleIntegerProperty(100);
IntegerProperty y = new SimpleIntegerProperty(200);

以下语句创建了一个名为sum的绑定,表示xy的总和:

NumberBinding sum = x.add(y);

一个绑定有一个isValid()方法,如果它有效,则返回true;否则,它返回false。您可以使用方法intValue()longValue()floatValue()doubleValue()分别获得NumberBinding的值,如intlongfloatdouble

清单 2-8 中的程序展示了如何基于前面的讨论创建和使用绑定。当 sum 绑定被创建时,它是无效的,并且它不知道它的值。从输出中可以明显看出这一点。一旦您使用sum.initValue()方法请求了它的值,它就会计算它的值并将自己标记为有效。当您更改它的一个依赖项时,它将变得无效,直到您再次请求它的值。

// BindingTest.java
package com.jdojo.binding;

import javafx.beans.binding.NumberBinding;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;

public class BindingTest {
        public static void main(String[] args) {
            IntegerProperty x = new SimpleIntegerProperty(100);
            IntegerProperty y = new SimpleIntegerProperty(200);

            // Create a binding: sum = x + y
            NumberBinding sum = x.add(y);

            System.out.println("After creating sum");
            System.out.println("sum.isValid(): " + sum.isValid());

            // Let us get the value of sum, so it computes its value and
            // becomes valid
            int value = sum.intValue();

            System.out.println("\nAfter requesting value");
            System.out.println("sum.isValid(): " + sum.isValid());
            System.out.println("sum = " + value);

            // Change the value of x
            x.set(250);

            System.out.println("\nAfter changing x");
            System.out.println("sum.isValid(): " + sum.isValid());

            // Get the value of sum again
            value = sum.intValue();

            System.out.println("\nAfter requesting value");
            System.out.println("sum.isValid(): " + sum.isValid());
            System.out.println("sum = " + value);
        }
}

After creating sum
sum.isValid(): false

After requesting value
sum.isValid(): true
sum = 300

After changing x
sum.isValid(): false

After requesting value
sum.isValid(): true
sum = 450

Listing 2-8Using a Simple Binding

一个绑定在内部将失效侦听器添加到它的所有依赖项中(清单 2-9 )。当它的任何依赖项无效时,它会将自己标记为无效。无效的绑定并不意味着它的值已经改变。这意味着下次请求值时,它需要重新计算它的值。

在 JavaFX 中,还可以将属性绑定到绑定。回想一下,绑定是一个自动与其依赖项同步的表达式。使用此定义,绑定属性是其值基于表达式计算的属性,当依赖关系更改时,该属性会自动同步。假设您有三个属性,x、y 和 z,如下所示:

IntegerProperty x = new SimpleIntegerProperty(10);
IntegerProperty y = new SimpleIntegerProperty(20);
IntegerProperty z = new SimpleIntegerProperty(60);

您可以使用Property接口的bind()方法将属性z绑定到表达式x + y,如下所示:

z.bind(x.add(y));

注意,你不能写z.bind(x + y),因为+操作符不知道如何将两个IntegerProperty对象的值相加。您需要使用绑定 API 来创建绑定表达式,就像您在前面的语句中所做的那样。我将很快介绍绑定 API 的细节。

现在,当xy或两者都改变时,z属性无效。下次请求z的值时,它会重新计算表达式x.add(y)来获得它的值。

您可以使用Property接口的unbind()方法来解除绑定属性。对未绑定或从未绑定的属性调用unbind()方法没有任何效果。您可以按如下方式解除z属性的绑定:

z.unbind();

解除绑定后,属性表现为普通属性,独立保持其值。解除属性绑定会断开属性与其依赖项之间的链接。

// BoundProperty.java
package com.jdojo.binding;

import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;

public class BoundProperty {
        public static void main(String[] args) {
            IntegerProperty x = new SimpleIntegerProperty(10);
            IntegerProperty y = new SimpleIntegerProperty(20);
            IntegerProperty z = new SimpleIntegerProperty(60);
            z.bind(x.add(y));
            System.out.println("After binding z: Bound = " + z.isBound() +
                ", z = " + z.get());

            // Change x and y
            x.set(15);
            y.set(19);
            System.out.println("After changing x and y: Bound = " +
                    z.isBound() + ", z = " + z.get());
            // Unbind z
            z.unbind();

            // Will not affect the value of z as it is not bound to
                // x and y anymore
            x.set(100);
            y.set(200);
            System.out.println("After unbinding z: Bound = " +
                    z.isBound() + ", z = " + z.get());
        }
}
After binding z: Bound = true, z = 30
After changing x and y: Bound = true, z = 34
After unbinding z: Bound = false, z = 34

Listing 2-9Binding a Property

单向和双向绑定

绑定有一个方向,即传播更改的方向。JavaFX 支持两种类型的属性绑定:单向绑定双向绑定。单向绑定只在一个方向起作用;依赖项中的更改会传播到绑定属性,反之亦然。双向绑定在两个方向上都起作用;依赖项的更改反映在属性中,反之亦然。

接口Propertybind()方法在属性和ObservableValue之间创建了一个单向绑定,这可能是一个复杂的表达式。bindBidirectional()方法在一个属性和同类型的另一个属性之间创建一个双向绑定。

假设 x,y,z 是IntegerProperty的三个实例。考虑以下绑定:

z = x + y

在 JavaFX 中,上述绑定只能表示为单向绑定,如下所示:

z.bind(x.add(y));

假设您能够在前一种情况下使用双向绑定。如果你能将z的值改为 100,你将如何反过来计算xy的值?因为z100,所以xy有无限多种可能的组合,例如,(99,1),(98,2),(101,–1),(200,–100),等等。将绑定属性的更改传播到其依赖项是不可能得到可预测的结果的。这就是将属性绑定到表达式只允许作为单向绑定的原因。

单向绑定有一个限制。一旦属性具有单向绑定,就不能直接更改属性的值;它的值必须根据绑定自动计算。在直接更改其值之前,必须先解除绑定。以下代码片段显示了这种情况:

IntegerProperty x = new SimpleIntegerProperty(10);
IntegerProperty y = new SimpleIntegerProperty(20);
IntegerProperty z = new SimpleIntegerProperty(60);
z.bind(x.add(y));

z.set(7878); // Will throw a RuntimeException

要直接更改z的值,您可以键入以下内容:

z.unbind();  // Unbind z first
z.set(7878); // OK

单向绑定还有另一个限制。一个属性一次只能有一个单向绑定。考虑属性z的以下两个单向绑定。假设xyzabIntegerProperty的五个实例:

z = x + y
z = a + b

如果xyab是四个不同的属性,那么前面显示的z的绑定是不可能的。想想x = 1y = 2a = 3b = 4。能定义一下z的值吗?会是 3 还是 7?这就是一个属性一次只能有一个单向绑定的原因。

重新绑定已经具有单向绑定的属性会解除以前的绑定。例如,下面的代码片段就很好:

IntegerProperty x = new SimpleIntegerProperty(1);
IntegerProperty y = new SimpleIntegerProperty(2);
IntegerProperty a = new SimpleIntegerProperty(3);
IntegerProperty b = new SimpleIntegerProperty(4);
IntegerProperty z = new SimpleIntegerProperty(0);

z.bind(x.add(y));
System.out.println("z = " + z.get());

z.bind(a.add(b)); // Will unbind the previous binding
System.out.println("z = " + z.get());
z = 3
z = 7

双向绑定在两个方向上都起作用。它有一些限制。它只能在相同类型的属性之间创建。也就是说,双向绑定只能是类型x = yy = x,其中xy属于同一类型。

双向绑定消除了单向绑定的一些限制。一个属性可以同时有多个双向绑定。双向绑定属性也可以独立更改;该更改反映在绑定到该属性的所有属性中。也就是说,使用双向绑定,以下绑定是可能的:

x = y
x = z

在前一种情况下,xyz的值将总是同步的。也就是说,在建立绑定后,所有三个属性将具有相同的值。您也可以在xyz之间建立双向绑定,如下所示:

x = z
z = y

现在出现了一个问题。前面的两个双向绑定最终会在xyz中具有相同的值吗?答案是否定的。最后一个双向绑定中右侧操作数的值(例如,请参见前面的表达式)是所有参与属性包含的值。我来阐述一下这一点。假设x为 1,y为 2,z为 3,则有如下双向绑定:

x = y
x = z

第一次绑定x = y,将设置x的值等于y的值。此时,xy将为 2。第二个绑定x = z,将设置x的值等于z的值。也就是xz会是 3。然而,x已经有了到y的双向绑定,这也将把x的新值 3 传播到y。因此,这三个属性的值将与z的值相同。清单 2-10 中的程序展示了如何使用双向绑定。

// BidirectionalBinding.java
package com.jdojo.binding;

import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;

public class BidirectionalBinding {
        public static void main(String[] args) {
            IntegerProperty x = new SimpleIntegerProperty(1);
            IntegerProperty y = new SimpleIntegerProperty(2);
            IntegerProperty z = new SimpleIntegerProperty(3);

            System.out.println("Before binding:");
            System.out.println("x=" + x.get() + ", y=" + y.get() +
                    ", z=" + z.get());

            x.bindBidirectional(y);
            System.out.println("After binding-1:");
            System.out.println("x=" + x.get() + ", y=" + y.get() +
                    ", z=" + z.get());

            x.bindBidirectional(z);
            System.out.println("After binding-2:");
            System.out.println("x=" + x.get() + ", y=" + y.get() +
                    ", z=" + z.get());

            System.out.println("After changing z:");
            z.set(19);
            System.out.println("x=" + x.get() + ", y=" + y.get() +
                    ", z=" + z.get());

            // Remove bindings
            x.unbindBidirectional(y);
            x.unbindBidirectional(z);
            System.out.println(
                    "After unbinding and changing them separately:");
            x.set(100);
            y.set(200);
            z.set(300);
            System.out.println("x=" + x.get() + ", y=" + y.get() +
                    ", z=" + z.get());
        }
}
Before binding:
x=1, y=2, z=3
After binding-1:
x=2, y=2, z=3
After binding-2:
x=3, y=3, z=3
After changing z:
x=19, y=19, z=19
After unbinding and changing them separately:
x=100, y=200, z=300

Listing 2-10Using Bidirectional Bindings

与单向绑定不同,创建双向绑定时,不会移除以前的绑定,因为一个属性可以有多个双向绑定。您必须使用unbindBidirectional()方法移除所有双向绑定,为属性的每个双向绑定调用一次该方法,如下所示:

// Create bidirectional bindings
x.bindBidirectional(y);
x.bindBidirectional(z);

// Remove bidirectional bindings
x.unbindBidirectional(y);
x.unbindBidirectional(z);

了解绑定 API

前几节简单快速地介绍了 JavaFX 中的绑定。现在是时候深入挖掘并详细理解绑定 API 了。绑定 API 分为两类:

  • 高级绑定 API

  • 低级绑定 API

高级绑定 API 允许您使用 JavaFX 类库定义绑定。对于大多数用例,您可以使用高级绑定 API。

有时,现有的 API 不足以定义绑定。在这些情况下,使用低级绑定 API。在低级绑定 API 中,从现有的绑定类派生一个绑定类,并编写自己的逻辑来定义绑定。

高级绑定 API

高级绑定 API 由两部分组成:Fluent API 和Bindings类。您可以只使用 Fluent API、只使用Bindings类或者结合使用两者来定义绑定。我们来看两部分,先分开再合起来。

使用 Fluent API

Fluent API 由不同接口和类中的几个方法组成。这个 API 被称为 Fluent ,因为方法名、它们的参数和返回类型已经被设计成允许流畅地编写代码。与使用非流畅 API 编写的代码相比,使用流畅 API 编写的代码可读性更好。设计一个流畅的 API 需要更多的时间。流畅的 API 对开发者更友好,对设计者不友好。fluent API 的一个特性是方法链接;您可以将单独的方法调用合并到一个语句中。考虑下面的代码片段来添加三个属性xyz。使用非流畅 API 的代码可能如下所示:

x.add(y);
x.add(z);

使用 Fluent API,前面的代码可能如下所示,这使读者更好地理解作者的意图:

x.add(y).add(z);

图 2-3 显示了IntegerBindingIntegerProperty类的类图。图中省略了一些属于IntegerProperty类层次的接口和类。longfloatdouble类型的类图类似。

img/336502_2_En_2_Fig3_HTML.jpg

图 2-3

IntegerBindingIntegerProperty的部分类图

ObservableNumberValueBinding接口到IntegerBinding类的类和接口是int数据类型的流畅绑定 API 的一部分。起初,看起来好像有很多课要学。大多数类和接口存在于属性和绑定 API 中,以避免原始值的装箱和拆箱。要学习流畅的绑定 API,需要重点关注XXXExpressionXXXBinding类和接口。XXXExpression类拥有用于创建绑定表达式的方法。

绑定接口

Binding接口的一个实例表示一个值,该值是从一个或多个称为依赖关系的源中导出的。它有以下四种方法:

  • public void dispose()

  • public ObservableList<?> getDependencies()

  • public void invalidate()

  • public boolean isValid()

方法dispose()的实现是可选的,它向一个Binding表明它将不再被使用,因此它可以删除对其他对象的引用。绑定 API 在内部使用弱失效侦听器,因此不需要调用此方法。

方法getDependencies()的实现是可选的,它返回不可修改的依赖关系ObservableList。它仅用于调试目的。不应在生产代码中使用此方法。

invalidate()方法的调用会使Binding无效。如果一个Binding有效,isValid()方法返回true。否则返回false

数字绑定接口

NumberBinding接口是一个标记接口,其实例包装了一个intlongfloatdouble类型的数值。由DoubleBindingFloatBindingIntegerBindingLongBinding类实现。

可观察的界面

ObservableNumberValue接口的一个实例包装了一个intlongfloatdouble类型的数值。它提供了以下四种获取值的方法:

  • double doubleValue()

  • float floatValue()

  • int intValue()

  • long longValue()

您使用了清单 2-8 中提供的intValue()方法从NumberBinding实例中获取int值。您使用的代码应该是

IntegerProperty x = new SimpleIntegerProperty(100);
IntegerProperty y = new SimpleIntegerProperty(200);

// Create a binding: sum = x + y
NumberBinding sum = x.add(y);
int value = sum.intValue(); // Get the int value

ObservableIntegerValue 接口

ObservableIntegerValue接口定义了一个返回特定类型的int值的get()方法。

数字表达式接口

NumberExpression接口包含几个使用流畅风格创建绑定的便利方法。它有超过 50 个方法,其中大多数都是重载的。这些方法返回一个Binding类型,比如NumberBindingBooleanBinding等等。表 2-2 列出了NumberExpression界面中的方法。大多数方法都是重载的。该表没有显示方法参数。

表 2-2

NumberExpression界面中方法的总结

|

方法名称

|

返回类型

|

描述

add()``subtract()``multiply()``divide() NumberBinding 这些方法创建一个新的NumberBinding,它是NumberExpression的和、差、积和除,以及一个数值或一个ObservableNumberValue
greaterThan()``greaterThanOrEqualTo()``isEqualTo()``isNotEqualTo()``lessThan()``lessThanOrEqualTo() BooleanBinding 这些方法创建一个新的BooleanBinding,存储NumberExpression和一个数值或ObservableNumberValue的比较结果。方法名足够清楚,可以告诉我们它们执行哪种比较。
negate() NumberBinding 它创建了一个新的NumberBinding,它是对NumberExpression的否定。
asString() StringBinding 它创建了一个StringBinding,将NumberExpression的值保存为一个String对象。此方法还支持基于区域设置的字符串格式。

在使用算术表达式定义绑定时,NumberExpression接口中的方法允许混合类型(intlongfloatdouble)。当该接口中方法的返回类型为NumberBinding时,实际返回的类型为IntegerBindingLongBindingFloatBindingDoubleBinding。算术表达式的绑定类型由与 Java 编程语言相同的规则决定。表达式的结果取决于操作数的类型。规则如下:

  • 如果操作数之一是double,则结果是double

  • 如果操作数中没有一个是double,而其中一个是float,那么结果就是一个float

  • 如果操作数都不是doublefloat,并且其中一个是long,则结果是long

  • 否则,结果是一个int

考虑以下代码片段:

IntegerProperty x = new SimpleIntegerProperty(1);
IntegerProperty y = new SimpleIntegerProperty(2);
NumberBinding sum = x.add(y);
int value = sum.intValue();

数字表达式x.add(y)只涉及int操作数(xy属于int类型)。因此,根据前面的规则,它的结果是一个int值,并且它返回一个IntegerBinding对象。因为NumberExpression中的add()方法将返回类型指定为NumberBinding,所以使用了一个NumberBinding类型来存储结果。您必须从ObservableNumberValue接口使用intValue()方法。您可以重写前面的代码片段,如下所示:

IntegerProperty x = new SimpleIntegerProperty(1);
IntegerProperty y = new SimpleIntegerProperty(2);

// Casting to IntegerBinding is safe
IntegerBinding sum = (IntegerBinding)x.add(y);
int value = sum.get();

NumberExpressionBase类是NumberExpression接口的一个实现。IntegerExpression类扩展了NumberExpressionBase类。它重写其超类中的方法,以提供特定于类型的返回类型。

清单 2-11 中的程序创建了一个DoubleBinding来计算圆的面积。它还创建了一个DoubleProperty并将其绑定到同一个表达式来计算面积。您可以选择是使用Binding对象还是绑定属性对象。这个程序向你展示了这两种方法。

// CircleArea.java
package com.jdojo.binding;

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

public class CircleArea {
        public static void main(String[] args) {
            DoubleProperty radius = new SimpleDoubleProperty(7.0);

            // Create a binding for computing area of the circle
            DoubleBinding area =
                    radius.multiply(radius).multiply(Math.PI);

            System.out.println("Radius = " + radius.get() +
                          ", Area = " + area.get());

            // Change the radius
            radius.set(14.0);
            System.out.println("Radius = " + radius.get() +
                    ", Area = " + area.get());

            // Create a DoubleProperty and bind it to an expression
            // that computes the area of the circle
            DoubleProperty area2 = new SimpleDoubleProperty();
            area2.bind(radius.multiply(radius).multiply(Math.PI));
            System.out.println("Radius = " + radius.get() +
                          ", Area2 = " + area2.get());
        }
}
Radius = 7.0, Area = 153.93804002589985
Radius = 14.0, Area = 615.7521601035994
Radius = 14.0, Area2 = 615.7521601035994

Listing 2-11Computing the Area of a Circle from Its Radius Using a Fluent Binding API

字符串绑定

包含绑定 API 中支持String类型绑定的类的类图如图 2-4 所示。

img/336502_2_En_2_Fig4_HTML.jpg

图 2-4

StringBinding的部分类图

ObservableStringValue接口声明了一个返回类型为Stringget()方法。StringExpression类中的方法允许您使用流畅的风格创建绑定。提供了一些方法来将一个对象连接到StringExpression,比较两个字符串,检查null,等等。它有两种方法获取它的值:getValue()getValueSafe()。两者都返回当前值。然而,当当前值为null.时,后者返回空的String

清单 2-12 中的程序展示了如何使用StringBindingStringExpression类。StringExpression类中的concat()方法接受一个Object类型作为参数。如果参数是ObservableValue,当参数改变时StringExpression自动更新。注意asString()方法在radiusarea属性上的使用。对一个NumberExpressionasString()方法返回一个StringBinding

// StringExpressionTest.java
package com.jdojo.binding;

import java.util.Locale;
import javafx.beans.binding.StringExpression;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;

public class StringExpressionTest {
        public static void main(String[] args) {
            DoubleProperty radius = new SimpleDoubleProperty(7.0);
            DoubleProperty area = new SimpleDoubleProperty(0);
            StringProperty initStr = new SimpleStringProperty(
                    "Radius = ");

            // Bind area to an expression that computes the area of
            // the circle
            area.bind(radius.multiply(radius).multiply(Math.PI));

            // Create a string expression to describe the circle
            StringExpression desc = initStr.concat(radius.asString())
                .concat(", Area = ")
               .concat(area.asString(Locale.US, "%.2f"));

            System.out.println(desc.getValue());

            // Change the radius
            radius.set(14.0);
            System.out.println(desc.getValue());
        }
}
Radius = 7.0, Area = 153.94
Radius = 14.0, Area = 615.75

Listing 2-12Using StringBinding and StringExpression

对象表达式对象绑定

现在是时候让ObjectExpressionObjectBinding类创建任何类型对象的绑定了。他们的类图与StringExpressionStringBinding类非常相似。ObjectExpression类有比较对象是否相等和检查空值的方法。清单 2-13 中的程序展示了如何使用ObjectBinding类。

// ObjectBindingTest.java
package com.jdojo.binding;

import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;

public class ObjectBindingTest {
        public static void main(String[] args) {
            Book b1 = new Book("J1", 90, "1234567890");
            Book b2 = new Book("J2", 80, "0123456789");
            ObjectProperty<Book> book1 = new SimpleObjectProperty<>(b1);
            ObjectProperty<Book> book2 = new SimpleObjectProperty<>(b2);

            // Create a binding that computes if book1 and book2 are equal
            BooleanBinding isEqual = book1.isEqualTo(book2);
            System.out.println(isEqual.get());

            book2.set(b1);
            System.out.println(isEqual.get());
        }
}
false
true

Listing 2-13Using the ObjectBinding Class

BooleanExpressionBooleanBinding

BooleanExpression类包含诸如and()or()not()之类的方法,允许您在表达式中使用布尔逻辑运算符。它的isEqualTo()isNotEqualTo()方法可以让你比较一个BooleanExpression和另一个ObservableBooleanValue。一个BooleanExpression的结果是truefalse

清单 2-14 中的程序展示了如何使用BooleanExpression类。它使用流畅的风格创建一个布尔表达式x > y && y <> z。注意,greaterThan()isNotEqualTo()方法是在NumberExpression接口中定义的。该程序只使用来自BooleanExpression类的and()方法。

// BooelanExpressionTest.java
package com.jdojo.binding;

import javafx.beans.binding.BooleanExpression;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;

public class BooelanExpressionTest {
        public static void main(String[] args) {
            IntegerProperty x = new SimpleIntegerProperty(1);
            IntegerProperty y = new SimpleIntegerProperty(2);
            IntegerProperty z = new SimpleIntegerProperty(3);

            // Create a boolean expression for x > y && y <> z
            BooleanExpression condition =
                    x.greaterThan(y).and(y.isNotEqualTo(z));

            System.out.println(condition.get());

            // Make the condition true by setting x to 3
            x.set(3);
            System.out.println(condition.get());
        }
}
false
true

Listing 2-14Using BooleanExpression and BooleanBinding

在表达式中使用三元运算

Java 编程语言提供了一个三元运算符(condition?value1:value2),用于执行形式为 when-then-otherwise 的三元运算。JavaFX 绑定 API 为此提供了一个When类。使用When类的一般语法如下所示:

new When(condition).then(value1).otherwise(value2)

condition必须是一个ObservableBooleanValue。当condition计算结果为true时,它返回value1。否则返回value2value1value2的类型必须相同。值可以是常量或ObservableValue的实例。

让我们使用一个三元运算,根据一个IntegerProperty的值是偶数还是奇数,分别返回一个String evenodd。Fluent API 没有计算模数的方法。你必须自己做这件事。对整数执行除以 2 的整数除法,并将结果乘以 2。如果你得到同样的数字,这个数字是偶数。否则,数字是奇数。例如,使用整数除法,(7/2)*2 得到 6,而不是 7。清单 2-15 提供了完整的程序。

// TernaryTest.java
package com.jdojo.binding;

import javafx.beans.binding.When;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.binding.StringBinding;

public class TernaryTest {
        public static void main(String[] args) {
            IntegerProperty num = new SimpleIntegerProperty(10);
            StringBinding desc =
                    new When(num.divide(2).multiply(2).isEqualTo(num))
                                         .then("even")
                                         .otherwise("odd");

            System.out.println(num.get() + " is " + desc.get());

            num.set(19);
            System.out.println(num.get() + " is " + desc.get());
        }
}
10 is even
19 is odd

Listing 2-15Using the When Class to Perform a Ternary Operation

使用绑定实用程序类

Bindings类是一个助手类,用于创建简单的绑定。它由 150 多个静态方法组成。他们中的大多数都超载了几个变种。我不会一一列举或讨论。请参考在线 JavaFX API 文档以获得完整的方法列表。表 2-3 列出了Bindings类的方法及其描述。它排除了属于集合绑定的方法。

表 2-3

Bindings类中方法的总结

|

方法名称

|

描述

add()``subtract()``multiply()``divide() 它们通过对它的两个参数应用算术运算来创建一个绑定。至少有一个参数必须是ObservableNumberValue。如果参数之一是一个double,它的返回类型是DoubleBinding;否则,其返回类型为NumberBinding
and() 它通过对它的两个参数应用布尔运算and来创建一个BooleanBinding
bindBidirectional()``unbindBidirectional() 它们创建和删除两个属性之间的双向绑定。
concat() 它返回一个保存其参数串联值的StringExpression。它需要一个varargs参数。
convert() 它返回一个包装其参数的StringExpression
createXXXBinding() 它允许您创建一个XXX类型的定制绑定,其中XXX可以是BooleanDoubleFloatIntegerStringObject
equal()``notEqual()``equalIgnoreCase()``notEqualIgnoreCase() 他们创建了一个BooleanBinding,包装了两个参数相等或不相等的比较结果。这些方法的一些变体允许传递公差值。如果两个参数在公差范围内,则认为它们相等。通常,容差值用于比较浮点数。这些方法的忽略大小写变量只对String类型有效。
format() 它创建一个StringExpression,保存根据指定格式String格式化的多个对象的值。
greaterThan()``greaterThanOrEqual()``lessThan()``lessThanOrEqual() 他们创建一个BooleanBinding来包装比较参数的结果。
isNotNull``isNull 他们创建一个BooleanBinding来包装与null进行比较的结果。
max()``min() 它们创建一个绑定,保存该方法的两个参数的最大值和最小值。其中一个参数必须是ObservableNumberValue
negate() 它创建一个NumberBinding来保存一个ObservableNumberValue的否定。
not() 它创建一个BooleanBinding来保存一个ObservableBooleanValue的逆。
or() 它创建一个BooleanBinding,保存对它的两个ObservableBooleanValue参数应用条件or操作的结果。
selectXXX() 它创建一个绑定来选择嵌套属性。嵌套属性可以是类型a.b.c。绑定的值将是c。像a.b.c这样的表达式中涉及的类和属性必须是公共的。如果表达式的任何部分不可访问,因为它们不是公共的或者它们不存在,类型的默认值,例如,null表示Object type,空的String表示String type,0 表示数值类型,而false表示布尔类型,就是绑定的值。(后面我会讨论一个使用select()方法的例子。)
when() 它创建了一个将条件作为参数的When类的实例。

我们使用 Fluent API 的大多数例子也可以使用Bindings类编写。清单 2-16 中的程序类似于清单 2-12 中的程序。它使用了Bindings类,而不是 Fluent API。它使用multiply()方法计算面积,使用format()方法格式化结果。做同一件事可能有几种方法。为了格式化结果,您还可以使用Bindings.concat()方法,如下所示:

// BindingsClassTest.java
package com.jdojo.binding;

import java.util.Locale;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.StringExpression;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;

public class BindingsClassTest {
        public static void main(String[] args) {
            DoubleProperty radius = new SimpleDoubleProperty(7.0);
            DoubleProperty area = new SimpleDoubleProperty(0.0);

            // Bind area to an expression that computes the area of
            // the circle
            area.bind(Bindings.multiply(
                    Bindings.multiply(radius, radius), Math.PI));

            // Create a string expression to describe the circle
            StringExpression desc = Bindings.format(Locale.US,
                "Radius = %.2f, Area = %.2f", radius, area);

            System.out.println(desc.get());

            // Change the radius
            radius.set(14.0);
            System.out.println(desc.getValue());
        }
}
Radius = 7.00, Area = 153.94
Radius = 14.00, Area = 615.75

Listing 2-16Using the Bindings Class

StringExpression desc = Bindings.concat("Radius = ",
    radius.asString(Locale.US, "%.2f"),
   ", Area = ", area.asString(Locale.US, "%.2f"));

让我们看一个使用Bindings类的selectXXX()方法的例子。它用于为嵌套属性创建绑定。在嵌套层次结构中,所有的类和属性都必须是公共的。假设您有一个拥有zip属性的Address类和一个拥有addr属性的Person类。这些类别分别显示在清单 2-17 和 2-18 中。

// Person.java
package com.jdojo.binding;

import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;

public class Person {
        private ObjectProperty<Address> addr =
               new SimpleObjectProperty(new Address());

        public ObjectProperty<Address> addrProperty() {
                return addr;
        }
}

Listing 2-18A Person Class

// Address.java
package com.jdojo.binding;

import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;

public class Address {
        private StringProperty zip = new SimpleStringProperty("36106");

        public StringProperty zipProperty() {
                return zip;
        }
}

Listing 2-17An Address Class

假设您创建了一个Person类的ObjectProperty,如下所示:

ObjectProperty<Person> p = new SimpleObjectProperty(new Person());

使用Bindings.selectString()方法,您可以为Person对象的addr属性的zip属性创建一个StringBinding,如下所示:

// Bind p.addr.zip
StringBinding zipBinding = Bindings.selectString(p, "addr", "zip");

前面的语句为StringProperty zip获取一个绑定,它是对象paddr属性的嵌套属性。selectXXX()方法中的一个属性可能有多层嵌套。你可以有一个selectXXX()的称呼

StringBinding xyzBinding = Bindings.selectString(x, "a", "b", "c", "d");

Note

JavaFX API 文档指出,如果任何属性参数不可访问,Bindings.selectString()将返回空的String。然而,运行时返回null

清单 2-19 展示了selectString()方法的使用。程序打印两次zip属性的值:一次是默认值,一次是更改后的值。最后,它试图绑定一个不存在的属性p.addr.state。绑定到不存在的属性会导致异常。

// BindNestedProperty.java
// Listing part of the example sources download for the book
36106
35217
null
Aug. 21, 2021 10:41:56 AM com.sun.javafx.binding.SelectBinding$SelectBindingHelper getObservableValue
WARNING: Exception while evaluating select-binding [addr, state]
java.lang.NoSuchMethodException: com.jdojo.binding.BindNestedProperty$Address.getState()
     at java.base/java.lang.Class.getMethod(Class.java:2195)
     ...
     at  JavaFXBook/
     com.jdojo.binding.BindNestedProperty.main(BindNestedProperty.java:57)

Listing 2-19Using the selectXXX() Method of the Bindings Class

结合 Fluent API 和绑定

在使用高级绑定 API 时,可以在同一个绑定表达式中使用 fluent 和Bindings类 API。以下代码片段展示了这种方法:

DoubleProperty radius = new SimpleDoubleProperty(7.0);
DoubleProperty area = new SimpleDoubleProperty(0);

// Combine the Fluent API and Bindings class API
area.bind(Bindings.multiply(Math.PI, radius.multiply(radius)));

使用低级绑定 API

高级绑定 API 并不适合所有情况。例如,它没有提供计算一个Observable数的*方根的方法。如果高级绑定 API 变得太麻烦而无法使用,或者它没有提供您需要的东西,您可以使用低级绑定 API。它以增加几行代码为代价,为您提供了强大的功能和灵活性。低级 API 允许您使用 Java 编程语言的全部潜力来定义绑定。

使用低级绑定 API 包括以下三个步骤:

  1. 创建一个扩展其中一个绑定类的类。例如,如果你想创建一个DoubleBinding,你需要扩展DoubleBinding类。

  2. 调用超类的bind()方法绑定所有依赖关系。注意,所有绑定类都有一个bind()方法实现。您需要调用此方法,将所有依赖项作为参数传递。它的参数类型是一个Observable类型的varargs

  3. 重写超类的computeValue()方法来编写绑定的逻辑。它计算绑定的当前值。它的返回类型与绑定的类型相同,例如,DoubleBinding的返回类型是double,而StringBinding的返回类型是String,依此类推。

此外,您可以重写绑定类的一些方法,为您的绑定提供更多功能。当绑定被释放时,您可以重写dispose()方法来执行额外的操作。可以覆盖getDependencies()方法来返回绑定的依赖列表。如果想在绑定无效时执行额外的操作,就需要重写onInvalidating()方法。

考虑计算圆的面积的问题。以下代码片段使用低级 API 来完成此任务:

final DoubleProperty radius = new SimpleDoubleProperty(7.0);
DoubleProperty area = new SimpleDoubleProperty(0);

DoubleBinding areaBinding = new DoubleBinding() {
    {
        this.bind(radius);
    }

    @Override
    protected double computeValue() {
        double r = radius.get();
        double area = Math.PI * r * r;
        return area;
    }
};

area.bind(areaBinding); // Bind the area property to the areaBinding

前面的代码片段创建了一个匿名类,它扩展了DoubleBinding类。它调用bind()方法,传递对radius属性的引用。匿名类没有构造器,所以你必须使用实例初始化器来调用bind()方法。computeValue()方法计算并返回圆的面积。属性radius已经被声明为final,因为它正在匿名类中使用。

清单 2-20 中的程序展示了如何使用低级绑定 API。它覆盖了区域绑定的computeValue()方法。对于描述绑定,它也覆盖了dispose()getDependencies()onInvalidating()方法。

// LowLevelBinding.java
// Listing part of the example sources download for the book
Radius = 7.00, Area = 153.94
Description is invalid.
Radius = 14.00, Area = 615.75

Listing 2-20Using the Low-Level Binding API to Compute the Area of a Circle

使用绑定使圆居中

让我们看一个使用绑定的 JavaFX GUI 应用程序的例子。您将创建一个带有圆形的屏幕,即使在调整屏幕大小时,它也将位于屏幕的中心。圆的周长将接触屏幕的较*的边。如果屏幕的宽度和高度相同,圆的周长将接触屏幕的所有四个边。

试图在没有绑定的情况下开发具有中心圆的屏幕是一项单调乏味的任务。javafx.scene.shape包中的Circle类代表一个圆。它有三个属性——centerXcenterYradius——DoubleProperty类型。centerXcenterY属性定义了圆心的(x,y)坐标。radius属性定义了圆的半径。默认情况下,圆用黑色填充。

创建一个圆,将centerXcenterYradius设置为默认值 0.0,如下所示:

Circle c = new Circle();

接下来,将圆添加到一个组中,并以该组作为其根节点创建一个场景,如下所示:

Group root = new Group(c);
Scene scene = new Scene(root, 150, 150);

以下绑定将根据场景的大小来定位和调整圆的大小:

c.centerXProperty().bind(scene.widthProperty().divide(2));
c.centerYProperty().bind(scene.heightProperty().divide(2));
c.radiusProperty().bind(Bindings.min(scene.widthProperty(),
     scene.heightProperty()).divide(2));

前两个绑定将圆的centerXcenterY分别绑定到场景的宽度和高度的中间。第三个绑定将圆的radius绑定到场景最小宽度和高度的一半(见divide(2))。就这样!当应用程序运行时,绑定 API 具有保持圆圈居中的魔力。

清单 2-21 有完整的程序。图 2-5 显示程序初始运行时的画面。图 2-6 显示屏幕水*拉伸时的屏幕。尝试垂直拉伸屏幕,您会注意到圆周仅接触屏幕的左侧和右侧。

img/336502_2_En_2_Fig6_HTML.jpg

图 2-6

CenteredCircle程序的屏幕水*伸展时的屏幕

img/336502_2_En_2_Fig5_HTML.png

图 2-5

最初运行CenteredCircle程序时的屏幕

// CenteredCircle.java
// Listing part of the example sources download for the book

Listing 2-21Using the Binding API to Keep a Circle Centered in a Scene

摘要

一个 Java 类可能包含两种类型的成员:字段和方法。字段表示其对象的状态,它们被声明为私有的。公共方法,也称为访问器,或者 getters 和 setters,用于读取和修改私有字段。对于所有或部分私有字段具有公共访问器的 Java 类称为 Java bean,访问器定义了 bean 的属性。Java bean 的属性允许用户定制其状态、行为或两者。

JavaFX 通过属性和绑定 API 支持属性、事件和绑定。JavaFX 中的属性支持是 JavaBeans 属性的巨大飞跃。JavaFX 中的所有属性都是可观察的。可以观察到它们的失效和值的变化。您可以拥有读/写或只读属性。所有读/写属性都支持绑定。在 JavaFX 中,属性可以表示一个值或一组值。

当属性值的状态第一次从有效变为无效时,属性会生成一个无效事件。JavaFX 中的属性使用惰性计算。当无效属性再次变为无效时,不会生成失效事件。无效的属性在重新计算后变得有效。

在 JavaFX 中,绑定是一个计算结果为值的表达式。它由一个或多个被称为依赖关系的可观察值组成。绑定观察其依赖关系的变化,并自动重新计算其值。JavaFX 对所有绑定都使用惰性求值。当绑定最初被定义或者当它的依赖关系改变时,它的值被标记为无效。无效绑定的值在下次请求时计算。JavaFX 中的所有属性类都内置了对绑定的支持。

绑定有一个方向,即传播更改的方向。JavaFX 支持两种类型的属性绑定:单向绑定和双向绑定。单向绑定只在一个方向起作用;依赖项中的更改会传播到绑定属性,反之亦然。双向绑定在两个方向上都起作用;依赖项的更改反映在属性中,反之亦然。

JavaFX 中的绑定 API 分为两类:高级绑定 API 和低级绑定 API。高级绑定 API 允许您使用 JavaFX 类库定义绑定。对于大多数用例,您可以使用高级绑定 API。有时,现有的 API 不足以定义绑定。在这些情况下,使用低级绑定 API。在低级绑定 API 中,从现有的绑定类派生一个绑定类,并编写自己的逻辑来定义绑定。

下一章将向您介绍 JavaFX 中的可观察集合。

三、可观察的集合

在本章中,您将学习:

  • JavaFX 中有哪些可观察的集合

  • 如何观察可观察集合的失效和变化

  • 如何使用可观察集合作为属性

本章的例子在com.jdojo.collections包中。为了让它们工作,您必须在module-info.java文件中添加相应的一行:

...
opens com.jdojo.collections to javafx.graphics, javafx.base;
...

什么是可观测集合?

JavaFX 中的可观察集合是 Java 中集合的扩展。Java 中的集合框架有ListSetMap接口。JavaFX 添加了以下三种类型的可观察集合,可以观察到它们内容的变化:

  • 可观察的列表

  • 可观察的集合

  • 可观察的地图

JavaFX 通过三个新接口支持这些类型的集合:

  • ObservableList

  • ObservableSet

  • ObservableMap

这些接口从java.util包中的ListSetMap继承而来。除了从 Java 集合接口继承之外,JavaFX 集合接口还继承了Observable接口。所有 JavaFX 可观察集合接口和类都在javafx.collections包中。图 3-1 显示了ObservableListObservableSetObservableMap接口的部分类图。

img/336502_2_En_3_Fig1_HTML.jpg

图 3-1

JavaFX 中可观察集合接口的部分类图

JavaFX 中的可观察集合有两个额外的特性:

  • 它们支持失效通知,因为它们是从Observable接口继承的。

  • 它们支持更改通知。您可以向它们注册更改侦听器,当它们的内容发生更改时会得到通知。

javafx.collections.FXCollections类是一个使用 JavaFX 集合的实用程序类。它由所有静态方法组成。

JavaFX 不公开可观察列表、集合和映射的实现类。您需要使用FXCollections类中的一个工厂方法来创建ObservableListObservableSetObservableMap接口的对象。

Tip

简单地说,JavaFX 中的可观察集合是一个列表、集合或映射,可以观察到它的失效和内容变化。

理解观察列表

一个ObservableList是一个java.util.List和一个具有变更通知特性的Observable。图 3-2 显示了ObservableList接口的类图。

img/336502_2_En_3_Fig2_HTML.jpg

图 3-2

ObservableList接口的类图

Tip

图中缺少方法filtered()sorted()。您可以使用它们来过滤和排序列表元素。有关详细信息,请参见 API 文档。

ObservableList接口中的The addListener()removeListener()方法允许您分别添加和移除ListChangeListener s。其他方法对列表执行操作,这会影响多个元素。

如果您想在ObservableList中发生变化时收到通知,您需要添加一个ListChangeListener接口,当列表中发生变化时会调用该接口的onChanged()方法。Change类是ListChangeListener接口的静态内部类。一个Change对象包含一个ObservableList中变化的报告。它被传递给ListChangeListeneronChanged()方法。我将在本节的后面详细讨论列表更改侦听器。

您可以使用从Observable接口继承的以下两种方法在ObservableList中添加或移除失效监听器:

  • void addListener(InvalidationListener listener)

  • void removeListener(InvalidationListener listener)

注意,ObservableList包含了List接口的所有方法,因为它从List接口继承了这些方法。

Tip

JavaFX 库提供了两个名为FilteredListSortedList的类,它们在javafx.collections.transformation包中。一个FilteredList是一个ObservableList,它使用一个指定的Predicate过滤它的内容。A SortedList对其内容进行排序。我不会在本章讨论这些类。所有关于可观察列表的讨论也适用于这些类的对象。

创建一个可观察列表

您需要使用FXCollections类的以下工厂方法之一来创建一个ObservableList:

  • <E> ObservableList<E> emptyObservableList()

  • <E> ObservableList<E> observableArrayList()

  • <E> ObservableList<E> observableArrayList(Collection<? extends E> col)

  • <E> ObservableList<E> observableArrayList(E... items)

  • <E> ObservableList<E> observableList(List<E> list)

  • <E> ObservableList<E> observableArrayList(Callback<E, Observable[]> extractor)

  • <E> ObservableList<E> observableList(List<E> list, Callback<E, Observable[]> extractor)

emptyObservableList()方法创建一个空的、不可修改的ObservableList。通常,当您需要一个ObservableList作为参数传递给一个方法,并且您没有任何元素要传递给那个列表时,就使用这个方法。您可以创建一个空的StringObservableList,如下所示:

ObservableList<String> emptyList = FXCollections.emptyObservableList();

observableArrayList()方法创建一个由ArrayList支持的ObservableList。该方法的其他变体创建一个ObservableList,其初始元素可以在一个Collection中指定为一个项目列表或一个List

前面列表中的最后两个方法创建了一个ObservableList,可以观察它的元素是否有更新。他们接受一个提取器,它是Callback<E, Observable[]>接口的一个实例。一个提取器用于获取Observable值的列表,以观察更新。我将在“观察 ObservableList 的更新”一节中介绍这两种方法的使用。

清单 3-1 展示了如何创建可观察列表以及如何使用ObservableList接口的一些方法来操作列表。最后,它展示了如何使用FXCollections类的concat()方法来连接两个可观察列表的元素。

// ObservableListTest.java
package com.jdojo.collections;

import javafx.collections.FXCollections;
import javafx.collections.ObservableList;

public class ObservableListTest {
        public static void main(String[] args) {
            // Create a list with some elements
            ObservableList<String> list =
                    FXCollections.observableArrayList("one", "two");
            System.out.println("After creating list: " + list);

            // Add some more elements to the list

            list.addAll("three", "four");
            System.out.println("After adding elements: " + list);

            // You have four elements. Remove the middle two
            // from index 1 (inclusive) to index 3 (exclusive)
            list.remove(1, 3);
            System.out.println("After removing elements: " + list);

            // Retain only the element "one"
            list.retainAll("one");
            System.out.println("After retaining \"one\": " + list);

            // Create another ObservableList
            ObservableList<String> list2 =
                FXCollections.<String>observableArrayList(
                      "1", "2", "3");

            // Set list2 to list
            list.setAll(list2);
            System.out.println("After setting list2 to list: " +
                     list);

            // Create another list
            ObservableList<String> list3 =
                FXCollections.<String>observableArrayList(
                       "ten", "twenty", "thirty");

            // Concatenate elements of list2 and list3
            ObservableList<String> list4 =
                     FXCollections.concat(list2, list3);
            System.out.println("list2 is " + list2);
            System.out.println("list3 is " + list3);
            System.out.println(
                     "After concatenating list2 and list3:" + list4);
        }

}
After creating list: [one, two]
After adding elements: [one, two, three, four]
After removing elements: [one, four]
After retaining "one": [one]
After setting list2 to list: [1, 2, 3]
list2 is [1, 2, 3]
list3 is [ten, twenty, thirty]
After concatenating list2 and list3:[1, 2, 3, ten, twenty, thirty]

Listing 3-1Creating and Manipulating Observable Lists

观察一个可观察列表的无效

您可以像添加任何一个Observable一样添加失效监听器到一个ObservableList。清单 3-2 展示了如何使用带有ObservableList的失效监听器。

Tip

ObservableList的情况下,失效监听器被通知列表中的每一个变化,而不管变化的类型。

// ListInvalidationTest.java
package com.jdojo.collections;

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

public class ListInvalidationTest {
        public static void main(String[] args) {
                // Create a list with some elements
                ObservableList<String> list =
                    FXCollections.observableArrayList("one", "two");

                // Add an InvalidationListener to the list
                list.addListener(ListInvalidationTest::invalidated);

                System.out.println("Before adding three.");
                list.add("three");
                System.out.println("After adding three.");

                System.out.println("Before adding four and five.");
                list.addAll("four", "five");
                System.out.println("Before adding four and five.");

                System.out.println("Before replacing one with one.");
                list.set(0, "one");
                System.out.println("After replacing one with one.");
        }

        public static void invalidated(Observable list) {
                System.out.println("List is invalid.");
        }
}
Before adding three.
List is invalid.
After adding three.
Before adding four and five.
List is invalid.
Before adding four and five

.
Before replacing one with one.
List is invalid.
After replacing one with one.

Listing 3-2Testing Invalidation Notifications for an ObservableList

观察可观察列表的变化

观察ObservableList的变化有点棘手。列表可以有多种变化。有些变化可能是排他性的,而有些变化可能与其他变化一起发生。列表中的元素可以被置换、更新、替换、添加和删除。学习这个话题你需要耐心,因为我会零零碎碎的讲。

您可以使用其addListener()方法向ObservableList添加一个变更监听器,该方法采用了一个ListChangeListener接口的实例。每次列表发生变化时,监听器的changed()方法都会被调用。下面的代码片段展示了如何向StringObservableList添加一个变更监听器。onChanged()方法简单;当它被通知更改时,它在标准输出上打印一条消息:

// Create an observable list
ObservableList<String> list = FXCollections.observableArrayList();

// Add a change listener to the list
list.addListener(new ListChangeListener<String>() {
        @Override
        public void onChanged(ListChangeListener.Change<? extends String>
                  change) {
            System.out.println("List has changed.");
        }

});

清单 3-3 包含了展示如何检测ObservableList中的变化的完整程序。它使用带有方法引用的 lambda 表达式(Java 8 的特性)来添加更改监听器。在添加了一个更改侦听器之后,它操纵列表四次,每次都通知侦听器,从下面的输出可以明显看出。

// SimpleListChangeTest.java
package com.jdojo.collections;

import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;

public class SimpleListChangeTest {
        public static void main(String[] args) {
            // Create an observable list
            ObservableList<String> list =
                    FXCollections.observableArrayList();

            // Add a change listener to the list
            list.addListener(SimpleListChangeTest::onChanged);

            // Manipulate the elements of the list
            list.add("one");
            list.add("two");
            FXCollections.sort(list);
            list.clear();
        }

        public static void onChanged(
                   ListChangeListener.Change<? extends String> change) {
            System.out.println("List has changed");
        }

}
List has changed.
List has changed.
List has changed.
List has changed.

Listing 3-3Detecting Changes in an ObservableList

了解 ListChangeListener。改变

有时,您可能想要更详细地分析列表的更改,而不仅仅是知道列表已经更改。传递给onChanged()方法的ListChangeListener.Change对象包含一个对列表执行的更改的报告。您需要使用其方法的组合来了解变更的细节。表 3-1 列出了ListChangeListener.Change类中的方法及其类别。

表 3-1

ListChangeListener.Change类中的方法

|

方法

|

种类

ObservableList<E> getList() 一般
boolean next()``void reset() 光标移动
boolean wasAdded()``boolean wasRemoved()``boolean wasReplaced()``boolean wasPermutated()``boolean wasUpdated() 更改类型
int getFrom()``int getTo() 受影响范围
int getAddedSize()``List<E> getAddedSubList() 添加
List<E> getRemoved()``int getRemovedSize() 搬迁
int getPermutation(int oldIndex) 排列

getList()方法在更改后返回源列表。一个ListChangeListener.Change对象可以报告多个块中的变化。这可能一开始并不明显。考虑以下代码片段:

ObservableList<String> list = FXCollections.observableArrayList();

// Add a change listener here...

list.addAll("one", "two", "three");
list.removeAll("one", "three");

在这段代码中,变更监听器将被通知两次:一次是针对addAll()方法调用,一次是针对removeAll()方法调用。ListChangeListener.Change对象报告受影响的索引范围。在第二个更改中,您删除了属于两个不同索引范围的两个元素。注意,在两个被移除的元素之间有一个元素"two"。在第二种情况下,Change对象将包含两个变更的报告。第一个变化将包含索引 0 处的元素"one"已被移除的信息。现在,列表只包含两个元素,元素"two"的索引为 0,元素"three"的索引为 1。第二个变化将包含索引 1 处的元素"three"已被移除的信息。

一个Change对象包含一个指向报告中特定变更的光标。next()reset()方法用于控制光标。当调用onChanged()方法时,光标指向报告中的第一个变更。第一次调用next()方法会将光标移动到报告中的第一个变更处。在试图读取变更的细节之前,您必须通过调用next()方法将光标指向变更。如果next()方法将光标移动到一个有效的变更,它将返回true。否则返回falsereset()方法在第一次改变前移动光标。通常,在 while 循环中调用next()方法,如以下代码片段所示:

ObservableList<String> list = FXCollections.observableArrayList();
...
// Add a change listener to the list
list.addListener(new ListChangeListener<String>() {
    @Override
    public void onChanged(ListChangeListener.Change<? extends String>
             change) {
        while(change.next()) {
            // Process the current change here...
        }
    }

});

在变更类型类别中,方法报告特定类型的变更是否已经发生。如果添加了元素,wasAdded()方法返回true。如果元素被移除,wasRemoved()方法返回true。如果元素被替换,wasReplaced()方法返回true。您可以将替换看作是在相同的索引处删除后添加。如果wasReplaced()返回true,则wasRemoved()wasAdded()也返回true。如果列表的元素被置换(即重新排序)但没有被删除、添加或更新,则wasPermutated()方法返回true。如果列表的元素被更新,wasUpdated()方法返回true

并非列表的所有五种类型的更改都是排他的。某些变更可能会在同一个变更通知中同时发生。置换和更新这两种类型的改变是互斥的。如果您对处理所有类型的更改感兴趣,那么您在onChanged()方法中的代码应该如下所示:

public void onChanged(ListChangeListener.Change change) {
        while (change.next()) {
                if (change.wasPermutated()) {
                        // Handle permutations
                }
                else if (change.wasUpdated()) {
                        // Handle updates
                }
                else if (change.wasReplaced()) {
                        // Handle replacements
                }
                else {
                        if (change.wasRemoved()) {
                                // Handle removals
                        }
                        else if (change.wasAdded()) {
                                // Handle additions
                        }
                }
        }
}

在受影响的范围类型类别中,getFrom()getTo()方法报告受变更影响的索引范围。getFrom()方法返回开始索引,getTo()方法返回结束索引加 1。如果wasPermutated()方法返回true,则该范围包括被置换的元素。如果wasUpdated()方法返回true,则该范围包括被更新的元素。如果wasAdded()方法返回true,则该范围包括添加的元素。如果wasRemoved()方法返回truewasAdded()方法返回false,那么getFrom()getTo()方法返回相同的数字——移除的元素在列表中的位置的索引。

getAddedSize()方法返回添加的元素数量。getAddedSubList()方法返回一个包含添加元素的列表。getRemovedSize()方法返回移除的元素数量。getRemoved()方法返回一个不可变的被移除或替换元素的列表。getPermutation(int oldIndex)方法返回排列后元素的新索引。例如,如果在置换过程中,索引 2 处的元素移动到索引 5 处,getPermutation(2)将返回5

关于ListChangeListener.Change类的方法的讨论到此结束。但是,您还没有完成这个课程!我仍然需要讨论如何在实际情况下使用这些方法,例如,当列表的元素被更新时。我将在下一节介绍如何处理列表元素的更新。我将用一个涵盖所有讨论内容的例子来结束这个主题。

观察可观察列表的更新

在“创建一个可观察列表一节中,我已经列出了下面两个创建ObservableListFXCollections类的方法:

  • <E> ObservableList<E> observableArrayList(Callback<E, Observable[]> extractor)

  • <E> ObservableList<E> observableList(List<E> list, Callback<E, Observable[]> extractor)

如果您希望在列表元素更新时得到通知,您需要使用以下方法之一创建列表。这两种方法有一个共同点:它们接受一个Callback<E,Observable[]>对象作为参数。Callback<P,R>接口在javafx.util包中。其定义如下:

public interface Callback<P,R> {
        R call(P param)
}

Callback<P,R>接口用于 API 在以后合适的时间需要进一步动作的情况。第一个泛型类型参数指定传递给call()方法的参数的类型,第二个指定call()方法的返回类型。

如果您注意到Callback<E,Observable[]>中类型参数的声明,第一个类型参数是E,它是列表元素的类型。第二个参数是一个Observable数组。当您向列表中添加一个元素时,会调用Callback对象的call()方法。添加的元素作为参数传递给call()方法。你应该从call()方法返回一个Observable的数组。如果返回的Observable数组中的任何元素发生变化,监听器将被通知列表元素的“更新”变化,因为call()方法已经为该列表返回了Observable数组。

让我们看看为什么需要一个Callback对象和一个Observable数组来检测列表元素的更新。列表存储其元素的引用。它的元素可以在程序的任何地方使用它们的引用来更新。列表不知道它的元素正在从其他地方被更新。它需要知道Observable对象的列表,其中任何一个对象的改变都可能被认为是对其元素的更新。Callback对象的call()方法满足了这一要求。列表将每个元素传递给call()方法。call()方法返回一个Observable数组。该列表监视Observable数组元素的任何变化。当它检测到一个变化时,它通知它的变化监听器,它的与Observable数组相关的元素已经被更新。这个参数被命名为提取器的原因是它为一个列表元素提取一个数组Observable

清单 3-4 展示了如何创建一个ObservableList,当它的元素被更新时,它可以通知它的变化监听器。

// ListUpdateTest.java
package com.jdojo.collections;

import java.util.List;
import javafx.beans.Observable;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.util.Callback;

public class ListUpdateTest {
        public static void main(String[] args) {
            // Create an extractor for IntegerProperty.
            Callback<IntegerProperty, Observable[]> extractor =
                    (IntegerProperty p) -> {
                   // Print a message to know when it is called
                   System.out.println("The extractor is called for " + p);
                   // Wrap the parameter in an Observable[] and return it
                   return new Observable[]{p};
               };
            // Create an empty observable list with a callback to
            // extract the observable values for each element of the list
            ObservableList<IntegerProperty> list =
                FXCollections.observableArrayList(extractor);

            // Add two elements to the list

            System.out.println("Before adding two elements...");
            IntegerProperty p1 = new SimpleIntegerProperty(10);
            IntegerProperty p2 = new SimpleIntegerProperty(20);
            list.addAll(p1, p2); // Will call the call() method of the
                          // extractor - once for p1 and once for p2.
            System.out.println("After adding two elements...");

            // Add a change listener to the list
            list.addListener(ListUpdateTest::onChanged);

            // Update p1 from 10 to 100, which will trigger
            // an update change for the list
            p1.set(100);
        }

        public static void onChanged(
               ListChangeListener.Change<? extends IntegerProperty>
                    change) {
            System.out.println("List is " + change.getList());

            // Work on only updates to the list
            while (change.next()) {
                if (change.wasUpdated()) {
                    // Print the details of the update
                    System.out.println("An update is detected.");

                    int start = change.getFrom();
                    int end = change.getTo();
                    System.out.println("Updated range: [" + start +
                               ", " + end + "]");

                    List<? extends IntegerProperty> updatedElementsList;
                    updatedElementsList =
                               change.getList().subList(start, end);

                    System.out.println("Updated elements: " +
                                updatedElementsList);
                }
            }
        }

}
Before adding two elements...
The extractor is called for IntegerProperty [value: 10]
The extractor is called for IntegerProperty [value: 20]
After adding two elements...
List is [IntegerProperty [value: 100], IntegerProperty [value: 20]]
An update is detected.
Updated range: [0, 1]
Updated elements: [IntegerProperty [value: 100]]

Listing 3-4Observing a List for Updates of Its Elements

ListUpdateTest类的main()方法创建一个提取器,它是Callback<IntegerProperty, Observable[]>接口的一个对象。call()方法接受一个IntegerProperty参数,并将其包装在一个Observable数组中返回。它还打印传递给它的对象。

提取器用于创建一个ObservableList。两个IntegerProperty对象被添加到列表中。当添加对象时,提取器的call()方法被调用,添加的对象作为它的参数。从输出中可以明显看出这一点。call()方法返回被添加的对象。这意味着列表将监视对象(IntegerProperty)的任何变化,并通知它的变化监听器。

列表中会添加一个更改监听器。它只处理列表的更新。最后,您将列表中第一个元素的值从 10 更改为 100,以触发更新更改通知。

观察可观察列表变化的完整示例

本节提供了一个完整的例子,展示了如何处理对ObservableList的不同种类的更改。

我们的起点是一个Person类,如清单 3-5 所示。在这里,您将使用Person对象中的ObservableListPerson类有两个属性:firstNamelastName。两种属性都是StringProperty类型。它的compareTo()方法被实现来按照先名后姓的升序对Person对象进行排序。它的toString()方法打印名字、空格和姓氏。

// Person.java
package com.jdojo.collections;

import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;

public class Person implements Comparable<Person> {
        private StringProperty firstName = new SimpleStringProperty();
        private StringProperty lastName = new SimpleStringProperty();

        public Person() {
                this.setFirstName("Unknown");
                this.setLastName("Unknown");
        }

        public Person(String firstName, String lastName) {
                this.setFirstName(firstName);
                this.setLastName(lastName);
        }

        // Complete listing part of the example sources download for
        // the book
        ...
}

Listing 3-5A Person Class with Two Properties Named firstName and lastName

清单 3-6 中所示的PersonListChangeListener类是一个变更监听器类。它实现了ListChangeListener接口的onChanged()方法,为Person对象的ObservableList处理所有类型的变更通知。

// PersonListChangeListener.java
// Listing part of the example sources download for the book

Listing 3-6A Change Listener for an ObservableList of Person Objects

清单 3-7 中所示的ListChangeTest类是一个测试类。它创建了一个带有提取器的ObservableList。提取器返回一个Person对象的firstNamelastName属性的数组。这意味着当这些属性中的一个被改变时,作为列表元素的一个Person对象被认为是更新的,并且一个更新通知将被发送给所有的改变监听器。它将更改侦听器添加到列表中。最后,它对列表进行了几种更改,以触发更改通知。更改通知的详细信息打印在标准输出上。

这就完成了关于为ObservableList编写变更监听器的最复杂的讨论之一。JavaFX 的设计者没有把它变得更复杂,你难道不庆幸吗?

// ListChangeTest.java
// Listing part of the example sources download for the book
Before adding Li Na: []
Change Type: Added
Added Size: 1
Added Range: [0, 1]
Added List: [Li Na]
After adding Li Na: [Li Na]

Before adding Vivi Gin and Li He: [Li Na]
Change Type: Added

Added Size: 2
Added Range: [1, 3]
Added List: [Vivi Gin, Li He]
After adding Vivi Gin and Li He: [Li Na, Vivi Gin, Li He]

Before sorting the list:[Li Na, Vivi Gin, Li He]
Change Type: Permutated
Permutated Range: [0, 3]
index[0] moved to index[1]
index[1] moved to index[2]
index[2] moved to index[0]
After sorting the list:[Li He, Li Na, Vivi Gin]

Before updating Li Na: [Li He, Li Na, Vivi Gin]
Change Type: Updated
Updated Range : [1, 2]
Updated elements are: [Li Smith]
After updating Li Smith: [Li He, Li Smith, Vivi Gin]

Before replacing Li He with Simon Ng: [Li He, Li Smith, Vivi Gin]
Change Type: Replaced
Change Type: Removed
Removed Size: 1
Removed Range: [0, 1]
Removed List: [Li He]
Change Type: Added

Added Size: 1
Added Range: [0, 1]
Added List: [Simon Ng]
After replacing Li He with Simon Ng: [Simon Ng, Li Smith, Vivi Gin]

Before setAll(): [Simon Ng, Li Smith, Vivi Gin]
Change Type: Replaced
Change Type: Removed
Removed Size: 3
Removed Range: [0, 3]
Removed List: [Simon Ng, Li Smith, Vivi Gin]
Change Type: Added
Added Size: 3
Added Range: [0, 3]
Added List: [Lia Li, Liz Na, Li Ho]
After setAll(): [Lia Li, Liz Na, Li Ho]

Before removeAll(): [Lia Li, Liz Na, Li Ho]
Change Type: Removed
Removed Size: 1
Removed Range: [0, 0]
Removed List: [Lia Li]
Change Type: Removed

Removed Size: 1
Removed Range: [1, 1]
Removed List: [Li Ho]
After removeAll(): [Liz Na]

Listing 3-7Testing an ObservableList of Person Objects for All Types of Changes

了解可观察设置

如果您在学习了ObservableList和 list change listeners 之后还活着,那么学习ObservableSet将会很容易!图 3-3 显示了ObservableSet接口的类图。

img/336502_2_En_3_Fig3_HTML.jpg

图 3-3

ObservableSet接口的类图

它继承自SetObservable接口。它支持失效和变更通知,并且从Observable接口继承了失效通知支持的方法。它添加了以下两种方法来支持更改通知:

  • void addListener(SetChangeListener<? super E> listener)

  • void removeListener(SetChangeListener<? super E> listener)

SetChangeListener接口的一个实例监听ObservableSet中的变化。它声明了一个名为Change的静态内部类,表示一个ObservableSet中的变化报告。

Note

集合是一个无序的集合。本节显示了输出中几个集合的元素。您可能会得到不同的输出,以不同于示例中所示的顺序显示集合的元素。

创建一个可观察集合

您需要使用FXCollections类的以下工厂方法之一来创建一个ObservableSet:

  • <E> ObservableSet<E> observableSet(E... elements)

  • <E> ObservableSet<E> observableSet(Set<E> set)

  • <E> ObservableSet<E> emptyObservableSet()

由于使用可观察集合与使用可观察列表没有太大的不同,我们不进一步研究这个主题。您可以参考 API 文档和com.jdojo.collections包中的示例类来了解更多关于可观察集的信息。

理解观察图

图 3-4 显示了ObservableMap接口的类图。它继承自MapObservable接口。它支持失效和更改通知。它从Observable接口继承了无效通知支持的方法,并增加了以下两个方法来支持变更通知:

img/336502_2_En_3_Fig4_HTML.jpg

图 3-4

ObservableMap接口的类图

  • void addListener(MapChangeListener<? super K, ? super V> listener)

  • void removeListener(MapChangeListener<? super K, ? super V> listener)

MapChangeListener接口的一个实例监听ObservableMap中的变化。它声明了一个名为Change的静态内部类,表示一个ObservableMap中的变化报告。

创建一个可观察地图

您需要使用FXCollections类的以下工厂方法之一来创建一个ObservableMap:

  • <K,V> ObservableMap<K, V> observableHashMap()

  • <K,V> ObservableMap<K, V> observableMap(Map<K, V> map)

  • <K,V> ObservableMap<K,V> emptyObservableMap()

第一种方法创建一个由HashMap支持的空的可观察地图。第二种方法创建一个由指定地图支持的ObservableMap。在ObservableMap上执行的突变被报告给监听器。直接在支持映射上执行的突变不会报告给监听器。第三种方法创建一个空的不可修改的可观察图。清单 3-8 展示了如何创建ObservableMap s。

// ObservableMapTest.java
package com.jdojo.collections;

import java.util.HashMap;
import java.util.Map;
import javafx.collections.FXCollections;
import javafx.collections.ObservableMap;

public class ObservableMapTest {
        public static void main(String[] args) {
            ObservableMap<String, Integer> map1 =
                    FXCollections.observableHashMap();

            map1.put("one", 1);
            map1.put("two", 2);
            System.out.println("Map 1: " + map1);

            Map<String, Integer> backingMap = new HashMap<>();
            backingMap.put("ten", 10);
            backingMap.put("twenty", 20);

            ObservableMap<String, Integer> map2 =
                    FXCollections.observableMap(backingMap);
            System.out.println("Map 2: " + map2);
        }

}
Map 1: {two=2, one=1}
Map 2: {ten=10, twenty=20}

Listing 3-8Creating ObservableMaps

因为使用可观察的地图与使用可观察的列表和集合没有太大的不同,所以我们不进一步研究这个主题。您可以参考 API 文档和com.jdojo.collections包中的示例类来了解更多关于可观察地图的信息。

JavaFX 集合的属性和绑定

可以将ObservableListObservableSetObservableMap集合公开为Property对象。它们还支持使用高级和低级绑定 API 的绑定。代表单一值的属性对象在第二章中讨论过。在继续本节之前,请确保您已经阅读了该章。

了解 ObservableList 属性和绑定

图 3-5 显示了ListProperty类的部分类图。ListProperty类实现了ObservableValu e 和ObservableList接口。它是一个可观察的值,因为它包含了一个ObservableList的参考。实现ObservableList接口使得它的所有方法对一个ListProperty对象可用。在ListProperty上调用ObservableList的方法与在被包装的ObservableList上调用它们具有相同的效果。

img/336502_2_En_3_Fig5_HTML.jpg

图 3-5

ListProperty类的部分类图

您可以使用SimpleListProperty类的以下构造器之一来创建ListProperty的实例:

  • SimpleListProperty()

  • SimpleListProperty(ObservableList<E> initialValue)

  • SimpleListProperty(Object bean, String name)

  • SimpleListProperty(Object bean, String name, ObservableList<E> initialValue)

使用ListProperty类的一个常见错误是在使用之前没有将ObservableList传递给它的构造器。在对其执行有意义的操作之前,ListProperty必须有对ObservableList的引用。如果不使用ObservableList来创建ListProperty对象,可以使用它的set()方法来设置ObservableList的引用。以下代码片段会生成一个异常:

ListProperty<String> lp = new SimpleListProperty<String>();

// No ObservableList to work with. Generates an exception.
lp.add("Hello");
Exception in thread "main" java.lang.UnsupportedOperationException
        at java.util.AbstractList.add(AbstractList.java:148)
        at java.util.AbstractList.add(AbstractList.java:108)
        at javafx.beans.binding.ListExpression.add(ListExpression.java:262)

Tip

在包装了null引用的ListProperty上执行的操作被视为在不可变的空ObservableList上执行的操作。

下面的代码片段展示了如何在使用之前创建和初始化一个ListProperty:

ObservableList<String> list1 = FXCollections.observableArrayList();
ListProperty<String> lp1 = new SimpleListProperty<String>(list1);
lp1.add("Hello");

ListProperty<String> lp2 = new SimpleListProperty<String>();
lp2.set(FXCollections.observableArrayList());
lp2.add("Hello");

观察列表属性的变化

您可以将三种类型的监听器附加到一个ListProperty:

  • 一个InvalidationListener

  • ChangeListener

  • ListChangeListener

当包装在ListProperty中的ObservableList的引用发生变化或者ObservableList的内容发生变化时,所有三个监听器都会得到通知。当列表的内容改变时,ChangeListenerschanged()方法接收对相同列表的引用作为新旧值。如果ObservableList的包装引用被一个新的替换,这个方法接收旧列表和新列表的引用。要处理列表更改事件,请参考本章中的“观察一个可观察列表的更改”一节。

清单 3-9 中的程序展示了如何处理对一个ListProperty的所有三种类型的改变。列表更改监听器以简单通用的方式处理列表内容的更改。具体如何处理一个ObservableList的内容变化事件,请参见本章“观察一个观察列表的变化”一节。

// ListPropertyTest.java
// Listing part of the example sources download for the book

Before addAll()
List property is invalid.
List Property has changed. Old List: [one, two, three], New List: [one, two, three]
Action taken on the list: Added. Removed: [], Added: [one, two, three]
After addAll()

Before set()
List property is invalid.
List Property has changed. Old List: [one, two, three], New List: [two, three]
Action taken on the list: Replaced. Removed: [one, two, three], Added: [two, three]
After set()

Before remove()
List property is invalid

.
List Property has changed. Old List: [three], New List: [three]
Action taken on the list: Removed. Removed: [two], Added: []
After remove()

Listing 3-9Adding Invalidation, Change, and List Change Listeners to a ListProperty

绑定列表属性大小属性

一个ListProperty公开了两个属性,sizeempty,它们分别属于类型ReadOnlyIntegerPropertyReadOnlyBooleanProperty。您可以使用sizeProperty()emptyProperty()方法访问它们。sizeempty属性对于 GUI 应用程序中的绑定非常有用。例如,GUI 应用程序中的模型可能由一个ListProperty支持,您可以将这些属性绑定到屏幕上标签的 text 属性。当模型中的数据发生变化时,标签会通过绑定自动更新。sizeempty属性在ListExpression类中声明。

清单 3-10 中的程序展示了如何使用sizeempty属性。它使用ListExpression类的asString()方法将包装的ObservableList内容转换为String

// ListBindingTest.java
package com.jdojo.collections;

import javafx.beans.property.ListProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;

public class ListBindingTest {
        public static void main(String[] args) {
            ListProperty<String> lp =
                        new SimpleListProperty<>(FXCollections.observableArrayList());

            // Bind the size and empty properties of the ListProperty
            // to create a description of the list
            StringProperty initStr = new SimpleStringProperty("Size: " );
            StringProperty desc = new SimpleStringProperty();
            desc.bind(initStr.concat(lp.sizeProperty())
                             .concat(", Empty: ")
                             .concat(lp.emptyProperty())
                             .concat(", List: ")
                             .concat(lp.asString()));

            System.out.println("Before addAll(): " + desc.get());
            lp.addAll("John", "Jacobs");
            System.out.println("After addAll(): " + desc.get());
        }

}
Before addAll(): Size: 0, Empty: true, List: []
After addAll(): Size: 2, Empty: false, List: [John, Jacobs]

Listing 3-10Using the size and empty Properties of a ListProperty Object

绑定到列表属性和内容

支持列表属性高级绑定的方法在ListExpressionBindings类中。低级绑定可以通过子类化ListBinding类来创建。一个ListProperty支持两种类型的绑定:

  • 绑定它所包装的ObservableList的引用

  • 绑定它所包装的ObservableList的内容

bind()bindBidirectional()方法用于创建第一种绑定。清单 3-11 中的程序展示了如何使用这些方法。如下面的输出所示,注意两个列表属性在绑定后都引用了同一个ObservableList

// BindingListReference.java
package com.jdojo.collections;

import javafx.beans.property.ListProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.collections.FXCollections;

public class BindingListReference {

        public static void main(String[] args) {
            ListProperty<String> lp1 =
                new SimpleListProperty<>(
                         FXCollections.observableArrayList());
            ListProperty<String> lp2 =
                new SimpleListProperty<>(
                         FXCollections.observableArrayList());

            lp1.bind(lp2);

            print("Before addAll():", lp1, lp2);
            lp1.addAll("One", "Two");
            print("After addAll():", lp1, lp2);

            // Change the reference of the ObservableList in lp2
            lp2.set(FXCollections.observableArrayList("1", "2"));
            print("After lp2.set():", lp1, lp2);

            // Cannot do the following as lp1 is a bound property
            // lp1.set(FXCollections.observableArrayList("1", "2"));
            // Unbind lp1
            lp1.unbind();
            print("After unbind():", lp1, lp2);

            // Bind lp1 and lp2 bidirectionally

            lp1.bindBidirectional(lp2);
            print("After bindBidirectional():", lp1, lp2);

            lp1.set(FXCollections.observableArrayList("X", "Y"));
            print("After lp1.set():", lp1, lp2);
        }

        public static void print(String msg, ListProperty<String> lp1,
                   ListProperty<String> lp2) {
            System.out.println(msg);
            System.out.println("lp1: " + lp1.get() + ", lp2: " +
                    lp2.get() + ", lp1.get() == lp2.get(): " +
                    (lp1.get() == lp2.get()));
            System.out.println("---------------------------");
        }
}
Before addAll():
lp1: [], lp2: [], lp1.get() == lp2.get(): true
---------------------------
After addAll():
lp1: [One, Two], lp2: [One, Two], lp1.get() == lp2.get(): true
---------------------------
After lp2.set():
lp1: [1, 2], lp2: [1, 2], lp1.get() == lp2.get(): true
---------------------------
After unbind():
lp1: [1, 2], lp2: [1, 2], lp1.get() == lp2.get(): true
---------------------------
After bindBidirectional():
lp1: [1, 2], lp2: [1, 2], lp1.get() == lp2.get(): true

---------------------------
After lp1.set():
lp1: [X, Y], lp2: [X, Y], lp1.get() == lp2.get(): true
---------------------------

Listing 3-11Binding the References of List Properties

通过bindContent()bindContentBidirectional()方法,您可以分别在一个方向和两个方向上将包装在ListProperty中的ObservableList的内容绑定到另一个ObservableList的内容。确保使用相应的方法unbindContent()unbindContentBidirectional()来解除两个可观察列表的内容绑定。

Tip

您还可以使用Bindings类的方法来为可观察列表的引用和内容创建绑定。

允许更改一个内容已经绑定到另一个ObservableListListProperty的内容,但这并不可取。在这种情况下,绑定的ListProperty将不会与其目标列表同步。清单 3-12 展示了这两种内容绑定的例子。

// BindingListContent.java
package com.jdojo.collections;

import javafx.beans.property.ListProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.collections.FXCollections;

public class BindingListContent {

        public static void main(String[] args) {
            ListProperty<String> lp1 =
                new SimpleListProperty<>(
                         FXCollections.observableArrayList());
            ListProperty<String> lp2 =
                new SimpleListProperty<>(
                         FXCollections.observableArrayList());

            // Bind the content of lp1 to the content of lp2
            lp1.bindContent(lp2);

            /* At this point, you can change the content of lp1\. However,
             * that will defeat the purpose of content binding, because
             * the content of lp1 is no longer in sync with the content of
             * lp2.
             * Do not do this:
             * lp1.addAll("X", "Y");
             */
            print("Before lp2.addAll():", lp1, lp2);
            lp2.addAll("1", "2");
            print("After lp2.addAll():", lp1, lp2);

            lp1.unbindContent(lp2);
            print("After lp1.unbindContent(lp2):", lp1, lp2);

            // Bind lp1 and lp2 contents bidirectionally

            lp1.bindContentBidirectional(lp2);

            print("Before lp1.addAll():", lp1, lp2);
            lp1.addAll("3", "4");
            print("After lp1.addAll():", lp1, lp2);

            print("Before lp2.addAll():", lp1, lp2);
            lp2.addAll("5", "6");
            print("After lp2.addAll():", lp1, lp2);
        }

        public static void print(String msg, ListProperty<String> lp1,
                   ListProperty<String> lp2) {
            System.out.println(msg + " lp1: " + lp1.get() +
                    ", lp2: " + lp2.get());
        }

}
Before lp2.addAll(): lp1: [], lp2: []
After lp2.addAll(): lp1: [1, 2], lp2: [1, 2]
After lp1.unbindContent(lp2): lp1: [1, 2], lp2: [1, 2]
Before lp1.addAll(): lp1: [1, 2], lp2: [1, 2]
After lp1.addAll(): lp1: [1, 2, 3, 4], lp2: [1, 2, 3, 4]
Before lp2.addAll(): lp1: [1, 2, 3, 4], lp2: [1, 2, 3, 4]
After lp2.addAll(): lp1: [1, 2, 3, 4, 5, 6], lp2: [1, 2, 3, 4, 5, 6]

Listing 3-12Binding Contents of List Properties

绑定到列表的元素

提供了如此多有用的特性,以至于我可以继续讨论这个话题至少 50 多页!我将用另外一个例子来结束这个话题。

可以使用ListExpression类的以下方法之一绑定到包装在ListProperty中的ObservableList的特定元素:

  • ObjectBinding<E> valueAt(int index)

  • ObjectBinding<E> valueAt(ObservableIntegerValue index)

该方法的第一个版本为列表中特定索引处的元素创建一个ObjectBinding。该方法的第二个版本将一个索引作为参数,它是一个可以随时间变化的ObservableIntegerValue。当valueAt()方法中的绑定索引在列表范围之外时,ObjectBinding包含null

让我们使用该方法的第二个版本来创建一个绑定,它将绑定到列表的最后一个元素。在这里,您可以利用ListPropertysize属性来创建绑定表达式。清单 3-13 中的程序展示了如何使用valueAt()方法。

// BindingToListElements.java
// Listing part of the example sources download for the book
List:[], Last Value: null
List:[John], Last Value: John
List:[John, Donna, Geshan], Last Value: Geshan
List:[John, Donna], Last Value: Donna
List:[], Last Value: null

Listing 3-13Binding to the Elements of a List

了解 ObservableSet 属性和绑定

一个SetProperty对象包装了一个ObservableSet。和SetProperty一起工作和和ListProperty一起工作非常相似。我不打算重复前面几节中讨论的关于ObservableList的属性和绑定的内容。同样的讨论也适用于ObservableSet的属性和绑定。以下是使用SetProperty时需要记住的要点:

  • SetProperty类的类图类似于图 3-5 中ListProperty类的类图。您需要将所有名称中的单词“List”替换为“Set”。

  • SetExpressionBindings类包含支持设置属性的高级绑定的方法。你需要子类化SetBinding类来创建底层绑定。

  • ListProperty一样,SetProperty公开了sizeempty属性。

  • ListProperty一样,SetProperty支持引用和它所包装的ObservableSet内容的绑定。

  • ListProperty一样,SetProperty支持三种类型的通知:失效通知、更改通知和设置更改通知。

  • 与列表不同,集合是项目的无序集合。它的元素没有索引。它不支持绑定到其特定元素。因此,SetExpression类不像ListExpression类那样包含类似于valueAt()的方法。

您可以使用SimpleSetProperty类的以下构造器之一来创建SetProperty的实例:

  • SimpleSetProperty()

  • SimpleSetProperty(ObservableSet<E> initialValue)

  • SimpleSetProperty(Object bean, String name)

  • SimpleSetProperty(Object bean, String name, ObservableSet<E> initialValue)

下面的代码片段创建了一个SetProperty的实例,并向属性包装的ObservableSet添加了两个元素。最后,它使用get()方法从属性对象中获取ObservableSet的引用:

// Create a SetProperty object
SetProperty<String> sp = new SimpleSetProperty<String>(FXCollections.observableSet());

// Add two elements to the wrapped ObservableSet
sp.add("one");
sp.add("two");

// Get the wrapped set from the sp property

ObservableSet<String> set = sp.get();

清单 3-14 中的程序演示了如何绑定SetProperty对象。

// SetBindingTest.java
// Listing part of the example sources download for the book
Before sp1.add(): Size: 0, Empty: true, Set: []
After sp1.add(): Size: 2, Empty: false, Set: [Jacobs, John]
Called sp1.bindContent(sp2)...
Before sp2.add(): sp1: [], sp2: []
After sp2.add(): sp1: [1], sp2: [1]
After sp1.unbindContent(sp2): sp1: [1], sp2: [1]
Before sp2.add(): sp1: [1], sp2: [1]
After sp2.add(): sp1: [1, 2], sp2: [2, 1]

Listing 3-14Using Properties and Bindings for Observable Sets

理解 ObservableMap 属性和绑定

一个MapProperty对象包装了一个ObservableMap。和MapProperty一起工作和和ListProperty一起工作非常相似。我不打算重复前面几节中讨论的关于ObservableList的属性和绑定的内容。同样的讨论也适用于ObservableMap的属性和绑定。以下是使用MapProperty时需要记住的要点:

  • MapProperty类的类图类似于图 3-5 中ListProperty类的类图。您需要将所有名称中的单词“List”替换为单词“map”,将泛型类型参数<E>替换为<K, V>,其中 K 和 V 分别代表 Map 中条目的键类型和值类型。

  • MapExpressionBindings类包含支持地图属性高级绑定的方法。你需要子类化MapBinding类来创建底层绑定。

  • ListProperty一样,MapProperty公开了sizeempty属性。

  • ListProperty一样,MapProperty支持引用和它所包装的ObservableMap内容的绑定。

  • ListProperty一样,MapProperty支持三种类型的通知:无效通知、更改通知和地图更改通知。

  • MapProperty支持使用其valueAt()方法绑定到特定键值。

使用下列SimpleMapProperty类的构造器之一创建MapProperty的实例:

  • SimpleMapProperty()

  • SimpleMapProperty(Object bean, String name)

  • SimpleMapProperty(Object bean, String name, ObservableMap<K,V> initialValue)

  • SimpleMapProperty(ObservableMap<K,V> initialValue)

下面的代码片段创建了一个MapProperty的实例,并添加了两个条目。最后,它使用get()方法获得被包装的ObservableMap的引用:

// Create a MapProperty object
MapProperty<String, Double> mp =
        new SimpleMapProperty<String, Double>(FXCollections.observableHashMap());

// Add two entries to the wrapped ObservableMap
mp.put("Ken", 8190.20);
mp.put("Jim", 8990.90);

// Get the wrapped map from the mp property
ObservableMap<String, Double> map = mp.get();

清单 3-15 中的程序展示了如何绑定MapProperty对象。它显示了两个地图之间的内容绑定。您还可以在两个地图属性之间使用单向和双向简单绑定来绑定它们包装的地图的引用。

// MapBindingTest.java
// Listing part of the example sources download for the book
Ken Salary: null
Before mp1.put(): Size: 0, Empty: true, Map: {}, Ken Salary: null
After mp1.put(): Size: 3, Empty: false, Map: {Jim=9800.8, Lee=6000.2, Ken=7890.9}, Ken Salary: 7890.9
Called mp1.bindContent(mp2)...
Before mp2.put(): Size: 0, Empty: true, Map: {}, Ken Salary: null
After mp2.put(): Size: 2, Empty: false, Map: {Cindy=7800.2, Ken=7500.9}, Ken Salary: 7500.9

Listing 3-15Using Properties and Bindings for Observable Maps

摘要

JavaFX 通过添加对可观察列表、集合和映射(称为可观察集合)的支持,扩展了 Java 中的集合框架。可观察集合是一个列表、集合或映射,可以观察到它的失效和内容变化。javafx.collections包中的ObservableListObservableSetObservableMap接口的实例代表 JavaFX 中可观察到的接口。您可以向这些可观察集合的实例添加失效和更改侦听器。

FXCollections类是一个使用 JavaFX 集合的实用程序类。它由所有静态方法组成。JavaFX 不公开可观察列表、集合和映射的实现类。您需要使用FXCollections类中的一个工厂方法来创建ObservableListObservableSetObservableMap接口的对象。

JavaFX 库提供了两个名为FilteredListSortedList的类,它们在javafx.collections.transformation包中。一个FilteredList是一个ObservableList,它使用一个指定的Predicate过滤它的内容。A SortedList对其内容进行排序。

下一章将讨论如何在 JavaFX 应用程序中创建和定制 stages。

四、管理舞台

在本章中,您将学习:

  • 如何获取屏幕的详细信息,如数量、分辨率和尺寸

  • JavaFX 中的舞台是什么,以及如何设置舞台的边界和样式

  • 如何移动未装饰的舞台

  • 如何设置舞台的形态和不透明度

  • 如何调整舞台的大小以及如何在全屏模式下显示舞台

本章的例子在com.jdojo.stage包中。为了让它们工作,您必须在module-info.java文件中添加相应的一行:

...
opens com.jdojo.stage to javafx.graphics, javafx.base;
...

了解屏幕的细节

javafx.stage包中的Screen类用于获取细节,例如,每英寸点数(DPI)设置和用户屏幕(或显示器)的尺寸。如果多个屏幕连接到一台计算机,其中一个屏幕被称为主屏幕,其他屏幕被称为非主屏幕。您可以使用Screen类的静态getPrimary()方法,用下面的代码获取主监视器的Screen对象的引用:

// Get the reference to the primary screen
Screen primaryScreen = Screen.getPrimary();

静态的getScreens()方法返回一个Screen对象的ObservableList:

ObservableList<Screen> screenList = Screen.getScreens();

您可以使用Screen类的getDpi()方法获得 DPI 中屏幕的分辨率,如下所示:

Screen primaryScreen = Screen.getPrimary();
double dpi = primaryScreen.getDpi();

您可以使用getBounds()getVisualBounds()方法分别获得边界和可视边界。这两个方法都返回一个Rectangle2D对象,该对象封装了一个矩形的左上角和右下角的(x,y)坐标、宽度和高度。getMinX()getMinY()方法分别返回矩形左上角的 x 和 y 坐标。getMaxX()getMaxY()方法分别返回矩形右下角的 x 和 y 坐标。getWidth()getHeight()方法分别返回矩形的宽度和高度。

屏幕的边界覆盖了屏幕上可用的区域。可视边界表示在考虑本机窗口系统使用的区域(如任务栏和菜单)后,屏幕上可供使用的区域。通常,但不是必须的,屏幕的可视边界表示比其边界更小的区域。

如果桌面跨越多个屏幕,非主屏幕的边界相对于主屏幕。例如,如果桌面跨越两个屏幕,主屏幕左上角的(x,y)坐标为(0,0),宽度为 1600,则第二个屏幕左上角的坐标为(1600,0)。

清单 4-1 中的程序在有两个屏幕的 Windows 桌面上运行时打印屏幕细节。您可能会得到不同的输出。请注意一个屏幕的边界和可视边界的高度差异,而另一个屏幕则没有。主屏幕在底部显示一个任务栏,从可视边界中去掉部分高度。非主屏幕不显示任务栏,因此它的边界和可视边界是相同的。

Tip

虽然在 API 文档中没有提到Screen类,但是在 JavaFX 启动器启动之前,您不能使用这个类。也就是说,您无法在非 JavaFX 应用程序中获得屏幕描述。这就是为什么您要在 JavaFX 应用程序类的start()方法中编写代码的原因。不需要在 JavaFX 应用程序线程上使用Screen类。您也可以在您的类的init()方法中编写相同的代码。

// ScreenDetailsApp.java
package com.jdojo.stage;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.collections.ObservableList;
import javafx.geometry.Rectangle2D;
import javafx.stage.Screen;
import javafx.stage.Stage;

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

       public void start(Stage stage) {
               ObservableList<Screen> screenList = Screen.getScreens();
               System.out.println("Screens Count: " + screenList.size());

               // Print the details of all screens
               for(Screen screen: screenList) {
                      print(screen);
               }

               Platform.exit();
       }

       public void print(Screen s) {
               System.out.println("DPI: " + s.getDpi());

               System.out.print("Screen Bounds: ");
               Rectangle2D bounds = s.getBounds();
               print(bounds);

               System.out.print("Screen Visual Bounds: ");
               Rectangle2D visualBounds = s.getVisualBounds();
               print(visualBounds);
               System.out.println("-----------------------");
       }

       public void print(Rectangle2D r) {
               System.out.format("minX=%.2f, minY=%.2f, width=%.2f,
                        height=%.2f%n",
                        r.getMinX(), r.getMinY(),
                        r.getWidth(), r.getHeight());
       }
}
Screens Count: 2
DPI: 96.0
Screen Bounds: minX=0.00, minY=0.00, width=1680.00, height=1050.00
Screen Visual Bounds: minX=0.00, minY=0.00, width=1680.00, height=1022.00
-----------------------
DPI: 96.0
Screen Bounds: minX = 1680.00, minY=0.00, width= 1680.00, height=1050.00
Screen Visual Bounds: minX = 1680.00, minY=0.00, width= 1680.00, height=1050.0
-----------------------

Listing 4-1Accessing Screen Details

什么是舞台?

JavaFX 中的舞台是承载场景的顶级容器,场景由可视元素组成。javafx.stage包中的Stage类表示 JavaFX 应用程序中的一个舞台。初级舞台由*台创建,并传递给Application类的start(Stage s)方法。您可以根据需要创建其他舞台。

Tip

JavaFX 应用程序中的舞台是顶级容器。这并不一定意味着它总是显示为一个单独的窗口。然而,对于本书的目的来说,一个舞台对应一个窗口,除非另有说明。

图 4-1 显示了从Window类继承而来的Stage类的类图。Window类是几个窗口行容器类的超类。它包含所有类型窗口共有的基本功能(例如,显示和隐藏窗口的方法;设置 x、y、宽度和高度属性。设置窗口的不透明度;等等。).Window类定义了xywidthheightopacity属性。它有show()hide()方法分别显示和隐藏一个窗口。Window类的setScene()方法为窗口设置场景。Stage类定义了一个close()方法,与调用Window类的hide()方法效果相同。

img/336502_2_En_4_Fig1_HTML.png

图 4-1

Stage类的类图

必须在 JavaFX 应用程序线程上创建和修改一个Stage对象。回想一下在 JavaFX 应用程序线程上调用了Application类的start()方法,并且创建了一个主Stage并将其传递给该方法。注意,通过start()方法的初级舞台没有显示出来。需要调用show()方法来展示。

需要讨论使用舞台的几个方面。在接下来的部分中,我将从基础到高级逐一处理它们。

显示初级舞台

让我们从最简单的 JavaFX 应用程序开始,如清单 4-2 所示。start()方法没有代码。当您运行应用程序时,您看不到窗口,也看不到控制台上的输出。应用程序将永远运行。您需要使用特定于系统的键来取消应用程序。如果你用的是 Windows,用你最喜欢的组合键 Ctrl + Alt + Del 来激活任务管理器!如果使用命令提示符,请使用 Ctrl + C。

// EverRunningApp.java
package com.jdojo.stage;

import javafx.application.Application;
import javafx.stage.Stage;

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

       @Override
       public void start(Stage stage) {
               // Do not write any code here
       }
}

Listing 4-2An Ever-Running JavaFX Application

要确定清单 4-2 中的程序有什么问题,您需要理解 JavaFX 应用程序启动器是做什么的。回想一下,当调用Platform.exit()方法或关闭最后一个显示的舞台时,JavaFX 应用程序线程被终止。当所有非守护进程线程死亡时,JVM 终止。JavaFX 应用程序线程是非守护进程线程。当 JavaFX 应用程序线程终止时,Application.launch()方法返回。在前面的示例中,无法终止 JavaFX 应用程序线程。这就是应用程序永远运行的原因。

start()方法中使用Platform.exit()方法可以解决这个问题。清单 4-3 中显示了start()方法的修改代码。当你运行程序时,它不做任何有意义的事情就退出了。

// ShortLivedApp.java
package com.jdojo.stage;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.stage.Stage;

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

       @Override
       public void start(Stage stage) {
               Platform.exit(); // Exit the application
       }
}

Listing 4-3A Short-Lived JavaFX Application

让我们通过关闭初级舞台来尝试修复一直运行的程序。调用start()方法时只有一个舞台,关闭它应该会终止 JavaFX 应用程序线程。让我们用下面的代码修改EverRunningAppstart()方法:

@Override
public void start(Stage stage) {
       stage.close(); // Close the only stage you have
}

即使有了这个用于start()方法的代码,EverRunningApp也会永远运行。如果舞台没有显示,则close()方法不会关闭舞台。初级舞台从未显示。因此,向start()方法添加一个stage.close()调用没有任何好处。下面的代码适用于start()方法。但是,这将导致舞台显示和关闭时屏幕闪烁:

@Override
public void start(Stage stage) {
       stage.show();  // First show the stage
       stage.close(); // Now close it
}

Tip

Stage类的close()方法与调用Window类的hide()方法具有相同的效果。JavaFX API 文档没有提到试图关闭一个不显示的窗口没有任何效果。

设定舞台的界限

舞台的边界由四个属性组成:xywidthheightxy属性决定舞台左上角的位置。widthheight属性决定了它的大小。在本节中,您将学习如何在屏幕上定位舞台并调整其大小。您可以使用这些属性的 getters 和 setters 来获取和设置它们的值。

让我们从一个简单的例子开始,如清单 4-4 所示。程序在显示前设置初级舞台的标题。当您运行这段代码时,您会看到一个带有标题栏、边框和空白区域的窗口。如果打开了其他应用程序,您可以透过舞台的透明区域看到它们的内容。窗口的位置和大小由*台决定。

Tip

当舞台没有场景并且其位置和大小没有明确设置时,其位置和大小由*台确定和设置。

// BlankStage.java
package com.jdojo.stage;

import javafx.application.Application;
import javafx.stage.Stage;

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

       @Override
       public void start(Stage stage) {
               stage.setTitle("Blank Stage");
               stage.show();
       }
}

Listing 4-4Displaying a Stage with No Scene and with the Platform Default Position and Size

让我们稍微修改一下逻辑。在这里,您将为舞台设置一个空场景,而不设置场景的大小。修改后的start()方法如下所示:

import javafx.scene.Group;
import javafx.scene.Scene;
...
@Override
public void start(Stage stage) {
       stage.setTitle("Stage with an Empty Scene");
       Scene scene = new Scene(new Group());
       stage.setScene(scene);
       stage.show();
}

请注意,您已经设置了一个没有子节点的Group作为场景的根节点,因为没有根节点就无法创建场景。当您使用前面的代码作为其start()方法运行清单 4-4 中的程序时,载物台的位置和大小由*台决定。这一次,内容区域将具有白色背景,因为场景的默认背景颜色是白色。

我们再修改一下逻辑。这里,我们给场景添加一个按钮。修改后的start()方法如下:

import javafx.scene.control.Button;
...
@Override
public void start(Stage stage) {
       stage.setTitle("Stage with a Button in the Scene");
       Group root = new Group(new Button("Hello"));
       Scene scene = new Scene(root);
       stage.setScene(scene);
       stage.show();
}

当您使用前面的代码作为其start()方法运行清单 4-4 中的程序时,舞台的位置和大小由场景的计算大小决定。舞台的内容区域足够宽,可以显示标题栏菜单或场景的内容,以较大者为准。舞台的内容区域足够高,可以显示场景的内容,在本例中只有一个按钮。载物台位于屏幕中央,如图 4-2 所示。

img/336502_2_En_4_Fig2_HTML.png

图 4-2

具有包含按钮的场景的舞台,其中场景的大小未指定

让我们通过在场景中添加一个按钮,并将场景的宽度和高度分别设置为 300 和 100,为逻辑添加另一个扭曲,如下所示:

@Override
public void start(Stage stage) {
       stage.setTitle("Stage with a Sized Scene");
       Group root = new Group(new Button("Hello"));
       Scene scene = new Scene(root, 300, 100);
       stage.setScene(scene);
       stage.show();
}

当您使用前面的代码作为其start()方法运行清单 4-4 中的程序时,舞台的位置和大小由场景的指定大小决定。舞台的内容区域与场景的指定大小相同。舞台的宽度包括两侧的边框,舞台的高度包括标题栏和下边框的高度。载物台位于屏幕中央,如图 4-3 所示。

img/336502_2_En_4_Fig3_HTML.jpg

图 4-3

具有特定大小场景的舞台

让我们在逻辑上再加一个转折。您将使用以下代码设置场景和舞台的大小:

@Override
public void start(Stage stage) {
       stage.setTitle("A Sized Stage with a Sized Scene");
       Group root = new Group(new Button("Hello"));
       Scene scene = new Scene(root, 300, 100);
       stage.setScene(scene);
       stage.setWidth(400);
       stage.setHeight(100);
       stage.show();
}

当您使用前面的代码作为其start()方法运行清单 4-4 中的程序时,舞台的位置和大小由舞台的指定大小决定。舞台在屏幕上居中,然后看起来如图 4-4 所示。

img/336502_2_En_4_Fig4_HTML.jpg

图 4-4

大小合适的舞台和大小合适的场景

Tip

舞台的默认居中方式是在屏幕上水*居中。舞台左上角的 y 坐标是屏幕高度的三分之一减去舞台高度。这是在Window类的centerOnScreen()方法中使用的逻辑。

让我回顾一下定位和调整舞台大小的规则。如果没有指定舞台的界限,并且

  • 它没有场景,它的边界由*台决定。

  • 它有一个没有可视节点的场景,它的边界由*台决定。在这种情况下,不指定场景的大小。

  • 它有一个带有视觉节点的场景,它的边界由场景中的视觉节点决定。在这种情况下,不指定场景的大小,舞台在屏幕上居中。

  • 它有一个场景,场景的大小是指定的,它的边界由场景的指定大小决定。舞台在屏幕中央。

如果您指定了舞台的大小,但没有指定其位置,则舞台将根据设置的大小调整大小,并在屏幕上居中,而不考虑是否存在场景以及场景的大小。如果您指定载物台的位置(x,y 坐标),它会相应地定位。

Tip

如果您想要设置舞台的宽度和高度以适合其场景的内容,请使用Window类的sizeToScene()方法。如果您希望在运行时修改场景后将舞台的大小与其场景的大小同步,则方法非常有用。使用Window类的centerOnScreen()方法使舞台在屏幕上居中。

如果您希望舞台在屏幕上水*和垂直居中,请使用以下逻辑:

Rectangle2D bounds = Screen.getPrimary().getVisualBounds();
double x = bounds.getMinX() + (bounds.getWidth() - stage.getWidth())/2.0;
double y = bounds.getMinY() + (bounds.getHeight() - stage.getHeight())/2.0;
stage.setX(x);
stage.setY(y);

使用前面的代码片段时要小心。它利用了舞台的大小。舞台的大小直到第一次演出时才知道。在显示舞台之前使用前面的逻辑不会真正使舞台在屏幕上居中。JavaFX 应用程序的以下start()方法将无法正常工作:

@Override
public void start(Stage stage) {
       stage.setTitle("A Truly Centered Stage");
       Group root = new Group(new Button("Hello"));
       Scene scene = new Scene(root);
       stage.setScene(scene);

       // Wrong!!!! Use the logic shown below after the stage.show() call
       // At this point, stage width and height are not known. They are NaN.
       Rectangle2D bounds = Screen.getPrimary().getVisualBounds();
       double x = bounds.getMinX() + (bounds.getWidth() –
                 stage.getWidth())/2.0;
       double y = bounds.getMinY() + (bounds.getHeight() –
                 stage.getHeight())/2.0;
       stage.setX(x);
       stage.setY(y);

       stage.show();
}

初始化舞台的样式

舞台的区域可以分为两部分:内容区和装饰区。内容区域显示其场景的可视内容。通常,装饰由标题栏和边框组成。标题栏的存在及其内容根据*台提供的装饰类型而有所不同。一些装饰品提供了额外的功能,而不仅仅是美观。例如,标题栏可用于将舞台拖动到不同的位置;标题栏中的按钮可用于最小化、最大化、恢复和关闭舞台;或者可以使用边框来调整舞台的大小。

在 JavaFX 中,舞台的样式属性决定了它的背景颜色和装饰。根据样式,在 JavaFX 中可以有以下五种类型的舞台:

  • 盛饰建筑的

  • 未加装饰的

  • 透明的

  • 统一的

  • 效用

一个装饰的舞台有纯白色的背景和*台装饰。一个没有装饰的舞台有纯白色的背景,没有任何装饰。一个透明的舞台,背景透明,没有任何装饰。统一舞台有*台装饰,客户区与装饰之间无边框;客户区背景与装饰相统一。要看统一舞台风格的效果,场景要用Color.TRANSPARENT填充。统一风格是有条件的特征。一个实用舞台有纯白色背景和最少的*台装饰。

Tip

一个舞台的风格仅仅决定了它的装饰。背景颜色由其场景背景控制,默认情况下是纯白色。如果你把一个舞台的风格设置为TRANSPARENT,你会得到一个纯白背景的舞台,这就是场景的背景。为了得到一个真正透明的舞台,你需要使用setFill()方法将场景的背景色设置为null

您可以使用Stage类的initStyle(StageStyle style)方法来设置舞台的样式。一个舞台的风格必须在第一次展示之前设定好。在舞台显示后,第二次设置它会引发运行时异常。默认情况下,舞台是装饰的。

舞台的五种样式在StageStyle枚举中定义为五个常量:

  • StageStyle.DECORATED

  • StageStyle.UNDECORATED

  • StageStyle.TRANSPARENT

  • StageStyle.UNIFIED

  • StageStyle.UTILITY

清单 4-5 展示了如何在舞台上使用这五种风格。在start()方法中,您一次只需要取消一条语句的注释,这将初始化 stage 的样式。您将使用一个VBox来显示两个控件:一个Label和一个ButtonLabel展示舞台的风格。提供Button是为了关闭舞台,因为不是所有的样式都提供带有关闭按钮的标题栏。图 4-5 显示了使用四种风格的舞台。背景中窗口的内容可以通过透明的舞台看到。这就是当您使用透明样式时,您会看到更多已添加到舞台的内容的原因。

img/336502_2_En_4_Fig5_HTML.jpg

图 4-5

使用不同风格的舞台

// StageStyleApp.java
package com.jdojo.stage;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import static javafx.stage.StageStyle.DECORATED;
import static javafx.stage.StageStyle.UNDECORATED;
import static javafx.stage.StageStyle.TRANSPARENT;
import static javafx.stage.StageStyle.UNIFIED;
import static javafx.stage.StageStyle.UTILITY;

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

       @Override
       public void start(Stage stage) {
               // A label to display the style type
               Label styleLabel = new Label("Stage Style");

               // A button to close the stage
               Button closeButton = new Button("Close");
               closeButton.setOnAction(e -> stage.close());

               VBox root = new VBox();
               root.getChildren().addAll(styleLabel, closeButton);
               Scene scene = new Scene(root, 100, 70);
               stage.setScene(scene);

               // The title of the stage is not visible for all styles.
               stage.setTitle("The Style of a Stage");

               /* Uncomment one of the following statements at a time */
               this.show(stage, styleLabel, DECORATED);
               //this.show(stage, styleLabel, UNDECORATED);
               //this.show(stage, styleLabel, TRANSPARENT);
               //this.show(stage, styleLabel, UNIFIED);
               //this.show(stage, styleLabel, UTILITY);
       }

       private void show(Stage stage, Label styleLabel, StageStyle style) {
               // Set the text for the label to match the style
               styleLabel.setText(style.toString());

               // Set the style
               stage.initStyle(style);

               // For a transparent style, set the scene fill to null.
               // Otherwise, the content area will have the default white
               // background of the scene.
               if (style == TRANSPARENT) {
                      stage.getScene().setFill(null);
                      stage.getScene().getRoot().setStyle(
                             "-fx-background-color: transparent");
               } else if(style == UNIFIED) {
                      stage.getScene().setFill(Color.TRANSPARENT);
               }

               // Show the stage
               stage.show();
       }
}

Listing 4-5Using Different Styles for a Stage

移动未装饰的舞台

您可以通过拖动其标题栏将舞台移动到不同的位置。在未装饰或透明的舞台中,标题栏是不可用的。您需要编写几行代码,让用户通过在场景区域拖动鼠标来移动这种舞台。清单 4-6 展示了如何编写代码来支持舞台的拖动。如果将舞台更改为透明,则需要通过仅在消息标签上拖动鼠标来拖动舞台,因为透明区域不会响应鼠标事件。

这个例子使用鼠标事件处理。我将在第九章详细介绍事件处理。这里简单介绍一下,以完成关于使用不同风格的舞台的讨论。

// DraggingStage.java
package com.jdojo.stage;

import javafx.application.Application;

import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.VBox;

import javafx.stage.Stage;
import javafx.stage.StageStyle;

public class DraggingStage extends Application {
       private Stage stage;
       private double dragOffsetX;
       private double dragOffsetY;

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

       @Override
       public void start(Stage stage) {
               // Store the stage reference in the instance variable to
               // use it in the mouse pressed event handler later.
               this.stage = stage;

               Label msgLabel = new Label(
                        "Press the mouse button and drag.");
               Button closeButton = new Button("Close");
               closeButton.setOnAction(e -> stage.close());

               VBox root = new VBox();
               root.getChildren().addAll(msgLabel, closeButton);

               Scene scene = new Scene(root, 300, 200);

               // Set mouse pressed and dragged even handlers for the
               // scene
               scene.setOnMousePressed(e -> handleMousePressed(e));
               scene.setOnMouseDragged(e -> handleMouseDragged(e));

               stage.setScene(scene);
               stage.setTitle("Moving a Stage");
               stage.initStyle(StageStyle.UNDECORATED);
               stage.show();
       }

       protected void handleMousePressed(MouseEvent e) {
               // Store the mouse x and y coordinates with respect to the
               // stage in the reference variables to use them in the
               // drag event
               this.dragOffsetX = e.getScreenX() - stage.getX();
               this.dragOffsetY = e.getScreenY() - stage.getY();
       }

       protected void handleMouseDragged(MouseEvent e) {
               // Move the stage by the drag amount
               stage.setX(e.getScreenX() - this.dragOffsetX);
               stage.setY(e.getScreenY() - this.dragOffsetY);
       }
}

Listing 4-6Dragging a Stage

以下代码片段将鼠标按下和鼠标拖动事件处理程序添加到场景中:

scene.setOnMousePressed(e -> handleMousePressed(e));
scene.setOnMouseDragged(e -> handleMouseDragged(e));

当你在场景中(除了按钮区域)按下鼠标,就会调用handleMousePressed()方法。MouseEvent对象的getScreenX()getScreenY()方法返回鼠标相对于屏幕左上角的 x 和 y 坐标。图 4-6 显示了坐标系的示意图。它显示舞台周围有一条细边框。但是,当您运行示例代码时,您将看不到任何边框。此处显示是为了区分屏幕区域和舞台区域。您将鼠标相对于舞台左上角的xy坐标存储在实例变量中。

img/336502_2_En_4_Fig6_HTML.png

图 4-6

计算鼠标相对于载物台的坐标

拖动鼠标时,会调用handleMouseDragged()方法。方法使用鼠标按下时的位置和拖动时的位置来计算和设置舞台的位置。

初始化舞台的模态

在 GUI 应用程序中,你可以有两种类型的窗口:模态窗口和非模态窗口。当显示模式窗口时,用户不能使用应用程序中的其他窗口,直到模式窗口被关闭。如果一个应用程序显示多个非模态窗口,用户可以随时在它们之间切换。

JavaFX 为舞台提供了三种类型的模态:

  • 没有人

  • 窗口模式

  • 应用模型

舞台的模态由javafx.stage包的Modality枚举中的以下三个常量之一定义:

  • NONE

  • WINDOW_MODAL

  • APPLICATION_MODAL

您可以使用Stage类的initModality(Modality m)方法设置舞台的形态,如下所示:

// Create a Stage object and set its modality
Stage stage = new Stage();
stage.initModality(Modality.WINDOW_MODAL);

/* More code goes here.*/

// Show the stage
stage.show();

Tip

必须在显示之前设置舞台的形态。在显示舞台后设置它的模态会引发运行时异常。为主要舞台设置通道也会引发运行时异常。

一个Stage可以有一个所有者。一辆Stage的拥有者是另一辆Window。您可以使用Stage类的initOwner(Window owner)方法来设置Stage的所有者。必须在舞台显示之前设置Stage的所有者。一辆Stage的车主可能是null,在这种情况下,据说Stage没有车主。设置一个Stage的所有者会创建一个所有者拥有的关系。例如,如果所有者被最小化或隐藏,则Stage被最小化或隐藏。

Stage的默认设备是NONE。当显示带有模态NONEStage时,它不会阻挡应用程序中的任何其他窗口。它的行为就像一个无模式窗口。

带有WINDOW_MODAL模态的Stage阻塞其所有者层级中的所有窗口。假设有四个舞台:s1、s2、s3 和 s4。舞台 s1 和 s4 具有设置为NONE的模态,并且没有所有者;s1 是 s2 的所有者;s2 是 s3 的所有者。将显示所有四个舞台。如果 s3 的主机设置为WINDOW_MODAL,您可以使用 s3 或 s4,但不能使用 s2 和 s1。所有者-所有者关系被定义为 s1 到 s2 到 s3。当显示 s3 时,它会阻止 s2 和 s1,这两个节点位于其所有者层次结构中。因为 s4 不在 s3 的所有者层次结构中,所以您仍然可以使用 s4。

Tip

对于没有所有者的舞台,WINDOW_MODAL的形态具有与形态被设置为NONE相同的效果。

如果显示的Stage的模态设置为APPLICATION_MODAL,您必须使用Stage并将其关闭,然后才能使用应用程序中的任何其他窗口。继续上一段中显示四个舞台的相同示例,如果您将 s4 的模态设置为APPLICATION_MODAL,焦点将被设置为 s4,您必须先将其关闭,然后才能处理其他舞台。请注意,一个APPLICATION_MODAL舞台阻塞了同一应用程序中的所有其他窗口,而不管所有者拥有的关系如何。

清单 4-7 显示了如何为一个舞台使用不同的模态。它用六个按钮显示初级舞台。每个按钮打开一个具有指定主机和所有者的次级舞台。按钮的文本告诉您它们将打开哪种次级舞台。当显示次要舞台时,尝试单击主要舞台。当第二舞台的模态阻塞第一舞台时,您将无法使用第一舞台;单击主要舞台会将焦点设置回次要舞台。

// StageModalityApp.java
package com.jdojo.stage;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.stage.Modality;
import static javafx.stage.Modality.NONE;
import static javafx.stage.Modality.WINDOW_MODAL;
import static javafx.stage.Modality.APPLICATION_MODAL;
import javafx.stage.Window;

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

       @Override
       public void start(Stage stage) {
               /* Buttons to display each kind of modal stage */
               Button ownedNoneButton = new Button("Owned None");
               ownedNoneButton.setOnAction(e -> showDialog(stage, NONE));

               Button nonOwnedNoneButton = new Button("Non-owned None");
               nonOwnedNoneButton.setOnAction(e ->
                        showDialog(null, NONE));

               Button ownedWinButton = new Button("Owned Window Modal");
               ownedWinButton.setOnAction(e ->
                        showDialog(stage, WINDOW_MODAL));

               Button nonOwnedWinButton =
                        new Button("Non-owned Window Modal");
               nonOwnedWinButton.setOnAction(e ->
                        showDialog(null, WINDOW_MODAL));

               Button ownedAppButton =
                        new Button("Owned Application Modal");
               ownedAppButton.setOnAction(e ->
                        showDialog(stage, APPLICATION_MODAL));

               Button nonOwnedAppButton =
                        new Button("Non-owned Application Modal");
               nonOwnedAppButton.setOnAction(e ->
                        showDialog(null, APPLICATION_MODAL));

               VBox root = new VBox();
               root.getChildren().addAll(
                        ownedNoneButton, nonOwnedNoneButton,
                        ownedWinButton, nonOwnedWinButton,
                        ownedAppButton, nonOwnedAppButton);
               Scene scene = new Scene(root, 300, 200);
               stage.setScene(scene);
               stage.setTitle("The Primary Stage");
               stage.show();
       }

       private void showDialog(Window owner, Modality modality) {
               // Create a Stage with specified owner and modality
               Stage stage = new Stage();
               stage.initOwner(owner);
               stage.initModality(modality);

               Label modalityLabel = new Label(modality.toString());
               Button closeButton = new Button("Close");
               closeButton.setOnAction(e -> stage.close());

               VBox root = new VBox();
               root.getChildren().addAll(modalityLabel, closeButton);
               Scene scene = new Scene(root, 200, 100);
               stage.setScene(scene);
               stage.setTitle("A Dialog Box");
               stage.show();
       }
}

Listing 4-7Using Different Modalities for a Stage

设置舞台的不透明度

舞台的不透明度决定了您透过舞台可以看到的程度。您可以使用Window类的setOpacity(double opacity)方法设置舞台的不透明度。使用getOpacity()方法获得当前舞台的不透明度。

不透明度值的范围从 0.0 到 1.0。不透明度为 0.0 表示舞台完全半透明;不透明度为 1.0 表示舞台完全不透明。不透明度会影响舞台的整个区域,包括其装饰。并非所有 JavaFX 运行时*台都需要支持不透明性。在不支持不透明度的 JavaFX *台上设置不透明度没有任何效果。以下代码片段将舞台的不透明度设置为半透明:

Stage stage = new Stage();
stage.setOpacity(0.5); // A half-translucent stage

调整舞台大小

您可以通过使用其setResizable(boolean resizable)方法来设置用户是否可以调整舞台的大小。注意,对setResizable()方法的调用是对实现的一个提示,使 stage 可调整大小。默认情况下,舞台是可调整大小的。有时,您可能希望将调整舞台大小的使用限制在一定的宽度和高度范围内。Stage类的setMinWidth()setMinHeight()setMaxWidth()setMaxHeight()方法允许您设置用户可以调整舞台大小的范围。

Tip

Stage对象上调用setResizable(false)方法会阻止用户调整舞台的大小。您仍然可以通过编程方式调整舞台的大小。

经常需要打开一个占据整个屏幕空间的窗口。为此,您需要将窗口的位置和大小设置为屏幕的可视边界。清单 4-8 提供了说明这一点的程序。它会打开一个空舞台,占据屏幕的整个可视区域。

// MaximizedStage.java
package com.jdojo.stage;

import javafx.application.Application;
import javafx.geometry.Rectangle2D;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.stage.Screen;
import javafx.stage.Stage;

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

       @Override
       public void start(Stage stage) {
               stage.setScene(new Scene(new Group()));
               stage.setTitle("A Maximized Stage");

               // Set the position and size of the stage equal to the
               // position and size of the screen
               Rectangle2D visualBounds =
                        Screen.getPrimary().getVisualBounds();
               stage.setX(visualBounds.getMinX());
               stage.setY(visualBounds.getMinY());
               stage.setWidth(visualBounds.getWidth());
               stage.setHeight(visualBounds.getHeight());

               // Show the stage
               stage.show();
       }
}

Listing 4-8Opening a Stage to Take Up the Entire Available Visual Screen Space

以全屏模式显示舞台

Stage类有一个fullScreen属性,它指定舞台是否应该以全屏模式显示。全屏模式的实现取决于*台和配置文件。如果*台不支持全屏模式,JavaFX 运行时将通过显示最大化和未修饰的舞台来模拟它。一个 stage 可以通过调用setFullScreen(true)方法进入全屏模式。当舞台进入全屏模式时,会显示一条关于如何退出全屏模式的简短消息:您需要按 ESC 键退出全屏模式。您可以通过调用setFullScreen(false)方法以编程方式退出全屏模式。使用isFullScreen()方法检查载物台是否处于全屏模式。

展示一个舞台并等待它关闭

您通常希望显示一个对话框并暂停进一步的处理,直到它被关闭。例如,您可能希望向用户显示一个消息框,其中包含单击“是”和“否”按钮的选项,并且您希望根据用户单击的按钮执行不同的操作。在这种情况下,当消息框显示给用户时,程序必须等待它关闭,然后才能执行下一个逻辑序列。考虑以下伪代码:

Option userSelection = messageBox("Close", "Do you want to exit?", YESNO);
if (userSelection == YES) {
       stage.close();
}

在这段伪代码中,当调用messageBox()方法时,程序需要等待执行后续的if语句,直到消息框被解除。

Window类的show()方法立即返回,使得在前面的例子中打开一个对话框没有用。您需要使用showAndWait()方法,该方法显示舞台并等待它关闭,然后返回给调用者。showAndWait()方法暂时停止处理当前事件,并开始一个嵌套的事件循环来处理其他事件。

Tip

必须在 JavaFX 应用程序线程上调用showAndWait()方法。不应在主要舞台调用它,否则将引发运行时异常。

您可以使用showAndWait()方法打开多个舞台。每次调用方法都会启动一个新的嵌套事件循环。当此方法调用后创建的所有嵌套事件循环都已终止时,对该方法的特定调用将返回给调用方。

这个规则在开始时可能会令人困惑。让我们看一个例子来详细解释这一点。假设您有三个舞台:s1、s2 和 s3。使用调用s1.showAndWait()打开舞台 s1。从 s1 中的代码开始,使用调用s2.showAndWait()打开舞台 s2。此时,有两个嵌套的事件循环:一个由s1.showAndWait()创建,另一个由s2.showAndWait()创建。对s1.showAndWait()的调用将只在 s1 和 s2 都关闭后返回,而不管它们关闭的顺序。s2 关闭后,s2.showAndWait()调用将返回。

清单 4-9 包含一个程序,它允许你使用多个舞台进行showAndWait()方法调用。使用Open按钮打开初级舞台。点击Open按钮使用showAndWait()方法打开第二舞台。第二级有两个按钮——Say HelloOpen——分别在控制台上打印信息和打开另一个第二级。在调用showAndWait()方法前后,控制台上会显示一条消息。您需要打开多个次级舞台,通过单击Say Hello按钮打印消息,按照您想要的任何顺序关闭它们,然后在控制台上查看输出。

// ShowAndWaitApp.java
package com.jdojo.stage;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class ShowAndWaitApp extends Application {
       protected static int counter = 0;
       protected Stage lastOpenStage;

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

       @Override
       public void start(Stage stage) {
               VBox root = new VBox();
               Button openButton = new Button("Open");
               openButton.setOnAction(e -> open(++counter));
               root.getChildren().add(openButton);
               Scene scene = new Scene(root, 400, 400);
               stage.setScene(scene);
               stage.setTitle("The Primary Stage");
               stage.show();

               this.lastOpenStage = stage;
       }

       private void open(int stageNumber) {
               Stage stage = new Stage();
               stage.setTitle("#" + stageNumber);

               Button sayHelloButton = new Button("Say Hello");
               sayHelloButton.setOnAction(
                  e -> System.out.println(
                        "Hello from #" + stageNumber));

               Button openButton = new Button("Open");
               openButton.setOnAction(e -> open(++counter));

               VBox root = new VBox();
               root.getChildren().addAll(sayHelloButton, openButton);
               Scene scene = new Scene(root, 200, 200);
               stage.setScene(scene);
               stage.setX(this.lastOpenStage.getX() + 50);
               stage.setY(this.lastOpenStage.getY() + 50);
               this.lastOpenStage = stage;

               System.out.println("Before stage.showAndWait(): " +
                        stageNumber);

               // Show the stage and wait for it to close
               stage.showAndWait();

               System.out.println("After stage.showAndWait(): " +
                        stageNumber);
       }
}

Listing 4-9Playing with the showAndWait() Call

Tip

JavaFX 不提供可用作对话框的内置窗口(消息框或提示窗口)。您可以通过为舞台设置适当的模态并使用showAndWait()方法显示来开发一个。

摘要

javafx.stage包中的Screen类用于获取与运行程序的机器挂钩的用户屏幕的详细信息,例如 DPI 设置和尺寸。如果有多个屏幕,其中一个屏幕称为主屏幕,其他的为非主屏幕。您可以使用Screen类的静态getPrimary()方法获取主监视器的Screen对象的引用。

JavaFX 中的舞台是承载场景的顶级容器,场景由可视元素组成。javafx.stage包中的Stage类表示 JavaFX 应用程序中的一个舞台。初级舞台由*台创建,并传递给Application类的start(Stage s)方法。您可以根据需要创建其他舞台。

一个舞台有包含其位置和大小的界限。舞台的边界由其四个属性定义:xywidthheightxy属性决定舞台左上角的位置。widthheight属性决定了它的大小。

舞台的区域可以分为两部分:内容区和装饰区。内容区域显示其场景的可视内容。通常,装饰由标题栏和边框组成。标题栏的存在及其内容根据*台提供的装饰类型而有所不同。JavaFX 中有五种类型的 stages:修饰的、未修饰的、透明的、统一的和实用的。

JavaFX 允许您拥有两种类型的窗口:模态窗口和非模态窗口。当显示模式窗口时,用户不能使用应用程序中的其他窗口,直到模式窗口被关闭。如果一个应用程序显示多个非模态窗口,用户可以随时在它们之间切换。JavaFX 为舞台定义了三种类型的模态:无模态、窗口模态和应用程序模态。“无”作为其模态的舞台是无模式窗口。将窗口模式作为其模式的舞台会阻止其所有者层次结构中的所有窗口。将应用程序模式作为其模式的舞台会阻塞应用程序中的所有其他窗口。

舞台的不透明度决定了您透过舞台可以看到的程度。您可以使用其setOpacity(double opacity)方法设置舞台的不透明度。不透明度值的范围从 0.0 到 1.0。不透明度为 0.0 表示舞台完全半透明;不透明度为 1.0 表示舞台完全不透明。不透明度会影响舞台的整个区域,包括其装饰。

您可以通过使用 stage 的setResizable(boolean resizable)方法来设置用户是否可以调整 stage 大小的提示。Stage类的setMinWidth()setMinHeight()setMaxWidth()setMaxHeight()方法允许您设置用户可以调整舞台大小的范围。一个 stage 可以通过调用它的setFullScreen(true)方法进入全屏模式。

你可以使用Stage类的show()showAndWait()方法来显示一个舞台。show()方法显示舞台并返回,而showAndWait()方法显示舞台并阻塞,直到舞台关闭。

下一章将向你展示如何创建场景和使用场景图。

五、创建场景

在本章中,您将学习:

  • JavaFX 应用程序中的场景和场景图是什么

  • 关于场景图形的不同渲染模式

  • 如何为场景设置光标

  • 如何确定场景中的焦点所有者

  • 如何使用PlatformHostServices

本章的例子在com.jdojo.scene包中。为了让它们工作,您必须在module-info.java文件中添加相应的一行:

...
opens com.jdojo.scene to javafx.graphics, javafx.base;
...

什么是场景?

一个场景代表一个舞台的视觉内容。javafx.scene包中的Scene类表示 JavaFX 程序中的一个场景。一个Scene对象一次最多只能连接到一个载物台。如果已经附加的场景被附加到另一个舞台,它将首先与前一个舞台分离。一个舞台在任何时候最多只能附加一个场景。

场景包含由可视节点组成的场景图。在这个意义上,场景充当了场景图的容器。场景图是一个树形数据结构,其元素被称为节点。场景图中的节点形成父子层次关系。场景图中的节点是javafx.scene.Node类的一个实例。节点可以是分支节点或叶节点。分支节点可以有子节点,而叶节点则不能。场景图中的第一个节点称为节点。根节点可以有子节点;但是,它从来没有父节点。图 5-1 显示了场景图中节点的排列。分支节点显示在圆角矩形中,叶节点显示在矩形中。

img/336502_2_En_5_Fig1_HTML.png

图 5-1

场景图中节点的排列

JavaFX 类库提供了许多类来表示场景图中的分支和叶节点。javafx.scene包中的Node类是场景图中所有节点的超类。图 5-2 显示了代表节点的类的部分类图。

img/336502_2_En_5_Fig2_HTML.jpg

图 5-2

javafx.scene.Node类的部分类图

场景总是有一个根节点。如果根节点是可调整大小的,例如一个Region或一个Control,它跟踪场景的大小。也就是说,如果调整了场景的大小,可调整大小的根节点会调整自身的大小以填充整个场景。基于根节点的策略,当场景的大小改变时,场景图可以被再次布局。

Group是一个不可调整大小的Parent节点,它可以被设置为场景的根节点。如果一个Group是一个场景的根节点,那么场景图的内容会被场景的大小裁剪掉。如果调整了场景的大小,场景图形将不会重新布局。

Parent是一个抽象类。它是场景图中所有分支节点的基类。如果要将分支节点添加到场景图形,请使用其具体子类之一的对象,例如,GroupPaneHBoxVBox。作为Node类而不是Parent类的子类的类表示叶节点,例如RectangleCircleTextCanvasImageView。场景图的根节点是一个特殊的分支节点,它是最顶端的节点。这就是创建Scene对象时使用GroupVBox作为根节点的原因。我将在第 10 和 12 章详细讨论表示分支和叶节点的类。表 5-1 列出了Scene类的一些常用属性。

表 5-1

Scene类的常用属性

|

类型

|

名字

|

属性和描述

ObjectProperty<Cursor> cursor 它为Scene定义了鼠标光标。
ObjectProperty<Paint> fill 它定义了Scene的背景填充。
ReadOnlyObjectProperty<Node> focusOwner 它定义了Scene中拥有焦点的节点。
ReadOnlyDoubleProperty height 它定义了Scene的高度。
ObjectProperty<Parent> root 它定义了场景图的根Node
ReadOnlyDoubleProperty width 它定义了Scene的宽度。
ReadOnlyObjectProperty<Window> window 它为Scene定义了Window
ReadOnlyDoubleProperty x 它定义了Scene在窗口上的水*位置。
ReadOnlyDoubleProperty y 它定义了Scene在窗口上的垂直位置。

图形渲染模式

场景图在屏幕上呈现 JavaFX 应用程序的内容时起着至关重要的作用。通常,有两种类型的 API 用于在屏幕上呈现图形:

  • 即时模式 API

  • 保留模式 API

在即时模式 API 中,当屏幕上需要一个框架时,应用程序负责发出绘制命令。图形直接画在屏幕上。当屏幕需要重新绘制时,应用程序需要向屏幕重新发出绘制命令。Java2D 是即时模式图形渲染 API 的一个例子。

在保留模式 API 中,应用程序创建图形对象并将其附加到图形。图形库,而不是应用程序代码,将图形保存在内存中。需要时,图形库会将图形呈现在屏幕上。应用程序只负责创建图形对象——“什么”部分;图形库负责存储和渲染图形,即“何时”和“如何”部分。保留模式呈现 API 将开发人员从编写呈现图形的逻辑中解放出来。例如,通过使用高级 API 从图形中添加或移除图形对象,从屏幕中添加或移除图形的一部分是简单的;图形库负责剩下的工作。与即时模式相比,保留模式 API 使用更多的内存,因为图形存储在内存中。JavaFX 场景图使用保留模式 API。

您可能认为使用即时模式 API 总是比使用保留模式 API 更快,因为前者直接在屏幕上呈现图形。然而,使用保留模式 API 打开了类库优化的大门,这在即时模式下是不可能的,在即时模式下,每个开发人员负责编写关于应该呈现什么以及何时呈现的逻辑。

图 5-3 和 5-4 分别说明了立即模式和保留模式 API 是如何工作的。它们展示了如何使用这两个 API 在屏幕上绘制文本、Hello 和六边形。

img/336502_2_En_5_Fig4_HTML.png

图 5-4

保留模式 API 的示例

img/336502_2_En_5_Fig3_HTML.png

图 5-3

即时模式 API 的示例

为场景设置光标

javafx.scene.Cursor类的一个实例代表一个鼠标光标。Cursor类包含许多常量,例如,HANDCLOSED_HANDDEFAULTTEXTNONEWAIT,用于标准鼠标光标。以下代码片段为场景设置WAIT光标:

Scene scene;
...
scene.setCursor(Cursor.WAIT);

您也可以为场景创建和设置自定义光标。如果指定的name是一个标准光标的名字,Cursor类的cursor(String name)静态方法返回一个标准光标。否则,它将指定的name视为光标位图的 URL。下面的代码片段从一个名为mycur.png的位图文件中创建一个光标,该文件假定位于CLASSPATH中:

// Create a Cursor from a bitmap
URL url = getClass().getClassLoader().getResource("mycur.png");
Cursor myCur = Cursor.cursor(url.toExternalForm());
scene.setCursor(myCur);

// Get the WAIT standard cursor using its name
Cursor waitCur = Cursor.cursor("WAIT")
scene.setCursor(waitCur);

场景中的焦点所有者

场景中只有一个节点可以是焦点所有者。Scene类的focusOwner属性跟踪拥有焦点的Node类。注意focusOwner属性是只读的。如果您希望场景中的特定节点成为焦点所有者,您需要调用Node类的requestFocus()方法。

您可以使用Scene类的getFocusOwner()方法来获取场景中具有焦点的节点的引用。一个场景可能没有焦点所有者,在这种情况下,getFocusOwner()方法返回null。例如,场景在创建时没有焦点所有者,但没有附加到窗口。

理解焦点所有者和拥有焦点的节点之间的区别很重要。每个场景可能有一个焦点所有者。比如打开两个窗口,就有两个场景,可以有两个焦点拥有者。但是,一次只能有两个焦点所有者中的一个拥有焦点。活动窗口的焦点所有者将获得焦点。要检查焦点所有者节点是否也有焦点,您需要使用Node类的focused属性。下面的代码片段显示了使用焦点所有者的典型逻辑:

表 5-2

*台类的方法

|

方法

|

描述

void exit() 它终止一个 JavaFX 应用程序。
boolean isFxApplicationThread() 如果调用线程是 JavaFX 应用程序线程,则返回true。否则返回false
boolean isImplicitExit() 它返回应用程序的隐式implicitExit属性的值。如果它返回true,意味着应用程序将在最后一个窗口关闭后终止。否则,你需要调用这个类的exit()方法来终止应用程序。
boolean isSupported(ConditionalFeature feature) 如果*台支持指定的条件特性,则返回true。否则返回false
void runLater(Runnable runnable) 它在 JavaFX 应用程序线程上执行指定的Runnable。执行的时间没有规定。该方法将Runnable发送到事件队列并立即返回。如果使用这种方法提交了多个Runnables,它们将按照提交到队列的顺序执行。
void setImplicitExit(boolean value) 它将implicitExit属性设置为指定的值。
Scene scene;
...
Node focusOwnerNode = scene.getFocusOwner();
if (focusOwnerNode == null) {
        // The scene does not have a focus owner
}
else if (focusOwnerNode.isFocused()) {
        // The focus owner is the one that has the focus
}
else {
        // The focus owner does not have the focus
}

了解*台

javafx.application包中的Platform类是用于支持*台相关功能的实用程序类。它由所有静态方法组成,这些方法在表 5-2 中列出。

runLater()方法用于向事件队列提交一个Runnable任务,因此它在 JavaFX 应用程序线程上执行。JavaFX 允许开发人员只在 JavaFX 应用程序线程上执行一些代码。清单 5-1 在init()方法中创建一个在 JavaFX 启动器线程上调用的任务。它使用Platform.runLater()方法提交稍后要在 JavaFX 应用程序线程上执行的任务。

Tip

使用Platform.runLater()方法执行在 JavaFX 应用程序线程之外的线程上创建的任务,但该任务需要在 JavaFX 应用程序线程上运行。

// RunLaterApp.java
package com.jdojo.scene;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.stage.Stage;

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

        @Override
        public void init() {
                System.out.println("init(): " +
                         Thread.currentThread().getName());

                // Create a Runnable task
                Runnable task = () ->
                         System.out.println("Running the task on the "
                       + Thread.currentThread().getName());

                // Submit the task to be run on the JavaFX Application
                // Thread
                Platform.runLater(task);
        }

        @Override
        public void start(Stage stage) throws Exception {
                stage.setScene(new Scene(new Group(), 400, 100));
                stage.setTitle("Using Platform.runLater() Method");
                stage.show();
        }
}
init(): JavaFX-Launcher
Running the task on the JavaFX Application Thread

Listing 5-1Using the Platform.runLater() Method

JavaFX 实现中的一些特性是可选的(或有条件的)。它们可能无法在所有*台上使用。在不支持可选功能的*台上使用该功能不会导致错误;可选特性被简单地忽略了。可选特性被定义为javafx.application包中ConditionalFeature枚举的枚举常量,如表 5-3 所列。

表 5-3

ConditionalFeature枚举中定义的常量

|

枚举常量

|

描述

EFFECT 指示滤镜效果的可用性,例如,倒影、阴影等。
INPUT_METHOD 指示文本输入法的可用性。
SCENE3D 指示 3D 功能的可用性。
SHAPE_CLIP 指示可以针对任意形状裁剪节点。
TRANSPARENT_WINDOW 指示全窗口透明度的可用性。

假设您的 JavaFX 应用程序根据用户需求使用 3D GUI。您可以编写启用 3D 功能的逻辑,如以下代码所示:

import javafx.application.Platform;
import static javafx.application.ConditionalFeature.SCENE3D;
...
if (Platform.isSupported(SCENE3D)) {
        // Enable 3D features
}
else {
        // Notify the user that 3D features are not available
}

了解主机环境

javafx.application包中的HostServices类提供与托管 JavaFX 应用程序的启动环境(本书的桌面)相关的服务。您不能直接创建HostServices类的实例。Application类的getHostServices()方法返回HostServices类的一个实例。以下是如何在从Application类继承的类中获取HostServices实例的示例:

HostServices host = getHostServices();

HostServices类包含以下方法:

  • String getCodeBase()

  • String getDocumentBase()

  • String resolveURI(String base, String relativeURI)

  • void showDocument(String uri)

getCodeBase()方法返回应用程序的代码库统一资源标识符(URI)。在独立模式下,它返回包含用于启动应用程序的 JAR 文件的目录的 URI。如果应用程序是使用类文件启动的,它将返回一个空字符串。

getDocumentBase()方法返回文档库的 URI。它返回以独立模式启动的应用程序的当前目录的 URI。

resolveURI()方法根据指定的基本 URI 解析指定的相对 URI,并返回解析后的 URI。

方法在新的浏览器窗口中打开指定的 URI。视浏览器偏好而定,它可能会在新标签页中打开 URI。以下代码片段打开 Yahoo!主页:

getHostServices().showDocument("http://www.yahoo.com");

清单 5-2 中的程序使用了HostServices类的所有方法。它显示了一个带有两个按钮和主机详细信息的阶段。一键打开雅虎!另一个显示一个警告框。根据应用程序的启动方式,舞台上显示的输出会有所不同。

// KnowingHostDetailsApp.java
package com.jdojo.scene;

import java.util.HashMap;
import java.util.Map;
import javafx.application.Application;
import javafx.application.HostServices;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import javafx.stage.Modality;
import javafx.stage.Stage;
import javafx.stage.StageStyle;

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

        @Override
        public void start(Stage stage) {
                String yahooURL = "http://www.yahoo.com";
                Button openURLButton = new Button("Go to Yahoo!");
                openURLButton.setOnAction(e →
                         getHostServices().showDocument(yahooURL));

                Button showAlert = new Button("Show Alert");
                showAlert.setOnAction(e -> showAlert());

                VBox root = new VBox();

                // Add buttons and all host related details to the VBox
                root.getChildren().addAll(openURLButton, showAlert);

                Map<String, String> hostdetails = getHostDetails();
                for(Map.Entry<String, String> entry :
                                hostdetails.entrySet()) {
                    String desc = entry.getKey() + ": " +
                              entry.getValue();
                    root.getChildren().add(new Label(desc));
                }

                Scene scene = new Scene(root);
                stage.setScene(scene);
                stage.setTitle("Knowing the Host");
                stage.show();
        }

        protected Map<String, String> getHostDetails() {
                Map<String, String> map = new HashMap<>();
                HostServices host = this.getHostServices();

                String codeBase = host.getCodeBase();
                map.put("CodeBase", codeBase);

                String documentBase = host.getDocumentBase();
                map.put("DocumentBase", documentBase);

                String splashImageURI =
                         host.resolveURI(documentBase, "splash.jpg");
                map.put("Splash Image URI", splashImageURI);

                return map;
        }

        protected void showAlert() {
                Stage s = new Stage(StageStyle.UTILITY);
                s.initModality(Modality.WINDOW_MODAL);

                Label msgLabel = new Label("This is an FX alert!");
                Group root = new Group(msgLabel);
                Scene scene = new Scene(root);
                s.setScene(scene);

                s.setTitle("FX Alert");
                s.show();
        }
}

Listing 5-2Knowing the Details of the Host Environment for a JavaFX Application

摘要

场景代表舞台的视觉内容。javafx.scene包中的Scene类表示 JavaFX 程序中的一个场景。一个Scene对象一次最多被附加到一个阶段。如果已经附加的场景被附加到另一个舞台,它将首先与前一个舞台分离。一个舞台在任何时候最多只能附加一个场景。

场景包含由可视节点组成的场景图。在这个意义上,场景充当了场景图的容器。场景图是一种树形数据结构,其元素称为节点。场景图中的节点形成父子层次关系。场景图中的节点是javafx.scene.Node类的一个实例。节点可以是分支节点或叶节点。分支节点可以有子节点,而叶节点则不能。场景图中的第一个节点称为根节点。根节点可以有子节点;但是,它从来没有父节点。

javafx.scene.Cursor类的一个实例代表一个鼠标光标。Cursor类包含许多常量,例如,HANDCLOSED_HANDDEFAULTTEXTNONEWAIT,用于标准鼠标光标。您可以使用Scene类的setCursor()方法为场景设置光标。

场景中只有一个节点可以是焦点所有者。Scene类的只读属性focusOwner跟踪拥有焦点的节点。如果您希望场景中的特定节点成为焦点所有者,您需要调用Node类的requestFocus()方法。每个场景可能有一个焦点所有者。例如,如果你打开两个窗口,你将有两个场景,你可能有两个焦点所有者。但是,一次只能有两个焦点所有者中的一个拥有焦点。活动窗口的焦点所有者将获得焦点。要检查焦点所有者节点是否也有焦点,您需要使用Node类的focused属性。

javafx.application包中的Platform类是用于支持*台相关功能的实用程序类。它包含终止应用程序、检查正在执行的代码是否在 JavaFX 应用程序线程上执行等方法。

javafx.application包中的HostServices类提供与托管 JavaFX 应用程序的启动环境(本书的桌面)相关的服务。您不能直接创建HostServices类的实例。Application类的getHostServices()方法返回HostServices类的一个实例。

下一章将详细讨论节点。

六、了解节点

在本章中,您将学习:

  • JavaFX 中的节点是什么

  • 关于笛卡尔坐标系

  • 关于节点的边界和边界框

  • 如何设置节点的大小以及如何定位节点

  • 如何在节点中存储用户数据

  • 什么是受管节点

  • 如何在坐标空间之间转换节点的边界

本章的例子在com.jdojo.node包中。为了让它们工作,您必须在module-info.java文件中添加相应的一行:

...
opens com.jdojo.node to javafx.graphics, javafx.base;
...

什么是节点?

第五章向你介绍了场景和场景图。场景图是一种树形数据结构。场景图中的每一项都称为一个节点javafx.scene.Node类的一个实例表示场景图中的一个节点。注意,Node类是一个抽象类,存在几个具体的类来表示特定类型的节点。

一个节点可以有子项(也称为子节点),这些节点称为分支节点。分支节点是Parent的一个实例,它的具体子类是GroupRegionWebView。不能有子项的节点称为叶节点。诸如RectangleTextImageViewMediaView之类的实例是叶节点的例子。每个场景图树中只有一个节点没有父节点,称为根节点。一个节点在场景图中的任何地方最多出现一次。

如果节点尚未附加到场景,则可以在任何线程上创建和修改节点。将节点附加到场景中以及随后的修改必须发生在 JavaFX 应用程序线程上。

一个节点有几种类型的边界。界限是相对于不同的坐标系确定的。下一节将讨论一般的笛卡尔坐标系;以下部分解释了如何使用笛卡尔坐标系来计算 JavaFX 中节点的边界。

笛卡尔坐标系

如果你在高中的坐标几何课上学习过(并且还记得)笛卡尔坐标系,你可以跳过这一节。

笛卡尔坐标系是唯一定义 2D *面上每个点的方法。有时,它也被称为直角坐标系。它由两条垂直的直线组成,即 x 轴和 y 轴。两轴相交的点称为原点

2D *面中的一个点由两个值定义,即它的 x 和 y 坐标。一个点的 x 和 y 坐标分别是它与 y 轴和 x 轴的垂直距离。沿着轴,距离在原点的一侧测量为正,在另一侧测量为负。原点有(x,y)坐标,比如(0,0)。这些轴将*面分成四个象限。注意,2D *面本身是无限的,四个象限也是无限的。笛卡尔坐标系中所有点的集合定义了该系统的坐标空间

图 6-1 显示了笛卡尔坐标系的图解。它显示了具有 x1 和 y1 的 x 和 y 坐标的点 P。它显示了每个象限中 x 和 y 坐标的值的类型。例如,右上象限显示(+、+),这意味着该象限中所有点的 x 和 y 坐标都为正值。

img/336502_2_En_6_Fig1_HTML.png

图 6-1

坐标几何中使用的二维笛卡尔坐标系

变换是坐标空间中的点到同一坐标空间的映射,保留一组预定义的几何属性。几种类型的变换可以应用于坐标空间中的点。变换类型的一些例子是*移旋转缩放剪切

在*移变换中,一对固定的数字被添加到所有点的坐标中。假设您想通过(a,b)将*移应用于坐标空间。如果一个点在*移之前具有坐标(x,y ),那么它在*移之后将具有坐标(x + a,y + b)。

在旋转变换中,轴围绕坐标空间中的轴心点旋转,并且点的坐标被映射到新的轴。图 6-2 显示了*移和旋转变换的例子。

img/336502_2_En_6_Fig2_HTML.png

图 6-2

*移和旋转变换的示例

在图 6-2 中,变换前的轴用实线表示,变换后的轴用虚线表示。注意,点 P 在(4,3)处的坐标在*移和旋转的坐标空间中保持不变。但是,该点相对于原始坐标空间的坐标在变换后会发生变化。原始坐标空间中的点以纯黑色填充颜色显示,而在转换后的坐标空间中,该点没有填充颜色。在旋转变换中,您已经使用原点作为轴心点。因此,原始坐标空间和变换坐标空间的原点是相同的。

节点的笛卡尔坐标系

场景图中的每个节点都有自己的坐标系。节点使用由 x 轴和 y 轴组成的笛卡尔坐标系。在计算机系统中,x 轴上的值向右增加,y 轴上的值向下增加,如图 6-3 所示。通常,当显示节点的坐标系时,x 轴和 y 轴的负边不会显示,即使它们总是存在。图 6-3 的右部显示了简化版坐标系。一个节点可以有负的 x 和 y 坐标。

img/336502_2_En_6_Fig3_HTML.png

图 6-3

节点的坐标系

在典型的 GUI 应用程序中,节点被放置在它们的父节点中。根节点是所有节点的最终父节点,它位于场景内部。场景放置在舞台内,舞台放置在屏幕内。组成一个窗口的每个元素,从节点到屏幕,都有自己的坐标系,如图 6-4 所示。

img/336502_2_En_6_Fig4_HTML.png

图 6-4

构成 GUI 窗口的所有元素的坐标系

最外面的矩形区域是屏幕,带有粗黑边框。剩下的是一个 JavaFX stage,带有一个区域和一个矩形。该区域的背景颜色为浅灰色,矩形的背景颜色为蓝色。该区域是矩形的父区域。这个简单的窗口使用五个坐标空间,如图 6-4 所示。我只标注了 x 轴。所有 y 轴都是垂直线,在原点与各自的 x 轴相交。

矩形左上角的坐标是什么?问题不完整。点的坐标是相对于坐标系定义的。如图 6-4 所示,你有五个坐标系,因此有五个坐标空间。因此,必须指定要知道矩形左上角坐标的坐标系。在一个节点的坐标系中,它们是(10,15);在父母的坐标系中,它们是(40,45);在一个场景的坐标系中,它们是(60,55);在一个阶段的坐标系中,它们是(64,83);在屏幕的坐标系中,它们是(80,99)。

边界和包围盒的概念

每个节点都有一个几何形状,它位于一个坐标空间中。节点的大小和位置统称为其边界。节点的边界是根据包围该节点的整个几何形状的边界矩形框来定义的。图 6-5 显示了一个三角形、一个圆形、一个圆角矩形和一个带实线边框的矩形。用虚线边框显示的矩形是这些形状(节点)的边界框。

img/336502_2_En_6_Fig5_HTML.png

图 6-5

定义节点几何形状的边界矩形框

由节点的几何形状及其边界框覆盖的面积(2D 空间中的面积和 3D 空间中的体积)可以不同。比如图 6-5 中的前三个节点,从左边数,节点的面积和它们的包围盒是不一样的。然而,对于没有圆角的最后一个矩形,其面积和其边界框的面积是相同的。

javafx.geometry.Bounds类的一个实例代表一个节点的边界。Bounds类是一个抽象类。BoundingBox类是Bounds类的具体实现。Bounds类被设计用来处理 3D 空间中的边界。它用边界框中的最小深度以及边界框的宽度、高度和深度封装左上角的坐标。方法getMinX()getMinY()getMinZ()用于获取坐标。使用getWidth()getHeight()getDepth()方法访问边界框的三个维度。Bounds类包含getMaxX()getMaxY()getMaxZ()方法,这些方法返回边界框中右下角最大深度的坐标。

在 2D 空间中,minXminY分别定义边界框左上角的 x 和 y 坐标,maxXmaxY分别定义右下角的 x 和 y 坐标。在 2D 空间中,边界框的 z 坐标值和深度值为零。图 6-6 显示了 2D 坐标空间中边界框的细节。

img/336502_2_En_6_Fig6_HTML.png

图 6-6

2D 空间中边界框的制作

Bounds类包含isEmpty()contains()intersects()实用方法。如果一个Bounds的三个维度(宽度、高度或深度)中的任何一个是负数,isEmpty()方法返回truecontains()方法允许您检查一个Bounds是否包含另一个Bounds、一个 2D 点或一个 3D 点。intersects()方法允许您检查一个Bounds的内部是否与另一个Bounds、2D 点或 3D 点的内部相交。

知道节点的边界

到目前为止,我已经讨论了与节点相关的坐标系统、边界和边界框等主题。那个讨论是为了让你为这一节做准备,这一节是关于知道一个节点的边界。您可能已经猜到(虽然不正确)了Node类应该有一个getBounds()方法来返回节点的边界。要是这么简单就好了!在这一节中,我将讨论不同类型的节点边界的细节。在下一节中,我将带您看一些例子。

图 6-7 显示了一个带有三种形式文本“关闭”的按钮。

img/336502_2_En_6_Fig7_HTML.jpg

图 6-7

有和没有效果和变形的按钮

第一个,从左边开始,没有效果或变换。第二个有投影效果。第三个有投影效果和旋转变换。图 6-8 显示了代表这三种形式的按钮边界的边界框。暂时忽略坐标,您可能会注意到按钮的边界会随着效果和变换的应用而改变。

img/336502_2_En_6_Fig8_HTML.jpg

图 6-8

具有和不具有效果的按钮以及具有边界框的变换

场景图中的节点有三种类型的边界,在Node类中定义为三个只读属性:

  • layoutBounds

  • boundsInLocal

  • boundsInParent

当你试图理解一个节点的三种界限时,你需要寻找三个点:

img/336502_2_En_6_Fig9_HTML.png

图 6-9

影响节点大小的因素

  • (minXminY)值是如何定义的。它们定义了由Bounds对象描述的边界框左上角的坐标。

  • 请记住,点的坐标总是相对于坐标空间来定义的。因此,请注意在第一步中描述的定义坐标的坐标空间。

  • 特定类型的边界中包含节点的哪些属性(几何图形、描边、效果、剪辑和变换)。

图 6-9 显示了构成节点边界的节点属性。它们按顺序从左到右应用。一些节点类型(例如,CircleRectangle)可能具有非零笔画。非零笔划被认为是节点几何的一部分,用于计算其边界。

表 6-1 列出了有助于特定类型节点边界的属性以及定义边界的坐标空间。节点的boundsInLocalboundsInParent也被称为其物理边界,因为它们对应于节点的物理属性。节点的layoutBounds被称为逻辑边界,因为它不一定绑定到节点的物理边界。当一个节点的几何图形改变时,所有的边界都被重新计算。

表 6-1

为节点边界提供属性

|

界限类型

|

坐标空间

|

贡献者

layoutBounds 节点(未转换) 节点的几何形状非零笔划
boundsInLocal 节点(未转换) 节点的几何形状非零笔划效果夹子
boundsInParent 父母 节点的几何形状非零笔划效果夹子转换

Tip

boundsInLocalBoundsInParent被称为物理或可视边界,因为它们对应于节点的可视外观。layoutBounds也被称为逻辑边界,因为它不一定对应于节点的物理边界。

布局绑定属性

layoutBounds属性是基于节点在未变换的局部坐标空间中的几何属性来计算的。不包括效果、剪辑和变换。根据节点的可调整行为,使用不同的规则来计算由layoutBounds描述的边界框左上角的坐标:

  • 对于一个可调整大小的节点(一个Region、一个Control和一个WebView),边界框左上角的坐标总是被设置为(0,0)。例如,对于一个按钮,layoutBounds属性中的(minXminY))值总是(0,0)。

  • 对于一个不可调整大小的节点(一个Shape,一个Text,和一个Group,边界框左上角的坐标是基于几何属性计算的。对于一种形状(矩形、圆形等。)或者一个Text,可以指定节点中特定点相对于该节点未变换坐标空间的(x,y)坐标。例如,对于一个矩形,您可以指定左上角的(x,y)坐标,该坐标成为由其layoutBounds属性描述的边界框的左上角的(x,y)坐标。对于一个圆,可以指定centerXcenterYradius属性,其中centerXcenterY分别是圆心的 x 和 y 坐标。由layoutBounds描述的圆形边界框左上角的(x,y)坐标计算如下(centerX–半径,centerY–半径)。

layoutBounds中的宽度和高度是节点的宽度和高度。有些节点允许您设置它们的宽度和高度;但是有些会自动为您计算它们,并让您覆盖它们。

在哪里使用节点的layoutBounds属性?容器根据它们的layoutBounds分配空间来布局子节点。让我们看一个如清单 6-1 所示的例子。它在一个VBox中显示四个按钮。第一个按钮有投影效果。第三个按钮有投影效果和 30 度旋转变换。第二个和第四个按钮没有效果或变形。产生的屏幕如图 6-10 所示。输出显示,不管效果和变换如何,所有按钮都具有相同的layoutBounds值。所有按钮的layoutBounds对象的大小(宽度和高度)由按钮的文本和字体决定,这对于所有按钮都是一样的。在您的*台上,输出可能有所不同。

img/336502_2_En_6_Fig10_HTML.png

图 6-10

layoutBounds属性不包括效果和变换

// LayoutBoundsTest.java
package com.jdojo.node;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.effect.DropShadow;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

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

        @Override
        public void start(Stage stage) {
                Button b1 = new Button("Close");
                b1.setEffect(new DropShadow());

                Button b2 = new Button("Close");

                Button b3 = new Button("Close");
                b3.setEffect(new DropShadow());
                b3.setRotate(30);

                Button b4 = new Button("Close");

                VBox root = new VBox();
                root.getChildren().addAll(b1, b2, b3, b4);

                Scene scene = new Scene(root);
                stage.setScene(scene);
                stage.setTitle("Testing LayoutBounds");
                stage.show();

                System.out.println("b1=" + b1.getLayoutBounds());
                System.out.println("b2=" + b2.getLayoutBounds());
                System.out.println("b3=" + b3.getLayoutBounds());
                System.out.println("b4=" + b4.getLayoutBounds());
        }
}
b1=BoundingBox [minX:0.0, minY:0.0, minZ:0.0, width:57.0, height:23.0, depth:0.0, maxX:57.0, maxY:23.0, maxZ:0.0]
b2=BoundingBox [minX:0.0, minY:0.0, minZ:0.0, width:57.0, height:23.0, depth:0.0, maxX:57.0, maxY:23.0, maxZ:0.0]
b3=BoundingBox [minX:0.0, minY:0.0, minZ:0.0, width:57.0, height:23.0, depth:0.0, maxX:57.0, maxY:23.0, maxZ:0.0]
b4=BoundingBox [minX:0.0, minY:0.0, minZ:0.0, width:57.0, height:23.0, depth:0.0, maxX:57.0, maxY:23.0, maxZ:0.0]

Listing 6-1Accessing the layoutBounds of Buttons with and Without Effects

有时,您可能希望在节点的layoutBounds中包含显示节点效果和变换所需的空间。解决这个问题的方法很简单。您需要将节点包装在一个Group中,将Group包装在一个容器中。现在,容器将向Group查询它的layoutBounds。一个GrouplayoutBounds是其所有子节点的boundsInParent的并集。回想一下(见表 6-1 )节点的boundsInParent包括显示效果和节点变换所需的空间。如果你改变陈述

root.getChildren().addAll(b1, b2, b3, b4);

在清单 6-1 中为

root.getChildren().addAll(new Group(b1), b2, new Group(b3), b4);

产生的屏幕如图 6-11 所示。这一次,VBox为第一组和第三组分配了足够的空间,以考虑应用于包装按钮的效果和变换。

img/336502_2_En_6_Fig11_HTML.png

图 6-11

使用Group为节点的效果和变换分配空间

Tip

基于节点的几何属性来计算节点的layoutBounds。因此,您不应该将节点的这种属性绑定到包含节点的layoutBounds的表达式。

boundsInLocal 属性

boundsInLocal属性是在节点的未变换坐标空间中计算的。它包括节点、效果和剪辑的几何属性。不包括应用于节点的变换。

清单 6-2 打印一个按钮的layoutBoundsboundsInLocalboundsInLocal属性包括按钮周围的阴影效果。注意,layoutBounds定义的边界框左上角的坐标是(0.0,0.0),boundsInLocal的坐标是(–9.0,–9.0)。不同*台上的输出可能会有所不同,因为节点的大小是根据运行程序的*台自动计算的。

// BoundsInLocalTest.java
package com.jdojo.node;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.effect.DropShadow;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

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

        @Override
        public void start(Stage stage) {
                Button b1 = new Button("Close");
                b1.setEffect(new DropShadow());

                VBox root = new VBox();
                root.getChildren().addAll(b1);

                Scene scene = new Scene(root);
                stage.setScene(scene);
                stage.setTitle("Testing LayoutBounds");
                stage.show();

                System.out.println("b1(layoutBounds)=" +
                         b1.getLayoutBounds());
                System.out.println("b1(boundsInLocal)=" +
                         b1.getBoundsInLocal());
        }
}
b1(layoutBounds)=BoundingBox [minX:0.0, minY:0.0, minZ:0.0, width:57.0, height:23.0, depth:0.0, maxX:57.0, maxY:23.0, maxZ:0.0]
b1(boundsInLocal)=BoundingBox [minX:-9.0, minY:-9.0, minZ:0.0, width:75.0, height:42.0, depth:0.0, maxX:66.0, maxY:33.0, maxZ:0.0]

Listing 6-2Accessing the boundsInLocal Property of a Node

什么时候使用节点的boundsInLocal?当你需要包含一个节点的效果和剪辑时,你可以使用boundsInLocal。假设你有一个带反射的Text节点,你想让它垂直居中。如果你使用Text节点的layoutBounds,它只会将节点的文本部分居中,而不会包括反射。如果您使用boundsInLocal,它将使文本与其倒影居中。另一个例子是检查有影响的球的碰撞。如果两个球之间发生碰撞,当一个球在另一个球的边界内移动,包括它们的效果,使用球的boundsInLocal。如果碰撞只发生在它们的几何边界相交时,使用layoutBounds

boundsInParent 属性

节点的boundsInParent属性位于其父节点的坐标空间中。它包括节点、效果、剪辑和变换的几何属性。它很少直接用在代码中。

群的界限

一个GrouplayoutBoundsboundsInLocalboundsInParent的计算不同于一个节点的计算。一个Group承担其子节点的集合边界。您可以对每个Group子对象分别应用效果、剪辑和变换。您还可以直接在Group上应用效果、剪辑和变换,它们会应用到它的所有子节点。

一个GrouplayoutBounds是其所有子节点的boundsInParent的并集。它包括直接应用到子对象的效果、剪辑和变换。它不包括直接应用于Group的效果、剪辑和变换。GroupboundsInLocal是通过取其layoutBounds并包括直接应用于Group的效果和剪辑来计算的。GroupboundsInParent通过取其boundsInLocal并包括直接应用于Group的变换来计算。

当您想要为应该包括效果、剪辑和变换的节点分配空间时,您需要尝试将节点包装在Group中。假设您有一个带有效果和变换的节点,并且您只想为它的效果而不是它的变换分配布局空间。您可以通过在节点上应用效果并将其包装在一个Group中,然后在Group上应用变换来实现这一点。

一个关于界限的详细例子

在这一节中,我将通过一个例子向您展示如何计算节点的边界。在本例中,您将使用一个矩形及其不同的属性、效果和变换。

请考虑下面的代码片段,它创建了一个 50 x 20 的矩形,并将其放置在矩形的局部坐标空间中的(0,0)处。生成的矩形如图 6-12 所示,其中显示了父节点的轴和节点未变换的局部轴(本例中为矩形),此时是相同的:

img/336502_2_En_6_Fig12_HTML.png

图 6-12

一个 50 x 20 的矩形,放置在(0,0)处,没有任何效果和变换

Rectangle r = new Rectangle(0, 0, 50, 20);
r.setFill(Color.GRAY);

矩形的三种边界是相同的,如下所示:

layoutBounds[minX=0.0, minY=0.0, width=50.0, height=20.0]
boundsInLocal[minX=0.0, minY=0.0, width=50.0, height=20.0]
boundsInParent[minX=0.0, minY=0.0, width=50.0, height=20.0]

让我们修改矩形,将其放置在(75,50)处,如下所示:

Rectangle r = new Rectangle(75, 50, 50, 20);

结果节点如图 6-13 所示。

img/336502_2_En_6_Fig13_HTML.jpg

图 6-13

放置在(75,50)处的 50 乘 20 的矩形,没有效果和变换

父节点和节点的轴仍然相同。所有界限都是相同的,如下所示。所有边界框的左上角已经移动到(75,50),宽度和高度都相同:

layoutBounds[minX=75.0, minY=50.0, width=50.0, height=20.0]
boundsInLocal[minX=75.0, minY=50.0, width=50.0, height=20.0]
boundsInParent[minX=75.0, minY=50.0, width=50.0, height=20.0]

让我们修改矩形,并给它一个阴影效果,如下所示:

Rectangle r = new Rectangle(75, 50, 50, 20);
r.setEffect(new DropShadow());

结果节点如图 6-14 所示。

img/336502_2_En_6_Fig14_HTML.png

图 6-14

放置在(75,50)处的一个 50 x 20 的矩形,带有投影,没有变换

父节点和节点的轴仍然相同。现在,layoutBounds没有改变。为了适应投影效果,boundsInLocalboundsInParent已经改变,它们具有相同的值。回想一下,boundsInLocal是在节点的未变换坐标空间中定义的,而boundsInParent是在父节点的坐标空间中定义的。在这种情况下,两个坐标空间是相同的。因此,两个边界的相同值定义了相同的边界框。界限的值如下:

layoutBounds[minX=75.0, minY=50.0, width=50.0, height=20.0]
boundsInLocal[minX=66.0, minY=41.0, width=68.0, height=38.0]
boundsInParent[minX=66.0, minY=41.0, width=68.0, height=38.0]

让我们修改前面的矩形,使其具有(150,75)的(x,y)*移,如下所示:

Rectangle r = new Rectangle(75, 50, 50, 20);
r.setEffect(new DropShadow());
r.getTransforms().add(new Translate(150, 75));

结果节点如图 6-15 所示。转换(在本例中是*移)转换了节点的坐标空间,结果,您看到的是被转换的节点。在这种情况下,您需要考虑三个坐标空间:父节点的坐标空间以及节点的未转换和已转换坐标空间。layoutBoundsboundsInParent是相对于节点未变换的局部坐标空间。boundsInParent是相对于父对象的坐标空间。图 6-15 显示了游戏中的所有坐标空间。界限的值如下:

img/336502_2_En_6_Fig15_HTML.jpg

图 6-15

一个 50 x 20 的矩形,放置在(75,50)处,带有投影和(150,75)*移

layoutBounds[minX=75.0, minY=50.0, width=50.0, height=20.0]
boundsInLocal[minX=66.0, minY=41.0, width=68.0, height=38.0]
boundsInParent[minX=216.0, minY=116.0, width=68.0, height=38.0]

让我们修改矩形,使其具有(150,75)的(x,y)*移和 30 度顺时针旋转:

Rectangle r = new Rectangle(75, 50, 50, 20);
r.setEffect(new DropShadow());
r.getTransforms().addAll(new Translate(150, 75), new Rotate(30));

产生的节点如图 6-16 所示。请注意,*移和旋转已应用于矩形的局部坐标空间,矩形出现在相对于其变换后的局部坐标轴的相同位置。layoutBoundsboundsInLocal保持不变,因为你没有改变矩形的几何形状和效果。boundsInParent已经改变,因为你添加了一个旋转。界限的值如下:

img/336502_2_En_6_Fig16_HTML.jpg

图 6-16

一个 50 x 20 的矩形,放置在(75,50)处,带有投影,*移(150,75),顺时针旋转 30 度

layoutBounds[minX=75.0, minY=50.0, width=50.0, height=20.0]
boundsInLocal[minX=66.0, minY=41.0, width=68.0, height=38.0]
boundsInParent[minX=167.66, minY=143.51, width=77.89, height=66.91]

作为最后一个示例,您将向矩形添加缩放和剪切变换:

Rectangle r = new Rectangle(75, 50, 50, 20);
r.setEffect(new DropShadow());
r.getTransforms().addAll(new Translate(150, 75), new Rotate(30),
                         new Scale(1.2, 1.2), new Shear(0.30, 0.10));

结果节点如图 6-17 所示。

img/336502_2_En_6_Fig17_HTML.jpg

图 6-17

一个放置在(75,50)处的 50 乘 20 的矩形,带有投影,一个(150,75)*移,一个 30 度顺时针旋转,一个 1.2 英寸的 x 和 y 缩放,以及一个 0.30 x 剪切和 0.10 y 剪切

请注意,只有boundsInParent发生了变化。界限的值如下:

layoutBounds[minX=75.0, minY=50.0, width=50.0, height=20.0]
boundsInLocal[minX=66.0, minY=41.0, width=68.0, height=38.0]
boundsInParent[minX=191.86, minY=171.45, width=77.54, height=94.20]

对于初学者来说,掌握节点不同类型界限背后的概念并不容易。初学者是第一次学习某样东西的人。我开始是一个初学者,学习边界。在学习过程中,另一个美丽的概念以及它在 JavaFX 程序中的实现出现了。这个程序是一个非常详细的演示应用程序,它帮助您直观地理解改变节点的状态是如何影响边界的。您可以保存带有所有坐标轴的场景图形。您可以运行清单 6-3 中所示的NodeBoundsApp类来查看本节中的所有示例。

// NodeBoundsApp.java
package com.jdojo.node;
...
public class NodeBoundsApp extends Application {
        // The code for this class is not included here as it is very big.
        // Please refer to the source code. You can download the source code
        // for all programs in this book from
          // http://www.apress.com/source-code
}

Listing 6-3Computing the Bounds of a Node

使用布局布局定位节点

如果您不理解所有与布局相关的属性背后的细节和原因,那么在 JavaFX 中布置节点是非常令人困惑的。Node类有两个属性,layoutXlayoutY,分别定义其坐标空间沿 x 轴和 y 轴的*移。Node类有做同样事情的translateXtranslateY属性。节点坐标空间的最终*移是两者之和:

finalTranslationX = layoutX + translateX
finalTranslationY = layoutY + translateY

为什么有两个属性来定义同类翻译?原因很简单。它们的存在是为了在不同的情况下获得相似的结果。使用layoutXlayoutY定位稳定布局的节点。使用translateXtranslateY为动态布局定位一个节点,例如在动画过程中。

记住layoutXlayoutY属性不指定节点的最终位置是很重要的。它们是应用于节点的坐标空间的*移。当您计算layoutXlayoutY的值以将节点定位在特定位置时,您需要考虑layoutBoundsminXminY值。要将节点边界框的左上角定位在finalXfinalY,请使用以下公式:

layoutX = finalX - node.getLayoutBounds().getMinX()
layoutY = finalY - node.getLayoutBounds().getMinY()

Tip

Node类有一个方便的方法relocate(double finalX, double finalY),将节点定位在(finalX, finalY)位置。该方法计算并正确设置layoutXlayoutY值,考虑layoutBoundsminXminY值。为了避免错误和节点的错位,我更喜欢使用relocate()方法,而不是setLayoutX()setLayoutY()方法。

有时,设置节点的layoutXlayoutY属性可能无法将它们定位在其父节点内的所需位置。如果遇到这种情况,请检查父类型。大多数父母是Region类的子类,他们使用自己的定位策略,忽略孩子的layoutXlayoutY设置。比如HBoxVBox使用自己的定位策略,他们会忽略layoutXlayoutY的值给孩子。

下面的代码片段将忽略两个按钮的layoutXlayoutY值,因为它们被放在使用自己的定位策略的VBox中。最终布局如图 6-18 所示。

img/336502_2_En_6_Fig18_HTML.jpg

图 6-18

两个按钮使用layoutXlayoutY属性并放置在一个VBox

Button b1 = new Button("OK");
b1.setLayoutX(20);
b1.setLayoutY(20);

Button b2 = new Button("Cancel");
b2.setLayoutX(50);
b2.setLayoutY(50);

VBox vb = new VBox();
vb.getChildren().addAll(b1, b2);

如果您想完全控制一个节点在其父节点中的位置,请使用PaneGroup。一个Pane是一个Region,不定位其子节点。您需要使用layoutXlayoutY属性来定位孩子。下面的代码片段将布局两个按钮,如图 6-19 所示,其中显示了坐标网格,线之间相隔 10px:

img/336502_2_En_6_Fig19_HTML.jpg

图 6-19

使用layoutXlayoutY属性的两个按钮,放置在GroupPane

Button b1 = new Button("OK");
b1.setLayoutX(20);
b1.setLayoutY(20);

Button b2 = new Button("Cancel");
b2.setLayoutX(50);
b2.setLayoutY(50);

Group parent = new Group(); //Or. Pane parent = new Pane();
parent.getChildren().addAll(b1, b2);

设置节点的大小

每个节点都有一个大小(宽度和高度),可以更改。也就是说,每个节点都可以调整大小。有两种类型的节点:可调整大小的节点和不可调整大小的节点。前面两句话不矛盾吗?答案是肯定的,也是否定的。的确,每个节点都有调整大小的潜力。但是,可调整大小的节点意味着在布局过程中,节点可以由其父节点调整大小。例如,按钮是可调整大小的节点,矩形是不可调整大小的节点。当一个按钮被放置在一个容器中时,例如,在一个HBox中,HBox决定了按钮的最佳大小。HBox根据按钮显示所需的空间和HBox可用的空间来调整按钮的大小。当一个矩形被放置在一个HBox中时,HBox并不决定它的大小;相反,它使用应用程序指定的矩形大小。

Tip

在布局过程中,可调整大小的节点可以由其父节点调整大小。在布局过程中,不可调整大小的节点不会被其父节点调整大小。如果要调整不可调整大小的节点的大小,需要修改影响其大小的属性。例如,要调整矩形的大小,您需要更改它的widthheight属性。RegionControlWebView是可调整大小的节点的例子。GroupTextShape是不可调整大小的节点的例子。

如何知道一个节点是否可以调整大小?Node类中的isResizable()方法为可调整大小的节点返回true;对于不可调整大小的节点,它返回false

清单 6-4 中的程序显示了布局期间可调整大小和不可调整大小的节点的行为。它向一个HBox添加一个按钮和一个矩形。运行程序后,缩短载物台的宽度。当按钮显示省略号(…)时,它会变得更小。矩形始终保持相同的大小。图 6-20 显示了调整尺寸过程中三个不同点的载物台。

img/336502_2_En_6_Fig20_HTML.jpg

图 6-20

调整舞台大小后,以全尺寸显示的按钮和矩形

// ResizableNodeTest.java
package com.jdojo.node;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;

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

        @Override
        public void start(Stage stage) {
                Button btn = new Button("A big button");
                Rectangle rect = new Rectangle(100, 50);
                rect.setFill(Color.WHITE);
                rect.setStrokeWidth(1);
                rect.setStroke(Color.BLACK);

                HBox root = new HBox();
                root.setSpacing(20);
                root.getChildren().addAll(btn, rect);

                Scene scene = new Scene(root);
                stage.setScene(scene);
                stage.setTitle("Resizable Nodes");
                stage.show();

                System.out.println("btn.isResizable(): " +
                         btn.isResizable());
                System.out.println("rect.isResizable(): " +
                         rect.isResizable());
        }
}
btn.isResizable(): true
rect.isResizable(): false

Listing 6-4A Button and a Rectangle in an HBox

可调整大小的节点

可调整大小的节点的实际大小由两个因素决定:

  • 放置节点的容器的大小调整策略

  • 由节点本身指定的大小范围

每个容器都有一个针对其子容器的调整大小策略。我将在第十章讨论容器的尺寸调整策略。一个可调整大小的节点可以指定其大小的范围(宽度和高度),这应该被一个用于布局节点的荣誉容器所考虑。可调整大小的节点指定构成其大小范围的三种类型的大小:

  • 首选尺寸

  • 最小尺寸

  • 最大尺寸

节点的首选大小是显示其内容的理想宽度和高度。例如,根据图像、文本、字体和文本换行等当前属性,一个按钮的首选大小足以显示其所有内容。节点的最小尺寸是它想要的最小宽度和高度。例如,最小尺寸的按钮足以显示图像和文本的省略号。节点的最大尺寸是它想要的最大宽度和高度。在按钮的情况下,按钮的最大大小与其首选大小相同。有时,您可能希望将节点扩展到无限大小。在这些情况下,最大宽度和高度被设置为Double.MAX_VALUE

大多数可调整大小的节点根据其内容和属性设置自动计算其首选、最小和最大大小。这些尺寸被称为它们的内在尺寸RegionControl类定义了两个常量,作为节点固有大小的标记值。这些常量是

  • USE_COMPUTED_SIZE

  • USE_PREF_SIZE

两个常量都是double类型。USE_COMPUTED_SIZEUSE_PREF_SIZE的值分别为–1 和Double.NEGATIVE_INFINITY。没有记载为什么相同的常量被定义了两次。也许设计者不想将它们在类层次结构中上移,因为它们不适用于所有类型的节点。

如果节点的大小设置为 sentinel 值USE_COMPUTED_SIZE,节点将根据其内容和属性设置自动计算该大小。USE_PREF_SIZE标记值用于设置最小和最大尺寸,如果它们与首选尺寸相同的话。

RegionControl类有六个DoubleProperty类型的属性来定义它们的宽度和高度的首选值、最小值和最大值:

  • prefWidth

  • prefHeight

  • minWidth

  • minHeight

  • maxWidth

  • maxHeight

默认情况下,这些属性被设置为标记值USE_COMPUTED_SIZE。这意味着节点会自动计算这些大小。您可以设置这些属性之一来覆盖节点的固有大小。例如,您可以将按钮的首选、最小和最大宽度设置为 50 像素,如下所示:

Button btn = new Button("Close");
btn.setPrefWidth(50);
btn.setMinWidth(50);
btn.setMaxWidth(50);

前面的代码片段将按钮的首选宽度、最小宽度和最大宽度设置为相同的值,使按钮在水*方向不可调整大小。

以下代码片段将按钮的最小和最大宽度设置为首选宽度,其中首选宽度本身是内部计算的:

Button btn = new Button("Close");
btn.setMinWidth(Control.USE_PREF_SIZE);
btn.setMaxWidth(Control.USE_PREF_SIZE);

Tip

在大多数情况下,节点的首选、最小和最大大小的内部计算值是合适的。仅当内部计算的大小不满足应用程序的需要时,才使用这些属性来重写内部计算的大小。如果您需要将一个节点的大小绑定到一个表达式,您将需要绑定prefWidthprefHeight属性。

如何获得节点的实际首选、最小和最大大小?您可能会猜测您可以使用getPrefWidth()getPrefHeight()getMinWidth()getMinHeight()getMaxWidth()getMaxHeight()方法来获得它们。但是您不应该使用这些方法来获取节点的实际大小。这些大小可以设置为 sentinel 值,节点将在内部计算实际大小。这些方法返回标记值或覆盖值。清单 6-5 创建了两个按钮,并将其中一个按钮的首选固有宽度覆盖为 100 像素。产生的屏幕如图 6-21 所示。以下输出证明,这些方法对于了解用于布局目的的节点的实际大小不是很有用。

img/336502_2_En_6_Fig21_HTML.jpg

图 6-21

按钮使用 sentinel 并覆盖其宽度值

// NodeSizeSentinelValues.java
package com.jdojo.node;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

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

        @Override
        public void start(Stage stage) {
                Button okBtn = new Button("OK");
                Button cancelBtn = new Button("Cancel");

                // Override the intrinsic width of the cancel button
                cancelBtn.setPrefWidth(100);

                VBox root = new VBox();
                root.getChildren().addAll(okBtn, cancelBtn);

                Scene scene = new Scene(root);
                stage.setScene(scene);
                stage.setTitle("Overriding Node Sizes");
                stage.show();

                System.out.println("okBtn.getPrefWidth(): " +
                         okBtn.getPrefWidth());
                System.out.println("okBtn.getMinWidth(): " +
                         okBtn.getMinWidth());
                System.out.println("okBtn.getMaxWidth(): " +
                         okBtn.getMaxWidth());

                System.out.println("cancelBtn.getPrefWidth(): " +
                         cancelBtn.getPrefWidth());
                System.out.println("cancelBtn.getMinWidth(): " +
                         cancelBtn.getMinWidth());
                System.out.println("cancelBtn.getMaxWidth(): " +
                         cancelBtn.getMaxWidth());
        }
}
okBtn.getPrefWidth(): -1.0
okBtn.getMinWidth(): -1.0
okBtn.getMaxWidth(): -1.0
cancelBtn.getPrefWidth(): 100.0
cancelBtn.getMinWidth(): -1.0
cancelBtn.getMaxWidth(): -1.0

Listing 6-5Using getXXXWidth() and getXXXHeight() Methods of Regions and Controls

要获得节点的实际大小,需要在Node类中使用以下方法。请注意,Node类没有定义任何与大小相关的属性。与尺寸相关的属性在RegionControl和其他类中定义。

  • double prefWidth(double height)

  • double prefHeight(double width)

  • double minWidth(double height)

  • double minHeight(double width)

  • double maxWidth(double height)

  • double maxHeight(double width)

在这里,您可以看到获取节点实际大小的另一个变化。您需要传递它的高度值来获得它的宽度,反之亦然。对于 JavaFX 中的大多数节点,宽度和高度是独立的。但是,对于某些节点,高度取决于宽度,反之亦然。当一个节点的宽度依赖于它的高度时,或者反之亦然,该节点被称为具有内容偏差。如果一个节点的高度取决于它的宽度,那么这个节点有一个水*内容偏差。如果一个节点的宽度取决于它的高度,那么这个节点有一个垂直内容偏差。请注意,一个节点不能同时具有水*和垂直内容偏好,这将导致循环依赖。

Node类的getContentBias()方法返回一个节点的内容偏差。它的返回类型是javafx.geometry.Orientation枚举类型,有两个常量:HORIZONTALVERTICAL。如果一个节点没有内容偏向,例如TextChoiceBox,该方法返回null

所有属于Labeled类的子类的控件,例如LabelButtonCheckBox,当它们启用了文本换行属性时,都有一个HORIZONTAL内容偏好。对于某些节点,它们的内容偏向取决于它们的方向。比如一个FlowPane的方位是HORIZONTAL,它的内容偏置是HORIZONTAL;如果它的方向是VERTICAL,那么它的内容偏差就是VERTICAL

您应该使用上面列出的六种方法来获得用于布局目的的节点的大小。如果一个节点类型没有内容偏向,您需要将–1 作为另一个维度的值传递给这些方法。例如,ChoiceBox没有内容偏好,您将获得其首选大小,如下所示:

ChoiceBox choices = new ChoiceBox();
...
double prefWidth = choices.prefWidth(-1);
double prefHeight = choices.prefHeight(-1);

对于那些有内容偏向的节点,您需要传递偏向的维度来获得另一个维度。例如,对于一个按钮,它有一个HORIZONTAL内容偏差,您可以传递–1 来获得它的宽度,并且可以传递它的宽度值来获得它的高度,如下所示:

Button b = new Button("Hello JavaFX");

// Enable text wrapping for the button, which will change its
// content bias from null (default) to HORIZONTAL
b.setWrapText(true);
...
double prefWidth = b.prefWidth(-1);
double prefHeight = b.prefHeight(prefWidth);

如果按钮没有启用文本换行属性,您可以将–1 传递给方法prefWidth()prefHeight(),因为它没有内容偏向。

获取用于布局目的的节点的宽度和高度的一般方法概述如下。该代码显示了如何获取首选的宽度和高度,该代码类似于获取节点的最小和最大宽度和高度:

Node node = get the reference of the node;
...
double prefWidth = -1;
double prefHeight = -1;

Orientation contentBias = b.getContentBias();

if (contentBias == HORIZONTAL) {
        prefWidth = node.prefWidth(-1);
        prefHeight = node.prefHeight(prefWidth);
} else if (contentBias == VERTICAL) {
        prefHeight = node.prefHeight(-1);
        prefWidth = node.prefWidth(prefHeight);
} else {
        // contentBias is null
        prefWidth = node.prefWidth(-1);
        prefHeight = node.prefHeight(-1);
}

现在您知道了如何获得节点的首选、最小和最大大小的指定值和实际值。这些值表示节点大小的范围。当一个节点被放置在一个容器中时,容器会尝试给这个节点一个自己喜欢的大小。但是,根据容器的策略和指定的节点大小,节点可能无法获得其首选大小。相反,一个荣誉容器会给一个节点一个在其指定范围内的大小。这被称为电流大小。如何获得一个节点的当前大小?RegionControl类定义了两个只读属性widthi ght,它们保存了节点的当前宽度和高度值。

现在让我们看看所有这些方法的实际应用。清单 6-6 将一个按钮放在一个HBox中,为按钮打印不同类型的尺寸,更改一些属性,并再次打印按钮的尺寸。以下输出显示,随着按钮的首选宽度变小,其首选高度变大。

// NodeSizes.java
package com.jdojo.node;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;

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

        @Override
        public void start(Stage stage) {
                Button btn = new Button("Hello JavaFX!");

                HBox root = new HBox();
                root.getChildren().addAll(btn);

                Scene scene = new Scene(root);
                stage.setScene(scene);
                stage.setTitle("Sizes of a Node");
                stage.show();

                // Print button's sizes
                System.out.println("Before changing button properties:");
                printSizes(btn);

                // Change button's properties
                btn.setWrapText(true);
                btn.setPrefWidth(80);
                stage.sizeToScene();

                // Print button's sizes
                System.out.println(
                         "\nAfter changing button properties:");
                printSizes(btn);

        }

        public void printSizes(Button btn) {
                System.out.println("btn.getContentBias() = " +
                         btn.getContentBias());

                System.out.println("btn.getPrefWidth() = " +
                         btn.getPrefWidth() +
                   ", btn.getPrefHeight() = " + btn.getPrefHeight());

                System.out.println("btn.getMinWidth() = " +
                         btn.getMinWidth() +
                    ", btn.getMinHeight() = " + btn.getMinHeight());

                System.out.println("btn.getMaxWidth() = " +
                         btn.getMaxWidth() +
                   ", btn.getMaxHeight() = " + btn.getMaxHeight());

                double prefWidth = btn.prefWidth(-1);
                System.out.println("btn.prefWidth(-1) = " + prefWidth +
                   ", btn.prefHeight(prefWidth) = " +
                         btn.prefHeight(prefWidth));

                double minWidth = btn.minWidth(-1);
                System.out.println("btn.minWidth(-1) = " + minWidth +
                   ", btn.minHeight(minWidth) = " +
                         btn.minHeight(minWidth));

                double maxWidth = btn.maxWidth(-1);
                System.out.println("btn.maxWidth(-1) = " + maxWidth +
                       ", btn.maxHeight(maxWidth) = " +
                               btn.maxHeight(maxWidth));

                System.out.println("btn.getWidth() = " + btn.getWidth() +
                    ", btn.getHeight() = " + btn.getHeight());
        }
}
Before changing button properties:
btn.getContentBias() = null
btn.getPrefWidth() = -1.0, btn.getPrefHeight() = -1.0
btn.getMinWidth() = -1.0, btn.getMinHeight() = -1.0
btn.getMaxWidth() = -1.0, btn.getMaxHeight() = -1.0
btn.prefWidth(-1) = 107.0, btn.prefHeight(prefWidth) = 22.8984375
btn.minWidth(-1) = 37.0, btn.minHeight(minWidth) = 22.8984375
btn.maxWidth(-1) = 107.0, btn.maxHeight(maxWidth) = 22.8984375
btn.getWidth() = 107.0, btn.getHeight() = 23.0

After changing button properties:
btn.getContentBias() = HORIZONTAL
btn.getPrefWidth() = 80.0, btn.getPrefHeight() = -1.0
btn.getMinWidth() = -1.0, btn.getMinHeight() = -1.0
btn.getMaxWidth() = -1.0, btn.getMaxHeight() = -1.0
btn.prefWidth(-1) = 80.0, btn.prefHeight(prefWidth) = 39.796875
btn.minWidth(-1) = 37.0, btn.minHeight(minWidth) = 22.8984375
btn.maxWidth(-1) = 80.0, btn.maxHeight(maxWidth) = 39.796875
btn.getWidth() = 80.0, btn.getHeight() = 40.0

Listing 6-6Using Different Size-Related Methods of a Node

获取或设置可调整大小的节点的方法还不止这些。有一些方便的方法可以用来执行与本节中讨论的方法相同的任务。表 6-2 列出了与尺寸相关的方法及其定义类别和用法。

表 6-2

可调整大小的节点的大小相关方法

|

方法/属性

|

定义类别

|

使用

属性:prefWidth``prefHeight``minWidth``minHeight``maxWidth``maxHeight RegionControl 它们定义了首选、最小和最大尺寸。默认情况下,它们被设置为标记值。使用它们来覆盖默认值。
方法:double prefWidth(double h)``double prefHeight(double w)``double minWidth(double h)``double minHeight(double w)``double maxWidth(double h)``double maxHeight(double w) Node 使用它们来获得节点的实际大小。如果节点没有内容偏向,则传递–1 作为参数。如果节点有内容偏差,则将另一维的实际值作为参数传递。请注意,这些方法没有对应的属性。
属性:width``height RegionControl 这些是只读的属性,保存可调整大小的节点的当前宽度和高度。
方法:void setPrefSize(double w, double h)``void setMinSize(double w, double h)``void setMaxSize(double w, double h) RegionControl 这些是覆盖节点的默认计算宽度和高度的方便方法。
方法:void resize(double w, double h) Node 它将节点调整到指定的宽度和高度。它由节点的父节点在布局期间调用。您不应该在代码中直接调用此方法。如果您需要设置节点的大小,请使用setMinSize()setPrefSize()setMaxSize()方法。此方法对不可调整大小的节点无效。
方法:void autosize() Node 对于可调整大小的节点,它将布局边界设置为其当前首选的宽度和高度。它会处理内容偏差。此方法对不可调整大小的节点无效。

不可调整的节点

在布局过程中,不可调整大小的节点不会被其父节点调整大小。但是,您可以通过更改它们的属性来更改它们的大小。不可调整大小的节点(例如,所有形状)具有决定其大小的不同属性。例如,矩形的宽度和高度、圆的半径以及直线的(startXstartYendXendY)决定了它们的大小。

Node类中定义了几个与大小相关的方法。当在不可调整大小的节点上调用这些方法或它们返回当前大小时,这些方法不起作用。例如,在不可调整大小的节点上调用Node类的resize(double w, double h)方法没有任何效果。对于不可调整大小的节点,Node类中的prefWidth(double h)minWidth(double h)maxWidth(double h)方法返回其layoutBounds宽度;而prefHeight(double w)minHeight(double w)maxHeight(double w)方法返回其layoutBounds高度。不可调整大小的节点没有内容偏见。将–1 作为其他维度的参数传递给所有这些方法。

在节点中存储用户数据

每个节点都维护一个用户定义属性(键/值对)的可见映射。你可以用它来储存任何有用的信息。假设您有一个TextField让用户操作一个人的名字。您可以将最初从数据库中检索到的人名存储为TextField的属性。您可以稍后使用该属性来重置名称,或者生成一个UPDATE语句来更新数据库中的名称。属性的另一个用途是存储微帮助文本。当节点获得焦点时,您可以读取它的 micro help 属性并显示它,例如,在状态栏中,以帮助用户理解节点的用法。

Node类的getProperties()方法返回一个ObservableMap<Object, Object>,您可以在其中添加或删除节点的属性。以下代码片段将带有值"Advik"的属性"originalData"添加到TextField节点:

TextField nameField = new TextField();
...
ObservableMap<Object, Object> props = nameField.getProperties();
props.put("originalData", "Advik");

以下代码片段从nameField节点读取"originalData"属性的值:

ObservableMap<Object, Object> props = nameField.getProperties();
if (props.containsKey("originalData")) {
        String originalData = (String)props.get("originalData");
} else {
        // originalData property is not set yet
}

Node类有两个方便的方法,setUserData(Object value)getUserData(),用来存储用户定义的值作为节点的属性。在setUserData()方法中指定的value使用相同的ObservableMap来存储getProperties()方法返回的数据。Node类使用内部的Object作为键来存储值。您需要使用getUserData()方法来获取使用setUserData()方法存储的值,如下所示:

nameField.setUserData("Saved"); // Set the user data
...
String userData = (String)nameField.getUserData(); // Get the user data

Tip

除非使用getUserData()方法,否则不能直接访问节点的用户数据。因为它存储在由getProperties()方法返回的同一个ObservableMap中,所以您可以通过迭代该映射中的值来间接访问它。

Node类有一个hasProperties()方法。您可以使用它来查看是否为该节点定义了任何属性。

什么是受管节点?

Node类有一个托管属性,它的类型是BooleanProperty。默认情况下,所有节点都被管理。受管节点的布局由其父节点管理。一个Parent节点在计算自己的大小时会考虑到它所有被管理的子节点的layoutBounds。一个Parent节点负责调整其托管的可调整大小的子节点的大小,并根据其布局策略定位它们。当被管理子节点的layoutBounds发生变化时,场景图的相关部分被重新显示。

如果一个节点是非托管的,应用程序单独负责布局(计算它的大小和位置)。也就是说,Parent节点不布局它的非托管子节点。非托管节点的layoutBounds中的变化不会触发其上的重新布局。非托管的Parent节点充当布局根。如果一个子节点调用了Parent.requestLayout()方法,那么只有以非托管Parent节点为根的分支才会被重发。

Tip

对比Node类的visible属性和它的managed属性。出于布局目的,Parent节点会考虑其所有不可见子节点的layoutBounds,并忽略非托管子节点。

什么时候使用非托管节点?通常,您不需要在应用程序中使用非托管节点,因为它们需要您做额外的工作。然而,只要知道它们的存在,如果需要的话,你就可以使用它们。

当您想在容器中显示一个节点而不考虑它的layoutBounds时,您可以使用一个非托管节点。您需要自己调整节点的大小和位置。清单 6-7 演示了如何使用非托管节点。当一个节点获得焦点时,它使用一个非托管的Text节点来显示一个微帮助。该节点需要有一个名为"microHelpText"的属性。当显示微帮助时,整个应用程序的布局不会被打乱,因为显示微帮助的Text节点是一个非托管节点。您在focusChanged()方法中将节点放置在适当的位置。该程序向场景的focusOwner属性注册了一个更改监听器,因此当场景内的焦点发生变化时,您可以显示或隐藏微帮助Text节点。当两个不同的节点具有焦点时,产生的屏幕如图 6-22 所示。注意,在这个例子中,定位Text节点很容易,因为所有节点都在同一个父节点GridPane中。如果节点放在不同的父节点中,定位Text节点的逻辑变得复杂。

img/336502_2_En_6_Fig22_HTML.jpg

图 6-22

使用非托管的Text节点显示微帮助

// MicroHelpApp.java
package com.jdojo.node;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.value.ObservableValue;
import javafx.geometry.VPos;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.stage.Stage;

public class MicroHelpApp extends Application {
        // An instance variable to store the Text node reference
        private Text helpText = new Text();

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

        @Override
        public void start(Stage stage) {
                TextField fName = new TextField();
                TextField lName = new TextField();
                TextField salary = new TextField();

                Button closeBtn = new Button("Close");
                closeBtn.setOnAction(e -> Platform.exit());

                fName.getProperties().put("microHelpText",
                         "Enter the first name");
                lName.getProperties().put("microHelpText",
                         "Enter the last name");
                salary.getProperties().put("microHelpText",
                   "Enter a salary greater than $2000.00.");

                // The help text node is unmanaged
                helpText.setManaged(false);
                helpText.setTextOrigin(VPos.TOP);
                helpText.setFill(Color.RED);
                helpText.setFont(Font.font(null, 9));
                helpText.setMouseTransparent(true);

                // Add all nodes to a GridPane
                GridPane root = new GridPane();

                root.add(new Label("First Name:"), 1, 1);
                root.add(fName, 2, 1);
                root.add(new Label("Last Name:"), 1, 2);
                root.add(lName, 2, 2);

                root.add(new Label("Salary:"), 1, 3);
                root.add(salary, 2, 3);
                root.add(closeBtn, 3, 3);
                root.add(helpText, 4, 3);

                Scene scene = new Scene(root, 300, 100);

                // Add a change listener to the scene, so you know when
                     // the focus owner changes and display the micro help
                scene.focusOwnerProperty().addListener(
                     (ObservableValue<? extends Node> value,
                            Node oldNode, Node newNode)
                          -> focusChanged(value, oldNode, newNode));
                stage.setScene(scene);
                stage.setTitle("Showing Micro Help");
                stage.show();
        }

        public void focusChanged(ObservableValue<? extends Node> value,
                    Node oldNode, Node newNode) {
                // Focus has changed to a new node
                String microHelpText =
                         (String)newNode.getProperties().get("microHelpText");

                if (microHelpText != null &&
                                    microHelpText.trim().length() > 0)  {
                        helpText.setText(microHelpText);
                        helpText.setVisible(true);

                        // Position the help text node
                        double x = newNode.getLayoutX() +
                            newNode.getLayoutBounds().getMinX() –
                            helpText.getLayoutBounds().getMinX();
                        double y = newNode.getLayoutY() +
                            newNode.getLayoutBounds().getMinY() +
                            newNode.getLayoutBounds().getHeight() -
                            helpText.getLayoutBounds().getMinX();

                        helpText.setLayoutX(x);
                        helpText.setLayoutY(y);
                        helpText.setWrappingWidth(
                                    newNode.getLayoutBounds().getWidth());
                }
                else {
                        helpText.setVisible(false);
                }
        }
}

Listing 6-7Using an Unmanaged Text Node to Show Micro Help

有时,如果某个节点变得不可见,您可能希望使用该节点所使用的空间。假设你有一个有几个按钮的HBox。当其中一个按钮变得不可见时,您希望从右向左滑动所有按钮。可以在VBox中实现上滑效果。通过将节点的managed属性绑定到visible属性,很容易在HBoxVBox(或任何其他具有相对定位的容器)中实现滑动效果。清单 6-8 展示了如何在HBox中实现向左滑动的特性。它显示四个按钮。第一个按钮用于使第三个按钮b2可见和不可见。b2按钮的托管属性绑定到它的 visible 属性:

b2.managedProperty().bind(b2.visibleProperty());

b2按钮变得不可见时,它就变得不受管理,并且HBox在计算它自己的layoutBounds时不使用它的layoutBounds。这使得b3按钮向左滑动。图 6-23 显示了应用程序运行时的两个屏幕截图。

img/336502_2_En_6_Fig23_HTML.jpg

图 6-23

模拟 B2 按钮的向左滑动功能

// SlidingLeftNodeTest.java
package com.jdojo.node;

import javafx.application.Application;
import javafx.beans.binding.When;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;

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

        @Override
        public void start(Stage stage) {
                Button b1 = new Button("B1");
                Button b2 = new Button("B2");
                Button b3 = new Button("B3");
                Button visibleBtn = new Button("Make Invisible");

                // Add an action listener to the button to make
                // b2 visible if it is invisible and invisible if it
                // is visible
                visibleBtn.setOnAction(e ->
                         b2.setVisible(!b2.isVisible()));

                // Bind the text property of the button to the visible
                // property of the b2 button
                visibleBtn.textProperty().bind(
                         new When(b2.visibleProperty())
                .then("Make Invisible")
                .otherwise("Make Visible"));

                // Bind the managed property of b2 to its visible
                // property
                b2.managedProperty().bind(b2.visibleProperty());

                HBox root = new HBox();
                root.getChildren().addAll(visibleBtn, b1, b2, b3);

                Scene scene = new Scene(root);
                stage.setScene(scene);
                stage.setTitle("Sliding to the Left");
                stage.show();
        }
}

Listing 6-8Simulating the Slide-Left Feature Using Unmanaged Nodes

变换坐标空间之间的界限

我已经介绍了节点使用的坐标空间。有时,您可能需要将一个Bounds或一个点从一个坐标空间转换到另一个坐标空间。Node类包含几个方法来支持这一点。支持以下Bounds或点的变换:

  • 本地到父

  • 本地到场景

  • 父到本地

  • 场景到本地

localToParent()方法将节点的本地坐标空间中的一个Bounds或一个点转换到其父节点的坐标空间。localToScene()方法将节点的局部坐标空间中的一个Bounds或一个点转换到其场景的坐标空间。parentToLocal()方法将节点的父节点的坐标空间中的一个Bounds或一个点转换到该节点的局部坐标空间。sceneToLocal()方法将节点的场景坐标空间中的Bounds或点转换到该节点的局部坐标空间。所有方法都有三个重载版本;一个版本将一个Bounds作为参数,并返回转换后的Bounds;另一个版本将一个Point2D作为参数,并返回转换后的Point2D;另一个版本获取一个点的 x 和 y 坐标,并返回转换后的Point2D

这些方法足以将一个坐标空间中的点的坐标变换到场景图形中的另一个坐标空间。有时,您可能需要将节点的局部坐标空间中的点的坐标转换到舞台或屏幕的坐标空间。您可以使用SceneStage类的xy属性来实现这一点。场景的(x,y)属性定义了场景左上角在其舞台坐标空间中的坐标。舞台的(x,y)属性定义了屏幕坐标空间中舞台左上角的坐标。例如,如果(x1,y1)是场景坐标空间中的一个点,(x1 + x2,y1 + y2)定义了舞台坐标空间中的同一点,其中 x2 和 y2 分别是舞台的xy属性。应用相同的逻辑来获得屏幕坐标空间中的点的坐标。

让我们看一个使用节点、其父节点及其场景的坐标空间之间的变换的例子。一个场景有三个Label和三个TextField放置在不同的父对象下。一个红色的小圆圈被放置在具有焦点的节点的边界框的左上角。随着焦点的改变,需要计算圆的位置,该位置与当前节点左上角相对于圆的父节点的位置相同。圆心需要与具有焦点的节点的左上角重合。图 6-24 显示焦点在名和姓节点的阶段。清单 6-9 有完整的程序来实现这一点。

img/336502_2_En_6_Fig24_HTML.jpg

图 6-24

使用坐标空间变换将圆移动到焦点节点

该节目有一个由三个LabelTextField组成的场景,一对Label和一对TextField被放置在一个HBox中。所有的HBox都放在一个VBox中。一个非托管的Circle被放置在VBox中。该程序向场景的focusOwner属性添加了一个变化监听器来跟踪焦点变化。当焦点改变时,圆被放置在具有焦点的节点的左上角。

placeMarker()包含主逻辑。它获取局部坐标空间中焦点节点边界框左上角的(x,y)坐标:

double nodeMinX = newNode.getLayoutBounds().getMinX();
double nodeMinY = newNode.getLayoutBounds().getMinY();

它将节点左上角的坐标从局部坐标空间转换到场景的坐标空间:

Point2D nodeInScene = newNode.localToScene(nodeMinX, nodeMinY);

现在节点左上角的坐标从场景的坐标空间转换到圆的坐标空间,程序中命名为marker:

Point2D nodeInMarkerLocal = marker.sceneToLocal(nodeInScene);

最后,节点左上角的坐标被转换到圆的父节点的坐标空间:

Point2D nodeInMarkerParent = marker.localToParent(nodeInMarkerLocal);

此时,nodeInMarkerParent是相对于圆的父点的点(焦点节点的左上角)。如果将圆重新定位到此点,则将圆边界框的左上角放置到焦点节点的左上角:

marker.relocate(nodeInMarkerParent.getX(), nodeInMarkerParent.getY())

如果要将圆心放在焦点节点的左上角,则需要相应地调整坐标:

// CoordinateConversion.java
package com.jdojo.node;

import javafx.application.Application;
import javafx.geometry.Point2D;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;

public class CoordinateConversion extends Application {
        // An instance variable to store the reference of the circle
        private Circle marker;

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

        @Override
        public void start(Stage stage) {
                TextField fName = new TextField();
                TextField lName = new TextField();
                TextField salary = new TextField();

                // The Circle node is unmanaged
                marker = new Circle(5);
                marker.setManaged(false);
                marker.setFill(Color.RED);
                marker.setMouseTransparent(true);

                HBox hb1 = new HBox();
                HBox hb2 = new HBox();
                HBox hb3 = new HBox();
                hb1.getChildren().addAll(
                         new Label("First Name:"), fName);
                hb2.getChildren().addAll(new Label("Last Name:"), lName);
                hb3.getChildren().addAll(new Label("Salary:"), salary);

                VBox root = new VBox();
                root.getChildren().addAll(hb1, hb2, hb3, marker);

                Scene scene = new Scene(root);

                // Add a focus change listener to the scene
                scene.focusOwnerProperty().addListener(
                    (prop, oldNode, newNode) -> placeMarker(newNode));

                stage.setScene(scene);
                stage.setTitle("Coordinate Space Transformation");
                stage.show();
        }

        public void placeMarker(Node newNode) {
                double nodeMinX = newNode.getLayoutBounds().getMinX();
                double nodeMinY = newNode.getLayoutBounds().getMinY();
                Point2D nodeInScene =
                         newNode.localToScene(nodeMinX, nodeMinY);
                Point2D nodeInMarkerLocal =
                         marker.sceneToLocal(nodeInScene);
                Point2D nodeInMarkerParent =
                         marker.localToParent(nodeInMarkerLocal);

                // Position the circle approperiately
                marker.relocate(
                         nodeInMarkerParent.getX()
                             + marker.getLayoutBounds().getMinX(),
                   nodeInMarkerParent.getY()e
                             + marker.getLayoutBounds().getMinY());
        }
}

Listing 6-9Transforming the Coordinates of a Point from One Coordinate Space to Another

marker.relocate(
    nodeInMarkerParent.getX() + marker.getLayoutBounds().getMinX(),
   nodeInMarkerParent.getY() + marker.getLayoutBounds().getMinY());

摘要

场景图是一种树形数据结构。场景图中的每个项目称为一个节点。javafx.scene.Node类的一个实例表示场景图中的一个节点。一个节点可以有子项(也称为子节点),这样的节点称为分支节点。分支节点是Parent类的一个实例,它的具体子类是GroupRegionWebView。不能有子项的节点称为叶节点。诸如RectangleTextImageViewMediaView之类的实例是叶节点的例子。每个场景图树中只有一个节点没有父节点,它被称为根节点。一个节点在场景图中的任何地方最多出现一次。

如果节点尚未附加到场景,则可以在任何线程上创建和修改节点。将节点附加到场景中以及随后的修改必须发生在 JavaFX 应用程序线程上。一个节点有几种类型的边界。界限是相对于不同的坐标系确定的。场景图中的节点有三种类型的边界:layoutBoundsboundsInLocalboundsInParent

layoutBounds属性是基于节点在未变换的局部坐标空间中的几何属性来计算的。不包括效果、剪辑和变换。boundsInLocal属性是在节点的未变换坐标空间中计算的。它包括节点、效果和剪辑的几何属性。不包括应用于节点的变换。节点的boundsInParent属性位于其父节点的坐标空间中。它包括节点、效果、剪辑和变换的几何属性。它很少直接用在代码中。

一个GrouplayoutBoundsboundsInLocalboundsInParent的计算不同于一个节点的计算。一个Group承担其子节点的集合边界。您可以对每个Group子对象分别应用效果、剪辑和变换。您还可以直接在Group上应用效果、剪辑和变换,它们会应用到它的所有子节点。一个GrouplayoutBounds是其所有子节点boundsInParent的并集。它包括直接应用到子对象的效果、剪辑和变换。它不包括直接应用于Group的效果、剪辑和变换。GroupboundsInLocal通过取其layoutBounds并包括直接应用于Group的效果和剪辑来计算。GroupboundsInParent通过取其boundsInLocal并包括直接应用于Group的变换来计算。

每个节点都维护一个用户定义属性(键/值对)的可见映射。你可以用它来储存任何有用的信息。节点可以是托管的,也可以是非托管的。托管节点由其父节点布局,而应用程序负责布局非托管节点。

下一章将讨论如何在 JavaFX 中使用颜色。

七、玩转颜色

在本章中,您将学习:

  • JavaFX 中如何表示颜色

  • 有哪些不同的颜色图案

  • 如何使用图像模式

  • 如何使用线性颜色渐变

  • 如何使用径向颜色渐变

本章的例子在com.jdojo.color包中。为了让它们工作,您必须在module-info.java文件中添加相应的一行:

...
opens com.jdojo.color to javafx.graphics, javafx.base;
...

这是我们第一次使用resources文件夹中的文件。为了简化对资源文件的访问,我们在包com.jdojo.util中引入了一个实用程序类:

 package com.jdojo.util;

import java.io.File;
import java.io.IOException;
import java.net.URL;

public class ResourceUtil {
      // Where the resources directory is, seen from current working
      // directory. This differs from build tool to build tool, and
      // from IDE to IDE, so you might have to adapt this.
      private final static String RSRC_PATH_FROM_CURRENT_DIR = "bin";

    public static URL getResourceURL(String inResourcesPath) {
        var fStr = (RSRC_PATH_FROM_CURRENT_DIR +
             "/resources/" +
             inResourcesPath).replace("/", File.separator);
        try {
             return new File(fStr).getCanonicalFile().toURI().toURL();
        } catch (IOException e) {
             System.err.println("Cannot fetch URL for '" +
                 inResourcesPath + "'");
             System.err.println("""
                 If the path is correct, try to adapt the
                 RSRC_PATH_FROM_CURRENT_DIR constant in class
                 ResourceUtil""".stripIndent());
             e.printStackTrace(System.err);
             return null;
        }
    }

    public static String getResourceURLStr(String inResourcesPath) {
      return getResourceURL(inResourcesPath).toString();
    }

    public static String getResourcePath(String inResourcesPath) {

        var fStr = (RSRC_PATH_FROM_CURRENT_DIR +
             "/resources/" +
             inResourcesPath).replace("/", File.separator);
      return new File(fStr).getAbsolutePath();
    }
}

理解颜色

在 JavaFX 中,可以为文本指定颜色,为区域指定背景颜色。您可以将颜色指定为统一颜色、图像图案或颜色渐变。统一颜色使用相同的颜色填充整个区域。图像图案允许您用图像图案填充区域。颜色渐变定义了一种颜色模式,其中颜色沿着一条直线从一种颜色变化到另一种颜色。颜色梯度的变化可以是线性的或放射状的。在这一章中,我将给出使用所有颜色类型的例子。图 7-1 显示了 JavaFX 中颜色相关类的类图。所有的类都包含在javafx.scene.paint包中。

img/336502_2_En_7_Fig1_HTML.jpg

图 7-1

JavaFX 中颜色相关类的类图

Paint类是一个抽象类,它是其他颜色类的基类。它只包含一个静态方法,该方法接受一个String参数并返回一个Paint实例。返回的Paint实例属于ColorLinearGradientRadialGradient类,如以下代码所示:

public static Paint valueOf(String value)

你不会直接使用Paint类的valueOf()方法。它用于转换从 CSS 文件的String中读取的颜色值。下面的代码片段从String创建了Paint类的实例:

// redColor is an instance of the Color class
Paint redColor = Paint.valueOf("red");

// aLinearGradientColor is an instance of the LinearGradient class
Paint aLinearGradientColor = Paint.valueOf("linear-gradient(to bottom right, red, black)" );

// aRadialGradientColor is an instance of the RadialGradient class
Paint aRadialGradientColor =     Paint.valueOf("radial-gradient(radius 100%, red, blue, black)");

均匀颜色、图像图案、线性颜色渐变和径向颜色渐变分别是ColorImagePatternLinearGradientRadialGradient类的实例。在处理颜色渐变时使用了Stop类和CycleMethod枚举。

Tip

通常,设置节点颜色属性的方法将Paint类型作为参数,允许您使用四种颜色模式中的任何一种。

使用颜色类

Color类表示 RGB 颜色空间中的纯色统一颜色。每种颜色都有一个定义在 0.0 到 1.0 或 0 到 255 之间的 alpha 值。alpha 值为 0.0 或 0 表示颜色完全透明,alpha 值为 1.0 或 255 表示颜色完全不透明。默认情况下,alpha 值设定为 1.0。有三种方式可以拥有一个Color类的实例:

  • 使用构造器

  • 使用工厂方法之一

  • 使用在Color类中声明的颜色常量之一

Color类只有一个构造器,让你在范围[0.0;1.0]:

public Color(double red, double green, double blue, double opacity)

以下代码片段创建了完全不透明的蓝色:

Color blue = new  Color(0.0, 0.0, 1.0, 1.0);

您可以在Color类中使用以下静态方法来创建Color对象。双精度值需要介于 0.0 和 1.0 之间,而int值需要介于 0 和 255 之间;

  • Color color(double red, double green, double blue)

  • Color color(double red, double green, double blue, double opacity)

  • Color hsb(double hue, double saturation, double brightness)

  • Color hsb(double hue, double saturation, double brightness, double opacity)

  • Color rgb(int red, int green, int blue)

  • Color rgb(int red, int green, int blue, double opacity)

通过valueOf()web()工厂方法,您可以从 web 颜色值格式的字符串中创建Color对象。以下代码片段使用不同的字符串格式创建蓝色Color对象:

Color blue = Color.valueOf("blue");
Color blue = Color.web("blue");
Color blue = Color.web("#0000FF");
Color blue = Color.web("0X0000FF");
Color blue = Color.web("rgb(0, 0, 255)");
Color blue = Color.web("rgba(0, 0, 255, 0.5)"); // 50% transparent blue

Color类定义了大约 140 个颜色常量,例如REDWHITETANBLUE等等。由这些常量定义的颜色是完全不透明的。

使用 ImagePattern

图像图案允许您用图像填充形状。图像可以填充整个形状,也可以使用*铺模式。以下是获取图像模式的步骤:

  1. 使用文件中的图像创建一个Image对象。

  2. 相对于要填充的形状的左上角定义一个矩形,称为定位矩形。

图像显示在锚定矩形中,然后调整大小以适合锚定矩形。如果要填充的形状的边框比锚定矩形的边框大,则带有图像的锚定矩形会以*铺模式在形状内重复。

您可以使用ImagePattern的一个构造器创建它的一个对象:

  • ImagePattern(Image image)

  • ImagePattern(Image image, double x, double y, double width, double height, boolean proportional)

第一个构造器用不带任何图案的图像填充整个边界框。第二个构造器允许您指定定位矩形的 x 和 y 坐标、宽度和高度。如果proportional argument为真,则根据单位正方形,相对于要填充的形状的边界框指定锚定矩形。如果proportional参数为 false,则在形状的局部坐标系中指定定位矩形。以下对两个构造器的两次调用将产生相同的结果:

ImagePatterm ip1 = new ImagePattern(anImage);
ImagePatterm ip2 = new ImagePattern(anImage, 0.0, 0.0, 1.0, 1.0, true);

对于此处的示例,您将使用图 7-2 中所示的图像。它是一个 37px 25px 的蓝色圆角矩形。可以在源代码文件夹下的resources/picture/blue_rounded_rectangle.png文件中找到。

img/336502_2_En_7_Fig2_HTML.png

图 7-2

蓝色圆角矩形

使用该文件,让我们使用以下代码创建一个图像模式:

Image img = create the image object...
ImagePattern p1 = new ImagePattern(img, 0, 0, 0.25, 0.25, true);

ImagePattern构造器中的最后一个参数设置为true,使得锚定矩形的边界 0、0、0.25 和 0.25 被解释为与要填充的形状的大小成比例。图像模式将在要填充的形状的(0,0)处创建一个锚定矩形。它的宽度和高度将是要填充形状的 25%。这将使锚定矩形水*重复四次,垂直重复四次。如果将下面的代码与前面的图像模式一起使用,将会产生一个如图 7-3 所示的矩形:

img/336502_2_En_7_Fig3_HTML.jpg

图 7-3

用图像图案填充矩形

Rectangle r1 = new Rectangle(100, 50);
r1.setFill(p1);

如果您使用相同的图像模式用下面的代码片段填充一个三角形,得到的三角形将如图 7-4 所示:

img/336502_2_En_7_Fig4_HTML.png

图 7-4

用图像图案填充三角形

Polygon triangle = new Polygon(50, 0, 0, 50, 100, 50);
triangle.setFill(p1);

在没有拼贴图案的情况下,如何用图像完全填充形状?您需要使用一个参数设置为 true 的ImagePattern。锚点矩形的中心应该在(0,0)处,其宽度和高度应该设置为 1,如下所示:

// An image pattern to completely fill a shape with the image
ImagePatterm ip = new ImagePattern(yourImage, 0.0, 0.0, 1.0, 1.0, true);

清单 7-1 中的程序展示了如何使用图像模式。产生的屏幕如图 7-5 所示。它的init()方法将图像加载到一个Image对象中,并将其存储在一个实例变量中。如果在CLASSPATH中没有找到图像文件,它会打印一条错误信息并退出。

img/336502_2_En_7_Fig5_HTML.jpg

图 7-5

用图像图案填充不同的形状

// ImagePatternApp.java
package com.jdojo.color;

import com.jdojo.util.ResourceUtil;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.layout.HBox;
import javafx.scene.paint.ImagePattern;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;

public class ImagePatternApp extends Application {
      private Image img;

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

      @Override
      public void init() {
            // Create an Image object
            final String imgPath = ResourceUtil.getResourceURLStr(
                 "picture/blue_rounded_rectangle.png");
            img = new Image(imgPath);
      }

      @Override
      public void start(Stage stage) {
            // An anchor rectangle at (0, 0) that is 25% wide and 25% tall
            // relative to the rectangle to be filled
            ImagePattern p1 = new ImagePattern(img, 0, 0, 0.25, 0.25, true);
            Rectangle r1 = new Rectangle(100, 50);
            r1.setFill(p1);

            // An anchor rectangle at (0, 0) that is 50% wide and 50% tall
            // relative to the rectangle to be filled
            ImagePattern p2 = new ImagePattern(img, 0, 0, 0.5, 0.5, true);
            Rectangle r2 = new Rectangle(100, 50);
            r2.setFill(p2);

            // Using absolute bounds for the anchor rectangle
            ImagePattern p3 = new ImagePattern(img, 40, 15, 20, 20, false);
            Rectangle r3 = new Rectangle(100, 50);
            r3.setFill(p3);

            // Fill a circle

            ImagePattern p4 = new ImagePattern(img, 0, 0, 0.1, 0.1, true);
            Circle c = new Circle(50, 50, 25);
            c.setFill(p4);

            HBox root = new HBox();
            root.getChildren().addAll(r1, r2, r3, c);

            Scene scene = new Scene(root);
            stage.setScene(scene);

            stage.setTitle("Using Image Patterns");
            stage.show();
      }
}

Listing 7-1Using an Image Pattern to Fill Different Shapes

了解线性颜色渐变

使用称为渐变线的轴来定义线性颜色渐变。渐变线上的每个点都有不同的颜色。垂直于渐变线的直线上的所有点都具有相同的颜色,即两条线的交点的颜色。渐变线由起点和终点定义。沿渐变线的颜色是在渐变线上的一些点定义的,这些点被称为停止色点(或停止点)。使用插值法计算两个停止点之间的颜色。

渐变线有方向,是从起点到终点。垂直于渐变线并通过停止点的线上的所有点将具有停止点的颜色。例如,假设您用颜色 C1 定义了一个停止点 P1。如果你画一条垂直于穿过 P1 点的渐变线的线,该线上的所有点都将具有 C1 的颜色。

图 7-6 显示了构成线性颜色渐变的元素的细节。它显示了一个用线性颜色渐变填充的矩形区域。从左侧到右侧定义渐变线。起点为白色,终点为黑色。在矩形的左侧,所有点都是白色,在右侧,所有点都是黑色。在左侧和右侧之间,颜色在白色和黑色之间变化。

img/336502_2_En_7_Fig6_HTML.png

图 7-6

线性颜色渐变的细节

使用 LinearGradient

在 JavaFX 中,LinearGradient类的一个实例表示线性颜色渐变。该类有以下两个构造器。他们最后的争论类型是不同的:

  • LinearGradient(double startX, double startY, double endX, double endY, boolean proportional, CycleMethod cycleMethod, List<Stop> stops)

  • LinearGradient(double startX, double startY, double endX, double endY, boolean proportional, CycleMethod cycleMethod, Stop... stops)

startXstartY参数定义了渐变线起点的 x 和 y 坐标。endXendY参数定义了渐变线终点的 x 和 y 坐标。

proportional参数影响起点和终点坐标的处理方式。如果为真,则起点和终点相对于单位正方形处理。否则,它们将被视为局部坐标系中的绝对值。这个论点的用法需要多一点解释。

通常,颜色渐变用于填充区域,例如矩形。有时候,你知道区域的大小,有时候你不会。此参数的值允许您以相对或绝对形式指定渐变线。在相对形式中,该区域被视为一个单位正方形。也就是说,左上角和右下角的坐标分别是(0.0,0.0)和(1.0,1.0)。区域中的其他点的 x 和 y 坐标将在 0.0 和 1.0 之间。假设你指定起点为(0.0,0.0),终点为(1.0,0.0)。它定义了一条从左到右的水*渐变线。(0.0,0.0)和(0.0,1.0)的起点和终点定义了一条从上到下的垂直渐变线。(0.0,0.0)和(0.5,0.0)的起点和终点定义了从区域左侧到中间的水*渐变线。

proportional参数为假时,起点和终点的坐标值被视为相对于局部坐标系的绝对值。假设你有一个宽 200 高 100 的矩形。(0.0,0.0)和(200.0,0.0)的起点和终点定义了一条从左到右的水*渐变线。(0.0,0.0)和(200.0,100.0)的起点和终点定义了一条从左上角到右下角的倾斜渐变线。

cycleMethod参数定义了由起点和终点定义的颜色渐变边界之外的区域应该如何填充。假设您将比例参数设置为true的起点和终点分别定义为(0.0,0.0)和(0.5,0.0)。这只覆盖了该区域的左半部分。区域的右半部分应该如何填充?您可以使用cycleMethod参数来指定这种行为。其值是在CycleMethod枚举中定义的枚举常量之一:

  • CycleMethod.NO_CYCLE

  • CycleMethod.REFLECT

  • CycleMethod.REPEAT

NO_CYCLE的循环方法用终端颜色填充剩余区域。如果您已将颜色定义为仅从区域左侧到中间的停止点,则右半部分将用为区域中间定义的颜色填充。假设您只为区域的中间一半定义了颜色渐变,而左侧的 25%和右侧的 25%未定义。NO_CYCLE方法将使用距离左侧 25%处定义的颜色填充左侧 25%的区域,使用距离右侧 25%处定义的颜色填充右侧 25%的区域。中间 50%的颜色将由颜色停止点决定。

REFLECT的循环方法通过从最*的填充区域开始到结束和结束到开始反映颜色渐变来填充剩余的区域。REPEAT的循环方法重复颜色渐变填充剩余区域。

stops参数定义了沿渐变线的颜色停止点。一个颜色停止点由一个Stop类的实例表示,它只有一个构造器:

Stop(double offset, Color color)

偏移值介于 0.0 和 1.0 之间。它定义了从起点开始沿渐变线的停止点的相对距离。例如,偏移 0.0 是起点,偏移 1.0 是终点,偏移 0.5 在起点和终点的中间,依此类推。您可以用两种不同的颜色定义至少两个停止点,以获得颜色渐变。您可以为颜色渐变定义的停止点数量没有限制。

以上是对LinearGradient构造器参数的解释。所以让我们来看一些如何使用它们的例子。

以下代码片段用线性颜色渐变填充一个矩形,如图 7-7 所示:

img/336502_2_En_7_Fig7_HTML.jpg

图 7-7

具有两个停止点的水*线性颜色渐变:起点为白色,终点为黑色

Stop[] stops = new Stop[]{new Stop(0, Color.WHITE), new Stop(1, Color.BLACK)};
LinearGradient lg = new LinearGradient(0, 0, 1, 0, true, NO_CYCLE, stops);
Rectangle r = new Rectangle(200, 100);
r.setFill(lg);

你有两个颜色停止点。起点的停止点是白色的,终点的停止点是黑色的。起点(0,0)和终点(1,0)定义了从左到右的水*渐变。proportional参数被设置为true,这意味着坐标值被解释为相对于单位正方形。设置为NO_CYCLE的循环方法参数在这种情况下不起作用,因为渐变边界覆盖了整个区域。在前面的代码中,如果您想将proportional参数值设置为false,以达到相同的效果,您可以如下创建LinearGradient对象。请注意,使用 200 作为终点的 x 坐标来表示矩形宽度的终点:

LinearGradient lg = new LinearGradient(0, 0, 200, 0, false, NO_CYCLE, stops);

让我们看另一个例子。运行以下代码片段后得到的矩形如图 7-8 所示:

img/336502_2_En_7_Fig8_HTML.jpg

图 7-8

有两个停止点的水*线性颜色渐变:起点为白色,中点为黑色

Stop[] stops = new Stop[]{new Stop(0, Color.WHITE), new Stop(1, Color.BLACK)};
LinearGradient lg = new LinearGradient(0, 0, 0.5, 0, true, NO_CYCLE, stops);
Rectangle r = new Rectangle(200, 100);
r.setFill(lg);

在这段代码中,您做了一点小小的修改。您定义了一条水*渐变线,该线从矩形的左侧开始,在中间结束。注意使用(0.5,0)作为终点的坐标。这使得矩形的右半部分没有颜色渐变。在这种情况下,循环方法是有效的,因为它的工作是填充未填充的区域。矩形中间的颜色是黑色,由第二个停止点定义。NO_CYCLE值使用终端黑色填充矩形的右半部分。

让我们看一下前一个例子的一个微小的变体。您将循环方法从NO_CYCLE更改为REFLECT,如以下代码片段所示,这将生成如图 7-9 所示的矩形。请注意,右半部分区域(具有未定义梯度的区域)是左半部分的反射:

img/336502_2_En_7_Fig9_HTML.jpg

图 7-9

带有两个停止点的水*线性颜色渐变:起点为白色,中点为黑色,循环方法为REFLECT

Stop[] stops = new Stop[]{new Stop(0, Color.WHITE), new Stop(1, Color.BLACK)};
LinearGradient lg = new LinearGradient(0, 0, 0.5, 0, true, REFLECT, stops);
Rectangle r = new Rectangle(200, 100);
r.setFill(lg);

让我们对前面的例子做一点小小的改变,这样终点坐标只覆盖了矩形宽度的十分之一。代码如下,生成的矩形如图 7-10 所示。矩形右边的 90%使用REFLECT循环方法填充,交替使用首尾相连和首尾相连的颜色模式:

img/336502_2_En_7_Fig10_HTML.png

图 7-10

带有两个停止点的水*线性颜色渐变:起点为白色,十分之一点为黑色,循环方法为REFLECT

Stop[] stops = new Stop[]{new Stop(0, Color.WHITE), new Stop(1, Color.BLACK)};
LinearGradient lg = new LinearGradient(0, 0, 0.1, 0, true, REFLECT, stops);
Rectangle r = new Rectangle(200, 100);
r.setFill(lg);

现在我们来看看使用REPEAT循环法的效果。下面的代码片段使用了一个位于矩形宽度中间的结束点和一个循环方法REPEAT。这产生了如图 7-11 所示的矩形。在本例中,如果将终点设置为宽度的十分之一,就会得到如图 7-12 所示的矩形。

img/336502_2_En_7_Fig12_HTML.jpg

图 7-12

带有两个停止点的水*线性颜色渐变:起点为白色,十分之一点为黑色,循环方法为REPEAT

img/336502_2_En_7_Fig11_HTML.jpg

图 7-11

带有两个停止点的水*线性颜色渐变:起点为白色,中点为黑色,循环方法为REPEAT

Stop[] stops = new Stop[]{new Stop(0, Color.WHITE), new Stop(1, Color.BLACK)};
LinearGradient lg = new LinearGradient(0, 0, 0.5, 0, true, REPEAT, stops);
Rectangle r = new Rectangle(200, 100);
r.setFill(lg);

您还可以定义两个以上的停止点,如下面的代码片段所示。它将渐变线上起点和终点之间的距离分为四段,每段占宽度的 25%。第一段(从左开始)的颜色介于红色和绿色之间,第二段介于绿色和蓝色之间,第三段介于蓝色和橙色之间,第四段介于橙色和黄色之间。产生的矩形如图 7-13 所示。如果你正在阅读这本书的印刷本,你可能看不到颜色。

img/336502_2_En_7_Fig13_HTML.jpg

图 7-13

具有五个停止点的水*线性颜色渐变

Stop[] stops = new Stop[]{new Stop(0, Color.RED),
                          new Stop(0.25, Color.GREEN),
                          new Stop(0.50, Color.BLUE),
                          new Stop(0.75, Color.ORANGE),
                          new Stop(1, Color.YELLOW)};
LinearGradient lg = new LinearGradient(0, 0, 1, 0, true, NO_CYCLE, stops);
Rectangle r = new Rectangle(200, 100);
r.setFill(lg);

您不仅限于定义水*颜色渐变。您可以使用任意角度的渐变线来定义颜色渐变。下面的代码片段创建了一个从左上角到右下角的渐变。请注意,当比例参数为真时,(0,0)和(1,1)定义了区域左上角和右下角的(x,y)坐标:

Stop[] stops = new Stop[]{new Stop(0, Color.WHITE), new Stop(1, Color.BLACK)};
LinearGradient lg = new LinearGradient(0, 0, 1, 1, true, NO_CYCLE, stops);
Rectangle r = new Rectangle(200, 100);
r.setFill(lg);

以下代码片段定义了(0,0)和(0.1,0.1)点之间的渐变线。它使用REPEAT循环方法来填充剩余的区域。产生的矩形如图 7-14 所示。

img/336502_2_En_7_Fig14_HTML.jpg

图 7-14

带有两个停止点的倾斜线性颜色渐变:起点(0,0)为白色,终点(0.1,0.1)为黑色,使用REPEAT作为循环方法

Stop[] stops = new Stop[]{new Stop(0, Color.WHITE), new Stop(1, Color.BLACK)};
LinearGradient lg = new LinearGradient(0, 0, 0.1, 0.1, true, REPEAT, stops);
Rectangle r = new Rectangle(200, 100);
r.setFill(lg);

使用字符串格式定义线性颜色渐变

您还可以使用LinearGradient类的静态方法valueOf(String colorString)指定字符串格式的线性颜色渐变。通常,字符串格式用于在 CSS 文件中指定线性颜色渐变。它具有以下语法:

linear-gradient([gradient-line], [cycle-method], color-stops-list)

方括号([和])中的参数是可选的。如果不指定可选参数,后面的逗号也需要排除。渐变线参数的默认值是“到底”循环方法参数的默认值是NO_CYCLE。可以用两种方式指定渐变线:

  • 使用两点——起点和终点

  • 使用侧面或角落

对渐变线使用两点的语法是

from point-1 to point-2

这些点的坐标可以以面积的百分比或以像素的实际测量值来指定。对于 200 像素宽 100 像素高的矩形,可以通过以下两种方式指定水*渐变线:

from 0% 0% to 100% 0%

或者

from 0px 0px to 200px 0px

使用边或角的语法是

to side-or-corner

边值或角值可以是上、左、下、右、左上、左下、右下或右上。当使用边或角定义坡线时,只需指定终点。起点推断。例如,值“到顶部”将起点推断为“从底部”,值“到右下角”将起点推断为“从左上角”,依此类推。如果缺少渐变线值,则默认为“到底部”

cycle-method的有效值是repeatreflect。如果缺少,则默认为NO_CYCLE。将cycle-method参数的值指定为NO_CYCLE是一个运行时错误。如果您希望它是NO_CYCLE,只需从语法中省略cycle-method参数。

color-stops-list参数是一个色标列表。色标由一个 web 颜色名称和一个位置(可选)组成,该位置以像素或起点百分比为单位。色标列表的示例有

  • white, black

  • white 0%, black 100%

  • white 0%, yellow 50%, blue 100%

  • white 0px, yellow 100px, red 200px

当您没有指定第一个和最后一个色标的位置时,第一个色标的位置默认为 0%,第二个色标的位置默认为 100%。因此,颜色停止列表"white, black""white 0%, black 100%"基本上是相同的。

如果您没有为列表中的任何颜色停止点指定位置,它们将被分配位置,使它们均匀地位于起点和终点之间。以下两个色标列表是相同的:

  • white, yellow, black, red, green

  • white 0%, yellow 25%, black 50%, red 75%, green 100%

您可以为列表中的某些色标指定位置,而不为其他色标指定位置。在这种情况下,没有位置的色标均匀分布在前面和后面有位置的色标之间。以下两个色标列表是相同的:

  • white, yellow, black 60%, red, green

  • white 0%, yellow 30%, black 50%, red 80%, green 100%

如果列表中某个色标的位置设置小于为任何先前色标指定的位置,则其位置将设置为等于为先前色标设置的最大位置。以下色标列表将第三个色标设置为 10%,小于第二个色标的位置(50%):

white, yellow 50%, black 10%, green

这将在运行时更改为使用 50%的第三个颜色停止,如下所示:

white 0%, yellow 50%, black 50%, green 100%

现在我们来看一些例子。下面的字符串将创建一个从上到下的线性渐变,使用NO_CYCLE作为循环方法。顶部和底部的颜色分别是白色和黑色:

linear-gradient(white, black)

该值与相同

linear-gradient(to bottom, white, black)

下面的代码片段将创建一个如图 7-15 所示的矩形。它定义了一个水*颜色渐变,其终点位于矩形宽度的中间。它使用repeat作为循环方法:

img/336502_2_En_7_Fig15_HTML.jpg

图 7-15

使用字符串格式创建线性颜色渐变

String value = "from 0px 0px to 100px 0px, repeat, white 0%, black 100%";
LinearGradient lg2 = LinearGradient.valueOf(value);
Rectangle r2 = new Rectangle(200, 100);
r2.setFill(lg2);

以下线性颜色渐变的字符串值将创建一个从左上角到右下角的对角线渐变,用白色和黑色填充该区域:

"to bottom right, white 0%, black 100%"

了解径向颜色渐变

在径向颜色渐变中,颜色从一个点开始,以圆形或椭圆形向外*滑过渡。这个形状,比如说一个圆,是由一个中心点和一个半径定义的。颜色的起点被称为渐变的焦点。颜色沿着一条线变化,从渐变的焦点开始,向各个方向变化,直到到达形状的外围。使用三个组件定义径向颜色渐变:

  • 渐变形状(渐变圆的中心和半径)

  • 具有渐变的第一种颜色的焦点

  • 颜色停止

渐变的焦点和渐变形状的中心点可能不同。图 7-16 显示了径向颜色渐变的组成部分。该图显示了两个径向梯度:在左侧,焦点和中心点位于同一位置;在右侧,焦点位于形状中心点的水*右侧。

img/336502_2_En_7_Fig16_HTML.png

图 7-16

定义径向颜色渐变的元素

聚焦点由聚焦角度和聚焦距离定义,如图 7-17 所示。焦点角度是穿过形状中心点的水*线和连接中心点和焦点的线之间的角度。焦距是形状的中心点和渐变的焦点之间的距离。

img/336502_2_En_7_Fig17_HTML.png

图 7-17

在径向颜色渐变中定义焦点

色标列表确定渐变形状内部某一点的颜色值。焦点定义了色标的 0%位置。圆周上的点定义了色标的 100%位置。如何确定渐变圆内某一点的颜色?你可以画一条穿过该点和焦点的线。将使用线中该点每侧最*的颜色停止点对该点的颜色进行插值。

使用径向梯度

RadialGradient类的一个实例代表一种径向颜色渐变。该类包含以下两个构造器,它们的最后一个参数的类型不同:

  • RadialGradient(double focusAngle, double focusDistance, double centerX, double centerY, double radius, boolean proportional, CycleMethod cycleMethod, List<Stop> stops)

  • RadialGradient(double focusAngle, double focusDistance, double centerX, double centerY, double radius, boolean proportional, CycleMethod cycleMethod, Stop... stops)

focusAngle参数定义焦点的聚焦角度。正聚焦角从穿过中心点的水*线和连接中心点和焦点的线开始顺时针测量。逆时针测量负值。

focusDistance参数用圆半径的百分比来表示。该值固定在–1 和 1 之间。也就是说,焦点总是在渐变圆内。如果焦点距离将焦点设置在渐变圆的外围之外,则使用的焦点是圆的外围与连接中心点和设置的焦点的线的交点。

聚焦角度和焦距可以有正值和负值。图 7-18 说明了这一点:它显示了位于距离中心点 80%处的四个焦点,正和负,正和负成 60 度角。

img/336502_2_En_7_Fig18_HTML.png

图 7-18

利用焦点角度和焦距定位焦点

centerXcenterY参数分别定义中心点的 x 和 y 坐标,半径参数是渐变圆的半径。这些参数可以相对于单位*方(在 0.0 和 1.0 之间)或以像素为单位来指定。

proportional参数影响中心点和半径坐标值的处理方式。如果这是真的,它们相对于单位正方形被处理。否则,它们将被视为局部坐标系中的绝对值。关于使用proportional参数的更多细节,请参考本章前面的“使用 LinearGradient 类”一节。

Tip

JavaFX 允许您创建圆形的径向渐变。但是,当要由径向颜色渐变填充的区域具有非方形边界框(例如,矩形)并且您相对于要填充的形状的大小指定渐变圆的半径时,JavaFX 将使用椭圆形径向颜色渐变。这在RadialGradient类的 API 文档中没有记载。我将很快给出一个这样的例子。

cycleMethodstops参数与前面使用LinearGradient类一节中描述的含义相同。在径向颜色渐变中,停止点是沿着连接焦点和渐变圆外围点的线定义的。焦点定义 0%停止点,圆周上的点定义 100%停止点。

让我们看一些使用RadialGradient类的例子。下面的代码片段为一个圆形产生一个径向颜色渐变,如图 7-19 所示:

img/336502_2_En_7_Fig19_HTML.png

图 7-19

具有相同中心点和焦点的径向颜色渐变

Stop[] stops = new Stop[]{new Stop(0, Color.WHITE), new Stop(1, Color.BLACK)};
RadialGradient rg = new RadialGradient(0, 0, 0.5, 0.5, 0.5, true, NO_CYCLE, stops);
Circle c = new Circle(50, 50, 50);
c.setFill(rg);

焦点角度和焦点距离的零值将焦点定位在渐变圆的中心。true proportional参数将中心点坐标(0.5,0.5)解释为 50 乘 50 的圆形矩形边界的(25px,25px)。半径值 0.5 被解释为 25px,这将渐变圆的中心放置在与要填充的圆的中心相同的位置。在这种情况下,NO_CYCLE的循环方法不起作用,因为渐变圆填充了整个圆形区域。在焦点处的色阶是白色的,在渐变圆的外围是黑色的。

以下代码片段将渐变圆的半径指定为要填充的圆的 0.2 倍。这意味着它将使用 10px (0.2 乘以 50px,这是要填充的圆的半径)的渐变圆。产生的圆如图 7-20 所示。由于循环方法被指定为NO_CYCLE,超出半径 0.2 的圆区域被填充为黑色:

img/336502_2_En_7_Fig20_HTML.png

图 7-20

具有相同中心点和焦点的径向颜色渐变具有半径为 0.20 的渐变圆

Stop[] stops = new Stop[]{new Stop(0, Color.WHITE), new Stop(1, Color.BLACK)};
RadialGradient rg = new RadialGradient(0, 0, 0.5, 0.5, 0.2, true, NO_CYCLE, stops);
Circle c = new Circle(50, 50, 50);
c.setFill(rg);

现在让我们使用前面代码片段中的循环方法REPEAT。最终的圆如图 7-21 所示。

img/336502_2_En_7_Fig21_HTML.png

图 7-21

中心点和焦点相同的径向颜色渐变,半径为 0.20 的渐变圆,循环方式为REPEAT

Stop[] stops = new Stop[]{new Stop(0, Color.WHITE), new Stop(1, Color.BLACK)};
RadialGradient rg = new RadialGradient(0, 0, 0.5, 0.5, 0.2, true, REPEAT, stops);
Circle c = new Circle(50, 50, 50);
c.setFill(rg);

所以现在让我们使用一个不同的中心点和焦点。使用 60 度聚焦角度和 0.2 倍半径作为焦距,如以下代码所示。产生的圆如图 7-22 所示。请注意将焦点从中心点移开所获得的 3D 效果。

img/336502_2_En_7_Fig22_HTML.jpg

图 7-22

使用不同中心和焦点的径向颜色渐变

Stop[] stops = new Stop[]{new Stop(0, Color.WHITE), new Stop(1, Color.BLACK)};
RadialGradient rg =
    new RadialGradient(60, 0.2, 0.5, 0.5, 0.2, true, REPEAT, stops);
Circle c = new Circle(50, 50, 50);
c.setFill(rg);

现在让我们用径向颜色渐变填充一个矩形区域(非正方形)。该效果的代码如下,生成的矩形如图 7-23 所示。注意 JavaFX 使用的椭圆渐变形状。您已经将渐变的半径指定为 0.5,并将proportional参数指定为true。由于您的矩形宽 200 像素,高 100 像素,因此会产生两个半径:一个沿 x 轴,一个沿 y 轴,从而产生一个椭圆。沿 x 轴和 y 轴的半径分别为 100 像素和 50 像素。

img/336502_2_En_7_Fig23_HTML.jpg

图 7-23

用径向颜色渐变填充的矩形,其比例参数值为 true

Stop[] stops = new Stop[]{new Stop(0, Color.WHITE), new Stop(1, Color.BLACK)};
RadialGradient rg =
    new RadialGradient(0, 0, 0.5, 0.5, 0.5, true, REPEAT, stops);
Rectangle r = new Rectangle(200, 100);
r.setFill(rg);

如果你想用圆形而不是椭圆形的颜色渐变填充矩形,你应该将proportional参数指定为false,半径值将以像素为单位。以下代码片段生成一个矩形,如图 7-24 所示:

img/336502_2_En_7_Fig24_HTML.jpg

图 7-24

用径向颜色渐变填充的矩形,其比例参数值为 false

Stop[] stops = new Stop[]{new Stop(0, Color.WHITE), new Stop(1, Color.BLACK)};
RadialGradient rg =
    new RadialGradient(0, 0, 100, 50, 50, false, REPEAT, stops);
Rectangle r = new Rectangle(200, 100);
r.setFill(rg);

如何用径向颜色渐变填充三角形或任何其他形状?径向梯度的形状,圆形或椭圆形,取决于几个条件。表 7-1 显示了决定径向颜色渐变形状的标准组合。

表 7-1

用于确定径向颜色渐变形状的标准

|

比例论点

|

填充区域的边界框

|

梯度形状

真实的 *方
真实的 非方形 椭圆
错误的 *方
错误的 非方形

我应该在这里强调,在前面的讨论中,我谈论的是要填补的区域的界限,而不是该区域。例如,假设您想要用径向颜色渐变填充一个三角形。三角形的边界将由其宽度和高度决定。如果三角形有相同的宽度和高度,它的边界是一个正方形区域。否则,它的边界采用矩形区域。

以下代码片段用顶点(0.0,0.0),(0.0,100.0)和(100.0,100.0)填充一个三角形。请注意,这个三角形的边界框是一个 100 像素乘 100 像素的正方形。由此产生的三角形是图 7-25 中的左图。

img/336502_2_En_7_Fig25_HTML.jpg

图 7-25

用圆形和椭圆形的径向颜色渐变填充三角形

Stop[] stops = new Stop[]{new Stop(0, Color.WHITE), new Stop(1, Color.BLACK)};
RadialGradient rg =
    new RadialGradient(0, 0, 0.5, 0.5, 0.2, true, REPEAT, stops);
Polygon triangle = new Polygon(0.0, 0.0, 0.0, 100.0, 100.0, 100.0);
triangle.setFill(rg);

图 7-25 右侧的三角形使用了一个 200px 乘 100px 的矩形边界框,由下面的代码片段生成。请注意,渐变使用了椭圆形状:

Polygon triangle = new Polygon(0.0, 0.0, 0.0, 100.0, 200.0, 100.0);

最后,我们来看一个使用多个色标的例子,焦点在圆的外围,如图 7-26 。产生这种效果的代码如下:

img/336502_2_En_7_Fig26_HTML.png

图 7-26

在径向颜色渐变中使用多个色标

Stop[] stops = new Stop[]{
   new Stop(0, Color.WHITE),
   new Stop(0.40, Color.GRAY),
   new Stop(0.60, Color.TAN),
   new Stop(1, Color.BLACK)};
RadialGradient rg =
   new RadialGradient(-30, 1.0, 0.5, 0.5, 0.5, true, REPEAT, stops);
Circle c = new Circle(50, 50, 50);
c.setFill(rg);

以字符串格式定义径向颜色渐变

您还可以使用RadialGradient类的静态方法valueOf(String colorString)指定字符串格式的径向颜色渐变。通常,字符串格式用于在 CSS 文件中指定径向颜色渐变。它具有以下语法:

radial-gradient([focus-angle], [focus-distance], [center], radius, [cycle-method], color-stops-list)

方括号中的参数是可选的。如果没有指定可选参数,后面的逗号也需要排除。

focus-anglefocus-distance的默认值为 0。您可以用度、弧度、梯度和圈数来指定焦点角度。焦距被指定为半径的百分比。例子如下:

  • focus-angle 45.0deg

  • focus-angle 0.5rad

  • focus-angle 30.0grad

  • focus-angle 0.125turn

  • focus-distance 50%

centerradius参数以相对于被填充区域的百分比或绝对像素来指定。不能将一个参数指定为百分比,而将另一个参数指定为像素。两者必须以相同的单位指定。中心的默认值是(0,0)单位。例子如下:

  • center 50px 50px, radius 50px

  • center 50% 50%, radius 50%

cycle-method参数的有效值是repeatreflect。如果未指定,则默认为NO_CYCLE

使用颜色及其位置来指定颜色色标列表。位置被指定为从焦点到渐变形状外围的直线上的距离的百分比。有关更多详细信息,请参考前面关于在线性颜色渐变中指定颜色停止点的讨论。例子如下:

  • white, black

  • white 0%, black 100%

  • red, green, blue

  • red 0%, green 80%, blue 100%

下面的代码片段会产生一个圆,如图 7-27 所示:

img/336502_2_En_7_Fig27_HTML.png

图 7-27

使用字符串格式指定径向颜色渐变

String colorValue =
   "radial-gradient(focus-angle 45deg, focus-distance 50%, " +
   "center 50% 50%, radius 50%, white 0%, black 100%)";
RadialGradient rg = RadialGradient.valueOf(colorValue);
Circle c = new Circle(50, 50, 50);
c.setFill(rg);

摘要

在 JavaFX 中,您可以为区域指定文本颜色和背景颜色。您可以将颜色指定为统一颜色、图像图案或颜色渐变。统一颜色使用相同的颜色填充整个区域。图像图案允许您用图像图案填充区域。颜色渐变定义了一种颜色模式,其中颜色沿着一条直线从一种颜色变化到另一种颜色。颜色梯度的变化可以是线性的或放射状的。所有的类都包含在javafx.scene.paint包中。

Paint类是一个抽象类,它是其他颜色类的基类。统一颜色、图像图案、线性颜色渐变和径向颜色渐变分别是ColorImagePatternLinearGradientRadialGradient类的实例。使用颜色渐变时会用到Stop类和CycleMethod枚举。您可以使用这些类之一的实例或字符串形式来指定颜色。当使用 CSS 样式化节点时,使用字符串形式指定颜色。

图像图案允许您用图像填充形状。图像可以填充整个形状,也可以使用*铺模式。

使用称为渐变线的轴来定义线性颜色渐变。渐变线上的每个点都有不同的颜色。垂直于渐变线的直线上的所有点都具有相同的颜色,即两条线的交点的颜色。渐变线由起点和终点定义。沿渐变线的颜色是在渐变线上的一些点定义的,这些点称为停止颜色点(或停止点)。使用插值法计算两个停止点之间的颜色。渐变线有方向,是从起点到终点。垂直于穿过停止点的渐变线的线上的所有点将具有停止点的颜色。例如,假设您用颜色 C1 定义了一个停止点 P1。如果你画一条垂直于穿过 P1 点的渐变线的线,该线上的所有点都将具有 C1 的颜色。

在径向颜色渐变中,颜色从一个点开始,以圆形或椭圆形向外*滑过渡。该形状由中心点和半径定义。颜色的起点被称为渐变的焦点。颜色沿着一条线变化,从渐变的焦点开始,向各个方向变化,直到到达形状的外围。

下一章将向你展示如何使用 CSS 样式化场景图中的节点。

八、样式化节点

在本章中,您将学习:

  • 什么是级联样式表

  • 样式、皮肤和主题之间的区别

  • JavaFX 中级联样式表样式的命名约定

  • 如何向场景添加样式表

  • 如何在 JavaFX 应用程序中使用和覆盖默认样式表

  • 如何为节点添加内联样式

  • 关于不同类型的级联样式表属性

  • 关于级联样式表样式选择器

  • 如何使用级联样式表选择器在场景图中查找节点

  • 如何使用已编译的样式表

本章的例子在com.jdojo.style包中。为了让它们工作,您必须在module-info.java文件中添加相应的一行:

...
opens com.jdojo.style to javafx.graphics, javafx.base;
...

什么是级联样式表?

级联样式表(CSS)是一种用于描述 GUI 应用程序中 UI 元素的表示(外观或样式)的语言。CSS 主要是为网页设计 HTML 元素而开发的。它允许将表示与内容和行为分离。在典型的 web 页面中,内容和表示分别使用 HTML 和 CSS 来定义。

JavaFX 允许您使用 CSS 定义 JavaFX 应用程序的外观(或风格)。您可以使用 JavaFX 类库或 FXML 来定义 UI 元素,并使用 CSS 来定义它们的外观。

CSS 提供了编写规则来设置可视属性的语法。一个规则由一个选择器和一组属性-值对组成。选择器是一个字符串,它标识将应用规则的 UI 元素。属性-值对由属性名及其对应的值组成,用冒号(:)分隔。两个属性-值对由分号(;).属性-值对的集合包含在选择器前面的大括号()中。CSS 中的规则示例如下:

.button {
        -fx-background-color: red;
        -fx-text-fill: white;
}

这里,.button是一个选择器,指定规则将应用于所有按钮;-fx-background-color-fx-text-fill是属性名,它们的值分别被设置为redwhite。当应用前面的规则时,所有按钮都将具有红色背景色和白色文本色。

Tip

在 JavaFX 中使用 CSS 类似于在 HTML 中使用 CSS。如果你以前用过 CSS 和 HTML,这一章的信息听起来会很熟悉。理解如何在 JavaFX 中使用 CSS 并不需要以前的 CSS 经验。本章涵盖了使您能够在 JavaFX 中使用 CSS 的所有必要材料。

什么是样式、皮肤和主题?

CSS 规则也被称为样式。CSS 规则的集合被称为样式表风格皮肤主题是三个相关的、高度混淆的概念。

样式提供了一种分离 UI 元素的表现和内容的机制。它们还有助于可视化属性及其值的分组,因此可以由多个 UI 元素共享。JavaFX 允许您使用 JavaFX CSS 创建样式。

皮肤是应用程序特定样式的集合,定义了应用程序的外观。换肤是动态改变应用程序外观(或皮肤)的过程。JavaFX 不提供特定的换肤机制。但是,使用 JavaFX CSS 和 JavaFX API(可用于Scene类和其他与 UI 相关的类),您可以轻松地为 JavaFX 应用程序提供皮肤。

主题是操作系统的视觉特征,反映在所有应用程序的 UI 元素的外观上。例如,更改 Windows 操作系统上的主题会更改所有正在运行的应用程序中 UI 元素的外观。对比皮肤和主题,皮肤是特定于应用程序的,而主题是特定于操作系统的。基于主题的皮肤是很典型的。也就是说,当当前主题改变时,您将改变应用程序的皮肤以匹配主题。JavaFX 不直接支持主题。

一个简单的例子

让我们看一个简单但完整的在 JavaFX 中使用样式表的例子。您将把所有按钮的背景颜色和文本颜色分别设置为红色和白色。清单 8-1 中显示了样式的代码。

.button {
    -fx-background-color: red;
    -fx-text-fill: white;
}

Listing 8-1The Content of the File buttonstyles.css

将清单 8-1 的内容保存在resources\css目录下的buttonstyles.css文件中。为了从代码内部访问资源文件夹,我们再次使用我们在第七章开始时介绍的ResourceUtil实用程序类。

一个场景包含一个样式表的字符串 URL 的ObservableList。您可以使用Scene类的getStylesheets()方法来获取ObservableList的引用。以下代码片段将buttonstyles.css样式表的 URL 添加到场景中:

Scene scene;
...
scene.getStylesheets().add(
    "file://path/to/folder/resources/css/buttonstyles.css");

ResourceUtil类帮助我们构建正确的 URL 路径。

清单 8-2 包含了完整的程序,它显示了三个红色背景和白色文本的按钮。如果您得到以下警告信息,并且没有看到红底白字的按钮,则表明您没有将resources\css目录放在正确的文件夹中;参见ResourceUtil类。

// ButtonStyleTest.java
package com.jdojo.style;

import com.jdojo.util.ResourceUtil;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;

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

      @Override
      public void start(Stage stage) {
            Button yesBtn = new Button("Yes");
            Button noBtn = new Button("No");
            Button cancelBtn = new Button("Cancel");

            HBox root = new HBox();
            root.getChildren().addAll(yesBtn, noBtn, cancelBtn);

            Scene scene = new Scene(root);

            // Add a style sheet to the scene
            var url = ResourceUtil.getResourceURLStr("css/buttonstyles.css");
            scene.getStylesheets().add(url);

            stage.setScene(scene);
            stage.setTitle("Styling Buttons");
            stage.show();
      }
}

Listing 8-2Using a Style Sheet to Change the Background and Text Colors for Buttons

WARNING: com.sun.javafx.css.StyleManager loadStylesheetUnPrivileged Resource "resources/css/buttonstyles.css" not found.

JavaFX CSS 中的命名约定

JavaFX 对 CSS 样式类和属性使用稍微不同的命名约定。CSS 样式的类名基于 JavaFX 类的简单名称,表示场景图中的节点。所有的样式类名都是小写的。例如,Button类的样式类名是button。如果 JavaFX 节点的类名由多个单词组成,例如TextField,则在两个单词之间插入一个连字符以获得样式类名。例如,TextFieldCheckBox类的样式类分别是text-fieldcheck-box

Tip

理解 JavaFX 类和 CSS 样式类之间的区别很重要。JavaFX 类是 Java 类,例如javafx.scene.control.Button。CSS 样式类被用作样式表中的选择器,例如清单 8-1 中的button

JavaFX 样式中的属性名以-fx-开头。例如,普通 CSS 样式中的属性名font-size在 JavaFX CSS 样式中变成了-fx-font-size。JavaFX 使用约定将样式属性名映射到实例变量。它接受一个实例变量;它在两个单词之间插入一个连字符;如果实例变量由多个单词组成,它会将名称转换为小写,并在前面加上前缀-fx-。例如,对于一个名为textAlignment的实例变量,样式属性名应该是-fx-text-alignment

添加样式表

您可以向 JavaFX 应用程序添加多个样式表。样式表被添加到场景或父对象中。SceneParent类维护一个链接到样式表的字符串 URL 的可见列表。使用SceneParent类中的getStylesheets()方法来获取可观察列表的引用,并向列表中添加额外的 URL。以下代码将完成此任务:

// Add two style sheets, ss1.css and ss2.css to a scene
Scene scene = ...
scene.getStylesheets().addAll(
    "file://.../resources/css/ss1.css",
    "file://.../resources/css/ss2.css");

// Add a style sheet, vbox.css, to a VBox (a Parent)
VBox root = new VBox();
root.getStylesheets().add("file://.../vbox.css");

你必须用“…”来代替通过正确的路径,或者再次使用ResourceUtil类。当然,如果可以通过互联网获得样式表,也可以使用http://URL。

默认样式表

在前面的章节中,您开发了带有 UI 元素的 JavaFX 应用程序,而没有使用任何样式表。然而,JavaFX 运行时总是在幕后使用样式表。该样式表被命名为modena.css,它被称为默认样式表用户代理样式表。JavaFX 应用程序的默认外观是在默认样式表中定义的。

modena.css文件打包在 JavaFX 运行时javafx.controls.jar文件中。如果你想知道如何为特定节点设置样式的细节,你需要看一下modena.css文件。您可以使用以下命令提取该文件:

jar -xf javafx.controls.jar ^
    com/sun/javafx/scene/control/skin/modena/modena.css

该命令将modena.css文件放在当前目录下的com\sun\javafx\scene\control\skin\modena目录中。注意,jar命令在JAVA_HOME\bin目录中。

在 JavaFX 8 之前,Caspian 是默认的样式表。里海是在名为com/sun/javafx/scene/control/skin/caspian/caspian.css的文件中的jfxrt.jar文件中定义的。从 JavaFX 8 开始,Modena 是默认的样式表。Application类定义了两个名为STYLESHEET_CASPIANSTYLESHEET_MODENAString常量来表示这两个主题。使用Application类的以下静态方法来设置和获取应用程序范围的默认样式表:

  • public static void setUserAgentStylesheet(String url)

  • public static String getUserAgentStylesheet()

使用setUserAgentStylesheet(String url)方法设置应用程序范围的默认值。值null将恢复*台默认样式表。以下语句将 Caspian 设置为默认样式表:

Application.setUserAgentStylesheet(Application.STYLESHEET_CASPIAN);

使用getUserAgentStylesheet()方法返回应用程序的当前默认样式表。如果其中一个内置样式表是默认的,它将返回null

添加内联样式

场景图中节点的 CSS 样式可能来自样式表或内联样式。在上一节中,您学习了如何向SceneParent对象添加样式表。在本节中,您将学习如何为节点指定内联样式。

Node类有一个属于StringProperty类型的style属性。style属性保存节点的内联样式。您可以使用setStyle(String inlineStyle)getStyle()方法来设置和获取一个节点的内联样式。

样式表中的样式和内联样式是有区别的。样式表中的样式由一个选择器和一组属性值对组成,它可能影响场景图中的零个或多个节点。样式表中受样式影响的节点数取决于与样式选择器匹配的节点数。内联样式不包含选择器。它只由一组属性值对组成。内联样式会影响设置它的节点。以下代码片段使用按钮的内联样式,以红色和粗体显示其文本:

Button yesBtn = new Button("Yes");
yesBtn.setStyle("-fx-text-fill: red; -fx-font-weight: bold;");

清单 8-3 显示六个按钮。它使用两个VBox实例来保存三个按钮。它将两个VBox实例放入一个HBox。内嵌样式用于为两个VBox实例设置 4.0px 的蓝色边框。HBox的内嵌样式设置了 10.0 像素的海军蓝边框。产生的屏幕如图 8-1 所示。

img/336502_2_En_8_Fig1_HTML.jpg

图 8-1

一个按钮、两个VBox实例和一个使用内嵌样式的HBox

// InlineStyles.java
package com.jdojo.style;

import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

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

        @Override

        public void start(Stage stage) {
                Button yesBtn = new Button("Yes");
                Button noBtn = new Button("No");
                Button cancelBtn = new Button("Cancel");

                // Add an inline style to the Yes button
                yesBtn.setStyle(
                         "-fx-text-fill: red; -fx-font-weight: bold;");

                Button openBtn = new Button("Open");
                Button saveBtn = new Button("Save");
                Button closeBtn = new Button("Close");

                VBox vb1 = new VBox();
                vb1.setPadding(new Insets(10, 10, 10, 10));
                vb1.getChildren().addAll(yesBtn, noBtn, cancelBtn);

                VBox vb2 = new VBox();
                vb2.setPadding(new Insets(10, 10, 10, 10));
                vb2.getChildren().addAll(openBtn, saveBtn, closeBtn);

                // Add a border to VBoxes using an inline style
                vb1.setStyle(
                         "-fx-border-width: 4.0; -fx-border-color: blue;");
                vb2.setStyle(
                         "-fx-border-width: 4.0; -fx-border-color: blue;");

                HBox root = new HBox();
                root.setSpacing(20);
                root.setPadding(new Insets(10, 10, 10, 10));
                root.getChildren().addAll(vb1, vb2);

                // Add a border to the HBox using an inline style
                root.setStyle(
                         "-fx-border-width: 10.0; -fx-border-color: navy;");

                Scene scene = new Scene(root);
                stage.setScene(scene);
                stage.setTitle("Using Inline Styles");
                stage.show();
        }
}

Listing 8-3Using Inline Styles

节点样式的优先级

在 JavaFX 应用程序中,节点的可视属性可能来自多个来源,这种情况非常普遍。例如,按钮的字体大小可以由 JavaFX 运行时设置,样式表可以添加到按钮的父级和场景中,可以为按钮设置内联样式,并且可以使用setFont(Font f)方法以编程方式添加。如果按钮的字体大小值可以从多个来源获得,JavaFX 将使用一个规则来决定使用哪个来源的值。

考虑下面的代码片段和清单 8-4 中显示的stylespriorities.css样式表:

.button {
        -fx-font-size: 24px;
        -fx-font-weight: bold;
}

Listing 8-4The Content of the stylespriorities.css File

Button yesBtn = new Button("Yes");
yesBtn.setStyle("-fx-font-size: 16px");
yesBtn.setFont(new Font(10));

Scene scene = new Scene(yesBtn);
scene.getStylesheets().addAll(
    "file://pat/to/resources/css/stylespriorities.css");
...

按钮的字体大小是多少?它会是 JavaFX 运行时设置的默认字体大小,24px,在stylespriorities.css中声明,16px 由 inline 样式设置,还是 10px 由程序使用setFont()方法设置?正确答案是 16px,是内嵌样式设置的。

JavaFX 运行时使用以下优先级规则来设置节点的可视属性。使用具有属性值的较高优先级的源:

  • 内嵌样式(最高优先级)

  • 父样式表

  • 场景样式表

  • 使用 JavaFX API 在代码中设置的值

  • 用户代理样式表(最低优先级)

添加到父节点的样式表比添加到场景中的样式表具有更高的优先级。这使得开发人员能够为场景图的不同分支定制样式。例如,您可以使用两个样式表来不同地设置按钮的属性:一个用于场景中的按钮,另一个用于任何HBox中的按钮。一个HBox中的按钮将使用其父按钮的样式,而所有其他按钮将使用场景中的样式。

使用 JavaFX API 设置的值,例如setFont()方法,具有第二低的优先级。

Note

使用 Java API 在样式表和代码中设置相同的节点属性是一个常见的错误。在这种情况下,样式表中的样式获胜,开发人员花费无数时间试图找到代码中设置的属性没有生效的原因。

用户代理使用的样式表的优先级最低。什么是用户代理?一般来说,用户代理是一个解释文档并将样式表应用于文档以进行格式化、打印或读取的程序。例如,web 浏览器是将默认格式应用于 HTML 文档的用户代理。在我们的例子中,用户代理是 JavaFX 运行时,它使用modena.css样式表为所有 UI 节点提供默认外观。

Tip

节点继承的默认字体大小由系统字体大小决定。并非所有节点都使用字体。字体仅由那些显示文本的节点使用,例如一个Button或一个CheckBox。为了试验默认字体,您可以更改系统字体,并使用这些节点的getFont()方法在代码中检查它。

清单 8-5 展示了从多个来源中选择一种风格的优先规则。它将样式表添加到场景中,如清单 8-4 所示。产生的屏幕如图 8-2 所示。

img/336502_2_En_8_Fig2_HTML.jpg

图 8-2

使用不同来源样式的节点

// StylesPriorities.java
package com.jdojo.style;

import com.jdojo.util.ResourceUtil;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.HBox;
import javafx.scene.text.Font;
import javafx.stage.Stage;

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

      @Override
      public void start(Stage stage) {
            Button yesBtn = new Button("Yes");
            Button noBtn = new Button("No");
            Button cancelBtn = new Button("Cancel");

            // Change the font size for the Yes button
            // using two methods: inline style and JavaFX API
            yesBtn.setStyle("-fx-font-size: 16px");
            yesBtn.setFont(new Font(10));

            // Change the font size for the No button using the JavaFX API
            noBtn.setFont(new Font(8));

            HBox root = new HBox();
            root.setSpacing(10);
            root.getChildren().addAll(yesBtn, noBtn, cancelBtn);

            Scene scene = new Scene(root);

            // Add a style sheet to the scene
            var url = ResourceUtil.getResourceURLStr(
                 "css/stylespriorities.css");
            scene.getStylesheets().addAll(url);

            stage.setScene(scene);
            stage.setTitle("Styles Priorities");
            stage.show();
      }
}

Listing 8-5Testing Priorities of Styles for a Node

Yes按钮的字体大小值有四个来源:

  • 内嵌样式(16px)

  • 添加到场景中的样式表(24px)

  • JavaFX API (10px)

  • 用户代理设置的默认字体大小(JavaFX 运行时)

Yes按钮从它的内嵌样式中获得 16px 的字体大小,因为它具有最高的优先级。No按钮的字体大小值有三个来源:

  • 添加到场景中的样式表(24px)

  • JavaFX API (10px)

  • 用户代理设置的默认字体大小(JavaFX 运行时)

No按钮从添加到场景中的样式表中获取 24px 字体大小,因为这在三个可用的源中具有最高的优先级。

Cancel按钮的字体大小值有两个来源:

  • 添加到场景中的样式表(24px)

  • 用户代理设置的默认字体大小(JavaFX 运行时)

Cancel按钮从添加到场景中的样式表中获取 24px 字体大小,因为这在两个可用的源中具有最高的优先级。所有按钮的文本都以粗体显示,因为您在样式表中使用了“-fx-font-weight: bold;”样式,并且该属性值不会被任何其他源覆盖。

此时,您可能会想到几个问题:

  • 如何让Cancel按钮使用 JavaFX 运行时设置的默认字体大小?

  • 如果按钮在HBox中,如何使用一种字体大小(或任何其他属性),如果按钮在VBox中,如何使用另一种字体大小?

通过对样式表中声明的样式使用适当的选择器,可以实现所有这些和其他一些效果。我将很快讨论 JavaFX CSS 支持的不同类型的选择器。

继承 CSS 属性

JavaFX 为 CSS 属性提供了两种类型的继承:

  • CSS 属性类型的继承

  • CSS 属性值的继承

在第一种类型的继承中,JavaFX 类中声明的所有 CSS 属性都被它的所有子类继承。比如,Node类声明了一个cursor属性,它对应的 CSS 属性是-fx-cursor。因为Node类是所有 JavaFX 节点的超类,所以-fx-cursor CSS 属性可用于所有节点类型。

在第二种类型的继承中,节点的 CSS 属性可以从其父节点继承其值。节点的父节点是场景图中节点的容器,而不是它的 JavaFX 超类。默认情况下,节点的某些属性值是从其父节点继承的,对于某些属性,节点需要明确指定它要从其父节点继承属性值。

如果希望从父节点继承值,可以将inherit指定为节点的 CSS 属性值。如果一个节点默认从它的父节点继承一个 CSS 属性,您不需要做任何事情,也就是说,您甚至不需要将属性值指定为inherit。如果要覆盖继承的值,需要显式指定该值(覆盖父值)。

清单 8-6 展示了一个节点如何继承其父节点的 CSS 属性。它给HBox增加了两个按钮,OK 和 Cancel。下列 CSS 属性是在父按钮和 OK 按钮上设置的。“取消”按钮上没有设置 CSS 属性:

/* Parent Node (HBox)*/
-fx-cursor: hand;
-fx-border-color: blue;
-fx-border-width: 5px;

/* Child Node (OK Button)*/
-fx-border-color: red;
-fx-border-width: inherit;

-fx-cursor CSS 属性在Node类中声明,默认情况下由所有节点继承。HBox覆盖默认值并覆盖到HAND光标上。OK 和 Cancel 按钮都从它们的父按钮HBox继承了-fx-cursorHAND光标值。当您将鼠标指向由HBox和这些按钮占据的区域时,您的鼠标指针将变为HAND光标。您可以使用 OK 和 Cancel 按钮上的"-fx-cursor: inherit"样式来实现默认的相同功能。

默认情况下,节点不会继承与边框相关的 CSS 属性。HBox将其-fx-border-color设置为蓝色,-fx-border-width设置为 5px。OK 按钮将其-fx-border-color设置为红色,将-fx-border-width设置为inheritinherit值将使 OK 按钮的-fx-border-width从其父按钮HBox继承,即 5px。图 8-3 显示了添加该编码后的变化。

img/336502_2_En_8_Fig3_HTML.png

图 8-3

从其父级继承其边框宽度和光标 CSS 属性的按钮

// CSSInheritance.java
package com.jdojo.style;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;

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

        @Override
        public void start(Stage stage) {
                Button okBtn = new Button("OK");
                Button cancelBtn = new Button("Cancel");

                HBox root = new HBox(10); // 10px spacing
                root.getChildren().addAll(okBtn, cancelBtn);

                // Set styles for the OK button and its parent HBox
                root.setStyle(
             "-fx-cursor: hand;-fx-border-color: blue;-fx-border-width: 5px;");
                okBtn.setStyle(
            "-fx-border-color: red;-fx-border-width: inherit;");

                Scene scene = new Scene(root);
                stage.setScene(scene);
                stage.setTitle("CSS Inheritance");
                stage.show();
        }

}

Listing 8-6Inheriting CSS Properties from the Parent Node

Tip

默认情况下,节点从其父节点继承-fx-cursor-fx-text-alignment-fx-font CSS 属性。

CSS 属性的类型

Java(以及 JavaFX)中的所有值都有一个类型。样式中设置的 CSS 属性值也有类型。每种类型的值都有不同的语法。JavaFX CSS 支持以下类型:

  • inherit

  • boolean

  • string

  • number, integer

  • size

  • length

  • percentage

  • angle

  • duration

  • point

  • color-stop

  • uri

  • effect

  • font

  • paint

  • color

请注意,CSS 类型与 Java 类型无关。它们只能用于指定 CSS 样式表或内联样式中的值。JavaFX 运行时负责在将这些类型分配给节点之前,将它们解析并转换为适当的 JavaFX 类型。

继承类型

在上一节中,您已经看到了一个使用inherit类型的例子。它用于从父节点继承节点的 CSS 属性值。

布尔类型

您可以将boolean类型值指定为truefalse。它们也可以被指定为字符串:"true""false"。下面的样式将TextField节点的-fx-display-caret CSS 属性设置为false:

.text-field {
        -fx-display-caret: false;
}

字符串类型

字符串值可以用单引号或双引号括起来。如果字符串值用双引号括起来,作为值的一部分的双引号应该被转义,例如\"\22。类似地,单引号作为包含在单引号中的字符串值的一部分必须被转义,例如\'\27。下面的样式使用字符串来设置皮肤和字体属性。它用双引号将皮肤属性的字符串值括起来,用单引号将字体属性的字体系列括起来:

.my-control {
        -fx-skin: "com.jdojo.MySkin";
        -fx-font: normal bold 20px 'serif';
}

Tip

字符串值不能直接包含换行符。要在字符串值中嵌入换行符,请使用转义序列\A\00000a

数字整数类型

数值可以用整数或实数来表示。它们是使用十进制数字格式指定的。以下样式将不透明度设置为 0.60:

.my-style {
        -fx-opacity: 0.60;
}

表示大小的 CSS 属性值可以用一个数字后跟一个长度单位来指定。长度的单位可以是px(像素)mm(毫米)cm(厘米)in(英寸)pt(点)pc(十二点活字)emex。还可以使用长度的百分比来指定大小,例如,节点的宽度或高度。如果指定了百分比单位,它必须紧跟在数字之后,例如 12px,2em,80%:

.my-style {
        -fx-font-size: 12px;
        -fx-background-radius: 0.5em;
        -fx-border-width: 5%;
}

尺寸类型

尺寸是以长度或百分比为单位的数字;参见前面的数字类型。

长度和百分比类型

长度是一个数加上一个px, mm, cm, in, pt, pc, em, ex。百分比是一个数字加上一个“%”符号。

角度类型

使用数字和单位来指定角度。角度的单位可以是deg(度)rad(弧度)grad(梯度)或turn(转角)。以下样式将-fx-rotate CSS 属性设置为 45 度:

.my-style {
        -fx-rotate: 45deg;
}

持续时间类型

持续时间是一个数字加上一个持续时间单位,可以是“s”(秒)、“ms”(毫秒)或“不定”

使用 x 和 y 坐标指定一个点。可以使用由空格分隔的两个数字来指定,例如0 0, 100 0, 90 67,或者以百分比形式指定,例如2% 2%。以下样式指定从点(0,0)到(100,0)的线性渐变颜色:

.my-style {
        -fx-background-color: linear-gradient(from 0 0 to 100 0, repeat,
           red, blue);
}

色挡

色标用于在线性或放射状颜色渐变中指定特定距离处的颜色。颜色光圈由颜色和光圈距离组成。颜色和距离由空格分隔。停止距离可以指定为百分比,例如 10%,或者指定为长度,例如 65px。颜色停止的一些例子是white 0%yellow 50%yellow 100px。请参阅第七章,了解更多关于如何使用颜色挡块的详细信息。

URI

可使用url(<address>)功能指定 URI。相对于 CSS 文件的位置解析相对文件<address>:

.image-view {
        -fx-image: url("http://jdojo.com/myimage.png");
}

效果类型

可以分别使用dropshadow()innershadow() CSS 函数为使用 CSS 样式的节点指定投影和内部阴影效果。他们的签名是

  • dropshadow(<blur-type>, <color>, <radius>, <spread>, <x-offset>, <y-offset>)

  • innershadow(<blur-type>, <color>, <radius>, <choke>, <x-offset>, <y-offset>)

<blur-type>值可以是高斯、一次通过框、三次通过框或两次通过框。阴影的颜色在<color>中指定。<radius>值在 0.0 和 127.0 之间指定阴影模糊内核的半径。阴影的扩散/阻塞指定在 0.0 和 1.0 之间。最后两个参数以像素为单位指定 x 和 y 方向上的阴影偏移。以下样式显示了如何指定-fx-effect CSS 属性的值:

.drop-shadow-1 {
        -fx-effect: dropshadow(gaussian, gray, 10, 0.6, 10, 10);
}

.drop-shadow-2 {
        -fx-effect: dropshadow(one-pass-box, gray, 10, 0.6, 10, 10);
}

.inner-shadow-1 {
        -fx-effect: innershadow(gaussian, gray, 10, 0.6, 10, 10);
}

字体类型

字体由四个属性组成:系列、大小、样式和粗细。有两种方法可以指定字体 CSS 属性:

  • 使用四个 CSS 属性分别指定字体的四个属性:-fx-font-family-fx-font-size-fx-font-style-fx-font-weight

  • 使用一个简单的 CSS 属性-fx-font将所有四个属性指定为一个值。

字体系列是一个字符串值,可以是系统上实际可用的字体系列,例如"Arial""Times",或者是通用的系列名称,例如"serif""sans-serif""monospace"

字体大小可以用pxemptincm等单位指定。如果省略字体大小的单位,则采用 px(像素)。

字体样式可以是normalitalicoblique

字体粗细可以指定为normalboldbolderlighter100200300400500600700800900

以下样式分别设置字体属性:

.my-font-style {
        -fx-font-family: "serif";
        -fx-font-size: 20px;
        -fx-font-style: normal;
        -fx-font-weight: bolder;
}

指定字体属性的另一种方法是将字体的所有四个属性合并为一个值,并使用-fx-font CSS 属性。使用-fx-font属性的语法是

-fx-font: <font-style> <font-weight> <font-size> <font-family>;

以下样式使用-fx-font CSS 属性来设置字体属性:

.my-font-style {
        -fx-font: italic bolder 20px "serif";
}

颜料颜色类型

绘画类型值指定一种颜色,例如,矩形的填充颜色或按钮的背景颜色。您可以通过以下方式指定颜色值:

  • 使用linear-gradient()功能

  • 使用radial-gradient()功能

  • 使用各种颜色值和颜色函数

关于如何使用linear-gradient()radial-gradient()函数在字符串格式中指定渐变颜色的完整讨论,请参考第七章。这些函数用于指定颜色渐变。以下样式显示了如何使用这些函数:

.my-style {
      -fx-fill: linear-gradient(from 0% 0% to 100% 0%, black 0%, red 100%);
      -fx-background-color: radial-gradient(radius 100%, black, red);
}

您可以通过多种方式指定纯色:

  • 使用命名的颜色

  • 使用查找的颜色

  • 使用rgb()rgba()功能

  • 使用红、绿、蓝(RGB)十六进制表示法

  • 使用hsb()hsba()功能

  • 使用颜色功能:derive()ladder()

您可以使用预定义的颜色名称来指定颜色值,例如,redbluegreenaqua:

.my-style {
        -fx-background-color: red;
}

您可以将颜色定义为节点或其任何父节点上的 CSS 属性,稍后,当您想要使用它的值时,可以通过名称来查找它。以下样式定义了一个名为my-color的颜色,并在以后引用它:

.root {
        my-color: black;
}

.my-style {
        -fx-fill: my-color;
}

您可以使用rgb(red, green, blue)rgba(red, green, blue, alpha)功能根据 RGB 分量定义颜色:

.my-style-1 {
        -fx-fill: rgb(0, 0, 255);
}

.my-style-2 {
        -fx-fill: rgba(0, 0, 255, 0.5);
}

您可以指定#rrggbb#rgb格式的颜色值,其中rrggbb分别是十六进制格式的红色、绿色和蓝色分量的值。请注意,您需要使用两位数字或一位十六进制数字来指定这三个组成部分。不能用一个十六进制数字指定某些组件,而用两个数字指定其他组件:

.my-style-1 {
        -fx-fill: #0000ff;
}

.my-style-2 {
        -fx-fill: #0bc;
}

您可以使用hsb(hue, saturation, brightness)hsba(hue, saturation, brightness, alpha)功能指定色调、饱和度和亮度(HSB)颜色分量中的颜色值:

.my-style-1 {
        -fx-fill: hsb(200, 70%, 40%);
}

.my-style-2 {
        -fx-fill: hsba(200, 70%, 40%, 0.30);
}

您可以使用derive()ladder()函数计算其他颜色的颜色。JavaFX 默认的 CSS,modena.css,使用了这种技术。它定义了一些基色,并从基色中派生出其他颜色。

derive函数有两个参数:

derive(color, brightness)

derive()功能导出指定颜色的更亮或更暗版本。亮度值的范围从–100%到 100%。–100%的亮度表示全黑,0%表示亮度没有变化,100%表示全白。以下样式将使用暗 20%的红色版本:

.my-style {
        -fx-fill: derive(red, -20%);
}

ladder()函数将一种颜色和一个或多个色标作为参数:

ladder(color, color-stop-1, color-stop-2, ...)

ladder()函数想象成使用色标创建渐变,然后使用指定颜色的亮度返回颜色值。如果指定颜色的亮度为 x%,将返回距离渐变起点 x%距离处的颜色。例如,对于 0%亮度,返回渐变 0.0 端的颜色;对于 40%的亮度,返回渐变 0.4 端的颜色。

考虑以下两种风格:

.root {
        my-base-text-color: red;
}

.my-style {
        -fx-text-fill: ladder(my-base-text-color, white 29%, black 30%);
}

ladder()功能将根据my-base-text-color的亮度返回颜色whiteblack。如果其亮度为 29%或更低,则返回white;否则,返回black。您可以在ladder()功能中指定任意数量的颜色停止,根据指定颜色的亮度从各种颜色中进行选择。

您可以使用这种技术动态改变 JavaFX 应用程序的颜色。默认的样式表modena.css定义了一些基色,并使用derive()ladder()函数来派生不同亮度的其他颜色。您需要在样式表中为root类重新定义基本颜色,以进行应用程序范围的颜色更改。

指定背景颜色

一个节点(一个Region和一个Control)可以有多个背景填充,这是使用三个属性指定的:

  • -fx-background-color

  • -fx-background-radius

  • -fx-background-insets

-fx-background-color属性是逗号分隔的颜色值列表。列表中颜色的数量决定了将要绘制的矩形的数量。您需要使用另外两个属性为每个矩形指定四个角的半径值和四个边的插入值。颜色值的数量必须与半径值和插入值的数量相匹配。

属性是一个由逗号分隔的四个半径值组成的列表,用于填充矩形。列表中的一组半径值可以只指定一个值,例如 10,或者用空格分隔的四个值,例如 10 5 15 20。按顺序为左上角、右上角、右下角和左下角指定半径值。如果只指定了一个半径值,则所有拐角使用相同的半径值。

属性是一个由逗号分隔的四个插入值组成的列表,用于填充矩形。列表中的一组插入值可以只指定一个值,例如 10,或者用空格分隔的四个值,例如 10 5 15 20。按顺序为顶部、右侧、底部和左侧指定插入值。如果只指定了一个插入值,则所有边都使用相同的插入值。

我们来看一个例子。下面的代码片段创建了一个Pane,它是Region类的子类:

Pane pane = new Pane();
pane.setPrefSize(100, 100);

图 8-4 显示了提供以下三种样式时Pane的外观:

img/336502_2_En_8_Fig4_HTML.png

图 8-4

有三种不同背景填充的Pane

.my-style-1 {
        -fx-background-color: gray;
        -fx-background-insets: 5;
        -fx-background-radius: 10;
}

.my-style-2 {
        -fx-background-color: gray;
        -fx-background-insets: 0;
        -fx-background-radius: 0;
}

.my-style-3 {
        -fx-background-color: gray;
        -fx-background-insets: 5 10 15 20;
        -fx-background-radius: 10 0 0 5;
}

这三种样式都使用灰色填充颜色,这意味着只绘制一个矩形。第一种样式在所有四个边上使用 5px 的插入,在所有角上使用 10px 的半径。第二种样式使用 0px 的嵌入和 0px 的半径,这使得填充矩形占据了窗格的整个区域。第三种样式在两侧使用不同的插图:顶部 5px,右侧 10px,底部 15px,左侧 20px。请注意,第三种样式的每一侧都有不同的未填充背景。第三种样式也为四个角的半径设置了不同的值:左上 10px,右上 0px,右下 0px,左下 5px。请注意,如果一个角的半径是 0px,角上的两条边以 90 度相交。

如果将以下样式应用于同一窗格,背景将被填充,如图 8-5 所示:

img/336502_2_En_8_Fig5_HTML.png

图 8-5

带有三种不同半径和插入值的背景填充的窗格

.my-style-4 {
        -fx-background-color: red, green, blue;
        -fx-background-insets: 5 5 5 5, 10 15 10 10, 15 20 15 15;
        -fx-background-radius: 5 5 5 5, 0 0 10 10, 0 20 5 10;
}

该样式使用三种颜色,因此将绘制三个背景矩形。背景矩形按照样式中指定的顺序绘制:红色、绿色和蓝色。插入和半径值的指定顺序与颜色的顺序相同。该样式对红色使用相同的插入值和半径值。可以用一个值替换四个相似值的集合;即前面样式中的 5 5 5 5 可以用 5 代替。

指定边框

一个节点(一个Region和一个Control)可以通过 CSS 拥有多个边界。使用五个属性指定边框:

  • -fx-border-color

  • -fx-border-width

  • -fx-border-radius

  • -fx-border-insets

  • -fx-border-style

每个属性由逗号分隔的项目列表组成。每个项目可能由一组值组成,这些值由空格分隔。

边框颜色

-fx-border-color属性列表中的项目数量决定了所绘制的边框数量。以下样式将用红色绘制一个边框:

-fx-border-color: red;

下面的样式指定了一组redgreenblueaqua颜色来分别绘制上、右、下和左侧的边框。请注意,它仍然只产生一个边框,而不是四个边框,四边的颜色不同:

-fx-border-color: red green blue aqua;

以下样式指定了两组边框颜色:

-fx-border-color: red green blue aqua, tan;

第一组由四种颜色组成red green blue aqua,第二组仅由一种颜色组成tan。这将导致两个边界。第一个边框将在四边涂上不同的颜色;第二个边框的四边将使用相同的颜色。

Tip

节点的形状可能不是矩形的。在这种情况下,只有集合中的第一个边框颜色(和其他属性)将用于绘制整个边框。

边框宽度

您可以使用-fx-border-width属性指定边框的宽度。您可以选择为边框的所有四条边指定不同的宽度。按顺序为顶部、右侧、底部和左侧指定不同的边框宽度。如果未指定宽度值的单位,则使用像素。

以下样式指定一个边框,所有边都以 2px 宽度涂为红色:

-fx-border-color: red;
-fx-border-width: 2;

下面的样式指定了三个边框,由在-fx-border-color属性中指定的三组颜色决定。前两个边框使用不同的四边边框宽度。第三个边框在所有边上都使用 3px 的边框宽度:

-fx-border-color: red green blue black, tan, aqua;
-fx-border-width: 2 1 2 2, 2 2 2 1, 3;

边界半径

您可以使用-fx-border-radius属性指定边框四个角的半径值。可以为所有拐角指定相同的半径值。按顺序为左上角、右上角、右下角和左下角指定不同的半径值。如果没有指定半径值的单位,则使用像素。

以下样式在所有四个角上指定一个红色边框,宽度为 2px,半径为 5px:

-fx-border-color: red;
-fx-border-width: 2;
-fx-border-radius: 5;

下面的样式指定了三个边框。前两个边界对四个角使用不同的半径值。第三个边界对所有角使用 0px 的半径值:

-fx-border-color: red green blue black, tan, aqua;
-fx-border-width: 2 1 2 2, 2 2 2 1, 3;
-fx-border-radius: 5 2 0 2, 0 2 0 1, 0;

边框嵌入

您可以使用-fx-border-insets属性指定边框四边的插入值。您可以为所有边指定相同的插入值。按顺序为顶部、右侧、底部和左侧指定不同的插入值。如果未指定插入值的单位,则使用像素。

下面的样式指定一个红色边框,宽度为 2px,半径为 5px,四边的嵌入量为 20px:

-fx-border-color: red;
-fx-border-width: 2;
-fx-border-radius: 5;
-fx-border-insets: 20;

下面的样式指定了三个边框,各边的插入距离分别为 10px、20px 和 30px:

-fx-border-color: red green blue black, tan, aqua;
-fx-border-width: 2 1 2 2, 2 2 2 1, 3;
-fx-border-radius: 5 2 0 2, 0 2 0 1, 0;
-fx-border-insets: 10, 20, 30;

Tip

插图是距将要绘制边框的节点一侧的距离。边界的最终位置还取决于其他属性,例如,-fx-border-width-fx-border-style

边框样式

属性定义了一个边框的样式。它的值可能包含如下几个部分:

-fx-border-style: <dash-style> [phase <number>] [<stroke-type>] [line-join <line-join-value>] [line-cap <line-cap-value>]

<dash-style>的值可以是nonesoliddotteddashedsegments(<number>, <number>...)<stroke-type>的值可以是centeredinsideoutside<line-join-value>的值可以是miter <number>bevelround<line-cap-value>的值可以是squarebuttround

最简单的边框样式是只指定<dash-style>的值:

-fx-border-style: solid;

segments()功能用于使用交替的破折号和间隙为图案添加边框:

-fx-border-style: segments(dash-length, gap-length, dash-length, ...);

该函数的第一个参数是破折号的长度;第二个论点是差距的长度;等等。在最后一个论点之后,这个模式从头开始重复。以下样式将使用 10px 破折号、5px 间距、10px 破折号等图案绘制边框:

-fx-border-style: segments(10px, 5px);

您可以向该函数传递任意数量的虚线和间隙线段。该函数希望您传递偶数个值。如果您传递奇数个值,这将导致值连接在一起,使它们的数量为偶数。比如你用了segments(20px, 10px, 5px),就跟你过了segments(20px, 10px, 5px, 20px, 10px, 5px)一样。

只有在使用segments()功能时,phase参数才适用。phase参数后面的数字指定了对应于笔画开始的虚线图案的偏移量。考虑以下样式:

-fx-border-style: segments(20px, 5px) phase 10.0;

它将phase参数指定为 10.0。虚线图案的长度为 25px。第一段将从模式开始处的 10px 开始。也就是说,第一个破折号的长度只有 10px。第二段将是一个 5px 的缺口,后跟一个 20px 的破折号,依此类推。phase的默认值为 0.0。

<stroke-type>有三个有效值:居中、内部和外部。它的值决定了边框相对于插图的绘制位置。假设您有一个 200 像素乘 200 像素的区域。假设您已经指定了上插图为 10px,上边框宽度为 4px。如果<stroke-type>被指定为居中,顶部的边界厚度将占据从区域顶部边界的第 8 个像素到第 12 个像素的区域。对于内部的<stroke-type>,边框粗细将占据从第 10 个像素到第 14 个像素的区域。对于作为外部的<stroke-type>,顶部的边框粗细将占据第六个像素到第十个像素的区域。

您可以使用line-join参数指定如何连接两个边界段。其值可以是miterbevelround。如果将line-join的值指定为miter,则需要传递一个斜接限制值。如果指定的斜接限制小于斜接长度,则改用斜角连接。斜接长度是斜接的内点和外点之间的距离。斜接长度是根据边框宽度来测量的。“斜接限制”参数指定两条相交边界线段的外侧边缘可以延伸多远以形成斜接。例如,假设斜接长度为 5,而您将斜接限制指定为 4,则使用斜角连接;但是,如果指定的斜接限制大于 5,则使用斜接联接。以下样式使用 30 的斜接限制:

-fx-border-style: solid line-join miter 30;

line-cap参数的值指定如何绘制边界线段的起点和终点。有效值为squarebuttround。下面的样式指定了一个roundline-cap:

-fx-border-style: solid line-join bevel 30 line-cap round;

我们来看一些例子。图 8-6 显示了 100 像素乘 50 像素的Pane类的四个实例,它们应用了以下样式:

img/336502_2_En_8_Fig6_HTML.png

图 8-6

使用边框样式

.my-style-1 {
        -fx-border-color: black;
        -fx-border-width: 5;
        -fx-border-radius: 0;
        -fx-border-insets: 0;
        -fx-border-style: solid line-join bevel line-cap square;
}

.my-style-2 {
        -fx-border-color: red, black;
        -fx-border-width: 5, 5;
        -fx-border-radius: 0, 0;
        -fx-border-insets: 0, 5;
        -fx-border-style: solid inside, dotted outside;
}

.my-style-3 {
        -fx-border-color: black, black;
        -fx-border-width: 1, 1;
        -fx-border-radius: 0, 0;
        -fx-border-insets: 0, 5;
        -fx-border-style: solid centered, solid centered;
}

.my-style-4 {
        -fx-border-color: red black red black;
        -fx-border-width: 5;
        -fx-border-radius: 0;
        -fx-border-insets: 0;
        -fx-border-style: solid line-join bevel line-cap round;
}

注意,第二种样式通过指定适当的插入和笔画类型(insideoutside)实现了两个边框的重叠,一个是纯红的,一个是点黑的。边框按照指定的顺序绘制。在这种情况下,首先绘制实线边框是很重要的;否则,您将看不到虚线边框。第三个绘制了两个边框,使它看起来像一个双边框类型。

Tip

一个Region也可以有一个背景图像和一个通过 CSS 指定的边框图像。请参考网上提供的 JavaFX CSS 参考指南,了解更多详情。JavaFX 中的节点支持许多其他 CSS 样式。这些节点的样式将在本书的后面讨论。

了解样式选择器

样式表中的每个样式都有一个关联的选择器,它标识场景图中关联的 JavaFX CSS 属性值所应用到的节点。JavaFX CSS 支持几种类型的选择器:类选择器、伪类选择器和 ID 选择器等等。让我们简单地看一下这些选择器类型。

使用类选择器

Node类定义了一个styleClass变量,它是一个ObservableList<String>。它的目的是维护一个节点的 JavaFX 风格类名列表。注意,JavaFX 类名和节点的样式类名是两回事。节点的 JavaFX 类名是一个 Java 类名,例如javafx.scene.layout.VBox,或者简称为VBox,用于创建该类的对象。节点的样式类名是 CSS 样式中使用的字符串名称。

您可以为一个节点分配多个 CSS 类名。下面的代码片段将两个样式类名"hbox""myhbox"分配给一个HBox:

HBox hb = new HBox();
hb.getStyleClass().addAll("hbox", "myhbox");

样式类选择器将关联的样式应用于所有节点,这些节点具有与选择器名称相同的样式类名称。样式类选择器以句点开头,后跟样式类名。请注意,节点的样式类名不以句点开头。

清单 8-7 显示了一个样式表的内容。它有两种风格。两种样式都使用样式类选择器,因为它们都以句点开头。第一个样式类选择器是“hbox”,这意味着它将用一个名为hbox的样式类匹配所有节点。第二种样式使用样式类名作为button。将样式表保存在CLASSPATH中名为resources\css\styleclass.css的文件中。

.hbox {
        -fx-border-color: blue;
        -fx-border-width: 2px;
        -fx-border-radius: 5px;
        -fx-border-insets: 5px;
        -fx-padding: 10px;
        -fx-spacing: 5px;
        -fx-background-color: lightgray;
        -fx-background-insets: 5px;
}

.button {
        -fx-text-fill: blue;
}

Listing 8-7A Style Sheet with Two Style Class Selectors Named hbox and button

清单 8-8 有完整的程序来演示样式类选择器hboxbutton的使用。产生的屏幕如图 8-7 所示。

img/336502_2_En_8_Fig7_HTML.jpg

图 8-7

使用样式表中的边框、填充、间距和背景色

// StyleClassTest.java
package com.jdojo.style;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;

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

        @Override
        public void start(Stage stage) {
                Label nameLbl = new Label("Name:");
                TextField nameTf = new TextField("");
                Button closeBtn  = new Button("Close");
                closeBtn.setOnAction(e -> Platform.exit());

                HBox root = new HBox();
                root.getChildren().addAll(nameLbl, nameTf, closeBtn);

                // Set the styleClass for the HBox to "hbox"
                root.getStyleClass().add("hbox");

                Scene scene = new Scene(root);
                scene.getStylesheets().add(
                         "resources/css/styleclass.css");

                stage.setScene(scene);
                stage.setTitle("Using Style Class Selectors");
                stage.show();
        }
}

Listing 8-8Using Style Class Selectors in Code

注意,您已经将HBox(在代码中命名为 root)的样式类名设置为"hbox",这将使用类选择器hbox将 CSS 属性从样式应用到HBoxClose按钮的文本颜色是蓝色的,因为样式类选择器按钮有第二种样式。您没有将Close按钮的样式类名称设置为“button”。Button类将一个名为"button"的样式类添加到它的所有实例中。这就是Close按钮被button样式类别选择器选中的原因。

JavaFX 中大多数常用的控件都有一个默认的样式类名。如果需要,可以添加更多的样式类名。默认的样式类名是由 JavaFX 类名构造的。JavaFX 类名被转换为小写,并在两个单词中间插入一个连字符。如果 JavaFX 类名只由一个单词组成,那么相应的默认样式类名是通过将其转换成小写字母来创建的。例如,默认的样式类名称是ButtonbuttonLabellabelHyperlinkhyperlinkTextFieldtext-fieldTextAreatext-areacheck-boxCheckBox

例如,RegionPaneHBoxVBox等 JavaFX 容器类没有默认的样式类名。如果您想使用样式类选择器来设置它们的样式,您需要向它们添加一个样式类名。这就是为什么您必须在清单 8-8 中使用的HBox中添加一个样式类名来使用样式类选择器。

Tip

JavaFX 中的样式类名区分大小写。

有时,您可能需要知道节点的默认样式类名,以便在样式表中使用它。有三种方法可以确定 JavaFX 节点的默认样式类名:

  • 猜测它使用描述的规则从 JavaFX 类名形成默认的样式类名。

  • 使用在线 JavaFX CSS 参考指南查找名称。

  • 写一小段代码。

下面的代码片段显示了如何打印Button类的默认样式类名。更改 JavaFX 节点类的名称,例如,从Button更改为TextField,以打印其他类型节点的默认样式类名称:

Button btn = new Button();
ObservableList<String> list = btn.getStyleClass();

if (list.isEmpty()) {
        System.out.println("No default style class name");
} else {
        for(String styleClassName : list) {
                System.out.println(styleClassName);
        }
}
button

节点的类选择器

场景的root节点被分配一个名为"root"的样式类。您可以对由其他节点继承的 CSS 属性使用root样式类选择器。root节点是场景图中所有节点的父节点。最好将 CSS 属性存储在root节点中,因为可以从场景图中的任何节点查找它们。

清单 8-9 显示了保存在文件resources\css\rootclass.css中的样式表的内容。带有root类选择器的样式声明了两个属性:-fx-cursor-my-button-color。所有节点都继承了-fx-cursor属性。如果这个样式表被附加到一个场景,所有的节点都会有一个HAND光标,除非它们覆盖了它。-my-button-color属性是查找属性,在第二种样式中查找,设置按钮的文本颜色。

.root {
        -fx-cursor: hand;
        -my-button-color: blue;
}

.button {
        -fx-text-fill: -my-button-color;
}

Listing 8-9The Content of the Style Sheet with Root As a Style Class Selector

运行清单 8-10 中的程序,看看这些变化的效果。请注意,当您在场景中的任何地方移动鼠标时,除了在名称文本字段上,您会得到一个HAND光标。这是因为TextField类覆盖了-fx-cursor CSS 属性,将其设置为TEXT光标。

// RootClassTest.java
package com.jdojo.style;

import com.jdojo.util.ResourceUtil;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;

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

        @Override

        public void start(Stage stage) {
                Label nameLbl = new Label("Name:");
                TextField nameTf = new TextField("");
                Button closeBtn = new Button("Close");

                HBox root = new HBox();
                root.getChildren().addAll(nameLbl, nameTf, closeBtn);

                Scene scene = new Scene(root);
                /* The root variable is assigned a default style
                        class name "root" */

                     var url =
                         ResourceUtil.getResourceURLStr("css/rootclass.css");
                     scene.getStylesheets().add(url);

                stage.setScene(scene);
                stage.setTitle("Using the root Style Class Selector");
                stage.show();
        }
}

Listing 8-10Using the Root Style Class Selector

使用 ID 选择器

Node类有一个StringProperty类型的id属性,可以用来为场景图中的每个节点分配一个唯一的id。维护场景图中id的唯一性是开发者的责任。为一个节点设置重复的id不是错误。

不要在代码中直接使用节点的id属性,除非您正在设置它。它主要用于使用 ID 选择器来设计节点的样式。下面的代码片段将Buttonid属性设置为"closeBtn":

Button b1 = new Button("Close");
b1.setId("closeBtn");

样式表中的 ID 选择器以井号(#)开头。请注意,为节点设置的 ID 值不包括#符号。清单 8-11 显示了一个样式表的内容,它包含两个样式,一个带有类选择器".button",一个带有 ID 选择器"#closeButton"。将清单 8-11 的内容保存在CLASSPATH中名为resources\css\idselector.css的文件中。图 8-8 显示了程序运行后的结果。

img/336502_2_En_8_Fig8_HTML.png

图 8-8

使用类别和 ID 选择器的按钮

.button {
        -fx-text-fill: blue;
}

#closeButton {
        -fx-text-fill: red;
}

Listing 8-11A Style Sheet That Uses a Class Selector and an ID Selector

清单 8-12 展示了使用清单 8-11 中样式表的程序。该程序创建了三个按钮。它将按钮的 ID 设置为"closeButton"。其他两个按钮没有 ID。当程序运行时,Close按钮的文本是红色的,而另外两个按钮是蓝色的。

// IDSelectorTest.java
package com.jdojo.style;

import com.jdojo.util.ResourceUtil;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;

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

      @Override
      public void start(Stage stage) {
            Button openBtn = new Button("Open");
            Button saveBtn = new Button("Save");

            Button closeBtn = new Button("Close");
            closeBtn.setId("closeButton");

            HBox root = new HBox();
            root.getChildren().addAll(openBtn, saveBtn, closeBtn);

            Scene scene = new Scene(root);
            var url = ResourceUtil.getResourceURLStr("css/idselector.css");
            scene.getStylesheets().add(url);

            stage.setScene(scene);
            stage.setTitle("Using ID selectors");
            stage.show();
      }
}

Listing 8-12Using an ID Selector in a Style Sheet

你注意到Close按钮的样式有冲突吗?JavaFX 中的所有按钮都被赋予一个名为button的默认样式类,Close按钮也是如此。Close按钮也有一个与 ID 样式选择器相匹配的 ID。因此,样式表中的两个选择器都匹配Close按钮。在有多个选择器匹配一个节点的情况下,JavaFX 使用选择器的特异性来决定使用哪个选择器。在使用类选择器和 ID 选择器的情况下,ID 选择器具有更高的特异性。这就是 ID 选择器匹配Close按钮,而不是类别选择器的原因。

Tip

CSS 使用复杂的规则来计算选择器的特异性。更多详情请参考 www.w3.org/TR/CSS21/cascade.html#specificity

组合 ID 和类选择器

选择器可以使用样式类和 ID 的组合。在这种情况下,选择器匹配具有指定样式类和 ID 的所有节点。考虑以下样式:

#closeButton.button {
        -fx-text-fill: red;
}

选择器#closeButton.button匹配所有具有closeButton ID 和button样式类的节点。您也可以颠倒顺序:

.button#closeButton {
        -fx-text-fill: red;
}

现在,它匹配所有具有button样式类和closeButton ID 的节点。

通用选择器

星号(*)用作通用选择器,它匹配任何节点。通用选择器的特异性最低。以下样式使用通用选择器将所有节点的文本填充属性设置为蓝色:

* {
        -fx-text-fill: blue;
}

当通用选择器没有自己出现时,可以忽略。比如选择器*.button.button是一样的。

将多个选择器分组

如果相同的 CSS 属性应用于多个选择器,您有两种选择:

  • 通过复制属性声明,可以使用多种样式。

  • 您可以将所有选择器组合成一种样式,用逗号分隔选择器。

假设您想将buttonlabel类的文本填充颜色设置为蓝色。下面的代码使用两种带有重复属性声明的样式:

.button {
        -fx-text-fill: blue;
}

.label {
        -fx-text-fill: blue;
}

这两种样式可以合并为一种样式,如下所示:

.button, .label {
        -fx-text-fill: blue;
}

后代选择器

后代选择器用于匹配作为场景图中另一个节点的后代的节点。后代选择器由两个或更多由空格分隔的选择器组成。以下样式使用后代选择器:

.hbox .button {
        -fx-text-fill: blue;
}

它将选择所有具有button样式类并且是具有hbox样式类的节点的后代的节点。术语后代在这个上下文中表示任何级别的孩子(直系或非直系)。

当您想要对 JavaFX 控件的某些部分进行样式化时,后代选择器就派上了用场。JavaFX 中的许多控件由子节点组成,这些子节点是 JavaFX 节点。在 JavaFX CSS 参考指南中,这些子节点被列为子结构。例如,CheckBox由样式类名为textLabeledText(不是公共 API 的一部分)和样式类名为boxStackPane组成。box包含另一个样式类名为markStackPane。您可以为CheckBox类的子结构使用这些信息来设计子部分的样式。以下样式使用后代选择器将所有CheckBox实例的文本颜色设置为蓝色,并将框设置为虚线边框:

.check-box .text {
        -fx-fill: blue;
}

.check-box .box {
        -fx-border-color: black;
        -fx-border-width: 1px;
        -fx-border-style: dotted;
}

子选择器

子选择器匹配子节点。它由两个或多个选择器组成,由大于号(>)分隔。以下样式匹配具有button样式类的所有节点,这些节点是具有hbox样式类的节点的子节点:

.hbox > .button {
        -fx-text-fill: blue;
}

Tip

CSS 支持其他类型的选择器,例如,兄弟选择器和属性选择器。JavaFX CSS 还不可靠地支持它们。

基于状态的选择器

基于状态的选择器也被称为伪类选择器。伪类选择器根据节点的当前状态匹配节点,例如,匹配具有焦点的节点或匹配只读的文本输入控件。伪类前面有一个冒号,并附加到现有的选择器中。例如,.button:focused是一个伪类选择器,它匹配一个具有button样式类名的节点,该节点也具有焦点;#openBtn:hover是另一个伪类选择器,当鼠标悬停在节点上时,它匹配 ID 为#openBtn的节点。清单 8-13 展示了具有伪类选择器的样式表的内容。当鼠标悬停在节点上时,它将文本颜色更改为红色。当您将此样式表添加到场景中时,当鼠标悬停在所有按钮上时,它们的文本颜色将变为红色。

.button:hover {
        -fx-text-fill: red;
}

Listing 8-13A Style Sheet with a Pseudo-class Selector

JavaFX CSS 不支持 CSS 支持的:first-child:lang伪类。JavaFX 不支持伪元素,这些元素允许您对节点的内容进行样式化(例如,TextArea中的第一行)。表 8-1 包含 JavaFX CSS 支持的伪类的部分列表。请参考在线 JavaFX CSS 参考指南获取 JavaFX CSS 支持的伪类的完整列表。

表 8-1

JavaFX CSS 支持的一些伪类

|

伪类

|

适用于

|

描述

disabled Node 它适用于节点被禁用的情况。
focused Node 当节点获得焦点时适用。
hover Node 当鼠标悬停在节点上时应用。
pressed Node 当鼠标按钮在节点上单击时应用。
show-mnemonic Node 它适用于应该显示助记符的情况。
cancel Button 如果事件未被消费,当Button将接收到VK_ESC时,它适用。
default Button 如果事件未被消费,当Button将接收到VK_ENTER时,它适用。
empty Cell Cell为空时适用。
filled Cell Cell不为空时适用。
selected Cell, CheckBox 它适用于选择节点的情况。
determinate CheckBox CheckBox处于确定状态时适用。
indeterminate CheckBox CheckBox处于不确定状态时适用。
visited Hyperlink Hyperlink已被访问时适用。
horizontal ListView 它适用于节点水*的情况。
vertical ListView 它适用于节点垂直的情况。

使用 JavaFX 类名作为选择器

允许使用 JavaFX 类名作为样式中的类型选择器,但不建议这样做。考虑样式表的以下内容:

HBox {
        -fx-border-color: blue;
        -fx-border-width: 2px;
        -fx-border-insets: 10px;
        -fx-padding: 10px;
}

Button {
        -fx-text-fill: blue;
}

请注意,类型选择器与类选择器的不同之处在于前者不以句点开头。类选择器是没有任何修改的节点的 JavaFX 类名(HBOXHBox不一样)。如果将包含上述内容的样式表附加到场景中,所有的HBox实例都将有一个边框,所有的Button实例都将有蓝色文本。

不建议使用 JavaFX 类名作为类型选择器,因为当您创建 JavaFX 类的子类时,类名可能会有所不同。如果您依赖于样式表中的类名,新类将不会选择您的样式。

在场景图中查找节点

可以使用选择器在场景图中查找节点。SceneNode类有一个lookup(String selector)方法,返回用指定的selector找到的第一个节点的引用。如果没有找到节点,则返回null。两个类中的方法工作方式略有不同。Scene类中的方法搜索整个场景图。Node类中的方法搜索调用它的节点及其子节点。Node类还有一个lookupAll(String selector)方法,该方法返回由指定的selector匹配的所有Node的一个Set,包括调用该方法的节点及其子节点。

下面的代码片段显示了如何使用 ID 选择器来使用查找方法。但是,在这些方法中,您并不局限于只使用 ID 选择器。您可以使用 JavaFX 中所有有效的选择器:

Button b1 = new Button("Close");
b1.setId("closeBtn");
VBox root = new VBox();
root.setId("myvbox");
root.getChildren().addAll(b1);
Scene scene = new Scene(root, 200, 300);
...
Node n1 = scene.lookup("#closeBtn");       // n1 is the reference of b1
Node n2 = root.lookup("#closeBtn");        // n2 is the reference of b1
Node n3 = b1.lookup("#closeBtn");          // n3 is the reference of b1
Node n4 = root.lookup("#myvbox");          // n4 is the reference of root
Node n5 = b1.lookup("#myvbox");            // n5 is null
Set<Node> s = root.lookupAll("#closeBtn"); // s contains the reference of b1

摘要

CSS 是一种用来描述 GUI 应用程序中 UI 元素表示的语言。它主要用在网页中,用于设计 HTML 元素的样式,并将表示从内容和行为中分离出来。在典型的 web 页面中,内容和表示分别使用 HTML 和 CSS 来定义。

JavaFX 允许您使用 CSS 定义 JavaFX 应用程序的外观。您可以使用 JavaFX 类库或 FXML 来定义 UI 元素,并使用 CSS 来定义它们的外观。

CSS 规则也称为样式。CSS 规则的集合被称为样式表。皮肤是特定于应用程序的样式的集合,它们定义了应用程序的外观。换肤是动态改变应用程序(或皮肤)外观的过程。JavaFX 不提供特定的换肤机制。主题是操作系统的视觉特征,反映在所有应用程序的 UI 元素的外观中。JavaFX 不直接支持主题。

您可以向 JavaFX 应用程序添加多个样式表。样式表被添加到场景或父对象中。Scene 和 Parent 类维护一个链接到样式表的字符串 URL 的可观察列表。

JavaFX 8 到 17 使用名为 Modena 的默认样式表。在 JavaFX 8 之前,默认的样式表叫做 Caspian。在 JavaFX 8 中,使用 Application 类的静态方法setUserAgentStylesheet(String url),您仍然可以将 Caspian 样式表作为默认样式表。您可以使用 Application 类中定义的常量STYLESHEET_CASPIANSTYLESHEET_MODENA来引用里海和摩德纳样式表的 URL。

节点的可视属性通常来自多个来源。JavaFX 运行时使用以下优先级规则来设置节点的可视属性:内联样式(最高优先级)、父样式表、场景样式表、使用 JavaFX API 在代码中设置的值以及用户代理样式表(最低优先级)。

JavaFX 为 CSS 属性提供了两种类型的继承:CSS 属性类型和 CSS 属性值。在第一种类型的继承中,JavaFX 类中声明的所有 CSS 属性都被它的所有子类继承。在第二种类型的继承中,节点的 CSS 属性可以从其父节点继承其值。节点的父节点是场景图中节点的容器,而不是它的 JavaFX 超类。

样式表中的每个样式都有一个选择器,用于标识应用该样式的场景图中的节点。JavaFX CSS 支持几种类型的选择器:类选择器,并且大多数选择器的工作方式与它们在 web 浏览器中的工作方式相同。您可以使用场景和节点类的选择器和lookup(String selector)方法在场景图中查找节点。

下一章将讨论如何在 JavaFX 应用程序中处理事件。

九、事件处理

在本章中,您将学习:

  • 什么是事件

  • 什么是事件源、事件目标和事件类型

  • 关于事件处理机制

  • 如何使用事件过滤器和事件处理程序处理事件

  • 如何处理鼠标事件、按键事件和窗口事件

本章的例子在com.jdojo.event包中。为了让它们工作,您必须在module-info.java文件中添加相应的一行:

...
opens com.jdojo.event to javafx.graphics, javafx.base;
...

什么是事件?

通常,术语事件用于描述感兴趣的事件。在 GUI 应用程序中,事件是用户与应用程序交互的发生。单击鼠标和按键盘上的键是 JavaFX 应用程序中的事件示例。

JavaFX 中的事件由javafx.event.Event类或其任何子类的对象表示。JavaFX 中的每个事件都有三个属性:

  • 事件源

  • 事件目标

  • 事件类型

当应用程序中发生事件时,通常通过执行一段代码来执行一些处理。响应事件而执行的代码片段被称为事件处理器事件过滤器。我将很快澄清这两者之间的区别。现在,把两者都看作一段代码,我将把它们都称为事件处理程序。当您想要处理 UI 元素的事件时,您需要向 UI 元素添加事件处理程序,例如,WindowSceneNode。当 UI 元素检测到事件时,它会执行您的事件处理程序。

调用事件处理程序的 UI 元素是这些事件处理程序的事件源。当一个事件发生时,它会通过一连串的事件调度程序。事件的源是事件调度程序链中的当前元素。当事件通过事件调度程序链中的一个调度程序传递到另一个调度程序时,事件源会发生变化。

事件目标是事件的目的地。事件目标决定了事件在处理过程中行进的路线。假设鼠标点击发生在一个Circle节点上。在这种情况下,Circle节点是鼠标点击事件的事件目标。

事件类型描述发生的事件的类型。事件类型以分层的方式定义。每个事件类型都有一个名称和一个父类型。

JavaFX 中所有事件共有的三个属性由三个不同类的对象表示。特定事件定义了附加的事件属性;例如,表示鼠标事件的 event 类添加了描述鼠标光标位置和鼠标按钮状态等属性。表 9-1 列出了事件处理中涉及的类和接口。JavaFX 有一个事件交付机制,它定义了事件发生和处理的细节。我将在随后的章节中详细讨论所有这些。

表 9-1

事件处理中涉及的类

|

名字

|

类别/接口

|

描述

Event 班级 此类的一个实例表示一个事件。存在几个Event类的子类来表示特定类型的事件。
EventTarget 连接 此接口的一个实例代表一个事件目标。
EventType 班级 此类的一个实例代表一个事件类型,例如,按下鼠标、释放鼠标、移动鼠标。
EventHandler 连接 此接口的一个实例表示一个事件处理程序或一个事件过滤器。它的handle()方法在它注册的事件发生时被调用。

事件类层次结构

JavaFX 中表示事件的类通过类继承以分层的方式排列。图 9-1 显示了Event类的部分类图。Event类位于类层次结构的顶端,它继承了图中没有显示的java.util.EventObject类。

img/336502_2_En_9_Fig1_HTML.jpg

图 9-1

javafx.event.Event类的分部类层次结构

Event类的子类代表特定类型的事件。有时,Event类的一个子类被用来表示某种普通事件。例如,InputEvent类代表一个通用事件来指示一个用户输入事件,而KeyEventMouseEvent类分别代表特定的输入事件,比如来自键盘和鼠标的用户输入。WindowEvent类的对象代表一个窗口的事件,例如窗口的显示和隐藏。ActionEvent的一个对象用来表示几种事件,表示某种类型的动作,例如,触发一个按钮或一个菜单项。如果用户用鼠标点击按钮、按下某些键或在触摸屏上触摸它,可能会触发按钮。

Event类提供了所有事件通用的属性和方法。getSource()方法返回一个Object,它是事件的来源。Event类从EventObject类继承了这个方法。getTarget()方法返回EventTarget接口的一个实例,它是事件的目标。getEventType()方法返回一个EventType类的对象,它指示事件的类型。

Event类包含了consume()isConsumed()方法。如前所述,在事件调度链中,事件从一个元素传递到另一个元素。在一个Event对象上调用consume()方法表明事件已经被消费,不需要进一步处理。在调用了consume()方法之后,事件不会移动到事件处理链中的下一个元素。如果调用了consume()方法,则isConsumed()方法返回true;否则返回false

特定的Event子类定义了更多的属性和方法。例如,MouseEvent类定义了getX()getY()方法,它们返回鼠标光标相对于事件源的 x 和 y 坐标。当我在本章或后续章节中讨论这些方法时,我将在特定于事件的类中解释这些方法的细节。

事件目标

一个事件目标是一个可以响应事件的 UI 元素(不一定只是Node s)。从技术上讲,想要响应事件的 UI 元素必须实现EventTarget接口。也就是说,在 JavaFX 中,实现EventTarget接口使得 UI 元素有资格成为事件目标。

WindowSceneNode类实现了EventTarget接口。这意味着所有节点,包括窗口和场景,都可以响应事件。一些 UI 元素的类,例如TabTreeItemMenuItem,并不从Node类继承。它们仍然可以响应事件,因为它们实现了EventTarget接口。如果开发自定义 UI 元素,并且希望 UI 元素响应事件,则需要实现此接口。

事件目标的职责是建立一个事件调度器链,也称为事件路径。一个事件调度器是一个EventDispatcher接口的实例。链中的每个调度程序都可以通过处理和使用来影响事件。链中的事件调度程序还可以修改事件属性,用新事件替换该事件,或者链接事件路由。通常,事件目标路由由与容器子层次结构中的所有 UI 元素关联的调度程序组成。假设您将一个Circle节点放在一个HBox中,后者放在一个Scene中。将Scene加到一个Stage上。如果鼠标点击Circle,则Circle成为事件目标。Circle构建了一个事件调度器链,其路线从头到尾依次为StageSceneHBoxCircle

事件类型

EventType类的一个实例定义了一个事件类型。为什么需要一个单独的类来定义事件类型?每个事件单独的事件类,例如KeyEventMouseEvent,不足以定义事件类型吗?不能根据事件类来区分一个事件和另一个事件吗?EventType类用于对事件类中的事件进行进一步分类。例如,MouseEvent类只告诉我们用户使用了鼠标。它没有告诉我们鼠标使用的细节,例如,鼠标是否被按下、释放、拖动或点击。EventType类用于对事件的这些子事件类型进行分类。EventType类是一个泛型类,其类型参数定义如下:

EventType<T extends Event>

事件类型是分层的。它们是按实现而不是按类继承来分层的。每个事件类型都有一个名称和一个父类型。EventType类中的getName()getSuperType()方法返回事件类型的名称和父类型。常量Event.ANY,与常量EventType.ROOT相同,是 JavaFX 中所有事件的超类型。图 9-2 显示了在一些事件类中预定义的一些事件类型的部分列表。

img/336502_2_En_9_Fig2_HTML.jpg

图 9-2

某些事件类的预定义事件类型的部分列表

注意,图中的箭头并不表示类继承。它们表示依赖关系。例如,InputEvent.ANY事件类型依赖于Event.ANY事件类型,因为后者是前者的超类型。

具有子事件类型的事件类定义了一个ANY事件类型。例如,MouseEvent类定义了一个ANY事件类型,表示任何类型的鼠标事件,例如,鼠标释放、鼠标点击、鼠标移动。MOUSE_PRESSEDMOUSE_RELEASEDMouseEvent类中定义的其他事件类型。事件类中的ANY事件类型是同一事件类中所有其他事件类型的超类型。例如,MouseEvent.ANY事件类型是MOUSE_RELEASEDMOUSE_PRESSED鼠标事件的超类型。

事件处理机制

当事件发生时,作为事件处理的一部分,会执行几个步骤:

  • 事件目标选择

  • 事件路线构建

  • 事件路径遍历

事件目标选择

事件处理的第一步是选择事件目标。回想一下,事件目标是事件的目的节点。基于事件类型选择事件目标。

对于鼠标事件,事件目标是鼠标光标处的节点。鼠标光标处可以有多个节点。例如,您可以在矩形上放置一个圆。鼠标光标处最顶端的节点被选为事件目标。

关键事件的事件目标是具有焦点的节点。节点如何获得焦点取决于节点的类型。例如,TextField可以通过在其中单击鼠标或使用焦点遍历键(如 Windows 格式的 Tab 或 Shift + Tab)来获得焦点。默认情况下,CirclesRectangles等形状不会获得焦点。如果你想让它们接收按键事件,你可以通过调用Node类的requestFocus()方法给它们焦点。

JavaFX 支持支持触摸的设备上的触摸和手势事件。通过触摸触摸屏产生触摸事件。每个触摸动作都有一个称为触摸点的接触点。可以用多个手指触摸触摸屏,从而产生多个触摸点。触摸点的每种状态,例如按压、释放等,都会产生触摸事件。触摸点的位置决定了触摸事件的目标。例如,如果触摸事件的位置是圆内的点,则该圆成为触摸事件的目标。在触摸点处有多个节点的情况下,选择最上面的节点作为目标。

用户可以使用手势与 JavaFX 应用程序进行交互。通常,触摸屏和跟踪板上的手势由具有触摸动作的多个触摸点组成。手势事件的例子是旋转、滚动、滑动和缩放。旋转手势是通过绕着彼此旋转两个手指来执行的。通过在触摸屏上拖动手指来执行滚动手势。通过在触摸屏上向一个方向拖动一个手指(或多个手指)来执行滑动手势。执行缩放手势以通过将两个手指拖开或拉*来缩放节点。

手势事件的目标是根据手势的类型选择的。对于直接手势,例如在触摸屏上执行的手势,在手势开始时所有触摸点的中心点处的最顶端节点被选择作为事件目标。对于间接手势,例如在跟踪板上执行的手势,鼠标光标处最顶端的节点被选为事件目标。

事件路线构建

事件通过事件调度链中的事件调度程序传播。事件调度链是事件路由。事件的初始和默认路线由事件目标决定。默认事件路由由从阶段开始到事件目标节点的容器子路径组成。

假设你在一个HBox中放置了一个Circle和一个Rectangle,并且HBox是一个StageScene的根节点。当您点击Circle时,Circle成为事件目标。Circle构造默认的事件路径,它是从阶段开始到事件目标(Circle)的路径。

事实上,事件路由由与节点相关联的事件调度程序组成。然而,出于实际和理解的目的,您可以将事件路由视为包含节点的路径。通常,您不直接与事件调度程序打交道。

图 9-3 显示了鼠标点击事件的事件路径。事件路线上的节点以灰色背景填充显示。事件路线上的节点由实线连接。注意,当点击Circle时,作为场景图一部分的Rectangle不是事件路径的一部分。

img/336502_2_En_9_Fig3_HTML.png

图 9-3

为事件构造默认的事件路径

一个事件调度链(或事件路线)有一个和一个。在图 9-3 中,StageCircle分别是事件调度链的头和尾。随着事件处理的进展,可以修改初始事件路线。通常,但不是必须的,在事件遍历步骤中,事件通过其路由中的所有节点两次,如下一节所述。

事件路径遍历

事件路径遍历包括两个阶段:

  • 捕获阶段

  • 起泡阶段

一个事件在其路由中经过每个节点两次:一次在捕获阶段,一次在冒泡阶段。您可以为特定的事件类型向节点注册事件过滤器和事件处理程序。在捕获阶段和冒泡阶段,当事件通过节点时,分别执行注册到节点的事件过滤器和事件处理程序。事件过滤器和处理程序作为事件源在当前节点的引用中传递。随着事件从一个节点传播到另一个节点,事件源不断变化。然而,事件目标从事件路径遍历的开始到结束保持不变。

在路由遍历期间,节点可以使用事件过滤器或处理程序中的事件,从而完成事件的处理。消费一个事件只需调用事件对象上的consume()方法。当一个事件被消费时,事件处理被停止,即使路由中的一些节点根本没有被遍历。

事件捕获阶段

在捕获阶段,事件从其事件调度链的头部移动到尾部。图 9-4 显示了在我们的例子中的Circle在捕获阶段鼠标点击事件的移动。图中的向下箭头表示事件传播的方向。当事件通过一个节点时,为该节点注册的事件过滤器被执行。请注意,对于当前节点,事件捕获阶段只执行事件过滤器,而不执行事件处理程序。

img/336502_2_En_9_Fig4_HTML.png

图 9-4

事件捕获阶段

在图 9-4 中,StageSceneHBoxCircle的事件过滤器按顺序执行,假设没有事件过滤器消耗事件。

您可以为一个节点注册多个事件过滤器。如果节点使用了它的一个事件过滤器中的事件,那么在事件处理停止之前,它的其他尚未执行的事件过滤器将被执行。假设您在我们的示例中为Scene注册了五个事件过滤器,执行的第一个事件过滤器使用该事件。在这种情况下,Scene的其他四个事件过滤器仍将被执行。对Scene执行第五个事件过滤器后,事件处理将停止,事件不会传播到剩余的节点(HBoxCircle)。

在事件捕获阶段,您可以拦截针对节点子节点的事件(并提供通用响应)。例如,在我们的示例中,您可以将鼠标点击事件的事件过滤器添加到Stage中,以拦截其所有子节点的所有鼠标点击事件。您可以通过在父节点的事件过滤器中使用事件来阻止事件到达其目标。例如,如果您在过滤器中为Stage使用鼠标点击事件,那么该事件将不会到达它的目标,在我们的例子中是Circle

事件冒泡阶段

在冒泡阶段,事件从其事件调度链的尾部移动到头部。图 9-5 显示了Circle在冒泡阶段鼠标点击事件的行进。

img/336502_2_En_9_Fig5_HTML.png

图 9-5

事件冒泡阶段

图 9-5 中的向上箭头表示事件行进的方向。当事件通过一个节点时,执行该节点的注册事件处理程序。注意,事件冒泡阶段执行当前节点的事件处理程序,而事件捕获阶段执行事件过滤器。

在我们的例子中,CircleHBoxSceneStage的事件处理程序按顺序执行,假设没有事件过滤器消耗事件。请注意,事件冒泡阶段从事件的目标开始,向上行进到父子层次结构中的最高父级。

您可以为一个节点注册多个事件处理程序。如果节点使用了它的一个事件处理程序中的事件,那么在事件处理停止之前,它的其他尚未执行的事件处理程序将被执行。假设在我们的例子中,您已经为Circle注册了五个事件处理程序,执行的第一个事件处理程序使用该事件。在这种情况下,Circle的其他四个事件处理程序仍然会被执行。在执行了Circle的第五个事件处理程序后,事件处理将停止,事件不会传播到剩余的节点(HBoxSceneStage)。

通常,事件处理程序注册到目标节点,以提供对事件的特定响应。有时,事件处理程序安装在父节点上,为其所有子节点提供默认事件响应。如果事件目标决定为事件提供特定的响应,它可以通过添加事件处理程序和使用事件来实现,从而阻止事件在事件冒泡阶段到达父节点。

让我们看一个微不足道的例子。假设您想在用户单击窗口中的任意位置时向用户显示一个消息框。您可以向窗口注册一个事件处理程序来显示消息框。当用户在窗口的圆圈内单击时,您希望显示特定的消息。您可以向 circle 注册一个事件处理程序,以提供特定的消息并使用该事件。这将在单击圆圈时提供特定的事件响应,而对于其他节点,窗口提供默认的事件响应。

处理事件

处理事件意味着执行应用程序逻辑以响应事件的发生。应用程序逻辑包含在事件过滤器和处理程序中,它们是EventHandler接口的对象,如以下代码所示:

public interface EventHandler<T extends Event> extends EventListener
        void handle(T event);
}

EventHandler类是javafx.event包中的通用类。它扩展了java.util包中的EventListener标记接口。handle()方法接收事件对象的引用,例如KeyEventMouseEvent的引用等等。

事件过滤器和处理程序都是同一个EventHandler接口的对象。仅仅看着一个EventHandler对象是一个事件过滤器还是一个事件处理器,你是无法分辨的。事实上,您可以将同一个EventHandler对象同时注册为事件过滤器和处理程序。这两者之间的区别是在它们注册到节点时确定的。节点提供不同的方法来注册它们。在内部,节点知道一个EventHandler对象是注册为事件过滤器还是处理程序。它们之间的另一个区别是基于调用它们的事件遍历阶段。在事件捕获阶段,注册过滤器的handle()方法被调用,而注册处理程序的handle()方法在事件冒泡阶段被调用。

Tip

本质上,处理事件意味着为EventHandler对象编写应用程序逻辑,并将它们注册到节点,作为事件过滤器、处理程序或两者。

创建事件过滤器和处理程序

创建事件过滤器和处理程序就像创建实现EventHandler接口的类的对象一样简单。使用 lambda 表达式是创建事件过滤器和处理程序的最佳选择,如以下代码所示:

EventHandler<MouseEvent> aHandler = e -> /* Event handling code goes here */;

我在本书中使用 lambda 表达式来创建事件过滤器和处理程序。如果您不熟悉 lambda 表达式,我建议您至少学习一些基础知识,以便能够理解事件处理代码。

下面的代码片段创建了一个MouseEvent处理程序。它打印发生的鼠标事件的类型:

EventHandler<MouseEvent> mouseEventHandler =
        e -> System.out.println("Mouse event type: " + e.getEventType());

注册事件过滤器和处理程序

如果您希望某个节点处理特定类型的事件,您需要向该节点注册这些事件类型的事件过滤器和处理程序。当事件发生时,节点的已注册事件过滤器和处理程序的handle()方法按照前面章节中讨论的规则被调用。如果节点不再对处理事件感兴趣,您需要从节点中注销事件过滤器和处理程序。注册和取消注册事件筛选器和处理程序也分别称为添加和删除事件筛选器和处理程序。

JavaFX 提供了两种向节点注册和取消注册事件过滤器和处理程序的方法:

  • 使用addEventFilter()addEventHandler()removeEventFilter()removeEventHandler()方法

  • 使用onXXX便利属性

使用 addXXX( )removeXXX( ) 方法

您可以使用addEventFilter()addEventHandler()方法分别向节点注册事件过滤器和处理程序。这些方法在Node类、Scene类和Window类中定义。一些类(例如MenuItemTreeItem)可以是事件目标;然而,它们不是从Node类继承的。这些类只为事件处理程序注册提供了addEventHandler()方法,例如

  • <T extends Event> void addEventFilter(EventType<T> eventType, EventHandler<? super T> eventFilter)

  • <T extends Event> void addEventHandler(EventType<T> eventType, EventHandler<? super T> eventHandler)

这些方法有两个参数。第一个参数是事件类型,第二个是EventHandler接口的一个对象。

您可以使用下面的代码片段来处理Circle的鼠标点击事件:

import javafx.scene.shape.Circle;
import javafx.event.EventHandler;
import javafx.scene.input.MouseEvent;
...
Circle circle = new Circle (100, 100, 50);

// Create a MouseEvent filter
EventHandler<MouseEvent> mouseEventFilter =
         e -> System.out.println("Mouse event filter has been called.");

// Create a MouseEvent handler
EventHandler<MouseEvent> mouseEventHandler =
         e -> System.out.println("Mouse event handler has been called.");

// Register the MouseEvent filter and handler to the Circle
// for mouse-clicked events
circle.addEventFilter(MouseEvent.MOUSE_CLICKED, mouseEventFilter);
circle.addEventHandler(MouseEvent.MOUSE_CLICKED, mouseEventHandler);

这段代码创建两个EventHandler对象,在控制台上打印一条消息。在这个阶段,它们不是事件过滤器或处理程序。他们只是两个EventHandler物体。请注意,给引用变量命名并打印使用单词 filter 和 handler 的消息,不会对它们作为过滤器和处理程序的状态产生任何影响。最后两条语句将一个EventHandler对象注册为事件过滤器,将另一个注册为事件处理程序;两者都注册了鼠标单击事件。

允许将同一个EventHandler对象注册为事件过滤器和处理程序。下面的代码片段使用一个EventHandler对象作为Circle的过滤器和处理程序来处理鼠标点击事件:

// Create a MouseEvent EventHandler object
EventHandler<MouseEvent> handler = e ->
    System.out.println("Mouse event filter or handler has been called.");

// Register the same EventHandler object as the MouseEvent filter and handler
// to the Circle for mouse-clicked events
circle.addEventFilter(MouseEvent.MOUSE_CLICKED, handler);
circle.addEventHandler(MouseEvent.MOUSE_CLICKED, handler);

Tip

您可以使用addEventFilter()addEventHandler()方法为一个节点添加多个事件过滤器和处理程序。您需要为要添加的事件过滤器和处理程序的每个实例调用一次这些方法。

清单 9-1 有完整的程序来演示如何处理一个Circle对象的鼠标点击事件。它使用一个事件过滤器和一个事件处理器。运行程序并在圆圈内单击。单击圆圈时,首先调用事件过滤器,然后调用事件处理程序。从输出中可以明显看出这一点。每当您单击圆内的任何一点时,都会发生鼠标单击事件。如果在圆圈外单击,鼠标单击事件仍会发生;但是,您看不到任何输出,因为您没有在HBoxSceneStage上注册事件过滤器或处理程序。

// EventRegistration.java
package com.jdojo.event;

import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;

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

        @Override
        public void start(Stage stage) {
                Circle circle = new Circle (100, 100, 50);
                circle.setFill(Color.CORAL);

                // Create a MouseEvent filter
                EventHandler<MouseEvent> mouseEventFilter = e ->
                         System.out.println(
                             "Mouse event filter has been called.");

                // Create a MouseEvent handler
                EventHandler<MouseEvent> mouseEventHandler = e ->
                         System.out.println(
                             "Mouse event handler has been called.");

                // Register the MouseEvent filter and handler to
                     // the Circle for mouse-clicked events
                circle.addEventFilter(MouseEvent.MOUSE_CLICKED,
                         mouseEventFilter);
                circle.addEventHandler(MouseEvent.MOUSE_CLICKED,
                         mouseEventHandler);

                HBox root = new HBox();
                root.getChildren().add(circle);
                Scene scene = new Scene(root);
                stage.setScene(scene);
                stage.setTitle("Registering Event Filters and Handlers");
                stage.show();
                stage.sizeToScene();
        }

}
Mouse event filter has been called.
Mouse event handler has been called.
...

Listing 9-1Registering Event Filters and Handlers

要注销事件过滤器和事件处理程序,您需要分别调用removeEventFilter()removeEventHandler()方法:

  • <T extends Event> void removeEventFilter(EventType<T> eventType, EventHandler<? super T> eventFilter)

  • <T extends Event> void removeEventHandler(EventType<T> eventType, EventHandler<? super T> eventHandler)

下面的代码片段向一个Circle添加和移除一个事件过滤器,然后移除它们。注意,一旦从一个节点中删除了一个EventHandler,当事件发生时就不会调用它的handle()方法:

// Create a MouseEvent EventHandler object
EventHandler<MouseEvent> handler = e ->
    System.out.println("Mouse event filter or handler has been called.");

// Register the same EventHandler object as the MouseEvent filter and handler
// to the Circle for mouse-clicked events
circle.addEventFilter(MouseEvent.MOUSE_CLICKED, handler);
circle.addEventHandler(MouseEvent.MOUSE_CLICKED, handler);

...

// At a later stage, when you are no longer interested in handling the mouse
// clicked event for the Circle, unregister the event filter and handler
circle.removeEventFilter(MouseEvent.MOUSE_CLICKED, handler);
circle.removeEventHandler(MouseEvent.MOUSE_CLICKED, handler);

在 XXX 上使用便利属性

NodeSceneWindow类包含事件属性来存储一些选定事件类型的事件处理程序。属性名使用事件类型模式。它们被命名为onXXX。例如,onMouseClicked属性存储鼠标点击事件类型的事件处理程序;属性存储键类型事件的事件处理程序;等等。您可以使用这些属性的setOnXXX()方法来注册节点的事件处理程序。例如,使用setOnMouseClicked()方法为鼠标点击事件注册一个事件处理程序,使用setOnKeyTyped()方法为键入事件注册一个事件处理程序,等等。各种类中的setOnXXX()方法被认为是注册事件处理程序的便利方法。

您需要记住关于onXXX便利属性的一些要点:

  • 它们只支持事件处理程序的注册,不支持事件过滤器。如果您需要注册事件过滤器,请使用addEventFilter()方法。

  • 他们只支持为一个节点注册一个事件处理程序。可以使用addEventHandler()方法为一个节点注册多个事件处理程序。

  • 这些属性只存在于节点类型的常用事件中。例如,onMouseClicked属性存在于NodeScene类中,但不存在于Window类中;onShowing属性存在于Window类中,但不存在于NodeScene类中。

清单 9-2 中的程序与清单 9-1 中的程序工作相同。这一次,您已经使用了Node类的onMouseClicked属性为这个圆注册了鼠标点击事件处理程序。注意,要注册事件过滤器,您必须像以前一样使用addEventFilter()方法。运行程序并在圆圈内单击。您将得到与运行清单 9-1 中的代码相同的输出。

// EventHandlerProperties.java
package com.jdojo.event;

import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;

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

        @Override
        public void start(Stage stage) {
                Circle circle = new Circle (100, 100, 50);
                circle.setFill(Color.CORAL);

                HBox root = new HBox();
                root.getChildren().add(circle);
                Scene scene = new Scene(root);
                stage.setScene(scene);
                stage.setTitle(

                         "Using convenience event handler properties");
                stage.show();
                stage.sizeToScene();

                // Create a MouseEvent filter
                EventHandler<MouseEvent> eventFilter = e ->
                         System.out.println(
                             "Mouse event filter has been called.");

                // Create a MouseEvent handler
                EventHandler<MouseEvent> eventHandler = e ->
                         System.out.println(
                             "Mouse event handler has been called.");

                // Register the filter using the addEventFilter() method
                circle.addEventFilter(MouseEvent.MOUSE_CLICKED,
                         eventFilter);

                // Register the handler using the setter method for
                // the onMouseCicked convenience event property
                circle.setOnMouseClicked(eventHandler);
        }
}

Listing 9-2Using the Convenience Event Handler Properties

便利事件属性没有提供单独的方法来注销事件处理程序。将属性设置为null会取消注册已经注册的事件处理程序:

// Register an event handler for the mouse-clicked event
circle.setOnMouseClicked(eventHandler);

...

// Later, when you are no longer interested in processing the mouse-clicked
// event, unregister it.
circle.setOnMouseClicked(null);

定义onXXX事件属性的类也定义了返回注册事件处理程序的引用的getOnXXX() getter 方法。如果没有设置事件处理程序,getter 方法返回null

事件过滤器和处理程序的执行顺序

相似和不同节点的事件过滤器和处理程序都有一些执行顺序规则:

  • 事件过滤器在事件处理程序之前被调用。事件过滤器按照父子顺序从最顶端的父对象到事件目标执行。事件处理程序以与事件过滤器相反的顺序执行。也就是说,事件处理程序的执行从事件目标开始,并按父子顺序向上移动。

  • 对于同一节点,特定事件类型的事件筛选器和处理程序在通用类型的事件筛选器和处理程序之前被调用。假设您已经为节点MouseEvent.ANYMouseEvent.MOUSE_CLICKED注册了事件处理程序。两种事件类型的事件处理程序都能够处理鼠标单击事件。当鼠标点击节点时,MouseEvent.MOUSE_CLICKED事件类型的事件处理程序在MouseEvent.ANY事件类型的事件处理程序之前被调用。请注意,鼠标按下事件和鼠标释放事件发生在鼠标单击事件发生之前。在我们的例子中,这些事件将由MouseEvent.ANY事件类型的事件处理程序来处理。

  • 没有指定节点的相同事件类型的事件过滤器和处理程序的执行顺序。这条规则有一个例外。使用addEventHandler()方法注册到节点的事件处理程序在使用setOnXXX()方便方法注册的事件处理程序之前执行。

清单 9-3 展示了不同节点的事件过滤器和处理程序的执行顺序。程序给一个HBox增加一个Circle和一个RectangleHBox被添加到Scene中。为鼠标点击事件的StageSceneHBoxCircle添加事件过滤器和事件处理程序。运行程序,点击圆圈内的任意位置。输出显示了过滤器和处理程序的调用顺序。输出包含事件阶段、类型、目标、源和位置。请注意,当事件从一个节点传播到另一个节点时,事件源会发生变化。该位置相对于事件源。因为每个节点都使用自己的局部坐标系,所以鼠标单击的同一点相对于不同的节点具有不同的(x,y)坐标值。

// CaptureBubblingOrder.java
package com.jdojo.event;

import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import static javafx.scene.input.MouseEvent.MOUSE_CLICKED;

Listing 9-3Execution Order for Event Filters and Handlers

如果单击矩形,您会注意到输出显示了事件通过其父级的相同路径,就像它通过圆形一样。事件仍然通过矩形,这是事件目标。但是,您看不到任何输出,因为您没有为矩形注册任何事件过滤器或处理程序来输出任何消息。您可以点按圆形和矩形外的任何点,以查看事件目标和事件路径。

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

        @Override
        public void start(Stage stage) {
                Circle circle = new Circle (50, 50, 50);
                circle.setFill(Color.CORAL);

                Rectangle rect = new Rectangle(100, 100);
                rect.setFill(Color.TAN);

                HBox root = new HBox();
                root.setPadding(new Insets(20));
                root.setSpacing(20);
                root.getChildren().addAll(circle, rect);

                Scene scene = new Scene(root);

                // Create two EventHandlders
                EventHandler<MouseEvent> filter = e ->
                         handleEvent("Capture", e);
                EventHandler<MouseEvent> handler = e ->
                         handleEvent("Bubbling", e);

                // Register filters

                stage.addEventFilter(MOUSE_CLICKED, filter);
                scene.addEventFilter(MOUSE_CLICKED, filter);
                root.addEventFilter(MOUSE_CLICKED, filter);
                circle.addEventFilter(MOUSE_CLICKED, filter);

                // Register handlers
                stage.addEventHandler(MOUSE_CLICKED, handler);
                scene.addEventHandler(MOUSE_CLICKED, handler);
                root.addEventHandler(MOUSE_CLICKED, handler);
                circle.addEventHandler(MOUSE_CLICKED, handler);

                stage.setScene(scene);
                stage.setTitle(
                         "Event Capture and Bubbling Execution Order");
                stage.show();
        }

        public void handleEvent(String phase, MouseEvent e) {
                String type = e.getEventType().getName();
                String source = e.getSource().getClass().getSimpleName();
                String target = e.getTarget().getClass().getSimpleName();

                // Get coordinates of the mouse cursor relative to the
                // event source
                double x = e.getX();
                double y = e.getY();

                System.out.println(phase + ": Type=" + type +
                    ", Target=" + target +
                    ", Source=" +  source +
                    ", location(" + x + ", " + y + ")");
        }
}

清单 9-4 展示了一个节点的事件处理程序的执行顺序。它显示一个圆。它为循环注册了三个事件处理程序:

  • 一个用于MouseEvent.ANY事件类型

  • 一个用于使用addEventHandler()方法的MouseEvent.MOUSE_CLICKED事件类型

  • 一个用于使用setOnMouseClicked()方法的MouseEvent.MOUSE_CLICKED事件类型

运行程序并在圆圈内单击。输出显示了调用三个事件处理程序的顺序。该顺序将类似于本节开始时的讨论中提出的顺序。

// HandlersOrder.java
package com.jdojo.event;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;

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

        @Override
        public void start(Stage stage) {
                Circle circle = new Circle(50, 50, 50);
                circle.setFill(Color.CORAL);

                HBox root = new HBox();
                root.getChildren().addAll(circle);
                Scene scene = new Scene(root);

                /* Register three handlers for the circle that can handle

                          mouse-clicked events */
                // This will be called last
                circle.addEventHandler(MouseEvent.ANY, e ->
                         handleAnyMouseEvent(e));

                // This will be called first
                circle.addEventHandler(MouseEvent.MOUSE_CLICKED, e ->
                         handleMouseClicked("addEventHandler()", e));

                // This will be called second
                circle.setOnMouseClicked(e ->
                         handleMouseClicked("setOnMouseClicked()", e));

                stage.setScene(scene);
                stage.setTitle(
                         "Execution Order of Event Handlers of a Node");
                stage.show();
        }

        public void handleMouseClicked(String registrationMethod,
                         MouseEvent e) {
                System.out.println(registrationMethod +
                    ": MOUSE_CLICKED handler detected a mouse click.");
        }

        public void handleAnyMouseEvent(MouseEvent e) {
                // Print a message only for mouse-clicked events,
                // ignoring other mouse events such as mouse-pressed,
                // mouse-released, etc.
                if (e.getEventType() == MouseEvent.MOUSE_CLICKED) {

                    System.out.println(
                               "MouseEvent.ANY handler detected a mouse click.");
                }
        }
}
addEventHandler(): MOUSE_CLICKED handler detected a mouse click.
setOnMouseClicked(): MOUSE_CLICKED handler detected a mouse click.
MouseEvent.ANY handler detected a mouse click.

Listing 9-4Order of Execution of Event Handlers for a Node

消费事件

通过调用事件的consume()方法来消耗事件。事件类包含方法,它由所有事件类继承。通常,在事件过滤器和处理程序的handle()方法中调用consume()方法。

使用事件向事件调度程序表明事件处理已完成,并且事件不应在事件调度链中继续传播。如果事件在节点的事件过滤器中被使用,则该事件不会传播到任何子节点。如果事件在节点的事件处理程序中使用,则该事件不会传播到任何父节点。

调用使用节点的所有事件筛选器或处理程序,而不管哪个筛选器或处理程序使用该事件。假设您为一个节点注册了三个事件处理程序,首先调用的事件处理程序使用事件。在这种情况下,仍然调用节点的另外两个事件处理程序。

如果父节点不希望其子节点响应某个事件,它可以在其事件过滤器中使用该事件。如果父节点对事件处理程序中的事件提供默认响应,则子节点可以提供特定响应并使用该事件,从而取消父节点的默认响应。

通常,节点在提供默认响应后会消耗大多数输入事件。规则是调用节点的所有事件过滤器和处理程序,即使其中一个使用了事件。这使得开发人员可以为节点执行他们的事件过滤器和处理程序,即使节点使用事件。

清单 9-5 中的代码展示了如何使用一个事件。图 9-6 显示运行程序时的屏幕。

img/336502_2_En_9_Fig6_HTML.jpg

图 9-6

消费事件

// ConsumingEvents.java
package com.jdojo.event;

import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.CheckBox;
import javafx.scene.input.MouseEvent;
import static javafx.scene.input.MouseEvent.MOUSE_CLICKED;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;

Listing 9-5Consuming Events

程序将一个Circle、一个Rectangle和一个CheckBox添加到一个HBox中。将HBox作为根节点添加到场景中。向StageSceneHBoxCircle添加事件处理程序。注意,您有一个不同的事件处理程序用于Circle,只是为了保持程序逻辑简单。当复选框被选中时,圆的事件处理程序消耗鼠标点击事件,从而防止事件向上传播到HBoxSceneStage。如果未选中该复选框,圆上的鼠标点击事件将从Circle移动到HBoxSceneStage。运行该程序,并使用鼠标单击场景的不同区域来查看效果。请注意,HBoxSceneStage的鼠标单击事件处理程序会被执行,即使您单击了圆圈外的点,因为它们位于所单击节点的事件调度链中。

public class ConsumingEvents extends Application {
        private CheckBox consumeEventCbx =
              new CheckBox("Consume Mouse Click at Circle");

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

        @Override
        public void start(Stage stage) {
                Circle circle = new Circle (50, 50, 50);
                circle.setFill(Color.CORAL);

                Rectangle rect = new Rectangle(100, 100);
                rect.setFill(Color.TAN);

                HBox root = new HBox();
                root.setPadding(new Insets(20));
                root.setSpacing(20);
                root.getChildren().addAll(circle, rect, consumeEventCbx);

                Scene scene = new Scene(root);

                // Register mouse-clicked event handlers to all nodes

,
                // except the rectangle and checkbox
                EventHandler<MouseEvent> handler = e ->
                         handleEvent(e);
                EventHandler<MouseEvent> circleMeHandler = e ->
                         handleEventforCircle(e);

                stage.addEventHandler(MOUSE_CLICKED, handler);
                scene.addEventHandler(MOUSE_CLICKED, handler);
                root.addEventHandler(MOUSE_CLICKED, handler);
                circle.addEventHandler(MOUSE_CLICKED, circleMeHandler);

                stage.setScene(scene);
                stage.setTitle("Consuming Events");
                stage.show();
        }

        public void handleEvent(MouseEvent e) {
                print(e);
        }

        public void handleEventforCircle(MouseEvent e) {
                print(e);
                if (consumeEventCbx.isSelected()) {
                        e.consume();
                }
        }

        public void print(MouseEvent e) {
                String type = e.getEventType().getName();
                String source = e.getSource().getClass().getSimpleName();
                String target = e.getTarget().getClass().getSimpleName();

                // Get coordinates of the mouse cursor relative to the
                     // event source
                double x = e.getX();
                double y = e.getY();

                System.out.println("Type=" + type + ", Target=" + target

                    ", Source=" +  source +
                   ", location(" + x + ", " + y + ")");
        }
}

单击复选框不会执行HBoxSceneStage的鼠标点击事件处理程序,而单击矩形会执行。你能想出这种行为的原因吗?原因很简单。该复选框有一个默认的事件处理程序,它采取默认的操作并使用该事件,防止它沿事件调度链向上移动。矩形不使用事件,允许它沿事件调度链向上移动。

Tip

事件过滤器中的事件目标使用事件不会影响任何其他事件过滤器的执行。但是,它防止了事件冒泡阶段的发生。在最顶层节点的事件处理程序中使用事件对事件处理没有任何影响,最顶层节点是事件调度链的头。

处理输入事件

输入事件指示用户输入(或用户动作),例如点击鼠标、按键、触摸触摸屏等。JavaFX 支持多种类型的输入事件。图 9-7 显示了一些代表输入事件的类的类图。所有与输入事件相关的类都在javafx.scene.input包中。InputEvent类是所有输入事件类的超类。通常,节点在采取默认操作之前会执行用户注册的输入事件处理程序。如果用户事件处理程序使用事件,节点不会采取默认操作。假设您为一个TextField注册了键类型的事件处理程序,它使用该事件。当您键入一个字符时,TextField不会将其添加并显示为其内容。因此,使用节点的输入事件使您有机会禁用节点的默认行为。在接下来的部分中,我将讨论鼠标和按键输入事件。

img/336502_2_En_9_Fig7_HTML.jpg

图 9-7

某些输入事件的类层次结构

处理鼠标事件

MouseEvent类的一个对象代表一个鼠标事件。MouseEvent类定义了以下鼠标相关的事件类型常量。所有常量都是EventType<MouseEvent>类型。Node类包含大多数鼠标事件类型的便利的onXXX属性,可用于为节点添加一个特定鼠标事件类型的事件处理程序:

  • ANY:是所有鼠标事件类型的超类型。如果一个节点想要接收所有类型的鼠标事件,您应该为这种类型注册处理程序。InputEvent.ANY是这个事件类型的超类型。

  • MOUSE_PRESSED:按下鼠标按钮产生此事件。MouseEvent类的getButton()方法返回负责该事件的鼠标按钮。鼠标按钮由MouseButton枚举中定义的NONEPRIMARYMIDDLESECONDARY常量表示。

  • MOUSE_RELEASED:释放鼠标按钮会产生这个事件。该事件被传递到鼠标被按下的同一个节点。例如,您可以在圆上按下鼠标按钮,将鼠标拖到圆外,然后释放鼠标按钮。MOUSE_RELEASED事件将被传递给圆圈,而不是释放鼠标按钮的节点。

  • MOUSE_CLICKED:在节点上点击鼠标按钮时产生该事件。应该在同一个节点上按下并释放按钮,此事件才会发生。

  • MOUSE_MOVED:在没有按下任何鼠标键的情况下移动鼠标会产生这个事件。

  • MOUSE_ENTERED:鼠标进入一个节点时产生该事件。此事件不会发生事件捕获和冒泡阶段。也就是说,不调用该事件的事件目标的父节点的事件过滤器和处理程序。

  • MOUSE_ENTERED_TARGET:鼠标进入一个节点时产生该事件。它是MOUSE_ENTERED事件类型的变体。与MOUSE_ENTERED事件不同,事件捕获和冒泡阶段发生在这个事件中。

  • MOUSE_EXITED:当鼠标离开一个节点时产生该事件。此事件不会发生事件捕获和冒泡阶段,也就是说,它只被传递到目标节点。

  • MOUSE_EXITED_TARGET:当鼠标离开一个节点时产生该事件。它是MOUSE_EXITED事件类型的变体。与MOUSE_EXITED事件不同,事件捕获和冒泡阶段发生在这个事件中。

  • DRAG_DETECTED:当鼠标在一个节点上按下并拖动超过特定于*台的距离阈值时,会生成此事件。

  • MOUSE_DRAGGED:按下鼠标按钮移动鼠标会产生此事件。无论鼠标指针在拖动过程中的位置如何,该事件都被传递到按下鼠标按钮的同一个节点。

获取鼠标位置

MouseEvent类包含当鼠标事件发生时给你鼠标位置的方法。您可以获得相对于事件源节点、场景和屏幕的坐标系的鼠标位置。getX()getY()方法给出了鼠标相对于事件源节点的(x,y)坐标。getSceneX()getSceneY()方法给出了鼠标相对于添加节点的场景的(x,y)坐标。getScreenX()getScreenY()方法给出了鼠标相对于添加节点的屏幕的(x,y)坐标。

清单 9-6 包含了展示如何使用MouseEvent类中的方法来知道鼠标位置的程序。它向舞台添加了一个MOUSE_CLICKED事件处理程序,当鼠标在其区域内的任何地方被单击时,舞台都可以接收到通知。运行程序并单击舞台中的任意位置,如果在桌面上运行,则不包括其标题栏。每次单击鼠标都会打印一条消息,描述源、目标以及鼠标相对于源、场景和屏幕的位置。

// MouseLocation.java
package com.jdojo.event;

import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;

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

        @Override
        public void start(Stage stage) {
                Circle circle = new Circle (50, 50, 50);
                circle.setFill(Color.CORAL);

                Rectangle rect = new Rectangle(100, 100);
                rect.setFill(Color.TAN);

                HBox root = new HBox();
                root.setPadding(new Insets(20));
                root.setSpacing(20);
                root.getChildren().addAll(circle, rect);

                // Add a MOUSE_CLICKED event handler to the stage

                stage.addEventHandler(MouseEvent.MOUSE_CLICKED, e ->
                         handleMouseMove(e));

                Scene scene = new Scene(root);
                stage.setScene(scene);
                stage.setTitle("Mouse Location");
                stage.show();
        }

        public void handleMouseMove(MouseEvent e) {
                String source = e.getSource().getClass().getSimpleName();
                String target = e.getTarget().getClass().getSimpleName();

                // Mouse location relative to the event source
                double sourceX = e.getX();
                double sourceY = e.getY();

                // Mouse location relative to the scene
                double sceneX = e.getSceneX();
                double sceneY = e.getSceneY();

                // Mouse location relative to the screen
                double screenX = e.getScreenX();
                double screenY = e.getScreenY();

                System.out.println("Source=" +  source +
                   ", Target=" + target +
                   ", Location:" +
                   " source(" + sourceX + ", " + sourceY + ")" +
                   ", scene(" + sceneX + ", " + sceneY + ")" +
                   ", screen(" + screenX + ", " + screenY + ")");
        }
}

Listing 9-6Determining the Mouse Location During Mouse Events

表示鼠标按钮

通常,鼠标有三个按钮。你也会发现有些只有一两个按钮。一些*台提供了模拟丢失鼠标按钮的方法。javafx.scene.input包中的MouseButton枚举包含代表鼠标按钮的常量。表 9-2 包含了在MouseButton枚举中定义的常量列表。

表 9-2

MouseButton枚举的常量

|

鼠标按钮枚举常量

|

描述

NONE 它表示没有按钮。
PRIMARY 它代表主要按钮。通常,它是鼠标中的左键。
MIDDLE 它代表中间的按钮。
SECONDARY 它代表二级按钮。通常,它是鼠标中的右键。

鼠标主按键和第二按键的位置取决于鼠标配置。通常,对于惯用右手的用户,左按钮和右按钮分别被配置为主要按钮和辅助按钮。对于惯用左手的用户,按钮以相反的顺序配置。如果你有一个两键鼠标,你没有中间键。

鼠标按钮的状态

代表鼠标事件的MouseEvent对象包含事件发生时鼠标按钮的状态。MouseEvent类包含许多报告鼠标按钮状态的方法。表 9-3 包含了这些方法的列表及其描述。

表 9-3

MouseEvent类中与鼠标按钮状态相关的方法

|

方法

|

描述

MouseButton getButton() 它返回负责鼠标事件的鼠标按钮。
int getClickCount() 它返回与鼠标事件相关的鼠标点击次数。
boolean isPrimaryButtonDown() 如果主按钮当前被按下,则返回true。否则返回false
boolean isMiddleButtonDown() 如果当前按下了中间按钮,则返回true。否则返回false
boolean isSecondaryButtonDown() 如果次级按钮当前被按下,则返回true。否则返回false
boolean isPopupTrigger() 如果鼠标事件是*台的弹出菜单触发事件,则返回true。否则返回false
boolean isStillSincePress() 如果鼠标光标停留在一个小区域内,即系统提供的滞后区域,在最后一次鼠标按下事件和当前鼠标事件之间,它返回true

在许多情况下,getButton()方法可能会返回MouseButton.NONE,例如,当使用手指而不是鼠标在触摸屏上触发鼠标事件时,或者当鼠标事件(如鼠标移动事件)不是由鼠标按钮触发时。

理解getButton()方法和其他方法之间的区别很重要,例如isPrimaryButtonDown(),它返回按钮被按下的状态。getButton()方法返回触发事件的按钮。并非所有的鼠标事件都是由按钮触发的。例如,当鼠标移动时触发鼠标移动事件,而不是通过按下或释放按钮。如果一个按钮不负责鼠标事件,getButton()方法返回MouseButton.NONE。如果主按钮当前被按下,则isPrimaryButtonDown()方法返回true,不管它是否触发了事件。例如,当您按下主按钮时,鼠标按下事件发生。getButton()方法将返回MouseButton.PRIMARY,因为这是触发鼠标按下事件的按钮。isPrimaryButtonDown()方法返回true,因为当鼠标按下事件发生时这个按钮被按下。假设你一直按下主按钮,然后按下辅助按钮。另一个鼠标按下事件发生。然而,这一次,getButton()返回MouseButton.SECONDARY,并且isPrimaryButtonDown()isSecondaryButtonDown()方法都返回true,因为这两个按钮在第二次鼠标按下事件时都处于按下状态。

一个弹出菜单,也称为上下文上下文快捷菜单,是一个给用户一组在应用程序的特定上下文中可用的选项的菜单。例如,当您在 Windows *台上的浏览器中单击鼠标右键时,会显示一个弹出菜单。使用鼠标或键盘时,不同的*台触发弹出菜单事件的方式不同。在 Windows *台上,通常是单击鼠标右键或按 Shift + F10 键。

如果鼠标事件是*台的弹出菜单触发事件,isPopupTrigger()方法返回true。否则,它返回false。如果根据此方法的返回值执行操作,则需要在按下鼠标和释放鼠标的事件中使用它。通常,当这个方法返回true时,您让系统显示默认的弹出菜单。

Tip

JavaFX 提供了一个上下文菜单事件,它是一种特定类型的输入事件。它由javafx.scene.input包中的ContextMenuEvent类表示。如果你想处理上下文菜单事件,使用ContextMenuEvent

GUI 应用程序中的滞后现象

滞后是允许用户输入在时间或位置范围内的特征。接受用户输入的时间范围称为滞后时间。接受用户输入的区域被称为滞后区域。滞后时间和面积取决于系统。例如,现代 GUI 应用程序提供了通过双击鼠标按钮来调用的功能。两次点击之间存在时间差。如果时间间隔在系统的滞后时间内,则两次点击被认为是双击。否则,它们将被视为两次单独的单击。

通常,在鼠标单击事件期间,鼠标在按下和释放事件之间移动非常小的距离。有时,考虑鼠标点击时移动的距离是很重要的。如果从上次按下鼠标事件到当前事件,鼠标停留在系统提供的滞后区域,则isStillSincePress()方法返回true。当您想考虑鼠标拖动动作时,这个方法很重要。如果这个方法返回true,你可以忽略鼠标拖动,因为鼠标移动仍然在距离鼠标最后被按下的点的滞后距离之内。

修饰键的状态

修饰键用于更改其他键的正常行为。修饰键的一些例子是 Alt、Shift、Ctrl、Meta、Caps Lock 和 Num Lock。并非所有*台都支持所有的修饰键。元密钥存在于 Mac 上,不存在于 Windows 上。有些系统允许您模拟修饰键的功能,即使修饰键实际上并不存在,例如,您可以使用 Windows 上的Windows键作为Meta键。MouseEvent方法包含了当鼠标事件发生时报告某些修饰键的按下状态的方法。表 9-4 列出了MouseEvent类中与修饰键相关的方法。

表 9-4

MouseEvent类中与修饰键状态相关的方法

|

方法

|

描述

boolean isAltDown() 如果这个鼠标事件的 Alt 键被按下,它将返回true。否则返回false
boolean isControlDown() 如果这个鼠标事件的 Ctrl 键被按下,它将返回true。否则返回false
boolean isMetaDown() 如果这个鼠标事件的 Meta 键被按下,它将返回true。否则返回false
boolean isShiftDown() 如果这个鼠标事件的 Shift 键被按下,它将返回true。否则返回false
boolean isShortcutDown() 如果针对这个鼠标事件按下了特定于*台的快捷键,它将返回true。否则返回false。快捷修饰键是 Windows 上的 Ctrl 键和 Mac 上的 Meta 键。

在边界上拾取鼠标事件

Node类有一个pickOnBounds属性来控制为节点选择(或生成)鼠标事件的方式。一个节点可以有任何几何形状,而它的边界总是定义一个矩形区域。如果属性设置为 true,则当鼠标位于节点的边界上或边界内时,将为节点生成鼠标事件。如果该属性设置为默认值 false,则当鼠标位于其几何形状的外围或内部时,将为该节点生成鼠标事件。一些节点,比如Text节点,将pickOnBounds属性的默认值设置为 true。

图 9-8 显示了一个圆的几何形状和边界的周长。如果圆形的pickOnBounds属性为 false,并且鼠标位于几何形状的周长和边界之间的四个角中的一个,则不会为圆形生成鼠标事件。

img/336502_2_En_9_Fig8_HTML.png

图 9-8

圆的几何形状和边界之间的差异

清单 9-7 包含显示一个Circle节点的pickOnBounds属性的效果的程序。显示如图 9-9 所示的窗口。程序给一个Group增加了一个Rectangle和一个Circle。请注意,Rectangle被添加到Circle之前的Group中,以保持前者在 Z 顺序上低于后者。

img/336502_2_En_9_Fig9_HTML.jpg

图 9-9

演示一个Circle节点的pickOnBounds属性的效果

Rectangle使用红色作为填充颜色,而浅灰色作为Circle的填充颜色。红色区域是几何图形的周界和Circle边界之间的区域。

您有一个控制圆的pickOnBounds属性的复选框。如果选中该属性,则该属性设置为 true。否则,它被设置为 false。

当你点击灰色区域时,Circle总是选择鼠标点击事件。当您在复选框未选中的情况下单击红色区域时,Rectangle会拾取该事件。当您在复选框被选中的情况下单击红色区域时,Circle会拾取该事件。输出显示了谁选择了鼠标点击事件。

// PickOnBounds.java
package com.jdojo.event;

import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.geometry.Insets;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.CheckBox;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;

public class PickOnBounds extends Application {
        private CheckBox pickonBoundsCbx = new CheckBox("Pick on Bounds");
        Circle circle = new Circle(50, 50, 50, Color.LIGHTGRAY);

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

        @Override
        public void start(Stage stage) {
                Rectangle rect = new Rectangle(100, 100);
                rect.setFill(Color.RED);

                Group group = new Group();
                group.getChildren().addAll(rect, circle);

                HBox root = new HBox();
                root.setPadding(new Insets(20));
                root.setSpacing(20);
                root.getChildren().addAll(group, pickonBoundsCbx);

                // Add MOUSE_CLICKED event handlers to the circle and

                // rectangle
                circle.setOnMouseClicked(e -> handleMouseClicked(e));
                rect.setOnMouseClicked(e -> handleMouseClicked(e));

                // Add an Action handler to the checkbox
                pickonBoundsCbx.setOnAction(e -> handleActionEvent(e));

                Scene scene = new Scene(root);
                stage.setScene(scene);
                stage.setTitle("Pick on Bounds");
                stage.show();
        }

        public void handleMouseClicked(MouseEvent e) {
                String target = e.getTarget().getClass().getSimpleName();
                String type = e.getEventType().getName();
                System.out.println(type + " on " + target);
        }

        public void handleActionEvent(ActionEvent e) {
                if (pickonBoundsCbx.isSelected()) {
                        circle.setPickOnBounds(true);
                } else {
                        circle.setPickOnBounds(false);
                }
        }
}

Listing 9-7Testing the Effects of the pickOnBounds Property for a Circle Node

鼠标透明度

Node类有一个mouseTransparent属性来控制一个节点及其子节点是否接收鼠标事件。对比pickOnBoundsmouseTransparent属性:前者决定生成鼠标事件的节点区域,后者决定节点及其子节点是否生成鼠标事件,与前者的值无关。前者仅影响设置它的节点;后者影响设置它的节点及其所有子节点。

清单 9-8 中的代码展示了CirclemouseTransparent属性的效果。这是清单 9-7 中程序的变体。它显示了一个与图 9-9 所示非常相似的窗口。当复选框MouseTransparency被选中时,它将圆的mouseTransparent属性设置为真。当复选框未被选中时,它将圆的mouseTransparent属性设置为 false。

当复选框被选中时,单击灰色区域中的圆圈,所有鼠标单击事件都将被传递到矩形中。这是因为圆圈是鼠标透明的,它让鼠标事件通过。取消选中该复选框,所有灰色区域中的鼠标单击都将传递到该圆。注意,单击红色区域总是将事件传递给矩形,因为默认情况下圆形的pickOnBounds属性设置为 false。输出显示了接收鼠标单击事件的节点。

// MouseTransparency.java
package com.jdojo.event;

import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.geometry.Insets;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.CheckBox;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;

public class MouseTransparency extends Application {

        private CheckBox mouseTransparentCbx =
              new CheckBox("Mouse Transparent");
        Circle circle = new Circle(50, 50, 50, Color.LIGHTGRAY);

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

        @Override
        public void start(Stage stage) {
                Rectangle rect = new Rectangle(100, 100);
                rect.setFill(Color.RED);

                Group group = new Group();
                group.getChildren().addAll(rect, circle);

                HBox root = new HBox();
                root.setPadding(new Insets(20));
                root.setSpacing(20);
                root.getChildren().addAll(group, mouseTransparentCbx);

                // Add MOUSE_CLICKED event handlers to the circle
                // and rectangle
                circle.setOnMouseClicked(e -> handleMouseClicked(e));
                rect.setOnMouseClicked(e -> handleMouseClicked(e));

                // Add an Action handler to the checkbox
                mouseTransparentCbx.setOnAction(e ->
                         handleActionEvent(e));

                Scene scene = new Scene(root);
                stage.setScene(scene);
                stage.setTitle("Mouse Transparency");
                stage.show();
        }

        public void handleMouseClicked(MouseEvent e) {
                String target = e.getTarget().getClass().getSimpleName();
                String type = e.getEventType().getName();
                System.out.println(type + " on " + target);
        }

        public void handleActionEvent(ActionEvent e) {
                if (mouseTransparentCbx.isSelected()) {
                        circle.setMouseTransparent(true);
                } else {
                        circle.setMouseTransparent(false);
                }
        }
}

Listing 9-8Testing the Effects of the mouseTransparent Property for a Circle Node

合成鼠标事件

可以使用多种类型的设备生成鼠标事件,如鼠标、跟踪板或触摸屏。触摸屏上的一些动作产生鼠标事件,这些事件被认为是合成鼠标事件。如果事件是使用触摸屏合成的,MouseEvent类的isSynthesized()方法返回true。否则返回false

当手指在触摸屏上拖动时,它会生成滚动手势事件和鼠标拖动事件。可以在鼠标拖动事件处理程序中使用isSynthesized()方法的返回值来检测事件是通过在触摸屏上拖动手指还是通过拖动鼠标生成的。

处理鼠标进入和退出的事件

四种鼠标事件类型处理鼠标进入或退出节点时的事件:

  • MOUSE_ENTERED

  • MOUSE_EXITED

  • MOUSE_ENTERED_TARGET

  • MOUSE_EXITED_TARGET

鼠标进入事件和鼠标退出事件有两组事件类型。一套包含两种类型,称为MOUSE_ENTEREDMOUSE_EXITED,另一套包含MOUSE_ENTERED_TARGETMOUSE_EXITED_TARGET。两者都有共同点,比如什么时候触发。它们的传送机制不同。我将在本节中讨论所有这些问题。

当鼠标进入一个节点时,会产生一个MOUSE_ENTERED事件。当鼠标离开一个节点时,会生成一个MOUSE_EXITED事件。这些事件不会经历捕获和冒泡阶段。也就是说,它们被直接传递到目标节点,而不是它的任何父节点。

Tip

MOUSE_ENTEREDMOUSE_EXITED事件不参与捕获和冒泡阶段。然而,所有的事件过滤器处理程序都是按照事件处理规则为目标执行的。

清单 9-9 中的程序展示了鼠标进入和鼠标退出事件是如何传递的。程序显示如图 9-10 所示的窗口。它在一个HBox内显示一个灰色填充的圆。鼠标进入和退出事件的事件处理程序被添加到HBoxCircle中。运行程序,将鼠标移进移出圆圈。当鼠标进入窗口的白色区域时,它的MOUSE_ENTERED事件被传递给HBox。当您将鼠标移进和移出圆圈时,输出显示MOUSE_ENTEREDMOUSE_EXITED事件仅传递给Circle,而不是HBox。请注意,在输出中,这些事件的源和目标总是相同的,这证明这些事件不会发生捕获和冒泡阶段。当您将鼠标移进和移出圆圈并保持在白色区域时,不会触发HBoxMOUSE_EXITED事件,因为鼠标停留在HBox上。要在HBox上触发MOUSE_EXITED事件,您需要将鼠标移动到场景区域之外,例如,在窗口之外或在窗口的标题栏上。

img/336502_2_En_9_Fig10_HTML.jpg

图 9-10

演示鼠标进入和鼠标退出事件

// MouseEnteredExited.java
package com.jdojo.event;

import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.event.EventHandler;
import javafx.stage.Stage;
import static javafx.scene.input.MouseEvent.MOUSE_ENTERED;
import static javafx.scene.input.MouseEvent.MOUSE_EXITED;

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

        @Override
        public void start(Stage stage) {
                Circle circle = new Circle (50, 50, 50);
                circle.setFill(Color.GRAY);

                HBox root = new HBox();
                root.setPadding(new Insets(20));
                root.setSpacing(20);
                root.getChildren().addAll(circle);

                // Create a mouse event handler
                EventHandler<MouseEvent> handler = e -> handle(e);

                // Add mouse-entered and mouse-exited event handlers to

                // the HBox
                root.addEventHandler(MOUSE_ENTERED, handler);
                root.addEventHandler(MOUSE_EXITED, handler);

                // Add mouse-entered and mouse-exited event handlers to
                // the Circle
                circle.addEventHandler(MOUSE_ENTERED, handler);
                circle.addEventHandler(MOUSE_EXITED, handler);

                Scene scene = new Scene(root);
                stage.setScene(scene);
                stage.setTitle("Mouse Entered and Exited Events");
                stage.show();
        }

        public void handle(MouseEvent e) {
                String type = e.getEventType().getName();
                String source = e.getSource().getClass().getSimpleName();
                String target = e.getTarget().getClass().getSimpleName();
                System.out.println("Type=" + type +
                         ", Target=" + target + ", Source=" +  source);
        }
}
Type=MOUSE_ENTERED, Target=HBox, Source=HBox
Type=MOUSE_ENTERED, Target=Circle, Source=Circle
Type=MOUSE_EXITED, Target=Circle, Source=Circle
Type=MOUSE_ENTERED, Target=Circle, Source=Circle
Type=MOUSE_EXITED, Target=Circle, Source=Circle
Type=MOUSE_EXITED, Target=HBox, Source=HBox
...

Listing 9-9Testing Mouse-Entered and Mouse-Exited Events

MOUSE_ENTEREDMOUSE_EXITED事件类型提供了大多数情况下所需的功能。有时,您需要这些事件经历正常的捕获和冒泡阶段,以便父节点可以应用过滤器并提供默认响应。MOUSE_ENTERED_TARGETMOUSE_EXITED_TARGET事件类型提供了这些特性。他们参与事件捕获和冒泡阶段。

MOUSE_ENTEREDMOUSE_EXITED事件类型是MOUSE_ENTERED_TARGETMOUSE_EXITED_TARGET事件类型的子类型。对其子节点的鼠标输入事件感兴趣的节点应该为MOUSE_ENTERED_TARGET类型添加事件过滤器和处理程序。子节点可以添加MOUSE_ENTEREDMOUSE_ENTERED_TARGET,或者同时添加事件过滤器和处理程序。当鼠标进入子节点时,父节点接收到MOUSE_ENTERED_TARGET事件。在事件被传递到子节点(事件的目标节点)之前,事件类型被改变为MOUSE_ENTERED类型。因此,在同一个事件处理中,目标节点接收MOUSE_ENTERED事件,而其所有父节点接收MOUSE_ENTERED_TARGET事件。因为MOUSE_ENTERED事件类型是MOUSE_ENTERED_TARGET类型的子类型,所以目标上的任一类型的事件处理程序都可以处理这个事件。这同样适用于鼠标退出事件及其相应的事件类型。

有时,在父事件处理程序内部,有必要区分触发MOUSE_ENTERED_TARGET事件的节点。当鼠标进入父节点本身或它的任何子节点时,父节点接收此事件。您可以在事件过滤器和处理程序中使用Event类的getTarget()方法检查目标节点引用是否与父节点的引用相等,以了解事件是否是由父节点触发的。

清单 9-10 中的程序展示了如何使用鼠标进入目标和鼠标离开目标事件。它给一个HBox增加了一个Circle和一个CheckBoxHBox被添加到Scene中。它向HBox添加鼠标进入目标和鼠标退出目标事件过滤器,并向Circle添加事件处理程序。它还向Circle添加了鼠标进入和鼠标退出的事件处理程序。当复选框被选中时,事件被HBox消费,因此它们不会到达Circle。以下是运行该程序时的一些观察结果:

  • 不选中该复选框,当鼠标进入或离开Circle时,HBox接收到MOUSE_ENTERED_TARGETMOUSE_EXITED_TARGET事件。Circle接收MOUSE_ENTEREDMOUSE_EXITED事件。

  • 选中复选框后,HBox接收MOUSE_ENTERED_TARGETMOUSE_EXITED_TARGET事件并消费它们。Circle不接收任何事件。

  • 当鼠标进入或离开HBox,窗口的白色区域时,HBox接收到MOUSE_ENTEREDMOUSE_EXITED事件,因为HBox是事件的目标。

通过移动鼠标,选择和取消选择复选框来玩应用程序。查看输出,了解这些事件是如何处理的。

// MouseEnteredExitedTarget.java
package com.jdojo.event;

import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.CheckBox;
import javafx.scene.input.MouseEvent;
import static javafx.scene.input.MouseEvent.MOUSE_ENTERED;
import static javafx.scene.input.MouseEvent.MOUSE_EXITED;
import static javafx.scene.input.MouseEvent.MOUSE_ENTERED_TARGET;
import static javafx.scene.input.MouseEvent.MOUSE_EXITED_TARGET;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;

public class MouseEnteredExitedTarget extends Application {
        private CheckBox consumeCbx = new CheckBox("Consume Events");

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

        @Override
        public void start(Stage stage) {
                Circle circle = new Circle(50, 50, 50);
                circle.setFill(Color.GRAY);

                HBox root = new HBox();
                root.setPadding(new Insets(20));
                root.setSpacing(20);
                root.getChildren().addAll(circle, consumeCbx);

                // Create mouse event handlers
                EventHandler<MouseEvent> circleHandler = e ->
                         handleCircle(e);
                EventHandler<MouseEvent> circleTargetHandler = e ->
                         handleCircleTarget(e);
                EventHandler<MouseEvent> hBoxTargetHandler = e ->

                         handleHBoxTarget(e);

                // Add mouse-entered-target and mouse-exited-target event
                // handlers to HBox
                root.addEventFilter(MOUSE_ENTERED_TARGET,
                         hBoxTargetHandler);
                root.addEventFilter(MOUSE_EXITED_TARGET,
                         hBoxTargetHandler);

                // Add mouse-entered-target and mouse-exited-target event
                // handlers to the Circle
                circle.addEventHandler(MOUSE_ENTERED_TARGET,
                         circleTargetHandler);
                circle.addEventHandler(MOUSE_EXITED_TARGET,
                         circleTargetHandler);

                // Add mouse-entered and mouse-exited event handlers to
                // the Circle
                circle.addEventHandler(MOUSE_ENTERED, circleHandler);
                circle.addEventHandler(MOUSE_EXITED, circleHandler);

                Scene scene = new Scene(root);
                stage.setScene(scene);
                stage.setTitle(
                         "Mouse Entered Target and Exited Target Events");
                stage.show();
        }

        public void handleCircle(MouseEvent e) {
                print(e, "Circle Handler");
        }

        public void handleCircleTarget(MouseEvent e) {
                print(e, "Circle Target Handler");
        }

        public void handleHBoxTarget(MouseEvent e) {
                print(e, "HBox Target Filter");
                if (consumeCbx.isSelected()) {
                    e.consume();
                    System.out.println(
                               "HBox consumed the " + e.getEventType() + " event");
                }
        }

        public void print(MouseEvent e, String msg) {
                String type = e.getEventType().getName();
                String source = e.getSource().getClass().getSimpleName();
                String target = e.getTarget().getClass().getSimpleName();
                System.out.println(msg + ": Type=" + type +
                                         ", Target=" + target +
                                         ", Source=" + source);
        }
}

Listing 9-10Using the Mouse-Entered-Target and Mouse-Exited-Target Events

处理关键事件

按键事件是一种表示击键发生的输入事件。它被传送到具有焦点的节点。在javafx.scene.input包中声明的KeyEvent类的一个实例代表一个键事件。按键、按键释放和按键输入是按键事件的三种类型。表 9-5 列出了KeyEvent类中的所有常量,它们代表关键事件类型。

表 9-5

KeyEvent类中的常量代表关键事件类型

|

常量

|

描述

ANY 它是其他关键事件类型的超类型。
KEY_PRESSED 它在按键时发生。
KEY_RELEASED 它在释放一个键时发生。
KEY_TYPED 当输入 Unicode 字符时会出现这种情况。

Tip

形状(例如圆形或矩形)也可以接收按键事件,这一点可能并不明显。节点接收键事件的标准是节点应该有焦点。默认情况下,形状不是焦点遍历链的一部分,鼠标单击不会为它们带来焦点。Shape节点可以通过调用requestFocus()方法获得焦点。

与键入事件相比,按键和按键释放事件是较低级别的事件;它们分别在按键和释放时发生,并且取决于*台和键盘布局。

键类型事件是更高级别的事件。一般不依赖于*台和键盘布局。它在键入 Unicode 字符时发生。通常,按键会生成键入事件。然而,按键释放也可以生成按键类型的事件。例如,在 Windows 上使用 Alt 键和数字键盘时,释放 Alt 键会生成键入的事件,而不管在数字键盘上输入的击键次数。按键式事件也可以通过一系列按键和释放来生成。例如,通过按 Shift + A 输入字符 A,这包括两次按键(Shift 和 A)。在这种情况下,两次按键会生成一个键入事件。并非所有的按键或释放都会生成按键事件。例如,当您按下功能键(F1、F2 等。)或修饰键(Shift、Ctrl 等。),没有输入 Unicode 字符,因此不会生成键入的事件。

KeyEvent类维护三个变量来描述与事件相关的键:代码、文本和字符。这些变量可以使用表 9-6 中列出的KeyEvent类中的 getter 方法来访问。

表 9-6

返回关键细节的KeyEvent类中的方法

|

方法

|

有效期为

|

描述

KeyCode getCode() KEY_PRESSED``KEY_RELEASED KeyCode枚举包含一个常量来表示键盘上的所有键。该方法返回与被按下或释放的键相关联的KeyCode枚举常量。对于击键事件,它总是返回KeyCode.UNDEFINED,因为击键事件不一定由一次击键触发。
String getText() KEY_PRESSED``KEY_RELEASED 它返回与按键和按键释放事件相关联的KeyCodeString描述。对于键类型的事件,它总是返回一个空字符串。
String getCharacter() KEY_TYPED 它返回一个字符或一系列与键入事件相关的字符作为一个String。对于按键和按键释放事件,它总是返回KeyEvent.CHAR_UNDEFINED

有趣的是,getCharacter()方法的返回类型是String,而不是char。这个设计是有意的。基本多语言*面之外的 Unicode 字符不能用一个字符表示。一些设备可以通过一次击键产生多个字符。getCharacter()方法的返回类型String涵盖了这些奇怪的情况。

KeyEvent类包含isAltDown()isControlDown()isMetaDown()isShiftDown()isShortcutDown()方法,这些方法可以让您检查当一个按键事件发生时,修饰键是否被按下。

处理按键和按键释放事件

简单地通过向KEY_PRESSEDKEY_RELEASED事件类型的节点添加事件过滤器和处理程序来处理按键和按键释放事件。通常,您使用这些事件来了解按下或释放了哪些键,并执行某个操作。例如,您可以检测 F1 功能键的按下,并显示焦点节点的自定义帮助窗口。

清单 9-11 中的程序展示了如何处理按键和按键释放事件。它显示一个Label和一个TextField。当你运行程序时,TextField有焦点。运行该程序时使用击键时,请注意以下几点:

  • 按下并释放一些键。输出将显示事件发生时的详细信息。不是每个按键事件都会发生按键释放事件。

  • 按键和按键释放事件之间的映射不是一一对应的。按键事件可能没有按键释放事件(参考下一项)。对于几个按键事件,可能有一个按键释放事件。长时间按住一个键会发生这种情况。有时,您这样做是为了多次键入同一个字符。按住 A 键一段时间,然后松开。这将生成几个按键事件和一个按键释放事件。

  • 按 F1 键。它将显示帮助窗口。请注意,按下 F1 键不会为按键释放事件生成输出,即使在您释放按键之后也是如此。你能想到这是什么原因吗?在按键事件中,将显示帮助窗口,该窗口将获取焦点。主窗口上的TextField不再有焦点。回想一下,关键事件被交付给具有焦点的节点,并且在 JavaFX 应用程序中只有一个节点可以具有焦点。因此,按键释放事件被传递到帮助窗口,而不是TextField

// KeyPressedReleased.java
package com.jdojo.event;

import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import static javafx.scene.input.KeyEvent.KEY_PRESSED;
import javafx.scene.layout.HBox;
import javafx.scene.text.Text;
import javafx.stage.Stage;

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

        @Override

        public void start(Stage stage) {
                Label nameLbl = new Label("Name:");
                TextField nameTfl = new TextField();

                HBox root = new HBox();
                root.setPadding(new Insets(20));
                root.setSpacing(20);
                root.getChildren().addAll(nameLbl, nameTfl);

                // Add key pressed and released events to the TextField
                nameTfl.setOnKeyPressed(e -> handle(e));
                nameTfl.setOnKeyReleased(e -> handle(e));

                Scene scene = new Scene(root);
                stage.setScene(scene);
                stage.setTitle("Key Pressed and Released Events");
                stage.show();
        }

        public void handle(KeyEvent e) {
                String type = e.getEventType().getName();
                KeyCode keyCode = e.getCode();
                System.out.println(type + ": Key Code=" +
                   keyCode.getName() +
                   ", Text=" + e.getText());

                // Show the help window when the F1 key is pressed
                if (e.getEventType() == KEY_PRESSED &&
                               e.getCode() == KeyCode.F1) {
                    displayHelp();
                    e.consume();
                }
        }

        public void displayHelp() {
                Text helpText = new Text("Please enter a name.");
                HBox root = new HBox();
                root.setStyle("-fx-background-color: yellow;");
                root.getChildren().add(helpText);

                Scene scene = new Scene(root, 200, 100);
                Stage helpStage = new Stage();
                helpStage.setScene(scene);
                helpStage.setTitle("Help");
                helpStage.show();
        }
}

Listing 9-11Handling Key-Pressed and Key-Released Events

处理键入的事件

键入的事件用于检测特定的击键。您不能使用它来阻止用户输入某些字符,为此,您可以使用格式化程序。这里我们不解释如何使用格式化程序,但是如果您需要使用这种功能,例如,TextField 控件的 API 文档中的setTextFormatter()方法描述会为您提供一个起点。

清单 9-12 中的程序显示了一个Label和一个TextField。它向TextField添加了一个按键类型的事件处理程序,该处理程序打印按键的一些信息。

// KeyTyped.java
package com.jdojo.event;

import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;

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

        @Override
        public void start(Stage stage) {
                Label nameLbl = new Label("Name:");
                TextField nameTfl = new TextField();

                HBox root = new HBox();
                root.setPadding(new Insets(20));
                root.setSpacing(20);
                root.getChildren().addAll(nameLbl, nameTfl);

                // Add key-typed event to the TextField
                nameTfl.setOnKeyTyped(e -> handle(e));

                Scene scene = new Scene(root);
                stage.setScene(scene);
                stage.setTitle("Key Typed Event");
                stage.show();
        }

        public void handle(KeyEvent e) {
                String type = e.getEventType().getName();
                System.out.println(type + ": Character=" +
                                e.getCharacter());
        }
}

Listing 9-12Using the Key-Typed Event

处理窗口事件

当显示、隐藏或关闭窗口时,会发生窗口事件。javafx.stage包中的WindowEvent类的一个实例代表一个窗口事件。表 9-7 列出了WindowEvent类中的常量。

表 9-7

WindowEvent类中的常量来表示窗口事件类型

|

常量

|

描述

ANY 它是所有其他窗口事件类型的超类型。
WINDOW_SHOWING 它发生在窗口显示之前。
WINDOW_SHOWN 它发生在窗口显示之后。
WINDOW_HIDING 它发生在窗口隐藏之前。
WINDOW_HIDDEN 它发生在窗口隐藏之后。
WINDOW_CLOSE_REQUEST 当有关闭此窗口的外部请求时,就会发生这种情况。

窗口显示和窗口显示事件很简单。它们发生在窗口显示之前和之后。窗口显示事件的事件处理程序应该具有耗时的逻辑,因为它会延迟向用户显示窗口,从而降低用户体验。初始化一些窗口级别的变量是您需要在这个事件中编写的代码的一个很好的例子。通常,窗口显示事件为用户设置开始方向,例如,将焦点设置到窗口上的第一个可编辑字段,并向用户显示关于需要他们注意的任务的警告等。

窗口隐藏和窗口隐藏事件是窗口显示和窗口显示事件的对应物。它们发生在隐藏窗口之前和之后。

当存在关闭窗口的外部请求时,window-close-request 事件发生。使用上下文菜单中的关闭菜单或窗口标题栏中的关闭图标,或者在 Windows 上按 Alt + F4 组合键,都被视为关闭窗口的外部请求。注意,以编程方式关闭窗口,例如,使用Stage类的close()方法或Platform.exit()方法,不被认为是外部请求。如果使用了 window-close-request 事件,则不会关闭窗口。

清单 9-13 中的程序展示了如何使用所有的窗口事件。您可能会得到与代码下面所示不同的输出。它向主要阶段添加了一个复选框和两个按钮。如果未选中该复选框,则会消耗关闭窗口的外部请求,从而阻止窗口关闭。“关闭”按钮关闭窗口。“隐藏”按钮隐藏主窗口并打开一个新窗口,因此用户可以再次显示主窗口。

该程序将事件处理程序添加到窗口事件类型的主要阶段。当调用舞台上的show()方法时,会生成窗口显示和窗口显示事件。当您单击隐藏按钮时,将生成窗口隐藏和窗口隐藏事件。当您单击弹出窗口上的按钮以显示主窗口时,将再次生成窗口显示和窗口显示事件。尝试单击标题栏上的关闭图标来生成窗口关闭请求事件。如果未选中“可以关闭窗口”复选框,则不会关闭窗口。当您使用关闭按钮关闭窗口时,会生成窗口隐藏和窗口隐藏事件,但不会生成窗口关闭请求事件,因为它不是关闭窗口的外部请求。

// WindowEventApp.java
package com.jdojo.event;

import javafx.application.Application;
import javafx.event.EventType;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;
import static javafx.stage.WindowEvent.WINDOW_CLOSE_REQUEST;

public class WindowEventApp  extends Application {
        private CheckBox canCloseCbx = new CheckBox("Can Close Window");

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

        @Override
        public void start(Stage stage) {
                Button closeBtn = new Button("Close");
                closeBtn.setOnAction(e -> stage.close());

                Button hideBtn = new Button("Hide");
                hideBtn.setOnAction(e -> {
                         showDialog(stage); stage.hide(); });

                HBox root = new HBox();
                root.setPadding(new Insets(20));
                root.setSpacing(20);
                root.getChildren().addAll(
                         canCloseCbx, closeBtn, hideBtn);

                // Add window event handlers to the stage
                stage.setOnShowing(e -> handle(e));
                stage.setOnShown(e -> handle(e));
                stage.setOnHiding(e -> handle(e));
                stage.setOnHidden(e -> handle(e));
                stage.setOnCloseRequest(e -> handle(e));

                Scene scene = new Scene(root);
                stage.setScene(scene);
                stage.setTitle("Window Events");
                stage.show();
        }

        public void handle(WindowEvent e) {

                // Consume the event if the CheckBox is not selected
                // thus preventing the user from closing the window
                EventType<WindowEvent> type = e.getEventType();
                if (type == WINDOW_CLOSE_REQUEST &&
                             !canCloseCbx.isSelected()) {
                         e.consume();
                }

                System.out.println(type + ": Consumed=" +
                         e.isConsumed());
        }

        public void showDialog(Stage mainWindow) {
                Stage popup = new Stage();

                Button closeBtn =
                         new Button("Click to Show Main Window");
                closeBtn.setOnAction(e -> {
                         popup.close(); mainWindow.show();});

                HBox root = new HBox();
                root.setPadding(new Insets(20));
                root.setSpacing(20);
                root.getChildren().addAll(closeBtn);

                Scene scene = new Scene(root);
                popup.setScene(scene);
                popup.setTitle("Popup");
                popup.show();
        }
}
WINDOW_SHOWING: Consumed=false
WINDOW_SHOWN: Consumed=false
WINDOW_HIDING: Consumed=false
WINDOW_HIDDEN: Consumed=false
WINDOW_SHOWING: Consumed=false
WINDOW_SHOWN: Consumed=false
WINDOW_CLOSE_REQUEST: Consumed=true

Listing 9-13Using Window Events

摘要

一般来说,术语“事件”用于描述感兴趣的事件。在 GUI 应用程序中,事件是用户与应用程序交互的发生,例如点击鼠标、按下键盘上的键等等。JavaFX 中的事件由javafx.event.Event类或其任何子类的对象表示。JavaFX 中的每个事件都有三个属性:事件源、事件目标和事件类型。

当应用程序中发生事件时,通常通过执行一段代码来执行一些处理。为响应事件而执行的这段代码称为事件处理程序或事件过滤器。当您想要处理 UI 元素的事件时,您需要向 UI 元素添加事件处理程序,例如,WindowSceneNode。当 UI 元素检测到事件时,它会执行您的事件处理程序。

调用事件处理程序的 UI 元素是这些事件处理程序的事件源。当一个事件发生时,它会通过一连串的事件调度程序。事件的源是事件调度程序链中的当前元素。当事件通过事件调度程序链中的一个调度程序传递到另一个调度程序时,事件源会发生变化。事件目标是事件的目的地,它决定了事件在处理过程中的行进路线。事件类型描述发生的事件的类型。它们是以分层的方式定义的。每个事件类型都有一个名称和一个父类型。

当事件发生时,依次执行以下三个步骤:事件目标选择、事件路径构建和事件路径遍历。事件目标是基于事件类型选择的事件的目的节点。事件通过事件调度链中的事件调度程序传播。事件调度链是事件路由。事件的初始和默认路线由事件目标决定。默认事件路由由从阶段开始到事件目标节点的容器子路径组成。

事件路由遍历包括两个阶段:捕获和冒泡。一个事件在其路由中经过每个节点两次:一次在捕获阶段,一次在冒泡阶段。您可以为特定的事件类型向节点注册事件过滤器和事件处理程序。在捕获和冒泡阶段,当事件通过节点时,分别执行注册到节点的事件过滤器和事件处理程序。

在路由遍历期间,节点可以使用事件过滤器或处理程序中的事件,从而完成事件的处理。消费一个事件只需调用事件对象上的consume()方法。当一个事件被消费时,事件处理被停止,即使路由中的一些节点根本没有被遍历。

用户使用鼠标与 UI 元素的交互(如单击、移动或按下鼠标)会触发鼠标事件。MouseEvent类的一个对象代表一个鼠标事件。

按键事件表示击键的发生。它被传送到具有焦点的节点。KeyEvent类的一个实例代表一个按键事件。按键、按键释放和按键输入是按键事件的三种类型。

当显示、隐藏或关闭窗口时,会发生窗口事件。javafx.stage包中的WindowEvent类的一个实例代表一个窗口事件。

下一章讨论用作其他控件和节点的容器的布局窗格。

十、了解布局窗格

在本章中,您将学习:

  • 什么是布局窗格

  • JavaFX 中表示布局窗格的类

  • 如何向布局窗格添加子项

  • 实用类如InsetsHPosVPosSidePriority等。

  • 如何使用一个Group来布局节点

  • 如何使用Region及其属性

  • 如何使用不同类型的布局窗格,如HBoxVBoxFlowPaneBorderPaneStackPaneTilePaneGridPaneAnchorPaneTextFlow

本章的例子在com.jdojo.container包中。为了让它们工作,您必须在module-info.java文件中添加相应的一行:

...
opens com.jdojo.container to javafx.graphics, javafx.base;
...

本章中所有编号的 Java 代码清单都只是简短地标记了一下。完整的列表,请到这本书的下载区。在container文件夹中找到本章的列表。

什么是布局窗格?

可以使用两种类型的布局来排列场景图中的节点:

  • 静态布局

  • 动态布局

在静态布局中,节点的位置和大小只计算一次,在调整窗口大小时保持不变。当窗口具有节点最初布局的大小时,用户界面看起来很好。

在动态布局中,每当用户动作需要改变节点的位置、大小或两者时,场景图中的节点被布局。通常,更改一个节点的位置或大小会影响场景图中所有其他节点的位置和大小。当调整窗口大小时,动态布局强制重新计算一些或所有节点的位置和大小。

静态和动态布局各有优缺点。静态布局让开发人员可以完全控制用户界面的设计。它能让你在合适的时候利用可用空间。动态布局需要更多的编程工作,逻辑也更加复杂。通常,支持 GUI 的编程语言,例如 JavaFX,通过库支持动态布局。库解决了动态布局的大部分用例。如果它们不能满足你的需求,你必须努力推出你自己的动态布局。

一个布局窗格是一个包含其他节点的节点,这些节点被称为其子节点(或子节点)。布局窗格的职责是在需要时对其子元素进行布局。布局窗格也称为容器布局容器

布局窗格有一个布局策略,控制布局窗格如何布局其子元素。例如,布局窗格可以水*、垂直或以任何其他方式布置其子元素。

JavaFX 包含几个与布局相关的类,这是本章讨论的主题。布局窗格执行两件事:

  • 它计算节点在其父节点中的位置(x 和 y 坐标)。

  • 它计算节点的大小(宽度和高度)。

对于 3D 节点,布局窗格还会计算大小的位置和深度的 z 坐标。

容器的布局策略是一组计算其子容器的位置和大小的规则。当我在本章中讨论容器时,请注意容器的布局策略,它们是如何计算子容器的位置和大小的。节点有三种大小:首选大小、最小大小和最大大小。大多数容器试图给孩子他们喜欢的尺寸。节点的实际(或当前)大小可能与其首选大小不同。节点的当前大小取决于窗口的大小、容器的布局策略、节点的扩展和收缩策略等。

布局窗格类

JavaFX 包含几个容器类。图 10-1 显示了容器类的部分类图。容器类是Parent类的直接或间接子类。

img/336502_2_En_10_Fig1_HTML.jpg

图 10-1

JavaFX 中容器类的类图

一个Group允许你将效果和变换应用到它的所有子节点上。Group级在javafx.scene包里。

Region类的子类用于布局子元素。它们可以用 CSS 样式化。Region类及其大多数子类都在javafx.scene.layout包中。

诚然,容器需要是Parent类的子类。然而,并不是所有的Parent类的子类都是容器。例如,Button类是Parent类的子类;但是,它是一个控件,而不是一个容器。必须将节点添加到容器中,使其成为场景图的一部分。容器根据其布局策略来布局其子容器。如果不希望容器管理节点的布局,需要将节点的managed属性设置为 false。请参考第六章,了解更多关于托管和非托管节点的详细信息和示例。

一个节点一次只能是一个容器的子节点。如果将一个节点添加到一个容器中,而该节点已经是另一个容器的子节点,则在将该节点添加到第二个容器之前,会将其从第一个容器中删除。通常,需要嵌套容器来创建复杂的布局。也就是说,您可以将一个容器作为子节点添加到另一个容器中。

Parent类包含三个方法来获取容器的子容器列表:

  • protected ObservableList<Node> getChildren()

  • public ObservableList<Node> getChildrenUnmodifiable()

  • protected <E extends Node> List<E> getManagedChildren()

getChildren()方法返回一个容器的子节点的可修改的ObservableList。如果您想要将节点添加到容器中,您可以将该节点添加到此列表中。这是容器类最常用的方法。我们一直用这种方法给GroupHBoxVBox等容器添加孩子。从第一个节目开始。

注意对getChildren()方法的protected访问。如果Parent类的子类不想成为一个容器,它将保持对这个方法的访问为protected。比如控制相关类(ButtonTextField等)。)将这个方法保持为protected,这样就不能向它们添加子节点。一个容器类覆盖了这个方法并使其成为public。例如,GroupPane类将这个方法公开为public

Parent类中getChildrenUnmodifiable()方法被声明为public。它返回一个只读的ObservableList子节点。它在两种情况下很有用:

  • 您需要将容器的子列表传递给一个不应该修改该列表的方法。

  • 你想知道控件是由什么组成的,而不是容器。

getManagedChildren()方法具有protected访问。容器类不会将其公开为public。在布局过程中,他们在内部使用它来获取托管子级的列表。您将使用这个方法推出您自己的容器类。

表 10-1 简要描述了容器类别。我们将在随后的章节中通过示例详细讨论它们。

表 10-1

容器类别列表

|

集装箱等级

|

描述

Group A Group将效果和变换一起应用到它的所有子节点。
Pane 它用于其子节点的绝对定位。
HBox 它将子对象水*排列在一行中。
VBox 它将子元素垂直排列在一列中。
FlowPane 它以行或列的形式水*或垂直排列其子节点。如果一行或一列放不下它们,它们将按指定的宽度或高度换行。
BorderPane 它将其布局区域划分为顶部、右侧、底部、左侧和中间区域,并将每个子区域放在五个区域之一。
StackPane 它以从后到前的堆栈方式排列其子元素。
TilePane 它将子节点排列在一个大小一致的网格中。
GridPane 它将子节点排列在大小可变的单元格网格中。
AnchorPane 它通过将子元素的边缘锚定到布局区域的边缘来排列子元素。
TextFlow 它展示了富文本,其内容可能由几个Text节点组成。

向布局窗格添加子项

容器是用来装孩子的。您可以在创建容器对象时或创建后将子对象添加到容器中。所有容器类都提供接受 var-args Node类型参数的构造器来添加初始的子集合。有些容器提供构造器来添加初始的一组子容器,并为容器设置初始属性。

创建容器后,您还可以随时向容器中添加子容器。容器将它们的子容器存储在一个可观察的列表中,可以使用getChildren()方法检索该列表。向容器添加节点就像向可观察列表添加节点一样简单。下面的代码片段显示了如何在创建HBox时和创建后向其添加子元素:

// Create two buttons
Button okBtn = new Button("OK");
Button cancelBtn = new Button("Cancel");

// Create an HBox with two buttons as its children
HBox hBox1 = new HBox(okBtn, cancelBtn);

// Create an HBox with two buttons with 20px horizontal spacing between them
double hSpacing = 20;
HBox hBox2 = new HBox(hSpacing, okBtn, cancelBtn);

// Create an empty HBox, and afterwards, add two buttons to it
HBox hBox3 = new HBox();
hBox3.getChildren().addAll(okBtn, cancelBtn);

Tip

当你需要在一个容器中添加多个子节点时,使用ObservableListaddAll()方法,而不是多次使用add()方法。

实用程序类和枚举

使用布局窗格时,您需要使用几个与间距和方向相关的类和枚举。这些类和枚举在独立使用时没有用。它们总是被用作节点的属性。本节描述了其中的一些类和枚举。

Insets 类

Insets类表示矩形区域四个方向的内部偏移:上、右、下、左。它是一个不可变的类。它有两个构造器,一个让您为所有四个方向设置相同的偏移,另一个让您为每个方向设置不同的偏移:

  • Insets(double topRightBottomLeft)

  • Insets(double top, double right, double bottom, double left)

Insets类声明了一个常量Insets.EMPTY,表示所有四个方向的零偏移。使用getTop()getRight()getBottom()getLeft()方法获得特定方向的偏移值。

通过查看对Insets类的描述来理解术语 insets 的确切含义有点令人困惑。让我们在这一节详细讨论它的含义。我们在两个矩形的上下文中讨论 insets。插入是相同边缘之间的距离(从上到下,从左到左,等等。)的两个矩形。有四个插入值,矩形的每一边一个。Insets类的对象存储四个距离。图 10-2 显示了两个矩形以及内矩形相对于外矩形的插入。

img/336502_2_En_10_Fig2_HTML.png

图 10-2

一个矩形区域相对于另一个矩形区域的插入

两个矩形可能重叠,而不是一个完全包含在另一个中。在这种情况下,一些插入值可能为正,一些为负。插入值相对于参考矩形进行解释。为了正确解释插入值,需要获得参考矩形的位置、其边缘以及需要测量插入值的方向。使用术语“插入”的上下文应该使这些信息可用。在图中,我们可以相对于内部或外部矩形定义相同的 insets。插入值不会改变。但是,参考矩形和测量插入的方向(确定插入值的符号)将会改变。

通常,在 JavaFX 中,术语 insets 和Insets对象在四种上下文中使用:

  • 边框嵌入

  • 背景插图

  • 出口

  • 昆虫

在前两个上下文中,insets 表示布局边界的边缘与边框的内边缘或背景的内边缘之间的距离。在这些内容中,insets 从布局边界的边缘向内测量。负值表示从布局边界边缘向外测量的距离。

边框笔画或图像可能落在Region的布局边界之外。外集是一个Region的布局边界的边缘和它的边界的外边缘之间的距离。外集也表示为一个Insets对象。

Javadoc for JavaFX 多次使用 insets 这个术语来表示从布局边界的所有边缘向内测量的边框和填充的厚度之和。在 Javadoc 中遇到 insets 这个术语时,要小心解释它的含义。

HPos 枚举

HPos枚举定义了三个常量:LEFTCENTERRIGHT,用来描述水*定位和对齐。

VPos 枚举

枚举的常量描述了垂直定位和对齐。它有四个常量:TOPCENTERBASELINEBOTTOM

Pos 枚举

Pos枚举中的常量描述了垂直和水*定位和对齐。它拥有所有VPosHPos常量组合的常量。Pos枚举中的常量有BASELINE_CENTERBASELINE_LEFTBASELINE_RIGHTBOTTOM_CENTERBOTTOM_LEFTBOTTOM_RIGHTCENTERCENTER_LEFTCENTER_RIGHTTOP_CENTERTOP_LEFTTOP_RIGHT。它有两个方法——getHpos()getVpos()——返回HPosVPos枚举类型的对象,分别描述水*和垂直定位和对齐。

水*方向枚举

HorizontalDirection枚举有两个常量,LEFTRIGHT,分别表示向左和向右的方向。

VerticalDirection 枚举

VerticalDirection枚举有两个常量,UPDOWN,分别表示向上和向下的方向。

方向枚举

Orientation枚举有两个常量,HORIZONTALVERTICAL,分别表示水*和垂直方向。

侧枚举

Side枚举有四个常量:TOPRIGHTBOTTOMLEFT,用来表示矩形的四条边。

优先级枚举

有时,一个容器的可用空间可能多于或少于按照子容器的首选大小来布局子容器所需的空间。Priority枚举用于表示当其父节点有更多或更少的空间时,节点增长或收缩的优先级。它包含三个常量:ALWAYSNEVERSOMETIMES。具有ALWAYS优先级的节点总是随着可用空间的增加或减少而增加或减少。具有NEVER优先级的节点不会随着可用空间的增加或减少而增长或缩小。当没有其他具有ALWAYS优先级的节点或具有ALWAYS优先级的节点无法消耗所有增加或减少的空间时,具有SOMETIMES优先级的节点会增大或缩小。

理解小组

一个Group具有容器的特征;比如它有自己的布局策略和坐标系,是Parent类的子类。然而,将其称为节点的集合或组*,而不是容器,可以最好地反映其含义。它用于将节点集合作为单个节点(或一个组)进行操作。应用于Group的变换、效果和属性会应用于Group中的所有节点。*

A Group有自己的布局策略,除了给孩子他们喜欢的尺寸外,不提供任何特定的布局:

  • 它按照添加节点的顺序呈现节点。

  • 它不定位其子节点。默认情况下,所有子对象都位于(0,0)处。您需要编写代码来定位一个Group的子节点。使用子节点的layoutXlayoutY属性将它们定位在Group中。

  • 默认情况下,它会将所有子元素的大小调整为自己喜欢的大小。可以通过将其autoSizeChildren属性设置为 false 来禁用自动调整大小行为。请注意,如果禁用自动调整大小属性,所有节点(形状除外)都将不可见,因为默认情况下它们的大小为零。

A Group没有自己的尺寸。它不能直接调整大小。它的大小是其子代的集合界限。当它的任何或所有子元素的边界改变时,它的边界也会改变。第六章解释了如何计算不同类型的界限。

创建组对象

您可以使用无参数构造器创建一个空的Group:

Group emptyGroup = new Group();

Group类的其他构造器允许您向Group添加子元素。一个构造器以一个Collection<Node>作为初始子体;另一个采用了一个Node类型的 var-args。

Button smallBtn = new Button("Small Button");
Button bigBtn = new Button("This is a big button");

// Create a Group with two buttons using its var-args constructor
Group group1 = new Group(smallBtn, bigBtn);

List<Node> initialList = new ArrayList<>();
initailList.add(smallBtn);
initailList.add(bigBtn);

// Create a Group with all Nodes in the initialList as its children
Group group2 = new Group(initailList);

渲染组中的节点

一个Group的子元素按照它们被添加的顺序被渲染。以下代码片段在阶段中显示时,如图 10-3 所示:

img/336502_2_En_10_Fig3_HTML.png

图 10-3

组中子对象的渲染顺序:第一个较小,第二个较大

Button smallBtn = new Button("Small button");
Button bigBtn = new Button("This is a big button");
Group root = new Group();
root.getChildren().addAll(smallBtn, bigBtn);
Scene scene = new Scene(root);

注意,我们在Group中添加了两个按钮。仅显示了其中一个按钮。较小的按钮首先呈现,因为它是集合中的第一个按钮。较大的按钮呈现为覆盖较小的按钮。两个按钮都存在。一个藏在另一个下面。如果我们交换添加按钮的顺序,使用下面的语句,产生的屏幕将如图 10-4 所示。请注意,较大按钮的左边部分被较小按钮覆盖,而右边部分仍然显示。

img/336502_2_En_10_Fig4_HTML.png

图 10-4

组中子对象的渲染顺序:第一个大,第二个小

// Add the bigger button first
root.getChildren().addAll(bigBtn, smallBtn);

Tip

如果您不希望Group中的节点重叠,您需要设置它们的位置。

在组中定位节点

您可以通过使用节点的layoutXlayoutY属性为子节点分配绝对位置来定位子节点。或者,您可以使用绑定 API 来定位它们相对于Group中其他节点的位置。

清单 10-1 展示了如何在Group中使用绝对和相对定位。图 10-5 显示了结果屏幕。程序在Group上增加了两个按钮( OKCancel )。 OK 按钮采用绝对定位;它被放置在(10,10)处。Cancel按钮相对于 OK 按钮放置;其垂直位置与 OK 按钮相同;其水*位置在 OK 按钮右边缘后 10px。注意使用流畅绑定 API 来完成取消按钮的相对定位。

img/336502_2_En_10_Fig5_HTML.png

图 10-5

具有两个使用相对位置的按钮的组

// NodesLayoutInGroup.java
// ... full listing in the book's download area.

Listing 10-1Laying Out Nodes in a Group

将效果和变换应用到群组

当您将效果和变换应用到Group时,它们会自动应用到它的所有子对象。在Group上设置属性,例如disableopacity属性,会在它的所有子节点上设置属性。

清单 10-2 展示了如何将效果、变换和状态应用到Group中。程序给Group增加了两个按钮。它应用 10 度的旋转变换、投影效果和 80%的不透明度。图 10-6 显示应用于Group的变换、效果和状态应用于它的所有子节点(本例中为两个按钮)。

img/336502_2_En_10_Fig6_HTML.png

图 10-6

效果、变换和状态应用到群组后,群组中的两个按钮

// GroupEffect.java
// ... full listing in the book's download area.

Listing 10-2Applying Effects and Transformations to a Group

用 CSS 样式化一个组

这个类没有提供太多的 CSS 样式。Node类的所有 CSS 属性都可用于Group类:例如-fx-cursor-fx-opacity-fx-rotate等。一个Group不能有自己的外观,比如填充、背景和边框。

了解区域

Region是所有布局窗格的基类。可以用 CSS 样式化。不像Group,它有自己的大小。它可以调整大小。它可以具有视觉外观,例如,具有填充、多种背景和多种边框。你不能直接使用Region类作为布局窗格。如果你想推出你自己的布局面板,扩展Pane类,它扩展了Region类。

Tip

Region类被设计成支持背景和边框的 CSS3 规范,因为它们适用于 JavaFX。“CSS 背景和边框模块三级”的规范可以在 www.w3.org/TR/css-backgrounds-3/ 在线找到。

默认情况下,Region定义一个矩形区域。然而,它可以被改变成任何形状。一个Region的绘图区域被分成几个部分。根据属性设置,Region可能会超出其布局边界。Region的零件:

  • 背景(填充和图像)

  • 内容区域

  • 填料

  • 边框(笔画和图像)

  • 边缘

  • 区域插图

图 10-7 显示了Region的部分。从 JavaFX 2 开始,不直接支持边距。使用Insets作为边框也可以得到同样的效果。

img/336502_2_En_10_Fig7_HTML.png

图 10-7

一个地区的不同部分

一个区域可以具有首先绘制的背景。内容区域是绘制Region(如控件)内容的区域。

填充是内容区域周围的可选空间。如果填充宽度为零,则填充边缘和内容区域边缘相同。

边界区域是填充周围的空间。如果边框宽度为零,则边框边缘和填充边缘相同。

边距是边框周围的空间。填充和边距非常相似。它们之间的唯一区别是边距定义了边框外边缘周围的空间,而填充定义了边框内边缘周围的空间。控件添加到窗格时支持边距,例如HBoxVBox等。然而,Region并不直接支持边距。

内容区域、填充和边框会影响Region的布局边界。您可以在Region的布局边界之外绘制边界,这些边界不会影响Region的布局边界。边距不影响Region的布局边界。

Region的布局边界边缘和它的内容区域之间的距离定义了Region的插入。Region类根据它的属性自动计算它的插入。它有一个只读的insets属性,您可以通过读取来了解它的 insets。请注意,布局容器需要知道放置其子容器的区域,并且它们可以在知道布局边界和插入的情况下计算内容区域。

Tip

按顺序绘制背景填充、背景图像、边框线条、边框图像和Region的内容。

设置背景

一个Region可以有一个由填充、图像或两者组成的背景。填充由颜色、四个角的半径和四条边的插入组成。填充按指定的顺序应用。颜色定义用于绘制背景的颜色。半径定义了用于拐角的半径;如果您想要矩形角,请将它们设置为零。插图定义了Region的边和背景填充的外边缘之间的距离。例如,顶部 10px 的插入意味着布局边界顶部边缘内 10px 的水*条带不会被背景填充绘制。填充的插图可能是负数。负插图将绘制区域扩展到Region的布局边界之外;在这种情况下,Region的绘制区域超出了它的布局边界。

以下 CSS 属性定义了Region的背景填充:

  • -fx-background-color

  • -fx-background-radius

  • -fx-background-insets

以下 CSS 属性用红色填充了Region的整个布局边界:

-fx-background-color: red;
-fx-background-insets: 0;
-fx-background-radius: 0;

下列 CSS 属性使用两种填充:

-fx-background-color: lightgray, red;
-fx-background-insets: 0, 4;
-fx-background-radius: 4, 2;

第一次填充用浅灰色覆盖整个Region(见 0px 插图);它对所有四个角使用 4px 半径,making the Region look like a rounded rectangle。第二次填充用红色覆盖Region;它在所有四个边上都使用了 4px 插入,这意味着来自Region边缘的 4px 没有被该填充绘制,并且该区域仍然具有第一次填充使用的浅灰色。第二次填充使用所有四个角的 2px 半径。

您也可以使用 Java 对象在代码中设置Region的背景。一个Background类的实例代表了一个Region的背景。该类定义了一个Background.EMPTY常量来表示空背景(没有填充和图像)。

Tip

一个Background对象是不可变的。可以放心用作多个Region的背景。

一个Background对象有零个或多个填充和图像。BackgroundFill类的一个实例代表一种填充;BackgroundImage类的一个实例代表一幅图像。

Region类包含一个ObjectProperty<Background>类型的background属性。使用setBackground(Background bg)方法设置Region的背景。

下面的代码片段创建了一个带有两个BackgroundFill对象的Background对象。将此设置为Region会产生与使用 CSS 样式绘制两种填充的背景相同的效果,如前面的代码片段所示。注意,InsetsCornerRadii类用于定义填充的插入和圆角半径。

import javafx.geometry.Insets;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.CornerRadii;
import javafx.scene.paint.Color;
...
BackgroundFill lightGrayFill =
        new BackgroundFill(Color.LIGHTGRAY,
                           new CornerRadii(4), new Insets(0));

BackgroundFill redFill =
           new BackgroundFill(Color.RED,
                              new CornerRadii(2), new Insets(4));

// Create a Background object with two BackgroundFill objects

Background bg = new Background(lightGrayFill, redFill);

清单 10-3 中的程序展示了如何使用 CSS 属性和Background对象为Pane(??)设置背景。结果屏幕如图 10-8 所示。getCSSStyledPane()方法创建一个Pane,使用 CSS 添加两种填充的背景,并返回PanegetObjectStyledPane()方法创建一个Pane,使用 Java 类添加带有两种填充的背景,并返回Panestart()方法将两个Pane加到另一个Pane上,并将它们并排放置。

img/336502_2_En_10_Fig8_HTML.jpg

图 10-8

两个具有相同背景设置的窗格:一个使用 CSS,一个使用 Java 对象

// BackgroundFillTest.java
// ... full listing in the book's download area.

Listing 10-3Using Background Fills As the Background for a Region

以下 CSS 属性定义了Region的背景图像:

  • -fx-background-image

  • -fx-background-repeat

  • -fx-background-position

  • -fx-background-size

属性是图片的 CSS URL。-fx-background-repeat属性指示图像将如何重复(或不重复)以覆盖Region的绘图区域。-fx-background-position决定图像在区域中的位置。-fx-background-size属性决定了图像相对于区域的大小。

以下 CSS 属性用红色填充了Region的整个布局边界:

-fx-background-image: URL('your_image_url_goes_here');
-fx-background-repeat: space;
-fx-background-position: center;
-fx-background-size: cover;

下面的代码片段和前面的 CSS 属性集在设置到Region时会产生相同的效果:

import javafx.scene.image.Image;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundImage;
import javafx.scene.layout.BackgroundPosition;
import javafx.scene.layout.BackgroundRepeat;
import javafx.scene.layout.BackgroundSize;
...
Image image = new Image("your_image_url_goes_here");
BackgroundSize bgSize = new BackgroundSize(100, 100, true, true, false, true);
BackgroundImage bgImage =
    new BackgroundImage(image,
                        BackgroundRepeat.SPACE,
                        BackgroundRepeat.SPACE,
                        BackgroundPosition.DEFAULT,
                        bgSize);

// Create a Background object with an BackgroundImage object

Background bg = new Background(bgImage);

设置填充

Region的填充是其内容区域周围的空间。Region类包含一个ObjectProperty<Insets>类型的padding属性。您可以为四条边中的每一条边设置单独的填充宽度:

// Create an HBox
HBox hb = new HBox();

// A uniform padding of 10px around all edges
hb.setPadding(new Insets(10));

// A non-uniform padding: 2px top, 4px right, 6px bottom, and 8px left
hb.setPadding(new Insets(2, 4, 6, 8));

设置边框

一个Region可以有一个边框,由笔画、图像或两者组成。如果笔画和图像不存在,则边框被认为是空的。笔画和图像按指定的顺序应用;所有描边都在图像之前应用。您可以使用 CSS 和代码中的Border类来设置边框。

Note

在本节中,我们将同义地使用短语“a Region的边缘”和“a Region的布局边界”,它们表示由Region的布局边界定义的矩形的边缘。

笔画由五个属性组成:

  • 一种颜色

  • 一种风格

  • 宽度

  • 四个角的半径

  • 四面有插图

颜色定义了笔画使用的颜色。您可以为四条边指定四种不同的颜色。

样式定义了笔画的样式:例如,实线、虚线等。样式还定义了边框相对于其插入的位置:例如,insideoutsidecentered。您可以为四条边指定四种不同的样式。

半径定义了拐角的半径;如果您想要矩形角,请将它们设置为零。

笔画的宽度决定了它的粗细。您可以为四条边指定四种不同的宽度。

笔画的插入定义了从绘制边界的Region的布局边界边的距离。侧边插入的正值是从Region边缘向内测量的。侧面的插入负值是从Region的边缘向外测量的。边上零的插入表示布局的边缘限制自身。一些侧面(例如顶部和底部)可能具有正插入,而另一些侧面(例如右侧和左侧)可能具有负插入。图 10-9 显示了正负插入相对于 a Region布局边界的位置。实线中的矩形是Region的布局边界,虚线中的矩形是插入线。

img/336502_2_En_10_Fig9_HTML.png

图 10-9

正插入和负插入相对于布局边界的位置

边框笔画可以画在Region布局边界的内部、外部,或者部分在内部,部分在外部。要确定一个笔画相对于布局边界的确切位置,需要查看它的两个属性, insetsstyle :

  • 如果笔画的样式是inside,则笔画会画在插图内。

  • 如果样式是外部的,它将绘制在插图的外部。

  • 如果样式是居中的,它一半画在插图的里面,一半画在插图的外面。

图 10-10 显示了Region的边界位置的一些例子。虚线中的矩形表示Region的布局边界。边框以浅灰色显示。每个Region下面的标签显示了边框属性的一些细节(例如,样式、插入和宽度)。

img/336502_2_En_10_Fig10_HTML.jpg

图 10-10

根据样式和插入内容确定边框位置的示例

以下 CSS 属性定义了Region的边框线条:

  • -fx-border-color

  • -fx-border-style

  • -fx-border-width

  • -fx-border-radius

  • -fx-border-insets

下面的 CSS 属性绘制一个宽度为 10px、颜色为红色的边框。边框的外边缘将与Region的边缘相同,因为我们已经分别将 insets 和 style 设置为零和内部。由于我们已经将所有角的半径设置为 5px,因此边界将在角上变圆:

-fx-border-color: red;
-fx-border-style: solid inside;
-fx-border-width: 10;
-fx-border-insets: 0;
-fx-border-radius: 5;

下列 CSS 属性使用两种线条作为边框。第一个笔画画在Region的边缘内,第二个笔画画在边缘外:

-fx-border-color: red, green;
-fx-border-style: solid inside, solid outside;
-fx-border-width: 5, 2 ;
-fx-border-insets: 0, 0;
-fx-border-radius: 0, 0;

Tip

Region边缘外绘制的边框部分不影响其布局边界。在Region边缘外绘制的边框部分在Region的布局边界内。换句话说,落在Region边缘内的边界区域会影响那个Region的布局边界。

到目前为止,我们已经讨论了边框笔画的插入。一个边框也有插入输出,这是根据其笔画和图像的属性自动计算的。考虑到在Region的边缘内绘制的所有笔画和图像,Region的边缘与其边框的内边缘之间的距离被称为边框的插入。考虑到在Region的边缘之外绘制的所有笔画和图像,Region的边缘与其边框的外边缘之间的距离被称为边框的外集*。你必须能够区分笔画的嵌入和边框的嵌入/外嵌。笔画的嵌入决定了笔画的位置,而边框的嵌入/外嵌告诉您边框在Region边缘的内侧/外侧延伸了多远。图 10-11 显示了如何计算一个边框的内插和外插。虚线显示了一个Region的布局边界,它的边界有两个笔画:一个红色,一个绿色。当在 150px X 50px Region上设置以下样式时,会产生如图 10-11 所示的边框:*

img/336502_2_En_10_Fig11_HTML.png

图 10-11

边框的内插/外插与区域的布局界限之间的关系

-fx-background-color: white;
-fx-padding: 10;
-fx-border-color: red, green, black;
-fx-border-style: solid inside, solid outside, dashed centered;
-fx-border-width: 10, 8, 1;
-fx-border-insets: 12, -10, 0;
-fx-border-radius: 0, 0, 0;

边框的四条边的 insets 都是 22px,这是通过将从Region的边缘开始在 12px (insets)内绘制的红色边框的 10px 宽度相加得到的(10px + 12px)。所有四条边的边框外集都是 18px,这是通过将从Region的边缘画在 10px(–10 英寸)之外的绿色边框的 8px 宽度相加计算出来的(8px + 10px)。

您也可以使用 Java 对象在代码中设置Region的边框。一个Border类的实例代表了一个Region的边界。该类定义了一个Border.EMPTY常量来表示空边框(没有笔画和图像)。

Tip

一个Border对象是不可变的。它可以安全地用于多个Region

一个Border对象有零个或多个笔画和图像。Border类提供了几个接受多个笔画和图像作为参数的构造器。Region类包含一个ObjectProperty<Border>类型的border属性。使用setBorder(Border b)方法设置Region的边界。

BorderStroke类的一个实例代表一个笔画;BorderImage类的一个实例代表一幅图像。BorderStroke类提供了设置笔画样式的构造器。下面是两个常用的构造器。第三个构造器允许你在四边设置不同的颜色和风格。

  • BorderStroke(Paint stroke, BorderStrokeStyle style, CornerRadii radii, BorderWidths widths)

  • BorderStroke(Paint stroke, BorderStrokeStyle style, CornerRadii radii, BorderWidths widths, Insets insets)

BorderStrokeStyle类表示笔画的样式。BorderWidths类表示边框四边的笔画宽度。它允许您将宽度设置为绝对值或Region尺寸的百分比。下面的代码片段创建了一个Border,并将其设置为一个Pane:

BorderStrokeStyle style =
    new BorderStrokeStyle(StrokeType.INSIDE,
                   StrokeLineJoin.MITER,
                   StrokeLineCap.BUTT,
                   10,
                   0,
                   null);
BorderStroke stroke =
    new BorderStroke(Color.GREEN,
                style,
                CornerRadii.EMPTY,
                new BorderWidths(8),
                new Insets(10));
Pane p = new Pane();
p.setPrefSize(100, 50);
Border b = new Border(stroke);
p.setBorder(b);

Border类提供了getInsets()getOutsets()方法,为Border返回 insets 和 outsets。两种方法都返回一个Insets对象。请记住Border的插入和伸出与笔画的插入是不同的。它们是根据Border拥有的笔画和图像的插图和样式自动计算的。

您可以使用BordergetStrokes()getImages()方法来获取Border的所有笔画和所有图像,这两个方法分别返回List<BorderStroke>List<BorderImage>。您可以使用两个 Border 对象和两个BorderStroke对象的equals()方法来比较它们是否相等。

清单 10-4 演示了如何创建和设置一个Pane的边框。它显示一个有两个Pane的屏幕。一个Pane使用 CSS 样式,另一个使用Border对象。Pane s 看起来与图 10-11 中所示的相似。该程序打印边界的插入和输出,并检查两个边界是否相同。两种边框都使用三种笔画。getCSSStyledPane()方法返回一个用 CSS 样式的PanegetObjectStyledPane()方法使用一个Border对象返回一个Pane样式。

// BorderStrokeTest.java

// ... full listing in the book's download area.
cssBorder insets:Insets [top=22.0, right=22.0, bottom=22.0, left=22.0]
cssBorder outsets:Insets [top=18.0, right=18.0, bottom=18.0, left=18.0]
objectBorder insets:Insets [top=22.0, right=22.0, bottom=22.0, left=22.0]
objectBorder outsets:Insets [top=18.0, right=18.0, bottom=18.0, left=18.0]
Borders are equal.

Listing 10-4Using Strokes As the Border for a Region

使用图像作为边框不像使用笔画那样简单。一幅图像定义了一个矩形区域;一个Region也是。在称为边界图像区域的区域中,围绕Region绘制边界。一个Region的边界区域可能是整个Region的区域;它可能部分或全部在Region的内部或外部。Region四边的插图定义了边界图像区域。为了使图像成为围绕Region的边界,边界图像区域和图像都被分成九个区域:四个角、四个边和一个中间。通过指定四边(上、右、下和左)的宽度,边框区域被分为九个部分。宽度是沿着那些边的边界的宽度。通过指定每边的切片宽度,图像也被切片(分割)成九个区域。图 10-12 显示了一个Region,一个图像及其九个区域(或切片)的边界图像区域。在图中,边界图像区域与区域的面积相同。

img/336502_2_En_10_Fig12_HTML.png

图 10-12

将区域和图像分割成九个部分

Tip

如果区域使用矩形以外的形状,则不会绘制边框图像。

注意,在划分边界区域和图像时,来自边缘的四个宽度不一定必须一致。例如,您可以将宽度指定为顶部 2px、右侧 10px、底部 2px 和左侧 10px。

将边框图像区域和图像分成九个区域后,需要指定控制图像切片的定位和调整大小行为的属性。图像的九个切片中的每一个都必须被定位并适合其在边界图像区域中的相应部分。例如,图像左上角的图像切片必须适合边界图像区域的左上角部分。图像切片及其对应的边界图像切片这两个分量可能大小不同。您将需要指定如何填充边界图像区域中的区域(缩放、重复等)。)和相应的图像切片。通常,图像的中间部分被丢弃。但是,如果您想要填充边界图像区域的中间区域,您可以使用图像的中间部分。

在图 10-12 中,Region和边界图像区域的边界是相同的。图 10-13 举例说明了边界图像区域的边界落在Region边界的内部和外部。边界图像区域的一些区域可能落在Region之外,而一些区域落在里面。

img/336502_2_En_10_Fig13_HTML.png

图 10-13

区域的面积和边界图像面积之间的关系

以下 CSS 属性定义了Region的边框图像:

  • -fx-border-image-source

  • -fx-border-image-repeat

  • -fx-border-image-slice

  • -fx-border-image-width

  • -fx-border-image-insets

属性是图片的 CSS URL。对于多个图像,使用图像的 CSS URLs 的逗号分隔列表。

-fx-border-image-repeat属性指定图像的一部分如何覆盖Region的相应部分。您可以分别为 x 轴和 y 轴指定属性。有效值:

  • no-repeat

  • repeat

  • round

  • space

no-repeat值指定应该缩放图像切片以填充该区域,而不重复它。repeat值指定图像应该重复(*铺)以填充该区域。round值指定图像应该重复(*铺)以使用整数个*铺块填充区域,并且如果需要,缩放图像以使用整数个*铺块。space值指定应重复(*铺)图像,以使用整数个*铺块填充该区域,而不缩放图像,并在*铺块周围均匀分布额外空间。

-fx-border-image-slice属性指定从图像的上、右、下和左边缘向内偏移,将图像分成九个部分。属性可以指定为数字文字或图像边长的百分比。如果值中存在单词 fill,则保留图像的中间部分,并用于填充边界图像区域的中间区域;否则,中间的切片被丢弃。

-fx-border-image-width属性指定从边界图像区域的四边向内偏移,将边界图像区域分成九个区域。请注意,我们将边界图像区域划分为九个区域,而不是区域。该属性可以指定为数字文本或边框图像区域边长的百分比。

-fx-border-image-insets属性指定了Region的边缘和四边的边框图像区域的边缘之间的距离。正嵌入是从Region的边缘向其中心测量的。从Region的边缘向外测量一个负嵌入。在图 10-13 中,中间Region的边界图像区域有正插图,而该区域(左三)的边界图像区域有负插图。

让我们看一些使用图像作为边框的例子。在所有的例子中,我们将使用图 10-12 所示的图像作为 200px X 70px Pane的边框。

清单 10-5 包含 CSS,图 10-14 显示了当-fx-border-image-repeat属性设置为no-repeatrepeatspaceround时得到的Pane s。注意,我们已经将-fx-border-image-width-fx-border-image-slice属性设置为相同的 9px 值。这将使角切片恰好适合边界图像区域的角。边界图像区域的中间区域没有被填充,因为我们没有为-fx-border-image-slice属性指定fill值。我们已经用笔画出了Pane的边界。

img/336502_2_En_10_Fig14_HTML.jpg

图 10-14

对重复使用不同的值,而不使用切片属性的填充值

-fx-border-image-source: url('image_url_goes_here') ;
-fx-border-image-repeat: no-repeat;
-fx-border-image-slice: 9;
-fx-border-image-width: 9;
-fx-border-image-insets: 10;
-fx-border-color: black;
-fx-border-width: 1;
-fx-border-style: dashed inside;

Listing 10-5Using an Image As a Border Without Filling the Middle Region

清单 10-6 包含 CSS,它是清单 10-5 的一个微小变化。图 10-15 显示了结果Panes。这一次,边界图像区域的中间区域被填充,因为我们已经为-fx-border-image-slice属性指定了fill值。

img/336502_2_En_10_Fig15_HTML.jpg

图 10-15

对切片属性的填充值使用不同的重复值

-fx-border-image-source: url('image_url_goes_here') ;
-fx-border-image-repeat: no-repeat;
-fx-border-image-slice: 9 fill;
-fx-border-image-width: 9;
-fx-border-image-insets: 10;
-fx-border-color: black;
-fx-border-width: 1;
-fx-border-style: dashed inside;

Listing 10-6Using an Image As a Border Filling the Middle Region

不可变的BorderImage类表示Border中的边框图像。边框图像的所有属性都在构造器中指定:

BorderImage(Image image,
            BorderWidths widths,
            Insets insets,
            BorderWidths slices,
            boolean filled,
            BorderRepeat repeatX,
            BorderRepeat repeatY)

BorderRepeat枚举包含STRETCHREPEATSPACEROUND常量,用于指示图像切片如何在 x 和 y 方向重复以填充边界图像区域。它们具有在 CSS 中指定no-repeatrepeatspaceround的相同效果。

BorderWidths regionWidths = new BorderWidths(9);
BorderWidths sliceWidth = new BorderWidths(9);
boolean filled = false;
BorderRepeat repeatX = BorderRepeat.STRETCH;
BorderRepeat repeatY = BorderRepeat.STRETCH;
BorderImage borderImage =
    new BorderImage(new Image("image_url_goes_here"),
                    regionWidths,
                    new Insets(10),
                    sliceWidth,
                    filled,
                    repeatX,
                    repeatY);

清单 10-7 有一个使用 CSS 和 Java 类创建边框的程序。产生的屏幕如图 10-16 所示。左边和右边的Panes用相同的边框装饰:一个使用 CSS,另一个使用 Java 类。

img/336502_2_En_10_Fig16_HTML.jpg

图 10-16

使用 CSS 和 Java 类创建带有笔画和图像的边框

// BorderImageTest.java
// ... full listing in the book's download area.

Listing 10-7Using Strokes and Images As a Border

设置边距

不直接支持在Region上设置边距。大多数布局窗格支持其子窗格的边距。如果您想要一个Region的边距,将其添加到一个布局窗格,例如一个HBox,并使用布局窗格而不是Region:

Pane p1 = new Pane();
p1.setPrefSize(100, 20);

HBox box = new HBox();

// Set a margin of 10px around all four sides of the Pane
HBox.setMargin(p1, new Insets(10));
box.getChildren().addAll(p1);

现在,使用box而不是p1来获得围绕p1的边距。

了解窗格

PaneRegion类的子类。它公开了Parent类的getChildren()方法,该类是Region class的超类。这意味着Pane类及其子类的实例可以添加任何子类。

一个Pane提供以下布局特征:

  • 需要绝对定位时可以使用。默认情况下,它将其所有子节点定位在(0,0)处。您需要显式设置子项的位置。

  • 它将所有可调整大小的子对象调整到他们喜欢的大小。

默认情况下,Pane有最小、首选和最大尺寸。它的最小宽度是左右插入的总和;它的最小高度是顶部和底部插入的总和。其首选宽度是在当前 x 位置以其首选宽度显示其所有子级所需的宽度;它的首选高度是在当前 y 位置显示其所有子级所需的高度,以及它们的首选高度。其最大宽度和高度设置为Double.MAX_VALUE

清单 10-8 中的程序展示了如何创建一个Pane,给它添加两个Buttons,以及如何定位Buttons。产生的屏幕如图 10-17 所示。Pane使用边框显示它在屏幕上占据的区域。试着调整窗口大小,你会发现Pane会收缩和扩张。

img/336502_2_En_10_Fig17_HTML.jpg

图 10-17

有两个按钮的窗格

// PaneTest.java
// ... full listing in the book's download area.

Listing 10-8Using Panes

一个Pane让你设置它的首选大小:

Pane root = new Pane();
root.setPrefSize(300, 200); // 300px wide and 200px tall

您可以通过将其首选宽度和高度重置为计算出的宽度和高度,告诉Pane根据其子尺寸计算其首选尺寸:

Pane root = new Pane();

// Set the preferred size to 300px wide and 200px tall
root.setPrefSize(300, 200);

/* Do some processing... */

// Set the default preferred size

root.setPrefSize(Region.USE_COMPUTED_SIZE, Region.USE_COMPUTED_SIZE);

Tip

A Pane不剪辑其内容;它的子对象可能会显示在其边界之外。

了解 HBox

一个HBox在一个水*行中布置它的子元素。它允许您设置相邻子元素之间的水*间距、任何子元素的边距、调整子元素的行为等。它使用 0px 作为相邻子项之间的默认间距。内容区域和 HBox 的默认宽度足以以其首选宽度显示其所有子级,默认高度是其所有子级的最大高度。

您不能在HBox中设置孩子的位置。它们由 HBox 自动计算。您可以通过定制HBox的属性和设置子节点的约束,在一定程度上控制子节点的位置。

创建 HBox 对象

HBox 类的构造器允许您创建HBox对象,指定或不指定间距和初始子对象集:

// Create an empty HBox with the default spacing (0px)
HBox hbox1 = new HBox();

// Create an empty HBox with a 10px spacing
HBox hbox2 = new HBox(10);

// Create an HBox with two Buttons and a 10px spacing
Button okBtn = new Button("OK");
Button cancelBtn = new Button("Cancel");
HBox hbox3 = new HBox(10, okBtn, cancelBtn);

清单 10-9 中的程序展示了如何使用HBox。它给一个HBox增加了一个Label、一个TextField和两个Buttons。相邻子项之间的间距设置为 10px。10px 的填充用于保持HBox的边缘与其子项的边缘之间的距离。产生的窗口如图 10-18 所示。

img/336502_2_En_10_Fig18_HTML.jpg

图 10-18

一个带有标签、文本字段和两个按钮的 HBox

// HBoxTest.java
// ... full listing in the book's download area.

Listing 10-9Using the HBox Layout Pane

HBox 属性

HBox类声明了表 10-2 中列出的三个属性。

表 10-2

HBox 类中声明的属性

|

财产

|

类型

|

描述

alignment ObjectProperty<Pos> 它指定子元素相对于HBox内容区域的对齐方式。如果垂直对齐设置为BASELINE,则fillHeight属性被忽略。默认值为Pos.TOP_LEFT
fillHeight BooleanProperty 它指定是否调整可调整大小的子元素的大小以填充HBox的整个高度,或者给它们自己喜欢的高度。如果垂直对齐设置为BASELINE,该属性被忽略。默认值为 true。
spacing DoubleProperty 它指定相邻子项之间的水*间距。默认值为零。

对齐属性

使用alignment属性很简单。它指定了如何在HBox的内容区域内对齐子元素。默认情况下,HBox会为其内容分配足够的空间,以他们喜欢的大小排列所有的子元素。当HBox变得比它的首选大小时,对齐属性的效果是显而易见的。

清单 10-10 中的程序使用了一个带有两个按钮的 HBox。它设置HBoxPos.BOTTOM_RIGHT的对准。它将HBox的首选大小设置为比容纳其所有子节点所需的大小稍大一些,因此您可以看到对齐的效果。产生的窗口如图 10-19 所示。当您调整窗口大小时,子窗口在右下角区域保持对齐。

img/336502_2_En_10_Fig19_HTML.jpg

图 10-19

具有两个按钮和设置为 Pos 的对齐属性的 HBox。右下

// HBoxAlignment.java
// ... full listing in the book's download area.

Listing 10-10Using the HBox Alignment Property

fillHeight 属性

fillHeight属性指定HBox是垂直扩展其子元素以填充其内容区域的高度,还是保持其首选高度。请注意,该属性只影响那些允许垂直扩展的子节点。例如,默认情况下,Button的最大高度被设置为其首选高度,即使有垂直空间,在HBox中,Button也不会变得比其首选宽度高。如果您想要一个按钮垂直扩展,将其最大高度设置为Double.MAX_VALUE。默认情况下,TextArea被设置为展开。因此,HBox内的TextArea会随着HBox高度的增加而变高。如果您不希望可调整大小的子元素填充HBox的内容区域的高度,请将fillHeight属性设置为 false。

Tip

一个HBox的内容区域的首选高度是其子元素的首选高度中最大的一个。如果可调整大小的子级的最大高度属性允许它们扩展,则它们将填充内容区域的整个高度。否则,它们会保持在自己喜欢的高度。

清单 10-11 中的程序展示了fillHeight属性如何影响一个HBox的子元素的高度。它显示了一个HBox中的一些控件。一个TextArea默认可以垂直生长。取消按钮的最大高度设置为Double.MAX_VALUE,可以垂直增长。提供一个CheckBox来改变HBoxfillHeight属性的值。初始窗口如图 10-20 所示。注意Ok按钮具有首选高度,而取消按钮垂直扩展以填充由TextArea确定的内容区域的高度。调整窗口大小使其变高,并使用CheckBox更改fillHeight属性;TextArea取消按钮垂直伸缩。

img/336502_2_En_10_Fig20_HTML.jpg

图 10-20

一个带有一些控件的 HBox,用户可以在其中更改 fillHeight 属性

// HBoxFillHeight.java
// ... full listing in the book's download area.

Listing 10-11Using the fillHeight Property of an HBox

间距属性

spacing 属性指定了HBox中相邻子元素之间的水*距离。默认情况下,它设置为 0px。它可以在构造器中设置或者使用setSpacing()方法。

在 HBox 中为孩子设置约束

HBox支持两种类型的约束, hgrowmargin ,可以在每个子节点上单独设置。hgrow约束指定当额外空间可用时,子节点是否水*扩展。边距约束指定子节点边缘之外的空间。HBox类提供了setHgrow()setMargin()静态方法来指定这些约束。您可以使用null和这些方法单独移除约束。使用clearConstraints(Node child)方法一次删除子节点的两个约束。

让孩子横向成长

默认情况下,HBox中的子元素会得到他们喜欢的宽度。如果HBox是水*扩展的,它的子节点可以获得额外的可用空间,前提是它们的hgrow优先级设置为增长。如果一个HBox被水*展开,并且它的所有子节点都没有设置hgrow约束,那么额外的空间就没有被使用。

通过指定子节点和优先级,使用HBox类的setHgrow()静态方法设置子节点的hgrow优先级:

HBox root = new HBox(10);
TextField nameFld = new TextField();

// Let the TextField always grow horizontally
root.setHgrow(nameFld, Priority.ALWAYS);

要重置子节点的 hgrow 优先级,使用null作为优先级:

// Stop the TextField from growing horizontally
root.setHgrow(nameFld, null);

清单 10-12 中的程序展示了如何将一个TextField的优先级设置为Priority.ALWAYS,这样当HBox展开时,它可以占用所有额外的水*空间。图 10-21 显示了初始和扩展窗口。请注意,在窗口水*扩展后,除了TextField之外的所有控件都保持其首选宽度。

img/336502_2_En_10_Fig21_HTML.jpg

图 10-21

文本字段设置为总是水*增长的 HBox

// HBoxHGrow.java
// ... full listing in the book's download area.

Listing 10-12Letting a TextField Grow Horizontally

为孩子设置边距

边距是在节点边缘之外添加的额外空间。以下代码片段显示了如何向HBox的子对象添加边距:

Label nameLbl = new Label("Name:");
TextField nameFld = new TextField();
Button okBtn = new Button("OK");
Button cancelBtn = new Button("Cancel");

HBox hbox = new HBox(nameLbl, nameFld, okBtn, cancelBtn);

// Set a margin for all children:
// 10px top, 2px right, 10px bottom, and 2px left
Insets margin = new Insets(10, 2, 10, 2);
HBox.setMargin(nameLbl, margin);
HBox.setMargin(nameFld, margin);
HBox.setMargin(okBtn, margin);
HBox.setMargin(cancelBtn, margin);

通过将 margin 值设置为 null,可以删除子节点的边距:

// Remove margins for okBtn
HBox.setMargin(okBtn, null);

Tip

使用HBoxspacing属性和其子元素的边距约束时要小心。两者都会增加相邻孩子之间的水*差距。如果希望应用边距,请保持子项之间的水*间距一致,并将子项的左右边距设置为零。

了解 VBox

一个VBox在一个单独的垂直列中布置它的子元素。它允许您设置相邻子元素之间的垂直间距、任何子元素的边距、调整子元素的行为等。它使用 0px 作为相邻子项之间的默认间距。一个VBox的内容区域的默认高度足够以他们喜欢的高度显示它的所有子元素,并且默认宽度是它所有子元素的最大宽度。

您不能在VBox中设置孩子的位置。它们由VBox自动计算。您可以通过定制VBox的属性和设置子节点的约束,在一定程度上控制子节点的位置。

VBox一起工作类似于与HBox一起工作,不同之处在于它们的工作方向相反。比如在一个HBox中,子项默认填充内容区域的高度,在一个VBox中,子项默认填充内容的宽度;一个HBox让您在子节点上设置hgrow约束,一个VBox让您设置vgrow约束。

创建 VBox 对象

VBox类的构造器允许您创建VBox对象,指定或不指定间距和初始子对象集:

// Create an empty VBox with the default spacing (0px)
VBox vbox1 = new VBox();

// Create an empty VBox with a 10px spacing
VBox vbox2 = new VBox(10);

// Create a VBox with two Buttons and a 10px spacing
Button okBtn = new Button("OK");
Button cancelBtn = new Button("Cancel");
VBox vbox3 = new VBox(10, okBtn, cancelBtn);

清单 10-13 中的程序展示了如何使用VBox。它给一个VBox增加了一个Label,一个TextField,两个Buttons。相邻子项之间的间距设置为 10px。10px 的填充用于保持VBox的边缘与其子项的边缘之间的距离。产生的窗口如图 10-22 所示。

img/336502_2_En_10_Fig22_HTML.jpg

图 10-22

一个带有标签、文本字段和两个按钮的 VBox

// VBoxTest.java
// ... full listing in the book's download area.

Listing 10-13Using the VBox Layout Pane

VBox 属性

VBox类声明了表 10-3 中列出的三个属性。

表 10-3

VBox 类中声明的属性

|

财产

|

类型

|

描述

alignment ObjectProperty<Pos> 它指定子元素相对于VBox内容区域的对齐方式。默认值为Pos.TOP_LEFT
fillWidth BooleanProperty 它指定可调整大小的子元素是调整大小以填充VBox的整个宽度,还是给它们自己喜欢的宽度。默认值为 true。
spacing DoubleProperty 它指定相邻子项之间的垂直间距。默认值为零。

对齐属性

使用alignment属性很简单。它指定了如何在VBox的内容区域内对齐子元素。默认情况下,VBox会为其内容分配足够的空间,以他们喜欢的大小来布局所有的孩子。当VBox变得比它的首选大小时,对齐属性的效果是显而易见的。

清单 10-14 中的程序使用一个带有两个ButtonVBox,它将VBox的对齐设置为Pos.BOTTOM_RIGHT。它将VBox的首选大小设置为比容纳其所有子节点所需的大小稍大一些,因此您可以看到对齐的效果。产生的窗口如图 10-23 所示。当您调整窗口大小时,子窗口在右下角区域保持对齐。

img/336502_2_En_10_Fig23_HTML.jpg

图 10-23

带有两个按钮和一个设置为 Pos 的对齐属性的 VBox。右下

// VBoxAlignment.java
// ... full listing in the book's download area.

Listing 10-14Using the VBox Alignment Property

fullwidth 属性

fillWidth属性指定VBox是水*扩展其子元素以填充其内容区域的宽度,还是将它们保持在自己喜欢的高度。请注意,该属性仅影响那些允许水*扩展的子节点。例如,默认情况下,Button的最大宽度被设置为其首选宽度,即使水*空间可用,Button也不会变得比其在VBox中的首选宽度更宽。如果你想要一个Button水*扩展,设置它的最大宽度为两倍。MAX_VALUE。默认情况下,TextField被设置为展开。因此,VBox内的TextField会随着VBox宽度的增加而变宽。如果您不希望可调整大小的子元素填充VBox内容区域的宽度,请将fillWidth属性设置为 false。运行清单 10-13 中的程序,尝试水*扩展窗口。随着窗口的扩展,TextField将水*扩展。

Tip

一个VBox的内容区域的首选宽度是其子元素的首选宽度的最大值。如果可调整大小的子级的最大宽度属性允许它们扩展,则它们会填充内容区域的整个宽度。否则,它们将保持其首选宽度。

在 GUI 应用程序中,经常需要在垂直列中排列一组Button并使它们大小相同。您需要将buttons添加到一个VBox中,并将所有按钮的最大宽度设置为Double.MAX_VALUE,这样它们就可以增长以匹配组中最宽的button的宽度。清单 10-15 中的程序展示了如何实现这一点。图 10-24 显示了该窗口。

img/336502_2_En_10_Fig24_HTML.png

图 10-24

带有一些控件的 VBox,用户可以在其中更改 fillWidth 属性

// VBoxFillWidth.java
// ... full listing in the book's download area.

Listing 10-15Using the fillWidth Property of a VBox

当您水*展开清单 10-16 中的VBox时,所有按钮都会增长以填充可用的额外空间。为了防止VBox在水*方向扩展时按钮变大,可以在HBox中添加VBox,并在场景中添加HBox

Tip

您可以通过嵌套HBoxVBox布局窗格来创建强大的视觉效果。您还可以在GridPane的一列中添加按钮(或任何其他类型的节点),使它们大小相同。更多详情请参考“了解网格板章节。

间距属性

spacing 属性指定了VBox中相邻子元素之间的垂直距离。默认情况下,它设置为 0px。它可以在构造器中设置或者使用setSpacing()方法。

在 VBox 中为子对象设置约束

VBox支持两种类型的约束, vgrowmargin ,可以在每个子节点上单独设置。vgrow约束指定当额外空间可用时,子节点是否垂直扩展。边距约束指定子节点边缘之外的空间。VBox类提供了setVgrow()setMargin()静态方法来指定这些约束。您可以使用null和这些方法单独移除约束。使用clearConstraints(Node child)方法一次删除子节点的两个约束。

让孩子垂直成长

默认情况下,VBox中的孩子会得到他们喜欢的身高。如果VBox垂直扩展,它的子节点可以获得额外的可用空间,前提是它们的vgrow优先级设置为增长。如果一个VBox被垂直展开,并且它的子节点都没有设置vgrow约束,那么额外的空间就没有被使用。

通过指定子节点和优先级,使用VBox类的setVgrow()静态方法设置子节点的 vgrow 优先级:

VBox root = new VBox(10);
TextArea desc = new TextArea();

// Let the TextArea always grow vertically
root.setVgrow(desc, Priority.ALWAYS);

要重置子节点的 vgrow 优先级,使用null作为优先级:

// Stop the TextArea from growing horizontally
root.setVgrow(desc, null);

清单 10-16 中的程序展示了如何设置TextAreaPriority.ALWAYS的优先级,这样当VBox展开时,它可以占用所有额外的垂直空间。图 10-25 显示了初始窗口和扩展窗口。请注意,在窗口垂直展开后,Label保持在它的首选高度。

img/336502_2_En_10_Fig25_HTML.jpg

图 10-25

TextArea 设置为始终垂直增长的 VBox

// VBoxVGrow.java
// ... full listing in the book's download area.

Listing 10-16Letting a TextArea Grow Vertically

为儿童设置边距

您可以使用setMargin()静态方法为VBox的子对象设置边距:

Button okBtn = new Button("OK");
Button cancelBtn = new Button("Cancel");
VBox vbox = new VBox(okBtn, cancelBtn);

// Set margins for OK and cancel buttons
Insets margin = new Insets(5);
VBox.setMargin(okBtn, margin);
VBox.setMargin(cancelBtn, margin);
...
// Remove margins for okBtn
VBox.setMargin(okBtn, null);

了解 FlowPane

一个FlowPane是一个简单的布局窗格,它以指定的宽度或高度将它的子元素排列成行或列。它让其子元素水*或垂直流动,因此得名“流动窗格”您可以指定首选的换行长度,这是水*流的首选宽度和垂直流的首选高度,内容在垂直流中换行。一个FlowPane用在孩子的相对位置不重要的场合:比如显示一系列图片或者按钮。一个FlowPane给所有的孩子他们喜欢的尺寸。行和列可以具有不同的高度和宽度。您可以自定义行中子级的垂直对齐和列中子级的水*对齐。

Tip

水*FlowPane中的子元素可以从左到右或从右到左排列,这由在Node类中声明的nodeOrientation属性控制。该属性的默认值设置为NodeOrientation.LEFT_TO_RIGHT。如果希望子元素从右向左流动,请将该属性设置为NodeOrientation.RIGHT_TO_LEFT。这适用于所有按行排列子元素的布局窗格(如HBoxTilePane等)。).

可以设置为水*或垂直的FlowPane的方向决定了其内容的流向。在水*的FlowPane中,内容按行流动。在垂直的FlowPane中,内容以列的形式流动。图 10-26 和 10-27 显示了一个带有十个按钮的FlowPane。按钮是按照它们被标记的顺序添加的。即Button 1加在Button 2之前。图 10-26 中的FlowPane为水*方向,而图 10-27 中的FlowPane为垂直方向。默认情况下,FlowPane具有水*方向。

img/336502_2_En_10_Fig27_HTML.jpg

图 10-27

显示十个按钮的垂直流程窗格

img/336502_2_En_10_Fig26_HTML.jpg

图 10-26

显示十个按钮的水*流程窗格

创建流程窗格对象

FlowPane类提供了几个构造器来创建具有指定方向(水*或垂直)、子对象之间的指定水*和垂直间距以及指定初始子对象列表的FlowPane对象:

// Create an empty horizontal FlowPane with 0px spacing
FlowPane fpane1 = new FlowPane();

// Create an empty vertical FlowPane with 0px spacing
FlowPane fpane2 = new FlowPane(Orientation.VERTICAL);

// Create an empty horizontal FlowPane with 5px horizontal and 10px
// vertical spacing
FlowPane fpane3 = new FlowPane(5, 10);

// Create an empty vertical FlowPane with 5px horizontal and 10px
// vertical spacing
FlowPane fpane4 = new FlowPane(Orientation.VERTICAL, 5, 10);

// Create a horizontal FlowPane with two Buttons and 0px spacing
FlowPane fpane5 =
    new FlowPane(new Button("Button 1"), new Button("Button 2"));

清单 10-17 中的程序展示了如何创建一个FlowPane并添加子节点。它增加了十个Buttons并使用 5px 水*和 10px 垂直间隙。窗口如图 10-28 所示。

img/336502_2_En_10_Fig28_HTML.jpg

图 10-28

带有十个按钮的水*窗格,使用 5px hgap 和 10px vgap

// FlowPaneTest.java
// ... full listing in the book's download area.

Listing 10-17Using a Horizontal FlowPane

流程窗格属性

表 10-4 列出了几个FlowPane类属性,用于定制其子元素的布局。

表 10-4

FlowPane 类中声明的属性列表

|

财产

|

类型

|

描述

alignment ObjectProperty<Pos> 它指定了行和列相对于FlowPane内容区域的对齐方式。默认值为Pos.TOP_LEFT
rowValignment ObjectProperty<VPos> 它指定了水*FlowPane中每一行内的子元素的垂直对齐。对于垂直FlowPane它被忽略。
columnHalignment ObjectProperty<HPos> 它指定了垂直FlowPane中每一列内的子元素的水*对齐方式。对于水*FlowPane它被忽略。
hgap, vgap DoubleProperty 它们指定子对象之间的水*和垂直间距。默认值为零。
orientation ObjectProperty <Orientation> 它指定了FlowPane的方向。默认为HORIZONTAL
prefWrapLength DoubleProperty 这是内容应该换行的水*方向的首选宽度FlowPane和垂直方向的首选高度FlowPane。默认值为 400。

对齐属性

FlowPanealignment属性控制其内容的对齐方式。一个Pos值包含垂直对齐(vpos)和水*对齐(hpos)。例如,Pos.TOP_LEFT将垂直对齐作为顶部,水*对齐作为左侧。在水*FlowPane中,每行使用alignment,的 hpos 值对齐,而行(整个内容)使用 vpos 值对齐。在竖排FlowPane中,使用alignment,的 vpos 值对齐每一列,使用 hpos 值对齐各列(整个内容)。

清单 10-18 中的程序在一个HBox中显示三个FlowPane。每个FlowPane都有不同的排列。每个FlowPane中的Text节点显示使用的校准。图 10-29 显示了该窗口。

img/336502_2_En_10_Fig29_HTML.jpg

图 10-29

为其内容使用不同对齐方式的流程窗格

// FlowPaneAlignment.java
// ... full listing in the book's download area.

Listing 10-18Using the Alignment Property of the FlowPane

rowValignment 和 columnHalignment 属性

一个FlowPane以他们喜欢的尺寸布局它的孩子。行和列可以具有不同的大小。您可以使用rowValignmentcolumnHalignment属性对齐每行或每列中的子元素。

在一个横排FlowPane中,一排的孩子可能身高不同。行的高度是该行中所有子级的最大首选高度。rowValignment属性允许您指定每行中子元素的垂直对齐方式。它的值可以被设置为VPos枚举的常量之一:BASELINETOPCENTERBOTTOM。如果子节点的最大高度值允许垂直扩展,则子节点将被扩展以填充行的高度。如果rowValignment属性被设置为VPos.BASELINE,子元素的大小将被调整到它们的首选高度,而不是扩展以填充行的整个高度。

在垂直FlowPane中,一列中的子元素可能有不同的宽度。列的宽度是该列中所有子列的最大首选宽度。columnHalignment属性允许您指定每列中子元素的水*对齐方式。它的值可以设置为HPos枚举的常量之一:LEFTRIGHTCENTER。如果子节点的最大宽度值允许水*扩展,子节点将被扩展以填充列的宽度。

清单 10-19 中的程序创建了三个FlowPane并将它们添加到一个HBox中。图 10-30 显示了该窗口。前两个FlowPanes有水*方向,最后一个有垂直方向。行和列的对齐显示在Text节点,而FlowPane的方向显示在TextArea节点。

img/336502_2_En_10_Fig30_HTML.jpg

图 10-30

使用不同行和列对齐方式的流程窗格

// FlowPaneRowColAlignment.java
// ... full listing in the book's download area.

Listing 10-19Using Row and Column Alignments in a FlowPane

hgap 和 vgap 属性

使用hgapvgap属性很简单。在水*FlowPane中,hgap属性指定一行中相邻子元素之间的水*间距,vgap属性指定相邻行之间的间距。在垂直FlowPane中,hgap属性指定相邻列之间的水*间距,vgap属性指定一列中相邻子列之间的间距。您可以在构造器中或使用 setter 方法来设置这些属性。我们已经在本节讨论的例子中使用了这些属性。

// Create a FlowPane with 5px hgap and 10px vgap
FlowPane fpane = new FlowPane(5, 10);
...
// Change the hgap to 15px and vgap to 25px
fpane.setHgap(15);
fpane.setVgap(25);

方向属性

属性指定了一个FlowPane中的内容流。如果设置为默认值Orientation.HORIZONTAL,内容将按行排列。如果设置为Orientation.VERTICAL,内容将按列流动。您可以在构造器中指定orientation,或者使用 setter 方法:

// Create a horizontal FlowPane
FlowPane fpane = new FlowPane();
...
// Change the orientation of the FlowPane to vertical
fpane.setOrientation(Orientation.VERTICAL);

prefWrapLength 属性

prefWrapLength属性是内容应该换行的水*FlowPane中的首选宽度或垂直FlowPane中的首选高度。这仅用于计算FlowPane的首选尺寸。默认为 400。将该属性的值视为调整您的FlowPane大小的提示。假设您将该值设置为小于子节点的最大首选宽度或高度。在这种情况下,不会考虑该值,因为行不能短于水*方向上最宽的子节点FlowPane,或者列不能短于垂直方向上最高的子节点FlowPane。如果 400px 对您的FlowPane来说太宽或太高,请将该值设置为一个合理的值。

流窗格的内容偏差

注意,水*FlowPane中的行数取决于其宽度,垂直FlowPane中的列数取决于其高度。也就是说,水*FlowPane具有水*内容偏差,垂直FlowPane具有垂直内容偏差。因此,当你得到一个FlowPane的大小时,一定要考虑到它的内容偏差。

了解边框窗格

A BorderPane将其布局区域分为五个区域:顶部、右侧、底部、左侧和中心。您最多可以在五个区域中的每个区域放置一个节点。图 10-31 显示五个Buttons放置在BorderPaneone Button的五个区域中。Button的标签与它们所在的地区相同。任何区域都可能是null。如果一个区域是null,则没有空间分配给它。

img/336502_2_En_10_Fig31_HTML.jpg

图 10-31

边框的五个区域

在典型的 Windows 应用程序中,屏幕使用五个区域来放置内容:

  • 顶部的菜单或工具栏

  • 底部的状态栏

  • 左侧的导航面板

  • 右侧的附加信息

  • 中心的主要内容

一个BorderPane满足一个典型的基于 Windows 的 GUI 屏幕的所有布局要求。这就是为什么BorderPane通常被用作场景的根节点。通常,一个窗口中有五个以上的节点。如果您要在BorderPane的五个区域中的一个区域放置多个节点,请将这些节点添加到布局窗格中,例如HBoxVBox等。,然后将布局窗格添加到BorderPane的所需区域。

A BorderPane对其子节点使用以下调整大小策略:

  • 顶部和底部区域中的子区域将被调整到其首选高度。它们的宽度被扩展以填充可用的额外水*空间,只要子元素的最大宽度允许扩展它们的宽度超过它们的首选宽度。

  • 右区域和左区域中的子区域被调整到它们的首选宽度。他们的高度被延长以填充额外的垂直空间,只要儿童的最大高度允许他们的高度超过他们的首选高度。

  • 中间的子节点将在两个方向上填充剩余的可用空间。

如果将BorderPane的尺寸调整到比其首选尺寸更小,则其中的子元素可能会重叠。重叠规则基于添加子项的顺序。子对象是按照添加的顺序绘制的。这意味着一个子节点可能会与之前添加的所有子节点重叠。假设区域按右、中、左的顺序填充。左区域可以与中心区域和右区域重叠,并且中心区域可以与右区域重叠。

Tip

您可以为区域内的所有子对象设置对齐方式。您可以为孩子设置边距。和所有布局窗格一样,你也可以用 CSS 来设计一个BorderPane的样式。

创建边框窗格对象

BorderPane类提供构造器来创建有或没有子对象的BorderPane对象:

// Create an empty BorderPane
BorderPane bpane1 = new BorderPane();

// Create a BorderPane with a TextArea in the center
TextArea center = new TextArea();
BorderPane bpane2 = new BorderPane(center);

// Create a BorderPane with a Text node in each of the five regions
Text center = new Text("Center");
Text top = new Text("Top");
Text right = new Text("Right");
Text bottom = new Text("Bottom");
Text left = new Text("Left");
BorderPane bpane3 = new BorderPane(center, top, right, bottom, left);

BorderPane类声明了名为 top、right、bottom、left 和 center 的五个属性,这些属性存储了五个区域中五个孩子的引用。使用这些属性的设置器向五个区域中的任何一个添加子节点。例如,使用setTop(Node topChild)方法向顶部区域添加一个子节点。要获取这五个区域中任何一个区域的子元素的引用,可以使用这些属性的 getters。例如,getTop()方法返回顶部区域中子节点的引用。

// Create an empty BorderPane and add a text node in each of the five regions
BorderPane bpane = new BorderPane();
bpane.setTop(new Text("Top"));
bpane.setRight(new Text("Right"));
bpane.setBottom(new Text("Bottom"));
bpane.setLeft(new Text("Left"));
bpane.setCenter(new Text("Center"));

Tip

不要使用BorderPanegetChildren()方法返回的ObservableList<Node>BorderPane添加子节点。添加到此列表中的孩子将被忽略。请改用toprightbottomleftcenter属性。

清单 10-20 中的程序展示了如何创建一个BorderPane并添加子节点。它将子项添加到右侧、底部和中心区域。两个Label、一个TextField和一个TextArea被添加到中心区域。一个带有两个按钮的VBox被添加到右侧区域。显示状态的Label被添加到底部区域。顶部和左侧区域设置为nullBorderPane被设置为场景的根节点。图 10-32 所示为窗口。

img/336502_2_En_10_Fig32_HTML.jpg

图 10-32

在顶部、右侧、底部和中间区域使用一些控件的边框窗格

// BorderPaneTest.java
// ... full listing in the book's download area.

Listing 10-20Using the BorderPane Layout Pane

边框窗格属性

BorderPane类声明了五个属性:toprightbottomleftcenter。它们是ObjectProperty<Node>型的。它们将子节点的引用存储在BorderPane的五个区域中。使用这些属性的设置器向BorderPane添加子元素。使用属性的 getters 获取任何区域中子节点的引用。

回想一下,不是所有的五个区域都需要有节点。如果一个区域没有节点,则不会为其分配空间。使用 null 从区域中移除子节点。例如,setTop(null)将删除已经添加到顶部区域的节点。默认情况下,所有区域都有空节点作为其子节点。

为 BorderPane 中的子对象设置约束

一个BorderPane允许你在单个子节点上设置对齐和边距约束。子节点的对齐是相对于其区域定义的。默认对齐方式:

  • Pos.TOP_LEFT为顶层子节点

  • Pos.BOTTOM_LEFT为底层子节点

  • Pos.TOP_LEFT为左侧子节点

  • Pos.TOP_RIGHT为右子节点

  • Pos.CENTER为中心子节点

使用BorderPane类的setAlignment(Node child, Pos value)静态方法为子级设置对齐方式。getAlignment(Node child)静态方法返回子节点的对齐方式:

BorderPane root = new BorderPane();
Button top = new Button("OK");
root.setTop(top);

// Place the OK button in the top right corner (default is top left)
BorderPane.setAlignment(top, Pos.TOP_RIGHT);
...
// Get the alignment of the top node
Pos alignment = BorderPane.getAlignment(top);

使用BorderPane类的setMargin(Node child, Insets value)静态方法为子元素设置边距。getMargin(Node child)静态方法返回子节点的边距:

// Set 10px margin around the top child node
BorderPane.setMargin(top, new Insets(10));
...
// Get the margin of the top child node
Insets margin = BorderPane.getMargin(top);

使用null将约束重置为默认值。使用BorderPaneclearConstraints(Node child)静态方法一次重置子对象的所有约束:

// Clear the alignment and margin constraints for the top child node
BorderPane.clearConstraints(top);

了解堆栈面板

一个StackPane在一个节点堆栈中布局它的子节点。使用起来很简单。然而,它提供了覆盖节点的强大手段。子对象是按照添加的顺序绘制的。即先画第一个子节点;接下来绘制第二个子节点,依此类推。例如,在形状上覆盖文本就像使用StackPane一样简单:添加形状作为第一个子节点,添加文本作为第二个子节点。首先绘制形状,然后绘制文本,这使得文本看起来好像是形状的一部分。

图 10-33 显示了一个将StackPane设置为场景根节点的窗口。一个Rectangle形状和一个带有文本“矩形”的Text节点被添加到StackPane中。最后添加Text,覆盖Rectangle。外边框是StackPane的边框。虚线内边框是Rectangle的边框。

img/336502_2_En_10_Fig33_HTML.jpg

图 10-33

在堆栈窗格中覆盖矩形的文本节点

Tip

通过叠加不同类型的节点,您可以使用StackPane创建非常吸引人的 GUI。您可以在图像上叠加文本,以获得文本是图像一部分的效果。您可以叠加不同类型的形状来创建复杂的形状。请记住,覆盖其他节点的节点是最后添加到StackPane的。

StackPane的首选宽度是其最宽子对象的宽度。它的首选高度是它最高的孩子的高度。StackPane确实剪辑了它的内容。因此,它的子对象可能会被绘制到它的边界之外。

一个StackPane调整它的可调整大小的子元素的大小来填充它的内容区域,只要它们的最大大小允许它们扩展超过它们的首选大小。默认情况下,一个StackPane将它的所有子元素对齐到其内容区域的中心。您可以单独更改子节点的对齐方式,也可以更改所有子节点的对齐方式以使用相同的对齐方式。

创建 StackPane 对象

StackPane类提供构造器来创建有或没有子对象的对象:

// Create an empty StackPane
StackPane spane1 = new StackPane();

// Add a Rectangle and a Text to the StackPane
Rectangle rect = new Rectangle(200, 50);
rect.setFill(Color.LAVENDER);
Text text = new Text("A Rectangle");
spane1.getChildren().addAll(rect, text);

// Create a StackPane with a Rectangle and a Text
Rectangle r = new Rectangle(200, 50);
r.setFill(Color.LAVENDER);
StackPane spane2 = new StackPane(r, new Text("A Rectangle"));

清单 10-21 中的程序展示了如何创建一个StackPane。它给一个StackPane增加了一个Rectangle和一个Text。首先添加的是Rectangle,因此它与Text重叠。图 10-33 显示了该窗口。

// StackPaneTest.java
// ... full listing in the book's download area.

Listing 10-21Using StackPane

您必须以特定的顺序将孩子添加到StackPane中,以创建所需的覆盖。孩子是按照他们在列表中的顺序绘制的。以下两条语句不会得到相同的结果:

// Overlay a Text on a Rectangle
spane1.getChildren().addAll(rect, text);

// Overlay a Rectangle on a Text
spane1.getChildren().addAll(text, rect);

如果Text小于Rectangle,在Text上叠加Rectangle将隐藏Text。如果Text尺寸大于Rectangle,则TextRectangle边界之外的部分将可见。

清单 10-22 中的程序显示了覆盖规则如何在StackPane中工作。createStackPane()方法创建一个带有RectangleTextStackPane。它接受文本节点的文本、Rectangle的不透明度和一个boolean值,该值指示是否应该首先将Rectangle添加到StackPane。start 方法创建五个StackPane并将它们添加到一个HBox中。图 10-34 为窗口。

  • 在第一个StackPane中,文本覆盖在矩形上。首先绘制矩形,然后绘制文本。两者都可见。

  • 在第二个StackPane中,矩形覆盖在文本上。当矩形绘制在文本上并且比文本大时,文本隐藏在矩形后面。

  • 在第三个StackPane中,矩形覆盖在文本上。与第二个 StackPane 不同,文本是可见的,因为我们将矩形的不透明度设置为 0.5,这使得它的透明度为 50%。

  • 在第四个StackPane中,矩形覆盖在一个大文本上。矩形的不透明度为 100%。因此,我们只能看到矩形边界之外的文本部分。

  • 在第五个StackPane中,矩形覆盖在一个大文本上。矩形的不透明度为 50%。我们可以看到整篇文章。矩形边界内的文本可见性为 50%,边界外的文本可见性为 100%。

img/336502_2_En_10_Fig34_HTML.jpg

图 10-34

在文本上覆盖一个矩形,反之亦然

// StackPaneOverlayTest.java
// ... full listing in the book's download area.

Listing 10-22Overlaying Rules in a StackPane

堆栈面板属性

StackPane类有一个ObjectProperty<Pos>类型的alignment属性。该属性定义了StackPane内容区域内所有子元素的默认对齐方式。默认情况下,它的值被设置为Pos.CENTER,这意味着默认情况下,所有子元素都在StackPane的内容区域的中心对齐。这就是我们在前面的例子中看到的。如果不希望所有子对象都使用默认对齐方式,可以将其更改为任何其他对齐方式值。请注意,更改alignment属性的值会为所有子对象设置默认对齐方式。

通过设置其alignment约束,单个子对象可以覆盖默认对齐。我们将在下一节讨论如何在子节点上设置对齐约束。

StackPane除了覆盖节点还有其他几个用途。每当您需要在特定位置对齐一个节点或一组节点时,请尝试使用StackPane。例如,如果您想在屏幕中央显示文本,请使用带有Text节点的StackPane作为场景的根节点。StackPane 负责在调整窗口大小时将文本保持在中心。如果没有StackPane,你将需要使用绑定来保持文本位于窗口的中心。

清单 10-23 中的程序在一个HBox中使用了五个StackPane。每个StackPane都有一个覆盖着TextRectangleStackPane及其所有子节点的对齐方式被用作文本节点的文本。图 10-35 为窗口。请注意,StackPane s 中的Rectangle s 比Text s 大。因此,Rectangle s 占据了 StackPanes 的整个内容区域,并且它们似乎不受对齐属性的影响。

img/336502_2_En_10_Fig36_HTML.jpg

图 10-36

在 StackPane 中使用不同对齐约束的子项

img/336502_2_En_10_Fig35_HTML.jpg

图 10-35

使用不同对齐值的堆栈窗格

// StackPaneAlignment.java
// ... full listing in the book's download area.

Listing 10-23Using the Alignment Property of a StackPane

为孩子设置约束

一个StackPane允许你在单个子节点上设置对齐和边距约束。子节点的对齐是相对于StackPane的内容区域定义的。

您应该能够区分StackPanealignment属性和其子元素上的对齐约束。alignment房产影响所有孩子。默认情况下,它的值用于对齐子级。子节点上的对齐约束覆盖了由alignment属性设置的默认对齐值。子节点上的对齐约束只影响该子节点的对齐,而alignment属性影响所有子节点。当绘制一个子节点时,JavaFX 使用子节点的对齐约束在StackPane的内容区域内对齐它。如果未设置对齐约束,则使用StackPanealignment属性。

Tip

StackPanealignment属性的默认值为Pos.CENTER。子对象对齐约束的默认值是null

使用StackPane类的setAlignment(Node child, Pos value)静态方法为子对象设置对齐约束。getAlignment(Node child)静态方法返回子节点的对齐方式,参见清单 10-24 和图 10-36 。:

// StackPaneAlignmentConstraint.java
// ... full listing in the book's download area.

Listing 10-24Using the Alignment Constraints for Children in a StackPane

// Place a Text node in the top left corner of the StackPane
Text topLeft = new Text("top-left");
StackPane.setAlignment(topLeft, Pos.TOP_LEFT);
StackPane root = new StackPane(topLeft);
...
// Get the alignment of the topLeft node
Pos alignment = StackPane.getAlignment(topLeft);

使用StackPane类的setMargin(Node child, Insets value)静态方法为子级设置边距。getMargin(Node child)静态方法返回子节点的边距:

// Set 10px margin around the topLeft child node
StackPane.setMargin(topLeft, new Insets(10));
...
// Get the margin of the topLeft child node
Insets margin = StackPane.getMargin(topLeft);

使用null将约束重置为默认值。使用StackPaneclearConstraints(Node child)静态方法一次重置子对象的所有约束:

// Clear the alignment and margin constraints for the topLeft child node
StackPane.clearConstraints(topLeft);

清除子节点的所有约束后,它将使用StackPanealignment属性的当前值作为其对齐方式,并使用 0px 作为边距。

了解 TilePane

一个TilePane把它的子节点放在一个统一大小的网格中,这个网格被称为瓦片。TilePane的工作方式类似于FlowPane的工作方式,但有一点不同:在FlowPane中,行和列可以有不同的高度和宽度,而在TilePane中,所有的行都有相同的高度,所有的列都有相同的宽度。最宽的子节点的宽度和最高的子节点的高度是 TilePane 中所有图块的默认宽度和高度。

可以设置为水*或垂直的TilePane的方向决定了其内容的流向。默认情况下,TilePane具有水*方向。在水*的TilePane中,内容按行流动。行中的内容可以从左到右(默认)或从右到左排列。在垂直TilePane中,内容以列的形式流动。图 10-37 和 10-38 显示水*和垂直TilePanes

img/336502_2_En_10_Fig38_HTML.jpg

图 10-38

显示一年中月份的垂直*铺窗格

img/336502_2_En_10_Fig37_HTML.jpg

图 10-37

显示一年中月份的水**铺窗格

您可以使用TilePane的属性或在单个子节点上设置约束来自定义其布局:

  • 您可以覆盖*铺的默认大小。

  • 您可以定制TilePane的整个内容在其内容区域内的对齐方式,默认为Pos.TOP_LEFT

  • 您还可以定制每个子节点在其 tile 中的对齐方式,默认为Pos.CENTER

  • 您可以指定相邻行和列之间的间距,默认为 0px。

  • 您可以指定水*方向的首选列数TilePane和垂直方向的首选行数TilePane。首选行数和列数的默认值是五。

创建*铺窗格对象

TilePane类提供了几个构造器来创建具有指定方向(水*或垂直)、子对象之间的指定水*和垂直间距以及指定初始子对象列表的TilePane对象:

// Create an empty horizontal TilePane with 0px spacing
TilePane tpane1 = new TilePane();

// Create an empty vertical TilePane with 0px spacing
TilePane tpane2 = new TilePane(Orientation.VERTICAL);

// Create an empty horizontal TilePane with 5px horizontal
// and 10px vertical spacing
TilePane tpane3 = new TilePane(5, 10);

// Create an empty vertical TilePane with 5px horizontal
// and 10px vertical spacing
TilePane tpane4 = new TilePane(Orientation.VERTICAL, 5, 10);

// Create a horizontal TilePane with two Buttons and 0px spacing
TilePane tpane5 = new TilePane(
    new Button("Button 1"), new Button("Button 2"));

清单 10-25 中的程序展示了如何创建一个TilePane并添加子节点。它使用来自java.time包的Month枚举来获取 ISO 月份的名称。产生的窗口如图 10-37 所示。

// TilePaneTest.java
// ... full listing in the book's download area.

Listing 10-25Using TilePane

您可以修改清单 10-25 中的代码,得到图 10-38 中的窗口。您需要将TilePane的方向指定为Orientation.VERTICAL,并使用 3 作为首选行数:

import javafx.geometry.Orientation;
...
double hgap = 5.0;
double vgap = 5.0;
TilePane root = new TilePane(Orientation.VERTICAL, hgap, vgap);
root.setPrefRows(3);

TilePane 属性

TilePane类包含几个属性,如表 10-5 中所列,这些属性允许你定制其子类的布局。

表 10-5

TilePane 类中声明的属性列表

|

财产

|

类型

|

描述

alignment ObjectProperty<Pos> 它指定了TilePane的内容相对于其内容区域的对齐方式。默认为Pos.TOP_LEFT
tileAlignment ObjectProperty<Pos> 它指定了*铺中所有子元素的默认对齐方式。默认为Pos.CENTER
hgap, vgap DoubleProperty hgap属性指定一行中相邻子元素之间的水*间距。属性指定一列中相邻子元素之间的垂直间距。两个属性的默认值都是零。
orientation ObjectProperty<Orientation> 它指定了TilePane的方向–水*或垂直。默认为HORIZONTAL
prefRows IntegerProperty 它指定了垂直TilePane的首选行数。对于水*TilePane它被忽略。
prefColumns IntegerProperty 它指定了水*TilePane的首选列数。对于垂直TilePane它被忽略。
prefTileWidth DoubleProperty 它指定了每个单幅图块的首选宽度。默认情况下,使用最宽子项的宽度。
prefTileHeight DoubleProperty 它指定了每个单幅图块的首选高度。默认情况下,使用最高的孩子的高度。
tileHeight ReadOnlyDoubleProperty 它是一个只读属性,存储每个图块的实际高度。
tileWidth ReadOnlyDoubleProperty 它是一个只读属性,存储每个单幅图块的实际宽度。

对齐属性

TilePanealignment属性控制其内容在其内容区域内的对齐方式。当TilePane的大小大于其内容时,您可以看到该属性的效果。该属性的工作方式与FlowPane的对齐属性相同。更多细节和说明请参见FlowPanealignment属性描述。

titlealignment 属性

tileAlignment属性指定子元素在它们的图块中的默认对齐方式。请注意,该属性会影响小于图块大小的子对象。此属性会影响*铺中所有子对象的默认对齐方式。这可以通过设置单个子对象的对齐约束来覆盖。清单 10-26 中的程序展示了如何使用tileAlignment属性。它显示显示窗口,如图 10-39 所示,有两个TilePane,一个tileAlignment属性设置为Pos.CENTER,另一个Pos.TOP_LEFT

img/336502_2_En_10_Fig39_HTML.jpg

图 10-39

使用 titlealignment 属性

// TilePaneTileAlignment.java
// ... full listing in the book's download area.

Listing 10-26Using the TileAlignment Property of TilePane

hgap 和 vgap 属性

hgapvgap属性指定相邻列和相邻行之间的间距。它们默认为零。它们可以在构造器中指定,或者使用TilePanesetHgap(double hg)setVgap(double vg)方法指定。

方向属性

属性指定了一个TilePane中的内容流。如果设置为默认值Orientation.HORIZONTAL,内容将按行排列。如果设置为Orientation.VERTICAL,内容将按列流动。您可以在构造器中指定orientation,或者使用 setter 方法:

// Create a horizontal TilePane
TilePane tpane = new TilePane();
...
// Change the orientation of the TilePane to vertical

tpane.setOrientation(Orientation.VERTICAL);

prefRows 和 prefColumns 属性

prefRows属性指定垂直TilePane的首选行数。对于一个横TilePane来说是忽略的。

prefColumns指定了水*TilePane的首选列数。对于一个垂直的TilePane,它被忽略。

prefRowsprefColumns的默认值为 5。建议您为这些属性使用合理的值。

请注意,这些属性仅用于计算TilePane的优选尺寸。如果TilePane的大小被调整到不同于其首选大小,这些值可能不会反映实际的行数或列数。在列表 10-26 中,我们指定了三列作为首选列数。如果您将列表 10-26 显示的窗口调整到更小的宽度,您可能只得到一两列,行数也会相应增加。

Tip

调用FlowPaneprefWrapLength属性,该属性用于确定FlowPane的首选宽度或高度。在TilePane中,prefRowsprefColumns属性的作用是一样的,在流程窗格中也是如此。

prefTileWidth 和 prefTileHeight 属性

A TilePane根据最宽和最高的孩子计算其瓷砖的首选尺寸。您可以使用prefTileWidthprefTileHeight属性覆盖计算出的*铺宽度和高度。他们默认为Region.USE_COMPUTED_SIZE。如果它们的最小和最大尺寸允许它们被调整,那么TilePane试图调整它的子元素的大小以适合*铺的尺寸。

// Create a TilePane and set its preferred tile width and height to 40px
TilePane tpane = new TilePane();
tpane.setPrefTileWidth(40);
tpane.setPrefTileHeight(40);

tileWidth 和 tileHeight 属性

tileWidthtileHeight属性指定每个图块的实际宽度和高度。它们是只读属性。如果您指定了prefTileWidthprefTileHeight属性,它们将返回它们的值。否则,它们会返回计算出的图块大小。

在 TilePane 中为子级设置约束

一个TilePane允许你在单个子节点上设置对齐和边距约束。子节点的对齐是在包含该子节点的图块中定义的。

您应该能够区分这三者:

  • 一个TilePanealignment属性

  • TilePanetileAlignment属性

  • TilePane的单个子对象的对齐约束

alignment属性用于对齐TilePane的内容区域内的内容(所有子内容)。它影响了整个TilePane的内容。

默认情况下,tileAlignment属性用于对齐图块中的所有子元素。修改此属性会影响所有子级。

子节点上的对齐约束用于在其图块内对齐子节点。它只影响设置它的子节点。它覆盖了使用TilePanetileAlignment属性设置的子节点的默认对齐值。

Tip

一个TilePanetileAlignment属性的默认值是Pos.CENTER。子对象对齐约束的默认值是null

使用TilePane类的setAlignment(Node child, Pos value)静态方法为孩子设置对齐约束。getAlignment(Node child)静态方法返回子节点的对齐方式:

// Place a Text node in the top left corner in a tile
Text topLeft = new Text("top-left");
TilePane.setAlignment(topLeft, Pos.TOP_LEFT);

TilePane root = new TilePane();
root.getChildren().add(topLeft);
...
// Get the alignment of the topLeft node
Pos alignment = TilePane.getAlignment(topLeft);

清单 10-27 中的程序给一个TilePane添加了五个按钮。标有“三”的按钮使用了一个定制的图块对齐约束Pos.BOTTOM_RIGHT。所有其他按钮都使用默认的*铺对齐方式,即Pos.CENTER。图 10-40 为窗口。

img/336502_2_En_10_Fig40_HTML.jpg

图 10-40

在 TilePane 中使用不同对齐约束的子级

// TilePaneAlignmentConstraint.java
// ... full listing in the book's download area.

Listing 10-27Using the Alignment Constraints for Children in a TilePane

使用TilePane类的setMargin(Node child, Insets value)静态方法为子级设置边距。getMargin(Node child)静态方法返回子节点的边距:

// Set 10px margin around the topLeft child node
TilePane.setMargin(topLeft, new Insets(10));
...
// Get the margin of the topLeft child node
Insets margin = TilePane.getMargin(topLeft);

使用null将约束重置为默认值。使用TilePaneclearConstraints(Node child)静态方法一次重置子对象的所有约束:

// Clear the tile alignment and margin constraints for the topLeft child node
TilePane.clearConstraints(topLeft);

清除子节点的所有约束后,它将使用TilePanetileAlignment属性的当前值作为其对齐方式,并使用 0px 作为边距。

理解网格

GridPane是最强大的布局窗格之一。随着权力而来的是复杂性。所以,学起来也有点复杂。

一个GridPane在一个动态的单元格网格中布置它的子元素,单元格按行和列排列。网格是动态的,因为网格中单元的数量和大小是根据子单元的数量确定的。它们取决于对孩子的约束。网格中的每个单元格都由其在列和行中的位置来标识。列和行的索引从零开始。子节点可以放置在跨越多个单元的网格中的任何位置。一行中的所有单元格高度相同。不同行中的单元可以具有不同的高度。一列中的所有单元格宽度相同。不同列中的单元格可能具有不同的宽度。默认情况下,一行的高度足以容纳其中最高的子节点。一个列的宽度足以容纳其中最宽的子节点。您可以自定义每行和每列的大小。GridPane还允许行与行之间的垂直间距和列与列之间的水*间距。

GridPane默认不显示网格线。出于调试目的,您可以显示网格线。图 10-41 显示了GridPane的三个实例。第一个GridPane只显示网格线,没有子节点。第二个GridPane显示单元格位置,由行和列索引标识。在该图中,(cM,rN)表示第(M+1)列和第(N+1)行的单元。例如,(c3,r2)表示第四列第三行的单元格。第三个GridPane显示了网格中的六个按钮。五个按钮横跨一行和一列;其中一个横跨两行一列。

img/336502_2_En_10_Fig41_HTML.jpg

图 10-41

GridPanes 仅包含网格、单元格位置以及放置在网格中的子元素

GridPane中,行从上到下被索引。最上面一行的索引为零。列从左到右或从右到左进行索引。如果GridPanenodeOrientation属性被设置为LEFT_TO_RIGHT,则最左边的列的索引为 0。如果设置为RIGHT_TO_LEFT,最右边的列的索引为零。图 10-41 中的第二个网格显示了索引为零的最左边的列,这意味着它的nodeOrientation属性从LEFT_TO_RIGHT开始设置。

Tip

关于GridPane经常被问到的一个问题是,“我们需要在GridPane中布置多少个单元,多大的单元?”答案很简单,但有时令初学者困惑。您可以为子级指定单元位置和单元跨度。GridPane会计算出单元格的数量(行和列)以及它们的大小。也就是说,GridPane根据您为子元素设置的约束来计算单元格的数量及其大小。

创建网格对象

GridPane类包含一个无参数的构造器。它创建一个空的GridPane,行和列之间的间距为 0px,将需要稍后添加的子元素放在内容区域的左上角:

GridPane gpane = new GridPane();

使网格线可见

GridPane类包含一个BooleanProperty类型的gridLinesVisible属性。它控制网格线的可见性。默认情况下,它设置为 false,并且栅格线不可见。它仅用于调试目的。当您想要查看子节点在网格中的位置时,请使用它。

GridPane gpane = new GridPane();
gpane.setGridLinesVisible(true); // Make grid lines visible

向 GridPane 添加子项

像大多数其他布局窗格一样,GridPane 将其子元素存储在一个由getChildren()方法返回引用的ObservableList<Node>中。你不应该把孩子直接添加到GridPane列表中。相反,您应该使用一种方便的方法将子对象添加到 GridPane 中。当您将孩子添加到GridPane时,您应该为他们指定约束。最小约束是列和行索引,以标识它们所在的单元格。

让我们首先来看看将孩子直接添加到GridPane的可观察列表中的效果。清单 10-28 包含了将三个按钮直接添加到一个GridPane的子列表中的程序。图 10-42 为窗口。请注意,这些按钮是重叠的。它们都被放置在相同的单元(c0,r0)中。它们按照添加到列表中的顺序绘制。

Tip

GridPane中,默认情况下,所有子代都添加到仅跨越一列和一行的第一个单元格(c0,r0)中,因此彼此重叠。它们是按照添加的顺序绘制的。

img/336502_2_En_10_Fig42_HTML.jpg

图 10-42

三个按钮直接添加到 GridPane 的子列表中

// GridPaneChildrenList.java
// ... full listing in the book's download area.

Listing 10-28Adding Children to the List of Children for a GridPane Directly

有两种方法可以解决清单 10-28 中的子元素重叠问题:

  • 我们可以在将它们添加到列表之前或之后设置它们的位置。

  • 我们可以使用GridPane类的便利方法,这些方法允许在向GridPane添加孩子时指定位置和其他约束。

设定儿童的位置

您可以使用GridPane类的以下三个静态方法之一来设置子节点的列和行索引:

  • public static void setColumnIndex(Node child, Integer value)

  • public static void setRowIndex(Node child, Integer value)

  • public static void setConstraints(Node child,int columnIndex, int rowIndex)

清单 10-29 中的程序是清单 10-28 中程序的修改版本。它将列索引和行索引添加到三个按钮中,因此它们位于一行中不同的列中。图 10-43 为窗口。

img/336502_2_En_10_Fig43_HTML.jpg

图 10-43

将三个按钮直接添加到 GridPane 中,然后设置它们的位置

// GridPaneChildrenPositions.java
// ... full listing in the book's download area.

Listing 10-29Setting Positions for Children in a GridPane

使用方便的方法添加孩子

GridPane类包含以下方便的方法来添加带有约束的子元素:

  • void add(Node child, int columnIndex, int rowIndex)

  • void add(Node child, int columnIndex, int rowIndex, int colspan,int rowspan)

  • void addRow(int rowIndex, Node... children)

  • void addColumn(int columnIndex, Node... children)

add()方法允许您添加一个指定列索引、行索引、列跨度和行跨度的子节点。

addRow()方法将指定的children添加到由指定的rowIndex标识的行中。子节点是按顺序添加的。如果该行已经包含子行,则指定的children将按顺序追加。例如,如果GridPane在指定的行中没有子节点,它将在列索引 0 处添加第一个子节点,在列索引 1 处添加第二个子节点,依此类推。假设指定的行已经有两个子行占据了列索引 0 和 1。addRow()方法将从列索引 2 开始添加子元素。

Tip

使用addRow()方法添加的所有子元素只跨越一个单元格。可以使用GridPane类的setRowSpan(Node child, Integer value)setColumnSpan(Node child, Integer value)静态方法修改子节点的行和列跨度。修改子节点的行和列跨度时,请确保更新受影响子节点的行和列索引,以便它们不会重叠。

addColumn()方法将指定的children依次添加到由指定的columnIndex标识的列中。这个方法将子元素添加到列中,就像addRow()方法将子元素添加到行中一样。

下面的代码片段创建了三个GridPane并使用三种不同的方式向它们添加了四个按钮。图 10-44 显示了其中一个GridPane s。它们看起来都一样。

img/336502_2_En_10_Fig44_HTML.jpg

图 10-44

一个有四个按钮的格子

// Add a child node at a time
GridPane gpane1 = new GridPane();
gpane1.add(new Button("One"), 0, 0);         // (c0, r0)
gpane1.add(new Button("Two"), 1, 0);         // (c1, r0)
gpane1.add(new Button("Three"), 0, 1);       // (c0, r1)
gpane1.add(new Button("Four"), 1, 1);        // (c1, r1)

// Add a row at a time
GridPane gpane2 = new GridPane();
gpane2.addRow(0, new Button("One"), new Button("Two"));
gpane2.addRow(1, new Button("Three"), new Button("Four"));

// Add a column at a time
GridPane gpane3 = new GridPane();
gpane3.addColumn(0, new Button("One"), new Button("Three"));
gpane3.addColumn(1, new Button("Two"), new Button("Four"));

指定行和列跨度

子节点可能跨越多个行和列,这可以使用rowSpancolSpan约束来指定。默认情况下,子节点跨越一列和一行。这些约束可以在添加子节点时指定,或者稍后在GridPane类中使用以下任何方法指定:

  • void add(Node child, int columnIndex, int rowIndex, int colspan, int rowspan)

  • static void setColumnSpan(Node child, Integer value)

  • static void setConstraints(Node child, int columnIndex, int rowIndex, int columnspan, int rowspan)

setConstraints()方法被重载。该方法的其他版本也允许您指定列/行跨度。

GridPane类定义了一个名为REMAINING的常量,用于指定列/行跨度。这意味着子节点跨越了剩余的列或剩余的行。

下面的代码片段将一个Label和一个TextField添加到第一行。它将一个TextArea添加到第二行的第一列,其colSpanREMAINING。这使得TextArea占据了两列,因为添加到第一行的控件创建了两列。图 10-45 为窗口。

img/336502_2_En_10_Fig45_HTML.jpg

图 10-45

使用 GridPane 的 TextArea。保留为列跨度值

// Create a GridPane and set its background color to lightgray
GridPane root = new GridPane();
root.setGridLinesVisible(true);
root.setStyle("-fx-background-color: lightgray;");

// Add a Label and a TextField to the first row
root.addRow(0, new Label("First Name:"), new TextField());

// Add a TextArea in the second row to span all columns in row 2
TextArea ta = new TextArea();
ta.setPromptText("Enter your resume here");
ta.setPrefColumnCount(10);
ta.setPrefRowCount(3);
root.add(ta, 0, 1, GridPane.REMAINING, 1);

假设您在第一列中再添加两个孩子,以占据第三和第四列:

// Add a Label and a TextField to the first row
root.addRow(0, new Label("Last Name:"), new TextField());

现在,列的数量从两列增加到了四列。这将使TextArea占据四列,因为我们将其colSpan设置为REMAINING。图 10-46 显示新窗口。

img/336502_2_En_10_Fig46_HTML.jpg

图 10-46

使用 GridPane 的 TextArea。保留为列跨度值

使用 gridpanel 创建表单

最适合创建表单。让我们用一个GridPane构建一个表单。该表单将类似于图 10-32 所示的使用BorderPane创建的表单。我们的新表单将如图 10-47 所示。该图显示了窗口的两个实例:带有子窗口的窗体(左侧)和只带有网格的窗体(右侧)。只显示了带有网格的表单,因此您可以直观地看到网格中子节点的位置和跨度。

img/336502_2_En_10_Fig47_HTML.jpg

图 10-47

一个 GridPane,带有一些创建表单的控件

网格将有三列四行。它有七个孩子:

  • 第一排的一个Label、一个TextField和一个 OK 按钮

  • 第二排的一个Label和一个Cancel按钮

  • 第三排的一个TextArea

  • 第四排的一位

以下代码片段创建所有子节点:

// A Label and a TextField
Label nameLbl = new Label("Name:");
TextField nameFld = new TextField();

// A Label and a TextArea
Label descLbl = new Label("Description:");
TextArea descText = new TextArea();
descText.setPrefColumnCount(20);
descText.setPrefRowCount(5);

// Two buttons
Button okBtn = new Button("OK");
Button cancelBtn = new Button("Cancel");

第一行中的所有子元素仅跨越一个单元格。第二行的“描述”标签跨越了两列(c0 和 c1),而取消按钮只有一列。第三行的TextArea横跨两列(c0 和 c1)。第四行的Label跨越三列(c0、c1 和 c1)。以下代码片段将所有子节点放在网格中:

// Create a GridPane
GridPane root = new GridPane();

// Add children to the GridPane
root.add(nameLbl, 0, 0, 1, 1);   // (c0, r0, colspan=1, rowspan=1)
root.add(nameFld, 1, 0, 1, 1);   // (c1, r0, colspan=1, rowspan=1)
root.add(descLbl, 0, 1, 3, 1);   // (c0, r1, colspan=3, rowspan=1)
root.add(descText, 0, 2, 2, 1);  // (c0, r2, colspan=2, rowspan=1)
root.add(okBtn, 2, 0, 1, 1);     // (c2, r0, colspan=1, rowspan=1)
root.add(cancelBtn, 2, 1, 1, 1); // (c2, r1, colspan=1, rowspan=1)

// Let the status bar start at column 0 and take up all remaning columns
// (c0, r3, colspan=REMAININg, rowspan=1)
root.add(statusBar, 0, 3, GridPane.REMAINING, 1);

如果我们将GridPane添加到一个场景中,它将给出我们想要的表单外观,但不是想要的调整大小行为。调整窗口大小时,子窗口将无法正确调整大小。我们需要为一些孩子指定正确的调整大小行为:

  • 确定取消按钮的大小应该相同。

  • 输入姓名的TextField应该水*展开。

  • 输入描述的TextArea应水*和垂直展开。

  • 底部用作状态栏的Label要水*展开。

OKCancel 按钮大小相同很容易。默认情况下,GridPane会调整其子元素的大小以填充它们的单元格,前提是子元素的最大大小允许。Button的最大尺寸被夹紧到其首选尺寸。我们需要将 OK 按钮的最大尺寸设置得足够大,这样它就可以扩展以填充其单元格的宽度,这将与其列中最宽节点的首选宽度相同(按钮取消):

// The max width of the OK button should be big enough, so it can fill the
// width of its cell
okBtn.setMaxWidth(Double.MAX_VALUE);

默认情况下,当调整GridPane大小时,GridPane中的行和列保持其首选大小。它们的水*和垂直增长约束指定了当有额外空间可用时它们如何增长。为了让名称、描述和状态栏字段在GridPane展开时增长,我们将适当地设置它们的hgrowvgrow约束:

// The name field in the first row should grow horizontally
GridPane.setHgrow(nameFld, Priority.ALWAYS);

// The description field in the third row should grow vertically
GridPane.setVgrow(descText, Priority.ALWAYS);

// The status bar in the last row should fill its cell
statusBar.setMaxWidth(Double.MAX_VALUE);

GridPane水*扩展时,由 name 字段占据的第二列通过获取额外的可用宽度而增长。它使描述和状态栏字段填充第二列中生成的额外宽度。

GridPane垂直展开时,由描述字段占据的第三行通过获取额外的可用高度而增长。一个TextArea的最大大小是无界的。也就是说,它可以增长以填充两个方向的可用空间。清单 10-30 中的程序包含完整的代码。

// GridPaneForm.java
// ... full listing in the book's download area.

Listing 10-30Using a GridPane to Create Forms

网格板属性

GridPane类包含几个属性,如表 10-6 所列,用于定制其布局。

表 10-6

GridPane 类中声明的属性列表

|

财产

|

类型

|

描述

alignment ObjectProperty<Pos> 它指定了网格(GridPane的内容)相对于其内容区域的对齐方式。默认为Pos.TOP_LEFT
gridLinesVisible BooleanProperty 建议仅用于调试目。它控制网格线是否可见。它默认为 false。
hgap, vgap DoubleProperty 它们指定相邻列和行之间的间隙。属性指定了相邻列之间的水*间距。属性指定了相邻行之间的垂直间距。它们默认为零。

对齐属性

GridPanealignment属性控制其内容在其内容区域内的对齐方式。当GridPane的大小大于其内容时,您可以看到该属性的效果。该属性的工作方式与FlowPane的对齐属性相同。更多细节和说明请参见FlowPanealignment属性描述。

gridLinesVisible 属性

gridLinesVisible设置为 true 时,GridPane 中的网格线可见。否则,他们是看不见的。您应该仅出于调试目的使用此功能:

GridPane gpane = new GridPane();
gpane.setGridLinesVisible(true); // Make grid lines visible

有时,您可能希望展示网格而不展示给孩子看,以了解网格是如何形成的。您可以通过隐藏所有孩子来做到这一点。GridPane计算所有受管理子节点的网格大小,而不考虑它们的可见性。

下面的代码片段创建了一个GridPane并将gridLinesVisible属性设置为 true。它创建了四个Buttons,使它们不可见,并将它们添加到GridPane中。图 10-48 显示了GridPane作为根节点添加到场景时的窗口。

img/336502_2_En_10_Fig48_HTML.png

图 10-48

显示没有子网格的网格面板

GridPane root = new GridPane();

// Make the grid lines visible
root.setGridLinesVisible(true);

// Set the padding to 10px
root.setStyle("-fx-padding: 10;");

// Make the gridLInes

Button b1 = new Button("One");
Button b2 = new Button("Two");
Button b3 = new Button("Three");
Button b4 = new Button("Four and Five");

// Make all children invisible to see only grid lines
b1.setVisible(false);
b2.setVisible(false);
b3.setVisible(false);
b4.setVisible(false);

// Add children to the GridPane
root.addRow(1, b1, b2);
root.addRow(2, b3, b4);

hgap 和 vgap 属性

您可以分别使用hgapvgap属性指定相邻列和行之间的间距。默认情况下,它们为零。清单 10-31 中的程序使用了GridPane的这些属性。网格线清晰可见,可以清楚地显示间隙。图 10-49 为窗口。

img/336502_2_En_10_Fig49_HTML.jpg

图 10-49

使用 hgap 和 vgap 属性的 gridpanel

// GridPaneHgapVgap.java
// ... full listing in the book's download area.

Listing 10-31Using the hgap and vgap Properties of a GridPane

自定义列和行

您可以使用列和行约束自定义GridPane中的列和行。例如,对于列/行,您可以指定

  • 如何计算宽度/高度。应该根据其内容、固定的宽度/高度还是可用宽度/高度的百分比来计算?

  • 孩子应该填充列/行的宽度/高度吗?

  • GridPane的大小调整到大于其首选的宽度/高度时,列/行应该增长吗?

  • 列/行中的子元素应该如何在其布局区域(单元格)内对齐?

ColumnConstraints类的对象表示对列的约束,RowConstraints类的对象表示对行的约束。两个类都声明了几个表示约束的属性。表 10-7 和 10-8 列出了ColumnConstraintsRowConstraints类的属性和简要描述。

表 10-8

RowConstraints 类的属性

|

属性

|

类型

|

描述

fillHeight BooleanProperty 它指定行中的子级是否扩展到超出其首选高度,以填充行的高度。默认值为真。
valignment ObjectProperty<HPos> 它指定行中子级的默认垂直对齐方式。其默认值为null。默认情况下,一行中的所有子元素都与VPos.CENTER垂直对齐。行中的单个子节点可能会覆盖此约束。
vgrow ObjectProperty<Priority> 它指定行的垂直增长优先级。当GridPane的大小调整到大于其首选高度时,该属性用于为行提供额外的空间。如果设置了percentHeight属性,该属性的值将被忽略。
MinHeight,prefHeight,maxHeight DoubleProperty 它们指定行的最小、首选和最大高度。如果设置了percentHeight属性,这些属性的值将被忽略。这些属性的默认值被设置为USE_COMPUTED_SIZE。默认情况下,行的最小高度是该行中子级的最大最小高度;首选高度是该行中儿童首选高度的最大值;并且最大高度是该行中孩子的最大高度中的最小高度。
percentHeight DoubleProperty 它指定了行相对于GridPane内容区域高度的百分比。如果它被设置为一个大于零的值,那么该行的大小将被调整为其高度是GridPane的可用高度的百分比。如果设置了该属性,则minHeightprefHeightmaxHeightvgrow属性将被忽略。

表 10-7

ColumnConstraints 类的属性列表

|

财产

|

类型

|

描述

fillWidth BooleanProperty 它指定列中的子级是否扩展到超出其首选宽度,以填充列的宽度。默认值为 true。
halignment ObjectProperty<HPos> 它指定列中子级的默认水*对齐方式。其默认值为null。默认情况下,一列中的所有子元素都与HPos.LEFT水*对齐。列中的单个子节点可能会覆盖此约束。
hgrow ObjectProperty<Priority> 它指定列的水*增长优先级。当GridPane的大小调整到大于其首选宽度时,该属性用于为列提供额外的空间。如果设置了percentWidth属性,该属性的值将被忽略。
MinWidth,prefWidth,maxWidth DoubleProperty 它们指定列的最小、首选和最大宽度。如果设置了percentWidth属性,这些属性的值将被忽略。这些属性的默认值被设置为USE_COMPUTED_SIZE。默认情况下,列的最小宽度是该列中子列的最大最小宽度;首选宽度是列中子级的最大首选宽度;并且最大宽度是该列中子的最大宽度中的最小宽度。
percentWidth DoubleProperty 它指定列的宽度相对于GridPane内容区域宽度的百分比。如果它被设置为一个大于零的值,那么列的大小将被调整为等于GridPane的可用宽度的百分比。如果设置了该属性,则minWidthprefWidthmaxWidthhgrow属性将被忽略。

ColumnConstraints 和 RowConstraints 类提供了几个构造器来创建它们的对象。它们的无参数构造器用默认属性值创建它们的对象:

// Create a ColumnConstraints object with default property values
ColumnConstraints cc1 = new ColumnConstraints();

// Set the percentWidth to 30% and horizontal alignment to center
cc1.setPercentWidth(30);
cc1.setHalignment(HPos.CENTER);

如果您想要创建一个固定宽度/高度的列/行,您可以使用一个方便的构造器:

// Create a ColumnConstraints object with a fixed column width of 100px
ColumnConstraints cc2 = new ColumnConstraints(100);

// Create a RowConstraints object with a fixed row height of 80px
RowConstraints rc2 = new RowConstraints(80);

如果希望获得与固定宽度列相同的效果,可以通过将首选宽度设置为所需的固定宽度值,并将最小和最大宽度设置为使用首选宽度来实现,如下所示:

// Create a ColumnConstraints object with a fixed column width of 100px
ColumnConstraints cc3 = new ColumnConstraints();
cc3.setPrefWidth(100);
cc3.setMinWidth(Region.USE_PREF_SIZE);
cc3.setMaxWidth(Region.USE_PREF_SIZE);

下面的代码片段将列宽设置为GridPane宽度的 30%,并将列中子级的水*对齐方式设置为居中:

ColumnConstraints cc4 = new ColumnConstraints();
cc4.setPercentWidth(30);                // 30% width
cc4.setHalignment(HPos.CENTER);

GridPane中,不同列/行的宽度/高度可以不同地计算。一些列/行可以设置百分比宽度/高度,一些固定大小,一些可以选择基于它们的内容来计算它们的大小。在分配空间时,百分比大小是第一优先选择。例如,如果两列根据百分比设置宽度,而一列使用固定宽度,则可用宽度将首先分配给使用百分比宽度的两列,然后分配给使用固定宽度的列。

Tip

所有列/行的百分比宽度/高度之和可能超过 100。例如,允许将GridPane中的列宽百分比设置为 30%、30%、30%和 30%。在这种情况下,百分比值用作权重,四列中的每一列都将被赋予四分之一(30/120)的可用宽度。又如,如果列使用 30%、30%、60%和 60%作为百分比宽度,它们将被视为权重,分别分配给可用宽度的六分之一(30/180)、六分之一(30/180)、三分之一(60/180)和三分之一(60/180)。

A GridPane将列和行的约束存储在ColumnConstraints and RowConstraintsObservableList中。您可以使用getColumnConstraints()getRowConstraints()方法获取列表的引用。列表中特定索引处的元素存储了GridPane中相同索引处的列/行的约束对象。例如,列表中的第一个元素存储第一列/行的列/行约束,第二个元素存储第二列/行的列/行约束,等等。可以为某些列/行设置列/行约束,但不能为其他列/行设置。在这种情况下,将根据默认值计算缺少列/行约束的列/行的约束。下面的代码片段创建三个ColumnConstraints对象,设置它们的属性,并将它们添加到GridPane的列约束列表中。使用RowConstraints对象设置行约束将使用类似的逻辑。

// Set the fixed width to 100px
ColumnConstraints cc1 = new ColumnConstraints(100);

// Set the percent width to 30% and horizontal alignment to center
ColumnConstraints cc2 = new ColumnConstraints();
cc2.setPercentWidth(30);
cc1.setHalignment(HPos.CENTER);

// Set the percent width to 50%
ColumnConstraints cc3 = new ColumnConstraints();
cc3.setPercentWidth(30);

// Add all column constraints to the column constraints list
GridPane root = new GridPane();
root.getColumnConstraints().addAll(cc1, cc2, cc3);

清单 10-32 中的程序使用列和行约束来定制GridPane中的列和行。图 10-50 显示了调整大小后的窗口。

img/336502_2_En_10_Fig50_HTML.jpg

图 10-50

使用列和行约束的 gridpanel

// GridPaneColRowConstraints.java
// ... full listing in the book's download area.

Listing 10-32Using Column and Row Constraints in a GridPane

第一列的宽度设置为 100px 固定宽度。第二列和第三列各占宽度的 35%。如图所示,如果所需宽度(35% + 35% + 100px)小于可用宽度,则多余的宽度将不会被使用。第一列的水*对齐方式设置为居中,因此第一列中的所有按钮都水*居中对齐。其他两列中的按钮使用左作为水*对齐方式,这是默认设置。我们有三排。但是,程序只为前两行添加约束。第三行的约束将根据其内容进行计算。

设置列/行约束时,不能跳过中间的一些列/行。也就是说,必须从第一列/行开始按顺序设置列/行的约束。为约束的对象设置null会在运行时抛出一个NullPointerException。如果要跳过为列表中的行/列设置自定义约束,请将其设置为使用无参数构造器创建的 constraints 对象,该对象将使用默认设置。下面的代码片段设置了前三列的列约束。第二列使用约束的默认设置:

// With 100px fixed width
ColumnConstraints cc1 = new ColumnConstraints(100);

// Use all default settings
ColumnConstraints defaultCc2 = new ColumnConstraints();

// With 200px fixed width

ColumnConstraints cc3 = new ColumnConstraints(200);

GridPane gpane = new GridPane();
gpane.getColumnConstraints().addAll(cc1, defaultCc2, cc3);

Tip

在列/行上设置的某些列/行约束可以被列/行中的子级单独覆盖。一些约束可以在列/行中的子级上设置,并且可能影响整个列/行。我们将在下一节讨论这些情况。

在 GridPane 中为子对象设置约束

表 10-9 列出了可以为GridPane中的子节点设置的约束。我们已经讨论了列/行索引和跨度约束。我们将在本节中讨论其余部分。GridPane类包含两组静态方法来设置这些约束:

表 10-9

可以为 GridPane 中的子级设置的约束列表

|

限制

|

类型

|

描述

columnIndex Integer 这是子节点布局区域开始的列索引。第一列的索引为 0。默认值为 0。
rowIndex Integer 这是子节点布局区域开始的行索引。第一行的索引为 0。默认值为 0。
columnSpan Integer 它是子节点布局区域跨越的列数。默认值为 1。
rowSpan Integer 它是子节点的布局区域跨越的行数。默认值为 1。
halignment HPos 它指定子节点在其布局区域内的水*对齐方式。
valignment VPos 它指定子节点在其布局区域内的垂直对齐方式。
hgrow Priority 它指定子节点的水*增长优先级。
vgrow Priority 它指定子节点的垂直增长优先级。
margin Insets 它指定子节点布局边界外部的边距空间。
  • setConstraints()方法

  • setXxx(Node child, CType cvalue)方法,其中Xxx是约束名,CType是它的类型

要删除子节点的约束,将其设置为null

对齐和对齐约束

halignmentvalignment约束指定子节点在其布局区域内的对齐方式。他们默认为HPos.LEFTVPos.CENTER。它们可以在影响所有子代的列/行上设置。儿童可以单独设置它们。适用于子节点的最终值取决于一些规则:

  • 当没有为列/行和子节点设置它们时,子节点将使用默认值。

  • 当它们是为列/行设置的而不是为子节点设置的时,子节点将使用为列/行设置的值。

  • 当为列/行和子节点设置它们时,子节点将使用为其设置的值,而不是为列/行设置的值。本质上,子节点可以覆盖默认值或为这些约束的列/行设置的值。

清单 10-33 中的程序演示了前面提到的规则。图 10-51 为窗口。该程序在一列中添加三个按钮。列约束覆盖子节点的halignment约束的默认值HPos.LEFT,并将其设置为HPos.RIGHT。标有“Two”的按钮将该设置覆盖为HPos.CENTER。因此,该列中的所有按钮都是水*向右对齐的,除了标记为“Two”的按钮是居中对齐的。我们为所有三行设置了约束。第一排和第二排将valignment设置为VPos.TOP。第三行保留默认值VPos.CENTER。标签为“One”的按钮覆盖第一行上设置的valignment约束,将其设置为VPos.BOTTOM。注意,所有的子节点都遵循前面的三个规则来使用valignmenthalignment约束。

img/336502_2_En_10_Fig51_HTML.png

图 10-51

子级重写 GridPane 中的 halignment 和 valignment 约束

// GridPaneHValignment.java
// ... full listing in the book's download area.

Listing 10-33Using the halignment and valignment Constraints for Children in a GridPane

hgrow 和 vgrow 约束

hgrowvgrow约束指定整个列和行的水*和垂直增长优先级,即使它可以为子元素单独设置。这些约束也可以使用列和行的ColumnConstraintsRowConstraints对象来设置。默认情况下,列和行不会增长。使用以下规则计算列/行的这些约束的最终值:

  • 如果没有为列/行设置约束,也没有为列/行中的任何子元素设置约束,那么如果GridPane的宽度/高度被调整为大于首选宽度/高度,列/行不会增长。

  • 如果为列/行设置了约束,则使用在hgrowvgrowColumnConstraintsRowConstraints对象中设置的值,而不管子对象是否设置了这些约束。

  • 如果没有为列/行设置约束,则为列/行中的子级设置的这些约束的最大值将用于整个列/行。假设一个列有三个子列,并且没有为该列设置列约束。第一个子节点将hgrow设置为Priority.NEVER;第二对Priority.ALWAYS;而第三个到Priority.SOMETIMES。在这种情况下,三个优先级中的最大值是Priority.ALWAYS,它将用于整个列。ALWAYS优先级最高,SOMETIMES第二高,NEVER最低。

  • 如果列/行设置为固定或百分比宽度/高度,则hgrow/vgrow约束将被忽略。

清单 10-34 中的程序演示了前面的规则。图 10-52 显示了水*展开时的窗口。请注意,第二列会增长,但第一列不会。该程序添加了排列在两列中的六个按钮。第一列将hgrow约束设置为Priority.NEVER。该列设置的hgrow值优先;当GridPane水*展开时,第一列不增长。第二列不使用列约束。该列中的孩子使用三种不同类型的优先级:ALWAYSNEVERSOMETIMES。三个优先级中最大的是ALWAYS,使得第二列横向增长。

img/336502_2_En_10_Fig52_HTML.jpg

图 10-52

在 GridPane 中使用 hgrow 约束的列和子级

// GridPaneHVgrow.java
// ... full listing in the book's download area.

Listing 10-34Using the hgrow Constraints for Columns and Rows in a GridPane

利润限制

使用GridPane类的setMargin(Node child, Insets value)静态方法为孩子设置边距(布局边界周围的空间)。getMargin(Node child)静态方法返回子节点的边距:

// Set 10px margin around the b1 child node
GridPane.setMargin(b1, new Insets(10));
...
// Get the margin of the b1 child node
Insets margin = GridPane.getMargin(b1);

使用null将余量重置为零的默认值。

清除所有约束

使用GridPane类的clearConstraints(Node child)静态方法一次为一个子级重置所有约束(columnIndexrowIndexcolumnSpanrowSpanhalignmentvalignmenthgrowvgrowmargin):

// Clear all constraints for the b1 child node
GridPane.clearConstraints(b1);

了解锚定窗格

一个AnchorPane通过将它的子节点的四条边锚定到它自己的四条边上指定的距离来布局它的子节点。图 10-53 显示了一个AnchorPane内的子节点,其四边都指定了锚定距离。

img/336502_2_En_10_Fig53_HTML.png

图 10-53

锚定窗格中子节点的四个边约束

一个AnchorPane可用于两个目的:

  • 用于沿AnchorPane的一个或多个边缘对齐子对象

  • 用于在调整AnchorPane大小时拉伸孩子

子对象的边和AnchorPane的边之间的指定距离被称为指定边的锚点约束。例如,子对象的顶边与AnchorPane的顶边之间的距离称为topAnchor 约束等。一个子节点最多可以指定四个锚约束:topAnchorrightAnchorbottomAnchorleftAnchor

当您将一个子节点锚定到两条相对的边(上/下或左/右)时,子节点的大小会随着AnchorPane的大小调整而调整,以保持指定的锚定距离。

Tip

锚点距离是从AnchorPane的内容区域的边缘和子项的边缘开始测量的。也就是说,如果AnchorPane有边框和填充,则距离是从插入的内边缘开始测量的(边框+填充)。

创建锚定窗格对象

您可以使用无参数构造器创建一个空的AnchorPane:

AnchorPane apane1 = new AnchorPane();

您还可以在创建AnchorPane时指定它的初始子列表,如下所示:

Button okBtn = new Button("OK");
Button cancelBtn = new Button("Cancel");
AnchorPane apane2 = new AnchorPane(okBtn, cancelBtn);

您可以在创建之后向AnchorPane添加子对象,如下所示:

Button okBtn = new Button("OK");
Button cancelBtn = new Button("Cancel");
AnchorPane apane3 = new AnchorPane();
apane3.getChildren().addAll(okBtn, cancelBtn);

AnchorPane一起工作时,你需要记住两点:

  • 默认情况下,一个AnchorPane将它的子节点放在(0,0)处。您需要为子节点指定锚定约束,以将它们锚定到AnchorPane的一个或多个指定距离的边上。

  • AnchorPane的首选大小是基于子首选大小和它们的锚约束计算的。它为每个子节点添加首选宽度、左锚和右锚。该值最大的孩子决定了AnchorPane的首选宽度。它为每个子节点添加首选高度、左锚点和右锚点。具有该值最大值的孩子决定了AnchorPane的首选高度。有可能孩子会重叠。子对象是按照添加的顺序绘制的。

清单 10-35 中的程序给一个AnchorPane增加了两个按钮。一个按钮有一个长标签,另一个有一个短标签。首先添加带有长标签的按钮,因此首先绘制它。第二个按钮被第二次绘制,覆盖在第一个按钮上,如图 10-54 所示。该图显示了窗口的两个视图:一个是程序运行时的视图,另一个是调整窗口大小时的视图。两个按钮都放置在(0,0)处。该程序没有利用AnchorPane的锚定功能。

img/336502_2_En_10_Fig54_HTML.jpg

图 10-54

具有两个按钮但未指定锚定约束的锚定窗格

// AnchorPaneDefaults.java
// ... full listing in the book's download area.

Listing 10-35Using Default Positions in an AnchorPane

为 AnchorPane 中的子对象设置约束

表 10-10 列出了可以为AnchorPane中的孩子设置的约束。请注意,锚点距离是从AnchorPane的内容区域的边缘开始测量的,而不是从布局边界的边缘开始测量的。回想一下,Region在内容区域的边缘和布局边界之间有填充和边框插入。

表 10-10

可以为 AnchorPane 中的子级设置的约束列表

|

限制

|

类型

|

描述

topAnchor Double 它指定了AnchorPane的内容区域的上边缘与子节点的上边缘之间的距离。
rightAnchor Double 它指定了AnchorPane的内容区域的右边缘与子节点的右边缘之间的距离。
bottomAnchor Double 它指定了AnchorPane的内容区域的下边缘与子节点的下边缘之间的距离。
leftAnchor Double 它指定了AnchorPane的内容区域的左边缘与子节点的左边缘之间的距离。

AnchorPane 类包含四个静态方法,允许您设置四个定位约束的值。若要移除子节点的约束,请将其设置为 null。

// Create a Button and anchor it to top and left edges at 10px from each
Button topLeft = new Button("Top Left");
AnchorPane.setTopAnchor(topLeft, 10.0);  // 10px from the top edge
AnchorPane.setLeftAnchor(topLeft, 10.0); // 10px from the left edge

AnchorPane root = new AnchorPane(topLeft);

使用clearConstraints(Node child)静态方法清除一个子节点的所有四个锚约束的值。

setXxxAnchor(Node child, Double value)方法将一个Double值作为它的第二个参数。因此,必须向这些方法传递一个双精度值或一个Double对象。当您传递一个 double 值时,Java 的自动装箱特性会将该值装箱到一个Double对象中。一个常见的错误是传递一个 int 值:

Button b1 = new Button("A button");
AnchorPane.setTopAnchor(b1, 10); // An error: 10 is an int, not a double

前面的代码会生成一个错误:

Error(18): error: method setTopAnchor in class AnchorPane cannot be applied to given types;

产生这个错误是因为我们将 10 作为第二个参数进行了传递。值 10 是一个int文字,它被装箱成一个Integer对象,而不是一个Double对象。将 10 更改为 10D 或 10.0 将使其成为double值,并将修复错误。

清单 10-36 中的程序给一个AnchorPane. The first button添加了两个Button,并设置了它的顶部和左侧锚点。第二个按钮设置了底部和右侧锚点。图 10-55 显示了窗口的两种状态:一种是程序运行时,另一种是调整窗口大小时。窗口的初始大小不够宽,无法显示两个按钮,因此按钮重叠。JavaFX 运行时根据右下角按钮的首选大小(具有最大的首选宽度)及其右锚点值来计算窗口内容区域的宽度。该图还显示了调整大小后的窗口。你需要为一个AnchorPane设置一个合理的首选大小,这样所有的孩子都是可见的,没有重叠。

img/336502_2_En_10_Fig55_HTML.jpg

图 10-55

锚定窗格中的两个按钮在左上角和右下角对齐

// AnchorPaneTest.java
// ... full listing in the book's download area.

Listing 10-36Using an AnchorPane to Align Children to Its Corners

AnchorPane中的子节点被锚定到相对的边时,例如,顶部/底部或左侧/右侧,AnchorPane拉伸子节点以保持指定的锚。

清单 10-37 中的程序给一个AnchorPane添加了一个按钮,并使用距离每条边 10px 的锚将它锚定到左边和右边(相对的边)。这将使按钮在AnchorPane被调整到比其首选宽度更大的宽度时被拉伸。该按钮也锚定到顶部边缘。图 10-56 显示了初始窗口和调整后的窗口。

img/336502_2_En_10_Fig56_HTML.jpg

图 10-56

有一个按钮锚定在相对两侧的锚定窗格

// AnchorPaneStretching.java
// ... full listing in the book's download area.

Listing 10-37Anchoring Children to Opposite Sides in an AnchorPane

了解 TextFlow

一个TextFlow布局窗格被设计用来显示富文本。富文本由多个Text节点组成。TextFlow将所有Text节点中的文本组合在一个文本流中显示。在Text子节点的文本中的换行符('\n')表示一个新段落的开始。文本以TextFlow的宽度换行。

一个Text节点有它的位置、大小和环绕宽度。然而,当它被添加到一个TextFlow窗格时,这些属性被忽略。Text节点被一个接一个地放置,必要时将它们包裹起来。一个TextFlow中的文本可以跨越多行,而在一个Text节点中,它只显示在一行中。图 10-57 显示了一个以TextFlow为根节点的窗口。

img/336502_2_En_10_Fig57_HTML.jpg

图 10-57

显示富文本的 TextFlow

TextFlow是专门为使用多个文本节点显示富文本而设计的。然而,您并不仅限于向一个TextFlow添加文本节点。你可以添加任何其他节点,例如,按钮,TextField等。文本节点以外的节点使用其首选大小显示。

Tip

你可以认为TextFlowFlowPane非常相似。像FlowPane一样,TextFlow通过不同地对待文本节点,在从一端到另一端的流中布置其子节点。当文本节点超出其宽度边界时,它会按文本节点的宽度断开文本节点的文本,并在下一行显示剩余的文本。

创建 TextFlow 对象

与其他布局窗格的类不同,TextFlow类在javafx.scene.text包中,所有其他与文本相关的类都在这里。

您可以使用无参数构造器创建一个空的TextFlow:

TextFlow tflow1 = new TextFlow ();

您也可以在创建TextFlow时指定它的初始子列表:

Text tx1 = new Text("TextFlow layout pane is cool! ");
Text tx2 = new Text("It supports rich text display.");
TextFlow tflow2 = new TextFlow(tx1, tx2);

您可以在创建之后向TextFlow添加子项:

Text tx1 = new Text("TextFlow layout pane is cool! ");
Text tx2 = new Text("It supports rich text display.");
TextFlow tflow3 = new TextFlow();
tflow3.getChildren().addAll(tx1, tx2);

清单 10-38 中的程序展示了如何使用TextFlow。它给一个TextFlow增加了三个Text节点。第三个文本节点中的文本以换行符(\n)开始,这将开始一个新段落。程序将TextFlow的首选宽度设置为 300 像素,行距设置为 5 像素。图 10-58 为窗口。当您调整窗口大小时,TextFlow会根据需要以新的宽度重新绘制文本换行。

img/336502_2_En_10_Fig58_HTML.jpg

图 10-58

在 TextFlow 中显示为富文本的几个文本节点

// TextFlowTest.java
// ... full listing in the book's download area.

Listing 10-38Using the TextFlow Layout Pane to Display Rich Text

一个TextFlow也可以让你嵌入除了Text节点之外的节点。您可以创建一个表单来显示与用户可以使用的其他类型节点混合在一起的文本。清单 10-39 中的程序将一对RadioButton、一个TextField和一个Button嵌入到一个TextFlow中,以创建一个带有文本的在线表单。用户可以使用这些节点与表单进行交互。

图 10-59 显示了窗口。在测试这个例子的时候,RadioButton s 和TextField节点没有使用鼠标获得焦点。使用Tab键导航到这些节点,使用spacebar键选择一个RadioButton

img/336502_2_En_10_Fig59_HTML.jpg

图 10-59

文本流中嵌入的文本节点以外的节点

// TextFlowEmbeddingNodes.java
// ... full listing in the book's download area.

Listing 10-39Embedding Nodes Other Than Text Nodes in a TextFlow

TextFlow 属性

TextFlow类包含两个属性,如表 10-11 所列,用于定制其布局。

表 10-11

TextFlow 类中声明的属性列表

|

财产

|

类型

|

描述

lineSpacing DoubleProperty 它指定行与行之间的垂直间距。其默认值为 0px。
tabSize IntegerProperty 制表符在空格中的大小。
textAlignment ObjectProperty<TextAlignment> 它指定了TextFlow内容的对齐方式。它的值是TextAlignment枚举的常量之一:LEFTRIGHTCENTERJUSTIFY。其默认值为LEFT

lineSpacing 属性指定了TextFlow中各行之间的垂直间距(以像素为单位)。我们已经在前面的例子中使用过了。

TextFlow tflow = new TextFlow();
tflow.setLineSpacing(5); // 5px lineSpacing

textAlignment属性指定了TextFlow的全部内容的对齐方式。默认情况下,内容靠左对齐。图 10-60 显示了在程序中创建TextFlow对象后添加以下语句时清单 10-39 中程序的窗口:

img/336502_2_En_10_Fig60_HTML.jpg

图 10-60

使用 CENTER 作为 textAlignment 的 TextFlow

// Set the textAlignment to CENTER
root.setTextAlignment(TextAlignment.CENTER);

在 TextFlow 中为子对象设置约束

TextFlow不允许你给它的子节点添加任何约束,甚至没有一个边距。

对齐像素

图 10-61 显示了一个五像素宽五像素高的设备屏幕。图中的圆圈代表一个像素。坐标(0,0)被映射到左上角像素的左上角。左上角像素的中心映射到坐标(0.5,0.5)。所有整数坐标都落在像素之间的角落和裂缝中。在该图中,实线穿过像素的裂缝,虚线穿过像素的中心。

img/336502_2_En_10_Fig61_HTML.png

图 10-61

屏幕上的 5X5 像素区域

在 JavaFX 中,可以用浮点数指定坐标,例如 0.5、6.0 等。,这使您可以表示像素的任何部分。如果浮点数是整数(例如 2.0、3.0 等)。),它将代表像素的角点。

使用浮点数作为坐标的 A Region不会精确对齐像素边界,其边界可能看起来模糊。Region类包含一个snapToPixel属性来解决这个问题。默认情况下,它被设置为 true,并且一个Region将它的子节点的位置、间距和大小值调整为一个整数,以匹配像素边界,从而为子节点产生清晰的边界。如果您不希望Region将这些值调整为整数,请将snapToPixel属性设置为 false。

摘要

一个布局窗格是一个包含其他节点的节点,这些节点被称为其子节点(或子节点)。布局窗格的职责是在需要时对其子窗格进行布局。布局窗格也称为容器布局容器。布局窗格有一个布局策略,它控制布局窗格如何布局其子元素。例如,布局窗格可以水*、垂直或以任何其他方式布置其子元素。JavaFX 包含几个与布局相关的类。布局窗格计算其子窗格的位置和大小。布局窗格的布局策略是一组计算其子窗格的位置和大小的规则。

以下类别的对象代表布局窗格:HBoxVBoxFlowPaneBorderPaneStackPaneTilePaneGridPaneAnchorPaneTextFlow。所有布局窗格类都继承自Pane类。

一个Group具有容器的特征;比如它有自己的布局策略和坐标系,是Parent类的子类。然而,将其称为节点的集合或组*,而不是容器,可以最好地反映其含义。它用于将节点集合作为单个节点(或一个组)进行操作。应用于Group的变换、效果和属性会应用于Group中的所有节点。一个Group有它自己的布局策略,除了给他们自己喜欢的尺寸,它不提供任何特定的布局给它的孩子。*

一个HBox在一个水*行中布置它的子元素。它允许您设置相邻子元素之间的水*间距、任何子元素的边距、调整子元素的行为等。它使用 0px 作为相邻子项之间的默认间距。内容区域和 HBox 的默认宽度足以以其首选宽度显示其所有子级,默认高度是其所有子级的最大高度。

一个VBox在一个单独的垂直列中布置它的子元素。它允许您设置相邻子元素之间的垂直间距、任何子元素的边距、调整子元素的行为等。它使用 0px 作为相邻子项之间的默认间距。一个VBox的内容区域的默认高度足够以他们喜欢的高度显示它的所有子元素,并且默认宽度是它所有子元素的最大宽度。

一个FlowPane是一个简单的布局窗格,它以指定的宽度或高度将它的子元素排列成行或列。它让其子元素水*或垂直流动,因此得名“流动窗格”您可以指定首选的换行长度,这是水*流的首选宽度和垂直流的首选高度,内容在垂直流中换行。一个FlowPane用在孩子的相对位置不重要的场合,比如显示一系列图片或者按钮。

A BorderPane将其布局区域分为五个区域:顶部、右侧、底部、左侧和中心。您最多可以在五个区域中的每个区域放置一个节点。顶部和底部区域中的子区域将被调整到其首选高度。它们的宽度被扩展以填充可用的额外水*空间,只要子元素的最大宽度允许扩展它们的宽度超过它们的首选宽度。右区域和左区域中的子区域被调整到它们的首选宽度。他们的高度被延长以填充额外的垂直空间,只要儿童的最大高度允许他们的高度超过他们的首选高度。中间的子节点将在两个方向上填充剩余的可用空间。

一个StackPane在一个节点堆栈中布局它的子节点。它提供了覆盖节点的强大手段。子对象是按照添加的顺序绘制的。

一个TilePane把它的子节点放在一个统一大小的网格中,这个网格被称为瓦片。TilePane的工作方式类似于FlowPane的工作方式,但有一点不同:在FlowPane中,行和列可以有不同的高度和宽度,而在TilePane中,所有的行都有相同的高度,所有的列都有相同的宽度。最宽子节点的宽度和最高子节点的高度是TilePane中所有图块的默认宽度和高度。可以设置为水*或垂直的TilePane的方向决定了其内容的流向。默认情况下,TilePane具有水*方向。

一个GridPane在一个动态的单元格网格中布置它的子元素,单元格按行和列排列。网格是动态的,因为网格中单元的数量和大小是根据子单元的数量确定的。它们取决于对孩子的约束。网格中的每个单元格都由其在列和行中的位置来标识。列和行的索引从零开始。子节点可以放置在跨越多个单元的网格中的任何位置。一行中的所有单元格高度相同。不同行中的单元可以具有不同的高度。一列中的所有单元格宽度相同。不同列中的单元格可能具有不同的宽度。默认情况下,一行的高度足以容纳其中最高的子节点。一个列的宽度足以容纳其中最宽的子节点。您可以自定义每行和每列的大小。GridPane还允许行与行之间的垂直间距和列与列之间的水*间距。出于调试目的,您可以显示网格线。图 10-41 显示了GridPane的三个实例。

一个AnchorPane通过将它的子节点的四条边锚定到它自己的四条边上指定的距离来布局它的子节点。一个AnchorPane可以用于沿着AnchorPane的一个或多个边缘对齐子节点,或者在AnchorPane调整大小时拉伸子节点。

子节点的边和AnchorPane的边之间的指定距离被称为指定边的约束。当您将一个子节点锚定到两个相对的边(上/下或左/右)时,子节点的大小会随着AnchorPane的大小调整而调整,以保持指定的锚定距离。

一个TextFlow布局窗格被设计用来显示富文本。富文本由多个Text节点组成。TextFlow将所有Text节点中的文本组合在一个文本流中显示。在Text子节点的文本中的换行符('\n')表示一个新段落的开始。文本以TextFlow的宽度换行。

十一、模型-视图-控制器模式

在本章中,您将学习:

  • 什么是模型-视图-控制器模式

  • 模型-视图-控制器模式的其他变体是什么,比如模型-视图-展示者模式

  • 如何使用模型-视图-演示者模式开发 JavaFX 应用程序

本章的例子在com.jdojo.mvc包中。为了让它们工作,您必须在module-info.java文件中添加相应的行:

...
opens com.jdojo.mvc to javafx.graphics, javafx.base;
opens com.jdojo.mvc.model to javafx.graphics, javafx.base;
opens com.jdojo.mvc.view to javafx.graphics, javafx.base;
...

什么是模型-视图-控制器模式?

JavaFX 允许您使用 GUI 组件创建应用程序。GUI 应用程序执行三项任务:接受用户输入、处理输入和显示输出。GUI 应用程序包含两种类型的代码:

  • 处理特定于领域的数据和业务规则的领域代码

  • 处理操作用户界面小部件的表示代码

通常要求特定领域中的相同数据以不同的形式呈现。例如,您可能有一个使用 HTML 的 web 界面和一个使用 JavaFX 的桌面界面来呈现相同的数据。为了便于维护应用程序代码,通常需要将应用程序分成两个逻辑模块,其中一个模块包含表示代码,另一个包含领域代码(特定于领域的业务逻辑和数据)。这种划分是以这样一种方式进行的,即表示模块可以看到域模块,但反之则不能。这种类型的划分支持具有相同域代码的多个表示。

模型-视图-控制器(model-view-controller,MVC)模式是最古老也是最流行的模式,用于为 GUI 应用程序建模,以促进这种划分。MVC 模式由三部分组成:模型视图控制器。图 11-1 显示了 MVC 组件以及它们之间的交互的图形视图。

img/336502_2_En_11_Fig1_HTML.png

图 11-1

经典 MVC 模式中参与者之间的交互

在 MVC 中,模型由模拟现实世界问题的领域对象组成。视图控制器由处理表示的表示对象组成,比如输入、输出和用户与 GUI 元素的交互。控制器接受用户的输入并决定如何处理。也就是说,用户直接与控制器交互。视图在屏幕上显示输出。每个视图都与一个唯一的控制器相关联,反之亦然。屏幕上的每个小部件都是一个视图,都有相应的控制器。因此,在 GUI 屏幕中通常有多个视图-控制器对。该模型不知道任何特定的视图和控制器。但是,视图和控制器是特定于模型的。控制器命令模型修改其状态。视图和模型总是保持同步。模型通知视图其状态的变化,因此视图可以显示更新的数据。通过一个观察者模式来促进模型到视图的交互。请记住,模型完全不知道任何特定的视图。该模型为视图提供了一种订阅其状态更改通知的方式。任何感兴趣的视图都可以订阅模型来接收状态更改通知。每当模型的状态改变时,模型通知所有已经订阅的视图。

到目前为止,关于 MVC 模式的描述是 MVC 的原始概念,它被用于在 1980 年创建的 Smalltalk-80 语言中开发用户界面。Smalltalk 有许多变体。MVC 中的概念,即 GUI 应用程序中的表示和域逻辑应该分离,仍然适用。然而,在 MVC 中,在三个组件之间划分职责存在问题。例如,哪个组件具有更新视图属性的逻辑,比如改变视图颜色或禁用它,这取决于模型的状态?视图可以有自己的状态。显示项目列表的列表包含当前选定项目的索引。选定的索引是视图的状态,而不是模型。一个模型可能同时与几个视图相关联,并且存储所有视图的状态不是模型的责任。

MVC 中哪个组件负责存储视图逻辑和状态的问题导致了 MVC 的另一个变种,称为应用程序模型 MVC (AM-MVC)。在 AM-MVC 中,在模型和视图/控制器之间引入了一个名为应用模型的新组件。它的目的是包含表示逻辑和状态,从而解决在原始 MVC 中哪个组件保持表示逻辑和状态的问题。MVC 中的模型与视图是解耦的,在 AM-MVC 中也是如此。两者都使用相同的观察者技术来保持视图和模型的同步。在 AM-MVC 中,应用程序模型应该保存视图相关的逻辑,但是不允许直接访问视图。当应用程序模型必须更新视图属性时,这会导致庞大而丑陋的代码。图 11-2 显示了 AM-MVC 组件以及它们之间的交互的图形视图。

img/336502_2_En_11_Fig2_HTML.png

图 11-2

AM-MVC 模式中参与者之间的交互

后来,像微软 Windows 和 Mac OS 这样的现代图形操作系统提供了本地小部件,用户可以直接与之交互。这些小部件将视图和控制器的功能合二为一。这导致了 MVC 的另一种变体,称为模型-视图-展示者(MVP)模式。现代小部件还支持数据绑定,这有助于用更少的代码行保持视图和模型同步。图 11-3 显示了 MVP 组件以及它们之间的交互的图示。

img/336502_2_En_11_Fig3_HTML.png

图 11-3

MVP 模式中参与者之间的交互

在 MVC 中,屏幕上的每个小部件都是一个视图,它有自己独特的控制器。在 MVP 中,视图由几个小部件组成。视图截取来自用户的输入,并将控制权交给演示者。请注意,视图不会对用户输入做出反应。它只会拦截它们。视图还负责显示来自模型的数据。

视图向演示者通知用户输入。它决定了如何对用户的输入做出反应。演示者负责演示逻辑,操作视图,并向模型发出命令。一旦演示者修改了模型,视图就会使用观察者模式进行更新,就像在 MVC 中一样。

模型负责存储特定领域的数据和逻辑。像 MVC 一样,它独立于任何视图和演示者。演示者命令模型改变,当视图从模型接收到状态改变的通知时,视图更新自己。

MVP 也有一些变种。他们在视图和演示者的责任上有所不同。在一个变体中,视图负责所有视图相关的逻辑,而不需要演示者的帮助。在另一个变体中,视图负责所有可以以声明方式处理的简单逻辑,除了当逻辑复杂时,由呈现者处理。在另一个变体中,展示者处理所有与视图相关的逻辑并操纵视图。这种变体被称为被动视图 MVP ,其中视图不知道模型。图 11-4 显示了 MVP 被动视图中的组件以及它们之间的交互。

img/336502_2_En_11_Fig4_HTML.png

图 11-4

被动视图 MVP 模式中参与者之间的交互

MVC 的概念,即表示逻辑应该从领域逻辑中分离出来,已经存在了 30 多年了,并且将以这样或那样的形式存在。MVC 的所有变体都试图实现与经典 MVC 相同的功能,尽管方式不同。这些变体在组件的职责上不同于传统的 MVC。当有人在 GUI 应用程序设计中谈论 MVC 时,请确保您理解使用了 MVC 的哪种变体,以及哪些组件执行哪些任务。

模型-视图-演示者示例

本节给出了一个使用 MVP 模式的详细例子。

要求

对于这里的例子,您将开发一个 GUI 应用程序,让用户输入一个人的详细信息,验证数据,并保存它。该表格应包含

  • 人员 ID 字段:自动生成的唯一不可编辑字段

  • 名字字段:一个可编辑的文本字段

  • 姓氏字段:可编辑的文本字段

  • 出生日期:可编辑的文本字段

  • 年龄类别:基于出生日期的自动计算的不可编辑字段

  • 保存按钮:保存数据的按钮

  • 关闭按钮:关闭窗口的按钮

应根据以下规则验证个人数据:

  • 名字和姓氏必须至少有一个字符长。

  • 如果输入了出生日期,它不能是将来的日期。

设计

三个类别将代表 MVP 的三个组成部分:

  • Person阶级

  • PersonViewPersonPresenter

Person类代表模型,PersonView类代表视图,PersonPresenter类代表演示者。按照 MVP 模式的要求,Person类对于PersonViewPersonPresenter类是不可知的。PersonViewPersonPresenter类将相互交互,它们将直接使用Person类。

让我们通过将与模型和视图相关的类放在不同的 Java 包中来对它们进行逻辑划分。com.jdojo.mvc.model包将包含与模型相关的类,com.jdojo.mvc.view包将包含与视图相关的类。图 11-5 显示完成的窗口。

img/336502_2_En_11_Fig5_HTML.jpg

图 11-5

人员管理窗口的初始屏幕截图

实施

以下段落描述了 MVP 示例应用程序的三个层的实现。

模型

清单 11-1 包含了Person类的完整代码。Person类包含领域数据和业务规则的代码。在现实生活中,您可能希望将这两者分成多个类。但是,对于像这样的小应用程序,让我们将它们放在一个类中。

// Person.java
// ...find in the book's download area.

Listing 11-1 The Person Class Used As the Model

Person类声明了一个AgeCategory枚举来表示不同的年龄:

public enum AgeCategory {BABY, CHILD, TEEN, ADULT, SENIOR, UNKNOWN};

个人 ID、名字、姓氏和出生日期由 JavaFX 属性表示。personId属性被声明为只读,并且是自动生成的。为这些属性提供了相关的 setter 和 getter 方法。

包含了isValidBirthDate()isValidPerson()方法来执行特定于域的验证。getAgeCategory()方法属于Person类,因为它根据出生日期计算一个人的年龄类别。我编了一些日期范围,把一个人的年龄分成不同的类别。您可能想将这个方法添加到视图中。但是,您需要为每个视图复制这个方法中的逻辑。该方法使用模型数据并计算一个值。它对视图一无所知,所以它属于模型,而不属于视图。

save()方法保存个人数据。保存方法很简单;如果个人数据有效,它只是在标准输出上显示一条消息。在实际应用中,它会将数据保存到数据库或文件中。

景色

清单 11-2 中显示的PersonView类表示这个应用程序中的视图。它主要负责显示模型中的数据。

// PersonView.java
// ...find in the book's download area.

Listing 11-2 The PersonView Class Used As the View

PersonView类继承自GridPane类。它包含每个 UI 组件的一个实例变量。它的构造器将模型(Person类的一个实例)和日期格式作为参数。日期格式是用于显示出生日期的格式。请注意,出生日期的格式是特定于视图的,因此它应该是视图的一部分。模型不知道视图显示出生日期的格式。

initFieldData()方法用数据初始化视图。我使用 JavaFX 绑定将 UI 节点中的数据绑定到模型数据,除了出生日期和年龄类别字段。此方法将出生日期和年龄类别字段与模型同步。layoutForm()方法在网格窗格中布置 UI 节点。bindFieldsToModel()方法将人员 ID、名字和姓氏TextField绑定到模型中相应的数据字段,因此它们保持同步。syncBirthDate()方法从模型中读取出生日期,对其进行格式化,并显示在视图中。syncAgeCategory()方法同步年龄类别字段,该字段由模型根据出生日期计算得出。

请注意,视图,PersonView类不知道演示者,PersonPresenter类。那么视图和演示者将如何交流呢?演示者的角色主要是从视图中获取用户的输入,并根据这些输入采取行动。演示者将拥有对视图的引用。它将向视图添加事件侦听器,因此当视图中的数据发生变化时,它会得到通知。在事件处理程序中,演示者控制并处理输入。如果应用程序需要引用视图中的演示者,您可以将其作为视图类的构造器的参数。或者,您可以在视图类中提供一个 setter 方法来设置演示者。

演示者

清单 11-3 中显示的PersonPresenter类表示这个应用程序中的演示者。它主要负责截取视图中的新输入并进行处理。它直接与模型和视图通信。

// PersonPresenter.java
// ...find in the book's download area.

Listing 11-3 The PersonPresenter Class Used As the Presenter

PersonPresenter类的构造器将模型和视图作为参数。attachEvents()方法将事件处理程序附加到视图的 UI 组件上。在这个例子中,您对截取视图中的所有输入不感兴趣。但是您对出生日期的更改以及点击保存和关闭按钮感兴趣。您不想检测出生日期字段中的所有编辑更改。如果您对出生日期字段中的所有更改感兴趣,您需要为它的text属性添加一个更改监听器。您希望仅在用户完成输入出生日期时检测更改。为此

  • 您将焦点监听器附加到场景,并检测出生日期是否已失去焦点。

  • 您将一个动作侦听器附加到出生日期字段,以便在字段获得焦点时拦截 Enter 键的按下。

每当出生日期字段失去焦点或焦点仍在字段中时按下 Enter 键,这将验证并刷新出生日期和年龄类别。

handleBirthDateChange()方法处理出生日期字段的变化。它在更新模型之前验证出生日期格式。如果出生日期无效,它会向用户显示一条错误消息。最后,它告诉视图更新出生日期和年龄类别。

当用户点击 Save 按钮时,调用saveData()方法,它命令模型保存数据。showError()方法不属于演示者。这里,您添加了它,而不是创建一个新的视图类。它用于显示错误消息。

把它们放在一起

让我们将模型、视图和演示者放在一起,在应用程序中使用它们。清单 11-4 中的程序创建模型、视图和展示者,将它们粘合在一起,并在如图 11-5 所示的窗口中显示视图。请注意,在创建演示者之前,必须将视图附加到场景。这是必需的,因为演示者将焦点改变监听器附加到场景。在将视图添加到场景之前创建演示者将导致一个NullPointerException

// PersonApp.java
// ...find in the book's download area.

Listing 11-4 The PersonApp Class Uses the Model, View, and Presenter to Create a GUI Application

摘要

通常要求相同的领域数据以不同的形式呈现。例如,您可能有一个使用 HTML 的 web 界面和一个使用 JavaFX 的桌面界面来呈现相同的数据。为了便于维护应用程序代码,通常需要将应用程序分成两个逻辑模块,其中一个模块包含表示代码,另一个包含领域代码(特定于领域的业务逻辑和数据)。这种划分是以这样一种方式进行的,即表示模块可以看到域模块,但反之则不能。这种类型的划分支持具有相同域代码的多个表示。MVC 模式是最古老也是最流行的模式,它为 GUI 应用程序建模以促进这种划分。MVC 模式由三个组件组成:模型、视图和控制器。

在 MVC 中,模型由模拟现实世界问题的领域对象组成。视图和控制器由处理表示的表示对象组成,例如输入、输出和用户与 GUI 元素的交互。控制器接受来自用户的输入并决定如何处理它们。也就是说,用户直接与控制器交互。视图在屏幕上显示输出。每个视图都与一个唯一的控制器相关联,反之亦然。屏幕上的每个小部件都是一个视图,都有相应的控制器。在 MVC 中,在三个组件之间划分职责产生了问题。例如,哪个组件具有更新视图属性的逻辑,比如改变视图颜色或禁用它,这取决于模型的状态?

MVC 中哪个组件负责存储视图逻辑和状态的问题导致了 MVC 的另一种变体,称为应用程序模型 MVC。在 AM-MVC 中,在模型和视图/控制器之间引入了一个新的组件,称为应用程序模型。它的目的是包含表示逻辑和状态,从而解决哪个组件在原始 MVC 中保持表示逻辑和状态的问题。

后来,像微软 Windows 和 Mac OS 这样的现代图形操作系统提供了本地小部件,用户可以直接与之交互。这些小部件将视图和控制器的功能合二为一。这导致了 MVC 的另一种变体,称为模型-视图-呈现者模式。

在 MVC 中,屏幕上的每个小部件都是一个视图,它有自己独特的控制器。在 MVP 中,视图由几个小部件组成。视图截取来自用户的输入,并将控制权交给演示者。请注意,视图不会对用户的输入做出反应;它只会拦截它们。视图通知演示者用户的输入,并决定如何对其做出反应。演示者负责演示逻辑,操作视图,并向模型发出命令。一旦演示者修改了模型,视图就会使用观察者模式进行更新,就像在 MVC 中一样。

MVP 也有一些变种。他们在视图和演示者的责任上有所不同。在一个变体中,视图负责所有视图相关的逻辑,而不需要演示者的帮助。在另一个变体中,视图负责所有可以以声明方式处理的简单逻辑,除了当逻辑复杂时,由呈现者处理。在另一个变体中,展示者处理所有与视图相关的逻辑并操纵视图。这种变体被称为被动视图 MVP,其中视图不知道模型。

下一章将向您介绍用于在 JavaFX 应用程序中构建视图的控件。

十二、了解控件

在本章中,您将学习:

  • Java 中的控件是什么

  • 关于实例表示 JavaFX 中控件的类

  • 关于LabelButtonCheckBoxRadioButtonHyperlinkChoiceBoxComboBoxListViewColorPickerDatePickerTextFieldTextAreaMenu等控件

  • 如何使用 CSS 设计控件的样式

  • 如何使用FileChooserDirectoryChooser对话框

本章的例子在com.jdojo.control包中。为了让它们工作,您必须在module-info.java文件中添加相应的一行:

...
opens com.jdojo.control to javafx.graphics, javafx.base;
...

JavaFX 中有很多控件,关于控件有很多要说的。出于这个原因,控件的示例代码只是以一种简化的方式呈现。要获得完整的列表,请查阅该书的下载区。

什么是控件?

JavaFX 允许您使用 GUI 组件创建应用程序。具有 GUI 的应用程序执行三项任务:

  • 接受用户通过键盘或鼠标等输入设备的输入

  • 处理输入(或根据输入采取行动)

  • 显示输出

UI 提供了一种在应用程序及其用户之间交换输入和输出信息的方式。使用键盘输入文本、使用鼠标选择菜单项、点击按钮或其他动作都是向 GUI 应用程序提供输入的示例。该应用程序使用文本、图表、对话框等在计算机显示器上显示输出。

用户使用称为控件小部件的图形元素与 GUI 应用程序进行交互。按钮、标签、文本字段、文本区域、单选按钮和复选框是控件的几个例子。键盘、鼠标和触摸屏等设备用于向控件提供输入。控件还可以向用户显示输出。控件生成指示用户和控件之间发生某种交互的事件。例如,使用鼠标或空格键按下按钮会生成一个动作事件,指示用户已经按下了该按钮。

JavaFX 提供了一组丰富的易于使用的控件。控件被添加到布局窗格中,对它们进行定位和调整大小。第十章讨论了布局窗格。本章讨论如何使用 JavaFX 中可用的控件。

通常,MVP 模式(在第十一章中讨论)用于在 JavaFX 中开发 GUI 应用程序。MVP 要求你至少有三个类,并以某种方式将你的业务逻辑放在某些类中。一般来说,这会使应用程序代码膨胀,尽管这样做是有道理的。本章将关注不同类型的控件,而不是学习 MVP 模式。您将把 MVP 模式所需的类嵌入到一个类中,以保持代码简洁并节省本书的大量空间!

了解控件类层次结构

JavaFX 中的每个控件都由一个类的实例来表示。如果多个控件共享基本功能,则它们从一个公共基类继承。控制类包含在javafx.scene.control包中。控件类是Control类的一个子类,直接或间接,而后者又继承自Region。回想一下,Region类继承自Parent类。所以,技术上来说,一个Control也是一个Parent。我们在前面章节中关于ParentRegion类的所有讨论也适用于所有控制相关的类。

一个Parent可以生孩子。通常,控件由另一个节点(有时是多个节点)组成,该节点是它的子节点。控件类不通过getChildren()方法公开其子类的列表,因此,您不能向它们添加任何子类。

控件类通过返回一个ObservableList<Node>getChildrenUnmodifiable()方法公开其内部不可修改的子控件列表。使用控件不需要知道控件的内部子级。然而,如果你需要他们的孩子的列表,getChildrenUnmodifiable()方法会给你。

图 12-1 显示了一些常用控件的类的类图。控件类的列表比类图中显示的要大得多。

img/336502_2_En_12_Fig1_HTML.jpg

图 12-1

JavaFX 中控件类的类图

Control类是所有控件的基类。它声明了三个属性,如表 12-1 所示,这些属性对所有控件都是通用的。

表 12-1

Control类中声明的属性

|

财产

|

类型

|

描述

contextMenu ObjectProperty<ContextMenu> 指定控件的内容菜单。
skin ObjectProperty<Skin<?>> 指定控件的外观。
tooltip ObjectProperty<Tooltip> 指定控件的工具提示。

属性指定控件的上下文菜单。上下文菜单为用户提供了一个选项列表。每个选择都是在控件的当前状态下可以对其采取的操作。有些控件有默认的上下文菜单。例如,当右键单击一个TextField时,会显示一个上下文菜单,其中包含撤销、剪切、复制和粘贴等选项。通常,当控件具有焦点时,当用户按下组合键(例如,Windows 上的 Shift + F10)或单击鼠标(Windows 上的右击)时,会显示上下文菜单。在讨论文本输入控件时,我将再次讨论contextMenu属性。

在撰写本文时,JavaFX 不允许访问或定制控件的默认上下文菜单。即使控件有默认的上下文菜单,contextMenu属性也是null。当您设置contextMenu属性时,它将替换控件的默认上下文。请注意,并非所有控件都有默认的上下文菜单,并且上下文菜单并不适合所有控件。例如,Button控件不使用上下文菜单。

控件的视觉外观被称为它的皮肤。外观通过更改其视觉外观来响应控件中的状态更改。一个皮肤由一个Skin接口的实例来表示。Control类实现了Skinnable接口,给予所有控件使用皮肤的能力。

Control类中的skin属性指定控件的自定义皮肤。开发新皮肤不是一件容易的事情。在大多数情况下,您可以使用 CSS 样式自定义控件的外观。所有的控件都可以使用 CSS 来设置样式。Control类实现了Styleable接口,所以所有的控件都可以被样式化。关于如何使用 CSS 的更多细节,请参考第八章。我将在本章中讨论一些常用的 CSS 属性。

当鼠标在控件上停留一小段时间时,控件会显示一条名为工具提示的短消息。Tooltip类的对象表示 JavaFX 中的工具提示。Control类中的tooltip属性指定控件的工具提示。

标签控件

一个labeled控件包含一个只读的文本内容和一个可选的图形作为其 UI 的一部分。LabelButtonCheckBoxRadioButtonHyperlink是 JavaFX 中标签控件的一些例子。所有带标签的控件都直接或间接地继承自被声明为抽象的Labeled类。Labeled类继承自Control类。图 12-2 显示了标签控件的类图。为了简洁起见,图中省略了一些类。

img/336502_2_En_12_Fig2_HTML.jpg

图 12-2

标记控件类的类图

Labeled类声明了textgraphic属性,分别代表文本和图形内容。它声明了几个其他属性来处理其内容的视觉方面,例如,对齐、字体、填充和文本换行。表 12-2 包含这些属性的列表及其简要描述。我将在随后的章节中讨论其中的一些属性。

表 12-2

Labeled类中声明的属性

|

财产

|

类型

|

描述

alignment ObjectProperty<Pos> 它指定内容区域内控件内容的对齐方式。当内容区域大于内容(文本+图形)时,其效果是可见的。默认值为Pos.CENTER_LEFT
contentDisplay ObjectProperty<ContentDisplay> 它指定图形相对于文本的位置。
ellipsisString StringProperty 它指定当文本被截断时为省略号显示的字符串,因为控件的大小小于首选大小。对于大多数语言环境,默认值是"..."。为此属性指定空字符串不会在截断的文本中显示省略号字符串。
font ObjectProperty<Font> 它指定文本的默认字体。
graphic ObjectProperty<Node> 它为控件指定一个可选图标。
graphicTextGap DoubleProperty 它指定了图形和文本之间的文本数量。
labelPadding ReadOnlyObjectProperty<Insets> 它是控件内容区域周围的空白。默认为Insets.EMPTY
lineSpacing DoubleProperty 它指定当控件显示多行时相邻行之间的间距。
mnemonicParsing BooleanProperty 它启用或禁用文本分析来检测助记符。如果设置为 true,则分析控件文本中的下划线(_)字符。第一个下划线后面的字符作为控件的助记键添加。在 Windows 计算机上按 Alt 键会突出显示所有控件的助记键。
textAlignment ObjectProperty<TextAlignment> 它为多行文字指定文字边界内的文字对齐方式。
textFill ObjectProperty<Paint> 它指定文本颜色。
textOverrun ObjectProperty<OverrunStyle> 它指定当文本内容超出可用空间时如何显示文本。
text StringProperty 它指定文本内容。
underline BooleanProperty 它指定文本内容是否应该加下划线。
wrapText BooleanProperty 它指定如果文本不能在一行中显示,是否应该换行。

定位图形和文本

标签控件的contentDisplay属性指定图形相对于文本的位置。其值是ContentDisplay枚举的常量之一:TOPRIGHTBOTTOMLEFTCENTERTEXT_ONLYGRAPHIC_ONLY。如果不想显示文本或图形,可以使用GRAPHIC_ONLYTEXT_ONLY值,而不是将文本设置为空字符串,将图形设置为null。图 12-3 显示了对一个LabelcontentDisplay属性使用不同值的效果。Label使用 Name:作为文本,蓝色矩形作为图形。contentDisplay属性的值显示在每个实例的底部。

img/336502_2_En_12_Fig3_HTML.jpg

图 12-3

contentDisplay属性对标签控件的影响

理解助记符和加速器

标签控件支持键盘助记符,也称为键盘快捷键键盘指示器。助记键是向控件发送ActionEvent的键。助记键通常与修饰键(如 Alt 键)一起按下。修改键依赖于*台;但是,它通常是一个 Alt 键。例如,假设您将 C 键设置为关闭按钮的助记键。当您按 Alt + C 时,关闭按钮被激活。

在 JavaFX 中找到关于助记符的文档并不容易。它隐藏在LabeledScene类的文档中。为标签控件设置助记键很容易。您需要在文本内容中的助记符前面加一个下划线,并确保控件的mnemonicParsing属性设置为 true。第一个下划线被删除,其后的字符被设置为控件的助记键。对于一些带标签的控件,助记符解析默认设置为 true,而对于其他控件,您需要设置它。

Tip

并非所有*台都支持助记符。至少在 Windows 上,控件文本中的助记符没有下划线,直到按下 Alt 键。

以下语句将 C 键设置为Close按钮的助记键:

// For Button, mnemonic parsing is true by default
Button closeBtn = new Button("_Close");

当您按下 Alt 键时,所有控件的助记符都带有下划线,按下任何控件的助记符都会将焦点设置到该控件并向其发送一个ActionEvent

JavaFX 在javafx.scene.input包中提供了以下四个类,以编程方式为所有类型的控件设置助记符:

  • Mnemonic

  • KeyCombination

  • KeyCharacterCombination

  • KeyCodeCombination

Mnemonic类的一个对象代表一个助记符。被声明为抽象的KeyCombination类的对象代表助记键的组合键。KeyCharacterCombinationKeyCodeCombination类是KeyCombination类的子类。使用前者用一个字符构造一个组合键;使用后者通过一个键码构造一个组合键。请注意,并非键盘上的所有键都代表字符。KeyCodeCombination类允许你为键盘上的任意键创建组合键。

为一个节点创建了Mnemonic对象,并将其添加到一个Scene中。当Scene接收到组合键的未使用的键事件时,它向目标节点发送一个ActionEvent

以下代码片段实现了与前面示例中使用一条语句相同的结果:

Button closeBtn = new Button("Close");

// Create a KeyCombination for Alt + C
KeyCombination kc = new KeyCodeCombination(KeyCode.C, KeyCombination.ALT_DOWN);

// Create a Mnemonic object for closeBtn
Mnemonic mnemonic = new Mnemonic(closeBtn, kc);

Scene scene = create a scene...;
scene.addMnemonic(mnemonic); // Add the mnemonic to the scene

KeyCharacterCombination类也可以用来创建 Alt + C 的组合键:

KeyCombination kc = new KeyCharacterCombination("C", KeyCombination.ALT_DOWN);

Scene类支持快捷键。当按下加速键时,执行一个Runnable任务。注意助记键和快捷键的区别。助记键与控件相关联,按下它的组合键会向控件发送一个ActionEvent。快捷键不与控件关联,而是与任务关联。Scene类维护一个ObservableMap<KeyCombination, Runnable>,其引用可以使用getAccelerators()方法获得。

下面的代码片段将一个快捷键(Windows 上的 Ctrl + X 和 Mac 上的 Meta + X)添加到一个Scene,它关闭与Scene关联的窗口。SHORTCUT键代表*台上的快捷键 Windows 上的 Ctrl,Mac 上的 Meta:

Scene scene = create a scene object...;
...
KeyCombination kc = new KeyCodeCombination(KeyCode.X,
                                           KeyCombination.SHORTCUT_DOWN);
Runnable task = () -> scene.getWindow().hide();
scene.getAccelerators().put(kc, task);

清单 12-1 中的程序展示了如何使用助记符和快捷键。按 Alt + 1 和 Alt + 2 分别激活按钮 1 和按钮 2。按下这些按钮会改变Label的文本。按快捷键+ X 将关闭窗口。

// MnemonicTest.java
package com.jdojo.control;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import javafx.scene.input.Mnemonic;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

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

        @Override
        public void start(Stage stage) {
                VBox root = new VBox();
                root.setSpacing(10);
                root.setStyle("-fx-padding: 10;" +
                              "-fx-border-style: solid inside;" +
                              "-fx-border-width: 2;" +
                              "-fx-border-insets: 5;" +
                              "-fx-border-radius: 5;" +
                              "-fx-border-color: blue;");

                Scene scene = new Scene(root);
                Label msg = new Label(
                      "Press Ctrl + X on Windows \nand " +
                      "\nMeta + X on Mac to close the window");
                Label lbl = new Label("Press Alt + 1 or Alt + 2");

                // Use Alt + 1 as the mnemonic for Button 1
                Button btn1 = new Button("Button _1");
                btn1.setOnAction(e -> lbl.setText("Button 1 clicked!"));

                // Use Alt + 2 as the mnemonic key for Button 2
                Button btn2 = new Button("Button 2");
                btn2.setOnAction(e ->
                         lbl.setText("Button 2 clicked!"));
                KeyCombination kc =
                         new KeyCodeCombination(KeyCode.DIGIT2,
                           KeyCombination.ALT_DOWN);
                Mnemonic mnemonic = new Mnemonic(btn2, kc);
                scene.addMnemonic(mnemonic);

                // Add an accelarator key to the scene
                KeyCombination kc4 =
                    new KeyCodeCombination(KeyCode.X,
                                 KeyCombination.SHORTCUT_DOWN);
                Runnable task = () -> scene.getWindow().hide();
                scene.getAccelerators().put(kc4, task);

                // Add all children to the VBox
                root.getChildren().addAll(msg, lbl, btn1, btn2);

                stage.setScene(scene);
                stage.setTitle("Using Mnemonics and Accelerators");
                stage.show();
        }
}

Listing 12-1Using Mnemonics and Accelerator Keys

了解标签控件

Label类的一个实例代表一个标签控件。顾名思义,Label只是一个标签,用来识别或描述屏幕上的另一个组件。它可以显示文本和/或图标。通常,Label被放置在它所描述的节点的旁边(右边或左边)或顶部。

A Label是不可穿越的焦点。也就是说,您不能使用 Tab 键将焦点设置为Label。一个Label控件不会产生任何在应用程序中通常使用的有趣事件。

一个Label控件也可以用来显示文本,如果没有足够的空间来显示整个文本,可以截断文本。请参考关于Labeled类的textOverrunellipsisString属性的 API 文档,了解更多关于如何在Label控件中控制文本截断行为的细节。

图 12-4 显示了一个带有两个Label控件的窗口,控件上有文字名字:和姓氏:。带有文本 First Name:的Label是一个指示器,指示用户应该在紧挨着它的字段中输入名字。类似的争论也适用于最后一个名字:Label控制。

img/336502_2_En_12_Fig4_HTML.jpg

图 12-4

带有两个Label控件的窗口

Label类有一个非常有用的ObjectProperty<Node>类型的labelFor属性。它被设置为场景图中的另一个节点。一个Label控件可以有助记符。默认情况下,Label控件的助记符解析设置为 false。当您按下Label的助记键时,焦点被设置到该LabellabelFor节点。下面的代码片段创建了一个TextField和一个LabelLabel设置助记符,启用助记符解析,并将TextField设置为其labelFor属性。当 Alt + F 键被按下时,焦点移动到TextField:

TextField fNameFld = new TextField();
Label fNameLbl = new Label("_First Name:"); // F is mnemonic
fNameLbl.setLabelFor(fNameFld);
fNameLbl.setMnemonicParsing(true);

清单 12-2 中的程序产生如图 12-4 所示的屏幕。按 Alt + F 和 Alt + L 在两个TextField控件之间切换焦点。

// LabelTest.java
// ... find in the book's download area.

Listing 12-2Using the Label Control

了解按钮

JavaFX 提供了三种代表按钮的控件:

  • 执行命令的按钮

  • 做出选择的按钮

  • 执行命令和做出选择的按钮

所有按钮类都继承自ButtonBase类。类图请参见图 12-2 。所有类型的按钮都支持ActionEvent。按钮被激活时会触发一个ActionEvent。可以用不同的方式激活按钮,例如,使用鼠标、助记键、加速键或其他组合键。

激活时执行命令的按钮称为命令按钮ButtonHyperlinkMenuButton类代表命令按钮。MenuButton让用户执行命令列表中的一个命令。用于向用户呈现不同选择的按钮被称为选择按钮ToggleButtonCheckBoxRadioButton类代表选择按钮。第三种按钮是前两种的混合。它们让用户执行命令或做出选择。SplitMenuButton类代表一个混合按钮。

Tip

所有按钮都标记为控件。因此,它们可以有文本内容和/或图形。所有类型的按钮都能够触发ActionEvent

了解命令按钮

您已经在多个实例中使用了命令按钮,例如,关闭窗口的关闭按钮。在这一节中,我将讨论用作命令按钮的按钮。

了解按钮控件

Button类的一个实例代表一个命令按钮。通常情况下,Button的标签是文本,并且向它注册了一个ActionEvent处理程序。默认情况下,Button类的mnemonicParsing属性被设置为 true。

Button可以处于三种模式之一:

  • 普通按钮

  • 默认按钮

  • 取消按钮

对于一个普通的按钮,当按钮被激活时,它的ActionEvent被触发。对于默认按钮,当 Enter 键被按下并且场景中没有其他节点消耗按键时,触发ActionEvent。对于“取消”按钮,当按下 Esc 键并且场景中没有其他节点消耗该按键时,会触发ActionEvent

默认情况下,Button是一个普通按钮。默认和取消模式由defaultButtoncancelButton属性表示。您可以将这些属性之一设置为 true,使按钮成为默认按钮或取消按钮。默认情况下,这两个属性都设置为 false。

下面的代码片段创建了一个普通的Button并添加了一个ActionEvent处理程序。当按钮被激活时,例如,用鼠标点击,调用newDocument()方法:

// A normal button
Button newBtn = new Button("New");
newBtn.setOnAction(e -> newDocument());

下面的代码片段创建了一个默认按钮并添加了一个ActionEvent处理程序。当按钮被激活时,调用save()方法。请注意,如果场景中没有其他节点消耗按键,默认的Button也会通过按回车键激活:

// A default button
Button saveBtn = new Button("Save");
saveBtn.setDefaultButton(true); // Make it a default button
saveBtn.setOnAction(e -> save());

清单 12-3 中的程序创建了一个正常按钮、一个默认按钮和一个取消按钮。它向所有三个按钮添加了一个ActionEvent监听器。请注意,所有按钮都有助记符(例如,N 代表New按钮)。当按钮被激活时,一条信息显示在Label中。您可以通过不同方式激活按钮:

  • 点击按钮

  • 使用 Tab 键和空格键将焦点设置到按钮上

  • 按 Alt 键和它们的助记键

  • 按下输入键激活Save按钮

  • 按 Esc 键激活Cancel按钮

无论您如何激活按钮,都会调用它们的ActionEvent处理程序。通常,按钮的ActionEvent处理程序包含按钮的命令。

// ButtonTest.java
// ... find in the book's download area.

Listing 12-3Using the Button Class to Create Command Buttons

Tip

可以将场景中的多个按钮设置为默认按钮或取消按钮。但是,只使用第一个。在一个场景中声明多个默认按钮和取消按钮是糟糕的设计。默认情况下,JavaFX 用浅色突出显示默认按钮,使其具有独特的外观。您可以使用 CSS 样式自定义默认按钮和取消按钮的外观。将同一个按钮设置为默认按钮和取消按钮也是允许的,但是这样做是糟糕设计的标志。

一个Button的默认 CSS 样式类名是buttonButton类支持两个 CSS 伪类:defaultcancel。您可以使用这些伪类来自定义“寻找默认值”和“取消”按钮。以下 CSS 样式将默认按钮的文本颜色设置为蓝色,取消按钮的文本颜色设置为灰色:

.button:default {
        -fx-text-fill: blue;
}

.button:cancel {
        -fx-text-fill: gray;
}

Tip

您可以使用 CSS 样式来创建时尚的按钮。请访问网站 http://fxexperience.com/2011/12/styling-fx-buttons-with-css/ 查看示例。

了解超链接控件

Hyperlink类的一个实例表示一个超链接控件,看起来像网页中的超链接。在网页中,超链接用于导航到另一个网页。然而,在 JavaFX 中,当一个Hyperlink控件被激活时,例如通过点击它,就会触发一个ActionEvent,并且您可以在ActionEvent处理程序中自由地执行任何操作。

一个Hyperlink控件只是一个看起来像超链接的按钮。默认情况下,助记符解析是关闭的。一个Hyperlink控件可以有焦点,默认情况下,当它有焦点时,它会绘制一个虚线矩形边框。当鼠标光标悬停在一个Hyperlink控件上时,光标会变成一个手形,并且其文本带有下划线。

Hyperlink类包含一个BooleanProperty类型的visited属性。当Hyperlink控件第一次被激活时,它被认为是“被访问过的”,并且visited属性被自动设置为真。所有访问过的超链接以不同于未访问过的颜色显示。您也可以使用Hyperlink类的setVisited()方法手动设置visited属性。

下面的代码片段创建了一个文本为"JDojo"Hyperlink控件,并为Hyperlink添加了一个ActionEvent处理程序。当Hyperlink被激活时, www.jdojo.com 网页在WebView中打开,这是另一个显示网页的 JavaFX 控件。在这里,我将使用它,不做任何解释:

Hyperlink jdojoLink = new Hyperlink("JDojo");
WebView webview = new WebView();
jdojoLink.setOnAction(e -> webview.getEngine().load("http://www.jdojo.com"));

清单 12-4 中的程序向一个BorderPane的顶部区域添加了三个Hyperlink控件。一个WebView控件被添加到中心区域。当您单击其中一个超链接时,会显示相应的网页。

// HyperlinkTest.java
// ... find in the book's download area.

Listing 12-4Using the Hyperlink Control

了解菜单按钮控件

一个控件看起来像一个按钮,行为像一个菜单。当它被激活时(通过单击或其他方式),它会以弹出菜单的形式显示一个选项列表。菜单中的选项列表保存在一个ObservableList<MenuItem>中,其引用由getItems()方法返回。要在菜单选项被选中时执行命令,您需要将ActionEvent处理程序添加到MenuItem

下面的代码片段创建了一个带有两个MenuItemMenuButton,每个菜单项都有一个ActionEvent处理程序。图 12-5 显示MenuButton处于不显示和显示两种状态。

img/336502_2_En_12_Fig5_HTML.jpg

图 12-5

MenuButton处于不显示和显示状态

// Create two menu items with an ActionEvent handler.
// Assume that the loadPage() method exists
MenuItem jdojo = new MenuItem("JDojo");
jdojo.setOnAction(e -> loadPage("http://www.jdojo.com"));

MenuItem yahoo = new MenuItem("Yahoo");
yahoo.setOnAction(e -> loadPage("http://www.yahoo.com"));

// Create a MenuButton and the two menu items
MenuButton links = new MenuButton("Visit");
links.getItems().addAll(jdojo, yahoo);

MenuButton类声明了两个属性:

  • popupSide

  • showing

popupSide属性为ObjectProperty<Side>类型,showing属性为ReadOnlyBooleanProperty类型。

属性决定了菜单的哪一面应该被显示。其值是Side枚举中的常量之一:TOPLEFTBOTTOMRIGHT。默认值为Side.BOTTOMMenuItem中的箭头表示由popupSide属性设置的方向。图 12-5 中箭头向下,表示popupSide属性设置为Side.BOTTOM。只有在该侧有空间显示菜单时,菜单才会按popupSide属性中设置的方向打开。如果没有可用的空间,JavaFX 运行时将明智地决定菜单应该显示在哪一边。当弹出菜单显示时,showing属性的值为真。否则就是假的。

清单 12-5 中的程序使用MenuButton控件创建了一个应用程序,其工作方式类似于清单 12-4 中使用Hyperlink控件的程序。运行应用程序,点击窗口右上方的访问MenuButton,选择要打开的页面。

// MenuButtonTest.java
// ... find in the book's download area.

Listing 12-5Using the MenuButton Control

了解选择按钮

JavaFX 提供了几个控件,用于从可用选项列表中进行一个或多个选择:

  • ToggleButton

  • CheckBox

  • RadioButton

Tip

JavaFX 还提供了ChoiceBoxComboBoxListView控件,允许用户从多个可用选项中进行选择。我将在单独的部分讨论这些控件。

这三个控件都被标记为控件,它们帮助您以不同的格式向用户提供多种选择。可用选择的数量可以从 2 到 N 变化,其中 N 是大于 2 的数。

从可用选项中进行选择可能是互斥的。也就是说,用户只能从选项列表中做出一个选择。如果用户改变选择,则自动取消选择先前的选择。例如,MaleFemaleUnknown三个选项的性别选择列表是互斥的。用户必须只选择三个选项中的一个,而不是两个或更多。在这种情况下通常使用ToggleButtonRadioButton控制。

有一种特殊的选择情况,选择的数量是两个。在这种情况下,选择属于boolean类型:对或错。有时,它也被称为是/否开/关选择。在这种情况下通常使用ToggleButtonCheckBox控件。

有时,用户可以从选项列表中进行多项选择。例如,您可以向用户提供一个爱好列表,让用户从列表中选择零个或多个爱好。这种情况下通常使用ToggleButtonCheckBox控件。

了解切换按钮控件

ToggleButton是一个双态按钮控件。这两种状态是选中未选中。它的selected属性表示它是否被选中。当selected属性处于选中状态时为真。否则就是假的。当它处于选中状态时,它会保持按下状态。按下它可以在选中和未选中状态之间切换,因此得名ToggleButton。对于ToggleButton来说,助记符解析是默认启用的。

图 12-6 显示了四个标签为春、夏、秋、冬的切换按钮。其中两个切换按钮“弹簧”和“下落”被选中,另外两个未选中。

img/336502_2_En_12_Fig6_HTML.jpg

图 12-6

显示四个切换按钮的窗口

使用下面的代码,您可以像创建Button一样创建一个ToggleButton:

ToggleButton springBtn = new ToggleButton("Spring");

一个ToggleButton用于选择一个选项,而不是执行一个命令。通常情况下,您不会将ActionEvent处理程序添加到ToggleButton中。有时,您可以使用ToggleButton来开始或停止一个动作。为此,您需要为其选定的属性添加一个ChangeListener

Tip

每次单击ToggleButton时,都会调用它的ActionEvent处理程序。请注意,第一次单击选择了一个ToggleButton,第二次单击取消了选择。如果您选择和取消选择一个ToggleButton,那么ActionEvent处理程序将被调用两次。

可在一组中使用切换按钮,从中可选择零个或一个ToggleButton。要将切换按钮添加到组中,您需要将它们添加到一个ToggleGroup中。ToggleButton类包含一个 t oggleGroup属性。要将ToggleButton添加到ToggleGroup中,请将ToggleButtontoggleGroup属性设置为组。将toggleGroup属性设置为null会从组中删除一个ToggleButton。下面的代码片段创建了四个切换按钮,并将它们添加到一个ToggleGroup中:

ToggleButton springBtn = new ToggleButton("Spring");
ToggleButton summerBtn = new ToggleButton("Summer");
ToggleButton fallBtn = new ToggleButton("Fall");
ToggleButton winterBtn = new ToggleButton("Winter");

// Create a ToggleGroup
ToggleGroup group = new ToggleGroup();

// Add all ToggleButtons to the ToggleGroup
springBtn.setToggleGroup(group);
summerBtn.setToggleGroup(group);
fallBtn.setToggleGroup(group);
winterBtn.setToggleGroup(group);

每个ToggleGroup保持一个ObservableList<Toggle>。注意,Toggle是一个由ToggleButton类实现的接口。ToggleGroup类的getToggles()方法返回组中Toggle的列表。通过将ToggleButton添加到由getToggles()方法返回的列表中,可以将ToggleButton添加到组中。前面的代码片段可以重写如下:

ToggleButton springBtn = new ToggleButton("Spring");
ToggleButton summerBtn = new ToggleButton("Summer");
ToggleButton fallBtn = new ToggleButton("Fall");
ToggleButton winterBtn = new ToggleButton("Winter");

// Create a ToggleGroup
ToggleGroup group = new ToggleGroup();

// Add all ToggleButtons to the ToggleGroup
group.getToggles().addAll(springBtn, summerBtn, fallBtn, winterBtn);

ToggleGroup类包含一个selectedToggle属性,用于跟踪组中选定的TogglegetSelectedToggle()方法返回被选中的Toggle的引用。如果组中没有选择Toggle,则返回null。如果您想跟踪在一个ToggleGroup中选择的变化,那么就给这个属性添加一个ChangeListener

Tip

您可以在一个ToggleGroup中选择零个或一个ToggleButton。选择群组中的ToggleButton会取消选择已经选择的ToggleButton。点击一个组中已经选中的ToggleButton会取消选中它,使该组中没有ToggleButton被选中。

清单 12-6 中的程序为一个ToggleGroup添加了四个切换按钮。您可以从组中选择无或最多一个ToggleButton。图 12-7 显示了两个截图:一个是没有选择的时候,一个是选择了标签为 Summer 的ToggleButton的时候。程序向组中添加一个ChangeListener来跟踪选择的变化,并在一个Label控件中显示所选ToggleButton的标签。

img/336502_2_En_12_Fig7_HTML.jpg

图 12-7

一个ToggleGroup中的四个切换按钮允许一次选择一个按钮

// ToggleButtonTest.java
// ... find in the book's download area.

Listing 12-6Using Toggle Buttons in a ToggleGroup and Tracking the Selection

了解单选按钮控件

RadioButton类的一个实例代表一个单选按钮。它继承自ToggleButton类。因此,它具有切换按钮的所有功能。与切换按钮相比,单选按钮的呈现方式不同。像切换按钮一样,单选按钮可以处于两种状态之一:选中未选中。它的selected属性表示它的当前状态。像切换按钮一样,它的助记符解析默认是启用的。就像一个切换按钮,当它被选中和取消选中时,它也会发送一个ActionEvent。图 12-8 显示了一个文本为 Summer 的RadioButton处于选中和未选中状态。

img/336502_2_En_12_Fig8_HTML.png

图 12-8

显示处于选中和未选中状态的单选按钮

单选按钮的使用与切换按钮的使用有很大的不同。回想一下,当在一个组中使用切换按钮时,该组中可能没有任何选定的切换按钮。当在组中使用单选按钮时,组中必须有一个选中的单选按钮。与切换按钮不同,单击组中选定的单选按钮不会取消对它的选择。为了强制执行必须在一组单选按钮中选择一个单选按钮的规则,默认情况下以编程方式从该组中选择一个单选按钮。

Tip

当用户必须从选项列表中进行选择时,可以使用单选按钮。当用户可以从选项列表中进行选择或不选择时,使用切换按钮。

清单 12-7 中的程序展示了如何在ToggleGroup中使用单选按钮。图 12-9 显示了运行代码结果的窗口。该程序与之前使用切换按钮的程序非常相似。使用以下代码,Summer 被设置为默认选择:

// Select the default season as Summer
summerBtn.setSelected(true);

将更改监听器添加到组中后,在单选按钮中设置默认季节,以便正确更新显示所选季节的消息。

img/336502_2_En_12_Fig9_HTML.jpg

图 12-9

一个ToggleGroup中的四个单选按钮

// RadioButtonTest.java
// ... find in the book's download area.

Listing 12-7Using Radio Buttons in a ToggleGroup and Tracking the Selection

了解复选框控件

CheckBox是三态选择控件:选中未选中未定义。未定义状态也称为不确定状态。A CheckBox支持三种选择:真/假/未知或是/否/未知。通常,CheckBox有文本作为标签,但没有图形(尽管它可以)。点击CheckBox将其从一种状态转换到另一种状态,在三种状态之间循环。

为一个CheckBox画一个方框。在未选中状态下,该框为空。当复选框处于选中状态时,它会显示一个勾号(或复选标记)。在未定义状态下,框中会出现一条水*线。图 12-10 显示了标记为饥饿的CheckBox的三种状态。

img/336502_2_En_12_Fig10_HTML.png

图 12-10

显示处于未选中、选中和未定义状态的复选框

默认情况下,CheckBox控件只支持两种状态:选中未选中allowIndeterminate属性指定第三种状态(未定义状态)是否可供选择。默认情况下,它设置为 false:

// Create a CheckBox that supports checked and unchecked states only
CheckBox hungryCbx = new CheckBox("Hungry");

// Create a CheckBox and configure it to support three states
CheckBox agreeCbx = new CheckBox("Hungry");
agreeCbx.setAllowIndeterminate(true);

CheckBox类包含selectedindeterminate属性来跟踪它的三种状态。如果indeterminate属性为真,则处于未定义状态。如果indeterminate属性为 false,则它是已定义的,并且可能处于选中或未选中状态。如果indeterminate属性为假而selected属性为真,则处于选中状态。如果indeterminate属性为假,selected属性为假,则处于未选中状态。表 12-3 总结了确定复选框状态的规则。

表 12-3

根据复选框的不确定属性和选定属性确定其状态

|

不确定

|

选中

|

状态

false true 检查
false false 未加抑制的
true true/false 不明确的

有时,您可能想要检测复选框中的状态转换。因为复选框在两个属性中维护状态信息,所以您需要向这两个属性添加一个ChangeListener。当一个复选框被点击时,一个ActionEvent被触发。你也可以使用一个ActionEvent来检测复选框的状态变化。下面的代码片段展示了如何使用两个ChangeListener来检测一个CheckBox中的状态变化。假设changed()方法和代码的其余部分属于同一个类:

// Create a CheckBox to support three states
CheckBox agreeCbx = new CheckBox("I agree");
agreeCbx.setAllowIndeterminate(true);

// Add a ChangeListener to the selected and indeterminate properties
agreeCbx.selectedProperty().addListener(this::changed);
agreeCbx.indeterminateProperty().addListener(this::changed);
...
// A change listener to track the selection in the group
public void changed(ObservableValue<? extends Boolean> observable,
                    Boolean oldValue,
                    Boolean newValue) {
        String state = null;
        if (agreeCbx.isIndeterminate()) {
                state = "Undefined";
        } else if (agreeCbx.isSelected()) {
                state = "Checked";
        } else {
                state = "Unchecked";
        }
        System.out.println(state);
}

清单 12-8 中的程序展示了如何使用CheckBox控件。图 12-11 显示了运行该代码产生的窗口。程序创建了两个CheckBox控件。饥饿的 ?? 只支持两个州。我同意CheckBox配置为支持三种状态。当您通过单击“我同意”CheckBox来更改其状态时,顶部的Label会显示该状态的描述。

img/336502_2_En_12_Fig11_HTML.jpg

图 12-11

两个复选框:一个使用两种状态,一个使用三种状态

// CheckBoxTest.java
// ... find in the book's download area.

Listing 12-8Using the CheckBox Control

一个CheckBox的默认 CSS 样式类名是check-boxCheckBox类支持三个 CSS 伪类:selecteddeterminateindeterminate。当selected属性为真时,selected伪类适用。当indeterminate属性为假时,determinate伪类适用。当indeterminate属性为真时,indeterminate伪类适用。

CheckBox控件包含两个子结构:boxmark。您可以设计它们的样式来改变它们的外观。您可以更改框的背景色和边框,也可以更改刻度线的颜色和形状。box 和 mark 都是StackPane的实例。显示的刻度线给出了StackPane的形状。您可以通过在 CSS 中提供不同的形状来更改标记的形状。通过更改标记的背景颜色,可以更改刻度线的颜色。下面的 CSS 将用褐色显示盒子,用红色显示刻度线:

.check-box .box {
        -fx-background-color: tan;
}

.check-box:selected .mark {
    -fx-background-color: red;
}

了解混合动力按钮控制

根据我们对不同按钮类型的定义,一个SplitMenuButton属于混合型。它结合了弹出式菜单和命令按钮的功能。它让你像选择一个MenuButton控件一样选择一个动作,像执行一个Button控件一样执行一个命令。SplitMenuButton类继承自MenuButton类。

一个SplitMenuButton分为两个区域:动作区和菜单打开区。当您在操作区域中单击时,ActionEvent被触发。注册的ActionEvent处理程序执行命令。单击菜单打开区域时,会显示一个菜单,用户可以从中选择要执行的操作。Mnemonic默认启用SplitMenuButton解析。

图 12-12 显示了一个SplitMenuButton处于两种状态。左边的图片显示它处于折叠状态。在右图中,它显示了菜单项。请注意将控件分成两半的垂直线。包含文本 Home 的那一半是操作区域。包含向下箭头的另一半是菜单打开区域。

img/336502_2_En_12_Fig12_HTML.png

图 12-12

处于折叠和显示状态的

您可以使用以下代码的构造器创建一个有菜单项或没有菜单项的SplitMenuButton:

// Create an empty SplitMenuItem
SplitMenuButton splitBtn = new SplitMenuButton();
splitBtn.setText("Home"); // Set the text as "Home"

// Create MenuItems
MenuItem jdojo = new MenuItem("JDojo");
MenuItem yahoo = new MenuItem("Yahoo");
MenuItem google = new MenuItem("Google");

// Add menu items to the MenuButton
splitBtn.getItems().addAll(jdojo, yahoo, google);

您需要添加一个ActionEvent处理程序,以便在动作区域中单击SplitMenuButton时执行一个动作:

// Add ActionEvent handler when "Home" is clicked
splitBtn.setOnAction(e -> /* Take some action here */);

清单 12-9 中的程序展示了如何使用SplitMenuButton。它在一个BorderPane的右上角区域添加了一个带有文本 Home 和三个菜单项的SplitMenuButton。在中心区域增加一个WebView。当你点击主页时,打开 www.jdojo.com 网页。当您通过单击向下箭头使用菜单选择网站时,相应的网站将会打开。该程序与您之前使用MenuButtonHyperlink控件开发的程序非常相似。

// SplitMenuButtonTest.java
// ... find in the book's download area.

Listing 12-9Using the SplitMenuButton Control

从项目列表中进行选择

在前面几节中,您已经看到了如何向用户显示一个项目列表,例如,使用切换按钮和单选按钮。切换和单选按钮更容易使用,因为所有选项对用户总是可见的。然而,它们占用了大量的屏幕空间。考虑使用单选按钮向用户显示美国所有 50 个州的名称。这会占用很多空间。有时,列表中的所有可用项目都不适合选择,因此您需要给用户一个机会来输入列表中没有的新项目。

JavaFX 提供了一些允许用户从项目列表中选择项目的控件。与按钮相比,它们占用更少的空间。它们提供高级功能来自定义它们的外观和行为。我将在后续章节中讨论以下此类控件:

  • ChoiceBox

  • ComboBox

  • ListView

  • ColorPicker

  • DatePicker

ChoiceBox允许用户从预定义项目的小列表中选择一个项目。ComboBoxChoiceBox的高级版本。它有很多特性,例如,可以编辑或者改变列表中项目的外观,这些都是ChoiceBox中没有的。ListView为用户提供从项目列表中选择多个项目的能力。通常情况下,用户始终可以看到ListView中的所有或多个项目。ColorPicker允许用户从标准调色板中选择一种颜色,或以图形方式定义自定义颜色。DatePicker允许用户从日历弹出窗口中选择日期。用户可以选择以文本形式输入日期。ComboBoxColorPickerDatePicker具有相同的超类ComboBoxBase

了解选择框控件

ChoiceBox用于让用户从一个小项目列表中选择一个项目。这些项目可以是任何类型的对象。ChoiceBox是一个参数化类。参数类型是列表中项目的类型。如果您想在一个ChoiceBox中存储混合类型的项目,您可以使用它的

// Create a ChoiceBox for any type of items
ChoiceBox<Object> seasons = new ChoiceBox<>();

// Instead create a ChoiceBox for String items
ChoiceBox<String> seasons = new ChoiceBox<>();

您可以在使用以下代码创建ChoiceBox时指定列表项:

ObservableList<String> seasonList = FXCollections.<String>observableArrayList(
        "Spring", "Summer", "Fall", "Winter");
ChoiceBox<String> seasons = new ChoiceBox<>(seasonList);

在您创建了一个ChoiceBox之后,您可以使用items属性将项目添加到它的项目列表中,该属性属于ObjectProperty<ObservableList<T>>类型,其中TChoiceBox的类型参数。以下代码将完成这一任务:

ChoiceBox<String> seasons = new ChoiceBox<>();
seasons.getItems().addAll("Spring", "Summer", "Fall", "Winter");

图 12-13 显示了四种不同状态下的选择框。在物品清单中有四个季节的名字。第一张图片(标记为#1)显示了没有选择时的初始状态。用户可以使用鼠标或键盘打开项目列表。单击控件内的任何位置都会在弹出窗口中打开项目列表,如标记为#2 的图片所示。当控件具有焦点时,按下向下箭头键也会打开项目列表。您可以通过单击或使用上/下箭头和 Enter 键从列表中选择一个项目。当您选择一个项目时,显示项目列表的弹出窗口被折叠,所选项目显示在控件中,如标记为#3 的图片所示。标签为#4 的图片显示了当选择一个项目(在本例中为 Spring)并显示列表项目时的控件。弹出窗口显示一个复选标记,表示控件中的该项已被选中。表 12-4 列出了在ChoiceBox类中声明的属性。

表 12-4

ChoiceBox类中声明的属性

|

财产

|

类型

|

描述

converter ObjectProperty <StringConverter<T>> 它充当一个转换器对象,调用该对象的toString()方法来获取列表中项目的字符串表示。
items ObjectProperty <ObservableList<T>> 这是要在ChoiceBox中显示的选项列表。
selectionModel ObjectProperty <SingleSelectionModel<T>> 它作为一个选择模型来跟踪ChoiceBox中的选择。
showing ReadOnlyBooleanProperty 它的 true 值指示控件正在向用户显示选项列表。它的 false 值表示选项列表是折叠的。
value ObjectProperty<T> 这是在ChoiceBox中选择的项目。

img/336502_2_En_12_Fig13_HTML.jpg

图 12-13

不同状态下的选择框

Tip

您并不局限于使用鼠标或键盘来显示项目列表。您可以分别使用show()hide()方法以编程方式显示和隐藏列表。

ChoiceBoxvalue属性存储控件中选中的项目。其类型为ObjectProperty<T>,其中T为控件的类型参数。如果用户没有选择项目,其值为null。下面的代码片段设置了value属性:

// Create a ChoiceBox for String items
ChoiceBox<String> seasons = new ChoiceBox<String>();
seasons.getItems().addAll("Spring", "Summer", "Fall", "Winter");

// Get the selected value
String selectedValue = seasons.getValue();

// Set a new value
seasons.setValue("Fall");

当您使用setValue()方法设置一个新值时,如果该值存在于项目列表中,ChoiceBox将选择控件中的指定值。可以设置项目列表中不存在的值。在这种情况下,value 属性包含新设置的项,但控件不显示它。控制项会持续显示先前选取的项目(如果有的话)。当新项目后来被添加到项目列表中时,控件显示在value属性中设置的项目。

ChoiceBox需要跟踪选中的项目及其在项目列表中的索引。为此,它使用一个单独的对象,称为选择模型ChoiceBox类包含一个selectionModel属性来存储项目选择细节。ChoiceBox使用SingleSelectionModel类的一个对象作为它的选择模型,但是你可以使用你自己的选择模型。默认选择模型在几乎所有情况下都有效。选择模型为您提供了与选择相关的功能:

  • 它允许您使用列表中项目的索引来选择项目。

  • 它允许您选择列表中的第一个、下一个、上一个或最后一个项目。

  • 它允许您清除选择。

  • 它的selectedIndexselectedItem属性跟踪所选项的索引和值。您可以向这些属性添加一个ChangeListener,以处理ChoiceBox中选择的变化。当没有选择项目时,选择的指标为–1,选择的项目为null

下面的代码片段通过默认选择列表中的第一项来强制在ChoiceBox中输入一个值:

ChoiceBox<String> seasons = new ChoiceBox<>();
seasons.getItems().addAll("Spring", "Summer", "Fall", "Winter", "Fall");

// Select the first item in the list
seasons.getSelectionModel().selectFirst();

使用选择模型的selectNext()方法从列表中选择下一个项目。当最后一项已经被选中时调用selectNext()方法没有任何效果。使用selectPrevious()selectLast()方法分别选择列表中的前一项和最后一项。select(int index)select(T item)方法分别使用项目的索引和值来选择项目。注意,您也可以使用ChoiceBoxsetValue()方法,通过值从列表中选择一个项目。选择模型的clearSelection()方法清除当前选择,将ChoiceBox返回到好像没有选择任何项目的状态。

清单 12-10 中的程序显示如图 12-14 所示的窗口。它使用一个带有四季列表的ChoiceBox。默认情况下,程序从列表中选择第一个季节。默认情况下,应用程序会强制用户选择一个季节名称。它将ChangeListener添加到选择模型的selectedIndexselectedItem属性中。他们在标准输出上打印选择更改的详细信息。当前选择显示在一个Label控件中,该控件的text属性绑定到ChoiceBoxvalue属性。从列表中选择不同的项目,并观察标准输出和窗口以了解详细信息。

img/336502_2_En_12_Fig14_HTML.jpg

图 12-14

带有预选项目的选择框

// ChoiceBoxTest.java
// ... find in the book's download area.

Listing 12-10Using ChoiceBox with a Preselected Item

选择框中使用域对象

在前面的例子中,您使用了String对象作为选择框中的项目。您可以使用任何对象类型作为项目。ChoiceBox调用每一项的toString()方法,并在弹出列表中显示返回值。下面的代码片段创建了一个选择框,并添加了四个Person对象作为它的项目。图 12-15 显示选择框处于showing状态。注意,这些项目是使用从Person类的toString()方法返回的String对象显示的。

img/336502_2_En_12_Fig15_HTML.jpg

图 12-15

显示四个Person对象作为其项目列表的选择框

import com.jdojo.mvc.model.Person;
import javafx.scene.control.ChoiceBox;
...
ChoiceBox<Person> persons = new ChoiceBox<>();
persons.getItems().addAll(new Person("John", "Jacobs", null),
                          new Person("Donna", "Duncan", null),
                          new Person("Layne", "Estes", null),
                          new Person("Mason", "Boyd", null));

通常,对象的toString()方法返回一个代表对象状态的String。它并不意味着提供要在选择框中显示的对象的自定义字符串表示。ChoiceBox类包含一个converter属性。这是一辆StringConverter<T>型的ObjectProperty。一个StringConverter<T>对象充当从对象类型T到字符串的转换器,反之亦然。该类被声明为抽象类,如下面的代码片段所示:

public abstract class StringConverter<T> {
        public abstract String toString(T object);
        public abstract T fromString(String string);
}

toString(T object)方法将类型T的对象转换成一个字符串。fromString(String string)方法将一个字符串转换成一个T对象。

默认情况下,选择框中的converter属性为null。如果设置了,则调用转换器的toString(T object)方法来获取项目列表,而不是项目的类的toString()方法。清单 12-11 中显示的PersonStringConverter类可以充当选择框中的转换器。请注意,您将fromString()方法中的参数string视为一个人的名字,并试图从中构造一个Person对象。您不需要为选择框实现fromString()方法。它将被用在一个ComboBox中,我接下来会讨论这个。ChoiceBox将只使用toString(Person p)方法。

// PersonStringConverter.java
package com.jdojo.control;

import com.jdojo.mvc.model.Person;
import javafx.util.StringConverter;

public class PersonStringConverter extends StringConverter<Person> {
        @Override
        public String toString(Person p) {
                return p == null?
                         null : p.getLastName() + ", " + p.getFirstName();
        }

        @Override
        public Person fromString(String string) {
                Person p = null;
                if (string == null) {
                        return p;
                }

                int commaIndex = string.indexOf(",");
                if (commaIndex == -1) {
                        // Treat the string as first name
                        p = new Person(string, null, null);
                } else {
                        // Ignoring string bounds check for brevity
                        String firstName =
                                    string.substring(commaIndex + 2);
                        String lastName = string.substring(
                                    0, commaIndex);
                        p = new Person(firstName, lastName, null);
                }
                return p;
        }
}

Listing 12-11A Person to String Converter

下面的代码片段使用了一个ChoiceBox中的转换器将项目列表中的Person对象转换成字符串。图 12-16 显示选择框处于showing状态。

img/336502_2_En_12_Fig16_HTML.jpg

图 12-16

Person在选择框中使用转换器的对象

import com.jdojo.mvc.model.Person;
import javafx.scene.control.ChoiceBox;
...
ChoiceBox<Person> persons = new ChoiceBox<>();

// Set a converter to convert a Person object to a String object
persons.setConverter(new PersonStringConverter());

// Add five person objects to the ChoiceBox
persons.getItems().addAll(new Person("John", "Jacobs", null),
                          new Person("Donna", "Duncan", null),
                          new Person("Layne", "Estes", null),
                          new Person("Mason", "Boyd", null));

选择框中允许空值

有时,选择框可能允许用户选择null作为有效选项。这可以通过使用null作为选择列表中的一项来实现,如下面的代码所示:

ChoiceBox<String> seasons = new ChoiceBox<>();
seasons.getItems().addAll(null, "Spring", "Summer", "Fall", "Winter");

前面的代码片段产生了一个如图 12-17 所示的选择框。请注意,null项显示为空白。

img/336502_2_En_12_Fig17_HTML.png

图 12-17

选择框中的选项为空

通常需要将null选项显示为自定义字符串,例如"[None]"。这可以通过转换器来实现。在上一节中,您使用了一个转换器来定制Person对象的选择。这里,您将使用转换器为null定制选择项。您也可以在一个转换器中完成这两项工作。下面的代码片段使用带有ChoiceBox的转换器将null选项转换为"[None]"。图 12-18 显示了产生的选择框。

img/336502_2_En_12_Fig18_HTML.png

图 12-18

转换为"[None]"的选择框中的null选项

ChoiceBox<String> seasons = new ChoiceBox<>();
seasons.getItems().addAll(null, "Spring", "Summer", "Fall", "Winter");

// Use a converter to convert null to "[None]"
seasons.setConverter(new StringConverter<String>() {
        @Override
        public String toString(String string) {
                return (string == null) ? "[None]" : string;
        }

        @Override
        public String fromString(String string) {
                return string;
        }
});

选择框中使用分隔符

有时,您可能希望将选择分成不同的组。假设您想在早餐菜单中显示水果和熟食,并且想将它们分开。您可以使用Separator类的一个实例来实现这一点。它在选项列表中显示为一条水*线。A Separator不可选择。下面的代码片段创建了一个选择框,其中的一项作为Separator。图 12-19 显示选择框处于展示状态。

img/336502_2_En_12_Fig19_HTML.jpg

图 12-19

使用分隔符的选择框

ChoiceBox breakfasts = new ChoiceBox();
breakfasts.getItems().addAll("Apple", "Banana", "Strawberry",
                      new Separator(),
                      "Apple Pie", "Donut", "Hash Brown");

用 CSS 对选择框进行样式化

一个ChoiceBox的默认 CSS 样式类名是choice-boxChoiceBox类支持一个showing CSS 伪类,当showing属性为真时应用。

ChoiceBox控件包含两个子结构:open-buttonarrow。您可以设计它们的样式来改变它们的外观。两者都是StackPane的实例。ChoiceBox显示在Label中选择的项目。选择列表显示在 ID 设置为choice-box-popup-menuContextMenu中。每个选项都显示在一个 id 设置为choice-box-menu-item的菜单项中。以下样式自定义ChoiceBox控件。目前,没有办法自定义单个选择框的弹出菜单。该样式将影响ChoiceBox控件在其设置级别(场景或布局窗格)的所有实例。

/* Set the text color and font size for the selected item in the control */
.choice-box .label {
        -fx-text-fill: blue;
        -fx-font-size: 8pt;
}

/* Set the text color and text font size for choices in the popup list */
#choice-box-menu-item * {
        -fx-text-fill: blue;
        -fx-font-size: 8pt;
}

/* Set background color of the arrow */
.choice-box .arrow {
        -fx-background-color: blue;
}

/* Set the background color for the open-button area */
.choice-box .open-button {
    -fx-background-color: yellow;
}

/* Change the background color of the popup */
#choice-box-popup-menu {
        -fx-background-color: yellow;
}

了解组合框控件

ComboBox用于让用户从项目列表中选择一个项目。你可以把ComboBox看作是ChoiceBox的高级版本。ComboBox高度可定制。ComboBox类继承自ComboBoxBase类,后者为所有类似ComboBox的控件提供通用功能,如ComboBoxColorPickerDatePicker。如果您想创建一个自定义控件,允许用户从弹出列表中选择一个项目,您需要从ComboBoxBase类继承您的控件。

ComboBox中的项目列表可以包括任何类型的对象。ComboBox是一个参数化类。参数类型是列表中项目的类型。如果您想在一个ComboBox中存储混合类型的项目,您可以使用它的

// Create a ComboBox for any type of items
ComboBox<Object> seasons = new ComboBox<>();

// Instead create a ComboBox for String items
ComboBox<String> seasons = new ComboBox<>();

您可以在创建ComboBox时指定列表项,如以下代码所示:

ObservableList<String> seasonList = FXCollections.<String>observableArrayList(
    "Spring", "Summer", "Fall", "Winter");
ComboBox<String> seasons = new ComboBox<>(seasonList);

创建组合框后,可以使用items属性将项目添加到项目列表中,该属性属于ObjectProperty<ObservableList<T>>类型,其中T是组合框的类型参数,如以下代码所示:

ComboBox<String> seasons = new ComboBox<>();
seasons.getItems().addAll("Spring", "Summer", "Fall", "Winter");

ChoiceBox一样,ComboBox需要跟踪选中的项目及其在项目列表中的索引。为此,它使用一个单独的对象,称为选择模型ComboBox类包含一个selectionModel属性来存储项目选择细节。ComboBox使用一个SingleSelectionModel类的对象作为它的选择模型。选择模型允许您从项目列表中选择一个项目,并允许您添加ChangeListener来跟踪索引和项目选择的变化。请参阅“了解选择框控件”一节,了解使用选择模型的更多详情。

ChoiceBox不同,ComboBox是可以编辑的。它的editable属性指定它是否是可编辑的。默认情况下,它不可编辑。当它可编辑时,它使用一个TextField控件来显示所选择或输入的项目。ComboBox类的editor属性存储了TextField的引用,如果组合框不可编辑,则为null,如下面的代码所示:

ComboBox<String> breakfasts = new ComboBox<>();

// Add some items to choose from
breakfasts.getItems().addAll("Apple", "Banana", "Strawberry");

// By making the control editable, let users enter an item
breakfasts.setEditable(true);

ComboBox有一个value属性,存储当前选择或输入的值。注意,当用户在可编辑的组合框中输入值时,输入的字符串被转换为组合框的项目类型T。如果项目类型不是字符串,则需要一个StringConverter<T>String值转换为类型T。我将很快给出一个例子。

您可以为组合框设置提示文本,当控件可编辑、没有焦点且其value属性为null时,将显示该提示文本。提示文本存储在promptText属性中,该属性属于StringProperty类型,如以下代码所示:

breakfasts.setPromptText("Select/Enter an item"); // Set a prompt text

ComboBox类包含一个placeholder属性,它存储一个Node引用。当项目列表为空或null时,弹出区域显示占位符节点。下面的代码片段将一个Label设置为占位符:

Label placeHolder = new Label("List is empty.\nPlease enter an item");
breakfasts.setPlaceholder(placeHolder);

清单 12-12 中的程序创建了两个ComboBox控件:seasonsbreakfasts。包含季节列表的组合框不可编辑。包含早餐项目列表的组合框是可编辑的。图 12-20 显示了当用户选择一个季节并输入一个早餐项目,甜甜圈,它不在早餐项目列表中时的屏幕截图。一个Label控件显示用户选择。当你在早餐组合框中输入一个新值时,你需要改变焦点,按回车键,或者打开弹出列表刷新消息Label

img/336502_2_En_12_Fig20_HTML.jpg

图 12-20

两个ComboBox控件:一个不可编辑,一个可编辑

// ComboBoxTest.java
// ... find in the book's download area.

Listing 12-12Using ComboBox Controls

检测组合框中的值变化

检测不可编辑的组合框中的项目变化很容易通过向其选择模型的selectedIndexselectedItem属性添加一个ChangeListener来执行。详情请参考“了解选择框控件”一节。

您仍然可以对selectedItem属性使用ChangeListener来检测可编辑组合框中的值何时改变,方法是从项目列表中选择或输入新值。当您输入一个新值时,selectedIndex属性不会改变,因为输入的值不在项目列表中。

有时,当组合框中的值发生变化时,您需要执行一个操作。您可以通过添加一个ActionEvent处理程序来做到这一点,当值以任何方式改变时,就会触发这个处理程序。您可以通过以编程方式设置它、从项列表中选择或输入新值来实现这一点,如下面的代码所示:

ComboBox<String> list = new ComboBox<>();
list.setOnAction(e -> System.out.println("Value changed"));

在可编辑的组合框中使用域对象

在可编辑的ComboBox<T>中,如果T不是String,你必须将converter属性设置为有效的StringConverter<T>。它的toString(T object)方法用于将 item 对象转换为字符串,以在弹出列表中显示。它的fromString(String s)方法被调用来将输入的字符串转换成 item 对象。用从输入的字符串转换的 item 对象更新value属性。如果输入的字符串不能转换为 item 对象,则value属性不会更新。

清单 12-13 中的程序展示了如何在一个组合框中使用一个StringConverter,该组合框在其条目列表中使用域对象。ComboBox使用了Person对象。如清单 12-11 所示的PersonStringConverter类被用作StringConverter。您可以在ComboBox中以姓氏、名字或名字的格式输入姓名,然后按 enter 键。输入的名称将被转换成一个Person对象并显示在Label中。程序忽略名称格式中的错误检查。例如,如果您输入 Kishori 作为名称,它会在Label中显示 null,Kishori。程序向选择模型的selectedItemselectedIndex属性添加一个ChangeListener来跟踪选择的变化。请注意,当您在ComboBox中输入一个字符串时,不会报告selectedIndex属性的变化。ComboBoxActionEvent处理程序用于保持组合框中的值和Label中的文本同步。

// ComboBoxWithConverter.java
// ... find in the book's download area.

Listing 12-13Using a StringConverter in a ComboBox

自定义弹出列表的高度

默认情况下,ComboBox在弹出列表中只显示十个项目。如果项目数超过十个,弹出列表会显示滚动条。如果项目数少于 10 个,弹出列表的高度会缩短,以便只显示可用的项目。ComboBoxvisibleRowCount属性控制弹出列表中可见的行数,如以下代码所示:

ComboBox<String> states = new ComboBox<>();
...
// Show five rows in the popup list
states.setVisibleRowCount(5);

使用节点作为组合框中的项目

组合框有两个区域:

  • 显示选定项目的按钮区域

  • 显示项目列表的弹出区域

两个区域都使用ListCells来显示项目。一个ListCell就是一个Cell。一个Cell是一个Labeled控件,用来显示某种形式的内容,可能有文本、图形或者两者都有。弹出区域是一个ListView,它包含列表中每个条目的一个ListCell实例。我将在下一节讨论ListView

组合框项目列表中的元素可以是任何类型,包括Node类型。不建议将Node类的实例直接添加到项目列表中。当节点用作项目时,它们会作为图形添加到单元格中。场景图形需要遵循一个节点不能同时在两个地方显示的规则。也就是说,一个节点一次只能在一个容器中。当从项目列表中选择一个节点时,该节点从弹出的ListView单元格中移除并添加到按钮区域。当弹出窗口再次显示时,所选节点不会显示在列表中,因为它已经显示在按钮区域中。为了避免显示中的这种不一致,请避免将节点直接用作组合框中的项。

图 12-21 显示了使用以下代码片段创建的组合框的三个视图。注意,代码添加了三个HBox实例,它是条目列表中的一个节点。标有#1 的图显示了第一次打开时的弹出列表,您可以正确地看到所有三个项目。在选择了第二个项目后,会出现标记为#2 的图,您会在按钮区域看到正确的项目。此时,列表中的第二个项目,一个矩形的HBox,被从ListView的单元格中移除,并添加到按钮区域的单元格中。标有#3 的图显示了第二次打开时的弹出列表。此时,列表中缺少第二个项目,因为它已经被选中。这个问题在前一段已经讨论过了。

img/336502_2_En_12_Fig21_HTML.jpg

图 12-21

项目列表中带有节点的组合框的三个视图

Label shapeLbl = new Label("Shape:");
ComboBox<HBox> shapes = new ComboBox<>();
shapes.getItems().addAll(new HBox(new Line(0, 10, 20, 10), new Label("Line")),
                new HBox(new Rectangle(0, 0, 20, 20), new Label("Rectangle")),
                new HBox(new Circle(20, 20, 10), new Label("Circle")));

您可以修复将节点用作项目时出现的显示问题。解决方案是在列表中添加非节点项,并提供一个单元工厂,以便在单元工厂中创建所需的节点。您需要确保非节点项将提供足够的信息来创建您想要插入的节点。下一节将解释如何使用细胞工厂。

组合框中使用单元格工厂

ComboBox类包含一个cellFactory属性,声明如下:

public ObjectProperty<Callback<ListView<T>, ListCell<T>>> cellFactory;

Callbackjavafx.util包中的一个接口。它有一个call()方法,接受类型为P的参数并返回类型为R的对象,如下面的代码所示:

public interface Callback<P,R> {
        public R call(P param);
}

属性cellFactory的声明声明它存储了一个Callback对象,该对象的call()方法接收一个ListView<T>并返回一个ListCell<T>。在call()方法中,创建一个ListCell<T>类的实例,并覆盖Cell类的updateItem(T item, boolean empty)方法来填充单元格。

让我们使用一个单元格工厂来显示组合框的按钮区域和弹出区域中的节点。清单 12-14 将是我们的起点。它声明了一个从ListCell<String>类继承而来的StringShapeCell类。您需要在其自动调用的updateItem()方法中更新其内容。该方法接收项目,在本例中是String,以及一个boolean参数,指示单元格是否为空。在方法内部,首先调用超类中的方法。您从字符串参数中派生出一个形状,并在单元格中设置文本和图形。该形状被设置为图形。getShape()方法从String返回一个Shape

// StringShapeCell.java
package com.jdojo.control;

import javafx.scene.control.ListCell;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Line;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.Shape;

public class StringShapeCell extends ListCell<String> {
        @Override
        public void updateItem(String item, boolean empty) {
                // Need to call the super first
                super.updateItem(item, empty);

                // Set the text and graphic for the cell
                if (empty) {
                        setText(null);
                        setGraphic(null);
                } else {
                        setText(item);
                        Shape shape = this.getShape(item);
                        setGraphic(shape);
                }
        }

        public Shape getShape(String shapeType) {
                Shape shape = null;
                switch (shapeType.toLowerCase()) {
                        case "line":
                                shape = new Line(0, 10, 20, 10);
                                break;
                        case "rectangle":
                                shape = new Rectangle(0, 0, 20, 20);
                                break;
                        case "circle":
                                shape = new Circle(20, 20, 10);
                                break;
                        default:
                                shape = null;
                }
                return shape;
        }
}

Listing 12-14A Custom ListCell That Displays a Shape and Its Name

下一步是创建一个Callback类,如清单 12-15 所示。这个清单中的程序非常简单。它的call()方法返回一个StringShapeCell类的对象。这个类将充当ComboBox的细胞工厂。

// ShapeCellFactory.java
package com.jdojo.control;

import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.util.Callback;

public class ShapeCellFactory implements Callback<ListView<String>, ListCell<String>> {
        @Override
        public ListCell<String> call(ListView<String> listview) {
                return new StringShapeCell();
        }
}

Listing 12-15A Callback Implementation for Callback<ListView<String>, ListCell<String>>

清单 12-16 中的程序展示了如何在组合框中使用自定义单元格工厂和按钮单元格。程序很简单。它创建了一个包含三个String项的组合框。它将ShapeCellFactory的一个对象设置为单元格工厂,如下面的代码所示:

// Set the cellFactory property
shapes.setCellFactory(new ShapeCellFactory());

在这种情况下,设置细胞工厂是不够的。它只会解决在弹出区域显示形状的问题。当您选择一个形状时,它会在按钮区域显示String项,而不是形状。为了确保您在选择列表中看到相同的项目,在您选择一个项目后,您需要设置buttonCell属性,如下面的代码所示:

// Set the buttonCell property
shapes.setButtonCell(new StringShapeCell());

注意在buttonCell属性和ShapeCellFactory类中使用了StringShapeCell类。

运行清单 12-16 中的程序。您应该能够从列表中选择一个形状,并且该形状应该正确显示在组合框中。图 12-22 显示了组合框的三视图。

img/336502_2_En_12_Fig22_HTML.jpg

图 12-22

带有细胞工厂的组合框的三视图

// ComboBoxCellFactory.java
package com.jdojo.control;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;

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

        @Override
        public void start(Stage stage) {
                Label shapeLbl = new Label("Shape:");
                ComboBox<String> shapes = new ComboBox<>();
                shapes.getItems().addAll("Line", "Rectangle", "Circle");

                // Set the cellFactory property
                shapes.setCellFactory(new ShapeCellFactory());

                // Set the buttonCell property
                shapes.setButtonCell(new StringShapeCell());

                HBox root = new HBox(shapeLbl, shapes);
                root.setStyle("-fx-padding: 10;" +
                              "-fx-border-style: solid inside;" +
                              "-fx-border-width: 2;" +
                              "-fx-border-insets: 5;" +
                              "-fx-border-radius: 5;" +
                              "-fx-border-color: blue;");

                Scene scene = new Scene(root);
                stage.setScene(scene);
                stage.setTitle("Using CellFactory in ComboBox");
                stage.show();
        }
}

Listing 12-16Using a Cell Factory in a Combo Box

在组合框中使用自定义单元格工厂和按钮单元格,可以让您非常方便地自定义弹出列表和所选项的外观。如果使用单元格工厂对你来说看起来很难或者很困惑,请记住单元格是一个Labeled控件,你是在updateItem()方法内设置那个Labeled控件中的文本和图形。因为ComboBox控件需要给你一个机会在它需要的时候创建一个单元格,所以Callback接口开始发挥作用。否则,您必须知道要创建多少个单元以及何时创建。没什么更多的了。

ComboBoxBase类提供了四个也可以与ComboBox一起使用的属性:

  • onShowing

  • onShown

  • onHiding

  • onHidden

这些属性属于类型ObjectProperty<EventHandler<Event>>。您可以为这些属性设置一个事件处理程序,在弹出列表显示之前、显示之后、隐藏之前和隐藏之后都会调用该事件处理程序。例如,当您想在弹出列表显示之前定制它时,onShowing事件处理程序非常方便。

用 CSS 设计组合框的样式

一个ComboBox的默认 CSS 样式类名是combo-box。一个组合框包含多个 CSS 子结构,如图 12-23 所示。

img/336502_2_En_12_Fig23_HTML.png

图 12-23

组合框的子结构,可以使用 CSS 单独设置样式

子结构的 CSS 名称是

  • arrow-button

  • list-cell

  • text-input

  • combo-box-popup

一个arrow-button包含一个名为arrow的子结构。arrow-buttonarrow都是StackPane的实例。list-cell区域代表用于在不可编辑的组合框中显示选中项目的ListCelltext-input区域是用于在可编辑的组合框中显示选中或输入的项目的TextFieldcombo-box-popup是点击按钮时显示弹出列表的Popup控件。它有两个子结构:list-viewlist-celllist-view是显示项目列表的ListView控件,list-cell代表ListView中的每个单元格。以下 CSS 样式定制了ComboBox的一些子结构的外观:

/* The ListCell that shows the selected item in a non-editable ComboBox */
.combo-box .list-cell {
        -fx-background-color: yellow;
}

/* The TextField that shows the selected item in an editable ComboBox */
.combo-box .text-input {
        -fx-background-color: yellow;
}

/* Style the arrow button area */
.combo-box .arrow-button {
        -fx-background-color: lightgray;
}

/* Set  the text color in the popup list for ComboBox to blue */
.combo-box-popup .list-view .list-cell {
        -fx-text-fill: blue;
}

了解 ListView 控件

ListView用于允许用户从项目列表中选择一个或多个项目。ListView中的每一项都由一个可以定制的ListCell类的实例来表示。ListView中的项目列表可以包含任何类型的对象。ListView是一个参数化类。参数类型是列表中项目的类型。如果您想在一个ListView中存储混合类型的项目,您可以使用它的

// Create a ListView for any type of items
ListView<Object> seasons = new ListView<>();

// Instead create a ListView for String items
ListView<String> seasons = new ListView<>();

您可以在创建ListView时指定列表项,如以下代码所示:

ObservableList<String> seasonList = FXCollections.<String>observableArrayList(
        "Spring", "Summer", "Fall", "Winter");
ListView<String> seasons = new ListView<>(seasonList);

在创建了一个ListView之后,您可以使用items属性将项目添加到它的项目列表中,该属性属于ObjectProperty<ObservableList<T>>类型,其中TListView的类型参数,如下面的代码所示:

ListView<String> seasons = new ListView<>();
seasons.getItems().addAll("Spring", "Summer", "Fall", "Winter");

设置它的首选宽度和高度,这通常不是你想要的宽度和高度。如果控件提供了一个像visibleItemCount这样的属性,这将有助于开发人员。不幸的是,ListView API 不支持这样的属性。您需要在代码中将它们设置为合理的值,如下所示:

// Set preferred width = 100px and height = 120px
seasons.setPrefSize(100, 120);

如果显示项目所需的空间大于可用空间,则会自动添加一个垂直滚动条、一个水*滚动条或两者都添加。

ListView类包含一个placeholder属性,它存储一个Node引用。当项目列表为空或null时,ListView的列表区显示占位符节点。下面的代码片段将一个Label设置为占位符:

Label placeHolder = new Label("No seasons available for selection.");
seasons.setPlaceholder(placeHolder);

ListView提供滚动功能。使用scrollTo(int index)scrollTo(T item)方法滚动到列表中指定的indexitem。如果指定的索引或项目尚不可见,则使其可见。当使用scrollTo()方法或用户进行滚动时,ListView类触发一个ScrollToEvent。您可以使用setOnScrollTo()方法设置一个事件处理程序来处理滚动。

使用ListCell类的实例显示ListView中的每个项目。本质上,ListCell是一个能够显示文本和图形的标签控件。ListCell的几个子类为ListView项目提供了自定义外观。ListView让您将Callback对象指定为单元格工厂,它可以创建自定义列表单元格。一个ListView不需要创建和项目数量一样多的ListCell对象。它只能有和屏幕上可见项目一样多的ListCell对象。当项目滚动时,它可以重用ListCell对象来显示不同的项目。图 12-24 显示了ListCell相关类的类图。

img/336502_2_En_12_Fig24_HTML.jpg

图 12-24

ListCell相关的类的类图

单元格在不同类型的控件中用作构造块。例如,ListViewTreeViewTableView控件以某种形式使用单元格来显示和编辑它们的数据。Cell类是所有单元格的超类。您可以覆盖它的updateItem(T object, boolean empty),完全控制单元格的填充方式。当单元格中的项需要更新时,这些控件会自动调用此方法。Cell类声明了几个有用的属性:editableeditingemptyitemselected。当Cell为空时,这意味着它不与任何数据项相关联,其empty属性为真。

IndexedCell类添加了一个index属性,它是底层模型中项的索引。假设一个ListView使用一个ObservableList作为模型。ObservableList中第二项的列表单元格的索引为 1(索引从 0 开始)。单元索引便于基于单元的索引来定制单元,例如,在奇数和偶数索引单元处对单元使用不同的颜色。当单元格为空时,其索引为–1。

列表视图的方向

ListView中的项目可以垂直排列成一列(默认)或水*排列成一行。它由orientation属性控制,如下面的代码所示:

// Arrange list of seasons horizontally
seasons.setOrientation(Orientation.HORIZONTAL);

图 12-25 显示了ListView的两个实例:一个使用垂直方向,一个使用水*方向。请注意,奇数和偶数行或列具有不同的背景颜色。这是ListView的默认外观。您可以使用 CSS 来更改外观。请参考“用 CSS 样式化 ListView”一节了解详细信息。

img/336502_2_En_12_Fig25_HTML.png

图 12-25

具有相同项目但不同方向的两个ListView实例

列表视图中选择模型

ListView有一个选择模型,存储其项目的选择状态。它的selectionModel属性存储选择模型的引用。默认情况下,它使用了一个MultipleSelectionModel类的实例。但是,您可以使用自定义选择模型,这是很少需要的。选择模型可以配置为在两种模式下工作:

  • 单一选择模式

  • 多重选择模式

在单一选择模式下,一次只能选择一个项目。如果选择了某个项目,则会取消选择之前选择的项目。默认情况下,ListView支持单选模式。可以使用鼠标或键盘选择项目。您可以使用鼠标单击来选择项目。使用键盘选择项目要求ListView有焦点。您可以使用垂直ListView中的上/下箭头和水*ListView中的左/右箭头来选择项目。

在多重选择模式下,一次可以选择多个项目。仅使用鼠标可让您一次仅选择一个项目。单击一个项目会选择该项目。按住 Shift 键单击一个项目会选择所有连续的项目。按住 Ctrl 键单击一个项目会选择一个取消选择的项目,并取消选择一个选定的项目。您可以使用上/下或左/右箭头键进行导航,并使用 Ctrl 键和空格键或 Shift 键和空格键来选择多个项目。如果您希望ListView在多重选择模式下运行,您需要设置其选择模型的selectionMode属性,如以下代码所示:

// Use multiple selection mode
seasons.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);

// Set it back to single selection mode, which is the default for a ListView
seasons.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);

MultipleSelectionModel类继承自SelectionModel类,后者包含selectedIndexselectedItem属性。

如果没有选择,selectedIndex属性为–1。在单选模式下,它是当前选定项的索引。在多重选择模式下,它是最后一个选定项目的索引。在多重选择模式下,使用getSelectedIndices()方法返回一个只读的ObservableList<Integer>,其中包含所有选中项目的索引。如果您对监听ListView中的选择变化感兴趣,您可以将ChangeListener添加到selectedIndex属性中,或者将ListChangeListener添加到由getSelectedIndices()方法返回的ObservableList中。

如果没有选择,selectedItem属性为null。在单选模式下,它是当前选定的项目。在多重选择模式下,它是最后选择的项目。在多重选择模式下,使用getSelectedItems()方法,该方法返回一个包含所有选中项目的只读ObservableList<T>。如果您有兴趣监听一个ListView中的选择变化,您可以向selectedItem属性添加一个ChangeListener,或者向由getSelectedItems()方法返回的ObservableList<T>添加一个ListChangeListener

ListView的选择模型包含了几种以不同方式选择项目的方法:

  • selectAll()方法选择所有项目。

  • selectFirst()selectLast()方法分别选择第一项和最后一项。

  • selectIndices(int index, int... indices)方法选择指定索引处的项目。有效范围之外的索引将被忽略。

  • selectRange(int start, int end)方法选择从start索引(含)到end索引(不含)的所有索引。

  • clearSelection()和 c learSelection(int index)方法分别清除所有选区和指定index处的选区。

清单 12-17 中的程序演示了如何使用ListView的选择模型进行选择并监听选择变化事件。图 12-26 显示了运行这段代码产生的窗口。运行应用程序,使用鼠标或窗口上的按钮选择ListView中的项目。选择的详细信息显示在底部。

img/336502_2_En_12_Fig26_HTML.jpg

图 12-26

一个带有几个按钮进行选择的ListView

// ListViewSelectionModel.java
// ... find in the book's download area.

Listing 12-17Using a ListView Selection Model

使用列表视图中的细胞工厂

ListView中的每一项都显示在ListCell的一个实例中,这是一个Labeled控件。回想一下,Labeled控件包含文本和图形。ListView类包含一个cellFactory属性,允许您为其项目使用自定义单元格。房产类型为ObjectProperty<Callback<ListView<T>,ListCell<T>>>。对ListView的引用被传递给Callback对象的call()方法,它返回一个ListCell类的实例。在一个大的ListView中,比如说 1000 个物品,从细胞工厂返回的ListCell可能会被重复使用。控件只需要创建一定数量的可见单元格。在滚动时,它可以重用视图之外的单元格来显示新可见的项目。ListCellupdateItem()方法接收新项目的引用。

默认情况下,ListView调用其项目的toString()方法,并在其单元格中显示字符串。在您的自定义ListCellupdateItem()方法中,您可以填充单元格的文本和图形,以根据单元格中的项目显示单元格中的任何内容。

Tip

在上一节中,您为组合框的弹出列表使用了自定义单元格工厂。组合框中的弹出列表使用了一个ListView。因此,在ListView中使用自定义单元格工厂与前面组合框部分中讨论的一样。

清单 12-18 中的程序展示了如何使用自定义单元格工厂来显示Person项的格式化名称。图 12-27 显示了运行代码后的结果窗口。程序中的代码片段创建并设置一个定制的单元工厂。ListCellupdateItem()方法格式化Person对象的名称,并添加一个序列号,该序列号是单元格的索引加 1。

img/336502_2_En_12_Fig27_HTML.jpg

图 12-27

一个ListView使用定制的单元格工厂在其项目列表中显示Person对象

// ListViewDomainObjects.java
// ... find in the book's download area.

Listing 12-18Using a Custom Cell Factory for ListView

使用可编辑的列表视图

ListView控件提供了许多定制,其中之一是它允许用户编辑项目的能力。在编辑ListView之前,您需要为其设置两个属性:

  • ListVieweditable属性设置为 true。

  • ListViewcellFactory属性设置为产生可编辑ListCell的单元格工厂。

选择一个单元格,然后单击开始编辑。或者,当单元格获得焦点时,按空格键开始编辑。如果一个ListView是可编辑的并且有一个可编辑的单元格,你也可以使用ListViewedit(int index)方法在指定的index编辑单元格中的项目。

Tip

ListView类包含一个只读的editingIndex属性。它的值是正在编辑的项目的索引。如果没有编辑任何项目,则其值为–1。

JavaFX 提供了细胞工厂,允许您使用TextFieldChoiceBoxComboBoxCheckBox编辑ListCell。您可以创建自定义单元格工厂,以其他方式编辑单元格。作为ListView中的列表单元格,TextFieldListCellChoiceBoxListCellComboBoxListCellCheckBoxListCell类的实例提供编辑支持。这些类包含在javafx.scene.control.cell包中。

使用文本字段编辑列表视图

TextFieldListCell的一个实例是一个ListCell,当项目没有被编辑时,它在Label中显示一个项目,当项目被编辑时,它在TextField中显示一个项目。如果你想编辑一个域对象到一个ListView,你将需要使用一个StringConverter来促进双向转换。TextFieldListCell类的forListView()静态方法返回一个配置为与String项一起使用的单元工厂。以下代码片段显示了如何将TextField设置为ListView的单元格编辑器:

ListView<String> breakfasts = new ListView<>();
...
breakfasts.setEditable(true);

// Set a TextField as the editor
Callback<ListView<String>, ListCell<String>> cellFactory =
         TextFieldListCell.forListView();
breakfasts.setCellFactory(cellFactory);

下面的代码片段展示了如何使用包含Person对象的ListView的转换器将TextField设置为单元格编辑器。代码中使用的转换器如清单 12-11 所示。converter 对象将用于将Person对象转换为String进行显示,并将String转换为Person对象进行编辑。

ListView<Person> persons = new ListView<>();
...
persons.setEditable(true);

// Set a TextField as the editor.
// Need to use a StringConverter for Person objects.
StringConverter<Person> converter = new PersonStringConverter();
Callback<ListView<Person>, ListCell<Person>> cellFactory
        = TextFieldListCell.forListView(converter);
persons.setCellFactory(cellFactory);

清单 12-19 中的程序展示了如何编辑TextField中的ListView项。它使用了一个域对象(Person)的ListView和一个String对象的ListView。运行程序后,双击两个ListView中的任意项目开始编辑。完成编辑后,按 Enter 键提交更改。

// ListViewEditing.java
// ... find in the book's download area.

Listing 12-19Using an Editable ListView

使用选择框 / 组合框编辑列表视图

ChoiceBoxListCell的一个实例是一个ListCell,当项目没有被编辑时,它在Label中显示一个项目,当项目被编辑时,它在ChoiceBox中显示一个项目。如果你想编辑一个域对象到一个ListView,你将需要使用一个StringConverter来促进双向转换。您需要提供要在选择框中显示的项目列表。使用ChoiceBoxListCell类的forListView()静态方法创建一个细胞工厂。以下代码片段显示了如何将选择框设置为ListView的单元格编辑器:

ListView<String> breakfasts = new ListView<>();
...
breakfasts.setEditable(true);

// Set a cell factory to use a ChoiceBox for editing
ObservableList<String> items =
        FXCollections.<String>observableArrayList(
        "Apple", "Banana", "Donut", "Hash Brown");
breakfasts.setCellFactory(ChoiceBoxListCell.forListView(items));

清单 12-20 中的程序使用一个选择框来编辑ListView中的项目。双击单元格中的项目开始编辑。在编辑模式下,单元格会变成一个选择框。单击箭头显示要选择的项目列表。使用组合框进行编辑类似于使用选择框。

// ListViewChoiceBoxEditing.java
// ... find in the book's download area.

Listing 12-20Using a ChoiceBox for Editing Items in a ListView

使用复选框编辑 ListView

CheckBoxListCell类提供了使用复选框编辑ListCell的能力。它在单元格中绘制一个复选框,可以选择或取消选择。注意,当使用复选框编辑ListView项目时,复选框的第三种状态不确定状态不可选择。

使用复选框编辑ListView项略有不同。您需要为ListView中的每一项向CheckBoxListCell类提供一个ObservableValue<Boolean>对象。在内部,可观察值被双向绑定到复选框的选定状态。当用户使用复选框选择或取消选择ListView中的一个项目时,相应的ObservableValue对象被更新为 true 或 false 值。如果您想知道哪个项目被选中,您将需要保存对ObservableValue对象的引用。

让我们使用复选框重做我们之前的早餐示例。下面的代码片段创建了一个映射,并将所有条目作为一个键和一个对应的值为 false 的ObservableValue条目添加进来。使用 false 值,您希望指示最初将取消选择这些项目:

Map<String, ObservableValue<Boolean>> map = new HashMap<>();
map.put("Apple", new SimpleBooleanProperty(false));
map.put("Banana", new SimpleBooleanProperty(false));
map.put("Donut", new SimpleBooleanProperty(false));
map.put("Hash Brown", new SimpleBooleanProperty(false));

现在,您创建一个可编辑的ListView,将地图中的所有关键点作为其项目:

ListView<String> breakfasts = new ListView<>();
breakfasts.setEditable(true);

// Add all keys from the map as items to the ListView
breakfasts.getItems().addAll(map.keySet());

下面的代码片段创建了一个Callback对象。它的call()方法返回传递给call()方法的指定itemObservableValue对象。CheckBoxListCell类会自动调用这个对象的call()方法:

Callback<String, ObservableValue<Boolean>> itemToBoolean =
    (String item) -> map.get(item);

现在是时候为ListView创建和设置一个细胞工厂了。CheckBoxListCell类的forListView()静态方法接受一个Callback对象作为参数。如果您的ListView包含域对象,您也可以使用下面的代码为这个方法提供一个StringConverter:

// Set the cell factory
breakfasts.setCellFactory(CheckBoxListCell.forListView(itemToBoolean));

当用户使用复选框选择或取消选择项目时,地图中相应的ObservableValue将被更新。要知道ListView中的项目是否被选中,您需要查看该项目的ObservableValue对象中的值。

清单 12-21 中的程序展示了如何使用复选框来编辑ListView中的项目。图 12-28 显示了运行代码后的结果窗口。使用鼠标选择项目。按下打印选择按钮在标准输出上打印所选项目。

img/336502_2_En_12_Fig28_HTML.jpg

图 12-28

带有用于编辑其项目的复选框的ListView

// ListViewCheckBoxEditing.java
// ... find in the book's download area.

Listing 12-21Using a Check Box to Edit ListView Items

编辑列表视图时处理事件

可编辑的ListView触发三种事件:

  • 编辑开始时的editStart事件

  • 提交编辑值时的editCommit事件

  • 取消编辑时的editcancel事件

ListView类定义了一个ListView.EditEvent<T>静态内部类来表示与编辑相关的事件对象。它的getIndex()方法返回被编辑项目的索引。getNewValue()方法返回新的输入值。getSource()方法返回触发事件的ListView的引用。ListView类提供了onEditStartonEditCommitonEditCancel属性来设置这些方法的事件处理程序。

以下代码片段将一个editStart事件处理程序添加到一个ListView中。处理程序打印正在编辑的索引和新的项目值:

ListView<String> breakfasts = new ListView<>();
...
breakfasts.setEditable(true);
breakfasts.setCellFactory(TextFieldListCell.forListView());

// Add an editStart event handler to the ListView
breakfasts.setOnEditStart(e ->
        System.out.println("Edit Start: Index=" + e.getIndex() +
                           ", item  = " + e.getNewValue()));

清单 12-22 包含了一个完整的程序来展示如何在ListView中处理与编辑相关的事件。运行程序,双击一个项目开始编辑。更改值后,按 Enter 键提交编辑,或按 Esc 键取消编辑。与编辑相关的事件处理程序在标准输出中打印消息。

// ListViewEditEvents.java
// ... find in the book's download area.

Listing 12-22Handling Edit-Related Events in a ListView

用 CSS 设计列表视图

一个ListView的默认 CSS 样式类名是list-view,对于ListCelllist-cellListView类有两个 CSS 伪类:horizontalvertical-fx-orientation CSS 属性控制ListView的方向,可以设置为水*垂直

您可以像设计任何其他控件一样设计ListView的样式。每个项目都显示在一个ListCell实例中。ListCell提供了几个 CSS 伪类:

  • empty

  • filled

  • selected

  • odd

  • even

当单元格为空时,empty伪类适用。当单元格不为空时,filled伪类适用。当单元格被选中时,selected伪类适用。oddeven伪类分别应用于奇数和偶数索引的单元格。代表第一项的单元格的索引为 0,它被视为偶数单元格。

以下 CSS 样式将突出显示褐色的偶数单元格和浅灰色的奇数单元格:

.list-view .list-cell:even {
    -fx-background-color: tan;
}

.list-view .list-cell:odd {
    -fx-background-color: lightgray;
}

开发人员经常会问如何移除ListView中默认的替代单元格高亮显示。在modena.css文件中,所有列表单元格的默认背景颜色被设置为-fx-control-inner-background,这是一种 CSS 派生的颜色。对于所有奇数列表单元格,默认颜色设置为derive(-fx-control-inner-background,-5%)。要保持所有单元格的背景颜色相同,您需要覆盖奇数列表单元格的背景颜色,如下所示:

.list-view .list-cell:odd {
    -fx-background-color: -fx-control-inner-background;
}

这仅仅解决了问题的一半;它只负责一个ListView中正常状态下列表单元格的背景颜色。列表单元格可以有几种状态,例如,focusedselectedemptyfilled。为了彻底解决这个问题,您需要为所有州的列表单元格设置适当的背景颜色。请参考modena.css文件,获取您需要修改列表单元格背景颜色的完整状态列表。

ListCell类支持一个-fx-cell-size CSS 属性,即垂直ListView中单元格的高度和水*ListView中单元格的宽度。

列表单元格的类型可以是ListCellTextFieldListCellChoiceBoxListCellComboBoxListCellCheckBoxListCellListCell子类的默认 CSS 样式类名是text-field-list-cellchoice-box-list-cellcombo-box-list-cellcheck-box-list-cell。您可以使用这些样式类名来自定义它们的外观。以下 CSS 样式将在黄色背景的可编辑ListView中显示TextField:

.list-view .text-field-list-cell .text-field {
        -fx-background-color: yellow;
}

了解颜色选择器控件

是一个组合框样式的控件,专门为用户从标准调色板中选择颜色或使用内置颜色对话框创建颜色而设计。ColorPicker类继承自ComboBoxBase<Color>类。因此,ComboBoxBase类中声明的所有属性也适用于ColorPicker控件。我在前面的“理解组合框控件”一节中已经讨论了其中的几个属性。如果您想了解这些属性的更多信息,请参阅该部分。例如,editableonActionshowingvalue属性在ColorPicker中的工作方式与它们在组合框中的工作方式相同。一个ColorPicker有三部分:

  • ColorPicker控制

  • 调色板颜色

  • 自定义颜色对话框

一个ColorPicker控件由几个部件组成,如图 12-29 所示。您可以自定义它们的外观。颜色指示器是一个显示当前颜色选择的矩形。颜色标签以文本格式显示颜色。如果当前选择是标准颜色之一,标签将显示颜色名称。否则,它以十六进制格式显示颜色值。图 12-30 显示了一个ColorPicker控件及其调色板。

img/336502_2_En_12_Fig30_HTML.png

图 12-30

ColorPicker控件及其调色板对话框

img/336502_2_En_12_Fig29_HTML.png

图 12-29

ColorPicker控件的组件

当您单击控件中的箭头按钮时,调色板显示为弹出窗口。调色板由三个区域组成:

  • 显示一组标准颜色的调色板区域

  • 显示自定义颜色列表的自定义颜色区域

  • 打开“自定义颜色”对话框的超链接

调色板区域显示一组预定义的标准颜色。如果您单击其中一种颜色,它会关闭弹出窗口,并将所选颜色设置为ColorPicker控件的值。

自定义颜色区域显示一组自定义颜色。当您第一次打开此弹出窗口时,此区域不存在。有两种方法可以得到这个区域的颜色。您可以加载一组自定义颜色,也可以使用“自定义颜色”对话框构建和保存自定义颜色。

当点击自定义颜色…超链接时,会显示一个自定义颜色对话框,如图 12-31 所示。您可以使用“HSB”、“RGB”或“Web”选项卡,使用其中一种格式来构建自定义颜色。也可以通过从对话框左侧的颜色区域或颜色垂直栏中选择颜色来定义新颜色。当您单击颜色区域和颜色栏时,它们会显示一个小圆圈和矩形来表示新颜色。单击“保存”按钮选择控件中的自定义颜色并保存它,以便以后再次打开弹出窗口时显示在自定义颜色区域中。单击“使用”按钮为控件选择自定义颜色。

img/336502_2_En_12_Fig31_HTML.jpg

图 12-31

ColorPicker的自定义颜色对话框

使用颜色选择器控件

ColorPicker类有两个构造器。其中一个是默认构造器,另一个以初始颜色作为参数。默认构造器使用白色作为初始颜色,如下面的代码所示:

// Create a ColorPicker control with an initial color of white
ColorPicker bgColor1 = new ColorPicker();

// Create a ColorPicker control with an initial color of red
ColorPicker bgColor2 = new ColorPicker(Color.RED);

控件的value属性存储当前选择的颜色。通常,value属性是在使用控件选择颜色时设置的。但是,您也可以直接在代码中设置它,如下所示:

ColorPicker bgColor = new ColorPicker();
...
// Get the selected color
Color selectedCOlor = bgColor.getValue();

// Set the ColorPicker color to yellow
bgColor.setValue(Color.YELLOW);

ColorPicker类的getCustomColors()方法返回您保存在自定义颜色对话框中的自定义颜色列表。请注意,自定义颜色只为当前会话和当前ColorPicker控件保存。如果需要,您可以将自定义颜色保存在文件或数据库中,并在启动时加载它们。您必须编写一些代码来实现这一点:

ColorPicker bgColor = new ColorPicker();
...
// Load two custom colors
bgColor.getCustomColors().addAll(Color.web("#07FF78"), Color.web("#C2F3A7"));
...
// Get all custom colors
ObservableList<Color> customColors = bgColor.getCustomColors();

通常,当在ColorPicker中选择一种颜色时,您希望将该颜色用于其他控件。当选择一种颜色时,ColorPicker控件产生一个ActionEvent。下面的代码片段向一个ColorPicker添加了一个ActionEvent处理程序。选择一种颜色后,处理程序会将新颜色设置为矩形的填充颜色:

ColorPicker bgColor = new ColorPicker();
Rectangle rect = new Rectangle(0, 0, 100, 50);

// Set the selected color in the ColorPicker as the fill color of the Rectangle
bgColor.setOnAction(e -> rect.setFill(bgColor.getValue()));

清单 12-23 中的程序展示了如何使用ColorPicker控件。当您使用ColorPicker选择颜色时,矩形的填充颜色会更新。

// ColorPickerTest.java
// ... find in the book's download area.

Listing 12-23Using the ColorPicker Control

ColorPicker控件支持三种外观:组合框外观、按钮外观和拆分按钮外观。组合框外观是默认外观。图 12-32 分别显示了这三种外观中的一个ColorPicker

img/336502_2_En_12_Fig32_HTML.jpg

图 12-32

三看 a ColorPicker

ColorPicker类包含两个字符串内容,它们是按钮和拆分按钮外观的 CSS 样式类名。这些常量是

  • STYLE_CLASS_BUTTON

  • STYLE_CLASS_SPLIT_BUTTON

如果您想改变ColorPicker的默认外观,添加一个前面的常量作为它的样式类,如下所示:

// Use default combo-box look
ColorPicker cp = new ColorPicker(Color.RED);

// Change the look to button
cp.getStyleClass().add(ColorPicker.STYLE_CLASS_BUTTON);

// Change the look to split-button
cp.getStyleClass().add(ColorPicker.STYLE_CLASS_SPLIT_BUTTON);

Tip

可以添加STYLE_CLASS_BUTTONSTYLE_CLASS_SPLIT_BUTTON作为ColorPicker的样式类。在这种情况下,使用STYLE_CLASS_BUTTON

使用 CSS 对颜色选择器进行样式化

一个ColorPicker的默认 CSS 样式类名是color-picker。您几乎可以设计ColorPicker的每个部分,例如,颜色指示器、颜色标签、调色板对话框和自定义颜色对话框。完整参考请参考modena.css文件。

ColorPicker-fx-color-label-visible CSS 属性设置颜色标签是否可见。其默认值为 true。以下代码使颜色标签不可见:

.color-picker {
        -fx-color-label-visible: false;
}

颜色指示器是一个矩形,它有一个样式类名picker-color-rect。颜色标签是一个Label,它有一个样式类名color-picker-label。以下代码显示蓝色的颜色标签,并在颜色指示器矩形周围设置 2px 粗的黑色线条:

.color-picker .color-picker-label {
        -fx-text-fill: blue;
}

.color-picker .picker-color .picker-color-rect {
        -fx-stroke: black;
        -fx-stroke-width: 2;
}

调色板的样式类名称是color-palette。以下代码隐藏了调色板上的自定义颜色…超链接:

.color-palette .hyperlink {
        visibility: hidden;
}

了解日期选择器控件

DatePicker是一个组合框样式的控件。用户可以输入文本形式的日期,也可以从日历中选择日期。日历显示为控件的弹出窗口,如图 12-33 所示。DatePicker类继承自ComboBoxBase<LocalDate>类。在ComboBoxBase类中声明的所有属性也可用于DatePicker控件。

img/336502_2_En_12_Fig33_HTML.jpg

图 12-33

一个DatePicker控件的日历弹出窗口

弹出窗口的第一行显示月份和年份。您可以使用箭头滚动月份和年份。第二行显示周的简称。第一列显示一年中的周数。默认情况下,不显示周数列。您可以使用弹出菜单上的上下文菜单来显示它,或者您可以设置控件的showWeekNumbers属性来显示它。

日历总是显示 42 天的日期。不能选择不适用于当月的日期。每一天单元格都是DateCell类的一个实例。您可以提供一个单元格工厂来使用您的自定义单元格。稍后您将看到一个使用定制单元工厂的示例。

右键单击第一行、“周名称”、“周编号”列或“禁用日期”会显示上下文菜单。上下文菜单还包含“显示今天”菜单项,该菜单项将日历滚动到当前日期。

使用日期选择器控件

您可以使用默认构造器创建一个DatePicker;它使用null作为初始值。您也可以将一个LocalDate作为初始值传递给另一个构造器,如下面的代码所示:

// Create a DatePicker with null as its initial value
DatePicker birthDate1 = new DatePicker();

// Use September 19, 1969 as its initial value
DatePicker birthDate2 = new DatePicker(LocalDate.of(1969, 9, 19));

控件的value属性保存控件中的当前日期。您可以使用属性来设置日期。当控件有一个null值时,弹出窗口显示当月的日期。否则,弹出窗口显示当前值的月份日期,如以下代码所示:

// Get the current value
LocalDate dt = birthDate.getValue();

// Set the current value
birthDate.setValue(LocalDate.of(1969, 9, 19));

DatePicker控件提供了一个TextField以文本形式输入日期。它的editor属性存储了TextField的引用。该属性是只读的。如果不希望用户输入日期,可以将DatePickereditable属性设置为 false,如以下代码所示:

DatePicker birthDate = new DatePicker();

// Users cannot enter a date. They must select one from the popup.
birthDate.setEditable(false);

DatePicker有一个converter属性,它使用一个StringConverter将一个LocalDate转换成一个字符串,反之亦然。它的value属性将日期存储为LocalDate,它的编辑器将其显示为一个字符串,也就是格式化的日期。当您以文本形式输入日期时,转换器会将其转换为LocalDate并存储在value属性中。当您从日历弹出菜单中选择一个日期时,转换器会创建一个LocalDate来存储在value属性中,并将其转换为一个字符串来显示在编辑器中。默认转换器使用默认的Locale和年表来格式化日期。当您以文本形式输入日期时,默认转换器期望文本采用默认的Locale和年表格式。

清单 12-24 包含了一个LocalDateStringConverter类的代码,它是LocalDateStringConverter。默认情况下,它将日期格式化为MM/dd/yyyy格式。您可以在其构造器中传递不同的格式。

// LocalDateStringConverter.java
package com.jdojo.control;

import javafx.util.StringConverter;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

public class LocalDateStringConverter extends StringConverter<LocalDate> {
        private String pattern = "MM/dd/yyyy";
        private DateTimeFormatter dtFormatter;

        public LocalDateStringConverter() {
                dtFormatter = DateTimeFormatter.ofPattern(pattern);
        }

        public LocalDateStringConverter(String pattern) {
                this.pattern = pattern;
                dtFormatter = DateTimeFormatter.ofPattern(pattern);
        }

        @Override
        public LocalDate fromString(String text) {
                LocalDate date = null;
                if (text != null && !text.trim().isEmpty()) {
                        date = LocalDate.parse(text, dtFormatter);
                }
                return date;
        }

        @Override
        public String toString(LocalDate date) {
                String text = null;
                if (date != null) {
                        text = dtFormatter.format(date);
                }
                return text;
        }
}

Listing 12-24A StringConverter to Convert a LocalDate to a String and Vice Versa

要将日期格式化为"MMMM dd, yyyy"格式,例如,2013 年 5 月 29 日,您需要创建并设置 convert,如下所示:

DatePicker birthDate = new DatePicker();
birthDate.setConverter(new LocalDateStringConverter("MMMM dd, yyyy"));

您可以将DatePicker控件配置为使用特定的时序,而不是默认时序。以下陈述将年表设定为泰国佛教年表:

birthDate.setChronology(ThaiBuddhistChronology.INSTANCE);

您可以为 JVM 的当前实例更改默认的LocaleDatePicker将使用默认的Locale的日期格式和年表:

// Change the default Locale to Canada
Locale.setDefault(Locale.CANADA);

弹出日历中的每个日期单元格都是从Cell<LocalDate>类继承而来的DateCell类的一个实例。DatePicker类的dayCellFactory属性允许您提供一个定制的日细胞工厂。这个概念与前面讨论的为ListView控件提供细胞工厂的概念相同。以下语句创建一个 day cell 工厂。它将周末单元格的文本颜色更改为蓝色,并禁用所有未来日期单元格。如果您将该日单元格工厂设置为DatePicker,弹出日历将不允许用户选择未来日期,因为您将禁用所有未来日单元格:

Callback<DatePicker, DateCell> dayCellFactory =
    new Callback<DatePicker, DateCell>() {
        public DateCell call(final DatePicker datePicker) {
            return new DateCell() {
                @Override
                public void updateItem(LocalDate item, boolean empty) {
                    // Must call super
                    super.updateItem(item, empty);
                    // Disable all future date cells
                    if (item.isAfter(LocalDate.now())) {
                        this.setDisable(true);
                    }
                    // Show Weekends in blue
                    DayOfWeek day = DayOfWeek.from(item);
                    if (day == DayOfWeek.SATURDAY ||
                            day == DayOfWeek.SUNDAY) {
                        his.setTextFill(Color.BLUE);
                    }
                }
            };
        }
   };

下面的代码片段为出生日期DatePicker控件设置了一个自定义的日单元格工厂。它还使控件不可编辑。该控件将强制用户从弹出日历中选择一个非未来日期:

DatePicker birthDate = new DatePicker();

// Set a day cell factory to disable all future day cells
// and show weekends in blue
birthDate.setDayCellFactory(dayCellFactory);

// Users must select a date from the popup calendar
birthDate.setEditable(false);

DatePicker控件的 value 属性改变时,它触发一个ActionEvent。当用户输入日期、从弹出窗口中选择日期或者以编程方式设置日期时,value属性可能会改变,如以下代码所示:

// Add an ActionEvent handler
birthDate.setOnAction(e -> System.out.println("Date changed to:" + birthDate.getValue()));

清单 12-25 有一个完整的程序显示如何使用DatePicker控件。它使用了DatePicker的大部分功能。显示如图 12-34 所示的窗口。该控件是不可编辑的,迫使用户从弹出窗口中选择一个非未来日期。

img/336502_2_En_12_Fig34_HTML.jpg

图 12-34

用于选择非未来日期的DatePicker控件

// DatePickerTest.java
// ... find in the book's download area.

Listing 12-25Using the DatePicker Control

用 CSS 设计日期选择器的样式

一个DatePicker的默认 CSS 样式类名是date-picker,对于它的弹出窗口,类名是date-picker-popup。您几乎可以为DatePicker的每个部分设置样式,例如,弹出窗口顶部区域的月-年窗格、日单元格、周数单元格和当前日单元格。完整参考请参考modena.css文件。

日单元格的 CSS 样式类名是day-cell。当前日期的日单元格的样式类名为today。以下样式以粗体显示当前日期,以蓝色显示所有日期:

/* Display current day numbers in bolder font */
.date-picker-popup > * > .today {
        -fx-font-weight: bolder;
}

/* Display all day numbers in blue */
.date-picker-popup > * > .day-cell {
    -fx-text-fill: blue;
}

了解文本输入控件

JavaFX 支持文本输入控件,允许用户使用单行或多行纯文本。我将在本节讨论TextFieldPasswordFieldTextArea文本输入控件。所有文本输入控件都继承自TextInputControl类。文本输入控件的类图请参见图 12-1 。

Tip

JavaFX 提供了名为HTMLEditor的富文本编辑控件。我将在本章后面讨论HTMLEditor

TextInputControl类包含适用于所有类型文本输入控件的属性和方法。与当前插入符号位置、移动和文本选择相关的属性和方法都在这个类中。子类添加适用于它们的属性和方法。表 12-5 列出了在TextInputControl类中声明的属性。

表 12-5

TextInputControl类中声明的属性

|

财产

|

类型

|

描述

anchor ReadOnlyIntegerProperty 它是文本选择的锚点。它位于所选内容中插入符号位置的另一端。
caretPosition ReadOnlyIntegerProperty 它是插入符号在文本中的当前位置。
editable BooleanProperty 如果控件是可编辑的,则为真。否则就是假的。
font ObjectProperty<Font> 这是控件的默认字体。
length ReadOnlyIntegerProperty 它是控件中的字符数。
promptText StringProperty 它是提示文本。当控件没有内容时,它显示在控件中。
redoable ReadOnlyBooleanProperty 告知是否可以重做最*的更改。
selectedText ReadOnlyStringProperty 它是控件中的选定文本。
selection ReadOnlyObjectProperty <IndexRange> 这是选定的文本索引范围。
text StringProperty 它是控件中的文本。
textFormatter ObjectProperty<TextFormatter<?>> 当前附加的文本格式化程序。
undoable ReadOnlyBooleanProperty 告知是否可以撤消最*的更改。

定位和移动插入符号

所有文本输入控件都提供一个插入符号。默认情况下,当控件有焦点时,插入符号是一条闪烁的垂直线。当前插入符号位置是键盘下一个输入字符的目标。插入符号位置从零开始,在第一个字符之前。位置 1 在第一个字符之后,第二个字符之前,依此类推。图 12-35 显示了有四个字符的文本输入控件中的插入符号位置。文本中的字符数决定了插入符号位置的有效范围,从零到文本长度。如果控件不包含文本,零是唯一有效的插入符号位置。

img/336502_2_En_12_Fig35_HTML.jpg

图 12-35

具有四个字符的文本输入控件中的插入符号位置

有几种方法将插入符号位置作为参数。这些方法将参数值固定在有效的插入符号位置范围内。传递有效范围之外的插入符号位置不会引发异常。例如,如果控件有四个字符,并且您希望将插入符号移动到位置 10,则插入符号将定位在位置 4。

只读的caretPosition属性包含当前插入符号的位置。使用positionCaret(int pos)方法将插入符号定位在指定的pos处。如果没有选择,那么backward()forward()方法分别将插入符号向前和向后移动一个字符。如果有选择,它们将插入符号位置移动到开头和结尾,并清除选择。home()end()方法分别将插入符号移动到第一个字符之前和最后一个字符之后,并清除选择。方法将插入符号移动到下一个单词的开头并清除选择。endOfNextWord()方法将插入符号移动到下一个单词的末尾并清除选择。方法将插入符号移动到前一个单词的开头并清除选择。

选择文本

TextInputControl类通过其属性和方法提供了丰富的 API 来处理文本选择。使用选择 API,您可以选择整个或部分文本,并获得选择信息。

selectedText属性包含所选文本的值。如果没有选择,其值为空字符串。属性包含一个保存选择的索引范围的 ??。IndexRange类的getStart()getEnd()方法分别返回选择的开始索引和结束索引,其getLength()方法返回选择的长度。如果没有选择,范围的下限和上限是相同的,它们等于caretPosition值。

anchorcaretPosition属性在文本选择中起着至关重要的作用。这些属性的值定义了选择范围。两个属性的值相同表示没有选择。任一属性可以指示选择范围的开始或结束。anchor值是选择开始时的插入符号位置。您可以通过前后移动插入符号来选择字符。例如,您可以在按住 Shift 键的同时使用向左或向右箭头键来选择一系列字符。如果在选择过程中向前移动插入符号,anchor值将小于caretPosition值。如果在选择过程中向后移动插入符号,anchor值将大于caretPosition值。图 12-36 显示了anchorcaretPosition值之间的关系。

img/336502_2_En_12_Fig36_HTML.png

图 12-36

文本输入控件的anchorcaretPosition属性之间的关系

在图 12-36 中,标记为#1 的部分显示了一个带有文本祝福的文本输入控件。caretPosition值为 1。用户通过将插入符号向前移动四个位置来选择四个字符,例如,通过按下 Shift 键和右箭头键或者通过拖动鼠标。如标记为#2 的部分所示,selectedText属性更少。anchor值为 1,caretPosition值为 5。选择属性的IndexRange为 1 到 5。

在标记为#3 的部分中,caretPosition值为 5。用户通过向后移动插入符号来选择四个字符,如标记为#4 的部分所示。如标记为#4 的部分所示,selectedText属性更少。anchor值为 5,caretPosition值为 1。选择属性的IndexRange为 1 到 5。请注意,在标记为#2 和#4 的部分中,anchorcaretPosition值不同,而selectedText和选择属性相同。

除了selection属性之外,TextInputControl还包含几个有用的与选择相关的方法:

  • selectAll()

  • deselect()

  • selectRange(int anchor, int caretPosition)

  • selectHome()

  • selectEnd()

  • extendSelection(int pos)

  • selectBackward()

  • selectForward()

  • selectPreviousWord()

  • selectEndOfNextWord()

  • selectNextWord()

  • selectPositionCaret(int pos)

  • replaceSelection(String replacement)

注意,您有一个positionCaret(int pos)方法和一个selectPositionCaret(int pos)方法。前者将插入符号定位在指定位置,并清除选定内容。后者将插入符号移动到指定的pos,如果存在,则扩展选择。如果不存在选择,它通过将当前插入符号位置作为锚点并将插入符号移动到指定的pos来形成选择。

方法用指定的替换来替换选中的文本。如果没有选定内容,则清除选定内容,并在当前插入符号位置插入指定的替换内容。

修改内容

TextInputControl类的text属性表示文本输入控件的文本内容。您可以使用setText(String text)方法更改内容,并使用getText()方法获取内容。clear()方法将内容设置为一个空字符串。

方法在指定的索引处插入指定的文本。如果指定的索引在有效范围之外(零到内容的长度),它抛出一个IndexOutOfBoundsExceptionappendText(String text)方法将指定的文本附加到内容中。deleteText()方法允许您从内容中删除一系列字符。您可以将范围指定为一个IndexRange对象或开始和结束索引。如果没有选择,deleteNextChar()deletePreviousChar()方法分别从当前插入符号位置删除下一个和上一个字符。如果有选择,他们会删除选择。如果删除成功,它们返回true。否则,它们返回false

只读的length属性表示内容的长度。当您修改内容时,它会发生变化。实际上,length值可以很大。没有直接的方法来限制文本输入控件中的字符数。我将很快介绍一个限制文本长度的例子。

剪切、复制和粘贴文本

文本输入控件使用鼠标和键盘以编程方式支持剪切、复制和粘贴功能。要使用鼠标和键盘使用这些功能,请使用您的*台支持的标准步骤。使用cut()copy()paste()方法以编程方式使用这些特性。cut()方法将当前选中的文本转移到剪贴板,并删除当前选中的文本。方法的作用是:将当前选中的文本转移到剪贴板,而不移除当前选中的文本。paste()方法用剪贴板中的内容替换当前选择。如果没有选定内容,则在当前插入符号位置插入剪贴板内容。

一个例子

清单 12-26 中的程序演示了文本输入控件的不同属性是如何变化的。显示如图 12-37 所示的窗口。该程序使用一个文本输入控件TextField来显示一行文本。通过将text属性绑定到TextField的属性,每个属性都显示在一个Label中。运行程序后,更改 name 字段中的文本,移动插入符号,并更改选择以查看TextField的属性是如何变化的。

img/336502_2_En_12_Fig37_HTML.jpg

图 12-37

使用文本输入控件的属性

// TextControlProperties.java
// ... find in the book's download area.

Listing 12-26Using the Properties of Text Input Controls

使用 CSS 样式化 TextInputControl

TextInputControl类引入了一个名为readonly的 CSS 伪类,它适用于控件不可编辑的情况。它添加了以下样式属性:

  • -fx-font

  • -fx-text-fill

  • -fx-prompt-text-fill

  • -fx-highlight-fill

  • -fx-highlight-text-fill

  • -fx-display-caret

默认情况下,-fx-font属性继承自父属性。-fx-display-caret属性的值可以是 true 或 false。如果为 true,则当控件获得焦点时会显示插入符号。否则,不显示插入符号。其默认值为 true。大多数其他属性会影响背景和文本颜色。

了解文本字段控件

TextField是一个文本输入控件。它继承自TextInputControl类。它允许用户输入单行纯文本。如果你需要一个控件来输入多行文本,使用TextArea来代替。文本中的换行符和制表符被删除。图 12-38 显示了一个带有两个TextField文本 Layne 和 Estes 的窗口。

img/336502_2_En_12_Fig38_HTML.jpg

图 12-38

带有两个TextField控件的窗口

您可以使用空的初始文本或指定的初始文本创建一个TextField,如下面的代码所示:

// Create a TextField with an empty string as initial text
TextField nameFld1 = new TextField();

// Create a TextField with "Layne Estes" as an initial text
TextField nameFld2 = new TextField("Layne Estes");

正如我已经提到的,TextFieldtext属性存储文本内容。如果您对处理一个TextField中的变化感兴趣,您需要将一个ChangeListener添加到它的文本属性中。大多数情况下,您将使用它的setText(String newText)方法来设置新文本,使用getText()方法来从中获取文本。TextField增加以下属性:

  • alignment

  • onAction

  • prefColumnCount

当有空白空间时,alignment属性决定了文本在TextField区域内的对齐方式。如果节点方向为LEFT_TO_RIGHT,则默认值为CENTER_LEFT,如果节点方向为RIGHT_TO_LEFT,则默认值为CENTER_RIGHTonAction属性是一个ActionEvent处理程序,在TextField中按回车键时调用,如下面的代码所示:

TextField nameFld = new TextField();
nameFld.setOnAction(e -> /* Your ActionEvent handler code...*/ );

prefColumnCount属性决定控件的宽度。默认情况下,其值为 12。一列的宽度足以显示一个大写字母 w。如果将它的值设置为 10,则TextField的宽度将足以显示十个字母 w,如下面的代码所示:

// Set the preferred column count to 10
nameFld.setPrefColumnCount(10);

TextField提供一个默认的上下文菜单,如图 12-39 所示,单击鼠标右键可以显示该菜单。菜单项根据上下文启用或禁用。您可以用自定义上下文菜单替换默认上下文菜单。目前,没有办法自定义默认的上下文菜单。

img/336502_2_En_12_Fig39_HTML.jpg

图 12-39

默认的上下文菜单为TextField

下面的代码片段为一个TextField设置了一个定制的上下文菜单。它显示一个菜单项,说明上下文菜单已禁用。选择菜单项没有任何作用。您需要向上下文菜单中的菜单项添加一个ActionEvent处理程序来执行一些操作。

ContextMenu cm = new ContextMenu();
MenuItem dummyItem = new MenuItem("Context menu is disabled");
cm.getItems().add(dummyItem);

TextField nameFld = new TextField();
nameFld.setContextMenu(cm);

清单 12-27 中的程序展示了如何使用TextField控件。它显示了两个TextField。它显示了ActionEvent处理程序,一个定制的上下文菜单,以及添加到TextFieldChangeListeners

// TextFieldTest.java
// ... find in the book's download area.

Listing 12-27Using the TextField Control

使用 CSS 样式化文本字段

一个TextField的默认 CSS 样式类名是text-field *。*它添加了一个-fx-alignment属性,该属性是其内容区域内文本的对齐方式。造型TextField没什么特别需要说的。

理解密码字段控件

PasswordField是一个文本输入控件。它从TextField继承而来,它的工作方式与TextField非常相似,除了它屏蔽了它的文本,也就是说,它不显示实际输入的字符。相反,它为输入的每个字符显示一个回显字符。默认回显字符是一个项目符号。图 12-40 显示了一个带有PasswordField的窗口。

img/336502_2_En_12_Fig40_HTML.jpg

图 12-40

使用PasswordField控件的窗口

PasswordField类只提供了一个构造器,它是一个无参数的构造器。您可以使用setText()getText()方法分别设置和获取PasswordField中的实际文本,如下面的代码所示。通常,您不需要设置密码文本。用户输入它。

// Create a PasswordField
PasswordField passwordFld = new PasswordField();
...
// Get the password text
String passStr = passwordFld.getText();

PasswordField覆盖了TextInputControl类的cut()copy()方法,使它们成为无操作方法。也就是说,您不能使用键盘快捷键或上下文菜单将PasswordField中的文本转移到剪贴板。

一个PasswordField的默认 CSS 样式类名是password-field。它拥有TextField的所有风格属性。它不添加任何样式属性。

了解 TextArea 控件

TextArea是一个文本输入控件。它继承自TextInputControl类。它允许用户输入多行纯文本。如果你需要一个控件来输入一行纯文本,使用TextField来代替。如果您想使用富文本,请使用HTMLEditor控件。与TextField不同,文本中的换行符和制表符被保留。在TextArea中,换行符开始一个新的段落。图 12-41 显示了带有TextFieldTextArea的窗口。用户可以在TextArea中输入多行简历。

img/336502_2_En_12_Fig41_HTML.png

图 12-41

带有TextArea控件的窗口

您可以使用以下代码创建一个带有空初始文本或指定初始文本的TextArea:

// Create a TextArea with an empty string as its initial text
TextArea resume1 = new TextArea();

// Create a TextArea an initial text
TextArea resume2 = new TextArea("Years of Experience: 19");

正如上一节已经讨论过的,TextAreatext属性存储文本内容。如果您对处理一个TextArea中的变化感兴趣,您需要向它的text属性添加一个ChangeListener。大多数时候,您将使用它的setText(String newText)方法设置新文本,使用它的getText()方法从中获取文本。

TextArea添加以下属性:

  • prefColumnCount

  • prefRowCount

  • scrollLeft

  • scrollTop

  • wrapText

prefColumnCount属性决定控件的宽度。默认情况下,其值为 32。一个列的宽度足以显示一个大写字母 w。如果将它的值设置为 80,那么TextArea的宽度将足以显示 80 个字母 Ws。以下代码实现了这一点:

// Set the preferred column count to 80
resume1.setPrefColumnCount(80);

prefRowCount属性决定控件的高度。默认情况下,它是 10。以下代码将行数设置为 20:

// Set the preferred row count to 20
resume.setPrefColumnCount(20);

如果文本超过了列数和行数,将自动显示水*和垂直滚动窗格。

TextFieldTextArea提供了一个默认的上下文菜单。请参考“理解文本输入控件”一节,了解关于如何定制默认上下文菜单的更多细节。

scrollLeftscrollTop属性是文本滚动到顶部和左侧的像素数。以下代码将其设置为 30px:

// Scroll the resume text by 30px to the top and 30 px to the left
resume.setScrollTop(30);
resume.setScrollLeft(30);

默认情况下,TextArea在其文本中遇到换行符时会开始新的一行。换行符还会在第一段之外创建一个新的段落。默认情况下,如果文本超出控件的宽度,它不会换行到下一行。属性决定了当文本超出控件的宽度时是否换行。默认情况下,其值为 false。以下代码会将默认值设置为 true:

// Wrap the text if needed
resume.setWrapText(true);

TextArea类的getParagraphs()方法返回文本中所有段落的不可修改列表。列表中的每个元素都是一个段落,是CharSequence的一个实例。返回的段落不包含换行符。下面的代码片段打印了resume TextArea中所有段落的详细信息,例如段落编号和字符数:

ObservableList<CharSequence> list = resume.getParagraphs();
int size = list.size();
System.out.println("Paragraph Count:" + size);
for(int i = 0; i < size; i++) {
        CharSequence cs = list.get(i);
        System.out.println("Paragraph #" + (i + 1) + ", Characters="  + cs.length());
        System.out.println(cs);
}

清单 12-28 中的程序展示了如何使用TextArea。它显示一个带有按钮的窗口,用于打印TextArea中文本的详细信息。

// TextAreaTest.java
// ... find in the book's download area.

Listing 12-28Using TextArea Controls

使用 CSS 对文本区域进行样式化

一个TextArea的默认 CSS 样式类名是text-area。它不会向其祖先TextInputControl中的 CSS 属性添加任何 CSS 属性。它包含scroll-panecontent子结构,分别是一个ScrollPane和一个Regionscroll-pane是当文本超出宽度或高度时出现的滚动窗格。content是显示文本的区域。

以下样式将水*和垂直滚动条策略设置为always,因此滚动条应该总是出现在TextArea中。内容区域的填充设置为 10px:

.text-area > .scroll-pane {
        -fx-hbar-policy: always;
        -fx-vbar-policy: always;
}

.text-area .content {
        -fx-padding: 10;
}

Tip

在撰写本文时,TextArea忽略了为scroll-pane子结构设置滚动条策略。

显示任务的进度

当您有一个长时间运行的任务时,您需要向用户提供一个关于任务进度的可视化反馈,以获得更好的用户体验。JavaFX 提供了两个控件来显示进度:

  • ProgressIndicator

  • ProgressBar

它们显示进度的方式不同。ProgressBar类继承自ProgressIndicator类。ProgressIndicator在圆形控件中显示进度,而ProgressBar使用水*条。ProgressBar类不添加任何属性或方法。它只是为控件使用了不同的形状。图 12-42 显示了不确定和确定状态下的ProgressIndicator。图 12-43 显示了处于不确定和确定状态的ProgressBar。这两个图在确定状态的四个实例中使用相同的进度值。

img/336502_2_En_12_Fig43_HTML.jpg

图 12-43

不确定和确定状态下的ProgressBar控制

img/336502_2_En_12_Fig42_HTML.jpg

图 12-42

不确定和确定状态下的ProgressIndicator控制

可以确定或不确定任务的当前进度。如果不能确定进度,就说处于不确定状态。如果进程是已知的,就说它处于确定的状态。ProgressIndicator类声明了两个属性:

  • indeterminate

  • progress

indeterminate属性是只读的boolean属性。如果它返回true,就意味着无法确定进度。这种状态下的一个ProgressIndicator被渲染成某种重复的动画。progress房产是一个double房产。其值表示 0%和 100%之间的进度。负值表示进度不确定。介于 0 和 1.0 之间的值表示进度介于 0%和 100%之间的确定状态。大于 1.0 的值被视为 1.0(即 100%进度)。

这两个类都提供了默认的构造器,用于创建处于不确定状态的控件,如下面的代码所示:

// Create an indeterminate progress indicator and a progress bar
ProgressIndicator indeterminateInd = new ProgressIndicator();
ProgressBar indeterminateBar = new ProgressBar();

其他接受进度值的构造器创建不确定或确定状态的控件。如果进度值为负,它们将创建处于不确定状态的控件。否则,它们会在确定状态下创建控件,如下面的代码所示:

// Create a determinate progress indicator with 10% progress
ProgressIndicator indeterminateInd = new ProgressIndicator(0.10);

// Create a determinate progress bar with 70% progress
ProgressBar indeterminateBar = new ProgressBar(0.70);

清单 12-29 中的程序显示了如何使用ProgressIndicatorProgressBar控件。点按“制作进度”按钮会使进度增加 10%。单击“完成任务”按钮,通过将进度设置为 100%来完成不确定的任务。通常,当任务进展到一个里程碑时,这些控件的progress属性由一个长期运行的任务更新。您使用了一个按钮来更新progress属性,以保持程序逻辑简单。

// ProgressTest.java
// ... find in the book's download area.

Listing 12-29Using the ProgressIndicator and ProgressBar Controls

带有 CSS 的样式渐进指示器

一个ProgressIndicator的默认 CSS 样式类名是progress-indicatorProgressIndicator支持determinateindeterminate CSS 伪类。当indeterminate属性为假时,determinate伪类适用。当indeterminate属性为真时,in determinate伪类适用。

ProgressIndicator有一个名为-fx-progress-color的 CSS 样式属性,是进度的颜色。以下样式将进度颜色设置为红色表示不确定进度,蓝色表示确定进度:

.progress-indicator:indeterminate {
        -fx-progress-color: red;
}

.progress-indicator:determinate {
        -fx-progress-color: blue;
}

ProgressIndicator包含四个子结构:

  • 一个indicator子结构,它是一个StackPane

  • 一个progress子结构,也就是一个StackPane

  • 一个percentage子结构,也就是一个Text

  • 一个tick子结构,也就是一个StackPane

您可以设计一个ProgressIndicator的所有子结构。样本代码请参考modena.css文件。

带有 CSS 的样式进度条指示器进度条

一个ProgressBar的默认 CSS 样式类名是progress-bar。它支持 CSS 样式属性:

  • -fx-indeterminate-bar-length

  • -fx-indeterminate-bar-escape

  • -fx-indeterminate-bar-flip

  • -fx-indeterminate-bar-animation-time

所有属性都适用于显示不确定进度的条形图。默认条形长度为 60px。使用-fx-indeterminate-bar-length属性指定不同的条形长度。

-fx-indeterminate-bar-escape属性为 true 时,条形起始边缘从轨道的起始边缘开始,条形尾边缘在轨道的结束边缘结束。也就是说,小节显示在轨道长度之外。当此属性为 false 时,条形在轨道长度内移动。默认值为 true。

-fx-indeterminate-bar-flip属性指示该条是只在一个方向上移动还是在两个方向上移动。默认值为 true,这意味着该条通过在每条边的末端翻转其方向而向两个方向移动。

属性是条从一个边缘到另一个边缘应该花费的时间(以秒为单位)。默认值为 2。

ProgressBar包含两个子结构:

  • 轨道底座,即StackPane

  • 一个条形子结构,它是一个区域

以下样式修改了ProgressBar控件的背景色、条和轨迹的半径,使其看起来如图 12-44 所示:

img/336502_2_En_12_Fig44_HTML.jpg

图 12-44

定制ProgressBar控件的条和轨道

.progress-bar .track  {
        -fx-background-color: lightgray;
        -fx-background-radius: 5;
}

.progress-bar .bar  {
        -fx-background-color: blue;
        -fx-background-radius: 5;
}

了解标题窗格控件

TitledPane是带标签的控件。TitledPane类继承自Labeled类。一个带标签的控件可以有文本和图形,所以它可以有一个TitledPaneTitledPane显示文本作为标题。该图形显示在标题栏中。

除了文本和图形,TitledPane还有内容,这是一个Node。通常,一组控件被放在一个容器中,该容器被添加为TitledPane的内容。TitledPane可处于折叠或展开状态。在折叠状态下,它只显示标题栏并隐藏内容。在展开状态下,它显示标题栏和内容。在其标题栏中,它显示一个箭头,指示它是展开还是折叠。单击标题栏中的任意位置可以展开或折叠内容。图 12-45 显示了处于两种状态的TitledPane及其所有零件。

img/336502_2_En_12_Fig45_HTML.png

图 12-45

处于折叠和展开状态

使用默认构造器创建一个没有标题和内容的TitledPane。您可以稍后使用setText()setContent()方法来设置它们。或者,您可以使用以下代码将标题和内容作为参数提供给其构造器:

// Create a TitledPane and set its title and content
TitledPane infoPane1 = new TitledPane();
infoPane1.setText("Personal Info");
infoPane1.setContent(new Label("Here goes the content."));

// Create a TitledPane with a title and content
TitledPane infoPane2 = new TitledPane("Personal Info", new Label("Content"));

您可以使用在Labeled类中声明的setGraphic()方法向TitledPane添加图形,如以下代码所示:

String imageStr = "resources/picture/privacy_icon.png";
URL imageUrl = getClass().getClassLoader().getResource(imageStr);
Image img = new Image(imageUrl.toExternalForm());
ImageView imgView = new ImageView(img);
infoPane2.setGraphic(imgView);

TitledPane类声明了四个属性:

  • animated

  • collapsible

  • content

  • expanded

animated属性是一个boolean属性,指示折叠和展开动作是否是动画的。默认情况下,这是真的,这些动作是动画。collapsible属性是一个boolean属性,表示TitledPane是否可以崩溃。默认情况下,它被设置为 true,并且TitledPane可以折叠。如果不希望TitledPane折叠,将该属性设置为 false。不可折叠的TitledPane在其标题栏不显示箭头。content属性是一个Object属性,存储任何节点的引用。当控件处于展开状态时,内容可见。expanded属性是一个boolean属性。当属性为 true 时,TitledPane处于展开状态。否则,它处于折叠状态。默认情况下,TitledPane处于展开状态。使用setExpanded()方法以编程方式展开和折叠TitledPane,如以下代码所示:

// Set the state to expanded
infoPane2.setExpanded(true);

Tip

如果您对处理一个TitledPane的展开和折叠事件感兴趣,可以将一个ChangeListener添加到它的expanded属性中。

通常,TitledPane控件在Accordion控件中成组使用,为了节省空间,每次只显示展开状态的组中的一个TitledPane。如果您想成组显示控件,也可以使用独立的TitledPane

Tip

回想一下,TitledPane的高度随着其展开和折叠而变化。不要在代码中设置它的最小、首选和最大高度。否则,可能会导致未指定的行为。

清单 12-30 中的程序显示了如何使用TitledPane控件。它显示一个带有TitledPane的窗口,让用户输入一个人的名字、姓氏和出生日期。

// TitledPaneTest.java
// ... find in the book's download area.

Listing 12-30Using the TitledPane Control

带有 CSS 的样式标题窗格

一个TitledPane的默认 CSS 样式类名是titled-paneTitledPane增加了两个boolean类型的样式属性:

  • -fx-animated

  • -fx-collapsible

这两个属性的默认值都是 true。-fx-animated属性指示展开和折叠动作是否是动画的。-fx-collapsible属性指示控件是否可以折叠。

TitledPane支持两种 CSS 伪类:

  • collapsed

  • expanded

当控件折叠时,collapsed伪类适用,当控件展开时,expanded伪类适用。

TitledPane包含两个子结构:

  • title

  • Content

title子结构是一个包含标题栏内容的StackPanetitle子结构包含文本和箭头按钮子结构。文本子结构是一个Label,它保存标题文本和图形。箭头按钮子结构是一个包含箭头子结构的StackPane,它也是一个StackPane。箭头子结构是一个指示器,它显示控件是处于展开状态还是折叠状态。内容子结构是一个包含控件内容的StackPane

让我们来看一个将四种不同样式应用于TitledPane控件的效果的例子,如下面的代码所示:

/* #1 */
.titled-pane > .title  {
        -fx-background-color: lightgray;
        -fx-alignment: center-right;
}

/* #2 */
.titled-pane > .title > .text {
        -fx-font-size: 14px;
        -fx-underline: true;
}

/* #3 */
.titled-pane > .title > .arrow-button > .arrow {
        -fx-background-color: blue;
}

/* #4 */
.titled-pane > .content {
        -fx-background-color: burlywood;
        -fx-padding: 10;
}

样式#1 将标题的背景色设置为浅灰色,并将图形和标题放在标题栏的中央右侧。样式#2 将标题文本的字体大小改为 14px 并加下划线。在撰写本文时,使用-fx-text-fill属性设置标题的文本颜色不起作用,并且在TitledPane上设置-fx-text-fill属性本身也会影响内容的文本颜色。样式#3 将箭头的背景色设置为蓝色。样式#4 设置内容区域的背景颜色和填充。图 12-46 显示应用前述样式后与图 12-45 相同的窗口。

img/336502_2_En_12_Fig46_HTML.jpg

图 12-46

将样式应用到TitledPane的效果

了解手风琴控件

Accordion是简单控件。它显示一组TitledPane控件,其中一次只有一个控件处于展开状态。图 12-47 显示了一个带有Accordion的窗口,包含三个TitledPanesTitledPane将军被扩大了。地址和电话TitledPane都被折叠了。

img/336502_2_En_12_Fig47_HTML.png

图 12-47

一个Accordion带三个TitledPanes

Accordion类只包含一个构造器(无参数构造器)来创建它的对象:

// Create an Accordian
Accordion root = new Accordion();

Accordion将它的TitledPane控件列表存储在一个ObservableList<TitledPane>中。getPanes()方法返回TitledPane的列表。使用该列表向Accordion添加或移除任何TitledPane,如以下代码所示:

TitledPane generalPane = new TitledPane();
TitledPane addressPane = new TitledPane();
TitledPane phonePane = new TitledPane();
...
Accordion root = new Accordion();
root.getPanes().addAll(generalPane, addressPane, phonePane);

Accordion类包含一个expandedPane属性,存储当前展开的TitledPane的引用。默认情况下,Accordion以折叠状态显示其所有的TitledPanes,该属性设置为null。点击TitledPane的标题栏或使用setExpandedPane()方法展开TitledPane。如果您对扩展的TitledPane何时改变感兴趣,请向该属性添加一个ChangeListener。清单 12-31 中的程序显示了如何创建和填充一个Accordion

// AccordionTest.java
// ... find in the book's download area.

Listing 12-31Using the Accordion Control

CSS 样式手风琴

一个Accordion的默认 CSS 样式类名是accordionAccordion不添加任何 CSS 属性。它包含一个first-titled-pane子结构,这是第一个TitledPane。以下样式设置所有TitledPane的标题栏的背景色和插图:

.accordion > .titled-pane > .title {
    -fx-background-color: burlywood;
        -fx-background-insets: 1;
}

下面的样式设置了Accordion的第一个TitledPane的标题栏的背景色:

.accordion > .first-titled-pane > .title {
    -fx-background-color: derive(red, 80%);
}

了解分页控件

Pagination用于显示一个大的单一内容,方法是将内容分成称为页面的小块,例如搜索结果。图 12-48 显示了一个Pagination控件。一个Pagination控件有一个页数,也就是其中的页数。如果页数未知,则页数可能不确定。每一页都有一个从零开始的索引。

img/336502_2_En_12_Fig48_HTML.png

图 12-48

控制

一个Pagination控件分为两个区域:

  • 内容区域

  • 航行区域

内容区域显示当前页面的内容。导航区域包含允许用户从一个页面导航到另一个页面的部分。您可以按顺序或随机地在页面之间导航。一个Pagination控制的部件如图 12-49 所示。

img/336502_2_En_12_Fig49_HTML.png

图 12-49

Pagination控件的组成部分

上一页和下一页箭头按钮允许用户分别导航到上一页和下一页。当您在第一页时,“上一页”按钮被禁用。当您在最后一页时,“下一页”按钮被禁用。页面指示器还允许您通过显示所有页码来导航到特定页面。默认情况下,页面指示器使用工具提示来显示页码,您可以选择使用 CSS 属性禁用页码。所选页面指示器显示当前页面。所选页面标签显示当前页面选择的详细信息。

Pagination类提供了几个构造器。它们以不同的方式配置控件。默认构造器创建一个控件,该控件具有不确定的页数,并以零作为选定页的索引,如下面的代码所示:

// Indeterminate page count and first page selected
Pagination pagination1 = new Pagination();

当页数不确定时,页面指示器标签显示x/...,其中 x 是当前页面索引加 1。

使用另一个构造器来指定页数,如下面的代码所示:

// 5 as the page count and first page selected
Pagination pagination2 = new Pagination(5);

您还可以使用另一个构造器来指定页数和选定的页索引,如下面的代码所示:

// 5 as the page count and second page selected (page index starts at 0)
Pagination pagination3 = new Pagination(5, 1);

Pagination类声明了一个INDETERMINATE常量,可用于指定不确定的页数,如以下代码所示:

// Indeterminate page count and second page selected
Pagination pagination4 = new Pagination(Pagination.INDETERMINATE, 1);

Pagination类包含以下属性:

  • currentPageIndex

  • maxPageIndicatorCount

  • pageCount

  • pageFactory

currentPageIndex是一个整数属性。它的值是要显示的页面的页面索引。默认值为零。您可以使用其中一个构造器或使用setCurrentPageIndex()方法来指定它的值。如果将其值设置为小于零,则第一页索引(为零)将被设置为其值。如果将其值设置为大于页数减 1,则其值将设置为页数减 1。如果您想知道新页面何时显示,可以在currentPageIndex属性中添加一个ChangeListener

maxPageIndicatorCount是一个整数属性。它设置要显示的页面指示器的最大数量。默认为十。如果其设置超出了页数范围,其值将保持不变。如果其值设置得太高,则减小该值,以便页面指示器的数量适合控件。您可以使用setMaxPageIndicatorCount()方法设置它的值。

pageCount是一个整数属性。它是Pagination控件中的页数。其值必须大于或等于 1。它默认为不确定。它的值可以在构造器中设置或者使用setPageCount()方法。

pageFactory是最重要的属性。它是一个Callback<Integer, Node>类型的对象属性。它用于生成页面。当需要显示页面时,控件调用传递页面索引的Callback对象的call()方法。call()方法返回一个作为页面内容的节点。下面的代码片段为一个Pagination控件创建并设置了一个页面工厂。页面工厂返回一个Label:

// Create a Pagination with an indeterminate page count
Pagination pagination = new Pagination();

// Create a page factory that returns a Label
Callback<Integer, Node> factory =
    pageIndex -> new Label("Content for page " + (pageIndex + 1));

// Set the page factory
pagination.setPageFactory(factory);

Tip

如果页面索引不存在,页面工厂的call()方法应该返回null。当call()方法返回null时,当前页面不变。

清单 12-32 中的程序展示了如何使用Pagination控件。它将页数设置为五。页面工厂返回一个带有显示页码的文本的Label。它将显示一个带有Pagination控件的窗口,类似于图 12-48 所示。

// PaginationTest.java
// ... find in the book's download area.

Listing 12-32Using the Pagination Control

页面指示器可以是数字按钮或项目符号按钮。默认情况下使用数字按钮。Pagination类包含一个名为STYLE_CLASS_BULLETString常量,如果你想使用项目按钮,它是控件的样式类。下面的代码片段创建了一个Pagination控件,并将其样式类设置为使用项目符号按钮作为页面指示器。图 12-50 显示了一个带有作为页面指示器的项目按钮的Pagination控件。

img/336502_2_En_12_Fig50_HTML.png

图 12-50

使用项目符号按钮作为页面指示器的Pagination控件

Pagination pagination = new Pagination(5);

// Use bullet page indicators
pagination.getStyleClass().add(Pagination.STYLE_CLASS_BULLET);

用 CSS 样式化分页

一个Pagination控件的默认 CSS 样式类名是paginationPagination增加了几个 CSS 属性:

  • -fx-max-page-indicator-count

  • -fx-arrows-visible

  • -fx-tooltip-visible

  • -fx-page-information-visible

  • -fx-page-information-alignment

属性指定要显示的页面指示器的最大数量。默认值为 10。属性指定上一页和下一页按钮是否可见。默认值为 true。-fx-tooltip-visible属性指定当鼠标悬停在页面指示器上时是否显示工具提示。默认值为 true。-fx-page-information-visible指定所选页面标签是否可见。默认值为 true。-fx-page-information-alignment指定所选页面标签相对于页面指示器的位置。可能的值有顶部、右侧、底部和左侧。默认值为 bottom,在页面指示器下方显示选定的页面指示器。

Pagination控件有两个StackPane类型的子结构:

  • page

  • pagination-control

page子结构代表内容区域。pagination-control子结构代表导航区域,它有以下子结构:

  • left-arrow-button

  • right-arrow-Button

  • bullet-button

  • number-button

  • page-information

left-arrow-buttonright-arrow-button子结构属于Button类型。它们分别代表“上一页”和“下一页”按钮。left-arrow-button子结构有一个left-arrow子结构,是一个StackPane,代表上一页按钮中的箭头。right-arrow-button子结构有一个right-arrow子结构,是一个StackPane,它代表下一页按钮中的箭头。bullet-buttonnumber-buttonToggleButton类型,代表页面指示器。page-information子结构是保存所选页面信息的Labelpagination-control子结构在一个叫做control-box的子结构中保存了上一页和下一页按钮以及页面指示器,这是一个HBox

下列样式使所选页面标签不可见,将页面背景设置为浅灰色,并在“上一页”、“下一页”和“页面指示器”按钮周围绘制边框。请参考modena.css文件,了解更多关于如何设计Pagination控件样式的细节。

.pagination  {
        -fx-page-information-visible: false;
}

.pagination > .page {
    -fx-background-color: lightgray;
}

.pagination  > .pagination-control > .control-box {
    -fx-padding: 2;
    -fx-border-style: dashed;
    -fx-border-width: 1;
    -fx-border-radius: 5;
    -fx-border-color: blue;
}

了解工具提示控件

工具提示是一个弹出控件,用于显示节点的附加信息。当鼠标指针悬停在节点上时,它会显示出来。当鼠标指针悬停在某个节点上时和显示该节点的工具提示时之间会有一小段延迟。工具提示在一小段时间后隐藏。当鼠标指针离开控件时,它也被隐藏。你不应该设计一个 GUI 应用程序,在那里用户依赖于看到控件的工具提示,因为如果鼠标指针从不停留在控件上,它们可能根本不会显示。图 12-51 显示了一个带有工具提示的窗口,显示保存数据文本。

img/336502_2_En_12_Fig51_HTML.jpg

图 12-51

显示工具提示的窗口

工具提示由继承自PopupControl类的Tooltip类的实例表示。工具提示可以包含文本和图形。您可以使用默认的构造器创建工具提示,该构造器没有文本和图形。您还可以使用其他构造器创建带有文本的工具提示,如下面的代码所示:

// Create a Tooltip with No text and no graphic
Tooltip tooltip1 = new Tooltip();

// Create a Tooltip with text
Tooltip tooltip2 = new Tooltip("Closes the window");

需要为使用Tooltip类的install()静态方法的节点安装一个工具提示。使用uninstall()静态方法卸载一个节点的工具提示:

Button saveBtn = new Button("Save");
Tooltip tooltip = new Tooltip("Saves the data");

// Install a tooltip
Tooltip.install(saveBtn, tooltip);
...
// Uninstall the tooltip
Tooltip.uninstall(saveBtn, tooltip);

工具提示经常用于 UI 控件。因此,为控件安装工具提示变得更加容易。Control类包含一个tooltip属性,它是一个Tooltip类型的对象属性。你可以使用Control类的setTooltip()方法为控件设置一个Tooltip。如果一个节点不是控件,例如一个Circle节点,您将需要使用install()方法来设置一个工具提示,如前面所示。以下代码片段显示了如何使用按钮的tooltip属性:

Button saveBtn = new Button("Save");

// Install a tooltip
saveBtn.setTooltip(new Tooltip("Saves the data"));
...
// Uninstall the tooltip
saveBtn.setTooltip(null);

Tip

工具提示可以在多个节点之间共享。工具提示使用一个Label控件来显示它的文本和图形。在内部,工具提示上设置的所有与内容相关的属性都委托给了Label控件。

Tooltip类包含几个属性:

  • text

  • graphic

  • contentDisplay

  • textAlignment

  • textOverrun

  • wrapText

  • graphicTextGap

  • font

  • activated

  • hideDelay

  • showDelay

  • showDuration

text属性是一个String属性,它是要在工具提示中显示的文本。graphic属性是一个Node类型的对象属性。它是工具提示的图标。contentDisplay属性是ContentDisplay枚举类型的对象属性。它指定图形相对于文本的位置。可能的值是ContentDisplay枚举中的常量之一:TOPRIGHTBOTTOMLEFTCENTERTEXT_ONLYGRAPHIC_ONLY。默认值为 LEFT,将图形放置在文本的左侧。

下面的代码片段使用一个图标作为工具提示,并将其放在文本上方。图标只是一个文本为 X 的Label。图 12-52 显示了刀头的外观。

img/336502_2_En_12_Fig52_HTML.jpg

图 12-52

使用图标并将其放在工具提示中文本的顶部

// Create and configure the Tooltip
Tooltip closeBtnTip = new Tooltip("Closes the window");
closeBtnTip.setStyle("-fx-background-color: yellow; -fx-text-fill: black;");

// Display the icon above the text
closeBtnTip.setContentDisplay(ContentDisplay.TOP);

Label closeTipIcon = new Label("X");
closeTipIcon.setStyle("-fx-text-fill: red;");
closeBtnTip.setGraphic(closeTipIcon);

// Create a Button and set its Tooltip
Button closeBtn = new Button("Close");
closeBtn.setTooltip(closeBtnTip);

textAlignment属性是TextAlignment枚举类型的对象属性。当文本跨越多行时,它指定文本对齐方式。可能的值是TextAlignment枚举中的常量之一:LEFTRIGHTCENTERJUSTIFY

textOverrun属性是OverrunStyle枚举类型的对象属性。它指定当工具提示中没有足够的空间来显示整个文本时要使用的行为。默认行为是使用省略号。

wrapText是一个boolean属性。它指定如果文本超出工具提示的宽度,是否应该换行。默认值为假。

graphicTextGap属性是一个double属性,它以像素为单位指定文本和图形之间的间距。默认值为四。font属性是一个Font类型的对象属性。它指定文本使用的默认字体。activated属性是只读的boolean属性。当工具提示被激活时,这是真的。否则就是假的。当鼠标移动到控件上时,工具提示被激活,并在激活后显示。

清单 12-33 中的程序显示了如何创建、配置和设置控件的工具提示。运行应用程序后,将鼠标指针放在 name 字段、Save 按钮和 Close 按钮上。过一会儿,他们的工具提示就会显示出来。“关闭”按钮的工具提示看起来与“保存”按钮不同。它使用一个图标和不同的背景和文本颜色。

// TooltipTest.java
// ... find in the book's download area.

Listing 12-33Using the Tooltip Control

用 CSS 样式化工具提示

一个Tooltip控件的默认 CSS 样式类名是tooltipTooltip增加了几个 CSS 属性:

  • -fx-text-alignment

  • -fx-text-overrun

  • -fx-wrap-text

  • -fx-graphic

  • -fx-content-display

  • -fx-graphic-text-gap

  • -fx-font

所有 CSS 属性都对应于Tooltip类中与内容相关的属性。有关所有这些属性的描述,请参考上一节。以下代码为Tooltip设置背景颜色、文本颜色和环绕文本属性:

.tooltip {
        -fx-background-color: yellow;
        -fx-text-fill: black;
        -fx-wrap-text: true;
}

在控件中提供滚动功能

JavaFX 提供了两个名为ScrollBarScrollPane的控件,为其他控件提供滚动功能。通常,这些控件不会单独使用。它们用于支持其他控件中的滚动。

理解滚动条控件

ScrollBar是一个基本控件,本身不提供滚动功能。它表示为一个水*或垂直的条,允许用户从一系列值中选择一个值。图 12-53 显示了水*和垂直滚动条。

img/336502_2_En_12_Fig53_HTML.png

图 12-53

水*和垂直滚动条及其部件

一个ScrollBar控件由四部分组成:

  • 增加数值的增量按钮

  • 减少按钮,用于减少数值

  • 显示当前值的拇指(或旋钮)

  • 拇指移动的轨迹

竖线ScrollBar中的增量和减量按钮分别位于底部和顶部。

ScrollBar类提供了创建水*滚动条的默认构造器。您可以使用setOrientation()方法将其方向设置为垂直:

// Create a horizontal scroll bar
ScrollBar hsb = new ScrollBar();

// Create a vertical scroll bar
ScrollBar vsb = new ScrollBar();
vsb.setOrientation(Orientation.VERTICAL);

minmax属性表示其值的范围。它的value属性是当前值。minmaxvalue属性的默认值分别为 0、100 和 0。如果您想知道value属性何时改变,您需要向它添加一个ChangeListener。以下代码将把value属性设置为 0、200 和 150:

ScrollBar hsb = new ScrollBar();
hsb.setMin(0);
hsb.setMax(200);
hsb.setValue(150);

滚动条的当前值可以通过三种不同的方式进行更改:

  • 以编程方式使用setValue()increment()decrement()方法

  • 通过用户在轨道上拖动拇指

  • 通过用户点击递增和递减按钮

blockIncrementunitIncrement属性分别指定当用户单击音轨和递增或递减按钮时调整当前值的量。通常,块增量设置为大于单位增量的值。

一个ScrollBar控件的默认 CSS 样式类名是scroll-barScrollBar支持两个 CSS 伪类:horizontalvertical。它的一些属性可以使用 CSS 来设置。

ScrollBar很少被开发者直接使用。它用于构建支持滚动的完整控件,例如ScrollPane控件。如果您需要为控件提供滚动功能,请使用ScrollPane,我将在下一节中讨论。

了解滚动条控件

一个ScrollPane提供了一个节点的可滚动视图。一个ScrollPane由一个水*ScrollBar、一个垂直ScrollBar和一个内容节点组成。ScrollPane提供滚动的节点是内容节点。如果您想要提供多个节点的可滚动视图,将它们添加到一个布局窗格,例如一个GridPane,然后将布局窗格作为内容节点添加到ScrollPaneScrollPane使用滚动策略指定何时显示特定的滚动条。内容可见的区域称为视窗。图 12-54 显示了一个以Label为内容节点的ScrollPane

img/336502_2_En_12_Fig54_HTML.jpg

图 12-54

以一个Label作为其内容节点的一个ScrollPane

Tip

一些常用的需要滚动功能的控件,例如一个TextArea,提供了一个内置的ScrollPane,它是这类控件的一部分。

您可以使用ScrollPane类的构造器创建一个空的ScrollPane或一个带有内容节点的ScrollPane,如下面的代码所示。您可以稍后使用setContent()方法设置内容节点。

Label poemLbl1 = ...
Label poemLbl2 = ...

// Create an empty ScrollPane
ScrollPane sPane1 = new ScrollPane();

// Set the content node for the ScrollPane
sPane1.setContent(poemLbl1);

// Create a ScrollPane with a content node
ScrollPane sPane2 = new ScrollPane(poemLbl2);

Tip

ScrollPane基于内容的布局边界为其内容提供滚动。如果内容使用效果或变换,例如缩放,您需要将内容包装在一个Group中,并将Group添加到ScrollPane中,以获得正确的滚动。

ScrollPane类包含几个属性,其中大多数通常不被开发人员使用:

  • content

  • pannable

  • fitToHeight

  • fitToWidth

  • hbarPolicy

  • vbarPolicy

  • hmin

  • hmax

  • hvalue

  • vmin

  • vmax

  • vvalue

  • prefViewportHeight

  • prefViewportWidth

  • viewportBounds

content属性是Node类型的对象属性,它指定了内容节点。您可以使用滚动条或*移来滚动内容。如果使用*移,您需要在按下左、右或两个按钮时拖动鼠标来滚动内容。默认情况下,ScrollPane是不可*移的,你需要使用滚动条来滚动内容。pannable属性是一个boolean属性,指定ScrollPane是否可*移。使用setPannable(true)方法使ScrollPane可*移。

fitToHeightfitToWidth属性分别指定是否调整内容节点的大小以匹配视口的高度和宽度。默认情况下,它们是假的。如果内容节点不可调整大小,则忽略这些属性。图 12-55 显示了与图 12-54 相同的ScrollPane,其fitToHeightfitToWidth属性设置为真。请注意,Label内容节点已经被调整大小以适合视窗。

img/336502_2_En_12_Fig55_HTML.jpg

图 12-55

fitToHeightfitToWidth属性设置为真的ScrollPane

hbarPolicyvbarPolicy属性是ScrollPane.ScrollBarPolicy枚举类型的对象属性。它们指定何时显示水*和垂直滚动条。可能的值有ALWAYSAS_NEEDEDNEVER。当策略设置为ALWAYS时,滚动条一直显示。当策略设置为AS_NEEDED时,滚动条会根据内容的大小在需要时显示。当策略设置为NEVER时,滚动条从不显示。

hminhmaxhvalue属性分别指定水*滚动条的最小值、最大值和值属性。vminvmaxvvalue属性分别指定垂直滚动条的最小值、最大值和值属性。通常,不设置这些属性。它们根据内容和用户滚动内容而变化。

prefViewportHeightprefViewportWidth分别是内容节点可用的视口的首选高度和宽度。

viewportBoundsBounds类型的对象属性。这是视口的实际边界。清单 12-34 中的程序展示了如何使用ScrollPane。它设置了一个有四行文本作为内容的Label。这也使得ScrollPane成为众矢之的。也就是说,你可以拖动鼠标点击它的按钮来滚动文本。

// ScrollPaneTest.java
// ... find in the book's download area.

Listing 12-34Using ScrollPane

一个ScrollPane控件的默认 CSS 样式类名是scroll-pane。样本样式请参考modena.css文件,ScrollPane支持的 CSS 属性和伪类的完整列表请参考在线 JavaFX CSS 参考指南

把事情分开

有时,您可能希望将逻辑上相关的控件水*或垂直并排放置。为了获得更好的外观,控件使用不同类型的分隔符进行分组。有时候,使用边框就足够了,但是有时候你会使用TitledPane控件。SeparatorSplitPane控件仅用于在视觉上区分两个控件或两组控件。

了解分离器控制

Separator是分隔两组控件的水*线或垂直线。通常,它们用在菜单或组合框中。图 12-56 显示由水*和垂直分隔符分隔的餐厅菜单项。

img/336502_2_En_12_Fig56_HTML.jpg

图 12-56

使用水*和垂直分隔符

默认的构造器创建一个水*的Separator。要创建一个垂直的Separator,可以在构造器中指定一个垂直方向或者使用setOrientation()方法,如下面的代码所示:

// Create a horizontal separator
Separator separator1 = new Separator();

// Change the orientation to vertical
separator1.setOrientation(Orientation.VERTICAL);

// Create a vertical separator
Separator separator2 = new Separator(Orientation.VERTICAL);

分隔符会自行调整大小以填充分配给它的空间。一个水*的Separator水*调整大小,一个垂直的Separator垂直调整大小。从内部来说,Separator就是Region。你可以用 CSS 改变它的颜色和厚度。

Separator类包含三个属性:

  • orientation

  • halignment

  • valignment

属性指定控件的方向。可能的值是Orientation枚举的两个常量之一:HORIZONTALVERTICALhalignment属性指定垂直分隔线宽度内分隔线的水*对齐方式。对于水*分隔符,该属性被忽略。可能的值是HPos枚举的常量之一:LEFTCENTERRIGHT。默认值是居中。v alignment属性指定水*分隔符高度内分隔线的垂直对齐方式。对于垂直分隔符,该属性被忽略。可能的值是VPos枚举的常量之一:BASELINETOPCENTERBOTTOM。默认值是居中。

带 CSS 的造型分隔符

一个Separator co控件的默认 CSS 样式类名是separatorSeparator包含 CSS 属性,对应其 Java 属性:

  • -fx-orientation

  • -fx-halignment

  • -fx-valignment

Separator支持分别应用于水*和垂直分隔符的horizontalvertical CSS 伪类。它包含一个line子结构,这是一个Region。您在分隔符中看到的线是通过指定line子结构的边界创建的。以下样式用于创建图 12-56 中的分隔符:

.separator > .line {
    -fx-border-style: solid;
    -fx-border-width: 1;
}

您可以使用图像作为分隔符。设置分隔符的适当宽度或高度,并使用图像作为背景图像。下面的代码假设separator.jpg图像文件与包含样式的 CSS 文件存在于同一个目录中。这些样式将水*分隔符的首选高度和垂直分隔符的首选宽度设置为 10px:

.separator {
        -fx-background-image: url("separator.jpg");
        -fx-background-repeat: repeat;
        -fx-background-position: center;
        -fx-background-size: cover;
}

.separator:horizontal {
        -fx-pref-height: 10;
}

.separator:vertical {
        -fx-pref-width: 10;
}

了解分割面板控件

SplitPane排列多个节点,用分隔线将它们水*或垂直分开。用户可以拖动分隔线,因此分隔线一侧的节点会扩展,另一侧的节点会收缩相同的量。通常,SplitPane中的每个节点都是一个包含一些控件的布局窗格。然而,你可以使用任何节点,例如一个Button。如果您使用过 Windows Explorer,那么您已经熟悉了使用SplitPane。在 Windows 资源管理器中,分隔线将树视图和列表视图分开。使用分隔线,您可以调整树视图的宽度,而列表视图的宽度会以相同的量向相反的方向调整。一个可调整大小的 HTML 框架集类似于一个SplitPane。图 12-57 显示了带有水*SplitPane的窗口。SplitPane包含两个VBox布局窗格;其中每个都包含一个Label和一个TextArea。图 12-57 显示分隔线向右拖动,左边的VBox比右边的宽度大。

img/336502_2_En_12_Fig57_HTML.png

图 12-57

一扇横窗SplitPane

您可以使用SplitPane类的默认构造器创建一个SplitPane:

SplitPane sp = new SplitPane();

SplitPane类的getItems()方法返回在SplitPane中存储节点列表的ObservableList<Node>。将所有节点添加到该列表中,如以下代码所示:

// Create panes
GridPane leftPane = new GridPane();
GridPane centerPane = new GridPane();
GridPane rightPane = new GridPane();

/* Populate the left, center, and right panes with controls here */

// Add panels to the a SplitPane
SplitPane sp = new SplitPane();
sp.getItems().addAll(leftPane, centerPane, rightPane);

默认情况下,SplitPane水*放置其节点。它的orientation属性可以用来指定方向:

// Place nodes vertically
sp.setOrientation(Orientation.VERTICAL);

分隔线可以在最左边和最右边或最上边和最下边之间移动,只要它不与任何其他分隔线重叠。分频器位置可以设置在 0 和 1 之间。位置 0 表示最顶端或最左边。位置 1 表示最底部或最右侧。默认情况下,分隔线放置在中间,其位置设定为 0.5。使用以下两种方法之一来设置分隔线的位置:

  • setDividerPositions(double... positions)

  • setDividerPosition(int dividerIndex, double position)

setDividerPositions()方法获取多个分隔线的位置。您必须提供从开始到您想要设置位置的所有分隔线的位置。

如果要设置特定分隔线的位置,使用setDividerPosition()方法。第一个分频器的索引为 0。忽略为超出范围的索引传入的位置。

方法返回所有分隔线的位置。它返回一个double数组。分隔符的索引与数组元素的索引相匹配。

默认情况下,SplitPane在调整大小时会调整其节点的大小。您可以使用setResizableWithParent()静态方法通过SplitPane阻止特定节点调整大小:

// Make node1 non-resizable
SplitPane.setResizableWithParent(node1, false);

清单 12-35 中的程序展示了如何使用SplitPane。显示如图 12-57 所示的窗口。运行程序并使用鼠标向左或向右拖动分隔线,以调整左右节点的间距。

// SplitPaneTest.java
// ... find in the book's download area.

Listing 12-35Using SplitPane Controls

使用 CSS 样式化分割窗格

一个SplitPane co控件的默认 CSS 样式类名是split-paneSplitPane包含-fx-orientation CSS 属性,决定其方向。可能的值是horizontalvertical

SplitPane支持分别应用于水*和垂直SplitPaneshorizontalvertical CSS 伪类。分频器是SplitPanesplit-pane-divider子结构,?? 是StackPane。下面的代码为分隔线设置了蓝色背景,为水*方向的分隔线设置了 5px 的首选宽度SplitPane,为垂直方向的分隔线设置了 5px 的首选高度SplitPane:

.split-pane > .split-pane-divider {
    -fx-background-color: blue;
}

.split-pane:horizontal > .split-pane-divider {
    -fx-pref-width: 5;
}

.split-pane:vertical > .split-pane-divider {
    -fx-pref-height: 5;
}

split-pane-divider子结构包含一个抓取器子结构,它是一个StackPane。它的 CSS 样式类名是横向SplitPanehorizontal-grabber和纵向SplitPanevertical-grabber。抓取器显示在分隔器的中间。

了解滑块控件

Slider让用户通过沿轨迹滑动拇指(或旋钮)从数值范围中选择一个数值。滑块可以是水*的或垂直的。图 12-58 所示为水*滑块。

img/336502_2_En_12_Fig58_HTML.png

图 12-58

水*Slider控件及其零件

滑块具有确定有效可选值范围的最小值和最大值。滑块的拇指表示其当前值。您可以沿轨道滑动滑块来更改当前值。主要和次要刻度线显示值在轨道上的位置。您也可以显示刻度标签。也支持自定义标签。

下面的代码使用默认构造器创建一个Slider控件,该构造器将 0、100 和 0 分别设置为最小值、最大值和当前值。默认方向是水*的。

// Create a horizontal slider
Slider s1 = new Slider();

使用另一个构造器来指定最小值、最大值和当前值:

// Create a horizontal slider with the specified min, max, and value
double min = 0.0;
double max = 200.0;
double value = 50.0;
Slider s2 = new Slider(min, max, value);

一个Slider控件包含几个属性。我将按类别讨论它们。

orientation属性指定滑块的方向:

// Create a vertical slider
Slider vs = new Slider();
vs.setOrientation(Orientation.VERTICAL);

以下属性与当前值和值的范围相关:

  • min

  • max

  • value

  • valueChanging

  • snapToTicks

minmaxvalue属性是double属性,它们分别代表滑块的最小值、最大值和当前值。滑块的当前值可以通过在轨道上拖动拇指或使用setValue()方法来改变。以下代码片段创建了一个滑块,并将其minmaxvalue属性分别设置为 0、10 和 3:

Slider scoreSlider = new Slider();
scoreSlider.setMin(0.0);
scoreSlider.setMax(10.0);
scoreSlider.setValue(3.0);

通常,您希望在滑块的value属性改变时执行一个动作。您需要向value属性添加一个ChangeListener。以下语句使用 lambda 表达式将一个ChangeListener添加到scoreSlider控件,并在 value 属性更改时打印旧值和新值:

scoreSlider.valueProperty().addListener(
         (ObservableValue<? extends Number> prop, Number oldVal,
             Number newVal) -> {
    System.out.println("Changed from " + oldVal + " to " + newVal);
});

valueChanging属性是一个boolean属性。当用户按下拇指时,它被设置为 true,当松开拇指时,它被设置为 false。随着用户拖动拇指,该值不断变化,并且valueChanging属性为真。如果您只想在值更改时执行一次操作,此属性有助于避免重复操作。

snapToTicks属性是一个boolean属性,默认为 false。它指定滑块的value属性是否总是与刻度线对齐。如果设置为 false,该值可以是minmax范围内的任何值。

ChangeListener中使用valueChanging属性时要小心。对于用户看到的一个变化,侦听器可能会被调用多次。期望当valueChanging属性从 true 变为 false 时ChangeListener会得到通知,您将动作的主要逻辑包装在一个if语句中:

if (scoreSlider.isValueChanging()) {
        // Do not perform any action as the value changes
} else {
        // Perform the action as the value has been changed
}

snapToTicks属性设置为 true 时,逻辑工作正常。只有当snapToTicks属性设置为真时,当valueChanging属性从真变为假时,才会通知value属性的ChangeListener。因此,除非已经将snapToTicks属性也设置为 true,否则不要编写前面的逻辑。

Slider类的以下属性指定了刻度间距:

  • majorTickUnit

  • minorTickCount

  • blockIncrement

majorTickUnit属性是一个double属性。它指定两个主要刻度之间的距离单位。假设min属性被设置为 0,而majorTickUnit被设置为 10。滑块将在 0、10、20、30 等位置有主要刻度。此属性的值超出范围将禁用主刻度。该属性的默认值为 25。

minorTickCount属性是一个整数属性。它指定两个主要刻度之间的次要刻度数。属性的默认值为 3。

您可以使用按键来更改缩略图的位置,例如,在水*滑块中使用左右箭头键,在垂直滑块中使用上下箭头键。blockIncrement属性是一个double属性。它指定当拇指使用按键操作时滑块当前值的调整量。该属性的默认值为 10。

下列属性指定是否显示刻度线和刻度标签;默认情况下,它们被设置为 false:

  • showTickMarks

  • showTickLabels

labelFormatter属性是StringConverter<Double>类型的对象属性。默认情况下,它是null,滑块使用默认的StringConverter,显示主要刻度的数值。主要刻度的值被传递给toString()方法,该方法应该为该值返回一个自定义标签。以下代码片段创建了一个带有自定义主要刻度标签的滑块,如图 12-59 所示:

img/336502_2_En_12_Fig59_HTML.jpg

图 12-59

带有自定义主要刻度标签的滑块

Slider scoreSlider = new Slider();
scoreSlider.setShowTickLabels(true);
scoreSlider.setShowTickMarks(true);
scoreSlider.setMajorTickUnit(10);
scoreSlider.setMinorTickCount(3);
scoreSlider.setBlockIncrement(20);
scoreSlider.setSnapToTicks(true);

// Set a custom major tick formatter
scoreSlider.setLabelFormatter(new StringConverter<Double>() {
        @Override
        public String toString(Double value) {
                String label = "";
                if (value == 40) {
                        label = "F";
                } else if (value == 70) {
                        label = "C";
                } else if (value == 80) {
                        label = "B";
                } else if (value == 90) {
                        label = "A";
                }

                return label;
        }

        @Override
        public Double fromString(String string) {
                return null; // Not used
        }
});

清单 12-36 中的程序展示了如何使用Slider控件。它向一个窗口添加了一个Rectangle、一个Label和三个Slider控件。它给Slider增加了一个ChangeListenerSlider代表一种颜色的红色、绿色和蓝色成分。当您更改滑块的值时,会计算新颜色并将其设置为矩形的填充颜色。

// SliderTest.java
// ... find in the book's download area.

Listing 12-36Using the Slider Control

CSS 样式化滑块

一个Slider co控件的默认 CSS 样式类名是sliderSlider包含以下 CSS 属性;它们中的每一个都对应于其在Slider类中的 Java 属性:

  • -fx-orientation

  • -fx-show-tick-labels

  • -fx-show-tick-marks

  • -fx-major-tick-unit

  • -fx-minor-tick-count

  • -fx-snap-to-ticks

  • -fx-block-increment

Slider支持分别应用于水*和垂直滑块的horizontalvertical CSS 伪类。一个Slider控件包含三个可以样式化的子结构:

  • axis

  • track

  • thumb

axis底座是一个NumberAxis。它显示刻度线和刻度标签。以下代码将刻度标签颜色设置为蓝色,主刻度长度设置为 15px,次刻度长度设置为 5px,主刻度颜色设置为红色,次刻度颜色设置为绿色:

.slider > .axis {
    -fx-tick-label-fill: blue;
    -fx-tick-length: 15px;
    -fx-minor-tick-length: 5px
}

.slider > .axis > .axis-tick-mark {
    -fx-stroke: red;
}

.slider > .axis > .axis-minor-tick-mark {
    -fx-stroke: green;
}

track底座是一个StackPane。下面的代码将track的背景色改为红色:

.slider > .track {
    -fx-background-color: red;
}

thumb底座是一个StackPane。拇指看起来是圆形的,因为它有一个背景半径。如果删除背景半径,它将看起来是矩形的,如下面的代码所示:

.slider .thumb {
    -fx-background-radius: 0;
}

您可以通过将thumb子结构的背景设置为如下所示的图像来制作类似于拇指的图像(假设thumb.jpg图像文件与包含该样式的 CSS 文件存在于同一目录中):

.slider .thumb {
        -fx-background-image: url("thumb.jpg");
}

您可以使用-fx-shape CSS 属性赋予缩略图任何形状。下面的代码给出了一个三角形的缩略图。对于水*滑块,三角形是倒置的,对于垂直滑块,三角形指向右边。图 12-60 显示了带有拇指的水*滑块。

img/336502_2_En_12_Fig60_HTML.jpg

图 12-60

带有倒三角形滑块的滑块

/* An inverted triangle */
.slider > .thumb {
        -fx-shape: "M0, 0L10, 0L5, 10 Z";
}

/* A triangle pointing to the right, only if orientation is vertical */
.slider:vertical > .thumb {
        -fx-shape: "M0, 0L10, 5L0, 10 Z";
}

下面的代码给出了一个放置在矩形旁边的三角形。对于水*滑块,三角形是倒置的,对于垂直滑块,三角形指向右边。图 12-61 显示了带有拇指的水*滑块。

img/336502_2_En_12_Fig61_HTML.jpg

图 12-61

矩形下方带有倒三角形拇指的滑块

/* An inverted triangle below a rectangle*/
.slider > .thumb {
        -fx-shape: "M0, 0L10, 0L10, 5L5, 10L0, 5 Z";
}

/* A triangle pointing to the right by the right side of a rectangle */
.slider:vertical > .thumb {
        -fx-shape: "M0, 0L5, 0L10, 5L5, 10L0, 10 Z";
}

理解菜单

菜单用于以紧凑的形式向用户提供可操作项目的列表。您还可以使用一组按钮提供相同的项目列表,其中每个按钮代表一个可操作的项目。使用哪一个是你的偏好问题:一个菜单或一组按钮。

使用菜单有一个明显的优点。与一组按钮相比,通过将一组项目折叠(或嵌套)在另一个项目下,它占用的屏幕空间要少得多。例如,如果您使用了文件编辑器,诸如“新建”、“打开”、“保存”和“打印”等菜单项会嵌套在顶级文件菜单下。用户需要单击文件菜单来查看其下可用的项目列表。通常,在一组按钮的情况下,所有项目对用户都是可见的,并且用户很容易知道哪些动作是可用的。因此,当你决定使用菜单或按钮时,在空间和可用性之间没有什么权衡。通常,菜单栏显示在窗口的顶部。

Tip

还有另外一种菜单,叫做上下文菜单或者弹出菜单,按需显示。我将在下一节讨论上下文菜单。

菜单由几部分组成。图 12-62 显示了另存为子菜单展开时的菜单及其组成部分。菜单栏是包含菜单的菜单的最顶端部分。菜单栏始终可见。文件、编辑、选项和帮助是如图 12-62 所示的菜单项。菜单包含菜单项和子菜单。在图 12-62 中,文件菜单包含四个菜单项:新建、打开、保存、退出;它包含两个分隔符菜单项和一个另存为子菜单。“另存为”子菜单包含两个菜单项:文本和 PDF。菜单项是可操作的项目。分隔符菜单项有一条水*线,将菜单中的一组相关菜单项与另一组菜单项分隔开来。通常,菜单代表一类项目。

img/336502_2_En_12_Fig62_HTML.png

图 12-62

带有菜单栏、菜单、子菜单、分隔符和菜单项的菜单

使用菜单是一个多步骤的过程。以下部分详细描述了这些步骤。以下是步骤摘要:

  1. 创建一个菜单栏并将其添加到一个容器中。

  2. 创建菜单并将其添加到菜单栏。

  3. 创建菜单项并将其添加到菜单中。

  4. ActionEvent处理程序添加到菜单项中,以便在菜单项被单击时执行操作。

使用菜单栏

菜单栏是作为菜单容器的水*栏。MenuBar类的一个实例代表一个菜单栏。您可以使用默认的构造器创建一个MenuBar:

MenuBar menuBar = new MenuBar();

MenuBar是控件。通常,它被添加到窗口的顶部。如果你使用一个BorderPane作为窗口中场景的根,顶部区域通常是一个MenuBar的位置:

// Add the MenuBar to the top region
BorderPane root = new BorderPane();
root.setBottom(menuBar);

MenuBar类包含一个useSystemMenuBar属性,它属于boolean类型。默认情况下,它被设置为 false。当设置为 true 时,如果*台支持,它将使用系统菜单栏。例如,Mac 支持系统菜单栏。如果在 Mac 上将该属性设置为 true,MenuBar将使用系统菜单栏显示其项目:

// Let the MenuBar use system menu bar
menuBar.setUseSystemMenuBar(true);

一个MenuBar本身不占用任何空间,除非你给它添加菜单。它的大小是根据它所包含的菜单的细节来计算的。一个MenuBar将它所有的菜单存储在一个MenuObservableList中,其引用由它的getMenus()方法返回:

// Add some menus to the MenuBar
Menu fileMenu = new Menu("File");
Menu editMenu = new Menu("Edit");
menuBar.getMenus().addAll(fileMenu, editMenu);

使用菜单

菜单包含可操作项目的列表,可根据需要显示,例如,通过单击它。当用户选择一个项目或将鼠标指针移出列表时,菜单项列表隐藏。菜单通常作为子菜单添加到菜单栏或其他菜单中。

Menu类的一个实例代表一个菜单。菜单显示文本和图形。使用默认构造器创建一个空菜单,然后设置文本和图形:

// Create a Menu with an empty string text and no graphic
Menu aMenu = new Menu();

// Set the text and graphic to the Menu
aMenu.setText("Text");
aMenu.setGraphic(new ImageView(new Image("image.jpg")));

您可以使用其他构造器创建包含文本或文本和图形的菜单:

// Create a File Menu
Menu fileMenu1 = new Menu("File");

// Create a File Menu
Menu fileMenu2 = new Menu("File", new ImageView(new Image("file.jpg")));

Menu类继承自MenuItem类,后者继承自Object类。Menu不是节点,因此不能直接添加到场景图中。你需要把它添加到一个MenuBar。使用getMenus()方法获取MenuBarObservableList<Menu>,并将Menu类的实例添加到列表中。下面的代码片段向一个MenuBar添加了四个Menu实例:

Menu fileMenu = new Menu("File");
Menu editMenu = new Menu("Edit");
Menu optionsMenu = new Menu("Options");
Menu helpMenu = new Menu("Help");

// Add menus to a menu bar
MenuBar menuBar = new MenuBar();
menuBar.getMenus().addAll(fileMenu, editMenu, optionsMenu, helpMenu);

单击菜单时,通常会显示其菜单项列表,但不会执行任何操作。Menu类包含以下属性,当它的选项列表分别显示、显示、隐藏和隐藏时,可以设置这些属性进行处理:

  • onShowing

  • onShown

  • onHiding

  • onHidden

  • showing

在显示菜单的菜单项之前,调用onShowing事件处理程序。显示菜单项后,调用onShown事件处理程序。onHidingonHidden事件处理程序分别对应于onShowingonShown事件处理程序。

通常,您会添加一个onShowing事件处理程序,它会根据某些标准启用或禁用菜单项。例如,假设您有一个带有剪切、复制和粘贴菜单项的编辑菜单。在onShowing事件处理程序中,您可以根据焦点是否在文本输入控件中、控件是否被启用或者控件是否有选择来启用或禁用这些菜单项:

editMenu.setOnAction(e -> {/* Enable/disable menu items here */});

Tip

用户在使用 GUI 应用程序时不喜欢惊喜。为了获得更好的用户体验,您应该禁用菜单项,而不是在它们不适用时使它们不可见。使它们不可见会改变其他项目的位置,用户必须重新定位它们。

showing属性是只读的boolean属性。当菜单中的项目显示时,它被设置为 true。当它们被隐藏时,它被设置为 false。

清单 12-37 中的程序将所有这些放在一起。它创建了四个菜单,一个菜单栏,将菜单添加到菜单栏,并将菜单栏添加到一个BorderPane的顶部区域。图 12-63 显示了窗口中的菜单栏。但是你还没有看到任何令人兴奋的菜单!您需要在菜单中添加菜单项来体验一些刺激。

img/336502_2_En_12_Fig63_HTML.jpg

图 12-63

有四个菜单的菜单栏

// MenuTest.java
package com.jdojo.control;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuBar;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;

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

        @Override
        public void start(Stage stage) {
                // Create some menus
                Menu fileMenu = new Menu("File");
                Menu editMenu = new Menu("Edit");
                Menu optionsMenu = new Menu("Options");
                Menu helpMenu = new Menu("Help");

                // Add menus to a menu bar
                MenuBar menuBar = new MenuBar();
                menuBar.getMenus().addAll(
                         fileMenu, editMenu, optionsMenu, helpMenu);

                BorderPane root = new BorderPane();
                root.setTop(menuBar);
                root.setStyle("""
                         -fx-padding: 10;
                   -fx-border-style: solid inside;
                   -fx-border-width: 2;
                   -fx-border-insets: 5;
                   -fx-border-radius: 5;
                   -fx-border-color: blue;""");

                Scene scene = new Scene(root);
                stage.setScene(scene);
                stage.setTitle("Using Menus");
                stage.show();
        }
}

Listing 12-37Creating a Menu Bar and Adding Menus to It

使用菜单项

菜单项是菜单中可操作的项目。与菜单项相关联的动作由鼠标或按键来执行。菜单项可以使用 CSS 样式。

MenuItem类的一个实例代表一个菜单项。MenuItem类不是一个节点。它继承自Object类,因此不能直接添加到场景图中。您需要将它添加到菜单中。

您可以将几种类型的菜单项添加到菜单中。图 12-64 显示了代表特定类型菜单项的MenuItem类及其子类的类图。

img/336502_2_En_12_Fig64_HTML.jpg

图 12-64

MenuItem类及其子类的类图

您可以使用以下类型的菜单项:

  • 一个可操作选项的MenuItem

  • A RadioMenuItem为一组互斥选项

  • 一个CheckMenuItem用于切换选项

  • 一个Menu,当用作菜单项时,作为一个保存菜单项列表的子菜单

  • 一个CustomMenuItem用于将任意节点用作菜单项

  • 一个SeparatorMenuItem,它是一个CustomMenuItem,用于将分隔符显示为菜单项

我将在接下来的小节中详细讨论所有的菜单项类型。

使用菜单项

一个MenuItem代表一个可操作的选项。当它被点击时,注册的ActionEvent处理程序被调用。下面的代码片段创建了一个退出MenuItem,并添加了一个退出应用程序的ActionEvent处理程序:

MenuItem exitItem = new MenuItem("Exit");
exitItem.setOnAction(e -> Platform.exit());

一个MenuItem被添加到菜单中。菜单将它的MenuItem的引用存储在一个ObservableList<MenuItem>中,其引用可以使用getItems()方法获得:

Menu fileMenu = new Menu("File");
fileMenu.getItems().add(exitItem);

MenuItem类包含以下适用于所有类型菜单项的属性:

  • text

  • graphic

  • disable

  • visible

  • accelerator

  • mnemonicParsing

  • onAction

  • onMenuValidation

  • parentMenu

  • parentPopup

  • style

  • id

textgraphic属性分别是菜单项的文本和图形,属于StringNode类型。disablevisible属性是boolean属性。它们指定菜单项是否被禁用和可见。accelerator属性是KeyCombination类型的对象属性,它指定了一个组合键,该组合键可用于在一次击键中执行与菜单项相关联的动作。下面的代码片段创建了一个Rectangle菜单项,并将其快捷键设置为 Alt + R,菜单项的快捷键显示在它的旁边,如图 12-65 所示,因此用户可以通过查看菜单项来了解它。用户可以通过按 Alt + R 直接激活矩形菜单项。

img/336502_2_En_12_Fig65_HTML.jpg

图 12-65

带有快捷键 Alt + R 的菜单项

MenuItem rectItem = new MenuItem("Rectangle");
KeyCombination kr = new KeyCodeCombination(KeyCode.R, KeyCombination.ALT_DOWN);
rectItem.setAccelerator(kr);

mnemonicParsing属性是一个boolean属性。它启用或禁用文本分析来检测助记符。默认情况下,对于菜单项,它设置为 true。如果设置为 true,则分析菜单项文本中的下划线字符。第一个下划线后面的字符被添加为菜单项的助记符。在 Windows 上按 Alt 键会突出显示所有菜单项的助记键。通常,助记符以下划线字体显示。按助记符键激活菜单项。

// Create a menu item with x as its mnemonic character
MenuItem exitItem = new MenuItem("E_xit");

onAction属性是一个ActionEvent处理程序,当菜单项被激活时调用,例如,通过用鼠标单击它或按下它的快捷键:

// Close the application when the Exit menu item is activated
exitItem.setOnAction(e -> Platform.exit());

onMenuValidation属性是一个事件处理程序,当使用其加速器访问MenuItem时,或者当调用其菜单(父菜单)的onShowing事件处理程序时,会调用该事件处理程序。对于菜单,当显示菜单项时,将调用此处理程序。

parentMenu属性是Menu类型的只读对象属性。它是包含菜单项的Menu的引用。使用这个属性和由Menu类的getItems()方法返回的项目列表,您可以从上到下导航菜单树,反之亦然。

parentPopup属性是ContextMenu类型的只读对象属性。它是菜单项出现的ContextMenu的引用。正常菜单中出现的菜单项为null

包含 style 和 ID 属性是为了支持使用 CSS 的样式。它们代表 CSS 样式和 ID。

使用单选按钮

一个RadioMenuItem代表一个互斥选项。通常,您将多个RadioMenuItem添加到一个ToggleGroup,因此只有一个项目被选中。RadioMenuItem选中时显示复选标记。下面的代码片段创建了三个RadioMenuItem实例,并将它们添加到一个ToggleGroup。最后,它们都被添加到一个文件Menu中。通常情况下,组中的RadioMenuItem被默认选中。图 12-66 显示了一组RadioMenuItem s:选择矩形时一次,选择圆形时一次。

img/336502_2_En_12_Fig66_HTML.jpg

图 12-66

RadioMenuItems在行动

// Create three RadioMenuItems
RadioMenuItem rectItem = new RadioMenuItem("Rectangle");
RadioMenuItem circleItem = new RadioMenuItem("Circle");
RadioMenuItem ellipseItem = new RadioMenuItem("Ellipse");

// Select the Rantangle option by default
rectItem.setSelected(true);

// Add them to a ToggleGroup to make them mutually exclusive
ToggleGroup shapeGroup = new ToggleGroup();
shapeGroup.getToggles().addAll(rectItem, circleItem, ellipseItem);

// Add RadioMenuItems to a File Menu
Menu fileMenu = new Menu("File");
fileMenu.getItems().addAll(rectItem, circleItem, ellipseItem);

如果您想在选择RadioMenuItem时执行一个动作,那么在它上面添加一个ActionEvent处理程序。下面的代码片段为每个RadioMenuItem添加了一个ActionEvent处理程序,它调用一个draw()方法:

rectItem.setOnAction(e -> draw());
circleItem.setOnAction(e -> draw());
ellipseItem.setOnAction(e -> draw());

使用检查菜单项

使用CheckMenuItem代表一个布尔菜单项,可以在选中和未选中状态之间切换。假设您有一个绘制形状的应用程序。可以有一个 Draw Stroke 菜单项作为CheckMenuItem。选中它时,将为该形状绘制一个笔划。否则,该形状将没有描边,如下面的代码所示。使用一个ActionEvent处理器,当CheckMenuItem的状态被切换时得到通知。

CheckMenuItem strokeItem = new CheckMenuItem("Draw Stroke");
strokeItem.setOnAction( e -> drawStroke());

选择CheckMenuItem时,旁边会显示一个复选标记。

使用子菜单项

注意,Menu类是从MenuItem类继承而来的。这使得使用Menu代替MenuItem成为可能。使用Menu作为菜单项创建子菜单。当鼠标悬停在子菜单上时,会显示其选项列表。

以下代码片段创建了一个MenuBar,添加了一个文件菜单,向文件菜单添加了新的和打开的MenuItem和一个另存为子菜单,并向另存为子菜单添加了文本和 PDF 菜单项。产生如图 12-67 所示的菜单。

img/336502_2_En_12_Fig67_HTML.jpg

图 12-67

用作子菜单的菜单

MenuBar menuBar = new MenuBar();
Menu fileMenu = new Menu("File");
menuBar.getMenus().addAll(fileMenu);

MenuItem newItem = new MenuItem("New");
MenuItem openItem = new MenuItem("Open");
Menu saveAsSubMenu = new Menu("Save As");

// Add menu items to the File menu
fileMenu.getItems().addAll(newItem, openItem, saveAsSubMenu);

MenuItem textItem = new MenuItem("Text");
MenuItem pdfItem = new MenuItem("PDF");
saveAsSubMenu.getItems().addAll(textItem, pdfItem);

通常,不需要为子菜单添加ActionEvent处理程序。相反,您可以为onShowing属性设置一个事件处理程序,在显示子菜单的项目列表之前调用该事件处理程序。事件处理程序用于启用或禁用菜单项。

使用自定义菜单项

是一个简单而强大的菜单项类型。它为设计菜单项的各种创意打开了大门。它允许您使用任何节点。例如,您可以使用一个SliderTextFieldHBox作为菜单项。CustomMenuItem类包含两个属性:

  • content

  • hideOnClick

content属性是Node类型的对象属性。它的值是要用作菜单项的节点。

单击菜单项时,所有可见的菜单都将隐藏,只有菜单栏中的顶级菜单保持可见。当您使用包含控件的自定义菜单项时,您不希望在用户单击它时隐藏菜单,因为用户需要与菜单项交互,例如,输入或选择一些数据。hideOnClick属性是一个boolean属性,允许您控制这种行为。默认情况下,它设置为 true,这意味着单击自定义菜单会隐藏所有显示的菜单。

CustomMenuItem类提供了几个构造器。默认构造器创建一个自定义菜单项,将content属性设置为null并将hideOnClick属性设置为 true,如以下代码所示:

// Create a Slider control
Slider slider = new Slider(1, 10, 1);

// Create a custom menu item and set its content and hideOnClick properties
CustomMenuItem cmi1 = new CustomMenuItem();
cmi1.setContent(slider);
cmi1.setHideOnClick(false);

// Create a custom menu item with a Slider content and
// set the hideOnClick property to false
CustomMenuItem cmi2 = new CustomMenuItem(slider);
cmi1.setHideOnClick(false);

// Create a custom menu item with a Slider content and false hideOnClick
CustomMenuItem cmi2 = new CustomMenuItem(slider, false);

下面的代码片段产生如图 12-68 所示的菜单。菜单项之一是一个CustomMenuItem,它使用一个slider作为它的内容:

img/336502_2_En_12_Fig68_HTML.jpg

图 12-68

作为自定义菜单项的滑块

CheckMenuItem strokeItem = new CheckMenuItem("Draw Stroke");
strokeItem.setSelected(true);

Slider strokeWidthSlider = new Slider(1, 10, 1);
strokeWidthSlider.setShowTickLabels(true);
strokeWidthSlider.setShowTickMarks(true);
strokeWidthSlider.setMajorTickUnit(2);
CustomMenuItem strokeWidthItem = new CustomMenuItem(strokeWidthSlider, false);

Menu optionsMenu = new Menu("Options");
optionsMenu.getItems().addAll(strokeItem, strokeWidthItem);

MenuBar menuBar = new MenuBar();
menuBar.getMenus().add(optionsMenu);

使用分隔符菜单项

关于SeparatorMenuItem没有什么特别要讨论的。它继承自CustomMenuItem。它使用一个水*的Separator控件作为它的content,并将hideOnClick设置为假。它用于分隔属于不同组的菜单项,如下面的代码所示。它提供了一个默认的构造器:

// Create a separator menu item
SeparatorMenuItem smi = SeparatorMenuItem();

将菜单的所有部分放在一起

理解菜单的各个部分很容易。然而,在代码中使用它们是很棘手的,因为您必须单独创建所有部分,向它们添加侦听器,然后组装它们。

清单 12-38 中的程序使用菜单创建一个形状绘制应用程序。它使用所有类型的菜单项。程序显示一个窗口,窗口的根是一个BorderPane。顶部区域包含一个菜单,中间区域包含一个在其上绘制形状的画布。

运行应用程序并使用文件菜单绘制不同类型的形状;单击“清除”菜单项将清除画布。单击退出菜单项关闭应用程序。

使用选项菜单绘制或不绘制笔划并设置笔划宽度。请注意,滑块被用作选项菜单下的自定义菜单项。当您调整滑块值时,所绘制形状的描边宽度也会相应调整。绘制笔划菜单项是一个CheckMenuItem。取消选择它时,滑块菜单项被停用,并且形状不使用笔画。

// MenuItemTest.java
// ... find in the book's download area.

Listing 12-38Using Menus in a Shape Drawing Application

使用 CSS 设计菜单样式

使用菜单涉及几个组件。表 12-6 列出了菜单相关组件的默认 CSS 样式类名。

表 12-6

菜单相关组件的 CSS 默认样式类名

|

菜单组件

|

样式类名

MenuBar menu-bar
Menu menu
MenuItem menu-item
RadioMenuItem radio-menu-item
CheckMenuItem check-menu-item
CustomMenuItem custom-menu-item
SeparatorMenuItem separator-menu-item

MenuBar支持一个-fx-use-system-menu-bar属性,默认设置为 false。它指示菜单栏是否使用系统菜单。它包含一个保存菜单栏菜单的menu子结构。Menu支持显示 CSS 伪类,在菜单显示时应用。RadioMenuItemCheckMenuItem支持一个selected CSS 伪类,当菜单项被选中时应用。

您可以设置菜单的几个组件的样式。样本样式请参考modena.css文件。

了解上下文菜单控件

ContextMenu是一个弹出控件,根据请求显示菜单项列表。它也被称为上下文弹出菜单。默认情况下,它是隐藏的。用户必须提出请求,通常是通过右击鼠标按钮来显示它。一旦做出选择,它将被隐藏。用户可以通过按 Esc 键或在上下文菜单边界外单击来关闭上下文菜单。

上下文菜单存在可用性问题。用户很难知道它的存在。通常,非技术用户不习惯点击鼠标右键并进行选择。对于这些用户,您可以使用工具栏或按钮来呈现相同的选项。有时,屏幕上会显示一条文本消息,说明用户需要右键单击才能查看或显示上下文菜单。

ContextMenu类的一个对象代表一个上下文菜单。它将菜单项的引用存储在一个ObservableList<MenuItem>中。getItems()方法返回可观察列表的引用。

您将在下面的示例中使用以下三个菜单项。注意,上下文菜单中的菜单项可以是MenuItem类或其子类的对象。有关菜单项类型的完整列表,请参考“理解菜单”一节。

MenuItem rectItem = new MenuItem("Rectangle");
MenuItem circleItem = new MenuItem("Circle");
MenuItem ellipseItem = new MenuItem("Ellipse");

ContextMenu类的默认构造器创建一个空菜单。您需要稍后添加菜单项:

ContextMenu ctxMenu = new ContextMenu();
ctxMenu.getItems().addAll(rectItem, circleItem, ellipseItem);

您可以使用另一个构造器创建包含菜单项初始列表的上下文菜单:

ContextMenu ctxMenu = new ContextMenu(rectItem, circleItem, ellipseItem);

通常,为控件提供上下文菜单以访问其常用功能,例如,文本输入控件的剪切、复制和粘贴功能。一些控件有默认的上下文菜单。control 类使显示上下文菜单变得容易。它有一个contextMenu属性。您需要将此属性设置为控件的上下文菜单引用。以下代码片段设置了一个TextField控件的上下文菜单:

ContextMenu ctxMenu = ...
TextField nameFld = new TextField();
nameFld.setContextMenu(ctxMenu);

当您右键单击TextField时,将显示您的上下文菜单,而不是默认菜单。

Tip

激活空的上下文菜单不会显示任何内容。如果您想禁用控件的默认上下文菜单,请将其contextMenu属性设置为空的ContextMenu

不是控件的节点没有contextMenu属性。您需要使用ContextMenu类的show()方法来显示这些节点的上下文菜单。show()方法让您可以完全控制上下文菜单的显示位置。如果您想微调上下文菜单的位置,也可以将它用于控件。show()方法被重载:

void show(Node anchor, double screenX, double screenY)
void show(Node anchor, Side side, double dx, double dy)

第一个版本使用相对于屏幕的 x 和 y 坐标来显示上下文菜单的节点。通常,您会在鼠标点击事件中显示一个上下文菜单,其中MouseEvent对象通过getScreenX()getScreenY()方法向您提供鼠标指针相对于屏幕的坐标。

以下代码片段显示了相对于屏幕坐标系位于(100,100)的画布的上下文菜单:

Canvas canvas = ...
ctxMenu.show(canvas, 100, 100);

第二个版本允许您微调上下文菜单相对于指定的anchor节点的位置。参数side指定了上下文菜单显示在anchor节点的哪一侧。可能的值是Side枚举的常量之一— TOPRIGHTBOTTOMLEFTdxdy参数分别指定相对于anchor节点坐标系的 x 和 y 坐标。这个版本的show()方法需要更多一点的解释。

side参数具有移动anchor节点的 x 轴和 y 轴的作用。轴移动后应用dxdy参数。请注意,当调用此版本的方法时,移动轴只是为了计算上下文菜单的位置。它们不会永久移动,并且anchor节点的位置根本不会改变。图 12-69 显示了side参数值的锚节点及其 x 轴和 y 轴。dxdy参数是该点相对于节点移动后的 x 轴和 y 轴的 x 和 y 坐标。

img/336502_2_En_12_Fig69_HTML.png

图 12-69

使用侧面参数值移动锚节点的 x 轴和 y 轴

注意,side参数的LEFTRIGHT值是基于anchor节点的节点方向来解释的。对于RIGHT_TO_LEFT的节点方向,LEFT值表示节点的右侧。

当您为side参数指定TOPLEFTnull时,dxdy参数相对于节点的原始 x 轴和 y 轴进行测量。当您为side参数指定BOTTOM时,节点的底部成为新的 x 轴,而 y 轴保持不变。当您为side参数指定RIGHT时,节点的右侧成为新的 y 轴,而 x 轴保持不变。

下面对show()方法的调用在anchor节点的左上角显示了一个上下文菜单。side参数的Side.LEFTnull的值将在同一位置显示上下文菜单:

ctxMenu.show(anchor, Side.TOP, 0, 0);

下面对show()方法的调用在anchor节点的左下角显示了一个上下文菜单:

ctxMenu.show(anchor, Side.BOTTOM, 0, 0);

dxdy的值可以是负值。下面对show()方法的调用在anchor节点的左上角显示了一个上下文菜单 10px:

ctxMenu.show(myAnchor, Side.TOP, 0, -10);

如果上下文菜单显示的话,ContextMenu类的hide()方法会隐藏它。通常,当您选择菜单项时,上下文菜单是隐藏的。当上下文菜单使用自定义菜单项且hideOnClick属性设置为 true 时,您需要使用hide()方法。

通常,一个ActionEvent处理程序被添加到上下文菜单的菜单项中。ContextMenu类包含一个onAction属性,这是一个ActionEvent处理程序。如果设置了ContextMenuActionEvent处理程序,则每次激活菜单项时都会调用该处理程序。当菜单项被激活时,您可以使用此ActionEvent来执行后续操作。

清单 12-39 中的程序展示了如何使用上下文菜单。它显示一个Label和一个Canvas。右键单击画布时,会显示一个包含三个菜单项的上下文菜单——矩形、圆形和椭圆形。从菜单项中选择一个形状,在画布上绘制该形状。单击鼠标指针时,将显示上下文菜单。

// ContextMenuTest.java
// ... find in the book's download area.

Listing 12-39Using the ContextMenu Control

使用 CSS 的样式化上下文菜单

一个ContextMenu的默认 CSS 样式类名是context-menu。请参考modena.css文件中定制上下文菜单外观的样式示例。默认情况下,上下文菜单使用投影效果。以下样式将字体大小设置为 8pt,并取消默认效果:

.context-menu {
        -fx-font-size: 8pt;
        -fx-effect: null;
}

了解工具栏控件

ToolBar用于显示一组节点,在屏幕上提供常用的动作项。通常情况下,ToolBar控件包含常用的项目,这些项目也可以通过菜单和上下文菜单获得。

一个ToolBar控件可以保存许多类型的节点。ToolBar中最常用的节点是按钮和切换按钮。Separator s 用于将一组按钮与其他按钮分开。通常,按钮通过使用小图标来保持较小,最好是 16px 乘 16px 的大小。

如果工具栏中的项目溢出,则会出现一个溢出按钮,允许用户导航到隐藏的项目。工具栏可以有水*或垂直方向。水*工具栏将项目水*排列在一行中。垂直工具栏将项目排列在一列中。图 12-70 显示了两个工具栏:一个没有溢出,一个有溢出。有溢出的显示一个溢出按钮(> >)。单击溢出按钮时,会显示隐藏的工具栏项目以供选择。

img/336502_2_En_12_Fig70_HTML.png

图 12-70

带有三个按钮的水*工具栏

您将在本章的示例中使用以下四个ToolBar项目:

Button rectBtn = new Button("", new Rectangle(0, 0, 16, 16));
Button circleBtn = new Button("", new Circle(0, 0, 8));
Button ellipseBtn = new Button("", new Ellipse(8, 8, 8, 6));
Button exitBtn = new Button("Exit");

一个ToolBar控件将项目的引用存储在一个ObservableList<Node>中。使用getItems()方法得到可观察列表的引用。

ToolBar类的默认构造器创建一个空工具栏:

ToolBar toolBar = new ToolBar();
toolBar.getItems().addAll(circleBtn, ellipseBtn, new Separator(), exitBtn);

ToolBar类提供了另一个允许您添加项目的构造器:

ToolBar toolBar = new ToolBar(
        rectBtn, circleBtn, ellipseBtn,
        new Separator(),
        exitBtn);

ToolBar类的orientation属性指定了它的方向:水*或垂直。默认情况下,工具栏使用水*方向。以下代码将其设置为垂直:

// Create a ToolBar and set its orientation to VERTICAL
ToolBar toolBar = new ToolBar();
toolBar.setOrientation(Orientation.VERTICAL);

Tip

默认 CSS 会自动调整工具栏中分隔符的方向。为工具栏中的项目提供工具提示是一种很好的做法,因为它们很小,通常不使用文本内容。

清单 12-40 中的程序展示了如何创建和使用ToolBar控件。它创建了一个工具栏并添加了四个项目。当您点按带有形状的项目之一时,它会在画布上绘制该形状。Exit 项关闭应用程序。

// ToolBarTest.java
// ... find in the book's download area.

Listing 12-40Using the ToolBar Control

用 CSS 设计工具栏的样式

一个ToolBar的默认 CSS 样式类名是tool-bar。它包含一个-fx-orientation CSS 属性,用可能的值水*垂直指定其方向。它支持分别在水*和垂直方向应用的horizontalvertical CSS 伪类。

工具栏使用容器来排列项目。容器是水*方向的HBox和垂直方向的VBox。容器的 CSS 样式类名是container。您可以使用容器的HBoxVBox的所有 CSS 属性。CSS 属性指定了容器中两个相邻项目之间的间距。您可以为工具栏或容器设置该属性。以下两种样式在水*工具栏上具有相同的效果:

.tool-bar  {
        -fx-spacing: 2;
}

.tool-bar > .container  {
        -fx-spacing: 2;
}

工具栏包含一个tool-bar-overflow-button子结构来表示溢出按钮。是一辆StackPanetool-bar-overflow-button包含一个arrow子结构来表示溢出按钮中的箭头。这也是一个StackPane

理解选项卡窗格选项卡

窗口可能没有足够的空间在一个页面视图中显示所有的信息。JavaFX 提供了几个控件来将大量内容分解成多个页面,例如,AccordionPagination控件。TabPaneTab让你在页面上更好地呈现信息。一个Tab代表一个页面,一个TabPane包含了Tab

A Tab不是控件。Tab类的一个实例代表一个TabTab类继承自Object类。然而,Tab像控件一样支持一些特性,例如,它们可以被禁用,使用 CSS 样式化,并且可以有上下文菜单和工具提示。

一个Tab由标题和内容组成。标题由文本、可选图形和关闭选项卡的可选关闭按钮组成。内容由控件组成。通常,控件被添加到一个布局窗格中,该窗格作为其内容被添加到Tab中。

通常,TabPaneTab的标题是可见的。内容区由所有Tabs共享。您需要通过点击标题来选择一个Tab,以查看其内容。在TabPane中,一次只能选择一个选项卡。如果所有选项卡的标题都不可见,则自动显示一个控制按钮,帮助用户选择不可见的选项卡。

TabPane中的Tab可以位于TabPane的顶部、右侧、底部或左侧。默认情况下,它们位于顶部。

图 12-71 显示了一个窗口的两个实例。该窗口包含一个带有两个选项卡的TabPane。在一种情况下,选择常规选项卡,而在另一种情况下,选择地址选项卡。

img/336502_2_En_12_Fig71_HTML.jpg

图 12-71

一个带有TabPane的窗口,其中包含两个选项卡

一个TabPane分为两部分:头区内容区。标题区域显示选项卡的标题;内容区域显示选定选项卡的内容。标题区分为以下几个部分:

  • 标题区域

  • 选项卡标题背景

  • 控制按钮选项卡

  • 标签区域

图 12-72 显示了 a TabPane的部分表头区域。标题区域是整个标题区域。标签标题背景是标签标题所占据的区域。控制按钮选项卡包含当TabPane的宽度不能显示所有选项卡时显示的控制按钮。“控制按钮”选项卡允许您选择当前不可见的选项卡。选项卡区域包含一个Label和一个关闭按钮(选项卡标签旁边的 X 图标)。Label显示标签的文本和图标。“关闭”按钮用于关闭选定的选项卡。

img/336502_2_En_12_Fig72_HTML.png

图 12-72

TabPane报头的不同部分

创建选项卡

您可以使用带有空标题的Tab类的默认构造器创建一个选项卡:

Tab tab1 = new Tab();

使用setText()方法设置标签的标题文本:

tab1.setText("General");

另一个构造器将标题文本作为参数:

Tab tab2 = new Tab("General");

设置选项卡的标题和内容

Tab类包含以下属性,允许您设置标题和内容:

  • text

  • graphic

  • closable

  • content

textgraphicclosable属性指定了标签标题栏中显示的内容。text属性指定一个字符串作为标题文本。graphic属性指定一个节点作为title图标。注意,graphic 属性的类型是Node,因此您可以使用任何节点作为图形。通常,小图标被设置为图形。可以在构造器中设置text属性,或者使用setText()方法。下面的代码片段创建了一个带有文本的选项卡,并将一个图像设置为其图形(假设文件resources/picture/address_icon.png包含在包中):

// Create an ImageView for graphic
String imagePath = "resources/picture/address_icon.png";
URL imageUrl = getClass().getClassLoader().getResource(imagePath);
Image img = new Image(imageUrl.toExternalForm());
ImageView icon = new ImageView(img);

// Create a Tab with "Address" text
Tab addressTab = new Tab("Address");

// Set the graphic
addressTab.setGraphic(icon);

closable属性是一个boolean属性,指定选项卡是否可以关闭。如果设置为 false,则无法关闭选项卡。关闭页签也受TabPane的关闭页签策略控制。如果closable属性设置为 false,则无论TabPane的标签关闭策略如何,用户都不能关闭标签。当我稍后讨论TabPane时,您将了解到标签关闭策略。

content属性是一个指定选项卡内容的节点。当选项卡被选中时,选项卡的内容可见。通常,带有控件的布局窗格被设置为选项卡的内容。以下代码片段创建了一个GridPane,添加了一些控件,并将GridPane设置为选项卡的内容:

// Create a GridPane layout pane with some controls
GridPane grid = new GridPane();
grid.addRow(0, new Label("Street:"), streetFld);
grid.addRow(1, new Label("City:"), cityFld);
grid.addRow(2, new Label("State:"), stateFld);
grid.addRow(3, new Label("ZIP:"), zipFld);

Tab addressTab = new Tab("Address");
addressTab.setContent(grid); // Set the content

创建标签窗格

TabPane类只提供了一个构造器——默认构造器。当您创建TabPane时,它没有选项卡:

TabPane tabPane = new TabPane();

将选项卡添加到选项卡窗格

一个TabPane在一个ObservableList<Tab>中存储其标签的引用。TabPane类的getTabs()方法返回可观察列表的引用。要给TabPane添加标签,你需要把它添加到可观察列表。下面的代码片段向一个TabPane添加了两个选项卡:

Tab generalTab = new Tab("General");
Tab addressTab = new Tab("Address");
...
TabPane tabPane = new TabPane();

// Add the two Tabs to the TabPane
tabPane.getTabs().addAll(generalTab, addressTab);

当一个标签不应该是TabPane的一部分时,你需要把它从可观察列表中移除。TabPane将自动更新其视图:

// Remove the Address tab
tabPane.getTabs().remove(addressTab);

Tab类的只读tabPane属性存储了包含选项卡的TabPane的引用。如果一个标签页还没有被添加到一个TabPane中,它的tabPane属性就是null。使用Tab类的getTabPane()方法获取TabPane的引用。

选项卡选项卡放在一起

我已经介绍了足够的信息,可以让你看到一个TabPaneTab在一起工作。通常,选项卡会被重复使用。从Tab类继承一个类有助于重用标签。清单 12-41 和 12-42 创建了两个Tab类。在后续示例中,您将把它们用作选项卡。GeneralTab类包含输入一个人的名字和出生日期的字段。AddressTab类包含输入地址的字段。

// AddressTab.java
package com.jdojo.control;

import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.Tab;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;

public class AddressTab extends Tab {
        TextField streetFld = new TextField();
        TextField cityFld = new TextField();
        TextField stateFld = new TextField();
        TextField zipFld = new TextField();

        public AddressTab(String text, Node graphic) {
                this.setText(text);
                this.setGraphic(graphic);
                init();
        }

        public void init() {
                GridPane grid = new GridPane();
                grid.addRow(0, new Label("Street:"), streetFld);
                grid.addRow(1, new Label("City:"), cityFld);
                grid.addRow(2, new Label("State:"), stateFld);
                grid.addRow(3, new Label("ZIP:"), zipFld);
                this.setContent(grid);
        }
}

Listing 12-42An AddressTab Class That Inherits from the Tab Class

// GeneralTab.java
package com.jdojo.control;

import javafx.scene.Node;
import javafx.scene.control.DatePicker;
import javafx.scene.control.Label;
import javafx.scene.control.Tab;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;

public class GeneralTab extends Tab {
        TextField firstNameFld = new TextField();
        TextField lastNameFld = new TextField();
        DatePicker dob = new DatePicker();

        public GeneralTab(String text, Node graphic) {
                this.setText(text);
                this.setGraphic(graphic);
                init();
        }

        public void init() {
                dob.setPrefWidth(200);
                GridPane grid = new GridPane();
                grid.addRow(0, new Label("First Name:"), firstNameFld);
                grid.addRow(1, new Label("Last Name:"), lastNameFld);
                grid.addRow(2, new Label("DOB:"), dob);
                this.setContent(grid);
        }
}

Listing 12-41A GeneralTab Class That Inherits from the Tab Class

清单 12-43 中的程序创建了两个选项卡。它们是GeneralTabAddressTab类的实例。它们被添加到一个TabPane,后者被添加到一个BorderPane的中心区域。程序显示如图 12-71 所示的窗口。

// TabTest.java
// ... find in the book's download area.

Listing 12-43Using a TabPane and Tabs Together

了解选项卡选择

TabPane支持单一选择模式,一次只能选择一个页签。如果用户或以编程方式选择了一个选项卡,则之前选择的选项卡将被取消选择。Tab类提供了 API 来处理单个选项卡的选择状态。TabPane类提供了允许选择所有标签的 API。

Tab类包含一个boolean类型的只读selected属性。当选项卡被选中时,该值为真。否则就是假的。请注意,这是Tab的财产,而不是TabPane的财产。

Tab允许您添加事件处理程序,当选项卡被选中或取消选中时会得到通知。onSelectionChanged属性存储这样一个事件的引用:

Tab generalTab = ...
generalTab.setOnSelectionChanged(e -> {
        if (generalTab.isSelected()) {
                System.out.println("General tab has been selected.");
        } else {
                System.out.println("General tab has been unselected.");
        }
});

TabPane跟踪选中的选项卡及其在选项卡列表中的索引。为此,它使用了一个单独的对象,称为选择模型TabPane类包含一个selectionModel属性来存储选项卡选择细节。该属性是SingleSelectionModel类的一个对象。你可以使用你自己的选择模型,这个模型几乎是不需要的。选择模型提供了与选择相关的功能:

  • 它允许您使用选项卡的索引来选择选项卡。第一个选项卡的索引为零。

  • 它允许您选择列表中的第一个、下一个、上一个或最后一个选项卡。

  • 它允许您清除选择。请注意,此功能是可用的,但并不常用。一个TabPane通常应该总是有一个选中的选项卡。

  • selectedIndexselectedItem属性跟踪所选选项卡的索引和引用。您可以向这些属性添加一个ChangeListener,以处理TabPane中选项卡选择的变化。

默认情况下,TabPane选择它的第一个标签。下面的代码片段选择了一个TabPane中的最后一个Tab:

tabPane.getSelectionModel().selectLast();

使用选择模型的selectNext()方法从列表中选择下一个选项卡。当已经选择了最后一个选项卡时调用此方法没有任何效果。

使用selectPrevious()selectLast()方法选择列表中的前一个和最后一个选项卡。select(int index)select(T item)方法使用选项卡的索引和引用来选择选项卡。

清单 12-44 中的程序给一个TabPane添加了两个标签。它向两个选项卡添加了一个选择更改事件处理程序。在TabPaneselectionModel属性的selectedItem属性中增加一个ChangeListener。做出选择后,详细的消息会打印在标准输出上。请注意,运行应用程序时会打印一条消息,因为默认情况下,TabPane选择模型会选择第一个选项卡。

// TabSelection.java
// ... find in the book's download area.

Listing 12-44Tracking Tab Selection in a TabPane

关闭选项卡窗格中的选项卡

有时,用户需要按需添加标签到TabPane中,并且他们也应该能够关闭标签。例如,所有现代的网络浏览器都使用标签来浏览,并允许你打开和关闭标签。按需添加选项卡需要在 JavaFX 中进行一些编码。然而,用户关闭标签是内置在TabTabPane类中的。

用户可以使用出现在Tab s 标题栏中的关闭按钮关闭TabPane中的Tab。标签关闭功能由以下属性控制:

  • Tab类的closable属性

  • TabPane类的tabClosingPolicy属性

一个Tab类的closable属性指定标签是否可以被关闭。如果设置为 false,则无论tabClosingPolicy的值如何,选项卡都不能关闭。属性的默认值为 true。tabClosingPolicy属性指定制表符关闭按钮如何可用。它的值是TabPane.TabClosingPolicy枚举的下列常量之一:

  • ALL_TABS

  • SELECTED_TAB

  • UNAVAILABLE

ALL_TABS表示关闭按钮对所有选项卡都可用。也就是说,只要选项卡的closable属性为真,任何选项卡都可以随时关闭。SELECTED_TAB表示关闭按钮只对选中的标签页出现。也就是说,任何时候都只能关闭选定的选项卡。这是TabPane的默认关闭标签策略。UNAVAILABLE表示关闭按钮对任何标签都不可用。也就是说,使用者不能关闭任何翼片,不管它们的可关闭特性如何。

必须区分以下两种情况:

  • 用户使用关闭按钮关闭标签

  • 通过从TabPaneTab的可观察列表中移除它们来以编程方式移除它们

两者具有相同的效果,即从TabPane中移除Tab s。本节中的讨论适用于用户关闭标签。

可以否决关闭选项卡的用户操作。您可以为选项卡的TAB_CLOSE_REQUEST_EVENT事件添加事件处理程序。当用户试图关闭选项卡时,会调用事件处理程序。如果事件处理程序使用事件,关闭操作将被取消。您可以使用Tab类的onCloseRequest属性来设置这样一个事件:

Tab myTab = new Tab("My Tab");
myTab.setOnCloseRequest(e -> {
    if (SOME_CONDITION_IS_TRUE) {
       // Cancel the close request
       e.consume();
   }
});

当用户关闭选项卡时,它也会生成一个关闭事件。使用Tab类的onClosed属性设置选项卡的关闭事件处理程序。事件处理程序通常用于释放选项卡持有的资源:

myTab.setOnClosed(e -> {/* Release tab resources here */});

清单 12-45 中的程序展示了如何使用标签关闭相关的属性和事件。它在一个TabPane中显示两个选项卡。一个复选框允许您否决选项卡的关闭。除非选中该复选框,否则在 close 请求事件中,关闭选项卡的尝试将被否决。如果关闭了选项卡,可以使用“恢复选项卡”按钮恢复它们。使用标签关闭策略ChoiceBox使用不同的标签关闭策略。例如,如果您选择UNAVAILABLE作为标签页关闭策略,关闭按钮将从所有标签页中消失。当标签关闭时,在标准输出上打印一条消息。

// TabClosingTest.java
// ... find in the book's download area.

Listing 12-45Using Properties and Events Related to Closing Tabs by Users

选项卡窗格中定位选项卡

TabPane中的标签可以位于顶部、右侧、底部或左侧。TabPaneside属性指定了制表符的位置。它被设置为Side枚举的常量之一:

  • TOP

  • RIGHT

  • BOTTOM

  • LEFT

侧属性的默认值是Side.TOP。下面的代码片段创建了一个 T abPane,并将 side 属性设置为Side.LEFT,以便在左侧放置制表符:

TabPane tabPane = new TabPane();
tabPane.setSide(Side.LEFT);

Tip

选项卡的实际位置也使用节点方向。例如,如果将side属性设置为Side.LEFT并将TabPane的节点方向设置为RIGHT_TO_LEFT,则选项卡将位于右侧。

TabPane类包含一个rotateGraphic属性,它是一个boolean属性。该属性与side属性相关。当side属性为Side.TOPSide.BOTTOM时,标题栏中所有页签的图形都处于垂直位置。默认情况下,当side属性变为Side.LEFTSide.RIGHT时,标题文本会旋转,使图形保持垂直。rotateGraphic属性指定图形是否随文本旋转,如下面的代码所示。默认情况下,它被设置为 false。

// Rotate the graphic with the text for left and right sides
tabPane.setRotateGraphic(true);

图 12-73 显示了侧属性设置为TOPLEFT的 TabPane 中选项卡的标题栏。注意当边属性为LEFT并且rotateGraphic属性为假和真时对图形的影响。当选项卡位于顶部或底部时,rotateGraphic属性不起作用。

img/336502_2_En_12_Fig73_HTML.jpg

图 12-73

TabPane的侧面和rotateGraphic属性的影响

选项卡窗格中调整选项卡的大小

TabPane将其布局分为两部分:

  • 标题区域

  • 内容区域

标题区域显示选项卡的标题。内容区域显示选定选项卡的内容。内容区域的大小是根据所有选项卡的内容自动计算的。TabPane包含以下属性,允许您设置选项卡标题栏的最小和最大尺寸:

  • tabMinHeight

  • tabMaxHeight

  • tabMinWidth

  • tabMaxWidth

最小宽度和高度的默认值为零,最大宽度和高度的默认值为Double.MAX_VALUE。默认大小是根据选项卡标题的上下文计算的。如果希望所有选项卡标题都是固定大小,请将最小和最大宽度和高度设置为相同的值。请注意,对于固定大小的选项卡,标题栏中较长的文本将被截断。

下面的代码片段创建了一个TabPane并设置了属性,因此所有的选项卡都是 100 像素宽,30 像素高:

TabPane tabPane = new TabPane();
tabPane.setTabMinHeight(30);
tabPane.setTabMaxHeight(30);
tabPane.setTabMinWidth(100);
tabPane.setTabMaxWidth(100);

使用嵌入式和浮动式标签面板

TabPane可以处于隐藏或浮动模式。默认模式是隐藏模式。在凹进模式下,它看起来被固定。在浮动模式下,它的外观被改变以使它看起来像是浮动的。在浮动模式下,标题区域的背景色被删除,内容区域的边框被添加。以下是决定使用哪种模式的经验法则:

  • 如果你在一个窗口中使用一个TabPane和其他控件,使用浮动模式。

  • 如果TabPane是窗口上唯一的控件,使用隐藏模式。

图 12-74 显示了两个具有相同TabPane的窗口:一个在隐藏模式,一个在浮动模式。

img/336502_2_En_12_Fig74_HTML.jpg

图 12-74

凹进和浮动模式下的 A TabPane

一个TabPane的浮动模式是由一个样式类指定的。TabPane类包含一个STYLE_CLASS_FLOATING常量。如果您将这个样式类添加到一个TabPane,它将处于浮动模式。否则,它处于凹进模式。下面的代码片段显示了如何打开和关闭TabPane的浮动模式:

TabPane tabPane = new TabPane();

// Turn on the floating mode
tabPane.getStyleClass().add(TabPane.STYLE_CLASS_FLOATING);
...
// Turn off the floating mode
tabPane.getStyleClass().remove(TabPane.STYLE_CLASS_FLOATING);

带有 CSS 的样式选项卡选项卡

一个tab和一个TabPane的默认 CSS 样式类名是tab-pane。你可以直接使用tab风格类或者使用TabPane的子结构来设计Tab的风格。通常使用后一种方法。

TabPane支持四个 CSS 伪类,对应于其side属性的四个值:

  • top

  • right

  • bottom

  • left

您可以使用以下 CSS 属性设置TabPane中标签标题的最小和最大尺寸。它们对应于TabPane类中的四个属性。有关这些属性的详细讨论,请参考“?? 选项卡窗格中的尺寸选项卡”一节:

  • -fx-tab-min-width

  • -fx-tab-max-width

  • -fx-tab-min-height

  • -fx-tab-max-height

A TabPane将其布局边界分为两个区域:标题区域和内容区域。请参考图 12-72 了解割台区域的不同子部件。标题区域称为tab-header-area子结构,它包含以下子结构:

  • headers-region

  • tab-header-background

  • control-buttons-tab

  • tab

control-buttons-tab子结构包含一个tab-down-button子结构,后者包含一个arrow子结构。tab子结构包含tab-labeltab-close-button子结构。tab-content-area子结构代表TabPane的内容区域。子结构让您可以设计TabPane的不同部分。

TabPane处于浮动模式时,下面的代码删除标题区域的背景颜色:

.tab-pane > .tab-header-area > .tab-header-background {
    -fx-background-color: null;
}

以下代码以粗体显示选定选项卡的文本。注意选择器.tab:selected中选项卡的所选伪类的使用:

.tab-pane > .tab-header-area > .headers-region > .tab:selected
> .tab-container > ,tab-label {
        -fx-font-weight: bold;
}

以下代码显示了蓝色背景的TabPane中的Tab和 10pt 白色标题文本:

.tab-pane > .tab-header-area > .headers-region > .tab  {
    -fx-background-color: blue;
}

.tab-pane > .tab-header-area > .headers-region > .tab > .tab-container
> .tab-label {
        -fx-text-fill: white;
        -fx-font-size: 10pt;
}

为浮动模式设计样式时,使用TabPanefloating样式类。以下样式在浮动模式下将边框颜色设置为蓝色:

.tab-pane.floating > .tab-content-area {
        -fx-border-color: blue;
}

请参考modena.css文件,了解用于TabPane的完整样式列表。

了解 HTMLEditor 控件

HTMLEditor控件为 JavaFX 应用程序提供了丰富的文本编辑功能。它使用 HTML 作为它的数据模型。也就是说,HTMLEditor中的格式化文本是以 HTML 格式存储的。一个HTMLEditor控件可以用于在业务应用程序中输入格式化的文本,例如,产品描述或评论。它还可以用于在电子邮件客户端应用程序中输入电子邮件内容。图 12-75 显示了一个带有HTMLEditor控件的窗口。

img/336502_2_En_12_Fig75_HTML.jpg

图 12-75

一个控件

一个HTMLEditor用它显示格式工具栏。您不能隐藏工具栏。它们可以使用 CSS 来设置样式。使用工具栏,您可以

  • 使用系统剪贴板复制、剪切和粘贴文本

  • 应用文本对齐

  • 缩进文本

  • 应用项目符号列表和编号列表样式

  • 设置前景色和背景色

  • 使用字体系列和字体大小应用段落和标题样式

  • 应用格式样式,如粗体、斜体、下划线和删除线

  • 添加水*标尺

该控件支持 HTML5。请注意,工具栏不允许您应用所有类型的 HTML。但是,如果您加载使用这些样式的文档,它允许您编辑它们。例如,您不能直接在控件中创建 HTML 表。但是,如果将包含 HTML 表格的 HTML 内容加载到控件中,您将能够编辑表格中的数据。

HTMLEditor没有提供 API 来从文件中加载 HTML 内容,以将其内容保存到文件中。您必须编写自己的代码来实现这一点。

创建一个html 编辑器

一个HTMLEditor类的实例代表一个HTMLEditor控件。该类包含在javafx.scene.web包中。使用默认的构造器,这是提供的唯一构造器,来创建一个HTMLEditor:

HTMLEditor editor = new HTMLEditor();

使用html 编辑器

HTMLEditor类有一个非常简单的 API,只包含三个方法:

  • getHtmlText()

  • setHtmlText(String htmlText)

  • print(PrinterJob job)

getHTMLText()方法以字符串的形式返回 HTML 内容。方法将控件的内容设置为指定的 HTML 字符串。print()方法打印控件的内容。

清单 12-46 中的程序展示了如何使用HTMLEditor。它显示一个HTMLEditor,一个TextArea,和两个Buttons。您可以使用按钮将HTMLEditor中的文本转换成 HTML 代码,反之亦然。

// HTMLEditorTest.java
// ... find in the book's download area.

Listing 12-46Using the HTMLEditor Control

使用 CSS 对 html 编辑器进行样式化

一个HTMLEditor的默认 CSS 样式类名是html-editorHTMLEditor使用Control的样式,比如填充、边框和背景色。

您可以分别设置工具栏中每个按钮的样式。以下是工具栏按钮的样式类名列表。名称是不言自明的,例如,html-editor-align-righthtml-editor-hr分别是用于文本右对齐和绘制水*标尺的工具栏按钮的样式类名称。

  • html-editor-cut

  • html-editor-copy

  • html-editor-paste

  • html-editor-align-left

  • html-editor-align-center

  • html-editor-align-right

  • html-editor-align-justify

  • html-editor-outdent

  • html-editor-indent

  • html-editor-bullets

  • html-editor-numbers

  • html-editor-bold

  • html-editor-italic

  • html-editor-underline

  • html-editor-strike

  • html-editor-hr

以下代码为工具栏中的“剪切”按钮设置自定义图像:

.html-editor-cut {
        -fx-graphic: url("my_html_editor_cut.jpg");
}

如果要将样式应用于所有工具栏按钮和切换按钮,请使用buttontoggle-button样式类名:

/* Set the background colors for all buttons and toggle buttons */
.html-editor .button, .html-editor .toggle-button {
    -fx-background-color: lightblue;
}

HTMLEditor显示两个ColorPicker供用户选择背景色和前景色。他们的风格类名是html-editor-backgroundhtml-editor-foreground。下面的代码显示了在ColorPicker中选择的颜色标签:

.html-editor-background {
    -fx-color-label-visible: true;
}

.html-editor-foreground {
    -fx-color-label-visible: true;
}

选择文件和目录

JavaFX 在javafx.stage包中提供了用于显示文件和目录对话框的FileChooserDirectoryChooser类。这些对话框具有依赖于*台的外观,并且不能使用 JavaFX 来设计样式。他们是而不是的控制者。我在本章中讨论它们是因为它们通常和控件一起使用。例如,单击按钮时会显示文件或目录对话框。在某些*台上,例如某些移动和嵌入式设备,用户可能无法访问文件系统。使用这些类来访问这些设备上的文件和目录没有任何作用。

文件选择器对话框

一个FileChooser是标准的文件对话框。它用于让用户选择要打开或保存的文件。它的某些部分,例如标题、初始目录和文件扩展名列表,可以在打开对话框之前指定。使用文件对话框有三个步骤:

  1. 创建一个FileChooser类的对象。

  2. 设置文件对话框的初始属性。

  3. 使用showXXXDialog()方法之一显示特定类型的文件对话框。

创建文件对话框

FileChooser类的一个实例用于打开文件对话框。该类包含一个无参数构造器来创建其对象:

// Create a file dialog
FileChooser fileDialog = new FileChooser();

设置对话框的初始属性

您可以设置文件对话框的以下初始属性:

  • Title

  • initialDirectory

  • initialFileName

  • 扩展过滤器

FileChooser类的title属性是一个 s t环,代表文件对话框的标题:

// Set the file dialog title
fileDialog.setTitle("Open Resume");

FileChooser类的initialDirectory属性是一个File,代表显示文件对话框时的初始目录:

// Set C:\ as initial directory (on Windows)
fileDialog.setInitialDirectory(new File("C:\\"));

FileChooser类的initialFileName属性是一个字符串,它是文件对话框的初始文件名。通常,它用于文件保存对话框。如果用于文件打开对话框,其效果取决于*台。例如,在 Windows 上它会被忽略:

// Set the initial file name
fileDialog.setInitialFileName("untitled.htm");

您可以为文件对话框设置扩展过滤器列表。过滤器显示为下拉框。一次只有一个过滤器处于活动状态。“文件”对话框仅显示与活动扩展名筛选匹配的文件。扩展过滤器由ExtensionFilter类的实例表示,它是FileChooser类的内部静态类。FileChooser类的getExtensionFilters()方法返回一个ObservableList<FileChooser.ExtensionFilter>。将扩展过滤器添加到列表中。扩展名过滤器有两个属性:一个描述和一个文件扩展名列表,格式为*.<extension>:

import static javafx.stage.FileChooser.ExtensionFilter;
...
// Add three extension filters
fileDialog.getExtensionFilters().addAll(
        new ExtensionFilter("HTML Files", "*.htm", "*.html"),
        new ExtensionFilter("Text Files", "*.txt"),
        new ExtensionFilter("All Files", "*.*"));

默认情况下,当显示文件对话框时,列表中的第一个扩展名筛选器处于活动状态。使用selectedExtensionFilter属性指定文件对话框打开时的初始活动过滤器:

// Continuing with the above snippet of code, select *.txt filter by default
fileDialog.setSelectedExtensionFilter(
    fileDialog.getExtensionFilters().get(1));

同一个selectedExtensionFilter属性包含文件对话框关闭时用户选择的扩展过滤器。

显示对话框

一个FileChooser类的实例可以打开三种类型的文件对话框:

  • 仅选择一个文件的文件打开对话框

  • 用于选择多个文件的文件打开对话框

  • 文件保存对话框

下面三个FileChooser类的方法用于打开三种类型的文件对话框:

  • showOpenDialog(Window ownerWindow)

  • showOpenMultipleDialog(Window ownerWindow)

  • showSaveDialog(Window ownerWindow)

直到文件对话框关闭,这些方法才返回。您可以将null指定为所有者窗口。如果指定了所有者窗口,当显示文件对话框时,所有者窗口的输入将被阻止。

showOpenDialog()showSaveDialog()方法返回一个File对象,它是被选择的文件,如果没有选择文件,则返回nullshowOpenMultipleDialog()方法返回一个List<File>,它包含所有选择的文件,或者如果没有选择文件,返回null:

// Show a file open dialog to select multiple files
List<File> files = fileDialog.showOpenMultipleDialog(primaryStage);
if (files != null) {
        for(File f : files) {
                System.out.println("Selected file :" + f);
        }
} else {
        System.out.println("No files were selected.");
}

使用FileChooser类的selectedExtensionFilter属性在文件对话框关闭时获取选定的扩展过滤器:

import static javafx.stage.FileChooser.ExtensionFilter;
...
// Print the selected extension filter description
ExtensionFilter filter = fileDialog.getSelectedExtensionFilter();
if (filter != null) {
    System.out.println("Selected Filter: " + filter.getDescription());
} else {
        System.out.println("No extension filter selected.");
}

使用文件对话框

清单 12-47 中的程序展示了如何使用打开和保存文件对话框。它显示一个带有HTMLEditor和三个按钮的窗口。使用“打开”按钮在编辑器中打开 HTML 文件。在编辑器中编辑内容。使用“保存”按钮将编辑器中的内容保存到文件中。如果您在“保存简历”对话框中选择了一个现有文件,该文件的内容将被覆盖。它留给读者作为增强程序的一个练习,所以它会在覆盖现有文件之前提示用户。

// FileChooserTest.java
// ... find in the book's download area.

Listing 12-47Using Open and Save File Dialogs

目录选择器对话框

有时,您可能需要让用户浏览计算机上可用文件系统中的目录。DirectoryChooser类让你显示一个依赖于*台的目录对话框。

DirectoryChooser类包含两个属性:

  • title

  • initialDirectory

title属性是一个字符串,是目录对话框的标题。initialDirectory属性是一个File,是对话框显示时对话框中选择的初始目录。

使用DirectoryChooser类的showDialog(Window ownerWindow)方法打开目录对话框。当对话框打开时,您最多可以选择一个目录,或者关闭对话框而不选择目录。该方法返回一个File,它是被选择的目录,如果没有选择目录,则返回null。该方法被阻止,直到对话框关闭。如果指定了所有者窗口,当对话框显示时,所有者窗口链中所有窗口的输入都被阻止。您可以指定一个空的所有者窗口。

以下代码片段显示了如何创建、配置和显示目录对话框:

DirectoryChooser dirDialog = new DirectoryChooser();

// Configure the properties
dirDialog.setTitle("Select Destination Directory");
dirDialog.setInitialDirectory(new File("c:\\"));

// Show the directory dialog
File dir = dirDialog.showDialog(null);
if (dir != null) {
        System.out.println("Selected directory: " + dir);
} else {
        System.out.println("No directory was selected.");
}

摘要

用户界面是一种在应用程序及其用户之间交换输入和输出信息的手段。使用键盘输入文本、使用鼠标选择菜单项以及单击按钮都是向 GUI 应用程序提供输入的示例。该应用程序使用文本、图表和对话框等在计算机显示器上显示输出。用户使用称为控件小部件的图形元素与 GUI 应用程序进行交互。按钮、标签、文本字段、文本区域、单选按钮和复选框是控件的几个例子。JavaFX 提供了一组丰富的易于使用的控件。控件被添加到布局窗格中,对它们进行定位和调整大小。

JavaFX 中的每个控件都由一个类的实例来表示。控制类包含在javafx.scene.control包中。JavaFX 中的控件类是Control类的一个直接或间接的子类,而后者又继承自Region类。回想一下,Region类继承自Parent类。所以,技术上来说,一个Control也是一个Parent。一个Parent可以生孩子。但是,控件类不允许添加子级。通常,控件由内部维护的多个节点组成。控件类通过返回一个ObservableList<Node>getChildrenUnmodifiable()方法公开其内部不可修改的子类列表。

一个labeled控件包含一个只读的文本内容和一个可选的图形作为其用户界面的一部分。LabelButtonCheckBoxRadioButtonHyperlink是 JavaFX 中标签控件的一些例子。所有带标签的控件都直接或间接地继承自Labeled类,而后者又继承自Control类。Labeled类包含所有标签控件共有的属性,例如内容对齐、文本相对于图形的位置以及文本字体。

JavaFX 提供了按钮控件,可用于执行命令和/或做出选择。所有按钮控件类都继承自ButtonBase类。所有类型的按钮都支持ActionEvent。按钮被激活时会触发一个ActionEvent。可以用不同的方式激活按钮,例如,使用鼠标、助记键、加速键或其他组合键。被激活时执行命令的按钮称为命令按钮。ButtonHyperlinkMenuButton类代表命令按钮。一个MenuButton让用户执行命令列表中的一个命令。用于向用户呈现不同选择的按钮被称为选择按钮。ToggleButtonCheckBoxRadioButton类代表选择按钮。第三种按钮是前两种的混合。它们让用户执行命令或做出选择。SplitMenuButton类代表一个混合按钮。

JavaFX 提供了允许用户从项目列表中选择项目的控件。与按钮相比,它们占用更少的空间。这些控件是ChoiceBoxComboBoxListViewColorPickerDatePicker. ChoiceBox,用户可以从预定义项目的小列表中选择一个项目。ComboBoxChoiceBox的高级版本。它有许多特性,例如,可编辑的能力或改变列表中项目的外观,这在ChoiceBox. ListView中没有提供,为用户提供了从项目列表中选择多个项目的能力。通常情况下,用户始终可以看到ListView中的所有或多个项目。ColorPicker允许用户从标准调色板中选择一种颜色,或以图形方式定义自定义颜色。DatePicker允许用户从日历弹出窗口中选择日期。用户可以选择以文本形式输入日期。ComboBoxColorPickerDatePicker具有相同的超类,即ComboBoxBase类。

文本输入控件允许用户使用单行或多行纯文本。所有文本输入控件都继承自TextInputControl类。有三种类型的文本输入控件:TextFieldPasswordFieldTextAreaTextField让用户输入单行纯文本;文本中的换行符和制表符被删除。PasswordField继承自TextField。它的工作方式与TextField非常相似,只是它屏蔽了文本。TextArea允许用户输入多行纯文本。一个换行符在TextArea中开始一个新的段落。

对于长时间运行的任务,您需要向用户提供指示任务进度的视觉反馈,以获得更好的用户体验。ProgressIndicatorProgressBar控件用于显示任务的进度。它们显示进度的方式不同。ProgressBar类继承自ProgressIndicator类。ProgressIndicator在圆形控件中显示进度,而ProgressBar使用水*条。

TitledPane是带标签的控件。它将文本显示为标题。该图形显示在标题栏中。除了文本和图形,它还有内容,这是一个节点。通常,一组控件被放在一个容器中,该容器被添加为TitledPane的内容。TitledPane可处于折叠或展开状态。在折叠状态下,它只显示标题栏并隐藏内容。在展开状态下,它显示标题栏和内容。

Accordion是显示一组TitledPane控件的控件,其中一次只有一个控件处于展开状态。

Pagination是一个控件,用于通过将一个大的单一内容分成称为页面的小块来显示它,例如,搜索的结果。

工具提示是一个弹出控件,用于显示节点的附加信息。当鼠标指针悬停在节点上时,它会显示出来。当鼠标指针悬停在某个节点上时和显示该节点的工具提示时之间会有一小段延迟。工具提示在一小段时间后隐藏。当鼠标指针离开控件时,它也被隐藏。你不应该设计一个 GUI 应用程序,在那里用户依赖于看到控件的工具提示,因为如果鼠标指针从不停留在控件上,它们可能根本不会显示。

ScrollBarScrollPane控件为其他控件提供滚动功能。这些控件不能单独使用。它们总是用于支持其他控件中的滚动。

有时,您希望将逻辑上相关的控件水*或垂直并排放置。为了获得更好的外观,控件使用不同类型的分隔符进行分组。SeparatorSplitPane控件用于在视觉上区分两个控件或两组控件。

Slider控件允许用户通过沿轨迹滑动拇指(或旋钮)从数值范围中选择一个数值。一个Slider可以是水*的,也可以是垂直的。

菜单用于以紧凑的形式向用户提供可操作项目的列表。菜单栏是作为菜单容器的水*栏。MenuBar类的一个实例代表一个菜单栏。一个menu包含一个可操作项目的列表,例如通过点击它来按需显示。当用户选择一个项目或将鼠标指针移出列表时,菜单项列表隐藏。菜单通常作为子菜单添加到菜单栏或其他菜单中。Menu类的一个实例代表一个菜单。一个Menu显示文本和图形。菜单项是菜单中可操作的项目。与菜单项相关联的动作由鼠标或按键来执行。菜单项可以使用 CSS 样式。MenuItem类的一个实例代表一个菜单项。MenuItem类不是一个节点。它继承自Object类,因此不能直接添加到场景图中。你需要把它加到一个Menu里。

ContextMenu是一个弹出控件,根据请求显示菜单项列表。它被称为上下文菜单或弹出菜单。默认情况下,它是隐藏的。用户必须提出请求,通常是通过右击鼠标按钮来显示它。一旦做出选择,它将被隐藏。用户可以通过按 Esc 键或在上下文菜单边界外单击来关闭上下文菜单。ContextMenu类的一个对象代表一个上下文菜单。

ToolBar用于显示一组节点,在屏幕上提供常用的动作项。通常情况下,ToolBar包含常用的项目,这些项目也可以通过菜单和上下文菜单获得。一个ToolBar可以容纳许多类型的节点。ToolBar中最常用的节点是按钮和切换按钮。Separator s 用于将一组按钮与其他按钮分开。通常,按钮通过使用小图标来保持较小,最好是 16px 乘 16px 的大小。

窗口可能没有足够的空间在单页视图中显示所有的信息。s 和 ?? 可以让你更好地在页面中呈现信息。一个Tab代表一个页面,一个TabPane包含选项卡。一个Tab不是控制。Tab类的一个实例代表一个TabTab类继承自Object类。然而,一个Tab像控件一样支持一些特性,例如,它们可以被禁用,使用 CSS 样式,并且有上下文菜单和工具提示。

一个Tab由标题和内容组成。标题由文本、可选图形和关闭选项卡的可选关闭按钮组成。内容由控件组成。通常,TabPane中标签的标题是可见的。内容区域由所有选项卡共享。TabPane中的Tab可能位于TabPane的顶部、右侧、底部或左侧。默认情况下,它们位于顶部。

HTMLEditor控件为 JavaFX 应用程序提供了丰富的文本编辑功能。它使用 HTML 作为它的数据模型。也就是说,HTMLEditor中的格式化文本是以 HTML 格式存储的。

JavaFX 在javafx.stage包中提供了FileChooserDirectoryChooser类,分别用于显示文件和目录对话框。这些对话框具有依赖于*台的外观,并且不能使用 JavaFX 来设计样式。它们不是控制。一个FileChooser是一个标准的文件对话框。它用于让用户选择要打开或保存的文件。一个DirectoryChooser让用户从机器上可用的文件系统中浏览一个目录。

下一章将讨论用于以表格格式显示和编辑数据的TableView控件。

十三、了解TableView

在本章中,您将学习:

  • 什么是TableView

  • 如何创建一个TableView

  • 关于将列添加到TableView

  • 关于用数据填充TableView

  • 关于在TableView中显示、隐藏和重新排序列

  • 关于排序和编辑TableView中的数据

  • 关于在TableView中添加和删除行

  • 关于在TableView中调整列的大小

  • 关于用 CSS 样式化一个TableView

本章的例子在com.jdojo.control包中。为了让它们工作,您必须在module-info.java文件中添加相应的一行:

...
opens com.jdojo.control to javafx.graphics, javafx.base;
...

本章中一些较长的列表是以缩略方式显示的。要获得完整的列表,请查阅该书的下载区。

什么是TableView

TableView是一个强大的控件,以表格形式显示和编辑数据模型中的数据。一个TableView由行和列组成。单元格是行和列的交集。单元格包含数据值。列的标题描述了它们包含的数据类型。列可以嵌套。调整列数据的大小和排序具有内置支持。图 13-1 显示了一个有四列的TableView,这四列有标题文本 Id、名字、姓氏和出生日期。它有五行,每行包含一个人的数据。例如,第四行第三列中的单元格包含姓氏 Boyd。

img/336502_2_En_13_Fig1_HTML.jpg

图 13-1

显示人员名单的

是一个强大但不简单的控件。您需要编写几行代码来使用最简单的向用户显示一些有意义的数据的TableView。与TableView一起工作的有几个类。当我讨论TableView的不同特性时,我将详细讨论这些类:

  • TableView

  • TableColumn

  • TableRow

  • TableCell

  • TablePosition

  • TableView.TableViewFocusModel

  • TableView.TableViewSelectionModel

TableView类代表一个TableView控件。TableColumn类表示TableView中的一列。通常,一个TableView包含多个TableColumn实例。一个TableColumn由单元格组成,这些单元格是TableCell类的实例。一个TableColumn使用两个属性来填充单元格并在其中呈现值。它使用单元格值工厂从项目列表中提取其单元格的值。它使用单元格工厂来呈现单元格中的数据。您必须为一个TableColumn指定一个单元格值工厂来查看其中的一些数据。一个TableColumn使用默认的单元格工厂,它知道如何呈现文本和图形节点。

TableRow类继承自IndexedCell类。一个TableRow的实例代表一个TableView中的一行。除非您想为行提供一个定制的实现,否则您几乎不会在应用程序中使用这个类。通常,您自定义单元格,而不是行。

TableCell类的一个实例代表了TableView中的一个单元格。单元格是高度可定制的。它们显示 TableView 的基础数据模型中的数据。它们能够显示数据和图形。

TableColumnTableRowTableCell类包含一个tableView属性,该属性保存对包含它们的TableView的引用。当TableColumn不属于某个TableView时,tableView属性包含null

一个TablePosition代表一个单元格的位置。它的getRow()getColumn()方法分别返回单元格所属的行和列的索引。

TableViewFocusModel类是TableView类的内部静态类。它代表了TableView管理行和单元格焦点的焦点模型。

TableViewSelectionModel类是TableView类的内部静态类。它代表了TableView管理行和单元格选择的选择模型。

ListViewTreeView控件一样,TableView是虚拟化的。它创建了刚好足够显示可视内容的单元格。当您滚动浏览内容时,单元格会被回收。这有助于将场景图形中的节点数量保持在最小。假设在一个TableView中有 10 列 1000 行,一次只能看到 10 行。低效的方法是创建 10,000 个单元,每个单元代表一条数据。TableView只创建了 100 个单元格,所以它可以显示十行十列。当您滚动内容时,同样的 100 个单元格将被循环显示其他可见的行。虚拟化使得将TableView与大型数据模型结合使用成为可能,并且在查看数据块时不会影响性能。

对于本章中的例子,我将使用 MVC 第十一章中的Person类。Person级在com.jdojo.mvc.model包里。在我开始详细讨论TableView控件之前,我将介绍一个PersonTableUtil类,如清单 13-1 所示。我将在给出的例子中多次重用它。它有静态方法来返回一个可见的 persona 列表和一个TableColumn类的实例来表示一个TableView中的列。

// PersonTableUtil.java
package com.jdojo.control;

import com.jdojo.mvc.model.Person;
import java.time.LocalDate;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.control.TableColumn;
import javafx.scene.control.cell.PropertyValueFactory;

public class PersonTableUtil {
    /* Returns an observable list of persons */
    public static ObservableList<Person> getPersonList() {
      Person p1 =
            new Person("Ashwin", "Sharan", LocalDate.of(2012, 10, 11));
      Person p2 =
            new Person("Advik", "Sharan", LocalDate.of(2012, 10, 11));
      Person p3 =
            new Person("Layne", "Estes", LocalDate.of(2011, 12, 16));
      Person p4 =
            new Person("Mason", "Boyd", LocalDate.of(2003, 4, 20));
      Person p5 =
            new Person("Babalu", "Sharan", LocalDate.of(1980, 1, 10));
      return FXCollections.<Person>observableArrayList(p1, p2, p3, p4, p5);
    }

    /* Returns Person Id TableColumn */
    public static TableColumn<Person, Integer> getIdColumn() {
        TableColumn<Person, Integer> personIdCol = new TableColumn<>("Id");
        personIdCol.setCellValueFactory(
               new PropertyValueFactory<>("personId"));
        return personIdCol;
    }

    /* Returns First Name TableColumn */
    public static TableColumn<Person, String> getFirstNameColumn() {
      TableColumn<Person, String> fNameCol =
            new TableColumn<>("First Name");
      fNameCol.setCellValueFactory(new PropertyValueFactory<>("firstName"));
      return fNameCol;
    }

    /* Returns Last Name TableColumn */
    public static TableColumn<Person, String> getLastNameColumn() {
        TableColumn<Person, String> lastNameCol =
               new TableColumn<>("Last Name");
        lastNameCol.setCellValueFactory(
               new PropertyValueFactory<>("lastName"));
        return lastNameCol;
    }

    /* Returns Birth Date TableColumn */
    public static TableColumn<Person, LocalDate> getBirthDateColumn() {
        TableColumn<Person, LocalDate> bDateCol =
            new TableColumn<>("Birth Date");
        bDateCol.setCellValueFactory(
               new PropertyValueFactory<>("birthDate"));
        return bDateCol;
    }
}

Listing 13-1A PersonTableUtil Utility Class

后续部分将带您完成在TableView中显示和编辑数据的步骤。

创建一个TableView

在下面的例子中,您将使用TableView类来创建一个TableView控件。TableView是一个参数化的类,它接受TableView包含的项目类型。或者,您可以将模型传递给提供数据的构造器。构造器创建了一个没有模型的TableView。下面的语句创建了一个TableView,它将使用Person类的对象作为它的项目:

TableView<Person> table = new TableView<>();

当你将前面的TableView添加到场景中时,它会显示一个占位符,如图 13-2 所示。占位符让您知道您需要向TableView添加列。在TableView数据中必须至少有一个可见的叶列。

img/336502_2_En_13_Fig2_HTML.jpg

图 13-2

没有显示占位符的列和数据的TableView

您可以使用另一个TableView类的构造器来指定模型。它接受一个可观察的项目列表。以下语句传递一个可观察的Person对象列表作为TableView的初始数据:

TableView<Person> table = new TableView<>(PersonTableUtil.getPersonList());

将列添加到TableView

TableColumn类的一个实例代表了TableView中的一列。一个TableColumn负责显示和编辑其单元格中的数据。一个TableColumn有一个可以显示标题文本和/或图形的标题。您可以为一个TableColumn创建一个上下文菜单,当用户在列标题中单击鼠标右键时,就会显示这个菜单。使用contextMenu属性设置一个上下文菜单。

TableColumn<S, T>类是一个泛型类。S参数为项目类型,与TableView的参数类型相同。T参数是该列所有单元格中的数据类型。例如,TableColumn<Person, Integer>的一个实例可以用来表示一个显示一个人的 ID 的列,它是int类型;一个TableColumn<Person, String>的实例可以用来表示一个显示一个人的名字的列,它是String类型的。以下代码片段创建了一个TableColumn with名字作为其标题文本:

TableColumn<Person, String> fNameCol = new TableColumn<>("First Name");

TableColumn需要知道如何从模型中获取其单元格的值(或数据)。要填充单元格,您需要设置TableColumncellValueFactory属性。如果TableView的模型包含基于 JavaFX 属性的类的对象,您可以使用PropertyValueFactory类的对象作为单元格值工厂,该工厂接受属性名。它从模型中读取属性值,并填充列中的所有单元格,如下面的代码所示:

// Use the firstName property of Person object to populate the column cells
PropertyValueFactory<Person, String> fNameCellValueFactory =
        new PropertyValueFactory<>("firstName");
fNameCol.setCellValueFactory(fNameCellValueFactory);

您需要为TableView中的每一列创建一个TableColumn对象,并设置其单元格值工厂属性。下一节将解释如果您的 item 类不基于 JavaFX 属性,或者您希望用计算值填充单元格,该怎么办。

设置TableView的最后一步是将TableColumns添加到它的列列表中。一个TableView将它的列的引用存储在一个ObservableList<TableColumn>中,?? 的引用可以使用TableViewgetColumns()方法获得:

// Add the First Name column to the TableView
table.getColumns().add(fNameCol);

这就是使用最简单形式的TableView所要做的一切,毕竟它并不那么“简单”!清单 13-2 中的程序展示了如何创建一个带有模型的TableView并向其中添加列。它使用PersonTableUtil类来获取人员和列的列表。程序显示如图 13-3 所示的窗口。

img/336502_2_En_13_Fig3_HTML.jpg

图 13-3

带有一个显示四列五行的TableView的窗口

// SimplestTableView.java
package com.jdojo.control;

import com.jdojo.mvc.model.Person;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.TableView;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

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

        @Override
        public void start(Stage stage) {
                // Create a TableView with a list of persons
                TableView<Person> table =
                         new TableView<>(PersonTableUtil.getPersonList());

                // Add columns to the TableView
                table.getColumns().addAll(
                         PersonTableUtil.getIdColumn(),
                         PersonTableUtil.getFirstNameColumn(),
                         PersonTableUtil.getLastNameColumn(),
                         PersonTableUtil.getBirthDateColumn());

                VBox root = new VBox(table);
                root.setStyle("""
                         -fx-padding: 10;
                   -fx-border-style: solid inside;
                   -fx-border-width: 2;
                   -fx-border-insets: 5;
                   -fx-border-radius: 5;
                   -fx-border-color: blue;""");

                Scene scene = new Scene(root);
                stage.setScene(scene);
                stage.setTitle("Simplest TableView");
                stage.show();
        }
}

Listing 13-2Using TableView in Its Simplest Form

TableView支持列嵌套。例如,您可以在名称列中嵌套两列,第一列和最后一列。一个TableColumn将嵌套列的列表存储在一个可观察列表中,该列表的引用可以使用TableColumn类的getColumns()方法获得。最里面的嵌套列被称为叶列。您需要为叶列添加单元格值工厂。嵌套列仅提供视觉效果。下面的代码片段创建了一个TableView,并添加了一个 Id 列和两个叶列,第一个和最后一个,它们嵌套在 Name 列中。结果TableView如图 13-4 所示。注意,您将最顶端的列添加到了TableView,而不是嵌套的列。TableView负责为最顶端的列添加所有嵌套的列。对列嵌套的级别没有限制。

img/336502_2_En_13_Fig4_HTML.jpg

图 13-4

具有嵌套列的TableView

// Create a TableView with data
TableView<Person> table = new TableView<>(PersonTableUtil.getPersonList());

// Create leaf columns - Id, First and Last
TableColumn<Person, String> idCol = new TableColumn<>("Id");
idCol.setCellValueFactory(new PropertyValueFactory<>("personId"));

TableColumn<Person, String> fNameCol = new TableColumn<>("First");
fNameCol.setCellValueFactory(new PropertyValueFactory<>("firstName"));

TableColumn<Person, String> lNameCol = new TableColumn<>("Last");
lNameCol.setCellValueFactory(new PropertyValueFactory<>("lastName"));

// Create Name column and nest First and Last columns in it
TableColumn<Person, String> nameCol = new TableColumn<>("Name");
nameCol.getColumns().addAll(fNameCol, lNameCol);

// Add columns to the TableView
table.getColumns().addAll(idCol, nameCol);

TableView类中的以下方法提供了关于可见叶列的信息:

TableColumn<S,?> getVisibleLeafColumn(int columnIndex)
ObservableList<TableColumn<S,?>> getVisibleLeafColumns()
int getVisibleLeafIndex(TableColumn<S,?> column)

getVisibleLeafColumn()方法返回指定列索引的列引用。只对可见的叶列计算列索引,索引从零开始。getVisibleLeafColumns()方法返回所有可见叶列的可见列表。getVisibleLeafIndex()方法返回可见叶列的指定列索引的列引用。

定制TableView占位符

TableView当占位符没有任何可见的叶列或内容时,显示占位符。考虑下面的代码片段,它创建了一个TableView并向其中添加了列:

TableView<Person> table = new TableView<>();
table.getColumns().addAll(PersonTableUtil.getIdColumn(),
                   PersonTableUtil.getFirstNameColumn(),
                   PersonTableUtil.getLastNameColumn(),
                   PersonTableUtil.getBirthDateColumn());

图 13-5 显示了前面TableView的结果。显示列和占位符,表示TableView没有数据。

img/336502_2_En_13_Fig5_HTML.jpg

图 13-5

有列但没有数据的TableView控件

您可以使用TableViewplaceholder属性替换内置占位符。属性的值是Node类的一个实例。以下语句设置了一个带有作为占位符的generic消息的Label:

table.setPlaceholder(new Label("No visible columns and/or data exist."));

您可以设置一个自定义占位符来通知用户导致TableView中不显示数据的具体情况。以下语句使用绑定来随着条件的变化而改变占位符:

table.placeholderProperty().bind(
    new When(new SimpleIntegerProperty(0)
                 .isEqualTo(table.getVisibleLeafColumns().size()))
            .then(new When(new SimpleIntegerProperty(0)
                              .isEqualTo(table.getItems().size()))
                      .then(new Label("No columns and data exist."))
                      .otherwise(new Label("No columns exist.")))
            .otherwise(new When(new SimpleIntegerProperty(0)
                           .isEqualTo(table.getItems().size()))
                           .then(new Label("No data exist."))
                           .otherwise((Label)null)));

用数据填充表格列

TableView行中的单元格包含与一个项目相关的数据,比如一个人、一本书等等。一行中某些单元格的数据可能直接来自该项目的属性,也可能是计算出来的。

TableView有一个ObservableList<S>类型的items属性。通用类型STableView的通用类型相同。它是TableView的数据模型。项目列表中的每个元素代表TableView中的一行。向条目列表添加新条目会向TableView添加新行。从项目列表中删除项目会从TableView中删除相应的行。

Tip

更新项目列表中的项目是否会更新TableView中的相应数据取决于该列的单元格值工厂是如何设置的。我将在本节讨论这两种类型的例子。

下面的代码片段创建了一个TableView,其中一行代表一个Person对象。它添加两行数据:

TableView<Person> table = new TableView<>();

Person p1 = new Person("John", "Jacobs", null);
Person p2 = new Person("Donna", "Duncan", null);
table.getItems().addAll(p1, p2);

TableView添加项目是没有用的,除非你向它添加列。除了其他一些东西,一个TableColumn对象定义了

  • 列的标题文本和图形

  • 用于填充列中单元格的单元格值工厂

TableColumn类让你完全控制如何填充一列中的单元格。TableColumn类的cellValueFactory属性负责填充列的单元格。单元格值工厂是Callback类的对象,它接收一个TableColumn.CellDataFeatures对象并返回一个ObservableValue

CellDataFeatures类是TableColumn类的静态内部类,它包装了TableViewTableColumn的引用,以及正在填充列单元格的行的项目。使用CellDataFeatures类的getTableView()getTableColumn()getValue()方法分别获取TableViewTableColumn的引用和该行的项目。

TableView需要单元格的值时,它调用单元格所属列的单元格值工厂对象的call()方法。call()方法应该返回一个ObservableValue对象的引用,该对象被监控是否有任何变化。返回的ObservableValue对象可以包含任何类型的对象。如果它包含一个节点,则该节点在单元格中显示为图形。否则调用对象的toString()方法,返回的字符串显示在单元格中。

以下代码片段使用匿名类创建了一个单元格值工厂。工厂返回对Person类的firstName属性的引用。注意,JavaFX 属性是一个ObservableValue

import static javafx.scene.control.TableColumn.CellDataFeatures;
...
// Create a String column with the header "First Name" for Person object
TableColumn<Person, String> fNameCol = new TableColumn<>("First Name");

// Create a cell value factory object
Callback<CellDataFeatures<Person, String>, ObservableValue<String>> fNameCellFactory =
  new Callback<CellDataFeatures<Person, String>, ObservableValue<String>>() {
    @Override
    public ObservableValue<String> call(CellDataFeatures<Person,
            String> cellData) {
      Person p = cellData.getValue();
      return p.firstNameProperty();
}};

// Set the cell value factory
fNameCol.setCellValueFactory(fNameCellFactory);

使用 lambda 表达式创建和设置单元格值工厂非常方便。前面的代码片段可以编写如下:

TableColumn<Person, String> fNameCol = new TableColumn<>("First Name");
fNameCol.setCellValueFactory(cellData ->
    cellData.getValue().firstNameProperty());

当 JavaFX 属性为列中的单元格提供值时,如果使用PropertyValueFactory类的对象,创建单元格值工厂会更容易。您需要将 JavaFX 属性的名称传递给它的构造器。下面的代码片段与前面显示的代码具有相同的功能。您将采用这种方法在PersonTableUtil类的实用方法中创建TableColumn对象。

TableColumn<Person, String> fNameCol = new TableColumn<>("First Name");
fNameCol.setCellValueFactory(new PropertyValueFactory<>("firstName"));

Tip

使用 JavaFX 属性作为单元格的值有一个很大的优势。TableView保持属性和单元格中的值同步。更改模型中的属性值会自动更新单元中的值。

TableColumn还支持 POJO (Plain Old Java Object)作为TableView中的条目。缺点是当模型更新时,单元值不会自动更新。您使用相同的PropertyValueFactory类来创建单元格值工厂。该类将使用您传递的属性名称查找公共 getter 和 setter 方法。如果只找到 getter 方法,该单元格将是只读的。对于一个xxx属性,它尝试使用 JavaBeans 命名约定寻找getXxx()setXxx()方法。如果xxx的类型是 boolean,它也会寻找isXxx()方法。如果找不到 getter 或 setter 方法,则会引发运行时异常。以下代码片段创建了一个标题为“年龄类别”的列:

TableColumn<Person, Person.AgeCategory> ageCategoryCol =
    new TableColumn<>("Age Category");
ageCategoryCol.setCellValueFactory(new PropertyValueFactory<>("ageCategory"));

表示项目类型为Person,列类型为Person.AgeCategory。它将ageCategory作为属性名传递给PropertyValueFactory类的构造器。首先,这个类将在Person类中寻找一个ageCategory属性。Person类没有这个属性。因此,它将尝试使用Person类作为该属性的 POJO。然后它会在Person类中寻找getAgeCategory()setAgeCategory()方法。它只找到 getter 方法getAgeCategory(),因此它将使该列成为只读的。

列单元格中的值不一定来自 JavaFX 或 POJO 属性。它们可以用一些逻辑来计算。在这种情况下,您需要创建一个定制的单元格值工厂,并返回一个包装计算值的ReadOnlyXxxWrapper对象。以下代码片段创建了一个年龄列,该列以年为单位显示计算出的年龄:

TableColumn<Person, String> ageCol = new TableColumn<>("Age");
ageCol.setCellValueFactory(cellData -> {
        Person p = cellData.getValue();
        LocalDate dob = p.getBirthDate();
        String ageInYear = "Unknown";
        if (dob != null) {
                long years = YEARS.between(dob, LocalDate.now());
                if (years == 0) {
                        ageInYear = "< 1 year";
                } else if (years == 1) {
                        ageInYear = years + " year";
                } else {
                        ageInYear = years + " years";
                }

        }
        return new ReadOnlyStringWrapper(ageInYear);
});

这就完成了在TableView中为一列单元格设置单元格值工厂的不同方式。清单 13-3 中的程序为 JavaFX 属性、POJO 属性和计算值创建单元格值工厂。显示如图 13-6 所示的窗口。

img/336502_2_En_13_Fig6_HTML.jpg

图 13-6

一个TableView包含 JavaFX 属性、POJO 属性和计算值的列

// TableViewDataTest.java
// ...find in the book's download area

Listing 13-3Setting Cell Value Factories for Columns

TableView中的单元格可以显示文本和图形。如果单元格值工厂返回一个Node类的实例,它可能是一个ImageView,单元格将它显示为图形。否则,它显示从对象的toString()方法返回的字符串。可以在单元格中显示其他控件和容器。然而,TableView并不意味着,这种用途是不鼓励的。有时,在单元格中使用特定类型的控件(例如复选框)来显示或编辑布尔值可以提供更好的用户体验。我将很快介绍这种单元格的定制。

使用地图作为TableView中的项目

有时,TableView的一行中的数据可能不会映射到域对象,例如,您可能希望在TableView中显示动态查询的结果集。物品清单包括一份可观察的Map清单。列表中的Map包含该行中所有列的值。您可以定义一个自定义的单元格值工厂来从Map中提取数据。MapValueFactory级就是为此目的专门设计的。它是单元格值工厂的一个实现,从一个指定键的Map中读取数据。

下面的代码片段创建了一个MapTableView。它创建一个 Id 列,并将MapValueFactory类的一个实例设置为它的单元格值工厂,将idColumnKey指定为包含 Id 列值的键。它创建一个Map,并使用idColumnKey填充 Id 列。您需要对所有的列和行重复这些步骤。

TableView<Map> table = new TableView<>();

// Define the column, its cell value factory and add it to the TableView
String idColumnKey = "id";
TableColumn<Map, Integer> idCol = new TableColumn<>("Id");
idCol.setCellValueFactory(new MapValueFactory<>(idColumnKey));
table.getColumns().add(idCol);

// Create and populate a Map an item
Map row1 = new HashMap();
row1.put(idColumnKey, 1);

// Add the Map to the TableView items list

table.getItems().add(row1);

清单 13-4 中的程序展示了如何使用MapValueFactory作为TableView中列的单元格值工厂。它显示由PersonTableUtil类中的getPersonList()方法返回的人的数据。

// TableViewMapDataTest.java
// ...find in the book's download area

Listing 13-4Using MapValueFactory As a Cell Value Factory for Cells in a TableView

显示和隐藏列

默认情况下,TableView中的所有列都是可见的。TableColumn类有一个visible属性来设置列的可见性。如果关闭父列(具有嵌套列的列)的可见性,其所有嵌套列也将不可见:

TableColumn<Person, String> idCol = new TableColumn<>("Id");

// Make the Id column invisible
idCol.setVisible(false);
...
// Make the Id column visible
idCol.setVisible(true);

有时,您可能希望让用户控制列的可见性。TableView类有一个tableMenuButtonVisible属性。如果设置为true,标题区会显示一个菜单按钮:

// Create a TableView
TableView<Person> table = create the TableView here...

// Make the table menu button visible
table.setTableMenuButtonVisible(true);

单击菜单按钮会显示所有叶列的列表。列显示为单选菜单项,可用于切换它们的可见性。图 13-7 显示了一个有四列的TableView。它的tableMenuButtonVisible属性被设置为 true。该图显示了一个菜单,其中所有列的名称都带有复选标记。单击菜单按钮时会显示菜单。列名旁边的复选标记表示这些列可见。单击列名可切换其可见性。

img/336502_2_En_13_Fig7_HTML.jpg

图 13-7

一个带有菜单按钮的TableView来切换列的可见性

TableView中的列进行重新排序

您可以用两种方式重新排列TableView中的列:

  • 通过将列拖放到不同的位置

  • 通过改变它们在由TableView类的getColumns()方法返回的可观察列表中的位置

默认情况下,第一个选项可用。用户需要在新位置拖放一列。当列被重新排序时,它在列列表中的位置会发生变化。第二个选项将直接在列列表中对列进行重新排序。

没有简单的方法来禁用默认的列重新排序特性。如果您想禁用这个特性,您需要向由TableViewgetColumns()方法返回的ObservableList添加一个ChangeListener。当报告更改时,重置列,使它们再次处于原始顺序。

要启用或禁用列重新排序特性,请对列使用setReorderable()方法:

table.getColumns().forEach(c -> {
      boolean b = ...; // determine whether column is reorderable

      c.setReorderable(b);
});

对表格视图中的数据进行排序

TableView内置了对列中数据排序的支持。默认情况下,它允许用户通过单击列标题对数据进行排序。它还支持以编程方式对数据进行排序。您还可以对TableView中的一列或所有列禁用排序。

按用户排序数据

默认情况下,可以对TableView中所有列的数据进行排序。用户可以通过单击列标题对列中的数据进行排序。第一次单击按升序对数据进行排序。第二次单击按降序对数据进行排序。第三次单击会从排序顺序列表中删除该列。

默认情况下,启用单列排序。也就是说,如果你点击一列,那么TableView中的记录只根据被点击列中的数据进行排序。要启用多列排序,您需要在单击要排序的列的标题时按住 Shift 键。

TableView在已排序列的标题中显示视觉线索,以指示排序类型和排序顺序。默认情况下,列标题中会显示一个指示排序类型的三角形。对于升序排序类型,它指向上;对于降序排序类型,它指向下。列的排序顺序由点或数字表示。点用于排序顺序列表中的前三列。从第四列开始使用数字。例如,排序顺序列表中的第一列显示一个点,第二列显示两个点,第三列显示三个点,第四列显示数字 4,第五列显示数字 5,依此类推。

图 13-8 显示了一个有四列的TableView。列标题显示了排序类型和排序顺序。姓氏的排序类型是降序,其他的排序类型是升序。姓氏、名字、出生日期和 Id 的排序顺序分别为 1、2、3 和 4。请注意,点用于前三列中的排序顺序,数字 4 用于 Id 列,因为它是排序顺序列表中的第四列。这种排序是通过按以下顺序单击列标题来实现的:姓氏(两次)、名字、出生日期和 Id。

img/336502_2_En_13_Fig8_HTML.jpg

图 13-8

显示排序类型和排序顺序的列标题

以编程方式排序数据

可以通过编程方式对列中的数据进行排序。TableViewTableColumn类为排序提供了一个非常强大的 API。排序 API 由两个类中的几个属性和方法组成。分拣的每个部分和每个阶段都是可定制的。以下部分通过示例描述了 API。

使列可排序

TableColumnsortable属性决定了该列是否可排序。默认情况下,它被设置为 true。将其设置为 false 可禁用列的排序:

// Disable sorting for fNameCol column
fNameCol.setSortable(false);

指定列的排序类型

一个TableColumn有一个排序类型,可以是升序也可以是降序。它是通过sortType属性指定的。TableColumn.SortType枚举的ASCENDINGDESCENDING常量分别代表列的升序和降序排序类型。sortType属性的默认值是TableColumn.SortType.ASCENDINGDESCENDING常量设置如下:

// Set the sort type for fNameCol column to descending
fNameCol.setSortType(TableColumn.SortType.DESCENDING);

为列指定比较器

一个TableColumn使用一个Comparator来排序它的数据。您可以使用comparator属性为TableColumn指定Comparator。被比较的两个单元格中的对象被传入comparator。A TableColumn使用默认的Comparator,用常量TableColumn.DEFAULT_COMPARATOR表示。默认比较器使用以下规则比较两个单元格中的数据:

  • 它检查null值。首先对null值进行排序。如果两个单元格都有null,则认为它们相等。

  • 如果被比较的第一个值是Comparable接口的实例,它调用第一个对象的compareTo()方法,将第二个对象作为参数传递给该方法。

  • 如果前面两个条件都不成立,它将两个对象转换成字符串,调用它们的toString()方法,并使用一个Comparator来比较两个String值。

大多数情况下,默认比较器就足够了。下面的代码片段为String列使用了一个自定义比较器,该比较器只比较单元格数据的第一个字符:

TableColumn<Person, String> fNameCol = new TableColumn<>("First Name");
...
// Set a custom comparator
fNameCol.setComparator((String n1, String n2) -> {
        if (n1 == null && n2 == null) {
                return 0;
        }

        if (n1 == null) {
                return -1;
        }

        if (n2 == null) {
                return 1;
        }

        String c1 = n1.isEmpty()? n1:String.valueOf(n1.charAt(0));
        String c2 = n2.isEmpty()? n2:String.valueOf(n2.charAt(0));
        return c1.compareTo(c2);
});

指定列的排序节点

TableColumn类包含一个sortNode属性,该属性指定一个节点在列标题中显示关于列的当前排序类型和排序顺序的可视线索。当排序类型为升序时,节点旋转 180 度。当列不是排序的一部分时,节点不可见。默认情况下,它是null,而TableColumn提供了一个三角形作为排序节点。

指定列的排序顺序

TableView类包含几个用于排序的属性。要对列进行排序,您需要将它们添加到TableView的排序顺序列表中。sortOrder属性指定了排序顺序。它是TableColumn的一只ObservableList。列表中TableColumn的顺序指定了列在排序中的顺序。根据列表中的第一列对行进行排序。如果列中两行的值相等,则排序顺序列表中的第二列用于确定两行的排序顺序,依此类推。

下面的代码片段将两列添加到一个TableView中,并指定它们的排序顺序。请注意,这两列都将按升序排序,这是默认的排序类型。如果您想按降序对它们进行排序,请按如下方式设置它们的sortType属性:

// Create a TableView with data
TableView<Person> table = new TableView<>(PersonTableUtil.getPersonList());

TableColumn<Person, String> lNameCol = PersonTableUtil.getLastNameColumn();
TableColumn<Person, String> fNameCol = PersonTableUtil.getFirstNameColumn();

// Add columns to the TableView
table.getColumns().addAll(lNameCol, fNameCol );

// Add columns to the sort order to sort by last name followed by first name
table.getSortOrder().addAll(lNameCol, fNameCol);

监视TableViewsortOrder属性的变化。如果修改了,TableView会立即根据新的排序顺序进行排序。向排序顺序列表中添加列并不保证该列包含在排序中。该列也必须是可排序的,才能包含在排序中。还监视TableColumnsortType属性的变化。在排序顺序列表中更改列的排序类型,会立即重新排序TableView数据。

获取TableView的比较器

TableView包含一个只读的comparator属性,它是一个基于当前排序顺序列表的Comparator。您很少需要在代码中使用这个Comparator。如果将两个TableView项传递给Comparatorcompare()方法,它将返回一个负整数、零或正整数,分别表示第一项小于、等于或大于第二项。

回想一下,TableColumn也有一个comparator属性,用于指定如何确定TableColumn单元格中值的顺序。TableViewcomparator属性组合了其排序顺序列表中所有TableColumnscomparator属性。

指定排序策略

一个TableView有一个排序策略来指定如何执行排序。它是一个Callback对象。TableView作为参数传递给call()方法。如果排序成功,该方法返回true。如果排序失败,则返回falsenull

TableView类包含一个DEFAULT_SORT_POLICY常量,用作TableView的默认排序策略。它使用comparator属性对TableView的条目列表进行排序。指定一个排序策略来完全控制排序算法。排序策略Callback对象的call()方法将对TableView的项目进行排序。

举个简单的例子,将排序策略设置为null将禁用排序,因为当用户或程序请求排序时,将不执行排序:

TableView<Person> table = ...

// Disable sorting for the TableView
table.setSortPolicy(null);

有时,出于性能原因,暂时禁用排序是有用的。假设您有一个包含大量条目的已排序的TableView,并且您想要对排序顺序列表进行一些更改。排序顺序列表中的每次更改都会触发对项目的排序。在这种情况下,您可以通过将排序策略设置为null来禁用排序,进行所有更改,并通过恢复原始排序策略来启用排序。排序策略的变化会触发立即排序。这项技术将只对项目排序一次:

TableView<Person> table = ...
...
// Store the current sort policy
Callback<TableView<Person>, Boolean> currentSortPolicy =
    table.getSortPolicy();

// Disable the sorting
table.setSortPolicy(null)

// Make all changes that might need or trigger sorting

...

// Restore the sort policy that will sort the data once immediately
table.setSortPolicy(currentSortPolicy);

手动排序数据

TableView包含一个sort()方法,该方法使用当前排序顺序列表对TableView中的项目进行排序。在向一个TableView添加了许多项目之后,您可以调用这个方法来对项目进行排序。当列的排序类型、排序顺序或排序策略发生变化时,会自动调用此方法。

处理排序事件

TableView在收到排序请求时,在将排序算法应用于项目之前,触发一个SortEvent。添加一个SortEvent监听器,在实际排序之前执行任何操作:

TableView<Person> table = ...
table.setOnSort(e -> {/* Code to handle the sort event */});

如果SortEvent被消耗,分类中止。如果您想禁用对一个TableView的排序,按如下方式消耗SortEvent:

// Disable sorting for the TableView
table.setOnSort(e -> e.consume());

禁用表视图的排序

有几种方法可以禁用对TableView的排序:

  • TableColumn设置sortable属性只会禁用该列的排序。如果将TableView中所有列的sortable属性设置为 false,那么TableView的排序将被禁用。

  • 您可以为TableViewnull设置分类策略。

  • 你可以用SortEventTableView

  • 从技术上来说,可以覆盖TableView类的sort()方法,并为该方法提供一个空体,但不推荐这样做。

对一个TableView部分或完全禁用排序的最好方法是对它的部分或全部列禁用排序。

自定义单元格中的数据呈现

TableColumn中的单元格是TableCell类的一个实例,它显示单元格中的数据。一个TableCell是一个Labeled控件,它能够显示文本和/或图形。

您可以为TableColumn指定一个细胞工厂。单元工厂的工作是呈现单元中的数据。TableColumn类包含一个cellFactory属性,它是一个Callback对象。它的call()方法在单元格所属的TableColumn的引用中传递。该方法返回一个TableCell的实例。TableCellupdateItem()方法被覆盖以提供单元格数据的自定义呈现。

如果未指定cellFactory属性,TableColumn将使用默认的单元格工厂。默认单元工厂根据数据类型显示单元数据。如果单元格数据包含一个节点,则数据显示在单元格的graphic属性中。否则,调用单元格数据的toString()方法,返回的字符串显示在单元格的text属性中。

到目前为止,您已经使用了一系列的Person对象作为示例中的数据模型,用于在TableView中显示数据。出生日期列的格式为 yyyy-mm-dd,这是由LocalDate类的toString()方法返回的默认 ISO 日期格式。如果要将出生日期格式化为 mm/dd/yyyy 格式,可以通过为出生日期列设置自定义单元格工厂来实现:

TableColumn<Person, LocalDate> birthDateCol = ...;
birthDateCol.setCellFactory (col -> {
    TableCell<Person, LocalDate> cell =
        new TableCell<Person, LocalDate>() {
          @Override
          public void updateItem(LocalDate item, boolean empty) {
              super.updateItem(item, empty);

              // Cleanup the cell before populating it
              this.setText(null);
              this.setGraphic(null);

              if (!empty) {
                  // Format the birth date in mm/dd/yyyy format
                  String formattedDob =
                      DateTimeFormatter.ofPattern("MM/dd/yyyy").
                         format(item);
                  this.setText(formattedDob);
              }
          }

    };
    return cell;
});

您还可以使用前面的技术在单元格中显示图像。在updateItem()方法中,为图像创建一个ImageView对象,并使用TableCellsetGraphic()方法显示它。TableCell包含tableColumntableRowtableView属性,分别存储其TableColumnTableRowTableView的引用。这些属性对于访问数据模型中表示单元格行的项目非常有用。

如果将前面代码片段中的if语句替换为以下代码,则出生日期列将显示出生日期和年龄类别,例如 10/11/2012(婴儿):

if (!empty) {
    String formattedDob =
         DateTimeFormatter.ofPattern("MM/dd/yyyy").format(item);

    if (this.getTableRow() != null ) {
        // Get the Person item for this cell
        int rowIndex = this.getTableRow().getIndex();
        Person p = this.getTableView().getItems().get(rowIndex);
        String ageCategory = p.getAgeCategory().toString();

        // Display birth date and age category together
        this.setText(formattedDob + " (" + ageCategory + ")" );
    }
}

下面是以不同方式呈现单元格数据的TableCell的子类。例如,CheckBoxTableCell在复选框中显示单元格数据,而ProgressBarTableCell使用进度条显示数字:

  • CheckBoxTableCell

  • ChoiceBoxTableCell

  • ComboBoxTableCell

  • ProgressBarTableCell

  • TextFieldTableCell

下面的代码片段创建了一个标签为 Baby?并设置一个单元格工厂来显示一个CheckBoxTableCell中的值。CheckBoxTableCell类的forTableColumn(TableColumn<S, Boolean> col)方法返回一个用作单元格工厂的Callback对象:

// Create a "Baby?" column
TableColumn<Person, Boolean> babyCol = new TableColumn<>("Baby?");
babyCol.setCellValueFactory(cellData -> {
        Person p = cellData.getValue();
        Boolean v =  (p.getAgeCategory() == Person.AgeCategory.BABY);
        return new ReadOnlyBooleanWrapper(v);
});

// Set a cell factory that will use a CheckBox to render the value
babyCol.setCellFactory(CheckBoxTableCell.<Person>forTableColumn(babyCol));

请浏览 API 文档了解TableCell的其他子类以及如何使用它们。例如,您可以在列的单元格中显示一个带有选项列表的组合框。用户可以选择其中一个选项作为单元格数据。

清单 13-5 有一个完整的程序来展示如何使用定制的细胞工厂。显示如图 13-9 所示的窗口。该程序使用单元格工厂将出生日期格式化为 mm/dd/yyyy 格式,并使用复选框显示一个人是否是婴儿。

img/336502_2_En_13_Fig9_HTML.jpg

图 13-9

使用自定义单元格工厂格式化单元格中的数据并在复选框中显示单元格数据

// TableViewCellFactoryTest.java

// ...find in the book's download area

Listing 13-5Using a Custom Cell Factory for a TableColumn

选择TableView中的单元格和行

TableView具有由其属性selectionModel表示的选择模型。选择模型是TableViewSelectionModel类的一个实例,它是TableView类的一个内部静态类。选择模型支持单元格级和行级选择。它还支持两种选择模式:单个和多个。在单一选择模式下,一次只能选择一个单元格或一行。在多选模式下,可以选择多个单元格或行。默认情况下,单行选择处于启用状态。您可以启用多行选择,如下所示:

TableView<Person> table = ...

// Turn on multiple-selection mode for the TableView
TableViewSelectionModel<Person> tsm = table.getSelectionModel();
tsm.setSelectionMode(SelectionMode.MULTIPLE);

通过将选择模型的cellSelectionEnabled属性设置为 true,可以启用单元格级别的选择,如下面的代码片段所示。当该属性设置为 true 时,TableView被置于单元格级选择模式,并且您不能选择整行。如果启用了多重选择模式,您仍然可以选择一行中的所有单元格。但是,行本身并没有被报告为选中,因为TableView处于单元格级别的选择模式。默认情况下,单元格级别的选择模式为 false。

// Enable cell-level selection
tsm.setCellSelectionEnabled(true);

选择模型提供关于所选单元格和行的信息。如果指定的rowIndex处的行被选中,isSelected(int rowIndex)方法返回true。使用isSelected(int rowIndex, TableColumn<S,?> column)方法了解指定的rowIndex和列中的单元格是否被选中。选择模型提供了几种方法来选择单元格和行,并获得所选单元格和行的报告:

  • selectAll()方法选择所有单元格或行。

  • select()方法被重载。它选择一行、一项的一行和一个单元格。

  • 如果没有选择,isEmpty()方法返回true。否则,它返回false

  • getSelectedCells()方法返回一个只读的ObservableList<TablePosition>,它是当前选中单元格的列表。当TableView中的选择改变时,列表也会改变。

  • getSelectedIndices()方法返回一个只读的ObservableList<Integer>,它是当前选择的索引的列表。当TableView中的选择改变时,列表也会改变。如果启用了行级选择,则列表中的项目是选定行的行索引。如果启用了单元格级别的选择,则列表中的一项是选择了一个或多个单元格的行的行索引。

  • getSelectedItems()方法返回一个只读的ObservableList<S>,其中STableView的通用类型。该列表包含已选择相应行或单元格的所有项目。

  • clearAndSelect()方法被重载。它允许您在选择一行或一个单元格之前清除所有选择。

  • clearSelection()方法被重载。它允许您清除对一行、一个单元格或整个TableView的选择。

TableView中的单元格或行选择发生变化时,通常需要做出一些改变或采取一些行动。例如,TableView可以作为主-详细数据视图中的主列表。当用户在主列表中选择一行时,您希望刷新详细视图中的数据。如果您对处理选择更改事件感兴趣,您需要将一个ListChangeListener添加到前面列出的方法返回的一个ObservableList中,该方法报告选定的单元格或行。下面的代码片段将一个ListChangeListener添加到由getSelectedIndices()方法返回的ObservableList中,以跟踪TableView中的行选择更改:

TableView<Person> table = ...
TableViewSelectionModel<Person> tsm = table.getSelectionModel();
ObservableList<Integer> list = tsm.getSelectedIndices();

// Add a ListChangeListener
list.addListener((ListChangeListener.Change<? extends Integer> change) -> {
        System.out.println("Row selection has changed");
});

编辑TableView中的数据

可以编辑TableView中的单元格。可编辑单元格在编辑和非编辑模式之间切换。在编辑模式下,用户可以修改单元格数据。要使单元格进入编辑模式,TableViewTableColumnTableCell必须是可编辑的。它们都有一个editable属性,可以使用setEditable(true)方法将其设置为 true。默认情况下,TableColumnTableCell是可编辑的。要使单元格在TableView中可编辑,您需要使TableView可编辑:

TableView<Person> table = ...
table.setEditable(true);

TableColumn类支持三种类型的事件:

  • onEditStart

  • onEditCommit

  • onEditCancel

当列中的单元格进入编辑模式时,触发onEditStart事件。当用户成功提交编辑时,例如通过按下TextField中的回车键,触发onEditCommit事件。当用户取消编辑时,例如通过在TextField中按下 Esc 键,触发onEditCancel事件。

事件由一个TableColumn.CellEditEvent类的对象表示。事件对象封装了单元格中的旧值和新值,TableViewTableColumnTablePosition的项目列表中的 row 对象(表示正在进行编辑的单元格位置)以及TableView的引用。使用CellEditEvent类的方法获得这些值。

使TableView可编辑并不能让您编辑它的单元格数据。在编辑单元格中的数据之前,您需要做更多的准备工作。单元格编辑功能是通过TableCell类的专门实现提供的。JavaFX 库提供了其中的一些实现。将列的单元格工厂设置为使用以下TableCell实现之一来编辑单元格数据:

  • CheckBoxTableCell

  • ChoiceBoxTableCell

  • ComboBoxTableCell

  • TextFieldTableCell

使用复选框编辑数据

一个CheckBoxTableCell在单元格内呈现一个复选框。通常,它用于表示列中的布尔值。该类提供了一种使用Callback对象将其他类型的值映射到布尔值的方法。如果值为真,则选中该复选框。否则,它将被取消选中。双向绑定用于绑定复选框的选中属性和底层ObservableValue。如果用户更改选择,则基础数据会更新,反之亦然。

Person类中没有布尔属性。您必须通过提供单元格值工厂来创建布尔列,如下面的代码所示。如果一个Person是婴儿,单元格值工厂返回true。否则返回false

TableColumn<Person, Boolean> babyCol = new TableColumn<>("Baby?");
babyCol.setCellValueFactory(cellData -> {
        Person p = cellData.getValue();
        Boolean v =  (p.getAgeCategory() == Person.AgeCategory.BABY);
        return new ReadOnlyBooleanWrapper(v);
});

让细胞工厂使用CheckBoxTableCell很容易。使用forTableColumn()静态方法获取列的单元格工厂:

// Set a CheckBoxTableCell to display the value
babyCol.setCellFactory(CheckBoxTableCell.<Person>forTableColumn(babyCol));

一个CheckBoxTableCell不会触发单元格编辑事件。复选框的 selected 属性被绑定到代表单元格中数据的ObservableValue。如果您对跟踪选择更改事件感兴趣,您需要为单元格的数据添加一个ChangeListener

使用选择框编辑数据

一个ChoiceBoxTableCell呈现一个选择框,在单元格内有一个指定的值列表。列表中值的类型必须与TableColumn的类型相匹配。当单元格未被编辑时,ChoiceBoxTableCell中的数据显示在Label中。编辑单元格时使用ChoiceBox

Person类没有性别属性。您想要向TableView<Person>添加一个性别列,可以使用选择框对其进行编辑。下面的代码片段创建了TableColumn并设置了一个单元格值工厂,它将所有单元格设置为一个空字符串。如果有的话,您可以设置单元格值工厂来使用Person类的性别属性。

// Gender is a String, editable, ComboBox column
TableColumn<Person, String> genderCol = new TableColumn<>("Gender");

// Use an appropriate cell value factory.
// For now, set all cells to an empty string
genderCol.setCellValueFactory(cellData -> new ReadOnlyStringWrapper(""));

您可以使用ChoiceBoxTableCell类的forTableColumn()静态方法创建一个单元格工厂,它使用一个选择框来编辑单元格中的数据。您需要指定要在选择框中显示的项目列表:

// Set a cell factory, so it can be edited using a ChoiceBox
genderCol.setCellFactory(
        ChoiceBoxBoxTableCell.<Person, String>forTableColumn(
               "Male", "Female")
);

当在选择框中选择一个项目时,该项目被设置为基础数据模型。例如,如果列基于域对象中的属性,则选定项将被设置为属性。您可以设置一个当用户选择一个项目时触发的onEditCommit事件处理程序。下面的代码片段为性别列添加了这样一个处理程序,它在标准输出中打印一条消息:

// Add an onEditCommit handler
genderCol.setOnEditCommit(e -> {
        int row = e.getTablePosition().getRow();
        Person person = e.getRowValue();
        System.out.println("Gender changed (" + person.getFirstName() +
               " " + person.getLastName() + ")" + " at row " + (row + 1) +
           ". New value = " + e.getNewValue());
});

单击选定的单元格会将该单元格置于编辑模式。双击未选中的单元格会将该单元格置于编辑模式。将焦点切换到另一个单元格或从列表中选择一项会将编辑单元格置于非编辑模式,当前值显示在Label中。

使用组合框编辑数据

一个ComboBoxTableCell呈现一个组合框,在单元格内有一个指定的值列表。它的工作原理类似于一个ChoiceBoxTableCell。请参考“使用选择框编辑数据”一节了解更多详细信息。

使用文本字段编辑数据

当单元格被编辑时,TextFieldTableCell在单元格内呈现一个TextField,用户可以在其中修改数据。当单元格未被编辑时,它在Label中呈现单元格数据。

单击选中的单元格或双击未选中的单元格会将单元格置于编辑模式,在TextField中显示单元格数据。一旦单元格处于编辑模式,您需要单击TextField(再单击一次!)将插入符号放在TextField中,以便您可以进行更改。注意,编辑一个单元格至少需要三次点击,这对于那些必须编辑大量数据的用户来说是一件痛苦的事情。让我们期待TableView API 的设计者在未来的版本中让数据编辑变得不那么麻烦。

如果您正在编辑单元格数据,请按 Esc 键取消编辑,这将使单元格返回到非编辑模式,并恢复到单元格中的旧数据。如果TableColumn是基于Writable ObservableValue的,按下回车键将数据提交到底层数据模型。

如果您正在使用TextFieldTableCell编辑一个单元格,将焦点移动到另一个单元格,例如,通过单击另一个单元格,取消编辑并将旧值放回单元格中。这不是用户所期望的。目前,这个问题没有简单的解决方法。您必须创建一个TableCell的子类并添加一个焦点改变监听器,这样您就可以在TextField失去焦点时提交数据。

使用TextFieldTableCell类的forTableColumn()静态方法获得一个使用TextField编辑单元格数据的单元格工厂。下面的代码片段展示了如何为 First Name String列执行此操作:

TableColumn<Person, String> fNameCol = new TableColumn<>("First Name");
fNameCol.setCellFactory(TextFieldTableCell.<Person>forTableColumn());

有时,您需要使用TextField来编辑非字符串数据,例如日期。日期可以表示为模型中的LocalDate类的对象。您可能希望在TextField中将它显示为格式化字符串。当用户编辑日期时,您希望将数据作为LocalDate提交给模型。TextFieldTableCell类通过StringConverter支持这种对象到字符串的转换,反之亦然。下面的代码片段为带有StringConverter的出生日期列设置了一个单元格工厂,它将字符串转换为LocalDate,反之亦然。列类型为LocalDate。默认情况下,LocalDateStringConverter采用 mm/dd/yyyy 的日期格式。

TableColumn<Person, LocalDate> birthDateCol = new TableColumn<>("Birth Date");
LocalDateStringConverter converter = new LocalDateStringConverter();
birthDateCol.setCellFactory(
    TextFieldTableCell.<Person, LocalDate>forTableColumn(converter));

清单 13-6 中的程序展示了如何使用不同类型的控件编辑TableView中的数据。TableView包含 Id、名字、姓氏、出生日期、婴儿和性别列。Id 列不可编辑。名、姓和出生日期列使用TextFieldTableCell,因此可以使用TextField进行编辑。Baby 列是不可编辑的计算字段,不受数据模型支持。它使用CheckBoxTableCell来呈现它的值。性别列是可编辑的计算字段。它不受数据模型的支持。它使用一个ComboBoxTableCell在编辑模式下向用户显示一个值列表(男性和女性)。当用户选择一个值时,该值不会保存到数据模型中。它呆在牢房里。添加了一个onEditCommit事件处理程序,在标准输出中打印性别选择。程序显示如图 13-10 所示的窗口,可以看到您已经为所有人选择了一个性别值。正在编辑第五行的出生日期值。

img/336502_2_En_13_Fig10_HTML.jpg

图 13-10

单元格处于编辑模式的TableView

// TableViewEditing.java
// ...find in the book's download area

Listing 13-6Editing Data in a TableView

使用任何控件编辑 TableCell 中的数据

在上一节中,我讨论了如何使用不同的控件编辑TableView单元格中的数据,例如,TextFieldCheckBoxChoiceBox。你可以子类化TableCell来使用任何控件来编辑单元格数据。例如,您可能希望使用DatePicker在日期列的单元格中选择日期,或者使用RadioButtons从多个选项中进行选择。可能性是无穷的。

您需要覆盖TableCell类的四个方法:

  • startEdit()

  • commitEdit()

  • cancelEdit()

  • updateItem()

单元格从非编辑模式转换到编辑模式的startEdit()方法。通常,您可以在带有当前数据的单元格的graphic属性中设置您选择的控件。

当用户操作(例如,按下TextField中的 Enter 键)表明用户已经完成了对单元格数据的修改,并且数据需要保存在底层数据模型中时,就会调用commitEdit()方法。通常,您不需要覆盖这个方法,因为如果TableColumn是基于Writable ObservableValue的,那么修改后的数据将被提交给数据模型。

当用户动作(例如,在TextField中按下 Esc 键)表明用户想要取消编辑过程时,调用cancelEdit()方法。当编辑过程取消时,单元格返回到非编辑模式。您需要重写此方法,并将单元格数据恢复为它们的旧值。

当单元格需要再次呈现时,调用updateItem()方法。根据编辑模式的不同,您需要适当地设置单元格的文本和图形属性。

现在让我们开发一个继承自TableCell类的DatePickerTableCell类。当你想用一个DatePicker控件编辑一个TableColumn的单元格时,你可以使用DatePickerTableCell的实例。TableColumn必须是LocalDate的。清单 13-7 有DatePickerTableCell类的完整代码。

// DatePickerTableCell.java
// ...find in the book's download area

Listing 13-7The DatePickerTableCell Class to Allow Editing Table Cells Using a DatePicker Control

DatePickerTableCell类支持StringConverterDatePicker的可编辑属性值。您可以将它们传递给构造器或forTableColumn()方法。当第一次调用startEdit()方法时,它会创建一个DatePicker控件。添加了一个ChangeListener,它在输入或选择新日期时提交数据。提供了几个版本的静态方法来返回单元工厂。以下代码片段显示了如何使用DatePickerTableCell类:

TableColumn<Person, LocalDate> birthDateCol = ...

// Set a cell factory for birthDateCol. The date format is mm/dd/yyyy
// and the DatePicker is editable.
birthDateCol.setCellFactory(DatePickerTableCell.<Person>forTableColumn());

// Set a cell factory for birthDateCol. The date format is "Month day, year"
// and and the DatePicker is non-editable
StringConverter converter = new LocalDateStringConverter("MMMM dd, yyyy");
birthDateCol.setCellFactory(DatePickerTableCell.<Person>forTableColumn(
    converter, false));

清单 13-8 中的程序使用DatePickerTableCell来编辑出生日期列单元格中的数据。运行应用程序,然后双击出生日期列中的单元格。该单元格将显示一个DatePicker控件。您不能编辑DatePicker中的日期,因为它是不可编辑的。您需要从弹出日历中选择一个日期。

// CustomTableCellTest.java
// ...find in the book's download area

Listing 13-8Using DatePickerTableCell to Edit a Date in Cells

TableView中添加和删除行

TableView中添加和删除行很容易。注意,TableView中的每一行都由项目列表中的一个项目支持。添加一行就像在项目列表中添加一个项目一样简单。当您向项目列表中添加项目时,在TableView中会出现一个新行,其索引与项目列表中已添加项目的索引相同。如果TableView已排序,则在添加新行后可能需要重新排序。增加一行后,调用TableViewsort()方法对行进行重新排序。

您可以通过从项目列表中移除项目来删除行。应用程序为用户提供了一种指示应该删除哪些行的方法。通常,用户选择一行或多行来删除。其他选项是为每一行添加一个删除按钮,或者为每一行提供一个删除复选框。单击删除按钮应该会删除该行。选中某行的“删除”复选框表示该行被标记为删除。

清单 13-9 中的程序展示了如何在TableView中添加和删除行。它显示一个包含三个部分的窗口:

  • 顶部的Add Person表单有三个用于添加个人详细信息的字段和一个 add 按钮。输入一个人的详细信息,然后单击 Add 按钮向TableView添加一条记录。代码中跳过了错误检查。

  • 中间有两个按钮。一个按钮用于恢复TableView中的默认行。另一个按钮删除选定的行。

  • 在底部,显示一个带有一些行的TableView。启用多行选择。用鼠标按住 Ctrl 或 Shift 键选择多行。

// TableViewAddDeleteRows.java
// ...find in the book's download area

Listing 13-9Adding and Deleting Rows in a TableView

代码中的大部分逻辑很简单。deleteSelectedRows()方法实现了删除所选行的逻辑。从项目列表中删除项目时,选择模型不会删除其索引。假设选择了第一行。如果从“项目”列表中删除第一个项目,将选择第二行,即第一行。为了确保不会发生这种情况,在将行从项目列表中移除之前,请清除该行的选择。您从最后到第一(从高索引到低索引)删除行,因为当您从列表中删除一个项目时,被删除项目之后的所有项目将具有不同的索引。假设您选择了索引 1 和索引 2 处的行。删除索引 1 处的行首先会将索引 2 的索引更改为 1。从最后到第一个执行删除可以解决这个问题。

表格视图中滚动

当行或列超出可用空间时,自动提供垂直和水*滚动条。用户可以使用滚动条滚动到特定的行或列。有时,您需要滚动的编程支持。例如,当您将一行追加到一个TableView中时,您可能希望通过将该行滚动到视图中来让用户看到它。TableView类包含四种方法,可以用来滚动到特定的行或列:

  • scrollTo(int rowIndex)

  • scrollTo(S item)

  • scrollToColumn(TableColumn<S,?> column)

  • scrollToColumnIndex(int columnIndex)

scrollTo()方法将带有指定索引或项目的行滚动到视图中。scrollToColumn()scrollToColumnIndex(方法分别滚动到指定的列和columnIndex

当请求使用上述滚动方法之一滚动到一行或一列时,TableView触发一个ScrollToEventScrollToEvent类包含一个getScrollTarget()方法,根据滚动类型返回行索引或列引用:

TableView<Person> table = ...

// Add a ScrollToEvent for row scrolling
table.setOnScrollTo(e -> {
        int rowIndex = e.getScrollTarget();
        System.out.println("Scrolled to row " + rowIndex);
});

// Add a ScrollToEvent for column scrolling
table.setOnScrollToColumn(e -> {
        TableColumn<Person, ?> column = e.getScrollTarget();
        System.out.println("Scrolled to column " + column.getText());
});

Tip

当用户滚动行和列时,不会触发ScrollToEvent。当您调用TableView类的四个滚动相关方法之一时,它被触发。

调整表格列的大小

用户是否可以调整一个TableColumn的大小是由它的resizable属性决定的。默认情况下,TableColumn是可调整大小的。如何调整TableView中一列的大小由TableViewcolumnResizePolicy属性指定。该属性是一个Callback对象。它的call()方法接受ResizeFeatures类的一个对象,该对象是TableView类的一个静态内部类。ResizeFeatures对象封装了调整列大小的增量、TableColumnTableView。如果成功地按增量调整了列的大小,call()方法将返回true。否则,返回false

TableView类提供了两个内置的调整大小策略作为常量:

  • CONSTRAINED_RESIZE_POLICY

  • UNCONSTRAINED_RESIZE_POLICY

CONSTRAINED_RESIZE_POLICY确保所有可见叶列的宽度之和等于TableView的宽度。调整列的大小会调整调整后的列右侧所有列的宽度。当列宽增加时,最右边一列的宽度会减少到其最小宽度。如果增加的宽度仍未得到补偿,最右边第二列的宽度将减少到其最小宽度,依此类推。当右边的所有列都达到其最小宽度时,列宽就不能再增加了。当调整列的大小以减小其宽度时,相同的规则适用于相反的方向。

当一列的宽度增加时,UNCONSTRAINED_RESIZE_POLICY将所有列向右移动宽度增加的量。当宽度减小时,右边的列向左移动相同的量。如果某列有嵌套列,则调整该列的大小会在直接子列之间均匀分布增量。这是TableView的默认列调整策略:

TableView<Person> table = ...;

// Set the column resize policy to constrained resize policy
table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);

您还可以创建自定义的列大小调整策略。以下代码片段将作为模板。您需要编写消耗 delta 的逻辑,delta 是列的新旧宽度之差:

TableView<Person> table = new TableView<>(PersonTableUtil.getPersonList());
table.setColumnResizePolicy(resizeFeatures -> {
    boolean consumedDelta = false; double delta = resizeFeatures.getDelta();
    TableColumn<Person, ?> column = resizeFeatures.getColumn();
    TableView<Person> tableView = resizeFeatures.getTable();

    // Adjust the delta here...

    return consumedDelta;
});

您可以通过设置一个不起任何作用的简单回调来禁用列大小调整。它的call()简单地返回true,表明它已经消耗了增量:

// Disable column resizing
table.setColumnResizePolicy(resizeFeatures -> true);

用 CSS 设计一个TableView

您可以样式化一个TableView及其所有部分,例如,列标题、单元格、占位符等等。将 CSS 应用到TableView非常复杂,范围也很广。这一部分简要概述了TableView的 CSS 样式。一个TableView的默认 CSS 样式类名是table-view。单元格、行和列标题的默认 CSS 样式类分别是table-celltable-row-cellcolumn-header:

/* Set the font for the cells */
.table-row-cell {
        -fx-font-size: 10pt;
        -fx-font-family: Arial;
}

/* Set the font size and text color for column headers */
.table-view .column-header .label{
        -fx-font-size: 10pt;
        -fx-text-fill: blue;
}

TableView支持以下 CSS 伪类:

  • cell-selection

  • row-selection

当单元格级别的选择被启用时,cell-selection伪类被应用,而row-selection伪类被应用于行级别的选择。当列调整策略为CONSTRAINED_RESIZE_POLICY时,应用constrained-resize伪类。

默认情况下,TableView中的交替行被高亮显示。下面的代码删除了替代行的突出显示。它为所有行设置白色背景色:

.table-row-cell {
    -fx-background-color: white;
}

.table-row-cell .table-cell {
        -fx-border-width: 0.25px;
        -fx-border-color: transparent gray gray transparent;
}

TableView显示空行以填充其可用高度。下面的代码删除空行。事实上,这使它们看起来像被移走了:

.table-row-cell:empty {
        -fx-background-color: transparent;
}

.table-row-cell:empty .table-cell {
        -fx-border-width: 0px;
}

TableView包含几个可以单独设计风格的子结构:

  • column-resize-line

  • column-overlay

  • placeholder

  • column-header-background

column-resize-line substructure是一个Region,当用户试图调整列大小时显示。column-overlay substructure是一个Region,显示为正在移动的列的覆盖图。placeholder substructure是一个StackPane,当TableView没有列或数据时显示,如以下代码所示:

/* Make the text in the placeholder red and bold */
.table-view .placeholder .label {
        -fx-text-fill: red;
        -fx-font-weight: bold;
}

column-header-background子结构是一个StackPane,是列标题后面的区域。它包含几个子结构。它的填充子结构是一个Region,是最右边的列和标题区域中TableView右边缘之间的区域。它的 show-hide-columns-button 子结构是一个StackPane,是显示菜单按钮的区域,用来显示要显示和隐藏的列的列表。请参考modena.css文件和 JavaFX CSS 参考指南以获得可以被样式化的TableView属性的完整列表。下面的代码将填充背景设置为白色:

/* Set the filler background to white*/
.table-view .column-header-background .filler {
        -fx-background-color: white;
}

摘要

TableView是一个用于以表格形式显示和编辑数据的控件。一个TableView由行和列组成。行和列的交叉点称为单元格。单元格包含数据值。列的标题描述了它们包含的数据类型。列可以嵌套。调整列数据的大小和排序具有内置支持。以下类别用于使用TableView控件:TableViewTableColumnTableRowTableCellTablePositionTableView.TableViewFocusModelTableView.TableViewSelectionModelTableView类代表一个TableView控件。TableColumn类表示TableView中的一列。通常,一个TableView包含多个TableColumn实例。一个TableColumn由单元格组成,这些单元格是TableCell类的实例。一个TableColumn负责显示和编辑其单元格中的数据。一个TableColumn有一个可以显示标题文本和/或图形的标题。您可以为TableColumn创建一个上下文菜单,当用户在列标题中单击鼠标右键时,就会显示这个菜单。使用contextMenu属性设置一个上下文菜单。

TableRow类继承自IndexedCell类。一个TableRow的实例代表一个TableView中的一行。除非您想为行提供一个定制的实现,否则您几乎从不在应用程序中使用这个类。通常,您自定义单元格,而不是行。

TableCell类的一个实例代表了TableView中的一个单元格。单元格是高度可定制的。它们为TableView显示来自底层数据模型的数据。它们能够显示数据和图形。TableView行中的单元格包含与一个项目相关的数据,比如一个人、一本书等等。一行中某些单元格的数据可能直接来自该项目的属性,也可能是计算出来的。

TableView有一个ObservableList<S>类型的items属性。通用类型STableView的通用类型相同。它是TableView的数据模型。项目列表中的每个元素代表TableView中的一行。向条目列表添加新条目会向TableView添加新行。从项目列表中删除项目会从TableView中删除相应的行。

TableColumnTableRowTableCell类包含一个tableView属性,该属性保存对包含它们的TableView的引用。当TableColumn不属于某个TableView时,tableView属性包含null

一个TablePosition代表一个单元格的位置。它的getRow()getColumn()方法分别返回单元格所属的行和列的索引。

TableViewFocusModel类是TableView类的内部静态类。它代表了TableView管理行和单元格焦点的焦点模型。

TableViewSelectionModel类是TableView类的内部静态类。它代表了TableView管理行和单元格选择的选择模型。

默认情况下,TableView中的所有列都是可见的。TableColumn类有一个visible属性来设置列的可见性。如果关闭父列(具有嵌套列的列)的可见性,其所有嵌套列都将不可见。

有两种方法可以重新排列TableView中的列:将列拖放到不同的位置,或者改变它们在由TableView类的getColumns()方法返回的可观察列表中的位置。默认情况下,第一个选项可用。

TableView内置了对列中数据排序的支持。默认情况下,它允许用户通过单击列标题对数据进行排序。它还支持以编程方式对数据进行排序。您还可以对TableView中的一列或所有列禁用排序。

TableView支持多层次定制。它允许您定制列的呈现,例如,您可以使用复选框、组合框或TextField在列中显示数据。你也可以使用 CSS 样式化一个TableView

下一章将讨论 2D 形状以及如何将它们添加到场景中。

Tip

本书省略了上一版的TreeViewTreeTableView章节。这些控件的处理非常类似于表格视图,并且章节非常大,所以为了将这个版本保持在一个合理的范围内,在附录中有一个对树控件的简明介绍。

十四、理解 2D 形状

在本章中,您将学习:

  • 什么是 2D 图形,它们在 JavaFX 中是如何表示的

  • 如何画 2D 图形

  • 如何使用Path类绘制复杂形状

  • 如何使用可缩放矢量图形(SVG)绘制形状

  • 如何组合形状以构建另一个形状

  • 如何为形状使用笔画

  • 如何使用级联样式表(CSS)设置形状样式

本章的例子在com.jdojo.shape包中。为了让它们工作,您必须在module-info.java文件中添加相应的一行:

...
opens com.jdojo.shape to javafx.graphics, javafx.base;
...

什么是 2D 形状?

任何能在二维*面上画出的形状都叫做 2D 形状。JavaFX 提供了各种节点来绘制不同类型的形状(线条、圆形、矩形等)。).您可以将形状添加到场景图。

形状可以是二维的,也可以是三维的。在这一章,我将讨论 2D 形状。第十六章讨论 3D 形状。

所有形状类都在javafx.scene.shape包中。表示 2D 形状的类继承自抽象的Shape类,如图 14-1 所示。

img/336502_2_En_14_Fig1_HTML.png

图 14-1

表示 2D 形状的类的类图

形状有大小和位置,这是由它们的属性定义的。例如,widthheight属性定义矩形的大小,radius属性定义圆的大小,xy属性定义矩形左上角的位置,centerXcenterY属性定义圆心,等等。

在布局过程中,父形状不会调整形状的大小。只有当形状的与大小相关的属性改变时,形状的大小才会改变。您可能会发现类似“JavaFX 形状是不可调整的”这样的短语这意味着在布局过程中,形状不可被其父对象改变大小。它们只能通过更改属性来调整大小。

形状有内部和描边。定义形状内部和笔画的属性在Shape类中声明。属性指定了填充 ?? 内部的颜色。默认填充为Color.BLACKstroke属性指定轮廓线条的颜色,默认为null,除了LinePolylinePath默认为strokestrokeWidth属性指定轮廓的宽度,默认为 1.0px。Shape类包含其他与笔画相关的属性,我将在“理解形状的笔画”一节中讨论这些属性

Shape类包含一个smooth属性,默认为真。其 true 值指示应该使用抗锯齿提示来呈现形状。如果设置为 false,将不使用抗锯齿提示,这可能会导致形状的边缘不清晰。

清单 14-1 中的程序创建了两个圆。第一个圆有浅灰色填充,没有描边,这是默认设置。第二个圆圈有黄色填充和 2.0 像素宽的黑色描边。图 14-2 显示了两个圆。

img/336502_2_En_14_Fig2_HTML.jpg

图 14-2

具有不同填充和描边的两个圆

// ShapeTest.java
package com.jdojo.shape;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;

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

        @Override
        public void start(Stage stage) {
            // Create a circle with a light gray fill and no stroke
            Circle c1 = new Circle(40, 40, 40);
            c1.setFill(Color.LIGHTGRAY);

            // Create a circle with an yellow fill and a black stroke
            // of 2.0px
            Circle c2 = new Circle(40, 40, 40);
            c2.setFill(Color.YELLOW);
            c2.setStroke(Color.BLACK);
            c2.setStrokeWidth(2.0);

            HBox root = new HBox(c1, c2);
            root.setSpacing(10);
            root.setStyle("""
               -fx-padding: 10;
               -fx-border-style: solid inside;
               -fx-border-width: 2;
               -fx-border-insets: 5;
               -fx-border-radius: 5;
               -fx-border-color: blue;""");

            Scene scene = new Scene(root);
            stage.setScene(scene);
            stage.setTitle("Using Shapes");
            stage.show();
        }
}

Listing 14-1Using fill and stroke Properties of the Shape Class

画 2D 图形

以下部分详细描述了如何使用代表 2D 形状的 JavaFX 类来绘制这些形状。

画线

Line类的一个实例代表一个线节点。一辆Line没有内饰。默认情况下,它的fill属性设置为null。设置fill没有效果。默认strokeColor.BLACK,默认strokeWidth为 1.0。Line类包含四个 double 属性:

  • startX

  • startY

  • endX

  • endY

Line表示(startX, startY)(endX, endY)点之间的线段。Line类有一个无参数构造器,它将所有四个属性默认为零,得到一条从(0,0)到(0,0)的线,表示一个点。另一个构造器接受startXstartYendXendY的值。创建了Line之后,可以通过改变四个属性中的任何一个来改变它的位置和长度。

清单 14-2 中的程序创建一些Line并设置它们的strokestrokeWidth属性。第一个Line将显示为一个点。图 14-3 为线条。

img/336502_2_En_14_Fig3_HTML.jpg

图 14-3

使用线节点

// LineTest.java
package com.jdojo.shape;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Line;
import javafx.stage.Stage;

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

        @Override

        public void start(Stage stage) {
            // It will be just a point at (0, 0)
            Line line1 = new Line();

            Line line2 = new Line(0, 0, 50, 0);
            line2.setStrokeWidth(1.0);

            Line line3 = new Line(0, 50, 50, 0);
            line3.setStrokeWidth(2.0);
            line3.setStroke(Color.RED);

            Line line4 = new Line(0, 0, 50, 50);
            line4.setStrokeWidth(5.0);
            line4.setStroke(Color.BLUE);

            HBox root = new HBox(line1, line2, line3, line4);
            root.setSpacing(10);
            root.setStyle("""
               -fx-padding: 10;
               -fx-border-style: solid inside;
               -fx-border-width: 2;
               -fx-border-insets: 5;
               -fx-border-radius: 5;
               -fx-border-color: blue;""");

            Scene scene = new Scene(root);
            stage.setScene(scene);
            stage.setTitle("Using Lines");
            stage.show();
        }
}

Listing 14-2Using the Line Class to Create Line Nodes

绘制矩形

Rectangle类的一个实例表示一个矩形节点。该类使用六个属性来定义矩形:

  • x

  • y

  • width

  • height

  • arcWidth

  • arcHeight

xy属性是矩形左上角在节点局部坐标系中的 x 和 y 坐标。widthheight属性分别是矩形的宽度和高度。指定相同的宽度和高度来绘制正方形。

默认情况下,矩形的角是尖锐的。通过指定arcWidtharcHeight属性,矩形可以有圆角。你可以把一个椭圆的一个象限定位在四个角上,使它们变圆。arcWidtharcHeight属性是椭圆的水*和垂直直径。默认情况下,它们的值为零,这使得矩形具有尖角。图 14-4 显示了两个矩形——一个带尖角,一个带圆角。显示椭圆是为了说明圆角矩形的arcWidtharcHeight属性之间的关系。

img/336502_2_En_14_Fig4_HTML.png

图 14-4

带尖角和圆角的矩形

Rectangle类包含几个构造器。它们将各种属性作为参数。xywidthheightarcWidtharcHeight属性的默认值为零。构造器是

  • Rectangle()

  • Rectangle(double width, double height)

  • Rectangle(double x, double y, double width, double height)

  • Rectangle(double width, double height, Paint fill)

当您将一个Rectangle添加到大多数布局窗格中时,您将看不到为其指定xy属性值的效果,因为它们将子元素放置在(0,0)处。A Pane使用这些属性。清单 14-3 中的程序将两个矩形添加到一个Pane中。第一个矩形使用 x 和 y 属性的默认值零。第二个矩形为x属性指定 120,为y属性指定 20。图 14-5 显示了Pane内两个矩形的位置。请注意,第二个矩形(右侧)的左上角位于(120,20)。

img/336502_2_En_14_Fig5_HTML.jpg

图 14-5

窗格内的矩形,它使用 x 和 y 属性来定位它们

// RectangleTest.java
package com.jdojo.shape;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;

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

        @Override
        public void start(Stage stage) {
            // x=0, y=0, width=100, height=50, fill=LIGHTGRAY, stroke=null
            Rectangle rect1 = new Rectangle(100, 50, Color.LIGHTGRAY);

            // x=120, y=20, width=100, height=50, fill=WHITE, stroke=BLACK
            Rectangle rect2 = new Rectangle(120, 20, 100, 50);
            rect2.setFill(Color.WHITE);
            rect2.setStroke(Color.BLACK);
            rect2.setArcWidth(10);
            rect2.setArcHeight(10);

            Pane root = new Pane();
            root.getChildren().addAll(rect1, rect2);
            Scene scene = new Scene(root);
            stage.setScene(scene);
            stage.setTitle("Using Rectangles");
            stage.show();
        }
}

Listing 14-3Using the Rectangle Class to Create Rectangle Nodes

画圆

Circle类的一个实例代表一个圆形节点。该类使用三个属性来定义圆:

  • centerX

  • centerY

  • radius

centerXcenterY属性是圆心在节点局部坐标系中的 x 和 y 坐标。radius属性是圆的半径。这些属性的默认值为零。

Circle类包含几个构造器:

  • Circle()

  • Circle(double radius)

  • Circle(double centerX, double centerY, double radius)

  • Circle(double centerX, double centerY, double radius, Paint fill)

  • Circle(double radius, Paint fill)

清单 14-4 中的程序给一个HBox增加了两个圆。注意HBox没有使用圆的centerXcenterY属性。将它们添加到一个Pane来观察效果。图 14-6 显示了两个圆。

img/336502_2_En_14_Fig6_HTML.jpg

图 14-6

使用圆形节点

// CircleTest.java
package com.jdojo.shape;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;

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

        @Override
        public void start(Stage stage) {
            // centerX=0, centerY=0, radius=40, fill=LIGHTGRAY,
            // stroke=null
            Circle c1 = new Circle(0, 0, 40);
            c1.setFill(Color.LIGHTGRAY);

            // centerX=10, centerY=10, radius=40\. fill=YELLOW,
            // stroke=BLACK
            Circle c2 = new Circle(10, 10, 40, Color.YELLOW);
            c2.setStroke(Color.BLACK);
            c2.setStrokeWidth(2.0);

            HBox root = new HBox(c1, c2);
            root.setSpacing(10);
            root.setStyle("""
               -fx-padding: 10;
               -fx-border-style: solid inside;
               -fx-border-width: 2;
               -fx-border-insets: 5;
               -fx-border-radius: 5;
               -fx-border-color: blue;""");

            Scene scene = new Scene(root);
            stage.setScene(scene);
            stage.setTitle("Using Circle");
            stage.show();
        }
}

Listing 14-4Using the Circle Class to Create Circle Nodes

绘制椭圆

Ellipse类的一个实例表示一个椭圆节点。该类使用四个属性来定义椭圆:

  • centerX

  • centerY

  • radiusX

  • radiusY

centerXcenterY属性是圆心在节点局部坐标系中的 x 和 y 坐标。radiusXradiusY是椭圆在水*和垂直方向上的半径。这些属性的默认值为零。当radiusXradiusY相同时,圆是椭圆的特例。

Ellipse类包含几个构造器:

  • Ellipse()

  • Ellipse(double radiusX, double radiusY)

  • Ellipse(double centerX, double centerY, double radiusX, double radiusY)

清单 14-5 中的程序创建了Ellipse类的三个实例。第三个实例画了一个圆,因为程序为radiusXradiusY属性设置了相同的值。图 14-7 显示了三个椭圆。

img/336502_2_En_14_Fig7_HTML.jpg

图 14-7

使用椭圆节点

// EllipseTest.java
package com.jdojo.shape;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Ellipse;
import javafx.stage.Stage;

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

        @Override
        public void start(Stage stage) {
            Ellipse e1 = new Ellipse(50, 30);
            e1.setFill(Color.LIGHTGRAY);

            Ellipse e2 = new Ellipse(60, 30);
            e2.setFill(Color.YELLOW);
            e2.setStroke(Color.BLACK);
            e2.setStrokeWidth(2.0);

            // Draw a circle using the Ellipse class (radiusX=radiusY=30)
            Ellipse e3 = new Ellipse(30, 30);
            e3.setFill(Color.YELLOW);
            e3.setStroke(Color.BLACK);
            e3.setStrokeWidth(2.0);

            HBox root = new HBox(e1, e2, e3);
            root.setSpacing(10);
            root.setStyle("""
               -fx-padding: 10;
               -fx-border-style: solid inside;
               -fx-border-width: 2;
               -fx-border-insets: 5;
               -fx-border-radius: 5;
               -fx-border-color: blue;""");

            Scene scene = new Scene(root);
            stage.setScene(scene);
            stage.setTitle("Using Ellipses");
            stage.show();
        }

}

Listing 14-5Using the Ellipse Class to Create Ellipse Nodes

绘制多边形

Polygon类的一个实例代表一个多边形节点。该类不定义任何公共属性。它允许您使用定义多边形顶点的(x,y)坐标数组来绘制多边形。使用Polygon类,你可以绘制任何类型的几何形状,这些几何形状是使用连接线创建的(三角形、五边形、六边形、*行四边形等)。).

Polygon类包含两个构造器:

  • Polygon()

  • Polygon(double... points)

无参数构造器创建一个空多边形。您需要添加形状顶点的(x,y)坐标。多边形将从第一个顶点到第二个顶点、从第二个顶点到第三个顶点等等绘制一条线。最后,通过从最后一个顶点到第一个顶点画一条线来闭合形状。

Polygon类将顶点的坐标存储在一个ObservableList<Double>中。您可以使用getPoints()方法获得可观察列表的引用。注意,它将坐标存储在一个列表Double中,这个列表只是一个数字。您的工作是成对传递数字,因此它们可以用作顶点的(x,y)坐标。如果传递奇数个数字,则不会创建任何形状。下面的代码片段创建了两个三角形——一个在构造器中传递顶点的坐标,另一个稍后将它们添加到可观察列表中。两个三角形在几何上是相同的:

// Create an empty triangle and add vertices later
Polygon triangle1 = new Polygon();
triangle1.getPoints().addAll(50.0, 0.0,
                      0.0, 100.0,
                      100.0, 100.0);

// Create a triangle with vertices
Polygon triangle2 = new Polygon(50.0, 0.0,
                        0.0, 100.0,

                        100.0, 100.0);

清单 14-6 中的程序使用Polygon类创建了一个三角形、一个*行四边形和一个六边形,如图 14-8 所示。

img/336502_2_En_14_Fig8_HTML.jpg

图 14-8

使用多边形节点

// PolygonTest.java
package com.jdojo.shape;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Polygon;
import javafx.stage.Stage;

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

        @Override
        public void start(Stage stage) {
            Polygon triangle1 = new Polygon();
            triangle1.getPoints().addAll(50.0, 0.0,
                0.0, 50.0,
                100.0, 50.0);
            triangle1.setFill(Color.WHITE);
            triangle1.setStroke(Color.RED);

            Polygon parallelogram = new Polygon();
            parallelogram.getPoints().addAll(
               30.0, 0.0,
               130.0, 0.0,
               100.00, 50.0,
               0.0, 50.0);
            parallelogram.setFill(Color.YELLOW);
            parallelogram.setStroke(Color.BLACK);

            Polygon hexagon = new Polygon(
                100.0, 0.0,
                120.0, 20.0,
                120.0, 40.0,
                100.0, 60.0,
                80.0, 40.0,
                80.0, 20.0);
            hexagon.setFill(Color.WHITE);
            hexagon.setStroke(Color.BLACK);

            HBox root = new HBox(triangle1, parallelogram, hexagon);
            root.setSpacing(10);
            root.setStyle("""
               -fx-padding: 10;
               -fx-border-style: solid inside;
               -fx-border-width: 2;
               -fx-border-insets: 5;
               -fx-border-radius: 5;
               -fx-border-color: blue;""");

            Scene scene = new Scene(root);
            stage.setScene(scene);
            stage.setTitle("Using Polygons");
            stage.show();
        }

}

Listing 14-6Using the Polygon Class to Create a Triangle, a Parallelogram, and a Hexagon

绘制多段线

折线类似于多边形,只是它不在最后一点和第一点之间绘制直线。也就是说,折线是一个开放的多边形。然而,fill颜色用于填充整个形状,就好像该形状是闭合的一样。

Polyline类的一个实例代表一个折线节点。该类不定义任何公共属性。它允许您使用定义折线顶点的(x,y)坐标数组来绘制折线。使用Polyline类,你可以绘制任何类型的几何形状,这些几何形状是使用连接线创建的(三角形、五边形、六边形、*行四边形等)。).

Polyline类包含两个构造器:

  • Polyline()

  • Polyline(double... points)

无参数构造器创建一条空折线。您需要添加形状顶点的(x,y)坐标。多边形将从第一个顶点到第二个顶点、从第二个顶点到第三个顶点等等绘制一条线。与Polygon不同,该形状不会自动闭合。如果要闭合形状,需要添加第一个顶点的坐标作为最后一对数字。

如果您想稍后添加顶点的坐标,请将它们添加到由Polyline类的getPoints()方法返回的ObservableList<Double>中。下面的代码片段使用不同的方法创建了两个具有相同几何属性的三角形。请注意,为了闭合三角形,第一对和最后一对数字是相同的:

// Create an empty triangle and add vertices later
Polygon triangle1 = new Polygon();
triangle1.getPoints().addAll(
    50.0, 0.0,
    0.0, 100.0,
    100.0, 100.0,
    50.0, 0.0);

// Create a triangle with vertices
Polygon triangle2 = new Polygon(
    50.0, 0.0,
    0.0, 100.0,
    100.0, 100.0,
    50.0, 0.0);

清单 14-7 中的程序使用Polyline类创建了一个三角形、一个开放的*行四边形和一个六边形,如图 14-9 所示。

img/336502_2_En_14_Fig9_HTML.jpg

图 14-9

使用折线节点

// PolylineTest.java
package com.jdojo.shape;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Polyline;
import javafx.stage.Stage;

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

        @Override
        public void start(Stage stage) {
            Polyline triangle1 = new Polyline();
            triangle1.getPoints().addAll(
                50.0, 0.0,
                0.0, 50.0,
                100.0, 50.0,
                50.0, 0.0);
            triangle1.setFill(Color.WHITE);
            triangle1.setStroke(Color.RED);

            // Create an open parallelogram

            Polyline parallelogram = new Polyline();
            parallelogram.getPoints().addAll(
                30.0, 0.0,
                130.0, 0.0,
                100.00, 50.0,
                0.0, 50.0);
            parallelogram.setFill(Color.YELLOW);
            parallelogram.setStroke(Color.BLACK);

            Polyline hexagon = new Polyline(
               100.0, 0.0,
               120.0, 20.0,
               120.0, 40.0,
               100.0, 60.0,
               80.0, 40.0,
               80.0, 20.0,
               100.0, 0.0);
            hexagon.setFill(Color.WHITE);
            hexagon.setStroke(Color.BLACK);

            HBox root = new HBox(triangle1, parallelogram, hexagon);
            root.setSpacing(10);
            root.setStyle("""
               -fx-padding: 10;
               -fx-border-style: solid inside;
               -fx-border-width: 2;
               -fx-border-insets: 5;
               -fx-border-radius: 5;
               -fx-border-color: blue;""");

            Scene scene = new Scene(root);
            stage.setScene(scene);
            stage.setTitle("Using Polylines");
            stage.show();
        }
}

Listing 14-7Using the Polyline Class to Create a Triangle, an Open Parallelogram, and a Hexagon

画弧线

Arc类的一个实例代表一个椭圆的一部分。该类使用七个属性来定义椭圆:

  • centerX

  • centerY

  • radiusX

  • radiusY

  • startAngle

  • length

  • type

前四个属性定义了一个椭圆。关于如何定义椭圆,请参考“绘制椭圆”一节。最后三个属性定义了椭圆的一个扇区,即Arc节点。startAngle属性指定从 x 轴正方向逆时针测量的部分的起始角度,单位为度。它定义了弧的起点。length是一个角度,以度为单位,从开始角度逆时针测量,以定义扇形的结束。如果length属性设置为 360 度,则Arc是一个完整的椭圆。图 14-10 说明了这些特性。

img/336502_2_En_14_Fig10_HTML.png

图 14-10

定义弧的属性

type 属性指定了关闭Arc的方式。它是ArcType枚举中定义的OPENCHORDROUND常量之一:

  • ArcType.OPEN不关闭圆弧。

  • ArcType.CHORD通过用直线连接起点和终点来闭合圆弧。

  • ArcType.ROUND通过将起点和终点连接到椭圆的中心来闭合圆弧。

图 14-11 显示了弧的三种闭合类型。一个Arc的默认类型是ArcType.OPEN。如果不对Arc应用描边,那么ArcType.OPENArcType.CHORD看起来是一样的。

img/336502_2_En_14_Fig11_HTML.png

图 14-11

弧的闭合类型

Arc类包含两个构造器:

  • Arc()

  • Arc(double centerX, double centerY, double radiusX, double radiusY, double startAngle, double length)

清单 14-8 中的程序展示了如何创建Arc节点。产生的窗口如图 14-12 所示。

img/336502_2_En_14_Fig12_HTML.jpg

图 14-12

使用弧形节点

// ArcTest.java
package com.jdojo.shape;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Arc;
import javafx.scene.shape.ArcType;
import javafx.stage.Stage;

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

        @Override
        public void start(Stage stage) {
            // An OPEN arc with a fill
            Arc arc1 = new Arc(0, 0, 50, 100, 0, 90);
            arc1.setFill(Color.LIGHTGRAY);

            // An OPEN arc with no fill and a stroke

            Arc arc2 = new Arc(0, 0, 50, 100, 0, 90);
            arc2.setFill(Color.TRANSPARENT);
            arc2.setStroke(Color.BLACK);

            // A CHORD arc with no fill and a stroke
            Arc arc3 = new Arc(0, 0, 50, 100, 0, 90);
            arc3.setFill(Color.TRANSPARENT);
            arc3.setStroke(Color.BLACK);
            arc3.setType(ArcType.CHORD);

            // A ROUND arc with no fill and a stroke
            Arc arc4 = new Arc(0, 0, 50, 100, 0, 90);
            arc4.setFill(Color.TRANSPARENT);
            arc4.setStroke(Color.BLACK);
            arc4.setType(ArcType.ROUND);

            // A ROUND arc with a gray fill and a stroke
            Arc arc5 = new Arc(0, 0, 50, 100, 0, 90);
            arc5.setFill(Color.GRAY);
            arc5.setStroke(Color.BLACK);
            arc5.setType(ArcType.ROUND);

            HBox root = new HBox(arc1, arc2, arc3, arc4, arc5);
            root.setSpacing(10);
            root.setStyle("""
               -fx-padding: 10;
               -fx-border-style: solid inside;
               -fx-border-width: 2;
               -fx-border-insets: 5;
               -fx-border-radius: 5;
               -fx-border-color: blue;""");

            Scene scene = new Scene(root);
            stage.setScene(scene);
            stage.setTitle("Using Arcs");
            stage.show();
        }
}

Listing 14-8Using the Arc Class to Create Arcs, Which Are Sectors of Ellipses

绘制二次曲线

贝塞尔曲线在计算机图形学中用于绘制*滑曲线。QuadCurve类的一个实例表示使用指定的贝塞尔控制点与两个指定点相交的二次贝塞尔曲线段。QuadCurve class包含六个属性来指定三个点:

  • startX

  • startY

  • controlX

  • controlY

  • endX

  • endY

QuadCurve类包含两个构造器:

  • QuadCurve()

  • QuadCurve(double startX, double startY, double controlX, double controlY, double endX, double endY)

清单 14-9 中的程序绘制了同一个二次贝塞尔曲线两次——一次用笔画和透明填充,一次没有笔画和浅灰色填充。图 14-13 显示了两条曲线。

img/336502_2_En_14_Fig13_HTML.jpg

图 14-13

使用二次贝塞尔曲线

// QuadCurveTest.java
package com.jdojo.shape;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.QuadCurve;
import javafx.stage.Stage;

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

        @Override
        public void start(Stage stage) {
            QuadCurve qc1 = new QuadCurve(0, 100, 20, 0, 150, 100);
            qc1.setFill(Color.TRANSPARENT);
            qc1.setStroke(Color.BLACK);

            QuadCurve qc2 = new QuadCurve(0, 100, 20, 0, 150, 100);
            qc2.setFill(Color.LIGHTGRAY);

            HBox root = new HBox(qc1, qc2);
            root.setSpacing(10);
            root.setStyle("""
               -fx-padding: 10;
               -fx-border-style: solid inside;
               -fx-border-width: 2;
               -fx-border-insets: 5;
               -fx-border-radius: 5;
               -fx-border-color: blue;""");

            Scene scene = new Scene(root);
            stage.setScene(scene);
            stage.setTitle("Using QuadCurves");
            stage.show();
        }
}

Listing 14-9Using the QuadCurve Class to Draw Quadratic BezierCurve

绘制三次曲线

CubicCurve类的一个实例使用两个指定的贝塞尔控制点表示与两个指定点相交的三次贝塞尔曲线段。关于贝塞尔曲线的详细解释和演示,请参考 http://en.wikipedia.org/wiki/Bezier_curves 的维基百科文章。CubicCurve class包含八个属性来指定四个点:

  • startX

  • startY

  • controlX1

  • controlY1

  • controlX2

  • controlY2

  • endX

  • endY

CubicCurve类包含两个构造器:

  • CubicCurve()

  • CubicCurve(double startX, double startY, double controlX1, double controlY1, double controlX2, double controlY2, double endX, double endY)

清单 14-10 中的程序绘制同一个三次贝塞尔曲线两次——一次用笔画和透明填充,一次没有笔画和浅灰色填充。图 14-14 显示了两条曲线。

img/336502_2_En_14_Fig14_HTML.jpg

图 14-14

使用三次贝塞尔曲线

// CubicCurveTest.java
package com.jdojo.shape;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.CubicCurve;
import javafx.stage.Stage;

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

        @Override
        public void start(Stage stage) {
            CubicCurve cc1 = new CubicCurve(0, 50, 20, 0, 50, 80, 50, 0);
            cc1.setFill(Color.TRANSPARENT);
            cc1.setStroke(Color.BLACK);

            CubicCurve cc2 = new CubicCurve(0, 50, 20, 0, 50, 80, 50, 0);
            cc2.setFill(Color.LIGHTGRAY);

            HBox root = new HBox(cc1, cc2);
            root.setSpacing(10);
            root.setStyle("""
               -fx-padding: 10;
               -fx-border-style: solid inside;

               -fx-border-width: 2;
               -fx-border-insets: 5;
               -fx-border-radius: 5;
               -fx-border-color: blue;""");

            Scene scene = new Scene(root);
            stage.setScene(scene);
            stage.setTitle("Using CubicCurves");
            stage.show();
        }
}

Listing 14-10Using the CubicCurve Class to Draw a Cubic Bezier Curve

使用 Path 类构建复杂形状

我在前面的章节中讨论了几个形状类。它们被用来画简单的形状。对于复杂的形状不方便使用它们。您可以使用Path类绘制复杂的形状。Path类的一个实例定义了形状的路径(轮廓)。路径由一个或多个子路径组成。子路径由一个或多个路径元素组成。每个子路径都有一个起点和一个终点。

path 元素是PathElement抽象类的一个实例。PathElement类的以下子类代表特定类型的路径元素:

  • MoveTo

  • LineTo

  • HLineTo

  • VLineTo

  • ArcTo

  • QuadCurveTo

  • CubicCurveTo

  • ClosePath

在您看到一个例子之前,让我们概述一下使用Path类创建一个形状的过程。这个过程类似于用铅笔在纸上画一个形状。首先,你把铅笔放在纸上。你可以重述一遍,“你把铅笔移到纸上的一点。”不管你想画什么形状,移动铅笔到一个点必须是第一步。现在,您开始移动铅笔来绘制路径元素(例如,水*线)。当前路径元素的起点与前一个路径元素的终点相同。根据需要绘制尽可能多的路径元素(例如,垂直线、弧线和二次贝塞尔曲线)。最后,您可以在开始的同一点或其他地方结束最后一个路径元素。

定义PathElement的坐标可以是绝对的,也可以是相对的。默认情况下,坐标是绝对的。它是由PathElement类的absolute属性指定的。如果为 true(这是默认值),则坐标是绝对的。如果为假,则坐标是相对的。绝对坐标是相对于节点的局部坐标系测量的。将前一个PathElement的终点作为原点,测量相对坐标。

Path类包含三个构造器:

  • Path()

  • Path(Collection<? extends PathElement> elements)

  • Path(PathElement... elements)

无参数构造器创建一个空形状。另外两个构造器将路径元素列表作为参数。一个Path在一个ObservableList<PathElement>中存储路径元素。您可以使用getElements()方法获取列表的引用。您可以修改路径元素列表来修改形状。下面的代码片段展示了使用Path类创建形状的两种方法:

// Pass the path elements to the constructor
Path shape1 = new Path(pathElement1, pathElement2, pathElement3);

// Create an empty path and add path elements to the elements list
Path shape2 = new Path();

shape2.getElements().addAll(pathElement1, pathElement2, pathElement3);

Tip

可以同时将一个PathElement实例作为路径元素添加到Path对象中。一个Path对它所有的路径元素使用相同的fillstroke

MoveTo 路径元素

一个MoveTo路径元素用于将指定的 x 和 y 坐标作为当前点。它具有将铅笔提起并放置在纸上指定点的效果。一个Path对象的第一个路径元素必须是一个MoveTo元素,并且不能使用相对坐标。MoveTo类定义了两个double属性,它们是点的 x 和 y 坐标:

  • x

  • y

MoveTo类包含两个构造器。无参数构造器将当前点设置为(0.0,0.0)。另一个构造器将当前点的 x 和 y 坐标作为参数:

// Create a MoveTo path element to move the current point to (0.0, 0.0)
MoveTo mt1 = new MoveTo();

// Create a MoveTo path element to move the current point to (10.0, 10.0)
MoveTo mt2 = new MoveTo(10.0, 10.0);

Tip

路径必须以MoveTo路径元素开始。一个路径中可以有多个MoveTo路径元素。后续的MoveTo元素表示新子路径的起点。

LineTo 路径元素

一个LineTo路径元素从当前点到指定点画一条直线。它包含两个double属性,分别是线条末端的 x 和 y 坐标:

  • x

  • y

LineTo类包含两个构造器。无参数构造器将行尾设置为(0.0,0.0)。另一个构造器将行尾的 x 和 y 坐标作为参数:

// Create a LineTo path element with its end at (0.0, 0.0)
LineTo lt1 = new LineTo();

// Create a LineTo path element with its end at (10.0, 10.0)
LineTo lt2 = new LineTo(10.0, 10.0);

有了MoveToLineTo路径元素的知识,您可以构建仅由线条组成的形状。下面的代码片段创建了一个如图 14-15 所示的三角形。图中显示了三角形及其路径元素。箭头显示了图纸的流向。注意,绘图从(0.0)开始,使用第一个MoveTo路径元素。

img/336502_2_En_14_Fig15_HTML.png

图 14-15

使用 MoveTo 和 LineTo 路径元素创建三角形

Path triangle = new Path(
    new MoveTo(0, 0),
    new LineTo(0, 50),
    new LineTo(50, 50),
    new LineTo(0, 0));

ClosePath path 元素通过从当前点到路径的起点画一条直线来闭合一条路径。如果路径中存在多个MoveTo路径元素,一个ClosePath会从当前点到最后一个MoveTo标识的点绘制一条直线。你可以用一个ClosePath为之前的三角形例子重写路径:

Path triangle = new Path(
    new MoveTo(0, 0),
   new LineTo(0, 50),
   new LineTo(50, 50),
   new ClosePath());

清单 14-11 中的程序创建了两个Path节点:一个三角形和一个带有两个倒三角形的节点,给它一个星形的外观,如图 14-16 所示。在第二个形状中,每个三角形都被创建为一个子路径,每个子路径都以一个MoveTo元素开始。注意ClosePath元素的两种用法。每个ClosePath关闭其子路径。

img/336502_2_En_14_Fig16_HTML.jpg

图 14-16

基于路径元素的形状

// PathTest.java
package com.jdojo.shape;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.HBox;
import javafx.scene.shape.ClosePath;
import javafx.scene.shape.LineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
import javafx.stage.Stage;

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

        @Override
        public void start(Stage stage) {
           Path triangle = new Path(
              new MoveTo(0, 0),
              new LineTo(0, 50),
              new LineTo(50, 50),
              new ClosePath());

            Path star = new Path();
            star.getElements().addAll(
               new MoveTo(30, 0),
               new LineTo(0, 30),
               new LineTo(60, 30),
               new ClosePath(),/* new LineTo(30, 0), */
               new MoveTo(0, 10),
               new LineTo(60, 10),
               new LineTo(30, 40),
               new ClosePath() /*new LineTo(0, 10)*/);

            HBox root = new HBox(triangle, star);
            root.setSpacing(10);
            root.setStyle("""
               -fx-padding: 10;
               -fx-border-style: solid inside;
               -fx-border-width: 2;
               -fx-border-insets: 5;
               -fx-border-radius: 5;
               -fx-border-color: blue;""");

            Scene scene = new Scene(root);
            stage.setScene(scene);
            stage.setTitle("Using Paths");
            stage.show();
        }

}

Listing 14-11Using the Path Class to Create a Triangle and a Star

lineto 和 lineto 路径元素

HLineTo path 元素从当前点到指定的 x 坐标画一条水*线。直线终点的 y 坐标与当前点的 y 坐标相同。HLineTo类的x属性指定了终点的 x 坐标:

// Create an horizontal line from the current point (x, y) to (50, y)
HLineTo hlt = new HLineTo(50);

VLineTo path 元素从当前点到指定的 y 坐标画一条垂直线。直线终点的 x 坐标与当前点的 x 坐标相同。VLineTo类的y属性指定了终点的 y 坐标:

// Create a vertical line from the current point (x, y) to (x, 50)
VLineTo vlt = new VLineTo(50);

Tip

LineTo路径元素是HLineToVLineTo的通用版本。

下面的代码片段创建了与上一节中讨论的相同的三角形。这一次,您使用HLineToVLineTo路径元素来绘制三角形的底边和高边,而不是使用LineTo路径元素:

Path triangle = new Path(
   new MoveTo(0, 0),
   new VLineTo(50),
   new HLineTo(50),
   new ClosePath());

ArcTo 路径元素

一个ArcTo路径元素定义了一段连接当前点和指定点的椭圆。它包含以下属性:

  • radiusX

  • radiusY

  • x

  • y

  • XAxisRotation

  • largeArcFlag

  • sweepFlag

radiusXradiusY属性指定椭圆的水*和垂直半径。xy属性指定圆弧终点的 x 和 y 坐标。请注意,圆弧的起点是路径的当前点。

XAxisRotation属性指定椭圆 x 轴的旋转角度。请注意,旋转是针对从中获得圆弧的椭圆的 x 轴,而不是节点坐标系的 x 轴。正值逆时针旋转 x 轴。

largeArcFlagsweepFlag属性是布尔类型,默认情况下,它们被设置为 false。它们的用途需要详细的解释。两个椭圆可以通过两个给定点,如图 14-17 所示,给我们四条弧线来连接这两点。

img/336502_2_En_14_Fig17_HTML.png

图 14-17

largeArcFlag 和 sweepFlag 属性对 ArcTo path 元素的影响

图 14-17 分别显示了标记为StartEnd的起点和终点。椭圆上的两点可以穿过较大的弧或较小的弧。如果largeArcFlag为真,则使用较大的圆弧。否则,使用较小的圆弧。

当决定使用较大或较小的圆弧时,您仍然有两个选择:将使用两个可能椭圆中的哪个椭圆?这由sweepFlag属性决定。尝试使用两个选定的圆弧(两个较大的圆弧或两个较小的圆弧)绘制从起点到终点的圆弧。对于一个圆弧,遍历将是顺时针方向,而对于另一个圆弧,遍历将是逆时针方向。如果sweepFlag为真,则使用顺时针遍历的椭圆。如果sweepFlag为假,则使用逆时针遍历的椭圆。表 14-1 显示了根据这两个属性将使用哪个椭圆的哪种类型的圆弧。

表 14-1

基于 largeArcFlag 和 sweepFlag 属性选择弧段和椭圆

|

长焦

|

扫雷日

|

弧型

|

椭圆

true true 更大的 椭圆-2
true false 更大的 椭圆-1
false true 较小的 椭圆-1
false false 较小的 椭圆-2

清单 14-12 中的程序使用一个ArcTo路径元素来构建一个Path对象。该程序允许用户改变ArcTo路径元素的属性。运行程序并更改largeArcFlagsweepFlag和其他属性,看看它们如何影响ArcTo路径元素。

// ArcToTest.java
package com.jdojo.shape;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.Slider;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.GridPane;
import javafx.scene.shape.ArcTo;
import javafx.scene.shape.HLineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
import javafx.scene.shape.VLineTo;
import javafx.stage.Stage;

public class ArcToTest extends Application {
        private ArcTo arcTo;

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

        @Override
        public void start(Stage stage) {
            // Create the ArcTo path element
            arcTo = new ArcTo();

            // Use the arcTo element to build a Path
            Path path = new Path(
               new MoveTo(0, 0),
               new VLineTo(100),
               new HLineTo(100),
               new VLineTo(50),
               arcTo);

            BorderPane root = new BorderPane();
            root.setTop(this.getTopPane());
            root.setCenter(path);
            root.setStyle("""
               -fx-padding: 10;
               -fx-border-style: solid inside;

               -fx-border-width: 2;
               -fx-border-insets: 5;
               -fx-border-radius: 5;
               -fx-border-color: blue;""");

            Scene scene = new Scene(root);
            stage.setScene(scene);
            stage.setTitle("Using ArcTo Path Elements");
            stage.show();
        }

        private GridPane getTopPane() {
            CheckBox largeArcFlagCbx = new CheckBox("largeArcFlag");
            CheckBox sweepFlagCbx = new CheckBox("sweepFlag");
            Slider xRotationSlider = new Slider(0, 360, 0);
            xRotationSlider.setPrefWidth(300);
            xRotationSlider.setBlockIncrement(30);
            xRotationSlider.setShowTickMarks(true);
            xRotationSlider.setShowTickLabels(true);

            Slider radiusXSlider = new Slider(100, 300, 100);
            radiusXSlider.setBlockIncrement(10);
            radiusXSlider.setShowTickMarks(true);
            radiusXSlider.setShowTickLabels(true);

            Slider radiusYSlider = new Slider(100, 300, 100);
            radiusYSlider.setBlockIncrement(10);
            radiusYSlider.setShowTickMarks(true);
            radiusYSlider.setShowTickLabels(true);

            // Bind ArcTo properties to the control data

            arcTo.largeArcFlagProperty().bind(
                    largeArcFlagCbx.selectedProperty());
            arcTo.sweepFlagProperty().bind(
                    sweepFlagCbx.selectedProperty());
            arcTo.XaxisRotationProperty().bind(
                    xRotationSlider.valueProperty());
            arcTo.radiusXProperty().bind(
                    radiusXSlider.valueProperty());
            arcTo.radiusYProperty().bind(
                    radiusYSlider.valueProperty());

            GridPane pane = new GridPane();
            pane.setHgap(5);
            pane.setVgap(10);
            pane.addRow(0, largeArcFlagCbx, sweepFlagCbx);
            pane.addRow(1, new Label("XAxisRotation"), xRotationSlider);
            pane.addRow(2, new Label("radiusX"), radiusXSlider);
            pane.addRow(3, new Label("radiusY"), radiusYSlider);

            return pane;
        }
}

Listing 14-12Using ArcTo Path Elements

QuadCurveTo 路径元素

QuadCurveTo类的一个实例使用指定的控制点(controlX,controlY)从当前点到指定的终点(x,y)绘制一条二次贝塞尔曲线。它包含四个属性来指定结束点和控制点。

  • x

  • y

  • controlX

  • controlY

xy属性指定终点的 x 和 y 坐标。controlXcontrolY属性指定控制点的 x 和 y 坐标。

QuadCurveTo类包含两个构造器:

  • QuadCurveTo()

  • QuadCurveTo(double controlX, double controlY, double x, double y)

下面的代码片段使用了一个带有(10,100)控制点和(0,0)结束点的QuadCurveTo。图 14-18 显示了结果路径。

img/336502_2_En_14_Fig18_HTML.png

图 14-18

使用 QuadCurveTo 路径元素

Path path = new Path(
   new MoveTo(0, 0),
   new VLineTo(100),
   new HLineTo(100),
   new VLineTo(50),
   new QuadCurveTo(10, 100, 0, 0));

三次曲线到路径元素

CubicCurveTo类的实例使用指定的控制点(controlX1,controlY1)和(controlX2,controlY2)绘制从当前点到指定终点(x,y)的三次贝塞尔曲线。它包含六个属性来指定结束点和控制点:

  • x

  • y

  • controlX1

  • controlY1

  • controlX2

  • controlY2

xy属性指定终点的 x 和 y 坐标。controlX1controlY1属性指定第一个控制点的 x 和 y 坐标。controlX2controlY2属性指定第二个控制点的 x 和 y 坐标。

CubicCurveTo类包含两个构造器:

  • CubicCurveTo()

  • CubicCurveTo(double controlX1, double controlY1, double controlX2, double controlY2, double x, double y)

下面的代码片段使用了一个CubicCurveTo,将(10,100)和(40,80)作为控制点,将(0,0)作为结束点。图 14-19 显示了结果路径。

img/336502_2_En_14_Fig19_HTML.png

图 14-19

使用 QuadCurveTo 路径元素

Path path = new Path(
   new MoveTo(0, 0),
   new VLineTo(100),
   new HLineTo(100),
   new VLineTo(50),
   new CubicCurveTo(10, 100, 40, 80, 0, 0));

ClosePath 路径元素

ClosePath path 元素关闭当前子路径。注意一个Path可能由多个子路径组成,因此,一个Path中可能有多个ClosePath元素。一个ClosePath元素从当前点到当前子路径的起始点画一条直线,并结束子路径。一个ClosePath元素后面可能跟着一个MoveTo元素,在这种情况下,MoveTo元素是下一个子路径的起点。如果一个ClosePath元素后面是一个路径元素而不是一个MoveTo元素,那么下一个子路径从被ClosePath元素关闭的子路径的起始点开始。

下面的代码片段创建了一个Path对象,它使用了两个子路径。每个子路径画一个三角形。使用ClosePath元素关闭子路径。图 14-20 显示了最终的形状。

img/336502_2_En_14_Fig20_HTML.png

图 14-20

使用两个子路径和一个 ClosePath 元素的形状

Path p1 = new Path(
   new MoveTo(50, 0),
   new LineTo(0, 50),
   new LineTo(100, 50),
   new ClosePath(),
   new MoveTo(90, 15),
   new LineTo(40, 65),
   new LineTo(140, 65),
   new ClosePath());
p1.setFill(Color.LIGHTGRAY);

路径的填充规则

一个Path可以用来画非常复杂的形状。有时,很难确定一个点是在形状内部还是外部。Path类包含一个fillRule属性,用于确定一个点是否在一个形状内。它的值可以是FillRule枚举:NON_ZEROEVEN_ODD的常量之一。如果某个点在形状内部,它将使用填充颜色进行渲染。图 14-21 显示了由一个Path和两个三角形公共区域中的一个点创建的两个三角形。我将讨论该点是否在形状内部。

img/336502_2_En_14_Fig21_HTML.png

图 14-21

由两个三角形子路径组成的形状

笔划的方向是决定一个点是否在形状内部的重要因素。图 14-21 中的形状可以用不同方向的笔画出来。图 14-22 显示了其中的两个。在形状-1 中,两个三角形都使用逆时针笔划。在形状-2 中,一个三角形使用逆时针笔划,另一个使用顺时针笔划。

img/336502_2_En_14_Fig22_HTML.png

图 14-22

由两个使用不同描边方向的三角形子路径组成的形状

Path的填充规则从该点到无限远绘制光线,因此它们可以与所有路径段相交。在NON_ZERO填充规则中,如果逆时针和顺时针方向上与射线相交的路径段数量相等,则该点在形状之外。否则,点在形状内部。你可以用一个从零开始的计数器来理解这个规则。对于逆时针方向与路径段相交的每条射线,计数器加 1。对于每一条沿顺时针方向与路径段相交的光线,从计数器中减去 1。最后,如果计数器不为零,则该点在内部;否则,重点就在外面。图 14-23 显示了应用NON_ZERO填充规则时,由两个三角形子路径组成的相同两条路径及其计数器值。从该点画出的射线用虚线表示。第一个形状中的点得分为 6(非零值),并且位于路径内部。第二个形状中的点得分为零,它在路径之外。

img/336502_2_En_14_Fig23_HTML.png

图 14-23

将非零填充规则应用于两个三角形子路径

NON_ZERO填充规则一样,EVEN_ODD填充规则也从一个点向无限远的所有方向绘制光线,因此所有路径段都相交。它计算光线和路径段之间的相交数。如果数字是奇数,则该点在路径内。否则,该点在路径之外。如果将图 14-23 中所示的两个图形的fillRule属性设置为EVEN_ODD,那么这两个图形的点都在路径之外,因为在这两种情况下,光线和路径段之间的交点数量都是六(偶数)。一个PathfillRule属性的默认值是FillRule.NON_ZERO

清单 14-13 中的程序是本节讨论的例子的一个实现。它绘制了四个路径:前两个(从左数)使用NON_ZERO填充规则,后两个使用EVEN_ODD填充规则。图 14-24 显示了路径。第一个和第三个路径使用逆时针笔划绘制两个三角形子路径。第二个和第四个路径是使用逆时针笔划绘制的,对于一个三角形使用顺时针笔划。

img/336502_2_En_14_Fig24_HTML.jpg

图 14-24

使用不同填充规则的路径

// PathFillRule.java
package com.jdojo.shape;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.FillRule;
import javafx.scene.shape.LineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
import javafx.scene.shape.PathElement;
import javafx.stage.Stage;

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

        @Override
        public void start(Stage stage) {
            // Both triangles use a counterclockwise stroke
            PathElement[] pathElements1 = {
               new MoveTo(50, 0),
               new LineTo(0, 50),
               new LineTo(100, 50),
               new LineTo(50, 0),
               new MoveTo(90, 15),
               new LineTo(40, 65),
               new LineTo(140, 65),
               new LineTo(90, 15)};

            // One triangle uses a clockwise stroke and
            // another uses a counterclockwise stroke
            PathElement[] pathElements2 = {
               new MoveTo(50, 0),
               new LineTo(0, 50),
               new LineTo(100, 50),
               new LineTo(50, 0),
               new MoveTo(90, 15),
               new LineTo(140, 65),
               new LineTo(40, 65),
               new LineTo(90, 15)};

            /* Using the NON-ZERO fill rule by default */
            Path p1 = new Path(pathElements1);
            p1.setFill(Color.LIGHTGRAY);

            Path p2 = new Path(pathElements2);
            p2.setFill(Color.LIGHTGRAY);

            /* Using the EVEN_ODD fill rule */
            Path p3 = new Path(pathElements1);
            p3.setFill(Color.LIGHTGRAY);
            p3.setFillRule(FillRule.EVEN_ODD);

            Path p4 = new Path(pathElements2);
            p4.setFill(Color.LIGHTGRAY);

            p4.setFillRule(FillRule.EVEN_ODD);

            HBox root = new HBox(p1, p2, p3, p4);
            root.setSpacing(10);
            root.setStyle("""
               -fx-padding: 10;
               -fx-border-style: solid inside;
               -fx-border-width: 2;
               -fx-border-insets: 5;
               -fx-border-radius: 5;
               -fx-border-color: blue;""");

            Scene scene = new Scene(root);
            stage.setScene(scene);
            stage.setTitle("Using Fill Rules for Paths");
            stage.show();
        }
}

Listing 14-13Using Fill Rules for Paths

绘制可缩放矢量图形

SVGPath类的一个实例从编码字符串中的路径数据绘制一个形状。你可以在 www.w3.org/TR/SVG 找到 SVG 规范。在 www.w3.org/TR/SVG/paths.html 可以找到构造字符串格式路径数据的详细规则。JavaFX 部分支持 SVG 规范。

SVGPath类包含一个无参数构造器来创建它的对象:

// Create a SVGPath object
SVGPath sp = new SVGPath();

SVGPath类包含两个属性:

  • content

  • fillRule

属性定义了 SVG path的编码字符串。fillRule属性指定形状内部的填充规则,可以是FillRule.NON_ZEROFillRule.EVEN_ODDfillRule属性的默认值是FillRule.NON_ZERO。有关填充规则的更多详细信息,请参考“路径的填充规则”一节。PathSVGPath的填充规则相同。

下面的代码片段将“M50,0 L0,50 L100,50 Z”编码字符串设置为SVGPath对象绘制三角形的内容,如图 14-25 所示:

img/336502_2_En_14_Fig25_HTML.jpg

图 14-25

使用 SVGPath 的三角形

SVGPath sp2 = new SVGPath();
sp2.setContent("M50, 0 L0, 50 L100, 50 Z");
sp2.setFill(Color.LIGHTGRAY);
sp2.setStroke(Color.BLACK);

SVGPath的内容是遵循一些规则的编码字符串:

  • 该字符串由一系列命令组成。

  • 每个命令名的长度正好是一个字母。

  • 命令后面是它的参数。

  • 命令的参数值由逗号或空格分隔。例如,“M50,0 L0,50 L100,50 Z”和“M50 0 L0 50 L100 50 Z”代表相同的路径。为了可读性,您将使用逗号来分隔两个值。

  • 您不需要在命令字符前后添加空格。比如“M50 0 L0 50 L100 50 Z”可以改写为“M50 0L0 50L100 50Z”。

让我们考虑一下上一个例子中使用的 SVG 内容:

M50, 0 L0, 50 L100, 50 Z

内容由四个命令组成:

  • M50, 0

  • L0, 50

  • L100, 50

  • Z

将 SVG 路径命令与Path API 比较,第一个命令是“MoveTo (50,0)”;第二个命令是“LineTo(0,50)”;第三个命令是“LineTo(100,50)”;第四个命令是“ClosePath”。

Tip

SVGPath内容中的命令名是代表Path对象中路径元素的类的第一个字母。例如,Path API 中的绝对MoveTo变成了SVGPath内容中的M,绝对LineTo变成了L,以此类推。

命令的参数是坐标,可以是绝对的,也可以是相对的。当命令名为大写时(如M),其参数被认为是绝对的。当命令名为小写(例如 m)时,其参数被认为是相对的。“关闭路径”命令是Zz。因为“closepath”命令不带任何参数,所以大写和小写版本的行为是相同的。

考虑两个SVG路径的内容:

  • M50, 0 L0, 50 L100, 50 Z

  • M50, 0 l0, 50 l100, 50 Z

第一条路径使用绝对坐标。第二条路径使用绝对和相对坐标。像Path一样,SVGPath必须以“moveTo”命令开始,该命令必须使用绝对坐标。如果SVGPath以相对“移动到”命令开始(如"m 50, 0",其参数被视为绝对坐标。在前面的 SVG 路径中,可以用"m50, 0"作为字符串的开头,结果是一样的。

前面的两个 SVG 路径将绘制两个不同的三角形,如图 14-26 所示,尽管两者使用相同的参数。第一条路径在左边绘制三角形,第二条路径在右边绘制三角形。第二条路径中的命令解释如下:

img/336502_2_En_14_Fig26_HTML.jpg

图 14-26

在 SVG 路径中使用绝对和相对坐标

  • 移动到(50,0)。

  • 从当前点(50,0)到(50,50)画一条线。终点(50,50)是通过将当前点的 x 和 y 坐标添加到相对的“lineto”命令(l)参数中得出的。终点变成(50,50)。

  • 从当前点(50,50)到(150,100)画一条线。同样,终点的坐标是通过将当前点的 x 和 y 坐标(50,50)与命令参数“l100,50”相加得出的(“l100,50”中的第一个字符是小写的 L,而不是数字 1)。

  • 然后关闭路径(Z)。

表 14-2 列出了SVGPath对象内容中使用的命令。它还列出了在Path API 中使用的等价类。下表列出了使用绝对坐标的命令。命令的相对版本使用小写字母。参数列中的加号(+)表示可以使用多个参数。

表 14-2

SVG 路径命令列表

|

命令

|

参数

|

命令名称

|

路径 API 类

M (x, y)+ moveto MoveTo
L (x,y)+ lineto LineTo
H x+ lineto HLineTo
V y+ lineto VLineTo
A (rx,ry,x 轴旋转,大圆弧标志,扫描标志,x,y)+ arcto ArcTo
Q (x1,y1,x,y)+ Quadratic Bezier curveto QuadCurveTo
T (x,y)+ Shorthand/smooth quadratic Bezier curveto QuadCurveTo
C (x1,y1,x2,y2,x,y)+ curveto CubicCurveTo
S (x2,y2,x,y)+ Shorthand/smooth curveto CubicCurveTo
Z 没有人 closePath ClosePath

“移动到”命令

“moveTo”命令M在指定的(x,y)坐标开始一个新子路径。它后面可能是一对或多对坐标。第一对坐标被认为是该点的 x 和 y 坐标,该命令将使其成为当前点。每个额外的对都被视为“lineto”命令的一个参数。如果“moveTo”命令是相对的,则“lineto”命令也是相对的。如果“moveTo”命令是绝对的,则“lineto”命令将是绝对的。例如,以下两个 SVG 路径是相同的:

M50, 0 L0, 50 L100, 50 Z
M50, 0, 0, 50, 100, 50 Z

“lineto”命令

有三个“行到”命令:LHV。它们被用来画直线。

命令L用于从当前点到指定的(x,y)点画一条直线。如果指定多对(x,y)坐标,它将绘制一条多段线。(x,y)坐标的最后一对成为新的当前点。下面的 SVG 路径将绘制相同的三角形。第一个使用两个L命令,第二个只使用一个:

  • M50, 0 L0, 50 L100, 50 L50, 0

  • M50, 0 L0, 50, 100, 50, 50, 0

HV命令用于从当前点绘制水*线和垂直线。命令H从当前点(cx,cy)到(x,cy)画一条水*线。命令V画一条从当前点(cx,cy)到(cx,y)的垂直线。您可以向它们传递多个参数。最后一个参数值定义了当前点。比如“M0,0H200,100 V50Z”会画一条从(0,0)到(200,0),从(200,0)到(100,0)的线。第二个命令将(100,0)作为当前点。第三个命令将绘制一条从(100,0)到(100,50)的垂直线。z 命令将绘制一条从(100,50)到(0,0)的直线。下面的代码片段绘制了一个 SVG 路径,如图 14-27 所示:

img/336502_2_En_14_Fig27_HTML.jpg

图 14-27

对“lineto”命令使用多个参数

SVGPath p1 = new SVGPath();
p1.setContent("M0, 0H-50, 50, 0 V-50, 50, 0, -25 L25, 0");
p1.setFill(Color.LIGHTGRAY);
p1.setStroke(Color.BLACK);

“arcto”命令

“arcto”命令A从当前点到指定的(x,y)点画一个椭圆弧。它使用 rx 和 ry 作为沿 x 轴和 y 轴的半径。x 轴旋转是椭圆 x 轴的旋转角度,以度为单位。大弧标志和扫描标志是用于从四个可能的弧中选择一个弧的标志。使用 0 和 1 作为标志值,其中 1 表示真,0 表示假。有关其所有参数的详细说明,请参考“ArcTo 路径元素”一节。您可以传递多个弧参数,在这种情况下,一个弧的终点将成为下一个弧的当前点。下面的代码片段用弧线绘制了两条 SVG 路径。第一个路径为“arcTo”命令使用一个参数,第二个路径使用两个参数。图 14-28 显示了路径。

img/336502_2_En_14_Fig28_HTML.jpg

图 14-28

使用“arcTo”命令绘制椭圆弧路径

SVGPath p1 = new SVGPath();

// rx=150, ry=50, x-axis-rotation=0, large-arc-flag=0,
// sweep-flag 0, x=-50, y=50
p1.setContent("M0, 0 A150, 50, 0, 0, 0, -50, 50 Z");
p1.setFill(Color.LIGHTGRAY);
p1.setStroke(Color.BLACK);

// Use multiple arcs in one "arcTo" command
SVGPath p2 = new SVGPath();

// rx1=150, ry1=50, x-axis-rotation1=0, large-arc-flag1=0,
// sweep-flag1=0, x1=-50, y1=50
// rx2=150, ry2=10, x-axis-rotation2=0, large-arc-flag2=0,
// sweep-flag2=0, x2=10, y2=10
p2.setContent("M0, 0 A150 50 0 0 0 -50 50, 150 10 0 0 0 10 10 Z");
p2.setFill(Color.LIGHTGRAY);
p2.setStroke(Color.BLACK);

“二次贝塞尔曲线”命令

命令QT都用于绘制二次贝塞尔曲线。

命令Q使用指定的(x1,y1)作为控制点,从当前点到指定的(x,y)点绘制一条二次贝塞尔曲线。

命令T使用一个控制点绘制一条从当前点到指定(x,y)点的二次贝塞尔曲线,该控制点是前一个命令的控制点的反射。如果没有先前的命令或先前的命令不是QqTt,则当前点被用作控制点。

命令Q将控制点作为参数,而命令T采用控制点。以下代码片段使用命令QT绘制二次贝塞尔曲线,如图 14-29 所示:

img/336502_2_En_14_Fig29_HTML.jpg

图 14-29

使用 Q 和 T 命令绘制二次贝塞尔曲线

SVGPath p1 = new SVGPath();
p1.setContent("M0, 50 Q50, 0, 100, 50");
p1.setFill(Color.LIGHTGRAY);
p1.setStroke(Color.BLACK);

SVGPath p2 = new SVGPath();
p2.setContent("M0, 50 Q50, 0, 100, 50 T200, 50");
p2.setFill(Color.LIGHTGRAY);
p2.setStroke(Color.BLACK);

“三次贝塞尔曲线”命令

命令CS用于绘制三次贝塞尔曲线。

命令C使用指定的控制点(x1,y1)和(x2,y2)绘制从当前点到指定点(x,y)的三次贝塞尔曲线。

命令S从当前点到指定点(x,y)绘制一条三次贝塞尔曲线。它假设第一个控制点是前一个命令上第二个控制点的反射。如果没有先前的命令或先前的命令不是 C、C、S或 s,则当前点用作第一个控制点。指定点(x2,y2)是第二个控制点。多组坐标画出一个凝聚体。

下面这段代码使用命令CS绘制三次贝塞尔曲线,如图 14-30 所示。第二条路径使用命令S将前一个命令C的第二个控制点的反射作为其第一个控制点:

img/336502_2_En_14_Fig30_HTML.jpg

图 14-30

使用 C 和 S 命令绘制三次贝塞尔曲线

SVGPath p1 = new SVGPath();
p1.setContent("M0, 0 C0, -100, 100, 100, 100, 0");
p1.setFill(Color.LIGHTGRAY);
p1.setStroke(Color.BLACK);

SVGPath p2 = new SVGPath();
p2.setContent("M0, 0 C0, -100, 100, 100, 100, 0 S200 100 200, 0");
p2.setFill(Color.LIGHTGRAY);
p2.setStroke(Color.BLACK);

“closepath”命令

“闭合路径”命令Zz从当前点到当前子路径的起点画一条直线,并结束子路径。该命令的大写和小写版本工作方式相同。

组合形状

Shape类提供了三个静态方法,让您执行形状的并集、交集和差集:

  • union(Shape shape1, Shape shape2)

  • intersect(Shape shape1, Shape shape2)

  • subtract(Shape shape1, Shape shape2)

这些方法返回一个新的Shape实例。它们对输入形状的区域进行操作。如果形状没有填充和描边,则其面积为零。新形状有一个描边和一个填充。union()方法组合两个形状的面积。intersect()方法使用形状之间的公共区域来创建新的形状。subtract()方法通过从第一个形状中减去指定的第二个形状来创建新的形状。

清单 14-14 中的程序使用并集、交集和减法运算来组合两个圆。图 14-31 显示了最终的形状。

img/336502_2_En_14_Fig31_HTML.jpg

图 14-31

由两个圆组合而成的形状

// CombiningShapesTest.java
package com.jdojo.shape;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Shape;
import javafx.stage.Stage;

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

        @Override
        public void start(Stage stage) {
            Circle c1 = new Circle (0, 0, 20);
            Circle c2 = new Circle (15, 0, 20);

            Shape union = Shape.union(c1, c2);
            union.setStroke(Color.BLACK);
            union.setFill(Color.LIGHTGRAY);

            Shape intersection = Shape.intersect(c1, c2);
            intersection.setStroke(Color.BLACK);
            intersection.setFill(Color.LIGHTGRAY);

            Shape subtraction = Shape.subtract(c1, c2);
            subtraction.setStroke(Color.BLACK);
            subtraction.setFill(Color.LIGHTGRAY);

            HBox root = new HBox(union, intersection, subtraction);
            root.setSpacing(20);
            root.setStyle("""
               -fx-padding: 10;
               -fx-border-style: solid inside;
               -fx-border-width: 2;
               -fx-border-insets: 5;
               -fx-border-radius: 5;
               -fx-border-color: blue;""");

            Scene scene = new Scene(root);
            stage.setScene(scene);
            stage.setTitle("Combining Shapes");
            stage.show();
        }

}

Listing 14-14Combining Shapes to Create New Shapes

了解形状的笔画

描边是画出形状轮廓的过程。有时,形状的轮廓也称为笔画。Shape类包含几个定义形状笔画外观的属性:

  • stroke

  • strokeWidth

  • strokeType

  • strokeLineCap

  • strokeLineJoin

  • strokeMiterLimit

  • strokeDashOffset

stroke属性指定笔画的颜色。对于除了LinePathPolyline之外的所有形状,默认的stroke被设置为null,它们的默认笔画为Color.BLACK

strokeWidth属性指定笔画的宽度。默认为 1.0px。

描边是沿着形状的边界绘制的。strokeType属性指定边界上笔画宽度的分布。它的值是StrokeType枚举的三个常量CENTEREDINSIDEOUTSIDE之一。默认值为CENTEREDCENTERED描边类型将描边宽度的一半画在边界外,另一半画在边界内。INSIDE笔划类型在边界内绘制笔划。OUTSIDE描边在边界外绘制描边。形状的描边宽度包含在其布局边界内。

清单 14-15 中的程序创建了四个矩形,如图 14-32 所示。所有矩形都有相同的widthheight ( 50px 和 50px)。从左边数起,第一个矩形没有描边,其布局边界为 50px X 50px。第二个矩形使用宽度为 4px 的笔画和INSIDE笔画类型。INSIDE笔画类型绘制在宽度和高度边界内,矩形的布局边界为 50px X 50px。第三个矩形使用 4px 的描边宽度和默认的CENTERED描边类型。笔画在边界内绘制 2px,在边界外绘制 2px。2px 外部笔划被添加到所有四个尺寸中,使得布局边界为 54px X 54px。第四个矩形使用 4px 描边宽度和OUTSIDE描边类型。整个笔画宽度超出了矩形的宽度和高度,使布局为 58px X 58px。

img/336502_2_En_14_Fig32_HTML.jpg

图 14-32

使用不同类型笔画的矩形

// StrokeTypeTest.java
package com.jdojo.shape;

import javafx.application.Application;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.StrokeType;
import javafx.stage.Stage;

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

        @Override
        public void start(Stage stage) {
            Rectangle r1 = new Rectangle(50, 50);
            r1.setFill(Color.LIGHTGRAY);

            Rectangle r2 = new Rectangle(50, 50);
            r2.setFill(Color.LIGHTGRAY);
            r2.setStroke(Color.BLACK);
            r2.setStrokeWidth(4);
            r2.setStrokeType(StrokeType.INSIDE);

            Rectangle r3 = new Rectangle(50, 50);
            r3.setFill(Color.LIGHTGRAY);
            r3.setStroke(Color.BLACK);
            r3.setStrokeWidth(4);

            Rectangle r4 = new Rectangle(50, 50);
            r4.setFill(Color.LIGHTGRAY);
            r4.setStroke(Color.BLACK);
            r4.setStrokeWidth(4);
            r4.setStrokeType(StrokeType.OUTSIDE);

            HBox root = new HBox(r1, r2, r3, r4);
            root.setAlignment(Pos.CENTER);
            root.setSpacing(10);
            root.setStyle("""
               -fx-padding: 10;
               -fx-border-style: solid inside;
               -fx-border-width: 2;
               -fx-border-insets: 5;
               -fx-border-radius: 5;
               -fx-border-color: blue;""");

            Scene scene = new Scene(root);
            stage.setScene(scene);
            stage.setTitle("Using Different Stroke Types for Shapes");
            stage.show();
        }

}

Listing 14-15Effects of Applying Different Stroke Types on a Rectangle

strokeLineCap属性为未闭合的子路径和虚线段指定笔画的结束修饰。它的值是StrokeLineCap枚举的常量之一:BUTTSQUAREROUND。默认为BUTTBUTT线帽不在子路径的末端添加装饰;笔画恰好在起点和终点开始和结束。SQUARE线帽延伸末端一半的行程宽度。ROUND线帽在末端增加一个圆帽。圆帽使用的半径等于笔划宽度的一半。图 14-33 显示三条线,为未闭合的子路径。所有线条的宽度都是 100 像素,使用 10 像素的笔画宽度。图中显示了他们使用的strokeLineCap。使用BUTT线帽的线条的布局边界宽度保持为 100 像素。但是,对于其他两行,布局边界的宽度增加到 110 像素,两端增加 10 像素。

img/336502_2_En_14_Fig33_HTML.png

图 14-33

笔画的不同线帽样式

注意,strokeLineCap属性应用于未闭合的子路径的线段末端。图 14-34 显示了由未闭合子路径创建的三个三角形。他们使用不同的笔画线帽。使用 SVG 路径数据“M50,0L0,50 M0,50 L100,50 M100,50 L50,0”来绘制三角形。填充设置为null,笔画宽度设置为 10px。

img/336502_2_En_14_Fig34_HTML.png

图 14-34

使用未闭合子路径的三角形使用不同的描边线帽

属性指定子路径的两个连续路径元素如何连接。它的值是StrokeLineJoin枚举的常量之一:BEVELMITERROUND。默认为斜接。BEVEL线连接通过一条直线连接路径元素的外角。MITER线连接延伸两个路径元素的外边缘,直到它们相遇。ROUND线条连接通过将两个路径元素的角圆化一半的笔画宽度来连接它们。图 14-35 显示了用 SVG 路径数据“M50,0L0,50 L100,50 Z”创建的三个三角形。填充颜色为空,描边宽度为 10px。如图所示,三角形使用不同的线连接。

img/336502_2_En_14_Fig35_HTML.png

图 14-35

使用不同线条连接类型的三角形

一个MITER线连接通过延伸它们的外边缘来连接两个路径元素。如果路径元素以较小的角度相交,连接的长度可能会变得很大。您可以使用strokeMiterLimit属性来限制连接的长度。它指定斜接长度和描边宽度的比率。斜接长度是连接的最内侧点和最外侧点之间的距离。如果两个路径元素不能通过在此范围内扩展它们的外边缘而相遇,则使用一个BEVEL连接。默认值为 10.0。也就是说,默认情况下,斜接长度可能高达笔画宽度的十倍。

下面的代码片段创建了两个三角形,如图 14-36 所示。默认情况下,两者都使用MITER线连接。第一个三角形使用 2.0 作为斜接限制。第二个三角形使用默认的斜接限制,即 10.0。笔画宽度为 10px。第一个三角形尝试通过将两条线延伸到 20px 来连接角,20px 是通过将 10px 描边宽度乘以 2.0 的斜接限制来计算的。在 20px 内不能使用MITER连接来连接拐角,所以使用了BEVEL连接。

img/336502_2_En_14_Fig36_HTML.jpg

图 14-36

使用不同笔划斜接限制的三角形

SVGPath t1 = new SVGPath();
t1.setContent("M50, 0L0, 50 L100, 50 Z");
t1.setStrokeWidth(10);
t1.setFill(null);
t1.setStroke(Color.BLACK);
t1.setStrokeMiterLimit(2.0);

SVGPath t2 = new SVGPath();
t2.setContent("M50, 0L0, 50 L100, 50 Z");
t2.setStrokeWidth(10);
t2.setFill(null);
t2.setStroke(Color.BLACK);

默认情况下,描边绘制实心轮廓。也可以有虚线轮廓。您需要提供一个虚线模式和虚线偏移量。虚线图案是存储在ObservableList<Double>中的double的数组。您可以使用Shape类的getStrokeDashArray()方法来获取列表的引用。列表的元素指定了虚线和间隙的模式。第一个元素是虚线长度、第二个间隙、第三个虚线长度、第四个间隙等等。急骤的图案被重复以画出轮廓。strokeDashOffset属性指定笔画开始处的虚线图案的偏移量。

下面的代码片段创建了两个Polygon实例,如图 14-37 所示。两者都使用相同的虚线模式,但虚线偏移不同。第一个使用 0.0 的虚线偏移,这是默认值。第一个矩形的笔画以 15.0px 的虚线开始,这是虚线图案的第一个元素,可以在从(0,0)到(100,0)绘制的虚线中看到。第二个Polygon使用虚线偏移量 20.0,这意味着笔画将在虚线图案内 20.0px 处开始。前两个元素 15.0 和 3.0 在虚线偏移 20.0 内。因此,第二个Polygon的笔画从第三个元素开始,这是一个 5.0px 的破折号。

img/336502_2_En_14_Fig37_HTML.jpg

图 14-37

两个多边形的轮廓使用虚线图案

Polygon p1 = new Polygon(0, 0, 100, 0, 100, 50, 0, 50, 0, 0);
p1.setFill(null);
p1.setStroke(Color.BLACK);
p1.getStrokeDashArray().addAll(15.0, 5.0, 5.0, 5.0);

Polygon p2 = new Polygon(0, 0, 100, 0, 100, 50, 0, 50, 0, 0);
p2.setFill(null);
p2.setStroke(Color.BLACK);
p2.getStrokeDashArray().addAll(15.0, 5.0, 5.0, 5.0);
p2.setStrokeDashOffset(20.0);

使用 CSS 设计形状样式

所有形状都没有默认的样式类名。如果您想使用 CSS 将样式应用于形状,您需要向它们添加样式类名。所有形状都可以使用下列 CSS 属性:

  • -fx-fill

  • -fx-smooth

  • -fx-stroke

  • -fx-stroke-type

  • -fx-stroke-dash-array

  • -fx-stroke-dash-offset

  • -fx-stroke-line-cap

  • -fx-stroke-line-join

  • -fx-stroke-miter-limit

  • -fx-stroke-width

所有 CSS 属性都对应于Shape类中的属性,我在上一节已经详细讨论过了。Rectangle支持两个额外的 CSS 属性来指定圆角矩形的弧宽和高度:

  • -fx-arc-height

  • -fx-arc-width

以下代码片段创建了一个Rectangle,并添加了矩形作为其样式类名:

Rectangle r1 = new Rectangle(200, 50);
r1.getStyleClass().add("rectangle");

下面的样式将产生一个如图 14-38 所示的矩形:

img/336502_2_En_14_Fig38_HTML.jpg

图 14-38

将 CSS 样式应用于矩形

.rectangle {
        -fx-fill: lightgray;
        -fx-stroke: black;
        -fx-stroke-width: 4;
        -fx-stroke-dash-array: 15 5 5 10;
        -fx-stroke-dash-offset: 20;
        -fx-stroke-line-cap: round;
        -fx-stroke-line-join: bevel;
}

摘要

任何能在二维*面上画出的形状都叫做 2D 形状。JavaFX 提供了各种节点来绘制不同类型的形状(线条、圆形、矩形等)。).您可以将形状添加到场景图。所有形状类都在javafx.scene.shape包中。代表 2D 形状的类继承自抽象的Shape类。形状可以有定义形状轮廓的笔划。形状可以有填充。

Line类的一个实例代表一个线节点。一辆Line没有内饰。默认情况下,它的fill属性设置为null。设置fill没有效果。默认strokeColor.BLACK,默认strokeWidth为 1.0。

Rectangle类的一个实例表示一个矩形节点。该类使用六个属性来定义矩形:xywidthheightarcWidtharcHeightxy属性是矩形左上角在节点局部坐标系中的 x 和 y 坐标。widthheight属性分别是矩形的宽度和高度。指定相同的宽度和高度来绘制正方形。默认情况下,矩形的角是尖锐的。通过指定arcWidtharcHeight属性,矩形可以有圆角。

Circle类的一个实例代表一个圆形节点。该类使用三个属性来定义圆:centerXcenterYradiuscenterXcenterY属性是圆心在节点本地坐标系中的 x 和 y 坐标。radius属性是圆的半径。这些属性的默认值为零。

Ellipse类的一个实例表示一个椭圆节点。该类使用四个属性来定义椭圆:centerXcenterYradiusXradiusYcenterXcenterY属性是圆心在节点的局部坐标系中的 x 和 y 坐标。radiusXradiusY是椭圆在水*和垂直方向上的半径。这些属性的默认值为零。当radiusXradiusY相同时,圆是椭圆的特例。

Polygon类的一个实例代表一个多边形节点。该类不定义任何公共属性。它允许您使用定义多边形顶点的(x,y)坐标数组来绘制多边形。使用Polygon类,你可以绘制任何类型的几何形状,这些几何形状是使用连接线创建的(三角形、五边形、六边形、*行四边形等)。).

折线类似于多边形,只是它不在最后一点和第一点之间绘制直线。也就是说,折线是一个开放的多边形。然而,fill颜色用于填充整个形状,就好像该形状是闭合的一样。Polyline类的一个实例代表一个折线节点。

Arc类的一个实例代表一个椭圆的一部分。该类使用七个属性来定义椭圆:centerXcenterYradiusXradiusYstartAnglelengthtype。前四个属性定义了一个椭圆。最后三个属性定义了椭圆的一个扇区,即Arc节点。startAngle属性指定从 x 轴正方向逆时针测量的截面起始角度,单位为度。它定义了弧的起点。length是一个角度,以度为单位,从开始角度逆时针测量,以定义扇形的结束。如果length属性设置为 360 度,则Arc是一个完整的椭圆。

贝塞尔曲线在计算机图形学中用于绘制*滑曲线。QuadCurve类的一个实例表示使用指定的贝塞尔控制点与两个指定点相交的二次贝塞尔曲线段。

CubicCurve类的一个实例使用两个指定的贝塞尔控制点表示与两个指定点相交的三次贝塞尔曲线段。

您可以使用Path类绘制复杂的形状。Path类的一个实例定义了形状的路径(轮廓)。路径由一个或多个子路径组成。子路径由一个或多个路径元素组成。每个子路径都有一个起点和一个终点。路径元素是PathElement抽象类的一个实例。PathElement类的几个子类代表特定类型的路径元素;这些级别是MoveToLineToHLineToVLineToArcToQuadCurveToCubicCurveToClosePath

JavaFX 部分支持 SVG 规范。SVGPath类的一个实例从编码字符串中的路径数据绘制一个形状。

JavaFX 允许您通过组合多个形状来创建一个形状。Shape类提供了三个名为union()intersect()subtract()的静态方法,允许您对作为参数传递给这些方法的两个形状执行并集、交集和差集操作。这些方法返回一个新的Shape实例。它们对输入形状的区域进行操作。如果形状没有填充和描边,则其面积为零。新形状有一个描边和一个填充。union()方法组合两个形状的面积。intersect()方法使用形状之间的公共区域来创建新的形状。subtract()方法通过从第一个形状中减去指定的第二个形状来创建新的形状。

描边是画出形状轮廓的过程。有时,形状的轮廓也称为笔画。Shape类包含几个属性,如strokestrokeWidth等,用于定义形状笔画的外观。

JavaFX 允许你用 CSS 设计 2D 形状。

下一章将讨论如何处理文本绘制。

十五、理解文本节点

在本章中,您将学习:

  • 什么是Text节点以及如何创建它

  • 用于绘制Text节点的坐标系

  • 如何在Text节点中显示多行文本

  • 如何设置Text节点的字体

  • 如何访问已安装的字体以及如何安装自定义字体

  • 如何设置Text节点的填充和描边

  • 如何对Text节点应用下划线、删除线等修饰

  • 如何应用字体*滑

  • 如何使用 CSS 样式化文本节点

本章的例子在com.jdojo.shape包中。为了让它们工作,您必须在module-info.java文件中添加相应的一行:

...
opens com.jdojo.shape to javafx.graphics, javafx.base;
...

什么是文本节点?

文本节点是用于呈现文本的Text类的一个实例。Text类包含几个定制文本外观的属性。Text类及其所有相关类——例如,Font类、TextAlignment枚举、FontWeight枚举等。—在javafx.scene.text包装中。

Text类继承自Shape类。也就是说,Text是一个Shape,它允许你在一个Text节点上使用Shape类的所有属性和方法。例如,您可以对一个Text节点应用填充颜色和描边。因为Text是一个节点,所以您可以使用Node类的特性:例如,应用效果和变换。您还可以设置文本对齐方式、字体系列、字体大小、文本环绕样式等。,在Text节点上。

图 15-1 显示了三个文本节点。第一个(左起)是一个简单的文本节点。第二种使用更大字体的粗体文本。第三个使用了Reflection效果,一个更大的字体,一个笔画,一个填充。

img/336502_2_En_15_Fig1_HTML.jpg

图 15-1

显示三个文本节点的窗口

创建文本节点

Text类的一个实例代表一个Text节点。一个Text节点包含文本和呈现文本的属性。您可以使用Text类的一个构造器创建一个Text节点:

  • Text()

  • Text(String text)

  • Text(double x, double y, String text)

无参数构造器创建一个以空字符串作为文本的Text节点。其他构造器允许您指定文本和定位节点。

Text类的text属性指定了Text节点的文本(或内容)。xy属性指定文本原点的 x 和 y 坐标,这将在下一节描述。

// Create an empty Text Node and later set its text
Text t1 = new Text();
t1.setText("Hello from the Text node!");

// Create a Text Node with initial text
Text t2 = new Text("Hello from the Text node!");

// Create a Text Node with initial text and position
Text t3 = new Text(50, 50, "Hello from the Text node!");

Tip

文本节点的widthheight由其字体自动确定。默认情况下,Text节点使用系统默认字体来呈现文本。

清单 15-1 中的程序创建三个Text节点,设置它们不同的属性,并将它们添加到一个HBox中。Text节点显示如图 15-1 所示。

// TextTest.java
package com.jdojo.shape;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.effect.Reflection;
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 TextTest extends Application {
        public static void main(String[] args) {
                Application.launch(args);
        }

        @Override

        public void start(Stage stage) {
                Text t1 = new Text("Hello Text Node!");

                Text t2 = new Text("Bold and Big");
                t2.setFont(Font.font("Tahoma", FontWeight.BOLD, 16));

                Text t3 = new Text("Reflection");
                t3.setEffect(new Reflection());
                t3.setStroke(Color.BLACK);
                t3.setFill(Color.WHITE);
                t3.setFont(Font.font("Arial", FontWeight.BOLD, 20));

                HBox root = new HBox(t1, t2, t3);
                root.setSpacing(20);
                root.setStyle("""
                         -fx-padding: 10;
                   -fx-border-style: solid inside;
                   -fx-border-width: 2;
                   -fx-border-insets: 5;
                   -fx-border-radius: 5;
                   -fx-border-color: blue;""");

                Scene scene = new Scene(root);
                stage.setScene(scene);
                stage.setTitle("Using Text Nodes");
                stage.show();
        }
}

Listing 15-1Creating Text Nodes

理解文本来源

除了本地和父坐标系,Text节点还有一个附加坐标系。这是用于绘制文本的坐标系。Text类的三个属性定义了文本坐标系:

  • x

  • y

  • textOrigin

xy属性定义了文本原点的 x 和 y 坐标。textOrigin属性的类型是VPos。其值可以是VPos.BASELINEVPos.TOPVPos.CENTERVPos.BOTTOM。默认是VPos.BASELINE。它定义了文本坐标系的 x 轴在文本高度内的位置。图 15-2 显示了一个文本节点的局部坐标系和文本坐标系。局部坐标轴用实线表示。文本坐标轴用虚线表示。

img/336502_2_En_15_Fig2_HTML.png

图 15-2

textOrigin 属性对文本绘制垂直位置的影响

textOriginVPos.TOP时,文本坐标系的 x 轴与文本顶部对齐。也就是说,Text节点的y属性是本地坐标系的 x 轴到显示文本顶部的距离。字体将其字符放在称为基线的一行上。VPos.BASELINE将文本坐标系的 x 轴与字体的基线对齐。注意,一些字符(例如,g、y、j、p 等。)延伸到基线以下。VPos.BOTTOM将文本坐标系的 x 轴与显示文本的底部对齐,说明字体的下降。VPos.CENTER(图中未显示)将文本坐标系的 x 轴对准显示文本的中间,说明字体的上升和下降。

Tip

Text类包含一个只读的baselineOffset属性。它的值是文本顶部和基线之间的垂直距离。它等于字体的最大上升。

大多数时候,您不需要担心Text节点的textOrigin属性,除非您需要将其相对于另一个节点垂直对齐。清单 15-2 展示了如何在场景中水*和垂直居中一个Text节点。要使节点垂直居中,必须将textOrigin属性设置为VPos.TOP。文本显示如图 15-3 所示。如果不设置textOrigin属性,它的 y 轴将与其基线对齐,并出现在场景中心线的上方。

img/336502_2_En_15_Fig3_HTML.jpg

图 15-3

场景中居中的文本节点

// TextCentering.java
package com.jdojo.shape;

import javafx.application.Application;
import javafx.geometry.VPos;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.text.Text;
import javafx.stage.Stage;

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

        @Override
        public void start(Stage stage) {
                Text msg = new Text("A Centered Text Node");

                // Must set the textOrigian to VPos.TOP to center
                // the text node vertcially within the scene
                msg.setTextOrigin(VPos.TOP);

                Group root = new Group();
                root.getChildren().addAll(msg);
                Scene scene = new Scene(root, 200, 50);
                msg.layoutXProperty().bind(
                         scene.widthProperty().subtract(
                     msg.layoutBoundsProperty().get().getWidth()).
                         divide(2));
                msg.layoutYProperty().bind(
                         scene.heightProperty().subtract(
                     msg.layoutBoundsProperty().get().getHeight()).
                         divide(2));

                stage.setTitle("Centering a Text Node in a Scene");
                stage.setScene(scene);
                stage.sizeToScene();
                stage.show();
        }

}

Listing 15-2Centering a Text Node in a Scene

显示多行文本

一个Text节点能够显示多行文本。它在两种情况下创建新行:

  • 文本中的换行符“\n”会创建一个新行,导致该换行符后面的字符换行到下一行。

  • Text类包含一个wrappingWidth属性,默认为 0.0。它的值是用像素而不是字符来指定的。如果该值大于零,则每行中的文本将按照指定的值换行。

属性以像素为单位指定两行之间的垂直间距。默认情况下为 0.0。

属性指定边界框中文本行的水*对齐方式。最宽的线定义边界框的宽度。它的值在单行Text节点中没有影响。它的值可以是TextAlignment枚举的常量之一:LEFTRIGHT, CENTERJUSTIFY。默认为TextAlignment.LEFT

清单 15-3 中的程序创建了三个多线Text节点,如图 15-4 所示。所有节点的文本都是相同的。文本包含三个换行符。第一个节点使用默认的LEFT文本对齐方式和 5px 的行距。第二个节点使用RIGHT文本对齐,默认行距为 0px。第三个节点使用 100px 的wrappingWidth。在 100 像素处创建一个新行以及一个换行符“\n”。

img/336502_2_En_15_Fig4_HTML.jpg

图 15-4

多行文本节点

// MultilineText.java
package com.jdojo.shape;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.HBox;
import javafx.scene.text.Text;
import javafx.scene.text.TextAlignment;
import javafx.stage.Stage;

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

        @Override
        public void start(Stage stage) {
                String text = """
                        Strange fits of passion have I known:
                  And I will dare to tell,
                  But in the lover's ear alone,
                  What once to me befell.""".stripIndent();

                Text t1 = new Text(text);
                t1.setLineSpacing(5);

                Text t2 = new Text(text);
                t2.setTextAlignment(TextAlignment.RIGHT);

                Text t3 = new Text(text);
                t3.setWrappingWidth(100);

                HBox root = new HBox(t1, t2, t3);
                root.setSpacing(20);
                root.setStyle("""
                         -fx-padding: 10;
                   -fx-border-style: solid inside;
                   -fx-border-width: 2;
                   -fx-border-insets: 5;
                   -fx-border-radius: 5;
                    -fx-border-color: blue;""");

                Scene scene = new Scene(root);
                stage.setScene(scene);
                stage.setTitle("Using Multiline Text Nodes");
                stage.show();
        }
}

Listing 15-3Using Multiline Text Nodes

设置文本字体

Text类的font属性定义文本的字体。使用的默认字体来自“常规”样式的“系统”字体系列。默认字体的大小取决于*台和用户的桌面设置。

一个字体有一个和一个族名。字体系列也称为字样。字体系列定义了字符的形状(或字形)。当使用属于不同字体系列的字体显示时,相同的字符以不同的方式出现。字体的变体是通过应用样式创建的。字体的每个变体都有一个由系列名称和样式名称组成的名称。例如,“Arial”是字体的系列名称,而“Arial Regular”、“Arial Bold”和“Arial Bold Italic”是“Arial”字体的变体名称。

创建字体

Font类的一个实例代表一种字体。Font类提供了两个构造器:

  • Font(double size)

  • Font(String name, double size)

第一个构造器创建一个指定大小的Font对象,它属于“System”字体族。第二个创建一个指定字体全名和指定大小的Font对象。字体的大小以磅为单位。下面的代码片段创建了一些“Arial”系列的字体对象。Font类的getFamily()getName()getSize()方法分别返回姓氏、全名和字体大小。

// Arial Plain
Font f1 = new Font("Arial", 10);

// Arial Italic
Font f2 = new Font("Arial Italic", 10);

// Arial Bold Italic
Font f3 = new Font("Arial Bold Italic", 10);

// Arial Narrow Bold
Font f4 = new Font("Arial Narrow Bold", 30);

如果没有找到完整的字体名称,将创建默认的“系统”字体。很难记住或知道所有字体变体的全名。为了解决这个问题,Font类提供了使用字体系列名称、样式和大小创建字体的工厂方法:

  • font(double size)

  • font(String family)

  • font(String family, double size)

  • font(String family, FontPosture posture, double size)

  • font(String family, FontWeight weight, double size)

  • font(String family, FontWeight weight, FontPosture posture, double size)

font()方法允许您指定字体系列名称、字体粗细、字体姿态和字体大小。如果只提供了系列名称,则使用默认字体大小,这取决于*台和用户的桌面设置。

字体粗细指定字体的加粗程度。其值是FontWeight枚举的常量之一:THINEXTRA_LIGHTLIGHTNORMALMEDIUMSEMI_BOLDBOLDEXTRA_BOLDBLACK。常量THIN代表最细的字体,常量BLACK代表最粗的字体。

字体的姿态决定了它是否是斜体。它由FontPosture枚举的两个常量之一表示:REGULARITALIC

以下代码片段使用 Font 类的工厂方法创建字体:

// Arial Regular
Font f1 = Font.font("Arial", 10);

// Arial Bold
Font f2 = Font.font("Arial", FontWeight.BOLD, 10);

// Arial Bold Italic
Font f3 = Font.font("Arial", FontWeight.BOLD, FontPosture.ITALIC, 10);

// Arial THIN
Font f4 = Font.font("Arial", FontWeight.THIN, 30);

Tip

使用Font类的getDefault()静态方法获取系统默认字体。

清单 15-4 中的程序创建Text节点并设置它们的font属性。第一个Text节点使用默认字体。图 15-5 显示了Text节点。Text节点的text是从它们的Font对象的toString()方法返回的String

img/336502_2_En_15_Fig5_HTML.jpg

图 15-5

使用“Arial”字体系列变体的文本节点

// TextFontTest.java
package com.jdojo.shape;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
import javafx.scene.text.FontPosture;
import javafx.scene.text.FontWeight;
import javafx.scene.text.Text;
import javafx.stage.Stage;

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

        @Override
        public void start(Stage stage) {
                Text t1 = new Text();
                t1.setText(t1.getFont().toString());

                Text t2 = new Text();

                t2.setFont(Font.font("Arial", 12));
                t2.setText(t2.getFont().toString());

                Text t3 = new Text();
                t3.setFont(Font.font("Arial", FontWeight.BLACK, 12));
                t3.setText(t2.getFont().toString());

                Text t4 = new Text();
                t4.setFont(Font.font(
                         "Arial", FontWeight.THIN, FontPosture.ITALIC, 12));
                t4.setText(t2.getFont().toString());

                VBox root = new VBox(t1, t2, t3, t4);
                root.setSpacing(10);
                root.setStyle("""
                         -fx-padding: 10;
                   -fx-border-style: solid inside;
                   -fx-border-width: 2;
                   -fx-border-insets: 5;
                   -fx-border-radius: 5;
                   -fx-border-color: blue;""");

                Scene scene = new Scene(root);
                stage.setScene(scene);
                stage.setTitle("Setting Fonts for Text Nodes");
                stage.show();
        }

}

Listing 15-4Setting Fonts for Text Nodes

访问已安装的字体

您可以获得您的机器上已安装的字体列表。您可以获得所有已安装字体的字体系列名称、完整字体名称和指定字体系列名称的完整字体名称列表。Font类中的以下静态方法提供了这些列表:

  • List<String> getFamilies()

  • List<String> getFontNames()

  • List<String> getFontNames(String family)

以下代码片段打印计算机上所有已安装字体的系列名称。输出是在 Windows 上生成的。显示了部分输出:

// Print the family names of all installed fonts
for(String familyName: Font.getFamilies()) {
        System.out.println(familyName);
}
Agency FB
Algerian
Arial
Arial Black
Arial Narrow
Arial Rounded MT Bold
...

以下代码片段打印了计算机上所有已安装字体的全名。输出是在 Windows 上生成的。显示了部分输出:

// Print the full names of all installed fonts
for(String fullName: Font.getFontNames()) {
        System.out.println(fullName);
}
Agency FB
Agency FB Bold
Algerian
Arial
Arial Black
Arial Bold
Arial Bold Italic
Arial Italic
Arial Narrow
Arial Narrow Bold

Arial Narrow Bold Italic
More output goes here...

以下代码片段打印了“Times New Roman”系列的所有已安装字体的全名:

// Print the full names of “Times New Roman” family
for(String fullName: Font.getFontNames("Times New Roman")) {
        System.out.println(fullName);
}
Times New Roman
Times New Roman Bold
Times New Roman Bold Italic
Times New Roman Italic

使用自定义字体

您可以从外部来源加载自定义字体:例如,从本地文件系统的文件或从 URL。Font类中的loadFont()静态方法加载自定义字体:

  • loadFont(InputStream in, double size)

  • loadFont(String urlStr, double size)

成功加载定制字体后,loadFont()方法向 JavaFX 图形引擎注册字体,因此可以使用Font类的构造器和工厂方法创建字体。该方法还创建一个指定的sizeFont对象并返回它。因此,size参数用于在同一个方法调用中加载字体并创建其对象。如果该方法无法加载字体,则返回null

清单 15-5 中的程序展示了如何从本地文件系统加载自定义字体。字体文件名为 4starfac.ttf 。这只是一个例子,你可以指定任何你喜欢的字体。假设该文件位于 resources\font 目录下的类路径中。字体加载成功后,为第一个Text节点设置。为它的家族名称创建一个新的Font对象,并为第二个文本节点设置。如果字体文件不存在或字体无法加载,窗口中会显示相应的错误消息。图 15-6 显示字体加载成功时的窗口。

img/336502_2_En_15_Fig6_HTML.jpg

图 15-6

使用自定义字体的文本节点

// TextCustomFont.java

package com.jdojo.shape;

import java.net.URL;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.HBox;
import javafx.scene.text.Font;
import javafx.scene.text.FontPosture;
import javafx.scene.text.FontWeight;
import javafx.scene.text.Text;
import javafx.stage.Stage;

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

        @Override
        public void start(Stage stage) {
                Text t1 = new Text();
                t1.setLineSpacing(10);

                Text t2 = new Text("Another Text node");

                // Load the custom font
                String fontFile =
                         "resources/font/4starfac.ttf";
                URL url =
                         this.getClass().getClassLoader().
                         getResource(fontFile);
                if (url != null) {
                    String urlStr = url.toExternalForm();
                    Font customFont = Font.loadFont(urlStr, 16);
                    if (customFont != null ) {
                        // Set the custom font  for the first
                                // Text node
                        t1.setFont(customFont);

                        // Set the text and line spacing

                        t1.setText(
                                    "Hello from the custom font!!! \n" +
                                    "Font Family: " +
                           customFont.getFamily());

                        // Create an object of the custom font and
                                // use it
                        Font font2 =
                                       Font.font(customFont.getFamily(),
                                                 FontWeight.BOLD,
                                   FontPosture.ITALIC,
                                               24);

                            // Set the custom font for the second
                                     // Text node
                            t2.setFont(font2);
                    } else {
                        t1.setText(
                                    "Could not load the custom font from " +
                                    urlStr);
                    }
                } else {
                        t1.setText(
                                    "Could not find the custom font file " +
                           fontFile +
                                    " in CLASSPATH. Used the default font.");
                }

                HBox root = new HBox(t1, t2);
                root.setSpacing(20);
                root.setStyle("""
                         -fx-padding: 10;
                   -fx-border-style: solid inside;
                   -fx-border-width: 2;
                   -fx-border-insets: 5;
                   -fx-border-radius: 5;
                   -fx-border-color: blue;""");

                Scene scene = new Scene(root);
                stage.setScene(scene);
                stage.setTitle("Loading and Using Custom Font");
                stage.show();
        }
}

Listing 15-5Loading and Using Custom Fonts Using the Font Class

设置文本填充和描边

一个Text节点是一个形状。像形状一样,它可以有填充和描边。默认情况下,Text节点有一个null描边和一个Color.BLACK填充。Text类从Shape类继承了设置其笔画和填充的属性和方法。我在第十四章中详细讨论了它们。

清单 15-6 中的程序展示了如何为文本节点设置笔画和填充。图 15-7 显示了两个Text节点。第一个使用红色笔画和白色填充。第二个使用黑色笔画和白色填充。第二个的笔画样式使用虚线。

img/336502_2_En_15_Fig7_HTML.jpg

图 15-7

使用描边和填充的文本节点

// TextFillAndStroke.java
package com.jdojo.shape;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.stage.Stage;

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

        @Override
        public void start(Stage stage) {
                Text t1 = new Text("Stroke and fill!");
                t1.setStroke(Color.RED);
                t1.setFill(Color.WHITE);
                t1.setFont(new Font(36));

                Text t2 = new Text("Dashed Stroke!");
                t2.setStroke(Color.BLACK);
                t2.setFill(Color.WHITE);
                t2.setFont(new Font(36));
                t2.getStrokeDashArray().addAll(5.0, 5.0);

                HBox root = new HBox(t1, t2);
                root.setSpacing(20);
                root.setStyle("""
                         -fx-padding: 10;
                   -fx-border-style: solid inside;
                   -fx-border-width: 2;
                   -fx-border-insets: 5;
                   -fx-border-radius: 5;
                   -fx-border-color: blue;""");

                Scene scene = new Scene(root);
                stage.setScene(scene);
                stage.setTitle("Using Stroke and Fill for Text Nodes");
                stage.show();
        }
}

Listing 15-6Using Stroke and Fill for Text Nodes

应用文本装饰

Text类包含两个布尔属性来对其文本应用文本装饰:

  • strikethrough

  • underline

默认情况下,这两个属性都被设置为false。如果strikethrough被设置为真,则在每行文本中画一条线。如果underline设置为 true,则在每行文本下面画一条线。下面的代码片段使用了对Text节点的修饰。节点如图 15-8 所示。

img/336502_2_En_15_Fig8_HTML.jpg

图 15-8

使用下划线和删除线装饰的文本节点

Text t1 = new Text("It uses the \nunderline decoration.");
t1.setUnderline(true);

Text t2 = new Text("It uses the \nstrikethrough decoration.");
t2.setStrikethrough(true);

应用字体*滑

Text类包含一个fontSmoothingType属性,可用于应用灰色或 LCD 字体*滑。它的值是FontSmoothingType枚举:GRAYLCD的常量之一。默认*滑类型为fontSmoothingType.GRAYLCD*滑类型用作提示。下面的代码片段创建了两个文本节点:一个使用LCD和一个GRAY字体*滑类型。Text节点如图 15-9 所示。

img/336502_2_En_15_Fig9_HTML.jpg

图 15-9

使用 LCD 和灰色字体*滑类型的文本节点

Text t1 = new Text("Hello world in LCD.");
t1.setFontSmoothingType(FontSmoothingType.LCD);

Text t2 = new Text("Hello world in GRAY.");
t2.setFontSmoothingType(FontSmoothingType.GRAY);

用 CSS 样式化文本节点

一个Text节点没有默认的 CSS 样式类名。除了Shape的所有 CSS 属性外,Text节点还支持以下 CSS 属性:

  • -fx-font

  • -fx-font-smoothing-type

  • -fx-text-origin

  • -fx-text-alignment

  • -fx-strikethrough

  • -fx-underline

我已经在前几节讨论了所有属性。-fx-font属性继承自父属性。如果父级没有设置属性,则使用默认的系统字体。-fx-font-smoothing-type 属性的有效值是lcdgray-fx-text-origin属性的有效值为baselinetopbottom。让我们创建一个名为 my-text 的样式,如下所示。它设置字体和线性渐变填充。填充以浅灰色开始,以黑色结束:

.my-text {
        -fx-font: 36 Arial;
        -fx-fill: linear-gradient(from 0% 0% to 100% 0%,
                                     lightgray 0%, black 100%);
        -fx-font-smoothing-type: lcd;
        -fx-underline: true;
}

下面的代码片段创建了一个文本节点,并将其样式类名设置为 my-text 。图 15-10 显示了应用了样式的Text节点。

img/336502_2_En_15_Fig10_HTML.jpg

图 15-10

使用 CSS 样式的文本节点

Text t1 = new Text("Styling Text Nodes!");
t1.getStyleClass().add("my-text");

摘要

文本节点是用于呈现文本的Text类的一个实例。Text类包含几个定制文本外观的属性。Text类及其所有相关类都在javafx.scene.text包中。Text类继承自Shape类。也就是说,Text是一个Shape,它允许您在一个Text节点上使用Shape类的所有属性和方法。一个Text节点能够显示多行文本。

一个Text节点包含文本和呈现文本的属性。您可以使用Text类的三个构造器之一创建一个Text节点。创建节点时,可以指定文本或文本以及文本的位置。无参数构造器使用空文本创建一个文本节点,该节点位于(0,0)处。

无参数构造器创建一个以空字符串作为文本的Text节点。其他构造器允许您指定文本和定位节点。文本节点的widthheight由其字体自动确定。默认情况下,Text节点使用系统默认字体来呈现其文本。

除了本地和父坐标系,Text节点还有一个附加坐标系。这是用于绘制文本的坐标系。Text类的xytextOrigin属性定义了文本坐标系。xy属性定义了文本原点的 x 和 y 坐标。textOrigin属性属于VPos类型。其值可以是VPos.BASELINEVPos.TOPVPos.CENTERVPos.BOTTOM。默认为VPos.BASELINE。它定义了文本坐标系的 x 轴在文本高度内的位置。

Text类的font属性定义文本的字体。使用的默认字体来自“常规”样式的“系统”字体系列。默认字体的大小取决于*台和用户的桌面设置。一个Font类的实例代表一种字体。Font类包含几个静态方法,让你访问你的计算机上安装的字体,并从字体文件中加载自定义字体。

一个Text节点是一个形状。像形状一样,它可以有填充和描边。默认情况下,Text节点有一个null描边和一个Color.BLACK填充。

Text类的strikethroughunderline属性允许你对文本进行修饰。默认情况下,这两个属性都设置为false

Text类包含一个fontSmoothingType属性,可用于应用灰色或 LCD 字体*滑。它的值是FontSmoothingType枚举:GRAYLCD的常量之一。默认*滑类型为fontSmoothingType.GRAYLCD*滑类型用作提示。

您可以使用 CSS 样式化Text节点。CSS 支持设置字体、文本对齐、字体*滑和修饰。

下一章将讨论如何在 JavaFX 中绘制 3D 图形。

十六、了解 3D 形状

在本章中,您将学习:

  • 关于 3D 形状和 JavaFX 中表示 3D 形状的类

  • 如何检查您的机器是否支持 3D

  • 关于 JavaFX 中使用的 3D 坐标系

  • 关于节点的渲染顺序

  • 如何绘制预定义的 3D 形状

  • 关于不同类型的相机以及如何使用它们渲染场景

  • 如何使用光源查看场景中的 3D 对象

  • 如何创建和使用子场景

  • 如何在 JavaFX 中绘制用户自定义的 3D 图形

本章的例子在com.jdojo.shape3d包中。为了让它们工作,您必须在module-info.java文件中添加相应的一行:

...
opens com.jdojo.shape3d to javafx.graphics, javafx.base;
...

什么是 3D 形状?

在三维空间中绘制的具有三维(长度、宽度和深度)的任何形状被称为 3D 形状。立方体、球体和金字塔都是例子。

JavaFX 提供真正的 3D 形状作为节点。它提供两种类型的 3D 形状:

  • 预定义形状

  • 用户定义的形状

长方体、球体和圆柱体是三种预定义的 3D 形状,您可以在 JavaFX 应用程序中轻松使用它们。您还可以使用三角形网格创建任何类型的 3D 形状。

图 16-1 显示了表示 JavaFX 3D 形状的类的类图。3D 形状类在javafx.scene.shape包中。BoxSphereCylinder类代表三种预定义的形状。MeshView类表示场景中用户定义的 3D 形状。

img/336502_2_En_16_Fig1_HTML.jpg

图 16-1

表示 3D 形状的类的类图

JavaFX 中的 3D 可视化是使用灯光和摄像机完成的。灯光和摄像机也是节点,它们被添加到场景中。将 3D 节点添加到场景中,用灯光照亮它,然后使用相机查看它。灯光和相机在空间中的位置决定了场景中的照明和可视区域。图 16-2 显示了一个 3D 盒子,它是使用Box类的一个实例创建的。

img/336502_2_En_16_Fig2_HTML.jpg

图 16-2

3D 盒子形状的示例

检查对 3D 的支持

JavaFX 3D 支持是一个有条件的特性。如果您的*台不支持该功能,当您运行试图使用 3D 功能的程序时,控制台上会显示一条警告消息。运行清单 16-1 中的程序,检查您的机器是否支持 JavaFX 3D。该程序将打印一条消息,说明 3D 支持是否可用。

// Check3DSupport.java
package com.jdojo.shape3d;

import javafx.application.ConditionalFeature;
import javafx.application.Platform;

public class Check3DSupport {
    public static void main(String[] args) {
        boolean supported =
              Platform.isSupported(ConditionalFeature.SCENE3D);
        if (supported) {
            System.out.println("3D is supported on your machine.");
        } else {
            System.out.println("3D is not supported on your machine.");
        }
    }
}

Listing 16-1Checking JavaFX 3D Support on Your Machine

3D 坐标系统

3D 空间中的点由(x,y,z)坐标表示。3D 对象有三个维度:x、y 和 z。图 16-3 显示了 JavaFX 中使用的 3D 坐标系。

img/336502_2_En_16_Fig3_HTML.png

图 16-3

JavaFX 中使用的 3D 坐标系

x 轴的正方向从原点指向右边;y 轴的正方向指向下方;z 轴的正方向指向屏幕(远离观察者)。轴上的负方向(未示出)在原点以相反的方向延伸。

节点的呈现顺序

假设你在远处看两个重叠的物体。离你较*的对象总是与离你较远的对象重叠,不管它们在视图中出现的顺序如何。在 JavaFX 中处理 3D 对象时,您希望它们以同样的方式出现。

在 JavaFX 中,默认情况下,节点按照添加到场景图的顺序进行渲染。考虑以下代码片段:

Rectangle r1 = new Rectangle(0, 0, 100, 100);
Rectangle r2 = new Rectangle(50, 50, 100, 100);
Group group = new Group(r1, r2);

两个矩形被添加到一个组中。矩形r1首先被渲染,随后是矩形r2。重叠区域将只显示r2的区域,不显示r1。如果群组被创建为new Group(r2, r1),矩形r2将首先被渲染,随后是矩形r1。重叠区域将显示r1的区域,而不是r2。让我们添加两个矩形的 z 坐标,如下所示:

Rectangle r1 = new Rectangle(0, 0, 100, 100);
r1.setTranslateZ(10);

Rectangle r2 = new Rectangle(50, 50, 100, 100);
r2.setTranslateZ(50);

Group group = new Group(r1, r2);

前面的代码片段将产生与前面相同的效果。矩形r1将首先被渲染,随后是矩形r2。矩形的z值被忽略。在这种情况下,您希望最后渲染矩形r1,因为它离查看者更*(z=10 比 z=50 更*)。

先前的渲染行为在 3D 空间中是不期望的。您希望 3D 对象的显示方式与它们在真实世界中的显示方式相同。要做到这一点,你需要做两件事。

  • 当创建一个Scene对象时,指定它需要有一个深度缓冲。

  • 在节点中指定渲染期间应使用的 z 坐标值。也就是说,需要根据它们的深度(离观察者的距离)来渲染它们。

当您创建一个Scene对象时,您需要指定depthBuffer标志,默认设置为 false:

// Create a Scene object with depthBuffer set to true
double width = 300;
double height = 200;
boolean depthBuffer = true;
Scene scene = new Scene(root, width, height, depthBuffer);

场景创建后,不能更改场景的depthBuffer标志。你可以使用Scene对象的isDepthBuffer()方法来检查场景是否有depthBuffer

Node类包含一个depthTest属性,可用于 JavaFX 中的所有节点。它的值是javafx.scene.DepthTest枚举的常量之一:

  • ENABLE

  • DISABLE

  • INHERIT

d epthTestENABLE值表示在渲染节点时应该考虑 z 坐标值。当为节点启用深度测试时,在渲染之前,它的 z 坐标会与启用深度测试的所有其他节点进行比较。

DISABLE值表示节点按照它们被添加到场景图的顺序被渲染。

INHERIT值表示节点的depthTest属性是从其父节点继承的。如果一个节点有一个null父节点,它与ENABLE相同。

清单 16-2 中的程序演示了使用场景深度缓冲和节点深度测试的概念。它将两个矩形添加到一个组中。矩形用红色和绿色填充。红色和绿色矩形的 z 坐标分别是 400 像素和 300 像素。绿色矩形首先被添加到组中。但是,由于它离查看者更*,因此会首先被渲染。您已经向场景中添加了一个摄影机,这是查看具有深度(z 坐标)的对象所必需的。CheckBox用于启用和禁用矩形的深度测试。当深度测试被禁用时,矩形按照它们被添加到组中的顺序进行渲染:绿色矩形跟随红色矩形。图 16-4 显示了两种状态下的矩形。

img/336502_2_En_16_Fig4_HTML.jpg

图 16-4

depthTest 属性对渲染节点的影响

// DepthTestCheck.java
package com.jdojo.shape3d;

import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.PerspectiveCamera;
import javafx.scene.Scene;
import javafx.scene.control.CheckBox;
import javafx.scene.layout.BorderPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.DepthTest;
import javafx.stage.Stage;

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

        @Override
        public void start(Stage stage) {
                // Create two rectangles and add then to a Group
                Rectangle red = new Rectangle(100, 100);
                red.setFill(Color.RED);
                red.setTranslateX(100);
                red.setTranslateY(100);
                red.setTranslateZ(400);

                Rectangle green = new Rectangle(100, 100);
                green.setFill(Color.GREEN);
                green.setTranslateX(150);
                green.setTranslateY(150);
                green.setTranslateZ(300);

                Group center = new Group(green, red);

                CheckBox depthTestCbx =
                         new CheckBox("DepthTest for Rectangles");
                depthTestCbx.setSelected(true);
                depthTestCbx.selectedProperty().addListener(
                    (prop, oldValue, newValue) -> {
                        if (newValue) {
                            red.setDepthTest(DepthTest.ENABLE);
                            green.setDepthTest(DepthTest.ENABLE);
                        }
                        else {

                            red.setDepthTest(DepthTest.DISABLE);
                            green.setDepthTest(DepthTest.DISABLE);
                        }
                });

                // Create a BorderPane as the root node for the scene.
                // Need to set the background transparent, so the camera
                // can view the rectangles behind the surface of the
                // BorderPane
                BorderPane root = new BorderPane();
                root.setStyle("-fx-background-color: transparent;");
                root.setTop(depthTestCbx);
                root.setCenter(center);

                // Create a scene with depthBuffer enabled
                Scene scene = new Scene(root, 200, 200, true);

                // Need to set a camera to look into the 3D space of
                // the scene
                scene.setCamera(new PerspectiveCamera());

                stage.setScene(scene);
                stage.setTitle("Depth Test");
                stage.show();
        }
}

Listing 16-2Enabling/Disabling the DepthTest Property for Nodes

使用预定义的三维形状

JavaFX 8 提供了以下三种内置 3D 几何图形:

  • Box

  • Sphere

  • Cylinder

这些形状由BoxSphereCylinder类的实例表示。这些类继承自Shape3D类,该类包含所有类型的 3D 形状共有的三个属性:

  • 材料

  • 绘图模式

  • 剔除面

我将在后续章节中详细讨论这些属性。如果没有为形状指定这些属性,则会提供合理的默认值。

特定于形状类型的属性在定义该形状的特定类中定义。例如,盒子的属性是在Box类中定义的。所有形状都是节点。因此,您可以对它们应用变换。您可以使用translateXtranslateYtranslateZ变换将它们定位在 3D 空间中的任意点。

Tip

3D 形状的中心位于该形状的局部坐标系的原点。

一个Box由以下三个属性定义:

  • width

  • height

  • depth

Box类包含两个构造器:

  • Box()

  • Box(double width, double height, double depth)

无参数构造器创建一个宽度、高度和深度均为 2.0 的Box。另一个构造器让您指定Box的尺寸。Box的中心位于其本地坐标系的原点;

// Create a Box with width=10, height=20, and depth=50
Box box = new Box(10, 20, 50);

一个Sphere仅由一个名为radius的属性定义。Sphere类包含三个构造器:

  • Sphere()

  • Sphere(double radius)

  • Sphere(double radius, int divisions)

无参数构造器创建一个半径为 1.0 的球体。

第二个构造器让您指定球体的radius

第三个构造器让您指定radiusdivisions。3D 球体由许多分割部分组成,这些分割部分由相连的三角形构成。划分数量的值定义了球体的分辨率。分割数越高,球体看起来越*滑。默认情况下,divisions的值为 64。divisions的值不能小于 1。

// Create a Sphere with radius =50
Sphere sphere = new Sphere(50);

一个Cylinder由两个属性定义:

  • radius

  • height

气缸的radius在 XZ *面上测量。圆柱体的轴沿 y 轴测量。气缸的height沿其轴线测量。Cylinder类包含三个构造器:

  • Cylinder()

  • Cylinder(double radius, double height)

  • Cylinder(double radius, double height, int divisions)

无参数构造器创建一个带有 1.0 radius和 2.0 heightCylinder

第二个构造器让您指定radiusheight属性。

第三个构造器让您指定divisions的编号,它定义圆柱体的分辨率。分段数越高,圆柱体看起来越*滑。它的缺省值是沿 x 轴和 z 轴各 64(文档指定此处为 15,这是错误的)。其值不能小于 3。如果指定的值小于 3,则使用值 3。请注意,分段数不适用于 y 轴。假设除法的个数是 10。这意味着圆柱体的垂直表面是用 10 个三角形创建的。三角形的高度将延伸到圆柱体的整个高度。圆柱体的底部也将使用 10 个三角形来创建。

// Create a cylinder with radius=40 and height=120
Cylinder cylinder = new Cylinder(40, 120);

清单 16-3 中的程序展示了如何创建 3D 形状。图 16-5 显示了形状。

img/336502_2_En_16_Fig5_HTML.jpg

图 16-5

基本 3D 形状:长方体、球体和圆柱体

// PreDefinedShapes.java
package com.jdojo.shape3d;

import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.PerspectiveCamera;
import javafx.scene.PointLight;
import javafx.scene.Scene;
import javafx.scene.shape.Box;
import javafx.scene.shape.Cylinder;
import javafx.scene.shape.Sphere;
import javafx.stage.Stage;

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

        @Override

        public void start(Stage stage) {
                // Create a Box
                Box box = new Box(100, 100, 100);
                box.setTranslateX(150);
                box.setTranslateY(0);
                box.setTranslateZ(400);

                // Create a Sphere

                Sphere sphere = new Sphere(50);
                sphere.setTranslateX(300);
                sphere.setTranslateY(-5);
                sphere.setTranslateZ(400);

                // Create a cylinder
                Cylinder cylinder = new Cylinder(40, 120);
                cylinder.setTranslateX(500);
                cylinder.setTranslateY(-25);
                cylinder.setTranslateZ(600);

                // Create a light
                PointLight light = new PointLight();
                light.setTranslateX(350);
                light.setTranslateY(100);
                light.setTranslateZ(300);

                // Add shapes and a light to the group
                Group root = new Group(box, sphere, cylinder, light);

                // Create a Scene with depth buffer enabled
                Scene scene = new Scene(root, 300, 100, true);

                // Set a camera to view the 3D shapes
                PerspectiveCamera camera = new PerspectiveCamera(false);
                camera.setTranslateX(100);
                camera.setTranslateY(-50);
                camera.setTranslateZ(300);
                scene.setCamera(camera);

                stage.setScene(scene);
                stage.setTitle(
                         "Using 3D Shapes: Box, Sphere and Cylinder");
                stage.show();
        }
}

Listing 16-3Creating 3D Primitive Shapes: Box, Sphere, and Cylinder

该程序创建了三个形状,并将它们放置在空间中。它创建一个灯光,它是PointLight的一个实例,并将其放置在空间中。注意,一盏灯也是一个Node。灯光用于照亮 3D 形状。所有形状和灯光被添加到一个组中,该组被添加到场景中。

要查看形状,需要在场景中添加一个摄像机。程序给场景添加了一个PerspectiveCamera。请注意,您需要定位相机,因为它在空间中的位置和方向决定了您所看到的内容。相机的局部坐标系的原点位于场景的中心。运行程序后,尝试调整窗口大小。您会注意到,当您调整窗口大小时,形状的视图会发生变化。发生这种情况是因为当您调整窗口大小时,场景的中心发生了变化,从而重新定位了相机,导致视图发生变化。

指定形状材料

材质用于渲染形状的表面。您可以使用在Shape3D类中定义的material属性指定 3D 对象表面的材质。material 属性是抽象类Material的一个实例。JavaFX 提供了PhongMaterial类作为Material的唯一具体实现。这两个类都在javafx.scene.paint包中。l 类的一个实例代表 Phong 着色材质。Phong shaded material 基于 Phong shading 和 Phong reflection model(也称为 Phong illumination 和 Phong lighting ),这是裴祥风在 1973 年的博士论文中在犹他大学开发的。对 Phong 模型的完整讨论超出了本书的范围。该模型提供了一个经验公式,根据PhongMaterial类中定义的以下属性来计算几何表面上像素的颜色:

  • diffuseColor

  • diffuseMap

  • specularColor

  • specularMap

  • selfIlluminationMap

  • specularPower

  • bumpMap

PhongMaterial类包含三个构造器:

  • PhongMaterial()

  • PhongMaterial(Color diffuseColor)

  • PhongMaterial(Color diffuseColor, Image diffuseMap, Image specularMap, Image bumpMap, Image selfIlluminationMap)

无参数构造器创建一个漫射颜色为Color.WHITEPhongMaterial。另外两个构造器用于创建一个具有指定属性的PhongMaterial

当您没有为 3D 形状提供材质时,将使用漫射颜色为Color.LIGHTGRAY的默认材质来渲染形状。清单 16-3 中的所有形状都使用默认材质。

下面的代码片段创建了一个Box,创建了一个带有褐色漫射颜色的PhongMaterial,并将材质设置为长方体:

Box box = new Box(100, 100, 100);
PhongMaterial material = new PhongMaterial();
material.setDiffuseColor(Color.TAN);
box.setMaterial(material);

你可以使用一个Image作为漫反射贴图来获得材质的纹理,如下面的代码所示:

Box boxWithTexture = new Box(100, 100, 100);
PhongMaterial textureMaterial = new PhongMaterial();
Image randomness = new Image("resources/picture/randomness.jpg");
textureMaterial.setDiffuseMap(randomness);
boxWithTexture.setMaterial(textureMaterial);

清单 16-4 中的程序显示了如何创建和设置形状的材质。它创建了两个盒子。它为一个框设置漫射颜色,为另一个框设置漫射贴图。用于漫反射贴图的图像为第二个长方体的表面提供了纹理。这两个盒子的外观如图 16-6 所示。

img/336502_2_En_16_Fig6_HTML.jpg

图 16-6

两个盒子:一个有褐色的漫射颜色,一个有使用漫射贴图的纹理

// MaterialTest.java
package com.jdojo.shape3d;

import com.jdojo.util.ResourceUtil;

import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.PerspectiveCamera;
import javafx.scene.PointLight;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.paint.Color;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.Box;
import javafx.stage.Stage;

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

        @Override
        public void start(Stage stage) {
                // Create a Box
                Box box = new Box(100, 100, 100);

                // Set the material for the box
                PhongMaterial material = new PhongMaterial();
                material.setDiffuseColor(Color.TAN);
                box.setMaterial(material);

                // Place the box in the space
                box.setTranslateX(250);
                box.setTranslateY(0);
                box.setTranslateZ(400);

                // Create a Box with texture
                Box boxWithTexture = new Box(100, 100, 100);
                PhongMaterial textureMaterial = new PhongMaterial();
                     Image randomness =
                         new Image(ResourceUtil.getResourceURLStr(
                             "picture/randomness.jpg"));
                textureMaterial.setDiffuseMap(randomness);
                boxWithTexture.setMaterial(textureMaterial);

                // Place the box in the space
                boxWithTexture.setTranslateX(450);
                boxWithTexture.setTranslateY(-5);
                boxWithTexture.setTranslateZ(400);

                PointLight light = new PointLight();
                light.setTranslateX(250);
                light.setTranslateY(100);
                light.setTranslateZ(300);

                Group root = new Group(box, boxWithTexture);

                // Create a Scene with depth buffer enabled
                Scene scene = new Scene(root, 300, 100, true);

                // Set a camera to view the 3D shapes
                PerspectiveCamera camera = new PerspectiveCamera(false);
                camera.setTranslateX(200);
                camera.setTranslateY(-50);
                camera.setTranslateZ(325);
                scene.setCamera(camera);

                stage.setScene(scene);
                stage.setTitle(

                         "Using Material Color and Texture for 3D Surface");
                stage.show();
        }
}

Listing 16-4Using the Diffuse Color and Diffuse Map to Create PhongMaterial

指定形状的绘制模式

3D 形状表面由许多由三角形组成的相连多边形组成。例如,Box由 12 个三角形组成——Box的每一边使用两个三角形。Shape3D类中的drawMode属性指定如何渲染 3D 形状的表面。它的值是DrawMode枚举的常量之一:

  • DrawMode.FILL

  • DrawMode.LINE

DrawMode.FILL是默认值,它填充三角形的内部。DrawMode.LINE只画三角形的轮廓。也就是说,它只绘制连接连续三角形顶点的线。

// Create a Box with outline only
Box box = new Box(100, 100, 100);
box.setDrawMode(DrawMode.LINE);

清单 16-5 中的程序显示了如何只绘制 3D 形状的轮廓。图 16-7 显示了形状。该程序类似于清单 16-3 中所示的程序。程序将所有形状的drawMode属性设置为DrawMode.LINE。程序规定了创建SphereCylinder的分工。将“分割”的值更改为较小的值。您会注意到,用于创建形状的三角形数量减少,使形状变得不太*滑。

img/336502_2_En_16_Fig7_HTML.jpg

图 16-7

绘制 3D 形状的轮廓

// DrawModeTest.java
package com.jdojo.shape3d;

import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.PerspectiveCamera;
import javafx.scene.PointLight;
import javafx.scene.Scene;
import javafx.scene.shape.Box;
import javafx.scene.shape.Cylinder;
import javafx.scene.shape.DrawMode;
import javafx.scene.shape.Sphere;
import javafx.stage.Stage;

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

        @Override
        public void start(Stage stage) {
                // Create a Box
                Box box = new Box(100, 100, 100);
                box.setDrawMode(DrawMode.LINE);
                box.setTranslateX(150);
                box.setTranslateY(0);
                box.setTranslateZ(400);

                // Create a Sphere: radius = 50, divisions=20
                Sphere sphere = new Sphere(50, 20);
                sphere.setDrawMode(DrawMode.LINE);
                sphere.setTranslateX(300);
                sphere.setTranslateY(-5);
                sphere.setTranslateZ(400);

                // Create a cylinder: radius=40, height=120, divisions=5
                Cylinder cylinder = new Cylinder(40, 120, 5);
                cylinder.setDrawMode(DrawMode.LINE);
                cylinder.setTranslateX(500);
                cylinder.setTranslateY(-25);
                cylinder.setTranslateZ(600);

                PointLight light = new PointLight();
                light.setTranslateX(350);
                light.setTranslateY(100);
                light.setTranslateZ(300);

                Group root = new Group(box, sphere, cylinder, light);

                // Create a Scene with depth buffer enabled
                Scene scene = new Scene(root, 300, 100, true);

                // Set a camera to view the 3D shapes
                PerspectiveCamera camera = new PerspectiveCamera(false);
                camera.setTranslateX(100);
                camera.setTranslateY(-50);
                camera.setTranslateZ(300);
                scene.setCamera(camera);

                stage.setScene(scene);
                stage.setTitle("Drawing Only Lines");
                stage.show();
        }
}

Listing 16-5Drawing Only Lines for 3D Shapes

指定形状的面剔除

3D 对象永远不会完全可见。例如,你不可能一次看到整栋建筑。当你改变视角时,你会看到建筑的不同部分。如果你面向建筑物的正面,你只能看到建筑物的正面。站在前面,如果你向右移动,你会看到建筑物的正面和右边。

3D 对象的表面由相连的三角形组成。每个三角形都有两个面:外部面和内部面。当您查看 3D 对象时,您会看到三角形的外表面。不是所有的三角形都是可见的。三角形是否可见取决于相机的位置。有一个简单的规则来确定组成 3D 对象表面的三角形的可见性。画一条从三角形的*面出来的线,这条线垂直于三角形的*面。从第一条线与三角形*面的交点向观察者再画一条线。如果两条线之间的角度大于 90 度,则视图看不到三角形的面。否则,观察者可以看到三角形的面。请注意,三角形的两面不是同时可见的。

面剔除是一种渲染 3D 几何图形的技术,其原理是对象的不可见部分不应被渲染。例如,如果您从正面面对一座建筑,则不需要渲染建筑的侧面、顶部和底部,因为您看不到它们。

Tip

在 3D 渲染中使用面剔除来提高性能。

Shape3D类包含一个cullFace属性,该属性指定在呈现形状时应用的剔除类型。它的值是CullFace枚举的常量之一:

  • BACK

  • FRONT

  • NONE

CullFace.BACK指定在当前位置不能通过摄像机看到的所有三角形都应该被剔除(即不渲染)。也就是说,应该剔除所有外部面不面向相机的三角形。如果你正对着建筑物的正面,这个设置将只渲染建筑物的正面。这是默认设置。

CullFace.FRONT指定所有外部面向摄像机的三角形都应该被剔除。如果你面对建筑物的正面,这个设置将渲染建筑物的所有部分,除了正面部分。

CullFace.NONE指定不应该应用面剔除。也就是说,组成形状的所有三角形都应该被渲染:

// Create a Box with no face culling
Box box = new Box(100, 100, 100);
Box.setCullFace(CullFace.NONE);

当您使用drawMode作为DrawMode.LINE来绘制形状时,很容易看到面剔除的效果。我将只画非空三角形。图 16-8 显示了相同的Box使用三种不同的面剔除。第一个Box(左起)使用背面剔除,第二个正面剔除,第三个不使用剔除。请注意,Box的第一张图片显示了正面、正面和顶面,而这些面在第二张Box中被剔除。在第二张图片中,你可以看到背面、左面和底面。请注意,当您使用正面剔除时,您会看到三角形的内部面,因为外部面在视图中是隐藏的。

img/336502_2_En_16_Fig8_HTML.png

图 16-8

使用不同 cullFace 属性的框

使用相机

摄像机用于渲染场景。有两种类型的摄像机可用:

  • 透视照相机

  • *行照相机

摄像机的名称暗示了它们用于渲染场景的投影类型。JavaFX 中的摄像机是节点。可以将它们添加到场景图中,并像其他节点一样定位。

抽象基类Camera代表一个相机。Camera类有两个具体的子类:PerspectiveCameraParallelCamera。这三个职业都在javafx.scene计划中。

A PerspectiveCamera定义了透视投影的观察体积,它是一个截顶的正棱锥,如图 16-9 所示。相机将包含在*剪裁*面和远剪裁*面内的对象投影到投影*面上。因此,剪裁*面外的任何对象都不可见。

img/336502_2_En_16_Fig9_HTML.png

图 16-9

由*剪裁*面和远剪裁*面定义的透视相机的观察体积

摄像机将投影到投影*面上的内容由Camera类中的两个属性定义:

  • nearClip

  • farClip

nearClip是摄像机和最*的裁剪*面之间的距离。比nearClip更靠*摄像机的物体不会被渲染。The default value is 0.1

farClip是摄像机和远裁剪*面之间的距离。比farClip距离摄像机更远的物体不会被渲染。默认值为 100。

PerspectiveCamera类包含两个构造器:

  • PerspectiveCamera()

  • PerspectiveCamera(boolean fixedEyeAtCameraZero)

无参数构造器创建一个PerspectiveCamera并将fixedEyeAtCameraZero标志设置为 false,这使它的行为或多或少像一个*行相机,其中当场景调整大小时,场景中 Z=0 处的对象保持相同的大小。第二个构造器让您指定这个标志。如果您想要查看具有真实 3D 效果的 3D 对象,您需要将此标志设置为 true。将此标志设置为 true 将在调整场景大小时调整 3D 对象的投影图像的大小。缩小场景也会使物体看起来更小。

// Create a perspective camera for viewing 3D objects
PerspectiveCamera camera = new PerspectiveCamera(true);

PerspectiveCamera类声明了两个附加属性:

  • fieldOfView

  • verticalFieldOfView

fieldOfView以度为单位,是相机的视角。其默认值为 30 度。

verticalFieldOfView属性指定fieldOfView属性是否适用于投影*面的垂直尺寸。默认情况下,其值为 true。图 16-10 描述了摄像机及其视角和视野。

img/336502_2_En_16_Fig10_HTML.png

图 16-10

透视相机的视角和视野

ParallelCamera的一个实例指定了*行投影的观察体积,它是一个矩形框。ParallelCamera类没有声明任何额外的属性。它包含一个无参数构造器:

ParallelCamera camera = new ParallelCamera();

您可以使用Scene类的setCamera()方法为场景设置摄像机:

Scene scene = create a scene....
PerspectiveCamera camera = new PerspectiveCamera(true);
scene.setCamera(camera);

因为摄影机是一个节点,所以可以将其添加到场景图形中:

PerspectiveCamera camera = new PerspectiveCamera(true);
Group group = new Group(camera);

在移动和旋转节点时,可以移动和旋转摄影机。要将其移动到不同的位置,请使用translateXtranslateYtranslateZ属性。要旋转,请使用Rotate变换。

清单 16-6 中的程序使用一个PerspectiveCamera来查看一个Box。您已经使用了两个灯光:一个照亮盒子的正面和顶面,另一个照亮盒子的底面。通过沿 x 轴无限旋转相机来激活相机。随着摄像机的旋转,它将盒子的不同部分带入视野。当盒子底部进入视野时,您可以看到两盏灯的效果。底部显示为绿色,而顶部和前面显示为红色。

// CameraTest.java
package com.jdojo.shape3d;

import javafx.animation.Animation;
import javafx.animation.RotateTransition;
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.PerspectiveCamera;
import javafx.scene.PointLight;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.scene.shape.Box;
import javafx.scene.shape.CullFace;
import javafx.scene.transform.Rotate;
import javafx.stage.Stage;
import javafx.util.Duration;

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

        @Override
        public void start(Stage stage) {
                Box box = new Box(100, 100, 100);
                box.setCullFace(CullFace.NONE);
                box.setTranslateX(250);
                box.setTranslateY(100);
                box.setTranslateZ(400);

                PerspectiveCamera camera = new PerspectiveCamera(false);
                camera.setTranslateX(100);
                camera.setTranslateY(-50);
                camera.setTranslateZ(300);

                // Add a Rotation animation to the camera
                RotateTransition rt =
                         new RotateTransition(Duration.seconds(2), camera);
                rt.setCycleCount(Animation.INDEFINITE);
                rt.setFromAngle(0);
                rt.setToAngle(90);
                rt.setAutoReverse(true);
                rt.setAxis(Rotate.X_AXIS);
                rt.play();

                PointLight redLight = new PointLight();
                redLight.setColor(Color.RED);
                redLight.setTranslateX(250);
                redLight.setTranslateY(-100);
                redLight.setTranslateZ(250);

                PointLight greenLight = new PointLight();
                greenLight.setColor(Color.GREEN);
                greenLight.setTranslateX(250);
                greenLight.setTranslateY(300);
                greenLight.setTranslateZ(300);

                Group root = new Group(box, redLight, greenLight);
                root.setRotationAxis(Rotate.X_AXIS);
                root.setRotate(30);

                Scene scene = new Scene(root, 500, 300, true);
                scene.setCamera(camera);
                stage.setScene(scene);
                stage.setTitle("Using camaras");
                stage.show();
        }

}

Listing 16-6Using a PerspectiveCamera As a Node

使用光源

与真实世界类似,您需要一个光源来查看场景中的 3D 对象。抽象基类LightBase的一个实例代表一个光源。它的两个具体子类,AmbientLightPointLight,代表环境光和点光。光源类别在javafx.scene包中。LightBase类继承自Node类。因此,光源是一个节点,它可以像任何其他节点一样添加到场景图形中。

光源有三个属性:灯光颜色、开/关开关和受影响的节点列表。LightBase类基本上包含以下两个属性:

  • color

  • lightOn

color指定光线的颜色。lightOn指定灯是否打开。LightBase类的getScope()方法返回一个ObservableList<Node>,它是受这个光源影响的节点的层次列表。如果列表为空,则光源的范围是 universe,这意味着它影响场景中的所有节点。然而,后者不影响属于排除范围的节点;有关详细信息,请参见 API 文档。

AmbientLight类的一个实例代表一个环境光源。环境光是一种似乎来自所有方向的无方向性光。其强度在受影响形状的表面上是恒定的。

// Create a red ambient light
AmbientLight redLight = new AmbientLight(Color.RED);

类的一个实例代表一个点光源。点光源是空间中的固定点,向所有方向均匀辐射光线。点光源的强度随着照明点离光源的距离增加而降低。

// Create a Add the point light to a group
PointLight redLight = new PointLight(Color.RED);
redLight.setTranslateX(250);
redLight.setTranslateY(-100);
redLight.setTranslateZ(290);
Group group = new Group(node1, node2, redLight);

创建子场景

一个场景只能使用一个摄像机。有时,您可能想要使用多个相机查看场景的不同部分。JavaFX 引入了子场景的概念。子场景是场景图的容器。它可以有自己的宽度、高度、填充颜色、深度缓冲、抗锯齿标志和相机。SubScene类的一个实例代表一个子场景。SubScene继承自Node类。因此,只要可以使用节点,就可以使用子场景。子场景可用于分隔应用程序中的 2D 和 3D 节点。您可以在子场景中使用摄像机来查看 3D 对象,这些对象不会影响主场景其他部分的 2D 节点。下面的代码片段创建了一个SubScene,并为其设置了一个摄像机:

SubScene ss = new SubScene(root, 200, 200, true, SceneAntialiasing.BALANCED);
PerspectiveCamera camera = new PerspectiveCamera(false);
ss.setCamera(camera);

Tip

如果SubScene包含具有灯光节点的Shape3D节点,则提供具有带Color.WHITE光源的PointLight的前照灯。前照灯位于摄像机位置。

清单 16-7 中的程序展示了如何使用子场景。getSubScene()方法创建一个带有BoxPerspectiveCameraPointLightSubScene。动画设置为沿指定轴旋转相机。start()方法创建两个子场景并将它们添加到一个HBox中。一个子场景沿 y 轴摆动相机,另一个子场景沿 x 轴摆动相机。HBox被添加到主场景中。

// SubSceneTest.java
package com.jdojo.shape3d;

import javafx.animation.Animation;
import javafx.animation.RotateTransition;
import javafx.application.Application;
import javafx.geometry.Point3D;
import javafx.scene.Group;
import javafx.scene.PerspectiveCamera;
import javafx.scene.PointLight;
import javafx.scene.Scene;
import javafx.scene.SceneAntialiasing;
import javafx.scene.SubScene;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Box;
import javafx.scene.shape.CullFace;
import javafx.scene.transform.Rotate;
import javafx.stage.Stage;
import javafx.util.Duration;

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

        @Override
        public void start(Stage stage) {
                SubScene ySwing = getSubScene(Rotate.Y_AXIS);
                SubScene xSwing = getSubScene(Rotate.X_AXIS);
                HBox root = new HBox(20, ySwing, xSwing);
                Scene scene = new Scene(root, 500, 300, true);
                stage.setScene(scene);
                stage.setTitle("Using Sub-Scenes");
                stage.show();
        }

        private SubScene getSubScene(Point3D rotationAxis) {
                Box box = new Box(100, 100, 100);
                box.setCullFace(CullFace.NONE);
                box.setTranslateX(250);
                box.setTranslateY(100);
                box.setTranslateZ(400);

                PerspectiveCamera camera = new PerspectiveCamera(false);
                camera.setTranslateX(100);
                camera.setTranslateY(-50);
                camera.setTranslateZ(300);

                // Add a Rotation animation to the camera
                RotateTransition rt =
                         new RotateTransition(Duration.seconds(2), camera);
                rt.setCycleCount(Animation.INDEFINITE);
                rt.setFromAngle(-10);
                rt.setToAngle(10);
                rt.setAutoReverse(true);
                rt.setAxis(rotationAxis);
                rt.play();

                PointLight redLight = new PointLight(Color.RED);
                redLight.setTranslateX(250);
                redLight.setTranslateY(-100);
                redLight.setTranslateZ(290);

                // If you remove the redLight from the following group,
                // a default head light will be provided by the SubScene

.
                Group root = new Group(box, redLight);
                root.setRotationAxis(Rotate.X_AXIS);
                root.setRotate(30);

                SubScene ss =
                          new SubScene(root, 200, 200, true,
                                       SceneAntialiasing.BALANCED);
                ss.setCamera(camera);
                return ss;
        }
}

Listing 16-7Using Subscenes

创建用户定义的形状

JavaFX 允许您使用多边形网格定义 3D 形状。抽象类Mesh的一个实例表示网格数据。TriangleMesh类是Mesh类的一个具体子类。TriangleMesh表示由三角形网格组成的 3D 表面。

Tip

在 3D 建模中,不同类型多边形的网格可用于构建 3D 对象。JavaFX 仅支持三角形网格。

MeshView类的一个实例代表一个 3D 表面。用于构造MeshView的数据被指定为Mesh的实例。

手动提供网格数据不是一件容易的事情。这个问题因为你需要指定数据的方式而变得复杂。我将通过从一个非常简单的用例到一个更复杂的用例来演示网格的使用,使它变得更简单。

一个TriangleMesh需要为一个 3D 对象的三个方面提供数据:

  • 纹理坐标

  • 表面

Note

如果您以前没有使用三角形网格处理过 3D 对象,解释可能会有点复杂。您需要耐心,一步一步地学习,以理解使用三角形网格创建 3D 对象的过程。

点是网格中三角形的顶点。您需要指定数组中顶点的(x,y,z)坐标。假设 v0、v1、v2、v3、v4 等是 3D 空间中表示网格中三角形顶点的点。一个TriangleMesh中的点被指定为一个floats的数组。

3D 表面的纹理被提供为作为 2D 对象的图像。纹理坐标是 2D *面中的点,它们被映射到三角形的顶点。你需要把一个展开的网格中的三角形放到一个 2D *面上。将为 3D 形状提供表面纹理的图像叠加到同一 2D *面上。将三角形的顶点映射到图像的 2D 坐标,以获得网格中每个顶点的一对(u,v)坐标。这种(u,v)坐标的数组就是纹理坐标。假设 t0、t1、t2、t3、t4 等等是纹理坐标。

面是通过连接三角形的三条边创建的*面。每个三角形有两个面:一个正面和一个背面。根据pointstexture coordinates数组中的索引指定一个面。一个面被指定为 v0,t0,v1,t1,v2,t2 等等,其中 v1 是顶点在points数组中的索引,t1 是顶点在texture coordinates数组中的索引。

考虑图 16-11 中所示的盒子。

img/336502_2_En_16_Fig11_HTML.png

图 16-11

由 12 个三角形组成的盒子

一个盒子由六个面组成。每条边都是长方形。每个矩形由两个三角形组成。每个三角形有两个面:一个正面和一个背面。一个盒子有八个顶点。在图中,您将顶点命名为 v0、v1、v2 等,将面命名为 f0、f1、f2 等。您看不到在长方体的当前方向上不可见的顶点和面的编号。每个顶点由三元组(x,y,z)定义,三元组是顶点在 3D 空间中的坐标。当您使用术语顶点 v1 时,从技术上讲,您指的是顶点的坐标(x1,y1,z1)。

要创建三角形网格,您需要指定组成 3D 对象的所有顶点。对于长方体,需要指定八个顶点。在TriangleMesh类中,顶点被称为points,它们被指定为float的可观察数组。下面的伪代码创建顶点数组。第一个数组只是为了便于理解。实际数组指定顶点的坐标:

// For understanding purpose only
float[] points = {
    v0,
    v1,
   v2,
   ...
   v7};

// The actual array contain (x, y, z) coordinates of all vertices
float[] points = {
    x0, y0, z0, // v0
   x1, y1, z1, // v1
   x2, y2, z2, // v2
   ...
   x7, y7, z7  // v7
};

points数组中,索引 0 到 2 包含第一个顶点的坐标,索引 3 到 5 包含第二个顶点的坐标,依此类推。你如何给顶点编号?即哪个顶点是#1,哪个是#2,以此类推?没有指定顶点顺序的规则。如何给它们编号完全取决于你。JavaFX 只关心一件事:您必须在points数组中包含组成形状的所有顶点。您已经完成了生成points数组的工作。你以后会用到它。

现在,您需要创建一个包含 2D 点坐标的数组。创建这个数组有点棘手。初学者很难理解这一点。考虑图 16-12 中所示的图形。

img/336502_2_En_16_Fig12_HTML.png

图 16-12

映射到 2D *面上的盒子的曲面

图 16-11 和 16-12 是同一箱体表面的两个视图。图 16-12 将表面从三维空间映射到 2D *面。把这个盒子想象成一个由 12 张三角形纸片组成的 3D 物体。图 16-11 显示了这 12 张纸放在一起形成一个 3D 盒子,而图 16-12 显示了同样的纸并排放在地板上(一个 2D *面)。

Tip

由您决定如何将 3D 对象的表面映射到 2D *面。例如,在图 16-12 中,你也可以将盒子的底边映射到单位正方形的下方、左侧或上方。

想一个你想用作盒子纹理的图像。图像将没有第三维(z 维)。图像需要应用在盒子的表面。JavaFX 需要知道盒子上的顶点如何映射到图像上的点。您可以通过将长方体顶点映射到图像上的点来提供此信息。

现在,想象一个代表纹理图像的单位正方形(1 x 1 正方形)。将单位正方形覆盖在盒子的展开面上。单位正方形如图 16-12 中虚线轮廓所示。正方形的左上角有坐标(0,0);左下角有坐标(0,1);右上角有坐标(1,0);右下角的坐标是(1,1)。

在图 16-12 中,当你打开盒子的表面把它放到一个 2D *面上时,一些顶点必须被分割成多个顶点。这个盒子有八个顶点。映射到 2D *面的盒子有 14 个顶点。该图示出了一些顶点,这些顶点具有与 3D 框中表示相同顶点的那些顶点相同的数目。映射到 2D *面的每个顶点(在图 16-12 中)成为texture coordinates数组中的一个元素。图 16-13 显示了这 14 个纹理点;它们被编号为 t0、t1、t2 等等。你可以按照任何顺序在 2D *面上给长方体的顶点编号。纹理点的 x 和 y 坐标将在 0 和 1 之间。这些坐标到实际图像大小的实际映射由 JavaFX 执行。比如,(0.25,0。)可用于顶点 t9 的坐标,而(0.25,0.25)可用于顶点 t10 的坐标。

img/336502_2_En_16_Fig13_HTML.png

图 16-13

用纹理坐标映射到 2D *面上的盒子表面

您可以创建如以下代码所示的texture coordinates数组。和points数组一样,下面是伪代码。第一个数组用于理解概念,第二个数组是代码中实际使用的数组:

// For understanding purpose-only
float[] texCoords = {
    t0,
   t1,
   t2,
   ...
   t14};

// The actual texture coordinates of vertices
float[] texCoords = {
    x0, y0, // t0
   x1, y1, // t1
   x2, y2, // t2
   ...
   x13, y13 // t13
};

您需要指定的第三条信息是一组面孔。注意,每个三角形都有两个面。在我们的图中,你只显示了三角形的正面。指定面是创建一个TriangleMesh对象中最令人困惑的步骤。使用points数组和texture coordinates数组指定一个面。使用point数组中顶点的索引和texture coordinates数组中纹理点的索引来指定一个面。使用以下格式的六个整数指定一个面:

iv0, it0, iv1, it1, iv2, it2

这里

  • iv0points数组中顶点v0的索引,it0texture coordinates数组中点t0的索引。

  • iv1it1pointstexture coordinates数组中顶点v1和点t1的索引。

  • iv2it2pointstexture coordinates数组中顶点v2和点t2的索引。

图 16-14 只显示了两个三角形,它们构成了盒子的正面。

img/336502_2_En_16_Fig14_HTML.png

图 16-14

盒子的两个三角形,它们的顶点在点和纹理坐标数组中

图 16-14 是图 16-12 和 16-13 中所示数字的叠加。图中显示了顶点编号及其对应的纹理坐标点数。要指定faces数组中的 f0,可以用两种方式指定三角形的顶点,逆时针和顺时针:

ivo, it1, iv2, it7, iv3, it10 (Counterclockwise)
ivo, it1, iv3, it10, iv2, it7 (Clockwise)

在指定面时,起始顶点并不重要。你可以从任何顶点开始,顺时针或逆时针方向。当一个面的顶点按逆时针方向指定时,它被认为是正面。否则被认为是背面。以下一系列数字将指定我们图中的面f1:

ivo, it1, iv1, it2, iv2, it7 (Counterclockwise: front-face)
ivo, it1, iv2, it7, iv1, it2 (Clockwise: back-face)

为了确定您是指定正面还是背面,应用以下规则,如图 16-15 所示:

img/336502_2_En_16_Fig15_HTML.png

图 16-15

三角形顶点的缠绕顺序

  • 画一条垂直于三角形表面向外延伸的线。

  • 想象一下,你正沿着这条线看着表面。

  • 尝试逆时针遍历顶点。顶点序列会给你一个正面。如果你顺时针遍历顶点,序列会给你一个背面。

下面的伪代码演示了如何创建一个用于指定面的int数组。int值是来自pointstexture coordinates数组的数组索引:

int[] faces = new int[] {
ivo, it1, iv2, it7, iv3, it10, // f0: front-face
ivo, it1, iv3, it10, iv2, it7, // f0: back-face
ivo, it1, iv1, it2, iv2, it7,  // f1: front-face
ivo, it1, iv2, it7, iv1, it2   // f1: back-face
...
};

一旦有了pointstexture coordinatesfaces数组,就可以如下构造一个TriangleMesh对象:

TriangleMesh mesh = new TriangleMesh();
mesh.getPoints().addAll(points);
mesh.getTexCoords().addAll(texCoords);
mesh.getFaces().addAll(faces);

TriangleMesh为构建用户定义的 3D 对象提供数据。一个MeshView对象用指定的TriangleMesh为对象创建表面:

// Create a MeshView
MeshView meshView = new MeshView();
meshView.setMesh(mesh);

一旦你有了一个MeshView对象,你需要把它添加到一个场景图中来查看它。您可以像查看预定义的 3D 形状BoxesSpheresCylinders一样查看它。

在接下来的几节中,您将使用TriangleMesh创建 3D 对象。您将从最简单的 3D 对象开始,它是一个三角形。

创建 3D 三角形

你可能会认为三角形是 2D 形状,而不是三维形状。人们一致认为三角形是 2D 形状。您将使用TriangleMesh在 3D 空间中创建一个三角形。三角形将有两个面。选择这个例子是因为它是你可以用三角形网格创建的最简单的形状。在三角形的情况下,网格只包含一个三角形。图 16-16 显示了三维空间中的一个三角形,它的顶点映射到一个 2D *面上。

img/336502_2_En_16_Fig16_HTML.png

图 16-16

三维空间中三角形的顶点,并映射到 2D *面上

可以使用一个三角形的网格来创建三角形。让我们为TriangleMesh对象创建points数组:

float[] points = {50, 0, 0,  // v0 (iv0 = 0)
             45, 10, 0, // v1 (iv1 = 1)
             55, 10, 0  // v2 (iv2 = 2)
};

图的第二部分显示在右边,将三角形的顶点映射到一个单位正方形。您可以按如下方式创建texture coordinates数组:

float[] texCoords = {0.5f, 0.5f,  // t0 (it0 = 0)
     0.0f, 1.0f,  // t1 (it1 = 1)
    1.0f, 1.0f   // t2 (it2 = 2)
};

使用pointstexture coordinates数组,您可以如下指定faces数组:

int[] faces = {
    0, 0, 2, 2, 1, 1,  // iv0, it0, iv2, it2, iv1, it1 (front face)
   0, 0, 1, 1, 2, 2   // iv0, it0, iv1, it1, iv2, it2 back face
};

清单 16-8 包含使用TriangleMesh创建三角形的完整程序。它添加了两种不同的灯光来照亮三角形的两面。动画会旋转相机,因此您可以用不同的颜色查看三角形的两边。createMeshView()方法有坐标值和逻辑来创建MeshView

// TriangleWithAMesh.java
package com.jdojo.shape3d;

import javafx.animation.Animation;
import javafx.animation.RotateTransition;
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.PerspectiveCamera;
import javafx.scene.PointLight;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.scene.shape.MeshView;
import javafx.scene.shape.TriangleMesh;
import javafx.scene.transform.Rotate;
import javafx.stage.Stage;
import javafx.util.Duration;

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

        @Override
        public void start(Stage stage) {
                // Create a MeshView and position it in the space
                MeshView meshView = this.createMeshView();
                meshView.setTranslateX(250);
                meshView.setTranslateY(100);
                meshView.setTranslateZ(400);

                // Scale the Meshview to make it look bigger
                meshView.setScaleX(10.0);
                meshView.setScaleY(10.0);
                meshView.setScaleZ(10.0);

                PerspectiveCamera camera = new PerspectiveCamera(false);
                camera.setTranslateX(100);
                camera.setTranslateY(-50);
                camera.setTranslateZ(300);

                // Add a Rotation animation to the camera
                RotateTransition rt =
                         new RotateTransition(Duration.seconds(2), camera);
                rt.setCycleCount(Animation.INDEFINITE);
                rt.setFromAngle(-30);
                rt.setToAngle(30);
                rt.setAutoReverse(true);
                rt.setAxis(Rotate.Y_AXIS);
                rt.play();

                // Front light is red

                PointLight redLight = new PointLight();
                redLight.setColor(Color.RED);
                redLight.setTranslateX(250);
                redLight.setTranslateY(150);
                redLight.setTranslateZ(300);

                // Back light is green
                PointLight greenLight = new PointLight();
                greenLight.setColor(Color.GREEN);
                greenLight.setTranslateX(200);
                greenLight.setTranslateY(150);
                greenLight.setTranslateZ(450);

                Group root = new Group(meshView, redLight, greenLight);

                // Rotate the triangle with its lights to 90 degrees
                root.setRotationAxis(Rotate.Y_AXIS);
                root.setRotate(90);

                Scene scene = new Scene(root, 400, 300, true);
                scene.setCamera(camera);
                stage.setScene(scene);
                stage.setTitle(
                         "Creating a Triangle using a TriangleMesh");
                stage.show();
        }

        public MeshView createMeshView() {
                float[] points = {50, 0, 0,  // v0 (iv0 = 0)
                              45, 10, 0, // v1 (iv1 = 1)
                              55, 10, 0  // v2 (iv2 = 2)
                             };

                float[] texCoords = { 0.5f, 0.5f, // t0 (it0 = 0)
                                 0.0f, 1.0f, // t1 (it1 = 1)
                                 1.0f, 1.0f  // t2 (it2 = 2)
                               };

                int[] faces = {
                    0, 0, 2, 2, 1, 1, // iv0, it0, iv2, it2, iv1, it1
                                             // (front face)
                    0, 0, 1, 1, 2, 2  // iv0, it0, iv1, it1, iv2, it2
                                             // (back face)
                };

                // Create a TriangleMesh
                TriangleMesh mesh = new TriangleMesh();
                mesh.getPoints().addAll(points);
                mesh.getTexCoords().addAll(texCoords);
                mesh.getFaces().addAll(faces);

                // Create a MeshView
                MeshView meshView = new MeshView();
                meshView.setMesh(mesh);

                return meshView;
        }
}

Listing 16-8Creating a Triangle Using a TriangleMesh

创建三维矩形

在本节中,您将使用两个三角形的网格创建一个矩形。这将给我们一个机会来运用你到目前为止所学的知识。图 16-17 显示了三维空间中的一个矩形,它的顶点被映射到一个 2D *面上。

img/336502_2_En_16_Fig17_HTML.png

图 16-17

三维空间中矩形的顶点,并映射到 2D *面

这个长方形由两个三角形组成。两个三角形都有两面。在图中,我只显示了两个面 f0 和 f1。以下是矩形四个顶点的points数组:

float[] points = {
    50, 0, 0,  // v0 (iv0 = 0)
   50, 10, 0, // v1 (iv1 = 1)
   60, 10, 0, // v2 (iv2 = 2)
   60, 0, 0   // v3 (iv3 = 3)
};

texture coordinates数组可以按如下方式构建:

float[] texCoords = {
    0.0f, 0.0f,  // t0 (it0 = 0)
   0.0f, 1.0f,  // t1 (it1 = 1)
   1.0f, 1.0f,  // t2 (it2 = 2)
   1.0f, 0.0f   // t3 (it3 = 3)
};

您将如下指定四个面:

int[] faces =
      { 0, 0, 3, 3, 1, 1,  // iv0, it0, iv3, it3, iv1, it1 (f0 front face)
        0, 0, 1, 1, 3, 3,  // iv0, it0, iv1, it1, iv3, it3 (f0 back face)
        1, 1, 3, 3, 2, 2,  // iv1, it1, iv3, it3, iv2, it2 (f1 front face)
        1, 1, 2, 2, 3, 3   // iv1, it1, iv2, it2, iv3, it3 (f1 back face)
      };

如果您将上述三个数组插入清单 16-8 中的createMeshView()方法,您将得到一个旋转的矩形。

创建四面体

现在,您准备创建一个稍微复杂的 3D 对象。您将创建一个四面体。图 16-18 显示了四面体的俯视图。

img/336502_2_En_16_Fig18_HTML.png

图 16-18

四面体

四面体由四个三角形组成。它有四个顶点。三个三角形相交于一点。图 16-19 显示了四面体的两个视图。在左侧,您将四个顶点编号为 v0、v1、v2 和 v3,将四个面编号为 f0、f1、f2 和 f3。请注意,面 f3 是三角形底部的面,从俯视图中看不到。第二个视图展开了四个三角形,在 2D *面上产生了八个顶点。虚线矩形是八个顶点将被映射到的单位正方形。

img/336502_2_En_16_Fig19_HTML.png

图 16-19

三维空间中四面体的顶点,并映射到 2D *面

您可以按如下方式创建pointsfacestexture coordinates数组:

float[] points = {10, 10, 10, // v0 (iv0 = 0)
             20, 20, 0,  // v1 (iv1 = 1)
             0, 20, 0,   // v2 (iv2 = 2)
             10, 20, 20  // v3 (iv3 = 3)
         };

float[] texCoords = {
        0.50f, 0.33f, // t0 (it0 = 0)
        0.25f, 0.75f, // t1 (it1 = 1)
        0.50f, 1.00f, // t2 (it2 = 2)
        0.66f, 0.66f, // t3 (it3 = 3)
        1.00f, 0.35f, // t4 (it4 = 4)
        0.90f, 0.00f, // t5 (it5 = 5)
        0.10f, 0.00f, // t6 (it6 = 6)
        0.00f, 0.35f  // t7 (it7 = 7)
};

int[] faces = {
        0, 0, 2, 1, 1, 3, // f0 front-face
        0, 0, 1, 3, 2, 1, // f0 back-face
        0, 0, 1, 4, 3, 5, // f1 front-face
        0, 0, 3, 5, 1, 4, // f1 back-face
        0, 0, 3, 6, 2, 7, // f2 front-face
        0, 0, 2, 7, 3, 6, // f2 back-face
        1, 3, 3, 2, 2, 1, // f3 front-face
        1, 3, 2, 1, 3, 2  // f3 back-face
};

清单 16-9 包含了一个完整的程序,展示了如何使用TriangleMesh构建一个四面体。四面体沿 y 轴旋转,因此您可以查看它的两个垂直面。图 16-20 显示了带有四面体的窗口。

img/336502_2_En_16_Fig20_HTML.jpg

图 16-20

使用三角形网格的四面体

// Tetrahedron.java
package com.jdojo.shape3d;

import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.PerspectiveCamera;
import javafx.scene.PointLight;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.scene.shape.MeshView;
import javafx.scene.shape.TriangleMesh;
import javafx.scene.transform.Rotate;
import javafx.stage.Stage;

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

        @Override
        public void start(Stage stage) {
                MeshView meshView = this.createMeshView();
                meshView.setTranslateX(250);
                meshView.setTranslateY(50);
                meshView.setTranslateZ(400);

                meshView.setScaleX(10.0);
                meshView.setScaleY(20.0);
                meshView.setScaleZ(10.0);

                PerspectiveCamera camera = new PerspectiveCamera(false);
                camera.setTranslateX(100);
                camera.setTranslateY(0);
                camera.setTranslateZ(100);

                PointLight redLight = new PointLight();
                redLight.setColor(Color.RED);
                redLight.setTranslateX(250);
                redLight.setTranslateY(-100);
                redLight.setTranslateZ(250);

                Group root = new Group(meshView, redLight);
                root.setRotationAxis(Rotate.Y_AXIS);
                root.setRotate(45);

                Scene scene = new Scene(root, 200, 150, true);
                scene.setCamera(camera);
                stage.setScene(scene);
                stage.setTitle("A Tetrahedron using a TriangleMesh");
                stage.show();
        }

        public MeshView createMeshView() {
                float[] points = {10, 10, 10, // v0 (iv0 = 0)
                             20, 20, 0,  // v1 (iv1 = 1)
                             0, 20, 0,   // v2 (iv2 = 2)
                             10, 20, 20  // v3 (iv3 = 3)
                           };

                float[] texCoords = {
                        0.50f, 0.33f, // t0 (it0 = 0)
                        0.25f, 0.75f, // t1 (it1 = 1)
                        0.50f, 1.00f, // t2 (it2 = 2)
                        0.66f, 0.66f, // t3 (it3 = 3)
                        1.00f, 0.35f, // t4 (it4 = 4)
                        0.90f, 0.00f, // t5 (it5 = 5)
                        0.10f, 0.00f, // t6 (it6 = 6)
                        0.00f, 0.35f  // t7 (it7 = 7)
                };

                int[] faces = {
                        0, 0, 2, 1, 1, 3, // f0 front-face
                        0, 0, 1, 3, 2, 1, // f0 back-face
                        0, 0, 1, 4, 3, 5, // f1 front-face
                        0, 0, 3, 5, 1, 4, // f1 back-face
                        0, 0, 3, 6, 2, 7, // f2 front-face
                        0, 0, 2, 7, 3, 6, // f2 back-face
                        1, 3, 3, 2, 2, 1, // f3 front-face
                        1, 3, 2, 1, 3, 2, // f3 back-face
                };

                TriangleMesh mesh = new TriangleMesh();
                mesh.getPoints().addAll(points);
                mesh.getTexCoords().addAll(texCoords);
                mesh.getFaces().addAll(faces);

                MeshView meshView = new MeshView();
                meshView.setMesh(mesh);

                return meshView;
        }
}

Listing 16-9Creating a Tetrahedron Using a TriangleMesh

摘要

在三维空间中绘制的具有三维(长度、宽度和深度)的任何形状被称为 3D 形状,例如立方体、球体、金字塔等。JavaFX 提供 3D 形状作为节点。它提供两种类型的 3D 形状:预定义的形状和用户定义的形状。

长方体、球体和圆柱体是三种预定义的 3D 形状,您可以在 JavaFX 应用程序中轻松使用它们。您可以使用三角形网格创建任何类型的 3D 形状。BoxSphereCylinder类代表三种预定义的形状。MeshView类表示场景中用户定义的 3D 形状。3D 形状类在javafx.scene.shape包中。

JavaFX 3D 支持是一个有条件的特性。如果您的*台不支持该功能,当您运行试图使用 3D 功能的程序时,控制台上会显示一条警告消息。如果您的*台支持 3D,方法Platform.isSupported(ConditionalFeature.SCENE3D)将返回true

在 JavaFX 中处理 3D 对象时,您可能希望离您较*的对象与离您较远的对象重叠。在 JavaFX 中,默认情况下,节点按照添加到场景图的顺序进行渲染。为了使 3D 形状看起来像在现实世界中一样,您需要指定两件事情。首先,当您创建一个Scene对象时,指定它需要有一个深度缓冲区,其次,指定在渲染时应该使用节点的 z 坐标值。

摄像机用于渲染场景。JavaFX 中的摄像机是节点。可以将它们添加到场景图中,并像其他节点一样定位。透视相机和*行相机是 JavaFX 中使用的两种相机,它们由PerspectiveCameraParallelCamera类表示。透视相机为透视投影定义了观察体积,透视投影是一个截顶的正棱锥体。相机将包含在*剪裁*面和远剪裁*面内的对象投影到投影*面上。因此,剪裁*面外的任何对象都不可见。*行摄像机指定*行投影的观察体积,*行投影是一个矩形框。

与真实世界类似,您需要一个光源来查看场景中的 3D 对象。抽象基类LightBase的一个实例代表一个光源。它的两个具体子类,AmbientLightPointLight,代表环境光和点光。

一个场景只能使用一个摄像机。有时,您可能想要使用多个相机查看场景的不同部分。JavaFX 包括子场景的概念。子场景是场景图的容器。它可以有自己的宽度、高度、填充颜色、深度缓冲、抗锯齿标志和相机。SubScene类的一个实例代表一个子场景。SubScene继承自Node类。

下一章将讨论如何对场景图中的节点应用不同类型的效果。

十七、应用效果

在本章中,您将学习:

  • 这是什么效果

  • 如何链接效果

  • 有哪些不同类型的效果

  • 如何使用透视变换效果

本章的例子在com.jdojo.effect包中。为了让它们工作,您必须在module-info.java文件中添加相应的一行:

...
opens com.jdojo.effect to javafx.graphics, javafx.base;
...

什么是效果?

效果是接受一个或多个图形输入、对输入应用算法并产生输出的过滤器。通常,将效果应用于节点以创建视觉上吸引人的用户界面。效果的例子有阴影、模糊、扭曲、发光、反射、混合和不同类型的照明等。JavaFX 库提供了几个与效果相关的类。效果是有条件的特征。它们应用于节点,如果它们在*台上不可用,将被忽略。图 17-1 显示了使用投影、模糊、发光和高光效果的四个Text节点。

img/336502_2_En_17_Fig1_HTML.png

图 17-1

Text具有不同效果的节点

Node类包含一个effect属性,指定应用于节点的效果。默认情况下,是null。下面的代码片段将投影效果应用于一个Text节点:

Text t1 = new Text("Drop Shadow");
t1.setFont(Font.font(24));
t1.setEffect(new DropShadow());

Effect类的一个实例代表一种效果。Effect类是所有效果类的抽象基础。所有效果类都包含在javafx.scene.effect包中。

清单 17-1 中的程序创建Text节点并对它们应用效果。这些节点如图 17-1 所示。我将在随后的章节中解释不同类型的效果及其用法。

// EffectTest.java
package com.jdojo.effect;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.effect.Bloom;
import javafx.scene.effect.BoxBlur;
import javafx.scene.effect.DropShadow;
import javafx.scene.effect.Glow;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
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.stage.Stage;

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

        @Override

        public void start(Stage stage) {
                Text t1 = new Text("Drop Shadow!");
                t1.setFont(Font.font(24));
                t1.setEffect(new DropShadow());

                Text t2 = new Text("Blur!");
                t2.setFont(Font.font(24));
                t2.setEffect(new BoxBlur());

                Text t3 = new Text("Glow!");
                t3.setFont(Font.font(24));
                t3.setEffect(new Glow());

                Text t4 = new Text("Bloom!");
                t4.setFont(Font.font("Arial", FontWeight.BOLD, 24));
                t4.setFill(Color.WHITE);
                t4.setEffect(new Bloom(0.10));

                // Stack the Text node with bloom effect over a
                     // Reactangle
                Rectangle rect = new Rectangle(100, 30, Color.GREEN);
                StackPane spane = new StackPane(rect, t4);

                HBox root = new HBox(t1, t2, t3, spane);
                root.setSpacing(20);
                root.setStyle("""
                         -fx-padding: 10;
                   -fx-border-style: solid inside;
                   -fx-border-width: 2;
                   -fx-border-insets: 5;
                   -fx-border-radius: 5;
                   -fx-border-color: blue;""");

                Scene scene = new Scene(root);
                stage.setScene(scene);
                stage.setTitle("Applying Effects");
                stage.show();
        }

}

Listing 17-1Applying Effects to Nodes

Tip

应用于Group的效果会应用于它的所有子对象。也可以链接多个效果,其中一个效果的输出成为链中下一个效果的输入。节点的布局边界不受应用于它的效果的影响。但是,局部边界和父边界会受到效果的影响。

连锁效应

当按顺序应用某些效果时,它们可以与其他效果链接在一起。第一个效果的输出成为第二个效果的输入,以此类推,如图 17-2 所示。

img/336502_2_En_17_Fig2_HTML.png

图 17-2

应用于节点的效果链

允许链接的效果类包含一个input属性来指定它前面的效果。如果inputnull,则该效果应用于设置该效果的节点,而不是应用于之前的输入效果。默认情况下,inputnull。以下代码片段在Text节点上创建了两个效果链,如图 17-3 所示:

img/336502_2_En_17_Fig3_HTML.png

图 17-3

用一个Reflection效果链接一个DropShadow效果

// Effect Chain: Text >> Reflection >> Shadow
DropShadow dsEffect = new DropShadow();
dsEffect.setInput(new Reflection());
Text t1 = new Text("Reflection and Shadow");
t1.setEffect(dsEffect);

// Effect Chain: Text >> Shadow >> Reflection
Reflection reflection = new Reflection();
reflection.setInput(new DropShadow());
Text t2 = new Text("Shadow and Reflection");
t2.setEffect(reflection);

在图 17-3 中,一个Reflection效果后跟一个DropShadow应用于左边的文本;一个跟随有Reflection效果的DropShadow被应用到右边的文本。请注意,效果的顺序对输出产生了影响。第二个效果链产生更高的输出,因为反射也包括阴影。

如果一个效果允许链接,它将有一个input属性。在随后的部分中,我将列出效果类的输入属性,但不讨论它。

阴影效应

阴影效果绘制阴影并将其应用于输入。JavaFX 支持三种类型的阴影效果:

  • DropShadow

  • InnerShadow

  • Shadow

阴影效果

DropShadow效果在输入后面画了一个阴影(模糊的图像),所以输入看起来是凸起的。它给输入一个 3D 的外观。输入可以是效果链中的一个节点或一个效果。

一个DropShadow类的实例代表一个DropShadow效果。效果的大小、位置、颜色和质量由DropShadow类的几个属性控制:

  • offsetX

  • offsetY

  • color

  • blurType

  • radius

  • spread

  • width

  • height

  • input

DropShadow类包含几个构造器,允许您指定属性的初始值:

  • DropShadow()

  • DropShadow(BlurType blurType, Color color, double radius, double spread, double offsetX, double offsetY)

  • DropShadow(double radius, Color color)

  • DropShadow(double radius, double offsetX, double offsetY, Color color)

offsetXoffsetY属性控制阴影相对于输入的像素位置。默认情况下,它们的值为零。offsetXoffsetY的正值分别在 x 轴和 y 轴的正方向移动阴影。负值会使阴影反向移动。

下面的代码片段创建了一个具有 10px 的offsetXoffsetYDropShadow对象。图 17-4 中左起第三个矩形显示了使用相同的矩形和不同的 x 和 y 偏移的效果。对于左数第四个矩形,阴影位于矩形的右下角,因为矩形大小(50,25)与偏移量(50,25)匹配。

img/336502_2_En_17_Fig4_HTML.png

图 17-4

offsetXoffsetY属性对DropShadow效果的影响

DropShadow dsEffect = new DropShadow();
dsEffect.setOffsetX(10);
dsEffect.setOffsetY(10);

Rectangle rect = new Rectangle(50, 25, Color.LIGHTGRAY);
rect.setEffect(dsEffect);

属性指定了阴影的颜色。默认是Color.BLACK。下面的代码将颜色设置为红色:

DropShadow dsEffect = new DropShadow();
dsEffect.setColor(Color.RED);

阴影中的模糊可以使用不同的算法来实现。属性指定阴影的模糊算法的类型。它的值是BlurType枚举的下列常量之一:

  • ONE_PASS_BOX

  • TWO_PASS_BOX

  • THREE_PASS_BOX

  • GAUSSIAN

ONE_PASS_BOX使用单遍框滤镜来模糊阴影。这两个_PASS_BOX使用两个盒子过滤器来模糊阴影。THREE_PASS_BOX使用三次盒式滤镜来模糊阴影。GAUSSIAN使用高斯模糊内核来模糊阴影。阴影的模糊质量在ONE_PASS_BOX中最少,在GAUSSIAN中最好。默认是THREE_PASS_BOX,质量上非常接*GAUSSIAN。以下代码片段设置了GAUSSIAN模糊类型:

DropShadow dsEffect = new DropShadow();
dsEffect.setBlurType(BlurType.GAUSSIAN);

radius属性指定阴影在源像素的每一侧扩散的距离。如果半径为零,阴影会有锐利的边缘。它的值可以在 0 到 127 之间。默认值为 10。阴影区域外的模糊是通过混合阴影颜色和背景颜色实现的。模糊颜色在距离边缘的半径距离上逐渐消失。

图 17-5 显示了一个矩形两次带有DropShadow效果。左边的一个使用 0.0 的radius,这导致了阴影的锐利边缘。右边的一个使用默认的半径 10.0,在边缘周围散布 10px 的阴影。以下代码片段生成了图形中第一个具有清晰阴影边缘的矩形:

img/336502_2_En_17_Fig5_HTML.png

图 17-5

DropShadow效果的半径属性的效果

DropShadow dsEffect = new DropShadow();
dsEffect.setOffsetX(10);
dsEffect.setOffsetY(10);
dsEffect.setRadius(0);

Rectangle rect = new Rectangle(50, 25, Color.LIGHTGRAY);
rect.setEffect(dsEffect);

属性指定了半径的部分,它和阴影有相同的颜色。半径剩余部分的颜色由模糊算法决定。其值介于 0.0 和 1.0 之间。默认值为 0.0。

假设你有一个radius为 10.0、spread值为 0.60 的DropShadow,阴影颜色为黑色。在这种情况下,源像素周围的模糊颜色将高达 6px。它将从第七个像素到第十个像素开始淡出。如果将“扩散”值指定为 1.0,则阴影不会模糊。图 17-6 显示了三个带DropShadow的矩形,半径为 10.0。三种DropShadow效果使用不同的扩散值。0.0 的扩散沿半径完全模糊。0.50 的扩散在半径的前半部分扩散阴影颜色,并模糊后半部分。1.0 的扩散沿半径完全扩散阴影颜色,没有模糊。以下代码片段产生了图 17-6 中的中间矩形:

img/336502_2_En_17_Fig6_HTML.png

图 17-6

DropShadow效果的扩展属性的效果

DropShadow dsEfefct = new DropShadow();
dsEfefct.setOffsetX(10);
dsEfefct.setOffsetY(10);
dsEfefct.setRadius(10);
dsEfefct.setSpread(.50);

Rectangle rect = new Rectangle(50, 25, Color.LIGHTGRAY);
rect.setEffect(dsEfefct);

widthheight属性分别指定从源像素到阴影颜色扩散处的水*和垂直距离。它们的值介于 0 和 255 之间。设置它们的值相当于设置radius属性,所以它们等于(2 *半径+ 1)。它们的默认值是 21.0。当您更改半径时,如果widthheight属性未绑定,则使用公式对其进行调整。但是,设置widthheight会改变半径值,因此widthheight的*均值等于(2 *半径+ 1)。图 17-7 显示了四个带DropShadow效果的矩形。它们的widthheight属性被设置,如每个矩形下所示。它们的radius属性被自动调整。左数第四个矩形是使用以下代码片段生成的:

img/336502_2_En_17_Fig7_HTML.png

图 17-7

设置DropShadow的宽度和高度的效果

DropShadow dsEffect = new DropShadow();
dsEffect.setOffsetX(10);
dsEffect.setOffsetY(10);
dsEffect.setWidth(20);
dsEffect.setHeight(20);

Rectangle rect = new Rectangle(50, 25, Color.LIGHTGRAY);
rect.setEffect(dsEffect);

清单 17-2 中的程序让你试验DropShadow效果的属性。显示如图 17-8 所示的窗口。更改属性以查看它们的实际效果。

img/336502_2_En_17_Fig8_HTML.png

图 17-8

允许您在运行时更改DropShadow效果属性的窗口

// DropShadowTest.java
// ...find in the book's download area.

Listing 17-2Experimenting with DropShadow Properties

内影效果

InnerShadow效果与DropShadow效果非常相似。它在输入的边缘内绘制输入的阴影(模糊图像),因此输入看起来具有深度或 3D 效果。输入可以是效果链中的一个节点或一个效果。

一个InnerShadow类的实例代表一个InnerShadow效果。效果的大小、位置、颜色和质量由InnerShadow类的几个属性控制:

  • offsetX

  • offsetY

  • color

  • blurType

  • radius

  • choke

  • width

  • height

  • input

InnerShadow类的属性数量等于DropShadow类的属性数量。DropShadow类中的spread属性被InnerShadow类中的choke属性替换,其工作方式类似于DropShadow类中的spread属性。有关这些属性的详细描述和示例,请参考上一节“阴影效果”。

DropShadow类包含几个构造器,允许您指定属性的初始值:

  • InnerShadow()

  • InnerShadow(BlurType blurType, Color color, double radius, double choke, double offsetX, double offsetY)

  • InnerShadow(double radius, Color color)

  • InnerShadow(double radius, double offsetX, double offsetY, Color color)

清单 17-3 中的程序创建了一个Text节点和两个Rectangle节点。一个InnerShadow应用于所有三个节点。图 17-9 显示了这些节点的结果。请注意,阴影没有扩散到节点的边缘之外。你需要设置offsetXoffsetY属性才能看到明显的效果。

img/336502_2_En_17_Fig9_HTML.png

图 17-9

使用InnerShadow效果的一个Text和两个Rectangle节点

// InnerShadowTest.java
// ...find in the book's download area.

Listing 17-3Using the InnerShadow Class

阴影效果

Shadow效果创建一个边缘模糊的阴影。与DropShadowInnerShadow不同,它修改原始输入本身,将其转换为阴影。通常,Shadow效果会与原始输入相结合,以创建更高级别的阴影效果:

  • 您可以将带有亮色的Shadow效果应用到节点上,并将其叠加到原始节点的副本上,以创建发光效果。

  • 您可以创建一个深色的Shadow效果,并将其放在原始节点的后面,以创建一个DropShadow效果。

一个Shadow类的实例代表一个Shadow效果。效果的大小、颜色和质量由Shadow类的几个属性控制:

  • color

  • blurType

  • radius

  • width

  • height

  • input

这些属性的工作方式与它们在DropShadow中的工作方式相同。有关这些属性的详细描述和示例,请参考“阴影*?? 效果”一节。*

Shadow类包含几个构造器,允许您指定属性的初始值:

  • Shadow()

  • Shadow(BlurType blurType, Color color, double radius)

  • Shadow(double radius, Color color)

清单 17-4 中的程序演示了如何使用Shadow效果。它创建了三个Text节点。阴影将应用于所有三个节点。显示第一个阴影的输出。第二个阴影的输出叠加在原始节点上,以实现光晕效果。第三个阴影的输出放在它的原始节点后面,以达到一个DropShadow效果。图 17-10 显示了这三个节点。

img/336502_2_En_17_Fig10_HTML.png

图 17-10

对一个Text节点应用阴影,并创建GlowDropShadow效果

// ShadowTest.java
// ...find in the book's download area.

Listing 17-4Using a Shadow Effect and Creating High-Level Effects

模糊效果

模糊效果产生输入的模糊版本。JavaFX 允许您应用不同类型的模糊效果,它们在用于创建这些效果的算法上有所不同。

框模糊效果

BoxBlur效果使用一个方框滤镜内核来产生模糊效果。BoxBlur类的一个实例代表一种BoxBlur效果。可以使用类的这些属性来配置效果的大小和质量:

  • width

  • height

  • iterations

  • input

widthheight属性分别指定效果的水*和垂直尺寸。想象一个由输入像素中心的宽度和高度定义的框。在模糊过程中,像素的颜色信息在框内扩散。这些属性的值介于 0.0 和 255.0 之间。默认值为 5.0。小于或等于 1.0 的值不会在相应方向上产生模糊效果。

iterations属性指定应用模糊效果的次数。值越高,模糊质量越好。它的值可以在 0 到 3 之间。默认值为 1。值为 3 会产生与高斯模糊相当的模糊质量,这将在下一节中讨论。零值根本不会产生模糊。

BoxBlur类包含两个构造器:

  • BoxBlur()

  • BoxBlur(double width, double height, int iterations)

无参数构造器用 5.0 像素的widthheight以及 1 像素的iterations创建一个BoxBlur对象。另一个构造器允许您为widthheightiterations属性指定初始值,如下面的代码部分所示:

// Create a BoxBlur with defaults: width=5.0, height=5.0, iterations=1
BoxBlur bb1 = new BoxBlur();

// Create a BoxBlur with width=10.0, height=10.0, iterations=3
BoxBlur bb2 = new BoxBlur(10, 10, 3);

下面的代码片段创建了四个Text节点,并应用了各种质量的BoxBlur效果。图 17-11 显示了这些Text节点的结果。注意,最后一个Text节点没有任何模糊效果,因为iterations属性被设置为零。

img/336502_2_En_17_Fig11_HTML.png

图 17-11

具有不同质量效果的文本节点

Text t1 = new Text("Box Blur");
t1.setFont(Font.font(24));
t1.setEffect(new BoxBlur(5, 10, 1));

Text t2 = new Text("Box Blur");
t2.setFont(Font.font(24));
t2.setEffect(new BoxBlur(10, 5, 2));

Text t3 = new Text("Box Blur");
t3.setFont(Font.font(24));
t3.setEffect(new BoxBlur(5, 5, 3));

Text t4 = new Text("Box Blur");
t4.setFont(Font.font(24));
t4.setEffect(new BoxBlur(5, 5, 0)); // Zero iterations = No blurring

高斯-布朗效应

GaussianBlur效果使用高斯卷积内核产生模糊效果。GaussianBlur类的一个实例代表一种GaussianBlur效果。可以使用类的两个属性来配置该效果:

  • radius

  • input

radius属性控制模糊在源像素中的分布。该值越大,模糊效果越明显。其值可以在 0.0 和 63.0 之间。默认值为 10.0。半径为 0 像素不会产生模糊效果。

GaussianBlur类包含两个构造器:

  • GaussianBlur()

  • GaussianBlur(double radius)

无参数构造器创建一个默认半径为 10.0px 的GaussianBlur对象。另一个构造器允许您指定半径的初始值,如以下代码所示:

// Create a GaussianBlur with a 10.0 pixels radius
GaussianBlur gb1 = new GaussianBlur();

// Create a GaussianBlur with a 20.0 pixels radius
GaussianBlur gb2 = new GaussianBlur(20);

下面的代码片段创建了四个Text节点,并应用了不同半径值的GaussianBlur效果。图 17-12 显示了这些Text节点的结果。注意,最后一个Text节点没有任何模糊效果,因为radius属性被设置为零。

img/336502_2_En_17_Fig12_HTML.png

图 17-12

具有不同大小的GaussianBlur效果的文本节点

Text t1 = new Text("Gaussian Blur");
t1.setFont(Font.font(24));
t1.setEffect(new GaussianBlur(5));

Text t2 = new Text("Gaussian Blur");
t2.setFont(Font.font(24));
t2.setEffect(new GaussianBlur(10));

Text t3 = new Text("Gaussian Blur");
t3.setFont(Font.font(24));
t3.setEffect(new GaussianBlur(15));

Text t4 = new Text("Gaussian Blur");
t4.setFont(Font.font(24));
t4.setEffect(new GaussianBlur(0)); // radius = 0 means no blur

运动模糊效果

MotionBlur效果通过运动产生模糊效果。输入看起来就像你看到它在移动。高斯卷积核与指定的角度一起使用来产生效果。MotionBlur类的一个实例代表一种MotionBlur效果。可以使用类的三个属性来配置该效果:

  • radius

  • angle

  • input

如前一节所述,radiusinput属性的工作方式与GaussianBlur类各自的属性相同。angle属性以度为单位指定运动的角度。默认情况下,角度为零。

MotionBlur类包含两个构造器:

  • MotionBlur()

  • MotionBlur(double angle, double radius)

无参数构造器创建一个默认半径为 10.0px、角度为 0.0 度的MotionBlur对象。另一个构造器允许您指定角度和半径的初始值,如以下代码所示:

// Create a MotionBlur with a 0.0 degrees angle and a 10.0 pixels radius
MotionBlur mb1 = new MotionBlur();

// Create a MotionBlur with a 30.0 degrees angle and a 20.0 pixels radius
MotionBlur mb1 = new MotionBlur(30.0, 20.0);

清单 17-5 中的程序展示了如何在Text节点上使用MotionBlur效果,结果如图 17-13 所示。这两个滑块允许您更改radiusangle属性。

img/336502_2_En_17_Fig13_HTML.png

图 17-13

具有不同大小的GaussianBlur效果的文本节点

// MotionBlurTest.java
// ...find in the book's download area.

Listing 17-5Using the MotionBlur Effect on a Text Node

绽放 效果

Bloom效果为亮度大于或等于指定限制的输入像素添加光晕。请注意,不是所有的像素在一个Bloom效果中都会发光。

一个Bloom类的实例代表一个Bloom效果。它包含两个属性:

  • threshold

  • input

threshold属性是一个介于 0.0 和 1.0 之间的数字。其默认值为 0.30。输入中亮度大于或等于threshold属性的所有像素都会发光。像素的亮度由其发光度决定。光度为 0.0 的像素一点都不亮。光度为 1.0 的像素是 100%明亮。默认情况下,亮度大于或等于 0.3 的所有像素都会发光。阈值为 0.0 会使所有像素发光。阈值为 1.0 时,几乎没有像素发光。

Bloom类包含两个构造器:

  • Bloom()

  • Bloom(double threshold)

无参数构造器创建一个默认阈值为 0.30 的Bloom对象。另一个构造器让您指定threshold值,如以下代码所示:

// Create a Bloom with threshold 0.30
Bloom b1 = new Bloom();

// Create a Bloom with threshold 0.10 - more pixels will glow.
Bloom b2 = new Bloom(0.10);

图 17-14 显示了具有不同阈值的Bloom效果的四个Text节点。使用一个StackPane将一个Text节点放置在一个矩形上。请注意,阈值越低,高光溢出效果越高。以下代码片段创建了图 17-14 中左起的第一个Text节点和Rectangle对:

img/336502_2_En_17_Fig14_HTML.png

图 17-14

具有Bloom效果的文本节点

Text t1 = new Text("Bloom");
t1.setFill(Color.YELLOW);
t1.setFont(Font.font(null, FontWeight.BOLD, 24));
t1.setEffect(new Bloom(0.10));
Rectangle r1 = new Rectangle(100, 50, Color.GREEN);
StackPane sp1 = new StackPane(r1, t1);

发光 效果

Glow效果使输入的亮像素更亮。Glow类的一个实例代表一种Glow效果。它包含两个属性:

  • level

  • input

属性指定了效果的强度。它是一个介于 0.0 和 1.0 之间的数字,默认值为 0.30。级别 0.0 不添加任何光晕,级别 1.0 添加最大光晕。

Glow类包含两个构造器:

  • Glow()

  • Glow(double level)

无参数构造器创建一个默认级别为 0.30 的Glow对象。另一个构造器允许您指定级别值,如下面的代码所示:

// Create a Glow with level 0.30
Glow g1 = new Glow();

// Create a Glow with level 0.90 - more glow.
Glow g2 = new Glow(0.90);

图 17-15 显示了具有不同等级值的Glow效果的四个Text节点。使用一个StackPane将一个Text节点放置在一个矩形上。请注意,级别值越高,发光效果就越高。以下代码片段创建了图 17-15 中左起的第一个Text节点和Rectangle对:

img/336502_2_En_17_Fig15_HTML.png

图 17-15

具有Glow效果的文本节点

Text t1 = new Text("Glow");
t1.setFill(Color.YELLOW);
t1.setFont(Font.font(null, FontWeight.BOLD, 24));
t1.setEffect(new Glow(0.10));
Rectangle r1 = new Rectangle(100, 50, Color.GREEN);
StackPane sp1 = new StackPane(r1, t1);

倒影 效果

Reflection效果在输入下方添加了输入的反射。Reflection类的一个实例代表一种反射效果。反射的位置、大小和不透明度由各种属性控制:

  • topOffset

  • fraction

  • topOpacity

  • bottomOpacity

  • input

topOffset指定输入底部和反射顶部之间的像素距离。默认情况下,它是 0.0。属性指定在反射中可见的输入高度的分数。它是从底部测量的。其值可以在 0.0 和 1.0 之间。值为 0.0 表示没有反射。值为 1.0 意味着整个输入在反射中可见。值为 0.25 意味着来自底部的 25%的输入在反射中可见。默认值为 0.75。topOpacitybottomOpacity属性指定了顶部和底部反射的不透明度。它们的值可以在 0.0 和 1.0 之间。topOpacity的默认值为 0.50,bottomOpacity的默认值为 0.0。

Reflection类包含两个构造器:

  • Reflection()

  • Reflection(double topOffset, double fraction, double topOpacity, double bottomOpacity)

无参数构造器创建一个Reflection对象,其属性使用默认的初始值。另一个构造器允许您指定属性的初始值,如下面的代码所示:

// Create a Reflection with default values
Reflection g1 = new Reflection();

// Create a Reflection with topOffset=2.0, fraction=0.90,
// topOpacity=1.0, and bottomOpacity=1.0
Reflection g2 = new Reflection(2.0, 0.90, 1.0, 1.0);

图 17-16 显示了四个Text节点,具有不同配置的Reflection效果。下面的代码片段创建了左起第二个Text节点,它将完整的输入显示为反射:

img/336502_2_En_17_Fig16_HTML.png

图 17-16

具有Reflection效果的文本节点

Text t2 = new Text("Chatar");
t2.setFont(Font.font(null, FontWeight.BOLD, 24));
t2.setEffect(new Reflection(0.0, 1.0, 1.0, 1.0));

乌贼墨 效果

棕褐色是一种红棕色。棕褐色调色是在黑白照片上进行的,目的是使照片具有更温暖的色调。一个SepiaTone类的实例代表一个SepiaTone效果。它包含两个属性:

  • level

  • input

属性指定了效果的强度。它是一个介于 0.0 和 1.0 之间的数字。其默认值为 1.0。0.0 的level不添加棕褐色色调,1.0 的level添加最大棕褐色色调。

SepiaTone类包含两个构造器:

  • SepiaTone ()

  • SepiaTone (double level)

无参数构造器创建一个默认为 1.0 的level对象。另一个构造器让您指定level值,如下面的代码所示:

// Create a SepiaTone with level 1.0
SepiaTone g1 = new SepiaTone ();

// Create a SepiaTone with level 0.50
SepiaTone g2 = new SepiaTone(0.50);

以下代码片段创建了两个Text节点,结果如图 17-17 所示。请注意,色阶值越高,棕褐色调色效果就越高:

img/336502_2_En_17_Fig17_HTML.png

图 17-17

具有SepiaTone效果的文本节点

Text t1 = new Text("SepiaTone");
t1.setFill(Color.WHITE);
t1.setFont(Font.font(null, FontWeight.BOLD, 24));
1.setEffect(new SepiaTone(0.50));
Rectangle r1 = new Rectangle(150, 50, Color.BLACK);
r1.setOpacity(0.50);
StackPane sp1 = new StackPane(r1, t1);

Text t2 = new Text("SepiaTone");
t2.setFill(Color.WHITE);
t2.setFont(Font.font(null, FontWeight.BOLD, 24));
t2.setEffect(new SepiaTone(1.0));
Rectangle r2 = new Rectangle(150, 50, Color.BLACK);
r2.setOpacity(0.50);
StackPane sp2 = new StackPane(r2, t2);

位移贴图 效果

DisplacementMap效果移动输入中的每个像素以产生输出。这个名字有两部分:“位移”和“地图。”第一部分意味着该效果移动了输入中的像素。第二部分意味着置换是基于为输出中的每个像素提供置换因子的映射。

DisplacementMap类的一个实例代表一个DisplacementMap。该类包含几个用于配置效果的属性:

  • mapData

  • scaleX

  • scaleY

  • offsetX

  • offsetY

  • wrap

  • input

mapData属性是FloatMap类的实例。一个FloatMap是一个数据结构,它为矩形区域中的每个点存储多达四个值,由它的widthheight属性表示。例如,您可以使用FloatMap为二维矩形中的每个像素存储颜色的四个分量(红色、绿色、蓝色和 alpha)。与FloatMap中的一对数字相关联的四个值中的每一个都位于编号为 0、1、2 和 3 的带中。每个区带中值的实际含义取决于上下文。以下代码提供了设置FloatMap宽度和高度的示例:

// Create a FloatMap (width = 100, height = 50)
FloatMap map = new FloatMap(100, 50);

现在您需要用每对数字的波段值填充FloatMap。您可以使用FloatMap类的以下方法之一来填充数据:

  • setSample(int x, int y, int band, float value)

  • setSamples(int x, int y, float s0)

  • setSamples(int x, int y, float s0, float s1)

  • setSamples(int x, int y, float s0, float s1, float s2)

  • setSamples(int x, int y, float s0, float s1, float s2, float s3)

setSample()方法为指定的(x,y)位置设置指定波段中的指定valuesetSamples()方法在由方法调用中值的位置决定的范围内设置指定的值。也就是说,第一个值设置为波段 0,第二个值设置为波段 1,依此类推:

// Set 0,50f for band 0 and band 1 for each point in the map
for (int i = 0; i < 100; i++) {
        for (int j = 0; j < 50; j++) {
                map.setSamples(i, j, 0.50f, 0.50f);
        }
}

DisplacementMap类要求您将mapData属性设置为一个FloatMap,其中包含输出中每个像素的波段 0 和波段 1 的值。

scaleXscaleYoffsetXoffsetY是双精度属性。它们被用在等式中(稍后描述)来计算像素的位移。scaleXscaleY属性的默认值为 1.0。offsetXoffsetY属性的默认值为 0.0。

以下等式用于计算输出中(x,y)坐标处的像素。等式中的缩写dstsrc分别代表目的地和源:

dst[x,y] = src[x + (offsetX + scaleX * mapData[x,y][0]) * srcWidth,
               y + (offsetY + scaleY * mapData[x,y][1]) * srcHeight]

如果前面的等式看起来非常复杂,不要被吓倒。事实上,一旦你阅读了下面的解释,这个等式是非常简单的。等式中的mapData[x,y][0]mapData[x,y][1]部分分别指(x,y)位置的FloatMap中波段 0 和波段 1 的值。

假设您想要获得输出中(x,y)坐标的像素,也就是说,您想要知道输入中的哪个像素将被移动到输出中的(x,y)。首先,确保你的出发点是正确的。重复一下,该等式从输出中的点(x,y)开始,并在输入中找到(x1,y1)处的像素,该像素将移动到输出中的(x,y)。

Tip

许多人会认为从输入中的一个像素开始,然后在输出中找到它的位置,从而得出错误的等式。这不是真的。这个等式反过来适用。它在输出中选取一个点(x,y ),然后找到输入中的哪个像素将移动到这个点。

以下是完整解释该等式的步骤:

  • 您希望找到输入中将要移动到输出中的点(x,y)的像素。

  • mapData获取(x,y)的值(波段 0 和波段 1)。

  • mapData值乘以刻度(x 坐标为scaleX,y 坐标为scaleY)。

  • 将相应的偏移值添加到上一步计算的值中。

  • 将前面的步长值乘以相应的输入尺寸。这为您提供了从输出(x,y)沿 x 和 y 坐标轴的偏移值,输入中的像素将从该处移动到输出中的(x,y)。

  • 将上一步中的值添加到输出中点的 x 和 y 坐标中。假设这些值是(x1,y1)。输入中(x1,y1)处的像素移动到输出中的点(x,y)。

如果您在理解像素移位逻辑方面仍有问题,您可以将前面的等式分成两部分:

x1 = x + (offsetX + scaleX * mapData[x,y][0]) * srcWidth
y1 = y + (offsetY + scaleY * mapData[x,y][1]) * srcHeight

您可以将这些等式理解为“输出中(x,y)处的像素是通过将输入中(x1,y1)处的像素移动到(x,y)获得的。”

如果将比例和偏移值保留为默认值

  • 在波段 0 中使用正值将输入像素向左移动。

  • 在波段 0 中使用负值将输入像素向右移动。

  • 在波段 1 中使用正值将输入像素上移。

  • 在波段 1 中使用负值将输入像素下移。

清单 17-6 中的程序创建了一个Text节点,并为该节点添加了一个DisplacementMap效果。在mapData中,它设置值,因此输入的上半部分的所有像素向右移动 1 个像素,输入的下半部分的所有像素向左移动 1 个像素。Text节点将如图 17-18 所示。

img/336502_2_En_17_Fig18_HTML.png

图 17-18

具有DisplacementMap效果的Text节点

// DisplacementmapTest.java

// ...find in the book's download area.

Listing 17-6Using the DisplacementMap Effect

DisplacementMap类包含一个wrap属性,默认设置为 false。输出中的像素是移动到新位置的输入中的像素。需要移动到新位置的像素在输入中的位置由以下等式计算得出。对于输出中的某些位置,输入中可能没有可用的像素。假设你有一个 100 像素宽 50 像素高的矩形,你应用了一个DisplacementMap效果来将所有像素向左移动 50 像素。输出中 x = 75 的点将获得输入中 x = 125 的像素。输入只有 100 像素宽。因此,对于输出中的所有点 x > 50,输入中将没有可用像素。如果wrap属性设置为 true,当输入中要移动的像素的位置在输入边界之外时,通过取它们与输入的相应维度(沿 x 轴的宽度和沿 y 轴的高度)的模数来计算位置。在示例中,x = 125 将减少到 125 % 100,即 25,输入中 x = 25 处的像素将移动到输出中 x = 75 处。如果wrap属性为假,输出中的像素保持透明。

图 17-19 显示了具有DisplacementMap效果的两个Text节点。两个节点中的像素都向左移动 100 像素。顶部的Text节点的wrap属性设置为 false,而底部的Text节点的wrap属性设置为 true。注意,底部节点的输出是通过包装输入来填充的。清单 17-7 中的程序用于应用包裹效果。

img/336502_2_En_17_Fig19_HTML.png

图 17-19

DisplacementMap中使用wrap属性的效果

// DisplacementMapWrap.java
// ...find in the book's download area.

Listing 17-7Using the wrap Property in the DisplacementMap Effect

颜色输入效果

ColorInput效果是一个简单的效果,用指定的颜料填充(泛光)一个矩形区域。通常,它被用作另一个效果的输入。

ColorInput类的一个实例代表了ColorInput效果。该类包含定义矩形区域的位置、大小和绘制的五个属性:

  • x

  • y

  • width

  • height

  • paint

创建一个ColorInput对象类似于创建一个用ColorInput的颜料填充的矩形。xy属性指定矩形区域左上角在本地坐标系中的位置。属性widthheight指定了矩形区域的大小。x、y、宽度和高度的默认值为 0.0。paint属性指定填充油漆。paint的默认值为Color.RED

您可以使用以下构造器来创建一个ColorInput类的对象:

  • ColorInput()

  • ColorInput(double x, double y, double width, double height, Paint paint)

以下代码片段创建了一个ColorInput效果,并将其应用于一个矩形。应用效果后的矩形如图 17-20 所示。请注意,当您将ColorInput效果应用到一个节点时,您看到的只是由ColorInput效果生成的矩形区域。如前所述,ColorInput效果不会直接应用于节点。相反,它被用作另一个效果的输入。

img/336502_2_En_17_Fig20_HTML.png

图 17-20

应用于矩形的ColorInput效果

ColorInput effect = new ColorInput();
effect.setWidth(100);
effect.setHeight(50);
effect.setPaint(Color.LIGHTGRAY);

// Size of the Rectangle does not matter to the rectangular area
// of the ColorInput
Rectangle r1 = new Rectangle(100, 50);
r1.setEffect(effect);

ColorAdjust 效果

ColorAdjust效果按指定的增量调整像素的色调、饱和度、亮度和对比度。通常,该效果用于一个ImageView节点来调整图像的颜色。

ColorAdjust类的一个实例代表了ColorAdjust效果。该类包含定义矩形区域的位置、大小和绘制的五个属性:

  • hue

  • saturation

  • brightness

  • contrast

  • input

huesaturationbrightnesscontrast属性指定所有像素的这些分量的调整增量。范围从–1.0 到 1.0。它们的默认值为 0.0。

清单 17-8 中的程序展示了如何在图像上使用ColorAdjust效果。它显示一个图像和四个滑块来改变ColorAdjust效果的属性。使用滑块调整它们的值以查看效果。如果程序没有找到图像,它会打印一条消息,并显示一个Text节点覆盖了一个StackPane中的矩形,该效果会应用到StackPane

// ColorAdjustTest.java

// ...find in the book's download area.

Listing 17-8Using the ColorAdjust Effect to Adjust the Color of Pixels in an Image

图像输入效果

ImageInput效果的工作原理类似于ColorInput效果。它将给定的图像作为输入传递给另一个效果。这种效果不会修改给定的图像。通常,它用作另一个效果的输入,而不是直接应用于节点的效果。

ImageInput类的一个实例代表了ImageInput效果。该类包含三个定义图像位置和来源的属性:

  • x

  • y

  • source

xy属性指定图像左上角在最终应用效果的内容节点的本地坐标系中的位置。它们的默认值为 0.0。source属性指定了要使用的Image对象。

您可以使用以下构造器来创建一个ColorInput类的对象:

  • ImageInput()

  • ImageInput(Image source)

  • ImageInput(Image source, double x, double y)

清单 17-9 中的程序展示了如何使用ImageInput效果。它将一个ImageInput作为输入传递给一个DropShadow效果,该效果应用于一个矩形,如图 17-21 所示。

img/336502_2_En_17_Fig21_HTML.png

图 17-21

DropShadow效果应用于矩形的ImageInput效果

// ImageInputTest.java
// ...find in the book's download area.

Listing 17-9Using an ImageInput Effect As an Input to a DropShadow Effect

融合 效果

混合将两个输入中相同位置的两个像素组合起来,在输出中生成一个复合像素。Blend效果采用两种输入效果,并混合输入的重叠像素以产生输出。两个输入的混合由混合模式控制。

Blend类的一个实例代表了Blend效果。该类包含指定的属性

  • topInput

  • bottomInput

  • mode

  • opacity

topInputbottomInput属性分别指定顶部和底部效果。他们默认是null。属性指定了混合模式,这是在BlendMode枚举中定义的常量之一。默认为BlendMode.SRC_OVER。JavaFX 提供了 17 种预定义的混合模式。表 17-1 列出了BlendMode枚举中的所有常量,并对每个常量进行了简要描述。所有混合模式都使用SRC_OVER规则来混合 alpha 组件。opacity属性指定在应用混合之前应用于顶部输入的不透明度。opacity默认为 1.0。

表 17-1

BlendMode枚举中的常量及其描述

|

BlendMode Enum 常量

|

描述

ADD 它将顶部和底部输入中的像素的颜色(红色、绿色和蓝色)和 alpha 值相加,以获得新的分量值。
MULTIPLY 它将两个输入的颜色分量相乘。
DIFFERENCE 它从任何一个输入中减去另一个输入中较亮颜色分量中的较暗颜色分量,以获得结果颜色分量。
RED 它用顶部输入的红色分量替换底部输入的红色分量,使所有其他颜色分量不受影响。
BLUE 它用顶部输入的蓝色分量替换底部输入的蓝色分量,使所有其他颜色分量不受影响。
GREEN 它用顶部输入的绿色分量替换底部输入的绿色分量,使所有其他颜色分量不受影响。
EXCLUSION 它将两个输入的颜色分量相乘,并将结果加倍。从底部输入的颜色分量的总和中减去由此获得的值,以获得结果颜色分量。
COLOR_BURN 它将底部输入颜色分量的倒数除以顶部输入颜色分量,并将结果反转。
COLOR_DODGE 它将底部输入颜色分量除以顶部输入颜色的倒数。
LIGHTEN 它使用两个输入中较亮的颜色分量。
DARKEN 它使用两个输入中较暗的颜色分量。
SCREEN 它反转来自两个输入的颜色分量,将它们相乘,然后反转结果。
OVERLAY 根据底部输入颜色,它会倍增或筛选输入颜色分量。
HARD_LIGHT 根据顶部输入颜色,它会倍增或筛选输入颜色分量。
SOFT_LIGHT 根据顶部输入颜色,它会使输入颜色分量变暗或变亮。
SRC_ATOP 它为非重叠区域保留底部输入,为重叠区域保留顶部输入。
SRC_OVER 顶部输入绘制在底部输入之上。因此,重叠区域显示顶部输入。

清单 17-10 中的程序创建了两个相同大小的ColorInput效果。它们的xy属性以重叠的方式设置。这两个效果被用作Blend效果的顶部和底部输入。提供了一个组合框和一个滑块来选择顶部输入的混合模式和不透明度。图 17-22 显示了运行该代码产生的窗口。运行程序,尝试选择不同的混合模式,看看Blend的效果。

img/336502_2_En_17_Fig22_HTML.png

图 17-22

Blend效应

// BlendTest.java
// ...find in the book's download area.

Listing 17-10Using the Blend Effect

灯光 效果

Lighting效果,顾名思义,模拟光源照射在场景中的指定节点上,给节点一个 3D 的外观。一个Lighting效果使用一个光源来产生效果,这个光源是Light类的一个实例。有不同类型的可配置灯可用。如果不指定光源,效果将使用默认光源。

一个Lighting类的实例代表一个Lighting效果。该类包含两个构造器:

  • Lighting()

  • Lighting(Light light)

无参数构造器使用默认光源。另一个构造器让你指定一个光源。

Lighting效果应用到一个节点可能是一个简单或复杂的任务,这取决于您想要实现的效果类型。让我们看一个简单的例子。下面的代码片段将一个Lighting效果应用到一个Text节点,给它一个 3D 的外观,如图 17-23 所示:

img/336502_2_En_17_Fig23_HTML.png

图 17-23

使用默认光源的具有Lighting效果的Text节点

// Create a Text Node
Text text = new Text("Chatar");
text.setFill(Color.RED);
text.setFont(Font.font(null, FontWeight.BOLD, 72));
HBox.setMargin(text, new Insets(10));

// Set a Lighting effect to the Text node
text.setEffect(new Lighting());

在前面的例子中,添加Lighting效果就像创建一个Lighting类的对象并将其设置为Text节点的效果一样简单。后面我会讨论一些复杂的Lighting效果。Lighting类包含几个属性来配置效果:

  • contentInput

  • surfaceScale

  • bumpInput

  • diffuseConstant

  • specularConstant

  • specularExponent

  • light

如果使用效果链,contentInput属性指定了Lighting效果的输入效果。在前面讨论的所有其他效果中,此属性被命名为输入。在本节中,我不会进一步讨论这个属性。有关如何使用该属性的更多详细信息,请参考“链接效果”一节。

自定义表面纹理

surfaceScalebumpInput属性用于为 2D 表面提供纹理,使其看起来像 3D 表面。基于像素的不透明度,像素看起来或高或低,以赋予表面纹理。透明像素看起来很低,不透明像素看起来很高。

surfaceScale属性允许您控制表面粗糙度。其值的范围从 0.0 到 10.0。默认值为 1.5。对于更高的surfaceScale,表面看起来更粗糙,给它一个更 3D 的外观。

您可以使用bumpInput属性将Effect作为输入传递给Lighting效果。使用bumpInput中像素的不透明度来获得光照表面像素的高度,然后应用surfaceScale来增加粗糙度。如果bumpInputnull,来自应用效果的节点的像素的不透明度用于生成表面的粗糙度。默认情况下,半径为 10 的Shadow效果被用作bumpInput。您可以使用ImageInput、模糊效果或任何其他效果作为bumpInputLighting效果。

清单 17-11 中的程序显示了一个带有Lighting效果的Text节点。将bumpInput设置为null。它提供了一个复选框来设置一个GaussianBlur效果作为bumpInput和一个滑块来调整surfaceScale值。图 17-24 显示了两个截图:一个没有凹凸输入,另一个有凹凸输入。请注意表面纹理的差异。

img/336502_2_En_17_Fig24_HTML.png

图 17-24

surfaceScalebumpInputLighting的影响对Text节点的影响

// SurfaceTexture.java
// ...find in the book's download area.

Listing 17-11Using the surfaceScale and bumpInput Properties

理解反射类型

当光落在不透明的表面上时,一部分光被吸收,一部分被透射,一部分被反射。3D 外观是通过显示部分表面较亮部分较暗来实现的。你会看到表面反射的光。3D 外观因光源和节点曲面反射光线的方式而异。微观级别的表面结构定义了反射的细节,例如强度和方向。在几种反射类型中,这里有两种类型值得一提:漫反射和镜面反射。

漫反射中,表面以多个角度反射入射光线。也就是说,漫反射通过向所有方向反射光线来散射光线。完美的漫反射将光线均匀地反射到各个方向。使用漫反射的表面从各个方向看起来都一样亮。这并不意味着整个漫反射表面都是可见的。漫反射曲面上某个区域的可见性取决于灯光的方向和曲面的方向。表面的亮度取决于表面类型本身和光线的强度。通常,粗糙的表面,例如衣服、纸张或灰泥墙,使用漫反射来反射光线。表面在肉眼看来可能是光滑的,例如纸或衣服,但在微观层面上它们是粗糙的,并且它们漫反射光。

在镜面反射中,表面只向一个方向反射光线。也就是说,一条入射光线只有一条反射光线。微观层面的光滑表面,例如镜子或磨光的大理石,会产生镜面反射。一些光滑的表面在微观水*上可能不是 100%光滑的,它们也可能漫反射部分光。与漫反射相比,镜面反射会产生更亮的表面。图 17-25 描绘了光在漫反射和镜面反射中的反射方式。

img/336502_2_En_17_Fig25_HTML.png

图 17-25

漫反射和镜面反射类型

Lighting类的三个属性用于控制反射的大小和强度:

  • diffuseConstant

  • specularConstant

  • specularExponent

这些属性是 double 类型的。diffuseConstant用于漫反射。specularConstantspecularExponent用于镜面反射。diffuseConstant属性指定漫反射强度的倍数。其值范围为 0.0 到 2.0,默认值为 1.0。值越高,表面越亮。specularConstant属性指定镜面反射应用到的光的比例。其值范围为 0.0 到 2.0,默认值为 0.30。更高的值意味着更大尺寸的镜面高光。specularExponent指定表面的光泽度。较高的值意味着反射更强烈,表面看起来更亮。specularExponent范围从 0.0 到 40.0,默认值为 20.0。

清单 17-12 包含一个实用程序类的代码,它将Lighting类的属性绑定到一些控件,这些控件将用于控制后面讨论的例子中的属性。

// LightingUtil.java
// ...find in the book's download area.

Listing 17-12A Utility Class That Creates a Set of Controls Bound to the Properties of a Lighting Instance

清单 17-13 中的程序使用工具类将Lighting效果的属性绑定到 UI 控件。显示如图 17-26 所示的窗口。使用滑块更改反射属性以查看其效果。

img/336502_2_En_17_Fig26_HTML.png

图 17-26

反射属性对照明节点的影响

// ReflectionTypeTest.java
// ...find in the book's download area.

Listing 17-13Controlling Reflection’s Details

了解光源

JavaFX 提供了三种内置光源:远光、点光和聚光灯。远光也称为定向光线性光。远光源在整个表面上均匀地发出特定方向的*行光线。太阳是地球上被照亮物体表面的一个完美的远距离光源的例子。光源离被照亮的物体很远,所以光线几乎是*行的。远光源均匀地照亮一个表面,而不考虑它离表面的距离。这并不意味着整个物体都被照亮。例如,当你站在阳光下,不是你身体的所有部分都被照亮。然而,你身体被照亮的部分有均匀的光。物体被照亮的部分取决于光源发出的光的方向。图 17-27 显示了一束远光照射到一个物体表面的某个部分。请注意,看到的是光线,而不是光源本身,因为对于远光来说,只有光的方向才是重要的,而不是光源与被照亮物体的距离。

img/336502_2_En_17_Fig27_HTML.png

图 17-27

打在物体表面的远光

光源从 3D 空间中一个极小的点向各个方向发出光线。理论上,光源是没有维度的。它均匀地向各个方向发光。因此,与远光不同,点光源相对于被照亮物体的方向并不重要。裸露的灯泡、星星(不包括太阳,它像一个遥远的光)和烛光都是点光源的例子。撞击表面的点光源的强度随着表面和点光源之间距离的*方而减小。如果点光源非常靠*曲面,它会创建一个热点,这是曲面上非常亮的点。为了避免热点,你需要将光源从表面移开一点。例如,使用点的 x、y 和 z 坐标,在 3D 空间中的特定点处定义点光源。图 17-28 显示了向各个方向辐射光线的点光源。物体表面上最靠*灯光的点将被照亮最多。

img/336502_2_En_17_Fig28_HTML.png

图 17-28

打在物体表面的点光源

点光源是一种特殊类型的点光源。像点光源一样,它从三维空间中一个极小的点放射出光线。与点光源不同,光线的辐射被限制在一个圆锥体定义的区域内——位于圆锥体顶点的光源向其底部发出光线,如图 17-29 所示。聚光灯的例子有汽车前灯、手电筒、聚光灯和带灯罩的台灯。聚光灯瞄准曲面上的一点,该点是曲面上圆锥体轴所在的点。圆锥轴是连接圆锥顶点和圆锥底面中心的线。在图 17-29 中,锥轴用虚线箭头表示。聚光灯的效果由圆锥体顶点的位置、圆锥体角度和圆锥体旋转来定义。圆锥体的旋转决定了曲面上与圆锥体轴相交的点。圆锥体的角度控制着照明区域的面积。聚光灯的强度沿圆锥轴最高。如果将聚光灯“拉远”,可以使用聚光灯模拟远光,这样到达表面的光线是*行的。

img/336502_2_En_17_Fig29_HTML.png

图 17-29

聚光灯打在物体表面

光源是抽象Light类的一个实例。灯光有颜色,这是通过使用Light类的color属性来指定的。例如,使用红色Light将使白色填充的Text节点看起来是红色的。

Light类有三个子类来代表特定类型的光源。子类是Light类的静态内部类:

  • Light.Distant

  • Light.Point

  • Light.Spot

代表光源的类的类图如图 17-30 所示。Light.Spot类继承自Light.Point类。类定义属性来配置特定类型的光源。

img/336502_2_En_17_Fig30_HTML.png

图 17-30

代表光源的类的类图

Tip

当你没有为灯光效果提供光源时,会使用远光,这是Light.Distant类的一个实例。

使用远处的光源

Light.Distant类的一个实例代表一个远处的光源。该类包含两个指定光源方向的属性:

  • azimuth

  • elevation

这两个属性都是 double 类型。它们的值以度为单位。这两个属性一起用于在 3D 空间中以特定方向定位光源。默认情况下,它们的值为 45 度。它们没有最大值和最小值。它们的值是使用模 360 计算的。例如,方位角值 400 实际上是 40 (400 模 360 = 40)。

azimuth属性指定 XY *面中的方向角。顺时针测量正值,逆时针测量负值。方位角的 0°值位于 3 点钟位置,6 点钟位置为 90°,9 点钟位置为 180°,12 点钟位置为 270°,3 点钟位置为 360°。–90°的方位角将位于 12 点钟方向。图 17-31 显示了不同方位角值下远光在 XY *面上的位置。

img/336502_2_En_17_Fig31_HTML.png

图 17-31

使用方位角值确定远光在 XY *面中的方向

elevation属性指定光源在 YZ *面上的方向角。elevation属性值为 0 和 180 使光源停留在 XY *面上。仰角为 90 时,光源位于场景前方,整个场景被照亮。大于 180°小于 360°的仰角会将光源放在场景后面,使场景看起来很暗(没有灯光)。

Light.Distant类包含两个构造器:

  • Light.Distant()

  • Light.Distant(double azimuth, double elevation, Color color)

无参数构造器使用 45.0 度作为azimuthelevationColor.WHITE的光色。另一个构造器允许您指定这些属性。

清单 17-14 中的程序显示了如何使用Light.Distant灯。它显示一个窗口,让您设置照射在矩形和Text节点上的远光的方向。图 17-32 显示了一个带有远光的文本和矩形的例子。

img/336502_2_En_17_Fig32_HTML.png

图 17-32

一束远光照亮了一个Text节点和一个矩形

// DistantLightTest.java
// ...find in the book's download area.

Listing 17-14Using a Distant Light Source

使用点光源

类的一个实例代表一个点光源。该类包含三个属性来指定光源在空间中的位置:xyzxyz属性是点光源在空间中所在点的 x、y 和 z 坐标。如果你将z属性设置为 0.0,光源将在场景的*面上显示为一个非常小的亮点,照亮一个非常小的区域。随着z值的增加,光源远离场景*面,照亮场景中更多的区域。负值的z会将光源移动到场景后面,使其没有光线,场景看起来会完全黑暗。

Light.Point类包含两个构造器:

  • Light.Point()

  • Light.Point(double x, double y, double z, Color color)

无参数构造器将点光源放置在(0,0,0)处,并为光源使用Color.WHITE颜色。另一个构造器让您指定光源的位置和颜色。

清单 17-15 中的程序显示了如何使用Light.Point灯。它会显示一个底部带有滑块的窗口,用于更改点光源的位置。随着点光源远离场景,场景中的某些区域会比其他区域更亮。图 17-33 显示了一个覆盖在矩形上的Text节点被点光源照亮的例子。

img/336502_2_En_17_Fig33_HTML.png

图 17-33

点光源照亮一个Text节点和一个矩形

// PointLightTest.java
// ...find in the book's download area.

Listing 17-15Using a Point Light Source

使用点光源

Light.Spot类的一个实例代表一个点光源。该类继承自Light.Point类。从Light.Point类继承的属性(xyz)指定了光源的位置,它与圆锥体的顶点重合。Light.Spot类包含四个属性来指定光源在空间中的位置:

  • pointsAtX

  • pointsAtY

  • pointsAtZ

  • specularExponent

pointsAtXpointsAtYpointsAtZ属性指定空间中的一个点来设置光线的方向。从(xyz)开始,向(pointsAtXpointsAtYpointsAtZ)方向走的一条线就是锥轴,也是光线的方向。默认情况下,它们被设置为 0.0。specularExponent属性定义了光线的焦点(圆锥体的宽度),范围从 0.0 到 4.0。默认值为 1.0。specularExponent的值越高,圆锥体越窄,场景上聚焦的光线越多。

Light.Spot类包含两个构造器:

  • Light.Spot()

  • Light.Spot(double x, double y, double z, double specularExponent, Color color)

无参数构造器将光线放置在(0,0,0)处,并为光线使用Color.WHITE颜色。因为pointsAtXpointsAtYpointsAtZ的默认值是 0.0,所以光线没有方向。另一个构造器让您指定光源的位置和颜色。圆锥轴将从指定的(x,y,x)到(0,0,0)。

清单 17-16 中的程序显示了如何使用Light.Spot灯。它会显示一个窗口,允许您使用底部的滑块配置灯光的位置、方向和焦点。图 17-34 显示了一个Light.Spot光几乎聚焦在矩形中间的例子。

img/336502_2_En_17_Fig34_HTML.png

图 17-34

聚光灯照亮一个Text节点和一个矩形

// SpotLightTest.java
// ...find in the book's download area.

Listing 17-16Using a Spot Light Source

透视变换效果

通过将角映射到不同的位置,一个PerspectiveTransform效果给了 2D 节点一个 3D 的外观。原始节点中的直线保持笔直。然而,原始节点中的*行线不一定保持*行。

一个PerspectiveTransform类的实例代表一个PerspectiveTransform效果。该类包含八个属性,用于指定四个角的 x 和 y 坐标:

  • ulx

  • uly

  • urx

  • ury

  • lrx

  • lry

  • llx

  • lly

属性名称中的第一个字母(u 或 l)表示 upper 和 lower。属性名中的第二个字母(l 或 r)表示左和右。属性名(x 或 y)中的最后一个字母表示角点的 x 或 y 坐标。例如,urx表示右上角的 x 坐标。

Tip

PerspectiveTransform类还包含一个 input 属性,用于指定效果链中的输入效果。

PerspectiveTransform类包含两个构造器:

  • PerspectiveTransform()

  • PerspectiveTransform(double ulx, double uly, double urx, double ury, double lrx, double lry, double llx, double lly)

无参数构造器创建一个PerspectiveTransform对象,所有新的角都在(0,0)处。如果将对象设置为节点的效果,节点将缩小为一个点,您将看不到该节点。另一个构造器允许您为节点的四个角指定新的坐标。

清单 17-17 中的程序创建了两组Text节点和一个矩形。它将两个集合添加到两个不同的组。它对第二组应用了一个PerspectiveTransform效果。两组如图 17-35 所示。左边的组显示原始节点;右边的组应用了效果。

img/336502_2_En_17_Fig35_HTML.png

图 17-35

具有PerspectiveTransform效果的TextRectangle节点

// PerspectiveTransformTest.java
// ...find in the book's download area.

Listing 17-17Using the PerspectiveTransform Effect

摘要

效果是接受一个或多个图形输入、对输入应用算法并产生输出的过滤器。通常,将效果应用于节点以创建视觉上吸引人的用户界面。效果的例子有阴影、模糊、扭曲、发光、反射、混合和不同类型的照明。JavaFX 库提供了几个与效果相关的类。效果是有条件的特征。如果应用于节点的效果在*台上不可用,将被忽略。Node类包含一个effect属性,指定应用于节点的效果。默认情况下,是nullEffect类的一个实例代表一种效果。Effect类是所有效果类的抽象基础。所有效果等级都包含在javafx.scene.effect包中。

一些效果可以与其他效果链接在一起。效果是按顺序应用的。第一个效果的输出成为第二个效果的输入,依此类推。允许链接的效果类包含一个input属性来指定它前面的效果。如果input属性为null,效果将应用于设置了该效果的节点。默认情况下,input属性为null

阴影效果绘制阴影并将其应用于输入。JavaFX 支持三种类型的阴影效果:DropShadowInnerShadowShadow

模糊效果产生输入的模糊版本。JavaFX 允许您应用不同类型的模糊效果,这些效果使用不同的算法来创建效果。三种类型的模糊效果是BoxBlurGaussianBlurMotionBlur

Bloom效果为亮度大于或等于指定限制的输入像素添加光晕。请注意,不是所有的像素在一个Bloom效果中都会发光。一个Bloom类的实例代表一个Bloom效果。

Glow效果使输入的亮像素更亮。Glow类的一个实例代表一种Glow效果。

Reflection效果在输入下方添加了输入的反射。Reflection类的一个实例代表一种reflection效果。

棕褐色是一种红棕色。棕褐色调色是在黑白照片上进行的,目的是使照片具有更温暖的色调。一个SepiaTone类的实例代表一个SepiaTone效果。

DisplacementMap效果移动输入中的每个像素以产生输出。名字有两部分:位移和地图。第一部分意味着该效果移动了输入中的像素。第二部分意味着置换是基于为输出中的每个像素提供置换因子的映射。DisplacementMap类的一个实例代表一个DisplacementMap

ColorInput效果是一个简单的效果,用指定的颜料填充(泛光)一个矩形区域。通常,它被用作另一个效果的输入。ColorInput类的一个实例代表了ColorInput效果。

ImageInput效果的工作原理类似于ColorInput效果。它将给定的图像作为输入传递给另一个效果。这种效果不会修改给定的图像。通常,它用作另一个效果的输入,而不是直接应用于节点的效果。ImageInput类的一个实例代表了ImageInput效果。

混合将两个输入中相同位置的两个像素组合起来,在输出中生成一个复合像素。Blend效果采用两种输入效果,并混合输入的重叠像素以产生输出。两个输入的混合由混合模式控制。JavaFX 提供了 17 种预定义的混合模式。Blend类的一个实例代表了Blend效果。

Lighting效果,顾名思义,模拟光源照射在场景中的指定节点上,给节点一个 3D 的外观。一个Lighting效果使用一个光源来产生效果,这个光源是Light类的一个实例。

通过将角映射到不同的位置,一个PerspectiveTransform效果给了 2D 节点一个 3D 的外观。原始节点中的直线保持笔直。然而,原始节点中的*行线不一定保持*行。PerspectiveTransform类的一个实例代表一种PerspectiveTransform效果。

下一章将讨论如何对节点应用不同类型的转换。

十八、理解变换

在本章中,您将学习:

  • 什么是转变

  • 什么是*移、旋转、缩放和剪切变换,以及如何将它们应用于节点

  • 如何对一个节点应用多个变换

本章的例子在com.jdojo.transform包中。为了让它们正常工作,您必须在module-info.java文件中添加相应的一行:

...
opens com.jdojo.transform to javafx.graphics, javafx.base;
...

什么是转型?

变换是坐标空间中的点到同一坐标空间中的点的映射,保留一组几何属性。几种类型的变换可以应用于坐标空间中的点。JavaFX 支持以下类型的变换:

  • 翻译

  • 循环

  • 大剪刀

  • 规模

  • 姻亲

抽象Transform类的一个实例表示 JavaFX 中的一个变换。Transform类包含节点上所有类型变换使用的公共方法和属性。它包含创建特定类型变换的工厂方法。图 18-1 显示了代表不同类型变换的类的类图。该类的名称与该类提供的变换类型相匹配。所有的课程都在javafx.scene.transform包里。

img/336502_2_En_18_Fig1_HTML.png

图 18-1

与变换相关的类的类图

仿射变换是一种广义变换,它保留了点的数量和唯一性、直线的直线性以及点在*面中表现出的特性。*行线(和*面)在变换后保持*行。它可能不会保留线之间的角度或点之间的距离。但是,直线上各点之间的距离比保持不变。*移、缩放、相似变换、相似变换、反射、旋转、剪切等等都是仿射变换的例子。

Affine类的一个实例代表一个仿射变换。这个类对于初学者来说不容易使用。它的使用需要先进的数学知识,如矩阵。如果您需要特定类型的变换,请使用特定的子类,如TranslateShear等等,而不要使用通用的Affine类。您也可以组合多个单独的变换来创建一个更复杂的变换。我们不会在本书中讨论这个类。

使用变换很容易。然而,有时它可能看起来令人困惑,因为有多种方法来创建和应用它们。

创建Transform实例有两种方法:

  • 使用Transform类的工厂方法之一——例如,创建Translate对象的translate()方法,创建Rotate对象的rotate()方法,等等。

  • 使用特定的类来创建特定类型的变换,例如,Translate类用于*移,Rotate类用于旋转,等等。

以下两个Translate对象代表相同的翻译:

double tx = 20.0;
double ty = 10.0;

// Using the factory method in the Transform class
Translate translate1 = Transform.translate(tx, ty);

// Using the Translate class constructor
Translate translate2 = new Translate(tx, ty);

有两种方法可以将变换应用到节点:

  • 使用Node类中的特定属性。例如,使用Node类的translateXtranslateYtranslateZ属性将翻译应用于节点。请注意,不能以这种方式应用剪切变换。

  • 使用节点的transforms序列。Node类的getTransforms()方法返回一个ObservableList<Transform>。用所有的Transform对象填充这个列表。Transforms将按顺序应用。只能使用这种方法应用剪切变换。

应用Transforms的两种方法工作起来有些不同。当我们讨论变换的具体类型时,我们将讨论这些差异。有时,可以使用上述两种方法来应用变换,在这种情况下,transforms序列中的变换在节点属性的变换集之前应用。

以下代码片段对矩形应用了三种变换:剪切、缩放和*移:

Rectangle rect = new Rectangle(100, 50, Color.LIGHTGRAY);
// Apply transforms using the transforms sequence of the Rectangle
Transform shear = Transform.shear(2.0, 1.2);
Transform scale = Transform.scale(1.1, 1.2);
rect.getTransforms().addAll(shear, scale);
// Apply a translation using the translatex and translateY
// properties of the Node class
rect.setTranslateX(10);
rect.setTranslateY(10);

使用transforms序列应用剪切和缩放。使用Node类的translateXtranslateY属性来应用翻译。在transforms序列中的变换,剪切和缩放,在*移之后依次应用。

翻译变换

*移会将节点的每个点相对于其父坐标系沿指定方向移动固定距离。这是通过将节点的局部坐标系的原点移动到新的位置来实现的。计算点的新位置很容易——只需在 3D 空间中每个点的坐标上添加一组数字。在 2D 空间中,给每个点的坐标加上一对数字。

假设您想通过(tx,ty,tz)将*移应用于 3D 坐标空间。如果一个点在*移之前具有坐标(x,y,z ),那么*移之后它的坐标将是(x + tx,y + ty,z + tz)。

图 18-2 显示了一个*移变换的例子。变换前的轴用实线表示。变换后的轴用虚线表示。注意,点 P 的坐标在变换后的坐标空间中保持不变(4,3)。但是,该点相对于原始坐标空间的坐标在变换后会发生变化。原始坐标空间中的点以纯黑色填充颜色显示,而在变换后的坐标空间中,该点没有填充颜色。坐标系的原点(0,0)已移动到(3,2)。点 P(移动的点)在原始坐标空间中的坐标变为(7,5),其计算为(4+3,3+2)。

img/336502_2_En_18_Fig2_HTML.png

图 18-2

翻译变换的示例

Translate类的一个实例代表一个翻译。它包含三个属性:

  • x

  • y

  • z

这些属性指定变换后节点的本地坐标系的新原点的 x、y 和 z 坐标。这些属性的默认值为 0.0。

Translate类提供了三个构造器:

  • Translate()

  • Translate(double x, double y)

  • Translate(double x, double y, double z)

无参数构造器用默认的xyz属性值创建一个Translate对象,这实际上表示没有翻译。另外两个构造器允许您指定沿三个轴的*移距离。对Group的变换被应用于Group中的所有节点。

比较Node类的layoutXlayoutY属性与translateXtranslateY属性的使用。layoutXlayoutY属性在其局部坐标系中定位节点,而不变换局部坐标系,而translateXtranslateY属性通过移动原点来变换节点的局部坐标系。通常,layoutXlayoutY用于在场景中放置节点,而*移用于在动画中移动节点。如果您为一个节点设置了这两个属性,它的局部坐标系将使用*移进行变换,然后,该节点将使用其layoutXlayoutY属性放置在新的坐标系中。

清单 18-1 中的程序创建了三个矩形。默认情况下,它们被放置在(0,0)处。它对第二个和第三个矩形应用*移。图 18-3 显示了*移后的矩形。

img/336502_2_En_18_Fig3_HTML.png

图 18-3

带*移的矩形

// TranslateTest.java
package com.jdojo.transform;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.transform.Translate;
import javafx.stage.Stage;

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

        @Override
        public void start(Stage stage) {
                Rectangle rect1 =
                         new Rectangle(100, 50, Color.LIGHTGRAY);
                rect1.setStroke(Color.BLACK);

                Rectangle rect2 = new Rectangle(100, 50, Color.YELLOW);
                rect2.setStroke(Color.BLACK);

                Rectangle rect3 =
                         new Rectangle(100, 50, Color.STEELBLUE);
                rect3.setStroke(Color.BLACK);

                // Apply a translation on rect2 using the transforms
                     // sequence
                Translate translate1 = new Translate(50, 10);
                rect2.getTransforms().addAll(translate1);

                // Apply a translation on rect3 using the translateX
                // and translateY properties
                rect3.setTranslateX(180);
                rect3.setTranslateY(20);

                Pane root = new Pane(rect1, rect2, rect3);
                root.setPrefSize(300, 80);
                Scene scene = new Scene(root);
                stage.setScene(scene);
                stage.setTitle(
                         "Applying the Translation Transformation");
                stage.show();
        }
}

Listing 18-1Applying Translations to Nodes

旋转变换

在旋转变换中,轴围绕坐标空间中的轴心点旋转,并且点的坐标被映射到新的轴。图 18-4 显示了旋转 30 度角的 2D *面中的坐标系轴。旋转轴是 z 轴。原始坐标系的原点用作旋转的轴心点。原始轴用实线表示,旋转后的轴用虚线表示。原始坐标系中的点 P 以黑色填充显示,而在旋转后的坐标系中没有填充。

img/336502_2_En_18_Fig4_HTML.png

图 18-4

旋转变换的一个例子

Rotate类的一个实例代表一个旋转变换。它包含五个描述旋转的属性:

  • angle

  • axis

  • pivotX

  • pivotY

  • pivotZ

angle属性以度为单位指定旋转的角度。默认值为 0.0 度。顺时针测量angle的正值。

axis属性指定枢轴点的旋转轴。它的值可以是在Rotate类中定义的常量X_AXISY_AXISZ_AXIS之一。默认旋转轴为Rotate.Z_AXIS

pivotXpivotYpivotZ属性是轴心点的 x、y 和 z 坐标。这些属性的默认值为 0.0。

Rotate类包含几个构造器:

  • Rotate()

  • Rotate(double angle)

  • Rotate(double angle, double pivotX, double pivotY)

  • Rotate(double angle, double pivotX, double pivotY, double pivotZ)

  • Rotate(double angle, double pivotX, double pivotY, double pivotZ, Point3D axis)

  • Rotate(double angle, Point3D axis)

无参数构造器创建一个身份旋转,它对变换后的节点没有任何影响。其他构造器允许您指定细节。

清单 18-2 中的程序创建了两个矩形并将它们放在相同的位置。第二个矩形的不透明度设置为 0.5,这样我们就可以看透它了。第二个矩形的坐标系以原点为支点顺时针旋转 30 度。图 18-5 为旋转后的矩形。

img/336502_2_En_18_Fig5_HTML.png

图 18-5

使用旋转变换的矩形

// RotateTest.java
package com.jdojo.transform;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.transform.Rotate;
import javafx.stage.Stage;

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

        @Override
        public void start(Stage stage) {
                Rectangle rect1 =
                         new Rectangle(100, 50, Color.LIGHTGRAY);
                rect1.setStroke(Color.BLACK);

                Rectangle rect2 =
                         new Rectangle(100, 50, Color.LIGHTGRAY);
                rect2.setStroke(Color.BLACK);
                rect2.setOpacity(0.5);

                // Apply a rotation on rect2\. The rotation angle is
                     // 30 degree clockwise
                // (0, 0) is the pivot point
                Rotate rotate = new Rotate(30, 0, 0);
                rect2.getTransforms().addAll(rotate);

                Pane root = new Pane(rect1, rect2);
                root.setPrefSize(300, 80);
                Scene scene = new Scene(root);
                stage.setScene(scene);
                stage.setTitle("Applying the Rotation Transformation");
                stage.show();
        }
}

Listing 18-2Using a Rotation Transformation

当轴心点是节点的局部坐标系的原点,并且节点的左上角也位于原点时,很容易看到旋转的效果。让我们考虑下面的代码片段,它旋转一个矩形,如图 18-6 所示:

img/336502_2_En_18_Fig6_HTML.png

图 18-6

使用枢轴点而不是局部坐标系的原点旋转矩形

Rectangle rect1 = new Rectangle(100, 50, Color.LIGHTGRAY);
rect1.setY(20);
rect1.setStroke(Color.BLACK);
Rectangle rect2 = new Rectangle(100, 50, Color.LIGHTGRAY);
rect2.setY(20);
rect2.setStroke(Color.BLACK);
rect2.setOpacity(0.5);
// Apply a rotation on rect2\. The rotation angle is 30 degree anticlockwise
// (100, 0) is the pivot point.
Rotate rotate = new Rotate(-30, 100, 0);
rect2.getTransforms().addAll(rotate);

矩形左上角的坐标被设置为(0,20)。(100,0)处的点用作旋转第二个矩形的枢轴点。轴心点位于矩形的 x 轴上。第二个矩形的坐标系固定在(100,0),然后逆时针旋转 30 度。请注意,第二个矩形在旋转后的坐标空间中保持其位置(0,20)。

您还可以使用Node类的rotaterotationAxis属性对节点应用旋转。rotate属性以度为单位指定旋转角度。rotationAxis属性指定旋转轴。节点的未变换布局边界的中心被用作轴心点。

Tip

transforms序列中使用的默认枢轴点是节点的本地坐标系的原点,而Node类的rotate属性使用节点的未变换布局边界的中心作为枢轴点。

清单 18-3 中的程序创建了两个类似于清单 18-2 中的矩形。它使用Node类的rotate属性将矩形旋转 30 度。图 18-7 为旋转后的矩形。比较图 18-5 和 18-7 中旋转后的矩形。前者以局部坐标系的原点为支点,后者以矩形的中心为支点。

img/336502_2_En_18_Fig7_HTML.png

图 18-7

使用 Node 类的 rotate 属性旋转的矩形

// RotatePropertyTest.java
package com.jdojo.transform;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;

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

        @Override
        public void start(Stage stage) {
               Rectangle rect1 = new Rectangle(100, 50, Color.LIGHTGRAY);
               rect1.setStroke(Color.BLACK);

               Rectangle rect2 = new Rectangle(100, 50, Color.LIGHTGRAY);
               rect2.setStroke(Color.BLACK);
               rect2.setOpacity(0.5);

               // Use the rotate property of the node class
               rect2.setRotate(30);

               Pane root = new Pane(rect1, rect2);
               root.setPrefSize(300, 80);
               Scene scene = new Scene(root);
               stage.setScene(scene);
               stage.setTitle("Applying the Rotation Transformation");
               stage.show();
        }
}

Listing 18-3Using the rotate Property of the Node Class to Rotate a Rectangle

规模变换

缩放变换通过缩放因子沿坐标系的轴缩放测量单位。这将导致节点的尺寸沿轴按指定的比例因子变化(拉伸或收缩)。沿轴的尺寸乘以沿该轴的比例因子。变换应用于变换后坐标保持不变的轴心点。

Scale类的一个实例代表一个比例变换。它包含以下六个描述变换的属性:

  • x

  • y

  • z

  • pivotX

  • pivotY

  • pivotZ

xyz属性指定沿 x 轴、y 轴和 z 轴的比例因子。默认情况下,它们是 1.0。

pivotXpivotYpivotZ属性是轴心点的 x、y 和 z 坐标。这些属性的默认值为 0.0。

Scale类包含几个构造器:

  • Scale()

  • Scale(double x, double y)

  • Scale(double x, double y, double z)

  • Scale(double x, double y, double pivotX, double pivotY)

  • Scale(double x, double y, double z, double pivotX, double pivotY, double pivotZ)

无参数构造器创建一个标识比例变换,它对变换后的节点没有任何影响。其他构造器允许您指定比例因子和轴心点。

您可以使用Scale类的对象或Node类的scaleXscaleYscaleZ属性来应用缩放变换。默认情况下,Scale类使用的枢轴点位于(0,0,0)。Node类的属性使用节点的中心作为轴心点。

清单 18-4 中的程序创建了两个矩形。两者被放置在相同的位置。其中一个是缩放的,另一个不是。未缩放矩形的不透明度设置为 0.5,所以我们可以透过它看。图 18-8 显示了矩形。缩放后的矩形更小。第二个矩形的坐标系沿 x 轴缩放 0.5,沿 y 轴缩放 0.50。scaleXscaleY属性用于应用变换,该变换使用矩形的中心作为枢轴点,使矩形收缩,但保持在同一位置。

img/336502_2_En_18_Fig8_HTML.png

图 18-8

使用缩放变换的两个矩形

// ScaleTest.java
package com.jdojo.transform;

import javafx.application.Application;

import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;

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

        @Override
        public void start(Stage stage) {
                Rectangle rect1 =
                         new Rectangle(100, 50, Color.LIGHTGRAY);
                rect1.setStroke(Color.BLACK);
                rect1.setOpacity(0.5);

                Rectangle rect2 =
                         new Rectangle(100, 50, Color.LIGHTGRAY);
                rect2.setStroke(Color.BLACK);

                // Apply a scale on rect2\. Center of the Rectangle is
                     // the pivot point.
                rect2.setScaleX(0.5);
                rect2.setScaleY(0.5);

                Pane root = new Pane(rect1, rect2);
                root.setPrefSize(150, 60);
                Scene scene = new Scene(root);
                stage.setScene(scene);
                stage.setTitle("Applying the Scale Transformation");
                stage.show();
        }
}

Listing 18-4Using Scale Transformations

如果轴心点不是节点的中心,缩放变换可能会移动节点。清单 18-5 中的程序创建了两个矩形。两者被放置在相同的位置。其中一个是缩放的,另一个不是。未缩放矩形的不透明度设置为 0.5,所以我们可以透过它看。图 18-9 显示了矩形。缩放后的矩形更小。带有transforms序列的Scale对象用于应用变换,它使用矩形的左上角作为枢轴点,使矩形收缩,但将其向左移动,以保持其左上角的坐标在变换后的坐标系中相同(150,0)。缩放后的矩形在两个方向上缩小一半(缩放因子= 0.50),并向左移动一半的距离。

img/336502_2_En_18_Fig9_HTML.jpg

图 18-9

使用缩放变换的两个矩形

// ScalePivotPointTest.java
package com.jdojo.transform;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;

import javafx.scene.transform.Scale;
import javafx.stage.Stage;

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

        @Override
        public void start(Stage stage) {
                Rectangle rect1 =
                         new Rectangle(100, 50, Color.LIGHTGRAY);
                rect1.setX(150);
                rect1.setStroke(Color.BLACK);
                rect1.setOpacity(0.5);

                Rectangle rect2 =
                         new Rectangle(100, 50, Color.LIGHTGRAY);
                rect2.setX(150);
                rect2.setStroke(Color.BLACK);

                // Apply a scale on rect2\. The origin of the local
                     // coordinate system of rect4 is the pivot point
                Scale scale = new Scale(0.5, 0.5);
                rect2.getTransforms().addAll(scale);

                Pane root = new Pane(rect1, rect2);
                root.setPrefSize(300, 60);
                Scene scene = new Scene(root);
                stage.setScene(scene);
                stage.setTitle("Applying the Scale Transformation");
                stage.show();
        }
}

Listing 18-5Using Scale Transformations

剪切变换

剪切变换会围绕枢轴点旋转节点的局部坐标系的轴,因此这些轴不再垂直。变换后,矩形节点变成了*行四边形。

Shear类的一个实例表示一个剪切变换。它包含四个描述变换的属性:

  • x

  • y

  • pivotX

  • pivotY

x属性指定了一个乘数,通过该乘数,点的坐标沿着正 x 轴移动该点的 y 坐标的一个因子。默认值为 0.0。

y属性指定一个乘数,通过该乘数,点的坐标沿着正 y 轴移动该点的 x 坐标的一个因子。默认值为 0.0。

pivotXpivotY属性是发生剪切的枢轴点的 x 和 y 坐标。它们的默认值为 0.0。支点不会因剪切而移动。默认情况下,轴心点是未变换坐标系的原点。

假设在一个节点内有一个点(x1,y1),通过剪切变换,该点移动到(x2,y2)。您可以使用以下公式来计算(x2,y2):

x2 = pivotX + (x1 - pivotX) + x * (y1 - pivotY)
y2 = pivotY + (y1 - pivotY) + y * (x1 - pivotX)

前面公式中的所有坐标(x1、y1、x2 和 y2)都在节点的未变换局部坐标系中。注意,如果(x1,y1)是枢轴点,则上述公式计算移动的点(x2,y2),这与(x1,y1)相同。也就是说,轴心点没有移动。

Shear类包含几个构造器:

  • Shear()

  • Shear(double x, double y)

  • Shear(double x, double y, double pivotX, double pivotY)

无参数构造器创建一个恒等式剪切变换,它对变换后的节点没有任何影响。其他构造器允许您指定剪切乘数和轴心点。

Tip

您可以仅使用transforms序列中的Shear对象对节点应用剪切变换。与其他类型的变换不同,Node类不包含允许你应用剪切变换的属性。

清单 18-6 中的程序将一个Shear应用于一个矩形,如图 18-10 所示。还显示了原始矩形。沿两个轴使用乘数 0.5。请注意,轴心点是(0,0),这是默认值。

img/336502_2_En_18_Fig10_HTML.png

图 18-10

使用(0,0)作为轴心点进行剪切变换的矩形

// ShearTest.java
package com.jdojo.transform;

import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.transform.Shear;
import javafx.stage.Stage;

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

        @Override
        public void start(Stage stage) {
                Rectangle rect1 =
                         new Rectangle(100, 50, Color.LIGHTGRAY);
                rect1.setStroke(Color.BLACK);

                Rectangle rect2 =
                         new Rectangle(100, 50, Color.LIGHTGRAY);
                rect2.setStroke(Color.BLACK);
                rect2.setOpacity(0.5);

                // Apply a shear on rect2\. The x and y multipliers are
                     // 0.5 and (0, 0) is the pivot point.
                Shear shear = new Shear(0.5, 0.5);
                rect2.getTransforms().addAll(shear);

                Group root = new Group(rect1, rect2);
                Scene scene = new Scene(root);
                stage.setScene(scene);
                stage.setTitle("Applying the Shear Transformation");
                stage.show();
        }
}

Listing 18-6Using the Shear Transformation

让我们使用(0,0)以外的枢轴点进行Shear变换。考虑以下代码片段:

Rectangle rect1 = new Rectangle(100, 50, Color.LIGHTGRAY);
rect1.setX(100);
rect1.setStroke(Color.BLACK);
Rectangle rect2 = new Rectangle(100, 50, Color.LIGHTGRAY);
rect2.setX(100);
rect2.setStroke(Color.BLACK);
rect2.setOpacity(0.5);

// Apply a shear on rect2\. The x and y multipliers are 0.5 and
// (100, 50) is the pivot point.
Shear shear = new Shear(0.5, 0.5, 100, 50);
rect2.getTransforms().addAll(shear);

代码类似于清单 18-6 中所示的代码。矩形的左上角放置在(100,0)处,因此我们可以完全看到剪切的矩形。我们使用矩形左下角的(100,50)作为枢轴点。图 18-11 显示了变换后的矩形。请注意,变换没有移动轴心点。

img/336502_2_En_18_Fig11_HTML.jpg

图 18-11

使用(100,50)作为轴心点进行剪切变换的矩形

让我们应用我们的公式来验证右上角的坐标,它最初相对于矩形的未变换坐标系位于(200,0):

x1 = 200
y1 = 0
pivotX = 100
pivotY = 50
x = 0.5
y = 0.5

x2 = pivotX + (x1 - pivotX) + x * (y1 - pivotY)
   = 100 + (200 - 100) + 0.5 * (0 - 50)
   = 175

y2 = pivotY + (y1 - pivotY) + y * (x1 - pivotX)
   = 50 + (0 -50) + 0.5 * (200 - 100)
   = 50

因此,(175,50)是矩形的未变换坐标系中右上角的移动位置。

应用多重变换

您可以对一个节点应用多个变换。如前所述,transforms序列中的变换是在节点属性的变换集之前应用的。当使用Node类的属性时,将依次应用*移、旋转和缩放。当使用transforms序列时,变换按照它们在序列中的存储顺序被应用。

清单 18-7 中的程序创建了三个矩形并将它们放置在相同的位置。它以不同的顺序对第二个和第三个矩形应用多个变换。图 18-12 显示了结果。第一个矩形显示在其原始位置,因为我们没有对它应用任何变换。请注意,两个矩形在不同的位置结束。如果如下所示更改第三个矩形的变换顺序,两个矩形将重叠:

img/336502_2_En_18_Fig12_HTML.png

图 18-12

具有多重变换的矩形

// MultipleTransformations.java
package com.jdojo.transform;

import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.transform.Rotate;
import javafx.scene.transform.Scale;
import javafx.scene.transform.Translate;
import javafx.stage.Stage;

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

        @Override
        public void start(Stage stage) {
                Rectangle rect1 =
                         new Rectangle(100, 50, Color.LIGHTGRAY);
                rect1.setStroke(Color.BLACK);

                Rectangle rect2 =
                         new Rectangle(100, 50, Color.LIGHTGRAY);
                rect2.setStroke(Color.BLACK);
                rect2.setOpacity(0.5);

                Rectangle rect3 =
                         new Rectangle(100, 50, Color.LIGHTCYAN);
                rect3.setStroke(Color.BLACK);
                rect3.setOpacity(0.5);

                // apply transformations to rect2
                rect2.setTranslateX(100);
                rect2.setTranslateY(0);
                rect2.setRotate(30);
                rect2.setScaleX(1.2);
                rect2.setScaleY(1.2);

                // Apply the same transformation as on rect2, but in a
                     // different order
                rect3.getTransforms().addAll(
                         new Scale(1.2, 1.2, 50, 25),
                   new Rotate(30, 50, 25),
                   new Translate(100, 0));

                Group root = new Group(rect1, rect2, rect3);
                Scene scene = new Scene(root);
                stage.setScene(scene);
                stage.setTitle("Applying Multiple Transformations");
                stage.show();
        }
}

Listing 18-7Using Multiple Transformations on a Node

rect3.getTransforms().addAll(
    new Translate(100, 0),
   new Rotate(30, 50, 25),
   new Scale(1.2, 1.2, 50, 25));

摘要

变换是坐标空间中的点到同一坐标空间中的点的映射,保留一组几何属性。几种类型的变换可以应用于坐标空间中的点。JavaFX 支持以下类型的变换:*移、旋转、剪切、缩放和仿射。

抽象Transform类的一个实例表示 JavaFX 中的一个变换。Transform类包含节点上所有类型变换使用的公共方法和属性。它包含创建特定类型变换的工厂方法。所有的变换类都在javafx.scene.transform包中。

仿射变换是一种广义变换,它保留了点的数量和唯一性、直线的直线性以及点在*面上表现出的特性。*行线(和*面)在变换后保持*行。仿射变换可能不会保留线之间的角度和点之间的距离。但是,直线上各点之间的距离比保持不变。*移、缩放、相似变换、相似变换、反射、旋转和剪切都是仿射变换的例子。Affine类的一个实例代表一个仿射变换。

有两种方法可以将变换应用到节点:使用Node类中的特定属性和使用节点的transforms序列。

*移会将节点的每个点相对于其父坐标系沿指定方向移动固定距离。这是通过将节点的局部坐标系的原点移动到新的位置来实现的。Translate类的一个实例代表一个翻译。

在旋转变换中,轴围绕坐标空间中的轴心点旋转,并且点的坐标被映射到新的轴。Rotate类的一个实例代表一个旋转变换。

缩放变换通过缩放因子沿坐标系的轴缩放测量单位。这将导致节点的尺寸沿轴按指定的比例因子变化(拉伸或收缩)。沿轴的尺寸乘以沿该轴的比例因子。变换应用于变换后坐标保持不变的轴心点。Scale类的一个实例代表一个比例变换。

剪切变换会围绕枢轴点旋转节点的局部坐标系的轴,因此这些轴不再垂直。变换后,矩形节点变成了*行四边形。Shear类的一个实例表示一个剪切变换。

您可以对一个节点应用多个变换。transforms序列中的变换在节点属性上的变换集之前应用。当使用Node类的属性时,将依次应用*移、旋转和缩放。当使用transforms序列时,变换按照它们在序列中的存储顺序被应用。

下一章将讨论如何将动画应用到节点上。

十九、理解动画

在本章中,您将学习:

  • JavaFX 里有什么动画

  • 关于 JavaFX 中用于在 JavaFX 中执行动画的类

  • 如何执行时间轴动画以及如何在时间轴动画上设置提示点

  • 如何控制动画,如播放、倒退、暂停和停止

  • 如何使用过渡来执行动画

  • 关于不同类型的插值器及其在动画中的作用

本章的例子在com.jdojo.animation包中。为了让它们工作,您必须在module-info.java文件中添加相应的一行:

...
opens com.jdojo.animation to javafx.graphics, javafx.base;
...

什么是动画?

在现实世界中,动画暗示了某种运动,这是通过快速连续显示图像而生成的。例如,当你看电影时,你正在观看图像,这些图像变化如此之快,以至于你产生了一种运动错觉。

在 JavaFX 中,动画被定义为随时间改变节点的属性。如果改变的属性决定了节点的位置,JavaFX 中的动画将产生电影中的运动错觉。不是所有的动画都必须包含运动;例如,随时间改变形状的fill属性是 JavaFX 中不涉及运动的动画。

为了理解动画是如何执行的,理解一些关键概念是很重要的:

  • 时间表

  • 关键帧

  • 关键字值

  • 内插器

动画是在一段时间内完成的。一个时间线表示在给定时刻动画期间与关键帧相关联的时间进程。一个关键帧代表在时间轴上特定时刻被动画化的节点的状态。关键帧有相关的键值。键值表示节点的属性值以及要使用的插值器。

假设你想在十秒钟内从左向右水*移动场景中的一个圆。图 19-1 显示了部分位置的圆。粗水*线表示时间线。实线圆圈表示时间轴上特定时刻的关键帧。与关键帧相关联的键值显示在顶行。例如,第五秒的关键帧的圆形的translateX属性值为 500,在图中显示为 tx=500。

img/336502_2_En_19_Fig1_HTML.png

图 19-1

使用时间轴沿水*线制作圆形动画

开发人员提供时间表、关键帧和关键值。在这个例子中,有五个关键帧。如果 JavaFX 在五个相应的时刻只显示五个关键帧,动画看起来会不稳定。为了提供*滑的动画,JavaFX 需要在时间轴上的任意时刻插入圆的位置。也就是说,JavaFX 需要在两个连续提供的关键帧之间创建中间关键帧。JavaFX 在一个插值器的帮助下完成这项工作。默认情况下,它使用一个线性插值器,随着时间线性改变动画的属性。也就是说,如果时间线上的时间超过了 x%,则属性值将是初始目标值和最终目标值之间的 x%。JavaFX 使用插值器创建带虚线轮廓的圆。

了解动画课程

JavaFX 中提供动画的类在javafx.animation包中,除了Duration类在javafx.util包中。图 19-2 显示了大多数动画相关类的类图。

img/336502_2_En_19_Fig2_HTML.png

图 19-2

动画中使用的核心类的类图

抽象的Animation类表示一个Animation。它包含所有类型的动画使用的通用属性和方法。

JavaFX 支持两种类型的动画:

  • 时间轴动画

  • 过渡

在时间轴动画中,创建时间轴并向其添加关键帧。JavaFX 使用插值器创建中间关键帧。Timeline类的一个实例代表一个时间轴动画。这种类型的动画需要更多一点的代码,但它给你更多的控制。

通常执行几种类型的动画(沿路径移动节点,随时间改变节点的不透明度等。).这些类型的动画被称为过渡。它们使用内部时间表来执行。Transition类的一个实例代表一个过渡动画。Transition类的几个子类支持特定类型的转换。例如,FadeTransition类通过随时间改变节点的不透明度来实现渐隐效果动画。您创建一个Transition类的实例(通常是它的一个子类的实例),并为要动画的属性指定初始值和最终值以及动画的持续时间。JavaFX 负责创建时间轴和执行动画。这种类型的动画更容易使用。

有时,您可能希望按顺序或同时执行多个过渡。SequentialTransitionParallelTransition类分别允许您顺序地和同时地执行一组转换。

了解实用程序类

在讨论 JavaFX 动画的细节之前,我将讨论几个用于实现动画的实用程序类。下面几节将讨论这些类。

了解持续时间类

Duration类在javafx.util包中。它以毫秒、秒、分钟和小时表示持续时间。它是一个不可变的类。一个Duration代表一个动画的每个周期的时间量。一个Duration可以代表一个正的或负的持续时间。

您可以通过三种方式创建一个Duration对象:

  • 使用构造器

  • 使用工厂方法

  • 使用String格式的持续时间中的valueOf()方法

构造器花费的时间以毫秒为单位:

Duration tenMillis = new Duration(10);

工厂方法为不同的时间单位创建Duration对象。分别是millis()seconds()minutes()hours():

Duration tenMillis = Duration.millis(10);
Duration tenSeconds = Duration.seconds(10);
Duration tenMinutes = Duration.minutes(10);
Duration tenHours = Duration.hours(10);

静态方法valueOf()接受一个包含持续时间的String参数,并返回一个Duration对象。参数的格式为“numberms|s|m|h”,其中number为时间量,mssmh分别表示millisecondssecondsminuteshours

Duration tenMillis = Duration.valueOf("10.0ms");
Duration tenMillisNeg = Duration.valueOf("-10.0ms");

您还可以分别使用Duration类的UNKNOWNINDEFINITE常量来表示一段未知的时间和一段不确定的时间。您可以使用isIndefinite()isUnknown()方法来检查持续时间是否表示不确定或未知的时间量。该类声明了另外两个常量,ONEZERO,分别代表一毫秒和零(无时间)的持续时间。

Duration类提供了几种操作持续时间的方法(将一个持续时间添加到另一个持续时间,将一个持续时间除以一个数,比较两个持续时间,等等。).清单 [19-1 展示了如何使用Duration类。

// DurationTest.java
package com.jdojo.animation;

import javafx.util.Duration;

public class DurationTest {
        public static void main(String[] args) {
                Duration d1 = Duration.seconds(30.0);
                Duration d2 = Duration.minutes(1.5);
                Duration d3 = Duration.valueOf("35.25ms");
                System.out.println("d1  = " + d1);
                System.out.println("d2  = " + d2);
                System.out.println("d3  = " + d3);

                System.out.println("d1.toMillis() = " + d1.toMillis());
                System.out.println("d1.toSeconds() = " + d1.toSeconds());
                System.out.println("d1.toMinutes() = " + d1.toMinutes());
                System.out.println("d1.toHours() = " + d1.toHours());

                System.out.println("Negation of d1  = " + d1.negate());
                System.out.println("d1 + d2 = " + d1.add(d2));
                System.out.println("d1 / 2.0 = " + d1.divide(2.0));

                Duration inf = Duration.millis(1.0/0.0);
                Duration unknown = Duration.millis(0.0/0.0);
                System.out.println("inf.isIndefinite() = " +
                         inf.isIndefinite());
                System.out.println("unknown.isUnknown() = " +
                         unknown.isUnknown());
        }
}
d1  = 30000.0 ms
d2  = 90000.0 ms
d3  = 35.25 ms
d1.toMillis() = 30000.0
d1.toSeconds() = 30.0
d1.toMinutes() = 0.5
d1.toHours() = 0.008333333333333333
Negation of d1  = -30000.0 ms
d1 + d2 = 120000.0 ms
d1 / 2.0 = 15000.0 ms
inf.isIndefinite() = true
unknown.isUnknown() = true

Listing 19-1Using the Duration Class

了解 KeyValue 类

KeyValue类的一个实例表示一个键值,该键值是在动画过程中针对特定间隔插入的。它概括了三件事:

  • 一个目标

  • 目标的结束值

  • 插值器

目标是一个WritableValue,它将所有 JavaFX 属性限定为一个目标。结束值是时间间隔结束时的目标值。插值器用于计算中间关键帧。

关键帧包含一个或多个关键值,它定义时间轴上的特定点。图 19-3 显示了时间线上的一个间隔。间隔由两个时刻定义:时刻 1时刻 2 。两个瞬间都有一个相关的关键帧;每个关键帧包含一个键值。动画可以在时间轴上向前或向后播放。当一个间隔开始时,目标的结束值取自该间隔的结束关键帧的关键值,其插值器用于计算中间关键帧。假设,在图中,动画正向播放,第一个瞬间发生在第二个瞬间之前。从时刻 1 到时刻 2,键值 2 的插值器将用于计算该间隔的关键帧。如果动画是反向进行的,键值 1 的插值器将用于计算从时刻 2 到时刻 1 的中间关键帧。

img/336502_2_En_19_Fig3_HTML.png

图 19-3

时间轴上两个瞬间的关键帧

KeyValue类是不可变的。它提供了两个构造器:

  • KeyValue(WritableValue<T> target, T endValue)

  • KeyValue(WritableValue<T> target, T endValue, Interpolator interpolator)

Interpolator.LINEAR用作默认插值器,它随时间线性插值动画属性。稍后我将讨论不同类型的插值器。

下面的代码片段创建了一个Text对象和两个KeyValue对象。translateX地产是目标。0 和 100 是目标的最终值。使用默认插值器:

Text msg = new Text("JavaFX animation is cool!");
KeyValue initKeyValue = new KeyValue(msg.translateXProperty(), 0.0);
KeyValue endKeyValue = new KeyValue(msg.translateXProperty(), 100.0);

下面的代码片段类似于前面显示的代码。它使用Interpolator.EASE_BOTH插值器,在开始和接*结束时减慢动画:

Text msg = new Text("JavaFX animation is cool!");
KeyValue initKeyValue = new KeyValue(msg.translateXProperty(), 0.0,
    Interpolator.EASE_BOTH);
KeyValue endKeyValue = new KeyValue(msg.translateXProperty(), 100.0,
    Interpolator.EASE_BOTH);

了解关键帧类

关键帧定义了时间轴上指定点的节点的目标状态。目标状态由与关键帧相关联的关键值来定义。

一个关键帧包含四件事:

  • 时间轴上的瞬间

  • 一组KeyValue

  • 一个名字

  • 一个ActionEvent处理者

时间轴上与关键帧相关联的瞬间由一个Duration定义,它是时间轴上关键帧的偏移量。

KeyValues组定义了关键帧目标的结束值。

一个关键帧可以有一个可选的名称,该名称可以用作一个提示点,以便在动画过程中跳转到它所定义的时刻。Animation类的getCuePoints()方法返回Timeline上提示点的Map

可选地,您可以将一个ActionEvent处理程序附加到一个KeyFrame上。在动画过程中,当关键帧到达时,就会调用ActionEvent处理程序。

KeyFrame类的一个实例代表一个关键帧。该类提供了几个构造器:

  • KeyFrame(Duration time, EventHandler<ActionEvent> onFinished, KeyValue... values)

  • KeyFrame(Duration time, KeyValue... values)

  • KeyFrame(Duration time, String name, EventHandler<ActionEvent> onFinished, Collection<KeyValue> values)

  • KeyFrame(Duration time, String name, EventHandler<ActionEvent> onFinished, KeyValue... values)

  • KeyFrame(Duration time, String name, KeyValue... values)

下面的代码片段创建了两个KeyFrame实例,分别在时间轴上的 0 秒和 3 秒指定了一个Text节点的translateX属性:

Text msg = new Text("JavaFX animation is cool!");
KeyValue initKeyValue = new KeyValue(msg.translateXProperty(), 0.0);
KeyValue endKeyValue = new KeyValue(msg.translateXProperty(), 100.0);

KeyFrame initFrame = new KeyFrame(Duration.ZERO, initKeyValue);
KeyFrame endFrame = new KeyFrame(Duration.seconds(3), endKeyValue);

了解时间轴动画

时间轴动画用于制作节点的任何属性的动画。Timeline类的一个实例代表一个时间轴动画。使用时间轴动画包括以下步骤:

  • 构建关键帧。

  • 创建一个带有关键帧的Timeline对象。

  • 设置动画属性。

  • 使用play()方法运行动画。

您可以在创建Timeline时或之后添加关键帧。Timeline实例将所有关键帧保存在一个ObservableList<KeyFrame>对象中。getKeyFrames()方法返回列表。您可以随时修改关键帧列表。如果时间轴动画已经在运行,您需要停止并重新启动它,以获得修改后的关键帧列表。

Timeline类包含几个构造器:

  • Timeline()

  • Timeline(double targetFramerate)

  • Timeline(double targetFramerate, KeyFrame... keyFrames)

  • Timeline(KeyFrame... keyFrames)

无参数构造器创建一个没有关键帧的Timeline,动画以最佳速度运行。其他构造器允许您指定动画的目标帧速率(即每秒的帧数)和关键帧。

注意,关键帧添加到Timeline的顺序并不重要。Timeline将根据它们的时间偏移对它们进行排序。

清单 19-2 中的程序启动了一个时间轴动画,该动画从右向左水*滚动文本,直到永远。图 19-4 显示了动画的截图。

img/336502_2_En_19_Fig4_HTML.png

图 19-4

使用时间轴动画滚动文本

// ScrollingText.java
// ...find in the book's download area.

Listing 19-2Scrolling Text Using a Timeline Animation

执行动画的逻辑在start()方法中。该方法首先创建一个Text对象,一个带有Text对象的Pane,并为舞台设置一个场景。在展示舞台之后,它设置一个动画。

它获取场景和Text对象的宽度:

double sceneWidth = scene.getWidth();
double msgWidth = msg.getLayoutBounds().getWidth();

创建了两个关键帧:一个用于时间= 0 秒,另一个用于时间= 3 秒。动画使用Text对象的translateX属性来改变其水*位置,使其滚动。在 0 秒时,Text被定位在场景宽度,所以它是不可见的。在三秒钟时,它被放置在场景的左侧,距离等于它的长度,因此它也是不可见的:

KeyValue initKeyValue = new KeyValue(msg.translateXProperty(), sceneWidth);
KeyFrame initFrame = new KeyFrame(Duration.ZERO, initKeyValue);

KeyValue endKeyValue = new KeyValue(msg.translateXProperty(), -1.0 * msgWidth);
KeyFrame endFrame = new KeyFrame(Duration.seconds(3), endKeyValue);

用两个关键帧创建一个Timeline对象:

Timeline timeline = new Timeline(initFrame, endFrame);

默认情况下,动画将只运行一次。也就是说,Text会从右向左滚动一次,动画就会停止。可以设置动画的循环次数,即动画需要运行的次数。通过将循环计数设置为Timeline.INDEFINITE,您可以永远运行动画:

timeline.setCycleCount(Timeline.INDEFINITE);

最后,通过调用play()方法启动动画:

timeline.play();

我们的例子有一个缺陷。当场景的宽度改变时,文本的滚动不会更新其初始水*位置。只要场景宽度发生变化,就可以通过更新初始关键帧来解决这个问题。将以下语句添加到列出 19-2 的start()方法中。它为场景宽度添加了一个ChangeListener,用于更新关键帧并重新启动动画:

scene.widthProperty().addListener( (prop, oldValue , newValue) -> {
        KeyValue kv = new KeyValue(msg.translateXProperty(),
              scene.getWidth());
        KeyFrame kf = new KeyFrame(Duration.ZERO, kv);
        timeline.stop();
        timeline.getKeyFrames().clear();
        timeline.getKeyFrames().addAll(kf, endFrame);
        timeline.play();
});

创建一个只有一个关键帧的Timeline动画是可能的。关键帧被视为最后一个关键帧。Timeline使用正在制作动画的WritableValue的当前值合成一个初始关键帧(时间= 0 秒)。为了查看效果,让我们替换该语句

Timeline timeline = new Timeline(initFrame, endFrame);

在清单 19-2 中有如下内容:

Timeline timeline = new Timeline(endFrame);

Timeline将用Text对象的translateX属性的当前值 0.0 创建一个初始关键帧。这一次,Text的滚动方式有所不同。开始滚动时,将Text设置为 0.0,并向左滚动,因此它超出了场景。

控制动画

Animation类包含的属性和方法可以用来以各种方式控制动画。以下部分将解释这些属性和方法,以及如何使用它们来控制动画。

播放动画

Animation类包含四种播放动画的方法:

  • play()

  • playFrom(Duration time)

  • playFrom(String cuePoint)

  • playFromStart()

play()方法从当前位置播放动画。如果动画从未开始或停止,它将从头开始播放。如果动画暂停,它将从暂停的位置播放。在调用play()方法之前,可以使用jumpTo(Duration time)jumpTo(String cuePoint)方法将动画的当前位置设置为特定的持续时间或提示点。调用play()方法是异步的。动画可能不会立即开始。在动画运行时调用play()方法没有任何效果。

playFrom()方法从指定的持续时间或指定的提示点播放动画。调用这个方法相当于使用jumpTo()方法设置当前位置,然后调用play()方法。

playFromStart()方法从头开始播放动画(持续时间= 0)。

延迟动画的开始

您可以使用delay属性指定动画开始的延迟时间。该值在Duration中指定。默认情况下,它为零毫秒:

Timeline timeline = ...

// Delay the start of the animation by 2 seconds
timeline.setDelay(Duration.seconds(2));

// Play the animation
timeline.play();

停止动画

使用stop()方法停止正在运行的动画。如果动画没有运行,则该方法无效。当调用方法时,动画可能不会立即停止,因为方法是异步执行的。方法将当前位置重置为开始位置。即在stop()之后调用play()将从头开始播放动画:

Timeline timeline = ...
...
timeline.play();
...
timeline.stop();

暂停动画

使用pause()方法暂停动画。当动画未运行时调用此方法没有任何效果。此方法异步执行。当动画暂停时调用play()方法,从当前位置播放动画。如果你想从头开始播放动画,调用playFromStart()方法。

了解动画的状态

动画可以是以下三种状态之一:

  • 运转

  • 暂停

  • 停止

这三种状态由Animation.Status枚举的RUNNINGSTOPPEDPAUSED常量表示。您不能直接更改动画的状态。它是通过调用Animation类的方法之一来改变的。该类包含一个只读的status属性,可用于随时了解动画的状态:

Timeline timeline = ...
...
Animation.Status status = timeline.getStatus();
switch(status) {
        case RUNNING:
                System.out.println("Running");
                break;
        case STOPPED:
                System.out.println("Stopped");
                break;
        case PAUSED:
                System.out.println("Paused");
                break;
}

循环播放动画

一个动画可以循环多次,甚至无限循环。cycleCount属性指定动画中的循环数,默认为 1。如果你想无限循环的运行动画,指定Animation.INDEFINITEcycleCountcycleCount必须设置为大于零的值。如果在动画运行过程中cycleCount发生变化,动画必须停止并重启以获得新值:

Timeline timeline1 = ...
Timeline1.setCycleCount(Timeline.INDEFINITE); // Run the animation forever

Timeline timeline2 = ...
Timeline2.setCycleCount(2); // Run the animation for two cycles

自动反转动画

默认情况下,动画仅向前运行。例如,我们的滚动文本动画在一个周期内从右向左滚动文本。在下一个循环中,再次从右向左滚动。

使用autoReverse属性,您可以定义动画是否在交替循环中反向执行。默认情况下,它被设置为 false。将其设定为 true 以反转动画的方向:

Timeline timeline = ...
timeline.setAutoReverse(true); // Reverse direction on alternating cycles

如果您更改了autoReverse,您需要停止并重启动画以使新值生效。

附加已完成的操作

当动画结束时,您可以执行一个ActionEvent处理程序。在动画运行时停止动画或终止应用程序将不会执行处理程序。您可以在Animation类的onFinished属性中指定处理程序。下面的代码片段将onFinished属性设置为在标准输出中打印消息的ActionEvent处理程序:

Timeline timeline = ...
timeline.setOnFinished(e -> System.out.print("Animation finished."));

请注意,具有Animation.INDEFINITE循环计数的动画将不会完成,并且将这样的动作附加到动画将永远不会执行。

了解动画的持续时间

动画包含两种类型的持续时间:

  • 播放动画一个循环的持续时间

  • 播放动画所有循环的持续时间

这些持续时间不是直接设置的。它们是使用动画的其他属性(循环计数、关键帧等)设置的。).

使用关键帧设置一个周期的持续时间。当动画以 1.0 的速率播放时,具有最大持续时间的关键帧决定了一个周期的持续时间。Animation类的只读cycleDuration属性报告一个周期的持续时间。

动画的总持续时间由只读的totalDuration属性报告。等于cycleCount * cycleDuration。如果cycleCount被设置为Animation.INDEFINITE,则totalDuration被报告为Duration.INDEFINITE

请注意,动画的实际持续时间取决于由rate属性表示的播放速率。因为播放速率可以在动画运行时改变,所以没有简单的方法来计算动画的实际持续时间。

调整动画的速度

Animation类的rate属性指定动画的方向和速度。其值的符号表示方向。数值的大小表示速度。正值表示向前方向的间隙。负值表示反向运动。值 1.0 被认为是正常播放速率,值 2.0 是正常速率的两倍,值 0.50 是正常速率的一半,依此类推。0.0 的rate停止播放。

可以反转正在运行的动画的rate。在这种情况下,动画从当前位置反向播放已经过去的时间。请注意,您不能使用负的rate来启动动画。带有否定rate的动画将不会启动。只有在动画播放一段时间后,您才能将rate改为负值。

Timeline timeline = ...

// Play the animation at double the normal rate
Timeline.setRate(2.0);
...
timeline.play();
...
// Invert the rate of the play
timeline.setRate(-1.0 * timeline.getRate());

只读currentRate属性表示动画播放的当前速率(方向和速度)。ratecurrentRate属性的值可能不相等。rate属性表示动画运行时的预期播放速率,而currentRate表示动画的播放速率。当动画停止或暂停时,currentRate值为 0.0。如果动画自动反转方向,currentRate将在反转过程中报告不同的方向;例如,如果rate为 1.0,则currentRate报告正向播放周期为 1.0,反向播放周期为–1.0。

理解提示点

您可以在时间轴上设置提示点。提示点被命名为时间轴上的瞬间。动画可以使用jumpTo(String cuePoint)方法跳转到提示点。一个动画保持一个提示点的ObservableMap<String,Duration>。地图中的关键字是提示点的名称,值是时间轴上相应的持续时间。使用getCuePoints()方法获得提示点地图的参考。

有两种方法可以向时间轴添加提示点:

  • 为您添加到时间线的KeyFrame命名,该时间线在提示点地图中添加提示点

  • 将名称-持续时间对添加到由Animation类的getCuePoints()方法返回的映射中

    提示每个动画都有两个预定义的提示点:“开始”和“结束”它们设置在动画的开始和结束处。这两个提示点不会出现在由getCuePoints()方法返回的地图中。

下面的代码片段创建了一个名为“midway”的KeyFrame当它被添加到时间线时,一个名为“中途”的提示点将被自动添加到时间线。你可以使用jumpTo("midway")跳转到这个KeyFrame

// Create a KeyFrame with name “midway”
KeyValue midKeyValue = ...
KeyFrame midFrame = new KeyFrame(Duration.seconds(5), "midway", midKeyValue);

下面的代码片段将两个提示点直接添加到时间轴的提示点映射中:

Timeline timeline = ...
timeline.getCuePoints().put("3 seconds", Duration.seconds(3));
timeline.getCuePoints().put("7 seconds", Duration.seconds(7));

清单 19-3 中的程序展示了如何在时间线上添加和使用提示点。它添加了一个带有“中途”名称的KeyFrame,该名称自动成为提示点。它将两个提示点“3 秒”和“7 秒”直接添加到提示点地图中。可用提示点列表显示在屏幕左侧的ListView中。一个Text物体以十秒的周期滚动。程序显示如图 19-5 所示的窗口。从列表中选择一个提示点,动画将从该点开始播放。

img/336502_2_En_19_Fig5_HTML.png

图 19-5

带有提示点列表的滚动文本

// CuePointTest.java
// ...find in the book's download area.

Listing 19-3Using Cue Points in Animation

理解转变

在前面的部分中,您看到了使用时间轴的动画,其中包括在时间轴上设置关键帧。在所有情况下使用时间轴动画并不容易。考虑在圆形路径中移动节点。创建关键帧和设置时间线以在圆形路径上移动节点并不容易。JavaFX 包含许多类(称为 transitions ),允许您使用预定义的属性来制作节点动画。

所有的转换类都继承自Transition类,而后者又继承自Animation类。Animation类中的所有方法和属性也可用于创建过渡。过渡类负责创建关键帧和设置时间轴。您需要指定节点、动画持续时间以及插值的结束值。特殊的过渡类可用于组合多个动画,这些动画可以按顺序或并行运行。

Transition类包含一个interpolator属性,该属性指定动画期间要使用的插值器。默认情况下,它使用Interpolator.EASE_BOTH,缓慢启动动画,加速,并在接*结束时减速。

了解渐变过渡

FadeTransition类的实例通过在指定的持续时间内逐渐增加或减少节点的opacity来表示节点的淡入或淡出效果。类别会定义下列属性来指定动画:

  • duration

  • node

  • fromValue

  • toValue

  • byValue

duration属性指定动画一个周期的duration

node属性指定了其opacity属性被改变的节点。

fromValue属性指定不透明度的初始值。如果未指定,则使用节点的当前opacity

toValue属性指定opacity的结束值。对于动画的一个周期,节点的opacity在初始值和toValue之间更新。

byValue属性允许您使用公式以不同的方式指定opacity的结束值

opacity_end_value = opacity_initial_value + byValue

byValue允许您通过增加或减少初始值一个偏移量来设置opacity的结束值。如果同时指定了toValuebyValue,则使用toValue

假设您想要在动画中将节点的初始和结束不透明度设置在 1.0 和 0.5 之间。您可以通过将fromValuetoValue设置为 1.0 和 0.50 或者将fromValuebyValue设置为 1.0 和–0.50 来实现。

节点的有效opacity值介于 0.0 和 1.0 之间。可以将FadeTransition属性设置为超出范围。该转换负责将实际值箝位在该范围内。

以下代码片段通过在两秒钟内将opacity从 1.0 更改为 0.20,为Rectangle设置淡出动画:

Rectangle rect = new Rectangle(200, 50, Color.RED);
FadeTransition fadeInOut = new FadeTransition(Duration.seconds(2), rect);
fadeInOut.setFromValue(1.0);
fadeInOut.setToValue(.20);
fadeInOut.play();

清单 19-4 中的程序为Rectangle创建了一个无限循环中的淡出和淡入效果。

// FadeTest.java

package com.jdojo.animation;

import javafx.animation.FadeTransition;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import javafx.util.Duration;

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

        @Override
        public void start(Stage stage) {
                Rectangle rect = new Rectangle(200, 50, Color.RED);
                HBox root = new HBox(rect);
                Scene scene = new Scene(root);
                stage.setScene(scene);
                stage.setTitle("Fade-in and Fade-out");
                stage.show();

                // Set up a fade-in and fade-out animation for the
                     // rectangle
                FadeTransition fadeInOut =
                         new FadeTransition(Duration.seconds(2), rect);
                fadeInOut.setFromValue(1.0);
                fadeInOut.setToValue(.20);
                fadeInOut.setCycleCount(FadeTransition.INDEFINITE);
                fadeInOut.setAutoReverse(true);
                fadeInOut.play();
        }
}

Listing 19-4Creating a Fading Effect Using the FadeTransition Class

了解填充过渡

FillTransition类的实例通过在指定的范围和持续时间之间逐渐过渡形状的fill属性来表示形状的填充过渡。类别会定义下列属性来指定动画:

  • duration

  • shape

  • fromValue

  • toValue

duration属性指定动画一个周期的duration

shape属性指定了其fill属性被改变的Shape

fromValue属性指定初始的fill颜色。如果未指定,则使用形状的当前fill

toValue属性指定fill的结束值。

在动画的一个循环中,形状的fill在初始值和toValue之间更新。Shape类中的fill属性被定义为一个Paint。然而,fromValuetoValue属于Color类型。也就是说,填充过渡适用于两个Color,而不是两个Paint

以下代码片段通过在两秒钟内将fill从蓝紫色更改为天蓝色,为Rectangle设置填充过渡:

FillTransition fillTransition = new FillTransition(Duration.seconds(2), rect);
fillTransition.setFromValue(Color.BLUEVIOLET);
fillTransition.setToValue(Color.AZURE);
fillTransition.play();

清单 19-5 中的程序创建一个填充过渡,在两秒钟内将Rectangle的填充颜色从蓝紫色变为天蓝色,这是一个无限循环。

// FillTest.java
// ...find in the book's download area.

Listing 19-5Creating a Fill Transition Using the FillTransition Class

了解笔画过渡

StrokeTransition类的实例通过在指定的范围和持续时间之间逐渐转换形状的stroke属性来表示形状的笔画转换。描边过渡的工作方式与填充过渡相同,只是它插入形状的stroke属性,而不是fill属性。StrokeTransition类包含与FillTransition类相同的属性。有关更多详细信息,请参考“了解填充过渡”一节。下面的代码片段开始在无限循环中制作Rectanglestroke动画。stroke在两秒钟的周期内从红色变为蓝色:

Rectangle rect = new Rectangle(200, 50, Color.WHITE);
StrokeTransition strokeTransition =
    new StrokeTransition(Duration.seconds(2), rect);
strokeTransition.setFromValue(Color.RED);
strokeTransition.setToValue(Color.BLUE);
strokeTransition.setCycleCount(StrokeTransition.INDEFINITE);
strokeTransition.setAutoReverse(true);
strokeTransition.play();

了解翻译转换

TranslateTransition类的一个实例通过在指定的持续时间内逐渐改变节点的translateXtranslateYtranslateZ属性来表示节点的*移过渡。类别会定义下列属性来指定动画:

  • duration

  • node

  • fromX

  • fromY

  • fromZ

  • toX

  • toY

  • toZ

  • byX

  • byY

  • byZ

duration属性指定动画一个周期的duration

node属性指定了其translateXtranslateYtranslateZ属性被改变的节点。

节点的初始位置由(fromXfromYfromZ)值定义。如果未指定,则使用节点的当前值(translateXtranslateYtranslateZ)作为初始位置。

(toXtoYtoZ)值指定结束位置。

(byXbyYbyZ)值允许您使用以下公式指定结束位置:

translateX_end_value = translateX_initial_value + byX
translateY_end_value = translateY_initial_value + byY
translateZ_end_value = translateZ_initial_value + byZ

如果同时指定了(toXtoYtoZ)和(byXbyYbyZ)值,则使用前者。

清单 19-6 中的程序通过在场景的宽度上滚动来为Text对象创建一个无限循环的*移过渡。清单 19-2 中的程序使用一个Timeline对象创建了相同的动画,只是有一点不同。他们使用不同的插值器。默认情况下,基于时间轴的动画使用Interpolator.LINEAR插值器,而基于过渡的动画使用Interpolator.EASE_BOTH插值器。当您运行清单 19-6 中的程序时,文本开始缓慢滚动,而在清单 19-2 中,文本一直以均匀的速度滚动。

// TranslateTest.java
// ...find in the book's download area.

Listing 19-6Creating a Translate Transition Using the TranslateTransition Class

了解旋转过渡

RotateTransition类的实例通过在指定的持续时间内逐渐改变其rotate属性来表示节点的旋转过渡。沿着指定的轴围绕节点的中心执行旋转。类别会定义下列属性来指定动画:

  • duration

  • node

  • axis

  • fromAngle

  • toAngle

  • byAngle

duration属性指定动画一个周期的duration

node属性指定了其rotate属性被改变的节点。

axis属性指定旋转轴。如果未指定,则使用noderotationAxis属性的值,默认为Rotate.Z_AXIS。可能的值有Rotate.X_AXISRotate.Y_AXISRotate.Z_AXIS

旋转的初始角度由fromAngle属性指定。如果未指定,节点的rotate属性的值将用作初始角度。

toAngle指定结束旋转角度。

byAngle允许您使用以下公式指定末端旋转角度:

rotation_end_value = rotation_initial_value + byAngle

如果同时指定了toAnglebyAngle值,则使用前者。所有角度都以度为单位。零度对应于 3 点钟位置。角度的正值是顺时针测量的。

清单 19-7 中的程序为Rectangle创建一个无限循环的旋转过渡。它交替顺时针和逆时针旋转Rectangle

// RotateTest.java
// ...find in the book's download area.

Listing 19-7Creating a Rotate Transition Using the RotateTransition Class

了解规模变化

ScaleTransition类的实例通过在指定的持续时间内逐渐改变节点的scaleXscaleYscaleZ属性来表示节点的缩放过渡。类别会定义下列属性来指定动画:

  • duration

  • node

  • fromX

  • fromY

  • fromZ

  • toX

  • toY

  • toZ

  • byX

  • byY

  • byZ

duration属性指定动画一个周期的duration

node属性指定了其scaleXscaleYscaleZ属性被改变的节点。

节点的初始比例由(fromXfromYfromZ)值定义。如果未指定,则使用节点的当前(scaleXscaleYscaleZ)值作为初始刻度。

(toXtoYtoZ)值指定结束刻度。

(byXbyYbyZ)值允许您使用以下公式指定结束刻度:

scaleX_end_value = scaleX_initial_value + byX
scaleY_end_value = scaleY_initial_value + byY
scaleZ_end_value = scaleZ_initial_value + byZ

如果同时指定了(toXtoYtoZ)和(byXbyYbyZ)值,则使用前者。

清单 19-8 中的程序通过在两秒钟内将Rectangle的宽度和高度在原始值的 100%和 20%之间改变,为其创建一个无限循环中的缩放过渡。

// ScaleTest.java
// ...find in the book's download area.

Listing 19-8Creating a Scale Transition Using the ScaleTransition Class

理解路径转换

PathTransition类的一个实例通过逐渐改变节点的translateXtranslateY属性来表示节点的路径转换,从而在指定的持续时间内沿着路径移动节点。路径由一个Shape的轮廓定义。类别会定义下列属性来指定动画:

  • duration

  • node

  • path

  • orientation

duration属性指定动画一个周期的duration

node属性指定了其rotate属性被改变的节点。

属性定义了节点移动的路径。是一辆Shape。您可以使用ArcCircleRectangleEllipsePathSVGPath等作为路径。

移动的节点可以保持相同的垂直位置,或者可以旋转它以保持它在路径上的任何点都垂直于路径的切线。属性指定了节点在路径上的垂直位置。它的值是PathTransition.OrientationType枚举的常量(NONEORTHOGONAL_TO_TANGENT)之一。默认为NONE,保持同样的直立姿势。ORTHOGONAL_TO_TANGENT值保持节点在任意点垂直于路径的切线。图 19-6 显示了使用PathTransition沿Circle移动的Rectangle的位置。注意使用ORTHOGONAL_TO_TANGENT方向时Rectangle沿路径旋转的方式。

img/336502_2_En_19_Fig6_HTML.png

图 19-6

使用 PathTransition 类的 orientation 属性的效果

您可以使用PathTransition类的属性或在构造器中指定路径转换的持续时间、路径和节点。该类包含以下构造器:

  • PathTransition()

  • PathTransition(Duration duration, Shape path)

  • PathTransition(Duration duration, Shape path, Node node)

清单 19-9 中的程序为Rectangle创建了一个无限循环中的路径转换。它沿着由Circle轮廓定义的圆形路径移动Rectangle

// PathTest.java
// ...find in the book's download area.

Listing 19-9Creating a Path Transition Using the PathTransition Class

了解暂停转换

PauseTransition类的一个实例代表一个暂停转换。它导致指定持续时间的延迟。它的用途并不明显。它不能单独使用。通常,它用在连续转场中,在两个转场之间插入暂停。它定义了一个duration属性来指定延迟的持续时间。

当一个转换完成后,如果您想在指定的持续时间后执行一个ActionEvent处理程序,暂停转换也是有用的。你可以通过设置它的onFinished属性来实现,这个属性是在Animation类中定义的。

// Create a pause transition of 400 milliseconds that is the default duration
PauseTransition pt1 = new PauseTransition();

// Change the duration to 10 seconds
pt1.setDuration(Duration.seconds(10));

// Create a pause transition of 5 seconds
PauseTransition pt2 = new PauseTransition(Duration.seconds(5));

如果您更改正在运行的暂停过渡的持续时间,您需要停止并重新启动过渡以获得新的持续时间。当我讨论顺序转换时,你会有一个例子。

了解顺序转换

SequentialTransition类的一个实例代表一个顺序转换。它按顺序执行一系列动画。动画列表可包含基于时间线的动画、基于过渡的动画或两者。

SequentialTransition类包含一个node属性,如果动画没有指定节点,该属性将用作列表中动画的节点。如果所有动画都指定一个节点,则不使用该属性。

一个SequentialTransition在一个ObservableList<Animation>中维护动画。getChildren()方法返回列表的引用。

下面的代码片段创建了一个渐变过渡、一个暂停过渡和一个路径过渡。三个过渡被添加到顺序过渡中。播放连续过渡时,它将按顺序播放渐变过渡、暂停过渡和路径过渡:

FadeTransition fadeTransition = ...
PauseTransition pauseTransition = ...
PathTransition pathTransition = ...

SequentialTransition st = new SequentialTransition();
st.getChildren().addAll(fadeTransition, pauseTransition, pathTransition);
st.play();

Tip

SequentialTransition类包含让你指定动画和节点列表的构造器。

清单 19-10 中的程序创建一个缩放过渡、填充过渡、暂停过渡和路径过渡,它们被添加到顺序过渡中。顺序转换在无限循环中运行。当程序运行时

  • 它将矩形放大两倍,然后缩小到原始大小。

  • 它将矩形的填充颜色从红色改为蓝色,然后再变回红色。

  • 它暂停 200 毫秒,然后在标准输出上打印一条消息。

  • 它沿着圆的轮廓移动矩形。

  • 前述动画序列被无限重复。

// SequentialTest.java
// ...find in the book's download area.

Listing 19-10Creating a Sequential Transition Using the SequentialTransition Class

理解并行转换

ParallelTransition类的一个实例代表一个并行转换。它同时执行一系列动画。动画列表可包含基于时间线的动画、基于过渡的动画或两者。

ParallelTransition类包含一个node属性,如果动画没有指定节点,该属性将用作列表中动画的节点。如果所有动画都指定一个节点,则不使用该属性。

一个ParallelTransition在一个ObservableList<Animation>中维护动画。getChildren()方法返回列表的引用。

下面的代码片段创建了一个渐变过渡和一个路径过渡。这些过渡被添加到*行过渡中。播放连续过渡时,它将应用淡入淡出效果,同时移动节点:

FadeTransition fadeTransition = ...
PathTransition pathTransition = ...

ParallelTransition pt = new ParallelTransition();
pt.getChildren().addAll(fadeTransition, pathTransition);
pt.play();

Tip

ParallelTransition类包含让你指定动画和节点列表的构造器。

清单 19-11 中的程序创建了一个渐变过渡和一个旋转过渡。它将它们添加到并行转换中。当程序运行时,矩形旋转,同时淡入/淡出。

// ParallelTest.java
// ...find in the book's download area.

Listing 19-11Creating a Parallel Transition Using the ParallelTransition Class

理解插值器

插值器是抽象Interpolator类的一个实例。插值器在动画中起着重要的作用。它的工作是在动画过程中计算中间关键帧的关键值。实现定制插值器很容易。你需要子类化Interpolator类并覆盖它的curve()方法。向curve()方法传递当前间隔经过的时间。时间在 0.0 和 1.0 之间标准化。间隔的开始和结束值分别为 0.0 和 1.0。当间隔时间过去一半时,传递给该方法的值将是 0.50。该方法的返回值指示动画属性的变化部分。

以下插值器称为线性插值器,其curve()方法返回传入的参数值:

Interpolator linearInterpolator = new Interpolator() {
        @Override
        protected double curve(double timeFraction) {
                return timeFraction;
        }
};

线性插值器要求动画属性的变化百分比与时间间隔的时间进程相同。

一旦有了自定义插值器,就可以用它来为基于时间轴的动画中的关键帧构造关键值。对于基于过渡的动画,您可以将其用作过渡类的interpolator属性。

动画 API 调用Interpolatorinterpolate()方法。如果动画属性是Number的实例,则返回

startValue + (endValue - startValue) * curve(timeFraction)

否则,如果动画属性是Interpolatable的实例,它将插值工作委托给Interpolatableinterpolate()方法。否则,插值器默认为离散插值器,当时间分数为 1.0 时返回 1.0,否则返回 0.0。

JavaFX 提供了一些动画中常用的标准插值器。它们在Interpolator类中作为常量或其静态方法可用:

  • 线性插值器

  • 离散插值器

  • 简易插补器

  • 缓出插值器

  • Ease-both 插值器

  • 样条插值器

  • 切线插值器

了解线性插值器

Interpolator.LINEAR常量代表线性插值器。它随时间线性插值节点的动画属性值。时间间隔内属性的百分比变化与经过时间的百分比相同。

了解分立插值器

Interpolator.DISCRETE常量代表离散插值器。离散插值器从一个关键帧跳到下一个关键帧,不提供中间关键帧。当时间分数为 1.0 时,插值器的curve()方法返回 1.0,否则返回 0.0。也就是说,动画属性值在间隔的整个持续时间内保持其初始值。它会在间隔结束时跳到结束值。清单 19-12 中的程序对所有关键帧使用离散插值器。当你运行这个程序时,它将文本从一个关键帧转移到另一个关键帧。将此示例与使用线性插值器的滚动文本示例进行比较。滚动文本示例*滑地移动了文本,而这个示例在移动中产生了抖动。

// HoppingText.java
// ...find in the book's download area.

Listing 19-12Using a Discrete Interpolator to Animate Hopping Text

了解渐强插值器

Interpolator.EASE_IN常量代表渐强插值器。它在时间间隔的前 20%缓慢启动动画,然后加速。

了解渐出插值器

Interpolator.EASE_OUT常量代表一个渐出插值器。它在 80%的时间间隔内以恒定的速度播放动画,之后速度变慢。

了解 Ease-Both 插值器

Interpolator.EASE_BOTH常量代表一个简单的内插器。在时间间隔的前 20%和后 20%播放动画较慢,否则保持恒定速度。

了解样条插值器

Interpolator.SPLINE(double x1double y1, double x2, double y2)静态方法返回一个样条插值器。它使用三次样条曲线形状来计算间隔中任意点的动画速度。参数(x1,y1)和(x2,y2)用(0,0)和(1,1)作为隐式锚点来定义三次样条形状的控制点。参数值介于 0.0 和 1.0 之间。

三次样条形状上给定点的斜率定义了该点的加速度。接*水*线的斜率表示减速,而接*垂直线的斜率表示加速。例如,使用(0,0,1,1)作为SPLINE方法的参数创建一个具有恒定速度的插值器,而参数(0.5,0,0.5,1.0)将创建一个在前半段加速而在后半段减速的插值器。更多详情请参考 www.w3.org/TR/SMIL/smil-animation.html#animationNS-OverviewSpline

了解切线插值器

Interpolator.TANGENT静态方法返回一个切线插值器,它定义了关键帧前后动画的行为。所有其他插值器在两个关键帧之间插值数据。如果为关键帧指定切线插值器,它将用于插值关键帧前后的数据。动画曲线是根据关键帧之前指定持续时间的切线(称为入切线)和关键帧之后指定持续时间的切线(称为出切线)来定义的。该插值器仅用于基于时间轴的动画,因为它影响两个间隔。

TANGENT静态方法被重载:

  • Interpolator TANGENT(Duration t1, double v1, Duration t2, double v2)

  • Interpolator TANGENT(Duration t, double v)

在第一个版本中,参数t1t2分别是关键帧前后的持续时间。参数v1v2是正切值和正切值。也就是说,v1是持续时间t1的正切值,v2是持续时间t2的正切值。第二个版本为两对指定了相同的值。

摘要

在 JavaFX 中,动画被定义为随时间改变节点的属性。如果改变的属性决定了节点的位置,那么 JavaFX 中的动画会产生一种运动的错觉。不是所有的动画都必须包含运动;例如,随时间改变一个Shapefill属性是 JavaFX 中一个不涉及运动的动画。

动画是在一段时间内完成的。一个时间线表示在给定时刻动画期间与关键帧相关联的时间进程。一个关键帧代表在时间轴上特定时刻被动画化的节点的状态。关键帧有相关的键值。键值表示节点的属性值以及要使用的插值器。

时间轴动画用于制作节点的任何属性的动画。Timeline类的一个实例代表一个时间轴动画。使用时间轴动画包括以下步骤:构造关键帧,创建带有关键帧的Timeline对象,设置动画属性,并使用play()方法运行动画。您可以在创建Timeline时或之后添加关键帧。Timeline实例将所有关键帧保存在一个ObservableList<KeyFrame>对象中。getKeyFrames()方法返回列表。您可以随时修改关键帧列表。如果时间轴动画已经在运行,您需要停止并重新启动它,以获得修改后的关键帧列表。

Animation类包含几个属性和方法来控制动画,比如播放、倒退、暂停和停止。

您可以在时间轴上设置提示点。提示点被命名为时间轴上的瞬间。动画可以使用jumpTo(String cuePoint)方法跳转到提示点。

在所有情况下使用时间轴动画并不容易。JavaFX 包含许多类(称为 transitions ),允许您使用预定义的属性来制作节点动画。所有的转换类都继承自Transition类,而后者又继承自Animation类。过渡类负责创建关键帧和设置时间轴。您需要指定节点、动画持续时间以及插值的结束值。特殊的过渡类可用于组合多个动画,这些动画可以按顺序或并行运行。Transition类包含一个interpolator属性,该属性指定在动画过程中使用的插值器。默认情况下,它使用Interpolator.EASE_BOTH,缓慢启动动画,加速,并在接*结束时减速。

插值器是抽象Interpolator类的一个实例。它的工作是在动画过程中计算中间关键帧的关键值。JavaFX 提供了几个内置插值器,如线性、离散、渐进和渐出。您还可以轻松实现自定义插值器。你需要子类化Interpolator类并覆盖它的curve()方法。向curve()方法传递当前间隔经过的时间。时间在 0.0 和 1.0 之间标准化。该方法的返回值指示动画属性的变化部分。

下一章将讨论如何在 JavaFX 应用程序中整合不同类型的图表。

二十、理解图表

在本章中,您将学习:

  • 什么是图表

  • JavaFX 中的图表 API 是什么

  • 如何使用图表 API 创建不同类型的图表

  • 如何用 CSS 设计图表样式

本章的例子在com.jdojo.chart包中。为了让它们工作,您必须在module-info.java文件中添加相应的一行:

...
opens com.jdojo.chart to javafx.graphics, javafx.base;
...

什么是图表?

图表是数据的图形表示。图表为可视化分析大量数据提供了一种更简单的方法。通常,它们用于监控和报告目的。存在不同类型的图表。它们表示数据的方式不同。并非所有类型的图表都适合分析所有类型的数据。例如,折线图适合于了解数据的比较趋势,而条形图适合于比较不同类别的数据。

JavaFX 支持图表,通过编写几行代码就可以将图表集成到 Java 应用程序中。它包含一个全面的、可扩展的图表 API,为几种类型的图表提供内置支持。

了解图表 API

图表 API 由javafx.scene.chart包中许多预定义的类组成。图 20-1 显示了代表不同类型图表的类的类图。

img/336502_2_En_20_Fig1_HTML.png

图 20-1

JavaFX 中表示图表的类的类图

抽象Chart是所有图表的基类。它继承了Node类。图表可以添加到场景图中。它们也可以像其他节点一样使用 CSS 样式。我将在讨论特定图表类型的章节中讨论图表样式。Chart类包含所有类型图表通用的属性和方法。

JavaFX 将图表分为两类:

  • 没有轴的图表

  • 具有 x 轴和 y 轴的图表

PieChart类属于第一类。它没有轴,是用来画饼图的。

XYChart类属于第二类。它是所有有两个轴的图表的抽象基类。它的子类,例如,LineChartBarChart等。,表示特定类型的图表。

JavaFX 中的每个图表都有三个部分:

  • 一个标题

  • 传奇

  • 内容(或数据)

不同类型的图表对数据的定义不同。Chart类包含以下所有类型图表共有的属性:

  • title

  • titleSide

  • legend

  • legendSide

  • legendVisible

  • animated

属性指定了图表的标题。属性指定了标题的位置。默认情况下,标题位于图表内容的上方。它的值是Side枚举的常量之一:TOP(默认)、RIGHTBOTTOMLEFT

通常,图表使用不同类型的符号来表示不同类别的数据。图例列出了符号及其说明。legend属性是一个Node,它指定了图表的图例。默认情况下,图例位于图表内容的下方。legendSide属性指定了图例的位置,它是Side枚举的常量之一:TOPRIGHTBOTTOM(默认)和LEFTlegendVisible属性指定图例是否可见。默认情况下,它是可见的。

animated属性指定图表内容的变化是否以某种类型的动画显示。默认情况下,这是真的。

用 CSS 设计图表样式

您可以设置所有类型图表的样式。Chart类定义了所有类型图表共有的属性。图表的默认 CSS 样式类名是图表。您可以为 CSS 中的所有图表指定legendSidelegendVisibletitleSide属性,如下所示:

.chart {
        -fx-legend-side: top;
        -fx-legend-visible: true;
        -fx-title-side: bottom;
}

每个图表定义两个子结构:

  • chart-title

  • chart-content

chart-title是一个Labelchart-content是一个Pane。以下样式将所有图表的背景色设置为黄色,标题字体设置为 Arial 16px 粗体:

.chart-content {
        -fx-background-color: yellow;
}

.chart-title {
        -fx-font-family: "Arial";
        -fx-font-size: 16px;
        -fx-font-weight: bold;
}

图例的默认样式类名称是图表图例。以下样式将图例背景色设置为浅灰色:

.chart-legend {
        -fx-background-color: lightgray;
}

每个图例都有两个子结构:

  • chart-legend-item

  • chart-legend-item-symbol

chart-legend-item是一个Label,它代表图例中的文本。chart-legend-item-symbol是一个Node,代表标签旁边的符号,默认为圆形。以下样式将图例中标签的字体大小设置为 10px,并将图例符号设置为箭头:

.chart-legend-item {
        -fx-font-size: 16px;
}

.chart-legend-item-symbol {
        -fx-shape: "M0 -3.5 v7 l 4 -3.5z";
}

Note

本章中的许多例子使用外部资源,如 CSS 文件。您需要确保ResourceUtil类指向resources目录(随书提供的源代码包的一部分)。从 www.apress.com/source-code 下载源码。

图表示例中使用的数据

我将很快讨论不同类型的图表。图表将使用表 20-1 中的数据,其中有世界上一些国家的实际和估计人口。数据摘自联合国在 www.un.org 发表的报告。人口数值已经四舍五入。

表 20-1

世界上一些国家目前和估计的人口(百万)

|   |

One thousand nine hundred and fifty

|

Two thousand

|

Two thousand and fifty

|

Two thousand one hundred

|

Two thousand one hundred and fifty

|

Two thousand two hundred

|

Two thousand two hundred and fifty

|

Two thousand three hundred

中国 Five hundred and fifty-five One thousand two hundred and seventy-five One thousand three hundred and ninety-five One thousand one hundred and eighty-two One thousand one hundred and forty-nine One thousand two hundred and one One thousand two hundred and forty-seven One thousand two hundred and eighty-five
印度 Three hundred and fifty-eight One thousand and seventeen One thousand five hundred and thirty-one One thousand four hundred and fifty-eight One thousand three hundred and eight One thousand three hundred and four One thousand three hundred and forty-two One thousand three hundred and seventy-two
巴西 Fifty-four One hundred and seventy-two Two hundred and thirty-three Two hundred and twelve Two hundred and two Two hundred and eight Two hundred and sixteen Two hundred and twenty-three
英国 Fifty Fifty-nine Sixty-six Sixty-four Sixty-six sixty-nine Seventy-one Seventy-three
美国 One hundred and fifty-eight Two hundred and eighty-five Four hundred and nine Four hundred and thirty-seven Four hundred and fifty-three Four hundred and seventy Four hundred and eighty-three Four hundred and ninety-three

了解饼图

饼图由一个圆组成,该圆被分成不同圆心角的扇形。通常,馅饼是圆形的。扇区也被称为饼图块饼图片。圆圈中的每个扇形代表某种数量。扇形面积的圆心角与它所代表的量成正比。图 20-2 显示了五个国家在 2000 年的人口饼图。

img/336502_2_En_20_Fig2_HTML.jpg

图 20-2

显示 2000 年五个国家人口的饼图

PieChart类的一个实例代表一个饼图。该类包含两个构造器:

  • PieChart()

  • PieChart(ObservableList<PieChart.Data> data)

无参数构造器创建一个没有内容的饼图。您可以稍后使用其data属性添加内容。第二个构造器创建一个以指定数据为内容的饼图。

// Create an empty pie chart
PieChart chart = new PieChart();

饼图中的一个扇区被指定为PieChart.Data类的一个实例。一个切片有一个名称(或标签)和一个饼图值,分别由PieChart.Data类的namepieValue属性表示。以下语句为饼图创建一个切片。切片名称为“China”,饼图值为 1275:

PieChart.Data chinaSlice = new PieChart.Data("China", 1275);

饼图的内容(所有切片)在ObservableList<PieChart.Data>中指定。下面的代码片段创建了一个ObservableList<PieChart.Data>,并向其中添加了三个饼图切片:

ObservableList<PieChart.Data> chartData = FXCollections.observableArrayList();
chartData.add(new PieChart.Data("China", 1275));
chartData.add(new PieChart.Data("India", 1017));
chartData.add(new PieChart.Data("Brazil", 172));

现在,您可以使用第二个构造器通过指定图表内容来创建饼图:

// Create a pie chart with content
PieChart charts = new PieChart(chartData);

您将使用 2050 年不同国家的人口作为我们所有饼图的数据。清单 20-1 包含一个实用程序类。它的getChartData()方法返回一个PieChart.DataObservableList作为饼图的数据。您将在本节的示例中使用这个类。

// PieChartUtil.java
package com.jdojo.chart;

import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.chart.PieChart;

public class PieChartUtil {
        public static ObservableList<PieChart.Data> getChartData() {
               ObservableList<PieChart.Data> data =
                        FXCollections. observableArrayList();
               data.add(new PieChart.Data("China", 1275));
               data.add(new PieChart.Data("India", 1017));
               data.add(new PieChart.Data("Brazil", 172));
               data.add(new PieChart.Data("UK", 59));
               data.add(new PieChart.Data("USA", 285));
               return data;
        }
}

Listing 20-1A Utility Class to Generate Data for Pie Charts

PieChart类包含几个属性:

  • data

  • startAngle

  • clockwise

  • labelsVisible

  • labelLineLength

data属性指定了ObservableList<PieChart.Data>.中图表的内容

startAngle属性指定第一个饼图扇区开始的角度,以度为单位。默认情况下,它是零度,对应于三点钟的位置。逆时针测量为正startAngle。例如,90 度的startAngle将从 12 点钟位置开始。

clockwise属性指定切片是否从startAngle开始顺时针放置。默认情况下,这是真的。

labelsVisible属性指定切片的标签是否可见。切片的标签显示在切片附*,并放置在切片外部。使用PieChart.Data类的 name 属性指定切片的标签。在图 20-2 中,“中国”、“印度”、“巴西”等。是切片的标签。

标签和切片通过直线连接。属性指定了这些线的长度。其默认值为 20.0 像素。

清单 20-2 中的程序使用饼图显示五个国家在 2000 年的人口。该程序创建一个空的饼图并设置其标题。图例位于左侧。之后,它为图表设置数据。数据是在getChartData()方法中生成的,该方法返回一个ObservableList<PieChart.Data>,其中包含作为饼图扇区标签的国家名称和作为饼图值的人口。程序显示如图 20-2 所示的窗口。

// PieChartTest.java
package com.jdojo.chart;

import javafx.application.Application;
import javafx.collections.ObservableList;
import javafx.geometry.Side;
import javafx.scene.Scene;
import javafx.scene.chart.PieChart;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

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

        @Override
        public void start(Stage stage) {
               PieChart chart = new PieChart();
               chart.setTitle("Population in 2000");

               // Place the legend on the left side
               chart.setLegendSide(Side.LEFT);

               // Set the data for the chart
               ObservableList<PieChart.Data> chartData =
                        PieChartUtil.getChartData();
               chart.setData(chartData);

               StackPane root = new StackPane(chart);
               Scene scene = new Scene(root);

               stage.setScene(scene);
               stage.setTitle("A Pie Chart");
               stage.show();
        }
}

Listing 20-2Using the PieChart Class to Create a Pie Chart

自定义饼图扇区

每个饼图切片数据由一个Node表示。使用PieChart.Data类的getNode()方法可以获得对Node的引用。将切片添加到饼图时,会创建Node。因此,在将切片添加到图表中之后,必须对代表切片的PieChart.Data调用getNode()方法。否则返回null。清单 20-3 中的程序定制了一个饼图的所有饼图扇区,为它们添加了一个工具提示。工具提示显示切片名称、饼图值和百分比饼图值。addSliceTooltip()方法包含访问切片Nodes和添加工具提示的逻辑。您可以自定义饼图切片来制作动画,让用户使用鼠标将它们从饼图中拖出,等等。见图 20-3 。

img/336502_2_En_20_Fig3_HTML.jpg

图 20-3

显示工具提示及其饼图值和占总饼图百分比的饼图扇区

// PieSliceTest.java
// ...find in the book's download area.

Listing 20-3Adding Tool Tips to Pie Slices

用 CSS 样式化饼图

除了在PieChart类中定义的data属性之外,所有属性都可以使用 CSS 进行样式化,如下所示:

.chart {
        -fx-clockwise: false;
        -fx-pie-label-visible: true;
        -fx-label-line-length: 10;
        -fx-start-angle: 90;
}

添加到饼图中的每个饼图扇区都添加了四个样式类:

  • chart-pie

  • data<i>

  • default-color<j>

  • negative

样式类名data<i>中的<i>是切片索引。第一片有阶级data0,第二片data1,第三片data2等等。

样式类名default-color<j>中的<j>是该系列的颜色索引。在饼图中,您可以将每个切片视为一个系列。默认 CSS (Modena.css)定义了八种系列颜色。如果您的饼图扇区超过八个扇区,扇区颜色将会重复。当我在下一节讨论双轴图表时,图表中系列的概念会更加明显。

只有当切片的数据为负时,才会添加样式类。

如果您希望将样式应用于所有饼图扇区,请为图表饼图样式类名称定义样式。以下样式将为所有饼图切片设置一个带有 2px 背景插入的白色边框。当您设置 2px 插入时,它将在两个切片之间显示更宽的间隙:

.chart-pie {
        -fx-border-color: white;
        -fx-background-insets: 2;
}

您可以使用以下样式定义饼图扇区的颜色。它只为五个切片定义颜色。超过第六个的切片将使用默认颜色:

.chart-pie.default-color0 {-fx-pie-color: red;}
.chart-pie.default-color1 {-fx-pie-color: green;}
.chart-pie.default-color2 {-fx-pie-color: blue;}
.chart-pie.default-color3 {-fx-pie-color: yellow;}
.chart-pie.default-color4 {-fx-pie-color: tan;}

使用超过八种系列颜色

一个图表中很可能有八个以上的系列(饼图中的切片),并且您不希望这些系列的颜色重复。我们将针对饼图讨论这种技术。但是,它也可以用于双轴图表。

假设您想使用一个显示十个国家人口的饼图。如果您使用此饼图的代码,第九个和第十个扇区的颜色将分别与第一个和第二个扇区的颜色相同。首先,您需要定义第九个和第十个切片的颜色,如清单 20-4 所示。

/* additional_series_colors.css */
.chart-pie.default-color8 {
        -fx-pie-color: gold;
}

.chart-pie.default-color9 {
        -fx-pie-color: khaki;
}

Listing 20-4Additional Series Colors

饼图扇区和图例符号将被分配样式类名称,如default-color0default-color2default-color7。您需要识别与索引大于 7 的数据项相关联的切片和图例符号的节点,并用新的名称替换它们的default-color<j>样式类名称。例如,对于第九个和第十个切片,样式类名称是default-color0default-color1,因为颜色系列号被指定为(dataIndex % 8))。你将用default-color9default-color10来代替它们。

清单 20-5 中的程序显示了如何改变切片和图例符号的颜色。它向饼图添加十个扇区。setSeriesColorStyles()方法替换第九个和第十个切片的切片节点的样式类名称及其相关的图例符号。图 20-4 为饼状图。注意“德国”和“印度尼西亚”的颜色是 CSS 中设置的金色和卡其色。注释start()方法中的最后一条语句,它是对setSeriesColorStyles()的调用,您会发现“德国”和“印度尼西亚”的颜色与“中国”和“印度”的颜色相同

img/336502_2_En_20_Fig4_HTML.jpg

图 20-4

使用超过八种切片颜色的饼图

// PieChartExtraColor.java
// ...find in the book's download area.

Listing 20-5A Pie Chart Using Color Series Up to Index 10

为饼图扇区使用背景图像

您也可以在饼图切片中使用背景图像。以下样式定义了第一个饼图扇区的背景图像:

.chart-pie.data0 {
        -fx-background-image: url("china_flag.jpg");
}

清单 20-6 包含一个名为 pie_slice.css 的 CSS 文件的内容。它定义了指定用于饼图扇区的背景图像、图例符号的首选大小以及连接饼图扇区及其标签的线的长度的样式。

// pie_slice.css
/* Set a background image for pie slices */
.chart-pie.data0 {-fx-background-image: url("china_flag.jpg");}
.chart-pie.data1 {-fx-background-image: url("india_flag.jpg");}
.chart-pie.data2 {-fx-background-image: url("brazil_flag.jpg");}
.chart-pie.data3 {-fx-background-image: url("uk_flag.jpg");}
.chart-pie.data4 {-fx-background-image: url("usa_flag.jpg");}

/* Set the preferred size for legend symbols */
.chart-legend-item-symbol {
        -fx-pref-width: 100;
        -fx-pref-height: 30;
}

.chart {
        -fx-label-line-length: 10;
}

Listing 20-6 A CSS for Customizing Pie Slices

清单 20-7 中的程序创建了一个饼图。它使用的数据与您在前面的示例中使用的数据相同。不同的是,它设置了一个在 pie_slice.css 文件中定义的 CSS:

// Set a CSS for the scene
scene.getStylesheets().addAll("resources/css/pie_slice.css");

产生的窗口如图 20-5 所示。请注意,切片和图例符号显示了国家的国旗。请记住,您已经将图表数据的索引和 CSS 文件中的索引进行了匹配,以匹配国家及其国旗,这一点很重要。

img/336502_2_En_20_Fig5_HTML.jpg

图 20-5

使用背景图像作为其切片的饼图

Tip

还可以设置饼图中连接饼图扇区及其标签、饼图扇区标签和图例符号的线条形状的样式。

// PieChartCustomSlice.java
// ...find in the book's download area.

Listing 20-7Using Pie Slices with a Background Image

了解 xy 图表

抽象类XYChart<X,Y>的具体子类的实例定义了一个双轴图表。通用类型参数XY分别是沿 x 轴和 y 轴绘制的值的数据类型。

在 xy 图表中表示坐标轴

抽象类Axis<T>的具体子类的一个实例定义了XYChart中的一个轴。图 20-6 显示了代表轴的类的类图。

img/336502_2_En_20_Fig6_HTML.png

图 20-6

在 xy 图表中表示轴的类的类图

抽象的Axis<T>类是所有表示轴的类的基类。通用参数T是沿轴绘制的值的类型,例如StringNumber等。坐标轴显示刻度和刻度标签。Axis<T>类包含定制刻度和刻度标签的属性。一个轴可以有一个标签,在label属性中指定。

具体的子类CategoryAxisNumberAxis分别用于绘制沿轴的StringNumber数据值。它们包含特定于数据值的属性。例如,NumberAxis继承了ValueAxis<T>lowerBoundupperBound属性,它们指定了绘制在轴上的数据的下限和上限。默认情况下,坐标轴上的数据范围是根据数据自动确定的。您可以通过将Axis<T>类中的autoRanging属性设置为 false 来关闭这个特性。以下代码片段创建了CategoryAxisNumberAxis的实例,并设置了它们的标签:

CategoryAxis xAxis = new CategoryAxis();
xAxis.setLabel("Country");
NumberAxis yAxis = new NumberAxis();
yAxis.setLabel("Population (in millions)");

Tip

使用CategoryAxis沿轴绘制String值,使用NumberAxis沿轴绘制数值。

向 xy 图表添加数据

XYChart中的数据代表由 x 轴和 y 轴定义的 2D *面中的点。使用 x 和 y 坐标来指定 2D *面中的点,这两个坐标分别是沿 x 轴和 y 轴的值。XYChart中的数据被指定为命名序列的ObservableList。一个序列由多个数据项组成,这些数据项是 2D *面上的点。点的呈现方式取决于图表类型。例如,散点图显示一个点的符号,而条形图显示一个点的条形。

嵌套的静态XYChart.Data<X,Y>类的一个实例代表一个系列中的一个数据项。类别会定义下列属性:

  • XValue

  • YValue

  • extraValue

  • node

XValueYValue分别是数据项沿 x 轴和 y 轴的值。它们的数据类型需要与图表的 x 轴和 y 轴的数据类型相匹配。extraValue是一个Object,可以用来存储数据项的任何附加信息。其用途取决于图表类型。如果图表不使用该值,您可以将它用于任何其他目的:例如,存储数据项的工具提示值。node为图表中的数据项指定要呈现的节点。默认情况下,图表将根据图表类型创建合适的节点。

假设一个XYChart的两个轴都绘制数值。以下代码片段为图表创建了一些数据项。数据项是 1950 年、2000 年和 2050 年的中国人口:

XYChart.Data<Number, Number> data1 = new XYChart.Data<>(1950, 555);
XYChart.Data<Number, Number> data2 = new XYChart.Data<>(2000, 1275);
XYChart.Data<Number, Number> data3 = new XYChart.Data<>(2050, 1395);

嵌套静态XYChart.Series<X,Y>类的一个实例代表一系列数据项。类别会定义下列属性:

  • name

  • data

  • chart

  • node

name是系列的名称。dataXYChart.Data<X,Y>的一个ObservableListchart是对系列所属图表的只读引用。node是该系列要显示的Node。默认节点是根据图表类型自动创建的。以下代码片段创建一个系列,设置其名称,并向其中添加数据项:

XYChart.Series<Number, Number> seriesChina = new XYChart.Series<>();
seriesChina.setName("China");
seriesChina.getData().addAll(data1, data2, data3);

XYChart类的data属性代表图表的数据。它是一架XYChart.Series级的ObservableList。假设数据系列seriesIndiaseriesUSA存在,以下代码片段为XYChart图表创建并添加数据:

XYChart<Number, Number> chart = ...
chart.getData().addAll(seriesChina, seriesIndia, seriesUSA);

系列数据项的显示方式取决于特定的图表类型。每种图表类型都有区分不同系列的方法。

您将多次重用代表某些国家某些年份人口的同一系列数据项。清单 20-8 有一个实用程序类的代码。该类由两个静态方法组成,它们生成并返回XYChart数据。getCountrySeries()方法返回系列列表,该列表沿 x 轴绘制年份,沿 y 轴绘制相应的人口。getYearSeries()方法返回一个系列列表,该列表沿 x 轴绘制国家,沿 y 轴绘制相应的人口。在后面的章节中,您将调用这些方法来为我们的XYCharts获取数据。

// XYChartDataUtil.java
// ...abbreviated, find the full listing in the book's download area.
package com.jdojo.chart;

import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.chart.XYChart;

@SuppressWarnings("unchecked")
public class XYChartDataUtil {
    public static
    ObservableList<XYChart.Series<Number, Number>> getCountrySeries() {
        XYChart.Series<Number, Number> seriesChina =
            new XYChart.Series<>();
        seriesChina.setName("China");
        seriesChina.getData().addAll(
            new XYChart.Data<>(1950, 555),
            new XYChart.Data<>(2000, 1275),
            ...
        );

        ...
        ObservableList<XYChart.Series<Number, Number>> data =
          FXCollections.<XYChart.Series<Number, Number>>observableArrayList();
        data.addAll(seriesChina, seriesIndia, seriesUSA);
        return data;
    }

    ...
}

Listing 20-8A Utility Class to Generate Data Used in XYCharts

了解条形图

条形图将数据项呈现为水*或垂直的矩形条。条形的长度与数据项的值成比例。

BarChart类的一个实例代表一个条形图。在条形图中,一个轴必须是CategoryAxis,另一个轴必须是ValueAxis / NumberAxis。根据CategoryAxis是 x 轴还是 y 轴,垂直或水*绘制条形。

BarChart包含两个属性来控制一个类别中两个条形之间的距离和两个类别之间的距离:

  • barGap

  • categoryGap

默认值是barGap的 4 个像素和categoryGap的 10 个像素。

BarChart类包含三个构造器,通过指定轴、数据和两个类别之间的间距来创建条形图:

  • BarChart(Axis<X> xAxis, Axis<Y> yAxis)

  • BarChart(Axis<X> xAxis, Axis<Y> yAxis, ObservableList<XYChart.Series<X,Y>> data)

  • BarChart(Axis<X> xAxis, Axis<Y> yAxis, ObservableList<XYChart.Series<X,Y>> data, double categoryGap)

请注意,在创建条形图时,必须至少指定轴。以下代码片段创建了两个轴和一个包含这些轴的条形图:

CategoryAxis xAxis = new CategoryAxis();
xAxis.setLabel("Country");

NumberAxis yAxis = new NumberAxis();
yAxis.setLabel("Population (in millions)");

// Create a bar chart
BarChart<String, Number> chart = new BarChart<>(xAxis, yAxis);

当类别轴作为 x 轴添加时,图表中的条形将垂直显示。您可以使用其setData()方法用数据填充图表:

// Set the data for the chart
chart.setData(XYChartDataUtil.getYearSeries());

清单 20-9 中的程序显示了如何创建和填充如图 20-7 所示的垂直条形图。

img/336502_2_En_20_Fig7_HTML.jpg

图 20-7

垂直条形图

// VerticalBarChart.java
// ...find in the book's download area.

Listing 20-9Creating a Vertical Bar Chart

清单 20-10 中的程序显示了如何创建和填充如图 20-8 所示的水*条形图。程序需要在XYChart.Series<Number,String>ObservableList中向图表提供数据。XYChartDataUtil类中的getYearSeries()方法返回XYChart.Series<String,Number>。程序中的getChartData()方法根据需要将系列数据从<String,Number>转换为<Number,String>格式,以创建水*条形图。

img/336502_2_En_20_Fig8_HTML.jpg

图 20-8

水*条形图

// HorizontalBarChart.java
// ...find in the book's download area.

Listing 20-10Creating a Horizontal Bar Chart

Tip

条形图中的每个条形都用一个节点表示。通过向表示数据项的节点添加事件处理程序,用户可以与条形图中的条形进行交互。请参考饼图上的部分,查看为饼图扇区添加工具提示的示例。

用 CSS 设计条形图的样式

默认情况下,BarChart被赋予样式类名称:图表条形图

以下样式将所有条形图的barGapcategoryGap属性的默认值设置为 0px 和 20px。同一类别中的条形将彼此相邻放置:

.bar-chart {
        -fx-bar-gap: 0;
        -fx-category-gap: 20;
}

您可以为每个系列或系列中的每个数据项自定义条形图的外观。BarChart中的每个数据项由一个节点表示。该节点获得五个默认的样式类名称:

  • chart-bar

  • series<i>

  • data<j>

  • default-color<k>

  • negative

series<i>中,<i>是数列指标。例如,第一个系列的样式类名称为series0,第二个系列的样式类名称为series1,依此类推。

data<j>中,<j>是数据项在一个序列中的索引。例如,每个系列中的第一个数据项的样式类名称为data0,第二个为data1,依此类推。

default-color<k>中,<k>是系列颜色索引。例如,第一个系列中的每个数据项将获得一个样式类名称default-color0,第二个系列中的每个数据项将获得一个样式类名称default-color1,依此类推。默认的 CS 仅定义八种系列颜色。<k>的值等于(i%8,其中i是序列索引。也就是说,如果条形图中有八个以上的系列,系列颜色将会重复。请参考饼图部分,了解如何对指数大于 8 的系列使用独特的颜色。逻辑将类似于饼图中使用的逻辑,不同之处在于,这一次,您将在一个系列中查找bar-legend-symbol,而不是pie-legend-symbol

如果数据值为负,则添加negative类。

条形图中的每个图例项都有以下样式类名称:

  • chart-bar

  • series<i>

  • bar-legend-symbol

  • default-color<j>

series<i>中,<i>是数列指标。在default-color<j>中,<j>是系列的颜色指数。如果系列数超过八个,图例颜色将重复,就像条形颜色一样。

以下样式定义了序列索引为 0、8、16、24 等的所有数据项的条形颜色。,为蓝色:

.chart-bar.default-color0 {
        -fx-bar-fill: blue;
}

了解堆积条形图

堆积条形图是条形图的变体。在堆积条形图中,类别中的条形是堆积的。除了条形的位置之外,它的工作方式与条形图相同。

StackedBarChart类的一个实例表示一个堆积条形图。这些条可以水*或垂直放置。如果 x 轴是一个CategoryAxis,则条形垂直放置。否则,它们会水*放置。和BarChart一样,其中一个轴必须是CategoryAxis,另一个必须是ValueAxis / NumberAxis

StackedBarChart类包含一个categoryGap属性,该属性定义相邻类别中条形之间的间距。默认间隙为 10px。与BarChart类不同,StackedBarChart类不包含barGap属性,因为一个类别中的条总是堆叠的。

StackedBarChart类的构造器类似于BarChart类的构造器。它们允许您指定坐标轴、图表数据和类别间距。

BarChartStackedBarChart创建CategoryAxis有一个显著的区别。BarChart从数据中读取类别值,而您必须显式地将所有类别值添加到StackedBarChartCategoryAxis中:

CategoryAxis xAxis = new CategoryAxis();
xAxis.setLabel("Country");

// Must set the categories in a StackedBarChart explicitly. Otherwise,
// the chart will not show bars.
xAxis.getCategories().addAll("China," "India," "Brazil," "UK," "USA");

NumberAxis yAxis = new NumberAxis();
yAxis.setLabel("Population (in millions)");

StackedBarChart<String, Number> chart = new StackedBarChart<>(xAxis, yAxis);

清单 20-11 中的程序展示了如何创建一个垂直堆积条形图。图表如图 20-9 所示。要创建水*堆积条形图,使用CategoryAxis作为 y 轴。

img/336502_2_En_20_Fig9_HTML.jpg

图 20-9

垂直堆积条形图

// VerticalStackedBarChart.java
// ...find in the book's download area.

Listing 20-11Creating a Vertical Stacked Bar Chart

用 CSS 样式化 StackedBarChart

默认情况下,StackedBarChart被赋予样式类名称:图表堆积条形图

以下样式将所有堆积条形图的categoryGap属性的默认值设置为 20px。类别中的条形将彼此相邻放置:

.stacked-bar-chart {
        -fx-category-gap: 20;
}

在堆积条形图中,分配给表示条形图和图例项的节点的样式类名称与条形图的名称相同。更多细节请参考章节"用 CSS 样式化条形图"

了解散点图

条形图将数据项呈现为符号。一个序列中的所有数据项都使用相同的符号。数据项符号的位置由数据项沿 x 轴和 y 轴的值决定。

ScatterChart类的一个实例表示一个散点图。x 轴和 y 轴可以使用任何类型的Axis。该类不定义任何附加属性。它包含允许您通过指定轴和数据来创建散点图的构造器:

  • ScatterChart(Axis<X> xAxis, Axis<Y> yAxis)

  • ScatterChart(Axis<X> xAxis, Axis<Y> yAxis, ObservableList<XYChart.Series<X,Y>> data)

回想一下,默认情况下,AxisautoRanging被设置为 true。如果您在散点图中使用数值,请确保将autoRanging设置为 false。适当设置数值范围以在图表中获得均匀分布的点是很重要的。否则,这些点可能密集地分布在一个小区域内,很难看清图表。

清单 20-12 中的程序展示了如何创建和填充如图 20-10 所示的散点图。两个轴都是数字轴。x 轴是定制的。自动排列被设置为 false 设定合理的下限和上限。刻度单位设置为 50。如果您不自定义这些属性,ScatterChart将自动确定它们,并且图表数据将难以读取:

NumberAxis xAxis = new NumberAxis();
xAxis.setLabel("Year");
xAxis.setAutoRanging(false);
xAxis.setLowerBound(1900);
xAxis.setUpperBound(2300);
xAxis.setTickUnit(50);

img/336502_2_En_20_Fig10_HTML.jpg

图 20-10

散点图

// ScatterChartTest.java
package com.jdojo.chart;

import javafx.application.Application;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.ScatterChart;
import javafx.scene.chart.XYChart;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

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

        @Override

        public void start(Stage stage) {
               NumberAxis xAxis = new NumberAxis();
               xAxis.setLabel("Year");

               // Customize the x-axis, so points are scattered uniformly
               xAxis.setAutoRanging(false);
               xAxis.setLowerBound(1900);
               xAxis.setUpperBound(2300);
               xAxis.setTickUnit(50);

               NumberAxis yAxis = new NumberAxis();
               yAxis.setLabel("Population (in millions)");

               ScatterChart<Number,Number> chart =
                        new ScatterChart<>(xAxis, yAxis);
               chart.setTitle("Population by Year and Country");

               // Set the data for the chart
               ObservableList<XYChart.Series<Number,Number>> chartData =
                   XYChartDataUtil.getCountrySeries();
               chart.setData(chartData);

               StackPane root = new StackPane(chart);
               Scene scene = new Scene(root);
               stage.setScene(scene);
               stage.setTitle("A Scatter Chart");
               stage.show();
        }
}

Listing 20-12Creating a Scatter Chart

Tip

您可以使用数据项的node属性来指定ScatterChart中的符号。

用 CSS 设计散点图的样式

除了图表之外,ScatterChart没有被赋予任何额外的样式类名称。

您可以自定义每个系列或系列中每个数据项的符号外观。ScatterChart中的每个数据项由一个节点表示。该节点获得五个默认的样式类名称:

  • chart-symbol

  • series<i>

  • data<j>

  • default-color<k>

  • negative

关于这些样式类名称中的<i><j><k>的含义,请参考“用 CSS 样式化条形图”一节。

散点图中的每个图例项都有以下样式类名称:

  • chart-symbol

  • series<i>

  • data<j>

  • default-color<k>

以下样式将第一个系列中的数据项显示为填充蓝色的三角形。注意,仅定义了八个颜色系列。之后,颜色会按照饼图中详细讨论的内容重复出现。

.chart-symbol.default-color0 {
        -fx-background-color: blue;
        -fx-shape: "M5, 0L10, 5L0, 5z";
}

了解折线图

折线图通过用线段连接数据项来显示一系列数据项。可选地,数据点本身可以由符号表示。您可以将折线图视为散点图,其中的符号由直线段连接成一系列。通常,折线图用于查看一段时间内或一个类别中的数据变化趋势。

LineChart类的一个实例代表一个折线图。该类包含一个createSymbols属性,默认设置为 true。它控制是否为数据点创建符号。将其设置为 false 将只显示连接系列中数据点的直线。

LineChart类包含两个通过指定轴和数据来创建折线图的构造器:

  • LineChart(Axis<X> xAxis, Axis<Y> yAxis)

  • LineChart(Axis<X> xAxis, Axis<Y> yAxis, ObservableList<XYChart.Series<X,Y>> data)

清单 20-13 中的程序显示了如何创建和填充如图 20-11 所示的折线图。该程序与使用散点图的程序相同,只是它使用了LineChart类。图表将圆圈显示为数据项的符号。创建折线图后,可以使用以下语句删除符号:

img/336502_2_En_20_Fig11_HTML.jpg

图 20-11

折线图

// LineChartTest.java
// ...find in the book's download area.

Listing 20-13Creating a Line Chart

// Do not create the symbols for the data items
chart.setCreateSymbols(false);

用 CSS 设计折线图的样式

除了图表之外,LineChart没有被赋予任何额外的样式类名称。下面的样式指定LineChart不应该创建符号:

.chart {
        -fx-create-symbols: false;
}

LineChart创建一个Path节点来显示连接一个系列的所有数据点的线条。为系列的线条分配以下样式类名称:

  • chart-series-line

  • series<i>

  • default-color<j>

这里,<i>是系列索引,<j>是系列的颜色索引。

如果createSymbols属性设置为 true,则为每个数据点创建一个符号。每个符号节点都被指定了以下样式类名称:

  • chart-line-symbol

  • series<i>

  • data<j>

  • default-color<k>

这里,<i>是系列索引,<j>是系列内的数据项索引,<k>是系列的颜色索引。

每个系列都分配有一个图例项,该图例项获得以下样式类名称:

  • chart-line-symbol

  • series<i>

  • default-color<j>

以下样式将系列颜色索引 0 的线条笔触设置为蓝色。该系列的符号也以蓝色显示:

.chart-series-line.default-color0 {
        -fx-stroke: blue;
}

.chart-line-symbol.default-color0 {
        -fx-background-color: blue, white;
}

了解 BubbleChart

气泡图与散点图非常相似,只是它能够表示一个数据点的三个值。气泡用于表示一个系列中的一个数据项。您可以设置气泡的半径来表示数据点的第三个值。

BubbleChart类的一个实例代表一个气泡图。该类不定义任何新属性。气泡图使用XYChart.Data类的extraValue属性来获取气泡的半径。气泡是一个椭圆,其半径根据轴使用的比例进行缩放。如果 x 轴和 y 轴的比例几乎相等,气泡看起来更像一个圆(或在一个方向上拉伸得更少)。

Tip

默认情况下设置气泡半径,并使用轴的比例因子进行缩放。如果轴的比例因子非常小,您可能看不到气泡。要查看气泡,请将数据项中的extraValue设置为较高的值,或者沿轴使用较高的比例因子。

BubbleChart类定义了两个构造器:

  • BubbleChart(Axis<X> xAxis, Axis<Y> yAxis)

  • BubbleChart(Axis<X> xAxis, Axis<Y> yAxis, ObservableList<XYChart.Series<X,Y>> data)

清单 20-14 中的程序展示了如何创建如图 20-12 所示的气泡图。图表数据被传递给setBubbleRadius()方法,该方法将所有数据点的extraValue显式设置为 20px。如果你想用气泡的半径来表示另一个维度的数据,你可以相应地设置extraValue

img/336502_2_En_20_Fig12_HTML.jpg

图 20-12

气泡图

// BubbleChartTest.java
// ...find in the book's download area.

Listing 20-14Creating a Bubble Chart

用 CSS 样式化 BubbleChart

除了图表之外,BubbleChart没有被赋予任何额外的样式类名称。

您可以为每个系列或系列中的每个数据项自定义气泡的外观。BubbleChart中的每个数据项由一个节点表示。该节点获得四个默认的样式类名称:

  • chart-bubble

  • series<i>

  • data<j>

  • default-color<k>

这里,<i>是系列索引,<j>是系列内的数据项索引,<k>是系列的颜色索引。

每个系列都分配有一个图例项,图例项具有以下样式类名称:

  • chart-bubble

  • series<i>

  • bubble-legend-symbol

  • default-color<k>

这里,<i><k>的含义与前面描述的相同。

以下样式将系列颜色索引 0 的填充颜色设置为蓝色。第一个系列中数据项的气泡和图例符号将以蓝色显示。对于系列索引 8、16、24 等,颜色会重复出现。

.chart-bubble.default-color0 {
        -fx-bubble-fill: blue;
}

了解面积图

面积图是折线图的变体。它绘制连接系列中所有数据项的线条,此外,还填充线条和 x 轴之间的绘制区域。不同的颜色用于绘制不同系列的区域。

AreaChart的一个实例代表一个面积图。像LineChart类一样,该类包含一个createSymbols属性来控制是否在数据点绘制符号。默认情况下,它被设置为 true。该类包含两个构造器:

  • AreaChart(Axis<X> xAxis, Axis<Y> yAxis)

  • AreaChart(Axis<X> xAxis, Axis<Y> yAxis, ObservableList<XYChart.Series<X,Y>> data)

清单 20-15 中的程序显示了如何创建如图 20-13 所示的面积图。程序中没有什么新东西,除了您已经使用了AreaChart类来创建图表。请注意,一个系列的区域覆盖了前一个系列的区域。

img/336502_2_En_20_Fig13_HTML.jpg

图 20-13

面积图

// AreaChartTest.java
// ...find in the book's download area.

Listing 20-15Creating an Area Chart

用 CSS 样式化面积图

除了图表之外,AreaChart没有被赋予任何额外的样式类名称。下面的样式指定AreaChart不应该创建表示数据点的符号:

.chart {
        -fx-create-symbols: false;
}

一个AreaChart中的每个序列由一个包含两个Path节点的Group表示。一个Path代表连接系列中所有数据点的线段,另一个Path代表系列覆盖的区域。代表系列线段的Path节点被赋予以下样式类名称:

  • chart-series-area-line

  • series<i>

  • default-color<j>

这里,<i>是系列索引,<j>是系列的颜色索引。

代表系列区域的Path节点被赋予以下样式类名称:

  • chart-series-area-fill

  • series<i>

  • default-color<j>

这里,<i>是系列索引,<j>是系列的颜色索引。

如果createSymbols属性设置为 true,则为每个数据点创建一个符号。每个符号节点都被指定了以下样式类名称:

  • chart-area-symbol

  • series<i>

  • data<j>

  • default-color<k>

这里,<i>是系列索引,<j>是系列内的数据项索引,<k>是系列的颜色索引。

每个系列都分配有一个图例项,该图例项获得以下样式类名称:

  • chart-area-symbol

  • series<i>

  • area-legend-symbol

  • default-color<j>

这里,<i>是系列索引,<j>是系列的颜色索引。

以下样式将系列颜色索引 0 的区域填充颜色设置为不透明度为 20%的蓝色。确保为区域填充设置透明颜色,因为区域在AreaChart中重叠:

.chart-series-area-fill.default-color0 {
        -fx-fill: rgba(0, 0, 255, 0.20);
}

以下样式将蓝色设置为系列颜色索引 0 的符号、线段和图例符号的颜色:

/* Data point symbols color */
.chart-area-symbol.default-color0\. {
        -fx-background-color: blue, white;
}

/* Series line segment color */
.chart-series-area-line.default-color0 {
        -fx-stroke: blue;
}

/* Series legend symbol color */
.area-legend-symbol.default-color0 {
        -fx-background-color: blue, white;
}

了解堆叠面积图

堆积面积图是面积图的变体。它通过为每个系列绘制一个区域来绘制数据项。与面积图不同,系列的面积不会重叠;它们堆叠在一起。

StackedAreaChart的一个实例代表一个堆积面积图。像AreaChart类一样,该类包含一个createSymbols属性。该类包含两个构造器:

  • StackedAreaChart (Axis<X> xAxis, Axis<Y> yAxis)

  • StackedAreaChart(Axis<X> xAxis, Axis<Y> yAxis, ObservableList<XYChart.Series<X,Y>> data)

清单 20-16 中的程序展示了如何创建如图 20-14 所示的堆积面积图。该程序与创建一个AreaChart的程序相同,除了您已经使用了StackedAreaChart类来创建图表。

img/336502_2_En_20_Fig14_HTML.jpg

图 20-14

堆积面积图

// StackedAreaChartTest.java
// ...find in the book's download area.

Listing 20-16Creating a Stacked Area Chart

用 CSS 样式化 StackedAreaChart

设计一个StackedAreaChart和设计一个AreaChart是一样的。更多细节请参考“用 CSS 样式化区域图”一节。

自定义 XYChart 外观

您已经看到了如何应用特定于图表的 CSS 样式来自定义图表的外观。在本节中,您将看到更多定制XYChart图和轴的方法。XYChart类包含几个布尔属性来改变图表绘图的外观:

  • alternativeColumnFillVisible

  • alternativeRowFillVisible

  • horizontalGridLinesVisible

  • verticalGridLinesVisible

  • horizontalZeroLineVisible

  • verticalZeroLineVisible

图表区分为由列和行组成的网格。穿过 y 轴上的主要刻度绘制水*线,组成行。穿过构成列的 x 轴上的主要记号绘制垂直线。

设置交替行/列填充

alternativeColumnFillVisiblealternativeRowFillVisible控制是否填充网格中的交替列和行。默认情况下,alternativeColumnFillVisible设置为假,alternativeRowFillVisible设置为真。

在撰写本文时,设置alternativeColumnFillVisiblealternativeRowFillVisible属性在 JavaFX 8 中没有任何效果,Java FX 8 默认使用 Modena CSS。有两种解决方案。您可以使用以下语句将 Caspian CSS 用于您的应用程序:

Application.setUserAgentStylesheet(Application.STYLESHEET_CASPIAN);

另一个解决方案是在您的应用程序 CSS 中包含以下样式:

.chart-alternative-column-fill {
        -fx-fill: #eeeeee;
        -fx-stroke: transparent;
        -fx-stroke-width: 0;
}

.chart-alternative-row-fill {
        -fx-fill: #eeeeee;
        -fx-stroke: transparent;
        -fx-stroke-width: 0;
}

这些样式取自里海 CSS。这些样式将 Modena CSS 中的fillstroke属性设置为null

显示零线轴

图表的坐标轴不能包含零线。是否包含零线取决于轴所代表的下限和上限。horizontalZeroLineVisibleverticalZeroLineVisible控制零线是否可见。默认情况下,它们是可见的。请注意,轴的零线仅在轴有正负数据要绘制时才可见。如果 y 轴上有负值和正值,则会出现一个额外的水*轴,指示 y 轴上的零值。同样的规则也适用于 x 轴上的值。如果使用轴的下限和上限显式设置轴的范围,则零线的可见性取决于零是否落在该范围内。

显示网格线

horizontalGridLinesVisibleverticalGridLinesVisible指定水*和垂直网格线是否可见。默认情况下,两者都设置为 true。

格式化数字刻度标签

有时,您可能想要格式化显示在数轴上的值。出于不同的原因,您希望格式化数值轴的标签:

  • 您想要为记号标签添加前缀或后缀。例如,您可能希望将数字 100 显示为$100 或 100 万。

  • 您可能正在提供图表刻度数据,以获得轴的适当刻度值。例如,对于实际值 100,您可能向图表提供 10。在这种情况下,您希望显示标签的实际值 100。

ValueAxis类包含一个tickLabelFormatter属性,它是一个StringConverter,用于格式化刻度标签。默认情况下,数字轴的刻度标签使用默认格式化程序进行格式化。默认格式化程序是静态内部类NumberAxis.DefaultFormatter的一个实例。

在我们的XYChart示例中,您已经将 y 轴的标签设置为“人口(百万)”,以表示该轴上的刻度值以百万为单位。您可以使用标签格式化程序将“M”附加到刻度值,以表示相同的含义。以下代码片段将完成这一任务:

NumberAxis yAxis = new NumberAxis();
yAxis.setLabel("Population");

// Use a formatter for tick labels on y-axis to append
// M (for millions) to the population value
yAxis.setTickLabelFormatter(new StringConverter<Number>() {
        @Override
        public String toString(Number value) {
               // Append M to the value
               return Math.round(value.doubleValue()) + "M";
        }

        @Override
        public Number fromString(String value) {
               // Strip M from the value
               value = value.replaceAll("M", "");
               return Double.parseDouble(value);
        }
});

NumberAxis.DefaultFormatter更适合为记号标签添加前缀或后缀。该格式化程序与轴的autoRanging属性保持同步。您可以向构造器传递前缀和后缀。下面的代码片段与前面的代码片段实现了相同的功能:

NumberAxis yAxis = new NumberAxis();
yAxis.setLabel("Population");
yAxis.setTickLabelFormatter(new NumberAxis.DefaultFormatter(yAxis, null, "M"));

您可以定制一个Axis的几个视觉方面。更多细节请参考Axis类及其子类的 API 文档。

清单 20-17 中的程序显示了如何定制折线图。图表如图 20-15 所示。它格式化 y 轴上的刻度标签,将“M”追加到标签值。它隐藏网格线并显示替代的列填充。

img/336502_2_En_20_Fig15_HTML.jpg

图 20-15

带有格式化刻度标签和自定义绘图的折线图

// CustomizingCharts.java
// ...find in the book's download area.

Listing 20-17Formatting Tick Labels and Customizing Chart Plot

摘要

图表是数据的图形表示。图表为可视化分析大量数据提供了一种更简单的方法。通常,它们用于报告目的。存在不同类型的图表。它们表示数据的方式不同。并非所有类型的图表都适合分析所有类型的数据。例如,折线图适合于了解数据的比较趋势,而条形图适合于比较不同类别的数据。

JavaFX 支持图表,通过编写几行代码就可以将图表集成到 Java 应用程序中。它包含一个全面的、可扩展的图表 API,为几种类型的图表提供内置支持。图表 API 由javafx.scene.chart包中许多预定义的类组成。这些级别中有几个是ChartXYChartPieChartBarChartLineChart

抽象Chart是所有图表的基类。它继承了Node类。图表可以添加到场景图中。它们也可以像其他节点一样使用 CSS 样式。JavaFX 中的每个图表都有三个部分:标题、图例和数据。不同类型的图表对数据的定义不同。Chart类包含处理标题和图例的属性。

图表可以是动画的。Chart类中的animated属性指定图表内容的变化是否以某种类型的动画显示。默认情况下,是true

饼图由一个圆组成,该圆被分成不同圆心角的扇形。通常,馅饼是圆形的。扇区也被称为饼图饼图。圆圈中的每个扇形代表某种数量。扇形面积的圆心角与它所代表的量成正比。PieChart类的一个实例代表一个饼图。

条形图将数据项呈现为水*或垂直的矩形条。条形的长度与数据项的值成比例。BarChart类的一个实例代表一个条形图。

堆积条形图是条形图的变体。在堆积条形图中,类别中的条形是堆积的。除了条形的位置之外,它的工作方式与条形图相同。StackedBarChart类的一个实例表示一个堆积条形图。

散点图将数据项呈现为符号。一个序列中的所有数据项都使用相同的符号。数据项符号的位置由数据项沿 x 轴和 y 轴的值决定。ScatterChart类的一个实例表示一个散点图。

折线图通过用线段连接数据项来显示一系列数据项。可选地,数据点本身可以由符号表示。您可以将折线图视为散点图,其中的符号由直线段连接成一系列。通常,折线图用于查看一段时间内或一个类别中的数据变化趋势。LineChart类的一个实例代表一个折线图。

气泡图与散点图非常相似,只是它能够表示一个数据点的三个值。气泡用于表示一个系列中的一个数据项。您可以设置气泡的半径来表示数据点的第三个值。BubbleChart类的一个实例代表一个气泡图。

面积图是折线图的变体。它绘制连接系列中所有数据项的线条,此外,还填充线条和 x 轴之间的绘制区域。不同的颜色用于绘制不同系列的区域。AreaChart的一个实例代表一个面积图。

堆积面积图是面积图的变体。它通过为每个系列绘制一个区域来绘制数据项。与面积图不同,系列的面积不会重叠;它们堆叠在一起。StackedAreaChart的一个实例代表一个堆积面积图。

除了使用 CSS 自定义图表的外观之外,Chart API 还提供了一些属性和方法来自定义图表的外观,例如添加替代的行/列填充、显示零线轴、显示网格线以及格式化数字刻度标签。

下一章将讨论如何使用 Image API 在 JavaFX 中处理图像。

二十一、了解图像 API

在本章中,您将学习:

  • 什么是图像 API

  • 如何加载图像

  • 如何查看ImageView节点中的图像

  • 如何执行图像操作,例如读取/写入像素、从头开始创建图像以及将图像保存到文件系统

  • 如何拍摄节点和场景的快照

本章的例子在com.jdojo.image包中。为了让它们工作,您必须在module-info.java文件中添加相应的一行:

...
opens com.jdojo.image to javafx.graphics, javafx.base;
...

什么是图像 API?

JavaFX 提供了 Image API,允许您加载和显示图像,以及读/写原始图像像素。图像 API 中类的类图如图 21-1 所示。所有的类都在javafx.scene.image包中。该 API 允许您

img/336502_2_En_21_Fig1_HTML.png

图 21-1

图像 API 中的类的类图

  • 将图像加载到内存中

  • 将图像显示为场景图中的节点

  • 从图像中读取像素

  • 将像素写入图像

  • 将场景图形中的节点转换为图像,并将其保存到本地文件系统

Image类的一个实例表示内存中的一幅图像。通过向一个WritableImage实例提供像素,可以在 JavaFX 应用程序中构造一个图像。

一个ImageView就是一个Node。它用于在场景图中显示一个Image。如果您想在应用程序中显示图像,您需要在Image中加载图像,并在ImageView中显示图像。

图像由像素构成。图像中像素的数据可以以不同的格式存储。PixelFormat定义如何存储给定格式的像素数据。WritablePixelFormat表示用全像素颜色信息写入像素的目的格式。

PixelReaderPixelWriter接口定义了从Image读取数据和向WritableImage写入数据的方法。除了一个Image之外,你可以从任何包含像素的表面读取像素,也可以向任何包含像素的表面写入像素。

我将在接下来的章节中介绍使用这些类的例子。

加载图像

Image类的一个实例是一个图像的内存表示。该类支持 BMP、PNG、JPEG 和 GIF 图像格式。它从一个源加载一个图像,这个源可以被指定为一个字符串 URL 或者一个InputStream。它还可以在加载时缩放原始图像。

Image类包含几个构造器,允许您为加载的图像指定属性:

  • Image(InputStream is)

  • Image(InputStream is, double requestedWidth, double requestedHeight, boolean preserveRatio, boolean smooth)

  • Image(String url)

  • Image(String url, boolean backgroundLoading)

  • Image(String url, double requestedWidth, double requestedHeight, boolean preserveRatio, boolean smooth)

  • Image(String url, double requestedWidth, double requestedHeight, boolean preserveRatio, boolean smooth, boolean backgroundLoading)

如果将InputStream指定为来源,则图像的来源没有歧义。如果将字符串 URL 指定为源,它可能是有效的 URL 或类路径中的有效路径。如果指定的 URL 不是有效的 URL,它将被用作路径,并且将在CLASSPATH中的路径上搜索图像源:

// Load an image from local machine using an InputStream
String sourcePath = "C:\\mypicture.png";
Image img = new Image(new FileInputStream(sourcePath));

// Load an image from an URL
Image img = new Image("http://jdojo.com/wp-content/uploads/2013/03/randomness.jpg");

// Load an image from the CLASSPATH. The image is located in the resources.picture package
Image img = new Image("resources/picture/randomness.jpg");

在前面的语句中,指定的 URL resources/picture/randomness.jpg不是有效的 URL。Image类将把它视为一条路径,期望它存在于CLASSPATH中。它将resource.picture视为一个包,将randomness.jpg视为该包中的一个资源。

Tip

如果您想测试本章中的代码片段,请确保添加有效的 URL。要么确保使用相对 URL,比如在CLASSPATH中的resources/picture/randomness.jpg,要么指定绝对 URL,比如http://path/to/my/server/resources/picture/randomness.jpgfile://some/absolute/path/resources/picture/randomness.jpg

指定图像加载属性

有些构造器允许您指定一些图像加载属性来控制图像质量和加载过程:

  • requestedWidth

  • requestedHeight

  • preserveRatio

  • smooth

  • backgroundLoading

requestedWidthrequestedHeight属性指定图像的缩放宽度和高度。默认情况下,图像以其原始大小加载。

The preserveRatio属性指定缩放时是否保留图像的纵横比。默认情况下,它是假的。

smooth属性指定在缩放中使用的过滤算法的质量。默认情况下,它是假的。如果设置为 true,将使用质量更好的过滤算法,这会稍微减慢图像加载过程。

属性指定是否异步加载图像。默认情况下,该属性设置为 false,并且同步加载图像。当Image对象被创建时,加载过程开始。如果此属性设置为 true,图像将在后台线程中异步加载。

读取加载的图像属性

Image类包含以下只读属性:

  • width

  • height

  • progress

  • error

  • exception

widthheight属性分别是加载图像的宽度和高度。如果图像加载失败,则它们为零。

progress属性表示加载图像数据的进度。当backgroundLoading属性设置为 true 时,了解进度是很有用的。其值介于 0.0 和 1.0 之间,其中 0.0 表示 0%负载,1.0 表示 100%负载。当backgroundLoading属性设置为 false(默认值)时,其值为 1.0。您可以在progress属性中添加一个ChangeListener来了解图像加载的进度。您可以在图像加载时将文本显示为图像的占位符,并在ChangeListener中用当前进度更新文本:

// Load an image in the background
String imagePath = "resources/picture/randomness.jpg";
Boolean backgroundLoading = true;
Image image = new Image(imagePath, backgroundLoading);

// Print the loading progress on the standard output
image.progressProperty().addListener((prop, oldValue, newValue) -> {
        System.out.println("Loading:" +
               Math.round(newValue.doubleValue() * 100.0) + "%");
});

error属性指示加载图像时是否出现错误。如果为真,exception属性指定了导致错误的Exception。在撰写本文时,Windows 不支持 TIFF 图像格式。以下代码片段试图在 Windows XP 上加载 TIFF 图像,并产生错误。该代码包含一个错误处理逻辑,如果backgroundLoading为真,则向error属性添加一个ChangeListener。否则,它检查error属性的值:

String imagePath = "resources/picture/test.tif";
Boolean backgroundLoading = false;
Image image = new Image(imagePath, backgroundLoading);

// Add a ChangeListener to the error property for background loading and
// check its value for non-background loading
if (image.isBackgroundLoading()) {
    image.errorProperty().addListener((prop, oldValue, newValue) -> {
           if (newValue) {
               System.out.println(
                        "An error occurred while loading the image.\n" +
                        "Error message: " +
                        image.getException().getMessage());
           }
    });
}
else if (image.isError()) {

    System.out.println("An error occurred while loading the image.\n" +
                  "Error message: " +
                        image.getException().getMessage());
}

An error occurred while loading the image.
Error message: No loader for image data

查看图像

ImageView类的一个实例用于显示加载到Image对象中的图像。ImageView类继承自Node类,这使得ImageView适合添加到场景图形中。该类包含几个构造器:

  • ImageView()

  • ImageView(Image image)

  • ImageView(String url)

无参数构造器创建一个没有图像的ImageView。使用image属性来设置图像。第二个构造器接受一个Image的引用。第三个构造器让您指定图像源的 URL。在内部,它使用指定的 URL 创建一个Image:

// Create an empty ImageView and set an Image for it later
ImageView imageView = new ImageView();
imageView.setImage(new Image("resources/picture/randomness.jpg"));

// Create an ImageView with an Image
ImageView imageView = new ImageView(new Image("resources/picture/randomness.jpg"));

// Create an ImageView with the URL of the image source
ImageView imageView = new ImageView("resources/picture/randomness.jpg");

清单 21-1 中的程序展示了如何在场景中显示图像。它将图像加载到一个Image对象中。在不保留纵横比的情况下缩放图像。Image对象被添加到一个ImageView,后者被添加到一个HBox。图 21-2 为窗口。

img/336502_2_En_21_Fig2_HTML.jpg

图 21-2

带有图像的窗口

// ImageTest.java
package com.jdojo.image;

import com.jdojo.util.ResourceUtil;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;

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

        @Override
        public void start(Stage stage) {
           String imagePath =
                   ResourceUtil.getResourceURLStr("picture/randomness.jpg");
           // Scale the image to 200 X 100
           double requestedWidth = 200;
           double requestedHeight = 100;
           boolean preserveRatio = false;
           boolean smooth = true;
           Image image = new Image(imagePath,
                             requestedWidth,
                             requestedHeight,
                             preserveRatio,
                             smooth);
           ImageView imageView = new ImageView(image);

           HBox root = new HBox(imageView);
           Scene scene = new Scene(root);
           stage.setScene(scene);
           stage.setTitle("Displaying an Image");
           stage.show();
        }

}

Listing 21-1Displaying an Image in an ImageView Node

图像的多个视图

一个Image从其来源将图像加载到内存中。同一个Image可以有多个视图。一位ImageView提供了其中一种观点。

您可以选择在加载和/或显示时调整原始图像的大小。选择哪个选项来调整图像的大小取决于手头的要求:

  • 在一个Image对象中调整图像的大小会在内存中永久地调整图像的大小,并且图像的所有视图都将使用调整后的图像。一旦调整了Image的大小,它的大小就不能改变了。您可能希望缩小Image对象中的图像尺寸以节省内存。

  • ImageView中调整图像的大小只会为该视图调整image的大小。即使图像已经显示,您也可以在ImageView中调整图像视图的大小。

我们已经讨论过如何在一个Image对象中调整图像的大小。在这一节中,我们将讨论在ImageView中调整图像的大小。

类似于Image类,ImageView类包含以下四个属性来控制图像视图的大小调整:

  • fitWidth

  • fitHeight

  • preserveRatio

  • smooth

fitWidthfitHeight属性分别指定调整后的图像的宽度和高度。默认情况下,它们是零,这意味着ImageView将使用Image中加载图像的宽度和高度。

属性指定在调整大小时是否保持图像的纵横比。默认情况下,它是假的。

属性指定在调整大小时使用的过滤算法的质量。其默认值取决于*台。如果设置为 true,则使用质量更好的过滤算法。

清单 21-2 中的程序以原始尺寸在Image对象中加载图像。它创建了指定不同大小的Image的三个ImageView对象。图 21-3 显示了三幅图像。图片显示的是一辆垃圾校车和一辆垃圾汽车。该图像经理查德·卡斯蒂略( www.digitizedchaos.com )许可使用。

img/336502_2_En_21_Fig3_HTML.jpg

图 21-3

同一图像的三视图

// MultipleImageViews.java
// ...find in the book's download area.

Listing 21-2Displaying the Same Image in Different ImageView in Different Sizes

在视口中查看图像

视口是一个矩形区域,用于查看图形的一部分。滚动条通常与视口一起使用。当滚动条滚动时,视口显示图形的不同部分。

ImageView可让您定义图像的视窗。在 JavaFX 中,视口是javafx.geometry.Rectangle2D对象的一个实例。Rectangle2D是不可改变的。它由四个属性定义:minXminYwidthheight。(minX,minY)值定义矩形左上角的位置。宽度和高度属性指定其大小。您必须在构造器中指定所有属性:

// Create a viewport located at (0, 0) and of size 200 X 100
Rectangle2D viewport = new Rectangle2D(0, 0, 200,100);

ImageView类包含一个viewport属性,它提供了一个进入显示在ImageView中的图像的视窗。viewport定义了图像中的一个矩形区域。ImageView只显示图像中落在视窗内的区域。视窗的位置是相对于图像定义的,而不是ImageView。默认情况下,ImageView的视窗为空,ImageView显示整个图像。

下面的代码片段在Image中加载原始大小的图像。Image被设置为ImageView的源。为ImageView设置尺寸为 200 X 100 的视窗。视口位于(0,0)处。这显示在ImageView图像的左上角 200 X 100 的区域:

String imagePath = "resources/picture/school_bus.jpg";
Image image = new Image(imagePath);
imageView = new ImageView(image);
Rectangle2D viewport = new Rectangle2D(0, 0, 200, 100);
imageView.setViewport(viewport);

以下代码片段将更改视区以显示图像的 200 X 100 右下角区域:

double minX = image.getWidth() - 200;
double minY = image.getHeight() - 100;
Rectangle2D viewport2 = new Rectangle2D(minX, minY, 200, 100);
imageView.setViewport(viewport2);

Tip

Rectangle2D类是不可变的。因此,每次想要将视口移动到图像中时,都需要创建一个新的视口。

清单 21-3 中的程序将图像加载到ImageView中。它为ImageView设置一个视口。您可以拖动鼠标,同时按下左、右或两个按钮,滚动到视图中图像的不同部分。

// ImageViewPort.java
// ...find in the book's download area.

Listing 21-3Using a Viewport to View Part of an Image

程序声明了一些类和实例变量。VIEWPORT_WIDTHVIEWPORT_HEIGHT是保存视口宽度和高度的常量。当鼠标被按下或拖动时,startXstartY实例变量将保存鼠标的 x 和 y 坐标。ImageView实例变量保存了ImageView的引用。在鼠标拖动的事件处理程序中,我们需要这个引用。

start()方法的开始部分很简单。它创建一个Image,一个ImageView,并为ImageView设置一个视口。然后,它将按下鼠标和拖动鼠标的事件处理程序设置为ImageView:

// Set the mouse pressed and mouse dragged event handlers
imageView.setOnMousePressed(this::handleMousePressed);
imageView.setOnMouseDragged(this::handleMouseDragged);

handleMousePressed()方法中,我们将鼠标的坐标存储在startXstartY实例变量中。坐标相对于ImageView:

startX = e.getX();
startY = e.getY();

由于鼠标拖动,handleMousePressed()方法计算图像内视窗的新位置,并在新位置设置一个新视窗。首先,它计算鼠标沿 x 轴和 y 轴的拖动距离:

// How far the mouse was dragged
double draggedDistanceX = e.getX() - startX;
double draggedDistanceY = e.getY() - startY;

您将startXstartY值重置为触发当前鼠标拖动事件的鼠标位置。这对于在用户按住鼠标、拖动鼠标、停止而不松开鼠标,然后再次拖动鼠标时获得正确的拖动距离非常重要:

// Reset the starting point for the next drag
// if the user keeps the mouse pressed and drags again
startX = e.getX();
startY = e.getY();

计算视口左上角的新位置。在ImageView中你总是有一个视窗。新视口将位于旧位置的拖动距离处:

// Get the minX and minY of the current viewport
double curMinX = imageView.getViewport().getMinX();
double curMinY = imageView.getViewport().getMinY();

// Move the new viewport by the dragged distance
double newMinX = curMinX + draggedDistanceX;
double newMinY = curMinY + draggedDistanceY;

将视口放在图像区域之外是可以的。当视窗落在图像区域之外时,它只显示一个空白区域。为了将视口限制在图像区域内,我们夹紧视口的位置:

// Make sure the viewport does not fall outside the image area
newMinX = clamp(newMinX, 0, imageView.getImage().getWidth() - VIEWPORT_WIDTH);
newMinY = clamp(newMinY, 0, imageView.getImage().getHeight() - VIEWPORT_HEIGHT);

最后,我们使用新位置设置一个新的视口:

// Set a new viewport
imageView.setViewport(new Rectangle2D(newMinX, newMinY, VIEWPORT_WIDTH, VIEWPORT_HEIGHT));

Tip

可以缩放或旋转ImageView并设置一个视窗来查看由视窗定义的图像区域。

了解图像操作

JavaFX 支持从图像中读取像素、向图像中写入像素以及创建场景的快照。它支持从头开始创建图像。如果图像是可写的,您还可以修改内存中的图像,并将其保存到文件系统中。图像 API 提供了对图像中每个像素的访问。它支持一次读写一个像素或一大块像素。本节将通过简单的例子讨论 Image API 支持的操作。

像素格式

JavaFX 中的 Image API 允许您访问图像中的每个像素。像素存储有关其颜色(红色、绿色、蓝色)和不透明度(alpha)的信息。像素信息可以以几种格式存储。

一个实例PixelFormat<T extends Buffer>代表一个像素的数据布局。当你从图像中读取像素时,你需要知道像素格式。将像素写入图像时,需要指定像素格式。WritablePixelFormat类继承自PixelFormat类,它的实例代表一种可以存储全彩色信息的像素格式。当向图像写入像素时,使用WritablePixelFormat类的一个实例。

PixelFormat和它的子类WritablePixelFormat都是抽象的。PixelFormat类提供了几个静态方法来获取PixelFormatWritablePixelFormat抽象类的实例。在我们讨论如何获得一个PixelFormat的实例之前,让我们讨论一下可用于存储像素数据的存储格式的类型。

一个PixelFormat有一个指定单个像素的存储格式的类型。PixelFormat.Type枚举的常量代表不同类型的存储格式:

  • BYTE_RGB

  • BYTE_BGRA

  • BYTE_BGRA_PRE

  • BYTE_INDEXED

  • INT_ARGB

  • INT_ARGB_PRE

BYTE_RGB格式中,像素被认为是不透明的。像素按顺序以红色、绿色和蓝色存储在相邻的字节中。

BYTE_BGRA格式中,像素按照蓝色、绿色、红色和 alpha 顺序存储在相邻的字节中。颜色值(红色、绿色和蓝色)不会与 alpha 值预先相乘。

BYTE_BGRA_PRE类型格式类似于BYTE_BGRA,除了在BYTE_BGRA_PRE中,存储的颜色分量值预先乘以阿尔法值。

BYTE_INDEXED格式中,一个像素是一个字节。提供了单独的颜色查找列表。像素的单字节值用作查找列表中的索引,以获取像素的颜色值。

INT_ARGB格式中,每个像素以 32 位整数存储。从最高有效字节(MSB)到最低有效字节(LSB)的字节存储 alpha、红色、绿色和蓝色值。颜色值(红色、绿色和蓝色)不会与 alpha 值预先相乘。以下代码片段显示了如何以这种格式从像素值中提取分量:

int pixelValue = get the value for a pixel...
int alpha = (pixelValue >> 24) & 0xff;
int red   = (pixelValue >> 16) & 0xff;
int green = (pixelValue >>  8) & 0xff;
int blue  = pixelValue & 0xff;

除了INT_ARGB_PRE存储预先乘以 alpha 值的颜色值(红色、绿色和蓝色)之外,INT_ARGB_PRE格式类似于INT_ARGB格式。

通常,当你写像素来创建一个新的图像时,你需要创建一个WritablePixelFormat。当您从图像中读取像素时,像素读取器将为您提供一个PixelFormat实例,告诉您像素中的颜色信息是如何存储的。下面的代码片段创建了WritablePixelFormat类的一些实例:

import javafx.scene.image.PixelFormat;
import javafx.scene.image.WritablePixelFormat;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
...
// BYTE_BGRA Format type
WritablePixelFormat<ByteBuffer> format1 = PixelFormat.getByteBgraInstance();

// BYTE_BGRA_PRE Format type
WritablePixelFormat<ByteBuffer> format2 =
    PixelFormat.getByteBgraPreInstance();

// INT_ARGB Format type
WritablePixelFormat<IntBuffer> format3 = PixelFormat.getIntArgbInstance();

// INT_ARGB_PRE Format type
WritablePixelFormat<IntBuffer> format4 = PixelFormat.getIntArgbPreInstance();

没有像素信息,像素格式类是没有用的。毕竟它们描述的是信息在一个像素中的布局!在接下来的章节中,当我们读写图像像素时,我们将使用这些类。它们的使用在例子中是显而易见的。

从图像中读取像素

接口的一个实例用于从图像中读取像素。使用Image类的getPixelReader()方法获得一个PixelReaderPixelReader接口包含以下方法:

  • int getArgb(int x, int y)

  • Color getColor(int x, int y)

  • Void getPixels(int x, int y, int w, int h, WritablePixelFormat<ByteBuffer> pixelformat, byte[] buffer, int offset, int scanlineStride)

  • void getPixels(int x, int y, int w, int h, WritablePixelFormat<IntBuffer> pixelformat, int[] buffer, int offset, int scanlineStride)

  • <T extends Buffer> void getPixels(int x, int y, int w, int h, WritablePixelFormat<T> pixelformat, T buffer, int scanlineStride)

  • PixelFormat getPixelFormat()

PixelReader接口包含一次读取一个或多个像素的方法。使用getArgb()getColor()方法读取指定(x,y)坐标的像素。使用getPixels()方法批量读取像素。使用getPixelFormat()方法获得最能描述源中像素存储格式的PixelFormat

只有当图像可读时,Image类的getPixelReader()方法才返回一个PixelReader。否则返回null。如果图像尚未完全加载、加载过程中出现错误或其格式不支持读取像素,则图像可能不可读:

Image image = new Image("file://.../resources/picture/ksharan.jpg");

// Get the pixel reader
PixelReader pixelReader = image.getPixelReader();
if (pixelReader == null) {
        System.out.println("Cannot read pixels from the image");
} else {
        // Read image pixels
}

一旦有了一个PixelReader,就可以调用它的一个方法来读取像素。清单 21-4 中的程序展示了如何从图像中读取像素。代码是不言自明的:

  • start()方法创建一个ImageImage同步加载。

  • 读取像素的逻辑在readPixelsInfo()方法中。该方法接收一个完全加载的Image。它使用PixelReadergetColor()方法获取指定位置的像素。它打印所有像素的颜色。最后,它打印像素格式,这是BYTE_RGB

// ReadPixelInfo.java
// ...find in the book's download area.
Color at (0, 0) = 0xb5bb41ff
Color at (1, 0) = 0xb0b53dff
...
Color at (233, 287) = 0x718806ff
Color at (234, 287) = 0x798e0bff
Pixel format type: BYTE_RGB

Listing 21-4Reading Pixels from an Image

批量读取像素比一次读取一个像素要困难一些。困难来自于您必须提供给getPixels()方法的设置信息。我们将通过使用PixelReader的以下方法批量读取所有像素来重复前面的示例:

void getPixels(int x, int y,
               int width, int height,
               WritablePixelFormat<ByteBuffer> pixelformat,
               byte[] buffer,
               int offset,
               int scanlineStride)

该方法按顺序从行中读取像素。读取第一行的像素,然后读取第二行的像素,依此类推。理解该方法所有参数的含义非常重要。

方法读取源中矩形区域的像素。

矩形区域左上角的 x 和 y 坐标在x and y参数中指定。

widthheight参数指定矩形区域的宽度和高度。

pixelformat指定了用于在指定的buffer中存储读取像素的像素格式。

buffer是一个byte数组,其中PixelReader将存储读取的像素。数组的长度必须足够大,以存储所有读取的像素。

offset指定了buffer数组中存储第一个像素数据的起始索引。其零值表示第一个像素的数据将从缓冲区中的索引 0 开始。

scanlineStride指定缓冲区中一行数据的起点和下一行数据的起点之间的距离。假设你在一行中有两个像素,你想以一个像素 4 个字节的BYTE_BGRA格式读取。一行数据可以存储在 8 个字节中。如果将参数值指定为 8,则下一行的数据将在前一行数据结束后立即在缓冲区中开始。如果将参数值指定为 10,则每行数据的最后 2 个字节将为空。第一行像素将从索引 0 到 7 存储。索引 8 和 9 将为空(或未被写入)。索引 10 至 17 将存储第二行的像素数据,索引 18 和 19 为空。如果以后要用自己的值填充空槽,可能需要为参数指定一个比存储一行像素数据所需的值更大的值。指定一个小于所需的值将会覆盖前一行中的部分数据。

以下代码片段显示了如何以BYTE_BGRA格式从一个byte数组中读取图像的所有像素:

Image image = ...
PixelReader pixelReader = image.getPixelReader();

int x = 0;
int y = 0;
int width = (int)image.getWidth();
int height = (int)image.getHeight();
int offset = 0;
int scanlineStride = width * 4;
byte[] buffer = new byte[width * height * 4];

// Get a WritablePixelFormat for the BYTE_BGRA format type
WritablePixelFormat<ByteBuffer> pixelFormat = PixelFormat.getByteBgraInstance();

// Read all pixels at once
pixelReader.getPixels(x, y,
                width, height,
                pixelFormat,
                buffer,
                offset,
                scanlineStride);

要读取的矩形区域的左上角的 x 和 y 坐标被设置为零。区域的宽度和高度被设置为图像的宽度和高度。这将设置参数来读取整个图像。

您希望从索引 0 开始将像素数据读入缓冲区,因此将offset参数设置为 0。

你想读取BYTE_BGRA格式类型的像素数据,需要 4 个字节来存储一个像素的数据。我们已经将参数值scanlineStride设置为width * 4,它是一行数据的长度,因此一行数据从上一行数据结束的下一个索引开始。

您获得了一个WritablePixelFormat的实例来读取BYTE_BGRA格式类型的数据。最后,我们调用PixelReadergetPixels()方法来读取像素数据。当getPixels()方法返回时,buffer将被像素数据填充。

Tip

设置scanlineStride参数的值和缓冲区数组的长度取决于pixelFormat参数。其他版本的getPixels()方法允许读取不同格式的像素数据。

清单 21-5 中的程序有完整的源代码来批量读取像素。读取所有像素后,它对(0,0)处像素的字节数组中的颜色分量进行解码。它使用getColor()方法读取(0,0)处的像素。通过两种方法获得的(0,0)处的像素数据打印在标准输出上。

// BulkPixelReading.java
// ...find in the book's download area.
red=181, green=187, blue=65, alpha=255
red=181, green=187, blue=65, alpha=255

Listing 21-5Reading Pixels from an Image in Bulk

将像素写入图像

您可以将像素写入图像或任何支持写入像素的表面。例如,您可以将像素写入一个WritableImage和一个Canvas

Tip

一个Image是一个只读像素表面。您可以从Image中读取像素。但是,您不能将像素写入Image。如果您想写入图像或从头开始创建图像,请使用WritableImage

接口的一个实例被用来将像素写到一个表面上。可写表面提供了一个PixelWriter。例如,您可以使用CanvasWritableImagegetPixelWriter()方法为它们获取一个PixelWriter

PixelWriter接口包含将像素写入表面并获得表面支持的像素格式的方法:

  • PixelFormat getPixelFormat()

  • void setArgb(int x, int y, int argb)

  • void setColor(int x, int y, Color c)

  • void setPixels(int x, int y, int w, int h, PixelFormat<ByteBuffer> pixelformat, byte[] buffer, int offset, int scanlineStride)

  • void setPixels(int x, int y, int w, int h, PixelFormat<IntBuffer> pixelformat, int[] buffer, int offset, int scanlineStride)

  • <T extends Buffer> void setPixels(int x, int y, int w, int h, PixelFormat<T> pixelformat, T buffer, int scanlineStride)

  • void setPixels(int dstx, int dsty, int w, int h, PixelReader reader, int srcx, int srcy)

getPixelFormat()方法返回像素可以写入表面的像素格式。setArgb()setColor()方法允许在目标表面的指定(x,y)位置写入一个像素。setArgb()方法接受 INT_ARGB 格式的整数像素数据,而setColor()方法接受颜色对象。setPixels()方法允许批量像素写入。

您可以使用WritableImage的实例从头开始创建图像。该类包含三个构造器:

  • WritableImage(int width, int height)

  • WritableImage(PixelReader reader, int width, int height)

  • WritableImage(PixelReader reader, int x, int y, int width, int height)

第一个构造器创建一个指定的widthheight的空图像:

// Create a new empty image of 200 X 100
WritableImage newImage = new WritableImage(200, 100);

第二个构造器创建指定的widthheight的图像。指定的reader用于用像素填充图像。如果阅读器从一个没有足够的行数和列数来填充新图像的表面读取,就会抛出一个ArrayIndexOutOfBoundsException。使用此构造器复制整个或部分图像。以下代码片段创建了一个图像的副本:

String imagePath = "file://.../resources/picture/ksharan.jpg";
Image image = new Image(imagePath, 200, 100, true, true);

int width = (int)image.getWidth();
int height = (int)image.getHeight();

// Create a copy of the image
WritableImage newImage =
    new WritableImage(image.getPixelReader(), width, height);

第三个构造器允许您从表面复制一个矩形区域。(xy)值是矩形区域左上角的坐标。(widthheight)值是使用reader读取的矩形区域的尺寸和新图像的所需尺寸。如果阅读器从一个没有足够的行数和列数来填充新图像的表面读取,就会抛出一个ArrayIndexOutOfBoundsException

WritableImage是一个读写映像。它的getPixelWriter()方法返回一个PixelWriter来将像素写入图像。它继承了返回一个从图像中读取数据的PixelReadergetPixelReader()方法。

下面的代码片段创建了一个Image和一个空的WritableImage。它从Image中一次读取一个像素,使像素变暗,并将相同的像素写入新的WritableImage。最后,我们创建了原始图像的一个更暗的副本:

Image image = new Image("file://.../resources/picture/ksharan.jpg";);
PixelReader pixelReader = image.getPixelReader();
int width = (int)image.getWidth();
int height = (int)image.getHeight();

// Create a new, empty WritableImage
WritableImage darkerImage = new WritableImage(width, height);
PixelWriter darkerWriter = darkerImage.getPixelWriter();

// Read one pixel at a time from the source and
// write it to the destinations - one darker and one brighter
for(int y = 0; y < height; y++) {
        for(int x = 0; x < width; x++) {
               // Read the pixel from the source image
               Color color = pixelReader.getColor(x, y);

               // Write a darker pixel to the new image at the same
                    // location
               darkerWriter.setColor(x, y, color.darker());
        }
}

清单 21-6 中的程序创建一个Image。它创建了三个WritableImage实例,并将原始图像中的像素复制到其中。复制的像素在写入目标之前会被修改。对于一个目的地,像素变暗,一个变亮,一个变成半透明。四幅图像都显示在ImageViews中,如图 21-4 所示。

img/336502_2_En_21_Fig4_HTML.jpg

图 21-4

原始图像和修改后的图像

// CopyingImage.java
// ...find in the book's download area.

Listing 21-6Writing Pixels to an Image

Tip

在 JavaFX 中裁剪图像很容易。使用PixelReadergetPixels()方法之一读取缓冲区中所需的图像区域,并将缓冲区写入新图像。这为您提供了一个新图像,它是原始图像的裁剪版本。

从头开始创建图像

在上一节中,我们通过从另一个图像复制像素来创建新图像。在将原始像素写入新图像之前,我们已经改变了它们的颜色和不透明度。这很简单,因为我们一次处理一个像素,我们接收一个像素作为Color对象。也可以从头开始创建像素,然后使用它们来创建新的图像。任何人都会承认,通过在代码中定义每个像素来创建一个新的、有意义的图像并不是一件容易的事情。然而,JavaFX 使这个过程变得很容易。

在这一节中,我们将创建一个新的图像,它由矩形组成,以类似网格的方式放置。使用连接左上角和右下角的对角线将每个矩形分成两部分。上面的三角形是绿色的,下面的是红色的。将创建一个新图像并用矩形填充。

从头开始创建映像包括三个步骤:

  • 创建一个WritableImage的实例。

  • 创建缓冲区(一个byte数组,一个int数组,等等)。)并根据您希望用于像素数据的像素格式用像素数据填充它。

  • 将缓冲区中的像素写入图像。

让我们编写为矩形区域创建像素的代码。让我们为矩形的宽度和高度声明常量:

static final int RECT_WIDTH = 20;
static final int RECT_HEIGHT = 20;

我们需要定义一个足够大的缓冲区(一个byte数组)来保存所有像素的数据。BYTE_RGB格式的每个像素占用 2 个字节:

byte[] pixels = new byte[RECT_WIDTH * RECT_HEIGHT * 3];

如果该区域是矩形的,我们需要知道高度与宽度的比率,以便将该区域分成上下两个矩形:

double ratio = 1.0 * RECT_HEIGHT/RECT_WIDTH;

以下代码片段填充了缓冲区:

// Generate pixel data
for (int y = 0; y < RECT_HEIGHT; y++) {
        for (int x = 0; x < RECT_WIDTH; x++) {
           int i = y * RECT_WIDTH * 3 + x * 3;
           if (x <= y/ratio) {
               // Lower-half
               pixels[i] = -1;  // red -1 means 255 (-1 & 0xff = 255)
               pixels[i+1] = 0; // green = 0
               pixels[i+2] = 0; // blue = 0
           } else {
               // Upper-half
               pixels[i] = 0;    // red = 0
               pixels[i+1] = -1; // Green 255
               pixels[i+2] = 0;  // blue = 0
           }
        }
}

像素以行优先的顺序存储在缓冲区中。循环中的变量i计算一个像素的 3 字节数据在缓冲区中的起始位置。例如,(0,0)处的像素的数据从索引 0 开始;(0,1)处的像素的数据从索引 3 开始;等等。像素的 3 个字节按照索引递增的顺序存储红色、绿色和蓝色值。颜色分量的编码值存储在缓冲区中,因此表达式“byteValue & 0xff”将产生 0 到 255 之间的实际颜色分量值。如果你想要一个红色像素,你需要为红色分量设置–1,因为“-1 & 0xff产生 255。对于红色,绿色和蓝色分量将被设置为零。字节数组将所有元素初始化为零。然而,我们已经在代码中明确地将它们设置为零。对于下半部分的三角形,我们将颜色设置为绿色。条件“x =<= y/ratio”用于确定一个像素的位置是落在上半三角形还是下半三角形。如果y/ratio不是一个整数,矩形分成两个三角形在右下角可能会有点偏离。

一旦我们获得了像素数据,我们需要将它们写入一个WritableImage。以下代码片段写入矩形的像素,一次在图像的左上角:

WritableImage newImage = new WritableImage(350, 100);
PixelWriter pixelWriter = newImage.getPixelWriter();
byte[] pixels = generate pixel data...

// Our data is in BYTE_RGB format
PixelFormat<ByteBuffer> pixelFormat = PixelFormat.getByteRgbInstance();
Int xPos 0;
int yPos =0;
int offset = 0;
int scanlineStride = RECT_WIDTH * 3;
pixelWriter.setPixels(xPos, yPos,
                 RECT_WIDTH, RECT_HEIGHT,
                 pixelFormat,
                 pixels, offset,
                 scanlineStride);

清单 21-7 中的程序从头开始创建一个图像。它通过为矩形区域写入行像素来填充图像,从而创建图案。图 21-5 为图示。

img/336502_2_En_21_Fig5_HTML.jpg

图 21-5

从零开始创造的图像

// CreatingImage.java
// ...find in the book's download area.

Listing 21-7Creating an Image from Scratch

将新图像保存到文件系统

Image保存到文件系统很容易:

  • 使用SwingFXUtils类的fromFXImage()方法将Image转换为BufferedImage

  • BufferedImage传递给ImageIO类的write()方法。

请注意,我们必须使用两个类— BufferedImage和 ImageIO—它们是标准 Java 库的一部分,而不是 JavaFX 库的一部分。以下代码片段显示了将图像保存到 PNG 格式的文件中所涉及的步骤概要:

import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.image.Image;
import javax.imageio.ImageIO;
...

Image image = create an image...
BufferedImage bImage = SwingFXUtils.fromFXImage(image, null);

// Save the image to the file
File fileToSave = ...
String imageFormat = "png";
try {
        ImageIO.write(bImage, imageFormat, fileToSave);
}
catch (IOException e) {
        throw new RuntimeException(e);
}

清单 21-8 中的程序有一个实用程序类ImageUtil的代码。它的静态saveToFile(Image image)方法可以用来将一个Image保存到本地文件系统。该方法要求输入文件名。用户可以为图像选择 PNG 或 JPEG 格式。

// ImageUtil.java
// ...find in the book's download area.

Listing 21-8A Utility Class to Save an Image to a File

清单 21-9 中的程序展示了如何将图像保存到文件中。点击Save Image按钮将图片保存到文件中。它会打开一个文件选择器对话框,让您选择文件名。如果取消文件选择器对话框,保存过程将中止。

// SaveImage.java
// ...find in the book's download area.

Listing 21-9Saving an Image to a File

拍摄节点和场景的快照

JavaFX 允许您拍摄下一帧中出现的NodeScene的快照。您在WritableImage中获取快照,这意味着您可以在获取快照后执行所有像素级操作。NodeScene类包含一个snapshot()方法来完成这个任务。

拍摄节点的快照

Node类包含一个重载的snapshot()方法:

  • WritableImage snapshot(SnapshotParameters params, WritableImage image)

  • void snapshot(Callback<SnapshotResult,Void> callback, SnapshotParameters params, WritableImage image)

第一个版本的snapshot()方法是同步的,而第二个是异步的。该方法允许您指定包含快照呈现属性的SnapshotParameters类的实例。如果为空,将使用默认值。您可以为快照设置以下属性:

  • 填充颜色

  • 一个转变

  • 视口

  • 一台照相机

  • 深度缓冲器

默认情况下,填充颜色是白色;不使用变换和视口;使用一个ParallelCamera;并且深度缓冲器被设置为假。请注意,这些属性仅在拍摄快照时在节点上使用。

您可以在snapshot()方法中指定一个WritableImage来保存节点的快照。如果这是null,则创建一个新的WritableImage。如果指定的WritableImage小于节点,节点将被裁剪以适应图像大小。

第一个版本的snapshot()方法在WritableImage中返回快照。该图像或者是作为参数传递的图像,或者是由方法创建的新图像。

第二个异步版本的snapshot()方法接受一个Callback对象,其call()方法被调用。一个SnapshotResult对象被传递给call()方法,该方法可用于通过以下方法获得快照映像、源节点和快照参数:

  • WritableImage getImage()

  • SnapshotParameters getSnapshotParameters()

  • Object getSource()

Tip

snapshot()方法使用节点的boundsInParent属性获取节点的快照。也就是说,快照包含应用于节点的所有效果和变换。如果正在对节点进行动画处理,快照将包括拍摄时节点的动画状态。

清单 21-10 中的程序展示了如何拍摄一个TextField节点的快照。在一个GridPane中显示一个Label,一个TextField,两个Buttons。按钮用于同步和异步拍摄TextField的快照。点击其中一个Buttons拍摄快照。将出现“文件保存”对话框,让您输入保存的快照的文件名。syncSnapshot()asyncSnapshot()方法包含获取快照的逻辑。对于快照,填充设置为红色,并应用了一个Scale和一个Rotate变换。图 21-6 为快照。

img/336502_2_En_21_Fig6_HTML.jpg

图 21-6

节点的快照

// NodeSnapshot.java
// ...find in the book's download area.

Listing 21-10Taking a Snapshot of a Node

拍摄场景的快照

Scene类包含一个重载的snapshot()方法:

  • WritableImage snapshot(WritableImage image)

  • void snapshot(Callback<SnapshotResult,Void> callback, WritableImage image)

比较Scene类和Node类的snapshot()方法。唯一的区别是Scene类中的snapshot()方法不包含SnapshotParameters参数。这意味着您无法自定义场景快照。除此之外,该方法的工作方式与针对Node类的工作方式相同,如前一节所述。

第一个版本的snapshot()方法是同步的,而第二个是异步的。您可以为保存节点快照的方法指定一个WritableImage。如果这是null,则创建一个新的WritableImage。如果指定的WritableImage小于场景,场景将被裁剪以适合图像大小。

清单 21-11 中的程序展示了如何拍摄一个场景的快照。程序中的主要逻辑与清单 21-10 中的程序基本相同,除了这一次,它拍摄了一个场景的快照。图 21-7 显示了快照。

img/336502_2_En_21_Fig7_HTML.png

图 21-7

场景的快照

// SceneSnapshot.java
// ...find in the book's download area.

Listing 21-11Taking a Snapshot of a Scene

摘要

JavaFX 提供了 Image API,允许您加载和显示图像,以及读/写原始图像像素。API 中的所有类都在 javafx.scene.image 包中。API 允许您对图像执行以下操作:将图像加载到内存中,将图像显示为场景图中的节点,从图像中读取像素,将像素写入图像,以及将场景图中的节点转换为图像并将其保存到本地文件系统。

Image类的一个实例是一个图像的内存表示。您还可以通过向一个WritableImage实例提供像素来在 JavaFX 应用程序中构造一个图像。Image类支持 BMP、PNG、JPEG 和 GIF 图像格式。它从一个源加载一个图像,这个源可以被指定为一个字符串 URL 或者一个InputStream。它还可以在加载时缩放原始图像。

ImageView类的一个实例用于显示加载到Image对象中的图像。ImageView类继承自Node类,这使得ImageView适合添加到场景图形中。

图像由像素构成。JavaFX 支持从图像中读取像素、向图像中写入像素以及创建场景的快照。它支持从头开始创建图像。如果图像是可写的,您还可以修改内存中的图像,并将其保存到文件系统中。图像 API 提供了对图像中每个像素的访问。它支持一次读写一个像素或一大块像素。

图像中像素的数据可以以不同的格式存储。PixelFormat定义如何存储给定格式的像素数据。WritablePixelFormat表示用全像素颜色信息写入像素的目的格式。

PixelReaderPixelWriter接口定义了从Image读取数据和向WritableImage写入数据的方法。除了一个Image之外,你可以从任何包含像素的表面读取像素,也可以向任何包含像素的表面写入像素。

JavaFX 允许您拍摄下一帧中出现的NodeScene的快照。您在WritableImage中获取快照,这意味着您可以在获取快照后执行所有像素级操作。NodeScene类包含一个snapshot()方法来完成这个任务。

下一章将讨论如何使用 Canvas API 在画布上绘图。

二十二、在画布上画画

在本章中,您将学习:

  • 什么是画布 API

  • 如何创建画布

  • 如何在画布上绘图,如基本形状、文本、路径和图像

  • 如何清除画布区域

  • 如何在GraphicsContext中保存和恢复绘图状态

本章的例子在com.jdojo.canvas包中。为了让它们工作,您必须在module-info.java文件中添加相应的一行:

...
opens com.jdojo.canvas to javafx.graphics, javafx.base;
...

什么是画布 API?

通过javafx.scene.canvas包,JavaFX 提供了 Canvas API,该 API 提供了一个绘图表面来使用绘图命令绘制形状、图像和文本。该 API 还提供了对绘图表面的像素级访问,您可以在表面上写入任何像素。API 只包含两个类:

  • Canvas

  • GraphicsContext

画布是位图图像,用作绘图表面。Canvas类的一个实例代表一个画布。它继承自Node类。因此,画布是一个节点。可以将它添加到场景图中,并对其应用效果和变换。

画布具有与之相关联的图形上下文,用于向画布发出绘制命令。GraphicsContext类的一个实例代表一个图形上下文。

创建画布

Canvas类有两个构造器。无参数构造器创建一个空画布。稍后,您可以使用画布的widthheight属性来设置画布的大小。另一个构造器将画布的宽度和高度作为参数:

// Create a Canvas of zero width and height
Canvas canvas = new Canvas();

// Set the canvas size
canvas.setWidth(400);
canvas.setHeight(200);

// Create a 400X200 canvas
Canvas canvas = new Canvas(400, 200);

在画布上画画

一旦创建了画布,就需要使用getGraphicsContext2D()方法获取它的图形上下文,如下面的代码片段所示:

// Get the graphics context of the canvas
GraphicsContext gc = canvas.getGraphicsContext2D();

所有绘图命令都作为方法在GraphicsContext类中提供。超出画布边界的绘图将被剪裁。画布使用缓冲区。绘图命令将必要的参数推送到缓冲区。值得注意的是,在将Canvas添加到场景图形之前,您应该使用来自任何一个线程的图形上下文。一旦Canvas被添加到场景图形中,图形上下文应该只在 JavaFX 应用程序线程上使用。GraphicsContext类包含绘制以下类型对象的方法:

  • 基本形状

  • 文本

  • 小路

  • 形象

  • 像素

绘制基本形状

GraphicsContext类提供了两种绘制基本形状的方法。方法fillXxx()绘制一个形状Xxx,并用当前的填充颜料填充它。方法strokeXxx()用当前笔画绘制形状Xxx。使用下列方法绘制形状:

  • fillArc()

  • fillOval()

  • fillPolygon()

  • fillRect()

  • fillRoundRect()

  • strokeArc()

  • strokeLine()

  • strokeOval()

  • strokePolygon()

  • strokePolyline()

  • strokeRect()

  • strokeRoundRect()

下面的代码片段绘制了一个矩形。描边颜色为红色,描边宽度为 2px。矩形的左上角位于(0,0)。矩形宽 100 像素,高 50 像素:

Canvas canvas = new Canvas(200, 100);
GraphicsContext gc = canvas.getGraphicsContext2D();
gc.setLineWidth(2.0);
gc.setStroke(Color.RED);
gc.strokeRect(0, 0, 100, 50);

绘图文本

您可以使用下面的代码片段,使用GraphicsContextfillText()strokeText()方法来绘制文本:

  • void strokeText(String text, double x, double y)

  • void strokeText(String text, double x, double y, double maxWidth)

  • void fillText(String text, double x, double y)

  • void fillText(String text, double x, double y, double maxWidth)

这两个方法都是重载的。一个版本允许您指定文本及其位置。另一个版本允许您指定文本的最大宽度。如果实际文本宽度超过指定的最大宽度,文本将调整大小以适合指定的最大宽度。以下代码片段绘制了两个字符串。图 22-1 显示了画布上的两根弦。

img/336502_2_En_22_Fig1_HTML.png

图 22-1

在画布上绘制文本

Canvas canvas = new Canvas(200, 50);
GraphicsContext gc = canvas.getGraphicsContext2D();
gc.setLineWidth(1.0);
gc.setStroke(Color.BLACK);
gc.strokeText("Drawing Text", 10, 10);
gc.strokeText("Drawing Text", 100, 10, 40);

绘制路径

您可以使用路径命令和 SVG 路径字符串来创建您选择的形状。路径由多个子路径组成。以下方法用于绘制路径:

  • beginPath()

  • lineTo(double x1, double y1)

  • moveTo(double x0, double y0)

  • quadraticCurveTo(double xc, double yc, double x1, double y1)

  • appendSVGPath(String svgpath)

  • arc(double centerX, double centerY, double radiusX, double radiusY, double startAngle, double length)

  • arcTo(double x1, double y1, double x2, double y2, double radius)

  • bezierCurveTo(double xc1, double yc1, double xc2, double yc2, double x1, double y1)

  • closePath()

  • stroke()

  • fill()

beginPath()closePath()方法分别启动和关闭一个路径。像arcTo()lineTo()这样的方法是绘制特定类型子路径的路径命令。不要忘记在最后调用stroke()fill()方法,它们将绘制轮廓或填充路径。下面这段代码画了一个三角形,如图 22-2 所示:

img/336502_2_En_22_Fig2_HTML.png

图 22-2

画三角形

Canvas canvas = new Canvas(200, 50);
GraphicsContext gc = canvas.getGraphicsContext2D();
gc.setLineWidth(2.0);
gc.setStroke(Color.BLACK);

gc.beginPath();
gc.moveTo(25, 0);
gc.appendSVGPath("L50, 25L0, 25");
gc.closePath();
gc.stroke();

绘制图像

您可以使用drawImage()方法在画布上绘制图像。该方法有三个版本:

  • void drawImage(Image img, double x, double y)

  • void drawImage(Image img, double x, double y, double w, double h)

  • void drawImage(Image img, double sx, double sy, double sw, double sh, double dx, double dy, double dw, double dh)

你可以画出图像的全部或一部分。可以在画布上拉伸或缩短绘制的图像。以下代码片段在画布上以原始大小(10,10)绘制了整个图像:

Image image = new Image("your_image_URL");
Canvas canvas = new Canvas(400, 400);
GraphicsContext gc = canvas.getGraphicsContext2D();
gc.drawImage(image, 10, 10);

下面的语句将在画布上绘制整个图像,方法是调整图像大小以适合 100 像素宽 150 像素高的区域。图像是拉伸还是缩短取决于其原始大小:

// Draw the whole image in 100X150 area at (10, 10)
gc.drawImage(image, 10, 10, 100, 150);

下面的语句将在画布上绘制图像的一部分。这里,假设源图像大于 100 像素乘 150 像素。正在绘制的图像部分宽 100 像素,高 150 像素,其左上角在源图像中的(0,0)处。图像的一部分以(10,10)绘制在画布上,并被拉伸以适合画布上 200 像素宽和 200 像素高的区域:

// Draw part of the image in 200X200 area at (10, 10)
gc.drawImage(image, 0, 0, 100, 150, 10, 10, 200, 200);

写入像素

你也可以直接在画布上修改像素。GraphicsContext对象的getPixelWriter()方法返回一个PixelWriter,可用于将像素写入关联的画布:

Canvas canvas = new Canvas(200, 100);
GraphicsContext gc = canvas.getGraphicsContext2D();
PixelWriter pw = gc.getPixelWriter();

一旦你得到一个PixelWriter,你就可以把像素写到画布上。第二十一章介绍了更多关于如何使用PixelWriter写像素的细节。

清除画布区域

画布是一个透明区域。像素将具有颜色和不透明度,这取决于在这些像素上绘制的内容。有时,您可能想要清除整个或部分画布,以便像素再次透明。GraphicsContextclearRect()方法让您清除画布上的指定区域:

// Clear the top-left 100X100 rectangular area from the canvas
gc.clearRect(0, 0, 100, 100);

保存和恢复绘图状态

GraphicsContext的当前设置用于所有后续绘图。例如,如果您将线条宽度设置为 5px,则所有后续笔画的宽度都将为 5px。有时,您可能希望临时修改图形上下文的状态,并在一段时间后恢复修改前的状态。

GraphicsContext对象的save()restore()方法分别让您保存当前状态和在以后恢复它。在你使用这些方法之前,让我们讨论一下它的必要性。假设您想按顺序向GraphicsContext对象发出以下命令:

  • 画一个没有任何效果的矩形

  • 绘制具有反射效果的字符串

  • 画一个没有任何效果的矩形

以下是实现这一点的第一次(也是不正确的)尝试:

Canvas canvas = new Canvas(200, 120);
GraphicsContext gc = canvas.getGraphicsContext2D();
gc.strokeRect(10, 10, 50, 20);
gc.setEffect(new Reflection());
gc.strokeText("Chatar", 70, 20);
gc.strokeRect(120, 10, 50, 20);

图 22-3 为画布的绘制。请注意,反射效果也应用于第二个矩形,这是不希望的。

img/336502_2_En_22_Fig3_HTML.png

图 22-3

绘制形状和文本

您可以在绘制文本后通过将Effect设置为null来解决这个问题。您已经修改了GraphicsContext的几个属性,然后必须手动恢复它们。有时,一个GraphicsContext可能被传递给你的代码,但是你不想修改它的现有状态。

save()方法存储堆栈上GraphicsContext的当前状态。restore()方法将GraphicsContext的状态恢复到上次保存的状态。图 22-4 显示了这样的结果。您可以使用以下方法解决该问题:

img/336502_2_En_22_Fig4_HTML.png

图 22-4

使用save()restore()方法绘制形状和文本

Canvas canvas = new Canvas(200, 120);
GraphicsContext gc = canvas.getGraphicsContext2D();

gc.strokeRect(10, 10, 50, 20);

// Save the current state
gc.save();

// Modify the current state to add an effect and darw the text
gc.setEffect(new Reflection());
gc.strokeText("Chatar", 70, 20);

// Restore the state what it was when the last save() was called and draw the
// second rectangle
gc.restore();
gc.strokeRect(120, 10, 50, 20);

一个画布绘画的例子

清单 22-1 中的程序展示了如何在画布上绘制基本的形状、文本、图像和行像素。图 22-5 显示了所有绘图的结果画布。

img/336502_2_En_22_Fig5_HTML.png

图 22-5

上面绘制有形状、文本、图像和原始像素的画布

// CanvasTest.java
package com.jdojo.canvas;

import com.jdojo.util.ResourceUtil;
import java.nio.ByteBuffer;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.image.Image;
import javafx.scene.image.PixelFormat;
import javafx.scene.image.PixelWriter;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;

public class CanvasTest extends Application {

        private static final int RECT_WIDTH = 20;
        private static final int RECT_HEIGHT = 20;

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

        @Override
        public void start(Stage stage) {
               Canvas canvas = new Canvas(400, 100);
               GraphicsContext gc = canvas.getGraphicsContext2D();

               // Set line width and fill color
               gc.setLineWidth(2.0);
               gc.setFill(Color.RED);

               // Draw a rounded rectangle
               gc.strokeRoundRect(10, 10, 50, 50, 10, 10);

               // Fill an oval
               gc.fillOval(70, 10, 50, 20);

               // Draw text
               gc.strokeText("Hello Canvas", 10, 85);

               // Draw an Image
               String imagePath =
                        ResourceUtil.getResourceURLStr("picture/ksharan.jpg");
               Image image = new Image(imagePath);
               gc.drawImage(image, 130, 10, 60, 80);

               // Write custom pixels to create a pattern
               writePixels(gc);

               Pane root = new Pane();
               root.getChildren().add(canvas);
               Scene scene = new Scene(root);
               stage.setScene(scene);
               stage.setTitle("Drawing on a Canvas");
               stage.show();
        }

        private void writePixels(GraphicsContext gc) {
               byte[] pixels = this.getPixelsData();
               PixelWriter pixelWriter = gc.getPixelWriter();

               // Our data is in BYTE_RGB format
               PixelFormat<ByteBuffer> pixelFormat =
                        PixelFormat.getByteRgbInstance();

               int spacing = 5;
               int imageWidth = 200;
               int imageHeight = 100;

               // Roughly compute the number of rows and columns
               int rows = imageHeight/(RECT_HEIGHT + spacing);
               int columns = imageWidth/(RECT_WIDTH + spacing);

               // Write the pixels to the canvas
               for (int y = 0; y < rows; y++) {
                   for (int x = 0; x < columns; x++) {
                       int xPos = 200 + x * (RECT_WIDTH + spacing);
                       int yPos = y * (RECT_HEIGHT + spacing);
                       pixelWriter.setPixels(xPos, yPos,
                           RECT_WIDTH, RECT_HEIGHT,
                          pixelFormat,
                          pixels, 0,
                          RECT_WIDTH * 3);
                   }
               }
        }

        private byte[] getPixelsData() {
               // Each pixel in the w X h region will take 3 bytes
               byte[] pixels = new byte[RECT_WIDTH * RECT_HEIGHT * 3];

               // Height to width ration

               double ratio = 1.0 * RECT_HEIGHT/RECT_WIDTH;

               // Generate pixel data
               for (int y = 0; y < RECT_HEIGHT; y++) {
                   for (int x = 0; x < RECT_WIDTH; x++) {
                       int i = y * RECT_WIDTH * 3 + x * 3;
                       if (x <= y/ratio) {
                          pixels[i] = -1;  // red -1 means
                                           // 255 (-1 & 0xff = 255)
                          pixels[i+1] = 0; // green = 0
                          pixels[i+2] = 0; // blue = 0
                       } else {
                          pixels[i] = 0;    // red = 0
                          pixels[i+1] = -1; // Green 255
                          pixels[i+2] = 0;  // blue = 0
                       }
                   }
               }
               return pixels;
        }
}

Listing 22-1Drawing on a Canvas

摘要

通过javafx.scene.canvas包,JavaFX 提供了 Canvas API,该 API 提供了一个绘图表面来使用绘图命令绘制形状、图像和文本。该 API 还提供了对绘图表面的像素级访问,您可以在表面上写入任何像素。这个 API 只包含两个类:CanvasGraphicsContext。画布是位图图像,用作绘图表面。Canvas类的一个实例代表一个画布。它继承自Node类。因此,画布是一个节点。可以将它添加到场景图中,并对其应用效果和变换。画布具有与之相关联的图形上下文,用于向画布发出绘制命令。GraphicsContext类的一个实例代表一个图形上下文。

Canvas类包含一个返回GraphicsContext类实例的getGraphicsContext2D()方法。获得画布的GraphicsContext后,向执行绘制的GraphicsContext发出绘制命令。

超出画布边界的绘图将被剪裁。画布使用缓冲区。绘图命令将必要的参数推送到缓冲区。在画布被添加到场景图之前,可以从任何一个线程使用画布的GraphicsContext。一旦画布被添加到场景图形中,图形上下文应该只在 JavaFX 应用程序线程上使用。GraphicsContext类包含绘制以下类型对象的方法:基本形状、文本、路径、图像和像素。

下一章将讨论如何使用拖放手势在同一个 JavaFX 应用程序的节点之间、两个不同的 JavaFX 应用程序之间以及 JavaFX 应用程序和本机应用程序之间传输数据。

二十三、理解拖放

在本章中,您将学习:

  • 什么是按下-拖动-释放手势

  • 如何使用拖板来促进数据传输

  • 如何启动和检测拖放动作

  • 如何使用拖放动作将数据从源传输到目标

  • 如何使用拖放手势传输图像

  • 如何使用拖放动作在源和目标之间传输自定义数据

本章的例子在com.jdojo.dnd包中。为了让它们工作,您必须在module-info.java文件中添加相应的一行:

...
opens com.jdojo.dnd to javafx.graphics, javafx.base;
...

什么是按下-拖动-释放手势?

按下-拖动-释放手势是按下鼠标按钮、用按下的按钮拖动鼠标并释放按钮的用户动作。手势可以在场景或节点上启动。几个节点和场景可以参与单个按压-拖动-释放手势。该手势能够生成不同类型的事件,并将这些事件传递给不同的节点。生成的事件和接收事件的节点的类型取决于手势的目的。可以出于不同的目的拖动节点:

  • 您可能希望通过拖动节点的边界来更改节点的形状,或者通过将其拖动到新位置来移动节点。在这种情况下,手势只涉及一个节点:启动手势的节点。

  • 您可能希望将一个节点拖放到另一个节点上,以某种方式连接它们,例如,在流程图中用符号连接两个节点。在这种情况下,拖动手势涉及多个节点。当源节点被放到目标节点上时,会发生一个动作。

  • 您可以将一个节点拖放到另一个节点上,将数据从源节点传输到目标节点。在这种情况下,拖动手势涉及多个节点。当源节点断开时,会发生数据传输。

JavaFX 支持三种类型的拖动手势:

  • 简单的按下-拖动-释放手势

  • 完全按下-拖动-释放手势

  • 拖放手势

本章将主要关注第三种手势:拖放手势。要全面了解拖放手势,理解前两种手势是非常重要的。我将简要讨论前两种类型的手势,每种类型都有一个简单的例子。

简单的按下-拖动-释放手势

简单的按下-拖动-释放手势是默认的拖动手势。当拖动笔势只涉及一个节点(笔势在其上启动的节点)时使用。在拖动手势过程中,所有的MouseDragEvent类型——鼠标拖动输入、鼠标拖动结束、鼠标拖动退出、鼠标和鼠标拖动释放——都只传递给手势源节点。在这种情况下,当按下鼠标按钮时,会选取最顶层的节点,所有后续的鼠标事件都会传递到该节点,直到松开鼠标按钮。当鼠标被拖动到另一个节点上时,手势开始所在的节点仍然在光标下,因此,在释放鼠标按钮之前,没有其他节点接收事件。

清单 23-1 中的程序演示了一个简单的按压-拖动-释放手势的例子。它向场景添加了两个TextFields:一个称为源节点,另一个称为目标节点。事件处理程序被添加到这两个节点中。目标节点添加了MouseDragEvent处理程序来检测其上的任何鼠标拖动事件。运行程序,在源节点上按下鼠标按钮,将其拖到目标节点上,最后,释放鼠标按钮。下面的输出显示源节点接收所有鼠标拖动的事件。目标节点不接收任何鼠标拖动事件。这是简单的按下-拖动-释放手势的情况,其中启动拖动手势的节点接收所有鼠标拖动事件。

// SimplePressDragRelease.java
package com.jdojo.dnd;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
import javafx.stage.Stage;

public class SimplePressDragRelease extends Application {
        TextField sourceFld = new TextField("Source Node");
        TextField targetFld = new TextField("Target node");

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

        @Override
        public void start(Stage stage) {
                // Build the UI
                GridPane root = getUI();

                // Add event handlers
                this.addEventHandlers();

                Scene scene = new Scene(root);
                stage.setScene(scene);
                stage.setTitle("A simple press-drag-release gesture");
                stage.show();
        }

        private GridPane getUI() {
                GridPane pane = new GridPane();
                pane.setHgap(5);
                pane.setVgap(20);
                pane.addRow(0, new Label("Source Node:"), sourceFld);
                pane.addRow(1, new Label("Target Node:"), targetFld);
                return pane;
        }

        private void addEventHandlers() {
                // Add mouse event handlers for the source
                sourceFld.setOnMousePressed(e ->
                         print("Source: pressed"));
                sourceFld.setOnMouseDragged(e ->
                         print("Source: dragged"));
                sourceFld.setOnDragDetected(e ->
                         print("Source: dragged detected"));
                sourceFld.setOnMouseReleased(e ->
                         print("Source: released"));

                // Add mouse event handlers for the target
                targetFld.setOnMouseDragEntered(e ->
                         print("Target: drag entered"));
                targetFld.setOnMouseDragOver(e ->
                         print("Target: drag over"));
                targetFld.setOnMouseDragReleased(e ->
                         print("Target: drag released"));
                targetFld.setOnMouseDragExited(e ->
                         print("Target: drag exited"));
        }

        private void print(String msg) {
                System.out.println(msg);
        }
}

Source: Mouse pressed
Source: Mouse dragged
Source: Mouse dragged detected
Source: Mouse dragged
Source: Mouse dragged
...
Source: Mouse released

Listing 23-1Demonstrating a Simple Press-Drag-Release Gesture

请注意,拖动鼠标后会生成一次检测到拖动事件。MouseEvent对象有一个dragDetect标志,可以在鼠标按下和鼠标拖动事件中设置。如果设置为 true,则生成的后续事件是检测到拖动事件。默认情况下是在鼠标拖动事件之后生成它。如果您想在鼠标按下事件之后生成它,而不是鼠标拖动事件之后,您需要修改事件处理程序:

sourceFld.setOnMousePressed(e -> {
        print("Source: Mouse pressed");

        // Generate drag detect event after the current mouse pressed event
        e.setDragDetect(true);
});

sourceFld.setOnMouseDragged(e -> {
        print("Source: Mouse dragged");

        // Suppress the drag detected default event generation after mouse
          // dragged
        e.setDragDetect(false);
});

完全按下-拖动-释放手势

当拖动手势的源节点接收到检测到拖动事件时,您可以通过调用源节点上的startFullDrag()方法来启动一个全按-拖动-释放手势。startFullDrag()方法存在于NodeScene类中,允许你为一个节点和一个场景启动一个完整的按下-拖动-释放手势。在这次讨论中,我将只使用术语节点。

Tip

只能从检测到拖动的事件处理程序中调用startFullDrag()方法。从任何其他地方调用这个方法都会抛出一个IllegalStateException

您需要再做一次设置才能看到完整的按下-拖动-释放手势。拖动动作的源节点仍将接收所有鼠标拖动的事件,因为它在发生拖动时位于光标之下。您需要将手势源的mouseTransparent属性设置为 false,这样它下面的节点将被选取,鼠标拖动的事件将被传递到该节点。在鼠标按下事件中将此属性设置为 true,在鼠标释放事件中将它设置回 false。

清单 23-2 中的程序演示了一个完整的按下-拖动-释放手势。该程序类似于清单 23-1 中所示的程序,除了以下几点:

  • 在源节点的鼠标按下事件处理程序中,源节点的mouseTransparent属性被设置为 false。它在释放鼠标的事件处理程序中被设置回 true。

  • 在检测到拖动的事件处理程序中,在源节点上调用startFullDrag()方法。

运行程序,在源节点上按下鼠标按钮,将其拖到目标节点上,最后,释放鼠标按钮。下面的输出显示,当鼠标被拖动到其边界内时,目标节点接收到鼠标拖动事件。这是完全按下-拖动-释放手势的情况,其中发生鼠标拖动的节点接收鼠标拖动事件。

// FullPressDragRelease.java
// ...find in the book's download area.

Source: Mouse pressed
Source: Mouse dragged
Source: Mouse dragged
Source: Mouse dragged detected
Source: Mouse dragged
Source: Mouse dragged
Target: drag entered
Target: drag over
Source: Mouse dragged
Target: drag over
Target: drag released
Source: Mouse released
Target: drag exited

Listing 23-2Demonstrating a Full Press-Drag-Release Gesture

拖放手势

第三种类型的拖动手势称为拖放手势,这是一种结合了鼠标移动和按下鼠标按钮的用户动作。用于将数据从手势源传输到手势目标。拖放动作允许将数据从

  • 一个节点到另一个节点

  • 场景的节点

  • 一幕接一幕

  • 场景到节点

源和目标可以在同一个 Java 或 JavaFX 应用程序中,也可以在两个不同的 Java 或 JavaFX 应用程序中。JavaFX 应用程序和本机应用程序也可以参与手势,例如:

  • 您可以将文本从 Microsoft Word 应用程序拖到 JavaFX 应用程序来填充TextArea,反之亦然。

  • 您可以将图像文件从 Windows 资源管理器中拖放到 JavaFX 应用程序中的ImageView上。ImageView可以显示图像。

  • 您可以从 Windows 资源管理器中拖放一个文本文件到 JavaFX 应用程序中的TextArea上。TextArea将读取文件并显示其内容。

执行拖放动作涉及几个步骤:

  • 在节点上按下了鼠标按钮。

  • 按住按钮拖动鼠标。

  • 该节点接收拖动检测事件。

  • 通过调用startDragAndDrop()方法在节点上启动拖放动作,使节点成为动作源。来自源节点的数据放在一个 dragboard 中。

  • 一旦系统切换到拖放手势,它就停止传送MouseEvents并开始传送DragEvents

  • 手势源被拖到潜在的手势目标上。潜在的手势目标检查它是否接受放置在 dragboard 中的数据。如果它接受数据,它可能成为实际的手势目标。节点指示它是否接受它的一个DragEvent处理程序中的数据。

  • 用户释放手势目标上按下的按钮,向其发送拖放事件。

  • 手势目标使用来自 dragboard 的数据。

  • drag-done 事件被发送到手势源,指示拖放手势完成。

我将在接下来的小节中详细讨论所有这些步骤。支持拖放手势的类包含在javafx.scene.input包中。

了解数据传输模式

在拖放手势中,数据可以通过三种模式传输:

  • 复制

  • 移动

复制模式表示数据将从手势源复制到手势目标。您可以将一个TextField拖放到另一个TextField上。后者获得前者中包含的文本的副本。

移动模式表示数据将从手势源移动到手势目标。您可以将一个TextField拖放到另一个TextField上。前者中的文本随后被移到后者中。

链接模式表示手势目标将创建一个链接(或引用)到正在传输的数据。“链接”的实际含义取决于应用。您可以在链接模式下将 URL 拖放到WebView中。然后,WebView加载 URL 内容。

三种数据传输模式由TransferMode枚举中的以下三个常量表示:

  • TransferMode.COPY

  • TransferMode.MOVE

  • TransferMode.LINK

有时,您可能需要三种传输模式的组合。TransferMode枚举包含三个方便的静态字段,它们是枚举常量的数组:

  • TransferMode[] ANY

  • TransferMode[] COPY_OR_MOVE

  • TransferMode[] NONE

ANY字段是一个由COPYMOVELINK枚举常量组成的数组。COPY_OR_MOVE字段是COPYMOVE枚举常量的数组。NONE常量是一个空数组。

每个拖放动作都包括使用TransferMode枚举常量。手势源指定其支持的数据传输模式。手势目标指定它接受数据传输的模式。

了解拖板

在拖放数据传输中,手势源和手势目标彼此不认识。事实上,它们可能属于两个不同的应用程序:两个 JavaFX 应用程序,或者一个 JavaFX 和一个 native。如果手势源和目标彼此不认识,它们之间的数据传输是如何发生的?在现实世界中,需要一个中介来促进两个未知方之间的交易。在拖放手势中,也使用中介来促进数据传输。

拖板充当手势源和手势目标之间的中介。拖板是保存正在传输的数据的存储设备。手势源将数据放入拖板中;dragboard 可供手势目标使用,因此它可以检查可用于传输的内容类型。当手势目标准备好传输数据时,它从 dragboard 获取数据。图 23-1 显示了拖板所扮演的角色。

img/336502_2_En_23_Fig1_HTML.png

图 23-1

拖放手势中的数据传输机制

Dragboard类的一个实例代表一个 dragboard。该类继承自Clipboard类。一个Clipboard类的实例代表一个操作系统剪贴板。通常,操作系统在剪切、复制和粘贴操作中使用剪贴板来存储数据。使用Clipboard类的静态getSystemClipboard()方法可以得到操作系统通用剪贴板的引用:

Clipboard systemClipboard = Clipboard.getSystemClipboard();

您可以将数据放在系统剪贴板中,系统中的所有应用程序都可以访问这些数据。您可以读取放置在系统剪贴板中的数据,这些数据可以由任何应用程序放置在那里。剪贴板可以存储不同类型的数据,例如,RTF 文本、纯文本、HTML、URL、图像或文件。该类包含几个方法来检查剪贴板中是否有特定格式的数据。如果特定格式的数据可用,这些方法返回true。例如,如果剪贴板包含一个普通字符串,hasString()方法返回truehasRtf()方法为富文本格式的文本返回true。该类包含以特定格式检索数据的方法。例如,getString()方法以纯文本格式返回数据;getHtml()返回 HTML 文本;getImage()返回图像;等等。clear()方法清除剪贴板。

Tip

您不能直接创建Clipboard类的实例。剪贴板是为了存储一个概念上的项目。概念一词意味着剪贴板中的数据可能以不同的格式存储,表示同一项。例如,您可以存储 RTF 文本及其纯文本版本。在这种情况下,剪贴板有相同项目的两个不同格式的副本。

剪贴板不限于仅存储固定数量的数据类型。任何可序列化的数据都可以存储在剪贴板上。存储在剪贴板上的数据具有相关联的数据格式。DataFormat类的一个实例代表一种数据格式。DataFormat类包含六个静态字段来表示常用的数据格式:

  • FILES

  • HTML

  • IMAGE

  • PLAIN_TEXT

  • RTF

  • URL

FILES表示一列java.io.File对象。HTML代表一个 HTML 格式的字符串。IMAGE表示特定于*台的图像类型。PLAIN_TEXT代表一个纯文本字符串。RTF代表一个 RTF 格式的字符串。URL表示一个编码为字符串的 URL。

您可能希望将剪贴板中的数据存储为不同于前面列出的格式。您可以创建一个DataFormat对象来表示任意格式。您需要为您的数据格式指定一个 mime 类型列表。以下语句创建一个将jdojo/personjdojo/personlist作为 mime 类型的DataFormat:

DataFormat myFormat = new DataFormat("jdojo/person", "jdojo/person");

Clipboard类提供了以下方法来处理数据及其格式:

  • boolean setContent(Map<DataFormat,Object> content)

  • Object getContent(DataFormat dataFormat)

剪贴板的内容是一个以DataFormat为键,以数据为值的映射。如果剪贴板中没有特定数据格式的数据,则getContent()方法返回null。以下代码片段存储 HTML 和纯文本版本的数据,并在以后检索这两种格式的数据:

// Store text in HTML and plain-text formats in the system clipboard
Clipboard clipboard = Clipboard.getSystemClipboard();

Map<DataFormat,Object> data = new HashMap<>();
data.put(DataFormat.HTML, "<b>Yahoo!</b>");
data.put(DataFormat.PLAIN_TEXT, "Yahoo!");
clipboard.setContent(data);
...

// Try reading HTML text and plain text from the clipboard
If (clipboard.hasHtml()) {
        String htmlText = (String)clipboard.getContent(DataFormat.HTML);
        System.out.println(htmlText);
}

If (clipboard.hasString()) {
        String plainText = (String)clipboard.getContent(DataFormat.PLAIN_TEXT);
        System.out.println(plainText);
}

准备存储在剪贴板中的数据需要编写一点臃肿的代码。ClipboardContent类的一个实例表示剪贴板的内容,它使得使用剪贴板数据变得更加容易。该类继承自HashMap<DataFormat,Object>类。它以putXxx()getXxx()的形式为常用的数据类型提供了方便的方法。下面的代码片段重写了前面的逻辑,将数据存储到剪贴板中。检索数据的逻辑保持不变:

Clipboard clipboard = Clipboard.getSystemClipboard();
ClipboardContent content = new ClipboardContent();
content.putHtml("<b>Yahoo!</b>");
content.putString("Yahoo!");
clipboard.setContent(content);

Dragboard类继承了Clipboard类中所有可用的公共方法。它添加了以下方法:

  • Set<TransferMode> getTransferModes()

  • void setDragView(Image image)

  • void setDragView(Image image, double offsetX, double offsetY)

  • void setDragViewOffsetX(double offsetX)

  • void setDragViewOffsetY(double offsetY)

  • Image getDragView()

  • Double getDragViewOffsetX()

  • double getDragViewOffsetY()

getTransferModes()方法返回手势目标支持的传输模式集。setDragView()方法将图像设置为拖动视图。拖动手势源时会显示图像。偏移量是光标在图像上的 x 和 y 位置。其他方法包括获取拖动视图图像和光标偏移量。

Tip

dragboard 是一种用于拖放动作的特殊系统剪贴板。您不能显式创建 dragboard。每当需要使用 dragboard 时,它的引用将作为方法的返回值或事件对象的属性提供。例如,DragEvent类包含一个getDragboard()方法,该方法返回包含被传输数据的Dragboard的引用。

示例应用程序

在接下来的部分中,我将详细讨论拖放动作的步骤,并且您将构建一个示例应用程序。应用程序将有两个TextFields显示在一个场景中。一个文本字段称为源节点,另一个称为目标节点。用户可以将源节点拖放到目标节点上。完成手势后,来自源节点的文本被传输(复制或移动)到目标节点。我将在讨论中提到这些节点。它们声明如下:

TextField sourceFld = new TextField("Source node");
TextField targetFld = new TextField("Target node");

启动拖放手势

拖放手势的第一步是将简单的按下-拖动-释放手势转换为拖放手势。这是在手势源的鼠标拖动检测事件处理程序中完成的。在手势源上调用startDragAndDrop()方法会启动一个拖放手势。该方法在NodeScene类中可用,因此一个节点和一个场景可以是拖放手势的手势源。方法签名是

Dragboard startDragAndDrop(TransferMode... transferModes)

该方法接受笔势源支持的传输模式列表,并返回一个 dragboard。手势源需要用它想要传输的数据填充 dragboard。下面的代码片段启动一个拖放动作,将源TextField文本复制到 dragboard,并使用该事件。拖放手势仅在TextField包含文本时启动:

sourceFld.setOnDragDetected((MouseEvent e) -> {
        // User can drag only when there is text in the source field
        String sourceText = sourceFld.getText();
        if (sourceText == null || sourceText.trim().equals("")) {
                e.consume();
                return;
        }

        // Initiate a drag-and-drop gesture
        Dragboard dragboard =
              sourceFld.startDragAndDrop(TransferMode.COPY_OR_MOVE);

        // Add the source text to the Dragboard
        ClipboardContent content = new ClipboardContent();
        content.putString(sourceText);
        dragboard.setContent(content);

        e.consume();
});

检测拖动手势

一旦启动了拖放手势,您就可以将手势源拖到任何其他节点上。手势源已经将数据放入 dragboard,声明它支持的传输模式。现在是潜在的手势目标声明它们是否接受手势源提供的数据传输的时候了。请注意,可能有多个潜在的手势目标。当手势源放在其中一个目标上时,它将成为实际的手势目标。

潜在手势目标接收几种类型的拖动事件:

  • 当手势源进入它的边界时,它接收一个拖动输入事件。

  • 当在它的边界内拖动手势源时,它接收一个拖动事件。

  • 当笔势源退出其边界时,它接收一个拖动退出事件。

  • 当通过释放鼠标按钮将手势源放在它上面时,它接收一个拖放事件。

在拖动事件处理程序中,潜在的手势目标需要通过调用DragEventacceptTransferModes(TransferMode... modes)方法来声明它打算参与拖放手势。通常,潜在目标在声明是否接受传输模式之前会检查 dragboard 的内容。下面的代码片段实现了这一点。目标TextField检查 dragboard 中的纯文本。它包含纯文本,因此目标声明它接受COPYMOVE传输模式:

targetFld.setOnDragOver((DragEvent e) -> {
        // If drag board has a string, let the event know that the
           // target accepts copy and move transfer modes
        Dragboard dragboard = e.getDragboard();

        if(dragboard.hasString()) {
                e.acceptTransferModes(TransferMode.COPY_OR_MOVE);
        }

        e.consume();
});

将源放到目标上

如果潜在手势目标接受手势源支持的转移模式,则手势源可被放到目标上。当手势源仍在目标上方时,通过释放鼠标按钮来完成放下。当手势源放到目标上时,该目标成为实际的手势目标。实际的手势目标接收拖放事件。您需要为手势目标添加一个拖放事件处理程序,它在其中执行两个任务:

  • 它访问 dragboard 中的数据。

  • 它调用DragEvent对象的setDropCompleted(boolean isTransferDone)方法。

将 true 传递给方法指示数据传输成功。传递 false 表示数据传输不成功。调用此方法后,无法访问 dragboard。

以下代码片段执行数据传输并设置适当的完成标志:

targetFld.setOnDragDropped((DragEvent e) -> {
        // Transfer the data to the target
        Dragboard dragboard = e.getDragboard();
        if(dragboard.hasString()) {
                String text = dragboard.getString();
                targetFld.setText(text);

                // Data transfer is successful
                e.setDropCompleted(true);
        } else {
                // Data transfer is not successful
                e.setDropCompleted(false);
        }

        e.consume();
});

完成拖放动作

放下手势源后,它会收到一个 drag-done 事件。DragEvent对象包含一个getTransferMode()方法。当从 drag-done 事件处理程序调用它时,它返回用于数据传输的传输模式。根据传输模式,您可以清除或保留手势源的内容。例如,如果传输模式是MOVE,最好清除源内容,让用户真正感受到数据移动。

您可能想知道是什么决定了数据传输模式。在这个例子中,手势源和目标都支持COPYMOVE。当目标在拖放事件中从 dragboard 访问数据时,它没有设置任何传输模式。系统根据某些键的状态以及源和目标来确定数据传输模式。例如,当您将一个TextField拖放到另一个TextField上时,默认的数据传输模式是MOVE。当按住 Ctrl 键执行相同的拖放操作时,会使用COPY模式。

如果getTransferMode()方法返回nullTransferMode.ONE,则表明没有发生数据传输。下面的代码片段处理源TextField的拖动完成事件。如果数据传输模式是MOVE,源文本被清除:

sourceFld.setOnDragDone((DragEvent e) -> {
        // Check how the data transfer happened. If it was moved, clear the
           // text in the source.
        TransferMode modeUsed = e.getTransferMode();

        if (modeUsed == TransferMode.MOVE) {
                sourceFld.setText("");
        }

        e.consume();
});

这就完成了对拖放手势的处理。如果你需要更多关于参与拖放动作的各方的信息,请参考DragEvent类的 API 文档。例如,使用getGestureSource()getGestureTarget()方法分别获取手势源和目标的引用。

提供视觉线索

有几种方法可以在拖放动作中提供视觉线索:

  • 在拖动手势期间,系统会在光标下提供一个图标。图标会根据系统确定的传输模式以及拖动目标是否是拖放手势的潜在目标而变化。

  • 您可以通过更改潜在目标的可视外观,为其拖动进入和拖动退出事件编写代码。例如,在拖动输入的事件处理程序中,如果允许数据传输,您可以将潜在目标的背景颜色更改为绿色,如果不允许,则更改为红色。在拖动退出事件处理程序中,您可以将背景颜色改回正常颜色。

  • 您可以在手势的 drag-detected 事件处理程序中的 dragboard 中设置拖动视图。拖动视图是一个图像。例如,您可以拍摄被拖动的节点或部分节点的快照,并将其设置为拖动视图。

一个完整的拖放示例

清单 23-3 中的程序有这个例子的完整源代码。显示如图 23-2 所示的窗口。您可以拖动手势源TextField并将其放到目标TextField上。源中的文本将被复制或移动到目标中。传输模式取决于系统。例如,在 Windows 上,在放下时按下 Ctrl 键将复制文本,在没有按下 Ctrl 键的情况下放下将移动文本。请注意,在拖动动作过程中,拖动图标会发生变化。当您放下信号源时,图标会提示您将会发生何种数据传输。例如,当您将源拖到不接受源提供的数据传输的目标上时,会显示一个“不允许”图标,即一个带有斜实线的圆圈。

img/336502_2_En_23_Fig2_HTML.png

图 23-2

允许使用拖放手势将文本从一个TextField转移到另一个的场景

// DragAndDropTest.java
// ...find in the book's download area.

Listing 23-3Performing a Drag-and-Drop Gesture

传输图像

拖放手势允许您传输图像。图像可以放在拖板上。您也可以在拖板上放置一个指向图像位置的 URL 或文件。让我们开发一个简单的应用程序来演示图像数据传输。要传输图像,用户可以将以下内容拖放到场景中:

  • 图像

  • 图像文件

  • 指向图像的 URL

清单 23-4 中的程序打开一个窗口,有一条文本消息、一个空的ImageView和一个按钮。ImageView将显示拖放的图像。使用按钮清除图像。

整个场景都是拖放动作的潜在目标。为场景设置了一个拖动事件处理程序。它检查拖板是否包含图像、文件列表或 URL。如果它在 dragboard 中找到这些数据类型中的一种,它将报告它将接受任何数据传输模式。在场景的拖放事件处理程序中,程序尝试按顺序读取图像数据、文件列表和 URL。如果是文件列表,那么查看每个文件的 mime 类型,看文件名是否以image/开头。您使用带有图像 mime 类型的第一个文件,忽略其余的文件。如果它是一个 URL,您只需尝试从它创建一个Image对象。您可以用不同的方式使用该应用程序:

  • 运行程序并在浏览器中打开 HTML 文件drag_and_drop.html。该文件包含在src/resources/html目录中。HTML 文件包含两个链接:一个指向本地图像文件,另一个指向远程图像文件。将链接拖放到场景中。该场景将显示链接所引用的图像。从网页中拖放图像。场景将显示图像。(图像的拖放在 Mozilla 和 Google Chrome 浏览器中运行良好,但在 Windows 资源管理器中就不行了。)

  • 打开文件资源管理器,例如 Windows 上的 Windows 资源管理器。选择一个图像文件,并将该文件拖放到场景中。场景将显示文件中的图像。您可以放下多个文件,但是场景将只显示其中一个文件的图像。

您可以通过允许用户将多个文件拖到场景中并在一个TilePane中显示它们来增强应用程序。您还可以添加更多关于拖放动作的错误检查和反馈给用户。

// ImageDragAndDrop.java
// ...find in the book's download area.

Listing 23-4Transferring an Image Using a Drag-and-Drop Gesture

传输自定义数据类型

如果数据是Serializable,您可以使用拖放手势传输任何格式的数据。在这一节中,我将演示如何传输自定义数据。你要转一个ArrayList<Item>Item级如清单 23-5 所示;是Serializable。这个类非常简单。它包含一个私有字段及其 getter 和 setter 方法。

// Item.java
package com.jdojo.dnd;

import java.io.Serializable;

public class Item implements Serializable {
        private String name = "Unknown";

        public Item(String name) {
                this.name = name;
        }

        public String getName() {
                return name;
        }

        public void setName(String name) {
                this.name = name;
        }

        @Override
        public String toString() {
                return name;
        }
}

Listing 23-5Using a Custom Data Type in Data Transfer

清单 23-6 中的程序展示了如何在拖放动作中使用自定义数据格式。显示如图 23-3 所示的窗口。该窗口包含两个ListViews。最初,只有一个ListViews填充了一个项目列表。两个ListViews都支持多选。您可以选择一个ListView中的项目,并将其拖放到另一个ListView中。将根据系统确定的传输模式复制或移动选定的项目。例如,在 Windows 上,默认情况下会移动项目。如果您在拖放时按下 Ctrl 键,项目将被复制。

img/336502_2_En_23_Fig3_HTML.png

图 23-3

在两个ListViews之间传送所选项目的列表

// CustomDataTransfer.java

// ...find in the book's download area.

Listing 23-6Transferring Custom Data Using a Drag-and-Drop Gesture

大部分程序和你以前看过的差不多。区别在于如何在 dragboard 中存储和检索ArrayList<Item>

您为该数据传输定义了一个新的数据格式,因为该数据不符合任何作为DataFormat类中的常量的类别。您必须将数据定义为常量,如以下代码所示:

// Our custom Data Format
static final DataFormat ITEM_LIST = new DataFormat("jdojo/itemlist");

现在,您已经为数据格式给出了一个惟一的 mime 类型jdojo/itemlist

在 drag-detected 事件中,您需要将选定项目的列表存储到 dragboard 上。下面的代码片段在dragDetected()方法中存储作业。请注意,在拖板上存储数据时,您使用了新的数据格式:

ArrayList<Item> selectedItems = this.getSelectedItems(listView);
ClipboardContent content = new ClipboardContent();
content.put(ITEM_LIST, selectedItems);
dragboard.setContent(content);

在拖过事件中,如果ListView没有被拖过自身,并且拖板包含ITEM_LIST数据格式的数据,ListView声明它接受COPYMOVE传输。下面的代码片段在dragOver()方法中完成了这项工作:

Dragboard dragboard = e.getDragboard();
if (e.getGestureSource() != listView && dragboard.hasContent(ITEM_LIST)) {
        e.acceptTransferModes(TransferMode.COPY_OR_MOVE);
}

最后,当源拖放到目标上时,您需要从 dragboard 中读取数据。您需要使用 dragboard 的getContent()方法,将ITEM_LIST指定为数据格式。返回的结果需要被强制转换为ArrayList<Item>。下面的代码片段在dragDropped()方法中完成了这项工作:

Dragboard dragboard = e.getDragboard();
if(dragboard.hasContent(ITEM_LIST)) {
        ArrayList<Item> list =
              (ArrayList<Item>)dragboard.getContent(ITEM_LIST);
        listView.getItems().addAll(list);

        // Data transfer is successful
        dragCompleted = true;
}

最后,在用dragDone()方法实现的拖动完成事件处理程序中,如果将MOVE用作传输模式,则从源ListView中移除选定的项目。注意,您已经使用了一个ArrayList<Item>,因为ArrayListItem类都是可序列化的。

摘要

按下-拖动-释放手势是按下鼠标按钮、用按下的按钮拖动鼠标并释放按钮的用户动作。手势可以在场景或节点上启动。几个节点和场景可以参与单个按压-拖动-释放手势。该手势能够生成不同类型的事件,并将这些事件传递给不同的节点。生成事件的类型和接收事件的节点取决于手势的目的。

JavaFX 支持三种类型的拖动手势:简单的按下-拖动-释放手势、完全按下-拖动-释放手势和拖放手势。

简单的按下-拖动-释放手势是默认的拖动手势。当拖动笔势只涉及一个节点(笔势在其上启动的节点)时使用。在拖动手势过程中,所有的MouseDragEvent类型——鼠标拖动输入、鼠标拖动结束、鼠标拖动退出、鼠标和鼠标拖动释放——都只传递给手势源节点。

当拖动手势的源节点接收到检测到拖动的事件时,您可以通过调用源节点上的startFullDrag()方法来启动一个完整的按下-拖动-释放手势。startFullDrag()方法存在于NodeScene类中,允许你为一个节点和一个场景启动一个完整的按下-拖动-释放手势。

第三种类型的拖动手势称为拖放手势,这是一种将鼠标移动与按下鼠标按钮相结合的用户动作。它用于将数据从手势源传输到手势目标。在拖放动作中,数据可以通过三种模式传输:复制、移动和链接。复制模式表示数据将从手势源复制到手势目标。移动模式表示数据将从手势源移动到手势目标。链接模式指示手势目标将创建到正在传输的数据的链接(或引用)。“链接”的实际含义取决于应用。

在拖放数据传输中,手势源和手势目标彼此不认识,它们甚至可能属于两个不同的应用程序。dragboard 充当手势源和手势目标之间的中介。dragboard 是保存正在传输的数据的存储设备。手势源将数据放在拖板上;dragboard 可供手势目标使用,因此它可以检查可用于传输的内容类型。当手势目标准备好传输数据时,它从 dragboard 获取数据。

使用拖放手势,数据传输分三步进行:由源发起拖放手势,由目标检测拖动手势,以及将源放到目标上。在该手势期间,为源节点和目标节点生成不同类型的事件。您还可以通过在拖放动作中显示图标来提供视觉线索。只要数据是可序列化的,拖放动作支持传输任何类型的数据。

下一章讨论如何在 JavaFX 中处理并发操作。

二十四、理解 JavaFX 中的并发性

在本章中,您将学习:

  • 为什么在 JavaFX 中需要一个并发框架

  • Worker<V>接口如何表示并发任务

  • 如何运行一次性任务

  • 如何运行可重用任务

  • 如何运行计划任务

本章的例子在com.jdojo.concurrent包中。为了让它们工作,您必须在module-info.java文件中添加相应的一行:

...
opens com.jdojo.concurrent to javafx.graphics, javafx.base;
...

对并发框架的需求

Java(包括 JavaFX) GUI(图形用户界面)应用程序本质上是多线程的。多个线程执行不同的任务,以保持 UI 与用户操作同步。与 Swing 和 AWT 一样,JavaFX 使用一个称为 JavaFX 应用程序线程的线程来处理所有 UI 事件。场景图中表示 UI 的节点不是线程安全的。设计非线程安全的节点有利也有弊。它们更快,因为不涉及同步。缺点是需要从单个线程访问它们,以避免处于非法状态。JavaFX 设置了一个限制,即只能从一个线程(JavaFX 应用程序线程)访问实时场景图形。这个限制间接地强加了另一个限制,即 UI 事件不应该处理长时间运行的任务,因为它会使应用程序没有响应。用户将得到应用程序被挂起的印象。

清单 24-1 中的程序显示如图 24-1 所示的窗口。它包含三个控件:

img/336502_2_En_24_Fig1_HTML.png

图 24-1

无响应的用户界面示例

  • 显示任务进度的Label

  • 一个启动按钮来启动任务

  • 一个退出按钮,用于退出应用程序

// UnresponsiveUI.java
package com.jdojo.concurrent;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class UnresponsiveUI extends Application {
        Label statusLbl = new Label("Not Started...");
        Button startBtn = new Button("Start");
        Button exitBtn = new Button("Exit");

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

        @Override
        public void start(Stage stage) {
                // Add event handlers to the buttons
                startBtn.setOnAction(e -> runTask());
                exitBtn.setOnAction(e -> stage.close());

                HBox buttonBox = new HBox(5, startBtn, exitBtn);
                VBox root = new VBox(10, statusLbl, buttonBox);
                Scene scene = new Scene(root);
                stage.setScene(scene);
                stage.setTitle("An Unresponsive UI");
                stage.show();
        }

        public void runTask() {
                for(int i = 1; i <= 10; i++) {
                   try {
                     String status = "Processing " + i + " of " + 10;
                     statusLbl.setText(status);
                     System.out.println(status);
                     Thread.sleep(1000);
                   }
                   catch (InterruptedException e) {
                     e.printStackTrace();
                   }
                }
        }
}

Listing 24-1Performing a Long-Running Task in an Event Handler

程序很简单。当你点击开始按钮时,一个持续十秒钟的任务开始。任务的逻辑在runTask()方法中,该方法简单地运行一个循环十次。在循环内部,任务让当前线程(JavaFX 应用程序线程)休眠一秒钟。这个程序有两个问题。

点击开始按钮,并立即尝试点击退出按钮。点击退出按钮,直到任务完成才生效。一旦你点击开始按钮,你就不能在窗口上做任何其他事情,除了等待十秒钟任务完成。也就是说,应用程序在十秒钟内没有响应。这就是你将这个类命名为UnresponsiveUI的原因。

runTask()方法的循环中,程序在标准输出中打印任务的状态,并在窗口的Label中显示。您会在标准输出中看到更新的状态,但不会在Label中看到。

反复强调 JavaFX 中的所有 UI 事件处理程序都运行在一个线程上,这个线程就是 JavaFX 应用程序线程。当点击 Start 按钮时,在 JavaFX 应用线程中执行runTask()方法。当任务正在运行时点击 Exit 按钮时,会为 Exit 按钮生成一个ActionEvent事件,并在 JavaFX 应用程序线程上排队。作为开始按钮的ActionEvent处理程序的一部分,在线程完成运行runTask()方法之后,退出按钮的ActionEvent处理程序在同一线程上运行。

场景图形更新时会生成脉冲事件。脉冲事件处理程序也在 JavaFX 应用程序线程上运行。在循环内部,Labeltext属性被更新了十次,这产生了脉冲事件。然而,场景图没有被刷新以显示Label的最新文本,因为 JavaFX 应用程序线程忙于运行任务,它没有运行脉冲事件处理程序。

这两个问题都是因为只有一个线程来处理所有的 UI 事件处理程序,而您在开始按钮的ActionEvent处理程序中运行了一个长时间运行的任务。

解决办法是什么?你只有一个选择。您不能更改处理 UI 事件的单线程模型。不得在事件处理程序中运行长时间运行的任务。有时,作为用户操作的一部分,业务需要处理大型作业。解决方案是在一个或多个后台线程中运行长时间运行的任务,而不是在 JavaFX 应用程序线程中。

清单 24-2 中的程序是你第一次错误地尝试提供解决方案。Start按钮的ActionEvent处理程序调用startTask()方法,这将创建一个新线程并在新线程中运行runTask()方法。

// BadUI.java
package com.jdojo.concurrent;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class BadUI extends Application {
        Label statusLbl = new Label("Not Started...");
        Button startBtn = new Button("Start");
        Button exitBtn = new Button("Exit");

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

        @Override

        public void start(Stage stage) {
                // Add event handlers to the buttons
                startBtn.setOnAction(e -> startTask());
                exitBtn.setOnAction(e -> stage.close());

                HBox buttonBox = new HBox(5, startBtn, exitBtn);
                VBox root = new VBox(10, statusLbl, buttonBox);
                Scene scene = new Scene(root);
                stage.setScene(scene);
                stage.setTitle("A Bad UI");
                stage.show();
        }

        public void startTask() {
                // Create a Runnable
                Runnable task = () -> runTask();

                // Run the task in a background thread
                Thread backgroundThread = new Thread(task);

                // Terminate the running thread if the application exits
                backgroundThread.setDaemon(true);

                // Start the thread
                backgroundThread.start();
        }

        public void runTask() {
            for(int i = 1; i <= 10; i++) {
              try {
                String status = "Processing " + i + " of " + 10;
                statusLbl.setText(status);
                System.out.println(status);
                Thread.sleep(1000);
              }
              catch (InterruptedException e) {
                e.printStackTrace();
              }
            }
        }

}

Listing 24-2A Program Accessing a Live Scene Graph from a Non-JavaFX Application Thread

运行程序,点击开始按钮。引发运行时异常。异常的部分堆栈跟踪如下:

Exception in thread "Thread-4" java.lang.IllegalStateException:
Not on FX application thread; currentThread = Thread-4
  at com.sun.javafx.tk.Toolkit.checkFxUserThread(Toolkit.java:209)
  at com.sun.javafx.tk.quantum.QuantumToolkit.checkFxUserThread(
      QuantumToolkit.java:393)...
   at com.jdojo.concurrent.BadUI.runTask(BadUI.java:47)...

runTask()方法中的以下语句生成了异常:

statusLbl.setText(status);

JavaFX 运行时检查是否必须从 JavaFX 应用程序线程访问实时场景。runTask()方法在一个新线程上运行,名为 Thread-4,如堆栈跟踪所示,它不是 JavaFX 应用程序线程。上述语句从 JavaFX 应用程序线程之外的线程为作为实时场景图一部分的Label设置了text属性,这是不允许的。

如何从 JavaFX 应用程序线程之外的线程访问实时场景图?简单的答案是你不能。复杂的答案是,当一个线程想要访问一个实时场景图时,它需要运行 JavaFX 应用程序线程中访问场景图的那部分代码。javafx.application包中的Platform类提供了两个静态方法来处理 JavaFX 应用程序线程:

  • public static boolean isFxApplicationThread()

  • public static void runLater(Runnable runnable)

如果调用此方法的线程是 JavaFX 应用程序线程,则isFxApplicationThread()方法返回 true。否则,它返回 false。

runLater()方法调度指定的Runnable在未来某个未指定的时间在 JavaFX 应用程序线程上运行。

Tip

如果您有使用 Swing 的经验,那么 JavaFX 中的Platform.runLater()就是 Swing 中的SwingUtilities.invokeLater()的对等物。

让我们来解决BadUI应用程序中的问题。清单 24-3 中的程序是访问现场图形逻辑的正确实现。图 24-2 显示了程序显示窗口的快照。

img/336502_2_En_24_Fig2_HTML.png

图 24-2

在后台线程中运行任务并正确更新实时场景图形的 UI

// ResponsiveUI.java
package com.jdojo.concurrent;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class ResponsiveUI extends Application {
        Label statusLbl = new Label("Not Started...");
        Button startBtn = new Button("Start");
        Button exitBtn = new Button("Exit");

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

        @Override

        public void start(Stage stage) {
                // Add event handlers to the buttons
                startBtn.setOnAction(e -> startTask());
                exitBtn.setOnAction(e -> stage.close());

                HBox buttonBox = new HBox(5, startBtn, exitBtn);
                VBox root = new VBox(10, statusLbl, buttonBox);
                Scene scene = new Scene(root);
                stage.setScene(scene);
                stage.setTitle("A Responsive UI");
                stage.show();
        }

        public void startTask() {
                // Create a Runnable
                Runnable task = () -> runTask();

                // Run the task in a background thread
                Thread backgroundThread = new Thread(task);

                // Terminate the running thread if the application exits
                backgroundThread.setDaemon(true);

                // Start the thread
                backgroundThread.start();
        }

        public void runTask() {
          for(int i = 1; i <= 10; i++) {
            try {
              String status = "Processing " + i + " of " + 10;

              // Update the Label on the JavaFx Application Thread
              Platform.runLater(() -> statusLbl.setText(status));
              System.out.println(status);
              Thread.sleep(1000);
            }
            catch (InterruptedException e) {
              e.printStackTrace();
            }
          }
        }

}

Listing 24-3A Responsive UI That Runs Long-Running Tasks in a Background Thread

程序会替换语句

statusLbl.setText(status);

BadUI类中用语句

// Update the Label on the JavaFx Application Thread
Platform.runLater(() -> statusLbl.setText(status));

现在,为Label设置text属性发生在 JavaFX 应用程序线程上。 Start 按钮的ActionEvent处理程序在后台线程中运行任务,从而释放 JavaFX 应用程序线程来处理用户动作。任务的状态会定期在Label中更新。在任务处理过程中,您可以点击退出按钮。

您是否克服了 JavaFX 的事件调度线程模型带来的限制?答案是有也有没有,你用了一个微不足道的例子来论证这个问题。你已经解决了这个小问题。然而,在现实世界中,在 GUI 应用程序中执行长时间运行的任务并不那么简单。例如,您的任务运行逻辑和 UI 紧密耦合,因为您在runTask()方法中引用了Label,这在现实世界中是不可取的。您的任务不返回结果,也没有可靠的机制来处理可能发生的错误。您的任务不能被可靠地取消、重新启动或安排在将来运行。

JavaFX 并发框架可以回答所有这些问题。该框架提供了在一个或多个后台线程中运行任务并在 GUI 应用程序中发布任务的状态和结果的可靠方式。该框架是本章讨论的主题。我花了几页来说明 JavaFX 中的并发框架。如果您理解了本节中提出的问题的背景,那么理解框架就很容易了。

了解并发框架 API

Java 通过java.util.concurrent包中的库包含了一个全面的 Java 编程语言并发框架。JavaFX 并发框架非常小。它构建在 Java 语言并发框架之上,记住它将在 GUI 环境中使用。图 24-3 显示了 JavaFX 并发框架中的类的类图。

img/336502_2_En_24_Fig3_HTML.png

图 24-3

JavaFX 并发框架中的类的类图

该框架由一个接口、四个类和一个枚举组成。

接口的一个实例代表一个需要在一个或多个后台线程中执行的任务。任务的状态可以从 JavaFX 应用程序线程中观察到。

TaskServiceScheduledService类实现了Worker接口。它们代表不同类型的任务。它们是抽象类。Task类的一个实例代表一个一次性任务。A Task不能重复使用。Service类的一个实例代表一个可重用的任务。ScheduledService类继承自Service类。一个ScheduledService是一个可以被安排在指定的时间间隔后重复运行的任务。

Worker.State枚举中的常量代表了Worker的不同状态。

WorkerStateEvent类的一个实例表示当Worker的状态改变时发生的一个事件。您可以将事件处理程序添加到所有三种类型的任务中,以监听它们的状态变化。

了解 Worker 接口

Worker<V>接口为 JavaFX 并发框架执行的任何任务提供了规范。Worker是在一个或多个后台线程中执行的任务。通用参数VWorker结果的数据类型。如果Worker没有产生结果,使用Void作为通用参数。任务的状态是可观察的。任务的状态在 JavaFX 应用程序线程上发布,使任务能够与场景图通信,这是 GUI 应用程序中通常需要的。

员工的状态转换

在生命周期中,Worker会经历不同的状态。Worker.State枚举中的常量代表了Worker的有效状态:

  • Worker.State.READY

  • Worker.State.SCHEDULED

  • Worker.State.RUNNING

  • Worker.State.SUCCEEDED

  • Worker.State.CANCELLED

  • Worker.State.FAILED

图 24-4 显示了一个Worker可能的状态转换,其中Worker.State枚举常量代表状态。

img/336502_2_En_24_Fig4_HTML.png

图 24-4

工人可能的状态转换路径

当一个Worker被创建时,它处于READY状态。在开始执行之前,它转换到SCHEDULED状态。当它开始运行时,它处于RUNNING状态。成功完成后,WorkerRUNNING状态转换到SUCCEEDED状态。如果Worker在执行过程中抛出异常,它将转换到FAILED状态。使用cancel()方法可以取消Worker。它可以从READYSCHEDULEDRUNNING状态转换到CANCELLED状态。这些是单触发Worker的正常状态转换。

可重用的Worker可以从CANCELLEDSUCCEEDEDFAILED状态转换到图中虚线所示的READY状态。

工人的属性

Worker接口包含九个只读属性,代表任务的内部状态:

  • title

  • message

  • running

  • state

  • progress

  • workDone

  • totalWork

  • value

  • exception

当您创建一个Worker时,您将有机会指定这些属性。这些属性也可以随着任务的进行而更新。

属性表示任务的标题。假设一个任务产生素数。你可以给这个任务一个标题“质数生成器”

message属性表示任务处理过程中的详细消息。假设一个任务产生几个素数;您可能希望定期或在适当的时候向用户提供反馈信息,比如“生成 X 个质数,共 Y 个质数”

running属性告知Worker是否正在运行。当工人处于SCHEDULEDRUNNING状态时,这是真的。否则就是假的。

state属性指定Worker的状态。它的值是Worker.State枚举的常量之一。

totalWorkworkDoneprogress属性代表任务的进度。totalWork是要完成的总工作量。workDone是已经完成的工作量。progressworkDonetotalWork的比值。如果它们的值未知,则设置为–1.0。

属性表示任务的结果。只有当Worker成功到达SUCCEEDED状态时,它的值才为非空。有时,任务可能不会产生结果。在这些情况下,通用参数V将是Void,而value属性将总是null

任务可能会因引发异常而失败。exception属性表示在任务处理过程中抛出的异常。只有当Worker的状态为FAILED时才不为空。它属于Throwable类型。

通常,当任务正在进行时,您希望在场景图中显示任务的详细信息。并发框架确保在 JavaFX 应用程序线程上更新Worker的属性。因此,可以将场景图中 UI 元素的属性绑定到这些属性。您还可以将InvalidationChangeListener添加到这些属性中,并从这些侦听器中访问现场图。

在随后的章节中,您将讨论Worker接口的具体实现。让我们创建一个可重用的 GUI,在所有的例子中使用。GUI 基于一个Worker来显示其属性的当前值。

示例的实用程序类

让我们创建程序的可重用 GUI 和非 GUI 部分,以便在后续部分的示例中使用。清单 24-4 中的WorkerStateUI类构建了一个GridPane来显示一个Worker的所有属性。它与一个Worker<ObservableList<Long>>一起使用。它通过 UI 元素向它们显示一个Worker的属性。通过向构造器传递一个Worker或者调用bindToWorker()方法,可以将Worker的属性绑定到 UI 元素。

// WorkerStateUI.java
package com.jdojo.concurrent;

import javafx.beans.binding.When;
import javafx.collections.ObservableList;
import javafx.concurrent.Worker;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressBar;
import javafx.scene.control.TextArea;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;

public class WorkerStateUI extends GridPane {
        private final Label title = new Label("");
        private final Label message = new Label("");
        private final Label running = new Label("");
        private final Label state = new Label("");
        private final Label totalWork = new Label("");
        private final Label workDone = new Label("");
        private final Label progress = new Label("");
        private final TextArea value = new TextArea("");
        private final TextArea exception = new TextArea("");
        private final ProgressBar progressBar = new ProgressBar();

        public WorkerStateUI() {
                addUI();
        }

        public WorkerStateUI(Worker<ObservableList<Long>> worker) {
                addUI();
                bindToWorker(worker);
        }

        private void addUI() {
                value.setPrefColumnCount(20);
                value.setPrefRowCount(3);
                exception.setPrefColumnCount(20);
                exception.setPrefRowCount(3);
                this.setHgap(5);
                this.setVgap(5);
                addRow(0, new Label("Title:"), title);
                addRow(1, new Label("Message:"), message);
                addRow(2, new Label("Running:"), running);
                addRow(3, new Label("State:"), state);
                addRow(4, new Label("Total Work:"), totalWork);
                addRow(5, new Label("Work Done:"), workDone);
                addRow(6, new Label("Progress:"),
                         new HBox(2, progressBar, progress));
                addRow(7, new Label("Value:"), value);
                addRow(8, new Label("Exception:"), exception);
        }

        public void bindToWorker(final Worker<ObservableList<Long>> worker) {
                // Bind Labels to the properties of the worker
                title.textProperty().bind(worker.titleProperty());
                message.textProperty().bind(worker.messageProperty());
                running.textProperty().bind(
                         worker.runningProperty().asString());
                state.textProperty().bind(
                         worker.stateProperty().asString());
                totalWork.textProperty().bind(
                         new When(worker.totalWorkProperty().isEqualTo(-1))
                    .then("Unknown")
                    .otherwise(worker.totalWorkProperty().asString()));
                workDone.textProperty().bind(
                         new When(worker.workDoneProperty().isEqualTo(-1))
                    .then("Unknown")

                    .otherwise(worker.workDoneProperty().asString()));
                progress.textProperty().bind(
                         new When(worker.progressProperty().isEqualTo(-1))
                    .then("Unknown")
                    .otherwise(worker.progressProperty().multiply(100.0)
                            .asString("%.2f%%")));
                progressBar.progressProperty().bind(
                         worker.progressProperty());
                value.textProperty().bind(
                         worker.valueProperty().asString());

                // Display the exception message when an exception occurs
                     // in the worker
                worker.exceptionProperty().addListener(
                         (prop, oldValue, newValue) -> {
                        if (newValue != null) {
                            exception.setText(newValue.getMessage());
                        } else {
                            exception.setText("");
                        }
                });
        }
}

Listing 24-4A Utility Class to Build UI Displaying the Properties of a Worker

清单 24-5 中的PrimeUtil类是一个实用程序类,用于检查一个数是否是质数。

// PrimeUtil.java
package com.jdojo.concurrent;

public class PrimeUtil {
        public static boolean isPrime(long num) {
            if (num <= 1 || num % 2 == 0) {
                    return false;
            }

            int upperDivisor = (int)Math.ceil(Math.sqrt(num));
            for (int divisor = 3; divisor <= upperDivisor; divisor += 2) {
                    if (num % divisor == 0) {
                            return false;
                    }
            }
            return true;
        }

}

Listing 24-5A Utility Class to Work with Prime Numbers

使用任务

Task<V>类的一个实例代表一个一次性任务。一旦任务完成、取消或失败,就不能重新启动。Task<V>类实现了Worker<V>接口。因此,Worker<V>接口指定的所有属性和方法在Task<V>类中都是可用的。

Task<V>类继承自FutureTask<V>类,后者是 Java 并发框架的一部分。FutureTask<V>实现了Future<V>RunnableFuture<V>Runnable接口。所以一个Task<V>也实现了所有这些接口。

创建任务

如何创建一个Task<V>?创建一个Task<V>很容易。您需要子类化Task<V>类,并为抽象方法call()提供一个实现。call()方法包含执行任务的逻辑。下面的代码片段展示了一个Task实现的框架:

// A Task that produces an ObservableList<Long>
public class PrimeFinderTask extends Task<ObservableList<Long>> {
        @Override
        protected ObservableList<Long>> call() {
                // Implement the task logic here...
        }
}

更新任务属性

通常,您会希望随着任务的进行更新其属性。必须在 JavaFX 应用程序线程上更新和读取这些属性,这样才能在 GUI 环境中安全地观察它们。Task<V>类提供了特殊的方法来更新它的一些属性:

  • protected void updateMessage(String message)

  • protected void updateProgress(double workDone, double totalWork)

  • protected void updateProgress(long workDone, long totalWork)

  • protected void updateTitle(String title)

  • protected void updateValue(V value)

您向updateProgress()方法提供了workDonetotalWork属性的值。progress属性将被设置为workDone/totalWork。如果workDone大于totalWork或者两者都小于–1.0,该方法抛出运行时异常。

有时,您可能希望在其value属性中发布任务的部分结果。为此使用了updateValue()方法。任务的最终结果是其call()方法的返回值。

所有的updateXxx()方法都在 JavaFX 应用程序线程上执行。它们的名称表示它们更新的属性。从Taskcall()方法中调用它们是安全的。如果您想直接从call()方法中更新Task的属性,您需要将代码包装在一个Platform.runLater()调用中。

监听任务转换事件

Task类包含以下属性,允许您为其状态转换设置事件处理程序:

  • onCancelled

  • onFailed

  • onRunning

  • onScheduled

  • onSucceeded

下面的代码片段添加了一个onSucceeded事件处理程序,当任务转换到SUCCEEDED状态时会调用该处理程序:

Task<ObservableList<Long>> task = create a task...
task.setOnSucceeded(e -> {
        System.out.println("The task finished. Let us party!")
});

取消任务

使用以下两种cancel()方法之一取消任务:

  • public final boolean cancel()

  • public boolean cancel(boolean mayInterruptIfRunning)

第一个版本从执行队列中删除任务或停止其执行。第二个版本让您指定运行任务的线程是否被中断。确保在call()方法中处理InterruptedException。一旦您检测到这个异常,您需要快速完成call()方法。否则,对cancel(true)的调用可能无法可靠地取消任务。可以从任何线程调用cancel()方法。

Task到达一个特定的状态时,它的以下方法被调用:

  • protected void scheduled()

  • protected void running()

  • protected void succeeded()

  • protected void cancelled()

  • protected void failed()

它们在Task类中的实现是空的。它们应该被子类覆盖。

运行任务

一个TaskRunnable也是一个FutureTask。要运行它,您可以使用一个后台线程或一个ExecutorService:

// Schedule the task on a background thread
Thread backgroundThread = new Thread(task);
backgroundThread.setDaemon(true);
backgroundThread.start();

// Use the executor service to schedule the task
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(task);

主要查找器任务示例

是时候看看Task的行动了。清单 24-6 中的程序是Task<ObservableList<Long>>的一个实现。它检查指定的lowerLimitupperLimit之间的质数。它返回该范围内的所有数字。请注意,任务线程在检查一个数字是否为质数之前会休眠一小段时间。这样做是为了给用户一个长期运行任务的印象。在现实世界的应用程序中不需要它。如果作为取消请求的一部分,任务被中断,call()方法处理一个InterruptedException并结束任务。

对方法updateValue()的调用几乎不需要解释:

updateValue(FXCollections.<Long>unmodifiableObservableList(results));

每次找到一个质数,结果列表就会更新。上述语句将结果列表包装在一个不可修改的可观察列表中,并将其发布给客户端。这使得客户端可以访问任务的部分结果。这是发布部分结果的一种快速而肮脏的方式。如果call()方法返回一个原始值,那么可以重复调用updateValue()方法。

Tip

在这种情况下,每当您找到一个新的质数时,您就在创建一个新的不可修改的列表,出于性能原因,这在生产环境中是不可接受的。发布部分结果的有效方法是为Task声明一个只读属性;在 JavaFX 应用程序线程上定期更新只读属性;让客户端绑定到只读属性以查看部分结果。

// PrimeFinderTask.java
// ...find in the book's download area.

Listing 24-6Finding Prime Numbers Using a Task<Long>

清单 24-7 中的程序包含了使用你的PrimeFinderTask类构建 GUI 的完整代码。图 24-5 显示任务运行时的窗口。您需要点击开始按钮来开始任务。点击取消按钮取消任务。任务一旦完成,就被取消或失败;您不能重启它,并且StartCancel按钮都被禁用。请注意,当任务找到一个新的质数时,它会立即显示在窗口上。

img/336502_2_En_24_Fig5_HTML.png

图 24-5

使用质数查找器任务的窗口

// OneShotTask.java
// ...find in the book's download area.

Listing 24-7Executing a Task in a GUI Environment

使用服务

Service<V>类是Worker<V>接口的一个实现。它封装了一个Task<V>。它通过允许启动、取消、重置和重启来使Task<V>可重用。

创建服务

记住一个Service<V>封装了一个Task<V>。因此,你需要一个Task<V>来拥有一个Service<V>Service<V>类包含一个返回Task<V>的抽象保护createTask()方法。要创建一个服务,您需要子类化Service<V>类并为createTask()方法提供一个实现。

下面的代码片段创建了一个封装了您之前创建的PrimeFinderTaskService:

// Create a service
Service<ObservableList<Long>> service = new Service<ObservableList<Long>>() {
        @Override
        protected Task<ObservableList<Long>> createTask() {
                // Create and return a Task
                return new PrimeFinderTask();
        }

};

每当服务启动或重启时,都会调用服务的createTask()方法。

更新服务属性

Service类包含所有属性(titlemessagestatevalue等)。)表示一个Worker的内部状态。它添加了一个executor属性,这是一个java.util.concurrent.Executor。该属性用于运行Service。如果没有指定,就会创建一个守护线程来运行Service

Task类不同,Service类不包含用于更新其属性的updateXxx()方法。它的属性被绑定到底层Task<V>的相应属性。当Task更新其属性时,这些变化会自动反映到Service和客户端。

监听服务转换事件

Service类包含所有用于设置状态转换监听器的属性,就像Task类所包含的一样。它增加了一个onReady property。该属性指定了一个状态转换事件处理程序,当Service转换到READY状态时会调用该处理程序。请注意,Task类不包含onReady属性,因为Task在创建时处于READY状态,并且它再也不会转换到READY状态。然而,一个Service可以多次处于READY状态。当Service被创建、复位和重启时,它会转换到READY状态。Service类还包含一个受保护的ready()方法,该方法将被子类覆盖。当Service转换到READY状态时,调用ready()方法。

取消服务

使用cancel()方法取消一个Service:该方法将Servicestate设置为CANCELLED

正在启动服务

调用Service类的start()方法会启动一个Service。该方法调用createTask()方法获得一个Task实例并运行Task。当调用其start()方法时,服务必须处于READY状态:

Service<ObservableList<Long>> service = create a service
...
// Start the service
service.start();

重置服务

调用Service类的reset()方法重置Service。重置会将所有的Service属性恢复到初始状态。state被设置为READY。仅当Service处于SUCCEEDEDFAILEDCANCELLEDREADY中的一种结束状态时,才允许复位Service。如果Service处于SCHEDULEDRUNNING状态,调用reset()方法会抛出运行时异常。

重新启动服务

调用Service类的restart()方法重启一个Service。如果任务存在,它会取消任务,重置服务,然后启动它。它依次调用服务对象上的三个方法:

  • cancel()

  • reset()

  • start()

Prime Finder 服务示例

清单 24-8 中的程序展示了如何使用ServiceService对象被创建并存储为一个实例变量。这个Service对象管理一个PrimeFinderTask对象,这是一个Task来寻找两个数之间的质数。增加了四个按钮:启动/重启取消复位退出。第一次启动Service启动按钮标记为重启。这些按钮的功能和它们的标签一样。当您无法调用按钮时,它们会被禁用。图 24-6 为点击开始按钮后的窗口截图。

img/336502_2_En_24_Fig6_HTML.png

图 24-6

使用服务查找质数的窗口

// PrimeFinderService.java
// ...find in the book's download area.

Listing 24-8Using a Service to Find Prime Numbers

使用 ScheduledService

ScheduledService<V>是一个Service<V>,自动重启。它可以在成功完成或失败时重新启动。故障重启是可配置的。ScheduledService<V>类继承自Service<V>类。ScheduledService适用于使用轮询的任务。例如,你可以用它每十分钟刷新一次比赛的比分或互联网上的天气预报。

创建 ScheduledService

创建一个ScheduledService的过程与创建一个Service的过程相同。您需要子类化ScheduledService<V>类,并为createTask()方法提供一个实现。

下面的代码片段创建了一个封装了您之前创建的PrimeFinderTaskScheduledService:

// Create a scheduled service
ScheduledService<ObservableList<Long>> service =
    new ScheduledService <ObservableList<Long>>() {
        @Override
        protected Task<ObservableList<Long>> createTask() {
                // Create and return a Task
                return new PrimeFinderTask();
        }
};

在手动或自动启动或重启服务时,调用服务的createTask()方法。请注意,ScheduledService会自动重启。您可以通过调用start()restart()方法来手动启动和重启它。

Tip

启动、取消、重置和重启ScheduledService的工作方式与Service上的这些操作相同。

更新 ScheduledService 属性

ScheduledService<ScheduledService>类继承了Service<V>类的属性。它添加了以下可用于配置服务计划的属性:

  • lastValue

  • delay

  • period

  • restartOnFailure

  • maximumFailureCount

  • backoffStrategy

  • cumulativePeriod

  • currentFailureCount

  • maximumCumulativePeriod

一个ScheduledService<V>被设计成运行多次。服务计算的当前值没有太大意义。您的类添加了一个新属性lastValue,它的类型是V,它是服务计算的最后一个值。

delay是一个Duration,它指定了服务启动和开始运行之间的延迟。服务在指定的延迟时间内保持在SCHEDULED状态。仅当手动调用start()restart()方法启动服务时,延迟才会生效。当服务自动重新启动时,是否接受 delay 属性取决于服务的当前状态。例如,如果服务在其定期计划之后运行,它将立即重新运行,忽略 delay 属性。默认延迟为零。

period是一个Duration,它指定了上一次运行和下一次运行之间的最短时间。默认周期为零。

restartOnFailure指定服务失败时是否自动重启。默认情况下,它被设置为 true。

currentFailureCount是定期服务失败的次数。当计划服务手动重新启动时,它将重置为零。

maximumFailureCount指定了服务在转换到FAILED状态之前可以失败的最大次数,并且不会自动重新启动。请注意,您可以随时手动重新启动计划服务。默认情况下,它被设置为Integer.MAX_VALUE

backoffStrategy是一个Callback<ScheduledService<?>,Duration>,它计算Duration以添加到每次故障的周期中。通常,如果服务失败,您希望在重试之前减慢速度。假设服务每 10 分钟运行一次。如果第一次失败,您可能希望在 15 分钟后重新启动它。如果第二次失败,您希望将重新运行时间增加到 25 分钟,依此类推。ScheduledService类提供了三个内置的退避策略作为常量:

  • EXPONENTIAL_BACKOFF_STRATEGY

  • LINEAR_BACKOFF_STRATEGY

  • LOGARITHMIC_BACKOFF_STRATEGY

重新运行间隔是根据非零时段和当前故障计数计算的。连续失败运行之间的时间在指数backoffStrategy中呈指数增长,在线性backoffStrategy中呈线性增长,在对数backoffStrategy中呈对数增长。LOGARITHMIC_BACKOFF_STRATEGY是默认设置。当period为零时,使用以下公式。计算的持续时间以毫秒为单位:

  • Exponential : Math.exp(currentFailureCount)

  • Linear: currentFailureCount

  • Logarithmic: Math.log1p(currentFailureCount)

以下公式用于非空值period:

  • Exponential: period + (period * Math.exp(currentFailureCount)

  • Linear: period + (period * currentFailureCount)

  • Logarithmic: period + (period * Math.log1p(currentFailureCount))

cumulativePeriod是一个Duration,它是当前运行失败和下一次运行之间的时间。它的值是使用backoffStrategy属性计算的。它会在计划服务成功运行时重置。它的值可以使用maximumCumulativePeriod属性来限定。

监听计划服务转换事件

ScheduledServiceService经历相同的转换状态。成功运行后,它会自动经过READYSCHEDULEDRUNNING状态。根据计划服务的配置方式,它可能会在运行失败后自动经历相同的状态转换。

您可以监听状态转换并覆盖与转换相关的方法(ready()running()failed()等)。)为一个Service就可以了。当您在ScheduledService子类中覆盖与转换相关的方法时,确保调用 super 方法来保持您的ScheduledService正常工作。

Prime Finder 计划服务示例

让我们将PrimeFinderTaskScheduledService一起使用。一旦开始,ScheduledService将永远继续运行。如果失败五次,它将通过转换到FAILED状态退出。您可以随时手动取消并重新启动该服务。

清单 24-9 中的程序展示了如何使用ScheduledService。该程序与清单 24-8 中显示的程序非常相似,除了两个地方。服务是通过子类化ScheduledService类创建的:

// Create the scheduled service
ScheduledService<ObservableList<Long>> service = new ScheduledService<ObservableList<Long>>() {
        @Override
        protected Task<ObservableList<Long>> createTask() {
                return new PrimeFinderTask();
        }
};

start()方法的开头配置ScheduledService,设置delayperiod,maximumFailureCount属性:

// Configure the scheduled service
service.setDelay(Duration.seconds(5));
service.setPeriod(Duration.seconds(30));
service.setMaximumFailureCount(5);

图 24-7 、 24-8 和 24-9 显示了ScheduledService未启动时、在SCHEDULED状态下观察延迟时间时以及运行时的状态。使用取消Reset按钮取消和重置服务。一旦服务被取消,您可以通过点击Restart按钮手动重启。

img/336502_2_En_24_Fig9_HTML.png

图 24-9

ScheduledService 已启动并正在运行

img/336502_2_En_24_Fig8_HTML.png

图 24-8

ScheduledService 第一次启动,它正在观察延迟时间

img/336502_2_En_24_Fig7_HTML.png

图 24-7

ScheduledService 未启动

// PrimeFinderScheduledService.java
// ...find in the book's download area.

Listing 24-9Using a ScheduledService to Run a Task

摘要

Java(包括 JavaFX) GUI 应用程序本质上是多线程的。多个线程执行不同的任务,以保持 UI 与用户操作同步。与 Swing 和 AWT 一样,JavaFX 使用一个称为 JavaFX 应用程序线程的线程来处理所有 UI 事件。场景图中表示 UI 的节点不是线程安全的。设计非线程安全的节点有利也有弊。它们更快,因为不涉及同步。缺点是需要从单个线程访问它们,以避免处于非法状态。JavaFX 设置了一个限制,即只能从一个线程(JavaFX 应用程序线程)访问实时场景图形。这个限制间接地强加了另一个限制,即 UI 事件不应该处理长时间运行的任务,因为它会使应用程序没有响应。用户将得到应用程序被挂起的印象。JavaFX 并发框架构建在 Java 语言并发框架之上,记住它将在 GUI 环境中使用。该框架由一个接口、四个类和一个枚举组成。它提供了一种设计多线程 JavaFX 应用程序的方法,该应用程序可以在工作线程中执行长时间运行的任务,保持 UI 的响应性。

接口的一个实例代表一个需要在一个或多个后台线程中执行的任务。任务的状态可以从 JavaFX 应用程序线程中观察到。TaskServiceScheduledService类实现了Worker接口。它们代表不同类型的任务。它们是抽象类。

Task类的一个实例代表一个一次性任务。A Task不能重复使用。

Service类的一个实例代表一个可重用的任务。

ScheduledService类继承自Service类。一个ScheduledService是一个可以被安排在指定的时间间隔后重复运行的任务。

Worker.State枚举中的常量代表了Worker的不同状态。WorkerStateEvent类的一个实例表示当Worker的状态改变时发生的一个事件。您可以将事件处理程序添加到所有三种类型的任务中,以监听它们的状态变化。

下一章将讨论如何在 JavaFX 应用程序中加入音频和视频。

二十五、播放音频和视频

在本章中,您将学习:

  • 什么是媒体 API

  • 如何播放简短的音频剪辑

  • 如何播放媒体(音频和视频)以及如何跟踪播放的不同方面,如播放速率、音量、播放时间、重复播放和媒体错误

本章的例子在com.jdojo.media包中。为了让它们工作,您必须在module-info.java文件中添加相应的一行:

...
opens com.jdojo.media to javafx.graphics, javafx.base;
...

了解媒体 API

JavaFX 支持通过 JavaFX Media API 播放音频和视频。还支持静态媒体文件和实时提要的 HTTP 实时流。支持多种媒体格式,包括 AAC、AIFF、WAV 和 MP3。还支持包含 VP6 视频和 MP3 音频的 FLV 以及 H.264/AVC 视频格式的 MPEG-4 多媒体容器。对特定媒体格式的支持取决于*台。某些媒体播放功能和格式不需要任何额外安装;有些需要安装第三方软件。有关 JavaFX 的系统要求和支持的媒体格式的详细信息,请参考位于 https://openjfx.io/javadoc/17/javafx.media/javafx/scene/media/package-summary.html#SupportedMediaTypes 的网页。

媒体 API 由几个类组成。图 25-1 显示了一个类图,它只包括媒体 API 中的核心类。API 中的所有类都包含在javafx.scene.media包中。

img/336502_2_En_25_Fig1_HTML.png

图 25-1

媒体 API 中核心类的类图

AudioClip用于以最小的延迟播放一小段音频剪辑。通常,这对于声音效果很有用,声音效果通常是很短的音频剪辑。使用MediaMediaPlayerMediaView类播放较长的音频和视频。

MediaMediaPlayer类用于播放音频和视频。Media类的一个实例代表一个媒体资源,可以是音频或视频。它提供有关介质的信息,例如介质的持续时间。MediaPlayer类的一个实例提供了播放媒体的控件。

MediaView类的一个实例提供了由MediaPlayer播放的媒体的视图。一个MediaView用于观看视频。

当您尝试播放媒体时,可能会出现一些问题,例如,媒体格式可能不受支持,或者媒体内容可能已损坏。MediaException类的一个实例表示在媒体回放期间可能发生的特定类型的媒体错误。当出现与介质相关的错误时,会生成一个MediaErrorEvent。您可以通过向媒体对象添加适当的事件处理程序来处理该错误。

我将在这一章中详细介绍如何在媒体 API 中使用这些类和其他支持类。

播放短音频剪辑

AudioClip类的一个实例用于以最小的延迟播放一小段音频剪辑。通常,这对于播放简短的音频剪辑很有用,例如,当用户出错时发出嘟嘟声,或者在游戏应用程序中产生简短的声音效果。

AudioClip类只提供了一个构造器,它接受一个字符串形式的 URL,即音频源的 URL。音频剪辑会立即以原始、未压缩的形式加载到内存中。这就是为什么您不应该将此类用于长时间播放的音频剪辑的原因。源 URL 可以使用 HTTP、file 和 JAR 协议。这意味着您可以播放来自互联网、本地文件系统和 JAR 文件的音频剪辑。

以下代码片段使用 HTTP 协议创建了一个AudioClip:

String clipUrl = "http://www.jdojo.com/myaudio.wav";
AudioClip audioClip = new AudioClip(clipUrl);

当一个AudioClip对象被创建时,音频数据被加载到内存中,并准备好立即播放。使用play()方法播放音频,使用stop()方法停止播放:

// Play the audio
audioClip.play();
...
// Stop the playback

audioClip.stop();

清单 25-1 中的程序展示了如何使用AudioClip类播放一个音频剪辑。它声明了一个实例变量来存储AudioClip引用。在init()方法中创建了AudioClip,以确保当窗口在start()方法中显示时,剪辑可以播放。您也可以在构造器中创建AudioClipstart()方法增加了开始和停止按钮。它们的动作事件处理程序分别开始和停止回放。

// AudioClipPlayer.java
package com.jdojo.media;

import com.jdojo.util.ResourceUtil;
import java.net.URL;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.HBox;
import javafx.scene.media.AudioClip;
import javafx.stage.Stage;

public class AudioClipPlayer extends Application {
        private AudioClip audioClip;

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

        @Override
        public void init() {
                URL mediaUrl =
                    ResourceUtil.getResourceURL("media/chimes.wav");

            // Create an AudioClip, which loads the audio data
                // synchronously
            audioClip = new AudioClip(mediaUrl.toExternalForm());
        }

        @Override

        public void start(Stage stage) {
            Button playBtn = new Button("Play");
            Button stopBtn = new Button("Stop");

            // Set event handlers for buttons
            playBtn.setOnAction(e -> audioClip.play());
            stopBtn.setOnAction(e -> audioClip.stop());

            HBox root = new HBox(5, playBtn, stopBtn);
            root.setStyle("-fx-padding: 10;");
            Scene scene = new Scene(root);
            stage.setScene(scene);
            stage.setTitle("Playing Short Audio Clips");
            stage.show();
        }
}

Listing 25-1Playing Back an Audio Clip Using an AudioClip Instance

AudioClip类支持在播放剪辑时设置一些音频属性:

  • cycleCount

  • volume

  • rate

  • balance

  • pan

  • priority

除了cycleCount之外,前面的所有属性都可以在AudioClip类上设置。Splay()方法的后续调用将使用它们作为默认值。play()方法也可以覆盖特定回放的缺省值。必须在AudioClip上指定cycleCount属性,所有后续回放将使用相同的值。

The cycleCount指定调用play()方法时剪辑播放的次数。它默认为一个,只播放一次剪辑。您可以使用以下三个INDEFINITE常量之一作为cycleCount来播放AudioClip循环,直到停止:

  • AudioClip.INDEFINITE

  • MediaPlayer.INDEFINITE

  • Animation.INDEFINITE

以下代码片段显示了如何无限期地播放一个音频剪辑五次:

// Play five times
audioClip.setCycleCount(5);
...
// Loop forever
audioClip.setCycleCount(AudioClip.INDEFINITE);

volume指定播放的相对音量。有效范围是 0.0 到 1.0。值 0.0 表示静音,而 1.0 表示最大音量。

rate指定播放音频的相对速度。有效范围是 0.125 到 8.0。值 0.125 表示剪辑播放速度慢八倍,值 8.0 表示剪辑播放速度快八倍。速率影响播放时间和音高。默认速率为 1.0,以正常速率播放剪辑。

balance指定左右声道的相对音量。有效范围是–1.0 到 1.0。值–1.0 将左声道的回放设定为正常音量,并将右声道静音。值 1.0 将右声道的回放设置为正常音量,并将左声道静音。默认值为 0.0,将两个通道中的回放设置为正常音量。

pan指定剪辑在左右声道之间的分布。有效范围是–1.0 到 1.0。值为–1.0 会将片段完全移到左通道。值为 1.0 会将剪辑完全移到右通道。默认值为 0.0,正常播放剪辑。设定单声道片段的声相值与设定*衡效果相同。您应该仅为使用立体声的音频剪辑更改此属性的默认值。

priority指定片段相对于其他片段的优先级。它仅在播放的剪辑数量超过系统限制时使用。将停止播放优先级较低的剪辑。它可以设置为任何整数。默认优先级设置为零。

play()方法被重载。它有三个版本:

  • Void play()

  • void play(double volume)

  • void play(double volume, double balance, double rate, double pan, int priority)

该方法的无参数版本使用在AudioClip上设置的所有属性。其他两个版本可以覆盖特定回放的指定属性。假设AudioClip的音量设置为 1.0。调用play()会以 1.0 的音量播放剪辑,调用play(0.20)会以 0.20 的音量播放剪辑,而AudioClip的音量属性保持为 1.0 不变。也就是说,带有参数的play()方法允许您在每次回放的基础上覆盖AudioClip属性。

AudioClip类包含一个isPlaying()方法来检查剪辑是否还在播放。如果剪辑正在播放,它将返回true。否则返回false

播放媒体

JavaFX 提供了一个统一的 API 来处理音频和视频。您使用相同的类来处理这两者。媒体 API 在内部将它们视为对 API 用户透明的两种不同类型的媒体。从现在开始,我将使用术语媒体来表示音频和视频,除非另有说明。

媒体 API 包含三个播放媒体的核心类:

  • Media

  • MediaPlayer

  • MediaView

创建媒体对象

Media类的一个实例代表一个媒体资源,可以是音频或视频。它提供与媒体相关的信息,例如持续时间、元数据、数据等等。如果媒体是视频,则提供视频的宽度和高度。一个Media对象是不可变的。它是通过提供媒体资源的字符串 URL 来创建的,如以下代码所示:

// Create a Media
String mediaUrl = "http://www.jdojo.com/mymusic.wav";
Media media = new Media(mediaUrl);

Media类包含以下属性,所有属性(除了onError)都是只读的:

  • duration

  • width

  • height

  • error

  • onError

duration以秒为单位指定媒体的持续时间。它是一个Duration对象。如果持续时间未知,则为Duration.UNKNOWN

widthheight分别以像素为单位给出源媒体的宽度和高度。如果媒体没有宽度和高度,它们被设置为零。

erroronError属性是相关的。error属性代表加载媒体时发生的MediaExceptiononError是一个Runnable对象,您可以设置它在错误发生时得到通知。发生错误时调用Runnablerun()方法:

// When an error occurs in loading the media, print it on the console
media.setOnError(() -> System.out.println(player.getError().getMessage()));

创建一个 MediaPlayer 对象

MediaPlayer提供控制,例如播放、暂停、停止、搜索、播放速度、音量调节,用于播放媒体。MediaPlayer只提供了一个接受Media对象作为参数的构造器:

// Create a MediaPlayer
MediaPlayer player = new MediaPlayer(media);

你可以使用MediaPlayer类的getMedia()方法从MediaPlayer中获取媒体的引用。

Media类一样,MediaPlayer类也包含用于报告错误的erroronError属性。当MediaPlayer出现错误时,Media对象也会报告同样的错误。

MediaPlayer类包含许多属性和方法。我将在随后的章节中讨论它们。

创建一个媒体视图节点

一个MediaView是一个节点。它提供了由MediaPlayer播放的媒体的视图。请注意,音频剪辑没有视觉效果。如果你尝试为一个音频内容创建一个MediaView,它将是空的。要观看视频,您需要创建一个MediaView并将其添加到场景图中。

MediaView类提供了两个构造器,一个是无参数构造器,另一个以MediaPlayer作为参数:

  • public MediaView()

  • public MediaView(MediaPlayer mediaPlayer)

无参数构造器创建一个附加到任何MediaPlayerMediaView。您需要使用mediaPlayer属性的 setter 来设置一个MediaPlayer:

// Create a MediaView with no MediaPlayer
MediaView mediaView = new MediaView();
mediaView.setMediaPlayer(player);

另一个构造器让您为MediaView指定一个MediaPlayer:

// Create a MediaView
MediaView mediaView = new MediaView(player);

结合媒体媒体播放器媒体视图

一个媒体的内容可以被多个Media对象同时使用。然而,一个Media对象在其生命周期中只能与一个媒体内容相关联。

一个Media对象可以与多个MediaPlayer对象相关联。然而,一只MediaPlayer在其一生中只与一只Media相关联。

一个MediaView可以可选地与一个MediaPlayer相关联。当然,与MediaPlayer无关的MediaView没有任何视觉效果。可以更改MediaViewMediaPlayer。改变MediaViewMediaPlayer类似于改变电视频道。MediaView的视图由其当前的MediaPlayer提供。您可以将同一个MediaPlayer与多个MediaViews相关联。不同的MediaViews在播放过程中可能会显示同一媒体的不同部分。图 25-2 显示了媒体播放中涉及的三类对象之间的关系。

img/336502_2_En_25_Fig2_HTML.png

图 25-2

不同媒体相关对象在媒体回放中的角色以及它们之间的关系

媒体播放器示例

现在,您已经有足够的背景知识来理解用于播放音频和视频的机制。清单 25-2 中的程序使用ResourceUtil查找文件位置来播放视频剪辑。程序使用一个视频文件resources/media/gopro.mp4。这个文件可能没有包含在源代码中,因为它大约有 50MB。如果 JavaFX 支持您自己的媒体文件格式,您可以在此程序中替换它。

// QuickMediaPlayer.java
package com.jdojo.media;

import com.jdojo.util.ResourceUtil;
import java.net.URL;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.media.Media;
import javafx.scene.media.MediaPlayer;
import javafx.scene.media.MediaView;
import javafx.stage.Stage;
import static javafx.scene.media.MediaPlayer.Status.PLAYING;

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

        @Override
        public void start(Stage stage) {
            // Locate the media content
                URL mediaUrl = ResourceUtil.getResourceURL("media/gopro.mp4");
            String mediaStringUrl = mediaUrl.toExternalForm();

            // Create a Media
            Media media = new Media(mediaStringUrl);

            // Create a Media Player
            MediaPlayer player = new MediaPlayer(media);

            // Automatically begin the playback
            player.setAutoPlay(true);

            // Create a 400X300 MediaView
            MediaView mediaView = new MediaView(player);
            mediaView.setFitWidth(400);
            mediaView.setFitHeight(300);

            // Create Play and Stop player control buttons and add action
            // event handlers to them
            Button playBtn = new Button("Play");
            playBtn.setOnAction(e -> {
                    if (player.getStatus() == PLAYING) {
                            player.stop();
                            player.play();
                    } else {
                            player.play();
                    }

            });

            Button stopBtn = new Button("Stop");
            stopBtn.setOnAction(e -> player.stop());

            // Add an error handler
            player.setOnError(() ->
                    System.out.println(player.getError().getMessage()));

            HBox controlBox = new HBox(5, playBtn, stopBtn);
            BorderPane root = new BorderPane();

            // Add the MediaView and player controls to the scene graph
            root.setCenter(mediaView);
            root.setBottom(controlBox);

            Scene scene = new Scene(root);
            stage.setScene(scene);
            stage.setTitle("Playing Media");
            stage.show();
        }
}

Listing 25-2Using the Media, MediaPlayer, and MediaView Classes to Play a Media

start()方法中的前两条语句为媒体文件准备了一个字符串 URL:

// Locate the media content
URL mediaUrl = ResourceUtil.getResourceURL("media/gopro.mp4");
String mediaStringUrl = mediaUrl.toExternalForm();

如果您想播放来自 Internet 的媒体,您可以用类似下面的语句替换这三个语句:

String mediaStringUrl = "http://www.jdojo.com/video.flv";

程序创建一个Media、一个MediaPlayer和一个MediaView。它将MediaPlayerautoPlay属性设置为 true,这将尽快开始播放媒体:

// Automatically begin the playback
player.setAutoPlay(true);

MediaView的尺寸设定为 400 像素宽 300 像素高。如果媒体是视频,视频将被缩放以适合此大小。你会看到一个空的音频区。您可以稍后增强MediaView,这样它将占用媒体所需的空间。

创建了PlayStop按钮。事件处理程序被添加到它们中。它们可以分别用于开始和停止播放。当媒体已经在播放时,点按“播放”按钮会停止播放并再次播放媒体。

播放媒体时,很多事情都会出错。程序为MediaPlayer设置onError属性,T1 是一个Runnable。它的run()方法在错误发生时被调用。run()方法在控制台上打印错误消息:

// Add an error handler

player.setOnError(() -> System.out.println(player.getError().getMessage()));

当你运行程序时,视频应该会自动播放。您可以使用屏幕底部的按钮停止和重放它。如果有错误,您将在控制台上看到一条错误消息。

Tip

类可以播放音频和视频。你所需要做的就是改变源的 URL 来指向你想要播放的媒体。

处理回放错误

RuntimeException类继承而来的MediaException类的一个实例表示一个可能发生在MediaMediaPlayerMediaView中的媒体错误。媒体播放可能由于多种原因而失败。API 用户应该能够识别特定的错误。MediaException类定义了一个静态枚举MediaException.Type,其常量标识了错误的类型。MediaException类包含一个getType()方法,该方法返回MediaException.Type枚举的一个常量。

  • MEDIA_CORRUPTED

  • MEDIA_INACCESSIBLE

  • MEDIA_UNAVAILABLE

  • MEDIA_UNSPECIFIED

  • MEDIA_UNSUPPORTED

  • OPERATION_UNSUPPORTED

  • PLAYBACK_HALTED

  • PLAYBACK_ERROR

  • UNKNOWN

MEDIA_CORRUPTED错误类型表示介质损坏或无效。MEDIA_INACCESSIBLE错误类型表示介质不可访问。但是,媒体可能存在。MEDIA_UNAVAILABLE错误类型表示介质不存在或不可用。MEDIA_UNSPECIFIED错误类型表示尚未指定介质。MEDIA_UNSUPPORTED错误类型表示该*台不支持该媒体。OPERATION_UNSUPPORTED错误类型表示*台不支持在介质上执行的操作。PLAYBACK_HALTED错误类型表示停止播放的不可恢复的错误。PLAYBACK_ERROR错误类型表示不属于任何其他描述类别的回放错误。UNKNOWN错误类型表示发生了未知错误。

MediaMediaPlayer类包含一个属于MediaExceptionerror属性。所有三个类——MediaMediaPlayerMediaView——都包含一个onError属性,这是一个在发生错误时调用的事件处理程序。这些类中的onError属性的类型不一致。对于MediaMediaPlayer类是一个Runnable,对于MediaView类是一个MediaErrorEvent。下面的代码片段显示了如何处理MediaMediaPlayerMediaView上的错误。他们在控制台上打印错误详细信息:

player.setOnError(() -> {
        System.out.println(player.getError().getMessage());
});

media.setOnError(() -> {
        System.out.println(player.getError().getMessage());
});

mediaView.setOnError((MediaErrorEvent e) ->  {
        MediaException error = e.getMediaError();
        MediaException.Type errorType = error.getType();
        String errorMsg = error.getMessage();
        System.out.println("Error Type:" + errorType +
              ", error mesage:" + errorMsg);
});

在 JavaFX 应用程序线程上调用媒体错误处理程序。因此,从处理程序更新场景图是安全的。

建议您将MediaMediaPlayerMediaView对象的创建放在try-catch块中,并适当地处理异常。这些对象的onError处理程序在对象被创建后被涉及。如果在创建这些对象的过程中出现错误,这些处理程序将不可用。例如,如果您尝试使用的媒体类型不受支持,创建Media对象会导致错误:

try {
        Media media = new Media(mediaStringUrl);
        ...
}
catch (MediaException e) {
        // Handle errors here
}

媒体播放器的状态转换

一个MediaPlayer总有一个状态。只读的status属性表示MediaPlayer的当前状态。在MediaPlayer上执行动作时,状态会改变。它不能直接设置。MediaPlayer的状态由MediaPlayer.Status枚举中的八个常量之一定义:

  • UNKNOWN

  • READY

  • PLAYING

  • PAUSED

  • STALLED

  • STOPPED

  • HALTED

  • DISPOSED

当调用以下方法之一时,MediaPlayer从一种状态转换到另一种状态:

  • play()

  • pause()

  • stop()

  • dispose()

图 25-3 显示了一个MediaPlayer的状态转换。图 25-3 不包括HALTEDDISPOSED状态,因为这两种状态都是终端状态。

img/336502_2_En_25_Fig3_HTML.png

图 25-3

媒体播放器的状态及其转换

MediaPlayer被创建时,其状态为UNKNOWN。一旦媒体被预卷并准备好播放,MediaPlayerUNKNOWN转换到READY。一旦MediaPlayer退出UNKNOWN状态,在其生命周期内就不能重新进入。

当调用play()方法时,MediaPlayer转换到PLAYING状态。此状态表示媒体正在播放。注意如果autoPlay属性设置为 true,MediaPlayer可能在创建后不需要显式调用play()方法就可以进入PLAYING状态。

MediaPlayer正在播放时,如果它的缓冲区中没有足够的数据可以播放,它可能会进入STALLED状态。该状态表示MediaPlayer正在缓冲数据。当足够的数据被缓冲时,它回到PLAYING状态。当一个MediaPlayer被停止时,调用pause()stop()方法,它分别转换到PAUSEDSTOPPED状态。在这种情况下,缓冲继续进行;然而,一旦缓冲了足够的数据,MediaPlayer不会转换到PLAYING状态。而是停留在PAUSEDSTOPPED状态。

调用pause()方法将MediaPlayer转换到PAUSED状态。调用stop()方法将MediaPlayer转换到STOPPED状态。

如果出现不可恢复的错误,MediaPlayer转换到HALTED终端状态。该状态表示MediaPlayer不能再次使用。如果您想再次播放媒体,您必须创建一个新的MediaPlayer

dispose()方法释放所有与MediaPlayer相关的资源。然而,MediaPlayer使用的Media对象仍然可以使用。调用dispose()方法将MediaPlayer转换到终端状态DISPOSED

在应用程序中显示MediaPlayer的状态是很常见的。向 status 属性添加一个ChangeListener来监听任何状态变化。

通常,当MediaPlayer的状态改变时,您会对收到通知感兴趣。有两种方法可以获得通知:

  • 通过向状态属性添加一个ChangeListener

  • 通过设置状态更改处理程序

如果您对监听任何类型的状态变化感兴趣,第一种方法是合适的。以下代码片段展示了这种方法:

MediaPlayer player = new MediaPlayer(media);

// Add a ChangeListener to the player
player.statusProperty().addListener((prop, oldStatus, newStatus) -> {
        System.out.println("Status changed from " + oldStatus +
               " to " + newStatus);
});

如果您对处理特定类型的状态更改感兴趣,第二种方法是合适的。MediaPlayer类包含以下可设置为Runnable对象的属性:

  • onReady

  • onPlaying

  • onRepeat

  • onStalled

  • onPaused

  • onStopped

  • onHalted

MediaPlayer进入特定状态时,调用Runnable对象的run()方法。例如,当玩家进入PLAYING状态时,调用onPlaying处理程序的run()方法。以下代码片段显示了如何为特定类型的状态更改设置处理程序:

// Add a handler for PLAYING status
player.setOnPlaying(() -> {
        System.out.println("Playing...");
});

// Add a handler for STOPPED status
player.setOnStopped(() -> {
        System.out.println("Stopped...");
});

重复媒体播放

媒体可以重复播放指定的次数,甚至可以无限期播放。cycleCount属性指定回放将被重复的次数。默认情况下,它被设置为 1。将其设置为MediaPlayer.INDEFINITE可无限重复播放,直到播放器暂停或停止播放。只读的currentCount属性被设置为已完成的播放周期数。当媒体正在播放第一个循环时,它被设置为零。在第一个周期结束时,它被设置为 1;它在第二个周期结束时增加到 2;等等。以下代码将设置四次回放周期:

// The playback should repeat 4 times
player.setCycleCount(4);

当播放周期的媒体结束时,您会收到通知。为MediaPlayer类的onEndOfMedia p属性设置一个Runnable来获取通知。注意,如果回放持续四个周期,媒体结束通知将被发送四次:

player.setOnEndOfMedia(() -> {
        System.out.println("End of media...");
});

您可以添加一个onRepeat事件处理程序,当一个回放周期的媒体结束并且回放将要重复时,将调用该事件处理程序。它在onEndOfMedia事件处理程序之后被调用:

player.setOnRepeat(() -> {
        System.out.println("Repeating...");
});

跟踪媒体时间

显示媒体持续时间和播放所用的时间对观众来说是一个重要的反馈。很好地理解这些持续时间类型对于开发一个好的媒体播放仪表板是很重要的。不同类型的持续时间可以与媒体相关联:

  • 媒体播放的当前持续时间

  • 媒体播放的持续时间

  • 媒体播放一个周期的持续时间

  • 开始偏移时间

  • 结束偏移时间

默认情况下,媒体按其原始持续时间播放。例如,如果媒体的持续时间为 30 分钟,则媒体将在一个循环中播放 30 分钟。MediaPlayer让您指定回放的长度,可以是媒体持续时间中的任何时间。例如,对于每个回放周期,您可以指定只播放媒体的中间 10 分钟(第 11 到第 12 分钟)。媒体播放的长度由MediaPlayer类的以下两个属性指定:

  • startTime

  • stopTime

这两个属性都属于Duration类型。startTimestopTime分别是媒体在每个周期开始和停止播放的时间偏移量。默认情况下,startTime设置为Duration.ZERO,而stopTime设置为媒体的持续时间。以下代码片段设置了这些属性,因此媒体将从第 10 分钟播放到第 21 分钟:

player.setStartTime(Duration.minutes(10));
player.setStartTime(Duration.minutes(21));

以下限制适用于startTimestopTime值:

0 ≤ startTime < stopTime
startTime < stopTime ≤ Media.duration

只读的currentTime属性是媒体播放中的当前时间偏移。只读的cycleDuration属性是stopTimestartTime的区别。它是每个循环的播放长度。The只读totalDuration属性指定播放的总持续时间,如果播放被允许继续直到结束。它的值是cycleDuration乘以cycleCount。如果cycleCountINDEFINITE,则totalDurationINDEFINITE。如果媒体持续时间为UNKNOWN,则totalDuration将为UNKNOWN

当您从网络播放媒体时,MediaPlayer可能会因为没有足够的数据继续播放而停止。只读的bufferProgressTime属性给出了媒体可以不间断播放的持续时间。

控制回放速率

MediaPlayerrate属性指定回放的速率。有效范围是 0.0 到 8.0。例如,2.0 的rate播放媒体的速度是正常速度的两倍。默认值为 1.0,以正常速率播放媒体。只读的currentRate属性是回放的当前速率。以下代码会将速率设置为正常速率的三倍:

// Play the media at 3x
player.setRate(3.0);

控制播放音量

MediaPlayer类中的三个属性控制媒体中音频的音量:

  • volume

  • mute

  • balance

volume指定音频的音量。范围是 0.0 到 1.0。值为 0.0 会使音频听不见,而值为 1.0 会以最大音量播放。默认值为 1.0。

mute指定音频是否由MediaPlayer产生。默认情况下,其值为 false,并产生音频。将其设置为 true 不会产生音频。请注意,设置mute属性不会影响volume属性。假设volume设置为 1.0,静音设置为真。没有产生音频。当mute设置为 false 时,音频将使用 1.0 的volume属性,并以最大音量播放。以下代码将音量设置为一半:

// Play the audio at half the full volumne
player.setVolumne(0.5);
...
// Mute the audio
player.setMute(true)

balance指定左右声道的相对音量。有效范围是–1.0 到 1.0。值–1.0 将左声道的回放设定为正常音量,并将右声道静音。值 1.0 将右声道的回放设置为正常音量,并将左声道静音。默认值为 0.0,将两个通道中的回放设置为正常音量。

定位媒体播放器

您可以使用seek(Duration position)方法将MediaPlayer定位在特定的回放时间:

// Position the media at the fifth minutes play time
player.seek(Duration.minutes(5.0));

调用seek()方法没有任何效果,如果

  • MediaPlayer处于STOPPED状态。

  • 媒体持续时间为Duration.INDEFINITE

  • 您将nullDuration.UNKNOWN传递给seek()方法。

  • 在所有其他情况下,该位置被夹在MediaPlayerstartTimestopTime之间。

在媒体上标记位置

您可以将标记与媒体时间线上的特定点相关联。标记是简单的文本,在许多方面都很有用。你可以用它们来插入广告。例如,您可以插入 URL 作为标记文本。当到达标记时,您可以暂停播放媒体并播放另一个媒体。注意,播放另一个媒体需要创建新的MediaMediaPlayer对象。你可以重用一个MediaView。播放广告视频时,将MediaView与新的MediaPlayer联系起来。广告播放结束后,将MediaView重新关联到主MediaPlayer

Media类包含一个返回ObservableMap<String, Duration>getMarkers()方法。您需要在地图中添加(键,值)对来添加标记。以下代码片段向媒体添加了三个标记:

Media media = ...
ObservableMap<String, Duration> markers = media.getMarkers();
markers.put("START", Duration.ZERO);
markers.put("INTERVAL", media.getDuration().divide(2.0));
markers.put("END", media.getDuration());

当到达一个标记时,MediaPlayer触发一个MediaMarkerEvent。您可以在MediaPlayeronMarker属性中为该事件注册一个处理程序。下面的代码片段显示了如何处理MediaMarkerEvent。事件的getMarker()方法返回一个Pair<String, Duration>,其键和值分别是标记文本和标记持续时间:

// Add a marker event handler
player.setOnMarker((MediaMarkerEvent e) -> {
        Pair<String, Duration> marker = e.getMarker();
        String markerText = marker.getKey();
        Duration markerTime = marker.getValue();
        System.out.println("Reached the marker " + markerText +
               " at " + markerTime);
});

显示媒体元数据

一些元数据可以被嵌入到描述媒体的媒体中。通常,元数据包含标题、艺术家姓名、专辑名称、流派、年份等等。下面的代码片段显示了当MediaPlayer进入READY状态时媒体的元数据。不要试图在创建Media对象后立即读取元数据,因为元数据可能不可用:

Media media = ...
MediaPlayer player = new MediaPlayer(media);

// Display the metadata data on the console
player.setOnReady(() -> {
        ObservableMap<String, Object> metadata = media.getMetadata();
        for(String key : metadata.keySet()) {
            System.out.println(key + " = " + metadata.get(key));
        }
});

您无法确定媒体中是否有元数据或媒体可能包含的元数据类型。在您的应用程序中,您可以只查找标题、艺术家、专辑和年份。或者,您可以读取所有元数据,并在两列表中显示它们。有时,元数据可能包含艺术家的嵌入图像。您需要检查映射中值的类名才能使用该图像。

定制媒体视图

如果媒体有视图(如视频),您可以使用以下属性自定义视频的大小、区域和质量:

  • fitHeight

  • fitWidth

  • preserveRatio

  • smooth

  • viewport

  • x

  • y

fitWidthfitHeight属性分别指定调整后的视频宽度和高度。默认情况下,它们为零,这意味着将使用媒体的原始宽度和高度。

属性指定在调整大小时是否保留媒体的纵横比。默认情况下,它是假的。

smooth属性指定在调整视频大小时使用的过滤算法的质量。默认值取决于*台。如果设置为 true,则使用质量更好的过滤算法。请注意,质量较好的过滤需要更多的处理时间。对于较小的视频,您可以将其设置为 false。对于较大的视频,建议将该属性设置为 true。

视口是一个矩形区域,用于查看图形的一部分。通过viewportxy属性,您可以指定将在MediaView中显示的视频中的矩形区域。视口是在原始媒体帧的坐标系中指定的Rectangle2Dxy属性是视口左上角的坐标。回想一下,一个MediaPlayer可以关联多个MediaViews。将多个MediaViews与视口一起使用,可以给观众分割视频的印象。使用一个带有视窗的MediaView,你可以让观众只看到视频可视区域的一部分。

一个MediaView是一个节点。因此,为了给观众更好的视觉体验,还可以对MediaView应用效果和变换。

开发媒体播放器应用程序

开发一个好看的、可定制的媒体播放器应用程序需要仔细的设计。我已经介绍了 JavaFX 中媒体 API 提供的大部分特性。结合开发用户界面和媒体 API 的知识,您可以设计和开发自己的媒体播放器应用程序。开发应用程序时,请记住以下几点:

  • 应用程序应该能够指定媒体源。

  • 应用程序应该提供一个 UI 来控制媒体播放。

  • 当媒体源改变时,您将需要创建一个新的Media对象和一个MediaPlayer。您可以通过使用setMediaPlayer()方法设置新的MediaPlayer来重用MediaView

摘要

JavaFX 支持通过 JavaFX Media API 播放音频和视频。还支持静态媒体文件和实时提要的 HTTP 实时流。支持多种媒体格式,如 AAC、AIFF、WAV 和 MP3。支持包含 VP6 视频和 MP3 音频的 FLV 以及 H.264/AVC 视频格式的 MPEG-4 多媒体容器。对特定媒体格式的支持取决于*台。某些媒体播放功能和格式不需要任何额外安装;但是有些需要安装第三方软件。媒体 API 由几个类组成。API 中的所有类都包含在javafx.scene.media包中。

一个AudioClip用于以最小的延迟播放一个短的音频剪辑。通常,这对于声音效果很有用,声音效果通常是很短的音频剪辑。使用MediaMediaPlayerMediaView类播放较长的音频和视频。

MediaMediaPlayer类用于播放音频和视频。Media类的一个实例代表一个媒体资源,可以是音频或视频。它提供有关介质的信息,例如介质的持续时间。MediaPlayer类的一个实例提供了播放媒体的控件。一个MediaPlayer总是指示播放的状态。只读的status属性表示MediaPlayer的当前状态。当在MediaPlayer上执行一个动作时status改变。状态可以是未知、就绪、正在播放、暂停、停止、停止或已处置。

MediaView类的一个实例提供了由MediaPlayer播放的媒体的视图。一个MediaView用于观看视频。

当您尝试播放媒体时,可能会出现一些问题,例如,媒体格式可能不受支持,或者媒体内容可能已损坏。MediaException类的一个实例表示在媒体回放期间可能发生的特定类型的媒体错误。当出现与介质相关的错误时,会生成一个MediaErrorEvent。您可以通过向媒体对象添加适当的事件处理程序来处理该错误。

下一章将讨论 FXML,这是一种基于 XML 的语言,用于为 JavaFX 应用程序构建用户界面。

二十六、理解 FXML

在本章中,您将学习:

  • 什么是 FXML

  • 如何编辑 FXML 文档

  • FXML 文档的结构

  • 如何在 FXML 文档中创建对象

  • 如何在 FXML 文档中指定资源的位置

  • 如何在 FXML 文档中使用资源包

  • 如何从一个 FXML 文档引用其他 FXML 文档

  • 如何在 FXML 文档中引用常量

  • 如何引用其他元素以及如何在 FXML 文档中复制元素

  • 如何在 FXML 文档中绑定属性

  • 如何使用 FXML 创建自定义控件

本章的例子在com.jdojo.fxml包中。为了让它们工作,您必须在module-info.java文件中添加相应的一行:

...
opens com.jdojo.fxml to javafx.graphics, javafx.base;
...

什么是 FXML?

FXML 是一种基于 XML 的语言,旨在为 JavaFX 应用程序构建用户界面。您可以使用 FXML 构建整个场景或场景的一部分。FXML 允许应用程序开发人员将构建 UI 的逻辑与业务逻辑分开。如果应用程序的 UI 部分发生变化,您不需要重新编译 JavaFX 代码。相反,您可以使用文本编辑器更改 FXML 并重新运行应用程序。您仍然使用 JavaFX 通过 Java 语言编写业务逻辑。FXML 文档是 XML 文档。理解本章需要 XML 的基础知识。

JavaFX 场景图是 Java 对象的层次结构。XML 格式非常适合存储表示某种层次结构的信息。所以用 FXML 存储场景图是非常直观的。在 JavaFX 应用程序中使用 FXML 构建场景图是很常见的。然而,FXML 的使用并不仅限于构建场景图。它可以构建 Java 对象的分层对象图。事实上,它只能用来创建一个对象,比如一个Person类的对象。

让我们快速预览一下 FXML 文档的样子。首先,创建一个简单的 UI,它由一个带有一个 ?? 的 ?? 和一个 ?? 组成。清单 26-1 包含了构建 UI 的 JavaFX 代码,这是您所熟悉的。清单 26-2 包含了用于构建相同 UI 的 FXML 版本。

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

<?import javafx.scene.layout.VBox?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.Button?>

<VBox>
        <children>

                <Label text="FXML is cool"/>
                <Button text="Say Hello"/>
        </children>
</VBox>

Listing 26-2A Code Snippet to Build an Object Graph in FXML

import javafx.scene.layout.VBox;
import javafx.scene.control.Label;
import javafx.scene.control.Button;

VBox root = new VBox();
root.getChildren().addAll(new Label("FXML is cool"), new Button("Say Hello"));

Listing 26-1A Code Snippet to Build an Object Graph in JavaFX

FXML中的第一行是 XML 解析器使用的标准 XML 声明。它在 FXML 中是可选的。如果省略,则版本和编码分别假定为 1 和 UTF-8。接下来的三行是导入语句,对应于 Java 代码中的导入语句。代表 UI 的元素,如VBoxLabelButton,与 JavaFX 类同名。<children>标签指定了VBox的子节点。使用各自元素的text属性来指定LabelButton的文本属性。

编辑 FXML 文档

FXML 文档只是一个文本文件。通常,文件名有一个.fxml扩展名(例如hello.fxml)。例如,您可以使用记事本在 Windows 中创建 FXML 文档。如果您使用过 XML,就会知道在文本编辑器中编辑大型 XML 文档并不容易。胶子公司提供了一个名为场景构建器的可视化编辑器,用于编辑 FXML 文档。场景构建器是开源的。可以从 https://gluonhq.com/products/scene-builder/ 下载其最新版本。Scene Builder 也可以集成到一些 ide 中,因此您可以在 IDE 中使用 Scene Builder 编辑 FXML 文档。本书不讨论场景构建器。

FXML 基础

本节涵盖了 FXML 的基础知识。您将开发一个简单的 JavaFX 应用程序,它由以下内容组成:

  • VBox

  • Label

  • Button

VBoxspacing属性被设置为 10px。LabelButtontext属性被设置为“FXML 太酷了!”还有“问好”。当点击Button时,Label中的文本变为“Hello from FXML!”。图 26-1 显示了应用程序显示的窗口的两个实例。

img/336502_2_En_26_Fig1_HTML.png

图 26-1

窗口的两个实例,其场景图形是使用 FXML 创建的

清单 26-3 中的程序是示例应用程序的 JavaFX 实现。如果你已经完成了书中的这一章,这个程序应该很容易。

// HelloJavaFX.java
package com.jdojo.fxml;

import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class HelloJavaFX extends Application {
        private final Label msgLbl = new Label("FXML is cool!");

        private final Button sayHelloBtn = new Button("Say Hello");

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

        @Override
        public void start(Stage stage) {
                // Set the preferred width of the label
                msgLbl.setPrefWidth(150);

                // Set the ActionEvent handler for the button
                sayHelloBtn.setOnAction(this::sayHello);

                VBox root = new VBox(10);
                root.getChildren().addAll(msgLbl, sayHelloBtn);
                root.setStyle("""
                         -fx-padding: 10;
                   -fx-border-style: solid inside;
                   -fx-border-width: 2;
                   -fx-border-insets: 5;
                   -fx-border-radius: 5;
                   -fx-border-color: blue;""");
                Scene scene = new Scene(root);
                stage.setScene(scene);
                stage.setTitle("Hello FXML");
                stage.show();
        }

        public void sayHello(ActionEvent e) {
                msgLbl.setText("Hello from FXML!");
        }

}

Listing 26-3The JavaFX Version of the FXML Example Application

创建 FXML 文件

让我们创建一个 FXML 文件sayhello.fxml。将文件存储在resources/fxml目录中,其中resources目录由ResourceUtil类正确寻址。

添加 UI 元素

FXML 文档的根元素是对象图中的顶级对象。你的顶层对象是一个VBox。因此,FXML 的根元素应该是

<VBox>
</VBox>

你怎么知道要在对象图中表示一个VBox,你需要在 FXML 中使用一个<VBox>标签?这既困难又容易。这很困难,因为没有关于 FXML 标记的文档。这很简单,因为 FXML 有几条规则解释标签名的构成。例如,如果一个标记名是一个类的简单名或完全限定名,该标记将创建该类的一个对象。前面的元素将创建一个VBox类的对象。可以使用完全限定的类名重写前面的 FXML:

<javafx.scene.layout.VBox>
</javafx.scene.layout.VBox>

在 JavaFX 中,布局窗格有子级。在 FXML 中,布局窗格的子元素是子元素。您可以为VBox添加一个Label和一个Button,如下所示:

<VBox>
        <Label></Label>
        <Button></Button>
</VBox>

这为这个示例应用程序定义了对象图的基本结构。它将创建一个带有一个Label和一个ButtonVBox。剩下的讨论将集中在添加细节上,例如,为控件添加文本和为VBox设置样式。

前面的 FXML 显示了LabelButtonVBox的子元素。从 GUI 的角度来看,确实如此。但是,从技术上来说,它们属于VBox对象的children属性,而不直接属于VBox。为了更专业(也更详细),您可以重写前面的 FXML,如下所示:

<VBox>
        <children>
                <Label></Label>
                <Button></Button>
        <children>
</VBox>

您如何知道可以忽略前面 FXML 中的<children>标签,并仍然得到相同的结果?JavaFX 库在javafx.beans包中包含一个注释DefaultProperty。它可以用来注释类。它包含一个String类型的值元素。元素指定类的属性,该属性应被视为 FXML 中的默认属性。如果 FXML 中的子元素不表示其父元素的属性,则它属于父元素的默认属性。VBox类继承自Pane类,其声明如下:

@DefaultProperty(value="children")
public class Pane extends Region {...}

Pane类的注释使children属性成为 FXML 中的默认属性。VBoxPane类继承了这个注释。这就是前面的 FXML 中可以省略<children>标签的原因。如果您在一个类上看到了DefaultProperty注释,这意味着您可以省略 FXML 中默认属性的标签。

在 FXML 中导入 Java 类型

要在 FXML 中使用 Java 类的简单名称,必须像在 Java 程序中一样导入类。有一个例外。在 Java 程序中,不需要从java.lang包中导入类。然而,在 FXML 中,您需要从所有包中导入类,包括java.lang包。导入处理指令用于从包中导入一个类或所有类。以下处理指令导入了VBoxLabelButton类:

<?import javafx.scene.layout.VBox?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.Button?>

以下导入处理指令从javafx.scene.controljava.lang包中导入所有类:

<?import javafx.scene.control.*?>
<?import java.lang.*?>

FXML 不支持导入静态成员。请注意,import 语句不使用尾随分号。

在 FXML 中设置属性

您可以在 FXML 中设置 Java 对象的属性。如果属性声明遵循 JavaBean 约定,则可以在 FXML 中设置对象的属性。设置属性有两种方式:

  • 使用 FXML 元素的属性

  • 使用属性元素

属性名称或属性元素名称与正在设置的属性的名称相同。下面的 FXML 创建一个Label并使用属性设置其text属性:

<Label text="FXML is cool!"/>

下面的 FXML 使用属性元素实现了同样的目的:

<Label>
        <text>FXML is cool!</text>
</Label>

下面的 FXML 创建一个Rectangle,并使用属性设置它的xywidthheightfill属性:

<Rectangle x="10" y="10" width="100" height="40" fill="red"/>

FXML 将属性值指定为Strings。自动应用适当的转换将String值转换为所需的类型。在前面的例子中,fill属性的值“红色”将被自动转换成一个Color对象,width属性的值“100”将被转换成一个双精度值,依此类推。

使用属性元素设置对象属性更加灵活。当可以从String自动转换类型时,可以使用属性。假设您想将一个Person类的对象设置为一个对象的属性。这可以使用属性元素来完成。下面的 FXML 设置了类MyCls的对象的person属性:

<MyCls>
        <person>
                <Person>
                        <!-- Configure the Person object here -->
                </Person>
        </person>
</MyCls>

只读属性是有 getter 但没有 setter 的属性。使用 property 元素可以在 FXML 中设置两种特殊类型的只读属性:

  • 只读的List属性

  • 只读的Map属性

使用 property 元素设置只读的List属性。property 元素的所有子元素都将被添加到属性 getter 返回的List中。下面的 FXML 设置了一个VBox的只读children属性:

<VBox>
        <children>
                <Label/>
                <Button/>
        <children>
</VBox>

您可以使用 property 元素的属性向只读的Map属性添加条目。属性的名称和值成为Map中的键和值。下面的代码片段声明了一个类Item,它有一个只读的map属性:

public class Item {
        private Map<String, Integer> map = new HashMap<>();
        public Map getMap() {
                return map;
        }
}

下面的 FXML 创建一个Item对象,并用两个条目(“n1”,100)和(“n2”,200)设置它的map属性。注意属性 n1 和 n2 的名称成为了Map中的键:

<Item>
        <map n1="100" n2="200"/>
</Item>

Java 对象有一种特殊类型的属性,称为静态属性。静态属性未在对象的类上声明。相反,它是使用另一个类的静态方法设置的。假设您想要为将被放置在VBox中的Button设置边距。JavaFX 代码如下所示:

Button btn = new Button("OK");
Insets insets = new Insets(20.0);;
VBox.setMargin(btn, insets);
VBox vbox = new VBox(btn);

通过为Button设置一个VBox.margin属性,您可以在 FXML 中实现同样的功能:

<?import javafx.scene.layout.VBox?>
<?import javafx.scene.control.Button?>
<?import javafx.geometry.Insets?>

<VBox>
    <Button text="OK">
        <VBox.margin>
            <Insets top="20.0" right="20.0" bottom="20.0" left="20.0"/>
        </VBox.margin>
    </Button>
</VBox>

您不能从String创建Insets对象,因此,您不能使用属性来设置 margin 属性。您需要使用属性元素来设置它。当您在 FXML 中使用GridPane时,您可以设置rowIndexcolumnIndex静态,如下所示:

<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.control.Button?>

<GridPane>
        <Button text="OK">
                <GridPane.rowIndex>0</GridPane.rowIndex>
                <GridPane.columnIndex>0</GridPane.columnIndex>
        </Button>
</GridPane>

因为rowIndexcolumnIndex属性也可以表示为Strings,所以可以使用属性来设置它们:

<GridPane>
        <Button text="OK" GridPane.rowIndex="0" GridPane.columnIndex="0"/>
</GridPane>

指定 FXML 命名空间

FXML 没有 XML 架构。它使用的名称空间需要使用名称空间前缀“fx”来指定。在大多数情况下,FXML 解析器会计算出标记名,比如作为类的标记名、类的属性等等。fxML 使用特殊的元素和属性名,必须用“FX”名称空间前缀进行限定。以下 fxML 声明了“FX”命名空间前缀:

<VBox xmlns:fx="http://javafx.com/fxml">...</VBox>

或者,您可以在命名空间 URI 中追加 FXML 的版本。FXML 解析器将验证它可以解析指定的 XML 代码。在撰写本文时,唯一支持的版本是 1.0:

<VBox xmlns:fx="http://javafx.com/fxml/1.0">...</VBox>

FXML 版本可以包括点、下划线和破折号。只比较下划线和破折号第一次出现之前的数字。以下三个声明都将 FXML 版本指定为 1.0:

<VBox xmlns:fx="http://javafx.com/fxml/1">...</VBox>
<VBox xmlns:fx="http://javafx.com/fxml/1.0-ea">...</VBox>
<VBox xmlns:fx="http://javafx.com/fxml/1.0-rc1-2014_03_02">...</VBox>

Tip

<fx:script>标签就是这种命名空间标签的一个例子。它用于向 FXML 文件添加脚本逻辑。但是,尽量避免。首先,FXML 中的脚本支持似乎不是很稳定。其次,将处理逻辑添加到 FXML 前端定义中被认为不是一种好的编程风格。最好使用控制器,我们简称它。

为对象分配标识符

在 FXML 中创建的对象可以在同一文档的其他地方引用。在 JavaFX 代码中获取在 FXML 中创建的 UI 对象的引用是很常见的。您可以通过首先用一个fx:id属性标识 FXML 中的对象来实现这一点。属性的值是对象的标识符。如果对象类型有一个id属性,该属性的值也将被设置。注意 JavaFX 中的每个Node都有一个id属性,可以用来在 CSS 中引用它们。以下是为Label指定fx:id属性的示例:

<Label fx:id="msgLbl" text="FXML is cool!"/>

现在,您可以使用msgLbl来引用Label。属性fx:id有几种用法。例如,它用于在加载 FXML 时将 UI 元素的引用注入到 JavaFX 类的实例变量中。我将在单独的部分讨论这一点。

添加事件处理程序

可以为 FXML 中的节点设置事件处理程序。设置事件处理程序类似于设置任何其他属性。JavaFX 类定义了onXxx属性来为Xxx事件设置事件处理程序。例如,Button类包含一个onAction属性来设置一个ActionEvent处理程序。在 FXML 中,可以指定两种类型的事件处理程序:

  • 编写事件处理程序脚本

  • 控制器事件处理程序

在本书中,我们只讨论控制器事件处理程序,因为通常最好将编程逻辑远离 GUI。我将在“在 FXML 中使用控制器”一节中讨论如何指定控制器事件处理程序。

*### 正在加载 FXML 文档

FXML 文档定义了 JavaFX 应用程序的视图(GUI)部分。您需要加载 FXML 文档来获得它所代表的对象图。加载 FXML 是由FXMLLoader类的一个实例执行的,它在javafx.fxml包中。

FXMLLoader类提供了几个构造器,允许您指定位置、字符集、资源包和其他用于加载文档的元素。您至少需要指定 FXML 文档的位置,这是一个URL。该类包含执行文档实际加载的load()方法。以下代码片段从 Windows 中的本地文件系统加载 FXML 文档:

String fxmlDocUrl = "file:///C:/resources/fxml/test.fxml";
URL fxmlUrl = new URL(fxmlDocUrl);
FXMLLoader loader = new FXMLLoader();
loader.setLocation(fxmlUrl);
VBox root = loader.<VBox>load();

load()方法有一个通用的返回类型。在前面的代码片段中,您已经在对load()方法(loader.<VBox>load())的调用中清楚地表达了您的意图,即您期望从 FXML 文档中得到一个VBox实例。如果您愿意,可以省略通用参数:

// Will work
VBox root = loader.load();

FXMLLoader支持使用InputStream加载 FXML 文档。下面的代码片段使用一个InputStream加载相同的 FXML 文档:

FXMLLoader loader = new FXMLLoader();
String fxmlDocPath = "C:\\resources\\fxml\\test.fxml";
FileInputStream fxmlStream = new FileInputStream(fxmlDocPath);
VBox root = loader.<VBox>load(fxmlStream);

在内部,FXMLLoader使用流读取文档,这可能会抛出一个IOException。所有版本的 l oad()方法在FXMLLoader类中抛出IOException。您在前面的示例代码中省略了异常处理代码。在您的应用程序中,您需要处理异常。

FXMLLoader类包含了几个版本的load()方法。有些是实例方法,有些是静态方法。如果您想从加载器中检索更多信息,比如控制器引用、资源包、位置、字符集和根对象,您需要创建一个FXMLLoader实例并使用 instance load()方法。如果你只想加载一个 FXML 文档而不考虑任何其他细节,你需要使用静态的load()方法。以下代码片段使用静态load()方法加载 FXML 文档:

String fxmlDocUrl = "file:///C:/resources/fxml/test.fxml";
URL fxmlUrl = new URL(fxmlDocUrl);
VBox root = FXMLLoader.<VBox>load(fxmlUrl);

加载 FXML 文档后,下一步做什么?至此,FXML 的作用已经结束,您的 JavaFX 代码应该接管了。我将在本文后面讨论加载器。

清单 26-4 中的程序有这个例子的 JavaFX 代码。它加载存储在 sayHello.fxml 文件中的 FXML 文档。程序使用ResourceUtil实用程序类加载文档。加载器返回一个VBox,它被设置为场景的根。除了在start()方法的声明中有一处不同之外,代码的其余部分与您一直使用的相同。该方法声明它可能抛出一个IOException,这是您必须添加的,因为您已经在方法内部调用了FXMLLoaderload()方法。运行程序时,显示如图 26-1 所示的窗口。

// SayHelloFXML.java
package com.jdojo.fxml;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import java.io.IOException;
import java.net.URL;
import com.jdojo.util.ResourceUtil;
import javafx.scene.Scene;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

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

        @Override
        public void start(Stage stage) throws IOException {
            // Construct a URL for the FXML document
                URL fxmlUrl =
                    ResourceUtil.getResourceURL("fxml/sayhello.fxml");

                // Load the FXML document
                VBox root = FXMLLoader.<VBox>load(fxmlUrl);
                Scene scene = new Scene(root);
                stage.setScene(scene);
                stage.setTitle("Hello FXML");
                stage.show();
        }
}

Listing 26-4Using FXML to Build the GUI

如果你点击这个按钮,什么都不会发生。我们将在下一节讨论将 UI 连接到 Java 代码。

在 FXML 中使用控制器

控制器只是一个类名,其对象由 FXML 创建,用于初始化 UI 元素。FXML 允许您使用fx:controller属性在根元素上指定一个控制器。注意,每个 FXML 文档只允许一个控制器,如果指定了控制器,必须在根元素上指定。

下面的 FXML 为VBox元素指定了一个控制器:

<VBox fx:controller="com.jdojo.fxml.SayHelloController"
      xmlns:fx="http://javafx.com/fxml">
</VBox>

控制器需要符合一些规则,它可以用于不同的原因:

  • 控制器由 FXML 加载器实例化。

  • 控制器必须有一个公共的无参数构造器。如果它不存在,FXML 加载器将无法实例化它,这将在加载时引发异常。

  • 控制器可以有可访问的方法,这些方法可以被指定为 FXML 中的事件处理程序。关于“可访问”的含义,请参考下面的讨论

  • FXML 加载器将自动寻找控制器的可访问实例变量。如果可访问实例变量的名称与元素的fx:id属性匹配,则 FXML 中的对象引用会自动复制到控制器实例变量中。这个特性使得控制器可以引用 FXML 中的 UI 元素。控制器可以在以后使用它们,比如将它们绑定到模型。

  • 控制器可以有一个可访问的initialize()方法,该方法应该不带参数,返回类型为void。FXML 加载器将在 FXML 文档加载完成后调用initialize()方法。

清单 26-5 显示了您将在本例中使用的控制器类的代码。

// SayHelloController.java
package com.jdojo.fxml;

import java.net.URL;
import java.util.ResourceBundle;
import javafx.fxml.FXML;
import javafx.scene.control.Label;

public class SayHelloController {
        // The reference of msgLbl will be injected by the FXML loader
        @FXML
        private Label msgLbl;

        // location and resources will be automatically injected by the
           // FXML loader
        @FXML
        private URL location;

        @FXML
        private ResourceBundle resources;

        // Add a public no-args constructor explicitly just to
        // emphasize that it is needed for a controller
        public SayHelloController() {
        }

        @FXML
        private void initialize() {
                System.out.println("Initializing SayHelloController...");
                System.out.println("Location = " + location);
                System.out.println("Resources = " + resources);
        }

        @FXML
        private void sayHello() {
                msgLbl.setText("Hello from FXML!");
        }
}

Listing 26-5A Controller Class

控制器类在一些成员上使用了一个@FXML注释。@FXML注释可以用在字段和方法上。它不能用于类和构造器。通过在成员上使用@FXML注释,您声明了 FXML 加载器可以访问成员,即使它是私有的。FXML loader 使用的公共成员不需要用@FXML注释。然而,用@FXML注释公共成员并不是错误。最好用@FXML注释来注释 FXML 加载器使用的所有成员,公共的和私有的。这告诉代码的读者成员是如何被使用的。

下面的 FXML 将控制器类的sayHello()方法设置为Button的事件处理程序:

<VBox fx:controller="com.jdojo.fxml.SayHelloController"
      xmlns:fx="http://javafx.com/fxml">
        <Button fx:id="sayHelloBtn" text="Say Hello" onAction="#sayHello"/>
...
</VBox>

有两个特殊的实例变量可以在控制器中声明,它们由 FXML 加载器自动注入:

  • @FXML private URL location;

  • @FXML private ResourceBundle resources;

location是 FXML 文档的位置。resources是 FXML 中使用的ResourceBundle的引用,如果有的话。

当事件处理程序属性值以散列符号(#)开始时,它向 FXML loader 表明sayHello是控制器中的方法。控制器中的事件处理程序方法应该符合一些规则:

  • 该方法可以不带参数,也可以只带一个参数。如果它接受一个参数,参数类型必须是与它应该处理的事件兼容的类型赋值。

  • 拥有该方法的两个版本并不是错误:一个不带参数,另一个只有一个参数。在这种情况下,使用带有单个参数的方法。

  • 按照惯例,方法返回类型应该是void,因为没有返回值的接受者。

  • FXML 加载器必须可以访问该方法:将其公开或者用@FXML对其进行注释。

当 FXML 加载器加载完 FXML 文档后,它调用控制器的initialize()方法。该方法不应采用任何参数。FXML 加载器应该可以访问它。在控制器中,您使用了@FXML注释使 FXML 加载器可以访问它。

FXMLLoader类允许您使用setController()方法为代码中的根元素设置控制器。使用getController()方法从加载器获取控制器的参考。开发人员在获取控制器的引用时会犯一个常见的错误。这个错误是由于load()方法的设计方式造成的。load()方法有七个重载版本:其中两个是实例方法,五个是静态方法。要使用getController()方法,必须创建一个FXMLLoader类的对象,并确保使用该类的一个实例方法来加载文档。下面是一个常见错误的例子:

URL fxmlUrl = new URL("file:///C:/resources/fxml/test.fxml");

// Create an FXMLLoader object – a good start
FXMLLoader loader = new FXMLLoader();

// Load the document -- mistake
VBox root = loader.<VBox>load(fxmlUrl);

// loader.getController() will return null
Test controller = loader.getController();
// controller is null here

前面的代码创建了一个FXMLLoader类的对象。然而,在loader变量中调用的load(URL url)方法是静态的load()方法,而不是实例load()方法。因此,loader实例从未获得控制器,当您向它请求控制器时,它会返回null。为了消除混淆,下面是load()方法的实例和静态版本,其中只有前两个版本是实例方法:

  • <T> T load()

  • <T> T load(InputStream inputStream)

  • static <T> T load(URL location)

  • static <T> T load(URL location, ResourceBundle resources)

  • static <T> T load(URL location, ResourceBundle resources, BuilderFactory builderFactory)

  • static <T> T load(URL location, ResourceBundle resources, BuilderFactory builderFactory, Callback<Class<?>,Object> controllerFactory)

  • static <T> T load(URL location, ResourceBundle resources, BuilderFactory builderFactory, Callback<Class<?>,Object> controllerFactory, Charset charset)

以下代码片段是使用load()方法的正确方式,因此您可以在 JavaFX 代码中获得控制器的引用:

URL fxmlUrl = new URL("file:///C:/resources/fxml/test.fxml");

// Create an FXMLLoader object – a good start
FXMLLoader loader = new FXMLLoader();
loader.setLocation(fxmlUrl);

// Calling the no-args instance load() method - Correct
VBox root = loader.<VBox>load();

// loader.getController() will return the controller
Test controller = loader.getController();

现在,您已经有了这个示例应用程序的控制器。让我们修改 FXML 来匹配控制器。清单 26-6 显示了修改后的 FXML。它保存在资源/fxml 目录下的 sayhellowithcontroller.fxml 文件中。

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

<?import javafx.scene.Scene?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.Button?>

<VBox fx:controller="com.jdojo.fxml.SayHelloController" spacing="10" xmlns:fx="http://javafx.com/fxml">
        <Label fx:id="msgLbl" text="FXML is cool!" prefWidth="150"/>
        <Button fx:id="sayHelloBtn" text="Say Hello" onAction="#sayHello"/>
        <style>
                -fx-padding: 10;
                -fx-border-style: solid inside;
                -fx-border-width: 2;
                -fx-border-insets: 5;
                -fx-border-radius: 5;
                -fx-border-color: blue;
        </style>
</VBox>

Listing 26-6The Contents of the sayhellowithcontroller.fxml File

清单 26-7 中的程序是这个例子的 JavaFX 应用程序。代码与清单 26-4 中显示的代码非常相似。主要的区别是使用控制器的 FXML 文档。加载文档时,加载器调用控制器的initialize()方法。该方法打印一条消息,包括所使用的资源包引用的位置。当您单击按钮时,控制器的sayHello()方法被调用,该方法在Label中设置文本。请注意,Label引用是由 FXML 加载器自动注入控制器的。

// SayHelloFXMLMain.java
package com.jdojo.fxml;

import java.io.IOException;
import java.net.URL;
import com.jdojo.util.ResourceUtil;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

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

        @Override
        public void start(Stage stage) throws IOException {
                // Construct a URL for the FXML document
                     URL fxmlUrl = ResourceUtil.getResourceURL(
                         "fxml/sayhellowithcontroller.fxml");
                VBox root = FXMLLoader.<VBox>load(fxmlUrl);
                Scene scene = new Scene(root);
                stage.setScene(scene);
                stage.setTitle("Hello FXML");
                stage.show();
        }
}

Listing 26-7A JavaFX Application Class Using FXML and a Controller

在 FXML 中创建对象

使用 FXML 的主要目的是创建一个对象图。所有类的对象不是以相同的方式创建的。例如,一些类提供构造器来创建它们的对象,一些静态的valueOf()方法,和一些工厂方法。FXML 应该能够创建所有类的对象,或者至少应该给你一些控制权来决定如何创建这些对象。在下面几节中,我将讨论在 FXML 中创建对象的不同方法。

使用无参数构造器

使用无参数构造器在 FXML 中创建对象很容易。如果一个元素名是一个类名,它有一个无参数的构造器,那么这个元素将创建一个该类的对象。下面的元素创建了一个VBox对象,因为VBox类有一个无参数构造器:

<VBox>
        ...
</VBox>

使用静态 valueOf()方法

有时候,不可变类提供了一个valueOf()方法来构造一个对象。如果valueOf()方法被声明为静态的,它可以接受单个String参数并返回一个对象。您可以使用fx:value属性通过方法创建一个对象。假设你有一个 Xxx 类,它包含一个静态的valueOf(String s)方法。以下是 Java 代码:

Xxx x = Xxx.valueOf("a value");

在 FXML 中也可以这样做

<Xxx fx:value="a value"/>

请注意,您已经声明了valueOf()方法应该能够接受一个String参数,该参数限定了该类别中的以下两个方法:

  • public static Xxx valueOf(String arg)

  • public static Xxx valueOf(Object arg)

以下元素创建了值为 100 和“Hello”的LongString对象:

<Long fx:value="100"/>
<String fx:value="Hello"/>

注意,String类包含一个创建空字符串的无参数构造器。如果您需要一个内容为空字符串的String对象,您仍然可以使用无参数构造器:

<!-- Will create a String object with "" as the content -->
<String/>

当使用前面的元素时,不要忘记导入类LongString,因为 FXML 不会自动从java.lang包中导入类。

值得注意的是,fx:value属性创建的对象类型是从valueOf()对象返回的对象类型,而不是元素的类类型。考虑下面这个Yyy类的方法声明:

public static Zzz valueOf(String arg);

以下元素将创建什么类型的对象?

<Yyy fx:value="hello"/>

如果你的答案是Yyy,那就错了。一般认为元素名是Yyy,所以创建了一个Yyy类型的对象。前面的元素与调用Yyy.valueOf("Hello")相同,后者返回一个Zzz类型的对象。因此,前面的元素创建了一个Zzz类型的对象,而不是Yyy类型的对象。尽管这种用例是可能的,但这是一种令人困惑的设计类的方式。通常,类Xxx中的valueOf()方法返回一个Xxx类型的对象。

使用工厂方法

有时,一个类提供工厂方法来创建它的对象。如果一个类包含一个返回对象的静态无参数方法,那么可以使用带有fx:factory属性的方法。下面的元素使用LocalDate类的now()工厂方法在 FXML 中创建一个LocalDate:

<?import java.time.LocalDate?>
<LocalDate fx:factory="now"/>

有时,您需要在 FXML 中创建 JavaFX 集合。FXCollections类包含几个创建集合的工厂方法。下面的 FXML 片段创建了一个ObservableList<String>,将四个水果名称添加到列表中:

<?import java.lang.String?>
<?import javafx.collections.FXCollections?>
<FXCollections fx:factory="observableArrayList">
        <String fx:value="Apple"/>
        <String fx:value="Banana"/>
        <String fx:value="Grape"/>
        <String fx:value="Orange"/>
</FXCollections>

清单 26-8 中的 FXML 是使用fx:factory属性创建ObservableList的一个例子。该列表用于设置一个ComboBoxitems属性。列表中的值“橙色”被设置为默认值。VBox将显示一个Label和一个ComboBox,上面列出了四种水果的名称。

<?import javafx.scene.layout.VBox?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ComboBox?>
<?import java.lang.String?>
<?import javafx.collections.FXCollections?>

<VBox xmlns:fx="http://javafx.com/fxml">
        <Label text="List of Fruits"/>
        <ComboBox>
            <items>
                <FXCollections fx:factory="observableArrayList">
                        <String fx:value="Apple"/>
                        <String fx:value="Banana"/>
                        <String fx:value="Grape"/>
                        <String fx:value="Orange"/>
                </FXCollections>
            </items>

            <value>
                <String fx:value="Orange"/>
            </value>
        </ComboBox>
</VBox>

Listing 26-8Creating a ComboBox, Populating It, and Selecting an Item

使用生成器

如果FXMLLoader不能创建一个类的对象,它会寻找一个可以创建该对象的构建器。构建器是Builder接口的一个实现。接口在javafx.util包中,它包含一个方法build():

public interface Builder<T> {
   public T build();
}

知道如何构建一个特定类型的对象。一个Builder与一个BuilderFactory一起使用,后者是同一个包中的另一个接口:

public interface BuilderFactory {
    public Builder<?> getBuilder(Class<?> type);
}

FXMLLoader允许你使用一个BuilderFactory。当它不能使用所有其他方法创建一个类的对象时,它通过传递对象的类型作为方法参数来调用BuilderFactorygetBuilder()方法。如果BuilderFactory返回一个非空的Builder,加载程序将在Builder中设置正在创建的对象的所有属性。最后,它调用Builderbuild()方法来获取对象。FXMLLoader类使用JavaFXBuilderFactory的一个实例作为默认的BuilderFactory

FXMLLoader支持两种类型的Builders:

  • 如果Builder实现了Map接口,则使用put()方法将对象属性传递给Builder。向put()方法传递属性的名称和值。

  • 如果Builder没有实现Map接口,那么对于 FXML 中指定的所有属性,Builder应该包含基于 JavaBeans 约定的 getter 和 setter 方法。

考虑清单 26-9 中Item类的声明。默认情况下,FXML 不能创建一个Item对象,因为它没有无参数构造器。该类有两个属性,id 和 name。

// Item.java
package com.jdojo.fxml;

public class Item {
        private Long id;
        private String name;

        public Item(Long id, String name) {
                this.id = id;
                this.name = name;
        }

        public Long getId() {
                return id;
        }

        public void setId(Long id) {
                this.id = id;
        }

        public String getName() {
                return name;
        }

        public void setName(String name) {
                this.name = name;
        }

        @Override
        public String toString() {
                return "id=" + id + ", name=" + name;
        }
}

Listing 26-9An Item Class That Does Not Have a no-args Constructor

清单 26-10 包含一个 FXML 文件items.fxml的内容。它用Item类的三个对象创建了一个ArrayList。如果您使用FXMLLoader加载这个文件,您会收到一个错误,提示加载程序无法实例化Item类。

<!-- items.fxml -->
<?import com.jdojo.fxml.Item?>
<?import java.util.ArrayList?>
<ArrayList>
        <Item name="Kishori" id="100"/>
        <Item name="Ellen" id="200"/>
        <Item name="Kannan" id="300"/>
</ArrayList>

Listing 26-10FXML to Create a List of Item Objects

让我们创建一个Builder来构建一个Item类的对象。清单 26-11 中的ItemBuilder类是Item类的Builder。它声明了idname实例变量。当FXMLLoader遇到这些属性时,加载程序将调用相应的 setters。setters 将值存储在实例变量中。当加载器需要对象时,它调用build()方法,该方法构建并返回一个Item对象。

// ItemBuilder.java
package com.jdojo.fxml;

import javafx.util.Builder;

public class ItemBuilder implements Builder<Item> {
        private Long id;
        private String name;

        public Long getId() {
                return id;
        }

        public String getName() {
                return name;
        }

        public void setId(Long id) {
                this.id = id;
        }

        public void setName(String name) {
                this.name = name;
        }

        @Override
        public Item build() {
                return new Item(id, name);
        }

}

Listing 26-11A Builder for the Item Class That Uses Property Setters to Build an Object

现在,您需要为Item类型创建一个BuilderFactory。清单 26-12 中显示的ItemBuilderFactory类实现了BuilderFactory接口。当getBuilder()被传递给Item类型时,它返回一个ItemBuilder对象。否则,它返回默认的 JavaFX builder。

// ItemBuilderFactory.java
package com.jdojo.fxml;

import javafx.util.Builder;
import javafx.util.BuilderFactory;
import javafx.fxml.JavaFXBuilderFactory;

public class ItemBuilderFactory implements BuilderFactory {
        private final JavaFXBuilderFactory fxFactory =
              new JavaFXBuilderFactory();

        @Override

        public Builder<?> getBuilder(Class<?> type) {
                // You supply a Builder only for Item type
                if (type == Item.class) {
                        return new ItemBuilder();
                }

                // Let the default Builder do the magic
                return fxFactory.getBuilder(type);
        }
}

Listing 26-12A BuilderFactory to Get a Builder for the Item Type

清单 26-13 和 26-14 有Item类型的BuilderBuilderFactory实现的代码。这次,Builder通过扩展AbstractMap类实现了Map接口。它覆盖了put()方法来读取传入的属性及其值。entrySet()方法需要被覆盖,因为它在AbstractMap类中被定义为抽象的。您没有任何有用的实现。你只是抛出一个运行时异常。build()方法创建并返回一个Item类型的对象。BuilderFactory实现类似于清单 26-12 中的实现,除了它返回一个ItemBuilderMap作为Item类型的Builder

// ItemBuilderFactoryMap.java

package com.jdojo.fxml;

import javafx.fxml.JavaFXBuilderFactory;
import javafx.util.Builder;
import javafx.util.BuilderFactory;

public class ItemBuilderFactoryMap implements BuilderFactory {
        private final JavaFXBuilderFactory fxFactory =
               new JavaFXBuilderFactory();

        @Override
        public Builder<?> getBuilder(Class<?> type) {
                if (type == Item.class) {
                    return new ItemBuilderMap();
                }
                return fxFactory.getBuilder(type);
        }
}

Listing 26-14Another BuilderFactory to Get a Builder for the Item Type

// ItemBuilderMap.java
package com.jdojo.fxml;

import java.util.AbstractMap;
import java.util.Map;
import java.util.Set;
import javafx.util.Builder;

public class ItemBuilderMap extends AbstractMap<String, Object> implements Builder<Item> {
        private String name;
        private Long id;

        @Override

        public Object put(String key, Object value) {
                if ("name".equals(key)) {
                    this.name = (String)value;
                } else if ("id".equals(key)) {
                    this.id = Long.valueOf((String)value);
                } else {
                    throw new IllegalArgumentException(
                               "Unknown Item property: " + key);
                }

                return null;
        }

        @Override
        public Set<Map.Entry<String, Object>> entrySet() {
                throw new UnsupportedOperationException();
        }

        @Override
        public Item build() {
                return new Item(id, name);
        }

}

Listing 26-13A Builder for the Item Class That Implements the Map Interface

让我们为Item类测试两个Builder。清单 26-15 中的程序对Item类使用了两个Builder。它从items.fxml文件中加载Item列表,假设该文件位于resources/fxml目录中。

// BuilderTest.java

package com.jdojo.fxml;

import java.io.IOException;
import java.net.URL;
import com.jdojo.util.ResourceUtil;
import java.util.ArrayList;
import javafx.fxml.FXMLLoader;
import javafx.util.BuilderFactory;

public class BuilderTest {
        public static void main(String[] args) throws IOException {
            // Use the Builder with property getter and setter
            loadItems(new ItemBuilderFactory());

            // Use the Builder with Map
            loadItems(new ItemBuilderFactoryMap());
        }

        public static void
           loadItems(BuilderFactory builderFactory) throws IOException {
                URL fxmlUrl = ResourceUtil.getResourceURL("fxml/items.fxml");

            FXMLLoader loader = new FXMLLoader();
            loader.setLocation(fxmlUrl);
            loader.setBuilderFactory(builderFactory);
            ArrayList items = loader.<ArrayList>load();
            System.out.println("List:" + items);
        }
}

List:[id=100, name=Kishori, id=200, name=Ellen, id=300, name=Kannan]
List:[id=100, name=Kishori, id=200, name=Ellen, id=300, name=Kannan]

Listing 26-15Using Builders to Instantiate Item Objects in FXML

Tip

您提供给FXMLLoaderBuilderFactory将替换默认的BuilderFactory。您需要确保您的BuilderFactory为您的定制类型返回一个特定的Builder,为其余的类型返回默认的Builder。目前,FXMLLoader不允许使用一个以上的BuilderFactory

在 FXML 中创建可重用对象

有时,您需要创建不直接属于对象图的对象。但是,它们可能在 FXML 文档中的其他地方使用。例如,您可能想创建一个InsetsColor并在几个地方重用它们。使用ToggleGroup是一个典型的用例。一个ToggleGroup被创建一次,并与几个RadioButton对象一起使用。

您可以使用<fx:define>块在 FXML 中创建一个对象,而不使其成为对象组的一部分。您可以通过其他元素属性值中的fx:id来引用在<fx:define>块中创建的对象。属性值必须以美元符号($)为前缀:

<?import javafx.scene.layout.VBox?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.Button?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.ToggleGroup?>
<?import javafx.scene.control.RadioButton?>

<VBox fx:controller="com.jdojo.fxml.Test" xmlns:fx="http://javafx.com/fxml">
    <fx:define>
        <Insets fx:id="margin" top="5.0" right="5.0"
                   bottom="5.0" left="5.0"/>
        <ToggleGroup fx:id="genderGroup"/>
    </fx:define>
    <Label text="Gender" VBox.margin="$margin"/>
    <RadioButton text="Male" toggleGroup="$genderGroup"/>
    <RadioButton text="Female" toggleGroup="$genderGroup"/>
    <RadioButton text="Unknown" toggleGroup="$genderGroup" selected="true"/>
    <Button text="Close" VBox.margin="$margin"/>
</VBox>

前面的 FXML 在一个<fx:define>块中创建了两个对象,一个Insets和一个ToggleGroup。他们被赋予了"margin"fx:id"genderGroup"。通过"$margin""$genderGroup"在作为对象图一部分的控件中引用它们。

Tip

如果属性的值以$符号开始,它被认为是对对象的引用。如果要使用前导$符号作为值的一部分,请用反斜杠("\$hello")对其进行转义。

在属性中指定位置

以@符号开头的属性值表示位置。如果@符号后跟一个正斜杠(@/),则该位置被认为是相对于CLASSPATH的。如果@符号后面没有正斜杠,则该位置被认为是相对于正在处理的 FXML 文件的位置。

在下面的 FXML 中,将根据包含元素的 FXML 文件的位置来解析图像 URL:

<ImageView>
        <Image url="@resources/picture/ksharan.jpg"/>
</ImageView>

在下面的 FXML 中,图像 URL 将相对于CLASSPATH进行解析:

<ImageView>
        <Image url="@/resources/picture/ksharan.jpg"/>
</ImageView>

如果您想使用前导@符号作为属性值的一部分,请用反斜杠("\@not-a-location"对其进行转义。

使用资源包

在 FXML 中使用ResourceBundle比在 Java 代码中使用要容易得多。在属性值中指定来自ResourceBundle的键使用默认Locale的相应值。如果一个属性值以%符号开头,它将被视为资源包中的键名。运行时,属性值将来自FXMLLoader中指定的ResourceBundle。如果您想在属性值中使用前导%符号,请用反斜杠将其转义(例如,"\%hello")。

考虑清单 26-16 中的 FXML 内容。它使用"%greetingText"作为Labeltext属性的值。属性值以%符号开始。FXMLLoader将在ResourceBundle中查找" greetingText "的值,并将其用于text属性。这一切都是为你做的,甚至不用写一行代码!

<?import javafx.scene.control.Label?>
<Label text="%greetingText"/>

Listing 26-16The Contents of the greetings.fxml File

清单 26-17 和 26-18 有ResourceBundle文件的内容:一个默认Locale名为greetings.properties,一个印度Locale名为greetings_hi.properties。文件名中的后缀_hi表示印度语 Hindi。

# The Indian greeting

greetingText = Namaste

Listing 26-18The Contents of the greetings_hi.properties File

# The default greeting
greetingText = Hello

Listing 26-17The Contents of the greetings.properties File

清单 26-19 中的程序使用了带有FXMLLoaderResourceBundleResourceBundle是从CLASSPATHresources/resourcebundles目录中加载的。FXML 文件从类别ResourceUtil中引用的文件夹resources/fxml/greetings.fxml中加载。该程序从 FXML 文件中加载了两次Label:一次是默认的地区 US,另一次是将默认的Locale改为 India Hindi。两个Label都显示在VBox中,如图 26-2 所示。

img/336502_2_En_26_Fig2_HTML.png

图 26-2

使用资源包填充文本属性的标签

// ResourceBundleTest.java
package com.jdojo.fxml;

import java.io.IOException;
import java.net.URL;
import com.jdojo.util.ResourceUtil;
import java.util.Locale;
import java.util.ResourceBundle;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;

import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

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

        @Override
        public void start(Stage stage) throws IOException {
                URL fxmlUrl =
                    ResourceUtil.getResourceURL("fxml/greetings.fxml");

            // Create a ResourceBundle to use in FXMLLoader
            String resourcePath = "resources/resourcebundles/greetings";
            ResourceBundle resourceBundle =
                    ResourceBundle.getBundle(resourcePath);

            // Load the Label for default Locale
            Label defaultGreetingLbl =
                    FXMLLoader.<Label>load(fxmlUrl, resourceBundle);

            // Change the default Locale and load the Label again
            Locale.setDefault(new Locale("hi", "in"));

            // We need to recreate the ResourceBundler to pick up the
                // new default Locale

            resourceBundle = ResourceBundle.getBundle(resourcePath);

            Label indianGreetingLbl =
                    FXMLLoader.<Label>load(fxmlUrl, resourceBundle);

            // Add both Labels to a Vbox
            VBox root =
                    new VBox(5, defaultGreetingLbl, indianGreetingLbl);
            Scene scene = new Scene(root);
            stage.setScene(scene);
            stage.setTitle("Using a ResourceBundle in FXML");
            stage.show();
        }
}

Listing 26-19Using a Resource Bundle with the FXMLLoader

包括 FXML 文件

使用<fx:include>元素,一个 FXML 文档可以包含另一个 FXML 文档。嵌套文档生成的对象图包含在嵌套文档在包含文档中出现的位置。<fx:include>元素接受一个source属性,该属性的值是嵌套文档的路径:

<fx:include source="nested_document_path"/>

如果嵌套文档路径以正斜杠开头,则该路径相对于CLASSPATH被解析。否则,它相对于包含文档的路径进行解析。

<fx:include>元素可以有fx:id属性和所有可用于被包含对象的属性。包含文档中指定的属性会覆盖包含文档中的相应属性。例如,如果您包含一个 FXML 文档,它会创建一个Button,那么您可以在包含文档和被包含文档中指定text属性。加载包含文档时,将使用包含文档的text属性。

FXML 文档可以选择使用根元素的fx:controller属性指定一个控制器。规则是每个 FXML 文档最多可以有一个控制器。嵌套文档时,每个文档都可以有自己的控制器。FXMLLoader允许您将嵌套的控制器引用注入到主文档的控制器中。您需要遵循命名约定来注入嵌套控制器。主文档的控制器应该有一个可访问的实例变量,其名称为

Instance variable name = "fx:id of the fx:include element" + "Controller"

如果<fx:include>元素的fx:id是“xxx”,那么实例变量名应该是xxxController

考虑清单 26-20 和 26-21 中显示的两个 FXML 文档。closebutton.fxml文件创建一个Button,将其文本属性设置为Close,并附加一个动作事件处理程序。事件处理程序使用 JavaScript 语言。它关闭包含它的窗口。

假设两个文件在同一个目录中,maindoc.fxml包括closebutton.fxml。它为<fx:include>元素指定了textfx:id属性。注意,包含的 FXML 指定“Close”作为测试属性,maindoc.fxml覆盖了它并将其设置为“Close”。

<!-- maindoc.fxml -->
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.control.Label?>

<VBox fx:controller="com.jdojo.fxml.MainDocController" xmlns:fx="http://javafx.com/fxml">
        <Label text="Testing fx:include"/>

        <!-- Override the text property of the included Button -->
        <fx:include source="closebutton.fxml" fx:id="includedCloseBtn"
               text="Hide"/>
</VBox>

Listing 26-21An FXML Document Using an <fx:include> Element

<!-- closebutton.fxml -->
<?language javascript?>
<?import javafx.scene.control.Button?>

<Button fx:controller="com.jdojo.fxml.CloseBtnController"
        text="Close"
        fx:id="closeBtn"
        onAction="#closeWindow"
        xmlns:fx="http://javafx.com/fxml">
</Button>

Listing 26-20An FXML Document That Creates a Close Button to Close the Containing Window

两个 FXML 文档都指定了清单 26-22 和 26-23 中列出的控制器。注意,主文档的控制器声明了两个实例变量:一个将引用被包含的Button,另一个将引用被包含文档的控制器。请注意,Button的引用也将包含在嵌套文档的控制器中。

// MainDocController.java

package com.jdojo.fxml;

import javafx.fxml.FXML;
import javafx.scene.control.Button;

public class MainDocController {
        @FXML
        private Button includedCloseBtn;

        @FXML
        private CloseBtnController includedCloseBtnController;

        @FXML
        public void initialize() {
                System.out.println("MainDocController.initialize()");
                // You can use the nested controller here
        }

}

Listing 26-23The Controller Class for the Main Document

// CloseBtnController.java

package com.jdojo.fxml;

import javafx.fxml.FXML;
import javafx.scene.control.Button;

public class CloseBtnController {
        @FXML
        private Button closeBtn;

        @FXML
        public void initialize() {
                System.out.println("CloseBtnController.initialize()");
        }

}

Listing 26-22The ControllerClass for the FXML Defining the Close Button

清单 26-24 中的程序加载maindoc.fxml并将加载的VBox添加到场景中。它显示一个窗口,带有closebutton.fxml文件中的隐藏按钮。点击Hide按钮将关闭窗口。

// FxIncludeTest.java
package com.jdojo.fxml;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
 import com.jdojo.util.ResourceUtil;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

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

        @Override

        public void
           start(Stage stage) throws MalformedURLException, IOException {
                     URL fxmlUrl = ResourceUtil.getResourceURL(
                         "fxml/maindoc.fxml");

                FXMLLoader loader = new FXMLLoader();
                loader.setLocation(fxmlUrl);
                VBox root = loader.<VBox>load();
                Scene scene = new Scene(root);
                stage.setScene(scene);
                stage.setTitle("Nesting Documents in FXML");
                stage.show();
        }
}

Listing 26-24Loading and Using a Nested FXML Document

使用常量

类、接口和枚举可以定义常量,这些常量是静态的最终变量。您可以使用fx:constant属性来引用这些常量。属性值是常量的名称。元素的名称是包含该常量的类型的名称。例如,对于Long.MAX_VALUE,您可以使用以下元素:

<Long fx:constant="MAX_VALUE"/>

注意,所有枚举常量都属于这个类别,可以使用fx:constant属性访问它们。以下元素访问Pos.CENTER枚举常量:

<Pos fx:constant="CENTER"/>

下面的 FXML 内容访问来自IntegerLong类以及Pos枚举的常量。它将VBoxalignment属性设置为Pos.CENTER:

<?import javafx.scene.layout.VBox?>
<?import javafx.scene.control.TextField?>
<?import java.lang.Integer?>
<?import java.lang.Long?>
<?import javafx.scene.text.FontWeight?>
<?import javafx.geometry.Pos?>

<VBox xmlns:fx="http://javafx.com/fxml">
        <fx:define>
                <Integer fx:constant="MAX_VALUE" fx:id="minInt"/>
        </fx:define>
        <alignment><Pos fx:constant="CENTER"/></alignment>
        <TextField text="$minInt"/>
        <TextField>
                <text><Long fx:constant="MIN_VALUE"/></text>
        </TextField>

</VBox>

引用另一个元素

您可以使用<fx:reference>元素引用文档中的另一个元素。fx:id属性指定了引用元素的fx:id:

<fx:reference source="fx:id of the source element"/>

下面的 FXML 内容使用一个<fx:reference>元素来引用一个Image:

<?import javafx.scene.layout.VBox?>
<?import javafx.scene.image.Image?>
<?import javafx.scene.image.ImageView?>
<VBox xmlns:fx="http://javafx.com/fxml">
        <fx:define>
            <Image url="resources/picture/ksharan.jpg" fx:id="myImg"/>
        </fx:define>
        <ImageView>
             <image>
                <fx:reference source="myImg"/>
            </image>
        </ImageView>

</VBox>

请注意,您也可以使用变量解引用方法重写前面的 FXML 内容,如下所示:

<VBox xmlns:fx="http://javafx.com/fxml">
        <fx:define>
            <Image url="resources/picture/ksharan.jpg" fx:id="myImg"/>
        </fx:define>
        <ImageView image="$myImg"/>
</VBox>

复制元素

有时,您想要复制一个元素。在这个上下文中,复制是通过复制源对象的属性来创建新对象。您可以使用<fx:copy>元素来实现:

<fx:copy source="fx:id of the source object" />

若要复制对象,该类必须提供复制构造器。复制构造器接受同一个类的对象。假设您有一个包含复制构造器的Item类:

public class Item {
        private Long id;
        private String name;

        public Item() {
        }

        // The copy constructor
        public Item(Item source) {
                this.id = source.id + 100;
                this.name = source.name + " (Copied)";
        }
        ...
}

下面的 FXML 文档在<fx:define>块中创建了一个Item对象。它多次复制Item对象,并将它们添加到ComboBox的项目列表中。注意,使用一个<fx:reference>元素将源Item本身添加到条目列表中:

<?import javafx.scene.layout.VBox?>
<?import javafx.scene.control.ComboBox?>
<?import javafx.collections.FXCollections?>
<?import com.jdojo.fxml.Item?>

<VBox xmlns:fx="http://javafx.com/fxml">
        <fx:define>
                <Item name="Kishori" id="100" fx:id="myItem"/>
        </fx:define>
        <ComboBox value="$myItem">
            <items>
                <FXCollections fx:factory="observableArrayList">
                    <fx:reference source="myItem"/>
                    <fx:copy source="myItem" />
                    <fx:copy source="myItem" />
                    <fx:copy source="myItem" />
                    <fx:copy source="myItem" />
                </FXCollections>
            </items>
        </ComboBox>

</VBox>

FXML 中的绑定属性

FXML 支持简单的属性绑定。您需要使用属性的属性将其绑定到另一个元素或文档变量的属性。属性值以$符号开始,后面跟着一对花括号。以下 FXML 内容创建了一个带有两个TextFieldVBoxmirrorText字段的text属性被绑定到mainText字段的文本属性:

<?import javafx.scene.layout.VBox?>
<?import javafx.scene.control.TextField?>

<VBox xmlns:fx="http://javafx.com/fxml">
        <TextField fx:id="mainText" text="Hello"/>
        <TextField fx:id="mirrorText" text="${mainText.text}" disable="true"/>
</VBox>

创建自定义控件

您可以使用 FXML 创建自定义控件。让我们创建一个带有两个Label、一个TextField、一个PasswordField和两个Button的登录表单。注意,根元素是一个<fx:root>。元素创建了一个对之前创建的元素的引用。使用setRoot()方法在FXMLLoader中设置<fx:root>元素的值。属性指定了将要注入的根的类型。

<!-- login.fxml -->
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.control.PasswordField?>

<fx:root type="javafx.scene.layout.GridPane"
              xmlns:fx="http://javafx.com/fxml">
        <Label text="User Id:" GridPane.rowIndex="0"
               GridPane.columnIndex="0"/>
        <TextField fx:id="userId" GridPane.rowIndex="0"
               GridPane.columnIndex="1"/>
        <Label text="Password:" GridPane.rowIndex="1"
               GridPane.columnIndex="0"/>
        <PasswordField fx:id="pwd" GridPane.rowIndex="1"
               GridPane.columnIndex="1"/>
        <Button fx:id="okBtn" text="OK" onAction="#okClicked"
               GridPane.rowIndex="0" GridPane.columnIndex="2"/>
        <Button fx:id="cancelBtn" text="Cancel" onAction="#cancelClicked"
               GridPane.rowIndex="1" GridPane.columnIndex="2"/>
</fx:root>

Listing 26-25The FXML Contents for a Custom Login Form

清单 26-26 中的类表示自定义控件的 JavaFX 部分。您将创建一个LogInControl类的对象,并将其用作任何其他标准控件。该类也用作login.fxml的控制器。在构造器中,类加载 FXML 内容。在加载内容之前,它将自己设置为FXMLLoader中的根和控制器。实例变量允许在类中注入userIdpwd控件。当点击Button时,您只需在控制台上打印一条消息。如果您想在实际应用程序中使用这个控件,还需要做更多的工作。当点击 OKCancel 按钮时,您需要为用户提供一种挂钩事件通知的方式。

// LoginControl.java
package com.jdojo.fxml;

import java.io.IOException;
import java.net.URL;
import com.jdojo.util.ResourceUtil;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.PasswordField;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;

public class LoginControl extends GridPane {
        @FXML
        private TextField userId;

        @FXML
        private PasswordField pwd;

        public LoginControl() {
                // Load the FXML
                     URL fxmlUrl =
                         ResourceUtil.getResourceURL("fxml/login.fxml");
                FXMLLoader loader = new FXMLLoader();
                loader.setLocation(fxmlUrl);
                loader.setRoot(this);
                loader.setController(this);
                try {
                        loader.load();
                }
                catch (IOException exception) {
                   throw new RuntimeException(exception);
                }
        }

        @FXML
        private void initialize() {
                // Do some work
        }

        @FXML
        private void okClicked() {
                System.out.println("Ok clicked");
        }

        @FXML
        private void cancelClicked() {
            System.out.println("Cancel clicked");
        }

        public String getUserId() {
                return userId.getText();
        }

        public String getPassword() {
                return pwd.getText();
        }
}

Listing 26-26A Class Implementing the Custom Control

清单 26-27 中的程序展示了如何使用自定义控件。使用自定义控件就像创建 Java 对象一样简单。自定义控件扩展了GridPane;所以可以作为一个GridPane。在 FXML 中使用控件与使用其他控件没有什么不同。该控件提供了一个无参数的构造器,这将允许通过使用一个类名为<LoginControl>的元素在 FXML 中创建它。

// LoginTest.java
package com.jdojo.fxml;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.GridPane;
import javafx.stage.Stage;

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

        @Override
        public void start(Stage stage) {
                // Create the Login custom control
                GridPane root = new LoginControl();
                Scene scene = new Scene(root);
                stage.setScene(scene);
                stage.setTitle("Using FXMl Custom Control");
                stage.show();
        }

}

Listing 26-27Using the Custom Control

摘要

FXML 是一种基于 XML 的语言,用于为 JavaFX 应用程序构建用户界面。您可以使用 FXML 构建整个场景或场景的一部分。FXML 允许应用程序开发人员将构建 UI 的逻辑与业务逻辑分开。如果应用程序的 UI 部分发生了变化,您不需要重新编译 JavaFX 代码:使用文本编辑器更改 FXML 并重新运行应用程序。您仍然使用 JavaFX 通过 Java 语言编写业务逻辑。FXML 文档是 XML 文档。

在 JavaFX 应用程序中使用 FXML 构建场景图是很常见的。然而,FXML 的使用并不仅限于构建场景图。它可以构建 Java 对象的分层对象图。事实上,它只能用来创建一个对象,比如一个Person类的对象。

FXML 文档只是一个文本文件。通常,文件名有一个.fxml扩展名(例如hello.fxml)。您可以使用任何文本编辑器来编辑 FXML 文档。胶子公司提供了一个名为场景构建器的开源可视化编辑器,用于编辑 FXML 文档。场景生成器也可以集成到一些 ide 中。

FXML 允许您使用无参数构造器、valueOf()方法、工厂方法和构建器来创建对象。

有时,您需要创建不直接属于对象图的对象。但是,它们可能在 FXML 文档中的其他地方使用。您可以使用<fx:define>块在 FXML 中创建一个对象,而不使其成为对象组的一部分。您可以通过其他元素属性值中的fx:id来引用在<fx:define>块中创建的对象。属性值必须以美元符号($)为前缀。

FXML 允许您通过指定资源的位置来引用资源。以@符号开头的属性值表示位置。如果@符号后面跟一个正斜杠(@/),则该位置被认为是相对于CLASSPATH的。如果@符号后面没有正斜杠,则该位置被认为是相对于正在处理的 FXML 文件的位置。

在 FXML 中使用ResourceBundle比在 Java 代码中使用要容易得多。在属性值中指定来自ResourceBundle的键使用默认Locale的相应值。如果一个属性值以%符号开头,它将被视为资源包中的键名。运行时,属性值将来自FXMLLoader中指定的ResourceBundle。如果您想在属性值中使用前导%符号,请用反斜杠将其转义(例如,"\%hello")。

使用<fx:include>元素,一个 FXML 文档可以包含另一个 FXML 文档。嵌套文档生成的对象图包含在嵌套文档在包含文档中出现的位置。

类、接口和枚举可以定义常量,这些常量是静态的最终变量。您可以使用fx:constant属性来引用这些常量。属性值是常量的名称。元素的名称是包含该常量的类型的名称。例如,对于Long.MAX_VALUE,可以使用元素<Long fx:constant="MAX_VALUE"/>

您可以使用<fx:reference>元素引用文档中的另一个元素。属性fx:id指定了被引用元素的fx:id。您可以使用<fx:copy>元素复制一个元素。它将通过复制源对象的属性来创建一个新对象。

FXML 支持简单的属性绑定。您需要使用属性的属性将其绑定到另一个元素或文档变量的属性。属性值以$符号开始,后面跟着一对花括号。您可以使用 FXML 创建自定义控件。

下一章将讨论 JavaFX 中的打印 API,它允许您在 JavaFX 应用程序中配置打印机和打印节点。*

二十七、了解打印 API

在本章中,您将学习:

  • 什么是打印 API

  • 如何获取可用打印机列表

  • 如何获取默认打印机

  • 如何打印节点

  • 如何向用户显示页面设置和打印对话框

  • 如何自定义打印机作业的设置

  • 如何设置打印页面布局

  • 如何打印显示在WebView中的网页

本章的例子在com.jdojo.print包中。为了让它们工作,您必须在module-info.java文件中添加相应的一行:

...
opens com.jdojo.print to javafx.graphics, javafx.base;
...

什么是打印 API?

JavaFX 通过javafx.print包中的 Print API 支持打印节点。API 由以下类和一些枚举(未列出)组成:

  • Printer

  • PrinterAttributes

  • PrintResolution

  • PrinterJob

  • JobSettings

  • Paper

  • PaperSource

  • PageLayout

  • PageRange

上面列出的类的实例代表打印过程的不同部分。例如,Printer代表可以用于打印作业的打印机;一个PrinterJob代表一个可以发送到Printer进行打印的打印作业;而Paper代表打印机上可用的纸张尺寸。

Print API 提供了对打印节点的支持,这些节点可能会也可能不会附加到场景图。打印 web 页面的内容,而不是包含 web 页面的WebView节点,这是一个常见的需求。javafx.scene.web.WebEngine类包含一个打印网页内容的print(PrinterJob job)方法,而不是WebView节点。

如果在打印过程中修改了节点,打印的节点可能看起来不正确。注意,节点的打印可以跨越多个脉冲事件,导致正在打印的内容的同时改变。为确保正确打印,请确保正在打印的节点在打印过程中未被修改。

节点可以打印在任何线程上,包括 JavaFX 应用程序线程。建议在后台线程上提交大型、耗时的打印作业,以保持 UI 的响应性。

Print API 中的类是最终的,因为它们表示现有的打印设备属性。它们中的大多数不提供任何公共构造器,因为你不能组成一个打印设备。相反,您可以使用各种类中的工厂方法来获取它们的引用。

Note

Print API 只为打印节点和网页提供基本的打印支持。您将无法使用它在 JavaFX 应用程序中打印报告。

列出可用的打印机

静态方法返回机器上已安装打印机的可见列表。请注意,方法返回的打印机列表可能会随着新打印机的安装或旧打印机的移除而改变。使用PrintergetName()方法获取打印机的名称。以下代码片段列出了运行该代码的机器上安装的所有打印机。您可能会得到不同的输出:

import javafx.collections.ObservableSet;
import javafx.print.Printer;
...
ObservableSet<Printer> allPrinters = Printer.getAllPrinters();
for(Printer p : allPrinters) {
        System.out.println(p.getName());
}
ImageRight Printer
Microsoft XPS Document Writer
PDF995
Sybase DataWindow PS
\\pro-print1\IS-CANON1
\\pro-print1\IS-HP4000
\\pro-print1\IS-HP4015
\\pro-print1\IS-HP4050
\\pro-print1\IS-HP4650
\\pro-print1\IS-HP4650(Color)

获取默认打印机

Printer.getDefaultPrinter()方法返回默认的Printer。如果没有安装打印机,该方法可能返回null。机器上的默认打印机可以更改。因此,该方法可能在调用之间返回不同的打印机,并且返回的打印机可能在一段时间后无效。以下代码片段显示了如何获取默认打印机:

Printer defaultprinter = Printer.getDefaultPrinter();
if (defaultprinter != null) {
        String name = defaultprinter.getName();
        System.out.println("Default printer name: " + name);
} else {
        System.out.println("No printers installed.");
}

打印节点

打印节点很简单:创建一个PrinterJob并调用它的printPage()方法,传递要打印的节点。使用具有所有默认设置的默认打印机打印节点只需要三行代码:

PrinterJob printerJob = PrinterJob.createPrinterJob();
printerJob.printPage(node); // node is the node to be printed
printerJob.endJob();

在现实世界的应用程序中,您需要处理错误。您可以重写代码来处理错误,如下所示:

// Create a printer job for the default printer
PrinterJob printerJob = PrinterJob.createPrinterJob();
if (printerJob!= null) {
        // Print the node
        boolean printed = printerJob.printPage(node);
        if (printed) {
                // End the printer job
                printerJob.endJob();
        } else {
                System.out.println("Printing failed.");
        }
} else {
        System.out.println("Could not create a printer job.");
}

您可以使用PrinterJob类的createPrinterJob()静态方法来创建打印机作业:

  • public static PrinterJob createPrinterJob()

  • public static PrinterJob createPrinterJob(Printer printer)

不带-args 的方法为默认打印机创建一个打印机作业。您可以使用其他版本的方法为指定的打印机创建打印机作业。

您可以通过调用setPrinter()方法来更改PrinterJob的打印机。如果新打印机不支持当前的打印机作业设置,新打印机会自动重置这些设置:

// Set a new printer for the printer job
printerJob.setPrinter(myNewPrinter);

为作业设置null打印机将使用默认打印机。

使用以下printPage()方法之一打印节点:

  • boolean printPage(Node node)

  • boolean printPage(PageLayout pageLayout, Node node)

该方法的第一个版本只接受要打印的节点作为参数。它使用作业的默认页面布局进行打印。

第二个版本允许您指定打印节点的页面布局。指定的PageLayout将覆盖作业的PageLayout,并且仅用于打印指定的节点。对于后续打印,将使用作业的默认PageLayout。您可以使用Printer类创建一个PageLayout。我将在后面讨论这类例子。

如果打印成功,printPage()方法返回 true。否则,它返回 false。完成打印后,调用endJob()方法。如果作业可以成功地假脱机到打印机队列,则该方法返回 true。否则,它将返回 false,这可能表示作业无法假脱机或已经完成。作业成功完成后,就不能再重复使用了。

Tip

您可以任意多次调用PrinterJob上的printPage()方法。调用endJob()方法告诉作业不再执行打印。该方法将作业状态转换到DONE,并且该作业不应再被重用。

您可以使用PrinterJobcancelJob()方法取消打印作业。打印可能不会立即取消,例如,当页面正在打印时。取消会尽快发生。如果出现以下情况,则该方法没有任何效果

  • 已经请求取消该作业。

  • 这项工作已经完成。

  • 作业有错误。

PrinterJob具有只读状态,由PrinterJob.JobStatus枚举的常量之一定义:

  • NOT_STARTED

  • PRINTING

  • CANCELED

  • DONE

  • ERROR

NOT_STARTED状态表示一个新任务。在这种状态下,可以配置作业,并且可以开始打印。PRINTING状态表示作业已请求打印至少一页,并且尚未终止打印。在这种状态下,无法配置作业。

其他三种状态,CANCELEDDONEERROR,表示作业的终止状态。一旦作业处于其中一种状态,就不应该重复使用。当状态变为CANCELEDERROR时,不需要调用endJob()方法。当打印成功并且调用了endJob()方法时,进入DONE状态。PrinterJob类包含一个只读的jobStatus属性,指示打印作业的当前状态。

清单 27-1 中的程序展示了如何打印节点。它显示一个TextArea,您可以在其中输入文本。提供了两个Button:一个打印TextArea节点,另一个打印整个场景。当开始打印时,打印作业状态显示在Label中。print()方法中的代码与前面讨论的相同。该方法包括在Label中显示作业状态的逻辑。程序显示如图 27-1 所示的窗口。运行程序;在TextArea中输入文本;然后点击两个按钮中的一个进行打印。

img/336502_2_En_27_Fig1_HTML.png

图 27-1

允许用户在文本区域和场景中打印文本的窗口

// PrintingNodes.java
package com.jdojo.print;

import javafx.application.Application;
import javafx.print.PrinterJob;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextArea;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class PrintingNodes  extends Application {
        private Label jobStatus = new Label();

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

        @Override
        public void start(Stage stage) {
                VBox root = new VBox(5);

                Label textLbl = new Label("Text:");
                TextArea text = new TextArea();
                text.setPrefRowCount(10);
                text.setPrefColumnCount(20);
                text.setWrapText(true);

                // Button to print the TextArea node
                Button printTextBtn = new Button("Print Text");
                printTextBtn.setOnAction(e -> print(text));

                // Button to print the entire scene
                Button printSceneBtn = new Button("Print Scene");
                printSceneBtn.setOnAction(e -> print(root));

                HBox jobStatusBox = new HBox(5,
                         new Label("Print Job Status:"), jobStatus);
                HBox buttonBox = new HBox(5,
                         printTextBtn, printSceneBtn);

                root.getChildren().addAll(
                         textLbl, text, jobStatusBox, buttonBox);
                Scene scene = new Scene(root);
                stage.setScene(scene);
                stage.setTitle("Printing Nodes");
                stage.show();
        }

        private void print(Node node) {
                jobStatus.textProperty().unbind();
                jobStatus.setText("Creating a printer job...");

                // Create a printer job for the default printer
                PrinterJob job = PrinterJob.createPrinterJob();
                if (job != null) {
                    // Show the printer job status
                    jobStatus.textProperty().bind(
                               job.jobStatusProperty().asString());

                    // Print the node
                    boolean printed = job.printPage(node);
                    if (printed) {
                        // End the printer job
                        job.endJob();
                    } else {

                        jobStatus.textProperty().unbind();
                        jobStatus.setText("Printing failed.");
                    }
                } else {
                    jobStatus.setText(
                               "Could not create a printer job.");
                }
        }
}

Listing 27-1Printing Nodes

显示页面设置和打印对话框

打印 API 允许用户与打印过程进行交互。用户可以在打印开始前交互更改打印机设置。API 允许您显示页面设置和打印设置对话框,用于设置作业的页面属性和打印机设置。

您可以让用户通过显示页面设置对话框来配置页面布局。使用PrinterJobshowPageSetupDialog(Window owner)方法显示页面设置对话框。用户可以设置页面大小、来源、方向和边距。该对话框可以允许用户访问其他打印属性,例如打印机列表。一旦用户确认了对话框上的设置,PrinterJob就会有新的设置。如果用户确认对话框上的设置,该方法返回true。如果用户取消对话框,它将返回false。如果对话框无法显示,例如当作业不在NOT_STARTED状态时,它也返回 false。

该方法的所有者参数是将成为对话框所有者的窗口。可以是null。如果指定,当对话框显示时,窗口的输入将被阻止:

PrinterJob job = PrinterJob.createPrinterJob();

// Show the page setup dialog
boolean proceed = job.showPageSetupDialog(null);
if (proceed) {
        // Start printing here or you can print later
}

您可以使用showPrintDialog(Window owner)方法显示一个打印对话框,用户可以在其中修改打印机和PrinterJob的设置。该方法的返回值和参数与showPageSetupDialog()方法的含义相似:

PrinterJob job = PrinterJob.createPrinterJob();

// Show the print setup dialog
boolean proceed = job.showPrintDialog(null);
if (proceed) {
        // Start printing here or you can print later
}

清单 27-2 中的程序显示了与清单 27-1 中的程序类似的窗口。这次,点击打印按钮显示页面设置和打印设置对话框(如图 27-2 )。一旦用户确认了对话框上的设置,就会打印出TextArea中的文本。注意,即使您在显示对话框之前为默认打印机创建了一个PrinterJob,您也可以使用对话框更改打印机,文本将使用更改后的打印机打印。

img/336502_2_En_27_Fig2_HTML.png

图 27-2

允许用户使用打印对话框来自定义打印机设置的窗口

// PrintDialogs.java
// ...find in the book's download area

Listing 27-2Showing the Page Setup and Print Dialogs to the User

自定义打印机作业设置

打印 API 包含两个与打印机和打印机作业设置相关的类:

  • PrinterAttributes

  • JobSettings

打印机具有指示打印机打印能力的属性。打印机属性的示例包括默认纸张尺寸、支持的纸张尺寸、最大份数和默认排序规则。一个PrinterAttributes对象封装了打印机的属性。打印 API 不允许您更改打印机属性,因为您不能更改打印机的功能。你只能使用它的能力。您不能直接创建一个PrinterAttributes对象。你需要使用getPrinterAttributes()方法从一个Printer对象中获取它。以下代码片段打印机器中默认打印机的一些属性。您可能会得到不同的输出:

import javafx.print.Collation;
import javafx.print.PageOrientation;
import javafx.print.PrintSides;
import javafx.print.Printer;
import javafx.print.PrinterAttributes;
...
Printer printer = Printer.getDefaultPrinter();
PrinterAttributes attribs = printer.getPrinterAttributes();

// Read some printer attributes
int maxCopies = attribs.getMaxCopies();
PrintSides printSides = attribs.getDefaultPrintSides();
Set<PageOrientation> orientations = attribs.getSupportedPageOrientations();
Set<Collation> collations = attribs.getSupportedCollations();

// Print the printer attributes
System.out.println("Max. Copies: " + maxCopies);
System.out.println("Print Sides: " + printSides);
System.out.println("Supported Orientation: " + orientations);
System.out.println("Supported Collations: " + collations);
Max. Copies: 999
Print Sides: ONE_SIDED
Supported Orientation: [PORTRAIT, LANDSCAPE, REVERSE_LANDSCAPE]
Supported Collations: [UNCOLLATED, COLLATED]

Tip

一个PrinterAttributes是一个不可变的对象。它包含打印机的默认属性和支持属性。你从一个Printer对象中获得PrinterAttributes

JobSettings包含用于特定打印机打印作业的打印机属性。您可以使用PrinterJob对象的getJobSettings()方法获得打印作业的JobSettings。一个JobSettings是一个可变对象。它包含可以为打印作业设置的每个打印机属性的属性。默认情况下,其属性被初始化为打印机的默认属性。您可以更改将用于当前打印作业的属性。如果您更改打印机不支持的JobSettings的属性,该属性将恢复为打印机的默认值。下面的代码片段将printSides属性设置为DUPLEX。在这种情况下,打印机仅支持ONE_SIDED打印。因此,printSides属性被设置为ONE_SIDED,这是默认设置,并且只有打印机支持printSides值。您可能会得到不同的输出:

// Create a printer job for the default printer
PrinterJob job = PrinterJob.createPrinterJob();

// Get the JobSettings for the print job
JobSettings jobSettings = job.getJobSettings();
System.out.println(jobSettings.getPrintSides());

// Set the printSides to DUPLEX
jobSettings.setPrintSides(PrintSides.DUPLEX);
System.out.println(jobSettings.getPrintSides());
ONE_SIDED
ONE_SIDED

对于打印作业,您可以使用JobSettingspageRanges属性指定页面范围。pageRanges属性是一个PageRange的数组。一个PageRange具有定义范围的startPageendPage属性。以下代码片段将作业的页面范围设置为 1–5 和 20–25:

PrinterJob job = PrinterJob.createPrinterJob();
JobSettings jobSettings = job.getJobSettings();
jobSettings.setPageRanges(new PageRange(1, 5), new PageRange(20, 25));

大多数打印机属性由枚举常量表示。例如,排序规则属性由Collation.COLLATEDCollation.UNCOLLATED常量表示。一些属性,比如要打印的份数,被指定为一个int。请参考JobSettings类中您可以为打印作业设置的属性列表。

设置页面布局

PageLayout类的一个实例表示打印作业的页面设置。默认情况下,它被设置为打印机默认值。您已经看到了使用页面设置对话框设置页面布局。一个PageLayout封装了三样东西:

  • 纸张尺寸

  • 页面方向

  • 页边距

PageLayout用于配置页面的可打印区域,该区域必须位于硬件的可打印区域内。如果页面呈现在硬件的可打印区域之外,内容将被剪切。

您不能直接创建一个PageLayout对象。您需要使用PrintercreatePageLayout()方法之一来获得PageLayout:

  • PageLayout createPageLayout(Paper paper, PageOrientation orient, double lMargin, double rMargin, double tMargin, double bMargin)

  • PageLayout createPageLayout(Paper paper, PageOrientation orient, Printer.MarginType mType)

边距可以指定为数字或以下Printer.MarginType枚举常量之一:

  • DEFAULT

  • EQUAL

  • EQUAL_OPPOSITES

  • HARDWARE_MINIMUM

DEFAULT边距类型要求所有边的缺省值为 0.75 英寸。

EQUAL边距类型在所有四边使用四个硬件边距中最大的一个,因此所有四边的边距都相等。

EQUAL_OPPOSITES边距类型为左右两边使用较大的左右硬件边距,为上下两边使用较大的上下硬件边距。

HARDWARE_MINIMUM要求在所有边上设置硬件允许的最小余量。

下面的代码片段为 A4 大小的纸张创建了一个PageLayoutLANDSCAPE页面方向,并在所有边上创建了相等的边距。PageLayout被设置为打印作业:

import javafx.print.JobSettings;
import javafx.print.PageLayout;
import javafx.print.PageOrientation;
import javafx.print.Paper;
import javafx.print.Printer;
import javafx.print.PrinterJob;
...
PrinterJob job = PrinterJob.createPrinterJob();
Printer printer = job.getPrinter();
PageLayout pageLayout = printer.createPageLayout(Paper.A4,
                                                 PageOrientation.LANDSCAPE,
                                                 Printer.MarginType.EQUAL);
JobSettings jobSettings = job.getJobSettings();
jobSettings.setPageLayout(pageLayout);

有时,您想知道页面上可打印区域的大小。你可以使用PageLayoutgetPrintableWidth()getPrintableHeight()方法得到它。如果您想在打印前调整节点大小,使其适合可打印区域,这很有用。下面的代码片段打印一个适合可打印区域的Ellipse:

PrinterJob job = PrinterJob.createPrinterJob();
JobSettings jobSettings = job.getJobSettings();
PageLayout pageLayout = jobSettings.getPageLayout();
double pgW = pageLayout.getPrintableWidth();
double pgH = pageLayout.getPrintableHeight();

// Make the Ellipse fit the printable are of the page
Ellipse node = new Ellipse(pgW/2, pgH/2, pgW /2, pgH/2);
node.setFill(null);
node.setStroke(Color.BLACK);
node.setStrokeWidth(1);

boolean printed = job.printPage(node);
if (printed) {
        // End the printer job
        job.endJob();
}

打印网页

打印网页内容有一种特殊的方法。使用WebEngine类的print(PrinterJob job)方法打印引擎加载的网页。该方法不修改指定的job。在print()方法调用后,作业可用于更多的打印:

WebView webView = new WebView();
WebEngine webEngine = webView.getEngine();
...
PrinterJob job = PrinterJob.createPrinterJob();
webEngine.print(job);

清单 27-3 中的程序显示了如何打印网页。这个项目中没有你没有涉及过的新内容。该程序显示一个带有 URL 字段的窗口,一个 Go 按钮,一个 Print 按钮和一个WebView。当网页成功加载时,Print按钮被激活。您可以输入一个网页 URL,然后单击 Go 按钮导航至该页面。点击打印按钮打印网页。

// PrintingWebPage.java
// ...find in the book's download area.

Listing 27-3Printing a Web Page

摘要

JavaFX 通过javafx.print包中的 Print API 支持打印节点。API 由几个类和一些枚举组成。Print API 提供了对打印节点的支持,这些节点可能会也可能不会附加到场景图。打印 web 页面的内容,而不是包含 web 页面的WebView节点,这是一个常见的需求。javafx.scene.web.WebEngine类包含一个打印网页内容的print(PrinterJob job)方法,而不是WebView节点。

如果在打印过程中修改了节点,打印的节点可能看起来不正确。注意,节点的打印可以跨越多个脉冲事件,导致正在打印的内容的同时改变。为了确保正确打印,请确保正在打印的节点在打印过程中没有被修改。

节点可以打印在任何线程上,包括 JavaFX 应用程序线程。建议在后台线程上提交大型、耗时的打印作业,以保持 UI 的响应性。

Print API 中的类是最终的,因为它们表示现有的打印设备属性。它们中的大多数不提供任何公共构造器,因为你不能组成一个打印设备。相反,您可以使用各种类中的工厂方法来获取它们的引用。

Printer类的一个实例代表一台打印机。静态方法返回机器上已安装打印机的可见列表。请注意,方法返回的打印机列表可能会随着新打印机的安装或旧打印机的移除而改变。使用PrintergetName()方法获取打印机的名称。

Printer.getDefaultPrinter()方法返回默认的Printer。如果没有安装打印机,该方法可能返回null。机器上的默认打印机可以更改。因此,该方法可能在调用之间返回不同的打印机,并且返回的打印机可能在一段时间后无效。

您可以通过调用PrinterJob.createPrinterJob()方法来创建打印机作业。它返回一个PrinterJob类的对象。一旦获得了一个PrinterJob对象,就调用它的printPage()方法来打印一个节点。要打印的节点作为参数传递给方法。

打印 API 允许用户与打印过程进行交互。用户可以在打印开始前交互更改打印机设置。API 允许您显示页面设置和打印设置对话框,用于设置作业的页面属性和打印机设置。您可以让用户通过显示页面设置对话框来配置页面布局。使用PrinterJobshowPageSetupDialog(Window owner)方法显示页面设置对话框。用户可以设置页面大小、来源、方向和页边距。该对话框可以允许用户访问其他打印属性,例如打印机列表。

打印 API 允许您自定义打印机作业设置。API 包含两个与打印机和打印机作业设置相关的类:PrinterAttributesJobSettings类。打印机具有指示打印机打印能力的属性,如默认纸张尺寸、支持的纸张尺寸、最大份数和默认排序规则。一个PrinterAttributes对象封装了打印机的属性。打印 API 不允许您更改打印机属性,因为您不能更改打印机的功能。您不能直接创建一个PrinterAttributes对象。你需要使用getPrinterAttributes()方法从一个Printer对象中获取它。

PageLayout类的一个实例表示打印作业的页面设置。默认情况下,它被设置为打印机默认值。PageLayout用于配置页面的可打印区域,该区域必须位于硬件的可打印区域内。如果页面呈现在硬件的可打印区域之外,内容将被剪切。你不能直接创建一个PageLayout对象。你需要使用PrintercreatePageLayout()方法之一来获得PageLayout

打印网页内容有一种特殊的方法。使用WebEngine类的print(PrinterJob job)方法打印引擎加载的网页。该方法不修改指定的job。在print()方法调用后,该作业可用于更多的打印。

posted @ 2024-08-06 16:36  绝不原创的飞龙  阅读(1)  评论(0编辑  收藏  举报