Part IV - Special Topics

In the following six chapters we cover several different topics which relate directly to the use of Swing. Chapter 22 discusses the powerful new Java 2 printing API. We construct examples showing how to print an image on multiple pages, construct a print preview component, print styled text, and print JTable data (in both portrait and landscape modes). Chapter 23 introduces a few Java2D features. Examples include a generic 2D chart class, a 2D label class, and the beginnings of a Pac-man game. Chapter 24 introduces Accessibility and shows how easy it is to integrate this functionality into existing apps. Chapter 25 covers the basics of the JavaHelp API, and includes examples showing how we can customize the Swing-based help viewer to our liking. Chapter 26 introduces CORBA and contains an example of a client-server, Swing-based app based on our ! StocksTable example from chapter 18. Chapter 27 consists of two examples contributed by experienced Swing developers: constructing custom multi-line labels and tooltips and an internet browser application. Unfortunately, due to space limitations, chapters 24-27 were not included in this edition. However, they remain freely available to all readers on the book’s web site.

Chapter 22. Printing

In this chapter:

22.1 Java 2 Printing API overview

With Java 2 comes a considerably advanced printing API. Java veterans may recall that JDK 1.0 didn't provide printing capabilities at all. JDK 1.1 provided access to native print jobs, but multi-page printing was a real problem for that API.

Now Java developers are able to perform multi-page printing using page count selection and other typical specifications in the native Print dialog, as well as page format selection in the native platform-specific Page Setup dialog. The printing-related API is concentrated in the java.awt.print package, and we'll start this chapter with an overview of these classes and interfaces.

 

Note: At this point the underlying communication with the native printing system is not yet matured. You will notice that some of the examples in this chapter run extremely slow, especially when dealing with Images. We expect these deficiencies to decrease, and the material presented here should be equally applicable in future releases of Java.

22.1.1 PrinterJob

class java.awt.print.PrinterJob

This is the main class which controls printing in Java 2. It is used to store print job properties, to initiate printing when necessary, and to control the display of Print dialogs. A typical printing process is shown in the following code:

    PrinterJob prnJob = PrinterJob.getPrinterJob();
    prnJob.setPrintable(myPrintable);
    if (!prnJob.printDialog())
      return;
    prnJob.print();

This code retrieves an instance of PrinterJob with the static getPrinterJob() method, passes a Printable instance to it (used to render a specific page on demand--see below), invokes a platform-dependent Print dialog by calling PrinterJob’s printDialog() method, and, if this method returns true (indicating the "ok" to print), starts the actual printing process by calling the print() method on that PrinterJob.

The Print dialog will look familiar, as it is the typical dialog used by most other applications on the user’s system. For example, figure 22.1 shows a Windows NT Print dialog:

Figure 22.1 Windows NT Print dialog: about to print a pageable job .

<<file figure22-1.gif>>

Though the PrinterJob is the most important constituent of the printing process, it can do nothing without a Printable instance that specifies how to actually perform the necessary rendering for each page.

22.1.2 The Printable interface

abstract interface java.awt.print.Printable

This interface defines only one method: print(), which takes three parameters:

Graphics graphics: the graphical context into which the page will be drawn.

PageFormat pageFormat: an object containing information about the size and orientation of the page being drawn (see below).

int pageIndex: the zero based index of the page to be drawn.

The print() method will be called to print a portion of the PrinterJob corresponding to a given pageIndex. An implementation of this method should perform rendering of a specified page, using a given graphical context and a given PageFormat. The return value from this method should be PAGE_EXISTS if the page is rendered successfully, or NO_SUCH_PAGE if the given page index is too large and does not exist. (These are static ints defined in Printable.)

 

Note: we never call a Printable’s print() method ourselves. This is handled deep inside the actual platform-specific PrinterJob implementation which we aren’t concerned with here.

A class that implements Printable is said to be a page painter. When a PrinterJob uses only one page painter to print each page it is referred to as a printable job. The notion of a document as being separated into a certain number of pages is not predefined in a printable job. In order to print a specific page, a printable job will actually render all pages leading up to that page first, and then it will print the specified page. This is because it does not maintain information about how much space each page will occupy when rendered with the given page painter. For example, if we specify, in our Print dialog, that we want to print pages 3 and 5 only, then pages 0 through 4 (because pages are 0-indexed) will be rendered with the print() method, but only 2 and 4 will actually be printed.

 

Warning: Since the system only knows how many pages a printable job will span after the rendering of the complete document takes place (i.e. after paint() has been called), Print dialogs will not display the correct number of pages to be printed. This is because there is no pre-print communication between a PrinterJob and the system that determines how much space the printable job requires. For this reason you will often see a range such as 1 to 9999 in Print dialogs when printing printable jobs. (This is not the case for pageable jobs--see below.)

In reality, it is often the case that print() will be called for each page more than once. From a draft of an overview of the Java Printing API: "This *callback* printing model is necessary to support printing on a wide range of printers and systems...This model also enables printing to a bitmap printer from a computer that doesn't have enough memory or disk space to buffer a full-page bitmap. In this situation, a page is printed as a series of small bitmaps or *bands*. For example, if only enough memory to buffer one tenth of a page is available, the page is divided into ten bands. The printing system asks the application to render each page ten times, once to fill each band. The application does not need to be aware of the number or size of the bands; it simply must be able to render each page when requested." -- http://java.sun.com/printing/jdk1.2/index.html

Though this explains some of the performance problems that we will see in the coming examples, it seems that the model described above is not exactly what we are dealing with in Java 2 FCS. In fact, after some investigation§ , it turns out that the division into bands is not based on available memory. Rather, a hard-coded 512k buffer is used. By increasing the size of this buffer, it is feasible to increase performance significantly. However, this would involve modification of peer-level classes; something that we are certainly not encouraged to do. We hope to see this limitation accounted for in future releases.

22.1.3 The Pageable interface

abstract interface java.awt.print.Pageable

It is possible to support multiple page painters in a single PrinterJob. As we know, each page printer can correspond to a different scheme of printing because each Printable implements its own print() method. Implemenatations of the Pageable interface are designed to manage groups of page painters, and a print job that uses multiple page painters is referred to as a pageable job. Each page in a pageable job can use a different page printer and PageFormat (see below) to perform its rendering.

Unlike printable jobs, pageable jobs do maintain the predefined notion of a document as a set of separate pages. For this reason pages of a pageable job can be printed in any order without the necessity of rendering all pages leading up to a specific page (as is the case with printable jobs). Also, because a Pageable instance carries with it an explicit page count, this can be communicated to the native printing system when a PrinterJob is established. So when printing a pageable job the native Print dialog will know the correct range of pages to display, unlike a printable job. (Note that this does not mean pageable jobs are not subject to the inherent limitations described above; we will see the same repetitive calling of print() that ! we do in printable jobs.)

When constructing a pageable PrinterJob, instead of calling PrinterJob’s setPrintable() method (see section 22.1.1 above), we call its setPageable() method. Figure 22.1 shows a Windows NT Print dialog about to print a pageable job. Notice that the range of pages is not 1 to 9999.

We won’t be working with pageable jobs in this chapter because all the documents we will be printing only require one Printable implementation, even if documents can span multiple pages. In most real-world applications, each page of a document is printed with identical orientation, margins, and other sizing characterstics. However, if greater flexibility is desired, Pageable implementations such as Book (see below) can be useful.

22.1.4 The PrinterGraphics interface

