Platinum Partner
java,ria,javafx,client-side

Progress Indicator: Creating a JavaFX Custom Node and Binding to a Model

In the Rolling Your Own JavaFX "Custom Nodes": A Graphical Menu Example post, I began showing you how to create your own UI controls in JavaFX.  In that post we defined the MenuNode and ButtonNode custom nodes so that you can easily create menus that consist of buttons that fade-in and expand when the mouse rolls over them.

Then, in the Getting Decked: Another JavaFX Custom Node post, we defined a DeckNode  that stores a set of Node instances and displays one of these nodes at a time.  It is used, for example, to show the node that pertains to a given menu button.

The posts mentioned above are part of a series in the JFX Custom Nodes category in which a graphics designer (Mark Dingman of Malden Labs) and I are collaborating on an imaginary "Sound Beans" application.  The objectives of building this application are to demonstrate how to create custom nodes, and to provide a case study in how a graphics designer and an application developer can work together effectively in developing JavaFX applications.

In today's post, we're going to do two things:

  1. Define a ProgressNode control that may be use to show the progress of an operation.
  2. Introduce a model class into the Sound Beans application.  As I've said before, the "way of JavaFX" is to bind the UI to a model, and this Sound Beans application has gone long enough without one.

Here's the mock-up that Mark gave me for the Burn CD page:

Burning_2

Based upon this image, I decided to create a "progress bar" control that consists of JavaFX graphical nodes (e.g. Rectangle, Text).  For this page, there are no image assets needed from Mark.

I'll show you the code in a bit, but first take a look at a screenshot of the Sound Beans application after the Burn button has been clicked:

Progressnodeexample_3

As you can see, I added a slider control for the purpose of simulating the progress of the burn.  Give it a whirl by clicking 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.

Webstartsmall2

Here's the code for the ProgressNode custom node, in a file named ProgressNode.fx:

/*
 *  ProgressNode.fx - 
 *  A custom node that functions as a progress bar
 *  TODO: Add the ability to have an "infinite progress" look as well
 *
 *  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.geometry.*;
import javafx.scene.paint.*;
import javafx.scene.text.*;

public class ProgressNode extends CustomNode { 

  /*
   * A number from 0.0 to 1.0 that indicates the amount of progress
   */
  public attribute progress:Number;
    
  /*
   * The fill of the progress part of the progress bar.  Because
   * this is of type Paint, a Color or gradient may be used.
   */
  public attribute progressFill:Paint = Color.BLUE;
    
  /*
   * The fill of the bar part of the progress bar. Because
   * this is of type Paint, a Color or gradient may be used.
   */
  public attribute barFill:Paint = Color.GREY;
    
  /*
   * The color of the progress percent text on the progress bar
   */
  public attribute progressPercentColor:Color = Color.WHITE;
    
  /*
   * The color of the progress text on the right side of the progress bar
   */
  public attribute progressTextColor:Color = Color.WHITE;
    
  /*
   * The progress text string on the right side of the progress bar
   */
  public attribute progressText:String;
    
  /*
   * Determines the width, in pixels, of the progress bar
   */
  public attribute width:Integer = 200;
    
  /*
   * Determines the height, in pixels, of the progress bar
   */
  public attribute height:Integer = 20;
    
  /**
   * Create the Node
   */
  public function create():Node {
    Group {
      var textRef:Text;
      var progTextRef:Text;
      var progBarFont =
        Font {
          name: "Sans serif"
          style: FontStyle.PLAIN
          size: 12 
        };
      content: [
        // The entire progress bar
        Rectangle {
          width: bind width
          height: bind height
          fill: bind barFill
        },
        // The progress part of the progress bar
        Polygon {
          points: bind [
            0.0, 0.0,
            0.0, height as Number,
            width * progress + height / 2.0, height as Number,
            width * progress - height / 2.0, 0.0
          ]
          fill: bind progressFill
          clip:
            Rectangle {
              width: bind width
              height: bind height
            }
        },
        // The percent complete displayed on the progress bar
        textRef = Text {
          translateX: width / 4
          translateY: 3
          textOrigin: TextOrigin.TOP
          font: progBarFont
          fill: bind progressPercentColor
          content: bind "{progress * 100 as Integer}%"
        },
        // The progress text displayed on the right side of the progress bar
        progTextRef = Text {
          translateX: bind width - progTextRef.getWidth() - 5
          translateY: 3
          textOrigin: TextOrigin.TOP
          font: progBarFont
          fill: bind progressTextColor
          content: bind progressText
        }
      ]
    }    
  }
}  


