Note from Jim Weaver: Please enjoy this post written by Dean Iverson. If you haven't met Dean, please read his first post on the JavaFXpert blog in which I introduced him as a regular contributor.
Time flies when you're playing with JavaFX. A few weeks ago, Jim started a series on this
blog with the goal of demonstrating how to write an application in JavaFX. He chose a simple
calculator application and wrote
two
good
articles to kick things off as part of the Simple Calculator App series. Since then, he has been busy with
other things
so he asked me to finish it up. We're going to have to move quickly to finish this thing
in one article. Since Jim so nicely got all of the basics out of the way, this article
will be a little more advanced and a bit longer. I'm going to assume that you, dear
reader, are familiar with the basic syntax of JavaFX. Also, if you'd like to have the full source code while you're reading this, download the SimpleCalc3 project. It's in NetBeans project format, and you'll need to unzip it, but you can view the sources in any editor. So grab your preferred caffeine
delivery system and buckle up. Here we go!
The Calculator Model
The model for a simple 9 character, 4 function calculator is pretty basic. I modeled the
display, a register to store intermediate results, and the keys. The code is shown below.
public class CalculatorModel {
public-init var displayLength = 9;
public-read var display = "0" on replace {
if( display.length() >= displayLength ) {
display = display.substring(0, displayLength);
}
}
public-read var keys = [
createCharacterKey( "0" ),
createCharacterKey( "1" ),
createCharacterKey( "2" ),
createCharacterKey( "3" ),
createCharacterKey( "4" ),
createCharacterKey( "5" ),
createCharacterKey( "6" ),
createCharacterKey( "7" ),
createCharacterKey( "8" ),
createCharacterKey( "9" ),
createOperatorKey( "+", "plus", add ),
createOperatorKey( "-", "minus", subtract ),
createOperatorKey( "x", "multiply", multiply ),
createOperatorKey( "\u00f7", "divide", divide ),
createCharacterKey( "." ),
KeyModel {
character: "="
description: "equals"
action: function() {
performOp( add );
display = if (register == (register as Integer))
"{register as Integer}"
else
"{register}";
register = 0;
}
}
];
var register = 0.0;
var operation = add;
var clearDisplayOnNextCharacter = true;
package function appendToDisplay( character: String ) {
if (character.matches( "[0-9\\.]" ))
display = "{display}{character}";
}
function createCharacterKey( character: String ): KeyModel {
KeyModel {
character: character
description: character
action: function() {
if (clearDisplayOnNextCharacter) {
display = "";
clearDisplayOnNextCharacter = false;
}
appendToDisplay( character );
}
}
}
function createOperatorKey( character:String, description:String, nextOp:function() ) {
KeyModel {
character: character
description: description
action: function() {
performOp( nextOp );
}
}
}
function performOp( nextOp: function() ) {
operation();
operation = nextOp;
clearDisplayOnNextCharacter = true;
}
function add(): Void {
register = register + Double.parseDouble( display );
}
function subtract(): Void {
register = register - Double.parseDouble( display );
}
function multiply(): Void {
register = register * Double.parseDouble( display );
}
function divide(): Void {
register = register / Double.parseDouble( display );
}
}
The first part declares the displayLength and the display variables. The displayLength
defaults to 9 characters and can be initialized in a declaration. The display variable
holds the string that is displayed on the calculator's screen. It is read-only by the
outside world and I used a replace trigger to make sure the display string stays the
proper length. Truncating the display string like this works for a lot of cases but not
all. Just don't use this calculator to design any bridges and everything will be fine.
The next section declares the sequence of keys. These are actually instances of the
KeyModel class which is a small class containing variables that define a key's display
character, description, and its action function. The action function is what gets
called when the key is pressed. You will notice that I created some helper functions
createCharacterKey
and createOperatorKey
to eliminate some
redundant code. Two other things to note are the use of a Unicode character to get a nice
symbol for the division key, and the special processing done by the action function
of the equals key. The equals key performs the pending operation and then moves the
contents of the register into the display string before zeroing out the register.
On to the display handling. Here we have the appendToDisplay
function that adds
a character to the display. It does a quick regular expression match to make sure the
character in the string is a legal calculator character and then appends it to the
existing display string. All of the normal Java string methods can be called on JavaFX strings.
Don't you just love the fact that all of the power of Java is available to JavaFX?
The rest of the class just takes care of the details of performing operations and
storing the results. One thing to remember is that the operation
var
holds the "pending" operation - the next operation to be performed. When you think
about the sequence of keys used to enter 2 + 2
you can see that the plus
key needs to store the first 2 in the register and then save the add function as the
pending operation since we don't know what to add to the first two until the next
operation key is pressed (which could be another arithmetic operator or the equals key).
Graphics usually grabs all of the headlines when JavaFX is being discussed. But I
have absolutely fallen in love with the power of the language itself. Binding to
arbitrary expressions is addictive and being able to create and pass around functions
(closures) as first class objects is incredibly powerful. I think Sun actually did
this language a disservice by saddling it with the "Script" label.
Now we're getting warmed up! On to the eye candy.
A Calculator Takes the Stage
Here is a look at the finished product.
To run the calculator as a JavaFX applet in a Web page, click the calculator image above. To run the calculator as a Web Start application, click the Launch button below:
And here is the code:
Stage {
// The model
def model = CalculatorModel {}
def columns = 4
def keyPositionMap = [
7, 8, 9, 10,
4, 5, 6, 11,
1, 2, 3, 12,
14, 0, 15, 13
]
var theScene: Scene;
title: "Calculator 3"
scene: theScene = Scene {
width: 300
height: 400
fill: Color.BLACK
stylesheets: "{__DIR__}simpleCalc3.css"
content: Grid {
border: 10
width: bind theScene.width
height: bind theScene.height
hgap: 10
vgap: 20
rows: [
Row {
cells: Cell {
columnSpan: columns
content: CalculatorDisplay {
text: bind model.display
width: 280
height: 50
}
}
},
for (i in [0..<(sizeof keyPositionMap / columns)]) {
Row {
cells: for (j in [0..<columns]) {
CalculatorKey {
var keyIndex = keyPositionMap[i * columns + j];
var keyModel = model.keys[keyIndex];
character: keyModel.character
action: keyModel.action
styleClass: "calculatorKey"
id: keyModel.description
}
}
}
}
]
}
}
}
This is all pretty compact and straightforward. The first thing I've done is define a
few constants: the calculator model, the number of columns and a keyPositionMap
.
This map is actually a sequence that contains the indexes of the keys from the model. They
are ordered by the position at which they will be displayed on the calculator's panel.
This really just allows me to easily place the keys where I want them.
Next I define a var to hold the scene. This is necessary so that I can bind to the
scene's width and height within my declarative code a littler farther down. Then we
get to the stage's title and, finally, to
the declaration of the scene itself. The width and height of the scene determines the size of the drawing area inside the application's window. Note that its background is filled with the color black
and that it uses a stylesheet. This should come as no surprise to you if you read my
previous article.
Stylesheets are a wonderful thing.
And finally we arrive at the content of the scene. I use the
Grid
layout from the excellent JFXtras project.
It is a natural fit for a calculator key layout. Placed inside the grid and spanning all
of the columns in the first row is the calculator's display. This is a custom node that
is composed of a Text node and a background Rectangle
. The content of the
text node is bound to the calculator model's display string. Easy! Here is the code for
the CalculatorDisplay
class.
public class CalculatorDisplay extends CustomNode {
public var displayColor = Color.BLACK;
public var textColor = Color.WHITE;
public var text: String;
public var width: Number = 100;
public var height: Number = 40;
var background = bind LinearGradient {
endX: 0.0
endY: 1.0
stops: [
Stop {
offset: 0.0
color: displayColor.ofTheWay( Color.WHITE, 0.90 ) as Color
},
Stop {
offset: 0.49
color: displayColor.ofTheWay( Color.WHITE, 0.10 ) as Color
},
Stop {
offset: 0.5
color: displayColor
},
Stop {
offset: 1.0
color: displayColor.ofTheWay( Color.WHITE, 0.25 ) as Color
},
]
}
override function create():Node {
Group {
def r:Rectangle = Rectangle {
width: bind width
height: bind height
arcHeight: 2
arcWidth: 2
smooth: true
stroke: Color.DIMGRAY
fill: bind background
}
def t:Text = Text {
translateX: bind (r.width - t.layoutBounds.width) - t.layoutBounds.minX
translateY: bind (r.height - t.layoutBounds.height) / 2 - t.layoutBounds.minY
content: bind text
fill: bind textColor;
font: Font {
size: 48
}
textOrigin: TextOrigin.TOP
}
content: [ r, t ]
}
}
}
There are a few points of interest in this class. One is the little trick
I use to generate gradients from a single color. The ofTheWay
function of
the Color
class works really well for this. You can use it to interpolate
between any two colors. Take the first stop in the gradient above as an example. It uses the
ofTheWay
function to create a color that, starting from
displayColor
, is 90% of the way to white. In other words, a very bright
version of displayColor
. You can make a darker version of a color by
using ofTheWay
with black. Technically this function is there for
animating color changes. But hey, why write code when you can re-use it, right?
Speaking of the gradient, you should note the use of a bind statement in the
declaration of the background
var. This way when the
displayColor
changes, the background gradient is automatically
updated. This becomes important when dealing with styling which I will get into
later.
Another interesting bit is the positioning of the Text node inside the Rectangle node. If you are still fuzzy about
the difference between a node's boundsInLocal
, boundsInParent
,
and layoutBounds
then you need to go read
this
immediately.
Okay, back to the stage and scene declarations. We'll go through the next section line by line.
I will reproduce it below so you don't have to scroll back up. That's just the kind of
service you should expect here at the JavaFXpert blog! Besides, Jim is paying me by the
word. Right, Jim? Jim?
Ok, maybe not. Here is that next chunk of code.
for (i in [0..<(sizeof keyPositionMap / columns)]) {
Row {
cells: for (j in [0..<columns]) {
CalculatorKey {
var keyIndex = keyPositionMap[i * columns + j];
var keyModel = model.keys[keyIndex];
character: keyModel.character
action: keyModel.action
styleClass: "calculatorKey"
id: keyModel.description
}
}
}
}
The value returned by this for
expression is a sequence of Row
objects. Each Row
contains a sequence of CalculatorKey
s, one
for each column, generated by a second for
expression. In JavaFX, code blocks
can return a value. The return value of a for
expression, then, is the
sequence of values returned by its associated block. This is a very powerful concept.
In this case, I am basically using a for
expression as a code generator.
As for the values that control the for
loops, the number of entries in the
keyPositionMap
divided by the number of columns gives the number of rows
that will be needed. Then for each row, I generate a CalculatorKey
for
each column. The current values of i
and j
allow me to figure
out where I am in the keyPositionMap
. The value stored at the current
location in the position map is the index of the desired KeyModel
. Once
we have that, it's just a matter of using the KeyModel
's values in the CalculatorKey
declaration. The CalculatorKey
class is
another CustomNode
and the code is very similar to that of
CalculatorDisplay
.
Note that I set both the styleClass
and the id
of each calculator
key. As we'll see in the next section, this allows me to apply styling to all of the
keys at once as well as to individual keys. Since styling is applied later, I can treat
all of the CalculatorKey
nodes exactly the same when I declare them. This
allows my code to be much more compact and readable.
Styling - The Final Touch
Ah, styling. How do I love thee? Let me count the ways:
for (i in [0..infinity])
. Ok, seriously, my point is that I really love
the styling support in JavaFX. It keeps the code clean by separating out the presentation
details and putting them into easily editable files. Throw in some skins and the
potential is limitless.
So let's start with the calculator's display. You can see in the code presented earlier
that the default display color is black with white text. Now, I like basic black as
much as anyone but I think we can do better. So let's lighten up the display's
background color a bit. Since displayColor
is a public var
in
the class, it is automatically eligible for styling. All we have to do is open the
scene's stylesheet and add:
CalculatorDisplay {
textColor: black;
displayColor: #d5d9b8;
}
Isn't this fun? On to the keys! Since each CalculatorKey
node has
a style class of "calculatorKey" associated with it, we can easily apply a
style to every key.
.calculatorKey {
keyColor: black;
characterColor: white;
}
The CalculatorKey
class has two public vars named keyColor
and characterColor
that I am setting.
Instead of using the name of the class like we did for the calculator's display, we use
the style class which, in CSS syntax, is a period followed by the class name. Since
I didn't specify anything before the period, every node in the entire scene graph that
has a style class of "calculatorKey" will get this style applied to it. Luckily, I
only applied that class to CalculatorKey
nodes so it will work fine. If
I wanted to make sure it was only applied to CalculatorKey
nodes, I could
have declared that style as CalculatorKey.calculatorKey
but in this case
that is redundantly redundant.
To take this further, I wanted to make sure that the special keys like the arithmetic
operations and the equals key stood out from the regular keys. So I applied a special
style to those keys only using the id
that I gave to each node when I
created it. In theory, every node's id
should be unique, but that is not
enforced in the scenegraph.
#plus, #minus, #multiply, #divide {
keyColor: brown;
}
In CSS, an id is specified by the "#" character. In this case, I have a comma separated
list that declares all of the node id
s that I want this style applied to.
I only have to set the keyColor
since I want these keys to have the same
characterColor
as the regular keys. Since I don't override that value here
it will be inherited from the .calculatorKey
style above.
Finally, I want the equals key to really stand out so I gave it the following style.
#equals {
keyColor: orange;
characterColor: black;
}
And there we have the finished application as it appears above. I kind of like the
basic black background of the scene. It is simple and elegant. But if I want to try
out a very subtle gradient for the background, I can do that easily.
Scene {
fill: linear (0%, 0%) to (0%, 100%) stops (0.00, #1a1a1a),
(0.25, #1d1d1d), (0.27, #222222), (0.66, #0d0d0d),
(1.0, #000000);
}
Well isn't that convenient? Even the Scene
's fill
attribute
can be styled from the stylesheet. Here are the two versions of the calculator in a
side by side comparison. On the left is the black background, on the right is the
subtle gradient. It's just enough to break up the background a tiny bit. Too subtle
for your taste? No problem! Just edit the stylesheet to change it.
This application demonstrates the potential that JavaFX has as a platform and the
power that is behind the language. This is a very exciting time to be working with
JavaFX. There are new discoveries waiting around every corner. If you haven't given
in to the siren's call of this new technology yet, I would encourage you to check it out.
A great place to start is the JFXtras project
where developers are coming together to fill in the gaps and explore the capabilities of
this new platform. Why let everyone else have all of the fun?
Dean Iverson
JavaFXpert.com
Recent Comments