JavaFX自定义组件在SceneBuilder中的使用

4

我正在使用Java 20、JavaFX 20和Maven创建一个小型个人项目。我在创建可重用组件并通过主场景控制器操纵它们方面遇到了问题。

首先,我按照官方文档中列出的步骤进行操作。之后,我进入SceneBuilder,在SceneBuilder中导入我的自定义组件的FXML文件(单击“库”处的小引擎图标 -> JAR/FXML管理器 -> 从文件系统添加库/FXML),并像使用任何默认组件一样将其添加到场景中。然后,我为我的自定义组件添加了fx:id,并将其添加到我的场景控制器类中,以便我可以对其进行操作,但是我收到了以下错误信息。

Exception in Application start method
java.lang.reflect.InvocationTargetException
    at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:119)
    at java.base/java.lang.reflect.Method.invoke(Method.java:578)
    at javafx.graphics@20/com.sun.javafx.application.LauncherImpl.launchApplicationWithArgs(LauncherImpl.java:464)
    at javafx.graphics@20/com.sun.javafx.application.LauncherImpl.launchApplication(LauncherImpl.java:363)
    at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
    at java.base/java.lang.reflect.Method.invoke(Method.java:578)
    at java.base/sun.launcher.LauncherHelper$FXHelper.main(LauncherHelper.java:1081)
Caused by: java.lang.RuntimeException: Exception in Application start method
    at javafx.graphics@20/com.sun.javafx.application.LauncherImpl.launchApplication1(LauncherImpl.java:893)
    at javafx.graphics@20/com.sun.javafx.application.LauncherImpl.lambda$launchApplication$2(LauncherImpl.java:195)
    at java.base/java.lang.Thread.run(Thread.java:1623)
Caused by: javafx.fxml.LoadException: 
/C:/Users/user/Desktop/eclipse-workspace/Project 3/target/classes/app/views/fxml/Menu.fxml:43

    at javafx.fxml@20/javafx.fxml.FXMLLoader.constructLoadException(FXMLLoader.java:2722)
    at javafx.fxml@20/javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2700)
    at javafx.fxml@20/javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2563)
    at javafx.fxml@20/javafx.fxml.FXMLLoader.load(FXMLLoader.java:2531)
    at app/app.Main.loadFXML(Main.java:29)
    at app/app.Main.start(Main.java:17)
    at javafx.graphics@20/com.sun.javafx.application.LauncherImpl.lambda$launchApplication1$9(LauncherImpl.java:839)
    at javafx.graphics@20/com.sun.javafx.application.PlatformImpl.lambda$runAndWait$12(PlatformImpl.java:483)
    at javafx.graphics@20/com.sun.javafx.application.PlatformImpl.lambda$runLater$10(PlatformImpl.java:456)
    at java.base/java.security.AccessController.doPrivileged(AccessController.java:400)
    at javafx.graphics@20/com.sun.javafx.application.PlatformImpl.lambda$runLater$11(PlatformImpl.java:455)
    at javafx.graphics@20/com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:95)
    at javafx.graphics@20/com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
    at javafx.graphics@20/com.sun.glass.ui.win.WinApplication.lambda$runLoop$3(WinApplication.java:185)
    ... 1 more
Caused by: java.lang.IllegalArgumentException: Can not set app.components.Custom field app.controllers.Menu.cc to javafx.scene.layout.VBox
    at java.base/jdk.internal.reflect.FieldAccessorImpl.throwSetIllegalArgumentException(FieldAccessorImpl.java:228)
    at java.base/jdk.internal.reflect.FieldAccessorImpl.throwSetIllegalArgumentException(FieldAccessorImpl.java:232)
    at java.base/jdk.internal.reflect.MethodHandleObjectFieldAccessorImpl.set(MethodHandleObjectFieldAccessorImpl.java:115)
    at java.base/java.lang.reflect.Field.set(Field.java:834)
    at javafx.fxml@20/javafx.fxml.FXMLLoader.injectFields(FXMLLoader.java:1175)
    at javafx.fxml@20/javafx.fxml.FXMLLoader$ValueElement.processValue(FXMLLoader.java:870)
    at javafx.fxml@20/javafx.fxml.FXMLLoader$ValueElement.processStartElement(FXMLLoader.java:764)
    at javafx.fxml@20/javafx.fxml.FXMLLoader.processStartElement(FXMLLoader.java:2853)
    at javafx.fxml@20/javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2649)
    ... 13 more
