Explore Java, JavaFX, and other software technologies with Jeff 'JavaJeff' Friesen
Units Converter

A few years ago, I developed a Windows-based units-conversion application that converts between different kinds of units (such as kilograms and pounds). More recently, I developed a JavaFX 1.2 version of this application, to reinforce my grasp of various control classes in the javafx.scene.control package, and to experiment in adapting a desktop-based user interface (UI) to browser and mobile contexts.

This article presents three versions of my units-conversion application. The first version reveals the basic user interface. The second version adapts the first version's UI to various contexts by scaling fonts (except for the listview control's items font) and other factors. The final version shows you how to scale the listview control's items font to various contexts, and also fixes a focus problem with this control.

I created these versions via NetBeans IDE 6.5.1 with JavaFX 1.2 UnitsConverter1, UnitsConverter2, and UnitsConverter3 projects.

Units Converter: Version One

The first version of the units-conversion application introduces a UI that's also employed by the second and third versions, albeit with minor changes in these other versions' UIs. As Figure 1 reveals, this UI consists of title text, three labels, a couple of textboxes, a listview, a button, and some more text on a gradient background.

The listview doesn't automatically select its first
item, an annoyance that will be fixed in the second version of this
application.

Figure 1: The listview doesn't automatically select its first item, an annoyance that will be fixed in the second version of this application.

Listing 1 presents the application's Main.fx source code.

/*
 * Main.fx
 */

package unitsconverter1;

import java.lang.NumberFormatException;

import javafx.geometry.HPos;
import javafx.geometry.VPos;

import javafx.scene.Scene;

import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ListView;
import javafx.scene.control.TextBox;

import javafx.scene.layout.HBox;
import javafx.scene.layout.LayoutInfo;
import javafx.scene.layout.VBox;

import javafx.scene.paint.Color;
import javafx.scene.paint.LinearGradient;
import javafx.scene.paint.Stop;

import javafx.scene.text.Font;
import javafx.scene.text.Text;

import javafx.stage.Stage;

class Conversion
{
    var src: String;
    var dst: String;
    var func: function (input: Double): Double
}

var conversions =
[
    Conversion
    {
        src: "Millimeters"
        dst: "Centimeters"
        func: function (input: Double): Double
        {
            input*0.1
        }
    }

    Conversion
    {
        src: "Centimeters"
        dst: "Millimeters"
        func: function (input: Double): Double
        {
            input*10
        }
    }

    Conversion
    {
        src: "Miles"
        dst: "Meters"
        func: function (input: Double): Double
        {
            input*1609.344
        }
    }

    Conversion
    {
        src: "Meters"
        dst: "Miles"
        func: function (input: Double): Double
        {
            input/1609.344
        }
    }

    Conversion
    {
        src: "Degrees Celsius"
        dst: "Degrees Fahrenheit"
        func: function (input: Double): Double
        {
            input*9.0/5.0+32.0
        }
    }

    Conversion
    {
        src: "Degrees Fahrenheit"
        dst: "Degrees Celsius"
        func: function (input: Double): Double
        {
            (input-32.0)*5.0/9.0
        }
    }
];

