Condividi tramite


Nuove frontiere per l'interfaccia utente

Nozioni fondamentali sulla stampa con Silverlight

Charles Petzold

Scarica il codice di esempio

Charles PetzoldIn Silverlight 4 è stata aggiunta la stampa all'elenco di funzionalità di Silverlight e vorrei illustrarla in dettaglio mostrando un piccolo programma che fa sorridere.

Il programma si chiama PrintEllipse la cui semplice funzione consiste nello stampare un'ellisse. Il file XAML dell'elemento MainPage contiene un pulsante e nella Figura 1 viene illustrato il file codebehind di MainPage completo.

Figura 1 Codice dell'elemento MainPage del programma PrintEllipse

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Printing;
using System.Windows.Shapes;

namespace PrintEllipse
{
  public partial class MainPage : UserControl
  {
    public MainPage()
    {
      InitializeComponent();
    }
    void OnButtonClick(object sender, RoutedEventArgs args)
    {
      PrintDocument printDoc = new PrintDocument();
      printDoc.PrintPage += OnPrintPage;
      printDoc.Print("Print Ellipse");
    }
    void OnPrintPage(object sender, PrintPageEventArgs args)
    {
      Ellipse ellipse = new Ellipse
      {
        Fill = new SolidColorBrush(Color.FromArgb(255, 255, 192, 192)),
        Stroke = new SolidColorBrush(Color.FromArgb(255, 192, 192, 255)),
        StrokeThickness = 24    // 1/4 inch
      };
      args.PageVisual = ellipse;
    }
  }
}

Si osservi la direttiva using per lo spazio dei nomi System.Windows.Printing. Facendo clic sul pulsante, viene creato un oggetto di tipo PrintDocument e viene assegnato un gestore per l'evento PrintPage. Quando viene chiamato il metodo Print, viene visualizzata la finestra di dialogo di stampa standard. L'utente può cogliere questa opportunità per impostare la stampante da utilizzare e impostare varie proprietà di stampa, quale la modalità verticale o orizzontale.

Quando l'utente fa clic su Print nella finestra di dialogo di stampa, il programma riceve una chiamata per il gestore eventi PrintPage. Questo particolare programma risponde creando un elemento Ellipse e impostandolo sulla proprietà PageVisual degli argomenti dell'evento. Ho intenzionalmente scelto colori pastello chiari per non utilizzare troppo inchiostro. In breve tempo, dalla stampante uscirà una pagina con un'ellisse gigantesca.

È possibile eseguire questo programma dal mio sito Web all'indirizzo bit.ly/dU9B7k e verificarlo in prima persona. Anche tutto il codice sorgente relativo a questo articolo è scaricabile, ovviamente.

Se la stampante in uso è simile alla maggior parte dei modelli in circolazione, l'hardware interno impedisce la stampa all'estremità del foglio. Le stampanti solitamente dispongono di un margine incorporato intrinseco in cui non è possibile stampare; la stampa è invece limitata a un'"area stampabile" inferiore alla dimensione completa della pagina.

Si osservi che con questo programma l'ellisse viene visualizzata completamente nell'area stampabile della pagina e ovviamente ciò avviene con uno sforzo minimo da parte del programma. L'area stampabile della pagina si comporta in maniera molto simile a un elemento contenitore della schermata: viene ritagliata solo una minima parte quando un elemento ha una dimensione che supera l'area. Alcuni ambienti grafici molto più sofisticati, quali Windows Presentation Foundation (WPF), non si comportano altrettanto bene (benché, ovviamente, WPF offra molto più controllo e flessibilità di stampa di Silverlight).

L'oggetto PrintDocument e gli eventi

Oltre all'evento PrintPage, nell'oggetto PrintDocument vengono definiti anche gli eventi BeginPrint e EndPrint, benché non abbiano la stessa importanza dell'evento PrintPage. L'evento BeginPrint segnala l'inizio di un processo di stampa. Viene generato quando l'utente esce dalla finestra di dialogo di stampa standard facendo clic sul pulsante Print e fornisce al programma l'opportunità di eseguire l'inizializzazione. La chiamata al gestore BeginPrint è seguita quindi dalla prima chiamata al gestore PrintPage.

In questo modo, è possibile stampare più di una pagina nell'ambito di un particolare processo di stampa. In ciascuna chiamata al gestore PrintPage, la proprietà HasMorePages della classe PrintPageEventArgs viene inizialmente impostata su False. Quando il gestore ha terminato con una pagina, può semplicemente impostare la proprietà su True per segnalare che è necessario stampare almeno un'altra pagina. Il gestore PrintPage viene quindi chiamato nuovamente. L'oggetto PrintDocument mantiene una proprietà PrintedPageCount che viene incrementata dopo ciascuna chiamata al gestore PrintPage.

