Jürgen Bayer Informatik  
Home      Dienstleistungen      Referenzen      Kontakt     

Jürgen Bayer

Erratum zum C# 2005 Codebook

Verbesserungen und Fehlerkorrekturen in den Rezepten

Stand: 18.02.2008

Druckversion dieses Artikels (PDF)

Inhalt

Einige Worte zuvor Das Dokument »Neue Rezepte« Die Mailingliste Beispiele und Rezepte Das Repository Verbesserte Rezepte 18.02.2008 24.08.2007 09.07.2007 05.05.2007 15.03.2007 17.02.2007 20.01.2007 18.10.2006 13.9.2006 Basics 035 Zufalls-String berechnen 041 Arrays, ArrayList- und andere Auflistungen durchsuchen 044 Die Nachrichten einer Exception und ihrer inneren Exceptions ermitteln Anwendungen, Anwendungs-Konfiguration, Prozesse und Dienste 074 Befehlszeilenargumente auswerten 075 Ausnahmen global behandeln 077 Konfigurationsdaten in der .config-Datei verwalten 092 Konsolenanwendungen starten und die Ausgabe auswerten Dateisystem 123 Programmdateien in den Systempfaden suchen Text-, binäre und ZIP-Dateien 154 Dateien in ZIP-Archive komprimieren 155 (ZIP-)Archive aus einem Ordner erzeugen 157 (ZIP-)Archive entpacken Internet 197 E-Mails über einen SMTP-Server versenden 198 E-Mails über MAPI bzw. Outlook versenden Formulare und Steuerelemente 227: Bei der Betätigung der Return-Taste die Tab-Taste simulieren 229 Die angezeigten Zeilen einer MultiLine-TextBox auslesen 230 ComboBox mit Autovervollständigung Benutzer, Gruppen und Sicherheit 262 Daten symmetrisch ver- und entschlüsseln 263 Daten mit Hashing-Verfahren verschlüsseln Bildbearbeitung 268 Das Format eines Bilds auslesen 269 Bild-Metadaten auslesen 270 Das Aufnahmedatum eines Bilds auslesen 271 Eingelesene Bilder im Originalformat speichern 272 Bild in Byte-Array umwandeln 273 Byte-Array in Bitmap umwandeln 274 Bilder aus der Zwischenablage auslesen 275 Screenshot erstellen 276 Bilder skalieren 277 Thumbnails aus Bildern erzeugen 278 Bilder konvertieren 279 (JPEG-)Bilder mit definierter Qualität speichern 280 Bilder drehen, neigen und spiegeln 281 Bildausschnitte auslesen 282 Farben von Bildern auf andere Farben mappen 283 Farbinformationen von Bildern gezielt verändern 284 Ein Negativ eines Bilds erzeugen 285 Die einzelnen Pixel eines Bilds bearbeiten 286 Farb-Bilder in Graustufen-Bilder umwandeln Zeichnen 289 Rechtecke mit abgerundeten Ecken zeichnen 290 Pfeile zeichnen 291 Transparente Bilder und Grafiken erzeugen 292 Bilder mit Schatten zeichnen 293 Schräg zeichnen und Zeichenobjekte rotieren 294 Den Drehpunkt eines Rechtecks so ermitteln, dass die Ecke links oben an derselben Position bleibt 295 Text an einer definierten Position in 90-Grad-Schritten gedreht ausgeben 296 Die Breite und Höhe eines auszugebenden Textes bestimmen 297 Texte zentriert oder rechtsbündig zeichnen 298 Strings beim Zeichnen wortgerecht umbrechen Reflection und Serialisierung 310 Objekte nach XML serialisieren und von XML deserialisieren Threading 312 In einem Thread sicher auf Steuerelemente zugreifen Datenbanken 317 Datenbanken erzeugen 318 Abfragen der automatisch vergebenen Id eines neuen Datensatzes 319 Bilder und andere binäre Daten in einer Datenbank verwalten

Einige Worte zuvor

Das Dokument »Neue Rezepte«

Ich veröffentliche die neuen Rezepte, die mit dem C# 2008 Codebook erscheinen, in einem separaten Dokument, das Sie hier finden:

www.juergen-bayer.net/buecher/csharpcodebook2/artikel/Erratum/Neue_Rezepte.aspx.

Die Mailingliste

An der Adresse www.juergen-bayer.net/buecher/ErratumMailingList.aspx?Erratum=CSharpCodebook2 können Sie Ihre E-Mail-Adresse in eine Mailingliste eintragen (und austragen). Wenn Sie dort registriert sind, erhalten sie jedes Mal, wenn ich das Erratum oder das Dokument mit den neuen Rezepten aktualisieren, eine Informations-E-Mail.

Beispiele und Rezepte

Dieses Erratum enthält einige verbesserte Rezepte meines C# 2005 Premium Codebooks. Beispiele zu diesen Rezepten finden Sie an der Adresse www.juergen-bayer.net/buecher/csharpcodebook2/neues/Neue-Beispiele.zip

Die Beispiele enthalten auch die neuen Rezepte, die ich in dem separaten Artikel beschreibe.

Sie können die geänderten und die neuen Rezepte auch in das Repository des Codebook übernehmen. Kopieren Sie dazu den Ordner Repository der Buch-CD auf Ihre Festplatte. Dann kopieren Sie die Dateien des Archivs, das Sie an der Adresse www.juergen-bayer.net/buecher/csharpcodebook2/neues/Repository-Add-Ons.zip downloaden können, in diesen Ordner. Beachten Sie, dass Sie beim Kopieren die Struktur der Unterordner beibehalten müssen.

Das Repository

Die gelbe Farbe des Buchs führt dazu, dass das Repository auf dem Bildschirm sehr schlecht lesbar ist. Deshalb sollten Sie die CSS-Datei des Repositories ändern. Kopieren Sie das Repository dazu auf Ihre Festplatte. Ersetzen Sie in der Datei \styles\styles.css die Farbe #E2AC16 auf eine besser lesbare. Ich habe z. B. die Farbe midnightblue verwendet. Eine entsprechende CSS-Datei finden Sie an der Adresse www.juergen-bayer.net/buecher/csharpcodebook2/neues/Styles.css.

Verbesserte Rezepte

18.02.2008

»074 Befehlszeilenargumente auswerten«: Dieses Rezept habe ich komplett überarbeitet, so dass es zum einen auch für WPF-Anwendungen funktioniert und die Auswertung nicht zwingend in der Main-Methode erfolgen muss.

»075 Ausnahmen global behandeln«: Dieses Rezept habe ich um die Behandlung globaler Ausnahmen in WPF-Anwendungen erweitert.

