[ Exercises | Chapter Index | Main Index ]

Solution for Programming Exercise 7.5


This page contains a sample solution to one of the exercises from Introduction to Programming Using Java.


Exercise 7.5:

The sample program RandomArtPanel.java from Subsection 6.5.1 shows a different random "artwork" every four seconds. There are three types of "art", one made from lines, one from circles, and one from filled squares. However, the program does not save the data for the picture that is shown on the screen. As a result, the picture cannot be redrawn when necessary. In fact, every time paintComponent() is called, a new picture is drawn.

Write a new version of RandomArtPanel.java that saves the data needed to redraw its pictures. The paintComponent() method should simply use the data to draw the picture. New data should be recomputed only every four seconds, in response to an event from the timer that drives the program.

To make this interesting, write a separate class for each of the three different types of art. Also write an abstract class to serve as the common base class for the three classes. Since all three types of art use a random gray background, the background color can be defined in their superclass. The superclass also contains a draw() method that draws the picture; this is an abstract method because its implementation depends on the particular type of art that is being drawn. The abstract class can be defined as:

private abstract class ArtData {
   Color backgroundColor;  // The background color for the art.
   ArtData() {  // Constructor sets background color to be a random gray.
      int x = (int)(256*Math.random());
      backgroundColor = new Color( x, x, x );
   }
   abstract void draw(Graphics g);  // Draws this artwork.
}

Each of the three subclasses of ArtData must define its own draw() method. It must also define instance variables to hold the data necessary to draw the picture. I suggest that you should create random data for the picture in the constructor of the class, so that constructing the object will automatically create the data for the random artwork. (One problem with this is that you can't create the data until you know the size of the panel, so you can't create an ArtData object in the constructor of the panel. One solution is to create an ArtData object at the beginning of the paintComponent() method, if the object has not already been created.) In all three subclasses, you will need to use several arrays to store the data.

The file RandomArtPanel.java only defines a panel class. A main program that uses this panel can be found in RandomArt.java, and an applet that uses it can be found in RandomArtApplet.java. You only need to modify RandomArtPanel.


Discussion

In my solution, I defined subclasses LineArtData, CircleArtData, and SquareArtData of the basic abstract ArtData class. An instance variable named artData, of type ArtData, points to the object that holds the data for the current picture. When it's time to create a new artwork, I call the following method, which changes the value of artData. Note that this method has an equal chance of producing each of the three types of art:

/**
 * Creates an object belonging to one of the three subclasses of
 * ArtData, and assigns that object to the instance variable, artData.
 * The subclass to use (that is, the type of art) is chosen at random.
 */
private void createArtData() {
   switch ( (int)(3*Math.random()) ) {
   case 0:
      artData = new LineArtData();
      break;
   case 1:
      artData = new CircleArtData();
      break;
   case 2:
      artData = new SquareArtData();
      break;
   }
}

This method is called by the action listener that responds to events from the timer, so a new artwork is created every time the timer generates an event, that is, every four seconds.

The paintComponent() method uses the background color from artData to fill the drawing area. It then tells artData to draw itself by calling its draw() method. As suggested in the exercise, it first makes sure that an art data object has been created by calling createArtData() if artData is still null; this can only be true the first time paintComponent() is called. So, the paintComponent() method is quite simple:

public void paintComponent(Graphics g) {

   if (artData == null)  // If no artdata has yet been created, create it.
      createArtData();
   
   // Note:  Since the next two lines fill the entire panel, there is
   // no need to call super.paintComponent(g), since any drawing
   // that it does will only be covered up anyway.
   
   g.setColor(artData.backgroundColor); // Fill with the art's background color.
   g.fillRect( 0, 0, getWidth(), getHeight() );

   artData.draw(g);  // Draw the art.
     
} // end paintComponent()

The only thing that remains for discussion is the three classes that define the three types of art. Note that we have been able to get this far without thinking about creating or drawing the actual art. This is because we have been thinking "abstractly," in terms of the abstract class.

Let's look in detail at LineArtData, one of the three concrete subclasses of ArtData. In the original program, line art was drawn in the paintComponent() method as follows:

for (int i = 0; i < 500; i++) {
   int x1 = (int)(getWidth() * Math.random());
   int y1 = (int)(getHeight() * Math.random());
   int x2 = (int)(getWidth() * Math.random());
   int y2 = (int)(getHeight() * Math.random());
   Color randomHue = Color.getHSBColor( (float)Math.random(), 1.0F, 1.0F);
   g.setColor(randomHue);
   g.drawLine(x1,y1,x2,y2);
}

