DevTrain

Autor: Tobi Ulm

Drucken unter Windows Forms .net

Das .net Framework bietet eine Menge an neuen Klassen die benutzt werden können um schnell und effektiv Programme zu entwickeln. Natürlich bietet das .net Framework auch neue Druck-Funktionalitäten, die allerdings auf den ersten Blick verwirren können. Dabei ist jedoch zu sagen, dass das Drucken unter .net einfacher ist als wenn man Drucken mit der Win32 Printer API implementiert. Der Ausgangspunkt für das Drucken unter .net ist der Namespace System.Drawing.Printing, deswegen muss dieser Namespace auch von der Anwendung Referenziert werden.
Das .net Printer Framework besteht aus mehreren Klassen, die alle aufeinander verweisen. So hat zum Beispiel die PrinterSettings Klasse eine Eigenschaft die da PageSettings heißt. Umgekehrt ist es so, das die PageSettings Klasse eine Eigenschaft besitzt die PrinterSettings heißt.
Um ihnen einen schnellen Einstieg in die Druckfunktionalität von .net zu ermöglichen habe ich ein Beispiel erzeugt welches einen einfachen Text Editor nachstellt. Die Funktionalität des Editors ist an dieser Stelle zweitrangig und sie können den Editor noch nach eigenem Bedarf verändern und mit mehr Funktionalität ausstatten. Auch zu erwähnen ist noch das die beiden Klassen PrinterSettings und PageSettings nicht in meiner Anwendung direkt über Instanzbildung verwendet werden, da die Standardeigenschaften des PrintDocument Objekts benutzt, bzw. überschrieben werden.

Das Quellcodebeispiel:
Der Editor ist eine einfache Windows Form Anwendung mit einer Form. Dabei werden beim Start der Anwendung zwei Methoden aufgerufen die die Oberfläche des Editors aufbauen:
- MakeTextArea()
und
- MakeMenu();
MakeTextArea baut dabei ein Textfeld auf das mit der Docking Eigenschaft System.Windows.Forms.DockStyle.Fill das ganze Fenster der Form ausfüllt. Weiterhin werden die Eigenschaften Multiline, Scrollbars, WordWrap und AcceptTab gesetzt. Die Eigenschaft Multiline wird auf True gesetzt um mehrzeilige Editfunktionalität in der Textbox zu ermöglichen. ScrollBars wird auf ScrollBars.Both gesetzt um horizontale und vertikale Scroll Balken im Textfeld zu ermöglichen. Die Eigenschaft WordWrap der TextBox1 wird auf True gesetzt. Die Eigenschaft AcceptTab wird ebenfalls auf true gesetzt um den Tabulator in der Textbox zu aktivieren.

Die Methode MakeMenu baut ein Hauptmenü mnPrinting mit einem MenuItem (Datei) und darunterhängende Menüeinträge (DateiÖffnen, Druckvorschau, Seite einrichten, Drucken)

Der Drucker
Unter Windows können ja auf einer Maschine mehrere Drucker "installiert" sein. Dabei muss der Drucker ja nicht direkt an dem Computer angeschlossen sein. Jedes Windows Forms Programm versucht den Windows Standard Drucker zu benutzen. Will man jedoch auf einem anderem Drucker drucken so kann man über die Klasse PrinterSettings den Druck auf einen anderen Drucker umleiten oder aber auch die Eigenschaften des aktuellen Druckers einzustellen. Wenn über den Aufruf:
PrinterSettings psDruckerEigenschaften = new PrinterSettings(); eine Instanz des PrinterSettings Klasse erzeugt wird werden die Standardwerte des Standarddruckers von Windows übernommen. Danach haben wir die Möglichkeit über die Eigenschaften PrinterName des Objektes psDruckerEigenschaften einen anderen Drucker auszuwählen. Über die Eigenschaft Landscape kann die Druckausgabe im Querformat eingestellt werden oder über die Duplex Eigenschaft kann die Doppelseitendruck eingestellt werden, allerdings nur dann wenn der Drucker auch Duplexfunktionaltität bietet. Dies kann über die Eigenschaft CanDuplex abgefragt werden.

