Обновлено: 28-11-2023. Использование OpenGL напрямую.

Обновлено: 19-02-2022. При открытии файла в стороннем приложении через ProcessBuilder код возврата fflush (stdio.h) на stdout может трактоваться как ошибка, приводя к завершению приложения.

Обновлено: 11-01-2022. Потеря биндингов. Утечка памяти в setContextMenu.

Обновлено: 01-09-2020. Проверка javafx.application.Platform#isSupported(ConditionalFeature feature) бросает исключение. Зачеркивание ячеек. SplitPane.Divider не Node. При масштабировании контента в ScrollPane скроллбары не обновляются. Вызов toString() не сквозной, некоторые классы не вызывают родительский метод.

Обновлено: 06-01-2020. DatePicker крашит Cinnamon при открытии custom colors, Accordion и expandedPane, слушатели ссылок на WebView, редактируемый ListView, транзитивная связь в таблицах, Drag-and-Drop и сериализация.

Сформировал небольшой список нюансов, проблем и граблей JavaFX (OpenJFX) по итогам написания нескольких программ. Конечно же, все баги есть в официальном багтрекере, а проблемы обсуждаются на Stack Overflow, но я все равно отмечу те из них, которые доставили мне особые неудобства или часто встречались. Можно сказать, что это мой личный рейтинг злоключений и памятник моему потерянному времени. Возможно, кому-то это будет полезным.

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

  1. Общий функционал и особенности
  2. Окна и Stage
  3. Контроллеры
  4. Контролы
  5. Графика