Here, the coordinates and colors for each line are chosen at random and the line is immediately drawn using that data. For the new version, we want to save the data in instance variables so that the picture can be drawn and redrawn on demand. The creation of the data will be split from the drawing; the data will be created in the constructor and will be used to draw the picture in the draw() method. To save the data for all 500 lines, we have to store the coordinates and colors in arrays. There are several ways to do this, but I used one array for each piece of data, x1, y1, x2, y2, and the color. To store data for 500 lines, each array should be of length 500. For example, x1 is declared as an instance variable of type int[], and the array is created with the command "x1 = new int[500];". The arrays are created in the LineArtData constructor and are filled with random data. In the draw() method, the i-th line can be drawn with the commands:

g.setColor( color[i] );    // Use the i-th color in the array
g.drawLine( x1[i], y1[i], x2[i], y2[i] );

We just need a for loop to draw all the lines. The complete definition of LineArtData class is as follows:

/**
 * Stores data for a picture that contains 500 random lines drawn in
 * different random colors.
 */
private class LineArtData extends ArtData {
   Color[] color;         // color[i] is the color of line number i
   int[] x1, y1, x2, y2;  // line i goes from (x1[i],y1[i]) to (x2[i],y2[i]).
   LineArtData() {  // Constructor creates arrays and fills then randomly.
      color = new Color[500];
      x1 = new int[500];
      y1 = new int[500];
      x2 = new int[500];
      y2 = new int[500];
      for (int i = 0; i < 500; i++) {
         x1[i] = (int)(getWidth() * Math.random());
         y1[i] = (int)(getHeight() * Math.random());
         x2[i] = (int)(getWidth() * Math.random());
         y2[i] = (int)(getHeight() * Math.random());
         color[i] = Color.getHSBColor( (float)Math.random(), 1.0F, 1.0F);
      }
   }
   void draw(Graphics g) {  // Draw the picture.
      for (int i = 0; i < 500; i++) {
         g.setColor(color[i]);
         g.drawLine( x1[i], y1[i], x2[i], y2[i] );
      }
   }
}

This is, by the way, a non-static nested class in the RandomArtPanel class. It can't be static because it uses the instance methods getWidth() and getHeight() from the containing class.

The other two classes are very similar. Note that we are using the technique of "parallel arrays" here. That is, the data for the i-th line is spread out over five different arrays. An array of objects, where each object holds all the data for one line, might be better style but it seemed to be overkill for this simple example.


The Solution

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

/**
 * A RandomArtPanel draws random pictures which might be taken to have
 * some vague resemblance to abstract art.  A new picture is produced every
 * four seconds.  There are three types of pictures:  random lines,
 * random circles, and random 3D rects.  The figures are drawn in
 * random colors on a background that is a random shade of gray.  The
 * data for a given piece of art is stored in a data structure so
 * that the picture can be redrawn if necessary.  The data is created
 * in response to the action event from a timer.
 */
public class RandomArtPanel2 extends JPanel {
   
