Today, my email in-box contained a pleasant surprise. Some time ago, Edgar Merino volunteered some simplifications to the code in the Graphical Menu Example post. Today, a gracious reader named Jase Batchelor sent me code for the DeckNode example converted to JavaFX SDK 1.0, as well as for the Graphical Menu Example that it is dependent upon. These example are part of a series of posts, based upon an earlier version of JavaFX, that I created to show you how to create custom UI elements in JavaFX. Today's post is the beginning of a new series, located in the JFX Custom Nodes SDK 1.0 category of this blog, that contains the code from the former series converted to JavaFX SDK 1.0 syntax and APIs.
Today's example features a simple custom node, named DeckNode, that I developed as a way to store a set of Node instances and display one of these nodes at a time. It is being used to augment the graphical menu example by showing the Node that pertains to a given menu button. Note that the concept is very similar to the Java CardLayout, so I wouldn't be surprised to see a similar class appear in the javafx.scene.layout package at some point. There is, by the way, a class with similar functionality in the open source JFXtras project. Anyway, just for grins, I provided the ability to specify a fade-in duration for the node being displayed. Here's a screenshot of the example application, with one of the nodes in the "deck" being displayed as the splash page:
One of the goals of JavaFX is for graphics designers and developers to be able to work together effectively in creating great looking applications. To demonstrate this, a graphics designer at Malden Labs created some graphical mock-ups for the fictitious CD application shown above. Here's an example mocked-up page in the form of a graphic that is being displayed as a result of pressing the Play button:
If you would like to try today's example out, click the Java Web Start link below:
Here's the code for the DeckNode class, contained in a file named DeckNode.fx:
/*
* DeckNode.fx -
* A node that shows a deck of nodes one at a time.
*
* Developed 2008 by James L. Weaver (jim.weaver at javafxpert.com)
* to demonstrate how to create custom nodes in JavaFX
*/
package com.javafxpert.custom_node;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.scene.CustomNode;
import javafx.scene.Group;
import javafx.scene.Node;
/**
* A node that shows a deck of nodes one at a time. When the
* visibleNodeId attribute is assigned a value, the Node whose
* id is the same as the name becomes visible. Note that the
* the id attribute of each Node must be assigned a unique name.
* This class also has an attribute in which a fade-in duration
* may be specified.
*/
public class DeckNode extends CustomNode {
/**
* A sequence that contains the Node instances in this "deck"
*/
public var content:Node[];
/**
* An optional node that will appear in the background of all nodes
*/
public var backgroundNode:Node;
/**
* The id of the node that is to be visible
*/
public var visibleNodeId:String on replace {
var nodes = for (node in content where node.id == visibleNodeId) node;
visibleNodeRef = if (sizeof nodes > 0) nodes[0] else null;
deckTimeline.playFromStart();
}
/**
* The amount of time to fade-in the new Node
*/
public var fadeInDur:Duration = 100ms;
/**
* This attribute is interpolated by a Timeline, and the opacity
* attribute of this DeckNode class is bound to it. This helps
* enable the fade-in effect.
*/
var opa:Number;
/**
* Override the opacity attribute so that it can be bound to the
* opa attribute that is interpolated by a Timeline
*/
override var opacity = bind opa;
/**
* A Timeline to control the fade-in behavior
*/
public var deckTimeline =
Timeline {
keyFrames: [
KeyFrame {
time: bind fadeInDur
values: [
opa => 1.0 tween Interpolator.LINEAR,
]
}
]
};
/**
* A reference to the Node in the Node instances that is visible
*/
protected var visibleNodeRef:Node;
/**
* Create the Node
*/
public override function create():Node {
Group {
content: bind [
backgroundNode,
visibleNodeRef
]
};
}
}
Shown below is the main script for this program, in a file named DeckNodeExampleMain.fx:
/*
* DeckNodeExampleMain.fx -
* An example of using the DeckNode custom node. It also demonstrates
* the MenuNode and ButtonNode custom nodes
*
* Developed 2008 by James L. Weaver (jim.weaver at lat-inc.com)
* to demonstrate how to create custom nodes in JavaFX
*/
package com.javafxpert.deck_node_example.ui;
import javafx.scene.Group;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.scene.text.Text;
import com.javafxpert.custom_node.*;
var deckRef: DeckNode;
var sceneRef: Scene;
var menuRef: MenuNode;
Stage {
title: 'DeckNode Example'
width: 500
height: 400
visible: true
scene: sceneRef = Scene {
fill: Color.BLACK
content: [
deckRef = DeckNode {
fadeInDur: 700ms
content: [
Group {
var vboxRef:VBox;
var splashFont = Font.font("Sans serif", FontWeight.BOLD, 12)
id: "Splash"
content: [
ImageView {
image:
Image {
url: "{__DIR__}images/splashpage.png"
}
},
vboxRef = VBox {
translateX: bind sceneRef.width - vboxRef.boundsInLocal.width - 20
translateY: 215
spacing: 1
content: [
Text {
content: "A Fictitious Audio Application that Demonstrates"
fill: Color.WHITE
font: splashFont
},
Text {
content: "Creating JavaFX Custom Nodes"
fill: Color.WHITE
font: splashFont
},
Text {
content: "Application Developer: Jim Weaver"
fill: Color.WHITE
font: splashFont
},
Text {
content: "Graphics Design: Malden Labs"
fill: Color.WHITE
font: splashFont
},
]
}
]
},
Group {
id: "Play"
content: [
ImageView {
image:
Image {
url: "{__DIR__}images/playlist.png"
}
}
]
},
Group {
id: "Burn"
content: [
ImageView {
image:
Image {
url: "{__DIR__}images/burning.png"
}
}
]
},
Group {
id: "Config"
content: [
ImageView {
image:
Image {
url: "{__DIR__}images/config.png"
}
}
]
},
Group {
id: "Help"
content: [
ImageView {
image:
Image {
url: "{__DIR__}images/help.png"
}
}
]
}
]
},
menuRef = MenuNode {
translateX: bind sceneRef.width / 2 - menuRef.boundsInLocal.width / 2
translateY: bind sceneRef.height - (menuRef.boundsInLocal.height)
buttons: [
ButtonNode {
title: "Play"
imageURL: "{__DIR__}icons/play.png"
action:
function():Void {
deckRef.visibleNodeId = "Play";
}
},
ButtonNode {
title: "Burn"
imageURL: "{__DIR__}icons/burn.png"
action:
function():Void {
deckRef.visibleNodeId = "Burn";
}
},
ButtonNode {
title: "Config"
imageURL: "{__DIR__}icons/config.png"
action:
function():Void {
deckRef.visibleNodeId = "Config";
}
},
ButtonNode {
title: "Help"
imageURL: "{__DIR__}icons/help.png"
action:
function():Void {
deckRef.visibleNodeId = "Help";
}
},
]
}
]
}
}
deckRef.visibleNodeId = "Splash";
Please notice the last line of the listing above is what causes the node that we named "Splash" to be displayed when the application starts.
The code for the other custom nodes used in this program from the Graphical Menu Example post was converted by the gracious reader as well. Here's ButtonNode.fx:
/*
* ButtonNode.fx -
* A node that functions as an image button
*
* Developed 2008 by James L. Weaver (jim.weaver at javafxpert.com)
* to demonstrate how to create custom nodes in JavaFX
*/
package com.javafxpert.custom_node;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.scene.CustomNode;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.input.MouseEvent;
import javafx.scene.effect.Glow;
import javafx.scene.shape.Rectangle;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.scene.text.Text;
import javafx.scene.text.TextOrigin;
public class ButtonNode extends CustomNode {
/**
* The title for this button
*/
public var title:String;
/**
* The Image for this button
*/
var btnImage:Image;
/**
* The URL of the image on the button
*/
public var imageURL:String on replace {
btnImage =
Image {
url: imageURL
};
}
/**
* The percent of the original image size to show when mouse isn't
* rolling over it.
* Note: The image will be its original size when it's being
* rolled over.
*/
public var scale:Number = 0.9;
/**
* The opacity of the button when not in a rollover state
*/
public var opacityValue:Number = 0.8;
/**
* The opacity of the text when not in a rollover state
*/
public var textOpacityValue:Number = 0.0;
/**
* A Timeline to control fading behavior when mouse enters or exits a button
*/
public var fadeTimeline =
Timeline {
keyFrames: [
KeyFrame {
time: 500ms
values: [
scale => 1.0 tween Interpolator.LINEAR,
opacityValue => 1.0 tween Interpolator.LINEAR,
textOpacityValue => 1.0 tween Interpolator.LINEAR
]
}
]
};
/**
* This attribute is interpolated by a Timeline, and various
* attributes are bound to it for fade-in behaviors
*/
var fade:Number = 1.0;
/**
* This attribute represents the state of whether the mouse is inside
* or outside the button, and is used to help compute opacity values
* for fade-in and fade-out behavior.
*/
var mouseInside:Boolean;
/**
* The action function attribute that is executed when the
* the button is pressed
*/
public var action:function():Void;
/**
* Create the Node
*/
public override function create():Node {
Group {
var textRef:Text;
content: [
Rectangle {
width: bind btnImage.width
height: bind btnImage.height
opacity: 0.0
},
ImageView {
image: btnImage
opacity: bind opacityValue;
scaleX: bind scale;
scaleY: bind scale;
translateX: bind btnImage.width / 2 - btnImage.width * scale / 2
translateY: bind btnImage.height - btnImage.height * scale
onMouseEntered:
function(me:MouseEvent):Void {
mouseInside = true;
fadeTimeline.rate = 1.0;
fadeTimeline.play();
}
onMouseExited:
function(me:MouseEvent):Void {
mouseInside = false;
fadeTimeline.rate = -1.0;
fadeTimeline.play();
me.node.effect = null
}
onMousePressed:
function(me:MouseEvent):Void {
me.node.effect = Glow {
level: 0.9
};
}
onMouseReleased:
function(me:MouseEvent):Void {
me.node.effect = null;
}
onMouseClicked:
function(me:MouseEvent):Void {
action();
}
},
textRef = Text {
translateX: bind btnImage.width / 2 - textRef.boundsInLocal.width / 2
translateY: bind btnImage.height - textRef.boundsInLocal.height
textOrigin: TextOrigin.TOP
content: title
fill: Color.WHITE
opacity: bind textOpacityValue
font: Font.font("Sans serif", FontWeight.BOLD, 16)
},
]
};
}
}
And here is the MenuNode.fx listing:
/*
* MenuNode.fx -
* A custom node that functions as a menu
*
* Developed 2008 by James L. Weaver (jim.weaver at lat-inc.com)
* to demonstrate how to create custom nodes in JavaFX
*/
package com.javafxpert.custom_node;
import javafx.scene.CustomNode;
import javafx.scene.Node;
import javafx.scene.effect.Reflection;
import javafx.scene.layout.HBox;
public class MenuNode extends CustomNode {
/*
* A sequence containing the ButtonNode instances
*/
public var buttons:ButtonNode[];
/**
* Create the Node
*/
public override function create():Node {
HBox {
spacing: 10
content: buttons
effect:
Reflection {
fraction: 0.50
topOpacity: 0.8
}
}
}
}
As always, please leave a comment if you have any questions.
Regards,
Jim Weaver
JavaFXpert.com
"Thanks James for your concern and helpfull blog !
Short question : what is the DeckNode::backgroundNode purpose ? it is used anywhere :(
What was its forst goal ?
Tahnks,
tibO"
The purpose of the DeckNode::backgroundNode variable is to provide a background for the deck node that is always visible no matter what node is also visible. It can be assigned when the DeckNode is created.
Thanks,
Jim Weaver
Posted by: Jim Weaver | April 22, 2009 at 10:51 AM
Thanks James for your concern and helpfull blog !
Short question : what is the DeckNode::backgroundNode purpose ? it is used anywhere :(
What was its forst goal ?
Tahnks,
tibO
Posted by: pitek71 | April 22, 2009 at 10:21 AM
"please let me know how it goes!"
Thanks, Jim. That worked.
The key is "content: bind [ activeScr ]" construction. Where "activeScr" is a reference to different instances of my classes which extends from Group and created in runtime depending on application state.
Posted by: Mike | January 09, 2009 at 01:15 PM
"Is there a way to change content of the scene in runtime? What I mean is what if you want to create multiple document interface in javaFX. So you would have a bunch of complicated GUI groups created in runtime, and you want to switch between them on your main scene. How do you do that?"
Mike,
Great idea! I haven't tried this, but here's a suggestion. Please let me know if it works or not: Create some instances of Scene that have the desired contents. Bind the scene to the stage in a similar manner to which this DeckNode example binds the visibleNodeRef to the content. For modularity, you may want to have multiple subclasses of Scene. That would have the advantage of being able to introduce a variable, perhaps named "id", that can identify each Scene (in the same manner that I'm using the id variable of Node). Please let me know if you'd like me to clarify, and again, please let me know how it goes!
Thanks,
Jim Weaver
Posted by: James Weaver | January 07, 2009 at 09:35 AM