»229 Die angezeigten Zeilen einer MultiLine-TextBox auslesen«: Dieses Rezept habe ich erneut korrigiert: In der ersten Version wurden angezeigte Zeilen nicht korrekt ausgelesen, wenn diese nur aus einem Zeichen bestanden. In der zweiten Version kam es zu Problemen, wenn Zeilen ein Pluszeichen enthielten. Die neue Version mit der Lösung von Axel Seibel (eines Lesers) sollte nun (endlich) keine Probleme mehr haben (.

»230 ComboBox mit Autovervollständigung«: Wegen des Problems mit der Windows.Forms-ComboBox, dass diese die Einträge nach deren String-Darstellung sortiert, was dann Probleme macht, wenn die Liste als Quelle der Autovervollständigungsliste verwendet wird, habe ich die im ersten Codebook entwickelte AutoCompleteComboBox in einer weiterentwickelten Form wirder aufgenommen.

»262 Daten symmetrisch ver- und entschlüsseln«: Dieses Rezept hatte Probleme mit dem Verschlüsseln von Strings, die Zeichen im Unicode-Bereich über 255 enthielten. Diesen Fehler habe ich beseitigt. Außerdem habe ich die Klasse SymmetricEncryptor um den AES-Algorithmus erweitert.

»263 Daten mit Hashing-Verfahren verschlüsseln«: Dieses Rezept habe ich um die Berücksichtigung der Hashing-Klassen in der CNG-Implementierung (Cryptography Next Generation) erweitert.

»263 Daten mit Hashing-Verfahren verschlüsseln«: Dieses Rezept habe ich um die Berücksichtigung der Hashing-Klassen in der CNG-Implementierung (Cryptography Next Generation) erweitert (die laut der Dokumentation nur unter Windows Vista, Windows XP SP2 und Windows Server 2003 unterstützt werden, auf meinem XP-SP2-System aber trotzdem eine NotSupportedException hervorrufen). Außerdem habe ich der Hasher-Klasse die Eigenschaften MaxKeyLength und SupportsKey hinzugefügt. Im set-Accessor der Key-Eigenschaft wird zusätzlich überprüft, ob die Länge des übergebenen Schlüssels die Maximallänge nicht überschreitet.

»290 Pfeile zeichnen«: Dieses Rezept zeichnet die viel schöneren Pfeile nun auch für WPF-Anwendungen (.

WPF

Die folgenden Rezepte habe ich um die Berücksichtigung von WPF erweitert:

»268 Das Format eines Bilds auslesen«

»269 Bild-Metadaten auslesen«

»270 Das Aufnahmedatum eines Bilds auslesen«

»271 Eingelesene Bilder im Originalformat speichern«

»272 Bild in Byte-Array umwandeln«

»273 Byte-Array in Bitmap umwandeln«

»274 Bilder aus der Zwischenablage auslesen«

»275 Screenshot erstellen«: Dieses Rezept habe ich neben der Berücksichtigung von WPF zusätzlich noch darum verbessert, dass halbtransparente Fenster unterstützt werden.

»276 Bilder skalieren«

»277 Thumbnails aus Bildern erzeugen«

»278 Bilder konvertieren«

»279 (JPEG-)Bilder mit definierter Qualität speichern«

»280 Bilder drehen, neigen und spiegeln«

»281 Bildausschnitte auslesen«

»282 Farben von Bildern auf andere Farben mappen«

»283 Farbinformationen von Bildern gezielt verändern«

»284 Ein Negativ eines Bilds erzeugen«

»285 Die einzelnen Pixel eines Bilds bearbeiten«

»286 Farb-Bilder in Graustufen-Bilder umwandeln«

»289 Rechtecke mit abgerundeten Ecken zeichnen«

»291 Transparente Bilder und Grafiken erzeugen«

»292 Bilder mit Schatten zeichnen«

»293 Schräg zeichnen und Zeichenobjekte rotieren«

»294 Den Drehpunkt eines Rechtecks so ermitteln, dass die Ecke links oben an derselben Position bleibt«

»295 Text an einer definierten Position in 90-Grad-Schritten gedreht ausgeben«

»296 Die Breite und Höhe eines auszugebenden Textes bestimmen«

»297 Texte zentriert oder rechtsbündig «

»298 Strings beim Zeichnen wortgerecht umbrechen«

»312 In einem Thread sicher auf Steuerelemente zugreifen«

LINQ to SQL

Die folgenden Rezepte habe ich um die Berücksichtigung von LINQ to SQL erweitert:

»317 Datenbanken erzeugen«

»318 Abfragen der automatisch vergebenen Id eines neuen Datensatzes«

»319 Bilder und andere binäre Daten in einer Datenbank verwalten«

24.08.2007

»229 Die angezeigten Zeilen einer MultiLine-TextBox auslesen«: In diesem Rezept wurden angezeigte Zeilen nicht korrekt ausgelesen, wenn diese nur aus einem Zeichen bestanden. Diesen Fehler habe ich korrigiert.

09.07.2007

»035 Zufalls-String berechnen«: Die GetRandomString-Methode habe ich um das Argument type erweitert. Über dieses Argument können Sie festlegen, dass der Zufalls-String aus allen »normalen« Zeichen zwischen dem ASCII-Wert 33 und 126, ausschließlich aus Buchstaben oder ausschließlich aus kleingeschriebenen Buchstaben bestehen soll.

»197 E-Mails über einen SMTP-Server versenden«: Dieses Rezept habe ich um das Anfordern einer Übertragungs- und einer Lesebestätigung erweitert und zusätzlich die Konfiguration von SMTP in der app.config erläutert. Zum Beispiel habe ich außerdem ein Formular hinzugefügt, das Sie in Ihren Projekten als Basis für ein Mailversand-Formular verwenden können.

05.05.2007

»041 Arrays, ArrayList- und andere Auflistungen durchsuchen«: In diesem Rezept habe ich die BinarySearch-Methode als effiziente Möglichkeit vorgestellt, ein Array oder eine Auflistung zu sortieren. Ein Fehler im Rezept ist, dass BinarySearch ein sortiertes Array für eine korrekte Funktionsweise erzwingt, und nicht, wie ich beschrieben habe, eine Sortierung lediglich die Performance verbessern würde.

Zudem wird die bessere Performance beim Suchen durch das im Vergleich dazu sehr langsame Sortieren aufgehoben. Das Sortieren dauert sogar häufig so lange, dass BinarySearch sich erst dann lohnt, wenn mehr als 10 Suchvorgänge hintereinander ausgeführt werden.

Eine weitere Verbesserung des Rezepts ist die Feststellung, dass die eigene sequentielle Suche schneller (!) und flexibler ist als die Suche über IndexOf.

»092 Konsolenanwendungen starten und die Ausgabe auswerten«:
Dieses Rezept habe ich um die Auswertung des Rückgabecodes und des Fehlerkanals der Anwendung erweitert.

»157 (ZIP-)Archive entpacken«: Neben dem bereits in einer vorherigen Version korrigierten Bug, dass bei Archiveinträgen, die mit dem Ordner »\« versehen waren, eine Exception auftrat, habe ich einen weiteren Bug beseitigt: Die Vorversion führte zu einer Exception wenn ein Archiv eine 0-Byte-Datei beinhaltete. Die aktuelle Version liest diese Datei natürlich nicht aus dem Archiv aus, erzeugt sie aber.

15.03.2007

»044 Die Nachrichten einer Exception und ihrer inneren Exceptions ermitteln«: Dieses Rezept habe ich um die Behandlung von SoapExceptions und HttpExceptions erweitert.

17.02.2007

»227: Bei der Betätigung der Return-Taste die Tab-Taste simulieren«

20.01.2007

»123 Programmdateien in den Systempfaden suchen«: Die Methode FindFileInSystemPaths berücksichtigte nicht die Tatsache, dass Pfadangaben im Pfad auch in Anführungszeichen eingebettet sein können. Dieser Fall führte zu einer Exception »Illegales Zeichen im Pfad«

18.10.2006

»290 Pfeile zeichnen«: Dieses Rezept zeichnet Pfeile nun viel schöner (.

»310 Objekte nach XML serialisieren und von XML deserialisieren«: In den Methoden zum Serialisieren eines Objekts in einen XML-String und zum Deserialisieren eines XML-String in ein Objekt habe ich einen Denkfehler eingebaut. Diesen Methoden konnte die Codierung übergeben werden, was aber beim Serialisieren/Deserialisieren eines (Unicode-)Strings keinen Sinn macht bzw. sogar zu Fehlern führt. Außerdem kann es auch dann zu einem Fehler beim Deserialisieren, wenn beim Serialisieren die Unicode-Codierung angegeben wurde. Der XmlSerializer hat in diesem Fall in der im Codebook gedruckten Implementierung der SerializeToXmlString-Methode leider ein Byte Order Mark (BOM) Zeichen an den Anfang des Unicode-Zeichen-Stream gehängt. Das hier geschriebene Zeichen 0xFEFF bezeichnet eine UTF-16-Codierung in Big-Endian1. Beim Deserialisieren kam es allerdings zu einer Exception (»Ungültiges Zeichen …«).

13.9.2006

»077 Konfigurationsdaten in der .config-Datei verwalten«

Basics

035 Zufalls-String berechnen

Die GetRandomString-Methode habe ich um das Argument type erweitert. Über dieses Argument können Sie festlegen, dass der Zufalls-Sgtring aus allen »normalen« Zeichen zwischen dem ASCII-Wert 33 und 126, ausschließlich aus Buchstaben oder ausschließlich aus kleingeschriebenen Buchstaben bestehen soll.

/* Enum für die Art der Erzeugung von Zufalls-Strings */
public enum RandomStringType
{
   /* Alle normalen Zeichen zwischen 33 und 122 */
   AllRegularChars,

   /* Nur Buchstaben */
   OnlyLetters,

   /* Nur kleingeschriebene Buchstaben */
   OnlyLowercaseLetters
}

/* Erzeugt einen Zufalls-String */
public static string GetRandomString(int count, 
   RandomStringType type)
{
   StringBuilder randomString = new StringBuilder(count);

   // Echte Zufallszahlen im Bereich von 1 bis 255 erzeugen
   RNGCryptoServiceProvider rngCSP = new RNGCryptoServiceProvider();
   byte[] numbers = new byte[count];
   rngCSP.GetNonZeroBytes(numbers);
   
   // Random-Instanz für einen String mit Großbuchstaben erzeugen
   Random ucaseRandom = new Random();
   
   if (type == RandomStringType.AllRegularChars)
   {
      // Die Zahlen so umrechnen, dass Werte zwischen 33 und 126 
      // herauskommen, und die daraus resultierenden Zeichen an den 
      // Ergebnisstring anhängen
      for (int i = 0; i < count; i++)
      {
         numbers[i] = (byte)(numbers[i] / 2.713 + 33);
         randomString.Append((char)numbers[i]);
      }
   }
   else
   {
      // Die Zahlen so umrechnen, dass Werte zwischen 97 und 122 
      // herauskommen, und die daraus resultierenden Zeichen an den 
      // Ergebnisstring anhängen
      for (int i = 0; i < count; i++)
      {
         numbers[i] = (byte)(numbers[i] / 9.81 + 97);
         if (type == RandomStringType.OnlyLetters)
         {
            // Den Buchstaben zufällig in einen 
            // Großbuchstaben umwandeln
            if (ucaseRandom.Next(3) == 2)
            {
               numbers[i] = (byte)(numbers[i] - 32);
            }
         }
         randomString.Append((char)numbers[i]);
      }
   }

   return randomString.ToString();
}

041 Arrays, ArrayList- und andere Auflistungen durchsuchen

In diesem Rezept habe ich die BinarySearch-Methode als effiziente Möglichkeit vorgestellt, ein Array oder eine Auflistung zu sortieren. Ein Fehler im Rezept ist, dass BinarySearch ein sortiertes Array für eine korrekte Funktionsweise erzwingt, und nicht, wie ich beschrieben habe, eine Sortierung lediglich die Performance verbessern würde.

Zudem wird die bessere Performance beim Suchen durch das im Vergleich dazu sehr langsame Sortieren aufgehoben. Das Sortieren dauert sogar häufig so lange, dass BinarySearch sich erst dann lohnt, wenn mehr als 10 Suchvorgänge hintereinander ausgeführt werden.

Hier ist mein neuer Text:

In Auflistungen und Arrays können Sie sequentiell, mit IndexOf und mit BinarySeach suchen.

Sequentielle Suche

Sie in der Regel schnellste und flexibelste Möglichkeit ist die eigene, sequentielle Suche. Warum diese Suche normalerweise schneller als die anderen ist, kläre ich nach der Beschreibung von IndexOf und am Ende der Beschreibung von BinarySearch. Flexibel ist diese Art der Suche aus dem Grund, dass Sie selbst entscheiden können, welche Felder oder Eigenschaften der Objekte oder ob Sie die Referenzen vergleichen wollen.

Die folgende Struktur implementiert z. B. Kreisdaten:

public struct CircleStruct 
{
   public int X;
   public int Y;
   public int Radius;

   public CircleStruct(int x, int y, int radius)
   {
      this.X = x;
      this.Y = y;
      this.Radius = radius;
   }
}

Listing 0.1: Struktur zur Speicherung von Kreisdaten

Listing 0.2 zeigt, wie Sie in einer Auflistung von Instanzen der CircleStruct-Struktur nach Kreisen mit einem Radius von 50 suchen:

// Auflistung von 1.000.000 Circle-Werttyp-Objekten erzeugen
List<CircleStruct> circleValueTypeList = new List<CircleStruct>();
Random random = new Random();
for (int i = 0; i < 1000000; i++)
{
   circleValueTypeList.Add(new CircleStruct(random.Next(1, 11), 
      random.Next(1, 11), random.Next(1, 101)));
}

// Alle Kreise mit dem Radius 50 suchen
List<int> result = new List<int>();
for (int i = 0; i < circleValueTypeList.Count; i++)
{
   if (circleValueTypeList[i].Radius == 50)
   {
      result.Add(i);
   }
}

Listing 0.2: Eigene sequentielle Suche

Die sequentielle Suche funktioniert natürlich genauso mit einer Auflistung oder einem Array von Referenztypen.

Suche mit IndexOf

Auflistungen besitzen normalerweise eine IndexOf-Methode, die den Index des Objekts zurückgibt, das Sie am ersten Argument übergeben. Zur Suche in Arrays können Sie die statische IndexOf-Methode der Array-Klasse verwenden.

Diese Methode sucht sequentiell nach dem übergebenen Objekt. Die Objekte werden dabei intern über deren Equals-Methode verglichen. Ist diese nicht in den Objekten überschrieben, werden bei Referenztypen die Referenzen und bei Werttypen alle öffentlichen Felder und Eigenschaften verglichen. Speichern Sie in einer Auflistung z. B. Instanzen einer Circle-Klasse und übergeben Sie IndexOf eine Referenz auf ein Circle-Objekt, sucht diese Methode nach genau diesem Objekt. Andere Objekte, die denselben Inhalt aufweisen wie das Suchobjekt (in unserem Beispiel dieselben Werte für X, Y und Radius), werden nicht gefunden. Speichern Sie in der Auflistung Instanzen einer Struktur wird natürlich nicht nach der Referenz gesucht. In diesem Fall findet IndexOf alle Objekte, deren Inhalt dem des Suchobjekts entspricht. Beim Suchen nach einem Kreis mit X = 5, Y = 10 und Radius = 100 werden alle Kreise gefunden, deren Eigenschaften dieselben Werte aufweisen. Implementieren die Objekte allerdings die von object geerbte Equals-Methode, entspricht das Suchergebnis natürlich dieser Implementierung.

Um alle Objekte zu finden, müssen Sie IndexOf in einer Schleife aufrufen.

Listing 0.3 zeigt, wie Sie mit IndexOf in einer Auflistung von Strukturen nach Objekten suchen:

// Auflistung von 1.000.000 Circle-Werttyp-Objekten erzeugen
List<CircleStruct> circleValueTypeList = new List<CircleStruct>();
Random random = new Random();
for (int i = 0; i < 1000000; i++)
{
   circleValueTypeList.Add(new CircleStruct(random.Next(1, 11), 
      random.Next(1, 11), random.Next(1, 101)));
}

// Mit IndexOf in der Auflistung der Werttyp-Kreise suchen.
// Bei der Suche in einer Auflistung von Werttypen werden immer die 
// öffentlichen Eigenschaften und Felder der Objekte verglichen.
CircleStruct compareCircle = new CircleStruct(5, 10, 50);
List<int> result = new List<int>();

// Ersten Kreis suchen
int index = circleValueTypeList.IndexOf(compareCircle);
while (index > -1)
{
   result.Add(index);

   // Weitersuchen
   index = circleValueTypeList.IndexOf(compareCircle, index + 1);
}

Listing 0.3: IndexOf-Suche nach Werttypen in einer Auflistung

Die Performance

Die Performance der sequentiellen Suche ist nach meinen Performance-Messungen (mit einem Array und einer List-Auflistung) wesentlich besser als die Suche über IndexOf. Das sequentielle Suchen nach Kreisen mit X = 5, Y = 10 und Radius = 50 in einer Auflistung mit 1.000.000 Circle-Werttyp-Objekten benötigte z. B. lediglich ca. 0,018 Sekunden, das Suchen mit IndexOf hingegen ca. 0,13 Sekunden. Die Suche mit IndexOf nach einem bestimmten Objekt in einer Auflistung mit 1.000.000 Circle-Referenztyp-Objekten benötigte ca. 0,022 Sekunden, die sequentielle Suche nach demselben Objekt hingegen lediglich ca. 0,006 Sekunden. IndexOf ist also für die performante Suche nicht geeignet.

Ich habe auch eine Erklärung für die bessere Performance beim eigenen sequentiellen Suchen: IndexOf verwendet intern eine Schleife über alle Elemente. Innerhalb der Schleife wird das zu suchende Objekt über die Equals-Methode verglichen. Der Aufruf einer Methode benötigt natürlich ein wenig mehr Zeit als ein direkter Vergleich von Objekten.

IndexOf ist also eigentlich nur dann für eine Suche geeignet, wenn Sie nach einem Objekt suchen, das nur einmal in der Liste vorkommt, und Sie den Vergleich über Equals verwenden wollen.

IndexOf ist also eigentlich nur dann für eine Suche geeignet, wenn Sie nach einem Objekt suchen, das nur einmal in der Liste vorkommt, und Sie den Vergleich über Equals verwenden wollen. In diesem Fall ist der Performanceverlust minimal, der Codierungsaufwand dafür aber geringer als bei der sequentiellen Suche.

Suchen mit BinarySearch

Die dritte Suchmöglichkeit ist die Suche mit der BinarySearch-Methode, die in der Array-Klasse (als statische Methode) und in vielen Auflistungen zur Verfügung steht. BinarySearch sucht nach dem übergebenen Objekt und gibt den Index des gefundenen Elements zurück. Wird kein Element gefunden, das zum Suchobjekt passt, gibt diese Methode -1 zurück.

BinarySearch führt eine effiziente binäre Suche in den Elementen aus, was allerdings voraussetzt, dass die Liste aufsteigend sortiert ist. Ohne eine aufsteigende Sortierung führt BinarySearch zu keinem oder einem falschen Ergebnis! Das Sortieren setzt voraus, dass der in der Auflistung verwaltete Typ CompareTo-Methode der IComparable-Schnittstelle implementiert.

Das notwendige Sortieren ist der Knackpunkt dieser Methode, da es sehr viel Zeit in Anspruch nimmt. BinarySearch lohnt sich nur deswegen dann, wenn Sie mehrfach in demselben Array bzw. in derselben Auflistung suchen müssen, oder wenn diese sowieso schon sortiert sind.

Listing 0.4 zeigt eine erweiterte CircleClass-Klasse, die das Sortieren und Suchen nach dem Radius ermöglicht.

public class CircleClass: IComparable<Circle>
{
   public int x;
   public int y;
   public int Radius;

   public CircleClass (int x, int y, int radius)
   {
      this.x = x;
      this.y = y;
      this.Radius = radius;
   }

   /* Implementierung der CompareTo-Methode */
   public int CompareTo(CircleClass otherCircle)
   {
      return (this.Radius.CompareTo(otherCircle.Radius));
   }
}

Listing 0.4: Klasse zur Speicherung von vergleichbaren Kreisdaten

Die CompareTo-Methode ist in diesem Beispiel sehr einfach, weil zum Vergleich die gleichnamige Methode der Radius-Eigenschaft aufgerufen werden kann.

Um mit BinarySearch zu suchen, rufen Sie diese Methode zunächst einmal auf, um das erste Objekt zu finden. Dieses Objekt ist aber nicht unbedingt auch das erste der Objekte, in der Auflistung, die die Suchkriterien erfüllen. Das liegt an der Natur des Suchalgorithmus, der die Liste immer wieder in Hälften aufteilt und das Objekt in der Mitte mit dem Suchobjekt vergleicht. Das erste gefundene Objekt kann also irgendwo innerhalb der Unter-Liste der Objekte liegen, die die Suchkriterien erfüllen.

Um alle Objekte zu finden, müssen Sie nach der ersten Suche die Liste ab dem gefundenen Index einmal nach oben und einmal nach unten durchgehen. Ich zeige dies am Beispiel der bereits oben verwendeten Auflistung von 1.000.000 Kreis-Objekten:

// Auflistung von 1.000.000 Kreis-Referenztyp-Objekten erzeugen
List<CircleClass> circleReferenceTypeList = new List<CircleClass>();
Random random = new Random();
for (int i = 0; i < 1000000; i++)
{
   circleReferenceTypeList.Add(new CircleClass(random.Next(1, 11), 
      random.Next(1, 11), random.Next(1, 101)));
}

// Ersten Kreis mit einem Radius von 50 suchen
CircleClass compareCircle = new CircleClass(0, 0, 50);
List<int> result = new List<int>();
int index = circleReferenceTypeList.BinarySearch(compareCircle);
if (index > -1)
{
   // Ergebnis speichern
   result.Add(index);
}

// Nächsten Kreis nach unten suchen
for (int i = index + 1; i < circleReferenceTypeList.Count; i++)
{
   if (circleReferenceTypeList[i].CompareTo(compareCircle) == 0)
   {
      // Ergebnis speichern
      result.Add(i);
   }
   else
   {
      // Schleife abbrechen
      break;
   }
}

// Nächsten Kreis nach oben suchen
for (int i = index - 1; i > -1; i--)
{
   if (circleReferenceTypeList[i].CompareTo(compareCircle) == 0)
   {
      // Ergebnis speichern
      result.Add(i);
   }
   else
   {
      // Schleife abbrechen
      break;
   }
}

Listing 0.5: Suchen mit BinarySearch

044 Die Nachrichten einer Exception und ihrer inneren Exceptions ermitteln

Dieses Rezept habe ich um die Behandlung von Ausnahmen vom Typ SoapException und HttpException erweitert:

SoapExceptions, die dann auftreten, wenn aufgerufene Webdienst-Methoden Exceptions werfen, erfahren eine Sonderbehandlung. Bei diesen Exceptions existiert nach meinen Erfahrungen und Informationen aus dem Internet keine innere Exception. Die Nachricht der SoapException ist leider (zumindest für den Endanwender) recht kryptisch. Eine solche Nachricht sieht prinzipiell (zumindest für .NET-Webdienste) so aus:

Die Anforderung konnte vom Server nicht verarbeitet werden. --->
<Nachricht der eigentlichen Ausnahme>

Dies gilt für den Fall, dass in der Webanwendung der Modus der CustomErrors-Einstellung auf on geschaltet ist (oder auf remoteOnly und der Aufruf der Webmethode stammt von einem anderen Rechner). Ist der Modus der

CustomErrors-Einstellung auf off geschaltet (oder auf remoteOnly und der Aufruf stammt von demselben Rechner), ist die Nachricht detaillierter:

System.Web.Services.Protocols.SoapException: Die Anforderung 
konnte vom Server nicht verarbeitet werden. ---> <Typ der 
eigentlichen Exception>: <Nachricht der eigentlichen Exception>
<Stack-Trace>
   --- Ende der internen Ausnahmestapelüberwachung ---

Die Auswertung der eigentlichen Nachricht gestaltet sich damit leider etwas kompliziert.

Der Start der Nachricht wird an dem String "--->" gefolgt von beliebigem Text und einem Doppelpunkt erkannt. Das Ende der Nachricht ist etwas schwerer zu erkennen, da es ja sein kann, dass der Stack-Trace an die Nachricht angehängt ist. Der Start des Stack-Trace könnte daran erkannt werden, dass die Zeile mit " bei" beginnt, aber das wäre sehr unsicher und müsste für alle möglichen Sprachen implementiert werden. Deshalb werte ich das Muster "\n " als Anfang des Stack-Trace.

Da ich in Webanwendungsprojekten immer wieder Probleme mit auftretenden Exceptions vom Typ HttpException und der davon abgeleiteten HttpUnhandledException2 (die in ihrer Nachricht nur eine allgemeine Fehlermeldung enthalten), habe ich die Behandlung solcher Ausnahmen auch noch in die GetExceptionMessages-Methode integriert.

Zum Kompilieren dieser Methode müssen Sie die Namensräume System und System.Text.RegularExpressions importieren. Wenn Sie den Teil mit der Auswertung der HttpException-Ausnahmen übernehmen, müssen Sie außerdem die Assembly System.Web referenzieren.

public static string GetExceptionMessages(Exception ex)
{
   string messages = null;

   if (ex is System.Web.HttpException)
   {
      // Bei einer HttpException sollte GetBaseException aufgerufen
      // werden um den zugrundeliegenden Fehler herauszufinden
      ex = ((System.Web.HttpException)ex).GetBaseException();
   }

   // SoapExceptions müssen separat behandelt werden, da diese
   // einen allgemeinen Text, die Nachricht der ursprünglichen
   // Exception und den StackTrace in der Message-Eigenschaft,
   // und leider keine InnerException verwalten.
   if (ex is System.Web.Services.Protocols.SoapException)
   {
      // Nachricht auslesen
      messages = ex.Message;

      // Löschen des einleitenden Textes, der mit ---> beginnt
      int pos1 = messages.IndexOf("--->");
      if (pos1 > -1)
      {
         int pos2 = messages.IndexOf(":", pos1 + 3);
         if (pos2 > -1)
         {
            messages = messages.Remove(0, pos2 + 1).Trim();
         }
         else
         {
            messages = messages.Remove(0, pos1 + 4).Trim();
         }
      }

      // Löschen des StackTrace (der mit "\n   " eingeleitet wird)
      Match match = Regex.Match(messages, "\n   ");
      if (match.Success)
      {
         messages = messages.Remove(match.Index,
            messages.Length - match.Index).Trim();
      }
   }
   else
   {
      // Die Nachricht der aktuellen Exception ermitteln
      messages = ex.Message;

      // Die Nachricht(en) der inneren Exception ermitteln
      if (ex.InnerException != null)
      {
         messages += Environment.NewLine +
            GetExceptionMessages(ex.InnerException);
      }

      // Überprüfen, ob Nachrichten eventuell mehrfach vorkommen
      string[] messageList = Regex.Split(messages, 
         Environment.NewLine);
      for (int i = 0; i < messageList.Length; i++)
      {
         string message = messageList[i];
         for (int j = i + 1; j < messageList.Length; j++)
         {
            if (messageList[j] == messageList[i])
            {
               messageList[j] = null;
            }
         }
      }

      // Die übrig gebliebenen Nachrichten wieder zu einem String
      // zusammensetzen
      messages = null;
      for (int i = 0; i < messageList.Length; i++)
      {
         if (messageList[i] != null)
         {
            if (messages != null)
            {
               messages += Environment.NewLine;
            }
            messages += messageList[i];
         }
      }
   }

   // Ergebnis zurückgeben
   return messages;
}

Listing 0.6: Methode zur Ermittlung aller Nachrichten einer Exception

Anwendungen, Anwendungs-Konfiguration, Prozesse und Dienste

074 Befehlszeilenargumente auswerten

Dieses Rezept habe ich komplett überarbeitet, so dass es zum einen auch für WPF-Anwendungen funktioniert und die Auswertung nicht zwingend in der Main-Methode erfolgen muss:

Viele Standardanwendungen können mit Befehlszeilenargumenten aufgerufen werden. Dem Windows-Explorer können Sie zum Beispiel beim Aufruf den Pfad zu einem Ordner übergeben, den dieser anzeigen soll:

explorer C:\Windows

Befehlszeilenargumente werden dabei durch Leerzeichen vom Programmdateinamen und von anderen Befehlszeilenargumenten getrennt. Beim folgenden Aufruf eines Programms:

Demo.exe Das ist ein Test

werden die Argumente "Das", "ist", "ein" und "Test" übergeben. Werden Argumente in Anführungszeichen eingeschlossen, resultiert ein einziges Argument:

Demo.exe "Das ist ein Test"

Dieses Beispiel resultiert in dem einzigen Argument "Das ist ein Test". Argumente, die selbst Leerzeichen enthalten, müssen beim Aufruf also in Anführungszeichen eingeschlossen werden:

explorer "C:\Dokumente und Einstellungen"

In Ihren (Windows-)Anwendungen können Sie Befehlszeilenargumente ebenfalls auswerten. In einer Windows.Forms- oder Konsolenanwendung können Sie dazu die Main-Methode, die normalerweise in der Klasse Program implementiert ist, um ein Argument vom Typ string-Array erweitern:

[STAThread]
static void Main(string[] arguments)
{

Listing 0.1: Erweitern der Main-Methode um ein String-Array-Argument

In diesem Array werden alle Argumente übergeben, die beim Aufruf des Programms angegeben wurden. Dieses Vorgehen ist jedoch nicht zu empfehlen, da Sie zum einen auf die Auswertung in der Main-Methode eingeschränkt sind. Zum anderen wird die Main-Methode in einer mit Visual Studio entwickelten WPF-Anwendung automatisch erzeugt und Sie haben keine Möglichkeit, darin zu programmieren.

Der flexiblere Weg ist der über die GetCommandLineArgs-Methode der Environment-Klasse aus dem Namensraum System. Diese Methode liefert ein String-Array zurück, das (anders als der Name vermuten lässt) den kompletten Aufruf der Anwendung enthält. Im ersten Element steht immer der Dateiname der Anwendung. In den folgenden Elementen werden alle Befehlszeilenargumente verwaltet. Damit ist es recht einfach, die einzelnen Argumente auszulesen.

Um keine Probleme bei der Auswertung zu haben, sollten Sie die Syntax der einzelnen Argumente so festlegen, dass diese prinzipiell keine Leerzeichen enthalten. In der Praxis bestehen viele Befehlszeilenargumente aus einem Binde- oder Schrägstrich, gefolgt von einem Namen, einem Doppelpunkt und einem Wert:

-Argumentname:Argumentwert

Beim Aufruf der Anwendung kann der Argumentwert in Anführungszeichen eingeschlossen werden, sofern dieser Leerzeichen enthält:

Imager.exe -imageFolder:"C:\Bilder für die Website"

Die Auswertung solcher Argumente ist dann relativ einfach. Dazu gehen Sie die Elemente des von GetCommandLineArgs zurückgegebenen String-Array ab dem Index 1 durch und vergleichen die einzelnen Argumente mit den erwarteten. Die Programmierung kann an beliebiger Stelle innerhalb der Anwendung erfolgen, wird aber meistens in der Load-Methode des Start-Formulars bzw. -Fensters oder in der Main-Methode untergebracht.

Das folgende Beispiel liest auf diese Weise die erwarteten Argumente -debugmode und -imagefolder:Ordnerangabe aus. Beim imagefolder-Argument wird die Angabe eines Ordners erwartet, der mit einem Doppelpunkt vom Argumentnamen getrennt wird. Um die Groß-/Kleinschreibung nicht zu berücksichtigen werden die Argumente über die ToLower-Methode in Kleinschreibung umgewandelt. Zur Sicherheit werden unbekannte Argumente in der String-Variablen unknownArguments gesammelt und falls vorhanden nach der Auswertung in einer MessageBox gemeldet.

Das Beispiel basiert auf einer WPF-Anwendung mit den üblichen Referenzen und using-Direktiven. Im Formular ist ein Label angelegt, das lblArguments heißt. In diesem Label werden die übergebenen Befehlszeilenargumente ausgegeben.

private void Window_Loaded(object sender, RoutedEventArgs e)
{
   // Auswerten der Befehlszeilenargumente
   bool debugMode = false;
   string imageFolder = null;
   string unknownArguments = null;
   string[] args = Environment.GetCommandLineArgs();
   for (int i = 1; i < args.Length; i++)
   {
      string loweredArgument = args[i].ToLower();
      if (loweredArgument == "-debugmode")
      {
         debugMode = true;
      }
      else if (loweredArgument.StartsWith("-imagefolder:"))
      {
         // Den Argumentwert auslesen
         imageFolder = args[i].Substring(13, args[i].Length - 13);
      }
      else
      {
         // Unbekanntes Argument
         if (unknownArguments != null)
         {
            unknownArguments += ", ";
         }
         unknownArguments += args[i];
      }
   }

   // Unbekannte Argumente auswerten
   if (unknownArguments != null)
   {
      MessageBox.Show("Die folgenden Argumente sind ungültig: " +
         unknownArguments, "Befehlszeilenargumente auswerten", 
         MessageBoxButton.OK, MessageBoxImage.Exclamation);
   }

   // Die Argumente auswerten
   this.lblArguments.Content = "debugmode: " + debugMode + 
      Environment.NewLine + "imageFolder: " + imageFolder;
}

Listing 0.2: Auswerten von Befehlszeilenargumenten

Sehr nett von Windows ist, dass in Anführungszeichen eingeschlossene Argumente automatisch so ausgewertet werden, dass die Anführungszeichen entfernt werden. Beim Aufruf mit den Argumenten

-debugMode -imageFolder:"C:\Bilder für die Website"

werden zum Beispiel "-debugMode" und "-imageFolder:C:\Bilder für die Website" übergeben.

Zum Testen von Befehlszeilenargumenten können Sie diese in Visual Studio in den Eigenschaften des Projekts im Register Debuggen in das Feld Befehlszeilenargumente eintragen.

075 Ausnahmen global behandeln

Dieses Rezept habe ich um die Behandlung globaler Ausnahmen in WPF-Anwendungen erweitert:

Ausnahmen, die in der Anwendung nicht behandelt werden, werden von der der CLR in einem einfachen Dialog gemeldet. Dieser Dialog (der bei Windows.Forms-Anwendungen per Voreinstellung ein anderer ist als bei Konsolen- und WPF-Anwendungen) ist mehr oder weniger aussagekräftig, wie Abbildung 0.1 und Abbildung 0.2 zeigen.

Abbildung 0.1: Meldung der CLR bei einer unbehandelten Ausnahme in einer Windows.Forms-Anwendung ohne Debug-Modus

Abbildung 0.2: Meldung der CLR bei einer unbehandelten Ausnahme in einer WPF- oder Konsolenanwendung oder in einer Windows.Forms-Anwendung, die zum Debugging eingestellt ist

Der Debuggen-Schalter im neueren Dialog (Abbildung 0.2) ist nur dann vorhanden, wenn auf dem System ein Debugger installiert ist. Dies ist nur dann der Fall, wenn das .NET-Framework-SDK installiert ist. Über diesen Schalter kann die Anwendung debugt werden. Der Debugger berücksichtigt aber für den Fall, dass der Quellcode nicht im Ordner der Anwendung gespeichert ist (was ja in der Regel nicht der Fall ist), nur den CIL-Code. Damit können nur absolute System-Profis etwas anfangen.

Für Windows.Forms-Anwendungen können Sie in der Konfiguration einstellen, dass statt des Standard-Dialogs (Abbildung 0.1) der neuere Dialog (Abbildung 0.2) mit der Möglichkeit, zu debuggen, angezeigt wird. Dazu setzen Sie in der Maschnien- oder Anwendungskonfiguration im Element system.windows.forms das Attribut jitDebugging auf true. Zusätzlich dazu muss die Anwendung im Debug-Modus kompiliert worden sein.

Die meisten Benutzer werden wohl mit diesen Möglichkeiten überfordert sein. Außerdem kann es sein, das beim Beenden der Anwendung Daten verloren gehen. Um dies zu verhindern können Sie selbstverständlich alle Ausnahmen im Programm explizit behandeln, was Sie dadurch erreichen, dass Sie zumindest in jeder Ereignismethode eine Ausnahmebehandlung implementieren. Eigentlich ist dies auch der bessere Weg, da Sie dem Benutzer dann genauere Informationen über den Kontext des Fehlers geben können.

Alternativ können Sie unbehandelte Ausnahmen aber auch global abfangen. Basis dieser Technik ist für WPF-Anwendungen das Ereignis DispatcherUnhandledException des Objekts, das die Anwendung repräsentiert. In einer Windows.Forms-Anwendung verwenden Sie das statische Ereignis OnThreadException der Klasse Windows.Forms.Application. Diese Ereignisse werden für alle unbehandelten Ausnahmen aufgerufen, die im UI-Thread der Anwendung eintreten.

Diese Ereignisse werden allerdings nicht für unbehandelte Ausnahmen aufgerufen, die in einem anderen als dem UI-Thread auftreten. In eigenen Arbeits-Threads sollten Sie also trotz globaler Ausnahmebehandlung immer eine eigene Ausnahmebehandlung vorsehen.

In einer WPF-Anwendung erreichen Sie das DispatcherUnhandledException-Ereignis über die von Application abgeleitete Klasse (normalerweise die App-Klasse), die die WPF-Anwendung repräsentiert. Im Konstruktor können Sie das Ereignis zuweisen. Die Eigenschaft Exception des Ereignisargument-Objekts liefert die aufgetretene Ausnahme. Nach der Verarbeitung der Ausnahme müssen Sie die Eigenschaft Handled des übergebenen Ereignisargument-Objekts auf true setzen, damit die Ausnahme nicht an die CLR weiter gegeben wird:

public partial class App : Application
{
   /* Konstruktor */
   public App()
   {
      // Zuweisen der globalen Ausnahmebehandlung für den UI-Thread
      this.DispatcherUnhandledException +=  new System.Windows.Threading.
         DispatcherUnhandledExceptionEventHandler(
         this.App_DispatcherUnhandledException);
   }

   /* Behandelt alle unbehandelten Ausnahmen, die im UI-Thread auftreten.
      Behandelt keine unbehandelten Ausnahmen, die in anderen Threads 
      auftreten! */
   private void App_DispatcherUnhandledException(object sender, 
      System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e)
   {
      // Ausnahme ausgeben
      MessageBox.Show("Unerwarteter Fehler: " + e.Exception.Message,
         "Globale Ausnahmebehandlung in WPF", MessageBoxButton.OK, 
         MessageBoxImage.Error);
      
      // Das Ereignis als 'Behandelt' kennzeichnen, damit die Ausnahme
      // nicht an die CLR weitergegebern wird
      e.Handled = true;

      // Hier können (und sollten) Sie Ausnahmen protokollieren. 
      // Wenn Sie den Stack-Trace protokollieren, hilft diese enorm
      // bei der späteren Fehlersuche.
   }
}

Listing 0.3: Globale Ausnahmebehandlung in einer WPF-Anwendung

In einer Windows.Forms-Anwendung weisen Sie dem OnThreadException-Ereignis in der Main-Methode (in Program.cs) eine passende Ereignisbehandlungsmethode zu. In OnThreadException existiert keine Handled-Eigenschaft, die Sie auf true setzen müssen (wie in WPF). Wenn in einer Windows.Forms-Anwendung Ausnahmen global abgefangen werden, werden diese implizit nicht an die CLR weitergegeben.

static class Program
{
   [STAThread]
   static void Main()
   {
      // Zuweisen der globalen Ausnahmebehandlung für den UI-Thread
      Application.ThreadException += 
         new System.Threading.ThreadExceptionEventHandler(
         Application_ThreadException);

      Application.EnableVisualStyles();
      Application.SetCompatibleTextRenderingDefault(false);
      Application.Run(new MainForm());
   }

   /* Behandelt alle unbehandelten Ausnahmen, die im UI-Thread auftreten. 
      Behandelt keine unbehandelten Ausnahmen, die in anderen Threads
      auftreten! */
   private static void Application_ThreadException(object sender,
      System.Threading.ThreadExceptionEventArgs e)
   {
      // Ausnahme ausgeben
      MessageBox.Show("Unerwarteter Fehler: " + e.Exception.Message, 
         Application.ProductName, MessageBoxButtons.OK,
         MessageBoxIcon.Error);

      // Hier können (und sollten) Sie Ausnahmen protokollieren. 
      // Wenn Sie den Stack-Trace protokollieren, hilft diese enorm
      // bei der späteren Fehlersuche.
   }
}

Listing 0.4:Globale Ausnahmebehandlung in einer Windows.Forms-Anwendung

Falls Sie im Programm alle erwarteten Ausnahmen explizit abfragen und in der Methode für die globale Ereignisbehandlung eigentlich nur unerwartete Ausnahmen abgefangen werden, sollten Sie dem Anwender neben einer Information über den aufgetretenen Fehler die Möglichkeit geben, die Details (der Ausnahme-Meldung inkl. aller inneren Ausnahmen und den Stack-Trace) an eine Support-E-Mail-Adresse zu mailen (siehe Rezept 197). Um genauere Informationen über die Ausnahme zu erhalten sollten Sie dem Release der Anwendung die automatisch erstellte Debug-Informationsdatei (mit der Endung .pdb) mitliefern. So erhalten Sie im Stack-Trace zusätzliche Informationen über die Quelle des Fehlers (inklusive der Zeilennummer). Diese Informationen erleichtern das Debuggen von Fehlern, die lediglich beim Anwender auftreten.

077 Konfigurationsdaten in der .config-Datei verwalten

Zum Schreiben und Lesen der Programmeinstellungen müssen Sie keine Instanz der Settings-Klasse erzeugen, sondern können Sie einfach die Default-Instanz verwenden, die Sie über die statische Default-Eigenschaft erreichen:

// Hintergrundfarbe des Formulars einlesen
this.BackColor = Properties.Settings.Default.BackColor;

// Position und Größe des Formulars auslesen
this.Left = Properties.Settings.Default.StartFormLeft;
this.Top = Properties.Settings.Default.StartFormTop;
this.Width = Properties.Settings.Default.StartFormWidth;
this.Height = Properties.Settings.Default.StartFormHeight;

Listing 0.5: Lesen von Programmeinstellungen

// Position und Größe des Formulars in der Konfiguration ablegen
Properties.Settings.Default.StartFormLeft = this.Left;
Properties.Settings.Default.StartFormTop = this.Top;
Properties.Settings.Default.StartFormWidth = this.Width;
Properties.Settings.Default.StartFormHeight = this.Height;

// Konfiguration speichern
try
{
   Properties.Settings.Default.Save();
}
catch (Exception ex)
{
   MessageBox.Show("Fehler beim Speichern der Konfiguration: " + ex.Message, 
      Application.ProductName, MessageBoxButtons.OK, MessageBoxIcon.Error);
}

Listing 0.6: Schreiben von Programmeinstellungen

092 Konsolenanwendungen starten und die Ausgabe auswerten

Dieses Rezept habe ich um die Auswertung des Rückgabecodes und des Fehlerkanals der Anwendung erweitert. Hier ist der neue Text:

Auch im Windows-Zeitalter gibt es noch eine Menge mehr oder weniger hilfreiche Konsolenanwendungen. Das für C#-Entwickler beste Beispiel ist der C#-Compiler csc.exe. Der Aufruf solcher Anwendungen in einem Programm ist kein Problem und wurde bereits in den vorherigen Rezepten besprochen. In einigen Fällen werden Sie jedoch die Ausgabe der aufgerufenen Konsolenanwendung auswerten wollen. Und das beschreibe ich in diesem Rezept.

Zunächst benötigen Sie ein ProcessStartInfo-Objekt (aus dem Namensraum System.Diagnostics), das Sie im Konstruktor mit dem Dateinamen der zu startenden Anwendung initialisieren. Das Beispiel in Listing 0.7 erzeugt ein solches Objekt für das Windows-Zubehör-Programm ipconfig (das die aktuelle IP-Konfiguration an der Konsole ausgibt). Wenn Sie Argumente übergeben wollen (oder müssen), schreiben Sie diese in die Eigenschaft Arguments.

Eine Konsolenanwendung schreibt normale Informationen in den Standard-Ausgabe- und Fehlerinformationen (normalerweise) in den Standard-Fehler-Kanal. Diese Kanäle geben ihre Informationen per Voreinstellung an der Konsole aus. Da Sie die Ausgabe des Programms umleiten wollen, setzen Sie die Eigenschaften RedirectStandardOutput und RedirectStandardError auf true. UseShellExecute müssen Sie in diesem Fall auf false setzen, da die Umleitung ansonsten nicht möglich ist. UseShellExecute legt fest, ob zum Starten des Prozesses die Betriebssystemshell verwendet wird. Ist diese Eigenschaft false, wird der Prozess direkt über die ausführbare Datei gestartet.

Dann starten Sie den Prozess über die Start-Methode der Process-Klasse, der Sie die ProcessStartInfo-Instanz übergeben. Über die WaitForExit-Methode warten Sie auf das Ende des Prozesses. Hier sollten Sie am ersten Argument einen Timeout in Millisekunden übergeben, da es vorkommen kann, dass die Ausführung der Konsolenanwendung blockiert wird (z. B. weil diese den Anwender fragt, ob eine bestimmte Aktion wirklich ausgeführt werden soll).

Die Start-Methode gibt eine Referenz auf das Process-Objekt zurück, das den gestarteten Prozess repräsentiert. Sie sollten diese Referenz in eine Variable schreiben.

Um auszuwerten, ob der Prozess innerhalb des Timeout beendet wurde, fragen Sie die HasExited-Eigenschaft des Process-Objekts ab. Ist diese true, sollten Sie zunächst ermitteln, ob ein Fehler aufgetreten ist. Viele Konsolenanwendungen geben dazu einen Integerwert zurück, den Sie über die Eigenschaft ExitCode erreichen. Der Wert 0 bedeutet normalerweise, dass kein Fehler aufgetreten ist. Verlassen können Sie sich darauf aber nicht, eine Anwendung ist nicht gezwungen, den Rückgabewert zu setzen. Sie müssen die Rückgabe des Programms im Einzelfall also immer prüfen.

ber die Eigenschaft StandardError können Sie den Fehlerkanal auslesen. Die eigentliche Ausgabe des Programms erreichen Sie über StandardOutput. Diese Eigenschaften referenzieren je einen StreamReader.

Das folgende Listing zeigt am Beispiel von ipconfig, wie Sie eine »normale« Konsolenanwendung starten und deren Ergebnis auswerten. Das Beispiel geht davon aus, dass die Anwendung bei einem Fehler einen Rückgabewert ungleich 0 zurückgibt und die Fehlermeldung in den Fehlerkanal schreibt.

Zum Kompilieren des Beispiels müssen Sie den Namensraum System.Diagnostics importieren.

// ProcessStartInfo-Objekt erzeugen und initialisieren
ProcessStartInfo psi = new ProcessStartInfo("ipconfig");
psi.Arguments = "/all";
psi.UseShellExecute = false;
psi.RedirectStandardOutput = true;
psi.RedirectStandardError = true;

// Prozess starten und auf dessen Ende warten
Process process = Process.Start(psi);
int timeout = 1000;
process.WaitForExit(timeout);

// Das Ergebnis auswerten
string errors = null;
string result = null;
if (process.HasExited)
{
   // Der Prozess wurde innerhalb des Timeout beendet:
   // Fehler auslesen
   if (process.ExitCode != 0)
   {
      errors = "Der Prozess wurde mit dem Code " +
         process.ExitCode + " beendet.";
   }
   string standardError = process.StandardError.ReadToEnd();
   if (string.IsNullOrEmpty(standardError) == false)
   {
      if (errors != null)
      {
         errors += Environment.NewLine;
      }
      errors += standardError;
   }
   
   // Das Ergebnis auslesen
   result = process.StandardOutput.ReadToEnd();
}
else
{
   // Der Prozess wurde innerhalb des Timeout nicht beendet
   errors = "Der Prozess konnte innerhalb des Timeout von " + 
      timeout + " Millisekunden nicht beendet werden";
}

// Das Ergebnis auswerten
if (errors == null)
{
   // Kein Fehler aufgetreten
   ...
}
else
{
   // Fehler aufgetreten
   ...
}

Listing 0.7: Starten einer Konsolenanwendung (ipconfig) und Auswerten der Ausgabe dieses Programms

Dateisystem

123 Programmdateien in den Systempfaden suchen

Die Methode FindFileInSystemPaths berücksichtigte nicht die Tatsache, dass Pfadangaben im Pfad auch in Anführungszeichen eingebettet sein können. Dieser Fall führte zu einer Exception »Illegales Zeichen im Pfad«. Die korrigierte Version berücksichtigt diesen Umstand:

/* Deklaration der API-Funktion GetWindowsDirectory */
[DllImport("kernel32.dll", SetLastError = true)]
private static extern uint GetWindowsDirectory(StringBuilder lpBuffer,
   uint uSize);

/* Sucht eine Datei in den Systempfaden */
public static string FindFileInSystemPaths(string fileName)
{
   string path = null;

   // Im Windows-Systemordner suchen
   path = Path.Combine(Environment.SystemDirectory, fileName);
   if (File.Exists(path))
   {
      return path;
   }

   // Im Windows-Ordner suchen
   const int MAX_PATH = 160;
   string windowsDirectoryName = null;
   StringBuilder buffer = new StringBuilder(MAX_PATH + 1);
   if (GetWindowsDirectory(buffer, 260) > 0)
   {
      windowsDirectoryName = buffer.ToString();
      path = Path.Combine(windowsDirectoryName, fileName);
      if (File.Exists(path))
      {
         return path;
      }
   }

   // In den Ordnern suchen, die in der Umgebungsvariablen Path eingestellt
   // sind
   string[] systemPaths = Environment.GetEnvironmentVariable(
      "path").Split(new char[] { ';' });
   for (int i = 0; i < systemPaths.Length; i++)
   {
      path = Path.Combine(systemPaths[i].Trim().Trim('\"'), fileName);
      if (File.Exists(path))
      {
         return path;
      }
   }

   return null;
}

Listing 0.1: Die korrigierte Methode zum Suchen einer Datei in den Systempfaden

Vielen Dank an Mirek Virius von der Czech Technical University, Prague, der mir diesen Fehler meldete.

Text-, binäre und ZIP-Dateien

154 Dateien in ZIP-Archive komprimieren

Die im Codebook beschriebene Methode ZipFiles ist leider ein klein wenig buggy: Wenn Sie am Argument comment null übergeben, resultiert dies in einer Exception beim Setzen des Kommentars. Deswegen überprüft die verbesserte Version, ob der Kommentar nicht null ist:

public static void ZipFiles(string[] sourceFileNames, string zipFileName, 
   int blockSize, int zipLevel, bool includePaths, string comment)
{
   // Datei-Stream als Basis-Stream erzeugen
   Stream zipFileStream = File.Open(zipFileName, FileMode.CreateNew);

   // ZipOutputStream zum Schreiben der Zip-Datei erzeugen
   ZipOutputStream zipOutputStream = new ZipOutputStream(zipFileStream);

   // Kompressionsrate definieren (0 bis 9)
   zipOutputStream.SetLevel(zipLevel);

   // Kommentar zum Archiv definieren
   if (comment != null)
   {
      zipOutputStream.SetComment(comment);
   }

   // Alle im Array sourceFileNames übergebenen Dateien 
   // durchgehen und in das Archiv schreiben
   for (int i = 0; i < sourceFileNames.Length; i++)
   {
      // ZipEntry-Objekt für die neue Datei erzeugen. Der im Konstruktor 
      // übergebene Name wird als Dateiname beim Extrahieren verwendet. 
      // Sie können hier auch (relative) Pfadangaben mit angeben. 
      // Das Programm speichert den Pfad (ohne Laufwerkangabe) mit, 
      // wenn das Argument includePaths true ist         
      string fileNameForZip;
      FileInfo fi = new FileInfo(sourceFileNames[i]);
      if (includePaths) 
      {
         // Dateiname ohne Laufwerkbuchstabe ermitteln
         int pos = fi.FullName.IndexOf('\\');
         if (pos > -1)
         {
            fileNameForZip = fi.FullName.Substring(pos,
               fi.FullName.Length - pos);
         }
         else
         {
            fileNameForZip = fi.FullName;
         }
      }
      else
      {
         // Nur den Dateinamen speichern
         fileNameForZip = fi.Name;
      }
      ZipEntry zipEntry = new ZipEntry(fileNameForZip);

      // ZipEntry-Objekt dem ZipOutputStream hinzufügen
      zipOutputStream.PutNextEntry(zipEntry);

      // Zu archivierende Datei in einem FileStream öffnen
      FileStream fileStream = new FileStream(sourceFileNames[i],
         FileMode.Open, FileAccess.Read);

      // Quellstream blockweise in ein Byte-Array lesen und in den
      // ZipOutputStream schreiben
      byte[] buffer = new byte[blockSize];
      int bytesRead = 0;
      while ((bytesRead = fileStream.Read(buffer, 0, buffer.Length)) > 0)
      {
         zipOutputStream.Write(buffer, 0, bytesRead);
      }

      // FileStream schließen
      fileStream.Close();
   }

   // ZipOutputStream abschließen und schließen
   zipOutputStream.Finish();
   zipOutputStream.Close();
}

Listing 0.1: Methode zum Erzeugen eines Zip-Archivs über #ziplib

155 (ZIP-)Archive aus einem Ordner erzeugen

Die in diesem Rezept beschriebene Methode ZipFolder habe ich um das params-Argument extensionsToInclude erweitert. In diesem Argument können Sie nun die Endungen der Dateien angeben, die in das Archiv kopiert werden sollen. Ist keine Endung angegeben, werden grundsätzlich alle Dateien übernommen. So können Sie zum Beispiel ausschließlich alle Text- und HTML-Dateien eines Ordners in ein Archiv packen, indem Sie am letzten Argument ".txt" angeben. Beachten Sie, dass Sie bei der Endung den Punkt mit angeben müssen.

Daneben habe ich noch einen kleinen Bug beseitigt: Wenn am Argument comment null übergeben wurde warf die SetComment-Methode des ZipOutputStream-Objekts eine Exception. Deshalb überprüft die Methode nun vor dem Setzen des Kommentars, ob dieser nicht null ist.

/* Archiviert einen Ordner in eine ZIP-Datei */
public static void ZipFolder(string folderName, string zipFileName,
   int blockSize, int zipLevel, string comment, params string[] extensionsToInclude)
{
   // Datei-Stream als Basis-Stream erzeugen
   Stream zipFileStream = File.Open(zipFileName, FileMode.CreateNew);

   // ZipOutputStream zum Schreiben der Zip-Datei erzeugen
   ZipOutputStream zipOutputStream = new ZipOutputStream(zipFileStream);

   // Kompressionsrate definieren (0 bis 9)
   zipOutputStream.SetLevel(zipLevel);

   // Kommentar zum Archiv definieren
   if (comment != null)
   {
      zipOutputStream.SetComment(comment);
   }

   // Ordner rekursiv archivieren
   AddFilesFromFolder(folderName, folderName, zipOutputStream, 
      blockSize, extensionsToInclude);

   // ZipOutputStream abschließen und schließen
   zipOutputStream.Finish();
   zipOutputStream.Close();
}

/* Fügt alle Dateien eines Ordners (rekursiv) einem ZIP-Archiv hinzu */
private static void AddFilesFromFolder(string baseFolderName,
   string folderName, ZipOutputStream zipOutputStream,
   int blockSize, params string[] extensionsToInclude)
{
   // Alle Dateien des Ordners durchgehen und in das Archiv schreiben
   DirectoryInfo folder = new DirectoryInfo(folderName);
   FileInfo[] files = folder.GetFiles();
   for (int i = 0; i < files.Length; i++)
   {
      // Überprüfen, ob Erweiterungen für einzuschließende Dateien 
      // angegeben sind
      bool includeFile = true;
      if (extensionsToInclude != null && extensionsToInclude.Length > 0)
      {
         includeFile = false;
         foreach (string extensionToInclude in extensionsToInclude)
         {
            if (files[i].Extension.ToLower() == extensionToInclude.ToLower())
            {
               includeFile = true;
               break;
            }
         }
      }

      if (includeFile == false)
      {
         continue;
      }

      // Relativen Pfad des aktuellen Ordners über das Entfernen des
      // Basisordnernamens ermitteln
      string relativePath = folderName.Replace(baseFolderName, "");
      if (relativePath != null)
      {
         if (relativePath.StartsWith("\\"))
            relativePath = relativePath.Remove(0, 1);
         if (relativePath.EndsWith("\\") == false)
            relativePath += "\\";
      }

      // ZipEntry-Objekt für die neue Datei mit dem relativen Pfad
      // erzeugen und dem ZipOutputStream hinzufügen
      zipOutputStream.PutNextEntry(
         new ICSharpCode.SharpZipLib.Zip.ZipEntry(
         relativePath + files[i].Name));

      // Zu archivierende Datei in einem FileStream öffnen und über ein
      // Byte-Array blockweise in den ZipOutputStream schreiben
      FileStream fileStream = files[i].OpenRead();
      byte[] buffer = new byte[blockSize];
      int bytesWritten = 0;
      do
      {
         int size = fileStream.Read(buffer, 0, buffer.Length);
         zipOutputStream.Write(buffer, 0, size);
         bytesWritten += size;

      } while (bytesWritten < fileStream.Length);

      // FileStream schließen
      fileStream.Close();
   }

   // Alle Unterordner durchgehen und die Methode rekursiv aufrufen
   DirectoryInfo[] subFolders = folder.GetDirectories();
   for (int i = 0; i < subFolders.Length; i++)
   {
      AddFilesFromFolder(baseFolderName, subFolders[i].FullName,
         zipOutputStream, blockSize);
   }
}

Listing 0.2: Methode zum Archivieren von Dateien aus einem Ordner in eine ZIP-Datei

157 (ZIP-)Archive entpacken

Die in diesem Rezept beschriebene Methode wies zwei Bugs auf: In einigen Fällen speichert ein ZIP-Archiv für die einzelnen Dateien den Unterordner \. In diesem Fall warf die in ExtractToFolder verwendete Path.Combine-Methode eine Exception. Außerdem berücksichtigte ExtractToFolder nicht die Tatsache, dass Dateien in ZIP-Archiven auch eine Größe von null Byte aufweisen können. Das Entpacken einer solchen Datei führte zu einer Exception. Die korrigierte Version finden Sie hier:

/* Merker für das Überschreiben aller Dateien */
private static bool overwriteAllFiles;
private static bool alreadyAskedForOverwriteAllFiles;

/* Extrahiert die Dateien eines ZIP-Archivs in einen Ordner */
public static void ExtractToFolder(string zipFileName, string folderName,
   int blockSize, bool overwriteWithoutWarning)
{
   // Eigenschaften voreinstellen
   overwriteAllFiles = false;
   alreadyAskedForOverwriteAllFiles = false;

   // ZipInputStream für die Zip-Datei erzeugen 
   ZipInputStream zipInputStream = new ZipInputStream(
      File.Open(zipFileName, FileMode.Open, FileAccess.Read));

   // Alle im Archiv gespeicherten ZipEntry-Objekte durchgehen
   ZipEntry zipEntry;
   while ((zipEntry = zipInputStream.GetNextEntry()) != null)
   {
      // Aus dem (relativen) Dateinamen den Unterordner und den
      // Namen extrahieren
      string subFolderName = Path.GetDirectoryName(zipEntry.Name);

      // Für den Fall, dass die Zip-Einträge mit einem \ beginnen,
      // den Unterordner auf null setzen
      if (subFolderName == "\\")
      {
         subFolderName = null;
      }

      // Den Dateinamen des Eintrags ermitteln
      string entryName = Path.GetFileName(zipEntry.Name);

      // Den vollen Ordnernamen des Zielordners ermitteln
      string destFolderName;
      if (subFolderName != null)
      {
         destFolderName = Path.Combine(folderName, subFolderName);
      }
      else
      {
         destFolderName = folderName;
      }

      // Unterordner erzeugen, falls notwendig
      if (subFolderName != null)
      {
         Directory.CreateDirectory(destFolderName);
      }

      if (zipEntry.IsDirectory == false &&
         entryName != null && entryName.Length > 0)
      {
         bool overwriteFile = true;

         if (overwriteWithoutWarning == false &&
            overwriteAllFiles == false)
         {
            // Wenn Dateien nicht ohne Warnung überschrieben werden
            // sollen: Überprüfen, ob bereits eine gleichnamige Datei
            // existiert
            if (File.Exists(Path.Combine(destFolderName, entryName)))
            {
               // Nachfragen, ob die Datei überschrieben werden soll
               switch (MessageBox.Show("Die Datei '" + entryName +
                  "' existiert bereits im Ordner '" + destFolderName +
                  "'\r\n\r\nSoll diese Datei überschrieben werden?",
                  Application.ProductName, MessageBoxButtons.YesNoCancel,
                  MessageBoxIcon.Question))
               {
                  case DialogResult.Yes:
                     overwriteFile = true;
                     break;
                  
                  case DialogResult.No:
                     overwriteFile = false;
                     break;
                  
                  case DialogResult.Cancel:
                     // Stream schließen und beenden
                     zipInputStream.Close();
                     return;
               }

               // Nachfragen, ob alle Dateien überschrieben werden
               // sollen, sofern dies noch nicht geschehen ist
               if (overwriteFile == true &&
                  alreadyAskedForOverwriteAllFiles == false)
               {
                  switch (MessageBox.Show("Sollen alle vorhandenen " +
                     "Dateien automatisch überschrieben werden?",
                     Application.ProductName, MessageBoxButtons.YesNoCancel,
                     MessageBoxIcon.Question))
                  {
                     case DialogResult.Yes:
                        overwriteAllFiles = true;
                        break;

                     case DialogResult.No:
                        overwriteAllFiles = false;
                        break;

                     case DialogResult.Cancel:
                        // Stream schließen und beenden
                        zipInputStream.Close();
                        return;
                  }
                  // Definieren, dass nicht noch einmal gefragt wird
                  alreadyAskedForOverwriteAllFiles = true;
               }
            }
         }

         if (overwriteFile)
         {
            // FileStream für die Datei erzeugen
            FileStream fileStream =
               File.Create(Path.Combine(destFolderName, entryName));

            if (zipEntry.CompressedSize > 0)
            {
               // Datei in Blöcken von maximal 1 MB in den Stream schreiben
               // um den Speicher nicht mit großen Dateien zu überlasten
               int size;
               byte[] buffer = new byte[1048576];
               do
               {
                  // Den nächsten Datenblock aus dem ZipInputStream lesen
                  size = zipInputStream.Read(buffer, 0, buffer.Length);
                  if (size > 0)
                  {
                     // Wenn Daten gelesen wurden, diese in die Datei
                     // schreiben
                     fileStream.Write(buffer, 0, size);
                  }
               } while (size > 0);
            }

            // FileStream schließen
            fileStream.Close();
         }
      }
   }

   // ZipInputStream schließen
   zipInputStream.Close();
}

Listing 0.3: Methode zum Entpacken eines ZIP-Archivs in einen Ordner

Internet

197 E-Mails über einen SMTP-Server versenden

Dieses Rezept habe ich um das Anfordern einer Übertragungs- und einer Lesebestätigung erweitert und zusätzlich die Konfiguration von SMTP in der app.config erläutert. Zu dem einfachen Beispiel habe ich außerdem ein Beispiel mit einem Formular hinzugefügt, das Sie in Ihren Projekten als Basis für ein Mailversand-Formular verwenden können.

Hier ist der geänderte Text:

Zum Senden einer E-Mail erzeugen Sie zunächst eine Instanz der SmtpClient-Klasse. Dem Konstruktor können Sie die Adresse des SMTP-Servers übergeben (sofern Sie die Konfiguration nicht in der Anwendungs-Konfigurationsdatei vornehmen und nicht die Pickup-Ausliefer-Methode über den IIS verwenden wollen, die ich weiter unten beschreibe):

// SmtpClient erzeugen
string smtpHost = "localhost";
SmtpClient smtpClient = new SmtpClient(smtpHost);

Wenn Sie den lokalen SMTP-Server angeben wollen (der ein Teil der Windows-Internet-Informationsdienste ist), geben Sie localhost an.

Da viele SMTP-Server eine Authentifizierung verlangen, können Sie die dazu notwendigen Informationen über die Eigenschaft Credentials übergeben. Dazu schreiben Sie die Referenz auf ein neues NetworkCredential-Objekt in dieser Eigenschaft. Im Konstruktor übergeben Sie diesem Objekt am ersten Argument den Benutzernamen und am zweiten das Passwort:

// Authentifizierungs-Informationen für SMTP-Server,
// die eine Authentifizierung verlangen
string userName = " jb ";
string password = "galaxy";
smtpClient.Credentials = new NetworkCredential(userName, password);

// Authentifizierungs-Informationen für den SMTP-Server von 
// Windows XP, wenn dieser so eingestellt ist, dass die 
// Clients sich über die Windows-Authentifizierung anmelden müssen
//smtpClient.Credentials = CredentialCache.DefaultNetworkCredentials;

Die Klasse SmtpClient ermöglicht verschiedene Ausliefer-Methoden, die Sie über die Eigenschaft DeliveryMethod einstellen können. Die Voreinstellung SmtpDeliveryMethod.Network sendet die E-Mail an den SMTP-Server an der über die Host-Eigenschaft des SmtpClient-Objekts angegebenen Adresse. Über SmtpDeliveryMethod.PickupDirectoryFromIis können Sie festlegen, dass die E-Mail im Pickup-Verzeichnis des IIS (per Voreinstellung C:\Inetpub\mailroot\Pickup) abgelegt wird. SmtpDeliveryMethod.SpecifiedPickupDirectory erlaubt die Eingabe eines eigenen Pickup-Verzeichnisses über die PickupDirectoryLocation-Eigenschaft des SmtpClient-Objekts. Bei einer Pickup-Auslieferung wird kein SMTP-Server verwendet. Hierbei kümmert sich der IIS um die Auslieferung der E-Mail.

// Ausliefer-Methode festlegen (Network ist Default)
smtpClient.DeliveryMethod = SmtpDeliveryMethod.Network;

Alternativ können Sie den SMTP-Server, die Authentifizierungsinformationen, die Absender-Adresse, die Ausliefer-Methode und weitere Grundeinstellungen auch in der Anwendungs-Konfigurationsdatei angeben:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.net>
    <mailSettings>
      <smtp from="jb@galaxy.com" deliveryMethod="Network">
        <network host="mail.galaxy.com" userName="jb" password="galaxy" />
      </smtp>
    </mailSettings>
  </system.net>
</configuration>

Listing 0.1: Einstellung der SMTP-Zugangsdaten in der Anwendungs-Konfigurationsdatei

In diesem Fall können Sie eine Instanz der SmtpClient-Klasse über den parameterlosen Konstruktor erzeugen und brauchen den SMTP-Server, die Authentifizierungs-Informationen, den Absender und die Ausliefer-Methode nicht anzugeben (was Sie aber trotzdem natürlich können).

ber die Methode Send können Sie E-Mails versenden. Der ersten Variante dieser Methode übergeben Sie vier Strings, wobei der erste den Sender, der zweite die Empfänger, der dritte den Betreff und der letzte den Text beinhaltet. Die zweite Send-Variante ist allerdings wesentlich flexibler. Dieser Variante übergeben Sie ein MailMessage-Objekt, über das Sie unter anderem auch Anhänge versenden können. Listing 0.3 zeigt, wie Sie ein solches Objekt erzeugen. Ich denke, die meisten Eigenschaften der MailMessage-Klasse erklären sich von selbst.

// Nachricht erzeugen und initialisieren
MailMessage message = new MailMessage();
message.From = new MailAddress("jb@galaxy.com", "Jürgen Bayer");
message.To.Add("zaphod@galaxy.com");
message.CC.Add("ford@galaxy.com");
message.Bcc.Add("trillian@galaxy.com");
message.Subject = "Party";
message.IsBodyHtml = true;
message.Body = "Hallo Zaphod,<br><br>" +
   "Lust auf eine Party im <i>Restaurant am Ende der Galaxis</i>?";

Listing 0.2: Erzeugen und Initialisieren eines MailMessage-Objekts

Im Beispiel setze ich die Eigenschaft IsBodyHtml des MailMessage-Objekts auf true und schreibe einen HTML-Text in den Body, um die E-Mail im HTML-Format zu versenden.

Um Sonderzeichen wie unsere Umlaute korrekt übertragen zu können, ist wichtig, dass Sie die Codierung des Body auf UTF-8 einstellen (UTF-8 ist eine Unicode-Codierung, die zum einen alle Zeichen des Unicode-Zeichensatzes beinhaltet und zum anderen von allen Routern und E-Mail-Servern im Internet unterstützt wird):

message.BodyEncoding = Encoding.UTF8;

Die Priorität der Nachricht können Sie über die Priority-Eigenschaft festlegen:

message.Priority = MailPriority.High;

Neben der Priorität sind in der Praxis das Anfordern einer Übertragungs- und das Anfordern einer Lesebestätigung wichtig.

bertragungsbestätigungen können erfolgen, wenn eine E-Mail in dem Mailserver eingegangen ist, der das Mailkonto des Empfängers verwaltet, wenn die E-Mail von keinem Mailserver entgegengenommen wird und/oder wenn der Empfang verzögert wird.

Eine Lesebestätigung wird vom E-Mail-Client gesendet, wenn dieser die E-Mail vom Mail-Server abgerufen und der Anwender die empfangene E-Mail geöffnet hat. Viele E-Mail-Clients senden angeforderte Lesebestätigungen automatisch, bei anderen (wie z. B. bei Outlook) kann der Anwender einstellen, ob Lesebestätigungen überhaupt, nur nach einer entsprechenden Nachfrage oder automatisch gesendet werden. In einigen Netzwerken wird das Senden von Lesebestätigungen auch verhindert. Sie können also nicht sicher sein, dass Sie eine Lesebestätigung auch erhalten.

Übertragungsbestätigungen werden per Voreinstellung beim Senden von E-Mails nicht angefordert. Sie müssen diese gegebenenfalls über die Eigenschaft DeliveryNotificationOptions des MailMessage-Objekts einstellen, indem Sie diese auf eine Kombination der Werte der DeliveryNotificationOptions-Aufzählung setzen. Das folgende Beispiel fordert eine Bestätigung für die erfolgreiche, die nicht erfolgreiche und die verzögerte Auslieferung der E-Mail an:

message.DeliveryNotificationOptions = DeliveryNotificationOptions.OnSuccess |
   DeliveryNotificationOptions.OnFailure | DeliveryNotificationOptions.Delay;

Das Anfordern einer Lesebestätigung ist leider nicht in die MailMessage-Klasse als Eigenschaft integriert. Um eine Bestätigung für das Lesen einer E-Mail einzuholen, müssen Sie den Nachrichten-Kopfeinträgen einen Header hinzufügen. Outlook verwendet dazu den Header »Return-Receipt-To«. Der Wert des Header-Eintrags ist die E-Mail-Adresse, die die Bestätigung empfangen soll (also in der Regel die Sender-Adresse). Leider schreibt die SmtpClient-Klasse die Header-Namen klein in die SMTP-Nachricht. Outlook zumindest scheint mit dem kleingeschriebenen »return-receipt-to«-Header aber Probleme zu haben und erkennt nicht, dass eine Lesebestätigung angefordert wurde. Als Alternative können Sie den Header »Disposition-Notification-To« verwenden, den Outlook auch kleingeschrieben versteht. Beide Header sind kein Teil des SMTP-Standards (RFC 822, siehe www.ietf.org/rfc/rfc0822.txt). Zur Sicherheit füge ich beide Header an (und hoffe, dass die gängigen E-Mail-Clients damit keine Probleme haben):

message.Headers.Add("Return-Receipt-To", message.From.Address)
message.Headers.Add("Disposition-Notification-To", message.From.Address);

ber die Add-Methode der Attachments-Eigenschaft können Sie der Nachricht Dateien anhängen. Dazu übergeben Sie eine neue Instanz der MailAttachment-Klasse, der Sie im Konstruktor den Dateinamen übergeben.

string fileName = Path.Combine(
   Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), 
   "dontpanic.gif");
message.Attachments.Add(new Attachment(fileName));

Schließlich können Sie die E-Mail über die Send-Methode der SmtpClient-Instanz versenden. Da beim Senden von E-Mails auch Exceptions vorkommen, deren aussagekräftige Meldungen sich in einer der inneren Exceptions verstecken, habe ich dem Beispiel die Methode GetExceptionMessages aus dem Rezept 44 hinzugefügt, die auch die Nachrichten der inneren Exceptions ermittelt. Diese Methode habe ich allerdings nicht in das Listing 0.3 aufgenommen.

Das Beispiel erfordert den Import der Namensräume System, System.IO, System.Net, System.Net.Mail, System.Windows.Forms, System.Text, System.Text.RegularExpressions und System.Reflection.

try
{
   smtpClient.Send(message);
   MessageBox.Show("E-Mail erfolgreich versendet", Application.ProductName, 
      MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (SmtpFailedRecipientException ex)
{
   if (ex.Message.IndexOf("Unable to relay for") > -1)
   {
      this.Cursor = Cursors.Default;
      MessageBox.Show("Der SMTP-Server '" + smtpClient.Host +
        "' kann die E-Mail an die angegebene Adresse nicht " +
        "weiterleiten. Wahrscheinlich ist die Weiterleitung " +
        "von E-Mails für die IP-Adresse des Client in der " +
        "Konfiguration des SMTP-Servers untersagt",
        Application.ProductName, MessageBoxButtons.OK,
        MessageBoxIcon.Exclamation);
   }
   else
   {
      this.Cursor = Cursors.Default;
      MessageBox.Show(this.GetExceptionMessages(ex), Application.ProductName,
         MessageBoxButtons.OK, MessageBoxIcon.Error);
   }
}
catch (Exception ex)
{
   this.Cursor = Cursors.Default;
   MessageBox.Show(this.GetExceptionMessages(ex), Application.ProductName,
      MessageBoxButtons.OK, MessageBoxIcon.Error);
}

Listing 0.3: Senden einer Mail über die Klasse SmtpClient

Im Beispiel zu diesem Rezept finden Sie neben dem einfachen Programmcode auch ein Formular zum Senden einer E-Mail. Dieses Formular können Sie natürlich gerne kopieren und in Ihren Anwendungen einsetzen.

198 E-Mails über MAPI bzw. Outlook versenden

Wie ich in diesem Rezept beschreibe, meldet Outlook jeden Versuch, von außen (oder über ein Add-In) auf das E-Mail-System zuzugreifen, fragt, ob der Zugriff erlaubt werden soll, und bietet dem Anwender erst nach fünf Sekunden den Ja-Schalter an. Eine in dem Rezept genannte Lösung für dieses Problem ist Express Click Yes (www.contextmagic.com/express-clickyes). Diese Lösung ist leider nicht optimal, da Express Click Yes den Ja-Schalter erst dann betätigt, nachdem dieser aktiviert wurde.

Eine andere Lösung, die ich vor kurzem im Internet gefunden habe, ist Advanced Security for Outlook (www.mapilab.com/outlook/security). Dieses Outlook-Add-In ersetzt den Outlook-Dialog, der beim Zugriff von außen angezeigt wird, durch einen eigenen. In diesem Dialog kann der Benutzer den Zugriff erlauben oder sperren. Die Besonderheit ist, dass über eine Checkbox auch festgelegt werden kann, dass diese Einstellungen für jeden Zugriff durch das spezifische Programm verwendet wird. Wenn Sie den Zugriff für ein Programm erlauben und die Checkbox einschalten, kann das Programm danach problemlos E-Mails versenden, ohne dass ein weiterer Dialog erscheint.

Der einzige Haken an diesem Advanced Security for Outlook ist, dass es für externe Programme, die E-Mails versenden, nur dann funktioniert, wenn diese das Outlook-Objektmodel verwenden. MAPI und CDO werden leider nur dann unterstützt, wenn der Zugriff auf Outlook-E-Mails von innen (also über ein Outlook-Add-In oder ein Makro) erfolgt.

Ansonsten ist die Verwendung einfach: Installieren Sie Advanced Security for Outlook, beenden Sie Outlook falls es ausgeführt wird und starten Sie Outlook wieder neu. Zumindest beim ersten Start meldet das Add-In, dass es installiert wurde. Wenn danach ein Programm über das Outlook-Objektmodell eine E-Mail versenden will, erscheint der Advanced Security for Outlook-Dialog (Abbildung 0.1).

Abbildung 0.1: Der Advanced Security for Outlook-Dialog

Da Sie, um dieses Tool nutzen zu können, E-Mails über das Outlook-Objektmodel senden müssen, folgt abschließend noch ein Beispiel. Dieses Beispiel erfordert, dass Sie dem Projekt eine Referenz auf die COM-Komponente »Microsoft Outlook 11 Object Model« bzw. auf die Outlook-Interop-Assembly hinzufügen.

// Outlook-Instanz erzeugen
Microsoft.Office.Interop.Outlook.Application ol =
   new Microsoft.Office.Interop.Outlook.ApplicationClass();

// Mail erzeugen
Microsoft.Office.Interop.Outlook.MailItem mailItem =
   (Microsoft.Office.Interop.Outlook.MailItem)
   ol.CreateItem(Microsoft.Office.Interop.Outlook.OlItemType.olMailItem);

// Mail füllen
mailItem.To = "zaphod@galaxy.com";
// mailItem.CC = ...;
// mailItem.BCC = ...;
mailItem.Subject = "Test";
mailItem.Body = "Ist nur'n Test";

// Datei anfügen
string fileName = Path.Combine(Path.GetDirectoryName(
   Assembly.GetEntryAssembly().Location), "dontpanic.gif");
mailItem.Attachments.Add(fileName, Missing.Value, Missing.Value,
   Missing.Value);

// Mail senden
try
{
   mailItem.Send();
   Console.WriteLine("Mail erfolgreich versendet");
}
catch (Exception ex)
{
   Console.WriteLine("Fehler beim Senden der Mail: " + ex.Message);
}

Listing 0.4: Versenden einer E-Mail über das Outlook-Objektmodell

Formulare und Steuerelemente

227: Bei der Betätigung der Return-Taste die Tab-Taste simulieren

In diesem Rezept ist mir ein kleiner Fehler unterlaufen, der dazu führte, dass das Programm eine Exception warf, wenn das Steuerelement, bei dem die Return-Taste in die Betätigung der Tab-Taste umgewandelt wurde, auf einem Container-Steuerelement (z. B. einem Panel) lag. In OnKeyPress habe ich nicht das ermittelte Container-Steuerelement, sondern den Parent des Steuerelements verwendet, um dessen ProcessTabKey-Methode aufzurufen. Hier ist die korrigierte Version:

public class Return2TabTextBox: TextBox
{
   private bool simulateTabOnReturn = true;
   /* Gibt an, ob bei der Betätigung der Return-Taste 
     die Tab-Taste simuliert wird */
   [DefaultValue(true)]
   [Category("Behavior")]
   public bool SimulateTabOnReturn
   {
      get { return this.simulateTabOnReturn; }
      set { this.simulateTabOnReturn = value; }
   }

   /* OnKeyPress wird überschrieben, um bei der Betätigung der */
   /* Return-Taste die TAB-Taste zu simulieren */
   protected override void OnKeyPress(KeyPressEventArgs e)
   {
      if (this.simulateTabOnReturn && e.KeyChar == '\r')
      {
         // Return wurde betätigt und soll in Tab umgewandelt werden
         ContainerControl container = this.getContainerControl(this);
         if (container != null)
         {
            // Die leider geschützte ProcessTabKey-Methode
            // des ContainerControls über Reflection aufrufen
            Type type = container.GetType();
            type.InvokeMember("ProcessTabKey",
               System.Reflection.BindingFlags.InvokeMethod |
               System.Reflection.BindingFlags.NonPublic |
               System.Reflection.BindingFlags.Instance, null,
               container, new object[] { true });

            // Das Ereignis als behandelt kennzeichnen
            e.Handled = true;
         }
      }

      // Die geerbte Methode aufrufen
      base.OnKeyPress(e);
   }

   /* Liefert das ContainerControl eines Steuerelements */
   private ContainerControl getContainerControl(Control control)
   {
      if (control.Parent != null)
      {
         if (control.Parent is ContainerControl)
         {
            return (ContainerControl)control.Parent;
         }
         else
         {
            // Rekursiv aufrufen um den Parent des
            // übergebenen Steuerelements zu überprüfen
            return this.getContainerControl(control.Parent);
         }
      }
      else
      {
         return null;
      }
   }
}

Listing 0.1: Die korrigierte Return2TabTextBox

229 Die angezeigten Zeilen einer MultiLine-TextBox auslesen

In diesem Rezept wurden angezeigte Zeilen nicht korrekt ausgelesen, wenn diese nur aus einem Zeichen bestanden. Diesen von Axel Seibel gefundenen Fehler habe ich korrigiert. Außerdem habe ich den weiteren Fehler mit einer von Axel Seibel gefundenen Lösung korrigiert, der dazu führte, dass Pluszeichen zu Problemen führten.

/* Deklaration der benötigten API-Funktion */
[DllImport("User32.Dll")]
private static extern int SendMessage(IntPtr hWnd, int msg,
     IntPtr wParam, IntPtr lParam);

/* Gibt die tatsächlich angezeigten Zeilen einer TextBox zurück */
public static ReadOnlyCollection<string> GetDisplayedTextBoxRows(
   TextBox textBox)
{
   // Ergebnis-Auflistung erzeugen
   List<string> result = new List<string>();

   // Konstanten für SendMessage
   const int EM_GETLINECOUNT = 0x00BA;
   const int EM_LINELENGTH = 0x00C1;
   const int EM_LINEINDEX = 0x00BB;
   const int EM_GETLINE = 0x00C4;

   // Anzahl der Zeilen ermitteln
   int lineCount = SendMessage(textBox.Handle, EM_GETLINECOUNT,
      IntPtr.Zero, IntPtr.Zero);

   for (uint lineIndex = 0; lineIndex < lineCount; lineIndex++)
   {
      // Die Position des ersten Zeichens der Zeile ermitteln
      int pos = (int)SendMessage(textBox.Handle, EM_LINEINDEX,
         (IntPtr)lineIndex, IntPtr.Zero);

      // Die Länge der Zeile ab dieser Position ermitteln
      int lineLength = (int)SendMessage(textBox.Handle, EM_LINELENGTH,
         (IntPtr)pos, IntPtr.Zero);

      if (lineLength > 0)
      {
         // Wenn die Zeile Zeichen enthält: 
         // Platz für das abschließende 0-Zeichen berücksichtigen
         lineLength++;

         IntPtr apiBuffer = IntPtr.Zero;
         try
         {
            // Speicher im nicht verwalteten Bereich 
            // mit der Länge der Zeile reservieren 
            apiBuffer = Marshal.AllocHGlobal(lineLength);

            // Die Länge der Zeile muss laut der Dokumentation in die ersten
            // beiden Bytes des Puffers geschrieben werden, also:
            // über BitConverter.GetBytes die Länge in ein Byte-Array
            // lesen und dieses in die ersten zwei Bytes des Puffers
            // kopieren
            byte[] lengthInfo = BitConverter.GetBytes((short)lineLength);
            Marshal.Copy(lengthInfo, 0, apiBuffer, 2);

            // Die Zeile einlesen
            SendMessage(textBox.Handle, EM_GETLINE,
              (IntPtr)lineIndex, apiBuffer);

            // Den unverwalteten Puffer in ein verwaltetes 
            // Byte-Array kopieren
            byte[] clrBuffer = new byte[lineLength];
            Marshal.Copy(apiBuffer, clrBuffer, 0, lineLength);
            string row = String.Empty;
            // Hier wird nicht mer Encoding.xyz.GetString kopiert, weil
            // dabei verschiedene Probleme auftraten, wie z. B., dass
            // Umlaute nicht korrekt funktionierten oder das Pluszeichen
            // zu Problemen führte. Ein Kopieren der einzelnen Zeichen
            // funktioniert aber. Dank an Axel Seibel, der mir die Fehler
            // und die Lösung gemeldet hat.
            for (int i = 0; i < clrBuffer.Length; i++)
            {
               row += Convert.ToChar(clrBuffer[i]);
            }
            if (row.EndsWith("\0"))
            {
               row = row.Remove(row.Length - 1, 1);
            }
            result.Add(row);
         }
         finally
         {
            if (apiBuffer != IntPtr.Zero)
            {
               // Puffer-Speicher freigeben
               Marshal.FreeHGlobal(apiBuffer);
            }
         }
      }
      else
      {
         // Die Zeile war leer, also eine leere Zeile hinzufügen,
         // allerdings nur dann, wenn die TextBox nicht leer ist 
         // (also nur eine virtuelle Zeile enthält)
         if (lineCount != 1)
         {
            result.Add(String.Empty);
         }
      }
   }

   // Ergebnis zurückgeben
   return new ReadOnlyCollection<string>(result);
}

Listing 0.2: Methode zum Auslesen der angezeigten Zeilen einer TextBox

230 ComboBox mit Autovervollständigung

Wegen des Problems mit der Windows.Forms-ComboBox, dass diese die Einträge nach deren String-Darstellung sortiert, was dann Probleme macht, wenn die Liste als Quelle der Autovervollständigungsliste verwendet wird, habe ich die im ersten Codebook entwickelte AutoCompleteComboBox in einer weiterentwickelten Form wirder aufgenommen.

Hier sind die möglichen Probleme mit der Windows.Forms-ComboBox:

  • Wenn Sie als Quelle der Autovervollständigungs-Liste die ComboBox-Liste verwenden, kann es sein, dass bei einer Eingabe nicht der erste passende Eintrag der ComboBox-Liste vorgeschlagen wird. Dies ist dann der Fall, wenn die ComboBox-Liste eine andere Sortierung aufweist als eine Sortierung der dargestellten Strings ergeben würde. Die Autovervollständigungs-Liste besteht nämlich aus Strings und wird von der ComboBox vor der Anzeige sortiert. Speichert die ComboBox z. B. die Versionswerte 1.0.0.5, 1.0.0.100 und 1.0.0.101, schlägt die Autovervollständigung den Wert 1.0.0.100 vor wenn der Benutzer eine 1 eingibt. Eigentlich sollte aber der erste passende Wert der Liste, nämlich 1.0.0.5 (der ja als Version gesehen kleiner ist als 1.0.0.100) vorgeschlagen werden.
  • Ist die Eigenschaft AutoCompleteMode auf Suggest oder SuggestAppend eingestellt und DropDownStyle auf Simple, scheint die ComboBox einen Bug zu beinhalten. Wenn nach einer Eingabe die Liste automatisch aufklappt und Sie über die Cursor-Tasten einen der Einträge auswählen, können Sie Ihre Auswahl nicht mit der Return-Taste übernehmen. Der Eintrag im Textfeld der ComboBox wird in diesem Fall einfach gelöscht. Sie können Ihre Auswahl lediglich über die Tab-Taste übernehmen. Ich habe diesen Bug gemeldet. Möglicherweise wird dieses Problem im ersten Service Pack des Dotnet-Framework 3.5 gelöst.

Und hier ist meine AutoCompleteComboBox:

public class AutoCompleteComboBox : ComboBox
{
   /* Privates Feld zur Vermeidung rekursiver Aufrufe 
      des TextChanged-Ereignisses */
   private bool dontHandleTextChanged = false;

   /* Verwaltet den Index des aktuellen Treffers */
   private int currentMatchIndex = -1;

   /* Verwaltet den aktuell vom Benutzer eingegebenen Text */
   private string currentInput = null;

   /* OnTextChanged wird überschrieben, um bei einer Änderung des Textes 
      den ersten passenden Eintrag zu suchen und den Text zu erweitern */
   protected override void OnTextChanged(EventArgs e)
   {
      if (this.dontHandleTextChanged == false)
      {
         // Die aktuelle Eingabe merken
         this.currentInput = this.Text;

         // Nach einem Eintrag suchen, der am Anfang dem eingegebenen
         // Text entspricht
         this.currentMatchIndex = this.FindString(this.currentInput);
         
         // Den aktuellen Treffer selektieren
         this.SelectCurrentMatch();
      }

      // Die geerbte Methode aufrufen 
      base.OnTextChanged(e);
   }

   /* OnKeyDown wird überschrieben, um die Verarbeitung von OnTextChanged zu 
      vermeiden, wenn die Backspace-, die Löschen-, die Cursor-Up oder die
      Cursor-Down-Taste betätigt wurde. Außerdem behandelt OnKeyDown die 
      Betätigung der Cursor-Up- und der Cursor-Down-Taste, um den
      vorherigen bzw. nächsten passenden Eintrag auszuwählen und die
      Betätigung der Backspace oder Löschen-Teste, um die intern
      verwaltete aktuelle Eingabe zurückzusetzen. */
   protected override void OnKeyDown(KeyEventArgs e)
   {
      // Verarbeitung vermeiden, wenn die Backspace- oder die Löschen-
      // Taste betätigt wurde
      this.dontHandleTextChanged = (e.KeyCode == Keys.Back || 
         e.KeyCode == Keys.Delete || e.KeyCode == Keys.Up || 
         e.KeyCode == Keys.Down);

      int newMatchIndex = -1;
      switch (e.KeyCode)
      {
         case Keys.Up:
            if (this.currentMatchIndex > -1)
            {
               // Ermitteln, ob der Eintrag über dem aktuellen 
               // zu der aktuellen Eingabe passt
               if (this.currentMatchIndex > 0)
               {
                  if (this.Items[this.currentMatchIndex - 1].ToString()
                     .StartsWith(this.currentInput,
                     StringComparison.CurrentCultureIgnoreCase))
                  {
                     newMatchIndex = this.currentMatchIndex - 1;
                  }
               }
            }
            break;

         case Keys.Down:
            if (this.currentMatchIndex > -1)
            {
               // Ermitteln, ob der Eintrag unter dem aktuellen zu der
               // aktuellen Eingabe passt
               if (this.currentMatchIndex < this.Items.Count - 1)
               {
                  if (this.Items[this.currentMatchIndex +1]
                     .ToString().StartsWith(this.currentInput,
                     StringComparison.CurrentCultureIgnoreCase))
                  {
                     newMatchIndex = this.currentMatchIndex + 1;
                  }
               }
            }
            break;

         case Keys.Back:
         case Keys.Delete:
         case Keys.Left:
         case Keys.Right:
            // Bei Backspace, Löschen, Cursor-Links oder Cursor-Rechts 
            // die aktuelle Eingabe zurücksetzen
            this.currentInput = null;
            this.currentMatchIndex = -1;
            break;
      }

      if (newMatchIndex > -1)
      {
         // Eintrag gefunden, der zur aktuellen Eingabe passt: 
         // Diesen selektieren
         this.currentMatchIndex = newMatchIndex;
         this.SelectCurrentMatch();
      }

      if (newMatchIndex > -1 || 
         ((e.KeyCode == Keys.Up || e.KeyCode == Keys.Down) && 
         this.currentMatchIndex > -1 && (this.SelectionLength > 0 &&
         this.SelectionLength < this.Text.Length)))
      {
         // Wenn ein neuer Treffer ermittelt wurde, oder wenn der Benutzer 
         // die Cursor-Up-oder die Cursor-Down-Taste betätigt hat, während 
         // aktuell ein Vorschlag angezeigt wird, aber nicht kein Text oder
         // der gesamte Text selektiert ist: Das Ereignis als behandelt 
         // kennzeichnen, damit die ComboBox die Tasten-Betätigung
         // nicht auswertet.
         e.Handled = true;
      }

      // Die geerbte Methode aufrufen
      base.OnKeyDown(e);
   }

   /* Schreibt den aktuellen Treffer in die TextBox und selektiert den von der
     aktuellen Eingabe differenten Teil */
   private void SelectCurrentMatch()
   {
      if (this.currentMatchIndex >= 0)
      {
         // Eintrag gefunden: Alten Text merken, neuen Eintrag
         // auswählen und den differenten Teil selektieren
         try
         {
            this.dontHandleTextChanged = true;
            this.SelectedIndex = -1;
            this.SelectedIndex = this.currentMatchIndex;
         }
         finally
         {
            this.dontHandleTextChanged = false;
         }
         this.Select(this.currentInput.Length, this.Text.Length);
      }
   }
}

Listing 0.3: Eigene ComboBox mit Autovervollständigung

Benutzer, Gruppen und Sicherheit

262 Daten symmetrisch ver- und entschlüsseln

Dieses Rezept hatte Probleme mit dem Verschlüsseln von Strings, die Zeichen im Unicode-Bereich über 255 enthielten. Einige dieser Zeichen wurden nicht korrekt ver- bzw. entschlüsselt. Dieses Problem habe ich dadurch gelöst, dass die Encrypt-Methode den übergebenen String über die UFT8-Codierung in einen MemoryStream liest, aber beim der Rückgabe des verschlüsselten Strings diesen über die ISO-8859-1-Codierung ermittelt. Diese Codierung verwendet Decrypt dann auch, um den übergebenen (verschlüsselten) String in einen MemoryStream umzuwandeln, bevor dieses entschlüsselt wird. Das Ergebnis-Byte-Array wird dann wieder über die UFT8-Codierung in einen String umgewandelt. Ein Test mit einem String, der einige Zeichen um Unicode-Bereich über 255 enthält, ergab keine Fehler mehr (siehe Beispiel).

Außerdem habe ich die Klasse SymmetricEncryptor um den AES-Algorithmus erweitert. Hier ist die neue Version dieser Klasse:

/* Aufzählung für die unterstützten symmetrischen Verschlüsselungen  */
public enum SymmetricEncryptAlgorithm
{
   /* Advanced-Encryption-Standard-Algorithmus */
   AES,

   /* Data-Encryption-Standard-Algorithmus */
   DES,

   /* TripleDES-Algorithmus */
   TrippleDES,

   /* RC2-Algorithmus */
   RC2,

   /* Rijndael-Algorithmus */
   Rijndael
}

/* Klasse zum Ver- und Entschlüseln von Daten */
public class SymmetricEncryptor
{
   /* Verwaltet die Instanz des Verschlüsselers */
   private SymmetricAlgorithm encryptor;

   /* Verwaltet den Namen des Verschlüsselungs-Algorithmus */
   private string algorithmName;

   /* Die Codierung für das Konvertieren von Strings in Byte-Arrays
      und umgekehrt. Es muss sich dabei zwingend um eine 8-Bit-Codierung
      handeln, da es ansonsten Probleme mit dem Entschlüsseln von
      verschlüsselten Strings gibt. */
   private Encoding stringEncoding = Encoding.GetEncoding("ISO-8859-1");

   /* Konstruktor  */
   public SymmetricEncryptor(SymmetricEncryptAlgorithm algorithm)
   {
      switch (algorithm)
      {
         case SymmetricEncryptAlgorithm.AES:
            this.encryptor = new AesManaged();
            this.algorithmName = "AES";
            break;

         case SymmetricEncryptAlgorithm.DES:
            this.encryptor = new DESCryptoServiceProvider();
            this.algorithmName = "DES";
            break;

         case SymmetricEncryptAlgorithm.TrippleDES:
            this.encryptor = new TripleDESCryptoServiceProvider();
            this.algorithmName = "TripleDES";
            break;

         case SymmetricEncryptAlgorithm.RC2:
            this.encryptor = new RC2CryptoServiceProvider();
            this.algorithmName = "RC2";
            break;

         case SymmetricEncryptAlgorithm.Rijndael:
            this.encryptor = new RijndaelManaged();
            this.algorithmName = "Rijndael";
            break;
      }
   }

   /* Gibt den Chiffrier-Modus an */
   public CipherMode CipherMode
   {
      get { return this.encryptor.Mode; }
      set { this.encryptor.Mode = value; }
   }

   /* Verwaltet die zu verwendende Blockgröße */
   public int BlockSize
   {
      get { return this.encryptor.BlockSize; }
      set { this.encryptor.BlockSize = value; }
   }

   /* Gibt den Padding-Modus an */
   public PaddingMode PaddingMode
   {
      get { return this.encryptor.Padding; }
      set { this.encryptor.Padding = value; }
   }

   /* Verwaltet den Schlüssel */
   public string Key
   {
      get
      {
         return this.stringEncoding.GetString(this.encryptor.Key);
      }

      set
      {
         // Den übergebenen Schlüssel überprüfen
         if (this.encryptor.ValidKeySize(value.Length * 8) == false)
         {
            // Ungültiger Schlüssel: Ausnahme mit erweiterten Informationen
            // werfen
            string allowedKeySizes = null;
            for (int i = 0; i < this.encryptor.LegalKeySizes.Length; i++)
            {
               if (allowedKeySizes != null)
               {
                  allowedKeySizes += ", ";
               }
               allowedKeySizes += this.encryptor.LegalKeySizes[i].MinSize +
                  ", " + this.encryptor.LegalKeySizes[i].MaxSize;
            }
            throw new CryptographicException("Der übergebene Schlüssel " +
               "ist mit " + (value.Length * 8) + " Bit für den " +
               this.algorithmName + "-Algorithmus ungültig. Erlaubt " +
               "sind die folgenden Größen: " + allowedKeySizes + ".");
         }

         // Auf Unicode-Zeichen größer 0x00FF überprüfen
         for (int i = 0; i < value.Length; i++)
         {
            if ((int)value[i] > 255)
            {
               throw new CryptographicException("Der übergebene " +
                  "Schlüssel enthält mindestens ein Unicode-Zeichen, " +
                  "das größer ist als 0x00FF (255): " + value[i] +
                  " (" + (int)value[i] + "). Unterstützt werden lediglich " +
                  "8-Bit-Unicode-Zeichen");
            }
         }

         // Den Schlüssel setzen
         this.encryptor.Key = this.stringEncoding.GetBytes(value);
      }
   }

   /* Verwaltet den Initialisierungsvektor */
   public string InitializationVector
   {
      get
      {
         return this.stringEncoding.GetString(this.encryptor.IV);
      }

      set
      {
         // Den übergebenen Initialisierungsvektor überprüfen
         if ((value.Length * 8) != this.encryptor.BlockSize)
         {
            // Ungültiger Initialisierungsvektor: Ausnahme mit erweiterten
            // Informationen werfen
            throw new CryptographicException("Der übergebene " +
               "Initialisierungsvektor ist mit " + (value.Length * 8) +
               " Bit für den " + this.algorithmName + "-Algorithmus " +
               "ungültig. Die Länge des Initialisierungsvektors muss " +
               "durch die Blockgröße (aktuell " + this.encryptor.BlockSize +
               ") ohne Rest teilbar sein");
         }

         // Auf Unicode-Zeichen größer 0x00FF überprüfen
         for (int i = 0; i < value.Length; i++)
         {
            if ((int)value[i] > 255)
            {
               throw new CryptographicException("Der übergebene " +
                  "Initialisierungsvektor enthält mindestens ein " +
                  "Unicode-Zeichen, das größer ist als 0x00FF (255): " +
                  value[i] + " (" + (int)value[i] + "). Unterstützt " +
                  "werden lediglich 8-Bit-Unicode-Zeichen");
            }
         }

         // Den Initialisierungsvektor setzen
         this.encryptor.IV = this.stringEncoding.GetBytes(value);
      }
   }

   /* Erzeugt einen zufälligen Schlüssel */
   public string GenerateKey()
   {
      this.encryptor.GenerateKey();
      return this.Key;
   }

   /* Erzeugt einen zufälligen Initialisierungsvektor */
   public string GenerateInitializationVector()
   {
      this.encryptor.GenerateIV();
      return this.InitializationVector;
   }

   /* Verschlüsselt die Daten aus einem Stream */
   public void Encrypt(Stream sourceStream, Stream destStream)
   {
      // CryptoStream zum Verschlüsseln erzeugen. Als Transformations-
      // Objekt wird das Verschlüssel-Objekt der aktuellen 
      // SymmetricAlgorithm-Instanz übergeben
      CryptoStream cryptoStream = new CryptoStream(destStream,
         this.encryptor.CreateEncryptor(), CryptoStreamMode.Write);

      // Die Rohdaten blockweise in den CryptoStream schreiben (der diese
      // verschlüsselt in den Ziel-Stream schreibt)
      int bytesRead = 0;
      byte[] buffer = new byte[1024];
      do
      {
         bytesRead = sourceStream.Read(buffer, 0, 1024);
         cryptoStream.Write(buffer, 0, bytesRead);
      } while (bytesRead > 0);

      // Den Zielstream aktualisieren und den Puffer löschen 
      cryptoStream.FlushFinalBlock();

      // Der CryptoStream darf hier nicht geschlossen werden, da
      // dieser den Ziel-Stream ansonsten auch schließt
   }

   /* Entschlüsselt die Daten aus einem Stream */
   public void Decrypt(Stream sourceStream, Stream destStream)
   {
      // CryptoStream zum Entschlüsseln erzeugen. Als Transformations-
      // Objekt wird das Entschlüssel-Objekt der aktuellen
      // SymmetricAlgorithm-Instanz übergeben
      CryptoStream cryptoStream = new CryptoStream(sourceStream,
         this.encryptor.CreateDecryptor(), CryptoStreamMode.Read);

      // Daten blockweise einlesen
      int bytesRead = 0;
      byte[] buffer = new Byte[1024];
      do
      {
         bytesRead = cryptoStream.Read(buffer, 0, 1024);
         destStream.Write(buffer, 0, bytesRead);
      } while (bytesRead > 0);

      // Der CryptoStream darf hier nicht geschlossen werden, da
      // dieser den Quell-Stream ansonsten auch schließt
   }

   /* Verschlüsselt einen String */
   public string Encrypt(string source)
   {
      // MemoryStreams für die Daten erzeugen
      MemoryStream sourceStream =
         new MemoryStream(Encoding.UTF8.GetBytes(source));
      MemoryStream destStream = new MemoryStream();

      // Daten verschlüsseln
      sourceStream.Position = 0;
      this.Encrypt(sourceStream, destStream);

      // Ergebnis auslesen
      destStream.Position = 0;
      byte[] encryptedBytes = destStream.ToArray();

      // Streams schließen
      sourceStream.Close();
      destStream.Close();

      // String zurückgeben
      return this.stringEncoding.GetString(encryptedBytes);
   }

   /* Entschlüsselt einen String */
   public string Decrypt(string source)
   {
      // MemoryStreams für die Daten erzeugen
      MemoryStream sourceStream =
         new MemoryStream(this.stringEncoding.GetBytes(source));
      MemoryStream destStream = new MemoryStream();

      // Daten entschlüsseln
      this.Decrypt(sourceStream, destStream);

      // Ergebnis auslesen
      destStream.Position = 0;
      byte[] encryptedBytes = destStream.ToArray();

      // Streams schließen
      sourceStream.Close();
      destStream.Close();

      // String zurückgeben
      return Encoding.UTF8.GetString(encryptedBytes);
   }
}

Listing 0.1: Verbesserte Klasse zum Ver- und Entschlüsseln

263 Daten mit Hashing-Verfahren verschlüsseln

Dieses Rezept habe ich um die Berücksichtigung der Hashing-Klassen in der CNG-Implementierung (Cryptography Next Generation) erweitert (die laut der Dokumentation nur unter Windows Vista, Windows XP SP2 und Windows Server 2003 unterstützt werden, auf meinem XP-SP2-System aber trotzdem eine NotSupportedException hervorrufen). Außerdem habe ich der Hasher-Klasse die Eigenschaften MaxKeyLength und SupportsKey hinzugefügt. Im set-Accessor der Key-Eigenschaft wird zusätzlich überprüft, ob die Länge des übergebenen Schlüssels die Maximallänge nicht überschreitet.

/* Aufzählung für die unterstützten Hashing-Algorithmen */
public enum HashAlgorithmKind
{
   MD5,
   MD5Cng,
   RIPEMD160,
   SHA1,
   SHA1Cng,
   SHA256,
   SHA256Cng,
   SHA384,
   SHA384Cng,
   SHA512,
   SHA512Cng,
   HMACMD5,
   HMACRIPEMD160,
   HMACSHA1,
   HMACSHA256,
   HMACSHA384,
   HMACSHA512,
   MACTripleDES
}

/* Klasse für verschiedene Hash-Algorithmen */
public class Hasher
{
   /* Verwaltet das Hash-Objekt */
   private HashAlgorithm hashAlgorithm;

   /* Die Codierung für das Konvertieren von Strings in Byte-Arrays 
      und umgekehrt. Es muss sich dabei zwingend um eine 8-Bit-Codierung 
      handeln. */
   private Encoding stringEncoding = Encoding.GetEncoding("ISO-8859-1");

   /* Verwaltet die maximale Schlüssel-Länge. Wird im Konstruktor gesetzt. */
   public int MaxKeyLength
   {
      private set;
      get;
   }

   /* Konstruktor */
   public Hasher(HashAlgorithmKind algorithm)
   {
      // Algorithmus definieren
      switch (algorithm)
      {
         case HashAlgorithmKind.MD5:
            this.hashAlgorithm = new MD5CryptoServiceProvider();
            break;

         case HashAlgorithmKind.MD5Cng:
            this.hashAlgorithm = new MD5Cng();
            break;

         case HashAlgorithmKind.RIPEMD160:
            this.hashAlgorithm = new RIPEMD160Managed();
            break;

         case HashAlgorithmKind.SHA1:
            this.hashAlgorithm = new SHA1Managed();
            break;

         case HashAlgorithmKind.SHA1Cng:
            this.hashAlgorithm = new SHA1Cng();
            break;

         case HashAlgorithmKind.SHA256:
            this.hashAlgorithm = new SHA256Managed();
            break;

         case HashAlgorithmKind.SHA256Cng:
            this.hashAlgorithm = new SHA256Cng();
            break;

         case HashAlgorithmKind.SHA384:
            this.hashAlgorithm = new SHA384Managed();
            break;

         case HashAlgorithmKind.SHA384Cng:
            this.hashAlgorithm = new SHA384Cng();
            break;

         case HashAlgorithmKind.SHA512:
            this.hashAlgorithm = new SHA512Managed();
            break;

         case HashAlgorithmKind.SHA512Cng:
            this.hashAlgorithm = new SHA512Cng();
            break;

         case HashAlgorithmKind.HMACMD5:
            this.hashAlgorithm = new HMACMD5();
            break;

         case HashAlgorithmKind.HMACRIPEMD160:
            this.hashAlgorithm = new HMACRIPEMD160();
            break;

         case HashAlgorithmKind.HMACSHA1:
            this.hashAlgorithm = new HMACSHA1();
            break;

         case HashAlgorithmKind.HMACSHA256:
            this.hashAlgorithm = new HMACSHA256();
            break;

         case HashAlgorithmKind.HMACSHA384:
            this.hashAlgorithm = new HMACSHA384();
            break;

         case HashAlgorithmKind.HMACSHA512:
            this.hashAlgorithm = new HMACSHA512();
            break;

         case HashAlgorithmKind.MACTripleDES:
            this.hashAlgorithm = new MACTripleDES();
            break;
      }

      // Die Maximallänge des Schlüssels ermitteln
      if (this.hashAlgorithm is KeyedHashAlgorithm)
      {
         this.MaxKeyLength =
            ((KeyedHashAlgorithm)this.hashAlgorithm).Key.Length;
      }
      else
      {
         this.MaxKeyLength = 0;
      }
   }

   /* Gibt zurück, ob der aktuell verwendete Algorithmus 
      einen Schlüssel erlaubt */
   public bool SupportsKey
   {
      get
      {
         return (this.MaxKeyLength > 0);
      }
   }

   /* Verwaltet den Schlüssel für Algorithmen, die einen solchen benötigen */
   public string Key
   {
      get
      {
         // Überprüfen, ob der Algorithmus einen Schlüssel erlaubt
         if (this.SupportsKey)
         {
            // Schlüssel in einen String umwandeln und zurückgeben
            return this.stringEncoding.GetString(
               ((KeyedHashAlgorithm)this.hashAlgorithm).Key);
         }
         else
         {
            throw new NotSupportedException("Der aktuell verwendete " +
               "Hash-Algorithmus unterstützt keine Schlüssel");
         }
      }

      set
      {
         // Auf Unicode-Zeichen größer 0x00FF überprüfen
         for (int i = 0; i < value.Length; i++)
         {
            if ((int)value[i] > 255)
            {
               throw new CryptographicException("Der übergebene " +
                  "Schlüssel enthält mindestens ein Unicode-Zeichen, " +
                  "das größer ist als 0x00FF (255): " + value[i] + " (" +
                  (int)value[i] + "). Unterstützt werden lediglich " +
                  "8-Bit-Unicode-Zeichen");
            }
         }

         // Überprüfen, ob der Algorithmus einen Schlüssel erlaubt
         if (this.SupportsKey)
         {
            // Schlüssel in ein Byte-Array überführen
            byte[] key = this.stringEncoding.GetBytes(value);

            // Überprüfen, ob die Schlüssellänge zum Algorithmus passt.
            if (key.Length <= this.MaxKeyLength)
            {
               // Schlüssel setzen
               ((KeyedHashAlgorithm)this.hashAlgorithm).Key = key;
            }
            else
            {
               throw new NotSupportedException("Der übergebene Schlüssel " +
                  "ist mit " + key.Length + " Byte zu groß für den " +
                  "gesetzten Hash-Algorithmus. Dieser unterstützt nur " +
                  "maximal " + this.MaxKeyLength + " Byte große Schlüssel");
            }
         }
         else
         {
            throw new NotSupportedException("Der aktuell verwendete " +
               "Hash-Algorithmus unterstützt keine Schlüssel");
         }
      }
   }

   /* Erzeugt einen Hash aus einem Byte-Array */
   public string ComputeHash(byte[] data)
   {
      return this.stringEncoding.GetString(
         this.hashAlgorithm.ComputeHash(data));
   }

   /* Erzeugt einen Hash aus einem Byte-Array */
   public string ComputeHash(byte[] data, int offset, int count)
   {
      return this.stringEncoding.GetString(
         this.hashAlgorithm.ComputeHash(data, offset, count));
   }

   /* Erzeugt einen Hash aus den Daten eines Stream */
   public string ComputeHash(Stream inputStream)
   {
      return this.stringEncoding.GetString(
         this.hashAlgorithm.ComputeHash(inputStream));
   }

   /* Erzeugt einen Hash für einen String */
   public string ComputeHash(string inputString)
   {
      // Byte-Array aus dem String erzeugen und damit den Hashcode erzeugen
      byte[] buffer = Encoding.Unicode.GetBytes(inputString);

      return this.stringEncoding.GetString(
         this.hashAlgorithm.ComputeHash(buffer));
   }
}

Listing 0.2: Klasse zum Erzeugen eines Hashcode für Strings und Streams

Bildbearbeitung

268 Das Format eines Bilds auslesen

Dieses Rezept habe ich (wie alle Bildverarbeitungs-Rezepte) um die Behandlung von WPF erweitert. Hier ist der WPF-Text:

In einer WPF-Anwendung können Sie das Format eines Bildes aus einem BitmapDecoder-Objekt auslesen, das Sie zum Einlesen des Bildes verwenden. Die Eigenschaft CodecInfo verwaltet Informationen zu Codec. Diese (unzureichend dokumentierte) Eigenschaft ist vom Typ BitmapCodecInfo. Die einzelnen Codecs werden leider nicht durch eigene Codec-Info-Klassen repräsentiert, obwohl BitmapCodecInfo abstrakt ist. Das liegt wahrscheinlich daran, dass auf einem System andere Codecs vorliegen können als auf einem anderen.

In der Eigenschaft MimeTypes dieses Objekts finden Sie eine Angabe der MIME-Typen, die dem Codec zugeordnet sind. Die möglichen (Standard-)MIME-Typen sind leider nicht dokumentiert. Ich habe die folgenden MimeTypes-Werte für die Standard-Bildformate herausgefunden:

Bildformat Mime-Typ-Angabe
Bitmap image/bmp
GIF image/gif
JPEG image/jpeg,image/jpe,image/jpg
TIFF image/tiff,image/tif
PNG image/png
Listing 3: Die von MimeTypes zurückgegebenen MIME-Typen für Standard-Bildformate

ber die Abfrage der MIME-Typen können Sie das Format also recht einfach auslesen. Da auch mehrere Angaben enthalten sind, fragt die Methode GetImageFormatName in Listing 4 einfach, ob die einer der MIME-Typen (siehe de.selfhtml.org/diverses/mimetypen.htm) der am häufigsten vorkommenden Bildformate enthalten ist.

Zum Kompilieren dieser Methode müssen Sie den Namensraum System.Windows.Media.Imaging einbinden.

public static string GetImageFormatName(BitmapDecoder decoder)
{
   // Das Format des Bildes ermitteln
   if (decoder.CodecInfo.MimeTypes.Contains("image/bmp"))
   {
      return "Bitmap";
   }
   else if (decoder.CodecInfo.MimeTypes.Contains("image/gif"))
   {
      return "GIF";
   }
   else if (decoder.CodecInfo.MimeTypes.Contains("image/jpeg"))
   {
      return "JPEG";
   }
   else if (decoder.CodecInfo.MimeTypes.Contains("image/tiff"))
   {
      return "TIFF";
   }
   else if (decoder.CodecInfo.MimeTypes.Contains("image/png"))
   {
      return "PNG";
   }
   else if (decoder.CodecInfo.MimeTypes.Contains("image/fif"))
   {
      return "FIF";
   }
   else if (decoder.CodecInfo.MimeTypes.Contains("image/ief"))
   {
      return "IEF";
   }
   else if (decoder.CodecInfo.MimeTypes.Contains("image/vasa"))
   {
      return "Vasa";
   }
   else if (decoder.CodecInfo.MimeTypes.Contains("image/vnd.wap.wbmp"))
   {
      return "Bitmap (WAP)";
   }
   else if (decoder.CodecInfo.MimeTypes.Contains("image/x-icon"))
   {
      return "Icon";
   }
   else if (decoder.CodecInfo.MimeTypes.Contains("image/x-rgb"))
   {
      return "RGB";
   }
   else if (decoder.CodecInfo.MimeTypes.Contains("image/x-xbitmap"))
   {
      return "X-Bitmap";
   }
   else
   {
      return "Unbekannt";
   }
}

Listing 4: Methode zum Ermitteln des Bildformats in einer WPF-Anwendung

Listing 5 zeigt eine beispielhafte Anwendung dieser Methode.

string fileName = "C:\\Bilder\\Hitchhiker.gif";

// Bild in einen BitmapDecoder einlesen
BitmapDecoder decoder = BitmapDecoder.Create(new Uri(fileName),
   BitmapCreateOptions.None, BitmapCacheOption.None);

// Das Format des Bildes ermitteln
string formatName = GetImageFormatName(decoder);

// Dateiname und Format ausgeben
Console.WriteLine(fileName + ": " + formatName);

Listing 5: Anwendung der Methode zur Abfrage des Bildformats in einer WPF-Anwendung

Interessant (und eigentlich auch logisch) ist, dass das Format eines Bilds nicht von der Dateiendung abhängt, sondern im Header der Bilddatei verwaltet wird. Wenn Sie z. B. die Endung einer GIF-Datei in .png ändern, wird die Datei trotzdem als GIF-Datei eingelesen. GetImageFormatName liefert dann auch wie erwartet »GIF« zurück.

269 Bild-Metadaten auslesen

Dieses Rezept habe ich (wie alle Bildverarbeitungs-Rezepte) um die Behandlung von WPF erweitert. Hier ist der komplette neue Text:

Viele Grafikformate verwalten nicht nur das eigentliche Bild, sondern auch zusätzliche Informationen zum Bild. Das JPEG-Format ist beispielsweise u. A. in der Lage, das Aufnahmedatum eines mit einer Digitalkamera aufgenommenen Bilds zu speichern. Diese Zusatzinformationen, die als Image Tag (Bild-Etikett) bezeichnet werden, können Sie in WPF- und in Windows.Forms-Anwendungen auswerten.

Grundlagen

Metadaten werden von verschiedenen Programmen in Bilder geschrieben. Eine Digitalkamera speichert z. B. das Aufnahmedatum, das Kameramodell um den Hersteller in den erzeugten Bildern. Die Nachrichtenagentur Reuters speichert Copyright-Informationen in den online veröffentlichten Bildern. Windows Vista zeigt diese Metadaten übrigens im Explorer an (wobei dieser die WPF-Klassen zum auslesen der Metainformationen verwendet).

Metadaten können in verschiedenen Standards (auch mehrere pro Bild) in einem Bild gespeichert sein. WPF unterstützt die Standards Exchangeable Image Format (EXIF, z. B. in JPEG, TIFF- und JFIF-Dateien), International Press Telecommunications Council (IPTC, z. B. in TIFF- und JFIF-Dateien), Extensible Metadata Platform (XMP, in PDF-Dateien), PNG Textual Data (tEXt, in PNG-Dateien) und Image File Directory (IFD, in TIFF-Dateien).

Nähere Informationen zu den Standards finden Sie größtenteils bei Wikipedia:

In dem zu Windows.Forms gehörenden GDI+ wird scheinbar nur der EXIF-Standard unterstützt (was aber nicht dokumentiert ist). Auf diesen Standard gehe ich deswegen auch näher ein.

EXIF ist ein Standard der Japan Electronic Industry Development Association (JEIDA) und beschreibt ein spezielles Dateiformat für Digitalkameras, in dem neben dem eigentlichen Bild zusätzliche Metadaten gespeichert werden können. Als Bildformat wird bei EXIF JPEG oder TIFF verwendet. Informationen zu EXIF finden Sie an der Adresse www.exif.org/specifications.html.

Die einzelnen Bild-Tags können unterschiedliche Datentypen besitzen, die (natürlich) im EXIF-Standard beschrieben sind. Tabelle 1 beschreibt diese Typen.

Datentyp Beschreibung
BYTE Byte
ASCII Nullterminierter 7-Bit-ASCII-String
SHORT 16-Bit Integer ohne Vorzeichen (!)
LONG 32-Bit (!) Integer ohne Vorzeichen (!)
RATIONAL RATIONAL besteht aus zwei LONG-Werten, die zusammen eine rationale Zahl ergeben. Der erste verwaltet den Zähler, der zweite den Nenner. Das Ergebnis eines RATIONAL-Wertes ist also die Division des ersten durch den zweiten Wert.
UNDEFINED Byte-Wert, der alle Werte annehmen kann, abhängig von der Tag-Beschreibung
SLONG 32-Bit (!) Integer mit Vorzeichen
SRATIONAL SRATIONAL ist RATIONAL ähnlich, besteht aber aus zwei SLONG-Werten.
Tabelle 1: Die Datentypen des EXIF-Standards

Die Werte aller Datentypen werden in Arrays verwaltet. Pro Image Tag sind also prinzipiell mehrere Werte möglich. ASCII-Tags verwalten einzelne Zeichen (in einem Byte-Array), alle anderen ein Array aus dem jeweiligen Datentyp. Der EXIF-Standard beschreibt für jedes Tag, wie viele Werte verwaltet werden. Meist handelt es sich nur um einen Wert, in diesem Fall müssen Sie also nur das erste Arrayelement auswerten. Bei einigen wenigen Tags, wie z. B. ISOSpeedRatings (ISO-Geschwindigkeiten der Kamera) können auch mehrere Werte verwaltet werden.

Das Ganze ist also nicht allzu einfach auszulesen. WPF erleichtert die Abfrage (und das in diesem Rezept nicht behandelte Schreiben) von Metadaten aber erheblich.

WPF

In einer WPF-Anwendung (oder einer Anwendung, die die WPF-Klassen verwendet …) stehen Metadaten über die Eigenschaft Metadata einer BitmapSource-Instanz zur Verfügung. Diese Eigenschaft verwaltet nur dann eine Instanz einer von ImageMetadata abgeleiteten Klasse, wenn das Bild Metadaten enthält. ImageMetadata selbst ist abstrakt und stellt nur Basis-Funktionalitäten zur Verfügung. Die zurzeit (.NET 3.5, erstes Release) einzige von ImageMetadata abgeleitete Klasse ist BitmapMetadata, und diese stellt einige Eigenschaften zur Verfügung, über die die Standard-Metadaten auslesen können (Tabelle 2).

Eigenschaft Beschreibung
Application¬Name Name der Anwendung, mit der das Bild ggf. erzeugt wurde
Author ReadOnlyCollection<String> mit den Autoren des Bildes
Camera¬Manufacturer Der Hersteller der Kamera, mit der das Bild ggf. aufgenommen wurde
CameraModel Das Kameramodell, mit dem das Bild ggf. aufgenommen wurde
Comment String mit Kommentaren
Copyright Eine Copyright-Meldung
DataTaken Das Aufnahmedatum als String. Das Format ist leider nicht dokumentiert. Mit meinen Testbildern erhielt ich das Datum immer in dem Format der Kultur, die für den aktuellen Thread eingestellt war. Ob das immer so ist, kann ich leider nicht sagen, es muss ja schließlich einen Sinn haben, dass hier (dummerweise) ein String zurückgegeben wird.
Format Das Format des Bildes als String, der die für das Format typische Dateiendung enthält
Keywords ReadOnlyCollection<String> mit Schlüsselwörtern, die dem Bild zugeordnet sind
Rating Integer-Wert, der eine Bewertung des Bildes enthält. In Vista können Sie Bilder z. B. im Explorer mit einem Wert von 0 bis 5 bewerten.
Subject Beschreibung des Themas des Bildes
Title Der Titel des Bildes
Tabelle 2: Die Metadaten-Eigenschaften der BitmapMetadata-Klasse

Andere Metadaten, die nicht zum »Standard« gehören, können Sie über die GetQuery-Methode der BitmapMetadata-Klasse auslesen. Soweit will ich aber nicht gehen, und zeige nur, wie Sie die Standard-Daten auslesen. Das Beispiel erfordert den Import der Namensräume System und System.Windows.Media.Imaging.

// BitmapSource-Objekt für das Bild erzeugen
string fileName = "C:\\Bilder\\Windsurfen.jpg";
BitmapSource bitmapSource = BitmapFrame.Create(new Uri(fileName));

// Ermitteln, ob Metadaten vorhanden sind
if (bitmapSource.Metadata != null)
{
   // Ermitteln, ob die Metadaten vom Typ BitmapMetadata sind
   // (die einzige zurzeit von ImageMetadata abgeleitete Klasse)
   BitmapMetadata bitmapMetadata = bitmapSource.Metadata as BitmapMetadata;
   if (bitmapMetadata != null)
   {
      Console.WriteLine("Autor(en):");
      if (bitmapMetadata.Author != null)
      {
         foreach (var author in bitmapMetadata.Author)
         {
            Console.WriteLine("   " + author);
         }
      }
      Console.WriteLine("Titel: " + bitmapMetadata.Title);
      Console.WriteLine("Copyright: " + bitmapMetadata.Copyright);
      Console.WriteLine("Bild-Format :" + bitmapMetadata.Format);
      Console.WriteLine("Themenbeschreibung: " + bitmapMetadata.Subject);
      Console.WriteLine("Kommentar: " + bitmapMetadata.Comment);
      Console.WriteLine("Aufnahmedatum: " + bitmapMetadata.DateTaken);
      Console.WriteLine("Kamera-Hersteller: " + 
         bitmapMetadata.CameraManufacturer);
      Console.WriteLine("Kamera-Modell: " + bitmapMetadata.CameraModel);
      Console.WriteLine("Name der Anwendung, mit der das Bild " +
         "erzeugt wurde: " + bitmapMetadata.ApplicationName);
      Console.WriteLine("Schlüsselwörter:");
      if (bitmapMetadata.Keywords != null)
      {
         foreach (var keyword in bitmapMetadata.Keywords)
         {
            Console.WriteLine(keyword);
         }
      }
      Console.WriteLine("Bildbewertung: " + bitmapMetadata.Rating);   }
   else
   {
      // Unbekannte (neue) Metadaten-Klasse
      Console.WriteLine("Unbekannte (neue) Metadaten-Klasse '" +
         bitmapSource.Metadata.GetType().Name + "'");
   }
}
else
{
   // Keine Metadaten vorhanden
   Console.WriteLine("Keine Metadaten vorhanden");
}

Listing 6: Ermitteln von Metadaten in einer WPF-Anwendung

Die Metadaten sind natürlich nicht immer gefüllt. Die Eigenschaften DateTaken, CameraManufacturer und CameraModel sind z. B. normalerweise nur dann mit einem Wert belegt, wenn das Bild von einer Digitalkamera erzeugt wurde.

Windows.Forms

In einer Windows.Forms-Anwendung werten Sie Bild-Metadaten leider nicht ganz so elegant wie in WPF über die Eigenschaft PropertyItems der Bitmap-Klasse aus. Diese Auflistung verwaltet Instanzen der Klasse PropertyItem. Die Eigenschaft Id gibt als int-Wert an, um welche Information es sich handelt. Die Konstanten für die verschiedenen möglichen Id-Werte finden Sie bei Microsoft in der GDI+-Referenz: msdn.microsoft.com/library/en-us/gdicpp/GDIPlus/GDIPlusReference/Constants/ImagePropertyTagConstants.asp.

Statt die Adresse einzugeben können Sie auch auf der Seite msdn.microsoft.com/library einfach nach »Image Property Tag Constants« suchen. Die Beschreibungen der einzelnen Tag-Werte finden Sie über den Link Property Item Descriptions.

In der Eigenschaft Value wird der Wert der Information gespeichert, eigenartigerweise leider nicht als object, sondern als (nicht weiter dokumentiertes) Byte-Array. Die Eigenschaft Len gibt die Länge dieses Arrays an. Der Typ des Werts wird in der Eigenschaft Type als short-Wert verwaltet. Die verwendeten Typen und deren Konstanten finden Sie auf der Seite msdn.microsoft.com/library/en-us/gdicpp/GDIPlus/GDIPlusReference/Constants/ImagePropertyTagTypeConstants.asp.

Die gespeicherten Werte entsprechen scheinbar (leider nicht dokumentiert) dem EXIF-Standard.

Für Bildinformationen, die als String dargestellt werden können (Textinformationen, Datumswerte), verwaltet die Value-Eigenschaft eines PropertyItem-Objekts ein (dem EXIF-Standard entsprechend) nullterminierte (C++-) 8-Bit-Zeichenkette. Die Auswertung solcher Werte ist relativ einfach.

Die Methode GetTagValueAsString (Listing 7) liefert den String zurück, der in der Eigenschaft Value eines Informations-Werts gespeichert ist. Da es sich bei der dazu auszulesenden Eigenschaft PropertyItems leider nur um ein Array handelt, muss diese Methode das Array durchgehen um den gesuchten Wert zu finden. Wurde der Wert gefunden, erfolgt die Konvertierung in einen String einfach über das Durchgehen des Byte-Arrays, wobei das letzte Zeichen (das abschließende 0-Zeichen) nicht mit eingelesen wird.

Datumswerte werden nach dem EXIF-Standard (und im Test bei den von mir getesteten JPEG-Bildern, die von fünf verschiedenen Kameras aufgenommen wurden) im Format yyyy:MM:dd HH:mm:ss zurückgegeben (das in der .NET-Dokumentation leider nicht dokumentiert ist). Als Sonderform kommt der String »0000:00:00 00:00:00« vor, der wohl für »kein Datum« steht. Außerdem kann es sein, dass bei einstelligen Zahlen statt der führenden Null ein Leerzeichen steht. Die Methode GetTagValueAsDateTime versucht deshalb, den aus einer Tag-Information ausgelesenen String entsprechend in einen DateTime-Wert zu konvertieren.

Das Beispiel liest (in einer Konsolenanwendung) alle Bilder eines Ordners und zu jedem Bild den Hersteller und das Modell des Geräts aus, über das das Bild erzeugt wurde, und das Datum der Aufzeichnung des Bilds.

Zum Kompilieren des Beispiels müssen Sie die Namensräume System, System.IO, System.Text, System.Drawing und System.Drawing.Imaging einbinden.

/* Schreibt ausgewählte Bild-Metadaten an die Konsole */
private static void WriteImageMetadata(Bitmap bitmap)
{
   // Hersteller und Name des Geräts ermitteln, über den das Bild 
   // erzeugt wurde
   const int PROPERTYTAGEQUIPMAKE = 0x010F;
   const int PROPERTYTAGEQUIPMODEL = 0x110;
   string equipmentManufacturer = GetTagValueAsString(bitmap,
      PROPERTYTAGEQUIPMAKE);
   string equipmentModel = GetTagValueAsString(bitmap, PROPERTYTAGEQUIPMODEL);
   Console.WriteLine("Hersteller, Modell: {0} {1}", equipmentManufacturer,
      equipmentModel);

   // Datum der Erzeugung des Bilds ermitteln
   const int PROPERTYTAGDATETIME = 0x0132;
   DateTime? imageCreateDate = GetTagValueAsDateTime(bitmap,
      PROPERTYTAGDATETIME);
   if (imageCreateDate != null)
   {
      Console.WriteLine("Bild erzeugt am {0}", imageCreateDate.ToString());
   }
}

/* Liefert den Wert einer Tag-Eigenschaft eines Bilds als String */
private static string GetTagValueAsString(Bitmap bitmap, int itemType)
{
   string result = null;
   for (int i = 0; i < bitmap.PropertyItems.Length; i++)
   {
      PropertyItem item = bitmap.PropertyItems[i];
      if (item.Id == itemType)
      {
         for (int j = 0; j < item.Len - 1; j++)
         {
            result += (char)item.Value[j];
         }
         break;
      }
   }
   return result;
}

/* Liefert den Wert einer Tag-Eigenschaft eines Bilds als DateTime */
private static DateTime? GetTagValueAsDateTime(Bitmap bitmap, int itemType)
{
   string propertyValue = GetTagValueAsString(bitmap, itemType);
   if (propertyValue != null)
   {
      // Versuch, den im Format yyyy:MM:dd HH:mm:ss 
      // ermittelten String in ein Datum zu konvertieren
      if (propertyValue != "0000:00:00 00:00:00")
      {
         // Berücksichtigen, dass bei einstelligen Zahlen auch Leerzeichen
         // statt der 0 angegeben sein können.
         propertyValue = Regex.Replace(propertyValue, @": (\d)", @":0${1}");
         propertyValue = Regex.Replace(propertyValue, @" (\d):", @"0${1}:");

         // Den String versuchen zu parsen
         DateTime result;
         if (DateTime.TryParseExact(propertyValue, "yyyy:MM:dd HH:mm:ss",
            CultureInfo.InvariantCulture, DateTimeStyles.None, out result))
         {
            return result;
         }
         else
         {
            throw new Exception("Der String '" + propertyValue +
               "' kann nicht in einen DateTime-Wert " +
               "konvertiert werden");
         }
      }
   }
   return null;
}

Listing 7: Methoden zur Ermittlung und Ausgabe des Herstellers, Kameramodells und des Aufnahmedatums von Jpeg-Bildern an der Konsole

Bei Informationen, die als Zahlwert gespeichert sind, wird die Auswertung etwas komplizierter. Das Byte-Array ist in diesem Fall lediglich eine Darstellung der für die Zahlwerte gespeicherten einzelnen Bytes. Für einen gespeicherten int-Wert enthält das Array also die vier Bytes, in denen der int-Wert gespeichert ist. Einige Zahlwerte werden auch als rationale Zahl (Bruchzahl) verwaltet. Hier wird dann eigentlich ein Array aus zwei Zahlen vom entsprechenden Typ (z. B. long) verwaltet, wobei die erste der Zähler und die zweite der Nenner ist. Das Byte-Array enthält dann wieder lediglich die einzelnen Bytes dieser Zahlwerte.

In C++ ist die Auswertung von Zahlwerten ganz einfach. Dazu casten Sie das erste Byte des Byte-Arrays einfach in einen Zeiger des entsprechenden Typs und werten die Zahl dann über den Zeiger aus. Einen long-Wert können Sie in C++ dann z. B. so auslesen:

long* ptrLong = (long*)(pProp.value);
printf("Der Wert ist %d.\n", ptrLong[0]);

Einen als rationale Zahl dargestellten long-Wert können Sie in C++ so auslesen:

long* ptrLong = (long*)(pProp.value);
printf("Der Wert ist %d/%d.\n", ptrLong[0], ptrLong[1]);

In C# können Sie Zahlwerte auch auf diese Weise auswerten, dazu müssen Sie allerdings mit Zeigern in einem unsicheren Codeblock arbeiten (und damit die ganze Assembly mit der Option Unsafe kompilieren). Einfacher ist es, stattdessen die Methoden der Klasse BitConverter zu verwenden, die ein Byte-Array in den gewünschten Typ umwandeln.

Als Beispiel habe ich die Helligkeit eines Bilds gewählt, die als PropertyTagTypeSRational-Wert gespeichert ist. Ein solcher Wert soll laut der Dokumentation zwei long-Werte mit Vorzeichen verwalten, die eine rationale Zahl darstellen. Der erste ist der Nenner und der zweite der Zähler.

In meinen Tests wurden hier aber nicht zwei long- sondern zwei int-Werte gespeichert (was auch daran zu erkennen ist, dass das Byte-Array lediglich acht Bytes verwaltet). Daran erkennen Sie, dass Sie etwas vorsichtig mit der Dokumentation umgehen und selbst ausprobieren müssen. Zur Sicherheit fragt die folgende Methode zur Ermittlung der Helligkeit eines Bilds die Länge des Arrays ab und ermittelt das Ergebnis entsprechend:

private static double GetBitmapBrightness(Bitmap bitmap)
{
   const int PropertyTagExifBrightness = 0x9203;
   double result = 0;
   for (int i = 0; i < bitmap.PropertyItems.Length; i++)
   {
      PropertyItem item = bitmap.PropertyItems[i];
      if (item.Id == PropertyTagExifBrightness)
      {
         // Die Werte für den Zähler (Numerator) und den 
         // Nenner (Denominator) ermitteln
         if (item.Len == 8)
         {
            // Zwei int-Werte für den Zähler und den Nenner
            // Anmerkung: Entspricht nicht der Dokumentation, 
            // kam in meinen Tests aber ausschließlich vor
            int numerator = BitConverter.ToInt32(item.Value, 0);
            int denominator = BitConverter.ToInt32(item.Value, 4);
         
            // Das Ergebnis berechnen
            result = numerator / (double)denominator;
         }
         else if (item.Len == 16)
         {
            // Zwei long-Werte für den Zähler und den Nenner
            // Anmerkung: Kam im Test nicht vor, ist aber laut der
            // Dokumentation die korrekte Variante
            long numerator = BitConverter.ToInt32(item.Value, 0);
            long denominator = BitConverter.ToInt32(item.Value, 4);
         
            // Das Ergebnis berechnen
            result = numerator / (double)denominator;
         }
         break;
      }
   }
   return result;
}

Listing 8: Methode zur Ermittlung der Helligkeit eines Bilds

270 Das Aufnahmedatum eines Bilds auslesen

Dieses Rezept habe ich (wie alle Bildverarbeitungs-Rezepte) um die Behandlung von WPF erweitert. Hier ist der vollständige neue Text:

Basierend auf dem Wissen aus Rezept 269 habe ich eine Methode entwickelt, die das Aufnahmedatum eines Bilds ausliest. Diese Methode liegt in zwei Varianten vor, einer für WPF- und einer für Windows.Forms-Anwendungen.

Beide Varianten sind nicht 100-Prozentig sicher, da das als String zurückgegebene Datum theoretisch jedes Format besitzen kann. Ein Test mit allen meinen Digitalkamera-Bildern (die mit verschiedenen Kameras aufgenommen wurden) hat aber ergeben, dass das Format für WPF immer das Format war, das der aktuellen Kultur entspricht. Für die Bitmap-Klasse (die in Windows.Forms-Anwendungen verwendet wird) wurde immer das (EXIF-)Format yyyy:MM:dd HH:mm:ss zurückgebeben. Die Methoden sollten also für die meisten (Digitalkamera-) Bilder funktionieren. Das Beispiel zu diesem Rezept liest übrigens das Erstelldatum aller Bilder ab einem anzugebenden Ordner ein und hält (mit Debugger.Break()) an, wenn ein Datum nicht dem erwarteten Format entspricht. Mit dieser Anwendung können Sie sehr gut prüfen, ob Ihre Bilder ggf. ein anderes Datumsformat verwalten.

Zum Kompilieren der WPF-Variante müssen Sie die Namensräume System und System.Windows.Media.Imaging importieren. Diese Methode gibt (wie auch die GDI-Variante) eine Nullable<DateTime>-Instanz zurück. null wird zuückgegeben wenn kein Datum existiert. Im Falle eines Datums, das nicht geparst werden kann, wirft GetCreationDate eine FormatException.

public static DateTime? GetCreationDate(BitmapSource bitmapSource)
{
   // Ermitteln, ob Metadaten vorhanden sind
   if (bitmapSource.Metadata != null)
   {
      // Ermitteln, ob die Metadaten vom Typ BitmapMetadata sind
      // (die einzige zurzeit von ImageMetadata abgeleitete Klasse)
      BitmapMetadata bitmapMetadata = bitmapSource.Metadata as BitmapMetadata;
      if (bitmapMetadata != null)
      {
         if (bitmapMetadata.DateTaken != null)
         {
            // Versuch, das Datum mit der aktuellen Kultur zu konvertieren
            DateTime result;
            if (DateTime.TryParse(bitmapMetadata.DateTaken, out result))
            {
               return result;
            }
            else
            {
               throw new FormatException("Der String '" +
                  bitmapMetadata.DateTaken +
                  "' kann nicht in einen DateTime-Wert " +
                  "konvertiert werden");
            }
         }
      }
   }

   // Das Datum existiert nicht oder ist nicht angegeben
   return null;
}

Listing 9: WPF-Variante der Methode zum Auslesen des Aufnahmedatums eines Bilds

Die (langsame) GDI-Variante (die wohl vorwiegend in Windows.Forms-Anwendungen Verwendung finden wird, die aus Speicher-Gründen die WPF-Assemblys nicht referenzieren sollen) benötigt den Import der Assemblys System, System.Drawing, System.Drawing.Imaging, System.Globalization und System.Text.RegularExpressions.

public static DateTime? GetCreationDate(Bitmap bitmap)
{
   const int propertyTagDateTime = 0x0132;

   string propertyValue = null;
   for (int i = 0; i < bitmap.PropertyItems.Length; i++)
   {
      PropertyItem item = bitmap.PropertyItems[i];
      if (item.Id == propertyTagDateTime)
      {
         // Den String ermitteln, der das Datum speichert
         for (int j = 0; j < item.Len - 1; j++)
         {
            propertyValue += (char)item.Value[j];
         }

         // Berücksichtigen, dass bei einstelligen Zahlen auch 
         // Leerzeichen statt der 0 angegeben sein können.
         propertyValue = Regex.Replace(propertyValue, @": (\d)", @":0${1}");
         propertyValue = Regex.Replace(propertyValue, @" (\d):", @"0${1}:");

         // Versuch, den im Format yyyy:MM:dd HH:mm:ss 
         // ermittelten String in ein Datum zu konvertieren
         if (propertyValue != "0000:00:00 00:00:00")
         {
            DateTime result;
            if (DateTime.TryParseExact(propertyValue, "yyyy:MM:dd HH:mm:ss",
               CultureInfo.InvariantCulture, DateTimeStyles.AllowInnerWhite,
               out result))
            {
               return result;
            }
            else
            {
               throw new FormatException("Der String '" + propertyValue +
                  "' kann nicht in einen DateTime-Wert " +
                  "konvertiert werden");
            }
         }

         break;
      }
   }

   // Das Datum existiert nicht oder ist nicht angegeben
   return null;
}

Listing 10: GDI-Variante der Methode zum Auslesen des Aufnahmedatums eines Bilds

271 Eingelesene Bilder im Originalformat speichern

Dieses Rezept habe ich (wie alle Bildverarbeitungs-Rezepte) um die Behandlung von WPF erweitert. Hier ist der WPF-Text:

In einer WPF-Anwendung lesen Sie Bilder (mehr oder weniger direkt) immer über Instanzen der Klasse BitmapDecoder oder davon abgeleiteter, auf bestimmte Codecs spezialisierte Klassen wie z. B. JpegBitmapDecoder. Wenn Sie BitmapDecoder verwenden, ermittelt die Create-Methode beim Einlesen eines Bildes den Codec und sucht diesen in der Windows-Registry. Wird ein passender Codec gefunden, wird das Bild über diesen dekodiert und eingelesen. Informationen über den Codec stehen in der Eigenschaft CodecInfo zur Verfügung.

Speichern können Sie über Instanzen der BitmapEncoder-Klasse oder davon abgeleiteter Klassen (wie JpegBitmapEncoder). Die spezialisierten Klassen erwarten keine weitere Angabe (des Codec). Ein Bild in einem der Standardformate zu speichern ist also kein Problem. Die Create-Methode der allgemeinen BitmapEncoder-Klasse erwartet aber den GUID des Codec, der verwendet werden soll. Und das ist auch schon der Trick um ein Bild in dem Format abzuspeichern, in dem es eingelesen wurde. Den Codec-GUID erhalten Sie nämlich über die Eigenschaft ContainerFormat des CodecInfo-Objekts.

Das folgende Beispiel liest ein Bild ein, erzeugt davon einen Klon (als Demo für eine Bearbeitung) und speichert den Klon dann unter einem anderen Namen, aber im Originalformat. Zum Kompilieren müssen Sie die Namensräume System, System.IO und System.Windows.Media.Imaging einbinden.

// Bild einlesen
string sourceFilename = "C:\\Bilder\\Hitchhiker.gif";
BitmapDecoder decoder = BitmapDecoder.Create(new Uri(sourceFilename),
   BitmapCreateOptions.None, BitmapCacheOption.None);
BitmapSource bitmapSource = decoder.Frames[0];

// Bild bearbeiten (hier nur: Kopie erzeugen) 
BitmapSource destBitmapSource = bitmapSource.Clone();

// Bild mit dem Original-Codec speichern
string destFilename = "C:\\Bilder\\Hitchhiker-Klon.gif";
BitmapEncoder encoder = BitmapEncoder.Create(
   decoder.CodecInfo.ContainerFormat);
encoder.Frames.Add(BitmapFrame.Create(destBitmapSource));
using (FileStream fileStream = new FileStream(destFilename, FileMode.Create))
{
   encoder.Save(fileStream);
}

Listing 11: Einlesen, Bearbeiten und Speichern eines Bildes im Originalformat

272 Bild in Byte-Array umwandeln

Dieses Rezept habe ich (wie alle Bildverarbeitungs-Rezepte) um die Behandlung von WPF erweitert. Hier ist der WPF-Text:

In WPF können Sie theoretisch die CopyPixels-Methode der BitmapSource-Klasse verwenden, um ein Bild in ein Byte-Array umzuwandeln. CopyPixels erzeugt ein Array, in dem die rohen Pixeldaten des Bildes enthalten sind. Wenn Sie die Daten eines Bildes aber in einer Datenbank speichern wollen, hat dieses Vorgehen (das ich im Rezept »285 Die einzelnen Pixel eines Bilds bearbeiten« auf Seite 64 einsetze), aber den Nachteil, dass Sie sich zum späteren Decodieren mehrere Informationen merken müssen (die Schrittweite des Bildes, die Farbtiefe, die Breite und die Höhe).

Deshalb habe ich eine andere Lösung entwickelt. Diese Lösung verwendet einen BitmapEncoder, der die Daten eines Bildes codiert und in einen Stream schreiben kann. Als Stream verwende ich einen MemoryStream, der dann einfach über ToArray in ein Byte-Array überführt wird.

Der Methode BitmapSource2Byte in Listing 12 müssen Sie deswegen neben dem zu konvertierenden Bild auch den BitmapEncoder übergeben. Damit überlasse ich Ihnen die Wahl des Bildformats (ich würde das verlustfreie PNG-Format bevorzugen, das allerdings keine Animationen unterstützt …).

Zum Kompilieren dieser Methode müssen Sie die Namensräume System, System.IO und System.Windows.Media.Imaging importieren.

public static byte[] BitmapSource2Byte(BitmapSource bitmapSource, 
   BitmapEncoder encoder)
{
   if (encoder == null)
   {
      throw new ArgumentNullException("encoder");
   }
   
   // Das Bild dem Encoder hinzufügen
   encoder.Frames.Add(BitmapFrame.Create(bitmapSource));
   
   // MemoryStream erzeugen und das Bild in diesen schreiben
   using (MemoryStream imageStream = new MemoryStream())
   {
      encoder.Save(imageStream);
      imageStream.Flush();

      // MemoryStream in ein Byte-Array schreiben und dieses zurückgeben
      return imageStream.ToArray();
   }
}

Listing 12: Methode zum Umwandeln eines BitmapSource-Objekts in ein Byte-Array

Die Anwendung dieser Methode ist einfach. Das folgende Beispiel verwendet den PngBitmapEncoder zum Kodieren der Bilddaten im PNG-Format:

// Die Datei in ein BitmapSource-Objekt einlesen
BitmapSource bitmapSource = BitmapFrame.Create(new Uri(imageFilename));

// Das Bitmap in ein byte-Array umwandeln
byte[] imageData2 = ImageUtils.BitmapSource2Byte(bitmapSource, 
   new PngBitmapEncoder());

Listing 13: Beispielhafte Anwendung der BitmapSource2Byte-Methode

273 Byte-Array in Bitmap umwandeln

Dieses Rezept habe ich (wie alle Bildverarbeitungs-Rezepte) um die Behandlung von WPF erweitert. Hier ist der WPF-Text:

Wenn Sie ein Bild nach dem Rezept 272 in WPF in ein Byte-Array geschrieben haben, das einem bestimmten Bildformat entspricht, können Sie dieses recht einfach wieder in ein BitmapSource-Objekt konvertieren, indem Sie die Byte-Daten in einen MemoryStream schreiben und diesen der Create-Methode der BitmapDecoder-Klasse übergeben (die das Format an den Daten automatisch erkennt). Die Methode Byte2BitmapSource in Listing 14 macht genau das. Damit der MemoryStream sauber geschlossen wird, erzeugt diese Methode den Stream in einer using-Anweisung. Der BitmapDecoder wird mit der BitmapCacheOption OnLoad erzeugt, was sehr wichtig ist, denn diese Option sorgt dafür, dass die Bitmap-Daten sofort aus dem Stream eingelesen werden (und nicht erst, wenn das Bild benötigt wird, das dann zu spät ist, da der Stream zwischenzeitlich geschlossen wurde).

Byte2BitmapSource benötigt den Import der Namensräume System.IO und System.Windows.Media.Imaging.

public static BitmapSource Byte2BitmapSource(byte[] imageBytes)
{
   // MemoryStream mit den Bytes des Bildes erzeugen und 
   // ein damit erzeugtes Bitmap zurückgeben
   using (MemoryStream imageStream = new MemoryStream(imageBytes))
   {
      BitmapDecoder decoder = BitmapDecoder.Create(imageStream,
         BitmapCreateOptions.None, BitmapCacheOption.OnLoad);
      return decoder.Frames[0];
   }
}

Listing 14: :WPF- Methode zum Erzeugen eines BitmapSource-Objekts aus einem Byte-Array

Das Beispiel zu diesem Rezept beweist, das das Konvertieren in ein Byte-Array und das Decodieren für alle gängigen Bildformate problemlos funktioniert. Schauen Sie es sich an …

274 Bilder aus der Zwischenablage auslesen

Dieses Rezept habe ich (wie alle Bildverarbeitungs-Rezepte) um die Behandlung von WPF erweitert. Hier ist der WPF-Text:

In WPF können Sie theoretisch die GetImage-Methode der Klasse Clipboard (aus dem Namensraum System.Windows) verwenden um ein Bild aus der Zwischenablage einzulesen. Über die ContainsImage-Methode können Sie überprüfen, ob überhaupt ein Bild in der Zwischenablage gespeichert ist. GetImage gibt allerdings einfach null zurück wenn die Zwischenablage kein Bild beinhaltet.

if (Clipboard.ContainsImage)
{
   BitmapSource bitmapSource = Clipboard.GetImage();
   ...
}

Listing 15: (Nicht so ganz funktionierendes) Einlesen eines Bildes aus der Zwischenablage in WPF

So weit die Theorie. Leider scheint das Ganze nicht so wirklich gut zu funktionieren. Sie können ein so eingelesenes Bild zwar problemlos in eine Datei speichern. Wenn Sie das Bild aber direkt nach dem Einlesen der Source-Eigenschaft eines Image-Steuerelements zuweisen, wird das Bild aus unerfindlichen Gründen entweder gar nicht angezeigt (bei kleinen Bildern, wie z. B. aus Paint heraus kopiert wurden), oder nur zerstückelt (wenn Sie z. B. mit der Druck-Taste den gesamten Desktop in die Zwischenablage kopieren). Vergleichen Sie dazu auch den Blog-Eintrag shevaspace.spaces.live.com/Blog/cns!FD9A0F1F8DD06954!441.entry.

Deshalb gehe ich mit meiner Lösung den (funktionierenden) Weg über GDI+. Die folgende Methode, die in ähnlicher Form auch für das Auslesen von Bildern in Windows.Forms-Anwendungen verwendet wird, setzt die Clipboard-Klasse aus Windows.Forms ein, um ein Bitmap-Objekt aus der Zwischenablage einzulesen, und konvertiert dieses schließlich in ein BitmapSource-Objekt.

GetBitmapSourceFromClipboard benötigt den Import der Namensräume System, System.Windows und System.Windows.Media.Imaging.

public static BitmapSource GetBitmapSourceFromClipboard()
{
   // Die Zwischenablagedaten auslesen und überprüfen
   System.Windows.Forms.IDataObject clipboardData =
      System.Windows.Forms.Clipboard.GetDataObject();
   if (clipboardData != null)
   {
      // Überprüfen, ob ein Bitmap gespeichert ist
      if (clipboardData.GetDataPresent(
         System.Windows.Forms.DataFormats.Bitmap))
      {
         // Bitmap auslesen
         System.Drawing.Bitmap bitmap =
            (System.Drawing.Bitmap)clipboardData.GetData(
            System.Windows.Forms.DataFormats.Bitmap);

         // Das Bitmap in ein BitmapSource-Objekt umwandeln
         return System.Windows.Interop.Imaging.CreateBitmapSourceFromHBitmap(
           bitmap.GetHbitmap(), IntPtr.Zero, Int32Rect.Empty,
           BitmapSizeOptions.FromEmptyOptions());
      }
   }

   // null zurückgeben, falls in der Zwischenablage kein Bild 
   // gespeichert ist
   return null;
}

Listing 16: : (Funktionierendes) Einlesen eines Bildes aus der Zwischenablage in WPF (über den Umweg über GDI)

275 Screenshot erstellen

Dieses Rezept habe ich neben der Berücksichtigung von WPF zusätzlich noch darum verbessert, dass halbtransparente Fenster unterstützt werden. Hier ist der vollständige neue Text:

Einen Screenshot des Bildschirms bzw. eines Formulars können Sie mit .NET-Klassen erzeugen. WPF stellt dazu, so weit ich herausgefunden habe, allerdings keine Funktionalität zur Verfügung. Sie müssen also, auch unter WPF, auf das systemeigene GDI+ zurückgreifen.

Prinzipiell können Sie einen Screenshot erzeugen, idem Sie ein Bitmap-Objekt von der benötigten Größe erzeugen, für dieses ein Graphics-Objekt erzeugen und dessen CopyFromScreen-Methode aufrufen. CopyFromScreen erwartet am ersten und zweiten Argument die X- und Y-Position des zu kopierenden Bildschirmausschnitts, am dritten und vierten Argument die X- und Y-Position im Ziel-Bitmap, an die kopiert werden soll (normalerweise 0, 0), am fünften Argument die Angabe der Größe des zu kopierenden Bildschirmausschnitts und am letzten Argument einen oder mehrere Werte der CopyPixelOperation-Aufzählung. Der CopyPixelOperation-Wert bestimmt, wie die einzelnen Pixel des Bildschirms auf das Bild kopiert werden. Für einen Screenshot wäre zumindest die Angabe CopyPixelOperation.SourceCopy notwendig, damit die Pixel der Quelle 1:1 in das Ziel-Bild kopiert werden:

Rectangle screenClip = Screen.PrimaryScreen.Bounds;

// Bitmap für das Ergebnis erzeugen und das& zugehörige
// Graphics-Objekt auslesen
Bitmap bitmap = new Bitmap(screenClip.Width, screenClip.Height);
Graphics g = Graphics.FromImage(bitmap);

// Den Bildschirminhalt in das Bitmap kopieren
g.CopyFromScreen(screenClip.Left, screenClip.Top, 0, 0,
   new Size(screenClip.Width, screenClip.Height),
   CopyPixelOperation.SourceCopy);

Listing 17: Prinzipiell mögliches Erstellen eines Screenshot

In WPF können Sie das Bitmap-Objekt dann in ein BitmapSource-Objekt umwandeln, wie ich es in´m Codebook (in einem neuen Rezept) zeige.

Das Problem mit dieser Variante ist lediglich, dass sie nicht für (mehr oder weniger) transparente Fenster funktioniert. Wenn Sie z. B. die Opacity-Eigenschaft eines Windows.Forms-Formulars auf einen Wert kleiner 1 setzen, und einen Screenshot von dem Bereich erzeugen, auf dem das Formular liegt, ist das Formular auf dem Screenshot nicht sichtbar.

Um halb-transparente Fenster in den Screenshot zu integrieren wäre die zusätzliche Angabe von CopyPixelOperation.CaptureBlt notwendig, was laut Dokumentation dafür sorgt, dass Fenster, die andere überlappen, mit in den Screenshot aufgenommen werden. Leider lässt CopyFromScreen aber die Kombination CopyPixelOperation.SourceCopy | CopyPixelOperation.CaptureBlt nicht zu und wirft in diesem Fall eine InvalidEnumArgumentException.

Die Lösung für das Problem ist, dass Sie das, was CopyFromScreen macht, selbst implementieren. Und dazu benötigen Sie API-Funktionen. Und ein wenig Theorie (aber nur ein ganz klein wenig):

Windows verwaltet in einem System mit mehreren angeschlossenen Monitoren alle Bildschirme gemeinsam als einen virtuellen Bildschirm. Wenn Sie z. B. zwei Bildschirme mit einer Auflösung von 1280 * 1024 besitzen und diese in den Eigenschaften des Desktop so platziert haben, dass der sekundäre Monitor rechts vom primären liegt, beginnt der primäre Bildschirm an der X-Position 0 und der sekundäre an der X-Position 1281. Da Sie den sekundären Monitor aber auch links vom primären platzieren können, kann die X-Position eines Bildschirms auch negativ sein. Ähnliches gilt für die Y-Position, die alle möglichen Werte besitzen kann, da Sie die einzelnen Bildschirme frei auf dem virtuellen Bildschirm ausrichten können.

ber die Funktion CreateDC können Sie nun den Device Context (DC) des virtuellen Bildschirms ermitteln. Ein Device Context repräsentiert im Windows-API die Zeichenoberfläche eines Fensters, Bitmaps oder eines Geräts wie eines Druckers. Über die Funktion BitBlt können Sie die Farbinformationen eines DC in einen anderen kopieren. Als Ziel-DC verwenden Sie zur Erstellung eines Screenshots den DC eines neu erzeugten System.Drawing.Bitmap-Objekts.

Die Methode Screenshot in Listing 19 zeigt, wie Sie diese Technik einsetzen können. Diese Methode erwartet am einzigen Argument ein Rechteck, das den zu kopierenden Bildschirm-Ausschnitt angibt. Dieser Ausschnitt bezieht sich auf den virtuellen Bildschirm.

Zum Kompilieren dieser Methode müssen Sie die Assemblys System.Drawing.dll und System.Windows.Forms.dll referenzieren und die Namensräume System, System.Drawing, System.Windows.Forms und System.Runtime.InteropServices importieren.

Zur Umsetzung sind zunächst einige API-Deklarationen notwendig:

[DllImport("gdi32.dll", SetLastError=true)]
private static extern int BitBlt(IntPtr hdcDest, int nXDest, int nYDest,
   int nWidth, int nHeight, IntPtr hdcSrc, int nXSrc, int nYSrc,
   CopyPixelOperation dwRop);

[DllImport("gdi32.dll", SetLastError=true)]
private static extern IntPtr CreateDC(string lpszDriver, string lpszDevice,
   string lpszOutput, IntPtr lpInitData);

private static int SRCCOPY = 0x00CC0020;

Listing 42: Deklaration der benötigten API-Funktionen und Konstanten

Screenshot ermittelt zunächst über CreateDC den DC des Bildschirms. Am ersten Argument wird dazu der String "DISPLAY" übergeben. Mit diesem DC erzeugt Screenshot ein Graphics-Objekt, das beim nachfolgenden Erzeugen eines Bitmap-Objekts am letzten Argument angegeben wird und das die Auflösung des Bilds bestimmt. Das Bitmap-Objekt wird dann in der Größe des Bildschirms erzeugt und ist das Ziel für den späteren BitBlt-Aufruf. Um den DC dieses Objekts zu erhalten, erzeugt Screenshot ein neues Graphics-Objekt und gibt das Bitmap-Objekt am Argument des Konstruktors an. Der DC wird dann über die Methode GetHdc dieses Objekts ausgelesen.

Vor dem Aufruf von BitBlt muss der DC des Bildschirms erneut ermittelt werden, da dieser anscheinend beim Aufruf der FromHdc-Methode der Graphics-Klasse freigegeben wurde (ohne dieses erneute Auslesen schlägt BitBlt ohne Fehlermeldung fehl und die Freigabe des DC führt zu einem Fehler).

Dann wird das Bild über BitBlt kopiert. Am letzten Argument übergibt Screenshot den Wert CopyPixelOperation.SourceCopy | CopyPixelOperation.CaptureBlt, was zum einen dafür sorgt, dass das Quellbild alle Farbinformationen des Ziels überschreibt. Zum anderen ermöglichen Sie damit auch das Kopieren von halbtransparenten Fenstern. Über andere Konstanten, die in der Referenz erläutert werden, können Sie die Farbinformationen des Ziels auch auf verschiedene Weise mit denen der Quelle vermischen, was aber für unsere Lösung nicht interessant ist. Schließlich müssen die DCs noch über die ReleaseHdc-Methode des jeweiligen Graphics-Objekts freigegeben werden, da diese Freigabe nicht automatisch über den Garbage Collector erfolgt.

public static Bitmap GetScreenshot(Rectangle screenClip)
{
   // Device Context für den virtuellen Windows-Bildschirm ermitteln 
   // und damit ein Graphics-Objekt erzeugen
   IntPtr screenDC = CreateDC("DISPLAY", null, null, (IntPtr)null);
   using (Graphics screenGraphics = Graphics.FromHdc(screenDC))
   {
      // Bitmap mit den Ausmaßen des zu erzeugenden Ausschnitts 
      // und der Auflösung des Graphics-Objekts erzeugen
      Bitmap bitmap = new Bitmap(screenClip.Width, screenClip.Height,
         screenGraphics);

      // Zweites Graphics-Objekt aus dem noch leeren Bitmap erzeugen 
      // um den DC des Bitmap-Objekts auslesen zu können
      using (Graphics bitmapGraphics = Graphics.FromImage(bitmap))
      {
         IntPtr bitmapDC = bitmapGraphics.GetHdc();

         // DC des Bildschirms noch einmal ermitteln
         screenDC = screenGraphics.GetHdc();

         // Über BitBlt das über den Bildschirm-DC repräsentierte Bild 
         // in das über den Bitmap-DC repräsentierte Bild kopieren
         if (BitBlt(bitmapDC, 0, 0, screenClip.Width, screenClip.Height,
            screenDC, screenClip.Left, screenClip.Top,
            CopyPixelOperation.SourceCopy | 
            CopyPixelOperation.CaptureBlt) == 0)
         {
            bitmapGraphics.ReleaseHdc(bitmapDC);
            screenGraphics.ReleaseHdc(screenDC);
            throw new Exception("API-Fehler " + Marshal.GetLastWin32Error() +
               " beim Aufruf von BitBlt");
         }

         // Die DCs freigeben und das Bild zurückgeben
         bitmapGraphics.ReleaseHdc(bitmapDC);
         screenGraphics.ReleaseHdc(screenDC);
         return bitmap;
      }
   }
}

Listing 19: Methode zum Erzeugen eines Screenshots eines Ausschnitts des virtuellen Bildschirms

Die Anwendung der Methode ist nun sehr einfach. Wenn Sie z. B. einen Screenshot des primären Bildschirms erstellen wollen, geben Sie die Bounds-Eigenschaft des Screen-Objekts an, das Screen.PrimaryScreen referenziert:

Bitmap captureBitmap = GetScreenshot(Screen.PrimaryScreen.Bounds);

In WPF müssen Sie übrigens wohl leider auch die Screen-Klasse aus dem Namensraum System.Windows.Forms verwenden, da WPF anscheinend keine Möglichkeit bietet, Informationen über die Bildschirme des Systems auszulesen (außer der Breite und Höhe des primären Bildschirms über SystemParameters.PrimaryScreenWidth und SystemParameters.PrimaryScreenHeight). Das zurückgegebene Bitmap wandeln Sie für WPF-Anwendungen in ein BitmapSource-Objekt um.

Wollen Sie einen Screenshot eines (sichtbaren) Formulars erzeugen, geben Sie dessen Bounds-Eigenschaft an:

Bitmap captureBitmap = GetScreenshot(form.Bounds);

In WPF-Anwendungen müssen Sie zur Erstellung eines Screenshots eines Fensters dessen auf den (virtuellen) Bildschirm bezogene Pixel-Position und -Größe ermitteln. Über die Methode PointToScreen können Sie die geräteunabhängigen 1/96-Zoll-Punkte von WPF in Bildschirm-Koordinaten umrechnen. Dabei werden allerdings der Titel und der Rand des Fensters nicht mit eingerechnet. Diesen müssen Sie also hinzurechnen, was natürlich davon abhängt, ob das Fenster einen Rand und/oder einen Titel besitzt). Leider fehlt WPF-Fenstern eine Information über die Größe des Innenbereichs (wie bei Windows.Forms-Formularen über die ClientRectangle-Eigenschaft). Darüber könnten Sie recht einfach die Breite des Randes und der Titelzeile bestimmen (Gesamtbreite – Innenbereich-Breite, Gesamthöhe – Innenbereich-Höhe). Als Notlösung könnten Sie die Eigenschaften WindowCaptionHeight (Höhe der Fenster-Titelzeile), ThickVerticalBorderWidth (Breite eines breiten rechten und linken Rahmens), ThinVerticalBorderWidth (Breite eines schmalen rechten und linken Rahmens), ThickHorizontalBorderHeight (Höhe eines dünnen oberen und unteren Rahmens) und ThickHorizontalBorderHeight (Höhe eines dünnen oberen und unteren Rahmens) der SystemParameters-Klasse verwenden. Das war mir aber zu kompliziert und unsicher, da Fenster unterschiedliche Rahmen besitzen können.

Meine Lösung des Problems setzt eine eigene Berechnung ein. Dazu müssen Sie allerdings die Auflösung des Bildschirms ermitteln, auf dem das Fenster liegt. Diese ist zwar normalerweise 96 DPI, kann aber auch eine andere sein (was Sie unter Windows in den Eigenschaften des Desktop einstellen können). Also muss ein wenig gerechnet werden. Die Methode GetScreenshot in Listing 44 zeigt, wie dies geht.

public static Bitmap GetScreenshot(Window window)
{
   // Das Argument überprüfen
   if (window == null)
   {
      throw new ArgumentNullException("window");
   }

   // PresentationSource für das Visual-Objekt erzeugen,
   // das das übergebene Fenster darstellt
   PresentationSource presentationSource = 
      PresentationSource.FromVisual(window);
   if (presentationSource != null)
   {
      // Die Auflösung ermitteln
      double dpiX = 96.0 * 
         presentationSource.CompositionTarget.TransformToDevice.M11;
      double dpiY = 96.0 * 
         presentationSource.CompositionTarget.TransformToDevice.M22;

      // Die auf den  Bildschirm bezogene Position 
      // und Größe des Fensters ermitteln
      int screenLeft = (int)(window.Left * 96 / dpiX);
      int screenTop = (int)(window.Top * 96 / dpiY);
      int screenWidth = (int)(window.Width * 96 / dpiX);
      int screenHeight = (int)(window.Height * 96 / dpiY);
      System.Drawing.Rectangle screenClip = new System.Drawing.Rectangle(
         screenLeft, screenTop, screenWidth, screenHeight);

      // Mit dem auf den Bildschirm bezogenen Rechteck 
      // einen Screenshot erzeugen und zurückgeben
      return GetScreenshot(screenClip);
   }
   else
   {
      throw new Exception("Kann für das Fenster kein "
         "PresentationSource-Objekt erzeugen");
   }
}

Listing 44: Methode zur Erstellung eines Screenshot für ein WPF-Fenster

Die Beispiele zu diesem Rezept zeigen einmal, wie Sie GetScreenshot in einer Windows.Forms- und zu anderen in einer WPF-Anwendung einsetzen.

276 Bilder skalieren

Dieses Rezept habe ich (wie alle Bildverarbeitungs-Rezepte) um die Behandlung von WPF erweitert. Hier ist der WPF-Text:

In WPF skalieren Sie Bilder, indem Sie ein neues TransformedBitmap-Objekt erzeugen und am Konstruktor neben dem zu transformierenden BitmapSource-Objekt eine ScaleTransform-Instanz übergeben. TransformedBitmap ist eine von BitmapSource abgeleitete Klasse, die in ihrem Konstruktor Transformationen auf dem übergebenen Bild ausführt. Neben Skalierungen können Sie auch andere Transformationen wie z. B. (über RotateTransform) ein Drehen des Bildes ausführen.

Der zum Skalieren verwendeten ScaleTransform-Instanz übergeben Sie am Konstruktor einen Faktor für die neue Breite und die neue Höhe. Das Skalieren gestaltet sich damit sehr einfach:

// Bild einlesen
string sourceFilename = "C:\\Bilder\\Karpathos.jpg";
BitmapDecoder decoder = BitmapDecoder.Create(new Uri(sourceFilename),
   BitmapCreateOptions.None, BitmapCacheOption.Default);
BitmapSource bitmapSource = decoder.Frames[0];

// Skalieren
double scaleFactor = 0.5;
ScaleTransform scaleTransform = new ScaleTransform(scaleFactor, scaleFactor);
TransformedBitmap transformedBitmap = new TransformedBitmap(bitmapSource,
   scaleTransform);

Listing 45: Einlesen und Skalieren eines Bildes

Der Beispiel-Quellcode benötigt den Import der Namensräume System, System.Windows.Media und System.Windows.Media.Imaging.

Die Qualität der Skalierung ist sehr gut. Leider können Sie aber in WPF die Interpolation nicht selbst bestimmen. In einigen Fällen ergibt eine andere Interpolation als die, die ScaleTransform verwendet (welche das ist, konnte ich nicht herausfinden), ein besseres Ergebnis (wie Sie ggf. mit dem GDI-Beispiel nachvollziehen können).

277 Thumbnails aus Bildern erzeugen

Dieses Rezept habe ich (wie alle Bildverarbeitungs-Rezepte) um die Behandlung von WPF erweitert. Hier ist der WPF-Text:

In WPF-Anwendungen können Sie Thumbnails über das Skalieren erzeugen, das ich in Rezept 276 beschreiben habe. Falls Sie auf die Thumbnail-Eigenschaft der BitmapDecoder-Klasse gestoßen sind: Diese ist vorgesehen für Bildformate wie JPEG und TIFF, die Thumbnails neben den Daten des eigentlichen Bildes verwalten können (aber nicht müssen).

Das folgende Beispiel liest eine Bilddatei ein und skaliert diese auf die Größe eines Image-Steuerelements (was dieses über die Stretch-Eigenschaft natürlich auch selber kann …). Beim Skalieren wird der Faktor so ausgerechnet, dass die Proportionen des Bildes erhalten bleiben. Das Programm läuft in einer WPF-Anwendung mit den üblichen Referenzen und einem Image-Steuerelement, das image genannt wird.

// Bild einlesen
string imageFileName = "C:\\Bilder\\Tabou Rocket Ltd 105.jpg");
BitmapDecoder decoder = BitmapDecoder.Create(new Uri(imageFileName),
   BitmapCreateOptions.None, BitmapCacheOption.Default);
BitmapSource bitmapSource = decoder1.Frames[0];

// Thumbnail über das Skalieren des Bildes erzeugen
double scaleX = this.image.Width / (double)bitmapSource.Width;
double scaleY = this.image.Height / (double)bitmapSource.Height;
double scaleFactor = Math.Min(scaleX, scaleY);
ScaleTransform scaleTransform = new ScaleTransform(scaleFactor, scaleFactor);
TransformedBitmap transformedBitmap = new TransformedBitmap(bitmapSource,
   scaleTransform);
this.image.Source = transformedBitmap;

Listing 46: Skalieren eines Bildes auf die Größe eines Image-Steuerelements

278 Bilder konvertieren

Dieses Rezept habe ich (wie alle Bildverarbeitungs-Rezepte) um die Behandlung von WPF erweitert. Hier ist der WPF-Text:

In einer WPF-Anwendung lesen Sie das Bild über eine BitmapDecoder-Instanz ein (die das Originalformat automatisch erkennt) und speichern Sie dieses über einen der verfügbaren Bild-Encoder ab. Zurzeit stehen die folgenden (aus dem Namensraum System.Windows.Media.Imaging) zur Verfügung, deren Format sich größtenteils aus dem Namen ablesen lässt: BmpBitmapEncoder, GifBitmapEncoder, JpegBitmapEncoder, PngBitmapEncoder, TiffBitmapEncoder und WmpBitmapEncoder (Windows-Media-Photo-Format).

Falls Sie auf Ihrem System Codecs für spezielle Formate installiert haben, können Sie deren GUID auch dem Konstruktor der BitmapEncoder-Klasse übergeben, um Bilder in einem der nicht direkt unterstützten Formate zu speichern.

Das folgende Beispiel zeigt, wie Sie eine Bilddatei einlesen und im PNG-Format speichern. Das Beispiel benötigt den Import der Namensräume System, System.IO und System.Windows.Media.Imaging.

// Bild einlesen
string imageFileName = "C:\\Bilder\\Karpathos.jpg";
BitmapDecoder decoder = BitmapDecoder.Create(new Uri(imageFileName),
   BitmapCreateOptions.None, BitmapCacheOption.Default);
BitmapSource bitmapSource = decoder.Frames[0];

// Bild im PNG-Format abspeichern
PngBitmapEncoder encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(bitmapSource));
string destFilename = Path.ChangeExtension(imageFileName, ".png");
using (FileStream fileStream = new FileStream(destFilename, FileMode.Create))
{
   encoder.Save(fileStream);
}

Listing 47: Einlesen eines JPEG-Bildes und Speichern des Bildes im PNG-Format

279 (JPEG-)Bilder mit definierter Qualität speichern

Dieses Rezept habe ich (wie alle Bildverarbeitungs-Rezepte) um die Behandlung von WPF erweitert. Hier ist der WPF-Text:

Bei den von BitmapEncoder abgeleiteten Klassen für die verschiedenen per Default unterstützten Bildformate bieten die Klassen JpegBitmapEncoder, PngBitmapEncoder, TiffBitmapEncoder und WmpBitmapEncoder Eigenschaften zur Einstellung der Qualität.

Bei der JpegBitmapEncoder-Klasse bestimmt die Eigenschaft QualityLevel, die mit einem Wert zwischen 0 und 100 die Qualität in Prozent bestimmt. Die Defaulteinstellung ist 75 (was auch für die Praxis ein sehr guter Kompromiss zwischen Bildqualität und Dateigröße ist).

Die PngBitmapEncoder-Klasse ermöglicht über die Eigenschaft Interlaced zu bestimmen, ob das Bild schicht- oder zeilenweise aufgebaut wird. Das hat zwar keinen Einfluss auf die End-Qualität des Bildes, ein Bild im Modus PngInterlaceOption.On (schichtweiser Aufbau) wird in einer Webanwendung aber schneller angezeigt, weil die einzelnen Schichten schrittweise von einer relativ geringen zur vollständigen Qualität führen. Die Defaulteinstellung ist PngInterlaceOption.Default, was bewirkt, dass die JpegBitmapEncoder-Klasse bestimmt, ob Interlacing verwendet wird oder nicht.

Bei der TiffBitmapEncoder-Klasse bestimmt die Eigenschaft Compression die Einstellung der zu verwendenden TIFF-Komprimierung. Die LZW-Komprimierung (TiffCompressOption.Lzw) ist für Farbbilder laut Informationen aus dem Internet die beste und verlustfrei. Die Komprimierungen Ccitt3, Ccitt4 und Rle können nur auf Schwarz-/Weiß-Bildern angewendet werden. Andere mögliche Komprimierungen sind None (keine) und Zip. Die Defaulteinstellung ist TiffCompressOption.Default, was bewirkt, dass die TiffBitmapEncoder-Klasse versucht, die bestmögliche Komprimierung zu ermitteln.

Die WmpBitmapEncoder-Klasse besitzt mehrere Eigenschaften zur Definition der Qualität:

Der GifBitmapEncoder lässt Qualitäts-Eigenschaften vermissen, obwohl das GIF-Format Qualitäts-Parameter wie den Verlust (Lossy) und Interlacing kennt.

Listing 48 zeigt, wie Sie ein eingelesenes Bild im JPEG-Format mit einer definierten Qualität speichern. Das Beispiel benötigt den Import der Namensräume System, System.IO und System.Windows.Media.Imaging.

// Bild einlesen
string imageFileName = "C:\\Bilder\\Los Roques.png";
BitmapDecoder decoder = BitmapDecoder.Create(new Uri(imageFileName),
   BitmapCreateOptions.None, BitmapCacheOption.Default);
BitmapSource bitmapSource = decoder.Frames[0];

// Als Jpeg-Bild mit definierter Qualität abspeichern
string destFileName = Path.ChangeExtension(imageFileName, ".jpg");
JpegBitmapEncoder encoder = new JpegBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(bitmapSource));
encoder.QualityLevel = 70; // 70% - Default ist 75%
using (FileStream fileStream = new FileStream(destFileName, FileMode.Create))
{
   encoder.Save(fileStream);
}

Listing 48: Einlesen eines Bildes und Speichern als JPEG-Bild mit definierter Qualität

280 Bilder drehen, neigen und spiegeln

Dieses Rezept habe ich (wie alle Bildverarbeitungs-Rezepte) um die Behandlung von WPF erweitert. Hier ist der WPF-Text:

In WPF drehen, neigen und spiegeln Sie Bilder (und alles andere, das dargestellt werden kann) über eine Transformation. Die Transformation eines Bildes können Sie wie beim Skalieren von Bildern in Rezept 276 über ein neues TransformedBitmap-Objekt ausführen, dem Sie am Konstruktor neben dem zu transformierenden BitmapSource-Objekt eine Instanz einer von Transform abgeleiteten Klasse übergeben.

Wenn Sie nicht das Bild selbst, sondern lediglich dessen Anzeige drehen oder spiegeln wollen, können Sie dies in WPF auch über eine Transformation des Steuerelements erreichen, wie ich in einem neuen Rezept im Codebook zeige.

Zum Drehen verwenden Sie RotateTransform. Wenn Sie Bilder transformieren, erlaubt RotateTransform aber lediglich die Angabe der Winkel -270, -180, -90, 0, 90, 180 und 270 Grad. Bei allen anderen Winkeln wird eine InvalidOperationException geworfen. Wenn Sie allerdings ein GUI-Element rotieren (z. B. ein Image-Steuerelement, das ein Bild anzeigt), können Sie jeden Winkel angeben.

Zum Spiegeln verwenden Sie eine ScaleTransform-Instanz, der Sie als Skalierungsfaktor für die Breite (ScaleX) -1 und für die Höhe (ScaleY) 1 übergeben, wenn Sie horizontal (um die X-Achse) spiegeln. Wollen Sie vertikal spiegeln, übergeben Sie für die X-Achse 1 und für die Y-Achse -1.

Beiden Klassen übergeben Sie am Konstruktor oder in Eigenschaften die Transformationswerte. Dazu gehört normalerweise auch die Angabe des Drehpunktes in den Eigenschaften CenterX und CenterY. Beim Drehen und Spiegeln von Bildern wird der Drehpunkt aber ignoriert, weil der dabei keinen Sinn macht. Beim Drehen und Spiegeln der Anzeige ist der Drehpunkt allerdings wichtig.

Wenn Sie mehrere Transformationen ausführen wollen, verwenden Sie dazu ein TransformationGroup-Objekt, dem Sie über die Children-Eigenschaft einzelne Transformation hinzufügen.

Listing 49 zeigt, wie Sie ein eingelesenes Bild um 90 Grad nach rechts drehen und um die Y-Achse spiegeln.

// Bild einlesen
string imageFileName = "C:\\Bilder\\Hitchhiker.gif");
BitmapDecoder decoder = BitmapDecoder.Create(new Uri(imageFileName),
   BitmapCreateOptions.None, BitmapCacheOption.None);
