Explore Java and more with Jeff 'JavaJeff' Friesen

Scrolling Through a Sequence of Labels

I previously demoed the javafx.scene.control.ScrollBar class in my Exploring JavaFX's ScrollBar Control article. In this article, I present a more useful ScrollBar demonstration.

The demo application's user interface provides a horizontal scrollbar that lets you scroll through a sequence of javafx.scene.control.Label instances. As shown in Figure 1, only the label associated with the scrollbar's current value is displayed.

Scrolling through a sequence of labels.

Figure 1: Scrolling through a sequence of labels.

As you interact with the scrollbar via the keyboard or mouse, the scrollbar's value variable is updated and its associated label (which shows a small country flag image and country name) is revealed above the scrollbar. The scrollbar's value setting is shown below the scrollbar.

I've created this application as a NetBeans IDE 6.5.1 with JavaFX 1.2 ScrollableStackOfLabels 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 scrollablestackoflabels;

import javafx.geometry.HPos;

import javafx.scene.Scene;

import javafx.scene.control.Label;
import javafx.scene.control.ScrollBar;

import javafx.scene.image.Image;
import javafx.scene.image.ImageView;

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

import javafx.scene.layout.Flow;
import javafx.scene.layout.Stack;

import javafx.scene.paint.Color;

import javafx.stage.Stage;

