Перевод статьи «JavaFX Tutorial: FXML and SceneBuilder» автора Vojtech Ruzicka.

Как создать графический интерфейс с JavaFX, используя разметку FXML и SceneBuilder.

Все посты в серии о JavaFX:

  1. Учебник по JavaFX: начало работы
  2. Учебник по JavaFX: Hello world!
  3. Учебник по JavaFX: FXML и SceneBuilder
  4. Учебник по JavaFX: основные макеты
  5. Учебник по JavaFX: расширенные макеты
  6. Учебник по JavaFX: CSS стилизация

Традиционный способ


В предыдущей статье мы создали простое приложение Hello World.

Просто напоминание — код выглядел так:

@Override
public void start(Stage primaryStage) throws Exception {
    primaryStage.setTitle("Hello world Application");
    primaryStage.setWidth(300);
    primaryStage.setHeight(200);

    InputStream iconStream = getClass().getResourceAsStream("/icon.png");
    Image image = new Image(iconStream);
    primaryStage.getIcons().add(image);

    Label helloWorldLabel = new Label("Hello world!");
    helloWorldLabel.setAlignment(Pos.CENTER);
    Scene primaryScene = new Scene(helloWorldLabel);
    primaryStage.setScene(primaryScene);

    primaryStage.show();
}

Как видите весь пользовательский интерфейс создан в Java коде.

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

У класса явно нет единой ответственности. Сравните это, например, с веб-интерфейсом, где каждая страница имеет четко разделенные задачи:

  • HTML — это структура
  • CSS — это визуальные эффекты
  • JavaScript — это поведение

Представляем FXML


Очевидно, что иметь весь код в одном месте не очень хорошая идея. Вам нужно как-то структурировать его, чтобы его было легче понять и сделать более управляемым.

В нействительности есть много шаблонов дизайна для этого. Как правило, в конечном итоге вы приходите к варианту «Model-View-Whatever» — это что-то вроде «Model View Controller», «Model View Presenter» или «Model View ViewModel».

Можно часами обсуждать плюсы и минусы разных вариантов — давайте не будем делать это здесь. Более важно то, что с JavaFx вы можете использовать любой из них.

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

Оказывается иерархическая структура XML — это отличный способ описать иерархию компонентов в пользовательском интерфейсе. HTML работает достаточно хорошо, верно?

Формат XML, специфичный для JavaFX, называется FXML. В нем вы можете определить все компоненты приложения и их свойства, а также связать их с контроллером, который отвечает за управление взаимодействиями.

Загрузка FXML файлов


Итак, как мы можем изменить наш метод запуска для работы с FXML?

FXMLLoader loader = new FXMLLoader();
URL xmlUrl = getClass().getResource("/mainScene.fxml");
loader.setLocation(xmlUrl);
Parent root = loader.load();

primaryStage.setScene(new Scene(root));
primaryStage.show();

Здесь root представляет корневой компонент вашего пользовательского интерфейса, остальные компоненты вложены в него.

Метод load имеет generic возвращаемое значение, поэтому вы можете указать конкретный тип, а не Parent. Далее, вы получаете доступ к компонентно-ориентированным методам. Однако, это делает ваш код более хрупким. Если вы измените тип корневого компонента в вашем FXML, приложение может перестать работать во время выполнения, но при этом во время компиляции не будет ошибок. Это происходит потому, что теперь есть несоответствие типа, объявленного в вашем FXML и в загрузчике Java FXML.

Создание FXML файла


Теперь мы знаем, как загрузить файл FXML, но нам все еще нужно его создать. Файл должен иметь расширение .fxml. В Maven проекте, вы можете поместить этот файл в папку ресурсов или FXMLLoader может загрузить его с внешнего URL-адреса.

После создадания файла в его первой строке необходимо ввести декларацию XML:

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

Импорт


Прежде чем добавить отдельные компоненты в файл, необходимо убедиться, что они правильно распознаются. Для этого необходимо добавить операторы импорта. Это очень похоже на импорт в Java классах. Вы можете импортировать отдельные классы или использовать знаки подстановки как обычно. Давайте рассмотрим пример раздела импорта:
<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

Хорошей новостью является то, что вместо добавления всех операторов импорта вручную, ваша IDE должна помочь вам добавить импорт аналогично добавлению их в классы Java.

Добавление компонентов


Теперь пришло время добавить некоторые компоненты. В предыдущей статье мы узнали, что каждая сцена может иметь только один дочерний компонент. Для начала давайте добавим простую метку (label):

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

<?import javafx.scene.control.Label?>

<!--Допускается только один компонент на корневом уровне-->
<Label>Hello World!</Label>

