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
包中的 SimpleIntegerProperty
和 SimpleStringProperty
以及 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获取。