How to Incorporate Web Content into a JavaFX Application


Learn how to work with web content in a JavaFX application in this book excerpt from Mastering JavaFX 10 by Sergey Grinev.

The WebView component is a component that can render modern HTML pages. It’s based on WebKit (https://webkit.org/), a widely used open-source browser engine.

WebView consists of two parts:

  • WebViewitself, which is a JavaFX node and can be used in SceneGraph
  • The WebEngineclass, which is responsible for all HTML and JavaScript logic

Technically, there is also a third component—a rendering engine. It’s not provided by WebKit and JavaFX has its own. But developers usually do not communicate with it.

Presenting web content with WebView

You can begin with a simple WebView example. Note that you can find the code files for this article at https://github.com/PacktPublishing/Mastering-JavaFX-10/tree/master/Chapter09.

// chapter9/web/WebViewDemo.java
public void start(Stage primaryStage) {
WebView wv = new WebView();
wv.getEngine().load(“https://stackoverflow.com/questions/tagged/javafx”);

StackPane root = new StackPane(wv);
primaryStage.setTitle(“JavaFX on SO”);
primaryStage.setScene(new Scene(root, 400, 250));
primaryStage.show();
}

This code loads and shows a page from stackoverflow.com:

The next section will introduce you to the useful properties of WebView:

  • Context menu
  • Accessibility features

Context menu

Setting the contextMenuEnabled property to true will enable a context menu with some basic functions in WebView:

It provides some basic operations, such as opening links, reloading, and working with a clipboard. But there is no API to control it in any way, so the usefulness of this menu is limited.

Note that the Open Link in New Window functionality requires setting a pop-up handler.

Accessibility features

You can also control the font size or zoom in WebView:

webView.setZoom(1.2);           // +20%

webView.setFontScale(1.5);      // +50%

The font scaled here is on the JavaFX rendering level, which means that web page layouts and styles will be distorted.

Web engine

The rest of the functionalities that one may expect from the WebView API is provided by WebEngine, most importantly, access to the JavaScript and HTML document of the represented web page.

Handling page loading progress with LoadWorker

The WebEngine.getLoadWorker() method returns a Worker object that tracks the progress of the page loading.

The most important properties of Worker are the following:

  • progressProperty: This represents the page loading progress
  • stateProperty: This represents the status of the loading process, especially State.FAILED and Worker.State.SUCCEEDED

In the next example, you can use these properties to add a progress loading indicator and messages about the end of the page loading:

// chapter9/web/LoadWorkerDemo.java
public void start(Stage primaryStage) {
WebView webView = new WebView();
WebEngine webEngine = webView.getEngine();
webEngine.load(“https://stackoverflow.com/questions/tagged/javafx”);

ProgressBar loadingBar = new ProgressBar(0);
loadingBar.setMinWidth(400);

// using binding to easily connect the worker and the progress bar
loadingBar.progressProperty().bind(
webEngine.getLoadWorker().progressProperty());

webEngine.getLoadWorker().stateProperty().addListener(
(observable, oldValue, newValue) -> {
if (newValue == Worker.State.SUCCEEDED) {
System.out.println(“Page was successfully loaded!”);
} else if (newValue == Worker.State.FAILED) {
System.out.println(“Page loading has failed!”);
}

});

VBox root = new VBox(5, loadingBar, webView);
primaryStage.setTitle(“JavaFX on SO”);
primaryStage.setScene(new Scene(root, 400, 300));
primaryStage.show();
}
Here is a screenshot of the application in the middle of loading. Note the progress bar at the top:

It’s very important to wait for Worker.State.SUCCEEDED before starting any work with web page content—both HTML and JavaScript. It’s similar to the JavaScript practice of writing code in the document.onload() method.

Another way to wait for page loading is the workDone property:

webEngine.getLoadWorker().workDoneProperty().addListener((observable, oldValue, newValue) -> {
if (newValue.intValue() == 100) {
// page is 100% loaded
}
});

Loading content and user interface callbacks

Besides a particular URL, you can directly provide HTML to WebEngine through the loadContent() method:
webEngine.loadContent(“<input type=button onclick=\”window.alert(‘hi’)\” value=’Click Me!’>”);
Note that you are calling a JavaScript window.alert() method here, which should show an alert window by JavaScript spec. WebEngine can’t create new windows by itself, but has a corresponding API that you can use:
// chapter9/web/WebEngineDemo.java
webEngine.setOnAlert((event) -> {
Stage stage = new Stage((StageStyle.UTILITY));
stage.setScene(new Scene(new StackPane(new Label(event.getData())), 100, 80));
stage.show();
});
webEngine.loadContent(“<input type=button onclick=\”window.alert(‘hi’)\” value=’Click Me!’>”);
A slightly more complex setup is required to handle the window.open() requests. They are called pop-up requests and you need to create a new WebView in the desired location (usually a new Stage) and provide a corresponding WebEngine as a return value:
// chapter9/web/WebEngineDemo.java
webEngine.setCreatePopupHandler((popupFeatures) -> {
// create a new stage with new webview
Stage stage = new Stage((StageStyle.UTILITY));
WebView webViewPopup = new WebView();
stage.setScene(new Scene(new StackPane(webViewPopup), 300, 300));
stage.show();
// return engine from the created webview
return webViewPopup.getEngine();
});
webEngine.loadContent(“<a href=’http://www.google.com’>google</a>”);
Thus the original WebView doesn’t care how the new window is created (and whether it was created at all); it just asks the provided WebEngine to load the corresponding URL:

Here is a list of other supported callbacks; you can find a full list in the WebEngine’s JavaDoc:

JavaScript method/propertyJavaFX WebEngine callback
window.confirm()confirmHandler
window.open()/window.close() eventonVisibilityChanged
window.prompt()promptHandler
Setting window.statusonStatusChanged
Any window resize callsonResized

Note that all these handlers work for WebEngine.load(URL string) calls as well. You can use loadContent() to see the corresponding HTML right in the code.

Using Document Object Model

The Document Object Model (DOM) can be retrieved through WebEngine.getDocument(), which returns org.w3c.dom.Document giving access to the whole web page model.

In the following demonstration, you can find and print all links on a web page using this API:

// chapter9/web/DOMModelDemo.java
public void start(Stage stage) {
WebView webView = new WebView();
WebEngine webEngine = webView.getEngine();
webEngine.load(“https://stackoverflow.com/questions/tagged/javafx”);

webEngine.getLoadWorker().stateProperty().addListener(
(observable, oldValue, newValue) -> {
// remember, we need to get the page loaded first
if (newValue == Worker.State.SUCCEEDED) {
NodeList links = webEngine.getDocument().getElementsByTagName(“a”);
for (int i = 0; i < links.getLength(); i++) {
System.out.println(links.item(i));
}
} else if (newValue == Worker.State.FAILED) {
System.out.println(“Page loading has failed!”);
}

});

stage.setTitle(“JavaFX on SO”);
stage.setScene(new Scene(new StackPane(webView), 400, 300));
stage.show();
}

Running JavaScript on a page

You can execute an arbitrary JavaScript by calling WebEngine.executeScript(). The return type is not defined; it depends on the type of JavaScript that has been executed, for example, String, Number, JSObject,. You can find a full list in the JavaDoc.

JavaScript numbers from the same function can be matched to different Java objects on each call—either Double or Integer, depending on JavaScript values. To avoid instanceof checks, you can use java.lang.Number for all numerical values.

In this example, you can use JavaScript commands to dynamically show an overlay over a WebView that will show the bounds of the HTML element under the cursor and its tag name.

For the overlay, you can put a separate transparent pane over WebView and draw the overlay there. JavaScript will be used to get the borders and type of the element under the cursor.

For example, you can see that the area under the mouse cursor is a DIV, as shown in the following screenshot:

See the inline comments for more details:

// chapter8/web/WebOverlay.java
public void start(Stage stage) {
WebView webView = new WebView();
// name of the element shown in the overlay
Text overlayText = new Text(5, 18, “”);
overlayText.setFont(Font.font(null, FontWeight.BOLD, 20));
// red overlay
Pane overlay = new Pane(overlayText);
overlay.setStyle(“-fx-background-color: rgba(255,0,0,0.5);”);
// transparent pane to hold our overlay, it covers all WebView
Pane pane = new Pane(overlay);
pane.setPrefSize(600, 600);

stage.setScene(new Scene(new StackPane(webView, pane), 600, 600));

pane.setOnMouseMoved((event) -> {
// calling javascript to find what element is under cursor
// result is netscape.javascript.JSObject
JSObject object = (JSObject) webView.getEngine().
executeScript(“document.elementFromPoint(” + event.getX() + “,” + event.getY() + “);”);
if (object != null) {
// calculating element’s bounds using JavaScript object
JSObject bounds = (JSObject) object.call(“getBoundingClientRect”);
// converting types from JavaScripts can be oververbose
overlay.setTranslateX(((Number) bounds.getMember(“left”)).doubleValue());
overlay.setTranslateY(((Number) bounds.getMember(“top”)).doubleValue());
overlay.setMinWidth(((Number) bounds.getMember(“width”)).doubleValue());
overlay.setMinHeight(((Number) bounds.getMember(“height”)).doubleValue());
// finding what this element is
overlayText.setText((String)object.getMember(“tagName”));
}
});
webView.getEngine().load(“https://stackoverflow.com/questions/tagged/javafx”);

stage.show();
}

Calling JavaFX code from JavaScript

The user interface callbacks were your first example of calling JavaFX code from JavaScript. But you can use such calls not only for preset API methods but also for any logic, for example, as a reaction to a button press. Here is how it works.

First of all, you need to get access to the JavaScript window object like in the previous section:

JSObject window = (JSObject) webEngine.executeScript(“window”);

Technically, it can be any JavaScript object, not only window. It is used here for simplicity.

Now you can inject a random object into the window:

window.setMember(“app”, this);

Given that you can call this code in the start() method, you can provide a link to the JavaFX application. You can use a specially crafted object instead:

MyContext context = new MyContext();window.setMember(“app”, context);

Now, on the JavaScript side, you have access to the app object and can call methods from it. Add an extra method to your application:

public void test(String param) {    System.out.println(“hi ” + param);}

You can load JavaScript to call the preceding method. Note that you can easily pass a parameter to the called method:

webEngine.loadContent(“<p><input type=button onclick=\”app.test(‘hi’)\” value=’Click Me!’>”);

Here are all the pieces together in a more interesting example:

In this example, you can type any color into the HTML textbox and assign it to the JavaFX background of the application; see the details in the inline comments:
// chapter9/web/JS2JavaBridgeDemo.java
public class JS2JavaBridgeDemo extends Application {
// declaring root as a variable to have access to it from setColor() method
StackPane root;

@Override
public void start(Stage primaryStage) {
WebView webView = new WebView();
WebEngine webEngine = webView.getEngine();

// here we have an HTML page with a text box and a button
// pressing the button will take entered text value and
// pass it to the JavaFX application
webEngine.loadContent(
“<p><input type=text id=’color’ value=’red’/>”
+ “<p><input type=button onclick=\”app.setColor(document.getElementById(‘color’).value)\” value=’Click Me!’>”);
JSObject window = (JSObject) webEngine.executeScript(“window”);
window.setMember(“app”, this);

root = new StackPane(webView);
// adding padding to have visible part of the background
root.setPadding(new Insets(10));
primaryStage.setTitle(“JavaFX JavaScript to JavaFX bridge demo”);
primaryStage.setScene(new Scene(root, 300, 250));
primaryStage.show();
}

// this method we will be calling from JavaScript
public void setColor(String param) {
// taking parameter and apply it as a color to the background
root.setStyle(“-fx-background-color: ” + param + “;”);
}

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

}

Now you can work with any HTML from JavaFX back and forth. You can enhance existing web pages with JavaFX functionality, provide a similar interface for web access and a JavaFX app for your service, or just use HTML5 capabilities as part of your application.

If you found this article helpful, you explore JavaFX 10 in depth in Sergey Grinev’s Mastering JavaFX 10. This book takes you on a journey to use JavaFX 10 to build applications that display information in a high-performance, modern user interface featuring audio, video, graphics, and animation.

Leave a comment

Your email address will not be published. Required fields are marked *