Jam*_*s_D 6

API 的历史

\n

目前形式的 JavaFX 首次发布为 2.0 版本(从开发人员的角度来看,1.x 版本完全不同,并且使用了脚本语言)。2.0 版本于 2011 年发布。这使得它早于Java 8 发布,这意味着许多现代语言功能(例如 lambda 表达式和函数式接口)不可用。

\n

JavaFX 广泛使用绑定和属性。这些属性是包装单个变量并提供getset访问和修改该值的方法的对象。除了访问器和修饰符之外,它们还提供了一种注册侦听器的机制,当值更改时将收到通知。这使得使用 MVC(或相关的,例如 MVP 或 MVVM)架构编写应用程序变得更加简单,因为可以使用这些属性来实现模型,从而使向视图发出通知变得非常容易。

\n

不时出现的一个要求是需要观察或绑定到“属性的属性”(一些示例如下所示)。在 lambda 表达式出现之前,以健壮、类型安全的方式支持此操作的 API 使用起来非常困难。JavaFX 2.0 使用的妥协 API 是Bindings.selectAPI。虽然此 API 可以工作,但它依赖于反射,没有编译时检查,并且不能优雅地处理 null 结果。

\n

Java 8 和 JavaFX 8 发布后,提出了功能请求以替换Bindings.select(...)为更强大的解决方案。Tomas Mikula 编写了一个实现,并将其包含在他的第三方库中:最初在(不再维护)Easy Bind中,也在ReactFX(似乎也不再维护)中。从 JavaFX 19 开始,其中一些功能已纳入库中。托马斯·米库拉(Tomas Mikula)开创了这一先河,值得高度赞扬。

\n

简单示例:map()

\n

假设我们有一个具有不可变值的类。在这里,我们将其实现为record

\n
record Person(String name){}\n
Run Code Online (Sandbox Code Playgroud)\n

我们可以使用ListView<Person>自定义列表单元格来显示人员姓名:

\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
Run Code Online (Sandbox Code Playgroud)\n

假设(有点人为地)我们想要一个标签来显示当前所选人员的姓名。na\xc3\xafve 尝试执行此操作可能看起来像

\n
Label selectedPerson = new Label();\nselectedPerson.textProperty().bind(\n    list.getSeletionModel().selectedItemProperty().asString()\n);\n
Run Code Online (Sandbox Code Playgroud)\n

这样做的问题是,asString()将调用String.valueOf()所选的Person. "null"如果选定的人是(即没有选择) ,这将给出文字字符串null,否则将给出结果Person.toString()。这不会给出所需的显示。

\n

相反,我们可以使用map

\n
selectedItemProperty().map(Person::name)\n
Run Code Online (Sandbox Code Playgroud)\n

结果是ObservableValue<String>. null如果为 null,则该可观察值保存该值selectedItemProperty(),否则保存该值selectedItemProperty().getValue().name()。如果selectedItemProperty()发生更改,则会ObservableValue<String>自动更新,并通知其侦听器(包括通过任何绑定注册的侦听器)。

\n

所以我们可以做

\n
ObservableValue<String> name = list.getSelectionModel().selectedItemProperty().map(Person::name);\nselectedPerson.textProperty().bind(name);\n
Run Code Online (Sandbox Code Playgroud)\n

如果未选择任何内容,则name包含 null,并且文本设置为null,这是Label. 如果选择了某个人,则文本将完全按照要求设置为该人的姓名。

\n

这是完整的示例:

\n
import 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}\n
Run Code Online (Sandbox Code Playgroud)\n

flatMap

\n

在使用“属性的属性”的第一个示例中,初始属性是选定的人,而属性的属性是人的姓名,它是不可变的。因此,我们需要关心的唯一变化是对所选人员的更改。

\n

在更复杂的情况下,“财产的财产”本身可能就是某种可观察的价值。

\n

这是一个非常基本的时钟实现:

\n
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        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}\n
Run Code Online (Sandbox Code Playgroud)\n

它包含一个标签,显示时间(作为文本)。AnimationTimer每次 JavaFX 系统渲染帧时都会更新标签,将其文本设置为当前时间。

\n

请注意,计时器始终在运行。虽然这在实践中可能并不重要,但最好在标签显示在窗口中时运行计时器。我们如何发现这一点?

\n

具有,labelsceneProperty()引用显示 的当前场景label(或者null如果它不在场景中)。反过来, 也Scene有一个windowProperty()引用包含场景的窗口(如果场景不在窗口中,则为 null)。最后,窗口有一个showingProperty(),它BooleanProperty表示窗口是否正在显示。

\n

因此,要获取包含标签的当前窗口,我们需要表单的逻辑

\n
private 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}\n
Run Code Online (Sandbox Code Playgroud)\n

如果出现以下情况,则需要重新计算

\n
    \n
  • 标签被放置在新场景中(或从场景中删除)
  • \n
  • 包含标签的场景被放置在新窗口中(或从窗口中删除)
  • \n
  • 窗口显示或隐藏
  • \n
