Using Design Patterns to Simplify Printing in Java 1.1 *

By James W. Cooper
IBM Thomas J. Watson Research Center

Java 1.1 and following versions introduced some simple printing methods that you could call from Java applications. Because of the Java security model, Web page applets are not allowed to print to your printer, but it is likely that this restriction will be relaxed in future versions of Java.

We will start with the relatively primitive print methods that Java provides and then examine object- oriented programming techniques. These techniques will allow us to build a more robust and useful Printer object.

Fundamentals of Printing in Java

Java 1.1 introduces the PrintJob class and the additional Container method print(Graphics g), which constitute the entire printing system. You obtain an instance of the PrintJob class from the Frame's Toolkit class.

PrintJob pj = getToolkit().getPrintJob(frm, "printing", null);

The first argument must be the Frame of the application, the second any name for the print job, and the third can be either an instance of the Properties class or a null. Since the Java printing system does not support any properties, you can use null just as well.

The PrintJob class has the following methods:

end() Ends the print job and does any necessary cleanup.
finalize() Ends this print job once it is no longer referenced.
getGraphics() Gets a Graphics object that will draw to the next page.
getPageDimension() Returns the dimensions of the page in pixels.
getPageResolution() Returns the resolution of the page in pixels per inch.
lastPageFirst() Returns true if the last page will be printed first.

A Simple Printing Program

The simplest printing program is one that
  1. creates a Frame
  2. obtains a PrintJob object
  3. obtains a printer Graphics object
  4. draws into the printer Graphics object
  5. disposes of the printer Graphics object, which ejects the page
  6. ends the PrintJob
Such a simple program is shown below:

import java.awt.*;
class helloPrinter extends Frame
{
 public helloPrinter()
 {
//get the print job
  PrintJob pjob = getToolkit().getPrintJob(this, "Printer", null);
  Graphics pg = pjob.getGraphics();      //get the graphics
  print(pg);                             //print something
  pg.dispose();                          //end the page
  pjob.end();                            //end the job
  System.exit(0);                                //and quit      
 }
 //-----------------------------------------
 public void print(Graphics g)
 {
   g.setFont(new Font("SansSerif", Font.PLAIN, 12));
   g.drawString("Hello Printer", 100, 100);
 }
 //-----------------------------------------
 static public void main(String argv[])
 {
   new helloPrinter();
 }
}

You can observe a number of things about this simple program that will help us create more general printing classes:

  1. The printer job must be created in a Frame. Thus, printer jobs are almost always visual applications.
  2. When you create a PrintJob class, the system printer dialog appears; you may then select the printer type and the number of copies you want.
  3. Although the printer job must be created in a Frame, there is no requirement that the Frame be visible or have a nonzero size.
  4. Before writing text to the printer, you must select a font.
  5. Printing takes place by drawing to a printer Graphics object.
  6. Printing takes place a page at a time, and each page has its own Graphics object. The dispose() method causes the page to be completed and to eject.

Drawbacks to Simple Printing Approach

This simple printing program is obviously a rather primitive approach to printing. We have to draw every string of text and specify every line break and font ourselves, as well as manage each page of output separately. We'd like to build a more powerful Printer object that would at least take care of fonts and line breaks more effectively. We'd also like it to hide most of this complexity.

A Printer Class

The Printer class we are going to build will still have to be in a Frame. However, it can be any container that we can add to a Frame rather than just the Frame container itself. We are going to create the printer object by deriving it from the Canvas object. Under normal conditions, this Canvas will be invisible and will be of minimal size: 1 x 1 pixels. However, since we are going to do our drawing into a graphics object within that canvas, we have the opportunity to create a page preview method almost for free simply by displaying the Canvas and calling its paint method.

Design of the Printer Class
Let's consider what we want the class to do for us. We'd like to be able to print strings, print lines with carriage returns, change fonts, and print columns that line up using tab stops. We'd also like to preview the text on the screen.

In the ideal case, we'd like our Printer object to take care of such niceties as arranging text into more than one column, even though we will print it sequentially. In order to allow for that eventuality, our design ought to keep the elements of the printed page stored in the Printer object until the page is completed and then print them all out at once. This is also required if we want to be able to preview the page.

