Jump into JavaFX, Part 4: The advanced APIs

how-to
Apr 7, 200930 mins

Explore the JavaFX media, GUI construction, and effects APIs

Jeff Friesen continues his comprehensive introduction to the JavaFX APIs, based on JavaFX Preview SDK, with a look at how JavaFX handles media, GUI construction, and effects. You’ll also try your hand at building a stock-ticker application, which you’ll then deploy as an applet to the Google Chrome browser. Level: Intermediate

In the previous article in this series I introduced you to the basic JavaFX APIs: javafx.lang, javafx.util, and javafx.animation. I also discussed nodes, shapes, and images. In this article I’ll build on that discussion by introducing the JavaFX APIs for handling media, constructing GUIs (via Swing components, groups, layout managers, and custom nodes), and creating sophisticated graphic effects. I’ll conclude with a hands-on exercise in creating a stock ticker application and deploying it (via the Java Deployment Toolkit) as an applet to the Google Chrome browser.

Note that the discussion in this article is based on the JavaFX APIs in the JavaFX Preview SDK, which will differ somewhat from the APIs in JavaFX SDK 1.x. What you learn here will serve as background for my first look at JavaFX SDK 1.0 in the final two articles of this series.

Media

Rich internet applications need to support video and audio content, a fact that Sun is addressing via its Java Media Components (JMC) project. For starters, JMC consists of a media player API, which allows you to play video and audio in a variety of formats. Eventually, JMC will also offer APIs for capturing video and audio, and for providing other capabilities.

JavaFX exposes the media player API to scripts via the javafx.scene.media package’s five classes:

  • Media represents a media resource, and contains information regarding the media’s source URI, resolution width and height, and more.
  • MediaError describes the various errors that can occur while trying to play a media resource.
  • MediaPlayer provides the means for playing and pausing media, responding to various media events, and more.
  • MediaTimer specifies a time and an action to be performed when the playing media reaches this time.
  • MediaView is a Node subclass that provides a transformable (depending on media support for transformations) view of playing media.

A simple player begins with a Media object whose source attribute is initialized to a target URI. This object is assigned to a MediaPlayer object’s media attribute, and the MediaPlayer object is assigned to a MediaView object’s mediaPlayer attribute. These relationships are illustrated in Listing 1’s BasicPlayer.fx source code.

Listing 1. BasicPlayer.fx

/*
 * BasicPlayer.fx
 *
 */

package apidemo5;

/**
 * @author Jeff Friesen
 */

import java.lang.System;

import javafx.application.Frame;
import javafx.application.Stage;

import javafx.input.MouseEvent;

import javafx.scene.media.Media;
import javafx.scene.media.MediaPlayer;
import javafx.scene.media.MediaView;

import javafx.scene.paint.Color;

Frame
{
    title: "BasicPlayer"

    width: 400
    height: 300

    var mediaURI = "https://swinghelper.dev.java.net/bin/blog/sam/jfx/movie.avi";

    var skip = false; // skip extra onMouseClicked() invocation when true

    var mediaPlayerRef: MediaPlayer on replace
    {
        if (mediaPlayerRef != null)
            mediaPlayerRef.paused = true // the initial paused value is false
    }

    var stageRef: Stage
    stage: stageRef = Stage
    {
        fill: Color.BLACK

        var mediaViewRef: MediaView
        content: mediaViewRef = MediaView
        {
            mediaPlayer: mediaPlayerRef = MediaPlayer
            {
                media: Media
                {
                    source: mediaURI
                }
            }

            onMouseClicked: function (me: MouseEvent): Void
            {
                if (skip)
                {
                    skip = false;
                    return
                }

                skip = true;

                if (mediaPlayerRef.paused)
                    mediaPlayerRef.play ()
                else
                    mediaPlayerRef.pause ()
            }

            translateX: bind (stageRef.width-mediaViewRef.getWidth ())/2
            translateY: bind (stageRef.height-mediaViewRef.getHeight ())/2
        }
    }

    closeAction: function ()
    {
        mediaPlayerRef.pause ();
        System.exit (0)
    }

    visible: true
}

Listing 1 initializes a mediaURI variable to the URI of a short space shuttle movie — this variable is subsequently assigned to a Media object’s source attribute. This listing also introduces a skip variable to overcome a problem with mouse event-handling functions attached to a MediaView component — each function is invoked twice for each mouse event.

More about JavaFX

Learn more about JavaFX and other client-side technologies with Jeff’s new weekly blog: Client-side Java Explorer.

I’ve attached a replace trigger to the mediaPlayerRef variable to deal with another problem (which may or may not be regarded as a bug). The MediaPlayer class provides a paused attribute that’s only set to true when playing media is paused. Because this attribute initializes to false, I use the trigger to initialize it to true, which makes paused more useful.

After loading the movie’s AVI file (which takes a little time), the script displays the movie’s first frame centered on the stage (see Figure 1). Clicking this frame starts the movie by invoking MediaPlayer‘s play() function. The next mouse click pauses the movie by invoking the pause() function. A third click resumes the movie from the point where it was paused.

If you’d like to experiment with BasicPlayer, insert autoPlay: true into the MediaPlayer object literal. In response, the video should begin playing when the script starts running without a click to the starting frame. If you want to double the playback rate and have the announcer sound like a chipmunk, insert rate: 2.0 into the MediaPlayer literal.

