Jim has posted 66 posts at DZone. You can read more from them at their website. View Full User Profile

Rolling Your Own JavaFX "Custom Nodes": A Graphical Menu Example

07.24.2008
| 13850 views |
  • submit to reddit

With the imminent release of the JavaFX SDK Technology Preview, 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:

Menunodeexample

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 5. Also, installing Java SE 6 update 10 will give you faster deployment time.

Webstartsmall2

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)
* 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 nonRolloverScale:Number = 0.9;

/**
* The opacity of the button when not in a rollover state
*/
public attribute nonRolloverOpacity:Number = .80;

/**
* A Timeline to control fading behavior when mouse enters or exits a button
*/
private attribute fadeTimeline =
Timeline {
keyFrames: [
KeyFrame {
time: 0ms
values: [
fade => 0.0
]
},
KeyFrame {
time: 600ms
values: [
fade => 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 {
var scale = bind if (mouseInside) fade * (1.0 - nonRolloverScale) +
nonRolloverScale
else 1.0 - fade * (1.0 - nonRolloverScale);
image: btnImage
opacity: bind if (mouseInside) fade * (1.0 - nonRolloverOpacity) + nonRolloverOpacity
else 1.0 - fade * (1.0 - nonRolloverOpacity)
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 if (mouseInside) fade else 1.0 - fade
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.

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

Published at DZone with permission of its author, Jim Weaver.

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)

Tags:

Comments

Dmitri Trembovetski replied on Thu, 2008/07/24 - 6:19pm

I'd suggest to modify the test to animate the rollover effects (fade in/out instead of changing abruptly), it'd look better and show off the JavaFX script's animation capabilities. Should be very easy to do..

Dmitri

 

Peter Pilgrim replied on Fri, 2008/07/25 - 10:36am

I think this is a good example of a great dashboard effect á la the Mac Look and Feel.

All that is needed is a good resolution on the icon graphics and a JavaFX equivalent of the Romain Guy's and Chet Haase scaledInstance() library to make a good Fish Eye effect. With larger images of the icon, say about twice the dimensions, a good Fish Eye would be possible. I have not looked at the source code for ImageView to see if this is even possible in the new UI libraries. I hope so ?

I agree with Dimitri too that you can now take the code and extend it also to fade in and out. That might be a job for you, Dimitri in your blog.

 

 

 

 

 

Jim Weaver replied on Fri, 2008/07/25 - 1:38pm in response to: Dmitri Trembovetski

I'd suggest to modify the test to animate the rollover effects (fade in/out instead of changing abruptly), it'd look better and show off the JavaFX script's animation capabilities. Should be very easy to do.

Dmitri,

I took your advice on using a Timeline to make the rollover effects more gradual.  Please take a look at the ButtonNode.fx listing and try the Java Web Start link again :-)

Dmitri Trembovetski replied on Fri, 2008/07/25 - 1:40pm

Looks much better now! =)

Just a couple more comments: I would make the default state "faded out" and make it fade in when under mouse pointer. Also,  you can scale the icon in focus up (animating, of course) a bit, and scale back when it loses focus, sort of like MacOS's dock.

Nice to see how easy all this stuff is to do..

Dmitri

Jim Weaver replied on Fri, 2008/07/25 - 1:42pm in response to: Peter Pilgrim

Peter,

It is definately possible to do a fisheye effect on rollover, simply by adjusting scaleX and scaleY of the button.  I decided not to do that for this example, but have a "fisheye lens" example planned for a future post.  Also, you may have noticed that I took yours and Dmitri's advice on animating the rollover.  Try the Java Web Start link again :-)

Peter Pilgrim replied on Fri, 2008/07/25 - 3:27pm

Great stuff! It works fine with the fading and animating the rollover. I look forward to seeing the fish eye demo.

Jim Weaver replied on Mon, 2008/07/28 - 2:02pm in response to: Dmitri Trembovetski

Just a couple more comments: I would make the default state "faded out" and make it fade in when under mouse pointer. Also,  you can scale the icon in focus up (animating, of course) a bit, and scale back when it loses focus, sort of like MacOS's dock.

Dmitri (and Peter),

I went ahead and updated the code and the Java Web Start link to implement both of these suggestions.  Thanks for your input, and please try it out!

Jose Jeria replied on Tue, 2008/07/29 - 3:56am

This above example is 2065 KB according to "Java Web Start" in Mac OS X.

How is it possible that this "application" is so big? It would never be so big using Flash or JavaScript/Canvas. It would probably end up being 3-5 Kb big using those technologies..

What would be the benefit of this over other technologies?

Dmitri Trembovetski replied on Mon, 2008/07/28 - 2:54pm

Looks even better now!

Dmitri

 

Jim Connors replied on Fri, 2008/10/03 - 2:18pm

 Jim,

 

A relative newbie to JavaFX, I realized that your create() function inside ButtonNode, starting on line 98, i.e.:

public function create():Node {  
    Group { 

was missing the return keyword.  Shouldn't it be:

public function create():Node {  
    return Group { 

Is this optional?

Scott Selvia replied on Tue, 2009/06/09 - 2:10pm

Is there a JavaFX 1.2 version of this code?

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.