BitmapSource bitmapSource = decoder.Frames[0];

// TransformGroup-Instanz für die Transformationen erzeugen
TransformGroup transformGroup = new TransformGroup();

// RotateTransform-Objekt für das Drehen des Bildes ermitteln
// und der Transformationsgruppe hinzufügen
double rotationAngle = 90;
RotateTransform rotateTransform = new RotateTransform(rotationAngle);
transformGroup.Children.Add(rotateTransform);

// ScaleTransform-Objekt für das Spiegeln des Bildes um die Y-Achse
// ermitteln und der Transformationsgruppe hinzufügen
ScaleTransform scaleTransform = new ScaleTransform(1, -1);
   transformGroup.Children.Add(scaleTransform);
transformGroup.Children.Add(scaleTransform);

// Das eingelesene Bild transformieren
TransformedBitmap transformedBitmap = new TransformedBitmap(
   bitmapSource, transformGroup);

Listing 49: Drehen und Spiegeln eines eingelesenen Bildes in WPF

281 Bildausschnitte auslesen

Dieses Rezept habe ich (wie alle Bildverarbeitungs-Rezepte) um die Behandlung von WPF erweitert. Hier ist der WPF-Text:

In WPF verwenden Sie zum Erstellen eines Bildausschnitts eine neue Instanz der CroppedBitmap-Klasse (aus dem Namensraum System.Windows.Media.Imaging) um einen Bildausschnitt zu erzeugen. Am Konstruktor übergeben Sie das Originalbild und ein Int32Rect-Rechteck, das den Bildausschnitt bestimmt. Beachten Sie, dass die Werte in diesem Rechteck scheinbar (leider nicht dokumentiert …) in Pixeln angegeben sind, und nicht in der ansonsten in WPF üblichen 1/96-Zoll-Einheit (die z. B. die Eigenschaften Width und Height einer BitmapSource-Instanz verwenden). Wenn Sie sich also auf die Breite oder Höhe eines Bildes beziehen, müssen Sie die Eigenschaften PixelWidth und PixelHeight verwenden.

