在JavaFX 8中,我能否使用字符串提供样式表?

12

是否可以将整个样式表包装在字符串中并将其应用于特定节点? 使用案例是为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; }");

@jewelsea 的确,我在9月份也写了一个新答案。https://dev59.com/kWAf5IYBdhLWcg3wZB6u#69234685 - Nand
JavaFX 17+现在支持从数据URI中加载样式表,这是首选的方法,正如Nand的回答所示。 - jewelsea
6个回答

12

我通过定义新的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;
    }
}

显然,“internal”协议可以是任何(不冲突的)规范字符串,并且(在这个简单的例子中)文件路径完全被忽略。

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

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

此方法的功劳归功于Jasper Potts(请参见此示例


1
如果您正在编写框架级别的代码,并且不想使用唯一可能的覆盖url流处理程序工厂的方法,请参阅下面我的答案,以获取一种更轻量级的方法,它不会覆盖静态工厂。如果在尝试设置流工厂时出现异常,则表示您已经有其他代码执行了此操作,您需要使用我在URL类源代码中发现的内部“服务加载器”语义。这使您可以注册处理程序而无需创建流工厂。 - Ajax
2
我猜原帖的作者可能不会改变他们选择的答案,以使用我在下面留下的解决方案。但是对于任何编写库或框架的人来说,你真的,真的,真的不应该覆盖默认的URL流处理程序;相反,你应该使用URL类本身使用的服务提供程序框架。阅读URL的文档,你会发现自己不需要(也不应该)覆盖默认的流处理程序。 - Ajax
Java 9+提供了一个更加规范的解决方案,即可子类化的URLStreamHandlerProvider,它实现了URLStreamHandlerFactory(因此上述代码几乎可以原封不动地使用); 然后可以注册此类而不会出现“每个进程只有一次调用”的问题,也可以覆盖/替换默认处理程序http、jar等:https://docs.oracle.com/javase/9/docs/api/java/net/spi/URLStreamHandlerProvider.html - CodeClown42

6
这是一个基于ZioBytre答案(+1非常好)的CSS更新器类。
这是一个自包含的类,可以轻松地复制到项目中并直接使用。
它依赖于commons IO IOUtils类,以返回基于StringStream。但如果需要,这可以很容易地内联或替换为另一个库。
我在一个项目中使用这个类,在应用程序内部动态编辑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 : https://dev59.com/kWAf5IYBdhLWcg3wZB6u
 */
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的工作 - 那是不完整的。 - WestCoastProjects
大家好。看看我的答案,这是在生产中使用的东西,而不覆盖默认的 URL 流处理程序(这可能会破坏其他依赖关系)。 - Ajax
你的例子中的 IOUtils 是什么? - Eugene Kartoyev
@EugeneKartoyev 这是来自Apache commons的一个实用类:https://commons.apache.org/proper/commons-io/apidocs/org/apache/commons/io/IOUtils.html - Pierre Henry

4
对于写框架级代码的人来说,如果不想使用全局静态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”,() -&gt;“* {-fx-css:blah}”);,并且您可以通过“css:my/path”在任何地方使用此样式表。
请注意,我只查看URL的路径部分; 我打算利用查询参数进一步增加动态性(通过使用接受参数映射的css工厂),但这超出了本问题的范围。

3
自JavaFX 17起,现在可以使用数据URI。例如:
scene.getStylesheets().add("data:text/css;base64," + Base64.getEncoder().encodeToString("* { -fx-color: red; }".getBytes(StandardCharsets.UTF_8)));

在JavaFX 17中,它将会直接工作。


2
我查看了文档,没有找到内置的方法来实现这个。getStylesheetsParent 中唯一与样式表相关的方法,它只接受“链接到样式表的字符串 URL”,而不是样式表本身。它返回一个通用的 ObservableList,因此其返回值没有针对不同类型的特殊方法;只有一个通用的 add。这与 getResource 返回 URL 一致,而 toExternalForm() 只是返回该 URL 对象的字符串版本。
然而,有一件事情你可以尝试:使用data URI。不要传递一个生成的URI到样式表文件中,而是传递一个数据URI,其内容为该样式表。我不知道API是否会接受这种类型的URI,因为在getStylesheets的文档中链接的CSS参考指南中说:

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