Swing components

JavaFX’s javafx.ext.swing package provides classes for implementing Swing-based GUIs. To play nicely with nodes, a component hierarchy based on these classes must be assigned to the component attribute of a ComponentView instance, which is assigned to the content attribute of a Stage instance, which is assigned to Frame‘s stage attribute. Listing 2 presents an example.

Listing 2. TempConverter.fx

/*
 * TempConverter.fx
 *
 */

package apidemo6;

/**
 * @author Jeff Friesen
 */

import javafx.application.Frame;
import javafx.application.Stage;

import javafx.ext.swing.Button;
import javafx.ext.swing.Component;
import javafx.ext.swing.ComponentView;
import javafx.ext.swing.FlowPanel;
import javafx.ext.swing.GridPanel;
import javafx.ext.swing.Label;
import javafx.ext.swing.TextField;

import javafx.scene.HorizontalAlignment;

Frame
{
    stage: Stage
    {
        content: ComponentView
        {
            component: GridPanel
            {
                columns: 1
                rows: 3

                var input: TextField;
                var result: Label
                content:
                [
                    FlowPanel
                    {
                        content:
                        [
                            Label
                            {
                                text: "Degrees"
                            },
                            input = TextField
                            {
                                columns: 10
                            }
                        ]
                    },
                    result = Label
                    {
                        text: " "
                        horizontalAlignment: HorizontalAlignment.CENTER
                    },
                    FlowPanel
                    {
                        content:
                        [
                            Button
                            {
                                text: "To Celsius"

                                action: function (): Void
                                {
                                    var val = input.text;
                                    var n = java.lang.Double.parseDouble (val);
                                    var res = (n-32.0)*5.0/9.0;
                                    result.text = "Result = {res}"
                                }
                            },
                            Button
                            {
                                text: "To Fahrenheit"

                                action: function (): Void
                                {
                                    var val = input.text;
                                    var n = java.lang.Double.parseDouble (val);
                                    var res = n*9.0/5.0+32.0;
                                    result.text = "Result = {res}"
                                }
                            }
                        ]
                    }
                ]
            }

            scaleX: 2.0
            scaleY: 2.0
        }
    }

    visible: true
}

This script in Listing 2 demonstrates various javafx.ext.swing classes. For example, it shows you how to work with the Button, Label, and TextField component classes (notice the absence of a J prefix), along with the GridPanel and FlowPanel hybrid container/layout manager classes.

The resulting GUI lets you input a temperature value via a textfield, and convert it to degrees Celsius or Fahrenheit via a pair of buttons. The conversion result is displayed above these buttons via a centered label. Figure 2 reveals this GUI, which is scaled up thanks to ComponentView being a subclass of Node.

Group

Just as the AWT and Swing use containers to organize components into hierarchies, JavaFX uses groups (instances of the javafx.scene.Group class, which is a Node subclass) to arrange nodes into hierarchies. Nodes (including groups) that are placed into a group share the group’s coordinate space, and can be transformed as if they were a single node, which Listing 3 demonstrates.

Listing 3. DraggableGroup.fx

/*
 * DraggableGroup.fx
 *
 */

package apidemo7;

/**
 * @author Jeff Friesen
 */

import javafx.application.Frame;
import javafx.application.Stage;

import javafx.input.MouseEvent;

import javafx.scene.Group;

import javafx.scene.geometry.Circle;
import javafx.scene.geometry.Rectangle;

import javafx.scene.paint.Color;

Frame
{
    width: 300
    height: 300

    stage: Stage
    {
        fill: Color.BLACK

        content:
        [
            Group
            {
                var beginX = 0.0;
                var beginY = 0.0;
                var endX = 0.0;
                var endY = 0.0;

                content:
                [
                    Rectangle
                    {
                        x: 0
                        y: 0
                        width: 50
                        height: 50
                        fill: Color.ORANGE
                    },
                    Circle
                    {
                        centerX: 25
                        centerY: 25
                        radius: 15
                        fill: Color.BLUE
                    }
                ]

                onMousePressed: function (e: MouseEvent): Void
                {
                    beginX = e.getDragX ()-endX;
                    beginY = e.getDragY ()-endY
                }

                onMouseDragged: function (e:MouseEvent): Void
                {
                    endX = e.getDragX ()-beginX;
                    endY = e.getDragY()-beginY
                }

                translateX: bind endX
                translateY: bind endY
            }
        ]
    }

    visible: true
}

Listing 3 specifies a scene consisting of a solid blue circle centered on top of a solid orange rectangle. Both nodes are placed into a single group by adding them to Group‘s content sequence attribute. The script attaches anonymous functions to Group‘s onMousePressed and onMouseDragged attributes that allow you to drag the group around the window.

Layout managers

Swing provides layout managers for laying out a GUI’s components in various ways. In a similar fashion, JavaFX provides layout managers in the form of the javafx.scene.layout package’s HBox and VBox classes, for laying out nodes in horizontal and vertical directions. Listing 4 provides a script that demonstrates these layout manager classes.

Listing 4. CircleGrid.fx

/*
 * CircleGrid.fx
 *
 */

package apidemo8;

/**
 * @author Jeff Friesen
 */

import java.lang.Math;

import javafx.application.Frame;
import javafx.application.Stage;

import javafx.scene.geometry.Circle;

import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;

import javafx.scene.paint.Color;

