TECHNOLOGY: JavaFX

As Published In
Oracle Magazine
November/December 2010

  

Customize Your Application Layout

By James L. Weaver

 

When stock layouts just won’t do, create custom layout managers in JavaFX.

JavaFX is a platform and scripting language for creating rich internet applications (RIAs) that execute on the desktop, in Web browsers, on mobile phones, and on TV set-top boxes. JavaFX was introduced in 2007, and version 1.3 was released in April 2010.

JavaFX uses a stage/scene metaphor in which the “stage” is akin to a drawing surface for program execution. The stage can hold one or more “scenes” comprising one or more node graphs, a node being the lowest-level data structure on the JavaFX platform. Each new release of JavaFX has expanded its capabilities and the ease with which developers can create, manipulate, and animate the contents rendered with this stage/scene metaphor. JavaFX 1.3 includes some changes to the underlying API that enable developers to combine approaches to layout management in new ways.

This article introduces some of the built-in layout managers included with JavaFX 1.3 and shows you how to extend layout management to include a unique custom layout. The article steps through a sample application that manipulates images of playing cards (see Figure 1).

 

o60 javafx figure 1

Figure 1: The completed sample application (PlayingCardLayoutB)
 

For more information about JavaFX, see “Building GUI Applications With JavaFX,” at download.oracle.com/javafx/1.3/tutorials/ui. For an introduction to JavaFX Script, see “Learning the JavaFX Script Programming Language,” at download.oracle.com/javafx/1.3/tutorials/core

Obtaining and Running the Example

To work through the sample code, you must have the NetBeans IDE installed and configured on your system (Linux, Mac, Oracle Solaris x86, or Windows). If you haven’t installed NetBeans yet, obtain NetBeans IDE 6.9.1 for JavaFX 1.3.1 (javafx.com/downloads/all.jsp), which includes JavaFX Composer, for developing JavaFX applications in a graphical development environment, as well as the JavaFX 1.3.1 SDK.

Download the appropriate installer for your development machine, and install the NetBeans IDE. Next download the PlayingCardLayout NetBeans project file from Oracle Technology Network at oracle.com/technetwork/issue-archive/ 2010/10-nov/o60javafx-168643.zip and unpack it into your working directory.

The PlayingCardLayoutA project contains the starter code that lays out several cards in vertical and horizontal arrangements. The PlayingCardLayoutB project contains the completed project with custom fan-style layout. Both projects consist primarily of three JavaFX script files: 

  • PlayingCard.fx
  • PlayingCardMain.fx
  • PlayingCardLayout.fx 

The PlayingCardMain script creates the stage and lays out the groups of playing cards according to the PlayingCardLayout script. The project also includes 54 image files, one for each of the cards in a full deck.

To get a sense of the application’s behavior, compile and run PlayingCardLayoutA as follows: 

  1. Start NetBeans, and select File -> Open Project.
  2. When the Open Project dialog box appears, navigate to the working directory and open the PlayingCardLayoutA project.
  3. Build the application by selecting Run -> Build Main Project.
  4. To run the application, select Run -> Run Main Project (or click Run Project on the toolbar). 

The application displays a horizontal card stack and a vertical card stack (see Figure 2).

 

o60 javafx figure 2

Figure 2: PlayingCardLayoutA project running from the NetBeans IDE
 

Click any card in either stack to observe the application’s behavior: whichever card you click moves to the frontmost position in the stack.

In this article, you’ll see how to modify the code to implement the fan-shaped arrangement of the PlayingCardLayoutB project. Let’s start with an overview of the PlayingCard class, which is the same for both projects. 

PlayingCard as a “Managed” Node

The PlayingCard.fx script (see Listing 1) defines the behavior of each playing card instance (node) in a stack. As shown in Listing 1, PlayingCard extends CustomNode, the abstract base class for defining node subclasses. The code overrides the children variable, assigning to it a sequence of nodes. Binding the node to the content attribute as a member of a Group object will cause the objects created with this class definition to be rendered, in order, as a sequence.