Das folgende Beispiel schneidet ein 100 * 100 Pixel großes Teil aus der Mitte eines eingelesenen Bilds aus. Es benötigt den Import der Namensräume System, System.Windows.Media.Imaging und System.Windows.

// Bild einlesen
string imageFileName = "C:\\Bilder\\Les Crosets.jpg";
BitmapDecoder decoder = BitmapDecoder.Create(new Uri(imageFileName),
   BitmapCreateOptions.None, BitmapCacheOption.None);
BitmapSource bitmapSource = decoder.Frames[0];

// Ein 100 * 100 Pixel großes Stück aus der Mitte auslesen
CroppedBitmap croppedBitmap = new CroppedBitmap(bitmapSource,
   new Int32Rect((bitmapSource.PixelWidth - 100) / 2,
   (bitmapSource.PixelHeight - 100) / 2, 100, 100));

Listing 50: Einlesen eines Bildes und Erstellen eines Ausschnitts in WPF

282 Farben von Bildern auf andere Farben mappen

Dieses Rezept habe ich (wie alle Bildverarbeitungs-Rezepte) um die Behandlung von WPF erweitert. Hier ist der WPF-Text:

Wenn Sie Farben eines Bildes auf andere Farben mappen wollen, können Sie unter GDI ein ColorMap-Array verwenden. WPF bietet scheinbar (nach eigener Recherche und nach Postings aus dem Microsoft-WPF-Forum) zurzeit noch keine direkte Möglichkeit, Farben zu transformieren. Sie können unter WPF allerdings die einzelnen Pixel eines Bildes bearbeiten, wie ich es diesem Rezept zwar einsetze, aber in Rezept 285 erst näher beschreibe. Eine mögliche Art der Transformation unter WPF ist übrigens das Konvertieren über eine ColorConvertedBitmap-Instanz, die den Farbraum des Bildes ändert, und eine FormatConvertedBitmap-Instanz, über die Sie das Pixelformat ändern können (z. B. um Graustufen-Bilder zu erzeugen, wie ich es in Rezept 286 zeige).

