Press "Enter" to skip to content

Stampare da una applicazione WPF

Ho pubblicato un post qualche giorno fa, in risposta ad una domanda sul Forum Microsoft dedicato a C# per mostrare come si può creare un report utilizzando Windows Forms e quel che ci mette a disposizione per le stampe.

Considerato che io lavoro in WPF, e che WPF è un sistema per scrivere programmi desktop molto più efficiente, potente ed interessante delle Windows Forms, credo sia opportuno mostrare quali sono gli strumenti che abbiamo a disposizione per effettuare delle stampe da WPF.

Per fare questo, ho creato una piccola applicazione simile a quella usata per windows forms, questa applicazione carica il contenuto della tabella Customers del database storico Northwind su una Datagrid, e ci mette a disposizione 3 buttons che mostrano i 3 metodi principali per stampare da WPF.

mainwindow_01

Vediamo dunque di capire come possiamo stampare il contenuto della grid utilizzando WPF.

Print Control

Il modo più semplice di stampare qualcosa in WPF è disegnarlo su una finestra e poi chiedere al sistema di stamparlo. Print Control dimostra questa funzionalità.

private void PrintControl_Click(object sender, RoutedEventArgs e)
{
    PrintDialog prn = new PrintDialog();
        bool? shown = prn.ShowDialog();
            if (shown.HasValue && shown.Value)
                {
                        prn.PrintVisual(CustomersListGrid, "Printing Just the Grid");
                         
                             }
                        }

Wpf, per darci modo di stampare ci mette a disposizione  l’oggetto PrintDialog, questo oggetto ha al suo interno due metodi per permetterci di stampare i nostri contenuti. In questo caso utilizziamo quello più diretto, ovvero PrintVisual. Questo metodo prende in input due parametri, il primo è un oggetto derivato dalla classe Visual (come ad esempio la nostra DataGrid), e una descrizione. Quello che fa una volta chiamato è semplicemente chiederci su quale stampante e poi stampa sulla pagina della stampante selezionata il controllo esattamente come lo vediamo. Vediamo cosa produce il click del pulsante per spiegare meglio.

print_visual_01

Prima di tutto, PrintDialog ci chiede su quale stampante vogliamo stampare, nel mio caso ho scelto la stampante PDF standard di Microsoft.

print_visual_02

La stampante PDF ci chiede dove salvare il file PDF che sarà prodotto e quale nome dargli, pertanto in questo caso ho creato una cartella temporanea su cui salvare i nostri file.

print_visual_03

Viene creato un file PDF con il nome da noi scelto che adesso andremo ad aprire.

print_visual_04

Come potete osservare, è stata creata una pagina contenente esattamente il controllo come si vede a video nella prima schermata di questo post.

PrintVisual è un metodo davvero potente, ma ovviamente non adatto a stampare una lista, torna invece estremamente utile se vogliamo ad esempio creare una finestra contenente dashboard con controlli grafici e dati contenuti in un unica pagina e poi stampare la suddetta dashboard.

Print As Document

Il secondo tipo di stampa, utilizza il secondo metodo messo a disposizione dalla PrintDialog, questo metodo però ha bisogno di uno specifico tipo di controllo per poter funzionare, ovvero un FlowDocument il tipo di controllo che permette di costruire un documento contenente una o più pagine in un formato simile a quello di un documento Word o Pdf. Pertanto per vedere come funziona, creiamo una finestra di supporto che ci farà anche da “PrintPreview”.

print_as_document_01

La PrintListWindow, contiene al suo interno non una DataGrid, ma un FlowDocumentReader che a sua volta contiene un FlowDocument. Dentro al Flow Document, andremo a creare la lista che vogliamo stampare.

<FlowDocumentReader>
    <FlowDocument
        Name="document"
        FontFamily="Arial"
        FontSize="12"
        IsOptimalParagraphEnabled="True"
        IsHyphenationEnabled="False"
        IsColumnWidthFlexible="False"
        ColumnWidth="640"
        PagePadding="Auto">           
   </FlowDocument>
</FlowDocumentReader>

Come potete vedere abbiamo specificato alcune caratteristiche di base del documento, quali la font ed alcune funzioni di base, quali la possibilità di portare il testo a capo e come si comportano le colonne del documento e quanto una colonna è ampia. Ovviamente sono solo alcune delle cose che FlowDocument mette a disposizione, probabilmente per fare un po’ di esempi su quello che ci mette a disposizione ci vorrebbe un articolo intero.

Ma vediamo invece il codice che c’è dietro alla finestra e in particolare al bottone Print di questa Window.

Il caricamento e i dati funzionali

public partial class PrintListWindow : Window, INotifyPropertyChanged

Per mostrare come non sia obbligatorio l’uso delle dependency property, in questa window abbiamo invece implementato INotifyPropertyChanged, l’interfaccia che ci permette di informare la User Interface quando modifichiamo il contenuto del suo Model.

public PrintListWindow()
{
    InitializeComponent();
    DataContext = this;
}

Anche in questo caso, la window è ViewModel di se stessa, visto che ha uno scopo predefinito.