Die Eigenschaften der Druck Seite
Die Klasse PageSettings kann die Eigenschaften wie ein Dokument auf dem Drucker ausgegeben wird erheblich beeinflussen. Damit kann zum Beispiel das Format auf 17*11 Inches gesetzt werden. Dabei ist zu beachten, dass diese Klasse sehr speziell mit einem Drucker zu tun hat. So sollte man das Format auch nur auf 17*11 Inches setzen, wenn der Drucker dieses Format auch unterstützt. Um das Ausgabeformat zu verändern muss lediglich ein Objekt der Klasse PageSettings erzeugt werden, die Eigenschaften entsprechend verändert werden und dann die PageSettings Eigenschaft eines PrinterSettings Objektes gesetzt werden. Zum Beispiel setzt die PageSettings.PageSize Eigenschaft die Größe des aktuellen zu druckenden Seite. Achtung: Auch hier handelt es sich um eine Eigenschaft die durch eine Instanz der Klasse PageSize gefüllt wird. Die Klasse PageSize wiederum hat eine Heigth und Width Eigenschaft welche die Seitengrößen in Hundertstel eines Inches angeben.

Die PrintDocument Klasse
Jedes Dokument das von ihrem Programm aus gedruckt wird, egal ob es sich um eine Seite oder um mehrere Seiten mit Text handelt, oder ob sie eine Grafik ausdrucken, wird über die Klasse PrintDocument repräsentiert. Sie müssen dabei für professionelle Druckausgaben jedoch einen Punkt beachten. So ist es recht einfach eine Instanz der PrintDocument Klasse zu erzeugen und die Methode Print() des Objektes aufzurufen. Was hier jedoch wichtig ist, ist die Implementierung eines Events. Der PrintDocument.PrintPage Event des Objektes ist dafür verantwortlich wie gedruckt wird. Das heißt in dem EventHandler für diesen Event können eigene Formatierungen gesetzt werden, wie zum Beispiel eine Überschrift oder Seitenzahlen. Dies wird auch in meinem Beispiel gemacht, da hier untersucht wird ob es sich bei dem zu druckenden Stream um mehrere Seiten handelt, und es wird außerdem noch ein Seitenrand eingestellt. Eine Instanz der PrintDocument Klasse muss also immer erzeugt werden, wenn ihr Programm etwas ausdrucken soll. Verwendet das Programm auch noch Standard Klassen wie die unten beschriebenen Klassen PrintDialog oder PrintPreview, muss das PrintDocument  Objekt an die jeweiligen Instanzen und deren Document Eigenschaft übergeben werden, damit diese Klassen auch wissen was eigentlich gedruckt werden soll.

Der PrintDocument.PagePrint Event
Wie bereits in der Beschreibung zu Klasse PrintDocument erwähnt ist der Event PagePrint der Event der implementiert werden muss, wenn eine eigene Formatierung des Druckergebnisses gewünscht ist. In unserem Quellcodebeispiel heißt der Eventhandler für den PagePrint Event PagePrint_Event. Da es sich ja bekanntlich bei Eventhandlern um Delegates handelt muss diese Funktion dem original EventHandler der PrintDocument Klasse entsprechen und deshalb finden sie neben dem obligatorischen Sende Objekt (object sender) auch noch die PrintPageEventArgs.  Dieses Objekt enthält alle wichtigen Daten die benötigt werden um einen Druckvorgang anzustoßen. Für unser Beispiel ist nun wichtig festzuhalten, dass das Drucken ja unter dem Namespace System.Drawing.Printing zu finden ist und das das PrintPageEventArgs Objekt eine weiteres Objekt innehat, nämlich das Graphics Objekt. Dieses Objekt repräsentiert den zu Druckenden Content, der den Druck Stream erzeugt und den wir auch über dieses Objekt überschreiben können.
Dies passiert während der Initialisierung eines neuen Graphics Objektes über den Aufruf Graphics grfx = e.Graphics;
Um nun den Text auf den Druck Stream zu bringen bedarf es eigentlich nur des Aufrufes der DrawString Mehtode des grfx Graphic Objektes. Diese Methode ist mehrfach Überladen und bietet verschiedenste Parameter die gesetzt werden können um das zu druckenden "Etwas" auf der Seite zu positionieren. Vorsicht ist an dieser Stelle angebracht, wenn der Text die PaperSize Eigenschaften des PrintDocuments überragt. Passt der Text nicht auf eine einzelne Seite sollte die Anzahl der gesamt Zeilen des Textes  und die gesamte Seitenanzahl ermittelt werden. In unserem Beispiel passiert das über ein, bzw. zwei RectangleF Objekte. Dies deshalb, weil ich der Seite einen Rahmen mitgeben will (rectfFull), der nicht bedruckt wird. Der zweite Rahmen (rectfText) steht für den eigentlichen Platz in dem gedruckt werden kann. Zusätzliche Funktionen liefern mir den Text abhängig von den Dimensionen des rectfText Objektes, der pro Seite Platz findet oder auf die nächste Seite gedruckt werden muss. Dazu hilft mir die Funktion CharsInLines(), die ermittelt wie viele Zeichen in den verschiedenen Zeilen enthalten sind. Der Rest des Quellcodes und der Formatierung ist "Nice To Have" und nicht zwingend notwendig. So erzeuge ich zum Beispiel Header und Footer für jede Seite. Der Header wird durch folgende Anweisung der zu druckenden Seite(n) hinzugefügt:

strfmt.Alignment = StringAlignment.Center;
grfx.DrawString(FileTitle(), font, Brushes.Black,
rectfFull, strfmt);

Der Footer durch:

 strfmt.LineAlignment = StringAlignment.Far;
   grfx.DrawString("Seite " + iPageNumber, font, Brushes.Black,
    rectfFull, strfmt);

Nun zu zwei weiteren nützlichen Klassen im .net Druckframework. Den Klassen PrintDialog und PrintPreview. Das .net Framework währe ja nicht das was es ist, wenn man alles selber coden müsste. Man kann diese Klassen als "Common Dialogs" ansehen.

Die PrintDialog Klasse
Die PrintDialog Klasse ist eine Klasse die dem Benutzer des Programms ein einfaches User Interface zur Verfügung stellt um den richtigen Drucker für den Druck Job auszuwählen oder was denn gedruckt werden soll und ob zum Beispiel Kopien erzeugt werden sollen. Wie bereits erwähnt muss ein PrintDocument der Document Eigenschaft des PrintDialog Objektes zugewiesen werden. Der Dialog kann dann ganz einfach per ppdPreview.Show(); angezeigt werden.

Die PrintPreview Klassen
Im Printing Namespace gibt es zwei Klassen, einmal die PrintPreviewControl Klasse und die PrintPreviewDialog Klasse. Die PrintPreviewControl Klasse sollte nur dann benutzt werden, wenn eine eigene Sichtweise der Druckvorschau implementiert werden soll, also wichtig für alle CAD & CO. Tools Erzeuger. Für die meisten Belange völlig ausreichend ist die PrintPreviewDialog Klasse die ein Standard User Interface für die Druckvorschau bietet.
Die PrintPreviewDialog Klasse ist eine schnelle Möglichkeit um dem User eine Druckvorschau generieren zu lassen. Das Generieren passiert abhängig davon ob ein PrintPage Event Handler implementiert wurde oder nicht. Wurde ein solcher EventHandler implementiert wird die Druckvorschau dementsprechend gerendert. Auch hier muss ein PrintDocument der Document Eigenschaft zugewiesen werden. Zum Anzeigen des Dialoges muss auch nur wieder die ShowDialog() Methode der Instanz aufgerufen werden:
psdSettings.ShowDialog();

Der gesamte Beispielcode:

using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Drawing.Printing;
using System.IO;

namespace netPrinting_CS
{
 /// <summary>
 /// Summary description for Form1.
 /// </summary>
 public class Form1 : System.Windows.Forms.Form
 {
  /// <summary>
  /// Required designer variable.
  /// </summary>
  protected string strFileName;
  protected TextBox txtEdit;
  protected PrintDocument pdText = new PrintDocument();
  protected PageSetupDialog psdSettings = new PageSetupDialog();
  protected PrintPreviewDialog ppdPreview = new PrintPreviewDialog();
  protected PrintDialog pdPrint = new PrintDialog();
  private System.ComponentModel.Container components = null;
  string  strPrintText;
  int iStartPage, iNumPages, iPageNumber;

  public Form1()
  {
   //
   // Required for Windows Form Designer support
   //
   InitializeComponent();
   //psSeitenEigenschaften.PaperSize =
   //
   // TODO: Add any constructor code after InitializeComponent call
   //
  }

  /// <summary>
  /// Clean up any resources being used.
  /// </summary>
  protected override void Dispose( bool disposing )
  {
   if( disposing )
   {
    if (components != null)
    {
     components.Dispose();
    }
   }
   base.Dispose( disposing );
  }

