Drawing Techniques

Now that we’ve learned about the basic tools, let’s put a few of them together. In this section, we’ll look at some techniques for doing fast and flicker-free drawing and painting. If you’re interested in animation, this is for you. Drawing operations take time, and time spent drawing leads to delays and imperfect results. Our goals are to minimize the amount of drawing work we do and, as much as possible, to do that work away from the eyes of the user. To do this, we use two techniques: clipping and double buffering. Fortunately, Swing now handles double buffering by default. You won’t have to implement this logic on your own, but it’s useful to understand it.

Our first example, DragImage, illustrates some of the issues in updating a display. Like many animations, it has two parts: a constant background and a changing object in the foreground. In this case, the background is a checkerboard pattern, and the object is a small, scaled that image we can drag around on top of it, as shown in Figure 20-4:

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

    public class DragImage extends JComponent
        implements MouseMotionListener
    {
      static int imageWidth=60, imageHeight=60;
      int grid = 10;
      int imageX, imageY;
      Image image;

      public DragImage(Image i) {
        image = i;
        addMouseMotionListener(this);
      }

      public void mouseDragged(MouseEvent e) {
        imageX = e.getX();
        imageY = e.getY();
        repaint();
      }
      public void mouseMoved(MouseEvent e) {}

      public void paint(Graphics g) {
        Graphics2D g2 = (Graphics2D)g;

        int w = getSize().width / grid;
        int h = getSize().height / grid;
        boolean black = false;
        for (int y = 0; y <= grid; y++)
          for (int x = 0; x <= grid; x++) {
            g2.setPaint(black ? Color.black : Color.white);
            black = !black;
            g2.fillRect(x * w, y * h, w, h);
          }
        g2.drawImage(image, imageX, imageY, this);
      }

      public static void main(String[] args) {
        String imageFile = "L1-Light.jpg";
        if (args.length > 0)
          imageFile = args[0];

        // Turn off double buffering
        //RepaintManager.currentManager(null).setDoubleBufferingEnabled(false);

        Image image = Toolkit.getDefaultToolkit().getImage(
            DragImage.class.getResource(imageFile));
        image = image.getScaledInstance(
            imageWidth,imageHeight,Image.SCALE_DEFAULT);
        JFrame frame = new JFrame("DragImage");
        frame.add( new DragImage(image) );
        frame.setSize(300, 300);
        frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
        frame.setVisible(true);
      }
    }
The DragImage application

Figure 20-4. The DragImage application

Run the application, optionally specifying an image file as a command-line argument. Then try dragging the image around on the pattern.

DragImage is a custom component that overrides the JComponent paint() method to do its drawing. In the main() method, we load the image and prescale it to improve performance. We then create the DragImage component and place it in the content pane. As the mouse is dragged, DragImage keeps track of its most recent position in two instance variables, imageX and imageY. On each call to mouseDragged(), the coordinates are updated, and repaint() is called to ask that the display be updated. When paint() is called, it looks at some parameters, draws the checkerboard pattern to fill the applet’s area and finally paints the small version of the image at the latest coordinates.

Now for a few arcane details about differences between JComponent and a plain AWT Component. First, the default JComponent update() method simply calls our paint() method. Prior to Java 1.4, the AWT Component class’s default update() method first cleared the screen area using a clearRect() call before calling paint. Remember that the difference between paint() and update() is that paint() draws the entire area and update() assumes the screen region is intact from the last draw. In AWT, update() was overly conservative; in Swing, it’s more optimistic. This is noteworthy if you are working with an older AWT-based application. In that case, you can simply override update() to call paint().

A more important difference between AWT and Swing is that Swing components by default perform double buffering of the output of their paint() method.

Double Buffering

Double buffering means that instead of drawing directly on the screen, Swing first performs drawing operations in an offscreen buffer and then copies the completed work to the display in a single painting operation, as shown in Figure 20-5. It takes the same amount of time to do the drawing work, but once it’s done, double buffering instantaneously updates our display so that the user does not perceive any flickering or progressively rendered output.

You’ll see how to implement this technique yourself when we use an offscreen buffer later in this chapter. However, Swing does this kind of double buffering for you whenever you use a Swing component in a Swing container. AWT components do not have automatic double buffering capability.

It is interesting to take our example and turn off double buffering to see the effect. Each Swing JComponent has a method called setDoubleBuffered() that can be set to false in order to disable the technique. Or you can disable it for all components using a call to the Swing RepaintManager, as we’ve indicated in comments in the example. Try uncommenting that line of DragImage and observe the difference in appearance.

Double buffering

Figure 20-5. Double buffering

