【JavaFX】Webブラウザを作ろう!

2018年6月11日

JavaFXのAPI眺めてたら、WebViewなるものが存在しているではないか!!

ということで、今回は自作のブラウザをJavaFXで作ってみたいと思います。

あと、普段ゲーム関連中心でやってるので触れなかったのですが、JavaFXにはFXMLと呼ばれるGUIのレイアウトを簡単に作る機能があるのです。いい機会なので、FXMLも少し利用してみようと思います。

今回紹介する内容で作成したシンプルブラウザのソースコードはこちらにて全て公開していますので、煮るなり焼くなり好きにしてもらって構いません。



関連クラスの紹介

WebView

Webブラウザみたいに画面表示できるNodeクラスです。Sceneクラスのroot要素としたり、Parentクラスの要素に追加することでWebブラウザで見ているような画面を簡単に表示できます。getEngineメソッドでWebブラウザに関する基本機能を利用できるオブジェクトを取得できます。

WebEngine

Webブラウザで一般的に使われそうな機能を利用できます。例えば特定のURLにジャンプするloadメソッドやページ表示の履歴情報を取得できるgetHistoryメソッドがあります。

WebHistory

表示したページの履歴情報が取得できます。ブラウザの戻るや進むを実装するためのgoメソッドが使用できます。

Web画面を表示する

Web画面を表示するにはWebViewクラスをインスタンス化し、SceneかParentに要素を追加します。

そのあとに、getEngineメソッドでWebEngineを取得し、loadメソッドで表示したいURLを引数にしましょう。

サンプルでは皆様お馴染みのgoogle検索ページを表示するようにしてみましょうか。

package application;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import javafx.stage.Stage;

public class Test extends Application{

	@Override
	public void start(Stage primaryStage) {
		try {

			WebView root = new WebView();
			WebEngine engine = root.getEngine();
			Scene scene = new Scene(root,800,600);
			primaryStage.setScene(scene);
			primaryStage.show();

			engine.load("https://www.google.co.jp/");
		} catch(Exception e) {
			e.printStackTrace();
		}
	}

	public static void main(String[] args) {
		launch(args);
	}
}
実行結果

※2018/06/09時点での表示結果となります。

アドレスバーを実装してみる

アドレスバーをヘッダーに表示してEnterでページを切り替えできるようにしてみます。

ここからは、画面レイアウトしやすいように、FXMLやcss、Controllerを利用して作成してみます。

test.fxml
<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.FlowPane?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.web.WebView?>

<!-- fx:controllerで関連付けるコントローラクラスを指定 -->
<BorderPane xmlns:fx="http://javafx.com/fxml/1" fx:controller="TestController">
	<top>
		<!-- 画面ヘッダーの表示 -->
		<TextField fx:id="addressBar" text="" onKeyPressed="#onAddressBarEvent" ></TextField>
	</top>
	<center>
		<!-- 画面中央のWeb画面表示 -->
		<WebView fx:id="webView"></WebView>
	</center>
</BorderPane>
TestController.java

import javafx.fxml.FXML;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyEvent;
import javafx.scene.web.WebView;

public class TestController {
	@FXML private TextField addressBar;
	@FXML private WebView webView;

	//初期化処理(自動呼出し)
	//initialize()メソッドは自動呼出しの対象となる
	public void initialize() {
		load("https://www.google.co.jp/");
	}

	//アドレスバーでEnter
	public void onAddressBarEvent(KeyEvent e) {
		switch(e.getCode()) {
		case ENTER:load(addressBar.getText());break;
		default:
			break;
		}
	}

	//URLロード
	public void load(String search) {
		if ( !search.matches("^https{0,1}://.+") ) {
			//httpじゃないならgoogle検索を実行
			search = "https://www.google.co.jp/search?q="+search;
		}
		webView.getEngine().load(search);
	}
}

Test.java
import java.io.File;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;

public class Test extends Application{

	@Override
	public void start(Stage primaryStage) {
		try {
			//fxmlを読み込んで画面表示
			BorderPane root = FXMLLoader.load(new File("test.fxml").toURI().toURL());
			Scene scene = new Scene(root,800,600);
			primaryStage.setScene(scene);
			primaryStage.show();
		} catch(Exception e) {
			e.printStackTrace();
		}
	}

	public static void main(String[] args) {
		launch(args);
	}
}
実行結果

「https://www.youtube.com/?gl=JP&hl=ja」を入力してEnterを押下してみた結果です。

