A Four Function Calculator, part two


Prerequisites

This magercise will walk you through connecting events to the Four-Function Calculator GUI you created in the AWT basics section.

Keep in mind that the only code you've actually typed had been the modifications that allow the CalcDisplay to be appended and the four compute functions that perform the actual integer adition, subtraction, multiplication and division. There will be a few lines of code to modify here, but less than 10 lines of raw Java code overall.

Work Location

Perform all work for this magercise in VisualAge project MageLang Magercises, package magercises.calc.

If this project does not appear in your Workspace add it from the repository (if it exists there) or create a new project using this name.

Tasks

Perform the following tasks:

  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.

  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.

  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.

    1. 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.

    2. Connect a button to the button trigger.
      1. First, bring up the pop-up menu for the button labeled "0".
      2. Choose "Connect->actionPerformed()". This starts a connection that is triggered when the "0" button is pressed.
      3. Move the cursor to the ButtonTrigger bean and click. Another pop-up menu appears.
      4. 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?
    3. 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.

      1. Bring up the pop-up menu for the connection line.
      2. Select "Connect->value".
      3. 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.
      4. 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?

    4. 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...)
    5. 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.

  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.)

    1. Bring up the pop-up menu for the ButtonTrigger.
    2. Select "button". This starts a new connection based on the event fired when the button property changes.
    3. Click on the CalcDisplay component. This brings up another pop-up menu.
    4. Select "All Features..." to bring up a list of anything you could possibly connect.
    5. Find append(java.lang.String) under the Methods list and select it.
    6. 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.

    1. Bring up the pop-up menu for ButtonTrigger.
    2. Select "Tear-Off Property...", which brings up a list of the properties of the ButtonTrigger.
    3. 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.

    1. Bring up the pop-up menu for the dotted, property-to-method connection we created between the ButtonTrigger and the CalcDisplay.
    2. Select "Connect->text", which starts a new connection.
    3. Click on the new, button bean that we had torn off from the ButtonTrigger, which will bring up yet another pop-up menu.
    4. 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.

    1. Bring up the pop-up menu of the ButtonTrigger object.
    2. Select "Connect->All Features".
    3. Click on "button" under the Event pane of the features dialog, then press "Ok".
    4. Click on the CalcDisplay component, bringing up (you guessed it) another pop-up menu.
    5. Select "All Features...", bringing up the window of everything that can be done with the CalcDisplay.
    6. 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:

    1. Bring up the pop-up menu for the new connection, and select "Properties".
    2. Press "Set parameters..."
    3. 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."
    4. Press "Set parameters..." again
    5. Change the value of value to false, and press "Ok".
    6. 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:

    1. Bring up the pop-up menu of the component in question, in this case the ButtonTrigger object.
    2. Select "Reorder connections from...".
    3. 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.
    4. Close the connection ordering browser.

  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:

    1. When you press a number button, the previous button's number gets appended.
    2. 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:

    1. append text to CalcDisplay
    2. set the torn-off button property to the pressed button
    3. 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

    1. Bring up the pop-up menu for ButtonTrigger and select "Open". This lets us edit the ButtonTrigger bean by itself.
    2. Click on the setButton method, and it's code appears in the display.
    3. 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.

    4. 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.

  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!

    1. Use the "Options->Add Bean..." menu item to place a new OperationTrigger class bean outside the Frame.
    2. 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.
    3. Connect the four operation buttons to the OperationTrigger:
      1. Bring up the "+" button's pop-up menu.
      2. Select "Connect->actionPerformed"
      3. Click on the OperationTrigger object.
      4. Select "operation"
      5. Bring up the pop-up menu for the new connection
      6. Section "Connect->value".
      7. Click on the "+" button.
      8. 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.

  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.

    1. 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.
    2. 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.
    3. Bring up the pop-up menu for zero.
    4. Select "Connect->this".
    5. Click on memory.
    6. Select "this."

  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:

    1. start: memory is 0, display is 0
    2. "-" pressed: memory is 0, display is 2, perform 0 + 2 -> memory = 2
    3. "*" pressed: memory is 2, display is 3, perform 2 - 3 -> memory = -1
    4. "=" 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.

    1. Bring up the pop-up menu for the "+" button.
    2. Select "Connect->this".
    3. Click on pendingOperation.
    4. 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.)

  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:

    1. Perform any pending operation, updating the memory value.
    2. Update the display with the results.
    3. Tell the display to start a new value.
    4. Set the pending operation to the button that was just pressed.

    Let's start by performing the pending operation. To do this:

    1. Bring up the pop-up menu for the OperationTrigger object.
    2. Select "Connect->All Features...".
    3. Select "operation" in the Events pane and press "Ok". We want to ensure that the change event is what we're connecting.
    4. Click on memory -- we want to update the memory value.
    5. 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.
    6. Select "Connect->value".
    7. Click on pendingOperation.
    8. Select "All Features..."
    9. Select "compute(int,int)" in the Methods pane and press "Ok".
    10. 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.
    11. Select "Connect->left".
    12. Click on memory.
    13. Select "All Features..."
    14. 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:

    1. Bring up the pop-up menu for the compute connection, the one that still needs a value for parameter "right".
    2. Select "Connect->right".
    3. Click on memory. This time we're just using it to easily access the parseInt method.
    4. Select "All Features..."
    5. Select "parseInt(java.lang.String)" in the Methods pane and press "Ok".
    6. Bring up the pop-up menu for the new connection.
    7. Select "Connect->s".
    8. Click on the CalcDisplay object.
    9. Select "text".

    So far so good (even though that last part was a bit evil.) Next, we need to update the displayed value:

    1. Bring up the pop-up menu for the OperationTrigger object.
    2. Select "Connection->All Features...".
    3. Select "operation" under the Event pane and press "Ok".
    4. Click on the CalcDisplay object.
    5. Select "text".
    6. Bring up the pop-up menu for the new connection.
    7. Select "Connect->value".
    8. Click on memory.
    9. Select "All Features..."
    10. 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:

    1. Bring up the pop-up menu for the OperationTrigger object.
    2. Select "Connection->All Features...".
    3. Select "operation" under the Event pane and press "Ok".
    4. Click on the CalcDisplay object.
    5. Select "All Features..."
    6. Select startNewValue in the Properties pane and press "Ok".
    7. Bring up the pop-up menu for the new connection.
    8. Select "Properties".
    9. Click on "Set Parameters".
    10. Change value to "true" and press "Ok".
    11. Press "Ok".

    One final task for the operation buttons -- we need to store the operation button that was pressed in pendingOperation:

    1. Bring up the pop-up menu for the OperationTrigger object.
    2. Select "Connection->All Features...".
    3. Select "operation" under the Event pane and press "Ok".
    4. Click on pendingOperation.
    5. Select "this".
    6. Bring up the pop-up menu for the new connection.
    7. Select "Connect->value".
    8. Click on the OperationTrigger object.
    9. Select "operation".

    Now test things out once more. Everything should work except the "=" and clear buttons.

  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.

    1. Bring up the pop-up menu for the "=" button.
    2. Select "Connect->actionPerformed".
    3. Click on the OperationTrigger object.
    4. Select "operation".
    5. Bring up the pop-up menu for the new connection.
    6. Select "Connect->value".
    7. Click on the "+" button. Pretend the "+" was pressed...
    8. Bring up the pop-up menu for the "=" button.
    9. Select "Connect->actionPerformed".
    10. Click on memory.
    11. Select "this
    12. Bring up the pop-up menu for the new connection.
    13. Select "Connect->value".
    14. Click on zero.
    15. Select "this".

  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:

    1. Bring up the pop-up menu for the clear button.
    2. Select "Connect->actionPerformed()".
    3. Click on the CalcDisplay.
    4. Select "text".
    5. Bring up the pop-up menu for the new connection.
    6. Select "Connect->value".
    7. Click on the "0" button.
    8. Select "label".

    So far, this sets the display to "0". Next:

    1. Bring up the pop-up menu for the clear button.
    2. Select "Connect->actionPerformed()".
    3. Click on the CalcDisplay.
    4. Select "All Features...".
    5. Select startNewValue under properties and press "Ok".
    6. Bring up the pop-up menu for the new connection.
    7. Select "Properties"
    8. Press "Set Parameters..."
    9. Change the value parameter to true and press "Ok".
    10. Press "Ok"

    Now we have told the CalcDisplay to start a new value. Next:

    1. Bring up the pop-up menu for the clear button.
    2. Select "Connect->actionPerformed".
    3. Click on memory.
    4. Select "this".
    5. Bring up the pop-up menu for the new connection.
    6. Select "Connect->value".
    7. Click on zero.
    8. Select "this".

    Almost done! Finally:

    1. Bring up the pop-up menu for the clear button.
    2. Select "Connect->actionPerformed".
    3. Click on pendingOperation.
    4. Select "this".
    5. Bring up the pop-up menu for the new connection.
    6. Select "Connect->value".
    7. Click on the "+" button.
    8. Select "this".

This has been a very long exercise, but hopefully it has given you some ideas of the power of the VisualAge Visual Composition Editor. Many applications can be designed and programmed using very little raw Java code if you are careful how you set up your connections. And once you are used to the way in which the connections are represented, the VCE can be a powerful tool to help see the design intentions that might otherwise get lost during program maintenance. The task numbers above are linked to the step-by-step help page. Also available is a complete solution to the problem, and expected behavior, to demonstrate it.

Copyright © 1996-1997 MageLang Institute. All Rights Reserved.