Stage
{
    title: "Units Converter"

    var sceneRef: Scene
    scene: sceneRef = Scene
    {
        width: 550
        height: 350

        fill: LinearGradient
        {
            startX: 0.0
            startY: 0.0
            endX: 0.0
            endY: 1.0
            stops:
            [
                Stop { offset: 0.0 color: Color.YELLOW }
                Stop { offset: 1.0 color: Color.PINK }
            ]
        }

        content: VBox
                 {
                     width: bind sceneRef.width
                     height: bind sceneRef.height

                     nodeHPos: HPos.CENTER
                     hpos: HPos.CENTER
                     vpos: VPos.CENTER
                     spacing: 20

                     var listView: ListView
                     var textInput: TextBox
                     var textOutput: TextBox

                     content:
                     [
                         Text
                         {
                             content: "Units Converter"
                             font: Font.font ("Verdana", 24)
                         }

                         HBox
                         {
                             content:
                             [
                                 Label
                                 {
                                     text: "Enter number of input units"
                                 }

                                 textInput = TextBox
                                 {
                                     columns: 20
                                     selectOnFocus: true
                                 }
                             ]

                             spacing: 20
                         }

                         VBox
                         {
                             spacing: 10
                             nodeHPos: HPos.CENTER

                             content:
                             [
                                 Label
                                 {
                                     text: "Select a conversion"
                                 }

                                 listView = ListView
                                 {
                                      items: for (conversion in conversions)
                                                 "{conversion.src} To "
                                                 "{conversion.dst}"

                                      layoutInfo: LayoutInfo
                                      {
                                          height: 76
                                          width: 300
                                      }
                                 }
                             ]
                         }

                         HBox
                         {
                             content:
                             [
                                 Label
                                 {
                                     text: "Equivalent number of output units"
                                 }

                                 textOutput = TextBox
                                 {
                                     columns: 20
                                     editable: false
                                 }
                             ]

                             spacing: 20
                         }

                         Button
                         {
                             text: "Convert"
                             action: function (): Void
                             {
                                 try
                                 {
                                     var index = listView.selectedIndex;
                                     var input = textInput.text;
                                     var inVal = Double.parseDouble (input);
                                     var outVal = conversions [index].
                                                    func (inVal);
                                     textOutput.text = "{outVal}"
                                 }
                                 catch (nfe: NumberFormatException)
                                 {
                                     textOutput.text = "Error"
                                 }
                             }
                         }

                         Text
                         {
                             content: "Created by Jeff Friesen"
                             font: Font.font ("Verdana", 8)
                         }
                     ]
                 }
    }
}

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

Following various imports, Listing 1 specifies Conversion as its model class. This class describes a conversion in terms of text that identifies the source units to be converted, text that identifies the resulting destination units, and a function that performs the conversion. This function takes the number of source units as its solitary argument, and returns the equivalent number of destination units as its result.

Listing 1 next specifies a sequence of Conversion instances. For brevity, I've kept this sequence short (there are only six instances). Furthermore, I haven't sorted the sequence. Feel free to add more Conversion instances to the sequence, and to sort all of these instances into whatever order you feel is appropriate.

Now that Listing 1 has taken care of the model, it turns its attention to the UI. After assigning a title to the stage's title variable, Listing 1 creates the UI's scene, assigning the result to a local sceneRef variable and the stage's scene variable. The sceneRef variable is required so that the scene's layout container (discussed later) can access scene width and height.

Regarding the scene, the first item of business is to specify the scene's width and height, by assigning values to the scene's width and height variables. Unlike the stage's equivalent width and height variables, the values assigned to the scene's width and height variables don't take into account window decorations such as a titlebar and border.

Because I'm focusing exclusively on the desktop profile for this version, I've been cavalier in the values Listing 1 assigns to width and height. In many cases, a mobile device's screen will be smaller and part of the UI will be hidden. Part of the UI will also be hidden when presenting the application as an applet with a smaller width and/or height.

Moving on, the listing assigns a LinearGradient instance to the scene's fill variable, to render the scene's background via a gradient -- I chose lighter colors for the gradient to achieve good contrast with the UI's black text. Alternatively, you could assign a solid color to this variable, or perhaps ignore fill in favor of presenting an image as the scene's background.

The scene's content is specified via a VBox instance, which is assigned to the scene's content variable. A vbox is a container that lays out its content sequence of nodes in a single vertical column. It resizes each content node whose class implements the Resizable interface to the node's preferred size.

Within the vbox, Listing 1 first binds this container's width and height variables to the scene's width and height variables (which are accessed via the sceneRef local variable). The idea is for the container to occupy the entire scene no matter how the scene is resized, and binding to the scene's width and height makes this possible.

To achieve the layout shown in Figure 1, it's necessary to center the entire column of nodes horizontally and vertically. Listing 1 accomplishes this task by assigning HPos.CENTER to the vbox's hPos variable, and by assigning VPos.CENTER to the vbox's vPos variable. It's also necessary to center the nodes within the column, by assigning HPos.CENTER to the vbox's nodeHPos variable.