The difference is most dramatic when you are using a slow system or performing complex drawing operations. Double buffering eliminates all of the flickering. However, on a slow system, it can decrease performance noticeably. In extreme cases (such as a game), it may be beneficial to provide an option to disable double buffering.

Our example is pretty fast, but we’re still doing some wasted drawing. Most of the background stays the same each time it’s painted. You might try to make paint() smarter, so that it wouldn’t redraw these areas, but remember that paint() has to be able to draw the entire scene because it might be called in situations when the display isn’t intact. The solution is to draw only part of the picture whenever the mouse moves. Next, we’ll talk about clipping.

Limiting Drawing with Clipping

Whenever the mouse is dragged, DragImage responds by updating its coordinates and calling repaint(). But repaint() by default causes the entire component to be redrawn. Most of this drawing is unnecessary. It turns out that there’s another version of repaint() that lets you specify a rectangular area that should be drawn—in essence, a clipping region.

Why does it help to restrict the drawing area? Foremost, drawing operations that fall outside the clipping region are not displayed. If a drawing operation overlaps the clipping region, we see only the part that’s inside. A second effect is that, in a good implementation, the graphics context can recognize drawing operations that fall completely outside the clipping region and ignore them altogether. Eliminating unnecessary operations can save time if we’re doing something complex, such as filling a bunch of polygons. This doesn’t save the time our application spends calling the drawing methods, but the overhead of calling these kinds of drawing methods is usually negligible compared to the time it takes to execute them. (If we were generating an image pixel by pixel, this would not be the case, as the calculations would be the major time sink, not the drawing.)

So we can save some time in our application by redrawing only the affected portion of the display. We can pick the smallest rectangular area that includes both the old image position and the new image position, as shown in Figure 20-6. This is the only portion of the display that really needs to change; everything else stays the same.

Determining the clipping region

Figure 20-6. Determining the clipping region

A smarter algorithm could save even more time by redrawing only those regions that have changed. However, the simple clipping strategy we’ve implemented here can be applied to many kinds of drawing and gives good performance, particularly if the area being changed is small.

One important thing to note is that, in addition to looking at the new position, our updating operation now has to remember the last position at which the image was drawn. Let’s fix our application so it will use a specified clipping region. To keep this short and emphasize the changes, we’ll take some liberties with design and make our next example a subclass of DragImage. Let’s call it ClippedDragImage.

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

    public class ClippedDragImage extends DragImage {
      int oldX, oldY;

      public ClippedDragImage( Image i ) { super(i); }

      public void mouseDragged(MouseEvent e) {
        imageX = e.getX();
        imageY = e.getY();
        Rectangle r = getAffectedArea(
            oldX, oldY, imageX, imageY, imageWidth, imageHeight);
        repaint(r);  // repaint just the affected part of the component
        oldX = imageX;
        oldY = imageY;
      }

      private Rectangle getAffectedArea(
        int oldx, int oldy, int newx, int newy, int width, int height)
      {
        int x = Math.min(oldx, newx);
        int y = Math.min(oldy, newy);
        int w = (Math.max(oldx, newx) + width) - x;
        int h = (Math.max(oldy, newy) + height) - y;
        return new Rectangle(x, y, w, h);
      }

      public static void main(String[] args) {
        String imageFile = "L1-Light.jpg";
        if (args.length > 0)
          imageFile = args[0];

        // Turn off double buffering
        //RepaintManager.currentManager(null).setDoubleBufferingEnabled(false);

        Image image = Toolkit.getDefaultToolkit().getImage(
            ClippedDragImage.class.getResource(imageFile));
        image = image.getScaledInstance(
            imageWidth,imageHeight,Image.SCALE_DEFAULT);
        JFrame frame = new JFrame("ClippedDragImage");
        frame.add( new ClippedDragImage(image) );
        frame.setSize(300, 300);
        frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
        frame.setVisible(true);
      }
    }

You may or may not find that ClippedDragImage is significantly faster. Modern desktop computers are so fast that this kind of operation is child’s play for them. However, the fundamental technique is important and applicable to more sophisticated applications.

What have we changed? First, we’ve overridden mouseDragged() so that instead of setting the current coordinates of the image, it figures out the area that has changed by using a new private method. getAffectedArea() takes the new and old coordinates and the width and height of the image as arguments. It determines the bounding rectangle as shown in Figure 20-6, then calls repaint() to draw only the affected area of the screen. mouseDragged() also saves the current position by setting the oldX and oldY variables.

Try turning off double buffering on this example and compare it to the unbuffered previous example to see how much less work is being done. You probably won’t see the difference; computers are just too fast nowadays. If you were using the 2D API to do some fancy rendering, it might help a lot.