Общий функционал и особенности
  • Результат сложных биндингов легко потерять, если не сохранить в поле: Bindings.and() in JavaFX not firing?

  • Некоторые классы не вызывают родительский toString(), который у Node содержит разную служебную информацию, например, о стилях. Если вы реализуете механизм дебага в окне с выводом иерархии узлов, то при использовании Node.toString() у некоторых узлов будет печататься информация о стилях, а у некоторых нет, что может создавать обманчивое впечатление, что у последних стили просто не выставлены, когда родительский toString() не вызывается. Коварная ловушка, сам попал в такую.

  • При вызове javafx.application.Platform#isSupported(ConditionalFeature.SWT) в модульном приложении вылетел эксепшен из-за отсутствия swt-классов. Неочевидно, что такой безобидный метод может положить приложение.

  • Отсутствие Toast-уведомлений. Есть несколько неплохих померших либ, из которых можно выдрать логику. Или использовать ControlsFX или альтернативы:
    https://controlsfx.bitbucket.io/org/controlsfx/control/Notifications.html

  • Отсутствие собственного трея. Нужно использовать awt (будьте внимательны, в каком потоке выполняется слушатели) или использовать сторонние либы, например:
    https://github.com/dorkbox/SystemTray

  • В GTK3 не работает DragAndDrop. Починят в 13 версии: https://bugs.openjdk.java.net/browse/JDK-8211302

  • Drag-and-Drop (по крайней мере, пока его не переделали) требует сериализованного объекта для работы с ClipboardContent, а компоненты тулкита не могут в сериализацию, их наличие на полях может все сильно осложнить: Serialize JavaFX components, Serialize objects for Dragboard content. По ссылкам можно найти и хаки.

  • Невозможность локализовать некоторые контролы, самая частая потребность — контекстное меню всех текстовых контролов: Копировать, Вырезать, Вставить и т. п. Этот баг мой любимый. Их там целая компания: It is not practical/possible to manually Localize the JavaFx controls, I18N for Default ContextMenu of TextInput и т.п.

  • Может произойти потеря буфера обмена при завершении приложения. Зависит от операционной системы и других переменных, стоит учитывать и проверять: Javafx - Clipboard content deleted when program ends.

  • Фриз большинства IDE, а в самом плохом случае и рабочего стола, когда при дебаге точка останова находится в выпадающих меню или в другом неудачном месте: IDE hangs in debug mode on break point in java fx application.
    Решается через -Dsun.awt.disablegrab=true, однако отключит функционал перетаскивания. В самом простом случае можно завести несколько профилей запуска или отладки, в флагом и без, или еще что-то в этом роде.

  • Может возникнуть тонкий баг из-за многократного помещения одного и того же узла в граф узлов сцены. Контролы могут исчезать, это очень трудно выловить, у меня так пропадали иконки кнопок управления медиа. Похожий пример: JavaFX. Add more than one Label with image to Pane.
    Исключение выскочит лишь в случае установки через setRoot в Scene, тогда тулкит сообщит, что узел уже является рутом в другой сцене.

  • getClass().getResource или getClassLoader().getResource возвращают null, хотя вроде бы все файлы лежат как нужно.
    Это не проблема JavaFX, но тут могут быть различия в поведении IDE и сборщика. Например, для модульного приложения при запуске из IDE поиск ресурсов может быть в module path, в который не попадает директория с ними: если сравнить системные свойства jdk.module.path и java.class.path и создать символическую ссылку из ресурсов в одну из директорий module path, то загрузка заработает. Таких ситуаций может быть много, как и могут быть побочные эффекты от IDE. В репозитории примеров есть варианты с ресурсами, можно подменить файлы на свои, меняя потом код с наблюдением поломок. Это алгоритм вообще крайне эффективен: получить работоспособный пример, а потом ломать его своим кодом, не наоборот.

  • Не проблема JavaFX, но также отмечу её в контексте десктопа. При попытке открытия файла или ссылки в системе через java.awt.Desktop по крайней мере под Linux можно подвесить намертво рабочий стол, наблюдал сам неоднократно. Баги проявляются в самых разных вариантах: Desktop.getDesktop().browse Hangs, Desktop browse does not work in java for Ubuntu, JavaFX Freeze on Desktop.open(file), Desktop.browse(uri) и т.п. Зависает рандомно и при испробовании очередного обхода бага может сложиться ложное чувство успешной победы, а потом опять зависнет. После нескольких таких зависаний я перешел на открытие через ProcessBuilder и "xdg-open", хотя опять же не для всех дистров это подойдет.
    Нужно также учитывать, что в случае открытия файла без обработки результата (например, как тут), сторонняя программа может проверять код возврата fflush (stdio.h) на stdout, получать -1 и завершаться, некоторые программы будут работать, а некоторые - нет. Так может себя вести логгер: при невозможности логирования в stdout приложение рассматривает это как недопустимый режим работы и скорее всего правильно делает.

  • Создание .exe для запуска.
    Обычно в таких вопросах склонны скрываться два разных: как запустить приложение простым способом, либо же как создать именно бинарный файл, запускающий JRE с JavaFX. Часто срабатывает аналогия с другими GUI-приложениями, создавая ложное впечатление, что для запуска необходим именно отдельный бинарник для запуска, но уже есть - это бинарник JRE в виде java.exe\javaw.exe, которому нужно указать данные для запуска JavaFX приложения.
    Современный и простой способ для модульных приложений - плагины сборщиков из примеров на сайте тулкита, которые через jlink создают легковесный кастомный JRE-образ с текстовым shell-скриптом, запускаемым командной оболочкой, в Unix - bash, sh, fish и т.п. PowerShell, batch file ("батник") у Windows и т.д. Плагины умеют создавать даже инсталлятор через jpackage, а также подстраиваться под ось, например, запуская на Windows через javaw для отключения окна консоли. Однако в случае крупной легаси библиотеки или по каким-то другим причинам с модулями могут быть проблемы.
    Более сложный, но и более гибкий способ - собственный шелл-скрипт, который можно подсмотреть в примерах или взять за основу созданный сборщиком. Если возникает модульный конфликт между библиотеками, то их можно загружать из class path, а сам JavaFX из module path. Вам могут помочь также и JVM-флаги: --list-modules, --show-module-resolution и т.д.
    Более сложный способ - использование другого языка для написания бинарника-запускалки, как и использование сторонних - launch4j и т.п. Сложность их, в отличие от шелл-скрипта, в создании дополнительного низкоуровневого слоя абстракции, в случае проблем с которым нужно понимать механику запуска: либо лезть в исходники, либо анализировать бинарный файл.