class Model
{
    attribute radius: Integer;
    attribute hspacing: Integer;
    attribute vspacing: Integer;

    function makeCircle (): Circle
    {
        Circle
        {
            centerX: 0
            centerY: 0
            radius: radius
            translateX: radius
            translateY: radius
            strokeWidth: 0
            fill: Color.rgb (rnd (256), rnd (256), rnd (256))
        }
    }

    function rnd (limit: Integer): Integer
    {
        Math.random ()*limit as Integer
    }
}

Frame
{
    var model = Model
    {
        radius: 10
        hspacing: 5
        vspacing: 5
    }

    width: 300
    height: 300

    var stageRef: Stage
    stage: stageRef = Stage
    {
        fill: Color.BLACK

        var vboxRef: VBox
        content:
        [
            vboxRef = VBox
            {
                spacing: bind model.vspacing

                translateY: bind (stageRef.height-vboxRef.getHeight ())/2

                content: bind
                [
                    for (row in [1..stageRef.height/(2*model.radius+
                                    model.vspacing)])
                         HBox
                         {
                             spacing: bind model.hspacing

                             translateX: bind (stageRef.width-
                                              vboxRef.getWidth ())/2

                             content: bind
                             [
                                 for (col in [1..stageRef.width/(2*model.radius+
                                                 model.hspacing)])
                                      model.makeCircle ()
                             ]
                         }
                ]
            }
        ]
    }

    visible: true
}

This script creates and displays a grid of colored circles (see Figure 3). The Model class provides attributes for specifying each circle’s radius, the amount of horizontal spacing between each column, and the amount of vertical spacing between each row. These attributes and the window’s width and height determine the total number of circles that are displayed in the grid.

A grid of circles.
Figure 3. A grid of circles.

The Model class also provides a makeCircle() function for creating a randomly colored Circle node. When the grid is first displayed, this function is called once for each column of each row. When the window is enlarged, this function is only called for new circles that need to be created, to fill in the extra space — if a circle doesn’t change color during a resize, it probably hasn’t been recreated.

The grid is created as a VBox group containing HBox groups, with each of these groups containing a row of Circle nodes. Each layout class’s spacing attribute specifies the amount of space appearing between successive circles vertically and horizontally, and these attributes are bound to equivalent model spacing attributes — change the model attributes and the grid also changes.

Effects

The javafx.scene.effect package provides an abstract Effect class, and assorted subclasses that describe visual effects for giving your scenes a polished look (think iPhone). For example, the GaussianBlur class provides a blur effect based on a Gaussian convolution kernel and a radius. Also, the Reflection class adds a reflection below the content being reflected. Figure 4 illustrates these classes.

Figure 4 reveals three side-by-side instances of what I like to call owl eyes. Each eye’s somewhat realistic white highlight was created via a Gaussian blur, and a reflection was used to mirror the eye. Also, a gold-to-black horizontal gradient was used to achieve a subtle 3D effect, where each eye appears slightly in front of the eye to its left. Listing 5 presents the script that created this illustration.

Listing 5. ReflectedOwlEyes.fx

/*
 * ReflectedOwlEyes.fx
 *
 */

package apidemo9;

/**
 * @author Jeff Friesen
 */

import javafx.application.Frame;
import javafx.application.Stage;

import javafx.scene.Group;
import javafx.scene.Node;

import javafx.scene.effect.GaussianBlur;
import javafx.scene.effect.Reflection;

import javafx.scene.geometry.Circle;

import javafx.scene.paint.Color;
import javafx.scene.paint.LinearGradient;
import javafx.scene.paint.Stop;

Frame
{
    width: 210
    height: 210

    stage: Stage
    {
        fill: Color.BLACK

        content:
        [
            for (i in [0..2])
                 Group
                 {
                     effect: Reflection
                     {
                         fraction: 1.0
                     }

                     content:
                     [
                         Circle
                         {
                             centerX: 50
                             centerY: 50
                             radius: 30
                             fill: LinearGradient
                             {
                                 startX: 0.0
                                 startY: 0.0
                                 endX: 1.0
                                 endY: 0.0
                                 stops:
                                 [
                                     Stop
                                     {
                                         offset: 0.0
                                         color: Color.GOLD
                                     },
                                     Stop
                                     {
                                         offset: 1.0
                                         color: Color.BLACK
                                     }
                                 ]
                             }
                         },
                         Circle
                         {
                             centerX: 50
                             centerY: 50
                             radius: 12
                             fill: Color.BLACK
                         },
                         Circle
                         {
                             centerX: 60
                             centerY: 45
                             radius: 6
                             fill: Color.WHITE
                             effect: GaussianBlur
                             {
                                 radius: 6
                             }
                         }
                     ]

                     translateX: i*50
                 }
        ]
    }

    visible: true
}

This script creates a scene out of three Group objects, each containing three Circle objects. Because Group and Circle inherit Node‘s effect attribute, which is of type Effect, it’s easy to attach a Reflection-based reflection to each group, and a GaussianBlur-based blur to each group’s small white circle.

GaussianBlur presents a radius attribute that lets you specify the radius of the blur kernel. You can specify any value from 1.0 through 63.0 — 10.0 is the default. Similarly, Reflection presents a fraction attribute that lets you specify how much of the image is reflected. You can specify any value from 0.0 (none) through 1.0 (all) — 0.75 is the default.

