如何确定 JavaFX 应用程序所需的 FXML 文件、CSS 文件、图像和其他资源的正确路径?

How do I determine the correct path for FXML files, CSS files, Images, and other resources needed by my JavaFX Application?

提问人:James_D 提问时间:5/1/2020 更新时间:3/17/2023 访问量:16586

问:

我的 JavaFX 应用程序需要能够找到 FXML 文件以加载它们,以及样式表(CSS 文件)和图像。当我尝试加载这些项目时,我经常会遇到错误,或者我尝试加载的项目在运行时根本无法加载。FXMLLoader

对于FXML文件,我看到的错误消息包括

Caused by: java.lang.NullPointerException: location is not set

对于图像,堆栈跟踪包括

Caused by: java.lang.IllegalArgumentException: Invalid URL: Invalid URL or resource not found

如何确定这些资源的正确资源路径?

JavaFX的

评论

0赞 James_D 5/1/2020
由于 JavaFX 标签上有很多关于加载资源的问题,我把这个问答作为社区 wiki 发布。如果您认为问题或答案可以改进,请编辑它们。
3赞 kleopatra 5/1/2020
好主意和答案:)添加到标签 wiki 的 faq 中,以便我们可以轻松找到它以进行重复闭合。
0赞 psyopus 6/15/2022
此外,您可以检查目标目录中的所有已编译类。有时,IDE 不想编译这些文件,因此,您无法在运行时获取它们。
0赞 James_D 6/15/2022
@psyopus 这在答案中的“故障排除”下进行了讨论。

答:

72赞 17 revs, 3 users 83%James_D #1

答案的简短版本:

  • 使用 或 创建资源getClass().getResource(...)SomeOtherClass.class.getResource(...)URL
  • 将绝对路径(带前导)或相对路径(不带前导)传递给方法。路径是包含资源的包,替换为 .//getResource(...)./
  • 请勿在资源路径中使用。如果应用程序捆绑为 jar 文件,这将不起作用。如果资源不在同一包中或类的子包中,请使用绝对路径。..
  • 对于 FXML 文件,请直接将 .URLFXMLLoader
  • 对于图像和样式表,调用 以生成要传递给 or 构造函数的 或添加到列表中。toExternalForm()URLStringImageImageViewstylesheets
  • 要进行故障排除,请检查生成文件夹(或 jar 文件)的内容,而不是文件夹的内容。
  • 在获取资源时放置在路径中总是错误的。该目录仅在开发和生成时可用,在部署和运行时不可用。srcsrc

完整答案

内容

  1. 本答案的范围
  2. 资源在运行时加载
  3. JavaFX 使用 URL 加载资源
  4. 资源作为流
  5. 资源名称规则
  6. 创建资源 URLgetClass().getResource(...)
  7. 组织代码和资源
  8. Maven(和类似)标准布局
  9. 故障 排除

本答案的范围

请注意,此答案涉及加载资源(例如 FXML 文件、图像和样式表),这些资源是应用程序的一部分,并与之捆绑在一起。因此,例如,加载用户从运行应用程序的计算机上的文件系统中选择的图像将需要此处未介绍的不同技术。

资源在运行时加载

关于加载资源,首先要了解的是,它们当然是在运行时加载的。通常,在开发过程中,应用程序是从文件系统运行的:也就是说,运行它所需的类文件和资源是文件系统上的单个文件。但是,一旦构建了应用程序,它通常是从 jar 文件执行的。在这种情况下,FXML 文件、样式表和图像等资源不再是文件系统上的单个文件,而是 jar 文件中的条目。因此:

代码不能使用 、 或 URL 加载资源FileFileInputStreamfile:

JavaFX 使用 URL 加载资源

JavaFX 使用 URL 加载 FXML、图像和 CSS 样式表。

显式期望将对象传递给它(传递给方法、构造函数或方法)。FXMLLoaderjava.net.URLstaticFXMLLoader.load(...)FXMLLoadersetLocation()

和 期望 s 表示 URL。如果传递的 URL 没有方案,则相对于类路径对其进行解释。这些字符串可以通过调用 以可靠的方式从 创建。ImageScene.getStylesheets().add(...)StringURLtoExternalForm()URL

