Channels ▼
RSS

Design

JavaFX Custom Controls


The Multi-select List View

To create the custom multi-select list view control, I created the following three classes:

  • ListItem that represents each entry within the list view
  • CustomList extends the JavaFX Control class
  • CustomListSkin which is the main user interface (extends the Skin class)

ListItem (see Listing 1) represents an entry in the custom list view control. It contains the text to be displayed as well as a rectangle for the item's background color. For example, by default each line in the list is displayed in a choice of two alternating background colors; the rectangle implements this. I decided to extend javafx.scene.CustomNode, which makes each line in the custom list view control its own GUI entity. As a result, each line can have its own height and width, as well as any of the characteristic of a JavaFX Node object. This allows for very customizable entries in the list view; i.e. each line can have its own font, height, color combinations, graphics, and so on.


package customlist;

import javafx.scene.CustomNode;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Text;
import javafx.scene.text.TextOrigin;
import javafx.scene.input.MouseEvent;
import javafx.scene.text.Font;

def SELECTED_COLOR = Color.rgb(91, 127, 255); // Blue-ish color

public class ListItem extends CustomNode {
    public var listView : CustomList = null;
    public var selected :Boolean = false;
    public var bgColor : Color = Color.WHITE;
    public var text : String = "";
    public var position : Integer = -1;
    public var font: Font = Font { name: "dialog" size: 10 };
    public var height = bind bgRect.height;
    var fgColor : Color = Color.BLACK;
    var prevBgColor : Color;

    var itemText = Text {
        x: 0
        y: 0
        font: bind font;
        content: bind text
        textOrigin: TextOrigin.TOP
        fill: bind fgColor;
    };

    var bgRect = Rectangle {
        x: 0
        y: 0
        width: bind listView.width
        height: bind this.itemText.boundsInLocal.height 
        fill: bind bgColor; 
    }

    override function create():Node {
        Group {
            content: [
                bgRect, itemText
            ]
        };
    }

    override var onMousePressed = function(e : MouseEvent) {
        if ( selected == false) {
            selected = true;
            prevBgColor = bgColor;
            fgColor = Color.WHITE;
            bgColor = SELECTED_COLOR; // Blue-ish color
        }
        else {
            selected = false;
            fgColor = Color.BLACK;
            bgColor = prevBgColor;
        }
    }
}

Listing 1

This class also maintains the item's position within its containing list control, and a flag that indicates whether or not the item is selected (has been clicked on) by the user. It also handles mouse-click events to toggle the item's selected status. However, the important part of the ListItem class is its content, which is set in the overriden create method:


override function create():Node {
    Group {
        content: [
            bgRect, itemText
        ]
    };
}

Here, a Rectangle and a Text object are placed in a group, laid out in the order they're listed. In this case, the

Rectangle

is displayed first with the Text object on top of it. The height of the Rectangle is bound to the height of the font used for the item's Text component (which by default is 10-point "dialog", but you can override that).

The mouse clicks are handled by overriding the onMousePressed function, where a Boolean flag is toggled to control the item's selection status:


override var onMousePressed = function(e : MouseEvent) {
    if ( selected == false) {
        selected = true;
        prevBgColor = bgColor;
        fgColor = Color.WHITE;
        bgColor = SELECTED_COLOR; // Blue-ish color
    }
    else {
        selected = false;
        fgColor = Color.BLACK;
        bgColor = prevBgColor;
    }
}

When the item becomes selected, the text is displayed in white on a blue background. Otherwise it's black on a white or grey background -- remember that the backgound color alternates. Next, let's look at the CustomList class, and how it manages the set of ListItem objects within it.

CustomList (see Listing 2) extends Control, maintains the list of all items in the list, and exposes an API to work with the list itelf. This includes the setItems method, which lets you add an array of ListItem objects to insert into the list; and getSelected, which returns an array of just the selected items in the list. Internally, this class handles colorizing the individual items in the list, as well as the arrow keys to scroll via the keyboard.


package customlist;

import javafx.scene.Node;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.scene.control.Control;
import javafx.scene.input.KeyCode;

public class CustomList extends Control {
    public var font = Font { name: "dialog" size: 10 };

    public var items : ListItem[] on replace {
        colorize();
    };

    override var focusTraversable = true;

    public function setItems(newItems : ListItem[]) {
        this.items = newItems;
    }

    function colorize() {
        var counter : Integer = 0;
        for ( item in items ) {
            item.listView = this;
            item.font = font;
            if ( counter++ == 1 ) {
                item.bgColor = Color.WHITESMOKE;
                counter = 0;
            }
        }
    }

    public function getSelected() {
        var selectedItems : ListItem[];
        var counter : Integer = 0;
        for ( item in items ) {
            if ( item.selected ) {
                item.position = counter;
                insert item into selectedItems;
            }
            counter++;
        }

        return selectedItems;
    }