While exploring Reflection, you’ll discover additional attributes (such as topOffset — specify the distance between the bottom of the reflected image and the top of its reflection; the default is 0.0). You’ll also discover that most of Effect‘s subclasses have an input attribute (of type Effect). This attribute lets you attach a chain of effects to a node, feeding an effect’s output to the next effect, as follows:

effect: Reflection
{
    input: Glow
    {
    }

    fraction: 1.0

    topOffset: 5.0
}

This example demonstrates effects chaining by first applying a glow effect to a node’s content (keeping the default glow level of 0.3), and then by applying a reflection effect to the “glowing eyes.” Back in Listing 5, if you replace Group‘s effect attribute with this example, and introduce an import statement for the Glow class, you’ll end up with the brighter image that’s shown in Figure 5.

CustomNode

Earlier I introduced the javafx.ext.swing package. Although you can work with this package’s classes to create Swing-based GUIs, these GUIs are restricted to the desktop. To also support mobile devices, televisions, and so on in a portable manner, you’ll need to create node-based GUIs.

Although the Preview SDK doesn’t contain any node-based components, there’s no reason why you can’t create your own, apart from the possibility that some of them will be redundant once the JavaFX team’s suite of node-based components is released. (JavaFX 1.0 introduces a single node-based TextBox component.)

The next three sections show you how to create your own node-based components, by subclassing the abstract javafx.scene.CustomNode class and overriding its protected abstract create(): Node function, and how to use these components.

Profiles

To better organize the JavaFX APIs, the JavaFX team has introduced the concept of profiles (groups of classes that are only available to certain platforms/devices). Many of the JavaFX API classes belong to the common profile. This profile’s classes are available to any kind of device, and include the Frame and Stage classes that I’ve been using in the various API-based scripts. In contrast, the javafx.ext.swing package’s classes belong to the desktop profile, and can only be used in a desktop environment.

Revisiting shapes

In the first article in this series, I demonstrated Project Nile’s SVG Converter tool to convert a shapes.svg Scalable Vector Graphics file’s XML content to a shapes.fx file. This file introduces a shapes class that extends CustomNode and overrides the create() function. Because I promised to show you how to interact with shapes.fx, examine Listing 6.

Listing 6. ViewShapes.fx

/*
 * ViewShapes.fx
 */

package apidemo10;

/**
 * @author Jeff Friesen
 */

import java.lang.System;

import javafx.application.Frame;
import javafx.application.Stage;

Frame
{
    title: "View Shapes"

    width: 700
    height: 800

    stage: Stage
    {
        content: shapes {}
    }

    closeAction: function ()
    {
        System.exit (0)
    }

    visible: true
}

Although it’s easy to access shapes, more work is required to prepare shapes.fx for access. You must replace that file’s package result; statement with package apidemo10;, introduce public attribute defs4: Group; and public attribute metadata7: Group; attribute definitions into the shapes class, and comment out the fillRule attribute initializers in a pair of Rectangle object literals.

To experiment with ViewShapes, start NetBeans, use the New Project wizard to introduce an APIDemo10 project with apidemo10.ViewShapes as the main file, and replace the skeletal ViewShapes.fx‘s // place your code here line with the code in Listing 6. Also, copy my version of shapes.fx (see this article’s code archive) into the same NetBeans project directory as ViewShapes.fx.

Build and run this project, and you’ll see most of the graphic that appears in Figure 13 from “Jump into JavaFX, Part 1.”

A custom text-scrolling component

Next, let’s create a CustomNode subclass that introduces a common profile component for scrolling a line of text horizontally from right to left. This subclass will have attributes for setting its text, location and dimensions, text style (including an effect), and animation delay (for determining the scroll speed). It will also have functions for starting and stopping the scroll. Listing 7 presents the source code.

Listing 7. TextScroller.fx

/*
 * TextScroller.fx
 *
 */

package apidemo11;

/**
 * @author Jeff Friesen
 */

import java.lang.StringBuffer;

import javafx.animation.Timeline;
import javafx.animation.KeyFrame;

import javafx.scene.CustomNode;
import javafx.scene.Group;
import javafx.scene.Node;

import javafx.scene.effect.Effect;

import javafx.scene.geometry.Rectangle;

import javafx.scene.paint.Paint;

import javafx.scene.Font;

import javafx.scene.text.Text;
import javafx.scene.text.TextOrigin;

public class TextScroller extends CustomNode
{
    public attribute text: String on replace o=n
    {
        if (n == "#reverse")
        {
            var sb = new StringBuffer (o);
            text = sb.<<reverse>> ().toString ();
        }

        if (isScrolling ())
            textRef.translateX = 0.0
    }

    public attribute x: Number;
    public attribute y: Number;
    public attribute width: Number;
    public attribute height: Number;

    public attribute font: Font;
    public attribute stroke: Paint;
    public attribute strokeWidth: Number;
    public attribute fill: Paint;
    public attribute _effect: Effect;

    public attribute delay = 20ms;

    public function create(): Node
    {
        Group
        {
            content:
            [
                textRef = Text
                {
                    content: bind text
                    textOrigin: TextOrigin.TOP
                    x: bind x+width
                    y: bind y+(height-textRef.getHeight ())/2
                    font: font
                    fill: fill
                    stroke: stroke
                    strokeWidth: strokeWidth
                    effect: _effect
                    cache: true
                }
            ]

            clip: Rectangle
            {
                x: x
                y: y
                width: bind width
                height: bind height
            }
        }
    }