为资源创建正确 URL 的推荐机制是使用 ,这是在适当的实例上调用的。这样的类实例可以通过调用(给出当前对象的类)或 来获得。该方法采用表示资源名称的 。Class.getResource(...)ClassgetClass()ClassName.classClass.getResource(...)String

资源作为流

如果需要将资源作为流,则可以使用 或 为资源数据创建流。但是,对于 JavaFX 中的大多数工作,您不需要这样做,因为 JavaFX API 主要使用 URL 作为输入而不是流。有一些 API,例如 Image 构造函数和 FXMLLoader,它们可以同时处理 URL 和流。对于此类 API,通常最好使用基于 URL 的 API 表单,而不是基于流的 API 表单。这允许像 FXMLLoader 这样的进程找到所包含资源的相对 URL,如果只是提供原始 InputStream,则无法做到这一点。getClass().getResourceAsStream(...)SomeOtherClass.class.getResourceAsStream(...)

资源名称规则

  • 资源名称是以 - 分隔的路径名。每个组件都表示一个包或子包名称组件。/
  • 资源名称区分大小写。
  • 资源名称中的各个组件必须是有效的 Java 标识符

最后一点有一个重要的后果:

.并且不是有效的 Java 标识符,因此不能在资源名称中使用它们。..

当应用程序从文件系统运行时,这些实际上可能起作用,尽管这实际上更像是实现 的意外。当应用程序捆绑为 jar 文件时,它们将失败。getResource()

同样,如果运行在操作系统上,该操作系统不区分仅大小写不同的文件名,则在资源名称中使用错误的大小写可能会在从文件系统运行时起作用,但在从 jar 文件运行时会失败。

以前导开头的资源名称是绝对的:换句话说,它们是相对于类路径进行解释的。不带前导的资源名称是相对于调用的类进行解释的。//getResource()

对此略有不同,即使用 .提供给的路径不能以 a 开头,并且始终是绝对的,即它是相对于类路径的。还应该注意的是,在模块化应用程序中,在某些情况下,使用资源访问受强封装规则的约束,此外,必须无条件打开包含资源的包。有关详细信息,请参阅文档getClass().getClassLoader().getResource(...)ClassLoader.getResource(...)/ClassLoader.getResource()

创建资源 URLgetClass().getResource()

要创建资源 URL,请使用 。通常,表示当前对象的类,并使用 .但是,情况并非如此,如下一节所述。someClass.getResource(...)someClassgetClass()

  • 如果资源与当前类位于同一包中,或者位于该类的子包中,请使用资源的相对路径:

     // FXML file in the same package as the current class:
     URL fxmlURL = getClass().getResource("MyFile.fxml");
     Parent root = FXMLLoader.load(fxmlURL);
    
     // FXML file in a subpackage called `fxml`:
     URL fxmlURL2 = getClass().getResource("fxml/MyFile.fxml");
     Parent root2 = FXMLLoader.load(fxmlURL2);
    
     // Similarly for images:
     URL imageURL = getClass().getResource("myimages/image.png");
     Image image = new Image(imageURL.toExternalForm());
    
  • 如果资源位于不是当前类的子包中的包中,请使用绝对路径。例如,如果当前类在包中,并且我们需要加载包中的CSS文件,则必须使用绝对路径:org.jamesd.examples.viewstyle.cssorg.jamesd.examples.css

     URL cssURL = getClass().getResource("/org/jamesd/examples/css/style.css");
     scene.getStylesheets().add(cssURL.toExternalForm());
    

    在此示例中,值得再次强调的是,该路径不包含有效的 Java 资源名称,如果应用程序捆绑为 jar 文件,则该路径将不起作用"../css/style.css"

组织代码和资源

我建议将代码和资源组织到由它们关联的 UI 部分确定的包中。Eclipse 中的以下源代码布局给出了此组织的示例:

enter image description here

使用此结构,每个资源在同一个包中都有一个类,因此很容易为任何资源生成正确的 URL:

FXMLLoader editorLoader = new FXMLLoader(EditorController.class.getResource("Editor.fxml"));
Parent editor = editorLoader.load();
FXMLLoader sidebarLoader = new FXMLLoader(SidebarController.class.getResource("Sidebar.fxml"));
Parent sidebar = sidebarLoader.load();

