1. Принцип Separation of Concerns, контроллер и представление
Отделение графического интерфейса от бизнес-логики приложения. Язык FXML.
До этого момента мы создавали графический интерфейс напрямую в коде приложения, явно создавая объекты графического интерфейса, меняли их свойства, назначали слушатели событий и так далее.
Такой подход обладает рядом недостатков:
такой программный код превращается в череду созданий объектов и вызовов методов для их настройки, что затрудняет чтение кода и приводит к тому, что он выглядит не лучшим образом;
такой код тяжело отлаживать и исправлять ошибки при реализации графического интерфейса. Необходимо постоянно компилировать код и смотреть полученный результат в работающем приложении;
графический интерфейс, как правило, проектируется отдельным специалистом в области UI/UX, который может не знать языка, на котором этот интерфейс проектируется (в нашем случае, это Java). Кроме того, это приводит к тому, что java-программист, кроме программирования логики приложения, должен проектировать GUI, что приводит к дополнительным материальным затратам;
в случае необходимости изменения графического интерфейса (добавления нового функционала, изменения внешнего вида, анимации, эффектов), необходимо «перелопачивать» код и вносить туда изменения, что приводит к многочисленным ошибкам и тормозит процесс разработки программного обеспечения.
Основополагающим принципом разработки программного обеспечения является принцип separation of concerns
Исходя из этого, следует запомнить первое правило, которому должен следовать любой программист при разработке даже самого простого приложения: внешний вид (пользовательский интерфейс) приложения всегда должен быть максимально отделен от бизнес-логики работы приложения. Этот принцип еще кратко называют принципом отделения представления от содержания (эти принципы называются «Separation of concerns» или «Separation of presentation and content»).
Повторим еще раз, что пользовательский интерфейс и внутренняя логика работы программы должны быть максимально независимы друг от друга. В идеале, вашей программе должно быть все равно – в каком виде она представлена пользователю – в виде GUI, консольного приложения, в окне браузера или еще как-то. В идеале, при изменении GUI, остальным классы программы остаются нетронутыми и изменения в них вносить не надо (в реальности, конечно, изменения вноситься будут, но они должны быть минимальны). Это помогает сделать программу более гибкой, а также помогает добиться разделения труда: программист пишет код, а дизайнер (верстальщик, специалист по UI/UX) занимается проектированием графического интерфейса.
Подавляющее большинство современных графических библиотек и фреймворков обладают тем или иным механизмом отделения представления от содержания.
Давайте разберемся, как это реализовано в JavaFX. Рассмотрим простой пример. Создадим пустое JavaFX-приложение
В примере определен объект Stage
, объект Scene
и корневой элемент графа сцены. Очевидным следующим шагом было бы создание новых объектов графического интерфейса, установка свойств объектов и формирование графа сцены.
Но в нашем случае мы будем использовать специальный язык для проектирования графического интерфейса, который называется FXML.
Выполним несколько шагов, чтобы настроить работу IntelliJ IDEA для использования языка FXML:
зайдем в настройки IntelliJ IDEA (Ctrl+Alt+S или File -> Settings) и в пункте JavaFX укажем путь к exe-файлу установленного SceneBuilder;
перезапустим IntelliJ IDEA.
Откроем наш проект с JavaFX-приложением и создадим в проекте новый FXML файл sample.fxml
.
Давайте разберемся, зачем мы создали этот файл и зачем нам нужен язык FXML. В JavaFX пользовательский интерфейс можно описать с помощью специального языка разметки, который называется FXML. Язык FXML является подмножеством языка XML и немного напоминает HTML.
Таким образом, вместо того, чтобы создавать объекты пользовательского интерфейса на языке Java в исходном коде программы, мы описываем граф сцены в виде fxml-файла, который выглядит определенным образом. Благодаря тому, что XML позволяет описать любые данные, мы можем очень точно описать объекты и их свойства, а древовидная структура XML идеально описывает граф сцены, который тоже имеет структуру дерева.
Откроем файл sample.fxml
и посмотрим его содержимое
Как мы видим, текст файла напоминает формат XML, в котором присутствуют некоторые элементы языка Java (в частности, подключение библиотек с помощью import
).
Опишем граф сцены из нескольких элементов, после чего разберем, что происходит в получившемся коде.
Итак, давайте попробуем разобраться в этом коде:
строка 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 в исходном коде выглядит следующим образом:
Визуальные редакторы UI
Использование специального языка описания FXML дает нам возможность использовать специальные визуальные редакторы, которые позволяют нам проектировать граф сцены в визуальном режиме.
В качестве визуального редактора мы будем использовать редактор SceneBuilder, который мы установили и подключили выше.
При работе с fmxl-файлами вы можете легко переключаться между текстовым и визуальным представлением файла, редактировать код, который сгенерировал визуальный редактор и видеть результат без необходимости каждый раз компилировать и запускать приложение.
Использование редактора очень сильно облегчает и ускоряет разработку графических приложений. Кроме того, при работе с fxml очень удобно подключать css-стили и даже вызывать скрипты на различных языках программирования, например, например, Groovy, Clojure и даже JavaScript (данный вопрос в рамках этого курса не рассматривается).
Используя визуальный редактор, создадим граф сцены, который визуально будет выглядеть следующим образом
В формате XML этот код будет выглядеть следующим образом:
У нас есть готовый граф сцены, но что дальше? Как известно, в Java всё является объектами некоторых классов, в том числе и элементы графического интерфейса.
До этого мы просто создавали в исходном коде объекты нужных нам классов, после чего вызывали их методы для формирования нужного нам графа сцены. Но у нас только текстовое описание графа сцены в формате FXML, как его преобразовать в набор объектов, вызвать нужные методы этих объектов и сформировать граф сцены?
Генерация объектов графа сцены. Загрузчик FXML
Для решения вышеуказанной проблемы было придумано много различных методик и подходов, которые были реализованы в различных графических библиотеках в различных языках программирования.
Фреймворк JavaFX использует распространенный подход, который состоит в генерации объектов. Его суть состоит в следующем: в JavaFX существует специальный класс FXMLLoader
(загрузчик FXML), который содержит статический метод load()
. Этот метод реализует следующий функционал:
метод считывает fxml-файл, URL которого вы должны указать;
метод «парсит» fxml-файл (разбивает файл на отдельные элементы с атрибутами);
используя результаты парсинга и механизм рефлексии (механизм рефлексии в данном курсе не рассматривается), метод создает объекты, устанавливает их свойства и помещает один объект в состав других объектов и таким образом формирует нужный нам граф сцены;
в качестве возвращаемого значения, метод
load()
возвращает ссылку на корневую вершину (root node), которая прямо или опосредованно содержит в себе весь сгенерированный граф сцены.
Вернемся к нашему исходному коду. Используя полеченные знания о работе класса FXML, выполним следующие шаги:
ссылку на корневой элемент графа сцены передадим в созданный объект сцены.
Полученный код выглядит следующим образом.
Запустим приложение и убедимся, что сгенерированный граф сцены соответствует тому, что мы визуально спроектировали в SceneBuilder.
Таким образом, мы визуально спроектировали граф сцены для нашего окна, после чего класс FXMLLoader
сгенерировал нужный граф сцены в объектном виде.
Но сразу же возникает следующая проблема, каким образом получить ссылки на эти сгенерированные объекты? Если мы хотим указать обработчик событий для элементов графического интерфейса или динамически изменить свойства некоторых элементов – нам нужна ссылка на объекты элементов графического интерфейса.
Когда мы создавали объекты самостоятельно в исходном коде классов, мы хранили ссылку на них и могли к ним обратиться. Но в нашем случае, объекты были сгенерированы в недрах класса FXMLLoader
, упакованы один в другой и всё, что у нас есть – ссылка на корневой элемент графа сцены.
Мы могли бы попытаться получить ссылку на нужный элемент через объект корневого элемента графа сцены, но это сильно затруднит написание обработчиков событий и бизнес-логики приложения в целом.
Контроллер графа цены. Получения ссылок на объекты графа сцены.
Получение ссылки на сгенерированный элемент графического интерфейса – это одна из проблем, которая возникает, когда объекты графического интерфейса создаются не вручную. В различных графических библиотеках и в разных языках программирования эта проблема решается по-разному. Давайте рассмотрим, как эта проблема решается в JavaFX.
При описании интерфейса, в fxml-файле вы можете указать т.н. контроллер – специальный класс, который будет «управлять» объектами, которые описаны в fxml-файле. Почему этот класс называется контроллером и что такое вообще «контроллер» (вы, наверняка, слышали этот термин и даже что-то читали по этому поводу), мы рассмотрим при изучении архитектурного паттерна MVC.
Класс-контроллер это обычный java-класс, который должен реализовывать интерфейс javafx.fxml.Initializable
, который определяет всего один метод initialize()
. Создадим класс SampleController
и пока оставим его пустым.
Дальнейший шаг – в 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
. В итоге, класс контроллера будет иметь следующий вид
Давайте разберемся, что происходит при указании контроллера и полей с аннотацией @FXML
:
класс
FXMLLoader
парсит fxml-файл;загрузчик «считывает» атрибут
fx:controller
и создает объект этого класса;загрузчик парсит аннотации
@FXML
и внедряет в эти поля ссылки на созданные объекты (используется механизм рефлексии);загрузчик вызывает метод
initialize()
(в этом методе мы прописываем все наши манипуляции с нужными элементами GUI).
Таким образом, если мы правильно указали fx:id
и не ошиблись с классами и названиями полей, то в момент вызова метода initialize()
, наши поля button1
и button2
будут содержать ссылки на наши две кнопки.
Добавим обработчик нажатия на кнопку 1
Всё прошло успешно, мы имеем ссылки на нужные нам элементы, и мы можем делать с ними что захотим.
Для второй кнопки реализуем слушатель иначе. В классе-контроллере создадим метод handleButton2()
и тоже пометим его аннотацией @FXML
.
То есть, мы видим, что мы можем помечать этой аннотацией не только поля, но и методы. Далее, зайдем в fxml-файл, в режим визуального редактора, выделите вторую кнопку, зайдите во вкладку Code: и в поле On Action из выпадающего списка выберите нужный метод
В текстовом виде это выглядит следующим образом
Компилируем приложение и смотрим результат
Как вы уже, наверное, поняли, фактически это означает следующее «если для кнопки2 произошло событие Action – вызови метод handleButton2()
. Таким образом, если у вас много слушателей, то удобнее просто закодировать методы и расставить нужные методы для нужных событий в нужных элементах, и не париться лишний раз с полями.
Last updated
Was this helpful?