After assigning 20 pixels of vertical space to the vbox's spacing variable, to leave this much empty vertical space between each node in this container's content sequence, Listing 1 focuses on populating this sequence. The following list highlights some of the more interesting aspects of this task:

  • Font's public font(family: java.lang.String, size: Number): Font function is used to specify a larger Verdana font for the title text. I've discovered that JavaFX uses Verdana as the default font for displaying text.
  • true is assigned to the input textbox's selectOnFocus variable, to cause the entire contents of this control to be selected when it receives focus. Doing this makes it easier to erase the control's contents (one keypress) when entering a new number of units for the next conversion.
  • A sequence comprehension is used to populate the listview control's items sequence with the source and destination text from the sequence of Conversion instances.
  • By default, the listview occupies too much screen space. To constrain this control to a smaller region, its preferred width and height are overridden by assigning a LayoutInfo instance to the control's layoutInfo variable. The height is constrained to 76 pixels, allowing four rows to be shown. Because this constraint greatly shrinks the control's width, the width is specified as 300 pixels, which allows the largest conversion item to be completely shown.
  • The output textbox is made uneditable by assigning false to its editable variable. However, you can still shift focus to this control via the keyboard or mouse.
  • The function assigned to the button's action variable performs the conversion. It obtains the listview's selectedIndex value, and obtains the input textbox's value, which is parsed into a double-precision floating-point value. After performing the appropriate conversion, the result is assigned to the output textbox. If the input textbox's value cannot be parsed, Double.parseDouble() throws a NumberFormatException, and Error is output instead.

Now that you've explored how the code works, you'll want to build and play with this application. Start up NetBeans IDE 6.5.1 with JavaFX 1.2 and introduce a new UnitsConverter1 project. Then replace the project's skeletal Main.fx source code with Listing 1. After accomplishing these tasks, press F6 to compile and run the application.

After starting the application, enter a numeric value into the input textbox and (without selecting an item in the listview) click the Convert button. The output textbox reveals 0.0 no matter what value you enter into the input textbox. Why? Hint: Check out the function assigned to the button's action variable.

The first line in this function, var index = listView.selectedIndex;, assigns the index of the selected listview item to index. When no item is selected, selectedIndex contains -1, which is assigned to index. This variable is subsequently used to index conversions. Instead of throwing an exception, JavaFX ignores the -1 index and assigns 0.0 to outVal, which is subsequently assigned to the output textbox.

This strange behavior, which results from JavaFX silently ignoring an invalid sequence index, is bound to confuse the application's users. Although you could fix this problem by using an if statement to check selectedIndex's value for -1, and displaying an error message if this is the case, I'll show you a better solution in the next section.

Suppose that you change the application's profile to mobile and re-run the application. Figure 2 shows you the resulting UI on the mobile emulator's screen.

Because the UI is partly hidden, you can only scroll
through the listview via the keyboard.

Figure 2: Because the UI is partly hidden, you can only scroll through the listview via the keyboard.

Now suppose that you change the profile to browser and re-run the application. Figure 3 shows you the resulting UI when displayed via a NetBeans-generated applet at its default 200-by-200-pixel dimensions.

Once again, you can only scroll through the listview
via the keyboard.

Figure 3: Once again, you can only scroll through the listview via the keyboard.

The fact that the application's UI looks terrible on mobile and browser screens is more than an annoyance: It's a real problem that must be overcome before we can deploy this application to these other contexts. Fortunately, there's a solution that lets us adapt the UI to whatever screen size we're faced with. I'll reveal this solution in the next section.

Units Converter: Version Two

The second version of the units-conversion application corrects an annoyance with the first version where no item is selected in the listview at startup. Ideally, the first item should be selected. The second version also adapts the desktop UI so that it scales nicely to a smaller size when viewed in browser or mobile profiles (where the size is typically smaller). For example, Figure 4 shows the UI adapted to the browser profile.

Although it looks better than Figure 3, the
browser-based UI reveals that the listview's text size is greatly out of
proportion to the rest of the text.

Figure 4: Although it looks better than Figure 3, the browser-based UI reveals that the listview's text size is greatly out of proportion to the rest of the text.

Listing 2 presents the application's Main.fx source code.

/*
 * Main.fx
 */

package unitsconverter2;

import java.lang.NumberFormatException;

import javafx.geometry.HPos;
import javafx.geometry.VPos;

import javafx.scene.Scene;

import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ListView;
import javafx.scene.control.TextBox;

import javafx.scene.layout.HBox;
import javafx.scene.layout.LayoutInfo;
import javafx.scene.layout.VBox;

import javafx.scene.paint.Color;
import javafx.scene.paint.LinearGradient;
import javafx.scene.paint.Stop;

import javafx.scene.text.Font;
import javafx.scene.text.Text;

import javafx.stage.Screen;
import javafx.stage.Stage;

import javafx.util.Math;

class Conversion
{
    var src: String;
    var dst: String;
    var func: function (input: Double): Double
}