ImageView logo = new ImageView();
logo.setImage(newImage(SidebarController.class.getResource("logo.png").toExternalForm()));

mainScene.getStylesheets().add(App.class.getResource("style.css").toExternalForm());

如果你有一个只有资源而没有类的包,例如,下面布局中的包images

enter image description here

您甚至可以考虑创建一个“标记接口”,仅用于查找资源名称:

package org.jamesd.examples.sample.images ;
public interface ImageLocation { }

现在,您可以轻松找到这些资源:

Image clubs = new Image(ImageLocation.class.getResource("clubs.png").toExternalForm());

从类的子包加载资源也相当简单。给定以下布局:

enter image description here

我们可以按如下方式加载类中的资源:App

package org.jamesd.examples.resourcedemo;
import java.net.URL;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class App extends Application {
    @Override
    public void start(Stage primaryStage) throws Exception {
                
        URL fxmlResource = getClass().getResource("fxml/MainView.fxml");
        
        Parent root = FXMLLoader.load(fxmlResource);
        Scene scene = new Scene(root);
        scene.getStylesheets().add(getClass().getResource("style/main-style.css").toExternalForm());
        primaryStage.setScene(scene);
        primaryStage.show();
    }
    
    public static void main(String[] args) {
        Application.launch(args);
    }
}

要加载不在从中加载它们的类的同一包或子包中的资源,您需要使用绝对路径:

    URL fxmlResource = getClass().getResource("/org/jamesd/examples/resourcedemo/fxml/MainView.fxml");

Maven(和类似)标准布局

根据 Maven 标准目录布局,Maven 和其他依赖项管理和构建工具建议使用源文件夹布局,其中资源与 Java 文件分开。上一个示例的 Maven 布局版本如下所示:

enter image description here

了解如何构建它来组装应用程序非常重要:

  • *.java文件夹中的文件被编译为类文件,这些类文件被部署到构建文件夹或 jar 文件中。src/main/java
  • 资源文件夹中的资源复制到 build 文件夹或 jar 文件中。src/main/resources

在此示例中,由于资源位于与定义源代码的包的子包相对应的文件夹中,因此生成的生成(默认情况下,在 Maven 中位于 )由单个结构组成。target/classes

请注意,这两个 和 都被视为生成中相应结构的根,因此只有它们的内容(而不是文件夹本身)是生成的一部分。换句话说,运行时没有可用的文件夹。构建结构显示在下面的“故障排除”部分中。src/main/javasrc/main/resourcesresources

请注意,在本例中,IDE (Eclipse) 显示源文件夹的方式与文件夹不同;在第一种情况下,它显示,但对于资源文件夹,它显示文件夹。确保您知道是在 IDE 中创建包(其名称为 -delimited)还是文件夹(其名称不得包含 ,或在 Java 标识符中无效的任何其他字符)。src/main/javasrc/main/resources..

如果您正在使用 Maven,并且为了便于维护,您宁愿将 .fxml 文件保留在引用它们的 .java 文件旁边(而不是严格遵守 Maven 标准目录布局),则可以这样做。只需告诉 Maven 将这些文件复制到输出目录中的同一文件夹,它将把从这些源文件生成的类文件放入该文件夹中,方法是在 pom.xml 文件中包含如下内容

    <build>
        ...
        <resource>
            <directory>src/main/java</directory>
            <includes>
                <include>**/*.fxml</include>
                <include>**/*.css</include>
            </includes>
        </resource>
        ...
    </build>

如果这样做,则可以使用一种方法,例如让您的类从包含自己的 .class 文件的目录中加载 .fxml 资源。FXMLLoader.load(getClass().getResource("MyFile.fxml"))

故障 排除

如果出现意想不到的错误,请首先检查以下内容:

  • 请确保没有为资源使用无效的名称。这包括在资源路径中使用 或。...
  • 确保在预期位置使用相对路径,在预期位置使用绝对路径。因为如果路径有前导,则路径是绝对的,否则是相对的。对于 ,路径始终是绝对的,并且不能以 .Class.getResource(...)/ClassLoader.getResource(...)/
  • 请记住,绝对路径是相对于类路径定义的。通常,类路径的根目录是 IDE 中所有源文件夹和资源文件夹的并集。

