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:

In order to get our shape to paint itself onto the screen there are a number of steps to go through.

  1. Make the shape that the factory creates accessible from anywhere inside of ShareCalc.
    1. This is easily enough accomplished by using a private field of type IShape.
    2. The call to the shape factory's makeShape() method simply sets this variable's value.
    3. You will need to modify your code that displays the shape's area accordingly.
  2. The shape is to paint itself onto the displayPnl, so the painting service provided by that panel must be overriden.
    1. 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!
    2. 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)
    3. In the body of paintComponent
      1. 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.
      2. 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.
  3. At this point, try running your program.
    1. 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.
      1. 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.
      2. 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.
      3. Since all shapes are made by factories, we thus need a null shape factory.
      4. 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.
      5. Initialize your shape field with the result of calling makeShape() on your NullFactory.
      6. Your null pointer errors should go away. Hooray!
    2. 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.)
  4. Now it's time to add some actual painting functionality to the shape.
    1. 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.
    2. 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.
    3. 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").
    4. Add the call to drawRect into the paint method of your square object and run your program. A square should appear. Yeah!
  5. 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.
    1. You need to have your program request that the frame (or display panel) be repainted after the new shape is made.
    2. 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.
    3. So, simply add a call to repaint() right after the shape is made.
    4. Re-run your program. Ahh...that's better!
  6. 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.

  1. 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.
  2. For convenience's sake, change the initialization of the text in typeTF to be "shapecalc.shapes.SquareFactory"
  3. You should be able to dynamically load either the SquareFactory or the NullFactory and the system should display them properly.
  4. Try this: type an incorrect class name into the text field and then click the "New..." button.
    1. Don't you just love the smell of null pointers in the morning?
    2. 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):

Requirements::

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