Neue Benutzeroberflächentechnologien

Grundlagen des Druckens mit Silverlight

Charles Petzold

Beispielcode herunterladen

Charles Petzold Seit Silverlight 4 gibt es ein neues Feature in Silverlight – die Druckfunktion. Und ich möchte als Erstes auf ein kleines Programm aufmerksam machen, das ein Lächeln auf mein Gesicht zaubert.

 Es heißt PrintEllipse, und der Name sagt alles. Die XAML-Datei für MainPage enthält eine Schaltfläche, und Abbildung 1 zeigt die gesamte MainPage-Codebehind-Datei an.

 Abbildung 1 Der MainPage-Code für 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;
    }
  }
}

Beachten Sie die Using-Direktive für System.Windows.Printing. Wenn Sie auf die Schaltfläche klicken, erstellt das Programm ein Objekt vom Typ „PrintDocument“ und weist einen Handler für das PrintPage-Ereignis zu. Wenn das Programm die Print-Methode aufruft, wird das Standarddialogfeld zum Drucken angezeigt. Der Benutzer kann dann festlegen, welcher Drucker verwendet werden soll, und verschiedene Druckeigenschaften angeben, z. B. Hoch- oder Querformat.

Wenn der Benutzer im Dialogfeld „Drucken“ auf „Drucken“ klickt, empfängt das Programm einen Aufruf für den PrintPage-Ereignishandler. Dieses Programm reagiert, indem es ein Ellipse-Element erstellt und für dieses Element die PageVisual-Eigenschaft der Ereignisargumente festlegt. (Ich habe mich absichtlich für Pastellfarben entschieden, damit Sie nicht zu viel Tinte verbrauchen.) Und schon gibt Ihr Drucker eine Seite mit einer riesigen Ellipse aus.

Überzeugen Sie sich selbst, indem Sie dieses Programm von meiner Website unter bit.ly/dU9B7k ausführen. Der Quellcode für diesen Artikel kann natürlich auch in seiner Gesamtheit heruntergeladen werden.

Sofern Ihr Drucker keine Ausnahme ist, verhindert die interne Hardware, dass bis an den Seitenrand gedruckt werden kann. Drucker haben in der Regel einen systeminternen integrierten Rand, in dem nichts gedruckt wird. Bedruckt wird lediglich der sogenannte Druckbereich, der nicht die gesamte Seitengröße umspannt.

Bei diesem Programm hingegen wird die Ellipse vollständig im Druckbereich der Seite angezeigt. Das ist natürlich kein besonders komplizierter Vorgang für das Programm. Der Druckbereich einer Seite ist mit einem Containerelement auf dem Bildschirm vergleichbar: Ein untergeordnetes Objekt wird nur zugeschnitten, wenn ein Element größer als dieser Bereich ist. Einige der weitaus komplexeren Grafikumgebungen, wie z. B. Windows Presentation Foundation (WPF), verhalten sich weniger gefällig (wobei WPF natürlich eine wesentlich bessere Drucksteuerung und mehr Flexibilität bietet als Silverlight).

PrintDocument und Ereignisse

Abgesehen vom PrintPage-Ereignis definiert PrintDocument auch die Ereignisse „BeginPrint“ und „EndPrint“, die jedoch lange nicht so wichtig sind wie „PrintPage“. Das BeginPrint-Ereignis gibt den Anfang eines Druckauftrags an. Es wird ausgelöst, wenn der Benutzer das Standarddialogfeld zum Drucken durch Klicken auf die Schaltfläche „Drucken“ beendet, und es lässt das Programm die Initialisierung ausführen. Auf den Aufruf für den BeginPrint-Handler folgt dann der erste Aufruf für den PrintPage-Handler.

Es ist jedem Programm freigestellt, mehrere Seiten pro Druckauftrag zu drucken. In jedem Aufruf des PrintPage-Handlers ist für die HasMorePages-Eigenschaft von PrintPageEventArgs zunächst der Wert „False“ festgelegt. Wenn der Handler mit der Seite fertig ist, kann er die Eigenschaft einfach in „True“ ändern, um anzugeben, dass mindestens eine weitere Seite zu drucken ist. PrintPage wird dann erneut aufgerufen. Das PrintDocument-Objekt verwaltet eine PrintedPageCount-Eigenschaft, die nach jedem Aufruf des PrintPage-Handlers schrittweise erhöht wird.

