Skip to content

Latest commit

 

History

History
1494 lines (1110 loc) · 66.6 KB

File metadata and controls

1494 lines (1110 loc) · 66.6 KB

十二、Java GUI 编程

本章概述了 Java 图形用户界面GUI)技术,并演示了如何使用 JavaFX 工具包创建 GUI 应用。JavaFX 的最新版本不仅提供了许多有用的特性,还允许保留和嵌入遗留的实现和样式

本章将讨论以下主题:

  • Java GUI 技术
  • JavaFX 基础
  • 你好,JavaFX
  • 控制元素
  • 图表
  • 应用 CSS
  • 使用 FXML
  • 嵌入 HTML
  • 播放媒体
  • 添加效果

Java GUI 技术

名称 Java 基础类JFC)可能会引起很多混淆。这意味着在 Java java T5 的基础上的类,而事实上,JFC 只包含与 GUI 相关的类和接口。准确地说,JFC 是三个框架的集合:抽象窗口工具包AWT)、Swing 和 Java2d

JFC 是 Java 类库JCL)的一部分,尽管 JFC 这个名字是 1997 年才出现的,而 AWT 从一开始就是 JCL 的一部分。当时,Netscape 开发了一个名为 互联网基础类IFC)的 GUI 库,微软也为 GUI 开发创建了应用基础类AFC)。因此,当 Sun Microsystems 和 Netscape 决定建立一个新的 GUI 库时,他们继承了Foundation这个词,并创建了 JFC。Swing 框架从 AWT 接管了 JavaGUI 编程,并成功地使用了近 20 年

Java8 中的 JCL 添加了一个新的 GUI 编程工具包 JavaFX。它是在 Java11 中从 JCL 中删除的,从那时起,它就作为一个开放源代码项目驻留在 Gluon 公司的支持下,作为 JDK 之外的一个可下载模块。JavaFX 使用与 AWT 和 Swing 稍有不同的 GUI 编程方法。它提供了一个更一致、更简单的设计,很有可能成为一个成功的 JavaGUI 编程工具包。

JavaFX 基础

纽约、伦敦、巴黎和莫斯科等城市有许多剧院,住在那里的人们几乎每周都会听到新的戏剧和作品。这使他们不可避免地熟悉戏剧术语,其中最常用的可能是舞台场景事件。这三个术语也是 Java 语言应用结构的基础。

JavaFX 中包含所有其他组件的顶级容器由javafx.stage.Stage类表示。所以,可以说,在 JavaFX 应用中,一切都发生在舞台上。从用户的角度来看,它是一个显示区域或窗口,所有控件和组件在其中执行它们的操作(就像剧院中的演员)。而且,与剧院中的演员类似,他们在场景的上下文中这样做,由javafx.scene.Scene类表示。因此,JavaFX 应用就像剧院中的戏剧一样,由Stage对象中呈现的Scene对象组成,一次呈现一个。每个Scene对象都包含一个图形,它定义了场景参与者的位置,在 JavaFX 中称为节点:控件、布局、组、形状等等。它们都扩展了抽象类javafx.scene.Node

一些节点控件与事件关联:例如,单击的按钮或选中的复选框。这些事件可以由与相应控制元素关联的事件处理器来处理

JavaFX 应用的主类必须扩展抽象类java.application.Application,它有几个生命周期方法。我们按照调用的顺序列出它们:launch()init()notifyPreloader()start()stop()。看来要记住的还真不少。但是,最有可能的是,您只需要实现一个方法start(),在这里构建并执行实际的 GUI。因此,我们将回顾所有方法的完整性:

  • static void launch(Class<? extends Application> appClass, String... args):启动应用,通常由main方法调用;直到Platform.exit()被调用或所有应用窗口关闭后才返回,appClass参数必须是Application的一个公共子类,具有一个公共的无参数构造器

  • static void launch(String... args):与前面的方法相同,假设Applicationpublic子类是立即封闭的类,这是启动 JavaFX 应用最常用的方法,我们也将在示例中使用它

  • void init():这个方法是在Application类被加载后调用的,通常用于某种资源初始化,默认实现什么都不做,我们不打算使用它

  • void notifyPreloader(Preloader.PreloaderNotification info):初始化时间长时可以显示进度,我们不使用

  • abstract void start(Stage primaryStage):我们要实现的方法,init()方法返回后调用,primaryStage参数是应用呈现场景的阶段

  • void stop():当应用应该停止时调用,可以用来释放资源,默认实现什么都不做,我们不使用

JavaFX 工具包的 API 可以在网上找到。在撰写本文时,最新版本是 11。Oracle 也提供了大量的文档和代码示例。文档包括 Scene Builder(一个开发工具)的描述和用户手册,它提供了一个可视化的布局环境,让您无需编写任何代码就可以快速地为 JavaFX 应用设计用户界面。这个工具对于创建复杂的 GUI 可能很有用,而且很多人一直都在这么做。

要做到这一点,首先需要三个步骤:

  1. 将以下依赖项添加到pom.xml文件:
<dependency>
   <groupId>org.openjfx</groupId>
   <artifactId>javafx-controls</artifactId>
   <version>11</version>
</dependency>
<dependency>
   <groupId>org.openjfx</groupId>
   <artifactId>javafx-fxml</artifactId>
   <version>11</version>
</dependency>
  1. 这个页面下载适用于您操作系统的 JavaFXSDK 并在任何目录中解压。
  2. 假设您已将 JavaFX SDK 解压到/path/JavaFX/文件夹中,请将以下选项添加到将在 Linux 平台上启动 JavaFX 应用的 Java 命令中:
--module-path /path/JavaFX/lib -add-modules=javafx.controls,javafx.fxml

在 Windows 上,相同的选项如下所示:

--module-path C:\path\JavaFX\lib -add-modules=javafx.controls,javafx.fxml

/path/JavaFX/C:\path\JavaFX\是占位符,您需要用包含 JavaFXSDK 的文件夹的实际路径替换它们。

假设应用的主类是HelloWorld,如果是 IntelliJ,则在VM options字段中输入前面的选项,如下所示:

这些选项必须添加到源代码包ch12_guiHelloWorldBlendEffectOtherEffects类的运行/调试配置中。如果您喜欢不同的 IDE 或有不同的操作系统,您可以在openjfx.io文档中找到如何设置它的建议。

要从命令行运行HelloWorldBlendEffectOtherEffects类,请在 Linux 平台上的项目根目录(即pom.xml文件所在的目录)中使用以下命令:

mvn clean package