\n

请注意,使用map()在这里使用并没有给我们带来我们想要的东西。

\n
label.sceneProperty().map(Scene::windowProperty)\n
Run Code Online (Sandbox Code Playgroud)\n

会返回一个ObservableValue<ReadOnlyObjectProperty<Window>>. 这不是正确的类型,并且如果场景放置在不同的窗口中也不会改变(包含的值windowProperty()会改变,但是ReadOnlyObjectProperty<Window>本身仍然是同一个对象。

\n

flatMap()方法解决了这个问题:

\n
label.sceneProperty()\n     .flatMap(Scene::windowProperty)\n
Run Code Online (Sandbox Code Playgroud)\n

是一个ObservableValue<Window>。它的值是包含包含标签的场景的窗口。如果为 null,则它将为label.getScene()null,或者如果label.getScene().getWindow()为空,则为空。如果标签放置在不同的场景中,或者场景放置在不同的窗口中,它将触发更新。

\n

相似地,

\n
label.sceneProperty()\n     .flatMap(Scene::windowProperty)\n     .flatMap(Window::showingProperty)\n
Run 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()

\n

这样我们就可以为我们的计时器制作一个Clock在处于正在显示的窗口中的场景中时自动启动,并在不再如此时自动停止:

\n
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}\n
Run Code Online (Sandbox Code Playgroud)\n

这是一个快速测试应用程序:

\n
import 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}\n
Run Code Online (Sandbox Code Playgroud)\n

orElse()

\n

在前面的例子中,我们必须专门处理以下情况的可能性:null值的可能性:

\n
        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        });\n
Run Code Online (Sandbox Code Playgroud)\n

这里的问题是showing包装ObservableValue对象Boolean引用(而不是boolean基元)。该引用可以采用三个可能的值:Boolean.TRUEBoolean.FALSEnullnull如果调用“链”中的任何步骤flatMap导致引用为空,那么它实际上会呈现一个值。例如,如果标签不在场景中(因此label.sceneProperty()contains null)或场景不在窗口中,就会发生这种情况。在这种情况下,调用isNowShowing.booleanValue()(或使用诸如以下的代码隐式拆箱)if (isNowShowing){...})将引发空指针异常。

\n

在此示例中,如果showingcontains null,则意味着标签不在窗口中,因此我们希望以与处理 的方式同等对待它Boolean.FALSE。该orElse(...)方法创建一个ObservableValue,如果它不为空,则包含与原始值相同的值ObservableValue;如果它为空,则包含提供的“默认”值。所以我们可以清理这部分代码:

\n
    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    });\n
Run 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        });\n
Run Code Online (Sandbox Code Playgroud)\n

when()

\n

when()方法创建一个新的ObservableValue,仅当提供的ObservableValue<Boolean>为真(或如果基础值已更改则变为真)时才触发更新。

\n

为了演示这一点,我们可以向时钟添加一个闹钟,以及一个激活闹钟的复选框。为了简单起见,我们的闹钟每分钟都会响起(即当秒数为零时)。Alert当警报被触发时,我们将发出通知(通过一个框),但前提是选中了该复选框

\n

我们可以使用前面的技术来创建一个当秒数达到零时ObservableValue发生变化的值(并且会在一秒后变回):truefalse

\n
clock.timeProperty()\n     .map(time -> time.getSecond() == 0)\n
Run Code Online (Sandbox Code Playgroud)\n

将包含true时间的秒部分何时为零,false否则。我们可以使用它when()来创建一个可观察对象,仅当选中复选框时发生变化时才向其侦听器触发通知:

\n
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     });\n
Run Code Online (Sandbox Code Playgroud)\n

在虚拟化控制单元中的使用

\n

map()方法flatMap()可以为虚拟化控件中的自定义单元实现提供一些不错的解决方案(例如ListViewTableView)。

\n

例如,本文中的第一个示例定义ListView

\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
Run Code Online (Sandbox Code Playgroud)\n

null这里的单元工厂返回一个自定义单元,如果该项为空,则将文本设置为,否则设置为调用name()该项的结果。ListCell这基本上可以通过绑定 text 属性在一行中完成,并且无需子类化:

\n
list.setCellFactory(lv -> {\n    ListCell<Person> cell = new ListCell<>();\n    cell.textProperty().bind(cell.itemProperty().map(Person::name));\n    return cell;\n});\n
Run 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}\n
Run Code Online (Sandbox Code Playgroud)\n

该类Stock的实现如下。它有两个读/写属性(名称和价格),以及两个指示价格最近是否上涨或下跌的只读属性。当价格发生变化时,其中一个属性将设置为 true,并且PauseTransition在固定时间段后将其关闭:

\n
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}\n
Run Code Online (Sandbox Code Playgroud)\n

为了设置行的样式,我们根据行中项目的状态设置recently-increased或CSS 伪类。recently-decreasedrowFactory需要几个侦听器将属性映射到 CSS 伪类即可:

\n
    TableView&