Конечно, метка в качестве корневого компонента — это не очень реалистичный пример. Обычно предпочтительнее использовать какой-то макет (layout), который является контейнером для нескольких компонентов и организует их расположение. Мы рассмотрим макеты позже в этой серии, а сейчас давайте просто воспользуемся простым VBox, который размещает свои дочерние элементы вертикально друг над другом.

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

<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.control.Button?>

<VBox>
    <Label text="Hello world!"/>
    <Label text="This is a simple demo application."/>
    <Button text="Click me!"/>
</VBox>

FX Namespace


Существует пара элементов и атрибутов FXML, которые по умолчанию недоступны. Вам нужно добавить пространство имен (Namespace) FXML, чтобы сделать их доступными. Его необходимо добавить к корневому компоненту:

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

<VBox xmlns="http://javafx.com/javafx"
      xmlns:fx="http://javafx.com/fxml">
    ...
</VBox>

Теперь можно использовать новые элементы из пространства имен fx. Давайте попробуем добавить уникальные идентификаторы в наши компоненты:

<Label fx:id="mainTitle" text="Hello world!"/>

Атрибут fx:id является уникальным идентификатором компонента, который можно использовать для ссылки на компонент из других частей нашего FXML и даже из нашего контроллера.

Скрипты


Наше приложение пока статично. Есть несколько меток и кнопка, но приложение не делает ничего динамического.

Давайте отреагируем на нажатие нашей кнопки и изменим заголовок с «Click me!» на «Click me again!».

Первое, что нужно сделать, это добавить обработчик события onAction для нашей кнопки.

<Button fx:id="mainButton" text="Click me!" onAction="buttonClicked()"/>

Обратите внимание на fx:id, это идентификатор, который будет использоваться позже для ссылки на кнопку.

Теперь нужно предоставить функцию, которая будет вызвана для обработки события. Ее можно определить внутри тега fx:script. Важно то, что вы можете использовать различные языки для написания скрипта, JavaScript, Groovy или Clojure. Давайте посмотрим пример на JavaScript:



Заметьте, что мы ссылаемся на наш компонент Button с помощью идентификатора mainButton, который был объявлен так:

fx:id = "mainButton"

Также необходимо указать, какой язык сценариев вы используете в файле FXML:

<?language javascript?>

Давайте рассмотрим полный текст примера:



Должен ли я использовать это?


В приведенном выше примере показано, как ссылаться на компоненты с помощью fx:id и как добавить простое поведение с помощью скрипта на JavaScript. Неужели это то, что вы должны на самом деле делать?

Ответ — в большинстве случаев нет. Есть несколько проблем с таким подходом. Причина, по которой введен FXML, была разделение интересов — чтобы отделить структуру и поведение пользовательского интерфейса. В этом скрипте снова вернулось поведение слитное со структурой пользовательского интерфейса. Более того, поскольку мы больше не работаем с кодом Java, а с XML, были утрачены все проверки кода во время компиляции и безопасность типов. Теперь все проблемы в приложении будут обнаружены во время выполнения, а не во время компиляции. Приложение стало очень хрупким и подверженым ошибкам.

Добавление контроллера


Итак, что можно сделать, чтобы получить четкое разделение интересов? Можно связать контроллер с нашим файлом FXML. Контроллер — это Java класс, который отвечает в приложении за обработку поведения и взаимодействия с пользователем. Таким образом можно вернуть безопасность типов и проверки времени компиляции.

Контроллер является POJO, он не должен расширять или реализовывать что-либо, а также не должен иметь никаких специальных аннотаций.

Как можно связать класс контроллера с нашим FXML? По существу, есть два варианта.

На Java


Вы можете создать экземпляр контроллера самостоятельно или использовать любые другие способы создания экземпляра, такие как инъекция зависимости. Затем просто загрузите вашим FXMLLoader.

FXMLLoader loader = new FXMLLoader();
loader.setController(new MainSceneController());

В FXML


Вы можете указать класс вашего контроллера как атрибут fx:controller, который должен находиться в корневом компоненте.

<VBox xmlns="http://javafx.com/javafx"
      xmlns:fx="http://javafx.com/fxml"
      fx:controller="com.vojtechruzicka.MainSceneController">
    ...
</VBox>

Если вы объявляете свой класс Controller в FXML, он автоматически создается для вас. Этот подход имеет одно ограничение — в контроллере нужно создать конструктор без аргументов, чтобы позволит легко создавать новый экземпляр класса Controller.

Для получения доступа к экземпляру контроллера, созданного автоматически, можно использовать загрузчик FXML:

FXMLLoader loader = new FXMLLoader();
loader.setLocation(getClass().getResource("/mainScene.fxml"));
MainSceneController controller = loader.getController();

Вызов методов контроллера


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

public class MainSceneController {
    
    public void buttonClicked() {
        System.out.println("Button clicked!");
    }
}