Methods in the Printer Class
We'd like to be able to manage printing and movement to new lines, so we'd like to be able to have both a print and a println method:

printer = new Printer(.. .);    //create object
printer.print("Hello ");        //print text
printer.println(" printer);     //print text and newline

We'd also like to be able to set new fonts:

printer.setFont(font); //set a new font 
and we'd like to be able to move to tab stops:
printer.tab(35);        //move to column 35 
In addition, we'll need to move to new pages without having to create a whole new object every time:
printer.newPage();      //page eject
and, of course, we'll need to end the printer job when we are finished:
printer.endJob();         //end printer job

Let's consider how we can implement these different methods so we can store the various commands inside the printer object and "play them back" when we print them or preview them on the screen. They are really quite disparate--made up of strings, fonts, and line advances.

Furthermore, if we continue to expand this printer object, we'd like to be able to represent graphic lines and images.

A Printer Object Vector
Whenever we have an indeterminate number of objects to store, our first thought is to store them using a Vector.

But what kind of objects will we store and how can we be sure which one will be next when we print them? Let's for a moment assume that we have String objects, Font objects, and NewLine objects stored in the Vector.

If we wanted to print them, we'd have to determine the object type and then call the right method for that type:

for (int i =0; i< objects.size; i++)
  {
  obj = objects.elementAt(i);

  if (obj instanceof String)
                print((String)obj);
  if (obj instanceof Font)
                //etc..

This is not only confusing to read, but more procedural than object-oriented. Rather than having an ever- increasing "skip chain" of if statements, let's try to recast this as a series of objects that know how to print (or draw) themselves.

The printerObject Class
Recognizing that we want to draw each object in whatever manner is appropriate, we'll design a printer object class that has a draw method. We'll have to tell it which Graphics surface to draw on and at which coordinates the drawing is to start:

abstract class printerObject
{
   abstract void draw(Graphics g, Point p);
}

Then we'll derive specific instances of this class from this abstract class, each of which will carry out the draw method differently. The advantage is that we'll no longer have to check the object type: each object ( String, Font, or NewLine) already has an appropriate draw method.

Recognizing the Design Patterns
In fact, if we think about this in terms of Design Patterns, we realize that each of these objects is actually a Command class. We've simply named the one method as draw instead of Execute, but the principle as the same.

In addition, as we create and add these various printer objects, we are actually using the Printer object as a Factory class, which generates one of these three different types of printerObject classes, depending on the operation we want to carry out.

We can then print the entire set of objects by simply moving through the vector and calling each printObject's draw() method.

public void print(Graphics g)
 {
    printerObject p;
    f.setFont(fnt);      //always start with some font
 for (int i = 0; i < objects.size(); i ++) 
         {
    p = (printerObject)objects.elementAt(i);

    p.draw(g, pt);
         }
 }

Note that while we get back general Objects from the Vector and we have to cast them to printerObjects, we don't have to know at any time which object we are actually drawing: they all have a draw() method.

The printObject Classes

Each of these classes contains a draw method that operates on the Graphics object and Point object that we pass in.

Each one can have a different constructor if we need to pass in different information, since we didn't specify a constructor as part of our abstract class definition.

The printString Class
This class prints a String at the current x,y position and advances the x-position to just beyond the end of the string:

class printString extends printerObject
{
   String st;
   public printString(String s)
   {
      st = s;        //save the string
   }
   //---------------------------------------------------   
   public void draw(Graphics g, Point p)
   {
   g.drawString(st, p.x, p.y);    //draw it
        //add in the width of the string
   p.x +=
                g.getFontMetrics(g.getFont()).stringWidth(st);
   }
}

Note that since the drawing position is passed in as a Point object rather than as an (x, y) pair, it is passed in by reference, and the change we make in p.x is reflected in that Point object in the calling routine.

The newLine Class
To move on to another line, we simply find the height of the current font, advance the y position by that amount, and reset the x position to the left side or 0.

class newLine extends printerObject
{
 //---------------------------------------------------   
   public void draw(Graphics g, Point p)
   {
   p.x = 0;               //reset x
        //advance y
   p.y += g.getFontMetrics(g.getFont()).getHeight();
   }
}

The setFont Class
Setting a new font as part of this Vector playback approach means that we execute the draw method on a very simple setFont ckass.

class printFont extends printerObject
{
   Font fnt;
   public printFont(Font f)
   {
      fnt = f;       //save the font
   }
   //------------------------------------ 
public void draw(Graphics g, Point p)
        {
   g.setFont(fnt);        //set the font
   if (p.y <= 0) 
      {
       p.y = g.getFontMetrics(fnt).getHeight();

      }
        }
}

In addition, if this font is at the top of a page where the y-coordinate is 0, we advance the y-coordinate by the height of the font, since fonts are drawn from the bottom up.

The printTab Class
This class advances the x-position by an amount equal to the number of character widths we specify. The question is: what character and what font size shall we use? The answer, of course, is that it doesn't matter how we determine the width of one "tab character," as long as we are consistent throughout our printing task.

To make sure that there is only one such width in use in all instances of the printTab class, we make the variable tab_width a static variable.

This way, all copies of the class will automatically refer to this same value. We set it once by default, if it is zero, to the width of the "W" character in whatever font we initialize as the reference tab font in the constructor. Once this value is set, this code is not executed again.

class printTab extends printerObject
{
   static int tab_width = 0;
   int tab_dist;
   Font tabFnt;
//---------------------------------------
   public printTab(Font tbFont, int tabdist)
   {
   tabFnt = tbFont;
   tab_dist = tabdist;
   }
//---------------------------------------
 public void draw(Graphics g, Point p)
 {
  if (tab_width == 0)
  tab_width = 
                g.getFontMetrics(tabFnt).stringWidth("W");
  if (p.x < (tab_dist*tab_width)) 
    {
    p.x = tab_dist * tab_width;
    }
 }
}

Drawing a tab of "n" character positions simply amounts to moving the x-position over by that number of tab character widths.

If the x-position is already past that point, we do nothing. Alternatively, we could advance by one line and move to that tab position on the new line.

The Printer Class Methods

Now that we've defined the printerObjects we'll be using, it is easy to see how the Printer object will use them. We simply create a new instance of each kind of printerObject and add it into the vector:

public void setFont(Font f)
{
  objects.addElement(new printFont(f));
}
//--------------------------------------
public void print(String s)
{
   objects.addElement(new printString(s));
}
//---------------------------------------------------   
public void println(String s)
{
    print(s);
    objects.addElement(new newLine());
}
//--------------------------------------
public void tab(int tabstop)
{
   objects.addElement(new printTab(tabFont, tabstop));
}

In addition to these printing methods, we need a newPage and endJob method. The newPage method actually does all the work. It calls the print(g) method, which goes through the entire list of printerObjects and calls their draw() methods.

public void newPage()
{
 if (pjob == null) 
   pjob = getToolkit().getPrintJob(f, "Printer", null);
 pg = pjob.getGraphics();
 print(pg);                      //print the whole vector
 pg.dispose();           //eject the page
 pt =new Point( 0,  0);  //reinitialize print posn
 objects = new Vector(); //and print objects
}
//-----------------------------------
public void print(Graphics g)
 {
  printerObject p;
  f.setFont(fnt);       //always start with some font
  for (int i = 0; i < objects.size(); i ++) 
        {
    p = (printerObject)objects.elementAt(i);

    p.draw(g, pt); //draw each object
        }
 }

The endJob() method just closes down the PrintJob. If you neglect to call it, we can also call it from the finalize() method.

public void finalize()
{
   if (objects.size() > 0) //make sure all printed
      newPage();
   endJob();
}
//--------------------------------------
public void endJob()
{
   pjob.end();
}

Previewing a Printed Page

With the Printer object we have just developed, printing the entire page to the screen is very simple: we just call the same print(g) method, using the screen Graphics object instead of the printer Graphics object. And where do we get the screen Graphics object? We get it from the standard paint() method, which we in turn use to call our print method.

public void paint(Graphics g)
{
   pt = new Point(0, 0);  //always start at top
   print(g);      //displays text as preview
}

The printer Program

This program creates a small 100 x 100 window with two buttons: Show and Plot. It creates a Printer object and adds it to the Frame. Then it prints 3 lines of text in 3 different fonts, using tabs, into an instance of the Printer object. When you click on the Print button, it sends these lines to the printer, which first brings up the Windows (or other operating system) printer dialog. Then it prints a page.

When you click on the Show button, it expands the window to the page size returned from the PrintJob and displays the text. The initial program window is shown in Figure 1, and the preview text window is shown in Figure 2.


Figure 1: The printing.java program, showing the two command buttons.


Figure 2: The page preview feature of the printing.java program.

Getting the Page Size
The PrintJob class's getPageDimension() method can only work if an instance of the PrintJob class has been created. Since it is possible that you might press the "Show" button first, we provide a workaround that does not create the PrintJob class if it doesn't already exist, because creating one always brings up the Windows printer dialog. This can be annoying if you only want to view the text. For a US 8 x 11 inch page, the default size returned by the getPageDimension() method is 620 x 790 pixels. Note that the pixel size it returns has nothing to do with printer resolution. It simple returns a pair of numbers proportional to the page shape so that you can position text within that page. Our workaround is simply for returning those values directly if the PrintJob class is not yet created:

public Dimension pageSize()
{
if (pjob == null) 
   return new Dimension(620, 790);
  
else
   {
   pjob = getToolkit().getPrintJob(f, "Printer", null);
   return pjob.getPageDimension();
   }
}

The Complete printing Program

We have discussed a number of the parts of the program for printing and the Printer class itself. The complete program is show below. It is really quite simple compared to all of the discussion leading up to it.

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

public class printing extends Frame
  implements ActionListener, WindowListener
{
   Button b, show;
   Printer printer;
//---------------------------------------------------
   public printing()
   {
     super("print test");           //frame title;
     addWindowListener(this);
     Panel p = new Panel();         //spaces buttons
     add("South", p);
     b = new Button("Print");               //2 buttons
     p.add("South", b);
     b.addActionListener(this);
     show = new Button("Show");
     p.add(show);
     show.addActionListener(this);
   
     printer = new Printer(this);   //printer added
     
     setBounds(100,100,100,100);
     setVisible(true);
     loadStrings();                 //do printing here
   }
//---------------------------------------------------
private void loadStrings()
 {
 //puts the text into the Printer object
 int i = 1;
 printer.setFont(new Font("SanSerif", Font.PLAIN, 12));
   
 printer.print(i++ +"."); printer.tab(10);
 printer.println("Hello printer");
   
 printer.print(i++ +".");  printer.tab(10);
 printer.setFont(new Font("SanSerif", Font.BOLD, 12));
 printer.println("A bold stroke");
   
 printer.print(i++ +".");  printer.tab(10);
printer.setFont(new Font("SanSerif", Font.ITALIC, 18));
printer.println("but emphatic");
}
//---------------------------------------------------
public void actionPerformed(ActionEvent ev)
 {
  Object obj = ev.getSource();
  if (obj == b) 
   {
   printer.newPage();     //print on page
   printer.endJob();
   }
  if (obj == show) 
   {
    showPreview(); //or on screen
   }
 }
//---------------------------------------------------   
   private void showPreview()
   {
      Dimension d = printer.pageSize();
      setBounds(0 , 0, d.width, d.height);
      printer.setBounds(0, 0, d.width, d.height);
      printer.setVisible(true);   
   }
//---------------------------------------------------   
   static public void main(String argv[])
   {
      new printing();
   }
   //-------------------------------------------------

   public void windowClosing(WindowEvent wEvt)
   {
    System.exit(0);   //exit on System exit box clicked
   }
   public void windowClosed(WindowEvent wEvt){}
   public void windowOpened(WindowEvent wEvt){}
   public void windowIconified(WindowEvent wEvt){}
   public void windowDeiconified(WindowEvent wEvt){}
   public void windowActivated(WindowEvent wEvt){}
   public void windowDeactivated(WindowEvent wEvt){}
   
}
//===============================================
class Printer extends Canvas
{
   Frame f;          //parent frame
   PrintJob pjob;    //printjob object
   Graphics pg;      //printer graphics handle
   Vector objects;   //array of printer instructions
   Point pt;         //current printer position
   Font fnt;         //current font
   Font tabFont;     //font to use in tab calcns
public Printer(Frame frm)
{
   f = frm;          //save form
   f.add(this);      //add this object to form
   setVisible(false);//but do not show it
   pjob = null;      //no print job yet

   pt =new Point( 0,  0);  //initialize print posn
   objects = new Vector(); //and print objects
   tabFont = new Font("MonoSpaced", Font.PLAIN, 12);
   fnt = new Font("SansSerif", Font.PLAIN, 12);
   
}
//--------------------------------------
public void setFont(Font f)
{
  objects.addElement(new printFont(f));
}
//--------------------------------------
public void print(String s)
{
   objects.addElement(new printString(s));
}
//---------------------------------------------------   
public void println(String s)
{
    print(s);
    objects.addElement(new newLine());
}
//--------------------------------------
public void newPage()
{
 if (pjob == null) 
   pjob = getToolkit().getPrintJob(f, "Printer", null);
   pg = pjob.getGraphics();
   print(pg);
   pg.dispose();
   pt =new Point( 0,  0);  //initialize print posn
   objects = new Vector(); //and print objects
}
//--------------------------------------
public void finalize()
{
   if (objects.size() > 0) 
      newPage();
   endJob();
}
//--------------------------------------
public void endJob()
{
   pjob.end();
}
//--------------------------------------
public void tab(int tabstop)
{
   objects.addElement(new printTab(tabFont, tabstop));
}
//--------------------------------------
public Dimension pageSize()
{
if (pjob == null) 
   return new Dimension(620, 790); 
else
   {
   pjob = getToolkit().getPrintJob(f, "Printer", null);
   return pjob.getPageDimension();
   }
}
//---------------------------------------------------   
public void paint(Graphics g)
{
   pt = new Point(0, 0);
   print(g);      //displays text as preview
}
//---------------------------------------------------   
public void print(Graphics g)
 {
    printerObject p;
    f.setFont(fnt);      //always start with some font
 for (int i = 0; i < objects.size(); i ++) 
 {
    p = (printerObject)objects.elementAt(i);

    p.draw(g, pt);
 }
    
 }
}  //end class
//===================================================

abstract class printerObject
{
   abstract void draw(Graphics g, Point p);
}
//===================================================

class newLine extends printerObject
{
 //---------------------------------------------------   
   public void draw(Graphics g, Point p)
   {
   p.x = 0;

   p.y += g.getFontMetrics(g.getFont()).getHeight();

   }
}
//=====================================================

class printString extends printerObject
{
   String st;
   public printString(String s)
   {
      st = s;

   }
   //-------------------------------------------------
public void draw(Graphics g, Point p)
 {
 g.drawString(st, p.x, p.y);
 p.x += g.getFontMetrics(g.getFont()).stringWidth(st);

 }
}
//===================================================

class printFont extends printerObject
{
   Font fnt;
   public printFont(Font f)
   {
      fnt = f;

   }
  //--------------------------------------------------   
public void draw(Graphics g, Point p)
 {
   g.setFont(fnt);
   if (p.y <= 0) 
      {
       p.y = g.getFontMetrics(fnt).getHeight();

      }
 }
}
//===================================================

class printTab extends printerObject
{
   static int tab_width = 0;

   int tab_dist;
   Font tabFnt;
   public printTab(Font tbFont, int tabdist)
   {
   tabFnt = tbFont;

   tab_dist = tabdist;

   }
   public void draw(Graphics g, Point p)
   {
   if (tab_width == 0)
    tab_width = 
                g.getFontMetrics(tabFnt).stringWidth("W");

   if (p.x < (tab_dist*tab_width)) 
     {
     p.x = tab_dist * tab_width;
     }
   }
}

Summary

In this tutorial, we examined the various methods of printing in Java. We first defined the simplest printing program and then developed a printing class that uses both the Command pattern and the Factory pattern. These patterns make it possible to produce an array of printerObjects, which amount to a series of printing instructions for strings, fonts, new line characters, and tabs. This type of object-oriented programming emphasizes the importance of using basic design patterns in creating simple, maintainable programs.


*  Excerpted from Principles of Object-Oriented Programming in Java 1.1, by James W. Cooper, Ventana Press, 1997.



JavaTM is a trademark of Sun Microsystems, Inc.

Other companies, products, and service names may be trademarks or service marks of others.

Copyright    Trademark



  Java Education Java Home  
IBM HomeOrderEmployment