JavaFX provides new capabilities to interoperate with HTML5. The underlying web page–rendering engine in JavaFX is the popular open-source API called Webkit. Webkit is also used in Google’s Chrome and Apple’s Safari browsers. HTML5 is the new standard markup language for rendering content in web browsers. HTML5 content consists of JavaScript, CSS, Scalable Vector Graphics (SVG), and new HTML element tags.
The relationship between JavaFX and HTML5 is important because they complement one another by drawing from each of their individual strengths. For instance, JavaFX’s rich client APIs coupled with HTML5’s rich web content create a user experience resembling a web application with the characteristics of desktop software. This new breed of applications is called RIAs.
In this chapter, we will cover the following:
You hope to get promoted out of your cubicle into an office with windows by impressing your boss by creating a proof of concepts using JavaFX with your existing web development skills.
Create a Hello World application using the NetBeans IDE 7.1 or later by using its new project wizard to create an application to run in a browser. Shown following are steps to follow to create a Hello World JavaFX application that is embedded in an HTML web page:
Note For in-depth JavaFX deployment strategies refer to Oracle’s deploying JavaFX Applications: http://download.oracle.com/javafx/2.0/deployment/deployment_toolkit.htm
.
Here are the steps to follow in running the new project wizard:
Figure 20-1. Creating a new JavaFX project
MyJavaFXApp.java
. Figure 20-3 shows a New JavaFX application wizard that specifies the project name and location. When you finish, click the Finish button.
Figure 20-3. New JavaFX Application dialog box, in which you specify Project Name and Project Location
MyJavaFXApp.java
.
Figure 20-4. MyJavaFXApp.java project
Figure 20-7. The MyJavaFXApp Hello World application running inside a browser
To create an embedded JavaFX application inside an HTML page, you use the NetBeans IDE. Although there are different deployment strategies, such as Webstart and Standalone modes, here you use the NetBeans new project wizard to automatically deploy as a local web page containing your JavaFX application in your browser. For in-depth JavaFX deployment strategies, refer to Oracle’s Deploying JavaFX Applications: http://download.oracle.com/javafx/2.0/deployment/deployment_toolkit.htms
.
Following is the code generated by this solution. You will notice the JavaFX classes being used; for example, Stage
, Group
, and Scene
classes.
Note You can drag the imports and body of code from another code file for this recipe into the body of your new main project class, changing the name on the class definition line, as appropriate.
Following is the source code when the NetBeans’ wizard generates a new project to create a JavaFX application embedded in a HTML web page:
package myjavafxapp;
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.stage.Stage;
/**
*
* @author cdea
*/
public class MyJavaFXApp extends Application {
/**
* @param args the command line arguments
*/
public static void main(String[] args) {
Application.launch(args);
}
@Override
public void start(Stage primaryStage) {
primaryStage.setTitle("Hello World");
Group root = new Group();
Scene scene = new Scene(root, 300, 250);
Button btn = new Button();
btn.setLayoutX(100);
btn.setLayoutY(80);
btn.setText("Hello World");
btn.setOnAction(new EventHandler<ActionEvent>() {
public void handle(ActionEvent event) {
System.out.println("Hello World");
}
});
root.getChildren().add(btn);
primaryStage.setScene(scene);
primaryStage.show();
}
}
In Step 1, you initiate a new project (shown in Figure 20-7). In Step 2, you select the standard JavaFX application to be created. After selecting the project type, you will be specifying the name of the project. Make sure you click the Create Application Class check box to allow the wizard to generate the MyJavaFXApp Java file. Once you have clicked Finish, your newly created application will appear in the projects tab. Next, you will take Step 5 in changing project properties.
In Step 5 you will be changing two categories: Sources and Run. In the Sources category, make sure the Source/Binary Format is set to JDK 1.6 or later. After updating the Sources category, you will be changing how the project will run (Step 6) through the Run category. In Step 6, after selecting the in Browser radio button option, you will notice the Width and Height below the working directory field. To use your own custom web page, you click the browse button to select an existing HTML file, but in this recipe you can leave it blank to allow the wizard to generate a generic HTML page. Assuming that you are done with your settings, click OK to close the Project Properties dialog window.
Last, you will run your embedded JavaFX web application (Step 7). To run your application you will want to make sure this project is set as the main project by selecting in the menu Run -> Set Main Project ->MyJavaFXApp
. Once you are initiating a run, your browser will launch, containing a generic web page with your JavaFX application. You’ll also notice that a convenient link allows you to launch the application as a Webstart application (not embedded).
You are so engrossed with a project for work that you often miss your kid’s soccer games. What you need is a clock application to keep track of the time.
Create a JavaFX based-application containing an analog clock that was created as HTML5 content. Use JavaFX’s WebView
API to render HTML5 content in your application.
The following source code is a JavaFX application displaying an animated analog clock. The application will load an SVG file named clock3.svg
and display the contents onto the JavaFX Scene graph:
package org.java7recipes.chapter20.recipe20_02;
import java.net.URL;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.scene.web.WebView;
import javafx.stage.Stage;
/**
*
* @author cdea
*/
public class DisplayHtml5Content extends Application {
private Scene scene;
@Override public void start(Stage stage) {
// create the scene
stage.setTitle("Chapter 20-2 Display Html5 Content");
final WebView browser = new WebView();
URL url = getClass().getResource("clock3.svg");
browser.getEngine().load(url.toExternalForm());
scene = new Scene(browser,590,400, Color.rgb(0, 0, 0, .80));
stage.setScene(scene);
stage.show();
}
public static void main(String[] args){
Application.launch(args);
}
}
This JavaFX code will load and render HTML5 content. Assuming that you have a designer who has provided content such as HTML5, it will be your job to render assets in JavaFX. The following code represents an SVG file named clock3.svg
that is predominantly generated by the powerful tool Inkscape, which is an illustrator tool capable of generating SVG. In the following code, notice hand-coded JavaScript code (inside the CDATA
tag) that will position the second, minute, and hour hands of the clock based on the current time of day. Because all the logic (from setting the time to animating the hands) is inside this file, things are self contained, which means any HTML5 capable viewer can display the file’s contents. So when debugging, you can easily render content in any HTML5-compliant browser. Later in this chapter, we will demonstrate JavaFX code that can interact with HTML5 content.
Shown here is a pared-down version of the SVG analog clock. (To obtain the file’s source code, download the code from the book’s web site.) This is anSVG analog clock created in Inkscape (clock3.svg
):
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="300"
height="250"
id="svg4171"
version="1.1"
inkscape:version="0.48.1 "
sodipodi:docname="clock3.svg" onload="updateTime()">
<script>
<![CDATA[
var xmlns="http://www.w3.org/2000/svg"
function updateTime()
{
var date = new Date()
var hr = parseInt(date.getHours())
if (hr > 12) {
hr = hr - 12;
}
var min = parseInt(date.getMinutes())
var sec = parseInt(date.getSeconds())
var pi=180
var secondAngle = sec * 6 + pi
var minuteAngle = ( min + sec / 60 ) * 6 + pi
var hourAngle = (hr + min / 60 + sec /3600) * 30 + pi
moveHands(secondAngle, minuteAngle, hourAngle)
}
function moveHands(secondAngle, minuteAngle, hourAngle) {
var secondHand = document.getElementById("secondHand")
var minuteHand = document.getElementById("minuteHand")
var hourHand = document.getElementById("hourHand")
secondHand.setAttribute("transform","rotate("+ secondAngle + ")")
minuteHand.setAttribute("transform","rotate("+ minuteAngle +")")
hourHand.setAttribute("transform","rotate("+ hourAngle + ")")
}
]]>
</script>
<defs id="defs4173">
... // beginning of SVG code
... // Main clock code
<g id="hands" transform="translate(108,100)">
<g id="minuteHand">
<line stroke-width="3.59497285" y2="50" stroke-linecap="round" stroke="#00fff6" opacity=".9" />
<animateTransform attributeName="transform" type="rotate" repeatCount="indefinite" dur="60min" by="360" />
</g>
<g id="hourHand">
<line stroke-width="5" y2="30" stroke-linecap="round" stroke="#ffcb00" opacity=".9" />
<animateTransform attributeName="transform" type="rotate" repeatCount="indefinite" dur="12h" by="360" />
</g>
<g id="secondHand">
<line stroke-width="2" y1="-20" y2="70" stroke-linecap="round" stroke="red"/>
<animateTransform attributeName="transform" type="rotate" repeatCount="indefinite" dur="60s" by="360" />
</g>
</g>
... // The rest of the Clock code: shiney glare, black button cover (center) on top of arms
</svg>
Figure 20-8 depicts a JavaFX application, rendering the SVG file clock3.svg
displaying an analog clock.
Figure 20-8. Analog clock
In this recipe, you will be creating an analog clock application that will take existing HTML5 content to be rendered onto the JavaFX Scene graph. HTML5 allows the use of SVG content to be shown in browsers. SVG is similar to JavaFX’s Scene graph, in which nodes can be scaled at different sizes while preserving details. To manipulate SVG or any HTML5 elements, you will be using the JavaScript language. Depicted in Figure 20-8 is a JavaFX application displaying an animated analog clock. To learn more about SVG, visit http://www.w3schools.com/svg/default.asp
. Before running this example, make sure the clock3.svg
file is located in the build path. In NetBeans you may need to perform a clean and build before running the application that will copy the resource (clock3.svg
) to the build path. You may also want to manually copy the clock3.svg
file to reside in the build path co-located where the DisplayHtml5Content.class
file is located if you are running application on the command line.
In software development you will undoubtedly experience working with a designer where he/she will use popular tools to generate web content that will be wired up to an application’s functions. To create an analog clock, I enlisted my daughter, who is quite proficient with the open-source tool Inkscape. Although Inkscape was used to generate the content for this recipe, I will not go into details regarding the tool because it is beyond the scope of this book. To learn more about Inkscape, please visit http://www.inkscape.org
for tutorials and demos. To model the Designer and Developer Workflow, she created a cool looking clock and I added JavaScript/SVG code to move the clock’s hour, minute, and second hands. Inkscape allows you to create shapes, text, and effects to generate amazing illustrations. Because SVG files are considered as HTML5 content, you will be able to display SVG drawings inside of an HTML5-capable browser. In this scenario, you will be displaying the analog clock in JavaFX’s WebView
node. You can think of a WebView
node as a mini browser capable of loading URLs to be displayed. When loading a URL you will notice the call to getEngine().load()
where the getEngine()
method will return an instance of javafx.scene.web.WebEngine
object. So, the WebView
object is implicitly creating one javafx.scene.web.WebEngine
object instance per WebView
object. Shown here is the JavaFX’s WebEngine
object loading a file clock3.svg
:
final WebView browser = new WebView();
URL url = getClass().getResource("clock3.svg");
browser.getEngine().load(url.toExternalForm());
You are probably wondering why the JavaFX source code is so small. The code is small because its job is to instantiate an instance of a javafx.scene.web.WebView
that instantiates a javafx.scene.web.WebEngine
class and passes a URL. After that, the WebEngine
object does all the work by rendering HTML5 content just like any browser. When rendering the content, notice that the clock’s arms move or animate; for example, the second hand rotates clockwise. Before animating the clock, you have to set the clock’s initial position by calling the JavaScript updateTime()
function via the onload
attribute on the entire SVG document (located on the root svg
element). Once the clock’s arms are set, you will add SVG code to draw and animate by using the line and animate transform elements, respectively. Shown here is a SVG code snippet to animate the second hand indefinitely:
<g id="secondHand">
<line stroke-width="2" y1="-20" y2="70" stroke-linecap="round" stroke="red"/>
<animateTransform attributeName="transform" type="rotate" repeatCount="indefinite"
dur="60s" by="360" />
</g>
On a final note, if you want to create a clock like the one depicted in this recipe, visit http://screencasters.heathenx.org/blog
to learn about all things Inkscape. Another impressive and beautiful display of custom controls that focuses on gauges and dials is the Steel Series by Gerrit Grunwald. To be totally amazed, visit his blog at http://harmoniccode.blogspot.com
.
You are an underpaid developer, and your boss refuses to let you relocate to the cube next to the window. You must find a way to determine the weather without leaving your workspace.
Create a weather application that fetches data from Yahoo’s weather service. The following code implements a weather application that retrieves Yahoo’s weather information to be rendered as HTML in a JavaFX application:
package org.java7recipes.chapter20.recipe20_03;
import javafx.animation.*;
import javafx.application.Application;
import javafx.beans.property.*;
import javafx.beans.value.*;
import javafx.concurrent.Worker.State;
import javafx.scene.*;
import javafx.scene.web.*;
import javafx.stage.Stage;
import javafx.util.Duration;
import org.w3c.dom.*;
/**
* Shows a preview of the weather and 3 day forecast
* @author cdea
*/
public class ManipulatingHtmlContent extends Application {
String url = "http://weather.yahooapis.com/forecastrss?p=USMD0033&u=f";
int refreshCountdown = 60;
@Override public void start(Stage stage) {
// create the scene
stage.setTitle("Chapter 20-3 Manipulating HTML content");
Group root = new Group();
Scene scene = new Scene(root, 460, 340);
final WebEngine webEngine = new WebEngine(url);
StringBuilder template = new StringBuilder();
template.append("<head>
");
template.append("<style type="text/css">body {background-color:#b4c8ee;}</style>
");
template.append("</head>
");
template.append("<body id='weather_background'>");
final String fullHtml = template.toString();
final WebView webView = new WebView();
IntegerProperty countDown = new SimpleIntegerProperty(refreshCountdown);
countDown.addListener(new ChangeListener<Number>() {
@Override
public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue){
// when change occurs on countDown call JavaScript to update text in HTMLwebView.getEngine().executeScript("document.getElementById('countdown').innerHTML = 'Seconds till refresh: " + newValue + "'");
if (newValue.intValue() == 0) {
webEngine.reload();
}
}
});
final Timeline timeToRefresh = new Timeline();
timeToRefresh.getKeyFrames().addAll(
new KeyFrame(Duration.ZERO, new KeyValue(countDown, refreshCountdown)),
new KeyFrame(Duration.seconds(refreshCountdown), new KeyValue(countDown, 0))
);
webEngine.getLoadWorker().stateProperty().addListener(new ChangeListener<State>() {
@Override
public void changed(ObservableValue<? extends State> observable, State oldValue, State newValue){
System.out.println("done!" + newValue.toString());
if (newValue != State.SUCCEEDED) {
return;
}
// request 200 OK
Weather weather = parse(webEngine.getDocument());
StringBuilder locationText = new StringBuilder();
locationText.append("<b>")
.append(weather.city)
.append(", ")
.append(weather.region)
.append(" ")
.append(weather.country)
.append("</b><br />
");
String timeOfWeatherTextDiv = "<b id="timeOfWeatherText">" + weather.dateTimeStr + "</b><br />
";
String countdownText = "<b id="countdown"></b><br />
";
webView.getEngine().loadContent(fullHtml + locationText.toString() +
timeOfWeatherTextDiv +
countdownText +
weather.htmlDescription);
System.out.println(fullHtml + locationText.toString() +
timeOfWeatherTextDiv +
countdownText +
weather.htmlDescription);
timeToRefresh.playFromStart();
}
});
root.getChildren().addAll(webView);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args){
Application.launch(args);
}
private static String obtainAttribute(NodeList nodeList, String attribute) {
String attr = nodeList
.item(0)
.getAttributes()
.getNamedItem(attribute)
.getNodeValue()
.toString();
return attr;
}
private static Weather parse(Document doc) {
NodeList currWeatherLocation = doc.getElementsByTagNameNS("http://xml.weather.yahoo.com/ns/rss/1.0", "location");
Weather weather = new Weather();
weather.city = obtainAttribute(currWeatherLocation, "city");
weather.region = obtainAttribute(currWeatherLocation, "region");
weather.country = obtainAttribute(currWeatherLocation, "country");
NodeList currWeatherCondition = doc.getElementsByTagNameNS("http://xml.weather.yahoo.com/ns/rss/1.0", "condition");
weather.dateTimeStr = obtainAttribute(currWeatherCondition, "date");
weather.currentWeatherText = obtainAttribute(currWeatherCondition, "text");
weather.temperature = obtainAttribute(currWeatherCondition, "temp");
String forcast = doc.getElementsByTagName("description")
.item(1)
.getTextContent();
weather.htmlDescription = forcast;
return weather;
}
}
class Weather {
String dateTimeStr;
String city;
String region;
String country;
String currentWeatherText;
String temperature;
String htmlDescription;
}
Figure 20-9 depicts the weather application that fetches data from the Yahoo Weather service. In the third line of displayed text, you’ll notice that Seconds till refresh: 31 is a countdown in seconds until the next retrieval of weather information. The actual manipulation of HTML content occurs here.
Figure 20-9. Weather application
The following is output to the console of the HTML that is rendered onto the WebView
node:
<head>
<style type="text/css">body {background-color:#b4c8ee;}
</style>
</head>
<body id='weather_background'><b>Berlin, MD US</b><br />
<b id="timeOfWeatherText">Thu, 06 Oct 2011 8:51 pm EDT</b><br />
<b id="countdown"></b><br />
<img src="http://l.yimg.com/a/i/us/we/52/33.gif"/><br />
<b>Current Conditions:</b><br />
Fair, 49 F<BR />
<BR /><b>Forecast:</b><BR />
Thu - Clear. High: 66 Low: 48<br />
Fri - Sunny. High: 71 Low: 52<br />
<br />
<a
href="http://us.rd.yahoo.com/dailynews/rss/weather/Berlin__MD/*http://weather.yahoo.com/foreca
st/USMD0033_f.html">Full Forecast at Yahoo! Weather</a><BR/><BR/>
(provided by <a href="http://www.weather.com" >The Weather Channel</a>)<br/>
In this recipe you will be creating a JavaFX application able to retrieve XML information from Yahoo’s weather service. Once the XML is parsed, HTML content is assembled and rendered onto JavaFX’s WebView
node. The WebView
object instance is a graph node capable of rendering and retrieving XML or any HTML5 content. The application will also display a countdown of the number of seconds until the next retrieval from the weather service.
When accessing weather information for your area through Yahoo’s weather service, you will need to obtain a location ID or the URL to the RSS feed associated with your city. Before I explain the code line by line, I will list the steps to obtain the URL for the RSS feed of your local weather forecasts.
http://weather.yahoo.com/
.http://weather.yahooapis.com/forecastrss?p=USMD0033&u=f
.Now that you have obtained a valid RSS URL web address, let’s use it in our recipe example. When creating the ManipulatingHtmlContent
class, you will need two instance variables: url
and refreshCountdown
. The url
variable will be assigned to the RSS URL web address from Step 4. The refreshCountdown
variable of type int
is assigned 60 to denote the time in seconds until a refresh or another retrieval of the weather information takes place.
Like all our JavaFX examples inside of the start()
method, we begin by creating the Scene
object for the initial main content region. Next, we create a javafx.scene.web.WebEngine
instance by passing in the url
into the constructor. The WebEngine
object will asynchronously load the web content from Yahoo’s weather service. Later we will discuss the callback method responsible for handling the content when the web content is done loading. The following code line will create and load a URL web address using a WebEngine
object:
final WebEngine webEngine = new WebEngine(url);
After you create a WebEngine
object, you will be creating an HTML document that will form as a template for later assembling when the web content is successfully loaded. Although the code contains HTML markup tags in Java code, which totally violates the principles of the separation of concerns, I inlined HTML by concatenating string values for brevity. To have a proper MVC-style separation, you may want to create a separate file containing your HTML content with substitution sections for data that will change over time. The code snippet that follows is the start of the creation of a template used to display weather information:
StringBuilder template = new StringBuilder();
template.append("<head>
")
.append("<style type="text/css">body {background-color:#b4c8ee;}</style>
")
.append("</head>
")
.append("<body id='weather_background'>");
Once you have created your web page by concatenating strings, you will create a WebView
object instance, which is a displayable graph node that will be responsible for rendering the web page. Remember from recipe 20-2, in which we discussed that a WebView
will have its own instance of a WebEngine
. Knowing this fact, we only use the WebView
node to render the assembled HTML web page, not to retrieve the XML weather information via a URL. In other words, the WebEngine
object is responsible for retrieving the XML from Yahoo’s Weather service to be parsed and then fed into the WebView
object to be displayed as HTML. The following code snippet instantiates a WebView
graph node that is responsible for rendering HTML5 content:
Next, you will create a countdown timer to refresh the weather information being displayed in the application window. First, you will instantiate an IntegerProperty
variable, countdown,
to hold the number of seconds until the next refresh time. Second, you will add a change listener (ChangeListener
) to update the HTML content dynamically using JavaFX’s capability to execute JavaScript. The change listener also will determine whether the countdown has reached zero. If so, it will invoke the webEngine
’s (WebEngine
) reload()
method to refresh or retrieve the weather information again. The following is the code that creates an IntegerProperty
value to update the countdown text within the HTML using the executeScript()
method:
IntegerProperty countDown = new SimpleIntegerProperty(refreshCountdown);
countDown.addListener(new ChangeListener<Number>() {
@Override
public void changed(ObservableValue<? extends Number> observable, Number oldValue,
Number newValue){
webView.getEngine().executeScript("document.getElementById('countdown').innerHTML =
'Seconds till refresh: " + newValue + "'");
if (newValue.intValue() == 0) {
webEngine.reload();
}
}
}); // addListener()
After implementing your ChangeListener
, you can create a TimeLine
object to cause change on the countdown
variable, thus triggering the ChangeListener
to update the HTML text depicting the seconds until refresh. The follow code implements a TimeLine
to update the countDown
variable:
final Timeline timeToRefresh = new Timeline();
timeToRefresh.getKeyFrames().addAll(
new KeyFrame(Duration.ZERO, new KeyValue(countDown, refreshCountdown)),
new KeyFrame(Duration.seconds(refreshCountdown), new KeyValue(countDown, 0))
);
In summary, the rest of the code creates a ChangeListener
that responds to a State.SUCCEEDED
. Once the webEngine
(WebEngine
) has finished retrieving the XML, the change listener (ChangeListener
) is responsible for parsing and rendering the assembled web page into the webView
node. The following code parses and displays the weather data by calling the loadContent()
method on the WebView
’s WebEngine
instance:
if (newValue != State.SUCCEEDED) {
return;
}
Weather weather = parse(webEngine.getDocument());
...// the rest of the inlined HTML
String countdownText = "<b id="countdown"></b><br />
";
webView.getEngine().loadContent(fullHtml + location.toString() +
timeOfWeatherTextDiv +
countdownText +
weather.htmlDescription);
To parse the XML returned by the webEngine
’s getDocument()
method, you will interrogate the org.w3c.dom.Document
object. For convenience, I created a parse()
method to walk the DOM to obtain weather data and return as a Weather
object. See Javadocs and Yahoo’s RSS XML Schema for more information on data elements returned from weather service.
You begin to feel sorry for your other cube mates who are also oblivious to the outside world. A storm is approaching and you want to let them know to take their umbrella before leaving the building.
Add a Panic Button to your weather application that will simulate an e-mail notification. A Calm Down button is also added to retract the warning message.
The following code implements the weather application with additional buttons to warn and disregard a warning of impending stormy weather:
@Override public void start(Stage stage) {
... // template building
This code will add HTML buttons with the onclick
attributes set to invoke the JavaScript alert
function:
template.append("<body id='weather_background'>");
template.append("<form>
");
template.append(" <input type="button" onclick="alert('warning')" value="Panic Button" />
");
template.append(" <input type="button" onclick="alert('unwarning')" value="Calm down" />
");
template.append("</form>
");
The following code is added to the start()
method to create the warning message with opacity set as zero to be invisible:
// calls the createMessage() method to build warning message
final Text warningMessage = createMessage(Color.RED, "warning: ");
warningMessage.setOpacity(0);
... // Countdown code
Continuing inside of the start()
method, this code section is added to update the warning message after weather information was retrieved successfully:
webEngine.getLoadWorker().stateProperty().addListener(new ChangeListener<State>() {
public void changed(ObservableValue<? extends State> observable, State oldValue, State newValue){
System.out.println("done!" + newValue.toString());
if (newValue != State.SUCCEEDED) {
return;
}
Weather weather = parse(webEngine.getDocument());
warningMessage.setText("Warning: " + weather.currentWeatherText + "
Temp: " + weather.temperature + "
E-mailed others");
... // the rest of changed() method
}); // end of addListener method
This code sets the OnAlert
property, which is an event handler to respond when a the Panic or Calm Down button is pressed:
webView.getEngine().setOnAlert(new EventHandler<WebEvent<String>>(){
public void handle(WebEvent<String> evt) {
warningMessage.setOpacity("warning".equalsIgnoreCase(evt.getData()) ? 1d : 0d);
}
}); // end of setOnAlert() method.
root.getChildren().addAll(webView, warningMessage);
stage.setScene(scene);
stage.show();
} // end of start() method
The following method is code that you will add as a private method that is responsible for creating a text node (javafx.scene.text.Text
) to be used as the warning message when the user presses the Panic Button:
private Text createMessage(Color color, String message) {
DropShadow dShadow = DropShadowBuilder.create()
.offsetX(3.5f)
.offsetY(3.5f)
.build();
Text textMessage = TextBuilder.create()
.text(message)
.x(100)
.y(50)
.strokeWidth(2)
.stroke(Color.WHITE)
.effect(dShadow)
.fill(color)
.font(Font.font(null, FontWeight.BOLD, 35))
.translateY(50)
.build();
return textMessage;
}
} // end of the RespondingToHtmlEvents class
Figure 20-10 shows our weather application displaying a warning message after the Panic Button has been pressed. To remove the warning message, you can press the Calm Down button.
Figure 20-10. Weather application displaying warning message
In this recipe you will add additional features to the weather application (from recipe 20-3) that responds to HTML events. The application you will be creating is similar to the previous recipe, except you will be adding HTML buttons on the web page to be rendered onto the WebView
node. The first button added is the Panic Button that, when pressed, displays a warning message stating the current weather condition and a simulated e-mail notification to your cube mates. To retract the warning message you will also add a Calm Down button.
Note Because the code is so similar to the previous recipe, I will point out the additions to the source code without going into great detail.
To add the buttons, you will use the HTML tag <input type=”button”…>
with an onclick
attribute set to use JavaScript’s alert()
function to notify JavaFX of an alert event. Shown here are the two buttons added to the web page:
StringBuilder template = new StringBuilder();
...// Header part of HTML Web page
template.append("<form>
");
template.append(" <input type="button" onclick="alert('warning')" value="Panic Button" />
");
template.append(" <input type="button" onclick="alert('unwarning')" value="Calm down" />
");
template.append("</form>
");
When the web page renders allowing you to press the buttons, the onclick
attribute will call JavaScript’s alert()
function that contains a string message. When the alert()
function is invoked, the web page’s owning parent (the webView
’sWebEngine
instance) will be notified of the alert via the WebEngine
’s OnAlert
attribute. To respond to JavaScript’s alerts, you will add an event handler (EventHandler
) to respond to WebEvent
objects. In the handle()
method, you will simply show and hide the warning message by toggling the opacity of the warningMessage
node (javafx.scene.text.Text
). The following code snippet toggles the opacity of the warning message based on comparing the event’s data (evt.getData()
) that contains the string passed in from the JavaScript’s alert()
function. So, if the message is “warning,” the warningMessage
opacity is set to 1; otherwise, set to 0 (both of type double
).
webView.getEngine().setOnAlert(new EventHandler<WebEvent<String>>(){
public void handle(WebEvent<String> evt) {
warningMessage.setOpacity("warning".equalsIgnoreCase(evt.getData()) ? 1d : 0d);
}
});
Please see the Javadocs for additional HTML web events (WebEvent
).
You want to keep up on the latest news monitoring the local legislature and science regarding the detrimental effects of the lack of light in small cubical work areas.
Create a JavaFX RSS reader. The RSS feed location URLs will be stored in a database to be later retrieved. Listed here are the main classes used in this recipe:
javafx.scene.control.Hyperlink
javafx.scene.web.WebEngine
javafx.scene.web.WebView
org.w3c.dom.Document
org.w3c.dom.Node
org.w3c.dom.NodeList
This recipe will be using an embedded database called Derby from the Apache group at http://www.apache.org
. As a requirement, you will need to download the Derby software. To download the software, visit http://db.apache.org/derby/derby_downloads.html
to download the latest version containing the libraries. Once downloaded, you can unzip or untar into a directory. To compile and run this recipe, you will need to update the classpath in your IDE or environment variable to point to Derby libraries (derby.jar
and derbytools.jar
). When running the example code you can type into the text field a valid RSS URL and then hit the enter key to load your new RSS headlines. After loading is complete the headline news is listed to the upper right frame region. Next, you will have an opportunity to choose a headline news article to read fully by clicking on a view button beneath it.
The following code implements an RSS reader in JavaFX:
package org.java7recipes.chapter20.recipe20_05;
import java.util.*;
import javafx.application.Application;
import javafx.beans.value.*;
import javafx.collections.ObservableList;
import javafx.concurrent.Worker.State;
import javafx.event.*;
import javafx.geometry.*;
import javafx.scene.*;
import javafx.scene.control.*;
import javafx.scene.input.*;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.web.*;
import javafx.stage.Stage;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
/**
* Display Contents From Database
* @author cdea
*/
public class DisplayContentsFromDatabase extends Application {
@Override public void start(Stage stage) {
Group root = new Group();
Scene scene = new Scene(root, 640, 480, Color.WHITE);
final Map<String, Hyperlink> hyperLinksMap = new TreeMap<>();
final WebView newsBrief = new WebView(); // upper right
final WebEngine webEngine = new WebEngine();
final WebView websiteView = new WebView(); // lower right
webEngine.getLoadWorker().stateProperty().addListener(new ChangeListener<State>() {
public void changed(ObservableValue<? extends State> observable, State oldValue, State newValue){
if (newValue != State.SUCCEEDED) {
return;
}
RssFeed rssFeed = parse(webEngine.getDocument(), webEngine.getLocation());
hyperLinksMap.get(webEngine.getLocation()).setText(rssFeed.channelTitle);
// print feed info:
StringBuilder rssSource = new StringBuilder();
rssSource.append("<head>
")
.append("</head>
")
.append("<body>
");
rssSource.append("<b>")
.append(rssFeed.channelTitle)
.append(" (")
.append(rssFeed.news.size())
.append(")")
.append("</b><br />
");
StringBuilder htmlArticleSb = new StringBuilder();
for (NewsArticle article:rssFeed.news) {
htmlArticleSb.append("<hr />
")
.append("<b>
")
.append(article.title)
.append("</b><br />")
.append(article.pubDate)
.append("<br />")
.append(article.description)
.append("<br />
")
.append("<input type="button" onclick="alert('")
.append(article.link)
.append("')" value="View" />
");
}
String content = rssSource.toString() + "<form>
" + htmlArticleSb.toString() + "</form></body>
";
System.out.println(content);
newsBrief.getEngine().loadContent(content);
// write to disk if not already.
DBUtils.saveRssFeed(rssFeed);
}
}); // end of webEngine addListener()
newsBrief.getEngine().setOnAlert(new EventHandler<WebEvent<String>>(){
public void handle(WebEvent<String> evt) {
websiteView.getEngine().load(evt.getData());
}
}); // end of newsBrief setOnAlert()
// Left and right split pane
SplitPane splitPane = new SplitPane();
splitPane.prefWidthProperty().bind(scene.widthProperty());
splitPane.prefHeightProperty().bind(scene.heightProperty());
final VBox leftArea = new VBox(10);
final TextField urlField = new TextField();
urlField.setOnAction(new EventHandler<ActionEvent>(){
public void handle(ActionEvent ae){
String url = urlField.getText();
final Hyperlink jfxHyperLink = createHyperLink(url, webEngine);
hyperLinksMap.put(url, jfxHyperLink);
HBox rowBox = new HBox(20);
rowBox.getChildren().add(jfxHyperLink);
leftArea.getChildren().add(rowBox);
webEngine.load(url);
urlField.setText("");
}
}); // end of urlField setOnAction()
leftArea.getChildren().add(urlField);
List<RssFeed> rssFeeds = DBUtils.loadFeeds();
for (RssFeed feed:rssFeeds) {
HBox rowBox = new HBox(20);
final Hyperlink jfxHyperLink = new Hyperlink(feed.channelTitle);
jfxHyperLink.setUserData(feed);
final String location = feed.link;
hyperLinksMap.put(feed.link, jfxHyperLink);
jfxHyperLink.setOnAction(new EventHandler<ActionEvent>() {
public void handle(ActionEvent evt) {
webEngine.load(location);
}
}
);
rowBox.getChildren().add(jfxHyperLink);
leftArea.getChildren().add(rowBox);
} // end of for loop
// Dragging over surface
scene.setOnDragOver(new EventHandler<DragEvent>() {
@Override
public void handle(DragEvent event) {
Dragboard db = event.getDragboard();
if (db.hasUrl()) {
event.acceptTransferModes(TransferMode.COPY);
} else {
event.consume();
}
}
}); // end of scene.setOnDragOver()
// Dropping over surface
scene.setOnDragDropped(new EventHandler<DragEvent>() {
@Override
public void handle(DragEvent event) {
Dragboard db = event.getDragboard();
boolean success = false;
HBox rowBox = new HBox(20);
if (db.hasUrl()) {
if (!hyperLinksMap.containsKey(db.getUrl())) {
final Hyperlink jfxHyperLink = createHyperLink(db.getUrl(), webEngine);
hyperLinksMap.put(db.getUrl(), jfxHyperLink);
rowBox.getChildren().add(jfxHyperLink);
leftArea.getChildren().add(rowBox);
}
webEngine.load(db.getUrl());
}
event.setDropCompleted(success);
event.consume();
}
}); // end of scene.setOnDragDropped()
leftArea.setAlignment(Pos.TOP_LEFT);
// Upper and lower split pane
SplitPane splitPane2 = new SplitPane();
splitPane2.setOrientation(Orientation.VERTICAL);
splitPane2.prefWidthProperty().bind(scene.widthProperty());
splitPane2.prefHeightProperty().bind(scene.heightProperty());
HBox centerArea = new HBox();
centerArea.getChildren().add(newsBrief);
HBox rightArea = new HBox();
rightArea.getChildren().add(websiteView);
splitPane2.getItems().add(centerArea);
splitPane2.getItems().add(rightArea);
// add left area
splitPane.getItems().add(leftArea);
// add right area
splitPane.getItems().add(splitPane2);
newsBrief.prefWidthProperty().bind(scene.widthProperty());
websiteView.prefWidthProperty().bind(scene.widthProperty());
// evenly position divider
ObservableList<SplitPane.Divider> dividers = splitPane.getDividers();
for (int i = 0; i < dividers.size(); i++) {
dividers.get(i).setPosition((i + 1.0) / 3);
}
HBox hbox = new HBox();
hbox.getChildren().add(splitPane);
root.getChildren().add(hbox);
stage.setScene(scene);
stage.show();
} // end of start()
private static RssFeed parse(Document doc, String location) {
RssFeed rssFeed = new RssFeed();
rssFeed.link = location;
rssFeed.channelTitle = doc.getElementsByTagName("title")
.item(0)
.getTextContent();
NodeList items = doc.getElementsByTagName("item");
for (int i=0; i<items.getLength(); i++){
Map<String, String> childElements = new HashMap<>();
NewsArticle article = new NewsArticle();
for (int j=0; j<items.item(i).getChildNodes().getLength(); j++) {
Node node = items.item(i).getChildNodes().item(j);
childElements.put(node.getNodeName().toLowerCase(), node.getTextContent());
}
article.title = childElements.get("title");
article.description = childElements.get("description");
article.link = childElements.get("link");
article.pubDate = childElements.get("pubdate");
rssFeed.news.add(article);
}
return rssFeed;
} // end of parse()
private Hyperlink createHyperLink(String url, final WebEngine webEngine) {
final Hyperlink jfxHyperLink = new Hyperlink("Loading News...");
RssFeed aFeed = new RssFeed();
aFeed.link = url;
jfxHyperLink.setUserData(aFeed);
jfxHyperLink.setOnAction(new EventHandler<ActionEvent>() {
public void handle(ActionEvent evt) {
RssFeed rssFeed = (RssFeed)jfxHyperLink.getUserData();
webEngine.load(rssFeed.link);
}
});
return jfxHyperLink;
} // end of createHyperLink()
public static void main(String[] args){
DBUtils.setupDb();
Application.launch(args);
}
}
class RssFeed {
int id;
String channelTitle = "News...";
String link;
List<NewsArticle> news = new ArrayList<>();
public String toString() {
return "RssFeed{" + "id=" + id + ", channelTitle=" + channelTitle + ", link=" + link + ", news=" + news + '}';
}
public RssFeed() {
}
public RssFeed(String title, String link) {
this.channelTitle = title;
this.link = link;
}
}
class NewsArticle {
String title;
String description;
String link;
String pubDate;
public String toString() {
return "NewsArticle{" + "title=" + title + ", description=" + description + ", link=" + link + ", pubDate=" + pubDate + ", enclosure=" + '}';
}
}
The following code is an exerpt from DBUtils.java showing the saveRssFeed()
method which is responsible for persisting RSS feeds:
public static int saveRssFeed(RssFeed rssFeed) {
int pk = rssFeed.link.hashCode();
loadDriver();
Connection conn = null;
ArrayList statements = new ArrayList();
PreparedStatement psInsert = null;
Statement s = null;
ResultSet rs = null;
try {
// database name
String dbName = "demoDB";
conn = DriverManager.getConnection(protocol + dbName
+ ";create=true", props);
rs = conn.createStatement().executeQuery("select count(id) from rssFeed where id = " + rssFeed.link.hashCode());
rs.next();
int count = rs.getInt(1);
if (count == 0) {
// handle transaction
conn.setAutoCommit(false);
s = conn.createStatement();
statements.add(s);
psInsert = conn.prepareStatement("insert into rssFeed values (?, ?, ?)");
statements.add(psInsert);
psInsert.setInt(1, pk);
String escapeTitle = rssFeed.channelTitle.replaceAll("'", "''");
psInsert.setString(2, escapeTitle);
psInsert.setString(3, rssFeed.link);
psInsert.executeUpdate();
conn.commit();
System.out.println("Inserted " + rssFeed.channelTitle + " " + rssFeed.link);
System.out.println("Committed the transaction");
}
shutdown();
} catch (SQLException sqle) {
sqle.printStackTrace();
} finally {
// release all open resources to avoid unnecessary memory usage
// ResultSet
close(rs);
// Statements and PreparedStatements
int i = 0;
while (!statements.isEmpty()) {
// PreparedStatement extend Statement
Statement st = (Statement) statements.remove(i);
close(st);
}
//Connection
close(conn);
}
return pk;
}
In Figure 20-11, our JavaFX reader displays three frames. The left column shows a text field at the top to allow the user to enter new urls and RSS feed sources as hyperlinks underneath. The upper-right frame contains the headline, an excerpt of the article, and a view button that renders the article’s web page in the bottom frame (lower-right region).
Figure 20-11. JavaFX RSS reader
Shown here is an example of output of the HTML to be rendered in the new headlines region (upper-right frame). You will also see the html view button responsible for notifying the application to load and render the entire article in the lower right frame region:
<head>
</head>
<body>
<b>Carl's FX Blog (10)</b><br />
<form>
<hr />
<b>
JavaFX Forms Framework Part 2</b><br />Mon, 03 Aug 2009 18:36:02 +0000<br />Introduction
This is the second installment of a series of blog entries relating to a proof of concept
for a JavaFX Forms Framework. Before I specify the requirements and a simple design of the
FXForms Framework, I want to follow-up on comments about tough issues relating to enterprise
application development and JavaFX. If you recall [...]<img alt="" border="0"
src="http://stats.wordpress.com/b.gif?host=carlfx.wordpress.com&blog=6443320&post=33
9&subd=carlfx&ref=&feed=1" width="1" height="1" /><br />
<input type="button" onclick="alert('http://carlfx.wordpress.com/2009/08/03/javafx-forms-
framework-part-2/')" value="View" />
... // the rest of the headlines
</form></body>
To create an RSS reader, you will need to store feed locations for later reading. When adding a new RSS feed, you will want to locate the little orange iconic button and drag the URL address line into your JavaFX RSS reader application. I find that the drag metaphor works on my FireFox browser. However, if dragging doesn’t work I’ve provided a text field to allow you to cut-and-paste the URL. Once the URL is entered you will hit the enter key to initiate the loading of the headline news. For example you can visit Google’s technology news RSS at:
http://news.google.com/news?pz=1&cf=all&ned=us&hl=en&topic=tc&output=rss.
Figure 20-12 depicts the orange RSS icon in the upper left.
Figure 20-12. RSS icon
Once the URL is accepted via drag-n-drop or text field, the JavaFX RSS reader application will save the URL location to a database. The RSS application consists of three frame regions: the RSS feed title column (left), headline news (upper right), and web site view (lower right). To display the news headlines, click the hyperlinks to the left. To show the entire article in the lower-right frame, click the View button below the headline in the upper-right frame. Before running the code, the application will require the jar libraries derby.jar and derbytools.jar included into your project classpath. These libraries allow you to save RSS URLs to an embedded JDBC database.
Similar to what you did in recipe 20-3, you retrieve news information from the Internet. The RSS retrieved will be using version 2.0. RSS is an XML standard providing really simple syndication, thus the acronym RSS. Now enough with the acronyms; let’s jump into the code, shall we?
In our start()
method, you will create a 640 by 480 white scene display area. Next, you will create a map (TreeMap
) containing Hyperlink
objects as values and keys representing the URL location (String
) to the RSS feed. As before when displaying HTML content, you will need to create WebView
s. Here you will create two WebView
s and one WebEngine
. The two WebView
s will render HTML for the news headline frame region and the viewing of the entire article region (lower right). The single WebEngine
is responsible for retrieving the RSS feed when the user clicks the left frame region containing the RSS hyperlinks.
To support the feature that allows the user to enter an RSS feed you will need to create a text field that is able to save and render the headline news. Below is the code snippet to save an RSS URL and to add an address as a new hyperlink to the list of feeds.
final VBox leftArea = new VBox(10);
final TextField urlField = new TextField();
urlField.setOnAction(new EventHandler<ActionEvent>(){
public void handle(ActionEvent ae){
String url = urlField.getText();
final Hyperlink jfxHyperLink = createHyperLink(url, webEngine);
hyperLinksMap.put(url, jfxHyperLink);
HBox rowBox = new HBox(20);
rowBox.getChildren().add(jfxHyperLink);
leftArea.getChildren().add(rowBox);
webEngine.load(url);
urlField.setText("");
}
}); // end of urlField setOnAction()
After a user has clicked on a hyperlink the news retrieval is initiated. Once a successful retrieve has occurred on the webEngine
(WebEngine
) object, you will need to add a ChangeListener
instance to respond when the state property changes to State.SUCCEEDED
. With a valid state of State.SUCCEEDED,
you will begin to parse the XML DOM returned from the WebEngine
’s getDocument()
method. Again, I provided a convenience method called parse()
to interrogate the Document
object representing the RSS news information.
RssFeed rssFeed = parse(webEngine.getDocument(), webEngine.getLocation());
Next, you will create an HTML page that will list the channel tile and the number of total news headlines returned. After creating the HTML to display the RSS channel title and number of articles, you will iterate over all the news headlines to build record sets or rows. Each row will contain an HTML button labeled View to notify the WebEngine
object of an alert containing the URL of the article. When the WebEngine
object is notified, the OnAlert
property will contain an event handler to render the entire article in the frame in the lower-right split region. After the web page is assembled, you will call the newsBrief
object’s getEngine().loadContent()
method to render the page. Once rendered you will save the URL rss Feed
(RssFeed
) object to the database by invoking the DBUtils.saveRssFeed(rssFeed)
. As a convenience, the saveRssFeed()
method will check for duplicates and not save them. The following code loads the web page to be rendered and saves the newly added rss Feed
URL:
newsBrief.getEngine().loadContent(content);
// write to disk if not already.
DBUtils.saveRssFeed(rssFeed);
As in the previous recipes, you will be responding to HTML WebEvent
s when the new headline View button is pressed, which calls a JavaScript’s alert()
function. Shown following is the code snippet to handle a web event (WebEvent
) containing a string of the URL that links to the entire article to be viewed in the frame to the lower right region:
newsBrief.getEngine().setOnAlert(new EventHandler<WebEvent<String>>(){
public void handle(WebEvent<String> evt) {
websiteView.getEngine().load(evt.getData());
}
});
When creating the headlines region (upper right) containing HTML buttons to render the article’s web page, you will notice the alert()
function containing the URL to be loaded and rendered in the lower bottom split frame region. Shown following is an example of HTML generated for an headline news containing a View button that can notify the web engine’s OnAlert
web event (WebEvent
).
<input type="button" onclick="alert('http://carlfx.wordpress.com/2009/08/03/javafx-forms-
framework-part-2/')" value="View" />
One last thing to point out is that the RSS application has missing features. One feature that comes to my mind is the ability to delete individual RSS hyperlinks on the left column region. A workaround is to remove all links by deleting the database on the file system. Because Derby is an embedded database, you can delete the directory containing the database. The JavaFX RSS application will re-create an empty database if one doesn’t exist. Hopefully, you can add new features to enhance this fun and useful application.