    public function isScrolling ()
    {
        timeline.running
    }

    public function startScrolling ()
    {
       timeline.start ()
    }

    public function stopScrolling ()
    {
        timeline.stop ()
    }

    private attribute timeline = Timeline
    {
        repeatCount: Timeline.INDEFINITE

        keyFrames:
        [
            KeyFrame
            {
                time: delay

                action: function ()
                {
                    if (textRef.translateX+textRef.getWidth ()+width < 0)
                    {
                        textRef.translateX = 0.0;
                        return
                    }

                    textRef.translateX -= 0.5
                }
            }
        ]
    }

    private attribute textRef: Text
}

TextScroller specifies several attributes starting with text, which identifies a line of text to scroll. This attribute’s replace trigger reverses its current value when the trigger detects an attempt to assign #reverse to the attribute. The trigger also resets the scroll position to its initial value. (Notice the doubled angle brackets surrounding reverse, which is a keyword.)

The x, y, width, and height attributes identify the region in which the text is scrolled. The font, stroke, strokeWidth, fill, and _effect attributes control the style of the text. Finally, the delay attribute determines the scrolling speed — a smaller value indicates faster scrolling.

TextScroller‘s create() function is called by the JavaFX runtime when instantiating the component. By convention, create() returns a Group node that specifies the component in terms of a nodes sequence. (Nodes are stacked in a Z-order; the last node in the sequence is painted last.) Because our component is basic, this sequence contains only a single Text node.

The Text object literal’s content attribute binds to TextScroller‘s text attribute, allowing existing text to be replaced with new text while the scroller runs. Similarly, x and y are bound to expressions based on width and height, allowing x and y to be updated when any expressions (possibly including stageRef.width and stageRef.height, revealed later) bound to width and height are updated.

The JavaFX API documentation for Node‘s cache attribute states that it determines if the node should be cached as a bitmap. Setting this attribute to true (the default value is false) enables caching, and a much smoother scrolling experience — comment out this attribute and you’ll soon see how jerky the horizontal movement becomes.

To perform horizontal scrolling, the text node is first positioned to the immediate right of the area in which it’s scrolled — this is why width is added to x in Text‘s x: bind x+width expression. Because this text is visible, possibly overwriting part of the GUI, it’s hidden outside the scrolling area by assigning the area as a clip region to the group’s clip attribute.

Scrolling is performed by a timeline, which is externally accessible via the isScrolling(), startScrolling(), and stopScrolling() functions. The timeline runs continually, and consists of a single key frame whose trigger function is invoked every delay milliseconds. This function performs scrolling by transforming the text node in small steps to the left.

At some point, the text will completely disappear to the left of the scrolling area. When this happens, I’ve elected to reset the scroll position to its initial value by assigning 0.0 to the translateX attribute. In reality, the text node is never actually moved — it retains its original location to the right of the scrolling area. Movement takes place via repeated translations.

TextScrollerDemo

I’ve assigned TextScroller‘s source code to its own file, to make this code easier to maintain. Although it would be easier to reuse this class if it were placed in its own package, I’ve kept TextScroller in the same apidemo11 package as a TextScrollerDemo demo script for simplicity. TestScrollerDemo‘s source code appears in Listing 8.

Listing 8. TextScrollerDemo.fx

/*
 * TextScrollerDemo.fx
 *
 */

package apidemo11;

/**
 * @author Jeff Friesen
 */

import java.lang.System;

import javafx.application.Frame;
import javafx.application.Stage;

import javafx.input.MouseEvent;

import javafx.scene.Font;

import javafx.scene.effect.DropShadow;

import javafx.scene.geometry.Rectangle;

import javafx.scene.paint.Color;
import javafx.scene.paint.LinearGradient;
import javafx.scene.paint.Stop;

Frame
{
    var textScrollerRef: TextScroller

    title: "TextScrollerDemo"

    width: 600
    height: 200

    var stageRef: Stage
    stage: stageRef = Stage
    {
        fill: LinearGradient
        {
            startX: 0.0
            startY: 0.0
            endX: 0.0
            endY: 1.0
            stops:
            [
                Stop { offset: 0.0 color: Color.DARKGRAY },
                Stop { offset: 1.0 color: Color.ORANGE }
            ]
        }

        content:
        [
            textScrollerRef = TextScroller
            {
                text: "This text scroller only scrolls text from left to right."

                x: 30.0
                y: 30.0
                width: bind stageRef.width-60.0
                height: bind stageRef.height-60.0

                font: Font
                {
                    name: "Arial Black"
                    size: 48
                }
                stroke: Color.BLUE
                strokeWidth: 1.0
                fill: Color.LIGHTBLUE

                _effect: DropShadow
                {
                    offsetX: 5.0
                    offsetY: 5.0
                    radius: 5.0
                }
            },
            Rectangle
            {
                x: bind textScrollerRef.x
                y: bind textScrollerRef.y
                width: bind textScrollerRef.width
                height: bind textScrollerRef.height

                stroke: Color.BLACK
                strokeWidth: 10.0
                arcWidth: 5.0
                arcHeight: 5.0
                fill: Color.rgb (255, 255, 255, 0.0)

                onMouseClicked: function (me: MouseEvent): Void
                {
                    if (not textScrollerRef.isScrolling ())
                        textScrollerRef.startScrolling ()
                    else
                        textScrollerRef.text = "#reverse"
                }
            }
        ]
    }

    closeAction: function ()
    {
        textScrollerRef.stopScrolling ();
        System.exit (0)
    }

    visible: true
}

