来源:《Learn JavaFX 8: Building User Experience and Interfaces with Java 8》
在这一章中,您到学到:
- 什么是JavaFX中的属性
- 如何去创造属性对象并且使用它
- JavaFX中属性类的层次结构
- 如何处理属性对象的失效和改变事件
- 什么是JavaFX中的绑定,并且如何使用单向绑定和双向绑定
- 关于JavaFX中高级绑定和低级绑定API
本章讨论Java和JavaFX中的属性和绑定。如果您有使用JavaBeans API进行属性和绑定的经验,那么您可以跳过讨论Java中的属性和绑定的前几节,并从“理解JavaFX中的属性”一节开始。
什么是属性?
一个Java类可以包含两种类型的成员:字段和方法。字段表示对象的状态,它们被声明为private。公共方法,例如访问器或getter和setter,用于读取和修改私有字段。简单地说,对其部分或全部私有字段具有公共访问器的Java类称为Java bean,而访问器定义了bean的属性。Java bean的属性允许用户自定义其状态、行为或两者。
Java bean是可观察的,它们支持在属性更改时发送通知。当Java bean的公共属性发生更改时,将会向所有感兴趣的侦听器发送通知。
本质上,Java bean定义了可重用的组件,构建器工具可以组装这些组件来创建Java应用程序。这为第三方开发Java bean打开了大门,并使它们可供其他人重用。
属性可以是只读、只写或者读写。只读的属性有getter但没有setter。只写的属性有setter但没有getter。读写的属性有一个getter和一个setter。
Java IDEs和其他构建工具(例如,GUI布局构建器)使用自检来获取bean的属性列表,并允许您在设计时操作这些属性。Java bean既可以是可视化的,也可以是非可视化的。bean的属性在构建工具或者编程中被使用。
JavaBeans API提供了一个类库,通过java.bean包和命名约定来创建并使用Java bean。下面是一个具有读写的name属性的Person bean的示例。getName()方法(getter)返回name字段的值。setName()方法(setter)设置name字段的值:
// Person.java
package com.jdojo.binding;
public class Person {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
按照约定,getter和setter方法的名称是通过将属性的名称(第一个字母是大写的)分别附加到单词get和set之后来创建的。getter方法不应该接受任何参数,它的返回类型应该与字段的类型相同。setter方法应该接受一个参数,它的类型应该与字段的类型相同,并且它的返回类型应该是void。
下面的代码片段以编程的方式操作Person bean的name属性:
Person p = new Person();
p.setName("John Jacobs");
String name = p.getName();
一些面向对象的编程语言,例如C#,提供了第三种类型的类成员,称为属性(property)。属性用于从类外部读取、写入和计算私有字段的值。C#允许你用Name属性声明Person类,如下所示:
// C# version of the Person class
public class Person {
private string name;
public string Name {
get { return name; }
set { name = value; }
}
}
在C#中,下面的代码片段使用Name属性操作name私有字段,它相当于前面显示的Java版本的代码:
Person p = new Person();
p.Name = "John Jacobs";
string name = p.Name;
如果属性的访问器用于执行返回和设置字段值的例行工作,C#提供了一个紧凑的格式来定义这样的属性。在这种情况下,甚至不需要声明私有字段。你可以用C#重写Person类,如下所示:
// C# version of the Person class using the compact format
public class Person {
public string Name { get; set; }
}
那么,什么是属性?属性是类的公共的可访问特征,它影响类的状态和行为。即使一个属性是公开可访问的,它的使用(读写)调用隐藏的实际实现的方法来访问数据。属性是可观察的,因此当其值发生变化时,相关方会得到通知。
本质上,属性定义了对象的公共状态,可以读、写和观察。与C#等其他编程语言不同,Java中的属性在语言级别上并不受支持。Java对属性的支持来自JavaBeans API和设计模式。有关Java中属性的更多详细信息,请参考JavaBeans规范,该规范可以从http://www.oracle.com/technetwork/java/javase/overview/spec-136004.html下载。
除了简单的属性(如Person bean的name属性)之外,Java还支持索引、绑定和约束属性。索引属性是使用索引访问的值数组。索引属性是使用数组的数据类型实现的。绑定属性发生更改时,会向所有侦听器发送通知。约束性属性是一个绑定属性,侦听器可以在其中否决更改。
什么是绑定?
在编程中,绑定这个术语被使用在许多不同的语境中。在这里,我想在数据绑定的语境中定义它。数据绑定定义了程序中数据元素(通常是变量)之间的关系,以保持它们的同步。在GUI应用程序中,数据绑定经常用于同步数据模型中的元素与相应的UI元素。
考虑下面的语句,假设x,y,z是数值变量:
x = y + z;
上面的语句定义了x、y和z之间的绑定关系。当它被执行时,x的值与y的值和z的值的总和同步。绑定还有一个时间因素。在上面的语句中,x的值绑定到y的值和z的值的总和,并且在语句执行时有效。x的值可能不是执行在上述语句前后的y的值和z的值的总和。
有时需要绑定保持一段时间。思考下面的语句,它使用listPrice、discounts和taxes定义了一个绑定:
soldPrice = listPrice - discounts + taxes;
在本例中,您希望使绑定永远有效,以便在listPrice、discounts和taxes发生变化时正确地计算销售价格。
在上面的绑定中,listPrice、discounts和taxes被称为依赖项,可以说soldPrice被listPrice、discounts和taxes绑定。
要使绑定正常地工作,必须在其依赖项发生更改时通知绑定。支持绑定的编程语言提供了一种用依赖项注册的侦听器机制。当依赖项变为无效或发生更改时,将通知所有的侦听器。绑定可以在接收到此类通知时将自身与其依赖项进行同步。
绑定可以是eager binding或lazy binding。在eager binding中,被绑定的变量在其依赖项更改后立即重新计算。在lazy binding中,当绑定变量的依赖项发生变化时,不会立即重新计算。而是在下次读取时再重新计算。lazy binding比eager binding性能更好。
绑定可以是单向的,也可以是双向的。单向绑定只在一个方向上工作,依赖项中的更改被传播到绑定变量上。双向绑定是在两个方向上工作。在双向绑定中,被绑定的变量和依赖项保持彼此的值同步。通常,双向绑定只定义在两个变量之间。例如,双向绑定 x = y 和 y = x 声明了x和y的值总是相同的。
在数学上,不可能在多个变量之间定义一个双向绑定。在上面的例子中,销售价格定义的是单向绑定。如果您希望使其成为双向绑定,那么当销售价格发生变化时,计算标价、折扣和税金的值的可能性就不是唯一的,它们有无限的可能性。
带有GUI的应用程序为用户提供UI小部件,例如文本字段、复选框和按钮,用于操作数据。UI小部件中显示的数据必须与底层数据模型同步,反之亦然。在这种情况下,需要双向绑定来保持UI和数据模型同步。
理解JavaBeans中的绑定支持
在讨论Java FX属性和绑定之前,让我们先简要介绍一下JavaBeans API中的绑定支持。如果以前使用过JavaBeans API,可以跳过本节。
Java从早期版本中就开始支持bean属性的绑定。表2-1显示了一个Employee bean,它有两个属性,名称和工资。
表2-1. 具有名称和工资两个属性的Employee bean
// Employee.java
package com.jdojo.binding;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
public class Employee {
private String name;
private double salary;
private PropertyChangeSupport pcs = new PropertyChangeSupport(this);
public Employee() {
this.name = "John Doe";
this.salary = 1000.0;
}
public Employee(String name, double salary) {
this.name = name;
this.salary = salary;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getSalary() {
return salary;
}
public void setSalary(double newSalary) {
double oldSalary = this.salary;
this.salary = newSalary;
// Notify the registered listeners about the change
pcs.firePropertyChange("salary", oldSalary, newSalary);
}
public void addPropertyChangeListener(PropertyChangeListener listener) {
pcs.addPropertyChangeListener(listener);
}
public void removePropertyChangeListener(PropertyChangeListener listener) {
pcs.removePropertyChangeListener(listener);
}
@Override
public String toString() {
return "name = " + name + ", salary = " + salary;
}
}
Employee bean的两个属性都是读写的。salary属性也是一个绑定属性,它的setter访问器在salary发生变化时生成属性改变的通知。
感兴趣的侦听器可以使用addPropertyChangeListener()方法和removePropertyChangeListener()方法注册或注销属性改变的通知。PropertyChangeSupport类是JavaBeans API的一部分,它促进了属性更改监听器的注册和删除以及属性更改通知的触发。
任何对基于工资变化同步值感兴趣的一方都需要向Employee bean注册,并在收到更改通知时采取必要的操作。
表2-2显示了如何为Employee bean注册工资变化的通知。下面的输出显示工资变化通知只触发了两次,而setSalary()方法被调用了三次。这毫无问题,因为第二次调用setSalary()方法时使用了与第一次调用相同的工资数额,并且PropertyChangeSupport类足够智能,可以检测到这一点。该示例还展示了如何使用JavaBeans API绑定变量。雇员的税款是按百分比计算的。在JavaBeans API中,属性更改通知被用于绑定变量。
表2-2. 一个EmployeeTest类,被用于测试Employee Bean的工资变化
// EmployeeTest.java
package com.jdojo.binding;
import java.beans.PropertyChangeEvent;
public class EmployeeTest {
public static void main(String[] args) {
final Employee e1 = new Employee("John Jacobs", 2000.0);
// Compute the tax
computeTax(e1.getSalary());
// Add a property change listener to e1
e1.addPropertyChangeListener(EmployeeTest::handlePropertyChange);
// Change the salary
e1.setSalary(3000.00);
e1.setSalary(3000.00); // No change notification is sent.
e1.setSalary(6000.00);
}
public static void handlePropertyChange(PropertyChangeEvent e) {
String propertyName = e.getPropertyName();
if ("salary".equals(propertyName)) {
System.out.print("Salary has changed. ");
System.out.print("Old:" + e.getOldValue());
System.out.println(", New:" + e.getNewValue());
computeTax((Double)e.getNewValue());
}
}
public static void computeTax(double salary) {
final double TAX_PERCENT = 20.0;
double tax = salary * TAX_PERCENT/100.0;
System.out.println("Salary:" + salary + ", Tax:" + tax);
}
}
Salary:2000.0, Tax:400.0
Salary has changed. Old:2000.0, New:3000.0
Salary:3000.0, Tax:600.0
Salary has changed. Old:3000.0, New:6000.0
Salary:6000.0, Tax:1200.0
理解JavaFX中的属性
JavaFX通过属性和绑定APIs支持属性、事件和绑定。JavaFX中的属性支持是JavaBeans属性的巨大飞跃。
JavaFX中的所有属性都是可观察的,可以观察到它们的失效和值的变化,可以是读写属性或是只读属性,所有的读写属性都支持绑定。
在JavaFX中,属性可以表示单个值或者是值的集合,本章涵盖了表示单个值的属性,我们将在第三章讨论表示值的集合的属性。
在JavaFX中,属性是对象。每种类型的属性都有一个属性类层次结构。例如,IntegerProperty、DoubleProperty和StringProperty类分别表示int、double和String类型的属性。这些属性类是抽象的。它们有两种实现类:一种表示读写属性,另一种表示只读属性的包装器。例如,SimpleDoubleProperty类和ReadOnlyDoubleWrapper类是具体的实现类,它们的对象分别代表读写属性和只读属性。
下面是一个如何创建初始值为100的IntegerProperty类对象的例子:
IntegerProperty counter = new SimpleIntegerProperty(100);
属性类分别提供两对getter和setter方法:get()、set() 和 getValue()、 setValue()。get()和set()方法分别获取和设置属性的值。对于基本类型的属性,它们使用基本类型的值。例如,对于IntegerProperty,get()方法的返回值类型和set()方法的参数类型都是int。getValue()和setValue()方法用于对象类型,例如,对于IntegerProperty来说,返回值类型和参数类型均为Integer。
对于引用类型的属性,例如 StringProperty 和 ObjectProperty<T>, 两对 getter 和 setter 都是使用对象类型。也就是说,StringProperty 的 get() 和 getValue() 方法都将返回一个 String, set() 和 setValue() 方法都将接受一个 String 参数。对于基本类型的自动装箱,使用哪个版本的 getter 和 setter 并不重要。getValue() 和 setValue() 方法的存在是为了帮助您根据对象类型编写泛型代码。
下面的代码片段使用 IntegerProperty 及其 get() 和 set() 方法。counter属性是一个读写属性,因为它是 SimpleIntegerProperty 类的对象:
IntegerProperty counter = new SimpleIntegerProperty(1);
int counterValue = counter.get();
System.out.println("Counter:" + counterValue);
counter.set(2);
counterValue = counter.get();
System.out.println("Counter:" + counterValue);
Counter:1
Counter:2
使用只读属性有点棘手。ReadOnlyXXXWrapper类包装了两个XXX类型的属性:一个是只读的,另一个是读写的。两个属性都是同步的。它的getReadOnlyProperty()方法返回一个ReadOnlyXXXProperty对象。
下面的代码片段展示了如何创建一个只读的Integer属性。idWrapper属性是读写的,而id属性是只读的。当idWrapper中的值发生变化时,id中的值也会自动发生变化:
ReadOnlyIntegerWrapper idWrapper = new ReadOnlyIntegerWrapper(100);
ReadOnlyIntegerProperty id = idWrapper.getReadOnlyProperty();
System.out.println("idWrapper:" + idWrapper.get());
System.out.println("id:" + id.get());
// Change the value
idWrapper.set(101);
System.out.println("idWrapper:" + idWrapper.get());
System.out.println("id:" + id.get())
idWrapper:100
id:100
idWrapper:101
id:101
通常,包装器属性用作类的私有实例变量,类可以在内部更改属性。它的一个方法返回包装器类的只读属性对象,因此相同的属性对于外部世界是只读的。
您可以使用表示单个值的七种类型的属性。这些属性的基类被命名为XXXProperty,只读基类被命名为ReadOnlyXXXProperty,包装类被命名为ReadOnlyXXXWrapper。表2-1列出了每种类型的XXX值。
表2-1. 包装单个值的属性类列表
属性对象封装了三条信息:
- 包含它的bean的引用
- 一个名字
- 一个值
在创建属性对象时,您可以提供以上三条信息中的全部或不提供。具体的属性类,例如SimpleXXXProperty和ReadOnlyXXXWrapper,提供了四个构造函数,允许您提供这三个信息的组合。以下是SimpleIntegerProperty类的构造函数:
SimpleIntegerProperty()
SimpleIntegerProperty(int initialValue)
SimpleIntegerProperty(Object bean, String name)
SimpleIntegerProperty(Object bean, String name, int initialValue)
初始值的默认值取决于属性的类型。对于数值类型,它为零;对于布尔类型,它为假;对于引用类型,它为空。
属性对象既可以是bean的一部分,也可以是独立的对象。指定的bean是对包含该属性的bean对象的引用。对于独立的属性对象,它可以为null,它的默认值为null。
属性的名字是它的名称。如果未提供,则默认为空字符串。
下面的代码片段创建一个属性对象作为bean的一部分,并设置所有三个值。SimpleStringProperty类构造函数的第一个参数是this,它是Person bean的引用,第二个参数“name”是属性的名称,第三个参数“Li”是属性的值:
public class Person {
private StringProperty name = new SimpleStringProperty(this, "name", "Li");
// More code goes here...
}
每个属性类都有getBean()和getName()方法,分别返回bean的引用和属性名。