   /**
    * The data for the currently displayed picture (unless it is null).
    */
   private ArtData artData;
   
   
   /**
    * The constructor creates a timer with a delay time of four seconds
    * (4000 milliseconds), and with a RepaintAction object as its
    * ActionListener.  It also starts the timer running.  The
    * RepaintAction class is a nested class, defined below.
    */
   public RandomArtPanel2() {
      RepaintAction action = new RepaintAction();
      Timer timer = new Timer(4000, action);
      timer.start();
   }

   
   /**
    * The paintComponent() method fills the panel with a random shade of
    * gray and then draws one of three types of random "art".  The data for
    * the picture is in the variable artData (if this variable is null,
    * then an artData object is created here).
    */
   public void paintComponent(Graphics g) {

      if (artData == null)  // If no artdata has yet been created, create it.
         createArtData();
      
      // Note:  Since the next two lines fill the entire panel, there is
      // no need to call super.paintComponent(g), since any drawing
      // that it does will only be covered up anyway.
      
      g.setColor(artData.backgroundColor); // Fill with the art's background color.
      g.fillRect( 0, 0, getWidth(), getHeight() );

      artData.draw(g);  // Draw the art.
        
   } // end paintComponent()
   
   
   /**
    * Creates an object belonging to one of the three subclasses of
    * ArtData, and assigns that object to the instance variable, artData.
    * The subclass to use (that is, the type of art) is chosen at random.
    */
   private void createArtData() {
      switch ( (int)(3*Math.random()) ) {
      case 0:
         artData = new LineArtData();
         break;
      case 1:
         artData = new CircleArtData();
         break;
      case 2:
         artData = new SquareArtData();
         break;
      }
   }
   
   
   /**
    * An abstract class that represents the data for a random work
    * of "art".  Different concrete subclasses of this class represent
    * different types of art.  This class contains a background
    * color which is a random shade of gray, selected when the object
    * is constructed.
    */
   private abstract class ArtData {
      Color backgroundColor;  // The background color for the art.
      ArtData() {  // Constructor sets background color to be a random shade of gray.
         int x = (int)(256*Math.random());
         backgroundColor = new Color( x, x, x );
      }
      abstract void draw(Graphics g);  // Draw the picture.
   }
   
   
   /**
    * Stores data for a picture that contains 500 random lines drawn in
    * different random colors.
    */
   private class LineArtData extends ArtData {
      Color[] color;         // color[i] is the color of line number i
      int[] x1, y1, x2, y2;  // line i goes from (x1[i],y1[i]) to (x2[i],y2[i]).
      LineArtData() {  // Constructor creates arrays and fills then randomly.
         color = new Color[500];
         x1 = new int[500];
         y1 = new int[500];
         x2 = new int[500];
         y2 = new int[500];
         for (int i = 0; i < 500; i++) {
            x1[i] = (int)(getWidth() * Math.random());
            y1[i] = (int)(getHeight() * Math.random());
            x2[i] = (int)(getWidth() * Math.random());
            y2[i] = (int)(getHeight() * Math.random());
            color[i] = Color.getHSBColor( (float)Math.random(), 1.0F, 1.0F);
         }
      }
      void draw(Graphics g) {  // Draw the picture.
         for (int i = 0; i < 500; i++) {
            g.setColor(color[i]);
            g.drawLine( x1[i], y1[i], x2[i], y2[i] );
         }
      }
   }
   
   
   /**
    * Stores data for a picture that contains 200 circles with 
    * radius 50, with random centers, and drawn in random colors.
    */
   private class CircleArtData extends ArtData {
      Color[] color;  // color[i] is the color of the i-th circle
      int[] centerX;  // center of circle i is at (centerX[i], centerY[i])
      int[] centerY;
      CircleArtData() {  // Constructor creates arrays and fills then randomly.
         color = new Color[200];
         centerX = new int[200];
         centerY = new int[200];
         for (int i = 0; i < 200; i++) {
            centerX[i] =  (int)(getWidth() * Math.random());
            centerY[i] = (int)(getHeight() * Math.random());
            color[i] = Color.getHSBColor( (float)Math.random(), 1.0F, 1.0F);
         }
      }
      void draw(Graphics g) {  // Draw the picture.
         for (int i = 0; i < 200; i++) {
            g.setColor(color[i]);
            g.drawOval(centerX[i] - 50, centerY[i] - 50, 100, 100);
         }
      }
   }
   
   
   /**
    * Stores data for a picture that contains 25 filled squares with 
    * random sizes and  with random centers, and drawn in random colors.
    */
   private class SquareArtData extends ArtData {
      Color[] color;  // color[i] is the color of the i-th square
      int[] centerX;  // the center of square i is (centerX[i], centerY[i])
      int[] centerY; 
      int[] size;     // size[i] is the length of the side of the i-th square
      SquareArtData() {  // Constructor creates arrays and fills then randomly.
         color = new Color[25];
         centerX = new int[25];
         centerY = new int[25];
         size = new int[25];
         for (int i = 0; i < 25; i++) {
            centerX[i] =  (int)(getWidth() * Math.random());
            centerY[i] = (int)(getHeight() * Math.random());
            size[i] = 30 + (int)(170*Math.random());
            color[i] = new Color( (int)(256*Math.random()), 
                  (int)(256*Math.random()), (int)(256*Math.random()) );
         }
      }
      void draw(Graphics g) {  // Draw the picture.
         for (int i = 0; i < 25; i++) {
            g.setColor(color[i]);
            g.fill3DRect(centerX[i] - size[i]/2, centerY[i] - size[i]/2, 
                  size[i], size[i], true);
         }
      }
   }
   

   /**
    * A RepaintAction object creates a new artData object and calls the repaint 
    * method of this panel each time its actionPerformed() method is called.  
    * An object of this type is used as an action listener for a Timer that 
    * generates an ActionEvent every four seconds.  The result is a new work of
    * art every four seconds.
    */
   private class RepaintAction implements ActionListener {
      public void actionPerformed(ActionEvent evt) {
         createArtData();
         repaint();
      }
   }
   
   
} // end class RandomArtPanel2

[ Exercises | Chapter Index | Main Index ]