Code Listing 1: PlayingCard.fx script—class definition for PlayingCard nodes 

package playingcard.ui;
import javafx.scene.*;
import javafx.scene.input.*;

public class PlayingCard extends CustomNode {
  public var node:Node;
  override public var blocksMouse = true;
  override var children = Group {
    content: bind node
    // Demonstrate the results of unmanaging a node
    onMousePressed: function(me:MouseEvent):Void {
      managed = false;
      toFront();
    }
    onMouseReleased: function(me:MouseEvent):Void {
      managed = true;
    }
  }
}

 

The PlayingCard class also defines two different handlers for mouse events on each node (playing card), using the inherited variables onMousePressed and onMouseReleased. The function defined for the onMousePressed variable sets the managed variable to false, thereby disassociating the selected node from the container’s management. (The managed variable is a new variable of Node, as of the JavaFX 1.3 release.)

The toFront() function then moves the unmanaged node to the front of that group of nodes, thus achieving the action of the application. That is, when a user clicks a particular card, the state of the card goes from managed to unmanaged and the unmanaged card is sent by the toFront() function to the front of the stack of cards.

When the mouse button is released (onMouseReleased), the managed variable is set once again to true and the node is held in its position within its container, which, in the case of the sample applications, is handled by the custom layout manager. 

Introduction to JavaFX Layout Managers

The built-in JavaFX layout managers are containers that support the positioning and sizing of UI components in a cross-platform manner. The layout managers inherit base properties and functions from the Container class (javafx.scene.layout.Container). In general, any custom layout manager you create, including those that are part of this sample project, will extend this same base class. The built-in layout managers include

ClipView—acts as a clipping view of its content, potentially responding to unblocked mouse events by panning the clipped region
  • Flow—lays out nodes in a horizontal or vertical flow, wrapping as necessary
  • HBox—lays out nodes in a single horizontal row, with configurable spacing and alignment
  • Stack—lays out content nodes in a back-to-front stack
  • Tile—lays out content nodes in uniform-size areas
  • VBox—lays out content nodes in a single vertical column 

One of the variables inherited from the Container class by all layout managers is the content instance variable, which identifies the nodes the layout manager will contain at runtime. For example, in the following code snippet, the content variable for the scene contains two layout managers, HBox and VBox, each containing its own content variable that is set to a different series of playing card nodes: 

Stage {
  var sceneRef:Scene;
  title: "PlayingCard Layout Example"
  scene: sceneRef = Scene {
    width: 600
    height: 400
    content: [
      HBox {
        layoutX: 100
        layoutY: 0
        spacing: -58
        content: for (i in [1..12])
          PlayingCard {
            node: ImageView {
            image: Image {
              url: "{__DIR__}images/{i}
.png"
                    }
                }
            }
        },

      VBox {
        layoutX: 0
        layoutY: 0
        spacing: -72
        content: for (i in [13..20])
          PlayingCard {
            node: ImageView {
            image: Image {
              url: "{__DIR__}images/{i}
.png"
                    }
                }
            }
        },
        ]
    }
}

 

This approach (directly setting the starting coordinates and the spacing between nodes) might be fine if all you wanted an application to do was lay out one row and one column of cards. But the approach breaks down when you consider adding more cards or when you want to lay out the cards in a different orientation. For example, for this project, you want to add a fan-style arrangement to the layout—that is, the cards fan out from the lower left corner of each card as the pivot point for the rotation, with card values and suits visible. None of the built-in layout managers provide such a layout. 

Overview of the PlayingCardMain.FX Script

Rather than hard-coding the layout manager directly into the scene specification, as shown in the previous snippet, both PlayingCardLayoutA and PlayingCardLayoutB use a custom layout manager in conjunction with instance variables to lay out the nodes at runtime. The custom layout manager is defined in PlayingCardLayout.fx. The PlayingCardMain.fx scripts include this layout manager class as one of their imports, along with the Stage, Scene, image, and layout classes, as shown here:

import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.image.*;
import javafx.scene.layout.*;
import playingcard.layout.PlayingCardLayout;

 

For the variously sized groups of PlayingCard objects, the PlayingCardMain .fx scripts also create instance variables for holding these groups (see Listing 2).

Code Listing 2: PlayingCardMain.fx script 

/* PlayingCardMain.fx - to demonstrate PlayingCardLayout custom layout */

package playingcard.ui;

import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.image.*;
import javafx.scene.layout.*;
import playingcard.layout.PlayingCardLayout;

var firstPile:PlayingCard[] = for (i in [1..10])
  PlayingCard {
    node: ImageView {
      image: Image {
        url: "{__DIR__}images/{i}.png"
      }
    }
  };

var secondPile:PlayingCard[] = for (i in [11..24])
  PlayingCard {
    node: ImageView {
      image: Image {
        url: "{__DIR__}images/{i}.png"
      }
    }
  };

var thirdPile:PlayingCard[] = for (i in [25..54])
  PlayingCard {
    node: ImageView {
      image: Image {
        url: "{__DIR__}images/{i}.png"
      }
    }
  };

Stage {
  var sceneRef:Scene;
  title: "PlayingCardLayout Example"
  scene: sceneRef = Scene {
    width: 600
    height: 400
    content: [
      HBox {
        layoutX: 20
        layoutY: 20
        spacing: 20
        content: [
          PlayingCardLayout {
            arrangementStyle: PlayingCardLayout.VERTICAL_ARRANGEMENT
            content: firstPile
          },
          PlayingCardLayout {
            arrangementStyle: PlayingCardLayout.HORIZONTAL_ARRANGEMENT
            content: secondPile
          },
          PlayingCardLayout {
            arrangementStyle: PlayingCardLayout.FAN_ARRANGEMENT
            content: thirdPile
          }
        ]
      }
    ]
  }
}

 

To accommodate the fan-shaped layout, add an instance variable to the code in PlayingCardLayoutA’s main script (PlayingCardMain.fx) for a third pile of cards. 

var thirdPile:PlayingCard[] = for (i in [25..54])
  PlayingCard {
    node: ImageView {
      image: Image {
        url: "{__DIR__}images/{i}.png"
      }
    }
  };

 

Come back to this main script again after you’ve set up the custom layout class, because it’s in that class that you’ll determine into which layout any given stack of cards should be populated at runtime. 

Creating a Custom Layout Class

To create a layout manager, create a subclass of the Container class (javafx.scene.layout.Container). For this project, the PlayingCardLayout.fx script creates this subclass (see Listing 3).

Code Listing 3: PlayingCardLayout.fx script—completed custom layout manager class

/*
 * PlayingCardLayout.fx -
 * A custom layout that arranges playing cards
 */

package playingcard.layout;

import javafx.util.Math;
import javafx.scene.Node;
import javafx.scene.layout.*;
import javafx.scene.layout.Container.*;
import javafx.scene.transform.*;

public def VERTICAL_ARRANGEMENT:Integer = 0;
public def HORIZONTAL_ARRANGEMENT:Integer = 1;
public def FAN_ARRANGEMENT:Integer = 2;

def verticalOffset:Number = 27;
def horizontalOffset:Number = 12;
def fanAngle:Number = 7.0;

public class PlayingCardLayout extends Container {

  public var arrangementStyle:Integer = VERTICAL_ARRANGEMENT on replace {
    preferredDirty = true;
    requestLayout();
  };

  var preferredWidth:Number;
  var preferredHeight:Number;
  var preferredDirty = true;

  override var content on replace {
      preferredDirty = true;
  }

