Units Converter
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. After presenting the first version and revealing three listview control-oriented annoyances, the article presents the second version, which fixes one of these annoyances (and reveals another), and which adapts its UI to various contexts. Lastly, the article presents the third version, which fixes the remaining annoyances by updating the listview control.
I created these versions via NetBeans IDE 6.5.1 with JavaFX 1.2
UnitsConverter1
,UnitsConverter2
, andUnitsConverter3
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.
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 aUnitsConverter1
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 moreConversion
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 localsceneRef
variable and the stage'sscene
variable. ThesceneRef
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
andheight
variables. Unlike the stage's equivalentwidth
andheight
variables, the values assigned to the scene'swidth
andheight
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
andheight
. 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. (I'll discuss this topic later.)Moving on, the listing assigns a
LinearGradient
instance to the scene'sfill
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 ignorefill
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'scontent
variable. A vbox is a container that lays out itscontent
sequence of nodes in a single vertical column. It resizes eachcontent
node whose class implements theResizable
interface to the node's preferred size.Within the vbox, Listing 1 first binds this container's
width
andheight
variables to the scene'swidth
andheight
variables (which are accessed via thesceneRef
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'shPos
variable, and by assigningVPos.CENTER
to the vbox'svPos
variable. It's also necessary to center the nodes within the column, by assigningHPos.CENTER
to the vbox'snodeHPos
variable.After assigning
20
pixels of vertical space to the vbox'sspacing
variable, to leave this much empty vertical space between each node in this container'scontent
sequence, Listing 1 focuses on populating this sequence. The following list highlights some of the more interesting aspects of this task:
Font
'spublic 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'sselectOnFocus
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 ofConversion
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'slayoutInfo
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 itseditable
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'sselectedIndex
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 aNumberFormatException
, andError
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 skeletalMain.fx
source code with Listing 1. After accomplishing these tasks, pressF6
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 reveals0.0
no matter what value you enter into the input textbox. Why? Hint: Check out the function assigned to the button'saction
variable.The first line in this function,
var index = listView.selectedIndex;
, assigns the index of the selected listview item toindex
. When no item is selected,selectedIndex
contains -1, which is assigned toindex
. This variable is subsequently used to indexconversions
. Instead of throwing an exception, JavaFX ignores the -1 index and assigns 0.0 tooutVal
, 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.While playing with the application, you'll probably discover two more annoyances. First, when the listview has the focus, and you shift focus forward (by pressing the
Tab
key on Windows), you probably expect to see the output textbox receive focus. However, the focus disappears: You must shift focus forward a second time for the output textbox to receive focus.The other annoyance occurs when you scroll down the listview via the keyboard. Keep pressing the down-arrow key to scroll down to the last item in the list; the listview's scrollbar disappears when you reach this item. After scrolling backwards (via the up-arrow key) a couple of times, the scrollbar reappears. As with the previous annoyance, I'll address this annoyance in the final section of this article.
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.
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.
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.
Figure 4: Although it looks better than Figure 3, the browser-based UI reveals a new annoyance: 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 aUnitsConverter2
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'spublic select(itemIndex: Integer): Void
function with a0
argument to select the first item in the listview. Of course,MyListView
must be instantiated instead ofListView
for this feature to take effect.You might be wondering why I didn't invoke
ListView
'spublic selectFirstRow(): Void
function to accomplish this task. After all, this function is more appropriately named. However, my decompiler shows that this function executesselect (0)
only whenListView
'sselectedIndex
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
andscaleY
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
andscale_factorY
), and for scaling control/text fonts (scale_factor
).The scaling factors are based on the scene's
width
andheight
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
andscale_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 ascale_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.
Figure 5: Once again, the listview's text size is out of proportion to the rest of the text.
Figures 4 and 5 reveal a new listview annoyance. Unfortunately, this 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 correct this and the two previously discussed annoyances, we need to create an improved listview control, which is the focus of the next section.Conclusion #
Because I'm still working on overcoming the listview control's scrollbar and focus annoyances (I've already overcome this control's font annoyance), and because I don't want to delay releasing the article until I finish, I've decided to release this article without its final section. I'll update the article with the final section as soon as possible.
Note: Applications created with JavaFX 1.2 (via NetBeans IDE 6.5.1) on top of Java SE 6u12.
via javajeff.mb.ca
Enjoy:)
Want to learn more?
Sign up to get a digest of my articles and interesting links via email every month.