Wenn der PrintPage-Handler beendet wird, während HasMorePages auf den Standardwert „False“ eingestellt ist, ist der Druckauftrag damit abgeschlossen, und das EndPrint-Ereignis wird ausgelöst. Damit erhält das Programm die Möglichkeit, Bereinigungsaufgaben auszuführen. Das EndPrint-Ereignis wird auch ausgelöst, wenn während des Druckvorgangs ein Fehler auftritt. Die Error-Eigenschaft von EndPrintEventArgs ist vom Typ „Exception“.

Druckerkoordinaten

Mit dem Code in Abbildung 1 wird die StrokeThickness-Eigenschaft der Ellipse auf 24 festgelegt, und wenn Sie das Druckergebnis messen, werden Sie feststellen, dass die Strichstärke einen guten halben Zentimeter (einen Viertel Zoll) beträgt. Wie Sie wissen, werden die Größen von grafischen Objekten und Steuerelementen in Silverlight-Programmen normalerweise ausschließlich in Pixel angegeben. Wenn es um Drucker geht, werden zur Angabe von Koordinaten und Größen geräteunabhängige Einheiten im 1/96-Zoll-Maß verwendet. Unabhängig von der Druckerauflösung handelt es sich aus Sicht eines Silverlight-Programms bei einem Drucker immer um ein 96-DPI-Gerät.

Ihnen ist vielleicht bekannt, dass dieses Koordinatensystem mit 96 Einheiten pro Zoll in WPF verwendet wird, wobei die Einheiten mitunter auch als „geräteunabhängige Pixel“ bezeichnet werden. 96 DPI ist kein willkürlich ausgewählter Wert: Standardmäßig nimmt Windows an, dass die Videoanzeige eine Auflösung von 96 Punkten pro Zoll (DPI, Dots Per Inch) besitzt, d. h., dass WPF-Programme oftmals in Pixel zeichnen. Bei der CSS-Spezifikation wird davon ausgegangen, dass jede Videoanzeige eine Auflösung von 96 DPI hat. Dieser Wert wird dann für die Konvertierung von Pixel, Zoll und Millimeter verwendet. Die Zahl 96 wird außerdem gerne zum Konvertieren von Schriftgrößen verwendet, die normalerweise in Punkten (1/72-Zoll-Maß) angegeben werden. Ein Punkt entspricht drei Vierteln eines geräteunabhängigen Pixels.

PrintPageEventArgs besitzt zwei nützliche Abrufeigenschaften, mit denen ebenfalls Größen im 1/96-Zoll-Maß angegeben werden: PrintableArea vom Typ „Size“ gibt die Abmessungen des Druckbereichs einer Seite an, und PageMargins vom Typ „Thickness“ entspricht der Breite der nicht bedruckbaren Ränder auf der linken und rechten Seite sowie oben und unten. Wenn Sie die beiden Angaben entsprechend addieren, erhalten Sie die Gesamtgröße des Papiers.

Mein Drucker gibt, wenn Papier im Letter-Format (8,5 x 11 Zoll bzw. 216 × 279 mm) geladen und das Hochformat festgelegt ist, für PrintableArea die Werte 791 x 993 an. Die vier Werte für die PageMargins-Eigenschaft lauten 12 (links), 6 (oben), 12 (rechts) und 56 (unten). Wenn man die horizontalen Werte 791, 12 und 12 addiert, erhält man 815. Die vertikalen Werte lauten 994, 6 und 56, also eine Summe von 1055. Allerdings weiß ich nicht genau, woher die Differenz von jeweils einer Einheit kommt, die sich zwischen diesen Werten und den Beträgen 816 bzw. 1056 ergibt, die man durch Multiplizieren der Seitengröße in Zoll mal 96 erhält.

Wenn für einen Drucker das Querformat festgelegt ist, sind die horizontalen und vertikalen Abmessungen von PrintableArea und PageMargins vertauscht. Tatsächlich ist die PrintableArea-Eigenschaft die einzige Möglichkeit, wie ein Silverlight-Programm ermitteln kann, ob für den Drucker das Quer- oder das Hochformat festgelegt wurde. Was immer von diesem Programm gedruckt wird, wird dem Format entsprechend automatisch ausgerichtet und rotiert.

