Scaffold small client application frameworkwww.meldcraft.com
Slacker Guide to Java Swing Application Development
Using the Scaffold small client application framework

Calculator Application

One powerful feature of the Scaffold framework is extensibility through layered properties files. Every Scaffold application can be declared in a single properties file, but in practice it is often prudent to organize the application into multiple files that are merged (at runtime) to create extended works.

The inspiration for the Calculator comes from the F3 blog (subsequently rebranded as JavaFX, apparently), which was inspired by an app for the Mac OSX implemented in HTML/CSS/Javascript.

F3 Calculator Screen Shot

The web is everywhere, and RIA is vying for ubiquity. A goal of Scaffold is to make Java a more viable RIA client platform, and making simple, attractive apps like the Calculator simple to implement is one way to achieve that.

Step 1 - Run the Application

Because we can!

java -cp Scaffold.jar \
com.meldcraft.application.AppManager \
com.meldcraft.calculator.CalculatorApp

Calculator1 Screen Shot

Hard to believe this scrunched up little JFrame will morph into an elegant Calculator...

Step 2 - Swing GUI

Here is the entire GUI using plain old Swing components declared in a single properties file - no Java required.

Application Files
com/meldcraft/calculator/CalculatorApp.properties
com/meldcraft/calculator/CalculatorApp.properties
Application.name = Calculator

Application.UIRoot.elementProperties = undecorated=true

Application.rootContent = CalcPanel

CalcPanel.baseResource = com.meldcraft.application.guis.SSFactory
CalcPanel.type = panel
CalcPanel.elementRows = NumberField                       ,\
                        MAdd | MSub | MRec | MClear | Div ,\
                        NumberPanel <<<             | Mul ,\
                        ^                           | Sub ,\
                        ^                           | Add ,\
                        ^                           | [fill:-1 12] ,\
                        ^                           | Equal

NumberField.baseResource = com.meldcraft.application.guis.SSFactory
NumberField.type = textField
NumberField.elementProperties = editable=false, background=white,\
                                horizontalAlignment=RIGHT,\
                                text=0

NumberPanel.baseResource = com.meldcraft.application.guis.SSFactory
NumberPanel.type = panel
NumberPanel.elementGrid = NP7 | NP8 | NP9 ,\
                          NP4 | NP5 | NP6 ,\
                          NP1 | NP2 | NP3 ,\
                          NP0 | NPDot | NPClear

KeyRep.{1}.name = {2}
KeyRep.{1}.elementProperties = focusable=false, margin=0,0,0,0,\
                               preferredSize=28,28
KeyRep.replicateElement = MAdd;m+,MSub;m-,MRec;mr,MClear;mc

OpRep.{1}.name = {2}
OpRep.{1}.accelerator = {3}
OpRep.{1}.elementProperties = focusable=false, margin=0,0,0,0,\
                              preferredSize=28,28
OpRep.replicateElement = Div;\u00f7;DIVIDE | SLASH,\
                         Mul;\u00d7;MULTIPLY | ASTERISK | shift 8,\
                         Add;+;PLUS | ADD | shift EQUALS,\
                         Sub;-;MINUS | SUBTRACT,\
                         Equal;=;EQUALS | ENTER,\
                         NPDot;.;DECIMAL | PERIOD,\
                         NPClear;c;C

Equal.elementProperties = focusable=false, margin=0,0,0,0, preferredSize=28,56

RadioRep.{1}.type = radioOp
RadioRep.{1}.buttonStyle = toggle
RadioRep.replicateElement = Div,Mul,Add,Sub

NPrep.NP{1}.name = {1}
NPrep.NP{1}.accelerator = {1} | NUMPAD{1}
NPrep.NP{1}.elementProperties = focusable=false, margin=0,0,0,0,\
                                preferredSize=38,38
NPrep.replicateElement = 0,1,2,3,4,5,6,7,8,9

Application.contextListeners = NPClear.action=ClearOpInvoker,\
                               Equal.action=ClearOpInvoker

ClearOpInvoker.forward = Op.selectValue=<null>

Compile the application (just copying files at this point), and run it:

java -cp Scaffold.jar;ScaffoldGUIS.jar;Calculator/classes \
com.meldcraft.application.AppManager \
com.meldcraft.calculator.CalculatorApp

Calculator2 Screen Shot

Mostly functional - the buttons work, the operators hold toggle state. The buttons are aligned neatly, and the undesired frame decorations are configured away.