abstract interface java.awt.print.PrinterGraphics

This interface defines only one method: getPrinterJob(), which retrieves the PrinterJob instance controlling the current printing process. It is implemented by Graphics objects that are passed to Printable objects to render a page. (We will not need to use this interface at all, as it is used deep inside PrinterJob instances to define Graphics objects passed to each Printable’s paint() method during printing.)

22.1.5 PageFormat

class java.awt.print.PageFormat

This class encapsulates a Paper object and adds to it an orientation property (landscape or portrait). We can force a Printable to use a specific PageFormat by passing one to PrinterJob’s overloaded setPrintable() method. For instance, the following would force a printable job to use a specific PageFormat with a landscape orientation:

    PrinterJob prnJob = PrinterJob.getPrinterJob();

    PageFormat pf = job.defaultPage();

    pf.setOrientation(PageFormat.LANDSCAPE);

    prnJob.setPrintable(myPrintable, pf);
    if (!prnJob.printDialog())
      return;
    prnJob.print();

PageFormat defines three orientations:

LANDSCAPE: The origin is at the bottom left-hand corner of the paper with x axis pointing up and y axis pointing to the right.

PORTRAIT (most common): The origin is at the top left-hand corner of the paper with x axis pointing to the right and y axis pointing down.

REVERSE_LANDSCAPE: The origin is at the top right-hand corner of the paper with x axis pointing down and y axis pointing to the left.

We can optionally display a page setup dialog in which the user can specify page characteristics such as orientation, paper size, margin size, etc. This dialog will return a new PageFormat to use in printing. The page setup dialog is meant to be presented before the Print dialog and can be displayed using PrinterJob’s pageDialog() method. The following code brings up a page setup dialog, and uses the resulting PageFormat for printing a printable job:

    PrinterJob prnJob = PrinterJob.getPrinterJob();

    PageFormat pf = job.pageDialog(job.defaultPage());

    prnJob.setPrintable(myPrintable, pf);

    if (!prnJob.printDialog())
      return;

    prnJob.print();

Note that we need to pass the pageDialog() method a PageFormat instance, as it uses it to clone and modify as the user specifies. If the changes are accepted the cloned and modifed version is returned. If they are not, the original version passed in is returned. Figure 22.2 shows a Windows NT page setup dialog:

Figure 22.2 Windows NT page setup dialog .

<<file figure22-2.gif>>

22.1.6 Paper

class java.awt.print.Paper

This class holds the size and margins of the paper used for printing. Methods getImageableX() and getImageableY() retrieve the coordinates of the top-left corner of the printable area in 1/72nds of an inch (which is approximately equal to one screen pixel--referred to as a "point" in typography). Methods getImageableWidth() and getImageableHeight() retrieve the width and height of the printable area (also in 1/72nds of an inch). We can also change the size of the useable region of the paper using its setImageableArea() method.

We can access the Paper object associated with a PageFormat using PageFormat’s getPaper() and setPaper() methods.

22.1.7 Book

class java.awt.print.Book

This class represents a collection of Printable instances with corresponding PageFormats to represent a complex document whose pages may have different formats. The Book class implements the Pageable interface, and Printables are added to a Book using one of its append() methods. This class also defines several methods allowing for the manipulation and replacement of specific pages. (A page in terms of a Book is a Printable-PageFormat pair. Each page does correspond to an actual printed page.) See the API docs and the Java Tutorial for more information about this class.

22.1.8 PrinterException

class java.awt.print.PrinterException

This exception may be thrown to indicate an error during a printing procedure. It has two concrete sub-classes: PrinterAbortException and PrinterIOException. The former indicates that a print job was terminated by the application or user while printing, and the latter indicates that there was a problem outputting to the printer.

 

Reference: For more information about the printing API and features that are expected to be implemented in future versions, refer to the Java tutorial.

 

22.2 Printing images

In this section we add printing capabilities to the JPEGEditor application introduced in chapter 13. This example will form a solid basis for the subsequent printing examples. Here we show how to implement the Printable interface to construct a custom panel with a print() method that can manage the printing of large images by splitting them up into a matrix of pages.

Figure 22.3 Running JPEGEditor example displaying native Print dialog.

<<figure22-3.gif>>

The Code: JPEGEditor.java

see \Chapter22\1

import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import java.util.*;
import java.io.*;

import javax.swing.*;
import javax.swing.border.*;
import javax.swing.event.*;
import javax.swing.filechooser.*;

import com.sun.image.codec.jpeg.*;

import java.awt.print.*;

// Unchanged code from section 13.4

public class JPEGEditor extends JFrame 
{
  // Unchanged code from section 13.4

  protected JMenuBar createMenuBar() {
    // Unchanged code from section 13.4

    mItem = new JMenuItem("Print...");
    mItem.setMnemonic('p');
    ActionListener lstPrint = new ActionListener() { 
      public void actionPerformed(ActionEvent e) {
        Thread runner = new Thread() {
          public void run() { 
            if (m_panel.getBufferedImage() != null)
              printData();
          }
        };
        runner.start();
      }
    };
    mItem.addActionListener(lstPrint);
    mFile.add(mItem);
    mFile.addSeparator();

    mItem = new JMenuItem("Exit");
    mItem.setMnemonic('x');
    lst = new ActionListener() { 
      public void actionPerformed(ActionEvent e) {
        System.exit(0);
      }
    };
    mItem.addActionListener(lst);
    mFile.add(mItem);
    menuBar.add(mFile);
    return menuBar;
  }

  // Unchanged code from section 13.4

  public void printData() {
    getJMenuBar().repaint();
    try {
      PrinterJob prnJob = PrinterJob.getPrinterJob();
      prnJob.setPrintable(m_panel);
      if (!prnJob.printDialog())
        return;
      setCursor( Cursor.getPredefinedCursor(
        Cursor.WAIT_CURSOR));
      prnJob.print();
      setCursor( Cursor.getPredefinedCursor(
        Cursor.DEFAULT_CURSOR));
      JOptionPane.showMessageDialog(this, 
        "Printing completed successfully", "JPEGEditor2",
        JOptionPane.INFORMATION_MESSAGE);
    }
    catch (PrinterException e) {
      e.printStackTrace();
      System.err.println("Printing error: "+e.toString());
    }
  }

  public static void main(String argv[]) {
    new JPEGEditor();
  }
}

class JPEGPanel extends JPanel implements Printable
{
  protected BufferedImage m_bi = null;

  public int m_maxNumPage = 1;

  // Unchanged code from section 13.4

  public int print(Graphics pg, PageFormat pageFormat,
   int pageIndex) throws PrinterException {
    if (pageIndex >= m_maxNumPage || m_bi == null)
      return NO_SUCH_PAGE;

    pg.translate((int)pageFormat.getImageableX(), 
      (int)pageFormat.getImageableY());
    int wPage = (int)pageFormat.getImageableWidth();
    int hPage = (int)pageFormat.getImageableHeight();

    int w = m_bi.getWidth(this);
    int h = m_bi.getHeight(this);
    if (w == 0 || h == 0)
      return NO_SUCH_PAGE;
    int nCol = Math.max((int)Math.ceil((double)w/wPage), 1);
      int nRow = Math.max((int)Math.ceil((double)h/hPage), 1);
    m_maxNumPage = nCol*nRow;

    int iCol = pageIndex % nCol;
    int iRow = pageIndex / nCol;
    int x = iCol*wPage;
    int y = iRow*hPage;
    int wImage = Math.min(wPage, w-x);
    int hImage = Math.min(hPage, h-y);

    pg.drawImage(m_bi, 0, 0, wImage, hImage, 
      x, y, x+wImage, y+hImage, this);
    System.gc();
        
    return PAGE_EXISTS;
  }
}