Quando il gestore PrintPage viene chiuso con la proprietà HasMorePages impostata sul valore predefinito False, il processo di stampa è completato e viene generato l'evento EndPrint, che fornisce al programma l'opportunità di eseguire le attività di pulizia. L'evento EndPrint viene inoltre generato quando si verifica un errore durante la procedura di stampa; la proprietà Error della classe EndPrintEventArgs è di tipo Exception.

Coordinate di stampa

Nel codice riportato nella Figura 1 la proprietà StrokeThickness dell'oggetto Ellipse è impostata su 24 e, misurando il risultato della stampa, ci si renderà conto che avrà uno spessore pari a un quarto di pollice (0,64 cm). Come saprete, un programma Silverlight calcola le dimensioni degli oggetti e dei controlli grafici interamente in unità di pixel. Tuttavia, quando si tratta di una stampante, le coordinate e le dimensioni vengono calcolate in unità indipendenti dal dispositivo pari a 1/96° di pollice (0,03 cm). A prescindere dall'effettiva risoluzione della stampante, dal punto di vista di un programma Silverlight la stampante risulterà sempre essere un dispositivo con una risoluzione a 96 DPI.

Probabilmente già saprete che il sistema di coordinate basato su 96 unità di pollice viene utilizzato in WPF, in cui le unità vengono talvolta definite come "DIP o Device-Independent Pixel". Questo valore di 96 DPI non è stato scelto arbitrariamente; Per impostazione predefinita, Windows presuppone che lo schermo video abbia una risoluzione pari a 96 punti per pollice, quindi in molti casi un programma WPF consente di disegnare effettivamente in unità di pixel. La specifica CSS presuppone che gli schermi video abbiano una risoluzione pari a 96 DPI e tale valore viene utilizzato per effettuare la conversione tra pixel, pollici e millimetri. Il valore di 96 è inoltre un numero adatto per la conversione delle dimensioni dei caratteri, che normalmente vengono specificati in punti o 1/72 ° di pollice. Un punto corrisponde a tre quarti di un DIP (Device-Independent Pixel).

La classe PrintPageEventArgs dispone di due comode proprietà di sola ricezione che anche segnalano le dimensioni in unità di 1/96° di pollice: la proprietà PrintableArea di tipo Size fornisce le dimensioni dell'area stampabile della pagina, mentre la proprietà PageMargins di tipo Thickness fornisce la larghezza dei margini non stampabili di sinistra, di destra, superiori e inferiori. Sommando le due proprietà (nel modo corretto) si ottiene la dimensione completa del foglio.

La mia stampante, caricata con fogli standard con dimensione pari a 8,5 x 11 pollici (21,6 x 27,9 cm) impostati in modalità verticale, segnala un'area stampabile pari a 791 x 993. I quattro valori della proprietà PageMargins sono 12 (sinistra), 6 (in alto), 12 (destra) e 56 (in basso). Sommando i valori orizzontali di 791, 12 e 12, si otterrà 815. I valori verticali sono 994, 6 e 56, che sommati danno come risultato 1.055. Non sono sicuro del motivo per cui si ottiene una differenza di un'unità tra questi valori e i valori di 816 e 1.056 ottenuti dalla moltiplicazione della dimensione della pagina in pollici per 96.

Quando la stampante è impostata in modalità orizzontale, le dimensioni orizzontali e verticali segnalate dalla proprietà PrintableArea e PageMargins vengono invertite. Senza dubbio, l'osservazione della proprietà PrintableArea rappresenta l'unico modo in cui un programma Silverlight può stabilire se la stampante sia impostata in modalità verticale o orizzontale. Tutto ciò che viene stampato dal programma viene automaticamente allineato e ruotato in base alla modalità.

