Creating Java User Interfaces with Project Scene Graph
- Hello, Project Scene Graph
- Nodes
- Animation
- Effects
- The FX Layer
- Conclusion
At the May 2007 JavaOne Conference, Sun Microsystems announced its Java-based JavaFX technology for developing rich internet applications (RIAs)—Web applications that typically use Web services for the backend, and provide browser-based, front-end interactivity similar to that offered by traditional desktop applications. JavaFX is itself dependent on another Sun technology: Project Scene Graph.
Project Scene Graph, which is also known as Scenario, amounts to a set of Java classes that provides the graphical runtime for the JavaFX Script language, but can also be used in Swing programs. These classes offer a low-level approach for specifying a graphical scene that's based on a scene graph—a structure that organizes a scene's representation.
This article focuses on Project Scene Graph. After introducing you to this technology via a "hello, world"–style Java application, you explore the "nuts and bolts" of Project Scene Graph: nodes, animation, and effects. You also discover Project Scene Graph's FX-prefixed classes, which offer several advantages to applications over the equivalent SG-prefixed classes.
Hello, Project Scene Graph
In the spirit of introducing new technologies via "hello, world" programs, I've created a similar example that demonstrates key aspects of Project Scene Graph. I've based the example (see Listing 1 for its source code) on a demo presented in former Sun employee and Project Scene Graph lead Hans Muller's January 8, 2008 blog entry.
Listing 1 HelloPSG.java
// HelloPSG.java import java.awt.*; import java.awt.geom.*; import java.util.*; import javax.swing.*; import com.sun.scenario.animation.*; import com.sun.scenario.effect.*; import com.sun.scenario.scenegraph.*; public class HelloPSG { final static int WIDTH = 550; final static int HEIGHT = 300; final static int BORDERWIDTH = 10; public static void main (String [] args) { Runnable r = new Runnable () { public void run () { createAndShowGUI (); } }; EventQueue.invokeLater (r); } public static void createAndShowGUI () { JFrame f = new JFrame ("Hello, PSG!"); f.setDefaultCloseOperation (JFrame.EXIT_ON_CLOSE); // JSGPanel is a JComponent that renders a scene graph. Instead of // instantiating this class, this method instantiates a subclass that // allows a scene graph to be properly centered. CenteringSGPanel panel = new CenteringSGPanel (); panel.setBackground (Color.BLACK); panel.setPreferredSize (new Dimension (WIDTH, HEIGHT)); // A scene graph is implemented as a tree of nodes. The scene node is an // instance of SGGroup, which serves as a container for adding child // nodes. SGGroup sceneNode = new SGGroup (); sceneNode.setID ("sceneNode: SGGroup"); // SGText represents a child node that renders a single line of text. SGText textNode = new SGText (); textNode.setID ("textNode: SGText"); textNode.setText ("Hello, PSG!"); textNode.setFont (new Font ("SansSerif", Font.PLAIN, 96)); textNode.setAntialiasingHint (RenderingHints.VALUE_TEXT_ANTIALIAS_ON); textNode.setFillPaint (Color.WHITE); // Create a node consisting of the text node surrounded by a border // node, and add it to the scene graph via the scene node. sceneNode.add (createBorderedNode (textNode)); // An SGEffect node is required to introduce an effect, such as a drop // shadow. SGEffect effectNode = new SGEffect (); effectNode.setID ("effectNode: SGEffect"); DropShadow shadow = new DropShadow (); shadow.setOffsetX (5); shadow.setOffsetY (5); effectNode.setEffect (shadow); effectNode.setChild (textNode); // Add the SGEffect node to the scene graph. sceneNode.add (effectNode); // An SGComposite node is required to specify opacity. The default // opacity is 1.0f (fully opaque). SGComposite compNode = new SGComposite (); compNode.setID ("compNode: SGComposite"); compNode.setOpacity (1.0f); compNode.setChild (effectNode); // Add the SGComposite node to the scene graph. sceneNode.add (compNode); // Establish the scene. panel.setScene (sceneNode); // Dump the scene graph hierarchy to the standard output. dump (panel.getScene (), 0); // Install the panel into the Swing hierarchy, pack it to its preferred // size, and don't let it be resized. Furthermore, center and display // the main window on the screen. f.setContentPane (panel); f.pack (); f.setResizable (false); f.setLocationRelativeTo (null); f.setVisible (true); // Create an animation clip for animating the scene's text opacity. Each // cycle will last for 1.5 seconds, the animation will continue // indefinitely, the animation will repeatedly invoke the SGComposite // timing target, the property being animated is Opacity, and this // property is animated from completely opaque (1.0) to completely // transparent (0.0). Also, each cycle reverses the animation direction. Clip fader = Clip.create (1500, Clip.INDEFINITE, compNode, "Opacity", 1.0f, 0.0f); // Start the animation. fader.start (); } static SGNode createBorderedNode (SGNode node) { Rectangle2D nodeR = node.getBounds (); double x = nodeR.getX ()-BORDERWIDTH; double y = nodeR.getY ()-BORDERWIDTH; double w = nodeR.getWidth ()+(2*BORDERWIDTH); double h = nodeR.getHeight ()+(2*BORDERWIDTH); double a = 1.5*BORDERWIDTH; SGShape borderNode = new SGShape (); borderNode.setID ("borderNode: SGShape"); borderNode.setShape (new RoundRectangle2D.Double (x, y, w, h, a, a)); borderNode.setFillPaint (new Color (0x660000)); borderNode.setDrawPaint (new Color (0xFFFF33)); borderNode.setDrawStroke (new BasicStroke ((float) (BORDERWIDTH/2.0))); borderNode.setMode (SGAbstractShape.Mode.STROKE_FILL); borderNode.setAntialiasingHint (RenderingHints.VALUE_ANTIALIAS_ON); SGGroup borderedNode = new SGGroup (); borderedNode.setID ("borderedNode: SGGroup"); borderedNode.add (borderNode); borderedNode.add (node); return borderedNode; } public static void dump (SGNode node, int level) { for (int i = 0; i < level; i++) System.out.print (" "); System.out.println (node.getID ()); if (node instanceof SGParent) { java.util.List<SGNode> children = ((SGParent) node).getChildren (); Iterator<SGNode> it = children.iterator (); while (it.hasNext ()) dump (it.next (), level+1); } } } // JSGPanel has been subclassed in order to properly center the scene within // this Swing component. class CenteringSGPanel extends JSGPanel { private SGNode sceneNode; private SGTransform.Translate centeringTNode; @Override public void doLayout () { if (sceneNode != null) { Rectangle2D bounds = sceneNode.getBounds (); centeringTNode.setTranslateX (-bounds.getX ()+ (getWidth ()-bounds.getWidth ())/2.0); centeringTNode.setTranslateY (-bounds.getY ()+ (getHeight ()-bounds.getHeight ())/2.0); } } @Override public void setScene (SGNode sceneNode) { this.sceneNode = sceneNode; centeringTNode = SGTransform.createTranslation (0f, 0f, sceneNode); centeringTNode.setID ("centeringTNode: SGTransform.Translate"); super.setScene (centeringTNode); } }
Listing 1 describes a Project Scene Graph application whose colorful scene consists of a "Hello, PSG!" message that's located within a rectangle with a rounded border. A black drop shadow is rendered behind the message's text, and the text's opacity continuously animates from opaque to transparent (and vice versa). The resulting scene appears in Figure 1.
Figure 1 Antialiasing is used to improve the text's and each border corner's appearance.
Additionally, HelloPSG outputs its scene's hierarchy of nodes (which I discuss later) to the standard output. This hierarchy (which appears below) is walked by Project Scene Graph each time it needs to render the scene—Project Scene Graph applications don't perform their own rendering. Along with painting pixels, Project Scene Graph performs a translation, applies a drop-shadow effect, and takes care of opacity.
centeringTNode: SGTransform.Translate sceneNode: SGGroup borderedNode: SGGroup borderNode: SGShape compNode: SGComposite effectNode: SGEffect textNode: SGText
To render the scene, Project Scene Graph has these nodes set up a translation (to center the scene), then render the rectangle (taking the translation into account) that serves as a border for the text, subsequently render the drop-shadow effect (taking the translation and opacity into account), and finally render the text (again taking translation and opacity into account).
Before you learn how to compile HelloPSG.java and run the application, consider the following two items, which are applicable to all Project Scene Graph applications:
- Packages: Project Scene Graph consists of nearly 100 classes, interfaces, and enumerations that are organized into seven packages: com.sun.scenario.animation, com.sun.scenario.animation.util, com.sun.scenario.effect, com.sun.scenario.effect.light, com.sun.scenario.scenegraph, com.sun.scenario.scenegraph.event, and com.sun.scenario.scenegraph.fx. You'll learn about many of these packages' types as you read this article.
- JSGPanel: The com.sun.scenario.scenegraph.JSGPanel class is used to integrate a scene into a Swing application. The idea is to create an instance of this class, invoke its public void setScene(SGNode scene) method with the scene's root node as an argument, set the panel's preferred size (as recommended by former Project Scene Graph lead Hans Muller), and add the JSGPanel instance to the Swing application's component hierarchy.
Build and Run HelloPSG
You need to obtain the latest version of the Project Scene Graph JAR file before you can build and run HelloPSG. Go to Project Scene Graph's Java.net-hosted project page and download Scenario-0.6.jar. If desired, you can also download the JAR file's source code (Scenario-0.6-src.zip) from this page.
While you're downloading the JAR file and its archived source code, you might want to download Project Scene Graph's Javadoc-based API documentation—this documentation is stored in Scenario-0.6-doc.zip. Although you can alternatively view the API Javadoc online, I don't recommend doing so because that documentation is for a previous version, and the API has subsequently changed.
Assuming that Scenario-0.6.jar and HelloPSG.java are located in the current directory, and assuming that Java SE 6 is installed, execute the command line below to compile the source code:
javac -cp Scenario-0.6.jar HelloPSG.java
If compilation succeeds, invoke the following command line to run the resulting application (refer to Figure 1 for the output, which appears in a Windows XP context):
java -cp Scenario-0.6.jar;. HelloPSG