def countries =
[
    "AD,Andorra",
    "AE,United Arab Emirates",
    "AF,Afghanistan",
    "AG,Antigua and Barbuda",
    "AI,Anguilla",
    "AL,Albania",
    "AM,Armenia",
    "AN,Netherlands Antilles",
    "AO,Angola",
    "AR,Argentina",
    "AS,American Samoa",
    "AT,Austria",
    "AU,Australia",
    "AW,Aruba",
    "AX,Åland Islands",
    "AZ,Azerbaijan",
    "BA,Bosnia and Herzegovina",
    "BB,Barbados",
    "BD,Bangladesh",
    "BE,Belgium",
    "BF,Burkina Faso",
    "BG,Bulgaria",
    "BH,Bahrain",
    "BI,Burundi",
    "BJ,Benin",
    "BM,Bermuda",
    "BN,Brunei Darussalam",
    "BO,Bolivia",
    "BR,Brazil",
    "BS,Bahamas",
    "BT,Bhutan",
    "BV,Bouvet Island",
    "BW,Botswana",
    "BY,Belarus",
    "BZ,Belize",
    "CA,Canada",
    "CC,Cocos (Keeling) Islands",
    "CD,Congo, the Democratic Republic of the",
    "CF,Central African Republic",
    "CG,Congo",
    "CH,Switzerland",
    "CI,Cote d'Ivoire Côte d'Ivoire",
    "CK,Cook Islands",
    "CL,Chile",
    "CM,Cameroon",
    "CN,China",
    "CO,Colombia",
    "CR,Costa Rica",
    "CU,Cuba",
    "CV,Cape Verde",
    "CX,Christmas Island",
    "CY,Cyprus",
    "CZ,Czech Republic",
    "DE,Germany",
    "DJ,Djibouti",
    "DK,Denmark",
    "DM,Dominica",
    "DO,Dominican Republic",
    "DZ,Algeria",
    "EC,Ecuador",
    "EE,Estonia",
    "EG,Egypt",
    "EH,Western Sahara",
    "ER,Eritrea",
    "ES,Spain",
    "ET,Ethiopia",
    "FI,Finland",
    "FJ,Fiji",
    "FK,Falkland Islands (Malvinas)",
    "FM,Micronesia, Federated States of",
    "FO,Faroe Islands",
    "FR,France",
    "GA,Gabon",
    "GB,United Kingdom",
    "GD,Grenada",
    "GE,Georgia",
    "GF,French Guiana",
    "GH,Ghana",
    "GI,Gibraltar",
    "GL,Greenland",
    "GM,Gambia",
    "GN,Guinea",
    "GP,Guadeloupe",
    "GQ,Equatorial Guinea",
    "GR,Greece",
    "GS,South Georgia and the South Sandwich Islands",
    "GT,Guatemala",
    "GU,Guam",
    "GW,Guinea-Bissau",
    "GY,Guyana",
    "HK,Hong Kong",
    "HM,Heard Island and McDonald Islands",
    "HN,Honduras",
    "HR,Croatia",
    "HT,Haiti",
    "HU,Hungary",
    "ID,Indonesia",
    "IE,Ireland",
    "IL,Israel",
    "IN,India",
    "IO,British Indian Ocean Territory",
    "IQ,Iraq",
    "IR,Iran, Islamic Republic of",
    "IS,Iceland",
    "IT,Italy",
    "JM,Jamaica",
    "JO,Jordan",
    "JP,Japan",
    "KE,Kenya",
    "KG,Kyrgyzstan",
    "KH,Cambodia",
    "KI,Kiribati",
    "KM,Comoros",
    "KN,Saint Kitts and Nevis",
    "KP,Korea, Democratic People's Republic of",
    "KR,Korea, Republic of",
    "KW,Kuwait",
    "KY,Cayman Islands",
    "KZ,Kazakhstan",
    "LA,Lao People's Democratic Republic",
    "LB,Lebanon",
    "LC,Saint Lucia",
    "LI,Liechtenstein",
    "LK,Sri Lanka",
    "LR,Liberia",
    "LS,Lesotho",
    "LT,Lithuania",
    "LU,Luxembourg",
    "LV,Latvia",
    "LY,Libyan Arab Jamahiriya",
    "MA,Morocco",
    "MC,Monaco",
    "MD,Moldova, Republic of",
    "ME,Montenegro",
    "MG,Madagascar",
    "MH,Marshall Islands",
    "MK,Macedonia, the former Yugoslav Republic of",
    "ML,Mali",
    "MM,Myanmar",
    "MN,Mongolia",
    "MO,Macao",
    "MP,Northern Mariana Islands",
    "MQ,Martinique",
    "MR,Mauritania",
    "MS,Montserrat",
    "MT,Malta",
    "MU,Mauritius",
    "MV,Maldives",
    "MW,Malawi",
    "MX,Mexico",
    "MY,Malaysia",
    "MZ,Mozambique",
    "NA,Namibia",
    "NC,New Caledonia",
    "NE,Niger",
    "NF,Norfolk Island",
    "NG,Nigeria",
    "NI,Nicaragua",
    "NL,Netherlands",
    "NO,Norway",
    "NP,Nepal",
    "NR,Nauru",
    "NU,Niue",
    "NZ,New Zealand",
    "OM,Oman",
    "PA,Panama",
    "PE,Peru",
    "PF,French Polynesia",
    "PG,Papua New Guinea",
    "PH,Philippines",
    "PK,Pakistan",
    "PL,Poland",
    "PM,Saint Pierre and Miquelon",
    "PN,Pitcairn",
    "PR,Puerto Rico",
    "PS,Palestinian Territory, Occupied",
    "PT,Portugal",
    "PW,Palau",
    "PY,Paraguay",
    "QA,Qatar",
    "RE,Reunion Réunion",
    "RO,Romania",
    "RS,Serbia",
    "RU,Russian Federation",
    "RW,Rwanda",
    "SA,Saudi Arabia",
    "SB,Solomon Islands",
    "SC,Seychelles",
    "SD,Sudan",
    "SE,Sweden",
    "SG,Singapore",
    "SH,Saint Helena",
    "SI,Slovenia",
    "SJ,Svalbard and Jan Mayen",
    "SK,Slovakia",
    "SL,Sierra Leone",
    "SM,San Marino",
    "SN,Senegal",
    "SO,Somalia",
    "SR,Suriname",
    "ST,Sao Tome and Principe",
    "SV,El Salvador",
    "SY,Syrian Arab Republic",
    "SZ,Swaziland",
    "TC,Turks and Caicos Islands",
    "TD,Chad",
    "TF,French Southern Territories",
    "TG,Togo",
    "TH,Thailand",
    "TJ,Tajikistan",
    "TK,Tokelau",
    "TL,Timor-Leste",
    "TM,Turkmenistan",
    "TN,Tunisia",
    "TO,Tonga",
    "TR,Turkey",
    "TT,Trinidad and Tobago",
    "TV,Tuvalu",
    "TW,Taiwan, Province of China",
    "TZ,Tanzania, United Republic of",
    "UA,Ukraine",
    "UG,Uganda",
    "UM,United States Minor Outlying Islands",
    "US,United States",
    "UY,Uruguay",
    "UZ,Uzbekistan",
    "VA,Holy See (Vatican City State)",
    "VC,Saint Vincent and the Grenadines",
    "VE,Venezuela",
    "VG,Virgin Islands, British",
    "VI,Virgin Islands, U.S.",
    "VN,Viet Nam",
    "VU,Vanuatu",
    "WF,Wallis and Futuna",
    "WS,Samoa",
    "YE,Yemen",
    "YT,Mayotte",
    "ZA,South Africa",
    "ZM,Zambia",
    "ZW,Zimbabwe"
];

var scrollBarRef: ScrollBar;

var countryLabels: Label [];