Step 3 - Business Logic

Next, we add some Java code to implement the calculator logic. Just a POJO based on some code found on the internet. The key method is calculate(), which takes a String argument (the key press), and returns the text that should be displayed.

A few more lines in the properties file hook up the GUI to the calculate() method.

Application Files
com/meldcraft/calculator/CalculatorApp.properties
com/meldcraft/calculator/Calculator.java
com/meldcraft/calculator/Calculator.java
package com.meldcraft.calculator;

public class Calculator
{
    /**
     * Issues command and returns the display text.  The command
     * is any key on the calculator.
     * 
     * @param command
     * @return
     */
    public synchronized String calculate(String command)
    {
        String ret = null;

        boolean handled = false;
        String pre = "calc.";
        if (command.startsWith(pre) && (command.length() > pre.length()))
        {
            if (doCalculate(command.substring(pre.length())))
            {
                handled = true;
                ret = text;
            }
        }

        if (!handled)
        {
            throw new RuntimeException("Unknown command: " + command);
        }

        return ret;
    }

    /*
     * Calculator implementation based on http://www.yamaza.com/java/Calc.java,
     * http://www.geocities.com/entity05/java/myjava.html
     */
    private double dReg1;
    private double dReg2;
    private String sOperator;
    private String text = "0";
    private boolean isFixReg = true;
    private String sText2;
    private String sText1;
    private double dMem;

    private boolean doCalculate(String arg)
    {
        boolean ret = true;

        //
        // numeric key input
        //
        if ("c".equals(arg))
        {
            dReg1 = 0.0d;
            dReg2 = 0.0d;
            sOperator = "";
            text = "0";
            isFixReg = true;
        }
        else if (("0".equals(arg)) | ("1".equals(arg)) | ("2".equals(arg))
               | ("3".equals(arg)) | ("4".equals(arg)) | ("5".equals(arg))
               | ("6".equals(arg)) | ("7".equals(arg)) | ("8".equals(arg))
               | ("9".equals(arg)) | (".".equals(arg)))
        {
            if (isFixReg)
                sText2 = (String) arg;
            else
            {
                if (!".".equals(arg) || sText2.indexOf(".") == -1)
                    sText2 = text + arg;
            }
            text = sText2;
            isFixReg = false;
        }
        //
        // operations
        //
        else if (("+".equals(arg)) | ("-".equals(arg))
               | ("\u00d7".equals(arg)) | ("\u00f7".equals(arg))
               | ("=".equals(arg)))
        {
            sText1 = text;
            dReg2 = (Double.valueOf(sText1)).doubleValue();
            dReg1 = calcValue(sOperator, dReg1, dReg2);
            Double dTemp = new Double(dReg1);
            sText2 = dTemp.toString();
            text = sText2;
            sOperator = (String) arg;
            isFixReg = true;
        }
        //
        // memory clear operation
        //
        else if ("mc".equals(arg))
        {
            dMem = 0;
        }
        //
        // memory read operation
        //
        else if ("mr".equals(arg))
        {
            Double dTemp = new Double(dMem);
            sText2 = dTemp.toString();
            text = sText2;
            sOperator = "";
            isFixReg = true;
        }
        //
        // memory add operation
        //
        else if ("m+".equals(arg))
        {
            sText1 = text;
            dReg2 = (Double.valueOf(sText1)).doubleValue();
            dReg1 = calcValue(sOperator, dReg1, dReg2);
            Double dTemp = new Double(dReg1);
            sText2 = dTemp.toString();
            text = sText2;
            dMem = dMem + dReg1;
            sOperator = "";
            isFixReg = true;
        }
        //
        // memory sub operation
        //
        else if ("m-".equals(arg))
        {
            sText1 = text;
            dReg2 = (Double.valueOf(sText1)).doubleValue();
            dReg1 = calcValue(sOperator, dReg1, dReg2);
            Double dTemp = new Double(dReg1);
            sText2 = dTemp.toString();
            text = sText2;
            dMem = dMem - dReg1;
            sOperator = "";
            isFixReg = true;
        }
        else
        {
            ret = false;
        }

        return ret;
    }