Most of the concepts used here were discussed in the posts referenced above.  One thing that I'd like to point out is the use of binding, for example, to display the current value of the progress attribute as a percentage in the last line of the listing.  Now take a look at the main program, in a file named ProgressNodeExampleMain.fx:

/*
* ProgressNodeExampleMain.fx -
* An example of using the ProgressNode custom node. It also demonstrates
* the DeckNode, 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.progress_node_example.ui;

import javafx.application.*;
import javafx.ext.swing.*;
import javafx.scene.*;
import javafx.scene.geometry.*;
import javafx.scene.image.*;
import javafx.scene.layout.*;
import javafx.scene.paint.*;
import javafx.scene.text.*;
import javafx.scene.transform.*;
import java.lang.System;
import com.javafxpert.custom_node.*;
import com.javafxpert.progress_node_example.model.*;

var deckRef:DeckNode;

Frame {
var model = ProgressNodeExampleModel.getInstance();
var stageRef:Stage;
var menuRef:MenuNode;
title: "ProgressNode Example"
width: 500
height: 400
visible: true
stage:
stageRef = Stage {
fill: Color.BLACK
content: [
deckRef = DeckNode {
fadeInDur: 700ms
content: [
// The "Splash" page
Group {
var vboxRef:VBox;
var splashFont =
Font {
name: "Sans serif"
style: FontStyle.BOLD
size: 12
};
id: "Splash"
content: [
ImageView {
image:
Image {
url: "{__DIR__}images/splashpage.png"
}
},
vboxRef = VBox {
translateX: bind stageRef.width - vboxRef.getWidth() - 10
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 Designer: Mark Dingman"
fill: Color.WHITE
font: splashFont
},
]
}
]
},
// The "Play" page
Group {
id: "Play"
content: [
ImageView {
image:
Image {
url: "{__DIR__}images/playlist.png"
}
}
]
},
// The "Burn" page
Group {
var vboxRef:VBox;
id: "Burn"
content: [
vboxRef = VBox {
translateX: bind stageRef.width / 2 - vboxRef.getWidth() / 2
translateY: bind stageRef.height / 2 - vboxRef.getHeight() / 2
spacing: 15
content: [
Text {
textOrigin: TextOrigin.TOP
content: "Burning custom playlist to CD..."
font:
Font {
name: "Sans serif"
style: FontStyle.PLAIN
size: 22
}
fill: Color.rgb(211, 211, 211)
},
ProgressNode {
width: 430
height: 15
progressPercentColor: Color.rgb(191, 223, 239)
progressTextColor: Color.rgb(12, 21, 21)
progressText: bind "{model.remainingBurnTime} Remaining"
progressFill:
LinearGradient {
startX: 0.0
startY: 0.0
endX: 0.0
endY: 1.0
stops: [
Stop {
offset: 0.0
color: Color.rgb(0, 192, 255)
},
Stop {
offset: 0.20
color: Color.rgb(0, 172, 234)
},
Stop {
offset: 1.0
color: Color.rgb(0, 112, 174)
},
]
}
barFill:
LinearGradient {
startX: 0.0
startY: 0.0
endX: 0.0
endY: 1.0
stops: [
Stop {
offset: 0.0
color: Color.rgb(112, 112, 112)
},
Stop {
offset: 1.0
color: Color.rgb(88, 88, 88)
},
]
}
progress: bind model.burnProgressPercent / 100.0
},
ComponentView {
component:
FlowPanel {
background: Color.BLACK
content: [
Label {
text: "Slide to simulate burn progress:"
foreground: Color.rgb(211, 211, 211)
},
Slider {
orientation: Orientation.HORIZONTAL
minimum: 0
maximum: 100
value: bind model.burnProgressPercent with inverse
preferredSize: [200, 20]
}
]
}
}
]
}
]
},
// The "Config" page
Group {
id: "Config"
content: [
ImageView {
image:
Image {
url: "{__DIR__}images/config.png"
}
}
]
},
// The "Help" page
Group {
id: "Help"
content: [
ImageView {
image:
Image {
url: "{__DIR__}images/help.png"
}
}
]
}
]
},
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 {
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";

 

Using the ProgressNode control

You'll notice that the main program above is almost identical to the main program in the Getting Decked post, except that instead of showing the mock-up graphic that Mark Dingman supplied, we're using JavaFX code to display something similar, including our new ProgressNode control.  Examine the Group block right after the //The "Play" page comment in the listing to see this additional code.  One thing that deserves repeating from previous posts is that JavaFX is moving to a node-centric approach, so 2D graphics as well as components will all be graphical nodes.  Because of this, I'm using the ComponentView class (which is a subclass of Node), to contain the Slider, which is a component.  The JavaFX team is rapidly developing a set of controls (e.g. Button) that are subclasses of Node, so very soon the ComponentView class won't be necessary.

Note that as the complexity of the individual pages grow, I'll tend to put them in their own files, subclassing CustomNode just like we're doing with this UI controls.


Introducing a Model into this Program

If you've followed this blog, you know that JavaFX inherently supports the model-view-controller pattern through constructs such as declarative programming syntax, binding, and triggers.  In this program, our model has an attribute named burnProgressPercent, for example, that holds the completion percent of the CD burn, as shown in the ProgressNodeExampleModel.fx listing below.  Notice that in the ProgressNodeExampleMain.fx listing above that the value attribute of the Slider is bound bi-directionally to this variable, and that the progress attribute of the ProgressNode is bound to it as well.  This is what causes the progress bar to be updated as you move the slider.  Here's the ProgressNodeExampleModel.fx listing:

/*
* ProgressNodeExampleModel.fx -
* The model behind the ProgressNode example
*
* Developed 2008 by James L. Weaver (jim.weaver at lat-inc.com)
*/
package com.javafxpert.progress_node_example.model;

