JavaFX в 2019

Зачем?

Зачем вообще кому-то писать толстый клиент в 2019 году? Web и atom уже победили. Можно поставить браузер или еще 8Гб оперативной памяти и получить переливающееся приложение. К сожалению, не все задачи можно решать через web. Для измерения АЧХ фильтров, мне необходимо было достаточно простое приложение:

  • запуск rtl_power и вывод результатов в виде графика
  • чтение .csv файла и вывод в виде графика
  • график должен реагировать на мышь и показывать текущее значение по X и Y

Понятно, что для такого простого приложения использовать web + web server или atom - это слишком. Другими альтернативами были swift, QT или какой-нибудь .NET. Но у всех у них есть один фатальный недостаток - они не на Java. Вообще, это не первое приложение, которое я писал на Java для десктопа. В далёком 2006 году у меня был небольшой компонент MultiLineTable, который работал поверх стандартного JTable из Swing. Он позволял делать сложную таблицу со вложенными столбцами.

Процесс

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

В случае JavaFX:

  • Model - FXML. Особый XML в котором описывается то, как компоненты будут расположены на формах. Поддерживаются вложенные XML, импорт XML и прочие приятные штуки. Писать модульный и переиспользуемый UI стало проще и стандартнее. Не обошлось и без ложки дегтя: расположение компонентов надо по-прежнему описывать с помощью различных Layout. С ними есть единственная проблема - никогда не знаешь, как будет выглядеть форма, пока не запустишь.
  • View - CSS. Все стили можно и нужно добавлять отдельным .css файлом. Тут не надо обольщаться - это не настоящий CSS. Многие атрибуты сделаны очень похожими на CSS, но их количество ограничено.
  • Controller - Controller. В JavaFX он прямо так и называется. Его можно явно привязать к форме. Вот пример:
<BorderPane fx:id="borderPane"
	xmlns:fx="http://javafx.com/fxml"
	fx:controller="ru.r2cloud.rtlspectrum.Controller">
</BorderPane>

И потом использовать методы из контроллера:

<Button text="Run now" onAction="#runNow" />

runNow - это публичный метод контроллера.

С помощью всех этих новых технологий и stackoverflow мне удалось за пару дней сделать вполне достойное приложение:

  • асинхронные задачи
  • progress bar
  • отображение достаточно кастомизированного LineChart

Передовой JavaFX

Несмотря на вполне рабочий результат, внутренний перфекционист не давал мне спать. Поэтому я потратил еще около 4 дней на исследования.

Тёмная тема

Во-первых, мне захотелось сделать тёмную тему в приложении. Дело в том, что MacOS у меня переключен на тёмную тему и многие приложения автоматически (или нет?) стали выглядеть в тон основным элементам ОС. Я решил узнать, можно ли поставить тёмную тему средствами JavaFX. Нельзя. По-умолчанию, все приложения JavaFX запускаются со стандартными стилями для заданной ОС. Вместе с умельцами со stackoverflow мне удалось сделать нечто похожее на тёмную тему.

  • все стили JavaFX зависят от одного базового стиля “-fx-base”, поэтому, изменив его на тёмный, можно поменять стиль всего приложения.
.root {
    -fx-base: rgba(60, 63, 65, 255);
}
  • Определить какая сейчас тема установлена в операционной системе можно через системную команду. Она возвращает “Dark” для тёмной темы.
ProcessBuilder builder = new ProcessBuilder().command("/usr/bin/defaults", "read", "-g", "AppleInterfaceStyle");
Process process;
try {
	process = builder.start();
	int resultCode = process.waitFor();
	if (resultCode != 0) {
		return false;
	}
} catch (Exception e1) {
	e1.printStackTrace();
	return false;
}

Результат вполне неплох:

Но шапка окна по-прежнему светлая! Ее, конечно, можно убрать, нарисовать свою тёмную и сэмулировать работу, но тогда придется эмулировать работу для каждой ОС. И в каждой ОС она выглядит и работает по-разному. В общем, я решил, что усилия и потенциальные баги не стоят того.

Выделение текста

rtlSpectrum на старте отображает небольшую подсказку с чего начать. В частности, есть текст команды для генерации .csv файла.

Очевидно, что эту команду надо бы дать скопировать. Как бы не так. Современные браузеры приучили нас к тому, что в любом приложении можно выделить текст. Однако, это не так в десктоп-приложениях: чтобы выделить текст в Label, необходимо приложить усилия. В частности для JavaFX необходимо сделать фиктивный Node и заменить Label на Input с хитрым стилем, который бы мимикрировал под Label. Слишком сложно для такого простого улучшения.

