Main Page

Previous Next

Managing Shapes

When we create shapes in Sketcher, we'll have no idea of the sequence of shape types that will occur. This is determined totally by the person using the program to produce a sketch. We'll therefore need to be able to draw shapes and perform other operations on them without knowing what they are - and polymorphism can help here.

We don't want to use the shape classes defined in java.awt.geom directly as we will want to add our own attributes such as color or line style, and store them as part of the object. We could consider using the shape classes as base classes for our shapes, but we couldn't use the GeneralPath class in this scheme of things because, as we have already seen, it's final and we might not want this restriction. We could consider defining an interface that all our shape classes would implement. However, there will be some methods that have a common implementation in all our shape classes so we would need to repeat this code in every class.

Taking all of this into account, the easiest approach might be to define a common base class for our shape classes, and include a member in each class to store a shape object of one kind or another. We'll then be able to include a polymorphic method to return a reference to a shape as type Shape for use with the draw() method of a Graphics2D object.

We can start by defining a base class, Element, from which we'll derive the classes defining specific types of shapes. The Element class will have data members that are common to all types of shapes, and we can put the methods that we want to execute polymorphically in this class too. All we need to do is make sure that each shape class that is derived from the Element class has its own implementation of these methods.

Click To expand

The diagram shows the initial members that we will declare in the Element base class. The only data member for now is the color member to store the color of a shape. The getShape()and getBounds() methods will be abstract here since the Element class is not intended to define a shape, but we will be able to implement the getColor() method in this class. The other methods will be implemented by the subclasses of Element.

Initially, we'll define the five classes shown in the diagram that represent shapes, with the Element class as a base. They provide objects that represent straight lines, rectangles, circles, freehand curves and blocks of text. These classes will all inherit the data members that we define for the Element class. As you can see from the names of our shape classes, they are all nested classes to the class Element. The Element class will serve as the base class, as well as house our shape classes. This will avoid any possible confusion with other classes that might have names such as Line or Circle for instance. Since there will be no Element objects around, we will declare our shape classes as static members of the Element class.

We can now define the base class, Element. Note that this won't be the final version, as we'll be adding more functionality in later chapters. Here's the code that needs to go in Element.java in the Sketcher directory:

import java.awt.Color;
import java.awt.Shape;

public abstract class Element {
  public Element(Color color) {
    this.color = color;  
  }

  public Color getColor() {
    return color;  
  }

  public abstract Shape getShape();
  public abstract java.awt.Rectangle getBounds();

  protected Color color;                             // Color of a shape
}

We have defined a constructor to initialize the data member, and the getColor() method. The other methods are abstract, so they must be implemented by the subclasses.

Note that the return type for the abstract getBounds() method is fully qualified using the package name. This is to prevent confusion with our own Rectangle class that we will add later on in this chapter.

Storing Shapes in the Document

Even though we haven't defined the classes for the shapes that Sketcher will create, we can implement the mechanism for storing them in the SketchModel class. We'll be storing all of them as objects of type Element. We can use a LinkedList collection class object to hold an arbitrary number of Element objects, since a LinkedList can store any kind of object. It also has the advantage that deleting a shape from the list is fast.

We can add a member to the SketchModel class that we added earlier to the Sketcher program to store elements:

import java.util.Observable;
import java.util.LinkedList;

class SketchModel extends Observable {
  protected LinkedList elementList = new LinkedList();
}

We will want methods to add and delete Element objects from the linked list, and a method to return an iterator for the list, so we should add those to the class too:

import java.util.Observable;
import java.util.LinkedList;
import java.util.Iterator;

class SketchModel extends Observable {
  public boolean remove(Element element) {
    boolean removed = elementList.remove(element);
    if(removed) {
      setChanged();
      notifyObservers(element.getBounds());
    }

    return removed;
  }
  
  public void add(Element element) {
    elementList.add(element);
    setChanged();
    notifyObservers(element.getBounds());
  }

  public Iterator getIterator() {
    return elementList.listIterator();  
  }

  protected LinkedList elementList = new LinkedList();
}

All three methods make use of methods defined in the LinkedList class so they are very simple. The add()and remove()functions have a parameter type of Element so only our shapes can be added to the linked list or removed from it. When we add or remove an element, the model is changed and therefore we call the setChanged() method inherited from Observable to record the change, and the notifyObservers() method to communicate this to any observers that have been registered with the model. We pass the Rectangle object returned by getBounds() for the shape to notifyObservers(). Each of the shape classes defined in java.awt.geom implements the getBounds() method to return the rectangle that bounds the shape. We will be able to use this in the view to specify the area that needs to be redrawn.

In the remove() method, it is possible that the element was not removed - because it was not there for instance - so we test the boolean value returned by the remove() method for the LinkedList object. We also return this value, as the caller may want to know if an element was removed or not.

Next, even though we haven't defined any of our specific shape classes, we can still make provision for displaying them in the view class.

Drawing Shapes

We will draw the shapes in the paint() method for the SketchView class, so remove the old code from the paint() method now. We can replace it for drawing our own shapes like this:

import javax.swing.JComponent;
import java.util.*;                  
import java.awt.*;

class SketchView extends JComponent implements Observer {
  public SketchView(Sketcher theApp) {
    this.theApp = theApp;
  }

  // Method called by Observable object when it changes
  public void update(Observable o, Object rectangle) {
    // Code to respond to changes in the model...
  }

  public void paint(Graphics g) {
    Graphics2D g2D = (Graphics2D)g;                     // Get a 2D device context
    Iterator elements = theApp.getModel().getIterator();
    Element element;                                    // Stores an element

    while(elements.hasNext()) {                         // Go through the list
      element = (Element)elements.next();               // Get the next element
      g2D.setPaint(element.getColor());                 // Set the element color
      g2D.draw(element.getShape());                     // Draw its shape
    }
  }

  // Method called by Observable object when it changes
  public void update(Observable o, Object rectangle) {
    // Code to respond to changes in the model...
  }

  private Sketcher theApp;           // The application object
}

The getModel() method that we implemented in the Sketcher class returns a reference to the SketchModel object, and this is used to call the getIterator()method which will return an iterator for the list of elements. Using a standard while loop, we iterate through all the elements in the list. For each element, we obtain its color and pass that to the setPaint() method for the graphics context. We then pass the Shape reference returned by the getShape() method to the draw() method for g2D. This will draw the shape in the color passed previously to the setPaint() method. In this way we can draw all the elements stored in the model.

It's time we put in place the mechanism for creating Sketcher shapes.

Previous Next
JavaScript Editor Java Tutorials Free JavaScript Editor