var conversions =
[
    Conversion
    {
        src: "Millimeters"
        dst: "Centimeters"
        func: function (input: Double): Double
        {
            input*0.1
        }
    }

    Conversion
    {
        src: "Centimeters"
        dst: "Millimeters"
        func: function (input: Double): Double
        {
            input*10
        }
    }

    Conversion
    {
        src: "Miles"
        dst: "Meters"
        func: function (input: Double): Double
        {
            input*1609.344
        }
    }

    Conversion
    {
        src: "Meters"
        dst: "Miles"
        func: function (input: Double): Double
        {
            input/1609.344
        }
    }

    Conversion
    {
        src: "Degrees Celsius"
        dst: "Degrees Fahrenheit"
        func: function (input: Double): Double
        {
            input*9.0/5.0+32.0
        }
    }

    Conversion
    {
        src: "Degrees Fahrenheit"
        dst: "Degrees Celsius"
        func: function (input: Double): Double
        {
            (input-32.0)*5.0/9.0
        }
    }
];

// Assume desktop profile and a 550-by-350-pixel viewing area.

var width = 550.0;
var height = 350.0;

// If desktop profile, narrow the width and height if they exceed the desktop's
// primary screen's current bounds.

if (__PROFILE__ == "desktop")
{
    def bounds = Screen.primary.bounds;
    width = Math.min (bounds.width, width);
    height = Math.min (bounds.height, height)
}

// At this point, the actual width and height of the desktop's viewing area are
// known.

// However, it's possible that the profile is mobile or browser, in which case
// the viewing area is probably smaller. To account for this possibility,
// calculate appropriate horizontal and vertical scaling factors, and choose an
// appropriate size for the default font.

var sceneRef: Scene;

def scale_factorX = bind sceneRef.width/width;
def scale_factorY = bind sceneRef.height/height;
def scale_factor = bind Math.min (scale_factorX, scale_factorY);

// According to the JavaFX Font documentation, the font size defaults to 12
// points. However, I've discovered that the actual setting (on a Windows XP 
// platform) is 11 points. This setting can be left as-is for the desktop
// profile, but will need to be shrunk for the mobile and browser profiles.

def font = bind Font.font ("Verdana", 5+scale_factor*6);
def titleFont = bind Font.font ("Verdana", 16+scale_factor*8);
def footerFont = bind Font.font ("Verdana", 4+scale_factor*4);

Stage
{
    title: "Units Converter"

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

        fill: LinearGradient
        {
            startX: 0.0
            startY: 0.0
            endX: 0.0
            endY: 1.0
            stops:
            [
                Stop { offset: 0.0 color: Color.YELLOW }
                Stop { offset: 1.0 color: Color.PINK }
            ]
        }

        content: VBox
                 {
                     width: bind sceneRef.width
                     height: bind sceneRef.height

                     nodeHPos: HPos.CENTER
                     hpos: HPos.CENTER
                     vpos: VPos.CENTER
                     spacing: bind 20*scale_factorY

                     var listView: ListView
                     var textInput: TextBox
                     var textOutput: TextBox

                     content:
                     [
                         Text
                         {
                             content: "Units Converter"
                             font: bind titleFont
                         }

                         HBox
                         {
                             content:
                             [
                                 Label
                                 {
                                     text: "Enter number of input units"
                                     font: bind font
                                 }

                                 textInput = TextBox
                                 {
                                     columns: bind 20*scale_factorX
                                     selectOnFocus: true
                                     font: bind font
                                 }
                             ]

                             spacing: bind 20*scale_factorX
                         }

                         VBox
                         {
                             spacing: bind 10*scale_factorY
                             nodeHPos: HPos.CENTER

                             content:
                             [
                                 Label
                                 {
                                     text: "Select a conversion"
                                     font: bind font
                                 }

                                 listView = MyListView
                                 {
                                      items: for (conversion in conversions)
                                                 "{conversion.src} To "
                                                 "{conversion.dst}"

                                      layoutInfo: LayoutInfo
                                      {
                                          height: bind 76*scale_factorY
                                          width: bind 300*scale_factorX
                                      }
                                 }
                             ]
                         }

                         HBox
                         {
                             content:
                             [
                                 Label
                                 {
                                     text: "Equivalent number of output units"
                                     font: bind font
                                 }

                                 textOutput = TextBox
                                 {
                                     columns: bind 20*scale_factorX
                                     editable: false
                                     font: bind font
                                 }
                             ]

                             spacing: bind 20*scale_factorX
                         }

                         Button
                         {
                             text: "Convert"
                             font: bind font
                             action: function (): Void
                             {
                                 try
                                 {
                                     var index = listView.selectedIndex;
                                     var input = textInput.text;
                                     var inVal = Double.parseDouble (input);
                                     var outVal = conversions [index].
                                                    func (inVal);
                                     textOutput.text = "{outVal}"
                                 }
                                 catch (nfe: NumberFormatException)
                                 {
                                     textOutput.text = "Error"
                                 }
                             }
                         }

                         Text
                         {
                             content: "Created by Jeff Friesen"
                             font: bind footerFont
                         }
                     ]
                 }
    }
}

