Channels ▼

Eric Bruno

Dr. Dobb's Bloggers

A JavaFX 2.0 Custom Control

April 03, 2011

In previous blogs over the past few months, I've loosely described JavaFX 2.0 and its big changes, soon to be available to the general public in beta form. Given I'm involved in the early access program, I've had a chance to work extensively with the new release and its all-new pure Java API, and I have to say I truly love it. It's not that JavaFX Script wasn't good to me, but working with Java is much more familiar and comfortable, especially for large JavaFX projects.

So far, I've built a few simple "crash and burn" experiment applications, as I like to call them. You know, the types of projects that you start to gain an understanding of something new, but quickly abandon because you've hacked the heck out of it. But I became proficient enough to port an existing custom JavaFX 1.3.1 control to JavaFX 2.0, with the help of the FXTranslator tool Oracle is working on to help translate existing JavaFX Script code to pure Java code that uses the new JavaFX 2.0 API. The results can seen in the comparison screen shot from my previous blog here.

However, to best illustrate the new JavaFX 2.0 API, I decided to build a simple custom JavaFX control. I've chosen to build a type of button that I've seen used on the iPhone and iPad; one that I call an "arrow button." This type of control works like a button, which contains text and is clickable, but indicates that you'll be moved to the next screen or step, or back to the previous screen, because the button is in the shape of an arrow. To illustrate, the screen shot below shows a mock bank account application, where the button labeled "Accounts" is an arrow button. Intuitively, the arrow indicates that you'll move to a new screen view when clicked.

AppScreen

Now, let's get to the code!

The JavaFX 2.0 Application

To begin, let's start with a simple JavaFX 2.0 application that will contain the components for the application shown in the picture above. In NetBeans, I've created a new project with a reference to the JavaFX 2.0 runtime JAR file. Next, I've create a Java class called Main, which has the usual public static void main() method to make it a Java application, as you would expect. To turn down the road to JavaFX 2.0 development land, simply extend the new Application class, add the required public void start method and a call to the JavaFX Launcher, as shown below:

    package arrowbutton;
    import javafx.application.Application;
    …

    public class Main extends Application {
   
        @Override public void start(Stage stage) {
            … 
        }

        public static void main(String[] args) {
            Launcher.launch(Main.class, args);
        }
    }

We'll get to the code inside the start method in a bit. For now, let's define the custom control.

The JavaFX 2.0 Custom Arrow Button

I tend to use interfaces where applicable, as it fosters good coding practice, and so it is with my custom control development. I begin to define the arrow button by creating its API interface, shown below:

    public interface ArrowButtonAPI {
        public static final int RIGHT = 1;
        public static final int LEFT = 2;
    
        public void setText(String text);
        public void setOnMouseClicked(EventHandler eh);
        public void setDirection(int direction);
    }

