原文:Pro JavaFX 9
三、属性和绑定
The sky is full of vigor and perseverance. Correspondingly, superior people keep their vitality constantly. -I ching
在第 1 和 2 章中,我们向您介绍了 JavaFX 9 平台,它是 Oracle JDK 9 的一部分。您可以用自己喜欢的 IDE 来设置开发环境:Eclipse、NetBeans 或 IntelliJ IDEA。您编写并运行了您的第一个 JavaFX GUI 程序。您学习了 JavaFX 的基本构建块:类Stage和Scene,以及进入Scene的Node。毫无疑问,您已经注意到使用用户定义的模型类来表示应用程序状态,并通过属性和绑定将该状态传递给 UI。
在本章中,我们将向您介绍 JavaFX 属性和绑定框架。在回顾了一点历史并展示了一个展示 JavaFX Property的各种使用方式的激励性示例之后,我们将介绍框架的关键概念:Observable、ObservableValue、WritableValue、ReadOnlyProperty、Property和Binding。我们向您展示了框架的这些基本接口所提供的功能。然后,我们向您展示如何将Property对象绑定在一起,如何利用属性和其他绑定来构建Binding对象——使用Bindings实用程序类中的工厂方法、fluent 接口 API,或者通过直接扩展实现Binding接口的抽象类来降低级别——以及如何使用它们来轻松地将程序一部分中的更改传播到程序的其他部分,而无需过多的编码。然后我们介绍 JavaFX Beans 命名约定,它是原始 JavaBeans 命名约定的扩展,使将数据组织到封装的组件中变得有条不紊。我们通过展示如何将旧式 JavaBeans 属性改编成 JavaFX 属性来结束本章。
因为 JavaFX 属性和绑定框架是 JavaFX 平台的非可视部分,所以本章中的示例程序本质上也是非可视的。我们处理Boolean、Integer、Long、Float、Double、String和Object类型属性和绑定,因为这些是 JavaFX 绑定框架专门处理的类型。您的 GUI 构建乐趣将在下一章和后续章节中继续。
JavaFX 绑定的先驱
在 Java 生命的早期,人们就认识到需要将 Java 组件的属性直接暴露给客户机代码,允许它们观察和操作这些属性,并在它们的值改变时采取行动。Java 1.1 中的 JavaBeans 框架通过现在熟悉的 getter 和 setter 约定提供了对属性的支持。它还通过其PropertyChangeEvent和PropertyChangeListener机制支持属性变化的传播。尽管 JavaBeans 框架在许多 Swing 应用程序中使用,但它的使用相当麻烦,需要相当多的样板代码。几年来,人们创建了几个高级数据绑定框架,取得了不同程度的成功。JavaFX 属性和绑定框架中 JavaBeans 的继承主要在于定义 JavaFX 组件时的 JavaFX Beans getter、setter 和属性 getter 命名约定。在讲述了 JavaFX 属性和绑定框架的关键概念和接口之后,我们将在本章的后面讨论 JavaFX Beans getter、setter 和属性 getter 命名约定。
JavaFX 属性和绑定框架的另一个继承来自 JavaFX Script 语言,它是 JavaFX 1.x 平台的一部分。尽管 JavaFX 平台不赞成使用 JavaFX Script 语言,而是支持基于 Java 的 API,但这种转变的目标之一是保留 JavaFX Script 的bind关键字的大部分功能,它的表达能力让许多 JavaFX 爱好者感到高兴。例如,JavaFX 脚本支持绑定到复杂表达式:
var a = 1;
var b = 10;
var m = 4;
def c = bind for (x in [a..b] where x < m) { x * x };
每当a、b或m的值改变时,该代码将自动重新计算c的值。
尽管 JavaFX 属性和绑定框架不支持 JavaFX 脚本的所有绑定结构,但它支持许多有用表达式的绑定。在介绍了框架的关键概念和接口之后,我们将更多地讨论如何构建复合绑定表达式。
激励人心的例子
让我们从清单 3-1 中的一个例子开始,它通过使用几个SimpleIntegerProperty类的实例展示了Property接口的功能。
import javafx.beans.InvalidationListener;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
public class MotivatingExample {
private static IntegerProperty intProperty;
public static void main(String[] args) {
createProperty();
addAndRemoveInvalidationListener();
addAndRemoveChangeListener();
bindAndUnbindOnePropertyToAnother();
}
private static void createProperty() {
System.out.println();
intProperty = new SimpleIntegerProperty(1024);
System.out.println("intProperty = " + intProperty);
System.out.println("intProperty.get() = " + intProperty.get());
System.out.println("intProperty.getValue() = " + intProperty.getValue().intValue());
}
private static void addAndRemoveInvalidationListener() {
System.out.println();
final InvalidationListener invalidationListener = observable ->
System.out.println("The observable has been invalidated: " + observable + ".");
intProperty.addListener(invalidationListener);
System.out.println("Added invalidation listener.");
System.out.println("Calling intProperty.set(2048).");
intProperty.set(2048);
System.out.println("Calling intProperty.setValue(3072).");
intProperty.setValue(Integer.valueOf(3072));
intProperty.removeListener(invalidationListener);
System.out.println("Removed invalidation listener.");
System.out.println("Calling intProperty.set(4096).");
intProperty.set(4096);
}
private static void addAndRemoveChangeListener() {
System.out.println();
final ChangeListener changeListener = (ObservableValue observableValue, Object oldValue, Object newValue) ->
System.out.println("The observableValue has changed: oldValue = " + oldValue + ", newValue = " + newValue);
intProperty.addListener(changeListener);
System.out.println("Added change listener.");
System.out.println("Calling intProperty.set(5120).");
intProperty.set(5120);
intProperty.removeListener(changeListener);
System.out.println("Removed change listener.");
System.out.println("Calling intProperty.set(6144).");
intProperty.set(6144);
}
private static void bindAndUnbindOnePropertyToAnother() {
System.out.println();
IntegerProperty otherProperty = new SimpleIntegerProperty(0);
System.out.println("otherProperty.get() = " + otherProperty.get());
System.out.println("Binding otherProperty to intProperty.");
otherProperty.bind(intProperty);
System.out.println("otherProperty.get() = " + otherProperty.get());
System.out.println("Calling intProperty.set(7168).");
intProperty.set(7168);
System.out.println("otherProperty.get() = " + otherProperty.get());
System.out.println("Unbinding otherProperty from intProperty.");
otherProperty.unbind();
System.out.println("otherProperty.get() = " + otherProperty.get());
System.out.println("Calling intProperty.set(8192).");
intProperty.set(8192);
System.out.println("otherProperty.get() = " + otherProperty.get());
}
}
Listing 3-1.
MotivatingExample.java
在这个例子中,我们创建了一个名为intProperty的SimpleIntegerProperty对象,初始值为1024。然后我们通过一系列不同的整数更新它的值,同时我们添加然后移除一个InvalidationListener,添加然后移除一个ChangeListener,最后,创建另一个名为otherProperty的SimpleIntegerProperty,将其绑定到,然后从intProperty解除绑定。我们利用 Java 8 lambda 语法来定义我们的侦听器。示例程序使用了大量的println调用来展示程序内部发生的事情。
当我们运行清单 3-1 中的程序时,以下输出被打印到控制台:
intProperty = IntegerProperty [value: 1024]
intProperty.get() = 1024
intProperty.getValue() = 1024
Added invalidation listener.
Calling intProperty.set(2048).
The observable has been invalidated: IntegerProperty [value: 2048].
Calling intProperty.setValue(3072).
The observable has been invalidated: IntegerProperty [value: 3072].
Removed invalidation listener.
Calling intProperty.set(4096).
Added change listener.
Calling intProperty.set(5120).
The observableValue has changed: oldValue = 4096, newValue = 5120
Removed change listener.
Calling intProperty.set(6144).
otherProperty.get() = 0
Binding otherProperty to intProperty.
otherProperty.get() = 6144
Calling intProperty.set(7168).
otherProperty.get() = 7168
Unbinding otherProperty from intProperty.
otherProperty.get() = 7168
Calling intProperty.set(8192).
otherProperty.get() = 7168
通过将输出行与程序源代码相关联(或者通过在您喜欢的 IDE 的调试器中单步调试代码),我们可以得出以下结论。
- 一个
SimpleIntegerProperty对象,比如intProperty和otherProperty持有一个int值。该值可以用get()、set()、getValue()和setValue()方法操作。get()和set()方法使用原语int类型执行它们的操作。getValue()和setValue()方法使用Integer包装器类型。 - 您可以在
intProperty中添加和删除InvalidationListener对象。 - 您可以在
intProperty中添加和删除ChangeListener对象。 - 另一个
Property对象如otherProperty可以将自己绑定到intProperty。当这种情况发生时,otherProperty接收intProperty的值。 - 当在
intProperty上设置一个新值时,连接到它的任何对象都会得到通知。如果对象被移除,则不会发送通知。 - 当被通知时,
InvalidationListener对象仅被告知哪个对象正在发出通知,并且该对象仅被称为Observable。 - 当被通知时,除了发送通知的对象之外,
ChangeListener对象还被告知另外两条信息——oldValue和newValue。发送对象被称为ObservableValue。 - 在绑定属性如
otherProperty的情况下,我们无法从输出中得知intProperty中值的变化何时或如何通知它。然而,我们可以推断它一定知道这个变化,因为当我们向otherProperty请求它的值时,我们得到了intProperty的最新值。
Note
尽管这个激励示例使用了一个Integer属性,但是类似的示例也可以使用基于Boolean、Long、Float、Double、String和Object类型的属性。在 JavaFX 属性和绑定框架中,当接口为具体类型扩展或实现时,它们总是为Boolean、Integer、Long、Float、Double、String和Object类型完成。
这个例子让我们注意到 JavaFX 属性和绑定框架的一些关键接口和概念:包括Observable和相关联的InvalidationListener接口、ObservableValue和相关联的ChangeListener接口、get()、set()、getValue()和setValue()方法,它们允许我们直接操作SimpleIntegerProperty对象的值,以及bind()方法,它们允许我们通过从属于另一个SimpleIntegerProperty对象来放弃对SimpleIntegerProperty对象的值的直接操作。
在下一节中,我们将更详细地向您展示 JavaFX 属性和绑定框架的这些以及其他一些关键接口和概念。
理解关键接口和概念
图 3-1 是一个 UML 图,显示了 JavaFX 属性和绑定框架的关键接口。它包括一些您在上一节中看到的界面,以及一些您还没有看到的界面。

