Graphics Programming

Most of the programs we have seen involve two classes: an application class and a tester class that contains a main method. A graphics program also consists of two classes: a component and a frame viewer. The component contains the graphics code. The frame viewer contains main, which creates a window (called a frame), creates a component, adds the component to the window, then displays the window.

Frame Viewers

Our frame viewer is simple. Main creates a JFrame, makes it pretty, adds a graphics component, then displays itself.

public class FrameViewer {

   public static final int FRAME_WIDTH = 300;
   public static final int FRAME_HEIGHT = 400;

   public static void main(String[] args) {
      JFrame frame = new JFrame();
      frame.setSize(FRAME_WIDTH, FRAME_HEIGHT);
      frame.setTitle("Frame Viewer");
      frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
      // create custom component:
      DemoComponent component = new DemoComponent();
      frame.add(component);
      frame.setVisible(true);
   }
}

Components

Every time the operating system decides a window needs to be redrawn, it calls the paintComponent method of every component contained in the window. The paintComponent method paints a picture of the component in the window. So the trick is to redefine the paintComponent method of one of these components.

To redefine a method from an existing class we define a new class that extends the existing class and define a method with the same name.

class DemoComponent extends JComponent {

   public void paintComponent(Graphics gc) {
      gc.setColor(Color.RED);
      gc.fillRect(10, 20, 200, 100);
      gc.setColor(Color.BLUE);
      gc.fillOval(50, 50, 100, 100);
   }
}

Remember, the operating system will call this method, not the program. The operating system will supply the input, gc, a graphical context. A graphical context is like a virtual artist's studio. It contains a palette of colors, a pen to draw lines, a brush to paint regions, stencils to draw shapes, and a blank canvas with a coordinate system:

To make all of these things work we need the following import statements:

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

Here's how the window looks when it's displayed:

 

2D Graphical Contexts and Shapes

The canvas provided by the graphical context has integer coordinates that map points on the canvas directly onto pixels in the display device. We call this canvas "device space".

A more sophisticated approach is to work on a canvas with real number coordinates. We call this canvas "user space". When it's time to display user space, its coordinates are automatically mapped onto device space coordinates.

Java's Graphics2D class provides canvases with user space coordinates. In fact, the graphical context passed to the paintComponent method really is a Graphics2D object and only needs to be retyped using a cast operation:

class DemoComponent extends JComponent {
   public void paintComponent(Graphics gc) {
      Graphics2D gc2d = (Graphics2D)gc;
      // graphics code goes here
   }
}

The main methods supplied by Graphics2D class are draw and fill:

public class Graphics2D {
   // plots shape s in user space:
   public void draw(Shape s) { ... }
   // plots shape s in user shape then fills it:
   public void fill(Shape s) { ... }
}

There are many types of shapes: Ellipses, Rectangles, Polygons, Lines, Curves. Each of these shapes comes in two flavors: Double and Float. Double shapes assume the coordinates of user space are given by doubles, while Float shapes assume coordinates are floats.

For example:

class DemoComponent extends JComponent {

   public void paintComponent(Graphics gc) {
      Graphics2D gc2d = (Graphics2D)gc;
      Rectangle2D.Double box =
         new Rectangle2D.Double(3.14, 2.5, 73.1, 34.6);
      gc2d.draw(box);
   }
}

Custom Shapes

We can follow this pattern and define our own shapes. For example, let's define a mouse shape. Like all other shapes, the size and location of a mouse shape will be specified by its bounding box (the smallest rectangle that can hold the shape.)  We will assume the bounding box is actually a square:

Here is our component code:

public class DemoComponent extends JComponent {
   private MouseShape mouse1;
   private MouseShape mouse2;
   private MouseShape mouse3;
   public DemoComponent() {
      mouse1 = new MouseShape(10, 10, 100);
      mouse2 = new MouseShape(10, 100, 100);
      mouse3 = new MouseShape(10, 200, 100);
   }

   public void paintComponent(Graphics gc) {
      Graphics2D gc2d = (Graphics2D)gc;
      mouse1.draw(gc2d);
      mouse2.draw(gc2d);
      mouse3.draw(gc2d);
   }
}

Notice that the constructor creates three mouse shapes. Each has the same size; each has the same x coordinate. Here is what will be displayed:

Here is our MouseShape class:

public class MouseShape {

   private int xc, yc, width;
   private Ellipse2D.Double leftEar, rightEar, face;
   private Color faceColor;

   public MouseShape(int x, int y, int w) {
      xc = x;
      yc = y;
      width = w;
      int earWidth = width/3;
      int faceWidth = width - earWidth;
      leftEar = new Ellipse2D.Double(xc, yc, earWidth, earWidth);
      rightEar = new Ellipse2D.Double(xc + faceWidth, yc, earWidth, earWidth);
      face = new Ellipse2D.Double(xc + earWidth/2, yc + earWidth/2, faceWidth, faceWidth);
      faceColor = Color.RED;
   }
   public void draw(Graphics2D gc) {
      gc.fill(leftEar);
      gc.fill(rightEar);
      Color oldColor = gc.getColor();
      gc.setColor(faceColor);
      gc.fill(face);
      gc.setColor(oldColor);
   }
}

Naturally, the size and position of the bounding box are fields. In addition, the face, ears, and face color are fields. This makes the code more modular, hence easier to understand and modify.

The size and position of the face and ears are calculated relative to the size and position of the bounding box. We decide by trial and error that the diameter of an ear should be one third the width of the bounding box and that the diameter of the face should be two-thirds the width of the bounding box.

The positions of the ears and face are determined by making test drawings on graph paper.

Notice that the old color is restored after the face is drawn. Remember, your code may be sharing a canvas with otehr code, and should therefore leave the virtual studio in its original condition.

A Procedure for drawing shapes

Step 1

Use a piece of graph paper to represent the bounding box of your shape. Draw the shape on this paper using only ellipses, lines, and rectangles. Use this drawing to determine the sizes and positions of these component shapes relative to the graph paper.

Step 2

Create a shape class:

public class MyShape {
   private int xc, yc, width, height;
   // fields holding component shapes can be defined here
   public MyShape(int x, int y, int w, int h) {
      xc = x;
      yc = y;
      width = w;
      height = h;
      // initialize component shape fields here
   }
   public draw(Graphics2D gc) {
      Color oldColor = gc.getColor();
      // plot each component shape using gc2d.draw or gc2d.fill
      gc.setColor(oldColor);
   }
}

Initialize each shape relative to xc, yc, width, and height. Plot them in the draw method.

Step 3

Implement the component class:

public class MyComponent extends JComponent {
   public void paintComponent(Graphics gc) {
      Graphics2D gc2d = (Graphics2D)gc;
      // for example:
      MyShape shape = new MyShape(10, 20, 30, 40);
      shape.draw(gc2d);
      // etc.
   }
}

Step 4

Implement the viewer class:

public class FrameViewer {

   public static final int FRAME_WIDTH = 300;
   public static final int FRAME_HEIGHT = 400;

   public static void main(String[] args) {
      JFrame frame = new JFrame();
      frame.setSize(FRAME_WIDTH, FRAME_HEIGHT);
      frame.setTitle("Frame Viewer");
      frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
      MyComponent component = new MyComponent();
      frame.add(component);
      frame.setVisible(true);
   }
}