1. Принцип Separation of Concerns, контроллер и представление

Отделение графического интерфейса от бизнес-логики приложения. Язык FXML.

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

Такой подход обладает рядом недостатков:

  • такой программный код превращается в череду созданий объектов и вызовов методов для их настройки, что затрудняет чтение кода и приводит к тому, что он выглядит не лучшим образом;

  • такой код тяжело отлаживать и исправлять ошибки при реализации графического интерфейса. Необходимо постоянно компилировать код и смотреть полученный результат в работающем приложении;

  • графический интерфейс, как правило, проектируется отдельным специалистом в области UI/UX, который может не знать языка, на котором этот интерфейс проектируется (в нашем случае, это Java). Кроме того, это приводит к тому, что java-программист, кроме программирования логики приложения, должен проектировать GUI, что приводит к дополнительным материальным затратам;

  • в случае необходимости изменения графического интерфейса (добавления нового функционала, изменения внешнего вида, анимации, эффектов), необходимо «перелопачивать» код и вносить туда изменения, что приводит к многочисленным ошибкам и тормозит процесс разработки программного обеспечения.

Основополагающим принципом разработки программного обеспечения является принцип separation of concerns

Исходя из этого, следует запомнить первое правило, которому должен следовать любой программист при разработке даже самого простого приложения: внешний вид (пользовательский интерфейс) приложения всегда должен быть максимально отделен от бизнес-логики работы приложения. Этот принцип еще кратко называют принципом отделения представления от содержания (эти принципы называются «Separation of concerns» или «Separation of presentation and content»).

В общем случае, принцип separation of concerns формулируется следующим образом - программа должна быть разделена на функциональные блоки, как можно меньше перекрывающие функции друг друга.

Повторим еще раз, что пользовательский интерфейс и внутренняя логика работы программы должны быть максимально независимы друг от друга. В идеале, вашей программе должно быть все равно – в каком виде она представлена пользователю – в виде GUI, консольного приложения, в окне браузера или еще как-то. В идеале, при изменении GUI, остальным классы программы остаются нетронутыми и изменения в них вносить не надо (в реальности, конечно, изменения вноситься будут, но они должны быть минимальны). Это помогает сделать программу более гибкой, а также помогает добиться разделения труда: программист пишет код, а дизайнер (верстальщик, специалист по UI/UX) занимается проектированием графического интерфейса.

Подавляющее большинство современных графических библиотек и фреймворков обладают тем или иным механизмом отделения представления от содержания.

Давайте разберемся, как это реализовано в JavaFX. Рассмотрим простой пример. Создадим пустое JavaFX-приложение

Main.java
public class Main extends Application {

    @Override
    public void start(Stage primaryStage) throws Exception {
        Parent root = FXMLLoader.load(getClass().getResource("sample.fxml"));
        primaryStage.setTitle("Hello World");
        primaryStage.setScene(new Scene(root, 300, 275));
        primaryStage.show();
    }


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

В примере определен объект Stage, объект Scene и корневой элемент графа сцены. Очевидным следующим шагом было бы создание новых объектов графического интерфейса, установка свойств объектов и формирование графа сцены.

Но в нашем случае мы будем использовать специальный язык для проектирования графического интерфейса, который называется FXML.

Выполним несколько шагов, чтобы настроить работу IntelliJ IDEA для использования языка FXML:

  1. скачаем и установим приложение SceneBuilder по ссылке http://gluonhq.com/products/scene-builder/;

  2. зайдем в настройки IntelliJ IDEA (Ctrl+Alt+S или File -> Settings) и в пункте JavaFX укажем путь к exe-файлу установленного SceneBuilder;

