Press "Enter" to skip to content

WPF Beginners – Costruire una stampa strutturata in WPF

Un articolo in risposta ad un problema posto sul forum Microsoft di C#, in
questo Thread, ovvero Come stampare le cartelle della tombola in WPF. Ho creato
un paio di classi per costruire le mie cartelle ed un metodo per riempirle di
numeri casuali da 01 a 90, anche se ovviamente un algoritmo perfetto dovrebbe
essere più sofisticato e preciso. Però al momento mi interessava la stampa, non
il calcolo dei numeri delle cartelle.

Detto questo, vediamo come stampare le cartelle della tombola usando i
componenti di stampa di WPF.

MainWindow.xaml & MainWindow.xaml.cs

Questa finestra di supporto, che mi serviva esclusivamente per testare le
classi prima del test di stampa contiene al suo interno 3 componenti:

  • Una TextBox – per generare e visualizzare i dati delle cartelle prima di
    stamparle.
  • Un Button – per generare le cartelle con numeri casuali.
  • Un Button – per chiamare il metodo di stampa.
<Grid>
 
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <TextBox
        Grid.Row="0"
        HorizontalAlignment="Stretch"
        VerticalAlignment="Stretch"
        HorizontalContentAlignment="Left"
        AcceptsReturn="True"
        Text="{Binding ResultText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
        VerticalScrollBarVisibility="Auto" />
        <StackPanel Grid.Row="1">
            <Button Click="Create_Click">
                Create Test
            </Button>
            <Button Click="Print_Click">
                Print Test
            </Button>
        </StackPanel>
    </Grid>

Vediamo ora il codice collegato ai click dei Button.

private void Create_Click(object sender, RoutedEventArgs e)
{
    Cartelle.Clear();
    Cartelle.AddRange(Cartella.GenerateRandom(26));
    ResultText = "";
    foreach (Cartella item in Cartelle)
    {
        ResultText += item.ToString() + Environment.NewLine;
 
    }
}

Il primo evento, genera una serie di cartelle memorizzandole in una Property
Generata allo scopo che ho chiamato Cartelle.

private void Print_Click(object sender, RoutedEventArgs e)
{
    PrintWindow pw = new PrintWindow();
    pw.Owner = this;
    pw.Title = "Test Di stampa";
 
    pw.Show();
    pw.LoadData(Cartelle);
}

Il Secondo evento, genera la finestra di stampa, passandogli le Cartelle da
stampare.

PrintWindow.xaml & PrintWindow.xaml.cs

La finestra di stampa, contiene 2 componenti primari, un FlowDocumentReader,
che è il componente che conterrà il documento da stampare dentro a cui
costruiremo la nostra stampa.

Contiene inoltre un Button nel cui evento Click chiameremo la stampa vera e
propria.

Nel FlowDocumentReader, abbiamo sistemato un FlowDocument, il componente che
ci fornisce le pagine vuote dove costruire la stampa.

Il report sarà costruito dinamicamente via codice, visto che non sappiamo a
priori quante cartelle vogliamo stampare, lo stesso vale per qualsiasi report
composto da un numero di record dinamico.

Vediamo il codice Xaml che è molto semplice:

<Grid>
 
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
 
        <FlowDocumentReader Grid.Row="0" ViewingMode="Scroll">
            <FlowDocument
            Name="document"
            ColumnWidth="800"
            FontFamily="Arial"
            FontSize="12"
            IsColumnWidthFlexible="False"
            IsHyphenationEnabled="False"
            IsOptimalParagraphEnabled="True"
            PagePadding="Auto" />
        </FlowDocumentReader>
        <StackPanel
        Grid.Row="1"
        FlowDirection="RightToLeft"
        Orientation="Horizontal">
            <Button
            Margin="4"
            Padding="10,2,10,2"
            Click="Print_Click">
                Print
            </Button>
        </StackPanel>
    </Grid>

Come potete osservare dai dati indicati, abbiamo dato solo dei parametri di
base alla dimensione della pagina del documento e al suo aspetto, in realtà
formatteremo poi i componenti che comporranno i vari elementi del report.

Vediamo ora il codice che compone le nostre cartelle inserendole nel
FlowDocument:

public void LoadData(List<Cartella> cartelle)
{
    BlockUIContainer buc = null;
    foreach (Cartella car in cartelle)
    {
        buc = new BlockUIContainer();
        document.Blocks.Add(buc);
    }
}

Procediamo per porzioni di codice, questo è il ciclo principale, che
inserisce nel FlowDocument un BlockUIContainer per ognuna delle cartelle. Il
BlockUIContainer può essere comparato ad un Paragrafo, ed è un oggetto che non
può essere stampato su più pagine, pertanto è l’ideale per creare le cartelle
perché il FlowDocument stamperà un intera cartella su una pagina, spostandola
nella pagina successiva se non c’è sufficiente spazio.

