1.10 Performing Custom Painting

«« Previous
Next »»

This lesson describes custom painting in Swing. Many programs will get by just fine without writing their own painting code; they will simply use the standard GUI components that are already available in the Swing API. But if you need specific control over how your graphics are drawn, then this lesson is for you. We will explore custom painting by creating a simple GUI application that draws a shape in response to the user's mouse activity. By intentionally keeping its design simple, we can focus on the underlying painting concepts, which in turn will relate to other GUI applications that you develop in the future.

This lesson explains each concept in steps as you construct the demo application. It presents the code as soon as possible with a minimum amount of background reading. Custom painting in Swing is similar to custom painting in AWT, but since we do not recommend writing your applications entirely with the AWT, its painting mechanism is not specifically discussed here.

1. Creating the Demo Application (Step 1)


All Graphical User Interfaces require some kind of main application frame in which to display. In Swing, this is an instance of javax.swing.JFrame. Therefore, our first step is to instantiate this class and make sure that everything works as expected. Note that when programming in Swing, your GUI creation code should be placed on the Event Dispatch Thread (EDT). This will prevent potential race conditions that could lead to deadlock. The following code listing shows how this is done.

An Instance of javax.swing.JFrame

Click the Launch button to run SwingPaintDemo1 using Java™ Web Start (download JDK 7 or later).

package painting;

import javax.swing.SwingUtilities;
import javax.swing.JFrame;

public class SwingPaintDemo1 {
    
    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                createAndShowGUI();
            }
        });
    }
    
    private static void createAndShowGUI() {
        System.out.println("Created GUI on EDT? "+
                SwingUtilities.isEventDispatchThread());
        JFrame f = new JFrame("Swing Paint Demo");
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.setSize(250,250);
        f.setVisible(true);
    }
}
This creates the frame, sets its title, and makes everything visible. We have used the SwingUtilities helper class to construct this GUI on the Event Dispatch Thread. Note that by default, a JFrame does not exit the application when the user clicks its "close" button. We provide this behavior by invoking the setDefaultCloseOperation method, passing in the appropriate argument. Also, we are explicity setting the frame's size to 250 x 250 pixels. This step will not be necessary once we start adding components to the frame.

Exercises:

1. Compile and run the application.
2. Test the minimize and maximize buttons.
3. Click the close button (the application should exit.)

2. Creating the Demo Application (Step 2)


Next, we will add a custom drawing surface to the frame. For this we will create a subclass of javax.swing.JPanel (a generic lightweight container) which will supply the code for rendering our custom painting.

A javax.swing.JPanel Subclass

Click the Launch button to run SwingPaintDemo2 using Java™ Web Start (download JDK 7 or later)

package painting;

import javax.swing.SwingUtilities;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.BorderFactory;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;

public class SwingPaintDemo2 {
   
    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                createAndShowGUI(); 
            }
        });
    }

    private static void createAndShowGUI() {
        System.out.println("Created GUI on EDT? "+
        SwingUtilities.isEventDispatchThread());
        JFrame f = new JFrame("Swing Paint Demo");
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.add(new MyPanel());
        f.pack();
        f.setVisible(true);
    }
}

class MyPanel extends JPanel {

    public MyPanel() {
        setBorder(BorderFactory.createLineBorder(Color.black));
    }

    public Dimension getPreferredSize() {
        return new Dimension(250,200);
    }

    public void paintComponent(Graphics g) {
        super.paintComponent(g);       

        // Draw Text
        g.drawString("This is my custom Panel!",10,20);
    }  
}

The first change you will notice is that we are now importing a number of additional classes, such as JPanel, Color, and Graphics. Since some of the older AWT classes are still used in modern Swing applications, it is normal to see the java.awt package in a few of the import statements. We have also defined a custom JPanel subclass, called MyPanel, which comprises the majority of the new code.

The MyPanel class definition has a constructor that sets a black border around its edges. This is a subtle detail that might be difficult to see at first (if it is, just comment out the invocation of setBorder and then recompile.) MyPanel also overrides getPreferredSize, which returns the desired width and height of the panel (in this case 250 is the width, 200 is the height.) Because of this, the SwingPaintDemo class no longer needs to specify the size of the frame in pixels. It simply adds the panel to the frame and then invokes pack.