Spesso quando si stampa qualcosa in una situazione reale, si definiscono margini in qualche modo più larghi dei margini non stampabili. Come effettuare questa operazione in Silverlight? All'inizio, credevo che tutto si riducesse all'impostazione della proprietà Margin sull'elemento da stampare e che il margine sarebbe stato calcolato partendo da un margine totale desiderato (in unità di 1/96° di pollice) e sottraendo i valori della proprietà PageMargins disponibili nella classe PrintPageEventArgs. Tale approccio non ha funzionato come previsto, ma la soluzione corretta si rivelò altrettanto semplice. Il programma PrintEllipseWithMargins (che è possibile eseguire all'indirizzo bit.ly/fCBs3X) è uguale al primo programma fatta eccezione per il fatto che la proprietà Margin è impostata sull'oggetto Ellipse e che quest'ultimo è impostato come figlio di un elemento Border, che riempie l'area stampabile. In alternativa, è possibile impostare la proprietà Padding nell'elemento Border. Nella Figura 2 viene illustrato il il nuovo metodo OnPrintPage.

Figura 2 Il metodo OnPrintPage per calcolare i margini

void OnPrintPage(object sender, PrintPageEventArgs args)
{
  Thickness margin = new Thickness
  {
    Left = Math.Max(0, 96 - args.PageMargins.Left),
    Top = Math.Max(0, 96 - args.PageMargins.Top),
    Right = Math.Max(0, 96 - args.PageMargins.Right),
    Bottom = Math.Max(0, 96 - args.PageMargins.Bottom)
  };
  Ellipse ellipse = new Ellipse
  {
    Fill = new SolidColorBrush(Color.FromArgb(255, 255, 192, 192)),
    Stroke = new SolidColorBrush(Color.FromArgb(255, 192, 192, 255)),
    StrokeThickness = 24,   // 1/4 inch
    Margin = margin
  };
  Border border = new Border();
  border.Child = ellipse;
  args.PageVisual = border;
}

L'oggetto PageVisual

Non esistono metodi grafici né classi grafiche speciali associati alla stampante. Si "disegna" un oggetto nella pagina della stampante nello stesso modo in cui si "disegna" sullo schermo video, ovvero assemblando una struttura visiva di oggetti che derivano da FrameworkElement. Tale struttura può comprendere elementi Panel, tra cui Canvas. Per stampare la struttura visiva, impostare l'elemento superiore sulla proprietà PageVisual della classe PrintPageEventArgs. La proprietà PageVisual è definita UIElement, ovvero la classe principale di FrameworkElement; tuttavia, in pratica, tutto ciò che viene impostato sulla proprietà PageVisual deriverà da FrameworkElement.

Quasi ogni classe che deriva da FrameworkElement dispone di implementazioni non elementari dei metodi MeasureOverride e ArrangeOverride ai fini del layout. All'interno del metodo MeasureOverride, un elemento stabilisce la propria dimensione desiderata, talvolta determinando le dimensioni desiderate degli elementi figlio chiamando i relativi metodi Measure. All'interno del metodo ArrangeOverride, un elemento dispone i relativi elementi figlio chiamando i metodi Arrange degli elementi figlio.

Quando si imposta un elemento sulla proprietà PageVisual della classe PrintPageEventArgs, il sistema di stampa di Silverlight chiama il metodo Measure sull'elemento superiore con la dimensione della proprietà PrintableArea corrispondente. Questo è il modo in cui (ad esempio) l'elemento Ellipse o Border vengono automaticamente dimensionati in base all'area stampabile della pagina.

Tuttavia, è possibile inoltre impostare tale proprietà PageVisual su un elemento incluso già nella struttura visiva visualizzata nella finestra del programma. In tal caso, il sistema di stampa non chiama il metodo Measure sull'elemento, bensì utilizza le misurazioni e il layout stabiliti per lo schermo video. Ciò consente di stampare un oggetto dalla finestra del programma con un livello di fedeltà accettabile, ma ciò implica anche il fatto che ciò che viene stampato potrebbe essere ritagliato in base alla dimensione della pagina.

È possibile, ovviamente, impostare le proprietà esplicite Width e Height sugli elementi stampati e utilizzare la dimensione della proprietà PrintableArea come supporto.

Scalabilità e rotazione

Il successivo programma in cui mi sono cimentato si è rivelato una sfida più di quanto previsto. L'obiettivo era un programma che consentisse all'utente di stampare qualsiasi file di immagine supportato da Silverlight, ovvero file PNG e JPEG, archiviati nella macchina locale dell'utente. Il programma avrebbe utilizzato la classe OpenFileDialog per caricare i file. Per motivi di sicurezza, la classe OpenFileDialog restituisce solo un oggetto FileInfo che consente al programma di aprire il file. Non viene fornito alcun nome file o directory.

