Java Programming   Java Programming
 |Sofia Home | Content Gallery |
Home
Syllabus
Schedule
Lessons
Assignments
Resources

Lesson 11.1 Painting

Painting and Graphics

From working with the drawString() method, you've seen that only Graphics objects can draw on the display of an applet or GUI application. You might wonder why this is. Why can't you just tell your computer to paint a red pixel at location 100, 100, without involving some Graphics object.

Who Does What?

In the "old days", back when every computer ran a single program at a time, telling your computer to turn a particular pixel red was a reasonable request. Today, however, it doesn't make much sense at all.

As I'm writing this, I have three applications running in the foreground in six different windows. If all three of my applications decide to paint at location 100, 100 at the same time, chaos would quickly follow. Obviously, with multiple applications running, each application cannot treat the display as its own private resource. Each one has to share with the others. They all have to agree to abide by the same "contract."

Back to Top

The AWT

The key figure in this picture is the AWT [the Abstract Window Toolkit] and its Graphics object. The AWT acts as a "traffic cop" to make sure your efforts to paint on the display don't interfere with other programs, or with different parts of your own program.

The AWT-Programmer contract.

It does this in three ways:

  1. The AWT "clips" your drawing region. If you create a drawing area that is 100 pixels square, and you try to paint a bright red circle that extends outside your drawing area, your Graphics object, in effect, holds a "stencil" over your drawing area so your efforts don't spill over on your neighbor's area.
  2. The AWT "watches over" your drawing region. If another application should drag a dialog box over your drawing area, or if your application should scroll a portion of your image out of its window, the AWT will "notify" you when your drawing region needs to be repainted.
  3. The AWT "schedules" access to your drawing region, so that different methods don't end up fighting over the area. Although you have to do the actual painting, you can ask the AWT to give you some time, by calling repaint(). This acts as a request to the AWT to update the display; this request may be combined ["batched"] with other requests.
Back to Top

Your Responsibility

The AWT manages the overall painting process, but it doesn't do the actual painting. That responsibility is up to you.

Here's what you'll normally do:

  • Invoke repaint() when you want the AWT to schedule an update.
  • Override the paint() and, optionally, the update() method, to change the way your object is painted. You'll handle your application-specific painting code in paint(), and your erasing logic in update().

Working With Graphics

Every AWT Component has its own Graphics object. A copy of that Graphics object [not the original] is passed to the paint() and update() methods when it schedules a repaint. You can also obtain an "unofficial" copy by sending the component a getGraphics() message. When you obtain this "unofficial" copy of a component's Graphics object, you must call its dispose() method when you're done working with it.

However you receive your Graphics object, you're going to use it for two things: 

  • The Graphics object provides the environment in which you'll work: your own "painting studio" to speak. There are several methods that allow you to modify that environment.
  • The Graphics object contains a bevey of methods that will help you in your actual painting. Think of these as your "painter's apprentice." You can use these methods to do your drawing.
Back to Top

The Environment

The Graphics object contains the "environment" used when any AWT component is drawn. This environment consists of:
  • a default Font, used when you call drawString().
  • a default "brush", used when painting lines or shapes.
  • a default painting color.
  • a default painting mode.
  • a default coordinate system.
The Graphics object that you receive "borrows" several of these items from the component it represents. A Graphics object uses its component's foreground property as its default painting color, for instance, and the Graphics coordinate system is relative to the upper-left corner of its component.

Inside your paint() method [or other method if your retrieve a Graphics object via getGraphics()] you can make changes to the Graphics copy you receive by calling the methods in the Graphics class.

For instance, to change the drawing color [assuming your Graphics object is named g], you could write:

g.setColor(Color.red);

This doesn't change the "master copy" of the Graphics object. For that to occur, you must write:

c.setForground(Color.red);

where c is the name of the Component whose Graphics object you want to modify. Make sure that you don't try this inside a paint() method, however. By the time the paint()method begins, the AWT has already made a copy of the "master" Graphics object, and passed it to your paint() method. Any changes you make to the "master" Graphics object don't affect the copy you paint with until the next time paint() is called.

