Перейти к основному содержимому

Введение в JavaFX

· 9 мин. чтения

1. Введение

JavaFX — это библиотека для создания многофункциональных клиентских приложений с помощью Java. Он предоставляет API для разработки приложений с графическим интерфейсом , которые работают практически на любом устройстве с поддержкой Java.

В этом уроке мы сосредоточимся на некоторых его ключевых возможностях и функциях и рассмотрим их.

2. API JavaFX

В Java 8, 9 и 10 для начала работы с библиотекой JavaFX дополнительная настройка не требуется. Проект будет удален из JDK, начиная с JDK 11.

2.1. Архитектура

JavaFX использует аппаратно ускоренный графический конвейер для рендеринга, известный как Prism . Более того, для полного ускорения использования графики он использует либо программный, либо аппаратный механизм рендеринга с внутренним использованием DirectX и OpenGL .

JavaFX имеет зависящий от платформы уровень инструментария Glass Window для подключения к собственной операционной системе . Он использует очередь событий операционной системы для планирования использования потоков. Кроме того, он асинхронно обрабатывает окна, события, таймеры.

Механизмы Media и Web обеспечивают воспроизведение мультимедиа и поддержку HTML/CSS.

Давайте посмотрим, как выглядит основная структура приложения JavaFX:

./82cfd8ba5a6f36283bb1bf8cf640aee7.png

Здесь мы замечаем два основных контейнера:

  • Stage — это основной контейнер и точка входа приложения . Он представляет главное окно и передается в качестве аргумента метода start() .
  • Сцена — это контейнер для хранения элементов пользовательского интерфейса, таких как представления изображений, кнопки, сетки, текстовые поля.

Сцена может быть заменена или переключена на другую сцену . Это представляет граф иерархических объектов, который известен как граф сцены . Каждый элемент в этой иерархии называется узлом. У отдельного узла есть свой ID, стиль, эффекты, обработчики событий, состояние.

Кроме того, сцена также содержит контейнеры макета, изображения, медиа.

2.2. Потоки

На системном уровне JVM создает отдельные потоки для запуска и рендеринга приложения :

  • Поток рендеринга Prism — отвечает за рендеринг Scene Graph отдельно.
  • Поток приложения — это основной поток любого приложения JavaFX. Все живые узлы и компоненты привязаны к этому потоку.

2.3. Жизненный цикл

Класс javafx.application.Application имеет следующие методы жизненного цикла:

  • init() — вызывается после создания экземпляра приложения . На данный момент API JavaFX еще не готов, поэтому мы не можем создавать здесь графические компоненты.
  • start (Стадия stage) — здесь создаются все графические компоненты. Кроме того, здесь начинается основной поток графических действий.
  • stop() – вызывается перед закрытием приложения; например, когда пользователь закрывает главное окно. Полезно переопределить этот метод для некоторой очистки перед завершением работы приложения.

Метод статического запуска() запускает приложение JavaFX.

2.4. FXML

JavaFX использует специальный язык разметки FXML для создания интерфейсов представления.

Это обеспечивает структуру на основе XML для отделения представления от бизнес-логики. Здесь больше подходит XML, так как он может вполне естественно представлять иерархию графа сцены .

Наконец, чтобы загрузить файл .fxml , мы используем класс FXMLLoader , в результате чего получается граф объектов иерархии сцены.

3. Начало работы

Для практики давайте создадим небольшое приложение, позволяющее осуществлять поиск по списку людей.

Во-первых, давайте добавим класс модели Person для представления нашего домена:

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

// getters, setters
}

Обратите внимание, как для переноса значений int, String и boolean мы используем классы SimpleIntegerProperty, SimpleStringProperty, SimpleBooleanProperty в пакете javafx.beans.property .

Далее давайте создадим класс Main , который расширяет абстрактный класс 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() , который является точкой входа в программу.

Затем FXMLLoader загружает иерархию графа объектов из SearchController.fxml в AnchorPane .

После запуска новой сцены мы устанавливаем ее на основную сцену . Мы также устанавливаем заголовок для нашего окна и вызываем его методом show() .

Обратите внимание, что полезно включить метод main() , чтобы иметь возможность запускать файл JAR без средства запуска JavaFX .

3.1. Представление FXML

Давайте теперь углубимся в XML-файл SearchController.

Для нашего поискового приложения мы добавим текстовое поле для ввода ключевого слова и кнопку поиска:

<AnchorPane 
xmlns:fx="http://javafx.com/fxml"
xmlns="http://javafx.com/javafx"
fx:controller="com.foreach.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 здесь является корневым контейнером и первым узлом иерархии графа. При изменении размера окна он переместит дочерний элемент в его точку привязки. Атрибут fx: controller связывает класс Java с разметкой.