This script introduces a window whose stage consists of text scroller and rectangle components. It attaches an anonymous function to the rectangle’s onMouseClicked attribute that either initiates scrolling or reverses what’s being scrolled when you click anywhere on the rectangle. The scrolling operation stops when the window closes.

The script demonstrates the DropShadow effect class, for rendering a node’s shadow behind the node. This class offers a color attribute for specifying the shadow’s color, offsetX and offsetY attributes for specifying an offset from the node to the shadow in the X and Y directions, and a radius attribute for specifying the radius of the shadow’s blur kernel.

Create an APIDemo11 project with apidemo11 as the package and TextScrollerDemo as the main file. Replace the TextScrollerDemo.fx skeleton with the code in Listing 8. Right-click apidemo11 under APIDemo11/Source Packages in the projects window, and select New/Empty JavaFX File from the popup menu to create an empty TextScroller.fx file. Replace its skeleton with Listing 7.

Compile and run this project by clicking the toolbar’s Run Main Project button (the green triangle button). If compilation fails, the cause is most likely your specified package not matching apidemo11. If this is the case, modify each source file’s package statement so that it appears as package apidemo11;. Assuming success, you should observe a GUI similar to the one shown in Figure 6.

Figure 6. Click once to start scrolling, and again to reverse the scrolling text. (Click to enlarge.)

Additional sources for custom components

JavaFX expert Jim Weaver demonstrates an intriguing node-based menu component containing node-based image-button components. Also, JavaFX developer Silveira Neto demonstrates an interesting node-based scrolling component, which can be used in applications that require side scrolling.

Stock ticker

Instead of creating scripts that you run only on your platform, you’ll want to deploy scripts for others to play with, so I’ll conclude with a short deployment exercise based on the environment-neutral javafx.application.Application class (for packaging a script as an application in preparation for deployment) and the Java Deployment Toolkit. Before we begin, take a look at Figure 7.

Figure 7. Two instances of an applet-based script running within Google’s Chrome browser and on the desktop. (Click to enlarge.)

Figure 7 reveals the user interface of a stock ticker application script’s implementation applet. I dragged one instance of this applet outside of the browser by pressing the Alt and left arrow keys, and then by positioning the mouse cursor over the applet and dragging the mouse. I subsequently reloaded the current Web page to generate the other instance.

The stock ticker application script implements a basic stock ticker that’s capable of horizontally scrolling recent stock activity for all companies whose symbols are identified to the script. It connects to an external Web service via Java’s java.net.URL class, reads these companies’ latest stock information, parses this information into a string, and scrolls the string horizontally. Listing 9 presents this script’s source code.

Listing 9. Main.fx

/*
 * Main.fx
 *
 */

package stockticker;

/**
 * @author Jeff Friesen
 */

import java.awt.EventQueue;

import java.lang.Runnable;
import java.lang.StringBuffer;
import java.lang.System;
import java.lang.Thread;

import java.io.BufferedReader;
import java.io.InputStreamReader;

import java.net.URL;

import javafx.application.Application;
import javafx.application.Stage;

import javafx.input.MouseEvent;

import javafx.scene.Font;
import javafx.scene.FontStyle;

import javafx.scene.effect.DropShadow;

import javafx.scene.geometry.Rectangle;

import javafx.scene.paint.Color;
import javafx.scene.paint.LinearGradient;
import javafx.scene.paint.Stop;

class Model
{
    public attribute stockInfo: String = "Initializing...";

    public attribute symbols: String [] on replace
    {
        var backgroundTask = Runnable
        {
            public function run ()
            {
                dummy = getStockInfo (symbols);

                var foregroundTask = Runnable
                {
                    public function run ()
                    {
                        stockInfo = dummy
                    }
                }

                EventQueue.invokeLater (foregroundTask)
            }
        }

        var t = new Thread (backgroundTask);
        t.start ()
    }

    private static attribute dummy: String;