Следующим шагом является регистрация вызова этого метода в качестве обработчика события onAction нашей кнопки. Чтобы ссылаться на методы из нашего контроллера, нам нужно использовать знак # перед именем метода:

<VBox xmlns="http://javafx.com/javafx"
      xmlns:fx="http://javafx.com/fxml" fx:controller="com.vojtechruzicka.MainSceneController">
    <Label fx:id="mainTitle" text="Hello world!"/>
    <Label fx:id="subTitle" text="This is a simple demo application."/>
    <Button fx:id="mainButton" text="Click me!" onAction="#buttonClicked"/>
</VBox>

При нажатии на кнопку, она вызывает метод MainSceneController.buttonClicked(). Имейте в виду, что это работает, только если метод объявлен public. Если модификатор доступа более строгий, необходимо аннотировать метод аннотацией @FXML.

@FXML
private void buttonClicked() {
    System.out.println("Button clicked!");
}

Внедрение компонентов в контроллер


Пока что мы просто печатаем на консоль. Что если мы снова захотим изменить текст нашей кнопки на «Click me again»? Как мы можем получить ссылки на компоненты в нашем контроллере?

К счастью, это легко. Помните эти атрибуты fx:id?

<Button fx:id="mainButton" text="Click me!" onAction="#buttonClicked"/>

JavaFX пытается автоматически сопоставить компоненты с fx:id с полями определенным в вашем контроллере с тем же именем.

Предположим, у нас есть кнопка описанная выше с

fx:id="mainButton"

JavaFX пытается внедрить объект кнопки в ваш контроллер в поле с именем mainButton:

public class MainSceneController {

    //Вот внедренный компонент с fx:id = "mainButton"
    @FXML
    private Button mainButton;
}

Как и в предыдущих методах, ваши поля должны быть public или аннотированными @FXML.

Теперь, когда у нас есть ссылка на нашу кнопку, можно легко изменить ее текст:

public class MainSceneController {

    @FXML
    private Button mainButton;

    @FXML
    private void buttonClicked() {
        mainButton.setText("Click me again!");
    }
}

Scene Builder


Написание вашей структуры GUI в XML может быть более естественным, чем в Java (особенно если вы знакомы с HTML). Тем не менее, до сих пор это не очень удобно. Хорошей новостью является то, что существует официальный инструмент под названием Scene Builder, который поможет вам в создании пользовательского интерфейса. В двух словах, это графический редактор для вашего графического интерфейса.



В редакторе имеется три основных области:

  1. В левой части отображаются доступные компоненты, которые можно перетащить в среднюю часть. Она также содержит иерархию всех компонентов в вашем пользовательском интерфейсе, поэтому вы можете легко перемещаться по ней.
  2. Средняя часть — это ваше приложение, отображаемое на основе вашего файла FXML.
  3. Справа находится инспектор текущих компонентов. Здесь вы можете редактировать различные свойства выбранного текущего компонента. Любой компонент, выбранный в средней части иерархии, отображается в инспекторе.

Standalone


Scene Builder можно загрузить как отдельное приложение, которое можно использовать для редактирования FXML файлов.

Интеграция с IntelliJ IDEA


В качестве альтернативы, Scene Builder предлагает интеграцию с IDE.

В IntelliJ IDEA вы можете нажать правой кнопкой мыши на любом FXML файле и затем выбрать опцию меню «Открыть» в SceneBuilder.

В качестве альтернативы, IntelliJ IDEA интегрирует SceneBuilder непосредственно в IDE. Если вы откроете файл FXML в IDEA, в нижней части экрана появятся две вкладки

  • Текст
  • SceneBuilder

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



В IntelliJ IDEA можто настроить расположение исполняемого файла SceneBuilder:

Settings > Languages & Frameworks > JavaFX > Path to SceneBuilder

Что дальше


В следующем посте из нашей серии будут рассмотрены некоторые основные макеты (layout), которые можно использовать для организации компонентов GUI приложения на JavaFX.

Комментарии (4)


  1. u_235
    13.11.2019 06:50

    Для связывания используется рефлексия и поэтому в конструкторе контроллера лучше ничего не делать?


    1. val6852 Автор
      13.11.2019 09:02

      Binding (связывание) в JavaFX — это вспомогательный класс javafx.beans.binding с множеством служебных функций. Этот класс предназначен для связывания свойств объектов.
      Например, вы можете привязать координаты кнопки к ширине окна и в результате, при изменении размеров формы, переместится и привязанная кнопка. Реализация этого примера приведена в статье «JavaFX изнутри»: habr.com/ru/post/130057.


      1. u_235
        14.11.2019 11:54

        Я имел в виду другое связывание: вызов обработчиков событий и инициализация переменных контроллера.


  1. Plesser
    13.11.2019 15:49

    Немного дурацкий вопрос, а JavaFX вообще востребовано на рынке труда?