第 2 章:JavaFX 体验





5.00/5 (1投票)
正如序言所暗示,JavaFX 具有一系列独特的功能组合。本章将让您领略该语言及其部分功能的魅力。
![]() |
Gail Anderson 和 Paul Anderson 由 Prentice Hall 出版 ISBN-10:0-13-704279-5 ISBN-13:978-0-13-704279-1 |
本章摘自 Gail Anderson 和 Paul Anderson 撰写,Prentice Hall Professional 于 2009 年 6 月出版的《Essential JavaFX》一书,ISBN 0137042795,版权归 Pearson Education, Inc. 所有。欲了解更多信息,请访问:http://www.informit.com/store/product.aspx?isbn=0137042795 Safari Books Online 订阅者可在此处访问该书:http://safari.informit.com/”
正如序言所暗示,JavaFX 具有一系列使其独特的功能组合。本章将让您领略该语言及其部分功能的魅力。我们的目标是选择一个具有代表性的示例,让您了解 JavaFX 可能实现的程序类型。该示例(吉他调音器)在保持讨论具体性的同时,也说明了语言结构。我们有时会偏离示例,以说明其他相关的 JavaFX 功能。虽然本概述绝不完整(请记住,这只是一个初步的体验),但我们希望吸引您进一步探索 JavaFX。
GuitarTuner 的源代码出现在本章末尾(参见“GuitarTuner 项目源代码”)。为了保持文本的流畅性,我们将在整个概述中展示该应用程序的片段。
你将学到什么
- JavaFX 作为脚本语言的独特之处
- 关于对象字面量和声明式构造
- JavaFX 场景图简介
- 声明变量、属性和对象
- 初始化对象和对象属性
- 容器坐标空间和布局基础
- 创建自定义节点
- 用颜色、效果和渐变操作对象
- 通过绑定、事件处理程序和动画实现功能
2.1 JavaFX 简介
什么是 JavaFX?JavaFX 是一种静态类型脚本语言。您可以根据需要从 JavaFX 调用 Java API 并使用类创建新的对象类型,但 JavaFX 也提供了一种简单的声明式语法。(声明式意味着您说出您想要什么,系统会为您找出如何实现。)JavaFX 提供用于在 2D 坐标系中操作对象、指定填充和描边颜色以及创建特殊效果的属性。您可以创建形状和线条、操作图像、播放视频和声音以及定义动画。
让我们通过介绍基础知识来探索 JavaFX。我们的介绍从 GuitarTuner 项目开始,您将看到 JavaFX 程序的主要结构。然后,您将探索一些 JavaFX 语言构造,并了解如何改进应用程序的外观。最后,您将了解如何使应用程序执行操作。
JavaFX 简介 - JavaFX 是静态类型的,这意味着程序数据类型在编译时已知。JavaFX 还使用类型推断。这意味着您不必声明每个变量的类型,因为 JavaFX 通常可以为您推断出来。这使得 JavaFX 既具有静态类型语言的效率,又具有声明式语言的易用性。
2.2 GuitarTuner 项目
GuitarTuner 项目可帮助您调音吉他。它显示一个带有六根弦的吉他指板视觉效果。指板旁边会出现对应吉他弦的字母(音符)。当您用鼠标点击琴弦时,您会听到所选琴弦合成的吉他音符,并伴随视觉振动。GuitarTuner 项目使用 Java 的 javax.sound.midi API 来生成声音。图 2.1 显示了 A 弦振动时该应用程序的运行情况。相应的 JavaFX 图形对象已标注。
JavaFX 应用程序 GuitarTuner
场景图元模型
带有图形用户界面的 JavaFX 程序在一个*舞台 (stage)*中定义一个*场景 (scene)*。舞台代表所有 JavaFX 对象的顶级容器;即小程序的內容区域或小部件的框架。JavaFX 中用于指定图形和用户交互的中心元模型是*场景图 (scene graph)*。场景定义了一个包含场景所有组件的分层节点结构。*节点 (nodes)*由图形对象表示,例如几何形状(圆形、矩形)、文本、UI 控件、图像查看器、视频查看器和用户创建的对象(例如我们示例中的 GuitarString)。节点也可以是容器,它们又可以容纳更多的节点,让您可以将节点分组到分层结构中。(例如,Group 是一个通用容器节点,HBox 提供水平布局对齐,VBox 提供垂直布局对齐。)场景图就是这种分层节点结构。
图 2.2 显示了 GuitarTuner 项目的场景图。将图 2.1 中的视觉图形元素与图 2.2 中描绘的场景图进行比较。
GuitarTunerScene 的嵌套场景图
通常,要构建一个 JavaFX 应用程序,您需要构建场景图,指定其所有节点的外观和行为。然后,您的应用程序就可以“运行”了。有些应用程序需要输入才能运行——用户操作会激活动画或影响组件属性。其他应用程序则自行运行。(构建场景图类似于给玩具上发条。完成后,应用程序就会运行。)
JavaFX 场景图 - 场景图的强大之处在于,您不仅可以将应用程序的整个结构捕获到一个数据结构中,而且只需修改场景图中对象的属性即可更改显示。(例如,如果您将节点的可见属性更改为 false,则该节点及其包含的任何节点都会消失。如果您更改节点的位置,它就会移动。)
在 GuitarTuner 项目的场景图中,您会看到顶层的 Scene,其中包含一个 Group。在 Group 中,有一个用于指板(吉他琴颈)的 Rectangle,两个代表品丝的 Line 节点,以及六个 GuitarString。每个 GuitarString 又是一个独立的 Group,由三个 Rectangle 和一个 Text 节点组成。包含其他节点的节点(如 Scene 和 Group)包含一个用于保存子节点的 content 属性。场景图的层次结构意味着同一级别的所有节点共享相同的坐标空间。因此,您构建的节点结构(如 GuitarString)使用相对坐标系。您很快就会明白这为什么有用。
像设计师一样思考 - JavaFX 鼓励您像设计师一样思考。作为第一步,请可视化您的应用程序或小部件的结构,并使用简单的形状和其他构建块来组合您的场景。
父容器中节点的顺序会影响它们的渲染。也就是说,容器中的第一个节点最先“绘制”。最后一个节点最后“绘制”,并位于视图顶部。节点(取决于它们在坐标系中的位置)可能会在视觉上遮挡或“裁剪”先前绘制的节点。在 GuitarTuner 中,节点必须按特定顺序排列。您首先绘制指板,然后是品丝,最后是吉他弦,吉他弦会出现在顶部。
更改容器中节点的相对顺序很简单。toFront() 函数将节点移到前面(顶部),toBack() 函数将节点发送到后面(底部)。
分层场景图
图 2.3 也显示了 GuitarTuner 项目的场景图。图 2.2 和图 2.3 描绘了相同的结构,但图 2.3 使用图形树视图显示了场景中节点之间的层次关系。同一级别的节点共享相同的坐标空间。例如,GuitarString 中的三个 Rectangle 和 Text 节点共享相同的坐标系。
GuitarTunerRectangle 项目的场景节点图
2.3 JavaFX 程序结构
JavaFX 程序结构很简单。对于习惯于传统编译程序的程序员来说,用 JavaFX 编程会感觉有所不同。通过静态类型,当您错误地使用类型时,JavaFX 会在编译时给您反馈。这极大地提高了您编写正确代码的能力。此外,借助 NetBeans IDE,您可以访问所有 JavaFX 类型(类)的 JavaDocs,并动态查询这些类的属性和函数,本质上在编辑时就能获得反馈。
让我们看看 Stage 和 Scene 如何构成 JavaFX 程序结构。
舞台与场景
Stage 是顶级容器,包含 Scene。Scene 又包含构成场景图的节点。每个包含图形对象的 JavaFX 程序都声明一个 Stage 对象。
这是 GuitarTuner 场景图的顶层实现,来自图 2.2(或图 2.3)。 (我们稍后将查看 GuitarString 的节点图。)
// Stage and Scene Graph
Stage {
title: "Guitar Tuner"
Scene {
// content is sequence of SceneGraph nodes
content: [
Group {
content: [
Rectangle { ... }
Line { ... }
Line { ... }
GuitarString { ... }
GuitarString { ... }
GuitarString { ... }
GuitarString { ... }
GuitarString { ... }
GuitarString { ... }
]
} // Group
]
} // Scene
} // Stage
对象字面量
舞台(Stage)和场景(Scene)对象通过对象字面量表达式或*对象字面量(object literals)*进行实例化。对象字面量提供了一种声明式编程风格。直观地说,声明式意味着“告诉我你想要什么,而不是如何去做”。正如您将看到的,JavaFX 真正的声明式部分是*绑定(binding)*。我们将在本章后面展示它为何如此强大。
对象字面量需要一个对象(或类)类型(例如 Stage 或 Scene),后跟花括号 { }。您需要初始化的任何属性都出现在花括号内。(Stage 有一个 title 属性,Scene 和 Group 都有 content 属性。)每个属性都有一个名称,后跟一个冒号 : 和该属性的初始值。您可以使用逗号、换行符或空格分隔属性。例如,这里是一个初始化 Rectangle 的对象字面量(属性 x 和 y 表示左上角原点)。
Rectangle { x: 10, y: 20, height: 15, width: 150 }
上述 Stage、Scene 和 Group 对象是用对象字面量定义的。请注意,Scene 对象嵌套在 Stage 对象内部。同样,Group 嵌套在 Scene 内部。方括号 [ ] 定义 Scene 或 Group 中 content 属性的项目序列。在这里,Scene 对象的 content 属性是 Scene 所有顶级节点的序列。在 GuitarTuner 应用程序中,这是一个 Group 节点(参见图 2.2 或图 2.3)。Group 节点同样包含一个 content 属性,其中包含所有子节点(Rectangle、Line 和自定义的 GuitarString)。您如何嵌套这些节点决定了场景图的结构。
这是 GuitarString 从其场景图图 2.2(和图 2.3)中的顶层实现。
// GuitarString - defined as custom class
Group {
content: [
Rectangle { ... }
Rectangle { ... }
Rectangle { ... }
Text { ... }
]
} // Group
GuitarString 由一个 Group 节点组成,其 content 属性定义了一个包含三个矩形和一个 Text 对象的序列。您将在稍后了解它如何融入 GuitarTuner 应用程序。
2.4 JavaFX 关键特性
GuitarTuner 是一个相当典型的 JavaFX 示例应用程序。它具有图形表示,并通过改变其一些视觉属性(以及产生吉他声音)来响应用户输入。让我们看看它使用的一些 JavaFX 关键特性,以便您对该语言有一个全面的了解。
JavaFX 标志性功能 - 任何 JavaFX 关键功能列表都应包括绑定、节点事件处理程序和动画。我们将在各自的章节中讨论这些重要的构造(参见“执行操作”)。
类型推断
JavaFX 提供 def 用于只读变量,var 用于可修改变量。
def numberFrets = 2; // read-only Integer
var x = 27.5; // variable Number
var y: Number; // default value is 0.0
var s: String; // default value is ""
编译器从您分配给变量的值中*推断类型*。只读变量 numberFrets 具有推断类型 Integer;变量 x 具有推断类型 Number (Float)。这意味着您不必在任何地方指定类型(当需要类型时,编译器会告诉您)。
字符串
JavaFX 支持动态字符串构建。String 表达式中的花括号 { } 会评估为包含变量的内容。您可以通过连接这些 String 表达式和 String 字面量来构建 String。例如,以下片段打印 "Greetings, John Doe!"。
def s1 = "John Doe";
println("Greetings, {s1}!"); // Greetings, John Doe!
Shapes
JavaFX 具有众多形状,可帮助您创建场景图节点。有用于创建线条的形状(Line、CubicCurve、QuadCurve、PolyLine、Path)和用于创建几何图形的形状(Arc、Circle、Ellipse、Rectangle、Polygon)。GuitarTuner 应用程序只使用 Rectangle 和 Line,但您将在本书中看到其他形状示例。
让我们来看看形状 Rectangle 和 Circle。它们都是标准的 JavaFX 形状,扩展了 Shape 类(在 javafx.scene.shape 包中)。您通过指定其 radius、centerX 和 centerY 属性的值来定义一个 Circle。对于 Rectangle,您指定 height、width、x 和 y 属性的值。
形状共享几个共同的属性,包括 fill 属性(类型为 Paint,用于填充形状内部)、stroke 属性(类型为 Paint,用于提供形状轮廓)和 strokeWidth 属性(一个表示轮廓宽度的 Integer)。
例如,这是一个圆形,中心点位于 (50,50),半径为 30,颜色为 Color.RED。
Circle {
radius: 30
centerX: 50
centerY: 50
fill: Color.RED
}
这是一个矩形,左上角位于点 (30, 100),高 30,宽 80,颜色为 Color.BLUE。
Rectangle {
x: 30, y: 100
height: 30, width: 80
fill: Color.BLUE
}
所有形状也是节点 (javafx.scene.Node)。Node 是一个至关重要的类,它为节点元素提供局部几何体,提供指定变换(如平移、旋转、缩放或剪切)的属性,以及指定鼠标和键盘事件功能的属性。节点还具有允许您分配 CSS 样式以指定渲染的属性。[1] 我们将在第 4 章详细讨论图形对象。
序列
序列允许您定义一个可以按顺序访问的对象集合。您必须声明序列将保存的对象类型或提供值,以便推断其类型。例如,以下语句定义了 GuitarString 和 Rectangle 对象的序列变量。
var guitarStrings: GuitarString[];
var rectangleSequence: Rectangle[];
这些语句使用 def 创建只读序列。这里,序列 noteValues 具有推断类型 Integer[];序列 guitarNotes 具有推断类型 String[]。
def noteValues = [ 40,45,50,55,59,64 ];
def guitarNotes = [ "E","A","D","G","B","E" ];
序列具有专门的运算符和语法。每当您需要跟踪同种对象类型的多个项目时,您都将在 JavaFX 中使用序列。GuitarTuner 应用程序使用序列和 for 循环来构建多个 Line 对象(品丝)和 GuitarString 对象。
// Build Frets
for (i in [0..<numberFrets])
Line { . . . }
// Build Strings
for (i in [0..<numberStrings])
GuitarString { . . . }
符号 [0..<n] 是一个序列字面量,定义了一个从 0 到 n-1(包括 0 和 n-1)的数字范围。
您可以轻松声明和填充序列。以下*声明式方法*将矩形插入名为 rectangleSequence 的序列中,垂直堆叠六个矩形。
def rectangleSequence = for (i in [0..5])
Rectangle {
x: 20
y: i * 30
height: 20
width: 40
}
您还可以使用 insert 运算符将数值或对象插入现有序列。以下*命令式方法*将六个矩形插入名为 varRectangleSequence 的序列中。
var varRectangleSequence: Rectangle[];
for (i in [0..5])
insert Rectangle {
x: 20
y: i * 30
height: 20
width: 40
} into varRectangleSequence;
JavaFX 提示 - 声明式方法(使用 rectangleSequence)总是首选(如果可能)。通过使用 def 而不是 var,并声明序列而不是将对象插入其中,类型推断更有可能帮助您,并且编译器可以更有效地优化代码。
您将在本书中看到更多序列类型的使用。
调用 Java API
您可以在 JavaFX 程序中调用任何 Java API 方法,而无需做任何特殊处理。GuitarString 节点通过调用 Java 类 SingleNote 中找到的 noteOn 函数来“演奏音符”。以下是 GuitarString 函数 playNote,它调用 SingleNote 成员函数 noteOn。
function playNote(): Void {
synthNote.noteOn(note); // nothing special to call Java methods
vibrateOn();
}
SingleNote 类使用 Java 的 javax.sound.midi 包生成具有特定值(60 代表“中央 C”)的合成音符。Java 类 SingleNote 是 GuitarTuner 项目的一部分。
扩展 CustomNode
JavaFX 为开发人员提供了用户定义类、重写虚函数和抽象基类等面向对象特性(也有“mixin”继承)。GuitarTuner 使用带有子类 GuitarString 继承自 JavaFX 类 CustomNode 的类层次结构,如图 2.4 所示。
GuitarString 类层次结构 GuitarString
这种方法允许您构建自己的图形对象。为了使自定义对象无缝地融入 JavaFX 场景图,您需要将其行为基于 JavaFX 提供的一个特殊类,即 CustomNode。CustomNode 类是一个场景图节点(一种 Node 类型,前面已讨论),它允许您指定从它 扩展 的新类。就像 Java 一样,“extends”是 JavaFX 语言中创建继承关系的构造。这里,GuitarString 扩展(继承自)CustomNode。然后,您为 GuitarString 对象提供所需的额外结构和行为,并重写 CustomNode 要求的任何函数。JavaFX 类构造在第 3 章(参见“类和对象”)中进行了更详细的讨论。
以下是 GuitarTuner 的 GuitarString 类的一些代码。create 函数返回一个定义 GuitarString 的 Group 场景图的 Node。(此场景图与图 2.2 和图 2.3 中的节点结构匹配。第 38 页的清单 2.2 更详细地展示了 create 函数。)
public class GuitarString extends CustomNode {
// properties, variables, functions
. . .
protected override function create(): Node {
return Group {
content: [
Rectangle { ... }
Rectangle { ... }
Rectangle { ... }
Text { ... }
]
} // Group
}
} // GuitarString
几何系统
在 JavaFX 中,节点在二维坐标系中定位,原点位于左上角。x 值水平向右增加,y 值垂直向下增加。坐标系始终相对于父容器。
布局/组
布局组件指定您希望对象相对于其他对象如何绘制。例如,布局组件 HBox(水平框)将其子节点均匀地分布在一行中。布局组件 VBox(垂直框)将其子节点均匀地分布在一列中。其他布局选项包括 Flow、Tile 和 Stack(参见“布局组件”)。您可以根据需要嵌套布局组件。
将节点分组为一个单一实体,可以直观地控制事件处理、动画、组级属性以及整个组的布局。每个组(或布局节点)都定义一个供其所有子节点使用的坐标系。在 GuitarTuner 中,场景图中的顶层节点是一个 Group,它在场景中垂直居中。所有子节点都相对于顶层 Group 中的原点 (0,0) 绘制。因此,将 Group 居中会使其内容作为一个整体居中。
相对坐标空间的好处 - 具有相同父节点的节点共享相同的相对坐标空间。这使得子节点的任何坐标空间计算都与父容器的布局问题分开。然后,当您移动父节点时,其下的所有内容都会移动,保持相对位置不变。
JavaFX 脚本构件
定义 Stage 和 Scene 是大多数 JavaFX 应用程序的核心。然而,JavaFX 脚本还可以包含包声明、导入语句、类声明、函数、变量声明、语句和对象字面量表达式。您已经看到了对象字面量表达式如何初始化场景图中的节点。现在让我们简要讨论如何使用这些其他构件。
由于 JavaFX 是静态类型的,您必须使用 import 语句或声明所有非内置类型。您通常会定义一个包,然后指定 import 语句。(我们将在第 3 章讨论如何使用包。请参阅“脚本文件和包”。)以下是 GuitarTuner 的包声明和 import 语句。
package guitartuner;
import javafx.scene.effect.DropShadow;
import javafx.scene.paint.Color;
import javafx.scene.paint.LinearGradient;
. . . more import statements . . .
import javafx.stage.Stage;
import noteplayer.SingleNote;
如果您正在使用 NetBeans,IDE 可以为您生成导入语句(在编辑器窗口中键入 Ctrl+Shift+I)。
您需要脚本级变量来存储数据,以及只读变量 (def) 用于存储不变的值。在 GuitarTuner 中,我们定义了几个只读变量来帮助构建吉他弦,以及一个变量 (singleNote) 来与 Java midi API 进行通信。请注意,noteValues 和 guitarNotes 是 def 序列类型。
def noteValues = [ 40,45,50,55,59,64 ];
def guitarNotes = [ "E","A","D","G","B","E" ];
def numberFrets = 2;
def numberStrings = 6;
var singleNote = SingleNote { };
当您声明一个 Stage 时,您定义了场景图中的嵌套节点。除了仅仅将节点声明为对象字面量表达式,还可以将这些对象字面量赋值给变量。这让您可以在代码中稍后引用它们。(例如,Scene 对象字面量和 Group 对象字面量被赋值给变量,以便计算将组在场景中垂直居中的偏移量。)
var scene: Scene;
var group: Group;
scene: scene = Scene { ... }
group = Group { ... }
您可能还需要执行 JavaFX 脚本语句或定义实用函数。以下是 GuitarTuner 如何让 SingleNote 对象发出“吉他”声音。
singleNote.setInstrument(27); // "Clean Guitar"
一旦您为应用程序设置了 Stage 和场景图,它就可以运行了。[2] 在 GuitarTuner 中,应用程序会等待用户拨动(点击)吉他弦。
2.5 让事物看起来更好
使用 JavaFX 功能增强图形对象的外观将有助于您的应用程序看起来具有专业设计感。以下是一些可以应用的简单添加。
渐变
渐变通过逐渐改变物体填充属性的颜色,为表面和背景增添深度。一般来说,矩形形状使用线性渐变,圆形和椭圆形使用径向渐变。在 GuitarTuner 中,背景是一个线性渐变,从 Color.LIGHTGRAY(顶部)过渡到较深的 Color.GRAY(底部),如图 2.5 所示。吉他指板也使用了线性渐变。
GuitarTuner 应用程序中的渐变
这是 GuitarTuner 中背景场景的 LinearGradient,为 fill 属性定义。请注意,指定渐变是声明式的;您指定所需的外观,系统会根据屏幕分辨率、颜色深度等因素找出如何实现它。
fill: LinearGradient {
startX: 0.0
startY: 0.0
endX: 0.0
endY: 1.0
proportional: true
stops: [
Stop {
offset: 0.0
color: Color.LIGHTGRAY
},
Stop {
offset: 1.0
color: Color.GRAY
}
]
}
背景渐变沿 y 轴改变颜色,沿 x 轴颜色保持不变(属性 startX 和 endX 相同)。属性 stops 是一个 Stop 对象序列,其中包含偏移量和颜色。偏移量是介于 0 和 1 之间(含 0 和 1)的值;每个后续偏移量必须大于前一个偏移量。
属性 proportional 指示起始值和结束值是按比例的(如果为 true,则定义在 [0..1] 之间)还是绝对的(如果为 false,则为绝对坐标)。
径向渐变适用于圆形形状,如图 2.6 所示。这里您看到了三个圆形形状,都带有径向渐变。第一个圆形定义了一个中心位于左下象限(centerX 为 0.25,centerY 为 0.75)的渐变。第二个圆形的渐变居中(centerX 和 centerY 都为 0.5),第三个圆形的渐变出现在右上象限(centerX 为 0.75,centerY 为 0.25)。
径向渐变与圆形形状搭配良好
这是中间圆的径向渐变。
fill: RadialGradient {
centerX: 0.5 // x center of gradient
centerY: 0.5 // y center of gradient
radius: 0.5 // radius of gradient
stops: [
Stop {
offset: 0
color: Color.WHITE
},
Stop {
offset: 1
color: Color.DODGERBLUE
}
]
}
请注意,渐变的大小是圆形的一半(半径为 0.5)。将渐变设置为小于全尺寸,可以让最后一个停止颜色显得更突出(深色占主导地位)。
Color
您可以使用 fill 属性指定形状的颜色。JavaFX 有许多预定义颜色,按字母顺序从 Color.ALICEBLUE 到 Color.YELLOWGREEN。(在 NetBeans IDE 中,当光标在 Color 后面的点处时按 Ctrl+Space 键,即可看到完整列表,如图 2.7 所示。)
使用 NetBeans IDE 探索颜色选择
您还可以使用 Color.rgb(每个 RGB 值范围从 0 到 255)、Color.color(每个 RGB 值范围从 0 到 1)和 Color.web(与传统十六进制三元组对应的字符串)指定任意颜色。可选的最后一个参数设置不透明度,其中 1 为完全不透明,0 为完全半透明。您还可以通过将其填充属性设置为 Color.TRANSPARENT 来使形状透明。
以下是几个颜色设置的示例。每个示例都将不透明度设置为 .5,这允许一些背景颜色透出来。
def c1 = Color.rgb(10, 255, 15, .5); // bright lime green
def c2 = Color.color(0.5, 0.1, 0.1, .5); // dark red
def c3 = Color.web("#546270", .5); // dark blue-gray
基于数字的颜色值(而不是十六进制字符串或预定义颜色)允许您编写函数和动画,以数字方式操作渐变、颜色或不透明度。例如,以下填充属性的 Color.rgb 值来自 for 循环变化的 i 值。循环根据 i 的值产生三种不同的绿色阴影。
def rectangleSequence = for (i in [0..2])
Rectangle {
x: 60 * i
y: 50
height: 50
width: 40
fill: Color.rgb(10 + (i*50), 100 + (i*40), i*50)
}
图 2.8 显示了具有不同填充值的矩形集。
操作基于数字的颜色值
带圆弧的矩形
您可以通过指定 arcWidth 和 arcHeight 属性来软化矩形的角,如图 2.9 所示。第一个矩形具有规则的直角。第二个矩形将 arcHeight 和 arcWidth 设置为 15,第三个矩形将这两个值都设置为 30。以下是第三个矩形的对象字面量。
Rectangle {
x: 180
y: 0
height: 70
width: 60
arcHeight: 30
arcWidth: 30
fill: LinearGradient { . . . }
}
用圆角软化矩形
投影(DropShadow)
您可以指定的众多效果之一是 DropShadow(效果是声明性的)。DropShadow 效果为其节点应用阴影,使节点具有三维外观。在 GuitarTuner 项目中,指板(吉他琴颈)使用默认的投影,如下所示。
effect: DropShadow { }
默认对象字面量提供了具有以下值的投影。
effect: DropShadow {
offsetX: 0.0
offsetY: 0.0
radius: 10.0
color: Color.BLACK
spread: 0.0
}
您可以通过更改 offsetX 和 offsetY 来操作阴影的位置。 offsetY 的负值将阴影设置在对象上方,offsetX 的负值将阴影设置在左侧。offsetX 和 offsetY 的正值分别将阴影放置在右侧和下方。您还可以更改阴影的大小 (radius)、颜色和扩散 (spread)(阴影显得有多“锐利”)。扩散值为 1 表示阴影清晰可见。值为 0 提供“模糊”外观。图 2.10 显示了三个矩形,它们的投影落在矩形的下方和右侧,使用了这些偏移量。
effect: DropShadow {
// shadow appears below and to the right of object
offsetX: 5.0
offsetY: 5.0
}
投影提供三维效果
2.6 执行操作
JavaFX 有三种主要的“做事”构造:绑定、定义事件处理程序的节点属性和动画。这些构造共同为基于用户输入或其他事件修改场景图提供了强大而优雅的解决方案。让我们看看 GuitarTuner 如何使用这些构造来完成任务。
数据绑定。
JavaFX 中的绑定是一种强大的技术,也是指定传统回调事件处理程序的一种简洁替代方案。基本上,绑定允许您使属性或变量依赖于表达式的值。当您更新表达式中的任何“绑定到”对象时,依赖对象会自动更改。例如,假设我们将 area 绑定到 height 和 width,如下所示。
var height = 3.0;
var width = 4.0;
def area = bind height * width; // area = 12.0
width = 2.5; // area = 7.5
height = 4; // area = 10.0
当 height 或 width 发生变化时,area 也会随之变化。一旦您绑定了属性(或变量),您就不能直接更新它。例如,如果您尝试直接更新 area,则会收到编译时错误。
area = 5; // compile time error
如果您将 area 设为 var 并提供绑定表达式,则如果尝试直接更新它,将收到运行时错误。
在 GuitarTuner 中,振动弦在运行时会同时改变其位置(属性 translateY)和厚度(属性 height),以呈现振动效果。这些属性绑定到其他值,这些值控制吉他弦节点的改变方式。
var vibrateH: Number;
var vibrateY: Number;
Rectangle {
x: 0.0
y: yOffset
width: stringLength
height: bind vibrateH // change height when vibrateH changes
fill: stringColor
visible: false
translateY: bind vibrateY // change translateY when vibrateY changes
}
GuitarTuner 还使用 bind 来通过绑定顶层组中的 layoutY 属性使指板垂直居中。
group = Group {
layoutY: bind (scene.height - group.layoutBounds.height) /
2 - group.layoutBounds.minY
. . .
}
节点属性 layoutBounds 提供其内容的边界信息。如果用户调整窗口大小,顶层组将自动在屏幕上垂直居中。绑定有助于减少事件处理代码,因为(例如,此处)您不必编写事件处理程序来检测窗口大小的变化。
绑定是好的 - 绑定适用于许多情况。例如,您可以根据程序状态的变化来改变节点的外观。您可以使组件可见或隐藏。您还可以使用绑定来声明性地指定布局约束。绑定不仅可以减少代码量,而且代码错误更少,更容易维护,并且通常更容易被编译器优化。
鼠标事件
JavaFX 节点具有处理鼠标和键盘事件的属性。这些属性设置为回调函数,当事件触发时系统会调用这些函数。在 GuitarTuner 中,“鼠标检测”矩形具有以下事件处理程序来检测鼠标点击事件。
onMouseClicked: function(evt: MouseEvent): Void {
if (evt.button == MouseButton.PRIMARY) {
// play and vibrate selected “string”
}
}
if 语句在处理事件之前检查主鼠标按钮(通常左键是主按钮)是否被点击。事件处理函数(在下一节中显示)会播放音符并使琴弦振动。
动画
JavaFX 专注于动画。(事实上,我们专门用一整章来讲解动画。请参阅第 7 章,从第 205 页开始。)您可以使用时间线定义动画,然后调用 Timeline 函数 play 或 playFromStart(还有函数 pause 和 stop)。时间线由一系列关键帧对象组成,这些对象定义了时间线中特定时间偏移处的帧。(关键帧是声明性的。您说“这是在关键时刻场景的状态”,然后让系统找出如何渲染受影响的对象。)在每个关键帧中,您指定值、动作或两者。传统上,人们认为动画是移动对象的一种方式。虽然这是真的,但您会发现 JavaFX 允许您对任何可写对象属性进行动画处理。例如,您可以使用动画来淡入、旋转、调整大小,甚至使图像变亮。
图 2.11 显示了一个具有简单动画的程序快照。它使一个圆形在其容器中来回移动。
时间线允许您指定动画
这是使用 KeyFrames 的专用简写符号实现此动画的时间轴。时间轴首先将变量 x 设置为 0。以逐渐、线性的增量,它改变 x,以便在四秒时,其值为 350。现在,它执行反向操作,逐渐改变 x,以便在接下来的四秒内回到 0(autoReverse 为 true)。此操作无限期重复(或直到时间轴停止或暂停)。常量 0s 和 4s 是 Duration 字面量。
var x: Number;
Timeline {
repeatCount: Timeline.INDEFINITE
autoReverse: true
keyFrames: [
at (0s) { x => 0.0 }
at (4s) { x => 350 tween Interpolator.LINEAR }
]
}.play(); // start Timeline
. . .
Circle {
. . .
translateX: bind x
}
JavaFX 关键字 tween 是一个关键帧运算符,它允许您指定变量如何变化。在这里,我们使用 Interpolator.LINEAR 进行线性变化。也就是说,x 不会从 0 跳到 350,而是以线性方式逐渐取值。线性插值平滑地将 Circle 从 0 移动到 350,完成时间线的一次迭代需要四秒。
JavaFX 还有其他插值器。Interpolator DISCRETE 从一个关键帧的值跳到第二个关键帧。Interpolator EASEIN 类似于 LINEAR,但变化率在开始时较慢。类似地,EASEOUT 在结束时较慢,EASEBOTH 在时间线的两端都提供缓动。
要将此动画应用于 Circle 节点,您需要将 Circle 的 translateX 属性绑定到由时间轴操纵的变量 (x)。属性 translateX 表示节点在 x 方向上的变化。
现在让我们检查 GuitarTuner 如何使用动画来使吉他弦振动并播放其音符。每个 GuitarString 对象使用两个矩形来实现其可见行为。一个矩形是静止的、细长的“弦”,表示静态状态下的弦。这个静止的矩形在场景中始终可见。第二个矩形仅在弦“演奏”时可见。这个矩形通过动画(时间线)快速扩展和收缩其高度。这个移动的矩形给用户提供了弦振动的错觉。
为了获得均匀的振动效果,矩形必须在顶部和底部均匀地膨胀和收缩。动画通过将矩形的高度从 1 变为 3,同时通过将其 translateY 属性在 5 和 4 之间变化来保持其垂直居中,从而使弦看起来在振动。当弦被点击时,弦的音符会播放,矩形会在指定的时间内振动。当时间轴停止时,只有静止的矩形是可见的。
让我们首先看看播放音符的时间轴。这个时间轴出现在 GuitarString 节点的事件处理程序中(参见第 38 页清单 2.2 中 GuitarString 的代码)。
onMouseClicked: function(evt: MouseEvent): Void {
if (evt.button == MouseButton.PRIMARY) {
Timeline {
keyFrames: [
KeyFrame {
time: 0s
action: playNote // play note and
// start vibration
}
KeyFrame {
time: 2.8s
action: stopNote // stop playing
// note and
// stop vibration
}
]
}.play(); // start Timeline
}
}
这里,时间轴是在事件处理程序内部定义的对象字面量,通过函数 play 调用。这个时间轴定义了一系列 KeyFrame 对象,其中函数 playNote 在时间偏移量 0 秒时调用,函数 stopNote 在时间偏移量 2.8 秒 (2.8s) 时调用。以下是函数 playNote 和 stopNote。
// play note and start vibration
function playNote(): Void {
synthNote.noteOn(note);
vibrateOn();
}
// stop playing note and stop vibration
function stopNote(): Void {
synthNote.noteOff(note);
vibrateOff();
}
函数 synthNote.noteOn 调用 Java 类 API 来播放吉他弦。函数 vibrateOn 引起弦的振动。
function vibrateOn(): Void {
play.visible = true; // make the vibrating rectangle visible
timeline.play(); // start the vibration timeline
}
这是振动时间轴。
def timeline = Timeline {
repeatCount: Timeline.INDEFINITE
autoReverse: true
keyFrames: [
at (0s) { vibrateH => 1.0 }
at (.01s) { vibrateH => 3.0 tween Interpolator.LINEAR }
at (0s) { vibrateY => 5.0 }
at (.01s) { vibrateY => 4.0 tween Interpolator.LINEAR }
]
};
这个时间线使用前面讨论的关键帧简写符号,并动画了两个变量:vibrateH 和 vibrateY。变量 vibrateH 改变表示振动弦的矩形的高度。变量 vibrateY 改变矩形的垂直位置,以在振动高度变化时保持其居中。
2.7 GuitarTuner 项目源代码
清单 2.1 和清单 2.2 分两部分展示了 GuitarString 类的代码。清单 2.1 包括 GuitarString 类的类声明、函数、类级变量和属性。请注意,有几个变量声明为 public-init。这个 JavaFX 关键字意味着类的用户可以使用对象字面量提供初始值,但这些属性否则是只读的。所有变量的默认可访问性是 script-private,使得其余声明为私有。
对只读变量使用 def,对可修改变量使用 var。GuitarString 类还提供实用函数,用于播放音符 (playNote) 或停止播放音符 (stopNote)。除了声音,吉他弦还会通过 vibrateOn 和 vibrateOff 进行振动。这些函数实现了 GuitarString 类的行为。
清单 2.1 类 GuitarString — 属性、变量和函数
package guitartuner;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.scene.Cursor;
import javafx.scene.CustomNode;
import javafx.scene.Group;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.Node;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import noteplayer.SingleNote;
public class GuitarString extends CustomNode {
// read-only variables
def stringColor = Color.WHITESMOKE;
// "Strings" are oriented sideways, so stringLength is the
// Rectangle width and stringSize is the Rectangle height
def stringLength = 300;
def stringSize = 1;
def stringMouseSize = 15;
def timeline = Timeline {
repeatCount: Timeline.INDEFINITE
autoReverse: true
keyFrames: [
at (0s) { vibrateH => 1.0 }
at (.01s) { vibrateH => 3.0 tween Interpolator.LINEAR }
at (0s) { vibrateY => 5.0 }
at (.01s) { vibrateY => 4.0 tween Interpolator.LINEAR }
]
};
// properties to be initialized
public-init var synthNote: SingleNote;
public-init var note: Integer;
public-init var yOffset: Number;
public-init var noteText: String;
// class variables
var vibrateH: Number;
var vibrateY: Number;
var play: Rectangle;
function vibrateOn(): Void {
play.visible = true;
timeline.play();
}
function vibrateOff(): Void {
play.visible = false;
timeline.stop();
}
function playNote(): Void {
synthNote.noteOn(note);
vibrateOn();
}
function stopNote(): Void {
synthNote.noteOff(note);
vibrateOff();
}
清单 2.2 显示了 GuitarString 类的代码的第二部分。
每个扩展 CustomNode 的类都必须定义一个返回 Node 对象的 create 函数。[3] 通常,您返回的节点将是一个 Group,因为 Group 是最通用的 Node 类型,可以包含子节点。但是,您也可以返回其他 Node 类型,例如 Rectangle (Shape) 或 HBox (horizontal box) 布局节点。
GuitarString 的场景图很有趣,因为它实际上由三个 Rectangle 节点和一个 Text 节点组成。第一个 Rectangle 用于检测鼠标点击,它是完全半透明的(其不透明度为 0)。这个 Rectangle 比吉他弦宽,因此用户可以用鼠标更容易地选择它。有几个属性实现了它的行为:cursor 属性让用户知道弦已被选中,onMouseClicked 属性提供了事件处理代码(播放音符并使弦振动)。
第二个 Rectangle 节点定义了可见的弦。第三个 Rectangle 节点(赋值给变量 play)通过移动和改变其高度来“振动”。这个矩形只有在音符播放时才可见,并提供“拨动”弦的振动效果。移动和高度改变通过动画和绑定实现。Text 节点简单地显示与吉他弦音符相关的字母(E、A、D 等)。
清单 2.2 GuitarString 的场景图
protected override function create(): Node {
return Group {
content: [
// Rectangle to detect mouse events for string plucking
Rectangle {
x: 0
y: yOffset
width: stringLength
height: stringMouseSize
// Rectangle has to be "visible" or scene graph will
// ignore mouse events. Therefore, we make it fully
// translucent (opacity=0) so it is effectively invisible
fill: Color.web("#FFFFF", 0) // translucent
cursor: Cursor.HAND
onMouseClicked: function(evt: MouseEvent): Void {
if (evt.button == MouseButton.PRIMARY){
Timeline {
keyFrames: [
KeyFrame {
time: 0s
action: playNote
}
KeyFrame {
time: 2.8s
action: stopNote
}
] // keyFrames
}.play(); // start Timeline
} // if
}
} // Rectangle
// Rectangle to render the guitar string
Rectangle {
x: 0.0
y: 5 + yOffset
width: stringLength
height: stringSize
fill: stringColor
}
// Special "string" that vibrates by changing its height
// and location
play = Rectangle {
x: 0.0
y: yOffset
width: stringLength
height: bind vibrateH
fill: stringColor
visible: false
translateY: bind vibrateY
}
Text { // Display guitar string note name
x: stringLength + 8
y: 13 + yOffset
font: Font {
size: 20
}
content: noteText
}
]
} // Group
}
} // GuitarString
清单 2.3 显示了 Main.fx 的代码,它是 GuitarTuner 的主程序。
清单 2.3 Main.fx
package guitartuner;
import guitartuner.GuitarString;
import javafx.scene.effect.DropShadow;
import javafx.scene.Group;
import javafx.scene.paint.Color;
import javafx.scene.paint.LinearGradient;
import javafx.scene.paint.Stop;
import javafx.scene.Scene;
import javafx.scene.shape.Line;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import noteplayer.SingleNote;
def noteValues = [ 40,45,50,55,59,64 ]; // numeric value required by midi
def guitarNotes = [ "E","A","D","G","B","E" ];
// guitar note name
def numberFrets = 2;
def numberStrings = 6;
var singleNote = SingleNote{};
singleNote.setInstrument(27); // "Clean Guitar"
var scene: Scene;
var group: Group;
Stage {
title: "Guitar Tuner"
visible: true
scene: scene = Scene {
fill: LinearGradient {
startX: 0.0
startY: 0.0
endX: 0.0
endY: 1.0
proportional: true
stops: [
Stop {
offset: 0.0
color: Color.LIGHTGRAY
},
Stop {
offset: 1.0
color: Color.GRAY
}
]
}
width: 340
height: 200
content: [
group = Group {
// Center the whole group vertically within the scene
layoutY: bind (scene.height - group.layoutBounds.height) /
2 - group.layoutBounds.minY
content: [
Rectangle { // guitar neck (fret board)
effect: DropShadow { }
x: 0
y: 0
width: 300
height: 121
fill: LinearGradient {
startX: 0.0
startY: 0.0
endX: 0.0
endY: 1.0
proportional: true
stops: [
Stop {
offset: 0.0
color: Color.SADDLEBROWN
},
Stop {
offset: 1.0
color: Color.BLACK
}
]
}
} // Rectangle
for (i in [0..<numberFrets]) // two frets
Line {
startX: 100 * (i + 1)
startY: 0
endX: 100 * (i + 1)
endY: 120
stroke: Color.GRAY
}
for (i in [0..<numberStrings]) // six guitar strings
GuitarString {
yOffset: i * 20 + 5
note: noteValues[i]
noteText: guitarNotes[i]
synthNote: singleNote
}
]
}
]
}
}
脚注
[1]层叠样式表 (CSS) 有助于设置网页样式,并让设计人员在整个应用程序、小部件或整个网站中提供统一的外观。您可以使用 CSS 以类似的方式设置 JavaFX 节点的样式。(有关将样式应用于 JavaFX 节点的详细信息,请参阅“层叠样式表 (CSS)”。)
[2]Java 开发者可能会好奇 main() 函数在哪里。事实证明,JavaFX 编译器会为您生成一个 main(),但从开发者的角度来看,您只有一个脚本文件。
[3]嗯,差不多。如果您没有定义 create 函数,那么您必须将类声明为 abstract。钢琴示例(参见“钢琴项目”)使用了一个抽象类。
© Pearson Education 版权所有。保留所有权利。