原文地址:Using JavaFX Properties and Binding
通过这份指南你可以学到如何在JavaFX中使用属性及其绑定。
本指南描述了相关的api,并提供了可编译运行的示例代码。
概述
许多年来,Java语言都使用JavaBeans组件体系结构来表示对象的属性(Properties)。此模型由一套API和一种设计模式组成;这已经是为广大的Java应用程序开发人员和开发工具所普遍了解的了。在本次版本上,表示对象属性(Properties)的支持被添加进了JavaFX,这个支持建立在久经考验的JavaBeans模型基础上,并在其上做了扩展和提升。
JavaFX的属性(Properties)通常与绑定(Binding)(一种用来表示变量之间直接关系的强大机制)一起使用。若对象参与了绑定(Binding),此时对一个对象所做的更改将自动反映到另一个对象上。这种机制对于各种各样的应用程序来说可能都是有用的。例如,可用于账单跟踪程序,当单张账单发生了变动,总账单就会自动被更新。或者也可用于一个图形用户界面(GUI),以此实现应用程序的数据的显示能够同步更新。
绑定(Binding)是由一个或多个依赖项组装而成的。绑定(Binding)监测依赖项列表的变动,并在发现依赖项变动时自动更新。
绑定(Binding)的API分为两大类:
高级API:提供了一种简单的途径来创建最常见的绑定(Binding)。它的语法很容易学习和使用,特别是在那些提供代码补全的编程环境里,比如NetBeans IDE。
低级API:提供了额外的灵活性,可以由高级开发人员在高级API不够用的情况下使用。低级API是为快速执行和小内存占用而设计的。
本教程的其余部分描述这些api,并提供了代码示例,你可以编译和运行它。
理解属性(Properties)
正如在概述里提到的,JavaFX的属性(Properties)支持是以建立在JavaBeans组件体系结构上的属性模型(property model)为基础的。本节先简要概述这意味着什么,然后再解释如何在JavaFX中应用属性(Properties)。
Java语言可以把数据封装到一个对象里,但并没有对你定义的方法做任何强制的命名约定。例如,你的代码可能会定义一个Person类,封装了first name和last name。没有命名约定的话,不同的开发人员完全可能为这些方法选择不同的命名:read_first(),firstName()或getFN()等都是完全合法的。然而,这样将无法保证这些名字对于其他开发人员来说也是能见名知义的。
JavaBeans组件体系结构通过定义一些简单的对所有程序都相容的命名约定来解决这个问题。在JavaBeans编程中,这些方法的完整签名是: public void setFirstName(String name), public String getFirstName(), public void setLastName(String name), and public String getLastName()。这种命名模式对于开发人员或者开发工具(例如NetBeans IDE)来说都是很容易辨认的。通过这样的定义,按JavaBeans术语来说,这时的Person对象包含有firstName和lastName属性。
JavaBeans模型还提供了对复杂属性类型的支持,和附加一个事件分发系统。它还包含许多支持类,所有可用的API都在java.bean包底下。因此,掌握JavaBeans编程涉及到学习所需的命名约定和其相应的API。(更多关于JavaBeans的背景阅读请看 the JavaBeans lesson of the Java Tutorial)。
同样的,理解JavaFX的属性(Properties)还需要学习一些新的API和命名约定。在JavaFX中,你完全有可能只感兴趣于使用那些包含属性(Properties)的类(而不是在你的定制类中自己实现),例1 - 1会带你熟悉JavaFX属性模式形式下的新的方法命名约定。例1 - 1定义了一个Bill类,并含有一个单独的属性amountDue。
例1 - 1 定义一个属性
package propertydemo;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
class Bill {
// 定义一个保存属性的变量
private DoubleProperty amountDue = new SimpleDoubleProperty();
// 定义一个读取器来获取属性的值
public final double getAmountDue(){return amountDue.get();}
// 定义一个设置器来设置属性的值
public final void setAmountDue(double value){amountDue.set(value);}
// 定义一个获取属性自身的读取器
public DoubleProperty amountDueProperty() {return amountDue;}
}
amountDue对象——一个javafx.beans.property.DoubleProperty的实例——被标记为private以使其对外不可见。这些在Java和JavaBeans应用程序开发中都是一种标准实践。然而需要注意的是,此对象的类型不是一个标准的Java原语,而是个封装了一个Java原语并添加了一些额外的功能的新的包装类(所有位于javafx.beans.property包底下的类都内建对属性与绑定的支持)。
属性方法命名约定如下:
getAmountDue()
方法是一个标准的值读取器,它返回amountDue属性的当前值。按照约定, 此方法被声明为final
。注意它的返回类型是double,
而不是DoubleProperty
。setAmountDue(double)
方法(也被声明为final)是一个标准的设置器,它允许调用者去设置属性值。设置器是可有可无的,它接收的参数类型也是double
。最后,
amountDueProperty()
方法是一个属性读取器。这是一种新的约定,先是写上属性名(在本例中就是amountDue
), 后面再接上“Property”。方法的返回类型则与属性自身的类型相同(在本例中就是DoubleProperty
).
当用JavaFX构建GUI应用程序时,你会发现某些类的API已经有了对属性的实施。例如,javafx.scene.shape.Rectangle类包含了arcHeight, arcWidth, height, width, x, and y等属性。这些属性将会有与先前描述的约定匹配的相应的方法。例如,getArcHeight(),setArcHeight(double),arcHeightProperty(),它们一起(对开发人员与工具)指明了给定属性(Properties)的存在。
你还可以通过添加一个change listener来收到属性值的变动通知。如例1 - 2所示。
例1 - 2 使用ChangeListener
package propertydemo;
import javafx.beans.value.ObservableValue;
import javafx.beans.value.ChangeListener;
public class Main {
public static void main(String[] args) {
Bill electricBill = new Bill();
electricBill.amountDueProperty().addListener(new ChangeListener(){
@Override public void changed(ObservableValue o,Object oldVal,
Object newVal){
System.out.println("Electric bill has changed!");
}
});
electricBill.setAmountDue(100.00);
}
}
运行这份代码将会在控制台打印"Electric bill has changed",这说明change listener起了作用。
使用高级绑定API
高级API是在你的应用程序中开始使用绑定的最快和最简单的方法。它由两部分组成:流式API(Fluent API),和Bindings类。流式API公布在各种依赖对象上的方法,而绑定类则提供了静态工厂方法。
为了开始使用流式API,我们来考虑一个简单的案例:绑定两个整数以使它们的值在任何时候都会加到一起。在例1-3中,涉及到三个变量:num1(依赖项),num2(依赖项),和sum(绑定项)。依赖项类型均为IntegerProperty,绑定项则是NumberBinding。
例1 - 3 使用流式API
package bindingdemo;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.binding.NumberBinding;
public class Main {
public static void main(String[] args) {
IntegerProperty num1 = new SimpleIntegerProperty(1);
IntegerProperty num2 = new SimpleIntegerProperty(2);
NumberBinding sum = num1.add(num2);
System.out.println(sum.getValue());
num1.set(2);
System.out.println(sum.getValue());
}
}
这份代码绑定了两个依赖项,并打印它们的和(sum变量),接着改变num1变量的值再打印一遍和。输出结果为“3”和“4”。这说明绑定起了作用。
你还可以使用Bindings类来完成相同的事情,如例1-4所示。
例1 - 4 使用Bindings类
package bindingdemo;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.binding.NumberBinding;
import javafx.beans.binding.Bindings;
public class Main {
public static void main(String[] args) {
IntegerProperty num1 = new SimpleIntegerProperty(1);
IntegerProperty num2 = new SimpleIntegerProperty(2);
NumberBinding sum = Bindings.add(num1,num2);
System.out.println(sum.getValue());
num1.setValue(2);
System.err.println(sum.getValue());
}
}
例1-5结合了两种方式:
例1 - 5 结合两种方式
package bindingdemo;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.binding.NumberBinding;
import javafx.beans.binding.Bindings;
public class Main {
public static void main(String[] args) {
IntegerProperty num1 = new SimpleIntegerProperty(1);
IntegerProperty num2 = new SimpleIntegerProperty(2);
IntegerProperty num3 = new SimpleIntegerProperty(3);
IntegerProperty num4 = new SimpleIntegerProperty(4);
NumberBinding total =
Bindings.add(num1.multiply(num2),num3.multiply(num4));
System.out.println(total.getValue());
num1.setValue(2);
System.err.println(total.getValue());
}
}
例1-5把代码改为调用来自流式API的multiply方法,并从Bindings类调用add静态方法。你可以从中了解到高级API允许你在做算术运算时混合类型,并且运算结果的类型的约定跟Java语言的约定相同。
如果其中一个操作数是double类型,则结果也为double类型。
否则若其中一个是float类型,则结果为float类型。
否则若其中一个是long类型,则结果为long类型。
否则结果为int类型。
下一节将探索可观察性(observability),并通过示例展示invalidation listeners和change listeners的差别。
探索 Observable,ObservableValue,InvalidationListener和ChangeListener
绑定API定义了一系列的接口,这些接口使得对象能在值发生变动后收到变动或者失效的通知。Observable和 ObservableValue接口发出变动通知,InvalidationListener和ChangeListener接口则负责接收这些通知。不同在于ObservableValue包裹了一个值并会发送其值的变动消息给那些已注册的ChangeListener,而Observable没有包裹任何值,仅仅把变动消息发送给已注册的InvalidationListener。
JavaFX的绑定和属性都支持惰性求值,这意味着当发生了变动,并不会立即去重新计算值,仅当这个值随后被请求了才会去重新计算。
在例1-6中,账单的总额(一个绑定项)会在第一次监测到其中一个依赖项发生变动后被置为失效,然而这个绑定项会在被再次请求时重新计算。
例1 - 6 使用InvalidationListener
package bindingdemo;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.binding.NumberBinding;
import javafx.beans.binding.Bindings;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
class Bill {
// 定义一个属性
private DoubleProperty amountDue = new SimpleDoubleProperty();
// 定义一个读取器来获取属性的值
public final double getAmountDue(){return amountDue.get();}
// 定义一个设置器来设置属性的值
public final void setAmountDue(double value){amountDue.set(value);}
// 定义一个获取属性自身的读取器
public DoubleProperty amountDueProperty() {return amountDue;}
}
public class Main {
public static void main(String[] args) {
Bill bill1 = new Bill();
Bill bill2 = new Bill();
Bill bill3 = new Bill();
NumberBinding total =
Bindings.add(bill1.amountDueProperty().add(bill2.amountDueProperty()),
bill3.amountDueProperty());
total.addListener(new InvalidationListener() {
@Override public void invalidated(Observable o) {
System.out.println("The binding is now invalid.");
}
});
// 第一次调用使得绑定项变为失效
bill1.setAmountDue(200.00);
// 此时绑定项为失效
bill2.setAmountDue(100.00);
bill3.setAmountDue(75.00);
// 请求total值(调用getValue())使绑定项变为有效
System.out.println(total.getValue());
// 变为失效
bill3.setAmountDue(150.00);
// 变为有效
System.out.println(total.getValue());
}
}
在改变了单张账单的值后,绑定项就变成了失效的了,同时invalidation listener会被触发。但如果绑定项在之前已经是失效状态的话,则不会被触发。即使其他依赖项再次发生了变动(在例1-6中,调用total.getValue()会使绑定项从失效变为有效)。我们可以通过在调用total.getValue()之后,invalidation listener会在随后第一次变动任一依赖项后被触发一次,而当其仍为失效状态时永远没被触发的这个事实来了解到这一点。
需注意到注册一个ChangeListener将会强制执行立即求值,尽管ObservableValue是被实现为支持惰性求值的。对于惰性求值来说,直到被重新计算之前都不可能确切地知道一个已失效的值是否发生了变动。为此,失效事件在懒惰或立即的实现上都可以被发出,而发生了变动事件后则需要立即求值[*1]。
使用低级绑定API
如果高级API不能满足你程序的需求,你可以使用低级API。低级API对于开发人员来说能比高级API提供更多的灵活性。例1-7演示了一个使用低级API的简单例子。
例1 - 6 使用低级API
package bindingdemo;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.binding.DoubleBinding;
public class Main {
public static void main(String[] args) {
final DoubleProperty a = new SimpleDoubleProperty(1);
final DoubleProperty b = new SimpleDoubleProperty(2);
final DoubleProperty c = new SimpleDoubleProperty(3);
final DoubleProperty d = new SimpleDoubleProperty(4);
DoubleBinding db = new DoubleBinding() {
{
super.bind(a, b, c, d);
}
@Override
protected double computeValue() {
return (a.get() * b.get()) + (c.get() * d.get());
}
};
System.out.println(db.get());
b.set(3);
System.out.println(db.get());
}
}
使用低级API需要继承一个绑定类并且重写其负责返回绑定项当前值的computeValue()方法。例1-7自定义了一个DoubleBinding的子类。调用super.bind()可以把所有依赖项传递给其父类以此得以保留DoubleBinding的缺省的‘失效’行为。一般来说对绑定项是否失效的检查不需要你去做了,因为这些行为已经由基类为你提供了。
到此为止,你对开始使用低级API已了解到了足够的信息。
[*1]译者注:读者可在此代码基础上在total上再添加一个ChangeListener,在重写其changed方法时打印第三个参数newValue,这时再运行代码则会看到打印出来的newValue的值都是最新的total值,而且每次依赖项值变动都会触发一次InvalidationListener,这时的invalidation listener不需要total值被再次请求也可以被立即触发了。