Back to Top

The Coordinate System

Most Graphics painting methods require you to pass particular coordinates such as the end-points for a line, or the upper-left corner of a rectangle. In Java, all coordinates are in pixels, not some sort of logical units. If you want to use logical units [which is highly recommended], you'll have to construct a system to do so.

The origin of the coordinate system [that is, position 0,0] is located in your component's upper-left corner. The horizontal [x] dimension increases as you move to the right, and the vertical [y] dimension increases as you move toward the bottom. Note that this is different than the traditional Cartesian graphing system you learned in algebra, where y increases as you move toward the top.

The coordinate system used when painting in Java. [Click to enlarge]

Java's AWT has no built-in way to change this default mapping, but it does give you an easy way to move the origin [point 0,0] of the coordinate system, by using the Graphics translate() method.

The translate() method takes two int arguments, dx and dy, that represent the amount to move position 0,0 in the horizontal and vertical dimensions. Each of these movements is relative to the current position of the origin, not absolute measurements from the upper-left corner.

Each time the paint() method is called, however, the Graphics object starts with 0,0 in its upper left-corner.

Here is an applet that uses translate() to paint a message at several different places on the screen. Note that

1) each message is painted at a "nominal" x,y position 0,0, and

2) that each translate() message moves only one of the coordinates.

You are free to move both coordinates at once.
Translate.java
import java.awt.*;
import java.applet.*;

public class Translate extends Applet
{
  public void paint(Graphics g)
  {
    g.setFont(new Font("Serif", Font.BOLD, 18));

    int h = getSize().height; // height of applet
    int w = getSize().width;  // width of applet
    int dy = h / 4, dx = w / 7, y = dy;

    for (int i = 0; i < 7; i++) // print 7 times
    {
      g.translate(0, dy); // Move in the y direction

      g.drawString("OOP"+i, 0, 0);

      g.translate(dx, 0); // Move in the x direction
      if ((y += dy) > h) dy = - dy;
    }
  }
}

Back to Top

Figure Drawing

The second "role" of the Graphics object is to provide methods that act as "artist's assistants." These methods draw lines, simple shapes, and complex geometric patterns. As you'll see in the Lesson 11.3, Images, these assistants know how to work with JPEGs and GIFs as well.

The simplest of these methods is the one that draws lines. Let's take it for a spin, right now.

Drawing Lines

The drawLine() method connects two end-points like this:

drawLine(x1, y1, x2, y2);

When you draw lines in Java, all lines are 1 pixel thick, and use the default Graphics brush color [retrieved from your component's foreground property]. You can change the color of the brush, used to paint the lines, but you can't change the thickness of the line. The algorithm used to draw lines in Java is inclusive. Both the point x1,y1 and the point x2,y2 are included in the line. Thus, if you want to draw a single pixel on the screen--at point 100,100, for instance, you do it like this:

drawLine(100, 100, 100, 100);

Here's an applet, Sticks.java, that draws a set of randomly sized, placed, and colored lines on your display, everytime repaint() is called. [This program is a translation of Doug Cooper's Oh! Pascal Sticks program.]
Sticks.java
import java.awt.*;
import java.applet.*;
import java.util.*;

public class Sticks extends Applet
{
  static Color[] colors = {
    Color.blue, Color.cyan, Color.darkGray, Color.gray,
    Color.green, Color.lightGray, Color.magenta,
    Color.orange, Color.pink, Color.red, Color.yellow };   public void paint(Graphics g)
  {
    int width = getSize().width;
    int height = getSize().height;

    for (int row = 0; row < height; row++)
    {
      int clr = (int)(Math.random() * colors.length);
      g.setColor(colors[clr] );
      g.drawLine((int) (width  * Math.random()),
                 (int) (height * Math.random()),
                 (int) (width  * Math.random()),
                 (int) (height * Math.random()));
    }
  }
}

Back to Top

The drawPolyline() Method