class MyListView extends ListView
{
    init
    {
        select (0)
    }
}

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

Listing 2 solves the annoyance problem of no listview item being initially selected, by subclassing ListView and invoking this superclass's public select(itemIndex: Integer): Void function with a 0 argument to select the first item in the listview. Of course, MyListView must be instantiated instead of ListView for this feature to take effect.

You might be wondering why I didn't invoke ListView's public selectFirstRow(): Void function to accomplish this task. After all, this function is more appropriately named. However, my decompiler shows that this function executes select (0) only when ListView's selectedIndex variable contains a value that's greater than or equal to 0 -- it defaults to -1.

More challenging than fixing this annoyance problem is the task of adapting the UI to look nice on smaller browser-based applet and mobile screens. I want to maintain Figure 1's desktop layout without having to rearrange the controls. Essentially, the controls and the spacings between them should scale down appropriately and still be readable.

Although I could simply assign fractional values (between 0.0 and 1.0) to a control's/container's scaleX and scaleY variables, this isn't appropriate because it results in text that's very hard to read. Instead, I've chosen to scale down each control's/text node's font, which you've probably figured out while examining Listing 2.

The code assumes that the desktop profile is current and generates an appropriate width and height based on the dimensions of the desktop's primary screen. It then creates scaling factors for scaling the horizontal/vertical spaces between controls as well as textbox columns (scale_factorX and scale_factorY), and for scaling control/text fonts (scale_factor).

The scaling factors are based on the scene's width and height variable values. Binding causes these values to eventually reflect the desktop's, the browser applet area's, or the mobile emulator screen's dimensions. For the desktop profile, scale_factorX and scale_factorY are set to 1.0. For the other profiles, these variables are set to fractions greater than 0.0 and less than 1.0.

Moving on, three font instances are created: one instance for label text, one instance for title text, and one instance for footer text. These instances are all based on the default Verdana font, and their sizes are dynamically calculated by taking the scale_factor value (between 0.0 and 1.0) into account. For a scale_factor value of 0.5 or higher, the text should be legible. (Try adjusting the various scaling constants.)

Finally, Listing 2 takes advantage of the font and scaling variables in a bind context to dynamically size text, various controls, and the spacing between these controls. You've already seen the result of this dynamic activity in Figure 4's browser context. Figure 5 shows the result of running this application in the mobile emulator, in landscape mode.

Once again, the listview's text size is out of
proportion to the rest of the text.

Figure 5: Once again, the listview's text size is out of proportion to the rest of the text.

Unfortunately, the listview control doesn't let us shrink the size of the font used to display each item's text -- assigning a different font to listview's caspian skin's font variable accomplishes nothing. To overcome this problem, we'll need to create an improved listview control, which is the focus of the next section.

Units Converter: Version Three

The third and final version of the units-conversion application extends the second version by scaling the font used by the listview control to present its text. It accomplishes this task by extending this control's caspian-based skin class with a new class that focuses on adjusting each list item's font. Figure 6 reveals the resulting UI when shown in a mobile context (in landscape mode).

Although not perfect (one listview item has its text
partly cut off), at least the size of each listview item's text is in keeping
with the size of each label's text.

Figure 6: Although not perfect (one listview item has its text partly cut off), at least the size of each listview item's text is in keeping with the size of each label's text.

Listing 3 presents the application's Main.fx source code.

/*
 * Main.fx
 */

package unitsconverter3;

import java.lang.NumberFormatException;

import javafx.geometry.HPos;
import javafx.geometry.VPos;

import javafx.scene.Scene;

import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ListView;
import javafx.scene.control.Skin;
import javafx.scene.control.TextBox;

import javafx.scene.layout.HBox;
import javafx.scene.layout.LayoutInfo;
import javafx.scene.layout.VBox;