Окна и Stage
  • javafx.stage.Stage.setOnCloseRequest слушатель не срабатывает при программном закрытии окна, хотя это кажется интуитивным: How to force JavaFX application close request programmatically.
    Также, в случае ручного вызова close, там внутри вызывается hide.

  • Нельзя создать StageStyle.TRANSPARENT или StageStyle.UNDECORATED без удаления окна из панели задач: Taskbar-less Undecorated Transparent Window.
    Нужно для создания виджетов. Я просто использую UTILITY.

  • На Linux системах может не появляться иконка окна, причем рандомно: stage.getIcons().add(icon); works sometimes. Баг не особо злобный, но жутко бесит.

Контроллеры
  • Соглашение о наименовании Nested Сontrollers: Introduction to FXML.
    Не всегда очевидно, что имя переменной контроллера формируется по соглашению как id из fxml + Controller. Ошибки бывают в самых разных сочетаниях: JavaFx Nested Controllers, Nested controller issue in Java FX и т.д.

  • Случайный вызов статического метода FXMLoader.load на созданном объекте. Особенно актуально для IDE, которые не предупреждают об этом. Все из-за расплывчатого эксепшена, который не конкретизирует причину. Ошибки бывают в самых разных формах: JavaFX Controller loading, JavaFX controller is always null