Il programma doveva consentire la stampa di un'immagine bitmap più larga possibile nella pagina (escluso un margine predefinito) senza alterare le proporzioni dell'immagine stessa. Normalmente questa è un'operazione semplicissima: La modalità Stretch predefinita dell'elemento Image è di tipo Uniform, il che significa che l'immagine bitmap viene adattata il più possibile senza distorsioni.

Tuttavia, decisi che non volevo che all'utente venisse richiesto di impostare la modalità verticale o orizzontale in modo specifico nella stampante in rapporto alla particolare immagine. Se la stampante era impostata in modalità verticale e l'immagine aveva una larghezza maggiore dell'altezza, volevo fare in modo che l'immagine venisse stampata lateralmente nella pagina verticale. Questa piccola funzionalità ha reso immediatamente il programma molto più complesso.

Se avessimo creato un programma WPF a tale scopo, il programma stesso avrebbe potuto cambiare la modalità della stampante da verticale a orizzontale. Tuttavia, ciò non è possibile in Silverlight. L'interfaccia della stampante è definita in modo tale che solo l'utente è in grado di modificare impostazioni simili.

Ancora una volta, se avessimo creato un programma WPF a tale scopo, in alternativa avrei potuto impostare una proprietà LayoutTransform sull'elemento Image per ruotarlo di 90 gradi. L'elemento Image ruotato verrebbe quindi ridimensionato per adattarsi alla pagina e l'immagine bitmap stessa sarebbe stata modificata per adattarsi all'elemento Image.

Tuttavia, Silverlight non supporta la proprietà LayoutTransform. Silverlight supporta solo la proprietà RenderTransform, quindi se l'elemento Image deve essere ruotato per creare spazio sufficiente per un'immagine orizzontale stampata in modalità verticale, anche l'elemento Image deve essere dimensionato manualmente in base alle dimensioni della pagina orizzontale.

È possibile provare il mio primo tentativo all'indirizzo bit.ly/eMHOsB. Il metodo OnPrintPage crea un elemento Image e imposta la proprietà Stretch su None, il che significa che l'elemento Image visualizza l'immagine bitmap con una dimensione in pixel, il che nella stampante significa che ciascun pixel viene considerato come 1/96° di pollice. Il programma quindi ruota, dimensiona e converte l'elemento Image mediante il calcolo di una trasformazione che si applica alla proprietà RenderTransform dell'elemento Image.

La parte difficile di tale codice è rappresentata, ovviamente, dai calcoli; quindi, è stato piacevole notare che il programma funzionava con immagini verticali e orizzontali con la stampante impostata in modalità verticale e orizzontale.

Tuttavia, non è stato altrettanto piacevole notare che il programma non riusciva a gestire immagini più grandi. È possibile effettuare tentativi in piena autonomia ciò che avviene con immagini che hanno dimensioni maggiori (divise per 96) della dimensione dell'immagine in pollici. L'immagine viene visualizzata nella dimensione corretta, ma non completamente.

Cosa accade? Beh, si tratta di un comportamento che ho già visto in precedenza sugli schermi video. Tenere presente che la proprietà RenderTransform riguarda solo il modo in cui l'elemento viene visualizzato e non il modo in cui appare al sistema di layout. Al sistema di layout, viene visualizzata un'immagine bitmap in un elemento Image con la proprietà Stretch impostata su None, il che significa che l'elemento Image ha una grandezza pari all'immagine bitmap stessa. Se l'immagine bitmap è più grande della pagina di stampa, parte dell'elemento Image non deve necessariamente essere sottoposto a rendering e, infatti, verrà ritagliato a prescindere dall'applicazione di una proprietà RenderTransform che riduce l'elemento Image in modo corretto.

Il mio secondo tentativo, che è possibile provare all'indirizzo bit.ly/g4HJ1C, assume una strategia in qualche modo diversa. Il metodo OnPrintPage viene illustrato nella Figura 3. All'elemento Image vengono assegnate impostazioni esplicite delle proprietà Width e Height che lo impostano esattamente sulla dimensione dell'area di visualizzazione calcolata. Poiché si trova tutto all'interno dell'area stampabile della pagina, nulla verrà ritagliato. La modalità Stretch è impostata su Fill, il che significa che l'immagine bitmap riempie l'elemento Image a prescindere dalle proporzioni. Se l'elemento Image non viene ruotato, una dimensione viene correttamente impostata e l'altra deve disporre di un fattore di proporzione che riduce la dimensione. Se anche l'elemento Image deve essere ruotato, i fattori di proporzione devono adattarsi alla diversa proporzione dell'elemento Image ruotato.