  function calcPreferredSize():Void {
    def managedContent = getManaged(content);
    def numNodes = sizeof managedContent;
    var lastNode:Node = null;  //Assumption: All cards are the same size
    var lastNodeWidth:Number = 0;
    var lastNodeHeight:Number = 0;
    if (sizeof managedContent > 0) {
      lastNode = managedContent[numNodes - 1];
      lastNodeWidth = getNodePrefWidth(lastNode);
      lastNodeHeight = getNodePrefHeight(lastNode);
    }
    preferredWidth = preferredHeight = 0;
    if (arrangementStyle == VERTICAL_ARRANGEMENT) {
      preferredWidth = lastNodeWidth;
      preferredHeight = numNodes * verticalOffset + lastNodeHeight;
    }
    else if (arrangementStyle == HORIZONTAL_ARRANGEMENT) {
      preferredWidth = numNodes * horizontalOffset + lastNodeWidth;
      preferredHeight = lastNodeHeight;
    }
    else {  // arrangementStyle == FAN_ARRANGEMENT
       preferredWidth = Math.sqrt(Math.pow(lastNodeWidth, 2) +
Math.pow(lastNodeHeight, 2)) * 2;
       preferredHeight = preferredWidth;
    }
    preferredDirty = false;
  }

  override function doLayout():Void {
    if (preferredDirty) {
      calcPreferredSize();
    }
    if (arrangementStyle == VERTICAL_ARRANGEMENT) {
      for (node in getManaged(content)) {
        layoutNode(node, 0, indexof node * verticalOffset,
                   getNodePrefWidth(node), getNodePrefHeight(node));
      }
    }
    else if (arrangementStyle == HORIZONTAL_ARRANGEMENT) {
     for (node in getManaged(content)) {
        layoutNode(node, indexof node * horizontalOffset, 0,
                   getNodePrefWidth(node), getNodePrefHeight(node));
      }
    }
    else if (arrangementStyle == FAN_ARRANGEMENT) {
      def startAngle = (sizeof getManaged(content)) * -0.5 * fanAngle;
      for (node in getManaged(content)) {
        def rot = Rotate.rotate(startAngle + (indexof node * fanAngle), 0,
                                node.layoutBounds.height);
        delete node.transforms;
        insert rot into node.transforms;
        var ph = getNodePrefHeight(node);
        var pw = getNodePrefWidth(node);
        layoutNode(node, ph, pw, pw, pw);
      }
    }
  }

  override function getPrefWidth(height:Number):Number {
    if (preferredDirty) {
      calcPreferredSize();
    }
    return preferredWidth;
  }
  override function getPrefHeight(width:Number):Number {
    if (preferredDirty) {
      calcPreferredSize();
    }
    return preferredHeight;
  }
}

The Container class provides numerous utility and other functions and instance variables that support the basic behavior needed by a containing object, one that manages nodes. To create the Container subclass, include these imports at the top of your custom layout script and make the class declaration, as in 

import javafx.scene.Node;
import javafx.scene.layout.*;
import javafx.scene.layout.Container.*;
...
public class PlayingCardLayout extends Container { 

 

In the body of the class definition, you must override three specific functions inherited from Container: 

