Now that the JavaFX SDK Technology Preview has been released, I'd like to get you up to speed on how to create your own "custom nodes". This is JavaFX-speak for widgets, gadgets, UI components, whatever, but the purpose is the same: to be able to create a potentially reusable UI thingy for JavaFX programs. Today's example demonstrates how to create a custom node (in fact, two), and here's a screenshot:
By the way, a big thanks goes to Edgar Merino for pointing out some simplifications to the code that have now been implemented in this example. If you would like to try it out, click on this Java Web Start link, keeping in mind that you'll need at least JRE 6. Also, installing Java SE 6 update 10 will give you faster deployment time.
As I mentioned in the JavaFX SDK Packages are Taking Shape post, JavaFX is adopting a graphical "node-centric" approach to UI development, so nearly everything in a JavaFX user interface is a Node. When you want to create your own custom node, you'll extend the CustomNode class, giving it your desired attributes and behavior. Shown below is the code for the custom node in the example that displays an image and responds to mouse events (e.g. becoming more translucent and showing the text when rolling the mouse over).
Note: You may be wondering why I don't just use the Button class that is located in the javafx.ext.swing package. The reason is that the Button class is a Component, not a Node, and I think that it is best to follow the stated direction of moving to a node-centric approach. At some point there will be a button that subclasses Node, at which point the ButtonNode class in this example may not be needed anymore.
ButtonNode.fx
/*
* ButtonNode.fx -
* A node that functions as an image button
*
* Developed 2008 by James L. Weaver (jim.weaver at lat-inc.com)
* and Edgar Merino (http://devpower.blogsite.org/) to demonstrate how
* to create custom nodes in JavaFX
*/
package com.javafxpert.custom_node;
import javafx.animation.*;
import javafx.input.*;
import javafx.scene.*;
import javafx.scene.effect.*;
import javafx.scene.geometry.*;
import javafx.scene.image.*;
import javafx.scene.paint.*;
import javafx.scene.text.*;
import javafx.scene.transform.*;
public class ButtonNode extends CustomNode {
/**
* The title for this button
*/
public attribute title:String;
/**
* The Image for this button
*/
private attribute btnImage:Image;
/**
* The URL of the image on the button
*/
public attribute 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 attribute scale:Number = 0.9;
/**
* The opacity of the button when not in a rollover state
*/
public attribute opacityValue:Number = 0.8;
/**
* The opacity of the text when not in a rollover state
*/
public attribute textOpacityValue:Number = 0.0;
/**
* A Timeline to control fading behavior when mouse enters or exits a button
*/
private attribute fadeTimeline =
Timeline {
toggle: true
keyFrames: [
KeyFrame {
time: 600ms
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
*/
private attribute 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.
*/
private attribute mouseInside:Boolean;
/**
* The action function attribute that is executed when the
* the button is pressed
*/
public attribute action:function():Void;
/**
* Create the Node
*/
public 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.start();
}
onMouseExited:
function(me:MouseEvent):Void {
mouseInside = false;
fadeTimeline.start();
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.getWidth() / 2
translateY: bind btnImage.height - textRef.getHeight()
textOrigin: TextOrigin.TOP
content: title
fill: Color.WHITE
opacity: bind textOpacityValue
font:
Font {
name: "Sans serif"
size: 16
style: FontStyle.BOLD
}
},
]
};
}
}
Some things to note in the ButtonNode.fx code listing above are:
- Our ButtonNode class extends CustomNode
- This new class introduces attributes for storing the image and text that will appear on the custom node.
- The create() function returns the declarative expression of our custom node's UI appearance and behavior.
- The Glow effect in the javafx.scene.effect package is used to brighten the image when clicked.
- The opacity of the image, the size of the image, and the title of the custom node, are transitioned as the mouse enters and exits the button. A Timeline is employed to make these transitions gradual.
- After adjusting opacity and applying a glow effect, the onMouseClicked function calls the action() function attribute defined earlier in the listing. This make our custom node behave like the familiar Button.
Arranging the ButtonNode instances into a "menu"
As shown in the Setting the "Stage" for the JavaFX SDK post, the HBox class is located in the javafx.scene.layout package, and is a node that arranges other nodes within it. The MenuNode custom node shown below arranges the ButtonNode instances horizontally, and it uses the Reflection class in the javafx.scene.effects package to add a nice reflection effect below the buttons. Here's the code:
MenuNode.fx
/*
* 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.*;
import javafx.scene.effect.*;
import javafx.scene.layout.*;
public class MenuNode extends CustomNode {
/*
* A sequence containing the ButtonNode instances
*/
public attribute buttons:ButtonNode[];
/**
* Create the Node
*/
public function create():Node {
HBox {
spacing: 10
content: buttons
effect:
Reflection {
fraction: 0.50
topOpacity: 0.8
}
}
}
}
Using our custom nodes
Now that the custom nodes have been defined, I'd like to show you how to use them in a simple program. If you've followed this blog, you know that "the way of JavaFX is to bind the UI to a model". In this simple example, since I really want to focus on teaching you how to create custom nodes, I'm not going to complicate things by creating a model and binding the UI to it. Rather, I'm simply printing a string to the console whenever a ButtonNode instance is clicked. Here's the code for the main program in this example:
MenuNodeExampleMain.fx
/*
* MenuNodeExampleMain.fx -
* An example of using the MenuNode custom node
*
* Developed 2008 by James L. Weaver (jim.weaver at lat-inc.com)
* to demonstrate how to create custom nodes in JavaFX
*/
package com.javafxpert.menu_node_example.ui;
import javafx.application.*;
import javafx.scene.paint.*;
import javafx.scene.transform.*;
import java.lang.System;
import com.javafxpert.custom_node.*;
Frame {
var stageRef:Stage;
var menuRef:MenuNode;
title: "MenuNode Example"
width: 500
height: 400
visible: true
stage:
stageRef = Stage {
fill: Color.BLACK
content: [
menuRef = MenuNode {
translateX: bind stageRef.width / 2 - menuRef.getWidth() / 2
translateY: bind stageRef.height - menuRef.getHeight()
buttons: [
ButtonNode {
title: "Play"
imageURL: "{__DIR__}icons/play.png"
action:
function():Void {
System.out.println("Play button clicked");
}
},
ButtonNode {
title: "Burn"
imageURL: "{__DIR__}icons/burn.png"
action:
function():Void {
System.out.println("Burn button clicked");
}
},
ButtonNode {
title: "Config"
imageURL: "{__DIR__}icons/config.png"
action:
function():Void {
System.out.println("Config button clicked");
}
},
ButtonNode {
title: "Help"
imageURL: "{__DIR__}icons/help.png"
action:
function():Void {
System.out.println("Help button clicked");
}
},
]
}
]
}
}
Notice that the action attributes are assigned functions that are called whenever the user clicks the mouse on the corresponding ButtonNode, as pointed out earlier. Also notice the the __DIR__ expression evaluates to the directory in which the CLASS file resides. In this case, the graphical images are located in a com/javafxpert/menu_node_example/ui/icons directory.
By the way, the images for this article can be downloaded so that you can build and run this example with the graphics. This is a zip file that you can expand in the project's classpath.
It is my intent to build up a library of useful custom nodes for the JavaFX SDK Technology Preview and post them in the JFX Custom Nodes category of this blog. If you have ideas for custom nodes, or would like to share ones that you've developed, please drop me a line at jim.weaver at lat-inc.com
By the way, after this post ran, Weiqi Gao reported some cool news in his Java WebStart Works On Debian GNU/Linux 4.0 AMD64 post. I'm partial to Weiqi (pronounced way-chee), of course, because he did a great job on the technical review of our JavaFX Script book ;-)
Thanks,
Jim Weaver
JavaFX Script: Dynamic Java Scripting for Rich Internet/Client-side Applications
Immediate eBook (PDF) download available at the book's Apress site
Thx a lot for this nice menu node it's really great. But maybee you can answer me a question, I'm using JavaFX 1.2 and the command
Timeline {
toggle: true
...
toogle doesn't exists anymore? Do you have any idea what I can use instead?
Thx
greetz
Posted by: Corinne | June 15, 2009 at 07:47 AM
Very nice article. I'm all agree with you.
Posted by: Betsson08 | November 30, 2008 at 05:08 PM
I'm fan of JavaFX. I developed some custom nodes and will publish them in very near future. MenuNode is alo fantastic.
Also please add Linux support ASAP ;) Linux systems are getting more users day by day.
Posted by: Betsson | November 24, 2008 at 09:48 AM
Ram,
The images for this article can be downloaded from the following location:
http://jmentor.com/JFX/MenuNodeExample/menu_node_example_assets.zip
That is a zip file that you can expand in the project's classpath.
Posted by: Jim Weaver | August 15, 2008 at 04:37 PM