JavaFXのWebViewで読み込んだHTMLからローカルのファイルを参照する

パッケージ製品開発担当の大です。こんにちは。

JavaFXには、WebKitをベースにしたブラウザコントロール(WebView)が用意されており、簡単にウェブブラウザの機能を組み込むことができます。今回は、このブラウザコントロールで読み込んだHTMLから、ローカルのファイルを参照する方法を紹介します。

ネタ元はこのへんです: Setting html content with setContent() does never show images | Oracle Forums

基本の動作

HTML中のimg要素のsrc属性に指定された画像ファイルがどのように見えるのか確認してみます。

こんなHTMLを用意しました: javafxtest.html

<!DOCTYPE html>
<html>
  <head>
    <title>WebViewのテスト</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  </head>
  <body>
    <table>
      <tbody>
        <tr>
          <th>相対パス指定</th>
          <th>Web上の画像を指定</th>
          <th>ローカルフォルダの画像を指定</th>
          <th>クラスパス上の画像を指定</th>
        </tr>
        <tr>
          <td><img id="img1" src="javafxtest.png" width="252" height="98"></td>
          <td><img id="img2" src="http://www.hos.co.jp/wp-content/uploads/2013/08/javafxtest.png" width="252" height="98"></td>
          <td><img id="img3" src="file:/c:///Temp/javafxtest.png" width="252" height="98"></td>
          <td><img id="img4" src="jar:file:/c:/Temp/WebViewTest.jar!/webviewtest/javafxtest.png" width="252" height="98"></td>
        </tr>
      </tbody>
    </table>
  </body>
</html>

このHTMLファイルをローカルのフォルダ(c:\Temp)に置いて、WebViewを使用して表示してみます: WebViewTest.java

public class WebViewTest extends Application {
    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage stage) throws Exception {
        WebView webView = new WebView();
        stage.setScene(new Scene(webView, 1040, 160));
        WebEngine engine = webView.getEngine();
        stage.titleProperty().bind(engine.titleProperty());
        stage.show();

        // (1)ローカルフォルダのHTMLを指定
        String url = "file:///c:/Temp/javafxtest.html";
        engine.load(url);
    }
}
ローカルフォルダのHTML

ローカルフォルダのHTML(クリックで拡大)

問題なく全部表示されています。HTMLをローカルに置いているので、相対パス指定でもローカルの画像が表示されています。

つづいてこのHTMLをクラスパス上(WebViewTestクラスと同じ場所)に置いて表示してみます。先ほどのソースのハイライト部分を下記のように変更します。

        // (2)クラスパス上のHTMLを指定
        String url = getClass().getResource("javafxtest.html").toExternalForm();
        engine.load(url);
クラスパス上のHTML

クラスパス上のHTML(クリックで拡大)

これだとローカルフォルダに置いた画像が表示されません。では、HTMLがWeb上にあったらどうでしょうか。

        // (3)Web上のHTMLを指定
        String url = "http://www.hos.co.jp/wp-content/uploads/2013/08/javafxtest.html";
        engine.load(url);
Web上のHTML

Web上のHTML(クリックで拡大)

これも、ローカルフォルダに置いた画像が表示されませんね。

要するに、ローカルフォルダの画像を表示するなら、HTML(や関連するJS、CSSファイル)もローカルフォルダに置くのが基本となります。

HTMLを文字列としてロードする

HTMLを文字列としてロードすれば、ローカルフォルダに置いた画像も表示されます。(JavaFX 2.0ではバグがあったみたいですが、現行の2.2では問題なく動作します)

        // (4)loadContentを使用してHTMLを文字列として指定
        String content = new Scanner(getClass().getResourceAsStream("javafxtest.html"), "utf-8").useDelimiter("\\A").next();
        engine.loadContent(content);
HTMLを文字列として指定

HTMLを文字列として指定(クリックで拡大)

ここでは例として、クラスパス上のHTMLファイルを文字列として読み込んでからやっていますが、Web上にある場合でも同様のことは可能ですね。

ただ、文字列としてロードしたためにベースURLがなく、相対パスが使用できません。これについては、 loadContent(String content, URL codeBase) のようにベースURLを指定してロードできるようなメソッドの追加が提案されていますが、現バージョン(2.2)ではまだありません。相対パスが必要な場合はHTML中で<base>要素で指定するなどの対策が必要です。

独自のURLStreamHandlerを指定する