    //-------------
    // Calculation
    //-------------
    private double calcValue(String sOperator, double dReg1, double dReg2)
    {
        if ("+".equals(sOperator)) dReg1 = dReg1 + dReg2;
        else if ("-".equals(sOperator)) dReg1 = dReg1 - dReg2;
        else if ("\u00d7".equals(sOperator)) dReg1 = dReg1 * dReg2;
        else if ("\u00f7".equals(sOperator)) dReg1 = dReg1 / dReg2;
        else dReg1 = dReg2;
        return dReg1;
    }
}
com/meldcraft/calculator/CalculatorApp.properties
Application.name = Calculator

target = CalculatorPOJO
CalculatorPOJO.className = com.meldcraft.calculator.Calculator

Application.UIRoot.elementProperties = undecorated=true

Application.rootContent = CalcPanel

Application.mainactions = Exit

Exit.name = Exit
Exit.target = Application
Exit.method = applicationClose
Exit.accelerator = shift X | X

CalcPanel.baseResource = com.meldcraft.application.guis.SSFactory
CalcPanel.type = panel
CalcPanel.elementRows = NumberField                       ,\
                        MAdd | MSub | MRec | MClear | Div ,\
                        NumberPanel <<<             | Mul ,\
                        ^                           | Sub ,\
                        ^                           | Add ,\
                        ^                           | [fill:-1 12] ,\
                        ^                           | Equal

NumberField.baseResource = com.meldcraft.application.guis.SSFactory
NumberField.type = textField
NumberField.elementProperties = editable=false, background=white,\
                                horizontalAlignment=RIGHT,\
                                text=0

NumberPanel.baseResource = com.meldcraft.application.guis.SSFactory
NumberPanel.type = panel
NumberPanel.elementGrid = NP7 | NP8 | NP9 ,\
                          NP4 | NP5 | NP6 ,\
                          NP1 | NP2 | NP3 ,\
                          NP0 | NPDot | NPClear

KeyRep.{1}.name = {2}
KeyRep.{1}.elementProperties = focusable=false, margin=0,0,0,0,\
                               preferredSize=28,28
KeyRep.{1}.method = calculate
KeyRep.{1}.method.arg = calc.{2}
KeyRep.{1}.invokeReturnContextKey = calcText
KeyRep.replicateElement = MAdd;m+,MSub;m-,MRec;mr,MClear;mc

OpRep.{1}.name = {2}
OpRep.{1}.accelerator = {3}
OpRep.{1}.elementProperties = focusable=false, margin=0,0,0,0,\
                              preferredSize=28,28
OpRep.{1}.method = calculate
OpRep.{1}.method.arg = calc.{2}
OpRep.{1}.invokeReturnContextKey = calcText
OpRep.replicateElement = Div;\u00f7;DIVIDE | SLASH,\
                         Mul;\u00d7;MULTIPLY | ASTERISK | shift 8,\
                         Add;+;PLUS | ADD | shift EQUALS,\
                         Sub;-;MINUS | SUBTRACT,\
                         Equal;=;EQUALS | ENTER,\
                         NPDot;.;DECIMAL | PERIOD,\
                         NPClear;c;C

Equal.elementProperties = focusable=false, margin=0,0,0,0, preferredSize=28,56

RadioRep.{1}.type = radioOp
RadioRep.{1}.buttonStyle = toggle
RadioRep.replicateElement = Div,Mul,Add,Sub

NPrep.NP{1}.name = {1}
NPrep.NP{1}.accelerator = {1} | NUMPAD{1}
NPrep.NP{1}.elementProperties = focusable=false, margin=0,0,0,0,\
                                preferredSize=38,38
NPrep.NP{1}.method = calculate
NPrep.NP{1}.method.arg = calc.{1}
NPrep.NP{1}.invokeReturnContextKey = calcText
NPrep.replicateElement = 0,1,2,3,4,5,6,7,8,9

Application.contextListeners = calcText.invokeReturn=CalcTextInvoker,\
                               NPClear.action=ClearOpInvoker,\
                               Equal.action=ClearOpInvoker

ClearOpInvoker.forward = Op.selectValue=<null>

CalcTextInvoker.target = NumberField
CalcTextInvoker.method = setText

Compile and run the program.

Calculator3 Screen Shot

OK! A fully functioning calculator with the basic essence of the target application. Note that the calculator responds to key presses (per the Action accelerators); pressing "X" closes the calculator (and exits the VM).

Still a bit far from beautiful...

Step 4 - More Style

The F3 Calculator uses images for the buttons, and a nice LCD font. This time, instead of just augmenting CalculatorApp.properties, we'll leave that file in tact (so we can run it again to remind us how it all started, perhaps), and add the GUI tweaks to a new properties file which extends CalculatorApp.properties.