/**
* The model behind the ProgressNode example
*/
public class ProgressNodeExampleModel {

/**
* The total estimated number of seconds for the burn.
* For this example program, we'll set it to 10 minutes
*/
public attribute estimatedBurnTime:Integer = 600;

/**
* The percent progress of the CD burn, represented by a number
* between 0 and 100 inclusive.
*/
public attribute burnProgressPercent:Integer on replace {
var remainingSeconds = estimatedBurnTime * (burnProgressPercent / 100.0) as Integer;
remainingBurnTime = "{remainingSeconds / 60}:{%02d (remainingSeconds mod 60)}";
};

/**
* The time remaining on the CD burn, expressed as a String in mm:ss
*/
public attribute remainingBurnTime:String;

//-----------------Use Singleton pattern to get model instance -----------------------
private static attribute instance:ProgressNodeExampleModel;

public static function getInstance():ProgressNodeExampleModel {
if (instance == null) {
instance = ProgressNodeExampleModel {};
}
else {
instance;
}
}
}

 

Take a look at the burnProgressPercent attribute and you'll notice a couple of things: 

  • It has an on replace trigger that gets executed whenever the value of burnProgressPercent changes.  In the on replace block we're altering the value of the remainingBurnTime attribute, which you may have noticed is being bound to by the progressText attribute of the ProgressNode.  See the ProgressNodeExampleMain.fx listing above to see this bind. 
  • Another item of interest in the on replace block is the use of a format string to pad the seconds with a leading zero.  The set of format strings available can be found in the java.util.Formatter class API documentation.

One last observation about this model class is that I'm using a singleton pattern to get a reference to it.  As our program grows, and more classes need a reference to the model class, this is an alternative to supplying a model reference via public attributes to every class in the program that needs a reference.

By the way, the other classes used by this example (ButtonNode, MenuNode and DeckNode) are located in the Rolling Your Own, and Getting Decked posts referred to above.  As always, please post a comment if you have any questions!

Regards,
Jim Weaver

{{ tag }}, {{tag}},

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}
{{ parent.authors[0].realName || parent.author}}

{{ parent.authors[0].tagline || parent.tagline }}

{{ parent.views }} ViewsClicks
Tweet

{{parent.nComments}}