Я хочу написать многоразовые компонуемые компоненты JavaFX/FXML на Kotlin. Я использую Java 21, и мой JavaFX предоставляется Gradle версии 22.0.1.
Мой основной класс загружает начальную сцену в окне через FXMLLoader.load, и я вижу тому подтверждение. Однако скелетный пользовательский компонент, который является частью этого графа сцены, не отображается.
Init {} контроллера пользовательского компонента вызывается, но никакая комбинация реализации/не реализации Initializable с аргументами или без них не приводит к вызову метода Initialize(). Я попытался расширить Control и VBox и использовать fx:root и VBox для моего корневого элемента.
Как я могу это исправить? Существует ли текущий и достаточно подробный ресурс, посвященный FXML с Kotlin? Я перепробовал все, что нашел в ресурсах Oracle и сторонних блогах, что отдаленно соответствует моему сценарию, но это не работает.
Это сцена верхнего уровня, которая отображается в окне, и я вижу «Test1».
// Launcher.fxml loaded via FXMLLoader.load in Main.kt
<?xml version = "1.0" encoding = "UTF-8"?>
<?package project.ui?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.VBox?>
<?import project.ui.LauncherPanel?>
<VBox xmlns = "http://javafx.com/javafx"
xmlns:fx = "http://javafx.com/fxml"
fx:controller = "project.ui.Launcher"
prefHeight = "400.0" prefWidth = "600.0">
<Label>Test1</Label>
<LauncherPanel/>
</VBox>
Однако внутренний элемент, похоже, не инициализирован, и «Test2» не отображается. Я пробовал использовать fx:root и просто VBox в качестве корневого элемента.
// LauncherPanel.fxml
<?xml version = "1.0" encoding = "UTF-8"?>
<?package project.ui?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<fx:root type = "javafx.scene.layout.VBox" xmlns = "http://javafx.com/javafx"
xmlns:fx = "http://javafx.com/fxml"
fx:controller = "project.ui.LauncherPanel">
<Label text = "Test2"/>
</fx:root>
Этот код программной части действительно кажется несколько запутанным, поскольку его init {} вызывается, но его инициализация() не вызывается ни при какой комбинации советов, которые я применил. Я также пробовал добавить @FXML в Initialize() и пытался расширить Control и VBox с реализацией Initialize и без нее, с параметрами для инициализации и без них, с fx:root и VBox в качестве корневого элемента FXML, согласно различным советам.
//LauncherPanel.kt
package project.ui
import javafx.fxml.Initializable
import javafx.scene.layout.VBox
import java.net.URL
import java.util.*
class LauncherPanel: VBox(), Initializable {
init {
println("init gets invoked")
}
override fun initialize(p0: URL?, p1: ResourceBundle?) {
println("initialize is not invoked")
}
}
«Я также пробовал... с реализацией Initializable и без нее...»
Тогда отдельно, если это «пользовательский компонент», то почему бы не использовать конструкцию fx:root, как описано в разделе Создание пользовательского элемента управления с помощью FXML?
«Я также пробовал... использовать как fx:root, так и VBox в качестве корневого элемента FXML, согласно различным советам», включая эту статью (как показано в показанном коде), но в любом случае эта статья предназначена для Java 8 и JavaFX 8, и более поздние примеры, похоже, не требуют ручного вызова FXMLLoader, когда в FXML установлен fx:controller, как в этой статье, что указывает мне на то, что он, возможно, устарел, хотя я тоже это пробовал.
Но код, который у вас есть в вопросе, насколько я могу судить, не будет работать ни в одном случае (я не могу говорить о коде, который не показан, и я не знаю Kotlin, поэтому могу ошибаться).
Спасибо, а не могли бы вы объяснить, почему нет?
Давайте продолжим обсуждение в чате.