アドレバーにHTTP系のURLが入力されたらそのページに飛んで、それ以外の場合はgoogle検索をする分岐を入れているので、通常のワードを入れると検索できるようになっています。

ページロード処理を検出

今のままではページ内リンクで画面が切り替わった時にアドレスバーの表示が変わらないので、ページのロード処理を検出したときにアドレスバーの文字列を書き換えて表示してみましょう。

ページの読み込み処理を検出するにはgetLoadWorkerで取得したオブジェクトにイベントオブジェクトを引き渡すことで検出できるようになります。

イベントタイプについてはこちらのAPIリファレンスを参照してください。

今回はロード完了したときのイベントでURL変更処理を実行します。イベントはSUCCEEDEDです。

TestController.javaを下記のように修正しました。

import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.Worker.State;
import javafx.fxml.FXML;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyEvent;
import javafx.scene.web.WebView;

public class TestController {
	@FXML private TextField addressBar;
	@FXML private WebView webView;

	//初期化処理(自動呼出し)
	//initialize()メソッドは自動呼出しの対象となる
	public void initialize() {
		//ページロードイベントでアドレスバーの更新
		webView.getEngine().getLoadWorker().stateProperty().addListener(
			new ChangeListener<State>() {
				public void changed(ObservableValue<? extends State> ov, State oldState, State newState) {
					if (newState == State.SUCCEEDED) {
						addressBar.setText(webView.getEngine().getLocation());
					}
				}
			}
		);
		load("https://www.google.co.jp/");
	}

	//アドレスバーでEnter
	public void onAddressBarEvent(KeyEvent e) {
		switch(e.getCode()) {
		case ENTER:load(addressBar.getText());break;
		default:
			break;
		}
	}

	//URLロード
	public void load(String search) {
		if ( !search.matches("^https{0,1}://.+") ) {
			//httpじゃないならgoogle検索を実行
			search = "https://www.google.co.jp/search?q="+search;
		}
		webView.getEngine().load(search);
	}
}
実行結果

実行して他のページにリンクからジャンプしてもアドレスバーの値が変更されるようになっています。

戻る、進むを実装してみる

一般のブラウザについている戻る、進むを実装します。

WebHistoryオブジェクトのgoメソッドでジャンプしたいインデックスを指定するだけで簡単に実装できます。

戻るなら-1、
進むなら+1

をgoメソッドに引き渡すだけです。

goメソッドの引数に-1など、存在しないインデックスを渡してしまうとエラーが発生するので、そこはif文で判断して実装しておきます。

getCurrentIndexメソッドで現在のインデックスが取得できるので、こいつで判断してやりましょうか。

それではサンプルです。test.fxml、TestController.java、Test.javaを下記のように修正し、新たにtest.cssを追加しました。

test.fxml
<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.FlowPane?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.web.WebView?>

<!-- fx:controllerで関連付けるコントローラクラスを指定 -->
<BorderPane xmlns:fx="http://javafx.com/fxml/1" fx:controller="TestController">
	<top>
		<!-- 画面ヘッダーの表示 -->
		<BorderPane styleClass="topView">
			<left>
				<FlowPane prefWidth="100">
					<Button onAction="#onBack" text="戻る"></Button>
					<Button onAction="#onNext" text="進む"></Button>
				</FlowPane>
			</left>
			<center>
				<TextField fx:id="addressBar" text="" onKeyPressed="#onAddressBarEvent" ></TextField>
			</center>
			<right>
				<FlowPane prefWidth="100">
				</FlowPane>
			</right>
		</BorderPane>
	</top>
	<center>
		<!-- 画面中央のWeb画面表示 -->
		<WebView fx:id="webView"></WebView>
	</center>
</BorderPane>
test.css
.topView{
	-fx-border-color:#000000;
	-fx-background-color:#888;
}
TestController.java

import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.Worker.State;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyEvent;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebHistory;
import javafx.scene.web.WebView;

public class TestController {
	@FXML private TextField addressBar;
	@FXML private WebView webView;

	//初期化処理(自動呼出し)
	//initialize()メソッドは自動呼出しの対象となる
	public void initialize() {
		//ページロードイベントでアドレスバーの更新
		webView.getEngine().getLoadWorker().stateProperty().addListener(
			new ChangeListener<State>() {
				public void changed(ObservableValue<? extends State> ov, State oldState, State newState) {
					if (newState == State.SUCCEEDED) {
						addressBar.setText(webView.getEngine().getLocation());
					}
				}
			}
		);
		load("https://www.google.co.jp/");
	}