  • doLayout()
  • getPrefWidth()
  • getPrefHeight() 

You can also take advantage of many of the underlying functions inherited from Container that are designed to manage nodes. Subsequent steps in this article show you how to do both, but first let’s add code to the PlayingCardLayout class to support different layouts.

Step 1: Define constants and variables in the custom layout. You want the PlayingCardLayout class to be able to support three different layout arrangements, so define three constants to do the job: 

public def 
VERTICAL_ARRANGEMENT:Integer = 0;
public def HORIZONTAL_ARRANGEMENT:Integer = 1;
public def FAN_ARRANGEMENT:Integer = 2;

 

If you’re working through the code for PlayingCardLayoutA, add the FAN_ARRANGEMENT definition to your PlayingCardLayout.fx script under the existing VERTICAL_ARRANGEMENT and HORIZONTAL_ARRANGEMENT statements.

In the body of the PlayingCardLayout class definition is a public instance variable, arrangementStyle, whose definition includes a replace trigger that automatically runs whenever the value of the variable changes: 

public var arrangementStyle:Integer = VERTICAL_ARRANGEMENT on replace {
   preferredDirty(true);
   requestLayout();

 

The variable is initialized with the VERTICAL_ARRANGEMENT constant, but the associated trigger is activated whenever this variable value changes, setting the preferredDirty variable to true and calling the requestLayout() function, another Container class function. (As you’ll discover in subsequent steps, the preferredDirty variable acts as a flag throughout the PlayingCardLayout code.)

The requestLayout() function tells the JavaFX runtime to make a layout pass run before rendering the next scene. This triggering mechanism, in conjunction with code in the doLayout() function, facilitates the rendering of the three layouts.

Note that the content variable is being overridden, also through use of a replace trigger. Each time the designated layout (horizontal, vertical, or fan-shaped) changes, the value of the variable changes. 

override var content on replace {
  preferredDirty = true;
}

 

All layout managers have a content instance variable, inherited from the Container class, whose purpose is to be assigned a sequence of nodes to be managed. The custom layout class uses this variable in the overridden doLayout() function in a subsequent step.

The calcPreferredSize() function is a helper function in PlayingCardLayout that calculates the preferred width and height for the nodes it is processing for the calling function and returns the dimensions that will be required for the layout. When it’s finished, the function sets the value of the preferredDirty variable to false. If you’re working through the example, add the following code to the calcPreferredSize() function to handle the width and height values for the fan-shaped layout: 

else {  // arrangementStyle == FAN_ARRANGEMENT
  preferredWidth = Math.sqrt(
    Math.pow(lastNodeWidth, 2) +
    Math.pow(lastNodeHeight, 2)) * 2;
  preferredHeight = preferredWidth;
}

 

And be sure to also change the else for the arrangementStyle == HORIZONTAL_ARRANGEMENT statement to an else if for proper syntax.

Step 2: Override the doLayout() function. The doLayout() function contains the rules for how nodes should be laid out. For the example projects, the overridden doLayout() function iterates over the managed nodes and affects their sizes and positions. 

override function doLayout():Void {
  if (preferredDirty) {
    calcPreferredSize();
  }

 

When the JavaFX runtime calls the doLayout() function, it first determines if the arrangement of a layout manager’s nodes should be re-evaluated by checking the value of the preferredDirty variable. When preferredDirty is set to true, doLayout() calls the calcPreferredSize() helper function to calculate the width and the height based on the arrangement style, size, and number of cards to be arranged, as well as the offset value.

The calcPreferredSize() function returns these values for the card stack in question and then resets the preferredDirty variable to false, so that when the processing control returns back to the calling function (in this case, doLayout()), the returned values will be handled by the appropriate IF statement in the function. In other words, the playing card will be positioned properly within its designated arrangement (horizontal, vertical, or fan-shaped). 

  if (arrangementStyle == VERTICAL_ARRANGEMENT) {
    for (node in getManaged(content)) {
      layoutNode(node, 0, 
        indexof node * verticalOffset,
        getNodePrefWidth(node),
        getNodePrefHeight(node));
    }
  }
  else if (arrangementStyle == HORIZONTAL_ARRANGEMENT) {
   for (node in getManaged(content)) {
      layoutNode(node, 
      indexof node * horizontalOffset, 0, 
      getNodePrefWidth(node),
      getNodePrefHeight(node));
    }
  }
  else if (arrangementStyle == 
FAN_ARRANGEMENT) {
    def startAngle = (sizeof getManaged(content)) * -0.5 * fanAngle;
    for (node in getManaged(content)) {
      def rot = Rotate.rotate(
          startAngle + (indexof node * fanAngle), 0,
          node.layoutBounds.height);
      delete node.transforms;
      insert rot into node.transforms;
      var ph = getNodePrefHeight(node);
      var pw = getNodePrefWidth(node);
      layoutNode(node, ph, pw, pw, pw);
    }
  }
}

 

Next Steps


 DOWNLOAD
the sample code for this article
the JavaFX 1.3.1 API and NetBeans IDE 6.9.1
 

 READ more
technical articles about JavaFX
Building GUI Applications With JavaFX
Learning the JavaFX Script Programming Language

 LEARN
how to get started with NetBeans IDE 6.9 for JavaFX
more about the JavaFX 1.3.1 API
 

 VISIT
more about the JavaFX 1.3.1 API

Within the overridden doLayout() function are several convenience functions (from Container) for managing nodes: 

  • getManaged()—returns the group (sequence) of nodes being managed by the container—that is, those nodes whose managed instance variable is true. By default, all nodes are created with the default value, but if you want a node to be exempt from the policies of the layout manager, set its managed variable to false.
  • layoutNode()—sets the position of a node and attempts to resize it. This is an overloaded function that has several variations from which you can choose, depending on your needs. Check the JavaFX API reference documentation (download.oracle.com/docs/cd/E17802_01/javafx/javafx/1.3/docs/api) for complete details.
  • getNodePrefWidth()—determines the preferred width of a node.
  • getNodePrefHeight()—determines the preferred height of a node. 

Step 3: Override the getPrefWidth() and getPrefHeight() functions. You must override the getPrefWidth() and getPrefHeight() functions, returning values calculated according to the layout manager’s contents. Like the doLayout() function, these two overridden functions also call the calcPreferredSize() function to obtain the necessary width and height values, and they also use the preferredDirty variable as a Boolean flag to force recalculation of the containing layout manager’s width and height, based on the actual content at runtime. 

...
override function getPrefWidth(height:
Number):Number {
  if (preferredDirty) {
    calcPreferredSize();
  }
  return preferredWidth;
}
override function getPrefHeight(width:
Number):Number {
  if (preferredDirty) {
    calcPreferredSize();
  }
  return preferredHeight;
}
...

  

Modify the PlayingCardMain Script

Much of the application’s work is handled by the custom layout class, so modifying the main script is minor. All you need to do to use the fan-style layout in the main script is add a few lines to the scene that references the content nodes using the fan-style arrangement, as shown here:

...
content: [
  PlayingCardLayout {
    arrangementStyle: PlayingCardLayout.VERTICAL_ARRANGEMENT
    content: firstPile
  },
  PlayingCardLayout {
    arrangementStyle: PlayingCardLayout.HORIZONTAL_ARRANGEMENT
    content: secondPile
  },
  PlayingCardLayout {
    arrangementStyle: PlayingCardLayout.FAN_ARRANGEMENT
    content: thirdPile
...

 

After making all the changes to PlayingCardLayoutA for the fan-style arrangement, build and run the application. Explore some of the many artifacts produced by the NetBeans IDE for various deployment options. As you would for any software you create, always thoroughly test your custom layout classes with various sizes and numbers of nodes to ensure that users of your layout class experience the behavior you’ve intended. Play around with both examples to see how the different variables and functions available in the API can enhance (or hurt) your user interface. 

Conclusion

JavaFX includes several built-in layout managers, but occasionally they don’t meet the requirements of an application. When this happens, consider creating your own, following the steps detailed in this article.

 

 


James L. Weaver has been a developer for more than 25 years. Since 2000 he has specialized in Java, object-oriented, and Web-based technologies. He is the author of Pro JavaFX Platform: Script, Desktop and Mobile RIA with Java Technology (Apress, 2009) and JavaFX Script: Dynamic Java Scripting for Rich Internet/Client-Side Applications (Apress, 2007).

 

 

Send us your comments