MokaByte 51 - Aprile 2001
Foto dell'autore non disponibile
di
Mario Fusco
Stampare con Java 2
Prima dell’introduzione del package java.awt.print a partire dalla jdk1.2, stampare in Java non era certo un task facile. A dir la verità questo package nella jdk1.2 soffriva di numerosissimi bug che lo rendevano praticamente inutilizzabile. Fortunatamente la grande maggioranza di questi bug sono stati fissati nella jdk1.3 per cui adesso stampare in Java è diventato un compito molto più agevole. E questo il motivo per cui il codice che utilizzerò come esempio in questo articolo funziona solo a patto di utilizzare la jdk1.3

La classe PrinterJob è la classe principale del package in quanto è quella demandata al controllo della stampa. Questa classe non ha un costruttore, ma un sua istanza può essere ottenuta invocando il metodo statico PrinterJob.getPrinterJob(). Un’altra classe fondamentale nel package è la classe Book che non è altro che una rappresentazione del documento da stampare che ovviamente può essere costituito da più pagine che possono anche essere di formato differente.Una volta ottenuto un Book vuoto mediante il costruttore Book() è infatti possibile aggiungere ad esso una o più pagine alla volta utilizzando i metodi: 

book.append(Printable painter, PageFormat pageFormat)
book.append(Printable painter, PageFormat pageFormat, int numPages)

Come si vede dalla firma di questi metodi al Book vengono aggiunti degli oggetti che implementano l’interfaccia Printable. Quest’interfaccia è costituita da un unico metodo la cui firma è:

int print(Graphics graphics, PageFormat pageFormat, int pageIndex)

in cui graphics è il contesto in cui la pagina da stampare verrà disegnata, mentre l’oggetto pageFormat contiene informazioni quali le dimensioni e l’orientamento della pagina. L’int ritornato da questo metodo deve essere uno dei due membri statici dell’interfaccia Printable, ovvero:

  • Printable.PAGE_EXISTS che indica che la pagina è stata disegnata con successo
  • Printable.NO_SUCH_PAGE che indica che il parametro pageIndex si riferisce ad una pagina non esistente
Infine, una volta definito il Book, tutto quello che resta da fare è passarlo al PrinterJob tramite il metodo printerJob.setPageable(book), subito prima di far eseguire il PrinterJob stesso usando il metodo printerJob.print(). 

Praticamente tutto quello che c’è da sapere per effettuare una stampa in Java è tutto qui. Nel seguito ho scritto una classe che offre alcune facilities per effettuare la stampa di testo semplice e tabelle, ma soprattutto che chiarisce alcune aspetti dell’utilizzo del package java.awt.print. L’unico costruttore di questa classe (che ho chiamato semplicemente Printer) è il seguente:

public class Printer {

    PrinterJob pj;
    PageFormat pf;
 
    Font font;
    FontMetrics fm;
 
    Graphics graphics;

    Vector itemsToPrint = new Vector();

    public Printer() {
        pj = PrinterJob.getPrinterJob(); 
        pf = pj.defaultPage(); 
        Book dummyBook = new Book();
        dummyBook.append(new DummyPage(), new PageFormat());
        pj.setPageable(dummyBook);
        try {
            pj.print();
        } catch (PrinterException pe) {
            pe.printStackTrace();
        }
    }
}

Come si può notare questo costruttore oltre a creare un nuovo PrinterJob a ad ottenere il suo PageFormat di defualt effettua anche la stampa di un Book. L’oggetto DummyPage (che implementa l’interfaccia Printable come richiesto) aggiunto al Book e poi mandato in stampa è il seguente:

class DummyPage implements Printable {

  public DummyPage() { }

  public int print(Graphics g, PageFormat pf, int pageIndex) 
                     throws PrinterException {
    graphics = g;
      return Printable.NO_SUCH_PAGE;
  }
}

In realtà quindi non viene mandato in stampa assolutamente niente in quanto il metodo print() ritorna Printable.NO_SUCH_PAGE a prescindere dal valore di pageIndex. L’unico motivo per cui viene effettuata questa stampa fittizia è quello di salvare in una variabile di classe l’oggetto Graphics passato al metodo print(). Quest’oggetto verrà utilizzato in seguito come tavolozza su cui disegnare gli elementi (testo e linee) che costituiranno la pagina da stampare.Tramite esso inoltre sarà possibile ottenere il FontMetrics  del Font correntemente utilizzato, che in seguito sarà indispensabile per formattare correttamente all’interno della pagina il testo da stampare. Per aggiungere dal testo da stampare all’oggetto Printer è possibile utilizzare i metodi:

