Explore Java and more with Jeff 'JavaJeff' Friesen

Exploring JavaFX's ScrollBar Control

JavaFX's scrollbar control lets you add customized scrolling to your user interfaces (UIs). For example, you might provide a scrollbar that lets the user scroll through a list of images, which is something that the listview control doesn't support (you can only scroll through text).

Before you can use the scrollbar control to introduce customized scrolling, you need to understand the various properties and functions that are made available by its javafx.scene.control.ScrollBar class. These properties and functions are listed below:

  • blockIncrement (of type Number) specifies the amount (10.0 is the default) that is added to or subtracted from value whenever the scrollbar's track is clicked. It isn't used if clickToPosition is set to true.
  • clickToPosition (of type Boolean) specifies whether (true) or not (false) a click on the scrollbar's track sets value to the amount specified by the clicked position in the track.
  • max (of type Number) specifies the scrollbar's maximum value. This value must be greater than or equal to min; otherwise, it's set equal to min.
  • min (of type Number) specifies the scrollbar's minimum value. This value must be less than or equal to max; otherwise, it's set equal to max.
  • unitIncrement (of type Number) specifies the amount (1.0 is the default) that's added to or subtracted from value whenever the scrollbar's down/right or up/left arrow (respectively) is clicked.
  • value (of type Number) specifies the scrollbar's current value. It must lie between min and max. If it's ever out of bounds (perhaps max or min have been changed), it will be clamped to remain in bounds.
  • vertical (of type Boolean) specifies the scrollbar's orientation. The scrollbar is vertical when true is assigned to vertical; otherwise, the scrollbar is horizontal.
  • public adjustValue(position: Number): Void adjusts value so that it's ultimately set to the value located at the position fraction (which must range from 0.0 to 1.0) between min and max. If clickToPosition is true, value is set to this value after a single call to adjustValue(). If clickToPosition is false, each call to adjustValue() either increments or decrements value by blockIncrement until value is set to this value. For example, assume that min is set to 0.0, max is set to 100.0, value is set to 25.0, and clickToPosition is set to true. Following a call to adjustValue(), value will be set to 50.0. However, if clickToPosition was set to false and blockIncrement was set to 10.0, the first call to adjustValue() would result in value being set to 35.0, the second call would result in value being set to 45.0, and the third call would result in value being set to 50.0. In either case, successive calls to adjustValue() would not change value's value -- it would remain set to 50.0.
  • public decrement(): Void decrements value by unitIncrement.
  • public increment(): Void increments value by unitIncrement.

Although these properties and functions should be fairly easy to grasp, I've created a simple application that demonstrates them to you. Before I reveal the application's source code, let's explore its UI (shown in Figure 1).

This UI lets you explore each of ScrollBar's
properties and functions.

Figure 1: This UI lets you explore each of ScrollBar's properties and functions.

The UI defaults to presenting a vertical scrollbar. You can switch to a horizontal scrollbar by clicking the Horizontal radio button. Interestingly, the UI isn't vertically centered when a horizontal scrollbar is shown.

Below the scrollbar are textboxes that reveal and let you enter min, value, max, block increment, and unit increment values. These controls are followed by a checkbox for setting (when checked) or clearing the click-to-position property.

The three buttons below the checkbox let you explore the increment(), adjustValue(), and decrement() functions. The button with the 0.5 label always passes 0.5 to adjustValue() in its action handler.

I've created this application as a NetBeans IDE 6.5.1 with JavaFX 1.2 ScrollBarDemo project -- the project can be downloaded via a link at the end of this article. The project's Main.fx source code appears in Listing 1.

/*
 * Main.fx
 */

package scrollbardemo;

import javafx.scene.Scene;

import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.RadioButton;
import javafx.scene.control.ScrollBar;
import javafx.scene.control.TextBox;
import javafx.scene.control.ToggleGroup;

import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;

import javafx.scene.layout.Flow;

import javafx.scene.paint.Color;

import javafx.stage.Stage;

class Model
{
    var min: Number on replace
    {
        smin = "{min}"
    }

    var smin: String on replace
    {
        min = Double.parseDouble (smin)
    }

    var value: Number on replace
    {
        svalue = "{value}"
    }

    var svalue: String on replace
    {
        value = Double.parseDouble (svalue)
    }

    var max: Number on replace
    {
        smax = "{max}"
    }

    var smax: String on replace
    {
        max = Double.parseDouble (smax)
    }

    var blockincr: Number on replace
    {
        sblockincr = "{blockincr}"
    }

    var sblockincr: String on replace
    {
        blockincr = Double.parseDouble (sblockincr)
    }

    var unitincr: Number on replace
    {
        sunitincr = "{unitincr}"
    }

    var sunitincr: String on replace
    {
        unitincr = Double.parseDouble (sunitincr)
    }
}

def model = Model
            {
                min: 0.0
                value: 50.0
                max: 100.0
                blockincr: ScrollBar {}.blockIncrement
                unitincr: ScrollBar {}.unitIncrement
            }