The paintComponent method is where all of your custom painting takes place. This method is defined by javax.swing.JComponent and then overridden by your subclasses to provide their custom behavior. Its sole parameter, a java.awt.Graphics object, exposes a number of methods for drawing 2D shapes and obtaining information about the application's graphics environment. In most cases the object that is actually received by this method will be an instance of java.awt.Graphics2D (a Graphics subclass), which provides support for sophisticated 2D graphics rendering.

Most of the standard Swing components have their look and feel implemented by separate "UI Delegate" objects. The invocation of super.paintComponent(g) passes the graphics context off to the component's UI delegate, which paints the panel's background. For a closer look at this process, see the section entitled "Painting and the UI Delegate" in the aforementioned SDN article.

Exercises:

1. Now that you have drawn some custom text to the screen, try minimizing and restoring the application as you did before.
2. Obscure a part of the text with another window, then move that window out of the way to re-expose the custom text. In both cases, the painting subsystem will determine that the component is damaged and will ensure that your paintComponent method is invoked.

3. Creating the Demo Application (Step 3)


Finally, we will add the event-handling code to programatically repaint the component whenever the user clicks or drags the mouse. To keep our custom painting as efficient as possible, we will track the mouse coordinates and repaint only the areas of the screen that have changed. This is a recommended best practice that will keep your application running as efficiently as possible.

The Completed Application

Figure 3: The Completed Application

Click the Launch button to run SwingPaintDemo3 using Java™ Web Start (download JDK 7 or later)

package painting;

import javax.swing.SwingUtilities;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.BorderFactory;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseMotionAdapter;

public class SwingPaintDemo3 {
    
    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                createAndShowGUI(); 
            }
        });
    }

    private static void createAndShowGUI() {
        System.out.println("Created GUI on EDT? "+
        SwingUtilities.isEventDispatchThread());
        JFrame f = new JFrame("Swing Paint Demo");
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 
        f.add(new MyPanel());
        f.pack();
        f.setVisible(true);
    } 
}

class MyPanel extends JPanel {

    private int squareX = 50;
    private int squareY = 50;
    private int squareW = 20;
    private int squareH = 20;

    public MyPanel() {

        setBorder(BorderFactory.createLineBorder(Color.black));

        addMouseListener(new MouseAdapter() {
            public void mousePressed(MouseEvent e) {
                moveSquare(e.getX(),e.getY());
            }
        });

        addMouseMotionListener(new MouseAdapter() {
            public void mouseDragged(MouseEvent e) {
                moveSquare(e.getX(),e.getY());
            }
        });
        
    }
    
    private void moveSquare(int x, int y) {
        int OFFSET = 1;
        if ((squareX!=x) || (squareY!=y)) {
            repaint(squareX,squareY,squareW+OFFSET,squareH+OFFSET);
            squareX=x;
            squareY=y;
            repaint(squareX,squareY,squareW+OFFSET,squareH+OFFSET);
        } 
    }
    

    public Dimension getPreferredSize() {
        return new Dimension(250,200);
    }
    
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);       
        g.drawString("This is my custom Panel!",10,20);
        g.setColor(Color.RED);
        g.fillRect(squareX,squareY,squareW,squareH);
        g.setColor(Color.BLACK);
        g.drawRect(squareX,squareY,squareW,squareH);
    }  
}

This change first imports the various mouse classes from the java.awt.event package, making the application capable of responding to the user's mouse activity. The constructor has been updated to register event listeners for mouse presses and drags. Whenever a MouseEvent received, it is forwarded to the moveSquare method, which updates the square's coordinates and repaints the component in an intelligent manner. Note that by default, any code that is placed within these event handlers will be executed on the Event Dispatch Thread.

But the most important change is the invocation of the repaint method. This method is defined by java.awt.Component and is the mechanism that allows you to programatically repaint the surface of any given component. It has a no-arg version (which repaints the entire component) and a multi-arg version (which repaints only the specified area.) This area is also known as the clip. Invoking the multi-arg version of repaint takes a little extra effort, but guarantees that your painting code will not waste cycles repainting areas of the screen that have not changed.