Контролы
  • При масштабировании контента в ScrollPane скроллбары могут не реагировать на это: ScrollPane : scrollbars not updated for transforms, нужна обертка в Group. Это как-бы написано в документации, но забывается и баг периодически всплывает вновь и вновь. Для начинающих странная пометка в документации может вообще ни о чем не говорить, я также её не заметил.

  • SplitPane.Divider не наследуется от Node, т.е. он имеет мало чего общего с тем разделителем, на который хочется повесить слушатель для отслеживания события мыши. Можно попробовать что-то вроде mainSplitContainer.lookupAll(".split-pane-divider") и проверить, что divider.getParent() == mainSplitContainer или что-то в этом роде.

  • Зачеркивание ячеек можно попробовать сделать через стили, как, например тут Java- Striking through item in ListView javafx? или получать узел через lookup(".text"). Это нужно учитывать для кастомного формата строки в TableView, например, для обходов багов с переносами и т.п., когда текст устанавливается графикой. В подобных случаях в строках таблиц текст может не иметь класса и узел нужно искать по имени.

  • Наезд скроллбара на контент, когда скроллбар должен показываться по необходимости. Очень и очень мерзкий баг: ScrollPane: horizontal scrollBar hides content, JavaFX ScrollPane's horizontal scroll bar hides content.

  • Переносы в кастомной ячейке ListView через setWrapText(true) не работают: ListView/ListCell: setWrapText/setMaxWidth has no effect. В комментах можно найти хак через установку очень малого значения. Только setWrapText(true).

  • Чтобы сделать ListView редактируемым только лишь setEditable(true) недостаточно, а нужно еще и предоставить соответствующую фабрику ячеек: JavaFX set listview to be editable.

  • В целях обратной совместимости у javafx.scene.control.ContextMenu есть метод show(Node anchor, double screenX, double screenY). В этом случае меню не будет закрываться при клике где-нибудь вне контрола. Для этого есть show(Window ownerWindow, double anchorX, double anchorY) из javafx.stage.PopupWindow: ContextMenu (Popupmenu) doesn't get dismissed when clicking outside.

  • При профилировании обнаружил, что сборщиком мусора не собираются закрытые окна, однако какого либо сохранения в поля, установки слушателей и т.п. не было. Методом тыка было определено потенциально проблемное место: метод TextInputControl.setContextMenu. Возможно, было что-то близко к Repeatedly creating a ContextMenu causes memory leak и этот участок кода есть смысл проверить дополнительно.

  • TableView, TreeView, ListView, TreeTableView теряет коммит при потере фокуса: Clicking outside of the edited cell, node, or entry should commit the value, TableCell commit on focus lost not possible in every case. Сам когда-то жутко костылял и все равно иногда работало криво.

  • Таблицы в JavaFX тесно связаны с объектом из которого заполняются. В случае наличия в нём разных свойств от ObjectProperty сильно упрощается редактирование, после коммита таблица уже содержит измененную сущность. Однако это рождает и проблемы: может быть нужна рефлексия (помним о модульности в Java 9), модели или бизнес-логика завязываются на gui-пакеты и может быть транзитивная зависимость: класс-сущность зависит от понятия предметной области, таблица зависит от сущности, в итоге таблица зависит от предметной области через класс-сущность. Это затрудняет добавление синтетических столбцов - нельзя добавлять чужеродные поля в класс-сущность, поскольку они не относятся к описываемому им понятию предметной области.
    Иными словами, в самом первом туториале о Table View используется класс Person, если в этой же таблице позже потребуется столбец с полной информацией или частью емайла, то могут быть проблемы, поскольку класс-сущность никак не может обо всем этом знать. Такая переделка может быть очень болезненной в случае сложной таблицы с многочисленной кастомизацией, фабриками и т.п., есть смысл задуматься сразу об отдельном классе. Технически, ограничения можно попытаться обойти через фабрики столбцов, но нужны жуткие костыли, ибо таблица имеет запутанное управление состояниями при редактировании и т.п.

  • В TreeView может прыгать скролл, проще заменить на TreeTableView, поскольку такое прыгание в рандомное место делает контрол совершенно неюзабельным: Unexpected scrolling behaviour of TreeView и быстро выводит из себя.

  • Menu при клике не работает как MenuItem. Кроме хака через графику: JavaFX 2.0 Activating a Menu like a MenuItem, есть вариант через EventFilter и вызывать menu.hide() вручную, чтобы скрыть вспомогательный MenuItem сразу после клика, конечно, в обертке Platform.runLater.

  • При вызове setValue у DatePicker срабатывает слушатель onAction: JavaFX DatePicker Triggers onAction event when setValue is called through code. Можно запросто пропустить и получить многократные вызовы методов. Я так получил три перезагрузки достаточно тяжелого окна с диаграммами вместо одной.

  • При тестировании на Linux Mint умирал Cinnamon при клике на Custom colors в DatePicker. У меня была таблица с кастомными ячейками, в которых при редактировании появлялся выбор цвета. Программа рандомно зависала и крашила рабочий стол, оставляя лишь один шанс перезагрузить систему - переключиться полностью в терминал. Такое же наблюдал с какой-то сторонней библиотекой кастомного DatePicker-а несколько версий минта назад, описания бага не нашел, он плавающий. Но это заставило меня заменить этот контрол во всех критичных участках, за основу взял идею от Jose Pereda Display custom color dialog directly- JavaFX ColorPicker и немного допилил под свои задачи.
    Стоит отметить, что похожие баги могут быть из-за запуска в IDE, например, при запуске из IntelliJ IDEA окошко диалога выбора директории вызывало краш, может быть там какие-нибудь флаги были JMV crash on show javafx.stage.DirectoryChooser

  • Tab не наследуется от Node. Из-за этого отличия усложняются методы установки графики, слушатели и т.п. Он не имеет удобных методов для отслеживания событий, например, хак для мыши через графику: Capturing MouseEnter event in a Tab. Такой способ имеет многочисленные проблемы если потребуется сложное поведение. Есть вариант отслеживать события через TabPane, способы самой разной степени костыльности, от фильтров событий, до прямого доступа через lookup, например: Javafx how to simulate a right click event on Tab.

  • При быстром удалении и добавлении табов (например, при перетаскивании тулбара с контентом) могут возникнуть артефакты графики и прочие глюки в TabPane. Это поведение похоже на TabPane does not update correctly tab positions in the header area after a quick remove and add operations. Несмотря на закрытый статус бага я снова встретился с подозрительно на него похожим в JavaFX 12. Исчез он также при добавлении задержки как и здесь: JavaFX TabPane tabs don't update position.

  • Практически всегда в Accordion есть какая-то дефолтная TitledPane, которая должна быть раскрыта. При этом SceneBuilder не поддерживает свойство expandedPane в Accordion, что не совсем очевидно: JavaFX: How to set a TiteledPane selected/opened by default. Если же пропишите в FXML, то он вывалится с ошибкой, может в будущем поддержку свойства добавят, проверяйте на всякий случай.

  • Иногда нужно отслеживать нажатия ссылок в WebView, например, для перенаправления в системный браузер. Если вы разбираете DOM с получением тегов, то стоит помнить о многочисленных подводных камнях вроде разных регистров, пустых тегов и т.п.: JavaFX WebView: Hyperlink listeners and issue with extracting href value from tag 'A'