  3. перезапустим IntelliJ IDEA.

Откроем наш проект с JavaFX-приложением и создадим в проекте новый FXML файл sample.fxml.

Давайте разберемся, зачем мы создали этот файл и зачем нам нужен язык FXML. В JavaFX пользовательский интерфейс можно описать с помощью специального языка разметки, который называется FXML. Язык FXML является подмножеством языка XML и немного напоминает HTML.

Таким образом, вместо того, чтобы создавать объекты пользовательского интерфейса на языке Java в исходном коде программы, мы описываем граф сцены в виде fxml-файла, который выглядит определенным образом. Благодаря тому, что XML позволяет описать любые данные, мы можем очень точно описать объекты и их свойства, а древовидная структура XML идеально описывает граф сцены, который тоже имеет структуру дерева.

Откроем файл sample.fxml и посмотрим его содержимое

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

<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<AnchorPane xmlns="http://javafx.com/javafx"
            xmlns:fx="http://javafx.com/fxml"
            fx:controller="sample.Sample"
            prefHeight="400.0" prefWidth="600.0">

</AnchorPane>

Как мы видим, текст файла напоминает формат XML, в котором присутствуют некоторые элементы языка Java (в частности, подключение библиотек с помощью import).

Опишем граф сцены из нескольких элементов, после чего разберем, что происходит в получившемся коде.

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

<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<HBox xmlns="http://javafx.com/javafx"
      xmlns:fx="http://javafx.com/fxml"
      prefHeight="400.0" prefWidth="600.0">
    <children>
        <Button text="Кнопка"/>
        <VBox prefHeight="200.0" prefWidth="100.0">
            <children>
                <Label text="Текст"/>
            </children>
        </VBox>
    </children>
</HBox>

Итак, давайте попробуем разобраться в этом коде:

  • строка 1 – в ней содержится так называемая XML-декларация. Она описывает версию XML и кодировку;

  • строки 3 – 6 отдаленно напоминают команды импорта в java, которые заключены между символами <? и ?>. Такая конструкция называется инструкцией обработки (Processing Instruction, PI), фактически, это инструкция тому приложению, которое будет «читать» XML-файл. Фактически, это означает инструкцию – подключить те или иные библиотеки;

  • строки 8-10 определяет корневой элемент графа сцены – элемент HBox. В качестве атрибутов xmlns указаны пространства имен для тэгов FXML. Все остальные элементы размещаются внутри HBox, что в точности соответствует структуре дерева;

  • в строке 11 указан элемента <children>. Потомки того или иного элемента содержатся внутри элемента <children>, что напоминает метод getChildren(), который мы используем для добавления потомков элемента;

  • свойства элементов (высота, ширина, текст надписи и так далее) описываются в виде атрибутов тех или иных элементов.

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

Граф сцены, описанный в файле sample.fxml с помощью традиционного способа создания объектов Java в исходном коде выглядит следующим образом:

@Override
public void start(Stage primaryStage) throws Exception {

    HBox hBox = new HBox();
    hBox.setPrefHeight(400);
    hBox.setPrefWidth(600);
    
    Button button = new Button("Кнопка");
    Label label = new Label("Текст");
    
    VBox vBox = new VBox();
    vBox.setPrefHeight(200);
    vBox.setPrefWidth(100);
    
    vBox.getChildren().add(label);
    hBox.getChildren().addAll(button,vBox);
    
    Scene scene = new Scene(hBox, 300, 200);
    primaryStage.setTitle("JavaFX-приложение");
    primaryStage.setScene(scene);
    primaryStage.show();
}

Визуальные редакторы UI

Использование специального языка описания FXML дает нам возможность использовать специальные визуальные редакторы, которые позволяют нам проектировать граф сцены в визуальном режиме.

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

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

Использование редактора очень сильно облегчает и ускоряет разработку графических приложений. Кроме того, при работе с fxml очень удобно подключать css-стили и даже вызывать скрипты на различных языках программирования, например, например, Groovy, Clojure и даже JavaScript (данный вопрос в рамках этого курса не рассматривается).

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

В формате XML этот код будет выглядеть следующим образом:

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

<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.text.*?>

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

<GridPane fx:controller="sample.SampleController" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/8.0.1" xmlns:fx="http://javafx.com/fxml/1">
  <columnConstraints>
    <ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />
    <ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />
  </columnConstraints>
  <rowConstraints>
    <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
    <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
    <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
  </rowConstraints>
   <children>
      <Button maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" minHeight="-Infinity" minWidth="-Infinity" mnemonicParsing="false" text="Кнопка 1" GridPane.hgrow="ALWAYS" GridPane.vgrow="ALWAYS">
         <font>
            <Font size="22.0" />
         </font>
         <GridPane.margin>
            <Insets />
         </GridPane.margin>
      </Button>
      <Button maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" mnemonicParsing="false" text="Кнопка 2" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" GridPane.vgrow="ALWAYS">
         <font>
            <Font size="22.0" />
         </font>
      </Button>
      <Button maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" mnemonicParsing="false" text="Кнопка 3" GridPane.hgrow="ALWAYS" GridPane.rowIndex="1" GridPane.vgrow="ALWAYS">
         <font>
            <Font size="22.0" />
         </font>
      </Button>
      <Button maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" mnemonicParsing="false" text="Кнопка 4" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" GridPane.rowIndex="1" GridPane.vgrow="ALWAYS">
         <font>
            <Font size="22.0" />
         </font>
      </Button>
      <Button maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" mnemonicParsing="false" text="Кнопка 5" GridPane.hgrow="ALWAYS" GridPane.rowIndex="2" GridPane.vgrow="ALWAYS">
         <font>
            <Font size="22.0" />
         </font>
      </Button>
      <Button maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" mnemonicParsing="false" text="Кнопка 6" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" GridPane.rowIndex="2" GridPane.vgrow="ALWAYS">
         <font>
            <Font size="22.0" />
         </font>
      </Button>
   </children>
</GridPane>

У нас есть готовый граф сцены, но что дальше? Как известно, в Java всё является объектами некоторых классов, в том числе и элементы графического интерфейса.

До этого мы просто создавали в исходном коде объекты нужных нам классов, после чего вызывали их методы для формирования нужного нам графа сцены. Но у нас только текстовое описание графа сцены в формате FXML, как его преобразовать в набор объектов, вызвать нужные методы этих объектов и сформировать граф сцены?

Генерация объектов графа сцены. Загрузчик FXML

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

Фреймворк JavaFX использует распространенный подход, который состоит в генерации объектов. Его суть состоит в следующем: в JavaFX существует специальный класс FXMLLoader (загрузчик FXML), который содержит статический метод load(). Этот метод реализует следующий функционал:

  1. метод считывает fxml-файл, URL которого вы должны указать;

  2. метод «парсит» fxml-файл (разбивает файл на отдельные элементы с атрибутами);

  3. используя результаты парсинга и механизм рефлексии (механизм рефлексии в данном курсе не рассматривается), метод создает объекты, устанавливает их свойства и помещает один объект в состав других объектов и таким образом формирует нужный нам граф сцены;

  4. в качестве возвращаемого значения, метод load() возвращает ссылку на корневую вершину (root node), которая прямо или опосредованно содержит в себе весь сгенерированный граф сцены.

Вернемся к нашему исходному коду. Используя полеченные знания о работе класса FXML, выполним следующие шаги:

  1. вызовем статический метод FXMLLoader.load(), на вход которого передадим URL ресурса, в качестве которого выступает наш fxml-файл (что такое ресурс и как нам получить URL ресурса подробнее читайте здесь https://goo.gl/FJfvR9);

  2. ссылку на корневой элемент графа сцены передадим в созданный объект сцены.

Полученный код выглядит следующим образом.

Main.java
public class Main extends Application {

    @Override
    public void start(Stage primaryStage) throws Exception {

        Parent root =
                FXMLLoader.load(getClass().getResource("sample.fxml"));

        Scene scene = new Scene(root, 300, 200);

        primaryStage.setTitle("JavaFX-приложение");
        primaryStage.setScene(scene);
        primaryStage.show();
    }


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

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

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

Но сразу же возникает следующая проблема, каким образом получить ссылки на эти сгенерированные объекты? Если мы хотим указать обработчик событий для элементов графического интерфейса или динамически изменить свойства некоторых элементов – нам нужна ссылка на объекты элементов графического интерфейса.

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

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

Контроллер графа цены. Получения ссылок на объекты графа сцены.

Получение ссылки на сгенерированный элемент графического интерфейса – это одна из проблем, которая возникает, когда объекты графического интерфейса создаются не вручную. В различных графических библиотеках и в разных языках программирования эта проблема решается по-разному. Давайте рассмотрим, как эта проблема решается в JavaFX.

При описании интерфейса, в fxml-файле вы можете указать т.н. контроллер – специальный класс, который будет «управлять» объектами, которые описаны в fxml-файле. Почему этот класс называется контроллером и что такое вообще «контроллер» (вы, наверняка, слышали этот термин и даже что-то читали по этому поводу), мы рассмотрим при изучении архитектурного паттерна MVC.

Класс-контроллер это обычный java-класс, который должен реализовывать интерфейс javafx.fxml.Initializable, который определяет всего один метод initialize(). Создадим класс SampleController и пока оставим его пустым.

SampleController.java
public class SampleController implements Initializable {
    @Override
    public void initialize(URL location, ResourceBundle resources) {
    }
}

Дальнейший шаг – в fxml-файле необходимо указать, что для данного графа сцены будет использоваться этот класс контроллера. Эта информация будет считана FXMLLoader при парсинге fxml-файла.

Откроем файл sample.fxml и в корневом узле графа сцены укажем атрибут fx:controller = ”ua.opu.SampleController”.

Таким образом, мы заявляем, что для данного fxml-файла будет использоваться указанный класс контроллера. Этот атрибут можно использовать только в корневом узле fxml-файла.

Чтобы отличить один объект элемента UI от других, необходимо присвоить элементам UI различные идентификаторы, используя которые мы сможем обратиться к тому или иному элементу. Для того чтобы установить идентификатор для элемента, необходимо указать атрибут fx:id в коде либо в свойствах в визуальном редакторе.

Идем дальше. Теперь я хочу каким-то образом в классе контроллера получить ссылку на первые две кнопки и как-то ними дальше манипулировать. Как мне это сделать? Чтобы получить доступ к каким-то элементам, мне нужно этим элементам присвоить определенный id. Вы можете сделать это либо в визуальном редакторе (выделите нужный элемент, зайдите во вкладку Code: и установите значение в поле fx:id).

Для данного примера присвоим идентификаторы для двух кнопок: button1 и button2.

Нам необходимо получить ссылки на объекты этих кнопок. Для этого зайдем в класс контроллера и создадим два поля типа javafx.scene.control.Button, с названиями, которые точно совпадают с идентификаторами этих кнопок в fxml-файле. После чего, укажем перед каждым полем аннотацию @FXML. В итоге, класс контроллера будет иметь следующий вид

SampleController.java
public class SampleController implements Initializable {
    
    @FXML
    private Button button1;
    
    @FXML
    private Button button2;
    
    @Override
    public void initialize(URL location, ResourceBundle resources) {
    }
}

Еще раз обратите внимание, что fx:id и имя переменной должно совпадать точно и перед каждым объявлением должно быть указано @FXML.

Давайте разберемся, что происходит при указании контроллера и полей с аннотацией @FXML:

  • класс FXMLLoader парсит fxml-файл;

  • загрузчик «считывает» атрибут fx:controller и создает объект этого класса;

  • загрузчик парсит аннотации @FXML и внедряет в эти поля ссылки на созданные объекты (используется механизм рефлексии);

  • загрузчик вызывает метод initialize() (в этом методе мы прописываем все наши манипуляции с нужными элементами GUI).

Таким образом, если мы правильно указали fx:id и не ошиблись с классами и названиями полей, то в момент вызова метода initialize(), наши поля button1 и button2 будут содержать ссылки на наши две кнопки.

Добавим обработчик нажатия на кнопку 1

SampleController.java
public class SampleController implements Initializable {

    @FXML
    private Button button1;

    @FXML
    private Button button2;

    @Override
    public void initialize(URL location, ResourceBundle resources) {

        button1.setOnAction(event -> {
            new Alert(Alert.AlertType.CONFIRMATION,"Вы нажали кнопку 1").showAndWait();
        });
    }
}

Всё прошло успешно, мы имеем ссылки на нужные нам элементы, и мы можем делать с ними что захотим.

Для второй кнопки реализуем слушатель иначе. В классе-контроллере создадим метод handleButton2() и тоже пометим его аннотацией @FXML.

SampleController.java
public class SampleController implements Initializable {

    @FXML
    private Button button1;

    @FXML
    private Button button2;

    @FXML
    protected void handleButton2(ActionEvent event) {
        new Alert(Alert.AlertType.CONFIRMATION,"Вы нажали кнопку 2").showAndWait();
    }

    @Override
    public void initialize(URL location, ResourceBundle resources) {

        button1.setOnAction(event -> {
            new Alert(Alert.AlertType.CONFIRMATION,"Вы нажали кнопку 1").showAndWait();
        });
    }
}

То есть, мы видим, что мы можем помечать этой аннотацией не только поля, но и методы. Далее, зайдем в fxml-файл, в режим визуального редактора, выделите вторую кнопку, зайдите во вкладку Code: и в поле On Action из выпадающего списка выберите нужный метод

В текстовом виде это выглядит следующим образом

Компилируем приложение и смотрим результат

Как вы уже, наверное, поняли, фактически это означает следующее «если для кнопки2 произошло событие Action – вызови метод handleButton2(). Таким образом, если у вас много слушателей, то удобнее просто закодировать методы и расставить нужные методы для нужных событий в нужных элементах, и не париться лишний раз с полями.

Last updated