Ho in mente parecchie idee carine sull'applicazione che
sta fungendo da base di studio per MCAD. Prima di passare a cose più serie, o
comunque a cose che bene o male ho già visto, (come ADO.NET o XML) ho
preferito questa volta vedere qualcosa che non ho mai preso in considerazione:
la stampa
. Non che le mie applicazioni non abbiamo
mai stampato, semplicemente mi sono sempre appoggiato a librerie o software
esterni (come il mitico Crystal Reports).
La Study Guide di Lorenzo ci parla di alcuni controlli e di alcuni component
da mettere sulla WF per implementare e per permettere al nostro software di
stampare. Essi sono:
- PrintDocument
- PrintPreviewDialog
- PageSetupDialog
- PrintDialog
Sono tutte classi contenute nel framework .NET per fare tutto quello che ci
serve. In questo post (e presumibilmente nel prossimo), ci occuperemo dei primi
due. Il più importante è sicuramente la classe
PrintDocument: contiene infatti il documento vero e proprio che
deve essere stampato, con tutto il testo, la grafica e in generale l'output che
deve essere mandato alla stampante. Vi state chiedendo come funziona? Vediamolo
subito, sono qui per questo!!
Innanzitutto, una precisazione. Venendo da una lunga esperienza con VB6, sono
abituato ad aggiungere sulla WF i controlli che mi servono a design-time,
indipendentemente dal fatto che mi servano sempre oppure no. Per esempio, se su
un form di VB6 devo lanciare una stampa, mettevo il controllo di Crystal
Reports, se dovevo leggere dalla COM, mettevo il controllo MSCOMM.OCX, anche se
dovevo leggere dalla seriale solo una volta ogni tanto. Questo con .NET
può (e, aggiungo io, deve, se possibile) essere
evitato. Perchè? Perchè aggiungere un controllo a design-time
vuol dire aggiungere codice, appesantire la InitializeComponent(); e quindi eseguire del codice che magari non sempre
serve, istanziando classi, facendo aspettare l'utente più del dovuto e
consumando memoria inutilmente. Il grande vantaggio di .NET è quello di poter
dichiarare oggetti solo quando servono
, come è il caso degli esempi che sto per andare a fare sul mio
progetto.
Quindi, morale della favola. E' vero che possiamo trascinare
dalla Toolbox il component PrintDocument, ma noi non facciamolo: io ho
preferito aggiungere un Button btnPrint e creare la
seguente function:
private void btnPrint_Click(object sender, System.EventArgs e)
{
PrintDocument prnDoc = new PrintDocument();
prnDoc.PrintPage += new PrintPageEventHandler(this.printBirthday);
prnDoc.DocumentName = resMan.GetString("TITLE_DOCUMENT");
prnDoc.Print();
}
In pratica, istanzio un oggetto della classe
PrintDocument solo quando l'utente clicca sul Button e non
prima, quando potrebbe anche non servire. Poi, delego l'evento
PrintPage alla function printBirthday(), do un nome al
documento e lancio il metodo Print()
della classe per eseguire la stampa vera e propria. Vi sembra un po' poco, vero?
Devo scrivo il codice per indicare quello che voglio effettivamente stampare?
Vediamolo!
Il succo del discorso è nell'evento PrintPage
della classe PrintDocument. Questo evento viene sollevato per ogni
pagina che voglio stampare. Al suo interno, usando GDI+ scrivo, tramite
i metodi della classe Graphics, quello che voglio. Ecco il codice che ho scritto
io (ma ovviamente potrebbe essere qualsiasi cosa):
private void printBirthday(object sender, PrintPageEventArgs ev)
{
// questo evento viene eseguito per ogni pagina
PageNumer++;
Font printFontHeader = new Font("Tahoma", 20, FontStyle.Bold);
Font printFontText = new Font("Tahoma", 14, FontStyle.Regular);
Font printFontFooter = new Font("Tahoma", 9, FontStyle.Italic);
// posizione dell'header
float posy = 50;
float posx = ev.MarginBounds.Left;
// spaziatura tra una riga e l'altra
float spaz = 5;
// scrivo l'header in alto a sinistra
ev.Graphics.DrawString(((PrintDocument) sender).DocumentName, printFontHeader, Brushes.Black, posx, posy);
posy += printFontHeader.GetHeight() + (spaz * 3);
// disegno una linea di separazione
Point p1 = new Point((int) posx, (int) posy);
Point p2 = new Point(ev.MarginBounds.Right, (int) posy);
ev.Graphics.DrawLine(new Pen(Brushes.Red, 1), p1, p2);
posy += 20;
// scrivo una linea per ciascun Item della ListBox
foreach (string item in this.lstResults.Items)
{
ev.Graphics.DrawString(item, printFontText, Brushes.Black, posx, posy);
posy += printFontText.GetHeight() + spaz;
}
// scrivo il numero di pagina in basso a sinistra
posy = ev.MarginBounds.Bottom;
ev.Graphics.DrawString(resMan.GetString("PAGE") + this.PageNumer, printFontText, Brushes.Black, posx, posy);
posx = ev.MarginBounds.Right - 120;
ev.Graphics.DrawString(DateTime.Now.ToShortDateString(), printFontText, Brushes.Black, posx, posy);
}
ev.Graphics.DrawString(((PrintDocument) sender).DocumentName,
printFontHeader, Brushes.Black, posx, posy);
scrivo il titolo del documento prelevato dal file di risorsa ("Lista dei
compleanni" in italiano, "List of birthday" in inglese). Uso un brush dato da
Brushes.Black. La posizione iniziale è data dalle variabili
posx e posy. posx viene inizialmente impostata
in base al margine sinistro (ev.MarginBounds.Left).
Traccio una linea di
separazione tra il titolo e il testo rimanente:
// disegno una linea di separazione
Point p1 = new Point((int) posx, (int) posy);
Point p2 = new Point(ev.MarginBounds.Right, (int) posy);
ev.Graphics.DrawLine(new Pen(Brushes.Red, 1), p1, p2);
Poi comincio un ciclo foreach per ciclare tutti gli
elementi contenuti nella ListBox sulla Windows Form e li scrivo uno per uno
usando ancora una volta DrawString. Notate l'uso di
GetHeight per calcolare la posizione verticale dell'elemento
successivo: questo metodo di Font in pratica ritorna l'altezza
di un testo scritto con quel particolare Font. Aggiungo al valore ottenuto da
GetHeight una variabile spaz per dare un po' più di spazio per
maggiore leggibilità. Con la stessa logica, scrivo in basso a sinistra il numero
della pagina, e in basso a destra la data corrente
(DateTime.Now.ToShortDateString()).
A questo punto, bisognerebbe controllare se serve una nuova pagina: in
tal caso è sufficiente impostare
ev.HasMorePages = true.In questo
modo l'evento printBirthday() viene eseguito di nuovo, creando
una seconda pagina, e così via. Quando tutte le pagine sono state create secondo
il metodo che abbiamo appena visto, l'output va direttamente alla
stampante, senza anteprima, perciò...attenti!
Se volete mostrare prima un'anteprima di stampa,
dovete utilizzare il controllo PrintPreviewDialog, di cui
parlerò la prossima volta. In questo momento, ho alcuni test e un po' di debug
da fare!