Графика
  • Искажение шрифтов в Linux-системах, особенно на старых версиях (JavaFX 8). Создана даже небольшая демонстрация: https://github.com/woky/javafx-hates-linux
    В последних версиях ситуация немного улучшилась. Но некоторые флаги prism уже удалены.

  • Баг в CSS функции derive: CSS derive() function not working properly when calculating lighter. Наряду с ladder часто используется для обсчета всей темы, чтобы сократить в ней количество цветов. Можно долго гадать, почему цвет отличается.

  • Один раз случайно выставил -fx-stroke-dash-array в css для класса .chart-vertical-grid-lines в 0, вылетело java.lang.IllegalArgumentException: dash lengths all zero. После него с частотой тиков pulse начинает прилетать из JavaFX потока NPE. Приложение фризится, стоит помнить о такой возможности его сломать.

  • Артефакты графики или кривое сглаживание в ImageView. Иногда иконка отображается хорошо, иногда портит. Можно попробовать глянуть что-то из этого: JavaFX ImageView without any smoothing, https://gist.github.com/jewelsea/5415891

  • Может быть проблема с gif-анимациями, тоже встречался с ним. Это баг самой джавы, но тоже отмечу его: stringtable overflow in GIFImageReader (ArrayIndexOutOfBoundsException).
    Будет эксепшен java.lang.ArrayIndexOutOfBoundsException: 4096, ArrayIndexOutOfBoundsException: 4096 while reading gif file.

  • В некоторых случаях даже простейшая анимация может загружать CPU на 100%. Может зависеть от рабочего стола, сталкивался с этим на Linux: Why does such a simple animation consume high cpu?, A simple animation consume high cpu.

  • Может быть незаметная потеря иконки, если в FXML будет указан неправильный путь к изображению. Возможно, как-то коррелирует с обработкой ошибок изображений: JavaFX Image Error Handling. SceneBuilder должен пометить такой случай иконкой с ошибкой.

  • Можно наткнуться на целый класс проблем, связанных с Prism, рендерингом или специфическим железом: Какую технологию использовать для непрерывное проигрывания видео?, White screen when running Java FX Applications.
    Сами магические флаги можно узнать из исходников, но в различных версиях они постоянно меняются. Еще некоторые есть на вики.
    Подергивание и артефакты часто встречается при перемещении узлов: correct way to move a node by dragging in javafx 2?, есть даже баг: Performance issue when tracking mouse events.

  • Встроенного 3D может быть недостаточно, а равно как и может потребоваться более низкоуровневое графическое api, например, OpenGL. Я использовал JOGL, хотя есть и другие варианты, например, openglfx, DriftFX и т.п. В JOGL есть NewtCanvasJFX, наследуемый от javafx.scene.canvas.Canvas и конструктор которого может принять GLWindow. Так, например, можно интегрировать libGDX в JavaFX для целей, например, GUI-редактора уровней. Ради эксперимента я запускал несколько libgdx-демок и они вполне неплохо работали. Будут, конечно же, определённые сложности от многопоточности и прочей специфики, но работать как-то будет.