Understanding the
Display Component Classes
There are two types of classes in the edu.kzoo.grid.display
package: classes
used
to display a grid as a whole, and classes used to display the various
objects in the grid. This document describes the classes that display
individual grid objects first and then the classes used to display the
overall grid.
Sections:
GridObject
Display Classes
Grid
Display Classes
GridObject
Display Classes
The following is the layout of the general GridObject
display classes
:
|
Figure 1
|
The structure of the display hierarchy is much less complicated than
it first appears. GridObjectDisplay
is a general
interface that
defines how all GridObject
s will be displayed. ScaledDisplay
implements the interface and provides the structure for how to
draw an
object. ScaledDisplay
implements the draw()
method that is
defined by GridObjectDisplay
. The implementation is
as follows:
public void draw(GridObject obj, Component comp, Graphics2D g2, Rectangle rect) { // Translate to center of object g2.translate(rect.x + rect.width/2, rect.y + rect.height/2);
// Scale to size of rectangle, adjust stroke back to 1-pixel wide float scaleFactor = Math.min(rect.width, rect.height); g2.scale(scaleFactor, scaleFactor); g2.setStroke(new BasicStroke(1.0f/scaleFactor));
// Apply the decorators if (!decorations.isEmpty()) { for (int i = 0; i < decorations.size(); i++) { ((DisplayDecorator)decorations.get(i)).decorate(this, obj, comp, g2); } }
// Adjust (e.g., rotate) as necessary. adjust(obj, comp, g2);
// Draw the image draw(obj, comp, g2); }
Code
Sample 1
|
The method does four distinct things:
- It scales the image to fit into a a single grid location.
- It applies any decorators for the display object.
- It calls
adjust()
.
- It calls the abstract
draw()
method.
The separation into four steps allows for a great deal of
customization.
The scaling is done using the following algorithm:
// Translate to center of object g2.translate(rect.x + rect.width/2, rect.y + rect.height/2); // Scale to size of rectangle, adjust stroke back to 1-pixel wide float scaleFactor = Math.min(rect.width, rect.height); g2.scale(scaleFactor, scaleFactor); g2.setStroke(new BasicStroke(1.0f/scaleFactor));
Code Sample 2
|
The DisplayDecorators
are stored in an ArrayList
defined in
ScaledDisplay
. After the image has been scaled any
decorators
found in the ArrayList
will be applied. (An explanation
of how
decorators work and how to design one will follow later.)
The adjust()
method allows for another step of
customization to the
drawing algorithm. This method pre-dates the use of display decorators,
and is provided primarily to avoid breaking existing code.
The call to the second draw()
method is used so that
each subclass
can define its own algorithm for drawing. If there were only one
draw()
method all of the associated GridObjects
would have to conform
to the exact same specification. For example: the
ColorBlockDisplay
calls the color()
method
on the associated
GridObject
. TextCellDisplay
calls
both color()
and text()
on the
associated GridObject
.
Example:
The ScaledImageDisplay
class defines
aditional instance
variables:
private ImageIcon icon; private Image originalImage; private DefaultDisplay defaultDisp; private HashMap tintedVersions = new HashMap();
Code Sample 3
|
It then references these properties in the
defined draw
method:
public void draw(GridObject obj, Component comp, Graphics2D g2) { if (icon.getImageLoadStatus() != MediaTracker.COMPLETE) { // Image failed to load, so fall back to default display. defaultDisp.draw(obj, comp, g2); return; } // Scale to shrink or enlarge the image to fit the size 1x1 cell. g2.scale(1.0/icon.getIconWidth(), 1.0/icon.getIconHeight()); icon.paintIcon(comp, g2, -icon.getIconWidth()/2, -icon.getIconHeight()/2); }
Code Sample 4
|
Decorators are another important aspect of the display
package. They allow further customization of the GridObject
displays. The use of decorators prevents an exponential
explosion
of new GridObjectDisplay
subclasses whenever additional
functionality
is desired; additional decorators can simply be applied to existing
ScaledDisplay
objects. Note that the decorators were
added at the
ScaledDisplay
level due to the preexisting structure of
the draw()
method. There are also no display objects that are not
scaled to
fit into a single grid location. The following is the structure
of the DisplayDecorator
hierarchy:
|
Figure 2
|
DisplayDecorator
provides an interface that all
decorators must
conform to so that decorate()
can be called from the draw()
method in ScaledDisplay
.
RotatedDecorator
, which allows the display for a GridObject
with a
direction()
method to change direction along with the
object, defines a
number of new instance variables and methods. The originalDirection
instance variable keeps track of the direction the image originaly was facing. This
is used as a reference point in rotating the image. The
decorate()
method is as follows:
public void decorate(ScaledDisplay sd, GridObject obj, Component comp, Graphics2D g2) { // Rotate drawing surface to compensate for the direction of the object // in the image (in case it is not facing North, as assumed). if ( ! originalDirection.equals(Direction.NORTH) ) { int rotationInDegrees = originalDirection.inDegrees(); g2.rotate(- Math.toRadians(rotationInDegrees)); if (rotationInDegrees >= 180) g2.scale(1, -1); // flip image upside-down }
// Now rotate again to represent the direction the object is facing. adjustForDirection(obj, g2); }
Code
Sample 5
|
After adjustments are made depending on whether or not the object is
facing north the image is adjusted for the actual direction with a call
to adjustForDirection()
.
public static void adjustForDirection(GridObject obj, Graphics2D g2) { // Rotate drawing surface to capture object's orientation // (direction). (Assumption is that without rotating the // drawing surface the object will be drawn facing North.) // Object must have a direction method. Class objClass = obj.getClass(); try { Method dirMethod = objClass.getMethod("direction", new Class[0]); Direction dir = (Direction)dirMethod.invoke(obj, new Object[0]); int rotationInDegrees = dir.inDegrees(); g2.rotate(Math.toRadians(rotationInDegrees)); //return rotationInDegrees; } catch (NoSuchMethodException e) { throw new IllegalArgumentException("Cannot rotate object of " + objClass + " class; cannot invoke direction method."); } catch (InvocationTargetException e) { throw new IllegalArgumentException("Cannot rotate object of " + objClass + " class; exception in direction method."); } catch (IllegalAccessException e) { throw new IllegalArgumentException("Cannot rotate object of " + objClass + " class; cannot access direction method."); } }
Code Sample 6
|
At this point all of the necessary changes have been made to the
display. Program control will then flow back to the draw()
method
of the ScaledDisplay
class.
Grid Display Classes
The display package also contains classes used to
display an entire Grid
. There is one primary grid
display class
provided, the ScrollableGridDisplay
class. It
implements
two interfaces: GridDisplay
and GridBackgroundDisplay
.
ScrollableGridDisplay
extends javax.swing.JPanel
.
The entire grid
display structure is as follows:
|
Figure 3
|
GridDisplay
requires a setGrid()
method
that sets the Grid
that will be
displayed as well as a showGrid()
method that will
display the Grid
. GridBackgroundDisplay
provides
a single method for drawing
the background for the grid. ScrollableGridDisplay
implements
both of these interfaces becauses it defines its own drawBackground()
method.
The showGrid()
method is the method called to display
the grid,
although almost none of the actual display code resides in that
method. It simply tells the display (which is a sbuclass of
JPanel
) to repaint itself.
public void showGrid() { repaint(); }
Code Sample 7
|
The method paintComponent()
controls the actually drawing
of the Grid
.
public void paintComponent(Graphics g) { Graphics2D g2 = (Graphics2D)g; super.paintComponent(g2);
if (grid() == null) return;
backgroundDisplay.drawBackground(g2);
GridObject[] allGridObjects = grid().allObjects(); for (int k = 0; k < allGridObjects.length; k++) drawGridObject(g2, allGridObjects[k]);
if ( gridLinesAreVisible() ) drawGridlines(g2); }
Code Sample 8
|
There are five steps involved in the method:
- Call the
paintComponent()
method of the super
class
(javax.swing.JPanel
).
- Check to see if there is in fact a
Grid
defined
for the display.
- Draw the background.
- Draw the
GridObject
s.
- Draw the grid lines if they are visible.
Drawing the background relies on the backgroundDisplay
instance
variable which is of type GridBackgroundDisplay
.
The
backgroundDisplay
variable for ScrollableGridDisplay
is set as a
reference to itself by default. That is why it was necessary for ScrollableGridDisplay
to
implement GridBackgroundDisplay
. If the backgroundDisplay
object
were set as a reference to a CheckeredBackgroundDisplay
object
then it
would use the drawBackground()
method defined in
CheckeredBackgroundDisplay
.
The drawBackground()
method defined in ScrollableGridDisplay
is as
follows:
public void drawBackground(Graphics2D g2) { fillBackground(g2, bgColor); }
Code
Sample 9
|
It simply calls a method that will fill the entire background in with
the same color:
public void fillBackground(Graphics2D g2, Color fillColor) { Color oldColor = g2.getColor(); Insets insets = getInsets(); g2.setColor(fillColor); g2.fillRect(insets.left, insets.top, numCols*outerCellSize + gridLineWidth, numRows*outerCellSize + gridLineWidth); g2.setColor(oldColor); }
Code
Sample 10
|
The drawBackground()
method in the checkeredBackgroundDisplay
class is
more complicated, but does largely the same thing. Rather than
fill the entire background with a solid color it draws a checkerboard
pattern using the colors defined in its instance variables
(upperLeftColor
and otherColor
).
public void drawBackground(Graphics2D g2){
// Fill the background with one of the two colors. overallDisplay.fillBackground(g2, otherColor);
// Fill in the checkerboard pattern with the other color. Insets insets = overallDisplay.getInsets();
int leftSide; int topSide;
Grid grid = overallDisplay.grid(); for (int row = 0; row < grid.numRows(); row++) { for (int col = 0; col < grid.numCols(); col++) {
// Calculate upper left corner of the cell to draw. leftSide = overallDisplay.colToXCoord(col); topSide = overallDisplay.rowToYCoord(row);
// Put the other checkered color in the top-left cell and // every other cell whose row and column are both even or // both odd. if ( (col % 2) == (row % 2 ) ) { g2.setColor(upperLeftColor); g2.fillRect(leftSide, topSide, overallDisplay.innerCellSize(), overallDisplay.innerCellSize()); } } } }
Code
Sample 11
|
After the background has been drawn the display draws the
GridObjects
that are in theGrid
. All
of the GridObject
s are put
in a temporary array and then looped through calling drawGridObject()
for each one.
protected void drawGridObject(Graphics2D g2, GridObject obj) { // Make sure that obj hasn't been removed from the grid // since it was placed in the list of objects to draw if ( obj.grid() != grid() ) return;
Location objLoc = obj.location();
Rectangle cellToDraw = new Rectangle(colToXCoord(objLoc.col()), rowToYCoord(objLoc.row()), innerCellSize(), innerCellSize());
// Only draw if the object is visible within the current clipping region. if (cellToDraw.intersects(g2.getClip().getBounds())) {
AffineTransform savedTransform = g2.getTransform(); // save
// Get the drawing object to display this grid object. GridObjectDisplay displayObj = DisplayMap.findDisplayFor(obj); displayObj.draw(obj, this, g2, cellToDraw);
g2.setTransform(savedTransform); // restore coordinate system }
Code
Sample 12
|
It first checks that the object is in the proper grid. It
then determines the cell in which it should be drawn in based on its Location
in theGrid
. It then makes sure that the
object will be visible
within the cell. If it is visible it performs the affine
transformation
on the graphics object (g2
). It then locates the
correct
GridObjectDisplay
from DisplayMap
and uses
that GridObjectDisplay
to
draw the object in the correct cell.
The final step for drawing the background is the drawing of the grid
lines if they are visible:
protected void drawGridlines(Graphics2D g2){
Rectangle curClip = g2.getClip().getBounds(); int top = getInsets().top, left = getInsets().left; int cellSize = outerCellSize; int miny = Math.max(0, (curClip.y - top)/cellSize)*cellSize + top; int minx = Math.max(0, (curClip.x - left)/cellSize)*cellSize + left; int maxy = Math.min(numRows,(curClip.y + curClip.height - top + cellSize - 1)/cellSize)*cellSize + top; int maxx = Math.min(numCols, (curClip.x + curClip.width - left + cellSize - 1)/cellSize)*cellSize + left;
g2.setColor(Color.black); g2.setStroke(new BasicStroke());
for (int y = miny; y <= maxy; y += cellSize) // draw horizontal lines g2.drawLine(minx, y, maxx, y);
for (int x = minx; x <= maxx; x += cellSize) // draw vertical lines g2.drawLine(x, miny, x, maxy);
}
Code
Sample 13
|
The locations of the lines are calculated using the size of the grid
display and the number of rows and columns.