1 使用JavaFX属性和绑定
在本教程中,您将学习如何在JavaFX应用程序中使用属性和绑定。
本教程描述了相关的API,并提供了可以编译和运行的示例。
概述
多年来,Java编程语言一直使用JavaBeans组件架构来表示对象的属性。该模型包括API和设计模式;它被Java应用程序开发人员和开发工具广泛理解。此版本将属性支持引入到JavaFX中,该支持基于经过验证的JavaBeans模型,但进行了扩展和改进。
JavaFX属性通常与绑定一起使用,绑定是一种表达变量之间直接关系的强大机制。当对象参与绑定时,对一个对象所做的更改将自动反映在另一个对象中。这在各种应用程序中都很有用。例如,绑定可以在账单发票跟踪程序中使用,当更改单个账单时,所有账单的总额将自动更新。或者,绑定可以在图形用户界面(GUI)中使用,该界面会自动与应用程序的底层数据保持同步。
绑定是由一个或多个源(称为依赖项)组装而成的。绑定会观察其依赖项列表以检测更改,然后在检测到更改后自动更新自身。
绑定API分为两个主要类别:
-
高级API:提供了一种简单的方式来创建最常见用例的绑定。其语法易于学习和使用,特别是在提供代码完成功能的环境中,如NetBeans IDE。
-
低级API:提供了额外的灵活性,可以由高级开发人员在高级API不足的情况下使用。低级API设计用于快速执行和小内存占用。
本教程的其余部分将描述这些API,并提供可以编译和运行的工作代码示例。
理解属性
如概述中所述,JavaFX属性支持基于JavaBeans组件架构建立的众所周知的属性模型。本节简要介绍了这意味着什么,然后解释了属性如何应用于JavaFX。
Java编程语言使得在对象内封装数据成为可能,但它不强制定义您定义的方法的任何特定命名约定。例如,您的代码可能定义了一个Person
类,它封装了一个名字和一个姓氏。但是,没有命名约定,不同的程序员可能会为这些方法选择不同的名称:read_first()
,firstName()
,getFN()
等都是完全有效的选择。然而,并不能保证这些名称对其他开发人员有意义。
JavaBeans组件架构通过定义一些简单的命名约定来解决这个问题,从而在项目之间带来一致性。在JavaBeans编程中,这些方法的完整签名将是:public void setFirstName(String name)
,public String getFirstName()
,public void setLastName(String name)
和public String getLastName()
。这种命名模式对人类程序员和编辑工具(如NetBeans IDE)都很容易识别。在JavaBeans术语中,Person
对象被称为包含firstName
和lastName
属性。
JavaBeans模型还提供了对复杂属性类型的支持,以及一个事件传递系统。它还包含许多支持类,全部都作为java.beans
包下的API可用。因此,掌握JavaBeans编程涉及学习所需的命名约定及其相应的API。(有关JavaBeans的更多背景阅读,请参阅Java教程中的JavaBeans教训)。
类似地,理解JavaFX属性还需要学习一些新的API和命名约定。在JavaFX中,您完全有可能只对包含属性的类感兴趣(而不是在自定义类中实现属性),但示例1-1将使您熟悉形成JavaFX属性模式的新方法命名约定。它定义了一个名为Bill
的类,该类实现了一个名为amountDue
的属性。
示例1-1 定义一个属性
package propertydemo; import javafx.beans.property.DoubleProperty; import javafx.beans.property.SimpleDoubleProperty; class Bill { // 定义一个变量来存储属性 private DoubleProperty amountDue = new SimpleDoubleProperty(); // 定义属性值的getter public final double getAmountDue(){return amountDue.get();} // 定义属性值的setter public final void setAmountDue(double value){amountDue.set(value);} // 定义属性本身的getter public DoubleProperty amountDueProperty() {return amountDue;} }
amountDue
对象是javafx.beans.property.DoubleProperty
类的一个实例,被标记为private
以封装它免受外部世界的影响。这是Java和JavaBeans应用程序开发中的标准做法。然而需要注意的是,该对象的类型不是标准的Java原始类型,而是一个新的包装类,它封装了一个Java原始类型并添加了一些额外的功能(javafx.beans.property
下的所有类都包含内置的支持可观察性和绑定的设计)。
属性方法的命名约定如下:
-
getAmountDue()
方法是一个标准的getter,返回amountDue
属性的当前值。按照约定,该方法被声明为final
。需要注意的是,该方法的返回类型是double
,而不是DoubleProperty
。 -
setAmountDue(double)
方法(也是final
)是一个标准的setter,允许调用者设置属性的值。setter方法是可选的。它的参数也是double
类型。 -
最后,
amountDueProperty()
方法定义了属性的getter。这是一种新的约定,其中方法名包含属性的名称(在本例中是amountDue
),后跟单词"Property"。返回类型与属性本身相同(在本例中是DoubleProperty
)。
在使用JavaFX构建GUI应用程序时,您会注意到API中的某些类已经实现了属性。例如,javafx.scene.shape.Rectangle
类包含了arcHeight
、arcWidth
、height
、width
、x
和y
的属性。对于每个属性,都会有相应的方法与先前描述的约定相匹配。例如,getArcHeight()
、setArcHeight(double)
、arcHeightProperty()
,它们一起表明(对开发人员和工具来说)给定的属性存在。
您还可以添加一个变化监听器,以在属性的值发生变化时得到通知,如示例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("电费账单已更改!"); } }); electricBill.setAmountDue(100.00); } }
运行此示例将在标准输出中打印消息“电费已更改”,证明更改监听器通知正常工作。
使用高级绑定API
高级API是在您自己的应用程序中开始使用绑定的最快最简单的方法。它由两部分组成:流畅API和Bindings
类。流畅API在各种依赖对象上公开方法,而Bindings
类则提供静态工厂方法。
要开始使用流畅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()); } }
这段代码将两个依赖项绑定在一起,打印它们的和,然后更改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修改了代码,使用Fluent API调用multiply
方法,并使用Bindings
类的add
方法。您还应该知道,高级API允许在定义算术操作时混合类型。结果的类型由与Java编程语言相同的规则定义:
-
如果操作数中有一个是
double
,结果是double
。 -
如果没有,并且操作数中有一个是
float
,结果是float
。 -
如果没有,并且操作数中有一个是
long
,结果是long
。 -
否则,结果是整数。
下一节将探讨可观察性,并演示无效化监听器与更改监听器的区别。
探索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(); // 定义属性值的getter public final double getAmountDue(){return amountDue.get();} // 定义属性值的setter public final void setAmountDue(double value){amountDue.set(value);} // 定义属性本身的getter 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("绑定现在无效。"); } }); // 第一次调用使绑定无效 bill1.setAmountDue(200.00); // 绑定现在无效 bill2.setAmountDue(100.00); bill3.setAmountDue(75.00); // 使绑定有效... System.out.println(total.getValue()); // 使无效... bill3.setAmountDue(150.00); // 使有效... System.out.println(total.getValue()); } }
通过更改单个账单的值,绑定将变为无效,并且无效化监听器将触发。但是,如果绑定已经无效,即使另一个账单发生变化,无效化监听器也不会再次触发。(在示例1-6中,调用total.getValue()
将绑定从无效变为有效。)我们知道这一点,因为对依赖列表中的任何账单进行的后续更改将再次触发无效化监听器。如果绑定仍然无效,这种情况不会发生。
请注意,注册ChangeListener
将强制进行急切计算,即使ObservableValue
的实现支持惰性评估。对于惰性评估的值,直到重新计算才能知道无效值是否真的发生了变化。因此,生成更改事件需要急切评估,而无效化事件可以为急切和惰性实现生成。
使用低级绑定API
如果高级API无法满足您的要求,您可以使用低级API。低级API适用于需要比高级API提供的更灵活(或性能更好)的开发人员。
示例1-7展示了使用低级API的基本示例。
示例1-7 使用低级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了。