【问题标题】:In JavaFX 8 can I provide a stylesheet from a String?在 JavaFX 8 中,我可以提供来自字符串的样式表吗?
【发布时间】:2014-09-02 11:30:34
【问题描述】:

是否可以将整个样式表包装在一个字符串中并将其应用于某个节点? 用例是为 PseudoClass 添加特定的(不可更改的)行为。 我知道我可以使用pane.getStylesheets().add(getClass().getResource("mycss.css").toExternalForm());,但我想知道是否有某种方法可以将它直接嵌入到源代码中;类似的东西:

pane.getStylesheets().add(
    ".button:ok { -fx-background-color: green; }\n"+
    ".button:ko { -fx-background-color: red; }");

【问题讨论】:

标签: css resources stylesheet embedded-resource javafx-8


【解决方案1】:

我通过定义一个新的 URL 连接找到了一种方法:

private String css;

public void initialize() {
    ...
    // to be done only once.
    URL.setURLStreamHandlerFactory(new StringURLStreamHandlerFactory());
    ...
}

private void updateCss(Node node) {
    // can be done multiple times.
    css = createCSS();
    node.getStylesheets().setAll("internal:"+System.nanoTime()+"stylesheet.css");
}

private class StringURLConnection extends URLConnection {
    public StringURLConnection(URL url){
        super(url);
    }

    @Override public void connect() throws IOException {}

    @Override public InputStream getInputStream() throws IOException {
        return new StringBufferInputStream(css);
    }
}

private class StringURLStreamHandlerFactory implements URLStreamHandlerFactory {
    URLStreamHandler streamHandler = new URLStreamHandler(){
        @Override protected URLConnection openConnection(URL url) throws IOException {
            if (url.toString().toLowerCase().endsWith(".css")) {
                return new StringURLConnection(url);
            }
            throw new FileNotFoundException();
        }
    };
    @Override public URLStreamHandler createURLStreamHandler(String protocol) {
        if ("internal".equals(protocol)) {
            return streamHandler;
        }
        return null;
    }
}

显然协议“内部”可以是任何(非冲突)格式良好的字符串,并且(在这个简单的示例中)文件路径被完全忽略。

我用它来设置全局 .css,所以我不需要记住多个字符串。 似乎 Stream 只打开了一次,但我不知道这是否适用于所有情况。

根据需要随意使代码复杂化;)

此方法的功劳归于 Jasper Potts (see this example)

【讨论】:

  • 如果您正在编写框架级代码并且不想用完唯一可能的 url 流处理程序工厂覆盖,请参阅下面我的答案以获取不会覆盖静态的更轻量级的方法工厂。如果您在尝试设置流工厂时遇到异常,那么您已经完成了其他一些代码,您需要使用我在 URL 类的源代码中找到的内部“服务加载器”语义。这使您无需创建流工厂即可注册处理程序。
  • 我想选择自己答案的原始发布者可能不会将其更改为我在下面留下的解决方案,但对于任何正在编写库或框架的人来说,你真的,真的,真的不应该' t 覆盖默认的 url 流处理程序;相反,您应该使用 URL 类本身使用的服务提供者框架。阅读 URL 的文档,你会发现你不必(也不应该)覆盖默认的流处理程序。
  • Java 9+ 以可实现URLStreamHandlerFactory 的子类URLStreamHandlerProvider 的形式为这个问题提供了更规范的解决方案(因此上面的代码几乎可以按原样使用);然后可以注册此类,而不会出现“每个进程只调用一次”问题,可能会覆盖/替换 http、jar 等的默认处理程序:docs.oracle.com/javase/9/docs/api/java/net/spi/…
【解决方案2】:

这是我基于 ZioBytre 回答的 CSS 更新程序类(+1 效果很好)。

这是一个自包含的类,可以很容易地复制到项目中并按原样使用。

它依赖于公共 IO IOUtils 类以基于 String 返回 Stream。但如果需要,这可以很容易地被内联或替换为另一个库。

