|
Comp201: Principles of Object-Oriented Programming I
Spring 2008 -- Shape Calculator, part
3
|
Painting a Shape onto a Panel and Instantiating
an Arbitrary Shape
Before starting this section, copy all
the code from part 2 into your part3
subdirectory and make any modifcations to the code in part3.
Do NOT modify your part2
code!
So far so good. We've got a GUI application and we've been able to successfully
use a factory to instantiate a shape. Now can we get that shape to paint itself
onto the screen?
Painting a Shape
As defined by IShape, all shapes
have the ability to paint themselves onto a Graphics
object. Well, what does that mean anyway? Let's discuss painting in Java for
a bit:
Whenever a graphical component in Java wishes to display itself on the screen
it must "paint" itself onto the screen. There are lots of issues and
technicalities surrounding the proper way to paint an image onto the screen--consult
any of a number of volumunous graphics textbooks on the subject. In Java, however,
there are few guiding and thankfully, simplifying viewpoints taken:
- Painting is a service provided to the the systems (i.e. the Java virtual
machine) so that when the system so desires, an object can display
itself onto the screen. An object does NOT determine for itself when it should
paint itself onto the screen.
- An object can request that the screen be repainted, but cannot determine
if or when that request is fulfilled. All graphical components (subclasses
of java.awt.Component) have
a repaint() method, which is
a request to the JVM to paint that component whenever the JVM can get around
to it.
- Any object does not either know about the conditions in the rest of the
system nor does it care about where on the screen it is located. When asked
to paint by the JVM, an object is handed a java.awt.Graphics
object, which provides an environment just for that particular painting
process. The Graphics object
provides a localized coordinate system to be used, releasing the object being
painted from knowing the specifics of its graphical environment. Graphics
provides information useful for the painting process, such as the current
pen color and provides services such as drawing a straight line, box, filled
circle, etc. The Graphics
object is NOT instantiated anywhere in the code under development--it is created
by the JVM expressly to paint a particular component.
In order to get our shape to paint itself onto the screen there are a number
of steps to go through.
- Make the shape that the factory creates accessible from anywhere inside
of ShareCalc.
- This is easily enough accomplished by using a private field of type
IShape.
- The call to the shape factory's makeShape()
method simply sets this variable's value.
- You will need to modify your code that displays the shape's area accordingly.
- The shape is to paint itself onto the displayPnl,
so the painting service provided by that panel must be overriden.
- We must therefore subclass JPanel
to make our custom painting panel. But since we only need one instance
of this special panel, we should use...you guessed it, an anonymous inner
class!
- Change the initialization of displayPnl
to be an anonymous inner class derived from JPanel,
where the following painting service method is overriden: public
void paintComponent(Graphics g)
- In the body of paintComponent
- First make a call to the superclass's paintComponent
method, i.e. super.paintComponent(g);.
This ensures that whatever is normally done in the process of painting,
e.g. clearing the area to be painted, gets done.
- Then call the shape's paint
method, handing it the Graphics
object upon which it will paint. The shape wil thus paint onto the
panel after the panel is done doing its normal painting processes.
- You can hand the paint's x
and y inputs any
value you'd like, but if you want the shape to paint in the middle
of the panel, use the getWidth()
and getHeight()
methods of a Component
(such as the displayPnl)
to pass the coordinates of the center of the panel, with
respect to its upper left corner, which coincides with the origin
of the Graphics
object.
- At this point, try running your program.
- The first thing you should get is a null pointer exception because your
shape variable hasn't been initialized but yet the system is trying to
paint it on the screen.
- In designing an object-oriented system, we give individual objects
behaviors such that the correct overall system behavior is a natural
outgrowth of the interactions between those objects. The problem with
the null value is that
it not an object, and in such, has no well defined behaviors or intelligence
of its own. Rather than using a conditional statement to attempt to
catch and eal with any possible situation that might encounter a null
value, as is done in procedural programming, in our OO system we want
to define a "null object" that has the proper behaviors
of and represents the null value. This is a description of the Null
Object Design Pattern. In mathematics, "zero" and the
"empty set" are examples of null objects.
- You need a "null shape"--an object that is a shape but
that represents no shape. Such an object should have zero area and
not paint anything onto the screen.
- Since all shapes are made by factories, we thus need a null shape
factory.
- Using your SquareFactory
as a template, write a singleton NullFactory
that returns a shape whose area is always 0.0
and whose paint method
does nothing.
- Initialize your shape field with the result of calling makeShape()
on your NullFactory.
- Your null pointer errors should go away. Hooray!
- Since your shape's paint method is a no-op, you should see no change
it the behavior of your program. Try putting a println
statement in the shape's paint
method to convince yourself that it really is being called whenever the
frame is repainted, for instance, when you resize it. (Note: moving the
frame on the screen does not always force a repaint because the operating
system simply copies the pixels from one place to another.)
- Now it's time to add some actual painting functionality to the shape.
- A Graphics object provides a method called drawRect that draws the outline
of a rectangle on the screen. The signature of this method is
public void drawRect(int x, int y, int width, int height)
where x &
y are the coordinates of the upper left hand corner of the rectangle,
where x is the left-to-right position in pixels and y is the up-to-down
positon in pixels. width
and height are the width
and height of the rectangle in pixels, respectively.
- If you set x and y
to be the paint method's
input parameters x &
y, the rectangle will draw
such that its upper left corner is at that point.
- The width and height are obviously the length of the side of the square.
You will need to cast the double
to an int however, which
will round the double down to the nearest integer value ("truncate").
- Add the call to drawRect
into the paint method of your square object and run your program. A square
should appear. Yeah!
- Well, almost. You probably noticed that the square only appears if you force
the frame to repaint by resizing it or some other means. The square didn't
appear automatically like you probably wanted it to.
- You need to have your program request that the frame (or display
panel) be repainted after the new shape is made.
- Technically, only the display panel needs to be repainted, but if you
ask the frame to repaint, the display panel will also repaint because
the frame, when painting, will ask all the components it contains to also
paint.
- So, simply add a call to repaint()
right after the shape is made.
- Re-run your program. Ahh...that's better!
- Did you notice that your square did not draw exactly at the center of the
panel? Fix the math in your code such that the square will always draw exactly
centered in the display panel.
Adding Dynamic Class Loading
Notice however, that other than where the SquareFactory
is hard coded into the system, none of the code written so far depends on what
sort of factory was being used. So we should not write into it some limiting
set of possible factories. Luckily, Java has the ability to load a class in
at run time and make it part of the currently running system. All one has to
do is to supply the full classname, which includes the package name, to Java's
"Reflection" system. Be sure to
import java.lang.reflect.* in your code.
Consider the following method, which you can
add to ShapeCalc:
/**
* Look for the Singleton field in the given class name.
*
* @param shapeName a String
that is the full classname of the desired class
* @return an IShapeFactory
*/
private IShapeFactory getFactory(String shapeName) {
// There may be errors encountered so try the following code
try {
// Find the specified class and find the Singleton field in it.
Field f = Class.forName(shapeName).getField("Singleton");
// Return the static value of the Singleton field
return (IShapeFactory)f.get(null);
}
// Do the following code only if an error was "caught"
catch (Exception e) {
System.err.println(e); // Print the error that occurred
return null; // return value when error occurred
}
}
Exception handling:
Sometimes in your program you can expect that errors may occur that are completely
out of your control as a programmer. These sorts of errors can be catastrophic
for a program. But often times, if one knew that the error occured the "exception"
as it's called, can be handled in a graceful manner. In the above code, for
instance, it is possible that the user requests a class that doesn't exist.
The Class.forName(shapeName) method
would thus fail. Luckily, Java has a very nice mechanism, call the try-catch
block" that handles such exceptions very well. The syntax for the try-catch
block is:/p>
try {
// code that might fail ("throw an exception")
}
catch(Exception e) {
// code to process the exception
}
Simplistically, the way a try-catch block works is that the code between the
curly braces following the try
keyword is executed. If no errors occur, then the execution continues onward,
skipping the code in the catch
statement. If an error occurs in the try
section of code ("throws an exception" in Java-ese), then execution
in the try section immediately
stops and the catch clause is
executed. I like to think of the catch
clause as a little function that the JVM calls when an error occurs, handing
that function and Exception object
containing information about the exception.
Try-catch blocks have more capabilities and issues than are outlined here and
this topic will be covered in more detail in lecture.
Using Dynamic Class Loading
To use the dynamic class loading, we will use the text field and allow the
user to type in any factory's class name they desire and have it loaded into
the system at run time.
- In the newBtn's ActionListener's
actionPerformed method, create
a local variable of type IShapeFactory
and initialize it to the return value from the above getFactory
method where the input String
is the text from the typeTF
textfield.
- For convenience's sake, change the initialization of the text in typeTF
to be "shapecalc.shapes.SquareFactory"
- You should be able to dynamically load either the SquareFactory or the NullFactory
and the system should display them properly.
- Try this: type an incorrect class name into the text field and then click
the "New..." button.
- Don't you just love the smell of null pointers in the morning?
- In the getFactory method
code above, what should be the value returned when an error occurs? Remember
that fundamentally, the method must return a factory. Adjust your code
accordingly. No conditional statements (e.g.
if) allowed!!
NNote: Ballworld used this sort of dynamic class loading to load the strategies
from the disk at run time.
A Shape That's a Bit More Fun
Let's make another shape factory that involves more fun things to do with graphics.
Call your new factory CircleFactory
(in the shapecalc.shapes package)
and let it create IShapes that
behave like circles. Let's have our circles be filled circles of a particular
solid color.
I will simple lay out some tools for you to use and you can put them together
yourself (see the
Java 1.5.0 API documentation for more information):
- TThe java.awt.Color class
represents a color in Java GUIs. It has a number of predefined static fields
for common colors: Color.RED,
Color.BLUE, Color.GREEN,
etc.
- The Graphics class as the
following useful methods:
- getColor() which returns
the current pen color (the color it paints with).
- setColor(Color c) which
sets the current pen color.
- fillOval(int x, int y, int width,
int height) paints a filled oval with the current pen color where
the size of the oval is determined by an imaginary "bounding box"
that exactly surrounds the oval and which has an upper left corner at
the given (x, y) coordinate and the given width and height.
- Math.PI is the value of pi.
Requirements::
- Before you set the graphics pen color, save the original color in a local
variable and then after you are done painting, restore the pen color to its
original color. This keeps your program from changing the pen color on another
graphics process that is assuming that the pen color is stable.
- Use a fixed value for the circle's radius for now.
- Contrary to first instincts, do not put a Color
field inside the IShape anonymous
inner class. Instead, put the field in the factory itself and call it _selectedColor.
The reasons for doing this are 1) we will use this field in the next part
of the lab and 2) it will cause an interesting problem that I would like you
to ponder.
- Adjust your mathematics so that the circle paints in the middle of the display
panel.
Test your program to prove that you can indeed load you new class and run it
properly without modifying or even recompiling any existing code! This
is a feat that cannot be duplicated with conditional statements.
On to part 4!
Last Revised
Thursday, 03-Jun-2010 09:50:28 CDT
©2008 Stephen Wong and Dung Nguyen