|
Comp201: Principles of Object-Oriented Programming I
Spring 2008 -- Shape Calculator, part
4
|
Adding a Dynamically Generated Dialog
Box to Set a Shape's Properties
Before starting this section, copy all
the code from part 3 into your part4
subdirectory and make any modifcations to the code in part4.
Do NOT modify your part3
code!
We've gotten a lot to work so far. One last thing to do...
Our system is currently capable of dynamically loading a factory class and
using it to create an arbitrary shape. There's a catch however. The parameters
necessary to specify how to make any given shape varies from shape to shape.
For example, to create a square requires a single value for the length of its
side, but to create a triangle requires two values, one for the length of its
base and the other for the length of its height. The null shape requires no
initialization parameters, but a circle requires a Color
object, which is not a numeric value at all!
One of the most important precepts in object-oriented programming is "delegate
to the object that knows best". So who knows what parameters a square
needs? There's only one choice here, as a square shape is defined by the factory
that made it. The SquareFactory
must be the one that determines what parameters are needed to instantiate a
square shape. Likewise, the CircleFactory
is the only object that knows what parameters are needed to construct a circle
shape.
The number and type of parameters is clearly a variant here, but then what
is the invariant?
As we've mentioned before, sometimes the obvious is the most difficult thing
to see: the invariant is that we always ask for the parameters to construct
the shape.
Thus what we need is a mechanism that enables us to ask for the construction
parameters but without know what those parameters are (the invariant process)
plus a way to get the values for the necessary specific, concrete parameters
but without knowing how to ask for them (the variant process).
Dialog Boxes
All modern windowed GUIs have a special kind of window called a dialog box
that pops up with some sort of question that we must answer before continuing,
e.g. "Do you agree to the following license agreement that indentures your
first born child? Yes? or No?". These "modal" windows are unlike
regular windows (frames) that come up and have a "life" of their own,
instead they are tied to a parent window and prevent ("block") the
parent from further processing until modal window has been closed.
Dialog boxes are all about asking questions of the user, so they suit our needs
quite well here. In the end,what we need is a dialog box which has a panel supplied
by the shape factory that has al the necessary labels, buttons and textfields
needed to ask the user what parameter values are desired to make the shape instance.
We will take this step by step again, but with less detailed directions now
since you are more versed at building GUIs! Reference your ShapeCalc
code while writing this code. Don't forget to import any required packages!
- Create a class shapecalc.AShapePropertyDlg
that extends JDialog.
- You wll need some import statements.
- Do not mark the class as abstract
just yet. We will make it abstract in a little bit, after we are sure that
everything is working up to that point.
- Create a constructor for AShapePropertyDlg
that takes two parameters, a JFrame
owner and a JPanel property
panel.
- When we are done, the owner will be ShapeCalc
and the property panel will be generated by the shape factory.
- In the constructor, call the superclass constructor with the following
3 parameters, in this exact order: owner, title String
(e.g. "Shape Properties") and the boolean
value true, signifying that
the dialog should be modal.
- Add whatever import statements are necessary.
- Test your code using DrJava's interaction pane.
- First import javax.swing.*
- Then instantiate an AShapePropertyDlg
object, handing 2 null
values to its constructor.
- Set the dialog's visibility (setVisible)
to true.
- You should see the dialog box pop up and the interactions pane should
not accept any input until the dialog is closed.
- Now using local variables, create 2 JButtons,
one that says "Ok" and one that says "Cancel".
- Likewise, create a button panel (JPanel)
to hold the two above buttons.
- For kicks, let's try a different layout other than the default FlowLayout
in the button panel. Let's use a BoxLayout,
which lays out components from the left to the right, or from the top to the
bottom, all non-centered.
- Set the layout of the button panel to a new BoxLayout.
- The constructor for a BoxLayout
takes 2 parameters,
- the first is the component it is laying out, here, the button panel.
- the second is the direction of the layout, here we'll use the static
field value: BoxLayout.X_AXIS
- Add the two buttons to the button panel. No additional parameters are
needed.
- Add the button panel to the SOUTH
edge of the dialog's content pane(defaults to BorderLayout
as all frames do).
- Add the properties panel to the CENTER
of the content pane.
- Using DrJava's interaction pane again, test your dialog box. Be sure to
give the constructor a new JPanel
as its second input parameter since it will be added to the content pane.
- Your dialog frame wll probably pop up too small, so add the call,
pack(), at the end of the constructor to force the layout manager
to set the sizes of the frame to its minimal size that still shows all
the components.
- Your dialog also probably doesn't pop up in the middle of the screen
either. Use the same method call as in ShapeCalc
to center the dialog on the screen.
- Write the following two methods with stub code that prints a message to
the console as their bodies:
- protected void okBtnClick(ActionEvent
e)
- protected void cancelBtnClick(ActionEvent
e)
- Back in the constructor, add ActionListeners
to the Ok and Cancel buttons using anonymous inner classes. The actionPerformed
methods of these listeners should call their respective "click"
methods above.
- Test your dialog box again to make sure that your buttons work properly.
When you click a button, you should see the message from the respective click
method appear on the console.
- Now that you've verified that your dialog box is fully operational, we can
make it abstract, as the actual click methods will perform duties specific
to our ShapeCalc system.
- Make the entire class abstract
- Make both click methods (in step 11 above) abstract and remove their
stub-coded method bodies completely.
Now all we have to do is to subclass AShapePropertyDlg
and override the two click methods with the specific behavior we need. If we
instantiate the dialog with a panel filed with all the necessary buttons, textfields,
etc needed to construct a shape, then we will have everything we need!
Notice how the development of this abstract class started at with very concrete
behavior that we could easily understand and test. Then we abstracted that behavior
to get the final expression of our problem. This is a classic technique to develop
highly abstract systems. And as usual, the driving motivation through this process
is the separation of the variant from the invariant. Here, AShapePropertyDlg
represents the invariant of asking the use for property values, without the
variant collection of the actual properties needed (the property panel) or the
variant process of what to do with those properties (the concrete click methods
implemented in any subclasses).
Factories Making Panels
Since the shape factories are going to be creating property panels to input
the varous parameters needd to construct their shapes, we need to add another
method to the IShapeFactory interface:
JPanel getPropertyPanel().
For the moment, simply implement that method in each of the factories by having
them return a private static
JPanel field that was initialized
to a plain JPanel instance. Why
is it ok to use a static field here?
Bringing Up a Dialog Box
Now we need to get ShapeCalc
to bring up the dialog box. The properties dialog needs to pop up after the
factory is instantiated but before the resultant shape is used. Find this point
in your code and insert the following code there. You will find that the new
code will literally engulf the code where the factory creates the new shape
instance.
- Use an anonymous inner class, subclass and instantiate a new instance of
AShapePropertyDlg, overriding
the two click methods (we'll fill in their method bodies in a minute).
- We've never made an anonymous inner class of a superclass whose constructor
required input parameters. But the syntax is very intuitive. In this case,
the constructor for AShapePropertyDlg
takes two parameters, so we simply say:
new AShapePropertyDlg(param1, param2) { .... }
- The first constructor parameter is the host frame, in this case, the
ShapeCalc object itself.
But since we are currently inside of the anonymous inner class for the
newBtn's ActionListener,
we can't say "this"
because it would refer to the wrong object. Instead, we must say "ShapeCalc.this"
to reference the entire ShapeCalc
object.
- The second constructor parameter is the property panel that the shape
factory is supposed to make, so simply use the result of calling the shape
factory's getPropertyPanel
method.
- The instance of the dialog box doesn't need to be assigned to a variable.
It simply needs to be set visible. Since it is modal, the execution of
the code will not proceed until the dialog closes. So, without assigning
to any sort of variable, instantiate the dialog box and set its visibility
to true.
- Now for the bodies of the click methods
- The okBtnClick method
runs if the user clicks the Ok button in the dialog. The user thus is
saying "Yes, please make this shape!" In this situation we need
to
- Tell the factory to make the shape. We had this code already, now
we know where to put it.
- Get rid of the dialog box. A dialog box, as any frame, has a dispose()
method that will close the window and destroy the object.
- The cancelBtnClick method
runs if the user clicks the Cancel button in the dialog. The user is thus
saying "No, please don't do anything!". In this siutation we
simply need to close the dialog down using the dispose()
method.
- Test your program. You should be getting some nice dialogs coming up now
with blank panels. Be sure to verify that the Ok and Cancel buttons work properly.
Progress, progress!
Now all we need to do is to get the respective shape factories to produce useful
property panels and we're all done!
Property Panels for Squares
The property panel for a square would contain simply a texfield into which
one could input the length of the side and a label describing the textfield.
- The text field needs to be a field of the factory so that it is usable by
the makeShape method. It can
be static--why?
- In the constructor of SquareFactory, add the label and the text field to
the property panel.
- A BoxLayout might be
nice here, but with a BoxLayout.Y_AXIS
orientation.
- All we need to do now is to initialize the field in the IShape
anonymous inner class that represents the length of the side of the square.
- Unfortunately, the field is a double
and the text field where the user input the desired value holds a String.
- All the wrapper classes for the Java primitive types have static methods
that can "parse" a String
and convert it to the associated primitive type. For instance, in this
case we can use thefollowing static method: public
static double Double.parseDouble(String s)
- Use the above method to initialize the square's field to the value in
the text field.
- One problem is that the user may have typed in an invalid number, such as
"3.g7" or perhaps nothing at all. If parseDouble
tries to parse such an invalid input, it will throw an exception and stop
the program.
- We need to "catch" the exception should it occur.
- Use the following try-catch syntax to replace the entire method body
of makeShape:
try {
[put entire original "return ....;" statement here]
}
catch(Exception parseError) {
[set the text field's text to the string "0.0"]
[call makeShape() again]
}
- This crude exception handling will simply replace any invalid input
with "0.0". But it does keep the program from crashing!
If you look simply at the code in ShapeCalc,
it does not look like the newly constructed shape is at all connected to the
property panel used to configure it. They are created and used in different
sections of the code after all. But when you look at the SquareFactory,
we see that the property panel and the new IShape
instance are indeed connected: the text field of the property page is in
the closure of the the shape object. Hence the shape object can reference
the values of the property panel!
Closures give us a tremendously powerful communications pathway that leaves
our systems beautifully decoupled!
Property Panels for Circles
The property panel for the CircleFactory
is very similar to that for the SquareFactory,
but it does need to use another label and a button.
The functionality that you want to achieve for the property panel includes
the following:
- The button to select the color should be the currently selected color.
- The currently selected color should persist between usages. That is, for
instance, if red was selected and then any number of non-circle shapes made,
then the next time CircleFactory
is used, then red should be the default color.
- Canceling the creation of a new shape should not affect the color
of the current shape, even if it is a circle.
- As with SquareFactory, an
invalid radius input value will cause a shape of radius=0 to be made.
Here's some hints and tools to use:
- Base your CircleFactory code
on your SquareFactory's code.
Just extend it to meet your new requirements.
- A JButton object has a method
called setBackground that takes
a Color object as an input
and sets the background color of the button.
- The JColorChooser class has
a static method called showDialog
which will bring up the standard modal dialog box for choosing colors.
- The signature for showDialog
is
public static Color showDialog(Component
parent, String title, Color initialColor)
- The return value of showDialog
is the color that was selected by the user. (Note: null
is returned if the user cancels, but null
usually appears as black).
- The parent Component
is the property panel.
- The title is the title at the top of the dialog box.
- The initialColor is the color that should be selected initially. See
functionality requirement #2 above.
- You may need to set the color button's background color in more than one
place in your code.
Depending on how you implemented things, you may observe some odd behavior
when you run the program. Try the following sequence of actions:
- Create a new circle shape using the CircleFactory.
Select any color and size you want. The shape is made correctly.
- Start to create another circle shape using CircleFactory
again. Select a new color but don't make the shape yet.
- Notice that even though the new circle shape has not been made, the color
of the old circle shape has changed to the newly selected color.
- Even if you cancel the construction of the new shape, the old shape retains
the new color.
Why is this happening? Try to explain the phenomenon in terms of closures.
Fix the problem if it occurs in your program so that you can achieve functionality
#3 above.
More stuff to do...
- Make more kinds of shapes!
- Make shapes with more kinds of construction parameters
- Let your imagination run wild--have fun!
Last Revised
Thursday, 03-Jun-2010 09:50:28 CDT
©2008 Stephen Wong and Dung Nguyen