Exception running application app.Main

我注意到一个奇怪的事情,就是当我将组件添加到主场景中时,它显示为VBox而不是Custom,尽管在“层次结构”选项卡中拖动组件时,显示组件的名称为Custom,而不是VBox。
以下是相关文件 Custom.java
package app.components;

import java.io.IOException;

import app.Main;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;

public class Custom extends VBox {
    
    @FXML private Button plusBtn;
    @FXML private Button minusBtn;
    @FXML private Label label;
    
    public Custom() {
        FXMLLoader loader = new FXMLLoader(Main.class.getResource("components/Custom.fxml"));
        loader.setRoot(this);
        loader.setController(this);
        try {
            loader.load();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
    
    public void newText(String text) {
        label.setText(text);
    }
}

Custom.fxml

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.VBox?>

<VBox alignment="CENTER" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" xmlns="http://javafx.com/javafx/19" xmlns:fx="http://javafx.com/fxml/1">
   <children>
      <Button fx:id="plusBtn" mnemonicParsing="false" text="+" />
      <Label fx:id="label" text="Label" />
      <Button fx:id="minusBtn" mnemonicParsing="false" text="-" />
   </children>
</VBox>

目前,我的主要场景只是一个空的StackPane,其中心是我自定义的组件,我将其命名为fx:id为"cc"。

Menu.java

package app.controllers;

import app.components.Custom;

public class Menu {

    @FXML
    private Custom cc;

        public void initialize() {
        cc.newText("Test");
    }
}

module-info.java

module app {
    requires javafx.controls;
    requires javafx.fxml;
    requires javafx.media;
    requires javafx.graphics;
    requires javafx.base;
    
    opens app to javafx.fxml;
    opens app.controllers to javafx.fxml;
    
    exports app;
}

问题在于,当我将组件从“自定义”部分拖放到“StackPane”中时,它显示为“VBox”,而不是“Custom”。这里有一张截图,可能会更清楚地说明我的意思:

image

我希望组件显示为Custom而不是VBox,因为SceneBuilder告诉我它找不到“cc”的可注入字段,尽管我在控制器类中有该字段。


1
你需要像你链接的文档中所示使用“动态根”(<fx:root …>)。 - James_D
@James_D 抱歉如果这会给您带来新的通知,但我无法编辑我的先前回复。首先,谢谢,现在我可以开始我的应用程序了。组件正确显示,但当我在控制器中声明它时,我只能将其创建为VBox而不是Custom,因此我无法使用Custom.java类中的方法。我该如何解决这个问题? - gsshj
1
你能澄清一下问题吗?你有'@FXML private Custom cc;',是什么让它不能正常工作呢?如果你想要更有效的帮助,最好提供一个最小可重现示例(例如,你没有提供与 'Menu' 相关联的 FXML 文件)。 - Slaw
@Slaw,我的自定义组件是一个VBox,其中有两个按钮和一个标签。我的Menu.fxml是一个空的StackPane,其中包含我的组件在中间。问题是,当我将(从自定义部分拖放到StackPane)我的组件添加时,它显示为VBox而不是Custom。这是一张截图,可能会更清楚说明我的意思 https://imgur.com/a/3Jmf0rY。我希望组件只显示为Custom,而不是VBox,因为SceneBuilder告诉我它找不到“cc”的可注入字段,即使我已经在控制器类中突出显示了您提到的字段。 - gsshj
1
啊,没意识到你的问题是关于组件在Scene Builder中的显示方式。不幸的是,我从未尝试过在正在开发的同一项目中使用自定义组件在Scene Builder中。不确定如何帮助。 - Slaw
2个回答

5

这个答案很长,但是有很多内容需要处理,所以就是这样。

这些步骤对我起作用。如果按照步骤执行,很可能对您有用。如果您有任何改动,不能保证它将工作,也不能保证我会给予太多支持。

我会简单说明要做什么。

基本方法是:

  1. 创建一个模块,其中包含所需的自定义控件。
  2. 将具有自定义控件的模块导入到SceneBuilder中。
  3. 使用SceneBuilder设计您的应用程序,使用自定义控件。
  4. 为使用SceneBuilder生成的FXML创建一个新项目。新项目依赖于自定义控件模块的功能。
  5. 构建并运行应用程序项目。

Idea可以创建和使用多模块的maven项目,因此多个模块可以在单个项目中(这可能是解决这个问题的合理方法),但是这种设置更加复杂,不是解决您的问题的核心,因此我没有在这里记录它。

第一步:为您的自定义组件创建JavaFX项目

在Idea中 -> 新建项目 -> JavaFX 项目 -> (使用Maven) -> 组名 "com.example",艺术品名称 "custom-component"。
用下面的文件替换生成的Java源文件,FXML和pom.xml。

src/main/java/com/example/customcomponent/CustomComponent.java

package com.example.customcomponent;

import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;

import java.io.IOException;

public class CustomComponent extends VBox {
    
    @FXML private Button plusBtn;
    @FXML private Button minusBtn;
    @FXML private Label label;
    
    public CustomComponent() {
        FXMLLoader loader = new FXMLLoader(
                CustomComponent.class.getResource(
                        "custom-component.fxml"
                )
        );
        loader.setRoot(this);
        loader.setController(this);
        try {
            loader.load();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
    
    public void newText(String text) {
        label.setText(text);
    }
}

src/main/java/module-info.java

module com.example.customcomponent {
    requires javafx.controls;
    requires javafx.fxml;

    opens com.example.customcomponent to javafx.fxml;
    exports com.example.customcomponent;
}

src/main/resources/com/example/customcomponent/custom-component.fxml

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<fx:root type="javafx.scene.layout.VBox" alignment="CENTER" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" xmlns="http://javafx.com/javafx/19" xmlns:fx="http://javafx.com/fxml/1">
    <children>
        <Button fx:id="plusBtn" mnemonicParsing="false" text="+" />
        <Label fx:id="label" text="Label" />
        <Button fx:id="minusBtn" mnemonicParsing="false" text="-" />
    </children>
</fx:root>

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>custom-component</artifactId>
    <version>1.0-SNAPSHOT</version>
    <name>custom-component</name>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.openjfx</groupId>
            <artifactId>javafx-controls</artifactId>
            <version>19</version>
        </dependency>
        <dependency>
            <groupId>org.openjfx</groupId>
            <artifactId>javafx-fxml</artifactId>
            <version>19</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.11.0</version>
                <configuration>
                    <source>17</source>
                    <target>17</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

修改后重新导入Maven项目。

进入Maven窗口,执行maven -> install命令。

步骤2:将自定义组件导入SceneBuilder

下载并安装SceneBuilder 19。

在SceneBuilder中创建一个新的(空)项目。

单击库搜索字段旁边的齿轮图标。

JAR/FXML Manager

选择“JAR/FXML 管理器”。

选择“从存储库手动添加库”。

输入 Group ID:“com.example”,Artifact ID:“custom-component”,然后按 TAB 键。

从下拉菜单中选择版本:“1.0-SNAPSHOT (local)”。

Manually add Library from repository

选择“添加JAR”。

“导入”对话框将显示带有勾号和组件预览图像的“CustomComponent”。

Import dialog

保持新组件的默认尺寸设置,然后选择“导入组件”。
新安装的库将会列在“库管理器”对话框中。

Installed library

关闭“库管理器”对话框。

步骤3:使用自定义组件设计应用程序UI

进入库搜索字段,输入“StackPane”。

将StackPane拖到您正在创建的场景中。

进入库搜索字段,输入“CustomComponent”。

将CustomComponent拖到StackPane的中心。

单击CustomComponent,然后单击“代码”面板并输入“fx:id”作为:“customComponent”。

单击控制器面板并输入控制器类名称“com.example.customcomponentdemo.HelloController”。

将您的FXML文件保存为“hello-view.xml”,它将如下所示:

hello-view in SceneBuilder

<?xml version="1.0" encoding="UTF-8"?>

<?import com.example.customcomponent.CustomComponent?>
<?import javafx.scene.layout.StackPane?>


<StackPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/19" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.example.customcomponentdemo.HelloController">
   <children>
      <CustomComponent fx:id="customComponent" />
   </children>
</StackPane>

步骤四:使用自定义组件创建JavaFX应用程序项目

在Idea中 -> 新建JavaFX项目(maven)-> Group ID:"com.example",Artifact ID:"custom-component-demo" -> 将生成的文件替换为以下内容:

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>custom-component-demo</artifactId>
    <version>1.0-SNAPSHOT</version>
    <name>custom-component-demo</name>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.openjfx</groupId>
            <artifactId>javafx-controls</artifactId>
            <version>20</version>
        </dependency>
        <dependency>
            <groupId>org.openjfx</groupId>
            <artifactId>javafx-fxml</artifactId>
            <version>20</version>
        </dependency>
        <dependency>
            <groupId>com.example</groupId>
            <artifactId>custom-component</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.11.0</version>
                <configuration>
                    <source>20</source>
                    <target>20</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

(在编辑pom.xml文件后重新导入Maven项目)

src/main/java/module-info.java

module com.example.customcomponentdemo {
    requires javafx.controls;
    requires javafx.fxml;
    requires com.example.customcomponent;

    opens com.example.customcomponentdemo to javafx.fxml;
    exports com.example.customcomponentdemo;
}

src/main/resources/com/example/customcomponentdemo/hello-view.fxml

使用你从SceneBuilder保存的文件。

src/main/java/com/example/customcomponentdemo/HelloApplication.java

从生成的代码中未更改:

package com.example.customcomponentdemo;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.stage.Stage;

import java.io.IOException;

public class HelloApplication extends Application {
    @Override
    public void start(Stage stage) throws IOException {
        FXMLLoader fxmlLoader = new FXMLLoader(HelloApplication.class.getResource("hello-view.fxml"));
        Scene scene = new Scene(fxmlLoader.load(), 320, 240);
        stage.setTitle("Hello!");
        stage.setScene(scene);
        stage.show();
    }

    public static void main(String[] args) {
        launch();
    }
}

src/main/java/com/example/customcomponentdemo/HelloController.java

package com.example.customcomponentdemo;

import com.example.customcomponent.CustomComponent;
import javafx.fxml.FXML;

public class HelloController {
    @FXML
    private CustomComponent customComponent;

    @FXML
    private void initialize() {
        customComponent.newText("xyzzy");
    }
}

步骤5:运行您的应用程序

双击HelloApplication.java文件,右键构建应用程序并运行它。

应用程序将显示您的自定义组件,其中组件的文本由应用程序控制器初始化。

App with customized custom component

版本注意事项

请仔细查看自定义组件项目文件中的Java源版本和目标版本,它们列出的是17而不是20。此外,JavaFX版本为19。这些设置与使用的SceneBuilder版本相匹配。SceneBuilder 19只能理解JavaFX 19组件(及更低版本),并且在JDK 17上运行,因此只能理解编译为Java 17(或更低版本)的JAR文件。如果您尝试使用比SceneBuilder理解的更高目标JDK构建自定义组件,则它将无法在JAR文件中找到您的组件以导入它。

正如您所看到的,对于执行项目,您没有相同的限制,因此它可以在JavaFX 20和JDK 20上自由运行,没有任何问题。

相关


1
如果您正在使用Maven构建应用程序,您可以连续运行"clean"和"install" Maven生命周期命令来生成组件的jar文件。
"clean"命令会清除项目目录中生成的文件和目标文件夹。
"install"命令会构建应用程序,并生成组件的jar文件。
注意事项:
  • 然后生成的 .jar 文件将存储在此目录中:

    %YOUR_PROJECT_DIRECTORY%\target
    
  • 您必须确保项目目录结构已设置,我的项目结构如下:

    project structure

  • Maven 命令可以在此处找到(在 IntelliJ 的右侧菜单栏中),并且可以通过双击运行(作为深入研究 JavaFX 的人,我希望您能预料到这一点 :)):

right side of intelliJ

我找到了@RandomCode描述这个过程的视频: 1 2 这个视频也很有用,可以帮助理解概念,但是这个方法对我来说不起作用:3

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接