WPF

Wie gesagt können Sie in WPF Farben mappen, indem Sie die einzelnen Pixel des Bildes durchgehen. Das folgende Beispiel zeigt, wie Sie die Farbe des ersten Pixel eines eingelesenen Bildes auf die Farbe Navy mappen. Das Beispiel erfordert den Import der Namensräume System, System.Windows.Media und System.Windows.Media.Imaging.

// Bild einlesen
string imageFileName = "C:\\Temp\\Hitchhiker.gif";
BitmapDecoder decoder = BitmapDecoder.Create(new Uri(imageFileName),
   BitmapCreateOptions.None, BitmapCacheOption.None);
BitmapSource bitmapSource = decoder.Frames[0];

// Das Bild zunächst in das BGRA-Format konvertieren, 
// falls es ein anderes Format aufweist
if (bitmapSource.Format != PixelFormats.Bgra32)
{
   bitmapSource = new FormatConvertedBitmap(bitmapSource, 
      PixelFormats.Bgra32, null, 0.0);
}

// Die Pixel einlesen
int stride = bitmapSource.PixelWidth * 
   ((bitmapSource.Format.BitsPerPixel + 7) / 8);
byte[] pixelData = new byte[stride * bitmapSource.PixelHeight];
bitmapSource.CopyPixels(pixelData, stride, 0);