And, of course we need to add all of those icon images. Note the special naming of the icons precludes them from having to be specified in the properties file. E.g. ai0.png is used for the 0 button, and ai0p.png is used for the 0 button in the pressed state (the "ai" prefix stands for "Action Icon").

Application Files
com/meldcraft/calculator/CalculatorApp.properties
com/meldcraft/calculator/Calculator.java
com/meldcraft/calculator/fancy/CalculatorFancy.properties
com/meldcraft/calculator/fancy/LCD-N___.TTF
com/meldcraft/calculator/fancy/ai0.png
com/meldcraft/calculator/fancy/ai0p.png
com/meldcraft/calculator/fancy/ai1.png
com/meldcraft/calculator/fancy/ai1p.png
com/meldcraft/calculator/fancy/ai2.png
com/meldcraft/calculator/fancy/ai2p.png
com/meldcraft/calculator/fancy/ai3.png
com/meldcraft/calculator/fancy/ai3p.png
com/meldcraft/calculator/fancy/ai4.png
com/meldcraft/calculator/fancy/ai4p.png
com/meldcraft/calculator/fancy/ai5.png
com/meldcraft/calculator/fancy/ai5p.png
com/meldcraft/calculator/fancy/ai6.png
com/meldcraft/calculator/fancy/ai6p.png
com/meldcraft/calculator/fancy/ai7.png
com/meldcraft/calculator/fancy/ai7p.png
com/meldcraft/calculator/fancy/ai8.png
com/meldcraft/calculator/fancy/ai8p.png
com/meldcraft/calculator/fancy/ai9.png
com/meldcraft/calculator/fancy/ai9p.png
com/meldcraft/calculator/fancy/aiAdd.png
com/meldcraft/calculator/fancy/aiAdds.png
com/meldcraft/calculator/fancy/aiDiv.png
com/meldcraft/calculator/fancy/aiDivs.png
com/meldcraft/calculator/fancy/aiEqual.png
com/meldcraft/calculator/fancy/aiEqualp.png
com/meldcraft/calculator/fancy/aiMul.png
com/meldcraft/calculator/fancy/aiMuls.png
com/meldcraft/calculator/fancy/aiNPDot.png
com/meldcraft/calculator/fancy/aiNPDotp.png
com/meldcraft/calculator/fancy/aiSub.png
com/meldcraft/calculator/fancy/aiSubs.png
com/meldcraft/calculator/fancy/aic.png
com/meldcraft/calculator/fancy/aicp.png
com/meldcraft/calculator/fancy/aim+.png
com/meldcraft/calculator/fancy/aim+p.png
com/meldcraft/calculator/fancy/aim-.png
com/meldcraft/calculator/fancy/aim-p.png
com/meldcraft/calculator/fancy/aimc.png
com/meldcraft/calculator/fancy/aimcp.png
com/meldcraft/calculator/fancy/aimr.png
com/meldcraft/calculator/fancy/aimrp.png
com/meldcraft/calculator/fancy/CalculatorFancy.properties
baseResourceLayers = com.meldcraft.calculator.CalculatorApp

NumberField.elementProperties+= ,font=<element:LCDFont>, preferredSize=38,38,\
                                foreground=0,0,0,0.4

LCDFont.className = com.meldcraft.application.guis.SSSupport
LCDFont.type = font
LCDFont.resource = /com/meldcraft/calculator/fancy/LCD-N___.TTF
LCDFont.size = 20

buttonattrs = borderPainted=false, background=<null>, opaque=false,\
              hideActionText=true

KeyRep.{1}.elementProperties+= ,<propertyRef:buttonattrs>
OpRep.{1}.elementProperties+= ,<propertyRef:buttonattrs>
Equal.elementProperties+= ,<propertyRef:buttonattrs>
NPrep.NP{1}.elementProperties+=,<propertyRef:buttonattrs>

Compile and run the program.

java -cp Scaffold.jar;ScaffoldGUIS.jar;Calculator/classes \
com.meldcraft.application.AppManager \
com.meldcraft.calculator.fancy.CalculatorFancy

Calculator4 Screen Shot

Better! Much closer to the look of the F3 Calculator.

One downside to this GUI is that even though the buttons look round, they function as squares. E.g. pressing the mouse in the blank space activates the nearest button. We'll fix this, and the background image in the next step.

Step 5 - Polish and Shine

Lets extend the fancy calculator with another new properties file to finish the job (and add a background image, too).