Beim Drucken im Alltag definiert man oft Ränder, die über den nicht bedruckbaren Rand hinausgehen. Und wie macht man das in Silverlight? Ich dachte zunächst, dass man einfach nur die Margin-Eigenschaft für das auszudruckende Element festlegen muss. Diese Eigenschaft würde berechnet, indem man vom gewünschten gesamten Rand (in 1/96-Zoll-Einheiten) die Werte der PageMargins-Eigenschaft (verfügbar unter PrintPageEventArgs) subtrahiert. Dieser Ansatz hat nicht funktioniert, aber die richtige Lösung war letztendlich auch nicht viel komplizierter. Das Programm PrintEllipseWithMargins (das Sie unter bit.ly/fCBs3X ausführen können) gleicht dem ersten Programm, nur dass für die Ellipse eine Margin-Eigenschaft festgelegt ist. Die Ellipse wird dann als untergeordnetes Element einer Border-Eigenschaft festgelegt, damit der Druckbereich ausgefüllt wird. Alternativ können Sie die Padding-Eigenschaft für Border festlegen. In Abbildung 2 wird die neue OnPrintPage-Methode dargestellt.

Abbildung 2 Die OnPrintPage-Methode zum Berechnen von Rändern

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

Das PageVisual-Objekt

Druckern sind keine besonderen Grafikmethoden oder -klassen zugeordnet. Man „zeichnet“ etwas auf der für den Drucker bestimmten Seite genau wie man etwas in der Videoanzeige zeichnet, nämlich durch Erstellen einer visuellen Struktur mit Objekten, die von FrameworkElement abgeleitet sind. Diese Struktur kann Panel-Elemente enthalten, einschließlich Canvas. Um diese visuelle Struktur zu drucken, legen Sie für das oberste Element die PageVisual-Eigenschaft von PrintPageEventArgs fest. (PageVisual ist als eine UIElement-Klasse definiert; dabei handelt es sich um die übergeordnete Klasse von FrameworkElement. Aber in der Praxis werden alle auf PageVisual festgelegten Elemente von FrameworkElement abgeleitet.)

Fast jede Klasse, die von FrameworkElement abgeleitet ist, verfügt zu Layoutzwecken über nicht triviale Implementierungen der MeasureOverride- und ArrangeOverride-Methoden. Mit der MeasureOverride-Methode wird die gewünschte Größe für ein Element festgelegt. Dazu werden mitunter die Measure-Methoden der untergeordneten Elemente aufgerufen, um so die gewünschten Größen für diese Elemente zu bestimmen. Mit der ArrangeOverride-Methode werden die untergeordneten Elemente in Bezug auf das übergeordnete Element angeordnet, indem die Arrange-Methoden für die untergeordneten Elemente aufgerufen werden.

Wenn Sie die PageVisual-Eigenschaft von PrintPageEventArgs für ein Element festlegen, ruft das Silverlight-Drucksystem die Measure-Methode für das oberste Element mit der Größe „PrintableArea“ auf. So wird z. B. die Größe einer Ellipse oder eines Rahmens automatisch auf den Druckbereich der Seite festgelegt.

Sie können die PageVisual-Eigenschaft aber auch für ein Element festlegen, das bereits zu einer visuellen Struktur gehört, die im Programmfenster angezeigt wird. In diesem Fall ruft das Drucksystem nicht die Measure-Methode für dieses Element auf, sondern verwendet das Layout und die Maße, die bereits für die Videoanzeige bestimmt wurden. So können Sie relativ zuversichtlich vom Programmfenster aus drucken, allerdings werden die gedruckten Elemente möglicherweise auf die Seitengröße zugeschnitten.

Sie können natürlich ausdrücklich die Eigenschaften „Width“ und „Height“ für Druckelemente festlegen, und Sie können auch die Size-Eigenschaft „PrintableArea“ verwenden.

Skalieren und Rotieren

Das nächste Programm, mit dem ich mich beschäftigt habe, erwies sich als eine größere Herausforderung als ich anfangs gedacht hatte. Ich habe ein Programm gesucht, mit dem ein Benutzer jede in Silverlight unterstützte Bilddatei (also PNG- und JPEG-Dateien) drucken kann, die auf seinem lokalen Computer gespeichert ist. Das entsprechende Programm lädt diese Dateien mithilfe der OpenFileDialog-Klasse. Aus Sicherheitsgründen gibt OpenFileDialog nur ein FileInfo-Objekt zurück, mit dem das Programm die Datei öffnen kann. Es wird weder ein Dateiname noch ein Verzeichnis angegeben.

Ich wollte, dass dieses Programm die Bitmap so groß wie möglich auf der Seite druckt, ohne den vordefinierten Seitenrand zu bedrucken und ohne das Seitenverhältnis für die Bitmap ändern zu müssen. Normalerweise ist das kein Problem: Der Standardwert für den Stretch-Modus des Image-Elements lautet „Uniform“. Dies bedeutet, dass die Bitmap so weit wie möglich ohne Verzerrung gedehnt wird.

