JavaFX 19 为接口引入了三种新方法ObservableValue:
<U> ObservableValue<U> map(Function<? super T, ? extends U> mapper)<U> ObservableValue<U> flatMap(Function<? super T, ? extends ObservableValue<? extends U>> mapper)ObservableValue<T> orElse(T constant)JavaFX 20进一步介绍了该方法
这些方法的预期用途是什么?为什么要引入它们以及我们如何使用它们?
目前形式的 JavaFX 首次发布为 2.0 版本(从开发人员的角度来看,1.x 版本完全不同,并且使用了脚本语言)。2.0 版本于 2011 年发布。这使得它早于Java 8 发布,这意味着许多现代语言功能(例如 lambda 表达式和函数式接口)不可用。
\nJavaFX 广泛使用绑定和属性。这些属性是包装单个变量并提供get和set访问和修改该值的方法的对象。除了访问器和修饰符之外,它们还提供了一种注册侦听器的机制,当值更改时将收到通知。这使得使用 MVC(或相关的,例如 MVP 或 MVVM)架构编写应用程序变得更加简单,因为可以使用这些属性来实现模型,从而使向视图发出通知变得非常容易。
不时出现的一个要求是需要观察或绑定到“属性的属性”(一些示例如下所示)。在 lambda 表达式出现之前,以健壮、类型安全的方式支持此操作的 API 使用起来非常困难。JavaFX 2.0 使用的妥协 API 是Bindings.selectAPI。虽然此 API 可以工作,但它依赖于反射,没有编译时检查,并且不能优雅地处理 null 结果。
Java 8 和 JavaFX 8 发布后,提出了功能请求以替换Bindings.select(...)为更强大的解决方案。Tomas Mikula 编写了一个实现,并将其包含在他的第三方库中:最初在(不再维护)Easy Bind中,也在ReactFX(似乎也不再维护)中。从 JavaFX 19 开始,其中一些功能已纳入库中。托马斯·米库拉(Tomas Mikula)开创了这一先河,值得高度赞扬。
map()假设我们有一个具有不可变值的类。在这里,我们将其实现为record:
record Person(String name){}\nRun Code Online (Sandbox Code Playgroud)\n我们可以使用ListView<Person>自定义列表单元格来显示人员姓名:
ListView<Person> list = new ListView<>();\n\n list.setCellFactory(lv -> new ListCell<>() {\n @Override\n protected void updateItem(Person person, boolean empty) {\n super.updateItem(person, empty);\n if (empty || person == null) {\n setText("");\n } else {\n setText(person.name());\n }\n }\n });\nRun Code Online (Sandbox Code Playgroud)\n假设(有点人为地)我们想要一个标签来显示当前所选人员的姓名。na\xc3\xafve 尝试执行此操作可能看起来像
\nLabel selectedPerson = new Label();\nselectedPerson.textProperty().bind(\n list.getSeletionModel().selectedItemProperty().asString()\n);\nRun Code Online (Sandbox Code Playgroud)\n这样做的问题是,asString()将调用String.valueOf()所选的Person. "null"如果选定的人是(即没有选择) ,这将给出文字字符串null,否则将给出结果Person.toString()。这不会给出所需的显示。
相反,我们可以使用map:
selectedItemProperty().map(Person::name)\nRun Code Online (Sandbox Code Playgroud)\n结果是ObservableValue<String>. null如果为 null,则该可观察值保存该值selectedItemProperty(),否则保存该值selectedItemProperty().getValue().name()。如果selectedItemProperty()发生更改,则会ObservableValue<String>自动更新,并通知其侦听器(包括通过任何绑定注册的侦听器)。
所以我们可以做
\nObservableValue<String> name = list.getSelectionModel().selectedItemProperty().map(Person::name);\nselectedPerson.textProperty().bind(name);\nRun Code Online (Sandbox Code Playgroud)\n如果未选择任何内容,则name包含 null,并且文本设置为null,这是Label. 如果选择了某个人,则文本将完全按照要求设置为该人的姓名。
这是完整的示例:
\nimport javafx.application.Application;\nimport javafx.beans.value.ObservableValue;\nimport javafx.fxml.FXMLLoader;\nimport javafx.scene.Scene;\nimport javafx.scene.control.Label;\nimport javafx.scene.control.ListCell;\nimport javafx.scene.control.ListView;\nimport javafx.scene.layout.BorderPane;\nimport javafx.scene.layout.HBox;\nimport javafx.stage.Stage;\n\nimport java.io.IOException;\nimport java.util.List;\n\nrecord Person(String name) {}\npublic class HelloApplication extends Application {\n @Override\n public void start(Stage stage) throws IOException {\n Label selectedPerson = new Label();\n ListView<Person> list = new ListView<>();\n\n list.setCellFactory(lv -> new ListCell<>() {\n @Override\n protected void updateItem(Person person, boolean empty) {\n super.updateItem(person, empty);\n if (empty || person == null) {\n setText("");\n } else {\n setText(person.name());\n }\n }\n });\n\n ObservableValue<String> selectedName = list.getSelectionModel().selectedItemProperty()\n .map(Person::name);\n selectedPerson.textProperty().bind(selectedName);\n\n BorderPane root = new BorderPane();\n root.setTop(new HBox(5, new Label("Selection:"), selectedPerson));\n root.setCenter(list);\n\n List.of("James", "Jennifer", "Jim", "Joanne", "John").stream()\n .map(Person::new)\n .forEach(list.getItems()::add);\n\n Scene scene = new Scene(root);\n stage.setScene(scene);\n stage.show();\n }\n\n public static void main(String[] args) {\n launch();\n }\n}\nRun Code Online (Sandbox Code Playgroud)\nflatMap在使用“属性的属性”的第一个示例中,初始属性是选定的人,而属性的属性是人的姓名,它是不可变的。因此,我们需要关心的唯一变化是对所选人员的更改。
\n在更复杂的情况下,“财产的财产”本身可能就是某种可观察的价值。
\n这是一个非常基本的时钟实现:
\nimport javafx.animation.AnimationTimer;\nimport javafx.beans.property.ReadOnlyObjectProperty;\nimport javafx.beans.property.ReadOnlyObjectWrapper;\nimport javafx.beans.value.ObservableValue;\nimport javafx.scene.Node;\nimport javafx.scene.Scene;\nimport javafx.scene.control.Label;\n\nimport java.time.LocalTime;\nimport java.time.format.DateTimeFormatter;\n\npublic class Clock {\n\n private final Label label ;\n private final AnimationTimer timer ;\n private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("h:mm:ss a");\n private final ReadOnlyObjectWrapper<LocalTime> time = new ReadOnlyObjectWrapper<>(LocalTime.now());\n private final ObservableValue<String> formattedTime = time.map(formatter::format);\n\n public Clock() {\n this.label = new Label();\n label.textProperty().bind(formattedTime);\n timer = new AnimationTimer() {\n @Override\n public void handle(long now) {\n time.set(LocalTime.now());\n }\n };\n timer.start(); \n\n }\n\n public void start() {\n System.out.println("Starting");\n timer.start();\n }\n\n public void stop() {\n System.out.println("Stopping");\n timer.stop();\n }\n\n public Node getView() {\n return label;\n }\n\n public ReadOnlyObjectProperty<LocalTime> timeProperty() {\n return time.getReadOnlyProperty();\n }\n\n public ObservableValue<String> formattedTimeProperty() {\n return formattedTime;\n }\n public String getFormattedTime() {\n return formattedTime.getValue();\n }\n}\nRun Code Online (Sandbox Code Playgroud)\n它包含一个标签,显示时间(作为文本)。AnimationTimer每次 JavaFX 系统渲染帧时都会更新标签,将其文本设置为当前时间。
请注意,计时器始终在运行。虽然这在实践中可能并不重要,但最好仅在标签显示在窗口中时运行计时器。我们如何发现这一点?
\n具有,label它sceneProperty()引用显示 的当前场景label(或者null如果它不在场景中)。反过来, 也Scene有一个windowProperty()引用包含场景的窗口(如果场景不在窗口中,则为 null)。最后,窗口有一个showingProperty(),它BooleanProperty表示窗口是否正在显示。
因此,要获取包含标签的当前窗口,我们需要表单的逻辑
\nprivate boolean isLabelShowing(Label label) {\n Scene scene = label.sceneProperty().get();\n if (scene == null) {\n return false ;\n }\n Window window = scene.windowProperty().get();\n if (window == null) {\n return false;\n }\n return window.showingProperty().get();\n}\nRun Code Online (Sandbox Code Playgroud)\n如果出现以下情况,则需要重新计算
\n请注意,使用map()在这里使用并没有给我们带来我们想要的东西。
label.sceneProperty().map(Scene::windowProperty)\nRun Code Online (Sandbox Code Playgroud)\n会返回一个ObservableValue<ReadOnlyObjectProperty<Window>>. 这不是正确的类型,并且如果场景放置在不同的窗口中也不会改变(包含的值windowProperty()会改变,但是ReadOnlyObjectProperty<Window>本身仍然是同一个对象。
这flatMap()方法解决了这个问题:
label.sceneProperty()\n .flatMap(Scene::windowProperty)\nRun Code Online (Sandbox Code Playgroud)\n是一个ObservableValue<Window>。它的值是包含包含标签的场景的窗口。如果为 null,则它将为label.getScene()null,或者如果label.getScene().getWindow()为空,则为空。如果标签放置在不同的场景中,或者场景放置在不同的窗口中,它将触发更新。
相似地,
\nlabel.sceneProperty()\n .flatMap(Scene::windowProperty)\n .flatMap(Window::showingProperty)\nRun Code Online (Sandbox Code Playgroud)\n是一个ObservableValue<Boolean>。它将包含nullif label.sceneProperty().flatMap(Scene::windowProperty)contains null(即 if label.getScene()isnull或 if label.getScene().getWindow()is null),否则将包含调用的(装箱的)结果label.getScene().getWindow().isShowing()。
这样我们就可以为我们的计时器制作一个Clock在处于正在显示的窗口中的场景中时自动启动,并在不再如此时自动停止:
import javafx.animation.AnimationTimer;\nimport javafx.beans.property.ReadOnlyObjectProperty;\nimport javafx.beans.property.ReadOnlyObjectWrapper;\nimport javafx.beans.value.ObservableValue;\nimport javafx.scene.Node;\nimport javafx.scene.Scene;\nimport javafx.scene.control.Label;\n\nimport java.time.LocalTime;\nimport java.time.format.DateTimeFormatter;\n\npublic class Clock {\n\n private final Label label ;\n private final AnimationTimer timer ;\n private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("h:mm:ss a");\n private final ReadOnlyObjectWrapper<LocalTime> time = new ReadOnlyObjectWrapper<>(LocalTime.now());\n private final ObservableValue<String> formattedTime = time.map(formatter::format);\n\n public Clock() {\n this.label = new Label();\n label.textProperty().bind(formattedTime);\n timer = new AnimationTimer() {\n @Override\n public void handle(long now) {\n time.set(LocalTime.now());\n }\n };\n\n ObservableValue<Boolean> showing = label.sceneProperty()\n .flatMap(Scene::windowProperty)\n .flatMap(window -> window.showingProperty());\n showing.addListener((obs, wasShowing, isNowShowing) -> {\n if (isNowShowing != null || isNowShowing.booleanValue()) {\n start();\n } else {\n stop();\n }\n });\n }\n\n public void start() {\n System.out.println("Starting");\n timer.start();\n }\n\n public void stop() {\n System.out.println("Stopping");\n timer.stop();\n }\n\n public Node getView() {\n return label;\n }\n\n public ReadOnlyObjectProperty<LocalTime> timeProperty() {\n return time.getReadOnlyProperty();\n }\n\n public ObservableValue<String> formattedTimeProperty() {\n return formattedTime;\n }\n public String getFormattedTime() {\n return formattedTime.getValue();\n }\n}\nRun Code Online (Sandbox Code Playgroud)\n这是一个快速测试应用程序:
\nimport javafx.application.Application;\nimport javafx.geometry.Insets;\nimport javafx.scene.Scene;\nimport javafx.scene.control.Label;\nimport javafx.scene.control.ToggleButton;\nimport javafx.scene.layout.VBox;\nimport javafx.stage.Stage;\n\nimport java.io.IOException;\n\npublic class ClockApp extends Application {\n @Override\n public void start(Stage stage) throws IOException {\n VBox root = new VBox(5);\n root.setPadding(new Insets(10));\n\n Clock clock = new Clock();\n\n\n VBox clockBox = new VBox(5, new Label("Current Time:"), clock.getView());\n ToggleButton showClock = new ToggleButton("Show Time");\n showClock.selectedProperty().addListener((obs, wasSelected, isNowSelected) -> {\n if (isNowSelected) {\n root.getChildren().add(clockBox);\n } else {\n root.getChildren().remove(clockBox);\n }\n });\n root.getChildren().add(showClock);\n\n Scene scene = new Scene(root, 400, 400);\n stage.setScene(scene);\n stage.show();\n }\n\n public static void main(String[] args) {\n launch();\n }\n}\nRun Code Online (Sandbox Code Playgroud)\norElse()在前面的例子中,我们必须专门处理以下情况的可能性:null值的可能性:
ObservableValue<Boolean> showing = label.sceneProperty()\n .flatMap(Scene::windowProperty)\n .flatMap(window -> window.showingProperty());\n\n showing.addListener((obs, wasShowing, isNowShowing) -> {\n if (isNowShowing != null && isNowShowing.booleanValue()) {\n start();\n } else {\n stop();\n }\n });\nRun Code Online (Sandbox Code Playgroud)\n这里的问题是showing包装ObservableValue对象Boolean引用(而不是boolean基元)。该引用可以采用三个可能的值:Boolean.TRUE、Boolean.FALSE或null。null如果调用“链”中的任何步骤flatMap导致引用为空,那么它实际上会呈现一个值。例如,如果标签不在场景中(因此label.sceneProperty()contains null)或场景不在窗口中,就会发生这种情况。在这种情况下,调用isNowShowing.booleanValue()(或使用诸如以下的代码隐式拆箱)if (isNowShowing){...})将引发空指针异常。
在此示例中,如果showingcontains null,则意味着标签不在窗口中,因此我们希望以与处理 的方式同等对待它Boolean.FALSE。该orElse(...)方法创建一个ObservableValue,如果它不为空,则包含与原始值相同的值ObservableValue;如果它为空,则包含提供的“默认”值。所以我们可以清理这部分代码:
ObservableValue<Boolean> showing = label.sceneProperty()\n .flatMap(Scene::windowProperty)\n .flatMap(window -> window.showingProperty())\n .orElse(Boolean.FALSE);\n\n showing.addListener((obs, wasShowing, isNowShowing) -> {\n if (isNowShowing) {\n start();\n } else {\n stop();\n }\n });\nRun Code Online (Sandbox Code Playgroud)\n或者(以更流畅的方式):
\n label.sceneProperty()\n .flatMap(Scene::windowProperty)\n .flatMap(window -> window.showingProperty())\n .orElse(Boolean.FALSE)\n .addListener((obs, wasShowing, isNowShowing) -> {\n if (isNowShowing) {\n start();\n } else {\n stop();\n }\n });\nRun Code Online (Sandbox Code Playgroud)\nwhen()该when()方法创建一个新的ObservableValue,仅当提供的ObservableValue<Boolean>为真(或如果基础值已更改则变为真)时才触发更新。
为了演示这一点,我们可以向时钟添加一个闹钟,以及一个激活闹钟的复选框。为了简单起见,我们的闹钟每分钟都会响起(即当秒数为零时)。Alert当警报被触发时,我们将发出通知(通过一个框),但前提是选中了该复选框。
我们可以使用前面的技术来创建一个当秒数达到零时ObservableValue发生变化的值(并且会在一秒后变回):truefalse
clock.timeProperty()\n .map(time -> time.getSecond() == 0)\nRun Code Online (Sandbox Code Playgroud)\n将包含true时间的秒部分何时为零,false否则。我们可以使用它when()来创建一个可观察对象,仅当选中复选框时发生变化时才向其侦听器触发通知:
CheckBox alarm = new CheckBox("Activate Alarm");\nAlert alert = new Alert(Alert.AlertType.INFORMATION);\nclock.timeProperty()\n .map(time -> time.getSecond() == 0)\n .when(alarm.selectedProperty())\n .addListener((obs, oldValue, isNewMinute) -> {\n if (isNewMinute) {\n alert.setContentText("Time is " + clock.getFormattedTime());\n alert.show()\n }\n });\nRun Code Online (Sandbox Code Playgroud)\n和map()方法flatMap()可以为虚拟化控件中的自定义单元实现提供一些不错的解决方案(例如ListView和TableView)。
例如,本文中的第一个示例定义ListView为
ListView<Person> list = new ListView<>();\n\n list.setCellFactory(lv -> new ListCell<>() {\n @Override\n protected void updateItem(Person person, boolean empty) {\n super.updateItem(person, empty);\n if (empty || person == null) {\n setText("");\n } else {\n setText(person.name());\n }\n }\n });\nRun Code Online (Sandbox Code Playgroud)\nnull这里的单元工厂返回一个自定义单元,如果该项为空,则将文本设置为,否则设置为调用name()该项的结果。ListCell这基本上可以通过绑定 text 属性在一行中完成,并且无需子类化:
list.setCellFactory(lv -> {\n ListCell<Person> cell = new ListCell<>();\n cell.textProperty().bind(cell.itemProperty().map(Person::name));\n return cell;\n});\nRun Code Online (Sandbox Code Playgroud)\n对于更复杂的示例,请考虑一个股票监控应用程序,该应用程序显示股票及其价格的表格,这些表格可能随时发生变化。我们希望根据股票价格最近是否上涨或下跌来设置行的样式,使用以下样式表:
\n.table-row-cell:recently-increased {\n -fx-control-inner-background: #c0c0ff;\n}\n\n.table-row-cell:recently-decreased {\n -fx-control-inner-background: #ffa0a0;\n}\nRun Code Online (Sandbox Code Playgroud)\n该类Stock的实现如下。它有两个读/写属性(名称和价格),以及两个指示价格最近是否上涨或下跌的只读属性。当价格发生变化时,其中一个属性将设置为 true,并且PauseTransition在固定时间段后将其关闭:
import javafx.animation.PauseTransition;\nimport javafx.beans.property.*;\nimport javafx.util.Duration;\n\npublic class Stock {\n\n private static final Duration RECENT_CHANGE_EXPIRATION = Duration.seconds(2);\n\n private final StringProperty name = new SimpleStringProperty();\n private final DoubleProperty price = new SimpleDoubleProperty();\n private final ReadOnlyBooleanWrapper recentlyIncreased = new ReadOnlyBooleanWrapper();\n private final ReadOnlyBooleanWrapper recentlyDecreased = new ReadOnlyBooleanWrapper();\n\n private final PauseTransition resetDelay = new PauseTransition(RECENT_CHANGE_EXPIRATION);\n\n public Stock(String name, double price) {\n setName(name);\n setPrice(price);\n resetDelay.setOnFinished(e -> {\n recentlyIncreased.set(false);\n recentlyDecreased.set(false);\n });\n priceProperty().addListener((obs, oldPrice, newPrice) -> {\n resetDelay.stop();\n recentlyIncreased.set(newPrice.doubleValue() > oldPrice.doubleValue());\n recentlyDecreased.set(newPrice.doubleValue() < oldPrice.doubleValue());\n resetDelay.playFromStart();\n });\n }\n\n public final String getName() {\n return nameProperty().get();\n }\n\n public StringProperty nameProperty() {\n return name;\n }\n\n public final void setName(String name) {\n this.nameProperty().set(name);\n }\n\n public final double getPrice() {\n return priceProperty().get();\n }\n\n public DoubleProperty priceProperty() {\n return price;\n }\n\n public final void setPrice(double price) {\n this.priceProperty().set(price);\n }\n\n public void adjustPrice(double percentage) {\n setPrice((1+percentage/100) * getPrice());\n }\n\n public boolean isRecentlyIncreased() {\n return recentlyIncreased.get();\n }\n\n public ReadOnlyBooleanWrapper recentlyIncreasedProperty() {\n return recentlyIncreased;\n }\n\n\n public boolean isRecentlyDecreased() {\n return recentlyDecreased.get();\n }\n\n public ReadOnlyBooleanWrapper recentlyDecreasedProperty() {\n return recentlyDecreased;\n }\n\n}\nRun Code Online (Sandbox Code Playgroud)\n为了设置行的样式,我们根据行中项目的状态设置recently-increased或CSS 伪类。recently-decreased只rowFactory需要几个侦听器将属性映射到 CSS 伪类即可:
TableView&
| 归档时间: |
|
| 查看次数: |
75 次 |
| 最近记录: |