The arrow has a direction it points (left or right), text that it displays, and behavior (it's clickable). You call setDirection to display an arrow button that points in the specified direction; setText to provide the button's text, and setOnMouseClicked to provide a callback in the form of a JavaFX 2.0 EventHandler, to be called when the user clicks on the button. Next, the control framework is created with a class that extends the javafx.scene.control.Control base class, as shown here:

package arrowbutton;
import javafx.event.EventHandler;
import javafx.scene.control.Control;
import javafx.scene.control.Skin;

public class ArrowButton extends Control implements ArrowButtonAPI  {
    private String title = "";

    public ArrowButton() {
        this.storeSkin( new ArrowButtonSkin(this) );
    }

    public ArrowButton(String title) {
        this();
        this.title = title;
        ArrowButtonSkin skin = (ArrowButtonSkin)this.getSkin();
        skin.setText(title);
    }

    public void setText(String text) {
        getSkin(getSkin()).setText(text);
    }

    public void setOnMouseClicked(EventHandler eh) {
        getSkin(getSkin()).setOnMouseClicked(eh);
    }

    public void setDirection(int direction) {
        getSkin(getSkin()).setDirection(direction);
    }

    private ArrowButtonSkin getSkin(Skin skin) {
        return (ArrowButtonSkin)skin;
    }
    … 
}

There are two constructors: one that takes a String as a shortcut to set the button's text, and one without. In either case, the arrow button's skin is created and stored within the control. The skin defines the actual graphical elements of the button, as well as most of its behavior. The remainder of the ArrowButton control class implements the ArrowButtonAPI interface, and other required methods such as those that handle layout (not shown), and so on. Let's move on to the real fun; the skin class.

The ArrowButtonSkin Class

The arrow button control contains a skin class that implements the javafx.scene.control.Skin interface, which requires you to define a root node for your control (which contains the graph of nodes that make up your control's look and feel), and methods to gain access to the root node, and so on:

public class ArrowButtonSkin implements Skin<ArrowButton>, ArrowButtonAPI {
    static final double ARROW_TIP_WIDTH = 5;

    ArrowButton control;
    String text = "";

    Group rootNode = new Group();
    Label lbl = null;
    int direction = ArrowButtonAPI.RIGHT;
    EventHandler clientEH = null;

    public ArrowButtonSkin(ArrowButton control) {
        this.control = control;
        draw();
    }

    public ArrowButton getControl() {
        return control;
    }

    public Node getNode() {
        return rootNode;
    }

    public void dispose() {
    }

    //////////////////////////////////////////////////////////////

    public void draw() {
        … 
    }

    public void setText(String text) {
        this.text = text;
        lbl.setText(text);

        // update button
        draw();
    }

    public void setOnMouseClicked(EventHandler eh) {
        clientEH = eh;
    }

    public void setDirection(int direction) {
        this.direction = direction;

        // update button
        draw();
    }
}

I've listed the required methods first, followed by the ArrowButtonAPI methods (which are called from the ArrowButton control class), as well as an additional method, draw. You don't need to implement it this way — in fact, the intent is to actually create the node graph for your custom control in the getNode method — but I prefer to do all of the layout this way so I can call it when certain things change. Alternatively, JavaFX binding can be used to eliminate that, but since it's still in a state of flux in JavaFX 2.0, I've avoided it so far. As binding progresses (and I get my head around how it actually works in the new API), I'll include a comprehensive example at a later date.

For now, let's look in more detail at the draw method, since this is the where the real action takes place. First, the code creates a label, stores its width and height (which varies by the text length and font size), and positions it within the control using offsets:

        if ( lbl == null )
            lbl = new Label(text);

        double labelWidth =lbl.getBoundsInLocal().getWidth();
        double labelHeight = lbl.getHeight();
        lbl.setTranslateX(2);
        lbl.setTranslateY(2);

Next comes the rather lengthy bit of code to create the lines and curves that make up the arrow button itself. This code uses the JavaFX 2.0 Path class to define a custom shape that makes up the control's body. The shape is comprised of a starting point, a top flat line, a curved line for the top part of the arrow, another curved line for the bottom part of the arrow, a bottom flat line, and a vertical line to close the shape:

        // Create arrow button line path elements
        Path path = new Path();
        MoveTo startPoint = new MoveTo();
        double x = 0.0f;
        double y = 0.0f;
        double controlX;
        double controlY;
        double height = labelHeight; 
        startPoint.setX(x);
        startPoint.setY(y);

        HLineTo topLine = new HLineTo();
        x += labelWidth;
        topLine.setX(x);

        // Top curve
        controlX = x + ARROW_TIP_WIDTH;
        controlY = y;
        x += 10;
        y = height / 2;
        QuadCurveTo quadCurveTop = new QuadCurveTo();
        quadCurveTop.setX(x);
        quadCurveTop.setY(y);
        quadCurveTop.setControlX(controlX);
        quadCurveTop.setControlY(controlY);

        // Bottom curve
        controlX = x - ARROW_TIP_WIDTH;
        x -= 10;
        y = height;
        controlY = y;
        QuadCurveTo quadCurveBott = new QuadCurveTo();
        quadCurveBott.setX(x);
        quadCurveBott.setY(y);
        quadCurveBott.setControlX(controlX);
        quadCurveBott.setControlY(controlY);

        HLineTo bottomLine = new HLineTo();
        x -= labelWidth;
        bottomLine.setX(x);

        VLineTo endLine = new VLineTo();
        endLine.setY(0);

        path.getElements().add(startPoint);
        path.getElements().add(topLine);
        path.getElements().add(quadCurveTop);
        path.getElements().add(quadCurveBott);
        path.getElements().add(bottomLine);
        path.getElements().add(endLine);

Next, to add a little UI flare (this is JavaFX, after all), the arrow shape is filled with a linear gradient, consisting of two grey colors. This is done by defining the stop points of the gradient within an array, and the type of gradient you want (linear or radial):

        // Create and set a gradient for the inside of the button
        Stop[] stops = new Stop[] {
            new Stop(0.0, Color.LIGHTGREY),
            new Stop(1.0, Color.SLATEGREY)
        };
        LinearGradient lg =
            new LinearGradient( 0, 0, 0, 1, true, CycleMethod.NO_CYCLE, stops);
        path.setFill(lg);

You can define as many stop points and colors as you'd like in a gradient. Finally, the text label and the custom shape path are added as child nodes to the root node with a single statement:

        rootNode.getChildren().setAll(path, lbl);

To pass along mouse clicks to the client callback EventHandler, we need to provide our own EventHandler to handle clicks on our control's root node. The simple way to handle this is with an anonymous inner class, shown here:

        rootNode.setOnMouseClicked(new EventHandler<MouseEvent>() {
            public void handle(MouseEvent me) {
                // Pass along to client if an event handler was provided
                if ( clientEH != null )
                    clientEH.handle(me);
            }
        });

The EventHandler we create handles MouseEvents (indicated with Java Generics), and passes along the event notifications if the client has provided its own EventHandler. All that's left is to write code that actually uses the arrow button. Let's look at that now.

Using the Arrow Button

Remember the start() method at the beginning of this blog? Let's fill it in now to create the JavaFX 2.0 application shown in the screen shot above. Notice that that application's Stage object is passed as a parameter; the JavaFX scene graph exists almost exactly as it did in earlier versions. To set the application title, you set the Stage's title:

    stage.setTitle("The JavaFX Bank");

Next, I've chosen a Group layout for this application. I could have otherwise chosen HBox, VBox, GridPane, and so on, but I prefer to handle the layout manually for this example. Next, I create a normal button (the Close button), an arrow button, and a label with some description text:

        // Create the node structure for display
        Group rootNode = new Group();
        Button normalBtn = new Button("Close");
        normalBtn.setTranslateX(140);
        normalBtn.setTranslateY(170);
        final Stage s = stage;
        normalBtn.setOnMouseClicked(new EventHandler<MouseEvent>() {
            public void handle(MouseEvent me) {
                // Close the stage (accessible through the scene graph)
                Node node = (Node)me.getSource();
                node.getScene().getStage().close();
            }
        });

        // Create a directional arrow button to display account information
        ArrowButton accountBtn = new ArrowButton("Accounts");
        accountBtn.setDirection(ArrowButton.RIGHT);
        accountBtn.setTranslateX(125);
        accountBtn.setTranslateY(10);
        accountBtn.setOnMouseClicked(new EventHandler<MouseEvent>() {
            public void handle(MouseEvent me) {
                // ...
            }
        });

        // Handle arrow button press
        accountBtn.setOnMouseClicked(new EventHandler<MouseEvent>() {
            public void handle(MouseEvent me) {
                System.out.println("Arrow button pressed");
            }
        });

        // Some description text
        Label description = new Label(
                "Thanks for logging into the\n"
                + "JavaFX Bank. Click the button\n"
                + "above to move to the next \n"
                + "screen, and view your active \n"
                + "bank accounts.");
        description.setTranslateX(10);
        description.setTranslateY(50);

Notice I created EventHandlers to know when the Close and arrow buttons are clicked. Next, the three controls are added to the scene graph as follows:

        rootNode.getChildren().add(accountBtn);
        rootNode.getChildren().add(description);
        rootNode.getChildren().add(normalBtn);

Finally, the Stage's Scene is created, the scene graph is completed, and the stage is made visible:

        Scene scene = new Scene(rootNode, 200, 200);
        stage.setScene(scene);
        stage.setVisible(true);

Voila! There you have a shiny new JavaFX 2.0 application with a custom JavaFX control that looks and works like one of those fancy arrow buttons on the iPhone. In a future blog, I plan to cover binding in JavaFX 2.0, as well as property change listeners, which can be used as an alternative to binding. Happy coding!

— EJB

Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.
 


Video