java --module-path /path/javaFX/lib --add-modules=javafx.controls,javafx.fxml -cp target/learnjava-1.0-SNAPSHOT.jar:target/libs/* com.packt.learnjava.ch12_gui.HelloWorld

java --module-path /path/javaFX/lib --add-modules=javafx.controls,javafx.fxml -cp target/learnjava-1.0-SNAPSHOT.jar:target/libs/* com.packt.learnjava.ch12_gui.BlendEffect

java --module-path /path/javaFX/lib --add-modules=javafx.controls,javafx.fxml -cp target/learnjava-1.0-SNAPSHOT.jar:target/libs/* com.packt.learnjava.ch12_gui.OtherEffects

在 Windows 上,相同的命令如下所示:

mvn clean package

java --module-path C:\path\JavaFX\lib --add-modules=javafx.controls,javafx.fxml -cp target\learnjava-1.0-SNAPSHOT.jar;target\libs\* com.packt.learnjava.ch12_gui.HelloWorld

java --module-path C:\path\JavaFX\lib --add-modules=javafx.controls,javafx.fxml -cp target\learnjava-1.0-SNAPSHOT.jar;target\libs\* com.packt.learnjava.ch12_gui.BlendEffect

java --module-path C:\path\JavaFX\lib --add-modules=javafx.controls,javafx.fxml -cp target\learnjava-1.0-SNAPSHOT.jar;target\libs\* com.packt.learnjava.ch12_gui.OtherEffects

HelloWorldBlendEffectOtherEffects每个类都有几个start()方法:start1()start2()等,运行一次该类后,将start()重命名为start1()start1()重命名为start(),再运行上述命令。然后将start()重命名为start2(),将start2()重命名为start(),再次运行上述命令。以此类推,直到所有的start()方法都被执行。这样,您将看到本章所有示例的结果。

这就是 JavaFX 的高级视图的全部内容。有了这些,我们进入了最激动人心的部分(对于任何程序员来说):编写代码。

你好,JavaFX

下面是显示文本 HelloWorld 的HelloWorldJavaFX 应用:

import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.control.Button;
import javafx.scene.layout.Pane;
import javafx.scene.text.Text;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class HelloWorld extends Application {
    public static void main(String... args) {
        launch(args);
    }
    @Override
    public void start(Stage primaryStage) {
        Text txt = new Text("Hello, world!");
        txt.relocate(135, 40);

        Button btn = new Button("Exit");
        btn.relocate(155, 80);
        btn.setOnAction(e:> {
            System.out.println("Bye! See you later!");
            Platform.exit();
        });

        Pane pane = new Pane();
        pane.getChildren().addAll(txt, btn);

        primaryStage.setTitle("The primary stage (top-level container)");
        primaryStage.onCloseRequestProperty()
               .setValue(e:> System.out.println("Bye! See you later!"));
        primaryStage.setScene(new Scene(pane, 350, 150));
        primaryStage.show();
    }
}

如您所见,应用是通过调用静态方法Application.launch(String... args)来启动的。start(Stage primaryStage)方法创建一个Text节点,消息是 HelloWorld 位于绝对位置 135(水平)和 40(垂直)。然后创建另一个节点Button,退出文本位于 155(水平)和 80(垂直)的绝对位置。分配给按钮的操作(单击时)将打印“GoodBye”,并强制应用使用Platform.exit()方法退出。这两个节点作为子节点添加到允许绝对定位的布局窗格中

Stage对象指定了主阶段(顶级容器)标题。它还指定了单击窗口上角的关闭窗口符号(x 按钮)的操作。此符号在 Linux 系统上显示在左侧,在 Windows 系统上显示在右侧

在创建动作时,我们使用了 Lambda 表达式,我们将在第 13 章、“函数式编程”中讨论。

创建的布局窗格设置在Scene对象上。场景大小水平设置为 350,垂直设置为 150。场景对象放置在舞台上。然后通过调用show()方法显示舞台。

如果我们运行前面的应用,将弹出以下窗口:

单击上角的按钮或 x 按钮将显示预期的消息:

但是如果在点击 x 按钮并关闭窗口后需要执行其他操作,可以在HelloWorld类中添加stop()方法的实现,例如如下所示:

@Override
public void stop(){
    System.out.println("Doing what has to be done before closing");
}

如果是,则单击 x 按钮后,显示屏将显示以下内容:

这个例子让您了解 JavaFX 是如何工作的。从现在开始,在回顾 JavaFX 功能的同时,我们将只展示start()方法中的代码。

这个工具箱有大量的包,每个包有许多类,每个类有许多方法,我们没有机会讨论所有这些。相反,我们将概述 JavaFX 功能的所有主要方面,并以最简单、最直接的方式展示它。

控制元素

控制元素包含在javafx.scene.control包装中。其中有 80 多个,包括按钮、文本字段、复选框、标签、菜单、进度条和滚动条等等。正如我们已经提到的,每个控制元素都是Node的一个子类,它有 200 多个方法。因此,您可以想象使用 JavaFX 可以构建多丰富、多精细的 GUI。然而,这本书的范围允许我们只涵盖一些元素及其方法。

我们已经看到一个按钮。现在让我们使用一个标签和一个文本字段来创建一个带有输入字段(名字、姓氏和年龄)和一个submit按钮的简单表单。我们将分步建造。以下所有代码片段都是start()方法的连续部分。

首先,让我们创建控件:

Text txt = new Text("Fill the form and click Submit");
TextField tfFirstName = new TextField();
TextField tfLastName = new TextField();
TextField tfAge = new TextField();
Button btn = new Button("Submit");
btn.setOnAction(e:> action(tfFirstName, tfLastName, tfAge));

正如你所猜测的,文本将被用作形式说明。其余部分非常简单,看起来与我们在HelloWolrd示例中看到的非常相似。action()是一个按以下方法实现的函数:

void action(TextField tfFirstName, 
                TextField tfLastName, TextField tfAge ) {
    String fn = tfFirstName.getText();
    String ln = tfLastName.getText();
    String age = tfAge.getText();
    int a = 42;
    try {
        a = Integer.parseInt(age);
    } catch (Exception ex){}
    fn = fn.isBlank() ? "Nick" : fn;
    ln = ln.isBlank() ? "Samoylov" : ln;
    System.out.println("Hello, " + fn + " " + ln + ", age " + a + "!");
    Platform.exit();
}

此函数接受三个参数(javafx.scene.control.TextField对象),然后获取提交的输入值并打印它们。该代码确保始终有一些默认值可用于打印,并且输入非数字的年龄值不会中断应用。

在控件和操作就位后,我们使用类javafx.scene.layout.GridPane将它们放入网格布局中:

GridPane grid = new GridPane();
grid.setAlignment(Pos.CENTER);
grid.setHgap(15);
grid.setVgap(5);
grid.setPadding(new Insets(20, 20, 20, 20));

GridPane布局窗格有行和列,这些行和列构成可以在其中设置节点的单元格。节点可以跨越列和行,setAlignment()方法将网格的位置设置为场景的中心(默认位置为场景的左上角)。setHgap()setVgap()方法设置列(水平)和行(垂直)之间的间距(以像素为单位)。setPadding()方法沿网格窗格的边界添加一些空间。Insets()对象按上、右、下、左的顺序设置值(以像素为单位)

现在我们将把创建的节点放在相应的单元格中(按两列排列):

int i = 0;
grid.add(txt,    1, i++, 2, 1);
GridPane.setHalignment(txt, HPos.CENTER);
grid.addRow(i++, new Label("First Name"), tfFirstName);
grid.addRow(i++, new Label("Last Name"),  tfLastName);
grid.addRow(i++, new Label("Age"), tfAge);
grid.add(btn,    1, i);
GridPane.setHalignment(btn, HPos.CENTER);

add()方法接受三个或五个参数:

  • 节点、列索引、行索引
  • 节点、列索引、行索引、要跨多少列、要跨多少行

列和行索引从0开始

setHalignment()方法设置节点在小区中的位置。枚举HPos有值:LEFTRIGHTCENTER。方法addRow(int i, Node... nodes)接受行索引和节点变量。我们用它来放置LabelTextField对象

start()方法的其余部分与HellowWorld示例非常相似(只有标题和大小发生了变化):

primaryStage.setTitle("Simple form example");
primaryStage.onCloseRequestProperty()
        .setValue(e -> System.out.println("Bye! See you later!"));
primaryStage.setScene(new Scene(grid, 300, 200));
primaryStage.show();

如果我们运行刚刚实现的start()方法,结果如下:

我们可以按如下方式填写数据,例如:

单击“提交”按钮后,将显示以下消息,并且应用已存在:

为了帮助可视化布局,特别是在更复杂的设计中,可以使用网格方法setGridLinesVisible(boolean v)使网格线可见。这有助于查看单元格的对齐方式。我们可以在示例中添加以下行:

grid.setGridLinesVisible(true);

我们再运行一次,结果如下:

如您所见,布局现在已明确列出,这有助于可视化设计。

javafx.scene.layout包包括 24 个布局类,例如Pane(我们在HelloWorld示例中看到过)、StackPane(允许我们覆盖节点)、FlowPane(允许节点的位置随着窗口大小的变化而流动)、AnchorPane(保留节点相对于其锚定点的位置),等等。VBox布局将在下一节“图表”中演示。

图表

JavaFX 为javafx.scene.chart包中的数据可视化提供了以下图表组件:

  • LineChart:在一系列数据点之间添加一条线;通常用于表示随时间变化的趋势
  • AreaChart:与LineChart类似,但填充连接数据点的线和轴之间的区域;通常用于比较一段时间内累积的总和
  • BarChart:以矩形条表示数据,用于离散数据的可视化
  • PieChart:表示一个分为若干段的圆(用不同的颜色填充),每一段代表一个值占总数的比例,我们将在本节中演示
  • BubbleChart:将数据呈现为二维椭圆形,称为气泡,允许呈现三个参数
  • ScatterChart:按原样显示序列中的数据点;有助于识别是否存在聚类(数据相关性)

下面的示例演示如何将测试结果显示为饼图。每个段表示成功、失败或忽略的测试数:

Text txt = new Text("Test results:");

PieChart pc = new PieChart();
pc.getData().add(new PieChart.Data("Succeed", 143));
pc.getData().add(new PieChart.Data("Failed" ,  12));
pc.getData().add(new PieChart.Data("Ignored",  18));

VBox vb = new VBox(txt, pc);
vb.setAlignment(Pos.CENTER);
vb.setPadding(new Insets(10, 10, 10, 10));

primaryStage.setTitle("A chart example");
primaryStage.onCloseRequestProperty()
        .setValue(e:> System.out.println("Bye! See you later!"));
primaryStage.setScene(new Scene(vb, 300, 300));
primaryStage.show();

我们已经创建了两个节点-TextPieChart,并将它们放置在VBox布局的单元格中,该布局将它们设置为一列,一个在另一列之上。我们在VBox窗格的边缘添加了 10 个像素的填充。请注意,VBox扩展了NodePane类,就像其他窗格一样。我们还使用setAlignment()方法将窗格放置在场景中心。其余部分与前面的所有示例相同,只是场景标题和大小不同

如果我们运行前面的示例,结果如下:

PieChart类以及任何其他图表都有许多其他方法,这些方法对于以用户友好的方式呈现更复杂和动态的数据非常有用。

应用 CSS

默认情况下,JavaFX 使用分发 Jar 文件附带的样式表。要覆盖默认样式,可以使用getStylesheets()方法将样式表添加到场景中:

scene.getStylesheets().add("/mystyle.css");

mystyle.css文件必须放在src/main/resources文件夹中。让我们这样做,并将具有以下内容的mystyle.css文件添加到HelloWorld示例中:

#text-hello {
  :fx-font-size: 20px;
   -fx-font-family: "Arial";
   -fx-fill: red;
}
.button {
   -fx-text-fill: white;
   -fx-background-color: slateblue;
}

如您所见,我们希望以某种方式设置按钮节点和具有 IDtext-helloText节点的样式。我们还必须修改 HelloWorld 示例,将 ID 添加到Text元素中,并将样式表文件添加到场景中:

Text txt = new Text("Hello, world!");
txt.setId("text-hello");
txt.relocate(115, 40);

Button btn = new Button("Exit");
btn.relocate(155, 80);
btn.setOnAction(e -> {
    System.out.println("Bye! See you later!");
    Platform.exit();
});

Pane pane = new Pane();
pane.getChildren().addAll(txt, btn);

Scene scene = new Scene(pane, 350, 150);
scene.getStylesheets().add("/mystyle.css");

primaryStage.setTitle("The primary stage (top-level container)");
primaryStage.onCloseRequestProperty()
        .setValue(e -> System.out.println("\nBye! See you later!"));
primaryStage.setScene(scene);
primaryStage.show();

如果现在运行此代码,结果如下:

或者,可以在将用于覆盖文件样式表的任何节点上设置内联样式,无论是否为默认样式

btn.setStyle("-fx-text-fill: white; -fx-background-color: red;");

如果我们再次运行该示例,结果如下:

浏览 JavaFXCSS 参考指南了解定制造型的种类和可能的选择。

使用 FXML

FXML 是一种基于 XML 的语言,它允许构建一个用户界面,并独立地维护应用(业务)逻辑的用户界面(就外观和感觉或其他与表示相关的更改而言)。使用 FXML,您甚至不用编写一行 Java 代码就可以设计用户界面。

FXML 没有模式,但其功能反映了用于构建场景的 JavaFX 对象的 API。这意味着您可以使用 API 文档来了解 FXML 结构中允许哪些标记和属性。大多数情况下,JavaFX 类可以用作标记,它们的属性可以用作属性。

除了 FXML 文件(视图)之外,控制器(Java 类)还可以用于处理模型和组织页面流。模型由视图和控制器管理的域对象组成。它还允许使用 CSS 样式和 JavaScript 的所有功能。但在本书中,我们将只能演示基本的 FXML 功能。剩下的和许多在线的好教程可以在 FXML 简介中找到。

为了演示 FXML 的用法,我们将复制在“控制元素”部分中创建的简单表单,然后通过添加页面流来增强它。以下是我们的名、姓和年龄表单如何在 FXML 中表达:

<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.Scene?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.text.Text?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.control.TextField?>
<Scene fx:controller="com.packt.learnjava.ch12_gui.HelloWorldController"
       xmlns:fx="http://javafx.com/fxml"
       width="350" height="200">
    <GridPane alignment="center" hgap="15" vgap="5">
        <padding>
            <Insets top="20" right="20" bottom="20" left="20"/>
        </padding>
        <Text id="textFill" text="Fill the form and click Submit"
              GridPane.rowIndex="0" GridPane.columnSpan="2">
            <GridPane.halignment>center</GridPane.halignment>
        </Text>
        <Label text="First name"
               GridPane.columnIndex="0" GridPane.rowIndex="1"/>
        <TextField fx:id="tfFirstName"
                   GridPane.columnIndex="1" GridPane.rowIndex="1"/>
        <Label text="Last name"
               GridPane.columnIndex="0" GridPane.rowIndex="2"/>
        <TextField fx:id="tfLastName"
                   GridPane.columnIndex="1" GridPane.rowIndex="2"/>
        <Label text="Age"
               GridPane.columnIndex="0" GridPane.rowIndex="3"/>
        <TextField fx:id="tfAge"
                   GridPane.columnIndex="1" GridPane.rowIndex="3"/>
        <Button text="Submit"
                GridPane.columnIndex="1" GridPane.rowIndex="4"
                onAction="#submitClicked">
            <GridPane.halignment>center</GridPane.halignment>
        </Button>
    </GridPane>
</Scene>

如您所见,它表达了您已经熟悉的所需场景结构,并指定了控制器类HelloWorldController,我们稍后将看到它。正如我们已经提到的,这些标记与我们用来仅用 Java 构建同一 GUI 的类名相匹配。我们将把helloWorld.fxml文件放入resources文件夹。

现在让我们看一下使用前面的FXML文件的HelloWorld类的start()方法实现:

try {
  FXMLLoader lder = new FXMLLoader();
  lder.setLocation(new URL("file:src/main/resources/helloWorld.fxml"));
  Scene scene = lder.load();

  primaryStage.setTitle("Simple form example");
  primaryStage.setScene(scene);
  primaryStage.onCloseRequestProperty()
          .setValue(e -> System.out.println("\nBye! See you later!"));
  primaryStage.show();
} catch (Exception ex){
    ex.printStackTrace();
}

start()方法只是加载helloWorld.fxml文件并设置舞台,后者的操作与前面的示例完全相同。现在让我们看看HelloWorldController类,如果需要,我们可以启动只有以下内容的应用:

public class HelloWorldController {
    @FXML
    protected void submitClicked(ActionEvent e) {
    }
}

表单将被显示,但按钮单击将不起任何作用。这就是我们在讨论独立于应用逻辑的用户界面开发时的意思。注意@FXML注解。它使用 FXML 标记的 ID 将方法和属性绑定到 FXML 标记。以下是完整控制器实现的外观:

@FXML
private TextField tfFirstName;
@FXML
private TextField tfLastName;
@FXML
private TextField tfAge;
@FXML
protected void submitClicked(ActionEvent e) {
    String fn = tfFirstName.getText();
    String ln = tfLastName.getText();
    String age = tfAge.getText();
    int a = 42;
    try {
        a = Integer.parseInt(age);
    } catch (Exception ex) {
    }
    fn = fn.isBlank() ? "Nick" : fn;
    ln = ln.isBlank() ? "Samoylov" : ln;
    System.out.println("Hello, " + fn + " " + ln + ", age " + a + "!");
    Platform.exit();
}

在大多数情况下,你应该很熟悉它。唯一的区别是我们并没有直接引用字段及其值(如前所述),而是使用带有注解@FXML的绑定。如果现在运行HelloWorld类,页面外观和行为将与我们在“控制元素”部分中描述的完全相同。

现在,我们添加另一个页面并修改代码,以便在点击Submit按钮后,控制器将提交的值发送到另一个页面并关闭表单。为了简单起见,新页面将只显示接收到的数据。以下是 FXML 的外观:

<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.Scene?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.text.Text?>
<?import javafx.scene.layout.GridPane?>

<Scene fx:controller="com.packt.lernjava.ch12_gui.HelloWorldController2"
       xmlns:fx="http://javafx.com/fxml"
       width="350" height="150">
    <GridPane alignment="center" hgap="15" vgap="5">
        <padding>
            <Insets top="20" right="20" bottom="20" left="20"/>
        </padding>
        <Text fx:id="textUser"
              GridPane.rowIndex="0" GridPane.columnSpan="2">
            <GridPane.halignment>center</GridPane.halignment>
        </Text>
        <Text id="textDo" text="Do what has to be done here"
              GridPane.rowIndex="1" GridPane.columnSpan="2">
            <GridPane.halignment>center</GridPane.halignment>
        </Text>
    </GridPane>
</Scene>

如您所见,页面只有两个只读的Text字段。第一个(带id="textUser")将显示上一页传递的数据。第二个只会显示消息“执行此处必须执行的操作”。这不是很复杂,但它演示了如何组织数据流和页面。

新页面使用不同的控制器,如下所示:

package com.packt.learnjava.ch12_gui;
import javafx.fxml.FXML;
import javafx.scene.text.Text;
public class HelloWorldController2 {
    @FXML
    public Text textUser;
}

正如您可能猜到的,公共字段textUser必须由第一个控制器HelloWolrdController填充值。我们开始吧。我们修改submitClicked()方法如下:

@FXML
protected void submitClicked(ActionEvent e) {
    String fn = tfFirstName.getText();
    String ln = tfLastName.getText();
    String age = tfAge.getText();
    int a = 42;
    try {
        a = Integer.parseInt(age);
    } catch (Exception ex) {}
    fn = fn.isBlank() ? "Nick" : fn;
    ln = ln.isBlank() ? "Samoylov" : ln;
    String user = "Hello, " + fn + " " + ln + ", age " + a + "!";
    //System.out.println("\nHello, " + fn + " " + ln + ", age " + a + "!");
    //Platform.exit();

    goToPage2(user);
    Node source = (Node) e.getSource();
    Stage stage = (Stage) source.getScene().getWindow();
    stage.close();
}

我们不只是打印提交的(或默认的)数据并退出应用(参见注释掉的两行),而是调用goToPage2()方法并将提交的数据作为参数传递。然后我们从事件中提取对当前窗口阶段的引用并关闭它

goToPage2()方法如下:

try {
  FXMLLoader lder = new FXMLLoader();
  lder.setLocation(new URL("file:src/main/resources/helloWorld2.fxml"));
  Scene scene = lder.load();

  HelloWorldController2 c = loader.getController();
  c.textUser.setText(user);

  Stage primaryStage = new Stage();
  primaryStage.setTitle("Simple form example. Page 2.");
  primaryStage.setScene(scene);
  primaryStage.onCloseRequestProperty()
            .setValue(e -> {
                System.out.println("Bye! See you later!");
                Platform.exit();
            });
  primaryStage.show();
} catch (Exception ex) {
    ex.printStackTrace();
}

它加载helloWorld2.fxml文件,从中提取控制器对象,并在其上设置传入的值。其余的与您现在已经见过几次的舞台配置相同。唯一的区别是第 2 页被添加到标题中

如果我们现在执行HelloWorld类,我们将看到熟悉的表单并用数据填充它:

单击“提交”按钮后,此窗口将关闭并显示新窗口:

我们单击左上角的 x 按钮(或者在 Windows 上单击右上角),会看到与我们之前看到的相同的消息:

同级动作功能和stop()方法如预期效果。

至此,我们结束了对 FXML 的介绍,并进入下一个主题,即向 JavaFX 应用添加 HTML。

嵌入 HTML

向 JavaFX 添加 HTML 很容易。您所要做的就是使用javafx.scene.web.WebView类,该类提供了一个窗口,在该窗口中,添加的 HTML 的呈现方式与浏览器中的呈现方式类似。WebView类使用开源浏览器引擎 WebKit,因此支持完整的浏览功能。

与所有其他 JavaFX 组件一样,WebView类扩展了Node类,可以在 Java 代码中这样处理。此外,它有自己的属性和方法,允许通过设置窗口大小(最大值、最小值和首选高度和宽度)、字体比例、缩放率、添加 CSS、启用上下文(右键单击)菜单等来调整浏览器窗口以适应所包括的应用是的。它提供了加载 HTML 页面、导航页面、对加载的页面应用不同样式、访问页面浏览历史和文档模型以及执行 JavaScript 的功能

要开始使用javafx.scene.web包,必须首先采取两个步骤:

  1. 将以下依赖项添加到pom.xml文件:
<dependency>
 <groupId>org.openjfx</groupId>
 <artifactId>javafx-web</artifactId>
 <version>11.0.2</version>
</dependency>

javafx-web的版本通常与 Java 版本保持同步,但在撰写本文时,javafx-web的第 12 版尚未发布,因此我们使用的是最新的可用版本 11.0.2。

  1. 因为javafx-web使用了从 Java9 中删除的包(com.sun.*),要从 Java9+ 访问com.sun.*包,除了设置--module-path--add-modules之外,还要设置以下 VM 选项,在ch12_gui包的HtmlWebView类的“JavaFX 基础”部分的运行/调试配置中描述:
--add-exports javafx.graphics/com.sun.javafx.sg.prism=ALL-UNNAMED 
--add-exports javafx.graphics/com.sun.javafx.scene=ALL-UNNAMED 
--add-exports javafx.graphics/com.sun.javafx.util=ALL-UNNAMED 
--add-exports javafx.base/com.sun.javafx.logging=ALL-UNNAMED 
--add-exports javafx.graphics/com.sun.prism=ALL-UNNAMED 
--add-exports javafx.graphics/com.sun.glass.ui=ALL-UNNAMED 
--add-exports javafx.graphics/com.sun.javafx.geom.transform=ALL-UNNAMED 
--add-exports javafx.graphics/com.sun.javafx.tk=ALL-UNNAMED 
--add-exports javafx.graphics/com.sun.glass.utils=ALL-UNNAMED 
--add-exports javafx.graphics/com.sun.javafx.font=ALL-UNNAMED 
--add-exports javafx.graphics/com.sun.javafx.application=ALL-UNNAMED 
--add-exports javafx.controls/com.sun.javafx.scene.control=ALL-UNNAMED 
--add-exports javafx.graphics/com.sun.javafx.scene.input=ALL-UNNAMED 
--add-exports javafx.graphics/com.sun.javafx.geom=ALL-UNNAMED 
--add-exports javafx.graphics/com.sun.prism.paint=ALL-UNNAMED 
--add-exports javafx.graphics/com.sun.scenario.effect=ALL-UNNAMED 
--add-exports javafx.graphics/com.sun.javafx.text=ALL-UNNAMED 
--add-exports javafx.graphics/com.sun.javafx.iio=ALL-UNNAMED
--add-exports javafx.graphics/com.sun.scenario.effect.impl.prism=ALL-UNNAMED
--add-exports javafx.graphics/com.sun.javafx.scene.text=ALL-UNNAMED

要从命令行执行类HtmlWebView,请使用以下命令:

mvn clean package

java --module-path /path/javaFX/lib --add-modules=javafx.controls,javafx.fxml --add-exports javafx.graphics/com.sun.javafx.sg.prism=ALL-UNNAMED --add-exports javafx.graphics/com.sun.javafx.scene=ALL-UNNAMED --add-exports javafx.graphics/com.sun.javafx.util=ALL-UNNAMED --add-exports javafx.base/com.sun.javafx.logging=ALL-UNNAMED --add-exports javafx.graphics/com.sun.prism=ALL-UNNAMED --add-exports javafx.graphics/com.sun.glass.ui=ALL-UNNAMED --add-exports javafx.graphics/com.sun.javafx.geom.transform=ALL-UNNAMED --add-exports javafx.graphics/com.sun.javafx.tk=ALL-UNNAMED --add-exports javafx.graphics/com.sun.glass.utils=ALL-UNNAMED  --add-exports javafx.graphics/com.sun.javafx.font=ALL-UNNAMED  --add-exports javafx.graphics/com.sun.javafx.application=ALL-UNNAMED --add-exports javafx.controls/com.sun.javafx.scene.control=ALL-UNNAMED --add-exports javafx.graphics/com.sun.javafx.scene.input=ALL-UNNAMED --add-exports javafx.graphics/com.sun.javafx.geom=ALL-UNNAMED  --add-exports javafx.graphics/com.sun.prism.paint=ALL-UNNAMED  --add-exports javafx.graphics/com.sun.scenario.effect=ALL-UNNAMED --add-exports javafx.graphics/com.sun.javafx.text=ALL-UNNAMED --add-exports javafx.graphics/com.sun.javafx.iio=ALL-UNNAMED --add-exports javafx.graphics/com.sun.scenario.effect.impl.prism=ALL-UNNAMED --add-exports javafx.graphics/com.sun.javafx.scene.text=ALL-UNNAMED  -cp target/learnjava-1.0-SNAPSHOT.jar:target/libs/* com.packt.learnjava.ch12_gui.HtmlWebView

在 Windows 上,相同的命令如下所示:

mvn clean package

java --module-path C:\path\JavaFX\lib --add-modules=javafx.controls,javafx.fxml --add-exports javafx.graphics/com.sun.javafx.sg.prism=ALL-UNNAMED --add-exports javafx.graphics/com.sun.javafx.scene=ALL-UNNAMED --add-exports javafx.graphics/com.sun.javafx.util=ALL-UNNAMED --add-exports javafx.base/com.sun.javafx.logging=ALL-UNNAMED --add-exports javafx.graphics/com.sun.prism=ALL-UNNAMED --add-exports javafx.graphics/com.sun.glass.ui=ALL-UNNAMED --add-exports javafx.graphics/com.sun.javafx.geom.transform=ALL-UNNAMED --add-exports javafx.graphics/com.sun.javafx.tk=ALL-UNNAMED --add-exports javafx.graphics/com.sun.glass.utils=ALL-UNNAMED  --add-exports javafx.graphics/com.sun.javafx.font=ALL-UNNAMED  --add-exports javafx.graphics/com.sun.javafx.application=ALL-UNNAMED --add-exports javafx.controls/com.sun.javafx.scene.control=ALL-UNNAMED --add-exports javafx.graphics/com.sun.javafx.scene.input=ALL-UNNAMED --add-exports javafx.graphics/com.sun.javafx.geom=ALL-UNNAMED  --add-exports javafx.graphics/com.sun.prism.paint=ALL-UNNAMED  --add-exports javafx.graphics/com.sun.scenario.effect=ALL-UNNAMED --add-exports javafx.graphics/com.sun.javafx.text=ALL-UNNAMED --add-exports javafx.graphics/com.sun.javafx.iio=ALL-UNNAMED --add-exports javafx.graphics/com.sun.scenario.effect.impl.prism=ALL-UNNAMED --add-exports javafx.graphics/com.sun.javafx.scene.text=ALL-UNNAMED  -cp target\learnjava-1.0-SNAPSHOT.jar;target\libs\* com.packt.learnjava.ch12_gui.HtmlWebView

HtmlWebView也包含几个start()方法。按照“JavaFX 基础”一节中的描述,逐个重命名并执行它们。

现在我们来看几个例子。我们创建一个新的应用HtmlWebView,并使用前面描述的 VM 选项--module-path--add-modules--add-exports为其设置 VM 选项。现在我们可以编写并执行一个使用WebView类的代码。

首先,下面是如何将简单的 HTML 添加到 JavaFX 应用:

WebView wv = new WebView();
WebEngine we = wv.getEngine();
String html = "<html><center><h2>Hello, world!</h2></center></html>";
we.loadContent(html, "text/html");
Scene scene = new Scene(wv, 200, 60);
primaryStage.setTitle("My HTML page");
primaryStage.setScene(scene);
primaryStage.onCloseRequestProperty()
            .setValue(e -> System.out.println("Bye! See you later!"));
primaryStage.show();

前面的代码创建一个WebView对象,从中获取WebEngine对象,使用获取的WebEngine对象加载 HTML,在场景中设置WebView对象,并配置舞台。loadContent()方法接受两个字符串:内容及其 MIME 类型。内容字符串可以在代码中构造,也可以通过读取.html文件来创建

如果我们运行前面的示例,结果如下:

如果需要,您可以在同一窗口中显示其他 JavaFX 节点以及WebView对象。例如,让我们在嵌入的 HTML 上面添加一个Text节点:

Text txt = new Text("Below is the embedded HTML:");

WebView wv = new WebView();
WebEngine we = wv.getEngine();
String html = "<html><center><h2>Hello, world!</h2></center></html>";
we.loadContent(html, "text/html");

VBox vb = new VBox(txt, wv);
vb.setSpacing(10);
vb.setAlignment(Pos.CENTER);
vb.setPadding(new Insets(10, 10, 10, 10));

Scene scene = new Scene(vb, 300, 120);
primaryStage.setScene(scene);
primaryStage.setTitle("JavaFX with embedded HTML");
primaryStage.onCloseRequestProperty()
            .setValue(e -> System.out.println("Bye! See you later!"));
primaryStage.show();

如您所见,WebView对象不是直接设置在场景上,而是与txt对象一起设置在布局对象上。然后,在场景中设置布局对象。上述代码的结果如下:

对于更复杂的 HTML 页面,可以使用load()方法直接从文件加载。为了演示这种方法,我们在resources文件夹中创建form.htm文件,内容如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>The Form</title>
</head>
<body>
<form action="http://server:port/formHandler" metrod="post">
    <table>
        <tr>
            <td><label for="firstName">Firts name:</label></td>
            <td><input type="text" id="firstName" name="firstName"></td>
        </tr>
        <tr>
            <td><label for="lastName">Last name:</label></td>
            <td><input type="text" id="lastName" name="lastName"></td>
        </tr>
        <tr>
            <td><label for="age">Age:</label></td>
            <td><input type="text" id="age" name="age"></td>
        </tr>
        <tr>
            <td></td>
            <td align="center">
                <button id="submit" name="submit">Submit</button>
            </td>
        </tr>
    </table>
</form>
</body>
</html>

这个 HTML 呈现的表单与我们在Using FXML部分中创建的表单类似。单击Submit按钮后,表单数据被发布到服务器的\formHandlerURI 中。要在 JavaFX 应用中显示此表单,可以使用以下代码:

Text txt = new Text("Fill the form and click Submit");

WebView wv = new WebView();
WebEngine we = wv.getEngine();
File f = new File("src/main/resources/form.html");
we.load(f.toURI().toString());

VBox vb = new VBox(txt, wv);
vb.setSpacing(10);
vb.setAlignment(Pos.CENTER);
vb.setPadding(new Insets(10, 10, 10, 10));

Scene scene = new Scene(vb, 300, 200);

primaryStage.setScene(scene);
primaryStage.setTitle("JavaFX with embedded HTML");
primaryStage.onCloseRequestProperty()
            .setValue(e -> System.out.println("Bye! See you later!"));
primaryStage.show();

如您所见,与其他示例的不同之处在于,我们现在使用File类及其toURI()方法直接访问src/main/resources/form.html文件中的 HTML,而无需先将内容转换为字符串。结果如下:

当您需要从 JavaFX 应用发送请求或发布数据时,此解决方案非常有用。但是当您希望用户填写的表单在服务器上已经可用时,您可以从 URL 加载它。例如,让我们将 Google 搜索合并到 JavaFX 应用中。我们可以通过将load()方法的参数值更改为要加载的页面的 URL 来实现:

Text txt = new Text("Enjoy searching the Web!");

WebView wv = new WebView();
WebEngine we = wv.getEngine();
we.load("http://www.google.com");

VBox vb = new VBox(txt, wv);
vb.setSpacing(20);
vb.setAlignment(Pos.CENTER);
vb.setStyle("-fx-font-size: 20px;-fx-background-color: lightblue;");
vb.setPadding(new Insets(10, 10, 10, 10));

Scene scene = new Scene(vb,750,500);
primaryStage.setScene(scene);
primaryStage.setTitle("JavaFX with the window to another server");
primaryStage.onCloseRequestProperty()
        .setValue(e -> System.out.println("Bye! See you later!"));
primaryStage.show();

我们还为布局添加了一个样式,以便增加字体并为背景添加颜色,这样我们就可以看到嵌入呈现的 HTML 的区域的轮廓。运行此示例时,将出现以下窗口:

在此窗口中,您可以执行通常通过浏览器访问的搜索的所有方面。

而且,正如我们已经提到的,您可以放大呈现的页面。例如,如果我们将wv.setZoom(1.5)行添加到前面的示例中,结果如下:

同样,我们可以从文件中设置字体的比例,甚至样式:

wv.setFontScale(1.5);
we.setUserStyleSheetLocation("mystyle.css");

但是请注意,我们在WebView对象上设置了字体比例,而在WebEngine对象中设置了样式

我们也可以使用WebEngine类方法getDocument()访问(和操作)加载页面的 DOM 对象:

Document document = we.getDocument();

我们还可以访问浏览历史,获取当前索引,并前后移动历史:

WebHistory history = we.getHistory();  
int currInd = history.getCurrentIndex(); 
history.go(-1);
history.go( 1);

对于历史记录的每个条目,我们可以提取其 URL、标题或上次访问日期:

WebHistory history = we.getHistory();
ObservableList<WebHistory.Entry> entries = history.getEntries();
for(WebHistory.Entry entry: entries){
    String url = entry.getUrl();
    String title = entry.getTitle();
    Date date = entry.getLastVisitedDate();
}

阅读WebViewWebEngine类的文档,了解如何利用它们的功能。

播放媒体

向 JavaFX 应用的场景添加图像不需要com.sun.*包,因此不需要“添加 HTML”部分中列出的--add-exportVM 选项。但是不管怎样,拥有它们并没有什么坏处,所以如果您已经添加了它们,那么就保留--add-export选项。

可以使用类javafx.scene.image.Imagejavafx.scene.image.ImageView将图像包括在场景中。为了演示如何做到这一点,我们将使用位于resources文件夹中的 Packt logopackt.png。下面是执行此操作的代码:

Text txt = new Text("What a beautiful image!");

FileInputStream input = 
               new FileInputStream("src/main/resources/packt.png");
Image image = new Image(input);
ImageView iv = new ImageView(image);

VBox vb = new VBox(txt, iv);
vb.setSpacing(20);
vb.setAlignment(Pos.CENTER);
vb.setPadding(new Insets(10, 10, 10, 10));

Scene scene = new Scene(vb, 300, 200);
primaryStage.setScene(scene);
primaryStage.setTitle("JavaFX with embedded HTML");
primaryStage.onCloseRequestProperty()
        .setValue(e -> System.out.println("Bye! See you later!"));
primaryStage.show();

如果我们运行前面的代码,结果如下:

当前支持的图像格式有 BMP、GIF、JPEG 和 PNG。查看ImageImageView学习根据需要格式化和调整图像的多种方法。

现在让我们看看如何在 JavaFX 应用中使用其他媒体文件。播放音频或电影文件需要在“添加 HTML”部分中列出的--add-exportVM 选项

当前支持的编码如下:

  • AAC:高级音频编码的音频压缩
  • H.264/AVC:H.264/MPEG-4/AVC高级视频编码)视频压缩
  • MP3:原始 MPEG-1、2 和 2.5 音频;第一层、第二层和第三层
  • PCM:未压缩的原始音频样本

您可以在 API 文档中看到对支持的协议、媒体容器和元数据标记的更详细的描述。

以下三个类允许构造可以添加到场景的媒体播放器:

javafx.scene.media.Media;
javafx.scene.media.MediaPlayer;
javafx.scene.media.MediaView;

Media类表示媒体的来源,MediaPlayer类提供了控制媒体播放的所有方法:play(),``stop()pause()setVolume()等。您还可以指定媒体播放的次数。MediaView类扩展了Node类,可以添加到场景中。它提供媒体播放器正在播放的媒体的视图。它负责在媒体上露面。

为了演示,让我们在HtmlWebView应用中添加另一个版本的start()方法,该方法播放位于resources文件夹中的jb.mp3文件:

Text txt1 = new Text("What a beautiful music!");
Text txt2 = new Text("If you don't hear music, turn up the volume.");

File f = new File("src/main/resources/jb.mp3");
Media m = new Media(f.toURI().toString());
MediaPlayer mp = new MediaPlayer(m);
MediaView mv = new MediaView(mp);

VBox vb = new VBox(txt1, txt2, mv);
vb.setSpacing(20);
vb.setAlignment(Pos.CENTER);
vb.setPadding(new Insets(10, 10, 10, 10));

Scene scene = new Scene(vb, 350, 100);
primaryStage.setScene(scene);
primaryStage.setTitle("JavaFX with embedded media player");
primaryStage.onCloseRequestProperty()
        .setValue(e -> System.out.println("Bye! See you later!"));
primaryStage.show();

mp.play();

注意如何基于源文件构造一个Media对象;然后基于Media对象构造MediaPlayer对象,然后将其设置为MediaView类构造器的属性。MediaView对象与两个Text对象一起设置在场景中。我们使用VBox对象来提供布局。最后,在舞台上设置场景并且舞台变得可见之后(在show()方法完成之后),在MediaPlayer对象上调用play()方法。默认情况下,媒体播放一次。

如果执行上述代码,将出现以下窗口并播放jb.m3文件:

我们可以添加控件来停止、暂停和调整音量,但这将需要更多的代码,这不符合本书的预期大小。您可以在 Oracle 在线文档中找到有关如何执行此操作的指南。

sea.mp4电影文件可以类似地播放:

Text txt = new Text("What a beautiful movie!");

File f = new File("src/main/resources/sea.mp4");
Media m = new Media(f.toURI().toString());
MediaPlayer mp = new MediaPlayer(m);
MediaView mv = new MediaView(mp);

VBox vb = new VBox(txt, mv);
vb.setSpacing(20);
vb.setAlignment(Pos.CENTER);
vb.setPadding(new Insets(10, 10, 10, 10));

Scene scene = new Scene(vb, 650, 400);
primaryStage.setScene(scene);
primaryStage.setTitle("JavaFX with embedded media player");
primaryStage.onCloseRequestProperty()
        .setValue(e -> System.out.println("Bye! See you later!"));
primaryStage.show();

mp.play();

唯一的区别是不同大小的场景需要显示这个特定剪辑的完整帧。经过几次试错调整,我们找到了必要的尺寸。或者,我们可以使用MediaView方法autosize()preserveRatioProperty()setFitHeight()setFitWidth()fitWidthProperty()fitHeightProperty()以及类似的方法来调整嵌入窗口的大小并自动匹配场景的大小。如果执行上述示例,将弹出以下窗口并播放片段:

我们甚至可以同时播放音频和视频文件,从而为电影提供配乐:

Text txt1 = new Text("What a beautiful movie and sound!");
Text txt2 = new Text("If you don't hear music, turn up the volume.");

File fs = new File("src/main/resources/jb.mp3");
Media ms = new Media(fs.toURI().toString());
MediaPlayer mps = new MediaPlayer(ms);
MediaView mvs = new MediaView(mps);

File fv = new File("src/main/resources/sea.mp4");
Media mv = new Media(fv.toURI().toString());
MediaPlayer mpv = new MediaPlayer(mv);
MediaView mvv = new MediaView(mpv);

VBox vb = new VBox(txt1, txt2, mvs, mvv);
vb.setSpacing(20);
vb.setAlignment(Pos.CENTER);
vb.setPadding(new Insets(10, 10, 10, 10));

Scene scene = new Scene(vb, 650, 500);
primaryStage.setScene(scene);
primaryStage.setTitle("JavaFX with embedded media player");
primaryStage.onCloseRequestProperty()
        .setValue(e -> System.out.println("Bye! See you later!"));
primaryStage.show();

mpv.play();
mps.play();

这是可能的,因为每个播放器都由自己的线程执行

有关javafx.scene.media包的更多信息,请在线阅读 API 和开发者指南:

添加效果

javafx.scene.effects包包含许多类,允许向节点添加各种效果:

  • Blend:使用一个预定义的BlendMode组合来自两个源(通常是图像)的像素
  • Bloom:使输入图像更亮,使其看起来发光
  • BoxBlur:为图像添加模糊
  • ColorAdjust:允许调整图像的色调、饱和度、亮度和对比度
  • ColorInput:呈现一个矩形区域,其中填充了给定的Paint
  • DisplacementMap:将每个像素移动指定的距离
  • DropShadow:在内容后面呈现给定内容的阴影
  • GaussianBlur:使用特定(高斯)方法添加模糊
  • Glow:使输入图像看起来发光
  • InnerShadow:在帧内创建阴影
  • Lighting:模拟光源照射在内容上;使平面对象看起来更逼真
  • MotionBlur:模拟运动中看到的给定内容
  • PerspectiveTransform:从一个角度转换内容
  • Reflection:呈现低于实际输入内容的输入的反射版本
  • SepiaTone:产生暗褐色的色调效果,类似于古董照片的外观
  • Shadow:创建具有模糊边缘的内容的单色副本

所有效果共享父级-抽象类EffectNode类具有setEffect(Effect e)方法,这意味着可以将任何效果添加到任何节点。这是将效果应用于节点的主要方式,演员在舞台上产生场景(如果我们回想一下本章开头介绍的类比)

唯一的例外是Blend效果,这使得它的使用比其他效果的使用更加复杂。除了使用setEffect(Effect e)方法外,一些Node类的子项还有setBlendMode(BlendMode bm)方法,可以调节图像重叠时如何相互融合。因此,可以以不同的方式设置不同的混合效果,以相互覆盖,并产生可能难以调试的意外结果。这就是为什么Blend效果的使用更加复杂,这就是为什么我们要开始概述Blend效果如何使用的原因。

有四个方面可以控制两个图像重叠区域的外观(我们在示例中使用两个图像使其更简单,但实际上,许多图像可以重叠):

  • 不透明度属性的值:定义通过图像可以看到多少;不透明度值 0.0 表示图像是完全透明的,而不透明度值 1.0 表示后面看不到任何东西。
  • 每种颜色的 alpha 值和强度:将颜色的透明度定义为 0.0-1.0 或 0-255 范围内的双倍值。
  • 混合模式,由enum BlendMode值定义:取决于每种颜色的模式、不透明度和 alpha 值,结果也可能取决于将图像添加到场景的顺序;第一个添加的图像称为底部输入,而重叠图像中的第二个称为顶部输入;如果顶部输入完全不透明,则底部输入被顶部输入隐藏。

重叠区域的结果外观是基于不透明度、颜色的 alpha 值、颜色的数值(强度)和混合模式计算的,混合模式可以是以下之一:

  • ADD:顶部输入的颜色和 alpha 分量与底部输入的颜色和 alpha 分量相加
  • BLUE:将底部输入的蓝色分量替换为顶部输入的蓝色分量;其他颜色分量不受影响
  • COLOR_BURN:将底部输入颜色分量的倒数除以顶部输入颜色分量,然后全部倒数以产生结果颜色
  • COLOR_DODGE:将底部输入颜色分量除以顶部输入颜色分量的倒数,得到结果颜色
  • DARKEN:选择来自两个输入的颜色分量中较暗的部分来产生结果颜色
  • DIFFERENCE:将两个输入的颜色分量中较深的分量从较浅的分量中减去,得到结果颜色
  • EXCLUSION:将两个输入的颜色分量相乘并加倍,然后从底部输入的颜色分量之和中减去,得到结果颜色
  • GREEN:将底部输入的绿色分量替换为顶部输入的绿色分量;其他颜色分量不受影响
  • HARD_LIGHT:根据顶部的输入颜色,输入颜色分量可以是相乘的,也可以是过滤的
  • LIGHTEN:从两个输入中选择颜色分量中的较浅者来产生结果颜色
  • MULTIPLY:第一次输入的颜色分量与第二次输入的颜色分量相乘
  • OVERLAY:根据底部的输入颜色,输入颜色分量可以是相乘的,也可以是过滤的
  • RED:将底部输入的红色分量替换为顶部输入的红色分量;其他颜色分量不受影响
  • SCREEN:来自两个输入的颜色分量被反转,彼此相乘,并且该结果再次被反转以产生结果颜色
  • SOFT_LIGHT:根据顶部的输入颜色,输入颜色组件要么变暗,要么变亮
  • SRC_ATOP:顶部输入位于底部输入内部的部分与底部输入混合
  • SRC_OVER:顶部输入与底部输入混合

为了演示Blend效果,让我们创建另一个名为BlendEffect的应用。它不需要com.sun.*包,因此不需要--add-exportVM 选项。编译和执行时只需设置--module-path--add-modules选项,如“JavaFX 基础”一节所述

本书的范围不允许我们演示所有可能的组合,因此我们将创建一个红色圆圈和一个蓝色正方形:

Circle createCircle(){
    Circle c = new Circle();
    c.setFill(Color.rgb(255, 0, 0, 0.5));
    c.setRadius(25);
    return c;
}

Rectangle createSquare(){
    Rectangle r = new Rectangle();
    r.setFill(Color.rgb(0, 0, 255, 1.0));
    r.setWidth(50);
    r.setHeight(50);
    return r;
}

我们使用Color.rgb(int red, int green, int blue, double alpha)方法来定义每个图形的颜色。但是还有很多方法可以做到。阅读Color类 API 文档了解更多详细信息

为了重叠创建的圆和正方形,我们将使用Group节点:

Node c = createCircle();
Node s = createSquare();
Node g = new Group(s, c);

在前面的代码中,正方形是底部输入。我们还将创建一个组,其中正方形是顶部输入:

Node c = createCircle();
Node s = createSquare();
Node g = new Group(c, s);

区别很重要,因为我们将圆定义为半不透明,而正方形是完全不透明的。我们将在所有示例中使用相同的设置

让我们比较两种模式MULTIPLYSRC_OVER。我们将使用setEffect()方法将它们设置在组上,如下所示:

Blend blnd = new Blend();
blnd.setMode(BlendMode.MULTIPLY);
Node c = createCircle();
Node s = createSquare();
Node g = new Group(s, c);
g.setEffect(blnd);

对于每个模式,我们创建两个组,一个顶部输入一个圆,另一个顶部输入一个正方形,然后我们将创建的四个组放置在一个GridPane布局中(详细信息请参见源代码)。如果我们运行BlendEffect应用,结果将是:

正如所料,当正方形位于顶部(右边的两个图像)时,重叠区域完全由不透明的正方形拍摄。但是,当圆是顶部输入(左边的两个图像)时,重叠区域在某种程度上是可见的,并基于混合效果进行计算。

但是,如果我们直接在组上设置相同的模式,结果会略有不同。让我们运行相同的代码,但在组上设置模式:

Node c = createCircle();
Node s = createSquare();
Node g = new Group(c, s);
g.setBlendMode(BlendMode.MULTIPLY);

如果再次运行应用,结果如下所示:

如您所见,圆圈的红色稍有变化,MULTIPLYSRC_OVER模式之间没有区别。这就是我们在本节开头提到的场景中添加节点的顺序的问题。

结果也会根据设置效果的节点而变化。例如,与其在组上设置效果,不如仅在圆上设置混合效果:

Blend blnd = new Blend();
blnd.setMode(BlendMode.MULTIPLY);
Node c = createCircle();
Node s = createSquare();
c.setEffect(blnd);
Node g = new Group(s, c);

我们运行应用并看到以下内容:

右侧的两个图像与前面所有示例中的图像相同,但左侧的两个图像显示了重叠区域的新颜色。现在让我们在正方形而不是圆形上设置相同的混合效果,如下所示:

Blend blnd = new Blend();
blnd.setMode(BlendMode.MULTIPLY);
Node c = createCircle();
Node s = createSquare();
s.setEffect(blnd);
Node g = new Group(s, c);

结果将再次发生轻微变化,并显示在以下屏幕截图上:

MULTIPLYSRC_OVER模式之间没有区别,但是红色与我们在圆上设置效果时的颜色不同

我们可以再次更改方法,并使用以下代码直接在圆上设置混合效果模式:

Node c = createCircle();
Node s = createSquare();
c.setBlendMode(BlendMode.MULTIPLY);

结果再次发生变化:

在正方形上设置混合模式只会再次消除MULTIPLYSRC_OVER模式之间的差异:

为了避免混淆并使混合的结果更可预测,必须观察节点添加到场景的顺序以及应用混合效果的方式的一致性。

在随书提供的源代码中,您将找到javafx.scene.effects包中包含的所有效果的示例。它们都是通过并排比较来证明的。下面是一个例子:

为方便起见,提供了“暂停”和“继续”按钮,允许您暂停演示并查看混合效果上设置的不同不透明度值的结果。

为了演示所有其他效果,我们创建了另一个名为OtherEffects的应用,它也不需要com.sun.*包,因此不需要--add-exportVM 选项。演示的效果包括BloomBoxBlurColorAdjustDisplacementMapDropShadowGlowInnerShadowLightingMotionBlurPerspectiveTransformReflectionShadowToneSepiaTone。我们使用了两个图像来展示应用每种效果的结果,即 Packt 徽标和山湖景观:

FileInputStream inputP = 
                   new FileInputStream("src/main/resources/packt.png");
Image imageP = new Image(inputP);
ImageView ivP = new ImageView(imageP);

FileInputStream inputM = 
                  new FileInputStream("src/main/resources/mount.jpeg");
Image imageM = new Image(inputM);
ImageView ivM = new ImageView(imageM);
ivM.setPreserveRatio(true);
ivM.setFitWidth(300);

我们还添加了两个按钮,允许您暂停并继续演示(它会迭代效果及其参数值):

Button btnP = new Button("Pause");
btnP.setOnAction(e1 -> et.pause());
btnP.setStyle("-fx-background-color: lightpink;");

Button btnC = new Button("Continue");
btnC.setOnAction(e2 -> et.cont());
btnC.setStyle("-fx-background-color: lightgreen;");

et对象是EffectsThread线程的对象:

EffectsThread et = new EffectsThread(txt, ivM, ivP);

线程遍历效果列表,创建相应的效果 10 次(使用 10 个不同的效果参数值),每次在每个图像上设置创建的Effect对象,然后休眠一秒钟,让您有机会查看结果:

public void run(){
    try {
        for(String effect: effects){
            for(int i = 0; i < 11; i++){
                double d = Math.round(i * 0.1 * 10.0) / 10.0;
                Effect e = createEffect(effect, d, txt);
                ivM.setEffect(e);
                ivP.setEffect(e);
                TimeUnit.SECONDS.sleep(1);
                if(pause){
                    while(true){
                        TimeUnit.SECONDS.sleep(1);
                        if(!pause){
                            break;
                        }
                    }
                }
            }
        }
        Platform.exit();
    } catch (Exception ex){
        ex.printStackTrace();
    }
}

接下来,我们将在带有效果结果的屏幕截图下展示如何创建每个效果。为了呈现结果,我们使用了GridPane布局:

GridPane grid = new GridPane();
grid.setAlignment(Pos.CENTER);
grid.setVgap(25);
grid.setPadding(new Insets(10, 10, 10, 10));

int i = 0;
grid.add(txt,    0, i++, 2, 1);
GridPane.setHalignment(txt, HPos.CENTER);
grid.add(ivP,    0, i++, 2, 1);
GridPane.setHalignment(ivP, HPos.CENTER);
grid.add(ivM,    0, i++, 2, 1);
GridPane.setHalignment(ivM, HPos.CENTER);
grid.addRow(i++, new Text());
HBox hb = new HBox(btnP, btnC);
hb.setAlignment(Pos.CENTER);
hb.setSpacing(25);
grid.add(hb,    0, i++, 2, 1);
GridPane.setHalignment(hb, HPos.CENTER);

最后,创建的GridPane对象被传递到场景中,场景又被放置在您熟悉的舞台上,这些舞台来自我们前面的示例:

Scene scene = new Scene(grid, 450, 500);
primaryStage.setScene(scene);
primaryStage.setTitle("JavaFX effect demo");
primaryStage.onCloseRequestProperty()
            .setValue(e3 -> System.out.println("Bye! See you later!"));
primaryStage.show();

下面的屏幕截图描述了 10 个参数值中的 1 个的效果示例。在每个屏幕截图下,我们展示了创建此效果的createEffect(String effect, double d, Text txt)方法的代码片段:

//double d = 0.9;
txt.setText(effect + ".threshold: " + d);
Bloom b = new Bloom();
b.setThreshold(d);

// double d = 0.3;
int i = (int) d * 10;
int it = i / 3;
txt.setText(effect + ".iterations: " + it);
BoxBlur bb = new BoxBlur();
bb.setIterations(i);

double c = Math.round((-1.0 + d * 2) * 10.0) / 10.0;      // 0.6
txt.setText(effect + ": " + c);
ColorAdjust ca = new ColorAdjust();
ca.setContrast(c);

double h = Math.round((-1.0 + d * 2) * 10.0) / 10.0;     // 0.6
txt.setText(effect + ": " + h);
ColorAdjust ca1 = new ColorAdjust();
ca1.setHue(h);

double st = Math.round((-1.0 + d * 2) * 10.0) / 10.0;    // 0.6
txt.setText(effect + ": " + st);
ColorAdjust ca3 = new ColorAdjust();
ca3.setSaturation(st);

int w = (int)Math.round(4096 * d);  //819
int h1 = (int)Math.round(4096 * d); //819
txt.setText(effect + ": " + ": width: " + w + ", height: " + h1);
DisplacementMap dm = new DisplacementMap();
FloatMap floatMap = new FloatMap();
floatMap.setWidth(w);
floatMap.setHeight(h1);
for (int k = 0; k < w; k++) {
    double v = (Math.sin(k / 20.0 * Math.PI) - 0.5) / 40.0;
    for (int j = 0; j < h1; j++) {
        floatMap.setSamples(k, j, 0.0f, (float) v);
    }
}
dm.setMapData(floatMap);

double rd = Math.round((127.0 * d) * 10.0) / 10.0; // 127.0
System.out.println(effect + ": " + rd);
txt.setText(effect + ": " + rd);
DropShadow sh = new DropShadow();
sh.setRadius(rd);

double rad = Math.round(12.1 * d *10.0)/10.0;      // 9.7
double off = Math.round(15.0 * d *10.0)/10.0;      // 12.0
txt.setText("InnerShadow: radius: " + rad + ", offset:" + off);
InnerShadow is = new InnerShadow();
is.setColor(Color.web("0x3b596d"));
is.setOffsetX(off);
is.setOffsetY(off);
is.setRadius(rad);

double sS = Math.round((d * 4)*10.0)/10.0;      // 0.4
txt.setText(effect + ": " + sS);
Light.Spot lightSs = new Light.Spot();
lightSs.setX(150);
lightSs.setY(100);
lightSs.setZ(80);
lightSs.setPointsAtX(0);
lightSs.setPointsAtY(0);
lightSs.setPointsAtZ(-50);
lightSs.setSpecularExponent(sS);
Lighting lSs = new Lighting();
lSs.setLight(lightSs);
lSs.setSurfaceScale(5.0);

double r = Math.round((63.0 * d)*10.0) / 10.0;      // 31.5
txt.setText(effect + ": " + r);
MotionBlur mb1 = new MotionBlur();
mb1.setRadius(r);
mb1.setAngle(-15);

// double d = 0.9;
txt.setText(effect + ": " + d); 
PerspectiveTransform pt =
        new PerspectiveTransform(0., 1\. + 50.*d, 310., 50\. - 50.*d,
                   310., 50\. + 50.*d + 1., 0., 100\. - 50\. * d + 2.);

// double d = 0.6;
txt.setText(effect + ": " + d);
Reflection ref = new Reflection();
ref.setFraction(d);

// double d = 1.0;
txt.setText(effect + ": " + d);
SepiaTone sep = new SepiaTone();
sep.setLevel(d);

本书提供了此演示的完整源代码,可以在 GitHub 中获得。

总结

在本章中,读者将了解 JavaFX 工具包、它的主要特性以及如何使用它创建 GUI 应用。涵盖的主题包括 JavaGUI 技术概述、JavaFX 控制元素、图表、使用 CSS、FXML、嵌入 HTML、播放媒体和添加效果。

下一章专门讨论函数式编程。它概述了 JDK 附带的函数式接口,解释了 Lambda 表达式是什么,以及如何在 Lambda 表达式中使用函数式接口。它还解释和演示了如何使用方法引用。

测验

  1. JavaFX 中的顶级内容容器是什么?
  2. JavaFX 中所有场景参与者的基类是什么?
  3. 说出 JavaFX 应用的基类。
  4. JavaFX 应用必须实现的一种方法是什么?
  5. main方法必须调用哪个Application方法来执行 JavaFX 应用?
  6. 执行 JavaFX 应用需要哪两个 VM 选项?
  7. 当使用上角的 x 按钮关闭 JavaFX 应用窗口时,调用哪个Application方法?
  8. 必须使用哪个类来嵌入 HTML?
  9. 说出三个必须用来播放媒体的类
  10. 要播放媒体,需要添加什么虚拟机选项?
  11. 说出五个 JavaFX 效果。