    private static function getStockInfo (stockSymbols: String []): String
    {
        var sb = new StringBuffer ();

        for (i in [0..sizeof stockSymbols-1])
        {
             var uri = "http://www.webservicex.net/stockquote.asmx/GetQuote?"+
                       "symbol="+stockSymbols [i];
             var url = new URL (uri);

             var br;
             br = new BufferedReader (new InputStreamReader (url.openStream ()));

             // Skip header line

             br.readLine ();

             // Read stock info line

             var response = br.readLine ();

             br.close ();

            // Replace &ampgt and &amplt with > and < equivalents, which makes for
            // easier parsing.

            response = response.replaceAll ("&gt;", ">");
            response = response.replaceAll ("&lt;", "<");

            // Parse tag data into values.

            sb.append (parse (response, "<Symbol>"));
            sb.append (" (");
            sb.append (parse (response, "<Name>"));
            sb.append (")");
            sb.append (" Date ");
            sb.append (parse (response, "<Date>"));
            sb.append (", Time ");
            sb.append (parse (response, "<Time>"));
            sb.append (", Open ");
            sb.append (parse (response, "<Open>"));
            sb.append (", High ");
            sb.append (parse (response, "<High>"));
            sb.append (", Low ");
            sb.append (parse (response, "<Low>"));
            sb.append (", Last ");
            sb.append (parse (response, "<Last>"));
            sb.append (", Volume ");
            sb.append (parse (response, "<Volume>"));

            // The following code has been commented out because I only wanted to
            // display a representative sample of stock info for each company.

/*
            sb.append (", MktCap ");
            sb.append (parse (response, "<MktCap>"));
            sb.append (", PreviousClose ");
            sb.append (parse (response, "<PreviousClose>"));
            sb.append (", Change ");
            sb.append (parse (response, "<Change>"));
            sb.append (", %Change ");
            sb.append (parse (response, "<PercentageChange>"));
            sb.append (", AnnRange ");
            sb.append (parse (response, "<AnnRange>"));
            sb.append (", Earns ");
            sb.append (parse (response, "<Earns>"));
*/

            // Place a separator between each company's stock info.

            if (i != sizeof stockSymbols-1)
                sb.append (" < | > ")
        }

        sb.toString ()
    }

    private static function parse (response: String, tag: String): String
    {
        var pos: Integer;
        if ((pos = response.indexOf (tag)) == -1)
        {
            return null
        }
        pos += tag.length ();

        var temp = new StringBuffer ();
        while (response.charAt (pos) != '<'.charAt (0))
        {
           temp.append (response.charAt (pos++))
        }
        return temp.toString ()
    }
}

Application
{
    var model = Model
    {
        symbols: [ "AAPL", "GOOG", "IBM", "JAVA", "MSFT" ]
    }

    var textScrollerRef: TextScroller

    var stageRef: Stage
    stage: stageRef = Stage
    {
        var rectangleRef: Rectangle

        fill: LinearGradient
        {
            startX: 0.0
            startY: 0.0
            endX: 0.0
            endY: 1.0
            stops:
            [
                Stop { offset: 0.0 color: Color.DARKGRAY },
                Stop { offset: 1.0 color: Color.LIGHTGRAY }
            ]
        }

        content:
        [
            Rectangle
            {
                x: bind textScrollerRef.x
                y: bind textScrollerRef.y
                width: bind textScrollerRef.width
                height: bind textScrollerRef.height
                arcWidth: bind textScrollerRef.height
                arcHeight: bind textScrollerRef.width

                fill: LinearGradient
                {
                    startX: 0.0
                    startY: 0.3
                    endX: 0.0
                    endY: 1.0

                    stops:
                    [
                        Stop { offset: 0.0 color: Color.color (0.0, 0.2, 0.7 ) },
                        Stop { offset: 1.0 color: Color.color (1.0, 1.0, 1.0) }
                    ]
                }

                effect: DropShadow
                {
                    color: Color.BLACK
                    radius: 20
                }

                onMouseClicked: function (me: MouseEvent): Void
                {
                    if (not textScrollerRef.isScrolling ())
                        textScrollerRef.startScrolling ()
                }

                cache: true // Needed to prevent jerky text movement.
            },
            textScrollerRef = TextScroller
            {
                text: bind model.stockInfo with inverse

                x: 30.0
                y: 30.0
                width: bind stageRef.width-60.0
                height: bind stageRef.height-60.0

                font: Font
                {
                    name: "ARIAL BLACK"
                    size: 28
                    style: FontStyle.BOLD
                }
                stroke: Color.BLUE
                strokeWidth: 1.0
                fill: Color.LIGHTBLUE

                _effect: DropShadow
                {
                    offsetX: 5.0
                    offsetY: 5.0
                    radius: 5.0
                }
            },
            rectangleRef = Rectangle
            {
                x: bind textScrollerRef.x
                y: bind textScrollerRef.y
                width: bind textScrollerRef.width*0.95
                height: bind textScrollerRef.height*0.5
                arcWidth: bind textScrollerRef.height*0.5
                arcHeight: bind textScrollerRef.width*0.95

                translateX: bind (textScrollerRef.width-rectangleRef.width)/2.0
                translateY: bind (textScrollerRef.height-rectangleRef.height)/2.0-
                                 textScrollerRef.height*0.1

                fill: Color.color (1.0, 1.0, 1.0, 0.3)
            }
        ]
    }

    onExit: function ()
    {
        textScrollerRef.stopScrolling ();
        System.exit (0)
    }
}

Listing 9 declares a Model class that specifies the script’s model, in terms of a stockInfo attribute that identifies the stock information string, and a symbols attribute that specifies a sequence of string-based company symbols. When a new sequence is assigned to this latter attribute, its replace trigger is invoked to update stockInfo.

Because it can take time to access the stock information, via the model’s private getStockInfo() function, from the Web service, the trigger invokes this function on a background thread to avoid delaying the AWT’s event-dispatching thread. When this method returns, stockInfo is updated on the EDT. The update takes place via a dummy attribute, which should be accessible to both threads.

Listing 9 changes

Because JavaFX 1.1 and later releases don’t support explicit thread creation, you’ll need to replace Listing 9’s threading logic (including the potentially problematic, from a volatile memory perspective, dummy attribute) with the javafx.io.http.HttpRequest class, which offers an API for making asynchronous HTTP requests. You should also be able to replace my parsing logic with JavaFX’s javafx.data.pull.PullParser-based parser. Finally, you can get rid of with inverse in text: bind model.stockInfo with inverse. (I don’t recall why I originally included with inverse, but it’s not necessary.)