Border b = new Border();
b.BorderThickness = new Thickness(1, 1, 1, 1);
b.BorderBrush = Brushes.Navy;
buc.Child = b;

Per delineare la nostra cartella, all’interno del BlockUIContainer abbiamo
inserito un oggetto Border, in WPF il Border non è un attributo dei controlli,
ma un controllo contenitore  dentro a cui inserire gli oggetti a cui
vogliamo dare il bordo.
La property BorderThickness, stabilisce la dimensione del Bordo, usando 4 numeri
(double) che definiscono lo spessore rispettivamente Sinistro, Superiore,
Destro, Inferiore (Left, Top, Right, Bottom).
La property BorderBrush, stabilisce il colore del bordo, altre property
permettono di stabilire la forma del bordo, se gli angoli sono arrotondati e
molto altro. Potrete divertirvi a provare tutte le sue capacità. Come potete
vedere, abbiamo posto il Border nella proprietà Child del BlockUIContainer, che
come molti controlli (Border compreso) può contenere un solo Componente al suo
interno.

StackPanel csp = new StackPanel();
b.Child = csp;
foreach (Riga row in car.Righe)
{
    StackPanel sp = new StackPanel();
    sp.Orientation = Orientation.Horizontal;
    csp.Children.Add(sp);
    foreach (int val in row.Caselle)
    {
        Border cb = new Border();
	sp.Children.Add(cb);
    }
}

Andando all’interno dei cicli più interni, abbiamo generato uno StackPanel,
dove metteremo le Righe delle nostre cartelle, lo StackPanel viene inserito nel
border creato in precedenza. Aggiungiamo un altro StackPanel dentro al primo,
per inserirvi le caselle con i numeri. Lo stack panel delle righe ha
l’Orientation di default che è Verticale, quello che conterrà le caselle con i
numeri ha invece Orientation Orizzontale. Anche nelle caselle delle righe,
inseriamo un border per creare un rettangolo attorno al numero.

Border cb = new Border();
cb.BorderThickness = new Thickness(0.2, 0.2, 0.2, 0.2);
cb.BorderBrush = Brushes.Orange;
cb.Margin = new Thickness(2);
cb.Width = 75;
cb.Height = 75;
cb.VerticalAlignment = VerticalAlignment.Stretch;
cb.HorizontalAlignment = HorizontalAlignment.Stretch;
TextBlock txt = new TextBlock();
txt.Text = val == 0 ? "" : val.ToString("##");
txt.Margin = new Thickness(4, 2, 4, 2);
txt.Margin = new Thickness(0);
txt.Padding = new Thickness(0);
txt.VerticalAlignment = VerticalAlignment.Center;
txt.HorizontalAlignment = HorizontalAlignment.Center;
txt.FontFamily = new FontFamily("Arial Rounded MT");
txt.FontSize = 48;
txt.FontWeight = FontWeights.Bold;
cb.Child = txt;
sp.Children.Add(cb);

Ed ora il dettaglio più interno, ovvero la costruzione del Border che
contiene e da forma alla casella ed il TextBlock che contiene il numero.

Usiamo il Border per disegnare un quadrato, di 75×75 unità standard WPF, poi
generiamo una TextBlock in cui memorizziamo il numero (oppure nulla se il valore
è 0). Abbiamo definito il tipo di font, la dimensione e la grossezza del numero
e lo inseriamo nella property Child del Border.

Terminato questo Metodo, il risultato della pressione del tasto di stampa
sulla MainWindow, provoca il seguente risultato:

Vediamo ora come procedere alla stampa osservando il codice dietro al click
del tasto Print.

        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");
            }
        }

Quattro semplici righe di codice, con cui creiamo un oggetto PrintDialog, che
ci permette di selezionare la stampante su cui stampare, seguito da una chiamata
al metodo PrintDocument a cui passiamo il nostro oggetto FlowDOcument, usando il
“DocumentPaginator” che si occupa in modo automatico di creare la corretta
paginazione del documento. Vediamo il risultato della chiamata:

Ci viene mostrata la finestra per la selezione della stampante.

Io ho selezionato Foxit Pdf Reader, per generare un PDF e questo è il
risultato della stampa del documento:

Sono state generate una serie di pagine, ciascuna delle quali contiene 4
cartelle. Essendo il mio algoritmo di generazione dei numeri primitivo, non sono
organizzati come ci si aspetterebbe ma lascio a chi volesse generare un sistema
per il gioco della tombola la creazione di qualcosa di più sofisticato.

Mi fermo qui per questa sera, il codice a corredo può essere scaricato al link seguente:

Nel codice completo trovate ovviamente il codice commentato completo e trovate anche le 2 classi che ho usato per generare le cartelle e l’algoritmo che genera I numeri casuali delle cartelle che non ho descritto perché sono off topic visto che questo articolo si concentra sui componenti per le stampe.

Se avete domande, osservazioni, commenti, potete scrivermi utilizzando l’opzione di menu con la bustina.