public void setText(String text) {
  itemsToPrint = new Vector();
  itemsToPrint.add(new TextToPrint(text, font));
}

public void appendText(String text) {
   itemsToPrint.add(new TextToPrint(text, font));
}

Entrambi questi metodi aggiungono un oggetto appartenente alla classe TextToPrint al Vector itemsToPrint (definito a livello di classe) con l’unica differenza che il primo svuota il Vector prima di usarlo. La classe TextToPrint  è definita semplicemente come:

class TextToPrint {
    public String text;
    public Font font;

    public TextToPrint(String text, Font font) {
        this.text = text;
        this.font = font;
    }
}

ed ha come unico scopo quello di immagazzinare il testo da stampare associandolo al Font che dovrà essere utilizzato per farlo. A tal proposito per settare il Font da usare nella stampa dei blocchi di testo che seguiranno occorre invocare il metodo:
 

public void setFont(Font font) {
   this.font = font;
}

Allo stesso modo per aggiungere una tabella alla lista degli elementi da stampare è possibile usare uno dei metodi seguenti:
 

public void appendTable(String[] header, Vector data) {
    itemsToPrint.add(new TableToPrint(font, header, data, null, null));
}

public void appendTable(String[] header, Vector data, 
                        int[] columnWidth) {
   itemsToPrint.add(
     new TableToPrint(font, header, data, columnWidth, null));
}

public void appendTable(String[] header, Vector data, boolean[] 
                        rightAlligned) {
   itemsToPrint.add(
     new TableToPrint(font, header, data, null, rightAlligned));
}

public void appendTable(String[] header, Vector data, 
                       int[] columnWidth,boolean[] rightAlligned){
 itemsToPrint.add(new TableToPrint(font, header,
                                   data,columnWidth,
                                   rightAlligned));
}

Tutti questi metodi necessitano come parametro in input di un array di String ciascuna contenente il nome di una delle colonne della tabella e di un Vector in cui ciascun elemento deve essere a sua volta un array di String (della stessa lunghezza del primo) e rappresenta una riga della tabella. Opzionalmente è possibile passare un array di int ciascuno dei quali indica la larghezza relativa della corrispondente colonna della tabella ed un array di boolean ciascuno dei quali (se settato a true) indica che la corrispondente colonna della tabella deve essere allineata a destra. Se si sceglie di non passare questi parametri le colonne vengono disegnate tutte della stessa larghezza e vengono tutte allineate a sinistra. Anche in tal caso ognuno di questi metodi aggiunge un elemento al Vector itemsToPrint, e precisamente un oggetto appartenente alla classe TableToPrint che come la classe TextToPrint  ha lo scopo di immagazzinare informazioni riguardanti la tabella da stampare:

 class TableToPrint {

    public int[] columnWidth;
    public boolean[] rightAlligned;
    public String[] header;
    public Vector data;
    public int columnNr;
    public Font font;
 

  public TableToPrint(Font font, String[] header, 
                      Vector data, int[]
                      columnWidth, 
                      boolean[] rightAlligned){
    this.header = header;
    this.data = data;
    this.columnWidth = columnWidth;
    this.columnNr = header.length;
    this.font = font;
 
    // set the column width
    if (columnWidth == null) 
      setDefaultColumnWidth();
    else {
      int totalWidth = 0;
      for (int i = 0; i < columnNr; i++)
       totalWidth += this.columnWidth[i];
      for (int i = 0; i < columnNr; i++)
        this.columnWidth[i] = this.columnWidth[i] 
                              * 1000 / totalWidth;
    }
    if (rightAlligned == null) 
      setDefaultRightAlligned();
    else
      this.rightAlligned = rightAlligned;
    }
 

  private void setDefaultColumnWidth() {
    int cw = 1000 / columnNr;
    this.columnWidth = new int[columnNr];
    for (int i = 0; i < columnNr; i++)
      this.columnWidth[i] = cw;
  }

  private void setDefaultRightAlligned() {
    this.rightAlligned = new boolean[columnNr];
    for (int i = 0; i < columnNr; i++)
      this.rightAlligned[i] = false;
  }
}

L’interfaccia della classe è ovviamente completato dal metodo che manda in stampa i blocchi di testo e le tabelle precedentemente immagazzinati nel Vector itemsToPrint:

public void print() {
  try {
    pj.setPageable(getPages());
    if (pj.printDialog()) pj.print();
  }
  catch (PrinterException pe) {
    pe.printStackTrace();
  }
}