	//アドレスバーでEnter
	public void onAddressBarEvent(KeyEvent e) {
		switch(e.getCode()) {
		case ENTER:load(addressBar.getText());break;
		default:
			break;
		}
	}

	//戻るボタン
	public void onBack(ActionEvent e) {
		WebEngine engine = webView.getEngine();
		WebHistory history = engine.getHistory();
		if ( history.getCurrentIndex() > 0 ) history.go(-1);
	}

	//進むボタン
	public void onNext(ActionEvent e) {
		WebEngine engine = webView.getEngine();
		WebHistory history = engine.getHistory();
		if ( history.getCurrentIndex() < history.getEntries().size() - 1 ) history.go(1);
	}

	//URLロード
	public void load(String search) {
		if ( !search.matches("^https{0,1}://.+") ) {
			//httpじゃないならgoogle検索を実行
			search = "https://www.google.co.jp/search?q="+search;
		}
		webView.getEngine().load(search);
	}
}
Test.java
import java.io.File;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;

public class Test extends Application{

	@Override
	public void start(Stage primaryStage) {
		try {
			//fxmlとcssを読み込んで画面表示
			BorderPane root = FXMLLoader.load(new File("test.fxml").toURI().toURL());
			Scene scene = new Scene(root,800,600);
			scene.getStylesheets().add(new File("test.css").toURI().toURL().toExternalForm());
			primaryStage.setScene(scene);
			primaryStage.show();
		} catch(Exception e) {
			e.printStackTrace();
		}
	}

	public static void main(String[] args) {
		launch(args);
	}
}
実行結果

ここまでくれば、かなり普通のブラウザになってきましたね。



HTMLドキュメントを操作する

WebEngineのgetDocumentでページのHTMLドキュメントを取得して表示したり、操作することができます。

試しにgoogleの検索ページを表示して、DOM操作を行いページを改造してみましょう。

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.Worker.State;
import javafx.scene.Scene;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import javafx.stage.Stage;

public class Test extends Application{

	@Override
	public void start(Stage primaryStage) {
		try {

			WebView root = new WebView();
			WebEngine engine = root.getEngine();
			Scene scene = new Scene(root,800,600);
			primaryStage.setScene(scene);
			primaryStage.show();

			engine.load("https://www.google.co.jp/");

			//ページロードイベントでbody要素に緑色適用
			engine.getLoadWorker().stateProperty().addListener(
				new ChangeListener<State>() {
					public void changed(ObservableValue<? extends State> ov, State oldState, State newState) {
						if (newState == State.SUCCEEDED) {
							//HTMLドキュメントを取得
							Document doc = engine.getDocument();
							NodeList list = doc.getElementsByTagName("body");
							for ( int i = 0;i < list.getLength();i++ ) {
								Element node = (Element)list.item(i);
								//HTMLスタイルを適用しbodyを緑にする
								node.setAttribute("style", "background-color:#0f0");
							}
						}
					}
				}
			);
		} catch(Exception e) {
			e.printStackTrace();
		}
	}

	public static void main(String[] args) {
		launch(args);
	}
}
実行結果

試しにいろいろなサイトに飛んでいただくと、やたらと緑に変わっている部分があるはずです。

JavaScriptを任意のタイミングで実行する

WebEngineにはJavaScriptを任意のタイミングで実行できるメソッドがあります。

WebEngineのexecuteScriptにJavaScriptプログラムを渡すと実行できます。

特定のページで特殊な自動処理を呼び出すことができれば便利な場合もあると思います。

例えば普通のブラウザではボタンを押さなければならないが、自作ブラウザならボタンを押さなくても自動で実行できるなどが考えられます。

それでは自動カウントアップが強制表示されるJavaScriptを実行させてみましょう。

import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.Worker.State;
import javafx.scene.Scene;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import javafx.stage.Stage;

public class Test extends Application{