Жирный текст

Начальную подсказку надо было как-то выделить. “Getting started” явно терялась среди пары абзацев текста. Самое очевидное - это сделать размер шрифта больше и сделать его жирным. Оказывается, на Mac OS шрифт по-умолчанию не поддерживает bold. На дворе 2019 год и Java не может отрисовать жирный шрифт. Решением стало использовать другой шрифт - “Arial”.

Сборка

Итак, приложение написано, шрифты побеждены, настало время сделать то, без чего ни одно приложение не может обойтись - сборка и дистрибьюция. Поскольку я знаю только Java Web start и он, вроде как, давно умер, то я начал фантазировать на тему того, чего я бы хотел от идеальной дистрибьюции:

  • нативное приложение
  • без зависимостей на jre
  • с инсталлятором
  • с иконкой
  • с gpg подписью и компанией “dernasherbrezon”

Для начала необходимо собрать нативное приложение. Быстрый поиск в интернете ни к чему не привел. С одной стороны есть множество статей, как собрать приложение с помощью ant-javafx.jar, а с другой JavaFX была удалена из JDK начиная с версии 11. К сожалению, я не смог восстановить хронологию событий, но похоже новый способ через jmod и jlink должен был быть гораздо правильнее. Команда Jmod предназначена для того, чтобы собрать модуль. Этот модуль отличается от .jar тем, что может включать в себя нативные библиотеки, скрипты и пр. Jlink используется для линковки этого модуля и JRE. В результате получается сборка JRE, в которой установлены только модули нужные для работы приложения.

Поскольку приложение у меня собирается с помощью maven, то первым делом я начал искать плагины. Вот тут меня ждало первое разочарование. Официальный плагин maven-jmod-plugin находится 2 года в статусе альфа. Почему? Неужели никто не использует новую модульную систему? При попытке использовать плагин, выскакивает ошибка раз и два. Я бросил это дело и решил вручную выполнить необходимые команды и посмотреть, действительно ли то, что получается, мне нужно.

После того как произошла сборка jmod, необходимо выполнить команду jlink:

jlink --module-path "target/jmods/" --add-modules rtlSpectrum --output ./target/image/

И…ошибка:

Error: Hash of javafx.base (a0351b6767b462b64c66b4cd99d6bfc1763761d1a27ce7c6c19e4db5ba33abc8) differs to expected hash (6ff380e1321d8dcc64b22151ebe8fb31eb10c4484040e49e452ca88d25c1b754) recorded in java.base

Откуда это вообще взялось? Что за хэш? Я ведь такого не генерировал. В тщетных попытках исправить ошибку я провел еще пару часов. В результате оказалось, что jmod может хранить в себе хэши других jmod, на которые ссылается.

jmod describe $JAVA_HOME/jmods/java.base.jmod | grep javafx.base
hashes javafx.base SHA-256 6ff380e1321d8dcc64b22151ebe8fb31eb10c4484040e49e452ca88d25c1b754

Но в 11 версии же нет JavaFX! Оказывается в liberica jdk JavaFX включён. Что же получается? Есть JDK/JRE в которых JavaFX модуль есть, а есть те в которых его нет. Если я буду писать приложение, которое зависит от JavaFX, то мне нужно его вместе с приложением поставлять или извне ожидать? В общем, явно существует путаница с тем, как паковать приложение.

Выкинув JavaFX из maven зависимостей, мне удалось собрать образ приложения. Он занимает ~100Мб и запускается командой ./bin/java -m rtlSpectrum. Простое приложение по выводу графика не должно занимать 100мегабайт. В результирующем образе нет rtlSpectrum.sh или просто rtlSpectrum. Этот образ - просто сборка jre, в которой по-умолчанию есть rtlSpectrum.

Все это привело меня к мысли о том, что надо делать по-старинке. Я откатился на java 8, где JavaFX есть по-умолчанию и начал собирать обычный jar. В итоге приложение мультиплатформенное, весит 26кб и требует java.

Что дальше?

За 6 дней у меня получилось написать достаточно простое приложение для десктопа. Его можно скачать на github, CI/CD сделан с помощью travis, качество кода анализируется sonarcloud. Из того, что не хватает:

  • Тестирование с помощью TestFx
  • Измерение скорости запуска и потребления памяти в сравнении с нативным приложением