// Die Farbe des ersten Pixel ermitteln
byte blue = pixelData[0];
byte green = pixelData[1];
byte red = pixelData[2];

// Die einzelnen Pixel durchgehen und deren Farbe ggf. mappen
for (int i = 0; i < bitmapSource.PixelHeight * stride;
   i += ((bitmapSource.Format.BitsPerPixel + 7) / 8))
{
   if (pixelData[i] == blue &&
      pixelData[i + 1] == green &&
      pixelData[i + 2] == red)
   {
      pixelData[i] = Colors.Navy.B;
      pixelData[i + 1] = Colors.Navy.G;
      pixelData[i + 2] = Colors.Navy.R;
   }
}

// Mit den neuen Pixel-Daten ein neues Bild erzeugen
BitmapSource transformedBitmap = BitmapSource.Create(
   bitmapSource.PixelWidth, bitmapSource.PixelHeight,
   bitmapSource.DpiX, bitmapSource.DpiY, PixelFormats.Bgra32,
   bitmapSource.Palette, pixelData, stride);

Listing 51: Mappen von Farben eines Bildes auf andere in WPF

Ein Problem dieser Technik ist, dass sie (besonders bei großen Bildern) nicht besonders schnell ist. Eine mögliche Lösung wäre das Durchgehen der nativen Bilddaten über einen Zeiger, das ich für WPF in Rezept 285 diskutiere und für GDI zeige.

283 Farbinformationen von Bildern gezielt verändern

Dieses Rezept habe ich (wie alle Bildverarbeitungs-Rezepte) um die Behandlung von WPF erweitert. Hier ist der WPF-Text:

Wie schon in Rezept 272 ist das gezielte Verändern von Farben unter WPF scheinbar nicht möglich. Um Farbinformationen gezielt zu verändern, müssen Sie die Pixel eines Bildes bearbeiten. Unter GDI ist eine Farbveränderung allerdings direkt (und sehr schnell) möglich.

WPF

In WPF können Sie einzelne Farben eines Bilds scheinbar nur so verändern, dass Sie die einzelnen Pixel des Bilds bearbeiten. Rezept 285 geht näher darauf ein. Hier zeige ich nur eine Lösung für das Aufhellen eines Bilds um 50%. Das Beispiel erfordert, dass Sie die Namensräume System, System.Windows.Media und System.Windows.Media.Imaging einbinden.

// Bild einlesen
string imageFileName = "C:\\Temp\\Hitchhiker.gif";
BitmapDecoder decoder = BitmapDecoder.Create(new Uri(imageFileName),
   BitmapCreateOptions.None, BitmapCacheOption.None);
BitmapSource bitmapSource = decoder.Frames[0];

// Das Bild zunächst in das BGRA-Format konvertieren, 
// falls es ein anderes Format aufweist
if (bitmapSource.Format != PixelFormats.Bgra32)
{
   bitmapSource = new FormatConvertedBitmap(bitmapSource,
      PixelFormats.Bgra32, null, 0.0);
}

// Die Pixel einlesen
int stride = bitmapSource.PixelWidth * 
   ((bitmapSource.Format.BitsPerPixel + 7) / 8);
byte[] pixelData = new byte[stride * bitmapSource.PixelHeight];
bitmapSource.CopyPixels(pixelData, stride, 0);

// Die einzelnen Pixel durchgehen und die Farbe aufhellen
for (int i = 0; i < bitmapSource.PixelHeight * stride;
   i += ((bitmapSource.Format.BitsPerPixel + 7) / 8))
{
   int blue = (int)(pixelData[i] * 1.5);
   if (blue > 255) blue = 255;
   int green = (int)(pixelData[i + 1] * 1.5);
   if (green > 255) green = 255;
   int red = (int)(pixelData[i + 2] * 1.5);
   if (red > 255) red = 255;
   pixelData[i] = (byte)blue;
   pixelData[i + 1] = (byte)green;
   pixelData[i + 2] = (byte)red;
}

// Mit den neuen Pixel-Daten ein neues Bild erzeugen
BitmapSource transformedBitmap = BitmapSource.Create(
   bitmapSource.PixelWidth, bitmapSource.PixelHeight,
   bitmapSource.DpiX, bitmapSource.DpiY, PixelFormats.Bgra32,
   bitmapSource.Palette, pixelData, stride);

Listing 52: Gezieltes Verändern der Farben eines Bilds in WPF

284 Ein Negativ eines Bilds erzeugen

Dieses Rezept habe ich (wie alle Bildverarbeitungs-Rezepte) um die Behandlung von WPF erweitert. Hier ist der WPF-Text:

In WPF müssen Sie (leider) wieder die einzelnen Pixel des Bildes von Hand bearbeiten. Die Methode CreateNegative in Listing 53 zeigt, wie das geht. Diese Methode erfordert den Import der Namensräume System.Windows.Media und System.Windows.Media.Imaging.

public static BitmapSource CreateNegative(BitmapSource sourceBitmap)
{
   // Das Bild zunächst in das BGRA-Format konvertieren, 
   // falls es ein anderes Format aufweist
   if (sourceBitmap.Format != PixelFormats.Bgra32)
   {
      sourceBitmap = new FormatConvertedBitmap(sourceBitmap,
         PixelFormats.Bgra32, null, 0.0);
   }

   // Die Pixel einlesen
   int stride = sourceBitmap.PixelWidth * 
      ((sourceBitmap.Format.BitsPerPixel + 7) / 8);
   byte[] pixelData = new byte[stride * sourceBitmap.PixelHeight];
   sourceBitmap.CopyPixels(pixelData, stride, 0);

   // Die einzelnen Pixel durchgehen und die Farbwerte negativieren
   for (int i = 0; i < sourceBitmap.PixelHeight * stride;
      i += ((sourceBitmap.Format.BitsPerPixel + 7) / 8))
   {
      pixelData[i] = (byte)(pixelData[i] * -1);
      pixelData[i + 1] = (byte)(pixelData[i + 1] * -1);
      pixelData[i + 2] = (byte)(pixelData[i + 2] * -1);
   }

   // Mit den neuen Pixel-Daten ein neues Bild erzeugen 
   // und zurückgeben
   return BitmapSource.Create(
      sourceBitmap.PixelWidth, sourceBitmap.PixelHeight,
      sourceBitmap.DpiX, sourceBitmap.DpiY, PixelFormats.Bgra32,
      sourceBitmap.Palette, pixelData, stride);
}

Listing 53: Methode zur Erzeugung eines Bildnegativs unter WPF

285 Die einzelnen Pixel eines Bilds bearbeiten

Dieses Rezept habe ich (wie alle Bildverarbeitungs-Rezepte) um die Behandlung von WPF erweitert. Hier ist der WPF-Text:

In WPF setzen Sie vorwiegend die CopyPixels-Methode der BitmapSource-Klasse ein um die Pixeldaten in ein Byte-Array zu kopieren. Dieses Array verwenden Sie zur Bearbeitung der einzelnen Pixel. Nach der Bearbeitung erzeugen Sie mit diesen Rohdaten ein neues BitmapSource-Objekt.

Wenn Sie mit CopyPixels arbeiten, enthält das erzeugte Array die rohen Daten des Bilds. Das Format ist vom Pixelformat des Bildes abhängig. Da dieses variieren kann, sollten Sie das Bild, das Sie bearbeiten wollen, über eine Transformation in ein für die Bearbeitung sinnvolles Format konvertieren. Ich verwende das Bgra32-Format, das pro Pixel vier Byte verwaltet. Das erste Byte definiert den Blauanteil, das zweite den Grünanteil das dritte den Rotanteil und das vierte den Alpha-Wert (die Transparenz). Das Bgra32-Format ist das einzige, das (nur) ein Byte pro Farbe verwendet und einen Alpha-Kanal zur Verfügung stellt.

Zum Kompilieren des folgenden Beispiels müssen Sie die Namensräume System, System.Windows.Media und System.Windows.Media.Imaging importieren.

// Bild einlesen
string imageFileName = "C:\\Temp\\Les Crosets.jpg";
BitmapDecoder decoder = BitmapDecoder.Create(new Uri(imageFileName),
   BitmapCreateOptions.None, BitmapCacheOption.None);
   BitmapSource bitmapSource = decoder.Frames[0];

// Das Bild zunächst in das BGRA-Format konvertieren, 
// falls es ein anderes Format aufweist
if (bitmapSource.Format != PixelFormats.Bgra32)
{
   bitmapSource = new FormatConvertedBitmap(bitmapSource,
      PixelFormats.Bgra32, null, 0.0);
}

Listing 54: Einlesen und Konvertieren eines Bilds in das Bgra32-Format