Figura 3 Stampa di un'immagine nell'oggetto PrintImage

void OnPrintPage(object sender, PrintPageEventArgs args)
{
  // Find the full size of the page
  Size pageSize = 
    new Size(args.PrintableArea.Width 
    + args.PageMargins.Left + args.PageMargins.Right,
    args.PrintableArea.Height 
    + args.PageMargins.Top + args.PageMargins.Bottom);

  // Get additional margins to bring the total to MARGIN (= 96)
  Thickness additionalMargin = new Thickness
  {
    Left = Math.Max(0, MARGIN - args.PageMargins.Left),
    Top = Math.Max(0, MARGIN - args.PageMargins.Top),
    Right = Math.Max(0, MARGIN - args.PageMargins.Right),
    Bottom = Math.Max(0, MARGIN - args.PageMargins.Bottom)
  };

  // Find the area for display purposes
  Size displayArea = 
    new Size(args.PrintableArea.Width 
    - additionalMargin.Left - additionalMargin.Right,
    args.PrintableArea.Height 
    - additionalMargin.Top - additionalMargin.Bottom);

  bool pageIsLandscape = displayArea.Width > displayArea.Height;
  bool imageIsLandscape = bitmap.PixelWidth > bitmap.PixelHeight;

  double displayAspectRatio = displayArea.Width / displayArea.Height;
  double imageAspectRatio = (double)bitmap.PixelWidth / bitmap.PixelHeight;

  double scaleX = Math.Min(1, imageAspectRatio / displayAspectRatio);
  double scaleY = Math.Min(1, displayAspectRatio / imageAspectRatio);

  // Calculate the transform matrix
  MatrixTransform transform = new MatrixTransform();

  if (pageIsLandscape == imageIsLandscape)
  {
    // Pure scaling
    transform.Matrix = new Matrix(scaleX, 0, 0, scaleY, 0, 0);
  }
  else
  {
    // Scaling with rotation
    scaleX *= pageIsLandscape ? displayAspectRatio : 1 / 
      displayAspectRatio;
    scaleY *= pageIsLandscape ? displayAspectRatio : 1 / 
      displayAspectRatio;
    transform.Matrix = new Matrix(0, scaleX, -scaleY, 0, 0, 0);
  }

  Image image = new Image
  {
    Source = bitmap,
    Stretch = Stretch.Fill,
    Width = displayArea.Width,
    Height = displayArea.Height,
    RenderTransform = transform,
    RenderTransformOrigin = new Point(0.5, 0.5),
    HorizontalAlignment = HorizontalAlignment.Center,
    VerticalAlignment = VerticalAlignment.Center,
    Margin = additionalMargin,
  };

  Border border = new Border
  {
    Child = image,
  };

  args.PageVisual = border;
}

Il codice è certamente disordinato e ho il sospetto che esistano delle semplificazioni non immediatamente ovvie, ma funziona per le immagini bitmap di tutte le dimensioni.

Un altro approccio consiste nel ruotare l'immagine bitmap stessa piuttosto che l'elemento Image. È necessario creare un elemento WriteableBitmap basata sull'oggetto BitmapImage caricato e un secondo elemento WritableBitmap con dimensioni orizzontali e verticali invertite, quindi copiare tutti i pixel dal primo elemento WriteableBitmap nel secondo con righe e colonne invertite.

Più pagine di calendario

In Silverlight esiste una tecnica derivata dalla classe UserControl molto comune per creare un controllo riutilizzabile senza troppi problemi. Gran parte della classe UserControl si riduce a una struttura visiva definita in linguaggio XAML.

È possibile sfruttare la classe UserControl anche per definire una struttura visiva da stampare. Questa tecnica è illustrata nel programma PrintCalendare, che è possibile provare all'indirizzo bit.ly/dIwSsn. Si indica un mese di inizio e un mese di fine e il programma stampa tutti i mesi compresi nell'intervallo, un mese per pagina. Le pagine possono essere quindi affisse alla parete proprio come un vero calendario.

Dopo la mia esperienza con il programma PrintImage, non volevo preoccuparmi dei margini e dell'orientamento; volevo invece includere un pulsante per trasferire la responsabilità all'utente, come mostrato nella Figura 4.

The PrintCalendar Button

Figura 4 Il pulsante PrintCalendar

La classe UserControl che definisce la pagina del calendario è denominata CalendarPage e il file XAML viene mostrato nella Figura 5. In un elemento TextBlock posto in alto vengono visualizzati il mese e l'anno, seguiti da una seconda griglia con sette colonne per i giorni della settimana e sei righe per un massimo di sei settimane o settimane parziali in un mese.