如果所有这些看起来都是正确的,但仍然看到错误,请检查生成或部署文件夹。此文件夹的确切位置因 IDE 和生成工具而异。如果您使用的是 Maven,则默认情况下为 .其他生成工具和 IDE 将部署到名为 、 、 或 的文件夹中。target/classesbinclassesbuildout

通常,IDE 不会显示生成文件夹,因此您可能需要使用系统文件资源管理器进行检查。

上面 Maven 示例的组合源代码和构建结构是

enter image description here

如果要生成 jar 文件,某些 IDE 可能允许您在树视图中展开 jar 文件以检查其内容。您还可以使用以下命令从命令行检查内容:jar tf file.jar

$ jar -tf resource-demo-0.0.1-SNAPSHOT.jar 
META-INF/
META-INF/MANIFEST.MF
org/
org/jamesd/
org/jamesd/examples/
org/jamesd/examples/resourcedemo/
org/jamesd/examples/resourcedemo/images/
org/jamesd/examples/resourcedemo/style/
org/jamesd/examples/resourcedemo/fxml/
org/jamesd/examples/resourcedemo/images/so-logo.png
org/jamesd/examples/resourcedemo/style/main-style.css
org/jamesd/examples/resourcedemo/Controller.class
org/jamesd/examples/resourcedemo/fxml/MainView.fxml
org/jamesd/examples/resourcedemo/App.class
module-info.class
META-INF/maven/
META-INF/maven/org.jamesd.examples/
META-INF/maven/org.jamesd.examples/resource-demo/
META-INF/maven/org.jamesd.examples/resource-demo/pom.xml
META-INF/maven/org.jamesd.examples/resource-demo/pom.properties
$ 

如果未部署资源,或者正在将资源部署到意外位置,请检查生成工具或 IDE 的配置。

示例图像加载故障排除代码

此代码故意比严格要求更详细,以便于为映像加载过程添加其他调试信息。它还使用 System.out 而不是记录器,以便于移植。

String resourcePathString = "/img/wumpus.png";
Image image = loadImage(resourcePathString);

// ...

private Image loadImage(String resourcePathString) {
    System.out.println("Attempting to load an image from the resourcePath: " + resourcePathString);
    URL resource = HelloApplication.class.getResource(resourcePathString);
    if (resource == null) {
        System.out.println("Resource does not exist: " + resourcePathString);

        return null;
    }

    String path = resource.toExternalForm();
    System.out.println("Image path: " + path);

    Image image = new Image(path);
    System.out.println("Image load error?  " + image.isError());
    System.out.println("Image load exception? " + image.getException());

    if (!image.isError()) {
        System.out.println("Successfully loaded an image from " + resourcePathString);
    }

    return image;
}

外部教程参考

关于资源位置的一个有用的外部教程是 Eden 编码的教程:

Eden 编码教程的好处是它很全面。除了涵盖有关从本问题中的 Java 代码中查找的信息外。Eden 教程涵盖的主题包括查找在 CSS 中编码为 url 的资源,或使用说明符或元素在 FXML 中引用资源(这些主题目前未在本答案中直接涉及)。@fx:include

评论

2赞 kleopatra 5/4/2020
值得一提的是大写/小写处理的潜在差异 - 这就是为什么我更喜欢小写资源名称的原因(尽管不是命名约定)
3赞 James_D 5/4/2020
@kleopatra 某些框架(例如afterburner.fx和FXWeaver)要求控制器类名与FXML名称匹配,这迫使FXML名称为大写。
0赞 Jaideep Shekhar 8/29/2020
很好。但是,请在fxml中添加有关url的部分(例如加载图像)。
1赞 Will Hartung 2/13/2021
关于如何使用 FXML 处理 @ 的注释会有所帮助。即 <URL value=“@/styles/root.css” />
1赞 kleopatra 8/25/2021
您可以考虑对 ClassLoader API 的查找进行一些更改:也许可以强调它不能有前导斜杠(正如 Jewelsea 在您的其他答案中的评论中指出的那样 stackoverflow.com/a/68913233/203657)