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をクラスパス上(WebViewTestクラスと同じ場所)に置いて表示してみます。先ほどのソースのハイライト部分を下記のように変更します。
// (2)クラスパス上のHTMLを指定 String url = getClass().getResource("javafxtest.html").toExternalForm(); engine.load(url);
これだとローカルフォルダに置いた画像が表示されません。では、HTMLがWeb上にあったらどうでしょうか。
// (3)Web上のHTMLを指定 String url = "http://www.hos.co.jp/wp-content/uploads/2013/08/javafxtest.html"; engine.load(url);
これも、ローカルフォルダに置いた画像が表示されませんね。
要するに、ローカルフォルダの画像を表示するなら、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ファイルを文字列として読み込んでからやっていますが、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を表示した場合:
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の中で動くようになります。