Figura 5 Il layout della classe CalendarPage

<UserControl x:Class="PrintCalendar.CalendarPage"
  xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
  FontSize="36">  
  <Grid x:Name="LayoutRoot" Background="White">
    <Grid.RowDefinitions>
      <RowDefinition Height="Auto" />
      <RowDefinition Height="*" />
    </Grid.RowDefinitions>
    <TextBlock Name="monthYearText" 
      Grid.Row="0"
       FontSize="48"
       HorizontalAlignment="Center" />
    <Grid Name="dayGrid" 
      Grid.Row="1">
      <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*" />
        <ColumnDefinition Width="*" />
        <ColumnDefinition Width="*" />
        <ColumnDefinition Width="*" />
        <ColumnDefinition Width="*" />
        <ColumnDefinition Width="*" />
        <ColumnDefinition Width="*" />
      </Grid.ColumnDefinitions>
      <Grid.RowDefinitions>
        <RowDefinition Height="*" />
        <RowDefinition Height="*" />
        <RowDefinition Height="*" />
        <RowDefinition Height="*" />
        <RowDefinition Height="*" />
        <RowDefinition Height="*" />
      </Grid.RowDefinitions>
    </Grid>
  </Grid>
</UserControl>

A differenza dei derivati dalla classe UserControl, la classe CalendarPage definisce un costruttore con un parametro come mostrato nella Figura 6.

Figura 6 Il costruttore codebehind della classe CalendarPage

public CalendarPage(DateTime date)
{
  InitializeComponent();
  monthYearText.Text = date.ToString("MMMM yyyy");
  int row = 0;
  int col = (int)new DateTime(date.Year, date.Month, 1).DayOfWeek;
  for (int day = 0; day < DateTime.DaysInMonth(date.Year, date.Month); day++)
  {
    TextBlock txtblk = new TextBlock
    {
      Text = (day + 1).ToString(),
      HorizontalAlignment = HorizontalAlignment.Left,
      VerticalAlignment = VerticalAlignment.Top
    };
    Border border = new Border
    {
      BorderBrush = blackBrush,
      BorderThickness = new Thickness(2),
      Child = txtblk
    };
    Grid.SetRow(border, row);
    Grid.SetColumn(border, col);
    dayGrid.Children.Add(border);
    if (++col == 7)
    {
      col = 0;
      row++;
    }
  }
  if (col == 0)
    row--;
  if (row < 5)
    dayGrid.RowDefinitions.RemoveAt(0);
  if (row < 4)
    dayGrid.RowDefinitions.RemoveAt(0);
}

Il parametro è una classe DateTime e il costruttore utilizza le proprietà Month e Year per creare un elemento Border contenente un TextBlock per ciascun giorno del mese. A entrambi vengono assegnate le proprietà collegate Grid.Row e Grid.Column e aggiunti alla griglia. Come saprete, spesso i mesi si estendono solo su cinque settimane e occasionalmente febbraio ha solo quattro settimane, quindi gli oggetti RowDefinition vengono in realtà rimossi dalla griglia se non necessari.

I derivati della classe UserControl normalmente non dispongono di costruttori con parametri poiché solitamente formano parti strutture visive più grandi. Tuttavia, la classe CalendarPage non viene utilizzata in questo modo. Al contrario, il gestore PrintPage assegna semplicemente una nuova istanza della classe CalendarPage alla proprietà PageVisual della classe PrintPageEventArgs. Di seguito è riportato il corpo completo del gestore, in cui viene illustrato chiaramente la quantità di lavoro effettuato dalla classe CalendarPage:

args.PageVisual = new CalendarPage(dateTime);
args.HasMorePages = dateTime < dateTimeEnd;
dateTime = dateTime.AddMonths(1);

L'aggiunta di un'opzione di stampa a un programma viene spesso considerato un'impresa ardua che implica la scrittura di molto codice. La capacità di definire la maggior parte di un'immagine stampata in un file XAML rende l'operazione meno terribile.        

Charles Petzold collabora da molto tempo con la rivista MSDN Magazine. Il suo nuovo libro intitolato "Programming Windows Phone 7" (Microsoft Press, 2010) è disponibile gratuitamente per il download all'indirizzo bit.ly/cpebookpdf.

Un ringraziamento ai seguenti esperti tecnici per la revisione dell'articolo: Saied Khanahmadi e Robert Lyon