Zum Auslesen der Pixel (und zum späteren Schreiben) müssen Sie die Schrittweite (englisch: Stride) des Bilds in den zu erzeugenden Rohdaten berechnen. Die Schrittweite ist die Anzahl der ganzen Bytes, die eine Zeile im Bild ausmachen. Ein Bild mit einer Auflösung von 32 Bit pro Pixel (bpp) und einer Breite von 100 Pixeln hat demnach eine (Mindest-)Schrittweite von 400 Byte. An der Schrittweite erkennen die Methoden zur Bildverarbeitung und -anzeige, wann eine neue Zeile in den hintereinander gehängten Bytes beginnt.

Die Schrittweite passt nicht immer genau zu den benötigten Bytes pro Zeile. Ein Bild mit einer Breite von elf Pixeln und einer Farbtiefe von vier Bits pro Pixel hat eine Schrittweite von theoretisch 44 Bits = 5,5 Bytes. Da das Byte die kleinste Speichereinheit im (Windows-)System ist, werden (zumindest) sechs Bytes pro Zeile verwendet. Die restlichen vier Bits pro Zeile bleiben einfach leer. In Bilddateien wird aus Performancegründen zusätzlich häufig dafür gesorgt, dass die Anzahl der Bytes pro Zeile ohne Rest durch vier teilbar ist. Im 4-bpp-Bild mit elf Pixeln Breite (6 Bytes) würden also z. B. an jede »Zeile« noch zwei leere Byte angehängt werden.

Wenn Sie selbst Bilder in Byte-Arrays speichern, müssen Sie die Schrittweite bestimmen, in der die Bilder im Array verwaltet werden. Der Grund liegt darin, dass Sie die Schrittweite beim Decodieren wieder angeben müssen, damit der BitmapDecoder weiß, wann eine Zeile aufhört. Die Schrittweite sollte natürlich so groß sein, dass eine Bildzeile hineinpasst.

Ich berechne die Schrittweite so, dass unabhängig vom gewählten Pixelformat genügend Platz bleibt:

int stride = bitmapSource.PixelWidth * 
   ((bitmapSource.Format.BitsPerPixel + 7) / 8);

In dem Zusammenhang verstehe ich ehrlich gesagt nicht, warum CopyPixels die Schrittweite nicht selbst bestimmt. Die Create-Methode der BitmapDecoder-Klasse, die Sie später verwenden, um aus dem Byte-Array wieder ein Bild zu erzeugen, könnte ebenfalls ohne die Schrittweite auskommen, weil diese aus der ebenfalls übergebenen Breite und der Farbtiefe berechnet werden kann. Vielleicht lag die Intention darin, dass Sie mit der Schrittweite auch Performance-Optimierungen vornehmen können (durch vier teilbare Bytezahl …).

Nun können Sie über CopyPixels die Pixel einlesen:

byte[] pixelData = new byte[stride * bitmapSource.PixelHeight];
bitmapSource.CopyPixels(pixelData, stride, 0);

Das letzte Argument gibt einen Offset an, der natürlich 0 ist wenn Sie das gesamte Bild einlesen wollen.

Dann können Sie das Array durchlaufen und die einzelnen Pixel bearbeiten. Dabei müssen Sie natürlich beachten, dass je nach gewähltem Pixelformat ein Pixel mehr oder weniger Bytes verwendet. In unserem Fall (Bgra32) wird ein Pixel in vier Byte definiert und Sie müssen demnach in einer Schleife um jeweils vier Byte weiterspringen. Um zum Pixelformat neutral zu bleiben, berechne ich die Sprungweite mit (bpp + 7) / 8.

Das Beispiel setzt jedes zweite Pixel auf die Farbe mit den Werten Rot = 0; Grün= 0, Blau = 50 und Alpha = 255:

for (int i = 0; i < bitmapSource.PixelHeight * stride;
   i += ((bitmapSource.Format.BitsPerPixel + 7) / 8))
{
   if ((i / 4) % 2 == 0)
   {
      pixelData[i] = 50;      // Blau
      pixelData[i + 1] = 0;   // Grün
      pixelData[i + 2] = 0;   // Rot
      pixelData[i + 3] = 255; // Alpha
   }
}

Listing 55: Bearbeiten der einzelnen Pixel

Schließlich erzeugen Sie aus den Rohdaten wieder ein BitmapSource-Objekt:

BitmapSource transformedBitmap = BitmapSource.Create(
   bitmapSource.PixelWidth, bitmapSource.PixelHeight,
   bitmapSource.DpiX, bitmapSource.DpiY, PixelFormats.Bgra32,
   bitmapSource.Palette, pixelData, stride);

286 Farb-Bilder in Graustufen-Bilder umwandeln

Dieses Rezept habe ich (wie alle Bildverarbeitungs-Rezepte) um die Behandlung von WPF erweitert. Hier ist der WPF-Text:

In WPF-Anwendungen können Sie einfach das Pixelformat eines Bildes in ein anderes transformieren um ein Graustufenbild zu erzeugen. Für solche bieten sich die Formate Gray16 und Gray32Float an, die 65.536 bzw.4 Millarden Grauschattierungen erlauben.

Zum Konvertieren in ein Graustufenbild erzeugen Sie eine neue Instanz der FormatConvertedBitmap-Klasse, der Sie am Konstruktor das zu konvertierende Bild, das neue Pixelformat, null am Argument destinationPalette und 0 am Argument alphaThreshold übergeben.

Das folgende Beispiel, das den Import der Namensräume System, System.Windows.Media und System.Windows.Media.Imaging erfordert, liest ein Bild ein und wandelt es auf diese Weise in ein Graustufenbild um.

// Bild einlesen
string imageFileName = "C:\\Bilder\\Les Crosets.jpg";
BitmapDecoder decoder = BitmapDecoder.Create(new Uri(imageFileName),
   BitmapCreateOptions.None, BitmapCacheOption.None);
BitmapSource bitmapSource = decoder.Frames[0];

// Das Bild in ein Graustufenbild umwandeln
FormatConvertedBitmap convertedBitmap = new FormatConvertedBitmap(
   bitmapSource, PixelFormats.Gray16, null, 0);

Listing 56: Konvertieren eines Bildes in ein Graustufenbild in WPF

In WPF können Sie natürlich (in meinem Fall leider nur theoretisch) auch in XAML dafür sorgen, dass ein Bild als Graustufenbild angezeigt wird:

<Image Height="100">
   <Image.Source>
      <FormatConvertedBitmap DestinationFormat="Gray16">
         <FormatConvertedBitmap.Source>
            <BitmapImage 
               UriSource="pack://siteOfOrigin:,,,/Les Crosets.jpg" />
         </FormatConvertedBitmap.Source>
      </FormatConvertedBitmap>
   </Image.Source>
</Image>

Listing 57: Anzeige eines konvertierten Bildes über die Deklaration in XAML

Obwohl das Beispiel funktionieren sollte, hatte ich (natürlich wieder …) Probleme. Visual Studio 2008 mochte die Deklaration nicht und meldete den (wirklich sehr aussagekräftigen …) Fehler »Der Wert liegt außerhalb des erwarteten Bereichs«. Versuche ohne FormatConvertedBitmap mit demselben Bild waren (teilweise) erfolgreich und einige sehr komische Dinge passierten während meines Testens. Nach dem Löschen der Zeilen, die FormatConvertedBitmap betrafen und dem nachfolgenden wieder rückgängig machen, zeigte Visual Studio auf einmal das in Graustufen umgewandelte Bild an. Daneben funktionierte die Angabe der Bild-Ressource in vielen Fällen auf keine der möglichen Arten (als relativ zum Anwendungsordner angegebener Dateiname wie im obigen Beispiel oder als Ressourcen-Angabe), wenn die Anwendung in einem Ordner mit einem langen Pfad lag. Dieses Problem könnte ich ja noch nachvollziehen, wenn das Bild als Datei (relativ zum Anwendungsordner) angegeben ist. Bei einer in die Anwendungs-Assembly integrierten Ressource sollten allerdings keine Probleme auftreten. Sehr eigenartig und nur sehr schwer nachzuvollziehen, besonders, da nach einem Visual-Studio-Neustart das Bild angezeigt wurde (wenn FormatConvertedBitmap nicht verwendet wurde) Da scheint noch einiges an Arbeit notwendig zu sein …

