Find out how to add Java 2's print functionality to your Swing-based apps The Java 2 printing APIs give applications developers the ability to add print capabilities to Java applications and signed applets. The Java 2 printing system consists of a small number of interfaces and classes that encapsulate print-related entities such as printer jobs, page formats, printable pages, and collections of printable pages.This month, we’ll add Java 2 print capabilities to our old pal, the Swing Forum. But first we’ll investigate the basics of the Java 2 printing system, including its operating principles and high-level features.Java 2 printing basicsThe Java 2 printing system offers applications the ability to print AWT text and graphics to any standard printer, whether networked or local. The printing system is a “callback” system, which means the system, not the printing application, drives the print process. In practice, this means the system determines the order in which pages are printed and makes callbacks into programmer-defined objects that know how to print pages. This architecture is similar to the 1.1 delegation event model and is highly analogous to the way component painting works. The printing system consists of two entity types: job-control entities, which control printer jobs, and document-related entities, which represent documents to be printed as well as their constituent pages. The printing API is contained in the package java.awt.print.Note: The Java 2 printing system should not be confused with the 1.1 printing system, although it is similar in some respects.Job-control entitiesClasses PrinterJob, PageFormat, and Paper are the job-control entities responsible for printer-job management. The PrinterJob class encapsulates a printer job. PrinterJob operations include retrieving the default page format, popping up print- and page-format dialogs, and printing the job.The PageFormat class represents a set of page-format settings. This class provides offset- and imageable-area information for use in rendering pages.The Paper class represents the physical properties of a piece of paper in the printer. This class is used by the system and is not necessary for applications development. In addition to the job-control entities described above, Java 2 printing makes use of document-related entities that provide both high level and low level document printing services, which we’ll look at next.Document-related entitiesDocument-related entities can be classified as either high level or low level. Pageable and Book are considered high level. In contrast, Printable “page painters” are low level.High-level document-related entities represent whole documents, such as a resume, a book, or any other collection of printable pages. A document is a collection of pages in which each page or group of pages may have different formats. For example, a business letter might consist of a “first page” that displays letterhead and standard header information and the beginning of the letter’s text. This page may be followed by a series of additional pages containing the rest of the letter’s text.The Pageable interface provides applications developers with high-level document-related services. The Java 2 API supplies the Book class, which is an implementation of Pageable, so that developers don’t have to write an implementation from scratch.Book allows the developer to create an ordered list of document sections. Each document section consists of an instance of PageFormat associated with a page painter (see below) and an int that specifies the number of pages in the section. Books are used by appending groups of pages to them. Here’s the correct syntax: book.append (Printable painter, PageFormat pageFormat, int numpages); High-level document-related entities make use of low-level entities to do the actual work of printing pages.A low-level page painter is an object that knows how to render a single kind of page. It implements the Printable interface, which has one method: public int print (Graphics graphics, PageFormat pageFormat, int pageIndex) throws PrinterException When the system calls this method on the page painter object, the page painter’s implementation will render the page specified by pageIndex, using the page format object pageFormat. Supplied graphics context rendering and component painting are done in exactly the same way.The application doing the printing may use multiple page painters, each of which knows how to render a different kind of page. For example, most documents have a title page, table of contents, and pages of content. Each of these page types could have an associated page painter, which would be collected in a Pageable implementation, such as an instance of Book.A single page painter can render multiple pages of the same type. In this case, the painter is responsible for rendering each page as it is requested by the system. The system may request pages in order or out of order, and it may request the same page more than once. Printable jobs versus pageable jobsA given printer job, represented by an instance of PrinterJob, can be either printable or pageable.Printable jobs are simple jobs that consist of a single page painter. As noted above, a single page painter can paint multiple pages, in which case the system keeps calling the painter’s print () method until the method returns Printable.NO_SUCH_PAGE.Pageable jobs represent documents. Pageable jobs can consist of multiple page formats, each associated with a Printable page painter. Pageable jobs also have the advantage of allowing the system to find out in advance the number of pages in the document, so that the user can choose which pages to print by means of the print dialog. The application specifies the type of printer job it wants by using one of the following two calls on the PrinterJob object (job is used as an example):job.setPrintable (Printable painter);job.setPageable (Pageable document);Now that we understand the Java 2 printing system, it's almost time to apply our knowledge -- but first, a quick peek at the Swing Forum. Swing Forum basicsIf you'll recall, the Swing Forum is a simple client/server discussion-forum system in which the client is implemented using Swing. The Swing Forum uses a <code>JTree</code> to allow users to access discussion threads and messages within those threads. Swing Forum articles are encapsulated in the <code>ForumArticle</code> class and are stored as user objects aggregated by nodes in the tree. To get the instance of <code>ForumArticle</code> associated with a node, the following incantation is used (when the article is selected from the tree by the user): ForumArticle art = (ForumArticle) node.getUserObject (); The client display area used to display messages for reading is a TextArea class. After the user selects an article from the tree and the application obtains the correct instance of ForumArticle, the application sets the display area's text to the article text stored in the ForumArticle: readArea.setText (art.getText ()); We're now ready to add message printing functionality to the Swing Forum. Add printing to the Swing ForumWith the addition of message printing, Swing users can print either a selected article or all the articles in a message thread, simply by selecting the appropriate node in the tree and then selecting File-Print Current from the menu bar. To add print functionality, we'll define a new menu item and event handler, a print-management helper method, a page painter for forum articles, and a <code>Thread</code> subclass to do the printing in a nonblocking fashion. The five steps of this process are as follows: Step 1. Add a new action printCurrentAction = new AbstractAction ("Print Current Article") { public void actionPerformed (ActionEvent e) { printCurrentArticle (); } }; Step 2. Add the action to the menu bar void layOutMenuBar () { menubar = new JMenuBar (); JMenu m = new JMenu ("File"); m.add (closeAction); m.add (printCurrentAction); ... Step 3. Create the printCurrent () method The <code>SwingForum</code> class uses the <code>printCurrent ()</code> method to kick off the printing process when the user actuates the process from the menu: synchronized void printCurrent () { // print based on the currently selected node TreePath path = tree.getSelectionPath (); if (path == null) { // nothing selected return; } DefaultMutableTreeNode node; node = (DefaultMutableTreeNode) path.getLastPathComponent (); int depth = path.getPathCount (); ArrayList list = new ArrayList (); if (depth == 2) { // node is a thread Enumeration children = node.children (); while (children.hasMoreElements ()) { DefaultMutableTreeNode child; child = (DefaultMutableTreeNode) children.nextElement (); list.add (child.getUserObject ()); } } else if (depth == 3) { // node is an article list.add (node.getUserObject ()); } if (list.size () > 0) { // do the actual printing in another thread new PrintingThread (list).start (); } } The <code>printCurrent ()</code> method first determines if a valid selection (thread or article) has been made. If so, it adds the selected <code>ForumArticle</code> instance(s) to an <code>ArrayList</code>. If the list turns out to have items in it, <code>printCurrent ()</code> creates and starts a new <code>PrintingThread</code> object, and then passes in the list. Note that since the items backing the list are immutable (an article object is never modified after it has been created), there is no need to clone the list to put it in another thread. Even in the unlikely event that the backing article object disappears from the tree (say, because the server removes the article and the client refreshes from the server while the print is in progress), the article object will still exist in the list until the printing thread goes out of scope. Step 4. Create the PrintingThread class The <code>PrintingThread</code> class is responsible for the application's print logic: public class PrintingThread extends Thread { ArrayList list; public PrintingThread (ArrayList l) { list = l; } public void run () { PrinterJob job = PrinterJob.getPrinterJob (); Book book = new Book (); // cover page could be appended to book here ForumArticlePagePainter fapp = new ForumArticlePagePainter (list); PageFormat pf = job.pageDialog (job.defaultPage ()); int count = fapp.calculatePageCount (pf); book.append (fapp, pf, count); job.setPageable (book); if (job.printDialog ()) { try { job.print (); } catch (PrinterException ex) { ex.printStackTrace (); } } } } At runtime, the PrintingThread object first obtains a new printer job by using the static method call PrinterJob.getPrinterJob(). It then creates a new Book. PrintingThread next instantiates the ForumArticlePagePainter fapp, and a page dialog pops up to allow the user to set margins and so forth. It then sends a query to fapp to cause it to calculate the number of pages the job will include, based on the user's previous PageFormat selection. Based on the page count, PrintingThread appends a new section -- containing the painter, the current PageFormat, and the current page count -- to Book. Finally, PrintingThread hands the print job to Book to print and starts the print job with a call to job.print (). Step 5. Create the ForumArticlePagePainter class ForumArticlePagePainter is the most interesting class we'll add to the Swing Forum. This class is responsible for rendering ForumArticle pages when the system requests them. Since it must know how to render pages, it should provide an operation to allow a Pageable such as Book to precalculate the total number of pages in a job. The difficulty in designing this class stems from the fact that the PageFormat passed into print() is not clearly guaranteed to be the same as the one used to precalculate the page size for the Book instance. The user conceivably could have changed it with the print dialog Properties option. To guard against this possibility, PageFormat instances passed into the precalculate method are kept separate from those passed into the print() method. If it weren't for this consideration, we could have designed the class to take a PageFormat object as well as the ArrayList of ForumArticle in its constructor. The PageFormat object would have become part of the object's state and the precalculation method wouldn't need to be parameterized. Note: The repagination functionality must be separate from rendering functionality because we want the ability to calculate the page count independently from the actual printing. We therefore create a division in functionality in which the "repaginator" puts lines of text in the ArrayList pages and the "renderer" just draws. In the sections below, we'll look first at the code listing, followed by the explanation. import java.awt.*; import java.awt.print.*; import java.util.*; public class ForumArticlePagePainter implements Printable { ArrayList articles, pages; PageFormat curPageFormat; Font font = new Font ("TimesRoman", Font.PLAIN, 12); public ForumArticlePagePainter (ArrayList l) { articles = l; } public int calculatePageCount (PageFormat pf) { // calculate pagecount based on pf and articles ArrayList pgs = repaginate (pf); return pgs.size (); } The above portion of the class represents the usual setup. It also contains the interface method <code>calculatePageCount ()</code>. public int print (Graphics g, PageFormat pf, int idx) throws PrinterException { // Printable's method implementation if (curPageFormat != pf) { curPageFormat = pf; pages = repaginate (pf); } if (idx >= pages.size ()) { return Printable.NO_SUCH_PAGE; } g.setFont (font); g.setColor (Color.black); renderPage (g, pf, idx); return Printable.PAGE_EXISTS; } In the above listing, the <code>print ()</code> method prints the page to the graphics context that is passed into it. It stores the latest <code>PageFormat</code> in an instance variable. Subsequent repaginations occur only if a different <code>PageFormat</code> object comes into the method. This shouldn't happen, so this optimization allows us to avoid redundant repaginations. Of course, if the current <code>PageFormat</code> instance were modified, we wouldn't be able to detect it. However, this would be nonsensical because it would mean that the page formatting had changed in midprint! Therefore, we can safely ignore this possibility. The <code>ForumArticlePagePainter</code>'s <code>renderPage()</code> method does the actual drawing on the graphics context. ArrayList repaginate (PageFormat pf) { // step through articles, creating pages of lines int maxh = (int) pf.getImageableHeight (); int lineh = font.getSize (); ArrayList pgs = new ArrayList (); Iterator it = articles.iterator (); while (it.hasNext ()) { ForumArticle art = (ForumArticle) it.next (); ArrayList page = new ArrayList (); int pageh = 0; // headers page.add ("Author: " + art.toString ()); page.add (" "); pageh += (lineh * 2); // body StringTokenizer st = new StringTokenizer (art.getText (), "n"); while (st.hasMoreTokens ()) { String line = st.nextToken (); if (pageh + lineh > maxh) { // need new page pgs.add (page); page = new ArrayList (); pageh = 0; } page.add (line); pageh += lineh; } pgs.add (page); } return pgs; } In the previous section, the <code>repaginate ()</code> method calculates the size of lines of text and puts as many as it can fit into a page, which is implemented as an <code>ArrayList</code> of <code>String</code>. As new pages are created and filled up, the method adds them to the <code>pages ArrayList</code>. void renderPage (Graphics g, PageFormat pf, int idx) { // render the lines from the pages list int xo = (int) pf.getImageableX (); int yo = (int) pf.getImageableY (); int y = font.getSize (); ArrayList page = (ArrayList) pages.get (idx); Iterator it = page.iterator (); while (it.hasNext ()) { String line = (String) it.next (); g.drawString (line, xo, y + yo); y += font.getSize (); } } } Here, the renderer simply loops through <code>pages</code>, drawing the lines it reads there. And that's all there is to the <code>ForumArticlePagePainter</code> class. The figure below illustrates the Swing Forum printing subsystem we've developed. <figure><img height="296" src="https://images.techhive.com/images/idge/imported/article/jvw/1999/06/swingforum_printing_sys-100157959-orig.gif" width="464"/><figcaption>The object model for the Swing Forum printing subsystem</figcaption></figure>Java 2 printing issuesJava 2 printing is pretty clean from the development perspective, but the developer community has reported some issues. The most important of these pertain to overly large spool-file size and its subsequent sluggishness. Also, printing support is limited to Win32 and Solaris -- other platforms may not work properly. There are other bugs as well that may be worth searching though if you need to do a production implementation. (See <a href="#resources">Resources</a> for more details.) Use itUsing the print-enabled Swing Forum is basically the same as using the nonprinting version. First you start the server, then you start the client: Step 1. Start the serverStart a command prompt or xtermChange to the server subdirectoryjava ForumSocketServerStep 2. Start the client Start a command prompt or xtermChange to the client subdirectoryjava SwingForumLauncher http://localhost:5000/Step 3. Print something! There's a message or two supplied with the local server; you can add your own as well. And most of all, enjoy! ConclusionIn this installment, we've seen the basics of how to add Java 2 printing to a Swing application. We've covered the highlights of the Java 2 API, such as document-oriented interfaces and classes, and job-oriented classes such as PrinterJob. Extensions to what we've developed here are of course possible; in particular, an excellent extension would be to modify the repaginate() method in ForumArticlePagePainter to wrap lines that extend beyond a set length. JavaSoftware DevelopmentTechnology Industry