The reason for the time delay is due to getStockInfo() making a separate connection to the Web service for each stock symbol located in the symbols sequence. In response to making a connection to the http://www.webservicex.net/stockquote.asmx/GetQuote service (with an appropriate symbol argument), the service sends back an XML header line, followed by a stock info line that needs to be parsed.

Because I believe that it’s overkill to perform parsing via DOM, SAX, or StAX in this simple example, I’ve implemented my own parsing logic via the model’s private parse() function. Although you might be surprised by parse()‘s response.charAt (pos) != '<'.charAt (0) expression, keep in mind that JavaFX Script considers '<' to be a String object.

This script prepares for deployment by instantiating an Application object. This object’s literal instantiates the model, specifies the script’s stage (which presents a text-scrolling node, described by Listing 7’s TextScroller.fx source code, sandwiched between a pair of rectangle nodes), and installs a function to shutdown the script upon exit.

The Application class provides a portable execution environment for scripts running on the desktop, mobile devices, and so on. Content is assigned to this class’s stage attribute, and the script’s lifecycle is specified by anonymous functions assigned to attributes such as onExit — I’ve found that the function assigned to this attribute (see Listing 9) doesn’t appear to be invoked.

Finally, the stage sandwiches the text-scrolling node between rectangle nodes to give the user interface a (hopefully) classy-looking iPhone-like appearance. For some reason, the background rectangle node’s cache attribute must be set to true — the text scroller node also sets its cache attribute to true — to prevent jerky scrolling.

Building and deploying the stock ticker

Before we can deploy the stock ticker, you need to build it, which you can do by carrying out the following steps:

  1. Create a NetBeans project called StockTicker, leaving stockticker.Main as the default package and filename.
  2. Replace Main.fx‘s skeletal contents with Listing 9.
  3. Copy Listing 7 to a TextScroller.fx file in the NetBeansProjectsStockTickersrcstockticker directory, replacing the file’s package statement with package stockticker;.
  4. Select Build Main Project from the Build menu, or press the F11 function key. If the build doesn’t succeed, check that the source code exactly matches what is specified, and that you’ve followed these steps in the right order.

The JavaScript-based Java Deployment Toolkit is the preferred way to deploy Java applications and applets. For example, we can use this toolkit with Listing 10’s HTML/JavaScript code, which is based on similar deployment HTML/JavaScript code provided by JavaFX expert Jim Weaver, to deploy our Application-wrapped script as a Java applet.

Listing 10. stockticker.html

<html>
<script src="http://java.com/js/deployJava.js"></script>

<script>
  var attributes =
  {
      codebase: 'http://javajeff.mb.ca/test',
      code: 'javafx.application.Applet',
      archive: 'StockTicker.jar, javafxrt.jar, Scenario.jar, javafxgui.jar, javafx-swing.jar',
      width: 600,
      height: 150,
      java_arguments: '-Djnlp.packEnabled=true'
  };

  var parameters =
  {
      "ApplicationClass": "stockticker.Main",
      "draggable": "true"
  };

  var version = '1.6.0';

  deployJava.runApplet (attributes, parameters, version);
</script>
</html>

Listing 10 references the script’s StockTicker.jar file, and four other JAR files needed to provide the JavaFX runtime. It also reveals that you can deploy Pack200-compressed JAR files (under Java SE 6 update 10 and up), and that you can drag the applet outside the browser and onto the desktop if Java SE 6u10 and a suitable browser (such as Mozilla Firefox 3 or Google Chrome) are used.

JAR signing

I’ve noticed that I don’t need to sign the JAR files when running under Java SE 6 update 10. (I’m not sure why this is the case, and would love an explanation.) However, I had to sign these files when running under Java SE 6 update 7 (which I used as my Java platform last September when writing this article).

Create a directory on your ISP’s server, and upload stockticker.html (after replacing http://javajeff.mb.ca/test with the equivalent for your server) and the five JAR files to this directory — StockTicker.jar locates in NetBeansProjectsStockTickerdist; the other four JAR files locate in NetBeansProjectsStockTickerdistlib.

If you use Google Chrome without Java, the browser will probably take you to java.com, from where you can install Java SE 6u10 or higher, when you try to run the applet. Under this browser and Mozilla Firefox 3, you should see the stock information scrolling, and even be able to drag the applet outside of the browser (see the previous Figure 7).

Check out Sun’s How to Deploy a JavaFX Applet – Code to Embed document to discover the recommended way to deploy a script as an applet.

In conclusion

This article and the previous one in the Jump into JavaFX series have focused on the JavaFX APIs, based on JavaFX Preview SDK. In this article you’ve also created a stock ticker application and deployed it to a browser environment. In the final two articles in this series, we’ll jump into JavaFX 1.0, where you’ll put to work what you’ve learned about JavaFX conceptual basics, the JavaFX Script language, and the JavaFX APIs, while also exploring the first official release of the JavaFX Platform.

Jeff Friesen is a freelance software developer and educator who specializes in Java technology. Check out his javajeff.mb.ca website to discover all of his published Java articles and more. His book Beginning Java SE 6 Platform: From Novice to Professional (Apress, October 2007) explores most of the new features introduced in Java SE 6.