ちょっと面倒くさいですが、fileプロトコルのURLを独自のプロトコルにいったん置き換えておいて、独自のURLStreamHandlerで処理すれば、相対パスも動作する状態でローカルフォルダのファイルも参照できるようになります。

CustomURLStreamHandler.java

public class CustomURLStreamHandler extends URLStreamHandler {

    private static final String PROTOCOL = "tekito";

    public static class Factory implements URLStreamHandlerFactory {

        @Override
        public URLStreamHandler createURLStreamHandler(String protocol) {
            return PROTOCOL.equals(protocol) ? new CustomURLStreamHandler() : null;
        }
    }

    public static String removeCustomProtocol(String url) {
        return url.substring(PROTOCOL.length() + 1);
    }

    public static String addCustomProtocol(String url) {
        return PROTOCOL + ":" + url;
    }

    @Override
    protected URLConnection openConnection(URL u) throws IOException {
        String s = u.toExternalForm();
        final URL url = new URL(removeCustomProtocol(s));
        return new URLConnection(url) {
            @Override
            public void connect() throws IOException {
            }

            @Override
            public InputStream getInputStream() throws IOException {
                return url.openStream();
            }
        };
    }
}

WebViewTest.java

public class WebViewTest extends Application {

    static {
        URL.setURLStreamHandlerFactory(new CustomURLStreamHandler.Factory());
    }

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

    @Override
    public void start(Stage stage) throws Exception {
        WebView webView = new WebView();
        stage.setScene(new Scene(webView, 1040, 160));
        final WebEngine engine = webView.getEngine();
        stage.titleProperty().bind(engine.titleProperty());
        stage.show();

        // HTMLの読み込みが終わった時点で処理を行います
        engine.getLoadWorker().stateProperty().addListener(new ChangeListener<State>() {
            @Override
            public void changed(ObservableValue<? extends State> ov, State s0, State s1) {
                if (State.SUCCEEDED.equals(s1)) {
                    Element img = engine.getDocument().getElementById("img3");
                    img.setAttribute("src", CustomURLStreamHandler.addCustomProtocol(img.getAttribute("src")));
                }
            }
        });

        // (1)ローカルフォルダのHTMLを指定
        String url = "file:///c:/Temp/javafxtest.html";
        engine.load(url);
    }
}

ローカルフォルダのHTMLを表示した場合:

ローカルフォルダのHTML

ローカルフォルダのHTML(クリックで拡大)

クラスパス上のHTMLを表示した場合:

クラスパス上のHTML

クラスパス上のHTML(クリックで拡大)

Web上のHTMLを表示した場合:

Web上のHTML

Web上のHTML(クリックで拡大)

この方法を使用した場合、注意しなくてはならないのは、URL.setURLStreamHandlerFactoryは一度しか使用できないということです。つまり、エンドユーザ向けのアプリケーションとして使用するような場合なら使用できても、コントロールやライブラリなどを配布するような場合には不向きだということです。

まとめ

ということで、HTMLからローカルのファイルを参照したい場合は、

  • HTMLもローカルに置く
  • HTMLを文字列としてロードする
  • 独自のURLStreamHandlerを指定する

のどれかを状況に合わせて選べば良いと思います。

おまけ:WebViewでのデバッグ

JavaFXのWebViewでHTMLの表示が思うようにいかない場合は、Firebug Liteで調べてみると良いです。
修正したWebViewTest.javaで、HTMLの読み込みが終わった時点で以下の処理を追加してみましょう。

engine.executeScript("(function(F,i,r,e,b,u,g,L,I,T,E){if(F.getElementById(b))return;E=F[i+'NS']&&F.documentElement.namespaceURI;E=E?F[i+'NS'](E,'script'):F[i]('script');E[r]('id',b);E[r]('src',I+g+T);E[r](b,u);(F[e]('head')[0]||F[e]('body')[0]).appendChild(E);E=new Image;E[r]('src',I+L);})(document,'createElement','setAttribute','getElementsByTagName','FirebugLite','4','firebug-lite.js','releases/lite/latest/skin/xp/sprite.png','https://getfirebug.com/','#startOpened');");

実行しているスクリプトは、Firebug Liteのサイトで用意されているブックマークレットをURLデコードしただけのものです。

これで、Firebug LiteがWebViewの中で動くようになります。

FireBugLiteを使用してデバッグ

FireBugLiteを使用してデバッグ(クリックで拡大)