我在一个项目中使用这个类,其中 CSS 可以在应用程序内部、服务器端动态编辑,并推送到 JavaFX 客户端。它可以用于 CSS 字符串不是来自文件或 URL 而是来自另一个来源(服务器应用程序、数据库、用户输入...)的任何场景

它有一个绑定字符串属性的方法,以便 CSS 更改一旦发生就会自动应用。

/**
 * Class that handles the update of the CSS on the scene or any parent.
 *
 * Since in JavaFX, stylesheets can only be loaded from files or URLs, it implements a handler to create a magic "internal:stylesheet.css" url for our css string
 * see : https://github.com/fxexperience/code/blob/master/FXExperienceTools/src/com/fxexperience/tools/caspianstyler/CaspianStylerMainFrame.java
 * and : http://stackoverflow.com/questions/24704515/in-javafx-8-can-i-provide-a-stylesheet-from-a-string
 */
public class FXCSSUpdater {

    // URL Handler to create magic "internal:stylesheet.css" url for our css string
    {
        URL.setURLStreamHandlerFactory(new StringURLStreamHandlerFactory());
    }

    private String css;

    private Scene scene;

    public FXCSSUpdater(Scene scene) {
        this.scene = scene;
    }

    public void bindCss(StringProperty cssProperty){
        cssProperty.addListener(e -> {
            this.css = cssProperty.get();
            Platform.runLater(()->{
                scene.getStylesheets().clear();
                scene.getStylesheets().add("internal:stylesheet.css");
            });
        });
    }

    public void applyCssToParent(Parent parent){
        parent.getStylesheets().clear();
        scene.getStylesheets().add("internal:stylesheet.css");
    }

    /**
     * URLConnection implementation that returns the css string property, as a stream, in the getInputStream method.
     */
    private class StringURLConnection extends URLConnection {
        public StringURLConnection(URL url){
            super(url);
        }

        @Override
        public void connect() throws IOException {}

        @Override public InputStream getInputStream() throws IOException {
            return IOUtils.toInputStream(css);
        }
    }

    /**
     * URL Handler to create magic "internal:stylesheet.css" url for our css string
     */
    private class StringURLStreamHandlerFactory implements URLStreamHandlerFactory {

        URLStreamHandler streamHandler = new URLStreamHandler(){
            @Override
            protected URLConnection openConnection(URL url) throws IOException {
                if (url.toString().toLowerCase().endsWith(".css")) {
                    return new StringURLConnection(url);
                }
                throw new FileNotFoundException();
            }
        };

        @Override
        public URLStreamHandler createURLStreamHandler(String protocol) {
            if ("internal".equals(protocol)) {
                return streamHandler;
            }
            return null;
        }
    }
}

用法:

StringProperty cssProp = new SimpleStringProperty(".root {-fx-background-color : red}");
FXCSSUpdater updater = new FXCSSUpdater(scene);
updater.bindCss(cssProp);

//new style will be applied to the scene automatically
cssProp.set(".root {-fx-background-color : green}");

//manually apply css to another node
cssUpdater.applyCssToParent(((Parent)popover.getSkin().getNode()));

【讨论】:

  • 感谢我放弃了 ZioByte 的工作 - 这是不完整的。
  • 大家好。查看下面我在生产中使用的答案,而不覆盖默认的 URL 流处理程序(这可能会破坏其他依赖项)。
  • 您的示例中的 IOUtils 是什么?
  • @EugeneKartoyev 这是一个来自 Apache commons 的 util 类:commons.apache.org/proper/commons-io/apidocs/org/apache/commons/…
【解决方案3】:

对于编写框架级代码且不想用完一个且仅覆盖全局静态 url 流工厂的任何人,您可以改为在 URL 类本身中绑定到内部“服务加载器”框架。

