第 3 章:JavaFX 入门 3





0/5 (0投票)
JavaFX Script 将声明式编程概念与面向对象相结合。它为应用程序提供了高效、灵活且健壮的基础。然而,这种灵活性也带来了开发者的责任。
![]() |
Jim Clarke; Jim Connors; Eric Bruno 由 Prentice Hall 出版 ISBN-10: 0-13-701287-X ISBN-13: 978-0-13-701287-9 |
本章摘录自新书《JavaFX: Developing Rich Internet Applications》,作者为 Jim Clarke、Jim Connors 和 Eric Bruno,由 Addison-Wesley Professional 作为 The Java Series 的一部分出版,ISBN 013701287X,版权所有 © 2009 Sun Microsystems, Inc.。更多信息,请访问:http://www.informit.com/store/product.aspx?isbn=013701287x Safari Books Online 订阅者可以在此处访问本书:http://safari.informit.com/9780137013524
JavaFX Script 基础
JavaFX 部分是一个声明式语言。使用声明式语言,开发者描述需要完成的任务,然后由系统来完成。Olof Torgersson,Chalmers 大学技术学院交互设计硕士项目主任,哥德堡大学副教授,研究声明式编程已超过 10 年。从他对声明式编程方法的分析中,我们找到了这个定义:
“从程序员的角度来看,基本属性是编程被提升到了一个更高的抽象级别。在这个更高的抽象级别上,程序员可以专注于陈述要计算的内容,而不必总是关注如何计算。”[1]
JavaFX Script 将声明式编程概念与面向对象相结合。它为应用程序提供了高效、灵活且健壮的基础。然而,这种灵活性也带来了开发者的责任。JavaFX Script 是一种宽容的语言,并且是声明式的,它假定了固有的规则,这些规则可能会掩盖编程错误。其中最明显的是,null 对象由运行时引擎处理,很少会引起 Java 空指针异常。因此,当在表达式中遇到 null 时,程序会继续执行,并产生一个有效的结果。但是,结果可能并非您所期望的。因此,开发者在编写代码时需要格外谨慎,在测试时需要更加彻底。起初,这可能看起来令人担忧;然而,JavaFX 的易用性和更高的生产力,以及 JavaFX 试图减轻用户体验到崩溃的事实,都弥补了这一点。
JavaFX 作为一种声明式语言的好处之一是,使对象交互的许多“粘合剂”已经在语言中提供了。这使得开发者能够更多地专注于需要显示的内容,而较少关注如何实现。接下来的部分概述了 JavaFX Script 语言,包括语法、运算符和其他特性。
JavaFX Script 语言
如前所述,JavaFX Script 是一种支持面向对象的声明式脚本语言。如果您熟悉 Java、JavaScript、Groovy、Adobe ActionScript 或 JRuby 等其他语言,JavaFX Script 会显得熟悉,但存在显著差异。在支持传统的纯脚本编写的同时,它还支持面向对象提供的封装和重用能力。这使得开发者能够使用 JavaFX 来生产和维护从小到大的应用程序。另一个关键特性是 JavaFX Script 与 Java 无缝集成。
从概念上讲,JavaFX Script 分为两个主要级别:脚本级别和类级别。在脚本级别,可以定义变量和函数。这些变量和函数可以与其他在脚本中定义的类共享,或者如果它们具有更广泛的访问权限,则可以与其他脚本和类共享。此外,还可以创建称为“宽松表达式”的表达式。这些都是在类定义之外声明的表达式。当脚本被评估时,所有宽松表达式都会被评估。
一个简单的脚本,用于在控制台显示 Hello World
println("Hello World");
另一个示例,演示如何计算 3 的阶乘,如列表 3.1 所示。
列表 3.1 3 的阶乘
def START = 3;
var result = START;
var a = result - 1;
while(a > 0) {
result *= a;
a--;
}
println("result = {result}");
开发者须知 - 如果您的脚本具有导出的成员——即任何外部可访问的成员,如 public、protected 和 package,函数或变量——那么所有宽松表达式必须包含在一个 run 函数中。例如,如果我们想让上一个示例中的 result 变量具有 public 可见性,我们就需要创建一个 run 函数。
public var result:Number; function run(args : String[]) : java.lang.Object { var num = if(sizeof args > 0) { java.lang.Integer.valueOf(args[0]); } else { 10; }; result = num; var a = result - 1; while(a > 0) { result *= a; a--; } println("{num}! = {result}"); }run 方法包含一个可选的 String[] 参数,该参数是在脚本运行时传递给脚本的命令行参数序列。
如果您没有导出的成员,仍然可以包含一个 run 方法。但是,run 方法本身被认为是导出的,即使您没有包含访问修饰符。因此,一旦添加了 run 方法,所有宽松导出的表达式现在都必须包含在其中。
除了脚本级别,类定义实例变量和函数,并且在使用前必须先实例化为一个对象。类函数或变量可以访问同一脚本文件中的脚本级别函数或变量,或者如果分配了适当的访问权限,可以访问其他脚本文件中的函数或变量。另一方面,脚本级别的函数只能通过创建类对象来访问类变量和函数,并且仅当类提供适当的访问权限时。访问权限在本章稍后会更详细地讨论。
类声明
要在 JavaFX 中声明一个类,请使用 class 关键字。
public class Title {
}
开发者须知 - 按照惯例,类名的首字母大写。
public 关键字称为访问修饰符,表示该类可以被任何其他类或脚本使用,即使该类是在另一个脚本文件中声明的。如果类没有修饰符,它只能在声明它的脚本文件内访问。例如,列表 3.2 中的 Point 类没有可见性修饰符,因此它仅具有脚本可见性,只能在 ArtWork 脚本中使用。
列表 3.2 Artwork.fx
class Point {// private class only
//visible to the ArtWork class
var x:Number;
var y:Number;
}
public class ArtWork {
var location: Point;
}
开发者须知 - 对于每个 JavaFX 脚本文件,都会生成一个使用该脚本文件名的类,即使没有显式定义。例如,在前面的 ArtWork.fx 示例中,有一个 ArtWork 类。即使我们没有包含 public class ArtWork 声明,这也是真的。
此外,脚本文件中定义的所有其他类的名称前面都会加上脚本文件的名称。例如,在前面的示例中,Point 类被完全限定为 ArtWork.Point。当然,如果 ArtWork 属于某个包,则也会使用包名来限定该名称。例如,com.acme.ArtWork.Point。
要扩展一个类,请使用 extends 关键字,后跟更通用的类名。JavaFX 类最多可以扩展一个 Java 或 JavaFX 类。如果要扩展 Java 类,该类必须有一个默认(无参)构造函数。
public class Porsche911 extends Porsche {
}
JavaFX 可以扩展多个 JavaFX mixin 类或 Java 接口。Mixin 类将在下一节讨论。
一个应用程序可能包含许多类,因此将它们以连贯的方式组织起来称为包非常有用。要声明您的类或脚本应属于某个包,请在脚本文件的开头包含一个 package 声明。以下示例意味着 Title 类属于 com._mycompany.components 包。Title 类的完整名称现在是 com.mycompany.components.Title。每当引用 Title 类时,都必须解析为其完整名称。
package com.mycompany.components;
public class Title {
}
为了更轻松地进行此解析,您可以在源文件的顶部包含一个 import 语句。例如:
import com.mycompany.components.Title;
var productTitle = Title{};
现在, wherever Title is referenced within that script file, it will resolve to com.mycompany.components.Title. You can also use a wildcard import declaration
import com.mycompany.components.*;
使用通配符导入形式,每当您引用 com.mycompany.components 包中的任何类时,它都会解析为完整的名称。以下代码示例显示了类名称的解析方式,并在注释中显示了完全限定的类名。
package com.mycompany.myapplication;
import com.mycompany.components.Title;
// com.mycompany.myapplication.MyClass
public class MyClass {
// com.mycompany.components.Title
public var title: Title;
}
使用 package 关键字而不是 public 关键字,类可以具有包可见性。这意味着该类只能从同一包中的类访问。
package class MyPackageClass {
}
类还可以声明为 abstract,这意味着该类不能直接实例化,而只能通过其子类实例化。抽象类不打算独立存在,而是封装了几个类可能使用的共享状态和函数的一部分。只有抽象类的子类才能实例化,并且通常子类需要填充抽象类中未涉及的那些独特状态或行为。
public abstract class MyAbstractClass {
}
如果一个类声明了一个抽象函数,它本身也必须声明为抽象。
public abstract class AnotherAbstractClass {
public abstract function
setXY(x:Number, y:Number) : Void;
}
Mixin 类
JavaFX 支持一种称为 mixin 继承的继承形式。为了支持这一点,JavaFX 包含一种特殊的类,称为 mixin。Mixin 类是提供某些功能供子类继承的类。它们不能独立实例化。Mixin 类与 Java 接口不同,因为 mixin 可以为其函数提供默认实现,还可以声明和初始化自己的变量。
要在 JavaFX 中声明一个 mixin 类,需要在类声明中包含 mixin 关键字。以下代码显示了这一点。
public mixin class Positioner {
Mixin 类可以包含任意数量的函数声明。如果函数声明有函数体,那么这就是函数的默认实现。例如,以下列表显示了一个用于将一个节点定位在另一个节点内的类的 mixin 类声明。
public mixin class Positioner {
protected bound function centerX(
node: Node, within: Node) : Number {
(within.layoutBounds.width -
node.layoutBounds.width)/2.0 -
node.layoutBounds.minX;
}
protected bound function centerY(node: Node,
within: Node) : Number {
(within.layoutBounds.height -
node.layoutBounds.height)/2.0 -
node.layoutBounds.minY;
}
}
想要实现 mixin 函数自己版本的子类必须在声明函数时使用 override 关键字。例如,以下代码显示了一个子类,它实现了 Positioner mixin 类中 centerX() 函数自己的版本。
public class My Positioner extends Positioner {
public override bound function centerX(node: Node,
within: Node) : Number {
(within.boundsInParent.width -
node.boundsInParent.width )/2.0;
}
}
如果 mixin 函数没有默认实现,则必须将其声明为 abstract,并且子类必须重写该函数以提供实现。例如,以下代码显示了一个添加到 Positioner mixin 类中的抽象函数。
public abstract function bottomY(node: Node,
within: Node, padding: Number) : Number;
子类必须使用 override 关键字实现此函数,如下一个列表所示。
public class My Positioner extends Positioner {
public override function bottomY(node: Node,
within: Node, padding: Number) : Number {
within.layoutBounds.height - padding -
node.layoutBounds.height;
}
}
如果两个 mixin 具有相同的函数签名或变量名,系统将根据在 extends 子句中声明的顺序来解析函数或变量。要指定特定的函数或变量,请使用 mixin 类名后跟函数或变量名。如下代码所示。
public class My Positioner extends Positioner,
AnotherPositioner {
var offset = 10.0;
public override bound function
centerX(node: Node, within: Node) : Number {
Positioner.centerX(node, within) + offset;
}
}
Mixin 还可以定义变量,带或不带默认值和触发器。子类继承这些变量,或者必须重写变量声明。以下列表演示了这一点。
public mixin class Positioner {
public var offset: Number = 10.0;
}
public class My Positioner extends Positioner {
public override var offset = 5.0 on replace {
println("{offset}");
}
}
如果一个类扩展了 JavaFX 类和一个或多个 mixin,JavaFX 类在变量初始化方面优先于 mixin 类。如果变量在超类中声明,则使用超类中指定的默认值;如果超类中未指定默认值,则使用该变量类型的“默认值”。对于 mixin 类,优先级基于它们在 extends 子句中定义的顺序。如果 mixin 中声明的变量有一个默认值,并且该变量在主类中被重写而没有默认值,则使用 mixin 中指定的初始值。
Mixin 还可以具有 init 和 postinit 块。Mixin init 和 postinit 块在超类 init 和 postinit 块之后,在子类 init 和 postinit 块之前运行。Mixin 类中的 init 和 postinit 块按照它们在子类的 extends 子句中声明的顺序运行。
对象字面量
在 JavaFX 中,对象使用对象字面量实例化。这是一种声明式语法,使用您要创建的类的名称,后跟该特定实例的初始化器和定义列表。在列表 3.3 中,一个 Title 类的对象在屏幕位置 10, 50 被创建,文本为“JavaFX is cool”。当鼠标单击时,将调用提供的函数。
列表 3.3 对象字面量
var title = Title {
text: "JavaFX is cool"
x: 10
y: 50
onMouseClicked: function(e:MouseEvent):Void {
// do something
}
};
声明对象字面量时,实例变量可以用逗号或空格分隔,也可以用分号分隔。
您还可以覆盖对象字面量声明中的抽象函数。以下对象字面量(如列表 3.4 所示)创建一个用于 java.awt .event.ActionListener 接口的对象,并重写抽象的 Java 方法 void actionPerformed(ActionEvent e)。
列表 3.4 对象字面量 – 重写抽象函数
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
var listener = ActionListener {
override function
actionPerformed(e: ActionEvent) : Void {
println("Action Performed!");
}
}
变量
JavaFX 支持两种变量:实例变量和脚本变量。脚本变量保存整个脚本的状态,而实例变量保存脚本文件中声明的类的特定实例的状态。
变量基本上有两种类型:不可赋值和可变。不可赋值变量使用 def 关键字声明,并且必须分配一个永不改变的默认值。
public def PI = 3.14;
这些变量不能在对象字面量中赋值、重写或初始化。从某种意义上说,它们可以被视为常量;但是,它们不是“纯”常量,并且可以参与绑定。(有关绑定的更多信息,请参阅第四章,同步数据模型—绑定和触发器。)
考虑以下定义包含对象的不可赋值变量的示例。对象实例不会改变,但这并不意味着该实例的状态不会改变。
def centerPoint = Point{x: 100, y:100};
centerPoint.x = 500;
分配给 centerPoint 的实际 Point 对象保持不变,但该对象实例的状态,即实际的 x 和 y 值,可能会发生变化。但是,当用于绑定时,centerPoint 是常量;如果 centerPoint 的状态发生变化,则会通知绑定上下文已发生更改。
可变实例变量使用 var 关键字声明,并带有可选的默认值。如果省略默认值,则使用合理的默认值;基本上,数字默认为零,布尔值默认为 false,字符串默认为空字符串,序列默认为空序列,其他所有内容默认为 null。
脚本变量声明在任何类声明之外,而实例变量声明在类声明之内。如果脚本变量使用访问修饰符之一(public、protected 或 package)声明,则可以通过引用其完全限定名称从脚本文件外部使用它。此完全限定名称是包名、脚本名和变量名的组合。以下是来自 javafx.scene.Cursor 类的 crosshair 光标的公共脚本变量的完全限定名称。
javafx.scene.Cursor.CROSSHAIR;
实例变量在类声明中声明,并在对象创建时产生。列表 3.5 举例说明了脚本和实例变量的几个示例。
列表 3.5 脚本和实例变量
import javafx.scene.Cursor;
import javafx.scene.paint.Color;
// import of script variable from javafx.scene.Cursor
import javafx.scene.Cursor.CROSSHAIR;
// Unchangeable script variable
def defaultText = "Replace ME"; // Script accessible only
// Changeable script variable
public var instanceCount: Integer; // Public accessible
public class Title {
// Unchangeable instance variables
def defStroke = Color.NAVY; // class only access,
//resolves to javafx.scene.paint.Color.NAVY
// Changeable instance variables
// defaults to the empty String ""
public var text:String;
public var width: Number; // defaults to zero (0.0)
public var height = 100; // Infers Integer type
public var stroke: Color = defaultStroke;
public var strokeWidth = 1.0; // Infers Number type
public var cursor = CROSSHAIR;
//resolves to javafx.scene.Cursor.CROSSHAIR
...
}
您可能已经注意到,有些声明包含类型,有些则没有。当未声明类型时,类型将从第一个赋值推断出来。String、Number、Integer 和 Boolean 是内置类型,其他所有类型都是 JavaFX 或 Java 类。(有一种特殊的语法可以轻松声明将在第七章“使用 JavaFX 动画添加动效”中讨论的 Duration 和 KeyFrame 类实例。)
表 3.1 列出了变量的访问修饰符及其含义和限制。您会注意到对初始化的引用,这指的是对象字面量声明。此外,您还会注意到变量被绑定。这是 JavaFX 的一个关键特性,将在第四章中深入讨论。
表 3.1 访问修饰符
访问修饰符 | 含义 |
var
|
默认访问权限是脚本访问,因此没有访问修饰符,变量只能在脚本内初始化、重写、读取、赋值或绑定。 |
def
|
默认访问权限是脚本访问;定义只能在脚本内读取或绑定。 |
public var
|
任何人都可以读写。此外,它可以从任何地方初始化、重写、读取、赋值或绑定。 |
public def
|
此定义可以从任何地方读取。无论访问权限如何,定义都不能赋值、初始化(在对象字面量中)或重写。它可以从任何地方绑定。 |
public-read var
|
任何人都可以读取,但只能在脚本内写入。 |
public-init var
|
可以在对象字面量中初始化,但只能由拥有脚本更新。仅允许用于实例变量。 |
package var
|
从包中可访问的变量。该变量只能从同一包中的类进行初始化、重写、读取、赋值或绑定。 |
package def
|
定义一个只能从同一包中的类读取或绑定的变量。 |
protected var
|
从包或子类可访问的变量。该变量只能从子类或同一包中的类进行初始化、重写、读取、赋值或绑定。 |
protected def
|
定义一个只能从同一包中的类或子类读取或绑定的变量。 |
public-read protected var
|
任何人都可以读取和绑定,但该变量只能从子类或同一包中的类进行初始化、重写或赋值。 |
public-init protected var
|
可以在对象字面量中初始化,任何人都可以读取和绑定,但只能从子类或同一包中的类进行重写或赋值。仅允许用于实例变量。 |
您还可以为变量声明更改触发器。更改触发器是 JavaFX 脚本块,每当变量值发生变化时就会被调用。要声明更改触发器,请使用 on replace 语法
public var x:Number = 100 on replace {
println("New value is {x}");
};
public var width: Number on replace (old) {
println("Old value is {old}, New value is {x}");
}
更改触发器将在第四章中更详细地讨论。
序列
序列是对象的有序列表。由于有序列表在编程中经常使用,JavaFX 将序列作为一流的功能来支持。语言内置了对声明序列、插入、删除和修改序列中的项的支持。还提供了强大的支持来从序列中检索项。
声明序列
要声明一个序列,请使用方括号,并用逗号分隔每个项。例如:
public def monthNames = ["January", "February", "March",
"April", "May", "June",
"July", August", "September",
"October", "November", "December"];
这个序列是字符串序列,因为括号内的元素是字符串。也可以这样声明:
public def monthNames: String[] = [ "January", .....];
要分配一个空序列,只需使用方括号 []。这也是序列的默认值。例如,以下两个语句都等于空序列。
public var nodes:Node[] = [];
public var nodes:Node[];
当序列发生变化时,您可以分配一个触发函数来处理该变化。这将在下一章中详细讨论。
声明整数和数字序列的简写形式是使用范围,即开始整数或数字和结束数字。所以,[1..9] 是从 1 到 9(含)的整数序列;排他形式是 [1..<9],即从 1 到 8。您还可以使用步长函数,例如,如果您想要正偶数,可以使用 [2..100 step 2]。对于数字,您可以使用小数,[0.1..1.0 step 0.1]。如果不指定步长,则默认为 1 或 1.0。
范围也可以按降序排列。为此,第一个数字必须高于第二个数字。但是,如果没有负步长函数,您最终总是会得到一个空序列。这是因为默认步长始终为正 1。
var negativeNumbers = [0..-10]; // Empty sequence
var negativeNumbers = [0..-10 step -1]; // 0,-1,-2,...-10
var negativeNumbers = [0..<-10 step -1]; // 0,-1,-2,...,-9
要构建包含其他序列元素的序列,只需在方括号内包含源序列。
var negativePlusEven = [ negativeNumbers, evenNumbers ];
此外,您还可以使用另一个序列来创建一个序列,使用布尔运算符。另一个序列用作源,布尔运算符应用于源序列中的每个元素,并且源中评估为 true 的元素将返回到新序列中。在以下示例中,n 代表正整数序列中的每个项,n mod 2 == 0 是评估。
var evenIntegers = positiveIntegers[n | n mod 2 == 0];
也可以从 for 循环分配一个序列。for 循环迭代“返回”的每个对象都会被添加到序列中。
// creates sequence of Texts
var lineNumbers:Text[] = for(n in [1..100]) {
Text { content: "{n}" };
};
// creates Integer sequence, using indexof operator
var indexNumbers = for(n in nodes) {
indexof n;
};
要获取序列的当前大小,请使用 sizeof 运算符。
var numEvenNumbers = sizeof evenNumbers;
访问序列元素
要访问单个元素,请在方括号内使用元素的数字索引。
var firstMonth = monthNames[0];
您还可以通过提供范围来获取序列的切片。以下两个序列是相等的。
var firstQuarter = monthNames[0..2];
var firstQuarter = monthNames[0..<3];
以下两个序列也相等。第二个示例使用范围语法来指示从索引开始并返回该索引之后的所有元素。
var fourthQuarter = monthNames[9..11 ];
var fourthQuarter = monthNames[9.. ];
要迭代序列,请使用 for 循环。
for( month in monthNames) {
println("{month}");
}
修改序列
要替换序列中的元素,只需将新值赋给该索引位置。
var students = [ "joe", "sally", "jim"];
students[0] = "vijay";
开发者须知 - 正如我们在本章开头所说,JavaFX 是一种宽容的语言,因此如果您为超出序列现有大小的元素索引位置赋值,该赋值将被静默忽略。
让我们使用上一个示例中的 students 序列。
students[3] = "john";对位置 3 的赋值将被忽略,因为 students 的大小目前是 3,而最高有效索引是 2。同样,由于同样的原因,对索引 -1 的赋值也会被静默忽略;-1 超出了序列范围。
此外,如果您访问的元素位置超出了序列的现有范围,则会返回默认值。对于数字,这是零;对于字符串,是空字符串;对于对象,是 null。
要将元素插入序列,请使用 insert 语句。
// add "vijay" to the end of students
insert "vijay" into students;
// insert "mike" at the front of students
insert "mike" before students[0];
// insert "george" after the second student
insert "george" after students[1];
要删除元素,请使用 delete 语句。
delete students[0]; // remove the first student
delete students[0..1]; // remove the first 2 students
delete students[0..<2]; // remove the first 2 students
delete students[1..]; // remove all but the first student
delete "vijay" from students;
delete students; // remove all students
原生数组
原生数组是一项允许您创建 Java 数组的功能。此功能主要用于处理数组在 JavaFX 和 Java 之间来回传输。以下代码显示了创建 Java int[] 数组的示例。
var ints: nativearray of Integer =
[1,2,3] as nativearray of Integer;
原生数组与序列不同,尽管它们看起来相似。您不能使用序列运算符,例如 insert 和 delete,或切片。但是,您可以对数组的元素进行赋值,如下面的代码所示。
ints[2] = 4;
但是,如果您在数组的当前边界之外赋值,您将收到 ArrayIndexOutOfBounds Exception。
您还可以使用 for 运算符迭代原生数组中的元素。以下代码显示了一个示例。
for(i in ints) {
println(i);
}
for(i in ints where i mod 2 == 0) {
println(i);
}
函数
函数定义行为。它们封装了对输入(函数参数)进行操作的语句,并且可能产生一个结果(返回的表达式)。像变量一样,函数要么是脚本函数,要么是实例函数。脚本函数在脚本级别运行,并可以访问在脚本级别定义的变量和其他函数。实例函数定义对象的行为,并可以访问函数声明类中包含的其他实例变量和函数。此外,实例函数可以访问其自身脚本文件中包含的任何脚本级别变量和函数。
要声明一个函数,请使用可选的访问修饰符 public、protected 或 package,后跟 function 关键字和函数名。如果未提供访问修饰符,则该函数对脚本文件是私有的。任何函数参数都包含在括号内。然后,您可以指定函数返回类型。如果省略返回类型,则函数返回类型从函数表达式块的最后一个表达式推断。特殊返回类型 Void 可用于指示函数不返回任何内容。
在以下示例中,两个函数声明是相等的。第一个函数推断返回类型为 Glow,因为函数块中的最后一个表达式是 Glow 对象的对象字面量。第二个函数显式声明返回类型为 Glow,并使用 return 关键字。
public function glow(level: Number) {
// return type Glow inferred
Glow { level: level };
}
public function glow(): Glow { // explicit return type
return glow(3.0); // explicit return keyword
}
当用作函数块的最后一个表达式时,return 关键字是可选的。但是,如果您想立即从 if/else 或循环中返回,则必须使用显式的 return。
在 JavaFX 中,函数本身就是对象,可以分配给变量。例如,要声明一个函数变量,将一个函数赋给该变量,然后通过该变量调用该函数。
var glowFunction : function(level:Number):Glow;
glowFunction = glow;
glowFunction(1.0);
函数定义也可以是匿名的。例如,对于函数变量:
var glowFunction:function(level:Number): Glow =
function(level:Number) {
Glow { level: level };
};
或者,在对象字面量声明中:
TextBox {
columns: 20
action: function() {
println("TextBox action");
}
}
使用 override 来重写超类中的函数。
class MyClass {
public function print() { println("MyClass"); }
}
class MySubClass extends MyClass {
override function print() { println("MySubClass"); }
}
字符串
字符串字面量
字符串字面量可以使用双引号(")或单引号(')指定。使用其中一个而不是另一个的主要原因是避免字符串字面量中的字符转义,例如,如果字符串字面量实际包含双引号。通过将字符串括在单引号中,您不必转义嵌入的双引号。考虑以下两个有效示例:
var quote = "Winston Churchill said:
\"Never in the field of human conflict was
so much owed by so many to so few.\""
var quote = 'Winston Churchill said:
"Never in the field of human conflict was
so much owed by so many to so few."'
可以通过使用花括号将表达式嵌入字符串字面量中。
var name = "Jim";
// prints My name is Jim
println ( "My name is {name}" );
嵌入的表达式必须是有效的 JavaFX 或 Java 表达式,它返回一个对象。该对象将通过其 toString() 方法转换为字符串。例如:
println ( "Today is {java.util.Date{}}" );
var state ="The state is {
if(running) "Running" else "Stopped"}";
println(" The state is {getStateStr()}" );
println("The state is {
if(checkRunning()) "Running" else "Stopped"}");
此外,字符串字面量可以跨越多行。
var quote = "Winston Churchill said: "
"\"Never in the field of human conflict was so much owed "
"by so many to so few.\"";
在此示例中,来自两行的字符串被连接成一个字符串。仅使用引号内的字符串字面量,并忽略引号外的任何空格。
可以使用 \u + *四位 Unicode* 在字符串字面量中输入 Unicode 字符。
var thanks = "dank\u00eb"; // dank
格式化
字符串字面量中的嵌入表达式可以包含一个格式代码,该代码指定如何显示嵌入的表达式。请看下面的示例。
var totalCountMessage = "The total count is {total}";
现在,如果 total 是一个整数,则生成的字符串将显示十进制数;但如果 total 是一个 Number,则生成的字符串将显示根据本地环境格式化的数字。
var total = 1000.0;
产生
The total count is 1000.0
要格式化表达式,您需要在嵌入表达式中包含一个格式代码。这是一个百分号(%)后跟格式代码。格式代码在 java.util.Formatter 类中定义。有关详细信息,请参阅其 JavaDoc 页面(http://java.sun.com/javase/6/docs/api/index.html)。
println("Total is {%f total}"); // Total is 1000.000000
println("Total is {%.2f total}"); // Total is 1000.00
println("Total is {%5.0f total}"); // Total is 1000
println("Total is {%+5.0f total}"); // Total is +1000
println("Total is {%,5.0f total}"); // Total is 1,000
开发者须知 - 要在字符串中包含百分号 (%) 字符,需要用另一个百分号 (%%) 进行转义。例如:
println("%%{percentage}"); // prints %25
国际化
要国际化字符串,您必须在字符串声明中使用“翻译键”语法。要创建翻译键,字符串赋值以 ##(井号,井号)开头,表示该字符串将被翻译为主机区域设置。## 组合在前面的双引号或单引号之前。可选地,可以在方括号 ([]) 中指定一个键。如果未指定键,则字符串本身将成为区域设置属性文件中的键。例如:
var postalCode = ## "Zip Code: ";
var postalCode = ##[postal]"Zip Code: ";
在前面的示例中,使用第一种形式,键是“Zip Code: ”,而对于第二种形式,键是“postal”。它是如何工作的?
默认情况下,本地化器会为每个唯一的脚本名称搜索一个属性文件。这是包名加上脚本文件名,以及一个区域设置和一个 .fxproperties 文件类型。因此,如果您的脚本名称是 com.mycompany.MyClass,本地化器代码将查找类路径上的一个名为 com/mycompany/MyClass_xx. fxproperties 的属性文件,其中 xx 是区域设置。例如,对于英国的英语,属性文件名将是 com/mycompany/MyClass_en_GB.fxproperties,而加拿大法语将是 com/mycompany/MyClass_fr_CA.fxproperties。如果您的默认区域设置只是英语,属性文件将是 MyClass_en.fxproperties。首先搜索更具体的文件,然后咨询最不具体的文件。例如,将搜索 MyClass_ en_GB.fxproperties 来查找键,如果找不到,则搜索 MyClass_en.fxproperties。如果根本找不到键,则使用字符串本身作为默认值。以下是一些示例:
示例 #1
println(##"Thank you");
法语 – MyClass_fr.fxproperties
"Thank you" = "Merci"
德语 – MyClass_de.fxproperties
"Thank you" = "Danke"
日语 – MyClass_ja.fxproperties
"Thank you" = "Arigato"
示例 #2
println(##[ThankKey] "Thank you");
法语 – MyClass_fr.fxproperties
"ThankKey" = "Merci"
德语 – MyClass_de.fxproperties
"ThankKey" = "Danke"
日语 – MyClass_ja.fxproperties
"ThankKey" = "Arigato"
当您使用带有嵌入式表达式的字符串时,字面量键包含一个 %s,其中表达式位于字符串内。例如:
println(##"Hello, my name is {firstname}");
在这种情况下,键是“Hello, my name is %s”。同样,如果您使用多个表达式,键将包含一个“%s”用于每个表达式。
println(##"Hello, my name is {firstname} {lastname}");
现在,键是“Hello, my name is %s %s”。
此参数替换也用于翻译后的字符串中。例如:
法语 – MyClass_fr.fxproperties
"Hello, my name is %s %s" = "Bonjour, je m'appelle %s %s"
最后,您可以将另一个 Properties 文件与脚本关联。这是使用 javafx.util.StringLocalizer 类完成的。例如:
StringLocalizer.associate("com.mycompany.resources.MyResources", "com.mycompany");
现在,com.mycompany 包中脚本的所有翻译查找都将查找属性文件 com/mycompany/resources/MyResources_xx.fxproperties,而不是使用使用脚本名称的默认值。同样,xx 被区域设置缩写代码替换。
表达式和运算符
块表达式
块表达式是语句列表,可以在花括号内包含变量声明或其他表达式。如果最后一个语句是表达式,则块表达式的值是该最后一个表达式的值;否则,块表达式不代表任何值。列表 3.6 显示了两个块表达式。第一个表达式计算为由subtotal值表示的数字。第二个块表达式不计算任何值,因为最后一个表达式是声明为 Void 的 println() 函数。
列表 3.6 块表达式
// block expression with a value
var total = {
var subtotal = 0;
var ndx = 0;
while(ndx < 100) {
subtotal += ndx;
ndx++;
};
subtotal; // last expression
};
//block expression without a value
{
var total = 0;
var ndx = 0;
while(ndx < 100) {
total += ndx;
ndx++;
};
println("Total is {total}");
}
异常处理
throw 语句与 Java 相同,并且只能抛出扩展 java.lang.Throwable 的类。
try/catch/finally 表达式与 Java 相同,但使用 JavaFX 语法。
try {
} catch (e:SomeException) {
} finally {
}
运算符
表 3.2 包含 JavaFX 中使用的运算符列表。优先级列指示运算符的求值顺序,优先级较高的运算符在前面的行中。具有相同优先级级别的运算符的求值是相等的。赋值运算符从右到左求值,而所有其他运算符从左到右求值。可以使用括号来更改此默认求值顺序。
表 3.2 运算符
优先级 | 运算符 | 含义 |
1 |
++/--(后缀) |
后增量/减量赋值 |
2 |
++/--(前缀) |
前增量/减量赋值 |
- |
一元减号 |
|
not |
逻辑补码;反转布尔值 |
|
sizeof |
序列大小 |
|
reverse |
反转序列顺序 |
|
indexof |
序列元素的索引 |
|
3 |
/, *, mod |
算术运算符 |
4 |
+, - |
算术运算符 |
5 |
==, != |
比较运算符(注意:所有比较都类似于 Java 中的 isEquals()) |
<, <=, >, >= |
数字比较运算符 |
|
6 |
instanceof, as |
类型运算符 |
7 |
和 |
逻辑与 |
8 |
或 |
逻辑或 |
9 |
+=, -=, *=, /= |
复合赋值 |
10 |
=>, tween |
动画插值运算符 |
11 |
= |
赋值 |
条件表达式
if/else
if 类似于其他语言中定义的 if。首先,评估一个条件,如果为 true,则评估表达式块。否则,如果提供了 else 表达式块,则评估该表达式块。
if (date == today) {
println("Date is today");
}else {
println("Out of date!!!");
}
if/else 的一个重要特性是每个表达式块都可以求值为一个可以赋值给变量的表达式。
var outOfDateMessage = if(date==today) "Date is today"
else "Out of Date";
此外,表达式块可以比简单表达式更复杂。列表 3.7 显示了一个复杂的赋值,使用 if/else 语句将值赋给 outOfDateMessage。
列表 3.7 使用 if/else 表达式进行复杂赋值
var outOfDateMessage = if(date==today) {
var total = 0;
for(item in items) {
total += items.price;
}
totalPrice += total;
"Date is today";
} else {
errorFlag = true;
"Out of Date";
};
在前面的示例中,块中的最后一个表达式,即错误消息字符串字面量,是被赋给变量的对象。这可以是任何 JavaFX 对象,包括数字。
由于 if/else 是一个表达式块,它可以与另一个 if/else 语句一起使用。例如:
var taxBracket = if(income < 8025.0) 0.10
else if(income < 32550.0)0.15
else if (income < 78850.0) 0.25
else if (income < 164550.0) 0.28
else 0.33;
循环表达式
For
for 循环与序列一起使用,允许您迭代序列的成员。
var daysOfWeek : String[] =
[ "Sunday", "Monday", "Tuesday" ];
for(day in daysOfWeek) {
println("{indexof day}). {day}");
}
为了类似于迭代计数的传统 for 循环,请使用方括号内定义的整数序列范围。
for( i in [0..100]} {
for 表达式也可以返回一个新序列。对于每次迭代,如果执行的表达式块求值为一个 Object,则该 Object 将插入到 for 表达式返回的新序列中。例如,在以下 for 表达式中,每周的每一天都会创建一个新的 Text 节点。整个 for 表达式返回一个包含 Text 图形元素的新序列,每个星期一天一个。
var textNodes: Text[] = for( day in daysOfWeek) {
Text {content: day };
}
for 表达式的另一个特性是它可以进行嵌套循环。列表 3.8 显示了使用嵌套循环的示例。
列表 3.8 嵌套 For 循环
class Course {
var title: String;
var students: String[];
}
var courses = [
Course {
title: "Geometry I"
students: [ "Clarke, "Connors", "Bruno" ]
},
Course {
title: "Geometry II"
students: [ "Clarke, "Connors", ]
},
Course {
title: "Algebra I"
students: [ "Connors", "Bruno" ]
},
];
for(course in courses, student in course.students) {
println("Student: {student} is in course {course}");
}
这将打印出:
Student: Clarke is in course Geometry I
Student: Connors is in course Geometry I
Student: Bruno is in course Geometry I
Student: Clarke is in course Geometry II
Student: Connors is in course Geometry II
Student: Connors is in course Algebra I
Student: Bruno is in course Algebra I
可以有零个或多个次要循环,它们用逗号与前一个循环分隔,并且可以引用前一个循环中的任何元素。
您还可以包含一个 where 子句来限制迭代,只包括 where 子句求值为 true 的元素。
var evenNumbers = for( i in [0..1000] where i mod 2 == 0 ) i;
while
while 循环的工作方式与在其他语言中看到的 while 循环类似。
var ndx = 0;
while ( ndx < 100) {
println("{ndx}");
ndx++;
}
请注意,与 JavaFX for 循环不同,while 循环不返回任何表达式,因此不能用于创建序列。
Break/Continue
break 和 continue 控制循环迭代。break 用于完全退出循环。它导致从该点开始的所有循环停止。另一方面,continue 只导致当前迭代停止,循环继续执行下一次迭代。列表 3.9 演示了这些用法。
列表 3.9 Break/Continue
for(student in students) {
if(student.name == "Jim") {
foundStudent = student;
break; // stops the loop altogether,
//no more students are checked
}
}
for(book in Books ) {
if(book.publisher == "Addison Wesley") {
insert book into bookList;
continue; // moves on to check next book.
}
insert book into otherBookList;
otherPrice += book.price;
}
类型运算符
instanceof 运算符允许您测试对象的类类型,而 as 运算符允许您将对象转换为另一个类。这很有用的一个方法是将通用对象转换为更具体的类,以便执行该更专业类的函数。当然,对象本身必须是那种类,而这正是 instanceof 运算符用于测试对象是否确实是那种类的地方。如果您尝试将一个对象转换为该对象不继承的类,您将收到一个异常。
在以下列表中,printLower() 函数会将字符串转换为小写,但对于其他类型的对象,它将按原样打印。首先,测试通用对象是否为 String。如果是,则使用 as 运算符将对象转换为 String,然后使用 String 的 toLowerCase() 方法将输出转换为全小写。列表 3.10 说明了 instanceof 和 as 运算符的用法。
列表 3.10 类型运算符
function printLower(object: Object ) {
if(object instanceof String) {
var str = object as String;
println(str.toLowerCase());
}else {
println(object);
}
}
printLower("Rich Internet Application");
printLower(3.14);
访问命令行参数
对于不声明导出类、变量或函数的纯脚本,可以使用 javafx.lang.FX .getArguments():String[] 函数检索命令行参数。它返回一个字符串序列,其中包含脚本启动时传递给脚本的参数。对于其他调用(如 Applet),还有另一个版本,其中参数使用名称-值对传递,javafx.lang.FX.getArguments(key:String) :String[]。类似地,还有一个用于获取系统属性的函数,javafx.lang.FX .getProperty(key:String):String[]。
如果脚本包含任何导出的类、变量或函数,则通过在脚本级别定义一个特殊的 run 函数来获取参数。
public function run(args:String[] ) {
for(arg in args) {
println("{arg}");
}
}
带有导出成员的宽松表达式 - 脚本级别(不在类声明内)的变量、函数和表达式称为宽松表达式。当这些变量和函数对脚本私有时,如果脚本从命令行执行,则不需要特定的 run 函数。但是,如果这些表达式中的任何一个使用 public、public-read、protected、package 访问权限导出到脚本外部,则在直接执行脚本时需要 run 函数。此 run 方法封装了导出的变量和函数。
内置函数和变量
有一组函数可供所有 JavaFX 脚本自动使用。这些函数在 javafx.lang.Builtins 中定义。
您已经看到过其中一个,println()。Println() 接受一个对象参数并逐行将其打印到控制台。它类似于 Java 的 System.out.println() 方法。它的配套函数是 print()。Print() 打印其参数,但不换行。调用参数的 toString() 方法来打印一个字符串。
println("This is printed on a single line");
print("This is printed without a new line");
来自 javafx.lang.Builtins 的另一个函数是 isInitialized()。此方法接受一个 JavaFX 对象,并指示该对象是否已完全初始化。它在变量触发器中很有用,可以确定对象在初始化期间的当前状态。有时您可能希望仅在对象通过初始化阶段后才执行某些功能。例如,列表 3.11 显示了在 on replace 触发器中使用的内置 isInitialized()。
列表 3.11 isInitialized()
public class Test {
public var status: Number on replace {
// will not be initialized
// until status is assigned a value
if(isInitialized(status)) {
commenceTest(status);
}
}
public function commenceTest(status:Number) : Void {
println("commenceTest status = {status}:);
}
}
在此示例中,当 Test 类首次实例化时,实例变量 status 首先采用默认值 0.0,然后评估 on replace 表达式块。但是,这会使 status 处于未初始化状态。只有当为 status 赋值时,状态才会变为已初始化。考虑以下:
var test = Test{}; // status is uninitialized
test.status = 1; // now status becomes initialized
在这种情况下,当使用对象字面量 Test{} 创建 Test 时,status 采用默认值 0.0;但是,它未初始化,因此 commenceTest 在对象创建期间不会被调用。现在,当我们为 status 赋值时,状态变为已初始化,因此 commenceTest 被调用。请注意,如果我们为 status 分配了一个默认值,即使该值为 0,status 也会立即设置为已初始化。以下示例演示了这一点。
public class Test {
public var status: Number = 0 on replace {
// will be initialized immediately.
if(isInitialized(status)) {
commenceTest(status);
}
}
最后一个内置函数是 isSameObject()。isSameObject() 指示两个参数是否实际上是同一个实例。这与 == 运算符相反。在 JavaFX 中,== 运算符确定两个对象是否被视为相等,但这并不意味着它们是同一个实例。== 运算符类似于 Java 的 isEquals() 函数,而 JavaFX 的 isSameObject 类似于 Java 的 == 运算符。如果您有 Java 背景,这可能会有点令人困惑!
内置变量是 __DIR__ 和 __FILE__。__FILE__ 保存包含的 JavaFX 类的资源 URL 字符串。__DIR__ 保存当前类所在目录的资源 URL 字符串。例如:
println("DIR = {__DIR__}");
println("FILE = {__FILE__}");
// to locate an image
var image = Image { url: "{__DIR__}images/foo.jpeg" };
以下示例显示了基于目录的类路径与使用基于 JAR 的类路径的输出。
Using a Jar file in classpath
$javafx -cp Misc.jar misc.Test
DIR = jar:file:/export/home/jclarke/Documents/
Book/FX/code/Chapter3/Misc/dist/Misc.jar!/misc/
FILE = jar:file:/export/home/jclarke/Documents/
Book/FX/code/Chapter3/Misc/dist/Misc.jar!/misc/Test.class
Using directory classpath
$ javafx -cp . misc.Test
DIR = file:/export/home/jclarke/Documents/Book/
FX/code/Chapter3/Misc/dist/tmp/misc/
FILE = file:/export/home/jclarke/Documents/Book/
FX/code/Chapter3/Misc/dist/tmp/misc/Test.class
注意 __DIR__ 上的尾部斜杠 - 由于 __DIR__ 上已存在尾部斜杠,因此在使用 __DIR__ 构建资源(如图像)的路径时,请勿添加额外的尾部斜杠。Image{ url: "{__DIR__}image/foo.jpeg"} 是正确的。
Image{ url: "{__DIR__}/image/foo.jpeg"} 是错误的。如果在 __DIR__ 之后添加尾部斜杠,将找不到图像,您将挠头为什么找不到。
章节总结
本章介绍了 JavaFX 脚本语言的关键概念。您了解了什么构成脚本和什么构成类。您学习了如何声明脚本和实例变量,如何创建和修改序列,以及如何控制逻辑流程。
您现在对 JavaFX 脚本语言的语法和运算符有了基本的了解。现在,是时候付诸实践了。在接下来的章节中,我们将深入探讨 JavaFX 的关键特性,并展示如何利用 JavaFX 脚本语言来利用这些特性。在下一章中,我们将通过讨论 JavaFX 运行时中的数据同步支持来开始探索 JavaFX。
[1] Torgersson, Olof. “A Note on Declarative Programming Paradigms and the Future of Definitional Programming,” Chalmers University of Technology and Göteborg University, Göteborg, Sweden. http://www.cs.chalmers.se/~oloft/Papers/wm96/wm96.html.
© 版权所有 © 2009 Sun Microsystems, Inc. 保留所有权利。