One of the most touted parts of the new JavaFX API is the ability to skin UI controls using CSS-like stylesheets. However the current 1.0 release seems to be rather light on skin-aware controls, while documentation and examples seem to be rarer than a woman at a Star Trek convention. (That's my derogatory stereotyping quota used up for this year!)

Not that lack of documentation ever stopped anyone, of course!

A few days back I was playing about with some code, trying to unlock how the stylesheet support might work. All of a sudden a forum posting by tamerkarakan turning up, containing some hastily written (but insightful) notes— presumably the rewards of his own digging around in the code and trial-and-error testing.

I thought I'd work his findings up into a more complete, practical, example. What follows is a step-by-step guide to creating your own style-aware JavaFX control, including an external stylesheet which can transform the look of the component without need for recompilation of the JavaFX Script code.

fx_css.png 

Step one is to create a new type of control which supports a skin. For this example I thought we'd build something pretty simple, a basic progress bar.

package skindemo;
import javafx.scene.control.Control;

public class Progress extends Control
{   public var minimum:Number = 0;
    public var maximum:Number = 100;
    public var value:Number = 50;

    init
    {   skin = ProgressSkin{};
    }
}

The class extends javafx.scene.control.Controlrather than javafx.scene.CustomNode, giving us access to an inherited field, skin. This is where we will plug our scene graph code, which actually draws the control. This class just houses our control's public properties.

Step two is to create the skin itself, giving our control a UI.

package skintest;
import javafx.scene.Group;
import javafx.scene.control.Skin;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.HBox;
import javafx.scene.paint.*;
import javafx.scene.shape.Rectangle;

public class ProgressSkin extends Skin
{   public var boxCount:Integer = 10;
    public var boxWidth:Number = 15;
    public var boxHeight:Number = 20;
    public var boxHGap:Number = 2;
    public var unsetHighColor:Color = Color.YELLOW;
    public var unsetMidColor:Color = Color.GREEN;
    public var unsetLowColor:Color = Color.DARKGREEN;
    public var setHighColor:Color = Color.ORANGE;
    public var setMidColor:Color = Color.RED;
    public var setLowColor:Color =  Color.DARKRED;
        
    def boxValue:Integer = bind
    {   var p:Progress = control as Progress;
        var v:Number = (p.value-p.minimum) / 
            (p.maximum-p.minimum);
        (boxCount*v) as Integer;
    }    
    

    init
    {   def border:Number = bind boxWidth/10;
        def arc:Number = bind boxWidth/2;
        def lgUnset:LinearGradient = bind makeLG
            (unsetHighColor,unsetMidColor,unsetLowColor);
        def lgSet:LinearGradient = bind makeLG
            (setHighColor,setMidColor,setLowColor);
    
        scene = HBox
        {   spacing: bind boxHGap;
            content: bind for(i in [0..<boxCount])
            {   Group
                {   content:
                    [   Rectangle
                        {   width: bind boxWidth;
                            height: bind boxHeight;
                            arcWidth: bind arc;
                            arcHeight: bind arc;
                            fill: bind 
                                if(i<boxValue) setLowColor
                                else unsetLowColor;
                        } ,
                        Rectangle
                        {   x: bind border;
                            y: bind border;
                            width: bind boxWidth-border*2;
                            height: bind boxHeight-border*2;
                            arcWidth: bind arc;
                            arcHeight: bind arc;
                            fill: bind 
                                if(i<boxValue) lgSet
                                else lgUnset;                    
                        }
                    ]
                };
            }
        };
    }
    
    function makeLG(c1:Color,c2:Color,c3:Color) : LinearGradient
    {   LinearGradient
        {   endX: 0;  endY: 1;
            proportional: true;
            stops:
            [   Stop { offset:0;    color: c2; } ,
                Stop { offset:0.25; color: c1; } ,
                Stop { offset:0.50; color: c2; } ,
                Stop { offset:0.85;    color: c3; } 
            ];
        };        
    }
}
Our skin extends (unsurprisingly) Skin. This is where our UI code for the Progress control actually lives. We create our scene graph and plug it into the inherited scenevariable. The public properties at the head of the file will be controllable through a stylesheet, as we'll see shortly.

 

Step three creates a test application for our new Progress control.

package skintest;
import javafx.animation.*;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;

var val:Number = 0;
Stage
{   scene: Scene
    {   content: VBox
        {   spacing:10;
            translateX: 5;  translateY: 5;
            content:
            [   Progress
                {   minimum: 0;  maximum: 100;
                    value: bind val;    
                } ,
                Progress
                {   id: "testId";
                    minimum: 0;  maximum: 100;
                    value: bind val;    
                } ,
                Progress
                {   styleClass: "testClass";
                    minimum: 0;  maximum: 100;
                    value: bind val;    
                }
            ];
        };
        stylesheets: [ "{__DIR__}../Test.css" ]
        fill:  Color.BLACK;
        width: 200;  height: 100;    
    };
    title: "CSS Test";
    visible: true;
};

Timeline
{   repeatCount: Timeline.INDEFINITE;
    autoReverse: true;
    keyFrames:
    [   at(0s)   { val => 0 } ,
        at(0.1s) { val => 0 tween Interpolator.LINEAR } ,
        at(0.9s) { val => 100 tween Interpolator.LINEAR } ,
        at(1s)   { val => 100 }
    ];
}.play();

Here we create three instances of our control. The second instance has a specific id, and the third has been assigned a style class. The first has neither. The significance of this will be apparent when we look at the stylesheet, next.

Step four is to create a stylesheet.

"skintest.Progress"
{   boxWidth: 15;
    boxHGap: 2;
    
    setHighColor: yellow;
    setMidColor: red;
    setLowColor: darkred;
    
    unsetHighColor: cyan;
    unsetMidColor: blue;
    unsetLowColor: darkblue;
}

"skintest.Progress"#testId
{   boxWidth: 25;
    boxHeight: 30;
    boxCount: 7;
    boxHGap: 1;
    
    unsetHighColor: white;
    unsetMidColor: silver;
    unsetLowColor: dimgray;
}

"skintest.Progress".testClass
{   boxWidth: 7;
    boxHGap: 2;
    boxCount: 20;
    
    setHighColor: yellow;
    setMidColor: limegreen;
    setLowColor: darkgreen;
}

The above is an external stylesheet file, with three example style rules. The first rule will match any instance of theskintest.Progress control (all of them, in effect); the second rule will match only that Progress control which has the id "testId"; while the third rule matches any Progress control with the style class "testClass".

In each case we are able to manipulate the class properties, change its size, the number of boxes, its colours, etc — all without recompiling the JavaFX Script code.

The source code for this mini-project is available here. Have fun (and don't forget to buy "JavaFX in Action" when it comes out! :)