Application Files
com/meldcraft/calculator/CalculatorApp.properties
com/meldcraft/calculator/Calculator.java
com/meldcraft/calculator/fancy/CalculatorFancy.properties
com/meldcraft/calculator/fancy/LCD-N___.TTF
com/meldcraft/calculator/fancy/ai0.png
com/meldcraft/calculator/fancy/ai0p.png
com/meldcraft/calculator/fancy/ai1.png
com/meldcraft/calculator/fancy/ai1p.png
com/meldcraft/calculator/fancy/ai2.png
com/meldcraft/calculator/fancy/ai2p.png
com/meldcraft/calculator/fancy/ai3.png
com/meldcraft/calculator/fancy/ai3p.png
com/meldcraft/calculator/fancy/ai4.png
com/meldcraft/calculator/fancy/ai4p.png
com/meldcraft/calculator/fancy/ai5.png
com/meldcraft/calculator/fancy/ai5p.png
com/meldcraft/calculator/fancy/ai6.png
com/meldcraft/calculator/fancy/ai6p.png
com/meldcraft/calculator/fancy/ai7.png
com/meldcraft/calculator/fancy/ai7p.png
com/meldcraft/calculator/fancy/ai8.png
com/meldcraft/calculator/fancy/ai8p.png
com/meldcraft/calculator/fancy/ai9.png
com/meldcraft/calculator/fancy/ai9p.png
com/meldcraft/calculator/fancy/aiAdd.png
com/meldcraft/calculator/fancy/aiAdds.png
com/meldcraft/calculator/fancy/aiDiv.png
com/meldcraft/calculator/fancy/aiDivs.png
com/meldcraft/calculator/fancy/aiEqual.png
com/meldcraft/calculator/fancy/aiEqualp.png
com/meldcraft/calculator/fancy/aiMul.png
com/meldcraft/calculator/fancy/aiMuls.png
com/meldcraft/calculator/fancy/aiNPDot.png
com/meldcraft/calculator/fancy/aiNPDotp.png
com/meldcraft/calculator/fancy/aiSub.png
com/meldcraft/calculator/fancy/aiSubs.png
com/meldcraft/calculator/fancy/aic.png
com/meldcraft/calculator/fancy/aicp.png
com/meldcraft/calculator/fancy/aim+.png
com/meldcraft/calculator/fancy/aim+p.png
com/meldcraft/calculator/fancy/aim-.png
com/meldcraft/calculator/fancy/aim-p.png
com/meldcraft/calculator/fancy/aimc.png
com/meldcraft/calculator/fancy/aimcp.png
com/meldcraft/calculator/fancy/aimr.png
com/meldcraft/calculator/fancy/aimrp.png
com/meldcraft/calculator/skinned/Calculator.png
com/meldcraft/calculator/skinned/CalculatorSkinned.properties
com/meldcraft/calculator/skinned/CalculatorSkinned.properties
baseResourceLayers = com.meldcraft.calculator.fancy.CalculatorFancy

Application.rootContent = LayerPanel

LayerPanel.className = com.meldcraft.application.guis.SSFactory
LayerPanel.type = panel
LayerPanel.elementLayers =  MouseMask, CalcPanel, CalcBackground


MouseMask.className = com.meldcraft.application.guis.SSFactory
MouseMask.type = mouseMask
MouseMask.maskedElement = CalcPanel

CalcBackground.className = com.meldcraft.application.guis.SSFactory
CalcBackground.type = label
CalcBackground.resourceImage = com/meldcraft/calculator/skinned/Calculator

CalcPanel.elementProperties = opaque=false,\
                        preferredSize=<element:CalcBackground>.preferredSize
CalcPanel.border = 3,0,0,0
NumberPanel.elementProperties = opaque=false

NumberField.elementProperties+= ,opaque=false
NumberField.border = 0,0,5,0

Compile and run the program.

java -cp Scaffold.jar;ScaffoldGUIS.jar;Calculator/classes \
com.meldcraft.application.AppManager \
com.meldcraft.calculator.skinned.CalculatorSkinned

Calculator5 Screen Shot

Voila! An artsy looking calculator written with plain old Swing components (only the MouseMask component is a specialized JComponent provided by the Scaffold framework).

And, not that anyone would want to, but we can still run each version of the calculator independently. The property layering feature of the Scaffold framework substantially facilitates branding and extending applications.

© 2007 Meldcraft Corporation