Printing Swing Components in Java 1.2

Background

Java 1.2 now supports high-quality printing via classes in the java.awt.print package. This differs from Java 1.1 printing in that you can obtain high-resolution results (not just blown up screen shots) and that you have much better control over the print jobs. This section of the Quick Swing Tutorial for AWT Programmers describes how to use this printing package to print Swing components. In particular, it illustrates two important concepts not sufficiently described in the API or in any of the other articles, book chapters, or on-line tutorials on Java 1.2 printing that I've seen:

Printing Basics

There are two basic steps required to print: settting up the print job, and rendering graphics on the printer.
  1. Setting Up the Print Job
    This is virtually always done the same way: get a PrinterJob object, pass a Printable to its setPrintable method, call printDialog to pop up an OS-specific print dialog, and finally, assuming the user hasn't cancelled the printing from the dialog (you check this by testing the return value of printDialog), call print on the PrinterJob.

    Note that the role of the Printable is to define a print method showing how the actual drawing to the printer will be done. That's described in the next section.

    Here's an example:

        PrinterJob printJob = PrinterJob.getPrinterJob();
        printJob.setPrintable(this);
        if (printJob.printDialog())
          try { 
            printJob.print();
          } catch(PrinterException pe) {
            System.out.println("Error printing: " + pe);
          }
    Here's a representative sample of what you see when you call printDialog:
    Windows Print Dialog
  2. Rendering Graphics on the Printer
    The Printable that is passed to setPrintable must have a print method that describes how to send drawing to the printer.

    In general, the first step is to decide what to do for different pages of your print job, since Java repeatedly calls print with higher and higher page indexes until print returns NO_SUCH_PAGE. In the specific case of printing Swing components, however, you only have a single page. So you return PAGE_EXISTS for index 0, NO_SUCH_PAGE otherwise.

    The second step is to start drawing. In general, you can do any sort of drawing you want. In the specific case of printing Swing components, however, your drawing should just be a high-resolution version of what the component looks like on the screen. So you cast the Graphics object to Graphics2D, scale the resolution to the printer, and call the component's paint method with this scaled Graphics2D. Note that it is this reliance on the Graphics2D's coordinate transformations that prevents this capability from being available in the Java 1.1 version of Swing. For more details on Graphics2D, please see my separate Java2D tutorial.

    Here's an example:

          public int print(Graphics g, PageFormat pageFormat, int pageIndex) {
            if (pageIndex > 0) {
              return(NO_SUCH_PAGE);
            } else {
              Graphics2D g2d = (Graphics2D)g;
              g2d.translate(pageFormat.getImageableX(), pageFormat.getImageableY());
              // Turn off double buffering
              componentToBePrinted.paint(g2d);
              // Turn double buffering back on
              return(PAGE_EXISTS);
            }
          }
          

The Role of Double Buffering

With Java Swing, almost all components have double buffering turned on by default. In general, this is a great boon, making for convenient and efficient paintComponent method. However, in the specific case of printing, it can be a huge problem. First, since printing components relies on scaling the coordinate system and then simply calling the component's paint method, if double buffering is enabled printing amounts to little more than scaling up the buffer (off-screen image). This results in ugly low-resolution printing like you already had available. Secondly, sending these huge buffers to the printer results in huge print spooler files which take a very long time to print.

Consequently, you need to make sure double buffering is turned off before you print. If you have only a single JPanel or other JComponent, you can call setDoubleBuffered(false) on it before calling the paint method, and setDoubleBuffered(true) afterwards. However, this suffers from the flaw that if you later nest another container inside, you're right back where you started from. A much better solution is to globally turn off double buffering via

RepaintManager currentManager = 
  RepaintManager.currentManager(c);
currentManager.setDoubleBufferingEnabled(false);
and then to re-enable it after calling paint via the setDoubleBufferingEnabled(true). (Thanks to Bob Evans for suggesting RepaintManager instead of the much uglier recursive descent method I was using to globally disable and re-enable double buffering.) Although this will completely fix the problem with low-resolution printouts, if the components have large, complex filled backgrounds you can still get big spool files and slow printing.

A General-Purpose Component-Printing Routine

When printing Swing components, the role of the print method is nothing more than to scale the Graphics, turn off double buffering, and call paint, there is no particular reason to put that print method in the component being printed. And requiring printable components to directly implement the Printable interface and define a print method makes for cumbersome code and prevents you from printing components that didn't plan for it. A better approach is to put the print method in an arbitrary object and tell that object whose paint method to call back to. This lets you build a generic printComponent method to which you simply pass the component you want printed.