图 3-1。
Key interfaces of the JavaFX properties and bindings framework Note
我们没有向您展示 UML 图中接口的完全限定名。这些接口分布在四个包中:javafx.beans、javafx.beans.binding、javafx.beans.property和javafx.beans.value。通过查看 JavaFX API 文档或您喜欢的 IDE 的“find class”特性,您可以很容易地确定哪个接口属于哪个包。
理解可观察界面
层次结构的根是Observable接口。您可以将InvalidationListener对象注册到一个Observable对象来接收失效事件。在上一节的激励示例中,您已经看到了从一种Observable对象,即SimpleIntegerProperty对象intProperty触发的失效事件。当调用set()或setValue()方法将底层值从一个int更改为另一个int时,它被触发。
Note
如果您连续多次使用相同的值调用 setter,JavaFX 属性和绑定框架中的Property接口的任何实现只会触发一次失效事件。
另一个引发失效事件的地方是来自Binding对象。你还没有看到一个Binding对象的例子,但是在本章的后半部分有大量的Binding对象。现在我们只注意到一个Binding对象可能会变得无效,例如,当它的invalidate()方法被调用时,或者如我们在本章后面所展示的,当它的一个依赖项触发一个无效事件时。
Note
如果一个失效事件连续几次失效,那么 JavaFX properties and bindings 框架中的任何一个Binding接口实现都只会触发一次。
了解 ObservableValue 接口
层次结构中的下一个是ObservableValue接口。它只是一个有值的Observable。它的getValue()方法返回它的值。我们在激励示例中对SimpleIntegerProperty对象调用的getValue()方法可以被认为是来自这个接口。您可以将ChangeListener对象注册到一个ObservableValue对象来接收变更事件。
在上一节的激励示例中,您看到了变更事件被激发。当 change 事件触发时,ChangeListener接收到另外两条信息:ObservableValue对象的旧值和新值。
Note
如果您连续多次使用相同的值调用 setter,JavaFX 属性和绑定框架中的任何ObservableValue接口实现只会触发一次 change 事件。
无效事件和变更事件之间的区别在于 JavaFX 属性和绑定框架可以支持惰性评估。我们通过查看激励示例中的三行代码来展示一个例子:
otherProperty.bind(intProperty);
intProperty.set(7168);
System.out.println("otherProperty.get() = " + otherProperty.get());
当调用intProperty.set(7168)时,它向otherProperty触发一个无效事件。在收到这个无效事件时,otherProperty简单地记录下它的值不再有效的事实。它不会通过查询intProperty来立即重新计算其值。当otherProperty.get()被调用时,重新计算被执行。想象一下,如果我们多次调用intProperty.set(),而不是像前面的代码那样只调用intProperty.set()一次;otherProperty仍然只重新计算一次它的值。
Note
ObservableValue接口不是Observable的唯一直接子接口。在javafx.collections包中还有另外四个Observable的直接子接口:ObservableList、ObservableMap、ObservableSet和ObservableArray,对应的ListChangeListener、MapChangeListener、SetChangeListener和ArrayChangeListener作为回调机制。这些 JavaFX 可观察集合将在第七章中介绍。
了解可写值接口
这可能是整个章节中最简单的部分,因为WritableValue界面确实像它看起来那样简单。它的目的是将getValue()和setValue()方法注入到这个接口的实现中。JavaFX 属性和绑定框架中WritableValue的所有实现类也实现了ObservableValue;所以你可以做一个论证,WritableValue的值只是为了提供setValue()方法。
你已经看到了激励例子中的setValue()方法。
了解 ReadOnlyProperty 接口
ReadOnlyProperty接口在其实现中注入了两个方法。getBean()方法应该返回包含ReadOnlyRroperty的Object,如果不包含在Object中,则返回 null。如果ReadOnlyProperty没有名字,那么getName()方法应该返回ReadOnlyProperty的名字或者空字符串。
包含对象和名称提供了关于ReadOnlyProperty的上下文信息。属性的上下文信息在无效事件的传播或值的重新计算中不起任何直接作用。但是,如果提供的话,会在一些外围计算中考虑到。
在我们的激励示例中,intProperty是在没有任何上下文信息的情况下构建的。如果我们使用完整的构造器给它命名,
intProperty = new SimpleIntegerProperty(null, "intProperty", 1024);
输出将包含属性名:
intProperty = IntegerProperty [name: intProperty, value: 1024]
了解属性接口
现在我们来到关键接口层次的底部。到目前为止,Property接口拥有我们已经研究过的所有四个接口作为它的超接口:Observable、ObservableValue、ReadOnlyProperty和WritableValue。因此,它继承了这些接口的所有方法。它还提供了自己的五种方法:
void bind(ObservableValue<? extends T> observableValue);
void unbind();
boolean isBound();
void bindBidirectional(Property<T> tProperty);
void unbindBidirectional(Property<T> tProperty);
在上一节的激励示例中,您已经看到了两种有效的方法:bind()和unbind()。
调用bind()会在Property对象和ObservableValue参数之间创建一个单向绑定或依赖关系。一旦他们进入这种关系,调用Property对象上的set()或setValue()方法将导致抛出一个RuntimeException。调用Property对象上的get()或getValue()方法将返回ObservableValue对象的值。当然,改变ObservableValue对象的值会使Property对象失效。调用unbind()释放Property对象可能有的任何现有单向绑定。如果单向绑定生效,isBound()方法返回true;否则,返回false。
调用bindBidirectional()会在Property调用者和Property参数之间创建一个双向绑定。注意,与接受ObservableValue参数的bind()方法不同,bindBidirectional()方法接受Property参数。只有两个Property对象可以双向绑定在一起。一旦它们进入这种关系,在任一个Property对象上调用set()或setValue()方法将导致两个对象的值都被更新。调用unbindBidirectional()释放调用者和参数可能有的任何现有双向绑定。清单 3-2 中的程序展示了一个简单的双向绑定。
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
public class BidirectionalBindingExample {
public static void main(String[] args) {
System.out.println("Constructing two StringProperty objects.");
StringProperty prop1 = new SimpleStringProperty("");
StringProperty prop2 = new SimpleStringProperty("");
System.out.println("Calling bindBidirectional.");
prop2.bindBidirectional(prop1);
System.out.println("prop1.isBound() = " + prop1.isBound());
System.out.println("prop2.isBound() = " + prop2.isBound());
System.out.println("Calling prop1.set(\"prop1 says: Hi!\")");
prop1.set("prop1 says: Hi!");
System.out.println("prop2.get() returned:");
System.out.println(prop2.get());
System.out.println("Calling prop2.set(prop2.get() + \"\\nprop2 says: Bye!\")");
prop2.set(prop2.get() + "\nprop2 says: Bye!");
System.out.println("prop1.get() returned:");
System.out.println(prop1.get());
}
}
Listing 3-2.
BidirectionalBindingExample.java
在这个例子中,我们创建了两个名为prop1和prop2的SimpleStringProperty对象,在它们之间创建了一个双向绑定,然后在两个属性上分别名为set()和get()。
当我们运行清单 3-2 中的程序时,以下输出被打印到控制台:
Constructing two StringProperty objects.
Calling bindBidirectional.
prop1.isBound() = false
prop2.isBound() = false
Calling prop1.set("prop1 says: Hi!")
prop2.get() returned:
prop1 says: Hi!
Calling prop2.set(prop2.get() + "\nprop2 says: Bye!")
prop1.get() returned:
prop1 says: Hi!
prop2 says: Bye!
Caution
每个Property对象一次最多可以有一个活动的单向绑定。它可以有任意多的双向绑定。isBound()方法只适用于单向绑定。当单向绑定已经生效时,用不同的ObservableValue参数第二次调用bind()将会解除现有的绑定并用新的替换它。
了解绑定接口
Binding接口定义了四种揭示接口意图的方法。一个Binding对象是一个ObservableValue,它的有效性可以用isValid()方法查询,用invalidate()方法设置。它有一个依赖列表,可以用getDependencies()方法获得。最后一个dispose()方法发信号通知绑定将不再被使用,它所使用的资源可以被清理。
从这个对Binding接口的简短描述中,我们可以推断出它代表了一个具有多个依赖关系的单向绑定。我们想象,每一个依赖项都可以是一个ObservableValue,Binding注册到这个 ?? 来接收无效事件。当调用get()或getValue()方法时,如果绑定无效,则重新计算其值。
JavaFX 属性和绑定框架不提供任何实现Binding接口的具体类。但是,它提供了多种方法来轻松创建自己的Binding对象:可以在框架中扩展抽象基类;您可以在实用程序类Bindings中使用一组静态方法,从现有的常规 Java 值(即不可观察的值)、属性和绑定中创建新的绑定;您还可以使用各种属性和绑定类中提供的一组方法,并形成一个流畅的接口 API 来创建新的绑定。我们将在本章后面的“创建绑定”一节中介绍实用程序方法和 fluent 接口 API。现在,我们通过扩展DoubleBinding抽象类向您展示第一个绑定示例。清单 3-3 中的程序使用绑定来计算一个矩形的面积。
import javafx.beans.binding.DoubleBinding;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
public class RectangleAreaExample {
public static void main(String[] args) {
System.out.println("Constructing x with initial value of 2.0.");
final DoubleProperty x = new SimpleDoubleProperty(null, "x", 2.0);
System.out.println("Constructing y with initial value of 3.0.");
final DoubleProperty y = new SimpleDoubleProperty(null, "y", 3.0);
System.out.println("Creating binding area with dependencies x and y.");
DoubleBinding area = new DoubleBinding() {
private double value;
{
super.bind(x, y);
}
@Override
protected double computeValue() {
System.out.println("computeValue() is called.");
return x.get() * y.get();
}
};
System.out.println("area.get() = " + area.get());
System.out.println("area.get() = " + area.get());
System.out.println("Setting x to 5");
x.set(5);
System.out.println("Setting y to 7");
y.set(7);
System.out.println("area.get() = " + area.get());
}
}
Listing 3-3.
RectangleAreaExample.java
在匿名内部类中,我们调用超类DoubleBinding中受保护的bind()方法,通知超类我们想要监听来自DoubleProperty对象x和y的失效事件。我们最终在超类DoubleBinding中实现了受保护的抽象computeValue()方法,以便在需要重新计算时进行实际计算。
当我们运行清单 3-3 中的程序时,以下输出被打印到控制台:
Constructing x with initial value of 2.0.
Constructing y with initial value of 3.0.
Creating binding area with dependencies x and y.
computeValue() is called.
area.get() = 6.0
area.get() = 6.0
Setting x to 5
Setting y to 7
computeValue() is called.
area.get() = 35.0
注意,当我们连续两次调用area.get()时,computeValue()只被调用一次。
Caution
DoubleBinding抽象类包含一个空的默认实现dispose()和一个返回空列表的默认实现getDependencies()。为了使这个例子成为一个正确的Binding实现,我们应该覆盖这两个方法来正确地运行。
现在您已经牢牢掌握了 JavaFX 属性和绑定框架的关键接口和概念,我们将向您展示这些通用接口如何专用于特定类型的接口,以及如何在特定类型的抽象和具体类中实现。
键接口的特定类型专门化
我们在上一节中没有强调这个事实,因为我们相信省略它不会影响那里的解释,但是除了Observable和InvalidationListener,其余的接口都是带有类型参数<T>的通用接口。在本节中,我们将研究这些通用接口是如何专用于感兴趣的特定类型的:Boolean、Integer、Long、Float、Double、String和Object。我们还研究了框架的一些抽象和具体类,并探索了每个类的典型使用场景。
Note
这些接口的专门化也存在于List、Map和Set中。它们是为处理可观察集合而设计的。我们将在第七章中讨论可观测集合。
特定类型接口的通用主题
尽管通用接口的专门化方式并不完全相同,但存在一个共同的主题:
Boolean型直接专门化。Integer、Long、Float和Double类型通过Number超类型特殊化。String型通过Object型专门化。
这个主题存在于所有关键接口的特定于类型的专门化中。例如,我们检查ObservableValue<T>接口的子接口:
ObservableBooleanValue扩展了ObservableValue<Boolean>,它提供了一个额外的方法。boolean get();
ObservableNumberValue扩展了ObservableValue<Number>,它提供了四个额外的方法。int intValue();long longValue();float floatValue();double doubleValue();
ObservableObjectValue<T>扩展了ObservableValue<T>,它提供了一个额外的方法。T get();
ObservableIntegerValue、ObservableLongValue、ObservableFloatValue和ObservableDoubleValue扩展了ObservableNumberValue,并且每个都提供了一个额外的get()方法来返回适当的原始类型值。ObservableStringValue扩展了ObservableObjectValue<String>并继承了其返回String的get()方法。
注意,我们在示例中使用的get()方法是在特定于类型的ObservableValue子接口中定义的。类似的检查揭示了我们在示例中使用的set()方法是在特定于类型的WritableValue子接口中定义的。
这种派生层次结构的一个实际结果是,任何数字属性都可以在任何其他数字属性或绑定上调用bind()。实际上,bind()方法对任何数字属性的签名如下:
void bind(ObservableValue<? extends Number> observable);
任何数值属性和绑定都可以赋给泛型参数类型。清单 3-4 中的程序显示,任何不同特定类型的数字属性都可以相互绑定。
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.FloatProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.LongProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleFloatProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleLongProperty;
public class NumericPropertiesExample {
public static void main(String[] args) {
IntegerProperty i = new SimpleIntegerProperty(null, "i", 1024);
LongProperty l = new SimpleLongProperty(null, "l", 0L);
FloatProperty f = new SimpleFloatProperty(null, "f", 0.0F);
DoubleProperty d = new SimpleDoubleProperty(null, "d", 0.0);
System.out.println("Constructed numerical properties i, l, f, d.");
System.out.println("i.get() = " + i.get());
System.out.println("l.get() = " + l.get());
System.out.println("f.get() = " + f.get());
System.out.println("d.get() = " + d.get());
l.bind(i);
f.bind(l);
d.bind(f);
System.out.println("Bound l to i, f to l, d to f.");
System.out.println("i.get() = " + i.get());
System.out.println("l.get() = " + l.get());
System.out.println("f.get() = " + f.get());
System.out.println("d.get() = " + d.get());
System.out.println("Calling i.set(2048).");
i.set(2048);
System.out.println("i.get() = " + i.get());
System.out.println("l.get() = " + l.get());
System.out.println("f.get() = " + f.get());
System.out.println("d.get() = " + d.get());
d.unbind();
f.unbind();
l.unbind();
System.out.println("Unbound l to i, f to l, d to f.");
f.bind(d);
l.bind(f);
i.bind(l);
System.out.println("Bound f to d, l to f, i to l.");
System.out.println("Calling d.set(10000000000L).");
d.set(10000000000L);
System.out.println("d.get() = " + d.get());
System.out.println("f.get() = " + f.get());
System.out.println("l.get() = " + l.get());
System.out.println("i.get() = " + i.get());
}
}
Listing 3-4.
NumericPropertiesExample.java
在本例中,我们创建了四个数字属性,并将它们绑定到一个大小递减的链中,以演示绑定是否按预期工作。然后,我们颠倒了链的顺序,将 double 属性的值设置为一个会溢出 integer 属性的数字,以强调这样一个事实:即使您可以将不同大小的数值属性绑定在一起,但是当依赖属性的值超出绑定属性的范围时,将应用普通的 Java 数值转换。
当我们运行清单 3-4 中的程序时,以下内容被打印到控制台:
Constructed numerical properties i, l, f, d.
i.get() = 1024
l.get() = 0
f.get() = 0.0
d.get() = 0.0
Bound l to i, f to l, d to f.
i.get() = 1024
l.get() = 1024
f.get() = 1024.0
d.get() = 1024.0
Calling i.set(2048).
i.get() = 2048
l.get() = 2048
f.get() = 2048.0
d.get() = 2048.0
Unbound l to i, f to l, d to f.
Bound f to d, l to f, i to l.
Calling d.set(10000000000L).
d.get() = 1.0E10
f.get() = 1.0E10
l.get() = 10000000000
i.get() = 1410065408
常用类别
我们现在给出四个包javafx.beans、javafx.beans.binding、javafx.beans.property和javafx.beans.value的内容的调查。在本节中,SimpleIntegerProperty系列的类是指在Boolean、Integer、Long、Float、Double、String和Object类型上外推的类。所以说的话也适用于SimpleBooleanProperty,以此类推。
- JavaFX 属性和绑定框架中最常用的类是
SimpleIntegerProperty系列的类。它们提供了Property接口的所有功能,包括惰性评估。到目前为止,本章的所有例子都使用了它们。 - JavaFX 属性和绑定框架中的另一组具体类是
ReadOnlyIntegerWrapper系列的类。这些类实现了Property接口,但也有一个getReadOnlyProperty()方法,该方法返回一个与主Property同步的ReadOnlyProperty。当你需要一个完整的Property来实现一个组件,但是你只想把一个ReadOnlyProperty交给组件的客户端时,它们非常方便使用。 - 抽象类的
IntegerPropertyBase系列可以被扩展以提供完整的Property类的实现,尽管实际上SimpleIntegerProperty系列的类更容易使用。在IntegerPropertyBase系列类中唯一的抽象方法是getBean()和getName()。 - 可以扩展抽象类的
ReadOnlyIntegerPropertyBase系列来提供ReadOnlyProperty类的实现。这很少是必要的。在ReadOnlyIntegerPropertyBase系列的类中仅有的抽象方法是get()、getBean()和getName()。 - 在调用
addListener()之前,WeakInvalidationListener和WeakChangeListener类可以用来包装InvalidationListener和ChangeListener实例。它们保存包装的侦听器实例的弱引用。只要您持有对您这边的包装侦听器的引用,弱引用将保持活动状态,并且您将接收事件。当您使用完包装的侦听器并从您的一端取消引用它时,弱引用将符合垃圾收集的条件,然后再进行垃圾收集。所有 JavaFX 属性和绑定框架Observable对象都知道如何在弱引用被垃圾收集后清理弱侦听器。当侦听器在使用后没有被删除时,这可以防止内存泄漏。WeakInvalidationListener和WeakListener类实现了WeakListener接口,如果包装的监听器实例被垃圾收集,其wasGarbageCollected()方法将返回true。
这涵盖了驻留在javafx.beans、javafx.beans.property和javafx.beans.value包中的所有 JavaFX 属性和绑定 API,以及javafx.beans.binding包中的一些 API,但不是全部。javafx.beans.property.adapters包提供了旧式 JavaBeans 属性和 JavaFX 属性之间的适配器。我们将在“使 JavaBeans 属性适应 JavaFX 属性”一节中介绍这些适配器。javafx.beans.binding包的其余类是 API,帮助您从现有的属性和绑定中创建新的绑定。这是下一节的重点。
创建绑定
现在,我们将注意力转向从现有的属性和绑定中创建新的绑定。在本章前面的“理解关键接口和概念”一节中,您已经了解到绑定是一个可观察的值,它有一系列依赖项,这些依赖项也是可观察的值。
JavaFX 属性和绑定框架提供了三种创建新绑定的方法:
- 扩展了
IntegerBinding系列的抽象类。 - 使用绑定——在实用程序类
Bindings中创建静态方法。 - 使用
IntegerExpression系列抽象类提供的 fluent 接口 API。
您在“理解绑定接口”一节中看到了直接扩展方法。接下来我们将探索Bindings实用程序类。
了解绑定实用程序类
Bindings类包含 236 个工厂方法,这些方法利用现有的可观察值和常规值进行新的绑定。考虑到可观察值和常规 Java(不可观察)值都可以用于构建新的绑定,大多数方法都被重载。至少有一个参数必须是可观察值。下面是九个重载的add()方法的签名:
public static NumberBinding add(ObservableNumberValue n1, ObservableNumberValue n2)
public static DoubleBinding add(ObservableNumberValue n, double d)
public static DoubleBinding add(double d, ObservableNumberValue n)
public static NumberBinding add(ObservableNumberValue n, float f)
public static NumberBinding add(float f, ObservableNumberValue n)
public static NumberBinding add(ObservableNumberValue n, long l)
public static NumberBinding add(long l, ObservableNumberValue n)
public static NumberBinding add(ObservableNumberValue n, int i)
public static NumberBinding add(int i, ObservableNumberValue n)
当调用add()方法时,它返回一个NumberBinding,其依赖项包括所有可观察值参数,其值是其两个参数值的和。类似的重载方法也存在于subtract()、multiply()和divide()中。
Note
从上一节回忆起,ObservableIntegerValue、ObservableLongValue、ObservableFloatValue和ObservableDoubleValue是ObservableNumberValue的子类。所以刚才说的四种算术方法,可以取这些可观测数值的任意组合,也可以取任何不可观测值。
清单 3-5 中的程序使用Bindings中的算术方法计算笛卡尔平面中顶点为(x1, y1)、(x2, y2)、(x3, y3)的三角形的面积,使用以下公式:
Area = (x1*y2 + x2*y3 + x3*y1 – x1*y3 – x2*y1 – x3*y2) / 2
import javafx.beans.binding.Bindings;
import javafx.beans.binding.NumberBinding;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
public class TriangleAreaExample {
public static void main(String[] args) {
IntegerProperty x1 = new SimpleIntegerProperty(0);
IntegerProperty y1 = new SimpleIntegerProperty(0);
IntegerProperty x2 = new SimpleIntegerProperty(0);
IntegerProperty y2 = new SimpleIntegerProperty(0);
IntegerProperty x3 = new SimpleIntegerProperty(0);
IntegerProperty y3 = new SimpleIntegerProperty(0);
final NumberBinding x1y2 = Bindings.multiply(x1, y2);
final NumberBinding x2y3 = Bindings.multiply(x2, y3);
final NumberBinding x3y1 = Bindings.multiply(x3, y1);
final NumberBinding x1y3 = Bindings.multiply(x1, y3);
final NumberBinding x2y1 = Bindings.multiply(x2, y1);
final NumberBinding x3y2 = Bindings.multiply(x3, y2);
final NumberBinding sum1 = Bindings.add(x1y2, x2y3);
final NumberBinding sum2 = Bindings.add(sum1, x3y1);
final NumberBinding sum3 = Bindings.add(sum2, x3y1);
final NumberBinding diff1 = Bindings.subtract(sum3, x1y3);
final NumberBinding diff2 = Bindings.subtract(diff1, x2y1);
final NumberBinding determinant = Bindings.subtract(diff2, x3y2);
final NumberBinding area = Bindings.divide(determinant, 2.0D);
x1.set(0); y1.set(0);
x2.set(6); y2.set(0);
x3.set(4); y3.set(3);
printResult(x1, y1, x2, y2, x3, y3, area);
x1.set(1); y1.set(0);
x2.set(2); y2.set(2);
x3.set(0); y3.set(1);
printResult(x1, y1, x2, y2, x3, y3, area);
}
private static void printResult(IntegerProperty x1, IntegerProperty y1,
IntegerProperty x2, IntegerProperty y2,
IntegerProperty x3, IntegerProperty y3,
NumberBinding area) {
System.out.println("For A(" +
x1.get() + "," + y1.get() + "), B(" +
x2.get() + "," + y2.get() + "), C(" +
x3.get() + "," + y3.get() + "), the area of triangle ABC is " + area.getValue());
}
}
Listing 3-5.
TriangleAreaExample.java
我们用IntegerProperty来表示坐标。构建NumberBinding area使用了Bindings的所有四种算术工厂方法。因为我们从IntegerProperty对象开始,即使来自Bindings的算术工厂方法的返回类型是NumberBinding,实际返回的对象,直到determinant,都是IntegerBinding对象。我们在divide()调用中使用了2.0D而不仅仅是2来强制划分为double划分,而不是int划分。我们构建的所有属性和绑定形成一个树形结构,以area为根,中间绑定为内部节点,属性x1、y1、x2、y2、x3、y3为叶。如果我们使用正则算术表达式的语法来解析面积公式的数学表达式,这个树类似于我们将得到的解析树。
当我们运行清单 3-5 中的程序时,以下输出被打印到控制台:
For A(0,0), B(6,0), C(4,3), the area of triangle ABC is 9.0
For A(1,0), B(2,2), C(0,1), the area of triangle ABC is 1.5
除了算术方法之外,Bindings类还有以下工厂方法。
- 逻辑运算符:
and、or、not - 数字运算符:
min、max、negate - 对象运算符:
isNull,isNotNull - 字符串运算符:
length、isEmpty、isNotEmpty - 关系运算符:
equalequalIgnoreCasegreaterThangreaterThanOrEquallessThanlessThanOrEqualnotEqualnotEqualIgnoreCase
- 创建运算符:
createBooleanBindingcreateIntegerBindingcreateLongBindingcreateFloatBindingcreateDoubleBindingcreateStringBindingcreateObjectBinding
- 选择运算符:
selectselectBooleanselectIntegerselectLongselectFloatselectDoubleselectString
除了创建操作符和选择操作符,前面的操作符都执行您认为它们会执行的操作。对象运算符仅对可观察的字符串值和可观察的对象值有意义。字符串运算符仅对可观察的字符串值有意义。除了IgnoreCase以外的所有关系运算符都适用于数值。在比较float或double值时,数值的equal和notEqual操作符有第三个double公差参数。equal和notEqual操作符也适用于boolean、字符串和对象值。对于字符串和对象值,equal和notEqual操作符使用equals()方法比较它们的值。
创建操作符提供了一种无需直接扩展抽象基类就能创建绑定的便捷方式。它接受一个Callable和任意数量的依赖项作为参数。清单 3-3 中的区域双重绑定可以使用 lambda 表达式作为Callable重写,如下所示:
DoubleBinding area = Bindings.createDoubleBinding(() -> {
return x.get() * y.get();
}, x, y);
选择操作符对所谓的 Java FX bean 进行操作,Java bean 是根据 Java FX bean 规范构造的 Java 类。我们将在本章后面的“理解 Java FX bean 约定”一节中讨论 Java FX bean。
在Bindings中有许多处理可观察集合的方法。我们将在第七章中介绍它们。
这涵盖了Bindings中返回绑定对象的所有方法。Bindings中有 18 个方法不返回绑定对象。各种bindBidirectional()和unbindBidirectional()方法创建双向绑定。事实上,各种属性类中的bindBidirectional()和unbindBidirectional()方法简单地调用了Bindings类中相应的方法。bindContent()和unbindContent()方法将一个普通集合绑定到一个可观察的集合。convert()、concat()和一对重载的format()方法返回StringExpression对象。最后,when()方法返回一个When对象。
When和StringExpression类是创建绑定的 fluent 接口 API 的一部分,我们将在下一小节中介绍。
了解 Fluent 接口 API
如果你问,“为什么有人会给一个方法命名为when()?”以及“When类会封装什么样的信息?”欢迎加入俱乐部。当您没有注意到的时候,面向对象编程社区发明了一种全新的 API 设计方法,它完全无视几十年来面向对象实践的原则。这种新方法不是封装数据和将业务逻辑分布到相关的域对象中,而是产生一种 API 风格,它鼓励方法链接,并使用一种方法的返回类型来确定哪种方法可用于火车的下一节车厢。选择方法名称不是为了传达完整的意思,而是为了让整个方法链读起来像一个流畅的句子。这种风格被称为流畅的界面 API。
Note
你可以在 Martin Fowler 的网站上找到关于 fluent 接口的更全面的阐述,在本章的最后引用。
用于创建绑定的 fluent 接口 API 在IntegerExpression系列的类中定义。IntegerExpression是IntegerProperty和IntegerBinding的超类,使得IntegerExpression的方法在IntegerProperty和IntegerBinding类中也可用。四个数值表达式类共享一个公共的超接口NumberExpression,所有的方法都在这里定义。特定于类型的表达式类覆盖了一些产生NumberBinding的方法,以返回更合适的绑定类型。
下面列出了可用于七种属性和绑定的方法:
- 对于
BooleanProperty和BooleanBindingBooleanBinding and(ObservableBooleanValue b)BooleanBinding or(ObservableBooleanValue b)BooleanBinding not()BooleanBinding isEqualTo(ObservableBooleanValue b)BooleanBinding isNotEqualTo(ObservableBooleanValue b)StringBinding asString()
- 适用于所有数字属性和绑定
BooleanBinding isEqualTo(ObservableNumberValue m)BooleanBinding isEqualTo(ObservableNumberValue m, double err)BooleanBinding isEqualTo(double d, double err)BooleanBinding isEqualTo(float f, double err)BooleanBinding isEqualTo(long l)BooleanBinding isEqualTo(long l, double err)BooleanBinding isEqualTo(int i)BooleanBinding isEqualTo(int i, double err)BooleanBinding isNotEqualTo(ObservableNumberValue m)BooleanBinding isNotEqualTo(ObservableNumberValue m, double err)BooleanBinding isNotEqualTo(double d, double err)BooleanBinding isNotEqualTo(float f, double err)BooleanBinding isNotEqualTo(long l)BooleanBinding isNotEqualTo(long l, double err)BooleanBinding isNotEqualTo(int i)BooleanBinding isNotEqualTo(int i, double err)BooleanBinding greaterThan(ObservableNumberValue m)BooleanBinding greaterThan(double d)BooleanBinding greaterThan(float f)BooleanBinding greaterThan(long l)BooleanBinding greaterThan(int i)BooleanBinding lessThan(ObservableNumberValue m)BooleanBinding lessThan(double d)BooleanBinding lessThan(float f)BooleanBinding lessThan(long l)BooleanBinding lessThan(int i)BooleanBinding greaterThanOrEqualTo(ObservableNumberValue m)BooleanBinding greaterThanOrEqualTo(double d)BooleanBinding greaterThanOrEqualTo(float f)BooleanBinding greaterThanOrEqualTo(long l)BooleanBinding greaterThanOrEqualTo(int i)BooleanBinding lessThanOrEqualTo(ObservableNumberValue m)BooleanBinding lessThanOrEqualTo(double d)BooleanBinding lessThanOrEqualTo(float f)BooleanBinding lessThanOrEqualTo(long l)BooleanBinding lessThanOrEqualTo(int i)StringBinding asString()StringBinding asString(String str)StringBinding asString(Locale locale, String str)
- 对于
IntegerProperty和IntegerBindingIntegerBinding negate()NumberBinding add(ObservableNumberValue n)DoubleBinding add(double d)FloatBinding add(float f)LongBinding add(long l)IntegerBinding add(int i)NumberBinding subtract(ObservableNumberValue n)DoubleBinding subtract(double d)FloatBinding subtract(float f)LongBinding subtract(long l)IntegerBinding subtract(int i)NumberBinding multiply(ObservableNumberValue n)DoubleBinding multiply(double d)FloatBinding multiply(float f)LongBinding multiply(long l)IntegerBinding multiply(int i)NumberBinding divide(ObservableNumberValue n)DoubleBinding divide(double d)FloatBinding divide(float f)LongBinding divide(long l)IntegerBinding divide(int i)
- 对于
LongProperty和LongBindingLongBinding negate()NumberBinding add(ObservableNumberValue n)DoubleBinding add(double d)FloatBinding add(float f)LongBinding add(long l)LongBinding add(int i)NumberBinding subtract(ObservableNumberValue n)DoubleBinding subtract(double d)FloatBinding subtract(float f)LongBinding subtract(long l)LongBinding subtract(int i)NumberBinding multiply(ObservableNumberValue n)DoubleBinding multiply(double d)FloatBinding multiply(float f)LongBinding multiply(long l)LongBinding multiply(int i)NumberBinding divide(ObservableNumberValue n)DoubleBinding divide(double d)FloatBinding divide(float f)LongBinding divide(long l)LongBinding divide(int i)
- 对于
FloatProperty和FloatBindingFloatBinding negate()NumberBinding add(ObservableNumberValue n)DoubleBinding add(double d)FloatBinding add(float g)FloatBinding add(long l)FloatBinding add(int i)NumberBinding subtract(ObservableNumberValue n)DoubleBinding subtract(double d)FloatBinding subtract(float g)FloatBinding subtract(long l)FloatBinding subtract(int i)NumberBinding multiply(ObservableNumberValue n)DoubleBinding multiply(double d)FloatBinding multiply(float g)FloatBinding multiply(long l)FloatBinding multiply(int i)NumberBinding divide(ObservableNumberValue n)DoubleBinding divide(double d)FloatBinding divide(float g)FloatBinding divide(long l)FloatBinding divide(int i)
- 对于
DoubleProperty和DoubleBindingDoubleBinding negate()DoubleBinding add(ObservableNumberValue n)DoubleBinding add(double d)DoubleBinding add(float f)DoubleBinding add(long l)DoubleBinding add(int i)DoubleBinding subtract(ObservableNumberValue n)DoubleBinding subtract(double d)DoubleBinding subtract(float f)DoubleBinding subtract(long l)DoubleBinding subtract(int i)DoubleBinding multiply(ObservableNumberValue n)DoubleBinding multiply(double d)DoubleBinding multiply(float f)DoubleBinding multiply(long l)DoubleBinding multiply(int i)DoubleBinding divide(ObservableNumberValue n)DoubleBinding divide(double d)DoubleBinding divide(float f)DoubleBinding divide(long l)DoubleBinding divide(int i)
- 对于
StringProperty和StringBindingStringExpression concat(Object obj)BooleanBinding isEqualTo(ObservableStringValue str)BooleanBinding isEqualTo(String str)BooleanBinding isNotEqualTo(ObservableStringValue str)BooleanBinding isNotEqualTo(String str)BooleanBinding isEqualToIgnoreCase(ObservableStringValue str)BooleanBinding isEqualToIgnoreCase(String str)BooleanBinding isNotEqualToIgnoreCase(ObservableStringValue str)BooleanBinding isNotEqualToIgnoreCase(String str)BooleanBinding greaterThan(ObservableStringValue str)BooleanBinding greaterThan(String str)BooleanBinding lessThan(ObservableStringValue str)BooleanBinding lessThan(String str)BooleanBinding greaterThanOrEqualTo(ObservableStringValue str)BooleanBinding greaterThanOrEqualTo(String str)BooleanBinding lessThanOrEqualTo(ObservableStringValue str)BooleanBinding lessThanOrEqualTo(String str)BooleanBinding isNull()BooleanBinding isNotNull()IntegerBinding length()BooleanExpression isEmpty()BooleanExpression isNotEmpty()
- 对于
ObjectProperty和ObjectBindingBooleanBinding isEqualTo(ObservableObjectValue<?> obj)BooleanBinding isEqualTo(Object obj)BooleanBinding isNotEqualTo(ObservableObjectValue<?> obj)BooleanBinding isNotEqualTo(Object obj)BooleanBinding isNull()BooleanBinding isNotNull()
使用这些方法,您可以创建无限多种绑定,方法是从属性开始,调用适合该属性类型的方法之一来获取绑定,调用适合该绑定类型的方法之一来获取另一个绑定,依此类推。这里值得指出的一个事实是,特定于类型的数值表达式的所有方法都是在返回类型为NumberBinding的NumberExpression基本接口中定义的,并且在具有相同参数签名但返回类型更特定的特定于类型的表达式类中被覆盖。这种用相同的参数签名但更具体的返回类型覆盖子类中的方法的方式被称为协变返回类型覆盖,并且自 Java 5 以来一直是 Java 语言的一个特性。这一事实的结果之一是,用 fluent 接口 API 构建的数字绑定比用Bindings类中的工厂方法构建的绑定有更多特定的类型。
有时有必要将特定于类型的表达式转换为保存相同类型值的对象表达式。这可以通过特定于类型的表达式类中的asObject()方法来完成。可以使用 expressions 类中的静态方法进行转换。对于IntegerExpression,这些静态方法如下:
static IntegerExpression integerExpression(ObservableIntegerValue value)
static <T extends java.lang.Number> IntegerExpression integerExpression(ObservableValue<T> value)
清单 3-6 中的程序是对清单 3-5 中三角形区域示例的修改,它使用了流畅的接口 API,而不是调用Bindings类中的工厂方法。
import javafx.beans.binding.Bindings;
import javafx.beans.binding.NumberBinding;
import javafx.beans.binding.StringExpression;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
public class TriangleAreaFluentExample {
public static void main(String[] args) {
IntegerProperty x1 = new SimpleIntegerProperty(0);
IntegerProperty y1 = new SimpleIntegerProperty(0);
IntegerProperty x2 = new SimpleIntegerProperty(0);
IntegerProperty y2 = new SimpleIntegerProperty(0);
IntegerProperty x3 = new SimpleIntegerProperty(0);
IntegerProperty y3 = new SimpleIntegerProperty(0);
final NumberBinding area = x1.multiply(y2)
.add(x2.multiply(y3))
.add(x3.multiply(y1))
.subtract(x1.multiply(y3))
.subtract(x2.multiply(y1))
.subtract(x3.multiply(y2))
.divide(2.0D);
StringExpression output = Bindings.format(
"For A(%d,%d), B(%d,%d), C(%d,%d), the area of triangle ABC is %3.1f",
x1, y1, x2, y2, x3, y3, area);
x1.set(0); y1.set(0);
x2.set(6); y2.set(0);
x3.set(4); y3.set(3);
System.out.println(output.get());
x1.set(1); y1.set(0);
x2.set(2); y2.set(2);
x3.set(0); y3.set(1);
System.out.println(output.get());
}
}
Listing 3-6.
TriangleAreaFluentExample.java
请注意清单 3-5 中用于构建区域绑定的 13 行代码和 12 个中间变量是如何减少到清单 3-6 中不使用中间变量的 7 行代码的。我们还使用了Bindings.format()方法来构建一个名为output的StringExpression对象。有两个带签名的重载Bindings.format()方法:
StringExpression format(Locale locale, String format, Object... args)
StringExpression format(String format, Object... args)
它们的工作方式与相应的String.format()方法类似,它们根据格式规范format和Locale locale或者默认的Locale对值args进行格式化。如果args中的任何一个是ObservableValue,其变化将反映在StringExpression中。
当我们运行清单 3-6 中的程序时,以下输出被打印到控制台:
For A(0,0), B(6,0), C(4,3), the area of triangle ABC is 9.0
For A(1,0), B(2,2), C(0,1), the area of triangle ABC is 1.5
接下来,我们将揭开When类的神秘面纱,以及它在构建本质上是 if/then/else 表达式的绑定中所扮演的角色。When类有一个接受ObservableBooleanValue参数的构造器:
public When(ObservableBooleanValue b)
它有以下 11 个重载的then()方法。
When.NumberConditionBuilder then(ObservableNumberValue n)
When.NumberConditionBuilder then(double d)
When.NumberConditionBuilder then(float f)
When.NumberConditionBuilder then(long l)
When.NumberConditionBuilder then(int i)
When.BooleanConditionBuilder then(ObservableBooleanValue b)
When.BooleanConditionBuilder then(boolean b)
When.StringConditionBuilder then(ObservableStringValue str)
When.StringConditionBuilder then(String str)
When.ObjectConditionBuilder<T> then(ObservableObjectValue<T> obj)
When.ObjectConditionBuilder<T> then(T obj)
从then()方法返回的对象类型取决于参数的类型。如果参数是数值类型,无论是可观察的还是不可观察的,返回类型都是嵌套类When.NumberConditionBuilder。同样,对于布尔参数,返回类型是When.BooleanConditionBuilder;对于字符串参数,When.StringConditionBuilder;而对于对象论证,When.ObjectConditionBuilder。
这些条件构建器又有下面的otherwise()方法。
- 对于
When.NumberConditionBuilderNumberBinding otherwise(ObservableNumberValue n)DoubleBinding otherwise(double d)NumberBinding otherwise(float f)NumberBinding otherwise(long l)NumberBinding otherwise(int i)
- 对于
When.BooleanConditionBuilderBooleanBinding otherwise(ObservableBooleanValue b)BooleanBindingotherwise(boolean b)
- 对于
When.StringConditionBuilderStringBinding otherwise(ObservableStringValue str)StringBinding otherwise(String str)
- 对于
When.ObjectConditionBuilderObjectBinding<T> otherwise(ObservableObjectValue<T> obj)ObjectBinding<T> otherwise(T obj)
这些方法签名的最终效果是,您可以通过以下方式构建一个类似于 if/then/else 表达式的绑定:
new When(b).then(x).otherwise(y)
b是一个ObservableBooleanValue,x和y是类似的类型,可以是可观测的,也可以是不可观测的。最终的绑定将是类似于x和y的类型。
清单 3-7 中的程序使用来自When类的 fluent 接口 API 来计算给定边a、b和c的三角形的面积。回想一下,要形成三角形,三条边必须满足以下条件:
a + b > c, b + c > a, c + a > b.
当满足上述条件时,可以使用 Heron 公式计算三角形的面积:
Area = sqrt(s * (s – a) * (s – b) * (s – c))
其中s是半参数:
s = (a + b + c) / 2.
import javafx.beans.binding.DoubleBinding;
import javafx.beans.binding.When;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
public class HeronsFormulaExample {
public static void main(String[] args) {
DoubleProperty a = new SimpleDoubleProperty(0);
DoubleProperty b = new SimpleDoubleProperty(0);
DoubleProperty c = new SimpleDoubleProperty(0);
DoubleBinding s = a.add(b).add(c).divide(2.0D);
final DoubleBinding areaSquared = new When(
a.add(b).greaterThan(c)
.and(b.add(c).greaterThan(a))
.and(c.add(a).greaterThan(b)))
.then(s.multiply(s.subtract(a))
.multiply(s.subtract(b))
.multiply(s.subtract(c)))
.otherwise(0.0D);
a.set(3);
b.set(4);
c.set(5);
System.out.printf("Given sides a = %1.0f, b = %1.0f, and c = %1.0f," +
" the area of the triangle is %3.2f\n", a.get(), b.get(), c.get(),
Math.sqrt(areaSquared.get()));
a.set(2);
b.set(2);
c.set(2);
System.out.printf("Given sides a = %1.0f, b = %1.0f, and c = %1.0f," +
" the area of the triangle is %3.2f\n", a.get(), b.get(), c.get(),
Math.sqrt(areaSquared.get()));
}
}
Listing 3-7.
HeronsFormulaExample.java
由于DoubleExpression中没有现成的绑定方法来计算平方根,我们为areaSquared创建了一个DoubleBinding。When()的构造器参数是由a、b和c三个条件构建的BooleanBinding。then()方法的参数是计算三角形面积平方的DoubleBinding。因为then()参数是数字,所以otherwise()参数也必须是数字。我们选择使用0.0D来表示遇到了无效的三角形。
Note
除了使用When()构造器,还可以使用Bindings实用程序类中的工厂方法when()来创建When对象。
当我们运行清单 3-7 中的程序时,以下输出被打印到控制台:
Given sides a = 3, b = 4, and c = 5, the area of the triangle is 6.00.
Given sides a = 2, b = 2, and c = 2, the area of the triangle is 1.73.
如果清单 3-7 中定义的绑定让您有点晕头转向,您并不孤单。我们选择这个例子只是为了说明由When类提供的 fluent 接口 API 的使用。事实上,我们在“理解绑定接口”一节中首次介绍的直接子类化方法可能更适合这个例子。
清单 3-8 中的程序通过使用直接扩展方法解决了与清单 3-7 相同的问题。
import javafx.beans.binding.DoubleBinding;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
public class HeronsFormulaDirectExtensionExample {
public static void main(String[] args) {
final DoubleProperty a = new SimpleDoubleProperty(0);
final DoubleProperty b = new SimpleDoubleProperty(0);
final DoubleProperty c = new SimpleDoubleProperty(0);
DoubleBinding area = new DoubleBinding() {
{
super.bind(a, b, c);
}
@Override
protected double computeValue() {
double a0 = a.get();
double b0 = b.get();
double c0 = c.get();
if ((a0 + b0 > c0) && (b0 + c0 > a0) && (c0 + a0 > b0)) {
double s = (a0 + b0 + c0) / 2.0D;
return Math.sqrt(s * (s - a0) * (s - b0) * (s - c0));
} else {
return 0.0D;
}
}
};
a.set(3);
b.set(4);
c.set(5);
System.out.printf("Given sides a = %1.0f, b = %1.0f, and c = %1.0f," +
" the area of the triangle is %3.2f\n", a.get(), b.get(), c.get(),
area.get());
a.set(2);
b.set(2);
c.set(2);
System.out.printf("Given sides a = %1.0f, b = %1.0f, and c = %1.0f," +
" the area of the triangle is %3.2f\n", a.get(), b.get(), c.get(),
area.get());
}
}
Listing 3-8.
HeronsFormulaDirectExtensionExample.java
对于复杂的表达式和超出可用运算符范围的表达式,首选直接扩展方法。
现在,您已经掌握了javafx.beans、javafx.beans.binding、javafx.beans.property和javafx.beans.value包中的所有 API,您已经准备好超越 JavaFX 属性和绑定框架的细节,并学习如何将这些属性组织成称为 JavaFX Beans 的更大的组件。
了解 JavaFX Beans 约定
JavaFX 引入了 JavaFX Beans 的概念,这是一组为 Java 对象提供属性支持的约定。在本节中,我们将讨论指定 JavaFX Beans 属性的命名约定、实现 JavaFX Beans 属性的几种方法,以及选择绑定的使用。
JavaFX Beans 规范
多年来,Java 一直使用 JavaBeans API 来表示对象的属性。JavaBeans 属性由一对 getter 和 setter 方法表示。属性更改通过激发 setter 代码中的属性更改事件传播到属性更改侦听器。
JavaFX 引入了 JavaFX Beans 规范,该规范通过 JavaFX 属性和绑定框架中的属性类的帮助,为 Java 对象添加了属性支持。
Caution
财产这个词在这里有两种不同的含义。当我们说“JavaFX Beans 属性”时,应该理解为是指类似于 JavaBeans 属性的更高层次的概念。当我们说“JavaFX 属性和绑定框架属性”时,应该理解为是指Property或ReadOnlyProperty接口的各种实现,比如IntegerProperty、StringProperty等等。JavaFX Beans 属性是使用 JavaFX 属性和绑定框架属性指定的。
像它们的 JavaBeans 对应物一样,JavaFX Beans 属性是由 Java 类中的一组方法指定的。要在 Java 类中定义 JavaFX Beans 属性,需要提供三个方法:getter、setter 和属性 getter。对于类型为double的名为height的属性,有三种方法:
public final double getHeight();
public final void setHeight(double h);
public DoubleProperty heightProperty();
getter 和 setter 方法的名称遵循 JavaBeans 约定。它们是通过将“get”和“set”与首字母大写的属性名称连接起来获得的。对于boolean类型属性,getter 名称也可以以is开头。属性 getter 的名称是通过将属性的名称与“Property”连接起来获得的。要定义一个只读的 JavaFX Beans 属性,您可以移除 setter 方法,或者将其更改为私有方法,并将属性 getter 的返回类型更改为ReadOnlyProperty。
该规范仅涉及 JavaFX Beans 属性的接口,并没有强加任何实现约束。根据 JavaFX Bean 可能拥有的属性数量以及这些属性的使用模式,有几种实现策略。毫不奇怪,它们都使用 JavaFX 属性和绑定框架属性作为 JavaFX Beans 属性值的后备存储。我们将在接下来的两个小节中向您展示这些策略。
理解急切实例化的属性策略
热切实例化属性策略是实现 JavaFX Beans 属性的最简单方式。对于要在对象中定义的每个 JavaFX Beans 属性,您需要在类中引入一个私有字段,该字段属于适当的 JavaFX 属性和绑定框架属性类型。这些私有字段在 bean 构建时被实例化。getter 和 setter 方法简单地调用私有字段的get()和set()方法。属性 getter 只是返回私有字段本身。
清单 3-9 中的程序定义了一个具有int属性i、String属性str和Color属性color的 JavaFX Bean。
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.scene.paint.Color;
public class JavaFXBeanModelExample {
private IntegerProperty i = new SimpleIntegerProperty(this, "i", 0);
private StringProperty str = new SimpleStringProperty(this, "str", "Hello");
private ObjectProperty<Color> color = new SimpleObjectProperty<Color>(this, "color",
Color.BLACK);
public final int getI() {
return i.get();
}
public final void setI(int i) {
this.i.set(i);
}
public IntegerProperty iProperty() {
return i;
}
public final String getStr() {
return str.get();
}
public final void setStr(String str) {
this.str.set(str);
}
public StringProperty strProperty() {
return str;
}
public final Color getColor() {
return color.get();
}
public final void setColor(Color color) {
this.color.set(color);
}
public ObjectProperty<Color> colorProperty() {
return color;
}
}
Listing 3-9.
JavaFXBeanModelExample.java
这是一个简单的 Java 类。在这个实现中,我们只想指出两件事。首先,按照惯例,getter 和 setter 方法被声明为final。第二,当私有字段被初始化时,我们用完整的上下文信息调用简单的属性构造器,并把this作为第一个参数提供给它们。在本章之前的所有例子中,我们使用null作为简单属性构造器的第一个参数,因为这些属性不是更高级 JavaFX Bean 对象的一部分。
清单 3-10 中的程序定义了一个视图类,它监视清单 3-9 中定义的 JavaFX Bean 的一个实例。它通过连接更改监听器来观察 bean 的i、str和color属性的更改,这些监听器将任何更改打印到控制台。
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.paint.Color;
public class JavaFXBeanViewExample {
private JavaFXBeanModelExample model;
public JavaFXBeanViewExample(JavaFXBeanModelExample model) {
this.model = model;
hookupChangeListeners();
}
private void hookupChangeListeners() {
model.iProperty().addListener(new ChangeListener<Number>() {
@Override
public void changed(ObservableValue<? extends Number> observableValue, Number
oldValue, Number newValue) {
System.out.println("Property i changed: old value = " + oldValue + ", new
value = " + newValue);
}
});
model.strProperty().addListener(new ChangeListener<String>() {
@Override
public void changed(ObservableValue<? extends String> observableValue, String
oldValue, String newValue) {
System.out.println("Property str changed: old value = " + oldValue + ", new
value = " + newValue);
}
});
model.colorProperty().addListener(new ChangeListener<Color>() {
@Override
public void changed(ObservableValue<? extends Color> observableValue, Color
oldValue, Color newValue) {
System.out.println("Property color changed: old value = " + oldValue + ",
new value = " + newValue);
}
});
}
}
Listing 3-10.
JavaFXBeanViewExample.java
清单 3-11 中的程序定义了一个可以修改模型对象的控制器。
import javafx.scene.paint.Color;
public class JavaFXBeanControllerExample {
private JavaFXBeanModelExample model;
private JavaFXBeanViewExample view;
public JavaFXBeanControllerExample(JavaFXBeanModelExample model, JavaFXBeanViewExampleÉ
view) {
this.model = model;
this.view = view;
}
public void incrementIPropertyOnModel() {
model.setI(model.getI() + 1);
}
public void changeStrPropertyOnModel() {
final String str = model.getStr();
if (str.equals("Hello")) {
model.setStr("World");
} else {
model.setStr("Hello");
}
}
public void switchColorPropertyOnModel() {
final Color color = model.getColor();
if (color.equals(Color.BLACK)) {
model.setColor(Color.WHITE);
} else {
model.setColor(Color.BLACK);
}
}
}
Listing 3-11.
JavaFXBeanControllerExample.java
请注意,这不是一个成熟的控制器,它对视图对象的引用不做任何事情。清单 3-12 中的程序提供了一个主程序,它以典型的模型-视图-控制器模式组装并测试驱动清单 3-9 到 3-11 中的类。
public class JavaFXBeanMainExample {
public static void main(String[] args) {
JavaFXBeanModelExample model = new JavaFXBeanModelExample();
JavaFXBeanViewExample view = new JavaFXBeanViewExample(model);
JavaFXBeanControllerExample controller = new JavaFXBeanControllerExample(model, view);
controller.incrementIPropertyOnModel();
controller.changeStrPropertyOnModel();
controller.switchColorPropertyOnModel();
controller.incrementIPropertyOnModel();
controller.changeStrPropertyOnModel();
controller.switchColorPropertyOnModel();
}
}
Listing 3-12.
JavaFXbeanMainExample.java
当我们运行清单 3-9 到 3-12 中的程序时,以下输出被打印到控制台:
Property i changed: old value = 0, new value = 1
Property str changed: old value = Hello, new value = World
Property color changed: old value = 0x000000ff, new value = 0xffffffff
Property i changed: old value = 1, new value = 2
Property str changed: old value = World, new value = Hello
Property color changed: old value = 0xffffffff, new value = 0x000000ff
理解延迟实例化属性策略
如果您的 JavaFX Bean 有许多属性,那么在 Bean 创建时预先实例化所有的 properties 对象可能是一种过于繁重的方法。如果只有少数属性被实际使用,那么所有 properties 对象的内存都被浪费了。在这种情况下,您可以使用几个延迟实例化的属性策略之一。两种典型的策略是半懒惰实例化策略和全懒惰实例化策略。
在半懒惰策略中,只有在使用不同于默认值的值调用 setter 时,或者在调用属性 getter 时,属性对象才会被实例化。清单 3-13 中的程序说明了这个策略是如何实现的。
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
public class JavaFXBeanModelHalfLazyExample {
private static final String DEFAULT_STR = "Hello";
private StringProperty str;
public final String getStr() {
if (str != null) {
return str.get();
} else {
return DEFAULT_STR;
}
}
public final void setStr(String str) {
if ((this.str != null) || !(str.equals(DEFAULT_STR))) {
strProperty().set(str);
}
}
public StringProperty strProperty() {
if (str == null) {
str = new SimpleStringProperty(this, "str", DEFAULT_STR);
}
return str;
}
}
Listing 3-13.
JavaFXBeanModelHalfLazyExample.java
在这种策略中,客户端代码可以多次调用 getter,而无需实例化属性对象。如果 property 对象为空,getter 只返回默认值。一旦用一个不同于默认值的值调用 setter,它将调用属性 getter,该属性 getter 惰性地实例化属性对象。如果客户端代码直接调用属性 getter,属性对象也会被实例化。
在全懒策略中,只有在调用属性 getter 时,属性对象才会被实例化。只有当属性对象已经被实例化时,getter 和 setter 才会检查它;否则,它们会通过一个单独的字段。
清单 3-14 中的程序展示了一个全懒惰属性的例子。
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
public class JavaFXBeanModelFullLazyExample {
private static final String DEFAULT_STR = "Hello";
private StringProperty str;
private String _str = DEFAULT_STR;
public final String getStr() {
if (str != null) {
return str.get();
} else {
return _str;
}
}
public final void setStr(String str) {
if (this.str != null) {
this.str.set(str);
} else {
_str = str;
}
}
public StringProperty strProperty() {
if (str == null) {
str = new SimpleStringProperty(this, "str", _str);
}
return str;
}
}
Listing 3-14.
JavaFXBeanModelFullLazyExample.java
Caution
全惰性实例化策略会产生额外的字段开销,以稍微延长对属性实例化的需求。类似地,半懒惰和全懒惰实例化策略都要付出实现复杂性和运行时性能的代价,以获得潜在的运行时内存占用减少的好处。这是软件工程中一个经典的权衡情况。您选择哪种策略将取决于您的应用环境。我们的建议是,只有在需要的时候才引入优化。
使用选择绑定
正如您在“理解绑定实用程序类”一节中看到的,Bindings实用程序类包含七个选择操作符。这些运算符的方法签名是:
select(Object root, String… steps)selectBoolean(Object root, String… steps)selectDouble(Object root, String… steps)selectFloat(Object root, String… steps)selectInteger(Object root, String… steps)selectLong(Object root, String… steps)selectString(Object root, String… steps)
这些选择操作符允许您创建观察深度嵌套的 JavaFX Beans 属性的绑定。假设您有一个具有属性的 JavaFX bean,其类型是具有属性的 JavaFX bean,其类型是具有属性的 JavaFX bean,依此类推。假设你正在通过一个ObjectProperty观察这个属性链的根。然后,您可以创建一个绑定来观察深度嵌套的 JavaFX Beans 属性,方法是调用一个 select 方法,该方法的类型与深度嵌套的 JavaFX Beans 属性的类型相匹配,并将ObjectProperty作为根,将到达深度嵌套的 JavaFX Beans 属性的后续 JavaFX Beans 属性名称作为其余参数。
Note
还有另一组选择方法,将一个ObservableValue作为第一个参数。它们是在 JavaFX 2.0 中引入的。以Object作为第一个参数的 select 方法集允许我们使用任何 Java 对象,而不仅仅是 JavaFX Beans,作为选择绑定的根。
在下面的例子中,我们使用了javafx.scene.effect包中的几个类——Lighting和Light——来说明选择操作符是如何工作的。在本书后面的章节中,我们会教你如何将光照应用到 JavaFX 场景图中。目前,我们感兴趣的是,Lighting是一个 JavaFX bean,它有一个名为light的属性,其类型为Light,而Light也是一个 JavaFX bean,它有一个名为color的属性,其类型为Color(在javafx.scene.paint中)。
清单 3-15 中的程序说明了如何观察灯光的颜色。
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.effect.Light;
import javafx.scene.effect.Lighting;
import javafx.scene.paint.Color;
public class SelectBindingExample {
public static void main(String[] args) {
ObjectProperty<Lighting> root = new SimpleObjectProperty<>(new Lighting());
final ObjectBinding<Color> selectBinding = Bindings.select(root, "light", "color");
selectBinding.addListener(new ChangeListener<Color>() {
@Override
public void changed(ObservableValue<? extends Color> observableValue, Color
oldValue, Color newValue) {
System.out.println("\tThe color changed:\n\t\told color = " +
oldValue + ",\n\t\tnew color = " + newValue);
}
});
System.out.println("firstLight is black.");
Light firstLight = new Light.Point();
firstLight.setColor(Color.BLACK);
System.out.println("secondLight is white.");
Light secondLight = new Light.Point();
secondLight.setColor(Color.WHITE);
System.out.println("firstLighting has firstLight.");
Lighting firstLighting = new Lighting();
firstLighting.setLight(firstLight);
System.out.println("secondLighting has secondLight.");
Lighting secondLighting = new Lighting();
secondLighting.setLight(secondLight);
System.out.println("Making root observe firstLighting.");
root.set(firstLighting);
System.out.println("Making root observe secondLighting.");
root.set(secondLighting);
System.out.println("Changing secondLighting's light to firstLight");
secondLighting.setLight(firstLight);
System.out.println("Changing firstLight's color to red");
firstLight.setColor(Color.RED);
}
}
Listing 3-15.
SelectBindingExample.java
在这个例子中,root是观察Lighting物体的ObjectProperty。绑定colorBinding观察Lighting对象的light属性的color属性,即root的值。然后,我们创建了一些Light和Lighting对象,并以各种方式更改了它们的配置。
当我们运行清单 3-15 中的程序时,以下输出被打印到控制台:
firstLight is black.
secondLight is white.
firstLighting has firstLight.
secondLighting has secondLight.
Making root observe firstLighting.
The color changed:
old color = 0xffffffff,
new color = 0x000000ff
Making root observe secondLighting.
The color changed:
old color = 0x000000ff,
new color = 0xffffffff
Changing secondLighting's light to firstLight
The color changed:
old color = 0xffffffff,
new color = 0x000000ff
Changing firstLight's color to red
The color changed:
old color = 0x000000ff,
new color = 0xff0000ff
不出所料,root观察到的物体配置的每一个变化都会触发一个变化事件,colorBinding的值总是反映当前Lighting物体在root中的光线颜色。
Caution
如果提供的属性名与 JavaFX bean 中的任何属性名都不匹配,JavaFX 属性和绑定框架不会发出任何警告。它将只包含类型的默认值:null表示对象类型,零表示数值类型,false表示boolean类型,空字符串表示字符串类型。
使 JavaBeans 属性适应 JavaFX 属性
自 JavaBeans 规范发布以来的许多年里,为各种项目、产品和库编写了许多 JavaBeans。为了更好地帮助 Java 开发人员利用这些 JavaBean,在javafx.beans.properties.adapter包中提供了一组适配器,通过在 JavaBeans 属性之外创建一个 JavaFX 属性,使它们在 JavaFX 世界中有用。
在本节中,我们首先通过一个简单的例子简要回顾 JavaBeans 规范对属性、绑定属性和约束属性的定义。然后,我们向您展示如何使用适配器从 JavaBeans 属性创建 JavaFX 属性。
了解 JavaBeans 属性
JavaBeans 属性是使用熟悉的 getter 和 setter 命名约定定义的。如果只提供了 getter,则属性是“只读”的,如果同时提供了 getter 和 setter,则属性是“读/写”的。JavaBeans 事件由事件对象、事件监听器接口和 JavaBean 上的监听器注册方法组成。JavaBeans 属性可以使用两种特殊的事件:当 JavaBeans 属性改变时,可以触发一个PropertyChange事件;当 JavaBeans 属性改变时,也可以触发一个VetoableChange事件;而如果监听器抛出一个PropertyVetoException,属性更改应该不会生效。setter 触发PropertyChange事件的属性称为绑定属性。其 setter 触发VetoableChange事件的属性称为受约束属性。助手类PropertyChangeSupport和VetoableChangeSupport允许在 JavaBean 类中轻松定义绑定属性和约束属性。
清单 3-16 用三个属性定义了一个 JavaBeanPerson:name、address和phoneNumber。address属性是一个绑定属性,phoneNumber属性是一个约束属性。
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.beans.PropertyVetoException;
import java.beans.VetoableChangeListener;
import java.beans.VetoableChangeSupport;
public class Person {
private PropertyChangeSupport propertyChangeSupport;
private VetoableChangeSupport vetoableChangeSupport;
private String name;
private String address;
private String phoneNumber;
public Person() {
propertyChangeSupport = new PropertyChangeSupport(this);
vetoableChangeSupport = new VetoableChangeSupport(this);
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
String oldAddress = this.address;
this.address = address;
propertyChangeSupport.firePropertyChange("address", oldAddress, this.address);
}
public String getPhoneNumber() {
return phoneNumber;
}
public void setPhoneNumber(String phoneNumber) throws PropertyVetoException {
String oldPhoneNumber = this.phoneNumber;
vetoableChangeSupport.fireVetoableChange("phoneNumber", oldPhoneNumber, phoneNumber);
this.phoneNumber = phoneNumber;
propertyChangeSupport.firePropertyChange("phoneNumber", oldPhoneNumber, this.phoneNumber);
}
public void addPropertyChangeListener(PropertyChangeListener l) {
propertyChangeSupport.addPropertyChangeListener(l);
}
public void removePropertyChangeListener(PropertyChangeListener l) {
propertyChangeSupport.removePropertyChangeListener(l);
}
public PropertyChangeListener[] getPropertyChangeListeners() {
return propertyChangeSupport.getPropertyChangeListeners();
}
public void addVetoableChangeListener(VetoableChangeListener l) {
vetoableChangeSupport.addVetoableChangeListener(l);
}
public void removeVetoableChangeListener(VetoableChangeListener l) {
vetoableChangeSupport.removeVetoableChangeListener(l);
}
public VetoableChangeListener[] getVetoableChangeListeners() {
return vetoableChangeSupport.getVetoableChangeListeners();
}
}
Listing 3-16.
Person.java
了解 JavaFX 属性适配器
javafx.beans.property.adapter包中的接口和类可以用来轻松地将 JavaBeans 属性适配到 JavaFX 属性。ReadOnlyJavaBeanProperty接口是ReadOnlyProperty的子接口,增加了两个方法:
void dispose()
void fireValueChangedEvent()
JavaBeanProperty接口扩展了ReadOnlyJavaBeanProperty和Property接口。这两个接口中的每一个都有针对Boolean、Integer、Long、Float、Double、Object和String类型的具体类专门化。这些类没有公共构造器。相反,提供了生成器类来创建这些类型的实例。我们在下面的示例代码中使用了JavaBeanStringProperty类。相同的模式适用于所有其他 JavaFX 属性适配器。JavaBeanStringPropertyBuilder支持以下方法:
public static JavaBeanStringPropertyBuilder create()
public JavaBeanStringProperty build()
public JavaBeanStringPropertyBuilder name(java.lang.String)
public JavaBeanStringPropertyBuilder bean(java.lang.Object)
public JavaBeanStringPropertyBuilder beanClass(java.lang.Class<?>)
public JavaBeanStringPropertyBuilder getter(java.lang.String)
public JavaBeanStringPropertyBuilder setter(java.lang.String)
public JavaBeanStringPropertyBuilder getter(java.lang.reflect.Method)
public JavaBeanStringPropertyBuilder setter(java.lang.reflect.Method)
要使用构建器,首先调用它的静态方法create()。然后调用返回构建器本身的方法链。最后,调用build()方法来创建属性。大多数情况下,调用bean()和name()方法来指定 JavaBean 实例和属性名就足够了。getter()和setter()方法可以用来指定一个不遵循命名约定的 getter 和 setter。beanClass()方法可以用来指定 JavaBean 类。在构建器上预先设置 JavaBean 类允许您更有效地为同一个 JavaBean 类的多个实例的同一个 JavaBeans 属性创建适配器。
Note
尽管 JavaFX 场景、控件等类的构建器已经被弃用,但是javafx.beans.property.adapter包中的构建器并没有被弃用。它们是生成 JavaBeans 属性适配器所必需的。
清单 3-17 中的程序说明了将Person类的三个 JavaBeans 属性改编成JavaBeanStringProperty对象。
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.adapter.JavaBeanStringProperty;
import javafx.beans.property.adapter.JavaBeanStringPropertyBuilder;
import java.beans.PropertyVetoException;
public class JavaBeanPropertiesExample {
public static void main(String[] args) throws NoSuchMethodException {
adaptJavaBeansProperty();
adaptBoundProperty();
adaptConstrainedProperty();
}
private static void adaptJavaBeansProperty() throws NoSuchMethodException {
Person person = new Person();
JavaBeanStringProperty nameProperty = JavaBeanStringPropertyBuilder.create()
.bean(person)
.name("name")
.build();
nameProperty.addListener((observable, oldValue, newValue) -> {
System.out.println("JavaFX property " + observable + " changed:");
System.out.println("\toldValue = " + oldValue + ", newValue = " + newValue);
});
System.out.println("Setting name on the JavaBeans property");
person.setName("Stephen Chin");
System.out.println("Calling fireValueChange");
nameProperty.fireValueChangedEvent();
System.out.println("nameProperty.get() = " + nameProperty.get());
System.out.println("Setting value on the JavaFX property");
nameProperty.set("Johan Vos");
System.out.println("person.getName() = " + person.getName());
}
private static void adaptBoundProperty() throws NoSuchMethodException {
System.out.println();
Person person = new Person();
JavaBeanStringProperty addressProperty = JavaBeanStringPropertyBuilder.create()
.bean(person)
.name("address")
.build();
addressProperty.addListener((observable, oldValue, newValue) -> {
System.out.println("JavaFX property " + observable + " changed:");
System.out.println("\toldValue = " + oldValue + ", newValue = " + newValue);
});
System.out.println("Setting address on the JavaBeans property");
person.setAddress("12345 Main Street");
}
private static void adaptConstrainedProperty() throws NoSuchMethodException {
System.out.println();
Person person = new Person();
JavaBeanStringProperty phoneNumberProperty = JavaBeanStringPropertyBuilder.create()
.bean(person)
.name("phoneNumber")
.build();
phoneNumberProperty.addListener((observable, oldValue, newValue) -> {
System.out.println("JavaFX property " + observable + " changed:");
System.out.println("\toldValue = " + oldValue + ", newValue = " + newValue);
});
System.out.println("Setting phoneNumber on the JavaBeans property");
try {
person.setPhoneNumber("800-555-1212");
} catch (PropertyVetoException e) {
System.out.println("A JavaBeans property change is vetoed.");
}
System.out.println("Bind phoneNumberProperty to another property");
SimpleStringProperty stringProperty = new SimpleStringProperty("866-555-1212");
phoneNumberProperty.bind(stringProperty);
System.out.println("Setting phoneNumber on the JavaBeans property");
try {
person.setPhoneNumber("888-555-1212");
} catch (PropertyVetoException e) {
System.out.println("A JavaBeans property change is vetoed.");
}
System.out.println("person.getPhoneNumber() = " + person.getPhoneNumber());
}
}
Listing 3-17.
JavaBeanPropertiesExamples.java
在adaptJavaBeanProperty()方法中,我们实例化了一个Person bean,并将其name JavaBeans 属性改编为 JavaFX JavaBeanStringProperty。为了帮助你理解什么时候一个ChangeEvent被传递到nameProperty,我们给它添加了一个ChangeListener(以 lambda 表达式的形式)。因为name不是一个绑定属性,当我们调用person.setName()时,nameProperty并不知道这个变化。为了通知nameProperty这个变化,我们调用它的fireValueChangedEvent()方法。当我们调用nameProperty.get()时,我们得到我们在person bean 上设置的名称。相反,在我们调用nameProperty.set()之后,对person.getName()的调用将返回我们在nameProperty上设置的内容。
在adaptBoundProperty()方法中,我们实例化了一个Person bean,并将其address JavaBeans 属性改编为 JavaFX JavaBeanStringProperty。为了帮助你理解什么时候一个ChangeEvent被传递到addressProperty,我们给它添加了一个ChangeListener(以 lambda 表达式的形式)。因为address是一个绑定属性,所以addressProperty被注册为person bean 的PropertyChangeListener;因此,当我们调用person.setAddress()时,会立即通知addressProperty,而无需我们调用fireValuechangedEvent()方法。
在adaptConstrainedProperty()方法中,我们实例化了一个Person bean,并将其phoneNumber JavaBeans 属性改编为JavaBeanStringProperty。我们再次添加了一个ChangeListener。因为phoneNumber是一个受约束的属性,phoneNumberProperty能够否决person.setPhoneNumber()调用。当这种情况发生时,person.setPhoneNumber()调用抛出一个PropertyVetoException。如果它本身绑定到另一个 JavaFX 属性,那么phoneNumberProperty将否决这样的更改。我们调用person.setPhoneNumber()两次,一次是在将phoneNumberProperty绑定到另一个 JavaFX 属性之前,一次是在绑定phoneNumberProperty之后。第一个调用成功地改变了phoneNumberProperty的值,第二个调用抛出了一个PropertyVetoException。
当我们运行清单 3-17 中的程序时,以下输出被打印到控制台:
Setting name on the JavaBeans property
Calling fireValueChange
JavaFX property StringProperty [bean: Person@776ec8df, name: name, value: Stephen Chin] changed:
oldValue = null, newValue = Stephen Chin
nameProperty.get() = Stephen Chin
Setting value on the JavaFX property
JavaFX property StringProperty [bean: Person@776ec8df, name: name, value: Johan Vos] changed:
oldValue = Stephen Chin, newValue = Johan Vos
person.getName() = Johan Vos
Setting address on the JavaBeans property
JavaFX property StringProperty [bean: Person@41629346, name: address, value: 12345 main Street] changed:
oldValue = null, newValue = 12345 main Street
Setting phoneNumber on the JavaBeans property
JavaFX property StringProperty [bean: Person@6d311334, name: phoneNumber, value: 800-555-1212] changed:
oldValue = null, newValue = 800-555-1212
Bind phoneNumberProperty to another property
JavaFX property StringProperty [bean: Person@6d311334, name: phoneNumber, value: 866-555-1212] changed:
oldValue = 800-555-1212, newValue = 866-555-1212
Setting phoneNumber on the JavaBeans property
A JavaBeans property change is vetoed.
person.getPhoneNumber() = 866-555-1212
摘要
在本章中,您学习了 JavaFX 属性和绑定框架的基础知识,以及 JavaFX Beans 规范。你现在应该明白下面的重要原则。
- JavaFX 属性和绑定是框架的基础。
- 它们符合框架的关键接口。
- 它们触发两种事件:无效事件和变更事件。
- 框架提供的所有属性和绑定都延迟重新计算它们的值——只有在请求值时。为了迫使他们急切地重新评估,需要附上一个
ChangeListener。 - 从现有的属性和绑定中以三种方式之一创建新的绑定:使用
Bindings实用程序类的工厂方法,使用 fluent 接口 API,或者直接扩展抽象类的IntegerBinding系列。 - JavaFX Beans 规范使用三种方法来定义属性:getter、setter 和属性 getter。
- JavaFX Beans 属性可以通过急切、半懒惰和全懒惰策略来实现。
- 旧式 JavaBeans 属性可以很容易地适应 JavaFX 属性。
资源
以下是关于属性和绑定的有用资源。
- 马丁·福勒关于流畅接口 API 的文章:
www.martinfowler.com/bliki/FluentInterface.html - Oracle 的 JavaFX.com 上的属性和绑定教程:
http://docs.oracle.com/javase/8/javafx/properties-binding-tutorial/ - Michael Heinrichs 的博客包含了关于 JavaFX 属性和绑定的条目:
http://blog.netopyr.com/
四、使用场景构建器创建用户界面
给我一根足够长的杠杆和一个支点,我可以撬动地球。—阿基米德
在第二章中,您了解了以编程方式和声明方式创建 JavaFX UI 的两种方式,以及如何使用 JavaFX APIs 以编程方式创建 UI。您熟悉 JavaFX UI 的剧场隐喻,其中的Stage代表 Windows、Mac 或 Linux 程序中的窗口,或者移动设备中的触摸屏,它包含的Scene和Node代表 UI 的内容。在本章中,我们将讨论 JavaFX 中 UI 故事的另一面:UI 的声明性创建。
这种 UI 设计方法的核心是 FXML 文件。它是一种 XML 文件格式,专门用于保存 UI 元素的信息。它包含 UI 元素的“是什么”,但不包含“如何”这就是为什么这种创建 JavaFX UIs 的方法被称为声明性的。FXML 的核心是一种 Java 对象序列化格式,可以用于以某种方式编写的任何 Java 类,包括所有旧式的 JavaBeans。然而,实际上,它仅用于指定 JavaFX UIs。
除了在文本编辑器或您喜欢的 Java 集成开发环境(ide)中直接编辑之外,FXML 文件还可以通过一个名为 JavaFX Scene Builder 的图形工具进行操作。JavaFX 场景构建器 1.0 于 2012 年 8 月发布,JavaFX 场景构建器 1.1 于 2013 年 9 月发布。1.0 和 1.1 都支持 JavaFX 2。JavaFX Scene Builder 2.0 于 2014 年 5 月发布,可与 JavaFX 8 配合使用。JavaFX Scene Builder 2.0 代码库以开源形式发布,虽然 Oracle JavaFX 团队仍在为其做出贡献,但 Scene Builder 的开发和发布现在由 Gluon 协调。
Gluon 合并了来自 Oracle、Gluon 工程师和社区贡献者的贡献,并维护一个公共代码库和一个问题跟踪器。此外,Gluon 为 Windows、Mac 和 Linux 创建了二进制版本。Scene Builder 上的所有信息,包括如何下载和安装,现在都在 http://gluonhq.com/products/scene-builder/ 维护。
JavaFX Scene Builder 是一个完全图形化的工具,允许您从可用容器、控件和其他可视节点的调色板中绘制 UI 屏幕,并通过在屏幕上直接操作和通过属性编辑器修改其属性来对其进行布局。
JavaFX 运行时使用FXMLLoader类将 FXML 文件加载到 JavaFX 应用程序中。加载 FXML 文件的结果总是一个 Java 对象,通常是一个容器Node,如Group或Pane。这个对象可以作为一个Scene的根,或者作为一个节点附加到一个更大的以编程方式创建的场景图中。对于 JavaFX 应用程序的其余部分,从 FXML 文件加载的节点与以编程方式构造的节点没有什么不同。
我们以螺旋上升的方式介绍了 FXML 文件的内容和格式,它们在运行时是如何加载的,以及它们在设计时是如何形成的。我们从一个完整的例子开始这一章,这个例子展示了如何使用 FXML 完成第二章的清单 2-1 中的 StageCoach 程序。然后,我们详细介绍 FXML 加载工具。然后,我们展示一系列手工制作的小 FXML 文件,突出 FXML 文件格式的所有特性。一旦您理解了 FXML 文件格式,我们将向您展示如何使用 JavaFX Scene Builder 创建这些 FXML 文件,涵盖 JavaFX Scene Builder 2.0 的所有功能。
Note
你需要从 http://gluonhq.com/products/scene-builder/ 下载并安装 Gluon 的开源 JavaFX Scene Builder 9.0 来浏览本章的例子。我们还强烈建议配置您最喜欢的 IDE,以使用 JavaFX Scene Builder 9.0 来编辑 FXML 文件。NetBeans 和 IntelliJ IDEA 捆绑了 JavaFX 支持。Eclipse 用户可以安装 e(fx)clipse 插件。配置完成后,您可以在 IDE 中右键单击项目中的任何 FXML 文件,然后选择“使用场景生成器编辑”上下文菜单项。当然,您也可以使用 IDE 的 XML 文件编辑功能将 FXML 文件编辑为 XML 文件。
使用 FXML 设置舞台
将第二章中的 StageCoach 程序从使用以编程方式创建的 UI 转换为使用以声明方式创建的 UI 的过程非常简单。
使用 JavaFX Scene Builder 以图形方式创建用户界面
我们首先用 JavaFX Scene Builder 创建了一个表示场景根节点的 FXML 文件。图 4-1 显示了创建该 UI 时的屏幕截图。

图 4-1。
StageCoach.fxml being created in JavaFX Scene Builder
我们将在本章的后半部分详细介绍如何使用 JavaFX Scene Builder。现在,只需观察工具的主要功能区域。中间是内容面板,显示正在处理的 UI 的外观。左侧是顶部的“库”面板,它包括可在内容面板中使用的所有可能的节点,这些节点被划分为简单的子集,如容器、控件、形状、图表等;下面是“文档”面板,它将内容面板中正在处理的场景图形显示为称为层次结构的树结构,以及为 UI 中的各种控件提供事件处理程序代码的控制器。右侧是检查器区域,其中包含允许您操作当前选定控件的属性、布局和代码连接的子区域。
了解 FXML 文件
清单 4-1 显示了 JavaFX Scene Builder 从我们创建的 UI 中保存的 FXML 文件。
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.CheckBox?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.Group?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.shape.Rectangle?>
<?import javafx.scene.text.Text?>
<Group fx:id="rootGroup"
onMouseDragged="#mouseDraggedHandler"
onMousePressed="#mousePressedHandler"
xmlns:fx="http://javafx.com/fxml/1"
fx:controller="projavafx.stagecoach.ui.StageCoachController">
<children>
<Rectangle fx:id="blue"
arcHeight="50.0"
arcWidth="50.0"
fill="SKYBLUE"
height="350.0"
strokeType="INSIDE"
width="250.0"/>
<VBox fx:id="contentBox"
layoutX="30.0"
layoutY="20.0"
spacing="10.0">
<children>
<Text fx:id="textStageX"
strokeType="OUTSIDE"
strokeWidth="0.0"
text="x:"
textOrigin="TOP"/>
<Text fx:id="textStageY"
layoutX="10.0"
layoutY="23.0"
strokeType="OUTSIDE"
strokeWidth="0.0"
text="y:"
textOrigin="TOP"/>
<Text fx:id="textStageH"
layoutX="10.0"
layoutY="50.0"
strokeType="OUTSIDE"
strokeWidth="0.0"
text="height:"
textOrigin="TOP"/>
<Text fx:id="textStageW"
layoutX="10.0"
layoutY="77.0"
strokeType="OUTSIDE"
strokeWidth="0.0"
text="width:"
textOrigin="TOP"/>
<Text fx:id="textStageF"
layoutX="10.0"
layoutY="104.0"
strokeType="OUTSIDE"
strokeWidth="0.0"
text="focused:"
textOrigin="TOP"/>
<CheckBox fx:id="checkBoxResizable"
mnemonicParsing="false"
text="resizable"/>
<CheckBox fx:id="checkBoxFullScreen"
mnemonicParsing="false"
text="fullScreen"/>
<HBox fx:id="titleBox">
<children>
<Label fx:id="titleLabel"
text="title"/>
<TextField fx:id="titleTextField"
text="Stage Coach"/>
</children>
</HBox>
<Button fx:id="toBackButton"
mnemonicParsing="false"
onAction="#toBackEventHandler"
text="toBack()"/>
<Button fx:id="toFrontButton"
mnemonicParsing="false"
onAction="#toFrontEventHandler"
text="toFront()"/>
<Button fx:id="closeButton"
mnemonicParsing="false"
onAction="#closeEventHandler"
text="close()"/>
</children>
</VBox>
</children>
</Group>
Listing 4-1.
StageCoach.fxml
Note
JavaFX Scene Builder 创建的 FXML 文件具有较长的行。我们重新格式化了 FXML 文件,以适应书的页面。
这个 FXML 文件的大部分可以直观地理解:它表示一个包含两个孩子的Group,一个Rectangle和一个VBox。VBox依次持有五个Text节点、两个CheckBox es、一个HBox和三个Button。HBox持有一个Label和一个TextField。这些节点的各种属性被设置为一些合理的值;例如三个Button上的text设置为"toBack()"、"toFront()"和"close()"。
这个 FXML 文件中的一些结构需要更多的解释。文件顶部的 XML 处理指令
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.CheckBox?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.Group?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.shape.Rectangle?>
<?import javafx.scene.text.Text?>
通知这个文件的消费者,或者在设计时通知 JavaFX Scene Builder,或者在运行时通知FXMLLoader,以导入提到的 Java 类。这些与 Java 源文件中的导入指令具有相同的效果。
为顶级元素Group提供了两个名称空间声明。JavaFX Scene Builder 将这些名称空间放在它创建的每个 FXML 文件中:
xmlns:fx="http://javafx.com/fxml/1"
Caution
FXML 文件没有根据任何 XML 架构进行验证。FXMLLoader、JavaFX Scene Builder 和 Java IDEs(如 NetBeans、Eclipse 和 IntelliJ IDEA)使用此处指定的名称空间来在编辑 FXML 文件时提供帮助。实际的前缀,即第一个名称空间的空字符串和第二个名称空间的“fx”不应该改变。
这个 FXML 文件包含两种带有fx前缀的属性,fx: controller和fx:id。fx:controller属性出现在顶层元素Group上。它通知 JavaFX 运行时,在当前 FXML 文件中设计的 UI 将与一个称为其控制器的 Java 类一起工作:
fx:controller="projavafx.stagecoach.ui.StageCoachController"
前面的属性声明StageCoach.fxml将与 Java 类projavafx.stagecoach.ui.StageCoachController一起工作。fx:id属性可以出现在代表 JavaFX Node的每个元素中。fx:id的值是控制器中一个字段的名称,表示 FXML 文件加载后的Node。StageCoach.fxml文件声明了下面的fx:ids(只显示了带有fx:id属性的行):
<Group fx:id="rootGroup"
<Rectangle fx:id="blue"
<VBox fx:id="contentBox"
<Text fx:id="textStageX"
<Text fx:id="textStageY"
<Text fx:id="textStageH"
<Text fx:id="textStageW"
<Text fx:id="textStageF"
<CheckBox fx:id="checkBoxResizable"
<CheckBox fx:id="checkBoxFullScreen"
<HBox fx:id="titleBox">
<Label fx:id="titleLabel"
<TextField fx:id="titleTextField"
<Button fx:id="toBackButton"
<Button fx:id="toFrontButton"
<Button fx:id="closeButton"
因此,在FXMLLoader完成加载 FXML 文件之后,可以在 Java 代码中访问和操作 FXML 文件中的顶层Group节点,作为StageCoachController类的rootGroup字段。在这个 FXML 文件中,我们为我们创建的所有节点分配了一个fx:id。这样做只是为了说明的目的。如果没有理由以编程方式操作节点,比如静态标签,那么可以省略控制器中的fx:id属性和相应的字段。
提供对 FXML 文件中节点的编程访问是控制器扮演的一个角色。控制器扮演的另一个角色是提供处理用户输入和来自 FXML 文件中节点的交互事件的方法。这些事件处理程序由名称以on开头的属性指定,例如onMouseDragged、onMousePressed和onAction。它们对应于Node类或其子类中的setOnMouseDragged()、setOnMousePressed()和setOnAction()方法。要将事件处理程序设置为控制器中的一个方法,请使用带有“#”字符的方法名称作为onMouseDragged、onMousePressed和onAction属性的值。StageCoach.fxml文件声明了以下事件处理程序(只显示了带有事件处理程序的行):
<Group fx:id="rootGroup"
onMouseDragged="#mouseDraggedHandler"
onMousePressed="#mousePressedHandler"
<Button fx:id="toBackButton"
onAction="#toBackEventHandler"
<Button fx:id="toFrontButton"
onAction="#toFrontEventHandler"
<Button fx:id="closeButton"
onAction="#closeEventHandler"
控制器类中的事件处理器方法通常应该符合EventHandler<T>接口中单个方法的签名
void handle(T event)
其中T是适当的事件对象,MouseEvent用于onMouseDragged和onMousePressed事件处理程序,ActionEvent用于onAction事件处理程序。不带任何参数的方法也可以设置为事件处理程序方法。如果不打算使用事件对象,可以使用这样的方法。
现在我们已经理解了 FXML 文件,接下来我们继续学习控制器类。
了解控制器
清单 4-2 显示了使用我们在上一小节中创建的 FXML 文件的控制器类。
package projavafx.stagecoach.ui;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Text;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
public class StageCoachController {
@FXML
private Rectangle blue;
@FXML
private VBox contentBox;
@FXML
private Text textStageX;
@FXML
private Text textStageY;
@FXML
private Text textStageH;
@FXML
private Text textStageW;
@FXML
private Text textStageF;
@FXML
private CheckBox checkBoxResizable;
@FXML
private CheckBox checkBoxFullScreen;
@FXML
private HBox titleBox;
@FXML
private Label titleLabel;
@FXML
private TextField titleTextField;
@FXML
private Button toBackButton;
@FXML
private Button toFrontButton;
@FXML
private Button closeButton;
private Stage stage;
private StringProperty title = new SimpleStringProperty();
private double dragAnchorX;
private double dragAnchorY;
public void setStage(Stage stage) {
this.stage = stage;
}
public void setupBinding(StageStyle stageStyle) {
checkBoxResizable.setDisable(stageStyle == StageStyle.TRANSPARENT
|| stageStyle == StageStyle.UNDECORATED);
textStageX.textProperty().bind(new SimpleStringProperty("x: ")
.concat(stage.xProperty().asString()));
textStageY.textProperty().bind(new SimpleStringProperty("y: ")
.concat(stage.yProperty().asString()));
textStageW.textProperty().bind(new SimpleStringProperty("width: ")
.concat(stage.widthProperty().asString()));
textStageH.textProperty().bind(new SimpleStringProperty("height: ")
.concat(stage.heightProperty().asString()));
textStageF.textProperty().bind(new SimpleStringProperty("focused: ")
.concat(stage.focusedProperty().asString()));
stage.setResizable(true);
checkBoxResizable.selectedProperty()
.bindBidirectional(stage.resizableProperty());
checkBoxFullScreen.selectedProperty().addListener((ov, oldValue, newValue) ->
stage.setFullScreen(checkBoxFullScreen.selectedProperty().getValue()));
title.bind(titleTextField.textProperty());
stage.titleProperty().bind(title);
stage.initStyle(stageStyle);
}
@FXML
public void toBackEventHandler(ActionEvent e) {
stage.toBack();
}
@FXML
public void toFrontEventHandler(ActionEvent e) {
stage.toFront();
}
@FXML
public void closeEventHandler(ActionEvent e) {
stage.close();
}
@FXML
public void mousePressedHandler(MouseEvent me) {
dragAnchorX = me.getScreenX() - stage.getX();
dragAnchorY = me.getScreenY() - stage.getY();
}
@FXML
public void mouseDraggedHandler(MouseEvent me) {
stage.setX(me.getScreenX() - dragAnchorX);
stage.setY(me.getScreenY() - dragAnchorY);
}
}
Listing 4-2.
StageCoachController.java
这个类是从第二章的StageCoachMain类中提取出来的,这是我们指定为 FXML 文件StageCoach.fxml.的控制器类的类。实际上,它包含了类型和名称与 FXML 文件中的fx:id相匹配的字段。它还包括名称和签名与 FXML 文件中各种节点的事件处理程序相匹配的方法。
唯一需要解释的是@FXML注释。属于javafx.fxml套餐。这是一个带有运行时保留的标记注释,可以应用于字段和方法。当应用于字段时,@FXML注释告诉 JavaFX Scene Builder 该字段的名称可以用作 FXML 文件中适当类型元素的fx:id。当应用于一个方法时,@FXML注释告诉 JavaFX Scene Builder 该方法的名称可以用作适当类型的事件处理程序属性的值。不管修饰符是什么,用@FXML标注的字段和方法都可以被 FXML 加载工具访问。因此,将所有的@FXML注释字段从public更改为private是安全的,不会对 FXML 加载过程产生负面影响。
StageCoachController类包含 FXML 文件中声明的所有fx:id的匹配字段。它还包括 FXML 文件中的事件处理程序属性指向的五个事件处理程序方法。所有这些字段和方法都用@FXML进行了注释。
StageCoachController还包括一些没有用@FXML注释标注的字段和方法。这些字段和方法出现在类中是为了其他目的。例如,stage字段、setStage()和setupBindings()方法直接在 Java 代码中使用。
了解 FXMLLoader
现在我们已经了解了 FXML 文件和使用 FXML 文件的控制器类,我们将注意力转向运行时 FXML 文件的加载。javafx.fxml包中的FXMLLoader类完成了加载 FXML 文件的大部分工作。在我们的例子中,FXML 文件的加载是在StageCoachMain类中完成的。清单 4-3 显示了StageCoachMain类。
package projavafx.stagecoach.ui;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.geometry.Rectangle2D;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import java.io.IOException;
import java.util.List;
public class StageCoachMain extends Application {
@Override
public void start(Stage stage) throws IOException {
final StageStyle stageStyle = configStageStyle();
FXMLLoader fxmlLoader = new FXMLLoader(StageCoachMain.class
.getResource("/projavafx/stagecoach/ui/StageCoach.fxml"));
Group rootGroup = fxmlLoader.load();
final StageCoachController controller = fxmlLoader.getController();
controller.setStage(stage);
controller.setupBinding(stageStyle);
Scene scene = new Scene(rootGroup, 250, 350);
scene.setFill(Color.TRANSPARENT);
stage.setScene(scene);
stage.setOnCloseRequest(we -> System.out.println("Stage is closing"));
stage.show();
Rectangle2D primScreenBounds = Screen.getPrimary().getVisualBounds();
stage.setX((primScreenBounds.getWidth() - stage.getWidth()) / 2);
stage.setY((primScreenBounds.getHeight() - stage.getHeight()) / 4);
}
public static void main(String[] args) {
launch(args);
}
private StageStyle configStageStyle() {
StageStyle stageStyle = StageStyle.DECORATED;
List<String> unnamedParams = getParameters().getUnnamed();
if (unnamedParams.size() > 0) {
String stageStyleParam = unnamedParams.get(0);
if (stageStyleParam.equalsIgnoreCase("transparent")) {
stageStyle = StageStyle.TRANSPARENT;
} else if (stageStyleParam.equalsIgnoreCase("undecorated")) {
stageStyle = StageStyle.UNDECORATED;
} else if (stageStyleParam.equalsIgnoreCase("utility")) {
stageStyle = StageStyle.UTILITY;
}
}
return stageStyle;
}
}
Listing 4-3.
StageCoachMain.java
在查看FXMLLoader代码之前,让我指出,对于这个例子,我们选择将StageCoach.fxml文件与StageCoachMain.java和StageCoachController.java文件放在一起。它们都位于projavafx/stagecoach/ui目录中。当我们编译源文件时,这种关系被保留下来。因此,当我们运行这个程序时,FXML 文件作为资源/projavafx/stagecoach/ui/StageCoach.fxml出现在类路径中。图 4-2 展示了我们例子中的文件布局。

图 4-2。
The file layout of the StageCoach example
FXML 文件的加载由以下代码片段执行:
FXMLLoader fxmlLoader = new FXMLLoader(StageCoachMain.class
.getResource("/projavafx/stagecoach/ui/StageCoach.fxml"));
Group rootGroup = fxmlLoader.load();
final StageCoachController controller = fxmlLoader.getController();
这里我们使用FXMLLoader类的单参数构造器构造一个fxmlLoader对象,并传入一个由StageCoachMain的Class对象上的getResource()调用返回的URL对象。这个 URL 对象是一个 jar URL 或一个文件 URL,这取决于您是否从 jar 运行这个程序。然后我们在fxmlLoader对象上调用load()方法。这个方法读取 FXML 文件,解析它,实例化它指定的所有节点,并根据它指定的包含关系将它们连接起来。因为控制器是在 FXML 文件中指定的,所以该方法还实例化了一个StageCoachController实例,并根据fx:id将节点分配给控制器实例的字段,这一步通常称为将 FXML 节点注入控制器。事件处理程序也被连接起来。load()方法返回 FXML 文件中的顶层对象,在我们的例子中是一个Group。该返回的Group对象被分配给rootGroup变量,并在后续代码中使用,使用方式与第二章中以编程方式创建的rootGroup相同。然后我们调用getController()方法来获取控制器,控制器的节点字段已经被FXMLLoader注入。该控制器被分配给controller变量,并在后续代码中使用,就像我们刚刚以编程方式创建了它并分配了它的节点字段一样。
既然我们已经完成了将 Stage 蔻驰程序从编程式 UI 创建切换到声明式 UI 创建,我们就可以运行它了。它的行为就像第二章一样。图 4-3 显示了使用transparent命令行参数运行的程序。

图 4-3。
The Stage Coach program run with transparent command-line argument
在这一节中,我们谈到了 FXML 设计时和运行时工具的所有方面。然而,我们只描述了每个设施的一部分,仅仅足以让我们的示例程序运行。在本章的其余部分,我们将详细研究每一个工具。
了解 FXML 加载工具
FXML 文件加载工具由两个类组成,一个接口、一个异常和javafx.fxml包中的一个注释。FXMLLoader是完成大部分工作的类,例如读取和解析 FXML 文件,识别 FXML 文件中的处理指令,并以必要的动作做出响应,识别 FXML 文件的每个元素和属性,并将对象创建任务委托给一组构建器,必要时创建控制器对象,并将节点和其他对象注入控制器。JavaFXBuilderFactory负责创建构建器,以响应FXMLLoader对特定类的构建器的请求。控制器类可以实现Initializable接口来接收来自FXMLLoader的信息,就像以前版本的 JavaFX 一样;然而,这个功能已经被注入方法所取代,所以我们不讨论它。如果 FXML 文件包含错误,使得FXMLLoader无法构建 FXML 文件中指定的所有对象,则会抛出LoadException。@FXML注释可以在控制器类中使用,将某些字段标记为注入目标,将某些方法标记为事件处理程序候选。
了解 FXMLLoader 类
FXMLLoader类有以下公共构造器:
FXMLLoader()FXMLLoader(URL location)FXMLLoader(URL location, ResourceBundle resources)FXMLLoader(URL location, ResourceBundle resources, BuilderFactory builderFactory)FXMLLoader(URL location, ResourceBundle resources, BuilderFactory BuilderFactory builderFactory, Callback<Class<?>, Object> controllerFactory)FXMLLoader(Charset charset)FXMLLoader(URL location, ResourceBundle resources, BuilderFactory BuilderFactory builderFactory, Callback<Class<?> controllerFactory, Object>, Charset charset)FXMLLoader(URL location, ResourceBundle resources, BuilderFactory BuilderFactory builderFactory, Callback<Class<?>, Object> controllerFactory, Charset charset, LinkedList<FXMLLoader> loaders)
参数较少的构造器委托给参数较多的构造器,缺少的参数用默认值填充。location参数是要加载的 FXML 文件的URL。默认为null。resources参数是与 FXML 文件一起使用的资源包。如果在 FXML 文件中使用国际化字符串,这是必要的。默认为null。builderFactory参数是生成器工厂,FXMLLoader用它来获得它创建的各种对象的生成器。它默认为JavaFXBuilderFactory的一个实例。这个构建器工厂了解所有可能出现在 FXML 文件中的标准 JavaFX 类型,所以很少使用定制的构建器工厂。controllerFactory是一个javafx.util.CallBack,当提供控制器的类时,它能够返回控制器的实例。默认为null,在这种情况下FXMLLoader将通过调用控制器类的无参数构造器,通过反射实例化控制器。因此,只有当控制器不能以这种方式构建时,才需要提供一个controllerFactory。解析 FXML 时使用charset。它默认为 UTF-8。loaders参数是一个FXMLLoader列表,默认为空列表。
FXMLLoader类有下面的 getter 和 setter 方法来改变FXMLLoader的状态:
URL getLocation()void setLocation(URL location)ResourceBundle getResources()void setResources(ResourceBundle resources)ObservableMap<String, Object> getNamespace()<T> T getRoot()void setRoot(Object root)<T> T getController()void setController(Object controller)BuilderFactory getBuilderFactory()void setBuilderFactory(BuilderFactory builderFactory)Callback<Class<?>, Object> getControllerFactory()void setControllerFactory(Callback<Class<?>, Object> controllerFactory)Charset getCharset()void setCharset(Charset charset)ClassLoader getClassLoader()void setClassLoader(ClassLoader classLoader)
从这个列表中可以看出,location、resources、builderFactory、controllerFactory、charset也可以在FXMLLoader构造完成后进行设置。另外,我们可以获取并设置root、controller、classLoader,获取FXMLLoader的namespace。只有当 FXML 文件使用fx:root作为其根元素时,root才相关,在这种情况下,必须在加载 FXML 文件之前调用setRoot()。我们将在下一节更详细地介绍fx:root的用法。只有当 FXML 文件的顶层元素中不存在fx:controller属性时,才需要在加载 FXML 文件之前设置controller。classLoader和namespace主要由FXMLLoader内部使用,通常不会被用户代码调用。
FXML 文件的实际加载发生在调用其中一个load()方法的时候。FXMLLoader类有以下加载方法:
<T> T load() throws IOException<T> T load(InputStream input) throws IOExceptionstatic <T> T load(URL location) throws IOExceptionstatic <T> T load(URL location, ResourceBundle resources) throws IOExceptionstatic <T> T load(URL location, ResourceBundle resources, BuilderFactory builderFactory) throws IOExceptionstatic <T> T load(URL location, ResourceBundle resources, BuilderFactory builderFactory, Callback<Class<?>, Object> controllerFactory) throws IOExceptionstatic <T> T load(URL location, ResourceBundle resources, BuilderFactory builderFactory, Callback<Class<?>, Object> controllerFactory, Charset charset) throws IOException
不带参数的load()方法可以在已经初始化了所有必要字段的FXMLLoader实例上调用。采用InputStream参数的load()方法将从指定的input加载 FXML。所有静态的load()方法都是方便的方法,它们将使用提供的参数实例化一个FXMLLoader,然后调用它的一个非静态的load()方法。
在我们的下一个例子中,我们故意没有在 FXML 文件中指定fx:controller。我们还向控制器类添加了一个单参数构造器。FXML 文件、控制器类和主类如清单 4-4 、 4-5 和 4-6 所示。
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.web.WebView?>
<VBox maxHeight="-Infinity"
maxWidth="-Infinity"
minHeight="-Infinity"
minWidth="-Infinity"
prefHeight="400.0"
prefWidth="600.0"
spacing="10.0"
xmlns:fx="http://javafx.com/fxml/1">
<children>
<HBox spacing="10.0">
<children>
<TextField fx:id="address"
onAction="#actionHandler"
HBox.hgrow="ALWAYS">
<padding>
<Insets bottom="4.0" left="4.0" right="4.0" top="4.0"/>
</padding>
</TextField>
<Button fx:id="loadButton"
mnemonicParsing="false"
onAction="#actionHandler"
text="Load"/>
</children>
</HBox>
<WebView fx:id="webView"
prefHeight="200.0"
prefWidth="200.0"
VBox.vgrow="ALWAYS"/>
</children>
<padding>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0"/>
</padding>
</VBox>
Listing 4-4.
FXMLLoaderExample.fxml
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.TextField;
import javafx.scene.web.WebView;
public class FXMLLoaderExampleController {
@FXML
private TextField address;
@FXML
private WebView webView;
@FXML
private Button loadButton;
private String name;
public FXMLLoaderExampleController(String name) {
this.name = name;
}
@FXML
public void actionHandler() {
webView.getEngine().load(address.getText());
}
}
Listing 4-5.
FXMLLoaderExampleController.
java
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class FXMLLoaderExampleMain extends Application {
@Override
public void start(Stage primaryStage) throws Exception {
FXMLLoader fxmlLoader = new FXMLLoader();
fxmlLoader.setLocation(
FXMLLoaderExampleMain.class.getResource("/FXMLLoaderExample.fxml"));
fxmlLoader.setController(
new FXMLLoaderExampleController("FXMLLoaderExampleController"));
final VBox vBox = fxmlLoader.load();
Scene scene = new Scene(vBox, 600, 400);
primaryStage.setTitle("FXMLLoader Example");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Listing 4-6.
FXMLLoaderExampleMain.
java
因为我们没有在 FXML 文件的顶层元素中指定fx:controller属性,所以我们需要在加载 FXML 文件之前在fxmlLoader上设置控制器:
FXMLLoader fxmlLoader = new FXMLLoader();
fxmlLoader.setLocation(
FXMLLoaderExampleMain.class.getResource("/FXMLLoaderExample.fxml"));
fxmlLoader.setController(
new FXMLLoaderExampleController("FXMLLoaderExampleController"));
final VBox vBox = fxmlLoader.load();
如果没有设置控制器,将抛出一个LoaderException,并显示消息“没有指定控制器”这是因为我们指定了控制器方法actionHandler作为文本字段和按钮的动作事件处理程序。FXMLLoader需要控制器来满足 FXML 文件中的这些规范。如果没有指定事件处理程序,FXML 文件将会成功加载,因为不需要控制器。
这个程序是一个非常原始的网络浏览器,有一个地址栏TextField,一个加载栏Button,和一个WebView。图 4-4 显示了工作中的 FXMLLoaderExample 程序。

图 4-4。
The FXMLLoaderExample program
我们的下一个示例 ControllerFactoryExample 与 FXMLLoaderExample 几乎相同,只有两处不同,所以我们在这里没有展示完整的代码。您可以在代码下载包中找到它。不像在FXMLLoaderExample中,我们在 FXML 文件中指定了fx:controller。这迫使我们删除主类中的setController()调用,因为否则我们会得到一个LoadException消息“控制器值已经指定”但是,因为我们的控制器没有默认的构造器,FXMLLoader会抛出一个因无法实例化控制器而导致的LoadException。这个异常可以通过我们在fxmlLoader上设置的简单控制器工厂来纠正:
fxmlLoader.setControllerFactory(
clazz -> new ControllerFactoryExampleController("ExampleController"));
这里我们使用了一个简单的 lambda 表达式来实现函数接口Callback<Class<?>, Object>,它只有一个方法:
Object call(Class<?>)
在我们的实现中,我们简单地返回一个ControllerFactoryExampleController的实例。
理解@FXML 注释
我们已经看到了@FXML注释的两种用法。它可以应用于 FXML 文件的控制器中的字段,这些字段的名称和类型与要注入节点的 FXML 元素的fx:id属性和元素名称相匹配。它可以应用于不带参数或者只带一个类型为javafx.event.Event或其子类型的参数的 void 方法,使它们有资格用作 FXML 文件中元素的事件处理程序。
FXMLLoader将把它的location和resources注入控制器,如果它有接收它们的字段的话:
@FXML
private URL location;
@FXML
private ResourceBundle resources;
FXMLLoader还将调用带有以下签名的@FXML带注释的初始化方法:
@FXML
public void initialize() {
// ...
}
清单 4-7 、 4-8 和 4-9 中的 FXMLInjectionExample 说明了这些特性是如何工作的。在这个例子中,我们将四个Label放在 FXML 文件的一个VBox中。我们将两个Label注入控制器。我们还在控制器类中指定了location和resources注入字段。最后,在initialize()方法中,我们将两个注入的Label的文本设置为location和resource的字符串表示。
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Font?>
<VBox alignment="CENTER_LEFT"
maxHeight="-Infinity"
maxWidth="-Infinity"
minHeight="-Infinity"
minWidth="-Infinity"
prefHeight="150.0"
prefWidth="700.0"
spacing="10.0"
xmlns:fx="http://javafx.com/fxml/1"
fx:controller="FXMLInjectionExampleController">
<children>
<Label text="Location:">
<font>
<Font name="System Bold" size="14.0"/>
</font>
</Label>
<Label fx:id="locationLabel" text="[location]"/>
<Label text="Resources:">
<font>
<Font name="System Bold" size="14.0"/>
</font>
</Label>
<Label fx:id="resourcesLabel" text="[resources]"/>
</children>
<opaqueInsets>
<Insets/>
</opaqueInsets>
<padding>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0"/>
</padding>
</VBox>
Listing 4-7.
FXMLInjectionExample.fxml
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import java.net.URL;
import java.util.ResourceBundle;
public class FXMLInjectionExampleController {
@FXML
private Label resourcesLabel;
@FXML
private Label locationLabel;
@FXML
private URL location;
@FXML
private ResourceBundle resources;
@FXML
public void initialize() {
locationLabel.setText(location.toString());
resourcesLabel.setText(resources.getBaseBundleName());
}
}
Listing 4-8.
FXMLInjectionExampleController.
java
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import java.util.ResourceBundle;
public class FXMLInjectionExampleMain extends Application {
@Override
public void start(Stage primaryStage) throws Exception {
FXMLLoader fxmlLoader = new FXMLLoader();
fxmlLoader.setLocation(
FXMLInjectionExampleMain.class.getResource("/FXMLInjectionExample.fxml"));
fxmlLoader.setResources(ResourceBundle.getBundle("FXMLInjectionExample"));
VBox vBox = fxmlLoader.load();
Scene scene = new Scene(vBox);
primaryStage.setTitle("FXML Injection Example");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Listing 4-9.
FXMLInjectionExampleMain.
java
注意,我们还创建了一个空的FXMLInjectionExample.properties文件,用作资源包来说明资源字段到控制器的注入。我们将在下一节解释如何使用带有 FXML 文件的资源包。当 FXMLInjectionExample 在我们的机器上运行时,屏幕上会显示图 4-5 中的 FXML 注入示例窗口。

图 4-5。
The FXMLInjection program
@FXML注释也可用于包含的 FXML 文件控制器注入,以及标记javafx.event.EventHandler类型的控制器属性,用作 FXML 文件中的事件处理程序。在下一节讨论 FXML 文件的相关特性时,我们将详细介绍它们。
探索 FXML 文件的功能
在本节中,我们将介绍 FXML 文件格式的特性。因为FXMLLoader的主要目标是将 FXML 文件反序列化为 Java 对象,所以它提供了有助于简化 FXML 文件编写的工具也就不足为奇了。
FXML 格式的反序列化能力
因为我们在这一节中讨论的特性与反序列化通用 Java 对象有更多的关系,所以我们将离开 GUI 世界,使用普通的 Java 类。我们在讨论中使用清单 4-10 中定义的 JavaBean。这是一个虚构的类,旨在说明不同的 FXML 特性。
package projavafx.fxmlbasicfeatures;
import javafx.scene.paint.Color;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class FXMLBasicFeaturesBean {
private String name;
private String address;
private boolean flag;
private int count;
private Color foreground;
private Color background;
private Double price;
private Double discount;
private List<Integer> sizes;
private Map<String, Double> profits;
private Long inventory;
private List<String> products = new ArrayList<String>();
private Map<String, String> abbreviations = new HashMap<>();
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
public Color getForeground() {
return foreground;
}
public void setForeground(Color foreground) {
this.foreground = foreground;
}
public Color getBackground() {
return background;
}
public void setBackground(Color background) {
this.background = background;
}
public Double getPrice() {
return price;
}
public void setPrice(Double price) {
this.price = price;
}
public Double getDiscount() {
return discount;
}
public void setDiscount(Double discount) {
this.discount = discount;
}
public List<Integer> getSizes() {
return sizes;
}
public void setSizes(List<Integer> sizes) {
this.sizes = sizes;
}
public Map<String, Double> getProfits() {
return profits;
}
public void setProfits(Map<String, Double> profits) {
this.profits = profits;
}
public Long getInventory() {
return inventory;
}
public void setInventory(Long inventory) {
this.inventory = inventory;
}
public List<String> getProducts() {
return products;
}
public Map<String, String> getAbbreviations() {
return abbreviations;
}
@Override
public String toString() {
return "FXMLBasicFeaturesBean{" +
"name='" + name + '\'' +
",\n\taddress='" + address + '\'' +
",\n\tflag=" + flag +
",\n\tcount=" + count +
",\n\tforeground=" + foreground +
",\n\tbackground=" + background +
",\n\tprice=" + price +
",\n\tdiscount=" + discount +
",\n\tsizes=" + sizes +
",\n\tprofits=" + profits +
",\n\tinventory=" + inventory +
",\n\tproducts=" + products +
",\n\tabbreviations=" + abbreviations +
'}';
}
}
Listing 4-10.
FXMLBasicFeaturesBean.java
清单 4-11 中的 FXML 文件被加载并打印到清单 4-12 中程序的控制台上。
<?import javafx.scene.paint.Color?>
<?import projavafx.fxmlbasicfeatures.FXMLBasicFeaturesBean?>
<?import projavafx.fxmlbasicfeatures.Utilities?>
<?import java.lang.Double?>
<?import java.lang.Integer?>
<?import java.lang.Long?>
<?import java.util.HashMap?>
<?import java.lang.String?>
<FXMLBasicFeaturesBean name="John Smith"
flag="true"
count="12345"
xmlns:fx="http://javafx.com/fxml/1">
<address>12345 Main St.</address>
<foreground>#ff8800</foreground>
<background>
<Color red="0.0" green="1.0" blue="0.5"/>
</background>
<price>
<Double fx:value="3.1415926"/>
</price>
<discount>
<Utilities fx:constant="TEN_PCT"/>
</discount>
<sizes>
<Utilities fx:factory="createList">
<Integer fx:value="1"/>
<Integer fx:value="2"/>
<Integer fx:value="3"/>
</Utilities>
</sizes>
<profits>
<HashMap q1="1000" q2="1100" q3="1200" a4="1300"/>
</profits>
<fx:define>
<Long fx:id="inv" fx:value="9765625"/>
</fx:define>
<inventory>
<fx:reference source="inv"/>
</inventory>
<products>
<String fx:value="widget"/>
<String fx:value="gadget"/>
<String fx:value="models"/>
</products>
<abbreviations CA="California" NY="New York" FL="Florida" MO="Missouri"/>
</FXMLBasicFeaturesBean>
Listing 4-11.
FXMLBasicFeatures.fxml
package projavafx.fxmlbasicfeatures;
import javafx.fxml.FXMLLoader;
import java.io.IOException;
public class FXMLBasicFeaturesMain {
public static void main(String[] args) throws IOException {
FXMLBasicFeaturesBean bean = FXMLLoader.load(
FXMLBasicFeaturesMain.class.getResource(
"/projavafx/fxmlbasicfeatures/FXMLBasicFeatures.fxml")
);
System.out.println("bean = " + bean);
}
}
Listing 4-12.
FXMLBasicFeaturesMain.java
我们使用了一个小的工具类,它包含一些常量和一个创建List<Integer>的工厂方法,如清单 4-13 所示。
package projavafx.fxmlbasicfeatures;
import java.util.ArrayList;
import java.util.List;
public class Utilities {
public static final Double TEN_PCT = 0.1d;
public static final Double TWENTY_PCT = 0.2d;
public static final Double THIRTY_PCT = 0.3d;
public static List<Integer> createList() {
return new ArrayList<>();
}
}
Listing 4-13.
Utilities.java
正在 FXML 文件中创建FXMLBasicFeaturesBean对象;FXML 文件的顶层元素是FXMLBasicFeaturesBean这一事实表明了这一点。name和address字段说明了可以将字段设置为属性或子元素:
<FXMLBasicFeaturesBean name="John Smith"
flag="true"
count="12345"
xmlns:fx="http://javafx.com/fxml/1">
<address>12345 Main St.</address>
foreground和background字段说明了设置javafx.scene.paint.Color子元素的两种方式,要么通过十六进制字符串,要么使用Color元素(记住Color是一个没有默认构造器的不可变对象):
<foreground>#ff8800</foreground>
<background>
<Color red="0.0" green="1.0" blue="0.5"/>
</background>
price字段说明了一种构造Double对象的方法。fx:value属性调用Double上的valueOf(String)方法。这适用于任何具有工厂方法valueOf(String)的 Java 类:
<price>
<Double fx:value="3.1415926"/>
</price>
discount字段说明了如何使用 Java 类中定义的常量。属性访问其元素类型的常量(public static final)字段。下面将折扣字段设置为Utilities.TEN_PCT,即0.1:
<discount>
<Utilities fx:constant="TEN_PCT"/>
</discount>
sizes字段说明了使用工厂方法创建对象。属性在其元素的类型上调用指定的工厂方法。在我们的例子中,它调用Utilities.createList()来创建一个Integer列表,然后用三个Integer填充它。注意sizes是一个读写属性。稍后您将看到一个如何填充只读列表属性的示例。
<sizes>
<Utilities fx:factory="createList">
<Integer fx:value="1"/>
<Integer fx:value="2"/>
<Integer fx:value="3"/>
</Utilities>
</sizes>
profits字段说明了如何填充读写映射。这里,我们将利润字段设置为一个用键/值对创建的HashMap:
<profits>
<HashMap q1="1000" q2="1100" q3="1200" a4="1300"/>
</profits>
inventory字段说明了如何在一个地方定义一个对象并在另一个地方引用它。元素创建了一个具有fx:id属性的独立对象。fx:reference元素创建了一个对别处定义的对象的引用,它的source属性指向一个先前定义的对象的fx:id:
<fx:define>
<Long fx:id="inv" fx:value="9765625"/>
</fx:define>
<inventory>
<fx:reference source="inv"/>
</inventory>
products字段说明了如何填充只读列表。FXML 的以下片段相当于调用bean.getProducts().addAll("widget", "gadget", "models"):
<products>
<String fx:value="widget"/>
<String fx:value="gadget"/>
<String fx:value="models"/>
</products>
abbreviations字段说明了如何填充只读地图:
<abbreviations CA="California" NY="New York" FL="Florida" MO="Missouri"/>
当 FXMLBasicFeaturesMain 程序运行时,以下输出将按预期打印到控制台:
bean = FXMLBasicFeaturesBean{name='John Smith',
address='12345 Main St.',
flag=true,
count=12345,
foreground=0xff8800ff,
background=0x00ff80ff,
price=3.1415926,
discount=0.1,
sizes=[1, 2, 3],
profits={q1=1000, q2=1100, q3=1200, a4=1300},
inventory=9765625,
products=[widget, gadget, models],
abbreviations={MO=Missouri, FL=Florida, NY=New York, CA=California}}
了解默认和静态属性
许多 JavaFX 类都有一个默认属性。默认属性是用类上的@DefaultProperty注释指定的。@DefaultProperty注释属于javafx.beans包。例如,javafx.scene.Group类的默认属性是它的children属性。在 FXML 文件中,当通过子元素指定默认属性时,默认属性本身的开始和结束标记可以省略。作为一个例子,下面的代码片段,您可以在清单 4-1 中看到。
<HBox fx:id="titleBox">
<children>
<Label fx:id="titleLabel"
text="title"/>
<TextField fx:id="titleTextField"
text="Stage Coach"/>
</children>
</HBox>
可以简化为
<HBox fx:id="titleBox">
<Label fx:id="titleLabel"
text="title"/>
<TextField fx:id="titleTextField"
text="Stage Coach"/>
</HBox>
静态属性是在对象上设置的属性,不是通过调用对象本身的 setter 方法,而是通过调用不同类的静态方法,将对象和属性值作为参数传递。许多 JavaFX 的容器Node都有这样的静态方法。这些方法在将一个Node添加到容器之前被调用,以影响某些结果。静态属性在 FXML 文件中表示为内部对象(作为静态方法的第一个参数传入的对象)的属性,其名称包括类名和静态方法名,用点分隔。您可以在清单 4-4 中找到一个静态属性的例子:
<WebView fx:id="webView"
prefHeight="200.0"
prefWidth="200.0"
VBox.vgrow="ALWAYS"/>
这里我们将一个WebView添加到一个VBox,VBox.vgrow属性表明FXMLLoader需要在将webView添加到VBox之前调用下面的。
VBox.vgrow(webView, Priority.ALWAYS)
静态属性除了作为属性出现之外,还可以作为子元素出现。
了解属性解析和绑定
正如你在本章前面所看到的,对象属性可以表示为属性和子元素。有时,将属性建模为子元素或属性同样有效。然而,FXMLLoader将对属性进行额外的处理,使得使用属性更有吸引力。处理属性时,FXMLLoader会执行三种属性值解析和表达式绑定。
当属性的值以@字符开始时,FXMLLoader会将该值视为相对于当前文件的位置。这被称为位置解析。当一个属性的值以一个%字符开始时,FXMLLoader会将该值视为资源包中的一个键,并用特定于地区的值替换该键。这称为资源解析。当一个属性的值以一个$字符开始时,FXMLLoader会将该值视为一个变量名,并将被引用变量的值替换为该属性的值。这被称为可变分辨率。
当一个属性的值以${开始,以}结束,并且如果该属性表示一个 JavaFX 属性,FXMLLoader将把该值视为一个绑定表达式,并将 JavaFX 属性绑定到包含的表达式。这叫做表达式绑定。您将在第三章中了解 JavaFX 属性和绑定。现在简单地理解一下,当一个属性被绑定到一个表达式时,每次表达式改变值时,这种改变都会反映在属性中。支持的表达式包括字符串、布尔、数值、一元运算符–(减)和!(求反)、算术运算符(+、–、*、/、%)、逻辑运算符(&&、||)和关系运算符(>、>=、<、<=、==、!=)。
清单 4-14 到 4-19 中显示的 ResolutionAndBindingExample 说明了位置解析、资源解析、变量解析以及表达式绑定的使用。
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Font?>
<?import java.util.Date?>
<VBox id="vbox" alignment="CENTER_LEFT" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity"
minWidth="-Infinity" prefHeight="200.0" prefWidth="700.0" spacing="10.0"
stylesheets="@ResolutionAndBindingExample.css"
xmlns:fx="http://javafx.com/fxml/1" fx:controller="ResolutionAndBindingController">
<children>
<Label text="%location">
<font>
<Font name="System Bold" size="14.0"/>
</font>
</Label>
<Label fx:id="locationLabel" text="[location]"/>
<Label text="%resources">
<font>
<Font name="System Bold" size="14.0"/>
</font>
</Label>
<Label fx:id="resourcesLabel" text="[resources]"/>
<Label text="%currentDate">
<font>
<Font name="System Bold" size="14.0"/>
</font>
</Label>
<HBox alignment="BASELINE_LEFT" spacing="10.0">
<children>
<fx:define>
<Date fx:id="capturedDate"/>
</fx:define>
<Label fx:id="currentDateLabel" text="$capturedDate"/>
<TextField fx:id="textField"/>
<Label text="${textField.text}"/>
</children>
</HBox>
</children>
<opaqueInsets>
<Insets/>
</opaqueInsets>
<padding>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0"/>
</padding>
</VBox>
Listing 4-14.
ResolutionAndBindingExample.fxml
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import java.net.URL;
import java.util.ResourceBundle;
public class ResolutionAndBindingController {
@FXML
private Label resourcesLabel;
@FXML
private Label locationLabel;
@FXML
private Label currentDateLabel;
@FXML
private URL location;
@FXML
private ResourceBundle resources;
@FXML
public void initialize() {
locationLabel.setText(location.toString());
resourcesLabel.setText(resources.getBaseBundleName() +
" (" + resources.getLocale().getCountry() +
", " + resources.getLocale().getLanguage() + ")");
}
}
Listing 4-15.
ResolutionAndBindingController.
java
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import java.util.ResourceBundle;
public class ResolutionAndBindingExample extends Application {
@Override
public void start(Stage primaryStage) throws Exception {
FXMLLoader fxmlLoader = new FXMLLoader();
fxmlLoader.setLocation(
ResolutionAndBindingExample.class.getResource(
"/ResolutionAndBindingExample.fxml"));
fxmlLoader.setResources(
ResourceBundle.getBundle(
"ResolutionAndBindingExample"));
VBox vBox = fxmlLoader.load();
Scene scene = new Scene(vBox);
primaryStage.setTitle("Resolution and Binding Example");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Listing 4-16.
ResolutionAndBindingExample.java
location=Location:
resources=Resources:
currentDate=CurrentDate:
Listing 4-17.
ResourceAndBindingExample.
properties
location=Emplacement:
resources=Resources:
currentDate=Date du jour:
Listing 4-18.
ResolutionAndBindingExample_fr_FR.properties
#vbox {
-fx-background-color: azure ;
}
Listing 4-19.
ResolutionAndBindingExample.css
FXML 文件中使用位置解析来指定 CSS 文件的位置。stylesheet属性被设置为位置“@ResolutionAndBindingExample.css”:
<VBox id="vbox" alignment="CENTER_LEFT" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity"
minWidth="-Infinity" prefHeight="200.0" prefWidth="700.0" spacing="10.0"
stylesheets="@ResolutionAndBindingExample.css"
xmlns:fx="http://javafx.com/fxml/1" fx:controller="ResolutionAndBindingController">
样式表将VBox的背景色设置为天蓝色。资源解析用于设置程序中三个标签的文本:
<Label text="%location">
<Label text="%resources">
<Label text="%currentDate">
在加载 FXML 文件之前,这些标签将从提供给FXMLLoader的资源包中获取文本。提供了属性文件的默认区域设置和法语区域设置翻译。可变解析发生在定义的java.util.Date实例和Label之间:
<fx:define>
<Date fx:id="capturedDate"/>
</fx:define>
<Label fx:id="currentDateLabel" text="$capturedDate"/>
定义的Date被赋予了capturedDate的fx:id,标签使用变量作为其文本。最后,表达式绑定发生在TextField和Label之间:
<TextField fx:id="textField"/>
<Label text="${textField.text}"/>
TextField被赋予了textField的fx:id,标签被绑定到表达式textField.text,结果标签模仿了文本字段中输入的内容。当使用法语语言环境运行 ResolutionAndBindingExample 时,将显示如图 4-6 所示的解析和绑定示例窗口。

图 4-6。
The ResolutionAndBindingExample program
使用多个 FXML 文件
因为加载一个 FXML 文件的结果是一个可以在一个Scene中使用的 JavaFX Node,所以对于任何一个Scene,您并不局限于只使用一个 FXML 文件。例如,您可以将场景分成两个或更多部分,并用各自的 FXML 文件来表示每个部分。然后,您可以在每个部分的 FXML 文件上调用FXMLLoader的load()方法之一,并以编程方式将结果节点组装到您的场景中。
FXML 文件格式支持另一种将单独准备的 FXML 文件组合在一起的机制。一个 FXML 文件可以包含另一个带有fx:include元素的 FXML 文件。元素支持三个属性:source属性保存包含的 FXML 文件的位置;resources属性保存被包含的 FXML 文件使用的资源包的位置;而charset属性保存包含的 FXML 文件的字符集。如果source属性以“/”字符开头,则解释为类路径中的路径;否则,它被解释为相对于包含 FXML 文件的位置。resource和charset属性是可选的。如果未指定它们,则使用它们用于加载包含 FXML 文件的值。用于加载包含 FXML 文件的构建器工厂和控制器工厂也用于加载包含 FXML 文件。
可以为一个fx:include元素指定一个fx:id。当指定了一个fx:id时,可以指定包含的 FXML 文件的控制器中的一个相应字段,并且FXMLLoader将把加载包含的 FXML 文件的结果注入这个字段。此外,如果被包含的 FXML 文件在其根元素中指定了fx:controller,则该被包含的 FXML 文件的控制器也可以被注入到被包含的 FXML 文件的控制器中,只要在被包含的文件的控制器中有适当命名和类型化的字段可用于接收被注入的被包含的 FXML 文件的控制器。在本节的示例应用程序中,我们使用两个 FXML 文件来表示应用程序的 UI。包含的 FXML 文件包含如下行:
<BorderPane maxHeight="-Infinity"
...
fx:controller="IncludeExampleTreeController">
<fx:include fx:id="details"
source="IncludeExampleDetail.fxml" />
包含的 FXML 有如下几行:
<VBox maxHeight="-Infinity"
...
fx:controller="IncludeExampleDetailController">
因此,加载包含的 FXML 文件将产生一个类型为VBox的根元素和一个类型为IncludeExampleDetailController的控制器。包含 FXML 文件的控制器,IncludeExampleTreeController有如下字段:
@FXML
private VBox details;
@FXML
private IncludeExampleDetailController detailsController;
当包含 FXML 文件被加载时,这些字段将保存包含 FXML 文件的加载根和控制器。
本节示例的完整源代码如清单 4-20 到 4-25 所示。
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TreeTableColumn?>
<?import javafx.scene.control.TreeTableView?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Font?>
<BorderPane maxHeight="-Infinity"
maxWidth="-Infinity"
minHeight="-Infinity"
minWidth="-Infinity"
prefHeight="400.0"
prefWidth="600.0"
xmlns:fx="http://javafx.com/fxml/1"
fx:controller="IncludeExampleTreeController">
<top>
<Label text="Product Details"
BorderPane.alignment="CENTER">
<font>
<Font name="System Bold Italic" size="36.0"/>
</font>
</Label>
</top>
<left>
<VBox spacing="10.0">
<children>
<Label text="List of Products:">
<font>
<Font name="System Bold" size="12.0"/>
</font>
</Label>
<TreeTableView fx:id="treeTableView"
prefHeight="200.0"
prefWidth="200.0"
BorderPane.alignment="CENTER"
VBox.vgrow="ALWAYS">
<columns>
<TreeTableColumn fx:id="category"
editable="false"
prefWidth="125.0"
text="Category"/>
<TreeTableColumn fx:id="name"
editable="false"
prefWidth="75.0"
text="Name"/>
</columns>
</TreeTableView>
</children>
<BorderPane.margin>
<Insets/>
</BorderPane.margin>
</VBox>
</left>
<center>
<fx:include fx:id="details"
source="IncludeExampleDetail.fxml"/>
</center>
<padding>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0"/>
</padding>
</BorderPane>
Listing 4-20.
IncludeExampleTree.fxml
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextArea?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Font?>
<VBox maxHeight="-Infinity"
maxWidth="-Infinity"
minHeight="-Infinity"
minWidth="-Infinity"
prefHeight="346.0"
prefWidth="384.0"
spacing="10.0"
xmlns:fx="http://javafx.com/fxml/1"
fx:controller="IncludeExampleDetailController">
<children>
<Label text="Category:">
<font>
<Font name="System Bold" size="12.0"/>
</font>
</Label>
<Label fx:id="category" text="[Category]"/>
<Label text="Name:">
<font>
<Font name="System Bold" size="12.0"/>
</font>
</Label>
<Label fx:id="name" text="[Name]"/>
<Label text="Description:">
<font>
<Font name="System Bold" size="12.0"/>
</font>
</Label>
<TextArea fx:id="description"
prefHeight="200.0"
prefWidth="200.0"
VBox.vgrow="ALWAYS"/>
</children>
<padding>
<Insets bottom="10.0" left="20.0" right="10.0" top="30.0"/>
</padding>
</VBox>
Listing 4-21.
IncludeExampleDetail.fxml
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.fxml.FXML;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeTableColumn;
import javafx.scene.control.TreeTableView;
import javafx.scene.layout.VBox;
public class IncludeExampleTreeController {
@FXML
private TreeTableView<Product> treeTableView;
@FXML
private TreeTableColumn<Product, String> category;
@FXML
private TreeTableColumn<Product, String> name;
@FXML
private VBox details;
@FXML
private IncludeExampleDetailController detailsController;
@FXML
public void initialize() {
Product[] products = new Product[101];
for (int i = 0; i <= 100; i++) {
products[i] = new Product();
products[i].setCategory("Category" + (i / 10));
products[i].setName("Name" + i);
products[i].setDescription("Description" + i);
}
TreeItem<Product> root = new TreeItem<>(products[100]);
root.setExpanded(true);
for (int i = 0; i < 10; i++) {
TreeItem<Product> firstLevel =
new TreeItem<>(products[i * 10]);
firstLevel.setExpanded(true);
for (int j = 1; j < 10; j++) {
TreeItem<Product> secondLevel =
new TreeItem<>(products[i * 10 + j]);
secondLevel.setExpanded(true);
firstLevel.getChildren().add(secondLevel);
}
root.getChildren().add(firstLevel);
}
category.setCellValueFactory(param ->
new ReadOnlyStringWrapper(param.getValue().getValue().getCategory()));
name.setCellValueFactory(param ->
new ReadOnlyStringWrapper(param.getValue().getValue().getName()));
treeTableView.setRoot(root);
treeTableView.getSelectionModel().selectedItemProperty()
.addListener((observable, oldValue, newValue) -> {
Product product = null;
if (newValue != null) {
product = newValue.getValue();
}
detailsController.setProduct(product);
});
}
}
Listing 4-22.
IncludeExampleTreeController.java
import javafx.beans.value.ChangeListener;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.control.TextArea;
public class IncludeExampleDetailController {
@FXML
private Label category;
@FXML
private Label name;
@FXML
private TextArea description;
private Product product;
private ChangeListener<String> listener;
public void setProduct(Product product) {
if (this.product != null) {
unhookListener();
}
this.product = product;
hookTo(product);
}
private void unhookListener() {
description.textProperty().removeListener(listener);
}
private void hookTo(Product product) {
if (product == null) {
category.setText("");
name.setText("");
description.setText("");
listener = null;
} else {
category.setText(product.getCategory());
name.setText(product.getName());
description.setText(product.getDescription());
listener = (observable, oldValue, newValue) ->
product.setDescription(newValue);
description.textProperty().addListener(listener);
}
}
}
Listing 4-23.
IncludeExampleDetailController.java
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class IncludeExample extends Application {
@Override
public void start(Stage primaryStage) throws Exception {
FXMLLoader fxmlLoader = new FXMLLoader();
fxmlLoader.setLocation(
IncludeExample.class.getResource("IncludeExampleTree.fxml"));
final BorderPane borderPane = fxmlLoader.load();
Scene scene = new Scene(borderPane, 600, 400);
primaryStage.setTitle("Include Example");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Listing 4-24.
IncludeExample.java
public class Product {
private String category;
private String name;
private String description;
public String getCategory() {
return category;
}
public void setCategory(String category) {
this.category = category;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
Listing 4-25.
Product.java
在这个 IncludeExample 程序中,我们在两个 FXML 文件中构建 UI,每个文件都有自己的控制器支持。UI 的特点是左边有一个TreeTableView,右边有一些Label和一个TextArea。TreeTableView加载有虚拟Product数据。当左边的一行TreeTableView被选中时,相应的Product会显示在右边。您可以使用右侧的TextArea编辑Product的描述字段。当您从左侧的旧行导航到新行时,右侧的Product会反映这一变化。但是,您对先前显示的Product所做的所有更改都会保留在模型中。当您导航回已修改的Product时,您的更改将再次显示。第六章中更详细地介绍了TreeTableView类。
我们使用了一个附加在TextField的textProperty上的ChangeListener<String>来同步TextField中的文本和Product中的description。JavaFX 属性和更改侦听器是 JavaFX 属性和绑定 API 的一部分。我们将在下一章讨论这个 API。
当 IncludeExample 运行时,显示如图 4-7 所示的 Include Example 窗口。

图 4-7。
The IncludeExample program
使用 fx:root 创建定制组件
元素允许我们将一个 FXML 文件附加到另一个 FXML 文件中。类似地,fx:root元素允许我们将 FXML 文件附加到代码中提供的Node上。fx:root元素必须是 FXML 文件中的顶级元素。必须为它提供一个type属性,该属性决定了需要在代码中创建的Node的类型,以便加载这个 FXML 文件。
最简单的形式是,您可以从
<SomeType ...
到
<fx:root type="some.package.SomeType" ...
在加载 FXML 文件之前,在代码中实例化SomeType并将其设置为FXMLLoader中的根,如下所示:
SomeType someType = new SomeType();
fxmlLoader.setRoot(someType);
fxmlLoader.load();
下一个例子更进一步。它定义了一个扩展 FXML 文件的fx:root类型的类,并作为 FXML 文件的根和控制器。它在其构造器中加载 FXML 文件,并使用initialize()方法在 FXML 文件中构建的节点之间建立所需的关系。然后,可以像使用本地 JavaFX 节点一样使用该类。以这种方式构造的类称为自定义组件。
我们在这里定义的定制组件是一个简单的复合定制组件,这意味着它由几个节点组成,这些节点共同满足一些业务需求。我们在这个例子中创建的定制组件叫做ProdId。它旨在帮助产品 ID 的数据输入,产品 ID 必须具有“A-123456”的形式,其中破折号前只有一个字符,并且必须是“A”或“B”或“c”。破折号后最多可以有六个字符。该程序如清单 4-26 至 4-28 所示。
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextField?>
<fx:root type="javafx.scene.layout.HBox"
alignment="BASELINE_LEFT"
maxHeight="-Infinity"
maxWidth="-Infinity"
minHeight="-Infinity"
minWidth="-Infinity"
xmlns:fx="http://javafx.com/fxml/1">
<children>
<TextField fx:id="prefix" prefColumnCount="1"/>
<Label text="-"/>
<TextField fx:id="prodCode" prefColumnCount="6"/>
</children>
</fx:root>
Listing 4-26.
ProdId.fxml
package projavafx.customcomponent;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.TextField;
import javafx.scene.layout.HBox;
import java.io.IOException;
public class ProdId extends HBox {
@FXML
private TextField prefix;
@FXML
private TextField prodCode;
private StringProperty prodId = new SimpleStringProperty();
public ProdId() throws IOException {
FXMLLoader fxmlLoader = new FXMLLoader(ProdId.class.getResource("ProdId.fxml"));
fxmlLoader.setRoot(this);
fxmlLoader.setController(this);
fxmlLoader.load();
}
@FXML
public void initialize() {
prefix.textProperty().addListener((observable, oldValue, newValue) -> {
switch (newValue) {
case "A":
case "B":
case "C":
prodCode.requestFocus();
break;
default:
prefix.setText("");
}
});
prodCode.textProperty().addListener((observable, oldValue, newValue) -> {
if (newValue.length() > 6) {
prodCode.setText(newValue.substring(0, 6));
} else if (newValue.length() == 0) {
prefix.requestFocus();
}
});
prodId.bind(prefix.textProperty().concat("-").concat(prodCode.textProperty()));
}
public String getProdId() {
return prodId.get();
}
public StringProperty prodIdProperty() {
return prodId;
}
public void setProdId(String prodId) {
this.prodId.set(prodId);
}
}
Listing 4-27.
ProdId.java
package projavafx.customcomponent;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
import javafx.stage.Stage;
public class CustomComponent extends Application {
@Override
public void start(Stage primaryStage) throws Exception {
VBox vBox = new VBox(10);
vBox.setPadding(new Insets(10, 10, 10, 10));
vBox.setAlignment(Pos.BASELINE_CENTER);
final Label prodIdLabel = new Label("Enter Product Id:");
final ProdId prodId = new ProdId();
final Label label = new Label();
label.setFont(Font.font(48));
label.textProperty().bind(prodId.prodIdProperty());
HBox hBox = new HBox(10);
hBox.setPadding(new Insets(10, 10, 10, 10));
hBox.setAlignment(Pos.BASELINE_LEFT);
hBox.getChildren().addAll(prodIdLabel, prodId);
vBox.getChildren().addAll(hBox, label);
Scene scene = new Scene(vBox);
primaryStage.setTitle("Custom Component Example");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Listing 4-28.
CustomComponent.
java
注意,在主程序CustomComponent类中,我们没有加载任何 FXML 文件。我们简单地实例化了ProdId,并继续使用它,就像它是一个本地 JavaFX 节点一样。FXML 文件只是将两个TextField和一个Label放在一个HBox类型fx:root中。没有设置fx:controller,因为我们想在ProdId类的构造器中设置它。除了两个注入的TextField之外,我们还有另一个名为prodId的StringProperty字段,为此我们定义了一个 getter getProdId(),一个 setter setProdId()和一个 property getter prodIdProperty()。
private StringProperty prodId = new SimpleStringProperty();
public String getProdId() {
return prodId.get();
}
public StringProperty prodIdProperty() {
return prodId;
}
public void setProdId(String prodId) {
this.prodId.set(prodId);
}
验证需求和便利功能在initialize()方法中,当FXMLLoader完成加载 FXML 文件时,将调用该方法。我们将ChangeListener连接到两个TextField的textProperty,只允许有效的变更发生。当prefix填充了正确的数据时,我们也将光标移动到prodCode。同样,当我们从prodCode字段后退时,光标会自然地跳到prefix文本字段。
@FXML
public void initialize() {
prefix.textProperty().addListener((observable, oldValue, newValue) -> {
switch (newValue) {
case "A":
case "B":
case "C":
prodCode.requestFocus();
break;
default:
prefix.setText("");
}
});
prodCode.textProperty().addListener((observable, oldValue, newValue) -> {
if (newValue.length() > 6) {
prodCode.setText(newValue.substring(0, 6));
} else if (newValue.length() == 0) {
prefix.requestFocus();
}
});
prodId.bind(prefix.textProperty().concat("-").concat(prodCode.textProperty()));
}
当 CustomComponent 程序运行时,显示如图 4-8 所示的自定义组件示例窗口。

图 4-8。
The CustomComponent program
使用脚本或控制器属性的事件处理
在上一节中,我们向您介绍了如何使用控制器的方法作为 FXML 文件中节点的事件处理程序。JavaFX 允许另外两种方式在 FXML 文件中设置事件处理程序。一种方法是使用脚本。可以使用任何基于 JSR-223 兼容javax.script的脚本引擎。必须在 FXML 文件的顶部指定用于编写脚本的语言。要使用 Oracle JDK 8 附带的 Nashorn JavaScript 引擎,以下处理指令必须出现在 FXML 文件的顶部:
<?language javascript?>
元素用于引入脚本。支持内联脚本和外部文件脚本。以下是一个内联脚本:
<fx:script>
function actionHandler(event) {
webView.getEngine().load(address.getText());
}
</fx:script>
外部脚本采用以下形式:
<fx:script source="myscript.js"/>
FXML 文件中具有fx:id的任何节点都可以通过它们的fx:id名称从脚本环境中访问。如果 FXML 文件有一个控制器,那么这个控制器就是一个名为controller的变量。在fx:script部分中声明的变量也可以用作 FXML 文件其余部分的属性中的变量。要使用fx:script部分定义的actionHandler(event)函数作为事件处理程序,可以指定如下:
<TextField fx:id="address"
onAction="actionHandler(event)"
Caution
如果您的事件处理程序不需要检查事件对象,您可以使用不带参数的函数,或者使用带一个参数作为事件处理程序属性值的函数,比如onAction。如果你调用一个只有一个参数的函数,那么你必须将系统提供的事件变量传递给它。
清单 4-29 和 4-30 中的脚本示例说明了使用脚本的事件处理。
<?xml version="1.0" encoding="UTF-8"?>
<?language javascript?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.web.WebView?>
<VBox maxHeight="-Infinity"
maxWidth="-Infinity"
minHeight="-Infinity"
minWidth="-Infinity"
prefHeight="400.0"
prefWidth="600.0"
spacing="10.0"
xmlns:fx="http://javafx.com/fxml/1">
<fx:script>
function actionHandler(event) {
webView.getEngine().load(address.getText());
}
</fx:script>
<children>
<HBox spacing="10.0">
<children>
<TextField fx:id="address"
onAction="actionHandler(event)"
HBox.hgrow="ALWAYS">
<padding>
<Insets bottom="4.0" left="4.0" right="4.0" top="4.0"/>
</padding>
</TextField>
<Button fx:id="loadButton"
mnemonicParsing="false"
onAction="actionHandler(event)"
text="Load"/>
</children>
</HBox>
<WebView fx:id="webView"
prefHeight="200.0"
prefWidth="200.0"
VBox.vgrow="ALWAYS"/>
</children>
<padding>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0"/>
</padding>
</VBox>
Listing 4-29.
ScriptingExample.
fxml
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class ScriptingExample extends Application {
@Override
public void start(Stage primaryStage) throws Exception {
FXMLLoader fxmlLoader = new FXMLLoader();
fxmlLoader.setLocation(
ScriptingExample.class.getResource("/ScriptingExample.fxml"));
final VBox vBox = fxmlLoader.load();
Scene scene = new Scene(vBox, 600, 400);
primaryStage.setTitle("Scripting Example");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Listing 4-30.
ScriptingExample.
java
运行 ScriptingExample 时,将显示与图 4-4 非常相似的脚本示例窗口。
您还可以使用变量语法指定事件处理程序:
<TextField fx:id="address"
onAction="$controller.actionHandler"
这将把控制器的actionHandler属性设置为onActionEvent的事件处理程序。在控制器中,actionHandler属性应该有正确的事件处理程序类型。对于onAction事件,属性应该如下所示:
@FXML
public EventHandler<ActionEvent> getActionHandler() {
return event -> {
// handle the event
};
}
现在我们已经对 FXML 文件格式有了透彻的理解,我们可以有效地利用 GUI 编辑的便利来创建 FXML 文件。
使用 JavaFX 场景构建器
在前面的章节中,您学习了 FXML 文件格式的基础知识。在尝试使用和理解 JavaFX Scene Builder 工具时,这些知识会非常有用。在本章的最后一节,我们将深入探讨 JavaFX Scene Builder 的用法。
因为设计 UI 是非常主观的,有时是艺术的努力,所以很大程度上取决于手边的应用程序以及 UI 和用户体验团队的设计敏感度。我们并不假装知道做 UI 设计的最好方法。因此,在本节中,我们将向您介绍 JavaFX Scene Builder 2.0 本身,向您指出 UI 设计器的各个部分,并讨论如何转动旋钮和切换齿轮以实现所需的结果。
JavaFX 场景构建器概述
当您启动 JavaFX Scene Builder 时,屏幕看起来如图 4-9 所示。

图 4-9。
The JavaFX Scene Builder program
首次启动时,JavaFX Scene Builder UI 顶部有一个菜单栏,屏幕左侧有两个名为 Library 和 Document 的折叠容器,屏幕中间有一个内容面板,屏幕右侧有一个名为 Inspector 的折叠容器。
了解菜单栏和菜单项
JavaFX Scene Builder 中有九个菜单。让我们一个一个地检查它们。
文件菜单如图 4-10 所示。

图 4-10。
The File menu
新的、打开、保存、另存为、恢复到已保存、在资源管理器(或 Finder,或桌面)中显示、关闭窗口和退出菜单项做了你认为它们应该做的事情。“从模板新建”菜单项从现有模板创建新的 FXML 文件。模板列表如图 4-11 所示。

图 4-11。
The templates
“导入”菜单项允许您将另一个 FXML 文件的内容复制到当前 FXML 文件中。它还允许您将图像和媒体文件添加到当前 FXML 文件中。这样导入的文件被包装在一个ImageView或MediaView节点中。“包含”菜单项允许您将一个fx:include元素添加到当前的 FXML 文件中。“关闭窗口”菜单项关闭当前窗口中正在编辑的 FXML 文件。“首选项”菜单项允许您设置某些控制 JavaFX Scene Builder 外观的首选项。“退出”菜单项允许您完全退出 JavaFX Scene Builder 应用程序。在关闭应用程序之前,它会要求您保存任何未保存的文件。
编辑菜单如图 4-12 所示。

图 4-12。
The Edit menu
撤消、重做、剪切、复制、粘贴、粘贴到、复制、删除、全选、不选、选择父项、选择下一个和选择上一个菜单项都执行它们正常的功能。“根据所选内容裁剪文档”菜单项删除所有未选中的内容。
视图菜单如图 4-13 所示。

图 4-13。
The View menu
内容菜单项将焦点放在屏幕中间的内容面板上。“属性”、“布局”和“代码”菜单项将焦点放在屏幕右侧检查器面板中的属性、布局或代码部分。“隐藏库”命令隐藏屏幕左侧顶部的“库”面板。一旦库被隐藏,菜单项将改变为显示库。“隐藏文档”菜单项对屏幕左侧底部的“文档”面板执行相同的操作。“显示 CSS 分析器”菜单项显示 CSS 分析器,它最初是不显示的。“隐藏左面板”和“隐藏右面板”菜单项隐藏左面板(库面板和文档面板)或右面板(检查器面板)。“显示轮廓”菜单项显示项目的轮廓。“显示样本数据”菜单项将显示TreeView、TableView和TreeTableView节点的样本数据,以帮助您可视化工作中的节点。示例数据不与 FXML 文件一起保存。“禁用对齐参考线”菜单项禁用在内容面板的容器中移动节点时显示的对齐参考线。这些对齐准则帮助您将节点定位在屏幕上的正确位置。“缩放”菜单项允许您更改内容面板的放大率。Show Sample Controller Skeleton 菜单项将打开一个对话框,显示基于在文档面板中进行的控制器设置和为 FXML 文件中的节点声明的fx:id的框架控制器类声明。
图 4-14 显示了带有 CSS 分析器的 JavaFX 场景构建器屏幕。

图 4-14。
The JavaFX Scene Builder screen with the CSS Analyzer shown
插入菜单如图 4-15 所示。

图 4-15。
The Insert menu
“插入”菜单包含子菜单和菜单项,允许您将不同种类的节点插入到内容面板中。子菜单及其菜单项表示与“库”面板中相同的层次结构。它们包括容器、控件、菜单、杂项、形状、图表和 3D 类别。我们将在后续章节中更详细地介绍这些节点。
修改菜单如图 4-16 所示。

图 4-16。
The Modify menu
“适合父节点”菜单项将扩展所选节点以填充一个AnchorPane容器,并将节点锚定到父节点的所有边上。使用计算尺寸菜单项会将所选元素的尺寸调整为USE_COMPUTED_SIZE。GridPane 子菜单包含与GridPane容器一起工作的项目。“设置效果”子菜单包含可以在当前节点上设置的每个效果的项目。添加弹出控件允许您向选定的节点添加一个ContextMenu或一个Tooltip。场景大小子菜单允许您将场景的大小更改为一些常见的大小,包括 320×240 (QVGA)、640×480 (VGA)、1280×800 和 1920×1080。
排列菜单如图 4-17 所示。

图 4-17。
The Arrange menu
“置于顶层”、“置于底层”、“置于顶层”和“置于底层”菜单项将选定节点移动到重叠节点的 z 顺序的前面、后面、上面或下面。“打包”子菜单包含每种容器类型的项目,并允许您将一组选定节点打包到容器中。例如,您可以选择将两个相邻的Label包装成一个HBox。“展开”菜单项从选定节点中移除容器。
预览菜单如图 4-18 所示。

图 4-18。
The Preview menu
“在窗口中显示预览”菜单项允许您在活动窗口中预览场景,以查看它在现实生活中的效果。这是最有用的菜单项,因为你会多次使用它。JavaFX 主题子菜单包含各种主题,您可以使用这些主题预览场景。“场景样式表”子菜单包含允许您添加、移除或编辑在预览期间应用到场景中的样式表的项目。“国际化”子菜单包含允许您添加、移除或编辑在预览期间使用的资源包的项目。预览尺寸子菜单包含预览期间首选屏幕尺寸的项目。
“窗口”菜单允许您在同时编辑的多个 FXML 文件之间切换。
“帮助”菜单显示 JavaFX Scene Builder 的联机帮助和“关于”框。
了解库面板
“库”面板位于左侧面板的顶部,可以使用“查看➤”“隐藏库”菜单项隐藏。它包含可以用来构建 UI 的容器和节点。图 4-19 显示了打开容器抽屉的库面板,显示了一些容器。你可以点击其他抽屉,看看里面装的是什么。图 4-20 显示了控件抽屉打开的库面板,显示了一些控件。
“库”面板顶部有一个搜索框。你可以在搜索框中输入一个容器或控件的名称,或者其他抽屉的名称。当您键入时,“库”面板会将其显示从折叠排列更改为单个列表,其中包含名称与搜索框中输入的名称相匹配的所有节点。这使您可以通过名称快速找到一个节点,而不必逐一查看抽屉。图 4-21 显示了搜索模式下的库面板。要退出搜索模式,只需单击搜索框右端的 x 标记。
找到容器或节点后,可以将其拖动到内容面板,拖动到文档面板中的层次树,或者双击它。将容器带到内容面板,然后用控件和其他节点填充容器,这就是在 JavaFX Scene Builder 中构建 UI 的方式。

图 4-20。
The Library panel with its Controls drawer open

图 4-19。
The Library panel with its Containers drawer open
搜索框的右侧是一个菜单按钮,其中包含几个菜单项和一个改变“库”面板行为的子菜单。图 4-22 显示了该菜单按钮的可用内容。“以列表形式查看”菜单项将“库”面板从在几个部分中显示其节点更改为一起显示其节点,而不显示部分。“按节查看”将“库”面板从在一个列表中显示其节点更改为在几个节中显示其节点。“导入 JAR/FXML 文件”菜单项允许您将外部 JAR 文件或 FXML 文件作为自定义组件导入 JavaFX Scene Builder。“导入选择”菜单项允许您将当前选定的节点作为自定义组件导入到 JavaFX Scene Builder 中。“自定资源库文件夹”子菜单包含两个菜单项。“在资源管理器中显示”菜单项打开操作系统的文件资源管理器(或 Finder)中保存自定义组件的文件夹,允许您删除任何导入的自定义库。“显示 jar 分析报告”菜单项显示一个报告,该报告显示 JavaFX Scene Builder 对导入的 JAR 文件的评估。

图 4-22。
The Library panel with its menu open

图 4-21。
The Library panel in search mode
为了说明如何将自定义组件导入 JavaFX Scene Builder,我们将上一节的 custom component 示例中的类文件和 FXML 文件打包到一个CustomComponent.jar文件中。然后我们调用 Import JAR/FXML File 菜单项,导航到目录,并选择要导入的CustomComponent.jar文件。我们一点击文件选择对话框中的打开按钮,JavaFX Scene Builder 就会打开导入对话框,如图 4-23 所示。

图 4-23。
The Import dialog for importing CustomComponent.jar
我们可以通过单击左侧列表中的定制组件名称来检查 jar 文件中包含的每个定制组件。关于所选定制组件的信息,包括定制组件的可视化表示,显示在屏幕的右侧。我们可以通过选择组件名称旁边的复选框来选择要导入的自定义组件。然后,我们单击 Import Component 按钮完成导入过程。导入后,ProdId 自定义组件将显示在“库”面板的“自定义”部分,并且可以添加到构建的任何其他 ui 中。
了解文档面板
文档面板位于左侧面板的底部,可以使用“查看➤”“隐藏文档”菜单项隐藏。它包含两个部分。“层次结构”部分显示添加到内容面板的所有节点的树视图,按包含关系组织。因为内容面板中节点的布局可能会使从内容面板中选择节点变得棘手,所以在“文档”面板的“层次结构”部分进行选择可能会更容易。
图 4-24 显示了清单 4-4 中 FXMLLoaderExample 中 FXML 文件的文档面板的层次结构部分。您可以看到选择 WebView 节点后展开的节点树。

图 4-24。
The Hierarchy section of the Document panel for FXMLLoaderExample.fxml
控制器部分显示关于 FXML 文件控制器的信息。图 4-25 显示了清单 4-7 中 FXMLInjectionExample 中 FXML 文件的文档面板控制器部分。您可以在此部分设置控制器类的名称。在本节中,您还可以选择使用 FXML 文件的fx:root结构。您还会看到一个带有已经设置好的fx:id的节点列表,您可以通过单击 Assigned fx:id 表中的行来选择节点。

图 4-25。
The Controller section of the Document panel for FXMLInjectionExample.fxml
文档面板的右上角有一个菜单按钮。它包含一个层次显示子菜单,该子菜单有三个菜单项,如图 4-26 所示。Info 菜单项使 Hierarchy 部分显示每个节点及其一般信息,通常也显示在同一节点的内容面板中。fx:id 菜单项使 Hierarchy 部分显示每个节点及其 fx:id(如果已设置)。“节点 ID”菜单项使“层次”部分显示每个节点及其节点 Id(如果已设置)。CSS 使用节点 ID 来查找节点并操作节点的样式。

图 4-26。
The Document panel with its menu open
了解内容面板
内容面板是组成 UI 的地方。首先将一个容器拖动到内容面板。然后,将其他节点拖到内容面板上,并定位到容器节点上。当您拖动节点时,当您拖动的节点到达特定的对齐和间距位置时,会出现红色的指引线。在这些指导方针的帮助下,您应该能够创建视觉上令人愉悦的 ui。
在内容面板上方,有一个面包屑条,显示内容区域中所选节点的路径。这使您可以轻松导航到当前选定节点的包含节点。出现这种情况时,JavaFX Scene Builder 会在该栏中显示警告和错误消息。
JavaFX Scene Builder 的一个便利功能是,当您选择了几个节点时,您可以通过右键单击选定的节点,选择“包裹”子菜单,然后选择其中一种容器类型来进入上下文菜单。也可以通过这种方式展开节点,移除包含该节点的任何容器。
图 4-27 显示了清单 4-20 中的IncludeExampleTree.fxml文件正在 JavaFX 场景构建器中编辑。

图 4-27。
The IncludeExampleTree.fxml file being edited in JavaFX Scene Builder
了解检查器面板
检查器面板位于右侧面板中,可以使用“查看➤”“隐藏右侧面板”菜单项隐藏。它包含属性、布局和代码部分。“属性”部分列出了内容面板中选定节点的所有常规属性。您可以通过更改此处显示的值来设置属性。您还可以通过调用属性右侧的小菜单按钮将属性改回默认值。您可以在 ID 属性编辑器的属性部分设置节点 ID。图 4-28 显示了检查器面板的属性部分。

图 4-28。
The Properties section of the Inspector panel
布局部分列出了当前选定节点的所有与布局相关的属性。图 4-29 显示了检查器面板的布局剖面。

图 4-29。
The Layout section of the Inspector panel
代码部分列出了内容面板中选定节点可能拥有的所有事件处理程序。它还允许您设置所选节点的fx:id。您可以在代码部分以任何方式连接事件处理程序,但是提供事件处理程序最方便的方式是将它们设置为控制器中正确签名的方法。图 4-30 显示了检查面板的代码部分。

图 4-30。
The Code section of the Inspector panel
摘要
在本章中,您学习了在 JavaFX 中创建 UI 的声明式方法。您学习了以下重要工具和信息:
- FXML 文件是声明性 UI 信息的载体,是 JavaFX 项目的核心资产。
- FXML 文件由
FXMLLoader加载到 JavaFX 应用程序中。加载的结果是一个可以合并到一个Scene中的节点。 - FXML 文件可以有一个配套的控制器类,它在运行时代表 FXML 文件中声明的节点执行编程功能,如事件处理。
- FXML 文件可以在您喜欢的 Java IDEs 中使用智能建议和补全功能轻松编辑。
- FXML 文件也可以在 Gluon Scene Builder 9.0 中编辑,这是一个用于编辑 FXML 文件的开源工具。
- JavaFX Scene Builder 是指定 JavaFX UIs 的高效工具。您可以将容器、控件和其他 JavaFX 节点添加到 FXML 文件的内容中。
- 您可以设置一个控制器,并定义场景中各个节点的
fx:ids。 - 通过操作“文档”面板的“层次”部分中的容器、控件和其他节点,可以组织 FXML 文件中的层次信息。
- 通过使用“检查器”面板中的“属性”、“布局”和“代码”部分,可以操作 FXML 文件中节点的属性。
- 您可以在内容面板中直观地编写您的 UI。
- 你可以用 CSS 分析器分析用户界面的 CSS。
资源
- 胶子场景生成器信息站点:
http://gluonhq.com/products/scene-builder - Jasper Pott 的博文宣布发布 JavaFX 场景构建器 2.0:
http://fxexperience.com/2014/05/announcing-scenebuilder-2-0/ - 展示 JavaFX Scene Builder 2.0 功能的九分钟视频(伴随 Jasper Pott 的发布):
https://www.youtube.com/watch?v=ij0HwRAlCmo&feature=youtu.be - e(fx)clipse Eclipse 插件为 Eclipse IDEs 提供 JavaFX 支持:
www.eclipse.org/efxclipse/install.html
992

被折叠的 条评论
为什么被折叠?



