Почему этот элемент FXML на базе Kotlin не инициализируется и не отображается?

Я хочу написать многоразовые компонуемые компоненты 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")
    }
}

Зачем реализовывать устаревший интерфейс @Initalizable? «Этот интерфейс был заменен автоматическим внедрением свойств местоположения и ресурсов в контроллер. FXMLLoader теперь будет автоматически вызывать любой соответствующим образом аннотированный метод инициализации() без аргументов, определенный контроллером. Рекомендуется использовать подход внедрения, когда это возможно. ".

jewelsea 10.06.2024 05:48

«Я также пробовал... с реализацией Initializable и без нее...»

jc0022 10.06.2024 05:51

Тогда отдельно, если это «пользовательский компонент», то почему бы не использовать конструкцию fx:root, как описано в разделе Создание пользовательского элемента управления с помощью FXML?

jewelsea 10.06.2024 05:51

«Я также пробовал... использовать как fx:root, так и VBox в качестве корневого элемента FXML, согласно различным советам», включая эту статью (как показано в показанном коде), но в любом случае эта статья предназначена для Java 8 и JavaFX 8, и более поздние примеры, похоже, не требуют ручного вызова FXMLLoader, когда в FXML установлен fx:controller, как в этой статье, что указывает мне на то, что он, возможно, устарел, хотя я тоже это пробовал.

jc0022 10.06.2024 05:53

Но код, который у вас есть в вопросе, насколько я могу судить, не будет работать ни в одном случае (я не могу говорить о коде, который не показан, и я не знаю Kotlin, поэтому могу ошибаться).

jewelsea 10.06.2024 05:57

Спасибо, а не могли бы вы объяснить, почему нет?

jc0022 10.06.2024 05:58

Давайте продолжим обсуждение в чате.

jewelsea 10.06.2024 05:58
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
7
69
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

FXML

Полным источником информации о том, как работает FXML, является документ Введение в FXML.

Существует также немало руководств по FXML. Один из них — Учебник по Oracle . Это руководство было написано для JavaFX 8, но с тех пор в FXML мало что изменилось, поэтому оно все еще применимо. Самое большое изменение — это то, что вам нужно сделать, если развертываете свое приложение в виде Java-модуля . Еще один хороший учебник — JavaFX Tutorials на jenkov.com, ссылка на который на самом деле есть на https://openjfx.io.

Проблема в вашем коде

Когда FXMLLoader видит элемент <LauncherPanel/> в файле FXML, он просто попытается создать экземпляр типа, рефлексивно вызывая один из его конструкторов; в данном случае это будет конструктор без аргументов. Но это все. Ничто в этом элементе или классе LauncherPanel не указывает на необходимость загрузки дополнительного FXML. Это означает, что класс не создается как контроллер FXML, не говоря уже о корне FXML, и, следовательно, метод initialize не вызывается.

Многоразовые компоненты FXML

Существует два способа создания повторно используемых компонентов FXML: fx:root и fx:include.

fx:root

Вы используете fx:root, когда хотите скрыть использование FXML. Вы создаете пользовательский класс, граф объектов которого определен в файле FXML, но код, использующий пользовательский класс, не знает об этом. При использовании этого подхода вы должны:

  1. Используйте fx:root в качестве корневого элемента файла FXML.

  2. Определите атрибут type в корневом элементе. Документ «Введение в FXML» устанавливает значение этого атрибута для класса, который расширяет пользовательский класс.

  3. Вручную установите root из FXMLLoader. Если контроллер должен быть тем же экземпляром, что и root, то также вручную установите controller загрузчика; в этом случае не определяйте атрибут fx:controller. В противном случае, если корень и контроллер относятся к разным классам, вы можете использовать fx:controller. Какой подход вы используете, зависит от вас.

  4. Загрузите 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'!"
  }
}

FXML-файлы

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>

Gradle-файлы

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 и представлением (хотя и с некоторой жесткой связью между ними).

James_D 10.06.2024 13:45

Другие вопросы по теме