for (i in [0..<sizeof countries])
     insert Label
            {
                graphic: ImageView
                {
                    image: Image
                    {
                        url: "{__DIR__}icons/"
                             "{countries [i].substring (0, 2).toLowerCase ()}"
                             ".png"
                    }
                }
                text: countries [i].substring (3)
                hpos: HPos.CENTER
                opacity: bind if ((scrollBarRef.value as Integer) == i) then 1.0
                                                                        else 0.0
            }
            into countryLabels;

Stage
{
    title: "ScrollableStackOfLabels"

    var sceneRef: Scene
    scene: sceneRef = Scene
    {
        width: 300
        height: 200
        fill: Color.GOLD

        var flowRef: Flow
        content: flowRef = Flow
        {
            layoutX: bind (sceneRef.width-flowRef.layoutBounds.width)/2
                           -flowRef.layoutBounds.minX
            layoutY: bind (sceneRef.height-flowRef.layoutBounds.height)/2
                           -flowRef.layoutBounds.minY

            vertical: true
            vgap: 40

            content:
            [
                Stack
                {
                    content: countryLabels
                }

                scrollBarRef = ScrollBar
                {
                    min: 0
                    max: sizeof countryLabels-1
                    unitIncrement: 1
                    clickToPosition: true

                    onKeyPressed: function (ke: KeyEvent): Void
                    {
                        if (ke.code == KeyCode.VK_LEFT)
                            scrollBarRef.decrement ()
                        else
                        if (ke.code == KeyCode.VK_RIGHT)
                            scrollBarRef.increment ()
                        else
                        if (ke.code == KeyCode.VK_HOME)
                            scrollBarRef.adjustValue (0.0)
                        else
                        if (ke.code == KeyCode.VK_END)
                            scrollBarRef.adjustValue (1.0)
                        else
                        if (ke.code == KeyCode.VK_PAGE_UP)
                            scrollBarRef.adjustValue ((scrollBarRef.value-10)/
                                                      scrollBarRef.max)
                        else
                        if (ke.code == KeyCode.VK_PAGE_DOWN)
                            scrollBarRef.adjustValue ((scrollBarRef.value+10)/
                                                      scrollBarRef.max)
                    }
                }

                Label
                {
                    text: bind "Value: {scrollBarRef.value}"
                }
            ]
        }
    }
}

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

Following various imports, Main.fx creates a sequence of Labels, assigning a javafx.scene.image.ImageView node containing a country's flag image to Label's graphic variable, and assigning the country name to the label's text variable.

Main.fx next creates the scene. It places scene content (a javafx.scene.layout.Stack instance, a ScrollBar instance, and a Label instance) inside a javafx.scene.layout.Flow container instance that lays out this content vertically.

The Stack instance lays out the Labels sequence such that one label appears over labels below its sequence position. Each label's content is centered within the stack (which sizes to contain the largest label) by virtue of previously assigning HPos.CENTER to Label's hpos variable.

For the scrolling to work, it's important that the scrollbar's min variable be set to 0, the index of the first label in the stack, and its max variable be set to the index of the last label in the stack.

Except for the currently displayed label, each label has its opacity variable set to 0.0, which hides the label. Because the scrollbar's value variable is part of an expression that's bound to the label's opacity variable, only the label associated with the current value is displayed.

The as Integer cast in the (scrollBarRef.value as Integer) == i expression prevents a problem where the label disappears when you drag the scrollbar's thumb (draggable bar within the scrollbar's track).

Without the cast, dragging the thumb causes scrollBarRef.value to contain a non-zero fraction that, when compared with i (after i is converted to a floating-point value with a zero fraction), results in 0.0 (hide) instead of 1.0 (show) being assigned to opacity.

The standalone Label that's added to Flow reveals the current setting of the scrollbar's value property, which is bound to the Label's text variable. As Figure 1 reveals, this value may have a non-zero fraction, which proves the need for the as Integer cast.

Perhaps you're wondering why I subtract -flowRef.layoutBounds.minX from the expression bound to flowRef's layoutX variable, and subtract -flowRef.layoutBounds.minY from flowRef's layoutY variable.

In her JavaFX1.2 Layout blog post, Amy Fowler mentioned that layoutX/layoutY define a translation on the node's coordinate space to adjust it from its current layoutBounds.minX/minY values.

Because layoutX and layoutY are not final position values, you must subtract layoutBounds.minX and layoutBounds.maxX from layoutX and layoutY to achieve the node's final layout position.

Conclusion

This article demonstrated a technique for scrolling through a sequence of labels where only the current label is displayed during the scroll. You can employ this technique when you need to add scrolling to your user interface, but limited space prevents the display of multiple items.


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