为此,您必须创建一个名为Handler extends URLStreamHandler 的类并更新系统属性java.protocol.handler.pkgs 以指向该类的包,减去最终的包后缀。因此,com.fu.css 会将属性设置为 com.fu,然后所有 css:my/path 请求将路由到此处理程序。

我将在下面粘贴我正在使用的类;原谅奇怪的集合和供应商界面;您可以猜出它们的作用并用标准实用程序替换它们而不会遇到太多麻烦。

package xapi.jre.ui.css;

import xapi.collect.X_Collect;
import xapi.collect.api.CollectionOptions;
import xapi.collect.api.StringTo;
import xapi.fu.Out1;
import xapi.io.X_IO;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.nio.charset.Charset;

/**
 * I abhor the name of this class,
 * but it must be called "Handler" in order for java.net.URL to be able to find us.
 *
 * It sucks, but it's not our api, and it's the only way to get dynamic stylesheets in JavaFx,
 * short of overriding the url stream handler directly (and this can only be done once in a single
 * JVM, and as framework-level code, it is unacceptable to prevent clients from choosing to
 * override the stream handler themselves).
 *
 * Created by James X. Nelson (james @wetheinter.net) on 8/21/16.
 */
public class Handler extends URLStreamHandler {

    private static final StringTo<Out1<String>> dynamicFiles;
    static {
        // Ensure that we are registered as a url protocol handler for css:/path css files.
        String was = System.getProperty("java.protocol.handler.pkgs", "");
        System.setProperty("java.protocol.handler.pkgs", Handler.class.getPackage().getName().replace(".css", "") +
            (was.isEmpty() ? "" : "|" + was ));
        dynamicFiles = X_Collect.newStringMap(Out1.class,
            CollectionOptions.asConcurrent(true)
                .mutable(true)
                .insertionOrdered(false)
            .build());
    }

    public static void registerStyleSheet(String path, Out1<String> contents) {
        dynamicFiles.put(path, contents);
    }

    @Override
    protected URLConnection openConnection(URL u) throws IOException {
        final String path = u.getPath();
        final Out1<String> file = dynamicFiles.get(path);
        return new StringURLConnection(u, file);
    }

    private static class StringURLConnection extends URLConnection {
        private final Out1<String> contents;

        public StringURLConnection(URL url, Out1<String> contents){
            super(url);
            this.contents = contents;
        }

        @Override
        public void connect() throws IOException {}

        @Override public InputStream getInputStream() throws IOException {
            return X_IO.toStream(contents.out1(), Charset.defaultCharset().name());
        }
    }
}

现在,任何代码都可以调用 Handler.registerStylesheet("my/path", ()->"* { -fx-css: blah }");,你可以在任何地方通过 "css:my/小路”。

请注意,我只查看 url 的路径部分;我打算利用查询参数来进一步增加动态性(通过使用接受参数映射的 css 工厂),但这超出了这个问题的范围。