import javafx.scene.paint.Color;
import javafx.scene.paint.LinearGradient;
import javafx.scene.paint.Stop;

import javafx.scene.text.Font;
import javafx.scene.text.Text;

import javafx.stage.Screen;
import javafx.stage.Stage;

import javafx.util.Math;

class Conversion
{
    var src: String;
    var dst: String;
    var func: function (input: Double): Double
}

var conversions =
[
    Conversion
    {
        src: "Millimeters"
        dst: "Centimeters"
        func: function (input: Double): Double
        {
            input*0.1
        }
    }

    Conversion
    {
        src: "Centimeters"
        dst: "Millimeters"
        func: function (input: Double): Double
        {
            input*10
        }
    }

    Conversion
    {
        src: "Miles"
        dst: "Meters"
        func: function (input: Double): Double
        {
            input*1609.344
        }
    }

    Conversion
    {
        src: "Meters"
        dst: "Miles"
        func: function (input: Double): Double
        {
            input/1609.344
        }
    }

    Conversion
    {
        src: "Degrees Celsius"
        dst: "Degrees Fahrenheit"
        func: function (input: Double): Double
        {
            input*9.0/5.0+32.0
        }
    }

    Conversion
    {
        src: "Degrees Fahrenheit"
        dst: "Degrees Celsius"
        func: function (input: Double): Double
        {
            (input-32.0)*5.0/9.0
        }
    }
];

// Assume desktop profile and a 550-by-350-pixel viewing area.

var width = 550.0;
var height = 350.0;

// If desktop profile, narrow the width and height if they exceed the desktop's
// primary screen's current bounds.

if (__PROFILE__ == "desktop")
{
    def bounds = Screen.primary.bounds;
    width = Math.min (bounds.width, width);
    height = Math.min (bounds.height, height)
}

// At this point, the actual width and height of the desktop's viewing area are
// known.

// However, it's possible that the profile is mobile or browser, in which case
// the viewing area is probably smaller. To account for this possibility,
// calculate appropriate horizontal and vertical scaling factors, and choose an
// appropriate size for the default font.

var sceneRef: Scene;

def scale_factorX = bind sceneRef.width/width;
def scale_factorY = bind sceneRef.height/height;
def scale_factor = bind Math.min (scale_factorX, scale_factorY);

// According to the JavaFX Font documentation, the font size defaults to 12
// points. However, I've discovered that the actual setting (on a Windows XP
// platform) is 11 points. This setting can be left as-is for the desktop
// profile, but will need to be shrunk for the mobile and browser profiles.

def font = bind Font.font ("Verdana", 5+scale_factor*6);
def titleFont = bind Font.font ("Verdana", 16+scale_factor*8);
def footerFont = bind Font.font ("Verdana", 4+scale_factor*4);

Stage
{
    title: "Units Converter"

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

        fill: LinearGradient
        {
            startX: 0.0
            startY: 0.0
            endX: 0.0
            endY: 1.0
            stops:
            [
                Stop { offset: 0.0 color: Color.YELLOW }
                Stop { offset: 1.0 color: Color.PINK }
            ]
        }

        content: VBox
                 {
                     width: bind sceneRef.width
                     height: bind sceneRef.height

                     nodeHPos: HPos.CENTER
                     hpos: HPos.CENTER
                     vpos: VPos.CENTER
                     spacing: bind 20*scale_factorY

                     var listView: ListView
                     var textInput: TextBox
                     var textOutput: TextBox

                     content:
                     [
                         Text
                         {
                             content: "Units Converter"
                             font: bind titleFont
                         }

                         HBox
                         {
                             content:
                             [
                                 Label
                                 {
                                     text: "Enter number of input units"
                                     font: bind font
                                 }

                                 textInput = TextBox
                                 {
                                     columns: bind 20*scale_factorX
                                     selectOnFocus: true
                                     font: bind font
                                 }
                             ]

                             spacing: bind 20*scale_factorX
                         }

                         VBox
                         {
                             spacing: bind 10*scale_factorY
                             nodeHPos: HPos.CENTER

                             content:
                             [
                                 Label
                                 {
                                     text: "Select a conversion"
                                     font: bind font
                                 }

                                 listView = MyListView
                                 {
                                      items: for (conversion in conversions)
                                                 "{conversion.src} To "
                                                 "{conversion.dst}"

                                      layoutInfo: LayoutInfo
                                      {
                                          height: bind 76*scale_factorY
                                          width: bind 300*scale_factorX
                                      }
                                 }
                             ]
                         }

                         HBox
                         {
                             content:
                             [
                                 Label
                                 {
                                     text: "Equivalent number of output units"
                                     font: bind font
                                 }

                                 textOutput = TextBox
                                 {
                                     columns: bind 20*scale_factorX
                                     editable: false
                                     font: bind font
                                 }
                             ]

                             spacing: bind 20*scale_factorX
                         }

                         Button
                         {
                             text: "Convert"
                             font: bind font
                             action: function (): Void
                             {
                                 try
                                 {
                                     var index = listView.selectedIndex;
                                     var input = textInput.text;
                                     var inVal = Double.parseDouble (input);
                                     var outVal = conversions [index].
                                                    func (inVal);
                                     textOutput.text = "{outVal}"
                                 }
                                 catch (nfe: NumberFormatException)
                                 {
                                     textOutput.text = "Error"
                                 }
                             }
                         }

                         Text
                         {
                             content: "Created by Jeff Friesen"
                             font: bind footerFont
                         }
                     ]
                 }
    }
}

