 




|
|
Help: A Four Function Calculator, part two
Help is available for each task, or you can go straight to
the solution source code.
Task 1
Version the calculator example
I know it may seem tedious, but just in case something goes wrong,
it's always best to save at working checkpoints. To version your work so far,
go to the workbench, select the Calculator project and bring up its pop-up
menu. Select "Version...", make sure the "Automatic" setting is selected, and press
"Finish." That's it! If anything goes wrong, you can easily recover back to
this checkpoint by selecting "Replace with->previous version" from the
project's pop-up menu.
Task 2
Return to the Visual Composition Editor
From the workbench, select the Calculator class, and choose
"Open to->Visual Composition" from its pop-up menu. We now resume
the Visual creation of our four-function calculator.
Task 3
Connect the number buttons to the button trigger
Remember that numeric trigger we created? What we want to do is have any
numeric button that is pressed set the button property of the trigger. This, in
turn, will generate a PropertyChangeEvent that we can use to perform the real action
we want.
- Add a ButtonTrigger to the application.
You do this by using the
"Options->Add Bean..." menu item, specifying a class bean of class calc.ButtonTrigger.
Because this is a non-visual bean, you need to add it outside of the visual area
of the GUI. Move the cursor outside the frame and click the left button.
- Connect a button to the button trigger.
- First, bring up the pop-up menu for the button labeled "0".
- Choose "Connect->actionPerformed()". This starts a connection that is triggered
when the "0" button is pressed.
- Move the cursor to the ButtonTrigger bean and click. Another pop-up menu appears.
- Select "button." You now have a connnect that says "when the '0' button is pressed, set the button
property of the ButtonTrigger." But what does it get set to?
- Set up the parameter for the connection.
The connection between the "0" button and the ButtonTrigger is represented
by a dotted line. That means there is some information missing.
- Bring up the pop-up menu for the connection line.
- Select "Connect->value".
- This starts a new connection that will be used to set the
parameter value for the connection -- in this case, it's the
value that will be assigned to the button property of the
ButtonTrigger.
- We want the value to be set to the button that was pressed. So
move the cursor to the "0" button and click. Once again, a
pop-up menu. Select "this."
So what does this connection mean? "When the '0' button is pressed, set
the button property of the Button Trigger to the '0' button." Sounds
simple now, eh?
- Repeat the same type of connection for buttons 1 through 9. (Connect the
value property of the connection to the button that was pressed, not
always the "0" button...)
- If the number of connections on the display is getting hard to deal with,
you can hide all the connections with the "hide connections" button on the
tool bar. You can redisplay connections associated with any component
by clicking that component and clicking the "show connections" button.
If you click on the background of the VCE (outside all components) the
"show connections" button will show all connections.
You can also move the ButtonTrigger around to change the angles of the
connection lines to make access easier.
And finally, you can bend the connection lines! Select a connection line
by clicking on it. Notice the handle square that appears at the middle
of the connection. You can drag that handle to bend the connection.
When you bend a connection, each segment forming the "bend" angle gets
its own drag handle, which can be used to further bend the connection
and so on. If you don't like the way the bend turns out, you can
select "Restore shape" from the connection's pop-up menu.
Task 4
Connect the ButtonTrigger to the CalcDisplay
So what happens when a number button is pressed? First, we want to append the
number to the CalcDisplay. Then, we want to tell the CalcDisplay that the next
number pressed will definitely be appended, and not start a new value. (Recall that
we had added a startNewValue property to CalcDisplay, and if it's set to True,
the next number pressed will clear the currently displayed value.)
- Bring up the pop-up menu for the ButtonTrigger.
- Select "button". This starts a new connection based on
the event fired when the button property changes.
- Click on the CalcDisplay component. This brings up another pop-up
menu.
- Select "All Features..." to bring up a list of anything you could
possibly connect.
- Find append(java.lang.String) under the Methods list and
select it.
- Press "Ok".
Now we want the string to be appended to be the label that was on
the button that was pressed. We can only access "button" from the
ButtonTrigger, not its label. So we need to "tear off" a copy of the
button property from the ButtonTrigger.
- Bring up the pop-up menu for ButtonTrigger.
- Select "Tear-Off Property...", which brings up a list of the properties
of the ButtonTrigger.
- Select button and press "Ok".
This creates a new bean to represent the button. There is a property-to-property
connection between the new button bean and the button property of the
ButtonTrigger. This basically means that the two values are kept in synch
no matter what happens. Better still, the new button bean gives us access
to the label of the button that was pressed!
Now we connect that pesky append parameter.
- Bring up the pop-up menu for the dotted, property-to-method connection
we created between the ButtonTrigger and the CalcDisplay.
- Select "Connect->text", which starts a new connection.
- Click on the new, button bean that we had torn off from the ButtonTrigger,
which will bring up yet another pop-up menu.
- Select "label".
We now have a connection that says "when the button property of the
ButtonTrigger is set, call the append method of the CalcDisplay, passing
it the label of the button property of the ButtonTrigger." (Whew!)
But, we're not done yet. We still need to tell the display that the next
appended text should definitely append, and not start a new value. This
connection is a bit tricky. We cannot just say "connect the button
property of the ButtonTrigger to the startNewValue property of the CalcDisplay.
That would mean that we are trying to keep the values in sync. We need
to explicitly say that we are interested in the event
associated with the button property change.
- Bring up the pop-up menu of the ButtonTrigger object.
- Select "Connect->All Features".
- Click on "button" under the Event pane of the features
dialog, then press "Ok".
- Click on the CalcDisplay component, bringing up (you guessed it) another
pop-up menu.
- Select "All Features...", bringing up the window of everything that
can be done with the CalcDisplay.
- Select "startNewValue" in the Property pane, and press "Ok."
Now we have a connection that says "when the button property changes, set the
startNewValue property of CalcDisplay." We always want the value of the
startNewValue to be set false in this scenario. So:
- Bring up the pop-up menu for the new connection, and select
"Properties".
- Press "Set parameters..."
- Due to an apparent bug in VisualAge, we have to play with this a bit.
The initial value of the value parameter is false.
That is the value we want, but unfortnately the connection is still
listed as unfinished. So, set the value to true
and press "Ok."
- Press "Set parameters..." again
- Change the value of value to false, and press "Ok".
- Press "Ok" one more time to finish the property change.
At this point, you may be thinking "I now have two connections based on the
changed value of ButtonTrigger's button property. In what order are they
executed?"
The connections are executed in the order in which they are added. If you
want to see the order, or, if the order is incorrect and you want to change
it:
- Bring up the pop-up menu of the component in question, in this case
the ButtonTrigger object.
- Select "Reorder connections from...".
- The list of connections and their order is displayed. You can
drag the connections up and down to reorder them, but don't do that
now, as they should be correct.
- Close the connection ordering browser.
Task 5
Give it a try!
Press the "Test Applet" button on the toolbar and see how the number buttons work.
Confused? Why don't things work quite right? There appears to be two problems:
- When you press a number button, the previous button's number gets
appended.
- If you press the same number button repeatedly, only one of that number
gets appended.
So what's the problem?
Remember when we looked at the ordering of the connections from the ButtonTrigger?
The connections based on button changing were ordered like:
- append text to CalcDisplay
- set the torn-off button property to the pressed button
- set startNewValue in CalcDisplay to false.
Aha! Remember that the append call looks at the current value of
that torn-off button? Well it hasn't been updated yet. So bring up that
"Reorder Connections From..." dialog and drag the "set torn-off button" connection
up above the "append text" connection. That will make sure that when you press a
button, that button's label is the one that gets appended.
But what about the other problem? As it turns out, PropertyChangeEvents are
only fired when the value of the property really changes, but just any time
the set method is called for that property. So how can we fix this?
We could set the value of button to null, but that would fire a PropertyChangeEvent
which would try to look at the "null" button's label. Not good...
A simple solution is to create a dummy button that has a label of "". As the
last connection performed when the button changes, set button to this dummy button.
It will cause one more round of appending, but other than wasted resources it won't
have any visible effect. Still not too good.
The proper way to handle this would be to modify the setButton method to make
sure it looks like the property changes any time setButton is called.
So, we
- Bring up the pop-up menu for ButtonTrigger and select "Open". This
lets us edit the ButtonTrigger bean by itself.
- Click on the setButton method, and it's code appears in the display.
- Looks like the easiest way to ensure the property looks like it
has really changed is to set oldValue to null instead of
using the real old value. So change the line
java.awt.Button oldValue = fieldButton;
to be
java.awt.Button oldValue = null;
and save the method.
- Close the class browser for ButtonTrigger, and the changes are done.
Test the bean again and see that the buttons append appropriately.
The advantage to the trigger technique may be apparent now -- we would have needed
two connections from each number button to the CalcDisplay to append and set its startNewValue
flag. Using the trigger technique, we cut that down to one connection from each
number button to the ButtonTrigger.
Task 6
Handle the Operation Buttons
The technique used here is similar to the way we handled the buttons.
First, let's get all the other connections out of the way. Click somewhere outside the
Frame to make sure no component is selected. Then press the "hide connections"
button on the toolbar. Isn't that better?
Let's start with the "+" button. Remember the OperationTrigger? Let's use it!
- Use the "Options->Add Bean..." menu item to place a new OperationTrigger class
bean outside the Frame.
- Thinking ahead, we realize that we'll have the same PropertyChangeEvent
problem as before. So, open the OperationTrigger bean, edit the
setOperation method, and set oldValue to null there as well.
- Connect the four operation buttons to the OperationTrigger:
- Bring up the "+" button's pop-up menu.
- Select "Connect->actionPerformed"
- Click on the OperationTrigger object.
- Select "operation"
- Bring up the pop-up menu for the new connection
- Section "Connect->value".
- Click on the "+" button.
- Select "this".
We have set up a connect that now says "when the '+' button is pressed,
set the operation property of the OperationTrigger object to the
button that was pressed."
Repeat the above steps for the other three operation buttons.
We now have the operation buttons set up to trigger the OperationTrigger.
Task 7
Set up a temporary, intermediate memory
Each calculation involves two values. One is the value stored in the CalcDisplay
object. The other is a working memory. We need to create that working memory.
- Use the "Options->Add Bean..." menu item to create a new java.lang.Integer
variable bean. Give it the name "memory", and add it outside of the Frame.
- We need to set an initial value. Create a new class bean of type
java.lang.Integer, name it zero, and add it outside the Frame.
- Bring up the pop-up menu for zero.
- Select "Connect->this".
- Click on memory.
- Select "this."
Task 8
What do the operation buttons really do?
Think about what happens when you press the "+" key on a calculator. It doesn't
immediately perform an add operation. You're actually in the middle of an add operation!
When an operation button is pressed, it finalizes the previous operation. So
suppose you had peformed the following button presses:
2 - 3 * 4 =
When you started, the display was initially "0". So is the internal memory of the
calculator. The first "-" finalizes the previous operation. But there wasn't a previous
operation! So how to deal with it? Rather than create a nasty condition test and
more code, assume that the first operation was a "+" and the internal memory is "0".
Adding 0 to any number doesn't change the number, and keeps our algorithm simple.
So, in the above example, the following events occur:
- start: memory is 0, display is 0
- "-" pressed: memory is 0, display is 2, perform 0 + 2 -> memory = 2
- "*" pressed: memory is 2, display is 3, perform 2 - 3 -> memory = -1
- "=" pressed: memory is -1, display is 4, perform -1 * 4 -> -4
Obvoiusly we need some way of keeping track of the previous operation.
So, use the "Options->Add Bean..." menu item to add a calc.OperationButton variable
outside of the Frame. (Note that this is a variable, as we don't want a separate
object (type = class), we want a reference (type = variable).) Name this variable
pendingOperation.
We need to set the initial value for pendingOperation.
- Bring up the pop-up menu for the "+" button.
- Select "Connect->this".
- Click on pendingOperation.
- Select "this".
Under these circumstances, pendingOperation will be initialized to the "+"
button instance, and will be updated whenever the pus button instance changes.
(Because it never changes, we end up with just an initialization.)
Task 9
Handle the OperationTrigger Events
When an operation button is pressed, so far, it will set the operation
property of the OperationTrigger object. Obviously we need to do more. We need to
set up the operation property change event to:
- Perform any pending operation, updating the memory value.
- Update the display with the results.
- Tell the display to start a new value.
- Set the pending operation to the button that was just pressed.
Let's start by performing the pending operation. To do this:
- Bring up the pop-up menu for the OperationTrigger object.
- Select "Connect->All Features...".
- Select "operation" in the Events pane and press "Ok". We want to ensure that the
change event is what we're connecting.
- Click on memory -- we want to update the memory value.
- Bring up properties for the new connection. We need to set the value
that the memory will be updated with. That will be the result of the
computation.
- Select "Connect->value".
- Click on pendingOperation.
- Select "All Features..."
- Select "compute(int,int)" in the Methods pane and press "Ok".
- Now we need to fill in the parameters to the compute method. Bring up the
pop-up menu for the new connection that's calling compute.
- Select "Connect->left".
- Click on memory.
- Select "All Features..."
- Select "intValue()" in the Methods pane and press "Ok". This returns an
int version of the memory Integer's value.
Time to think for a minute. The right operand in the expression should be the
contents of the CalcDisplay. But we need an int for the value, and the CalcDisplay
contains a String.
To convert, we need to use the Integer class' parseInt method. So:
- Bring up the pop-up menu for the compute connection, the one that still needs
a value for parameter "right".
- Select "Connect->right".
- Click on memory. This time we're just using it to easily access the parseInt
method.
- Select "All Features..."
- Select "parseInt(java.lang.String)" in the Methods pane and press "Ok".
- Bring up the pop-up menu for the new connection.
- Select "Connect->s".
- Click on the CalcDisplay object.
- Select "text".
So far so good (even though that last part was a bit evil.) Next, we need to update
the displayed value:
- Bring up the pop-up menu for the OperationTrigger object.
- Select "Connection->All Features...".
- Select "operation" under the Event pane and press "Ok".
- Click on the CalcDisplay object.
- Select "text".
- Bring up the pop-up menu for the new connection.
- Select "Connect->value".
- Click on memory.
- Select "All Features..."
- Select "toString()" in the Methods pane and press "Ok".
Now the display should get updated. Next, we need to tell the display that the
next number key press starts a new value:
- Bring up the pop-up menu for the OperationTrigger object.
- Select "Connection->All Features...".
- Select "operation" under the Event pane and press "Ok".
- Click on the CalcDisplay object.
- Select "All Features..."
- Select startNewValue in the Properties pane and press "Ok".
- Bring up the pop-up menu for the new connection.
- Select "Properties".
- Click on "Set Parameters".
- Change value to "true" and press "Ok".
- Press "Ok".
One final task for the operation buttons -- we need to store the operation
button that was pressed in pendingOperation:
- Bring up the pop-up menu for the OperationTrigger object.
- Select "Connection->All Features...".
- Select "operation" under the Event pane and press "Ok".
- Click on pendingOperation.
- Select "this".
- Bring up the pop-up menu for the new connection.
- Select "Connect->value".
- Click on the OperationTrigger object.
- Select "operation".
Now test things out once more. Everything should work except the "=" and clear buttons.
Task 10
Handling the "=" button.
What should the "=" button do? Seems like it should behave pretty much like
any other operation button except it should set the next operation to "+" and
clear the internal memory. So perhaps the trick here is to make it look like the plus
button was actually pressed, then clear the internal memory.
- Bring up the pop-up menu for the "=" button.
- Select "Connect->actionPerformed".
- Click on the OperationTrigger object.
- Select "operation".
- Bring up the pop-up menu for the new connection.
- Select "Connect->value".
- Click on the "+" button. Pretend the "+" was pressed...
- Bring up the pop-up menu for the "=" button.
- Select "Connect->actionPerformed".
- Click on memory.
- Select "this
- Bring up the pop-up menu for the new connection.
- Select "Connect->value".
- Click on zero.
- Select "this".
Task 11
Handle the Clear Button
What should the clear button do? It should:
- Set the CalcDisplay's text to "0"
- Tell the CalcDisplay that the next keypress should start a new value.
Note that this is not really necessary, because a "0" value in the
CalcDisplay will disappear when the next number button is pressed.
However, being the good OO students that we are, we don't want to
rely on internal implementation details. So we'll explicitly set
it to make our intent clear.
- Clear any intermediate results stored.
- Reset the pendingOperation to the add button.
So:
- Bring up the pop-up menu for the clear button.
- Select "Connect->actionPerformed()".
- Click on the CalcDisplay.
- Select "text".
- Bring up the pop-up menu for the new connection.
- Select "Connect->value".
- Click on the "0" button.
- Select "label".
So far, this sets the display to "0". Next:
- Bring up the pop-up menu for the clear button.
- Select "Connect->actionPerformed()".
- Click on the CalcDisplay.
- Select "All Features...".
- Select startNewValue under properties and press "Ok".
- Bring up the pop-up menu for the new connection.
- Select "Properties"
- Press "Set Parameters..."
- Change the value parameter to true and press "Ok".
- Press "Ok"
Now we have told the CalcDisplay to start a new value. Next:
- Bring up the pop-up menu for the clear button.
- Select "Connect->actionPerformed".
- Click on memory.
- Select "this".
- Bring up the pop-up menu for the new connection.
- Select "Connect->value".
- Click on zero.
- Select "this".
Almost done! Finally:
- Bring up the pop-up menu for the clear button.
- Select "Connect->actionPerformed".
- Click on pendingOperation.
- Select "this".
- Bring up the pop-up menu for the new connection.
- Select "Connect->value".
- Click on the "+" button.
- Select "this".
|