首先尝试一个非常简单的数据URI以查看它是否有效。您可以使用此在线工具生成一个数据URI。如果Java接受数据URI,则只需要将包含CSS的字符串包装在一些方法调用中,将字符串转换为数据URI,类似于以下内容:

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

DataURI 是假设的。如果JavaFX接受手动生成的数据URI,则您将不得不自己找到提供该DataURI类的库;我相信这样的库在某个地方存在。

还有一种方法可以将内联CSS指定为String,以用于特定Node,这几乎是您要寻找的内容。 它在CSS参考指南中提到:

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

然而,听起来它不支持CSS中的选择器,只支持规则 - 因此,与其说.red { color: red; },你只能写color: red;,它将应用于该Node的所有子元素。这听起来不是你想要的。因此,数据URI是你唯一的希望。


“EDIT: 虽然这是一个聪明的想法(我之前不知道数据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=警告):
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的支持,这些URL映射到任何您想要的内容,而无需覆盖默认的流工厂。 - Ajax

0
关于实际问题("Java FX Stylesheet via String"):新的JavaFX 17 CSS数据URL协议是正确的选择。
但是关于@Ajax关于通过Handler注册URL协议的建议,这里有一个完整的实际示例,展示了如何使用建议的方法注册多个协议。
这段代码是在最古老的JDK之一上编写和测试的:v1.2(1998)。因此,其中的一些部分比现代JDK更冗长,但它也已经在JDK 17和1.8(也称为"8")上进行了测试。
在这个示例中,通过一个实用类注册了两个URL协议。
所以,总共有3个包和5个类。 首先,是实用类(包括一个测试)...
package com.stackoverflow.handler.util;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;

public class HandlerUtil {

    private HandlerUtil() {/* Please do not instantiate. */}

    public synchronized static String register(final Class clazz) {

        final String className     = clazz.getName();
        /*
         * There must be at least one '.' in the Package Name.
         */
        final int    dotLast       = className.lastIndexOf('.');
        final int    dotLastButOne = className.lastIndexOf('.', dotLast - 1);

        System.out.println("Class Name............................: " + className);
        System.out.println("dotLast...............................: " + dotLast);
        System.out.println("dotLastButOne.........................: " + dotLastButOne);
        /*
         * Split the Package Name in two.:
         * - the part BEFORE the final '.' ("SuperPackage" Key for the Protocol Handler)
         * - the part AFTER  the final '.' (the Protocol Name, which MUST be unique)
         *   (Unique, because otherwise new URL("protocol:...") may find the Protocol in another SuperPackage)
         */
        final String ourSuperPackage;
        final String protocolName;
        try {
            ourSuperPackage = className.substring(0, dotLastButOne);
            protocolName    = className.substring(   dotLastButOne + 1, dotLast);
        }
        catch (final StringIndexOutOfBoundsException e) {
            throw new IllegalArgumentException("Full Classname must contain at least 2 dots. Class=" + className);
        }

        System.out.println("PROTOCOL_NAME.........................: " + protocolName);
        /*
         * Get the List of registered Handler "SuperPackages"...
         */
        final String handlerProperty    = "java.protocol.handler.pkgs";
        final char   sepBar             = '|';

        final String handlerPackageList = System.getProperty(handlerProperty, "").trim();
        /*
         * Precede & append '|' to both List & New Entry to facilitate Pre-Existence check...
         */
        final String sepListSep         = sepBar + handlerPackageList + sepBar;
        final String sepNewSep          = sepBar + ourSuperPackage    + sepBar;
        /*
         * Only append our "SuperPackage" Name to the List if it's not already present...
         */
        if (sepListSep.indexOf(sepNewSep) < 0) {

            final String updatedList;

            if (handlerPackageList.length() == 0) {
                updatedList =                                   ourSuperPackage;
            } else {
                updatedList = handlerPackageList  +  sepBar  +  ourSuperPackage;
            }
            System.out.println("Updated handlerPackageList............: " + updatedList);
            System.setProperty(handlerProperty,                             updatedList);
        }
        return protocolName;
    }

    public  static void main(final String[] args) throws MalformedURLException, IOException {

        testProtocol(com.stackoverflow.handler.reverse.Handler.PROTOCOL_NAME, "path/reverseme");
        testProtocol(com.stackoverflow.handler.dubble .Handler.PROTOCOL_NAME, "path/doubleme");
        testProtocol("unregisteredProtocol",                                  "path/unregistered");
    }