public DataTable PrintingSource
{
    get
    
         return mPrintingSource;
    }
    set
    {
        mPrintingSource = value;
        OnPropertyChanged(FLD_PrintingSource);
    }
}

La property che ci permette di passare la sorgente dati dalla window principale a questa window di supporto.

internal void LoadParagraphs()
{
    BlockUIContainer buc = null;
    foreach (DataRow row in PrintingSource.Rows)
    {
        buc = new BlockUIContainer();
        StackPanel sp = new StackPanel();
        sp.Orientation = Orientation.Horizontal;
        buc.Child = sp;
        TextBlock txt = new TextBlock();
        txt.Text = row["CompanyName"].ToString();
        txt.Margin = new Thickness(4, 2, 4, 2);
        txt.Width = 260;
        sp.Children.Add(txt);
                
        txt = new TextBlock();
        txt.Text = row["Address"].ToString();
        txt.Margin = new Thickness(4, 2, 4, 2);
        txt.Width = 260;
sp.Children.Add(txt); txt = new TextBlock(); txt.Text = row["City"].ToString(); txt.Margin = new Thickness(4, 2, 4, 2); txt.Width = 120; sp.Children.Add(txt); document.Blocks.Add(buc); } }

LoadParagraphs è il metodo che carica i dati all’interno del Flow Document, per creare il nostro documento, effettuiamo un ciclo sul contenuto della tabella sorgente, ed utilizziamo un controllo BlockUIContainer, che è semplicemente un pannello che può essere inserito nella collezione Blocks del Flow Document per ospitare i contenuti di detto documento. All’interno di questo blocco, inseriamo uno stack panel con orientamento orizzontale e nello stack panel mettiamo un textblock per ognuna delle 3 colonne che vogliamo stampare. I textblock per creare le colonne hanno una larghezza predefinita. Aggiungiamo il BlockUIContainer alla collezione del nostro FlowDocument.