Allerdings wollte ich vermeiden, dass der Benutzer für dieses Bild und den entsprechenden Drucker das Hoch- oder Querformat manuell festlegen muss. Wenn für den Drucker das Hochformat festgelegt wurde und das Bild breiter als hoch ist, soll das Bild seitwärts auf die Seite im Hochformat gedruckt werden. Diese kleine Feature hat das Programm sofort wesentlich komplizierter gemacht.

Wenn ich ein WPF-Programm für diese Aufgabe schreiben würde, könnte ich das Programm anweisen, für den Drucker je nach Bedarf das Hoch- bzw. Querformat festzulegen. In Silverlight geht das leider nicht. Die Druckerschnittstelle ist so definiert, dass nur der Benutzer derartige Einstellungen ändern kann.

Eine andere Möglichkeit, wenn ich ein WPF-Programm schreiben würde, wäre, mit einer LayoutTransform-Eigenschaft für das Image-Element festzulegen, dass das Bild um 90 Grad rotiert würde. Dann würde die Größe des rotierten Image-Elements an die Seitengröße und die Bitmap an die Größe des Image-Elements angepasst.

Aber Silverlight bietet keine Unterstützung für LayoutTransform. Silverlight unterstützt lediglich RenderTransform. Wenn also das Image-Element rotiert werden muss, damit ein Querformatbild im Hochformat gedruckt werden kann, muss die Größe des Image-Elements auch manuell an die Abmessungen der Querformatseite angepasst werden.

Sie können meinen ersten Versuch unter bit.ly/eMHOsB ausprobieren. Die OnPrintPage-Methode erstellt ein Image-Element und legt die Stretch-Eigenschaft auf „None“ fest. Das Image-Element zeigt also die Bitmap in ihrer Pixelgröße an, wobei beim Drucker davon ausgegangen wird, dass ein Pixel 1/96 Zoll beträgt. Anschließend wird dieses Image-Element vom Programm rotiert und umgerechnet, vergrößert oder verkleinert, indem eine Transformation berechnet und auf die RenderTransform-Eigenschaft des Image-Elements angewendet wird.

Der schwierigste Teil beim Verfassen eines derartigen Codes ist natürlich das Berechnen. Ich war daher angenehm überrascht, zu sehen, dass das Programm mit Bildern im Hoch- wie im Querformat funktioniert, wenn für den Drucker das Hoch- bzw. Querformat festgelegt ist.

Weniger angenehm war allerdings, dass das Programm bei großen Bildern nicht funktioniert. Sie können dies selber mit Bildern ausprobieren, deren Abmessungen (geteilt durch 96) größer als die Seite im Zoll-Maß sind. Das Bild wird jeweils in der richtigen Größe angezeigt, aber nicht vollständig.

Was geht hier vor sich? Ich kenne dieses Phänomen von Videoanzeigen. Man muss bedenken, dass sich RenderTransform nur auf die Anzeige des Elements auswirkt, nicht darauf, wie das Layoutsystem das Element behandelt. Aus Sicht des Layoutsystems zeige ich eine Bitmap in einem Image-Element an, dessen Stretch-Eigenschaft auf „None“ festgelegt ist – das Image-Element ist also so groß wie die eigentliche Bitmap. Wenn die Bitmap größer als die Druckerseite ist, braucht ein Teil dieses Image-Elements nicht gerendert werden, sondern es wird abgeschnitten. Dabei spielt es keine Rolle, ob das Image-Element mithilfe von RenderTransform entsprechend verkleinert wird.

Bei meinem zweiten Versuch (den Sie unter bit.ly/g4HJ1C ausprobieren können) habe ich eine andere Strategie verfolgt. Die OnPrintPage-Methode wird in Abbildung 3 gezeigt. Für das Image-Element habe ich ausdrückliche Width- und Height-Einstellungen festgelegt, wodurch es genau so groß wie der berechnete Anzeigebereich wird. Da sich das Ganze innerhalb des Druckbereichs der Seite abspielt, wird nichts abgeschnitten. Für den Stretch-Modus ist „Fill“ angegeben. Dies bedeutet, dass die Bitmap das Image-Element unabhängig vom Seitenverhältnis ausfüllt. Wenn das Image-Element nicht rotiert wird, ist eine Abmessung bereits richtig angegeben, und auf die andere Abmessung muss ein Skalierungsfaktor zum Verkleinern angegeben werden. Wenn das Image-Element rotiert werden muss, muss bei den Skalierungsfaktoren das abweichende Seitenverhältnis für das rotierte Image-Element berücksichtigt werden.

