1. 概述

JavaFX一个是用来创建富客户端应用的Java库。它提供了开发GUI界面应用程序所需的API接口,基于JavaFX的应用能够运行在所有受Java支持的设备上。

在本教程中,我们将重点介绍JavaFX提供的主要功能和能力。

2. JavaFX API

Java 8, 9及Java 10 集成了JavaFX,无需进行额外配置。从JDK 11开始,该项目将从JDK中删除,需要添加以下依赖项和插件添加到pom.xml中:

<dependencies>
    <dependency>
        <groupId>org.openjfx</groupId>
        <artifactId>javafx-controls</artifactId>
        <version>19</version>
    </dependency>
    <dependency>
        <groupId>org.openjfx</groupId>
        <artifactId>javafx-fxml</artifactId>
        <version>19</version>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.openjfx</groupId>
            <artifactId>javafx-maven-plugin</artifactId>
            <version>0.0.8</version>
            <configuration>
                <mainClass>Main</mainClass>
            </configuration>
        </plugin>
    </plugins>
</build>

2.1. 架构

JavaFX 使用Prism硬件加速的图形管道进行渲染。为了完全加速图形的使用,它通过内部使用DirectX和OpenGL,利用软件或硬件渲染机制。

JavaFX 有一个依赖平台的Glass Windowing Toolkit层,用于连接到本地操作系统。它使用操作系统的事件队列来调度线程的使用。此外,它还异步处理窗口、事件和计时器。

媒体 和Web引擎支持媒体播放和HTML/CSS。

下面是JavaFX 应用程序的主要结构图

在这里,我们注意到两个主要容器:

  • Stage 是应用程序的主容器和入口点。它表示主窗口,并作为start()方法的入参。
  • Scene 用于保存 UI 元素(例如图像视图、按钮、网格、文本框)的容器。

一个Stage可以包含一个或多个Scene,但在任一时刻,只有一个Scene是活动的。Scene可以替换或切换到另一个Scene。这代表分层对象的图,称为场景图。该层次结构中的每个元素称为节点。单个节点具有其 ID、样式、效果、事件处理程序和状态。

此外,Scene还包含布局容器、图片、媒体。

2.2. 线程

在系统级别,JVM 创建单独的线程来运行和显示应用程序:

  • Prism 渲染线程 —— 负责单独渲染Scene图。
  • 应用程序线程 —— 是任何 JavaFX 应用程序的主线程。所有活动节点和组件都附加到此线程。

2.3. 生命周期

javafx.application.Application 类具有以下生命周期方法:

  • init() – 在创建应用程序实例后调用。此时,JavaFX API 尚未准备就绪,因此我们无法在此处创建图形组件。
  • start(Stage stage) – 所有图形组件都在此处创建。此外,图形活动的主线程也从此处启动
  • stop() – 在应用程序关闭前调用;例如,当用户关闭主窗口时。可重写此方法以在程序退出前执行一些清理工作。

launch() 方法启动 JavaFX 应用程序。

2.4. FXML

类似于HTML,JavaFX 使用 FXML 标记语言来设计图像界面

FXML 基于 XML 的结构,将视图与业务逻辑分开。XML 非常适合,因为它能够非常自然地表示Scene图层次结构。

最后,为了加载.fxml 文件,我们使用 FXMLLoader 类,它会生成Scene层次的对象图。

3. 开始使用

下面我们开始练习,开发一个可以搜索人员列表的简单示例程序。

首先定义我们定义一个 Person 实体

public class Person {
    private SimpleIntegerProperty id;
    private SimpleStringProperty name;
    private SimpleBooleanProperty isEmployed;

    // getters, setters 方法
}

注意,我们使用 javafx.beans.property 包中的 SimpleIntegerPropertySimpleStringProperty 以及 SimpleBooleanProperty 分别包装 int、String、boolean类型值。

然后创建 Main class 继承 Application 抽象类:

public class Main extends Application {