private void Print_Click(object sender, RoutedEventArgs e)
{
    PrintDialog prn = new PrintDialog();
    bool? shown = prn.ShowDialog();
    if (shown.HasValue && shown.Value
    {
        prn.PrintDocument(((IDocumentPaginatorSource)document).DocumentPaginator, "Print as a document");
    }
}

Il tasto Print, come vediamo è simile al precedente, solo che utilizza il metodo PrintDocument di PrintDialog  a cui fornisce il FlowDocument, con un cast all’opportuna interfaccia che FlowDocument fortunatamente Implementa, fornendo a PrintDocument un DocumentPaginator, ovvero una classe che implementa i metodi necessari a stampare un oggetto contenente più pagine.

Vediamo qual’è il risultato che usare questo oggetto ci fa ottenere.

print_as_document_02

Anche in questo caso scegliamo la stampante e poi dovremo dare un nome al file prodotto.

print_as_document_03

Come potete notare, quello che ci viene prodotto in questo caso, sono delle pagine che contengono esattamente quello che vediamo a video sulla nostra finestra, infatti, se osservate la prima pagina nello screenshot della PrintListWindow è identica. Cosa ci dice questo?

Che se cambiamo la dimensione della finestra di Preview, cambierà anche la nostra stampa.

print_as_document_04

Come vedete ho ingrandito la nostra finestra e rifacendo la stampa, ecco come appare il nuovo documento PDF.

print_as_document_05

Diciamo che questo metodo di stampa è un po’ più sofisticato della semplice stampa di un controllo, che ci da spazio di manovra, perché possiamo ad esempio cambiare la dimensione della finestra prima di stampare per creare una pagina simil A4, ma è ancora piuttosto grezzo.

C’è qualcosa che si può fare in più?

Print Using Paginator

Quello che possiamo fare in più per poter avere maggiore controllo sulla nostra stampa è creare un Paginator personalizzato, dove costruiremo la stampa come vogliamo noi. Vediamo come generarlo.

Abbiamo bisogno di due classi per il nostro paginator, la prima è un controllo visuale, che sarà il nostro “canvas” su cui disegneremo la pagina da stampare.

<UserControl x:Class="PrintData.CustomerListPage"
     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
     xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
     xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
     xmlns:local="clr-namespace:PrintData"
     mc:Ignorable="d"
     d:DesignHeight="300" d:DesignWidth="300">
    <Grid >
    </Grid>
    </UserControl>
public partial class CustomerListPage : UserControl
{
   private readonly DataRow[] customerItems;
   
    private readonly Size pageSize;
    public CustomerListPage (DataRow[] custItems, Size pageSize )
        {
            InitializeComponent();
            this.HorizontalAlignment = HorizontalAlignment.Stretch;
            this.VerticalAlignment = VerticalAlignment.Stretch;
            this.customerItems = custItems;
            this.pageSize = pageSize;
        }
    protected override void OnRender(DrawingContext drawingContext)
    {
        base.OnRender(drawingContext);
        Point point = new Point(0,0);
        foreach (DataRow item in customerItems)
        {
            point.X = 0;
            FormattedText text = Utilities.FormatText(item["CompanyName"].ToString());
            drawingContext.DrawText(text, point);
            point.X = (this.pageSize.Width) / 3;
            text = Utilities.FormatText(item["Address"].ToString());
            drawingContext.DrawText(text, point);
            point.X = ((this.pageSize.Width) / 3) * 2;
            text = Utilities.FormatText(item["City"].ToString());
            drawingContext.DrawText(text, point);
            point.Y += text.Height;
        }
    }
}

Qui sopra il codice per il nostro User control, utilizzeremo l’evento Render del controllo per disegnare il testo dei nostri dati sul controllo.  Ovviamente questo è un disegno piuttosto grezzo, essendo un esempio, probabilmente utilizzando qualcosa di più sofisticato si può creare qualcosa di meno brutale.

La classe Paginator

public class CustomersListPaginator : DocumentPaginator
{
    private readonly DataRow[] customerItems;
    private Size pageSize;
    private int pageCount;
    private int maxRowsPerPage;........

La classe che si occuperà di fornire i dati paginati al metodo di PrintDialog possiamo notare che deriva da DocumentPaginator, che è una classe Astratta, che ci obbligherà ad implementare una serie di metodi al suo interno.

Intanto creiamo le variabili che supporteranno i dati e il calcolo di quante righe possiamo stampare su ogni pagina. Importante è la pageSize, che è la dimensione che verrà passata per la grandezza della pagina che poi verrà utilizzata per costruire la stampa.

public CustomersListPaginator (    DataRow[] custItems, Size pageSize )
{
    this.customerItems = custItems;
    this.pageSize = pageSize;
    PaginateCustomerItems();
}

Il costruttore della classe, a cui passiamo un array contenente i nostri dati (in questo caso le righe della DataTable, e la dimensione della pagina di stampa.

private void PaginateCustomerItems()
{
    FormattedText text = Utilities.FormatText("Any Text");
    maxRowsPerPage = (int)((pageSize.Height) / text.Height) -2;
    
    pageCount = (int)Math.Ceiling((double)customerItems.Length / maxRowsPerPage);
}

Questo metodo viene utilizzato dalla classe per calcolare, in base alla dimensione del testo e al numero di elementi che possono essere contenuti dalla pagina quante righe stampare in ogni pagina.

private static DataRow[] GetRange(DataRow[] array, int start, int end)
{
    List<DataRow> cItems = new List<DataRow>();
    for (int i = start; i < end; i++)
    {
        if (i >= array.Count())
        {
            break;
        }
        cItems.Add(array[i]);
    }
    return cItems.ToArray();
}

Questo metodo restituisce gli elementi della lista dati in base alla pagina che stiamo per stampare.

public override DocumentPage GetPage(int pageNumber)
{
    // Compute the range of inventory items to display
    int start = pageNumber * maxRowsPerPage;
    int end = start + maxRowsPerPage;
    CustomerListPage page = new CustomerListPage(GetRange(customerItems, start, end), pageSize);
    page.Measure(pageSize);
    page.Arrange(new Rect(pageSize));
    return new DocumentPage(page);
}

Questo metodo costruisce la pagina e la restituisce al sistema di stampa. Per farlo istanzia e disegna un istanza del nostro user control.

public override bool IsPageCountValid
{
    get
    {
        return true;
    }
}

public override int PageCount
{
    get
    {
         return pageCount;
    }
}

public override System.Windows.Size PageSize
{
    get
    {
        return pageSize;
    }
    set
    {
         if (pageSize.Equals(value) != true)
         {
              pageSize = value;
              PaginateCustomerItems();
         }
    }
}

public override IDocumentPaginatorSource Source
{
    get
    {
        return null;
    }
}

Queste property possono essere utilizzate ove servisse per fornire ulteriori informazioni sul documento e sull’oggetto in stampa e possono essere utilizzate dal sistema di stampa. Nel nostro caso, salvo la dimensione della pagina non le utilizziamo.

Vediamo ora il risultato della nostra stampa.

Premendo il tasto Print Using Paginator e selezionando la stampante standard Microsoft PDF, e creando un nuovo file PDF otteniamo il seguente documento:

print_using_paginator_01

Come possiamo vedere le pagine generate hanno la dimensione del testo stampato, che sicuramente è buona cosa quando si stampa su una stampante, un po’ meno quando si crea un PDF come in questo caso, è chiaro che l’esempio è piuttosto rozzo, ma può darci delle idee relative a cosa fare e come farlo per creare dei report anche complessi utilizzando WPF.

Riepilogo

In questo post abbiamo visto:

  • Come stampare un controllo visuale da WPF
  • Come stampare dati paginati utilizzando un FlowDocument
  • Come creare un DocumentPaginator per poter stampare dati paginati in modo personalizzato.
  • Abbiamo anche visto come funziona il controllo PrintDialog di WPF e i suoi metodi PrintVisual e PrintDocument.

Potete scaricare il progetto esempio dal link qui indicato:

Per qualsiasi domanda, osservazione, commento, approfondimento, o per segnalare un errore, potete usare il link alla form di contatto in cima alla pagina.