Stage
{
    title: "ScrollBar Demo"

    var sceneRef: Scene
    scene: sceneRef = Scene
    {
        width: 450
        height: 450

        fill: Color.GOLD
        
        var flowRef: Flow
        content: flowRef = Flow
        {
            vertical: true
            vgap: 20

            layoutX: bind (sceneRef.width-flowRef.layoutBounds.width)/2-
                           flowRef.layoutBounds.minX
            layoutY: bind (sceneRef.height-flowRef.layoutBounds.height)/2-
                           flowRef.layoutBounds.minY

            var scrollBarRef: ScrollBar
            var toggleGroup = ToggleGroup {}
            var checkBoxRef: CheckBox
            content:
            [
                Flow
                {
                    hgap: 10

                    content:
                    [
                        RadioButton
                        {
                            text: "Horizontal"
                            toggleGroup: toggleGroup
                        }

                        RadioButton
                        {
                            text: "Vertical"
                            selected: true
                            toggleGroup: toggleGroup
                        }
                    ]
                }

                scrollBarRef = ScrollBar
                {
                    min: bind model.min with inverse
                    value: bind model.value with inverse
                    max: bind model.max with inverse
                    blockIncrement: bind model.blockincr with inverse
                    unitIncrement: bind model.unitincr with inverse
                    vertical: bind if (toggleGroup.selectedButton.text ==
                                       "Vertical") then true else false
                    clickToPosition: bind if (checkBoxRef.selected) then true
                                                                    else false
                    onKeyPressed: function (ke: KeyEvent): Void
                    {
                        if (ke.code == KeyCode.VK_UP or
                            ke.code == KeyCode.VK_LEFT)
                            scrollBarRef.decrement ()
                        else
                        if (ke.code == KeyCode.VK_DOWN or
                            ke.code == KeyCode.VK_RIGHT)
                            scrollBarRef.increment ()
                        else
                        if (ke.code == KeyCode.VK_PAGE_UP)
                            scrollBarRef.value -= scrollBarRef.blockIncrement
                        else
                        if (ke.code == KeyCode.VK_PAGE_DOWN)
                            scrollBarRef.value += scrollBarRef.blockIncrement
                        else
                        if (ke.code == KeyCode.VK_HOME)
                            scrollBarRef.value = scrollBarRef.min
                        else
                        if (ke.code == KeyCode.VK_END)
                            scrollBarRef.value = scrollBarRef.max
                    }
                }

                Flow
                {
                    hgap: 10

                    content:
                    [
                        Label
                        {
                            text: "Min"
                        }

                        TextBox
                        {
                            text: bind model.smin with inverse
                        }

                        Label
                        {
                            text: "Value"
                        }

                        TextBox
                        {
                            text: bind model.svalue with inverse
                        }

                        Label
                        {
                            text: "Max"
                        }

                        TextBox
                        {
                            text: bind model.smax with inverse
                        }
                    ]
                }

                Flow
                {
                    hgap: 10

                    content:
                    [
                        Label
                        {
                            text: "Block Increment"
                        }

                        TextBox
                        {
                            text: bind model.sblockincr with inverse
                        }

                        Label
                        {
                            text: "Unit Increment"
                        }

                        TextBox
                        {
                            text: bind model.sunitincr with inverse
                        }
                    ]
                }

                checkBoxRef = CheckBox
                {
                    text: "ClickToPosition"
                }

                Flow
                {
                    hgap: 10

                    content:
                    [
                        Button
                        {
                            text: "+"
                            action: function (): Void
                            {
                               scrollBarRef.increment ()
                            }
                        }

                        Button
                        {
                            text: "0.5"
                            action: function (): Void
                            {
                                scrollBarRef.adjustValue (0.5)
                            }
                        }

                        Button
                        {
                            text: "-"
                            action: function (): Void
                            {
                               scrollBarRef.decrement ()
                            }
                        }
                    ]
                }
            ]
        }
    }
}

Listing 1: Main.fx (from a ScrollBarDemo NetBeans IDE 6.5.1 with JavaFX 1.2 project)

Following a list of imports, Main.fx declares a Model class that describes the application's model. This model consists of various numeric and string variables that repectively bind with the UI's scrollbar and textboxes.

I provide numeric and string variables to overcome a limitation with binding. Basically, I want the value textbox to always reflect the scrollbar's current value. Also, I want the scrollbar's current value to always reflect whatever legal value is entered into the textbox.

The limitation is that I cannot directly bind two values of different types. Specifically, I cannot bind the ScrollBar instance's value property (of type Number) to the TextBox instance's text property (of type String) and vice-versa.

I overcome this limitation by binding to model variables (of the appropriate types), using replace triggers to convert between types whenever a model variable changes, and specifying with inverse to update a model variable whenever a control property changes.

When initializing the Model instance's variables, I specify blockincr: ScrollBar {}.blockIncrement and unitincr: ScrollBar {}.unitIncrement. This is a convenient way to initialize blockincr and unitincr to ScrollBar's default values.

I chose to organize the UI around a flow container that lays out the UI in terms of rows. I assigned bound expressions to the container's layoutX and layoutY variables to keep the UI centered, although the UI doesn't vertically center when you choose the horizontal scrollbar.

By default, the scrollbar control doesn't have a keyboard interface (although you can shift focus to this control via the keyboard). However, it's not hard to add keyboard support: Simply add an onKeyPressed function handler to accomplish this task.

Conclusion

The ScrollBarDemo application's scrollbar is not like the other input controls in that no Caspian border surrounds this control when it receives the focus. However, with the help of a good decompiler, it's possible to achieve this border, but that's the subject for another article.


Download code.zip

Note: Application created with JavaFX 1.2 (via NetBeans IDE 6.5.1) on top of Java SE 6u16.


LEARN JAVA FROM APRESS