  #region Windows Form Designer generated code
  /// <summary>
  /// Required method for Designer support - do not modify
  /// the contents of this method with the code editor.
  /// </summary>
  private void InitializeComponent()
  {
   //
   // Form1
   //
   this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);
   this.ClientSize = new System.Drawing.Size(504, 429);
   this.Name = "Form1";
   this.Text = "Editor";
   this.Load += new System.EventHandler(this.Form1_Load);
   psdSettings.Document = pdText;
   ppdPreview.Document = pdText;
   pdPrint.Document = pdText;
   pdText.PrintPage += new PrintPageEventHandler(PagePrint_Event);
   pdPrint.AllowSomePages = true;
   pdPrint.PrinterSettings.FromPage = 1;
   pdPrint.PrinterSettings.ToPage = pdPrint.PrinterSettings.MaximumPage;
  }
  #endregion

  /// <summary>
  /// The main entry point for the application.
  /// </summary>
  [STAThread]
  static void Main()
  {
   Application.Run(new Form1());
  }

  private void Form1_Load(object sender, System.EventArgs e) {
   this.Text = "Editor";
   MakeTextArea();
   MakeMenu();
  }
  protected  void MakeTextArea() {
   txtEdit = new TextBox();
   txtEdit.Multiline = true;
   txtEdit.Dock = System.Windows.Forms.DockStyle.Fill;
   txtEdit.Parent = this;
   txtEdit.ScrollBars  = ScrollBars.Both;
   txtEdit.AcceptsTab  = true;
   txtEdit.WordWrap = true;
   //this.Controls.Add(txtEdit);
  }
  private void MakeMenu() {
   //Neues Hauptmnenue
   MainMenu mnPrinting = new MainMenu();
   this.Menu = mnPrinting;
   //Datei Menue
   MenuItem mniDatei = new MenuItem("&Datei");
   Menu.MenuItems.Add(mniDatei);
   //Datei Öffnen
   MenuItem mniFileOpen = new MenuItem("Datei &öffnen");
   Menu.MenuItems[0].MenuItems.Add(mniFileOpen);
   mniFileOpen.Click += new EventHandler(FileOpen_Click);
   //Seiteneinstellungen
   MenuItem mniPageSettings = new MenuItem("&Seite einrichten");
   Menu.MenuItems[0].MenuItems.Add(mniPageSettings);
   mniPageSettings.Click += new EventHandler(PageSettings_Click);
   //Druckvorschau
   MenuItem mniPrintPreview = new MenuItem("Druck&vorschau");
   Menu.MenuItems[0].MenuItems.Add(mniPrintPreview);
   mniPrintPreview.Click += new EventHandler(Preview_Click);
   //Der PrintDialog
   MenuItem mniPrintDialog = new MenuItem("PrintDialo&g");
   Menu.MenuItems[0].MenuItems.Add(mniPrintDialog);
   mniPrintDialog.Click += new EventHandler(PrintDialog_Click);
   //Drucken
   MenuItem mniPrint = new MenuItem("&Drucken");
   Menu.MenuItems[0].MenuItems.Add(mniPrint);
   mniPrint.Click += new EventHandler(Print_Click);
   //Programm beenden
   MenuItem mniExit = new MenuItem("&Beenden");
   Menu.MenuItems[0].MenuItems.Add(mniExit);
   mniExit.Click += new EventHandler(Exit_Click);
  }

  private void Print_Click(object sender, EventArgs e) {
   strPrintText = txtEdit.Text;
   iStartPage   = 1;
   iNumPages    = pdPrint.PrinterSettings.MaximumPage;
   iPageNumber  = 1;
   pdText.Print();
  }
  private void Preview_Click(object sender, EventArgs e) {
   strPrintText = txtEdit.Text;
   iStartPage   = 1;
   iNumPages    = pdPrint.PrinterSettings.MaximumPage;
   iPageNumber  = 1;
   ppdPreview.Show();
  }
  private void PageSettings_Click(object sender, EventArgs e) {
   psdSettings.ShowDialog();
  }
  private void Exit_Click(object sender, System.EventArgs e) {
   Application.Exit();
  }
  private void FileOpen_Click(object sender, System.EventArgs e) {
   OpenFileDialog ofdOpen = new OpenFileDialog();
   ofdOpen.Filter = "Text Dokumente(*.txt)|*.txt|Alle Dateien(*.*)|*.*";
   ofdOpen.FileName = "*.txt";
   if (ofdOpen.ShowDialog() == DialogResult.OK) {
    try {
     StreamReader srText = new StreamReader(ofdOpen.FileName.ToString());
     strFileName = ofdOpen.FileName.ToString();
     txtEdit.Text = srText.ReadToEnd();
     srText.Close();
    }
    catch (Exception eXP) {
     System.Windows.Forms.MessageBox.Show(eXP.ToString());
     return;
    }
   }
  }