Understanding the Code

Class JPEGEditor

The java.awt.print package is imported to provide printing capabilities. A new menu item titled "Print..." has been added to the "File" menu of this application. If this item is selected and an image has been loaded, our new custom printData()method is called.

The printData() method retrieves a PrinterJob instance and passes it our m_panel component (an instance of JPEGPanel -- which now implements the Printable interface, see below). It then invokes a native Print dialog and initializes printing by calling print(). If no exception was thrown, a "Printing completed successfully" message is displayed when printing completes. Otherwise the exception trace is printed.

Class JPEGPanel

This class, which was originally designed to just display an image, now implements the Printable interface and is able to print a portion of its displayed image upon request. A new instance variable, m_maxNumPage, holds a maximum page number available for this printing. This number is set initially to one and its actual value is calculated in the print() method (see below).

The print() method prints a portion of the current image corresponding to the given page index. If the current image is larger than a single page, it will be split into several pages which are arranged as several rows and columns (a matrix). When printed they can be placed in this arrangement to form one big printout.

First this method shifts the origin of the graphics context to take into account the page's margins, and calculates the width and height of the area available for drawing: wPage and hPage.

pg.translate((int)pageFormat.getImageableX(),

(int)pageFormat.getImageableY());

int wPage = (int)pageFormat.getImageableWidth();

int hPage = (int)pageFormat.getImageableHeight();

Local variables w and h represent the width and height of the whole BufferedImage to be printed. (If any of these happens to be 0 we return NO_SUCH_PAGE.) Comparing these dimensions with the width and height of a single page, we can calculate the number of columns (not less than 1) and rows (not less than 1) in which the original image should be split to fit to the page's size:

int nCol = Math.max((int)Math.ceil((double)w/wPage), 1);

int nRow = Math.max((int)Math.ceil((double)h/hPage), 1);

m_maxNumPage = nCol*nRow;

The product of rows and columns gives us the number of pages in the print job, m_maxNumPage.

Now, because we know the index of the current page to be printed (it was passed as parameter pageIndex) we can determine the current column and row indices (note that enumeration is made from left to right and then from top to bottom), iCol and iRow:

int iCol = pageIndex % nCol;

int iRow = pageIndex / nCol;

int x = iCol*wPage;

int y = iRow*hPage;

int wImage = Math.min(wPage, w-x);

int hImage = Math.min(hPage, h-y);

We also can calculate the coordinates of the top-left corner of the portion of the image to be printed on this page (x and y), and the width and height of this region (wImage and hImage). Note that in the last column or row of our image matrix, the width and/or height of a portion can be less then the maximum values (which we calculated above--wPage and hPage).

Now we have everything ready to actually print a region of the image to the specified graphics context. We now need to extract this region and draw it at (0, 0), as this will be the origin (upper-left hand corner) of our printed page. The Graphics drawImage() method does the job. It takes ten parameters: an Image instance, four coordinates of the destination area (top-left and bottom-right--not width and height), four coordinates of the source area, and an ImageObserver instance.

pg.drawImage(m_bi, 0, 0, wImage, hImage,

x, y, x+wImage, y+hImage, this);

System.gc();

 

Note: Because the print() method may be called many times for the same page (see below), it makes good sense to explicitly invoke the garbage collector in this method. Otherwise we may run out of memory.

Running the Code

Figure 22.2 shows a Page Setup dialog brought up by our program when run on a Windows NT platform. Be aware that the print job could take up to 15 minutes to print (and this assumes you don’t run out of memory first)!

As we mentioned in the beginning of this chapter, the Java 2 printing environment is not yet fully matured. It doesn't work with all printers as expected, so writing and debugging printing applications may be difficult in many cases (a print preview capability is great help, as we will see in the next section). Also note that because we are using a printable job and not a pageable job, the page range is displayed as 1 to 9999 (see section 22.1.2--this may differ depending on your platform).

The most annoying thing with Java 2 printing is that is terribly slow. This is mainly because we are dealing with an Image (BufferedImage is a subclass of Image). Images and printing clash severely. As we will see in later examples, printing is much faster when Images are not involved.

The size of a relatively simple print job spooled to the printer may be unreasonably large. This makes Java 2 printing applications hardly comparable with native applications (at least at the time of this writing). Be sure to have plenty of memory, time, and patience when running this example. Or, alternatively, wait for the next Java 2 release.

 

Note: It is recommended that the DoubleBuffered property of components be set to false during printing if the print() method directly calls a component’s paint() method. Note that it is only safe to call paint() from the print() method if we are sure that print() is executing in the AWT event dispatching thread. Refer back to chapter 2 for how to shut off double-buffering, and how to check if a method is running within the A! WT event-dispatching thread.

 

22.3 Print preview

Print preview functionality has became a standard service provided by most modern print-enabled applications. It only makes sense to include this service in Java 2 applications. The example in this section shows how to construct a print preview component.

 

Note: An additional reason for Java developers to add print preview to their programs is that this service can be very useful for debugging print code. Slow performance of the Java printing API can make debugging impractical using an actual printer.

The print preview component displays small images of the printed pages as they would appear after printing. A GUI attached to the preview component typically allows for changing the scale of the preview images and invoking a print. The following example demonstrates such a component which can be easily added to any print-aware Swing application.

Figure 22.4 Print preview showing a 1200x1500 image split into 9 parts.

<<figure22-4.gif>>

The Code: JPEGEditor.java

see \Chapter22\2

public class JPEGEditor extends JFrame 
{
  // Unchanged code from section 22.2