Because we are manually setting the clip, our moveSquare method invokes the repaint method not once, but twice. The first invocation tells Swing to repaint the area of the component where the square previously was (the inherited behavior uses the UI Delegate to fill that area with the current background color.) The second invocation paints the area of the component where the square currently is. An important point worth noting is that although we have invoked repaint twice in a row in the same event handler, Swing is smart enough to take that information and repaint those sections of the screen all in one single paint operation. In other words, Swing will not repaint the component twice in a row, even if that is what the code appears to be doing.

Exercises:

1. Comment out the first invocation of repaint and note what happens when you click or drag the mouse. Because that line is responsible for filling in the background, you should notice that all squares remain on screen after they are painted.
2. With multiple squares on screen, minimize and restore the application frame. What happens? You should notice that the act of maximizing the screen causes the system to entirely repaint the components surface, which will erase all squares except the current one.
3. Comment out both invocations of repaint, and add a line at the end of the paintComponent method to invoke the zero-arg version of repaint instead. The application will appear to be restored to its original behavior, but painting will now be less efficient since the entire surface area of the component is now being painted. You may notice slower performace, especially if the application is maximized.

4. Refining the Design


For demonstration purposes it makes sense to keep the painting logic entirely contained within the MyPanel class. But if your application will need to track multiple instances, one pattern that you could use is to factor that code out into a separate class so that each square can be treated as an individual object. This technique is common in 2D game programming and is sometimes referred to as "sprite animation."

Click the Launch button to run SwingPaintDemo4 using Java™ Web Start (download JDK 7 or later).

package painting;

import javax.swing.SwingUtilities;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.BorderFactory;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics; 
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseMotionAdapter;

public class SwingPaintDemo4 {
    
    public static void main(String[] args) {

        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                createAndShowGUI(); 
            }
        });
    }

    private static void createAndShowGUI() {
        System.out.println("Created GUI on EDT? "+
        SwingUtilities.isEventDispatchThread());
        JFrame f = new JFrame("Swing Paint Demo");
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 
        f.add(new MyPanel());
        f.setSize(250,250);
        f.setVisible(true);
    } 

}

class MyPanel extends JPanel {

    RedSquare redSquare = new RedSquare();

    public MyPanel() {

        setBorder(BorderFactory.createLineBorder(Color.black));

        addMouseListener(new MouseAdapter(){
            public void mousePressed(MouseEvent e){
                moveSquare(e.getX(),e.getY());
            }
        });

        addMouseMotionListener(new MouseAdapter(){
            public void mouseDragged(MouseEvent e){
                moveSquare(e.getX(),e.getY());
            }
        });

    }

    private void moveSquare(int x, int y){

        // Current square state, stored as final variables 
        // to avoid repeat invocations of the same methods.
        final int CURR_X = redSquare.getX();
        final int CURR_Y = redSquare.getY();
        final int CURR_W = redSquare.getWidth();
        final int CURR_H = redSquare.getHeight();
        final int OFFSET = 1;

        if ((CURR_X!=x) || (CURR_Y!=y)) {

            // The square is moving, repaint background 
            // over the old square location. 
            repaint(CURR_X,CURR_Y,CURR_W+OFFSET,CURR_H+OFFSET);

            // Update coordinates.
            redSquare.setX(x);
            redSquare.setY(y);

            // Repaint the square at the new location.
            repaint(redSquare.getX(), redSquare.getY(), 
                    redSquare.getWidth()+OFFSET, 
                    redSquare.getHeight()+OFFSET);
        }
    }

    public Dimension getPreferredSize() {
        return new Dimension(250,200);
    }
    
    public void paintComponent(Graphics g) {
        super.paintComponent(g);       
        g.drawString("This is my custom Panel!",10,20);

        redSquare.paintSquare(g);
    }  
}

class RedSquare{

    private int xPos = 50;
    private int yPos = 50;
    private int width = 20;
    private int height = 20;

    public void setX(int xPos){ 
        this.xPos = xPos;
    }

    public int getX(){
        return xPos;
    }

    public void setY(int yPos){
        this.yPos = yPos;
    }

    public int getY(){
        return yPos;
    }

    public int getWidth(){
        return width;
    } 

    public int getHeight(){
        return height;
    }