Offscreen Drawing

In addition to serving as buffers for double buffering, offscreen images are useful for saving complex, hard-to-produce, background information. We’ll look at a fun, simple example: the doodle pad. DoodlePad is a simple drawing tool that lets us scribble by dragging the mouse, as shown in Figure 20-7. It draws into an offscreen image; its paint() method simply copies the image to the display area.

    //file: DoodlePad.java
    import java.awt.*;
    import java.awt.event.*;
    import javax.swing.*;

    public class DoodlePad
    {
      public static void main(String[] args)
      {
        JFrame frame = new JFrame("DoodlePad");
        frame.setLayout(new BorderLayout());
        final DrawPad drawPad = new DrawPad();
        frame.add(drawPad, BorderLayout.CENTER);
        JPanel panel = new JPanel();
        JButton clearButton = new JButton("Clear");
        clearButton.addActionListener(new ActionListener() {
          public void actionPerformed(ActionEvent e) {
            drawPad.clear();
          }
        });
        panel.add(clearButton);
        frame.add(panel, BorderLayout.SOUTH);
        frame.setSize(280, 300);
        frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
        frame.setVisible(true);
      }

    } // end of class DoodlePad

    class DrawPad extends JComponent
    {
      Image image;
      Graphics2D graphics2D;
      int currentX, currentY, oldX, oldY;

      public DrawPad() {
        setDoubleBuffered(false);
        addMouseListener(new MouseAdapter() {
          public void mousePressed(MouseEvent e) {
            oldX = e.getX();
            oldY = e.getY();
          }
        });
        addMouseMotionListener(new MouseMotionAdapter() {
          public void mouseDragged(MouseEvent e) {
            currentX = e.getX();
            currentY = e.getY();
            if (graphics2D != null)
              graphics2D.drawLine(oldX, oldY, currentX, currentY);
            repaint();
            oldX = currentX;
            oldY = currentY;
          }
        });
      }

      public void paintComponent(Graphics g) {
        if (image == null) {
          image = createImage(getSize().width, getSize().height);
          graphics2D = (Graphics2D)image.getGraphics();
          graphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
              RenderingHints.VALUE_ANTIALIAS_ON);
          clear();
        }
        g.drawImage(image, 0, 0, null);
      }

      public void clear() {
        graphics2D.setPaint(Color.white);
        graphics2D.fillRect(0, 0, getSize().width, getSize().height);
        graphics2D.setPaint(Color.black);
        repaint();
      }
    }
The DoodlePad application

Figure 20-7. The DoodlePad application

Give it a try. Draw a nice moose or a sunset. We just drew a lovely cartoon of Bill Gates. If you make a mistake, hit the Clear button and start over.

The parts should be familiar by now. We made a type of JComponent called DrawPad. The new DrawPad component uses inner classes to supply handlers for the MouseListener and MouseMotionListener interfaces. We used the JComponent createImage() method to create an empty offscreen image buffer to hold our scribble. Mouse-dragging events trigger us to draw lines into the offscreen image and call repaint() to update the display. DrawPad’s paint() method does a drawImage() to copy the offscreen drawing area to the display. In this way, DrawPad saves our sketch information.

What is unusual about DrawPad is that it does some drawing outside of paint(). In this example, we want to let the user scribble with the mouse, so we should respond to every mouse movement. Therefore, we do our work, drawing to the offscreen buffer in mouseDragged() itself. As a rule, we should be careful about doing heavy work in event-handling methods because we don’t want to interfere with other tasks that the windowing system’s painting thread is performing. In this case, our line drawing option should not be a burden, and our primary concern is getting as close a coupling as possible between the mouse movement events and the sketch on the screen. A more elaborate example might push coordinates into a queue for some other drawing thread to consume, thus freeing up the event handler thread.

In addition to drawing a line as the user drags the mouse, the mouseDragged() handler maintains a pair of previous coordinates to be used as a starting point for the next line segment. The mousePressed() handler resets the previous coordinates to the current mouse position whenever the user moves the mouse. Finally, DrawPad provides a clear() method that clears the offscreen buffer and calls repaint() to update the display. The DoodlePad application ties the clear() method to an appropriately labeled button through another anonymous inner class.

What if we wanted to do something with the image after the user has finished scribbling on it? As we’ll see in the next chapter, we could get the pixel data for the image and work with that. It wouldn’t be hard to create a save facility that stores the pixel data and reproduces it later. Think about how you might go about creating a networked “bathroom wall,” where people could scribble on your web pages.

Get Learning Java, 4th Edition now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.