Полным источником информации о том, как работает FXML, является документ Введение в FXML.
Существует также немало руководств по FXML. Один из них — Учебник по Oracle . Это руководство было написано для JavaFX 8, но с тех пор в FXML мало что изменилось, поэтому оно все еще применимо. Самое большое изменение — это то, что вам нужно сделать, если развертываете свое приложение в виде Java-модуля . Еще один хороший учебник — JavaFX Tutorials на jenkov.com, ссылка на который на самом деле есть на https://openjfx.io.
Когда FXMLLoader видит элемент <LauncherPanel/> в файле FXML, он просто попытается создать экземпляр типа, рефлексивно вызывая один из его конструкторов; в данном случае это будет конструктор без аргументов. Но это все. Ничто в этом элементе или классе LauncherPanel не указывает на необходимость загрузки дополнительного FXML. Это означает, что класс не создается как контроллер FXML, не говоря уже о корне FXML, и, следовательно, метод initialize не вызывается.
Существует два способа создания повторно используемых компонентов FXML: fx:root и fx:include.
fx:rootВы используете fx:root, когда хотите скрыть использование FXML. Вы создаете пользовательский класс, граф объектов которого определен в файле FXML, но код, использующий пользовательский класс, не знает об этом. При использовании этого подхода вы должны:
Используйте fx:root в качестве корневого элемента файла FXML.
Определите атрибут type в корневом элементе. Документ «Введение в FXML» устанавливает значение этого атрибута для класса, который расширяет пользовательский класс.
Вручную установите root из FXMLLoader. Если контроллер должен быть тем же экземпляром, что и root, то также вручную установите controller загрузчика; в этом случае не определяйте атрибут fx:controller. В противном случае, если корень и контроллер относятся к разным классам, вы можете использовать fx:controller. Какой подход вы используете, зависит от вас.
Загрузите FXML в конструктор пользовательского класса.
Затем вы используете пользовательский класс, как и любой другой.
Поскольку именно этот подход вы используете в своем вопросе, ниже приведен полный пример использования fx:root.
fx:includeИспользовать fx:include проще. В включенном вами файле FXML нет ничего особенного. Он не использует fx:root, он может определять атрибут fx:controller как обычно, и вы не связываете его с пользовательским классом. Все что тебе нужно это:
<fx:include source = "<path to reusable FXML file>"/>
Всякий раз, когда вы хотите вложить один файл FXML в другой.
Обратите внимание, что этот подход предназначен не только для создания повторно используемых компонентов FXML. Иногда вам может просто потребоваться разделить монолитный файл FXML на несколько файлов FXML меньшего размера для удобства обслуживания.
Обратите внимание, что интерфейс Initializable, хотя и не является технически устаревшим, устарел. Из его документации:
ПРИМЕЧАНИЕ. Этот интерфейс был заменен автоматическим внедрением свойств
locationиresourcesв контроллер.FXMLLoaderтеперь будет автоматически вызывать любой соответствующим образом аннотированный метод без аргументовinitialize(), определенный контроллером. По возможности рекомендуется использовать инъекционный подход.
Это означает, что предпочтительный подход выглядит так:
import javafx.fxml.FXML
import java.net.URL
import java.util.ResourceBundle
class Controller {
@FXML private lateinit var location: URL
@FXML private lateinit var resources: ResourceBundle
@FXML
private fun initialize() {
// perform any needed initialization
}
}
И свойства location и resources, и метод initialize являются необязательными по отдельности. Включайте их только тогда, когда они вам нужны.
Обратите внимание, что если ваш контроллер способен работать с ResourceBundle, но также может работать, когда пакет не указан, вам следует определить свойство следующим образом:
@FXML private var resources: ResourceBundle? = null
fx:rootВот рабочий пример использования fx:root с классом Kotlin, где «корневой тип» используется в другом файле FXML (как в вашем вопросе).
Пример был разработан с использованием:
Ява 22.0.1
JavaFX 22.0.1
Градл 8.8
Windows 11
<project-directory>
| build.gradle.kts
| gradlew
| gradlew.bat
| settings.gradle.kts
|
+---gradle
| \---wrapper
| gradle-wrapper.jar
| gradle-wrapper.properties
|
\---src
\---main
+---kotlin
| LauncherPanel.kt
| Main.kt
|
\---resources
LauncherPanel.fxml
Main.fxml
Main.kt
package com.example.fxmlsample
import javafx.application.Application
import javafx.stage.Stage
import javafx.fxml.FXMLLoader
import javafx.scene.Scene
import javafx.scene.Parent
fun main(args: Array<out String>) {
Application.launch(Main::class.java, *args)
}
class Main : Application() {
override fun start(primaryStage: Stage) {
val loader = FXMLLoader().apply {
location = Main::class.java.getResource("/Main.fxml")!!
}
primaryStage.scene = Scene(loader.load<Parent>())
primaryStage.title = "FXML Sample"
primaryStage.show()
}
}
LauncherPanel.kt
package com.example.fxmlsample
import javafx.scene.layout.StackPane
import javafx.scene.control.Label
import javafx.fxml.FXML
import javafx.fxml.FXMLLoader
class LauncherPanel : StackPane() {
@FXML
private lateinit var label: Label
init {
val loader = FXMLLoader().apply {
location = LauncherPanel::class.java.getResource("/LauncherPanel.fxml")!!
setRoot(this@LauncherPanel)
setController(this@LauncherPanel)
}
loader.load<LauncherPanel>()
}
@FXML
private fun initialize() {
label.text = "Hello, from 'initialize'!"
}
}
Main.fxml
<?xml version = "1.0" encoding = "UTF-8"?>
<?import com.example.fxmlsample.LauncherPanel?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.Separator?>
<?import javafx.scene.layout.VBox?>
<VBox xmlns = "http://javafx.com/javafx" xmlns:fx = "http://javafx.com/fxml"
spacing = "10" alignment = "TOP_CENTER" prefWidth = "500" prefHeight = "300">
<padding>
<Insets topRightBottomLeft = "10"/>
</padding>
<Label text = "FXML Sample"/>
<Separator/>
<LauncherPanel VBox.vgrow = "ALWAYS"/>
</VBox>
Панель запуска.fxml
<?xml version = "1.0" encoding = "UTF-8"?>
<?import javafx.scene.control.Label?>
<fx:root type = "javafx.scene.layout.StackPane" xmlns = "http://javafx.com/javafx"
xmlns:fx = "http://javafx.com/fxml">
<Label fx:id = "label"/>
</fx:root>
settings.gradle.kts
rootProject.name = "fxml-sample"
build.gradle.kts
plugins {
kotlin("jvm") version "2.0.0"
id("org.openjfx.javafxplugin") version "0.1.0"
application
}
group = "com.example"
version = "0.1.0"
repositories {
mavenCentral()
}
javafx {
modules("javafx.controls", "javafx.fxml")
version = "22.0.1"
}
application {
mainClass = "com.example.fxmlsample.MainKt"
}
Обратите внимание, что использование fx:root и fx:controller по сути ортогонально. Не требуется, чтобы динамический корень также выступал в качестве контроллера: вы можете использовать fx:root и вызывать setRoot(this), но также использовать fx:controller и определить отдельный класс контроллера обычным способом. Это, вероятно, делает класс, реализующий динамический корень, очень тонким, но он по-прежнему скрывает использование FXML и сохраняет разделение между контроллером FXML и представлением (хотя и с некоторой жесткой связью между ними).
Зачем реализовывать устаревший интерфейс @Initalizable? «Этот интерфейс был заменен автоматическим внедрением свойств местоположения и ресурсов в контроллер. FXMLLoader теперь будет автоматически вызывать любой соответствующим образом аннотированный метод инициализации() без аргументов, определенный контроллером. Рекомендуется использовать подход внедрения, когда это возможно. ".