    override var onKeyPressed = function(e) {
        var customSkin = skin as CustomListSkin;

        if ( e.code == KeyCode.VK_UP ) {
            customSkin.scrollBar.adjustValue(-1);
        } else if ( e.code == KeyCode.VK_DOWN ) {
            customSkin.scrollBar.adjustValue(1);
        }
    }

    init {
        skin = CustomListSkin{};
    }
}

Listing 2

When you set the list's items, either by calling setItems directly, or by setting the items property within your JavaFX Script, the colorize method is called to set the alternating background colors appropriately. This is done easily thanks to JavaFX Scipt's on replace keywords, as in the following declaration:


public var items : ListItem[] on replace {
    colorize();
};

As a result, when the value of the variable items is changed, the on replace keywords cause the colorize method to be called. Next, the getSelected method returns back an array of the ListItems that have been selected by the user via the mouse:


public function getSelected() {
    var selectedItems : ListItem[];
    var counter : Integer = 0;
    for ( item in items ) {
        if ( item.selected ) {
            item.position = counter;
            insert item into selectedItems;
        }
        counter++;
    }
    return selectedItems;
}

The code scans the litems in the list, and querys each item's selected flag. All selected items are inserted into an array that's returned to the caller. Notice that it's not until getSelected is called that each item's position within the list is calculated. This is an optimization that allows ListItem inserts to be fast, while deferring the position calculations until they're needed.

To see how the litems and the rest of the custom multi-select list view control are displayed, let's example the CustomListSkin class.

CustomListSkinCustomListSkin (see Listing 3) contains all of the UI elements for the list, such as a Scrollbar control, a VBox container component to contain the individual ListItem node objects, and a Rectagle that serves as a box that outines the entire control. These items are laid out in the class's init method:


init {
    node = Group {
        content: [
            Group { 
                content: [ listView, outline ] 
            },
            scrollBar
        ]
        clip: Rectangle {
            width: bind (customList.width + scrollBar.width);
            height: bind customList.height;
        }
    }
}

The node is set to a main Group of components consisting of an inner Group of components and a Scrollbar. The inner Group consists of the list items (as a VBox to stack them individually and vertically), and the outlining Rectangle. The entire node is clipped to a bounding rectangle to ensure that both individual list item's text don't go beyond the width of the list view, and that the list of visible items fits into the control's height.


public class CustomListSkin extends Skin 
{
    override public function contains( 
      arg0 : Number, arg1 : Number ) : Boolean {
        return true;
    }

    override public function intersects( 
      arg0 : Number, arg1 : Number, 
      arg2 : Number, arg3 : Number) : Boolean {
        return true;
    }

    var customList: CustomList = bind control as CustomList;

    var outline : Rectangle = Rectangle {
        x: 0 
        y: 0
        width: bind (customList.width + scrollBar.width);
        height: bind customList.height
        fill: Color.TRANSPARENT;
        stroke: Color.GRAY;
    }

    public var itemHeight : Number = bind customList.items[0].height;

    public var scrollBar : ScrollBar = ScrollBar {
        translateX: bind customList.width
        translateY: 0
        min: 0
        max: bind 
         (customList.items.size() * 
           (itemHeight + listView.spacing) );
        vertical: true
        height: bind customList.height

        // click on track
        blockIncrement: bind (itemHeight + listView.spacing ) *
          (customList.height / (itemHeight + listView.spacing + 1) )

        // click on buttons
        unitIncrement: bind itemHeight + listView.spacing
    };

    var listView = VBox {
        spacing: 0
        translateX: 2
        translateY: bind - (scrollBar.value) + 2
        content: bind customList.items
    };

    init {
        node = Group {
            content: [
                Group { content: [ listView, outline ] },
                scrollBar
            ]
            clip: Rectangle {
                width: bind (customList.width + scrollBar.width);
                height: bind customList.height;
            }
        }

    }

    override public function getPrefWidth(width: Number): Number {
        return this.control.width;
    }

    override public function getPrefHeight(height: Number): Number {
        return this.control.height;
    }
}

Listing 3

The Scrollbar control needs to be told how to scroll through the list. To do this, we set the following properties:

  • unitIncrement: Set to be the height of an individual item within the list plus spacing, if any, between items. This is used when you click on the up or down arrows.
  • blockIncrement: Set to the height of the list view divided by the height of an individual item (the number of items visible at any time). This is used when you click on the scrollbar's track to page up or down.
  • max: Set to the total number of items in the list multiplied by an individual item's height plus spacing between them. This is used to size the track button and calculate the scrolling rate as you move the button up or down along the track (see Figure 2).

[Click image to view at full size]
Figure 2: The anatomy of a scroll bar.

Now that you have an idea of how the custom control built and displayed, and how it manages a list of text entries, let's take a look at how to use it in an actual JavaFX application.


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