The drawPolyline() method is useful when you want to draw connected lines. Using drawLine(), you specify both endpoints of each line. With drawPolyline(), you supply two arrays, representing the x and y coordinates of each endpoint in the line, along with the number of lines, like this:
drawPolyline(int[] ax, int[] ay, int n);

Java marches through the array using n as an index and extracts a value from each of the arrays. You are responsible for making sure that both the ax and ay arrays have n elements. Here's an applet, PolyDraw.java, that stores mouse clicks in the two arrays ax and ay and uses drawPolyline() to draw all of the points in the array. Click your way around its surface to see what you can create.
PolyDraw.java
import java.awt.*;
import java.applet.*;

public class PolyDraw extends Applet
{
  int[] ax = new int[100];
  int[] ay = new int[100];
  int n = 0;   // Java 1.0 event handling
  public boolean mouseDown(Event e, int x, int y)
  {
    if (n >= ax.length) n = 0; // wrap around     ax[n] = x;
    ay[n] = y;
    n++;     repaint();
    return true;
  }

  public void paint(Graphics g)
  {
    g.drawPolyline(ax, ay, n);
  }

  public void update(Graphics g)
  {
    paint(g);
  }
}

The PolyDraw applet. Click the mouse at various places on the canvas to draw an image.

A Note on update()

As you might have noticed, the PolyDraw applet overrides the update() method, in addition to the paint() method. In the normal painting "life-cycle", the update() method is always called before paint().

The default behavior of update() is to erase the background of the component and then to call paint(). By overriding update() you can eliminate the unnecessary [for this applet] erasing of the applet, thereby getting a smoother running program.

Back to Top

Drawing Shapes

In addition to lines, Java has facilities for drawing simple geometric shapes, including rectangles, ovals, arcs, round-cornered rectangles, and polygons.

The bounding rectangle used to specify the size and position of enclosed figures.