Abbildung 3 Drucken eines Image-Elements mit 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;
}

Der Code ist unübersichtlich, keine Frage. Und ich vermute, dass ich vielleicht Stellen übersehen habe, die vereinfacht werden können. Aber der Code funktioniert für Bitmaps jeder Größe!

Ein anderer Ansatz ist, die Bitmap und nicht das Image-Element zu rotieren. Erstellen Sie ein WriteableBitmap-Objekt aus dem geladenen BitmapImage-Objekt, und kreieren Sie dann ein zweites WritableBitmap-Objekt, bei dem die horizontalen und vertikalen Abmessungen vertauscht sind. Kopieren Sie dann alle Pixel aus dem ersten WriteableBitmap-Objekt in das zweite, und vertauschen Sie dabei Zeilen und Spalten.

Mehrere Kalenderseiten

Das Ableiten von UserControl ist eine äußerst beliebte Methode bei der Silverlight-Programmierung, um mit wenig Aufwand ein wiederverwendbares Steuerelement zu erstellen. UserControl besteht zum großen Teil aus einer in XAML definierten visuellen Struktur.

Sie können durch Ableiten von UserControl auch eine visuelle Struktur zum Drucken definieren! Diese Technik wird im PrintCalendar-Programm verdeutlicht, das Sie unter bit.ly/dIwSsn ausprobieren können. Sie geben einen Start- und einen Endmonat ein, und das Programm druckt alle Monate in diesem Bereich aus – jeweils einen Monat pro Seite. Sie können die Seiten genau wie bei jedem anderen Wandkalender an der Wand aufhängen und beschriften.

Nach meinen Erfahrungen mit dem PrintImage-Programm wollte ich weder die Ränder noch die Ausrichtung festlegen. Stattdessen habe ich eine Schaltfläche integriert (siehe Abbildung 4) und so die Verantwortung dem Benutzer übertragen.

The PrintCalendar Button

Abbildung 4 Die PrintCalendar-Schaltfläche

Das UserControl-Steuerelement, das die Kalenderseite definiert, heißt CalendarPage, und die XAML-Datei ist in Abbildung 5 dargestellt. Durch ein TextBlock-Element relativ weit oben werden Monat und Jahr angezeigt. Darauf folgt ein zweites Grid-Element mit sieben Spalten (für die Wochentage) und sechs Zeilen (für bis zu sechs Wochen bzw. Teilwochen pro Monat).

Abbildung 5 Das Layout für 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>

Im Gegensatz zu den meisten UserControl-Ableitungen definiert CalendarPage einen Konstruktor mit einem Parameter (siehe Abbildung 6).

Abbildung 6 Der Codebehind-Konstruktor für 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);
}

Es handelt es sich um einen DateTime-Parameter, und der Konstruktor erstellt mit den Eigenschaften „Month“ und „Year“ ein Border-Element, das für jeden Tag des Monats ein TextBlock-Element enthält. Diesen Elementen werden jeweils Grid.Row- und Grid.Column-Eigenschaften zugewiesen, bevor sie zum Grid-Element hinzugefügt werden. Wie Sie wissen, ziehen sich viele Monate über fünf Wochen hin, und in manchen Jahren besteht der Februar nur aus vier Wochen. Deshalb werden nicht benötigte RowDefinition-Objekte aus dem Raster entfernt.

UserControl-Ableitungen definieren in der Regel keine Konstruktoren mit Parametern, da sie normalerweise Teil einer größeren visuellen Struktur sind. Doch CalendarPage wird anders verwendet. Hier weist der PrintPage-Handler der PageVisual-Eigenschaft von PrintPageEventArgs einfach eine neue Instanz von CalendarPage zu. Nachfolgend ist der gesamte Text des Handlers aufgeführt, und Sie können erkennen, wie viel Arbeit von CalendarPage übernommen wird:

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

Ein Druckoption zu einem Programm hinzuzufügen wird oftmals als unangenehme Aufgabe mit jeder Menge Code angesehen. Wenn man den Großteil einer gedruckten Seite in einer XAML-Datei definieren kann, sieht die Sache schon viel machbarer aus.        

Charles Petzold schreibt seit langem redaktionelle Beiträge für das MSDN Magazin. Sein neues Buch „Programming in Windows Phone 7“ (Microsoft Press 2010) ist als kostenloser Download unter bit.ly/cpebookpdf verfügbar.

Unser Dank gilt den folgenden technischen Experten für die Durchsicht dieses Artikels: Saied Khanahmadi und Robert Lyon