Доступны и другие встроенные макеты:

  • BorderPane — делит макет на пять частей: сверху, справа, снизу, слева, по центру.
  • HBox — расположить дочерние компоненты на горизонтальной панели
  • VBox — дочерние узлы расположены в вертикальном столбце
  • GridPane — полезно для создания сетки со строками и столбцами.

В нашем примере внутри горизонтальной панели HBox мы использовали Label для размещения текста, TextField для ввода и Button . С помощью fx: id мы помечаем элементы, чтобы их можно было использовать позже в коде Java.

На панели VBox мы будем отображать результаты поиска.

Затем, чтобы сопоставить их с полями Java — мы используем аннотацию @FXML :

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() будет вызываться автоматически. Здесь мы можем выполнять дополнительные действия над компонентами пользовательского интерфейса, такие как регистрация прослушивателей событий, добавление стиля или изменение свойства текста.

В методе initTable() мы создадим таблицу, которая будет содержать результаты, с 3 столбцами, и добавим ее в контейнер данных 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);
}

Наконец, вся описанная здесь логика создаст следующее окно:

./0b99371af9d2fbab528f76b2e2965ffb.png

4. API привязки

Теперь, когда визуальные аспекты обработаны, давайте приступим к изучению данных привязки.

API привязки предоставляет некоторые интерфейсы, которые уведомляют объекты, когда происходит изменение значения другого объекта.

Мы можем привязать значение, используя метод bind() или добавив слушателей.

Однонаправленная привязка обеспечивает привязку только для одного направления:

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

Здесь любое изменение в поле поиска будет обновлять текстовое значение метки.

Для сравнения, двунаправленная привязка синхронизирует значения двух свойств в обоих направлениях.

Альтернативным способом привязки полей являются 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);

Здесь любое изменение модели, такое как вставка, обновление или удаление элементов, будет немедленно уведомлять элементы управления пользовательского интерфейса.

Список masterData будет содержать исходный список объектов Person , а список результатов будет списком, который мы отображаем при поиске.

Нам также необходимо обновить метод 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. Параллелизм

Работа с компонентами пользовательского интерфейса в графе сцены не является потокобезопасной, поскольку доступ к ним осуществляется только из потока приложения. Пакет 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() полностью выполняется в фоновом потоке и возвращает результат в поток приложения. Это означает, что любые манипуляции с компонентами пользовательского интерфейса в этом методе вызовут исключение во время выполнения.

Тем не менее, updateProgress(), updateMessage() могут быть вызваны для обновления элементов потока приложения. Когда состояние задачи переходит в состояние SUCCEEDED, обработчик события onSucceeded() вызывается из потока приложения:

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

В том же обратном вызове мы обновили данные tableView до нового списка результатов.

Task is Runnable , поэтому для его запуска нам нужно просто запустить новый поток с параметром задачи : ``

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

Флаг setDaemon(true) указывает, что поток завершится после завершения работы.

6. Обработка событий

Мы можем описать событие как действие, которое может быть интересно приложению.

Например, действия пользователя, такие как щелчки мышью, нажатия клавиш, изменение размера окна, обрабатываются или уведомляются классом javafx.event.Event или любым из его подклассов.

Также мы различаем три типа событий:

  • InputEvent — все типы действий клавиш и мыши, такие как KEY_PRESSED, KEY_TYPED, KEY_RELEASED или MOUSE_PRESSES, MOUSE_RELEASED
  • ActionEvent — представляет различные действия, такие как срабатывание кнопки или завершение ключевого кадра.
  • WindowEventWINDOW_SHOWING, WINDOW_SHOWN

Чтобы продемонстрировать, фрагмент кода ниже перехватывает событие нажатия клавиши Enter над searchField :

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

7. Стиль

Мы можем изменить пользовательский интерфейс приложения JavaFX, применив к нему индивидуальный дизайн.

По умолчанию JavaFX использует modena.css в качестве ресурса CSS для всего приложения. Это часть файла jfxrt.jar .

Чтобы переопределить стиль по умолчанию, мы можем добавить в сцену таблицу стилей:

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

Мы также можем использовать встроенный стиль; например, чтобы установить свойство стиля для определенного узла:

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

8. Заключение

Этот краткий обзор охватывает основы JavaFX API. Мы прошлись по внутренней структуре и представили ключевые возможности его архитектуры, жизненного цикла и компонентов.

В результате мы научились и теперь умеем создавать простое приложение с графическим интерфейсом.

И, как всегда, полный исходный код руководства доступен на GitHub .