    public void paintSquare(Graphics g){
        g.setColor(Color.RED);
        g.fillRect(xPos,yPos,width,height);
        g.setColor(Color.BLACK);
        g.drawRect(xPos,yPos,width,height);  
    }
}

In this particular implementation we have created a RedSquare class entirely from scratch. Another approach would be to reuse the functionlity of java.awt.Rectangle by making the RedSquare a subclass of it. Regardless of how RedSquare is implemented, the important point is that we have given the class a method that accepts a Graphics object, and that method is invoked from the panel's paintComponent method. This separation keeps your code clean because it essentially tells each red square to paint itself.

5. A Closer Look at the Paint Mechanism


By now you know that the paintComponent method is where all of your painting code should be placed. It is true that this method will be invoked when it is time to paint, but painting actually begins higher up the class heirarchy, with the paint method (defined by java.awt.Component.) This method will be executed by the painting subsystem whenever you component needs to be rendered. Its signature is:

◈ public void paint(Graphics g)

javax.swing.JComponent extends this class and further factors the paint method into three separate methods, which are invoked in the following order:

◈ protected void paintComponent(Graphics g)
◈ protected void paintBorder(Graphics g)
◈ protected void paintChildren(Graphics g)

The API does nothing to prevent your code from overriding paintBorder and paintChildren, but generally speaking, there is no reason for you to do so. For all practical purposes paintComponent will be the only method that you will ever need to override.

As previously mentioned, most of the standard Swing components have their look and feel implemented by separate UI Delegates. This means that most (or all) of the painting for the standard Swing components proceeds as follows.

1. paint() invokes paintComponent().
2. If the ui property is non-null, paintComponent() invokes ui.update().
3. If the component's opaque property is true, ui.update() fills the component's background with the background color and invokes ui.paint().
4. ui.paint() renders the content of the component.

This is why our SwingPaintDemo code invokes super.paintComponent(g). We could add an additional comment to make this more clear:

public void paintComponent(Graphics g) {
    // Let UI Delegate paint first, which 
    // includes background filling since 
    // this component is opaque.

    super.paintComponent(g);       
    g.drawString("This is my custom Panel!",10,20);
    redSquare.paintSquare(g);
}  

If you have understood all of the demo code provided in this lesson, congratulations! You have enough practical knowledge to write efficient painting code in your own applications. If however you want a closer look "under the hood", please refer to the SDN article linked to from the first page of this lesson.

6. Summary


  1. In Swing, painting begins with the paint method, which then invokes paintComponent, paintBorder, and paintChildren. The system will invoke this automatically when a component is first painted, is resized, or becomes exposed after being hidden by another window.
  2. Programatic repaints are accomplished by invoking a component's repaint method; do not invoke its paintComponent directly. Invoking repaint causes the painting subsystem to take the necessary steps to ensure that your paintComponent method is invoked at an appropriate time.
  3. The multi-arg version of repaint allows you to shrink the component's clip rectangle (the section of the screen that is affected by painting operations) so that painting can become more efficient. We utilized this technique in the moveSquare method to avoid repainting sections of the screen that have not changed. There is also a no-arg version of this method that will repaint the component's entire surface area.
  4. Because we have shrunk the clip rectangle, our moveSquare method invokes repaint not once, but twice. The first invocation repaints the area of the component where the square previously was (the inherited behavior is to fill the area with the current background color.) The second invocation paints the area of the component where the square currently is.
  5. You can invoke repaint multiple times from within the same event handler, but Swing will take that information and repaint the component in just one operation.
  6. For components with a UI Delegate, you should pass the Graphics paramater with the line super.paintComponent(g) as the first line of code in your paintComponent override. If you do not, then your component will be responsible for manually painting its background. You can experiment with this by commenting out that line and recompiling to see that the background is no longer painted.
  7. By factoring out our new code into a separate RedSquare class, the application maintains an object-oriented design, which keeps the paintComponent method of the MyPanel class free of clutter. Painting still works because we have passed the Graphics object off to the red square by invoking its paintSquare(Graphics g) method. Keep in mind that the name of this method is one that we have created from scratch; we are not overriding paintSquare from anywhere higher up in the Swing API.
«« Previous
Next »»