Add snap to a recurring GUI device

Design a rubber band class hierarchy that you can reuse and extend

Any user of modern graphical environments or applications has encountered a rubber band. In its most common use, the graphical rubber band selects, or lassos, a group of objects such as icons.

The rubber band can do more than select objects. A GUI may also employ a rubber band to draw shapes or erase an area of the screen. Behind the scenes, a rubber band object may aid in the interactive resizing of a graphical component. Most shocking, and counter to the preconceived notions of the rubber band, this graphical oddity does not have to be rectangular. Yes, a rubber band may dare to be circular!

Fortunately, Java makes things simple. Using the Abstract Windowing Toolkit, or AWT, as well as taking advantage of Java's powerful interfaces, a developer can easily design a reusable rubber band class -- and even extend it from GUI to GUI. When designing any class, we should always look for ways to keep the design flexible. The more flexible a class is, the more we will be able to use it again. And if we've done our job, the class will also allow for easy extensions so that we can add features without having to rewrite major sections of the class. Avoiding unnecessary future work is always a goal to strive for.

Java is prewired to allow for flexible class structures. As we go through the design of the rubber band, we will rely heavily on Java interfaces. Java interfaces lend themselves to flexible and extendable architectures since they free us from rigid class hierarchies. Instead of being forced to inherit from a common ancestor, objects can play together as long as they implement the appropriate interface. Being locked into a particular class hierarchy can make it difficult to plug in new objects and can sometimes destroy an architecture. It is not always convenient or even possible to extend from a common object if the source is not readily available.

Now, let's get to the design.

(To download the complete source code for this article as a jar file, see Resources.)

The Pieces: RubberBand and RubberBandCanvas

Before implementing the rubber band, we need to isolate those elements involved in drawing and manipulating it. Roughly, we split the rubber band concept into two elements: the rubber band and the rubber band canvas. The rubber band is the object that draws a band on the canvas in response to events. The rubber band canvas is simply the place where the rubber band listens for events and where it draws itself.

An optimal design will completely abstract the canvas from its rubber band; that is, the canvas shouldn't have any knowledge of how the rubber band works. Likewise, the rubber band should interact with the canvas only in a well-defined but generic manner. A successful design will also allow the canvas to be any object that can display itself. By following these principles, we can design a rubber band that fits easily into any GUI.

RubberBand

In our implementation, the RubberBand class does not extend from AWT's Component or from Java Foundation Classes' JComponent. The rubber band is never autonomous; instead, it is always associated with a canvas. That fact allows us to implement RubberBand as a lightweight displayable object. Instead of existing as a component that provides its own drawing context and events, RubberBand draws itself on a graphics object supplied by RubberBandCanvas. Likewise, RubberBand does not generate its own events. Rather, it responds only to events generated by RubberBandCanvas. Now let's examine the RubberBand class:

import java.awt.*;
import java.awt.event.*;
public class RubberBand
{
   private RubberBandCanvas canvas;
   private Point startPoint;
   private Point endPoint;
   private boolean eraseSomething = false;
   private class MouseHandler extends MouseAdapter
   {
      public void mousePressed(MouseEvent e)
      {
         start(e.getPoint()); // anchor the RubberBand
      }
      public void mouseReleased(MouseEvent e) 
      {
         erase();            // erase the final band
      }
   }
   private class MouseMotionHandler extends MouseMotionAdapter
   {
      public void mouseDragged(MouseEvent e) 
      {
         erase();            // erase any old bands
         stop(e.getPoint()); // set end point
         draw();             // draw the new band
      }
   }
   public RubberBand(RubberBandCanvas c) 
   {
      super();
      setCanvas(c);
      getCanvas().addMouseListener(new MouseHandler());
      getCanvas().addMouseMotionListener(new MouseMotionHandler());
   }
   protected void draw()
   {
      Graphics g = getCanvas().getGraphics();
      if(g != null)
      {
         try
         {
            // We always want to draw using XOR mode
            // so that we don't need to call redraw
            // to erase the band.
            g.setXORMode(canvas.getBackground());
            drawRubberBand(g);
            // We have drawn something, set the flag
            // to indicate that there is something to erase.
            setEraseSomething(true);
         }
         finally
         {
            g.dispose();
         }
      }
   }
   protected void drawRubberBand(Graphics g)
   {
      // The following if/else block determines where to draw the band.
      // Based on the anchor point, the band may be drawn in any of 
      // four quadrants.  The if/else determines which quadrant to draw
      // the band in. 
      if((getEndPoint().x > getStartPoint().x) && (getEndPoint().y > getStartPoint().y))
      {
         g.drawRect(
            getStartPoint().x,
            getStartPoint().y,
            getEndPoint().x-getStartPoint().x,
            getEndPoint().y-getStartPoint().y
         );
      }
      else if((getEndPoint().x < getStartPoint().x) && (getEndPoint().y < getStartPoint().y))
      {
         g.drawRect(
            getEndPoint().x,
            getEndPoint().y,
            getStartPoint().x-getEndPoint().x,
            getStartPoint().y-getEndPoint().y
         );
      }
      else if((getEndPoint().x > getStartPoint().x) && (getEndPoint().y < getStartPoint().y))
      {
         g.drawRect(
            getStartPoint().x,
            getEndPoint().y,
            getEndPoint().x-getStartPoint().x,
            getStartPoint().y-getEndPoint().y
         );
      }
      else if((getEndPoint().x < getStartPoint().x) && (getEndPoint().y > getStartPoint().y))
      {
         g.drawRect(
            getEndPoint().x,
            getStartPoint().y,
            getStartPoint().x-getEndPoint().x,
            getEndPoint().y-getStartPoint().y
         );
      }
   }
   protected void erase()
   {
      // We only erase if there is something to erase!
      if(getEraseSomething())
      {
         draw();
         setEraseSomething(false);
      }
   }
   protected final RubberBandCanvas getCanvas() { return this.canvas; }
   protected final Point getEndPoint()
   {
      if(this.endPoint == null)
      {
         setEndPoint(new Point(0,0));
      }
         return this.endPoint;
   }
   protected final boolean getEraseSomething() { return this.eraseSomething; }
   protected final Point getStartPoint()
   {
      if(this.startPoint == null)
      {
         setStartPoint(new Point(0,0));
      }
      return this.startPoint;
   }
   protected final void setCanvas(RubberBandCanvas c) { this.canvas = c; }
   protected final void setEndPoint(Point newValue) { this.endPoint = newValue; }
   protected final void setEraseSomething(boolean newValue)
   {
      this.eraseSomething = newValue;
   }
   protected final void setStartPoint(Point newValue) { this.startPoint = newValue; }
   protected void start(Point p)
   {
      // anchor the band
      setEndPoint(p);
      setStartPoint(p);
   }
   protected void stop(Point p)
   {
      // set the end point, but no coordinate should be < 0
      if(p.x < 0)
      {
         p.x = 0;
      }
      if(p.y < 0)
      {
         p.y = 0;
      }
      setEndPoint(p);
   }
}   

The code above presents the initial implementation of the RubberBand class in its entirety. That implementation works as a baseline for the versions presented in the following sections.

RubberBandCanvas

The implementation of RubberBandCanvas may or may not be a component. From the point of view of a RubberBand, it doesn't matter what RubberBandCanvas's implementation is, just so long as RubberBand can obtain a Graphics object from RubberBandCanvas, query RubberBandCanvas's background color, and register itself with RubberBandCanvas as a Mouse and MouseMotion listener.

For this reason, RubberBandCanvas should be implemented as an interface with the ability to supply a Graphics instance, supply the background color, and register Mouse and MouseMotion listeners. Since RubberBandCanvas is an interface, its actual implementation can be an applet or some other lightweight component. Moreover, its being an interface frees us from being tied down to any specific implementation. With this information, it is easy to formulate an initial RubberBandCanvas interface:

public abstract interface RubberBandCanvas 
{
   public abstract Graphics getGraphics();
   public abstract void addMouseMotionListener(MouseMotionListener listener);
   public abstract void addMouseListener(MouseListener listener);
   public abstract Color getBackground();
}

The code contained below presents a basic example of an applet that implements the RubberBandCanvas interface and associates itself with a RubberBand instance:

import java.applet.*;
import java.awt.*;
import java.awt.event.*;
public class RubberBandTest extends Applet implements RubberBandCanvasIF
{
   protected String str = "RubberBands!!!!";
   private RubberBand rubberband;
   protected final RubberBand getRubberBand()
   {
      if(this.rubberband == null)
      {
         setRubberBand(new RubberBand(this));
      }
      return this.rubberband;
   }
   public void init()
   {
      super.init();
      getRubberBand();
   }
   public void paint(Graphics g) { g.drawString(str, 5, 50); }      
   protected final void setRubberBand(RubberBand newValue) { this.rubberband = newValue; }
}

With the applet above, RubberBand displays itself whenever the user presses and drags the mouse.

Figure 1. A sample RubberBand

This is all well and good. However, the current RubberBand is nothing more than eye candy. It doesn't really do anything! RubberBand needs to communicate with the corresponding RubberBandCanvas. RubberBand can then notify RubberBandCanvas whenever it draws a bounding box.

RubberBand communication

The example above demonstrates a basic RubberBand. However, it lacks the facilities to do anything to the area it bounds. Embedding such logic inside the RubberBand class would restrict RubberBand's flexibility. The inflexibility would force the developer to extend RubberBand and reimplement the logic each time a RubberBand was needed.

Instead, RubberBandCanvas needs to implement the application-specific logic. RubberBand simply needs a way to communicate the bounding area back to RubberBandCanvas. Fortunately, the problem is easily corrected: add one method to RubberBandCanvas and call that method at appropriate times from within RubberBand.

First, we need to add a method -- areaBounded() -- to RubberBandCanvas that allows RubberBand to pass it the upper left-hand corner and lower right-hand corner of RubberBand's bounding box:

public void areaBounded(int startX, int startY, int endX, int endY);

Now RubberBand simply calls areaBounded() whenever it draws itself. Be aware that RubberBand does not call areaBounded() directly from its drawing code. Rather, RubberBand calls the helper method NotifyRubberBandCanvas(). NotifyRubberBandCanvas() calculates the bounding coordinates and then calls areaBounded(). The call to NotifyRubberBandCanvas() is made from within the inner class MouseMotionHandler's mouseDragged() method. The MouseMotionHandler class is shown below:

private class MouseMotionHandler extends MouseMotionAdapter
{
   public void mouseDragged(MouseEvent e)
   {
      erase();
      stop(e.getPoint());
      draw();
      // Let the canvas know an area is bounded
      notifyRubberBandCanvas();
   }
}
protected void notifyRubberBandCanvas()
{
   int startX, startY, endX, endY;
   // We always want to return the upper left hand corner
   // and the lower right hand corner, the coordinates need
   // to be filtered accordingly and returned in the right order.
   if(getStartPoint().x < getEndPoint().x)
   {
      startX = getStartPoint().x;
      endX = getEndPoint().x;
   }
   else
   {
      startX = getEndPoint().x;
      endX = getStartPoint().x;
   }
   if(getStartPoint().y < getEndPoint().y)
   {
      startY = getStartPoint().y;
      endY = getEndPoint().y;
   }
   else
   {
      startY = getEndPoint().y;
      endY = getStartPoint().y;
   }
   getCanvas().areaBounded(startX, startY, endX, endY);
}

Since RubberBandCanvas is an interface, areaBounded() has no defined behavior. RubberBand simply makes the call; it doesn't care what RubberBandCanvas does with the information. RubberBandCanvas can even ignore the information.

1 2 3 Page 1
Page 1 of 3