  protected JMenuBar createMenuBar() {

    // Unchanged code from section 22.2

    mItem = new JMenuItem("Print Preview");
    mItem.setMnemonic('v');
    ActionListener lstPreview = new ActionListener() { 
      public void actionPerformed(ActionEvent e) { 
        Thread runner = new Thread() {
          public void run() {
            setCursor(Cursor.getPredefinedCursor(
              Cursor.WAIT_CURSOR));
            if (m_panel.getBufferedImage() != null)
              new PrintPreview(m_panel, 
               m_currentFile.getName()+" preview");
            setCursor(Cursor.getPredefinedCursor(
            Cursor.DEFAULT_CURSOR));
          }
        };
        runner.start();
      }
    };
    mItem.addActionListener(lstPreview);
    mFile.add(mItem);

    mFile.addSeparator();

// The rest of the code is unchanged from section 22.2

The Code: PrintPreview.java

see \Chapter22\2

import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import java.util.*;
import java.awt.print.*;

import javax.swing.*;
import javax.swing.border.*;
import javax.swing.event.*;

public class PrintPreview extends JFrame 
{
  protected int m_wPage;
  protected int m_hPage;
  protected Printable m_target;
  protected JComboBox m_cbScale;
  protected PreviewContainer m_preview;

  public PrintPreview(Printable target) {
    this(target, "Print Preview");
  }

  public PrintPreview(Printable target, String title) {
    super(title);
    setSize(600, 400);
    m_target = target;

    JToolBar tb = new JToolBar();
    JButton bt = new JButton("Print", new ImageIcon("print.gif"));
    ActionListener lst = new ActionListener() { 
      public void actionPerformed(ActionEvent e) { 
        try {
          // Use default printer, no dialog
          PrinterJob prnJob = PrinterJob.getPrinterJob();
          prnJob.setPrintable(m_target);
          setCursor( Cursor.getPredefinedCursor(
            Cursor.WAIT_CURSOR));
          prnJob.print();
          setCursor( Cursor.getPredefinedCursor(
            Cursor.DEFAULT_CURSOR));
          dispose();
        }
        catch (PrinterException ex) {
          ex.printStackTrace();
          System.err.println("Printing error: "+ex.toString());
        }
      }
    };
    bt.addActionListener(lst);
    bt.setAlignmentY(0.5f);
    bt.setMargin(new Insets(4,6,4,6));
    tb.add(bt);

    bt = new JButton("Close");
    lst = new ActionListener() { 
      public void actionPerformed(ActionEvent e) { 
        dispose();
      }
    };
    bt.addActionListener(lst);
    bt.setAlignmentY(0.5f);
    bt.setMargin(new Insets(2,6,2,6));
    tb.add(bt);

    String[] scales = { "10 %", "25 %", "50 %", "100 %" };
    m_cbScale = new JComboBox(scales);
    lst = new ActionListener() { 
      public void actionPerformed(ActionEvent e) { 
        Thread runner = new Thread() {
          public void run() {
            String str = m_cbScale.getSelectedItem().
              toString();
            if (str.endsWith("%"))
              str = str.substring(0, str.length()-1);
            str = str.trim();
              int scale = 0;
            try { scale = Integer.parseInt(str); }
            catch (NumberFormatException ex) { return; }
            int w = (int)(m_wPage*scale/100);
            int h = (int)(m_hPage*scale/100);

            Component[] comps = m_preview.getComponents();
            for (int k=0; k<comps.length; k++) {
              if (!(comps[k] instanceof PagePreview))
                continue;
              PagePreview pp = (PagePreview)comps[k];
                pp.setScaledSize(w, h);
            }
            m_preview.doLayout();
            m_preview.getParent().getParent().validate();
          }
        };
        runner.start();
      }
    };
    m_cbScale.addActionListener(lst);
    m_cbScale.setMaximumSize(m_cbScale.getPreferredSize());
    m_cbScale.setEditable(true);
    tb.addSeparator();
    tb.add(m_cbScale);
    getContentPane().add(tb, BorderLayout.NORTH);

    m_preview = new PreviewContainer();

    PrinterJob prnJob = PrinterJob.getPrinterJob();
    PageFormat pageFormat = prnJob.defaultPage();
    if (pageFormat.getHeight()==0 || pageFormat.getWidth()==0) {
      System.err.println("Unable to determine default page size");
        return;
    }
    m_wPage = (int)(pageFormat.getWidth());
    m_hPage = (int)(pageFormat.getHeight());
    int scale = 10;
    int w = (int)(m_wPage*scale/100);
    int h = (int)(m_hPage*scale/100);

    int pageIndex = 0;
    try {
      while (true) {
        BufferedImage img = new BufferedImage(m_wPage, 
          m_hPage, BufferedImage.TYPE_INT_RGB);
        Graphics g = img.getGraphics();
        g.setColor(Color.white);
        g.fillRect(0, 0, m_wPage, m_hPage);
        if (target.print(g, pageFormat, pageIndex) != 
         Printable.PAGE_EXISTS)
          break;
        PagePreview pp = new PagePreview(w, h, img);
        m_preview.add(pp);
        pageIndex++;
      }
    }
    catch (PrinterException e) {
      e.printStackTrace();
      System.err.println("Printing error: "+e.toString());
    }

    JScrollPane ps = new JScrollPane(m_preview);
    getContentPane().add(ps, BorderLayout.CENTER);

    setDefaultCloseOperation(DISPOSE_ON_CLOSE);
    setVisible(true);
  }

  class PreviewContainer extends JPanel
  {
    protected int H_GAP = 16;
    protected int V_GAP = 10;

    public Dimension getPreferredSize() {
      int n = getComponentCount();
      if (n == 0)
        return new Dimension(H_GAP, V_GAP);
      Component comp = getComponent(0);
      Dimension dc = comp.getPreferredSize();
      int w = dc.width;
      int h = dc.height;
           
      Dimension dp = getParent().getSize();
      int nCol = Math.max((dp.width-H_GAP)/(w+H_GAP), 1);
      int nRow = n/nCol;
      if (nRow*nCol < n)
        nRow++;

      int ww = nCol*(w+H_GAP) + H_GAP;
      int hh = nRow*(h+V_GAP) + V_GAP;
      Insets ins = getInsets();
      return new Dimension(ww+ins.left+ins.right, 
        hh+ins.top+ins.bottom);
    }

    public Dimension getMaximumSize() {
      return getPreferredSize();
    }

    public Dimension getMinimumSize() {
      return getPreferredSize();
    }

    public void doLayout() {
      Insets ins = getInsets();
      int x = ins.left + H_GAP;
      int y = ins.top + V_GAP;

      int n = getComponentCount();
      if (n == 0)
        return;
      Component comp = getComponent(0);
      Dimension dc = comp.getPreferredSize();
      int w = dc.width;
      int h = dc.height;
            
      Dimension dp = getParent().getSize();
      int nCol = Math.max((dp.width-H_GAP)/(w+H_GAP), 1);
      int nRow = n/nCol;
      if (nRow*nCol < n)
        nRow++;

      int index = 0;
      for (int k = 0; k<nRow; k++) {
        for (int m = 0; m<nCol; m++) {
          if (index >= n)
            return;
          comp = getComponent(index++);
          comp.setBounds(x, y, w, h);
          x += w+H_GAP;
        }
        y += h+V_GAP;
        x = ins.left + H_GAP;
      }
    }
  }

  class PagePreview extends JPanel
  {
    protected int m_w;
    protected int m_h;
    protected Image m_source;
    protected Image m_img;

    public PagePreview(int w, int h, Image source) {
      m_w = w;
      m_h = h;
      m_source= source;
      m_img = m_source.getScaledInstance(m_w, m_h, 
        Image.SCALE_SMOOTH);
      m_img.flush();
      setBackground(Color.white);
      setBorder(new MatteBorder(1, 1, 2, 2, Color.black));
    }

    public void setScaledSize(int w, int h) {
      m_w = w;
      m_h = h;
      m_img = m_source.getScaledInstance(m_w, m_h, 
        Image.SCALE_SMOOTH);
      repaint();
    }

    public Dimension getPreferredSize() {
      Insets ins = getInsets();
      return new Dimension(m_w+ins.left+ins.right, 
        m_h+ins.top+ins.bottom);
    }

    public Dimension getMaximumSize() {
      return getPreferredSize();
    }

    public Dimension getMinimumSize() {
      return getPreferredSize();
    }

    public void paint(Graphics g) {
      g.setColor(getBackground());
      g.fillRect(0, 0, getWidth(), getHeight());
      g.drawImage(m_img, 0, 0, this);
      paintBorder(g);
    }
  }
}

Understanding the Code

Class JPEGEditor

Compared to the previous example this class has only one difference: it creates a menu item titled "Print Preview." When selected, this item creates an instance of the PrintPreview class (see below). This class’s constructor takes two parameters: a reference to a Printable instance and a text string for the frame's title. As we have seen in the previous example, our m_panel component implements the Printable interface and provides the actual printing functionality, so we use it to create the PrintPreview instance. Note that this call is wrapped in a thr! ead because, when used with large images, creation of a PrintPreview instance can take a significant amount of time.

 

Note: As you can see, we only need to have a reference to an instance of the Printable interface to create a PrintPreview component. Thus, this component can be added to any print-aware application with only a couple lines of code. We will use it in the remaining examples as well, because it is such a simple feature to add.

 

Class PrintPreview

This class represents a JFrame-based component which is capable of displaying the results of printing before actual printing occurs. Several instance variables are used:

Printable m_target: an object whose printout will be previewed.

int m_wPage: width of the default printing page.

int m_hPage: height of the default printing page.

JComboBox m_cbScale: combobox which selects a scale for preview.

PreviewContainer m_preview: container which holds previewing pages.

Two public constructors are provided. The first one takes an instance of the Printable interface and passes control to the second constructor, using the Printable along with the "Print Preview" String as parameters. The second constructor takes two parameters: an instance of the Printable interface and the title string for the frame. This second constructor is the one that actually sets up the PrintPreview component.

First, a toolbar is created and a button titled "Print" is added to perform printing of the m_target instance as described in the previous example. The only difference is that no Print dialog is invoked, and the default system printer is used (this approach is typical for print preview components). When the printing is complete, this print preview component is disposed. The second button added to the toolbar is labeled "Close" and merely disposes of this frame component.

The third (and the last) component added to the toolbar is the editable combobox m_cbScale, which selects a percent scale to zoom the previewed pages. Along with several pre-defined choices (10 %, 25 %, 50 %, and 100 %) any percent value can be entered. As soon as that value is selected and the corresponding ActionListener involved, the zoom scale value is extracted and stored in the local variable scale. This determines the width and height of each PreviewPage component we will be creating:

int w = (int)(m_wPage*scale/100);

int h = (int)(m_hPage*scale/100);

Then all child components of the m_preview container in turn are cast to PagePreview components (each child is expected to be a PagePreview instance, but instanceof is used for precaution), and the setScaledSize() method is invoked to assign a new size to the preview pages. Finally doLayout() is invoked on m_preview to lay out the resized child components, and validate() is invoked on the scroll pane. This scroll pane is the parent of the m_preview component in the second generation (the first parent is a JViewport component--see chapter 7). This last call is necessary to display/hide scroll bars as needed for the new size of the m_preview container. This whole process is wrapped in a thread to avoid clogging up the AWT event-dispatching thread.

When toolbar construction is complete, the m_preview component is created and filled with the previewed pages. To do so we first retrieve a PrinterJob instance for a default system printer without displaying a Page Setup dialog, and retrieve a default PageFormat instance. We use this to determine the initial size of the previewed pages by multiplying its dimensions by the computed scaling percentile (which is 10% at initialization time, because scale is set to 10).

To create these scalable preview pages we set up a while loop to continuously call the print() method of the given Printable instance, using a page index that gets incremented each iteration, until it returns something other than Printable.PAGE_EXISTS.

Each page is rendered into a separate image in memory. To do this, an instance of BufferedImage is created with width m_wPage and height m_hPage. A Graphics instance is retrieved from that image using getGraphics():

  BufferedImage img = new BufferedImage(m_wPage, m_hPage, BufferedImage.TYPE_INT_RGB);
  Graphics g = img.getGraphics();
  g.setColor(Color.white);
  g.fillRect(0, 0, m_wPage, m_hPage);
  if (target.print(g, pageFormat, pageIndex) != Printable.PAGE_EXISTS)
    break;

After filling the image's area with a white background (most paper is white), this Graphics instance, along with the PageFormat and current page index, pageIndex, are passed to the print() method of the Printable object.

 

Note: The BufferedImage class in the java.awt.image package allows direct image manipulation in memory. This class will be discussed in more detail in Chapter 23, as well as other classes from the Java 2 2D API.

If the call to the print() method returns PAGE_EXISTS, indicating success in the rendering of the new page, a new PagePreview component is created:

PagePreview pp = new PagePreview(w, h, img);

m_preview.add(pp);

pageIndex++;

Note that our newly created BufferedImage is passed to the PagePreview constructor as one of the parameters. This is so that we can use it now and in the future for scaling each PagePreview component separately. The other parameters are the width and height to use, which, at creation time, are 10% of the page size (as discussed above).

Each new component is added to our m_preview container. Finally, when the Printable’s print() method finishes, our m_preview container is placed in a JScrollPane to provide scrolling capabilities. This scroll pane is then added to the center of the PrintPreview frame, and our frame is then made visible.

Class PrintPreview.PreviewContainer

This inner class extends JPanel to serve as a container for PagePreview components. The only reason this custom container is developed is because we have specific layout requirements. What we want here is a layout which places its child components from left to right, without any resizing (using their preferred size), and leaves equal gaps between them. When the available container's width is filled, a new row should be started from the left edge, without regard to the available height (we assume scrolling functionality will be made available).

You may want to refer back to our discussion of layouts in chapter 4. The code constituting this class does not require much explanation and provides a good exercise for custom layout development (even though this class is not explicitly a layout manager).

Class PrintPreview.PagePreview

This inner class extends JPanel to serve as a placeholder for the image of each printed page preview. Four instance variables are used:

int m_w: the current component's width (without insets).

int m_h: the current component's height (without insets).

Image m_source: the source image depicting the previewed page in full scale.

Image m_img: the scaled image currently used for rendering.

The constructor of the PagePreview class takes its initial width, height, and the source image. It creates a scaled image by calling the getScaledInstance() method and sets its border to MatteBorder(1, 1, 2, 2, Color.black) to imitate a page laying on a flat surface.

The setScaledSize() method may be called to resize this component. It takes a new width and height as parameters and creates a new scaled image corresponding to the new size. Usage of the SCALE_SMOOTH option for scaling is essential to get a preview image which looks like a zoomed printed page (although it is not the fastest option).

The paint() method draws a scaled image and draws a border around the component.

Running the Code

At this point you can compile and execute this example. Figure 22.2 shows a preview of the large image which will be printed on the nine pages. Select various zoom factors in the combobox and see how the size of the previewed pages is changed. Then press the "Print" button to print to the default printer directly from the preview frame.

22.4 Printing styled text

In this section we’ll add printing capabilities to the RTF word processor application developed in chapter 20. The printing of styled text would be easy if JTextComponent or JTextPane implemented the Printable interface and provided the capability to print their content. Unfortunately this is not the case (at least as of Java 2 FCS). So we have to get fairly clever, and create our own BoxView subclass to specifically handle printing.

Our styled editor class will now implement the Printable interface and delegate the mechanics of printing of each page to our custom BoxView subclass. Note that this custom view is not actually displayed on the screen as the editor. It sits in the background and is used only for printing and display in our print preview component.

Recall, from our discussion in chapters 11 and 19, that styled documents consist of a hierarchy of elements: paragraphs, images, components, etc. Each element is rendered by an associated view, which are all children of the root view. A BoxView in particular, arranges all its child views along either the x or y axis (typically the y axis). So, in this example, when we need to render a page, we start from the first child view of our custom BoxView, and render each of the child views sequentially, placing each below the previous in the vertical direction. When the next page should be rendered, we start from the first remaining view and continue in this fashion until all child views have been rendered. (This process will be explained in greater detail below.)

Figure 22.5 Print preview showing a four page RTF document.

<<figure22-5.gif>>

The Code: WordProcessor.java

see \Chapter22\3

import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.util.*;
import java.sql.*;

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

import javax.swing.*;
import javax.swing.text.*;
import javax.swing.event.*;
import javax.swing.border.*;
import javax.swing.text.rtf.*;
import javax.swing.undo.*;

import dl.*;

public class WordProcessor extends JFrame implements Printable
{
  // Unchanged code from section 20.9

  protected PrintView m_printView;

  // Unchanged code from section 20.9

  protected JMenuBar createMenuBar() {

    // Unchanged code from section 20.9

    mFile.addSeparator();

    Action actionPrint = new AbstractAction("Print...", 
     new ImageIcon("print.gif")) { 
      public void actionPerformed(ActionEvent e) {
        Thread runner = new Thread() {
          public void run() {
            printData();
          }
        };
        runner.start();
      }
    };
    item =  mFile.add(actionPrint);  
    item.setMnemonic('p');

    item = new JMenuItem("Print Preview");
    item.setMnemonic('v');
    ActionListener lstPreview = new ActionListener() { 
      public void actionPerformed(ActionEvent e) {
        Thread runner = new Thread() {
          public void run() {
            setCursor(Cursor.getPredefinedCursor(
              Cursor.WAIT_CURSOR));
            new PrintPreview(WordProcessor.this);
            setCursor(Cursor.getPredefinedCursor(
              Cursor.DEFAULT_CURSOR));
          }
        };
        runner.start();
      }
    };
    item.addActionListener(lstPreview);
    mFile.add(item);

    mFile.addSeparator();

    // Unchanged code from section 20.9
  }

  // Unchanged code from section 20.9

  public void printData() {
    getJMenuBar().repaint();
    try {
      PrinterJob prnJob = PrinterJob.getPrinterJob();
      prnJob.setPrintable(this);
      if (!prnJob.printDialog())
        return;
      setCursor( Cursor.getPredefinedCursor(
        Cursor.WAIT_CURSOR));
      prnJob.print();
      setCursor( Cursor.getPredefinedCursor(
        Cursor.DEFAULT_CURSOR));
      JOptionPane.showMessageDialog(this, 
        "Printing completed successfully", "Info",
        JOptionPane.INFORMATION_MESSAGE);
    }
    catch (PrinterException e) {
      e.printStackTrace();
      System.err.println("Printing error: "+e.toString());
    }
  }

  public int print(Graphics pg, PageFormat pageFormat,
   int pageIndex) throws PrinterException {
    pg.translate((int)pageFormat.getImageableX(), 
      (int)pageFormat.getImageableY());
    int wPage = (int)pageFormat.getImageableWidth();
    int hPage = (int)pageFormat.getImageableHeight();
    pg.setClip(0, 0, wPage, hPage);

    // Only do this once per print
    if (m_printView == null) {
      BasicTextUI btui = (BasicTextUI)m_monitor.getUI();
      View root = btui.getRootView(m_monitor);
      m_printView = new PrintView(
        m_doc.getDefaultRootElement(), 
        root, wPage, hPage);
    }
        
    boolean bContinue = m_printView.paintPage(pg, 
      hPage, pageIndex);
    System.gc();
        
    if (bContinue)
      return PAGE_EXISTS;
    else {
      m_printView = null;
      return NO_SUCH_PAGE;
    }
  }

  public static void main(String argv[]) {
    new WordProcessor();
  }

  // Unchanged code from section 20.9

  class PrintView extends BoxView
  {
    protected int m_firstOnPage = 0;
    protected int m_lastOnPage = 0;
    protected int m_pageIndex = 0;

    public PrintView(Element elem, View root, int w, int h) {
      super(elem, Y_AXIS);
      setParent(root);
      setSize(w, h);
      layout(w, h);
    }

    public boolean paintPage(Graphics g, int hPage, 
     int pageIndex) {
      if (pageIndex > m_pageIndex) {
        m_firstOnPage = m_lastOnPage + 1;
        if (m_firstOnPage >= getViewCount())
          return false;
        m_pageIndex = pageIndex;
      }
      int yMin = getOffset(Y_AXIS, m_firstOnPage);
      int yMax = yMin + hPage;
      Rectangle rc = new Rectangle();

      for (int k = m_firstOnPage; k < getViewCount(); k++) {
        rc.x = getOffset(X_AXIS, k);
        rc.y = getOffset(Y_AXIS, k);
        rc.width = getSpan(X_AXIS, k);
        rc.height = getSpan(Y_AXIS, k);
        if (rc.y+rc.height > yMax)
          break;
        m_lastOnPage = k;
        rc.y -= yMin;
        paintChild(g, rc, k);
      }
      return true;
    }
  }

// Remaining code is unchanged from section 20.9

 

Class WordProcessor

In comparison to the example of section 20.9, this class imports two new packages: java.awt.print and javax.swing.plaf.basic. The first one provides the necessary printing API, while the second is used to gain access to text component UI delegates (we will soon see why this is necessary).

One new instance variable, PrintView m_printView, represents our custom view used to print the styled document (see below). The createMenuBar() method now creates and adds to the "File" menu two new menu items titled "Print..." and "Print Preview". When the first one is selected it calls the printData() method, while the second one creates a PrintPreview instance by passing WordProcessor.this as the Printable reference. The printData() method obtains a PrinterJob instance, invokes a native Print dialog, and initializes printing the same way as we've seen in previous examples.

The print() method is called to print a given page of the current styled document. First, this method determines the size and origin of the printable area using a PageFormat instance as we've seen before. Next we need to set a clip area of the graphics context to the size of this printable area. This is necessary for the rendering of text component Views because they do clipping area intersection detection for optimized painting. If we don’t set the clipping area, they won’t know how to render themselves.

Unfortunately, the Printable interface does not provide any methods which can be called to initialize specific resources before printing, and release these resources after printing. So we must implement this functionality ourselves. The actual job of rendering the styled document is done by the m_printView object, which must be instantiated before printing begins, and released when it ends. Being forced to do all this in a single method, we first check if the m_printView reference is null. If it is then we assign it a new instance of PrintVew. If it isn’t null we don’t modify it (this indicates that we are in the midst of a printing session). When printing ends, we then set it to null so that the remaining PrintView instance can be garbage collected.

    // Only do this once per print 
    if (m_printView == null) {
      BasicTextUI btui = (BasicTextUI)m_monitor.getUI();
      View root = btui.getRootView(m_monitor);
      m_printView = new PrintView(m_doc.getDefaultRootElement(), 
                                  root, wPage, maxHeight);
    }

To create an m_printView object we need to access the BasicTextUI instance for our m_monitor JTextPane component, and retrieve its root View (which sits on the top of the hierarchy of views--see chapter 19) using BasicTextUI’s getRootView() method. At this point the PrintView instance can be created. Its constructor takes four parameters: the root element of the curre! nt document, the root view, and the width and height of the entire document’s printing bounds.

As soon as we're sure that the m_printView object exists, we call its custom paintPage() method to render a page with the given index to the given graphical context. Then the garbage collector is called explicitly in an attempt to cut down on the heavy memory usage.

Finally if the paintPage() call returns true, the PAGE_EXISTS value is returned to indicate a successful render. Otherwise we set the m_printView reference to null, and return NO_SUCH_PAGE to indicate that no more pages can be rendered.

Class WordProcessor.PrintView

This inner class extends BoxView and is used to render the content of a styled document. (Note that since this class extends BoxView, we have access to some of its protected methods, such as getOffset(), getSpan(), layout(), and paintChild().)

Three instance variables are defined:

int m_firstOnPage: index of the first view to be rendered on the current page.

int m_lastOnPage: index of the last view to be rendered on the current page.

int m_pageIndex: index of the current page.

The PrintView constructor creates the underlying BoxView object for a given root Element instance (this should be the root element in the document model of the text component we are printing) and the specified axis used for format/break operations (this is normally Y_AXIS). A given View instance is then set as the parent for this PrintView (this should be the root View of the text component we are printing). The setSize() method is called to set the size of this view, and layout() is called to lay out the child views based on the specified width and height (this is done to calculate the coordinates of all views used in the rendering of this document). These operations may be time consuming for large documents. Fortunately they are only performed at construction time:

   public PrintView(Element elem, View root, int w, int h) {
      super(elem, Y_AXIS);
      setParent(root);
      setSize(w, h);
      layout(w, h);
   }

 

Note: We found that setParent()must be called prior to setSize() and layout() to avoid undesirable side effects.

Our paintPage() method renders a single page of a styled document. It takes three parameters:

Graphics g: the graphical context to render the page in.

int hPage: the height of the page.

int pageIndex: the index of the page to render.

This method will return true if the page with the given index is rendered successfully, or false if the end of the document is reached. We assume that the pages to be rendered will be fetched in sequential order (although more than one call can be made to print the most recently rendered page). If a new page index is greater than m_pageIndex (which holds the index of the last rendered page), we begin rendering from the next view after the last one rendered on the previous page, and set m_firstOnPage to m_lastOnPage + 1. If this exceeds the number of child views, no more rendering can be done, so we retu! rn false.

      m_firstOnPage = m_lastOnPage + 1;

      if (m_firstOnPage >= getViewCount())
        return false;

Local variables yMin and yMax denote top and bottom coordinates of the page being rendered relative to the top of the document. yMin is determined by the offset of the first view to be rendered, and yMax is then yMin plus the height of the page:

int yMin = getOffset(Y_AXIS, m_firstOnPage);

int yMax = yMin + hPage;

All child views, from m_firstOnPage to the last view that will fit on the current page, are examined sequentially in a loop. In each iteration, local variable, Rectangle rc, is assigned the coordinates of where the associated child view is placed in the document (not on the current page). Based on the height of this view, if there is enough room horizontally to render it (note that it is guaranteed to fit vertically, since the page’s width was specified in the layout() call above), the paintChild() method is called to render it into the graphics context. Also note that we offset the y-coordinate of the view by yMin because, as we just mentioned, each child view is positioned in terms of the whole document, and we are only concerned with its position on the current page. If at any point a view will not fit within the remaining page space we exit the loop.

      for (int k = m_firstOnPage; k < getViewCount(); k++) {
        rc.x = getOffset(X_AXIS, k);
        rc.y = getOffset(Y_AXIS, k);
        rc.width = getSpan(X_AXIS, k);
        rc.height = getSpan(Y_AXIS, k);
        if (rc.y+rc.height > yMax)
          break;
        m_lastOnPage = k;
          rc.y -= yMin;
        paintChild(g, rc, k);
      }
      return true;

 

Note: A more sophisticated and precise implementation might examine the y coordinates of all views in the hierarchy, not only the children of the root view. It might be the case that a large paragraph should be split between two or more pages. Our simple approach is not this flexible. In fact, in the case of a paragraph that spans a height larger than the page size, we could be in real trouble with this implementation. Although this is not common, it must be accounted for in professional implementations.

Running the Code

At this point you can compile and execute this example. Figure 22.3 shows a preview of a text document which will occupy four pages. Try previewing and printing a styled document. We’ve included the License.rtf file for you to experiment with.

22.5 Printing tables

In this section we'll add printing capabilities to the JTable application developed earlier in chapter 18. Unlike other examples in this chapter, a printed table should not resemble the JTable component as displayed on the screen. This requires us to add detailed code for the rendering of the table's contents as it should be displayed in a printout. The resulting code, however, does not depend on the table's structure and can be easily used for printing any table component. Thus, the code presented here can be plugged into any JTable application that needs printing functionality. Combined with our print preview component (see previous examples), the amount of work we need to do to support printing of tab! les in professional applicatons is minimal.

Figure 22.6 Print preview of JTable data.

<<figure22-6.gif>>

The Code: StocksTable.java

see \Chapter22\4

import java.awt.*;
import java.awt.event.*;
import java.util.*;
import java.io.*;
import java.text.*;
import java.sql.*;
import java.awt.print.*;

import javax.swing.*;
import javax.swing.border.*;
import javax.swing.event.*;
import javax.swing.table.*;

public class StocksTable extends JFrame implements Printable
{
  protected JTable m_table;
  protected StockTableData m_data;
  protected JLabel m_title;

  protected int m_maxNumPage = 1;

  // Unchanged code from section 18.6

  protected JMenuBar createMenuBar() {
    // Unchanged code from section 18.6

    JMenuItem mPrint = new JMenuItem("Print...");
    mPrint.setMnemonic('p');
    ActionListener lstPrint = new ActionListener() { 
      public void actionPerformed(ActionEvent e) {
        Thread runner = new Thread() {
          public void run() {
            printData();
          }
        };
        runner.start(); 
      }
    };
    mPrint.addActionListener(lstPrint);
    mFile.add(mPrint);

    JMenuItem mPreview = new JMenuItem("Print Preview");
    mPreview.setMnemonic('v');
    ActionListener lstPreview = new ActionListener() { 
      public void actionPerformed(ActionEvent e) {
        Thread runner = new Thread() {
          public void run() {
            setCursor(Cursor.getPredefinedCursor(
              Cursor.WAIT_CURSOR));
            new PrintPreview(Table7.this, 
            m_title.getText()+" preview");
            setCursor(Cursor.getPredefinedCursor(
              Cursor.DEFAULT_CURSOR));
          }
        };
        runner.start();
      }
    };
    mPreview.addActionListener(lstPreview);
    mFile.add(mPreview);
    mFile.addSeparator();

    // Unchanged code from section 18.6
  }

  public void printData() {
    try {
      PrinterJob prnJob = PrinterJob.getPrinterJob();
      prnJob.setPrintable(this);
      if (!prnJob.printDialog())
        return;
      m_maxNumPage = 1;
      prnJob.print();
    }
    catch (PrinterException e) {
      e.printStackTrace();
      System.err.println("Printing error: "+e.toString());
    }
  }
    
  public int print(Graphics pg, PageFormat pageFormat,
   int pageIndex) throws PrinterException {
    if (pageIndex >= m_maxNumPage)
      return NO_SUCH_PAGE;

    pg.translate((int)pageFormat.getImageableX(), 
      (int)pageFormat.getImageableY());
    int wPage = 0;
    int hPage = 0;
    if (pageFormat.getOrientation() == pageFormat.PORTRAIT) {
      wPage = (int)pageFormat.getImageableWidth();
      hPage = (int)pageFormat.getImageableHeight();
    }
    else {
      wPage = (int)pageFormat.getImageableWidth();
      wPage += wPage/2;
      hPage = (int)pageFormat.getImageableHeight();
      pg.setClip(0,0,wPage,hPage);
    }

    int y = 0;
    pg.setFont(m_title.getFont());
    pg.setColor(Color.black);
    Font fn = pg.getFont();
    FontMetrics fm = pg.getFontMetrics();
    y += fm.getAscent();
    pg.drawString(m_title.getText(), 0, y);
    y += 20; // space between title and table headers

    Font headerFont = m_table.getFont().deriveFont(Font.BOLD);
    pg.setFont(headerFont);
    fm = pg.getFontMetrics();

    TableColumnModel colModel = m_table.getColumnModel();
    int nColumns = colModel.getColumnCount();
    int x[] = new int[nColumns];
    x[0] = 0;
        
    int h = fm.getAscent();
    y += h; // add ascent of header font because of baseline
            // positioning (see figure 2.10)
 
    int nRow, nCol;
    for (nCol=0; nCol<nColumns; nCol++) {
      TableColumn tk = colModel.getColumn(nCol);
      int width = tk.getWidth();
      if (x[nCol] + width > wPage) {
        nColumns = nCol;
        break;
      }
      if (nCol+1<nColumns)
        x[nCol+1] = x[nCol] + width;
      String title = (String)tk.getIdentifier();
      pg.drawString(title, x[nCol], y);
    }

    pg.setFont(m_table.getFont());
    fm = pg.getFontMetrics();
        
    int header = y;
    h = fm.getHeight();
    int rowH = Math.max((int)(h*1.5), 10);
    int rowPerPage = (hPage-header)/rowH;
    m_maxNumPage = Math.max((int)Math.ceil(m_table.getRowCount()/
      (double)rowPerPage), 1);

    TableModel tblModel = m_table.getModel();
    int iniRow = pageIndex*rowPerPage;
    int endRow = Math.min(m_table.getRowCount(), 
      iniRow+rowPerPage);
        
    for (nRow=iniRow; nRow<endRow; nRow++) {
      y += h;
      for (nCol=0; nCol<nColumns; nCol++) {
        int col = m_table.getColumnModel().getColumn(nCol).getModelIndex();
        Object obj = m_data.getValueAt(nRow, col);
        String str = obj.toString();
        if (obj instanceof ColorData)
          pg.setColor(((ColorData)obj).m_color);
        else
          pg.setColor(Color.black);
          pg.drawString(str, x[nCol], y);
      }
    }

    System.gc();
    return PAGE_EXISTS;
  }

// Remaining code unchanged from section 18.6

Understanding the Code

Class StocksTable

In comparison with the table examples of chapter 18, we now implement the Printable interface. In our createMenuBar() method we add a "Print..." menu item, which calls our new printData() method which acts just like the printData() methods we implemented in the examples above.

In our implementation of the print() method, we first determine whether a valid page index has been specified by comparing it to the maximum number of pages, m_maxNumPage:

if (pageIndex > m_maxNumPage)

return NO_SUCH_PAGE;

The catch is that we don't know this maximum number in advance. So we assign an initial value of 1 to m_maxNumPage (the code above works for the 0-th page), and adjust m_maxNumPage to the real value later in the code, just as we’ve done in the examples above.

We then translate the origin of the graphics context to the origin of the given PageFormat instance and determine the width and height of the area available for printing. These dimensions are used to determine how much data can fit on the given page. This same technique was also used in the previous examples. However, in this example we’ve added the ability to print with a landscape orientation because tables can be quite wide, and we normally don’t want table data to span multiple pages (at least horizontally). In order to do this we have to first check the orientation of the given PageFormat instance. If it is PORTRAIT we determine its width and height as we have always done. If it is not PORTRAIT, then it must be either LANDSCAPE or REVERSE_LANDSCAPE (see section 22.1.5). In this case we need to increase the width of the page because the default is not adequate. After increasing the width we must also explicitly set the size of the graphics clip,

This is all we have to do to allow printing in either orientation.

Local variable y is created to keep track of the current vertical position on the page, and we are now ready to actually start the rendering, and begin with the the table's title. Note that we use the same font as is used in the table application for consistency. We add some white space below the title (by increasing y) and then we make preparations for printing our table's headers and body. A bold font is used for our table's header. An array, x[], is created which will be used to store the x coordinate of each column’s upper left corner (taking into account that they may be resized and moved). Variable nColumns contains the total number of columns in our table.

Now we actually iterate through the columns and print each column header while filling our x[] array. We check each iteration to see if the x coordinate of the previous column, combined with the width of the column under consideration, will be more than the width of the page. If so we set the total number of columns, nColumns, to the number that will actually fit on the page, and then break out of the loop. If not we set the x coordinate corresponding to the current column, print its title, and continue on to the next iteration.

Since we've completed the printing of our table's title and headers, we know how much space is left for printing our table's body. We also know the font's height, so we can calculate how many rows can be printed on one page, which is rowPerPage below (the height of the page minus the current y offset, all divided by the height of the current font or 10, whichever is larger). Finally we calculate the real number of pages, m_maxNumPage, by dividing the total row count of our table by the number of rows per page we just calculated as rowPerPage. The minimum page count will be 1.

Now we need to actually print the table data. First we calculate the initial iniRow and final endRow rows to be printed on this page:

TableModel tblModel = m_table.getModel();

int iniRow = pageIndex*rowPerPage;

int endRow = Math.min(m_table.getRowCount(),

iniRow+rowPerPage);

Then, in a double for loop, iterating through each column of each row in turn, we print the table’s contents. This is done by extracting each cell’s data as an Object (using getValueAt()). We store its toString() String representation in a local variable and check if the object is an instance of our custom inner class, ColorData (defined in earlier chapter 18 examples). This class is designed to associate a color with a given data object. So if the object is a ColorData instance we grab its color and assign it as the current color of the graphics context. If it isn’t we use black. Finally, we print that object’s toString() representation and continue on to the remaining cells.

 

Note: We are assuming that each object’s toString() representation is what we want to print. For more complex TableCellRenderer implementations, this printing code will need to be customized.

We end by explicitly invoking the garbage collector and returning PAGE_EXISTS to indicate a successful print.

Running the Code

At this point you can compile and execute this example. Figure 22.4 shows a print preview of our table application. Try manipulating the table's contents (by choosing different dates if you have JDBC and ODBC -- see chapter 18) and column orders to see how it affects the table's printout and print preview.

You will notice that in order to fit the whole table on the paper it must be condensed considerably. It is natural at this point to want to print it with a landscape orientation. When we choose landscape from the Page Setup dialog this modifies the PageFormat object that will be sent to our print() method when printing begins. However, this will not actually tell the printer to print in landscape mode. In order to do this, we have to explicitly choose landscape mode from the Print dialog as well. Unfortunately, the Page Setup information does not inform the printer, but it is necessary to inform our application.

Though our application can print successfully with a landscape orientation, our print preview component is not designed to display anything but portrait-oriented previews. Because of the way our PrintPreview component has been constructed, it is quite easy to add the ability to preview landscape-oriented pages if desired. The only modification that is necessary, is the addition of a parameter to its constructor which specifyies the orientation to use. This parameter can then be assigned to the PageFormat object used in constructing each PagePreview object. We will not show the code here, but we have included a modified version of PrintPreview and the StocksTable application to demonstrate how you can implement this functionality. See \Chapter22\5. Figure 22.7 illustrates.

Figure 22.7 Print preview component modified for landscape orientation.

<<figure22-7.gif>>