Si noti che l’effettivo invio in stampa del PrinterJob tramite il comando printerJob.print() è condizionato dal valore ritornato dal metodo printerJob.printDialog(). Invocando questo metodo viene mostrata all’utente una dialog che gli consente di definire l’intervallo di pagine del documento da stampare ed il numero di copie da fare. Questo metodo ritorna false se l’utente decide di annullare la stampa, per cui in tal caso il metodo printerJob.print() non viene invocato. Come già è stato anticipato, per settare il lavoro da mandare in stampa, occorre passare al PrinterJob il Book che si vuole stampare tramite il metodo setPageable(). In tal caso il Book da stampare, viene costruito utilizzando il seguente metodo:
 

private Book getPages() {
    Book book = new Book();
    Vector currentPage = new Vector();

    int yLimit = (int)pf.getImageableHeight();
    int xLimit = (int)pf.getImageableWidth();

    int currX = 0;
    int currY = 0;
 
  for (int i = 0; i < itemsToPrint.size(); i++) {
   if (itemsToPrint.elementAt(i) instanceof TableToPrint) {
     TableToPrint table=(TableToPrint)itemsToPrint.elementAt(i);
     currY=printTable(table, book, currentPage, 
                      currY,
                      xLimit,
                      yLimit);
    }
    else{
      TextToPrint text=(TextToPrint)itemsToPrint.elementAt(i);
      currY = printText(text, book, currentPage, 
                        currY,
                        xLimit,
                        yLimit);
    }
  }

  if (currentPage.size() > 0)
    book.append(new Page(currentPage), pf);
  return book;
}

Come si può notare questo metodo non fa altro che scorrere tutti gli elementi del Vector itemsToPrint e aggiungerli uno ad uno al Book da mandare in stampa utilizzando i metodi printTable() o printText() a seconda che l’elemento da stampare sia una tabella o un blocco di testo piano. Questi metodi non fanno altro che formattare gli elementi da stampare (il testo e le linee che costituiscono i bordi delle celle delle tabelle) all’interno della pagina, utilizzando l’oggetto Graphics proprio come se si stesse disegnando su una porzione dello schermo. E’ a questo punto che si rivela indispensabile l’aver effettuato la stampa fittizia di cui si parla all’inizio dell’articolo. Questo piccolo trucco è infatti, come si è detto, l’unico modo possibile per ottenere il FontMetrics del Font correntemente impiegato e quindi suddividere il testo da stampare in righe della giusta lunghezza. I particolari implementativi di questi metodi non sono a mio avviso fondamentali ai fini della trattazione, ma sono comunque facilmente desumibili dal codice sorgente della classe Printer, allegato a questo documento, essendo peraltro commentati in maniera abbastanza esauriente. 

L’ultima cosa che resta da evidenziare è il fatto che gli elementi formattati all’interno della pagina vengono di volta in volta aggiunti ad un secondo Vector. Quando si è raggiunta la fine della pagina questo Vector viene passato al costruttore della classe Page prima di essere svuotato per la costruzione della pagina successiva:

class Page implements Printable {

    // A Vector of the items that will be printed in this page
    private Vector text;

    /**
     * The contructor
     *
     * @param text The Vector of the items that will be printed in this page
     */
    public Page(Vector text) {
        this.text = text; 
    }
 

 public int print(Graphics g, PageFormat pf, 
                  int pageIndex) throws PrinterException {
   g.setColor(Color.black);

  StringToPrint currentString;
   for (int i = 0; i < text.size(); i++) {
     if (text.elementAt(i) instanceof StringToPrint) {
       currentString=(StringToPrint)text.elementAt(i);
       g.setFont(currentString.font);
       g.drawString(currentString.theString, 
                    currentString.xPos,
                    currentString.yPos);
     }
     else {
       LineToPrint currentLine=(LineToPrint)text.elementAt(i);
       g.drawLine(currentLine.x1,
                  currentLine.y1,
                  currentLine.x2,
                  currentLine.y2);
      }
    }
    return Printable.PAGE_EXISTS; 
  }
}

La classe Page implementa l’interfaccia Printable e quindi definisce tramite il metodo print() le modalità con cui la pagina deve essere stampata e può essere aggiunta al Book che verrà mandato in stampa usando uno dei suoi due metodi append(). 
Il codice sorgente dell'esempio può essere scaricato cliccando qui

Vai alla Home Page di MokaByte
Vai alla prima pagina di questo mese


MokaByte®  è un marchio registrato da MokaByte s.r.l.
Java®, Jini®  e tutti i nomi derivati sono marchi registrati da Sun Microsystems; tutti i diritti riservati
E' vietata la riproduzione anche parziale
Per comunicazioni inviare una mail a
mokainfo@mokabyte.it