    private static void testProtocol(final String protocol, final String path) throws MalformedURLException, IOException {
        
        final URLConnection urlConnection  = new URL(protocol + ':' + path).openConnection();
        ;                   urlConnection.connect();

        System.out.println("[main] urlConnection..................: " + urlConnection.getClass());
        System.out.println("[main] urlInputStream.................: " + new String(readBytes(urlConnection)));
    }

    private static byte[] readBytes(final URLConnection urlConnection) throws IOException {

        final byte[] bytes     = new byte[8192];
        final int    byteCount = urlConnection.getInputStream(/* ByteArrayInputStream for our test Protocols */).read(bytes);
        final byte[] bytesCopy = new byte[byteCount];

        System.arraycopy(bytes, 0, bytesCopy, 0, byteCount);

        return bytesCopy;
    }
}

现在,为第一个协议(URLConnection和Handler)创建两个类。
package com.stackoverflow.handler.dubble;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;

public final class DoubleURLConnection extends URLConnection  {

    protected DoubleURLConnection(final URL url) {
        super(url);
        System.out.println("DublURLConnection.new.................: " + url);
    }

    public void connect() throws IOException {
        System.out.println("DublURLConnection.connect.............:");
    }
    public InputStream getInputStream() throws IOException {
        System.out.println("DublURLConnection.getInputStream......:");

        final String urlString = this.getURL().toString();

        return new ByteArrayInputStream(("DUBL_" + urlString + '_' + urlString + "_DUBL").getBytes());
    }
}

处理程序...
package com.stackoverflow.handler.dubble;

import java.io.IOException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;

import com.stackoverflow.handler.util.HandlerUtil;

/**
 * This Class MUST be called "Handler".
 */
public final class Handler extends URLStreamHandler {

    /**
     * Explicitly register the Protocol.<br>
     * This is not necessary if you reference the Protocol Name via
     * {@link  #PROTOCOL_NAME}.
     */
    public  static void register() {
    }
    /**
     * The Protocol Name for this Handler.<br>
     * (which, when referenced, will implicitly register the Protocol)
     */
    public  static final String PROTOCOL_NAME = HandlerUtil.register(DoubleURLConnection.class);

    protected URLConnection openConnection(final URL url) throws IOException {
        System.out.println("DUBL Handler.openConnection.(URL).....: " + url);
        System.out.println("DUBL Handler.openConnection.(Protocol): " + url.getProtocol());
//      System.out.println("DUBL Handler.openConnection.(Path)....: " + url.getPath());

        return new DoubleURLConnection(url);
    }
}

最后,为第二个协议(URLConnection和Handler)创建了两个类。
package com.stackoverflow.handler.reverse;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;

public final class ReverseURLConnection extends URLConnection  {

    protected ReverseURLConnection(final URL url) {
        super(url);
        System.out.println("RvrsURLConnection.new.................: " + url);
    }

    public void connect() throws IOException {
        System.out.println("RvrsURLConnection.connect.............:");
    }
    public InputStream getInputStream() throws IOException {
        System.out.println("RvrsURLConnection.getInputStream......:");

        final      StringBuffer sbd      = new StringBuffer();
        final      char[]       urlChars = this.getURL().toString().toCharArray();

        for (int i=0; i < urlChars.length ; i++) {
            sbd.insert(0, urlChars[i]);
        }
        return new ByteArrayInputStream(sbd.toString().getBytes());
    }
}

处理程序...
package com.stackoverflow.handler.reverse;

import java.io.IOException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;

import com.stackoverflow.handler.util.HandlerUtil;

/**
 * This Class MUST be called "Handler".
 */
public final class Handler extends URLStreamHandler {

    /**
     * Explicitly register the Protocol.<br>
     * This is not necessary if you reference the Protocol Name via
     * {@link  #PROTOCOL_NAME}.
     */
    public  static void register() {
    }
    /**
     * The Protocol Name for this Handler.<br>
     * (which, when referenced, will implicitly register the Protocol)
     */
    public  static final String PROTOCOL_NAME = HandlerUtil.register(ReverseURLConnection.class);

    protected URLConnection openConnection(final URL url) throws IOException {
        System.out.println("Handler.openConnection.(URL)..........: " + url);
        System.out.println("Handler.openConnection.(Protocol).....: " + url.getProtocol());
//      System.out.println("Handler.openConnection.(Path).........: " + url.getPath());

        return new ReverseURLConnection(url);
    }
}

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