    @Override
    public void start(Stage primaryStage) throws Exception {
        FXMLLoader loader = new FXMLLoader(
          Main.class.getResource("/SearchController.fxml"));
        AnchorPane page = (AnchorPane) loader.load();
        Scene scene = new Scene(page);

        primaryStage.setTitle("Title goes here");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

start() 方法是我们程序的入口。

温馨提醒,添加main方法,方便在没有 JavaFX Launcher 的情况下运行 JAR 文件。

然后,FXMLLoader 将对象图层次结构从 SearchController.fxml 文件加载到 AnchorPane 中。

开启新 Scene 后,我们将其设置为主 Scene 。我们还设置了窗口的标题并显示它。

3.1. FXML View

SearchController XML 文件用于描述UI布局,可类比HTML。

本例中,我们添加一个文本输入框用来输入关键字,以及一个搜索按钮。

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.Pagination?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.*?>
<AnchorPane 
    xmlns:fx="http://javafx.com/fxml"
    xmlns="http://javafx.com/javafx"
    fx:controller="com.baeldung.view.SearchController">
    <children>

        <HBox id="HBox" alignment="CENTER" spacing="5.0">
            <children>
                <Label text="Search Text:"/>
                <TextField fx:id="searchField"/>
                <Button fx:id="searchButton"/>
            </children>
        </HBox>

        <VBox fx:id="dataContainer"
                AnchorPane.leftAnchor="10.0"
                AnchorPane.rightAnchor="10.0"
                AnchorPane.topAnchor="50.0">
        </VBox>

    </children>
</AnchorPane>

这里 AnchorPane 是root容器,也是图形层次结构的第一个节点。调整窗口大小时,它会将子项重新定位到其锚点。fx:controller 属性关联对应的 Java 类。

还有一些其他可用的内置布局:

  • BorderPane – 将布局分为五个部分: top, right, bottom, left, center
  • HBox - 将子组件水平摆放
  • VBox – 将子节点垂直摆放
  • GridPane – 用于创建网格布局,可以设置行和列

本例中,我们在 HBox 里放置一个 Label 组件,TextField 输入框,以及 Button 按钮组件。并为其设置 id 属性,在后面的Java代码中会用到。

最后,搜索结果将展示在 VBox 里。

OK,现在我使用 @FXML 将其映射为Java字段:

public class SearchController {
 
    @FXML
    private TextField searchField;
    @FXML
    private Button searchButton;
    @FXML
    private VBox dataContainer;
    @FXML
    private TableView tableView;
    
    @FXML
    private void initialize() {
        // search panel
        searchButton.setText("Search");
        searchButton.setOnAction(event -> loadData());
        searchButton.setStyle("-fx-background-color: #457ecd; -fx-text-fill: #ffffff;");

        initTable();
    }
}

填充 @FXML 注释字段后,将自动调用 initialize()。这里,我们可以对 UI 组件执行进一步的操作 - 例如注册事件监听器、添加样式或更改文本属性。

在 initTable() 方法中,我们初始化table组件,设置列头,并将其添加到 dataContainer VBox:

private void initTable() {        
    tableView = new TableView<>();
    TableColumn id = new TableColumn("ID");
    TableColumn name = new TableColumn("NAME");
    TableColumn employed = new TableColumn("EMPLOYED");
    tableView.getColumns().addAll(id, name, employed);
    dataContainer.getChildren().add(tableView);
}

最后我们运行试试,截图如下

4. 数据绑定

OK,完成界面设计后,现在我们开始学习JavaFX数据绑定。

JavaFX 绑定 API 可以实现一个对象的值发生变化时通知相关对象。

我们可以使用 bind() 方法或通过添加监听器来绑定一个值。

绑定分为单向和双向,单向绑定仅提供一个方向的绑定:

searchLabel.textProperty().bind(searchField.textProperty());

这里对搜索框(searchField)的任何修改,都会同步更新searchLabel的文本值。

双向绑定,则会相互同步更新。

另一种方式是通过添加监听器 ChangeListeners 实现:

searchField.textProperty().addListener((observable, oldValue, newValue) -> {
    searchLabel.setText(newValue);
});

Observable 接口允许观察对象值的变化。

为了解释这一点,最常用的实现是 javafx.collections.ObservableList<T> 接口:

ObservableList<Person> masterData = FXCollections.observableArrayList();
ObservableList<Person> results = FXCollections.observableList(masterData);

在这里,任何model的更改,如元素的插入、更新或删除,都会立即通知UI控件。

masterData 列表将包含初始的 Person 对象列表,results 列表将是我们搜索时显示的列表。

我们还需要更新 initTable() 方法,以将表中的数据绑定到初始列表,并将每列关联到 Person 类的字段上。

private void initTable() {        
    tableView = new TableView<>(FXCollections.observableList(masterData));
    TableColumn id = new TableColumn("ID");
    id.setCellValueFactory(new PropertyValueFactory("id"));
    TableColumn name = new TableColumn("NAME");
    name.setCellValueFactory(new PropertyValueFactory("name"));
    TableColumn employed = new TableColumn("EMPLOYED");
    employed.setCellValueFactory(new PropertyValueFactory("isEmployed"));

    tableView.getColumns().addAll(id, name, employed);
    dataContainer.getChildren().add(tableView);
}

5. 并发

在场景图中使用UI组件不是线程安全的,因为它只能从应用程序线程访问。 javafx.concurrent 包提供了多线程的支持。

下面让我们看看如何在后台线程中完成数据搜索功能:

private void loadData() {
    String searchText = searchField.getText();
    Task<ObservableList<Person>> task = new Task<ObservableList<Person>>() {
        @Override
        protected ObservableList<Person> call() throws Exception {
            updateMessage("Loading data");
            return FXCollections.observableArrayList(masterData
                    .stream()
                    .filter(value -> value.getName().toLowerCase().contains(searchText))
                    .collect(Collectors.toList()));
        }
    };
}

在这里,我们创建一个一次性任务 javafx.concurrent.Task 对象并重写 call() 方法。

call() 方法完全在后台线程上运行,并将结果返回到应用程序线程。这意味着在此方法中对UI组件的任何操作都会抛出运行时异常。

然而,可以调用 updateProgress()updateMessage() 来更新应用程序线程的items。当任务状态转换为 SUCCEEDED 状态时,onSucceeded() 事件处理程序会从应用程序线程中调用:

task.setOnSucceeded(event -> {
    results = task.getValue();
    tableView.setItems(FXCollections.observableList(results));
});

在同一个回调中,我们将 tableView 的数据更新为新的结果列表。

Task 实现了 Runnable 接口,需要开启线程启动:

Thread th = new Thread(task);
th.setDaemon(true);
th.start();

setDaemon(true) 表示线程将在完成工作后终止。

6. 事件处理

我们可以监听感兴趣的事件。例如鼠标点击、键盘按键、窗口尺寸变化等。

主要分为三种类型:

  • InputEvent – 所有的键盘和鼠标动作,如 KEY_PRESSED、KEY_TYPED、KEY_RELEASED 或 MOUSE_PRESSED、MOUSE_RELEASED
  • ActionEvent – 如触发按钮或完成 KeyFrame 等动作
  • WindowEvent – 窗口变化,WINDOW_SHOWING、WINDOW_SHOWN

例如下面示例, searchField 监听按下回车键事件:

searchField.setOnKeyPressed(event -> {
    if (event.getCode().equals(KeyCode.ENTER)) {
        loadData();
    }
});

7. 自定义样式

下面学习如何修改JavaFX应用UI界面样式。

默认情况下,JavaFX使用 modena.css 作为整个应用程序的CSS资源。这是 jfxrt.jar 的一部分。

要覆盖默认样式,我们可以向Scene添加CSS样式:

scene.getStylesheets().add("/search.css");

我们还可以使用内联样式;例如,要为特定节点设置样式属性:

searchButton.setStyle("-fx-background-color: slateblue; -fx-text-fill: white;");

8. 总结

本快速教程,介绍了JavaFX基础知识,学习如何快速创建一个简单的GUI程序。

最后,本文中的完整代码可前往GitHub获取。