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, 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. (I'll discuss this topic later.)

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:

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.

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.

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 a new annoyance: 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 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 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.

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.

Download code.zip

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

via javajeff.mb.ca

Enjoy:)



Share on Hacker News
Share on LinkedIn


← Home


Want to learn more?

Sign up to get a digest of my articles and interesting links via email every month.

* indicates required

Please select all the ways you would like to hear from Krzysztof Kula:

You can unsubscribe at any time by clicking the link in the footer of my emails.

I use Mailchimp as a marketing platform. By clicking below to subscribe, you acknowledge that your information will be transferred to Mailchimp for processing. Learn more about Mailchimp's privacy practices here.