Most of the simple geometric shapes use the principle of a "bounding box". A bounding box is the smallest rectangle that can "contain" the object you are drawing. You specify this bounding box using an x,y position [usually the upper-left corner of the bounding box], along with the width and height of the rectangle, rather than supplying the lower-right corner. (This is easy to forget, because the drawLine() method uses x1 ,y1 to x2, y2. This bounding box has a few non-intuitive characteristics. Here's what the official Sun documents say about it:
Sun's Explanation
"Coordinates are infinitely thin and lie between the pixels of the output device. Operations which draw the outline of a figure operate by traversing an infinitely thin path between pixels with a pixel-sized pen that hangs down and to the right of the anchor point on the path. Operations which fill a figure operate by filling the interior of that infinitely thin path."

Here's what it means in practice. When you draw a simple geometric shape, such as a rectangle, your shape will be one-pixel larger on the right. and one pixel larger on the bottom than you expect when you use the "outline" drawing methods. Your shape will be exactly the size you would expect when you use the "fill" drawing methods. Here's a specific example, using the rectangle drawing methods, which we'll examine shortly:

g.drawRect(0, 0, 10, 10);
g.fillRect(0, 0, 10, 10);

Both example draw a rectangle using the same coordinates, but the first rectangle is eleven pixels high and wide, while the second is ten pixels high and wide.

Back to Top

Drawing Plain Rectangles

There are three methods that allow you to draw, fill, or clear a given rectangle:

drawRect(x, y, width, height);
fillRect(x, y, width, height);
clearRect(x, y, width, height);
  • The drawRect() method draws a 1-pixel wide outline rectangle that starts at x, y, and goes to width + 1 and height + 1, as previously mentioned. With most components, the color of the line is taken from the default foreground color. You can change it at will, of course, by using Graphics setColor().
  • The fillRect() method uses the same pen color as drawRect(), but paints a solid block starting at x,y and extending width and height pixels. As previously mentioned, a rectangle drawn with fillRect() will fall one pixel short on the right and bottom, compared to a rectangle drawn with drawRect().
  • The clearRect() method works exactly like fillRect() except it draws using the component's background color rather than the foreground or Graphics color attribute.
Here's a simple example, SimpleRect.java. SimpleRect draws an outline rectangle starting at 10,10 that is 101 pixels high and wide.

It then changes the painting color to red, and paints a filled rectangle at 111,10 that is 100 pixels high and wide. Notice that the second rectangle must start at 111, not 110, because the first rectangle is 101 pixels wide. Also, note that the rectangle on the right is shorter, by one pixel, than the rectangle on the left. After drawing the red filled rectangle, SimpleRect uses clearRect() to clear a portion of the filled rectangle starting at 112, 11.
SimpleRect.java
import java.awt.*;
import java.applet.*;

public class SimpleRect extends Applet
{
  public void paint(Graphics g)
  {
    g.drawRect(10, 10, 100, 100);

    g.setColor(Color.red);
    g.fillRect(111, 10, 100, 100);

    g.clearRect(112, 11, 80, 80);
  }
}

Here's a more extensive example program, RandRect.java, that draws twenty-five random outline rectangles on the left side of the screen, and another twenty-five random filled rectangles on the right.
RandRect.java
import java.awt.*;
import java.applet.*;

public class RandRect extends Applet
{
  static Color[] colors = {
    Color.blue, Color.cyan, Color.darkGray, Color.gray,
    Color.green, Color.lightGray, Color.magenta,
    Color.orange, Color.pink, Color.red, Color.yellow };   public void paint(Graphics g)
  {
    int width   = getSize().width;
    int mid     = width / 2;
    int height  = getSize().height;     for(int i = 0; i < 25; i++)
    {
      int x = (int)(Math.random() * mid);
      int y = (int)(Math.random() * height);

      int clr = (int)(Math.random() * colors.length);
      // Left of the screen
      g.setColor(colors[clr]);
      g.drawRect( x, y,
         (int)(Math.random() * (mid - x)),
         (int)(Math.random() * (height - y)));

      // Right of the screen
      x += mid;
      clr = (int)(Math.random() * colors.length);
      g.setColor(colors[clr]);
      g.fillRect( x, y,
          (int)(Math.random() * (width - x)),
          (int)(Math.random() * (height - y)));
    }
  }
}

Back to Top

Fancy Rectangles

In addition to the plain old rectangles, Java also has two fancy new rectangles: the rounded rectangle and the 3D rectangle. Let's take a quick look at both.

There are two methods to draw rounded rectangles: one to draw an outline and one to draw a filled shape. There is no "clear" method, but you can easily write your own by setting the Graphics color using getBackground() before drawing the rectangle. Here are the rounded rectangle methods:

drawRoundRect(x, y, w, h, xArc, yArc);
fillRoundRect(x, y, w, h, xArc, yArc);

Like plain-old-rectangles, rounded rectangles are specified by supplying an x,y coordinate along with a width and height. The outline rounded rectangles are one pixel higher and wider than their filled brethren, just as with plain-old rectangles. The two additional arguments determine how "round" the corners of each rectangle will be. The argument you supply is the diameter of an ellipse placed against the corner of the bounding box like this:

Use of the xArc and yArc arguments to the drawRoundRect() method.

If you use the some arguments for width and xArc, along with identical arguments for height and yArc, you'll end up with an ellipse. The corners will "consume" all the rectangle portion of your shape. If all four arguments--width, height, xArc, yArc--are the same, then you end up with a circle. Java has one additional rectangle drawing method:

draw3DRect(x, y, width, height, raised);

Like the rounded rectangles, the 3D variety requires an additional argument. The last argument to draw3DRect(), raised, is a boolean. If you pass true, the rectangle will "pop out" like a button; if you pass false, the rectangle will appear sunken. Even with 3D rectangles, however, Java draws using a one-pixel pen. If you want to create "deeper" or "higher" rectangles, you need to use a loop. Here's an applet, OtherRects.java, that creates a "sunken" panel, and then draws a "raised" button and a few rounded rectangles.
OtherRects.java
import java.awt.*;
import java.applet.*;

public class OtherRects extends Applet
{
  public void init()
  {
    setBackground(Color.lightGray);
  }   public void paint(Graphics g)
  {
    g.setColor(getBackground());     int x = 0, y = 0,
        w = getSize().width - 1,
        h = getSize().height - 1;

    // 1. Draw the sunken panel first
    for (int i = 0; i < 2; i++)
      g.draw3DRect(x+i, y+i, w-(i * 2), h-(i * 2), false);

    // 2. Draw the "button" at the bottom
    w /= 3;
    h /= 5;
    x = w;
    y = 3 * h ;
    for (int i = 0; i < 4; i++)
      g.draw3DRect(x+i, y+i, w - (i*2), h - (i*2), true);

    // 3. Draw some rounded rects on the top
    g.setColor(Color.red);
    x /= 2;
    y = h;
    g.fillRoundRect(x, y, w, h, w/2, h/2);
    g.setColor(getBackground());
    g.fillRoundRect(x+4, y+4, w-8, h-8, (w-8)/2, (h-8)/2);

    g.setColor(Color.red);
    x *= 3;
    g.drawRoundRect(x, y, w, h, w, h);
  }
}

Back to Top

Drawing Ovals

Drawing ovals works almost exactly like drawing plain rectangles. There are two methods:

drawOval(x, y, width, height);
fillOval(x, y, width, height);

Each of these draws an oval that touches an imaginary “bounding box” represented by coordinates passed as width and height. As with the rectangles, "drawn" ovals are 1-pixel wider and deeper than "filled" ovals. Like rectangles, ovals are drawn using a line that is one-pixel wide. You can simulate thicker lines using a loop like this to draw a ten-pixel thick line:

for (int i = 0; i < 10; i++)
  g.drawOval(x+i, y+i, w-(i*2), h-(i*2));

This scheme works fine for rectangles, but is not that effective for ovals because of the aliasing ["stairstepping" or "jaggies"] that occurs as each one-pixel-wide oval is drawn. An often better solution is to draw two ovals using fillOval(). Draw the larger oval first, using the "line" color. Then draw the inside oval using the background color like this:

g.setColor(lineColor);
g.fillOval(x, y, w, h);
g.setColor(getBackground());
g.fillOval(x+10, y+10, w-(10*2), h-(10*2));

Here's an applet, DrawOvals.java, that uses both methods. The oval on the left uses a loop, while the oval on the right uses the fillOval() technique.
DrawOvals.java
import java.awt.*;
import java.applet.*;

public class DrawOvals extends Applet
{
  public void paint(Graphics g)
  {
    int x=10, y=10;
    int w = (getSize().width / 2) - 20;
    int h = getSize().height - 10;

    // 1. Loop method
    g.setColor(Color.blue);
    for (int i = 0; i < 10; i++)
      g.drawOval(x+i, y+i, w - (i*2), h - (i*2));

    x = getSize().width / 2 + 10;

    // 2. fillOval() method
    g.fillOval(x, y, w, h);
    g.setColor(getBackground());
    g.fillOval(x+10, y+10, w - (10*2), h - (10*2));
  }
}

Back to Top

Polygons

You've already seen the drawPolyline() method, which draws a series of connected line segments. The polygon methods work almost identically to the drawPolyline() method. The difference is that the polygon methods assume you are drawing a closed figure, so the first and last point in the series are automatically connected if required.

The two polygon drawing methods are:

drawPolygon(int[] ax, int[] ay, int n);
fillPolygon(int[] ax, int[] ay, int n);

The fillPolygon() method uses the "odd-even" fill rule to fill the different parts of the polygon when two lines overlap. Both methods ignore any call where n is less than three.

Something to Talk About

Here's a challenge:
  • Download and compile PolyDraw.java. Change the program, so that the fillPolygon() method is used instead of drawPolyline().
  • Each time the paint() method is called, set the Graphics object to a different random color.
Tell us how you did it.

Please continue to the next section of this lesson.

 

Back to Top

 

Content Developed by Stephen Gilbert, Licensed under a Creative Commons License
Published by the Sofia Open Content Initiative
© 2004 Foothill-De Anza Community College District &The William and Flora Hewlett Foundation