	@Override
	public void start(Stage primaryStage) {
		try {

			WebView root = new WebView();
			WebEngine engine = root.getEngine();
			Scene scene = new Scene(root,800,600);
			primaryStage.setScene(scene);
			primaryStage.show();

			engine.load("https://www.google.co.jp/");

			//ページロードイベントでbodyにカウントアップの要素を追加し、タイマーでカウントアップを実行する
			engine.getLoadWorker().stateProperty().addListener(
				new ChangeListener<State>() {
					public void changed(ObservableValue<? extends State> ov, State oldState, State newState) {
						if (newState == State.SUCCEEDED) {
							//JavaScriptプログラムを生成
							StringBuffer sb = new StringBuffer();
							sb.append("var cnt = 0;");
							sb.append("var e = document.createElement('div');");
							sb.append("e.style.position = 'fixed';");
							sb.append("e.style.left = '100px';");
							sb.append("e.style.top = '100px';");
							sb.append("e.style.fontSize = '50pt';");
							sb.append("e.style.zIndex = 99999;");
							sb.append("document.body.appendChild(e);");
							sb.append("setInterval(function(){cnt++;e.innerText=cnt;},1000);");
							engine.executeScript(sb.toString());
						}
					}
				}
			);
		} catch(Exception e) {
			e.printStackTrace();
		}
	}

	public static void main(String[] args) {
		launch(args);
	}
}
実行結果

画面左上にどこからともなく現れた数値がカウントアップされています。もちろん別ページに飛んでもカウントアップが表示されます。

alertやconsole.log等のJavaScriptの関数を検出する

WebEngineにはJavaScriptの標準関数である、alertの実行イベントを検出したり、console.logを実行したときに任意のJavaメソッドを呼び出すようにすることができます。

alertやconfirmの検出について

下記のようなイベントを利用します。
setOnAlert(EventHandler<WebEvent<String>> handler)
setPromptHandler(Callback<PromptData,String> handler)
setConfirmHandler(Callback<String,Boolean> handler)

イベントハンドラの詳細はこちらです。

それではalertを実装するサンプルを作成してみましょう。

Test.java
import java.io.File;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Dialog;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import javafx.stage.Stage;

public class Test extends Application{

	@Override
	public void start(Stage primaryStage) {
		try {

			WebView root = new WebView();
			WebEngine engine = root.getEngine();
			Scene scene = new Scene(root,800,600);
			primaryStage.setScene(scene);
			primaryStage.show();

			//アラートの処理を実装
			engine.setOnAlert(e -> {
				Dialog<ButtonType> d = new Alert(AlertType.INFORMATION);
				d.setContentText(e.getData());
				d.show();
			});

			engine.load(new File("index.html").toURI().toString());

		} catch(Exception e) {
			e.printStackTrace();
		}
	}

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

下記が読み込むHTMLファイルです。

index.html
<html>
<head>
<script>alert("Yes!");</script>
</head>
<body>
</body>
</html>
実行結果

console.log等、イベント設定できないタイプの関数検出について

console.logは私が探した範囲ではイベントを検出する方法が用意されておらず、実装方法はないのかと思いましたが、任意のJavaScriptを実行し、標準関数を書き換えることでJava側のメソッドを呼び出すことができます。

まず、WebEngineのexecuteScriptメソッドを利用し、windowオブジェクトをJava側に取得します。

次にwindowオブジェクトに対し、setMemberメソッドでJavaのオブジェクトを設定します。

最後にexecuteScriptメソッドで任意の関数を書き換え、Javaのメソッドを呼び出すようにしておけば準備OK。

試しにconsole.logが呼び出された時に、System.out.println(msg);が実行されるようにしてみましょう。

Test.java
import java.io.File;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import javafx.stage.Stage;
import netscape.javascript.JSObject;

public class Test extends Application{
	//イベント受付クラス(publicでなければならない)
	public class EventHandler{
		public void log(String msg) {
			System.out.println(msg);
		}
	}

	@Override
	public void start(Stage primaryStage) {
		try {

			WebView root = new WebView();
			WebEngine engine = root.getEngine();
			Scene scene = new Scene(root,800,600);
			primaryStage.setScene(scene);
			primaryStage.show();

			//コンソールログの実装
			JSObject window = (JSObject) engine.executeScript("window");
			window.setMember("eventHandler", new EventHandler());
			engine.executeScript("console.log = function(msg){window.eventHandler.log(msg);};");

			engine.load(new File("index.html").toURI().toString());
		} catch(Exception e) {
			e.printStackTrace();
		}
	}

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

下記が実行したHTMLファイルです。

index.html
<html>
<head>
<script>console.log("Hello JavaFX WebView World!");</script>
</head>
<body>
</body>
</html>
実行結果

Hello JavaFX WebView World!

JavaJavaFX

Posted by nompor