class MyListView extends ListView
{
    var _skin = MyListViewSkin {}

    var _font = bind font on replace
    {
        _skin._font = _font;
    }

    init
    {
        select (0);
    }

    override function createDefaultSkin (): Skin
    {
        _skin
    }
}

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

Listing 3 extends Listing 2's MyListView class by introducing a new _skin variable that's assigned a MyListViewSkin instance (this class is described shortly), by introducing a new _font variable whose replace trigger initializes _skin's _font variable whenever font changes, and by overriding the ListView superclass's createDefaultSkin() function to return _skin's value.

Listing 4 presents the application's MyListViewSkin.java source code.

/*
 * MyListViewSkin.java
 */

package unitsconverter3;

import javafx.scene.control.Label;

import javafx.scene.text.Font;

import com.sun.javafx.scene.control.Cell;

import com.sun.javafx.scene.control.caspian.ListViewSkin;

public class MyListViewSkin extends ListViewSkin
{
    Font _font;

    MyListViewSkin ()
    {
        loc$com$sun$javafx$scene$control$caspian$ListViewSkin$vbar.get ().
          set$focusTraversable(false);
    }

    @Override
    public Cell createCell(int itemIndex, Cell recycled)
    {
        Cell cell = super.createCell (itemIndex, recycled);
        Label l = (Label) cell.$content;
        l.set$font (_font);
        return cell;
    }
}

Listing 4: MyListViewSkin.java (from a UnitsConverter3 NetBeans IDE 6.5.1 with JavaFX 1.2 project)

MyListViewSkin's constructor overcomes a focus problem with the listview control. Specifically, pressing the Tab key (on Windows) doesn't transfer focus to the output textbox. In fact, no non-listview control has the focus (as indicated by surrounding the control with a blue focus rectangle). You must press Tab a second time to transfer focus to the output textbox.

It turns out that the first Tab press causes focus to shift to the scrollbar control, which is part of the listview control. The solution to this problem is to remove the scrollbar from the focus traversal cycle. In JavaFX, you would simply assign false to scrollbar's inherited focusTraversable variable. Listing 4 shows how this task is accomplished in Java.

The reason for implementing MyListViewSkin in Java instead of JavaFX is that I couldn't implement the createCell() method in JavaFX. This method ends up creating a label control for a listview cell. After extracting the cell's label, createCell() assigns MyListView's current _font setting to the label's font variable.

Conclusion

Although I've accomplished my objectives with units converter, I feel like overhauling the listview control (beyond customizing the font and fixing the focus problem). As you work with this control, you'll encounter various problems, such as empty space appearing at the top of the list, or the scrollbar disappearing once you move to the last item in the list (by continually pressing the down-arrow key).

Apart from listview, I'm troubled by small text appearing to "bleed" on the mobile emulator's screen. When you observe the smaller text in Figures 4 (browser context) and 5/6 (mobile context), you'll probably notice that it appears ragged in Figures 5/6. Ironically, it looks crisper in Figure 4's smaller screen size, which suggests a problem with the mobile emulator's ability to clearly display small text.


Download code.zip

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


Privacy Policy

If this website is helpful to you, please make a PayPal donation to help offset the cost of hosting and maintenance. Thankyou.

Valid HTML 4.01 Strict