PrintUtilities.java (Download source code)

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

public class PrintUtilities implements Printable {
  private Component componentToBePrinted;

  public static void printComponent(Component c) {
    new PrintUtilities(c).print();
  }
  
  public PrintUtilities(Component componentToBePrinted) {
    this.componentToBePrinted = componentToBePrinted;
  }
  
  public void print() {
    PrinterJob printJob = PrinterJob.getPrinterJob();
    printJob.setPrintable(this);
    if (printJob.printDialog())
      try {
        printJob.print();
      } catch(PrinterException pe) {
        System.out.println("Error printing: " + pe);
      }
  }

  public int print(Graphics g, PageFormat pageFormat, int pageIndex) {
    if (pageIndex > 0) {
      return(NO_SUCH_PAGE);
    } else {
      Graphics2D g2d = (Graphics2D)g;
      g2d.translate(pageFormat.getImageableX(), pageFormat.getImageableY());
      disableDoubleBuffering(componentToBePrinted);
      componentToBePrinted.paint(g2d);
      enableDoubleBuffering(componentToBePrinted);
      return(PAGE_EXISTS);
    }
  }

  public static void disableDoubleBuffering(Component c) {
    RepaintManager currentManager = RepaintManager.currentManager(c);
    currentManager.setDoubleBufferingEnabled(false);
  }

  public static void enableDoubleBuffering(Component c) {
    RepaintManager currentManager = RepaintManager.currentManager(c);
    currentManager.setDoubleBufferingEnabled(true);
  }
}

Printing in Java: An Example

Here's an example that has nested JPanels, one of which has a custom paintComponent method, and another of which contains a JButton. A simple call to PrintUtilities.printComponent prints it.

PrintExample Result

Printable Window

PrintExample.java (Download source code)

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

public class PrintExample extends JFrame
                          implements ActionListener {
  public static void main(String[] args) {
    new PrintExample();
  }

  public PrintExample() {
    super("Printing Swing Components");
    WindowUtilities.setNativeLookAndFeel();
    addWindowListener(new ExitListener());
    Container content = getContentPane();
    JButton printButton = new JButton("Print");
    printButton.addActionListener(this);
    JPanel buttonPanel = new JPanel();
    buttonPanel.setBackground(Color.white);
    buttonPanel.add(printButton);
    content.add(buttonPanel, BorderLayout.SOUTH);
    DrawingPanel drawingPanel = new DrawingPanel();
    content.add(drawingPanel, BorderLayout.CENTER);
    pack();
    setVisible(true);
  }

  public void actionPerformed(ActionEvent event) {
    PrintUtilities.printComponent(this);
  }
}
Note: also requires WindowUtilities.java and ExitListener.java, shown earlier, plus DrawingPanel.java, shown below.

DrawingPanel.java (Download source code)

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

public class DrawingPanel extends JPanel {
  private int fontSize = 90;
  private String message = "Java 2D";
  private int messageWidth;
  
  public DrawingPanel() {
    setBackground(Color.white);
    Font font = new Font("Serif", Font.PLAIN, fontSize);
    setFont(font);
    FontMetrics metrics = getFontMetrics(font);
    messageWidth = metrics.stringWidth(message);
    int width = messageWidth*5/3;
    int height = fontSize*3;
    setPreferredSize(new Dimension(width, height));
  }

  public void paintComponent(Graphics g) {
    super.paintComponent(g);
    Graphics2D g2d = (Graphics2D)g;
    int x = messageWidth/10;
    int y = fontSize*5/2;
    g2d.translate(x, y);
    g2d.setPaint(Color.lightGray);
    AffineTransform origTransform = g2d.getTransform();
    g2d.shear(-0.95, 0);
    g2d.scale(1, 3);
    g2d.drawString(message, 0, 0);
    g2d.setTransform(origTransform);
    g2d.setPaint(Color.black);
    g2d.drawString(message, 0, 0);
  }
}


This page is part of the Quick Java Swing Tutorial for AWT Programmers. © 1999 Marty Hall. All source code freely available for unrestricted use. Created for for work in the Research and Technology Development Center of the Johns Hopkins University Applied Physics Lab, for courses in the Johns Hopkins Part-Time MS Program in Computer Science, and for various industry seminars and Java short courses.