  private void PagePrint_Event(object sender, PrintPageEventArgs e) {
   Graphics grfx   = e.Graphics;
   Font font   = txtEdit.Font;
   float cyFont = font.GetHeight(grfx);
   StringFormat strfmt = new StringFormat();
   RectangleF rectfFull, rectfText;
   int iChars, iLines;
   // Berechnen des Rahmens RectangleF für den Header und den Footer.
   if (grfx.VisibleClipBounds.X < 0) {       // Drucken der Vorschau
    rectfFull = e.MarginBounds;
   }
   else{                                    // 'Normales' Drucken
    rectfFull = new RectangleF(
     e.MarginBounds.Left - (e.PageBounds.Width -
     grfx.VisibleClipBounds.Width) / 2,
     e.MarginBounds.Top - (e.PageBounds.Height -
     grfx.VisibleClipBounds.Height) / 2,
     e.MarginBounds.Width, e.MarginBounds.Height);
   }
   // Berechnen des Rahmens RectangleF für den Text.
   rectfText = RectangleF.Inflate(rectfFull, 0, -2 * cyFont);
   int iDisplayLines = (int) Math.Floor(rectfText.Height / cyFont);
   rectfText.Height = iDisplayLines * cyFont;
   // Einstellen des StringFormat Objektes um den Text in einem Rechteck darzustellen.
   if (txtEdit.WordWrap) {
    strfmt.Trimming = StringTrimming.Word;
   }
   else {
    strfmt.Trimming = StringTrimming.EllipsisCharacter;
    strfmt.FormatFlags |= StringFormatFlags.NoWrap;
   }
   // For "some pages" get to the first page.
   while ((iPageNumber < iStartPage) && (strPrintText.Length > 0)) {
    if (txtEdit.WordWrap){
     grfx.MeasureString(strPrintText, font, rectfText.Size,
      strfmt, out iChars, out iLines);
    }
    else{
     iChars = CharsInLines(strPrintText, iDisplayLines);
    }
    strPrintText = strPrintText.Substring(iChars);
    iPageNumber++;
   }
   // Falls kein zu druckender Text mehr vorhanden ist, Druck abbrechen.
   if (strPrintText.Length == 0) {
    e.Cancel = true;
    return;
   }
   // Text der aktuellen Seite ermitteln
   grfx.DrawString(strPrintText, font, Brushes.Black,
    rectfText, strfmt);
   // Text der nächsten Seite ermitteln.
   if (txtEdit.WordWrap){
    grfx.MeasureString(strPrintText, font, rectfText.Size,
     strfmt, out iChars, out iLines);
   }
   else{
    iChars = CharsInLines(strPrintText, iDisplayLines);
   }
   strPrintText = strPrintText.Substring(iChars);
   // Reset StringFormat für den  Header und den Footer der einzelnen Seite.
   strfmt = new StringFormat();
   // Anzeigen des Dateinamens am Anfang der Seite
   strfmt.Alignment = StringAlignment.Center;
   grfx.DrawString(FileTitle(), font, Brushes.Black,
    rectfFull, strfmt);
   // Anzeigen der Seitenzahl am Seitenende
   strfmt.LineAlignment = StringAlignment.Far;
   grfx.DrawString("Seite " + iPageNumber, font, Brushes.Black,
    rectfFull, strfmt);
   // Weitere Seite drucken?
   iPageNumber++;
   e.HasMorePages = (strPrintText.Length > 0) &&
    (iPageNumber < iStartPage + iNumPages);
   // Reinitialisierung der Variablen um aus der Druckvorschau drucken zu können.
   if (!e.HasMorePages) {
    strPrintText = txtEdit.Text;
    iStartPage   = 1;
    iNumPages    = pdPrint.PrinterSettings.MaximumPage;
    iPageNumber  = 1;
   }
  }
  int CharsInLines(string strPrintText, int iNumLines) {
   int index = 0;
   for (int i = 0; i < iNumLines; i++) {
    index = 1 + strPrintText.IndexOf(' ', index);
    if (index == 0){
     return strPrintText.Length;
    }
   }
   return index;
  }
  protected string FileTitle() {
   return (strFileName != null && strFileName.Length > 1) ?
    Path.GetFileName(strFileName) : "Unbenannt";
  }

  protected void PrintDialog_Click(object sender, System.EventArgs e) {
   pdPrint.ShowDialog();
  }
 }
}


Erfasst am: 21.05.2002 - Artikel-URL: http://www.devtrain.de/news.aspx?artnr=771
© Copyright 2003 ppedv AG - http://www.ppedv.de