Auf die Abbildung der Beispielanwendung, die ein Farb-Bild auf diese Weise in ein Graustufen-Bild umwandelt, verzichte ich an dieser Stelle. Sie würden im Buch wohl keinen Unterschied erkennen (.

Zeichnen

289 Rechtecke mit abgerundeten Ecken zeichnen

Dieses Rezept habe ich um die Behandlung von WPF erweitert. Hier ist der WPF-Text:

In WPF können Sie komplexe Formen über PathGeometry- oder ein StreamGeometry-Objekt zeichnen. PathGeometry kann in XAML eingesetzt werden, StreamGeometry ist für den Einsatz im Programmcode optimiert, und kann auch nur dort eingesetzt werden. Der etwas irreführende Name der StreamGeometry-Klasse kommt übrigens daher, dass diese Klasse intern einen Stream zur Verwaltung ihrer Daten einsetzt (und deswegen weniger Speicher verbraucht als PathGeometry). StreamGeometry erlaubt über Methoden des StreamGeometryContext-Objekts, das Sie über die Open-Methode erhalten, die Definition eines Pfades, der in der Regel den Umriss einer Figur darstellt.

Da es in diesem Kapitel mehr um das programmatische Zeichnen geht (und nicht um die Deklaration von Figuren in XAML), konzentriere ich mich auf die StreamGeometry-Klasse. Zum Zeichnen erzeugen Sie zunächst eine Instanz dieser Klasse um holen dann über die Open-Methode einen StreamGeometryContext. Dieser erlaubt über die Methode BeginFigure den Beginn einer (neuen) Figur. Dabei geben Sie den ersten Punkt der Figur an. Über LineTo können Sie eine Linie hinzufügen, über ArcTo eine Kreissegment. Daneben existieren noch andere Methoden wie BeginFigure, BezierTo, PolyBezierTo und PolyLineTo. Alle Methoden beginnen die Definition des Teilpfades immer da, wo der letzte Teilpfad beendet wurde.

LineTo übergeben Sie am ersten Argument den Zielpunkt, ArcTo übergeben Sie den Zielpunkt, die Größe der Kurve (als Breite und Höhe am Argument size), einen Drehwinkel (rotationAngle), über den Sie die Krümmung einer ovalen Kurve bestimmen können (in unserem Fall nicht erforderlich), eine Information, ob es sich um einen Bogen mit mehr als 180 Grad handelt (isLargeArc) und eine Information darüber und ob der Bogen im Uhrzeigersinn gezeichnet wird (sweepDirection).

An den letzten beiden Argumenten übergeben Sie beiden Methoden eine Information darüber, ob die Linie bzw. Kurve tatsächlich als Line (später, über einen Pen) gezeichnet wird (isStroked) und ob die Linie/Kurve mit angrenzenden Linien/Kurven weich verknüpft wird (isSmoothJoin).

Ist der Pfad fertig definiert, können Sie das StreamGeometry-Objekt über die DrawGeometry-Methode des DrawingContext-Objekts, auf dem Sie die Zeichnung ausgeben wollen, zeichnen. Die Methode DrawRoundedRectangle im folgenden Listing nimmt Ihnen diese Arbeit ab.

Diese Methode erwartet an ihren Argumenten das DrawingContext-Objekt, auf dem gezeichnet werden soll, die Position und die Ausmaße des zu zeichnenden Rechtecks, den Radius der abgerundeten Ecke, einen Brush, der zum Füllen verwendet wird, und einen Pen, über den der Rahmen des Rechtecks gezeichnet wird. Für den Brush und den Pen können Sie auch null übergeben, wenn Sie nicht wollen, dass das Rechteck gefüllt bzw. dass der Rand gezeichnet wird.

Zum Kompilieren dieser Methode müssen Sie die Namensräume System.Windows und System.Windows.Media importieren.

public static void DrawRoundedRectangle(this DrawingContext dc, int x, int y, 
   int width, int height, int cornerRadius, Brush brush, Pen pen)
{
   // StreamGeometry-Instanz erzeugen, die die Geometrie der Form verwaltet
   // und deren StreamGeometryContext holen
   StreamGeometry geometry = new StreamGeometry();
   using (StreamGeometryContext sgc = geometry.Open())
   {
      // Size-Objekt für die Größe des Viertelkreises der Ecken berechnen
      Size cornerSize = new Size(cornerRadius, cornerRadius);

      // Figur starten
      sgc.BeginFigure(new Point(x + cornerRadius, y), true, true);

      // Linie oben hinzufügen
      sgc.LineTo(new Point(x + width - cornerRadius, y), true, true);

      // Ecke rechts oben hinzufügen
      sgc.ArcTo(new Point(x + width, y + cornerRadius), cornerSize, 
         0, false, SweepDirection.Clockwise, true, true);

      // Linie rechts hinzufügen
      sgc.LineTo(new Point(x + width, y + height - cornerRadius), true, true);

      // Ecke rechts unten hinzufügen
      sgc.ArcTo(new Point(x + width - cornerRadius, y + height), cornerSize,
         0, false, SweepDirection.Clockwise, true, true);

      // Linie unten hinzufügen
      sgc.LineTo(new Point(x + cornerRadius, y + height), true, true);

      // Ecke links unten hinzufügen
      sgc.ArcTo(new Point(x, y + height - cornerRadius), cornerSize, 
         0, false, SweepDirection.Clockwise, true, true);

      // Linie links hinzufügen
      sgc.LineTo(new Point(x, y + cornerRadius), true, true);

      // Ecke links oben hinzufügen
      sgc.ArcTo(new Point(x + cornerRadius, y), cornerSize, 
         0, false, SweepDirection.Clockwise, true, true);
   }

   // Das Geometry-Objekt zeichnen
   dc.DrawGeometry(brush, pen, geometry);
}

290 Pfeile zeichnen

Dieses Rezept zeichnet Pfeile nun viel schöner (. Und in der ganz neuen Version auch für WPF (.

Ich stelle hier lediglich die WPF-Variante der Methode DrawArrow vor. Die GDI-Variante arbeitet ähnlich und ist im Repository, in der Codebook-Klassenbibliothek und im Windows.Forms-Beispiel zu diesem Rezept zu finden. Die Argumente der WPF-Version sind die folgenden:

  • dc: Das DrawingContext-Objekt, auf dem gezeichnet werden soll
  • startPoint: Der Startpunkt des Pfeils
  • endPoint: Der Endpunkt des Pfeils (Ende der Pfeilspitze)
  • shaftWidth: Die Breite des Pfeilschafts
  • arrowHeadLength: Gibt die Länge der Pfeilspitze bis zum linken bzw. rechten Eckpunkt an
  • arrowHeadLengthFromShaft: Gibt die Länge der Pfeilspitze auf der Y-Linie an, die die Mitte des Pfeilschafts bildet
  • arrowHeadWidth: Gibt die Breite der Pfeilspitze am Anfang derselben an
  • arrowHeadMiddleWidth: Gibt die Breite der Pfeilspitze in der Mitte derselben an
  • brush: Der Brush, mit dem der Pfeil gefüllt werden soll. Kann null sein.
  • pen: Der Pen, der den Rand des Pfeils definiert. Kann null sein.

Ein Wert größer 0 in arrowHeadMiddleWidth führt dazu, dass die Seitenlinien der Pfeilspitze als Kurve gezeichnet werden. Bei einem Wert kleiner/gleich 0 werden diese Linien als Gerade gezeichnet.

DrawArrow benötigt den Import der Namensräume System, System.Windows und System.Windows.Media.

public static void DrawArrow(this DrawingContext dc, Point startPoint,
   Point endPoint, double shaftWidth, double arrowHeadLength,
   double arrowHeadLengthFromShaft, double arrowHeadWidth,
   double arrowHeadMiddleWidth, Brush brush, Pen pen)
{
   // Länge des Pfeils berechnen
   double x1 = Math.Abs(startPoint.X);
   double y1 = Math.Abs(startPoint.Y);
   double x2 = Math.Abs(endPoint.X);
   double y2 = Math.Abs(endPoint.Y);
   double a = x2 - x1;
   double b = y2 - y1;
   double arrowLength = Math.Sqrt(Math.Pow(a, 2) + Math.Pow(b, 2));

   // Der Pfeil wird virtuell so gezeichnet, dass die Spitze 
   // auf dem Punkt (0,0) liegt und der Anfang auf dem Punkt (0, Pfeillänge): 
   // Virtuellen Start- und Endpunkt berechnen
   double widthOffset = (shaftWidth / 2);
   Point virtualEndPoint = new Point(0, 0);
   Point virtualStartPoint = new Point(virtualEndPoint.X,
      virtualEndPoint.Y + arrowLength);
   Point virtualCenterPoint = new Point(virtualEndPoint.X,
      virtualEndPoint.Y + (int)(arrowLength / 2F));

   // Den Mittelpunkt des Pfeils ermitteln
   double xDiff = (endPoint.X - startPoint.X) / 2F;
   double yDiff = (endPoint.Y - startPoint.Y) / 2F;
   Point centerPoint = new Point(startPoint.X + xDiff, startPoint.Y + yDiff);

   // Der Pfeil wird beim Zeichnen so transformiert, dass er korrekt gedreht
   // und verschoben gezeichnet wird:

   // Den Rotationswinkel für die notwendige Drehung berechnen 
   Point rotationPoint = new Point(endPoint.X - centerPoint.X,
      endPoint.Y - centerPoint.Y);
   int angleOffset;
   // Grundrechnung: Tan(alpha) = b / a => alpha = Atan(b / a)
   if (rotationPoint.X >= 0 && rotationPoint.Y < 0)
   {
      // Erster Quadrant
      a = rotationPoint.X;
      b = rotationPoint.Y * -1;
      angleOffset = 90;
   }
   else if (rotationPoint.X >= 0 && rotationPoint.Y >= 0)
   {
      // Zweiter Quadrant
      a = rotationPoint.Y;
      b = rotationPoint.X;
      angleOffset = 180;
   }
   else if (rotationPoint.X < 0 && rotationPoint.Y >= 0)
   {
      // Dritter Quadrant
      a = rotationPoint.X * -1;
      b = rotationPoint.Y;
      angleOffset = 270;
   }
   else
   {
      // Vierter Quadrant
      b = rotationPoint.X * -1;
      a = rotationPoint.Y * -1;
      angleOffset = 360;
   }

   // Winkel im Bogenmaß berechnen
   double radian = Math.Atan(b / a);

   // Winkel in Grad umrechnen
   double rotationAngle = angleOffset - (radian * (180 / Math.PI));

   // Matrix erzeugen und die Rotation anwenden
   Matrix matrix = new Matrix();
   matrix.RotateAt(rotationAngle, virtualCenterPoint.X, virtualCenterPoint.Y);

   // Verschiebung berechnen und anwenden
   double offsetX = centerPoint.X - virtualCenterPoint.X;
   double offsetY = centerPoint.Y - virtualCenterPoint.Y;
   matrix.Translate(offsetX, offsetY);

   // Punkte für den Pfeil zusammenstellen
   Point[] arrowPoints;
   if (arrowHeadMiddleWidth > 0)
   {
      arrowPoints = new Point[10];
      // Linkes unteres Ende des Pfeilschafts
      arrowPoints[0] = new Point(widthOffset * -1, virtualStartPoint.Y);
      // Linkes oberes Ende des Pfeilschafts 
      arrowPoints[1] = new Point(arrowPoints[0].X, arrowHeadLength);
      // Linke untere Ecke der Pfeilspitze
      arrowPoints[2] = new Point((arrowHeadWidth / 2) * -1,
         arrowHeadLengthFromShaft);
      // Mitte der Pfeilspize links (für eine gerümmte Pfeilspitze)
      arrowPoints[3] = new Point(-(arrowHeadMiddleWidth / 2),
         arrowHeadLengthFromShaft / 2);
      // Pfeilspitze
      arrowPoints[4] = new Point(0, 0);
      // Mitte der Pfeilspitze rechts (für eine gekrümmte Pfeilspitze)
      arrowPoints[5] = new Point(arrowHeadMiddleWidth / 2,
         arrowHeadLengthFromShaft / 2);
      // Rechte untere Ecke der Pfeilspitze
      arrowPoints[6] = new Point(arrowHeadWidth / 2,
         arrowHeadLengthFromShaft);
      // Rechtes oberes Ende des Pfeilschafts
      arrowPoints[7] = new Point(arrowPoints[1].X + shaftWidth,
         arrowPoints[1].Y);
      // Rechtes unteres Ende des Pfeilschafts
      arrowPoints[8] = new Point(arrowPoints[0].X + shaftWidth,
         arrowPoints[0].Y);
   }
   else
   {
      arrowPoints = new Point[7];
      // Linkes unteres Ende des Pfeilschafts
      arrowPoints[0] = new Point(widthOffset * -1, virtualStartPoint.Y);
      // Linkes oberes Ende des Pfeilschafts 
      arrowPoints[1] = new Point(arrowPoints[0].X, arrowHeadLength);
      // Linke untere Ecke der Pfeilspitze
      arrowPoints[2] = new Point((arrowHeadWidth / 2) * -1,
         arrowHeadLengthFromShaft);
      // Pfeilspitze
      arrowPoints[3] = new Point(0, 0);
      // Rechte untere Ecke der Pfeilspitze
      arrowPoints[4] = new Point(arrowHeadWidth / 2,
         arrowHeadLengthFromShaft);
      // Rechtes oberes Ende des Pfeilschafts
      arrowPoints[5] = new Point(arrowPoints[1].X + shaftWidth,
         arrowPoints[1].Y);
      // Rechtes unteres Ende des Pfeilschafts
      arrowPoints[6] = new Point(arrowPoints[0].X + shaftWidth,
         arrowPoints[0].Y);
   }

   // StreamGeometry-Instanz erzeugen, die die Geometrie der Form verwaltet
   // und deren StreamGeometryContext holen
   StreamGeometry geometry = new StreamGeometry();
   using (StreamGeometryContext sgc = geometry.Open())
   {
      sgc.BeginFigure(arrowPoints[0], true, true);
      if (arrowHeadMiddleWidth > 0)
      {
         sgc.LineTo(arrowPoints[1], true, true);
         sgc.LineTo(arrowPoints[2], true, true);
         sgc.BezierTo(arrowPoints[2], arrowPoints[3], arrowPoints[4],
            true, true);
         sgc.BezierTo(arrowPoints[4], arrowPoints[5], arrowPoints[6],
            true, true);
         sgc.LineTo(arrowPoints[7], true, true);
         sgc.LineTo(arrowPoints[8], true, true);
      }
      else
      {
         // Aus den Punkten einen Pfad mit gerader Pfeilspitze bilden
         sgc.PolyLineTo(arrowPoints, true, true);
      }
   }

   // Die Transformation anwenden
   geometry.Transform = new MatrixTransform(matrix);

   // Das Geometry-Objekt zeichnen
   dc.DrawGeometry(brush, pen, geometry);
}

Listing 58: Methode zum Zeichnen eines Pfeils unter WPF

Abbildung 1 zeigt einige mit DrawArrow (über Zufallswerte) gezeichnete Pfeile.

Abbildung 1: Einige mit DrawArrow gezeichnete eigene Pfeile

291 Transparente Bilder und Grafiken erzeugen

Dieses Rezept habe ich um die Behandlung von WPF erweitert. Hier ist der WPF-Text:

In WPF können Sie zum transparenten Zeichnen von Bildern leider keine Transformation verwenden. Sie können aber die Pixel des Bildes einzeln bearbeiten, um die Transparenz jedes einzelnen Pixels zu bestimmen. Listing 59 verwendet diese Technik, um den Hintergrund eines eingelesenen Bildes voll- und die restlichen Pixel halb-transparent zu definieren und das Bild schließlich zu zeichnen.

Das Beispiel läuft in einem WPF-Fenster, das ein Image-Steuerelement enthält. Die Quelle des Steuerelements ist auf eine Ressource der Anwendung gelegt, die Moon.jpg heißt (und die ein Bild des Mondes ist). Das Beispiel erfordert den Import der Namensräume System, System.Threading, System.Windows, System.Windows.Media und System.Windows.Media.Imaging.

public partial class StartWindow : Window
{
   /* DrawingVisual auf dem gezeichnet werden kann */
   private DrawingVisual drawingVisual = new DrawingVisual();

   /* Konstruktor. Erzeugt die Steuerelemente und Komponenten und 
      die Zeichnung in dem DrawingVisual. */
   public StartWindow()
   {
      InitializeComponent();

      // Den DrawingContext für den DrawingVisual holen
      using (DrawingContext dc = this.drawingVisual.RenderOpen())
      {
         // Halb-transparentes Rechteck mit Text zeichnen
         dc.DrawRectangle(new SolidColorBrush(Color.FromArgb(80, 0, 0, 30)),
            null, new Rect(20, 40, this.Width - 40, 40));
         FormattedText formattedText = new FormattedText("Keine Panik",
            Thread.CurrentThread.CurrentCulture, FlowDirection.LeftToRight,
            new Typeface("Tahoma"), 28, new SolidColorBrush(
            Color.FromArgb(150, 0, 0, 255)));
         double x = (this.Width - formattedText.Width) / 2;
         double y = 45;
         dc.DrawText(formattedText, new Point(x, y));

         // Bild einlesen
         BitmapDecoder decoder = BitmapDecoder.Create(
            new Uri("Hitchhiker.gif", UriKind.Relative),
            BitmapCreateOptions.None, BitmapCacheOption.None);
         BitmapSource bitmapSource = decoder.Frames[0];

         // Das Bild so umwandeln, dass die Farbe des ersten Pixels 
         // voll-transparent und der Rest des Bildes halb-transparent 
         // erscheint
         if (bitmapSource.Format != PixelFormats.Bgra32)
         {
            bitmapSource = new FormatConvertedBitmap(bitmapSource,
               PixelFormats.Bgra32, null, 0.0);
         }
         int stride = bitmapSource.PixelWidth *
            ((bitmapSource.Format.BitsPerPixel + 7) / 8);
         byte[] pixelData = new byte[stride * bitmapSource.PixelHeight];
         bitmapSource.CopyPixels(pixelData, stride, 0);
         byte firstPixelBlue = pixelData[0];
         byte firstPixelGreen = pixelData[1];
         byte firstPixelRed = pixelData[2];
         for (int i = 0; i < bitmapSource.PixelHeight * stride;
            i += ((bitmapSource.Format.BitsPerPixel + 7) / 8))
         {
            if (pixelData[i] == firstPixelBlue &&
               pixelData[i + 1] == firstPixelGreen &&
               pixelData[i + 2] == firstPixelRed)
            {
               // Den Alpha-Wert des Pixels auf 0 setzen
               pixelData[i + 3] = 0;
            }
            else
            {
               // Den Alpha-Wert des Pixels auf 160 setzen
               pixelData[i + 3] = 160;
            }
         }

         // Mit den neuen Pixel-Daten ein neues Bild erzeugen 
         BitmapSource transformedBitmap = BitmapSource.Create(
            bitmapSource.PixelWidth, bitmapSource.PixelHeight,
            bitmapSource.DpiX, bitmapSource.DpiY, PixelFormats.Bgra32,
            bitmapSource.Palette, pixelData, stride);

         // Das transformierte Bild zeichnen
         Rect bitmapRect = new Rect((this.Width - 20 -
           transformedBitmap.Width) / 2, 55, transformedBitmap.Width,
           transformedBitmap.Height);
         dc.DrawImage(transformedBitmap, bitmapRect);
      }

      // Den DrawingVisual dem Fenster als visuelles und logisches 
      // Child-Element hinzufügen
      this.AddVisualChild(this.drawingVisual);
      this.AddLogicalChild(this.drawingVisual);
   }

   /* Wird überschrieben, um die Anzahl der visuellen Kind-Elemente 
      um das eigene zu erhöhen */
   protected override int VisualChildrenCount
   {
      get
      {
         return base.VisualChildrenCount + 1;
      }
   }

   /* Wird überschrieben, um das eigene visuelle Kind-Element zurückzugeben */
   protected override Visual GetVisualChild(int index)
   {
      if (index < base.VisualChildrenCount)
      {
         return base.GetVisualChild(index);
      }
      else
      {
         return this.drawingVisual;
      }
   }
}

Listing 59: Transparentes Zeichnen in einem WPF-Fenster

292 Bilder mit Schatten zeichnen

Dieses Rezept habe ich (wie die meisten Zeichnen-Rezepte) um die Behandlung von WPF erweitert. Hier ist der WPF-Text:

In WPF können Sie jedem UI-Element (allen Klassen, die von UIElement abgeleitet sind), über die Eigenschaft BitmapEffect (u. a.) einen Schatten zuordnen, indem Sie ein DropShadowBitmapEffect-Objekt in diese Eigenschaft schreiben. Das geht natürlich direkt in XAML:

<Image Margin="10,10,83,123" Name="image1" Stretch="None">
    <Image.Source>
        <BitmapImage UriSource="Hitchhiker.jpg"/>
    </Image.Source>
    <Image.BitmapEffect>
        <DropShadowBitmapEffect Color="Black" Direction="310" Noise="0.1"
           Opacity="0.7" ShadowDepth="10" Softness="0.5" />
    </Image.BitmapEffect>
</Image>

Listing 60: Image-Steuerelement mit Schatten-Effekt

ber die verschiedenen Eigenschaften der DropShadowBitmapEffect-Klasse können Sie den Schatten beeinflussen:

Sie können in WPF aber auch mit Schatten zeichnen. Dazu können Sie zum einen dem DrawingVisual-Objekt, auf dem Sie zeichnen, über dessen BitmapEffect-Eigenschaft eine Instanz der DropShadowBitmapEffect-Klasse zuweisen, die Sie Ihren Anforderungen entsprechend initialisiert haben. Zum anderen können Sie dem DrawingContext über dessen PushEffect-Methode eine DropShadowBitmapEffect-Instanz zuweisen. Effekte, die über PushEffect hinzugefügt wurden, bleiben für alle folgenden Zeichenoperationen erhalten, bis die Pop-Methode aufgerufen wird, die Effekte (und Transformationen) wieder entfernt.

Das folgende Beispiel zeigt das Zeichnen eines Bildes, das als Ressource in der Anwendung gespeichert ist, an dem kompletten Quellcode eines (leeren) Fensters. Der wesentliche Programmcode ist das Erzeugen, Initialisieren und Zuweisen des DropShadowBitmapEffect beim Zeichnen im Konstruktor.

Das Beispiel erfordert, dass eine Bild-Ressource mit dem Namen Hitchhiker.jpg in der Anwendung gespeichert ist. Es erfordert außerdem den Import der Namensräume System, System.Windows, System.Windows.Media, System.Windows.Media.Effects und System.Windows.Media.Imaging.

public partial class StartWindow : Window
{
   /* DrawingVisual erzeugen, auf dem gezeichnet werden kann */
   private DrawingVisual drawingVisual = new DrawingVisual();

   /* Konstruktor. Erzeugt die Steuerelemente und Komponenten. */
   public StartWindow()
   {
      InitializeComponent();

      // Den DrawingContext für den DrawingVisual holen
      using (DrawingContext dc = this.drawingVisual.RenderOpen())
      {
         // Das Bild aus der Ressource lesen
         BitmapDecoder decoder = BitmapDecoder.Create(
            new Uri("pack://application:,,,/Hitchhiker.jpg"),
            BitmapCreateOptions.None, BitmapCacheOption.None);
         BitmapSource bitmapSource = decoder.Frames[0];

         // Den DropShadowBitmapEffect erzeugen und initialisieren
         DropShadowBitmapEffect effect = new DropShadowBitmapEffect {
            Direction = 310, Noise = 0.1, Opacity = 0.7, ShadowDepth = 10,
            Softness = 0.5 };

         // Den DropShadowBitmapEffect dem DrawingContext hinzufügen
         dc.PushEffect(effect, null);

         // Das Bild zeichnen
         dc.DrawImage(bitmapSource, new Rect(200, 10,
            bitmapSource.Width, bitmapSource.Height));

         // Den Effekt wieder entfernen
         dc.Pop();
      }

      // Den DrawingVisual dem Fenster als visuelles und logisches 
      // Child-Element hinzufügen
      this.AddVisualChild(this.drawingVisual);
      this.AddLogicalChild(this.drawingVisual);
   }

   /* Wird überschrieben, um die Anzahl der visuellen Kind-Elemente 
      um das eigene zu erhöhen  */
   protected override int VisualChildrenCount
   {
      get
      {
         return base.VisualChildrenCount + 1;
      }
   }

   /* Wird überschrieben, um das eigene visuelle Kind-Element zurückzugeben */
   protected override Visual GetVisualChild(int index)
   {
      if (index < base.VisualChildrenCount)
      {
         return base.GetVisualChild(index);
      }
      else
      {
         return this.drawingVisual;
      }
   }
}

Listing 61: Zeichnen eines Bildes mit Schatten-Effekt in WPF

293 Schräg zeichnen und Zeichenobjekte rotieren

Dieses Rezept habe ich (wie die meisten Zeichnen-Rezepte) um die Behandlung von WPF erweitert. Hier ist der WPF-Text:

Das schräge Zeichnen (bzw. in WPF auch das schräge Ausgeben) von (Zeichen-)Objekten ist über eine Transformation recht einfach. Eine Transformation habe ich bereits beim Zeichnen eines Pfeils. Eine Transformation bewirkt, dass die Originalkoordinaten eines zu zeichnenden Objekts vor den Ausgaben transformiert werden. Sie können eine Verschiebung (in X/Y-Richtung), eine Rotation in einem Drehwinkel, eine Skalierung und ein Kippen des Objekts erreichen.

WPF

In WPF können Sie wie auch beim Anwenden von Effekten Transformationen auf allen UI-Elementen über deren LayoutTransform-Eigenschaft anwenden. Sie können Transformationen aber auch beim Zeichnen anwenden, wie ich dies in Rezept 290 gemacht habe, um den gezeichneten Pfeil zu drehen und zu verschieben.

Zum Transformieren stehen Ihnen die folgenden Klassen (aus dem Namensraum System.Windows.Media) zur Verfügung:

In XAML können Sie Transformationen über die LayoutTransform-Eigenschaft auch deklarativ anwenden. Das folgende Beispiel deklariert ein Label, das um 270 Grad verdreht wird, ein Rechteck, das um 30 Grad gedreht wird und ein Rechteck, das um 30 Grad in X-Richtung gekippt und auf den Faktor 0,8 skaliert wird:

<!-- Ein um 270 Grad gedrehtes Label -->
<Label Height="28" HorizontalAlignment="Left" Margin="10,10,0,0" 
  Name="rotateTransformDemoLabel" VerticalAlignment="Top"  
  Content="Das ist ein in XAML gedrehter Text">
    <Label.LayoutTransform>
        <RotateTransform Angle="270"/>
    </Label.LayoutTransform>
</Label>

<!-- Ein um 30 Grad gedrehtes Rechteck -->
<Rectangle Width="100" Height="100" Fill="Red" Stroke="Navy" HorizontalAlignment="Left" 
   Margin="50,15,0,0" VerticalAlignment="Top">
    <Rectangle.LayoutTransform>
        <RotateTransform Angle="30"/>
    </Rectangle.LayoutTransform>
</Rectangle>

<!-- Ein um 30 Grad in X-Richtung gekipptes Rechteck -->
<Rectangle Width="100" Height="100" Fill="Red" Stroke="Navy" HorizontalAlignment="Left" 
   Margin="200,15,0,0" VerticalAlignment="Top">
    <Rectangle.LayoutTransform>
        <TransformGroup>
            <!-- Kipp-Transformation -->
            <SkewTransform AngleX="30"/>
            <!-- Skalierungs-Transformation -->
            <ScaleTransform ScaleX="0.8" ScaleY="0.8"/>
        </TransformGroup>
    </Rectangle.LayoutTransform>
</Rectangle>

Listing 62: Transformationen von UI-Elementen in XAML

Das Ergebnis lässt sich am besten im Designer beurteilen, weil dieser die Fläche, die die Objekte ohne Transformation ausfüllen würden, zusätzlich zu den transformierten Objekten darstellt (Abbildung 2).

Abbildung 2: Das XAML-Transformations-Beispiel im Designer

Transformationen können Sie natürlich auch beim eigenen Zeichnen anwenden. Dazu können Sie einmal die Transform-Eigenschaft des DrawingVisual mit einer Transformations-Instanz (oder mehreren in einer TransformGroup-Instanz) belegen um den gesamten Visual (auf dem Sie zeichnen) zu transformieren.

Wenn Sie beim Zeichnen allerdings dynamisch transformieren wollen, setzen Sie idealerweise die PushTransform-Methode des DrawingContext ein, auf dem Sie zeichnen. Diese Methode können Sie mehrfach aufrufen um mehrere Transformationen zu definieren. Diese Transformationen werden auf alle nachfolgenden Zeichenoperationen angewendet, bis Sie Transformationen über die Pop-Methode wieder entfernen! So können Sie recht einfach Objekte verdrehen, skalieren, kippen und verschieben.

Beim Verdrehen und Kippen von Objekten ist der Rotationsmittelpunkt wichtig. Dieser befindet sich per Voreinstellung an der Position 0,0, also der linken oberen Ecke des DrawingVisual, auf dem Sie zeichnen. Objekte werden aber meist um ihre linke obere Ecke oder um ihren Mittelpunkt gedreht oder gekippt. Deshalb sollten Sie den Drehpunkt über die Eigenschaften CenterX und CenterY des Transformationsobjekts (über den Konstruktor) entsprechend festlegen.

Das folgende Beispiel zeichnet zunächst einen Text um 45 Grad gedreht. Als Drehpunkt wird die linke obere Ecke des Textes verwendet. Danach wird ein Rechteck um 45 Grad gedreht gezeichnet, dessen Drehpunkt auf die Mitte des Rechtecks gelegt wird. Schließlich wird dasselbe Rechteck noch einmal normal (nicht gedreht) ausgegeben.

Das Programm läuft in einem WPF-Fenster, das ein privates Feld drawingVisual vom Typ DrawingVisual enthält (und das entsprechend zum Zeichnen eingerichtet ist, siehe in Listing 61).

// Den DrawingContext für den DrawingVisual holen
using (DrawingContext dc = this.drawingVisual.RenderOpen())
{
   // Einen Text um 45 Grad verdreht ausgeben. Der Drehpunkt
   // wird dabei auf den Startpunkt des Textes gesetzt.
   FormattedText formattedText = new FormattedText(
      "Das ist selbst gezeichneter, gedrehter Text",
      Thread.CurrentThread.CurrentCulture, FlowDirection.LeftToRight,
      new Typeface("Tahoma"), 14, Brushes.Black);
   dc.PushTransform(new RotateTransform(45, 350, 10));
   dc.DrawText(formattedText, new Point(350, 10));
   dc.Pop(); // Transformationen und Effekte wieder entfernen

   // Ein Rechteck um 45 Grad nach rechts gekippt zeichnen.
   // Als Drekpunkt wird die Mitte des Rechtecks angegeben.
   dc.PushTransform(new RotateTransform(45, 455, 105));
   dc.DrawRectangle(Brushes.Navy, new Pen(Brushes.Red, 2),
      new Rect(450, 100, 10, 10));
   dc.Pop(); // Transformationen und Effekte wieder entfernen

   // Normales Rechteck zeichnen
   dc.DrawRectangle(Brushes.Navy, new Pen(Brushes.Red, 2),
      new Rect(450, 100, 10, 10));
}

Listing 63: Transformationen beim Zeichnen über PushTransform

Damit sind die Möglichkeiten aber noch nicht erschöpft, denn Sie können auch einige Objekte, die Sie zum Zeichnen verwenden, wie z. B. ein Geometry-Objekt, über deren Transform-Eigenschaft separat transformieren. Andere Typen, wie z. B. die Rect-Struktur, besitzen die Möglichkeit, über ein Matrix-Objekt transformiert zu werden. Ein Matrix-Objekt verwaltet die Daten einer oder mehrerer Transformationen. Über die Methode Translate können Sie eine Verschiebung in X/Y-Richtung bewirken, die Methoden Rotate und RotateAt führen zu einer Rotation. Rotate dreht dabei um die linke obere Ecke, bei RotateAt können Sie den Drehpunkt bestimmen. Ein Matrix-Objekt habe ich mit einer MatrixTransformation auf einem StreamGeometry-Objekt in meinem Pfeil-Rezept eingesetzt. Vergleichen Sie dieses um zu erfahren, wie Sie dies programmieren.

294 Den Drehpunkt eines Rechtecks so ermitteln, dass die Ecke links oben an derselben Position bleibt

Dieses Rezept habe ich (wie die meisten Zeichnen-Rezepte) um die Behandlung von WPF erweitert. Hier ist diee WPF-Version meiner Methode mit dem langen Namen:

public static Point 
   GetCenterOfRotationForRotationWithRetentionOfUpperLeftAngle(
   Rect rect, bool rotateClockwise)
{
   // Rotier-Punkt berechnen
   double offset = 0;
   if (rotateClockwise)
   {
      offset = rect.Height / 2;
   }
   else
   {
      offset = rect.Width / 2;
   }

   // Ergebnis zurückgeben
   return new Point(rect.X + offset, rect.Y + offset);
}

Listing 64: WPF-Version der Methode zur Berechnung des Rotationspunkts eines Rechtecks für eine Rotation um 90 Grad unter Beibehaltung der linken oberen Ecke

Die Beispiele zu diesem Rezept erläutern die Anwendung dieser Methode.

295 Text an einer definierten Position in 90-Grad-Schritten gedreht ausgeben

Dieses Rezept habe ich (wie die meisten Zeichnen-Rezepte) um die Behandlung von WPF erweitert. Hier ist der komplette neue Text:

Text gedreht an einer bestimmten Position auszugeben ist gar nicht so einfach. Das Problem ist, dass Sie den Drehpunkt korrekt berechnen müssen (vergleichen Sie dazu das Rezept 294). Ich habe allerdings eine Lösung für das Drehen von Text in 90-Grad-Schritten gefunden. Die (Erweiterungs-)Methode DrawRotatedText in Listing 65 berechnet dazu zuerst das Rechteck, das den normal gezeichneten Text umfasst. Für dieses Rechteck ermittelt DrawRotatedText dann nach der Technik aus Rezept 294 den Drehpunkt, der das Rechteck so drehen würde, dass die linke, obere Ecke bestehen bleibt. Bei einer 180-Grad-Drehung ist der Drehpunkt allerdings einfach die Mitte des Rechtecks. Dieser Drehpunkt wird dann für eine Transformation verwendet, bevor der Text gezeichnet wird. Listing 65 zeigt die WPF-Version von DrawRotatedText. Die GDI-Version finden Sie im GDI-Beispiel zu diesem Rezept, in der Codebook-Klassenbibliothek und im Repository.

Zum Kompilieren des Quelltexts müssen Sie die Namensräume System.Windows, System.Windows.Media und System.Windows.Media.Imaging einbinden.

public static void DrawRotatedText(this DrawingContext dc,
   FormattedText formattedText, Point origin, Rotation rotation)
{
   // Rechteck berechnen, das den Text umfasst
   Rect textRect = new Rect(origin.X, origin.Y, formattedText.Width,
      formattedText.Height);

   // Drehpunkt so berechnen, dass das Rechteck 
   // an der angegebenen Position bleibt
   Point rotationPoint = new Point();
   float rotationAngle = 0;
   double offset = 0;
   switch (rotation)
   {
      case Rotation.Rotate0:
         // Den Text direkt ausgeben
         dc.DrawText(formattedText, origin);
         return;

      case Rotation.Rotate90:
         offset = textRect.Height / 2;
         rotationPoint.X = textRect.X + offset;
         rotationPoint.Y = textRect.Y + offset;
         rotationAngle = 90;
         break;

      case Rotation.Rotate180:
         // Bei einer Drehung um 180 Grad liegt der Drehpunkt in der 
         // Mitte des Rechtecks
         rotationPoint.X = textRect.X + (textRect.Width / 2);
         rotationPoint.Y = textRect.Y + (textRect.Height / 2);
         rotationAngle = 180;
         break;

      case Rotation.Rotate270:
         offset = textRect.Width / 2;
         rotationPoint.X = textRect.X + offset;
         rotationPoint.Y = textRect.Y + offset;
         rotationAngle = 270;
         break;
   }

   // Text gedreht ausgeben
   dc.PushTransform(new RotateTransform(rotationAngle,
      rotationPoint.X, rotationPoint.Y));
   dc.DrawText(formattedText, origin);
   dc.Pop();
}

Listing 65: Methode zum gedrehten Zeichnen von Text unter WPF

296 Die Breite und Höhe eines auszugebenden Textes bestimmen

Dieses Rezept habe ich (wie die meisten Zeichnen-Rezepte) um die Behandlung von WPF erweitert. Hier ist der WPF-Text:

In WPF ist diese Aufgabe einfach zu lösen, da Sie zum Zeichnen von Texten ein FormattedText-Objekt verwenden. Die Eigenschaft Width liefert die Breite des Textes, die Eigenschaft Height die Höhe. Damit können Sie z. B. mittig auf einem Fenster zeichnen.

Das folgende Beispiel läuft in einem WPF-Fenster, das ein privates Feld drawingVisual vom Typ DrawingVisual enthält (und das entsprechend zum Zeichnen eingerichtet ist, siehe in Listing 61 in Rezept 292).

// Den DrawingContext für den DrawingVisual holen
using (DrawingContext dc = this.drawingVisual.RenderOpen())
{
   // Einen Text zentriert ausgeben
   FormattedText formattedText = new FormattedText("Zentrierter Text",
      Thread.CurrentThread.CurrentCulture, FlowDirection.LeftToRight,
      new Typeface("Tahoma"), 14, Brushes.Black);
   double x = (this.Width - SystemParameters.ResizeFrameVerticalBorderWidth -
      formattedText.Width) / 2;
   double y = (this.Height - SystemParameters.WindowCaptionHeight - 
      SystemParameters.ResizeFrameHorizontalBorderHeight - 
      formattedText.Height) / 2;
   dc.DrawText(formattedText, new Point(x, y));
}

Listing 66: Mittiges Zeichnen auf einem WPF-Fenster

297 Texte zentriert oder rechtsbündig zeichnen

Dieses Rezept habe ich (wie die meisten Zeichnen-Rezepte) um die Behandlung von WPF erweitert. Hier ist der WPF-Text:

In einer WPF-Anwendung setzen Sie ein FormattedText-Objekt zum Zeichnen von Texten ein. Dieses können Sie über die Eigenschaft TextAlignment so einstellen, dass der Text linksbündig (TextAlignment.Left, Voreinstellung), rechtsbündig (TextAlignment.Right), zentriert (TextAlignment.Center) oder im Blocksatz (TextAlignment.Justify) erscheint.

Für die Ausrichtung des Textes ist das Rechteck wichtig, in dem der Text ausgegeben wird. Die X- und Y-Position wird durch den Punkt definiert, den Sie beim Zeichnen spezifizieren. Die Breite und Höhe des virtuellen Rechtecks ist zunächst auf unendlich eingestellt. Ein Text wird also nach rechts bzw. (bei rechtsbündigen Texten) nach links auch über das Fenster hinaus gezeichnet. Sie können die Breite und Höhe des Rechtecks aber über die Eigenschaften MaxTextWidth und MaxTextHeight bestimmen. Der Wert 0 (Voreinstellung) steht für eine unendliche Breite bzw. Höhe. Wenn der Text nicht mehr in das so definierte Rechteck passt, bestimmt übrigens die Eigenschaft Trimming, wie der Text abgeschnitten wird. Die Voreinstellung TextTrimming.WordEllipsis bewirkt, dass am Ende des noch passenden Textes an einer Wortgrenze drei Punkte gezeichnet werden.

Listing 67 zeigt nun, die Sie in einem WPF-Fenster Text normal, zentriert und rechtsbündig ausgeben. Der wesentliche Trick dabei ist die Berechnung der inneren Fensterbreite und -höhe, für die ich keine andere Lösung gefunden habe, als die Breite des Fenster-Randes, und bei der Höhe zusätzlich noch die Höhe des Fenster-Titels von der Gesamtbreite bzw. -höhe des Fensters abzuziehen.

Das Beispiel läuft in einem WPF-Fenster, das ein privates Feld drawingVisual vom Typ DrawingVisual enthält (und das entsprechend zum Zeichnen eingerichtet ist).

// Den DrawingContext für den DrawingVisual holen
using (DrawingContext dc = this.drawingVisual.RenderOpen())
{
   // Text normal ausgeben
   const int xMargin = 10;
   FormattedText formattedText = new FormattedText("Beispiel-Text, " +
      "der normal ausgegeben wird", Thread.CurrentThread.CurrentCulture,
      FlowDirection.LeftToRight, new Typeface("Tahoma"), 12,
      Brushes.Black);
   formattedText.MaxTextWidth = this.Width -
      SystemParameters.ThickVerticalBorderWidth - (xMargin * 2);
   dc.DrawText(formattedText, new Point(xMargin, 10));

   // Text zentriert und mittig ausgeben
   formattedText = new FormattedText("Beispiel-Text," +
      Environment.NewLine + "der auf dem Formular horizontal und " +
      "vertikal zentriert ausgegeben wird",
      Thread.CurrentThread.CurrentCulture, FlowDirection.LeftToRight,
      new Typeface("Tahoma"), 12, Brushes.Black);
   formattedText.TextAlignment = TextAlignment.Center;
   formattedText.MaxTextWidth = this.Width -
      SystemParameters.ThickVerticalBorderWidth - (xMargin * 2);
   double x = (this.Width - SystemParameters.FixedFrameVerticalBorderWidth -
      formattedText.Width) / 2;
   double y = (this.Height - SystemParameters.WindowCaptionHeight -
      SystemParameters.FixedFrameHorizontalBorderHeight -
      formattedText.Height) / 2;
   dc.DrawText(formattedText, new Point(x, y));

   // Text rechtsbündig ausgeben
   formattedText = new FormattedText("Beispiel-Text," +
      Environment.NewLine + "der an einer definierten Y-Position " +
      "auf dem Fenster rechtsbündig " + Environment.NewLine +
      "mit einem kleinen Rand ausgegeben wird",
      Thread.CurrentThread.CurrentCulture, FlowDirection.LeftToRight,
      new Typeface("Tahoma"), 12, Brushes.Black);
   formattedText.TextAlignment = TextAlignment.Right;
   formattedText.MaxTextWidth = this.Width -
      SystemParameters.ThickVerticalBorderWidth - (xMargin * 2);
   x = 10;
   y = this.Height - SystemParameters.WindowCaptionHeight -
      SystemParameters.ThickHorizontalBorderHeight - 55;
   dc.DrawText(formattedText, new Point(x, y));
}

Listing 67: Normales, rechtsbündiges und zentriertes Zeichnen eines Textes in einem WPF-Fenster

298 Strings beim Zeichnen wortgerecht umbrechen

Dieses Rezept habe ich (wie die meisten Zeichnen-Rezepte) um die Behandlung von WPF erweitert. Hier ist der WPF-Text:

In WPF-Anwendungen definieren Sie ein Rechteck, in dem Text ausgegeben werden soll, über die X- und Y-Position, an der Sie den Text ausgeben, und über die Eigenschaften MaxTextWidth und MaxTextHeight des FormattedText-Objekts, das Sie zum Zeichnen verwenden. Diese Eigenschaften sind per Voreinstellung mit 0 definiert, was für eine unendliche Breite bzw. Höhe steht. Die Eigenschaft Trimming bestimmt, wie der Text abgeschnitten wird wenn er nicht komplett in das Rechteck passt.

Das folgende Beispiel demonstriert den Umgang mit diesen Eigenschaften an einem längeren Text, der mit etwas Rand in einem Fenster ausgegeben wird. Das Beispiel erfordert ein WPF-Fenster, das ein privates Feld drawingVisual vom Typ DrawingVisual enthält (und das entsprechend zum Zeichnen eingerichtet ist, siehe in Listing 61 in Rezept 292).

// Den DrawingContext für den DrawingVisual holen
using (DrawingContext dc = this.drawingVisual.RenderOpen())
{
   // Die Position und die Maße des Text-Rechtecks bestimmen
   const double x = 30;
   const double y = 30;
   double textRectangleWidth = this.Width - 
      SystemParameters.FixedFrameVerticalBorderWidth - (2 * x);
   double textRectangleHeight = this.Height - 
      SystemParameters.WindowCaptionHeight -
      SystemParameters.FixedFrameHorizontalBorderHeight - (2 * y);

   // FormattedText-Instanz für den Text erzeugen
   FormattedText formattedText = new FormattedText("Weit draußen in den " +
      "unerforschten Einoden eines total aus der Mode gekommenen " +
      "Ausläufers des westlichen Spiralarms der Galaxis leuchtet " +
      "unbeachtet eine kleine gelbe Sonne. Um sie kreist in einer " +
      "Entfernung von ungefähr achtundneunzig Millionen Meilen ein " +
      "absolut unbedeutender, kleiner blaugrüner Planet, dessen vom " +
      "Affen stammende Bioformen so erstaunlich primitiv sind, dass " +
      "sie Digitaluhren noch immer für eine unwahrscheinlich tolle " +
      "Erfindung halten.",
      Thread.CurrentThread.CurrentCulture, FlowDirection.LeftToRight,
      new Typeface("Tahoma"), 14, Brushes.Black);
   
   // Die Maximalbreite und -höhe definieren
   formattedText.MaxTextWidth = textRectangleWidth;
   formattedText.MaxTextHeight = textRectangleHeight;

   // Die Art des Abschneidens (auf den Defaultwert) festlegen
   formattedText.Trimming = TextTrimming.WordEllipsis;

   // Den Text zeichnen
   dc.DrawText(formattedText, new Point(x, y));
}

Listing 68: Zeichnen eines Textes mit wortgerechtem Umbruch in WPF

Reflection und Serialisierung

310 Objekte nach XML serialisieren und von XML deserialisieren

In den Methoden zum Serialisieren eines Objekts in einen XML-String und zum Deserialisieren eines XML-String in ein Objekt habe ich einen Denkfehler eingebaut. Diesen Methoden konnte die Codierung übergeben werden, was aber beim Serialisieren/Deserialisieren eines (Unicode-)Strings keinen Sinn macht bzw. sogar zu Fehlern führt. Außerdem kann es auch dann zu einem Fehler beim Deserialisieren, wenn beim Serialisieren die Unicode-Codierung angegeben wurde. Der XmlSerializer hat in diesem Fall in der im Codebook gedruckten Implementierung der SerializeToXmlString-Methode leider ein Byte Order Mark (BOM) Zeichen an den Anfang des Unicode-Zeichen-Stream gehängt. Das hier geschriebene Zeichen 0xFEFF bezeichnet eine UTF-16-Codierung in Big-Endian3. Beim Deserialisieren kam es allerdings zu einer Exception (»Ungültiges Zeichen …«).

Der Verzicht auf die Codierung hat die Methoden auch gleich viel schlanker gemacht (:

public static string SerializeToXmlString(object obj)
{
   // XmlSerializer für den Typ des Objekts erzeugen
   XmlSerializer serializer = new XmlSerializer(obj.GetType());

   // Objekt über einen StringWriter serialisieren
   using (StringWriter stringWriter = new StringWriter())
   {
      serializer.Serialize(stringWriter, obj);

      // Das Ergebnis zurückgeben
      return stringWriter.ToString();
   }
}

public static object DeserializeFromXmlString(string xmlString,
   Type objectType)
{
   // XmlSerializer für den Typ des Objekts erzeugen
   XmlSerializer serializer = new XmlSerializer(objectType);

   // Objekt über ein StreamReader-Objekt deserialisieren
   using (StringReader stringReader = new StringReader(xmlString))
   {
      return serializer.Deserialize(stringReader);
   }
}

Threading

312 In einem Thread sicher auf Steuerelemente zugreifen

Dieses Rezept habe ich um die Behandlung von WPF erweitert. Hier ist der WPF-Text:

In einer WPF-Anwendung leiten Sie Methodenaufrufe über die Invoke-Methode des Dispatcher-Objekts um, das die Dispatcher-Eigenschaft eines Steuerelements oder Fensters liefert. Prinzipiell können Sie immer die Dispatcher-Eigenschaft des jeweiligen Fensters verwenden, da dessen Steuerelemente immer auch in demselben Thread laufen (sollten). Möglicherweise bringt aber die Verwendung der Dispatcher-Eigenschaft des Objekts, da Sie aktualisieren oder abfragen wollen, einen Performance-Vorteil.

Invoke existiert in mehreren Überladungen. Der von mir verwendeten Variante übergeben Sie am ersten Argument einen Wert der DispatcherPriority-Aufzählung (aus dem Namensraum System.Windows.Threading), die die Priorität des Aufrufs bestimmt. Am zweiten Argument übergeben Sie einen Delegaten mit der auszuführenden Methode. Um das Ganze einfach zu halten, können Sie hier eine Instanz einer der passenden Action- oder Func-Varianten verwenden und dieser einen Lambda-Ausdruck übergeben. An den folgenden Argumenten übergeben Sie die Argumente, die der Methode übergeben werden sollen. Falls Sie einen Func-Delegaten verwenden (die Methode also ein Wert zurückgibt) erhalten Sie den Rückgabewert über den Object-Typen, den Invoke zurückgibt.

Um zu überprüfen, ob der (zeitintensive) Aufruf von Invoke überhaupt notwendig ist, rufen Sie die CheckAccess-Methode auf, die true zurückgibt, wenn der direkte Zugriff möglich ist.

Das folgende Beispiel zeigt, wie Sie dies programmieren. Es enthält eine Methode, die in einem Thread ausgeführt werden soll. Diese Methode simuliert eine Bearbeitung in einer Schleife. Innerhalb der Schleife wird ein Label mit einer Information aktualisiert. Invoke wird dazu ein Delegat vom Typ Action<Label, string> übergeben. Der Lambda-Ausdruck für diesen Delegaten erhält eine Referenz auf das zu aktualisierende Label und den neuen Text übergeben.

Das Programm läuft in einem Fenster, das ein Label enthält, das resultLabel heißt. Es erfordert den Import der Namensräume System, System.Threading und System.Windows.Forms. Die Methode muss natürlich über eine Thread-Instanz gestartet werden.

private void DemoThreadMethod()
{
   // Schleife als Demo für die Aktualisierung
   for (int i = 0; i < 100; i++)
   {
      // Sicher auf das Steuerelement zugreifen
      if (this.resultLabel.Dispatcher.CheckAccess())
      {
         // Der Zugriff erfolgt in demselben Tread 
         this.resultLabel.Content = i.ToString();
      }
      else
      {
         // Der Zugriff erfolgt nicht in demselben Tread
         this.resultLabel.Dispatcher.Invoke(
            System.Windows.Threading.DispatcherPriority.Normal,
            new Action<Label, string>(
            (label, content) => label.Content = content),
            this.resultLabel, i.ToString());
     }

      // Kleine Demo-Pause
      Thread.Sleep(50);
   }
}

Listing 69: Beispiel für den sicheren Zugriff auf ein Steuerelement in einem Thread in einer WPF-Anwendung über Dispatcher.Invoke

Datenbanken

317 Datenbanken erzeugen

Dieses Rezept habe ich um die Behandlung von LINQ to SQL erweitert. Hier ist der LINQ-to-SQL-Text:

LINQ to SQL macht das Erzeugen von Datenbanken auf dem System, auf dem Ihren Anwendungen später ausgeführt werden, sehr einfach. Dazu erzeugen Sie zunächst auf Ihrem System ein LINQ-to-SQL-Modell (über den LINQ-to-SQL-Designer in Visual Studio). Sie können das Modell komplett von Hand im Designer erzeugen. Einfacher ist aber ggf. die Erzeugung der Datenbank in einem dafür vorgesehenen Werkzeug (wie dem SQL Server Management Studio) und das Erzeugen des LINQ-to-SQL-Modells über das Ziehen der Tabellen vom Visual-Studio-Server-Exporer in den LINQ-to-SQL-Designer.

Im Programm müssen Sie dann lediglich eine Instanz des DataContext-Objekts erzeugen, über die DatabaseExists-Methode abfragen, ob die Datenbank (die über den Verbindungsstring des DataContext-Objekts definiert ist) existiert und im negativen Fall die CreateDatabase-Methode aufrufen um die Datenbank zu erzeugen. Das folgende Beispiel zeigt die an einem DataContext-Objekt, das für die Datenbank erzeugt wurde, die auch im ADO.NET-Abschnitt erzeugt wird:

// Den DataContext erzeugen
BookstoreDataContext dataContext = new BookstoreDataContext();

// Die Datenbank erzeugen falls diese nicht existiert
if (dataContext.DatabaseExists() == false)
{
   dataContext.CreateDatabase();
}

Listing 70: Erzeugen einer Datenbank über LINQ to SQL

318 Abfragen der automatisch vergebenen Id eines neuen Datensatzes

Dieses Rezept habe ich um die Behandlung von LINQ to SQL erweitert. Hier ist der LINQ-to-SQL-Text:

Wenn Sie Datenbanken mit LINQ to SQL bearbeiten, haben Sie keine Probleme mit dem Abfragen einer automatisch vergebenen ID, denn LINQ to SQL fragt diese nach dem Hinzufügen von Datensätzen automatisch ab und Sie können die neue ID nach dem Hinzufügen des Datensatzes aus der entsprechenden Eigenschaft der Daten-Klasse auslesen:

// DataContext erzeugen
BookstoreDataContext dataContext = new BookstoreDataContext();

// Einen Autoren hinzufügen
Author newAuthor = new Author { FirstName = "Matt", LastName = "Ruff" };
dataContext.Authors.InsertOnSubmit(newAuthor);
dataContext.SubmitChanges();

// Den Id-Wert auslesen
int identityValue = newAuthor.Id;

Listing 71: Abfragen der automatisch vergebenen ID eines neu hinzugefügten Datensatzes in LINQ to SQL

319 Bilder und andere binäre Daten in einer Datenbank verwalten

Dieses Rezept habe ich um die Behandlung von LINQ to SQL erweitert. Hier ist der LINQ-to-SQL-Text:

Die meisten Datenbanksysteme kennen einen Feldtyp, der es erlaubt, binäre Daten zu speichern. Beim SQL Server heißt dieser Typ (etwas irreführend) image. Im Allgemeinen werden solche Felder allerdings als BLOB-Felder (Binary Large Object) bezeichnet.

In einem BLOB-Feld können Sie beliebige binäre Daten ablegen. Dieses Rezept zeigt, wie Sie das für LINQ to SQL und für ADO.NET programmieren. Die Beispiele basieren auf einer Datenbank mit Namen Persons mit einer Tabelle, die mit SQL folgendermaßen erzeugt werden kann:

CREATE TABLE Persons (
   Id int PRIMARY KEY NOT NULL IDENTITY, 
   FirstName nvarchar(255),
   LastName nvarchar(255), 
   Picture image) 

Das Feld Id wird beim Hinzufügen von Datensätzen automatisch um 1 erhöht. Das Feld Picture ist vom Typ image und verwaltet deswegen binäre Daten.

LINQ to SQL

LINQ to SQL repräsentiert Felder mit binären Daten über den Typ Binary. Ein Binary-Objekt verwaltete binäre Daten als Byte-Array. Beim Schreiben erzeugen Sie eine neue Instanz der Binary-Klasse und übergeben Sie das Byte-Array am Konstruktor. Beim Lesen holen Sie die Daten über die ToArray-Methode. Das einzige Problem ist, die Daten in ein Byte-Array zu konvertieren und beim Lesen wieder zurückzukonvertieren. Dieses Problem können Sie für Bilder z. B. über das Codebook lösen (.

Das folgende Beispiel basiert auf einer WPF-Anwendung, die ein LINQ-to-SQL-Modell für die Persons-Datenbank enthält, die der oben angegebenen SQL-Anweisung entspricht. Es erfordert den Import der Namensräume System, System.Data.Linq, System.IO, System.Linq und System.Windows.Media.Imaging.

// DataContext erzeugen
PersonsDataContext dataContext = new PersonsDataContext();

// Neue Person erzeugen
Person newPerson = new Person();
newPerson.FirstName = "Zaphod";
newPerson.LastName = "Beeblebrox";

// Das Bild einlesen
string imageFileName = "C:\\Bilder\\Zaphod.jpg";
BitmapDecoder decoder = BitmapDecoder.Create(new Uri(imageFileName), 
   BitmapCreateOptions.None, BitmapCacheOption.OnLoad);
BitmapSource bitmapSource = decoder.Frames[0];

// Das Bild in ein Byte-Array konvertieren (das dem PNG-Format entspricht)
PngBitmapEncoder encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(bitmapSource));
byte[] bitmapData = null;
using (MemoryStream imageStream = new MemoryStream())
{
   encoder.Save(imageStream);
   imageStream.Flush();
   bitmapData = imageStream.ToArray();
}

// Das Bild im Person-Objekt ablegen
newPerson.Picture = new Binary(bitmapData);

// Die neue Person speichern
dataContext.Persons.InsertOnSubmit(newPerson);
dataContext.SubmitChanges();

Listing 72: Speichern binärer Daten über LINQ to SQL

Das Lesen binärerer Daten erfolgt ähnlich einfach:

// DataContext erzeugen
PersonsDataContext dataContext = new PersonsDataContext();

// Die Id der Person definieren
int personId = 1;

// Die Person ermitteln
Person person = dataContext.Persons.Single<Person>(
   p => p.Id == personId);
if (person != null)
{
   // Vorname und Nachname auslesen
   string personName = person.FirstName " " + person.LastName;

   if (person.Picture != null)
   {
      // Die Bilddaten auslesen
      byte[] bitmapData = person.Picture.ToArray();

      // Die Bilddaten in ein BitmapSource-Objekt konvertieren
      BitmapSource bitmapSource = null;
      using (MemoryStream imageStream = new MemoryStream(bitmapData))
      {
         BitmapDecoder decoder = BitmapDecoder.Create(imageStream,
            BitmapCreateOptions.None, BitmapCacheOption.OnLoad);
         bitmapSource = decoder.Frames[0];
      }

      // Das Bild verarbeiten
      ...
   }
}

Listing 73: Lesen binärer Daten über LINQ to SQL

© Jürgen Bayer 2008     
www.juergen-bayer.net