【讨论】:

    【解决方案4】:

    我查看了文档,但没有看到执行此操作的内置方法。 getStylesheetsParent 中唯一与样式表相关的方法,它只接受“链接到样式表的字符串 URL”,而不接受样式表本身。它返回一个泛型的ObservableList,所以它的返回值没有针对不同类型的特殊方法;只有一个通用的add。这与 getResource 返回 URLtoExternalForm() 仅返回该 URL 对象的 String 版本是一致的。

    但是,您可以尝试一件事:data URI。不是将生成的 URI 传递给样式表文件,而是传递一个数据 URI,其内容是该样式表。不过,我不知道 API 是否会接受这种 URI,因为 getStylesheets 的文档中链接的 CSS Reference Guide

    样式表 URL 可以是绝对 URL 或相对 URL。

    首先尝试一个非常简单的数据 URI,看看它是否有效。您可以使用this online tool 生成一个。如果 Java 确实接受数据 URI,那么您只需要使用一些将字符串转换为数据 URI 的方法调用来包装包含 CSS 的字符串,如下所示:

    pane.getStylesheets().add(new DataURI(
        ".button:ok { -fx-background-color: green; }\n"+
        ".button:ko { -fx-background-color: red; }").toString());
    

    DataURI 类是假设的。如果 JavaFX 接受手动生成的数据 URI,那么您必须自己找到一个提供该 DataURI 类的库;我确定某个地方存在。

    还有一种方法可以将某个Node 的内联CSS 指定为String,这几乎就是您要找的。在CSS Reference Guide中提到:

    CSS 样式可以来自样式表或内联样式。样式表从 Scene 对象的 stylesheets 变量中指定的 URL 加载。如果场景图包含控件,则加载默认的用户代理样式表。内联样式通过 Node setStyle API 指定。内联样式类似于 HTML 元素的 style="…" 属性。

    但是,听起来它不支持 CSS 中的选择器,只支持规则 - 所以不要说 .red { color: red; },你只能写 color: red;,它适用于 @987654350 的所有子代@。这听起来不像你想要的。所以数据 URI 是你唯一的希望。


    编辑:虽然这是一个聪明的想法(我以前不知道数据 URI),但它不起作用。我有同样的要求,所以我尝试了。它不会引发异常,但日志中有警告并且未应用样式:

    我用过这种风格:

    .root{
        -fx-font-family: "Muli";
        -fx-font-weight: lighter;
        -fx-font-size: 35pt;
        -fx-padding: 0;
        -fx-spacing: 0;
    }
    

    并使用提供的工具生成以下数据 URI:

    data:text/css;charset=utf-8,.root%7B%0D%0A%20%20%20%20-fx-font-family%3A%20%22Muli%22%3B%0D%0A%20%20%20%20-fx-font-weight%3A%20lighter%3B%0D%0A%20%20%20%20-fx-font-size%3A%2035pt%3B%0D%0A%20%20%20%20-fx-padding%3A%200%3B%0D%0A%20%20%20%20-fx-spacing%3A%200%3B%0D%0A%7D
    

    将它应用到我的场景中:

        scene.getStylesheets().add("data:text/css;charset=utf-8,.root%7B%0D%0A%20%20%20%20-fx-font-family%3A%20%22Muli%22%3B%0D%0A%20%20%20%20-fx-font-weight%3A%20lighter%3B%0D%0A%20%20%20%20-fx-font-size%3A%2035pt%3B%0D%0A%20%20%20%20-fx-padding%3A%200%3B%0D%0A%20%20%20%20-fx-spacing%3A%200%3B%0D%0A%7D");
    

    结果(请原谅我的法语,AVERTISSEMENT=WARNING):

    janv. 07, 2015 12:02:03 PM com.sun.javafx.css.StyleManager loadStylesheetUnPrivileged
    AVERTISSEMENT: Resource "data:text/css;charset=utf-8,%23header%7B%0D%0A%20%20%20%20-fx-background-color%3A%23002D27%3B%0D%0A%20%20%20%20-fx-font-size%3A%2035pt%3B%0D%0A%20%20%20%20-fx-text-fill%3A%20%23fff%3B%0D%0A%7D" not found.
    

    很遗憾,JavaFX 似乎不知道数据 URI。

    【讨论】:

    • 大家好。我在下面的回答利用了 url 流处理程序服务提供者来添加对映射到...的css:/blah url 的支持,而无需覆盖默认流工厂。
    【解决方案5】:

    从 JavaFX 17 开始,现在可以使用数据 URI。

    例如,

    scene.getStylesheets().add("data:text/css;base64," + Base64.getEncoder().encodeToString("* { -fx-color: red; }".getBytes(StandardCharsets.UTF_8)));
    

    将简单地在 JavaFX 17 中工作。

    【讨论】:

      猜你喜欢
      • 2013-09-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-02-27
      • 1970-01-01
      • 1970-01-01
      • 2019-07-31
      • 1970-01-01
      相关资源
      最近更新 更多