This content was uploaded by our users and we assume good faith they have the permission to share this book. If you own the copyright to this book and it is wrongfully on our website, we offer a simple DMCA procedure to remove your content from our site. Start by pressing the button below!
, rückruf As AsyncCallback, status As Object) As IAsyncResult
: alle Methodenparameter in der richtigen Reihenfolge
Hinweis Das eigentlich erforderliche EndInvoke zeigt Abschnitt 5.4.4, »Rückgabe«.
489
5.4
5
Multithreading
Das folgende Beispiel ist der einfachste Fall eines asynchronen Aufrufs. Da wir den Wert Nothing für die beiden letzten Parameter verwenden, ist es eine Einwegekommunikation: Die Methode wird aufgerufen und dann »vergessen«. '...\ Multitasking\Asynchron\Aufruf.vb
Option Strict On Imports System.Threading Namespace Multitasking Module Aufruf Delegate Sub Nachtisch(ByVal zutaten As String) Sub Machen(ByVal zutaten As String) For i As Integer = 0 To 10 Console.Write("|") : Thread.Sleep(10) Next End Sub Sub Test() Dim del As New Nachtisch(AddressOf Machen) del.BeginInvoke("Schoko", Nothing, Nothing) For i As Integer = 0 To 200 Console.Write("-") : Thread.Sleep(10) Next Console.WriteLine("Arbeit abgeschlossen.") Console.ReadLine() End Sub End Module End Namespace
Eine typische Ausgabe zeigt, wie sich – nach der zum Start der asynchronen Methode Machen notwendigen Zeit – die beiden Threads abwechseln. --------------------------------------------------------------------------------------------------|-|-||-|-|-|-|-|--|-|-------------------------------------------------------------------------------------------Arbeit abg eschlossen.
5.4.2
Rückruf
Kommen wir zur nächsten Stufe: Die aufgerufene Methode meldet sich beim Aufrufer, dass sie fertig ist. Dazu muss im AsyncCallback-Parameter eine Methode angegeben werden, die automatisch bei Beendigung der Methode aufgerufen wird. Die Signatur dieser Rückrufmethode ist festgelegt. Public Delegate Sub AsyncCallback(ar As IAsyncResult)
In unserem Beispiel schreibt die Rückrufmethode auch in die Konsole. Zur Identifikation werden außerdem Threadnummern ausgegeben.
490
Asynchrone Methodenaufrufe
'...\ Multitasking\Asynchron\Rückruf.vb
Option Strict On Imports System.Threading Namespace Multitasking Module Rückruf Delegate Sub Bescheid(ByVal zutaten As String) Sub Machen(ByVal zutaten As String) Console.Write("Beginn in Thread {0}", _ Thread.CurrentThread.ManagedThreadId) For i As Integer = 0 To 10 Console.Write("|") : Thread.Sleep(10) Next Thread.Sleep(100) End Sub Sub Fertig(ByVal ar As IAsyncResult) Console.Write("fertig in Thread {0}", _ Thread.CurrentThread.ManagedThreadId) End Sub Sub Test() Dim del As New Bescheid(AddressOf Machen) Console.Write("Hauptthread {0}",Thread.CurrentThread.ManagedThreadId) del.BeginInvoke("Schoko", AddressOf Fertig, Nothing) For i As Integer = 0 To 200 Console.Write("-") : Thread.Sleep(10) Next Console.WriteLine("Arbeit abgeschlossen.") Console.ReadLine() End Sub End Module End Namespace
Eine typische Ausgabe zeigt, wie nach einiger Zeit die Rückmeldung erfolgt. Der Abstand zu den Ausgaben von Machen ist bedingt durch die zusätzliche Sleep-Anweisung an deren Ende. Außerdem sehen Sie, dass die Rückmeldung innerhalb des Threads der aufgerufenen Methode erfolgt, es wird also kein neuer Thread für den Rückruf erstellt. Hauptthread 8----------------------------------------------------------------------------------------------------Beginn in Thread 10|-|-|-|-|-|-|-| -||-|-----------fertig in Thread 10-------------------------------------------------------------------------------Arbeit abgeschlossen.
5.4.3
Zustand
Die Methode BeginInvoke hat einen letzten Parameter, mit dem Sie Informationen an die Rückrufmethode durchreichen können. Welche das sind, hängt ausschließlich von der Logik
491
5.4
5
Multithreading
Ihrer Anwendung ab. In unserem Beispiel übergeben wir nur der Einfachheit halber eine Zeichenkette. '...\ Multitasking\Asynchron\Zustand.vb
Option Strict On Imports System.Threading Namespace Multitasking Module Zustand Delegate Sub Auftraggeber(ByVal zutaten As String) Sub Machen(ByVal zutaten As String) For i As Integer = 0 To 10 Console.Write("|") : Thread.Sleep(10) Next Thread.Sleep(100) End Sub Sub Fertig(ByVal ar As IAsyncResult) Console.Write("Hallo {0}, bin fertig", ar.AsyncState) End Sub Sub Test() Dim del As New Auftraggeber(AddressOf Machen) del.BeginInvoke("Schoko", AddressOf Fertig, "Paul") For i As Integer = 0 To 200 Console.Write("-") : Thread.Sleep(10) Next Console.WriteLine("Arbeit abgeschlossen.") Console.ReadLine() End Sub End Module End Namespace
Die Ausgabe zeigt die an BeginInvoke übergebene Zeichenkette: ----------------------------------------------------------------------------------------------------|-|-|-|-|-|-|-|-|-|-|----------Hallo Paul, bin fertig-------------------------------------------------------------------------------Arbeit abgeschlossen.
5.4.4
Rückgabe
Asynchrone Methoden dürfen auch einen Wert zurückgeben. Um an den Wert zu kommen, brauchen Sie das richtige Objekt vom Typ IAsyncResult: Es wird sowohl von BeginInvoke zurückgegeben als auch als Parameter an eine Rückrufmethode übergeben. Sie übergeben das Objekt der Methode EndInvoke des Delegates. EndInvoke(, ar As IAsyncResult) As
492
Asynchrone Methodenaufrufe
Hinweis Der Aufruf blockiert gegebenenfalls so lange, bis die asynchron aufgerufene Methode fertig ist. EndInvoke darf maximal einmal pro asynchronem Aufruf aufgerufen werden.
Der Aufruf von EndInvoke ist erforderlich, auch wenn keine Rückgabe ausgewertet wird (das exakte Warum wird im Internet kontrovers diskutiert).
Im folgenden Beispiel ist kein Parameter der asynchron aufgerufenen Methode mit ByRef gekennzeichnet, sodass EndInvoke nur mit dem letzten Argument aufgerufen wird. Der Aufruf von EndInvoke gibt den Rückgabewert der asynchron aufgerufenen Methode typrichtig zurück. Das ist einer der Gründe dafür, warum der Compiler den Code automatisch generiert und nicht eine allgemeine »Delegate-Klasse« zur Verfügung gestellt wird. Um die Ausgabe zu kürzen, testen wir in der Schleife des Hauptthreads, ob die asynchrone Methode fertig ist. '...\ Multitasking\Asynchron\Rückgabe.vb
Option Strict On Imports System.Threading Namespace Multitasking Module Rückgabe Delegate Function Zeit(ByVal zutaten As String) As Integer Function Machen(ByVal zutaten As String) As Integer For i As Integer = 0 To 10 Console.Write("|") : Thread.Sleep(10) Next Thread.Sleep(100) Return 200 End Function Sub Test() Dim del As New Zeit(AddressOf Machen) Dim ar As IAsyncResult = del.BeginInvoke("Schoko", Nothing, Nothing) For i As Integer = 0 To 200 If ar.IsCompleted Then Exit For Console.Write("-") : Thread.Sleep(10) Next Console.WriteLine("Zeitbedarf: {0} Minuten", del.EndInvoke(ar)) Console.ReadLine() End Sub End Module End Namespace
Der Hauptthread bekommt den richtigen Rückgabewert von EndInvoke. ------------------------------------------------------------------------------------------------|-|-|-|-|-|-|-|-||-|-----------Zeitbedarf: 200 Minu ten
493
5.4
5
Multithreading
5.4.5
ByRef-Parameter
Das vorige Beispiel muss nur minimal geändert werden, um auch ByRef-Parameter zu erfassen, hier ist das der kühlschrank. An welcher Stelle diese Art Parameter in der Parameterliste stehen, spielt keine Rolle. Wichtig ist, dass EndInvoke nur mit den ByRef-Parametern der asynchron aufgerufenen Methode aufgerufen wird. '...\ Multitasking\Asynchron\Rückgabe.vb
Option Strict On Imports System.Threading Namespace Multitasking Module ByRefParameter Delegate Sub Kühlschrank(ByRef kühlschrank As Integer, _ ByVal zutaten As String) Sub Machen(ByRef kühlschrank As Integer, ByVal zutaten As String) For i As Integer = 0 To 10 Console.Write("|") : Thread.Sleep(10) Next Thread.Sleep(100) kühlschrank = 4 End Sub Sub Test() Dim ks As Integer Dim del As New Kühlschrank(AddressOf Machen) Dim ar As IAsyncResult = del.BeginInvoke(ks,"Schoko",Nothing,Nothing) For i As Integer = 0 To 200 If ar.IsCompleted Then Exit For Console.Write("-") : Thread.Sleep(10) Next del.EndInvoke(ks, ar) Console.WriteLine("Kühlschrank {0}", ks) Console.ReadLine() End Sub End Module End Namespace
Nach dem Aufruf von EndInvoke hat der Hauptthread den richtigen Wert. -----------------------------------------------------------------------------------------------------|-|-|-|-|-|-|-|-|-|-|----------Kühlschrank 4
5.5
Threadpools
Da es relativ aufwendig ist, einen Thread zu starten, stellt .NET auch einen Pool von wiederverwendbaren Threads zur Verfügung. Sie müssen weder gestartet noch beendet werden; die
494
Threadpools
Verwaltung übernimmt komplett .NET. Standardmäßig sind pro Prozessor 25 Arbeitsthreads (und 1000 I/O-Threads) im Pool. Tabelle 5.11 zeigt die Mitglieder der Poolklasse. Methode
Beschreibung
SetMaxThreads(worker As Integer, io As Integer) As Boolean GetMaxThreads(worker As Integer, io As Integer)
Maximal gleichzeitig mögliche Threads im Pool (Rückgabe: gibt an, ob erfolgreich gesetzt)
SetMinThreads(worker As Integer, io As Integer) As Boolean GetMinThreads(worker As Integer, io As Integer)
Mindestzahl an Threads im Pool (Rückgabe: gibt an, ob erfolgreich gesetzt)
GetAvailableThreads(worker As Integer, io As Integer)
Gerade verfügbare Threads
QueueUserWorkItem(call As WaitCallback, state As Object) As Boolean UnsafeQueue...: dito, ohne Sicherheitsprüfungen
Ausführung von call in Threadpoolthread, ein optionales state wird an call durchgereicht.
RegisterWaitForSingleObject(w As WaitHandle, Ausführung von call in Threadpoolthread, wenn w signalisiert oder bei call As WaitOrTimerCallback, state As Object, , once As Boolean) As RegisteredWaitHandle Zeitüberschreitung UnsafeRegister...: dito, ohne Sicherheitsprüfungen : millisecs As Integer / time As TimeSpan UnsafeRegisterWaitForSingleObject
Wie RegisterWaitForSingleObject, aber ohne Sicherheitsprüfungen
BindHandle(osHandle As SafeHandle) As Boolean
Native I/O-Operation im Pool ausführen
UnsafeQueueNativeOverlapped( overlapped As NativeOverlapped*) As Boolean
I/O-Operation ohne Sicherheitsüberprüfung
Tabelle 5.11
Methoden der Klasse »System.Threading.ThreadPool« (alle Shared, kursiv = ByRef)
Die wichtigste Methode ist QueueUserWorkItem, mit der Sie eine Methode asynchron ausführen. Die Signatur der Methode ist festgelegt. Public Delegate Sub WaitCallback(state As Object)
Das folgende Beispiel macht einen Geschwindigkeitsvergleich zwischen normalen Threads und denen aus dem Pool. '...\ Multitasking\Asynchron\Zeit.vb
Option Strict On Imports System.Threading Namespace Multitasking Module Zeit
495
5.5
5
Multithreading
Sub Rechnen(ByVal eingabe As Object) nr += 1 End Sub Private nr As Integer Private Const anz As Integer = 100 Private Const tps As Long = TimeSpan.TicksPerSecond Sub Test() ThreadPool.SetMaxThreads(anz, anz * 10) ThreadPool.SetMinThreads(anz, anz * 10) Thread.Sleep(10000) Dim t0 As Date nr = 0 : t0 = Now For i As Integer = 1 To anz ThreadPool.QueueUserWorkItem(AddressOf Rechnen) Next While nr < anz : Thread.Sleep(0) : End While Console.WriteLine("{0} Threadpoolthreads brauchen {1}", _ anz, (Now.Ticks – t0.Ticks) / tps) nr = 0 : t0 = Now For i As Integer = 1 To anz Dim th As New Thread(AddressOf Rechnen) : th.Start() Next While nr < anz : Thread.Sleep(0) : End While Console.WriteLine("{0} Threads brauchen {1}", _ anz, (Now.Ticks – t0.Ticks) / tps) Console.ReadLine() End Sub End Module End Namespace
Die Ausgabe zeigt die große Zeitdifferenz der beiden Arten von Threads. Die genauen Zeiten und das Verhältnis unterliegen großen Schwankungen. 100 Threadpoolthreads brauchen 0,1301872 100 Threads brauchen 2,6137584
496
Gleichartige Daten können komfortabel in Auflistungen gespeichert werden. In diesem Kapitel werden passende Schnittstellen und Klassen ebenso vorgestellt, wie die Abfragesprache LINQ.
6
Collections und LINQ
Im Namensraum System.Collections.Generic sind Klassen, die, ähnlich den in Abschnitt 2.11, »Datenfelder (Arrays)«, beschriebenen Arrays, Sammlungen von Objekten speichern. Anders als bei Arrays erfolgt der Zugriff nicht nur über einen ganzzahligen Index, sondern die Elemente werden auch direkt identifiziert. Außerdem ist die Größe der Sammlung nicht fest, sondern passt sich automatisch an. So wird das Einfügen und Löschen von Elementen zum Kinderspiel. Hinweis Ich habe hier wegen ihrer Typsicherheit die generischen Varianten gewählt. Die meisten Darstellungen gelten auch für die Variante ohne Of-Klausel.
Hinweis Durch die Möglichkeiten, Klassen zu erweitern, können Collections noch viel mehr als hier beschrieben (siehe Abschnitt 4.8.4, »Klassenerweiterungen: Extension«).
6.1
Sammlungsschnittstellen
Die Sammlungen basieren auf Schnittstellen, die die Verwaltung der Elemente größtenteils regeln. Auf weitergehende Funktionalität der implementierenden Klassen gehen wir in Abschnitt 6.2, »Sammlungsklassen«, ein. Bitte beachten Sie, dass dieselbe Schnittstelle durch Mehrfachvererbung mehr als einmal in dem folgenden Ausschnitt aus der Vererbungshierarchie auftauchen kann. Bei Klassen ist dies wegen der Einfachvererbung nicht möglich. 1: 2: 3: 4: 5: 6: 7:
System System.AddIn.Contract System.Collections System.Collections.Generic System.Linq System.ServiceModel System.ServiceModel.Dispatcher
497
6
Collections und LINQ
3ÄÂIEnumerable ³ Ã3ÄICollection ³ Ã4ÄÄÄÄÄÄÄÄIEnumerable(Of T) ³ ³ Ã4ÄÄICollection(Of T) ³ ³ ³ Ã4ÄÂIDictionary(Of TKey,TValue) ³ ³ ³ ³ ³ À7ÄÄIMessageFilterTable(Of TFilterData) ³ ³ ³ ³ ÀIList(Of T) ³ ³ ³ À6ÄÄIExtensionCollection ³ ³ ³ (Of T As {IExtensibleObject(Of T)})}) ³ ³ À5ÄÂIGrouping(Of TKey,TElement) ³ ³ ÃILookup(Of TKey,TElement) ³ ³ ÃIOrderedEnumerable(Of TElement) ³ À5ÄÄIQueryable ³ ³ À5ÄÂÄÄÄÄÄÄÁÄÄÄÄÄÄÄÄÄÄIQueryable(Of T) ³ ³ ³ ³ ÀIOrderedQueryable ³ ³ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÁÄ5ÄÄIOrderedQueryable(Of T) ÀIEnumerator ³ 1ÄÄIDisposable ³ Ã2ÄÄIEnumeratorContract(Of T) ÀÄÄÄÄÄÄÁ4ÄÄIEnumerator(Of T)
6.1.1
IEnumerable
Die Schnittstelle IEnumerable(Of T) ist uns schon implizit bei der For Each-Schleife begegnet, denn Typen, die diese Schnittstelle implementieren, können mit dieser Schleife durchlaufen werden. Ihre einzige Funktion GetEnumerator() As IEnumerator(Of T) liefert das Objekt, das zum Durchlaufen verwendet wird (siehe Tabelle 6.1). Mitglied
Beschreibung
MoveNext() As Boolean
Fortschritt zum nächsten Element und Rückgabe, ob weitere Elemente folgen
Current As T
Auslesen des aktuellen Elements
Reset()
Zurücksetzen des Elementzeigers
Tabelle 6.1
Mitglieder von IEnumerator(Of T)
Damit lässt sich die For Each-Schleife »nachprogrammieren«: Dim text As String = "Sammlung" Dim en As IEnumerator(Of Char) = text.GetEnumerator() While en.MoveNext() Console.Write(en.Current & " ") End While
Die Ausgabe zeigt, dass die Buchstabensammlung (der Text) durchlaufen wurde: S a m m l u n g
498
Sammlungsschnittstellen
6.1.2
ICollection
Die Schnittstelle IEnumerable(Of T) ist zwar sehr einfach, hat aber den Nachteil, dass nur eine bereits vorhandene Sammlung ausgelesen, nicht aber eine neue aufgebaut werden kann. Dies ist mit der Schnittstelle ICollection(Of T) möglich (siehe Tabelle 6.2). Mitglied
Beschreibung
Add(item As T)
Neues Element hinzufügen
Clear()
Sammlung leeren
Contains(item As T) As Boolean
Gibt an, ob ein Element bereits enthalten ist.
CopyTo(array As T(), index As Integer)
Kopie der Sammlung ein in Array, angefangen bei index
Remove(item As T) As Boolean
Entfernt ein Element und gibt eine Erfolgsmeldung aus.
Count As Integer
Elementanzahl in der Sammlung auslesen
IsReadOnly As Boolean
Auslesen, ob die Sammlung schreibgeschützt ist
Tabelle 6.2
Mitglieder von ICollection(Of T)
Damit kann bereits sinnvoll gearbeitet werden. Das folgende Codefragment nutzt ein paar der in der Schnittstelle definierten Mitglieder. '...\Sammlungen\Schnittstellen\ICollection.vb
Option Explicit On Imports System.Collections.ObjectModel Namespace Sammlungen Module ICollection Sub Test() Dim text As ICollection(Of Char) = New Collection(Of Char)() For Each ch As Char In "superkalifragilistischexpialigetisch" text.Add(ch) Next Console.WriteLine("Das Wort hat {0} Buchstaben.", text.Count) text.Remove("i"c) Console.WriteLine("Da sind noch ""i"": {0}.", text.Contains("i"c)) For Each ch As Char In text : Console.Write(ch) : Next Console.ReadLine() End Sub End Module End Namespace
Die Ausgabe zeigt, dass Remove genau ein Element entfernt und nicht alle. Das Wort hat 36 Buchstaben. Da sind noch "i": True. superkalfragilistischexpialigetisch
499
6.1
6
Collections und LINQ
Die Lückenlosigkeit der letzten Zeile macht noch auf etwas sehr Wichtiges aufmerksam: Sammlungen sind lückenlos: Beim Einfügen rutschen Elemente nach hinten, beim Löschen rücken welche nach.
Diese Schnittstelle schlägt mit der Methode CopyTo eine Brücke zu den Arrays. Dies ist wichtig, weil es viele Methoden gibt, die nur ein Array akzeptieren. Die beiden folgenden Zeilen zeigen exemplarisch einen Aufruf: Dim c(35) As Char text.CopyTo(c, 0)
6.1.3
IDictionary
Wörterbücher beliebiger Art basieren auf der Schnittstelle IDictionary(Of TKey, TValue) (siehe Tabelle 6.3). Da im Gegensatz zu den bisherigen Sammlungsschnittstellen kein einfacher Wert im Spiel ist, sondern ein Schlüssel-Wert-Paar, müssen die Funktionen zum Einfügen und Löschen von Elementen neu festgelegt werden. Insbesondere die von ICollection geerbte Add-Methode ist hier noch nicht spezifiziert und wird erst durch die Implements-Klausel der implementierenden Klasse festgelegt. Mitglied
Beschreibung
ContainsKey(key As TKey) As Boolean
Gibt an, ob ein Schlüssel Teil der Sammlung ist.
Add(key As TKey, value As TValue)
Neues Element hinzufügen
Remove(key As TKey) As Boolean
Entfernt das zum Schlüssel gehörende Element und gibt eine Erfolgsmeldung.
TryGetValue(key As TKey, ByRef value As TValue) As Boolean
Speichert den zum Schlüssel gehörenden Wert und gibt eine Erfolgsmeldung.
Item(key As TKey) As TValue
Indexer: Sammlung(Schlüssel) liest Wert.
Keys() As ICollection(Of TKey)
Alle Schlüssel in der Reihenfolge von Values
Values() As ICollection(Of TValue)
Gespeicherte Werte in der Reihenfolge von Keys
Tabelle 6.3
Mitglieder von IDictionary(Of TKey, TValue)
Das folgende Codefragment verwendet einige der Mitglieder von IDictionary: '...\Sammmlungen\Schnittstellen\IDictionary.vb
Option Explicit On Namespace Sammlungen Module IDictionary Sub Test() Dim Größe As IDictionary(Of String, Integer) = _ New Dictionary(Of String, Integer)() Größe.Add("Napoleon", 168)
500
Sammlungsschnittstellen
Größe.Add("Merkel", 164) Größe.Add("Kohl", 193) Console.WriteLine("Arzt entfernt: {0}", Größe.Remove("Schweitzer")) For Each name As String In Größe.Keys Console.WriteLine("{0} ist/war {1} cm groß", name, Größe(name)) Next Console.WriteLine("Die Größe von Bush ist {0}bekannt", _ If(Größe.ContainsKey("Bush"), "", "un")) Dim gr As Integer Größe.TryGetValue("Bush", gr) Console.WriteLine("Wert für unbekannte Größe ist {0}", gr) Console.ReadLine() End Sub End Module End Namespace
Die Ausgabe zeigt, dass Länge und Größe nicht immer korrelieren: Arzt entfernt: False Napoleon ist/war 168 cm groß Merkel ist/war 164 cm groß Kohl ist/war 193 cm groß Die Größe von Bush ist unbekannt Wert für unbekannte Größe ist 0
6.1.4
IList
Die Schnittstelle IList(of T) ergänzt ICollection um einen indexbasierten Zugriff (siehe Tabelle 6.4). Damit ist eine Verwendung ähnlich einem Array möglich, wenn auch mit erheblich höherem Komfort (die Liste wächst automatisch, objektbasierter Zugriff ist außerdem möglich). Mitglied
Beschreibung
IndexOf(item As T) As Integer
Position eines Elements
Insert(index As Integer, item As T)
Neues Element an einer bestimmten Stelle einfügen
RemoveAt(index As Integer)
Element an einer bestimmten Stelle löschen
Item(index As Integer) As T
Indexer: Sammlung(Index) gibt den Wert aus.
Tabelle 6.4 Mitglieder von IList(Of T)
Da die Add-Methode von ICollection(Of T) geerbt wird und diese Schnittstelle nichts von Indizes weiß, kann auch keine allgemeingültige Aussage über die Position eingefügter Elemente gemacht werden. In vielen Fällen wird das Element angehängt. Wenn Sie jedoch sicher sein wollen, ohne die Dokumentation lesen zu müssen, verwenden Sie am besten Insert. Dabei sollten Sie beachten, dass eine nicht existierende Position eine ArgumenrOutOfRangeException auslöst. Das folgende Codefragment nutzt das Einfügen an einer gegebenen Position:
501
6.1
6
Collections und LINQ
'...\Sammlungen\Schnittstellen\IList.vb
Option Explicit On Namespace Sammlungen Module IList Sub Test() Dim Warteschlange As IList(Of String) = New List(Of String)() Warteschlange.Insert(0, "Müller") Warteschlange.Insert(1, "Maier") Warteschlange.Insert(1, "Schulze") For Each name As String In Warteschlange Console.Write("{0} ", name) Next Console.WriteLine() Console.WriteLine("Maier ist auf Position {0}", _ Warteschlange.IndexOf("Maier")) Console.WriteLine("An zweiter Stelle steht {0}", Warteschlange(1)) Warteschlange(1) = "Schmidt" Console.WriteLine("An zweiter Stelle steht {0}", Warteschlange(1)) Try Warteschlange.Insert(4, "Springer") Catch ex As Exception Console.WriteLine("Ausnahme {0}", ex.Message) End Try Console.ReadLine() End Sub End Module End Namespace
Die Ausgabe zeigt, wie der eingefügte Name den anderen nach hinten schiebt. Ein Elementersatz ist durch einfache Zuweisung wie im Fall Schmidt möglich. Außerdem sehen Sie, dass die Positionsangabe in Insert höchstens so groß sein darf, wie die Liste lang ist, da sonst undefinierte Lücken entstehen würden. Müller Schulze Maier Maier ist auf Position 2 An zweiter Stelle steht Schulze An zweiter Stelle steht Schmidt Ausnahme Index must be within the bounds of the List. Parameter name: index
502
Sammlungsklassen
6.2
Sammlungsklassen
Der folgende Ausschnitt aus der Vererbungshierarchie zeigt einige spezifische Sammlungen und mit ihnen zusammenhängende Klassen. Um die Übersicht kurz zu halten sind optionale Namensteile in eckige Klammern gesetzt. Object à 1ÄÂComparer(Of T) ³ Ã[Sorted]Dictionary(Of TKey,TValue) ³ ÃEqualityComparer(Of T) ³ ÃHashSet(Of T) ³ ÃLinkedList[Node](Of T) ³ Ã[Sorted]List(Of T) ³ ÃQueue(Of T) ³ ÃStack(Of T) ³ ÃSynchronizedCollection(Of T) ³ ³ À 1ÄÄSynchronizedKeyedCollection(Of String,ClientOperation|...) ³ ÃSynchronizedCollection(Of IExtension(Of T As ³ ³ ³ {IExtensibleObject(Of T)})})) ³ ³ À 4ÄÄExtensionCollection(Of T As {IExtensibleObject(Of T)})}) ³ ÀSynchronizedReadOnlyCollection(Of T) À 2ÄÂCollection(Of T) ³ à 2ÄÂKeyedCollection(Of TKey, TItem) ³ ³ ³ À 1ÄÄKeyedByTypeCollection(Of TItem) ³ ³ ÀObservableCollection(Of T) ³ À 3ÄÄBindingList(Of T) ÀReadOnlyCollection(Of T) À 2ÄÄReadOnlyObservableCollection(Of T) 1: 2: 3: 4:
System.Collections.Generic System.Collections.ObjectModel System.ComponentModel System.ServiceModel
Bevor wir einige Klassen im Detail anschauen, zeigt Tabelle 6.5 erst einmal eine Übersicht über einige Sammlungen. Auch hier sind optionale Teile in eckige Klammern gesetzt. Klasse
Beschreibung
[Synchronized][ReadOnly] Collection(Of T)
Werteliste, die threadsicher (Synchronized) und/oder schreibgeschützt (ReadOnly) sein kann
[ReadOnly] [Schreibgeschützte] Werteliste, die automatisch über Änderungen ObservableCollection(Of T) informiert, und mit Methoden zum Bewegen von Elementen und
Kopieren in ein Array [Sorted]List(Of T)
[Sortierte] Werteliste mit Indexzugriff und Methoden zum Suchen und Kopieren in ein Array
Tabelle 6.5 Generische Sammlungen in »System.Collections.Generic« ([ ] = optional, Collection mit [ ] außer »Synchronized« in »System.Collections.ObjectModel«)
503
6.2
6
Collections und LINQ
Klasse
Beschreibung
HashSet(Of T)
Wertemenge ohne Doubletten (Hashcode durch Comparer) mit Mengenoperationen (Vereinigung, Komplement, Schnitt, Teilmenge) und Kopie in ein Array
[Sorted]Dictionary (Of Key,Value)
Hashbasierte [sortierte] Schlüssel (durch Comparer berechnet) werden auf Werte abgebildet, Doubletten und Nothing-Schlüssel sind verboten.
KeyedCollection (Of TKey, TItem)
Liste von TItem Werten, die einen TKey Schlüssel enthalten (Kombination von IList und IDictionary).
LinkedList(Of T) LinkedListNode(Of T)
Doppelt verkettete Liste aus Knoten (Node) mit Methoden zum Löschen/Einfügen an den Enden sowie relativ zu einem Eintrag, Wertsuche von den Enden, Kopie in ein Array
Queue(Of T)
FiFo-Schlange mit Methoden zum Einstellen, Auslesen, Abholen und Kopieren in ein Array
Stack(Of T)
LiFo-Stapel mit Methoden zum Einstellen, Auslesen, Abholen und Kopieren in ein Array
Comparer(Of T)
Sortierer, für zum Beispiel SortedList(Of Key,Val), SortedDictionary(Of Key,Val), List(Of T).Sort und List(Of T).BinarySearch. Implementiert IComparer(Of T). Eigenschaft Default liefert Standardsortierer.
EqualityComparer(Of T)
Vergleicher, zum Beispiel für Schlüssel, Eigenschaft Default liefert Standardvergleicher und verwendet IEquatable(Of T) bzw. Object.Equals und Object.GetHashCode. Implementiert IEqualityComparer(Of T) .
Tabelle 6.5 Generische Sammlungen in »System.Collections.Generic« ([ ] = optional, Collection mit [ ] außer »Synchronized« in »System.Collections.ObjectModel«) (Forts.)
6.2.1
Collection
Die einfachste Implementierung einer Sammlung ist Collection(Of T). Über die implementierten Schnittstellen IList(Of T), ICollection(Of T), IEnumerable(Of T), IList, ICollection und IEnumerable hinaus stellt sie keine zusätzliche Funktionalität zur Verfügung. Die dem Konstruktor übergebene optionale Sammlung wird direkt verwendet, es wird also mit dem Original gearbeitet. Die Schnittstellenfunktionalität ist in Abschnitt 6.1, »Sammlungsschnittstellen«, beschrieben. Etwas mehr Komfort bietet ObservableCollection(Of T) durch die Implementierung der Schnittstellen INotifyCollectionChanged und INotifyPropertyChanged mit der Möglichkeit, Änderungen an der Sammlung zu verfolgen. Die Klasse ist in Tabelle 6.6 aufgelistet und ist in der Bibliothek WindowsBase.dll definiert, die gegebenenfalls dem Projekt hinzugefügt werden muss.
504
Sammlungsklassen
Klasse
Beschreibung
New([list As List(Of T)])
Öffentlicher Konstruktor
CollectionChanged As NotifyCollectionChangedEventHandler
Auslösung bei Änderung der Zusammensetzung der Sammlung (Registrierung automatisch)
PropertyChanged As PropertyChangedEventHandler
Auslösung bei Änderung eines Elements (nach manueller Registrierung)
Move(old As Integer, new As Integer)
Element verschieben
Tabelle 6.6
ObservableCollection(Of T) ([ ] = optional)
Wie das folgende Beispiel zeigt, kann die Überwachung auch sehr gut außerhalb von Benutzeroberflächen genutzt werden. '...\Sammlungen\Klassen\ObservableCollection.vb
Option Strict On Imports System.Collections.ObjectModel Imports System.Collections.Specialized Namespace Sammlungen Module Observable Public Sub CollectionChanged(ByVal sender As Object, _ ByVal ev As NotifyCollectionChangedEventArgs) If ev.NewItems IsNot Nothing Then For Each el As String In ev.NewItems Console.Write("Hallo {0} ", el) : Next End If If ev.OldItems IsNot Nothing Then For Each el As String In ev.OldItems Console.Write("Tschö {0} ", el) : Next End If End Sub Sub Test() Dim feier As ObservableCollection(Of String) = _ New ObservableCollection(Of String)() AddHandler feier.CollectionChanged, AddressOf CollectionChanged feier.Add("Ganymed") : feier.Add("Europa") : feier.Add("Io") feier.Add("Kallisto") : feier.Remove("Io") : feier.Remove("Europa") Console.ReadLine() End Sub End Module End Namespace
Die Reaktion auf Änderungen der Collection erfolgt automatisch. Hallo Ganymed Hallo Europa Hallo Io Hallo Kallisto Tschö Io Tschö Europa
505
6.2
6
Collections und LINQ
6.2.2
List
Deutlich umfangreicher und komfortabler ist List(Of T) (und damit auch ArrayList). Es implementiert dieselben Schnittstellen wie Collection. Um Tabelle 6.7 möglichst kurz zu halten, habe ich kleingeschriebene Parameternamen und großgeschriebene Typen gemischt. Klasse
Beschreibung
New([capacity | IEnumerable(Of T)])
Öffentlicher Konstruktor
TrimExcess()
Kapazität der Ist-Größe anpassen
AddRange(IEnumerable(Of T)) InsertRange(index, IEnumerable(Of T))
Andere Liste anhängen bzw. einfügen
RemoveRange(index, count) RemoveAll(Predicate(Of T)) As Integer
Bereiche nach Index oder Kriterium löschen
BinarySearch([from, count, ]item [, IComparer(Of T)]) As Integer Find[Last]Index([from, [count,]] Predicate(Of T)) As Integer [Last]IndexOf(item [,index [,count]]) As Integer
Position eines Elements mittels Vergleich durch implementierende Klasse oder Delegate (IndexOf nutzt EqualityComparer(Of T).Default).
Find[Last](Predicate(Of T)) As T
Element, das das Kriterium erfüllt
FindAll(Predicate(Of T)) As List(Of T)
Elemente, die das Kriterium erfüllen
AsReadOnly() As ReadOnlyCollection(Of T)
Schreibgeschützte Referenz (Original ist änderbar)
Reverse([index, count])
Elementreihenfolge umdrehen
Sort([[index, count, ] IComparer(Of T)]) Sort(comparison(Of T))
Umsortierung mit implementierender Klasse oder Delegate
CopyTo([sourceIndex,] array, [arrayIndex [, count]]) ToArray() As T()
Flache Kopie in ein eindimensionales Feld mit
GetRange(index, count) As List(Of T)
Flache Kopie eines Bereichs der Liste
ConvertAll(Converter(Of T,TOutput)) As List(Of TOutput)
Neue Liste, elementweise mit converter konvertiert
ForEach(Action(Of T))
Aktion elementweise anwenden
Exists(Predicate(Of T)) As Boolean
Ein Element genügt dem Kriterium.
TrueForAll(Predicate(Of T)) As Boolean
Alle Element genügen dem Kriterium.
Tabelle 6.7
Array.Copy
Methoden der Klasse List(Of T) ( [ ] = optional, | = Alternativen)
Insbesondere hat diese Auflistung die Möglichkeiten, Elemente zu suchen sowie die gesamte Auflistung zu sortieren. Fangen wir mit dem Sortieren an. Jede Sortiermethode muss zu sortierende Elemente vergleichen. Bei der großen Vielfalt an Typen gibt es keine allgemeingülti-
506
Sammlungsklassen
gen Kriterien, nach denen sortiert werden könnte. Daher setzt die Methode Sort dazu die in der Schnittstelle IComparable definierte Methode CompareTo voraus und überlässt es implementierenden Klassen, zu definieren, was kleiner und größer ist. Sie sind sowieso die einzigen, die das überhaupt entscheiden können. Public Interface IComparable(Of T) Function CompareTo(ByVal other As T) As Integer End Interface
Die Methode liefert einen negativen Wert, wenn das aktuelle Objekt »kleiner« als das übergebene ist. Analog ist der Wert positiv, wenn es »größer« ist. Sind beide »gleich«, ist der Rückgabewert Null. Die absoluten Zahlenwerte spielen keine Rolle. Zum Beispiel sind die Rückgabewerte -27 und -1 gleichwertig. Der Typ der implementierenden Klasse muss nicht mit dem des Typparameters übereinstimmen. Im folgenden Beispiel werden Bücher aufgrund ihrer Seitenzahl sortiert. Die Klasse Buch implementiert die Vergleichsschnittstelle. Anstatt selbst den Vergleich durchzuführen, reicht CompareTo den Vergleich an den Typ Integer durch, der die Schnittstelle auch implementiert. Lediglich der Fall einer Nullreferenz ist gesondert zu behandeln. '...\Sammlungen\Klassen\Sortieren.vb
Option Strict On Namespace Sammlungen Module Sortieren Class Buch : Implements IComparable(Of Buch) Private titel As String Private seitenzahl As Integer Sub New(ByVal t As String, ByVal s As Integer) titel = t : seitenzahl = s End Sub Public Function CompareTo(other As Buch) As Integer Implements IComparable(Of Buch).CompareTo If other Is Nothing Then Return 100 Return seitenzahl.CompareTo(other.seitenzahl) End Function Public Overrides Function ToString() As String Return titel & " (" & seitenzahl & ")" End Function End Class Sub Test() Dim Bücher As New List(Of Buch) Bücher.Add(New Buch("Fähnrich Hornblower", 317)) Bücher.Add(New Buch("Der Kapitän", 245)) Bücher.Add(Nothing)
507
6.2
6
Collections und LINQ
Bücher.Add(New Buch("Der Kommodore", 351)) Bücher.Add(New Buch("Lord Hornblower", 301)) For Each b As Buch In Bücher Console.Write(If(b Is Nothing, "-", b.ToString()) & " ") : Next Console.WriteLine() Bücher.Sort() For Each b As Buch In Bücher Console.Write(If(b Is Nothing, "-", b.ToString()) & " ") : Next Console.ReadLine() End Sub End Module End Namespace
Die Ausgabe zeigt die korrekte Sortierung nach der Seitenzahl. Fähnrich Hornblower (317) Der Kapitän (245) – Der Kommodore (351) Lord Hornblower (301) - Der Kapitän (245) Lord Hornblower (301) Fähnrich Hornblower (317) Der Kommodore (351)
Eine andere Möglichkeit, zu vergleichen, besteht in der Übergabe eines Objekts, das den Vergleich durchführt. Das vergleichende Objekt muss die Schnittstelle IComparer implementieren, die IComparable sehr ähnlich ist. Public Interface IComparer(Of T) Function [Compare](ByVal x As T, ByVal y As T) As Integer End Interface
Der Rückgabewert ist genauso organisiert wie bei IComparable. Daher ist das folgende Beispiel dem letzten sehr ähnlich: '...\Sammlungen\Klassen\Sortierung.vb
Option Strict On Namespace Sammlungen Module Sortierung Class Buch Private titel As String Friend seitenzahl As Integer Sub New(t As String, s As Integer) titel = t : seitenzahl = s End Sub Public Overrides Function ToString() As String Return titel & " (" & seitenzahl & ")" End Function End Class
508
Sammlungsklassen
Class Vergleich : Implements IComparer(Of Buch) Public Function Compare(x As Buch, y As Buch) As Integer _ Implements System.Collections.Generic.IComparer(Of Buch).Compare If x Is Nothing AndAlso y Is Nothing Then Return 0 If x Is Nothing Then Return –1 If y Is Nothing Then Return 1 Return x.seitenzahl.CompareTo(y.seitenzahl) End Function End Class Sub Test() Dim Bücher As New List(Of Buch) Bücher.Add(New Buch("Fähnrich Hornblower", 317)) Bücher.Add(New Buch("Der Kapitän", 245)) Bücher.Add(Nothing) Bücher.Add(New Buch("Der Kommodore", 351)) Bücher.Add(New Buch("Lord Hornblower", 301)) For Each b As Buch In Bücher Console.Write(If(b Is Nothing, "-", b.ToString()) & " ") : Next Console.WriteLine() Bücher.Sort(New Vergleich()) For Each b As Buch In Bücher Console.Write(If(b Is Nothing, "-", b.ToString()) & " ") : Next Console.ReadLine() End Sub End Module End Namespace
Die Sortierung ist identisch zur letzten: Fähnrich Hornblower (317) Der Kapitän (245) – Der Kommodore (351) Lord Hornblower (301) - Der Kapitän (245) Lord Hornblower (301) Fähnrich Hornblower (317) Der Kommodore (351)
Einige Suchfunktionen arbeiten mit einer Testfunktion. Sowie diese True zurückgibt, gilt der als Argument übergebe Wert als der gefundene. Die Testfunktion kann als Delegate oder als Funktionsobjekt gegeben werden, wie im folgenden Beispiel. Dort wird die erste Zahl gesucht, die größer als 70 ist. '...\Sammlungen\Klassen\Finden.vb
Option Strict On Namespace Sammlungen Module Finden Function Siebzig(ByVal z As Integer) As Boolean Return z > 70 End Function
509
6.2
6
Collections und LINQ
Sub Test() Dim rnd As New Random(1) Dim zahlen As New List(Of Integer)() For i As Integer = 0 To 10 zahlen.Add(rnd.Next(0, 100)) Next For Each z As Integer In zahlen : Console.Write(z & " ") : Next Console.WriteLine() Console.WriteLine("Erste größer als 70: {0}", _ zahlen.Find(Function(z As Integer) z > 70)) Console.WriteLine("Erste größer als 70: {0}", _ zahlen.Find(AddressOf Siebzig)) Console.ReadLine() End Sub End Module End Namespace
Die Zahl wird auf beide Arten korrekt gefunden: 24 11 46 77 65 43 35 94 10 64 2 Erste größer als 70: 77 Erste größer als 70: 77
6.2.3
Wörterbücher
Alle Wörterbuchklassen implementieren die Schnittstelle IDictionary, die bereits im Abschnitt 6.1, »Sammlungsschnittstellen«, vorgestellt wurde. Hier möchte ich noch auf ein Problem sortierter Auflistungen hinweisen, da Wörterbücher oft sortiert gespeichert werden. Der Sortiervorgang setzt einen Vergleich der Elemente der Auflistung voraus. Wenn Sie dem Konstruktor kein Objekt vom Typ IComparer(Of T) übergeben, wird ein Standardvergleich durchgeführt, der nicht immer passend ist. Die einzelnen Einträge sind Paare von Schlüsseln und Werten. Wenn Sie ein Wörterbuch durchlaufen, ist der Typ des Elements weder der des Schlüssels noch der des Wertes, sondern des Paares. Im folgenden Beispiel werden Paare von Zeichenketten und Zahlen verarbeitet. '...\Sammlungen\Klassen\Paar.vb
Option Strict On Namespace Sammlungen Module Paar Sub Test() Dim tab As New Dictionary(Of String, Integer)() tab.Add("Perm", 299) : tab.Add("Kreide", 145) tab.Add("Karbon", 360) : tab.Add("Jura", 200) For Each p As KeyValuePair(Of String, Integer) In tab Console.Write("Beginn {0} vor {1} Mio Jahren ", p.Key, p.Value) Next
510
Sammlungsklassen
Console.ReadLine() End Sub End Module End Namespace
Die Ausgabe bestätigt die korrekte Arbeitsweise: Beginn Perm vor 299 Mio Jahren Beginn Kreide vor 145 Mio Jahren Beginn Karbon vor 360 Mio Jahren Beginn Jura vor 200 Mio Jahren
6.2.4
Schlangen
Eine Gruppe von Auflistungen ist dadurch gekennzeichnet, dass nur auf die Elemente an einem der beiden Enden zugegriffen werden kann. Aufgrund der Art von Änderungen werden zwei Ausprägungen unterschieden: 왘
FiFo (first in first out): Das erste hinzugefügte Element wird auch als Erstes wieder entnommen, implementiert von der Klasse Queue(Of T) .
왘
LiFo (last in first out): Das letzte hinzugefügte Element wird als Erstes wieder entnommen, implementiert von der Klasse Stack(Of T) .
Im folgenden Beispiel werden dieselben Zahlen mit Push in einem Stack und mit Enqueue in einer Queue abgelegt und mit Pop bzw. Dequeue wieder ausgelesen. '...\Sammlungen\Klassen\Schlangen.vb
Option Strict On Namespace Sammlungen Module Schlangen Sub Test() Dim st As New Stack(Of Integer), qu As New Queue(Of Integer) For Each no As Integer In New Integer() {17, 45, 7, 82, 8} Console.Write(no & " ") : st.Push(no) : qu.Enqueue(no) Next Console.WriteLine() While st.Count > 0 Console.Write(st.Pop() & " ") End While Console.WriteLine() While qu.Count > 0 Console.Write(qu.Dequeue() & " ") End While Console.ReadLine() End Sub End Module End Namespace
Die Ausgabe zeigt, dass Stack die Reihenfolge umkehrt und Queue sie beibehält.
511
6.2
6
Collections und LINQ
17 45 7 82 8 8 82 7 45 17 17 45 7 82 8
6.2.5
Gleichheit
Die Schnittstellen IEquatable(Of T) und IComparable(Of T) werden leicht vergessen, wenn es darum geht, was unter Gleichheit von Objekten zu verstehen ist. Sie sind jedoch wichtig in Zusammenhang mit Sammlungen.
6.3
Array
Aufgrund der in diesem Kapitel besprochenen Schnittstellen möchte ich noch einmal kurz auf Arrays zu sprechen kommen. Die Klasse implementiert einige Schnittstellen: Public MustInherit Class Array Implements ICloneable, IList, ICollection, IEnumerable
Bei der Deklaration eines Arrays leitet der Compiler diese Klasse ab. Sie dürfen die Klasse nicht selbst ableiten. Wenn Sie eine Referenz auf ein Array-Objekt brauchen, können Sie eine der klassengebundenen Fabrikmethoden CreateInstance verwenden, zum Beispiel: Public Shared Function CreateInstance( _ elementType As Type, ParamArray lengths As Integer()) As Array
Jeder Index nach der Typangabe repräsentiert eine Dimension, zum Beispiel ein zweidimensionales Fließkomma-Array: Dim ar As Array = Array.CreateInstance(GetType(Long), 10, 2)
Die Wertzuweisung findet durch die Methode SetValue statt, hier der Wert 77: ar.SetValue(77, 5, 1)
Die Startindizes können auch vorgegeben werden; Public Shared Function CreateInstance(elementType As Type, _ lengths As Integer(),lowerBounds As Integer()) As Array
6.3.1
Die Eigenschaften eines Array-Objekts
Tabelle 6.8 zeigt die vier wichtigsten Eigenschaften eines Arrays.
512
Array
Eigenschaft
Beschreibung
IsFixedSized
Gibt an, ob das Array eine feste Größe hat; für Arrays immer True.
IsReadOnly
Gibt an, ob das Array schreibgeschützt ist; für Arrays immer False.
IsSynchronized
Gibt an, ob der Zugriff threadsicher ist; für Arrays immer False.
Length, LongLength
Die Gesamtanzahl der Array-Elemente in allen Dimensionen
Rank
Die Anzahl der Dimensionen eines Arrays
Tabelle 6.8
6.3.2
Eigenschaften von Array
Methoden von Array
In Tabelle 6.9 bedeutet flache Kopie, dass ein Element, das eine Referenz ist, als Referenz kopiert wird und dann das Objekt, auf das sie zeigt, nicht kopiert wird. Ziel-Arrays müssen bereits richtig dimensioniert sein. Zum Sortieren kann ein Comparer-Objekt den Sortierprozess steuern. Methode
Beschreibung
AsReadOnly
Verpackt das Array in einen Schreibschutz.
S
BinarySearch
Suche nach Elementen
S
Clear
Elemente zu null bzw. Nothing setzen
S
Clone
Erstellt eine flache Kopie.
ConstrainedCopy
Flache Kopie, bei Scheitern ist Ziel unverändert.
S
ConvertAll
Typumwandlung aller Elemente (erzeugt Kopie).
S
Copy
Multidimensionale Kopie
S
CopyTo
Eindimensionale Kopie
CreateInstance
Array erzeugen
S
Exists
Gibt an, ob ein Element existiert.
S
Find, FindAll, FindLast
Element suchen
S
FindIndex, FindLastIndex
Element suchen
S
FoEach
Aktion auf alle Elemente anwenden
S
GetEnumerator
Eunmerator zum Durchlaufen des Arrays
GetLength, GetLongLength
Elementanzahl einer Dimension
GetLowerBound, GetUpperBound
Minimaler und maximaler Index einer Dimension
GetValue, SetValue
Elementzugriff
IndexOf, LastIndexOf
Element suchen
Initialize
Alle Elemente mit Standardwert belegen
Resize
Redimensionierung
S
Reverse
Umkehrung der Elementreihenfolge
S
Sort
Array sortieren
S
TrueForAll
Gibt an, ob alle Elemente eine Bedingung erfüllen.
S
Tabelle 6.9
S
Methoden von Array (S = Shared)
513
6.3
6
Collections und LINQ
6.4
LINQ
Die Analyse von Daten zu programmieren kann recht aufwendig werden. Da die Aufgaben bei sehr verschiedenen Datenquellen immer wieder dieselben sind, wurde die Schnittstelle LINQ (Language Integrated Query) geschaffen. Dadurch können Sie Analysen von Daten programmieren und später mit minimalem Aufwand die Datenquelle ändern. Zum Beispiel können Sie ein Programm mit einfachen Auflistungen entwickeln und erst später die Daten in einer Datenbank bearbeiten – oft ohne eine Zeile Code zu ändern. Für LINQ sind bereits Anbindungen für einige Datenquellen definiert: 왘
LINQ to Objects steht im Namensraum System.Linq und ist das Fundament aller LINQAbfragen. Datenquellen sind Auflistungen und Objekte, die untereinander in Beziehung gesetzt werden können.
왘
LINQ to XML bietet eine Programmierschnittstelle für XML im Arbeitsspeicher, die das in .NET sprachintegrierte Abfrage-Framework nutzt.
왘
LINQ to SQL ist Microsofts Provider für das eigene Datenbanksystem SQL Server.
왘
LINQ to ADO.NET arbeitet mit einem DataSet oder SQL-Abfragen.
Die Darstellungen in diesem Kapitel basieren auf LINQ to Objects. Da LINQ eine Schnittstellentechnologie ist, ist das keine große Beschränkung. Aufgrund der Nähe zu Datenbankabfragen wird ein LINQ-Aufruf als Abfrageausdruck bezeichnet. Die Syntax ist recht ähnlich zu SQL.
6.4.1
Neue Sprachkonzepte
Um die Syntax einfach zu halten, wurden neue Konzepte in Visual Basic eingeführt: 왘
Implizit typisierte Variablen: Aus dem Wert wird der Datentyp ermittelt, nicht zu verwechseln mit Option Strict Off. Eingeführt, weil jede Datenabfrage eine unterschiedliche Struktur mit unterschiedlichen Typen hat.
왘
Lambda-Ausdrücke: Funktionen außerhalb eines Datentyps (siehe Abschnitt 3.9.5, »Funktionsobjekte: Function(λ-Ausdrücke)«). Eingeführt, um einfach auf die Mitglieder strukturierter Datentypen wie Datenzeilen zugreifen zu können.
왘
Anonyme Klassen: Typdefinition ohne eigentliche Klassendefinition (siehe Abschnitt 4.6, »Anonyme Klassen«). Eingeführt, um die Resultate von Abfragen zu speichern.
왘
Erweiterungsmethoden: Erweiterungsmethoden dienen zum Nachrüsten von Methoden für beliebige Klassen (siehe Abschnitt 4.8.4, »Klassenerweiterungen: Extension«). Eingeführt, um leichter auf verschiedenen Datenquellen arbeiten zu können.
Sie werden diese Konzepte meistens einsetzen, ohne es überhaupt zu bemerken.
Implizit typisierte Variablen Die Ableitung eines Datentyps aus einem Wert findet vor der Kompilierung statt und ist auf die Initialisierung lokaler Variablen beschränkt. Lokal sind alle Variablen, die nicht auf Klassenebene definiert sind, also auch Schleifenvariablen. Alle Datentypen im Wert müssen ein-
514
LINQ
deutig sein, das heißt, eine Zuweisung von Nothing ist nicht erlaubt. Die Deklaration von Variablen wird bis auf den fehlenden Datentyp durch die Typinferenz nicht geändert. In den folgenden Zeilen ist der ermittelte Datentyp im Kommentar angegeben. Dim Dim Dim For
x a v i
= = = =
2.9 'Double New Integer() {} 'Integer From r In a Select r 'IEnumerable(Of Integer) 1 To 5 : Next 'Integer
Durch die Ermittlung des Datentyps kann oft auf Delegates zur Spezifikation des Rückgabetyps verzichtet werden. Im Rahmen von LINQ ist die Ableitung der Datentypen unverzichtbar. An anderer Stelle empfehle ich dringend, darauf zu verzichten. Durch explizite Typangaben wird der Quelltext erheblich leichter lesbar.
Neue Schlüsselwörter Um die Formulierung von Abfragen einfach und ähnlich zu SQL zu halten, ist Visual Basic um einige speziell interpretierte Bezeichner erweitert worden (siehe Tabelle 6.10). Ich vermeide das Wort Schlüsselwörter, da Bezeichner als normale Variablen verwendet werden können und Schlüsselwörter nicht. Die Bezeichner haben also nur im Rahmen einer LINQ-Syntax eine besondere Bedeutung. Aggregate
Ascending
By
Descending
Distinct
From
Group
Into
Join
Let
Order
Select
Skip
Take
Where
Tabelle 6.10 LINQ-spezifische Bezeichner
6.4.2
Erweiterungsmethoden
LINQ-Abfragen müssen letztendlich in die Programmiersprache Visual Basic übersetzt werden. Zum besseren Verständnis der Abfragesyntax lohnt es sich daher, einmal von der anderen Seite zu kommen und eine Abfrage in normaler Visual-Basic-Syntax zu formulieren und dann zu sehen, wie dieselbe Abfrage in LINQ formuliert wird. Zentral für die Abfrage sind die zu verwendenden Daten. Da in .NET nichts außerhalb von Objekten (und Klassen) existieren kann, werden Manipulationen der Daten als Operationen auf den Daten formuliert. Die Daten sind dabei in einem Objekt gespeichert, das mehrere Daten gleichen Typs aufnehmen kann. Erlaubt sind: 왘
Datentyp IQueryable
왘
Datentyp IEnumerable
왘
Das Objekt hat eine Methode AsQueryable() As IQueryable.
왘
Das Objekt hat eine Methode AsEnumerable() As IEnumerable.
왘
Es gibt eine Umwandlungsmethode Cast(Of T)() As IQueryable oder IEnumerable.
515
6.4
6
Collections und LINQ
Mit diesen grundlegenden Schnittstellen ist fast jede Auflistung erlaubt, und andere Datentypen werden leicht zu kompatiblen Typen. Die Funktionalität steckt in den Erweiterungsklassen Queryable und Enumerable. Tabelle 6.11 listet alle Methoden dieser Klassen auf. Methode
Beschreibung
Aggregate
Akkumuliert Daten.
All
Gibt an, ob alle Elemente eine Bedingung erfüllen.
Any
Gibt an, ob ein Element eine Bedingung erfüllt.
AsEnumerable AsQueryable
Durchreichen der Eingabe
Average
Mittelwert numerischer Elemente
Cast
Konvertierung in IEnumerable(Of T)
Concat
Zwei Auflistungen verbinden
Contains
Gibt an, ob ein Element vorhanden ist.
Count
Anzahl der Elemente
DefaultIfEmpty
Standardwert statt fehlender Werte
Distinct
Doppelte Werte entfernen
ElementAt
Element an der gegebenen Position
ElementAtOrDefault
Element an der gegebenen Position oder Standardwert, wenn das Element fehlt
Except
Komplementärmenge
First
Erstes Element
FirstOrDefault
Erstes Element oder Standardwert, wenn das Element fehlt
GroupBy
Gruppierung nach Schlüsseln
GroupJoin
Kombination jedes Elements der ersten Liste mit je einer Teilmenge der zweiten
Intersect
Schnittmenge
Join
Von der Kombination jedes Wertes mit jedem Wert der zweiten Liste die auswählen, die eine Bedingung erfüllen
Last
Letztes Element
LastOrDefault
Letztes Element oder Standardwert, wenn das Element fehlt
LongCount
Anzahl der Elemente
Max
Maximum der Elemente
Min
Minimum der Elemente
OfType
Nach Typ selektieren
OrderBy
In aufsteigender Reihenfolge sortieren
OrderByDescending
In absteigender Reihenfolge sortieren
Range
Sequenz natürlicher Zahlen
Repeat
Menge mit identischen Elementen
Reverse
Umgekehrte Reihenfolge
Tabelle 6.11
516
Methoden von »Enumerable« und »Queryable« (E = nur »Enumerable«)
E
LINQ
Methode
Beschreibung
Select
Transformation jedes Elements
SelectMany
Verbindung der Transformation jedes Elements in eine Sequenz von Werten
SequenceEqual
Gibt an, ob zwei Listen gleich sind.
Single
Das einzige Element, das eine Bedingung erfüllt
SingleOrDefault
Das einzige Element, das eine Bedingung erfüllt oder Standardwert, wenn das Element fehlt
Skip
Restmenge nach gegebener Position
SkipWhile
Restmenge, nachdem eine Bedingung nicht erfüllt ist
Sum
Summe numerischer Werte
Take
Restmenge bis zur gegebenen Position
TakeWhile
Restmenge, solange eine Bedingung erfüllt ist
ThenBy
Nachgeschaltete sekundäre aufsteigende Sortierung
ThenByDescending
Nachgeschaltete sekundäre abssteigende Sortierung
ToArray
In Array konvertieren
E
ToDictionary
In Dictionary(Of TKey, TValue) konvertieren
E
ToList
In List(Of T) konvertieren
E
ToLookup
In Lookup(Of TKey, TElement) konvertieren
E
Union
Vereinigungsmenge
Where
Nach Kriterium selektieren
Tabelle 6.11
Methoden von »Enumerable« und »Queryable« (E = nur »Enumerable«) (Forts.)
Damit können wir eine erste Abfrage in Visual-Basic-Syntax formulieren. Das folgende Beispiel definiert eine Klasse Person mit einem Feld für den Namen und einem Feld für das Alter. In der Methode Test() wird eine Liste von Personen erstellt. Da Arrays die Schnittstelle IEnumerable implementieren, können sie in Abfragen benutzt werden. Die eigentliche Abfrage startet mit daten, sortiert diese mit OrderBy und wendet mit ThenBy ein zweites Sortierkriterium an. '...\Sammlungen\Linq\VBAbfrage.vb
Option Strict On Namespace Sammlungen Module VBAbfrage Class Person Friend Name As String, Alter As Integer Sub New(ByVal n As String, ByVal a As Integer) Name = n : Alter = a End Sub End Class
517
6.4
6
Collections und LINQ
Sub Test() Dim daten() As Person = New Person() {New Person("Emil", 12), _ New Person("Jens", 9), New Person("Marie", 12), New Person("Hugo", 9)} Dim v As IEnumerable(Of Person) = daten _ .OrderBy(Function(a) a.Alter) _ .ThenBy(Function(a) a.Name) For Each p As Person In v Console.Write(p.Name & "(" & p.Alter & ") ") Next Console.ReadLine() End Sub End Module End Namespace
Die Sortierung erfolgt nach beiden Kriterien. Hugo(9) Jens(9) Emil(12) Marie(12)
Sie sehen, wie einfach Sie Abfragen hintereinanderschalten können. Dies ist der Schlüssel zu komplexen Abfragen. Jedes Einzelteil einer Abfrage mündet letztendlich in einen einfachen Methodenaufruf der Klasse Enumerable, die über den in Abschnitt 4.8.4, »Klassenerweiterungen: Extension«, beschriebenen Mechanismus automatisch das Objekt, hier daten und das Resultat von OrderBy, als ersten Parameter bekommt. Es gibt zwar viele Methoden in Enumerable, aber jede für sich ist gut überschaubar. Im Folgenden werden wir diese in LINQSyntax näher untersuchen.
6.4.3
Abfragesyntax
Die eben formulierte Abfrage können Sie auch in der Abfragesyntax von LINQ formulieren, die sich sehr stark an SQL orientiert. Einer der wenigen Unterschiede ist die Platzierung der From-Klausel. SQL beginnt mit Select, LINQ dagegen mit From. Der Grund ist die konsequente Erzeugung der Daten aus einer Quelle, die daher an erster Stelle genannt wird. Die Syntaxen in LINQ und in Visual Basic sind ähnlich. Die Abfrage des letzten Abschnitts Dim v As IEnumerable(Of Person) = _ daten .OrderBy(Function(a) a.Alter) .ThenBy(Function(a) a.Name)
ist in LINQ genauso aufgebaut, aber noch einfacher in der Formulierung: '...\Sammlungen\Linq\VBAbfrage.vb
Dim v = From p In daten Order By p.Alter Order By p.Name
Ich habe bewusst ein Beispiel mit einer Umbenennung gewählt, um Sie darauf aufmerksam zu machen, dass nicht immer eine hundertprozentige Korrespondenz besteht. Um etwas mit der Syntax warm zu werden, gehen wir nun den umgekehrten Weg und fangen mit der LINQVariante an. Im folgenden Beispiel suchen wir aus einer Liste von Kunden diejenigen heraus,
518
LINQ
die höchstens vier Teile bestellt haben, und nehmen dann nur ihre Namen. Die Syntax ist in LINQ und direkt darunter mit Erweiterungsmethoden formuliert. '...\Sammlungen\Linq\LinqZuVB.vb
Option Strict On Namespace Sammlungen Module LinqZuVB Class Kunde Friend Name As String, Bestellmenge As Integer Sub New(ByVal n As String, ByVal b As Integer) Name = n : Bestellmenge = b End Sub End Class Sub Test() Dim daten() As Kunde = New Kunde() {New Kunde("Vogel", 4), _ New Kunde("Li", 9), New Kunde("Maier", 2), New Kunde("Schmidt", 5)} Dim kdeLinq = From k In daten Where k.Bestellmenge < 5 Select k.Name Dim kdeVB = daten .Where(Function(k) k.Bestellmenge < 5) _ .Select(Function(k) k.Name) For Each p As String In kdeLinq : Console.Write(p & " ") : Next Console.WriteLine() For Each p As String In kdeVB : Console.Write(p & " ") : Next Console.ReadLine() End Sub End Module End Namespace
Beide Abfragen erzeugen eine identische Ausgabe: Vogel Maier Vogel Maier
In LINQ werden zwei Schreibweisen unterschieden: 왘
Abfragesyntax (Query-Expression-Syntax)
왘
Erweiterungsmethodensyntax (Extension-Method-Syntax)
Letztere ist zwar schwerer zu lesen, schöpft aber die volle Leistungsfähigkeit von LINQ aus. Nicht alle Abfrageausdrücke lassen sich in der Schreibweise der Abfragesyntax ausdrücken. In einigen Fällen kommen Sie an der Erweiterungsmethodensyntax nicht vorbei. Sie können beide Schreibweisen mischen. In jedem Fall wandelt der Compiler die Abfrage in die Erweiterungsmethodensyntax um.
519
6.4
6
Collections und LINQ
6.4.4 Abfrageoperatoren LINQ stellt Ihnen zahlreiche Abfrageoperatoren zur Verfügung. Alle haben korrespondierende Erweiterungsmethoden in der Klasse Enumerable im Namensraum System.Linq. In Tabelle 6.12, »LINQ-Abfrageoperatoren«, sind alle angegeben. Operatortyp
Operator
Aggregat
Aggregate, Average, Count, LongCount, Min, Max, Sum
Umwandlung
Cast, OfType, ToArray, ToDictionary, ToList, ToLookup, ToSequence
Element
DefaultIfEmpty, ElementAt, ElementAtOrDefault, First, FirstOrDefault, Last, LastOrDefault, Single, SingleOrDefault
Gleichheit
EqualAll
Sequenz
Empty, Range, Repeat
Gruppierung
GroupBy
Verbindung
Join, GroupJoin
Sortierung
OrderBy, ThenBy, OrderByDescending, ThenByDescending, Reverse
Aufteilung
Skip, SkipWhile, Take, TakeWhile
Quantifizierung
All, Any, Contains
Restriktion
Where
Projektion
Select, SelectMany
Menge
Concat, Distinct, Except, Intersect, Union
Tabelle 6.12
LINQ-Abfrageoperatoren
Wir werden im weiteren Verlauf des Kapitels auf die meisten der hier aufgeführten LINQOperatoren genauer eingehen.
6.4.5 From-Klausel Ein Abfrageausdruck beginnt mit der From-Klausel. Sie beschreibt die abzufragende Datenquelle und definiert eine lokale Bereichsvariable, die jedes Element in der Datenquelle repräsentiert, ähnlich wie die Variable in einer For Each-Schleife. Wie in Abschnitt 6.4.2, »Erweiterungsmethoden«, beschrieben, muss die Datenquelle den Typ IEnumerable(Of T), IQueryable(Of T), IEnumerable oder IQueryable haben oder in einen solchen über fest vorgeschriebene Methoden umwandelbar sein. Datenquelle und Bereichsvariable sind streng typisiert. Wenn Sie mit Dim kunden() As Kunde From kunde In kunden
das Array aller Kunden als Quelle angeben, ist die Bereichsvariable vom Typ Kunde. Wenn Sie nichtgenerische Auflistungen verwenden, müssen Sie die Bereichsvariable explizit typisieren, denn solche Auflistungen speichern Elemente vom Typ Object. Zum Beispiel:
520
LINQ
Dim arr As New ArrayList() arr.Add(New Kunde()) arr.Add(New Kunde()) Dim cust = From c As Kunde In arr Select Name
Manchmal enthält ein Element der Datenquelle Mitglieder eines strukturierten Typs, wie zum Beispiel das Array im folgenden Typ: Public Class Bestellung Public Nummer As Integer Public Menge As Integer End Class Public Class Kunde Public Name As String Public Bestellungen() As Bestellung End Class Dim kunden() As Kunde
Jedem Kunden ist ein Array vom Typ Bestellung zugeordnet. Um die Bestellungen abzufragen, muss eine weitere From-Klausel angeführt werden, die auf die Bestellliste des jeweiligen Kunden zugreift. Jede From-Klausel kann separat mit Where gefiltert oder mit OrderBy sortiert werden. Dim p = From k in kunden Where k.Name == "Hans" _ From b in k.Bestellungen Where b.Menge > 6 Select b.Nummer
In diesem Codefragment wird die Liste aller Kunden zuerst nach Hans durchsucht. Die gefundene Dateninformation extrahiert anschließend die Bestellinformationen und beschränkt das Ergebnis auf alle Bestellungen (von Hans), die eine Bestellmenge > 6 haben.
6.4.6 Der Restriktionsoperator Where Um eine Folge von Elementen zu filtern, verwenden Sie den Where-Operator. Angenommen, Sie möchten alle Flüsse in Deutschland auflisten. Class Fluss Public Name As String Public Land As String End Class Dim flüsse(3) As Fluß flüsse(0) = New Fluss() flüsse(1) = New Fluss() flüsse(2) = New Fluss() flüsse(3) = New Fluss()
: : : :
flüsse(0).Name flüsse(1).Name flüsse(2).Name flüsse(3).Name
= = = =
"Elbe" : flüsse(0).Land = "D" "Maas" : flüsse(1).Land = "NL" "Ems" : flüsse(2).Land = "D" "Main" : flüsse(3).Land = "D"
For Each n In From f In flüsse Where f.Land="D" Select f.Name Console.Write(n & " ") Next
521
6.4
6
Collections und LINQ
Mit dem Select-Operator geben Sie das Element an, das in die Ergebnisliste aufgenommen werden soll. In diesem Fall ist das der Name jedes entsprechend dem Where-Operator gefundenen Flusses. Die Ergebnisliste wird in der For Each-Schleife durchlaufen und an der Konsole ausgegeben: Elbe Ems Main. Sie können die Abfragesyntax auch durch die Erweiterungsmethodensyntax ersetzen. Geben Sie dabei direkt das zu durchlaufende Array an. An der Codierung der Konsolenausgabe ändert sich nichts. For Each n In flüsse .Where(Function(f) f.Land="D") _ .Select(Function(f) f.Name)
Um aus einem Objekt für die Ergebnisliste mehrere spezifische Daten zu filtern, übergeben Sie dem Select-Operator einen anonymen Typ, der sich aus den gewünschten Elementen zusammensetzt. Interessiert Sie beispielsweise neben dem Namen auch das Land des gefundenen Flusses in der Ergebnisliste, sieht der Code der LINQ-Abfrage wie folgt aus: For Each n In From f In flüsse Where f.Land="D" _ Select New With {f.Name, f.Land} Console.Write("{" & n.Name & "," & n.Land & "}") Next
Die Ergebnisliste setzt sich aus den objektspezifischen Elementen Name und Land zusammen und muss bei der Ausgabe beachtet werden. Mehrere Filterkriterien zu berücksichtigen ist nicht weiter schwierig. Sie müssen nur den Where-Operator ergänzen. Dazu benutzen Sie die Visual-Basic-spezifischen Operatoren. Im nächsten Codefragment werden alle mit »E« anfangenden Flüsse in Deutschland ausgegeben. For Each n In From f In flüsse Where f.Land="D" AndAlso f.Name(0)= "E"c _ Select f.Name Console.Write(n & " ") Next
oder: For Each n In flüsse.Where(Function(f) f.Land="D" AndAlso f.Name(0)= "E"c) _ .Select(Function(f) f.Name) Console.Write(n & " ") Next
Überladungen des Where-Operators Wenn Sie sich die .NET-Dokumentation des Where-Operators ansehen, finden Sie die beiden folgenden Signaturen: Public Shared Function Where(Of TSource)( _ source As IEnumerable(Of TSource), _ predicate As Func(Of TSource, Boolean) _ ) As IEnumerable(Of TSource) Public Shared Function Where(Of TSource)( _ source As IEnumerable(Of TSource), _
522
LINQ
predicate As Func(Of TSource, Integer, Boolean) _ ) As IEnumerable(Of TSource)
Die erste wird für Abfragen verwendet, wie wir sie weiter oben eingesetzt haben. Die IEnumerable(Of TSource)-Collection wird dabei komplett gemäß den Filterkriterien durchsucht. Mit der zweiten Signatur können Sie den Bereich der Ergebnisliste einschränken, und zwar anhand des nullbasierten Index des Elements, der als Integer übergeben wird. Wenn nur Flüsse mit ungeradem Index berücksichtigt werden sollen (Maas Main), sieht das so aus: For Each n In flüsse.Where(Function(f,i) i Mod 2 = 1) _ .Select(Function(f) f.Name) Console.Write(n & " ") Next
Hier müssen Sie die Erweiterungsmethodensyntax einsetzen, um der überladenen Erweiterungsmethode Where die erforderlichen Argumente übergeben zu können.
Funktionsweise des Where-Operators Betrachten wir noch einmal die folgende Anweisung: Dim f = flüsse.Where(Function(f) f.Land="D") var result = customers .Where( cust => cust.City == Cities.Aachen)
Where ist eine Erweiterungsmethode der Schnittstelle IEnumerable(Of T) und gilt auch für das Array vom Typ flüsse. Der Ausdruck Function(f) f.Land="D"
ist ein Lambda-Ausdruck, im eigentlichen Sinne also das Delegate auf eine anonyme Methode. In der Definition des Where-Operators wird dieses Delegate durch Func(Of TSource, Boolean)
beschrieben (siehe Definition von Where oben). Der generische Typparameter TSource wird durch die Elemente in der zugrunde liegenden Collection beschrieben, die die Schnittstelle IEnumenerable(Of TSource) implementiert. In unserem Beispiel handelt es sich um FlussObjekte. Daher können wir bei korrekter Codierung innerhalb des Lambda-Ausdrucks auch auf die IntelliSense-Liste zurückgreifen. Der zweite Parameter spezifiziert den Typ des Rückgabewerts des Lambda-Ausdrucks. Hier wird ein boolescher Typ vorgegeben, denn über True weiß LINQ, dass auf das untersuchte Element das Suchkriterium zutrifft und bei einer Rückgabe von False eben nicht. Das Zusammenspiel zwischen den neuen Lambda-Ausdrücken und Erweiterungsmethoden im Kontext generischer Typen und Delegates ist hier sehr gut zu erkennen. In ähnlicher Weise funktionieren auch viele andere Operatoren. Ich werde daher im Folgenden nicht jedes Mal erneut das komplexe Zusammenspiel der verschiedenen Sprachkomponenten erörtern.
523
6.4
6
Collections und LINQ
6.4.7
Projektionsoperatoren
Select Der Select-Operator speichert die Ergebnisse der Abfrage in einem Objekt, das die Schnittstelle IEnumerable(Of T) implementiert, zum Beispiel: Class Kreis : Public X, Y, D As Double : End Class Dim kreise() As Kreis Dim res = From k in kreise Select k.X
oder alternativ: Dim res = kreise.Select(Function(k) k.X)
Die Rückgabe ist in beiden Fällen eine Liste mit den x-Positionen der in der Collection vertretenen Kreise. Liefert der Select-Operator eine Liste mit neu strukturierten Datenzeilen, müssen Sie einen anonymen Typ als Ergebnismenge definieren: Dim res = From k in kreise Select New With {f.X, f.Y}
SelectMany SelectMany kommt dann sinnvoll zum Einsatz, wenn es sich bei den einzelnen Elementen in einer Elementliste um Arrays handelt, deren Einzelelemente von Interesse sind. In der Anwendung Musterdaten trifft das auf alle Objekte vom Typ Customer zu, weil die Bestellungen in einem Array verwaltet werden. Public Class Order : Public Nummer, Menge As Integer : End Class Public Class Kunde : Public Name As String, Best() As Order : End Class Dim kl = From k in kunden Where k.Name = "Hans" _ From b in k.Best Where b.Menge > 6 Select b.Nummer
Weiter oben hatten wir uns bereits mit Untermengen dieser Art beschäftigt. In der Erweiterungsmethodensyntax heißt der Operator SelectMany: Dim kv = kunden .Where(Function(k) k.Name="Hans") _ .SelectMany(Function(k) k.Best) _ .Where(Function(o) o.Menge > 6) _ .Select(Function(o) o.Nummer)
6.4.8 Sortieroperatoren Sortieroperatoren ermöglichen eine Sortierung von Elementen in Ausgabefolgen mit einer angegebenen Sortierrichtung. Mit dem Operator Order By können Sie auf- und absteigend sortieren, mit Order By Descending nur absteigend. Hier sehen Sie ein Beispiel für eine aufsteigende Sortierung. Dabei werden die Größen aller Äpfel der Reihe nach in die Ergebnisliste geschrieben. Class Apfel Public Farbe As String, Größe As Integer, Preis As Double
524
LINQ
Sub New(ByVal f As String, ByVal g As Integer, ByVal p As Double) Farbe = f : Größe = g : Preis = p End Sub End Class Dim ap() As Apfel = New Apfel() {New Apfel("Rot", 50, 1.3), _ New Apfel("Grün", 70, 1.4), New Apfel("Rot", 60, 1.4), _ New Apfel("Grün", 40, 1.1)} For Each af In _ (From a In ap Order By a.Größe Select New With {a.Größe, a.Preis}) Console.WriteLine("{" & af.Größe & "," & af.Preis & "}") Next
Hinweis Durch Klammerung der LINQ-Syntax kann sie mit Visual Basic gemischt werden.
Sehen wir uns diese LINQ-Abfrage noch in der Erweiterungsmethodensyntax an: For Each af In ap.OrderBy(Function(a) a.Größe) _ .Select(Function(a) New With {a.Größe, a.Preis}) Console.WriteLine("{" & af.Größe & "," & af.Preis & "}") Next
Durch die Ergänzung von Descending bekommen wir eine absteigende Sortierung: ... Order By a.Größe Descending ...
Das folgende Codefragment zeigt, wie Sie mit dem Operator OrderByDescending zum gleichen Ergebnis kommen: For Each af In ap.OrderByDescending(Function(a) a.Größe) _ .Select(Function(a) New With {a.Größe, a.Preis}) Console.WriteLine("{" & af.Größe & "," & af.Preis & "}") Next
Wenn Sie mehrere Sortierkriterien festlegen wollen, helfen Ihnen die beiden Operatoren Then By und Then By Descending weiter. Deren Einsatz setzt aber voraus, dass vorher Order By oder Order By Descending verwendet worden sind. Sortieren wir die Äpfel nach ihrem Preis und danach nach ihrer Größe. Nehmen wir an, die erste Sortierung soll die Bestellmenge berücksichtigen und die zweite, ob die Bestellung bereits ausgeliefert ist. Die Anweisung dazu lautet: For Each af In ap.OrderBy(Function(a) a.Preis) _ .ThenBy(Function(a) a.Größe) _ .Select(Function(a) New With {a.Größe, a.Preis}) Console.WriteLine("{" & af.Größe & "," & af.Preis & "}") Next
525
6.4
6
Collections und LINQ
Manchmal kann es vorkommen, dass Sie die gesamte Ergebnisliste in umgekehrter Reihenfolge benötigen. Hier kommt der Operator Reverse zum Einsatz, der am Ende auf die Ergebnisliste angewendet wird: For Each af In ap.OrderBy(Function(a) a.Größe) _ .Select(Function(a) New With {a.Größe, a.Preis}) _ .Reverse() Console.WriteLine("{" & af.Größe & "," & af.Preis & "}") Next
Wie oben bereits erwähnt wurde, können Sie LINQ- und Visual-Basic-Syntax mischen, wenn Sie die LINQ-Abfrage in runde Klammern setzen. So können Sie Reverse auch dann anwenden. Wie Sie wissen, werden einige Abfrageoperatoren als Schlüsselwörter von C# angeboten und gestatten die sogenannte Abfragesyntax. Reverse und ThenBy zählen nicht dazu. Möchten Sie die von einer Abfragesyntax gelieferte Ergebnismenge umkehren, können Sie sich eines kleinen Tricks bedienen. Sie schließen die Abfragesyntax in runde Klammern ein und können darauf den Punktoperator mit folgendem Reverse angeben: For Each af In _ (From a In ap Order By a.Größe Select New With {a.Größe, a.Preis}) _ .Reverse() Console.WriteLine("{" & af.Größe & "," & af.Preis & "}") Next
6.4.9 Gruppieren mit GroupBy Manchmal ist es notwendig, Ergebnisse anhand spezifischer Kriterien zu gruppieren. Dazu dient der Operator Group By. Das folgende Beispiel gruppiert Personen nach Geschlecht. '...\Sammlungen\Linq\GroupBy.vb
Option Strict On Namespace Sammlungen Module GroupBy Class Person Public Geschlecht, Name As String Sub New(ByVal g As String, ByVal n As String) Geschlecht = g : Name = n End Sub End Class Sub Test() Dim pp() As Person = New Person() {New Person("m", "Harry"), _ New Person("w", "Sally"), New Person("w", "Liesl"), _ New Person("m", "Karl")} For Each ps As IGrouping(Of String, Person) In _ pp.GroupBy(Function(p) p.Geschlecht)
526
LINQ
Console.Write("Geschlecht {0}: ", ps.Key) For Each pg In ps Console.Write(pg.Name & " ") Next Console.WriteLine() Next Console.ReadLine() End Sub End Module End Namespace
Die Gruppierung ist wie erwartet: Geschlecht m: Harry Karl Geschlecht w: Sally Liesl
Der Operator GroupBy ist vielfach überladen. In der folgenden Syntax sind optionale Parameter kursiv gesetzt: Public Shared Function GroupBy(Of TSource, TKey, TElement)( _ source As IEnumerable(Of TSource), _ keySelector As Func(Of TSource, TKey), _ elementSelector As Func(Of TSource, TElement), _ comparer As IEqualityComparer(Of TKey) _ ) As IEnumerable(Of IGrouping(Of TKey, TElement)) Public Shared Function GroupBy(Of TSource, TKey, TElement, TResult)( _ source As IEnumerable(Of TSource), _ keySelector As Func(Of TSource, TKey), _ elementSelector As Func(Of TSource, TElement), _ resultSelector As Func(Of TKey, IEnumerable(Of TElement), TResult), _ comparer As IEqualityComparer(Of TKey) _ ) As IEnumerable(Of TResult)
Die Schnittstelle IGrouping(Of TKey, TElement) ist eine spezialisierte Form von IEnumerable(Of T) : Public Interface IGrouping(Of TKey, TElement) Inherits IEnumerable(Of TElement), IEnumerable ReadOnly Property Key As TKey End Interface
Betrachten wir nun die äußere Schleife: For Each ps As IGrouping(Of String, Person) In _ pp.GroupBy(Function(p) p.Geschlecht)
Der Schnittstelle IGrouping weisen Sie im Typparameter den Datentyp des Elements zu, nach dem gruppiert werden soll. Der zweite Typparameter beschreibt den Typ des zu gruppierenden Elements.
527
6.4
6
Collections und LINQ
Die äußere Schleife beschreibt die einzelnen Gruppen und gibt als Resultat alle die Elemente zurück, die zu der entsprechenden Gruppe gehören. In unserem Beispielcode wird diese Untergruppe in der Variablen pg erfasst. In der inneren Schleife werden anschließend alle Elemente von ps durchlaufen und die gewünschten Informationen ausgegeben.
6.4.10 Verknüpfungen mit Join Mit dem Join-Operator definieren Sie Beziehungen zwischen mehreren Auflistungen, ähnlich wie Sie in SQL mit dem gleichnamigen Join-Statement Tabellen miteinander in Beziehung setzen. Im folgenden Beispiel werden die Daten von Produkten und Bestellungen mit gleicher Nummer zusammengefasst. '...\Sammlungen\Linq\Join.vb
Option Strict On Namespace Sammlungen Module Join Public Class Produkt Public Nummer As Integer, Name As String Sub New(ByVal n As Integer, ByVal nm As String) Nummer = n : Name = nm End Sub End Class Public Class Order Public Nummer, Menge As Integer Sub New(ByVal n As Integer, ByVal m As Integer) Nummer = n : Menge = m End Sub End Class Sub Test() Dim pd() As Produkt = New Produkt() { _ New Produkt(22, "Tasse"), New Produkt(34, "Becher")} Dim od() As Order = New Order() {New Order(22, 100), _ New Order(34, 200), New Order(22, 50), New Order(34, 100)} For Each det In _ od.Join(pd, Function(o) o.Nummer, Function(p) p.Nummer, _ Function(o, p) New With {p.Nummer, p.Name, o.Menge}) Console.Write("{{{0},{1},{2}}} ", det.Name, det.Nummer, det.Menge) Next Console.ReadLine() End Sub End Module End Namespace
528
LINQ
Es werden alle Bestellungen mit einem Produktnamen versehen. {Tasse,22,100} {Becher,34,200} {Tasse,22,50} {Becher,34,100}
Der Join-Operator ist überladen. Der letzte Parameter ist optional: Public Shared Function Join(Of TOuter, TInner, TKey, TResult)( _ outer As IEnumerable(Of TOuter), _ inner As IEnumerable(Of TInner), _ outerKeySelector As Func(Of TOuter, TKey), _ innerKeySelector As Func(Of TInner, TKey), _ resultSelector As Func(Of TOuter, TInner, TResult), _ comparer As IEqualityComparer(Of TKey) _ ) As IEnumerable(Of TResult)
Join wird als Erweiterungsmethode der Liste definiert, auf die Join aufgerufen wird. In unserem Beispiel ist es die Liste od aller Bestellungen. Die innere Liste wird durch das Argument beschrieben und ist in unserem Beispielcode die Liste aller Produkte pd. Als zweites Argument erwartet Join im Parameter outerKeySelector das Schlüsselfeld der äußeren Liste (hier: od), das im vierten Argument mit dem Schlüsselfeld der inneren Liste (hier: pd) in Beziehung gesetzt wird.
Im vierten Argument wird die Ergebnisliste bestimmt. Dazu werden zwei Parameter übergeben: Der erste projiziert ein Element der äußeren Liste, der zweite ein Element der inneren Liste in das Ergebnis der Join-Abfrage. Die Schlüssel (in unserem Beispiel werden dazu die Felder genommen), die die Nummer beschreiben, haben den generischen Typ TKey, die Ergebnisliste ist vom Typ TResult. Sie können eine Join-Abfrage auch in Abfragesyntax notieren: For Each det In _ From o In od Join p In pd On o.Nummer Equals p.Nummer _ Select New With {p.Nummer, p.Name, o.Menge} Console.Write("{{{0},{1},{2}}} ", det.Name, det.Nummer, det.Menge) Next
Sie sollten darauf achten, dass Sie beim Vergleich links von Equals den Schlüssel der äußeren Liste angeben und rechts davon den der inneren. Wenn Sie beide vertauschen, erhalten Sie einen Compilerfehler.
Der Operator GroupJoin Join führt Daten aus der linken und rechten Liste genau dann zusammen, wenn die angegebenen Kriterien alle erfüllt sind. Ist eines oder sind mehrere der Kriterien nicht erfüllt, so entsteht kein Datensatz in der Ergebnismenge. Damit entspricht der Join-Operator dem INNER JOIN-Statement einer SQL-Abfrage. Das Äquivalent zu einem LEFT OUTER JOIN oder RIGHT OUTER JOIN implementiert der GroupJoin-Operator.
529
6.4
6
Collections und LINQ
Im folgenden Beispiel werden die Bestellungen aller Kunden nach Produkt gruppiert ausgegeben. '...\Sammlungen\Linq\GroupJoin.vb
Option Strict On Namespace Sammlungen Module GroupJoin Public Class Produkt Public Nummer As Integer, Name As String Sub New(ByVal n As Integer, ByVal nm As String) Nummer = n : Name = nm End Sub End Class Public Class Order Public Nummer, Menge As Integer Sub New(ByVal n As Integer, ByVal m As Integer) Nummer = n : Menge = m End Sub End Class Public Class Kunde Public Name As String, best() As Order Sub New(ByVal n As String, ByVal ParamArray b() As Order) Name = n : best = b End Sub End Class Sub Test() Dim pd() As Produkt = New Produkt() { New Produkt(12, "Glas"), _ New Produkt(22, "Tasse"), New Produkt(34, "Becher")} Dim od() As Order = New Order() {New Order(22, 100), _ New Order(34, 200), New Order(22, 50), New Order(34, 100)} Dim kd() As Kunde = New Kunde() {New Kunde("Hans", od(1), od(0)), _ New Kunde("Peter", od(3), od(0)), New Kunde("Willi", od(2))} For Each det In _ pd.GroupJoin(kd.SelectMany(Function(k) k.best), _ Function(p) p.Nummer, Function(o) o.Nummer, _ Function(p, o) New With {p.Nummer, p.Name, .best = o}) Console.Write("{{{0},{1},{{ ", det.Name, det.Nummer) For Each o In det.best Console.Write("{{{0},{1}}} ", o.Nummer, o.Menge) Next Console.WriteLine("}} ") Next Console.ReadLine() End Sub
530
LINQ
End Module End Namespace
Alle Bestellungen werden erfasst, ebenso das nicht bestellte Produkt: {Glas,12,{ }} {Tasse,22,{ {22,100} {22,100} {22,50} }} {Becher,34,{ {34,200} {34,100} }}
GroupJoin arbeitet sehr ähnlich wie der Join-Operator. Der Unterschied zwischen den beiden
Operatoren besteht in dem, was in die Ergebnismenge aufgenommen wird. Mit Join sind es nur Daten, deren Schlüssel sowohl in der outer-Liste als auch der inner-Liste vertreten sind. Findet Join in der inner-Liste kein passendes Element, wird das outer-Element nicht in die Ergebnisliste aufgenommen. Ganz anders ist das Verhalten von GroupJoin. Dieser Operator nimmt auch dann ein Element aus der outer-Liste in die Ergebnisliste auf, wenn keine entsprechenden Daten in inner vorhanden sind (siehe die erste Ausgabezeile oben). Sie können den Group Join-Operator auch in einem Abfrageausdruck beschreiben. Er wird mit Group Join... Into... definiert. For Each det In From p In pd _ Group Join c In (From k In kd From o In k.best Select o) _ On p.Nummer Equals c.Nummer Into Group _ Select New With {p.Nummer, p.Name, .best = Group} Console.Write("{{{0},{1},{{ ", det.Name, det.Nummer) For Each o In det.best Console.Write("{{{0},{1}}} ", o.Nummer, o.Menge) Next Console.WriteLine("}} ") Next
6.4.11 Die Set-Operator-Familie Distinct Vielleicht kennen Sie die Wirkungsweise von DISTINCT bereits von SQL. In LINQ hat der Distinct-Operator die gleiche Aufgabe: Er garantiert, dass in der Ergebnismenge ein Element nicht doppelt auftritt. Dim cities() As string = New String() { _ "Aachen", "Köln", "Bonn", "Aachen", "Bonn", "Frankfurt"} For Each c In (From p In cities Select p).Distinct() Console.Write(c & " ") Next
Im Array cities kommen die beiden Städte Aachen in Bonn je zweimal vor. Der auf die Ergebnismenge angewendete Distinct-Opertor erkennt dies und sorgt dafür, dass jede Stadt nur einmal angezeigt wird.
531
6.4
6
Collections und LINQ
Union Der Union-Operator verbindet zwei Listen miteinander. Dabei werden doppelte Vorkommen ignoriert. Dim cities() As String = New String() { _ Aachen", "Bonn", "Aachen", "Frankfurt"} Dim namen() As String = New String() {"Peter", "Willi", "Hans"} For Each u In cities.Union(namen) Console.Write(u & " ") Next
In der Ergebnisliste werden der Reihe nach Aachen, Köln, Bonn, Frankfurt, Peter, Willi und Hans erscheinen.
Intersect Der Intersect-Operator bildet eine Ergebnisliste aus zwei anderen Listen. In der Ergebnisliste sind aber nur die Elemente enthalten, die in beiden Listen gleichermaßen enthalten sind. Intersect bildet demnach eine Schnittmenge ab. Dim cities() As String = New String() { _ "Aachen", "Köln", "Bonn", "Aachen", "Frankfurt"} Dim namen() As String = New String() { _ "Düsseldorf", "Bonn", "Bremen", "Köln"} For Each u In cities.Intersect(namen) Console.Write(u & " ") Next
Das Ergebnis wird durch die Städte Köln und Bonn gebildet.
Except Während Intersect die Gemeinsamkeiten aufspürt, sucht der Operator Except nach allen Elementen, durch die sich die Listen voneinander unterscheiden. Dabei sind nur die Elemente in der Ergebnisliste enthalten, die in der ersten Liste angegeben sind und in der zweiten Liste fehlen. Wenn Sie in dem Codefragment anstelle von Intersect den Operator Except verwenden, enthält die Ergebnisliste die Orte Aachen und Frankfurt.
6.4.12 Die Familie der Aggregatoperatoren LINQ stellt mit Count, LongCount, Sum, Min, Max, Average und Aggregate eine Reihe von Aggregatoperatoren zur Verfügung, um Berechnungen an Quelldaten durchzuführen.
Die Operatoren Count und LongCount Sehr einfach einzusetzen sind die beiden Operatoren Count und LongCount. Beide unterscheiden sich dahingehend, dass Count einen Integer als Typ zurückgibt und LongCount einen Long.
532
LINQ
Dim x(7) As Integer Console.WriteLine("Anzahl {0}", x.Count()) Console.WriteLine("Anzahl {0}", x.LongCount())
Sum Der Operator liefert eine Summe als Ergebnis der LINQ-Abfrage. Im folgenden Codefragment wird die Summe aller Werte ermittelt, die das Array bilden. Das Ergebnis ist 114. Dim x() As Integer = New Integer() {1, 3, 7, 4, 99} Console.WriteLine("Anzahl {0}", x.Sum())
Das folgende Beispiel ist nicht mehr so einfach. Hier soll der Gesamtbestellwert über alle Produkte für jeden Kunden ermittelt werden. '...\Sammlungen\Linq\Summe.vb
Option Strict On Namespace Sammlungen Module Summe Public Class Produkt Public Nummer As Integer, Name As String, Preis As Double Sub New(ByVal n As Integer, ByVal nm As String, ByVal p As Double) Nummer = n : Name = nm : Preis = p End Sub End Class Public Class Order Public Nummer, Menge As Integer Sub New(ByVal n As Integer, ByVal m As Integer) Nummer = n : Menge = m End Sub End Class Public Class Kunde Public Name As String, best() As Order Sub New(ByVal n As String, ByVal ParamArray b() As Order) Name = n : best = b End Sub End Class Sub Test() Dim pd() As Produkt = New Produkt() {New Produkt(12, "Glas", 2.1), _ New Produkt(22, "Tasse", 1.9), New Produkt(34, "Becher", 1.7)} Dim od() As Order = New Order() {New Order(22, 100), _ New Order(34, 200), New Order(22, 50), New Order(34, 100)} Dim kd() As Kunde = New Kunde() {New Kunde("Hans", od(1), od(0)), _ New Kunde("Peter", od(3), od(0)), New Kunde("Willi", od(2))} Dim allOrders = From k In kd From o In k.best _ Join p In pd On o.Nummer Equals p.Nummer _ Select New With {k.Name, o.Nummer, .Volumen = o.Menge * p.Preis}
533
6.4
6
Collections und LINQ
Dim summe = From k In kd Group Join o In allOrders _ On k.Name Equals o.Name Into Group _ Select New With _ {k.Name, .TotalSumme = Group.Sum(Function(s) s.Volumen)} For Each v In summe Console.WriteLine("Name {0} Summe {1}", v.Name, v.TotalSumme) Next Console.ReadLine() End Sub End Module End Namespace
Hier ist das Ergebnis: Name Hans Summe 530 Name Peter Summe 360 Name Willi Summe 95
Analysieren wir den Code schrittweise, und überlegen wir, was das Resultat des folgenden Abfrageteilausdrucks ist. Dim allOrders = From k In kd From o In k.best _ Join p In pd On o.Nummer Equals p.Nummer _ Select New With {k.Name, o.Nummer, .Volumen = o.Menge * p.Preis}
Zuerst ist es notwendig, die Bestellungen aus jedem kd-Objekt zu filtern. Danach wird ein Join gebildet, der die Nummer aus den einzelnen Bestellungen eines Kunden mit der Nummer aus der Liste der Artikel verbindet. Nun gilt es noch, die Ergebnisliste nach den Kunden zu gruppieren und dann die Gesamtsumme aller Bestellungen zu bilden. Dim summe = From k In kd Group Join o In allOrders _ On k.Name Equals o.Name Into Group _ Select New With {k.Name, .TotalSumme = Group.Sum(Function(s) s.Volumen)}
Wir sollten uns daran erinnern, dass der GroupJoin-Operator mit diesen Fähigkeiten ausgestattet ist. Es müssen zuerst die beiden Listen kd und allOrders zusammengeführt werden. Sie können sich das so vorstellen, dass die Gruppierung mit GroupJoin zur Folge hat, dass für jeden Kunden eine eigene »Tabelle« erzeugt wird, in der alle seine Bestellungen beschrieben sind. Die Variable s steht hier für ein Gruppenelement, letztendlich also für eine Bestellung. Die Gruppierung nach Kunde-Objekten gestattet es uns nun, mit dem Operator Sum den Inhalt der Spalte Volumen zu summieren.
Die Operatoren Min, Max und Average Die Aggregatoperatoren Min und Max ermitteln den minimalen bzw. maximalen Wert in einer Datenliste, und Average ermittelt das arithmetische Mittel. Grundsätzlich ist der Einsatz der Operatoren sehr einfach, wie das folgende Codefragment exemplarisch an Max zeigt: Dim max = (From p in Products Select p.Price).Max()
534
LINQ
Das funktioniert aber auch nur, solange numerische Werte als Datenquelle vorliegen. Sie brauchen den Code nur wie folgt leicht zu ändern, um festzustellen, dass nun eine ArgumentException ausgelöst wird. Dim max = (From p in Products Select New With { p.Price }).Max()
Die Meldung zu der Exception besagt, dass mindestens ein Typ die IComparable-Schnittstelle implementieren muss. In der ersten funktionsfähigen Version des Codes stand in der Ergebnisliste ein numerischer Wert, der der Forderung entspricht. Im zweiten, fehlerverusachenden Codefragment hingegen wird ein anonymer Typ benutzt, der mit der geforderten Schnittstelle überhaupt nicht dienen kann. Die Lösung dieser Problematik ist nicht schwer. Die Operatoren sind alle so überladen, dass ihnen einen Wertselektor übergeben werden kann. Mit anderen Worten: Geben Sie das gewünschte Element aus der Liste der Elemente, die den anonymen Typ bilden, als zu bewertenden Ausdruck an: Dim max = (From p in Products Select New With { p.Price }) _ .Max(Function(x) x.Price)
6.4.13 Generierungsoperatoren Range Dieser Operator liefert ausgehend von einem Startwert eine Gruppe von Integerwerten, die aus einem spezifizierten Wertebereich ausgewählt werden. Die Definition des Operators lautet wie folgt: Public Shared Function Range(start As Integer, count As Integer) _ As IEnumerable(Of Integer)
Bei genauer Betrachtung ist dieser Operator mit einer For-Schleife vergleichbar. Sie übergeben dem ersten Parameter den Startwert und teilen mit dem zweiten Parameter mit, wie oft eine bestimmte Operation ausgeführt werden soll. Der Range-Operator ist gut geeignet, um mathematische Operationen zu codieren. Dies demonstriert der folgende Code: Dim nums = Enumerable.Range(1, 10).Select(Function(x) 2 * x) For Each num In nums : Console.Write(num & " ") : Next
Repeat Der Repeat-Operator arbeitet ähnlich wie der zuvor besprochene Range-Operator. Repeat gibt eine Gruppe zurück, in der dasselbe Element mehrfach enthalten ist. Die Anzahl der Wiederholungen ist dabei festgelegt. Auch zu diesem Operator wollen wir uns zunächst die Definition ansehen: Public Shared Function Repeat(Of TResult)( _ element As TResult, count As Integer) As IEnumerable(Of TResult)
535
6.4
6
Collections und LINQ
Dem ersten Parameter übergeben Sie das Element, das wiederholt werden soll. Dem zweiten Parameter teilen Sie die Anzahl der Wiederholungen mit. Mit For Each s In Enumerable.Repeat("S&N", 3) Console.Write(s & " ") Next
wird beispielsweise die Zeichenfolge dreimal ausgegeben.
6.4.14 Quantifizierungsoperatoren Wenn Sie beabsichtigen, die Existenz von Elementen in einer Liste anhand von Bedingungen oder definierten Regeln zu überprüfen, helfen die Quantifizierungsoperatoren Ihnen weiter.
Any Any ist ein Operator, der ein Prädikat auswertet und einen booleschen Wert zurückliefert.
Nehmen wir an, Sie möchten wissen, ob der Kunde Willi auch das Produkt mit der ID = 6 bestellt hat. Any hilft Ihnen dabei, das festzustellen. Dim res As Boolean = (From cust in Customers From ord in cust.Orders _ Where cust.Name == "Willi" Select New With { ord.ProductID }) _ .Any(ord => ord.ProductID == 7) Console.WriteLine("ProductID 3 ist {0}enthalten", If(res, "", "nicht "))
Die Elemente werden so lange ausgewertet, bis der Operator auf ein Element stößt, das die Bedingung erfüllt.
All Während Any schon True liefert, wenn für ein Element die Bedingung erfüllt ist, liefert der Operator All nur dann True, wenn alle untersuchten Elemente der Bedingung entsprechen. Möchten Sie beispielsweise feststellen, ob alle Preise der Einzelprodukte > 3 sind, genügt die folgende LINQ-Abfrage: Dim res As Boolean = Products.All(Function(p) p.Price > 3)
6.4.15 Aufteilungsoperatoren Mit Where und Select filtern Sie eine Datenquelle nach vorgegebenen Kriterien. Das Ergebnis ist anschließend eine neue Menge von Daten, die den Kriterien entspricht. Möchten Sie nur eine Teilmenge der Datenquelle betrachten, ohne Filterkriterien einzusetzen, eignen sich die Aufteilungsoperatoren.
Take Sie könnten zum Beispiel daran interessiert sein, nur die ersten drei Produkte aus der Liste aller Produkte auszugeben. Mit dem Take-Operator ist das sehr einfach zu realisieren: Dim arr() As String = New String() {"sieben", "neun", "acht", "drei"} For Each s In arr.Take(3) : Console.Write(s & " ") : Next
536
LINQ
TakeWhile Der Operator Take basiert auf einem Integer als Zähler. Sehr ähnlich arbeitet auch TakeWhile. Der Unterschied zum zuvor behandelten Operator ist, dass Sie ein Prädikat angeben können, das als Kriterium der Filterung angesehen wird. TakeWhile durchläuft die Datenquelle und gibt das gefundene Element zurück, wenn das Ergebnis der Prüfung True ist. Beendet wird der Durchlauf unter zwei Umständen: 왘
Das Ende der Datenquelle ist erreicht.
왘
Das Ergebnis einer Untersuchung lautet False.
Wir wollen uns das an einem Beispiel ansehen. Auch dabei wird als Quelle auf die Liste der Produkte zurückgegriffen. Das Prädikat sagt aus, dass die Werte in der Ergebnisliste erfasst werden sollen, solange sie positiv sind: Dim nr() As Integer = New Integer() {3, 8, 3, –2, 7, 1} For Each no In nr.TakeWhile(Function(x) x > 0) Console.Write(no & " ") Next
Beachten Sie, dass in der Ergebnisliste 7 und 1 fehlen, da die Schleife vorher beendet wird.
Skip und SkipWhile Take und TakeWhile werden um Skip und SkipWhile ergänzt. Skip überspringt eine bestimmte Anzahl von Elementen in einer Datenquelle. Der verblei-
bende Rest bildet die resultierende Ergebnismenge. Um zum Beispiel die ersten beiden in der Liste enthaltenen Produkte aus der Ergebnisliste auszuschließen, codieren Sie die folgenden Anweisungen: Dim res = (From prod in prods _ Select New With { prod.ProductName, prod.Price }).Skip(2);
SkipWhile erwartet ein Prädikat. Die Elemente werden damit verglichen. Dabei werden die Elemente so lange übersprungen, wie das Ergebnis der Überprüfung True liefert. Sobald eine Überprüfung False ist, werden das betreffende Element und alle Nachfolgeelemente in die Ergebnisliste aufgenommen.
Das Prädikat im folgenden Codefragment sucht in der Liste aller Produkte nach dem ersten Produkt, für das die Bedingung nicht gilt, dass der Preis > 3 ist. Dieses und alle darauf folgenden Elemente werden in die Ergebnisliste geschrieben. Dim res = (From prod in prods Select New With { prod.ProductName, prod.Price }) .SkipWhile(Function(x) x.Price > 3 )
6.4.16 Die Elementoperatoren Bisher lieferten uns alle Operatoren immer eine Ergebnismenge zurück. Oft möchten Sie aber aus einer Liste ein bestimmtes einzelnes Element herausfinden. Hierbei unterstützen uns die Operatoren, denen wir uns nun widmen.
537
6.4
6
Collections und LINQ
First Dieser Operator sucht das erste Element in einer Datenquelle. Wegen der Überladung kann es sich um das von der Position her erste Element handeln oder um das erste Element einer mit einem Prädikat gebildeten Ergebnisliste. Das folgende Beispiel zeigt, wie einfach der Einsatz von First ist. Als Ergebnis werden sieben für das prädikatlose und acht für das prädikatbehaftete Kommando ausgegeben. Dim arr() As String = New String() {"sieben", "neun", "acht", "drei"} Console.WriteLine(arr.First()) Console.WriteLine(arr.First(Function(x) x(0) < "g"c))
FirstOrDefault Versuchen Sie einmal, das letzte Codefragment mit dem Prädikat Function(x) x(0) < "a"c)
auszuführen. Sie werden eine Fehlermeldung erhalten, weil in der Datenquelle kein Element enthalten ist, das der genannten Bedingung entspricht. In solchen Fällen empfiehlt es sich, anstelle des Operators First den Operator FirstOrDefault zu benutzen. Für den Fall, dass kein Element gefunden wird, liefert der Operator den Standardwert des Datentyps zurück. Handelt es sich um einen Referenztyp, ist das Nothing. FirstOrDefault liegt ebenfalls in zwei Überladungen vor. Sie können neben der parameter-
losen Variante auch die parametrisierte Überladung benutzen, der Sie das gewünschte Prädikat übergeben: Dim arr() As String = New String() {"sieben", "neun", "acht", "drei"} Console.WriteLine(arr.FirstOrDefault(Function(x) x(0) < "a"c) Is Nothing)
Last und LastOrDefault Sicherlich können Sie sich denken, dass die beiden Operatoren Last und LastOrDefault Ergänzungen der beiden im Abschnitt zuvor behandelten Operatoren sind. Beide operieren auf die gleich Weise wie First und FirstOrDefault, nur dass das letzte Element der Liste das Ergebnis bildet. Dim arr() As String = New String() {"sieben", "neun", "acht", "drei"} Console.WriteLine(arr.Last()) Console.WriteLine(arr.Last(Function(x) x(0) > "g"c))
Single und SingleOrDefault Alle bislang vorgestellten Elementoperatoren lieferten eine Ergebnismenge, aus der ein Element herausgelöst wurde: Entweder liefern sie das erste oder das letzte Element. Mit Single bzw. SingleOrDefault können Sie nach einem bestimmten, eindeutigen Element Ausschau halten. Eindeutig bedeutet in diesem Zusammenhang, dass es kein Zwischenergebnis gibt, aus dem anschließend ein Element das Ergebnis bildet, vergleichbar mit der Primärschlüsselspalte einer Datenbanktabelle.
538
LINQ
Mit Single und SingleOrDefault prüfen Sie auf eine einelementige Liste. Hat eine Liste mehrere Elemente, wird eine InvalidOperationException ausgelöst. Auch für dieses Pärchen gilt: Besteht die Möglichkeit, dass mehr als ein Element gefunden wird, sollten Sie den Operator SingleOrDefault einsetzen, der – wie bei den anderen Operatoren auch – gegebenenfalls Standardwerte als Rückgabewert liefert und keine Ausnahme auslöst wie Single in diesem Fall. Sie können beide Operatoren parameterlos aufrufen oder ein Prädikat angeben. Dim arr() As String = New String() {"vier"} Console.WriteLine(arr.Single()) arr = New String() {"sieben", "neun", "acht", "drei"} Console.WriteLine(arr.Single(Function(x) x(0) > "o"c))
ElementAt und ElementOrDefault Wenn Sie ein bestimmtes Element anhand seiner Position aus einer Liste extrahieren möchten, sollten Sie entweder die Methode ElementAt oder die Methode ElementAtOrDefault verwenden. Dim arr() As String = New String() {"sieben", "neun", "acht", "drei"} Console.WriteLine(arr.ElementAt(2)) Console.WriteLine(arr.ElementAtOrDefault(10) Is Nothing)
Beide Methoden erwarten die Angabe des Index in der Liste. Da Listen nullbasiert sind, wird bei der Angabe »3« das vierte Element extrahiert. ElementAtOrDefault liefert wieder den Standardwert, falls der Index negativ oder größer als die Elementanzahl ist.
DefaultIfEmpty Standardmäßig liefert dieser Operator eine Liste von Elementen ab. Sollte die Liste jedoch leer sein, führt dieser Operator nicht sofort zu einer Ausnahme. Stattdessen ist der Rückgabewert dann entweder der Standardwert oder – falls Sie die überladene Fassung von DefaultIfEmpty eingesetzt haben – ein spezifischer Wert. Im Beispiel wird 77 ausgegeben. Dim arr(-1) As Integer For Each i As Integer In arr.DefaultIfEmpty(77) Console.Write(i & " ") Next
539
6.4
Die Kommunikation mit der Welt außerhalb des Hauptspeichers ist Thema dieses Kapitels. Ein Kommunikationskanal wird Stream (dt. Strom) genannt. Ein Beispiel ist eine Datei. Nachdem wir uns Dateizugriffe angesehen haben, betrachten wir allgemeine Streams.
7
Eingabe und Ausgabe
In diesem Kapitel geht es primär darum, Dateninformationen aus einer beliebigen Datenquelle zu holen und an ein beliebiges Ziel zu schicken. Meist sind sowohl die Quelle als auch das Ziel eines Datenstroms Dateien, aber es kann auch noch ganz andere Anfangs- und Endpunkte geben, beispielsweise: 왘
eine Benutzeroberfläche
왘
Netzwerkverbindungen
왘
Speicherblöcke
왘
Drucker
왘
andere Peripheriegeräte
In gehobenen Programmiersprachen wird ein Datenfluss als Stream bezeichnet. Ein Stream hat einen Anfangs- und einen Endpunkt: eine Quelle, aus der der Datenstrom entspringt, und das Ziel, das den Datenstrom empfängt. Die Methoden Console.WriteLine und Console.ReadLine, mit denen wir praktisch schon von der ersten Seite dieses Buches an arbeiten, erzeugen auch solche Datenströme (siehe Abbildung 7.1).
Streams
Abbildung 7.1
Datenströme einer lokalen Arbeitsstation
541
7
Eingabe und Ausgabe
Die jedem Stream eigenen Charakteristika werden auf verschiedene Stream-Klassen abgebildet. Jeder Stream dient ganz speziellen Anforderungen. Beispielsweise gibt es Streams, deren Daten direkt als Text interpretiert werden, während andere nur Bytesequenzen transportieren, die der Empfänger zur Interpretation erst in das richtige Format bringen muss. Ein Stream ist nicht dauerhaft, er wird geöffnet und liest oder schreibt Daten. Nach dem Schließen sind die Daten verloren, wenn sie nicht von einem Empfänger, beispielsweise einer Datei, dauerhaft gespeichert werden.
7.1
Namensräume der Ein- bzw. Ausgabe
Mit externen Daten zu operieren, setzt Eingabe- und Ausgabe-Operationen (E/A-Operationen; englisch Input/Output, kurz I/O) voraus. Die in diesem Zusammenhang wichtigsten Klassen stehen im Namensraum System.IO mit folgendem grob umrissenen Inhalt: 왘
Klassen, die ihre Dienste auf der Basis von Dateien und Verzeichnissen anbieten
왘
Klassen, die den Datentransport beschreiben
왘
Ausnahmeklassen
Speziellere Aufgaben werden von den Klassen anderer Namensräume erledigt: 왘
System.IO.Compression bietet mit DeflateStream und GZipStream zwei Klassen zur Datenkomprimierung bzw. Datendekomprimierung.
왘
System.IO.IsolatedStorage stellt eine Art virtuelles Dateisystem zur Verfügung. Meist
wird es zur Speicherung anwendungsbezogener temporärer Daten verwendet, zum Beispiel als Ersatz für eine temporäre Registry. Weniger vertrauenswürdiger Code kann auf die dort befindlichen Daten nicht zugreifen. 왘
System.IO.Ports stellt Klassen zur Implementierung von eigenen Streams zur Verfügung,
die nicht auf das Dateisystem zugreifen wie zum Beispiel die serielle Schnittstelle.
7.2
Ausnahmebehandlung
Bei fast allen Dateioperationen kann es zur Laufzeit eines Programms aus den verschiedensten Gründen sehr schnell zum Auslösen von Ausnahmen kommen: Die zu kopierende Datei wird im angegebenen Pfad nicht gefunden, das Zielverzeichnis existiert nicht, als Quelle oder Ziel wird ein Leerstring übergeben usw. Daher sollten Sie unbedingt darauf achten, eine Fehlerbehandlung zu implementieren. Die Dokumentation beschreibt alle Ausnahmen, die beim Aufruf einer Methode auftreten könnten. Alle Ausnahmen im Zusammenhang mit E/A-Operationen werden auf eine gemeinsame Basis zurückgeführt: IOException. Sie sollten auch diesen allgemeinen Fehler immer behandeln, damit der Anwender nicht Gefahr läuft, durch eine unberücksichtigte Ausnahme das Programms unfreiwillig zu beenden. In den Beispielen hier wird meist darauf verzichtet, um die Quelltexte übersichtlich zu halten.
542
Dateien und Verzeichnisse
7.3
Dateien und Verzeichnisse
Die Klassenbibliothek des .NET Frameworks unterstützt den Entwickler mit den in Tabelle 7.1 aufgelisteten Klassen, die Laufwerke, Verzeichnisse und Dateien beschreiben. Klasse
Beschreibung
File
Klassengebundene Methoden zum Zugriff auf eine Datei als Ganzes
FileInfo
Instanzmethoden zum Zugriff auf eine Datei als Ganzes
Directory
Klassengebundene Methoden zum Zugriff auf ein Verzeichnis
DirectoryInfo
Instanzmethoden zum Zugriff auf ein Verzeichnis
Path
Klassengebundene Methoden für Pfadangaben
DriveInfo
Instanzmethoden für Laufwerksinformationen
SpecialDirectories
Pfade Windows-eigener Verzeichnisse
Tabelle 7.1
7.3.1
Klassen zur Beschreibung des Dateisystems
Dateizugriff mit File
Die nicht ableitbare Klasse File stellt nur statische Methoden zur Verfügung. Die Methoden von FileInfo sind Instanzmethoden und werden über eine Objektreferenz aufgerufen. Funktionell sind sich beide Klassen recht ähnlich. Mit den Klassenmethoden von File lässt sich eine Datei erstellen, kopieren, löschen usw. Sie können auch die Attribute einer Datei lesen oder setzen, und – was auch sehr wichtig ist – Sie können eine Datei öffnen. In Tabelle 7.2, »Methoden in File«, sind die wichtigsten Methoden samt Rückgabetyp aufgeführt. Methode
Rückgabetyp
Beschreibung
AppendAllText
-
Fügt eine Zeichenfolge an und schließt die Datei.
AppendText
StreamWriter
Hängt Text an eine existierende Datei an.
Copy
-
Kopiert eine bestehende Datei.
Create
FileStream
Erzeugt eine Datei in einem angegebenen Pfad.
CreateText
StreamWriter
Erstellt oder öffnet eine Textdatei.
Delete
-
Löscht eine Datei.
Exists
Boolean
Gibt an, ob die angegebene Datei existiert.
GetAttributes
FileAttributes
Bitfeld der Dateiattribute
GetCreationTime
DateTime
Erstellungsdatum und die Uhrzeit einer Datei
GetLastAccessTime
DateTime
Datum und Uhrzeit des letzten Zugriffs
GetLastWriteTime
DateTime
Datum und Uhrzeit des letzten Schreibzugriffs
Move
-
Verschiebt eine Datei in einen anderen Ordner oder benennt sie um.
Open
FileStream
Öffnet eine Datei.
Tabelle 7.2
Methoden in File
543
7.3
7
Eingabe und Ausgabe
Methode
Rückgabetyp
Beschreibung
OpenRead
FileStream
Öffnet eine Datei zum Lesen.
OpenText
StreamReader
Öffnet eine Textdatei zum Lesen.
OpenWrite
FileStream
Öffnet eine Datei zum Schreiben.
ReadAllBytes
Byte()
Liest binär in ein Byte-Array und schließt die Datei.
ReadAllLines
String()
Liest jede Zeile in ein String und schließt die Datei.
ReadAllText
String
Liest eine Textdatei und schließt sie.
SetAttributes
–
Setzt Dateiattribute.
SetCreationTime
–
Setzt Erstellungsdatum und -uhrzeit.
SetLastAccessTime
–
Setzt Datum und Uhrzeit des letzten Zugriffs.
SetLastWriteTime
–
Setzt Datum und Uhrzeit des letzten Schreibzugriffs.
WriteAllBytes
–
Schreibt ein gegebenes Byte-Array in eine neue Datei.
WriteAllLines
–
Schreibt ein gegebenes String-Array in neue Datei.
WriteAllText
–
Schreibt einen gegebenen Text in eine neue Datei.
Tabelle 7.2
Methoden in File (Forts.)
Alle Methoden, die eine Datei nach der Operation offen lassen, geben ein Stream-Objekt zurück. Mit ihm haben Sie Zugriff auf den Inhalt der Datei. Ein FileStream beschreibt dabei einfache Bytesequenzen, ein StreamReader basiert auf einer Textdatei. Einige typische Dateioperationen wollen wir uns nun näher ansehen.
Kopieren einer Datei Zum Kopieren einer Datei dient die Methode Copy, die einfach überladen ist: Public Shared Sub Copy(sourceFileName As String, destFileName As String) Public Shared Sub Copy(sourceFileName As String, destFileName As String, _ overwrite As Boolean)
Das erste Argument ist der Dateiname der zu kopierenden Datei. Befindet sie sich in keinem bekannten Suchpfad, muss der gesamte Zugriffspfad beschrieben werden. Im zweiten Argument geben Sie das Zielverzeichnis und den Namen der Dateikopie an. Die Betriebssysteme der Quelle und des Ziels bestimmen jeweils, welche Namen erlaubt sind. Befindet sich im Zielverzeichnis bereits eine gleichnamige Datei, bestimmt der dritte Parameter das Verhalten. Fehlt er oder ist er False, wird die Ausnahme DirectoryNotFoundException ausgelöst. Beim Wert True wird kommentarlos überschrieben. Ein Beispiel: File.Copy("C:\Test1.txt", "D:\Test2.txt")
Wenn Sie die Pfadangabe in einem String-Literal festlegen, müssen Sie beachten, dass jedes Anführungszeichen im Pfad gedoppelt werden muss, damit die Zeichenkette nicht vorzeitig beendet wird.
544
Dateien und Verzeichnisse
Im folgenden Beispiel wird der Anwender dazu aufgefordert, den Pfad und das Zielverzeichnis der zu kopierenden Datei einzugeben. Dabei kann es zu verschiedenen Ausnahmen kommen, beispielsweise wenn die zu kopierende Datei nicht gefunden wird oder der Anwender das Quell- oder Zielverzeichnis an der Konsole nicht angibt. Diese Fehler werden im Code aufgefangen und behandelt. Unspezifische oder vergessene Fehler von IO-Operationen fängt der letzte Catch-Zweig ab. '...\IO\Dateien\Kopieren.vb
Option Strict On Imports System.IO Namespace EA Module Kopieren Sub Test() Dim source, destination As String Console.Write("Datei zum Kopieren: ") : source = Console.ReadLine() Console.Write("Kopierziel: ") : destination = Console.ReadLine() Try File.Copy(source, destination) Console.WriteLine("Erfolgreich kopiert.") Catch e As DirectoryNotFoundException Console.WriteLine(e.Message) ' Zielverzeichnis nicht gefunden Catch e As FileNotFoundException Console.WriteLine(e.Message) ' zu kopierende Datei nicht gefunden Catch e As ArgumentException Console.WriteLine(e.Message) ' Ziel- oder Quellverzeichnis fehlt Catch e As IOException Console.WriteLine(e.Message) ' unbehandelte Ausnahme End Try Console.ReadLine() End Sub End Module End Namespace
Als Pfad ist neben der absoluten Angabe, die auch Suchpfade einschließt, eine relative Angabe zulässig. Relative Pfadangaben beziehen sich dabei auf das aktuelle Arbeitsverzeichnis. Beim Programmstart ist das das Verzeichnis, in dem sich die ausführbare Datei der Anwendung befindet. Normalerweise werden Sie als Entwickler nicht wissen, in welchem Pfad der Anwender das Programm installiert hat. Sie können ihn mit Directory.GetCurrentDirectory()
ermitteln. Anschließend wird noch der Dateiname angehängt. Da GetCurrentDirectory nicht mit einem Backslash (\) abschließt, verbinden wir die Zeichenketten nicht direkt mit &. Dim source As String = _ Path.Combine(Directory.GetCurrentDirectory(), "MyDoc.txt") Dim destination As String = "C:\DuplicatedDoc.txt" File.Copy(source, destination)
545
7.3
7
Eingabe und Ausgabe
Löschen einer Datei Die Syntax der Methode Delete zum Löschen einer Datei lautet: Public Shared Sub Delete(path As String)
Der Parameter path erwartet entweder eine absolute oder relative Pfadangabe. Ungültige Angaben können auch hier unterschiedliche Ausnahmen auslösen. File.Delete("C:\MyDoc.txt")
Verschieben einer Datei Die Syntax der Methode Move zum Verschieben einer Datei ähnelt der der Methode Copy: Public Shared Sub Move(sourceFileName As String, destFileName As String)
Dem ersten Parameter wird die zu verschiebende Datei übergeben, dem zweiten der neue Pfad der Datei. Mit Move lassen sich Dateien nicht nur aus einem Quell- in ein Zielverzeichnis verschieben, sondern auch umbenennen. File.Move("C:\MyDoc.txt", "C:\Test.Doc")
Prüfen, ob eine Datei existiert Bevor Sie eine Datei öffnen, sollten Sie prüfen, ob sie überhaupt existiert. Die Klasse File hat dazu die Methode Exists, die False zurückliefert, wenn die Datei nicht gefunden wird. Dim strFile As String = "C:\MyFile.txt" If File.Exists(strFile) Then ...
Eine ähnliche Codesequenz ist in jedem Programm sinnvoll, in dem eine Operation die Existenz einer Datei voraussetzt. Das erspart die Codierung einer Ausnahmebehandlung.
Öffnen einer Datei Bevor Sie den Inhalt einer Datei lesen bzw. ändern können, muss diese geöffnet werden. Eine Datei zu öffnen sagt aber noch nichts darüber aus, was ein Benutzer mit dieser Datei anfangen darf – ob er sie nur lesen kann oder auch ändern darf. Auch die Berechtigung gleichzeitiger Zugriffe ist nicht selbstverständlich. Vier Methoden der Klasse File öffnen eine Datei: OpenRead, OpenText, OpenWrite sowie die überladene Methode Open. In ihr sind die beiden letzten Parameter optional. Public Shared Function Open(path As String, mode As FileMode, _ access As FileAccess, share As FileShare) As FileStream
Dem Parameter path wird beim Aufruf die Pfadangabe als Zeichenfolge übergeben. Sie besteht aus dem Pfad und dem Dateinamen.
546
Dateien und Verzeichnisse
Das Öffnen einer Datei erledigt das Betriebssystem, das wissen muss, wie es die Datei öffnen soll. Der mode-Parameter vom Typ der Enumeration FileMode steuert dieses Verhalten (siehe Tabelle 7.3). Konstante
Beschreibung
Append
Öffnet eine bestehende Datei und setzt den Dateizeiger an das Dateiende. Existiert die Datei noch nicht, wird sie erzeugt.
Create
Erzeugt eine neue Datei bzw. überschreibt eine existierende.
CreateNew
Erzeugt immer eine neue Datei. Existiert sie, tritt eine IOException auf. Öffnet eine bestehende Datei. Existiert sie nicht, tritt eine FileNotFound-
Open
Exception auf. OpenOrCreate
Öffnet eine bestehende Datei. Existiert sie nicht, wird eine neue erzeugt.
Truncate
Öffnet eine Datei und löscht deren Inhalt.
Tabelle 7.3 Die Enumeration »FileMode«
Mit dem Parameter access wird festgelegt, ob die Datei gelesen oder/und geschrieben werden darf. Der Parameter ist vom Typ der Enumeration FileAccess (siehe Tabelle 7.4). Konstante
Beschreibung
Read
Die Datei wird für den Lesezugriff geöffnet.
Write
Die Datei wird für den Schreibzugriff geöffnet.
ReadWrite
Die Datei wird für den Lese- und Schreibzugriff geöffnet.
Tabelle 7.4 Die Enumeration »FileAccess«
Eine Datei, die mit FileAccess.Read geöffnet wird, ist schreibgeschützt. Eine lesegeschützte Datei, deren Inhalt verändert werden soll, wird mit FileAccess.Write geöffnet. Die Konstante FileAccess.ReadWrite erlaubt sowohl einen lesenden als auch einen schreibenden Zugriff. Versuchen Sie, auf eine mit FileAccess.Write geöffnete Datei lesend zuzugreifen, wird die Ausnahme NotSupportedException ausgelöst. Die Namensgebung ist unglücklich. Die Ausnahme bemängelt eine Operation, die aufgrund des Öffnungsmodus nicht unterstützt wird (besser wäre der Name NotSupportedFileAccessException). Kommen wir nun zum letzten Parameter der Open-Methode – share. Er regelt den Fall, dass mehrere Anwendungen oder Threads gleichzeitig auf dieselbe Datei zugreifen (netzwerklose, nicht multitasking-fähige Rechner scheinen mir anachronistisch zu sein). Der Parameter ist vom Typ der Enumeration FileShare (siehe Tabelle 7.5). Konstante
Beschreibung
None
Weitere Versuche, diese Datei zu öffnen, werden konsequent abgelehnt.
Read
Die Datei darf von anderen Threads nur zum Lesen geöffnet werden.
Write
Die Datei darf von anderen Threads nur zum Editieren geöffnet werden.
Tabelle 7.5
Die Enumeration »FileShare«
547
7.3
7
Eingabe und Ausgabe
Konstante
Beschreibung
ReadWrite
Die Datei darf von anderen Anwendungen oder Threads sowohl zum Lesen als auch zum Editieren geöffnet werden.
Tabelle 7.5
Die Enumeration »FileShare« (Forts.)
Durch die Parameter von Open können Sie eine Datei im gewünschten Modus öffnen. Ob Sie auf den Dateiinhalt zugreifen dürfen, ist damit nicht gesagt. Das Öffnen ist dafür eine notwendige, aber nicht hinreichende Voraussetzung (die Haustür ist offen, die Wohnungstür aber nicht unbedingt auch). Hier sehen Sie ein Beispiel dafür, wie eine Datei geöffnet wird: Dim fs As FileStream = File.Open("C:\MyTestfile.txt", _ FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None)
Die Parameter besagen, dass die Datei MyTestfile.txt im Stammverzeichnis C:\ geöffnet werden soll – falls es dort eine solche gibt. Wenn nicht, wird sie neu erzeugt. Der Inhalt der Datei lässt sich nach dem Öffnen sowohl lesen als auch ändern. Gleichzeitig werden weitere Zugriffe auf die Datei strikt unterbunden.
Weitere Öffnungsmethoden File.Open ist die allgemeinste Variante, eine Datei zu öffnen. Die Klasse File bietet drei spezialisierte Formen an: Public Shared Function OpenRead (path As String) As FileStream Public Shared Function OpenWrite(path As String) As FileStream Public Shared Function OpenText (path As String) As StreamReader
Mit OpenRead wird die angegebene Datei zum Lesen geöffnet, mit OpenWrite zum Schreiben. Diese beiden Methoden können, wie auch Open, Dateien beliebigen Inhalts öffnen. OpenText hingegen ist auf Textdateien spezialisiert. Das macht sich in einem unterschiedlichen Rückgabetyp bemerkbar. StreamReader liest einzelne Zeichen vom Typ Char, während FileStream uninterpretierte Bytes liest. Welche Bedeutung diese haben, wird durch die Interpretation des datenverarbeitenden Programms bestimmt. Damit bestimmt die Art der Daten, ob ein FileStream oder StreamReader angemessen ist. Erst nach dieser Entscheidung wird die passende Methode zum Lesen ausgesucht. Auf Details gehen wir später ein.
Öffnen einer Textdatei Im folgenden Beispiel wird eine Textdatei geöffnet und der Inhalt an der Konsole ausgeben. Dazu wird im Code OpenText aufgerufen und die zurückgelieferte Referenz einer Objektvariablen des Typs StreamReader zugewiesen. Die Operationen, die im Zusammenhang mit dieser Klasse stehen, sollen uns an dieser Stelle noch nicht weiter interessieren. '...\IO\Dateien\Textdatei.vb
Option Strict On Imports System.IO
548
Dateien und Verzeichnisse
Namespace EA Module Textdatei Sub Test() Console.Write("Zu öffnende Datei: ") Dim strFile As String = Console.ReadLine() If Not File.Exists(strFile) Then Console.WriteLine("Die Datei {0} existiert nicht!", strFile) Else ' Datei öffnen Dim sr As StreamReader = File.OpenText(strFile) Dim zeile As String zeile = sr.ReadLine() ' zeilenweise Ausgabe an der Konsole While zeile IsNot Nothing Console.WriteLine(zeile) zeile = sr.ReadLine() End While sr.Close() Console.WriteLine("---- Programm- und Dateiende ----") End If Console.ReadLine() End Sub End Module End Namespace
Nach dem Start des Programms gibt der Anwender den Pfad zu einer Textdatei an. Dann wird mit File.Exists geprüft, ob die Datei im spezifizierten Pfad überhaupt vorhanden ist. Gibt es die Datei nicht, erscheint eine entsprechende Meldung an der Konsole. Existiert die Datei, wird sie mit File.OpenText geöffnet. In einer Schleife wird die Datei zeilenweise mit der Methode ReadLine des StreamReader-Objekts bis zum Dateiende eingelesen. Danach wird die Schleife beendet und die Datei geschlossen.
Einfach schreiben und lesen Einfacher zu programmieren, aber weniger flexibel, sind die Methoden ReadAllBytes, ReadAllLines und ReadAlltext zum Lesen und die Methoden WriteAllBytes, WriteAllLines und WriteAllText zum Schreiben. Alle gehören zur Klasse File. Mit der simplen Anweisung File.WriteAllText("C:\MyTextFile.txt", strText)
können Sie bereits den Inhalt der Variablen strText in die angegebene Datei schreiben. Existiert die Datei schon, wird sie einfach überschrieben. Genauso einfach ist das Lesen: Console.WriteLine(File.ReadAllText("C:\MyTextFile.txt"))
7.3.2
Dateizugriff mit FileInfo
Das Pendant zur der im vorigen Abschnitt beschriebenen Klasse File ist die ebenfalls nicht ableitbare Klasse FileInfo, die statt klassengebundener Methoden Instanzmethoden hat. Die Instanziierung erfolgt mit einem konkreten Dateipfad, zum Beispiel:
549
7.3
7
Eingabe und Ausgabe
Dim myFile As New FileInfo("C:\TestDir\Testfile.txt")
Der Konstruktor prüft nicht, ob die Datei tatsächlich existiert. Bevor Sie Operationen ausführen, sollten Sie daher in jedem Fall vorher mit Exists prüfen, ob die Datei existiert. If myFile.Exists Then ...
Während Exists in der Klasse File als Methode implementiert ist, der die Pfadangabe beim Aufruf übergeben werden muss, handelt es sich in der Klasse FileInfo um eine schreibgeschützte Eigenschaft des FileInfo-Objekts.
Eigenschaften Die in Tabelle 7.6 aufgelistete Klasse FileInfo ist von FileSystemInfo abgeleitet. Sie ist auch Basisklasse der Klasse DirectoryInfo, die wir weiter unten ansprechen. Die Instanzeigenschaften beschreiben den Zustand der Datei, wie z. B. deren Länge oder Verzeichnis. Eigenschaft
Beschreibung
Attributes
Dateiattribute (Hidden, Archive, ReadOnly usw.)
CreationTime
Erstellungsdatum der Datei
Directory
Instanz des Verzeichnisses
R
DirectoryName
Vollständige Pfadangabe, jedoch ohne den Dateinamen
R
Exists
Gibt an, ob die Datei existiert.
R
Extension
Dateierweiterung einschließlich des vorangestellten Punktes
R
FullName
Vollständige Pfadangabe einschließlich des Dateinamens
R
IsReadOnly
Gibt an, ob die Datei schreibgeschützt ist.
LastAccessTime
Zeit des letzten Zugriffs auf die Datei
LastWriteTime
Zeit des letzten schreibenden Zugriffs auf die Datei
Length
Gibt die Länge der Datei zurück.
R
Name
Vollständiger Name der Datei
R
Tabelle 7.6
Eigenschaften von »FileInfo« (R = ReadOnly, Zeit-Eigenschaften auch als UTC)
Attributes vom Typ FileAttributes ist ein Bitfeld fester Größe. Jedes Attribut einer Datei
wird durch Setzen eines einzelnen Bits beschrieben. Um festzustellen, ob ein Dateiattribut gesetzt ist, muss das alle Attribute beschreibende Bitfeld mit dem gesuchten Dateiattribut bitweise And-verknüpft werden. Weicht das Ergebnis von null ab, ist das Bit gesetzt. Angenommen, das Attribut XYZ sei durch die Bitkombination 0000 1000 (= 8) beschrieben und das Bitfeld habe den Wert 0010 1001 (= 41). Die Prüfung 0000 1000 And 0010 1001 -------------= 0000 1000
ergibt nicht Nnull, das Attribut ist also gesetzt. Um aus einer Datei ein bestimmtes Attribut herauszufiltern, beispielsweise Hidden, gehen wir daher wie folgt vor:
550
Dateien und Verzeichnisse
Dim f As New FileInfo("C:\Testfile.txt") If 0 (f.Attributes And FileAttributes.Hidden) Then ... ' versteckte Datei
Zum Setzen verwenden wir den Or-Operator: f.Attributes = f.Attributes Or FileAttributes.Hidden
In gleicher Weise können Sie mit den Methoden GetAttributes und SetAttributes der Klasse File arbeiten.
Methoden Die Klassen File und FileInfo sind sich sehr ähnlich: Es lassen sich Dateien löschen, verschieben, umbenennen, kopieren, öffnen usw. (siehe Tabelle 7.7). Die meisten geben ein Stream-Objekt für weitergehende Operationen zurück. Methode
Rückgabetyp
Beschreibung
AppendText
StreamWriter
Hängt Text an eine existierende Datei an.
CopyTo
FileInfo
Kopiert die Datei an einen anderen Speicherort.
Create
FileStream
Erzeugt eine Datei.
CreateText
StreamWriter
Erzeugt eine neue Textdatei. Löscht die Datei.
Delete Boolean
Exists
Gibt an, ob die Datei existiert. Verschiebt die Datei in einen anderen Ordner oder benennt sie um.
MoveTo Open
FileStream
Öffnet eine Datei.
OpenRead
FileStream
Öffnet eine Datei zum Lesen.
OpenText
StreamReader
Öffnet eine Textdatei zum Lesen.
OpenWrite
FileStream
Öffnet eine Datei zum Schreiben.
Tabelle 7.7
7.3.3
Methoden vonin »FileInfo«
Die Klassen Directory und DirectoryInfo
Analog zu den Dateiklassen enthält Directory nur klassengebundene Methoden, während DirectoryInfo auf einer konkreten Instanz basiert. Der entscheidende Unterschied besteht in der internen Arbeitsweise. Zugriffe auf das Dateisystem setzen immer operative Berechtigungen voraus. Verfügt der Anwender nicht über die entsprechenden Rechte, wird die angeforderte Aktion abgelehnt. Die beiden Klassen File und Directory prüfen das bei jedem Zugriff erneut und belasten so das System unnötig, während die Überprüfung von den Klassen DirectoryInfo und FileInfo nur einmal ausgeführt wird. Mit Directory kann man Ordner anlegen, löschen oder verschieben, die in einem Verzeichnis physikalisch gespeicherte Dateinamen abrufen und verzeichnisspezifische Eigenschaften sowie das Erstellungsdatum oder das Datum des letzten Zugriffs ermitteln (siehe Tabelle 7.8).
551
7.3
7
Eingabe und Ausgabe
Methode
Beschreibung
CreateDirectory
Erzeugt ein Verzeichnis oder Unterverzeichnis.
Delete
Löscht ein Verzeichnis.
Exists
Überprüft, ob das angegebene Verzeichnis existiert.
GetCreationTime
Liefert das Erstellungsdatum samt Uhrzeit.
GetDirectories
Liefert die Namen aller Unterverzeichnisse eines spezifizierten Ordners.
GetFiles
Liefert alle Dateinamen eines spezifizierten Ordners.
GetFileSystemEntries
Liefert die Namen aller Unterverzeichnisse und Dateien eines spezifizierten Ordners.
GetParent
Liefert den Namen des übergeordneten Verzeichnisses.
Move
Verschiebt ein Verzeichnis samt Dateien.
SetCreationTime
Legt Datum und Uhrzeit eines Verzeichnisses fest.
Tabelle 7.8
Methoden von »Directory«
Die Fähigkeiten der Klasse DirectoryInfo ähneln denen von Directory, setzen jedoch ein konkretes Objekt für den Zugriff auf die Elementfunktionen voraus.
7.3.4
Dateizugriffe in Aktion
Im folgenden Beispiel werden einige Methoden und Eigenschaften der Klassen File, FileInfo und Directory benutzt. Das Programm fordert den Anwender dazu auf, an der Konsole ein beliebiges Verzeichnis anzugeben, dessen Unterverzeichnisse und Dateien ermittelt und unter Angabe der Dateigröße und der Dateiattribute an der Konsole ausgegeben werden. '...\IO\Dateien\Dir.vb
Option Strict On Imports System.IO Namespace EA Module Dir Sub Test() Console.Write("Zu durchsuchender Ordner ") Dim dir As String = Console.ReadLine() ' Pfad muss mit \ enden If dir.Substring(dir.Length – 1) "\" Then dir += "\" For Each el As String In Directory.GetFileSystemEntries(dir) If 0 = (File.GetAttributes(el) And FileAttributes.Directory) Then Dim datei As New FileInfo(el) Console.WriteLine("{0,-30}{1,25} kB {2,-10} ", _ el.Substring(dir.Length – 1), datei.Length / 1024, _ GetFileAttributes(datei)) Else Console.WriteLine("{0,-30}{1,-15}", _ el.Substring(dir.Length), "Dateiordner") End If
552
Dateien und Verzeichnisse
Next Console.ReadLine() End Sub Function GetFileAttributes(ByVal datei As FileInfo) As String Dim attr As String = "" Dim tp As Type = GetType(FileAttributes) For Each fa As FileAttributes In [Enum].GetValues(tp) If 0 (fa And datei.Attributes) Then _ attr += [Enum].GetName(tp, fa).Substring(0, 1) Next Return attr End Function End Module End Namespace
Eine typische Ausgabe zeigt Abbildung 7.2.
Abbildung 7.2
Beispielausgabe mit »Dir«
Die Pfadangabe des Anwenders wird immer mit einem Backslash abgeschlossen, damit die Ausgabe immer gleich aussieht (Substring kappt den kompletten Pfad). In einer Schleife werden die von GetFileSystemEntries gelieferten Verzeichniseinträge durchlaufen. Jeder Eintrag kann sowohl eine Datei- als auch eine Verzeichnisangabe enthalten. Handelt es sich um ein Verzeichnis, ist das Attribut Directory gesetzt. Mit If 0 = (File.GetAttributes(el) And FileAttributes.Directory) Then
wird das geprüft. Die Bedingung liefert True, wenn eine Datei vorliegt. Da das Programm die Größe der Datei ausgeben soll, können wir nicht mit File arbeiten, da mit ihr keine Dateilänge ermittelt werden kann. Deswegen erledigt die Eigenschaft Length der Klasse FileInfo den Job. Die Methode GetAttributes dient dazu, das übergebene FileInfo-Objekt auf die Attribute Hidden, ReadOnly, Archive und System hin zu untersuchen. Aus dem Ergebnis wird eine Zeichenfolge zusammengesetzt.
553
7.3
7
Eingabe und Ausgabe
7.3.5
Pfade mit Path
Die Klassen File und Directory greifen immer wieder auf Pfadangaben zurück. Eine Pfadangabe beschreibt den Ort einer Datei oder eines Verzeichnisses. Die Schreibweise ist betriebssystemabhängig; manche starten mit einer Laufwerksangabe. Das Trennzeichen zwischen Verzeichnissen ist ebenfalls plattformspezifisch. Windows benutzt in der Regel den Backslash (\), Unix einen Slash (/). Die nicht ableitbare Klasse Path ermöglicht plattformunabhängige Pfadangaben. Außerdem kann sie eine Pfadangabe in ihre Bestandteile zerlegen. Schließlich gibt uns Path das Verzeichnis für temporäre Dateien.
Methoden Alle Path-Klassenmitglieder sind klassengebunden und haben die Aufgabe, eine Pfadangabe in einer bestimmten Weise zu filtern (siehe Tabelle 7.9). Methode
Beschreibung
GetDirectoryName
Das Verzeichnis einer gegebenen Pfadangabe
GetExtension
Dateierweiterung einschließlich des führenden Punktes
GetFileName
Liefert den vollständigen Dateinamen.
GetFileNameWithoutExtension
Liefert den Dateinamen ohne Dateierweiterung.
GetFullPath
Liefert die komplette Pfadangabe.
GetPathRoot
Liefert das Stammverzeichnis.
Tabelle 7.9
Methoden von »Path«
Beachten Sie dabei, dass keine dieser Methoden testet, ob die Datei oder das Verzeichnis tatsächlich existiert. Es werden lediglich die Zeichenkette und die Vorschriften der spezifischen Plattform zur Bestimmung des Ergebnisses herangezogen. Mit Dim strPath As String = "C:\winnt\system\OLE2.dll"
liefern die Methoden die folgenden Rückgaben: Console.WriteLine(Path.GetPathRoot(strPath)) ' C:\ Console.WriteLine(Path.GetDirectoryName(strPath)) ' C:\winnt\system Console.WriteLine(Path.GetFileNameWithoutExtension(strPath)) ' OLE2 Console.WriteLine(Path.GetFileName(strPath)) ' OLE2.dll Console.WriteLine(Path.GetFullPath(strPath)) ' C:\winnt\system\OLE2.dll Console.WriteLine(Path.GetExtension(strPath)) ' .dll
Temporäre Verzeichnisse Viele Anwendungen arbeiten mit temporären Dateien. Die Methode GetTempPath der Klasse Path liefert das temporäre Verzeichnis des aktuell angemeldeten Benutzers. Public Shared Function GetTempPath() As String
Unter Windows XP finden Sie dieses Verzeichnis standardmäßig unter dem Namen Temp in: C:\Dokumente und Einstellungen\Username\Lokale Einstellungen
554
Dateien und Verzeichnisse
Mit GetTempFileName wird eine leere Datei im temporären Verzeichnis angelegt. Der Rückgabewert ist die komplette Pfadangabe: Public Shared Function GetTempFileName() As String
Eine temporäre Datei kann dazu benutzt werden, Zwischenergebnisse zu speichern, Informationen kurzfristig zu sichern und Abläufe zu protokollieren. Allerdings sollten Sie nicht vergessen, temporäre Dateien nach Gebrauch auch wieder zu löschen.
7.3.6
Laufwerksinformation mit DriveInfo
Mit DriveInfo können Sie bestimmen, welche Laufwerke verfügbar sind und um welchen Typ von Laufwerk es sich dabei handelt. Zudem können Sie die Kapazität und den verfügbaren freien Speicherplatz auf dem Laufwerk ermitteln. Tabelle 7.10 listet die Eigenschaften von DriveInfo auf. Eigenschaft
Rückgabetyp
Beschreibung
AvailableFreeSpace
Long
Freier Speicherplatz auf einem Laufwerk
DriveFormat
String
Ruft den Namen des Dateisystems ab.
DriveType
DriveType
Ruft den Laufwerkstyp ab.
IsReady
Boolean
Gibt an, ob das Laufwerk bereit ist.
Name
String
Liefert den Namen des Laufwerks.
RootDirectory
DirectoryInfo
Liefert das Stammverzeichnis des Laufwerks.
TotalFreeSpace
Long
Liefert den verfügbaren Speicherplatz.
TotalSize
Long
Gesamter Speicherplatz auf einem Laufwerk
VolumeLabel
String
Datenträgerbezeichnung eines Laufwerks
Tabelle 7.10 Eigenschaften von »DriveInfo«
Die Eigenschaft DriveType ist vom Typ der gleichnamigen Enumeration. Diese hat insgesamt sieben Mitglieder, die Sie der Tabelle 7.11 entnehmen können. Member
Beschreibung
CDRom
Optischer Datenträger (z. B. CD oder DVD)
Fixed
Festplatte
Network
Netzlaufwerk
NoRootDirectory
Das Laufwerk hat kein Stammverzeichnis.
Ram
RAM-Datenträger
Removable
Wechseldatenträger
Unknown
Unbekannter Laufwerkstyp
Tabelle 7.11
Die Enumeration »DriveType«
555
7.3
7
Eingabe und Ausgabe
7.3.7
SpecialDirectories
My.Computer.FileSystem.SpecialDirectories listet Windows-spezifischer Pfade, zum Beispiel den zu Eigene Bilder. Die Angaben werden über die in Tabelle 7.12 gezeigten statischen Eigenschaften als Zeichenfolge geliefert.
Eigenschaft
Beschreibung
AllUserApplicationData
Anwendungsdaten aller Benutzer, typischerweise \Dokumente und Einstellungen\All Users\ Anwendungsdaten.
CurrentUserApplicationData
Anwendungsdaten des aktuellen Benutzers
Desktop, MyDocuments, MyMusic, MyPictures, ProgramFiles, Programs, Temp
Pfad zu Desktop, Eigene Dateien, Eigene Musik, Eigene Bilder, Program Files, Programme, Temp.
Tabelle 7.12
7.4
Die Klasse »SpecialDirectories«
Datenflüsse in Streams
Ein Stream ist die abstrahierte Darstellung eines Datenflusses aus einer geordneten Abfolge von Bytes. Woher die Daten kommen und wohin sie gehen, ist nicht wesentlich für einen Stream und hängt vom Sender, Empfänger und Betriebssystem ab. Durch die Verwendung von Streams werden Ihre Programme durch die Abstraktion plattform- und datenkanalunabhängig. Streams führen drei elementare Operationen aus: 왘
Dateninformationen müssen in einen Sream geschrieben werden. Nach welchem Muster das geschieht, wird durch den Typ des Streams vorgegeben.
왘
Aus dem Datenstrom muss gelesen werden. Es gibt viele Ziele: Dateien, Netzwerk, Variablen, Datenbanken, Drucker, Monitor usw.
왘
Nicht immer muss der gesamte Datenstrom ausgewertet werden. Manchmal reicht es aus, ab einer bestimmten Position zu lesen. Man spricht dann vom wahlfreien Zugriff.
Nicht alle Datenströme können diese drei Punkte gleichzeitig erfüllen. Beispielsweise unterstützen Datenströme im Netzwerk nicht den wahlfreien Zugriff. Bei den Streams werden grundsätzlich zwei Typen unterschieden: 왘
Base-Streams, die direkt aus einem Strom lesen oder in ihn hineinschreiben. Die Vorgänge können z. B. in Dateien, im Hauptspeicher oder einer Netzwerkverbindung enden.
왘
Pass-Through-Streams basieren auf einem anderen Stream und kommunizieren nicht direkt mit den Enden. Sie haben spezielle Funktionalitäten, zum Beispiel Verschlüsselung oder Pufferung. Pass-Through-Streams lassen sich beliebig in Reihe schalten.
7.4.1
Die abstrakte Klasse Stream
Die Klasse Stream ist die abstrakte Basisklasse aller anderen Streamklassen und enthält deren Basisfunktionalität. Die von der Klasse Stream abgeleiteten Klassen unterstützen nur Operationen auf Bytesequenzen, die noch keinen Typ festlegen und noch interpretiert werden müssen.
556
Datenflüsse in Streams
Eigenschaften Streams stellen Schreib-, Lese- und Suchoperationen bereit. Ob ein konkreter Stream eine Operation unterstützt, können Sie mit den Eigenschaften CanRead, CanWrite und CanSeek prüfen. Die Eigenschaft Length liefert die Länge des Streams und Position die aktuelle Position innerhalb des Streams. Letztere wird allerdings nur von den Streams bereitgestellt, die auch die Positionierung mit der Seek-Methode unterstützen. Tabelle 7.13 fasst die Eigenschaften von Stream zusammen. Eigenschaft
Wert in einer abgeleiteten Klasse
CanRead
Gibt an, ob der aktuelle Stream Lesevorgänge unterstützt.
R
CanWrite
Gibt an, ob der aktuelle Stream Schreibvorgänge unterstützt.
R
CanSeek
Gibt an, ob der aktuelle Stream Suchvorgänge unterstützt.
R
CanTimeout
Gibt an, ob eine Zeitüberschreitung berücksichtigt wird.
R
Length
Die Länge des Streams in Byte
R
Position
Die Position im aktuellen Stream
ReadTimeout
Die Zeit, nach der eine Leseoperation abgebrochen wird
WriteTimeout
Die Zeit, nach der eine Schreiboperation abgebrochen wird
Tabelle 7.13 Eigenschaften von »Stream« (R = ReadOnly)
Methoden Die wichtigsten Methoden aller Stream-Klassen sind Read, Write und Seek. Fangen wir mit Read und Write an, die von jeder abgeleiteten Klasse überschrieben werden müssen. Ein geöffneter Stream sollte am Ende ordnungsgemäß mit Close geschlossen werden. Public MustOverride buffer As Byte(), Public MustOverride buffer As Byte(),
Function Read( _ offset As Integer, count As Integer) As Integer Sub Write( _ offset As Integer, count As Integer)
Die Write-Methode liest den ersten Parameter byteweise ein und schreibt die Daten in den Strom. Der Empfänger des Datenstroms nimmt die Bytes aus dem ersten Parameter von Read. Der zweite Parameter, offset, bestimmt die Position im Array, ab der der Lese- bzw. Schreibvorgang beginnt, meistens null. Der letzte Parameter gibt an, wie viele Bytes gelesen oder geschrieben werden sollen. Im Rückgabewert von Read steht die Anzahl gelesener Bytes. Er ist null, wenn das Ende des Streams erreicht ist. Er kann kleiner sein als der dritte Parameter, wenn der Stream nicht genug Daten hat. Die abstrakte Klasse Stream implementiert zwei Methoden, die nur ein Byte aus dem Datenstrom lesen oder in ihn hineinschreiben. ReadByte gibt -1 zurück, wenn das Ende des Datenstroms erreicht ist.
557
7.4
7
Eingabe und Ausgabe
Public Overridable Function ReadByte() As Integer Public Overridable Sub WriteByte(ByVal value As Byte)
Den Datenzeiger bewegt Seek. Der Parameter offset beschreibt die Verschiebung in Bytes ab der unter origin festgelegten Ursprungsposition. Public MustOverride Function Seek(offset As Long, origin As SeekOrigin) _ As Long
origin ist vom Typ der Enumeration SeekOrigin (siehe Tabelle 7.14).
Konstante
Beschreibung
Begin
Gibt den Anfang eines Streams an (das erste Byte).
Current
Gibt die aktuelle Position innerhalb eines Streams an.
End
Gibt das Ende eines Streams an (erste Position hinter dem letzten Byte).
Tabelle 7.14
Die Enumeration »SeekOrigin«
Tabelle 7.15 listet die Methoden von Stream auf. Methode
Beschreibung
Close
Schließt den aktuellen Stream und gibt alle dem aktuellen Stream zugeordneten Ressourcen frei.
Read
Liest eine Folge von Bytes aus dem aktuellen Stream und setzt den Datenzeiger im Stream um die Anzahl der gelesenen Bytes weiter.
ReadByte
Liest ein Byte aus dem Stream und erhöht die Position im Stream um ein Byte. Der Rückgabewert ist –1, wenn das Ende des Streams erreicht ist.
Seek
Legt die Position im aktuellen Stream fest.
Write
Schreibt eine Folge von Bytes in den aktuellen Stream und erhöht den Datenzeiger im Stream um die Anzahl der geschriebenen Bytes.
WriteByte
Schreibt ein Byte an die aktuelle Position im Stream und setzt den Datenzeiger um eine Position im Stream weiter.
Tabelle 7.15
7.4.2
Methoden von Stream
Stream-Klassen im Überblick
Tabelle 7.16 zeigt einige der von Stream abgeleiteten Klassen aus verschiedenen Namensräumen. Stream-Typ
Beschreibung
BufferedStream
Durch Pufferung der Daten im Hauptspeicher wird die Zahl an Betriebssystemaufrufen reduziert und der Stream beschleunigt.
CryptoStream
Verschlüsselt gelesene oder geschriebene Daten.
Tabelle 7.16
558
Einige von »Stream« abgeleitete Klassen (B = Base-Stream)
Datenflüsse in Streams
Stream-Typ
Beschreibung
FileStream
Zugriff auf das Dateisystem, lokal oder über das Netzwerk
GZipStream
Dekompriemierung gelesener bzw. Komprimierung geschriebener Daten
MemoryStream
Sehr schneller Ersatz im Arbeitsspeicher für temporäre Dateien
B
NetworkStream
Zugriff auf Sockets. Daten können nur vollständig gelesen/geschrieben werden; kein wahlfreier Zugriff.
B
Tabelle 7.16
7.4.3
B
Einige von »Stream« abgeleitete Klassen (B = Base-Stream) (Forts.)
Dateizugriff mit FileStream
Dieser Strom kann aus beliebigen Dateien byteweise lesen oder in sie schreiben. Ein Positionszeiger kann auf eine beliebige Position innerhalb des Streams gesetzt werden. Der Datenpuffer von standardmäßig 8 KByte erhöht die Ausführungsgeschwindigkeit. Die FileStream-Klasse bietet eine Reihe von Konstruktoren an. In der folgenden Syntax sind optionale Parameter kursiv und Alternativen durch einen senkrechten Strich getrennt. Public Sub New(path As String, mode As FileMode, _ access As FileAccess, _ share As FileShare, bufferSize As Integer, _ useAsync As Boolean | options As FileOptions) Public Sub New(path As String, mode As FileMode, _ rights As FileSystemRights, _ share As FileShare, bufferSize As Integer, options As FileOptions, _ fileSecurity As FileSecurity) Public Sub New(handle As SafeFileHandle, access As FileAccess, _ bufferSize As Integer, isAsync As Boolean)
Der Parameter path gibt den Pfad zur Datei an, mode ist in Tabelle 7.3, »Die Enumeration FileMode«, (FileMode.Append, FileMode.Create, ...) beschrieben. Den Zugriff (FileAccess.Read, FileAccess.Write oder FileAccess.ReadWrite) spezifiziert access (siehe Tabelle 7.4, »Die Enumeration FileAccess«), während share konkurrierende Zugriffe regelt (siehe Tabelle 7.5, »Die Enumeration FileShare«). Im fünften Parameter geben Sie die Puffergröße an. Mit dem Parameter useAsync kann noch angegeben werden, ob das Objekt asynchrone Zugriffe unterstützen soll.
Schreiben Das folgende Codefragment demonstriert, wie mit einem FileStream-Objekt Daten in eine Datei geschrieben werden. Sub Test() Dim arrWrite() As Byte = {10, 20, 30, 40, 50, 60, 70, 80, 90, 100} Dim path As String = IO.Path.GetTempFileName()
559
7.4
7
Eingabe und Ausgabe
Dim fs As New FileStream(path, FileMode.Create) fs.Write(arrWrite, 0, arrWrite.Length) ' lesen, siehe unten fs.Close() File.Delete(path) Console.ReadLine() End Sub
Zunächst wird ein Byte-Array deklariert und mit insgesamt zehn Zahlen initialisiert. Die zweite Anweisung legt den Dateinamen fest, in den das Array geschrieben wird. Das FileStream-Objekts wird unter Angabe des Dateipfads und des Modus erzeugt. Die Konstante FileMode.Create erlaubt dem FileStream-Objekt, eine neue Datei zu erzeugen, oder, falls im angegebenen Pfad bereits eine gleichnamige Datei existiert, diese zu überschreiben. Mit fs.Write(arrWrite, 0, arr.Length)
wird der Inhalt des Arrays arrWrite dem Stream-Objekt übergeben. Die Syntax lautet: Public Overrides Sub Write( _ array As Byte(), offset As Integer, count As Integer)
Dabei haben die drei Parameter die in Tabelle 7.17 gezeigte Bedeutung. Parameter
Beschreibung
array
Ein Byte-Array, in das die übergebenen Daten gelesen werden
offset
Die Indexposition im Array, bei dem die Leseoperation beginnen soll
count
Die Anzahl der zu lesenden Bytes
Tabelle 7.17
Parameter von »FileStream.Write«
Der Schreibvorgang des Beispiels startet mit dem ersten Array-Element (zweiter Parameter) und schreibt das ganze Array (dritter Parameter). Zum Schluss wird der FileStream mit der Methode Close geschlossen.
Positionszeiger Die Schreib- bzw. Leseposition in einem Datenstrom wird durch einen Positionszeiger beschrieben. Dadurch kennt der Strom das nächste zu bearbeitende Byte. Nach der Instanziierung steht der Zeiger auf dem ersten Byte. Mit jeder Schreib- und Leseoperation wird der Positionszeiger um die Anzahl verarbeiteter Bytes weitergeschoben. In unserem Beispiel werden 10 Bytes eingelesen, der Positionszeiger steht nach der Schreiboperation also auf dem elften Byte. Folgende Leseoperationen starten dort und haben nichts zu lesen, weil der Positionszeiger an der falschen Stelle steht. Abbildung 7.3 zeigt die Situation. Vor der nächsten Leseoperation müssen wir den Positionszeiger mit der Methode Seek an eine passende Stelle setzen.
560
Datenflüsse in Streams
Positionszeiger vor dem ersten Write-Aufruf
10
20
30
40
50
60
70
80
90 100
Positionszeiger nach dem letzten Write-Aufruf
Datenstrom
Abbildung 7.3
Positionszeiger eines Stroms
Public Overrides Function Seek(offset As Long, origin As SeekOrigin) As Long
Um eine Datei komplett lesen zu können, setzen wir den Positionszeiger auf das erste Byte, origin + offset muss also null ergeben. Mit den in Tabelle 7.14, »Die Enumeration SeekOrigin«, aufgelisteten Werten haben Sie in unserem Beispiel drei gleichwertige Möglichkeiten, auf das erste Byte an der Position null zu springen. fs.Seek(0, SeekOrigin.Begin) fs.Seek(-10, SeekOrigin.Current) fs.Seek(-10, SeekOrigin.End)
Welche Variante Sie wählen, ist Geschmackssache.
Lesen Das Lesen aus einem FileStream können wir an der gerade geschriebenen Datei testen. Dazu ergänzen wir das Beispiel um ein paar Zeilen: Sub Test() ... Dim arrRead(9) As Byte fs.Read(arrRead, 0, 10) For i As Integer = 0 To arrRead.Length – 1 Console.WriteLine(arrRead(i)) Next fs.Close() End Sub
Da wir die Anzahl zu lesender Bytes kennen, deklarieren wir ein Byte-Array passender Größe und lesen die Datei mit der Methode Read aus: Public Overrides Function Read( _ array As Byte(), offset As Integer, count As Integer) As Integer
561
7.4
7
Eingabe und Ausgabe
Die auszulesende Datei wurde bereits bei der Erzeugung des FileStream-Objekts im letzten Abschnitt spezifiziert. Die Parameter sind analog zur Write-Methode. Die aus der Datei gelesenen Daten werden in den ersten Parameter, array, geschrieben, beginnend bei der im zweiten Parameter, offset, angegebenen Position. Die Anzahl zu lesender Bytes steht im dritten Parameter, count. Im Beispiel fangen wir durch 0 am Anfang des Arrays an und lesen 10 Bytes und nutzen so das gesamte Array. Zur Kontrolle geben wir den Arrayinhalt in einer Schleife aus. Lesen Sie über das Ende der Datei hinaus, wird eine ArgumentException ausgelöst. Hinweis Zur Konvertierung der Bytes in ein anderes Datenformat bietet die Klasse System.Convert einige Methoden an.
Schreiben und Lesen Zum Schluss wollen wir noch einmal den Code zusammenfassen: '...\IO\Ströme\Dateistrom.vb
Option Strict On Imports System.IO Namespace EA Module Dateistrom Sub Test() ' schreiben Dim arrWrite() As Byte = {10, 20, 30, 40, 50, 60, 70, 80, 90, 100} Dim path As String = IO.Path.GetTempFileName() Dim fs As New FileStream(path, FileMode.Create) fs.Write(arrWrite, 0, arrWrite.Length) 'Positionszeiger setzen fs.Seek(0, SeekOrigin.Begin) ' lesen Dim arrRead(9) As Byte fs.Read(arrRead, 0, 10) For i As Integer = 0 To arrRead.Length – 1 Console.WriteLine(arrRead(i)) Next ' schließen fs.Close() File.Delete(path) Console.ReadLine() End Sub End Module End Namespace
562
Texte mit TextReader und TextWriter
7.5
Texte mit TextReader und TextWriter
Zeichenketten werden in .NET in Unicode codiert. Jeder Buchstabe besteht aus 1 bis 2 Byte (bzw. 4 bei Surrogaten). Beim Lesen und Schreiben einzelner Bytes ist es sehr lästig, einzelne Bytes zusammenzusetzen oder auseinanderzupflücken. Daher gibt es in .NET die beiden abstrakten Klassen TextReader und TextWriter, die sich um das korrekte Hantieren mit Texten kümmern. Aus der folgenden Klassenhierarchie werden wir uns mit den Klassen StreamReader/StreamWriter und StringReader/StringWriter beschäftigen. Die Stream-Variante operiert auf Dateien, die String-Variante auf Zeichenketten. System.Object ÀSystem.MarshalByRefObject À3ÄÂTextReader ³ Ã1ÄÄDocDataTextReader ³ À3ÄÂStreamReader ³ ÀStringReader ÀTextWriter Ã1ÄÄDocDataTextWriter Ã2ÄÄIndentedTextWriter Ã3ÄÂStreamWriter ³ ÀStringWriter Ã4ÄÄHttpWriter À5ÄÄHtmlTextWriter Ã5ÄÂHtml32TextWriter ³ ³ À5ÄÄChtmlTextWriter ³ ÀXhtmlTextWriter À6ÄÄMultiPartWriter À6ÄÄMobileTextWriter Ã6ÄÂHtmlMobileTextWriter ³ ³ À6ÄÄChtmlMobileTextWriter ³ ÀWmlMobileTextWriter ³ À6ÄÄUpWmlMobileTextWriter À7ÄÄXhtmlMobileTextWriter 1: 2: 3: 4: 5: 6: 7:
Microsoft.VisualStudio.Shell.Design.Serialization System.CodeDom.Compiler System.IO System.Web System.Web.UI System.Web.UI.MobileControls.Adapters System.Web.UI.MobileControls.Adapters.XhtmlAdapters
7.5.1
Texte mit StreamWriter schreiben
Konstruktoren Wir wenden uns zunächst einigen Konstruktoren der Klasse StreamWriter zu. In der folgenden Syntax sind optionale Parameter kursiv gesetzt.
563
7.5
7
Eingabe und Ausgabe
Public Sub New(path As String, _ append As Boolean, encoding As Encoding, bufferSize As Integer) Public Sub New(stream As Stream, _ encoding As Encoding, bufferSize As Integer)
Der Endpunkt des Writers wird im ersten Parameter übergeben. Es ist entweder ein Dateipfad oder ein beliebiger von Stream abgeleiteter Strom, zum Beispiel FileStream. In der Einleitung zu Abschnitt 7.4, »Datenflüsse in Streams«, haben wir die Ströme in zwei Klassen eingeteilt. In dieser Terminologie ist der StreamWriter mit Stream-Parameter ein Pass-Through-Stream, sein erster Parameter ist entweder ein Base-Stream oder selbst ein Pass-Through-Stream. Dadurch kann jeder Strom (nachträglich) mit Textverarbeitungsfähigkeiten ausgestattet werden. Zum Zugriff auf eine Datei können Sie einfach den Pfad zur Datei angeben, zum Beispiel so: Dim myStreamWriter As StreamWriter = New StreamWriter("C:\MyText.txt")
Wir erzeugen mit dieser Anweisung einen Base-Stream, der eine Zeichenfolge in eine Datei schreiben kann. Alternativ können wir den StreamWriter auf einem FileStream aufbauen. Dim fs As FileStream = New FileStream("C:\Test.txt", FileMode.CreateNew) Dim myStreamWriter As StreamWriter = New StreamWriter(fs)
In der ersten Zeile erzeugen wir einen FileStream, der auf die Datei Test.txt im Stammverzeichnis des Laufwerks C: zugreifen kann. Auf ihn bauen wir in der zweiten Zeile einen StreamWriter auf. Durch die Hintereinanderschaltung von FileStream und StreamWriter werden deren Fähigkeiten kombiniert: Der FileStream-Teil kümmert sich um das byteweise Lesen und Schreiben, während der StreamWriter aus den einzelnen Bytes Zeichen macht bzw. diese in Bytes zerlegt (das Encoding regelt, wie das geschieht). Genauso können Sie natürlich auch einen MemoryStream oder NetworkStream als Argument übergeben. Standardmäßig verschlüsselt StreamWriter nach UTF-8, Abweichungen davon geben Sie im Parameter vom Typ System.Text.Encoding an, zum Beispiel UTF7Encoding oder UnicodeEncoding (entspricht der UTF-16-Kodierung). Hinweis Welchen Buchstaben ein Bytewert beschreibt, regelt die Kodierung. Unter Windows ist in Mitteleuropa ANSI (Codeseite 1252) üblich, das Codes zwischen 0 und 255 erlaubt und Umlaute und einige Sonderzeichen enthält. Demgegenüber läuft ASCII von 0 bis 127 und ist in diesem Bereich mit ANSI identisch. Diesen starken Restriktionen ist Unicode nicht unterworfen, das 1-4 Byte zur Kodierung benutzt. Je nach minimal benutzter Bitzahl unterscheidet man UTF-7, UTF-8, UTF-16 und UTF-32. Der unter .NET verwendete Standard UTF-8 kodiert Codes zwischen 0 und 127 identisch zu ASCII, sodass für diese keine Umkodierung nötig ist und reine ASCII-Systeme damit zurechtkommen.
Ein auf einem Pfad basierender StreamWriter gibt im Parameter append an, ob geschriebene Daten an die Datei angehängt werden oder die Datei überschrieben wird. Existiert die Datei nicht, wird sie erstellt, und der Parameter hat keinen Einfluss. Im letzten Parameter können Sie eine Größe für den Puffer angeben.
564
Texte mit TextReader und TextWriter
Schreiben in den Datenstrom Das folgende Codefragment schreibt einen kurzen Text in eine (neue) Datei: Dim sw As StreamWriter = New StreamWriter("C:\NewFile.txt") sw.WriteLine("Es saß ein Wiesel") sw.WriteLine("auf einem Kiesel") sw.Close()
Zuerst wird ein StreamWriter-Objekt unter Angabe eines Dateipfads erzeugt. Daraufhin wird entweder die Datei erzeugt oder eine existierende gleichnamige im angegebenen Verzeichnis überschrieben. Mit jedem Aufruf der von TextWriter geerbten Methode WriteLine wird eine Zeile und ein Zeilenumbruch in die Datei geschrieben, hier zwei Zeilen: Public Overridable Sub WriteLine() Public Overridable Sub WriteLine(buffer As Char()) Public Overridable Sub WriteLine(buffer As Char(), _ index As Integer, count As Integer) Public Overridable Sub WriteLine(value As Boolean) Public Overridable Sub WriteLine(value As Char) Public Overridable Sub WriteLine(value As Object) Public Overridable Sub WriteLine(value As String) Public Overridable Sub WriteLine(value As Zahl) Public Overridable Sub WriteLine( _ format As String, ParamArray arg As Object()) Zahl: Decimal, Double, Integer, Long, Single, UInt32, UInt64
Der letzten Überladung bedient sich die Klasse Console zur Ausgabe. Korrespondierend zu dieser Methode schreibt Write Daten, ohne einen Zeilenvorschub anzuhängen (es fehlt nur die parameterlose Variante). Die Methoden Write und WriteLine bilden den Kern der Klasse StreamWriter; Tabelle 7.18 zeigt alle ihre Methoden. Methode
Beschreibung
Close
Schließt das aktuelle Objekt sowie alle eingebetteten Streams.
Flush
Schreibt die gepufferten Daten in den Stream und löscht den Pufferinhalt.
Synchronized
Erstellt einen threadsicheren Wrapper (Shared).
Write
Schreibt in den Stream, ohne einen Zeilenumbruch anzuhängen.
WriteLine
Schreibt in den Stream und schließt mit einem Zeilenumbruch ab.
Tabelle 7.18
Methoden von »StreamWriter«
Eigenschaften Mit AutoFlush=True wird bei jedem Aufruf von Write-/WriteLine der Puffer in den Datenstrom geschrieben. Die Eigenschaft Encoding liefert die Zeichenkodierung.
565
7.5
7
Eingabe und Ausgabe
Dim sw As StreamWriter = _ New StreamWriter("C:\NewFile.txt", False, Encoding.Unicode) Console.WriteLine("Format: {0}", sw.Encoding.ToString())
BaseStream ist der zugrunde liegende Stream, ob explizit gegeben oder implizit durch eine
Pfadangabe erzeugt. Tabelle 7.19 zeigt alle Eigenschaften von StreamWriter. Eigenschaften
Beschreibung
AutoFlush
Löscht den Puffer nach jedem Aufruf von Write oder WriteLine.
BaseStream
Liefert eine Referenz auf den Base-Stream zurück.
R
Encoding
Liefert das aktuelle Encoding-Schema zurück.
R
FormatProvider
Art der Formatierung (Zahlen, Datum)
R
NewLine
Verwendeter Zeilenumbruch
Null
Datenziel »Nirgendwo« (klassengebundenes Feld)
Tabelle 7.19
7.5.2
R
Eigenschaften von »StreamWriter« (R = ReadOnly)
Texte mit StreamReader lesen
Die aus der Klasse TextReader abgeleitete Klasse StreamReader ist das Gegenstück zum StreamWriter des vorigen Abschnitts. Statt mir Write zu schreiben, liest sie mit Read. Die Konstruktoren sind identisch zu StreamWriter, außer dass der append-Parameter wegfällt und der neue Parameter detectEncodingFromByteOrderMarks hinzukommt, der angibt, ob die ersten drei Bytes zur automatischen Erkennung des Encodings benutzt werden sollen. Tabelle 7.20 enthält die Methoden eines StreamReaders. Methode
Beschreibung
Close
Schließt das aktuelle Objekt sowie alle eingebetteten Streams.
DiscardBufferedData
Verwirft gepufferte Daten.
Peek
Liest ein Zeichen aus dem Strom, ohne den Positionszeiger zu ändern, und liefert das Zeichen als Integer. Ist der Zeiger hinter dem Datenstrom, ist dieser Wert -1.
Read
Liest ein Zeichen aus dem Strom unter Änderung des Positionszeigers und liefert das Zeichen als Integer. Ist der Zeiger hinter dem Datenstrom, ist dieser Wert –1. Eine zweite Variante liest mehrere Zeichen und gibt die Anzahl gelesener Zeichen zurück.
ReadLine
Liest eine Zeile aus dem Strom – entweder bis zum Zeilenumbruch oder bis zum Ende des Stroms. Der Rückgabewert hat den Typ String (mit dem Wert Nothing, wenn hinter dem Strom gelesen wird).
ReadToEnd
Liest von der aktuellen Position des Positionszeigers bis zum Ende des Stroms alle Zeichen ein (am Stromende steht ein Leerstring).
Tabelle 7.20
Methoden von »StreamReader«
Wir wollen nun an einem Codebeispiel das Lesen aus einem Strom testen.
566
Texte mit TextReader und TextWriter
'...\IO\Ströme\Texte.vb
Option Strict On Imports System.IO Namespace EA Module Texte Sub Test() ' Datei erzeugen und mit Text füllen Dim path As String = IO.Path.GetTempFileName() Dim sw As New StreamWriter(path) sw.WriteLine("Es saß ein Wiesel") sw.WriteLine("auf einem Kiesel") sw.Close() ' Datei einlesen und an der Konsole ausgeben Dim sr As New StreamReader(path) While sr.Peek() –1 Console.WriteLine(sr.ReadLine()) End While sr.Close() File.Delete(path) Console.ReadLine() End Sub End Module End Namespace
Um etwas zu lesen zu haben, schreiben wir mit einem StreamWriter einen zweizeiligen Text in eine beliebige Datei (die Endung des Dateinamens spielt keine Rolle). Damit ein anderer Strom die Datei auslesen kann, schließen wir den StreamWriter. Mit einem StremReader, der denselben Pfad benutzt, lesen wir mit ReadLine die Zeilen der Datei in einer Schleife aus. Die Abbruchbedingung der Schleife verwendet Peek zur Erkennung des Dateiendes. Die Methode gibt dann -1 zurück. Die Ausgabe zeigt genau zwei Zeilen: Es saß ein Wiesel auf einem Kiesel
Wenn Sie eine Datei komplett einlesen wollen, können Sie statt der Schleife auch Console.WriteLine(sr.ReadToEnd())
verwenden oder mit File.ReadAllText(path)
komplett auf den StreamReader verzichten.
Die Read-Methode Die Read-Methode der Klasse StreamReader ist flexibler als ReadLine und kommt auch mit nicht-zeilenorientierten Strömen zurecht.
567
7.5
7
Eingabe und Ausgabe
Public Overrides Function Read() As Integer Public Overrides Function Read( _ buffer As Char(), index As Integer, count As Integer) As Integer
Die parameterlose Variante liest ein Zeichen ein und setzt den Positionszeiger auf das nächste zu lesende Zeichen. Die parametrisierte Variante liest maximal count Zeichen und schreibt sie ab der Position index in das Array buffer. Sie gibt die Zahl der tatsächlich gelesenen Zeichen zurück (0 bei Stromende). Bei einer Datei gibt Ihnen die Eigenschaft Length von FileInfo die maximale Zahl einlesbarer Zeichen an (maximal, weil ein Buchstabe aus bis zur vier Byte besteht). Die folgende Zeile liest einen Dateiinhalt komplett in den angegebenen Puffer. sr.Read(puffer, 0, CType(fi.Length, Integer))
Bitte beachten Sie beim Zeichenzählen, dass jeder Zeilenvorschub auch mitzählt. Unter Windows sind es die beiden Zeichen mit den Codes 10 und 13.
7.5.3
Zeichenketten mit StringWriter und StringReader
Der Endpunkt der Klassen StringReader/StringWriter ist eine Zeichenkette. Damit können Sie diese mit der ganzen Funktionalität der Ströme »nachrüsten«. StringReader liest Zeichen eines Strings, der dem Konstruktor übergeben wird. Public Sub New(s As String)
Die Klasse hat keine Eigenschaften, aber (fast) alle Methoden, die auch ein StreamReader hat (mit gleicher Funktionalität). Public Overrides Sub Close() Public Overrides Function Peek() As Integer Public Overrides Function Read() As Integer Public Overrides Function Read( _ buffer As Char(), index As Integer, count As Integer) As Integer Public Overrides Function ReadLine() As String Public Overrides Function ReadToEnd() As String
Dazu ein Beispiel: Dim str As String = "" str += "Dunkel war's, der Mond schien helle, " & Environment.NewLine str += "Als ein Wagen mit Blitzesschnelle" & Environment.NewLine str += "Langsam um die Ecke fuhr." & Environment.NewLine ' Einlesen und Ausgeben der ersten beiden Zeilen Dim sr2 As New StringReader(str)
568
Binärdaten mit BinaryReader und BinaryWriter
Console.WriteLine(sr2.ReadLine()) Console.WriteLine(sr2.ReadLine())
StringWriter schreibt die Ausgabe in ein Objekt vom Typ StringBuilder, der im Konstruk-
tor ebenso spezifiziert werden kann wie die Art der Formatierung (Zahlen, Datum). Public Public Public Public
Sub Sub Sub Sub
New() New(formatProvider As IFormatProvider) New(sb As StringBuilder) New(sb As StringBuilder, formatProvider As IFormatProvider)
Mit Write schreiben Sie Einzelzeichen oder Zeichenketten in den StringBuilder: Dim sw As New StringWriter() sw.Write("Text des StringWriter-Objekts") Console.WriteLine(sw.GetStringBuilder())
Außer GetStringBuilder hat die Klasse keine besonderen Methoden: Public Overrides Sub Close() Public Overridable Function GetStringBuilder() As StringBuilder Public Overrides Function ToString() As String Public Overrides Sub Write(value As Char) Public Overrides Sub Write(value As String) Public Overrides Sub Write( _ buffer As Char(),index As Integer, count As Integer)
7.6
Binärdaten mit BinaryReader und BinaryWriter
Bis jetzt haben wir von den primitiven Datentypen Bytes sowie Zeichen und Zeichenketten mit Strömen verarbeitet. Nun kommen wir zu den anderen Primitiven.
7.6.1
Schreiben mit BinaryWriter
Ein BinaryWriter-Objekt erzeugen Sie unter Angabe des zu schreibenden Stroms. Optional können Sie die Kodierung von zeichenbasierten Daten angeben: Public Sub New(output As Stream) Public Sub New(output As Stream, encoding As Encoding)
Neben den Schreibroutinen gibt es nur Methoden zum Schließen, Pufferleeren und Setzen des Datenzeigers:
569
7.6
7
Eingabe und Ausgabe
Public Overridable Sub Close() Public Overridable Sub Flush() Public Overridable Function Seek(offset As Integer, origin As SeekOrigin) As Long
Den Kern bilden die Methoden zum Schreiben verschiedener primitiver Datentypen. Die Typen werden binär übermittelt, zum Beispiel 4 Bytes für Integer und 8 Bytes für Double: Public Overridable Sub Write(value As Datum) Datum: Boolean, Byte, Char, Decimal, Double, Integer, Long, SByte, Short, Single, String, UInt16, UInt32, UInt64
Von Bytes und Zeichen können ganze Arrays gleichzeitig geschrieben werden: Public Overridable Sub Write(buffer As Byte()) Public Overridable Sub Write(chars As Char())
7.6.2
Lesen mit BinaryReader
Ein BinaryReader-Objekt erzeugen Sie unter Angabe des zu lesenden Stroms. Optional können Sie die Kodierung von zeichenbasierten Daten angeben. Public Sub New(input As Stream) Public Sub New(input As Stream, encoding As Encoding)
Neben den Leseroutinen gibt es nur Methoden zum Schließen und Lesen ohne Fortschritt des Datenzeigers: Public Overridable Sub Close() Public Overridable Function PeekChar() As Integer
Den Kern bilden die Methoden zum Lesen verschiedener primitiver Datentypen. Es werden so viele Bytes gelesen, wie der Datentyp beansprucht, zum Beispiel 4 für Integer und 8 für Double. Die spezifischen Varianten lösen EndOfStreamException aus, wenn es nichts mehr zu lesen gibt. Public Overridable Function Read() As Integer Public Overridable Function ReadDatum() As Datum Datum: Boolean, Byte, Char, Decimal, Double, Integer, Long, SByte, Short, Single, String, UInt16, UInt32, UInt64
570
Binärdaten mit BinaryReader und BinaryWriter
Von Bytes und Zeichen können auch mehrere gleichzeitig eingelesen werden: Public Overridable Function Read( _ buffer As Byte(), index As Integer, count As Integer) As Integer Public Overridable Function ReadBytes(count As Integer) As Byte() Public Overridable Function Read( _ buffer As Char(),index As Integer, count As Integer) As Integer Public Overridable Function ReadChars(count As Integer) As Char()
Auf den zugrunde liegenden Strom greifen Sie mit der Eigenschaft BaseStream zu. Public Overridable ReadOnly Property BaseStream As Stream
7.6.3
Binäre Datenströme auswerten
Der Vorteil binärer Daten ist deren Effizienz bezüglich Speicherplatz und Geschwindigkeit. Ihr Nachteil ist der komplette Datenverlust, wenn auch nur ein Byte »falsch« interpretiert wird. Im folgenden Beispiel wird die Zahl 500 binär geschrieben und dann byteweise und als Text eingelesen. '...\IO\Ströme\Binärdaten.vb
Option Strict On Imports System.IO Namespace EA Module Binärdaten Sub Test() Dim path As String = IO.Path.GetTempFileName() Dim fi As New FileInfo(path) ' Datei erzeugen und mit Daten füllen Dim fs As New FileStream(path, FileMode.Create) Dim bw As New BinaryWriter(fs) bw.Write(500) bw.Close() ' Datei binär auswerten Dim byteDaten(CType(fi.Length, Integer)) As Byte fs = New FileStream(path, FileMode.Open) fs.Read(byteDaten, 0, CType(fi.Length, Integer)) Console.Write("Als Byte-Array: ") For Each b As Byte In byteDaten : Console.Write(b & " ") : Next Console.WriteLine() fs.Close()
571
7.6
7
Eingabe und Ausgabe
' Datei als Text auswerten Console.Write("Als Text: ") Console.WriteLine(File.ReadAllText(path)) File.Delete(path) Console.ReadLine() End Sub End Module End Namespace
Das Lesen Byte für Byte mit der Methode Read von FileStream läuft völlig korrekt ab. Als Byte-Array: 244 1 0 0 0
Wenn man das Originaldatenformat nicht kennt, gibt es keine Möglichkeit, um festzustellen, ob die Daten vier einzelne Bytes, zwei Short-Werten oder einen Integer repräsentieren (oder noch etwas ganz anderes). Zum Beispiel ist die Interpretation als Text nicht besonders überzeugend: Als Text: ?
Schauen wir uns die Abbildung der vier Zahlen auf einen Integer an. Als Erstes müssen wir die richtige Reihenfolge berücksichtigen. Auf Intel-Prozessoren wird das niederwertigste Byte zuerst gespeichert (Little-Endian). Von groß nach klein sortiert ist die Bytefolge: 0 0 1 244
Das können wir direkt auf eine Bitfolge abbilden: 0000 0000 0000 0000 0000 0001 1111 0100
Die Kombination aller Bits ergibt tatsächlich die Dezimalzahl 500. Diese hardwareabhängige Konvertierung überlassen wir besser .NET und nutzen eine »passende« Methode: Dim byteDaten(CType(fi.Length, Integer)) As Byte Dim br as New BinaryReader(fs) Console.WriteLine(br.ReadInt32())
7.6.4
Komplexe binäre Daten
Auch nicht-primitive Datentypen können binär geschrieben und gelesen werden, denn letztendlich setzen sie sich aus primitiven Typen zusammen, die in die richtige Struktur (Klasse/ Objekt) eingebettet sind. Zur korrekten Handhabung komplexer Typen als Binärdaten müssen bekannt sein: 왘
das Binärformat jedes primitiven Datentyps
왘
die Reihenfolge der Daten im Strom
왘
die Position jedes Datums im komplexen Typ als Funktion seiner Position im Strom
Da die beiden letzten Punkte für praktisch jede Anwendung verschieden sind, hat jedes Programm sein eigenes Binärdatenformat. Die Vorgehensweise bei der Behandlung solcher binärer Daten schauen wir uns am Beispiel einer einfachen Struktur an. Sie hat gegenüber einer Klasse den Vorteil, ein Werttyp zu sein, sodass wir nicht auch noch Referenzen auflösen müs-
572
Binärdaten mit BinaryReader und BinaryWriter
sen. So bleibt das Beispiel übersichtlicher, ohne seine Allgemeingültigkeit zu verlieren, denn die gezeigten Schritte lassen sich einfach auf beliebig komplexe andere Datentypen übertragen. Der Anschaulichkeit halber verwenden wir die Repräsentierung eines Punkts: '...\IO\Ströme\Binärstruktur.vb
Structure Punkt Public X, Y As Integer Public Farbe As Long End Structure
Die ganzzahligen Datentypen sind bewusst verschieden gewählt, damit eine Behandlung als Integer-Array ausscheidet. Wir entwickeln eine Anwendung, die solche Punkte speichern kann und wahlfrei einen beliebigen Punkt auslesen kann. In der Datei stehen die Punkte einfach hintereinander. Die Information über deren Anzahl können wir auf zwei Arten erhalten: 왘
Das Ende der Datei wird durch Daten markiert, die unmöglich ein Punkt sein können.
왘
Am Anfang der Datei steht die Punktzahl.
Wir entscheiden uns für die zweite Variante (siehe Abbildung 7.4). Da alle Daten binär sind, muss auch das Format dieser Anzahl genau bekannt sein, damit jedes Byte in der Datei eine klare Zuordnung erhält. Im Folgenden wird diese Information in einem Integer gespeichert. Dateianfang
4 Byte – Anzahl der PointObjekte
16 Byte – 1. Point
16 Byte – 2. Point
16 Byte – 3. Point
Gesamtgröße der Datei = 52 Byte
Abbildung 7.4
Datei mit drei gespeicherten Punkten
Die Auswertung der ersten vier Bytes liefert die Anzahl der gespeicherten Punkte, die folgenden insgesamt 16 Byte großen Blöcke beschreiben jeweils einen Punkt. Wir fassen die Methoden zur Handhabung in einem Modul namens Binärpunkte zusammen. 왘
Speichern
왘
Lesen
Die Methode Speichern bekommt als Parameter den Pfad zu einer Datei und ein Array mit den zu speichernden Punkten. Ein FileStream für diesen Pfad bildet die Basis eines BinaryWriter. Er schreibt zuerst die Anzahl Punkte binär in die Datei und dann in einer Schleife alle Punkte. Sie legt die Reihenfolge der Daten fest. Der Wert FileMode.Create für den Dateistrom garantiert, dass nur die in diesem Aufruf geschriebenen Daten in der Datei sind.
573
7.6
7
Eingabe und Ausgabe
'...\IO\Ströme\Binärstruktur.vb
Sub Schreiben(ByVal pfad As String, ByVal punkte As Punkt()) Dim fs As New FileStream(pfad, FileMode.Create) Dim bw As New BinaryWriter(fs) ' Anzahl der Punkte in die Datei schreiben bw.Write(punkte.Length) ' die Punkte in die Datei schreiben For Each p As Punkt In punkte bw.Write(p.X) : bw.Write(p.Y) : bw.Write(p.Farbe) Next bw.Close() End Sub
Mit der Schreibroutine liegt das Datenformat fest, und wir können die Leseroutine implementieren. Das Lesen erfolgt in derselben Reihenfolge wie das Schreiben: '...\IO\Ströme\Binärstruktur.vb
Function Lesen(ByVal pfad As String) As Punkt() Dim fs As New FileStream(pfad, FileMode.Open) Dim br As New BinaryReader(fs) ' die ersten 4 Bytes spezifizieren die Anzahl der Punkte Dim anzahl As Integer = br.ReadInt32() ' einlesen der Punkte Dim punkte(anzahl – 1) As Punkt For i As Integer = 0 To anzahl – 1 punkte(i).X = br.ReadInt32() : punkte(i).Y = br.ReadInt32() punkte(i).Farbe = br.ReadInt64() Next br.Close() Return punkte End Function
Da uns der Platzbedarf aller Daten bekannt ist, können wir an die richtige Position in der Datei springen und einen einzelnen Punkt einlesen. Um das Beispiel einfach zu halten, verzichte ich auf die Implementierung eigener Ausnahmeklassen. '...\IO\Ströme\Binärstruktur.vb
Function Lesen(ByVal pfad As String, ByVal nr As Integer) As Punkt Dim fs As New FileStream(pfad, FileMode.Open) Dim pos As Integer = 4 + (nr – 1) * 16 Dim br As New BinaryReader(fs) ' Ist die Position gültig? If nr br.ReadInt32() Then Throw New ArgumentException("Zu groß")
574
Binärdaten mit BinaryReader und BinaryWriter
' Daten des gewünschten Points einlesen fs.Seek(pos, SeekOrigin.Begin) Dim p As New Punkt() p.X = br.ReadInt32() : p.Y = br.ReadInt32():p.Farbe = br.ReadInt64() br.Close() Return p End Function
Am Dateianfang belegt die Punktzahl 4 Bytes, danach belegt jeder Punkt 16 Bytes. Damit haben wir die Position vor dem einzulesenden Punkt: Dim pos As Integer = 4 + (nr – 1) * 16
Sie wird der Seek-Methode des BinaryReader übergeben: fs.Seek(pos, SeekOrigin.Begin)
Damit ist unsere Klassendefinition fertig, und wir können abschließend die Implementierung testen. Dazu erzeugen wir ein Array mit zwei Testpunkten und speichern es mit Binärpunkte.Schreiben in eine Datei. Diese lesen wir mit Binärpunkte.Lesen aus und geben die gelesenen Punkte aus. Schließlich lesen wir einen Einzelpunkt und geben ihn aus. '...\IO\Ströme\Binärstruktur.vb
Option Strict On Imports System.IO Namespace EA Structure Punkt ... Module Binärpunkte ... Module Binärstruktur Sub Test() Dim path As String = IO.Path.GetTempFileName() ' Testpunkte erzeugen Dim punkte(1) As Punkt punkte(0).X = 10 : punkte(0).Y = 20 : punkte(0).Farbe = 310 punkte(1).X = 40 : punkte(1).Y = 50 : punkte(1).Farbe = 110 ' Punkte speichern Binärpunkte.Schreiben(path, punkte) ' Punkte einlesen und ausgeben For Each p As Punkt In Binärpunkte.Lesen(path) Console.WriteLine("{{X={0}, Y={1}, Farbe={2}}}", p.X, p.Y, p.Farbe) Next ' Einzelpunkt einlesen und ausgeben Console.Write("Nummer des einzulesenden Punkts: ") Dim nr As Integer = Convert.ToInt32(Console.ReadLine()) Try Dim p As Punkt = Binärpunkte.Lesen(path, nr) Console.WriteLine("{{X={0}, Y={1}, Farbe={2}}}", p.X, p.Y, p.Farbe)
575
7.6
7
Eingabe und Ausgabe
Catch ex As ArgumentException Console.WriteLine("Fehler: {0}", ex.Message) End Try File.Delete(path) Console.ReadLine() End Sub End Module End Namespace
Die Ausgabe bestätigt die korrekte Implementierung: {X=10, {X=40, Nummer {X=10,
7.7
Y=20, Farbe=310} Y=50, Farbe=110} des einzulesenden Punkts: 1 Y=20, Farbe=310}
Serialisierung
Die in Abschnitt 7.6.4, »Komplexe binäre Daten«, gezeigte Speicherung und Wiederherstellung ist eine Möglichkeit, Daten zur Wiederverwendung zu sichern. Viel einfacher ist es, sich auf die Möglichkeiten von .NET zu stützen. Der Prozess, Daten in einen Strom (zum Beispiel eine Datei) zu speichern, wird Serialisierung genannt. Die Serialisierung kann ein im Hauptspeicher befindliches Objekt in ein bestimmtes Format konvertieren und in einen Strom schreiben. Außerdem kann das Objekt in seinem ursprünglichen Format rekonstruiert werden. Diese Prozesse laufen automatisch ab. Die gespeicherten Daten bestehen aus dem Namen der Anwendung, dem Namen der Klasse und den Objektdaten. Dadurch wird die spätere Rekonstruktion in einer exakten Kopie möglich. Ein zu sichernder Objektzustand ist vollständig durch die Felder des Objekts beschrieben. Alle weiteren Informationen sind mit der Klasse verbunden und nicht Teil des Zustandes.
7.7.1
Serialisierungsverfahren
Die .NET-Klassenbibliothek hat für die Formatierung der Objektdaten drei Klassen (siehe Tabelle 7.21). Sie bestimmen das Datenformat, das in den Strom geschrieben wird bzw. aus ihm gelesen wird. Klasse
Übertragungsformat
BinaryFormatter
Binäres Format, das zirkuläre Referenzen unterstützt
SoapFormatter
SOAP-Format (Simple Object Access Protocol), das zirkuläre Referenzen unterstützt. System.Runtime.Serialization.Formatters.Soap.dll muss eingebunden werden.
XmlSerializer
XML-Format, das zirkuläre Referenzen nicht unterstützt. System.Xml.dll muss eingebunden werden.
Tabelle 7.21
576
.NET-Serialisierungsklassen
Serialisierung
Alle drei implementieren die Schnittstelle IFormatter. Kommen Sie mit keiner der drei Klassen zurecht, müssen Ihre eigenen Klassen diese Schnittstelle implementieren. Public Interface IFormatter Function Deserialize(serializationStream As Stream) As Object Sub Serialize(serializationStream As Stream, graph As Object) Property Binder As SerializationBinder Property Context As StreamingContext Property SurrogateSelector As IsurrogateSelector End Interface
Die Speicherung von Objektdaten in einen Strom übernimmt Serialize. Das erste Argument spezifiziert den Strom, in den geschrieben wird; oft ist das ein FileStream. Das zu serialisierende Objekt wird im zweiten Parameter übergeben. Die Methode Deserialize rekonstruiert ein Objekt. Das Argument spezifiziert den Strom, aus dem gelesen wird; oft ist das ein FileStream. Der Rückgabewert ist vom Typ Object und muss deshalb noch in den richtigen Typ konvertiert werden.
7.7.2
Testklassen
Alle Objekte einer Klasse, die das Attribut Serializable hat, sind serialisierbar. Public Class ClassA ...
Wenn Sie versuchen, Objekte von Klassen zu serialisieren, die das Attribut nicht haben, wird die Ausnahme SerializationException ausgelöst. Alle Felder der Klasse ClassA, unabhängig davon, ob sie privat oder öffentlich deklariert sind, werden von der Serialisierung erfasst. Es gibt aber auch eine Einschränkung: Lokale und klassengebundene Variablen nehmen an einem Serialisierungsprozess nicht teil.
Die meisten Beispiele zur Serialisierung verwenden eine Klasse ClassA mit einem privaten und einem öffentliches Feld, die beide über einen Konstruktor initialisiert werden. '...\IO\Serialisierung\ClassA.vb
Namespace EA Public Class ClassA Public intVar As Integer Private strVar As String Public Sub New() End Sub Public Sub New(ByVal x As Integer, ByVal str As String) intVar = x
577
7.7
7
Eingabe und Ausgabe
strVar = str End Sub Property Name() As String Get Return strVar End Get Set(ByVal value As String) strVar = value End Set End Property End Class End Namespace
Einige Beispiele verwenden außerdem eine Klasse Person mit einem öffentlichen Feld: '...\IO\Serialisierung\Binär.vb
Namespace EA Public Class Person Public Name As String Public Sub New(str As String) Name = str End Sub End Class End Namespace
Bei der Serialisierung greift der Prozess den Inhalt von intVar und strName und speichert ihn entweder in einer Datei, im Netzwerk oder in einer Datenbank. Die Deserialisierung belegt diese beiden mit den gelesenen Werten.
7.7.3
Serialisierung mit BinaryFormatter
Im folgenden Beispiel verwenden wir BinaryFormatter zur Formatierung der über einen Dateistrom geschickten Daten (zu ClassA siehe Abschnitt 7.7.2, »Testklassen«). Nach dessen Instanziierung erzeugen wir ein Testobjekt. Die Methode Serialize aus der Klasse BinaryFormatter speichert dieses Objekt in einem FileStream, der es wiederum in der angegebenen Datei speichert. Durch den Wert FileMode.Create ist sichergestellt, dass die Datei keine zusätzlichen Daten enthält. Danach lesen wir ein Objekt mit Deserialize aus einem FileStream, der auf dieselbe Datei zugreift, und konvertieren es in den richtigen Typ. Schließlich geben wir das gelesene Objekt aus. Um das Beispiel kurz zu halten, ist alles in einer Methode. '...\IO\Serialisierung\Binär.vb
Option Strict On Imports System.IO Namespace EA Module Binär
578
Serialisierung
Sub Test() Dim path As String = IO.Path.GetTempFileName() Dim fs As FileStream ' Objekt und Formatierer erzeugen Dim bf As New System.Runtime.Serialization.Formatters.Binary. _ BinaryFormatter() Dim obj As New ClassA(310, "Peter") ' speichern fs = New FileStream(path, FileMode.Create) bf.Serialize(fs, obj) fs.Close() ' lesen fs = New FileStream(path, FileMode.Open) Dim obj2 As ClassA = CType(bf.Deserialize(fs), ClassA) fs.Close() ' Ausgabe Console.WriteLine("{{intVar={0}, Name={1}}}", obj2.intVar, obj2.Name) File.Delete(path) Console.ReadLine() End Sub End Module End Namespace
Die Ausgabe zeigt die Daten des mit New erzeugten Objekts; die Serialisierung hat geklappt. {intVar=310, Name=Peter}
Serialisierung mehrerer Objekte Der Serialisierungsprozess kann auch im Typ unterschiedliche Objekte erfassen. Für jedes Objekt erfolgt ein eigener Aufruf von Serialize bzw. Deserialize für denselben Strom. Die Reihenfolge der Objekte ist beim Schreiben und Lesen identisch. Das ist ein FIFO-Prinzip (first in – first out): Das zuerst serialisierte Objekt muss auch als Erstes wieder deserialisiert werden. Bitte beachten Sie, dass das Lesen über das Ende des Datenstroms hinaus eine Ausnahme zur Folge hat. Anstatt jedes Objekt einzeln zu behandeln, packen wir die Objekte in eine Auflistung, die serialisierbar ist – im Beispiel ArrayList. Da sie die Objekte in Feldern speichert, werden sie von der Serialisierung erfasst, und durch den rekursiven Charakter der Serialisierung werden deren Felder ebenfalls serialisiert. Wie das folgende Beispiel zeigt, tritt dann einfach die Auflistung an die Stelle eines Einzelobjekts. Das bedeutet: Es gibt nur einen Aufruf Serialize und nur einen Aufruf Deserialize, für egal wie viele Objekte. Das Beispiel verwendet die Klassen ClassA und Person des Abschnitts 7.7.2, »Testklassen«. Erst wird eine Liste mit verschiedenen Objekttypen serialisiert. Dann folgt die Deserialisierung mit anschließender Testausgabe.
579
7.7
7
Eingabe und Ausgabe
'...\IO\Serialisierung\Gemischt.vb
Option Strict On Imports System.IO Imports System.Runtime.Serialization Imports System.Runtime.Serialization.Formatters.Binary Namespace EA Module Gemischt Sub Test() Dim path As String = IO.Path.GetTempFileName() Dim fs As FileStream ' Objekt und Formatierer erzeugen Dim liste As New ArrayList() liste.Add(New ClassA(2334, "Freddy")) liste.Add(New Person("Microsoft")) liste.Add(New ClassA(13, "Beate")) liste.Add(New Person("Tollsoft")) Dim bf As New BinaryFormatter() ' speichern fs = New FileStream(path, FileMode.Create) bf.Serialize(fs, liste) fs.Close() ' lesen fs = New FileStream(path, FileMode.Open) Dim liste2 As ArrayList Try liste2 = CType(bf.Deserialize(fs), ArrayList) Catch ex As SerializationException Console.WriteLine("Serialisierung: {0}", ex.Message) : Return Catch ex As IOException Console.WriteLine("Lesefehler: {0}", ex.Message) : Return Finally fs.Close() End Try ' Ausgabe For Each o As Object In liste2 If TypeOf o Is ClassA Then Dim c As ClassA = CType(o, ClassA) Console.WriteLine("{{intVar={0}, Name={1}}}", c.intVar, c.Name) Else Dim p As Person = CType(o, Person) Console.WriteLine("{{Name={0}}}", p.Name) End If Next File.Delete(path) Console.ReadLine()
580
Serialisierung
End Sub End Module End Namespace
Die Ausgabe bestätigt die korrekte Serialisierung: {intVar=2334, Name=Freddy} {Name=Microsoft} {intVar=13, Name=Beate} {Name=Tollsoft}
Nichtserialisierte Daten Alle Felder mit dem Attribut NonSerialized sind explizit von der Serialisierung ausgenommen. Zum Beispiel sollten Felder mit Kennwörtern nicht gespeichert werden: Class Login Public Name As String Private Kennwort As String ... End Class
Serialisierung und Vererbung Das Serializable-Attribut wird nicht vererbt. Wenn Sie eine serialisierbare Klasse ClassA entwickeln und daraus die Klasse ClassB ableiten, muss diese selbst dann das Attribut haben, wenn nur die aus ClassA geerbten Mitglieder serialisiert werden sollen. Ansonsten ist die Subklasse nicht serialisierbar. Weiterhin ist eine Kindklasse nur serialisierbar, wenn auch die Elternklasse serialisierbar ist.
7.7.4
Serialisierung mit SoapFormatter
Die Serialisierung von Objektdaten mit SoapFormatter unterscheidet sich nicht von der mit BinaryFormatter. Allerdings muss zuerst eine weitere Bibliotheksdatei, wie in Tabelle 7.21, ».NET-Serialisierungsklassen« angegeben, unter Verweise eingetragen werden. Abbildung 7.5 zeigt eine typische Ausgabe, geöffnet im Internet Explorer.
Abbildung 7.5
Serialisierte Daten im SOAP-Format
581
7.7
7
Eingabe und Ausgabe
7.7.5
Serialisierung mit XmlSerializer
Die Serialisierung mit der Klasse XmlSerializer aus dem Namensraum System.Xml.Serialization.XmlSerializer unterscheidet sich gravierend von den Klassen BinaryFormatter und SoapFormatter: 왘
Die im XML-Format zu serialisierende Klasse muss als Public definiert sein.
왘
Nur als Public deklarierte Instanzfelder werden serialisiert, klassengebundene sind ausgeschlossen.
왘
Die zu serialisierende Klasse muss einen öffentlichen, parameterlosen Konstruktor haben. Dieser wird von XmlSerializer aufgerufen.
왘
Die Steuerung der XML-Serialisierung erfolgt mit Attributen aus dem Namensraum System.Xml.Serialization.
왘
Das Serializable-Attribut ist nicht zwingend vorgeschrieben.
Obwohl die XML-Serialisierung aufwändiger zu programmieren ist, hat sie einen großen Vorteil: XML ist ein offener Standard und deshalb plattformunabhängig. Die auf diese Weise serialisierten Daten lassen sich folglich von »beliebigen« Anwendungen verarbeiten. Im folgenden Beispiel wird das Beispiel aus Abschnitt 7.7.3, »Serialisierung mit BinaryFormatter«, so umgeschrieben, dass statt des BinaryFormatter ein XmlSerializer verwendet wird. Die Serialisierungsklassen ClassA und Person zeigt Abschnitt 7.7.2, »Testklassen«. '...\IO\Serialisierung\Xml.vb
Option Strict On Imports System.IO Imports System.Xml.Serialization Namespace EA Module Xml Sub Test() Dim path As String = IO.Path.GetTempFileName() Dim fs As FileStream ' Objekt und Formatierer erzeugen Dim bf As New XmlSerializer(GetType(ClassA)) Dim obj As New ClassA(310, "Peter") ' speichern fs = New FileStream(path, FileMode.Create) bf.Serialize(fs, obj) fs.Close() ' lesen fs = New FileStream(path, FileMode.Open) Dim obj2 As ClassA = CType(bf.Deserialize(fs), ClassA) fs.Close() ' Ausgabe Console.WriteLine("{{intVar={0}, Name={1}}}", obj2.intVar, obj2.Name) Console.WriteLine(File.ReadAllText(path))
582
Serialisierung
File.Delete(path) Console.ReadLine() End Sub End Module End Namespace
Nur eine Zeile unterscheidet die binäre von der XML-Serialisierung: Dim bf As New XmlSerializer(GetType(ClassA))
Der Inhalt der XML-Datei ist die zweite Ausgabe: {intVar=310, Name=Peter} 310 Peter
XML-Serialisierung mit Attributen steuern Die Auswahl der serialisierten Daten und deren Ausgabeformat lassen sich mit Attributen steuern, die im Namensraum System.Xml.Serialization definiert sind. Tabelle 7.22 gibt einen kleinen Überblick über die wichtigsten Attribute. Attribut
Beschreibung
XmlArray
Ein bestimmtes Klassenmitglied wird als Array serialisiert.
XmlArrayItem
Legt den XML-Bezeichner für den vom Array verwalteten Typ fest.
XmlAttribute
Die Eigenschaft wird als XML-Attribut statt als XML-Element serialisiert.
XmlElement
XML-Elementnamen, standardmäßig der Bezeichner des Feldes
XmlIgnore
Legt fest, dass die Eigenschaft nicht serialisiert werden soll.
XmlRoot
XML-Wurzelelement, standardmäßig der Name der serialisierten Klasse
Tabelle 7.22
Attribute zur Steuerung der Ausgabe in einer XML-Datei
Das folgende Beispiel verwendet einige der Attribute. Hier sehen Sie zuerst die Definition der zu serialisierenden Klasse: eine Teilnehmerliste mit einzelnen Teilnehmern. '...\IO\Serialisierung\XmlAttribute.vb
Option Strict On Imports System.IO Imports System.Xml.Serialization Namespace EA Public Class Teilnehmerliste Public Listenname As String
583
7.7
7
Eingabe und Ausgabe
_ Public Personen As Teilnehmer() Public Sub New() End Sub Public Sub New(ByVal name As String) Listenname = name End Sub End Class Public Class Teilnehmer Public Zuname As String Public Ort As String Public Lebensalter As Integer Public ID As String Public Sub New() End Sub Public Sub New(zuname As String, ort As String, _ alter As Integer, id As String) Me.Zuname = zuname Me.Ort = ort Me.Lebensalter = alter Me.ID = id End Sub End Class ... End Namespace
Im Testcode erzeugen wir eine Teilnehmerliste mit zwei Teilnehmer-Einträgen, die wir wie üblich serialisieren und deserialisieren. Ansatt des XML-Dateiinhalts serialisieren wir das gerade deserialisierte Objekt in die Konsole. '...\IO\Serialisierung\XmlAttribute.vb
Option Strict On Imports System.IO Imports System.Xml.Serialization Namespace EA ... Module XmlAttribute Sub Test() Dim path As String = IO.Path.GetTempFileName() Dim fs As FileStream ' Objekt und Formatierer erzeugen Dim katalog As New Teilnehmerliste("Teilnehmerliste")
584
Serialisierung
Dim personen(1) As Teilnehmer personen(0) = New Teilnehmer("Peter", "Berlin", 45, "117") personen(1) = New Teilnehmer() personen(1).Zuname = "Franz-Josef" personen(1).Ort = "Aschaffenburg" katalog.Personen = personen Dim bf As New XmlSerializer(GetType(Teilnehmerliste)) ' speichern fs = New FileStream(path, FileMode.Create) bf.Serialize(fs, katalog) fs.Close() ' lesen fs = New FileStream(path, FileMode.Open) Dim katalog2 As Teilnehmerliste = _ CType(bf.Deserialize(fs), Teilnehmerliste) fs.Close() ' Ausgabe bf.Serialize(Console.Out, katalog2) 'Console.WriteLine(File.ReadAllText(path)) File.Delete(path) Console.ReadLine() End Sub End Module End Namespace
Bis auf das encoding-Attribut der ersten Zeile zeigt die Ausgabe den Inhalt der XML-Datei: Teilnehmerliste Peter Berlin 45 Franz-Josef Aschaffenburg 0
Beachten Sie, wie die Verwendung der Attribute die Elementbezeichner in der XML-Ausgabe ändert.
585
7.7
Der Aufbau einer .NET-Anwendung und das Zusammenspiel mit Bibliotheken bildet den ersten Teil dieses Kapitels, der zweite Teil widmet sich Konfigurationsdateien.
8
Anwendungen: Struktur und Installation
8.1
Bibliotheken
Sobald Projekte etwas größer werden, teilen Sie den Code auf mehrere Dateien auf. Damit Sie das Rad nicht bei jedem Projekt neu erfinden, sollten Sie die eventuell für andere Projekte nützliche Funktionalität in Bibliotheken zusammenfassen. Sie werden vom Compiler in Dateien mit der Endung dll abgelegt und sind selbst nicht lauffähig, sondern müssen von einer Anwendung aus angesprochen werden. Das Visual Studio bietet zur Erstellung einige Vorlagen an. Wir konzentrieren uns hier auf die Projektvorlage Klassenbibliothek. Vorhandene Quelltextdateien können Sie auf zwei Arten in einer Bibliothek integrieren: 왘
In den Projekteigenschaften, erreichbar über das Menü Projekt, legen Sie auf der Karteikarte Anwendung im Listenfeld den Anwendungstyp Klassenbibliothek fest.
왘
Fügen Sie über das Kontextmenü Hinzufügen 폷 Vorhandenes Element die Quelltextdateien hinzu.
8.1.1
Projekte in einer Projektmappe
In Abschnitt 2.4, »Projektorganisation«, habe ich Ihnen bereits gezeigt, wie Sie mehrere Projekte in einer Projektmappe organisieren können. Das hat den Vorteil, dass Sie nicht für jedes offene Projekt eine eigene Instanz von Visual Studio starten müssen. Die Zusammenfassung von Projekten in einer Projektmappe hat keinen Einfluss auf die Kompilation. Abhängigkeiten der einzelnen Projekte müssen Sie in den Projekteigenschaften festlegen. Von den Projekten einer Mappe kann nur jeweils eines aus dem Visual Studio heraus gestartet werden. Das Startprojekt ist im Projektmappenexplorer fett geschrieben. Über den Kontextmenüpunkt Als Startprojekt festlegen eines Projekts können Sie es zum Startprojekt machen. Physikalisch werden Projekte als Unterordner des übergeordneten Ordners der Projektmappe gespeichert. Die .sln-Datei in diesem Verzeichnis definiert die Zusammensetzung der Projektmappe, die .suo-Datei speichert die für die Mappe festgelegten Optionen.
587
8
Anwendungen: Struktur und Installation
8.1.2
Zugriffsmodifikatoren
Damit eine Anwendung auf die Funktionalität einer Bibliothek zugreifen kann, muss diese die Sichtbarkeit Public oder, im Falle von Kindklassen, Protected haben. Auf Klassenebene gibt es nur die Modifizierer Public und Friend. Wie in Abschnitt 3.2.1, »Kombination«, beschrieben ist, können Modifizierer nur einschränken, nicht erweitern. Damit muss eine Bibliotheksklasse den Modifizierer Public haben, und ihre Mitglieder müssen Public oder Protected sein. Neben diesen Modifizierern können Sie Friend-Klassen und -Mitglieder einer gegebenen Anwendung oder Bibliothek zugänglich machen. Dazu geben Sie der Bibliothek das Attribut InternalsVisibleTo aus dem Namensraum System.Runtime.CompilerServices. Dem ersten Argument übergeben Sie den Namen der Bibliothek oder Anwendung, die auf die Friend-Elemente zugreifen können soll, zum Beispiel: Imports System.Runtime.CompilerServices Namespace ClassLibrary Class X ... End Namespace
8.1.3
Einbinden einer Klassenbibliothek
Über den Knoten Verweise eines Projekts im Projektmappen-Explorer können Sie dem Projekt eine Bibliothek bekannt machen (gegebenenfalls müssen Sie den Knoten über die Schaltfläche Alle Dateien anzeigen sichtbar machen). Im Kontextmenü des Knotens wählen Sie Verweis hinzufügen... Alternativ klicken Sie auf die gleichnamige Schaltfläche auf der Karteikarte Verweise der Projekteigenschaften. Daraufhin öffnet sich der in Abbildung 8.1 gezeigte Dialog mit den Registerkarten .NET, COM, Projekte, Durchsuchen und Aktuell (siehe Tabelle 8.1). Registerkarte
Bedeutung
.NET
Grundlegende Bibliotheken im Global Assembly Cache (siehe unten)
COM
Common Object Model: Vorgängertechnologie von .NET zur Interoperabilität von Anwendungen »beliebiger« Programmiersprachen
Projekte
Alle in der Visual Studio-Umgebung verfügbaren Projekte
Durchsuchen
Navigation im Dateisystem zu einer Bibliothek
Aktuell
Zeigt alle vor kurzem hinzugefügten Verweise an
Tabelle 8.1
Registerkarten des »Verweise«-Dialogs
Bitte beachten Sie, dass selbst teilweise innerhalb von .NET ein Bibliotheksname nichts mit dem oder den Namensräumen der Definitionen zu tun haben muss.
588
Assemblies
Abbildung 8.1
8.2
Der Dialog »Verweise hinzufügen«
Assemblies
Wenn Sie eine Konsolen-, Windows- oder Windows-Dienst-Anwendung entwickeln, wird eine EXE-Datei erzeugt. Ist das Projekt zum Beispiel vom Typ Klassenbibliothek, wird eine nicht eigenständig ausführbare DLL-Datei generiert. Die Kompilate werden, abhängig von der Konfigurationseinstellung, im Ordner /bin/Debug bzw. obj/Debug unterhalb des Projektordners gespeichert. Die Interoperabilität von (D)COM basiert auf einem eigenen Typsystem und der Orientierung an Schnittstellen und nicht an Implementierungen. Die Schnittstellenbeschreibungen sind in der Registrierungsdatenbank abgelegt, der Programmcode getrennt davon im Dateisystem. Das Typsystem erlaubt keine unterschiedlichen Versionen derselben Komponente: Das ist die sogenannte DLL-Hölle. Wegen dieser Probleme führte .NET ein neues Konzept ein: 왘
Code und Selbstbeschreibung einer Komponente bilden eine Einheit (Registry unnötig).
왘
Verschiedene Versionen einer Komponente können parallel installiert und auch parallel ausgeführt werden (keine Abwärtskompatibilität).
왘
Eine Anwendung muss selber wissen, welche Komponentenversionen sie nutzen kann. Die Laufzeitumgebung muss dementsprechend versionsrichtige Komponenten laden.
Diese Komponenten werden Assembly genannt. Ihre Selbstbeschreibung (inklusive Version und Sicherheitsrichtlinien) und die Beschreibung benötigter Komponenten (inklusive Version) erfolgt als ein Teil des Codes, der als Manifest bezeichnet wird. Es gibt zwei AssemblyTypen:
589
8.2
8
Anwendungen: Struktur und Installation
왘
Private Assemblies die nur von einer Anwendung genutzt werden können und im selben Verzeichnis liegen müssen.
왘
Gemeinsame Assemblies (globale Assemblies), die von allen Anwendungen genutzt werden können und im Global Assembly Cache (GAC) liegen. Für eine Veröffentlichung im GAC ist ein kryptografischer Schlüssel Voraussetzung. Dieser gewährleistet, dass eine Assembly von einer anderen, zufälligerweise gleichnamigen Assembly eines anderen Entwicklers eindeutig unterschieden werden kann.
8.2.1
Struktur einer Assembly
Um eine einfache und sichere Versionierung und Verteilung zu erreichen, muss die Selbstbeschreibung einer Assembly einige Informationen enthalten: 왘
der Name, um die Assembly zu identifizieren
왘
eine Angabe darüber, ob die vorliegende Assembly das ursprüngliche Original oder eine neuere Version ist
왘
Abhängigkeiten von anderen Komponenten, inklusive deren Namen und Versionsnummern
왘
Informationen über die von der Assembly exportierten Typen
왘
Bezeichner aller Methoden, inklusive Parameternamen und -typen, und den Typ des Rückgabewertes
Diese Metadaten sind Daten, die andere Daten beschreiben. Beispiele dafür sind Datentypen und Standardwerte von Tabellenspalten. Eine Assemblierung hat damit drei Blöcke: 왘
Metadaten, die die Assembly allgemein beschreiben (das Manifest)
왘
Typmetadaten, die die öffentlichen Typen beschreiben
왘
den IL-Code inklusive Ressourcen (z. B. Bilder, Tabellen für Fremdsprachenübersetzung)
Eine Assembly muss Metadaten enthalten, selbst der Code und die Ressourcen sind optional.
Manifest und Metadaten Metadaten sind binär in der DLL- bzw. EXE-Datei (der Assembly) gespeichert. Sie werden von der Common Language Runtime benötigt, um Datentypen und Objekte überhaupt verwenden zu können. Sie lassen sich in zwei Gruppen einteilen: 왘
Das Manifest, das die Struktur einer Assembly beschreibt, enthält unter anderem: 왘
Typname
왘
Versionsnummer
왘
öffentliche Schlüssel
왘
Liste aller Dateien, aus denen sich die Assembly zusammensetzt
왘
Liste aller weiteren Assemblies, die an die aktuelle Assembly statisch gebunden sind
왘
Sicherheitsrichtlinien, die die Berechtigungen an der Assembly steuern
590
Assemblies
왘
Typmetadaten, die die Typen des IL-Codes innerhalb einer Komponente beschreiben. Das schließt den Namen des Typs, seine Sichtbarkeit, seine Basisklassen und die von ihm implementierten Schnittstellen ein.
Es spielt keine Rolle, in welcher Sprache eine Assembly entwickelt worden ist. Das Manifest verwischt die Spuren des zugrunde liegenden Quellcodes. Die Intermediate Language (IL) und die Common Language Runtime (CLR) schaffen mittels des Manifests die Voraussetzung für den problemlosen Austausch, ohne weitere Informationen zu benötigen.
IL-Disassembler Das mit Visual Studio gelieferte Tool ildasm.exe, der sogenannten IL-Disassembler, macht die Metadaten sichtbar. In der Standardinstallation befindet er sich im Verzeichnis ...\Programme\Microsoft SDKs\Windows\v6.0A\Bin. Dieser Pfad ist in der Eingabeaufforderung bereits gesetzt, die Sie über den Startmenüpunkt Start 폷 Alle Programme 폷 Microsoft Visual Studio 2008 폷 Visual Studio Tools 폷 Visual Studio 2008 Eingabeaufforderung öffnen. Entweder geben Sie hinter ildasm einen Dateipfad zu einer Assembly an, oder Sie öffnen die grafische Oberfläche durch Weglassen des Dateipfades. Zum Beispiel: ildasm C:\MeineProjekte\MyFirstAssembly.exe
Sehen wir uns mit dem ILDASM-Tool das Manifest der folgenden Konsolenanwendung an, die in der Datei ClassA.vb die Main-Methode und eine Klasse ClassA enthält und in der Datei ClassB.vb die Klasse ClassB. Um den Informationsgehalt im Disassembler zu verdeutlichen, enthält der Typ ClassB insgesamt drei Variablendeklarationen mit unterschiedlichen Sichtbarkeiten. '...\Applikation\ILdasm\ClassA.vb
Imports System.Data Namespace MyAssembly Module Start Sub Main() Dim col As DataColumn = New DataColumn() Console.WriteLine("Hallo Welt.") Console.ReadLine() End Sub End Module Public Class ClassA Public intVar As Integer End Class End Namespace
'...\Applikation\ILdasm\ClassB.vb
Public Class ClassB Public intVar As Integer Private lngVar As Long
591
8.2
8
Anwendungen: Struktur und Installation
Protected strText As String End Class
Der Name der Assembly ist ILdasm. Sehen wir uns jetzt an, was uns das ILDASM-Tool liefert, ohne dabei allzu sehr in die Details zu gehen (siehe Abbildung 8.2).
Abbildung 8.2
Anzeige des ILDASM-Tools
Unterhalb des Wurzelknotens, der den Pfad zu der Assemblierung angibt, steht das Manifest. Darunter ist der Knoten ILdasm, der in der Abbildung bis auf den Namensraum My vollständig geöffnet ist. Neben den Typmetadaten listet das Tool alle Variablen und Klassenmethoden auf – in unserem Beispiel nur die statische Methode Main aus Start sowie die mit .ctor bezeichneten Konstruktoren – und gibt den Sichtbarkeitsbereich der Variablen an. Der Rückgabewert der Methoden wird, getrennt durch einen Doppelpunkt, hinter dem Methodennamen angeführt. Das Manifest sehen Sie nach einem Doppelklick auf den Manifest-Eintrag in einem neuen Fenster. Der Reihe nach werden zuerst alle externen Assemblies aufgelistet, von denen die aktuelle Anwendung abhängt, unter anderem auch System.Data. Die wichtigste Assembly mscorlib aus mscorlib.dll macht den Anfang. .assembly extern mscorlib { .publickeytoken = (B7 7A 5C 56 19 34 E0 89 ) .ver 2:0:0:0 }
592
// .z\V.4..
Assemblies
Jede gemeinsam genutzte Assembly im GAC hat einen öffentlichen Schlüssel. Im Manifest steht er hinter dem Attribut .publickeytoken in geschweiften Klammern. Neben dem Namen einer Assemblierung ist unter anderem auch der Schlüssel Teil ihrer Identität. Er sichert gleichzeitig die Identität des Komponentenentwicklers (siehe unten). Ein weiterer Teil der Identität ist die Version, die im Manifest im Attribut .ver gespeichert ist. Dadurch sind gleichnamige Assemblierungen verschiedener Versionen unterschiedlich und können nebeneinander existieren. Der Liste der externen Assemblierungen schließt sich im Block .assembly ILdasm
eine Liste diverser Attribute an, mit denen die Assemblierung beschrieben wird. Die Attribute werden in den Projekteigenschaften oder der Datei AssemblyInfo.vb festgelegt.
8.2.2
Globale Assemblies
Eine globale Assembly steht allen .NET-Anwendungen zur Verfügung, ein Beispiel sind die Klassen des .NET Frameworks. Bereits vor der Kompilierung entscheiden Sie, ob eine Assembly privat oder öffentlich wird. Global wird sie durch die Installation in einem speziellen Verzeichnis: dem Global Assembly Cache (GAC). Der GAC ist ein Speicherort, an dem sogar mehrere unterschiedliche Versionen derselben Assembly installiert werden dürfen. Zu finden ist der GAC in folgendem Verzeichnis: \\assembly Der Windows Explorer lässt uns nicht in das Verzeichnis assembly »hineinschauen«. Sie können jedoch an der Eingabekonsole die Struktur des GAC erkunden. Die Bibliotheksnamen werden unter Berücksichtigung der Metadaten auf die Verzeichnisstruktur abgebildet. \assembly\GAC_MSIL\ \__\Bibliothek.dll
Zum Beispiel finden Sie die Bibliothek System.Drawing.dll der Version 2.0.0.0 mit dem Schlüssel b03f5f7f11d50a3a und keiner Kulturangabe (neutrale Kultur) in folgendem Verzeichnis C:\WINDOWS\assembly\GAC_MSIL\System.Drawing\2.0.0.0__b03f5f7f11d50a3a Mit den Informationen zu Dateiname, Version, Kultur und öffentlichem Schlüssel aus dem Manifest der Anwendung sucht die CLR die passende Assembly im GAC. Wird die CLR nicht fündig, geht sie von einer privaten Assembly aus und sucht im Verzeichnis der Anwendung. Konfigurationsdateien beeinflussen diesen Suchprozess (siehe unten). Hinweis Wegen der eindeutigen Identifizierbarkeit werden die Namen von Assemblies mit einem Schlüssel auch starke Namen genannt.
593
8.2
8
Anwendungen: Struktur und Installation
Versionierung von Assemblies Die Version einer Assembly besteht aus vier durch jeweils einen Punkt getrennten Zahlen: 왘
Hauptversion
왘
Nebenversion
왘
Build
왘
Revision
Versionen, die sich in einer der beiden ersten Zahlen unterscheiden, sind inkompatibel. Die beiden letzten Ziffern erfassen kompatible Korrekturen (siehe Abbildung 8.3). Hauptversion
Nebenversion
Inkompatible Änderung Abbildung 8.3
Build
Revision
abwärtskompatible Änderung
Schema der Versionierung
Das Manifest einer Anwendung spezifiziert die Versionen benötigter Assemblies. Findet die CLR zur Laufzeit diese nicht, weicht sie auf kompatible Versionen aus (gegebenenfalls einer anderen Kultur). Scheitert dies auch, kann die Assembly nicht geladen werden. Die Versionsnummer ist ein Attribut der Assembly und steht in der Datei AssemblyInfo.vb.
Sie tragen sie entweder dort ein oder in dem Dialog aus Abbildung 8.4, den Sie über die Schaltfläche Assemblyinformationen auf der Karteikarte Anwendung der Projekteigenschaften finden. Nach dem Schließen des Dialogs werden die Informationen in die Datei AssemblyInfo.vb eingetragen.
Abbildung 8.4
594
Festlegen der Assemblyversion
Assemblies
Schlüsseldatei erzeugen Globale Assemblies sind gekennzeichnet durch die Signierung mit einem binären Schlüsselpaar, das aus einem öffentlichen und einem privaten Schlüssel besteht. Nur der Besitzer des privaten Schlüssels kann eine neue »schlüsselrichtige« Version der Assembly erstellen. Beim Kompiliervorgang wird ein Teil des öffentlichen Schlüssels (Token) in das Manifest geschrieben und die Datei, die das Manifest enthält, mit dem privaten Schlüssel signiert. Nutzer der Assembly brauchen nur den öffentlichen Schlüssel, der private Schlüssel dient »nur« zur Authentifizierung. Er sichert Ihre Arbeit – verwahren Sie ihn gut. Öffentlicher und privater Schlüssel werden durch eine Schlüsseldatei beschrieben, die mit dem Tool sn.exe erzeugt wird. Sie können dieses Tool an der Kommandozeile aufrufen oder einfacher über die Auswahlbox Assembly signieren auf der Karteikarte Signierung der Projekteigenschaften. Über die darunterliegende Auswahlliste können Sie dann eine vorhandenen Schlüsseldatei wählen oder eine neue erstellen. Bei der Neuerstellung geben Sie einen Dateinamen an und können die Datei mit einem Kennwort schützen (siehe Abbildung 8.5).
Abbildung 8.5
Schlüsseldatei mit Visual Studio 2008 erzeugen
Beim Signieren einer Assembly haben Sie möglicherweise nicht immer sofort Zugriff auf den privaten Schlüssel. In diesem Fall nehmen Sie eine verzögerte Signierung vor, die zunächst nur den öffentlichen Schlüssel verfügbar macht. Markieren Sie hierzu den Optionsschalter Nur verzögerte Signierung. Das Hinzufügen des privaten Schlüssels wird auf den Zeitpunkt der Bereitstellung der Assembly verschoben.
595
8.2
8
Anwendungen: Struktur und Installation
Installation im GAC mit dem Tool gacutil.exe Nachdem nun eine Schlüsseldatei in die Assembly eingebunden ist, kann das Kompilat im GAC installiert werden. Dazu haben Sie zwei Möglichkeiten: 왘
das Kommandozeilenprogramm gacutil.exe des .NET Frameworks.
왘
eine Installationsroutine mit dem Micosoft Windows Installer
Fangen wir mit der ersten Variante an. Wie alle anderen Tools findet man gacutil.exe unter: \Programme\Microsoft SDKs\Windows\V6.0A\bin Die allgemeine Aufrufsyntax lautet: gacutil [Optionen] [Assemblyname]
Aus der Liste der Optionen ragen zwei besonders heraus: der Schalter /i, um die darauf folgend angegebene Assembly im GAC zu installieren, und der Schalter /u, um eine gemeinsam genutzte Assembly zu deinstallieren, zum Beispiel so: gacutil /i MyGlobalAssembly.dll gacutil /u MyGlobalAssembly
Installation im GAC mit einer Setup-Routine Visual Studio bietet die Projektvorlage Setup-Projekt an, die eine automatisch ablaufende Installationsroutine erzeugt. Die Projektdetails übergehe ich hier und lege den Fokus auf die Installation im GAC. Als Beispiel dienen eine winzige Bibliothek und eine Testanwendung. Die Projekteinstellungen der Bibliothek verwenden einen leeren Stamm-Namespace. '...\Applikation\GacBibliothek\GacKlasse.vb
Namespace GacTest Public Class GacKlasse Public Function Version() As String Return "Erste Version" End Function End Class End Namespace
'...\Applikation\GacAnwendung\Programm.vb
Module Programm Sub Main(ByVal args() As String) Dim g As New GacTest.GacKlasse() For no As Integer = 1 To args.Length Console.WriteLine("{0}: {1}", no, args(no – 1)) Next Console.WriteLine(g.Version())
596
Assemblies
Console.ReadLine() End Sub End Module
Wie beschrieben, ergänzen wir eine Schlüsseldatei, die im Projektmappen-Explorer zu sehen ist (siehe Abbildung 8.6).
Abbildung 8.6
Projektmappe mit Schlüsseldatei
Danach sollten Sie nicht vergessen, das Projekt noch einmal zu kompilieren. Der Projektmappe fügen Sie anschließend über ihren Kontextmenüpunkt Hinzufügen 폷 Neues Projekt ein Setup-Projekt hinzu (siehe Abbildung 8.7).
Abbildung 8.7
Auswahl des Setup-Projekts
Nach dem Klick auf OK wird im Codeeditor der Dateisystemeditor des Setup-Projekts angezeigt (siehe Abbildung 8.8). Er fasst unter anderem alle zu installierenden kompilierten Dateien zusammen.
Abbildung 8.8
Ansicht des Dateisystem-Editors
597
8.2
8
Anwendungen: Struktur und Installation
Im Kontextmenü des linken Knotens Anwendungsordner wählen Sie zuerst Hinzufügen und danach Datei. Sie wählen die kompilierte EXE-Datei der Anwendung GacAnwendung. Danach ist diese sowie die Bibliothek als benötigte Komponente im rechten Teil zu sehen. Im Kontextmenü des obersten Knoten Dateisystem auf Zielcomputer fügen Sie mit Speziellen Ordner hinzufügen das Verzeichnis Cacheordner für globale Assembly hinzu. Verschieben Sie die Bibliothek aus dem Anwendungsordner in diesen Ordner. Nach der Kompilation ist eine setup.exe-Datei im Ausgabeordner des Setup-Projekts. Installieren Sie nun durch deren Aufruf das Programm, oder nutzen Sie dazu den Punkt Installieren im Kontextmenü des Setup-Projekts (Sie brauchen Administratorrechte). Die Abbildung 8.9 zeigt, dass die Bibliothek im GAC ist.
Abbildung 8.9
Klassenbibliothek »GacBibliothek« im GAC
Der Vorteil des Global Assembly Caches ist, dass andere Anwendungen ebenfalls dieselbe zentral registrierte GacBibliothek nutzen können. Beim Starten von GacAnwendung wird ein GacKlasse-Objekt erzeugt und die Methode Version aufgerufen. Erwartungsgemäß wird »Erste Version« ausgegeben.
Neue Version der globalen Assembly installieren Angenommen, die erste Version von GacBibliothek sei bereits verteilt (unter anderem über GacAnwendung). Nun soll eine überarbeitete Klasse GacKlasse verteilt werden, hier simuliert durch die Rückgabe einer anderen Version: Public Function Version() As String Return "Zweite Version" End Function
Entweder Sie fügen einer neuen Projektmappe die beiden Projekte hinzu, oder Sie heben die installierte EXE-Datei zur späteren Verwendung auf. Nachdem Sie die Rückgabe geändert haben, setzen Sie die Version der Assembly GacBibliothek auf 2.0.0.0. Fügen Sie der Projektmappe ein neues Setup-Projekt hinzu, dem Sie nur GacBibliothek im Knoten Cacheordner für globale Assembly hinzufügen, nicht GacAnwendung. Auch die neue Setup-Routine muss ausgeführt werden, damit sich die zweite Version von GeometricsObject in den GAC einträgt (siehe Abbildung 8.10). Die bereits installierte GacAnwendung nutzt weiterhin die erste Version der Bibliothek. Damit sie die zweite Version nutzt, muss eine Konfigurationsdatei geändert werden.
598
Konfigurationsdateien
Abbildung 8.10
8.3
Zwei versionsverschiedene Einträge im GAC
Konfigurationsdateien
Die Common Language Runtime (CLR) wertet Konfigurationsdateien beim Start einer Anwendung aus. Während des Laufs können Sie über Programmcode darauf zugreifen. Sie ersetzen Einstellungen in der Registry oder in INI-Dateien. Der Inhalt dieser Dateien wird von den Anforderungen der Anwendung bestimmt, zum Beispiel Verbindungszeichenfolgen für ADO.NET und Angaben über die zu verwendende Version einer Bibliothek. Für Anwendungen auf der lokalen Maschine gibt es drei Arten Konfigurationsdateien: Anwendungskonfigurationsdateien, Herausgeberrichtliniendateien und Maschinenkonfigurationsdatei (machine.config). Unser Fokus liegt auf der ersten Art, die aus zwei Gruppen unterschiedlicher Daten besteht: 왘
Anwendungsdaten, die benutzerunabhängig für die gesamte Anwendung gültig sind
왘
Daten, die benutzerspezifisch sind
Konfigurationsdateien sind XML-Dateien. XML (Extended Markup Language) ist ein klar definierter Standard, der nicht dieselbe Lässigkeit wie HTML erlaubt. Zum Beispiel müssen Tags immer geschlossen werden, und es wird zwischen Groß- und Kleinschreibung unterschieden.
8.3.1
Die verschiedenen Konfigurationsdateien
Namen von Konfigurationsdateien enden typischerweise auf config. Im Wesentlichen gibt es drei Arten, die in folgender Reihenfolge nach Einstellungen durchsucht werden: 왘
Anwendungskonfigurationsdateien: Sie sind optional. Wenn sie existieren, dann immer im Stammverzeichnis der Anwendung, und sie leiten ihren Namen aus der Anwendung ab. (MyApp.exe hat die Konfigurationsdatei MyApp.exe.config.) Ihre Einstellungen wirken sich nur auf diese Anwendung aus.
왘
Publisherrichtliniendateien (Herausgeberrichtliniendateien): Sie sind ebenfalls optional und regeln die automatische Umleitung von Aufrufen einer älteren Version einer GACAssembly auf eine neuere.
왘
Maschinenkonfigurationsdatei: Standardmäßig unter \Windows\Microsoft .NET\Framework\v\Config\machine.config installiert, enthält die Maschinenkonfigurationsdatei viele .NET-weite Einstellungen. Die Datei ist erforderlich.
599
8.3
8
Anwendungen: Struktur und Installation
Bei der Suche nach einer gültigen Einstellung wird zuerst das Manifest ausgewertet. Danach werden die drei Konfigurationsdateien in der angegebenen Reihenfolge durchsucht. Jede darf die Einstellungen vorheriger überschreiben, nach dem Motto »Der Letzte gewinnt«. Durch einen Umweg kann dieser Mechanismus »ausgehebelt« werden. Zum Beispiel kann der Automatismus der Versionsumleitung in der Anwendungskonfigurationsdatei abgeschaltet werden. Wird die Abschaltung selbst nicht durch eine der folgenden Konfigurationsdateien wieder abgeschaltet (d. h. Umleitung ein), ist eine in folgenden Konfigurationsdateien geänderte Versionsumleitung wirkungslos (wegen der Abschaltung). Die Abschaltung ist zum Beispiel sinnvoll, wenn machine.config sonst eine inkompatible Version erzwingen würde.
8.3.2
Struktur einer Anwendungskonfigurationsdatei
Da Konfigurationsdateien reiner XML-Code sind, können Sie sie in einem beliebigen Editor erstellen. Über den Kontextmenüpunkt Hinzufügen... 폷 NeuesElement hinzufügen|Anwendungskonfigurationsdatei eines Projekts können Sie sich einen Rahmen erstellen lassen. Den gewählten Namen app.config dürfen Sie nicht ändern. Die Kompilierung ändert den Namen im Ausgabeverzeichnis automatisch. ...
Die XML-Deklaration in der ersten Zeile legt die Versionsnummer und den zum Lesen des Dokuments verwendeten Zeichensatz fest, hier UTF-8. Alle Einstellungen stehen innerhalb des fest vorgegebenen Wurzelelements . Anhand des folgenden (gekürzten) Beispiels möchte ich ein paar Einstellungen ansprechen. Hallo Morph!
600
Konfigurationsdateien
Hallo Sprache!
Innerhalb von sind vier Sektionen definiert: 왘
: Sie fasst die beiden untergeordneten Sektionen und zusammen.
왘
: Diese Sektion enthält benutzerunabhängige Daten. Sie entspre-
chen einer öffentlich deklarierten Eigenschaft, die mit ReadOnly schreibgeschützt ist. Durch die Änderung eines Wertes in der Konfigurationsdatei wird ein Variablenwert ohne Neukompilierung geändert. 왘
: Hier werden benutzereigene Einstellungen abgelegt. Sie dürfen im Code
geändert werden. Die neuen Werte werden in der Datei Dokumente und Einstellungen\ \Lokale Einstellungen\Anwendungsdaten\\\\\ user.config gespeichert. 왘
: Der Abschnitt erfüllt die gleiche Aufgabe wie die Sektion , kann jedoch aus dem Code der laufenden Anwendung heraus editiert werden.
Die Notation der beiden Abschnitte und ist identisch. Jedem Element entspricht eine Klasse, im Beispiel die Klasse MySettings im Namensraum GacAnwendung.My. Die Namen werden von Visual Studio automatisch erzeugt, wenn Sie eine Eigenschaft auf der Karteikarte Einstellungen der Projekteigenschaften eintragen. Die untergeordneten -Elemente beschreiben mit dem Attribut name den Bezeichner der gespeicherten Variablen, und serializeAs gibt den Datentyp an. Meist werden die Daten als Zeichenfolge serialisiert, ansonsten sind auch noch xml, binary und custom mögliche Alternativen. Das Tag schließlich beschreibt den gespeicherten Wert.
8.3.3
Anwendungskonfigurationsdatei automatisch erstellen
Wenn Sie auf der Karteikarte Einstellungen in den Projekteinstellungen Werte eintragen, wird gegebenenfalls eine Anwendungskonfigurationsdatei app.config erzeugt und entsprechend den von Ihnen gemachten Einträgen befüllt. In der Spalte Name tragen Sie den Namen der Variablen ein, und unter Typ legen Sie deren Datentyp fest. Die Dropdown-Liste Bereich legt fest, ob die Einstellung anwendungsweit gilt oder benutzerspezifisch ist. In der letzten Spalte können Sie einen Startwert angeben. Abbildung 8.11 zeigt die Einstellungen für die weiter oben gezeigte Anwendungskonfigurationsdatei.
601
8.3
8
Anwendungen: Struktur und Installation
Abbildung 8.11
Festlegen der Konfigurationseinstellungen im Visual Studio
Möchten Sie noch weitere Sektionen festlegen, beispielsweise , öffnen Sie app.config und nehmen die Ergänzung manuell vor. IntelliSense unterstützt Sie dabei.
8.3.4
Anwendungskonfiguarionsdatei auswerten
Das folgende Beispiel setzt die Bibliothek System.Configuration.dll voraus und wertet die Anwendungskonfiguarionsdatei aus. Die drei Sektionen , und werden ausgelesen, und den Variablen in und werden neue Werte zugewiesen. '...\Applikation\Einstellungen\Einstellungen.vb
Imports System.Configuration Imports System.Collections.Specialized Namespace Einstellunegn Module Einstellungen Sub Main() ' und auswerten Dim setting As New My.MySettings() Console.WriteLine("Variable1 = {0}", setting.Variable1) Console.WriteLine("Variable2 = {0}", setting.Variable2) ' auswerten Dim col As NameValueCollection = ConfigurationManager.AppSettings For i As Integer = 0 To col.Count – 1 Console.WriteLine("Name: {0} – Wert: {1}", col.Keys(i), col(i)) Next ' neuen Wert zuordnen setting.Variable1 = "Bonjour langue!" setting.Save()
602
Konfigurationsdateien
' einen anderen Wert zuweisen col("Test") = "Aachen" Console.WriteLine("Test (neu): {0}", col("Test")) Console.ReadLine() End Sub End Module End Namespace
Die Datei Settings.Designer.vb im Knoten My Project im Projektmappenexplorer definiert die Klasse MySettings, die von System.Configuration.ApplicationSettingsBase abgeleitet ist. Die Datei wird automatisch von Visual Studio generiert. Die Klasse ist das Bindeglied zur Konfigurationsdatei und definiert pro Einstellung eine Instanzeigenschaft. Anwendungsweit gültige Instanzeigenschaften sind mit ReadOnly schreibgeschützt. Schauen wir uns nun die beiden folgenden Anweisungen an: setting.Variable1 = "Hallo Peter!"; setting.Save();
Variable1 gehört zu der Sektion . Während beim ersten Aufruf noch der
Inhalt Hallo Morph! lautet, wird dieser nach der Konsolenausgabe geändert. Das bewirkt, dass die Zeichenfolge Bonjour langue! in die Datei user.config geschrieben wird, die unter Dokumente und Einstellungen\\Lokale Einstellungen\Anwendungsdaten\\ \ zu finden ist. Der Inhalt lautet: Bonjour langue!
Bei allen folgenden Aufrufen wird anschließend immer die benutzerbezogene Zeichenfolge angezeigt. Noch eine Bemerkung zur Klasse MySettings: Wenn Sie auf die Schaltfläche Code anzeigen auf der Karteikarte Einstellungen der Projekteigenschaften klicken, wird die partielle Klasse mit ein paar Kommentaren angezeigt. Sie weisen auf die Möglichkeit hin, Ereignishandler zu registrieren, die beim Laden, Ändern und Speichern der Eigenschaften ausgelöst werden.
Configuration und ConfigurationManager Die beiden Klassen im Namensraum System.Configuration (benötigt Verweis auf System.Configuration.dll) dienen dazu, Konfigurationsdateien auszuwerten und zu ändern. Die erste Klasse gibt es seit .NET 2.0, die zweite seit 1.0.
603
8.3
8
Anwendungen: Struktur und Installation
Über die ConfigurationManager-Klasse können Sie auf Maschinen- und Anwendungskonfigurationsinformationen zugreifen (siehe Tabelle 8.2). Die beiden einzigen Eigenschaften ermöglichen den Zugriff auf die Abschnitte und . Eigenschaft
Zugriff auf Abschnitt der Konfigurationsdatei
AppSettings
der Standardkonfiguration
ConnectionStrings
der Standardkonfiguration
Tabelle 8.2
Eigenschaften der Klasse »ConfigurationManager«
Tabelle 8.3 zeigt die wichtigsten Methoden der Klasse. Methode
Beschreibung
GetSection
Liefert den angegebenen Konfigurationsabschnitt der Standardkonfiguration.
OpenExeConfiguration
Öffnet die angegebene Datei als Configuration-Objekt.
OpenMachineConfiguration
Öffnet die Computerkonfiguration als Configuration-Objekt.
RefreshSection
Aktualisiert den benannten Abschnitt.
Tabelle 8.3
Methoden der Klasse »ConfigurationManager«
Configuration repräsentiert die Einstellungen einer Anwendung oder des Computers. Die
Werte berücksichtigen die Überschreibung von Anwendungseinstellungen durch machine.config. Die Klasse hat keine öffentlichen Konstruktoren, und die Fabrikmethoden OpenExeConfiguration und OpenMachineConfiguration der Klasse ConfigurationManager liefern Instanzen der Klasse Configuration. Mit seinen Eigenschaften und Methoden greifen Sie auf Sektionen bzw. Sektionsgruppen, den physikalischen Pfad der Konfigurationsdatei und Einstellungen zu. In Tabelle 8.4 und Tabelle 8.5 sind die wichtigsten Eigenschaften und Methoden aufgeführt. Eigenschaft
Beschreibung
AppSettings
Konfigurationsabschnitt des AppSettingsSection-Objekts
ConnectionStrings
ConnectionStringsSection-Konfigurationsabschnittsobjekt
FilePath
Physikalischer Pfad zur Konfigurationsdatei
Locations
Die in diesem Configuration-Objekts definierten Speicherorte
RootSectionGroup
Die Stamm-ConfigurationSectionGroup
SectionGroups
Auflistung der in der Konfiguration definierten Abschnittsgruppen
Sections
Auflistung der von dieser Konfiguration definierten Abschnitte
Tabelle 8.4
Schreibgeschützte Eigenschaften der Klasse »Configuration«
Methode
Beschreibung
GetSection
Gibt das angegebene ConfigurationSection-Objekt zurück.
GetSectionGroup
Ruft das angegebene ConfigurationSectionGroup-Objekt ab.
Tabelle 8.5
604
Methoden der Klasse »Configuration«
Konfigurationsdateien
Methode
Beschreibung
Save
Sichert die Konfiguration als XML in die aktuelle Datei.
SaveAs
Sichert die Konfiguration als XML in die angegebene Datei.
Tabelle 8.5
Methoden der Klasse »Configuration« (Forts.)
Wie Sie die Klassen Configuration und ConfigurationManager einsetzen können, zeige ich Ihnen im folgenden Abschnitt.
8.3.5
Editierbare, anwendungsbezogene Einträge mit
Anwendungsweite Einstellungen können Sie ändern, wenn sie im Abschnitt stehen. Das folgende Beispiel zeigt, wie Configuration und ConfigurationManager auf diesen Abschnitt zugreift. Einträge, die Sie in der Anwendungskonfigurationsdatei im Abschnitt vornehmen, sind nicht editierbar – zumindest aus dem Code einer Anwendung heraus. Man kann die Werte jedoch jederzeit ändern, indem man die Datei mit einem beliebigen Editor öffnet. Das ist insofern sinnvoll, als dass nicht ein Benutzer von den Änderungen eines anderen Benutzers abhängig gemacht wird. Nichtsdestotrotz könnten Sie als Entwickler auch einmal in die Situation kommen, aus dem Code heraus eine anwendungsweite Einstellung ändern zu wollen oder eine neue hinzuzufügen. Das kann nur in der Sektion erfolgen. Das folgende Beispiel AppSettingsDemo zeigt, wie Sie mit den Klassen Configuration und ConfigurationManager die Einstellungen in beeinflussen können. '...\Applikation\AnwendungsEinstellungen\AnwendungsEinstellungen.vb
Imports System.Configuration Imports System.Collections.Specialized Namespace Applikation Module AnwendungsEinstellungen Sub ShowAppSettings() Dim appStgs As NameValueCollection = ConfigurationManager.AppSettings For i As Integer = 0 To appStgs.Count – 1 Console.WriteLine("Nr. {0} – Wert: {1}", i, appStgs(i)) Next End Sub Sub Main() Console.WriteLine("Ursprüngliche 'appSettings'-Werte:") ShowAppSettings() Console.WriteLine(Environment.NewLine & "Hinzufügen eines Eintrags:") ' Bezeichner des Eintrags festlegen Dim entry As String = "Nr" & ConfigurationManager.AppSettings.Count ' 'appSettings'-Eintrag hinzufügen
605
8.3
8
Anwendungen: Struktur und Installation
Dim cfg As Configuration = ConfigurationManager. _ OpenExeConfiguration(ConfigurationUserLevel.None) cfg.AppSettings.Settings.Add(entry, Now.ToLongTimeString()) ' Ändern des ersten Eintrags If cfg.AppSettings.Settings.Count > 2 Then cfg.AppSettings.Settings( _ "Nr" & (cfg.AppSettings.Settings.Count – 3)).Value = "veraltet" ' Speichern aller Änderungen cfg.Save(ConfigurationSaveMode.Modified) ' Erneutes Auslesen der Sektion 'appSettings' ConfigurationManager.RefreshSection("appSettings") Console.WriteLine(Environment.NewLine & "Neue 'appSettings'-Werte:") ShowAppSettings() ' Anzeige des letzten Eintrags in der Konfigurationsdatei Console.WriteLine(Environment.NewLine & _ "Letzter Eintrag: {0}", ConfigurationManager.AppSettings(entry)) Console.ReadLine() End Sub End Module End Namespace
Beim Start aus der Entwicklungsumgebung heraus wird App.config vor jedem Start restauriert, sodass Sie den Effekt der Sicherung nicht sehen können. Die folgende Ausgabe ergibt sich beim vierten Start innerhalb einer Eingabeaufforderung: Ursprüngliche Nr. 0 – Wert: Nr. 1 – Wert: Nr. 2 – Wert:
'appSettings'-Werte: veraltet 21:44:21 21:44:25
Hinzufügen eines Eintrags: Neue 'appSettings'-Werte: Nr. 0 – Wert: veraltet Nr. 1 – Wert: veraltet Nr. 2 – Wert: 21:44:25 Nr. 3 – Wert: 21:46:06 Letzter Eintrag: 21:46:06
Bei jedem Start wird ein zusätzlicher Eintrag mit der Uhrzeit in die Anwendungskonfigurationsdatei geschrieben. Alle Einträge außer den beiden letzten werden als »veraltet« gekennzeichnet«. Die Methode ShowAppSettings durchläuft in einer Schleife die in ConfigurationManager.AppSettings gespeicherten Einstellungen. Mit OpenExeConfiguration holen wir uns eine Referenz auf die Einstellungen. Deren Auflistung AppSettings.Settings wird mit Add ein neuer Eintrag hinzugefügt und mit Save gespeichert. Die Enumeration ConfigurationSaveMode beschreibt mit Full, Minimal und Modified den zu speichernden Umfang.
606
Konfigurationsdateien
8.3.6
Versionsumleitung in einer Konfigurationsdatei
In Abschnitt 8.2.2, »Globale Assemblies», haben wir dem GAC eine zweite Version der Bibliothek GacBibliothek hinzugefügt. Nun werden wir mittels einer Konfiguration Aufrufe auf die zweite Version umleiten, auch für die bereits erstellte Anwendung GacAnwendung. Dazu erstellen wir im Verzeichnis der Anwendung eine Textdatei namens GacAnwendung.exe.config mit folgendem Inhalt:
Wenn Sie GacAnwendung starten, zeigt die Ausgabe Zweite Version, dass die Umleitung funktioniert hat – ohne Kompilierung. Das Element bindingRedirect spezifiziert die Umleitung. Den öffentlichen Schlüssel entnehmen Sie zum Beispiel der Anzeige des GAC (\assembly) im Windows Explorer. Jede Bibliothek steht in einem eigenen dependentAssembly-Abschnitt, der außerdem im Element codebase den Speicherort der Bibliothek angeben kann (außerhalb des GAC). Der übergeordnete Abschnitt assemblyBinding kann neben der hier gezeigten Bindung auch noch mit probing und qualifyAssembly die Runtime bei der Suche nach Assemblies unterstützen. Der Abschnitt runtime schließlich kann außerdem Sicherheitsrichtlinien, den Suchpfad für Assemblies und den Garbage Collector beeinflussen. Was passiert, wenn die Versionsangaben nicht exakt passen? Eine Spezifikation mit einer nicht existierenden alten Version wird schlicht ignoriert, und bei einer falschen neuen Version wird eine Ausnahme FileLoadException ausgelöst. Die Angabe der alten Version kann auch einen ganzen Bereich umfassen, zum Beispiel:
Alternativ zur manuellen Erstellung gibt es auch eine grafische Oberfläche zur Konfiguration namens Microsoft .NET Framework 2.0-Konfiguration. Sie finden sie im Dialog Verwaltung der Systemsteuerung, die Sie über das Startmenü erreichen. Dort fügen Sie GacAnwendung.exe dem Knoten Arbeitsplatz/Anwendungen hinzu. In dem neu erstellten Knoten markieren Sie den Knoten Verwaltete konfigurierte Assemblys und klicken auf die Schaltfläche Assembly konfigurieren im rechten Fensterteil. Im dann erscheinenden Dialog wählen Sie eine der beiden Versionen von GacBibliothek und bestätigen den Dialog. Im folgenden Dialog aktivieren Sie die Bindungsrichtlinie auf der Karteikarte Allgemein und tragen auf
607
8.3
8
Anwendungen: Struktur und Installation
der Karteikarte Bindungsrichtlinie die Versionsumleitung ein – hier von der angeforderten Version 1.0.0.0 zur neuen Version 2.0.0.0. Hinweis Das Verwaltungstool ist seit 2008 nicht mehr Teil von Visual Studio und muss extra installiert werden.
8.3.7
Die Herausgeberrichtliniendatei
Da die Herausgeberrichtliniendatei nach der Anwendungskonfigurationsdatei ausgewertet wird (siehe Abschnitt 8.3.1, »Die verschiedenen Konfigurationsdateien«), kann sie deren Einträge überschreiben, zum Beispiel um Versionsumleitungen zu forcieren. Sie wird in kompilierter Form im GAC installiert. Die Kompilierung übernimmt das Kommandozeilentool Assembly Linker al.exe im folgenden Verzeichnis: ...\Programme\Microsoft SDKs\Windows\ v6.0A\Bin Dem Tool wird der Name der Publisherrichtliniendatei, also der XML-Datei, angegeben, der Name der Herausgeberrichtlinienassembly und die Schlüsseldatei der Bibliothek, die Sie beeinflussen wollen. al /link: /out: /keyfile:
Das Namensformat der Ausgabedatei ist festgelegt: policy...
Für unser Beispiel kann das Kommando lauten: al /link:neu.config /out:policy.1.0.GacBibliothek.dll /keyfile:TestSchlüssel.snk
Die Herausgeberrichtlinienassembly muss zusammen mit der Assembly im GAC installiert werden. Dazu können Sie wieder das Tool gacutil mit dem Schalter /i benutzen, zum Beispiel: gacutil /i policy.1.0.GacBibliothek.dll
Alternativ erstellen Sie mit einem Setup-Projekt einen Installer (siehe Abschnitt 8.2.2, »Globale Assemblies»).
8.4
Weitergabe mit MS-Installer
Für lokale .NET-Anwendungen, die ausschließlich mit ihrem eigenen Verzeichnis auskommen, reicht für eine Installation ein einfacher Kopierbefehl. Insbesondere sind weder Systemdateien noch die Registrierungsdatenbank betroffen. Haben Sie nur solche Anwendungen, können Sie diesen Abschnitt getrost überspringen. Wollen Sie dem Benutzer aber eine grafi-
608
Weitergabe mit MS-Installer
sche Installationsoberfläche bieten oder außer der eigentlichen Anwendung noch andere Teile des Computers beeinflussen, brauchen Sie einen Installer. In Visual Studio wählen Sie dazu im Projektauswahldialog innerhalb des Knotens Setup und Bereitstellung eine Projektvorlage.
8.4.1
Weitergabeprojekte
Visual Studio 2008 stellt Ihnen sechs verschiedene Weitergabeprojekttypen zur Verfügung: 왘
Setup-Projekt
왘
Websetup-Projekt
왘
Merge-Modulprojekt
왘
CAB-Projekt
왘
CAB-Projekt für intelligente Geräte
왘
Setup-Assistent (weniger Möglichkeiten, aber später änderbar)
Websetup-Projekt und Setup-Projekt Diese beiden Typen unterscheiden sich darin, woher das Installationsprogramm die zu installierenden Dateien nimmt. Bei einem Setup-Projekt ist es das Dateisystem auf einem Rechner, bei einem Websetup-Projekt ein virtuelles Verzeichnis auf einem Webserver. Merge-Modulprojekte Ein Merge-Modulprojekt ist kein Setup-Projekt im eigentlichen Sinne und kann daher auch nicht eigenständig installiert werden. Ein Merge-Modul wird in andere Weitergabeprojekte integriert und erleichtert damit nur das Erstellen mehrerer Installationsprogramme, die identische Komponenten beinhalten. Merge-Module lassen sich daher besser mit DLL-Dateien vergleichen, denen eine ähnliche Aufgabe zukommt. CAB-Projekte CAB-Dateien beinhalten komprimierte Dateien, die zu einem leicht zu verteilenden Paket zusammengeschnürt werden. Eine CAB-Datei kann wie ein Merge-Modul in anderen Projekten verwendet werden, eignet sich aber auch zur Weitergabe von Dateien über das Internet. Hinweis Wir werden uns in diesem Kapitel nur mit dem Setup-Projekt beschäftigen.
8.4.2
Windows-Installer
Bei den Dateien, die bei der Kompilierung eines Setup-Projekts erzeugt werden, handelt es sich im Wesentlichen nur um setup.exe und eine komprimierte Datei mit der Dateierweiterung .MSI. Die Datei setup.exe hat die Aufgabe zu überprüfen, ob auf dem Zielrechner der Windows Installer installiert ist. Falls er nicht installiert ist, wird in Abhängigkeit vom Betriebssystem entweder die Datei InstMsiW.exe oder die Datei InstMsiW.exe installiert (die
609
8.4
8
Anwendungen: Struktur und Installation
Situation ist unwahrscheinlich, weil .NET ihn bereits zur Installation gebraucht hat). Erst danach kann die MSI-Datei installiert werden, die alle Dateien und Informationen zum Installationsprozess enthält.
8.4.3
Setup-Projekt
Zuerst erstellen wir ein Setup-Projekt. Da der Projektbezeichner den Namen der MSI-Datei bestimmt, sollten Sie einen aussagekräftigen Namen wählen (siehe Abbildung 8.12).
Abbildung 8.12
Setup-Projekt
Außer zu Testzwecken werden Sie keine Debuginformation mit ausliefern wollen. Sie sollten daher zuerst eine Release-Version des Projekts kompilieren. Die Umschaltung dazu erfolgt in der Symbolleiste der Entwicklungsumgebung. Im linken Fensterteil fügen Sie die zu installierende Anwendung dem Anwendungsordner hinzu, benutzte Bibliotheken werden in der Regel automatisch mit eingebunden. In Abbildung 8.13 wurde GacAnwendung aus Abschnitt 8.2.2, »Globale Assemblies«, hinzugefügt.
Abbildung 8.13
Setup-Anwendung
Kompilieren des Weitergabeprojekts Sind alle Einstellungen vorgenommen (einschließlich der, die wir noch erörtern werden), muss das Projekt nur noch kompiliert werden. Die vorgenommenen Einstellungen werden in einer Datei mit der Erweiterung VDPROJ gespeichert. Es ist eine reine Textdatei, die allerdings nicht verändert werden sollte. Vorausgesetzt, die Release-Konfiguration wurde erstellt, befinden sich die zu verteilenden Dateien im Unterordner Release des Setup-Projekts. Es handelt sich dabei um die Datei setup.exe, mit der die Installation gestartet wird, sowie um eine MSI-Datei mit den komprimierten Dateien des Projekts. 610
Weitergabe mit MS-Installer
Hinweis Sie können die Installation bzw. Deinstallation über den Kontextmenüeintrag In/Deinstallieren des Weitergabeprojekts ausführen.
8.4.4 Editoren eines Weitergabeprojekts Mit den Basiseinstellungen des Setup-Projekts wird der Benutzer willkommen geheißen. Dann wird ihm ein Vorschlag für das Installationsverzeichnis gemacht, der Fortschritt der Installation wird durch einen Fortschrittsbalken angezeigt, und am Ende wird die hoffentlich erfolgreiche Installation gemeldet. Ein Installationsprogramm kann auch zusätzliche Ordner auf dem Zielrechner anlegen. Es lassen sich Bedingungen zur Installation bestimmter Dateien festlegen, Sie können zusätzliche Dialoge einbinden, Dateitypen können registriert werden usw. Ausgangspunkt sind die in Abbildung 8.14 gezeigten Weitergabeeditoren am oberen Rand des Weitergabeprojekts im Projektmappen-Explorers.
Abbildung 8.14
Symbolleiste eines Weitergabeprojekts im Projektmappen-Explorer
Von links nach rechts stehen in der Symbolleiste: 왘
Eigenschaften: Dialogfenster zur Festlegung allgemeiner Verteilungsbedingungen.
왘
Dateisystem-Editor: Hier werden die weiterzugebenden Dateien angegeben sowie die Ordner auf dem Zielrechner, in denen die Dateien installiert werden.
왘
Registrierungs-Editor: Hier legen Sie Registrierungsschlüssel und -werte für den Zielrechner fest. Nach erfolgreicher Installation kann ein Programm darauf zugreifen.
왘
Dateityp-Editor: Hier verknüpfen Sie die Anwendung mit Dateierweiterungen. Darüber hinaus werden die für den Dateityp zulässigen Aktionen festgelegt.
왘
Benutzeroberflächen-Editor: Hier fügen Sie dem Installationsvorgang zusätzliche Dialoge hinzu. Dieser Editor bietet die größte Einflussmöglichkeit auf den Installationsprozess.
왘
Editor für benutzerdefinierte Aktionen: Aktionen, die während der Installationsphase auf dem Zielrechner ausgeführt werden, zum Beispiel wenn ein Fehler auftritt oder im Falle einer Deinstallation.
왘
Editor für Startbedingungen: Aktionen, die beim Fehlen von Dateien auf dem Zielrechner vor der Installation ausgeführt werden. Ein Beispiel ist die Nachinstallation von .NET.
Hinweis Jeder Editor schreibt seine Bezeichnung in den Karteireiter des Fensters.
611
8.4
8
Anwendungen: Struktur und Installation
8.4.5
Dateisystem-Editor
Der Dateisystem-Editor ist nach dem Start eines neuen Setup-Projekts geöffnet. Er enthält alle Dateien, die vom Installationsprogramm benötigt werden, beispielsweise die Symboldateien. Die Ordner im linken Fenster repräsentieren die Ordner auf dem Rechner, auf den installiert wird. Weitere Ordner können Sie über das Kontextmenü eines der Fensterteile hinzufügen (die grauen in Abbildung 8.15 existieren schon). Dadurch können Sie beispielsweise eine Anwendung in das Startmenü eintragen oder einen Link auf dem Desktop des Benutzers setzen.
Abbildung 8.15
Spezielle Ordner
Wenn Sie einen Ordnerknoten markieren, werden im Eigenschaftsfenster dessen Eigenschaften angezeigt. Tabelle 8.6 listet die wichtigsten auf. Eigenschaft
Beschreibung
AlwaysCreate
Bei True wird der Ordner auch angelegt, wenn er keine Dateien enthält.
Condition
Bedingung, die erfüllt sein muss, damit der Ordner installiert wird.
DefaultLocation
Standardinstallationsverzeichnis auf dem Zielrechner.
Transitive
Bei True wird bei jeder Installation der Anwendung die Condition-Eigenschaft ausgewertet, andernfalls nur bei der ersten Installation.
Tabelle 8.6
Eigenschaften eines Dateisystemordners
Die Eigenschaft DefaultLocation hat folgenden Wert: [ProgramFilesFolder][Manufacturer]\[ProductName]
612
Weitergabe mit MS-Installer
Für den Firmennamen Tollsoft und den Produktnamen MyFirstApp wird der Installationsordner C:\Programme\Tollsoft\MyFirstApp angeboten. Standardmäßig ist der Produktname der Name des Projekts. Sie können sowohl den Produkt- als auch den Firmennamen in den Projekteigenschaften festlegen. Hinweis Viele der Eigenschaften des Setup-Projekts finden Sie in den Eigenschaften der MSI-Datei im Windows-Explorer wieder.
Die Spezifikation von DefaultLocation kann spezielle Ordnerreferenzen in eckigen Klammern enthalten. Alle erlaubten finden Sie in der MSDN-Library unter dem Stichwort Windows Installer unter Properties. Tabelle 8.7 gibt einen Auszug wieder. Eigenschaft
Verzeichnis
AppDataFolder
Anwendungsdaten des aktuellen Benutzers
CommonAppDataFolder
Anwendungsdaten für alle Benutzer
CommonFilesFolder
\Programme\Gemeinsame Dateien des aktuellen Benutzers
DesktopFolder
Desktop
FavoritesFolder
Favoriten des aktuellen Benutzers
MyPicturesFolder
Eigene Bilder des aktuellen Benutzers
ProgramFilesFolder
Programme
SendToFolder
SendTo des aktuellen Benutzers
StartMenuFolder
Startmenü des aktuellen Benutzers
SystemFolder
Sytem32
TempFolder
Temp
TemplateFolder
Vorlagen des aktuellen Benutzers
WindowsFolder
Winnt
Tabelle 8.7
Pfadbezogene Eigenschaften im Windows Installer
Hinzufügen von Dateien und Ordnern Zu installierende Komponenten fügen Sie über die Untermenüpunkte Ordner, Projektausgabe, Datei und Assembly des Kontextmenüs Anwendungsordner 폷 Hinzufügen hinzu. Die Entwicklungsumgebung fügt nicht nur die ausgewählte Programmdatei in das Weitergabeprojekt ein, sondern erkennt darüber hinaus auch alle Abhängigkeiten, sowohl die Abhängigkeit vom .NET Framework als auch die Abhängigkeit von benutzerdefinierten Bibliotheken (DLL-Dateien), die automatisch in das Projekt einbezogen werden.
613
8.4
8
Anwendungen: Struktur und Installation
Dateizugriff vom Programmcode Wenn Sie davon ausgehen müssen, dass eine bestimmte Datei nicht auf dem Zielrechner in einem bestimmten Verzeichnis vorgefunden wird, müssen Sie die Datei in die Installationsroutine mit aufnehmen. Dazu eignet sich prinzipiell jedes Verzeichnis, das auf dem Zielrechner identifiziert werden kann. Möchten Sie die Datei im Anwendungsordner verteilen, fügen Sie die Datei dem gleichnamigen Knoten im Dateisystem-Editor hinzu. Startmenü und Desktop-Icon Im linken Teilfenster des Dateisystem-Editors werden Programmmenü- und Desktop-Ordner bereits angeboten. Sie müssen jeweils nur noch eine Verknüpfung zur Programmdatei einrichten. Dazu bieten sich zwei Wege an, die für beide Knoten identisch sind: 왘
Wählen Sie den Kontextmenüpunkt Neue Verknüpfung erstellen im rechten Fenster des Knotens, und wählen Sie im erscheinenden Dialog die Anwendungsdatei.
왘
Wählen Sie den Kontextmenüpunkt Verknüpfung erstellen zu... der Anwendungsdatei, und verschieben Sie den erzeugten Link in den entsprechenden Knoten.
Durch das Hinzufügen von Unterordnern können Sie den Link zur Anwendungsdatei in eine geeignete Untermenüstruktur verschieben. Sie sollten nicht vergessen, die automatisch generierten Verknüpfungsnamen umzubenennen. Um einer Verknüpfung ein individuelles Symbol zuzuordnen, markieren Sie die Verknüpfung, klicken im Eigenschaftsfenster auf Icon und wählen aus der Dropdown-Liste die Option Durchsuchen aus. Im folgenden Dialog Symbol navigieren Sie zu der Symboldatei mit der Endung ICO, die Sie vorher dem Dateisystem-Editor an beliebiger Stelle hinzugefügt haben. Desktop-Icons sind 32×32 Pixel groß, das Programmmenü braucht 16×16 Pixel. Da die Symboldateien zur Liste der zu verteilenden Dateien hinzugefügt wurden, werden sie normalerweise auf dem Zielrechner installiert. Um das zu vermeiden, können Sie die Eigenschaft Exclude dieser Dateien auf True einstellen. Die Folge ist, dass die Dateien dann nicht installiert werden, wohl aber bei der Installation zur Einrichtung verwendet werden. Sie werden nicht mehr im Dateisystem-Editor angezeigt, sondern im Projektmappenexplorer mit einem Verbotszeichen markiert. Hinweis Im Projektordner der Anwendung befindet sich bei Windows-Anwendungen die Symboldatei App.ico. Diese dient als Symbol, wenn im Windows-Explorer die Ansicht Große Symbole ausgewählt ist.
Cacheordner für globale Assembly Eine besondere Bedeutung kommt dem Cacheordner für globale Assembly zu. Dateien, die Sie diesem Ordner hinzufügen, müssen einen starken Namen haben und werden automatisch im GAC (Global Assembly Cache) eingetragen. Intern wird demnach bei der Installation jede hier eingetragene Datei mit dem gacutil-Tool registriert.
614
Weitergabe mit MS-Installer
Globale Assemblys, die Sie diesem Ordner hinzufügen, müssen nicht zwangsläufig auch unter Anwendungsordner aufgeführt sein. Verzichten Sie darauf, wird die Assembly nur im GAC eingetragen, kann aber nicht von anderen Anwendungen benutzt werden, denn über Verweise können in der Entwicklungsumgebung nur DLL-Dateien eingebunden werden. Möchten Sie die Dienste auch anderen Anwendungen zur Verfügung stellen, müssen Sie die Assemblierung deswegen zusätzlich in einem anderen Ordner eintragen. Meistens wird das auch der Anwendungsordner oder eines seiner Unterverzeichnisse sein. Hinweis Verteilen Sie zusammen mit der globalen Assembly eine Publisher-Richtlinien-Assembly, muss diese auch im Cacheordner für globale Assembly angegeben werden.
8.4.6 Der Registrierungs-Editor Die Registrierungsdatenbank ist die zentrale interne Datenbank von Windows. Da sie unabhängig von der zu installierenden Anwendung ist, können zum Beispiel dort Informationen abgelegt werden, die beim ersten Start der Anwendung bereits benötigt werden. Ich persönlich mag sie nicht, denn sie macht Anwendungen unflexibel und undurchschaubar für andere. Zum Beispiel können Sie eine installierte Anwendung nicht mal eben auf ein anderes Laufwerk verschieben und weiterarbeiten. Oder versuchen Sie mal herauszubekommen, welche Anwendungen automatisch beim Systemstart auch starten (etwa 30 Schlüssel). Schließlich können andere fehlerhafte Anwendungen aus Versehen Ihre Schlüssel ändern. Im Registrierungs-Editor werden die Registrierungsschlüssel angezeigt, die den Standardregistrierungsschlüsseln von Windows entsprechen: 왘
HKEY_CLASSES_ROOT
왘
HKEY_CURRENT_USER
왘
HKEY_LOCAL_MACHINE
왘
HKEY_USERS
왘
Benutzer/Computer-Hive
Die unter Benutzer/Computer-Hive eingegebenen Unterschlüssel und Werte werden im Knoten HKEY_CURRENT_USER installiert, wenn ein Benutzer bei der Installation die Option Aktueller Benutzer wählt. Wenn ein Benutzer bei der Installation Alle Benutzer auswählt, werden die Angaben im Schlüssel HKEY_USERS eingetragen. Anmerkung Die Auswahl Aktueller Benutzer bzw. Alle Benutzer erfolgt in dem Dialog der Installation, in dem der Benutzer den Installationsordner bestätigt oder neu angibt.
Einen neuen Unterschlüssel legen Sie über das Kontextmenü eines Knotens an; beliebig tiefe Gliederungsstrukturen sind erlaubt. Soll zu einem Schlüssel ein Wert definiert werden, müs-
615
8.4
8
Anwendungen: Struktur und Installation
sen Sie zuerst den Datentyp festlegen. Es kann sich dabei um eine Zeichenfolge, einen Umgebungs-Zeichenfolgewert, einen Binärwert oder einen DWORD-Wert handeln. Anschließend wird dem Wert ein passender Name zugewiesen. Das kann sowohl im rechten Teilfenster des Registrierungs-Editors erfolgen als auch im Eigenschaftsfenster unter Value. In Abbildung 8.16 ist im Registrierungs-Editor ein Schlüssel eingetragen, der TestKey heißt. Der Wert ist vom Typ String und beschreibt eine zugewiesene Zeichenfolge, die zumindest einmal nach der Installation angezeigt werden soll. Hinweis Jeder Schlüssel darf genau einen Standardwert haben. Sie legen ihn fest, indem Sie dem Wert einen leeren Namen geben (Kontextmenü), der als (Standard) angezeigt wird.
Abbildung 8.16
Der Registrierungs-Editor mit einem zusätzlichen Schlüssel
Die Eigenschaft DeleteAtUninstall eines Schlüssels scheint keinen Effekt zu zeigen, da die Schlüsseleinträge einer Anwendung nach der Deinstallation immer gelöscht werden.
8.4.7
Dateityp-Editor
Dieser Editor legt das Doppelklickverhalten und Kontextmenüeinträge für Dateien gegebener Endungen fest. Über das Kontextmenü des Knotens Dateitypen auf dem Zielcomputer fügen Sie eine oder mehrere Dateitypen hinzu. Sie erscheinen im Dateityp-Editor als untergeordnete Knoten, denen Sie passende Namen geben sollten. Standardmäßig ist mit &Öffnen jedem Knoten sofort eine Aktion zugeordnet.
Eigenschaften eines Dateityps Der Knoten eines zu verknüpfenden Dateityps hat mehrere Eigenschaften (siehe Tabelle 8.8). Eigenschaft
Beschreibung
Command
Ausführbare Datei, die bei einer Aktion mit diesem Dateityp gestartet wird
Description
Anzeige in der Spalte Typ in der Detailansicht des Windows Explorers
Extensions
Durch Semikolon getrennte zu registrierende Dateierweiterungen ohne *
Tabelle 8.8
616
Eigenschaften eines registrierten Dateityps
Weitergabe mit MS-Installer
Eigenschaft
Beschreibung
Icon
Symbol, das für die Dateien dieses Typs angezeigt werden soll
MIME
Zuzuordnende MIME-Typen (Multipurpose Internet Mail Extensions helfen Webbrowser und E-Mail bei der Verarbeitung binärer Daten.)
Name
Der im Dateityp-Editor verwendete Name
Tabelle 8.8
Eigenschaften eines registrierten Dateityps (Forts.)
Nachdem Sie der Eigenschaft Extensions eine oder auch mehrere durch Semikolon getrennte Dateierweiterungen zugeordnet haben, legen Sie die mit den Erweiterungen verbundene öffnet einen Auswahldialog). Sie Anwendung in der Eigenschaft Command fest (der Button wird bei allen noch festzulegenden Aktionen aufgerufen. Meistens ist es eine EXE-Datei, die unter Anwendungsordner zu finden ist. Sie sollten zur besseren (professionelleren) Identifikation für den Dateityp mit der IconEigenschaft ein individuelles Symbol festlegen. Die dazugehörige ICO-Datei muss ebenfalls im Dateisystem-Editor dem Weitergabeprojekt hinzugefügt worden sein. Zuletzt tragen Sie unter Description noch eine Zeichenfolge ein, die in der Detailansicht des Windows Explorers den Benutzern eine informative Beschreibung des Dateityps anzeigt.
Aktionen Jeder neue Dateitypeintrag hat standardmäßig die Aktion &Öffnen. Als Standardaktion ist sie fett geschrieben und wird durch einen Doppelklick ausgelöst. Hinweis Nur die oberste Position eines Dateitypknotens ist die Standardaktion. Sie können Aktionen durch Drag&Drop verschieben.
Das Kontextmenü einer Datei im Windows Explorer zeigt alle der für den Dateityp spezifizierten Aktionen mit dem in der Eigenschaft Name angegebenen Namen. Das & vor einem Buchstaben kennzeichnet ein Tastenkürzel für den schnelleren Zugriff. Eine typische Aktion ist das Drucken eines Dokuments. Hinweis Alle Aktionen starten die unter Command angegebene Anwendung mit den unter Arguments angegebenen Argumenten. "%x" ist das x-te Argument, "%1" der Pfad zur Anwendung.
Nehmen wir als Beispiel die hier verwendete GacAnwendung. Sie hat eine Main-Methode, die die übergebenen Argumente als Zeichenkettenarray entgegennimmt und ausdruckt. Zur Erinnerung sehen Sie hier noch einmal den Quelltext:
617
8.4
8
Anwendungen: Struktur und Installation
'...\Applikation\GacAnwendung\Programm.vb
Module Programm Sub Main(args() As String) Dim g As New GacTest.GacKlasse() For no As Integer = 1 To args.Length Console.WriteLine("{0}: {1}", no, args(no – 1)) Next Console.WriteLine(g.Version()) Console.ReadLine() End Sub End Module
Abbildung 8.17 zeigt die beiden Aktionen Öffnen und Report, die beide mit den Dateiendungen .xxx und .zzz verknüpft sind.
Abbildung 8.17
Aktionen im Dateityp-Editor
Im Eigenschaftsfenster sind für die zweite Aktion spezielle Argumente eingetragen.
Abbildung 8.18
Aktionenargumente
Nach der Installation finden Sie die Aktionen im Kontextmenü.
Abbildung 8.19
618
Kontextmenü der installierten Anwendung
Weitergabe mit MS-Installer
Die gezeigte Aktion führt zur Ausgabe der Argumente (siehe das Listing etwas weiter oben). 1: Pfad 2: C:\TestInstall\MeinDocument.xxx Zweite Version
Wenn Sie im Programm GacAnwendung die übergebenen Argumente in Kontrollstrukturen auswerten, können Sie beliebige Reaktionen der Aktion implementieren.
8.4.8 Benutzeroberflächen-Editor Dieser Editor legt die Abfolge der bei der Installation gezeigten Dialoge fest. Der Knoten Installation definiert die Installation auf der lokalen Maschine, der Knoten Administratorinstallation die Installation im Netzwerk. Die Dialoge der in Abbildung 8.20 gezeigten Standardinstallation sind nur wenig beeinflussbar. Durch eigene Dialoge können Sie aber die volle Kontrolle erlangen. Auch eine automatische Installation ohne alle Dialoge ist erlaubt. Da die Dialoge immer nur als Standard-Icon angezeigt werden, empfiehlt sich eine Testinstallation.
Abbildung 8.20
Der Benutzeroberflächen-Editor
Eigenschaften der Standard-Installationsdialoge Die Eigenschaften in Tabelle 8.9 sind nicht in allen dieser Dialoge zu finden. Eigenschaft
Beschreibung
BannerBitmap
Bitmap- oder JPEG-Grafikdatei, die im Dialog angezeigt wird
CopyrightWarning
Text für einen Copyright-Vermerk im Dialog Willkommen
ShowProgressBar
Gibt an, ob der Fortschrittsbalken im Dialog Status angezeigt werden soll.
UpdateText
Text, der im Dialog Fertig angezeigt werden soll
WelcomeText
Text, der im Dialog Willkommen angezeigt werden soll
Tabelle 8.9
Eigenschaften der Standard-Installationsdialoge
Die Dialoge zeigen am oberen Rand in einem Bereich von 500×70 Pixel eine Hintergrundgrafik an, deren Pfad die Eigenschaft BannerBitmap speichert. Die im Dateisystem-Editor hinzugefügte BMP- oder JPEG-Grafikdatei wird weder gestreckt noch gestaucht. Es scheint keine Möglichkeit zu geben, die Beschriftung durch eine andere zu ersetzen.
619
8.4
8
Anwendungen: Struktur und Installation
Die im Dialog Willkommen angezeigte Warnung vor Urheberrechtsverletzung in der Eigenschaft CopyrightWarning sollten Sie anpassen, denn »US-amerikanische Urheberrechtsgesetze« scheinen mir in Europa unpassend. Die Begrüßung in WelcomeText ist allgemeiner: Der Installer wird Sie durch die zur Installation von [ProductName] erforderlichen Schritte führen.
Den Fortschrittsbalken im Dialog Status können Sie mit der Eigenschaft ShowProgressBar ausschalten. Im letzten Standarddialog Fertig werden dem Benutzen in UpdateText Hinweise gegeben. Der Standardtext bezieht sich auf das .NET Rahmenwerk: Prüfen Sie mit Windows Update, ob wichtige Aktualisierungen für .NET Framework zur Verfügung stehen.
Weitere Dialoge einfügen In jede der drei Gruppen Starten, Status und Beenden können Sie nach Bedarf weitere Dialoge einfügen. Jeder Dialog darf nur einmal in einem Weitergabeprojekt verwendet werden. Die Dialoge sind keine normalen Fenster. Sie haben eine begrenzte Auswahl zur Verfügung: 왘
Der Splash-Dialog zeigt eine Begrüßungsbitmap an.
왘
Der Button Benutzer registrieren im gleichnamigen Dialog ruft eine beliebige ausführbare Datei auf.
왘
Im Dialog Kundeninformationen gibt der Anwender seinen Namen, den Namen der Firma und optional einer Seriennummer ein.
왘
Den Lizenzvertrag im Dialog Lizenzvertrag muss der Benutzer lesen und bestätigen.
왘
Der Dialog Infodatei zeigt einen Text an.
왘
je ein Dialog mit zwei, drei oder vier Optionsschaltflächen
왘
drei Dialoge mit bis zu vier Kontrollkästchen
왘
drei Dialoge mit bis zu vier Textfeldern
Dialoge fügen Sie über die Kontextmenüpunkte Dialogfeld hinzufügen der Knoten Installation und Administratorinstallation hinzu.
Der Dialog Splash Die Eigenschaft SplashBitmap spezifiziert eine Bitmap oder JPEG-Datei für den 480×320 Pixel großen Anzeigebereich des Bildes, das weder gestreckt noch gestaucht wird. Mit der Voreinstellung Sunken=True wird die Grafik innerhalb des Rahmens abgesenkt dargestellt. Zur Fortsetzung muss der Anwender selbst auf eine Weiter-Schaltfläche klicken. Der Dialog Benutzer registrieren Der Dialog zeigt eine Schaltfläche Jetzt registrieren, die eine beliebige von Ihnen unter Executable angegebene ausführbare Datei aufruft. Zusätzliche Befehlszeilenargumente tragen Sie in der Eigenschaft Arguments ein. Abbildung 8.21 zeigt einen Ausschnitt mit dem festen Text (der Wert SetupDemo von ProductName wird automatisch eingesetzt) und der Schaltfläche.
620
Weitergabe mit MS-Installer
Abbildung 8.21
Benutzer registrieren
Der Dialog Kundeninformationen Der Dialog fordert vom Benutzer die Eingabe seines Namens, seiner Firma und optional der Seriennummer (siehe Abbildung 8.22).
Abbildung 8.22
Der Dialog »Kundeninformationen«
Neben BannerBitmap besitzt dieser Dialog die drei in Tabelle 8.10 gezeigten speziellen Eigenschaften. Eigenschaft
Beschreibung
SerialNumberTemplate
Vorlage zur Überprüfung der eingegebenen Seriennummer
ShowOrganization
Gibt an, ob das Feld Organisation angezeigt wird.
ShowSerialNumber
Gibt an, ob das Feld Seriennummer angezeigt wird.
Tabelle 8.10
Eigenschaften des Dialogs »Kundeninformationen«
Wird vom Anwender die Angabe einer Seriennummer verlangt, muss er sich an das in der Eigenschaft SerialNumberTemplate gegebene Muster halten. Der Windows Installer addiert
621
8.4
8
Anwendungen: Struktur und Installation
die an den Platzhalterstellen # stehenden Zahlen (siehe Tabelle 8.11). Ist die Summe nicht durch sieben teilbar, wird ein Meldungsfenster angezeigt, und die Installation kann nicht fortgesetzt werden. Platzhalter
Beschreibung
#
Eine Zahl, die nicht vom Überprüfungsalgorithmus erfasst wird
%
Eine Zahl, die vom Überprüfungsalgorithmus erfasst wird
?
Ein vom Überprüfungsalgorithmus nicht erfasstes alphanumerisches Zeichen
^
Ein Zeichen in Großschreibung oder eine vom Überprüfungsalgorithmus nicht erfasste Zahl
-
Kennzeichnung, dass ein neues Eingabefeld beginnt (selbst keine Eingabe)
Tabelle 8.11
Platzhalter der Eigenschaft SerialNumberTemplate
Die Schablone SerialNumberTemplate steht in spitzen Klammern, zum Beispiel:
Bei der Installation werden zwei Eingabefelder angezeigt: In das erste muss der Anwender drei Zahlen eingeben, in das zweite sieben. Die Summe aus der ersten, vierten, fünften, siebten und zehnten Zahl muss durch sieben teilbar sein. Der Mechanismus ist eine einfache Hürde gegen unrechtmäßige Installationen. Nur die beiden Platzhalterkombinationen »^« mit »?« und »#« mit »%« werden gemeinsam in einem Eingabefeld angezeigt – es sei denn, mit einem Bindestrich werden separate Eingabefelder erzwungen. Alle anderen Kombinationen führen dazu, dass zwischen den Platzhaltern intern ein Bindestrich gesetzt wird.
Der Dialog Lizenzvertrag Dieser Dialog zeigt einen Lizenzvertrag, den der Benutzer lesen (sollte) und bestätigen muss. Die Weiter-Schaltfläche wird erst dann aktiviert, wenn der Benutzer auf die Optionsschaltfläche Ich stimme zu klickt (siehe den Ausschnitt in Abbildung 8.23). Die Lizenzinformationen sind in einer RTF-Datei gespeichert, die natürlich auch im Dateisystem-Editor hinzugefügt werden muss. Ein RTF-Dokument können Sie beispielsweise mit WordPad erstellen. In der Eigenschaft LicenseFile des Dialogs geben Sie die Datei an. Außer bei der Eigenschaft BannerBitmap können Sie mit der Eigenschaft Sunken den Inhalt der Lizenzinformationen in einem abgesenkten Rahmen darstellen.
Der Dialog Infodatei Es gibt nur zwei Unterschiede zum Dialog Lizenzvertrag. Statt in LicenseFile steht der Dateipfad in ReadmeFile. Außerdem fehlt eine die Weiter-Schaltfläche blockierende Optionsschaltfläche Ich stimme zu.
622
Weitergabe mit MS-Installer
Abbildung 8.23
Der Dialog »Lizenzvertrag«
Dialoge mit Optionsschaltflächen Die Dialoge mit zwei, drei oder vier sich ausschließenden Auswahlmöglichkeiten dürfen je maximal einmal verwendet werden. Mit diesem Dialog können Sie beispielsweise den Anwender eine Sprache wählen lassen. Die Auswahl des Anwenders wird während des Installationsprozesses ausgewertet und berücksichtigt. Abbildung 8.24 zeigt die Eigenschaften des Dialogs.
Abbildung 8.24
Eigenschaften von Optionsschaltflächen
Für Abbildung 8.25 wurde BannerText der Wert Lingua gegeben und BodyText der Text Choisissez une langue s.v.p. zugewiesen. Als Beschriftung der Auswahlmöglichkeiten habe ich English für Button1Label und Deutsch für Button2Label gewählt. Button1Value ist der Wert, den die erste Optionsschaltfläche zurückliefert, wenn sie ausgewählt ist. Entsprechend ist es Button2Value für die zweite. Die Eigenschaft ButtonProperty benennt die Schaltflächengruppe. Der Name muss eindeutig bezüglich der gesamten Installation sein. Die letzte Eigenschaft ist DefaultValue. Sie selektiert die Optionsschaltfläche vor, die den angegebenen Wert in ihrer Value–Eigenschaft hat.
623
8.4
8
Anwendungen: Struktur und Installation
Hinweis Inkonsistenzen, zum Beispiel identische Werte für Optionsschaltflächen, fallen erst während der Installation auf.
Abbildung 8.25
Dialog mit zwei Optionsschaltflächen
Auf die Auswahl können Sie in der Condition-Eigenschaft von zu installierenden Komponenten zugreifen, indem Sie den in der Eigenschaft ButtonProperty festgelegten Namen verwenden. Ihm wird automatisch der zur Auswahl korrespondierende Wert zugewiesen. Nur wenn die Condition-Eigenschaft leer oder ihr Wert True ist, wird die Komponente installiert. Dateien, Ordner, Registrierungseinträge sowie benutzerdefinierte Aktionen und Startbedingungen haben eine Condition-Eigenschaft. Hat in unserem Beispiel die Datei Lizenz.rtf unter Condition den Wert LANGUAGE=13 eingetragen und wurde Deutsch gewählt, wird die Datei installiert.
Dialoge mit Kontrollkästchen Diese drei identischen Dialoge haben bis zu vier Kontrollkästchen, die Sie einzeln mit Visible=False ausblenden können. Jedes Kästchen kann die Werte Checked oder Unchecked annehmen und wird in der Condition-Eigenschaft von zu installierenden Komponenten unter dem Property-Namen angesprochen, zum Beispiel als CHECKBOXA3 (entspricht LANGUAGE des vorigen Abschnitts). Ansonsten sind die Eigenschaften und die Nutzung ganz analog zu den im letzten Abschnitt beschriebenen Optionsschaltflächen. Hinweis Achten Sie wegen des identischen Aufbaus der drei Dialoge auf eine eindeutige Benennung der Kästchen (die Standardnamen gewährleisten dies bereits).
Dialoge mit Textfeldern Diese drei Dialoge zeigen bis zu vier Textfelder für individuelle Benutzereingaben. Die Beschriftungen stehen in je einer Label-Eigenschaft, die Vorbelegung steht in einer Value624
Weitergabe mit MS-Installer
Eigenschaft, und der Name, der in Condition-Eigenschaften verwendet wird, steht in einer Property -Eigenschaft. Hinweis Achten Sie wegen des identischen Aufbaus der drei Dialoge auf eine eindeutige Benennung der Textfelder (die Standardnamen gewährleisten dies bereits).
8.4.9 Editor für benutzerdefinierte Aktionen Der Editor hat vier Bereiche: Installieren, Commit ausführen, Rollback und Deinstallieren (siehe Abbildung 8.26). Jedem dieser Bereiche kann über sein Kontextmenü eine benutzerdefinierte Aktion zugeteilt werden – also nichts anderes als eine Datei, die ausgeführt wird (EXE, DLL oder Skript). Die Dateien müssen natürlich zum Weitergabeprojekt gehören.
Abbildung 8.26
Editor für benutzerdefinierte Aktionen
Der Knoten Installieren enthält die Aktionen, die am Ende der Installationsphase ausgeführt werden sollen, also wenn alle anderen Dateien bereits installiert worden sind. Während der Installation kann es zu Fehlern kommen. Wollen Sie in dieser Situation bestimmte Operationen automatisch ausführen lassen, tragen Sie diese unter dem Knoten Rollback ein. Nach einer erfolgreichen Installation werden alle Aktionen ausgeführt, die unter dem Knoten Commit ausführen eingetragen sind. Benutzerdefinierte Aktionen im Knoten Deinstallieren werden aufgerufen, wenn eine Anwendung deinstalliert wird.
8.4.10 Editor für Startbedingungen Ob eine Anwendung überhaupt installiert werden kann beziehungsweise in welchem Umfang eine Installation erfolgt, kann mit dem Editor für Startbedingungen festgelegt werden. So können Sie zum Beispiel nach einer bestimmten Datei suchen, nach Registrierungseinträgen oder auch nach der Information, ob auf dem Zielcomputer das passende .NET Framework installiert ist. Die Einstellungen im Editor für Startbedingungen werden vor Beginn der Installation überprüft. Ist das Ergebnis negativ, kann das Installationsprogramm zwei Dinge tun: 왘
Die Installation wird mit einer Fehlermeldung abgebrochen.
왘
Die Installation wird fortgesetzt. Möglicherweise werden dabei auch die Voraussetzungen dafür geschaffen, dass die Anwendung später problemlos ausgeführt werden kann.
625
8.4
8
Anwendungen: Struktur und Installation
Die Oberfläche des Editors Unterhalb des Stammknotens Anforderungen für den Zielcomputer sind mit Zielcomputer durchsuchen und Startbedingungen bereits zwei untergeordnete Knoten eingetragen. Letzterer sucht auf dem Zielrechner nach dem .NET Framework und kann nicht gelöscht werden. Das Kontextmenü des ersten Kindknotens hat die drei in Abbildung 8.27 gezeigten Startkonditionen.
Abbildung 8.27
Der Editor für Startbedingungen
Bedingungen Alle unter dem Knoten Startbedingungen angegebenen Bedingungen werden der Reihe nach ausgewertet. Jede der Bedingungen hat drei Eigenschaften: 왘
Condition: ein boolescher Ausdruck, der das Ergebnis der Bedingung formuliert
왘
InstallUrl: die lokale Adresse oder Internetadresse, die bei einem Klick auf die Schaltfläche Ja des »gescheitert«-Dialogs an den Windows Explorer geschickt wird
왘
Message: Text im »gescheitert«-Dialog Der Dialog hat die Schaltflächen Ja und Nein.
Die erste Bedingung, deren Condition-Eigenschaft den Wert False ergibt, führt zum Abbruch und zeigt einen Dialog mit den Schaltflächen Ja und Nein, dem Produktnamen in der Titelleiste und dem Text der Message-Eigenschaft. Die gesamte Installation findet nicht statt. In der Condition-Eigenschaft können Sie Werte von Optionsschaltflächen, Kontrollkästchen oder der nun folgenden Zielcomputersuche verwenden. Diese muss nicht für die Startbedingungen verwendet werden, sondern kann auch die Installation einzelner Komponenten steuern. Zum Beispiel unterdrückt Condition = NOT FILEEXISTS1 die Installation einer Komponente, wenn die Zielcomputersuche namens FILEEXISTS1 den Wert False ergibt (die Datei also bereits existiert). Hinweis Condition-Eigenschaften dürfen boolesche Operatoren wie NOT, AND, OR und = enthalten. Die Installation wird abgebrochen, wenn der Wert False ist.
.NET Framework-Startbedingung Die wichtigste Voraussetzung zur Installation und Ausführung eines .NET-Programms ist, dass das .NET Framework auf dem Zielcomputer vorhanden ist – natürlich in der richtigen Version. Die Common Language Runtime sowie die .NET Framework-Komponenten sind in der Datei dotnetfx.exe enthalten, die es in mehreren Sprachversionen gibt. 626
ClickOnce-Verteilung
Der Editor für Startbedingungen fügt automatisch eine entsprechende unlöschbare Startbedingung hinzu. Die für die Anwendung erforderliche Version des .NET Frameworks tragen Sie in der Eigenschaft Version ein. Mit AllowLaterVersions können Sie höhere Versionen des .NET Frameworks als die angegebene akzeptieren.
Suche nach einer Datei Nach der Auswahl im Kontextmenü benennen Sie das neue Element und selektieren es. Im Eigenschaftsfenster legen Sie mit Folder und FileName den Dateipfad fest. Die Eigenschaft MinDate, MinSize und MinVersion sowie die korrespondierenden Max-Varianten schränken die Suche zusätzlich ein. Das Suchergebnis ist True oder False und kann in der ConditionEigenschaft einer zu installierenden Komponente unter dem Namen angesprochen werden, der in der Property-Eigenschaft festgelegt ist, zum Beispiel FILEEXISTS1. Suche nach Registrierungseinträgen Elemente, die nach einem Eintrag in der Registry suchen, haben fünf Eigenschaften: 왘
Name: Bezeichnung der Suche
왘
Property: Name in Condition-Eigenschaften
왘
RegKey: Schlüsselpfad (relativ zu Root und ohne Value)
왘
Root: Wurzelknoten (vsdrrHKLM, vsdrrHKCU, vsdrrHKCR, vsdrrHKU)
왘
Value: Schlüsselwert
Das Programm regedit.exe zeigt die Registrierungsdatenbank in einer halbwegs lesbaren Form. Wenn Sie hier unbekümmert agieren, können Sie sich Ihr gesamtes System unwiederbringlich zerstören.
Suche nach einer Windows-Installer-Startbedingung Die Formulierung einer Windows-Installer-Startbedingung dient zur Suche nach einer Komponente, die in der Registrierungsdatenbank mit einer GUID (Globally Unique Identifier) eingetragen ist. Die GUID, eine 128-Bit-Zahl, die als Zeichenfolge beschrieben wird, muss in der Eigenschaft ComponentID eingetragen werden. Ansonsten unterscheiden sich die Suche nach der Komponente und die Auswertung des Suchergebnisses nicht von der Suche nach einer Datei oder eines Registrierungseintrags.
8.5
ClickOnce-Verteilung
Die Installation einer Anwendung mit ClickOnce statt mit dem im letzten Abschnitt besprochenen Windows Installer hat einige besondere Eigenschaften: 왘
Die zu installierende Komponente liegt im Netzwerk oder auf einer CD-ROM.
왘
Komponenten auf einem Webserver werden immer in einem verschlüsselten Pfad im Benutzerprofil des aktuell angemeldeten Anwenders installiert (Dokumente und Einstellungen\\Lokale Einstellungen\Apps).
627
8.5
8
Anwendungen: Struktur und Installation
왘
Mit ClickOnce verteilte Anwendung nehmen weder Einträge in der Registrierungsdatenbank noch unter Desktop vor.
왘
Eine ClickOnce-Anwendung stellt selbst fest, ob eine Aktualisierung erforderlich ist, und lädt nur die neueren Teile herunter. Anschließend wird die vollständige aktualisierte Anwendung von einem neuen parallelen Ordner aus neu installiert.
왘
ClickOnce-Anwendungen im Online-Modus sind nicht im Startmenü und werden über einen Link gestartet.
Der Kern der neuen Architektur beruht auf zwei XML-Manifestdateien, die automatisch von Visual Studio erzeugt werden: 왘
Anwendungsmanifest: Anwendung (inklusive Assemblies) und der Speicherort für Updates
왘
Bereitstellungsmanifest: Art der Bereitstellung (inklusive Speicherort des Anwendungsmanifests sowie der Version der Anwendung)
Das Ziel von ClickOnce ist, die Verteilung von Anwendungen zentral zu verwalten und zu vereinfachen. Aber es gibt auch Einschränkungen, die schon in der Planungsphase zu berücksichtigen sind. Während der Einrichtung einer ClickOnce-Anwendung sind keine Operationen erlaubt, die administrative Rechte voraussetzen. Dazu gehören der Zugriff auf das Dateisystem und der Zugriff auf die Registrierungsdatenbank. Genauso wenig können Assemblies in den Global Assembly Cache (GAC) eingetragen oder Windows-Dienste eingerichtet werden.
8.5.1
Erstellen einer ClickOnce-Anwendung
Die Karteikarte Veröffentlichen der Projekteinstellungen ist die zentrale Stelle zur Konfiguration der Einstellungen, die beim Veröffentlichen auf dem Server zur Verfügung stehen. Abbildung 8.28 zeigt die Karteikarte.
Abbildung 8.28
628
ClickOnce-Optionen einer Windows-Anwendung
ClickOnce-Verteilung
Entscheidend ist zunächst einmal der Ort, an dem die ClickOnce-Komponente abgelegt wird. Als Veröffentlichungsort ist standardmäßig ein virtuelles Verzeichnis auf dem lokalen Webserver eingetragen. Über die Schaltfläche ellipsebutton können Sie zu einem der folgenden erlaubten Orte navigieren, an den die Anwendung kopiert werden soll: 왘
einen Dateipfad
왘
auf den lokalen Webserver (IIS, siehe Abbildung 8.29)
왘
auf einen FTP-Server
왘
auf eine entfernte Website, die mit den FrontPage-Servererweiterungen konfiguriert ist
Abbildung 8.29
Veröffentlichungsort
Fällt die Wahl auf den Offline-Modus, werden für die Anwendung ein Startmenüeintrag und ein Eintrag in den Systemeinstellungen unter Software hinzugefügt. Hierüber kann der Anwender später unter Umständen die Anwendung auch wieder deinstallieren. Beachten Sie, dass Sie im Offline-Modus über die dann aktivierte Schaltfläche Updates… auch das Aktualisieren beeinflussen können. Den Dialog sehen Sie in Abbildung 8.30. Vorgegeben ist, dass die Anwendung nach Updates suchen soll. Braucht Ihre Software keine automatischen Updates, können Sie diese Option deaktivieren. Andernfalls konfigurieren Sie die Häufigkeit der Update-Prüfung.
Abbildung 8.30
Konfiguration der Update-Suche
629
8.5
8
Anwendungen: Struktur und Installation
Ein Klick auf die Schaltfläche Anwendungsdateien… in der Registerkarte Veröffentlichen öffnet einen Dialog, in dem Sie angeben, welche Dateien auf den Server kopiert werden sollen. Ist eine benutzerdefinierte Klassenbibliothek unter Verweise eingebunden, wird die DLL automatisch mit in den Verteilungsprozess einbezogen. Im Dialog Erforderliche Komponenten… wählen Sie die Komponenten aus, die ebenfalls auf den Server kopiert werden sollen, wie zum Beispiel das .NET Framework. Zuletzt können Sie noch unter Optionen… diverse Einstellungen vornehmen, die mehr allgemeiner Natur sind. Die Anwendung soll hier auf den lokalen Webserver kopiert werden, und die Offline-Ausführung der ClickOnce-Anwendung soll ausgewählt sein. Auf dem Webserver wird ein virtuelles Verzeichnis angelegt, das standardmäßig wie das Projekt heißt. Das Verzeichnis enthält neben dem Bereitstellungsmanifest (das ist die Datei mit der Erweiterung .APPLICATION) auch eine setup.exe-Datei, die die Installation startet, sowie die Datei publish.htm, die der Anwender aufruft, um die Anwendung zu installieren. Zudem wird der Anwendung ein Unterverzeichnis hinzugefügt, dessen Name sich aus dem Namen der Anwendung plus einer vierstelligen Versionsnummer (mit Unterstrichen statt Punkten) ergibt. Dieses Verzeichnis enthält die tatsächlichen Anwendungsdaten und das Anwendungsmanifest mit der Erweiterung .MANIFEST. Nun folgt ein Klick auf den Button Jetzt veröffentlichen – das war’s.
8.5.2
Die Installation einer ClickOnce-Anwendung
Zur Installation öffnen wir im Webbrowser die Datei publish.htm. Die Webseite enthält eine Schaltfläche, über die die Anwendung im lokalen Cache eingerichtet wird (siehe Abbildung 8.31). Zuvor müssen Sie jedoch die Installation der Anwendung bestätigen, da der Herausgeber als nicht vertrauenswürdig eingestuft wird – obwohl Sie in diesem Fall selbst der Herausgeber sind. Anschließend wird das Programm gestartet.
Abbildung 8.31
630
Webseite zur Installation
ClickOnce-Verteilung
Da die Anwendung für den Offline-Modus eingerichtet worden ist, können Sie diese nachfolgend über das Startmenü starten. Dazu ist keine Verbindung zum Webserver notwendig. Zum Test können Sie im Internetdienste-Manager den Webserver anhalten. Es wird zwar versucht, Kontakt zum Webserver aufzunehmen, aber das Starten der Anwendung ist nicht davon abhängig. Hätten Sie sich für den Online-Modus bei der Kompilierung der Anwendung entschieden, wäre ein Aufruf von publish.htm notwendig gewesen. Diese steht aber nur dann zur Verfügung, wenn der Webserver seine Dienste ausführt. Veröffentlichen Sie die Anwendung erneut, wird automatisch die Versionsnummer erhöht – falls Sie auf der Registerkarte Veröffentlichen des Projekteigenschaftsfensters die Zahlen nicht geändert haben Auf dem Webserver wird ein zweites Unterverzeichnis für die neue Version angelegt, und im Bereitstellungsmanifest wird die Umleitung darauf eingetragen. Wenn Sie die Anwendung über das Startmenü aufrufen, sucht der Client nach eventuellen Updates. Ist der Webserver in Betrieb, wird die neue Version erkannt, geladen und ausgeführt. Der Anwender braucht in diesem Fall in keiner Weise einzugreifen oder selbst für die Neuinstallation der Anwendung zu sorgen. Der ClickOnce-Prozess übernimmt das vollkommen automatisch.
631
8.5
Das Testen eines Programms ist unumgänglich. Dieses Kapitel stellt Protokollierung und Debugging vor. Am Ende werden noch Klassen grafisch visualisiert, ohne den Kontakt zum Code zu verlieren.
9
Code erstellen und debuggen
9.1
Ausnahmen
Bereits in Abschnitt 2.8, »Fehlerbehandlung«, habe ich Ihnen die Wirkungsweise der strukturierten Fehlerbehandlung mit Try/Catch/Finally gezeigt. In diesem Abschnitt beschränke ich mich auf einige fehlende Aspekte. Hinweis Leider kann im Programmcode weder getestet werden, ob eine Methode eine Ausnahme auslösen könnte, noch muss deklariert werden, ob bzw. wie mit einer möglichen Ausnahme umzugehen ist. Der Compiler hilft Ihnen in diesem Punkt nicht weiter.
9.1.1
Methodenaufrufe
In »älteren« Programmiersprachen gibt es keine gesonderte Fehlerbehandlung. Meist wird ein Fehler durch einen speziellen Rückgabewert einer Funktion signalisiert. Das hat den enormen Nachteil, dass der Aufrufer nicht gezwungen ist, sich mit dem Fehler auseinanderzusetzen – er darf ihn ignorieren. In .NET dagegen werden Fehler durch das Auslösen einer Ausnahme signalisiert, die schlicht zum Abbruch eines Programms führt, wenn sie nicht behandelt wird. Damit dieser Zwang durchgesetzt werden kann, sind Ausnahmen unabhängig von dem, was Sie programmieren. Insbesondere funktionieren sie auch bei beliebig tief verschachtelten Methodenaufrufen. Nicht Sie, sondern die Laufzeitumgebung kümmert sich um den geordneten Ausstieg, bis eine Stelle erreicht ist, an der ein Fehler in einem Catch-Zweig behandelt wird. Damit geht einher, dass Sie Fehler an einer beliebigen, geeigneten Stelle in der Aufrufhierarchie der Methoden behandeln können: direkt beim Aufruf, in derselben Methode, in der übergeordneten Methode oder bei einem Aufrufer der Methode. Bezüglich dieser Transparenz gegenüber dem Aufrufstack gibt es eine Besonderheit zu beachten. In Abschnitt 2.8.4, »Try/Catch/Finally«, haben wir uns mit dem Finally-Zweig beschäftigt. Er wird sowohl bei Fehlerfreiheit als auch im Fehlerfall ausgeführt. Wie weit das geht, zeigt das folgende Beispiel. Der erste Aufruf von Kehrwert läuft fehlerfrei, im zweiten wird im Catch-Zweig ein Return ausgeführt.
633
9
Code erstellen und debuggen
'...\Lauf\Ausnahmen\ReturnFinally.vb
Option Strict On Namespace Lauf Module ReturnFinally Function Kehrwert(ByVal nenner As Integer) As Double Try Return 1 / nenner Catch ex As Exception Return Double.NaN Finally Console.WriteLine("Arbeit beendet.") End Try End Function Sub Test() Console.WriteLine("1/5={0}", Kehrwert(5)) Console.WriteLine("1/0={0}", Kehrwert(0)) Console.ReadLine() End Sub End Module End Namespace
Die Ausgabe zeigt, dass mit und ohne Return der Finally-Zweig immer ausgeführt wird. Arbeit beendet. 1/5=0,2 Arbeit beendet. 1/0=+unendlich
Damit ist der Finally-Zweig der richtige Ort für Anweisungen, die immer – unabhängig vom Erfolg – ausgeführt werden müssen.
9.1.2
Hierarchie der Ausnahmen
Bei der großen Anzahl an Ausnahmeklassen im .NET Framework – ich habe 636 gezählt – ist eine Organisation unumgänglich. Daher sind auch diese Klassen in einer Vererbungshierarchie organisiert, von der ich hier exemplarisch einen Ausschnitt mit den wichtigsten aus dem Namensraum System und allen aus dem Namensraum System.IO zeige. Object ÀException ÃApplicationException ÀSystemException ÃAccessViolationException ÃArgumentException ³ ÃArgumentNullException ³ ÃArgumentOutOfRangeException ³ ÀDuplicateWaitObjectException ÃArithmeticException ³ ÃDivideByZeroException
634
Ausnahmen
³ ÃNotFiniteNumberException ³ ÀOverflowException ÃFormatException ³ ÃUriFormatException ³ ÀIO.FileFormatException ÃIndexOutOfRangeException ÃInvalidCastException ÃInvalidOperationException ³ ÀObjectDisposedException ÃNotImplementedException ÃNotSupportedException ³ ÀPlatformNotSupportedException ÃNullReferenceException ÃOutOfMemoryException ³ ÀInsufficientMemoryException ÃRankException ÃStackOverflowException ÃTypeInitializationException ÃIO.InternalBufferOverflowException ÃIO.InvalidDataException ÀIO.IOException ÃIO.DirectoryNotFoundException ÃIO.DriveNotFoundException ÃIO.EndOfStreamException ÃIO.FileLoadException ÃIO.FileNotFoundException ÃIO.PathTooLongException ÀIO.PipeException
Die Hierarchie wirkt sich an zwei Stellen besonders aus. Zum Ersten wird eine Referenz einer Ausnahme auf eine ihrer Basisklassen als allgemeiner betrachtet als die Ausnahme selbst. Da die Catch-Zweige immer in der Reihenfolge von speziell zu allgemein sortiert werden müssen, tauchen solche Referenzen weiter unten auf als die Ausnahme selbst. Außerdem kann durch die Ist-eine-Beziehung das Auffangen einer Basisklassen-Ausnahme alle Kindklassen-Ausnahmen mit erfassen, sodass keine vergessen werden kann. Im folgenden Beispiel wird die spezielle Ausnahme DivideByZeroException vor der allgemeineren ArithmeticException behandelt. In der Methode Test werden Werte übergeben, die jede der drei Ausnahmen in den Catch-Zweigen auslösen. '...\Lauf\Ausnahmen\Hierarchie.vb
Option Strict On Namespace Lauf Module Hierarchie Class Zahl : Public Wert As Integer : End Class Function Rechnung(ByVal z As Zahl) As Integer Try Return z.Wert * z.Wert \ z.Wert
635
9.1
9
Code erstellen und debuggen
Catch ex As DivideByZeroException Console.Write(ex.Message & ": ") : Return Integer.MaxValue Catch ex As NullReferenceException Console.Write(ex.Message & ": ") : Return Integer.MinValue Catch ex As ArithmeticException Console.Write(ex.Message & ": ") : Return 0 End Try End Function Sub Test() Dim z As New Zahl() z.Wert = 0 : Console.WriteLine(Rechnung(Nothing)) z.Wert = 0 : Console.WriteLine(Rechnung(z)) z.Wert = Integer.MinValue : Console.WriteLine(Rechnung(z)) Console.ReadLine() End Sub End Module End Namespace
Die dritte Zeile zeigt, wie die »vergessene« Ausnahme OverflowException von dem CatchZweig mit deren Basisklasse ArithmeticException erfasst wurde. Object reference not set to an instance of an object.: –2147483648 Attempted to divide by zero.: 2147483647 Arithmetic operation resulted in an overflow.: 0
Der zweite Effekt der Vererbungshierarchie der Ausnahmen ist, dass ausnahmslos alle Ausnahmen von der gemeinsamen Basisklasse Exception abgeleitet sind und deren Funktionalität nutzen können. Tabelle 9.1 zeigt deren Eigenschaften. Eigenschaft
Beschreibung
Data
Zusätzliche benutzerdefinierte Informationen (IDictionary)
HelpLink
Verweist auf eine Hilfedatei, die diese Ausnahme beschreibt
HResult
Fehlercode zur Interoperabilität mit COM-Klassen (Protected)
InnerException
Referenz auf die tatsächliche Ausnahme. Diese Information dient dazu, auf geeignetere Weise auf die Ausnahme zu reagieren.
R
Message
Gibt einen String mit der Beschreibung des aktuellen Fehlers zurück.
R
Source
Beschreibung der fehlerauslösenden Anwendung oder des Objekts
StackTrace
String mit der aktuellen Aufrufreihenfolge aller Methoden
R
TargetSite
Methode, in der die Ausnahme ausgelöst worden ist
R
Tabelle 9.1
R
Eigenschaften der Klasse Exception (R = ReadOnly)
Besonders hinweisen möchte ich auf InnerException, die oft benutzt wird, wenn eine Ausnahme aufgefangen wird und eine andere auslöst. Wenn vor dem Auslösen die innere Ausnahme gesetzt wird, kann der Aufrufer den »wahren« Grund der Ausnahme ermitteln.
636
Ausnahmen
9.1.3
Eigene Ausnahmen
Sie sollten Fehler, die spezifisch für Ihre Anwendung sind, durch eigene Ausnahmen kennzeichnen. Diese müssen sich direkt oder indirekt von Exception ableiten. Damit Anwender nicht denken, dass eine Ihrer Ausnahmen Teil von .NET ist, empfiehlt sich die Ableitung von ApplicationException. Beide genannten Ausnahmeklassen haben die gleichen Konstruktoren, von denen Sie einen – implizit oder explizit – in dem Konstruktor Ihrer Ausnahmeklasse aufrufen müssen. Public Sub New() Public Sub New(message As String) Protected Sub New(info As SerializationInfo, context As StreamingContext) Public Sub New(message As String, innerException As Exception)
Von den 636 Ausnahmen, die ich in .NET gezählt habe, enden nur 30 nicht auf Exception. Sie sollten sich bei eigenen Ausnahmeklassen auch an diese Konvention halten, sie macht Ihren Code für andere leichter lesbar. Das folgende Beispiel definiert eine kleine Ausnahmeklassenhierarchie: ApplicationException->FinanzamtException->BuchungsException. In der Methode Summe wird ein Fehler mit einer BuchungsException quittiert. Der Aufrufer von Summe, die Methode Abschreibung, verpackt den Fehler in die allgemeinere Ausnahme FinanzamtException. In der Methode Test wird Abschreibung mit Werten aufgerufen, die einen Fehler auslösen. Der Catch-Zweig ist spezifisch für diese Anwendung und behandelt nur hier definierte Ausnahmen. Im Falle eines Fehlers wird nicht nur die Fehlermeldung ausgegeben, sondern auch der dem Fehler zugrunde liegende Fehler. '...\Lauf\Ausnahmen\Eigene.vb
Option Strict On Namespace Lauf Module Eigene Class FinanzamtException : Inherits ApplicationException Sub New(nachricht As String, grund As Exception) MyBase.New(nachricht, grund) End Sub Sub New(nachricht As String) MyBase.New(nachricht) End Sub End Class Class BuchungsException : Inherits FinanzamtException Sub New(nachricht As String) MyBase.New(nachricht) End Sub End Class Function Summe(wert As Short, jahre As Short) As Short Try
637
9.1
9
Code erstellen und debuggen
Return wert \ jahre Catch ex As Exception Throw New BuchungsException("Null Jahre.") End Try End Function Function Abschreibung(wert As Short, jahre As Short) As Short Try Return Summe(wert, jahre) Catch ex As BuchungsException Throw New FinanzamtException("Buchungsfehler.", ex) End Try End Function Sub Test() Try Abschreibung(1000, 0) Catch ex As FinanzamtException Console.WriteLine("Fehler beim Finanzamt: {0}", ex.Message) Console.WriteLine("Wahrer Grund: {0}", ex.InnerException) End Try Console.ReadLine() End Sub End Module End Namespace
Die Formatierung der inneren Ausnahme als String beinhaltet unter anderem den Inhalt der Eigenschaft StackTrace und gibt so weitere Hinweise zur Fehlerbeseitigung. Fehler beim Finanzamt: Buchungsfehler. Wahrer Grund: Ausnahmen.Lauf.Eigene+BuchungsException: Null Jahre. at Ausnahmen.Lauf.Eigene.Summe(Int16 wert, Int16 jahre) in M:\VisualStudioWS\Lauf\Ausnahmen\Eigene.vb:line 21 at Ausnahmen.Lauf.Eigene.Abschreibung(Int16 wert, Int16 jahre) in M:\VisualStudioWS\Lauf\Ausnahmen\Eigene.vb:line 26
9.2
Protokollierung
Wenn ein Fehler nicht zu einer Ausnahme führt, ist er viel schwerer aufzuspüren. Dann bleibt nichts anderes als eine Analyse des Programmlaufes. Die meiste Zeit bei der Fehlersuche verbringt man mit der Suche nach der Stelle, die den Fehler ausgelöst hat. Hier können selbst definierte Ausgaben sehr viel helfen. Sie haben den Nachteil, nicht so »komfortabel« wie ein grafischer Debugger zu sein. Andererseits haben sie ein paar unschätzbare Vorteile: 왘
Sie bestimmen die ausgedruckte Information.
왘
Sie beeinflussen den Programmlauf nur minimal. (Das ist hilfreich bei zeitkritischen und nebenläufigen Anwendungen, in denen mehrere Threads gleichzeitig laufen.)
왘
Sie brauchen keinen Debugger. (Das ist hilfreich zur Fehlersuche bei einem Kunden.)
638
Protokollierung
왘
Wenn sie entsprechend konfiguriert sind, können sie selbst bei Programmen helfen, die einen Debugger zum Absturz bringen.
왘
Auch andere Ausgaben können speziell protokolliert werden, die mit der eigentlichen Programmlogik nichts zu tun haben (zum Beispiel Dateizugriffe).
9.2.1
Ausgabe mit Debug
Die einfachste Art, Informationen auszudrucken, ist die Methode Console.WriteLine. Sie hat den Nachteil, dass Fehlerausgaben von normalen Ausgaben nicht getrennt sind. Außerdem müssen sie manuell vor Auslieferung der Software gelöscht werden. Daher bietet uns .NET die Klasse Debug im Namensraum System.Diagnostics an, mit der wir Ausgaben an einen eigenen Ausgabekanal schicken können. Standardmäßig ist das das Fenster Ausgabe in Visual Studio, das auch während der Kompilierung benutzt wird. Sie können es sich anzeigen lassen, indem Sie im Menü Ansicht das Untermenü Andere Fenster 폷 Ausgabe wählen.
Protokoll anzeigen Analog zur Klasse Console schreiben Sie mit Debug.Write eine Ausgabe, allerdings mit etwas weniger Überladungen. Die folgende Syntax zeigt alle Methoden zur Erzeugung einer unbedingten Ausgabe. Es fehlt insbesondere eine formatierte Ausgabe. Sie müssen die Ausgabe also selbst zu einer Zeichenkette zusammensetzen. Die Zeichenkette kategorie wird dem wert vorangestellt: Public Shared Sub Ausgabe(wert As Art) Public Shared Sub Ausgabe(wert As Art, kategorie As String) Ausgabe: Write oder WriteLine, Art: String oder Object
Analog zu diesen Ausgabemethoden gibt es Varianten, die nur dann eine Ausgabe machen, wenn die im ersten Parameter angegebene Bedingung erfüllt ist: Public Shared Sub AusgabeIf(bedingung As Boolean, wert As Art) Public Shared Sub AusgabeIf(bedingung As Boolean, wert As Art, _ kategorie As String) Ausgabe: Write oder WriteLine, Art: String oder Object
Im folgenden Beispiel wird bei jedem größeren Wert mit WriteLineIf eine Ausgabe im Ausgabe-Fenster produziert: '...\Lauf\Protokollierung\Eingrenzung.vb
Option Strict On Namespace Lauf Module Eingrenzung Private rnd As New Random(1234)
639
9.2
9
Code erstellen und debuggen
Sub Ausgeben(ByRef wert As Integer, ByVal i As Integer) Dim aus As Integer = rnd.Next(0, 7) wert += aus Debug.WriteLineIf(aus > 4, _ i & ": wieder eine Fünfer weg: " & wert, "Geld") End Sub Sub Test() Dim wert As Integer For i As Integer = 0 To 10 Ausgeben(wert, i) Next Console.WriteLine("Gesamtausgabe: {0}", wert) Console.ReadLine() End Sub End Module End Namespace
Die normale Ausgabe ist sehr kurz: Gesamtausgabe: 40
Das Ausgabe-Fenster enthält nur die Ausgaben von Debug (siehe Abbildung 9.1). Der Kategorieparameter Geld steht vor den eigentlichen Ausgaben.
Abbildung 9.1
Ausgabe von »Debug.WriteLineIf«
Hinweis Sollten Sie im Ausgabe-Fenster nichts sehen, prüfen Sie in den Optionen des Debuggers (Menü Extras 폷 Optionen), ob die Ausgabe in das Direktfenster umgeleitet wird.
Einrücken der Ausgabe Mit der Methode Indent wird die Einzugsebene um eins erhöht, mit Unindent wird sie um eins verringert. Mit der Eigenschaft IndentSize bestimmen Sie die Einheit. Standardmäßig sind es vier Leerzeichen. IndentLevel legt eine Einzugsebene direkt fest (ersetzt Indent-Aufrufe). Debug.WriteLine("Ausgabe 1") Debug.Indent() Debug.WriteLine("Ausgabe 2") Debug.IndentLevel = 3
640
Protokollierung
Debug.WriteLine("Ausgabe 3") Debug.Unindent() Debug.WriteLine("Ausgabe 4") Debug.IndentSize = 2 Debug.IndentLevel = 1 Debug.WriteLine("Ausgabe 5")
Der Code führt zu folgender Ausgabe: Ausgabe 1 Ausgabe 2 Ausgabe 3 Ausgabe 4 Ausgabe 5
Prüfung mit Assert Sie sollten die Methode Assert einsetzen, um Annahmen zu prüfen, von denen Sie eigentlich meinen, dass sie immer erfüllt sind. Die Methode ist also zur Erkennung schwerwiegender Fehler gedacht. Die Methode zeigt eine Fehlermeldung an, wenn ein Ausdruck mit False ausgewertet wird. Debug.Assert(wert >= 0, "Negativer Wert")
Ist wert negativ, erscheint die Nachricht aus Abbildung 9.2.
Abbildung 9.2
Meldung der Methode »Debug.Assert«
Das Dialogfenster enthält neben der Zeichenfolge des zweiten Parameters auch Informationen darüber, in welcher Klasse und welcher Methode der Assertionsfehler aufgetreten ist.
9.2.2
Ausgabe mit Trace
Die Klasse Trace hat die gleiche Funktionalität wie Debug. Ein Unterschied macht sich bei einem Wechsel der Build-Konfiguration zwischen Release und Debug bemerkbar (siehe Abbildung 9.3).
641
9.2
9
Code erstellen und debuggen
Abbildung 9.3
Debug/Release-Build-Konfiguration
Mit der Standardeinstellung Debug erzeugen Debug und Trace Ausgaben, während bei Release Aufrufe von Debug ignoriert werden. Dies liegt daran, dass im zweiten Fall die Aufrufe gar nicht kompiliert werden und die Anwendung nicht aufblähen. Der Hintergrund ist das in Abschnitt 4.8.1, »Beispiel: Bedingte Kompilierung«, beschriebene Attribut. Unterhalb des Verzeichnisses der Quellcodedateien legt die Entwicklungsumgebung das Verzeichnis \bin an, das – je nach eingestellter Build-Konfiguration – die beiden Verzeichnisse \Debug und \Release enthält und das entsprechende Kompilat aufnimmt. Die PDB-Dateien in diesen Verzeichnissen ermöglichen dem Debugger eine für Menschen lesbare Ausgabe.
9.2.3
Ausgabeziel mit TraceListener
Im Namensraum System.Diagnostics sind fünf Listener als Ziel der Protokollierung definiert, die alle aus der abstrakten Klasse TraceListener abgeleitet sind (siehe Tabelle 9.2): Listener-Klasse
Ziel der Ausgabe
ConsoleTraceListener
Konsole
DefaultTraceListener
Entwicklungsumgebung (Standardeinstellung)
EventLogTraceListener
Windows-Ereignisprotokoll
TextWriterTraceListener
Stream, Textwriter oder Datei (siehe Kapitel 7, »Eingabe und Ausgabe«)
Tabelle 9.2
Einige Listener-Klassen des Namensraums »System.Diagnostics«
Die folgende Klassenhierarchie zeigt die von TraceListener abgeleiteten Klassen in .NET. System.Object ÀSystem.MarshalByRefObject À2ÄÄTraceListener Ã1ÄÄFileLogTraceListener Ã2ÄÂDefaultTraceListener ³ ÃEventLogTraceListener ³ ÀTextWriterTraceListener ³ À2ÄÂConsoleTraceListener ³ ÃDelimitedListTraceListener ³ ÃEventSchemaTraceListener ³ ÀXmlWriterTraceListener Ã3ÄÄEventProviderTraceListener À4ÄÂIisTraceListener ÀWebPageTraceListener
642
Protokollierung
1: 2: 3: 4:
Microsoft.VisualBasic.Logging System.Diagnostics System.Diagnostics.Eventing System.Web
Die Ausgabeziele von Debug und Trace sind in deren Eigenschaft Listeners vom Auflistungstyp TraceListenerCollection gespeichert. Sie können mit Add, Remove, Clear usw. die Liste nach Bedarf anpassen. Beachten Sie, dass die Eigenschaft Listeners beider Klassen dieselbe Auflistung bezeichnet. Im folgenden Codefragment wird die Liste geleert und dann die Konsole als einziges Ausgabeziel hinzugefügt. Debug und Trace schreiben damit nur noch in die Konsole. Debug.Listeners.Clear() TextWriterTraceListener console = new TextWriterTraceListener(Console.Out) Debug.Listeners.Add(console)
Dateiausgabe mit TextWriterTraceListener Mit diesem Listener leiten Sie einfach Ausgaben in eine Datei um. Übergeben Sie dem Konstruktor einen Dateipfad, werden die Ausgaben an die (gegebenenfalls neu erstellte) Datei angehängt. Brauchen Sie mehr Kontrolle, verwenden Sie einen anderen Konstruktor. Im folgenden Codefragment stellt Flush sicher, dass vor dem Schließen der Ausgabepuffer geleert wird. Alternativ setzen Sie AutoFlush=True. Da Debug und Trace dieselben Listener verwenden, werden zwei Zeilen in die angegebene Datei geschrieben. Dim listener As New TextWriterTraceListener("C:\DebugProtocol.txt") Trace.Listeners.Add(listener) Debug.WriteLine("Debug.WriteLine-Anweisung") Trace.WriteLine("Trace.WriteLine-Anweisung") listener.Flush() listener.Close()
Das folgende Beispiel zeigt, wie mithilfe dieses Mechanismus datierte Fehlerinformationen auch über das Ende einer Anwendung hinaus gerettet werden können: '...\Lauf\Protokollierung\PostMortem.vb
Option Strict On Namespace Lauf Module PostMortem Sub Test() Debug.Listeners.Clear() Dim tl = New TextWriterTraceListener("C:\Temp\ErrorProtocol.txt") Debug.Listeners.Add(tl) Debug.AutoFlush = True Try Operation() Catch ex As Exception Trace.WriteLine("Fehler: " & Now & " – " & ex.Message)
643
9.2
9
Code erstellen und debuggen
Finally tl.Close() End Try End Sub Sub Operation() Throw New Exception("Werch ein Illtum.") End Sub End Module End Namespace
Der Inhalt der Datei zeigt die Protokollierung der Ausnahme: Fehler: 20.12.2008 20:52:02 – Werch ein Illtum.
Mehrere Listener verwalten Debug und Trace schreiben in alle in Listeners gespeicherten Ausgabeziele. Um die Ziele zu beschränken, müssen die entsprechenden Listener (temporär) aus der Liste entfernt werden. Sie können leichter gefunden werden, wenn ihnen mit einem zweiten Konstruktorparameter ein Name gegeben wird. Im folgenden Beispiel werden zwei benannte Listener registriert und es wird eine Meldung geschrieben. Anschließend wird ein Listener aus der TraceListenerCollection gelöscht. Die anschließende Meldung wird nur vom verbleibenden Listener weitergeleitet. Dim ZielA As TextWriterTraceListener("C:\A.txt", "AL") Trace.Listeners.Add(ZielA) Dim ZielB As TextWriterTraceListener("C:\B.txt", "BL") Trace.Listeners.Add(ZielB) Trace.AutoFlush = True Trace.WriteLine("Erste Information") Trace.Listeners.Remove("BL") Trace.WriteLine("Zweite Information")
Nutzen Sie statt AutoFlush die Methode Flush, muss sie vor der Deregistrierung aufgerufen werden.
Ereignisprotokollausgabe Mit dem Ziel EventLogTraceListener schreiben Sie in das Windows-Ereignisprotokoll. Public Sub New() Public Sub New(eventLog As EventLog) Public Sub New(source As String)
Im folgenden Codefragment wird ein Ereignisprotokoll namens MyProtocol auf der lokalen Maschine eingerichtet, die durch einen Punkt benannt ist. Mit der Eigenschaft Source geben Sie einen Bezeichner für die Ereignisquelle an. Innerhalb der Ereignisprotokolle von Win-
644
Protokollierung
dows muss dieser eindeutig sein. EventLog.GetEventLogs(".") listet alle bekannten Ereignisprotokolle auf. Windows definiert bereits Application, System und Security. Dim log As New EventLog("MyProtocol", ".") log.Source = "MyApplication" Dim el New EventLogTraceListener(log) Debug.Listeners.Add(el) Debug.WriteLine("Ein Eintrag im Ereignisprotokoll") el.Flush()
Ereignisprotokolle sollten nur für wenige, besonders wichtige Meldungen verwendet werden.
9.2.4
Steuerung mit Konfigurationsdateien
Die Methoden WriteIf und WriteLineIf erlauben die bedingte Protokollierung. Die im ersten Parameter angegebene Bedingung ist der Schlüssel zur Steuerung. Normalerweise wird sie durch Code gesetzt. Wenn Sie eine von System.Diagnostics.Switch abgeleitete Klasse verwenden, können Sie die Bedingung in einer Konfigurationsdatei steuern. Object ÀSwitch ÀÂBooleanSwitch ÃSourceSwitch ÀTraceSwitch
BooleanSwitch Dieser Schalter kennt nur die Stellungen An und Aus. Dem Konstruktor übergeben Sie den Namen des Schalters und eine Beschreibung: Dim meiner As New BooleanSwitch("MeinSchalter", "Ablaufverfolgung in MyApp")
Der Schalter startet in der Stellung False und kann mit meiner.Enabled = True angeschaltet werden. Diese Eigenschaft wird auch in den Protokollfunktionen verwendet. Trace.WriteLineIf(meiner.Enabled, "Ablaufverfolgung:" & Now.ToString())
Die Schalterstellung können Sie auch in der Anwendungskonfigurationsdatei festlegen:
Jeder Schalter wird in einem eigenen add-Element dem switches-Knoten hinzugefügt. Das Attribut name gibt den Namen des Schalters an und das Attribut value die Schalterstellung. Dabei steht 1 für True und 0 für False. Beim Start der Anwendung wird die Konfigurations-
645
9.2
9
Code erstellen und debuggen
datei eingelesen und schafft eine Vorbelegung des Schalters, die im Code geändert werden kann. Durch Änderung der XML-Datei vor dem Programmstart wird die Anwendung gesteuert.
TraceSwitch Dieser Schalter kennt nicht nur die Stellungen An und Aus, sondern verschiedene Einstellungen, die über die Eigenschaft Level gesetzt werden. Public Property Level As TraceLevel
Die Eigenschaft ist vom Typ der Enumeration TraceLevel (siehe Tabelle 9.3). Jeder Wert, bis auf Off, umfasst die niederwertigeren. Wenn zum Beispiel Info gesetzt ist, dann sind damit automatisch auch Error und Warning gesetzt. Konstante
Wert
Art der ausgegebenen Meldungen
Off
0
Keine
Error
1
Fehlermeldungen
Warning
2
Fehler- und Warnmeldungen
Info
3
Fehler-, Warn- und Informationsmeldungen
Verbose
4
Alle Meldungen
Tabelle 9.3
Die Enumeration »TraceLevel«
Der TraceSwitch-Konstruktor bekommt einen Namen und eine Beschreibung übergeben. Dim meiner As New TraceSwitch("MeinSchalter", "Ablaufverfolgung in MyApp") meiner.Level = TraceLevel.Warning
Die Methoden WriteIf und WriteLineIf brauchen im ersten Parameter einen booleschen Wert. Daher definiert TraceSwitch die booleschen Eigenschaften TraceError, TraceWarning, TraceInfo und TraceVerbose. Trace.WriteLineIf(meiner.TraceWarning, "Ablaufverfolgung:" & Now.ToString())
Analog zu BooleanSwitch kann ein Schalter in einer Konfigurationsdatei definiert und vorbelegt werden – im folgenden Beispiel mit TraceLevel.Info:
646
Visual Studio Debugger
9.3
Visual Studio Debugger
Die im letzten Abschnitt gezeigte Protokollierung ist sehr robust, erlaubt aber keinen Eingriff, wenn Sie aufgrund der Ausgabe einen Fehler entdecken und das Programm unterbrechen möchten. Diese Möglichkeit bietet ein Debugger. Das Visual Studio unterstützt das Debuggen sowohl von lokalen als auch von .NET-Anwendungen im Netzwerk. Ich beschränke mich hier auf den Test lokaler Anwendungen. Die Unterbrechung einer Anwendung durch den Debugger kann drei Ursachen haben: 왘
Die Laufzeit der Anwendung erreicht einen Haltepunkt.
왘
Die Anwendung führt die Methode System.Diagnostics.Debugger.Break aus.
왘
Es tritt eine Ausnahme auf.
9.3.1
Debuggen im Haltemodus
Wie bereits in Abschnitt 2.3.2, »Start und Test«, dargestellt wurde, können Sie durch einen Klick in den grauen Bereich neben dem Quelltext einen Haltepunkt setzen, der als roter Punkt erscheint. Alternativ drücken Sie die Taste (F9). Ein erneuter Klick oder Tastendruck entfernt den Haltepunkt. Der Debugger hält an der Stelle, bevor die Codezeile ausgeführt wurde. Im Haltemodus können Sie Variablen untersuchen, ändern oder das Programm fortsetzen. Dabei werden Sie auch von mehreren Fenstern des Debuggers unterstützt: Überwachen, Lokal und Auto. Die Fortsetzung erfolgt über das Menü Debuggen, die gleichnamige Symbolleiste (gegebenenfalls über Menü Ansicht 폷 Symbolleisten einblenden) und diverse Tastenkürzel: 왘
Einzelschritt : Der Programmcode wird Zeile für Zeile ausgeführt, auch in einer benutzer. definierten Methode. Das Tastaturkürzel dafür ist (F11), die Schaltfläche
왘
Prozedurschritt: Der Programmcode wird weiterhin in Einzelschritten ausgeführt, aber beim Aufruf benutzerdefinierter Methoden wird nicht in diese verzweigt. Das Tastaturkür. zel ist (F10), die Schaltfläche
왘
Ausführen bis Rücksprung: Die aktuelle Methode wird ohne weiteren Halt ausgeführt und direkt nach dem Aufruf wieder angehalten. Das Tastaturkürzel ist (ª) + (F11), die Schaltfläche .
QuickInfo-Fenster Bewegen Sie im Haltemodus die Maus über eine Variable, wird deren Wert angezeigt, den Sie auch ändern können. Bedingte Haltepunkte Über das Kontextmenü eines Haltepunktes können Sie eine Bedingung festlegen, die erfüllt sein muss, damit der Programmlauf an diesem Punkt unterbrochen wird. In Abbildung 9.4 muss intVar kleiner als 8 sein.
647
9.3
9
Code erstellen und debuggen
Abbildung 9.4
Festlegen einer Haltepunktbedingung
Ist die Option Hat sich geändert markiert, prüft der Debugger, ob sich der Wert der Variablen seit dem letzten Erreichen des Haltepunktes geändert hat. Wenn ja, hält das Programm an.
Haltepunkt mit Trefferanzahl aktivieren Additiv zur Bedingung können Sie über den Kontextmenüpunkt Trefferanzahl... fordern, dass der Haltepunkt außerdem eine bestimmte Anzahl Male erreicht wurde, zum Beispiel in einer Schleife (siehe Abbildung 9.5).
Abbildung 9.5
Festlegen der Trefferanzahl
Verwalten der Haltepunkte Der Menüpunkt Debuggen 폷 Fenster 폷 Haltepunkte zeigt eine Liste aller Haltepunkte (siehe Abbildung 9.6). Mit dem Auswahlkästchen können Sie Haltepunkte deaktivieren, ohne seine Einstellungen zu verlieren, sodass er bei Reaktivierung noch alle Bedingungen kennt. Deaktivierte Haltepunkte werden durch einen nicht ausgefüllten Punkt markiert, die Existenz von Bedingungen durch ein weißes Pluszeichen.
Abbildung 9.6
648
Liste aller Haltepunkte
Unit Tests
9.3.2
Fenster zum Zugriff auf Variablen
Visual Studio bietet mehrere Fenster zur Untersuchung und Änderung von Variablenwerten. Jedes der Fenster kann über das Menü Debuggen 폷 Fenster oder das Kontextmenü des Codeeditors geöffnet werden.
Direktfenster In diesem Fenster haben Sie im Wesentlichen drei Möglichkeiten: 왘
Variablenwert ausgeben: ?Variable
왘
Variablenwert zuweisen: Variable = Wert
왘
Methode ausführen (darf Ausnahmen auslösen): Methode(...)
Jede der Eingaben werten Sie durch Druck auf die Taste (¢) aus.
Auto Das Auto-Fenster zeigt alle Variablen der Codezeile an, in der sich der Haltemodus aktuell befindet, sowie alle Variablen der drei vorausgehenden Codezeilen. Angezeigt werden neben dem Namen der Inhalt und der Datentyp. Durch einen Doppelklick gelangen Sie in den Editiermodus, in dem Sie Variablenwerte ändern können. Lokal Dieses Fenster enthält alle Variablen mit Namen, Wert und Typ, die in der aktuellen Methode definiert sind. Variablen, die sich zwar im Gültigkeitsbereich einer Methode befinden, aber außerhalb deklariert sind, werden nicht vom Lokal-Fenster erfasst. Überwachen In diesem Fenster geben Sie Variablen an, die vom Debugger überwacht werden sollen. Neue Variablen fügen Sie über den Kontextmenüpunkt Überwachung hinzufügen der Variablen hinzu oder tragen sie direkt im Fenster ein. Durch einen Doppelklick gelangen Sie in den Editiermodus, in dem Sie Variablenwerte ändern können.
9.4
Unit Tests
Wenn Sie an einem Projekt arbeiten, ergeben sich immer wieder Änderungen im Code. Die vielen Abhängigkeiten Ihrer Klassen untereinander führen schnell dazu, dass Korrekturen an einer Stelle zu Problemen an einer anderen führen. Hier setzen automatisierte Tests an, die Sie nach Änderungen durchlaufen lassen, um festzustellen, ob eine Korrektur Seiteneffekte hat. Ich verwende folgendes einfaches Testprojekt: '...\Lokal\Debugging\Programm.vb
649
9.4
9
Code erstellen und debuggen
Namespace Lokal Module Programm Sub Main() Dim o As New Klasse() Console.WriteLine(o.Methode(3, 4)) Console.ReadLine() End Sub End Module End Namespace
'...\Lokal\Debugging\Klasse.vb
Option Strict On Namespace Lokal Public Class Klasse Function Methode(ByVal i As Integer, ByVal j As Integer) As Integer Return i + j End Function End Class End Namespace
Als Erstes erstellen wir einen Test über das in Abbildung 9.7 gezeigte Kontextmenü des Codefensters.
Abbildung 9.7
So starten Sie das Erstellen von Tests.
Im sich öffnenden Dialog (siehe Abbildung 9.8) wählen wir die zu testenden Komponenten. Da ein eigenes Testprojekt erstellt wird und eine der Methoden implizit den Modifizierer Friend hat, müssen wir im folgenden Dialog (siehe Abbildung 9.9) einer Sichtbarkeitserhöhung zustimmen. Daraufhin wird ein neues Testprojekt erzeugt, dem ich den Namen DebuggingTest gegeben habe (siehe Abbildung 9.10).
650
Unit Tests
Abbildung 9.8
Testauswahl
Abbildung 9.9
Sichtbarkeit erhöhen
Abbildung 9.10
Testprojekte
Damit nicht gleich der erste Test »scheitert«, ändern wir noch die Datei KlasseTest.vb ein wenig. Die Aufrufe Assert.Inconclusive werden auskommentiert, und die Methode zum Testen der Methode wird durch Werte für die Ein- und Ausgabe ergänzt (Werte 3, 4 und 7). _ Public Sub MethodeTest()
651
9.4
9
Code erstellen und debuggen
Dim target As Klasse = New Klasse ' TODO: Passenden Wert initialisieren Dim i As Integer = 3 ' TODO: Passenden Wert initialisieren Dim j As Integer = 4 ' TODO: Passenden Wert initialisieren Dim expected As Integer = 7 ' TODO: Passenden Wert initialisieren Dim actual As Integer actual = target.Methode(i, j) Assert.AreEqual(expected, actual) 'Assert.Inconclusive("Überprüfen Sie die Richtigkeit dieser Testmethode.") End Sub
Nun können wir über das in Abbildung 9.11 gezeigte Kontextmenü des Projekts »DebuggingTest« den Test starten.
Abbildung 9.11
Test starten
Das Testergebnis im Fenster Testergebnisse ist zufriedenstellend. Die Methode Main kann nicht gestestet werden, weil sie keinen Rückgabewert hat. Abbildung 9.12 zeigt, dass die beiden anderen Tests erfolgreich verliefen.
Abbildung 9.12
652
Dieser Test war erfolgreich.
Refactoring
Zum Schluss ändern wir unsere Testmethode. Statt Return i + j verwenden wir Return i + 2 * j und starten den Test erneut über das Kontextmenü des Projekts »DebuggingTest«. Das Testergebnis zeigt das Problem (siehe Abbildung 9.13).
Abbildung 9.13
Dieser Test ist gescheitert.
Im Menü Test finden Sie einige Punkte, um den Test zu verfeinern.
9.5
Refactoring
Refactoring bezeichnet die automatische Umbenennung und Umstrukturierung vorhandenen Codes. Unter Visual Basic bietet das Visual Studio nur sehr wenig, aber das Wenige kann auch schon sehr hilfreich sein. Im Kontextmenü eines Bezeichners (Variable, Methodenname usw.) finden Sie einen Punkt Umbenennen, mit dem Sie alle Vorkommnisse umbenennen können. In anderen Programmiersprachen ist zum Teil sehr viel mehr in Visual Studio zu finden. Sollten Sie größere Projekte haben, lohnt sich ein Blick in das Internet. Es gibt einige Anbieter, die das Refactoring erheblich erweitern, zum Teil sogar kostenlos.
9.6
UML
In vielen Unternehmen erstellen die Entwickler zur Designphase das Klassendesign mithilfe der UML (Unified Modeling Language), um die schwierigen und komplexen Anforderungen an eine ausgefeilte Software zu erfüllen. UML ist eine Spezifikation, die eine Reihe von Diagrammen definiert, mit denen eine objektorientierte Software während der Designphase nicht nur visuell dargestellt, sondern auch modelliert werden kann. Mit dem Klassendiagramm von UML lassen sich zum Beispiel Klassen samt ihrer Beziehungen und der Vererbungslinien darstellen. Im Visual Studio ist mit dem Klassendesigner ein Werkzeug integriert, mit dem Klassendiagramme, ähnlich den UML-Klassendiagrammen, modelliert werden können.
9.6.1
Klassendiagramm
Ein Klassendiagramm können Sie zum Beispiel aus einem vorhandenen Coding erstellen. In diesem Abschnitt verwende ich folgende kleine Klassenhierarchie:
653
9.5
9
Code erstellen und debuggen
'...\Lauf\Uml\Fahrzeug.vb
Namespace Lauf MustInherit Class Fahrzeug Private ID As Integer Public Besitzer As String MustOverride Sub Fahren() End Class End Namespace
'...\Lauf\Uml\Auto.vb
Namespace Lauf Class Auto : Inherits Fahrzeug Public Verbrauch As Double Overrides Sub Fahren() End Sub End Class End Namespace
'...\Lauf\Uml\Rad.vb
Namespace Lauf Class Rad : Inherits Fahrzeug Overrides Sub Fahren() End Sub End Class End Namespace
Durch den Kontextmenüpunkt Klassendiagramm anzeigen können Sie aus dem Coding das Diagramm erstellen, das Sie in Abbildung 9.14 sehen. Auf diese Art lässt sich der Informationsgehalt von Quelltextdateien enorm verdichten. So behalten Sie auch in komplexen Projekten die Übersicht und erkennen leichter die Vererbungsstrukturen. Das Klassendiagramm sollte eigentlich Typdiagramm heißen, denn es kann auch andere Typen enthalten, zum Beispiel Strukturen, Delegates und Schnittstellen. Das Klassendiagramm in Visual Basic ist ein Zweiwege-Tool. Änderungen im Designer ändern den Quelltext und umgekehrt. Ein Doppelklick auf ein Klassenmitglied im Klassendiagramm bringt Sie zur richtigen Stelle im Quelltext. Die CD-Datei des Klassendiagramms ist eine XMLDatei, die nicht nur die einzelnen Typen, sondern auch deren Eigenschaften, Methoden usw. samt deren Parametern exakt beschreibt, die im Klassendiagramm dargestellt werden. In der Darstellung können Sie die Klassenmitglieder durch einen Klick auf den Doppelpfeil rechts oben in der Klasse ein- und ausblenden.
654
UML
Abbildung 9.14
Einfaches Klassendiagramm
Möchten Sie die Klassenmitglieder sehen, haben Sie drei Alternativen: 왘
nur die Bezeichner
왘
die Bezeichner einschließlich der Typangabe
왘
die Bezeichner einschließlich der kompletten Signatur
Die Umschaltung erfolgt in der in Abbildung 9.15 gezeigten Symbolleiste des Klassendesigners. Bezeichner einschl. Typangabe Bezeichner Bezeichner sowie die komplette Signatur
Abbildung 9.15
9.6.2
Symbolleiste des Klassendesigners
Toolbox
Die in Abbildung 9.16 gezeigte Toolbox des Klassendesigners zeigt neben den verschiedenen Datentypen auch Vererbungs- und Zuordnungslinien, die Sie mit Drag&Drop in den Designbereich ziehen können.
9.6.3
Das Fenster Klassendetails
Für die im Klassendiagramm aktuell markierte Klasse werden die Klassenmitglieder im Fenster Klassendetails unterhalb des Klassendiagramms angezeigt (siehe Abbildung 9.17). Sie können es über Ansicht 폷 Weitere Fenster öffnen. Mit den Buttons links im Fenster können Sie Klassenmitglieder hinzufügen. Die Spalte Zusammenfassung repräsentiert das -Tag der XML-Dokumentation. Die rechte Spalte, Ausblenden, wirkt sich nur auf die Anzeige aus, die Definition bleibt unverändert.
655
9.6
9
Code erstellen und debuggen
Abbildung 9.16
Toolbox des Klassendesigners
Abbildung 9.17
Das Fenster »Klassendetails«
Die Modifizierer Overridable, Overrides, MustOverride, NotOverridable können Sie nicht in den Klassendetails spezifizieren, sondern nur im Quelltext oder im Eigenschaftsfenster der Selektion in den Klassendetails. Dieses bietet noch weitere Einstellmöglichkeiten (siehe Tabelle 9.4). Eigenschaft
Beschreibung
Benutzerdefinierte Attribute
Etwas irreführend können Sie hier alle Attribute angeben, die mit dem Mitglied verknüpft sind.
Hinweise
Der Inhalt des -Tags der XML-Dokumentation
Name
Der Bezeichner des Mitglieds
Neu
True entspricht Shadows im Quelltext.
Rückgabewerte
Der Inhalt des -Tags der XML-Dokumentation
Statisch
True entspricht Shared im Quelltext.
Tabelle 9.4
656
Eigenschaften der Selektion in den Klassendetails
Codeausschnitte (Code Snippets)
Eigenschaft
Beschreibung
Typ
Der Typ des Mitglieds
Vererbungsmodifizierer
Overridable, Overrides, MustOverride oder NotOverridable
Zugriff
Der Sichtbarkeitsmodifizierer
Zusammenfassung
Der Inhalt des -Tags der XML-Dokumentation
Tabelle 9.4
Eigenschaften der Selektion in den Klassendetails (Forts.)
9.6.4 Klassendiagramme als Bilder exportieren Durch das Menü Klassendiagramm 폷 Diagramm als Bild exportieren... können Sie ein Klassendiagramm als Bild speichern, unter anderem in den Formaten BMP, GIF und JPEG.
9.7
Codeausschnitte (Code Snippets)
Ein Codeausschnitt (Code Snippet) ist eine Art Makro, das ein Quelltextfragment erzeugt und so Tipparbeit spart. Code Snippets können Sie auf drei verschiedene Arten einfügen: 왘
Im Code tippen Sie den Bezeichner eines Ausschnitts und drücken dann die (ÿ_)-Taste.
왘
Sie wählen aus der Liste, die der Menüpunkt Ausschnitt einfügen des Kontextmenüs hervorbringt.
왘
Dieselbe Liste erzeugt das Menü Bearbeiten 폷 IntelliSense 폷 Ausschnitt einfügen
Teils sind die Codeausschnitte Vorlagen, die Sie anpassen müssen. Abbildung 9.18 zeigt eine so hinzugefügte For-Schleife.
Abbildung 9.18
9.7.1
Durch ein Code Snippet hinzugefügte For-Schleife
Anatomie
Bei Codeausschnitten handelt es sich um XML-Dateien mit der Dateinamenserweiterung .snippet. Wir wollen uns nun exemplarisch die Datei ansehen, die für den Codeausschnitt einer If-Bedingung verantwortlich ist: \Microsoft Visual Studio 9.0\VB\Snippets\1031\common code patterns\ conditionals and loops\IfEndifStatement.snippet
If..End If Statement
657
9.7
9
Code erstellen und debuggen
Microsoft Corporation Inserts an If..End If statement. If Condition Boolean Replace with an expression that evaluates to either True or False. True
In der Dokumentation finden Sie unter dem Stichwort Schemareferenz für Codeausschnitte weitere Informationen.
9.7.2
Eigene Codeausschnitte
Von den Tags sind nur wenige wirklich notwendig, sodass eigene Ausschnitte schnell erstellt sind. Beachten Sie, dass der Inhalt eines CDATA-Abschnitts wörtlich in das Dokument übernommen wird, inklusive aller Leerzeichen. Der -Abschnitt ist das, was in der IntelliSense-Liste angezeigt wird. Hier ein einfaches Beispiel. Fehlermeldung
Mit dem Codeausschnitt-Manager machen Sie den Ausschnitt verfügbar. Er wird über den Menüpunkt Extras 폷 Codeausschnitts-Manager geöffnet. Bitte wählen Sie im oberen Listenfeld die richtige Sprache. Mit der Schaltfläche Importieren machen Sie den Ausschnitt bekannt.
658
Dieses Kapitel fasst die fundamentalen Klassen Object, String, StringBuilder, DateTime und TimeSpan zusammen und geht auf Besonderheiten ein, die nützlich für fortgeschrittene Programmierung sind.
10
Einige Basisklassen
10.1
Object
Diese Klasse ist als Wurzel der Klassenhierarchie die einzige, die von keiner anderen Klasse abgeleitet ist. Wenn Sie keine explizite Elternklasse angeben, fügt der Compiler implizit Object ein. Die Funktionalität dieses gemeinsamen Vorfahrs teilen alle Objekte.
10.1.1 Methoden Sieben Methoden vererbt die Klasse Object an Subklassen: fünf öffentliche und zwei, die mit Protected auf Nachfahren beschränkt sind (siehe Tabelle 10.1). Methode
Beschreibung
New
Parameterloser Konstruktor
Equals
Gibt an, ob das gegebene Objekt diesem gleich ist.
O
Finalize
Aufgerufen bei Zerstörung des Objekts (z. B. Ressourcenfreigabe)
P
GetHashCode
Objektspezifischer Identifizierer (nicht garantiert eindeutig)
O
GetType
Eine Type-Instanz, die den Typ des Objekts beschreibt
MemberwiseClone
Dupliziert die aktuelle Instanz und liefert die Referenz der Kopie.
P
ReferenceEquals
Gibt an, ob die beiden Referenzen auf dasselbe Objekt zeigen.
S
ToString
Liefert den vollqualifizierenden Namen einer Klasse.
O
Tabelle 10.1
Methoden von »Object« (O = Overridable, P = Protected, S = Shared)
Hinweis Wenn Equals Objekte inhaltlich vergleichen soll, muss die zugrunde liegende Klasse die Methode Equals neu definieren (siehe Abschnitt 3.14.5, »Equals«) oder den Gleichheitsoperator = überladen (siehe Abschnitt 3.11.3, »Vergleich«).
Den Nutzen der Methode ToString habe ich bereits in Abschnitt 3.14.6, »ToString«, angedeutet. Hier möchte ich nur noch auf das Standardverhalten hinweisen, das den vollqualifizierten Klassennamen liefert, ohne Objekte derselben Klasse zu unterscheiden. Viele Klassen überschreiben das Verhalten, zum Beispiel Zahlenprimitive wie Double und Zeichenketten.
659
10
Einige Basisklassen
Public Overridable Function ToString() As String
Finalize wurde bereits in Abschnitt 3.16.2, »Destruktoren«, gezeigt. Die anderen Methoden werden in den folgenden Abschnitten behandelt.
10.1.2 GetType Zur Laufzeit liegen für jeden Datentyp Metainformationen vor, die in einem Type-Objekt gespeichert sind. Public Function [GetType]() As Type
Um den Rahmen dieses Kapitels nicht zu sprengen, zeigt das folgende Beispiel nur eine ganz einfache Typanalyse. Mit den Klassen im Namensraum System.Reflection können Sie zur Laufzeit mit etwas Aufwand sogar neue Datentypen erzeugen. '...\Basisklassen\KlasseObject\GetType.vb
Option Strict On Namespace Basisklassen Class Kind Public Feld As Integer = 9 End Class Class Enkel : Inherits Kind : End Class Module [GetType] Private Sub Eltern(ByVal type As Type) If type IsNot GetType(Object) Then Eltern(type.BaseType) Console.Write(type.Name & " ") End Sub Sub Test() Dim kind As New Enkel() Dim kindertyp As Type = kind.GetType() Console.WriteLine(kindertyp.Assembly.FullName) Eltern(kindertyp) For Each fi As System.Reflection.FieldInfo In kindertyp.GetFields() Console.Write(fi.Name & "=" & fi.GetValue(kind).ToString()) Next Console.ReadLine() End Sub End Module End Namespace
660
Object
10.1.3 Gleichheit Wie in Abschnitt 3.1.9, »Datentypen«, bereits erläutert wurde, muss bei Gleichheit von Objekten zwischen Objektreferenzen auf identische Objekte und Objekten mit gleichen (oder sogar denselben) Werten unterschieden werden. Sowohl Equals als auch ReferenceEquals in der Klasse Object verstehen unter Gleichheit die Identität zweier Referenzen. Für einen inhaltlichen Vergleich muss die Methode Equals in einer Kindklasse überschrieben werden. Public Overridable Function Equals(obj As Object) As Boolean Public Shared Function Equals(objA As Object, objB As Object) As Boolean Public Shared Function ReferenceEquals(a As Object, b As Object) As Boolean
Hinweis Auch wenn die Methode Equals der Schnittstelle IEquatable(Of T) als typsicher bevorzugt werden sollte, kann sie die Methode in Object nicht ersetzen, sondern nur ergänzen.
Was Sie als »gleich« ansehen, bleibt Ihnen überlassen. In der Regel werden die Werte der Felder herangezogen. Im folgenden Beispiel hat die Klasse Ball keine Felder, und verschiedene Bälle werden nicht unterschieden; Equals gibt True zurück. So hält der Torwart einen Ball und nicht den Ball. Bitte beachten Sie den Test auf eine Nullreferenz und korrekten Typ in der Methode Equals, die als Parametertyp immer Object hat. '...\Basisklassen\KlasseObject\Feldgleichheit.vb
Option Strict On Namespace Basisklassen Module Feldgleichheit Class Ball Public Overrides Function Equals(ByVal obj As Object) As Boolean If obj Is Nothing OrElse Not TypeOf obj Is Ball Then Return False Return True End Function End Class Class Spieler Friend ball As Ball End Class Sub Test() Dim regulär As New Ball(), zuviel As New Ball() Dim stürmer As New Spieler : stürmer.ball = regulär Dim torwart As New Spieler : torwart.ball = zuviel Console.WriteLine("Stürmerball ähnelt Torwartball: {0}", _ stürmer.ball.Equals(torwart.ball)) Console.WriteLine("Stürmerball ist Torwartball: {0}", _ Object.ReferenceEquals(stürmer.ball, torwart.ball))
661
10.1
10
Einige Basisklassen
Console.ReadLine() End Sub End Module End Namespace
Der Schiedsrichter wird sich wohl auf den zweiten Vergleich stützen. Stürmerball ähnelt Torwartball: True Stürmerball ist Torwartball: False
Nicht immer wird die Methode Equals zur Definition von Gleichheit herangezogen. Viele Funktionen nutzen aus Effizienzgründen die Methode GetHashCode, zum Beispiel einige der Auflistungen: Public Overridable Function GetHashCode() As Integer
Wenn Sie Ihren Klassen eine eigene Vorstellung von Gleichheit mitgeben wollen, gilt daher: Equals() und GetHashCode() sollten immer gemeinsam überschrieben werden.
Das folgende Beispiel weist auf das Problem hin. Obwohl die verwendete Auflistung GetHashCode zur Sortierung benutzt, müssen beide Methoden in Ware überschrieben werden. Wird nur eine weggelassen, wird das Objekt nach der Änderung nicht mehr gefunden. '...\Basisklassen\KlasseObject\Boxing.vb
Option Strict On Namespace Basisklassen Module Boxing Structure Ware Public Menge, Preis As Double Public Overrides Function Equals(ByVal obj As Object) As Boolean If obj Is Nothing OrElse Not TypeOf obj Is Ware Then Return False Return CType(obj, Ware).Preis.Equals(Preis) End Function Public Overrides Function GetHashCode() As Integer Return Preis.GetHashCode() End Function End Structure Sub Test() Dim waren As New HashSet(Of Ware) Dim mehl As New Ware() : mehl.Preis = 0.52 : waren.Add(mehl) Console.WriteLine("Mehl gefunden: {0}", waren.Contains(mehl)) mehl.Menge = 0.54 Console.WriteLine("Mehl gefunden: {0}", waren.Contains(mehl)) Console.ReadLine()
662
Object
End Sub End Module End Namespace
Der Compiler sucht sich für einen Methodenaufruf immer den am besten passenden Typ heraus. Aus diesem Grund wurde die Schnittstelle IEquatable(Of T) geschaffen. Sie stellt das typisierte Pendant zu Equals() dar. Diese Möglichkeit entbindet Sie aber nicht von der Notwendigkeit, auch die allgemeine Equals()-Methode zu definieren (und damit auch GetHashCode()), denn nicht in jeder Situation hat der Compiler genügend Informationen, um die am besten passende Methode zu wählen.
10.1.4 Objekte kopieren Die Methode MemberwiseClone kopiert ein vorhandenes Objekt und liefert die Referenz auf die Kopie: Protected Function MemberwiseClone() As Object
Von allen Feldern wird eine sogenannte flache Kopie erzeugt. Speichert ein Feld eine Objektreferenz, wird die Referenz als solche kopiert, nicht aber das Objekt, auf das sie zeigt. Durch den Zugriffsmodifizierer können Sie die Methode nicht von außerhalb der Klassenhierarchie aufrufen. Für diesen Zweck gibt es die Schnittstelle ICloneable. Public Interface ICloneable Function Clone() As Object End Interface
Damit lässt sich nun einfach eine Kopierfunktion erstellen. Das folgende Beispiel nutzt MemberwiseClone, um eine flache Kopie zu erzeugen. Innerhalb von Clone wird für das Feld Clone aufgerufen, um eine tiefe Kopie zu erzeugen: '...\Basisklassen\KlasseObject\Kopie.vb
Option Strict On Namespace Basisklassen Module Kopie Class Gemälde : Implements ICloneable Public Wert As Single = Rnd() Public Function Clone() As Object Implements ICloneable.Clone Return Me.MemberwiseClone() End Function End Class Class Fälscher : Implements ICloneable Public obj As New Gemälde() Public Function Clone() As Object Implements ICloneable.Clone
663
10.1
10
Einige Basisklassen
Dim kopie As Fälscher = CType(Me.MemberwiseClone(), Fälscher) kopie.obj = CType(obj.Clone(), Gemälde) Return kopie End Function End Class Sub Test() Dim meister As New Fälscher() Dim stift As Fälscher = CType(meister.Clone(), Fälscher) Console.WriteLine("Selben Fälscher {0}", meister Is stift) Console.WriteLine("Selben Gemälde {0}", meister.obj Is stift.obj) Console.WriteLine("Selben Werte {0}", meister.obj.Wert = stift.obj.Wert) Console.ReadLine() End Sub End Module End Namespace
Hinweis Wenn Feldvariablen Arrays speichern, müssen diese für eine tiefe Kopie erst kopiert werden, bevor die Einzelelemente geklont werden.
Die Ausgabe zeigt, dass eine tiefe Kopie erzeugt wurde: Selben Fälscher False Selben Gemälde False Selben Werte True
10.2
String
Wie in Abschnitt 3.1.9, »Datentypen«, bereits erwähnt wurde, sind Zeichenketten vom Typ String unveränderlich. Bei jeder Änderung wird ein neues Objekt erzeugt. Häufig zu ändernde Zeichenfolgen repräsentieren Sie effizienter mit der Klasse StringBuilder. Um keinen Speicherplatz zu verschwenden, werden String-Literale in einem Pool verwaltet.
10.2.1 Erzeugung Die Initialisierung einer Zeichenkette kann durch direkte Zuweisung oder über einen Konstruktor erfolgen, von denen drei häufiger eingesetzt werden: Dim z As String = "Literal" Dim z As String = Public Sub New(c As Char, count As Integer) Public Sub New(value As Char()) Public Sub New(value As Char(), startIndex As Integer, length As Integer)
664
String
Der erste Konstruktor wiederholt ein Zeichen, die beiden anderen bedienen sich aus einem Zeichenarray. Dim Dim Dim Dim
z1 z2 z3 z4
As As As As
String = New Char(){"1"c,"2"c,"3"c,"4"c} New String("x"c, 3) New String(New Char(){"1"c,"2"c,"3"c,"4"c}) New String(New Char(){"1"c,"2"c,"3"c,"4"c}, 1, 2)
'ergibt 'ergibt 'ergibt 'ergibt
"1234" "xxx" "1234" "23"
10.2.2 Eigenschaften String weist nur zwei Eigenschaften auf: Length und Chars. Length liefert die Anzahl der Zeichen, und mit Chars greifen Sie auf ein einzelnes Zeichen aus der Zeichenfolge zu: Public ReadOnly Default Property Chars(index As Integer) As Char
Im folgenden Codefragment wird das Zeichen »L« in c gespeichert: Dim str As String = "HALLO" Dim c As Char = str(2)
10.2.3 Methoden Die Methoden dienen im Wesentlichen zur Manipulation von Zeichenketten (siehe Tabelle 10.2). Methode
Beschreibung
Clone
Referenz auf diesen String (keine Kopie)
Compare
Vergleich zweier Zeichenketten
S
CompareOrdinal
Vergleich zweier Zeichenketten auf Codebasis
S
CompareTo
Vergleich zweier Zeichenketten
Concat
Zeichenketten zusammenfügen
Contains
Gibt an, ob die gegebene Zeichenkette in dieser Zeichenkette vorkommt.
Copy
Kopie der Zeichenkette
CopyTo
Kopie eines Teilstrings in ein Zeichenarray
EndsWith
Gibt an, ob die gegebene Zeichenkette das Ende dieser Zeichenkette darstellt.
Equals
Gibt an, ob zwei Zeichenketten gleiche Buchstabenfolgen haben.
(S)
Format
Formatierung ähnlich Write
S
GetEnumerator
»Zähler« zum Durchlaufen in einer Schleife
GetTypeCode
Gleichnamige Enumeration hat Primitive, Object, DBNull und Empty.
IndexOf
Erstes Auftreten der gegebenen Zeichenkette in dieser Zeichenkette
IndexOfAny
Erstes Auftreten der gegebenen Buchstaben in dieser Zeichenkette
Insert
Einfügen eines Teilstrings
Tabelle 10.2
S S
Methoden in »String« (S = Shared)
665
10.2
10
Einige Basisklassen
Methode
Beschreibung
Intern
Interne Referenz auf diese Zeichenkette (gegebenenfalls in Pool überführt) S
IsInterned
Referenz auf Pool-String oder Nothing
IsNormalized
Gibt an, ob die Zeichenkette in Unicode »Normalform« vorliegt.
IsNullOrEmpty
Gibt an, ob ein Leerstring oder Nothing vorliegt.
S
Join
Verbinden eines String-Arrays mit gegebenem Separator
S
LastIndexOf
Letztes Auftreten der gegebenen Zeichenkette in dieser Zeichenkette
LastIndexOfAny
Letztes Auftreten der gegebenen Zeichen in dieser Zeichenkette
Normalize
Unicode in »Normalform« konvertieren
PadLeft
Auffüllen von links mit gegebenen Zeichen, bis Länge erreicht ist
PadRight
Auffüllen von rechts mit gegebenen Zeichen, bis Länge erreicht ist
Remove
Eine gegebene Anzahl Zeichen löschen
Replace
Ersetzen von Teilstrings durch gegebene Zeichenfolgen
Split
String beim Auftreten der gegebenen Zeichen trennen
StartsWith
Gibt an, ob die gegebene Zeichenkette den Anfang dieser Zeichenkette darstellt.
Substring
Extraktion eines Teilstrings
ToCharArray
Kopie von Buchstaben in ein Char-Array
ToLower
Umwandlung in Kleinbuchstaben
ToLowerInvariant
Umwandlung in Kleinbuchstaben mit neutraler Kultur
ToUpper
Umwandlung in Großbuchstaben
ToUpperInvariant
Umwandlung in Großbuchstaben mit neutraler Kultur
Trim
Entfernen gegebener Zeichen am Rand
TrimEnd
Entfernen gegebener Zeichen am rechten Rand
TrimStart
Entfernen gegebener Zeichen am linken Rand
Tabelle 10.2
S
Methoden in »String« (S = Shared) (Forts.)
Zeichenfolgen-Vergleiche Die vier Methoden Equals, Compare, CompareTo und CompareOrdinal sind sehr ähnlich. Wie bereits in Abschnitt 6.2.2, »List«, gezeigt wurde, liefert CompareTo einen negativen Wert, wenn die Zeichenketten lexikalisch sortiert sind, und einen positiven Wert, wenn nicht. Bei Gleichheit wird Null zurückgegeben. Hinweis Standardmäßig wird bei String-Variablen zwischen Groß- und Kleinschreibung unterschieden.
Entscheidend dafür, wann ein Zeichen als größer oder kleiner im Vergleich zu einem zweiten gilt, ist die spezifische Ländereinstellung. Im mitteleuropäischen Sprachraum ist festgelegt, dass den Großbuchstaben ein größerer Wert zugeordnet ist als ihrem kleingeschriebenen Pendant. Compare vergleicht erst Buchstaben und dann die Schreibung:
666
String
a < A < b < B < c < C ... < y < Y < z < Z
CompareOrdinal vergleicht zwei Zeichenfolgen auf Basis der Zeichencodes. Sind alle iden-
tisch, gibt die Funktion Null zurück. Sonst gibt sie die Differenz der Codes des ersten unterschiedlichen Buchstabens zurück. A < B < C ... X < Y < Z ... a < b < c ... x < y < z
Das folgende Codefragment testet die beiden Vergleiche: '...\Basisklassen\KlasseString\Vergleich.vb
Option Strict On Namespace Basisklassen Module Vergleich Sub Vergleichen(ByVal fun As Func(Of String, String, Integer)) For Each s As String In New String() {"A", "a", "E", "e"} Console.Write("{0}-{1}:{2} ", "C", s, fun("C", s)) Next Console.WriteLine() End Sub Sub Test() Console.Write("Compare: ") Vergleichen(AddressOf String.Compare) Console.Write("CompareOrdinal: ") Vergleichen(AddressOf String.CompareOrdinal) Console.ReadLine() End Sub End Module End Namespace
Die Ausgabe zeigt die verschiedene Einsortierung der Kleinbuchstaben: Compare: C-A:1 C-a:1 C-E:-1 C-e:-1 CompareOrdinal: C-A:2 C-a:-30 C-E:-2 C-e:-34
Suchen in einer Zeichenkette Es gibt zwei Gruppen von Suchfunktionen. Die Methoden StartsWith, Contains und EndsWith testen, ob ein Teilstring mit einer gegebenen Zeichenfolge übereinstimmt, während IndexOf und LastIndexOf eine konkrete Position liefern, die in den Methoden Substring und Insert verwendet werden kann. Die Länge der beteiligten Zeichenketten ist beliebig, die Suchfunktionen berücksichtigen Groß- und Kleinschreibung. Das folgende Beispiel zeigt einige der Methoden im Einsatz. Die Abbruchbedingung der Schleife nutzt die Rückgabe von -1 der Methode IndexOf, wenn der Teilstring nicht gefunden wird.
667
10.2
10
Einige Basisklassen
'...\Basisklassen\KlasseString\Suchen.vb
Option Strict On Namespace Basisklassen Module Suchen Sub Test() Dim str As String = "Da wird der Hund in der Pfanne verrückt." Console.WriteLine("Start mit Da: {0}", str.StartsWith("Da")) Dim pos As Integer = –1 Do pos += 1 pos = str.IndexOf("der", pos) If pos > 0 Then Console.Write(str.Substring(pos, 7) & " -") Loop While pos >= 0 Console.WriteLine() Console.WriteLine("Rest: {0}", str.Substring(str.LastIndexOf("der"))) Console.ReadLine() End Sub End Module End Namespace
Der Satzanfang wird korrekt gefunden, und es werden jeweils 7 Buchstaben lange Teilstrings ausgegeben, die mit »der« beginnen (IndexOf liefert den Beginn der gesuchten Zeichenfolge). Die einparametrige Variante von Substring extrahiert einen Teilstring von der gegebenen Position bis zum Ende der gesamten Zeichenkette. Start mit Da: True der Hun -der Pfa Rest: der Pfanne verrückt.
Textränder bearbeiten Leerzeichen sind naturgemäß nicht sichtbar. Bei einer Texteingabe können sie insbesondere am Anfang und Ende der Eingabe leicht übersehen werden. Werden die Eingaben ungefiltert gespeichert und wird später ohne Leerzeichen danach gesucht, sind die Daten »unauffindbar«. Die unerwünschten Zusatzzeichen können Sie mit Trim, TrimStart oder TrimEnd entfernen. Public Public Public Public
Function Function Function Function
Trim() As String Trim(ParamArray trimChars As Char()) As String TrimEnd(ParamArray trimChars As Char()) As String TrimStart(ParamArray trimChars As Char()) As String
Die Überladungen der Methode entfernen auch andere Zeichen an den Textenden. Im folgenden Codefragment wird das Trennzeichen für Verzeichnisse in einer Pfadangabe entfernt, ganz ohne If-Anweisungen: Dim str As String = "C:\Temp\ " Console.WriteLine("""{0}"" -> ""{1}""", str, str.TrimEnd("\ ".ToCharArray()))
668
String
Die Ausgabe zeigt die Kürzung: "C:\Temp\ " -> "C:\Temp"
Der gegenteilige Fall sind fehlende Zeichen an den Textenden. Die Methoden PadLeft und PadRight füllen eine Zeichenkette bis zur angegebenen Länge auf. Public Public Public Public
Function Function Function Function
PadLeft(totalWidth As Integer) As String PadLeft(totalWidth As Integer, padding As Char) As String PadRight(totalWidth As Integer) As String PadRight(totalWidth As Integer, padding As Char) As String
Dies ist zum Beispiel für tabellarische Darstellungen praktisch. For Each z As Double In New Double() {-1.27, 8.24, 12.98, –100.01} Console.WriteLine(z.ToString().PadLeft(7)) Next
Die Zahlen werden rechtsbündig ausgegeben: –1,27 8,24 12,98 –100,01
Zeichenfolgen ändern Neben den drei grundlegenden Änderungsoperationen Einfügen einer Zeichenkette, Löschen von Bereichen und Ersetzen eines Teilstrings durch einen anderen gibt es Methoden, um alle Buchstaben einer Zeichenkette klein- oder großzuschreiben, wobei Kulturabhängigkeiten berücksichtigt werden können. Public Public Public Public Public
Function Function Function Function Function
Insert(startIndex As Integer, value As String) As String Remove(startIndex As Integer) As String Remove(startIndex As Integer, count As Integer) As String Replace(oldChar As Char, newChar As Char) As String Replace(oldValue As String, newValue As String) As String
Public Public Public Public Public Public
Function Function Function Function Function Function
ToLower() As String ToLower(culture As CultureInfo) As String ToLowerInvariant() As String ToUpper() As String ToUpper(culture As CultureInfo) As String ToUpperInvariant() As String
Im folgenden Codefragment nutzen wir IndexOf, um die richtige Einfügeposition in Insert für den zweiten Vornamen zu erhalten. Diesen schreiben wir danach durch Ersetzen mit Replace aus bzw. geben den Namen mit ToUpper in Großschreibung aus:
669
10.2
10
Einige Basisklassen
Dim str As String = "James Kirk" str = str.Insert(str.IndexOf("Kirk"), "T. ") Console.WriteLine("Kurzform: {0}", str) Console.WriteLine("Voller Name: {0}", str.Replace("T.", "Tiberius")) Console.WriteLine("Großgeschrieben: {0}", str.ToUpper())
Die verschiedenen Namensformen werden korrekt ausgegeben: Kurzform: James T. Kirk Voller Name: James Tiberius Kirk Großgeschrieben: JAMES T. KIRK
Daten in Zeichenketten Programme können nicht alle denkbaren Datenformate speichern, sondern nur die bei der Entwicklung berücksichtigten. So weiß zum Beispiel eine Datenbank in der Regel nichts von einer von Ihnen entwickelten Datenstruktur. Wollen Sie die Daten dennoch in zusammenhängender Form speichern, bietet sich eine Konvertierung in eine Zeichenkette an. Die einzelnen Datenfelder werden durch Zeichen getrennt, die in den Daten selbst nicht vorkommen. Ein einfaches Beispiel ist eine Textverarbeitung, die die Zeilen des Textes zusammenhängend speichert. Das Trennzeichen ist in diesem Fall ein Zeilenvorschub. Am einfachsten setzen Sie die Zeichenkette mit Join zusammen, und Sie zerlegen sie mit Split. In der folgenden Syntax sind optionale Parameter kursiv gesetzt. Public Shared Function Join(separator As String, value As String(), _ startIndex As Integer, count As Integer) As String Public Function Split(ParamArray separator As Char()) As String() Public Function Split(separator As Char(), count As Integer, _ options As StringSplitOptions) As String() Public Function Split(separator As String(), count As Integer, _ options As StringSplitOptions) As String()
Im folgenden Beispiel wird Split im Konstruktor der Klasse Person benutzt, um die Werte der einzelnen Felder zu ermitteln. Das Zusammensetzen für die Ausgabe in ToString übernimmt Join. Ich benutze die Methoden GetFields, GetValue und SetValue aus der Klasse Type, um das Listing kurz zu halten und Ihnen Anregungen für eigene Klassen zu geben. '...\Basisklassen\KlasseString\Teilen.vb
Option Strict On Namespace Basisklassen Module Teilen Class Person Public Name, Straße, Ort As String Public Sub New(ByVal p As String) Dim felder() As String = p.Split(";"c) For nr As Integer = 0 To Me.GetType().GetFields().Length – 1
670
Ausgabeformatierung
Me.GetType().GetFields()(nr).SetValue(Me, felder(nr)) Next End Sub Public Overrides Function ToString() As String Return String.Join(";", Me.GetType().GetFields(). _ Select(Function(f) CType(f.GetValue(Me), String)).ToArray()) End Function End Class Sub Test() Dim p As New Person("E. van Haar;Engelsstr. 10;42283 Wuppertal") Console.WriteLine("Person : {0}", p) Console.WriteLine("Kontrolle: {0};{1};{2}", p.Name, p.Straße, p.Ort) Console.ReadLine() End Sub End Module End Namespace
Die ausgegebenen Informationen sind identisch zur Eingabe: Person : E. van Haar;Engelsstr. 10;42283 Wuppertal Kontrolle: E. van Haar;Engelsstr. 10;42283 Wuppertal
Wollen Sie die Separatorzeichen lieber selbst einsetzen, können Sie als Alternative zum Verkettungsoperator & die Methode Concat verwenden: Public Shared Function Concat(ParamArray args As Object()) As String Public Shared Function Concat(ParamArray values As String()) As String
Zeichenfolgen und Char-Arrays Viele Methoden im .NET Framework haben Zeichenketten als Parameter, andere erwarten ein Char-Array. Daher sind Umwandlungen öfter nötig. Der Weg zur Zeichenkette führt über den Konstruktor (siehe Abschnitt 10.2.1, »Erzeugung«). Für die umgekehrte Richtung gibt es eine Methode: Public Function ToCharArray() As Char() Public Function ToCharArray(start As Integer, length As Integer) As Char()
10.3
Ausgabeformatierung
Zur Formatierung einer Ausgabe stehen Ihnen zwei Möglichkeiten zur Verfügung: 왘
die statische Methode Format der Klasse String
왘
die Methode ToString der Schnittstelle IFormatable
671
10.3
10
Einige Basisklassen
10.3.1 Formatierung mit der Methode String.Format Die in Abschnitt 2.6.1, »Ausgabe mit Write und WriteLine«, gezeigten Formatierungen erreichen die Schreibroutinen, indem sie intern die Methode String.Format aufrufen. Daher sind die beiden folgenden Aufrufe völlig gleichwertig: Console.WriteLine("{0:F}", DateTime.Now) Console.WriteLine(String.Format("{0:F}", DateTime.Now))
Formatierungsvarianten Die einfachste Variante der überladenen Format-Methode hat als ersten Parameter eine Zeichenkette, die die Formatierung beschreibt. Alle weiteren Parameter werden entsprechend den Formatangaben in geschweiften Klammern in den Formatstring eingefügt. Public Shared Function Format(format As String, ParamArray args As Object()) As String
Sie können auch festlegen, welche Sprache bzw. Kultur die Formatierung benutzt: Public Shared Function Format(provider As IFormatProvider, _ format As String, ParamArray args As Object()) As String
Die Schnittstelle IFormatProvider. wird im .NET Framework von drei Klassen im Namensraum System.Globalization implementiert: 왘
CultureInfo: Schriftsystem und verwendeter Kalender
왘
DateTimeFormatInfo: Anzeige von Datum und Uhrzeit
왘
NumberFormatInfo: Darstellung numerischer Werte abhängig von der Kultur
Um beispielsweise das aktuelle Systemdatum in italienischer Sprache auszugeben, müssen Sie nur ein entsprechendes CultureInfo-Objekt bereitstellen: Dim ci As New CultureInfo("it-IT") Console.Write(String.Format(ci, "{0:D}", Now))
' sabato 20 settembre 2003
Mit den Eigenschaften DateTimeFormat und NumberFormat der Klasse CultureInfo kann das Ausgabeformat der spezifischen Kultur abgefragt und neu festgelegt werden. Dazu veröffentlichen die beiden Klassen DateTimeFormatInfo und NumberFormatInfo eine größere Anzahl von Eigenschaften. Wie Sie das Dezimaltrennzeichen einer gegebenen Kultur abweichend vom Standard spezifisch festlegen können, zeigt das folgende Codefragment: Dim d As Double = 12.25 Dim ci As New CultureInfo("de-DE") Dim nfi As NumberFormatInfo = ci.NumberFormat nfi.NumberDecimalSeparator = "*" Console.WriteLine(String.Format(ci, "{0}", d))
672
Ausgabeformatierung
Es wird zuerst ein CultureInfo-Objekt erzeugt, das mit de-DE die deutsche Kultur beschreibt. Über dessen Eigenschaft NumberFormat wird die dazu entsprechende Referenz auf NumberFormatInfo ermittelt. Mit der Eigenschaft NumberDecimalSeparator wird anschließend das Zeichen »*« als neues Dezimaltrennzeichen festgelegt. Es kann sich als nützlich erweisen, ein kulturunabhängiges Format zur Verfügung zu stellen. Dieses erhalten Sie mit der statischen Eigenschaft InvariantCulture der Klasse CultureInfo. Das zurückgegebene CultureInfo-Objekt ist der englischen Sprache zugeordnet, ohne dabei landesspezifische Unterschiede zu berücksichtigen.
Standardformatzeichen von NumberFormatInfo Tabelle 2.6, »Standardformate für die Ausgabe«, in Abschnitt 2.6.1, »Ausgabe mit Write und WriteLine«, zeigt bereits die Formatierungsmöglichkeiten. Tabelle 10.3 fasst die Formatangaben zusammen. Formatangabe
Beschreibung
C
Lokales Währungsformat
D
Dezimaler Integer (nur ganze Zahlen)
E
Wissenschaftliches Format (Exponentialschreibweise)
F
Festpunktformat
G
Das »kompaktere« Format der Formate E und F
N
Festpunktformat einschließlich Tausenderseparatoren
P
Prozentzahl
R
Anzeigegarantie für alle Stellen (nur Single und Double)
X
Hexadezimalnotation (nur ganze Zahlen)
Tabelle 10.3
Formatcodes für Zahlen (alternativ Kleinbuchstaben)
Standardformatzeichen der Klasse DateTimeFormatInfo In Tabelle 10.4 sind die wichtigsten Standardmuster zur Formatierung von Datum und Uhrzeit aufgezählt. Maßgeblich sind auch hier die Einstellungen unter Ländereinstellung. Wie schon die Standardformatmuster der Klasse NumberFormatInfo können Sie über Eigenschaften einige der Standardmuster nach eigenen Vorstellungen ändern. Weitere Informationen entnehmen Sie auch hier der .NET-Dokumentation. Zeichen
Beschreibung
d
Kurzes Datum (22.09.2003)
D
Langes Datum (Montag, 22. September 2003)
f
Langes Datum inklusive Zeitangabe (Montag, 22. September 2003 22:30)
F
Langes Datum inklusive langer Zeitangabe (Montag, 22. September 2003 22:30:45)
g
Kurzes Datum inklusive Zeitangabe (22.09.2003 22:30)
G
Kurzes Datum inklusive langer Zeitangabe (22.09.2003 22:30:45)
Tabelle 10.4
Formatcodes für Datum und Uhrzeit
673
10.3
10
Einige Basisklassen
Zeichen
Beschreibung
M oder m
Tag und Monat (22 September)
R oder r
Datum nach dem Muster des RFC1123 (Mon, 22 Sep 2003 22:30:45 GMT)
t
Kurze Zeitangabe (22:30)
T
Lange Zeitangabe (22:30:45)
Y oder y
Monat und Jahr (September 2003)
Tabelle 10.4
Formatcodes für Datum und Uhrzeit (Forts.)
Dazu zwei Beispiele: Dim str As String str = String.Format("{0:F}", Now) str = String.Format("{0:M}", Now)
' Dienstag, 23. September 2003 12:12:55 ' 23 September
10.3.2 Formatierung mit ToString Eine Überladung der Methode ToString für Zahlen nimmt als Parameter einen Formatstring entgegen. Da nur ein Wert formatiert wird, fallen die geschweiften Klammern weg, zum Beispiel eine Fließkommazahl als Prozentzahl oder im Exponentialformat: Dim d As Double = 0.01985 Console.WriteLine(d.ToString("P")) Console.WriteLine(d.ToString("E"))
Auch hier wird die Einstellung der aktuellen Kultur berücksichtigt. Die Formatierung erfolgt mit den Formatzeichen der Klassen DateTimeFormatInfo und NumberFormatInfo. Um landesspezifische Ausgaben zu ermöglichen, ist auch die Übergabe eines IFormatProvider-Objekts möglich, zum Beispiel in Double. Function ToString(provider As IFormatProvider) As String Function ToString(format As String, provider As IFormatProvider) As String
Verschiedene Datentypen haben unterschiedlich viele Überladungen von ToString.
10.3.3 Benutzerdefinierte Formatierung Über die Standardformatierung hinaus können Sie eigene Muster festlegen.
Zahlen und Zeichenfolgen Tabelle 2.7, »Benutzerdefinierte Formate für die Ausgabe«, in Abschnitt 2.6.1, »Ausgabe mit Write und WriteLine«, zeigt bereits einige Formatierungsmöglichkeiten. Tabelle 10.5 fasst die Formatzeichen zusammen.
674
Ausgabeformatierung
Zeichen
Beschreibung
0
Nichtsignifikante Stellen werden durch die Zahl 0 dargestellt.
#
Nichtsignifikante Stellen werden durch Leerzeichen dargestellt.
.
Der erste Punkt bestimmt die Position des Dezimaltrennzeichens.
,
1) zwischen # bzw. 0: Einfügen von Tausendertrennzeichen 2) vor Dezimaltrennzeichen: je eine Division durch 1000
%
Bewirkt die Multiplikation mit 100. Das Prozentzeichen wird dargestellt.
‰
Bewirkt die Multiplikation mit 1000. Das Promillezeichen wird dargestellt.
E0, E+0, E-0, e0, e+0, e-0
Exponentialdarstellung einer Zahl. Mit E+0 und e+0 wird das positive Vorzeichen immer angezeigt, mit allen anderen immer nur das negative. Die Anzahl der Nullen bestimmt die Mindestanzahl von Ziffern des Exponenten.
"ABC", 'ABC'
Die in Anführungszeichen stehenden Zeichen werden direkt in die Ergebniszeichenfolge kopiert.
;
Gleichzeitige Formatdefinition für positive, negative und Nullwerte.
Tabelle 10.5
Formatzeichen für benutzerdefinierte Zahlen- und Zeichenformate
Hier sind einige Beispiele: Dim d As Double = 12345.6789 Console.WriteLine(d.ToString("0000000")) Console.WriteLine(d.ToString("#,#####")) Console.WriteLine(d.ToString("#.##")) Console.WriteLine(d.ToString("000e+000")) Console.WriteLine(d.ToString("0%0"))
' ' ' ' '
Ausgabe: Ausgabe: Ausgabe: Ausgabe: Ausgabe:
00123456 12.346 12345,68 123e+002 123456%8
Datums- und Zeitangaben Sie können mit vordefinierten Codes der Klasse DateTimeFormatInfo eigene Mustervorgaben zur Darstellung des Datums und der Uhrzeit festlegen (siehe Tabelle 10.6): Console.WriteLine(Now.ToString("MMM/yyyy/dd")) Console.WriteLine(DateTime.Now.ToString("HH:mm:ss"))
' Sep.2004.12 ' 09:22:30
Formatmuster
Beschreibung
d bzw. dd
Monatstag ohne bzw. mit führender 0 (1–31 bzw. 01–31)
ddd
Abkürzung des Wochentags (Mon)
dddd
Vollständiger Name des Wochentags (Montag)
M bzw. MM
Monat ohne bzw. mit führender 0 (1–12 bzw. 01–12)
MMM
Abkürzung des Monatsnamens (Jan)
MMMM
Vollständiger Monatsname (Januar)
y bzw. yy
Zweistellige Jahreszahl ohne bzw. mit führender 0 (3 bzw. 03)
yyyy
Vollständige Jahreszahl (2003)
gg
Angabe der Zeitära
h bzw. hh
Stundenangabe in 12-Stunden-Schreibweise ohne bzw. mit führender 0
Tabelle 10.6
Formatzeichen für benutzerdefinierte Datums- und Zeitformate
675
10.3
10
Einige Basisklassen
Formatmuster
Beschreibung
H bzw. HH
Stundenangabe in 24-Stunden-Schreibweise ohne bzw. mit führender 0
m bzw. mm
Minutenangabe ohne bzw. mit führender 0 (0–59 bzw. 00–59)
s bzw. ss
Sekundenangabe ohne bzw. mit führender 0 (0–59 bzw. 00–59)
f–ffffff
Angabe von Sekundenbruchteilen
t
Erstes Zeichen von AM/PM (A entspricht AM, P entspricht PM)
tt
AM oder PM
z
Zeitzonenangabe (»+« oder »–«, dann Stundenangabe; ohne führende 0)
zz
Zeitzonenangabe (»+« oder »–«, dann Stundenangabe; mit führender 0)
zzz
Vierstellige Zeitzonenangabe
/
Es wird das Standardtrennzeichen für Datumsangaben eingesetzt.
:
Es wird das Standardtrennzeichen für Zeitangaben eingesetzt.
Tabelle 10.6
10.4
Formatzeichen für benutzerdefinierte Datums- und Zeitformate (Forts.)
StringBuilder
Objekte der Klasse String sind unveränderlich, bei jeder Änderung wird ein neues Objekt generiert. Damit ist die Klasse ungeeignet, wenn Texte sehr oft geändert werden. Dafür besser geeignet ist die Klasse StringBuilder im Namensraum System.Text. Das folgende Codefragment zeigt einen Geschwindigkeitsvergleich bei häufigen Änderungen. '...\Basisklassen\KlasseStringBuilder\Vergleich.vb
Option Strict On Namespace Basisklassen Module Vergleich Sub Test() Dim t0 As Date = Now Dim ts, tb As Long Dim str As String = "" For i As Integer = 0 To 100000 : str += "." : Next ts = Now.Ticks – t0.Ticks t0 = Now Dim sb As New Text.StringBuilder() For i As Integer = 0 To 100000 : sb.Append(".") : Next tb = Now.Ticks – t0.Ticks Console.WriteLine("Verhältnis: {0}", ts / tb) Console.ReadLine() End Sub End Module End Namespace
676
StringBuilder
Die Ausgabe zeigt, dass hier StringBuilder etwa 2000-mal schneller war: Verhältnis: 1939
Die Klasse StringBuilder hat weniger Möglichkeiten als String. Insbesondere gibt es keine Methoden zur Auswertung oder Extraktion eines Teilstrings. Hinweis Die Methode ToString liefert eine Kopie der Zeichen im StringBuilder-Objekt als String.
10.4.1 Kapazität Nicht immer wird der von einem StringBuilder-Objekt belegte Speicher voll ausgenutzt. Die Anzahl der in dem Objekt speicherbaren Zeichen ist die Kapazität, die gerade gespeicherten Zeichen bestimmen die Länge. Bei Bedarf wird die Kapazität automatisch angepasst und neuer Speicher reserviert (jeweils mindestens Verdopplung). Sie können sie auch festlegen, aber sie darf niemals kleiner als die Länge sein.
10.4.2 Konstruktoren Im Konstruktor können Sie eine Initialisierungszeichenfolge und die Kapazität(en) festlegen, die keine Potenz von 2 sein müssen. Public Sub Public Sub Public Sub Public Sub Public Sub Public Sub capacity
New() New(capacity New(value As New(capacity New(value As New(value As As Integer)
As Integer) String) As Integer, maxCapacity As Integer) String, capacity As Integer) String, startIndex As Integer, length As Integer, _
Bei Überschreitung der Maximalgröße wird die Ausnahme ArgumentOutOfRangeException ausgelöst.
10.4.3 Eigenschaften Außer auf die Kapazitäten (aktuelle und maximale) und die Länge (gerade gespeicherte Zeichen) greift der Indexer auf einzelne Buchstaben zu (siehe Tabelle 10.7). Das folgende Codefragment gibt »4« aus: Dim sb As New StringBuilder("1234567") Console.Write(sb(3))
677
10.4
10
Einige Basisklassen
Eigenschaft
Methode
Capacity
Kapazität des StringBuilder-Objekts
Chars
Das Zeichen an einer Position in der Zeichenfolge (Indexer)
Length
Die Länge der Zeichenfolge
MaxCapacity
Die Maximalkapazität des StringBuilder-Objekts
Tabelle 10.7
R
Eigenschaften von »StringBuilder« (R = ReadProtected)
10.4.4 Methoden In der Klasse StringBuilder sind viel weniger Methoden als in String (siehe Tabelle 10.8). Methode
Eigenschaft
Append
Hängt an eine bestehende Instanz eine Zeichenfolge an.
AppendFormat
Fügt eine Zeichenfolge mit Formatangaben an.
AppendLine
Fügt eine Zeile hinzu.
CopyTo
Kopiert einen Teil des Objekts in ein Char-Array.
EnsureCapacity
Stellt sicher, dass die Kapazität mindestens so groß ist wie angegeben.
Insert
Fügt an einer Position eine Zeichenfolge ein.
Remove
Löscht ab einer Position eine Zeichensequenz.
Replace
Ersetzt in der gesamten Zeichenfolge ein Zeichen durch ein anderes.
Tabelle 10.8
Methoden von »StringBuilder«
Zuweisung einer Zeichenkette Statt einer direkten Zuweisung verwenden Sie Append zum Aufbau von Zeichenfolgen. Alle Überladungen sind als Public Function deklariert. Append(value Append(value Append(value Append(value Append(value
As As As As As
Basis) As StringBuilder Char()) As StringBuilder Char, repeatCount As Integer) As StringBuilder String, start As Integer, count As Integer) As StringBuilder Char(), start As Integer, count As Integer) As StringBuilder
Basis: Boolean, Byte, Char, Decimal, Double, Short, Integer, Long, Object, SByte, Single, String, UInt16, UInt32, UInt64
Durch die Rückgabe eines StringBuilder-Objekts können Sie Befehle aneinanderhängen: Dim sb As New StringBuilder() sb.Append("Domkloster ").Append(4).Append(" (Köln)")
'Domkloster 4 (Köln)
Brauchen Sie eine Zeichenkette vom Typ String, liefert ToString eine Kopie der im StringBuilder-Objekt gespeicherten Zeichen.
678
Zeitmessung
Einfügen einer Zeichenfolge Bis auf die Überladung mit dem Wiederholungszeichen hat Insert zu jeder der Überladungen von Append ein Pendant, das als zusätzliches erstes Argument die Einfügeposition angibt. Dim sb As New StringBuilder() Console.Write(sb.Append("fällt Schnee").Insert(0, "Im Winter "))
Die Ausgabe dieses Codefragments lautet: Im Winter fällt Schnee
Löschen aus einer Zeichenfolge Remove löscht ab der im ersten Argument angegebenen Position die im zweiten Argument angegebene Anzahl Zeichen. Zum Beispiel schreiben Sie zum Löschen der Zeichen 4 und 5: sbObject.Remove(3, 2)
Ersetzen eines Zeichens oder einer Zeichenfolge Zu den Ersetzungsmöglichkeiten der Klasse String kommen noch zwei Überladungen, die den Bearbeitungsbereich einschränken. Alle Überladungen sind mit Public Function deklariert. Replace(alt As Char, neu As Char) As StringBuilder Replace(alt As String, neu As String) As StringBuilder Replace(alt As Char, neu As Char, start As Integer, count As Integer) _ As StringBuilder Replace(alt As String, neu As String, start As Integer, count As Integer) _ As StringBuilder
10.5
Zeitmessung
Um ein Datum einschließlich einer Zeitangabe in einer Variablen zu speichern, deklarieren Sie die Variable vom Typ DateTime, beispielsweise: Dim datum As New DateTime(2003, 12, 6) Console.WriteLine(datum)
'Ausgabe: 06.12.2003
00:00:00
Mit einer Variablen des Typs DateTime lässt sich ein Datum zwischen dem 1. Januar 01 und dem 31. Dezember 9999 behandeln – nach dem gregorianischen Kalender. Die Grenzen sind in den klassengebundenen schreibgeschützten Feldern MinValue und MaxValue gespeichert.
10.5.1 Die Zeitspanne Tick Die Einheit der Zeitmessung ist 100 Nanosekunden. Sie wird als Tick bezeichnet. Damit braucht bereits ein Tag eine zwölfstellige Zahl zur Zählung der Ticks. Ein Long ist sehr viel größer, sodass der gesamte Bereich seit Beginn unserer Zeitrechnung bis zum Ende des
679
10.5
10
Einige Basisklassen
Jahres 9999 abdeckt werden kann (es könnten sogar knapp 30.000 Jahre sein). Die Ticks definieren einen Zeitpunkt, der dem Konstruktor übergeben werden kann. Im folgenden Codefragment addieren wir die Anzahl der Ticks in einer Sekunde zur aktuellen Tickzahl. Dim d As DateTime = Now Console.WriteLine("{0} ist eine Sekunde vor {1}", _ d, New DateTime(d.Ticks + TimeSpan.TicksPerSecond))
Die Ausgabe lautet: 30.12.2008 15:14:43 ist eine Sekunde vor 30.12.2008 15:14:44
10.5.2 Konstruktoren von DateTime Ein neuer Zeitpunkt kann entweder durch eine Tickanzahl oder durch Tages- und Zeitangaben definiert werden. Die Enumeration DateTimeKind gibt den Bezug an: unspezifiziert (Unspecified), Weltzeit (Utc) oder lokale Zeit (Local). Vordefinierte Kalender vom Typ System. Globalization.Calendar umfassen auch nah- und fernöstliche Varianten. Sie ordnen den Zeitangaben Wochen, Monate und Jahre zu, was auch die Formatierung beeinflusst. In der folgenden Syntax sind optionale Parameter kursiv gesetzt. Public Sub New(ticks As Long, kind As DateTimeKind) Public Sub New(year As Integer, month calendar As Calendar) Public Sub New(year As Integer, month hour As Integer, minute As Integer, millisecond As Integer, calendar As
As Integer, day As Integer, _ As Integer, day As Integer, _ second As Integer, _ Calendar, kind As DateTimeKind)
Eine fehlende Zeit wird durch Mitternacht ersetzt. Sind die Jahreszahlen nicht zwischen 1 und 9999 oder sind die weiteren Angaben ungültig, wird eine Ausnahme ausgelöst. Die Datumszählung beginnt jeweils bei 1, die Zeitzählung jeweils bei 0. Liegt ein DateTime-Objekt vor, lässt sich die Zeitbasis nicht mehr verändern. Sie können die Einstellung jedoch mit der Eigenschaft Kind jederzeit auslesen.
10.5.3 Eigenschaften von DateTime Für jedes Datums- und Zeitelement gibt es eine Eigenschaft: Year, Month, Day, Hour, Minute, Second und Millisecond. Den Wochentag bekommen Sie mit DayOfWeek vom Typ der gleichnamigen Enumeration. Monatsnamen sind nicht vorgesehen und müssen bei Bedarf selbst implementiert werden. Das folgende Codefragment gibt einen Zeitpunkt aus: Dim d As DateTime = Now Console.WriteLine("{0}, {1}.{2}.{3} {4}:{5}:{6},{7}", d.DayOfWeek, _ d.Day, d.Month, d.Year, d.Hour, d.Minute, d.Second, d.Millisecond)
Das Format entspricht deutschen Gepflogenheiten: Dienstag, 30.12.2008 16:5:19,405
680
Zeitmessung Now liefert die aktuelle Systemzeit und hängt damit unter Windows-Plattformen von der Ein-
stellung in der Systemsteuerung ab. Eine sehr ähnliche Eigenschaft ist UtcNow, die aus den Einstellungen in der Systemsteuerung die Greenwich Mean Time (GMT) ermittelt. Wenn auf eine Zeitangabe verzichtet werden kann, verwenden Sie die klassengebundene Eigenschaft Today, die das Datum ohne Zeitangabe liefert. Diese wird auf 00:00:00 gesetzt.
10.5.4 Methoden von DateTime Nahezu alle Methoden der Klasse lassen sich in zwei Gruppen zusammenfassen: 왘
Umwandlungen
왘
Add-Methoden, mit denen zu einem Datum eine Zeitspanne addiert oder subtrahiert wird
Umwandlungen Vier Methoden erlauben eine einfache Ausgabe eines Zeitpunkts: Dim d As New DateTime(2006, 2, 3, 5, 25, 30)
'3.Februar 2006 5:25:30
Console.WriteLine(d.ToLongDateString()) Console.WriteLine(d.ToShortDateString()) Console.WriteLine(d.ToLongTimeString()) Console.WriteLine(d.ToShortTimeString())
'Freitag, 3. Februar 2006 '03.02.2006 '05:25:30 '05:25
Formatieren Sie die Ausgabe selbst, können der Test IsLeapYear auf ein Schaltjahr und der Test IsDaylightSavingTime auf Sommerzeit nützlich sein. Ist ein Zeitpunkt als Zeichenkette gegeben, können Sie ihn mit Parse, ParseExact, TryParse und TryParseExact in ein DateTime-Objekt umwandeln.
Rechenoperationen Eine ganz einfache Operation ist der Vergleich zweier Zeitpunkte. Entweder Sie verwenden Equals, Compare, CompareTo oder Vergleichsoperatoren, zum Beispiel: Dim d As New DateTime(2006, 2, 3, 5, 25, 30) Console.WriteLine("d 2 Then str = textBox1.Lines(2)
Sie können über die Eigenschaft Lines einem Eingabefeld auch einen Inhalt zuweisen. Übergeben Sie dazu ein Array mit dem gewünschten Text. Jedes Array-Element wird dann in einer eigenen Zeile angezeigt: Dim stadt As String = "München" Dim stra As string() = New String(){"Berlin","Hamburg", stadt} textBox1.Lines = str
Wollen Sie einer mehrzeiligen Textbox mehrere Zeilen übergeben, müssen Sie nicht die Lines-Eigenschaft benutzen, sondern können der Eigenschaft Text eine Zeichenkette mit Zeilenvorschüben zuweisen: Dim str As String = "Berlin" & vbNewLine & "Hamburg" & vbNewLine & "München" textBox1.Text = str
Mehrzeilige Textfelder und die Tastatur In Forms haben die (ÿ_)- und die (Enter)-Taste eine besondere Bedeutung: Mit der (ÿ_)Taste bewegt man den Fokus von einem fokussierbaren Steuerelement zum nächsten, und mit
757
12.6
12
Die wichtigsten Steuerelemente
der Eingabetaste wird üblicherweise die OK-Schaltfläche aktiviert sowie der Ereignishandler ausgeführt, der mit dem Click-Ereignis verknüpft ist. Während der Fokuswechsel mit der (ÿ_)-Taste das Standardverhalten einer Form ist, wird mit der Eigenschaft AcceptButton der Form eine Schaltfläche (meist die OK-Schaltfläche) zur Standardfläche der Eingabetaste gemacht. Bei mehrzeiligen Textfeldern ist häufig ein anderes Verhalten dieser beiden Tasten wünschenswert. Der Anwender erwartet, dass mit der Eingabetaste ein Zeilenumbruch und mit der Tabulatortaste ein Tabstopp-Zeichen in das Steuerelement eingefügt wird. Damit die beiden Tasten diesen Erwartungen entsprechen, müssen zwei Eigenschaften des TextBoxObjekts auf True gesetzt werden: AcceptsReturn und AcceptsTab. Um mit der Einstellung AcceptsTab=True den Fokus zum nächsten Steuerelement zu verschieben, muss der Anwender nun allerdings die Tastenkombination (Strg) + (ÿ_) betätigen. Enthält das Formular keine Schaltfläche, die mit der Eigenschaft AcceptButton der Form zur Standardschaltfläche des Formulars wird, wirkt sich das Drücken der Eingabetaste unabhängig von der Einstellung der Eigenschaft AcceptsReturn immer als Zeilenumbruch aus.
12.7
Beschriftungen (Label)
Steuerelemente vom Typ Label dienen zur Beschriftung anderer Steuerelemente oder enthalten allgemeine Informationen für den Anwender. In Abbildung 12.6 konnten Sie einige Labels sehen, die zur Beschriftung der Textfelder benutzt wurden. Ein wichtiges Merkmal der Label-Steuerelemente ist, dass sie nicht fokussiert werden können. Sie reihen sich zwar in die Fokussierreihenfolge des Formulars ein, geben aber den Fokus an das folgende Steuerelement weiter. Text enthält die angezeigte Zeichenfolge. Bitte beachten Sie, dass die Eigenschaft AutoSize
standardmäßig auf True gesetzt ist. Weisen Sie zur Laufzeit dem Label eine neue Zeichenfolge zu, wird sich die Breite des Labels so weit vergrößern, bis der Text vollständig angezeigt wird. Mit AutoSize=False behält das Label zwar seine bei der Entwicklung festgelegte Breite bei, allerdings wird der Text gegebenenfalls automatisch umbrochen, was nicht gut aussieht. In solchen Fällen ist es besser, die Eigenschaft AutoEllipses=True zu setzen. Am Ende des sichtbaren Textbereichs werden dann drei Punkte (...) angehängt, um dem Anwender zu signalisieren, dass der Text nicht vollständig angezeigt werden kann. Fährt der Benutzer zur Laufzeit mit der Maus über das Label, wird in einem QuickInfo-Textfenster der gesamte Text angezeigt. Zur Darstellung eines Labels gibt es eine Reihe von Eigenschaften, unter anderem zur Anzeige eines Bildes oder zur Ausrichtung des Textes, die schon von den anderen Steuerelementen her bekannt sind (zum Beispiel Button). Darüber hinaus kann mit der steuerelementspezifischen Eigenschaft BorderStyle die Rahmenart eines Label-Steuerelements festgelegt oder ausgewertet werden. Es sind drei Einstellungen möglich: Normal, FixedSingle und Fixed3D. Der Standard ist Normal. Abbildung 12.9 zeigt die Darstellungsformen.
758
Popuptextfenster (ToolTip)
Abbildung 12.9
12.8
Die BorderStyle-Eigenschaft eines Label-Steuerelements
Popuptextfenster (ToolTip)
Viele Steuerelemente zeigen dem Anwender zur Laufzeit in einem kleinen Fenster einen kurzen erläuternden Text, wenn sich die Maus über dem Steuerelement bewegt. Diese Texte werden auch Quickinfos genannt. Diese Fähigkeit ist im Steuerelement ToolTip enthalten. Wie alle anderen Steuerelemente kann es mittels Drag&Drop auf den Forms-Designer gezogen werden. Allerdings wird das Steuerelement nicht im Entwurfsformular angezeigt. Stattdessen öffnet sich unterhalb des Windows Forms-Designers ein weiteres Fenster in der Entwicklungsumgebung – das Komponentenfach. In diesem werden alle Steuerelemente angeordnet, die zur Laufzeit keine visuelle Präsentation haben. Das ToolTip-Steuerelement ist das erste dieser Art, das Sie kennenlernen.
12.8.1 Definition von Hinweistexten Um Steuerelemente mit einem QuickInfo-Text auszustatten, wird mindestens eine ToolTipInstanz pro Form benötigt. Das ToolTip-Steuerelement kann anschließend für jede sichtbare Komponente in der Form eingesetzt werden. Sobald Sie Ihrer Form ein ToolTip-Steuerelement hinzugefügt haben, wird jede Komponente im Eigenschaftsfenster um eine Eigenschaft ergänzt. Angenommen, das ToolTip-Steuerelement heißt toolTipXY, dann lautet der Eintrag: ToolTip auf toolTipXY
In der Wertespalte tragen Sie anschließend den Text ein, der für das entsprechende Steuerelement angezeigt werden soll. Soll der Text mehrzeilig sein, öffnen Sie über die Pfeilschaltfläche nur ein kleines Zusatzfenster (siehe Abbildung 12.10). Sie können das ToolTip-Steuerelement auch mittels Programmcode zur Laufzeit einem anderen Steuerelement zuordnen. Dazu rufen Sie die Methode SetToolTip auf und übergeben dem ersten Parameter den Bezeichner des zuzuordnenden Steuerelements und dem zweiten Parameter den anzuzeigenden Text, z. B.: tooltip1.SetToolTip(textBox1, "Geben Sie Ihren Namen ein")
Eigentlich ist das bereits alles. Fährt der Anwender zur Laufzeit mit dem Mauszeiger über das Steuerelement – hier eine Textbox –, wird der Text in einem kleinen rechteckigen Kästchen mit hellgelbem Hintergrund für eine bestimmte Zeitspanne angezeigt.
759
12.8
12
Die wichtigsten Steuerelemente
Abbildung 12.10
Text an ToolTip zuweisen
Möchten Sie vielleicht kein rechteckiges Kästchen, sondern bevorzugen vielmehr einen Ballon? Kein Problem: Stellen Sie die Eigenschaft IsBalloon=True ein. Vielleicht sollte auch noch ein kleines Symbol den QuickInfo-Text zieren? Mit der Eigenschaft ToolTipIcon können Sie zwischen None, Info, Warning und Error wählen. Über ToolTipTitle können Sie darüber hinaus sogar einen Titel einblenden, der neben dem Symbol fett dargestellt wird. UseAnimation und UseFading beeinflussen das Ein- und Ausblenden des Informationstextes. Beide sind auf True voreingestellt. Sollten Ihnen die vom System vorgegebenen Standardfarben für den Hintergrund und die Schrift nicht zusagen, können Sie nun auch noch mit BackColor und ForeColor eine eigene Auswahl treffen. Abbildung 12.11 zeigt eine Gestaltung eines Tooltips.
Abbildung 12.11
QuickInfo-Text als Ballon mit Symbol und Titel
12.8.2 Anzeigedauer der Texte QuickInfo-Texte sind Ihnen sicherlich schon häufig begegnet. Zwischen dem Positionieren des Mauszeigers auf einem Steuerelement und der Anzeige des Textes vergeht eine kleine Zeitspanne. Diese beträgt standardmäßig 0,5 Sekunden. Nach fünf Sekunden (Voreinstellung) wird die Anzeige automatisch ausgeblendet – vorausgesetzt, der Mauszeiger bewegt sich in dieser Zeit nicht. Dieses Verhalten wird von insgesamt vier Eigenschaften bestimmt. InitialDelay ist die Verzögerung in Millisekunden, die bis zum Aufruf des QuickInfo-Textes verstreicht, AutoPopDelay die maximale Anzeigedauer, ebenfalls in Millisekunden. Bewegt
760
Auswahllisten (ListBox)
sich der Mauszeiger zu einem anderen Steuerelement, das ebenfalls einen Quickinfo-Text unterstützt, dauert es zwischen den beiden Anzeigen die unter ReshowDelay angegebene Zeitdauer, bis das nächste Hilfstextfenster geöffnet wird. Alle drei Zeiten können nur dann individuell eingestellt werden, wenn AutomaticDelay keinen positiven Wert aufweist. Wie im Falle eines von 0 abweichenden Wertes die anderen Eigenschaften beeinflusst werden, entnehmen Sie der Tabelle 12.7. Eigenschaft
Wert
AutomaticDelay
500 Millisekunden
InitialDelay
= AutomaticDelay
ReshowDelay
= AutomaticDelay / 5
AutoPopDelay
= 10 × AutomaticDelay
Tabelle 12.7
Zeiten in Abhängigkeit von »AutomaticDelay«
12.8.3 Weitere Eigenschaften Zwei weitere Eigenschaften wirken sich auf die Anzeige des Quickinfo-Textes aus. Indem Sie Active=False setzen, unterbinden Sie die Anzeige aller QuickInfo-Texte, die mit diesem Steuerelement verbunden sind. Mit ShowAlways=True wird der Info-Text auch dann angezeigt, wenn das übergeordnete Formular des Steuerelements aktuell nicht aktiv ist.
12.9
Auswahllisten (ListBox)
Oft ist es erforderlich, dem Benutzer zur Laufzeit eine Auflistung von Elementen in Form von Zeichenfolgen anzubieten. Aus dieser Liste kann der Benutzer ein oder auch mehrere Elemente auswählen. .NET stellt zu diesem Zweck mehrere Steuerelemente zur Verfügung, von denen die drei wichtigsten 왘
ListBox
왘
CheckedListBox
왘
ComboBox
sind. In diesem Abschnitt werden wir uns zunächst der Klasse ListBox widmen, die beiden anderen werden danach erläutert.
12.9.1 Daten einer ListBox Die von einer ListBox verwalteten Listeneinträge sind Elemente einer Auflistung vom Typ ListBox.ObjectCollection, auf die über die Eigenschaft Items zugegriffen werden kann. Wenn wir der Listbox ein Element hinzufügen wollen, ein bestimmtes Element aus ihr entfernen wollen oder nach einem bestimmten Element suchen, greifen wir auf die Methoden dieser Auflistung zurück.
761
12.9
12
Die wichtigsten Steuerelemente
Neue Listenelemente Manchmal ist es möglich, eine ListBox bereits zur Entwicklungszeit vollständig zu füllen. Sie brauchen in diesem Fall im Eigenschaftsfenster nur die Eigenschaft Items zu markieren und können über die Schaltfläche in der Wertespalte einen Dialog öffnen, in dem Sie die Listenelemente der Reihe nach eintragen. Meistens allerdings werden Sie Listen erst zur Laufzeit dynamisch füllen können, um sich unterschiedlichen Umgebungsbedingungen anzupassen. Als Mitglieder einer Collection werden Listenelemente über Indizes verwaltet. Das erste Listenelement hat, wie üblich, den Index 0. Drei Methoden ermöglichen es Ihnen, ein Element einer Listbox hinzuzufügen: Add, AddRange und Insert. Betrachten wir zuerst die Add-Methode, deren Rückgabewert der Index ist, an dem das Element hinzugefügt worden ist. Das Element, das hinzugefügt werden soll, ist vom Typ Object und damit beliebig. Angezeigt wird der Rückgabewert der Methode ToString. Im folgenden Codefragment werden mit der Add-Methode die drei Elemente Franz, Joseph und Uwe hinzugefügt. Dim listBox1 As New ListBox(); listBox1.Items.Add("Franz") listBox1.Items.Add("Joseph") listBox1.Items.Add("Uwe")
Eine Alternative zum sich wiederholenden Add-Aufruf bietet die Methode AddRange, die ein Objekt-Array erwartet: listBox1.Items.AddRange(New String() {"Franz", "Joseph", "Uwe"})
Soll ein neues Element nicht an das Ende angehängt werden, sondern eine bestimmte Position innerhalb aller Listenelemente einnehmen, verwenden Sie die Methode Insert. Dem ersten Parameter wird der gewünschte Index des hinzuzufügenden Elements übergeben, dem zweiten Parameter das Element. Die folgende Anweisung fügt Peter mit dem Index 1 an der zweiten Position der Listbox ein: listBox1.Items.Insert(1, "Peter")
Wenn der Index größer ist als die Anzahl der Elemente in der Listbox, kommt es zu der Ausnahme ArgumentOutOfRangeException. Listenelemente können mit Sorted auch alphabetisch sortiert werden. Sorted ist standardmäßig auf False eingestellt. Fügen Sie ein Element in eine sortierte Liste ein, wird das neue Element direkt einsortiert.
Hinzufügen vieler Listenelemente Die Methode Add hat im Vergleich zur AddRange-Methode den Nachteil, dass die Listbox mit jedem hinzugefügten Listenelement neu aufgebaut wird. Handelt es sich um eine größere Elementanzahl, hat das einen spürbaren Leistungsverlust zur Folge. Besonders deutlich wird der Effekt in Kombination mit Sorted=True. Mit der Methode BeginUpdate lässt sich das Aktuali-
762
Auswahllisten (ListBox)
sieren einer Listbox so lange unterdrücken, bis EndUpdate aufgerufen wird. Der Leistungsgewinn kann unter Umständen drastisch sein. Die folgenden Anweisungen demonstrieren den Einsatz der beiden Methoden. Für die Dauer des Schleifendurchlaufs wird der Mauscursor als Sanduhr angezeigt, um zu signalisieren, dass die Anwendung eine länger andauernde Operation ausführt. listBox1.Sorted = True listBox1.BeginUpdate() Me.Cursor = Cursors.WaitCursor For i As Integer = 0 To 999 listBox1.Items.Add(i) Next listBox1.EndUpdate() Me.Cursor = Cursors.Default
Den Effekt können Sie sehr gut beobachten, wenn Sie die Aufrufe von BeginUpdate und EndUpdate auskommentieren. Der Aufbau der Listbox benötigt dann ein Vielfaches der Zeit.
Löschen eines Elements Jedes Element in einer Listbox kann durch den in der Listbox angezeigten Text und durch seinen Index beschrieben werden. Analog hat ListBox.ObjectCollection zwei Methoden, um ein bestimmtes Element aus der Auflistung zu entfernen: Remove und RemoveAt. Remove erwartet als Argument die Referenz auf ein Objekt und wertet wiederum dessen ToString-Rückgabe aus; RemoveAt nimmt den Index entgegen. Wird ein Element mitten aus der Liste entfernt, verschieben sich alle Nachfolgeindizes, denn eine Auflistung kann niemals eine unbesetzte Position enthalten. listBox1.Items.Remove("Joseph")
Clear löscht alle Listenelemente gleichzeitig.
Doppeleinträge vermeiden Die Listenelemente einer Listbox sind nicht eindeutig – Sie können dasselbe Element auch mehrfach hinzufügen. Um das zu vermeiden, muss ein hinzuzufügendes Element zuerst daraufhin überprüft werden, ob es bereits eingetragen ist. Die Information darüber liefert die Methode Contains unter Angabe des Objekts. Der Rückgabewert ist vom Typ Boolean und True, wenn das im Parameter genannte Objekt in der Liste enthalten ist. If Not listBox1.Items.Contains("Franz") Then listBox1.Items.Add("Franz")
Position eines Listenelements Die Methode IndexOf() liefert die Position eines Elements in der Listbox. Dim index As Integer = listBox1.Items.IndexOf("Marokko")
Ist das angegebene Element in der Liste nicht enthalten, ist der Rückgabewert -1.
763
12.9
12
Die wichtigsten Steuerelemente
12.9.2 Darstellung einer Listbox Jetzt wissen Sie, wie die Elemente einer Listbox in der speziellen Auflistung ListBox.ObjectCollection verwaltet werden. Wir können uns nun den Eigenschaften der ListBox zuwen-
den, die die Darstellung zur Laufzeit kontrollieren. Die Höhe eines Steuerelements kann grundsätzlich beliebig eingestellt werden. Bei einer ListBox kann das dazu führen, dass ein Listenelement nur teilweise angezeigt wird. Mit der
Eigenschaft IntegralHeight wird das vermieden. Diese Eigenschaft ist auf True voreingestellt und bewirkt, dass sich die Höhe der Listbox automatisch so anpasst, dass alle Elemente vollständig angezeigt werden. Läuft die Liste über und können nicht alle Elemente gleichzeitig angezeigt werden, wird zur Laufzeit automatisch eine vertikale Bildlaufleiste eingeblendet. Insbesondere bei Listboxen, die zur Laufzeit dynamisch gefüllt werden, möchten Sie vielleicht auch dann eine Bildlaufleiste anzeigen, wenn die Anzahl der Listenelemente das Steuerelement noch nicht vollständig ausfüllt. Stellen Sie dazu die Eigenschaft ScrollAlwaysVisible=True ein. Überschreitet die Breite des Listeneintrags die Breite der Listbox, ist der nicht sichtbare Teil rechts abgeschnitten. In diesem Fall kann mit der Eigenschaft HorizontalScrollbar eine horizontale Bildlaufleiste angezeigt werden.
Mehrspaltige Listboxen Unabhängig davon, wie viele Elemente in der Auflistung enthalten sind, ist eine Listbox zunächst immer nur einspaltig. In Abbildung 12.12 enthält die Listbox alle auf dem aktuellen System installierten Schriftarten, die über die Liste InstalledFontCollection im Namensraum System.Drawing.Text bereitgestellt werden. Um die Listbox beim Laden des Formulars zu füllen, eignet sich das Load-Ereignis oder der Konstruktor der Form: Private Sub Fonts_Load(ByVal sender As Object, ByVal e As EventArgs) _ Handles MyBase.Load Dim fonts As New System.Drawing.Text.InstalledFontCollection() Dim familie As FontFamily() = fonts.Families For Each f As FontFamily In familie Art.Items.Add(f.Name) Next ... End Sub
Die Eigenschaft Families eines InstalledFontCollection-Objekts liefert ein Array vom Typ FontFamily zurück. Dieser Typ definiert eine Schriftart und veröffentlicht die Eigenschaft Name, die den Bezeichner des Fonts liefert. In der Schleife werden die Namen aller Schriftarten mit der Add-Methode zur Items-Auflistung hinzugefügt. Wenn Sie möchten, können Sie die Listenelemente auch auf mehrere Spalten aufteilen. Die Verteilung der Spalten erfolgt dabei so, dass keine vertikalen Bildlaufleisten mehr notwendig sind, sondern nur noch eine horizontale, die die Navigation zwischen den Spalten ermöglicht. Dazu muss die Eigenschaft MultiColumn=True gesetzt werden. Eine Listbox mit mehreren Spalten sehen Sie in Abbildung 12.13.
764
Auswahllisten (ListBox)
Abbildung 12.12
Anzeige aller installierten Fonts in einer Listbox
Abbildung 12.13
Mehrspaltige Anzeige in einer Listbox
Die Spaltenbreite können Sie mit ColumnWidth kontrollieren. Aber seien Sie hier vorsichtig. Wenn Sie nämlich eine zu geringe Breite wählen, wird der »überhängende« Teil von der Folgespalte überdeckt. Der Standardwert von 0 legt eine vordefinierte Breite fest, die allerdings nicht die Lesbarkeit aller Elemente sicherstellt.
12.9.3 Einfach- und Mehrfachauswahl Standardmäßig kann in einer Listbox zur Laufzeit nur ein Eintrag ausgewählt werden. Soll der Anwender gleichzeitig mehrere Einträge markieren können, legen Sie die Eigenschaft SelectionMode der Listbox entsprechend fest. Diese Eigenschaft bezieht ihre Werte aus der gleichnamigen Enumeration, die die vier Mitglieder in Tabelle 12.8 hat. Konstante
Selektion
None
Es kann kein Listenelement selektiert werden.
One
Nur ein Listenelement kann selektiert werden.
MultiSimple
Jede Auswahl eines Listenelements verändert die Selektion um genau ein Element.
MultiExtended
Durch Druck auf die (Strg)-Taste oder die Umschalttaste ergänzen Sie eine Auswahl eines Listenelements um eine bereits vorhandene Selektion.
Tabelle 12.8
Die Enumeration »SelectionMode«
Bei der Einstellung MultiSimple kann der Anwender mittels Mausklick in beliebiger Reihenfolge die Elemente auswählen, die dann invertiert dargestellt werden. Zudem kann er mit den Pfeiltasten durch die Liste navigieren und mit der Leertaste das fokussierte Element selektie-
765
12.9
12
Die wichtigsten Steuerelemente
ren. Ein Mausklick oder das Drücken der Leertaste auf ein ausgewähltes Element hebt die Markierung des Elements wieder auf. Die Auswahl MultiExtended ist dann vorteilhaft, wenn der Anwender einen Bereich aufeinanderfolgender Elemente auswählen soll. Wird ein Element mit der Maus ausgewählt und danach die Umschalttaste gedrückt, sind nach dem Klick auf ein zweites Listenelement alle Elemente zwischen diesen beiden automatisch selektiert. Ähnlich kann auch die Auswahl mit der Tastatur erweitert werden, indem man die Umschalttaste beim Drücken der Pfeiltasten gedrückt hält. Bei gleichzeitigem Druck auf die (Strg)-Taste kann die Selektion um genau ein Element verändert werden. So können auch nicht zusammenhängende Bereiche selektiert werden.
12.9.4 Listboxen mit Einfachauswahl Verwenden Sie eine Listbox, die nur die Auswahl eines Elements zulässt, gestaltet sich der Zugriff auf das entsprechende Element anders als bei einer Listbox mit Mehrfachauswahl. Betrachten wir zuerst Listboxen, deren Eigenschaft SelectionMode auf One eingestellt ist. Am einfachsten ist es, die Eigenschaft Text der Listbox abzurufen, zum Beispiel: MessageBox.Show(listBox1.Text)
Nahezu gleichwertig können Sie auch über SelectedItem auf das ausgewählte Listenelement zugreifen, allerdings ist der Rückgabewert dieser Eigenschaft nicht vom Typ String, sondern die Referenz auf das Objekt. Dies macht unter Umständen eine Konvertierung mit ToString() bei dessen Verwendung erforderlich. MessageBox.Show(listBox1.SelectedItem.ToString())
Eine dritte Möglichkeit bietet sich mit der Eigenschaft Items, die die Referenz auf eine ListBox.ObjectCollection liefert. Die Auflistung stellt einen Indexer bereit, dem der Index des ausgewählten Elements übergeben wird. Weil der Indexer den Typ Object ausgibt, muss die Rückgabe gegebenenfalls in eine Zeichenfolge umgewandelt werden: MessageBox.Show(listBox1.Items(12).ToString())
Die Indexangabe darf natürlich nicht statisch codiert werden, weil sie sich zur Laufzeit abhängig von der Wahl des Anwenders ändert. Hier hilft uns die Eigenschaft SelectedIndex der Listbox weiter, die den Index des ausgewählten Elements bereitstellt. Die Anweisung zur Auswertung des ausgewählten Elements über den Indexer der Auflistung muss daher so lauten: MessageBox.Show(listBox1.Items(listBox1.SelectedIndex).ToString())
SelectedIndex ruft nicht nur den Index des ausgewählten Elements in einer Listbox ab, son-
dern kann ihn auch festlegen. Das ist sehr nützlich, denn standardmäßig wird nach dem Laden und Anzeigen eines Formulars kein Listenelement vorselektiert.
Das Ereignis SelectedIndexChanged In einer Listbox führt sehr häufig das Anklicken eines Listenelements zur sofortigen Verarbeitung. Statt Click wird das Ereignis SelectedIndexChanged ausgelöst. Sehen wir uns das an einem Beispiel an, dessen Form zwei Listboxen enthält (siehe Abbildung 12.14). 766
Auswahllisten (ListBox)
Abbildung 12.14
Form des Beispiels »Einfachauswahl«
In der linken Listbox wird die Liste aller installierten Fonts angezeigt, in der rechten hat der Anwender die Auswahl der Schriftgröße. Die Änderung der Auswahl in einer der beiden Listboxen bewirkt in der Textbox die Anpassung der Zeichenfolge. '...\WinControls\Selektionen\Einfachauswahl.vb
Public Class Einfachauswahl Private Sub Laden(ByVal sender As Object, ByVal e As EventArgs) _ Handles MyBase.Load Dim fonts As New System.Drawing.Text.InstalledFontCollection() Dim familie As FontFamily() = fonts.Families ' Listbox mit den installierten Schriftarten füllen For Each f As FontFamily In familie.Where( _ Function(family) family.IsStyleAvailable(FontStyle.Regular)) Art.Items.Add(f.Name) Next ' Listbox mit einer Auswahl an Schriftgrößen füllen For Each no As Integer In Enumerable.Range(8, 17) Größe.Items.Add(no) Next ' Vorauswahl in den Listboxen Art.SelectedIndex = 0 Größe.SelectedIndex = Größe.Items.IndexOf(10) SelectedIndexChanged(Nothing, Nothing) End Sub Private Sub SelectedIndexChanged(sender As Object, e As EventArgs) _ Handles Art.SelectedIndexChanged, Größe.SelectedIndexChanged If Not Art.Text.Equals("") AndAlso Not Größe.Text.Equals("") Then _ Beispieltext.Font = New Font(Art.Text, Convert.ToSingle(Größe.Text)) End Sub End Class
Zuerst werden die beiden Listboxen im Load-Ereignishandler der Form gefüllt. Die Elemente, die in der Listbox Art angeboten werden, beziehen wir aus der Auflistung InstalledFont-
767
12.9
12
Die wichtigsten Steuerelemente Collection. Anders als weiter oben gezeigt, sollen nur Schriften erscheinen, die auch »nor-
mal« (das heißt zum Beispiel nicht fett oder kursiv) verwendet werden können. Ohne diese Filterung kommt es zu einem Laufzeitfehler, wenn eine Schriftart gewählt wird, die keinen »normalen« Schriftschnitt unterstützt, wie zum Beispiel Monotype Corsiva. Damit nicht immer die gleiche Art einer If-Abfrage auftaucht, verwende ich die Methode Where zur Selektion. Die Methode IsStyleAvailable wird zur Prüfung verwendet. familie.Where(Function(family) family.IsStyleAvailable(FontStyle.Regular))
Weil in einer Listbox nach dem Start der Anwendung zunächst kein Element vorselektiert ist, sollten wir das per Programmcode für beide Listboxen vorgeben: Art.SelectedIndex = 0 Größe.SelectedIndex = Größe.Items.IndexOf(10)
Die Ereignisse SelectedIndexChanged der beiden Listboxen sind mit dem gemeinsamen Handler SelectedIndexChanged verknüpft, der die Schrift in der Textbox an die Auswahl anpasst. Bitte beachten Sie die If-Abfrage in der Methode, bevor der neue Font zugewiesen wird. Vor der Spezifikation einer konkreten Selektion der Listboxen haben die Text-Eigenschaften den Wert "", der weder ein gültiger Font noch eine gültige Größe ist. Ohne diesen Test kommt es zu einem Laufzeitfehler. Alternativ können Sie statt der Handles-Klausel des Ereignishandlers die Registrierung mit AddHandler vornehmen, wenn diese nach der Festlegung einer Selektion erfolgt. Im Ereignishandler müssen wir nur die Font-Klasse mit einem passenden Konstruktor instanziieren. Geeignet ist in unserem Fall der Konstruktor, der die Zeichenfolge der Schriftart und die Größe der Schrift erwartet. Das neue Font-Objekt wird der Eigenschaft Font des Textfeldes zugewiesen. Dabei müssen wir allerdings noch den Übergabewert an den zweiten Parameter in den erwarteten Typ Single konvertieren: Beispieltext.Font = New Font(Art.Text, Convert.ToSingle(Größe.Text))
Die letzte Anweisung im Load-Ereignishandler ist der Aufruf der Methode SelectedIndexChanged. Damit stellen wir sicher, dass nach dem Starten die Schriftart in der Textbox der Vor-
einstellung in den Listboxen entspricht.
12.9.5 Listboxen mit Mehrfachauswahl Mit der Eigenschaft SelectionMode kann die einfache oder erweiterte Mehrfachauswahl zugelassen werden. Listenelemente mit Mehrfachauswahl verwalten die ausgewählten Listenelemente in zwei Auflistungen: 왘
SelectedObjectCollection
왘
SelectedIndexCollection.
Der Zugriff erfolgt über die Eigenschaften SelectedIndices und SelectedItems. Beide enthalten die ausgewählten Elemente der Listbox, zu denen sie eine Beziehung entweder über die Referenz oder über den Index herstellen.
768
Auswahllisten (ListBox)
Angenommen, Peter, Uwe und Willi bilden in dieser Reihenfolge die Liste der Listbox. Sind Uwe und Willi ausgewählt, enthält SelectedObjectCollection im ihrem ersten, nullindizierten Element den Verweis auf Uwe und im zweiten, mit 1 indizierten Element den Verweis auf Willi. Analog speichert SelectedIndexCollection die Indizes 0 und 1. Beide Auflistungen verfügen über die üblichen Eigenschaften und Methoden von Auflistungen, beispielsweise Count, Contains, IndexOf, sowie über einen Indexer, um auf ein bestimmtes Element zuzugreifen. Sehen wir uns an einem Beispiel an, wie ein Listenelement mit Mehrfachauswahl im Programmcode behandelt wird. Die in Abbildung 12.15 gezeigte Form enthält dazu zwei Listboxen, die beide eine Mehrfachauswahl mit SelectionMode=MultiSimple zulassen. In der linken wird nach dem Start der Anwendung eine Namensliste angezeigt. Der Benutzer kann mehrere Einträge in der linken Listbox auswählen und durch Klicken auf den oberen Button die ausgewählten Einträge in die rechte Listbox verschieben. Die markierten und verschobenen Einträge müssen danach in der Ursprungslistbox gelöscht werden. Der umgekehrte Vorgang, das Verschieben aus der rechten in die linke Listbox, verläuft analog.
Abbildung 12.15
Form des Beispiels »Mehrfachauswahl«
'...\WinControls\Selektionen\Mehrfachauswahl.vb
Public Class Mehrfachauswahl Private Sub Laden(ByVal sender As Object, ByVal e As EventArgs) _ Handles MyBase.Load Links.Items.AddRange(New String() {"Peter", "Uwe", _ "Michael", "Reiner", "Brigitte", "Thea", "Jackie"}) End Sub Private Sub Verschieben(ByVal von As ListBox, ByVal nach As ListBox) Dim elems(von.SelectedItems.Count – 1) As String von.SelectedItems.CopyTo(elems, 0) nach.Items.AddRange(elems) ' ausgewählte Elemente in Quelle löschen For i As Integer = von.SelectedItems.Count – 1 To 0 Step –1 von.Items.RemoveAt(von.SelectedIndices(i)) Next End Sub
769
12.9
12
Die wichtigsten Steuerelemente
Private Sub NachRechts_Click(sender As Object, e As EventArgs) _ Handles NachRechts.Click Verschieben(Links, Rechts) End Sub Private Sub NachLinks_Click(sender As Object, e As EventArgs) _ Handles NachLinks.Click Verschieben(Rechts, Links) End Sub End Class
Im Ereignishandler des Load-Ereignisses der Form wird die linke Listbox gefüllt. Die Ereignishandler zu den Click-Ereignissen der Schaltflächen gleichen sich bis auf die Bezeichner, sodass wir die Funktionalität in der Methode Verschieben() zusammenfassen. Die Ziel-Listbox soll mit der AddRange-Methode gefüllt werden. AddRange verlangt als Übergabeargument ein Objekt-Array, das wir uns vorher besorgen müssen. Deshalb deklarieren wir zuerst ein Array und initialisieren dieses mit einer Kapazität, die es erlaubt, es mit den ausgewählten Listenelementen aus der Ausgangslistbox füllen zu können. Die Array-Größe resultiert aus der Anzahl der ausgewählten Elemente, die in der Eigenschaft SelectedItems vom Typ SelectedObjectCollection gespeichert ist. Die Count-Eigenschaft der Klasse liefert die Anzahl. Dim elems(von.SelectedItems.Count – 1) As String
Mit von.SelectedItems.CopyTo(elems, 0)
kopieren wir den Inhalt der Auflistung SelectedObjectCollection in das zuvor deklarierte Array elems, beginnend beim Index 0. Dieses Array übergeben wir der AddRange-Methode. Nun müssen wir die ausgewählten und bereits kopierten Listenelemente in der Listbox auch löschen. SelectedIndices der Quell-Listbox liefert uns als Rückgabewert die Indizes der ausgewählten Elemente. Diesen Rückgabewert übergeben wir der Methode RemoveAt der Listbox, die in einer Schleife für jedes markierte Listenelement aufgerufen wird: For i As Integer = von.SelectedItems.Count – 1 To 0 Step –1 von.Items.RemoveAt(von.SelectedIndices(i)) Next
Achten Sie darauf, die Schleife ausgehend vom höchsten Zählerwert (der der Anzahl der ausgewählten Elemente minus 1 entspricht) bis 0 zu durchlaufen. Damit wird sichergestellt, dass zuerst das markierte Element mit dem höchsten Index aus der Listbox gelöscht wird und erst zum Schluss das mit dem kleinsten Index. Sie vermeiden so Fehler, die sich aus der Verschiebung der Indizes ergeben, denn auch die Listbox lässt keine unbesetzten Indizes zu und verschiebt alle Nachfolgeelemente um eine Position, um eine entstandene Lücke zu schließen. Die Übergabe des Schleifenzählers an SelectedIndices setzt aber eine konstante Zuordnung voraus, um einen logischen Fehler zu vermeiden.
770
Auswahllisten (ListBox)
12.9.6 Benutzerdefinierte Sortierung Wenn Sie die Methode Sort auf die Listenelemente einer Listbox anwenden, werden die sichtbaren Einträge alphabetisch sortiert. Eine Alternative bietet sich nicht an, da die Methode keine entsprechenden Überladungen hat. Manchmal ist es jedoch erforderlich, eine andere Sortierreihenfolge festzulegen. Angenommen, in einem ListBox-Objekt sollen Objekte vom Typ Person angezeigt werden. Public Class Person Public Zuname As String Public Vorname As String Public Alter As Integer Public Sub New(zuname As String, vorname As String, alter As Integer) Me.Zuname = zuname Me.Vorname = vorname Me.Alter = alter End Sub Public Overrides Function ToString() As String Return Me.Zuname & " / " & Me.Vorname & " / " & Me.Alter End Function End Class
In der Klasse sind drei Felder öffentlich deklariert. Außerdem ist die Methode ToString() überschrieben, weil die Listbox bei der Übergabe von Referenzen immer den Rückgabewert von ToString() anzeigt. Die Überschreibung stellt sicher, dass zuerst der Zuname, dann der Vorname und zum Schluss das Alter für jeden Eintrag angezeigt werden. Nun sollen die Elemente in der Listbox nach einem der drei Felder sortiert werden. Die Sorted-Eigenschaft der Listbox leistet das nicht. Wir müssen einen anderen Weg gehen. Dabei hilft uns die überladene Methode Sort der ArrayList weiter, die uns die Möglichkeit gibt, eine Vergleichsklassenreferenz vom Typ IComparer zu übergeben. Public Overridable Sub Sort(ByVal comparer As IComparer)
'Klasse ArrayList
Eine Vergleichsklasse bereitzustellen, ist kein großes Problem. Eine Hürde ist vielmehr, dass die Methode Sort auf die Referenz einer ArrayList aufgerufen wird, wir jedoch eine Listbox benutzerdefiniert sortieren wollen. Auch über diese Schwierigkeit hilft uns die Klasse ArrayList hinweg: Sie veröffentlicht die statische Methode Adapter, der die Referenz auf ein IList-Objekt übergeben wird. Public Shared Function Adapter(ByVal list As IList) As ArrayList
Der Zufall will es so, dass die von der Eigenschaft Items zurückgelieferte Referenz vom Typ ListBox.ObjectCollection genau diese Schnittstelle implementiert. Damit hätten wir alle notwendigen Informationen gesammelt, die uns in die Lage versetzen, eine benutzerdefinierte Sortierung der Listboxelemente anzubieten. Das folgende Beispielprogramm zeigt den Programmcode. Die Form in Abbildung 12.16 enthält neben einer Listbox auch drei Schaltflächen. Je nachdem, welche Schaltfläche der Benutzer anklickt, wird die Ausgabe in der Listbox nach einem der drei Felder sortiert.
771
12.9
12
Die wichtigsten Steuerelemente
Abbildung 12.16
Form des Beispiels »Sortierung«
'...\WinControls\Selektionen\Sortierung.vb
Public Class Sortierung Public Class Person ... End Class Private sortiert As ArrayList Private Class Sortierer : Implements IComparer Private feld As System.Reflection.FieldInfo Sub New(ByVal feld As String) Me.feld = GetType(Person).GetField(feld) End Sub Public Function Compare(x As Object, y As Object) As Integer _ Implements System.Collections.IComparer.Compare Return feld.GetValue(x).CompareTo(feld.GetValue(y)) End Function End Class Private Sub Laden(ByVal sender As Object, ByVal e As EventArgs) _ Handles MyBase.Load Dim pers As Person() = New Person() { _ New Person("Müller", "Peter", 30), _ New Person("Fischer", "Helmut", 56), _ New Person("Popalowski", "Fred", 22), _ New Person("Heinrich", "Walter", 29), _ New Person("Meier", "Uwe", 12) _ } ' das Array der Listbox übergeben Personen.Items.AddRange(pers) sortiert = ArrayList.Adapter(Personen.Items) sortiert.Sort(New Sortierer("Zuname")) End Sub
772
Auswahllisten (ListBox)
Private Sub Sortieren(ByVal sender As Object, ByVal e As EventArgs) _ Handles Zuname.Click, Vorname.Click, Alter.Click sortiert.Sort(New Sortierer(CType(sender, Button).Name)) End Sub End Class
12.9.7 Auswahlliste mit Datenquelle (DataSource) Die Add-Methode des Objekts ObjectCollection fügt Listenelemente hinzu. Alternativ weisen Sie der Eigenschaft DataSource eine Referenz auf die Daten der Listbox zu. Eine Bedingung dafür ist, dass das Objekt die Schnittstelle IList implementiert. Üblicherweise wird diese Eigenschaft dazu benutzt, um beispielsweise ein ADO.NET-Objekt vom Typ DataSet zu befüllen. Wir werden uns ADO.NET an späterer Stelle in diesem Buch widmen. Die Vorgehensweise ist bei einem ArrayList-Objekts ganz analog. Haben Sie die Daten der Listbox mit DataSource festgelegt, müssen Sie noch deren Verwendung festlegen: 왘
DisplayMember spezifiziert den Namen der Eigenschaft, die angezeigt wird.
왘
DisplayValue spezifiziert den Namen der Eigenschaft, auf den sich SelectedValue
bezieht. Machen wir uns das an einem konkreten Beispiel deutlich: Public Class Wohnung Private flaeche As Double Private preis As Single Private zimmeranzahl As Integer Public Sub New(flaeche As Double, preis As Single, anzahl As Integer) Me.flaeche = flaeche Me.preis = preis zimmeranzahl = anzahl End Sub Public ReadOnly Property Wohnflaeche() As Double Get Return Wohnflaeche End Get End Property Public ReadOnly Property Miete() As Single Get Return preis End Get End Property Public ReadOnly Property Bezeichnung() As String Get Return zimmeranzahl.ToString() & "-Zimmer-Wohnung" End Get End Property End Class
773
12.9
12
Die wichtigsten Steuerelemente
In einer Listbox sollen die Bezeichnungen der Wohnungen angezeigt werden, in zwei Textboxen sowohl der Mietpreis als auch die Wohnfläche des ausgewählten Listenelements (siehe Abbildung 12.17). Wir verwenden die drei Eigenschaften DataSource, DisplayMember und ValueMember, um die Daten der Listbox festzulegen.
Abbildung 12.17
Form des Beispiels »Datenquelle«
'...\WinControls\Selektionen\Datenquelle.vb
Public Class Datenquelle Public Class Wohnung ... End Class Private Wohnungen As New ArrayList() Private Sub Laden(sender As Object, e As EventArgs) Handles MyBase.Load Wohnungen.Add(New Wohnung(25, 300, 1)) Wohnungen.Add(New Wohnung(54, 470, 2)) Wohnungen.Add(New Wohnung(87, 729, 4)) Wohnungen.Add(New Wohnung(60, 650, 2)) Wohnungen.Add(New Wohnung(75, 680, 3)) Wohnungsliste.DataSource = Wohnungen Wohnungsliste.DisplayMember = "Bezeichnung" Wohnungsliste.ValueMember = "Miete" Wohnungsliste.SelectedIndex = 1 End Sub Private Sub SelectedIndexChanged(sender As Object, e As EventArgs) _ Handles Wohnungsliste.SelectedIndexChanged Miete.Text = Wohnungsliste.SelectedValue.ToString() Fläche.Text = _ CType(Wohnungsliste.SelectedItem, Wohnung).Wohnfläche.ToString() End Sub End Class
DataSource erwartet die Referenz auf ein Objekt, das IList implementiert. Hier ist es ein ArrayList-Objekt mit der Bezeichnung Wohnungen, dem mehrere Wohnung-Objekte hinzugefügt werden. Anschließend wird das ArrayList-Objekt der DataSource-Eigenschaft zugewiesen. Im ListBox-Steuerelement sollen die Inhalte der Eigenschaft Bezeichnung angezeigt werden. Das erreichen wir mit folgender Zuweisung:
774
Markierte Auswahllisten (CheckedListBox)
Wohnungsliste.DisplayMember = "Bezeichnung"
Als »Wert« wählen wir willkürlich Miete und weisen dessen Name der Eigenschaft ValueMember zu. Die Spezifikation von ValueMember ist optional. Wohnungsliste.ValueMember = "Miete"
Wählt der Anwender ein Listenelement aus, wird das Ereignis SelectedIndexChanged ausgelöst. In der Textbox Miete zeigen wir den Mietpreis des gewählten Listenelements an, indem wir die Eigenschaft SelectedValue auswerten, die uns den Wert der unter ValueMember genannten Eigenschaft liefert. Die Wohnfläche in der Textbox Fläche wird durch Auswertung von SelectedItem mit anschließender Konvertierung in den Typ Wohnung ermittelt. Beachten Sie bitte, dass eine Listbox nicht mit Sorted sortiert werden kann, wenn die Eigenschaft DataSource zum Füllen benutzt wird.
12.10
Markierte Auswahllisten (CheckedListBox)
Die CheckedListBox ähnelt der ListBox sehr. Der größte Unterschied besteht darin, dass in der CheckListBox vor jedem Listenelement ein Auswahlkästchen angeboten wird. Die standardmäßige Auswahl eines Elements ist ein wenig gewöhnungsbedürftig: Zuerst muss ein Listenelement markiert werden, bevor mit einem zweiten Klick das Häkchen im Auswahlkästchen gesetzt werden kann. Wenn Sie dieses Verhalten nicht wünschen, müssen Sie die Eigenschaft CheckOnClick=True setzen. Das Layout der CheckedListBox kann mit der Eigenschaft ThreeDCheckBoxes ein wenig verändert werden. Der Wert True zeigt die Auswahlkästchen mit einem 3D-Effekt (siehe Abbildung 12.18).
Abbildung 12.18
Das »CheckedListBox«-Steuerelement
Über die Eigenschaft Items wird die Referenz auf ein internes Objekt vom Typ CheckedListBox.ObjectCollection geliefert. Sie verfügt beispielsweise über die Methode Add, um das
Steuerelement mit Einträgen zu füllen. Die Auswertung erfolgt über die Eigenschaften CheckedIndices und CheckedItems, die die Referenz auf ein CheckedIndexCollection- bzw. CheckedItemCollection-Objekt bereitstellen. Beide Auflistungen gleichen denen einer Listbox mit Mehrfachauswahl.
775
12.10
12
Die wichtigsten Steuerelemente
12.11
Kombinationslistenfelder (ComboBox)
Das Kombinationslistenfeld wird durch die Klasse ComboBox beschrieben. Comboboxen lassen drei Darstellungsarten zu, die von der Eigenschaft DropDownStyle kontrolliert werden. Die Eigenschaft DropDownStyle hat den Typ der Enumeration ComboBoxStyle, deren Werte Tabelle 12.9 auflistet. Konstante
Beschreibung
Simple
Das Kombinationslistenfeld wird wie eine Listbox angezeigt. Das Textfeld über der Liste kann editiert werden.
DropDown
Um die Liste zu zeigen, muss der Anwender auf die Schaltfläche klicken. Das angezeigte Feld kann editiert werden.
DropDownList
Um die Liste zu zeigen, muss der Anwender auf die Schaltfläche klicken. Das angezeigte Feld kann nicht editiert werden.
Tabelle 12.9
Die Enumeration »ComboBoxStyle«
Wenn Sie eine Combobox aus der Toolbox in Ihr Formular ziehen, hat die Eigenschaft DropDownStyle die Einstellung DropDown. Der im Feld angezeigte Inhalt kann editiert werden. Öffnet sich die Dropdown-Liste, werden standardmäßig acht Elemente angezeigt. Ist die Liste länger, kann mit Bildlaufleisten durch die Liste navigiert werden. Mit der Eigenschaft MaxDropDownItems kann die angezeigte Anzahl zwischen 1 und 100 verändert werden. Ähnlich präsentiert sich auch ein Kombinationslistenfeld mit ComboBoxStyle.DropDownList. Allerdings ist mit dieser Einstellung das Textfeld über der Liste nicht editierbar. ComboBoxStyle.Simple wird in der Praxis seltener eingesetzt. Mit dieser Einstellung zeigt das
Steuerelement keine Pfeilschaltfläche. Die Combobox sieht dann wie eine Textbox aus, unter der ein Listenfeld angehängt ist. Das in der Liste ausgewählte Element wird im Textbereich angezeigt. Über die Size-Eigenschaft kann die Höhe und somit auch die Anzahl der angezeigten Listenelemente beeinflusst werden. Eine Combobox ähnelt einer Listbox mit einfacher Auswahl – nicht nur hinsichtlich der Funktionalität, auch die Programmierung unterscheidet sich nur unwesentlich. Wie eine Listbox weist auch eine Combobox die Eigenschaft Items auf, die eine Referenz auf die Auflistung ComboBox.ObjectCollection bereitstellt, in der alle Listenelemente der Combobox verwaltet werden. Auf die Referenz der ObjectCollection können die üblichen Methoden aufgerufen werden (Add, Remove usw.). Außerdem wird eine Eigenschaft DataSource zum Füllen einer Combobox mit dem Inhalt eines IList-Objekts angeboten (siehe dazu auch Abschnitt 12.9.7, »Auswahlliste mit Datenquelle (DataSource)«). Zwei Eigenschaften der Combobox geben den Index und die Referenz des ausgewählten Elements aus der Auflistung ComboBox.ObjectCollection zurück: SelectedIndex und SelectedItem. Die erste liefert den Index des ausgewählten Elements, die zweite die Referenz auf das Listenelement. Wenn Sie eine Combobox während des Ladens der Form mit Daten füllen, wird das Textfenster der Combobox bei der ersten Anzeige der Form kein Listenelement anzeigen. Sie sollten
776
Kombinationslistenfelder (ComboBox)
deshalb mit SelectedIndex oder SelectedItem sofort ein Listenelement anzeigen lassen, zum Beispiel so: Private Sub Form1_Load(sender As Object, e As EventArgs) comboBox1.Items.AddRange(New String(){ _ "Frankreich", "Schweiz", "Spanien", "Deutschland", "Italien"}) ' Starteintrag der Combobox comboBox1.SelectedItem = "Spanien" End Sub
Die Zeichenfolge des angezeigten Elements können Sie sowohl mit der Eigenschaft Text als auch mit SelectedText abfragen. Beiden Eigenschaften kann auch eine Zeichenfolge zugewiesen werden, wenn der Typ nicht ComboBoxStyle.DropDownList ist. Sie ersetzt den Text oberhalb der Liste. Beachten Sie aber, dass die so übergebene Zeichenfolge nicht automatisch zu den Listenelementen hinzugefügt wird. Dazu müssen Sie die Add-Methode des ComboBox.ObjectCollection-Objekts verwenden.
12.11.1
Ereignisse
Im Gegensatz zu einer Listbox verfügt die Combobox über das Click-Ereignis. So wichtig dieses Ereignis für andere Steuerelemente wie eine Schaltfläche auch ist, bei einer Combobox gibt es selten eine Situation, in der man dieses Ereignis sinnvoll einsetzen könnte, denn es wird sowohl beim Klicken der Pfeilschaltfläche ausgelöst als auch dann, wenn mit dem Mauszeiger in das Textfeld geklickt wird. Damit ist aber noch keine Auswahländerung oder auch nur die Änderung des angezeigten Textes verbunden. Wichtig ist vielmehr das Ereignis, das eine Änderung des Inhalts des Textfeldes signalisiert: TextChanged. Die Ereignisauslösung kann zwei Ursachen haben: 왘
Der Benutzer hat eine Auswahl aus der Dropdown-Liste getroffen.
왘
Der Benutzer hat den im Eingabefeld angezeigten Text geändert (nur möglich, wenn die ComboBox nicht vom Typ DropDownList ist).
Einschränkender und damit auch präziser verhält sich das Ereignis SelectedIndexChanged, das nur nach einer Änderung der Auswahl ausgelöst wird. Daher ist dieses Ereignis besonders dazu geeignet, anderen Komponenten die Neuauswahl mitzuteilen. Aktionen, die beim Öffnen oder Schließen der Liste ausgeführt werden sollen, sind in den beiden Ereignissen DropDown bzw. DropDownClosed zu implementieren. Allerdings werden diese beiden Events nur ausgelöst, wenn das Kombinationslistenfeld nicht auf DropDownStyle=Simple eingestellt ist.
12.11.2 Autovervollständigung Textboxen und Kombinationslistenfelder haben die Fähigkeit der Autovervollständigung. Bei diesem Feature spielen die Eigenschaften AutoCompleteCustomSource, AutoCompleteMode und AutoCompleteSource die entscheidende Rolle. Wir haben uns mit der Autovervollständigung bereits im Zusammenhang mit Textboxen genauer beschäftigt (siehe Abschnitt 12.6.1, »Einzeilige Eingabefelder«).
777
12.11
12
Die wichtigsten Steuerelemente
12.12
Standarddialoge
Bei Standarddialogen handelt es sich um Dialoge (Forms), die wir in nahezu jeder WindowsAnwendung vorfinden. Insgesamt fünf werden uns in der Toolbox als Steuerelemente angeboten, die – weil sie nicht direkt mit dem Benutzer interagieren – im Komponentenfach unterhalb des Windows Forms-Designers abgelegt werden: 왘
ColorDialog
왘
OpenFileDialog
왘
FolderBrowserDialog
왘
SaveFileDialog
왘
FontDialog
Bis auf OpenFileDialog und SaveFileDialog sind alle direkt von der abstrakten Basisklasse CommonDialog abgeleitet. Die beiden Dateidialoge schalten noch die abstrakte Klasse FileDialog dazwischen. Die wichtigste Methode aller Standarddialoge ist ShowDialog. Sie dient zum Öffnen eines Dialogfensters, zum Beispiel: openFileDialog.ShowDialog()
Diese Methode liefert einen Rückgabewert vom Typ DialogResult.
12.12.1 Datei zum Öffnen wählen (OpenFileDialog) OpenFileDialog ist ein Dialogfenster, in dem der Anwender zur Laufzeit eine Datei auswählen kann, die er öffnen möchte. Die Klasse hat nur einen parameterlosen Konstruktor. Dim dlgFileOpen As New OpenFileDialog() dlgFileOpen.ShowDialog()
Die beiden Anweisungen bewirken das Öffnen des Dialogfensters aus Abbildung 12.19. Es unterstützt die Navigation zu der Datei, die geöffnet werden soll. Wenn Sie auf die Schaltfläche klicken, wird die ausgewählte Datei nicht tatsächlich geöffnet, sondern nur in der Eigenschaft FileName des Dialogs gespeichert. In den folgenden Unterabschnitten zeige ich Ihnen, wie sowohl das Layout als auch das Verhalten eines OpenFileDialog-Fensters in Grenzen an spezifische Forderungen angepasst wird.
Beschriftung der Titelleiste Standardmäßig wird in der Titelleiste des Dialogs die Beschriftung Öffnen angezeigt. Sie können den Titel mit der Eigenschaft Title beliebig festlegen. Dim dlgFileOpen As New OpenFileDialog() dlgFileOpen.Titel = "Öffnen eines Projektordners"
778
Standarddialoge
Abbildung 12.19
Der Standarddialog »OpenFileDialog«
Standardverzeichnis Der Dialog zum Öffnen einer Datei zeigt beim ersten Mal den Inhalt des Ordners, in dem sich die ausführbare Datei befindet. Beim nächsten Öffnen ist es immer das Verzeichnis, aus dem heraus zuvor eine Datei ausgewählt worden ist. Alternativ wird vor dem Öffnen des Dialogs in der Eigenschaft InitialDirectory das anzuzeigende Verzeichnis spezifiziert. Wenn Sie keinen Wert festlegen, enthält die Eigenschaft einen Leerstring. dlgFileOpen.InitialDirectory = "C:\MyProjects"
Möglicherweise benötigen Sie aber eine Pfadangabe zu einem systemspezifischen Verzeichnis. Um die notwendigen Informationen über die Umgebung der aktuellen Maschine zu erhalten, können Sie auf die Dienste der Klasse Environment der .NET-Klassenbibliothek zurückgreifen. Systemspezifische Verzeichnisse geben Sie indirekt an. Die Klasse Environment im Namensraum System definiert Eigenschaften und Methoden, die zum Abrufen von maschinenspezifischen Informationen nützlich sind, beispielsweise die statische Methode GetFolderPath, die den Pfad eines bestimmten Systemordners abruft. Sie erwartet als Argument einen Wert der Enumeration Environment.SpecialFolder. Die Tabelle 12.10 zeigt nur einen Auszug der Enumeration; einen vollständigen Überblick gibt die .NET-Dokumentation. Member
Rückgabewert (Standardinstallationsvorgabe)
CommonProgramFiles
C:\Programme\Gemeinsame Dateien
History
\Lokale Einstellungen\Verlauf
Personal
\Eigene Dateien
ProgramFiles
C:\Programme
Programs
\Startmenü\Programme
Tabelle 12.10 Die Enumeration »Environment.SpecialFolder« (Auszug, Benutzereinstellungen=C:\Dokumente und Einstellungen\ )
779
12.12
12
Die wichtigsten Steuerelemente
Member
Rückgabewert (Standardinstallationsvorgabe)
Recent
\Recent
StartMenu
\Startmenü
Startup
\Startmenü\Programme\Autostart
System
C:\WINDOWS\System32
Tabelle 12.10 Die Enumeration »Environment.SpecialFolder« (Auszug, Benutzereinstellungen=C:\Dokumente und Einstellungen\ ) (Forts.)
Hinweis Unter Vista sind einige der Verzeichnisse in Wirklichkeit Links.
Möchten Sie das zu öffnende Standardverzeichnis auf den Ordner C:\Dokumente und Einstellungen\\Eigene Dateien festlegen, lautet die Anweisung: dlgFileOpen.InitialDirectory = _ Environment.GetFolderPath(Environment.SpecialFolder.Personal)
Dateifilter setzen Die Eigenschaft Filter des OpenFileDialog-Steuerelements legt die Dateitypen fest, die dem Anwender aus dem angezeigten Ordner zum Öffnen angeboten werden, zum Beispiel: dlgFileOpen.Filter = "Textdateien (*.txt)|*.txt"
Nach welchen Dateitypen gefiltert wird, kann der Anwender durch eine entsprechende Auswahl in einem Kombinationslistenfeld unterhalb des Anzeigebereichs bestimmen. 3Die durch die Eigenschaft Filter beschriebene Zeichenfolge unterliegt einem strengen Muster. Die Anweisung oben definiert einen Dateifilter, der im Auswahlbereich des Dialogs nur Textdateien mit der Dateierweiterung .txt anbietet. Die Zeichenfolge zur Festlegung eines Filters setzt sich immer aus zwei Teilen zusammen, die durch einen senkrechten Strich voneinander getrennt werden. Die Teilzeichenfolge links vom Strich wird dem Benutzer im Kombinationslistenfeld Dateityp angezeigt, die rechts vom Strich stehende Teilzeichenfolge gibt an, nach welchen Kriterien die Dateien gefiltert werden sollen. Das »*«-Zeichen dient als Platzhalter. Soll das Kombinationslistenfeld mehrere Auswahloptionen anbieten, ergänzen Sie die Zeichenfolge der Filter-Eigenschaft nach dem bekannten Muster um die gewünschten Filter. Zur Abgrenzung untereinander verwenden Sie ebenfalls einen senkrechten Strich. Im folgenden Beispiel wird das Kombinationslistenfeld mit zwei Einträgen gefüllt: Der erste ist die Filterung nach Textdateien, und die zweite Auswahloption zeigt sämtliche Dateien des geöffneten Ordners an. dlgFileOpen.Filter = "Textdateien (*.txt)|*.txt|Alle Dateien (*.*)|*.*"
780
Standarddialoge
Wird zur Laufzeit der Anwendung die erste Auswahl getroffen, werden die TXT-Dateien herausgefiltert; entscheidet sich der Anwender für die zweite, werden alle Dateien, unabhängig von der Dateierweiterung, angezeigt. Die im Kombinationslistenfeld angebotene Liste kann durchaus sehr lang werden, wenn Sie den Anwender mit vielen Filteroptionen verwöhnen wollen. dlgFileOpen.Filter = _ "Textdateien (*.txt)|*.txt| HTML-Dateien|*.htm;*.html|" _ & "Bitmap-Dateien (*.bmp)|*.bmp|" _ & "Ausführbare Dateien (*.exe)|*.exe|" _ & "Word-Dokumente(*.doc)|*.doc|" _ & "Alle Dateien (*.*)|*.*)"
Manchmal möchte man auch mehrere Dateitypen gleichzeitig anzeigen lassen. Das ist beispielsweise dann der Fall, wenn Sie nach mehreren Bildformaten filtern wollen. Listen Sie dazu in der rechten Teilzeichenfolge alle gewünschten Dateiformate auf. Diese müssen durch ein Semikolon voneinander getrennt werden: dlgFileOpen.Filter = "Alle Bilddateien|*.bmp;*.jpeg;*.gif"
Anwendungsspezifischer Standardfilter Normalerweise wird nach dem Öffnen des Dialogs der zuerst aufgeführte Filter im Kombinationslistenfeld angezeigt. Mit der Eigenschaft FilterIndex kann aber auch ein beliebiger aus der Liste gewählt werden: dlgFileOpen.FilterIndex = 1
Entgegen den sonst üblichen Gepflogenheiten beginnt die Indizierung der Filter nicht mit dem Index 0, sondern mit dem Index 1. Soll nach dem Öffnen des Dialogs beispielsweise nach den im angezeigten Verzeichnis befindlichen Word-Dokumenten gefiltert werden (siehe das Codefragment oben), muss der Eigenschaft FilterIndex die Zahl 4 übergeben werden.
Die ausgewählte Datei Das Ziel des Dialogs ist, eine Datei auszuwählen und die Wahl im weiteren Programmcode zum tatsächlichen Öffnen der Datei zu verwenden. Die Eigenschaft FileName liefert uns den Dateinamen einschließlich der gesamten Pfadangabe. Ihn können wir als Übergabeargument bei der Instanziierung einer Klasse aus dem Namensraum System.IO mit einem Konstruktor benutzen, der einen path-Parameter entgegennimmt. Zum Beispiel: Dim dlgFileOpen As New OpenFileDialog() if dlgFileOpen.ShowDialog() = DialogResult.OK Then Dim sr As StreamReader = New StreamReader(dlgFileOpen.FileName) ...
Mehrfachauswahl von Dateien Möchten Sie eine Mehrfachauswahl von Dateien zulassen, müssen Sie die Eigenschaft MultiSelect des Dialogs auf True setzen. Das Verhalten bei der Dateiauswahl entspricht dann dem eines Listenfeldes mit erweiterter Mehrfachauswahl. 781
12.12
12
Die wichtigsten Steuerelemente
Bei einer einfachen Auswahlmöglichkeit kann man den Pfad zu der zu öffnenden Datei über die Eigenschaft FileName abfragen. Bei der Mehrfachauswahl gibt FileNames ein Array vom Typ String zurück: Public ReadOnly Property FileNames As String()
Im Gegensatz zu FileName ist diese Eigenschaft schreibgeschützt.
Ereignisse von OpenFileDialog Nur zwei spezifische Ereignisse zeichnen das Dialogfeld aus. Zum einen können Sie in Verbindung mit dem Klicken auf die Schaltfläche OK einen Ereignishandler mit dem Ereignis FileOK verknüpfen. Anstatt die Rückgabe des Aufrufs der Methode ShowDialog auszuwerten, können Sie somit auch im Ereignishandler auf die Dateiauswahl im Dialog reagieren. Unter bestimmten Umständen ist das Auswerten des Ereignisses FileOk flexibler, denn der Ereignishandler hat ein Argument vom Typ CancelEventArgs, das uns erlaubt, über die Eigenschaft Cancel das Schließen des Dialogs zu verhindern. Dadurch wird es dem Anwender möglich, mehrere Dateien einzeln hintereinander auszuwählen, ohne dass der Dialog wiederholt geöffnet werden muss. Das Ereignis HelpRequest setzt voraus, dass die Eigenschaft ShowHelp=True gesetzt ist und im Dialog eine Hilfe-Schaltfläche angezeigt wird. Ausgelöst wird dieses Ereignis durch Klicken auf diese Schaltfläche.
Das Beispiel »Texteditor« Im folgenden Beispiel wollen wir einen einfachen Texteditor entwickeln und für die allgemeinen Operationen zum Öffnen und Speichern Standarddialoge benutzen. Zunächst soll nur das Öffnen von Dateien implementiert werden. Später wird das Beispiel auch um die Speicherung der Dokumentinformationen ergänzt. Die Form des Texteditors in Abbildung 12.20 enthält eine Textbox, die auch dann den gesamten Clientbereich der Form ausfüllt, wenn die Form zur Laufzeit vergrößert oder verkleinert wird. Am einfachsten ist das zu realisieren, wenn die Eigenschaft Dock der Textbox auf DockStyle.Fill eingestellt wird. Das Formular enthält ein Menü mit dem Hauptmenüpunkt Datei und den Menüunterelementen Öffnen, Neu, Speichern, Speichern unter und Beenden.
Abbildung 12.20
782
Formular des Beispiels »Texteditor«
Standarddialoge
Mit dem Erstellen eines Menüs werden wir uns später beschäftigen. Im Moment ist es für Sie nur wichtig zu wissen, dass ein Menüelement das Ereignis Click bereitstellt, das ausgelöst wird, sobald der Anwender einen Menüpunkt auswählt. '...\WinControls\Dialoge\Texteditor.vb
Public Class Texteditor Private Function Weiter() As Boolean Dim fortfahren As Boolean = True If Textfenster.Modified Then Dim erg As DialogResult = MessageBox.Show( _ "Wollen Sie den geänderten Text speichern?", _ Application.ProductName, MessageBoxButtons.YesNoCancel, _ MessageBoxIcon.Question, MessageBoxDefaultButton.Button2) If erg = DialogResult.Yes Then fortfahren = Speichern() ElseIf erg = DialogResult.Abort OrElse erg = DialogResult.Cancel Then fortfahren = False End If End If Return fortfahren End Function Private Sub Neu_Click(ByVal sender As Object, ByVal e As EventArgs) _ Handles NeuMenüpunkt.Click If Weiter() Then Datei = "" Textfenster.Text = "" Text = "Unbenannt – Texteditor" End If End Sub Private Sub Öffnen_Click(ByVal sender As Object, ByVal e As EventArgs) _ Handles ÖffnenMenüpunkt.Click If Weiter() Then Dim ofd As New OpenFileDialog() ofd.Filter = "Textdateien (*.txt)|*.txt|Alle Dateien (*.*)|*.*" ofd.Title = "Öffnen einer Textdatei" If ofd.ShowDialog() = DialogResult.OK Then Dim sr As IO.StreamReader = Nothing Try sr = New IO.StreamReader(ofd.FileName) Textfenster.Text = sr.ReadToEnd() Text = ofd.FileName Datei = ofd.FileName Catch ex As Exception MessageBox.Show(ex.Message, "Texteditor") Finally Try
783
12.12
12
Die wichtigsten Steuerelemente
If sr IsNot Nothing Then sr.Close() Catch ex As Exception MessageBox.Show(ex.Message, "Texteditor") End Try End Try End If End If End Sub Private Sub Beenden_Click(ByVal sender As Object, ByVal e As EventArgs) _ Handles BeendenMenüpunkt.Click If Weiter() Then Application.Exit() End Sub Private Sub Schließen(sender As Object, e As FormClosingEventArgs) _ Handles MyBase.FormClosing e.Cancel = Not Weiter() End Sub ... End Class
Im Zusammenhang mit dem Thema dieses Abschnitts ist der wichtigste Codeabschnitt der Dialog zum Öffnen einer Textdatei: Dim ofd As New OpenFileDialog() ... If ofd.ShowDialog() = DialogResult.OK Then ...
Wie alle modalen Dialogfenster liefert der Aufruf der Methode ShowDialog einen Rückgabewert vom Typ DialogResult. Wir müssen nur noch feststellen, welche Schaltfläche der Anwender im Dialog geklickt hat, denn nur mit der Bestätigung durch OK soll die markierte Datei auch geöffnet und im Textfenster angezeigt werden. Eine Textdatei kann mit mehreren Klassen des Namensraums System.IO realisiert werden. Im Beispiel wird die Klasse StreamReader eingesetzt und dem Konstruktor die Eigenschaft FileName des Dialogs zugewiesen. Die Methode ReadToEnd() liest Eingaben von der aktuellen Position des Dateizeigers bis zum Ende der Datei ein und berücksichtigt dabei auch Zeilenumbrüche innerhalb des Textes. Der eingelesene Dateiinhalt wird in der Textbox angezeigt. sr = New IO.StreamReader(ofd.FileName) Textfenster.Text = sr.ReadToEnd() ... Finally ... If sr IsNot Nothing Then sr.Close()
Da insbesondere Dateioperationen schnell einen Fehler verursachen können, wird das Öffnen und Einlesen in einen Try-Anweisungsblock eingeschlossen. Sollte eine Ausnahme auftreten, wird die Fehlermeldung in einem Meldungsfenster angezeigt. Das Schließen des StreamReader-Objekts erfolgt im Finally-Zweig, damit es auch geschlossen wird, wenn während des
784
Standarddialoge
Lesens eine Ausnahme ausgelöst wird. Damit eine Ausnahme während des Schließens die Anwendung nicht beendet, wird Close() innerhalb eines Try-Blocks aufgerufen. Die Methode Weiter() speichert, wenn gewünscht, den Text in einer Datei und liefert als Rückgabewert die Information, ob der Vorgang abgeschlossen wurde (gegebenenfalls auch, ohne zu speichern). Eine Speicherung ist nur erforderlich, wenn der Text geändert wurde. Dies wird durch die Eigenschaft Modified der Textbox kenntlich gemacht, die bei Zuweisung der Eigenschaft Text der Textbox automatisch auf False gesetzt wird. Auf die Implementierung der Methode Speichern() gehe ich im nächsten Abschnitt ein. Da bei jeder Textänderung eine Prüfung auf Speicherung erfolgen sollte, wird Weiter() sowohl vor dem Öffnen einer Datei als auch vor dem Löschen des Textes durch den Menüpunkt Neu aufgerufen. Analog wird vor dem Schließen des Fensters und vor dem Ende des Programms dem Benutzer durch Aufruf von Weiter() die Möglichkeit zum Speichern gegeben. Alle Aufrufe sind so platziert, dass die Bearbeitung nur dann fortgesetzt wird, wenn keine Änderungen vorliegen oder der Benutzer sich eindeutig für die Fortsetzung entscheidet. Somit fehlt nur noch die Implementierung der Speicherroutine.
12.12.2 Datei zum Speichern wählen (SaveFileDialog) Die Klasse SaveFileDialog stellt ein Dialogfenster dar, das den Anwender zum Speichern einer Datei auffordert. Da die beiden Klassen SaveFileDialog und OpenFileDialog die gemeinsame Basisklasse FileDialog haben, ähneln sich beide Dialoge nicht nur optisch, sondern weisen auch viele gemeinsame Eigenschaften auf, beispielsweise Title, InitialDirectory und Filter. Verschaffen wir uns daher in Tabelle 12.11 einen Überblick über die Eigenschaften, durch die sich die beiden Klassen unterscheiden. Eigenschaft
Beschreibung
AddExtension
Gibt an, ob einem Dateinamen im Dialogfeld automatisch eine Erweiterung hinzugefügt wird, wenn der Benutzer keine Erweiterung angibt.
DefaultExt
Die Standarddateinamenerweiterung
CreatePrompt
Gibt an, ob die Neuanlage einer nicht existierenden Datei in einem Meldungsfenster durch den Benutzer bestätigt werden muss.
OverwritePrompt
Gibt an, ob das Überschreiben einer vorhandenen Datei in einem Meldungsfenster durch den Benutzer bestätigt werden muss.
Tabelle 12.11
Spezifische Eigenschaften der Klasse »SaveFileDialog«
Viele Anwendungen ermöglichen es einem Benutzer, auch ohne die Angabe einer Dateierweiterung eine Datei zu speichern. Eine passende Dateierweiterung wird dann automatisch angehängt. MS Word erweitert den Dateinamen beispielsweise um die Erweiterung .doc, der Texteditor Notepad um .txt. Wollen Sie diese Unterstützung auch in Ihrer eigenen Anwendung realisieren, setzt das zunächst voraus, dass die Eigenschaft AddExtension=True gesetzt wird. Der Eigenschaft DefaultExt teilen Sie die gewünschte Standarddateinamenerweiterung mit. Die Zeichenkette der Eigenschaft DefaultExt wird ohne den Punkt angegeben, der Dateiname und die Dateierweiterung voneinander trennt. Der Punkt wird automatisch eingefügt. Haben
785
12.12
12
Die wichtigsten Steuerelemente
Sie eine anwendungsspezifische Dateierweiterung festgelegt und gibt der Anwender die Dateierweiterung trotzdem an, erkennt der Dialog das und hängt keine zweite an. saveFileDialog1.AddExtension = True saveFileDialog1.DefaultExt = "vb"
Die beiden Eigenschaften CreatePrompt und OverwritePrompt informieren den Anwender mit einem Meldungsfenster. Ist CreatePrompt=True und gibt der Anwender einen Dateinamen an, der im aktuell geöffneten Verzeichnis des Dialogs noch nicht vorhanden ist, wird im Meldungsfenster um Bestätigung gebeten, ob die Datei neu erstellt werden soll. Übernehmen Sie den Standardwert True der Eigenschaft OverwritePrompt, wird ein Meldungsfenster angezeigt, mit dem der Anwender bestätigen kann, ob eine vorhandene Datei gleichen Namens überschrieben werden soll.
Der Speichern-Dialog im Beispiel »Texteditor« Das Beispiel Texteditor des vorigen Abschnitts soll jetzt um eine Speicherfunktionalität erweitert werden. Das Feld Datei speichert den Namen der Datei oder "", wenn noch kein Name gewählt wurde. Die Methode Speichern() ruft den Ereignishandler des Menüpunkts zum Speichern auf. Fortfahren ist nur False, wenn der Benutzer in einem Dialog Cancel angeklickt hat oder eine Ausnahme aufgetreten ist. '...\WinControls\Dialoge\Texteditor.vb
Public Class Texteditor ... Private Datei As String = "" Private Fortfahren As Boolean Private Function Speichern() As Boolean Fortfahren = True Speichern_Click(Nothing, Nothing) Return Fortfahren End Function Private Sub Speichern_Click(ByVal sender As Object, ByVal e As EventArgs) _ Handles SpeichernMenüpunkt.Click If Datei.Equals("") Then SpeichernUnter_Click(Nothing, Nothing) Else Dim sw As IO.StreamWriter = Nothing Try sw = New IO.StreamWriter(Datei, False) sw.Write(Textfenster.Text) Textfenster.Modified = False Catch ex As Exception Fortfahren = False MessageBox.Show(ex.Message, "Texteditor") Finally
786
Standarddialoge
Try If sw IsNot Nothing Then sw.Close() Catch ex As Exception MessageBox.Show(ex.Message, "Texteditor") End Try End Try End If End Sub Private Sub SpeichernUnter_Click(sender As Object, e As EventArgs) _ Handles SpeichernUnterMenüpunkt.Click Dim sfd As New SaveFileDialog() sfd.Filter = "Textdateien (*.txt)|*.txt|Alle Dateien (*.*)|*.*" sfd.Title = "Speichern einer Textdatei" If Not Datei.Equals("") Then sfd.FileName = Datei If sfd.ShowDialog() = DialogResult.OK Then Datei = sfd.FileName Speichern_Click(Nothing, Nothing) Text = IO.Path.GetFileName(Datei) & " – Texteditor" Else Fortfahren = False End If End Sub End Class
Sowohl das Speichern unter bekanntem Namen als auch unter einem neuen Namen findet in dem Ereignishandler Speichern_Click() des Menüpunkts Speichern statt. Ist noch kein Dateiname spezifiziert, ruft die Methode ihrerseits SpeichernUnter_Click() auf, um einen Namen festzulegen, die Datei innerhalb von Speichern_Click() zu speichern und die Titelleiste der Anwendung anzupassen. Im Zusammenhang mit dem Thema dieses Abschnitts ist der wichtigste Codeabschnitt der Dialog zum Speichern einer Textdatei: Dim sfd As New SaveFileDialog() ... If sfd.ShowDialog() = DialogResult.OK Then Datei = sfd.FileName ...
Die Anweisungen ermitteln einen Dateinamen, der im Feld Datei gespeichert wird. Die Zuweisung an die Eigenschaft FileName vor dem Öffnen des Dialogs dient nur dazu, im Textfeld für den Dateinamen bereits etwas stehen zu haben. Der eigentliche Speichervorgang im Ereignishandler Speichern_Click() ist analog zu dem des Öffnens aufgebaut: sw = New IO.StreamWriter(Datei, False) sw.Write(Textfenster.Text) ... Finally ... If sw IsNot Nothing Then sw.Close()
787
12.12
12
Die wichtigsten Steuerelemente
Der Strom wird geöffnet und der Text mit Write() hineingeschrieben. Der zweite Parameter des Konstruktors sorgt dafür, dass der Text die Datei gegebenenfalls überschreibt und nicht an sie angehängt wird. Auch hier findet das Schließen in einem Finally-Zweig statt, um den Strom auch bei Auftreten einer Ausnahme zu schließen. Um die Anwendung nicht vorzeitig durch eine Ausnahme während des Schließens zu beenden, wird das Schließen in einem eigenen Try-Block ausgeführt.
12.12.3 Ordner selektieren (FolderBrowserDialog) Mit OpenFileDialog können Sie eine bestimmte Datei auswählen. Das Steuerelement bietet jedoch keine Möglichkeit, die Wahl auf einen Ordner zu beschränken. Dafür stellt das .NET Framework ein anderes Steuerelement zur Verfügung: FolderBrowserDialog. Etwas enttäuschend ist das in Abbildung 12.21 gezeigte Layout des Dialogs. Während uns in anderen Anwendungen ein Dialog ähnlich dem des Dialogs zum Öffnen einer Datei angeboten wird (unter anderem auch von Visual Studio), präsentiert sich die Oberfläche eines FolderBrowserDialog-Objekts nach dem Aufruf der Methode ShowDialog ziemlich spartanisch.
Abbildung 12.21
Der Standarddialog »FolderBrowserDialog«
Die Einflussnahme auf die Anzeige des Dialogs ist auch nur sehr begrenzt. Oberhalb des Anzeigebereichs können Sie einen Text ausgeben. Die entsprechende Zeichenfolge weisen Sie der Eigenschaft Description zu. Standardmäßig kann ein Anwender aus allen lokalen oder sich im Netzwerk befindlichen Ordnern auswählen. Mit der Eigenschaft RootFolder kann ein anderer Ordner zum Stammordner des Dialogs gemacht werden. Falls Sie es dem Benutzer nicht erlauben wollen, einen neuen Ordner mit der Schaltfläche Neuer Ordner anzulegen, müssen Sie die Schaltfläche ausblenden. Dazu dient die Eigenschaft ShowNewFolderButton, die Sie dann auf False setzen müssen. Die Pfadangabe des ausgewählten Ordners kann nach dem Schließen des Dialogs mit der Eigenschaft SelectedPath ausgewertet werden, die vom Typ String ist.
788
Standarddialoge
12.12.4 Farbe bekennen (ColorDialog) Noch einfacher als OpenFileDialog und SaveFileDialog ist das Dialogfenster zu programmieren, das dem Anwender ermöglicht, interaktiv Farben auszuwählen, beispielsweise um der Eigenschaft BackColor oder ForeColor zur Laufzeit eine neue Farbe zuzuweisen. Zum Öffnen des Dialogs wird ebenfalls die Methode ShowDialog aufgerufen. Danach sieht das Standarddialogfeld zur Farbauswahl wie in Abbildung 12.22 aus.
Abbildung 12.22
Der Standarddialog »ColorDialog«
Ein Klick auf die Schaltfläche Farben definieren erweitert das Fenster und ermöglicht es dem Anwender, über die vordefinierten Farben hinaus nach eigener Vorstellung Farbwerte festzulegen. Beabsichtigen Sie, dem Anwender von Anfang an das erweiterte Dialogfenster anzubieten, müssen Sie vor dem Öffnen des Dialogs die Eigenschaft FullOpen=True setzen. Die ausgewählte Farbe wird in der Eigenschaft Color des Dialogs gespeichert. Im folgenden Codefragment wird die Hintergrundfarbe der Form gemäß der Auswahl im Dialog verändert. Dem Benutzer wird der erweiterte Dialog angezeigt. Wir werten allerdings nur die OK-Schaltfläche des Dialogs aus, da die Abbrechen-Schaltfläche bedeutungslos ist: Private Sub btnSetColor_Click(sender As Object, e As EventArgs) colorDialog1.FullOpen = True If colorDialog1.ShowDialog() = DialogResult.OK Then _ Me.BackColor = colorDialog1.Color End Sub
Im Zustand FullOpen hat der Anwender die Möglichkeit, 16 benutzerdefinierte Farben festzulegen, die beim erneuten Öffnen des Dialogs zur Auswahl stehen können. Allerdings ist die Wiedergabe bereits festgelegter benutzerdefinierter Farben kein Standardverhalten, sondern benötigt zusätzlichen Programmcode. Verantwortlich für das Speichern benutzerdefinierter Farben ist die Eigenschaft CustomColors.
789
12.12
12
Die wichtigsten Steuerelemente
Damit die vom Anwender ausgewählten Farben zur Laufzeit zur Verfügung stehen, müssen wir zuerst ein Integer-Array mit ausreichender Kapazität deklarieren: Dim myColors(15) As Integer
Bevor mit ShowDialog das Dialogfenster geöffnet wird, weisen wir den Inhalt dieses Arrays der Eigenschaft CustomColors zu: colorDialog1.CustomColors = myColors
Nach dem Schließen des Dialogs werden die Farben im Array gespeichert und stehen bei einem späteren erneuten Öffnen des Dialogs zur Verfügung: myColors = colorDialog1.CustomColors
Fassen wir zum Abschluss den Code in einem Click-Ereignishandler zusammen: Public Partial Class Form1 : Inherits Form Private myColors(15) As Integer ... Private Sub btnSetColor_Click(sender As Object, e As EventArgs) fontDialog1.FullOpen = True if colorDialog1.ShowDialog() = DialogResult.OK Then _ Me.BackColor = colorDialog1.Color myColors = colorDialog1.CustomColors End Sub End Class
12.12.5 Schriftart wählen (FontDialog) Der nächste Vertreter in der Runde der Standarddialoge ist FontDialog. Dieser Dialog ermöglicht die Auswahl einer Schriftart und des Schriftstils. Sie weisen Ihre Auswahl der Font-Eigenschaft zu, indem Sie auf OK klicken. Das Erscheinungsbild des Dialogs kann durch vier Eigenschaften manipuliert werden, die in Tabelle 12.12 aufgeführt sind. Eigenschaft
Beschreibung
ShowApply
Gibt an, ob der Dialog eine Übernehmen-Schaltfläche hat.
ShowColor
Gibt an, ob der Dialogfeld eine Farbauswahl ermöglicht.
ShowEffects
Gibt an, ob Unterstrichen oder Durchgestrichen ausgewählt werden können.
ShowHelp
Gibt an, ob der Dialog eine Hilfe-Schaltfläche anzeigt.
Tabelle 12.12
Eigenschaften zur Darstellung des Dialogs »FontDialog«
Die Schrift in einer Komponente wird durch die Eigenschaften Color und Font festgelegt, die im FontDialog-Objekt gespeichert sind. Die Farbe, die im FontDialog-Objekt eingestellt wird, ist die Schriftfarbe einer Komponente, die durch die Eigenschaft ForeColor beschrieben wird. Die Eigenschaft Font beschreibt die Schrift inklusive ihres Schnitts (fett, kursiv usw.).
790
Menüs, Symbol- und Statusleisten
Mit MinSize und MaxSize lässt sich festlegen, innerhalb welcher Spanne die Schriftgröße ausgewählt werden kann. MaxSize muss größer MinSize sein, und beide Werte müssen größer als 0 sein. Geben Sie den Wert »0« ein, entspricht das der Standardeinstellung. Haben beide Eigenschaften denselben Wert, ist dieser als konstanter Wert der Schriftgröße anzusehen, die der Benutzer nicht ändern kann. Eine weitere Eigenschaft ist FixedPitchOnly vom Typ Boolean. Hat die Eigenschaft den Wert True, werden im Dialog nur Schriftarten mit fester Zeichenbreite angeboten. Ein typisches
Beispiel dafür ist der Schrifttyp Courier.
12.13
Menüs, Symbol- und Statusleisten
In diesem Abschnitt wenden wir uns den folgenden Steuerelementen zu: 왘
ContextMenuStrip
왘
MenuStrip
왘
StatusStrip
왘
ToolStrip
Mit MenuStrip wird die Menüleiste einer Windows-Form beschrieben, mit StatusStrip die Statusleiste und mit ToolStrip eine Schaltflächengruppe. Diese drei können, wenn sie in einem Steuerelement vom Typ ToolStripContainer platziert werden, zur Laufzeit mittels Drag&Drop beliebig an allen Seiten eines Formulars angedockt werden. Wenden wir uns daher zuerst diesem Steuerelement zu.
12.13.1 Menüsammlung (ToolStripContainer) In Abbildung 12.23 sehen Sie das Steuerelement, nachdem es aus der Toolbox in die Form gezogen wurde. Es gliedert sich in insgesamt fünf Bereiche: vier Seitenbereiche und einen zentralen Bereich. Die Seitenbereiche, die über Laschen geöffnet werden können, dienen dazu, die Menü-, Symbol- und Statusleisten aufzunehmen und die Positionierung dieser drei Elemente in Abhängigkeit von der oder den Symbolleisten im Hintergrund zu verwalten. Der Zentralbereich nimmt die Steuerelemente auf, die die eigentliche Funktionalität der Form ausmachen. Ein ToolStripContainer wird in der Regel den gesamten Clientbereich der Form einnehmen. Sie erreichen das in gewohnter Weise mit der Einstellung Dock=DockStyle.Fill oder indem Sie im SmartTag-Hilfsfenster in Abbildung 12.23 den Link Ausfüllformular andocken auswählen. Hier können Sie auch entscheiden, an welchen Seitenrändern die Symbol- und Menüleisten zur Laufzeit andockbar sein dürfen. Möchten Sie die Entscheidung auf einen späteren Zeitpunkt vertagen oder die vorgenommene Auswahl ändern, stehen Ihnen dazu die Eigenschaften BottomToolStripPanelVisible, LeftToolStripPanelVisible usw. zur Verfügung. Die vier Seitenbereiche werden durch Objekte vom Typ ToolStripPanel beschrieben. Das Erscheinungsbild dieses Typs können Sie über Eigenschaften wie BackColor oder BackGroundImage beeinflussen.
791
12.13
12
Die wichtigsten Steuerelemente
Abbildung 12.23
Das Steuerelement ToolStripContainer
Nur Strip-Steuerelemente, die später in den einen der vier Seitenbereiche gezogen werden, können vom ToolStripContainer verwaltet werden. Alle anderen Controls werden vom ToolStripContainer nicht erfasst. Jedes Panel kann auch mehrere Strip-Controls enthalten. Der zentrale Bereich, beschrieben durch den Typ ContentPanel, enthält die Steuerelemente, die die Funktionalität des Fensters beschreiben, also Text- und Listboxen, Buttons usw. Dazu enthält ContentPanel wie jede von Control abgeleitete Klasse eine Eigenschaft Controls vom Typ Control.ControlCollection, der alle Steuerelemente hinzugefügt werden, während ContentPanel selbst Mitglied der typgleichen Auflistung der Form ist.
12.13.2 Menüdefinition Eines der wichtigsten Elemente eines Formulars ist das meist unterhalb der Titelleiste angeordnete Menü. Der sichtbare Teil eines Menüs wird als Hauptmenü bezeichnet. Zu ihm gehören typischerweise Menüelemente wie Datei, Bearbeiten, Extras usw. Der Klick auf ein Element des sichtbaren Hauptmenüs öffnet ein Untermenü, das auch als Popup-Menü bezeichnet wird und untergeordnete Menüelemente enthält. Jedes Unterelement kann seinerseits wieder Ausgangspunkt einer weiteren Untermenüebene sein. Bei mehr als vier Ebenen ist das Programm in der Regel nicht gut zu bedienen.
Hierarchie der Menüklassen Im Steuerelement MenuStrip kann jedes Menüelement, unabhängig davon, ob es im Hauptoder in einem Untermenü angesiedelt ist, ein Menüelement, eine Textbox oder ein Kombinationslistenfeld sein. Beschrieben werden diese durch die Klassen: 왘
ToolStripMenuItem
왘
ToolStripComboBox
왘
ToolStripTextBox
792
Menüs, Symbol- und Statusleisten
Die wichtigste Klasse ist ToolStripMenuItem, die anderen beiden spielen in herkömmlichen Anwendungen eine untergeordnete Rolle. Die kommenden Ausführungen werden daher fast ausschließlich diesen Menüelementtyp voraussetzen. Ein MenuStrip-Objekt beschreibt einen Container, in dem alle Hauptmenüelemente enthalten sind, die in einer Auflistung vom Typ ToolStripItemCollection verwaltet werden. Die Auflistung kann Objekte beinhalten, die vom Typ ToolStripItem bzw. von dieser Klasse abgeleitet sind. Dazu gehören unter anderem auch die eben erwähnten Typen ToolStripMenuItem, ToolStripComboBox und ToolStripTextBox. Die Menüelemente werden dem MenuStrip-Objekt mit der Methode Add oder AddRange der Eigenschaft Items hinzugefügt, die eine Referenz auf die Menüelemente liefert. Me.menuStrip1.Items.Add(New ToolStripMenuItem("Hilfe"))
Meistens werden Sie das Menü im Designer definieren. Sollten Sie aber eine Anwendung schreiben, die zur Laufzeit ein Hauptmenü dynamisch aufbaut, können Sie auch per Code jederzeit das Menü programmgesteuert manipulieren. Dazu stehen Ihnen die Methoden Remove, Contains, Clear usw. zur Verfügung. Die Auflistung enthält alle Menüelemente des Hauptmenüs. Handelt es sich um den Typ ToolStripMenuItem, kann dieser ein Knotenpunkt eines untergeordneten Menüs sein, das sich selbst wiederum aus mehr oder weniger vielen Menüelementen zusammensetzt. Die einem ToolStripMenuItem-Objekt untergeordneten Menüelemente werden in einer dem ToolStripMenuItem eigenen Liste verwaltet, die auch den Typ ToolStripItemCollection hat. Die Eigenschaft DropDownItems liefert die Referenz auf die Auflistung: newMenu.DropDownItems.Add(New ToolStripMenuItem("1. Untermenü")) newMenu.DropDownItems.Add(New ToolStripMenuItem("2. Untermenü"))
Menüerzeugung Ausgangspunkt eines Menüs ist eine Instanz der Klasse MenuStrip. Das Objekt steht für das Hauptmenü einer Form, beschreibt aber selbst keine Menüeinträge, sondern ist nur ein Container, der Menüeinträge oder auch Combo- bzw. Textboxen aufnehmen kann. Sie können sich zwischen drei Möglichkeiten entscheiden, um ein vollständiges Menü bereitzustellen: 왘
Sie geben die Menüelemente direkt im Designer der Form an.
왘
Sie benutzen den Elementauflistungs-Editor.
왘
Sie lassen die gebräuchlisten Menüelemente automatisch von Visual Studio generieren.
Alle drei Techniken lassen sich parallel verwenden. Welche Sie davon bevorzugt einsetzen, hängt von Ihrem Programmierstil ab.
Menüs im Designer der Form Am einfachsten erstellen Sie ein Menü, indem Sie aus der Toolbox ein MenuStrip-Control in die Form oder auf die gewünschte Lasche des ToolStripContainers ziehen. Im noch leeren Menü in der Form können Sie nun die Beschriftung der einzelnen Menüelemente direkt ein-
793
12.13
12
Die wichtigsten Steuerelemente
tragen – sowohl für die Hauptmenüelemente als auch für alle Unterelemente. Wünschen Sie die Darstellung als Combo- oder Textbox, öffnen Sie über die Pfeilschaltfläche einfach die entsprechende Auswahlliste zu dem neuen Element:
Abbildung 12.24
Auswahlliste der möglichen Menüelemente
In den meisten Anwendungen ist ein Buchstabe der Beschriftung eines Menüelements unterstrichen. Durch Drücken der (Alt)-Taste in Kombination mit der Taste, die dem unterstrichenen Zeichen entspricht, wird das Untermenü dieses Hauptmenüpunkts geöffnet. Sie erzielen dieses Navigationsverhalten, wenn Sie vor den zu unterstreichenden Buchstaben ein »&«-Zeichen setzen. Um innerhalb des Menütextes ein »&«-Zeichen anzuzeigen, müssen im Text zwei »&«-Zeichen aufeinanderfolgen. Trennstriche innerhalb eines Untermenüs zur optischen Unterstützung und Verdeutlichung von Gruppierungen erhalten Sie durch die Eingabe eines einfachen Bindestrichs (-). In diesem Moment wandelt sich der Typ ToolStripMenuItem in den Typ ToolStripSeparator um, der naturgemäß deutlich weniger Eigenschaften aufweist. Sollten Sie zu einem späteren Zeitpunkt feststellen, dass die Anordnung der Menüelemente nicht optimal ist, können Sie diese mit der Maus verschieben.
Menüs mit dem Elementauflistungs-Editor Um ein Menü zu erstellen, können Sie auch einen Assistenten starten. Dazu öffnen Sie über die Eigenschaft Items im Eigenschaftsfenster des MenuStrip-Objekts den ElementauflistungsEditor, der Sie intuitiv bei der Zusammenstellung und weiteren Gestaltung des Menüs unterstützt (siehe Abbildung 12.25). Der Editor erlaubt die Definition einer einzelnen Menüebene. Wollen Sie ausgehend von einem Menüelement ein Untermenü bereitstellen, klicken Sie auf die Eigenschaft DropDownItems des Menüelements – entweder im Elementauflistungs-Editor oder im Eigenschaftsfenster. Daraufhin öffnet sich ein weiteres Editorfenster, in dem Sie die nächste Menüebene definieren können. Automatische Erzeugung eines Menüs Die meisten Anwendungen haben viele Menüeinträge gemeinsam, deren Untermenüs ebenfalls weitgehend identisch sind (Datei, Bearbeiten, Optionen, Hilfe). Um Ihnen einen Teil der stupiden Tipparbeit abzunehmen, wird Ihnen im unteren Bereich des Eigenschaftsfensters der Link Standardelemente einfügen angeboten (siehe Abbildung 12.26). Die eben erwähnten Menüelemente samt allen üblichen Untermenüs werden durch einen Klick auf den Link erzeugt.
794
Menüs, Symbol- und Statusleisten
Abbildung 12.25
Der Elementauflistungs-Editor
Abbildung 12.26
»Standardelemente hinzufügen« im Eigenschaftsfenster eines MenuStrip
Dynamische Menüanordnung (AllowItemReorder) Normalerweise haben alle Menüelemente eine feste Position zueinander: Links außen steht Datei, rechts daneben Bearbeiten usw. Das ist eine Konvention, an die Sie sich auch halten sollten. Nichtsdestotrotz können Sie es auch dem Benutzer überlassen, eine ganz individuelle Anordnung festzulegen. Diese Möglichkeit wird kontrolliert von der Eigenschaft AllowItemReorder. Sie steht zunächst auf False und verbietet dem Anwender die Neuanordnung. Stellen Sie True ein, kann der Benutzer die Menüelemente mit der Maustaste bei gleichzeitig gedrückter (Alt)-Taste an die von ihm bevorzugte Position ziehen. 795
12.13
12
Die wichtigsten Steuerelemente
Ausrichtung der Menüelemente (LayoutStyle) Ändert sich die Größe einer Form zur Laufzeit, kann das dazu führen, dass die Gesamtbreite der Menüzeile die Breite der Form überschreitet. Was passiert mit den Menüelementen, die rechts im Menü zu finden sind und keinen Platz mehr finden, um sich darzustellen? Sie fallen weg und werden nicht angezeigt. Müssen Sie sicherstellen, dass der Anwender auch diese Menüpunkte auswählen kann, sollten Sie die Eigenschaft LayoutStyle anders als HorizontalStackWithOverflow festlegen. LayoutStyle kann insgesamt die fünf in Tabelle 12.13 gezeigten Werte annehmen, die in der Enumeration ToolStripLayoutStyle festgelegt sind. Konstante
Beschreibung
Flow
Elemente fließen nach Bedarf horizontal oder vertikal.
HorizontalStackWithOverflow
Elemente sind horizontal angeordnet und laufen gegebenenfalls über.
StackWithOverflow
Elemente werden automatisch angeordnet.
Table
Elemente werden linksbündig angeordnet.
VerticalStackWithOverflow
Elemente sind vertikal angeordnet, innerhalb des Steuerelements zentriert und laufen gegebenenfalls über.
Tabelle 12.13
Die Enumeration »ToolStripLayoutStyle«
Menüleiste verschieben Normalerweise werden Menüleisten immer oben in der Form angezeigt. Einige Programme erlauben es dem Anwender, das linke Ende der Menüleiste mit der Maus zu greifen und diese an einer anderen Seite der Form anzudocken. Dieses Feature steht auch Ihnen zur Verfügung. Sie müssen dazu die Eigenschaft GripStyle des MenuStrips auf Visible stellen; der Standard ist Hidden. Es werden dann drei Punkte am linken Rand angezeigt, die als Griff zum Ziehen dienen. Wichtig ist, dass sich die Menüleiste in einem ToolStripContainer befindet, denn nur dieser Container ist in der Lage, das Steuerelement neu auszurichten. Einen Nebeneffekt hat dabei jedoch die Einstellung der vorher besprochenen Eigenschaft LayoutStyle. Haben Sie diese nämlich auf Flow eingestellt, hat die Einstellung von GripStyle keine Auswirkungen: Die Ziehpunkte werden nicht angezeigt, und die Menüleiste ist nicht verschiebbar.
12.13.3 Eigenschaften von Menüs Wenden wir uns nun zuerst den Eigenschaften der ToolStripMenuItem-Objekte zu, die übliche Menüelemente beschreiben.
Beschriftung Die Beschriftung eines Menüelements wird durch die Eigenschaft Text festgelegt. Sie können die Beschriftung auch zur Laufzeit ändern, zum Beispiel so: menuNeu.Text = "Neu"
796
Menüs, Symbol- und Statusleisten
Shortcuts Zusätzlich zum Tastenkürzel in Verbindung mit der (Alt)-Taste kann ein Menüelement über eine vordefinierte Tastenkombination angesteuert werden, die in der Eigenschaft ShortcutKeys im Eigenschaftsfenster festgelegt wird. Sobald Sie die Pfeilschaltfläche in der Wertespalte der Eigenschaft ShortcutKeys eines ToolStripMenuItem anklicken, öffnet sich ein Hilfsfenster, in dem Sie die gewünschte Tastenkombination festlegen können. Falls Sie die Tastenkürzel nicht anzeigen wollen, können Sie die Eigenschaft ShowShortcutKeys abweichend von der Standardvorgabe auf False einstellen.
Sichtbarkeit Wird die Eigenschaft Visible auf False gesetzt, ist ein Menüelement unsichtbar. Bemerkenswert ist dabei die Wechselwirkung zwischen Visible und der Eigenschaft ShortcutKeys. Selbst dann, wenn ein Menüelement, dem eine Tastenkombination zugewiesen wird, mit Visible=False ausgeblendet wird, reagiert die Tastenkombination und führt den mit dem Click-Ereignis verknüpften Ereignishandler aus: eine herrliche Spielwiese zur Implementierung undokumentierter Funktionalitäten. Markierung Menüelemente, die mit einem Häkchen versehen sind, verdeutlichen, ob die durch den Menüpunkt beschriebene Option gesetzt ist oder nicht. Diese Menüelemente bieten eine Auswahlmöglichkeit, die vergleichbar mit der einer Checkbox ist. Sie arbeiten also wie ein »Ein/ Aus-Schalter«. Dieses Verhalten wird bei einem Menüpunkt angeschaltet, wenn die Eigenschaft CheckedOnClick auf True gesetzt wird. Zur Laufzeit wird das Menüelement dann mit einem Häkchen versehen, wenn vor dem Anklicken kein Häkchen gesetzt war, und umgekehrt. Das Markieren eines Menüelements mit einem Häkchen wird von der Eigenschaft Checked gesteuert. Legen Sie Checked=True fest, wird das Häkchen angezeigt. ToolStripMenuItem bietet keine direkte Möglichkeit, anstelle des Häkchens einen ausgefüllten Kreis zu setzen, der ähnlich wie eine Gruppe von Radiobuttons signalisiert, dass nur eine von mehreren angebotenen Auswahloptionen ausgewählt werden kann. Können oder wollen Sie auf ein solches Feature nicht verzichten, haben Sie zwei Alternativen: 왘
Sie blenden ein Image im ToolStripMenuItem ein bzw. aus.
왘
Sie machen sich die Eigenschaft CheckState des Menüelements zunutze.
Die Eigenschaft CheckState kennt drei Einstellungen, die in der gleichnamigen Enumeration vordefiniert sind: CheckState.Checked, CheckState.Unchecked und CheckState.Indeterminate. Letztere dient eigentlich dazu, einen nicht eindeutigen Zustand anzuzeigen, lässt sich aber durchaus auch für unser Vorhaben gebrauchen, denn zur Laufzeit wird zwar kein ausgefüllter Kreis, aber zumindest ein auf der Spitze stehendes Quadrat angezeigt. Wenn Sie auf diese Weise einen Radiobutton-Effekt simulieren wollen, sollten Sie die Eigenschaft CheckOnClick=False einstellen. Im Click-Ereignis des Menüelements müssen Sie dann
797
12.13
12
Die wichtigsten Steuerelemente
dafür sorgen, dass je nach Ausgangszustand zwischen CheckState.Unchecked und CheckState.Indeterminate umgeschaltet wird, wie im folgenden Codefragment gezeigt. Private Sub menuRadio_Click(sender As Object, e As EventArgs) If menuRadio.CheckState = CheckState.Indeterminate Then menuRadio.CheckState = CheckState.Unchecked Else If menuRadio.CheckState = CheckState.Unchecked Then menuRadio.CheckState = CheckState.Indeterminate End If End Sub
Die Wechselwirkung zwischen mehreren Menüelementen, die eine Gruppe bilden, aus der nur ein Menüelement markiert sein darf, müssen Sie natürlich selbst codieren.
Symbol anzeigen Um ein Menüelement mit einem Symbol auszustatten, öffnen Sie über das Eigenschaftsfenster der Eigenschaft Image den Dialog Ressource auswählen. Importieren Sie hier das Symbol, das Sie dem Menüelement zuordnen möchten. Unter Umständen sollten Sie unter ImageTransparentColor eine Farbe angeben, die Transparenz zulässt, sodass die Hintergrundfarbe des Symbols keinen unansehnlichen Effekt im Menü hinterlässt. Wenn Sie wollen, stehen Ihnen über ein Bildchen hinaus weitere Möglichkeiten offen, jedes Menüelement individuell zu gestalten. Mit DisplayStyle können Sie festlegen, ob Sie sowohl das Symbol als auch den Text, nur das Symbol bzw. den Text oder keines der beiden Elemente angezeigt haben wollen. In den Menüelementen lässt sich auch die Schriftdarstellung mit der Eigenschaft Font problemlos beliebig anpassen. Sie können die Hintergrund- oder auch Schriftfarbe mit BackColor und ForeColor setzen und zu guter Letzt auch mit BackgroundImage ein Hintergrundbildchen einbauen und dessen Darstellung mit BackgroundImageLayout innerhalb des Menüpunkts nach Belieben festlegen.
ToolStripComboBox und ToolStripTextBox An dieser Stelle auf die Eigenschaften und Verhaltensweisen der Menüelemente ToolStripComboBox und ToolStripTextBox einzugehen, erübrigt sich, denn die beiden unterscheiden sich nur geringfügig von denen, die Sie einer Windows-Form hinzufügen. Ereignisse Unter den vielen Ereignissen ist nur ein Ereignis besonders wichtig, nämlich Click. Dieses wird ausgelöst, wenn der Benutzer einen Menüpunkt anklickt. Enthält der Menüpunkt Unterpunkte, kommt es zu einer Ereigniskette: 왘
DropDownOpening wird ausgelöst, wenn die Maus über einen Menüpunkt gezogen wird,
der weitere Unterpunkte besitzt. Das Untermenü ist dann noch nicht geöffnet. 왘
DropDownOpened wird ausgelöst, nachdem sich das Untermenü geöffnet hat.
798
Menüs, Symbol- und Statusleisten
왘
Klickt der Anwender auf ein Element des Untermenüs, tritt zuerst im übergeordneten Menüelement DropDownItemClicked auf, bevor das Click-Ereignis des ausgewählten Menüelements die »eigentliche« Funktion ausführt.
ToolStripMenuItem-Objekte können auch per Doppelklick aktiviert werden. Dazu muss die
Eigenschaft DoubleClickEnabled=True gesetzt werden. Die Standardvorgabe ist False. Allerdings wirkt sich ein Doppelklick nur auf die Elemente im Hauptmenü aus, da alle anderen Menüelemente schon nach dem Empfang des ersten Mausklicks geschlossen werden und einen Doppelklick nicht mehr verarbeiten können. Alle Menüelemente reagieren zudem auf die Mausereignisse wie beispielsweise MouseMove, MouseDown und MouseUp. Das folgende Programmbeispiel zeigt den Einsatz des Ereignisses DropDownOpening. Im Ereignishandler wird geprüft, ob sich in der Zwischenablage Textdaten befinden. Wenn ja, wird der Menüpunkt Einfügen aktiviert angezeigt, sonst ist er deaktiviert. Klickt der Anwender zur Laufzeit auf Einfügen, wird in der Textbox der Inhalt der Zwischenablage angezeigt. '...\WinControls\Menü\DropDownOpening.vb
Public Class DropDownOpening Private Sub MenüÖffnen(ByVal sender As Object, ByVal e As EventArgs) _ Handles BearbeitenMenü.DropDownOpening Einfügen.Enabled = _ Clipboard.GetDataObject().GetDataPresent(DataFormats.Text) End Sub Private Sub Einfügen_Click(ByVal sender As Object, ByVal e As EventArgs) _ Handles Einfügen.Click Dim daten As IDataObject = Clipboard.GetDataObject() Textfenster.Text = daten.GetData(GetType(String)) Clipboard.Clear() 'zum Testen eines leeren Clipboards End Sub End Class
Mit der Klasse Clipboard, die Methoden bereitstellt, um mit der Zwischenablage Daten auszutauschen, werden wir uns weiter unten in Abschnitt 18.4 noch intensiver beschäftigen.
Beispielprogramm Im folgenden Beispiel enthält die Form in Abbildung 12.27 zwei Textboxen, die durch ein Splitter-Steuerelement voneinander getrennt sind. Ein Splitter dient dazu, einen Clientbereich in mehrere einzelne, voneinander unabhängige Bereiche aufzuteilen. Die Teilbereiche werden meist von Steuerelementen vollständig beansprucht (Einstellung Dock=Fill). Zur Laufzeit kann der Benutzer mit der Maus den Splitter greifen und nach eigenem Ermessen die Teilbereiche verkleinern oder vergrößern, ähnlich wie im Windows Explorer. Das Menü ist in ein ToolStripContainer-Steuerelement eingebettet. Das wäre im vorliegenden Fall zwar nicht notwendig, aber die gezeigte Form soll im Verlauf des Kapitels noch wei-
799
12.13
12
Die wichtigsten Steuerelemente
ter entwickelt werden und neben einer Status- auch eine Symbolleiste erhalten, die innerhalb der Form verschoben werden soll. Das Hauptmenü hat die drei Menüpunkte Datei, Bearbeiten und Optionen nebst den wichtigsten Untermenüelementen. Optionen hat nur einen Untermenüpunkt (Hintergrundfarbe), der selbst eine weitere Untermenüebene hat. In dieser werden vier Menüelemente angezeigt, aus denen der Benutzer die gewünschte Hintergrundfarbe der beiden Textboxen auswählen kann. Die aktuelle Hintergrundfarbe ist mit einem Häkchen versehen.
Abbildung 12.27
Das Fenster des Beispiels »MenuStrip«
In diesem Beispiel sind bereits die Untermenüelemente von Bearbeiten enthalten, jedoch noch ohne Programmcode. Dazu muss ich Ihnen zuerst noch die Programmierung der Zwischenablage weiter unten in diesem Kapitel vorstellen. Die Anwendung ist jedoch bereits in der Lage, Dateien in der oberen Textbox zu öffnen, diese zu bearbeiten und anschließend auf einfache Weise zu speichern. Außerdem wird gezeigt, wie Sie die Unterelemente von Hintergrundfarbe codieren können. '...\WinControls\Menü\MenuStrip.vb
Public Class MenuStrip Private Sub Farbe(ByVal sender As Object, ByVal e As EventArgs) _ Handles Weiß.Click, Gelb.Click, Rot.Click, Blau.Click For Each temp As ToolStripMenuItem In Hintergrundfarbe.DropDownItems temp.Checked = sender Is temp Next Dim farbe As Color Select CType(sender, ToolStripMenuItem).Text Case "Weiß" : farbe = Color.White Case "Rot" : farbe = Color.Red Case "Gelb" : farbe = Color.Yellow Case "Blau" : farbe = Color.Blue End Select
800
Menüs, Symbol- und Statusleisten
Oben.BackColor = farbe Unten.BackColor = farbe End Sub Private Sub Neu_Click(ByVal sender As Object, ByVal e As EventArgs) _ Handles Neu.Click Oben.Text = "" Unten.Text = "" End Sub Private Sub Öffnen_Click(ByVal sender As Object, ByVal e As EventArgs) _ Handles Öffnen.Click ÖffnenDialog.Filter = "Text-Dateien (.txt)|*.txt|Alle Dateien (*.*)|*.*" Dim result As DialogResult = ÖffnenDialog.ShowDialog() Try If result = DialogResult.OK Then Dim sr As New IO.StreamReader(ÖffnenDialog.FileName) Oben.Text = sr.ReadToEnd() sr.Close() End If Catch ex As Exception MessageBox.Show(ex.Message, "Texteditor") End Try End Sub Private Sub Speichern_Click(ByVal sender As Object, ByVal e As EventArgs) _ Handles Speichern.Click Dim result As DialogResult = SichernDialog.ShowDialog() Try If result = DialogResult.OK Then Dim sw As New IO.StreamWriter(SichernDialog.FileName, False) sw.Write(Oben.Text) sw.Close() End If Catch ex As Exception MessageBox.Show(ex.Message) End Try End Sub Private Sub Beenden_Click(ByVal sender As Object, ByVal e As EventArgs) _ Handles Beenden.Click Application.Exit() End Sub End Class
Auf den Code zum Öffnen und Speichern einer Datei in den jeweiligen Ereignishandlern der entsprechenden Menüelemente gehe ich an dieser Stelle nicht ein. Die Abschnitte 12.12.1, »Datei zum Öffnen wählen (OpenFileDialog)«, und 12.12.2, »Datei zum Speichern wählen (SaveFileDialog)«, beschreiben eine ausführlicher codierte Variante. Sehen wir uns vielmehr die Methode Farbe() an, die als gemeinsamer Ereignishandler für alle Click-Ereignisse der Untermenüelemente von Hintergrundfarbe dient.
801
12.13
12
Die wichtigsten Steuerelemente
Zuerst wird in einer For Each-Schleife die Auflistung der farbgebenden Menüebene durchlaufen. Die Referenz auf die Auflistung liefert die Eigenschaft DropDownItems des Objekts Hintergrundfarbe. Der Test sender Is temp gewährleistet, dass nur der ereignisauslösende Menüpunkt ein Häkchen bekommt. For Each temp As ToolStripMenuItem In Hintergrundfarbe.DropDownItems temp.Checked = Der Testsender Is temp Next
Je nach Wahl des Anwenders muss nun die Hintergrundfarbe der Textboxen angepasst und das dazugehörige Menüelement markiert werden. Um festzustellen, welches Element für den Aufruf von Farbe() gesorgt hat, muss der Parameter sender untersucht werden. Da wir wissen, dass ein Objekt vom Typ ToolStripItem hinter dem Aufruf steckt, können wir sender ohne Prüfung konvertieren. Um den Ereignisauslöser zu ermitteln, wertet die Methode die Eigenschaft Text aus. Das Ergebnis der Untersuchung wird einer Select-Anweisung als Ausdruck übergeben: Select CType(sender, ToolStripMenuItem).Text
In den Case-Zweigen wird anschließend die Hintergrundfarbe ermittelt und das dafür zuständige Menüelement mit einem Häkchen versehen. Die Farbe wird danach gesetzt.
12.13.4 Kontextmenüs Ein Kontextmenü ist einfacher zu entwickeln als ein herkömmliches Menü, da ein Hauptmenü nicht erforderlich ist und ein Kontextmenü nur selten eine tiefere Gliederung hat. Kontextmenüs werden durch das Steuerelement ConextMenustrip beschrieben. Sie können der Form mehrere Kontextmenüs hinzufügen, da sich in der Regel die Kontextmenüs der einzelnen Komponenten unterscheiden. Die Zuordnung eines Kontextmenüs zu einem bestimmten Steuerelement erfolgt über die Eigenschaft ContextMenuStrip des jeweiligen Controls. Einige Steuerelemente besitzen bereits ein eingebautes Kontextmenü, beispielsweise die Textbox. Weisen Sie einem solchem Steuerelement ein benutzerdefiniertes Kontextmenü zu, wird das eingebaute ausgeblendet. Das bedeutet also: ganz oder gar nicht. Wenn Sie ein eingebautes Kontextmenü ergänzen wollen, müssen Sie ein neues Kontextmenü definieren und die Funktionalität des eingebauten nachprogrammieren, um dann die eigenen Ergänzungen vorzunehmen. Die Menüelemente eines Kontextmenüs sind vom Typ ToolStripMenuItem. Dieser Typ wurde in den vorigen Abschnitten beschrieben.
Position Wollen Sie vom Standardverhalten abweichen und das Öffnen eines Kontextmenüs einem anderen Ereignis zuordnen oder das Kontextmenü an einer anderen als der standardmäßig vordefinierten Position anzeigen lassen, müssen Sie in Ihrem Code die Show-Methode des ContextMenuStrip-Objekts aufrufen (anstatt des eingebauten Mechanismus):
802
Menüs, Symbol- und Statusleisten
Public Sub Show(control As Control, pos As Point)
Dem ersten Parameter wird eine Referenz auf die Komponente übergeben, der das Kontextmenü zugeordnet ist. Im zweiten Parameter stehen die Koordinaten, an denen das Menü angezeigt werden soll. Sie sind relativ zu der Komponente des ersten Parameters. Angenommen, Sie beabsichtigen, das Kontextmenü einer Form nicht an der Spitze der Mauszeigerposition, sondern im Ursprungspunkt der Form zu öffnen, dann lautet die Anweisung: contextMenuStrip1.Show(Me, New Point(0, 0)
Dabei ist contextMenuStrip1 die Referenz auf ein ContextMenuStrip-Objekt. Jetzt müssen Sie noch einen Ereignishandler auswählen, der diesen Aufruf ausführt. Natürlich können Sie ein beliebiges Ereignis wählen; die Wahl wird durch die gewünschte Benutzerführung bestimmt. Üblicherweise handelt es sich dabei um das Ereignis MouseUp, das ausgelöst wird, wenn eine der Maustasten losgelassen wird. Es ist vom Typ MouseEventHandler. Der zweite Parameter ist ein Objekt vom Typ MouseEventArgs mit Daten zum Ereignis. Dazu gehört auch die Eigenschaft Button vom Typ der Aufzählung MouseButtons, die unter anderem drei Konstanten für drei Maustasten enthält: Left, Middle und Right. Im Ereignishandler des MouseUp-Events wird, wenn die rechte Maustaste das Ereignis ausgelöst hat, die Methode Show des ContextMenuStrip-Objekts mit passenden Argumenten aufgerufen: Private Sub Form1_MouseUp(sender As Object, e As MouseEventArgs) If e.Button = MouseButtons.Right Then _ contextMenuStrip1.Show(Me, New Point(0,0)) End Sub
Ereignisse Öffnet ein Anwender mit der üblicherweise rechten Maustaste das Kontextmenü einer Komponente, treten zwei Ereignisse auf: Opening, bevor das Kontextmenü geöffnet wird, und Opened direkt nach dem Öffnen. Damit verhalten sich beide Ereignisse genauso wie die Ereignisse DropDownOpening und DropDownOpened eines ToolStripMenuItem-Objekts. Im ersten Moment erscheint es so, als wären die Ereignisse identisch. Dennoch gibt es einen Unterschied: Der zweite Parameter von Opening ist vom Typ CancelEventArgs und ermöglicht über die Eigenschaft Cancel, das eingeleitete Öffnen des Kontextmenüs abzubrechen. Dazu ist e.Cancel=True zu setzen. Diese Möglichkeit bietet DropDownOpening nicht.
12.13.5 Symbolleisten Wenden wir uns nun der Gestaltung einer Symbolleiste zu, die auf der Klasse ToolStrip basiert. Eine Symbolleiste enthält eine Reihe von Schaltflächen, die ein schnelles Ausführen der wichtigsten Funktionen einer Anwendung ermöglichen. Dabei ist eine Symbolleiste nicht nur auf Schaltflächen beschränkt, sondern kann auch andere Elemente enthalten. In Tabelle 12.14 sind alle von ToolStrip unterstützten Steuerelemente aufgeführt.
803
12.13
12
Die wichtigsten Steuerelemente
Steuerelement
Beschreibung
ToolStripButton
Gewöhnliche Schaltfläche. Optional als Schalter, der bis zu einem erneuten Klick vertieft angezeigt wird.
ToolStripLabel
Beschriftung in der Symbolleiste
ToolStripDropDownButton
Dropdown-Steuerelement, mit dem nach einem Klick ein Menü oder ein anderes Fenster angezeigt wird.
ToolStripSplitButton
Kombination aus einer gewöhnlichen Schaltfläche und einem Dropdown-Button
ToolStripSeparator
Senkrechter Strich zur Gruppierung von Steuerelementen
ToolStripComboBox
Fest in der Symbolleiste verankertes Kombinationslistenfeld
ToolStripTextBox
Fest in der Symbolleiste verankerte Textbox
ToolStripProgressBar
Fest in der Symbolleiste verankerter Fortschrittsbalken
Tabelle 12.14
Steuerelemente einer Symbolleiste
Alle zu einem ToolStrip-Objekt gehörenden Steuerelemente werden in einer Auflistung vom Typ ToolStripItemCollection verwaltet, deren Methoden stark einer ArrayList ähneln. Eine Referenz darauf liefert die Eigenschaft Items. Natürlich können Sie eine komplette Symbolleiste auch mittels Programmcode erzeugen. Am schnellsten stellen Sie eine Symbolleiste mit Visual Studio zusammen. Die Schaltfläche mit den drei Punkten der Eigenschaft Items im Eigenschaftsfenster des ToolStrip-Objekts öffnet einen Elementauflistungs-Editor zum Zusammenstellen der Leiste. Sie haben ihn schon im Zusammenhang mit den Menüs kennengelernt. Alternativ können Sie im Forms-Designer die Symbolleiste zusammensetzen oder im Fußbereich des Eigenschaftsfensters den Link Standardelemente einfügen anklicken. Letzterer generiert eine Symbolleiste mit den gängigsten Schaltflächen.
Ausrichtung der Symbolleistenelemente Ist die Symbolleiste breiter als die zur Verfügung stehende Breite der Form, ist das Standardverhalten ein wenig anders als das der Menüleiste. Während Menüelemente rechts »verschwinden«, wird für fehlende Symbolleistenelemente rechts eine Dropdown-Schaltfläche angezeigt, die beim Anklicken alle nicht sichtbaren Elemente anbietet. Dieses Verhalten wird durch den Wert HorizontalStackWithOverflow der Eigenschaft LayoutStyle erzeugt. Flow sorgt dafür, die Symbolleiste zu umbrechen.
Eigenschaften der Symbolleistenschaltflächen Symbolleistenschaltflächen vom Typ ToolStripButton werden angeklickt und führen daraufhin eine bestimmte Aktion aus. Sie weisen in der Regel ein typisches Bildchen auf, damit der Anwender sehr schnell eine bestimmte Funktionalität identifizieren kann. Das Bildchen ordnen Sie über die Eigenschaft Image zu. Standardmäßig werden die Symbole in einer Größe von 16 x 16 Pixel dargestellt. Eine andere Darstellungsgröße stellen Sie mit der Eigenschaft ImageScalingSize des ToolStrip-Objekts ein.
804
Menüs, Symbol- und Statusleisten
Soll die Schaltfläche über das Bild hinaus auch eine Beschriftung haben, tragen Sie diese in Text ein. Damit Bildchen und Beschriftung angezeigt werden, weisen Sie der Eigenschaft DisplayStyle den Wert ImageAndText zu (Standardwert ist Image). Mit ImageAlign und TextAlign richten Sie Text und Bild auf der Schaltfläche aus. Die relative Lage zueinander kontrolliert die Eigenschaft TextImageRelation. Nehmen Sie keine Änderungen vor, sehen Sie links das Bildchen und rechts daneben den Text. Sie können aber auch die Positionen tauschen oder das Bild über oder unter dem Text anzeigen lassen. Weiter oben haben Sie erfahren, dass mit der Eigenschaft AllowItemReorder=True dem Benutzer die Möglichkeit gegeben wird, Menüelemente mit der Maus bei gedrückter (Alt)Taste beliebig anzuordnen. Das ist auch bei der Symbolleiste möglich. Manche Symbolleistenschaltflächen ähneln in der Wirkungsweise den Checkboxen, andere erinnern an eine Gruppierung von Radiobuttons. Abbildung 12.28 zeigt ein Beispiel.
Abbildung 12.28
Ausschnitt aus einer Symbolleiste von Microsoft Word
Um Text fett, kursiv oder unterstrichen darzustellen, werden Schaltflächen eingesetzt, die bei jedem Anklicken ihren Zustand verändern. Diese agieren wie Checkboxen, sind also entweder an oder aus. Um Text am Rand eines Dokuments auszurichten, stehen vier Schaltflächen zur Verfügung, die direkt voneinander abhängig sind. Nur eine der Schaltflächen kann aktiviert sein. Schaltflächen dieser Art verhalten sich wie Radiobuttons. Funktional nicht zusammenhängende Schaltflächen werden optisch durch einen senkrechten Strich getrennt. Er ist vom Typ ToolStripSeparator. Die Trennung macht es dem Anwender leichter, sich in umfangreichen Leisten zurechtzufinden. Um eine Symbolleistenschaltfläche mit checkboxähnlichem Verhalten bereitzustellen, brauchen Sie nur die Eigenschaft CheckOnClick=True einzustellen und die Eigenschaft Checked auszuwerten, die bei eingedrücktem Schalter True ist und False sonst. Die Auswertung des aktuellen Zustands können Sie in den Ereignissen Click oder CheckedChanged vornehmen. Beide Ereignisse werden ausgelöst, nachdem sich der Zustand der Schaltfläche geändert hat. Private Sub tsButton_CheckedChanged(sender As Object, e As EventArgs) If tsButton.Checked = True Then ' Anweisungen Else If tsButton.Checked = False Then ' Anweisungen End If End Sub
Nicht immer kann ein Zustand eindeutig als wahr oder falsch angesehen werden. Ein gutes Beispiel dafür ist die linke Schaltfläche für fette Schrift in Abbildung 12.28. Enthält der markierte Text sowohl fette als auch »normale« Buchstaben, wird ein dritter Zustand benötigt. Daher darf die Eigenschaft CheckState die drei Werte Checked, Unchecked oder Indeterminate annehmen. Indeterminate beschreibt den nicht eindeutig definierbaren Zustand.
805
12.13
12
Die wichtigsten Steuerelemente
Und was ist, wenn sich drei Symbolleistenschaltflächen in einer Gruppe befinden, wobei sich die Schaltflächen wie Radiobuttons verhalten sollen, also zu jedem Zeitpunkt nur einer ausgewählt sein darf? Was im ersten Moment vielleicht recht schwierig aussieht, ist recht einfach zu lösen. Das folgende Codefragment zeigt eine mögliche Implementierung für drei Symbolleistenschaltflächen namens tsButton1, tsButton2 und tsButton3: Private Sub tsButton1_Click(sender As Object, e As EventArgs) If tsButton1.Checked Then tsButton2.Checked = False tsButton3.Checked = False Else tsButton2.Checked = True End If End Sub Private Sub tsButton2_Click(sender As Object, e As EventArgs) If tsButton2.Checked Then tsButton1.Checked = False tsButton3.Checked = False Else tsButton3.Checked = True End If End Sub Private Sub tsButton3_Click(sender As Object, e As EventArgs) If tsButton3.Checked Then tsButton1.Checked = False tsButton2.Checked = False Else tsButton1.Checked = True End If End Sub
Dieser Code spiegelt das Verhalten der Schaltflächen zur Textausrichtung unter MS Word wider. Klicken Sie hier auf eine gedrückte Symbolleistenschaltfläche, wird die angeklickte deaktiviert und die in der Reihenfolge nächste ausgewählt. Die Liste Items der Symbolleiste hält Referenzen vom Typ ToolStripItem, der Basisklasse der Steuerelemente einer Symbolleiste. In einer Schleife müssen Sie daher eine Typkonvertierung durchführen, bevor Sie auf ein Steuerelement zugreifen können.
Aufklappschaltflächen Eine Aufklappschaltfläche unterscheidet sich von den bisherigen Symbolleistenschaltflächen dadurch, dass sie nach dem Klicken ein Untermenü öffnet (siehe Abbildung 12.29). Diese Schaltflächen haben den Typ ToolStripDropDownButton. Die Menüelemente sind vom Typ ToolStripMenuItem und haben deshalb dieselben Fähigkeiten wie alle anderen Menüelemente.
806
Menüs, Symbol- und Statusleisten
Abbildung 12.29
Symbolleiste mit einer Aufklappschaltfläche
Zwittertyp ToolStripSplitButton ToolStripSplitButton-Objekte stellen eine Kombination aus ToolStripButton und ToolStripDropDownButton dar. Klickt man auf den linken Teil der Schaltfläche, wird das ClickEreignis ausgeführt, klickt man dagegen auf den rechten Teil mit dem Pfeil, öffnet sich ein Untermenü, das Sie im Forms-Designer zusammenstellen können. Alternativ können Sie den Weg über den bekannten Editor nehmen, zu dem Sie Zugang über die Eigenschaft DropDownItems haben. Comboboxen In vielen Anwendungen sind Comboboxen zu einem wichtigen Element in der Symbolleiste geworden. Diese Kombinationslistenfelder im ToolStrip-Steuerelement sind vom Typ ToolStripComboBox. Zur individuellen Anpassung der Darstellung steht neben BackColor, ForeColor und Font auch die Eigenschaft FlatStyle zur Verfügung, die die Werte Flat, Popup, Standard und System annehmen kann. Die Elemente, die Sie beim Öffnen dem Benutzer anbieten wollen, tragen Sie in eine Auflistung ein, deren Referenz die Eigenschaft Items bereitstellt. Mit Sorted können alle Einträge sortiert werden. Mit der Eigenschaft DropDownStyle legen Sie fest, wie die Liste angezeigt wird und ob der Anwender zur Laufzeit im Textfeld Einträge vornehmen kann. In der Standardeinstellung DropDown kann der Anwender den Text ändern, bei DropDownList nicht. Simple als dritte Möglichkeit ist optisch nicht besonders gelungen und lässt die Liste immer aufgeklappt, wodurch die Symbolleiste sehr groß wird. Damit direkt nach dem Start der Anwendung keine leere Combobox angezeigt wird, sollten Sie mit SelectedIndex oder SelectedItem ein Startelement festlegen, zum Beispiel: toolStripComboBox1.SelectedIndex = 0
Dazu bietet sich das Load-Ereignis der Form an. Da jede Änderung oder Auswahl durch den Benutzer das Textfeld ändert, spiegelt die Eigenschaft Text die Wahl des Benutzers wider. Für die Auswertung ist das Ereignis Click ungeeignet, da es bei jedem Klick auf das Kombinationslistenfeld ausgelöst wird, auch wenn dadurch keine Auswahl stattfindet. Besser implementieren Sie den erforderlichen Programmcode im Ereignishandler von SelectedIndexChanged.
807
12.13
12
Die wichtigsten Steuerelemente
Textboxen Unveränderbaren Text stellen Sie am besten in einem ToolStripLabel dar, während ein durch den Benutzer änderbarer Text in eine ToolStripTextBox gehört. Obwohl letztere die Eigenschaft Lines hat, wird immer nur eine Zeile angezeigt, die auch in der Eigenschaft Text gespeichert ist. Symbolleistentextboxen unterstützen Autovervollständigung und weitere Eigenschaften, die auch eine herkömmliche TextBox hat. Fortschrittsbalken (ProgressBar) Zu guter Letzt können Sie dem Anwender zur Laufzeit auch einen Fortschrittsbalken anbieten, der über den aktuellen Stand einer länger andauernden Operation informiert. Fortschrittsbalken in einer Symbolleiste sind vom Typ ToolStripProgressBar. Die Fähigkeiten sind praktisch identisch zu denen eines Objekts vom Typ ProgressBar, dem wir uns in Abschnitt 12.15, »Fortschrittsbalken (ProgressBar)«, widmen. Ereignisse Viele Steuerelemente der Symbolleiste reagieren auf das Click-Ereignis, zum Beispiel übliche Schaltflächen, Label und Aufklappschaltflächen. Sie können jedes Ereignis separat programmieren, was oft zu einer sehr großen Zahl von Ereignishandlern führt. Eine andere, übersichtlichere Lösung ist ein gemeinsamer Ereignishandler für alle ClickEreignisse einer Symbolleiste. Hier hilft uns das ToolStrip-Objekt weiter, das mittels ItemClicked das passende Ereignis bereitstellt. Wie üblich gibt der erste Parameter des Ereignishandlers an, wer das Ereignis ausgelöst hat: die Symbolschaltfläche. Erst durch die Eigenschaft ClickedItem des zweiten Parameters vom Typ ToolStripItemClickedEventArgs können Sie die Schaltfläche ermitteln, die dem Ereignis zugeordnet ist. Das folgende Codefragment macht die Programmreaktion vom Index der Schaltfläche innerhalb der Symbolleiste abhängig. Private Sub toolStrip1_ItemClicked(sender As object, _ e As ToolStripItemClickedEventArgs) Select toolStrip1.Items.IndexOf(e.ClickedItem) Case 0: MessageBox.Show("Erste Schaltfläche angeklickt.") Case 1: MessageBox.Show("Zweite Schaltfläche angeklickt.") ... End Select End Sub
Die feste Bindung an einen Index lässt das Programm scheitern, wenn sich der Index zur Laufzeit ändert. Zum Beispiel kann der Benutzer die Reihenfolge in der Symbolleiste ändern, wenn er Elemente verschieben darf, weil AllowItemReorder=True gesetzt ist. Mit der geänderten Reihenfolge zerbricht die feste Zuordnung zwischen Schaltflächen und Indizes. Ein flexibler Lösungsansatz identifiziert die angeklickte Symbolleistenschaltfläche auf einem anderen Weg. Alle Steuerelemente haben die Eigenschaft Tag, die eine beliebige Information speichern kann. Die Schaltflächen erhalten also ein »Fähnchen« zur Identifikation.
808
Menüs, Symbol- und Statusleisten
In unserem konkreten Fall können wir in der Tag-Eigenschaft jeder Symbolleistenschaltfläche eine in der Auflistung eindeutige Zeichenfolge speichern, die wir in einer Select-Anweisung nutzen. Private Sub toolStrip1_ItemClicked(sender As object, _ e As ToolStripItemClickedEventArgs) Select e.ClickedItem.Tag.ToString() Case "Open": MessageBox.Show("Erste Schaltfläche angeklickt.") Case "New" : MessageBox.Show("Zweite Schaltfläche angeklickt.") ... End Select End Sub
Beispiel einer komplexeren Symbolleiste Das folgende Beispiel DropDownButtons enthält in der Symbolleiste sowohl Umschaltflächen als auch eine Aufklappschaltfläche namens colorButton. Damit kann der Text einer formfüllenden Textbox entweder fett, kursiv, unterstrichen oder in einer anderen Schriftfarbe dargestellt werden. Eine DropDownButton-Symbolleistenschaltfläche zeigt dabei immer die aktuell gewählte Schriftfarbe als quadratisches Symbol an und passt sich gegebenenfalls auch Änderungen an. '...\WinControls\Menü\Symbolleiste.vb
Public Class Symbolleiste Private stil As FontStyle = fontstyle.Regular Private farbwahl() As ToolStripMenuItem Private Sub Laden(ByVal sender As Object, ByVal e As EventArgs) _ Handles MyBase.Load farbwahl = New ToolStripMenuItem() {Rot, Grün, Blau} Rot_Click(Rot, Nothing) Textfenster.SelectionStart = Textfenster.Text.Length End Sub Private Sub Werkzeuge_ItemClicked(ByVal sender As Object, _ ByVal e As ToolStripItemClickedEventArgs) Handles Werkzeuge.ItemClicked If e.ClickedItem.Tag IsNot Nothing Then Select CType(e.ClickedItem.Tag, String) Case "fett" : stil = stil Xor FontStyle.Bold Case "kursiv" : stil = stil Xor FontStyle.Italic Case "unterstrichen" : stil = stil Xor FontStyle.Underline End Select Textfenster.Font = New Font(Textfenster.Font, stil) End If End Sub Private Sub SetFontColor(button As ToolStripItem, color As Color) Dim bmp As New Bitmap(16, 16)
809
12.13
12
Die wichtigsten Steuerelemente
Dim graph As Graphics = Graphics.FromImage(bmp) graph.Clear(color) ' füllt Bitmap mit der neuen Farbe Farben.Image = bmp Textfenster.ForeColor = color End Sub Private Sub Rot_Click(ByVal sender As Object, ByVal e As EventArgs) _ Handles Rot.Click SetFontColor(Werkzeuge.Items("Rot"), Color.Red) SetChecked(sender) End Sub Private Sub Grün_Click(ByVal sender As Object, ByVal e As EventArgs) _ Handles Grün.Click SetFontColor(Werkzeuge.Items("Grün"), Color.Green) SetChecked(sender) End Sub Private Sub Blau_Click(ByVal sender As Object, ByVal e As EventArgs) _ Handles Blau.Click SetFontColor(Werkzeuge.Items("Blau"), Color.Blue) SetChecked(sender) End Sub Private Sub SetChecked(ByVal sender As Object) For Each farbe In farbwahl farbe.Checked = sender Is farbe Next End Sub End Class
Betrachten wir zuerst die drei Symbolleistenschaltflächen, mit denen der Schriftstil fett, kursiv und unterstrichen eingestellt werden kann. Im Ereignishandler von ItemClicked wird geprüft, ob eine Schaltfläche in der Symbolleiste angeklickt worden ist, die für die Umschaltung einer der genannten Schriftstile verantwortlich ist. In der Select-Anweisung wird dazu die Tag-Eigenschaft ausgewertet. Da durch den Typ FontStyle eine Bitkombination beschrieben wird, müssen wir nur das entsprechende Bit, das Auskunft über die fette, kursive oder unterstrichene Darstellung einer Schrift gibt, bei jedem Klick auf die zugeordnete Schaltfläche invertieren. Dazu bietet sich der Xor-Operator an. Bei der Schaltfläche zum Umstellen auf fette bzw. nicht fette Darstellung sieht die Anweisung dazu wie folgt aus: stil = stil Xor FontStyle.Bold
Die Menüelemente Rot, Gelb und Blau der Aufklappschaltfläche werden im Array farbwahl vom Typ ToolStripMenuItem verwaltet, das auf Klassenebene deklariert und im Load-Ereignis initialisiert wird. Wir erleichtern uns damit den Zugriff auf die Menüelemente, wenn das Auswahlhäkchen gesetzt wird, um die gewählte Schriftfarbe auch optisch hervorzuheben. Diese Aufgabe übernimmt die Methode SetChecked().
810
Menüs, Symbol- und Statusleisten
Um dem Anwender die aktuelle Farbwahl durch ein farblich angepasstes Bildchen in der Symbolleiste anzuzeigen, müssen wir in wenig in die Trickkiste greifen. Jeder Klick auf eines der drei Menüelemente löst ein Ereignis aus. Im zugeordneten Ereignishandler wird die Methode SetFontColor aufgerufen und dabei die Referenz auf die Symbolleistenschaltfläche und die gewählte Farbe übergeben; zum Beispiel: SetFontColor(Werkzeuge.Items("Rot"), Color.Red)
Jedes Bildchen der Symbolleiste basiert auf einer Bitmap. In SetFontColor wird daher zuerst mit Dim bmp As New Bitmap(16, 16)
ein Objekt dieses Typs in der Größe 16 × 16 Pixel neu erzeugt. Um grafische Operationen ausführen zu können, benötigen wir die Referenz auf das Graphics-Objekt dieser Bitmap: Dim graph As Graphics = Graphics.FromImage(bmp)
Danach legen wir die Hintergrundfarbe mit graph.Clear(color)
fest. Die neue Bitmap ist damit fertig. Wir weisen nur noch dessen Referenz der Eigenschaft Image des ToolStripDropDownButton-Objekts Farben zu. Farben.Image = bmp
12.13.6 Statusleisten Eine Statusleiste vom Typ StatusStrip stellt Informationen dar, meist am unteren Rand einer Form. In manchen Anwendungen wird der Zustand der Tasten angezeigt, in anderen wiederum das Datum oder die Uhrzeit. Genauso wie in der Symbolleiste können Sie Steuerelemente in die Statusleiste platzieren, wenn auch die Auswahl nicht ganz so vielfältig ist: 왘
StatusLabel (Klasse ToolStripStatusLabel)
왘
ProgressBar (Klasse ToolStripProgressBar)
왘
DropDownButton (Klasse ToolStripDropDownButton)
왘
SplitButton (Klasse ToolStripSplitButton)
Alle Unterelemente eines StatusStrip-Objekts werden wie üblich in einer Auflistung namens Items verwaltet. Sie ist vom Typ ToolStripItemCollection. Natürlich können Sie die Ele-
mente in der Statusleiste auch zur Laufzeit dynamisch hinzufügen.
Statusleiste erzeugen Visual Studio bietet Ihnen zwei komfortable Möglichkeiten, eine Statusleiste zu definieren. Zum einen können Sie die Eigenschaft Items des StatusStrip-Objekts im Eigenschaftsfenster anklicken und damit den Elementauflistungs-Editor öffnen, in dem Sie die Elemente hinzufü-
811
12.13
12
Die wichtigsten Steuerelemente
gen und deren Eigenschaften festlegen. Die Alternative ist das Hinzufügen im Forms-Designer. Die Eigenschaften lassen sich dann für das aktuell markierte Statusleistenelement im Eigenschaftsfenster bestimmen. Außer im ProgressBar-Element können Sie in allen ein Bildchen anzeigen lassen, das in der Eigenschaft Image gespeichert wird. Wollen Sie neben dem Bildchen auch Text ausgeben, dürfen Sie nicht vergessen, dass die Eigenschaft DisplayStyle auf ImageAndText eingestellt sein muss. Die Anordnung zwischen Bild und Text regelt die Eigenschaft TextImageRelation. TextAlign und ImageAlign regeln die Ausrichtung auf dem übergeordneten Container. Auch hier kann der Anwender mit AllowItemReorder=True Statusleistenelemente neu anordnen, genau wie bei Menü- und Symbolleisten.
Steuerelemente ProgressBar, SplitButton und DropDownButton unterscheiden sich nicht von ihren Pendants in der Menüleiste. Das gängigste Element, ToolStripStatusLabel, weist zwei besondere Eigenschaften auf. Beschriftungsfelder können in der Statusleiste optisch hervorgehoben werden. Hierzu dient die Eigenschaft BorderStyle, die vom Typ Border3DStyle ist, eine Enumeration im Namensraum System.Windows.Forms. Entscheiden Sie sich für Border3DStyle.Sunken, wird der übliche abgesenkte Rahmen für das Label angezeigt. Dazu Sie müssen auch spezifizieren, welche Seitenränder von der Einstellung in BorderStyle profitieren sollen. Standardmäßig ist das keine. Mit der Eigenschaft BorderSites können Sie jede der vier Seiten ausdrücklich zulassen.
Ereignis ItemClicked In den meisten Fällen wird eine Statusleiste den Anwender nur mit Informationen über den aktuellen Zustand der Anwendung versorgen. Seltener wird eine Statusleiste auf Benutzeraktionen reagieren. Soll dies aber möglich sein, verwenden Sie das Ereignis ItemClicked des StatusStrip-Objekts, das ausgelöst wird, wenn der Anwender auf ein Element außer dem Fortschrittsbalken (ProgressBar) klickt. Der Namenskonvention folgend hat der zweite Parameter des Ereignishandlers den Typ ToolStripItemClickedEventArgs. Der Typbezeichner mag zwar einen langen Namen haben,
das Objekt hat aber wenig zu bieten. Die einzige Eigenschaft ClickedItem vom Typ ToolStripItem speichert eine Referenz auf das vom Benutzer geklickte Element. Hier finden Sie übrigens auch die Begründung, weshalb ein Fortschrittsbalken nicht auf ItemClicked reagiert – die Klasse ToolStripProgressbar ist nicht von der abstrakten Klasse ToolStripItem abgeleitet.
12.14
Bildlaufleisten (HScrollBar und VScrollBar)
Einige Windows-Komponenten, die automatisch Bildlaufleisten am Rand ihres Clientbereichs bereitstellen können, habe ich Ihnen bereits vorgestellt. Die Klasse TextBox gehört ebenso dazu wie die Klasse Form.
812
Bildlaufleisten (HScrollBar und VScrollBar)
Mit HScrollBar und VScrollBar stehen Ihnen zwei Klassen zur Verfügung, um in einem Bereich stufenlos oder in vordefinierten Sprüngen Werte einzustellen, die von anderen Komponenten ausgewertet werden können. Beide Klassen beerben die gemeinsame Basisklasse ScollBar, die ihrerseits ein direkter Abkömmling von Control ist. HScrollBar ist eine horizontale Bildlaufleiste, VScrollBar eine vertikale. Bis auf die Orientierung sind beide Bildlaufleisten identisch.
Eigenschaften Nur die fünf in Tabelle 12.15 gezeigten Eigenschaften kontrollieren die Bildlaufleisten. Das Steuerelement kann eine Wertskala zwischen einem definierten Minimum und einem Maximum beschreiben. Value gibt die aktuelle Position an. Klickt der Benutzer auf eine der beiden Pfeiltasten, ändert sich Value um einen Wert, der in SmallChange festgelegt ist. Bei einem Klick in den Bereich zwischen dem Bildlauffeld und einer Pfeiltaste ändert sich Value dagegen um einen Wert aus LargeChange. Value erreicht zur Laufzeit den Wert Maximum nicht. Tatsächlich reicht der Wertebereich nur
von Minimum bis Maximum – LargeChange + 1
Warum ist der Maximalwert von Value ein anderer, als in Maximum festgelegt? Stellen Sie sich dazu ein Dokument vor, das 1000 Zeilen Text enthält, die von 0 bis 999 indiziert sind. Nehmen wir an, dass auf einer Seite 25 Zeilen angezeigt werden können. Um mit einem Mausklick von Seite zu Seite zu navigieren, legen Sie den Wert LargeChange=25 fest. Wird die erste Seite angezeigt, handelt es sich um die Zeilen 0 bis 24. Dafür steht Value=0. Wird um LargeChange gescrollt, werden die Zeilen 25 bis 49 angezeigt. In diesem Moment hat Value den Wert 25. Die letzten 25 Zeilen des Dokuments (975 bis 999) werden angezeigt, wenn Value=975 aufweist. Der Maximalwert wird also überhaupt nicht benötigt, um durch das gesamte Dokument navigieren zu können. Eigenschaft
Beschreibung
Value
Aktuelle Position des Bildlauffeldes
Minimum
Minimalwert
Maximum
Maximalwert
SmallChange
Betrag der Wertänderung von Value bei kleinen Schritten (»Zeile«)
LargeChange
Betrag der Wertänderung von Value bei großen Schritten (»Seite«)
Tabelle 12.15
Eigenschaften der Klasse »ScrollBar«
Ereignisse Von der Basisklasse ScrollBar erben die beiden abgeleiteten Klassen zwei wichtige Ereignisse: ValueChanged und Scroll. In den meisten Fällen werden Sie das Ereignis ValueChanged behandeln, das immer dann auftritt, wenn sich die Eigenschaft Value der Bildlaufleiste geändert hat. Scroll tritt auf, wenn das Bildlauffeld mit der Tastatur oder der Maus verschoben wird. Wird mittels Programmcode ein neuer Wert für Value gesetzt, wird Scroll nicht ausgelöst. Dessen
813
12.14
12
Die wichtigsten Steuerelemente
zweiter Parameter vom Typ ScrollEventArgs liefert in seiner Eigenschaft NewValue den neu eingestellten Wert der Bildlaufleiste, in OldValue den alten. Wird das Bildlauffeld mit der Tastatur oder der Maus verschoben, kommt es zu der Ereigniskette Scroll/ValueChanged/Scroll. Dabei zeigt sich, dass Value bei der ersten Auslösung von Scoll noch den alten Wert hat, bei der zweiten Auslösung den neuen.
Farbreglerbeispiel Im folgenden Beispiel dient eine Instanz des Steuerelements PictureBox dazu, eine Fläche mit einer Hintergrundfarbe darzustellen, deren Rot-, Grün- und Blauanteil mittels Bildlaufleisten stufenlos eingestellt werden kann (siehe Abbildung 12.30). Die Eigenschaft Minimum der Bildlaufleisten steht auf 0, SmallChange auf 1 und LargeChange auf 15. Da Value den Wert 255 erreichen muss, berechnet sich Maximum zu: 255 + LargeChange – 1 = 269
Die Werte der drei Bildlaufleisten werden der klassengebundenen Methode FromArgb der Klasse Color übergeben, um daraus den passenden Farbwert zu generieren.
Abbildung 12.30
Formular des Beispiels »ScrollBars«
'...\WinControls\Container\ScrollBars.vb
Public Class ScrollBars Private Sub Einfärben(ByVal sender As Object, ByVal e As EventArgs) _ Handles Rotwert.ValueChanged, Grünwert.ValueChanged, Blauwert.ValueChanged Farbe.BackColor = Color.FromArgb( _ Rotwert.Value, Grünwert.Value, Blauwert.Value) Rot.Text = Convert.ToString(Rotwert.Value) Grün.Text = Convert.ToString(Grünwert.Value) Blau.Text = Convert.ToString(Blauwert.Value) End Sub End Class
814
Fortschrittsbalken (ProgressBar)
12.15
Fortschrittsbalken (ProgressBar)
Manchmal führen Anwendungen eine länger dauernde Aufgabe durch, z. B. das Kopieren von Dateien, das Herunterladen von Dateien aus dem Internet oder eine länger dauernde Rechenoperation – Vorgänge, die dem Anwender Geduld abverlangen. Ein Programm, das eine länger andauernde Aufgabe ausführt, sollte den Benutzer davon in Kenntnis setzen, damit dieser nicht denkt, die Anwendung reagiere nicht mehr. Man kann das dadurch erreichen, dass man für die Dauer der Ausführung den Mauszeiger als Sanduhr darstellt. Das ist ein erster Ansatz, um auf die Hintergrundtätigkeit der Anwendung aufmerksam zu machen. Wie lange diese andauern wird, kann der Anwender jedoch nicht im Geringsten abschätzen. Jetzt schlägt die Stunde des ProgressBar-Steuerelements, das dem Anwender über einen Fortschrittsbalken den Stand der Operation rückmeldet. Die Programmierung dieses Steuerelements ähnelt den zuvor behandelten Bildlaufleisten. Zur Entwicklungszeit legen Sie die Eigenschaften Maximum und Minimum fest, zur Laufzeit ermitteln Sie aus dem aktuellen Stand der Operation den Wert der Eigenschaft Value. Entweder weisen Sie der Eigenschaft einen Wert zu oder rufen die Methode PerformStep auf, um die aktuelle Position der Statusanzeige zu erhöhen. Die Schrittweite wird durch die Eigenschaft Step vorgegeben. Mit der Eigenschaft Style können Sie den Anzeigestil des Fortschrittsbalkens festlegen. Dazu gibt es drei Einstellmöglichkeiten, die Sie der Tabelle 12.16 entnehmen können. Konstante
Anzeige im Steuerelement
Blocks
Steigende Anzahl unterteilter Blöcke
Continuous
Ein durchgehender Balken
Marquee
Blöcke, die wie eine Laufschrift den Anzeigebereich durchlaufen
Tabelle 12.16
Die Enumeration »ProgressBarStyle«
Für die Einstellung ProgressBarStyle.Marquee kontrolliert die Eigenschaft MarqueeAnimationSpeed, wie schnell die Laufbalken durch den Anzeigebereich laufen. Die Angabe erfolgt in Millisekunden.
Beispielprogramm Im folgenden Beispielprogramm wird eine Datei geöffnet und deren Inhalt in einer Textbox ausgegeben. Da der Ladevorgang eine längere Zeit in Anspruch nimmt, wird der Ladestatus in einem ProgressBar-Steuerelement angezeigt (siehe Abbildung 12.31). Zu Demonstrationszwecken erzeugen wir im Folgenden eine Datei, während die Form geladen wird. Dabei wird in einer Schleife 20001-mal ein Zeichen in die Datei geschrieben, jeweils ein Buchstabe des Alphabets.
815
12.15
12
Die wichtigsten Steuerelemente
Abbildung 12.31
Formular des Beispiels »Fortschritt«
'...\WinControls\Dialoge\Fortschritt.vb
Public Class Fortschritt Private file As String = IO.Path.Combine( _ Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), _ "TestFortschritt.txt") Private Sub Laden(ByVal sender As Object, ByVal e As EventArgs) _ Handles MyBase.Load ' Datei erzeugen und mit Zeichen füllen Dim fs As IO.FileStream = IO.File.Open(file, IO.FileMode.Create) For i As Integer = 0 To 20000 : fs.WriteByte((i Mod 26) + 97) : Next fs.Close() End Sub Private Sub Aktion(ByVal sender As Object, ByVal e As EventArgs) _ Handles Füllen.Click Me.Cursor = Cursors.WaitCursor Textfenster.Clear() Textfenster.Refresh() ' Fortschrittsbalken einstellen Fortschrittsbalken.Maximum = (New IO.FileInfo(file)).Length Fortschrittsbalken.Step = 1 Fortschrittsbalken.Value = 0 ' Datei einlesen und Fortschrittsbalken aktualisieren Dim text As String = "" Dim fs As IO.FileStream = IO.File.Open(file, IO.FileMode.Open) While True Dim var As Integer = fs.ReadByte()
816
Animationen (Timer)
If var = –1 Then Exit While 'Dateiende text += Convert.ToChar(var) Fortschrittsbalken.PerformStep() End While fs.Close() Textfenster.Text = text Me.Cursor = Cursors.Default End Sub Private Sub Ende(ByVal sender As Object, ByVal e As FormClosedEventArgs) _ Handles MyBase.FormClosed IO.File.Delete(file) End Sub End Class
Im Ereignishandler des Click-Ereignisses der Schaltfläche wird – nachdem ein Wartecursor gesetzt und die Textbox mit einem Objekt von Typ FileInfo geleert wurde – die Größe der einzulesenden Datei ermittelt, um die Eigenschaft Maximum des ProgressBar-Steuerelements zu setzen. Als Schrittweite des Fortschrittsbalkens wird der Wert 1 festgelegt. Zum Einlesen wird ein Objekt vom Typ FileStream verwendet, das in einer Schleife die Datei byteweise mit ReadByte einliest und nach jedem gelesenen Byte den Fortschrittsbalken aktualisiert. Sind alle Daten eingelesen, wird die Schleife beendet, der Dateistrom geschlossen und der Inhalt der Datei, der sich in der Variablen text angesammelt hat, in der Textbox angezeigt. Ich habe bewusst auf einen StringBuilder verzichtet, um das Programm etwas auszubremsen. Zum Schluss wird der Mauscursor wiederhergestellt. Beim Schließen der Form wird die erstellte Datei gelöscht.
12.16
Animationen (Timer)
Möchten Sie, dass eine Methode in bestimmten Zeitintervallen wiederholt ausgeführt wird? Dann benötigen Sie ein Steuerelement vom Typ Timer, das im Komponentenfach des Windows Forms-Designers abgelegt und zur Laufzeit nicht angezeigt wird.
Eigenschaften und Methoden Timer-Objekte haben zwei wichtige Eigenschaften: Enabled und Interval. Mit Enabled wird der Taktgeber ein- und ausgeschaltet. Damit er nicht unkontrolliert loslegt, ist er standardmäßig mittels Enabled=False ausgeschaltet. Erst wenn die zu triggernde Aktion wirklich starten soll, setzen Sie den Wert selbst auf True. Ein Timer löst alle durch die Eigenschaft Interval festgelegte Anzahl Millisekunden das Ereignis Tick aus, sodass dessen Ereignishandler in festen Zeitabständen aufgerufen wird. Der Standardwert für Interval ist 100 Millisekunden. Um den Timer zum Beispiel jede Sekunde auszuführen, setzen Sie Interval=1000. Der Wert muss größer als null sein. Starten können Sie den Taktgeber mit Enabled=True oder Start(), während Enabled=False oder Stop() ihn anhalten.
817
12.16
12
Die wichtigsten Steuerelemente
Hinweis Da Windows von Hause aus nicht echtzeitfähig ist, werden die Zeitintervalle nicht perfekt eingehalten.
Programmbeispiel Das folgende Beispiel zeigt, wie einfach das Timer-Steuerelement zu programmieren ist. Die Einstellung Interval ist auf 1000 Millisekunden festgelegt. Mit der Schaltfläche in Abbildung 12.32 kann der Taktgeber aktiviert bzw. deaktiviert werden. Im Ereignishandler des TickEreignisses werden das aktuelle Datum und die aktuelle Zeit in die Titelleiste eingetragen und sekündlich aktualisiert.
Abbildung 12.32
Formular des Beispiels »Taktgeber«
'...\WinControls\Dialoge\Taktgeber.vb
Public Class Taktgeber Private Sub Kontrolle_Click(ByVal sender As Object, ByVal e As EventArgs) _ Handles Kontrolle.Click Takt.Enabled = Not Takt.Enabled Kontrolle.Text = "Timer " & If(Takt.Enabled, "stoppen", "starten") End Sub Private Sub Aktualisieren(ByVal sender As Object, ByVal e As EventArgs) _ Handles Takt.Tick Text = Now End Sub End Class
12.17
Container (Panel)
Panel ist ein Steuerelement, das selbst als Container für andere Steuerelemente dient, die in
der Liste Controls vom Typ Control.ControlCollection gespeichert werden. Ein Panel könnte man daher als einen eigenständigen Clientbereich bezeichnen, der im Gegensatz zum Clientbereich einer Form selbst auf einen umgebenden Container angewiesen ist.
818
Registerkarten (TabControl)
Um Bildlaufleisten anzuzeigen, muss AutoScroll=True gesetzt werden. Damit kann auch in verdeckte Bereiche gescrollt werden, wenn der Platzbedarf der enthaltenen Steuerelemente größer als der Anzeigebereich ist. Panel eignen sich auch besonders in Zusammenhang mit grafischen Komponenten. Wenn Sie
beispielsweise ein Bild anzeigen wollen, dessen Abmessungen es nicht erlauben, dass das vollständige Bild angezeigt werden kann, platzieren Sie ein PictureBox-Steuerelement auf einem Panel. Der Eigenschaft Image der PictureBox übergeben Sie den Pfad auf das anzuzeigende Bild und legen außerdem die Eigenschaft SizeMode auf PictureBoxSizeMode.AutoSize fest. Ist das Panel bildlauffähig, wird, falls erforderlich, automatisch eine horizontale und vertikale Bildlaufleiste angezeigt. Panel weisen standardmäßig keinen Rahmen auf und unterscheiden sich optisch nicht vom
Clientbereich der darunterliegenden Komponente. Mit der Eigenschaft BorderStyle können Sie für das Steuerelement drei verschiedene Rahmenarten festlegen: 왘
BorderStyle.None
왘
BorderStyle.Fixed3D
왘
BorderStyle.FixedSingle
12.18
Registerkarten (TabControl)
Registerkarten helfen, einen Dialog besser und übersichtlicher zu gliedern und Funktionalitäten zu gruppieren. Typischerweise werden Registerkarten gern für Optionen benutzt. Alle Registerkarten einer Form bilden einen logischen Verbund und werden durch die Klasse TabControl repräsentiert. Diese fasst die einzelnen Registerkarten vom Typ TabPage zusammen und verwaltet sie. Über die Eigenschaft TabPages des TabControl-Objekts können Sie der Auflistung die neue Registerkarten hinzufügen. Dazu markieren Sie diese Eigenschaft im Eigenschaftsfenster und klicken auf die Schaltfläche in der Wertespalte. Es öffnet sich daraufhin ein Dialog, über dessen Schaltfläche Hinzufügen eine neue Registerkarte angelegt wird. Im gleichen Dialog können Sie auch die Eigenschaften der neuen Registerkarte eintragen.
Eigenschaften Die optische Darstellung der Registerkarten wird mit der Eigenschaft Appearance des Registerkarten-Steuerelements festgelegt. Zur Auswahl stehen drei Optionen: Normal, Buttons und FlatButtons (siehe Abbildung 12.33). Standardmäßig werden alle Registerkarten am oberen Rand des Steuerelements angezeigt. Mit Alignment können Sie die Tabellenreiter aber auch rechts, links oder unten anordnen.
Manchmal passen nicht alle Reiter in eine Zeile. Um dann durch alle Registerkarten navigieren zu können, werden zwei Pfeilschalter eingeblendet. Alternativ können Sie mit der Einstellung MultiLine=True die Registerkarten auf mehrere Zeilen aufteilen. Passen alle Reiter in eine Zeile, hat die Einstellung keinen Effekt.
819
12.18
12
Die wichtigsten Steuerelemente
Abbildung 12.33
Darstellungsformen der Registerkarten
SizeMode gibt an, wie die Größe der einzelnen Karteireiter bestimmt wird, und Padding legt
den Abstand zwischen der Beschriftung und dem Rand des Karteireiters fest. Padding.X gibt dabei den Abstand zum linken und rechten Rand des Karteireiters in Pixel an, Padding.Y den Abstand zum oberen und unteren Rand. Jeder Karteireiter kann neben der Schrift auch noch ein Bildchen anzeigen. Voraussetzung dafür ist, dass in der Eigenschaft ImageList des TabControl-Objekts ein ImageList-Steuerelement eingetragen ist, das Sie vorher der Form hinzugefügt haben.
Registerkarten aktivieren Um aus der aktuellen Registerkarte heraus mittels Programmcode eine andere zu aktivieren, stellt die Klasse TabControl zwei Eigenschaften zur Verfügung: SelectedIndex und SelectedTab. Angenommen, Kartei ist der Name eines TabControl-Objekts mit mindestens drei Registerkarten, dann können Sie mit Kartei.SelectedIndex = 2
die dritte Registerkarte öffnen. Es gibt auch zwei Methoden zur Aktivierung einer anderen Registerkarte. SelectTab können Sie den Index, den Namen oder die Referenz der Registerkarte übergeben, die aktiviert werden soll. DeselectTab aktiviert die nächste in der Liste folgende Registerkarte.
Ereignisse Wenn sich die aktuelle Registerkarte in einem TabControl ändert, treten die folgenden Ereignisse in der angegebenen Reihenfolge ein: 왘
Deselecting
왘
Deselected
왘
Selecting
왘
Selected
820
Baumansichten (TreeView und TreeNode) Deselecting und Deselected werden vor bzw. nach der Deaktivierung einer Registerkarte
ausgelöst, Selecting und Selected analog, wenn eine Registerkarte aktiviert wird. Mit dem zweiten Parameter des Ereignishandlers von Deselecting und Selecting vom Typ TabControlCancelEventArgs können Sie über dessen Eigenschaft Cancel den eingeleiteten
Vorgang abbrechen. Das haben wir schon bei anderen Ereignissen ähnlich gesehen, zum Beispiel beim Ereignis FormClosing der Form. Werden Deselected und Selected ausgelöst, ist der Vorgang bereits abgeschlossen und der zweite Parameter hat den Typ TabControlEventArgs. Den beiden Args-Objekten können Sie über Eigenschaften den Index und die Referenz der betroffenen Registerkarte entnehmen. Die Eigenschaft Action vom Typ TabControlAction spezifiziert das auslösende Ereignis. Die vier Werte der Enumeration haben die gleichen Namen wie die Ereignisse.
Steuerelemente in den Registerkarten Die Gestaltung der Registerkarten ist denkbar einfach. In der Entwicklungsumgebung wählen Sie eine Registerkarte aus und können darin in bekannter Weise die Steuerelemente einfügen. Jede Registerkarte verwaltet die Steuerelemente in einer eigenen Auflistung vom Typ Control.ControlCollection, deren Referenz die Eigenschaft Controls bereitstellt.
12.19
Baumansichten (TreeView und TreeNode)
Im Clientbereich des Microsoft Explorer finden Sie drei Steuerelemente, mit denen wir uns in diesem und den folgenden Abschnitten beschäftigen wollen. Im linken Teil des Fensters werden in einer hierarchischen Struktur alle verfügbaren Laufwerke aufgeführt, die sich bei einem Klick auf das »+«-Zeichen öffnen und die darin enthaltene Ordnerstruktur zeigen. Dieses Verhalten wird durch ein TreeView-Steuerelement bereitgestellt, das auch als Strukturansicht bezeichnet wird. Von einem Ordner werden Dateien verwaltet, die wahlweise in verschiedenen Ansichten im rechten Teil des Explorers ausgegeben werden. .NET bietet uns zu diesem Zweck das Steuerelement ListView an. Die beiden durch ein TreeView- und ein ListView-Objekt gebildeten Teilfenster haben keine statische Breite, sondern können vom Anwender nach Bedarf unter Beibehaltung der Breite des Fensters mit der Maus vergrößert oder verkleinert werden. Für diese Änderung ist das Steuerelement Splitter zuständig.
12.19.1 Knotenpunkte definieren Zunächst widmen wir uns dem TreeView-Control, das sich nicht nur zur Anzeige der Laufwerke und deren untergeordneten Verzeichnissen eignet, sondern auch zur Darstellung beliebiger hierarchischer Strukturen wie beispielsweise der einer Datenbank. Betrachten Sie zunächst Abbildung 12.34, in der in einem TreeView-Steuerelement die fünf Erdteile unterhalb des Stammknotens Erde ausgegeben werden.
821
12.19
12
Die wichtigsten Steuerelemente
Abbildung 12.34
Einfache Anzeige in einem TreeView
Eine TreeView-Strukturansicht ist ein Container für TreeNode-Objekte. Ein Objekt dieses Typs entspricht einem Knoten in der Strukturansicht – unabhängig von der Position innerhalb der hierarchischen Struktur. In Abbildung 12.34 haben wir es mit sechs Knoten zu tun: Erde, Amerika, Asien, Afrika, Australien und Europa. Sowohl die TreeView als auch jedes TreeNode-Objekt kann beliebig viele TreeNode-Objekte enthalten, die in einer Auflistung vom Typ TreeNodeCollection gespeichert werden. Das Strukturansicht-Steuerelement in der Abbildung enthält in seiner eigenen Auflistung nur ein TreeNode-Objekt, nämlich Erde. Dieser Knoten wird von der TreeNodeCollection des Controls verwaltet, während die Elemente Amerika, Asien usw. in der TreeNodeCollection des Knotens Erde enthalten sind. Die Knoten werden in der Eigenschaft Nodes gespeichert, die wie alle Auflistungen Methoden enthält, um auf die verwalteten TreeNode-Objekte zuzugreifen, neue hinzuzufügen oder zu löschen. Die Anweisungen zur Erzeugung der Kontinente der letzten Abbildung zeigen den Aufbau des Baumes: Dim tr As New TreeView() Dim kn As String() tr.Dock = DockStyle.Fill tr.Nodes.Add("Erde") kn = New String() {"Amerika", "Asien", "Afrika", "Australien", "Europa"} For Each k As String In kn : tr.Nodes(0).Nodes.Add(k) : Next Me.Controls.Add(tr)
Nach der Instanziierung mit Dim tr As New TreeView()
sowie der Positionierung und Größenfestlegung wird das Element Erde hinzugefügt, indem zu der TreeNodeCollection der Strukturansicht ein TreeNode-Objekt hinzugefügt wird: tr.Nodes.Add("Erde")
Erde ist das erste Element in der Auflistung und hat den Index 0. Es wird auch als Stammele-
ment bezeichnet. Durch Übergabe einer Zeichenfolge an Add() wird ein TreeNode-Objekt erzeugt, das Sie auch direkt übergeben können. Weil auch das TreeNodeCollection-Objekt, wie die meisten anderen Auflistungen, einen Indexer bereitstellt, kann durch Angabe eines Index ein Element angesprochen werden. Über
822
Baumansichten (TreeView und TreeNode)
dessen Eigenschaft Nodes haben Sie Zugriff auf die TreeNodeCollection des Elements. Darauf lässt sich erneut die Add-Methode aufrufen, zum Beispiel: tr.Nodes(0).Nodes.Add(k)
Alle Elemente unterhalb der Stammelemente werden als untergeordnete Elemente bezeichnet, zum Beispiel der Wert Amerika für k. Alternativ fügen Sie mit der AddRange-Methode ein Array vom Typ TreeNode hinzu. Dim nodesErde As TreeNode() = New TreeNode() {New TreeNode("Amerika"), _ New TreeNode("Asien"), New TreeNode("Afrika"), _ New TreeNode("Australien"), New TreeNode("Europa")} tr.Nodes(0).Nodes.AddRange(nodesErde)
Ausnahmslos jedes in einer Strukturansicht angezeigte Element ist ein TreeNode-Objekt, das über eine eigene Auflistung ihm untergeordneter TreeNode-Objekte verfügt, die nur dann leer ist, wenn das Element das letzte in der Hierarchie ist. Mit dieser Erkenntnis können wir unser Beispiel beliebig tief strukturieren und jedem Erdteil Staaten zuordnen, die dann selbst in ihrer eigenen Auflistung Städte enthalten. Das erste Beispiel zum TreeView-Steuerelement wird jetzt in diesem Sinn ergänzt: ein paar Staaten Amerikas und Europas werden ergänzt, zusätzlich Städte in Deutschland und den USA. Dim tr As New TreeView() Dim kn As String() tr.Dock = DockStyle.Fill tr.Nodes.Add("Erde") kn = New String() {"Amerika", "Asien", "Afrika", "Australien", "Europa"} For Each k As String In kn : tr.Nodes(0).Nodes.Add(k) : Next Dim europa As TreeNode = tr.Nodes(0).Nodes(4) kn = New String() {"England", "Frankreich", "Deutschland", "Italien"} For Each s As String In kn : europa.Nodes.Add(s) : Next Dim deutschland As TreeNode = europa.Nodes(2) kn = New String() {"Bonn", "Aachen", "Hamburg", "Berlin"} For Each s As String In kn : deutschland.Nodes.Add(s) : Next Dim amerika As TreeNode = tr.Nodes(0).Nodes(0) kn = New String() {"USA", "Kanada", "Mexiko"} For Each s As String In kn : amerika.Nodes.Add(s) : Next Dim usa As TreeNode = amerika.Nodes(0) kn = New String() {"Miami", "New York", "San Francisco", "Seattle"} Forr Each s As String In kn : usa.Nodes.Add(s) : Next Me.Controls.Add(tr)
Abbildung 12.35 zeigt das Beispiel, wenn alle Knoten geöffnet sind. Beachten Sie, dass das TreeView-Objekt standardmäßig eine Bildlaufleiste einblendet, sobald die Länge der Liste die Höhe des Steuerelements überschreitet.
823
12.19
12
Die wichtigsten Steuerelemente
Abbildung 12.35
Tiefer strukturiertes TreeView-Steuerelement
Am Programmcode ist deutlich Folgendes zu erkennen: Je tiefer die Hierarchiestruktur ist, desto unübersichtlicher wird der Code. Zur besseren Lesbarkeit wird deshalb eine innere TreeNode-Referenz benutzt, die einen Knoten im Innern der Hierarchie referenziert. Dim europa As TreeNode = tr.Nodes(0).Nodes(4)
Die Referenz europa verweist hier auf den fünften Knoten des Stammelements, also erde. Damit reduziert sich der Code, den Sie brauchen, um einen europäischen Staat hinzuzufügen, auf: europa.Nodes.Add(s)
Ansonsten hätte die Anweisung so lauten müssen: tr.Nodes(0).Nodes(4).Nodes.Add(s)
Steuerelementstruktur anzeigen Wir wollen das Erlernte in einem kleinen Beispiel umsetzen. Unsere Aufgabe soll es sein, in einem TreeView-Control sämtliche Steuerelemente der aktuellen Form anzuzeigen. Dabei soll die interne Struktur aller Containersteuerelemente sichtbar sein. In der Strukturansicht soll jeder Elementeintrag durch ein steuerelementspezifisches Bildchen ergänzt werden, und der Beschriftungstext soll Objektname und Typ enthalten. Abbildung 12.36 zeigt die Ausgabe des Beispielprogramms. '...\WinControls\Container\Steuerelementebaum.vb
Public Class Steuerelementebaum Private Sub btnShowControls_Click(sender As Object, e As EventArgs) _ Handles btnShowControls.Click Me.TreeView1.Nodes.Add(Me.Name) Me.TreeView1.Nodes(0).ImageIndex = 0
824
Baumansichten (TreeView und TreeNode)
Me.TreeView1.Nodes(0).SelectedImageIndex = 0 Me.GetControls(Me.TreeView1.Nodes(0), Me) End Sub Private Sub GetControls(ByVal node As TreeNode, ByVal container As Control) For Each control As Control In container.Controls Dim index As Integer = node.Nodes.Add(New TreeNode(control.Name)) SetImage(node.Nodes(index), control) GetControls(node.Nodes(index), control) Next End Sub Private Shared Sub SetImage(node As TreeNode, control As Control) If TypeOf control Is Form Then : node.ImageIndex = 0 ElseIf TypeOf control Is Button Then : node.ImageIndex = 1 ElseIf TypeOf control Is GroupBox Then : node.ImageIndex = 2 ElseIf TypeOf control Is RadioButton Then : node.ImageIndex = 3 ElseIf TypeOf control Is TextBox Then : node.ImageIndex = 4 ElseIf TypeOf control Is TreeView Then : node.ImageIndex = 5 End If node.Text = control.Name & " (" & control.GetType().Name & ")" node.SelectedImageIndex = node.ImageIndex End Sub End Class
Abbildung 12.36
Die Ausgabe des Beispiels »Steuerelementebaum«
btnShowControls ist der Ereignishandler der Schaltfläche mit der Beschriftung Anzeigen. Hier wird zuerst der Stammknoten erzeugt, der die Form beschreibt. Sie ist ein Container für Steuerelemente. Die Eigenschaft Controls der Form liefert eine ControlCollection der Steuerelemente.
825
12.19
12
Die wichtigsten Steuerelemente
Danach werden in der Methode GetControls() alle Steuerelemente in der Form durchlaufen und der TreeView hinzugefügt. Damit auch Steuerelemente in einem Container erfasst werden, wird die Methode GetControls() rekursiv aufgerufen. Durch die Rekursion dürfen diese wieder Container enthalten und so fort. Damit werden beliebig tief geschachtelte Steuerelemente erfasst. SetImage hat die Aufgabe, den Typ des gefundenen Steuerelements zu ermitteln und aus der ImageList der Baumansicht das passende Symbol festzulegen. Sie müssen darauf achten, dass
keine Symbolumschaltung erfolgt, wenn sich der Zustand eines Strukturelements ändert. Daher wird in SetImage mit node.Nodes(index).SelectedImageIndex = node.Nodes(index).ImageIndex
der Index des ausgewählten Zustands gleich dem Index des nicht ausgewählten Zustands gesetzt.
12.19.2 Eigenschaften In der »normalen« Ansicht werden geschlossene Knoten durch ein »+«-Symbol gekennzeichnet, geöffnete durch »-«. Der aktuell markierte Knoten wird farblich invertiert dargestellt. Viele Installationsroutinen bieten Strukturansichten mit Auswahlkästchen an. Diese können Sie auch anbieten, indem Sie der Eigenschaft CheckBoxes den Wert True geben. Die Einzugsbreite der untergeordneten Knoten ist in der Eigenschaft Indent festgelegt und beträgt standardmäßig 19 Pixel. Für eine einfachere Identifizierung der Knoten sollten Sie für diese Symbole in Betracht ziehen. Die Gesamtverwaltung aller Knotensymbole obliegt dem TreeView, nicht den einzelnen Knoten. Verwaltet werden die Symbole von einem ImageList-Objekt, das Sie der Form hinzufügen und der gleichnamigen Eigenschaft des TreeView im Eigenschaftsfenster zuweisen. Da eine ImageList nicht sehr intuitiv ist, sollten Sie bei der Zusammenstellung der Symbole sofort berücksichtigen, dass ausgewählte und nicht ausgewählte Elemente in der Strukturansicht üblicherweise mit unterschiedlichen Bildchen gekennzeichnet werden, die alle in dieser ImageList enthalten sein müssen. Besser wäre es sicherlich gewesen, wenn uns die .NETEntwickler für jeden Zustand je eine Bildauflistung spendiert hätten. Die Eigenschaften ImageIndex/IndexKey (nicht ausgewählt) und SelectedImageIndex/ SelectedImageKey (ausgewählt) bestimmen, welche Bildchen standardmäßig für einen ausge-
wählten bzw. nicht ausgewählten Knoten angezeigt werden. Die Einstellungen kommen nur dann zum Tragen, wenn einem Knoten kein spezielles Symbol zugeordnet wird. Unter StateImageList dürfen Sie auch eine zweite ImageList angeben. Hier tragen Sie mindestens zwei Bildchen ein, die in den Auswahlkästchen für den Zustand »ausgewählt« und »nicht ausgewählt« stehen. Das erste Symbol kennzeichnet den nicht ausgewählten Zustand, das zweite den ausgewählten. Sie dürfen dieser ImageList natürlich auch noch mehr Symbole hinzufügen, um für jeden Knoten ein ganz individuelles Bildchen im Kontrollkästchen zuzuordnen (dann muss CheckBoxes=False sein). Trotzdem rate ich davon ab, weil zu viele Symbole beim Benutzer leicht zu Irritationen führen.
826
Baumansichten (TreeView und TreeNode)
Das aktuell markierte Element in der Strukturansicht wird farblich in der Breite der Beschriftung hervorgehoben. Mit FullRowSelect=True können Sie die farbliche Hervorhebung über die gesamte Breite der Strukturansicht spannen. Allerdings wirkt sich diese Einstellung nur dann aus, wenn ShowLines=False ist. Mit ShowLines werden die Linien zwischen den nebenund untergeordneten Elementen dargestellt, mit LineColor wird deren Farbe festgelegt. Wenn Sorted auf True festgelegt ist, werden die TreeNode-Objekte in alphabetischer Reihenfolge nach den Werten ihrer Text-Eigenschaft sortiert. Bei einer großen Anzahl sortierter Elemente sollten Sie immer die Methoden BeginUpdate und EndUpdate aufrufen, um Leistungseinbußen zu verhindern. Haben Sie LabelEdit=True eingestellt, kann der Benutzer zur Laufzeit den Beschriftungstext ändern. Sie müssen dann die Methode Sort aufrufen, um die Elemente neu zu sortieren. Obwohl dies unüblich ist, können Sie mit ShowPlusMinus=False auf die Plus-/Minusschaltflächen in einer Strukturansicht verzichten. ShowRootLines steuert die Anzeige von Linien zwischen den Stammknoten. Wenn die HotTracking-Eigenschaft auf True festgelegt ist, wird jede Strukturknotenbezeichnung als blau unterstrichener Hyperlink dargestellt, während der Mauszeiger darüber bewegt wird. Die Darstellung wird nicht durch die Interneteinstellungen im Betriebssystem des Benutzers gesteuert. Sie können das Aussehen nur beeinflussen, wenn Sie den Knoten selbst zeichnen. Die Eigenschaft SelectedNode ist eine Referenz auf das TreeNode-Objekt, das gerade ausgewählt ist. Durch Zuweisung ändern Sie die Selektion. Der Eigenschaft PathSeparator eines TreeView-Objekts kommt im Zusammenhang mit der Eigenschaft FullPath eines TreeNode-Objekts besondere Bedeutung zu. FullPath liefert für jeden Knoten eine Zeichenfolge zurück, in der der Bezeichner des Knotens mit allen seinen zum Ursprung zurückführenden Knoten verbunden wird. PathSeparator legt das Trennzeichen fest, das standardmäßig ein Backslash ist.
12.19.3 Ereignisse Das Aufklappen und Schließen eines Knotens wird von vier Ereignissen begleitet: je einem, das auftritt, bevor die damit verbundene, sichtbare Reaktion des Knotens erfolgt (BeforeExpand bzw. BeforeCollapse), und je einem zum Abschluss des Vorgangs (AfterExpand bzw. AfterCollapse). In diesem Zusammenhang spielen noch drei weitere Ereignisse eine Rolle. Wird der angeklickte Knoten beim Öffnen oder Schließen gleichzeitig ausgewählt, haben wir es mit dem Ereignispaar BeforeSelect und AfterSelect zu tun. Unabhängig davon, ob ein Knotenelement beim Anklicken ausgewählt ist oder nicht, wird NodeMouseClick ausgelöst. Tabelle 12.17 fasst die Ereignisse zusammen.
827
12.19
12
Die wichtigsten Steuerelemente
Ereignis
Auslösung
BeforeExpand
Bevor der Knoten geöffnet wird
AfterExpand
Nachdem der Knoten geöffnet worden ist
BeforeSelect
Bevor ein Knoten ausgewählt wird
AfterSelect
Nachdem ein Knoten ausgewählt wurde
BeforeCollaps
Bevor der Knoten geschlossen wird
AfterCollaps
Nachdem der Knoten geschlossen worden ist
NodeMouseClick
Wenn der Benutzer auf einen Knoten klickt
Tabelle 12.17
Ereignisse in Zusammenhang mit einer Knotenauswahl
BeforeExpand, BeforeSelect und BeforeCollapse übergeben dem Ereignishandler ein
Objekt vom Typ TreeViewCancelEventArgs, und die Ereignisse AfterExpand, AfterSelect und AfterCollapse übergeben ein Objekt vom Typ TreeViewEventArgs. Das zuerst aufgeführte Args-Objekt unterscheidet sich nur dadurch, dass der eingeleitete Vorgang mit Cancel abgebrochen werden kann. Die Eigenschaften zeigt die Tabelle 12.18. Eigenschaft
Beschreibung
Action
Gibt an, wie das Ereignis ausgelöst wurde (Maus oder Tastatur) und ob auf- oder zugeklappt wurde (Enumeration TreeViewAction).
Cancel
Gibt an, ob das Ereignis abgebrochen werden soll.
Node
Strukturknoten, der aktiviert, erweitert, reduziert oder ausgewählt wurde
Tabelle 12.18
Die Eigenschaften von »TreeViewCancelEventArgs«
Die Action-Eigenschaft ist vom Typ der Enumeration TreeViewAction (siehe Tabelle 12.19) und liefert uns einen Wert, aus dem die Ursache der Ereignisauslösung entnommen werden kann. Konstante
Grund der Auslösung
Unknown
unbekannt
ByKeyboard
Tastatureingabe
ByMouse
Mausvorgang
Collapse
Zusammenklappen eines Knotens
Expand
Expandieren eines Knotens
Tabelle 12.19
Die Enumeration TreeViewAction
12.19.4 Knotenexpansion und -reduktion Die Ansicht des TreeView-Objekts wird jedes Mal aktualisiert, sobald ein Element hinzugefügt wird. Das kann zu trägen Reaktionen und zum Flackern der Anzeige führen, wenn beispielsweise in einer Schleife viele Strukturknoten hinzugefügt werden. Um das zu vermeiden, sollte man vor dem Einfügen die Methode BeginUpdate aufrufen. Damit wird das Zeichnen des Steuerelements so lange unterbunden, bis die EndUpdate-Methode aufgerufen wird. Erstaun-
828
Teiler (Splitter und SplitContainer)
licherweise machen die Methoden ExpandAll und CollapseAll, die auf die gesamte Hierarchiestruktur wirken, keinen Gebrauch von diesem Mechanismus. Die Tabelle 12.20 fasst die Methoden noch einmal zusammen. Methode
Beschreibung
BeginUpdate
Deaktiviert das Neuzeichnen des Steuerelements.
CollapseAll
Reduziert alle Strukturknoten.
EndUpdate
Aktiviert das Neuzeichnen des Steuerelements.
ExpandAll
Expandiert alle Strukturknoten.
Tabelle 12.20
Methoden in »TreeView«
Parallel zur grafischen Änderung der Anzeige durch einen Klick auf das »+«- oder »-«-Zeichen, die durch TreeView verarbeitet werden, stellt die Klasse TreeNode Methoden zur Verfügung, um einen Konten zu erweitern oder zu reduzieren.
12.19.5 Knoten (TreeNode) Drei Eigenschaften unterstützen Sie bei der Identifikation eines Knotens: Eigenschaft
Beschreibung
Index
Position des aktuellen Knotens in der Nodes-Liste des Elternknotens.
Text
Angezeigter Text des Knotens
TreeView
TreeView-Objekt, das den Knoten enthält
Tabelle 12.21
R R
Identifikation eines Knotens (R = ReadOnly)
Andere Eigenschaften dienen zur Navigation durch die Hierarchie. Mit Parent kann zum Beispiel der übergeordnete Knoten bestimmt werden, mit FirstNode, LastNode, PrevNode und NextNode kann zu den nebengeordneten Knoten auf gleicher Hierarchieebene navigiert werden. Alle geben die Referenz auf das angesteuerte TreeNode-Objekt zurück. Mit den Eigenschaften IsExpanded und IsSelected können Sie feststellen, ob ein Knoten aktuell reduziert, erweitert oder markiert ist. Auch auf Knotenebene können Sie mit den Methoden Collapse, Expand, ExpandAll und Toggle untergeordnete Knoten expandieren bzw. reduzieren. Anders als im TreeView-Objekt betrifft das nicht immer alle Knoten, sondern nur die Kindknoten des aktuellen Knotens. Die Methode Toggle invertiert den Zustand des Knotens, also entweder von erweitert nach reduziert oder von reduziert nach erweitert.
12.20
Teiler (Splitter und SplitContainer)
Im Windows Explorer befindet sich zwischen dem durch ein TreeView-Steuerelement darstellbaren Teilfenster und dem rechten Teilfenster ein Steuerelement, das mit der Maus gegriffen und verschoben werden kann. Dieses wird durch die Klasse Splitter beschrieben. Splitter sind immer an einem anderen Steuerelement angedockt, dessen Größe direkt vom
829
12.20
12
Die wichtigsten Steuerelemente
Teiler verändert wird. Im Windows Explorer ist es das TreeView-Steuerelement, das selbst am linken Fensterrand angedockt ist. An der rechten Seite des TreeViews befindet sich der Splitter. Das Steuerelement, das sich auf der nicht angedockten Seite des Splitters befindet, nimmt häufig den gesamten verbleibenden Clientbereich ein. Das sehr ähnliche Steuerelement SplitContainer vereinfacht die Aufteilung von Clientbereichen und hat ein paar zusätzliche Eigenschaften.
12.20.1 Splitter Eigenschaften Mit den beiden Eigenschaften MinSize und MinExtra geben Sie an, wie klein das neben dem Splitter befindliche Steuerelement werden darf. Da ein Teiler immer zwischen zwei Steuerelementen sitzt, bezieht sich MinSize auf das Steuerelement, das am Rand des Containers angedockt ist, und MinExtra auf das andere Steuerelement. Mit diesen Einstellungen, die standardmäßig 25 Pixel betragen, wird vermieden, dass das zusammengestauchte Steuerelement so klein wird, dass es vom Anwender nicht mehr erkannt und bedient werden kann. BorderStyle legt die Rahmendarstellung eines Steuerelements fest. Die Standardeinstellung ist BorderStyle=BorderStyle.None. Mit BorderStyle.FixedSingle können Sie eine einfache Umrandung einstellen oder mit BorderStyle.Fixed3D eine dreidimensionale.
Ereignisse Während der Teiler bewegt wird, treten permanent SplitterMoving-Ereignisse auf, vergleichbar mit den Ereignissen MouseMove, wenn der Mauszeiger über eine Komponente bewegt wird. Das Loslassen der Maustaste löst einmalig das Ereignis SplitterMoved aus. Danach wird die Größe der beiden Steuerelemente geändert. Bei der Auslösung der Ereignisse wird ein Objekt vom Typ SplitterEventArgs mit den in Tabelle 12.22 aufgelisteten Eigenschaften erzeugt. Eigenschaft
Beschreibung
X
X-Koordinate des Mauszeigers
R
Y
Y-Koordinate des Mauszeigers
R
SplitX
X-Koordinate der oberen linken Ecke des Splitters
SplitY
Y-Koordinate der oberen linken Ecke des Splitters
Tabelle 12.22
Eigenschaften in »SplitterEventArgs« (R = ReadOnly)
Die Eigenschaften SplitX und SplitY geben die Position der linken oberen Ecke des Teilers an – relativ zum Clientbereich. SplitX ist bei horizontalen Teilern immer 0, während SplitY bei vertikalen Teilern immer 0 ist. Beim Ereignis SplitterMoving sind die X- und Y-Koordinaten auch dann die Mauszeigerkoordinaten, wenn das Ziehen des Teilers den Anzeigebereich der Form verlässt. Der splittertypische Mauszeiger mit seinen zwei vertikalen oder horizontalen Linien wird auch außerhalb des
830
Listenansicht (ListView)
Formularbereichs angezeigt, vorausgesetzt, man lässt die Maustaste nicht los. Sogar negative Koordinaten sind möglich. Das sollten Sie bei der Auswertung der Koordinaten in Ihren Programmen berücksichtigen.
12.20.2 SplitContainer SplitContainer ist dem zuvor behandelten Splitter sehr ähnlich. Es besteht aus einer
verschiebbaren Leiste, die den Anzeigebereich eines Containers in zwei Bereiche mit veränderbarer Größe teilt. Die beiden Bereiche werden jeweils durch ein SplitterPanel beschrieben, in dem Sie nach Belieben Steuerelemente unterbringen können. Sie können mehrere SplitContainer ineinander verschachteln, sodass Sie auch sehr komplexe Fensterteilungen erreichen können. Im Gegensatz zu dem im vorherigen Abschnitt beschriebenen Splitter ist das Arbeiten mit SplitContainer intuitiver, und er hat zusätzliche Eigenschaften. Beispielsweise kann nun auch der Benutzer mit den Steuerungstasten der Tastatur den Splitter zwischen den beiden Teilflächen verschieben. Mit SplitterIncrement legen Sie fest, wie groß dabei die Schrittweite in Pixel ist. Voreingestellt ist ein einziges Pixel. Die Orientation-Eigenschaft des SplitContainer-Steuerelements bestimmt die Richtung des Teilers. Mit der Standardeinstellung Vertical verläuft der Splitter von oben nach unten und erstellt eine linke und eine rechte SplitterPanel-Fläche, wobei die mit 1 nummerierte Fläche Panel1 je nach Orientierung links oder oben ist. Sie können eines der beiden SplitterPanel fixieren mit der Folge, dass dieses bei einer Größenänderung der Form seine ursprüngliche Größe beibehält. Möchten Sie verhindern, dass der Benutzer den Teiler verschiebt, legen Sie IsSplitterFixed=True fest. Um einen der beiden Bereiche auszublenden, weisen Sie der Eigenschaft Panel1Collapsed oder Panel2Collapsed den Wert True zu. Die Mindestgröße der beiden Bereiche legen Sie mit den Eigenschaften Panel1MinSize und Panel2MinSize fest. Schließlich haben Sie über die Eigenschaften Panel1 und Panel2 den vollen Zugriff auf die beiden Teilflächen und können diese beliebig konfigurieren.
12.21
Listenansicht (ListView)
In der linken Fensterhälfte des Windows Explorers wird die Struktur des Dateisystems durch ein TreeView-Steuerelement angezeigt, und in der rechten Hälfte erscheinen die untergeordneten Elemente des ausgewählten Knotens. Hier hat der Anwender die Wahl, wie er sich die Elemente anzeigen lässt. Beispielsweise können die Elemente als Tabelle aufgelistet werden, zum Beispiel mit Spalten für die Dateigröße, das Änderungsdatum usw. Jedes Element kann auch durch ein kleines oder ein großes Symbol dargestellt werden. Wir können diese Fähigkeiten des Explorers durch das Steuerelement ListView nachbilden, das auch als Listenansicht bezeichnet wird. Die Programmierung ist etwas komplexer, weil bei diesem Steuerelement deutlich mehr Klassen eine Rolle spielen als bei den Steuerelementen, die wir bisher behandelt haben.
831
12.21
12
Die wichtigsten Steuerelemente
12.21.1 Beteiligte Klassen Fangen wir mit einem Überblick über die wichtigsten Klassen an, die im Zusammenhang mit Listenansichten stehen. Ein ListView-Objekt repräsentiert eine Tabelle und stellt insgesamt sechs Auflistungen bereit: 왘
Elemente: Eine Tabelle besteht aus Zeilen vom Typ ListViewItem. Jeder Spaltenwert hat den Typ ListViewSubItem und wird nur in der Detailansicht angezeigt. Die Zeilen werden in einer Liste vom Typ ListViewItemCollection gespeichert, und jede Zeile speichert ihre Spalten in einer Liste vom Typ ListViewSubItemCollection.
왘
Spaltenköpfe: Überschriften vom Typ ColumnHeader werden in einer Liste vom Typ ColumnHeaderCollection gespeichert und nur in der Detailansicht angezeigt.
왘
Selektion: Die Liste SelectedListViewItemCollection liefert Referenzen der ausgewählten Listenelemente zurück, SelectedIndexCollection deren Indizes.
왘
Gruppierung: ListViewGroupCollection gruppiert logisch zusammengehörige Zeilen. Jede Gruppe besteht aus einer Überschrift, auf den eine horizontale Linie und die dieser Gruppe zugewiesenen Elemente folgen.
Eine Listenansicht kann durch Bildchen klarer gestaltet werden. Dazu können dem Steuerelement drei ImageList-Steuerelemente mittels folgender Eigenschaften hinzufügt werden: 왘
LargeImageList: Darstellung Grosse Symbole
왘
SmallImageList: Darstellung Kleine Symbole
왘
StateImageList: Zwei Bildchen zur Unterscheidung zwischen ausgewählten und nicht ausgewählten Elementen
12.21.2 Eigenschaften und Ereignisse Listenelemente und Spaltenbeschreibungen Über die Eigenschaft Items vom Typ ListViewItemCollection sprechen Sie auf die Elemente (Zeilen) der Listenansicht an, während Columns vom Typ ColumnHeaderCollection auf die Spaltenüberschriften der Detailansicht zugreift. Beide Auflistungen können Sie mit den schon von anderen Auflistungen her bekannten Methoden manipulieren (Add, AddRange, Contains, Insert usw.). Markieren Sie im Eigenschaftsfenster der Entwicklungsumgebung die Eigenschaften Items bzw. Columns, werden Dialoge zum Hinzufügen von Listenelementen und Spalten geöffnet. Beide sind zwar intuitiv zu bedienen, können aber nur den Zustand der Listenansicht beim Start der Anwendung beschreiben. In den meisten Fällen wird eine Listenansicht jedoch zur Laufzeit dynamisch aufgebaut. Daher nimmt bei einer Listenansicht die Programmierung einen höheren Stellenwert ein als bei den meisten anderen Steuerelementen.
Ansichtsmodi Es sind zwei ImageList-Objekte notwendig, wenn eine Listenansicht vollständig entwickelt werden soll: Eines enthält die Liste der kleinen Symbole, das andere die Liste der großen. Die
832
Listenansicht (ListView)
Listenansicht muss mit beiden Bildlisten verknüpft werden. Dazu dienen die Eigenschaften SmallImageList und LargeImageList. Mit der Eigenschaft View legt man fest, wie die Elemente zur Laufzeit dargestellt werden. View ist vom Typ der gleichnamigen Enumeration mit Konstanten, die die Ansichten beschreiben, die wir vom Windows Explorer her kennen: Konstante
Elemente werden dargestellt als
Details
Zeilen mit spezifischen Informationen in Spalten und Spaltenüberschriften für die ganze Tabelle der Listenansicht
LargeIcon
große Symbole (oft 32 × 32 oder 48 × 48 Pixel) mit darunterliegender Bezeichnung. Sie haben eine feste Breite mit neuen Zeilen bei Überlauf.
List
kleine Symbole (meist 16 × 16 Pixel) mit Beschriftung rechts davon. Sie haben eine feste Höhe mit neuen Spalten bei Überlauf.
SmallIcon
kleine Symbole mit Beschriftung rechts davon. Sie haben eine feste Breite mit neuen Zeilen bei Überlauf.
Tile
kleine Symbole mit Beschriftung und Zusatzinformationen rechts davon. Sie haben eine feste Breite mit neuen Zeilen bei Überlauf. Diese Konstante setzt XP oder Server 2003 voraus.
Tabelle 12.23
Die Enumeration »View«
Hinweis Ohne Spaltenüberschriften hat die Detailansicht Darstellungsprobleme.
Elementaktivierung Die Listenansicht bietet viele Eigenschaften, um die Anzeige und die Funktionalität des Steuerelements zu gestalten. Mit AllowColumnReorder=True kann der Anwender die Reihenfolge der Spalten verändern, HoverSelection=True erlaubt die Auswahl eines Elements ohne Mausklick, und MultiSelect=True gibt an, ob mehrere Listeneinträge gleichzeitig ausgewählt werden dürfen. Bei einer Listenansicht muss genau zwischen Aktivierung und Auswahl unterschieden werden. Ein Listenelement wird standardmäßig ausgewählt, wenn mit der Maus einmal auf das Element geklickt wird oder die Auswahl mit den Pfeiltasten verändert wird. Aktiviert wird ein Listenelement, wenn durch einen Doppelklick eine Programmaktion gestartet wird. Das ist zumindest der Standard. Manchmal ist es wünschenswert, vom Standardverhalten abweichend eine andere Aktivierung vorzusehen. Die Eigenschaft Activation vom Typ der der Enumeration ItemActivation bietet dazu die drei in Tabelle 12.24 gezeigten Möglichkeiten.
833
12.21
12
Die wichtigsten Steuerelemente
Konstante
Beschreibung
Standard
Die Elemente werden durch einen Doppelklick aktiviert.
OneClick
Die Aktivierung erfolgt durch einen Einfachklick. Ein Mauszeiger über einem ausgewählten Element wird als Hand angezeigt, und die Farbe des Elementtextes ändert sich.
TwoClick
Verhält sich wie Standard. Ein Mauszeiger über einem ausgewählten Element wird als Hand angezeigt, und die Farbe des Elementtextes ändert sich.
Tabelle 12.24
Die Enumeration »ItemActivation«
Wichtige Eigenschaften Die Tabelle 12.25 gibt einen Überblick über die wichtigsten Eigenschaften einer Listenansicht. Einige werden im weiteren Verlauf des Abschnitts noch erläutert. Eigenschaft
Beschreibung
Activation
Gibt an, wie ein Element der Listenansicht aktiviert werden kann. Mögliche Werte stehen in der Enumeration ItemActivation.
AllowColumnReorder
Gibt an, ob der Anwender die Spaltenreihenfolge verändern darf.
CheckBoxes
Gibt an, ob neben jedem Element eine Checkbox angezeigt wird.
Columns
Referenz auf die ColumnHeaderCollection
FullRowSelect
Gibt an, ob in der Detailansicht jeweils ganze Zeilen selektiert werden. Der Standard False nimmt nur das Element selbst in der ersten Spalte.
GridLines
Gibt an, ob Linien zwischen den Zeilen und Spalten mit den Elementen und Unterelementen angezeigt werden. Der Standard ist False.
Groups
Referenz auf die ListViewGroupCollection
HeaderStyle
Gibt an, ob in der Detailansicht (anklickbare) Spaltenköpfe angezeigt werden: None, Clickable (Standard) und Nonclickable.
HotTracking
Elemente präsentieren sich als Hyperlinks, wenn die Maus darüber bewegt wird.
HoverSelection
Auswahl eines Elements durch Verharren mit der Maus darüber für einige Sekunden. Der Standard ist False.
Items
Referenz auf die Auflistung der angezeigten Elemente
LabelEdit
Beschriftung der Elemente durch den Benutzer
LabelWrap
Gibt an, ob eine zu lange Beschriftung eines großen Symbols umbrochen wird. Der Standard ist True.
LargeImageList
Bilderliste, aus der die goßen Symbole über einen Index zugewiesen werden
MultiSelect
Gibt an, ob mehrere Listeneinträge gleichzeitig ausgewählt werden können.
ShowGroups
Gibt an, ob die Listenelemente in Gruppenform angezeigt werden.
SmallImageList
Bilderliste, aus der die kleinen Symbole über einen Index zugewiesen werden
Sorting
Art der Sortierung der Elemente
StateImageList
Bilderliste, die den von der Anwendung definierten Zuständen im Steuerelement zugeordnet ist
Tabelle 12.25
834
Eigenschaften von »ListView« (Auszug)
Listenansicht (ListView)
Ereignisse Von den vielen Ereignissen einer ListView möchte ich nur drei gesondert erwähnen: 왘
SelectedIndexChanged wird ausgelöst, wenn der Anwender ein anderes Element in der
Listenansicht auswählt. 왘
ItemActivate wird ausgelöst, wenn der Anwender ein Element aktiviert. Was als Aktivie-
rung interpretiert wird, wird durch die Eigenschaft Activation festgelegt. 왘
ColumnClick tritt auf, wenn der Benutzer auf einen Spaltenkopf klickt. Dieses Ereignis ist
insbesonders dann interessant, wenn eine Spalte sortiert werden soll. Die beiden zuerst angeführten Ereignisse sind vom Typ EventHandler. Das bedeutet, dass dem Ereignishandler keine weiteren Informationen zur Verfügung gestellt werden. ColumnClick liefert im Parametertyp ColumnClickEventArgs in der Eigenschaft Column den Index der angeklickten Spalte.
12.21.3 Listenelemente (ListViewItem) Hinzufügen und verwalten Die in einer Listenansicht enthaltenen Elemente sind vom Typ ListViewItem und werden in der Ansicht Detail in der linken Spalte angezeigt oder in den Ansichten Kleine Symbole bzw. Grosse Symbole als Symbol. Ist die Detailansicht aktiviert, werden in weiteren Spalten die Eigenschaften des Elements angezeigt. Handelt es sich bei den Elementen um Dateien, können das beispielsweise die Dateigröße und der Typ sein. Ein ListViewItem-Objekt ist immer eine Zeichenfolge. Das spiegelt sich in den Konstruktoren wider, die bis auf den parameterlosen Konstruktor alle eine Zeichenfolge oder ein Zeichenfolge-Array erwarten. Mit der Methode Add der ListViewItemCollection der Listenansicht wird ein Listenelement hinzugefügt: lvwPerson.Items.Add(New lvwPerson.Items.Add(New lvwPerson.Items.Add(New lvwPerson.Items.Add(New lvwPerson.Items.Add(New
ListViewItem("Abel")) ListViewItem("Müller")) ListViewItem("Rosnick")) ListViewItem("Berenbach")) ListViewItem("Keser"))
Hier ist lvwPerson das ListView-Objekt. Sie müssen nicht unbedingt ausdrücklich die Klasse ListViewItem instanziieren. Sie können als Argument auch eine Zeichenfolge übergeben: listView1.Items.Add("Müller")
Ein Listenelement kann durch spezifische Eigenschaften beschrieben werden. Sie werden auch als Unterelemente bezeichnet und haben den Typ ListViewSubItem. Alle Unterelemente sind ebenfalls nur Zeichenfolgen und werden von der Auflistung ListViewSubItemCollection des Listenelements verwaltet. Die Eigenschaft SubItems eines Elements liefert die Referenz auf die untergeordnete ListViewSubItemCollection. Die folgende Anweisung greift auf die vierte Zusatzinformation des dritten Elements der Listenansicht zu. lvwPerson.Items(2).SubItems(3)
835
12.21
12
Die wichtigsten Steuerelemente
Bildchen In den beiden Ansichten Grosse Symbole und Kleine Symbole können Sie jedem Listenelement ein Bildchen zuordnen und so die Darstellung optisch ansprechender und übersichtlicher gestalten. Dazu müssen Sie der Form zwei ImageList-Steuerelemente hinzufügen, die die gewünschten Symbole enthalten. Außerdem kann ein Listenelement, unabhängig von der gewählten Ansicht, zur Laufzeit auch ein Bildchen anzeigen, das den ausgewählten bzw. nicht ausgewählten Zustand symbolisiert. Um Zustandssymbole anzubieten, sind einige Vorgaben einzuhalten. Zunächst müssen Sie der Form eine weitere Bildliste hinzufügen, in der nur zwei Symbole enthalten sein sollten: Das erste Bild mit dem Index 0 wird vor den nicht ausgewählten Listenelementen angezeigt, das Bild mit dem Index 1 vor den ausgewählten. Anschließend weisen Sie der Eigenschaft StateImageList des ListView-Objekts die Referenz auf diese Bildliste zu. Im letzten Schritt muss die Eigenschaft CheckBoxes=True gesetzt werden, denn Zustandssymbole ersetzen die Checkboxen. Wollen Sie sich von der starren Zuordnung der beiden Bildchen zu den beiden Status lösen, können Sie mit der Eigenschaft StateImageIndex eines ListViewItem-Objekts einen Bildindex spezifizieren, zum Beispiel um die Reihenfolge der Zuordnung umzukehren. Zur Laufzeit steht der Index des Bildes in direktem Zusammenhang zur Eigenschaft Checked des Listenelements. Zur Laufzeit genügt ein Klick auf das Symbol, um ein Listenelement auszuwählen beziehungsweise die Auswahl aufzuheben. Dabei wechselt das Symbol automatisch.
Auswahl der Listenelemente In einer Listenansicht können Sie durch den Standardwert True der Eigenschaft MultiSelect der Listenansicht entweder ein oder mehrere Elemente auswählen. Die Eigenschaft Selected eines Listenelements beschreibt den Auswahlzustand und kann nicht nur gelesen, sondern auch gesetzt werden, beispielsweise um bei der ersten Anzeige der Listenansicht bereits eine Vorauswahl zu treffen. Im Fall einer Mehrfachauswahl können Sie die ausgewählten Elemente durch zwei Auflistungen der ListView ermitteln, ähnlich wie bei einer ListBox. Beide unterscheiden sich nur durch die Art der Verwaltung der Listenelemente. 왘 왘
SelectedIndexCollection enthält die Indizes der ausgewählten Listenelemente. SelectedListViewItemCollection enthält die Referenzen auf die ausgewählten Listenele-
mente. Sie greifen auf die Auswahl über die Eigenschaften SelectedIndices und SelectedItems zu. Ein konkretes Element erreichen Sie einfach über den Indexer der Auflistung. Welche der beiden Listen Sie verwenden, ist reine Geschmackssache. Sie bekommen in beiden Fällen die Referenzen auf die ausgewählten Listenelemente zurückgeliefert, um damit die erforderlichen Operationen auszuführen. Die folgende Anweisung liest die Text-Eigenschaft des ersten ausgewählten Elements über den Indexer ein und gibt sie in einem Meldungsfenster aus: MessageBox.Show(listView1.SelectedItems(0).Text)
836
Listenansicht (ListView)
12.21.4 Unterelemente in Spalten (ListViewSubItem) Die Unterelemente eines ListViewItem-Objekts sind vom Typ der inneren Klasse ListViewSubItem. Jedes Unterelement wird durch eine Zeichenfolge beschrieben. Verwaltet werden die Unterelemente in einer Auflistung ListViewSubItemCollection. Bitte beachten Sie, dass das erste Unterelement mit dem Index 0 in der ListViewSubItemCollection immer das Element ist, das die Unterelemente besitzt. Wenn Sie alle Unterelemente abfragen wollen, müssen Sie demnach die Auflistung vom Index 1 an durchlaufen und nicht vom Index 0 an. Am einfachsten erzeugen Sie ein Unterelement mit dem Konstruktor, dem Sie neben der beschreibenden Zeichenfolge im zweiten Argument die Referenz auf das Listenelement übergeben, dem das neue Unterelement zugeordnet werden soll. Public Sub New(ByVal owner As ListViewItem, ByVal [text] As String)
Sie können bei der Erzeugung eines Listenelements auch sofort die Eigenschaften, also Unterelemente festlegen. Benutzen Sie dazu bei der Instanziierung der Klasse ListViewItem einen Konstruktor, der ein Zeichenfolge-Array entgegennimmt. Dann werden alle Array-Elemente automatisch zur Auflistung der Unterelemente hinzugefügt. Etwas ungewöhnlich ist die Tatsache, dass sich jedes ListViewItem-Objekt nicht nur in die Auflistung ListViewItemCollection der Listenansicht einträgt, sondern gleichzeitig auch das erste Element in seiner eigenen ListViewSubItemCollection ist. lvwPerson.Items.Add(New ListViewItem(New String() _ {"Abel","Rainer", "30", "Essen"})) lvwPerson.Items.Add(New ListViewItem(New String() _ {"Müller", "Uwe", "25", "Berlin"})) lvwPerson.Items.Add(New ListViewItem(New String() _ {"Rosnick", "Bernd", "44", "Bonn"})) lvwPerson.Items.Add(New ListViewItem(New String() _ {"Berenbach", "Peter", "35", "Köln"})) lvwPerson.Items.Add(New ListViewItem(New String() _ {"Keser", "Wolfgang", "48", "Düren"}))
Eigenschaften der Unterelemente Unterelemente haben viel weniger Eigenschaften als Elemente. Neben der Eigenschaft Text, die die textuelle Beschreibung des Unterelements enthält, gibt es nur noch die Eigenschaften BackColor, ForeColor und Font, um der Anzeige zur Laufzeit ein anderes oder ansprechenderes Aussehen zu verleihen.
12.21.5 Spaltenüberschriften (ColumnHeader) Neben der internen Auflistung ListViewItemCollection verfügt die ListView-Klasse über die bereits erwähnte Auflistung ColumnHeaderCollection, in der alle Spaltenköpfe vom Typ ColumnHeader aufgelistet sind (siehe Tabelle 12.26). Diese werden nur in der Detailansicht angezeigt. Die Referenz auf die interne Auflistung liefert die Eigenschaft Columns. Auch für die
837
12.21
12
Die wichtigsten Steuerelemente
Spaltendefinitionen stellt Visual Studio einen intuitiv zu bedienenden Assistenten zur Verfügung, der Ihnen das einfache Hinzufügen und Beschreiben der Spalten ermöglicht. Ein Spaltenkopf wird nur durch wenige Eigenschaften beschrieben: Text legt die Beschriftung der Spalte fest, Width die Breite der Spalte und TextAlign die Ausrichtung der Spaltenüberschrift innerhalb der Spalte. Durch die beiden Eigenschaften ImageIndex oder ImageKey, die sich beide auf die Eigenschaft SmallImageList der ListView beziehen, können Sie auch Symbole in den Spaltenköpfen anzeigen lassen. Die schreibgeschützte Eigenschaft Index beschreibt die Position der Spalte innerhalb der ColumnHeaderCollection. Der Wert entspricht nicht zwangsläufig der sichtbaren Position des Spaltenkopfes, denn wenn die Eigenschaft AllowColumnReorder der Listenansicht auf True eingestellt ist, kann der Anwender die Anzeigereihenfolge geändert haben. Eigenschaft
Beschreibung
ImageIndex
Index des in der Spalte angezeigten Bilds
ImageKey
Schlüssel des in der Spalte angezeigten Bilds
Index
Position innerhalb der ColumnHeaderCollection
Text
Im Spaltenkopf angezeigter Text
TextAlign
Horizontale Ausrichtung des in ColumnHeader angezeigten Textes
Width
Breite der Spalte
Tabelle 12.26
R
Die Eigenschaften von »ColumnHeader« (R = ReadOnly)
12.21.6 Listenelemente Gruppen zuordnen Sie können alle angezeigten Elemente visuell logischen Gruppen zuordnen. Jede einzelne Gruppe wird durch einen Text beschrieben, der beliebig ausgerichtet werden kann. Gruppen sind vom Typ ListViewGroup (siehe Tabelle 12.27). Die Gruppen eines ListView-Steuerelements werden, da es sich in der Regel um mehrere handelt, von einer Auflistung verwaltet, die in der Eigenschaft Groups des Steuerelements gespeichert ist. Ein Assistent erleichtert Ihnen auch hier die Arbeit. Jedes Listenelement können Sie nach eigenen Kriterien einer Gruppe zuordnen. Sie weisen ein Element einer Gruppe zu, indem Sie die Gruppe im ListViewItem-Konstruktor angeben, die ListViewItem.Group-Eigenschaft festlegen oder das Element direkt der ItemsAuflistung einer Gruppe hinzufügen. Alle Elemente, die keiner Gruppe zugewiesen sind, werden in der Standardgruppe mit der Bezeichnung Default angezeigt. Eigenschaft
Beschreibung
Header
Beschriftung der Gruppe
HeaderAlignment
Ausrichtung der Beschriftung
Items
Auflistung aller Elemente, die dieser Gruppe zugeordnet sind
Name
Name der Gruppe
Tabelle 12.27
838
Die Eigenschaften des Gruppierungselements »ListViewGroup« (R = ReadOnly)
R
Listenansicht (ListView)
Die Personenauflistung von oben kann nach Altersklassen gruppiert werden, zum Beispiel: For Each item As ListViewItem in lvwPerson.Items setGroup(item) Next
Die benutzerdefinierte Methode setGroup entscheidet über die Gruppenzugehörigkeit, die in Abbildung 12.37 zu sehen ist: Private Sub setGroup(item As ListViewItem) If Convert.ToInt32(item.SubItems(2).Text) < 31 Then item.Group = lvwPerson.Groups(0) Else If Convert.ToInt32(item.SubItems(2).Text) < 41 Then item.Group = lvwPerson.Groups(1) Else item.Group = lvwPerson.Groups(2) End If End Sub
Abbildung 12.37
Ausgabe von Gruppierungen
Sie können dem Benutzer auch die Entscheidung selbst überlassen, ob er die Gruppierung der Listenelemente angezeigt bekommt oder nicht. Die Anzeige richtet sich nämlich nach der Eigenschaft ShowGroups der Listenansicht. Voreingestellt ist True.
12.21.7 Sortierung der Spalten ListView sortiert die Listeneinträge, wenn die Eigenschaft Sorting vom Typ der Enumeration SortOrder einen vom Standardwert None abweichenden Wert hat. Ascending sortiert aufstei-
gend, Descending absteigend. Im Windows Explorer führt ein Klick auf einen Spaltenkopf dazu, dass alle Zeilen nach dieser Spalte sortiert werden. Das ist auch in einer ListView sehr einfach zu realisieren, wenn Sie die Standardeinstellung HeaderStyle=Clickable beibehalten haben.
839
12.21
12
Die wichtigsten Steuerelemente
Eine vom Standard abweichende Sortierung wird mit der Eigenschaft ListViewItemSorter definiert, die nicht im Eigenschaftsfenster der Entwicklungsumgebung angezeigt wird: Public Property ListViewItemSorter As IComparer
Wir müssen der Eigenschaft ein Objekt zuweisen, das die Schnittstelle IComparer implementiert. Uns kommt dabei entgegen, dass alle Einträge in der Listenansicht vom Typ String sind. Daher benötigen wir nicht für jede Spalte eine eigene Vergleichsklasse, sondern kommen, wenn wir es geschickt anstellen, mit einer einzigen aus. In der Vergleichsklasse sollten wir auch noch eine übliche Verhaltensweise berücksichtigen: Wird ein Spaltenkopf ein zweites Mal angeklickt, kehrt sich die Sortierreihenfolge der Listenelemente um. Ein Klasse, die alle Ansprüche erfüllt, kann wie folgt codiert werden: Public Class ListViewComparer : Implements Icomparer Private col As Integer Private order As SortOrder Public Sub New(ByVal col As Integer, ByVal order As SortOrder) Me.col = col Me.order = order End Sub Public Function Compare(ByVal x As Object, ByVal y As Object) As Integer _ Implements System.Collections.IComparer.Compare Dim item1, item2 As ListViewItem item1 = CType(If(order = SortOrder.Ascending, x, y), ListViewItem) item2 = CType(If(order = SortOrder.Ascending, y, x), ListViewItem) Return item1.SubItems(col).Text.CompareTo(item2.SubItems(col).Text) End Function End Class
Um die Klasse allgemein zu halten, übergeben wir dem Konstruktor sowohl den Index der angeklickten Spalte als auch die gewünschte Sortierreihenfolge. Diese Informationen werden von der Methode Compare als Grundlage des Vergleichs benötigt. Ihr werden zwei Objekte vom Typ ListViewItem übergeben. Das etwas eigenartige Verhalten eines ListViewItemObjekts, sich in seine eigene ListViewSubItemCollection einzutragen, ist nun für uns von großem Nutzen. Auch die konstante Indizierung der Spaltenköpfe, die nicht zwangsläufig der sichtbaren Reihenfolge entspricht (wenn AllowColumnReorder=True eingestellt ist), macht die Vergleichsoperation sehr einfach. Bei genauer Betrachtung hat die Klassendefinition sogar einen allgemeinen Charakter und kann, wenn keine besonderen Vergleichskriterien eine Rolle spielen, praktisch von jeder Listenansicht benutzt werden. Wir wollen jetzt die Liste der Personen, der wir uns im Laufe des Abschnitts schon einige Male bedient haben, vervollständigen und im ListView auch die Spaltensortierung vorsehen.
840
Listenansicht (ListView)
'...\WinControls\Container\Listensortierung.vb
Public Class Listensortierung ... Private Sub Laden(ByVal sender As Object, ByVal e As EventArgs) _ Handles MyBase.Load For Each c As String In _ New String() {"Zuname", "Vorname", "Alter", "Wohnort"} Personen.Columns.Add(c) Next Personen.Groups.Add("Teenies", "30 oder jünger") Personen.Groups.Add("Jung", "31 bis 40") Personen.Groups.Add("Alt", "30 oder älter") Personen.Items.Add(New ListViewItem(New String() {"Abel", "Rainer", "30", "Essen"})) Personen.Items.Add(New ListViewItem(New String() {"Müller", "Uwe", "25", "Berlin"})) Personen.Items.Add(New ListViewItem(New String() {"Rosnick", "Bernd", "44", "Bonn"})) Personen.Items.Add(New ListViewItem(New String() {"Berenbach", "Peter", "35", "Köln"})) Personen.Items.Add(New ListViewItem(New String() {"Keser", "Wolfgang", "48", "Düren"}))
_ _ _ _ _
For Each item As ListViewItem In Personen.Items setGroup(item) Next End Sub Private Sub setGroup(ByVal item As ListViewItem) If Convert.ToInt32(item.SubItems(2).Text) < 31 Then item.Group = Personen.Groups(0) ElseIf Convert.ToInt32(item.SubItems(2).Text) < 41 Then item.Group = Personen.Groups(1) Else item.Group = Personen.Groups(2) End If End Sub Private Sub Hinzufügen_Click(sender As Object, e As EventArgs) _ Handles Hinzufügen.Click Personen.Items.Add(New ListViewItem( _ New String() {"Hamster", "Karl", "34", "Bremen"})) setGroup(Personen.Items(Personen.Items.Count – 1)) Hinzufügen.Enabled = False End Sub Private Sub Gruppen_CheckedChanged(sender As Object, e As EventArgs) _ Handles Gruppen.CheckedChanged
841
12.21
12
Die wichtigsten Steuerelemente
Personen.ShowGroups = Gruppen.Checked End Sub Dim col As Integer Private Sub Personen_ColumnClick(sender As Object, _ e As ColumnClickEventArgs) Handles Personen.ColumnClick If e.Column = col Then If Personen.Sorting = SortOrder.Ascending Then Personen.Sorting = SortOrder.Descending Else Personen.Sorting = SortOrder.Ascending End If End If col = e.Column Personen.ListViewItemSorter = _ New ListViewComparer(e.Column, Personen.Sorting) End Sub End Class
Um die Sortierreihenfolge zu ändern, klickt der Anwender auf einen Spaltenkopf. Dabei wird das Ereignis ColumnClick ausgelöst, das in der Eigenschaft Column des Args-Parameters den Index des angeklickten Spaltenkopfs bereitstellt. Die Sortierreihenfolge kehrt sich bei jedem erneuten Anklicken derselben Spalte um. Die Vergleichsklasse wird mit der korrekten Sortierung instanziiert und der Eigenschaft ListViewItemSorter zugewiesen. Eine zusätzliche Schaltfläche fügt noch eine Person hinzu, um den Einfluss auf die Sortierreihenfolge zu testen. Das neue Listenelement wird an der richtigen Position eingefügt. Erfreulicherweise wird auch in der Gruppierungsansicht innerhalb einer Gruppe richtig sortiert. Wenn Sie die Listenansicht mit vielen Elementen ergänzen, indem Sie die Add-Methode aufrufen, wird das Steuerelement jedes Mal neu gezeichnet. Möglicherweise kann das zum Flackern der Anzeige führen. Sie können das vermeiden, wenn Sie vor dem Hinzufügen der neuen Listenelemente mit der Methode BeginUpdate das wiederholte Neuzeichnen unterdrücken. Nach dem Hinzufügen des letzten Listenelements müssen Sie das ListView-Objekt mit EndUpdate jedoch neu aufbauen. Hinweis Die Gruppendarstellung war bei mir nach einer Neusortierung ohne Gruppen fehlerhaft.
12.21.8 Listenelemente ändern Standardmäßig werden die Listenelemente nur angezeigt und können nicht verändert werden. Setzen Sie LabelEdit=True für die Listenansicht, darf der Anwender den Text des Listenelements editieren. Zur Laufzeit muss das zu ändernde Listenelement zunächst mit einem Klick markiert werden. Beim nächsten Klick befindet sich der Eingabecursor am Ende des Textes. Eine Möglichkeit, auch den Inhalt der Unterelemente zu verändern, gibt es nicht.
842
Listenansicht (ListView)
Eine Änderung hat nicht zur Folge, dass auch die zugrunde liegenden Daten geändert werden, wenn eine ListView beispielsweise die Listenelemente aus einer Datenbank bezogen hat. Im Kontext mit dem Zurückschreiben von Änderungen bieten sich zwei Ereignisse an: BeforeLabelEdit und AfterLabelEdit. Das erste Ereignis wird nach dem zweiten Klick ausgelöst, das zweite beim Verlassen der Eingabebox. Beide Ereignisse stellen dem Ereignishandler im zweiten Parameter ein Objekt vom Typ LabelEditEventArgs zur Verfügung. Mittels der Eigenschaft CancelEdit kann die Änderung
des Elements verworfen werden, Item ruft den Index des zur Änderung anstehenden ListViewItem-Objekts ab, und Label beinhaltet die Zeichenfolge, die der Text-Eigenschaft
des Listenelements zugewiesen werden soll. Die Daten schreiben Sie am besten im AfterLabelEdit-Ereignishandler in die Originaldatenquelle zurück. Vorher sollten Sie den Text in der Eigenschaft Label auf Richtigheit überprüfen. Brechen Sie die Operation mit CancelEdit=True ab, wird der ursprüngliche Inhalt automatisch wiederhergestellt.
12.21.9 Dateiexplorer-Beispiel Vielleicht haben Sie den Eindruck gewonnen, dass das ListView-Steuerelement kompliziert zu programmieren ist. Es umfasst eine große Anzahl von Klassen und Eigenschaften. Wie komplex eine Listenansicht letztendlich wird, hängt natürlich von den darzustellenden Datenstrukturen und den Anforderungen an die Qualität des Layouts ab. Zum Beispiel sind zusätzliche Bildchen etwas mehr Aufwand. Noch schwieriger ist es, wenn der Benutzer das Aussehen zur Laufzeit ändern können soll. Ich möchte Ihnen nun das Beispiel Explorer vorstellen, das den Windows Explorer teils nachprogrammiert. Das Beispielprogramm soll und kann kein vollwertiger Ersatz des Microsoft Explorers sein, aber dennoch die wichtigsten Aspekte berücksichtigen. Das Beispiel kann für Sie die Basis eines ganz individuellen Explorers sein. Abbildung 12.38 zeigt das Aussehen der hier vorgestellten Implementation. Fangen wir mit der Initialisierung an, die ich im Load-Ereignis des Formulars untergebracht habe. In der ersten Schleife wird die Baumstruktur für den linken Teil aufgebaut. Als Wurzeln werden die von GetLogicalDrives() gelieferten Laufwerke genommen. Damit beim Öffnen eines Knotens die darunterliegenden Verzeichnisse mit Unterverzeichnissen ein »+«-Zeichen zur Expansion zeigen, wird für jeden Knoten der Ereignishandler der Expansion aufgerufen, der dynamisch Knoten hinzufügt. Das Laufwerksbildchen ist in der ImageList und hat den Index null. Um nicht ohne Selektion zu starten, wird das erste Laufwerk selektiert. Als Nächstes werden die Überschriften der Detailansicht festgelegt. In der zweiten Schleife werden schließlich die Menüpunkte zur Ansichtswahl der Listendarstellung im rechten Fensterteil hinzugefügt und wird die erste Ansichtsart gewählt. Durch den Menüaufbau in der Schleife kann keine Darstellungsart vergessen werden.
843
12.21
12
Die wichtigsten Steuerelemente
Abbildung 12.38
Die Ausgabe des Beispiels »Explorer«
'...\WinControls\Container\Explorer.vb
Public Class Explorer Private Sub Laden(sender As Object, e As EventArgs) Handles Me.Load For Each lw As String In Environment.GetLogicalDrives() Dim node As New TreeNode(lw) node.Tag = lw node.ImageIndex = –1 'nur eine Rekursion in Struktur_BeforeExpand() Dim pos As Integer = Struktur.Nodes.Add(node) Struktur_BeforeExpand(node, New TreeViewCancelEventArgs( _ node, False, TreeViewAction.Unknown)) node.ImageIndex = 0 node.SelectedImageIndex = 0 Next Struktur.SelectedNode = Struktur.Nodes(0) Inhalt.Columns.Add("Name") Inhalt.Columns.Add("Größe") Inhalt.Columns.Add("Zeit") Inhalt.Columns.Add("Attribute") For Each v As View In [Enum].GetValues(GetType(View)) Dim item As New ToolStripMenuItem(v.ToString()) AddHandler item.Click, AddressOf Wechseln item.Tag = v Ansicht.DropDownItems.Add(item)
844
Listenansicht (ListView)
Next Wechseln(Ansicht.DropDownItems(0), Nothing) End Sub ... End Class
Damit nicht alle Datenträger vor dem Öffnen des Formulars analysiert werden müssen, fügt der Ereignishandler von BeforeExpand nur die nächsten beiden Knotenebenen dem Baum hinzu. Die erste enthält die Kindknoten für die Darstellung, die zweite (rekursiver Aufruf von Struktur_BeforeExpand()) sorgt für die korrekte Anzeige von »+«-Zeichen. Der temporär negative Wert von ImageIndex stoppt eine weitere Rekursion. Jeder Verzeichnis-Knoten speichert in seiner Eigenschaft Tag den kompletten Pfad, um die Darstellung von dem Datenmodell zu entkoppeln. Verzeichnisse ohne Leserechte erzeugen eine Ausnahme vom Typ UnauthorizedAccessException. In diesem Fall wird ein anderes Bildchen zur Darstellung gewählt. Andere Ausnahmen werden nur berichtet. Ein Grund können unter Windows ungültige Zeichen in einem Verzeichnisnamen sein, die auf einem Rechner im Netzwerk mit anderem Betriebssystem gültig sind. '...\WinControls\Container\Explorer.vb
Public Class Explorer ... Private Sub Struktur_BeforeExpand(sender As Object, _ e As TreeViewCancelEventArgs) Handles Struktur.BeforeExpand Dim di As New IO.DirectoryInfo(e.Node.Tag) Try e.Node.Nodes.Clear() For Each dir As IO.DirectoryInfo In di.GetDirectories() Dim node As TreeNode = e.Node.Nodes.Add(dir.Name) node.Tag = dir.FullName node.ImageIndex = –1 If e.Node.ImageIndex >= 0 Then Struktur_BeforeExpand(node, New TreeViewCancelEventArgs( _ node, False, TreeViewAction.Unknown)) End If If node.ImageIndex < 1 Then node.ImageIndex = 1 Next Catch ex As UnauthorizedAccessException e.Node.ImageIndex = 3 e.Node.SelectedImageIndex = 3 Catch ex As Exception MessageBox.Show("Ausnahme für Pfad " & di.FullName & ": " & ex.Message) e.Cancel = True End Try End Sub ... End Class
845
12.21
12
Die wichtigsten Steuerelemente
Damit kein Speicher verschwendet wird, entsorgt ein geschlossener Knoten Urenkel, die ausschließlich zur korrekten Anzeige der »+«-Zeichen benötigt werden. '...\WinControls\Container\Explorer.vb
Public Class Explorer ... Private Sub Struktur_AfterCollapse(sender As Object, _ e As TreeViewEventArgs) Handles Struktur.AfterCollapse For Each kind As TreeNode In e.Node.Nodes For Each enkel As TreeNode In kind.Nodes enkel.Nodes.Clear() Next Next End Sub ... End Class
Als Nächstes wird für die Baumansicht der Ereignishandler von AfterSelect implementiert, der ausgelöst wird, wenn ein Verzeichnis-Knoten im Baum selektiert wird. Der Handler baut die Listenansicht im rechten Teil des Formulars neu auf. Wenn für das Verzeichnis keine Leserechte bestehen, wird die Methode nach dem Löschen des Inhalts der Listenansicht verlassen. Sonst werden die Elemente der Listenansicht mit allen Spalten aus den Unterverzeichnissen und danach den Dateien aufgebaut. Um das Beispiel einfach zu halten, habe ich auf Mehrdeutigkeiten der Dateiattribute nicht geachtet (mehrere Attribute fangen mit demselben Buchstaben an). Die Eigenschaften LargeImageList und SmallImageList der Listenansicht sollten korrespondierende Bilder enthalten, da derselbe Index für beide benutzt wird (hier: Nummer eins für Ordner und Nummer zwei für Dateien). Durch die Anweisung Struktur.SelectedNode = Struktur.Nodes(0) während der Initialisierung startet die Anwendung mit einer befüllten Listenansicht. '...\WinControls\Container\Explorer.vb
Public Class Explorer ... Private Sub Struktur_AfterSelect(sender As Object, e As TreeViewEventArgs) _ Handles Struktur.AfterSelect Inhalt.Items.Clear() If Struktur.SelectedNode.ImageIndex = 3 Then Return Dim item As ListViewItem Dim dirInfo As New IO.DirectoryInfo(Struktur.SelectedNode.FullPath) Try For Each dir As IO.DirectoryInfo In dirInfo.GetDirectories() item = Inhalt.Items.Add(dir.Name, 1) item.SubItems.Add("") item.SubItems.Add(dir.LastWriteTime.ToString())
846
Listenansicht (ListView)
item.Tag = dir.FullName Next For Each file As IO.FileInfo In dirInfo.GetFiles() item = Inhalt.Items.Add(file.Name, 2) item.SubItems.Add(file.Length.ToString()) item.SubItems.Add(file.LastWriteTime.ToString()) Dim attr As String = "" For Each a As IO.FileAttributes In _ [Enum].GetValues(GetType(IO.FileAttributes)) If (file.Attributes And a) 0 Then _ attr += a.ToString().Substring(0, 1) Next item.SubItems.Add(attr) Next Catch ex As Exception MessageBox.Show("Ausnahme in Liste: " & ex.Message) End Try End Sub ... End Class
Damit die Listenansicht sich wie gewohnt verhält, muss noch das Ereignis ItemActivate behandelt werden, das bei einem Doppelklick oder durch Druck auf die (¢)-Taste ausgelöst wird. Eine äußere Schleife untersucht alle selektierten Elemente (Mehrfachauswahl ist erlaubt). Ein aktiviertes Verzeichnis durchsucht den Elternknoten nach sich selbst und expandiert den Knoten in der Baumansicht, was wiederum die Listenansicht mit dem Verzeichnisinhalt füllt. Ist das aktivierte Element eine Datei, wird sie mit Process.Start() aus dem Namensraum System.Diagnostics gestartet bzw. wird die Datei in der unter Windows registrierten zugehörigen Applikation geöffnet. '...\WinControls\Container\Explorer.vb
Public Class Explorer ... Private Sub Inhalt_ItemActivate(sender As Object, e As EventArgs) _ Handles Inhalt.ItemActivate For Each item As ListViewItem In Inhalt.SelectedItems If item.Tag IsNot Nothing Then ' nur Verzeichnisse bekommen ein Tag in Struktur_AfterSelect() For Each node As TreeNode In Struktur.SelectedNode.Nodes If item.Tag.Equals(node.Tag) Then Struktur.SelectedNode = node Struktur.SelectedNode.Expand() Struktur.Refresh() Exit For End If Next Else
847
12.21
12
Die wichtigsten Steuerelemente
Dim path As String = _ IO.Path.Combine(Struktur.SelectedNode.Tag, item.Text) Try Process.Start(path) Catch ex As Exception MessageBox.Show("Fehler beim Start von " & path & ": " & ex.Message) End Try End If Next End Sub ... End Class
Im Menü Ansicht kann die Darstellung der Listenansicht beliebig in Kleine Symbole, Grosse Symbole, Liste und Details verändert werden. Der Ereignishandler Wechseln() der korrespondierenden Menüpunkte markiert nur die gewählte Ansicht im Menü und setzt diese in der Listenansicht. '...\WinControls\Container\Explorer.vb
Public Class Explorer ... Private Sub Wechseln(ByVal sender As Object, ByVal e As EventArgs) For Each a As ToolStripMenuItem In Ansicht.DropDownItems a.Checked = a Is sender If a.Checked Then Inhalt.View = CType(a.Tag, View) Next End Sub ... End Class
848
In diesem Kapitel wird der Umgang mit Tastatur- und Mausereignissen beschrieben.
13
Tastatur- und Mausereignisse
13.1
Die Tastaturschnittstelle
13.1.1 Allgemeines Die Tastatur ist die wichtigste Eingabeschnittstelle. Ihre Tasten sind entweder mit Zeichencodes oder allgemeinen Funktionen verknüpft und lassen sich in vier Gruppen einteilen: 왘
Zeichentasten: Die Zeichentasten dienen zur Eingabe von Buchstaben, Zahlen und Symbolen. Die Zeichencodes gehören zum ASCII-Zeichensatz, intern werden sie als 16-Bit-Unicode behandelt.
왘
Zustandstasten: Zu ihnen zählen die (Alt)-, (Strg)- und (ª)-Tasten. In der Regel interpretieren eingabefähige Komponenten nur die Kombination mit Zeichen- oder Funktionstasten.
왘
Funktionstasten: Sie führen eine bestimmte Funktionalität aus. Dazu gehören die (Einf)-, (Pause)- und (Entf)-Tasten ebenso wie die Funktionstasten (F1) bis (F12) und die Cursortasten.
왘
Ein/Aus-Tasten: Sie dienen als Schalter, unter anderem die (Num)- und (Rollen)-Tasten.
Tastatureingaben des Anwenders werden von der fokussierten Komponente entgegengenommen. Einfache Reaktionen auf die Eingaben werden von der Komponente selbst ausgeführt. Beispielsweise können Sie in eine Textbox Zeichen eingeben, mit der Rücktaste das vor dem Eingabecursor stehende Zeichen löschen usw. Sollen bei einer Eingabe über den internen Standard hinausgehende Reaktionen erfolgen, müssen Sie das codieren. Damit lässt sich zum Beispiel erreichen, dass das Drücken der (F8)-Taste einen anwendungsspezifischen Dialog öffnet oder bestimmte Zeichen der Benutzereingabe nicht weiterverarbeitet werden.
13.1.2 Tastaturereignisse In der Klasse Control sind drei tastaturbezogene Ereignisse definiert: KeyDown, KeyPress und KeyUp. Allerdings veröffentlichen nicht alle von der Basisklasse Control abgeleiteten Komponenten diese drei Ereignisse, denn Voraussetzung dafür ist die Fokussierfähigkeit. Ein Label beispielsweise reiht sich zwar in die Fokussierreihenfolge ein, ist aber nicht in der Lage, Tastatureingaben zu verarbeiten.
849
13
Tastatur- und Mausereignisse KeyDown und KeyUp sind mit dem sogenannten Tastaturcode verknüpft, der für jede Taste ein-
deutig festgelegt ist. Damit werden beide bei jedem Tastaturanschlag ausgelöst: 왘
KeyDown, wenn die Taste gedrückt wird
왘
KeyUp, wenn die Taste losgelassen wird
Das Ereignis KeyPress tritt nach KeyDown auf, wenn der Anwender auf eine Buchstaben- oder Zifferntaste drückt. Dabei sind einige Sonderfälle zu berücksichtigen: 왘
Wird eine Tastenkombination aus einer oder mehreren Zustandstasten zusammen mit einer Taste für einen Zeichencode gedrückt, z. B. (ª) + (A), wird das KeyDown-Ereignis sowohl für die Zustandstaste als auch für die Zeichentaste ausgelöst. Dasselbe gilt auch für KeyUp.
왘
Hält man eine Zustandstaste längere Zeit gedrückt, tritt KeyDown wiederholt auf.
왘
Wird eine Zeichentaste längere Zeit gedrückt, schreibt sich das Zeichen wiederholt in das Ausgabeziel, beispielsweise in eine Textbox. Dabei tritt für die Dauer des Tastendrucks das Ereignispaar KeyDown/KeyPress so lange auf, bis die Taste losgelassen wird und ein abschließendes KeyUp ausgelöst wird.
왘
Ist in einer Textbox die Eigenschaft AcceptsTab auf True gesetzt, kann der Benutzer mit der (ÿ_)-Taste ein Tabstopp-Zeichen in ein mehrzeiliges Textfeld eingeben. In diesem Fall wird kein Tastaturereignis ausgelöst.
왘
KeyPress tritt auf, wenn ein ASCII-Zeichen erzeugt wird. Das gilt auch für den Fall, dass
eine Kombination aus zwei Zustandstasten mit einer Zeichentaste gedrückt wird, zum Beispiel (Strg) + (Alt) + (Q) oder (AltGr) + (Q) für das Zeichen @.
13.1.3 Die Ereignisse KeyDown und KeyUp Die Parameterlisten eines Ereignishandlers und des Delegates des Ereignisses müssen übereinstimmen. Sowohl KeyDown als auch KeyUp sind vom Typ KeyEventHandler: Public Delegate Sub KeyEventHandler(sender As Object, e As KeyEventArgs)
Das Objekt vom Typ KeyEventArgs beschreibt genauer, welche Taste(n) gedrückt wurden: Eigenschaft
Typ
Beschreibung
Alt
Boolean
Gibt an, ob die (Alt)-Taste gedrückt ist.
R
Control
Boolean
Gibt an, ob die (Strg)-Taste gedrückt ist.
R
Handled
Boolean
Gibt an, ob das Ereignis behandelt wurde, oder unterbindet dies.
KeyCode
Keys
Der Tastencode, der zur Auslösung des Ereignisses geführt hat
R
KeyData
Keys
Beschreibt alle gleichzeitig gedrückten Tasten.
R
KeyValue
Integer
Die Darstellung der KeyData-Eigenschaft als Ganzzahl
R
Tabelle 13.1
850
Eigenschaften von »KeyEventArgs« (R = ReadOnly)
Die Tastaturschnittstelle
Eigenschaft
Typ
Beschreibung
Modifiers
Keys
Angabe darüber, welche Zustandstasten gedrückt sind
R
Shift
Boolean
Gibt an, ob die (ª)-Taste gedrückt ist.
R
SuppressKeyPress
Boolean
Gibt an, ob das Ereignis an das untergeordnete Steuerelement gesendet werden soll.
Tabelle 13.1
Eigenschaften von »KeyEventArgs« (R = ReadOnly) (Forts.)
Die Eigenschaften Alt, Control und Shift sagen, ob die gleichnamige Zustandstaste gedrückt ist, Modifiers kombiniert diese drei Informationen in einem Wert. KeyData und KeyValue haben zwar denselben Informationsgehalt, unterscheiden sich jedoch im Typ. KeyData enthält die Tastaturcodes aller gleichzeitig gedrückten Tasten und ist somit die Zusammenfassung der Eigenschaften KeyCode und Modifiers.
KeyCode und die Enumeration Keys Drei Eigenschaften in der Tabelle 13.1 sind von einem noch unbekannten Typ: Keys. Jede Taste einer Tastatur wird durch eine eindeutige Zahl beschrieben, den Tastaturcode. Die Enumeration Keys beschreibt alle erlaubten Tastencodes. Ich möchte nur die wichtigsten vorstellen, auf die Sie bei der täglichen Programmierung häufig zugreifen. Der besseren Übersicht wegen habe ich sie zu mehreren logischen Gruppen zusammengefasst. Die wichtigste Gruppe ist die der Buchstabentasten, die insgesamt 26 Mitglieder enthält. Die Werte der Konstanten in Keys sind mit den ASCII-Codes für Großbuchstaben identisch: Konstante
Enumerationswert
A
65
B
66
C
67
... Y
89
Z
90
Tabelle 13.2
Buchstaben in der Enumeration »Keys«
Auch bei den Zahlen stimmen die Konstantenwerte mit den ASCII-Codes überein: Konstante
Enumerationswert
D0
48
D1
49
D2
50
... D8
56
D9
57
Tabelle 13.3
Zahlen in der Enumeration »Keys«
851
13.1
13
Tastatur- und Mausereignisse
Die von uns üblicherweise als Alt bezeichnete Taste heißt hier Menu (siehe Tabelle 13.4). Das stammt vermutlich daher, dass die (Alt)-Taste sehr häufig zur Menüauswahl eingesetzt wird. Es gibt noch ähnlich lautende Konstanten, die zwischen der linken bzw. rechten Anordnung unterscheiden. Die Notwendigkeit der Unterscheidung kommt nur recht selten vor. Konstante
Enumerationswert
ShiftKey
16
ControlKey
17
Menu
18
Tabelle 13.4
Zustandstasten in der Enumeration »Keys«
Sehr häufig anzutreffen sind die mit den Funktionstasten verbundenen anwendungsspezifischen Funktionalitäten (Tabelle 13.5). Tastaturen mit 24 Funktionstasten sind etwas unüblich, Ich habe einmal eine in einer Bibliothek gesehen. Normal sind 12 Funktionstasten. Konstante
Enumerationswert
F1
112
F2
113
... F23
134
F24
135
Tabelle 13.5
Funktionstasten in der Enumeration »Keys«
Wenn Sie Konstantenbezeichner anderer Tasten brauchen, schlagen Sie in der Dokumentation der Keys-Enumeration nach. Wie man mit den Keys-Konstanten arbeitet, zeigt das folgende Beispiel eines Fensters, das eine Textbox enthält. Die Benutzereingabe wird im Ereignis KeyDown überprüft. Gibt der Anwender einen der Buchstaben M, N oder I ein, wird je nach Buchstabe die WindowStateEigenschaft der Form auf maximiert, minimiert oder normal eingestellt. '...\TastaturMaus\Tastatur\KeyDown.vb
Public Class KeyDown Private Sub Taste(sender AsObject, e As KeyEventArgs) _ Handles TextBox1.KeyDown If e.KeyCode = Keys.N Then Me.WindowState = FormWindowState.Normal ElseIf e.KeyCode = Keys.M Then Me.WindowState = FormWindowState.Maximized ElseIf e.KeyCode = Keys.I Then Me.WindowState = FormWindowState.Minimized End If End Sub End Class
852
Die Tastaturschnittstelle
Der Tastencode wird mit der Eigenschaft KeyCode des KeyEventArgs-Objekts abgefragt. Es spielt dabei keine Rolle, ob der Anwender die Zeichen als Groß- oder Kleinbuchstaben eingibt, denn ausschlaggebend ist einzig und allein die gedrückte Taste und nicht das Zeichen. Ist eine Unterscheidung zwischen Groß- und Kleinschreibung zwingend notwendig, gibt es zwei Alternativen: Entweder wird der Zustand der (ª)-Taste abgefragt, oder – was einfacher ist – es wird nicht das Ereignis KeyDown, sondern das Ereignis KeyPress behandelt.
Eigenschaft KeyData Die Klasse KeyEventArgs stellt in drei Eigenschaften Informationen zu gedrückten Tasten zur Verfügung, die sich teils überlappen. Alle haben den Typ Keys und hängen zusammen: KeyData = KeyCode + Modifiers. Jeder Tastendruck hat die Ereigniskette KeyDown und KeyUp zur Folge, bei mehreren gleichzeitig gedrückten Tasten löst jede gedrückte Taste ein eigenes Ereignis aus. Bei der Untersuchung einer Tastenkombination interessiert aber nicht die einzelne Taste, sondern die Gesamtheit aller gleichzeitig gedrückten Tasten. Um diese Information zu erhalten, wertet man die Bitmaske der vier Byte großen Eigenschaft KeyData aus. Die beiden unteren Bytes beschreiben, welche Tasten der Anwender gedrückt hält. Darin sind auch die Zustandstasten enthalten, gegebenenfalls nach linker und rechter Taste unterschieden. Wenn das Ereignis einer »normalen« Taste ausgelöst wird, taucht die gegebenenfalls auch gedrückte Zustandstaste nicht in den beiden unteren Bytes auf. Um diese zu erfassen, beschreiben die oberen beiden Bytes den Zustand von (Alt), (Strg) und (ª) (nicht die Tasten selbst, so dass rechte und linke Version nicht unterschieden werden). Die Werte zeigt Tabelle 13.6. Konstante
Enumerationswert
None
0 bzw. 0x00000000
Shift
65536 bzw. 0x00010000
Control
131072 bzw. 0x00020000
Alt
262144 bzw. 0x00040000
Tabelle 13.6
Zustände in der Enumeration »Keys«
Beachten Sie, dass der Wert für die (Alt)-Taste nun tatsächlich auch Alt heißt und nicht mehr Menu. Durch die Werte wird bei gedrückter Zustandstaste immer nur ein ganz bestimmtes Bit im vierten Byte gesetzt: Shift = 0000 0000 0000 0001 .... Control = 0000 0000 0000 0010 .... Alt = 0000 0000 0000 0100 ....
Sind mehrere Zustandstasten gleichzeitig gedrückt, werden die zugeordneten Bits einfach bitweise ODER-verknüpft. Dazu ein Beispiel. Die Taste (F9) wird durch die Zahl 120 beschrieben, deren Bitmuster 0111 1000
853
13.1
13
Tastatur- und Mausereignisse
ist und hexadezimal 0x0078 lautet. Ist nur eine Taste gedrückt, hat KeyData denselben Inhalt wie KeyCode. Wird beispielsweise nur die (F9)-Taste gedrückt, führen die beiden folgenden Zeilen demnach gleichermaßen zum Schließen der aktuellen Form: If e.KeyCode = Keys.F9 Then Me.Close() If e.KeyData = Keys.F9 Then Me.Close()
Verändern wir nun das Beispiel und fragen eine Tastenkombination ab. Mit der Tastenkombination (ª)+(Alt)+(F9) soll das aktuelle Formular geschlossen werden: Private Sub Taste(sender AsObject, e As KeyEventArgs) _ Handles TextBox1.KeyDown If e.KeyData = (Keys.Shift Or Keys.Alt Or Keys.F9) Then Me.Close() End Sub
Die Keys-Werte der gewünschten Kombination werden mit dem bitweisen ODER-Operator verknüpft, und das Ergebnis wird mit dem aktuellen Inhalt von KeyData verglichen. Stimmt die gewünschte Tastenkombination mit dem vorliegenden Bitmuster überein, wird die Methode Close auf das aktuelle Form-Objekt aufgerufen.
Eigenschaft Modifiers Mit Modifiers werden alle drei Zustandstasten zusammen beschrieben. Das entspricht dem Inhalt der oberen beiden Bytes von KeyData. Damit gilt folgerichtig auch, dass die bitweise Verknüpfung von Modifiers und KeyCode der Inhalt von KeyData ist. Eine ergänzende Alternative zu der Eigenschaft Modifiers Die Klasse Control hat eine weitere Möglichkeit, den Status der drei Zustandstasten festzustellen: die klassengebundene, schreibgeschützte Eigenschaft ModifierKeys. Während die Eigenschaft Modifiers der Klasse KeyEventArgs immer an das KeyDown- oder KeyUp-Ereignis einer bestimmten Komponente gebunden ist, agiert die Eigenschaft ModifierKeys vollkommen ereignisunabhängig und ist somit auch unabhängig von jeglichem Steuerelement. Sie wird auf die Klasse Control aufgerufen und liefert wie Modifiers den Zustand der Tasten (Alt), (Strg) und (ª). Damit wird es möglich, die Zustandstasten auch im Zusammenhang mit Ereignissen auszuwerten, die darüber keine Informationen liefern. Das folgende Codefragment ist der Ereignishandler des Click-Ereignisses einer Schaltfläche. Obwohl das Ereignis keine Tastaturinformationen bereitstellt, wird ein Meldungsfenster geöffnet, wenn entweder die (Alt)- oder die (ª)-Taste während des Klickens gedrückt ist. Private Sub X_Click(sender As Object, ByVal e As EventArgs) Handles X.Click Dim keys As Keys = Control.ModifierKeys If keys = Keys.Alt OrElse keys = Keys.Shift Then _ MessageBox.Show("Zustandstaste 'Alt' oder 'Shift' gedrückt") End Sub
Soll eine Kombination mehrerer Zustandstasten zu einem bestimmten Verhalten der Anwendung führen, müssen die entsprechenden Enumerationskonstanten mit dem bitweisen ODEROperator verknüpft werden. Im folgenden Beispiel wird das Meldungsfenster nur angezeigt, wenn die (Alt)- und die (ª)-Taste gleichzeitig gedrückt sind:
854
Die Tastaturschnittstelle
Private Sub X_Click(sender As Object, ByVal e As EventArgs) Handles X.Click Dim keys As Keys = Control.ModifierKeys If keys = (Keys.Alt Or Keys.Shift) Then _ MessageBox.Show("Zustandstasten 'Alt' und 'Shift' gedrückt") End Sub
Die Eigenschaft SuppressKeyPress Möchten Sie verhindern, dass ein oder mehrere Tastaturereignisse vom Steuerelement empfangen und verarbeitet werden, setzen Sie im Ereignishandler SuppressKeyPress=True. if e.KeyData = Keys.A Then e.SuppressKeyPress = True
Mit diesem Codefragment wird das Drücken der Taste (A) unterdrückt und nicht zur Verarbeitung an das Steuerelement weitergeleitet.
13.1.4 Das Ereignis KeyPress Die bisher besprochenen Eigenschaften KeyDown und KeyUp behandeln ganz allgemein das Drücken einer Taste auf der Tastatur. Viele Tasten erzeugen aber Zeichencodes, die für das Ausgabeziel bestimmt sind. Bei diesen Tasten wird zwischen KeyDown und KeyUp ein weiteres Ereignis ausgelöst: KeyPress. Der Ereignishandler von KeyPress hat ein Argument vom Typ KeyPressEventArgs, der das Ereignis durch zwei Eigenschaften beschreibt: KeyChar und Handled (siehe Tabelle 13.7). Eigenschaft
Typ
Beschreibung
Handled
Boolean
Gibt an, ob das Ereignis behandelt worden ist.
KeyChar
Char
Gibt an, welchem ASCII-Zeichen die gedrückte Taste(nkombination) entspricht (meist nur mit oder ohne (ª)).
Tabelle 13.7
Die Eigenschaften von »KeyPressEventArgs«
Die Eigenschaft KeyChar enthält das ASCII-Zeichen der gedrückten Taste. Um den Unterschied zwischen den Ereignissen KeyDown und KeyUp und KeyPress zu verdeutlichen, ist das Beispiel KeyDown nun so umgeschrieben worden, dass die Benutzereingabe nicht mehr von KeyDown, sondern von KeyPress ausgewertet wird. '...\TastaturMaus\Tastatur\KeyPress.vb
Public Class KeyPress Private Sub Taste(sender AsObject, e As KeyPressEventArgs) _ Handles TextBox1.KeyPress If e.KeyChar = "N"c Then Me.WindowState = FormWindowState.Normal ElseIf e.KeyChar = "M"c Then Me.WindowState = FormWindowState.Maximized ElseIf e.KeyChar = "I"c Then
855
13.1
13
Tastatur- und Mausereignisse
Me.WindowState = FormWindowState.Minimized End If End Sub End Class
Rufen wir uns noch einmal in Erinnerung: Im Beispiel KeyDown wurde nur die gedrückte Taste ausgewertet. Der Zustand der Taste (ª) spielte dabei keine Rolle. Deshalb wurde auch nicht zwischen der Groß- und Kleinschreibung unterschieden, und das Programm reagierte in beiden Fällen gleich. KeyPress wertet jedoch den ASCII-Code aus, der bei den Klein- und Großbuchstaben unterschiedlich ist. Der Anwender wird also gezwungen, »M« und nicht »m« einzugeben, um das Fenster zu maximieren.
Eigenschaft Handled Normalerweise werden Tastatureingaben automatisch verarbeitet. Gibt der Anwender in einer Textbox den Buchstaben »A« ein, wird dieser sofort im Steuerelement ausgegeben. Die Tastaturereignisse sind meist dann von Interesse, wenn die Benutzereingabe ausgewertet werden muss. Das ist zum Beispiel dann der Fall, wenn eine bestimmte Eingabe unterdrückt und durch eine andere ersetzt werden soll. Nehmen wir an, dass die Eingabe eines Umlauts automatisch durch »ae«, »oe« bzw. »ue« ersetzt werden soll. Am einfachsten manipulieren Sie den Inhalt der Textbox über die Eigenschaft SelectedText, um die Zeichenfolge direkt in die Anzeige der Textbox zu schreiben. Ist ein Teil der Zeichenfolge markiert, werden die markierten Zeichen durch die neue Zeichenfolge ersetzt; ist kein Text markiert, wird die Zeichenfolge an der aktuellen Cursorposition eingesetzt. Private Sub Umlaut(ByVal sender As Object, ByVal e As KeyPressEventArgs) _ Handles TextBox1.KeyPress If e.KeyChar = "ä"c Then TextBox1.SelectedText = "ae" ElseIf e.KeyChar = "ö"c Then TextBox1.SelectedText = "oe" ElseIf e.KeyChar = "ü"c Then TextBox1.SelectedText = "ue" End If End Sub
Geben wir das mit TextBox1 bezeichnete TextBox-Objekt »Händler« an der Tastatur ein, lautet die Ausgabe Haeändler. Das Programm hat zwar die Eingabe des Umlauts richtig erkannt und »ae« in die Textbox geschrieben, allerdings sorgt die automatische Eingabeverarbeitung auch dafür, dass der unerwünschte Umlaut außerdem ausgegeben wird. Jetzt kommt die Eigenschaft Handled vom Typ Boolean ins Spiel. Der Standardwert False betrachtet die Verarbeitung als unvollendet, und das Programm macht mit der Standardverarbeitung weiter. Wird Handled=True gesetzt, schalten wir die automatische Eingabeverarbeitung aus und nehmen die Ereignisbehandlung komplett selbst in die Hand.
856
Die Tastaturschnittstelle
'...\TastaturMaus\Tastatur\Handled.vb
Public Class Handled Private Sub Umlaut(ByVal sender As Object, ByVal e As KeyPressEventArgs) _ Handles TextBox1.KeyPress If e.KeyChar = "ä"c Then TextBox1.SelectedText = "ae" : e.Handled = True ElseIf e.KeyChar = "ö"c Then TextBox1.SelectedText = "oe"" : e.Handled = True ElseIf e.KeyChar = "ü"c Then TextBox1.SelectedText = "ue"" : e.Handled = True End If End Sub End Class
Die Anweisung E.Handled=True sollte übrigens nicht nach der letzten If-Anweisung stehen. Wenn Sie das machen, schalten Sie – bis auf die Umlaute – die Behandlung aller anderen Zeichen ab. Handled wird auch von den beiden Ereignissen KeyDown und KeyUp unterstützt. Allerdings
zeigt sich hier ein anderes Verhalten, wenn auf eine Ziffern- oder Buchstabentaste gedrückt wird. Dazu ein Beispiel: Private Sub textBox1_KeyDown(sender As Object, e As KeyEventArgs e) _ Handles textBox1.KeyDown If e.KeyCode = Keys.A Then textBox1.SelectedText = "Hallo" e.Handled = True End If End Sub
Obwohl der Code sehr ähnlich zum Beispiel Handled ist, wird ein eingegebenes Zeichen »a« (bzw. »A«) nicht durch die Zeichenfolge »Hallo« ersetzt, sondern nur ergänzt. Die Einstellung Handled=True bleibt wirkungslos, weil für die Zeichenverarbeitung nur das Ereignis KeyPress verantwortlich ist. Selbst ein zweiter KeyDown-Ereignishandler wird ausgeführt.
Abschalten einer bestimmten Taste Manchmal kommt es vor, dass man die Funktionalität einer bestimmten Taste nicht zulassen kann und diese deaktivieren muss. Auch hierzu bietet sich Handled an. Im folgenden Codefragment wird die Home-Taste (= (Pos1)) abgeschaltet. Damit wird es dem Anwender unmöglich gemacht, den Eingabecursor an die erste Position zu setzen. Dass das mit den Steuerungstasten immer noch möglich ist, übersehen wir hierbei großzügig. Private Sub textBox1_KeyDown(sender As Object, e As KeyEventArgs e) _ Handles textBox1.KeyDown If e.KeyCode = Keys.Home Then e.Handled = True End Sub
857
13.1
13
Tastatur- und Mausereignisse
Mit Return zur nächsten Textbox Normalerweise wird mit der Tabulatortaste von einer Textbox in die nächste gesprungen. Manche Anwender verwenden aber stattdessen lieber die (¢)-Taste. Das folgende Beispiel soll zeigen, wie das Drücken von (¢) abgefangen und das nächste Steuerelement fokussiert wird. Dabei hilft uns die Methode ProcessTabKey der Form weiter, der wir True übergeben, um das nächste in der Fokussierreihenfolge stehende Steuerelement zu erreichen. Private Sub TextBox1_KeyPress(sender As Object, e As KeyPressEventArgs) _ Handles TextBox1.KeyPress If e.KeyChar.Equals(ChrW(13)) Then Me.ProcessTabKey(True) e.Handled=True End If End Sub
13.1.5 Tastaturereignisse der Form Als abgeleitete Klasse von Control erbt auch die Klasse Form die Tastaturereignisse. Sie werden jedoch unterdrückt, sobald die Form ein Steuerelement enthält, das selbst Tastaturereignisse verarbeiten kann. Manchmal möchte man aber bestimmte Tasten nur in der Form – seltener sowohl in der Form als auch in einem Steuerelement – verarbeiten. Damit das Fenster Tastaturereignisse empfangen kann, muss die Eigenschaft KeyPreview des Form-Objekts auf True gesetzt werden. Jedes Tastaturereignis wird dann zuerst von der Form entgegengenommen und erst danach vom aktiven Steuerelement. 왘
KeyDown (Form)
왘
KeyDown (Steuerelement)
왘
KeyPress (Form)
왘
KeyPress (Steuerelement)
왘
KeyUp (Form)
왘
KeyUp (Steuerelement)
Soll mit der Einstellung KeyPreview=True ausschließlich die Form für die Verarbeitung eines der drei Tastaturereignisse verantwortlich sein, muss im entsprechenden Ereignishandler der Form Handled=True gesetzt werden. Die Auslösung des entsprechenden Ereignisses des Steuerelements wird damit verhindert.
13.1.6 Tastatureingaben simulieren Mit der statischen Methode Send aus der Klasse SendKeys im Namensraum System.Windows. Forms können Sie eine Tastatureingabe auch mittels Programmcode simulieren, zum Beispiel: SendKeys.Send("Hallo")
Empfänger der Zeichenfolge ist das Steuerelement, das den Eingabefokus hat. Damit ließe sich das Beispiel Handled auch wie folgt codieren:
858
Die Mausschnittstelle
Private Sub Umlaut(ByVal sender As Object, ByVal e As KeyPressEventArgs) _ Handles TextBox1.KeyPress If e.KeyChar = "ä"c Then SendKeys.Send("ae") e.Handled = True ... End Sub
Die Send-Methode nimmt eine Zeichenfolge entgegen. Die einfachste Form ist ein Text in Anführungsstrichen. Es gibt auch Tastatureingaben, die kein Zeichen ausgeben. Für diese Tasten gilt eine besondere Festlegung: Sie werden durch einen festgelegten Zeichencode beschrieben, der in geschweifte Klammern eingeschlossen wird. Die folgende Anweisung simuliert beispielsweise das Drücken der (F4)-Taste: SendKeys.Send("{F4}")
Die Zeichencodes, die den einzelnen Tasten zugeordnet sind, können Sie der Dokumentation der Klasse SendKeys entnehmen. Wollen Sie eine Tastenkombination mit den Zustandstasten (ª), (Strg) und (Alt) simulieren, wird der jeweilige Buchstabe in runden Klammern angegeben und davor ein Codezeichen gesetzt. Die drei Codezeichen können Sie der Tabelle 13.8 entnehmen. Taste
Code
(ª)
+
(Alt)
%
(Strg)
^
Tabelle 13.8
Code der Zustandstasten der Methode »SendKeys.Send«
Die Anweisung für die Tastenkombination (ª) und (A) lautet dann: SendKeys.Send("+(A)")
Neben der Methode Send enthält die Klasse SendKeys noch die Methode SendWait, die zuerst auf die vollständige Verarbeitung der Tastatureingabe wartet.
13.2
Die Mausschnittstelle
Obwohl die meisten Anwendungen auch ohne Maus bedient werden können, gehört die Maus zur Standardausrüstung jedes PC. Die Ausführungen reichen von einer Zweitasten- über eine Dreitastenmaus bis hin zur IntelliMouse, bei der anstelle einer dritten Taste ein Rädchen zu finden ist, das beim Drücken die dritte Taste ersetzt. Mit Programmen, die das Rädchen unterstützen, können Sie bequemer durch lange Dokumente scrollen. Durch eine Mausaktion können mehrere verschiedene Ereignisse ausgelöst werden. Zwei haben wir im Verlauf der letzten Kapitel schon eingesetzt: Click und DoubleClick. Da diese Ereignisse nicht nur bei einem Mausklick, sondern auch durch die Tastatur ausgelöst werden
859
13.2
13
Tastatur- und Mausereignisse
können, sind beide durch MouseClick und MouseDoubleClick ergänzt worden, die nur auf die Maus reagieren. Darüber hinaus stehen Ihnen noch folgende Ereignisse zur Verfügung: 왘
MouseDown
왘
MouseEnter
왘
MouseHover
왘
MouseLeave
왘
MouseMove
왘
MouseUp
왘
MouseWheel
In einer Windows-Form kann immer nur eine Komponente Mausereignisse empfangen. Wird der Mauscursor über das Steuerelement einer Form bewegt, empfängt nicht die Form das Mausereignis, sondern das Steuerelement. Das setzt selbstverständlich voraus, dass das Steuerelement sichtbar und aktiviert ist (Enabled=True). Liegen zwei Steuerelemente übereinander, empfängt dasjenige Steuerelement Mausereignisse, das in der z-Reihenfolge an oberster Stelle liegt. Eine Form empfängt nur Mausereignisse, wenn sich der Mauszeiger über einem Teil des Clientbereichs befindet, der weder sichtbare und aktive Steuerelemente enthält noch Teil des Rahmens oder der Titelleiste ist.
13.2.1 Die Ereignisse MouseDown, MouseMove und MouseUp Wir wollen die Ereignisse MouseDown, MouseMove und MouseUp näher betrachten, da sie in nahezu jeder Anwendung zu finden sind. Aus deren Bezeichnern kann schon entnommen werden, wann die Ereignisse ausgelöst werden: MouseDown beim Druck auf eine Maustaste und MouseUp beim Loslassen einer Maustaste. Wird die Maus über eine Komponente bewegt, werden sehr viele MouseMove-Ereignisse ausgelöst. Alle drei Ereignisse sind vom Typ MouseEventHandler und übergeben dem Ereignishandler im zweiten Parameter ein Objekt vom Typ MouseEventArgs (siehe Tabelle 13.9). Eigenschaft
Typ
Beschreibung (alle ReadOnly)
Button
MouseButtons
Gibt an, welche Maustaste gedrückt wurde.
Clicks
Integer
Gibt an, wie oft die Maustaste gedrückt wurde.
Delta
Integer
Anzahl der Arretierungen, um die das Mausrad gedreht wurde. Je nach Drehrichtung ist die Zahl positiv oder negativ.
Location
Point
Liefert die Mauszeigerkoordinaten x und y in Pixel.
X
Integer
X-Koordinate eines Mausklicks
Y
Integer
Y-Koordinate eines Mausklicks
Tabelle 13.9
Die Eigenschaften in »MouseEventArgs«
Da die Mausereignisse unabhängig davon sind, welche Maustaste der Anwender gedrückt hat, ist die Auswertung der Maustaste wichtig. Die Eigenschaft Button liefert uns die Taste mittels Werten der Enumeration MouseButtons (siehe Tabelle 13.10).
860
Die Mausschnittstelle
Konstante
Beschreibung
None
Keine Maustaste wurde gedrückt.
Left
Die linke Maustaste wurde gedrückt.
Right
Die rechte Maustaste wurde gedrückt.
Middle
Die mittlere Maustaste wurde gedrückt.
XButton1
XButton eins wurde gedrückt.
XButton2
XButton zwei wurde gedrückt.
Tabelle 13.10
Konstanten in »MouseButtons«
Die ersten vier Enumerationskonstanten sind selbsterklärend, da sie zum Standard gehören. Die letzten beiden, XButton1 und XButton2, beziehen sich auf die Tasten der IntelliMouse Explorer, die über fünf Tasten verfügt. Die Auswertung der gedrückten Maustaste ist einfach. Im Ereignishandler, der mit dem Mausereignis verknüpft ist, wird die Eigenschaft Button des zweiten Parameters vom Typ MouseEventArgs abgefragt und darauf entsprechend reagiert: ' Ereignishandler eines MouseDown-Events Private Sub MouseEvent(sender As Object, e As MouseEventArgs) If e.Button = MouseButtons.Left Then _ ... ' die linke Maustaste wurde gedrückt End Sub
Die Enumeration MouseButtons hat das Flags-Attribut. Daher können die Werte bitweise miteinander kombiniert werden, um gleichzeitig gedrückte Tasten zu erfassen. Zum Beispiel kann so eine Zweitastenmaus eine mittlere Taste durch Klick auf beide Tasten simulieren. Sollten Sie als Linkshänder in der Systemsteuerung von Windows die linke und rechte Maustaste vertauscht haben, wird Ihnen bei der Auswertung von MouseButtons trotz des Drückens der rechten Maustaste Left zurückgegeben. Sinnvoller wäre es daher gewesen, zwischen einer primären und einer sekundären Maustaste zu unterscheiden und dabei die primäre mit der gleichzusetzen, mit der standardmäßig das Click-Ereignis ausgelöst wird. Die MouseEventsArgs-Eigenschaft Clicks liefert nur die Zahlen 1 oder 2. Damit lässt sich feststellen, ob es sich um einen einfachen Klick oder um einen Doppelklick des Anwenders handelt. Hinweis Die Zeitspanne in Millisekunden, die unterschritten werden muss, um aus zwei einfachen Klicks einen Doppelklick zu machen, können Sie aus der klassengebundenen Eigenschaft DoubleClickTime der Klasse SystemInformation im Namensraum System.Windows.Forms auslesen.
Die Eigenschaften X und Y enthalten die aktuellen Koordinaten des Mauscursors, und Location liefert beide Werte gleichzeitig über ein Point-Objekt. Die Werte in Pixel beziehen sich
immer auf das Steuerelement oder die Form, über deren Bereich sich der Mauszeiger befin-
861
13.2
13
Tastatur- und Mausereignisse
det. Dabei ist der Nullpunkt immer die linke obere Ecke der Komponente, die das Mausereignis ausgelöst hat. Im folgenden Beispiel MouseMove wird in einer PictureBox an der aktuellen Mauszeigerposition ein Fadenkreuz aus einer vertikalen und einer horizontalen Linie gezeichnet, die jeweils von einem Rand bis zum gegenüberliegenden reicht (siehe Abbildung 13.1).
Abbildung 13.1
Ausgabe des Beispiels »MouseMove«
'...\TastaturMaus\Maus\MouseMove.vb
Public Class MouseMove Private MousePoint As Point Private Sub Fadenkreuz_MouseMove(sender As Object, e As MouseEventArgs) _ Handles Fadenkreuz.MouseMove X.Text = e.X.ToString() Y.Text = e.Y.ToString() Dim graph As Graphics = Fadenkreuz.CreateGraphics() ' altes Fadenkreuz löschen DrawLines(graph, MousePoint, Fadenkreuz.BackColor) ' neues Fadenkreuz zeichnen MousePoint = New Point(e.X, e.Y) DrawLines(graph, MousePoint, Color.White) graph.Dispose() End Sub Private Sub DrawLines(graph As Graphics, p As Point, c As Color) Dim pen As New Pen(c) Dim startHor As New Point(0, p.Y) Dim endHor As New Point(Fadenkreuz.Width, p.Y) Dim startVert As New Point(p.X, 0) Dim endVert As New Point(p.X, Fadenkreuz.Height)
862
Die Mausschnittstelle
' Fadenkreuz zeichnen graph.DrawLine(pen, startHor, endHor) graph.DrawLine(pen, startVert, endVert) End Sub End Class
Das Fadenkreuz wird gezeichnet, sobald die Maus in den Clientbereich der Picturebox eintritt. Die Koordinaten des Mauszeigers werden in je einer Textbox ausgegeben. Damit die Picturebox beim Ziehen der Maus nur das aktuelle Fadenkreuz anzeigt, muss zuerst das ungültige gewordene Fadenkreuz gelöscht werden. Dazu ist auf Klassenebene das Feld MousePoint deklariert, das immer die Mauskoordinaten enthält, mit denen das letzte Fadenkreuz gezeichnet worden ist. Mit diesen Punktkoordinaten wird zuerst ein Fadenkreuz in der Hintergrundfarbe der Picturebox gezeichnet. Das ungültig gewordene Fadenkreuz ist damit »gelöscht«. Die aktuellen Mauskoordinaten werden anschließend in das Feld MousePoint geschrieben, und das neue Fadenkreuz wird in weißer Farbe gezeichnet. Das Zeichnen eines Fadenkreuzes übernimmt die Methode DrawLines der Form, die aus dem MouseMove-Ereignis heraus sowohl für das zu löschende als auch für das neu zu zeichnende Fadenkreuz aufgerufen wird. Die eigentliche Zeichenoperation übernimmt die Methode DrawLine der Klasse Graphics. Das erste Argument ist ein Pen-Objekt, das die Farbe des Fadenkreuzes beschreibt. Je zwei Point-Objekte legen den Start- und den Endpunkt jeder Linie fest.
13.2.2 Das Ereignis MouseWheel Ohne offensichtlichen Grund wird das MouseWheel-Ereignis nicht mit den anderen Ereignissen im Eigenschaftsfenster gelistet. Dennoch unterstützen alle Steuerelemente dieses Ereignis, die sich für den Einsatz des Mausrads eignen (z. B. die Textbox), ohne dass eine Zeile Code implementiert werden muss. Ebenso können Ihre Steuerelemente das Ereignis behandeln; es fehlt lediglich der Komfort der automatischen Erstellung eines Ereignishandlers. Manuell ist die Implementierung aber auch schnell erledigt. Private Sub MouseWheelHandler(sender As Object, e As MouseEventArgs e) _ Handles textBox1.MouseWheel ' Anweisungen End Sub
Zur Steuerung des Bildlaufs dient die Eigenschaft Delta des MouseEventArgs-Objekts. Beträgt der Wert 120, entspricht das einem Bildlauf. Der Wert 120 stammt aus den Tiefen der Win32API. Er entspricht dem Weiterdrehen des Mausrades um ein Raster. Die Richtung der Drehung spiegelt sich im Vorzeichen von Delta wider. Die Frage, um wie viele Zeilen der Bildlauf bei der Drehung des Mausrades um 120 Einheiten erfolgt, kann der Eigenschaft SystemInformation.MouseWheelScrollLines entnommen werden. Der Standard ist auf 3 festgelegt.
863
13.2
13
Tastatur- und Mausereignisse
13.2.3 Weitere Mausereignisse Die folgenden drei Mausereignissen befassen sich mit der Bewegung über ein Steuerelement: 왘
MouseEnter wird ausgelöst, wenn der Mauszeiger den Bereich der Komponente betritt.
왘
MouseHover tritt auf, wenn der Mauszeiger im Clientbereich der Komponente einen Augenblick lang bewegungslos verharrt, d. h., wenn er nicht kontinuierlich bewegt wird.
왘
MouseLeave ist das Gegenstück zu MouseEnter und wird ausgelöst, wenn der Mauszeiger
den Clientbereich der Komponente verlässt. Alle drei Ereignisse sind vom Typ EventHandler und stellen dem Ereignishandler somit auch keine weiteren Informationen zur Verfügung, denn das Objekt EventArgs enthält keine spezifischen Eigenschaften.
13.2.4 Click-Ereignisse Die Ereignisse Click und DoubleClick sowie die analogen MouseClick und MouseDoubleClick sind an die primäre Maustaste gebunden. Standardmäßig ist das die linke; Linkshänder stellen in der Systemsteuerung oft die rechte Maustaste als primäre Maustaste ein. Das Click-Ereignis tritt zwischen den Ereignissen MouseDown und MouseUp auf. Ein Doppelklick wird dann erkannt, wenn kurz genug hintereinander auf die primäre Maustaste gedrückt wird. Die Zeitspanne wird in der Systemsteuerung eingestellt.
13.2.5 Verhalten der Maus Die Eigenschaft Cursor Die Darstellung des Mauszeigers zur Laufzeit wird durch die in der Klasse Control definierte Eigenschaft Cursor festgelegt. Ausnahmsweise sind die erlaubten Cursortypen nicht in einer Aufzählung, sondern in der Klasse Cursors enthalten. Der Mauscursor kann ein optischer Hinweis auf laufende Aktionen sein. Beispielsweise weist die in Cursors.WaitCursor beschriebene Sanduhr den Anwender auf eine länger andauernde Operation hin. Nach der Operation dürfen Sie nicht vergessen, den ursprünglichen Mauscursor wiederherzustellen: Me.Cursor = Cursors.WaitCursor ... Me.Cursor = Cursors.Default
Sowohl das Formular als auch die Steuerelemente ermöglichen eine Anpassung des Cursors an die aktuellen Gegebenheiten. Stellen Sie diese Eigenschaft für die Form ein, gilt dies auch für die darin enthaltenen Steuerelemente, solange Sie diesen nicht ausdrücklich eine andere Einstellung zuweisen.
Die Eigenschaft Cursor.Position Die Klasse Cursor veröffentlicht eigene Eigenschaften und Methoden. Die interessanteste ist wahrscheinlich die klassengebundene Eigenschaft Position, mit der die Position des Maus-
864
Die Mausschnittstelle
zeigers nicht nur abgerufen, sondern auch gesetzt werden kann. Die durch das Point-Objekt beschriebenen Koordinaten sind Bildschirmkoordinaten. Daher führt die Anweisung Cursor.Position = New Point(0, 0)
dazu, dass der Mauszeiger in die obere linke Ecke des Bildschirms springt – auch wenn die neue Position außerhalb der aktuellen Form liegt.
Die Eigenschaft Control.MousePosition Die Position des Mauszeigers relativ zum Bildschirm können Sie auch mit der klassengebundenen Eigenschaft MousePosition der Klasse Control auslesen. Die Anweisung Me.Text = Control.MousePosition.ToString()
gibt die Positionskoordinaten in der Titelleiste des Formulars in einer gut lesbaren Form aus. Dies ist ein gutes Beispiel dafür, dass die von Object geerbte ToString-Methode immer sinnvoll überschrieben werden sollte.
Die Methode PointToClient MousePosition liefert immer Bildschirmkoordinaten zurück. Manchmal muss man diese in Clientkoordinaten umrechnen, und am einfachsten geht dies mit der Methode PointToClient der Klasse Control. Übergeben wird zum Beispiel der Wert der Eigenschaft MousePosition: Me.Text = Me.PointToClient(Control.MousePosition).ToString()
Das von PointToClient zurückgegebene Point-Objekt kann sogar negative Werte enthalten. Folglich bietet sich PointToClient an, um den Abstand des Mauszeigers von einem beliebigen Steuerelement in Pixel zu ermitteln.
Druck auf eine Zustandstaste Manchmal möchte man, dass Mausaktionen nur dann ausgeführt werden, wenn eine oder mehrere der Zustandstasten (Strg), (Alt) und (ª) gedrückt sind. Hier kann die oben erwähnte klassengebundene Eigenschaft ModifierKeys eingesetzt werden. Soll das im Beispiel MouseMove gezeichnete Fadenkreuz nur dann an der aktuellen Mausposition dargestellt werden, wenn gleichzeitig die (Alt)-Taste gedrückt ist, können Sie den Ereignishandler des Ereignisses MouseMove wie folgt ergänzen: Private Sub Fadenkreuz_MouseMove(sender As Object, e As MouseEventArgs) _ Handles Fadenkreuz.MouseMove If Control.ModifierKeys Keys.Alt Then Return ... End Sub
865
13.2
Wenn ein Fenster Unterfenster haben darf, liegt ein Multiple Document Interface (MDI) vor. Die Besonderheiten im Zusammenspiel von Haupt- und Subfenstern sind Gegenstand dieses Kapitels.
14
MDI-Anwendungen
MDI-Fähigkeiten (Multiple Document Interface) waren bis vor wenigen Jahren Standard bei allen Anwendungen, die mehrere gleichartige Dokumente verwalteten, zum Beispiel die älteren Versionen von Microsoft Word oder Microsoft Excel. Kennzeichnend für MDI-Anwendungen ist ein übergeordnetes Hauptfenster, das im Zusammenspiel mit den ihm untergeordneten Fenstern besondere Verhaltensweisen zeigt. Die untergeordneten MDI-Fenster sind vom Hauptfenster abhängig. Das Hauptfenster agiert dabei als Container und steuert die Subfenster. So lassen sich zum Beispiel alle Subfenster innerhalb des Hauptfensters anordnen und ermöglichen einen einfachen Zugriff des Hauptfensters auf das aktive Subfenster. Üblicherweise haben MDI-Hauptfenster einen dunkelgrauen Hintergrund (siehe Abbildung 14.1). Es gibt noch weitere typische, visuelle Charakteristiken, beispielsweise die Titelleiste der MDI-Hauptform und die Menüverwaltung. Wir werden darauf später noch eingehen. Natürlich können neben den MDI-Subfenstern auch »normale« Fenster aus einem MDIHauptfenster heraus geöffnet und angezeigt werden. Diese sind dann nicht vom Hauptfenster abhängig. Dazu zählen unter anderem auch Dialogfenster. Anwendungen, die kein MDI-Hauptfenster haben, werden als SDI-Anwendungen (Single Document Interface) bezeichnet. Bisher haben wir nur SDI-Anwendungen entwickelt.
Abbildung 14.1
Beispiel einer MDI-Anwendung
867
14
MDI-Anwendungen
14.1
Das MDI-Hauptfenster
Normalerweise wird eine MDI-Anwendung mit dem Öffnen des Hauptfensters gestartet. Dazu setzen Sie die Eigenschaft IsMdiContainer der dafür vorgesehenen Form auf True. Der Unterschied zu einem herkömmlichen Fenster wird durch eine geänderte Rahmenart und Hintergrundfarbe kenntlich gemacht. Eine Änderung über die Eigenschaft BackColor ist nicht möglich, sondern Sie müssen die Änderungen in der Systemsteuerung vornehmen. MDI-Hauptfenster haben üblicherweise eine Menü- und Symbolleiste und darüber hinaus auch meistens eine Statusleiste zur Anzeige von Zustandsinformationen. Symbol- und Statusleiste sind am Rand der Containerform angedockt. Sie können auch beliebige andere Steuerelemente wie beispielsweise eine Schaltfläche im MDI-Hauptfenster positionieren. Diese sollten dann aber angedockt werden, sonst wird das nicht angedockte Steuerelement des Hauptfensters auf einem Subfenster angezeigt.
Schließen des Hauptfensters Ein Fenster kann auf verschiedene Arten geschlossen werden: 왘
Die Methode Close wird auf das zu schließende Formular aufgerufen.
왘
Der Anwender wählt im Systemmenü Schliessen.
왘
Der Anwender klickt auf die X-Schaltfläche in der Titelleiste.
왘
Sie rufen die Methode Exit bzw. ExitThread der Klasse Application auf.
Wird das Hauptfenster einer MDI-Anwendung geschlossen, werden zuerst die FormClosingEreignisse aller Subfenster aufgerufen, erst daran anschließend das FormClosing-Ereignis des Hauptfensters. Das FormClosing-Ereignis bietet sich dazu an, gegebenenfalls Daten zu sichern oder sogar den Schließvorgang durch e.Cancel=True abzubrechen. Wenn in einer MDI-Anwendung ein untergeordnetes Fenster das Schließen verweigert, führt das sogar dazu, dass die Anwendung nicht beendet werden kann.
Mausereignisse des Hauptfensters Bei der Entwicklung eines Hauptfenster dürfen Sie ein wichtiges Unterscheidungsmerkmal im Vergleich zu einem »normalen« Fenster nicht unberücksichtigt lassen: MDI-Hauptfenster empfangen generell keine Mausereignisse.
14.2
Die Subfenster
Meistens sind die von einem MDI-Hauptfenster verwalteten Subfenster vom gleichen Typ. Eine Anwendung darf aber auch mit verschiedenartigen Subfenstern arbeiten.
868
Die Subfenster
Ein Fenster in einer Anwendung, das ein MDI-Hauptfenster enthält, ist nicht automatisch ein Subfenster – dazu muss erst mit der Eigenschaft MdiParent eine Beziehung hergestellt werden. Wenn frm die Referenz auf das Subfenster ist, kann die Anweisung dazu so lauten: frm.MdiParent = Me
Der Eigenschaft wird die Referenz auf das MDI-Hauptfenster übergeben. Subfenster werden meistens aus dem Hauptfenster heraus geöffnet. Meist befindet sich zu diesem Zweck in der Menüleiste des Hauptfensters unter Datei ein Menüpunkt Neu... Die im Click-Ereignis des Menüpunktes angesprochene Methode erstellt ein Subfenster, das in einer eigenen Klasse als Windows-Form definiert ist. Public Class Subfenster Private Shared Anzahl As Integer Sub New(ByVal haupt As Form) InitializeComponent() If haupt Is Nothing OrElse Not haupt.IsMdiContainer Then _ Throw New ArgumentException("Kein MDI-Fenster.") Me.MdiParent = haupt Anzahl += 1 Me.Text = "Dokument " & Anzahl End Sub End Class Private Sub Neu_Click(ByVal sender As Object, ByVal e As EventArgs) _ Handles Neu.Click Dim frm As New Subfenster(Me) frm.Show() End Sub
In der ersten Anweisung wird das Subfenster mit dem Klassenbezeichner Subfenster instanziiert und mit Show angezeigt. Der Konstruktor stellt sicher, dass es an einen MDI-Container gebunden wird und die Titelleiste die Nummer des Fensters angibt. Beachten Sie, dass die Eigenschaft MdiParent im Eigenschaftsfenster nicht angeboten wird – diese Anweisung müssen Sie in jedem Fall codieren. MDI-Subfenster zeichnen sich durch einige besondere Charakteristika aus: 왘
Sie können mit der Maus an der Titelleiste gegriffen und verschoben werden, aber den Clientbereich des MDI-Hauptfensters können sie nicht verlassen. Stattdessen werden im Hauptfenster Bildlaufleisten angezeigt.
왘
Nach dem Maximieren des MDI-Subfensters nimmt es den gesamten Clientbereich des Hauptfensters ein und überdeckt alle anderen Subfenster. Die drei Titelleistenschaltflächen des maximierten MDI-Subfensters (Maximieren, Minimieren, Schliessen) werden in der Menüleiste des Hauptfensters eingeblendet. Außerdem wird der Text in der Titelleiste des Hauptfensters durch den des Subfensters in eckigen Klammern ergänzt.
왘
Im Gegensatz zu normalen Fenstern werden MDI-Subfenster nach dem Minimieren am unteren Rand des MDI-Hauptfensters und nicht in der Taskleiste von Windows abgelegt.
869
14.2
14
MDI-Anwendungen
Sowohl beim Aktivieren als auch beim Schließen eines untergeordneten Fensters tritt im MDI-Hauptfenster das Ereignis MdiChildActivate auf, das dem Ereignishandler keine weiteren Informationen zum Ereignis zur Verfügung stellt.
14.3
Zugriff auf die Subfenster
14.3.1 Alle Subfenster Sehen Sie sich noch einmal die Anweisung an, mit der neue Subfenster erstellt und angezeigt werden: Dim frm As New Subfenster(Me) frm.Show()
Wird das erste Subfenster erzeugt, kann dieses mit frm angesprochen werden. Mit dem Erzeugen des zweiten Subfensters wird die Objektvariable frm »umgebogen« und zeigt auf die Startadresse des zweiten Subfensters. Das erste wird zwar nicht mehr unter frm referenziert, ist aber weiterhin im Hauptfenster zu sehen, weil das Objekt nicht zerstört worden ist. Wie können Sie auf die einzelnen Subfenster zugreifen? Damit das Hauptfenster die Subfenster anzeigen kann, verwaltet es die Referenzen aller geöffneten Subfenster in einem Array, auf das Sie über die Eigenschaft MdiChildren des MDIHauptfensters zugreifen können. Public ReadOnly Property MdiChildren As Form()
Die Eigenschaft bietet sich dann an, wenn für alle Subfenster dieselben Operationen ausgeführt werden müssen – beispielsweise, um die darin enthaltenen Daten vor dem Schließen der Anwendung zu speichern. Das Schließen von Subfenstern hinterlässt keine Lücken oder Nullreferenzen, sodass ein Durchlauf durch das ganze Array exakt alle gerade geöffneten Fenster erfasst. For Each kind As Form in Me.MdiChildren ' Anweisungen Next
Die Überprüfung auf Nothing ist nicht notwendig.
14.3.2 Aktive Subfenster In der Menüleiste eines MDI-Hauptfensters findet man normalerweise immer die Menüpunkte Speichern und Speichern unter..., um die Eingaben im aktiven Subfenster zu sichern. Die Eigenschaft ActiveMdiChild des MDI-Hauptfensters speichert die Referenz auf das selektierte MDI-Subfenster. Nehmen wir an, dass das Subfenster eine Textbox enthält; mittels MultiLine=True sowie Dock=DockStyle.Fill füllt sie den gesamten Clientbereich. Mit Dim frm As Subfenster = CType(Me.ActiveMdiChild, Subfenster)
870
Menü einer MDI-Anwendung
besorgen Sie sich die Referenz auf das aktive Subfenster. Die Typumwandlung mit CType sollte eigentlich zwingend sein, da ActiveMdiChild vom Typ Form und nicht vom Typ Subfenster ist. Hinweis Der Zugriffsmodifizierer der Steuerelemente des Subfensters, auf die vom Hauptfenster aus zugegriffen wird, muss Friend oder Public sein. Im Eigenschaftsfenster können Sie die Zugriffsmodifizierer auch mit der Eigenschaft Modifiers passend einstellen.
Je nach Art des Zugriffs auf ActiveMdiChild kann es erforderlich sein zu prüfen, ob überhaupt ein Subfenster geöffnet ist. Wenn aktuell kein MDI-Subfenster geöffnet ist, hat die Eigenschaft ActiveMdiChild den Wert Nothing. Das sollten Sie unbedingt prüfen. If Me.ActiveMdiChild IsNot Nothing Then Dim frm As SubForm = CType(Me.ActiveMdiChild, SubForm) frm.textBox1.Text = "......." End If
14.3.3 Subfenster eines bestimmten Typs MDI-Anwendungen haben meistens nur einen Subfenstertyp, aber das ist nicht zwingend. Daher müssen Sie davon ausgehen, dass zur Laufzeit mehrere Typen innerhalb des Hauptfensters geöffnet sein können und dass das von MdiChildren zurückgelieferte Array mehrere Typen enthalten kann. Soll gezielt auf das Steuerelement eines untergeordneten Fensters zugegriffen werden, muss daher zuerst der Typ mit dem Is-Operator überprüft werden. For Each kind As Form in Me.MdiChildren If TypeOf kind Is Subfenster Then CType(kind,Subfenster).textBox1.Text = "......." Else If TypeOf kind Is Subfenster2 Then ' Anweisungen End If Next
14.4
Menü einer MDI-Anwendung
Nach Ihrem bisherigen Wissen über Steuerelemente würden Sie die Positionierung und Anordnung von Menü-, Symbol- und Statusleiste einem ToolStripContainer überlassen. Vertrauen Sie diesem Steuerelement aber nicht zu sehr. Denn sobald Sie diesen Container in den Arbeitsbereich eines MDI-Hauptfensters gelegt haben, verliert das Hauptfenster seine für es typischen Eigenschaften hinsichtlich der Verwaltung und Anzeige der Subfenster. Unter anderem sind sie gar nicht mehr sichtbar. Sie müssen demnach auf einen ToolStripContainer im MDI-Hauptfenster verzichten und können nicht von dessen Fähigkeiten profitieren.
871
14.4
14
MDI-Anwendungen
14.4.1 Subfenster anordnen Haben Sie im MDI-Hauptfenster mehrere Subfenster geöffnet, werden diese standardmäßig überlappend angezeigt. Das erste öffnet sich in der linken oberen Ecke des Hauptfensters, jedes weitere um jeweils einen bestimmten Betrag nach rechts unten versetzt. Die Subfenster können noch auf zwei weitere Arten automatisch angeordnet werden: entweder neben- oder untereinander. Üblicherweise wird die Wahl über das Menü Fenster des Hauptfensters bereitgestellt, siehe Abbildung 14.2.
Abbildung 14.2
Auswahlmenü zur Anordnung der MDI-Subfenster
Im Ereignishandler des Click-Ereignisses der Menüelemente wird die Methode LayoutMdi des Hauptfensters aufgerufen, der Sie als Argument einen der insgesamt vier Konstanten der Enumeration MdiLayout übergeben (siehe Tabelle 14.1). Konstante
Wert
Anordnung im MDI-Hauptfenster
Cascade
0
Subfenster werden überlappend angeordnet.
TileHorizontal
1
Subfenster werden horizontal angeordnet.
TileVertical
2
Subfenster werden vertikal angeordnet.
ArrangeIcons
3
Alle auf Icongröße reduzierten Subfenster werden am unteren Rand angeordnet.
Tabelle 14.1
Die Enumeration »MdiLayout«
Ein gemeinsamer Ereignishandler reicht aus, um alle Subfenster wunschgemäß anzuordnen. Sie prüfen dann in einer If-Anweisung den Parameter sender, um festzustellen, welches Menüelement den Aufruf des Handlers ausgelöst hat. Mit der gewonnenen Information rufen Sie LayoutMdi mit dem entsprechenden Enumerationswert auf. Private Sub Anordnung(ByVal sender As Object, ByVal e As EventArgs) _ Handles Überlappend.Click, Nebeneinander.Click, Untereinander.Click If sender Is Überlappend Then Me.LayoutMdi(MdiLayout.Cascade) ElseIf sender Is Untereinander Then Me.LayoutMdi(MdiLayout.TileHorizontal) Else Me.LayoutMdi(MdiLayout.TileVertical) End If End Sub
872
Menü einer MDI-Anwendung
Die gewünschte Anordnung spezifiziert die Hauptausrichtung der Subfenster, die für den Fall vertikaler und horizontaler Ausrichtung die gesamte Clientfläche ohne Überlappung füllen. Es sind mehr Subfenster in der gewünschten Richtung vorhanden als in der anderen. Ein »Extremfall« tritt für vier Subfenster auf, in dem je zwei Fenster neben- und untereinander stehen – die vertikale und die horizontale Anordnung sind dann identisch.
14.4.2 Subfenster mit eigenen Menüs Subfenster können genauso wie MDI-Hauptfenster eine eigene Menüleiste haben. Allerdings wird diese nicht im Sub-, sondern im Hauptfenster angezeigt. Dabei wird die Menüleiste des Hauptfensters um die Menüleiste des Subfensters ergänzt. Die Menüelemente in der kombinierten Menüleiste können unterschiedlich angeordnet werden. Standardmäßig wird die Menüleiste des Hauptfensters rechts um die Menüleiste des Subfensters ergänzt. Sie können die beiden Menüleisten aber auch beliebig kombinieren, Menüelemente ausblenden, Menüelemente im Hauptfenster durch Untermenüs des Subfensters ergänzen usw. Die Eigenschaften MergeAction und MergeIndex in der Klasse ToolStripItem kontrollieren die Kombination der beiden Menüs aus Haupt- und Subfenster. Die Einstellungen dieser Eigenschaften wirken sich nur dann aus, wenn das MenuStrip-Objekt mit AllowMerge=True ein Zusammenführen erlaubt. Das ist die Standardeinstellung und muss nur umgeschaltet werden, wenn das Zusammenführen der Menüleisten unerwünscht ist. MergeAction beschreibt, wie die beiden Menüleisten kombiniert werden. Dabei sind Einstel-
lungen möglich, die in der gleichnamigen Enumeration festgelegt sind (siehe Tabelle 14.2). Konstante
Beschreibung
Append
Das Menüelement wird an die Menüleiste im Hauptfenster angehängt.
Insert
Fügt ein Menüelement an der unter MergeIndex spezifizierten Position ein.
MatchOnly
Das Untermenü des Menüelements wird an dasjenige Untermenü im Hauptfenster angehängt, dessen Position durch MergeIndex beschrieben wird.
Remove
Das Menüelement ist in einem zusammengeführten Menü nicht enthalten.
Replace
Das unter MergeIndex angegebene Menüelement des Hauptfensters wird vollständig ersetzt.
Tabelle 14.2
Die Enumeration »MergeAction«
MergeAction.Append hängt das Menüelement direkt hinter das rechts außen stehenden
Menüelement des Hauptfensters an. Diese Standardeinstellung der Eigenschaft MergeAction der Subfenster-Menüelemente spielt die komplette Subfenster-Menüleiste in die Menüleiste des Hauptfensters ein. Mit MergeAction.Insert nehmen Sie Einfluss auf die angezeigte Reihenfolge. Sie geben in der Eigenschaft MergeIndex die Position des Menüelements in der zusammengeführten Menüleiste an. Bedenken Sie, dass alle Menüelemente von einer Auflistung des MenuStripObjekts verwaltet werden. Mit MergeAction = MergeAction.Insert MergeIndex = 1
873
14.4
14
MDI-Anwendungen
wird das Menüelement als zweites Element im zusammengeführten Menü des Hauptfensters angezeigt. Angenommen, im Hauptfenstermenü gibt es ein Menüelement Datei und im Subfenster ebenfalls. Soll das Menüelement im Subfenster das Menüelement des Hauptfensters komplett ersetzen, spezifizieren Sie zuerst mit MergeAction = MergeAction.Replace
dass ersetzt werden soll und unter MergeIndex die nullbasierte Position im Menü des Hauptfensters. Da Datei meist links außen zu finden ist und damit auch die erste Position in der ToolStripItemCollection einnimmt, ist die Position 0 meist richtig. MergeIndex = 0
Alternativ können Sie das ursprüngliche Datei-Menü im Hauptfenster durch passende Einträge des Subfensters ergänzen, um den spezifischen Anforderungen des Subfensters zu genügen. Unter MergeIndex geben Sie dazu den Index des Menüelements an, das ergänzt werden soll, und MergeAction legen Sie auf MergeAction.MatchOnly fest. Es ist übrigens nicht erforderlich, dass die zusammengeführten Menüelemente denselben Bezeichner haben.
14.5
Symbol- und Statusleiste des Subfensters
MergeAction und MergeIndex sind Eigenschaften, die in der Klasse ToolStripItem definiert
sind. Damit lassen sich alle Elemente aus dem folgenden Ausschnitt der Klassenhierarchie wie im letzten Abschnitt beschrieben zusammenführen, ersetzen oder löschen. Insbesondere gilt dies auch für Elemente der Symbol- und Statusleisten, nicht jedoch für die Leisten an sich, deren Basisklasse ToolStrip an anderer Stelle in der Vererbungshierarchie steht. ToolStripItem ÀÄÂToolStripButton ÃToolStripControlHost ³ ÀÄÂToolStripComboBox ³ ÃToolStripProgressBar ³ ÀToolStripTextBox ÃToolStripDropDownItem ³ ÀÄÂToolStripDropDownButton ³ ³ ÀÄToolStripOverflowButton ³ ÃToolStripMenuItem ³ ÀToolStripSplitButton ÃToolStripLabel ³ ÀÄToolStripStatusLabel ÀToolStripSeparator
14.6
Die Liste der geöffneten untergeordneten Fenster
Unter dem Fenster-Menü des Hauptfensters werden häufig nicht nur Menüs zum Anordnen der Subfenster angeboten, sondern wird auch eine Liste der aktuell geöffneten Subfenster
874
Beispiel einer MDI-Anwendung
angezeigt, in der das aktive MDI-Subfenster mit einem Häkchen versehen ist. Als Menütext wird dabei der Titelleistentext des jeweiligen Subfensters benutzt. Die Liste ermöglicht es dem Benutzer, ein anderes untergeordnetes Fenster zu aktivieren. Dieses Feature anzubieten, ist sehr einfach. Sie brauchen nur der Eigenschaft MdiWindowListItem des MenuStrip-Objekts anzugeben, welches Menüelement die Liste anzeigen soll. Im Eigenschaftsfenster werden dazu in der Wertespalte die Namen aller Menüelemente aufgelistet. Weiter brauchen Sie nichts einzustellen, alles andere passiert automatisch.
14.7
Beispiel einer MDI-Anwendung
Das folgende Beispiel enthält einige der vorher besprochenen Elemente. Der Code beschränkt sich auf das Wesentliche und hat neben den MDI-typischen Anweisungen keine weitere Funktionalität. Insbesondere kann über das Menü nichts gespeichert, beendet oder bearbeitet werden. Das Hauptmenü enthält zwei Menüelemente: Datei und Fenster. Datei hat ein Untermenü mit den beiden Punkten Neu und Beenden. Fenster ist nach dem Start der Anwendung deaktiviert und wird erst beim Öffnen des ersten Subfensters aktiviert. Falls zur Laufzeit alle Subfenster geschlossen werden, muss Fenster auch wieder deaktiviert werden. Den dazu erforderlichen Code führen wir bei der Auslösung des Ereignisses MdiChildActivate aus, das nicht nur bei der Aktivierung, sondern zusätzlich auch beim Schließen eines Subfensters auftritt. Im Ereignishandler wird die Eigenschaft ActiveMdiChild überprüft: Sie liefert Nothing, wenn kein Subfenster geöffnet ist. Das Schließen des MDI-Haupt- und der MDI-Subfenster erfordert die ausdrückliche Bestätigung des Anwenders innerhalb des Ereignishandlers von FormClosing des Subfensters. Wird bei einem Dialog Cancel angeklickt, verhindert das das Schließen als Ganzes. Ein zusätzlicher FormClosing-Ereignishandler für das Hauptfenster würde mit e.Cancel = True nach der Abarbeitung aller Subfenster starten. Das Subfenster hat eine Menüleiste mit den Elementen Datei und Bearbeiten, die in das Menü des Hauptfensters integriert werden. Die einzelnen Menüelemente sollen nicht an das Untermenü angehängt, sondern konventionsgemäß platziert werden, d.h. zuerst Neu, dann Speichern und Speichern unter und zum Schluss Beenden, sowie Bearbeiten zwischen den Menüs Datei und Fenster. Erreicht wird das durch die entsprechenden Einstellungen von MergeAction und MergeIndex im Eigenschaftsfenster für die einzuhängenden Menüpunkte. Das Menü Bearbeiten, das zur Menüleiste des Subfensters gehört, soll sich zwischen Datei und Fenster des Hauptfensters positionieren. Auch das wird durch die dazu erforderlichen Eigenschaftseinstellungen im Eigenschaftsfenster bewirkt. Einen Nebeneffekt werden Sie feststellen, wenn Ihr Subfenster eine Menüleiste hat: Nach dem Zusammenführen wird im Subfenster auch dann die Menüleiste angezeigt, wenn sie aufgrund des Zusammenführens leer ist. Wenn Sie das stört, legen Sie für das ToolStripMenuObjekt Visible=False fest.
875
14.7
14
MDI-Anwendungen
Abbildung 14.3
Layout des Beispiels MDI
'...\MDI\MDI\Hauptfenster.vb
Public Class Hauptfenster Private Sub Neu_Click(ByVal sender As Object, ByVal e As EventArgs) _ Handles Neu.Click Dim frm As New Subfenster(Me) frm.Show() End Sub Private Sub Anordnung(ByVal sender As Object, ByVal e As EventArgs) _ Handles Überlappend.Click, Nebeneinander.Click, Untereinander.Click If sender Is Überlappend Then Me.LayoutMdi(MdiLayout.Cascade) ElseIf sender Is Untereinander Then Me.LayoutMdi(MdiLayout.TileHorizontal) Else Me.LayoutMdi(MdiLayout.TileVertical) End If End Sub Private Sub Kind(ByVal sender As Object, ByVal e As EventArgs) _ Handles MyBase.MdiChildActivate Fenster.Enabled = ActiveMdiChild IsNot Nothing End Sub End Class
'...\MDI\MDI\Subfenster.vb
Public Class Subfenster Private Shared Anzahl As Integer
876
Beispiel einer MDI-Anwendung
Sub New(ByVal haupt As Form) InitializeComponent() If haupt Is Nothing OrElse Not haupt.IsMdiContainer Then _ Throw New ArgumentException("Kein MDI-Fenster.") Me.MdiParent = haupt Anzahl += 1 Me.Text = "Dokument " & Anzahl End Sub Private Sub Schliessen(sender As Object, e As FormClosingEventArgs) _ Handles MyBase.FormClosing If Not Inhalt.Modified Then Return Dim erg As DialogResult = MessageBox.Show( _ "Beenden?", "Ende " & Text, _ MessageBoxButtons.OKCancel, _ MessageBoxIcon.Question, MessageBoxDefaultButton.Button2) e.Cancel = erg Windows.Forms.DialogResult.OK End Sub End Class
877
14.7
Standardoberflächen stoßen manchmal an ihre Grenzen. Daher bietet .NET auch die Möglichkeit frei gestalteter Grafiken. Der Umgang mit Zeichenfläche, Stift und Pinsel wird hier erläutert.
15
Grafiken mit GDI+
In den vorhergehenden Kapiteln haben Sie gelernt, wie Sie herkömmliche Fenster basierend auf den im Namensraum System.Windows.Forms definierten Typen entwickeln. In diesem Kapitel wollen wir uns der Entwicklung von grafikfähigen Anwendungen zuwenden, die auf GDI+ basieren. Die Abkürzung GDI steht für Graphical Device Interface. Das »+«-Zeichen deutet an, dass es sich um eine Fortentwicklung des in den bisherigen Systemen üblichen GDIs handelt.
15.1
Namensräume von GDI+
Die GDI+-Bibliothek umfasst eine Reihe von Namensräumen mit Typen, die grafische Routinen für recht verschiedene Einsatzzwecke bereitstellen. Einige dieser Typen haben wir in den vorangehenden Kapiteln schon eingesetzt, zum Beispiel Color, Pen und Graphics. Die folgende Liste gibt einen Überblick über die Namensräume des .NET Frameworks, die direkt mit der Grafikprogrammierung zu tun haben. 왘
System.Drawing: Das ist der »Wurzel«-Namensraum mit Grundelementen der Grafikpro-
grammierung. Dazu gehört beispielsweise die Klasse Graphics. 왘
System.Drawing.Design: Hier finden sich Elemente, um die Entwurfszeit-Benutzeroberflä-
왘
System.Drawing.Drawing2D: Dieser Namensraum dient zur erweiterten zweidimensiona-
chen von Grafikprogrammen zu erweitern, zum Beispiel Font- und Bitmap-Editor. len Grafikprogrammierung, unter anderem auch Füllmuster und Farbverläufe. 왘
System.Drawing.Imaging: Das ist der Namensraum zur Bearbeitung von Bildern (Bitmap
und Metafile). 왘
System.Drawing.Printing: Das ist der Namensraum zur Darstellung von Bildern oder Texten auf einer Druckseite.
왘
System.Drawing.Text: Dieser Namensraum dient zur Verwaltung von Schriftarten.
Hauptsächlich werden wir uns in diesem Kapitel mit dem Namensraum System.Drawing beschäftigen. Das setzt voraus, dass auf die Bibliothek System.Drawing.dll verwiesen wird. Bei Windows-Anwendungen ist das standardmäßig der Fall.
879
15
Grafiken mit GDI+
Die Möglichkeiten, die uns GDI+ mit den vielen Klassen und deren Methoden zur Verfügung stellt, sind mit einem Wort zu beschreiben: enorm! Eine komplette Beschreibung würde recht unübersichtlich werden und den Rahmen des Buches sprengen. Daher werde ich Ihnen die wichtigsten Konzepte vorstellen.
15.2
Die Klasse Graphics
Um ein Bild zeichnen zu können, brauchen Sie als Erstes eine Zeichenfläche. Der Leinwand oder dem Papier entspricht in .NET ein Objekt vom Typ Graphics. Es hat Methoden zum Zeichnen, Messen, Transformieren und Beschneiden. Die dafür notwendigen Informationen, zum Beispiel die Auflösung des Bildschirms, bilden den sogenannten Grafikkontext. Weil die Kombination von Stiften oder Pinseln mit dem Grafikkontext das Ergebnis einer Zeichenoperation beeinflusst, ist der Grafikkontext sehr wichtig. In der Realität verhält sich ja zum Beispiel Aquarellpapier anders als eine Schiefertafel. Bei durchscheinenden Farben sind bereits gezeichnete Flächen von Bedeutung. Daher gilt: Ohne Graphics-Objekt gibt es auch keine Zeichnung. Die Zeichenoperationen auf dem Graphics-Objekt gliedern sich in zwei Gruppen: 왘
Methoden mit dem Präfix Draw erzeugen Strichzeichnungen. Sie können damit Rechtecke zeichnen, aber auch Linien, Kreise, Ellipsen, Kreis- und Ellipsenabschnitte, Polygone, Bezierkurven, Icons und Bilder sowie Texte. Typische Vertreter dieser Gruppe sind DrawRectangle, DrawLine und DrawEllipse.
왘
Methoden mit dem Präfix Fill erzeugen gefüllte Flächen geometrischer Figuren, zum Beispiel FillEllipse und FillRectangle.
Über die Zeichenoperationen hinaus beinhaltet Graphics auch viele Eigenschaften, die Informationen in Zusammenhang mit dem Grafikkontext bereitstellen.
15.2.1 Ein Graphics-Objekt besorgen Um in ein Steuerelement zeichnen zu können, brauchen Sie eine Referenz auf ein GraphicsObjekt. Das Besondere an Graphics ist, dass die Klasse keinen öffentlichen Konstruktor hat und deshalb nicht instanziiert werden kann. An die Referenz kommen Sie auf drei Wegen: 왘
Dem Ereignishandler des Paint-Ereignisses einer Komponente wird ein Objekt vom Typ PaintEventArgs übergeben. Die Eigenschaft Graphics dieses Objekts liefert das GraphicsObjekt, das auf dem jeweiligen Control basiert.
왘
Die Fabrikmethode CreateGraphics eines Steuerelements erzeugt ein Graphics-Objekt.
왘
Der Rückgabewert der statischen Methode FromHwnd der Klasse Graphics ist ein GraphicsObjekt.
880
Die Klasse Graphics
Paint-Ereignis Das Paint-Ereignis wird ausgelöst, wenn zumindest Teile eines Steuerelements neu gezeichnet werden müssen. Das ist auch der Fall, wenn das Steuerelement zum ersten Mal sichtbar wird oder wenn es durch andere Fenster verdeckt war. Deshalb werden Zeichenanweisungen vorzugsweise in Paint codiert. Paint wird aber auch dann ausgelöst, wenn sich die Größe des Formulars ändert. Standardmäßig werden dabei nur die Bereiche neu gezeichnet, die bei einer Vergrößerung hinzukommen. Grafiken, die von der Größe der Komponente abhängig sind, passen sich der Größenänderung nicht automatisch an, was zu sehr hässlichen Effekten führt. Wir werden darauf gleich noch einmal eingehen. Sehen wir uns den Ereignishandler von Paint einer Form an: Private Sub Form1_Paint(ByVal sender As Object, ByVal e As PaintEventArgs) ... End Sub
Der zweite Parameter ist ein PaintEventArgs-Objekt, das uns mit 왘
ClipRectangle
왘
Graphics
ereignisspezifische Daten zur Verfügung stellt. Graphics liefert die Referenz auf den Grafikkontext der Komponente, und ClipRectangle gibt das neu zu zeichnende Rechteck in seiner Breite und Höhe an. Der Rückgabewert ist vom Typ Rectangle. Public ReadOnly Property ClipRectangle As Rectangle
Damit sind Sie in der Lage, die Zeichenoperationen auf die Bereiche zu beschränken, die ein Neuzeichnen erforden.
Paint-Ereignis im Code auslösen Manchmal ist es erforderlich, das Neuzeichnen einer Komponente zu erzwingen. Dazu ist das programmgesteuerte Auslösen von Paint erforderlich, zum Beispiel durch die Methode Invalidate. Mit pictureBox1.Invalidate()
wird das Paint-Ereignis eines Steuerelements namens pictureBox1 ausgelöst und der gesamte Bereich neu gezeichnet. Bei aufwändigen Grafiken kann das zu lange dauern. Deshalb hat Invalidate Überladungen mit der Angabe des Bereichs, der neu gezeichnet werden muss. Eine Überladung erwartet beispielsweise die Beschreibung durch die Angabe eines Rectangle-Objekts: pictureBox1.Invalidate(New Rectangle(100, 100, _ pictureBox1.ClientSize.Width – 100, pictureBox1.ClientSize.Height – 100)
881
15.2
15
Grafiken mit GDI+
CreateGraphics aufrufen Manchmal ist es notwendig, außerhalb des Paint-Ereignisses in eine Komponente zu zeichnen. Dann muss man sich die Graphics-Referenz des entsprechenden Elements mit der parameterlosen Methode CreateGraphics besorgen. Möchten Sie, dass das Klicken auf einen Button bewirkt, ein Rechteck in die Form zu zeichnen, kann der Code wie folgt aussehen: Private Sub button1_Click(sender As Object, e As EventArgs) Dim graph As Graphics = Me.CreateGraphics() graph.DrawRectangle(New Pen(Brushes.Blue, 10), 20, 20, 100, 100) End Sub
Die Methode DrawRectangle des Graphics-Objekts beschreibt hier ein Quadrat, das in einer Strichstärke von 10 Pixeln gezeichnet wird. Der Ursprungspunkt des Quadrats (die Ecke links oben) hat je 20 Pixel Abstand vom Rand des Arbeitsbereichs der Form. Die Seitenlänge des Quadrats beträgt 100 Pixel.
Graphics-Referenz mit FromHwnd Eine Alternative zu CreateGraphics bietet die klassengebundene Methode FromHwnd der Klasse Graphics. Die Methode erwartet als Argument einen Handle auf die Komponente, in die gezeichnet werden soll. Diesen stellt die Eigenschaft Handle bereit, die in der Klasse Control definiert ist und von allen Steuerelementen geerbt wird. Angenommen, in einer Form befindet sich eine PictureBox namens pictureBox1 und wir wollen auf das Anklicken einer Schaltfläche hin in dieses Steuerelement eine Diagonale zeichnen, so können wir den Ereignishandler von Click wie folgt implementieren: Private Sub button1_Click(sender As Object, e As EventArgs) Dim gr As Graphics = Graphics.FromHwnd(pictureBox1.Handle) gr.DrawLine(New Pen(Brushes.Black), 0, 0, 100, 100) End Sub
15.2.2 Neuzeichnen mit ResizeRedraw und Invalidate Wird eine Grafik teilweise verdeckt, beispielsweise durch eine andere Form, und anschließend wieder sichtbar, wird nicht die gesamte Grafik neu gezeichnet, sondern nur der Teil, der verdeckt war. Dasselbe passiert auch, wenn eine Form vergrößert wird. Sind die grafischen Routinen abhängig von der Größe des Fensters, tritt ein Effekt auf, wie Sie ihn in Abbildung 15.1 sehen können.
Abbildung 15.1
882
Vergrößern ohne Neuzeichnen
Die Klasse Graphics
Den Abbildungen liegt der folgende Code im Paint-Ereignis der Form zugrunde. Beachten Sie, dass die Größe der Ellipsen mit den Abmessungen des Clientbereichs der Form skaliert. Private Sub Ellipsen_Paint(sender As Object, e As PaintEventArgs) Dim gr As Graphics = e.Graphics Dim w As Integer = Me.ClientSize.Width, h = Me.ClientSize.Height For i As Integer = 0 To 4 gr.DrawEllipse(New Pen(Brushes.Black), i * w \ 10, i * h \ 10, _ w – 2 * i * w \ 10, h – 2 * i * h \ 10) Next End Sub
Wird das Fenster zur Laufzeit vergrößert, wird Paint zwar mehrfach ausgelöst, aber es werden immer nur die neu hinzugekommenen Bereiche neu gezeichnet. Das Resultat sehen Sie im rechten Fenster der Abbildung – es sieht dilettantisch aus. Wenn Sie die Eigenschaft ResizeRedraw der Form auf den Wert True setzen, wird bei jeder Größenänderung der gesamte Bereich der Form neu gezeichnet. Die Eigenschaft wird nicht im Eigenschaftsfenster angezeigt. Der Konstruktor oder das Load-Ereignis der Form sind geeignet, den Wert festzulegen: Private Sub Ellipsen_Load(ByVal sender As Object, ByVal e As EventArgs) _ Handles MyBase.Load Me.ResizeRedraw = True End Sub
ResizeRedraw ist Protected definiert, und damit ist der Zugriff nur aus der Klasse Control
selbst heraus möglich oder aus einer Klasse, die davon abgeleitet ist. Bei der benutzerdefinierten Form ist das der Fall, weil diese von der Basisklasse Form abgeleitet ist, die ihrerseits die Klasse Control beerbt.
15.2.3 Zerstören grafischer Objekte (Dispose) Wenn eine Klasse die Schnittstelle IDisposable implementiert, ruft der Garbage Collector (GC) während des Bereinigungsprozesses im Destruktor automatisch die Dispose-Methode des Objekts auf. Wann der GC dies tut, ist nicht vorhersehbar. Das kann sich nachteilig auswirken, wenn die Speicherressourcen knapp werden: Das System verlangsamt sich. Insbesondere Anwendungen, die eine sehr große Anzahl grafischer Objekte erzeugen, sind davon betroffen, denn grafische Objekte verbrauchen meist sehr viele Speicherressourcen. Der GC wird aktiv, wenn sich die Ressourcen verknappen. Durch den GC verlangsamt sich dann das System in einem Ausmaß, das nicht akzeptabel ist. Ein kleines Beispiel soll das verdeutlichen. Private Sub button1_Click(sender As Object, e As EventArgs) Dim starttime As DateTime = DateTime.Now Dim bmp As Bitmap For i As Integer = 0 To 999 bmp = new Bitmap(1000, 1000) bmp.Dispose() Next
883
15.2
15
Grafiken mit GDI+
Dim difftime As TimeSpan = DateTime.Now.Subtract(starttime) Me.Text = String.Format( _ "{0}.{1} Sekunden", difftime.Seconds, difftime.Milliseconds) End Sub
In einer Schleife werden 1000 Bitmap-Objekte der Größe 1000 × 1000 Pixel erzeugt. Die Zeit für die Ausführung der Operation wird gemessen und in der Titelleiste der Form angezeigt. Die Zeitspanne, die für das Erzeugen der Bitmaps benötigt wird, schwankt in Abhängigkeit von der Hardware-Ausstattung des Systems. Auf meinem Rechner betrug sie etwa zehn Sekunden – wenn in der Schleife jedes neue Bitmap-Objekt mit Dispose sofort aufgegeben wird. Durch Auskommentieren von Dispose lief die Operation doppelt so lange.
15.2.4 Koordinatensystem Bevor wir uns mit den Zeichenmethoden der Klasse Graphics beschäftigen, müssen wir das zugrunde liegende GDI+-Koordinatensystem verstehen, auf das sich die Grafikroutinen beziehen. Das Koordinatensystem in einem Graphics-Objekt ist vordefiniert, lässt sich aber nach eigenen Wünschen modifizieren, zum Beispiel durch Umstellung der Maßeinheit oder durch die Festlegung eines anderen Koordinatenursprungs.
Maßeinheiten Die Standardmaßeinheit ist Pixel. Mit der Eigenschaft PageUnit des Graphics-Objekts können wir diese Einstellung festlegen oder den aktuellen Wert abrufen. Der Eigenschaft können wir die Konstanten aus der GraphicsUnit-Enumeration zuweisen (siehe Tabelle 15.1). Konstante
Einheit
Display
1/100 Zoll
Document
1/300 Zoll
Inch
Zoll (2,54 cm)
Millimeter
Millimeter
Pixel
Bildschirmpixel
Point
1/72 Zoll
World
Abstrakte Einheit des globalen Koordinatensystems: verboten!
Tabelle 15.1
Die Enumeration »GraphicsUnit«
Abbildung 15.2 zeigt für das folgende Codefragment die Auswirkung einer Änderung der Einheit vom Standard GraphicsUnit.Pixel in GraphicsUnit.Point anhand zweier Rechtecke, deren Werte für Abmessung und Position identisch sind. Zur besseren optischen Unterscheidung ist das erste Rechteck mit der Farbe Color.Azure gefüllt, das zweite wird nur durch die schwarzen Randlinien dargestellt. Private Sub Einheit_Paint(sender As Object, e As PaintEventArgs) e.Graphics.FillRectangle(New SolidBrush(Color.Azure), 10, 10, 100, 100) e.Graphics.PageUnit = GraphicsUnit.Point e.Graphics.DrawRectangle(New Pen(Color.Black), 10, 10, 100, 100) End Sub
884
Die Klasse Graphics
Abbildung 15.2
Einheitenwechsel
Skalierung der Grafikausgabe Die Eigenschaft PageScale des Graphics-Objekts dient dazu, die grafische Ausgabe um einen bestimmten Faktor zu skalieren. Verwenden Sie die beiden Eigenschaften PageUnit und PageScale parallel, multiplizieren sich die Faktoren. In Abbildung 15.3 können Sie die Auswirkungen von PageScale sehen. Beachten Sie, dass sich auch die Linienbreite entsprechend verändert. Private Sub Skalierung_Paint(sender As Object, e As PaintEventArgs) e.Graphics.DrawRectangle(New Pen(Color.Black), 5, 5, 50, 50) e.Graphics.PageScale = 2 e.Graphics.DrawRectangle(New Pen(Color.Black), 5, 5, 50, 50) End Sub
Abbildung 15.3
Skalierung
15.2.5 Koordinatenursprung Standardmäßig liegt der Ursprungspunkt in der linken oberen Ecke des Clientbereichs (0, 0). Die positive Richtung der x-Achse zeigt dabei nach rechts, die positive Richtung der y-Achse nach unten und unterscheidet sich damit von der mathematischen, die nach oben zeigt. Mit den XxxTransform-Methoden der Graphics-Klasse lässt sich das Koordinatensystem verändern. Mit TranslateTransform wird das Koordinatensystem um die in den beiden Parametern übergebenen Werte verschoben. Mit graph.TranslateTransform(10, –30)
sind das zehn Einheiten in positiver x-Richtung und 30 Einheiten in negativer Richtung.
885
15.2
15
Grafiken mit GDI+
Die Methode ScaleTransform skaliert das Koordinatensystem sowohl in x- als auch in y-Richtung. Im Gegensatz zur Methode PageScale werden mit ScaleTransform auch gezeichnete Schriften mitskaliert. Der an die Methode RotateTransform übergebene Winkel dreht das Koordinatensystem um den angegebenen Betrag. Die Rotationsachse ist der Ursprung. Wollen Sie das Koordinatensystem wieder an seine Ausgangsposition zurückversetzen, können Sie alle Transformationen mit ResetTransform zurücksetzen.
Beispiel: Verschiebung des Koordinatensystems Wir wollen uns nun die Verschiebung des Koordinatensystems an einem Beispielprogramm ansehen (siehe Abbildung 15.4). In einer PictureBox werden die Achsen des Koordinatensystems in Rot gezeichnet. Über zwei Schieberegler können sowohl die x- als auch die y-Achse innerhalb der Clientfläche beliebig verschoben werden. Ausgehend vom Ursprung des Koordinatensystems wird ein Rechteck mit einem schwarzen Rahmen gezeichnet, dessen Position sich bezüglich des Koordinatensystems nicht ändert. Es wird also in gleichem Maße mitverschoben.
Abbildung 15.4
Das Beispiel »Transformation«
'...\GDI\Koordinaten\Transformation.vb
Public Class Transformation Private Sub Transformation_Load(sender As Object, e As EventArgs) _ Handles MyBase.Load DeltaY.Maximum = Zeichnung.ClientSize.Width DeltaX.Maximum = Zeichnung.ClientSize.Height DeltaY.TickFrequency = DeltaY.Maximum / 10 DeltaX.TickFrequency = DeltaX.Maximum / 10 End Sub Private Sub Zeichnung_Paint(sender As Object, e As PaintEventArgs) _ Handles Zeichnung.Paint Dim g As Graphics = e.Graphics
886
Die Klasse Graphics
' neues Koordinatensystem festlegen g.TranslateTransform(DeltaX.Value, DeltaY.Value) ' x-Koordinate neu zeichnen g.DrawLine(New Pen(Color.Red), -DeltaX.Value, 0, _ Zeichnung.ClientSize.Width – DeltaX.Value, 0) ' y-Koordinate zeichnen g.DrawLine(New Pen(Color.Red), 0, -DeltaY.Value, 0, _ Zeichnung.ClientSize.Height – DeltaY.Value) ' Rechteck zeichnen g.DrawRectangle(New Pen(Color.Black), 0, 0, 100, 100) End Sub Private Sub Ursprung(ByVal sender As Object, ByVal e As EventArgs) _ Handles DeltaX.Scroll, DeltaY.Scroll Zeichnung.Invalidate() End Sub End Class
Die Grafik der PictureBox wird im Ereignis Paint erstellt. Zuerst wird mit g.TranslateTransform(DeltaX.Value, DeltaY.Value)
das Koordinatensystem an die Position verschoben, die durch die Werte der beiden Schieberegler DeltaX und DeltaY gegeben sind. Alle Zeichenoperationen des Graphics-Objekts verwenden die neue Position des Koordinatensystems. In unserem Beispiel sind das die Aufrufe der Methoden Drawline und DrawRectangle. Bei jeder Auslösung des Paint-Ereignisses wird die Arbeitsfläche des zugrunde liegenden Steuerelements neu gezeichnet. Grafische Elemente, die vorher in dem Steuerelement zu sehen waren, werden damit gleichzeitig auch verworfen. Das erleichtert uns die Programmierung ganz wesentlich, denn bei einer Verschiebung des Koordinatensystems legen wir keinen Wert mehr darauf, die Grafik außerdem auf der alten Position zu sehen. Das Scroll-Ereignis des Schiebereglers tritt auf, wenn er durch eine Maus- oder Tastaturaktion bewegt wird. Da unsere Grafikroutinen im Paint-Ereignis der PictureBox implementiert sind, müssen wir nur noch dieses Ereignis auslösen. Genau dies leistet die InvalidateMethode der PictureBox.
15.2.6 Zeichnen mit der Klasse Graphics Die Klasse Graphics ist die Basisklasse grafischer Operationen. Sie stellt Methoden bereit, mit denen unterschiedliche geometrische Figuren gezeichnet werden können (siehe Tabelle 15.2). Die meisten Methoden sind vielfach überladen. Methode
Gezeichnete Figur
DrawArc
Ellipsenbogen
DrawBezier
Durch vier Point-Strukturen definierte Bézier-Splinekurve
DrawBeziers
Bézier-Splinekurvenzug, definiert durch ein Array von Point-Strukturen; der Endpunkt eines Abschnitts ist der Anfang des nächsten.
Tabelle 15.2
Zeichenoperationen in »Graphics«
887
15.2
15
Grafiken mit GDI+
Methode
Gezeichnete Figur
DrawClosedCurve
»Gespannter« geschlossener Splinekurvenzug durch ein Point-Array
DrawCurve
»Gespannter« Splinekurvenzug durch ein Array von Point-Strukturen
DrawEllipse
Ellipse in einem umschließenden Rechteck
DrawLine
Linie zwischen zwei Punkten
DrawLines
Linienzug durch die Punkte eines Point-Arrays
DrawPie
Ellipsensegment
DrawPolygon
Kontur eines Polygons
DrawRectangle
Rechteck, definiert durch die Eckpunkte
DrawRectangles
Mehrere Rechtecke, gegeben als Rectangle-Array
DrawString
Zeichenfolge an einer bestimmten Position
FillEllipse
Farbgefüllte Ellipse in einem umschließenden Rechteck
FillClosedCurve
Farbgefüllter geschlossener Splinekurvenzug durch ein Point-Array
FillPie
Farbgefülltes Ellipsensegment
FillPolygon
Farbgefüllte Kontur eines Polygons
FillRectangle
Farbgefülltes Rechteck, definiert durch die Eckpunkte
FillRectangles
Mehrere farblich ausgefüllte Rechtecke, gegeben als Rectangle-Array
Tabelle 15.2
Zeichenoperationen in »Graphics«
15.2.7 Eine Linie Mit der vierfach überladenen Methode DrawLine zeichnen Sie eine Linie. Als Erstes haben alle Überladungen ein Argument vom Typ Pen, mit dem die Art der zu zeichnenden Linie beschrieben wird. Es wird mindestens die Farbe spezifiziert. Die Angaben der Linienbreite, der Art der Linienenden und -verbindungen und Strichelungen sind optional. Eine Linie ist durch ihren Anfangs- und Endpunkt definiert. Sie können die Koordinaten der Punkte einzeln als Integer oder Single übergeben oder als geordnete Point-Struktur. Private Sub Form1_Paint(sender As Object, e As PaintEventArgs) e.Graphics.DrawLine(New Pen(Brushes.Red, 6), 12, 20, 120, 200) End Sub
15.2.8 Mehrere Linien DrawLines zeichnet einen Linienzug. Die Methode erwartet ebenfalls die Referenz auf ein Pen-Objekt sowie zusätzlich ein Array von Point-Objekten. Die Anzahl der Array-Elemente
(= Punkte) kann ungerade sein, da der Endpunkt der einen Linie zum Anfangspunkt der nächsten Linie wird. Das Ergebnis ist eine Kette von miteinander verbundenen Linien, wobei das geometrische Gebilde nicht automatisch geschlossen wird. Private Sub Form1_Paint(sender As Object, e As PaintEventArgs) Dim points() As PointF = New PointF() {New Point(0, 0), _ New Point(25, 35), New Point(120, 50), New Point(25, 130)}
888
Die Klasse Graphics
e.Graphics.DrawLines(New Pen(Color.Black, 2), points) End Sub
In der Abbildung 15.5 sehen Sie das Ergebnis der Zeichenoperation.
Abbildung 15.5
Linienzug mit DrawLines
15.2.9 Rechtecke Die Parameterlisten der DrawRectangle-Methoden ähneln denen der DrawLine-Methode. Der erste Parameter erwartet ein Pen-Objekt, den folgenden Parametern werden zwei Punkte übergeben, die die Koordinaten der linken oberen und rechten unteren Ecke angeben. Alternativ können Sie auch eine Rectangle-Struktur übergeben. Private Sub Form1_Paint(sender As Object, e As PaintEventArgs) e.Graphics.DrawRectangle(New Pen(Brushes.Blue), 0, 0, 100, 60) End Sub
Benötigen Sie mehrere Rechtecke, die sich weder in ihrer Farbe noch der Linienart unterscheiden, bietet sich die Methode DrawRectangles an, die ein Array der Struktur Rectangle erwartet. Alle im Array enthaltenen Rechtecke werden in derselben Farbe und Strichstärke gezeichnet. Private Sub Form1_Paint(sender As Object, e As PaintEventArgs) Dim rectArr() As Rectangle = New Rectangle() {New Rectangle(0,0,20,40), _ New Rectangle(8,15,45,70), New Rectangle(15,20,80,120)} e.Graphics.DrawRectangles(New Pen(Brushes.Black), rectArr) End Sub
Der Hintergrund der mit DrawRectangle und DrawRectangles gezeichneten Rechtecke wird vom Container übernommen. Wollen Sie farblich ausgefüllte Rechtecke zeichnen, müssen Sie auf die Methoden FillRectangle bzw. FillRectangles zurückgreifen. Diesen wird im ersten Parameter kein Pen-Objekt übergeben, sondern ein Brush-Objekt. Da die Klasse Brush abstrakt ist und nicht unmittelbar verwendet werden kann, wird im Code die Referenz auf eine von Brush abgeleitete Klasse übergeben. Damit ist es auch beispielsweise möglich, anstelle einer Farbfüllung eine beliebige Schraffur zu wählen.
15.2.10 Polygone Die Methode DrawPolygon ähnelt DrawLines und kann sie oft im Programmcode ersetzen. Der Unterschied zwischen den beiden Methoden ist, dass DrawPolygon eine zusätzliche Linie zeichnet, die den letzten mit dem ersten Punkt verbindet und so die Figur schließt.
889
15.2
15
Grafiken mit GDI+
Private Sub Form1_Paint(sender As Object, e As PaintEventArgs) Dim pArr() As Point = New Point() _ {New Point(2, 7), New Point(25, 12), New Point(67, 100)} e.Graphics.DrawPolygon(New Pen(Brushes.Black, 2), pArr) End Sub
15.2.11 Ellipsen und Teile davon Gezeichnet wird eine Ellipse innerhalb eines umschließenden Rechtecks, daher gleichen sich auch die Parameterlisten von DrawRectangle und DrawEllipse. Im folgenden Codefragment wird eine Ellipse in die Form gezeichnet, deren umschließendes Rechteck seine linke obere Ecke im Ursprungspunkt des Clientbereichs der Form hat. Die Breite beträgt 200 Pixel, die Höhe 100 Pixel. Private void Form1_Paint(sender As Object, e As PaintEventArgs) e.Graphics.DrawEllipse(New Pen(Brushes.Blue, 3), 0, 0, 200, 100) End Sub
Einen Kreis- bzw. Ellipsenbogen zeichnen Sie mit DrawArc. Auch hier wird das umschließende Rechteck der Ellipse übergeben. Der vorletzte Parameter gibt den Startwinkel zwischen der x-Achse und dem Anfangspunkt des Bogens an, der letzte Parameter den Winkel des zu zeichnenden Bogens. Die Winkel werden in Grad und im Uhrzeigersinn angegeben.
15.2.12 Kurvenzüge Die Methoden DrawCurve und DrawClosedCurve zeichnen Kurven (Cardinal-Splinekurven), die durch in einem Point-Array definierte Punkte führen. DrawClosedCurve schließt den Kurvenzug. Beide Methoden sind mehrfach überladen. Mehrere Varianten erwarten einen Parameter vom Typ Single, der die »Spannung« der Kurve angibt. Der Wert 0 bedeutet unendliche Spannung an den Punkten und erzeugt einen eckigen Polygonzug. Maximal »weiche« Kurven ergibt der Wert 1. Höhere Werte »stauchen« die Kurve. Standard ist 0.5. Abbildung 15.6 zeigt die beiden Kurven, die mit dem folgenden Programmcode gezeichnet werden. Die im Point-Array definierten Punkte sind durch einen kleinen Kreis optisch hervorgehoben. Private Sub Form1_Paint(sender As Object, e As PaintEventArgs) Dim pt() As Point = New Point() {New Point(0, 4), New Point(50, 150), _ New Point(100, 70), New Point(150, 155), New Point(200, 145), _ New Point(250, 160), New Point(300, 20)} e.Graphics.DrawCurve(New Pen(Brushes.Red, 3), pt) e.Graphics.DrawCurve(New Pen(Brushes.Black), pt, 1) For Each p As Point In pt e.Graphics.DrawEllipse(New Pen(Brushes.Black, 2), p.X-2, p.Y-3, 6, 6) Next End Sub
890
Die Klasse Graphics
Abbildung 15.6
Kurven mit DrawCurve
15.2.13 Bézierkurven Mit DrawBezier verbinden Sie zwei Punkte durch eine glatte Splinekurve. Die Steigung der Kurve an den Endpunkten wird durch je eine Tangente definiert, die von den Endpunkten und je einem zusätzlich gegebenen Punkt gebildet werden. Die Kurve liegt vollständig innerhalb des von den Kontrollpunkten gebildeten Polygons. Der Zeichenmethode werden der Startpunkt, der Tangentenpunkt am Start, der Tangentenpunkt am Ende und der Endpunkt übergeben. e.Graphics.DrawBezier(New Pen(Brushes.Black), start,tangStart,tangEnde,ende)
Abbildung 15.7 zeigt zusätzlich zu dieser Bezierkurve eine dicke Linie zwischen Start- und Endpunkt. Die Bézierkurve wird durch die beiden ober- und unterhalb liegenden Stützpunkte auf den gewünschten Verlauf »gezogen«. Die Tangenten sind gepunktet dargestellt.
Abbildung 15.7
Bezierkurve
Der Abbildung liegt der folgende Programmcode zugrunde: Private Sub Form1_Paint(sender As Object, e As PaintEventArgs) Dim start As New Point(10, 100), tangStart As New Point(30, 30) Dim tangEnde As New Point(150, 150), ende As New Point(300, 100) Dim dick As New Pen(Brushes.Black, 2), dünn As New Pen(Brushes.Black, 1) ' Gerade zwischen Start- und Endpunkt e.Graphics.DrawLine(dick, start, ende)
891
15.2
15
Grafiken mit GDI+
' Bezierkurve e.Graphics.DrawBezier(dünn, start, tangStart, tangEnde, ende) ' Tangenten dick.DashStyle = Drawing2D.DashStyle.Dot e.Graphics.DrawLine(dick, start, tangStart) e.Graphics.DrawLine(dick, tangEnde, ende) ' Punkte markieren e.Graphics.DrawEllipse(dünn, start.X – 2, start.Y – 3, 6, 6) e.Graphics.DrawEllipse(dünn, tangStart.X – 2, tangStart.Y – 3, 6, 6) e.Graphics.DrawEllipse(dünn, tangEnde.X – 2, tangEnde.Y – 3, 6, 6) e.Graphics.DrawEllipse(dünn, ende.X – 2, ende.Y – 3, 6, 6) End Sub
15.3
Stifte und Farben
Ich greife hier zwei Aspekte einer Grafik auf, die nicht von der konkreten Figur abhängen.
15.3.1 Linienstile mit Pen Die Klasse Pen repräsentiert einem Stift, der Linien in einem festgelegten Format zeichnet. Ohne weitere Festlegungen zeichnet ein Pen-Objekt eine durchgehende Linie. Mit der Eigenschaft DashStyle können Sie das Linienmuster auch anders festlegen, zum Beispiel: Dim p As New Pen(Color.Red) p.DashStyle = DashStyle.Dash
Erlaubte Linienarten listet die DashStyle-Enumeration auf (siehe Tabelle 15.3). Konstante
Beschreibung
Solid
Durchgezogene Linie
Dash
Gestrichelte Linie
Dot
Gepunktete Linie
DashDot
Abwechselnd Strich-Punkt
DashDotDot
Abwechselnd Strich-Punkt-Punkt
Custom
Benutzerdefiniert
Tabelle 15.3
Die Enumeration »DashStyle«
15.3.2 Farben mit Color Bei der Festlegung von Farben haben wir bisher immer eine der insgesamt 140 vordefinierten Konstanten der Struktur Color benutzt. Wenn Ihnen diese Farbpalette nicht ausreicht, dann können Sie auf eine der vier statischen Methoden FromArgb der Color-Struktur zurückgreifen und eine von über 16 Millionen Farben selbst definieren, indem Sie den Rot-, Grün- und Blauanteil festlegen. Die zulässigen Werte liegen im Bereich von 0 bis 255. Die Farben Schwarz, Rot, Grün, Blau und Weiß erhalten Sie durch
892
Schriftdarstellung
schwarz = Color.FromArgb(0, 0, 0) : rot = Color.FromArgb(255, 0, 0) grün = Color.FromArgb(0, 255, 0) : blau = Color.FromArgb(0, 0, 255) weiß = Color.FromArgb(255, 255, 255)}
In einem optionalen ersten Parameter legen Sie die Deckkraft der Farbe fest. Er wird AlphaKanal genannt und nimmt wie die drei Farben Werte von 0 bis 255 an. Eine vollständige Transparenz erreichen Sie mit dem Wert 0 (die Farben spielen dann keine Rolle). Ein Hauch von einem Rot erhalten Sie zum Beispiel mit Color.FromArgb(50, 255, 0, 0)
Lassen Sie den Alpha-Kanal weg, ist die Farbe vollständig deckend (255).
Systemfarben ermitteln Systemfarben von Windows können durch den Benutzer frei eingestellt werden. Ihre Verwendung hat den Vorteil, dass sich eine Anwendung nahtlos in das Farbschema des Benutzers eingliedert, an das er gewöhnt ist (was insbesondere bei Problemen mit dem Farbensehen wichtig ist). Systemfarben sind nicht statisch, sondern müssen zur Laufzeit dynamisch ermittelt werden. Dazu dient die Klasse SystemColors im Namensraum System.Drawing. Die Klasse ist eine Sammlung statischer Eigenschaften vom Typ Color. Mit der Eigenschaft Window können Sie zum Beispiel die Standardhintergrundfarbe des Clientbereichs eines Fensters ermitteln. Das ist nicht die über die Eigenschaft BackColor zugewiesene Farbe, sondern eine unter Windows im Eigenschaftsfenster des Desktops festgelegte.
15.4
Schriftdarstellung
Vor Windows 3.1 wurden hauptsächlich Bitmap-Schriften verwendet. Sie waren in einer festen Größe gespeichert und konnten nicht skaliert werden. Mit der Einführung von Windows 3.1 änderte sich das durch die Einführung skalierbarer TrueType-Schriften. Ende der 90erJahre wurde das OpenType-Schriftformat vorgestellt, das eine Kombination aus TrueType und dem in PostScript verwendeten Type-1 ist. GDI+ unterstützt ausschließlich TrueType und OpenType. Sollten Sie im Konstruktor der Font-Klasse eine Schriftart angeben, die nicht diesen Formaten entspricht, wird automatisch die Standardschrift Microsoft Sans Serif verwendet.
15.4.1 Schriftart (Font und FontFamily) Das Format einer Schrift wird durch mehrere Eigenschaften festgelegt: Schriftgröße und Schrifttyp sind am offensichtlichsten. Dazu gesellen sich weitere Darstellungseigenschaften wie fett, kursiv usw. Daher hat die Klasse Font, die eine Schriftart erzeugt, viele Konstruktoren. In der folgenden Syntax sind optionale Parameter kursiv gesetzt und Alternativen durch einen senkrechten Strich voneinander getrennt. Wenn der letzte Parameter die Einheit ist, kann der Stil auch als nichtletztes Argument entfallen.
893
15.4
15
Grafiken mit GDI+
Public Sub New(prototype As Font, newStyle As FontStyle) Public Sub New(family As FontFamily|String, emSize As Single, _ style As FontStyle, unit As GraphicsUnit, _ gdiCharSet As Byte, gdiVerticalFont As Boolean)
Mit Dim f As New Font("Arial", 12)
erzeugen Sie ein neues Font-Objekt, das als Schriftart »Arial« hat und eine Schriftgröße von 12 Punkten. Wenn Sie möchten, können Sie darüber hinaus auch sofort den Stil festlegen.
15.4.2 Schriftstil mit FontStyle Der Schriftstil beschreibt, ob eine Schrift fett, kursiv, unterstrichen oder durchgestrichen dargestellt wird. Die Stile eines Font-Objekts sind schreibgeschützt. Daher müssen Sie ein neues Font-Objekt erzeugen, um einen anderen Stil zu bekommen. Geeignet ist dazu der Kopierkonstruktor, der die Referenz auf ein existierendes Font-Objekt erwartet sowie die Angabe des zu setzenden Stils, zum Beispiel: Dim f As New Font(Me.Font, FontStyle.Bold)
Erlaubte Schriftstile sind in der Enumeration FontStyle aufgelistet (siehe Tabelle 15.4). Konstante
Formatierung
Bold
Fett
Italic
Kursiv
Regular
Normal
Strikeout
Durchgestrichen
Underline
Unterstrichen
Tabelle 15.4
Die Enumeration »FontStyle«
Soll eine Schrift beispielsweise fett und kursiv sein, müssen die entsprechenden Konstanten mit dem bitweisen ODER-Operator verknüpft werden. Die folgende Anweisung erzeugt eine Schrift, die sowohl fett als auch kursiv dargestellt wird. frm.Font = New Font(frm.Font, FontStyle.Bold Or FontStyle.Italic)
15.4.3 Ausgabe einer Zeichenfolge Analog zu den Methoden zur grafischen Darstellung von Linien, Kurven und geometrischen Figuren hat die Klasse Graphics mit DrawString eine sechsfach überladene Methode, um eine Zeichenfolge grafisch auszugeben. In der folgenden Syntax sind optionale Teile kursiv gesetzt und Alternativen durch einen senkrechten Strich voneinander getrennt.
894
Schriftdarstellung
Public Sub DrawString(s As String, font As Font, brush As Brush _ , point As PointF | layoutRect As RectangleF | (x As Single,y As Single) _ , format As StringFormat)
Die ersten drei Parameter sind erforderlich und in allen Methoden gleich: Es wird zuerst die zu zeichnende Zeichenfolge angegeben, anschließend die Schriftart und dann noch ein Objekt vom Typ Brush, um dem grafischen Layout einen großen Spielraum zu eröffnen. Der vierte Parameter spezifiziert die Position und kann ein Punkt, ein umschreibendes Rechteck oder ein Koordinatenpaar sein. Dem letzten Parameter vom Typ StringFormat widme ich mich in Abschnitt 15.4.5, »Textlayout mit StringFormat«. Hinweis Die Struktur Point gibt die Punktkoordinaten als Integer an, PointF als Single.
Im folgenden Beispiel wird mit der DrawString-Methode eine Zeichenfolge in die linke obere Ecke des zweiten Quadranten (oben links) des Clientbereichs der Form gezeichnet und noch einmal in die linke obere Ecke des vierten Quadranten (rechts unten). Die Aufteilung des Fensters in Quadranten verdeutlichen eine vertikale und eine horizontale Linie. Abbildung 15.8 zeigt das Aussehen der Form.
Abbildung 15.8
Texte
'...\GDI\Figuren\Texte.vb
Public Class Texte Private Sub Texte_Paint(sender As Object, e As PaintEventArgs) _ Handles MyBase.Paint Dim str As String = "Hallo Welt" Dim ft As New Font("Arial", 20) Dim gr As Graphics = e.Graphics Dim w As Integer = ClientSize.Width, h As Integer = ClientSize.Height gr.DrawString(str, ft, Brushes.Black, 0, 0) gr.DrawLine(New Pen(Brushes.Blue), 0, h \ 2, w, h \ 2) gr.DrawLine(New Pen(Brushes.Blue), w \ 2, 0, w \ 2, h) gr.DrawString(str, ft, Brushes.Black, w / 2, h / 2) End Sub End Class
895
15.4
15
Grafiken mit GDI+
15.4.4 Abmessungen mit MeasureString Die Festlegung des Startpunkts einer Zeichenfolge ist einfach. Die Abmessungen einer Zeichenfolge im Voraus zu wissen ist aber auch wichtig. Sie wollen beispielsweise die Zeichenfolge »Hallo Welt« mittig im Clientbereich einer grafikfähigen Komponente zeichnen lassen. Dazu müssen Sie wissen, wie viel Platz die Zeichenfolge sowohl horizontal als auch vertikal für sich beansprucht. Hier hilft uns die Methode MeasureString der Klasse Graphics. Diese Methode ist wieder vielfach überladen, liefert aber in allen Fällen die Referenz auf ein SizeF-Objekt zurück. In der folgenden Syntax sind optionale Teile kursiv gesetzt. Bitte beachten Sie, dass ohne Übergabe eines StringFormat-Objekts die ermittelte Größe etwas zu groß ist, insbesondere wenn Unterschneidungen oder Ligaturen von Buchstaben auftreten. Als Formatierungsobjekt eignet sich die Rückgabe von StringFormat.GenericTypographic. Für genaue Messungen sollte außerdem graphics.TextRenderingHint = TextRenderingHint.AntiAlias gesetzt werden. Exakter passende Messungen erhalten Sie mit MeasureCharacterRanges. Public Function MeasureString([text] As String, font As Font _ , layoutArea As SizeF, stringFormat As StringFormat _ , ByRef charactersFitted As Integer, ByRef linesFilled As Integer) _ As SizeF Public Function MeasureString([text] As String, font As Font, _ origin As PointF, stringFormat As StringFormat) As SizeF Public Function MeasureString( [text] As String, font As Font, _ maxWidth As Integer, format As StringFormat) As SizeF Public Function MeasureCharacterRanges([text] As String, font As Font, _ layoutRect As RectangleF, stringFormat As StringFormat) As Region()
Die Struktur SizeF repräsentiert einen rechteckigen Bereich, dessen Abmessungen in den Eigenschaften Height und Width vom Typ Single gespeichert sind. Um einen Text zu zentrieren, müssen Sie ihn um die Hälfte der Abmessungen vom Zentrum nach links oben verschieben, da der Textursprung links oben liegt. Ein alternativer Weg führt über die Klasse StringFormat.
15.4.5 Textlayout mit StringFormat Ein Objekt vom Typ StringFormat enthält Informationen für das Textlayout, beispielsweise die Textausrichtung und den Zeilenabstand. Mit den beiden Eigenschaften Alignment und LineAlignment wird eine Zeichenfolge positioniert. Alignment beschreibt die horizontale Ausrichtung eines Textes, LineAlignment die vertikale. Beide sind vom Typ der Enumeration StringAlignment (siehe Tabelle 15.5).
896
Bilddateien
Konstante
Beschreibung
Near
Die Ausrichtung orientiert sich an der linken oberen Ecke.
Center
Die Ausrichtung ist zentriert in der Mitte.
Far
Die Ausrichtung orientiert sich an der rechten unteren Ecke.
Tabelle 15.5
Die Enumeration »StringAlignment«
Damit wird die zentrierte Ausgabe einer Zeichenfolge wie in Abbildung 15.9 einfach: Wir erzeugen ein StringFormat-Objekt und legen die beiden Eigenschaften Alignment und LineAlignment auf StringAlignment.Center fest. Anschließend übergeben wir das StringFormat-Objekt der DrawString-Methode. '...\GDI\Figuren\TextZentriert.vb
Public Class TextZentriert Private Sub TextZentriert_Paint(sender As Object, e As PaintEventArgs) _ Handles MyBase.Paint Dim ft As New Font("Arial", 20) Dim gr As Graphics = e.Graphics Dim format As New StringFormat() format.Alignment = StringAlignment.Center format.LineAlignment = StringAlignment.Center Dim w As Integer = ClientSize.Width, h As Integer = ClientSize.Height gr.DrawLine(New Pen(Brushes.Blue), 0, h \ 2, w, h \ 2) gr.DrawLine(New Pen(Brushes.Blue), w \ 2, 0, w \ 2, h) gr.DrawString("Hallo Welt", ft, Brushes.Black, w / 2, h / 2, format) End Sub End Class
Abbildung 15.9
15.5
Zentrierter Text
Bilddateien
Grafiken, Bilder und Zeichnungen werden mithilfe verschiedener Technologien dargestellt, bearbeitet und letztendlich auch gespeichert. Prinzipiell werden dabei zwei Kategorien zur Bearbeitung grafischer Elemente unterschieden: 왘
Rastergrafiken
왘
Vektorgrafiken
897
15.5
15
Grafiken mit GDI+
Rastergrafiken Oft werden Bilder oder Fotos als Rastergrafik im Bitmap-Format hinterlegt. Ein solches Bild ist eine Aneinanderreihung einzelner Bildpunkte, die durch je eine Farbe beschrieben werden. Weil eine Rastergrafik sehr simpel aufgebaut wird, ist auch ihre Erzeugung bzw. Nachbearbeitung sehr einfach. Allerdings erkauft man sich diesen Vorteil zu einem hohen Preis: 왘
Kurven sind immer stufig, weil sie sich aus einzelnen Rechtecken zusammensetzen.
왘
Eine Rastergrafik benötigt sehr viel Speicherplatz. Zur Abmilderung wurden zahlreiche Komprimierungsverfahren entwickelt, um die Datenmenge zu reduzieren. GIF für Liniengrafiken und JPEG für Fotos sind die bekanntesten Beispiele.
왘
Bei einer Skalierung (Größenänderung) kommt es zu sehr unschönen optischen Effekten. Beispielsweise werden bei einer Verkleinerung einfach Spalten oder Zeilen weggelassen.
Vektorgrafiken Bei einer Vektorgrafik werden Linien, Kurven und Texte durch wenige Koordinaten beschrieben. Verschiedene Funktionen beschreiben, wie aus Koordinaten Grafiken werden. Jede Koordinate wird durch einen Vektor beschrieben. Ihn können Sie sich als Linie vorstellen, die durch einen Anfangspunkt, eine Richtung und eine Länge beschrieben ist. Gibt es für ein Bildelement keine passende Funktion, wird es aus Einzelteilen zusammengesetzt, für die jeweils eine Funktion existiert. Die funktionale Beschreibung hat einige Vorteile. 왘
Eine Vektorgrafik kann sehr kompakt gespeichert werden. Ein Bild der Größe 300 × 500 mit einer Diagonalen benötigt als Rastergrafik 150.000 Bildpunkte, als Vektorgrafik genügen der Anfangs- und der Endpunkt ((0,0),(300,500)).
왘
Da Grafiken durch Koordinaten beschrieben werden, kann eine Vektorgrafik beliebig skaliert werden. Das Programm rechnet die Koordinaten um und zeichnet die Grafik neu.
왘
In einigen Programmen (Beispiel CAD) ist das Bearbeiten einer Vektorgrafik einfacher, denn das Programm kennt die einzelnen Elemente und hat direkten Zugriff darauf. Mit der Maus lassen sich die einzelnen Bildelemente sehr einfach greifen und bearbeiten.
Das hört sich ausgesprochen vorteilhaft an, und es stellt sich die Frage, warum Vektorgrafiken die Rastergrafiken nicht ablösen konnten. Der Grund ist, dass in vielen Bildern die einzelnen Bildelemente unkorreliert sind, zum Beispiel die Pixel eines Fotos. Es gibt keine Funktion, die mehrere Elemente beschreiben kann. So müsste jedes Pixel durch einen eigenen Vektor beschrieben werden, was sehr viel Speicherplatz braucht. Daraus folgt: 왘
Zeichenprogramme, die hauptsächlich auf Linien und Kurven basieren, speichern die Daten des Bildes als Vektordatei, die oft binäre Daten enthält.
왘
Malprogramme und Bilder werden im Bitmap-Format gespeichert.
15.5.1 Bilder und Grafiken in .NET Rastergrafiken werden als Bitmaps gespeichert, Vektorgrafiken als Metadateien. Die Klassenbibliothek stellt die Klassen Image, Bitmap, Icon und Metafile bereit, deren Klassenhierarchie in Abbildung 15.10 gezeigt ist.
898
Bilddateien
Die Klasse Image ist die abstrakte Basisklasse für die beiden Typen Bitmap und Metafile und stellt diesen Funktionen zur Bildbearbeitung zur Verfügung. Ein wenig aus dem Rahmen fällt die Klasse Icon. Grundsätzlich ist auch ein Icon-Objekt eine Bitmap, es wird aber in einem Windows-eigenen Dateiformat gespeichert und hat nur wenig Bearbeitungsmethoden. Object
MarshalByRefObject
Icon
Image
Bitmap Abbildung 15.10
Metafile
Hierarchie der Bildklassen
Der Bitmap-Klasse kommt eine deutlich höhere Bedeutung zu als den Klassen Icon und Metafile. Im Weiteren beschränke ich mich auf die Bitmap-Klasse.
15.5.2 Bitmap-Dateiformate Eine Bitmap ist die Ansammlung von Pixeln in einem Array. Höhe und Breite werden in Pixeln angegeben. Die Anzahl der darstellbaren Farben wird Farbtiefe genannt. Sie wird als Anzahl der Bits gegeben, die jedem Pixel einer Bitmap gleichermaßen zur Verfügung steht. Der Bereich reicht von 1 bis 32 Bits, die Anzahl der darstellbaren Farben ergibt sich zu:
Farbenanzahl = 2Bits pro Pixel Für RGB-Farben wird für den Rot-, Grün- und Blauanteil (die sogenannten Primärfarben) jeweils ein Byte bereitgestellt, das insgesamt 256 Farbabstufungen umfasst. Die ARGB-Farbskala hat noch ein viertes Byte, mit dem die Transparenz ebenfalls zwischen 0 (durchsichtig) und 255 (undurchsichtig) eingestellt wird. Es haben sich einige feste Bitmap-Formate etabliert. Werden die Farben eines Pixels durch acht Bit beschrieben, ergeben sich 256 Farbabstufungen. Meistens wird dieses Format zur Darstellung von Graustufen-Bitmaps benutzt. Stehen jedem Pixel drei Byte mit je acht Bit zur Farbdarstellung zur Verfügung, erhöht sich die Farbanzahl sofort deutlich auf 224 = 16.777.216. Wir kennen diese Farbskala unter dem Namen True Color. Der Nachteil herkömmlicher Bitmaps ist die Größe der Datei. Eine Bitmap mit den Abmessungen 300 × 300 Pixel und einer Farbtiefe von 24 Bit beansprucht bereits etwas über 260 KByte. Als die Rechner noch nicht so üppig mit Hauptspeicher ausgerüstet waren wie heutzutage, waren das schon Größenordnungen, die kaum noch akzeptabel waren. Heutzutage mangelt es den Maschinen zwar nicht mehr an RAM, allerdings gibt es ein anderes Nadelöhr hinsichtlich des Speichervolumens: das Web. Eine Internetseite, die mehrere Bitmaps beinhaltet, kann ein unzumutbares Ladeverhalten zeigen.
899
15.5
15
Grafiken mit GDI+
Die Tabelle 15.6 beschreibt kurz verschiedene Dateiformate für Bitmaps. Dateiformat
Beschreibung
BMP
BMP ist das Standardformat von Windows für Bitmap-Dateien. Die Farbtiefe ist auf 24 Bit beschränkt, und es wird unkomprimiert gespeichert.
GIF
Das Graphics Interchange Format hat maximal 256 Farben, aber ein effizientes Komprimierungsverfahren. Es eignet sich für Bilder mit transparenten Bereichen sowie für im Internet oft anzutreffende Animationen.
JPEG
JPEG (Joint Photographic Experts Group) ist ein international standardisiertes Komprimierungsverfahren, das für Fotos entwickelt wurde. Das Format ist auch als JFIF (JPEG File Interchange Format) bekannt geworden.
PNG
PNG (Portable Network Graphics) ist ein 1995 als Konkurrenz zu GIF entwickelter und von den meisten Browsern neben GIF und JPEG unterstützter Bildstandard. PNG bietet hohe Kompressionsraten, Halbtransparenz und Echtfarben.
TIFF
TIFF (Tagged Image File Format) dient dem Austausch von Dateien zwischen Programmen und Plattformen. TIFF ist ein flexibles Bitmap-Format, das von fast allen Mal- und Bildbearbeitungsprogrammen unterstützt wird.
Tabelle 15.6
Dateiformate für Bitmaps
15.5.3 Bilder vom Typ Image Ein Bild laden Image ist die abstrakte Basisklasse der beiden abgeleiteten Klassen Bitmap und Metafile. Daher gibt es keinen Konstruktor in der Klasse Image, um daraus ein Objekt zu erzeugen. Mehrere klassengebundene Methoden in Image liefern die Referenz auf ein Bild. Die am häufigsten benutzte Methode erstellt ein Image aus einer Datei, deren Zugriffspfad als Zeichenkette übergeben wird. Wir wollen sie im folgenden Beispielprogramm benutzen und eine JPEG-Datei in einer Form anzeigen, die dem Projekt so, wie in Abbildung 15.11 zu sehen ist, hinzugefügt wird. Gegebenenfalls müssen Sie mit der Symbolleistenschaltfläche im Projektmappen-Explorer alle Dateien anzeigen lassen.
Abbildung 15.11
Bilddatei zum Projekt hinzufügen
Im Eigenschaftsfenster sorgen wir dafür, dass die Datei mit im Ausgabeverzeichnis landet (siehe Abbildung 15.12).
900
Bilddateien
Abbildung 15.12
Bilddatei zum Programm hinzufügen
Der Programmcode ist kurz: '...\GDI\Bilddateien\Laden.vb
Public Class Laden Private Sub Laden_Paint(sender As Object, ByVal e As PaintEventArgs) _ Handles MyBase.Paint Try Dim strFile As String = IO.Path.Combine("Bilder", "Egypt.jpg") Dim img As Image = Image.FromFile(strFile) e.Graphics.DrawImage(img, 10, 20) Catch ex As Exception MessageBox.Show("Ladefehler: " & ex.Message, "Fehler") End Try End Sub End Class
Eine Referenz auf das Bild gibt uns die an die Klasse Image gebundene Methode FromFile, der wir den Pfad zur Bilddatei übergeben. Durch den Einschluss der Bilddatei in das Projektausgabeverzeichnis können wir die Angabe relativ zur ausführbaren Datei machen. Die Anzeige übernimmt die Methode DrawImage des Graphics-Objekts der Form. Hier wird das unveränderte Bild mit seiner linken oberen Ecke an der Stelle (10,20) gezeichnet. Wird das Bild nicht gefunden, zum Beispiel weil nur die ausführbare Datei vorliegt, wird im Catch-Zweig eine Fehlermeldung produziert.
Bildanzeige mit DrawImage DrawImage bietet 30 verschiedene Arten, ein Bild zu zeichnen. Bilder können gestreckt, gestaucht oder gedreht angezeigt werden. In der folgenden Syntaxübersicht sind optionale Teile kursiv gesetzt. Public Public Public Public
Sub Sub Sub Sub
DrawImage(bild As DrawImage(bild As DrawImage(bild As DrawImage(,
Image, ) Image, ) Image, ) , , , , )
: linke obere Ecke als Point/PointF oder 2 Integer/Single : Ort und Größe als Rectangle/RectangleF oder 4 Integer/Single
901
15.5
15
Grafiken mit GDI+
: Einpassung in Parallelogramm, gegeben als Point()/PointF() : Ort, Bereich oder Scherung : Bildausschnitt (Zahlenart wie Ziel) und Einheit als GraphicsUnit 4 Punkte->Rectangle, Rechteck->2 Punkte/Rechteck/Parallelogramm : ImageAttributes (wie Quelle außer 2 Punkte/Rechteck) : Callback Prüffunktion vom Typ DrawImageAbort (Ladekontrolle) : Daten für Callback als Integer
Zwei weitere Beispiele sollen die Flexibilität der Zeichenmethode DrawImage demonstrieren. Für das erste brauchen Sie nur die Zeile des Methodenaufrufs von DrawImage im Beispiel oben gegen die folgende Anweisung auszutauschen: e.Graphics.DrawImage(img, New Rectangle(0, 0, 250, 150))
Der zweite Methodenparameter ist ein Rectangle-Objekt, das die Lage und die Abmessungen des Bereichs beschreibt, in dem das Bild angezeigt werden soll. Die daraus resultierende Bildausgabe sehen Sie in Abbildung 15.13 – das Bild wird in seiner Breite gestreckt.
Abbildung 15.13
Ausgabe eines gestreckten Bildes
Tauschen wir noch einmal den DrawImage-Aufruf gegen einen anderen aus, und übergeben wir ein Point-Array. Dieser Übergabe liegt die folgende überladene Methode zugrunde: Public Sub DrawImage(image As Image, destPoints As Point())
In diesem Point-Array sind drei Punkte definiert: Der erste gibt die obere linke Ecke des Bildes an, der zweite die obere rechte Ecke und der dritte die untere linke Ecke. Der vierte Punkt wird automatisch so ermittelt, dass das Ergebnis ein Parallelogramm bildet, wie in Abbildung 15.14 zu sehen. e.Graphics.DrawImage(img, New Point() _ {New Point(80, 0), New Point(300, 80), New Point(30, 100)})
Zeichnen auf Bildern Bilder sind bezüglich nachfolgender Zeichenoperationen nichts Besonderes. Im folgenden Beispiel zeichnen wir mit DrawImage ein Bild und schreiben mit DrawString an der Stelle (5,5) einen gelben Text darauf (siehe Abbildung 15.15). Um den Code kurz zu halten, hat er keinen Try-Block. 902
Bilddateien
Abbildung 15.14
Ausgabe eines gedrehten und gestreckten Bildes
'...\GDI\Bilddateien\Beschriftung.vb
Public Class Beschriftung Private Sub Beschriftung_Paint(sender As Object, e As PaintEventArgs) _ Handles MyBase.Paint Dim strFile As String = IO.Path.Combine("Bilder", "Egypt.jpg") Dim img As Image = Image.FromFile(strFile) e.Graphics.DrawImage(img, New Rectangle(0, 0, 257, 172)) Dim strText As String = "Ägyptischer Restaurateur" Dim ft As New Font("Arial", 12, FontStyle.Bold Or FontStyle.Underline) e.Graphics.DrawString(strText, ft, Brushes.Yellow, New Point(5, 5)) End Sub End Class
Abbildung 15.15
Bildausgabe mit Beschriftung
Eigenschaften der Klasse Image Als abstrakte Basisklasse stellt Image den beiden abgeleiteten Klassen Metafile und Bitmap die in Tabelle 15.7 gezeigten Eigenschaften zur Verfügung, die Informationen über das ImageObjekt liefern.
903
15.5
15
Grafiken mit GDI+
Eigenschaft
Beschreibung
Flags
Details wie Skalierbarkeit und Farbraum
R
FrameDimensionsList
IDs im Bild enthaltener Frames und Auflösungen
R
Height
Höhe des Bildes in Pixel
R
HorizontalResolution
Horizontale Auflösung des Bildes in DPI (Dots per Inch)
R
Palette
Farbpalette des Bildes (ARGB Farben)
PhysicalDimension
Größe des Bildes als SizeF-Objekt. Einheiten: Bitmaps in Pixel, Metafiles in 1/100 mm
R
PixelFormat
Gibt an, wie ein Pixelwert auf eine Farbe abgebildet wird.
R
PropertyIdList
IDs der Metainformationen
R
PropertyItems
Metainformationen zum Bild
R
RawFormat
Bildformat des Bildes als ImageFormat
R
Size
Bildgröße in Pixel
R
Tag
Benutzerdefinierte Zusatzdaten
VerticalResolution
Vertikale Auflösung des Bildes in DPI (Dots per Inch)
R
Width
Breite des Bildes in Pixel
R
Tabelle 15.7
Eigenschaften von »Image«
15.5.4 Bitmaps Die abstrakte Klasse Image ist im Gegensatz zur beerbenden Klasse Bitmap nicht instanziierbar. Die Konstruktoren zerfallen in zwei Gruppen. Entweder übernehmen Sie ein Bild, das als Datenfeld, Image, Stream oder Dateipfad gegeben ist. Sie können es skalieren oder Farben korrigieren. Die zweite Gruppe erstellt eine leere Bitmap mit optionaler Auflösung (gegeben als Graphics) oder optionaler Farbcodierung (gegeben als PixelFormat). Am einfachsten lässt sich eine Bitmap mit dem Konstruktor erzeugen, der die Breite und Höhe in Pixel angibt: Dim bmp As New Bitmap(300, 400)
Die erzeugte Bitmap ist 300 Pixel breit und 400 Pixel hoch. Es handelt sich um eine ARGBBitmap (A = Alpha-Kanal, R = Rot, G = Grün, B = Blau), deren Werte mit 0 initialisiert werden. Da der A-Kanal zur Darstellung der Transparenz den Startwert 0 hat, bedeutet das, dass eine Bitmap (zunächst) durchsichtig ist.
»Malen« mit SetPixel Die Methode SetPixel ändert den Zustand eines einzelnen Pixels in der Bitmap: bmp.SetPixel(10, 10, Color.Blue)
Die ersten beiden Parameter geben die Koordinaten eines Pixels an, das mit der Farbe gezeichnet wird, die im dritten Parameter genannt ist. Mit der DrawImage-Methode des GraphicsObjekts kann die Bitmap auf einem beliebigen Steuerelement angezeigt werden, da Control die Methode OnPaint bereitstellt. Das folgende Beispiel zeichnet einen zweidimensionalen Farbverlauf.
904
Bilddateien
Private Sub SetPixel_Paint(sender As Object, ByVal e As PaintEventArgs) Dim bmp As New Bitmap(255, 255) For i As Integer = 0 To 254 For j As Integer = 0 To 254 bmp.SetPixel(j, i, Color.FromArgb(i, 0, j)) Next j, i e.Graphics.DrawImage(bmp, 0, 0) End Sub
Die Bitmap hat eine Höhe und Breite von jeweils 255 Pixeln. In zwei For-Schleifen wird für jedes Pixel in der Bitmap eine neue Farbe festgelegt. Die äußere Schleife durchläuft dabei jede Pixelzeile, die innere jede Pixelspalte. Die Farbe des Rot- und Blauanteils wird aus dem jeweiligen Zählerstand mit der statischen FromArgb-Methode der Color-Klasse ermittelt. Die Farbe eines Pixels liefert das Pendant der SetPixel-Methode: GetPixel. Sie müssen wieder die Koordinaten des Pixels angeben und erhalten die Farbe als Color-Struktur.
Speichern von Bitmaps Ebenso einfach wie das Laden ist das Speichern einer Datei. Die Klasse Image bietet dazu die überladene Methode Save an, die von der abgeleiteten Klasse Bitmap geerbt wird. Die einparametrige Version, die nur die Angabe des Speicherpfades als Zeichenfolge entgegennimmt, erzeugt ein PNG-Format. Allerdings sollten Sie beim Speichern immer das Bildformat angeben, zum Beispiel mit folgender Überladung: Public Sub Save(filename As String, format As ImageFormat)
Anstatt an eine Datei können Sie die Bitmap auch an ein Stream-Objekt übergeben. Ein Objekt für den Formatparameter vom Typ System.Drawing.Imaging.ImageFormat liefern die zehn in Tabelle 15.8 aufgelisteten klassengebundenen Eigenschaften dieser Klasse. Alle sind schreibgeschützt. Eigenschaft
Beschreibung
Bmp
Bitmap-Bildformat (BMP)
Emf
Windows-Bildformat Erweiterte Metadatei (Enhanced Meta File – EMF)
Exif
Exif-Format (Exchangeable Image File)
Gif
GIF-Bildformat (Graphics Interchange Format)
Icon
Bildformat für Windows-Symbole
Jpeg
JPEG-Format (Joint Photographic Experts Group)
MemoryBmp
Bitmap-Bildformat im Speicher
Png
PNG-Bildformat (W3C Portable Network Graphics)
Tiff
TIFF-Bildformat (Tagged Image File Format)
Wmf
WMF-Bildformat (Windows Metafile)
Tabelle 15.8
Statische Eigenschaften der Klasse »ImageFormat«
905
15.5
15
Grafiken mit GDI+
Zum Speichern geben Sie den Pfad zur Datei an. Die Dateierweiterung sollte mit der Angabe des Bildformats übereinstimmen. bmp.Save("C:\MyBitmap.jpg", ImageFormat.Jpeg)
Die Bitmap wird vor dem Speichern in das angegebene Format konvertiert. Wenn Sie die Bitmap beispielsweise als GIF-Datei speichern, verringert sich die Anzahl der Farben auf 256, was zu einem Verlust der Darstellungsqualität führen kann.
Ein einfaches Malprogramm Im folgenden Beispielprogramm kann mit der Maus eine Grafik auf die Clientfläche der Form gezeichnet werden (siehe Abbildung 15.16). Dazu wird die linke Maustaste gedrückt und gleichzeitig gezogen. Die folgende Abbildung zeigt eine damit erstellte Zeichnung.
Abbildung 15.16
Einfaches Malprogramm
Zeichnungen auf dem Bildschirm sind sehr flüchtig. Kaum wird ein Fenster(bereich) verdeckt, sind sie weg. Kommt das Fenster wieder zum Vorschein, muss die Zeichnung wieder restauriert werden. Unter allen gängigen grafischen Oberflächen wird dazu der sichtbar werdende Bereich neu gezeichnet. Eine besonders einfache Wiederherstellung ist möglich, wenn die Pixelinformation des Fensterbereichs zuvor gespeichert wurde. Anstatt bei jeder Änderung den Bildschirmbereich des Fensters abzuspeichern, gehen wir hier den umgekehrten Weg. Wir zeichnen auf eine Bitmap, die wir bei Änderungen und der Wiederherstellung zur Anzeige bringen. Das folgende Codefragment zeigt den Initialisierungsteil des Beispiels. Die wesentlichen Teile sind die Erzeugung einer Bitmap zur Speicherung und die Erzeugung eines Graphics-Objekts, das auf die Bitmap zeichnen kann. Die Dialoge werden später zum Laden und Speichern verwendet. '...\GDI\Bilddateien\Malen.vb
Public Class Malen Private grafik As Bitmap, blatt As Graphics Private dö As New OpenFileDialog(), ds As New SaveFileDialog() Private Sub Laden(sender As Object, e As EventArgs) Handles MyBase.Load grafik = New Bitmap(ClientSize.Width, ClientSize.Height) blatt = Graphics.FromImage(grafik)
906
Bilddateien
Dim exe As String = Application.ExecutablePath dö.InitialDirectory = IO.Directory.GetParent(exe).FullName ds.InitialDirectory = IO.Directory.GetParent(exe).FullName ds.DefaultExt = "bmp" : ds.AddExtension = True End Sub ... End Class
Die eigentliche Zeichnerei besteht aus zwei Teilen. Durch einen Mausklick wird ein Linienzug begonnen. Im Ereignishandler wird der Linienbeginn gespeichert. Bei jeder Mausbewegung bei gedrückter Maustaste wird der Linienzug um ein Stück verlängert. Dazu wird zwischen dem bisherigen Ende des Linienzugs, der in start gespeichert ist, und der in ende gespeicherten Mausposition mit DrawLine eine Linie in das Graphics-Objekt blatt gezeichnet, das die Linie an die darunterliegende Bitmap grafik weiterreicht (noch erscheint die Linie nicht auf dem Bildschirm). Die linke Maustaste zeichnet in Gelb, die rechte löscht, indem sie in der Hintergrundfarbe Schwarz zeichnet. Das neu gezeichnete Element des Linienzugs liegt innerhalb des in quelle gespeicherten Rechtecks und wird mit DrawImage auf das Graphics-Objekt g des Formulars gezeichnet und erscheint damit auf dem Bildschirm. Da das Ereignis MouseMove nicht primär zum Zeichnen gedacht ist, stellt es keinen Grafikkontext zur Verfügung. Wir beschaffen ihn uns mit der Methode CreateGraphics(). Es reicht, den Bereich von quelle zu zeichnen, da nur er neu ist. Alternativ zu DrawImage können Sie Inavidate(quelle) aufrufen (CreateGraphics() ist dann überflüssig). Schließlich wird die aktuelle Mausposition durch start=ende zum neuen Ende des Linienzugs. Der Using-Block gibt automatisch Ressourcen des durch CreateGraphics() erzeugten Objekts durch den impliziten Aufruf von Dispose() frei. '...\GDI\Bilddateien\Malen.vb
Public Class Malen ... Private start As Point Private Sub Anfang(sender As Object, ByVal e As MouseEventArgs) _ Handles MyBase.MouseDown start = New Point(e.X, e.Y) End Sub Private Sub Zeichnen(sender As Object, e As MouseEventArgs) _ Handles MyBase.MouseMove If e.Button = Windows.Forms.MouseButtons.None Then Return 'schneller Dim ende As New Point(e.X, e.Y) Using g As Graphics = Me.CreateGraphics() Dim w As Integer If e.Button = MouseButtons.Left Then w = 2 : blatt.DrawLine(New Pen(Brushes.Yellow, 2), start, ende) ElseIf e.Button = MouseButtons.Right Then w = 8 : blatt.DrawLine(New Pen(Brushes.Black, 8), start, ende) End If Dim quelle As New Rectangle( _
907
15.5
15
Grafiken mit GDI+
Math.Min(start.X, ende.X) – w\2, Math.Min(start.Y, ende.Y) – w\2, _ Math.Abs(start.X – ende.X) + w, Math.Abs(start.Y – ende.Y) + w) g.DrawImage(grafik, quelle.Left, quelle.Top, quelle, GraphicsUnit.Pixel) start = ende End Using End Sub ... End Class
Die Wiederherstellung eines verdeckten Bereichs im Ereignishandler von Paint ist schon fast trivial. Der wieder sichtbare Bereich wird aus der Bitmap grafik restauriert. '...\GDI\Bilddateien\Malen.vb
Public Class Malen ... Private Sub Auffrischen(sender As Object, ByVal e As PaintEventArgs) _ Handles MyBase.Paint Dim r As Rectangle = e.ClipRectangle e.Graphics.DrawImage(grafik, r.Left, r.Top, r, GraphicsUnit.Pixel) End Sub ... End Class
Zur Abrundung hat das Beispiel noch drei Menüpunkte zum Neustart des Zeichnens, zum Öffnen einer auf Platte gespeicherten Bitmap und zum Speichern von Zeichnungen. Analog zum Konstruktor werden für eine neue Zeichnung die Zeichenfläche und das zugehörige GraphicsObjekt erstellt. Der Aufruf von Invalidate() erzwingt ein Neuzeichnen. Eine zu öffnende Bitmapdatei wird mit OpenFileDialog.ShowDialog() ermittelt und in ein Bitmap-Objekt geladen. Die einfachste Konstruktorüberladung von Bitmap Public Sub New(filename As String)
ist leider ungeeignet, da sie die Bitmap zwar lädt, aber gleichzeitig geöffnet lässt. Dadurch ist sie gesperrt und kann nicht mehr unter dem gleichen Namen im selben Verzeichnis gespeichert werden. Der Umweg über einen expliziten Stream umgeht das Problem. Die Anpassung der Auflösung ist zum Beispiel beim Öffnen von JPEG-Dateien nötig. Die Auswahl einer Datei durch einen Doppelklick im Öffnen-Dialog erzeugt implizit ein MouseMove-Ereignis im Formular, wenn sich die Mausposition innerhalb des Formulars befindet (auch wenn dieses durch den Dialog verdeckt war). Um dann keine Linie zu zeichnen, wird der Beginn des Linienzugs auf die aktuelle Mausposition gesetzt. Im Ereignishandler von MouseMove führt dies zu einem Liniensegment der Länge null. Die Speicherung ist deutlich einfacher. SaveFileDialog.ShowDialog() ermittelt den Dateinamen, und den Rest übernimmt die Methode Bitmap.Save().
908
Bilddateien
'...\GDI\Bilddateien\Malen.vb
Public Class Malen ... Private Sub Löschen(sender As Object, e As EventArgs) Handles Neu.Click grafik = New Bitmap(ClientSize.Width, ClientSize.Height) blatt = Graphics.FromImage(grafik) Me.Invalidate() End Sub Private Sub Bild(sender As Object, e As EventArgs) Handles Öffnen.Click If dö.ShowDialog() = DialogResult.OK Then Dim st As New IO.FileStream(dö.FileName, IO.FileMode.Open) grafik = New Bitmap(st) st.Close() grafik.SetResolution(blatt.DpiX, blatt.DpiY) blatt = Graphics.FromImage(grafik) start = Control.MousePosition Me.Invalidate() End If End Sub Private Sub Sichern(sender As Object, e As EventArgs) _ Handles Speichern.Click If ds.ShowDialog() = DialogResult.OK Then _ grafik.Save(ds.FileName, Imaging.ImageFormat.Bmp) End Sub End Class
Das Beispiel ist bewusst einfach gehalten und kann Ihnen als Startpunkt eigener Entwicklungen dienen.
909
15.5
Das papierlose Büro ist auf absehbare Zeit eine Illusion. Daher zeigt dieses Kapitel, wie Sie Grafiken und Text zu Papier bringen. Die millimetergenaue Positionierung und Größe sind ebenso Thema wie mehrseitige Dokumente und eine Druckvorschau.
16
Drucken
Das Drucken gehörte noch nie zu den einfacheren Aufgaben eines Entwicklers. Das hat sich auch mit der Einführung von .NET im Jahr 2002 nicht geändert, obwohl vieles einfacher geworden ist. Die Problematik ist dabei nicht so sehr in der komplexen Programmierung zu suchen, sondern vielmehr in den verhältnismäßig stark miteinander verflochtenen, voneinander abhängigen Klassen.
16.1
Überblick
Drei Klassen im Namensraum System.Drawing.Printing sind in Zusammenhang mit dem Drucken besonders wichtig: 왘
PrintDocument
왘
PageSettings
왘
PrinterSettings
16.1.1 PrintDocument Sie kennen es aus der täglichen Praxis: Sie klicken im Menü auf Drucken, und das aktuelle Dokument wird auf dem Drucker ausgegeben. Der Drucker hat einen Druckauftrag, man spricht auch von einem Druckjob. Dieser ist das Kernelement, um das sich alles dreht. Ein Druckjob wird unter .NET von der Klasse PrintDocument beschrieben. Im einfachsten Fall reichen zwei Zeilen Programmcode aus, um den Drucker in Aktion zu setzen.
16.1.2 PrinterSettings Die Ausführung eines Druckjobs setzt einen Drucker voraus. Jeder Drucker verfügt über unterschiedliche Eigenschaften. Sie werden mit der Klasse PrinterSettings beschrieben. Sie müssen sich überhaupt nicht um ein Objekt dieser Klasse bemühen, denn das den Druckauftrag beschreibende PrintDocument-Objekt stellt eine Referenz darauf bereit.
911
16
Drucken
16.1.3 PageSettings Jeder Druckauftrag kann anders aussehen: Jede Seite kann unterschiedlich formatiert werden, beispielsweise der Seitenrand. Dafür ist ein PageSettings-Objekt zuständig. Auch hier gilt: Ein PageSettings-Objekt wird vom Druckjob (dem PrintDocument-Objekt) bereitgestellt, wir brauchen diese Klasse demnach nicht zu instanziieren.
16.2
Einen Druckauftrag erteilen
16.2.1 Methoden und Eigenschaften von PrintDocument Ein Druckauftrag wird durch ein Objekt vom Typ PrintDocument beschrieben. Wenn Sie mit dem Windows Forms-Designer arbeiten, können Sie das gleichnamige Steuerelement in die Form ziehen (und finden es danach im Komponentenfach wieder). Sie können die Klasse nur mit einem parameterlosen Konstruktor instanziieren: Dim printDoc As New PrintDocument()
Den Druckauftrag schicken Sie an den Drucker, indem Sie die parameterlose Print-Methode des PrintDocument-Objekts aufrufen: printDoc.Print()
Ist ein Drucker am Rechner angeschlossen, wird daraufhin der Druckjob gestartet und kurz ein Meldungsfenster angezeigt, das den eingeleiteten Druckvorgang und die Anzahl der zu druckenden Seiten ausgibt. Ohne weitere Angaben wird der Standarddrucker angesteuert. Stehen mehrere Drucker zur Auswahl, kann über die Eigenschaft PrinterSettings ein anderer Drucker ausgewählt werden. Eine Alternative dazu bietet das PrintDialog-Steuerelement. Bevor der Druckauftrag mit der Print-Methode abgeschickt wird, sollte dem Druckjob mittels der Eigenschaft DocumentName des PrintDocument-Objekts eine passende Bezeichnung gegeben werden. Standardmäßig lautet diese document. Der Name wird unter anderem in der Druckerwarteschlange verwendet. Eigenschaften und Methoden von PrintDocument finden Sie in der Tabelle 16.1. Methode/Eigenschaft
Beschreibung
Print
Startet den Druckauftrag (Methode).
DefaultPageSettings
Standardseiteneinstellungen für alle zu druckenden Seiten
DocumentName
Zeichenfolge zur Identifizierung des Druckauftrags
OriginAtMargins
Gibt an, ob das ein einer Seite zugeordnete Grafikobjekt innerhalb der vom Benutzer angegebenen Seitenränder oder in der linken oberen Ecke des Druckbereichs der Seite positioniert ist.
PrintController
Druckercontroller, der den Druckvorgang steuert
PrinterSettings
Einstellungen des Druckers, auf dem das Dokument gedruckt wird
Tabelle 16.1
912
Eigenschaften und Methoden von »PrintDocument«
Einen Druckauftrag erteilen
16.2.2 Die Ereignisse in PrintDocument Die beiden Codezeilen Dim printDoc As New PrintDocument() printDoc.Print()
drucken zwar ein Blatt Papier aus, aber es ist leer. Woher soll der Drucker auch wissen, was er zu Papier bringen soll? Die Steuerung des Dokumentenausdrucks erfolgt über vier Ereignisse des PrintDocument-Objekts: 왘
BeginPrint tritt direkt nach dem Aufruf der Print-Methode auf.
왘
QueryPageSettings ist der richtige Platz zur Änderung des Layouts folgender Seiten.
왘
PrintPage wird für jede zu druckende Seite nach QueryPageSettings ausgelöst. Hier werden die Dateninformationen übergeben, die der Drucker ausgeben soll.
왘
EndPrint tritt nach dem Beenden des Druckens auf oder wenn während des Druckvor-
gangs eine Ausnahme ausgelöst wird.
16.2.3 Die Ereignisse BeginPrint und EndPrint BeginPrint eignet sich für seitenübergreifende Initialisierungen zum Beispiel von Schriftarten, und EndPrint sollte nötige Aufräumarbeiten durchführen, zum Beispiel Schriftarten wieder freigeben. Die Ereignisse beider Ereignishandler haben als zweiten Parameter ein Objekt vom Typ PrintEventArgs, das in seinen beiden Eigenschaften Cancel und PrintAction den Abbruch der Druckoperation ermöglicht (Cancel=True) bzw. das Druckziel angibt (siehe Tabelle 16.2).
Eigenschaft
Beschreibung
Cancel
Ermöglicht den Abbruch der Operation.
PrintAction
Beschreibt das Ziel des Druckvorgangs. Sie ist vom Typ PrintAction.
Tabelle 16.2
Eigenschaften von »PrintEventArgs«
PrintAction ist vom Typ der in Tabelle 16.3 gezeigten gleichnamigen Enumeration.
Konstante
Wohin wird gedruckt?
PrintToFile
Datei
PrintToPreview
Druckvorschau
PrintToPrinter
Drucker
Tabelle 16.3 Die Enumeration »PrintAction«
16.2.4 Das Ereignis QueryPageSettings Das zweite Argument des Ereignishandlers ist vom Typ QueryPageSettingsEventArgs. Es hat drei Eigenschaften: Cancel zum Abbruch des Druckvorgangs, PrintAction zur Beschreibung des Ausgabeziels sowie PageSettings. Letztere stellt die Referenz auf ein gleichnamiges
913
16.2
16
Drucken
Objekt bereit, mit dem die Einstellungen der zu druckenden Seite festgelegt werden. Das in Tabelle 16.4 gezeigte QueryPageSettingsEventArgs spielt nur dann eine Rolle, wenn zu druckende Seiten mit unterschiedlichen Seiteneinstellungen ausgedruckt werden sollen. Eigenschaft
Beschreibung
Cancel
Ermöglicht den Abbruch der Operation.
PageSettings
Beschreibt die Seiteneinstellung der aktuellen Seite.
PrintAction
Beschreibt das Ziel des Druckvorgangs. Sie ist vom Typ PrintAction.
Tabelle 16.4
Eigenschaften von »QueryPageSettingsEventArgs«
16.2.5 Das Ereignis PrintPage PagePrint ist das wichtigste Ereignis eines Druckjobs, denn in diesem Ereignis wird festgelegt, was der Drucker zu Papier bringen soll. Der Ereignishandler erhält vom Ereignis ein Objekt vom Typ PrintPageEventArgs, in dem die sechs in Tabelle 16.5 gezeigten Eigenschaften definiert sind, die für den Ausdruck von wesentlicher Bedeutung sind:
Eigenschaft
Beschreibung
Cancel
Gibt an, ob der Druckauftrag abzubrechen ist.
Graphics
Kontext zum Zeichnen der Seite
HasMorePages
Gibt an, ob noch mehr Seiten gedruckt werden sollen.
MarginBounds
Rechteckiger Bereich der Seite nach Abzug der Seitenränder
R
PageBounds
Rechteckiger Bereich, der die Gesamtfläche der Seite darstellt
R
PageSettings
Seiteneinstellung der aktuellen Seite
R
Tabelle 16.5
R
Eigenschaften von »PrintPageEventArgs« (R = ReadOnly)
Für jede zu druckende Seite werden die Ereignisse QueryPageSettings und PrintPage ausgelöst. Die Eigenschaft PageSettings kann in QueryPageSettings geändert werden und ist in PrintPage schreibgeschützt.
Drucken auf ein Graphics-Objekt Der Aufruf der Print-Methode setzt die Ereigniskette des PrintDocument-Objekts in Gang. Sie müssen das PrintPage-Ereignis behandeln, damit nicht nur leere Seiten ausgedruckt werden. Hierbei spielt das vom Parameter PrintPageEventArgs übergebene Graphics-Objekt die Schlüsselrolle. Es hat die gleiche Rolle wie das Graphics-Objekt, das vom Paint-Ereignis oder über CreateGraphics bereitgestellt wird, lediglich das Ziel der Ausgabe ist nicht der Bildschirm, sondern die Druckvorschau, der Drucker oder eine Datei. Die Standardmaßeinheit GraphicsUnit.Display entspricht 1/100 Zoll. Außer Myanmar haben alle Länder inzwischen offiziell metrische Einheiten (in der Praxis sind diese in den USA jedoch »unbekannt«). Sie sollten daher die Einheiten umstellen (g ist ein Graphics-Objekt). g.PageUnit = GraphicsUnit.Millimeter
914
Einen Druckauftrag erteilen
Für jede zu druckende Seite wird das Ereignis PrintPage erneut ausgelöst. Das hat weitreichende Konsequenzen, denn damit wird auch jedes Mal ein neues Graphics-Objekt erstellt. Wenn Sie eine Graphics-Einstellung für eine Druckseite vornehmen, wird diese nicht bei von der nächsten zu druckenden Seite übernommen. Deshalb müssen Sie die Grundeinstellungen von Graphics für jede weitere Seite erneut festlegen.
Seitenränder Die Eigenschaft PageBounds liefert die Abmessungen des zu bedruckenden Papiers. Public ReadOnly Property PageBounds As Rectangle
Wenn Sie Width und Height des zurückgegebenen Rectangle-Objekts abfragen, erhalten Sie die Abmessungen des entsprechenden Papierformats, zum Beispiel die Breite in Millimeter (die Standardeinheit von 1/100 Zoll wurde nicht geändert): e.PageBounds.Width * 25.4/100
PageBounds hat seinen Ursprung (0, 0) in der linken oberen Ecke des vom Drucker bedruck-
baren Bereichs. Bei den meisten Druckern ist dieser Punkt gegenüber der linken oberen Ecke des Papiers verschoben. 4 mm Rand ist bereits relativ wenig. Alles, was außerhalb des bedruckbaren Bereichs liegt, wird einfach abgeschnitten. Daher definiert die Eigenschaft MarginBounds des PrintPageEventArgs-Objekts ein kleineres Rechteck, das standardmäßig 100 Einheiten (also 25,4 mm) kleiner als die Seite ist. Dieses Rechteck ist um denselben Betrag verschoben wie PageBounds, sodass es asymmetrisch bezüglich der Seite ist. Mit der Einstellung OriginAtMargins=True des Druckdokuments vom Typ PrintDocument sind alle Koordinaten relativ zu dem durch MarginBounds festgelegten Bereich und nicht relativ zum bedruckbaren Bereich.
Bedruckbarer Bereich Die Aufteilung des bedruckbaren und des nicht bedruckbaren Bereichs können Sie mit einigen Eigenschaften in 1/100 Zoll ermitteln. Alle Eigenschaften sind schreibgeschützt. Die Werte hängen vom Druckermodell ab. 왘
PrintableArea im PrinterSettins-Objekt vom Typ RectangleF beschreibt die bedruck-
bare Fläche. 왘
HardMarginX und HardMarginY im PrinterSettins-Objekt vom Typ Single geben den
unbedruckbaren Rand an. 왘
VisibleClipBounds im Graphics-Objekt vom Typ RectangleF beschreibt die bedruckbare
Fläche. Wenn Sie die Werte für den linken und oberen Rand von den Koordinaten zu zeichnender Elemente abziehen, ist der Bezugspunkt nicht mehr der bedruckbare Bereich, sondern die linke obere Ecke der physikalischen Seite. Damit können Sie Elemente auf der Seite absolut positionieren und das Papier maximal ausnutzen.
915
16.2
16
Drucken
Beispielausdruck der Seitenränder Im folgenden Programmbeispiel werden die verschiedenen Ränder durch den Ausdruck von Rechtecken kenntlich gemacht: je eins für die Seite (PageBounds), den Clientbereich (MarginBounds) und des bedruckbaren Bereichs (VisibleClipBounds). Schließlich wird der druckbare Bereich in PrintableArea genutzt, um ein Rechteck absolut zu positionieren. Dazu wird erst ein Druckdokument doc erzeugt und im Ereignishandler der Schaltfläche mit Print ausgedruckt. Dadurch wird der Ereignishandler Druck aufgerufen. '...\Drucken\Eigenschaften\Seitenrand.vb
Imports System.Drawing.Printing Public Class Seitenrand Private WithEvents doc As New PrintDocument() Private Sub Druck(sender As Object, e As PrintPageEventArgs) _ Handles doc.PrintPage Dim g As Graphics = e.Graphics Dim schwarz As New Pen(Brushes.Black, 1), rot As New Pen(Brushes.Red, 1) ' vordefinierte Bereiche Dim page As Rectangle = e.PageBounds g.DrawRectangle(schwarz, page.Left, page.Top, page.Width, page.Height) Dim marg As Rectangle = e.MarginBounds g.DrawRectangle(schwarz, marg.X, marg.Y, marg.Width, marg.Height) Dim clip As RectangleF = g.VisibleClipBounds g.DrawRectangle(rot, clip.X, clip.Y, clip.Width, clip.Height) ' bedruckbaren Bereich erfassen g.DrawLine(schwarz, –10, 150, page.Width + 20, 150) g.DrawLine(schwarz, 150, –10, 150, page.Height + 20) ' exaktes Rechteck Dim pa As RectangleF = e.PageSettings.PrintableArea Dim dx As Single = 200 – pa.Left, dy As Single = 200 – pa.Top g.DrawRectangle(schwarz, dx, dy, pa.Width – dx, pa.Height – dy) End Sub Private Sub Drucken_Click(sender As Object, e As EventArgs) _ Handles Drucken.Click doc.Print() End Sub End Class
Die ausgedruckten Rechtecke im Einzelnen: 왘
PageBounds: Linke obere Ecke entsprechend dem nicht bedruckbaren Bereich um ein paar
Millimeter von der Seitenecke nach rechts unten verschoben. Da Breite und Höhe sich auf die gesamte Seite beziehen, sind die rechte und die untere Kante abgeschnitten. 왘
MarginBounds: Gegenüber PageBounds um 100 Einheiten von 1/100 Zoll in allen Richtun-
gen kleineres Rechteck, um je 100 Einheiten nach rechts unten verschoben.
916
Einen Druckauftrag erteilen
왘
VisibleClipBounds: Rechteck, das den Druckbereich maximal ausnutzt. Die linke obere
Ecke ist die gleiche wie in PageBounds, wird aber nirgends abgeschnitten. 왘
PrintableArea: Durch Abzug des unbedruckbaren Bereichs ist die linke obere Ecke 50,8 mm von der der Seite entfernt und nutzt die Seite rechts und unten maximal aus.
Einheiten Die Standardeinheit 1/100 Zoll ist etwas anachronistisch. Besser ist die Verwendung von Millimeter als Einheit. Das folgende Beispiel druckt ein Rechteck der Breite 100 mm und der Höhe 120 mm, das 50 mm vom linken und 80 mm vom oberen Rand entfernt ist. Durch die Zuweisung an PageUnit sind die Abmessungen ohne weitere Umrechnung anzugeben. Dies gilt nicht für die Verschiebung um den nicht bedruckbaren Rand, da er immer in 1/100 Zoll angegeben ist. Die in Pen angegebene Strichstärke wird auch in Millimeter gemessen. Bei solch dicken Linien ist es wichtig zu beachten, dass die Position und Abmessungen der Zeichenfunktionen sich immer auf die Linienmitte beziehen. '...\Drucken\Eigenschaften\Seitenrand.vb
Imports System.Drawing.Printing Public Class Seitenrand Private WithEvents doc As New PrintDocument() Private Sub Druck(sender As Object, e As PrintPageEventArgs) _ Handles doc.PrintPage Dim g As Graphics = e.Graphics Dim schwarz As New Pen(Brushes.Black, 1) g.PageUnit = GraphicsUnit.Millimeter Dim pa As RectangleF = e.PageSettings.PrintableArea g.DrawRectangle(schwarz, 50-pa.X*25.4\100, 80-pa.X*25.4\100, 100, 120) End Sub Private Sub Drucken_Click(sender As Object, e As EventArgs) _ Handles Drucken.Click doc.Print() End Sub End Class
Voreinstellungen Alle Eigenschaften, die die Seitenränder beschreiben, sind schreibgeschützt. Daher müssen sie gesetzt werden, bevor der Druckvorgang beginnt. Da sie vom Druckjob unabhängig sind, werden sie im Druckdokument gesetzt und werden immer dann genutzt, wenn nicht an anderer Stelle spezifischere Einstellungen gemacht werden. Public Property DefaultPageSettings As PageSettings
PageSettings hat die Eigenschaft Margin, die selbst die Eigenschaften Left, Right, Top und Bottom aufweist (siehe dazu auch Abschnitt 16.3, »Seiteneinstellungen mit PageSettings«). Die
917
16.2
16
Drucken
Einheiten sind dieselben wie im Graphics-Objekt (Voreinstellung 1/100 Zoll). Mit folgender Anweisung legen Sie beispielsweise den linken Rand auf eine Breite von 20 mm fest: printDocument1.DefaultPageSettings.Margin.Left = 2 * 100 \ 25.4
Das folgende Beispiel setzt Seitenränder von 50, 60, 80 und 97 für den linken, rechten, oberen und unteren Rand. Durch die Umschaltung der Einheit in der Methode Druck sind dies Millimeter. Mit TranslateTransform wird der Bezugspunkt folgender Ausgaben auf die linke obere Ecke des Druckbereichs gesetzt. '...\Drucken\Eigenschaften\Seitenrand.vb
Imports System.Drawing.Printing Public Class Voreinstellung Private WithEvents doc As New PrintDocument() Private Sub Druck(ByVal sender As Object, ByVal e As PrintPageEventArgs) _ Handles doc.PrintPage Dim g As Graphics = e.Graphics g.PageUnit = GraphicsUnit.Millimeter Dim pa As RectangleF = e.PageSettings.PrintableArea g.TranslateTransform(e.MarginBounds.Left – pa.Left * 25.4 \ 100, _ e.MarginBounds.Top – pa.Top * 25.4 \ 100) g.DrawRectangle(New Pen(Brushes.Black, 1), 0, 0, 100, 120) End Sub Private Sub Drucken_Click(ByVal sender As Object, ByVal e As EventArgs) _ Handles Drucken.Click doc.DefaultPageSettings.Margins = New Margins(50, 60, 80, 97) doc.Print() End Sub End Class
Mit OriginAtMargins=True statt TranslateTransform ergibt sich ein Versatz, dessen Ursache ich nicht befriedigend erklären kann.
16.2.6 Beenden des Druckauftrags Mit den beiden Eigenschaften HasMorePages und Cancel des PrintPageEventArgs-Objekts kann ein Druckauftrag beendet werden, jedoch unter verschiedenen Voraussetzungen. HasMorePages ist standardmäßig auf False gesetzt. Sollen mehrere Seiten ausgedruckt werden, muss der Wert True sein, damit der Ereignishandler noch einmal aufgerufen wird. Nach dem Druck der letzten Seite muss der Wert False sein. Dazu muss im Programmcode ermittelt werden, ob noch eine weitere Seite zum Drucken ansteht. In Abschnitt 16.7, »Mehrseitiger Text«, werden wir so einen Fall programmieren. Cancel steht standardmäßig auf False. Auf True gesetzt, wird der Druckauftrag abgebrochen
– unabhängig davon, wie viele Seiten noch zu drucken sind.
918
Einen Druckauftrag erteilen
16.2.7 WYSIWYG Graphics-Objekte der Ereignisse PrintPage und Paint unterscheiden sich »nur« durch das
Ziel der Ausgabe. Es lohnt sich daher, die Teile der Anzeige eines Programms, die sowohl auf dem Bildschirm angezeigt als auch ausgedruckt werden, durch eine einzige Methode erstellen zu lassen. Das Ziel ist eine möglichst weitgehende Korrespondenz von Anzeige und Ausdruck (WYSIWYG – what you see is what you get). Ganz ohne Kontrollstrukturen zur Anpassung der Ausgabe werden Sie aber nicht auskommen, denn die Ausgabegeräte unterscheiden sich deutlich. Ausdrucke haben eine feste Seitengröße, während Sie auf dem Bildschirm beliebig scrollen können. Bei einer Animation müssen Sie sich entscheiden, welches der Bilder ausgedruckt werden soll. Ein großer Vorteil von Ausdrucken ist die höhere Auflösung, die aber eine »sauberere« Definition der Ausgabe erfordert, damit zum Beispiel Texte in Blocksatz keine ausgefransten Ränder haben. Noch wichtiger ist die korrekte Ausgabe bei beidseitigem Druck, damit bei dünnem Papier nicht Textzeilen der Rückseite zwischen den Zeilen der Vorderseite landen und durchscheinen. Das folgende Beispielprogramm zeigt, wie Sie eine gemeinsame Ausgabefunktion für Bildschirm und Drucker prinzipiell realisieren können. Dazu wird ein einfaches in Abbildung 16.1 gezeigtes Linienmuster nach dem Starten des Programms in einer PictureBox angezeigt. Nach dem Anklicken des Buttons wird die Grafik auf dem (Standard-)Drucker ausgedruckt.
Abbildung 16.1
Ausgabe des Beispiels »Ausdruck«
'...\Drucken\Druck\Ausdruck.vb
Imports System.Drawing.Printing Public Class Ausdruck Private Sub DrawGraphic(g As Graphics, _ x1 As Int32, y1 As Int32, x2 As Int32, y2 As Int32) Dim p As New Pen(Color.Black) For i As Integer = x1 To x2 Step 20 : g.DrawLine(p, x1, y1, i, y2) : Next For i As Integer = x2 To x1 Step –20 : g.DrawLine(p, x2, y1, i, y2) : Next End Sub
919
16.2
16
Drucken
Private Sub Zeichnung_Paint(sender As Object, ByVal e As PaintEventArgs) _ Handles Zeichnung.Paint Me.DrawGraphic(e.Graphics, _ 0, 0, Zeichnung.ClientSize.Width, Zeichnung.ClientSize.Height) End Sub Private WithEvents doc As New PrintDocument() Private Sub Druck(sender As Object, e As PrintPageEventArgs) _ Handles doc.PrintPage Me.DrawGraphic(e.Graphics, e.MarginBounds.X, e.MarginBounds.Y, _ e.MarginBounds.Width + e.MarginBounds.X, _ e.MarginBounds.Height + e.MarginBounds.Y) End Sub Private Sub Aus(sender As Object, e As EventArgs) Handles Drucken.Click doc.Print() End Sub End Class
Die Routine DrawGraphic übernimmt die Ausgabe der Grafik. Diese Methode wird aus dem Paint-Ereignis der PictureBox und dem PrintPage-Ereignis des PrintDocument-Objekts aufgerufen. Damit die grafischen Methoden auch beim richtigen Empfänger, also dem Bildschirm oder dem Drucker, landen, muss die benutzerdefinierte Methode eine Referenz auf das Graphics-Objekt entgegennehmen, auf das die Grafikroutinen aufgerufen werden. Für die Ausgabe der Linien in DrawGraphic müssen der obere linke und der untere rechte Eckpunkt festgelegt werden. Auf dem Bildschirm werden die beiden Punkte durch (0, 0) und (pictureBox.ClientSize.Width, pictureBox.ClientSize.Height) beschrieben. Damit keine Linie des Ausdrucks abgeschnitten wird, habe ich die Eckpunkte des Ausdrucks mit den Eigenschaften X und Y von MarginBounds festgelegt.
16.3
Seiteneinstellungen mit PageSettings
Mit einem Objekt vom Typ PageSettings werden die Eigenschaften der jeweils zum Druck anstehenden Seite beschrieben. Wir brauchen kein Objekt dieses Typs zu erzeugen, obwohl das auch möglich wäre, denn über einige Eigenschaften können wir darauf zugreifen: 왘
über DefaultPageSettings des PrintDocument-Objekts (Hier werden die Standardeinstellungen für alle zu druckenden Seiten festgelegt.)
왘
über PageSettings des QueryPageSettingsEventArgs-Objekts des QueryPageSettingsEreignisses
왘
über PageSettings des PrintPageEventArgs-Objekts des PrintPage-Ereignisses
Ein PageSettings-Objekt verfügt über die elf in Tabelle 16.6 gezeigten Eigenschaften.
920
Der Drucker als PrinterSettings
Eigenschaft
Beschreibung
Bounds
Größe der Seite unter Berücksichtigung von Landscape
Color
Gibt an, ob die Seite in Farbe gedruckt wird (falls der Drucker dies kann).
HardMarginX
Druckerspezifischer Abstand zum linken Rand in 1/100 Zoll
R
HardMarginY
Druckerspezifischer Abstand zum oberen Rand in 1/100 Zoll
R
Landscape
Gibt an, ob die Seite im Hoch- oder Querformat gedruckt werden soll.
Margins
Seitenränder für die Seite
PaperSize
Papiergröße für diese Seite
PaperSource
Papierzufuhr für die Seite
PrintableArea
Abmessungen des druckerspezifischen Druckbereichs
PrinterResolution
Druckerauflösung für die Seite
PrinterSettings
Druckereinstellungen für die Seite
Tabelle 16.6
R
R
Eigenschaften von »PageSettings« (R = ReadOnly)
Die Einstellung der Eigenschaft Landscape ist True, wenn die Seite im Querformat gedruckt werden soll. Der Standardwert wird durch den Drucker bestimmt. Die drei Eigenschaften PrinterResolution, PaperSource und PaperSize sind jeweils vom Typ gleichen Namens. Um den Rahmen dieses Kapitels nicht zu sprengen, verweise ich Sie auf die .NET-Dokumentation. Vier Eigenschaften des PageSettings-Objekts können nur gelesen werden. Die Eigenschaft Margins ist vermutlich die am häufigsten benutzte Eigenschaft, denn sie speichert die Stan-
dardseitenränder. Public Property Margins As Margins
Die Klasse Margins definiert mit ihren vier Eigenschaften Left, Right, Top und Bottom die Abmessungen. Die Standardeinheit ist wieder 1/100-Zoll. Wir müssen deshalb den von uns vorgesehenen Rand in Zentimeter passend in das von den vier Eigenschaften interpretierte Maßsystem umrechnen. Soll der Rand 15 mm betragen, lautet die Wertzuweisung: Margin.Left = 15 * 100 \ 25.4
16.4
Der Drucker als PrinterSettings
Das PrinterSettings-Objekt enthält die Informationen darüber, welcher Drucker zum Ausdruck verwendet wird und wie das Dokument gedruckt wird. Analog zu PageSettings im vorigen Abschnitt instanziieren Sie normalerweise kein PrinterSettings-Objekt selbst, sondern lesen es aus den Eigenschaften: 왘
PrinterSettings im PrintDocument-Objekt
왘
PrinterSettings im PageSettings-Objekt
921
16.4
16
Drucken
16.4.1 Mehrere installierte Drucker Auf einem Rechner können mehrere Drucker installiert sein, die in der klassengebundenen Eigenschaft InstalledPrinters im PrinterSettings-Objekt gespeichert sind. Die Auflistung vom Typ PrinterSettings.StringCollection enthält die Namen der Drucker. For Each Druckername As String in PrinterSettings.InstalledPrinters ... Next
Die Anzahl der Drucker ermitteln Sie über die Eigenschaft Count der Auflistung. Der Zugriff auf einen bestimmten Drucker erfolgt über die Angabe seines Index. Die folgende Anweisung gibt beispielsweise den Namen des dritten Druckers in der Auflistung zurück: PrinterSettings.InstalledPrinters(2)
16.4.2 Eigenschaften von PrinterSettings Wenn ein neuer Drucker installiert wird, muss bei der Installation ein Druckername zur Identifizierung angegeben werden. Diese Angabe wird in der Eigenschaft PrinterName gespeichert. Sie ist sehr wichtig, denn sie gibt den Drucker an, der zum Ausdruck verwendet wird. Wenn Sie PrinterName den Namen eines anderen, gültigen Druckers aus der InstalledPrintersCollection zuweisen, werden sämtliche Eigenschaften des PrinterSettings-Objekts mit den zu diesem Drucker gehörenden Einstellungen belegt. Die Eigenschaft IsValid liefert in diesem Fall den Wert True. Wenn IsDefaultPrinter den Wert True hat, wird bei ungültigem PrinterName der Standarddrucker verwendet. Mit DefaultPageSettings werden die Standardseiteneinstellungen abgerufen. Weitere Eigenschaften finden Sie in Tabelle 16.7. Hinweis Beim Drucken sind immer zwei PageSettings-Objekte beteiligt: Das erste enthält die Standardeinstellungen jeder zu druckenden Seite, und das zweite gilt für die zum Druck anstehende Seite. Daher ist der folgende Ausdruck False (pd ist vom Typ PrintDocument): pd.PrinterSettings.DefaultPageSettings Is pd.DefaultPageSettings
Eigenschaft
Beschreibung
CanDuplex
Gibt an, ob beidseitiges Drucken unterstützt wird.
Collate
Gibt an, ob die Ausgabe sortiert erfolgt.
Copies
Anzahl der zu druckenden Exemplare des Dokuments
DefaultPageSettings
Standardseiteneinstellungen für diesen Drucker
Duplex
Einseitiger oder zweiseitiger horizontaler/vertikaler Druck
FromPage
Nummer der ersten zu druckenden Seite
InstalledPrinters
Namen aller installierten Drucker
SR
IsDefaultPrinter
Gibt an, ob der Standarddrucker ungültige Angaben ersetzt.
R
Tabelle 16.7
922
Eigenschaften von »PrinterSettings« (S = Shared, R = ReadOnly)
R
R
Steuerelemente zum Drucken
Eigenschaft
Beschreibung
IsPlotter
Gibt an, ob der Drucker ein Plotter ist.
R
IsValid
Gibt an, ob PrinterName einen gültigen Drucker bezeichnet.
R
LandscapeAngle
Drehwinkel zwischen Hoch- und Querformat
R
MaximumCopies
Erlaubte Anzahl Kopien
R
MaximumPage
Maximalwert für FromPage bzw. ToPage in PrintDialog
MinimumPage
Minimalwert für FromPage bzw. ToPage in PrintDialog
PaperSizes
Vom Drucker unterstützte Papierformate
R
PaperSources
Vom Drucker unterstützte Papierschächte
R
PrinterName
Name des zu verwendenden Druckers
PrinterResolutions
Vom Drucker unterstützte Auflösungen
PrintFileName
Dateiname bei Ausgabe in Druckdatei
PrintRange
Benutzerdefinierte Nummern der zu druckenden Seiten
PrintToFile
Gibt an, ob in eine Datei gedruckt wird.
SupporteColor
Gibt an, ob der Drucker Farbdruck unterstützt.
ToPage
Nummer der letzten zu druckenden Seite
Tabelle 16.7
16.5
R
R
Eigenschaften von »PrinterSettings« (S = Shared, R = ReadOnly) (Forts.)
Steuerelemente zum Drucken
Mit PrintDocument, PageSettings und PrinterSettings kennen Sie nun die wichtigsten Klassen, die im Zusammenhang mit dem Drucken stehen. Darüber hinaus bietet die Toolbox von Visual Studio fünf Steuerelemente an, die uns mit ihren Fähigkeiten die Programmierung teilweise erheblich erleichtern. Ich möchte Ihnen diese Controls nun vorstellen.
16.5.1 PrintDocument Dieses Steuerelement, das einen Druckjob wie weiter oben beschrieben kapselt, gehört zu der Gruppe der Steuerelemente, die zur Laufzeit nicht sichtbar sind und deshalb im Komponentenfach einer Form abgelegt werden. Im Eigenschaftsfenster können Sie nur die Eigenschaft DocumentName einstellen, der in Meldungsfenstern und der Druckerwarteschlange verwendet wird.
16.5.2 PrintDialog PrintDialog gehört zu der Gruppe der Standarddialoge. Im Kapitel über die Steuerelemente
haben wir uns schon mit den Dialogen OpenFileDialog, SaveFileDialog usw. auseinandergesetzt. PrintDialog ist das letzte zu dieser Gruppe gehörende Steuerelement. Es ist aus der Klasse System.Windows.Forms.CommonDialog abgeleitet. Es ermöglicht dem Anwender die Auswahl eines Druckers und die Festlegung der Anzahl der Ausdrucke sowie des Druckbereichs (siehe Abbildung 16.2).
923
16.5
16
Drucken
Abbildung 16.2
Das Dialogfenster PrintDialog
Der Dialog wird aufgerufen, um einen Druckjob zu starten. Deshalb muss dessen Eigenschaft Document auf den durch das PrintDocument-Objekt beschriebenen Druckauftrag verweisen. Das können Sie sowohl im Programmcode als auch im Eigenschaftsfenster einstellen. Vergessen Sie die Zuweisung, wird eine Ausnahme ausgelöst, weil der Dialog zu seiner Anzeige die Angabe eines Druckers benötigt, der aus der PrinterSettings-Eigenschaft des PrintDocument-Objekts bezogen wird. Im Eigenschaftsfenster werden neben Document noch weitere Eigenschaftseinstellungen ermöglicht, die in Tabelle 16.8 zusammengefasst sind (PrinterSettings nur im Code oder implizit durch Document). Eigenschaft
Beschreibung
AllowCurrentPage
Gibt an, ob die Auswahloption Aktuelle Seite angeboten wird.
AllowPrintToFile
Gibt an, ob das Auswahlkästchen zum Ausdruck in eine Datei angeboten wird.
AllowSelection
Gibt an, ob das Optionsfeld Seiten von ... bis angeboten wird.
AllowSomePages
Gibt an, ob das Optionsfeld Seiten angeboten wird.
Document
Der Druckjob, der die Druckereinstellungen liefert
PrinterSettings
Durch den Dialog zu modifizierender Druckjob
PrintToFile
Gibt an, ob das Auswahlkästchen zum Ausdruck in eine Datei selektiert ist.
ShowHelp
Gibt an, ob eine Hilfsschaltfläche angezeigt wird. Wenn ja, muss das Ereignis HelpRequest behandelt werden.
ShowNetwork
Gibt an, ob eine Schaltfläche Netzwerk angezeigt wird.
UseEXDialog
Gibt an, ob das Dialogfenster im Stil von Windows XP angezeigt werden soll.
Tabelle 16.8
924
Eigenschaften von »PrintDialog«
Steuerelemente zum Drucken
Wir wollen uns das Zusammenspiel des PrintDocument- und des PrintDialog-Objekts nun an einem Codefragment ansehen. Die Methode sei der Ereignishandler des Drucken-Menüpunkts. Aufgerufen wird der Dialog mit der Methode ShowDialog. Bekanntermaßen liefert diese Methode einen Rückgabewert aus der Enumeration DialogResult. Dieser entscheidet darüber, ob der Anwender die vorgenommenen Einstellungen übernommen haben möchte oder nicht. Private Sub drucken_Click(sender As Object, e As EventArgs e) printDialog1.Document = printDocument1 if printDialog1.ShowDialog() = DialogResult.OK Then _ printDocument1.Print() End Sub
16.5.3 PageSetupDialog Das in Abbildung 16.3 gezeigte PageSetupDialog-Steuerelement erlaubt die Festlegung der Seitenränder, die Angabe des Papierformats und der Papierquelle sowie die Ausrichtung im Hoch- bzw. Querformat. Dieser Dialog wird üblicherweise über den Menüpunkt Seite einrichten angezeigt.
Abbildung 16.3
Das Dialogfenster »PageSetupDialog«
PageSetupDialog bezieht Einstellungen des PageSettings- und PrinterSettings-Objekts
aus dem PrintDocument-Objekt, das ebenfalls der Eigenschaft Document übergeben werden muss. Neben dieser Eigenschaft werden weitere im Eigenschaftsfenster angezeigt (siehe Tabelle 16.9) und beeinflussen die Anzeige (PageSettings und PrinterSettings nur im Code oder implizit durch Document).
925
16.5
16
Drucken
Eigenschaft
Beschreibung
AllowMargins
Gibt an, ob die Seitenränder geändert werden können.
AllowOrientation
Gibt an, ob die Optionen Hoch- oder Querformat angeboten werden.
AllowPaper
Gibt an, ob die Papiereinstellungen (Größe und Zufuhr) angeboten werden.
AllowPrinter
Gibt an, ob die Druckerschaltfläche aktiviert ist.
Document
Der Druckjob, der die Druckereinstellungen liefert
EnableMetric
Gibt an, ob in Millimeter angezeigte Randeinstellungen automatisch in oder aus 1/100 Zoll konvertiert werden sollen.
MinMargins
Die von den Benutzern auswählbare minimale Randbreite im aktuell eingestellten Maßsystem
PageSettings
Durch den Dialog zu modifizierende Seiteneinstellungen
PrinterSettings
Durch den Dialog zu modifizierender Druckjob
ShowHelp
Gibt an, ob eine Hilfsschaltfläche angezeigt wird. Wenn ja, muss das Ereignis HelpRequest behandelt werden.
ShowNetwork
Gibt an, ob eine Schaltfläche Netzwerk angezeigt wird.
Tabelle 16.9
Eigenschaften von »PageSetupDialog«
Einstellungen im Dialog ändern das PageSettings-Objekt. Eine Kleinigkeit ist bei den Seitenrändern zu beachten. Damit die in DefaultPageSettings eingestellten Ränder richtig in den Dialog übernommen werden, muss EnableMetric den Wert True haben. Jedoch ist dies nur notwendig, wenn die Ländereinstellungen unter Windows einem metrischen System entsprechen. Im Gegensatz zu früheren .NET-Versionen werden die Werte aus dem Dialog unabhängig von dieser Einstellung richtig übernommen (dort musste mit PrinterUnitConvert.Convert(, PrinterUnit.Display, PrinterUnit.TenthsOfAMillimeter) konvertiert werden). Bitte beachten Sie, dass die Ränder vor der Zuweisung an die Margins-Eigenschaft im DefaultPageSettings-Objekt immer in die Einheit 1/100 Zoll konvertiert werden. Dies müssen Sie insbesondere berücksichtigen, wenn Sie der Eigenschaft PageUnit des GraphicsObjects eine andere Einheit zuweisen, da die Werte von Margins in dieser Einheit gemessen werden.
16.5.4 PrintPreviewDialog Mit dem PrintPreviewDialog-Steuerelement bieten Sie eine Druckvorschau an. Der Anwender kann im Fenster die Größe der Vorschau in vorgegebenen Stufen wählen. Bei einem mehrseitigen Druckjob ist die Anzeige von gleichzeitig bis zu sechs Druckseiten möglich. Die Programmierung ist sehr einfach und der anderer Steuerelemente ähnlich. Voraussetzung ist, dass Sie das Steuerelement mit dem PrintDocument-Objekt in der Eigenschaft Document verbinden, damit es sich mit den notwendigen Informationen versorgen kann. Setzen Sie die Eigenschaft UseAntiAlias auf True, werden Schriften geglättet – was allerdings gleichzeitig auch mit einer Verschlechterung der Performance verbunden ist. Das in Abbildung 16.4 gezeigte Fenster weist eine Schaltfläche mit einem Druckersymbol auf. Die Schaltfläche entspricht der OK-Schaltfläche anderer Dialoge. Damit das Dokument ausgedruckt wird, muss der Rückgabewert der Methode ShowDialog ausgewertet werden:
926
Steuerelemente zum Drucken
Private Sub mnuDruckvorschau_Click(sender As Object, e As EventArgs) printPreviewDialog1.Document = printDocument1 if printPreviewDialog1.ShowDialog() = DialogResult.OK Then _ printDocument1.Print() End Sub
Abbildung 16.4
Das Dialogfenster »PrintPreviewDialog«
16.5.5 PrintPreviewControl Während das Steuerelement PrintPreviewDialog im Komponentenfach abgelegt wird, weil es nicht in der Form angezeigt wird, ist das Steuerelement PrintPreviewControl zur Laufzeit der Anwendung für den Anwender sichtbar und zeigt den Bereich der Seitenvorschau an. Da es von Control abgeleitet ist, erbt es all dessen Möglichkeiten. Der Vorteil gegenüber PrintPreviewDialog ist die viel weiter gehende Kontrolle über die Seiten- bzw. Druckvorschau. Nur die sieben in Tabelle 16.10 gezeigten spezifischen Eigenschaften sind nötig, um die Druckvorschau zu steuern. Eigenschaft
Beschreibung
AutoZoom
Gibt an, ob die Dokumentvorschau auf den verfügbaren Bereich skaliert wird.
Columns
Bei mehrseitigen Druckjobs: die Anzahl nebeneinander gezeigter Seiten
Document
Gibt an, welcher Druckjob (PrintDocument) die Druckinformationen liefert.
Rows
Bei mehrseitigen Druckjobs: die Anzahl untereinander gezeigter Seiten
StartPage
Bei mehrseitigen Druckjobs: die Nummer der ersten anzuzeigenden Seite
UseAntiAlias
Schriften werden geglättet: bessere Anzeige bei schlechterer Performance.
Zoom
Steuert die Verkleinerung/Vergrößerung des angezeigten Dokuments.
Tabelle 16.10
Eigenschaften von »PrintPreviewControl«
927
16.5
16
Drucken
Die wichtigste ist die schon bekannte Eigenschaft Document, denn ohne einen Druckjob kann das PrintPreviewControl nichts anzeigen. Ein Dokument kann aus mehr als einer Seite bestehen. Diesem Thema werden wir uns später noch widmen. Wenn mehrere Dokumentseiten im Ansichtsbereich angezeigt werden sollen, müssen Sie in den Eigenschaften Rows und Columns angeben, wie viele Reihen und Spalten gleichzeitig dargestellt werden sollen. Standardmäßig sind beide Eigenschaften auf den Wert 1 eingestellt. Mit der Eigenschaft Zoom kann die Darstellung einer Seite verkleinert oder vergrößert werden. Die volle Größe eines Dokuments entspricht dem Wert 1.0, der Standardwert ist 0,3. Eine Veränderung des Wertes von Zoom bewirkt, dass die Eigenschaft AutoZoom auf False gesetzt wird. Ist diese Eigenschaft True, wird die Seitenvorschau des Dokuments so angepasst, dass es noch im Anzeigebereich des Steuerelements dargestellt werden kann. Die Eigenschaft StartPage gibt bei mehreren anzuzeigenden Seiten die Nummer der ersten Seite an. Dabei steht 0 für die erste Seite. UseAntiAlias dient wie schon beim PrintPreviewDialog zum Glätten von Schriften.
Beispiel »Eigene Vorschau« Einen eigenen Dialog zur Seitenvorschau zu programmieren, erfordert nicht besonders viel Programmcode und reduziert sich auf einige wenige Zeilen Programmcode, wie im folgenden Beispiel zu sehen ist. Abbildung 16.5 zeigt das Hauptfenster und Abbildung 16.6 die Druckvorschau. '...\Drucken\Eigenschaften\Vorschau.vb
Imports System.Drawing.Printing Public Class Vorschau Private Sub DrawGraphic(g As Graphics, _ x1 As Int32, y1 As Int32, x2 As Int32, y2 As Int32) Dim p As New Pen(Color.Black) For i As Integer = x1 To x2 Step 20 : g.DrawLine(p, x1, y1, i, y2) : Next For i As Integer = x2 To x1 Step –20 : g.DrawLine(p, x2, y1, i, y2) : Next End Sub Private Sub Linien(sender As Object, e As PaintEventArgs) Handles Me.Paint Me.DrawGraphic(e.Graphics, 0, Hauptmenü.Height, _ ClientSize.Width, ClientSize.Height) End Sub Private WithEvents doc As New PrintDocument() Private Sub Druck(sender As Object, e As PrintPageEventArgs) _ Handles doc.PrintPage Dim m As Rectangle = e.MarginBounds Me.DrawGraphic(e.Graphics, m.X, m.Y, m.Width + m.X, m.Height + m.Y) End Sub Private Sub Druck(sender As Object, e As EventArgs) Handles Drucken.Click Dim d As New PrintDialog() : d.Document = doc
928
Steuerelemente zum Drucken
If d.ShowDialog() = DialogResult.OK Then doc.Print() End Sub Private Sub Seite(sender As Object, e As EventArgs) Handles Einrichten.Click Dim d As New PageSetupDialog() : d.Document = doc : d.EnableMetric = True d.ShowDialog() End Sub Private Sub Ansicht(sender As Object, ByVal e As EventArgs) _ Handles Seitenansicht.Click Dim d As New PrintPreviewDialog() : d.Document = doc If d.ShowDialog() = DialogResult.OK Then doc.Print() End Sub Private Sub Selbst(sender As Object, e As EventArgs) _ Handles Druckvorschau.Click Dim frm As New EigeneVorschau() frm.VorschauControl.Document = doc frm.ShowDialog() End Sub End Class
Abbildung 16.5 Das Hauptfenster des Beispiels »Eigene Vorschau«
'...\Drucken\Eigenschaften\EigeneVorschau.vb
Public Class EigeneVorschau Private Sub Groß(sender As Object, e As EventArgs) Handles Größer.Click Try : VorschauControl.Zoom += 0.05 : Catch : End Try End Sub
929
16.5
16
Drucken
Private Sub Klein(sender As Object, e As EventArgs) Handles Kleiner.Click Try : VorschauControl.Zoom -= 0.05 : Catch : End Try End Sub Private Sub Passend(sender As Object, e As EventArgs) _ Handles Automatisch.Click VorschauControl.AutoZoom = True End Sub End Class
Abbildung 16.6
16.6
Der Dialog des Beispiels »EigeneVorschau«
Drucken von Grafiken
16.6.1 Das Problem der Maßeinheiten Widmen wir uns nun der Beziehung zwischen den Maßeinheiten der Bildschirmausgabe und der Druckerausgabe. Dazu verwenden wir ein einfaches Programm, das auf dem Monitor ein Quadrat von 100 × 100 Einheiten ausgibt und es durch Klick auf eine Schaltfläche ausdruckt. Private Sub Skalierung_Paint(sender As Object, e As PaintEventArgs) _ Handles MyBase.Paint Dim g As Graphics = e.Graphics g.DrawRectangle(New Pen(Brushes.Black, 1), 10, 10, 100, 100) End Sub Private WithEvents doc As New PrintDocument() Private Sub Druck(sender As Object, e As PrintPageEventArgs) _ Handles doc.PrintPage Dim g As Graphics = e.Graphics g.DrawRectangle(New Pen(Brushes.Black, 1), _ g.VisibleClipBounds.X, g.VisibleClipBounds.Y, 100, 100) End Sub
930
Drucken von Grafiken
Sie werden feststellen, dass die beiden Quadrate unterschiedlich groß sind. Messen Sie die Kantenlänge des ausgedruckten Quadrats nach – es sind 25,4 mm. Wie groß die Kantenlänge auf dem Bildschirm ist, hängt von dessen Auflösung ab. Die unterschiedlichen Abmessungen der beiden Quadrate lassen sich auf verschiedene Einheiten zurückführen, die den beiden Graphics-Objekten zugrunde liegen. Die Koordinatenwerte, die an die Zeichenfunktionen des Graphics-Objekts des PrintPage-Ereignisses übergeben werden, werden standardmäßig in Einheiten von 1/100 Zoll gemessen. Das entspricht der Einstellung der Eigenschaft PageUnit=GraphicsUnit.Display. Die 100 Einheiten, die wir im Beispiel oben zur Festlegung der Kantenlänge angegeben haben, führen deshalb zu einer Ausgabe, die exakt ein Zoll ist. War die Ausgabe der Grafik basierend auf diesem Maßsystem beabsichtigt, brauchen wir uns keine weiteren Gedanken zu machen. Stand hinter der Kantenlänge von 100 Einheiten jedoch die Absicht, ein Quadrat von 100 mm zu zeichnen, müssen wir die Druckausgabe noch an das metrische System anpassen. Die Einheit der Bildschirmausgabe ist standardmäßig Pixel. Die PageUnit-Eigenschaft des Graphics-Objekts des Paint-Ereignisses hat demnach den Wert GraphicsUnit.Pixel. Damit
ergibt sich als Seitenlänge des Quadrats 100 Pixel * Pixelgröße. Letztere wird durch die Bildschirmabmessungen und die gewählte Auflösung bestimmt. Das Graphics-Objekt hat die schreibgeschützten Eigenschaften DpiX und DpiY. Sie können diese Einstellungen unter Windows ändern, wie in den folgenden drei Screenshots zu sehen ist. Ist das Lineal richtig eingestellt, werden fortan Grafiken in der richtigen Größe ausgegeben. Leider ist auf meinem System die Schriftdarstellung dann etwas »ungewohnt«. Im Internet wird von weiteren Fontproblemen berichtet, also: Die Änderung erfolgt auf eigenes Risiko.
Abbildung 16.7 Bildschirmeinstellungen und DPI
931
16.6
16
Drucken
Abbildung 16.8
Bildschirmskalierung
16.6.2 Festlegung der Einheiten und Skalierung In Abschnitt 16.2.5, »Das Das Ereignis PrintPage«, haben wir uns bereits die Eigenschaft PageUnit des Graphics-Objekts und ihre Auswirkungen auf die Darstellung geometrischer Figuren angesehen. Wir können diese Eigenschaft so einstellen, dass das geometrische Objekt in der richtigen Größe auf dem Papier landet. Soll unser Quadrat eine Kantenlänge von 100 mm haben, ergänzen wir im PrintPage-Ereignis die folgende Anweisung: e.Graphics.PageUnit = GraphicsUnit.Millimeter
Die Maßangaben werden jetzt nicht mehr in 1/100 Zoll gemessen, sondern in Einheiten von einem Millimeter. Die Folge ist, dass das Quadrat mit einer Kantenlänge von 100 mm ausgedruckt wird, während die Grafikausgabe auf dem Bildschirm unverändert bleibt – zumindest solange wir im Paint-Ereignis keine Änderung der Einheiten vornehmen. Es kommt vor, dass wir deutlich größere Figuren ausdrucken lassen wollen, zum Beispiel das quadratische Grundstück einer Villa – 100 × 100 m2 oder 100000 × 100000 mm2. Sie sind also gezwungen, die Maße so zu skalieren, dass sie noch auf das Druckpapier passen. Dazu können Sie einen unbequemen Weg wählen und die Maße bereits vor der Eingabe skalieren, oder Sie nutzen die Eigenschaft PageScale im Graphics-Objekt. Mit e.Graphics.PageUnit = GraphicsUnit.Millimeter e.Graphics.PageScale = 0.001
erhalten Sie wieder ein Quadrat, das mit einer Kantenlänge von exakt 100 mm ausgedruckt wird – sofern Sie das Graphics-Objekt im PrintPage-Ereignis so festlegen.
16.6.3 Größenrichtige Ausgabe einer Grafik Im folgenden Beispiel wird ein Quadrat in Pixeleinheiten angezeigt und mit einer Seitenlänge von 50 mm auf dem Drucker ausgedruckt. Innerhalb des Quadrats beschreibt darüber hinaus eine Zeichenfolge die Größe des geometrischen Objekts.
932
Drucken von Grafiken
'...\Drucken\Druck\Skaliert.vb
Imports System.Drawing.Printing Public Class Skaliert Private Sub Draw(g As Graphics, x As Integer, y As Integer) Dim c As Color = Color.Black Dim f As Font = New Font("Arial", 8) g.DrawRectangle(New Pen(c), x, y, 50, 50) g.DrawString("Größe: 50x50", f, New SolidBrush(c), New Point(x + 2, y + 2)) End Sub Private Sub Bildschirm(sender As Object, e As PaintEventArgs) _ Handles MyBase.Paint Dim g As Graphics = e.Graphics g.PageUnit = GraphicsUnit.Millimeter g.PageScale = 1.15 ' bildschirmabhängig, nach DPI-Anpassung 1.0 Me.Draw(g, 0, 0) End Sub Private WithEvents doc As New PrintDocument() Private Sub Drucker(sender As Object, e As PrintPageEventArgs) _ Handles doc.PrintPage Dim g As Graphics = e.Graphics g.PageUnit = GraphicsUnit.Millimeter Me.Draw(g, e.MarginBounds.X * 25.4\100, e.MarginBounds.Y * 25.4\100) End Sub Private Sub Druck(sender As Object, e As EventArgs) Handles Drucken.Click doc.Print() End Sub End Class
Da die beiden Methoden DrawString und DrawRectangle sowohl zur Anzeige auf dem Bildschirm als auch zur Ausgabe des Druckers aufgerufen werden, sollten sie in einer eigenen Methode stehen, die von den beiden Ereignishandlern aufgerufen wird. Der Routine Draw werden nur der Grafikkontext und die Position des Rechtecks übergeben. Alle Größenangaben sind innerhalb von Draw fixiert, um für Bildschirm und Drucker identische Zahlenwerte zu verwenden. Die Kantenlänge des Quadrats beträgt 50 Einheiten. Damit dies 50 mm entspricht, werden die Einheiten beider Grafikkontexte auf Millimeter festgelegt. g.PageUnit = GraphicsUnit.Millimeter
Wenn die Bildschirmauflösung in DPI (dots per inch) nicht der realen Bildschirmauflösung entspricht, sollte die Eigenschaft PageScale im Ereignishandler von Paint entsprechend gesetzt werden, um die Umrechnung jeder einzelnen Koordinate zu vermeiden. Auf meinem System ist ein Faktor von 1.15 gerade passend. g.PageScale = 1.15
933
16.6
16
Drucken
Im zweiten und dritten Parameter erwartet Draw die Koordinaten x und y für den Startpunkt des auszudruckenden Quadrats. Die Linienmitten der linken und oberen Kante des Quadrats sollen gerade mit dem in MarginBounds definierten Rand zusammenfallen. MarginBounds liefert aber nur »Einheiten«, nämlich genau 100. Als 1/100 Zoll interpretiert, bedeutet das einen Seitenrand von 25,4 mm (plus des obligatorischen außerhalb des Druckbereichs liegenden Rands). Da wir aber auf GraphicsUnit.Millimeter umgeschaltet haben, werden diese Angaben in Millimeter gemessen und würden zu einem Randabstand des Quadrats von über 10 cm führen. Damit die Einheiten richtig interpretiert werden, müssen wir den Wert von MarginBounds mit 0,254 multiplizieren (die ganzzahlige Division »\« vermeidet eine Typumwandlung in Integer): e.MarginBounds.X * 25.4\100
Linienbreite des Zeichenstifts Hinsichtlich der Linienbreite der Druckausgabe können wir uns ein Problem einhandeln. Im Beispiel Skaliert legt das Pen-Objekt für den Aufruf der DrawRectangle-Methode nur die Farbe fest: New Pen(c)
Da wir keine Stiftbreite angegeben haben, beträgt diese standardmäßig 1. Ohne Festlegung der Einheit ist das 1 Pixel in der Bildschirmausgabe und 1/100 Zoll in der Druckausgabe. Wir haben GraphicsUnit.Millimeter eingestellt, also sind die Linien 1 mm breit. Wünschen wir eine andere Linienbreite, müssen wir das schon beim Konstruktoraufruf berücksichtigen. Beispielsweise wird die folgende Anweisung im Programmbeispiel zu einer Strichbreite von 0,5 mm führen: New Pen(c, 0.5)
Schriftgröße der Druckausgabe Die Größe einer Schrift wird nicht durch die Eigenschaften PageScale und PageUnit beeinflusst. Ausschlaggebend ist nur die Schriftgröße, die in Punkteinheiten angegeben ist. Von den vielen existierenden Punktmaßen wird in der Softwaretechnologie (fast) ausschließlich ein Punkt der Größe 1/72 Zoll verwendet, was etwa 0,0139 Zoll entspricht. Im Beispielprogramm wird die Größe der Schrift mit 8 Punkt festgelegt, also ca. 0,11 Zoll bzw. 2,8 mm. Wenn Ihnen die Umrechnung zu aufwändig ist, können Sie auch andere Konstruktoren der Font-Klasse benutzen, die neben der Angabe der Schriftfamilie (bzw. der Schriftart als Zei-
chenfolge) und der Schriftgröße auch eine Einheit vom Typ GraphicUnit spezifizieren. Mit Dim f As New Font("Arial", 8, GraphicsUnit.Millimeter)
wird die Höhe einer auf diesem Font basierenden Zeichenfolge 8 mm betragen. Sie können auch zur Bildschirmausgabe eine von Punkt abweichende Maßeinheit angeben. Sie sollten dabei jedoch berücksichtigen, dass die tatsächliche Höhe auf dem Monitor auch von der DPIEinstellung unter Windows abhängt.
934
Mehrseitiger Text
Hinweis Der Schriftgrad ist minimal größer als die tatsächliche Buchstabengröße.
16.7
Mehrseitiger Text
Den Abschluss der Druckthematik bildet ein Beispiel, das den Inhalt einer Textbox formatiert ausgibt und außerdem berücksichtigt, dass der Ausdruck mehrere Druckseiten lang sein kann. Das Programm lädt eine Textdatei in eine Textbox und druckt den Text aus. Zusammen mit Abschnitt 12.12.1, »Datei zum Öffnen wählen (OpenFileDialog)«, Abschnitt 12.12.2, »Datei zum Speichern wählen (SaveFileDialog)« und Abschnitt 18.4, »Die Zwischenablage«, haben Sie einen Startpunkt zur Entwicklung eines Texteditors. Sehen wir uns nun den Programmcode zusammengefasst an – zunächst nur die Passagen, die keiner genaueren Erklärung bedürfen. Dazu gehört nicht der Ereignishandler des PrintPageEreignisses, den wir anschließend Zeile für Zeile behandeln. Der Einfachheit halber habe ich auf die komplette Fehlerbehandlung verzichtet. '...\Drucken\Druck\Textdateien.vb
Imports System.Drawing.Printing Public Class Textdateien Private Sub Neu_Click(sender As Object, e As EventArgs) Handles Neu.Click Textfenster.Text = "" Text = "Unbenannt" End Sub Private Sub Öffnen_Click(sender As Object, e As EventArgs) _ Handles Öffnen.Click Dim ofd As New OpenFileDialog() ofd.Filter = "Textdateien (*.txt)|*.txt|Alle Dateien (*.*)|*.*" ofd.Title = "Öffnen einer Textdatei" If ofd.ShowDialog() = DialogResult.OK Then Textfenster.Text = IO.File.ReadAllText( _ ofd.FileName, System.Text.Encoding.Default) Text = ofd.FileName End If End Sub Private WithEvents doc As New PrintDocument() Private zuDrucken As String Private von, bis, nummer As Integer Private Sub Drucken_Click(sender As Object, e As EventArgs) _ Handles Drucken.Click Dim d As New PrintDialog() : d.Document = doc : d.AllowSomePages = True If d.ShowDialog() = DialogResult.OK Then
935
16.7
16
Drucken
doc.DocumentName = Text & " " & Now zuDrucken = Textfenster.Text Select Case d.PrinterSettings.PrintRange Case PrintRange.AllPages von = 1 : bis = d.PrinterSettings.MaximumPage Case PrintRange.SomePages von = d.PrinterSettings.FromPage : bis = d.PrinterSettings.ToPage End Select nummer = 1 doc.Print() End If End Sub Private Sub Einrichten_Click(sender As Object, e As EventArgs) _ Handles Einrichten.Click Dim d As New PageSetupDialog() : d.Document=doc : d.EnableMetric=True d.ShowDialog() End Sub Private Sub Seitenansicht_Click(sender As Object, e As EventArgs) _ Handles Seitenansicht.Click Dim p As New PrintPreviewDialog() : p.Document = doc Dim d As New PrintDialog() : d.Document = doc : d.AllowSomePages = True zuDrucken = Textfenster.Text von = 1 bis = d.PrinterSettings.MaximumPage nummer = 1 p.ShowDialog() End Sub ... End Class
Der Ereignishandler von PrintPage Im PrintPage-Ereignishandler befindet sich die gesamte Steuerung des Druckvorgangs. Dazu gehören die Ermittlung der für den Textausdruck zur Verfügung stehenden Rechteckfläche und die Anzahl der Druckzeilen jeder Seite. Dabei darf die letzte Zeile der Seite nicht abgeschnitten werden. Zusätzlich soll jede gedruckte Dokumentenseite eine Kopfzeile mit dem Dateinamen enthalten und in einer Fußzeile die aktuelle Seitenzahl anzeigen. Nach der Deklaration einiger lokaler Variablen, die innerhalb des Ereignishandlers benötigt werden, wird der Druckbereich des gesamten Ausdrucks festgelegt. außen = e.MarginBounds
innen legt die Größe des Rechtecks fest, in dem der Text aus der Textbox ausgedruckt wird.
Die Methode Inflate der Klasse Rectangle vergrößert/verkleinert eine Rechteckfläche. In unserem Fall subtrahieren wir Kopf- und Fußzeile. innen = RectangleF.Inflate(außen, 0, –2 * ft.GetHeight(e.Graphics))
936
Mehrseitiger Text
Die Anzahl der Zeilen muss in jedem Fall durch eine ganze Zahl beschrieben werden. Durch eine ganzzahlige Division erhalten wir die gerade passenden Zeilen. Alternativ können Sie die klassengebundene Methode Floor der Klasse Math verwenden. Dim anzahlZeilen As Integer = innen.Height \ ft.GetHeight(e.Graphics)
Nun können wir den Ausdruck beginnen. Nur Seiten im gewünschten Bereich erscheinen. Deshalb wird der Ausdruck von einer If-Anweisung umschlossen. If nummer >= von AndAlso zuDrucken.Length >= 0 Then ... End If
Der Ausdruck besteht aus drei Teilen. Zuerst wird der normale Text gedruckt. Es wird ein Formatierungsobjekt übergeben, damit kein Wort in der Mitte abgeschnitten wird. form.Trimming = StringTrimming.Word e.Graphics.DrawString(zuDrucken, ft, Brushes.Black, innen, form)
Danach werden Kopf- und Fußzeilen gedruckt. Die Kopfzeile wird zentriert ausgegeben. form.Alignment = StringAlignment.Center e.Graphics.DrawString(Text, ft, Brushes.Black, außen, form)
Die Fußzeile wird rechtsbündig formatiert. form.LineAlignment = StringAlignment.Far e.Graphics.DrawString("S " & nummer, ft, Brushes.Black, außen, form)
Nachdem der Text gedruckt worden ist (oder der Ausdruck übersprungen wurde), wird er aus dem zu druckenden Resttext entfernt. Dadurch kann jede Seite so tun, als beginne sie am Anfang des Textes. Korrespondierend zum Ausdruck wird wortweise formatiert. Die Anzahl Zeilen, die auf die Seite passen, wird mit MeasureString ermittelt. form.Trimming = StringTrimming.Word e.Graphics.MeasureString(zuDrucken,ft,innen.Size,form,zeichen,zeilen) zuDrucken = zuDrucken.Substring(zeichen)
Danach wird die Seitenzahl erhöht und zur nächsten Seite fortgeschritten. Wenn der Ausdruck übersprungen wurde, druckt die Methode Druck die nächste Seite. Auf diese Weise wird die Auslösung des PrintPage-Ereignisses vermieden und wird keine leere Seite ausgegeben. Wurde eine Seite ausgegeben, wird in HasMorePages gespeichert, Gibt an, ob weitere Seiten folgen. nummer += 1 If nummer – 1 < von Then Druck(sender, e) Else e.HasMorePages = zuDrucken.Length > 0 AndAlso nummer = von AndAlso zuDrucken.Length >= 0 Then ' zu langen Text hinter dem letztem vollständig passenden Wort kappen form.Trimming = StringTrimming.Word e.Graphics.DrawString(zuDrucken, ft, Brushes.Black, innen, form) ' Dateiname in der Kopfzeile anzeigen form.Alignment = StringAlignment.Center e.Graphics.DrawString(Text, ft, Brushes.Black, außen, form) ' Seitennummer in der Fußzeile anzeigen form.LineAlignment = StringAlignment.Far e.Graphics.DrawString("S " & nummer, ft, Brushes.Black, außen, form) End If ' gedruckten Text aus Drucktext nehmen; zeichen/zeilen: Größe der Seite form.Trimming = StringTrimming.Word e.Graphics.MeasureString(zuDrucken,ft,innen.Size,form,zeichen,zeilen) zuDrucken = zuDrucken.Substring(zeichen) nummer += 1 If nummer – 1 < von Then Druck(sender, e) Else e.HasMorePages = zuDrucken.Length > 0 AndAlso nummer 0 Ausschneiden.Enabled = Kopieren.Enabled Löschen.Enabled = Kopieren.Enabled End Sub
Auch die Implementierung der Ereignishandler der Menüpunkte wird durch die Hilfsmethode vereinfacht. Fangen wir mit Kopieren an, der einen Text in die Zwischenablage schreibt. Den selektierten Text der von TextBox() gelieferten aktiven Textbox schreiben wir mit SetText in die Zwischenablage. TextBox() kann keine Nullreferenz sein, da in diesem Fall der Menüpunkt beim Aufklappen des Bearbeiten-Menüs deaktiviert wurde und gar kein Ereignishandler angesprochen wird.
996
Die Zwischenablage
Private Sub Kopieren_Click(sender As Object, e As EventArgs) _ Handles Kopieren.Click Clipboard.SetText(TextBox().SelectedText) End Sub
Beim Löschen muss der selektierte Text entfernt werden, ohne dass die Zwischenablage ins Spiel kommt. Bei einer Zuweisung eines neuen Textes verliert die Textbox die Cursorposition. Deswegen speichern wir den Start der Selektion. Nach dem Ausschneiden sind Selektionsanfang und -ende identisch, und die Zuweisung der gespeicherten Cursorposition an SelectionStart liefert die richtige Position. Das Entfernen besteht aus dem Zusammensetzen der Textteile vor und nach der Selektion. Private Sub Löschen_Click(sender As Object, e As EventArgs) _ Handles Löschen.Click Dim txt As TextBox = TextBox() If txt.SelectionLength = 0 Then Return Dim cursorPosition As Integer = txt.SelectionStart txt.Text = txt.Text.Substring(0, cursorPosition) & _ txt.Text.Substring(cursorPosition + txt.SelectionLength) txt.SelectionStart = cursorPosition End Sub
Damit wird das Ausschneiden fast trivial. Wir verwenden einfach die bereits definierten Ereignishandler, um die Selektion in die Zwischenablage zu schreiben und dann zu löschen. Private Sub Ausschneiden_Click(sender As Object, e As EventArgs) _ Handles Ausschneiden.Click Kopieren_Click(sender, e) Entfernen() End Sub
Das Einfügen eines Textes ist etwas aufwändiger. Wir müssen zwei Situationen betrachten: 왘
Ist kein Text in der Textbox markiert, ist die Position des Cursors die Einfügeposition.
왘
Ist Text markiert, muss er durch den Inhalt der Zwischenablage ersetzt werden.
Das Einfügen bildet die neue Zeichenfolge und setzt den Cursor an das Ende des eingefügten Textes. Wenn vor dem Einfügen Text markiert ist, entfernt ihn der Ereignishandler Löschen_Click. Der neue Text besteht aus drei Teilen: aus dem Text bis zur Cursorposition (gegebenenfalls entstanden durch Löschen), aus dem Text der Zwischenablage und aus dem Rest des Textes. Private Sub Einfügen_Click(sender As Object, e As EventArgs) _ Handles Einfügen.Click Dim txt As TextBox = TextBox() Löschen_Click(sender, e) ' markierten Text ggf. entfernen Dim cursorPosition As Integer = txt.SelectionStart txt.Text = txt.Text.Substring(0, cursorPosition) & _ Clipboard.GetText() & _ txt.Text.Substring(cursorPosition) txt.SelectionStart = cursorPosition + Clipboard.GetText().Length End Sub
997
18.4
18
Programmiertechniken
Da die Signaturen der Menüpunkte für Kontextmenüs identisch sind, können Sie die hier gemachten Definitionen direkt für Kontextmenüs übernehmen, indem Sie die Handles-Klauseln entsprechend erweitern. Lediglich die Aktivierung der Menüpunkte im Ereignis Opening des Kontextmenüs muss nochmals wie im Ereignis DropDownOpening des Menüs implementiert werden, da andere Menüelemente referenziert werden.
998
WPF, die Alternative zu WinForms, trennt Design in einem XML-Dialekt und Programmierung in Visual Basic. Dieses Kapitel beschreibt die neue Syntax und erste, einfache Anwendungen.
19
WPF – Grundlagen
Mit dem .NET Framework 3.0 wurde eine neue Programmierschnittstelle für WindowsAnwendungen eingeführt, die sich Windows Presentation Foundation (WPF) nennt. Mit dem Visual Studio 2008 wurde WPF nun neben anderen neuen Technologien in die Entwicklungsumgebung fest integriert. In den kommenden Kapiteln werden wir uns mit WPF beschäftigen. Alle Aspekte zu berücksichtigen würde den Rahmen dieses Buches sprengen. Aber ich möchte Ihnen einen Einstieg in die neue Technologie geben und Ihnen zeigen, wie Windows-Anwendungen in Zukunft entwickelt werden. Sie werden feststellen, dass die Lernkurve nicht so steil ist wie bei den nun in die Jahre kommenden WindowsForms, die Thema der letzten Kapitel waren. Denn neben den Kenntnissen in der Programmierung müssen Sie nur noch eine einfache »Sprache« lernen, mit der die Oberflächen in WPF beschrieben werden. Der Aufwand lohnt sich: WPF kann viel mehr als WindowsForms.
Dieses Kapitel beschäftigt sich mit vier Themenkreisen. Einem kurzen Überblick folgt eine Einführung in XML. Der WPF-spezifische XML-Dialekt namens XAML ist das dritte Thema. Den Abschluss bildet die Erstellung und Analyse erster Beispiele.
19.1
Merkmale einer WPF-Anwendung
Zuerst möchte ich grob die Charakteristika einer WPF-Anwendung skizzieren: 왘
Die Benutzeroberfläche wird in einem XML-Dialekt namens XAML (eXtensible Markup Language, gesprochen semml) geschrieben. Dadurch kann die Beschreibung der Benutzeroberfläche strikt vom Code getrennt werden – analog zu ASP.NET.
왘
Anzeige derselben Benutzeroberfläche in einem normalen Fenster oder im Browser
왘
Umfangreiche Unterstützung von 2D- und 3D-Grafiken, inklusive schneller Ausgabe durch DirectX, Animationen, Unterstützung von Videos, Bilder und Audio
왘
Das neue Inhaltsmodell erlaubt schönere Oberflächen und ist trotzdem einfacher.
왘
Reichhaltige Datenbindungsmöglichkeiten für die Komponenten
왘
Verteilung mit XCopy und ClickOnce möglich
999
19
WPF – Grundlagen
Dies ist nur ein kleiner Überblick. Wie Sie sehen, liegt der Fokus auf Design und Layout – ganz im Sinne des letzten Betriebssystems Windows Vista. Sollte eine Neuentwicklung nun WinForms oder WPF nutzen? Die folgenden Überlegungen können Ihnen bei der Entscheidungsfindung helfen. Die grafischen Fähigkeiten von WPF stellen wohl alles Vergangene in den Schatten. Wollen Sie runde Buttons? Kein Problem. Wollen Sie runde Fenster? Ebenfalls kein Problem. Die exzellenten grafischen Möglichkeiten von WPF mögen vielleicht einer der großen Vorteile sein, bergen aber auch gleichzeitig die Gefahr, Oberflächen zu entwickeln, die vom Benutzer nicht mehr intuitiv bedient werden können. Zudem sind WPF-Anwendungen nur unter Windows XP, Windows Server 2003 und Vista lauffähig. Für größere Projekte ist die strikte Trennung von Oberflächenbeschreibung und Code sehr wichtig, sodass die Oberfläche von einem Grafiker gestaltet wird, während der Entwickler den Code dazu schreibt. Die Oberflächenbeschreibung erfolgt in XAML, die Programmlogik kann in VB.NET oder C# codiert werden. Die folgende Übersicht zeigt diese Trennung: Grafiker Softwareentwickler | | Designtool Programmierumgebung (Visual Studio, Expression Blend, (Visual Studio, Editor) ZAM 3D, Aurora) | | | XAML VB.NET und C#.NET | | Übersetzung in eine binäre Ressource Übersetzung in MSIL (".g.vb" oder ".g.cs") (".dll") | | ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄLinkerÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ | Anwendung
WPF ist eine neue Technologie. Die Erfahrung lehrt uns, dass neue Technologien oft in der ersten Version noch nicht ausgereift sind. Das soll nicht bedeuten, dass neue Konzepte fehlerbehaftet sind. Vielmehr fehlen Komponenten, die für ältere Technologien selbstverständlich sind. WPF bildet da keine Ausnahme. Sie werden einige liebgewonnene Steuerelemente vermissen, andererseits werden auch zahlreiche neue, WPF-spezifische, angeboten.
19.2
XML
Das Kürzel XML steht für Extensible Markup Language (erweiterbare Textauszeichnungssprache), eine Sprache zur strukturierten Darstellung von Datenstrukturen. Texte in dieser Sprache sind, nach einer kurzen Eingewöhnungsphase, einfach zu lesen. Andererseits sind sie so gut strukturiert, dass sie auch einfach durch Programme verarbeitet werden können. Das .NET Framework bietet im Namensraum System.Xml dazu viele Klassen an, die kaum Wünsche offen lassen. Microsoft hat einen Dialekt dieser Sprache namens XAML entwickelt, der ganz
1000
XML
auf die Entwicklung von grafischen Benutzerschnittstellen zugeschnitten ist. Ihr zur Seite gestellt ist VB.NET, um alle nicht das Aussehen betreffenden Aspekte einer Anwendung zu formulieren. Da XAML auf XML aufbaut, wird XML in diesem Abschnitt vorgestellt, bevor XAML im nächsten Abschnitt folgt.
19.2.1 Tags Jedes XML- (und jedes XAML-)Dokument besteht aus Sätzen, die Elemente genannt werden. Das Etikett eines Elements (Tag) kennzeichnet die Art des Elements, vergleichbar mit zum Beispiel Aussage- und Fragesätzen in der gesprochenen Sprache. Über den Inhalt des Elements bzw. Satzes wird nichts ausgesagt. In der gesprochenen Sprache werden Interpunktionszeichen wie Komma, Punkt und Apostroph zur Trennung von Wörtern und Sätzen gebraucht. Dieselbe Idee wird in XML verfolgt, nur die Zeichen sind etwas anders. Ein Element beginnt immer mit einem »Kleiner als«-Zeichen , das die Einleitung des Elements abschließt. In der gesprochenen Sprache steht am Ende jedes Satzes ein Punkt. In XML treten an die Stelle des Punktes vier Teile: ein »Kleiner als«-Zeichen . Damit können wir unseren ersten Satz in XML formulieren:
Dieses Element ist komplett richtig, hat aber keinen Inhalt. Bitte beachten Sie, dass die wörtliche Wiederholung des Tags zwingend ist, XML unterscheidet außerdem zwischen Groß- und Kleinschreibung. Für ein Element ohne Inhalt gibt es auch eine Kurzschreibweise ohne schließendes Tag.
19.2.2 Inhalt Jeglicher Inhalt des Elements steht zwischen dem < des Elementanfangs und dem > des Elementendes. Bitte beachten Sie, dass aufeinanderfolgende Leerzeichen, Tabulatoren und Zeilenvorschübe oder Kombinationen davon in der Voreinstellung wie ein einziges Leerzeichen behandelt werden. Lebenslauf: ...
Größere Dokumente werden in der Regel durch Überschriften gegliedert. In XML wird diese Strukturierung durch eine Schachtelung der Elemente erreicht. Anstelle des reinen Texts treten dann ein oder mehrere komplette Elemente. Im folgenden Beispiel sind es zwei: Lebenslauf: ...
1001
19.2
19
WPF – Grundlagen
19.2.3 Attribute In der gesprochenen Sprache können Sätze verschieden betont werden. Dieses Konzept findet sich bei XML in den Attributen eines Elements wieder, die ein Element näher beschreiben. Im folgenden Beispiel bekommt ein leeres Element einen Namen. Es wird je einmal voll ausgeschrieben und abgekürzt. Ein Leerzeichen vor Name ist zwingend, das hinter dem konkreten Namen optional. Ein Attribut, hier Name, darf pro Element maximal einmal benutzt werden, und sein Wert muss in Anführungszeichen gesetzt werden.
19.2.4 Wurzel XML ist gegenüber der gesprochenen Sprache noch etwas strikter. Der gesamte Dokumentinhalt muss unter einer einzigen Überschrift stehen. Daher sollten in einem XML-Dokument einheitliche Daten stehen. Im Fall von XAML sind dies die Daten zu einer einzigen grafischen Benutzeroberfläche. Lebenslauf: ...
Innerhalb des Wurzelelements werden in XAML alle Elemente angegeben, aus denen sich das Fenster zusammensetzt. Üblicherweise fügt man aber zuerst ein Containerelement ein, in dem die anderen Elemente wie Button oder TextBox positioniert werden. Typische Containerelemente sind Grid oder StackPanel. Diese Containerelemente haben die Eigenschaft Children, die eine Liste von UIElement-Objekten verwaltet.
19.2.5 Namensräume Wenn, wie im Fall von XAML, ein XML-Dialekt sehr viele verschiedene Tags und Attribute enthält, kann es leicht zu Konflikten in Benennungen kommen. Daher erlaubt es XML, die Namen in Namensräumen zu organisieren. Vergleichbar den Namensräumen in .NET, haben gleichlautende Namen verschiedener Namensräume nichts miteinander zu tun. Sie werden als Attribut eines Elements deklariert und wirken auf das Element sowie alle in diesem Element enthaltenen Unterelemente. Im nächsten Beispiel ist in der Namensraumdeklaration in der ersten Zeile der Teil vor dem Doppelpunkt xmlns fest, und der dahinter kann frei gewählt werden, muss aber eindeutig sein. Ein Namensraum fasst alle Namen mit einem Bezug zu dem angegebenen Link zu einer Gruppe zusammen und kann – mit einem folgenden Doppelpunkt – einem Element- oder Attributnamen vorangestellt werden, um den Namen eindeutig zu
1002
XML
qualifizieren. Im Beispiel werden so die Kennungen id für Personen und Gebäude eindeutig unterscheiden. Die voll qualifizierten Namen haben nichts miteinander zu tun. Lebenslauf
19.2.6 Kommentare Kommentare werden zwischen eingeschlossen und können dort stehen, wo auch Elementinhalte stehen. Sie dürfen keinen doppelten Bindestrich -- enthalten. Geschachtelte Kommentare sind also verboten.
19.2.7 Entitäten Beim Inhalt von Elementen und Attributen müssen Sie dafür sorgen, dass es zu keiner Fehlinterpretation der Zeichen und & kommt, sodass irrtümlich durch den Text die Struktur des Dokuments verändert wird. Daher werden die Zeichen innerhalb eines Textes als und & geschrieben: 왘
왘
&
wird im Text zu &
19.2.8 Zusammenfassung Hier sind noch einmal die wichtigsten Aspekte von XML (und damit auch von XAML) aufgelistet: 왘
Elemente werden durch Tags beschrieben.
왘
Zu jedem Starttag muss es ein Endtag geben (gegebenenfalls abkürzend als />).
왘
Der Inhalt eines Elements steht zwischen dem Start- und dem Endtag.
왘
Elemente dürfen Attribute haben mit Zeichenketten als Wert.
왘
Elemente können ineinander verschachtelt werden.
왘
Ein Dokument hat genau ein Wurzelelement.
1003
19.2
19
WPF – Grundlagen
왘
Groß-/Kleinschreibung muss beachtet werden.
왘
Namen dürfen in Namensräumen organisiert sein.
왘
Kommentare sind möglich.
왘
Die Zeichen und & in Attributwerten müssen als Entität codiert werden.
19.3
XAML
In diesem Abschnitt werden einige Spezialitäten des XAML-Dialekts besprochen, die in (fast) allen Programmen verwendet werden, die von den Klassen der WPF Gebrauch machen. Dazu gehören sowohl Windows-Programme als auch solche, die für das Webbrowser-Plugin Silverlight geschrieben worden sind. Der Compiler bildet XAML in korrespondierende Sprachkonstrukte ab: 왘
Elementname Typ mit öffentlichem parameterlosen Konstruktor
왘
Attributname Eigenschaft, Ereignis
왘
Namensraum Namensraum in der CLR
19.3.1 Standardnamensräume XAML benötigt mindestens zwei Namensräume, die in einem neuen WPF-Projekt von Visual Studio automatisch hinzugefügt werden (der zweite enthält nur wenige Namen). Dadurch hat das Programm Zugriff auf alle wesentlichen Namen und Klassen, die zur Erstellung einer grafischen Oberfläche benötigt werden. Alle Element- und Attributnamen ohne explizite Angaben zum Namensraum verwenden den Namensraum, der keinen Namen hinter xmlns hat. Haben alle Namensräume Namen, müssen ausnahmslos alle Element- und Attributnamen mit Namensraum: qualifiziert werden. Es ist daher praktisch, um sich Tipparbeit zu sparen, dem Namensraum keinen Namen zu geben, in dem die meisten zu verwendenden Namen definiert sind. Die eckigen Klammern in der folgenden Syntax enthalten den optionalen Namensraum; kursiv gesetzte Teile können Sie Ihren Bedürfnissen anpassen. Zwei konkrete Spezifikationen sind nach der Syntax angegeben. xmlns[:Namensraum]= ""
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x=http://schemas.microsoft.com/winfx/2006/xaml
19.3.2 Zusätzliche Namensräume In vielen WPF-Anwendungen müssen Sie über die Standardangabe hinaus noch weitere Namensräume angeben: Manchmal ist es ein Namensraum der Common Language Runtime (CLR), manchmal auch ein Namensraum, der in der aktuellen Anwendung beschrieben ist.
1004
XAML
Eine typische Angabe lautet: xmlns:data="clr-namespace:System.Data;assembly=System.Data"
Der Alias, hier data, ist frei wählbar, muss aber eindeutig sein. Die Namensraumspezifikation setzt sich aus clr-namespace, einem Doppelpunkt und dem Namensraum zusammen. Danach folgt der Verweis auf die Bibliothek, in der der Namensraum definiert ist. Die Endung .dll wird ignoriert. Ist der Namensraum Teil des aktuellen Projekts, kann die Angabe entfallen. xmlns:src="clr-namespace:WpfApplication2"
19.3.3 Tags Jedem Tag in XAML entspricht eine WPF-Klasse. Durch die Angabe in XAML wird das Element über den parameterlosen Konstruktor der Klasse automatisch instanziiert. Hinweis Eigene Klassen in XAML müssen einen parameterlosen Konstruktor haben.
Wie in XML können Elemente geschachtelt werden. Diese logische Struktur wird automatisch auf das Aussehen abgebildet, und Kindelemente sind Teil des Elternelements. Diese knappe Feststellung stellt eine der größten Neuerungen von WPF dar: das neue Inhaltsmodell. So können viele Steuerelemente (fast) beliebige andere Steuerelemente enthalten. Durch die schier endlosen Kombinationsmöglichkeiten sind praktisch alle Forderungen an eine Benutzeroberfläche erfüllbar. Dies birgt aber auch die Gefahr, »unbenutzbare« Programme zu entwerfen. Sie sollten sich daher am Motto »Weniger ist mehr« orientieren. Das folgende Beispiel soll nur andeuten, wie weit man das neue Inhaltsmodell treiben kann. Eine selbst aktive Listbox auf eine aktive Taste zu setzen, ist gewöhnungsbedürftig. Das Aussehen zeigt Abbildung 19.1. Franz Willi Ich bin ein 'Label'
Abbildung 19.1
Verschachtelte WPF-Komponenten
1005
19.3
19
WPF – Grundlagen
Das Attribut Name der Elemente hat eine besondere Bedeutung. Es gibt den Namen der Variablen an, unter der das automatisch generierte Objekt im Programmcode ansprechbar ist. Innerhalb eines Gültigkeitsbereiches muss damit die vergebene Bezeichnung eindeutig sein. Bereiche werden im Wesentlichen durch das Wurzelelement, Vorlagen und Stile gebildet. Hinweis Auf Elemente ohne Attribut Name kann im Programm nicht zugegriffen werden.
19.3.4 Attribute Eigenschaften und Ereignisse können in zweierlei Weise in XAML spezifiziert werden. Immer möglich ist eine Spezifikation eines solchen Wertes als Kindelement. Diese Schreibweise wird Property-Element-Syntax (Eigenschaftselementschreibweise) genannt. Dem Wertnamen wird der Elementname, durch einen Punkt getrennt, vorangestellt. Ist der Wert »einfach«, kann er als Attribut des Elements angegeben werden. Kursiv gesetzte Namen der folgenden Syntax passen Sie wieder Ihren Bedürfnissen an. Im folgenden Beispiel wird die Beschriftung einer Taste in beiden »Geschmacksrichtungen« angegeben und in einer dritten Schreibweise, die nur für den Inhalt eines Elements zugelassen ist: Wert
Beschriftung Beschriftung
Es gibt Fälle, in denen die erste Schreibweise verwendet werden muss. Das folgende Beispiel definiert einen linearen Farbverlauf als Hintergrund der Taste:
Einige Attributwerte sind aus mehreren Werten zusammengesetzt. Sie können wie oben als Element oder Attribut angegeben werden. Im folgenden Beispiel sind die Werte durch Kommata voneinander getrennt.
1006
XAML
19.3.5 Konvertierungen Damit die Interpretation von Attributwerten funktioniert, muss der Typ des Attributs mit einem Konvertierer verknüpft sein, der die eigentliche Analyse der Zeichenkette übernimmt. Im letzten Beispiel hat Margin den Typ Padding und einen korrespondierenden Konvertierer vom Typ PaddingConverter, der sich um die Analyse der durch Kommata getrennten Werte kümmert. Die allgemeine Syntax lautet (kursive Bezeichner passen Sie Ihren Bedürfnissen an, Alternativen sind durch einen senkrechten Strich getrennt): _ Public Structure|Class Eigenschaftstyp ... End Structure|Class Public Class Konvertierer : Inherits TypeConverter ... Public Overrides Function ConvertFrom( _ context As ITypeDescriptorContext, culture As CultureInfo, _ value As Object _ ) As Object 'analysiere value, und speichere korrespondierende Werte 'wenn Analyse erfolgreich, dann Return New Eigenschaftstyp(...) 'sonst Throw New FormatException(text) End Function End Class
Wenn ein Element eine Liste erwartet, aber nur ein einzelnes Kind direkt enthält, wird automatisch eine Auflistung mit einem einzigen Element erzeugt, wie in der folgenden Syntax zu sehen ist. Daher sind die beiden folgenden Beispieltasten gleichwertig, obwohl die mittleren Zeilen unterschiedlich sind.
1007
19.3
19
WPF – Grundlagen
19.3.6 Indirektion und Markup-Erweiterungen Werte können indirekt spezifiziert werden, ein Wert ist dann durch den Wert eines anderen Elements oder Objekts gegeben. Dadurch können Sie einfach konsistente Oberflächen schaffen. Zum Beispiel kann die Hintergrundfarbe eines Elements die aller anderen Elemente bestimmen. Ändern Sie diese, sind automatisch alle anderen Farben auch angepasst. Um die Auflösung der Referenz auf einen Wert (dies ergibt den Wert selbst) kümmert sich eine Klasse, die von System.Windows.Markup.MarkupExtension abgeleitet sein muss. Oft wird zur Kennzeichnung das Suffix Extension an den Klassennamen angehängt, das in XAML ausgelassen werden darf. In der folgenden Syntax muss ExtensionElement von MarkupExtension abgeleitet sein. Die kursiv gesetzten Namen wählen Sie nach Bedarf. Welche Attribute NameX nötig sind, wird durch ExtensionElement bestimmt; sehr selten werden sie ganz fehlen. Innerhalb von WertX dürfen andere Referenzierungen stehen. Im folgenden Beispiel bestimmt die Hintergrundfarbe der ersten Taste die der zweiten. Die Verwendung der Attribute wird durch die Klasse Binding festgelegt. Eine eigene Erweiterung könnte zum Beispiel nur das Referenzobjekt spezifizieren und automatisch dessen Hintergrundfarbe nehmen. Wert1 Wert2 ...
Quelle Ziel
Die Ableitung von MarkupExtension hat eine weitere Konsequenz. Durch eine besondere Syntax kann die Indirektion auch in Attributen verwendet werden, die nur Zeichenketten erlauben. Dazu wird die Spezifikation statt in Elementsyntax als Zeichenkette mit geschweiften Klammern geschrieben, und Anführungszeichen werden weggelassen. Auch hier sind Schachtelungen erlaubt. Wie üblich passen Sie kursiv gesetzte Namen in der folgenden Syntax Ihren Bedürfnissen an. Wieder bestimmt ExtensionElement, ob und welche NameX nötig sind. Wegen der besonderen Bedeutung zeigen die beiden letzten Zeilen zusätzlich zwei spezielle Erweiterungen. Im folgenden Beispiel wird die Hintergrundfarbe einer Taste einmal direkt und dreimal indirekt
1008
XAML
angegeben. Die letzte Farbangabe wird während der Laufzeit angepasst, wenn die Hintergrundfarbe des Desktops verändert wird, während das Programm läuft.
Farbe fest statisch dynamisch
Hinweis Eine einzelne geschweifte Klammer »{« im Text erzeugen Sie durch »{}{».
19.3.7 Verbundene Attribute Einige Attribute beziehen sich auf umgebende Elemente. Sie werden verbundene Attribute genannt, und ihrem Namen wird, getrennt durch einen Punkt, der Elementname des umgebenden Elements vorangestellt. Kursiv gesetzte Namen in der Syntax sind wieder frei wählbar. Das folgende Beispiel platziert die Taste relativ zum oberen Rand der Leinwand.
Hinweis Im Code lautet die Syntax: Klasse.SetAttribut(ElementY, Wert).
19.3.8 Code Behind Schließlich muss noch die Klasse an XAML gebunden werden, die die Funktionalität enthält, die nicht in XAML formuliert ist. Dies passiert durch das Attribut x:Class. Die folgenden zwei Codeabschnitte zeigen den XAML-Teil einer Applikation sowie den benutzerdefinierten Teil der Klasse des x:Class-Attributs. Theoretisch kann der Code der Klasse mit in den XAML-Teil eingebunden werden (Sonderzeichen sind dann zu kodieren (siehe Abschnitt 19.2.7, »Entitäten«). Ich halte dies aber nicht für ratsam, da sowohl die Trennung von Design und Code aufgehoben wird als auch die Flexibilität reduziert wird.
1009
19.3
19
WPF – Grundlagen
... Namespace Namensraum Partial Public Class Logik ... End Class End Namespace
19.3.9 Schlüsselwörter XAML definiert eine Reihe von Schlüsselwörtern, die besondere Aspekte bei der Entwicklung berücksichtigen. Die Tabelle 19.1 stellt Ihnen einige davon vor. In den folgenden Kapiteln werden Sie auf einige von diesen in den Beispielen stoßen. Schlüsselwort
Bedeutung
x:Class
Verbindet das Wurzelelement in XAML mit der Code-Behind-Datei.
x:Code
Definition eines Inline-Codebereichs (mischt Design und Code!)
x:Key
Der eindeutige Name eines Elements in einer Ressource
x:Name
Benennung von Elementen, die keine Eigenschaft Name haben.
Tabelle 19.1
Schlüsselwörter von XAML (Auszug)
Zum Abschluss zeigt Tabelle 19.2 die Markup-Erweiterungen im Namensraum x. Erweiterung
Beschreibung
x:Array
Definition von Arrays in XAML
x:Null
Der Wert Nothing in XAML
x:Static
Referenzierung typgebundener Werte (Shared sowie Konstanten)
x:Type
Attributwert vom Typ Type (zum Beispiel in Stildefinitionen)
Tabelle 19.2
19.4
Markup-Erweiterungen von XAML
WPF-Anwendungen
Das Visual Studio 2008 bietet Ihnen verschiedene Anwendungstypen an: 왘
Windows-Fenster mit der Basisklasse System.Windows.Forms.Form
왘
WPF-Anwendung
왘
ASP.NET: HTML-Seite mit Anbindung an .NET
1010
WPF-Anwendungen
왘
WPF-Browseranwendung
왘
WPF-Benutzersteuerelementbibliothek
왘
Benutzerdefinierte WPF-Steuerelemente
Die beiden letztgenannten beziehen sich auf das Entwickeln von WPF-Steuerelementen. Darauf werden wir nicht weiter eingehen. Die beiden ersten sind Anwendungen im herkömmlichen Sinne und können mit dem MS Installer (MSI) oder mit ClickOnce installiert werden. Mit beiden Installationsvarianten haben wir uns im Kapitel 8 beschäftigt. Als Browseranwendungen laufen die beiden mittleren nicht in einem eigenen Fenster und werden nicht installiert. Da das Design von Anwendungen für das Webbrowser-Plugin Silverlight auch über XAML erfolgt, lassen sich die Konzepte der WPF-Anwendungen auf diese Art von WebbrowserAnwendungen übertragen. Um das Silverlight-Plugin klein zu halten, stehen nicht alle visuellen Elemente und nicht alle Methoden zur Verfügung. Was jedoch vorhanden ist, kann in der gleichen Art und Weise verwendet werden.
19.4.1 Erstes Fenster Schauen wir uns eine erste WPF-Anwendung an. In Visual Studio erstellen wir ein neues Projekt vom Typ WPF-Anwendung. Dazu wählen wir den Menüpunkt Neues Projekt ... im Datei-Menü. Im folgenden Dialog wählen wir WPF-Anwendung und geben ihr den Namen ErsteWpfAnwendung.
Abbildung 19.2
WPF-Projekt
Der Assistent und der visuelle Editor erzeugen eine Reihe von Dateien. Normalerweise wird nur Source Code der Dateien mit den Endungen .vb und .xaml auf der obersten Verzeichnis-
1011
19.4
19
WPF – Grundlagen
ebene sowie Dateien im Verzeichnis My Project vom Benutzer verändert. Alle anderen Dateien entstehen automatisch, gegebenenfalls durch Projekteinstellungen. Auf die Dateien gehe ich in Abschnitt 19.5, »Dateien einer WPF-Anwendung«, ein. Die Anordnung der Fenster ähnelt der bei einem Windows-Forms-Projekt. Nur der Codeeditor ist in zwei Bereiche unterteilt: Im oberen Bereich wird das Fenster angezeigt, der dazugehörige XAML-Code erscheint darunter (siehe Abbildung 19.3). Sie gestalten die Oberfläche des Fensters, indem Sie Steuerelemente aus der Toolbox in den Designer ziehen oder alternativ direkt den XAML-Code schreiben. Beide Fensterbereiche synchronisieren sich automatisch gegenseitig.
Abbildung 19.3
WPF-Editor
Hinweis Der dicke Pfeil in Abbildung 19.3 macht auf eine Neuerung in WPF aufmerksam: Sie können die Ansicht zoomen. Dies konnte einfach ermöglicht werden, weil Größenangaben in WPF in Fließkommazahlen (Typ Double) angegeben werden.
Die zentralen Dateien sind Application.xaml, die das Aussehen der Anwendung festlegt, und Application.xaml.vb, die die Funktionalität enthält, wie zum Beispiel die Funktionen der Menüeinträge. Wenden wir uns zuerst der XAML-Datei zu. Das Wurzelelement dieser XMLDatei ist Window. Dies spiegelt die Tatsache wieder, dass sich alle sichtbaren Elemente einer Anwendung innerhalb eines Fensters der Anwendung befinden müssen. Eine Anwendung darf mehrere Fenster haben. Der Inhalt dieses Fensters ist eine leere Tabelle, Grid genannt, die später die Elemente des Fensters aufnimmt, wie zum Beispiel Texte und Tasten (Buttons). Das Window-Element selbst hat noch ein paar Attribute, die es genauer spezifizieren. Da der Inhalt von Window dessen Eigenschaften erbt, müssen zum Beispiel die Namensräume xmlns nicht noch einmal in Grid spezifiziert werden. Das unten stehende Fenster hat eine Breite und Höhe von 300 Pixeln, und es steht Window1 in der Titelleiste.
1012
WPF-Anwendungen
' ...\WPF\ErsteWpfAnwendung\Window1.xaml
Nach dem Start der Anwendung (Menü Debuggen 폷 Debugging starten oder mit dem grünen Rechtspfeil der Toolleiste oder (F5) wird ein neues leeres Fenster geöffnet. Es hat keinen Inhalt, aber eine Titelleiste mit Tasten zur Minimierung, Maximierung und zum Schließen des Fensters. Das Fenster lässt sich auch in der Größe verändern. Nach dem Schließen stellt man fest, dass einige zusätzliche Dateien erzeugt worden sind. In den Verzeichnissen bin, My Project und obj stehen .manifest-Dateien im XML-Format mit Informationen zum Einstiegspunkt der Anwendung, zur Sicherheit und zu Abhängigkeiten. Installationsinformationen stehen in .application-Dateien in den Verzeichnissen bin und obj. Schließlich finden sich noch .pdbund .exe-Binärdateien in den Verzeichnissen bin und obj.
19.4.2 Button Der nächste Schritt ist es, das Fenster mit Leben zu füllen. Dazu stecken wir einen Button in das Grid-Element der Datei Window1.xaml. Damit das Drücken des Buttons sich auch bemerkbar macht, fügen wir zum Button noch ein Click-Attribut hinzu. Es deklariert, dass es eine Funktion gibt, die beim Drücken des Buttons ausgeführt werden soll. Der Patient Button äußert also eine Befindlichkeitsstörung Click, um die sich ein Therapeut kümmert. Langweiliger ausgedrückt löst ein Click-Ereignis (Event) eine Ereignisbehandlungsroutine (Eventhandler) aus. Diese Funktion sollte in der Visual Basic-Datei Window1.xaml.vb kodiert werden, damit eine Korrespondenz zur XAML-Datei gewahrt bleibt. Das erste Argument des Ereignishandlers ist das Objekt, das das Ereignis auslöst. Das letzte Argument kann weitere Informationen zum Ereignis enthalten, zum Beispiel zur Position des Mauszeigers. Beim Klick auf einen Button sind keine weiteren Informationen nötig. Die Methode kann automatisch erzeugt werden, und zwar durch einen Doppelklick auf den Button im Designer oberhalb des XAML-Codes. Im Funktionsrumpf rufen wir eine Methode auf, die sich als Dialog auf dem Bildschirm bemerkbar macht, um eine Rückmeldung zu haben, dass der Button angeklickt wurde. Die beiden Dateien lauten also wie folgt: ' ...\WPF\ErsteWpfAnwendung\Window2.xaml
1013
19.4
19
WPF – Grundlagen
Drück mich!
' ...\WPF\ErsteWpfAnwendung\Window2.xaml.vb
Class Window2 Private Sub Therapeut(sender As Object, e As System.Windows.RoutedEventArgs) MessageBox.Show("Jetzt geht es besser!") End Sub End Class
Wie kommt es nun, dass der Button das ganze Fenster ausfüllt? Er selbst hat erst einmal keine Ahnung, wie groß er ist. Also fragt er seine Eltern, wie groß er ist. Und wie im realen Leben dürfen Elternelemente faul sein und sagen: »Weiß nicht, frag doch mal die Großeltern.« Dies wird so lange fortgesetzt, bis der Button weiß, wie groß er ist. Dieses Konzept wird visuelle Vererbung (englisch Visual Inheritance) genannt. In der letzten Anwendung wurde der Button beim Fenster fündig und hat seine Größe übernommen. Im nächsten Beispiel wird dem Button eine explizite Größe gegeben. ' ...\WPF\ErsteWpfAnwendung\Window3.xaml
Drück mich!
' ...\WPF\ErsteWpfAnwendung\Window3.xaml.vb
Class Window3 Private Sub Therapeut(sender As Object, e As System.Windows.RoutedEventArgs) MessageBox.Show("Jetzt geht es besser!") End Sub End Class
Hinweis Abschnitt 3.10, »Ereignisse«, beschreibt die Mechanismen von Ereignissen im Detail. Auf die WPFspezifischen Ereignisse geht Abschnitt 21.1, »Ereignisse programmieren«, ein.
1014
WPF-Anwendungen
19.4.3 Browseranwendung Auf der Basis von XAML können auch Anwendungen für das Webbrowser-Plugin Silverlight geschrieben werden. Es kann von der Seite http://www.microsoft.com/silverlight heruntergeladen werden. Diese Anwendungen haben einige Besonderheiten: 왘
Die Anwendung existiert nur im Hauptspeicher und nicht auf der Festplatte.
왘
Das Wurzelelement ist UserControl statt Window.
왘
Die Anwendung ist beschränkt (Sandbox):
왘
왘
keine Popupfenster außer den Dialogen für Dateien und Meldungen
왘
keine Dateizugriffe
왘
kein unverwalteter Code
Hybridanwendungen, die unter Windows und im Browser laufen, sind möglich.
Zur Entwicklung mit Visual Studio müssen Sie erst den Zusatz Silverlight Tools for Visual Studio von http://silverlight.net/GetStarted herunterladen und installieren. Danach können Sie ein neues Projekt des richtigen Typs erzeugen.
Abbildung 19.4
Silverlight-Projekt
Im folgenden Dialog können Sie wählen, wie die HTML-Seite des Projekts aussieht: 왘
neue Website: ASP.NET-basiert
왘
einzelne HTML-Seite
Um sich nicht um die Erstellung einer Webseite kümmern zu müssen, wählen wir wie in Abbildung 19.5 gezeigt die zweite Möglichkeit.
1015
19.4
19
WPF – Grundlagen
Abbildung 19.5
Silverlight-Webseite
Hinweis Der Fehlermeldung, dass eine xap-Datei fehlt, kann durch Neuerstellung der Projektmappe begegnet werden.
Dem Design in der Projektdatei Page.xaml fügen wir noch einen Button hinzu: ' ...\WPF\ErsteSilverlightAnwendung\Page.xaml
Analog zur normalen WPF-Anwendung wird in Page.xaml.vb der Code für den Ereignishandler des Buttons hinterlegt. Der MessageBox-Dialog steht in Webanwendungen nicht zur Verfügung und muss durch einen entsprechenden Dialog ersetzt werden. ' ...\WPF\ErsteSilverlightAnwendung\Page.xaml.vb
Option Strict On Partial Public Class Page : Inherits UserControl Public Sub New()
1016
Dateien einer WPF-Anwendung
InitializeComponent() End Sub Private Sub Therapeut(ByVal sender As System.Object, _ ByVal e As System.Windows.RoutedEventArgs) System.Windows.Browser.HtmlPage.Window.Alert("Jetzt geht es besser!") End Sub End Class
Hinweis System.Windows.Interop.BrowserInteropHelper.IsBrowserHosted testet, ob der Code in
einem Webbrowser läuft.
Nach dem Start der Anwendung (Menü Debuggen 폷 Debugging starten oder mit dem grünen Rechtspfeil der Toolleiste oder (F5) wird der Standardwebbrowser mit der Seite geöffnet. Beim ersten Mal erscheint noch ein Dialog, in dem die Anpassung der Konfiguration zum Debuggen angeboten wird, die wir hier akzeptieren. Der Button reagiert beim Druck genauso wie in der normalen WPF-Anwendung. Hinweis Eine Fehlermeldung »Failed to start monitoring changes« kann auf nicht ausreichende Rechte für den ASP-Server zurückzuführen sein. Dies kann passieren, wenn über ein Netzwerk zugegriffen wird. Zum Testen ist es am einfachsten, die Projekte dann lokal abzulegen. In realen Anwendungen muss auch noch an die Erstellung eines Zertifikats gedacht werden.
Vorausgesetzt, der Webbrowser hat das Silverlight-Plugin installiert, reicht es, die Datei ErsteSilverlightAnwendungTestPage.html und den Ordner ClientBin aus dem Verzeichnis ErsteSilverlightAnwendung_Web zu kopieren und die HTML-Datei im Webbrowser zu öffnen (auf meinem Macintosh musste ich das Firefox-Plugin zweimal installieren).
19.5
Dateien einer WPF-Anwendung
In diesem Abschnitt wollen wir uns einmal die vier Projektdateien Application.xaml, Application.xaml.vb, Windows1.xaml und Windows1.xaml.vb für eine Anwendung namens WpfApplication1 näher ansehen.
19.5.1 Window1.xaml In dieser Datei steckt der XAML-Code, den Sie im unteren Bereich des Codeeditors finden.
1017
19.5
19
WPF – Grundlagen
In der ersten Zeile wird mit x:*" eine Einheit breit, Height="3*" ist drei Einheiten hoch. Eine Einheit ist gleich dem nach Abzug von Elementen fester Abmessungen verbleibenden Platz, dividiert durch die Summe aller *-Angaben. Zum Beispiel: Ein Gitter der Breite 400 Pixel hat die Spaltenbreiten *, 100, 2*, 3*. Der verbleibende Platz ist 300 Pixel, die Summe relativ bemaßter Spalten 6*. Damit ist eine *-Einheit 50 Pixel. Bei den Spalten *, 400, 2*, 3* haben die *-Spalten die Breite null. Da der Zahlenwert den Typ Double hat, können Sie bei Bedarf eine sehr genaue Aufteilung erzielen. Abbildung 20.19 zeigt den Fall *, 2*, 3*.
Abbildung 20.19
Relative Spaltenbreiten
Die Komponenten im Gitter werden innerhalb von Grid parallel zu Grid.RowDefinitions und Grid.ColumnDefinitions eingetragen. Die Attribute Grid.Column und Grid.Row geben den nullbasierten Spalten- bzw. Zeilenindex einer Komponente an. Fehlende Angaben werden durch null ersetzt, auch wenn dadurch Komponenten übereinander liegen.
1032
Grid
Mit Angaben für Grid.ColumnSpan oder/und Grid.RowSpan können sich Komponenten über mehrere Spalten oder Zeilen erstrecken (siehe Abbildung 20.20). Die beiden Eigenschaften Grid.Column und Grid.Row geben dann die linke obere Zelle des Elements an. Button1
Abbildung 20.20
Mehrzellige Komponente
Zellengröße durch GridSplitter Wenn der Anwender die Zeilenhöhe oder Spaltenbreite mit der Maus verändern darf, kann er das Gitter optimal für sich einstellen. Damit dies geht, braucht er eine Art »Griff« vom Typ GridSplitter. Er wird wie eine normale Komponente im Grid platziert und kann die benachbarten Zellen entlang seiner längeren Ausdehnung (Anzahl Zellen) in der Größe ändern (vertikal bei quadratischem Splitter). Machen Sie den Splitter sehr »dünn«, wird die Bedienung erschwert. Dies kann auch zur Laufzeit passieren, wenn die in Margins spezifizierten Ränder sehr schmal sind und der Splitter bis zum Anschlag bewegt wird. Zur Beschleunigung können Sie ShowPreview auf den Wert True setzen. Gegenüber dem Standardwert False werden die Zellen dann nicht in Echtzeit angepasst, sondern es wird nur eine Kopie des Splitters bewegt, und erst beim Loslassen der Maustaste werden die Zellen angepasst. Im folgenden Code ist ein vertikaler und ein horizontaler GridSplitter definiert, die in Abbildung 20.21 zu sehen sind. Mit den Eigenschaften Column, Row, ColumnSpan und RowSpan
1033
20.7
20
Layoutcontainer
wird die Abmessung festgelegt. Die Ausrichtung, vertikal oder horizontal, ergibt sich aus der längeren Dimension. Button1 Button2 Button3 Button4
Abbildung 20.21
GridSplitter zur Zellgrößenänderung
20.8 Positionierung von Steuerelementen Wo ein Steuerelement endgültig dargestellt wird, hängt von seiner Position und seinen Rändern ab.
1034
Positionierung von Steuerelementen
20.8.1 Position mit Top, Bottom, Right und Left Nur der Container Canvas erlaubt die explizite Positionierung von Steuerelementen. Alle anderen ordnen die Komponenten automatisch an. Der Canvas gibt die vier Eigenschaften als sogenannte verbundene Eigenschaften (englisch Attached Properties) weiter, die sich auf das übergeordnete Steuerelement beziehen (hier Canvas). Deren Syntax finden Sie in Abschnitt 19.3.7, »Verbundene Attribute«. Erläuterungen folgen in Abschnitt 22.1.2, »Angehängte Eigenschaften«. Button1
20.8.2 Außenränder mit Margin Die Eigenschaft Margin legt den Abstand zum Außenrand der nächstgelegenen Komponente beziehungsweise eines umgebenden Layout-Containers fest. Gemessen wird vom Außenrand einer Komponente. Durch Margin bekommt die Komponente »Abstandhalter« (siehe Abbildung 20.22). Sie können Margin auf drei verschiedene Weisen spezifizieren: 왘
Einzelwert: Mit Margin="10" bleibt ein Rand von zehn Pixeln nach allen vier Seiten.
왘
Zwei Werte: Mit Margin="10, 20" sind der linke und der rechte Rand 10 Pixel breit, der obere und der untere Rand 20 Pixel.
왘
Vier Werte: Margin="10, 20, 5, 25" lässt links 10, oben 20, rechts 5 und unten 25 Pixel Rand.
Height="50">Button1
Abbildung 20.22
Außenränder
20.8.3 Innenränder mit Padding Die Eigenschaft Padding gibt den Abstand vom Komponentenrand zu seinem Inhalt an, eine Art innerer »Abstandhalter«. Wie bei Margin können Sie Werte auf drei Arten angeben, jedoch spezifizieren hier zwei Werte den Abstand nach links und oben. Die beiden anderen Arten – Einzelwert für gleichmäßigen Rand und vier Werte (links, oben, rechts, unten) – werden wie bei Margin interpretiert.
1035
20.8
20
Layoutcontainer
Im folgenden Beispiel wird in einem Button ein Image angezeigt. Der Button hat einen Abstand von fünf Pixel zum Container, das Bild in der Schaltfläche einen Rand von zehn Pixel (siehe Abbildung 20.23).
Abbildung 20.23
Innenränder
20.9 Verschachtelung Layout-Container können beliebige Steuerelemente aufnehmen, auch beliebige Container. Durch dieses Prinzip können Sie mit einer kleinen Zahl an Grundbausteinen Schachtelungen beliebiger Komplexität erzeugen, mit denen Sie beliebige Designforderungen erfüllen können. Beim Entwurf können Sie einen Bottom-Up-Ansatz wählen und aus kleinen Einheiten immer komplexere aufbauen oder mit einem Top-Down-Ansatz eine grobe Struktur immer weiter verfeinern. Durch das hierarchische Baukastensystem der Container verlieren Sie nicht so schnell den Überblick. Ich habe mich nach einer kurzen Eingewöhnungszeit gefragt, wie ich vorher ohne diese Hierarchisierung ausgekommen bin. Das folgende Beispiel gibt einen ersten Eindruck von Verschachtelungen. Die Bedeutung der noch nicht besprochenen Steuerelemente ist nicht von Belang. Es soll nur die Hierarchisierung demonstriert werden. Abbildung 20.24 zeigt das Aussehen des Beispiels. Im Window sind neben mehreren Buttons auch zwei Checkboxen, zwei Radiobuttons, eine Listbox und ein Label enthalten. Um eine solche Form zu gestalten, werden Sie zumindest am Anfang noch einmal zu Papier und Bleistift greifen. Lösungen gibt es viele. Ich möchte Ihnen an dieser Stelle die folgende zeigen.
1036
Verschachtelung
Abbildung 20.24
Verschachtelung
OK Beenden Fett Schwarz Kursiv Blau
1037
20.9
20
Layoutcontainer
Ausgabe aller befreundeten Personen: Peter Andreas Petra Franz Beate Neues Element Abbrechen
1038
Dieses Kapitel stellt eine repräsentative Auswahl an Steuerelementen vor, mit der Sie viele Anwendungen programmieren können.
21
WPF-Steuerelemente
Alle für Benutzeroberflächen üblichen Steuerelemente sind im WPF-Framework enthalten, von einem einfachen Steuerelement wie einer Schaltfläche bis hin zu komplexen Steuerelementen wie Baumansichten. Um übersichtlich zu bleiben, greift dieses Kapitel eine repräsentative Auswahl heraus. Beim Blick in die Dokumentation sollten Sie beachten, dass es viele gleichnamige Steuerelemente im WinForms-Namensraum System.Windows.Forms und im WPF-Namensraum System.Windows gibt. Der falsche Namensraum fällt oft nicht sofort auf, da viele Steuerelemente sich stark ähneln.
21.1
Ereignisse programmieren
Wie alle modernen Benutzeroberflächen reagiert auch eine WPF-Anwendung nur auf Ereignisse. Fast jedes Ereignis in WindowsForms hat eine Entsprechung in WPF (aber nicht umgekehrt, siehe unten). Die Ereignishandler haben zwei Parameter: das auslösende Element vom Typ Object und Ereignisinformationen vom Typ EventArgs oder einer davon abgeleiteten Klasse. Wie bei WindowsForms-Anwendungen hat das Eigenschaftsfenster eine Ereignisliste. Zusätzlich bietet die XAML-Datei IntelliSense-Unterstützung. Handler werden nicht mit einer Handles-Klausel, sondern dynamisch an das Ereignis gebunden. Angenommen, Sie möchten das Click-Ereignis einer Schaltfläche in der XAML-Datei programmieren. In der IntelliSense-Unterstützung wählen Sie das Click-Ereignis und binden durch zweimaliges Drücken der (ÿ_)-Taste das Ereignis an einen Ereignishandler, der in der Code-Behind-Datei erzeugt wird. Gegebenenfalls müssen Sie die Datei manuell öffnen. Button1
Hat eine Komponente ein Standardereignis, wie zum Beispiel Button, können Sie den Ereignishandler auch mittels Doppelklick auf die Komponente erzeugen. In diesem Fall wird er mit einer Handles-Klausel an das Ereignis gebunden. Alternativ (nicht additiv) können Sie die Verknüpfung zwischen Ereignis und Ereignishandler auch im Code festlegen, zum Beispiel:
1039
21
WPF-Steuerelemente
Public Partial Class Window1 : Inherits Window Private Sub Laden(sender As Object, e As RoutedEventArgs) _ Handles MyBase.Loaded AddHandler Button1.Click, AddressOf Button1_Click End Sub Private Sub Button1_Click(sender As Object, e As RoutedEventArgs) MessageBox.Show("Im Click-Ereignishandler 1") End Sub Private Sub Button2_Click(sender As Object, e As RoutedEventArgs) _ Handles Button2.Click MessageBox.Show("Im Click-Ereignishandler 2") End Sub End Class
21.1.1 Weiterleiten von Ereignissen Die Auslösung eines Ereignisses ist unabhängig von dessen Behandlung. Jede Ereignisart hat geeignete Routinen, die entscheiden, welche der möglichen Ereignishandler von einem konkreten Ereignis betroffen sind. Zum Beispiel kommen bei einem Click-Ereignis alle Elemente unter der aktuellen Mausposition prinzipiell in Betracht. Im einfachsten Fall »gewinnt« von diesen die zuoberst liegende Komponente: Nur sie behandelt das Ereignis. Zum Beispiel ist nur ein Button betroffen und nicht die optisch darunter- und logisch darüberliegende Form, sie erfährt überhaupt nichts von dem Ereignis. Das ist ein Ereignismodell, wie es in WinForms implementiert ist. WPF-Komponenten ergänzen dieses Ereignismodell durch die Möglichkeit der Weiterleitung von Ereignissen an über- sowie untergeordnete Komponenten. So können mehrere Komponenten gleichzeitig von einem Ereignis betroffen sein. Im Beispiel des Click-Ereignisses wird auch ein Ereignishandler im übergeordneten WPF-Window aufgerufen. Die Möglichkeit einer Weiterleitung ist sinnvoll, denn in der WPF können Komponenten beliebig ineinander verschachtelt werden. Ereignisse in WPF, die weitergeleitet werden können, werden als Routed Events bezeichnet. Es gibt drei verschiedene Mechanismen der Ereignisbehandlung: 왘
Die einfachste Variante funktioniert wie in Windows-Forms. Ein Ereignis wird nur von der Komponente verarbeitet, bei der es aufgetreten ist. Es wird Direct genannt.
왘
Beim Bubbling eines Ereignisses wird es an die übergeordnete Komponente weitergereicht. Diese kann nun ebenfalls darauf reagieren und/oder es weiterreichen.
왘
Beim Tunneling beginnt die Ereigniskette beim Wurzelelement, in der Regel Window. Es reicht das Ereignis an das nächste untergeordnete Element weiter, das seinerseits das ihm untergeordnete Element informiert. Das geht so lange, bis der Auslöser erreicht ist. Die Tunneling-Ereignisse sind durch das Präfix Preview gekennzeichnet.
Wir wollen uns nun ansehen, wie die Ereignisweiterleitung funktioniert.
1040
Ereignisse programmieren
21.1.2 Event Bubbling Das folgende Beispielprogramm definiert in Window ein StackPanel, das einen Button und eine ListBox enthält. Der Button hat ein untergeordnetes Element vom Typ Label. Die ListBox dient ausschließlich der Anzeige der Abläufe. Event Bubbling bedeutet, dass ein Ereignis nach der direkt betroffenen Komponente sukzessive an übergeordnete Komponenten weitergereicht wird, ähnlich wie Blasen, die in einer Flüssigkeit aufsteigen. Im folgenden Beispiel, dessen Ansicht Sie in Abbildung 21.1 sehen, testen wir das für das Ereignis MouseDown. Beachten Sie, dass dieses Ereignis sowohl für das Label als auch für den Button, das StackPanel und das Window definiert ist. Nach diesen Überlegungen muss ein Klick auf das Label bewirken, dass das Ereignis zuerst im Label, danach der Reihe nach im Button, dem StackPanel und zuletzt in Window verarbeitet werden. Wird nur der Button angeklickt, wird das Ereignis an das StackPanel und das Window weitergereicht. Sehen wir uns zuerst den XAML-Code an: '...\WPFControls\Ereignisse\Bubbling.xaml
Label1
Nun folgt noch der Code in der Code-Behind-Datei: '...\WPFControls\Ereignisse\Bubbling.xaml.vb
Partial Public Class Bubbling Private Sub Laden(sender As Object, e As RoutedEventArgs) Handles MyBase.Loaded button1.AddHandler(MouseDownEvent, _ New MouseButtonEventHandler(AddressOf Me.Klick), _ True) End Sub Private Sub Klick(sender As Object, e As MouseButtonEventArgs) If (TypeOf sender Is Button) Then : listBox1.Items.Add("BUTTON") ElseIf TypeOf sender Is Label Then : listBox1.Items.Add("LABEL") ElseIf TypeOf sender Is Menu Then : listBox1.Items.Add("MENU") ElseIf TypeOf sender Is StackPanel Then listBox1.Items.Add("STACKPANEL") ElseIf TypeOf sender Is Bubbling Then : listBox1.Items.Add("WINDOW")
1041
21.1
21
WPF-Steuerelemente
End If e.Handled = False End Sub End Class
Abbildung 21.1
Bubbling
Wie Sie in Abbildung 21.1: Bubbling sehen, wird bei einem Klick auf das Label das Ereignis durch die Hierarchie bis an das Wurzelelement Window weitergereicht. Das Ereignis MouseDown für den Button ist nicht in der XAML-Datei registriert, sondern durch die Methode AddHandler in der Code-Behind-Datei. button1.AddHandler(MouseDownEvent, _ New MouseButtonEventHandler(AddressOf Me.Klick), True)
Der Methode übergeben Sie zuerst den Ereignistyp, danach ein Delegate auf den Ereignishandler. Der boolesche Parameter gibt an, ob bereits behandelte Ereignisse weiterverarbeitet werden sollen. Für False wird – bei einem Klick auf Label – nur das Ereignis für das Label behandelt, alle anderen nicht mehr, da der Button bei dieser Einstellung die Ereigniskette unterbricht. Ein Klick auf den Button zeigt keine sichtbare Reaktion. Das MouseDown-Ereignis wird vom Button in ein Click-Ereignis umgemünzt, und Ersteres wird als behandelt markiert, bevor unser Ereignishandler aufgerufen wird und durch die Einstellung False die Behandlung ablehnt. Verwenden Sie statt Button ein Menu, werden beim Klick auf das Menü die Ereignisse für Menu, StackPanel und Window ausgelöst. Für den Wert True werden beim Klick auf den Button die Ereignisse für Button, StackPanel und Window ausgelöst. Die Weiterleitung von Ereignissen ist ein passiver Vorgang. Eine Komponente kann die Ereigniskette aktiv unterbrechen, die Weiterleitung dagegen läuft automatisch. Daher spielt es auch keine Rolle, ob sich jede Komponente in einer Hierarchie für ein Ereignis interessiert. Registriert ein »Zwischenelement«, beispielsweise das StackPanel, das Ereignis nicht, wird es einfach übersprungen, und es wird mit der darüberliegenden Komponente fortgefahren.
21.1.3 Event Tunneling Getunnelte Ereignisse verlaufen analog, nur die Richtung ist umgekehrt: vom obersten Element in der Hierarchie bis zum eigentlichen Ereignisempfänger. Im vorigen Beispiel müssen wir nur MouseDown durch PreviewMouseDown ersetzen. Da durch die umgekehrte Richtung beim Klick auf den Button er der letzte in der Hierarchie ist, kann auf eine besondere Form der Registrierung verzichtet werden, und sie erfolgt in der XAML-Datei.
1042
Ereignisse programmieren
'...\WPFControls\Ereignisse\Tunneling.xaml
Label1
'...\WPFControls\Ereignisse\Tunneling.xaml.vb
Partial Public Class Tunneling Private Sub Klick(sender As Object, e As MouseButtonEventArgs) If (TypeOf sender Is Button) Then : listBox1.Items.Add("BUTTON") ElseIf TypeOf sender Is Label Then : listBox1.Items.Add("LABEL") ElseIf TypeOf sender Is StackPanel Then listBox1.Items.Add("STACKPANEL") ElseIf TypeOf sender Is Tunneling Then : listBox1.Items.Add("WINDOW") End If End Sub End Class
Nun beginnen die Ereignisse im an oberster Stelle stehenden Window und pflanzen sich über StackPanel bis zum Auslöser durch. Bei Abbildung 21.2 wurde auf das Label geklickt, und ein Klick auf den Button löst die Kette Window, StackPanel, Button aus.
Abbildung 21.2
Tunneling
21.1.4 Weiterleitung abbrechen Bei einem Ereignis kann die Ereigniskette abgebrochen werden, indem der Eigenschaft Handled des EventArgs-Parameters der Wert True zugewiesen wird. Außerdem dürfen Ereignishandler nicht, wie der Button im Bubbling-Beispiel, den Abbruch unterdrücken (butt.AddHandler(Ereignis, Handler, True)). Um das zu testen, ändern Sie den Code des vorigen Beispiels durch Einfügen der fett gesetzten Zeile:
1043
21.1
21
WPF-Steuerelemente
'...\WPFControls\Ereignisse\Abbruch.xaml.vb
Partial Public Class Abbruch Private Sub Klick(ByVal sender As Object, ByVal e As MouseButtonEventArgs) If (TypeOf sender Is Button) Then : listBox1.Items.Add("BUTTON") ElseIf TypeOf sender Is Label Then : listBox1.Items.Add("LABEL") ElseIf TypeOf sender Is StackPanel Then e.Handled = True listBox1.Items.Add("STACKPANEL") ElseIf TypeOf sender Is Abbruch Then : listBox1.Items.Add("WINDOW") End If End Sub End Class
Da der Ereigniscode für das StackPanel die Ereigniskette als behandelt kennzeichnet, werden die Ereignisse von Button und Label nicht mehr behandelt.
21.1.5 Reihenfolge der Routed Events Sie können Tunneled- und Bubbling-Ereignisse gleichzeitig registrieren. Das folgende Beispiel zeigt die Reihenfolge weitergeleiteter Ereignisse. Zuerst das Layout: '...\WPFControls\Ereignisse\Reihenfolge.xaml
Nun der Visual Basic-Code in der Code-Behind-Datei mit der Registrierung der Handler: '...\WPFControls\Ereignisse\Reihenfolge.xaml.vb
Partial Public Class Reihenfolge Private Sub Registrieren(ByVal elem As FrameworkElement) elem.AddHandler(MouseDownEvent, _ New MouseButtonEventHandler(AddressOf Klick)) elem.AddHandler(PreviewMouseDownEvent, _ New MouseButtonEventHandler(AddressOf Klick)) If elem.Parent IsNot Nothing Then Registrieren(elem.Parent) End Sub
1044
Die Basisklasse Control
Private Sub Laden(ByVal sender As Object, ByVal e As RoutedEventArgs) _ Handles MyBase.Loaded Registrieren(Bild) End Sub Private Sub Klick(ByVal sender As Object, ByVal e As MouseButtonEventArgs) listBox1.Items.Add(e.RoutedEvent.Name & " – " & _ sender.GetType().Name) End Sub End Class
Auch hier werden die Abläufe in einer ListBox registriert. Abbildung 21.3 zeigt, dass zuerst die getunnelten Events ausgelöst werden, beginnend beim Fenster. Nachdem das auslösende Element Image erreicht ist, setzt sich die Ereigniskette mit den Bubbled Events bis zum Wurzelelement Reihenfolge fort.
Abbildung 21.3
21.2
Ereigniskette
Die Basisklasse Control
Die meisten WPF-Controls sind auf die gemeinsame Basisklasse Control im Namensraum System.Windows.Controls zurückzuführen. Verwechseln Sie dieses nicht mit dem gleichnamigen Steuerelement im Namensraum der Windows-Forms (System.Windows.Forms). In der folgenden Hierarchie beginnen alle Namensräume mit System. Object ÀÄWindows.Threading.DispatcherObject ÀÄWindows.DependencyObject ÀÄWindows.Media.Visual ÀÄWindows.UIElement ÀÄWindows.FrameworkElement ÀÄWindows.Controls.Control ÀÄWindows.Controls.ContentControl
Ein guter Einstiegspunkt ist DependencyObject, das die Handhabung der im Abschnitt 21.1.1, »Abhängige Eigenschaften«, besprochenen abhängigen Eigenschaften regelt. Davon abgeleitet ist die Klasse Visual. Sie unterstützt eine Komponente hinsichtlich Clipping und Rendering (Darstellung). Das ihm folgende Kind UIElement beschreibt die Verhaltenswei-
1045
21.2
21
WPF-Steuerelemente
sen hinsichtlich des Layouts und bietet viele Eigenschaften und Ereignisse, unter anderem auch für Maus und Tastatur. Die »Äußerlichkeiten« wie Abmessungen und Rahmen finden Sie in der von UIElement abgeleiteten Klasse FrameworkElement. Dessen Kind Control widmet sich den ersten Aspekten des Inhalts, zum Beispiel Ausrichtung, Padding, Farben und Font. Die Summe der Mitglieder dieser Vererbungshierarchie finden Sie in Control wieder. Da fast alle Steuerelemente auf dieser Klasse basieren, betrifft der Überblick in Tabelle 21.1 auch sie. Eigenschaft
Beschreibung
Background
Hintergrundfarbe
BorderBrush
Rahmenfarbe
Cursor
Mauszeiger
FontFamily
Schriftfarbe
FontSize
Schriftgröße
FontStyle
Schriftstil (fett, kursiv,…)
Foreground
Vordergrundfarbe
HorizontalAlignment VerticalAlignment
Horizontale bzw. vertikale Ausrichtung bezüglich des übergeordneten Elements
HorizontalContentAlignment VerticalContentAlignment
Horizontale/vertikale Ausrichtung der Komponenteninhalte
Height bzw. Width
Höhe bzw. Breite
Margin
Außenrand der Komponente
Name
Bezeichner der Komponente
Opacity
Transparenz
Padding
Innenabstand
Parent
Referenz auf das übergeordnete Element
Resources
Verweis auf Ressourcen
Style
Verweis auf eine Stildefinition
TabIndex
Index in der Tabulatorreihenfolge
Tag
Frei verfügbare Eigenschaft vom Typ Object
ToolTip
Hinweisfenster, das an der Komponente angezeigt wird
Visibility
Sichtbarkeit des Controls
Tabelle 21.1
21.3
Eigenschaften in »Control« (inklusive vererbte, Auswahl)
Familie der Buttons
Zu den wichtigsten Steuerelementen gehören die Schaltflächen. Die Basisklasse aller Buttons ist ButtonBase, in der alle gemeinsamen Mitglieder definiert sind. Von ihr sind drei ButtonControls abgeleitet, die in Tabelle 21.2 aufgeführt sind.
1046
Familie der Buttons
Button-Typ
Beschreibung
Button
Eine Standardschaltfläche
RepeatButton
Löst während des Drückens kontinuierlich Click-Ereignisse aus.
ToggleButton
Löst ein einzelnes Click-Ereignis aus. Bei jedem Klick wird zwischen »gedrückt« und »nicht gedrückt« umgeschaltet.
Tabelle 21.2
Schaltflächen der Basisklasse »ButtonBase«
Wann das Ereignis Click ausgelöst wird, bestimmt die Eigenschaft ClickMode vom Typ der gleichnamigen Enumeration (siehe Tabelle 21.3). Die Art der Benutzerführung wird dadurch bestimmt. Konstante
Ereignisauslösung
Release
Wenn auf den Button geklickt und die Maustaste losgelassen wird (Standard)
Press
Beim Herunterdrücken der Maustaste
Hover
Wenn der Mauszeiger das Steuerelement betritt
Tabelle 21.3
Die Enumeration »ClickMode«
21.3.1 Button Button ist eine einfache Schaltfläche, die auf Click-Ereignisse reagiert. Die zusätzlichen Eigen-
schaften IsCancel und IsDefault legen fest, ob die Schaltfläche auch auf die (Esc)- und die (¢)-Taste reagiert. In einem Dialog wird IsCancel=True in der Regel für die Abbrechen-Schaltfläche gesetzt. Ist der Dialog modal, wird er daraufhin geschlossen, und es werden keine Eingaben an das aufrufende Fenster übermittelt. Analog wird für die OK-Schaltfläche IsDefault=True gesetzt. Ein modaler Dialog wird geschlossen, und die Eingaben werden verarbeitet.
21.3.2 RepeatButton Der Unterschied zu Button ist, dass dieser Schaltflächentyp kontinuierlich Click-Ereignisse auslöst, solange der Mauszeiger bei gedrückter Maustaste auf die Schaltfläche zeigt. Die Zeitspanne vom Drücken bis zur ersten Click-Auslösung wird von Delay in Millisekunden festgelegt und die Zeitspanne zwischen den sich wiederholenden Ereignissen von Interval ebenfalls in Millisekunden. Sinnvoll ist dieses Steuerelement, wenn Werte kontinuierlich erhöht oder verringert werden sollen. Damit können Sie beispielsweise ein eigenes »Up-Down«-Steuerelement entwickeln.
21.3.3 ToggleButton Dieses Steuerelement ist eng mit der CheckBox verwandt. Es beschreibt seinen Zustand durch die Eigenschaft IsChecked mit den Werten True, False oder Nothing. Letzterer tritt auf, wenn
1047
21.3
21
WPF-Steuerelemente
zur Beschreibung eines Zwischenzustandes IsThreeState=True gesetzt ist. Wird auf eine solche Schaltfläche geklickt, wird IsChecked zuerst auf True gesetzt, beim nächsten Klick auf Nothing und beim dritten auf False. Typischerweise verwenden Sie ToggleButton in Symbolleisten. Beim Zustandswechsel treten die Ereignisse Checked und Unchecked auf, wenn IsChecked die Werte True und False annimmt. Den Wert Nothing erfassen diese Ereignisse nicht. ToggleButton ...
In der Code-Behind-Datei können Sie die Ereignishandler programmieren, zum Beispiel: Private Sub toggleButton1_Checked(sender As Object, e As RoutedEventArgs) MessageBox.Show("Aktueller Zustand: Checked") End Sub Private Sub toggleButton1_Unchecked(sender As Object, e As RoutedEventArgs) MessageBox.Show("Aktueller Zustand: Unchecked") End Sub
Alternativ kann der Zustand auch über das Click-Ereignis ermittelt werden – und zwar für alle drei Zustände: Private Sub toggleButton1_Click(sender As Object, e As RoutedEventArgs) MessageBox.Show("Zustand " & If(T.IsChecked, "Checked", _ If(T.IsChecked Is Nothing, "Nothing", "False"))) End Sub
21.4
Textboxen
Die WPF enthält drei Steuerelemente für Texteingaben: 왘
TextBox
왘
RichTextBox
왘
PasswordBox
21.4.1 TextBox Die Eigenschaft Text einer TextBox speichert die Zeichenfolge in dieser Textbox. Standardmäßig können in einer Textbox keine Zeilenvorschübe eingeben werden (ein zugewiesener Text kann mehrere Absätze enthalten). Die Zeilenlänge ist unbeschränkt, aber zur Navigation stehen keine Rollbalken zur Verfügung. Um Zeilenvorschübe mit der (Enter)-Taste eingeben zu können, setzen Sie die Eigenschaft AcceptsReturn auf den Wert True. Die Darstellung von Zeilen, die länger als die Textbox sind, steuern Sie mit der Eigenschaft TextWrapping vom Typ der gleichnamigen Enumeration (siehe die Tabelle 21.4).
1048
Label
Konstante
Beschreibung
NoWrap
Keine automatischen Zeilenumbrüche
Wrap
Zeilen werden bei Bedarf am Ende der Box umbrochen, bevorzugt bei Leerzeichen, notfalls aber auch innerhalb eines Wortes.
WrapWithOverflow
Umbruch nur bei Leerzeichen. Wörter, die länger als die Textbox sind, werden abgeschnitten (nur die Darstellung).
Tabelle 21.4
Die Enumeration »TextWrapping«
Mit HorizontalScrollBarVisibility und VerticalScrollBarVisibility legen Sie fest, ob bei Textüberlauf ein horizontaler bzw. vertikaler Rollbalken angezeigt wird. Horizontale Rollbalken erscheinen nur, wenn TextWrapping = NoWrap gesetzt ist. Mit Disabled unterbinden Sie sogar das Rollen mit den Cursortasten. Eine sehr interessante Eigenschaft ist SpellCheck.IsEnabled. Mit True wird eine interne Rechtschreibüberprüfung eingeschaltet, die alle missverständlichen Wörter rot unterstreicht. Das Kontextmenü eines markierten Worts bietet Korrekturvorschläge an. Es wird das Wörterbuch verwendet, auf das auch Microsoft Office zurückgreift.
Über die genannten Eigenschaften hinaus stehen auch eine Reihe von Methoden zur Verfügung, beispielsweise Cut, Copy, Paste, Undo und Redo.
21.4.2 PasswordBox Das Steuerelement PasswordBox wurde zur Eingabe eines Passworts entworfen. Es ist deutlich einfacher in der Handhabung als eine TextBox, da viele Eigenschaften und Methoden nicht angeboten werden. Der vom Benutzer eingegebene Text wird nicht angezeigt, die einzelnen Buchstaben werden durch ein Maskierungszeichen ersetzt. Dieses wird mit der Eigenschaft PasswordChar festlegt; voreingestellt ist ein fetter Punkt. Die eingegebene Zeichenfolge ist in der Eigenschaft Password gespeichert.
21.5
Label
Ein Label dient dazu, Text anzuzeigen. Darüber hinaus werden von diesem Steuerelement auch Zugriffstasten unterstützt und die Bindung an andere, spezifizierte Steuerelemente. Beim Druck auf eine Zugriffstaste erhält das in der Eigenschaft Target angegebene Steuerelement den Fokus. So können Sie über die Tastatur ein mit einem Label »benanntes« Element fokussieren. Die Zugriffstaste wird durch einen Unterstrich vor dem passenden Buchstaben im Labeltext kenntlich gemacht. Soll innerhalb der Zeichenfolge ein Unterstrich stehen, müssen Sie zwei angeben. Drücken Sie zur Laufzeit die (Alt)-Taste, wird der Buchstabe der Zugriffstaste unterstrichen. Durch gleichzeitigen Druck auf die Zugriffstaste springt der Fokus.
1049
21.5
21
WPF-Steuerelemente
Im folgenden Beispiel wird durch den Druck auf (Alt) + (W) die Textbox txtWohnort fokussiert. Die Ansicht des Beispiels zeigt Abbildung 21.4. _Wohnort:
Abbildung 21.4
21.6
Fokussierung über Zugriffstaste
Auswahlelemente
21.6.1 CheckBox CheckBox und der weiter oben besprochene ToggleButton sind nahe Verwandte. CheckBox
stellt einen booleschen Wert grafisch dar. Tatsächlich ist CheckBox sogar von ToggleButton abgeleitet, sodass sich beide im Wesentlichen nur durch ihr Design unterscheiden.
21.6.2 RadioButton Auch RadioButton ist von ToggleButton abgeleitet. Besonders beim RadioButton ist seine Zuordnung zu einer bestimmten Gruppe von Optionsschaltern. Innerhalb dieser Gruppe kann immer nur ein RadioButton-Element aktiviert sein. Zur Gruppierung haben Sie zwei Möglichkeiten. Alle RadioButton-Steuerelemente mit demselben Wert der Eigenschaft GroupName gehören zur selben Gruppe. Andererseits fasst der übergeordnete Container alle Elemente ohne eine Spezifikation des Gruppennamens implizit zu einer eigenen Gruppe zusammen. Im folgenden Codefragment sind die beiden Gruppen A und B definiert. Drei RadioButton gehören der Gruppe A an und beschreiben eine Altersgruppenzugehörigkeit, zwei ermöglichen die Auswahl des Geschlechts (siehe Abbildung 21.5). Altergruppe 0 – 10 Altergruppe 11 – 20 Altergruppe 21- 30 Männlich Weiblich
Abbildung 21.5
21.7
Gruppierung von RadioButton
Listen
21.7.1 ListBox Eine ListBox zeigt eine Liste, in der der Anwender ein oder mehrere Elemente markieren kann. Standardmäßig erlaubt eine ListBox nur eine Einfachauswahl. Setzen Sie die Eigenschaft SelectionMode auf Multiple, können beliebig viele Elemente durch einen einfachen Klick gewählt werden. Bei der Einstellung Extended drückt der Anwender beim Klicken des Listenelements die (ª)-Taste für zusammenhängende Bereiche und die (Strg)-Taste zur Auswahl zusätzlicher einzelner Elemente. Eine klassische ListBox verwendet für jedes Element die Klasse ListBoxItem. Peter Franz Rolf Hans-Günther
Statt einer Liste von ListBoxItem-Elementen können Sie auch andere Steuerelemente auflisten. Im folgenden Codefragment sind dies CheckBox-Steuerelemente (siehe Abbildung 21.6). Das Ankreuzen der Kästchen ist unabhängig von der Selektion von Listenelementen (ganze Zeilen). Peter Franz Rolf Hans-Günter
Sie können der ListBox sowohl im XAML-Code als auch in der Code-Behind-Datei Listenelemente hinzufügen. Die Eigenschaft Items liefert eine Referenz auf die Listenelemente. Durch Aufruf der Methode Add fügen Sie nach Bedarf Elemente hinzu.
1051
21.7
21
WPF-Steuerelemente
Abbildung 21.6
Checkboxliste
Dim chk1 As New CheckBox() chk1.Content = "Beate" chk1.Margin = New Thickness(3) Dim chk2 As New CheckBox() chk2.Content = "Gudrun" chk2.Margin = New Thickness(3) ListBox1.Items.Add(chk1) ListBox1.Items.Add(chk2)
Beachten Sie, dass die Eigenschaft Margin vom Typ Thickness ist.
Zugriff auf ausgewählte Elemente Das bzw. die ausgewählten Elemente werden in der Regel im Programmcode für weitere Operationen benutzt. Die ListBox hat die in Tabelle 21.5 gezeigten Eigenschaften zum Zugriff auf die Selektion. Eigenschaft
Beschreibung
SelectedIndex
Index des ersten Elements in der aktuellen Auswahl oder –1 bei leerer Auswahl. Durch Zuweisung wird ein Element selektiert.
SelectedItem
Erstes Element in der aktuellen Auswahl oder Nothing bei leerer Auswahl
SelectedItems
Ruft alle ausgewählten Elemente ab.
Tabelle 21.5
R
Selektion in einer »ListBox« (R = ReadOnly)
Mit der folgenden Zuweisung wird das erste Listenelement selektiert. Geschieht dies während der Programminitialisierung, startet die Anwendung mit dieser Vorselektierung. ListBox1.SelectedIndex = 0
Um mehrere Elemente der Reihe nach auszuwerten, eignet sich SelectedItems. Sie können die Liste beispielsweise in einer For Each-Schleife durchlaufen. Private Sub btnShowItem_Click(sender As Object, e As RoutedEventArgs) Dim items As string = "" For Each item As ListBoxItemin ListBox1.SelectedItems items += item.Content & Environment.NewLine Next MessageBox.Show(items) End Sub
1052
Menüleiste
21.7.2 ComboBox Die ComboBox ähnelt einer ListBox mit Einfachauswahl, jedoch zeigt die ComboBox, außer während der Selektion, nur ein Element an und nimmt somit nicht viel Platz in Anspruch. Mit dem Standardwert False von IsEditable kann der Anwender keine neuen ComboBox-Einträge hinzufügen. Erlauben Sie mit True die Erweiterung, sollten Sie im Programmcode sicherstellen, dass die neuen Elemente nicht »verloren gehen«. Jedes Element in einer ComboBox ist vom Typ ComboBoxItem. Berlin Hamburg Bremen Düsseldorf Dresden München
Die Eigenschaft Items liefert eine Referenz auf die Einträge in Form einer ItemCollection. Im Programmcode erweitern Sie die ComboBox mit deren Add-Methode. comboBox1.Items.Add("Stuttgart")
Die textuelle Repräsentation des ausgewählten Elements steht in der Eigenschaft Text. Bei einfachen Einträgen lässt sich mit dieser Eigenschaft auch die Auswahl festlegen. Ohne Einschränkung können Sie mit SelectedIndex den Index des gewünschten Elements angeben. Zwei Ereignisse sind spezifisch für die ComboBox: DropDownOpened wird beim Öffnen der Liste ausgelöst und DropDownClosed bei deren Schließen.
21.8
Menüleiste
Die Menüleiste wird durch die Klasse Menu beschrieben, die untergeordneten Menüpunkte durch MenuItem. Trennstriche werden durch Separator beschrieben. Eine typische Struktur zeigt das nächste Codefragment:
1053
21.8
21
WPF-Steuerelemente
Menüs stehen meistens am Fensterrand. Zur Positionierung bietet sich das DockPanel an. Im Menü selbst wird die Eigenschaft DockPanel.Dock auf Top festgelegt. Um den verbleibenden Bereich auszufüllen, ist im Code nach Menu noch ein StackPanel. Das Hauptmenü bilden die Menüelemente auf oberster Ebene, direkt unterhalb von Menu. Jedes Menüelement ist vom Typ MenuItem und kann selbst wieder Menüelemente enthalten, in XAML als Kindelemente des MenuItem-Elements. Den Namen eines Menüelements legt das Attribut Header fest. So wie bei anderen Steuerelementen auch, kann mit einem Unterstrich ein Access-Key festgelegt werden. In Abbildung 21.7 sehen Sie die Ausgabe des XAML-Codes.
Abbildung 21.7
Menü
Die Programmierung erfolgt wie üblich. Die Funktionalität der Menüelemente wird in den Handlern ihrer Click-Ereignisse codiert.
21.8.1 Gestaltung der Menüelemente Von den vielen Eigenschaften der Klasse MenuItem greife ich hier in Tabelle 21.6 exemplarisch vier heraus.
1054
Menüleiste
Eigenschaft
Beschreibung
Icon
Ein neben dem Menütext angezeigtes Bild
IsCheckable
Gibt an, ob ein MenuItem markiert werden kann und ein Häkchen bekommt.
IsChecked
Gibt an, ob ein MenuItem markiert ist.
InputGestureText
Beschreibt die Tastenkombination.
Tabelle 21.6
Eigenschaften der Klasse »MenuItem«
21.8.2 Symbole anzeigen Die Eigenschaft Icon spezifiziert ein neben dem Menütext anzuzeigendes Bild vom Typ Image. Die Bilddatei gibt dessen Attribut Source an. Das Bild binden Sie mit der verbundenen Eigenschaft MenuItem.Icon an. Im folgenden Codeabschnitt wird die oben gezeigte Menüleiste um zwei Symbole ergänzt (siehe Abbildung 21.8). ’ ...
21.8.3 Tastenkürzel Um ein Menüelement über die Tastatur ansprechen zu können, setzen Sie vor einen Buchstaben einen Unterstrich. Zur Laufzeit selektiert die Kombination (Alt)-Buchstabe das Menüelement. Mit der Eigenschaft InputGestureText definieren Sie ein Tastenkürzel zur Aktivierung eines Menüelements, das im Menü angezeigt wird (siehe Abbildung 21.8). ...
Noch reagiert das Menüelement nicht auf das zuletzt definierte Tastenkürzel. Es muss noch mit einem Kommando verbunden werden, das wir im Abschnitt 22.5, »Kommandos«, besprechen.
1055
21.8
21
WPF-Steuerelemente
Abbildung 21.8
Menü mit Symbolen und Shortcuts
21.8.4 Selektierbare Menüelemente Manche Menüelemente sind Ein-/Ausschaltern ähnlich. Sie signalisieren ihren augenblicklichen Zustand durch ein Häkchen. Solch ein Menüelement erzeugen Sie mittels der Eigenschaft IsCheckable, der aktuelle Zustand ist in IsChecked gespeichert.
Ist für ein Menüelement IsEnabled auf False gesetzt, ist es »ausgegraut« und inaktiv.
21.9
Kontextmenü
Kontextmenüs sind ähnlich der eben vorgestellten Menüleiste. Der wesentliche Unterschied ist, dass ein Kontextmenü dem entsprechenden Steuerelement zugeordnet werden muss. Das folgende Beispiel zeigt einen Button mit einem Kontextmenü:
21.10
Symbolleisten
Symbolleisten vom Typ ToolBar sind einfach aufgebaut. Es sind Container für Steuerelemente. Normalerweise ist eine Symbolleiste unterhalb der Menüleiste angedockt.
1056
Symbolleisten
Arial Courier Windings
Beim Verkleinern des Fensters kann es passieren, dass die Fensterbreite nicht mehr ausreicht, um alle in einer ToolBar enthaltenen Komponenten anzuzeigen. Es wird dann ein Überlaufbereich erzeugt, an dessen Ende ein Button mit einem Pfeil angezeigt wird. Über diese Schaltfläche lässt sich ein Menü aufklappen, in dem die nicht mehr darstellbaren Elemente angezeigt werden. Das Überlaufverhalten der Steuerelemente legt die zugeordnete Eigenschaft OverflowMode vom Typ der gleichnamigen Enumeration fest (siehe Tabelle 21.7). Konstante
Beschreibung
Always
Das Steuerelement wird immer im Überlaufbereich angezeigt.
AsNeeded
Das Steuerelement wird bei Bedarf im Überlaufbereich angezeigt.
Never
Das Steuerelement wird nie im Überlaufbereich angezeigt.
Tabelle 21.7
Die Enumeration »OverflowMode«
Der folgende Code definiert eine Symbolleiste mit drei ComboBox-Steuerelementen. Jeder ist eine andere Einstellung der Eigenschaft OverflowMode zugewiesen. Arial
1057
21.10
21
WPF-Steuerelemente
Courier Windings Bonn München Nürnberg Test1 Test2 Test3
Abbildung 21.9 zeigt die Auswirkungen der Einstellungen. Das Kombinationslistenfeld mit der Einstellung OverflowMode=Always ist auch dann nur über die Dropdown-Schaltfläche in der Symbolleiste zu erreichen, wenn die Breite der Form zur Darstellung ausreichen würde. Wird die Fensterbreite stark verringert, wird nur noch die ComboBox in der Symbolleiste angezeigt, deren Einstellung OverflowMode=Never lautet.
Abbildung 21.9
Einfluss von »OverflowMode«
21.10.1 Positionieren mit der Komponente ToolBarTray Die Komponente ToolBarTray ist ein Container für ToolBar-Steuerelemente. In einem ToolBarTray ist es möglich, Symbolleisten hintereinander oder in mehreren Reihen anzuzeigen und mittels Drag&Drop zu verschieben. Zur Positionierung stellt das ToolBar-Steuerelement die Eigenschaften Band und BandIndex zur Verfügung. Mit Band geben Sie an, in welcher Zeile die ToolBar erscheinen soll. Mit BandIndex legen Sie deren Position innerhalb der Zeile fest. ... ...
1058
Statusleiste
... ...
Die Einstellungen legen die Darstellung der ToolBars nach dem Starten des Fensters fest. Abbildung 21.10 zeigt ein Beispiel. Zur Laufzeit kann der Anwender die Position nach Belieben mittels Drag&Drop verändern.
Abbildung 21.10
21.11
»ToolBarTray«
Statusleiste
Die meisten Statusleisten werden unten im Fenster angezeigt und informieren den Anwender über den Zustand des laufenden Programms. In WPF heißt die Statusleiste StatusBar. Sie können in die StatusBar beliebige Komponenten direkt einfügen, zum Beispiel TextBox oder Label. Besser ist es, mit StatusBarItem-Elementen Bereiche zu definieren, in die die Komponenten eingebettet sind. Das macht die Ausrichtung der Komponenten einfach, zum Beispiel mit den Eigenschaften HorizontalAlignment oder auch VerticalAlignment. ... ... Start Suchen: Suchbegriff
1059
21.11
21
WPF-Steuerelemente
Anzahl: 2
Abbildung 21.11 zeigt die Statusleiste des Beispiels.
Abbildung 21.11
21.12
Statusleiste
Bilder mit Image
In einigen Beispielen haben wir schon ein Image-Control eingebaut. In der wichtigsten Eigenschaft Source steht der relative oder absolute Pfad einer Grafikdatei. Image ermöglicht das Laden der folgenden Bildtypen: .bmp, .gif, .ico, .jpg, .png, .wdp und
.tiff. Unterscheiden sich die Größen des Image-Controls und der Grafik, können Sie mit den Eigenschaften Stretch und StretchDirection festlegen, wie die Grafik gestreckt werden soll. StretchDirection erlaubt die Werte Both, DownOnly und UpOnly. Abbildung 21.12 zeigt, wie sich das Strecken eines Bildes auf die Darstellung auswirkt. Der Abbildung liegt der folgende Code zugrunde:
Abbildung 21.12
1060
Bildstreckung
Bilder mit Image
21.12.1 Grafik zur Laufzeit laden Mit der Klasse BitmapImage im Namensraum System.Windows.Media.Imaging laden Sie ein Bild im Code. Der Ladevorgang wird mit der Methode BeginInit initialisiert und mit EndInit abgeschlossen. Das BitmapImage-Objekt spezifiziert in der Eigenschaft UriSource vom Typ Uri die Datenquelle. Dem Uri-Konstruktor übergeben Sie den Pfad zu der Bitmap und die Information, ob es sich um eine relative oder absolute Pfadangabe handelt. Der Code in der XAML-Datei ist sehr kurz:
Geladen wird das Bild beim Laden des Fensters im Handler des Ereignisses Loaded. Private Sub Window_Loaded(sender As Object, e As RoutedEventArgs) Dim bitmap As New BitmapImage() bitmap.BeginInit() bitmap.UriSource = New Uri("Bilder/JungerHund.jpg", UriKind.Relative) bitmap.EndInit() MyImage.Source = bitmap End Sub
1061
21.12
Die Beherrschung dynamisch aktualisierter Steuerelemente und die Erzeugung eines einheitlichen Designs schaffen professionelle Anwendungen, deren Funktionalität durch Kommandos leichter zu organisieren ist.
22
Konzepte von WPF
22.1
Abhängige und angehängte Eigenschaften
WPF erweitert die Eigenschaften der Common Language Runtime (CLR) um zwei besondere Gruppen: 왘
Abhängige Eigenschaften (Dependency Properties)
왘
Angehängte Eigenschaften (Attached Properties)
Beide werden ähnlich wie »normale« Eigenschaften verwendet, aber anders deklariert.
22.1.1 Abhängige Eigenschaften Stellen Sie sich vor, im Auslieferungslager eines Unternehmens befindet sich der Artikel A. Mit dem Artikel ist ein bestimmter Verkaufspreis verknüpft. Wird der Artikel ins Ausland verkauft, in dem eine andere Währung Zahlungsmittel ist, muss der Verkaufspreis dynamisch an den aktuellen Wechselkurs angepasst werden. Damit ist der Verkaufspreis eine abhängige Eigenschaft. Abhängige Eigenschaften haben gegenüber herkömmlichen Eigenschaften einige Vorteile: 왘
Die Werte werden automatisch aktualisiert.
왘
Zur Signalisierung von Wertänderungen werden Callback-Methoden verwendet.
왘
Es kann eine interne Validierung erfolgen.
왘
Es können Standardwerte definiert werden.
Die Einführung von abhängigen Eigenschaften war notwendig, um die vielen WPF-Features realisieren zu können, da einige davon abhängen, ob sich im System oder in der Anwendung Eigenschaftswerte verändert haben. Zu den Features, die grundlegend auf abhängige Eigenschaften angewiesen sind, gehören beispielsweise die Datenbindung und Animationen. Das folgende Beispiel zeigt die Definition einer abhängigen Eigenschaft. Die Klasse Linie ist von DependencyObject abgeleitet. Class Linie : Inherits DependencyObject ' abhängige Eigenschaft selbst Public Shared ReadOnly LengthProperty As DependencyProperty
1063
22
Konzepte von WPF
' Registrieren der abhängigen Eigenschaft Shared Sub New() Linie.LengthProperty = DependencyProperty.Register( _ "Length", GetType(Integer), GetType(Line), _ New FrameworkPropertyMetadata(10, _ New PropertyChangedCallback(AddressOf Linie.OnLengthChanged))) End Sub ' Schnittstelle der Eigenschaft nach draußen Public Property Length() As Integer Get Return CType(GetValue(Linie.LengthProperty), Integer) End Get Set(ByVal value As Integer) SetValue(Linie.LengthProperty, value) End Set End Property ' Callback-Methode Private Shared Sub OnLengthChanged(ByVal dpObj As DependencyObject, _ ByVal e As DependencyPropertyChangedEventArgs) ' ... End Sub End Class
Abhängige Eigenschaften sind vom Typ DependencyProperty. Sie müssen als öffentliche, klassengebundene Felder definiert werden. Die Verwaltung abhängiger Eigenschaften wird vom WPF-Subsystem übernommen. Aus diesem Grund müssen abhängige Eigenschaften mit der Methode Register registriert werden. Die Methodenparameter sind der Name der Eigenschaft (hier: Length), der Typ der Eigenschaft und die Klasse, die die abhängige Eigenschaft beherbergt. Zudem werden bei der Registrierung ein Standardwert und eine Rückrufmethode festgelegt. Der Zugriff auf die Eigenschaft erfolgt über GetValue und SetValue, die öffentliche Methoden der Klasse DependencyObject sind. Vor dem ersten Aufruf von SetValue gibt GetValue den Standardwert zurück. Sie dürfen GetValue und SetValue auch direkt benutzen und gar keine Eigenschaft definieren. Von der Callback-Methode schließlich werden alle Wertänderungen überwacht.
22.1.2 Angehängte Eigenschaften In hierarchischen Datenstrukturen besteht eine Beziehung zwischen den verschiedenen Ebenen, zum Beispiel wie ein Container seine Komponenten anordnet. Dies erfordert eine Koordination durch das übergeordnete Element, und die notwendige Information, zum Beispiel der Randabstand, wird im Elternelement gespeichert. Im XAML-Code könnte dies als Liste im Elternelement realisiert sein mit einem Eintrag für jedes Kindelement. Dann müsste aber bei jeder Änderung des Containerinhalts diese Liste manuell mit synchronisiert werden. Eleganter ist die Spezifikation der Eigenschaft im Kindelement unter Angabe des betroffenen Elternelements. Als Nebeneffekt bleibt dieses übersichtlicher.
1064
Ressourcen
Diese Spezifikation wird angehängte Eigenschaft (Attached Property) genannt. In WPF sind diese Eigenschaften meistens gleichzeitig abhängige Eigenschaften. In der folgenden Syntax sind die kursiv gesetzten Teile kontextabhängig: Elternelement.Eigenschaft="Wert" ...
Hinweis Durch angehängte Eigenschaften haben viele Komponenten mehr Eigenschaften, als aus der Klassendefinition ersichtlich ist.
Im folgenden Beispiel wird eine TextBox in einem Grid positioniert. Die Eigenschaften Grid.Column und Grid.Row werden der TextBox vom Grid zur Verfügung gestellt. Hallo
Sie können die Position der TextBox auch mittels Code festlegen, müssen dann aber die Eigenschaften SetColumn und SetRow des Grid-Controls aufrufen (und nicht der TextBox): Grid.SetColumn(txtText, 1) Grid.SetRow(txtText, 2)
22.2
Ressourcen
Wenn Sie alle Daten fest im Code zu verankern, ist eine Anwendung unflexibel und der Quelltext oft schwer lesbar (zum Beispiel eine Inline-Bitmap). Für einige Daten ist es oft besser, statt der Daten im Code einen eindeutigen Schlüssel (Key) zu verwenden und die eigentlichen Daten an einer anderen Stelle zu speichern. Solche externen (bezüglich der Stelle im Code) Daten werden Ressourcen genannt und sind später austauschbar, ohne die Stelle im Code ändern zu müssen, da dort der Schlüssel und nicht die Daten selbst verwendet werden. Typische Ressourcen sind Füllmuster, geometrische Objekte oder Stile. Eine enorme Arbeitserleichterung ergibt sich, wenn dieselben Daten mehrfach verwendet werden. Im Code steht für diese immer derselbe Schlüssel. Eine Änderung eines einzigen Datums in der Ressource wirkt sich automatisch auf alle Stellen aus, die es über den Schlüssel referenzieren. Das macht ein einheitliches Design einfach, denn alle Elemente beziehen sich auf dieselben Daten, und Änderungen erfolgen nur an einer Stelle. An die Stelle einer Programmierung tritt eine Konfiguration. Ressourcen lassen sich nahezu jedem Objekt hinzufügen und müssen nicht an einer zentralen Stelle verwaltet werden. Um eine Ressource anwendungsweit bereitzustellen, definieren Sie sie in der Datei Application.xaml, in der bereits ein Abschnitt für Ressourcen steht.
1065
22.2
22
Konzepte von WPF
Auf die hier definierten Ressourcen können alle Elemente der Anwendung zugreifen, sowohl alle Fenster als auch alle Steuerelemente. Ein in Window explizit angegebener Ressourcenbereich ist dagegen nur innerhalb des Fensters gültig.
Die Suche nach den Daten zu einem Ressourcenschlüssel erfolgt vom Speziellen zum Allgemeinen. Zuerst wird in der Komponente gesucht, die den Schlüssel verwendet, zum Beispiel Button. Hat sie keinen Ressourcenabschnitt oder ist dort kein passender Schlüssel, setzt sich die Suche in der übergeordneten Komponente fort, zum Beispiel Grid. Die Analyse übergeordneter Komponenten kann bis zur Ebene der Anwendung selbst erfolgen. Ist auch dort kein passender Schlüssel, findet sich immer etwas Passendes in den System-Ressourcen. Wird ein passender Schlüssel auf einer Ebene gefunden, wird der zugehörige Wert verwendet und nicht weitergesucht. Mit den bisherigen Beispielen ergibt sich eine maximale Suchkette Button, Grid, Window, Application, Laufzeitumgebung bzw. Betriebssystem.
22.2.1 Definition Nachdem wir geklärt haben, wo Ressourcen definiert werden, widmen wir uns nun dem Wie. Im folgenden Beispiel sind zwei Ressourcen im Ressourcenbereich des Window definiert. Eine Ressource beschreibt einen blauen Farbton, die zweite einen gelben. Vier Schaltflächen sind im Fenster, zwei greifen auf beide Ressourcen zu, einer nutzt nur eine Ressource, und der letzte nutzt keine (siehe Abbildung 22.1).
1066
Ressourcen
'...\WPFKonzepte\Ressourcen\Farben.xaml
CornflowerBlue Button1 Button2 Button3 Button3
Abbildung 22.1
Farben als Ressourcen
Jede Ressource muss einen eindeutigen Schlüssel haben, unter dem sie aufrufbar ist. In unserem Beispiel lauten die beiden Schlüssel: x:Key="blau" x:Key="gelb"
Ressourcenschlüssel hängen von der Groß-/Kleinschreibung ab. Die gewünschte Ressource wird in geschweiften Klammern angegeben, der Namensbestandteil Ressource ist optional (siehe Abschnitt 19.3.6, »Indirektion und Markup-Erweiterungen«): Background="{StaticResource blau}"
Die Elementschreibweise ist für komplexe Eigenschaften nötig (siehe Abschnitt 19.3.4, »Attribute«). Button3
1067
22.2
22
Konzepte von WPF
22.2.2 Zeitpunkt der Wertbestimmung Der Wert einer Ressource kann zu zwei Zeitpunkten bestimmt werden: einmalig beim Laden der Anwendung oder dynamisch nach Bedarf.
Statische Ressourcen Wird eine Ressource statisch an eine Eigenschaft gebunden, wird der Wert der Ressource nur ein einziges Mal ausgewertet und der Eigenschaft zugewiesen. Ändert sich danach eine Ressource, wird der neue Wert nicht mehr berücksichtigt. Eine solche Anbindung erfolgt mit StaticResource. Diese Markup-Erweiterung wird zusammen mit dem eindeutigen Schlüssel der Ressource in geschweiften Klammern der Eigenschaft übergeben. Existiert die Ressource nicht, wird eine Ausnahme ausgelöst.
Dynamische Ressourcen Um Änderungen der Ressource zur Laufzeit zu erfassen, müssen Sie sie dynamisch an die Eigenschaft binden. Dynamische Ressourcen werden von WPF automatisch auf Änderungen hin geprüft. Das kostet Rechenzeit. Extrem viele dynamische Bindungen machen eine Anwendung träge. Der Wert einer dynamischen Ressource muss erst dann bekannt sein, wenn er gebraucht wird. Fehlende Werte dynamischer Ressourcen werden einfach ignoriert. Wird während der Laufzeit ein Wert erzeugt (oder geändert), aktualisiert WPF die Eigenschaft automatisch. Die Festlegung dynamischer Ressourcen kann im Programmcode erfolgen. Im folgenden Beispiel wird der Hintergrund einer Schaltfläche beim Klicken auf diese durch die Erzeugung einer dynamischen Ressource im Code des Ereignishandlers geändert (siehe Abbildung 22.2). Die Ressource wird im Window-Objekt abgelegt. '...\WPFKonzepte\Ressourcen\Dynamisch.xaml
Button1
'...\WPFKonzepte\Ressourcen\Dynamisch.xaml.vb
Partial Public Class Dynamisch Private Sub Klick(ByVal sender As Object, ByVal e As RoutedEventArgs) Dim col As New LinearGradientBrush(Colors.Yellow, Colors.Blue, 0) Me.Resources.Remove("hintergrund") ' nötig ab dem zweiten Klick Me.Resources.Add("hintergrund", col) End Sub End Class
1068
Stile
Abbildung 22.2 Dynamische Ressourcenbindung
22.2.3 Abrufen von Systemressourcen Bisher haben wir nur selbst definierte Ressourcen verwendet. Aber auch das Betriebssystem stellt Ressourcen zur Verfügung. WPF macht einen Teil davon über die klassengebundenen schreibgeschützten Eigenschaften dreier Klassen zugänglich: 왘
SystemColors: abstrakte Farben wie MenuColor
왘
SystemFonts: abstrakte Font-Informationen wie MenuFontFamily, IconFontSize
왘
SystemParameters: Verschiedenes wie IconWidth, VirtualScreenTop, IsTabletPC
Auf jede der Ressourcen können Sie auf zwei Arten zugreifen. Eigenschaften ohne das Suffix Key liefern direkt einen Wert, während die Variante mit Key eine Referenz auf den eigentlichen Wert darstellt. Zum Beispiel gibt SystemParameters.CaptionWidth die Breite der Titelleiste als Double an, und SystemParameters.CaptionWidthKey ist eine Referenz vom Typ RessourceKey. Bei einer statischen Bindung brauchen Sie den Wert direkt (CaptionKey), bei einer dynamischen den Schlüssel (CaptionWidthKey). Sehen wir uns zuerst eine statische Ressourcenabfrage an. Beachten Sie, dass für den Zugriff auf die Systemressourcen die Markup-Erweiterung x:Static vorgeschrieben ist.
Und nun der dynamische Zugriff.
22.3
Stile
Stile sind das Mittel der Wahl, um Benutzeroberflächen ein einheitliches Design zu geben. Abbildung 22.3 zeigt drei Buttons mit gleichem Aussehen. Ohne Stile sieht der XAML-Code für die erste der drei Schaltflächen wie folgt aus:
1069
22.3
22
Konzepte von WPF
18 Button1
Abbildung 22.3
Wiederverwendete Stile
Bis auf die Beschriftung ist der Code für alle drei Buttons identisch. Eine dreifache Kopie der umfangreichen Anweisungen ist unübersichtlich und anfällig für inkonsistente Änderungen, wenn eine der Kopien vergessen wird. Es ist also besser, Sie definieren einen Stil als Ressource und beziehen sich in den Buttons darauf.
22.3.1 Einfache Stile Stile können innerhalb einer Ressource oder direkt im Element definiert werden. Sie können von untergeordneten Komponenten genutzt werden. Einfacher zu handhaben sind in Ressourcen definierte Stile. Je nach Nutzung wählen Sie eine Anwendungs- oder WindowRessource.
Innerhalb eines Style-Elements wird jede Formatvorlage durch ein eigenes Setter-Element spezifiziert. Im Attribut Property wird festgelegt, auf welche Eigenschaft der Stil wirkt; Value gibt den zu verwendenden Wert an. Stile können nur auf abhängige Eigenschaften wirken. Die Eigenschaft wird über ein klassengebundenes schreibgeschütztes Feld vom Typ DependencyProperty spezifiziert. Das folgende Beispiel formuliert das letzte Beispiel unter Verwendung einfacher Stile: '...\WPFKonzepte\Stile\Einfach.xaml
1070
Stile
Button1 Button2 Button3
22.3.2 Typisierte Stile Die bisher definierten Stile sind ein Angebot. Eine Komponente muss explizit deklarieren, dass sie den Stil verwendet. In der Praxis häufiger sind Stile, die das Design aller Komponenten gleichen Typs als Standardvorgabe festlegen, ohne dass eine Komponente die Verwendung des Stils deklarieren muss. Die einzelne Komponente kann selbstverständlich diese Vorgaben durch explizite Werte überschreiben. Der Zieltyp wird im Attribut TargetType des Style-Elements angegeben, zum Beispiel:
Das umgeschriebene Beispiel zeigt die fehlenden Style-Attribute in den Schaltflächen: '...\WPFKonzepte\Stile\Typisiert.xaml
Button1 Button2
1071
22.3
22
Konzepte von WPF
Button3
Allgemeingültige Stile Je allgemeiner Sie die Typangabe in TargetType halten, auf desto mehr verschiedene Komponenten wird es angewendet. Mit dem Typ Control erfassen Sie alle Steuerelemente, die von Control abgeleitet sind (das heißt fast alle). Allerdings scheint dann die Angabe eines Schlüssels zwingend zu sein, damit der Stil angewendet wird. Unabhängig von TargetType kann eine Liste von Setter-Elementen Eigenschaften verschiedener Typen spezifizieren, die durch ihre Klasse qualifiziert werden, zum Beispiel Property="Button.ClickMode" und Property="Control.FontSize" in einem Style-Element mit TargetType="{x:Type Control}". Dieselbe Eigenschaft kann innerhalb desselben StyleElements nicht unterschiedlich für verschiedene Typen gesetzt werden. Wird ein solcher Mischstil einem Steuerelement zugewiesen, verwendet es nur die Eigenschaften daraus, die auf das Element zutreffen. Alle anderen Eigenschaften werden ignoriert.
Überschreiben einer Stildefinition Geben Sie in einer Komponente eine Eigenschaft an, die auch in einem Stil definiert ist, überschreibt die Angabe in der Komponente den Stil. Wenn Sie die Definition des ersten Buttons im Beispiel oben durch Button1
ersetzen, wird die Schaltfläche in Beige dargestellt.
Vererben von Stilen Durch die Angabe eines Stils im Attribut BasedOn in einem Style-Element können Sie diesen beerben. Die Referenzierung erfolgt als Ressource mit statischer oder dynamischer Bindung. Im erbenden Stil können Eigenschaften des Elternstils überschrieben werden. Haben Sie im Basisstil TargetType angegeben, muss der abgeleitete Stil in seinem TargetType-Attribut dieselbe Klasse oder eine davon abgeleitete Klasse verwenden. '...\WPFKonzepte\Stile\Vererbung.xaml
1072
Trigger
Button1 Button2 Button3
22.4
Trigger
Die bisher verwendeten Ressourcen (inklusive Stile) ändern sich nicht durch den Zustand der Anwendung. Mit Zustand ist hier nicht der Satz an Stilen gemeint, die sich bei einer dynamischen Bindung automatisch ändern, sondern stilunabhängige Zustände. Zum Beispiel können häufiger genutzte Buttons etwas größer dargestellt werden, oder eine Textverarbeitung kann die Textdarstellung ändern, wenn ein Text sehr lange nicht mehr gesichert wurde. Die Lösung sind Trigger, die für die Reaktion auf Ereignisse, Eigenschaftsänderungen usw. verwendet werden. Trigger werden oft zusammen mit Stilen verwendet, können aber auch der Eigenschaft Triggers einer Komponente direkt zugewiesen werden. Eine Komponente darf auch mehrere Trigger haben, um auf verschiedene Zustandsänderungen unterschiedlich zu reagieren.
22.4.1 Eigenschaftstrigger Bewegen Sie beispielsweise die Maus über eine Schaltfläche, ändert sich der Hintergrund und der Rahmen des Steuerelements in einer vordefinierten Art. Wollen Sie die Art der Änderung selbst definieren, können Sie einen passenden Ereignishandler implementieren. Alternativ können Sie in XAML die Zustandsänderung erfassen und darauf reagieren. WPF-Komponenten haben Eigenschaften, die einen bestimmten Zustand beschreiben. Mit IsMoueOver lässt sich beispielsweise feststellen, ob sich der Mauszeiger aktuell über der Komponente befindet, und mit IsPressed prüfen Sie, ob auf die Komponente geklickt wird. Die Prüfung beschränkt sich aber nicht nur auf IsXXX-Eigenschaften. So lässt sich zum Beispiel auch die Eigenschaft Text einer TextBox auf einen bestimmten Inhalt hin untersuchen. Eigenschaftstrigger werden in einem Style-Element definiert. Jeder Trigger steht in einem eigenen Trigger-Element innerhalb eines Style.Triggers-Abschnitts. Dessen Eigenschaften Property und Value nennen die Eigenschaft und deren Wert, die überwacht werden sollen.
1073
22.4
22
Konzepte von WPF
Hat die Eigenschaft zur Laufzeit den Wert der Value-Einstellung, wird der Trigger aktiviert, und die darin enthaltenen Setter-Elemente werden ausgewertet.
Das Beispiel aus Abschnitt 22.3.1, »Einfache Stile«, wird im Folgenden durch einen Trigger ergänzt, der die Darstellung der Schaltfläche unter der Maus ändert: '...\WPFKonzepte\Trigger\Einfach.xaml
Button1 Button2
1074
Trigger
Button3
Ein Trigger nimmt Änderungen an einer oder mehreren Komponenten vor. Diese werden rückgängig gemacht, wenn die im Trigger angegebene Bedingung nicht mehr erfüllt ist. Verlässt in unserem Beispiel der Mauszeiger den Bereich eines Buttons, kehrt dieser wieder in seinen Ausgangszustand zurück.
MultiTrigger Einen von mehreren Bedingungen abhängigen Trigger definieren Sie in einem MultiTriggerElement. Jede Bedingung wird in einem eigenen Condition-Element angegeben, das sich im Conditions-Abschnitt des MultiTrigger-Elements befindet. Nur wenn alle Bedingungen erfüllt sind, löst der Trigger aus. Im folgenden Beispiel enthält Window eine TextBox-Komponente mit einem Textinhalt. Wenn die Textbox leer ist und sich gleichzeitig der Mauszeiger über der Textbox befindet, wird deren Hintergrund in Rot dargestellt. '...\WPFKonzepte\Trigger\Multi.xaml
Windows Presentation Foundation WPF
22.4.2 Ereignistrigger Ereignistrigger werden durch Ereignisse vom Typ RoutedEvent ausgelöst. Darüber können Sie »Animationen« starten. Neben grafischen Spielereien können zum Beispiel Tabellen mit Börsendaten permanent auf dem aktuellen Stand gehalten werden.
1075
22.4
22
Konzepte von WPF
Hinweis Eigenschaften in einem Ereignistrigger permanent zu ändern ist nicht möglich.
Sehen wir uns zuerst die Struktur eines solchen Triggers in XAML an: ...
Ereignistrigger werden in einem Style-Element oder direkt in einer Komponente definiert. Jeder Ereignistrigger steht in einem eigenen EventTrigger-Element mit einem zwingend vorgeschriebenen RoutedEvent-Attribut. Hier wird das auslösende Ereignis angegeben. In einem EventTrigger.Actions-Element werden die Aktionen festgelegt, die beim Auslösen des Triggers ausgeführt werden sollen. Der Start und das Ende der Aktionen werden in den Elementen BeginStoryBoard und StopStoryBoard spezifiziert. Eine StoryBoard-Komponente ist eine Art Container, in dem die Animation über eine Zeitlinie hinweg abläuft. Das folgende Beispiel zeigt einen simplen Einsatz anhand des Ereignisses MouseEnter. Wird zur Laufzeit die Maus über die Schaltfläche gezogen, »verschwindet« diese zunächst, weil die Eigenschaft Opacity auf 0 gesetzt wird. Im Laufe der folgenden 10 Sekunden wird die Sichtbarkeit des Buttons kontinuierlich wiederhergestellt. '...\WPFKonzepte\Trigger\Ereignis.xaml
1076
Kommandos
Button1
22.4.3 Datentrigger Im Gegensatz zu Eigenschaftstriggern reagieren Datentrigger auf die Änderung einer beliebigen Eigenschaft. Ereignistrigger berücksichtigen hingegen nur abhängige Eigenschaften. Jeder Datentrigger wird in einem eigenen DataTrigger-Element spezifiziert, das in einer Triggers-Liste steht. Da Datentrigger auch mit nicht-abhängigen Eigenschaften umgehen können, wird anstelle des Attributs Property das Attribut Binding angegeben. Den Einsatz eines Datentriggers zeigt das folgende Beispiel. Das Fenster enthält eine TextBox. Wird zur Laufzeit die Zeichenfolge »Weg damit« eingetragen, wird sie deaktiviert. '...\WPFKonzepte\Trigger\Daten.xaml
22.5
Kommandos
Viele Operationen brauchen Sie in fast jeder Anwendung: das Kopieren von Daten in die Zwischenablage, das Ausschneiden von markiertem Text, das Speichern von Änderungen usw. WPF vereinfacht die Codierungsarbeit dadurch, dass Ihnen eine große Zahl vorgefertigter Operationen zur Verfügung gestellt wird. Entwickeln Sie beispielsweise ein Menü mit dem Hauptmenüpunkt Bearbeiten und den untergeordneten Elementen Kopieren, Ausschneiden und Einfügen, brauchen Sie nur auf die vordefinierten Kommandos zurückzugreifen.
1077
22.5
22
Konzepte von WPF
22.5.1 Vordefiniert Die von WPF bereitgestellten Kommandos lassen sich in sechs Kategorien unterteilen. Jede Kategorie wird durch eine Klasse beschrieben: 왘
System.Windows.Annotations.AnnotationService
왘
System.Windows.Input.ApplicationCommands
왘
System.Windows.Input.ComponentCommands
왘
System.Windows.Documents.EditingCommands
왘
System.Windows.Input.MediaCommands
왘
System.Windows.Input.NavigationCommands
Jedes Kommando ist in einer klassengebundenen Eigenschaft vom Typ RoutedUICommand gespeichert. Um die Übersicht zu behalten, zeige ich in Tabelle 22.1 anhand einiger wichtiger Vertreter der Klasse ApplicationCommands den Umgang mit Kommandos. Kategorie
Kommandos
Dateien
Close, New, Open, Save, SaveAs
Drucken
CancelPrint, Print, PrintPreview
Bearbeiten
Copy, CorrectionList, Cut, Delete, Paste, Redo, SelectAll, Undo
Verschiedenes
ContextMenu, Find, Help, NotACommand, Properties, Replace, Stop
Tabelle 22.1
Vordefinierte Kommandos der Klasse »ApplicationCommands«
22.5.2 Beispielanwendung Das folgende Beispiel zeigt ein Fenster mit zwei Textboxen und einem Menü mit den Punkten Datei und Bearbeiten: '...\WPFKonzepte\Kommandos\Anwendung.xaml
1078
Kommandos
Die Menüpunkte werden automatisch beschriftet und haben die üblichen Shortcuts (siehe Abbildung 22.4). Wenn Sie die Anwendung starten, funktioniert das gesamte Menü Bearbeiten bereits einwandfrei. Sie können Text markieren und kopieren oder ausschneiden und an anderer Stelle einfügen – in derselben Textbox oder von einer in die andere. Kann eine Operation nicht ausgeführt werden, zum Beispiel, weil sich keine Daten in der Zwischenablage befinden oder weil in der fokussierten TextBox keine Zeichen markiert sind, werden die entsprechenden Menüpunkte deaktiviert – all das, ohne eine Zeile Code zu schreiben.
Abbildung 22.4
Beispiel zu »ApplicationCommands«
22.5.3 Kommando-Ziel Standardmäßig ist das Ziel eines Kommandos das in dem Moment aktive Steuerelement. Bei Bedarf können Sie aber auch ein anderes Ziel festlegen. Neben dem Command-Attribut muss dann
1079
22.5
22
Konzepte von WPF
auch noch das Attribut CommandTarget angegeben werden. Dabei muss wieder die BindingSyntax verwendet werden, wie das folgende Beispiel zeigt: ... ...
22.5.4 Kommandos an Ereignisse binden Für die dateibezogenen Kommandos gibt es keine allgemeingültige Verhaltensweise. Daher sind sie standardmäßig mit keinem Code verbunden, und die Menüpunkte sind deaktiviert. Die Bindung eines Kommandos an einen Ereignishandler übernimmt ein Objekt vom Typ CommandBinding. Ein CommandBinding-Objekt muss mit seiner Eigenschaft Command an ein Kommando vom Typ Command gebunden werden. Üblicherweise werden diese Objekte im Element Window angegeben. Das Ereignis Executed von CommandBinding wird zur Ausführung der gewünschten Operation ausgelöst, und mit CanExecute wird optional geprüft, ob der Befehl ausgeführt werden kann. Das Ergebnis der Prüfung speichern Sie innerhalb des Ereignishandlers in der Eigenschaft CanExecute des zweiten Parameters, der vom Typ CanExecuteRoutedEventArgs ist. Mit CanExecute=False wird das Kommando deaktiviert. Wir ergänzen die Anwendung so, dass das komplette Menü mit Kommandos verbunden ist. Dabei sollen die Menüpunkte Speichern und Speichern als nur dann aktiviert werden, wenn die obere der beiden Textboxen nicht leer ist. '...\WPFKonzepte\Kommandos\Anwendung.xaml
...
In der Code-Behind-Datei sind die Operationen implementiert. Um das Beispiel einfach zu halten, werden die Aktionen durch Meldungsfenster simuliert.
1080
Datenbindung
Partial Public Class Anwendung Private Sub Neu(sender As Object, e As ExecutedRoutedEventArgs) MessageBox.Show("Menüpunkt 'Neu'") End Sub Private Sub Öffnen(sender As Object, e As ExecutedRoutedEventArgs) MessageBox.Show("Menüpunkt 'Öffnen'") End Sub Private Sub Speichern(sender As Object, e As ExecutedRoutedEventArgs) MessageBox.Show("Menüpunkt 'Speichern'") End Sub Private Sub SpeichernAls(sender As Object, e As ExecutedRoutedEventArgs) MessageBox.Show("Menüpunkt 'Speichern als'") End Sub Private Sub KannSpeichern(sender As Object, e As CanExecuteRoutedEventArgs) e.CanExecute = txtOben.Text "" End Sub End Class
22.5.5 Kommandos programmieren Kommandos können Sie auch im Code erzeugen. Der entsprechende Ereignishandler wird mit der bekannten Notation an das Ereignis gebunden. Private Sub Laden(sender As Object, e As RoutedEventArgs) _ Handles MyBase.Loaded Dim cmdSave As New CommandBinding(ApplicationCommands.Save) AddHandler cmdSave.Executed, AddressOf Speichern End Sub
Sie können die Execute-Methode eines Kommandos auch aufrufen, um das Ereignis auszulösen. Der erste Parameter ist ein benutzerdefiniertes Objekt, der zweite die Angabe des Kommandoziels. Private Sub button1_Click(sender As Object, e As RoutedEventArgs) ApplicationCommands.Save.Execute(Nothing, txtOben) End Sub
Hier ist das Kommandoziel eine Komponente namens txtOben.
22.6
Datenbindung
Mit WindowsForms können Sie eine Datenbindung nur relativ eingeschränkt realisieren. Die Datenbindung von WPF ist viel flexibler, denn alle Eigenschaften, genau genommen alle abhängigen Eigenschaften, sind datenbindungsfähig.
1081
22.6
22
Konzepte von WPF
Die Datenquellen sind ausgesprochen vielfältig. Es kann sich um folgende handeln: 왘
die Eigenschaft einer anderen Komponente
왘
eine XML-Datei
왘
eine Collection
왘
Datenbanken
22.6.1 Einfache Datenbindung Im einfachsten Fall einer Datenbindung können wir die Eigenschaften zweier Komponenten miteinander verbinden: Eine fungiert als Quelle und die andere als Empfänger (Konsument). Im folgenden Beispiel hat ein Fenster zwei Textboxen (siehe Abbildung 22.5). Der Text der oberen Textbox soll auch in der unteren Textbox angezeigt werden – und das bei jedem Tastendruck. '...\WPFKonzepte\Datenbindung\Einfach.xaml
Abbildung 22.5
Beispiel zu einfacher Datenbindung
Für die Bindung sorgt der XAML-Code: Binding ElementName="oben" Path="Text
Mit einem Binding-Objekt wird die Bindung beschrieben. Dessen Eigenschaft ElementName gibt den Namen der Datenquelle an, und Path bezeichnet die Eigenschaft der Datenquelle, an die gebunden werden soll. Im Beispiel bindet sich der Inhalt der unteren Textbox an den Text der oberen Textbox. In einer anderen Schreibweise wird die Bindung in geschweiften Klammern angegeben. Sie wird auch als Binding Markup Extension bezeichnet. Diese Notation haben wir schon in einigen Beispielen benutzt.
1082
Bindungsarten
Beachten Sie bei der Notation, dass die Name-Wert-Paare durch Kommata getrennt werden. Voraussetzung der Datenbindung ist, dass die Zieleigenschaft eine abhängige Eigenschaft ist. Die Zieleigenschaft bestimmt auch den Datentyp der Datenbindung. Es muss eine implizite Konvertierung des Datentyps der Quelle in den Datentyp des Ziels geben.
Bindung mit Code Bindungen im XAML-Code wirken sich sofort aus. Im Code können Sie bei Bedarf eine Bindung erst zur Laufzeit aktivieren. Dazu wird zuerst ein Binding-Objekt erzeugt, und wenn es gewünscht ist, geben Sie die Zieleigenschaft an. Der Eigenschaft ElementName weisen Sie den Namen der Quellkomponente als Zeichenfolge zu und rufen anschließend die Methode SetBindung der Zielkomponente auf. Der erste Parameter ist die abhängige Eigenschaft der Datenquelle, der zweite das gerade erzeugte Binding-Objekt. Die Eigenschaft ist als klassengebundene Eigenschaft der Quellklasse gespeichert. '...\WPFKonzepte\Datenbindung\Code.xaml
Bindung erzeugen
'...\WPFKonzepte\Datenbindung\Code.xaml.vb
Private Sub Klick(sender As Object, e As RoutedEventArgs) Dim binding As New Binding("Text") binding.ElementName = "oben" unten.SetBinding(TextBox.TextProperty, binding) End Sub
22.7
Bindungsarten
Bisher haben wir uns nicht darum gekümmert, nach welchen Regeln die Synchronisation der verbundenen Eigenschaften funktioniert. Mit dem Attribut Mode legen Sie fest, in welchen Richtungen die Bindung aktiv ist. Beispielsweise können Sie die Standardvorgaberichtung von der Quelle zum Ziel auch umkehren. Dann aktualisiert das Ziel die Quelle.
1083
22.7
22
Konzepte von WPF
Tabelle 22.2 listet die möglichen Modi vom Typ der Enumeration BindingMode auf. Bindungstyp
Übertragung des Wertes
Default
Der für die Eigenschaft definierte Standard
OneTime
Einmalig von der Quelle zum Ziel, danach Kappen der Verbindung
OneWay
Von der Quelle zum Ziel. Nach Änderung der Daten im Zielobjekt wird die Verbindung gekappt.
OneWayToSource
Von der Quelle zum Ziel. Ändert sich der Wert in der Quelle, bleibt zwar die Bindung bestehen, aber die Daten werden nicht übertragen.
TwoWay
Von der Quelle zum Ziel und umgekehrt
Tabelle 22.2
Bindungsarten
Das folgende Beispielprogramm demonstriert anschaulich die Auswirkung der verschiedenen Modi. Dazu hat das Formular in Abbildung 22.6 für jeden Modus je zwei Textboxen.
Abbildung 22.6
Demonstration der Bindungsarten
'...\WPFKonzepte\Datenbindung\Modus.xaml
1084
Bindungsarten
Die Eigenschaft Text einer TextBox ist standardmäßig auf TwoWay eingestellt. Änderungen in der Quelle werden sofort vom Ziel angezeigt. Ändern Sie aber den Inhalt im Ziel, müssen Sie erst die TextBox verlassen, ehe die Änderung in der Quelle wirksam wird.
22.7.1 Aktualisierung der Quelle Die eben beschriebene Verhaltensweise ist nicht immer wünschenswert. Sie lässt sich mit dem Attribut UpdateSourceTrigger vom Typ der gleichnamigen Enumeration steuern (siehe Tabelle 22.3). Konstante
Beschreibung
Default
Komponentenabhängige Standardeinstellung. Oft ist dies PropertyChanged, seltener LostFocus (wie bei einer TextBox).
Explicit
Änderung erfolgt nur durch expliziten Aufruf der UpdateSource-Methode des Zielobjekts.
LostFocus
Die Quelle wird aktualisiert, wenn das Ziel den Fokus verliert.
PropertyChanged
Die Aktualisierung erfolgt bei jeder Änderung (ressourcenintensiv!).
Tabelle 22.3 Die Enumeration »UpdateSourceTrigger«
1085
22.7
22
Konzepte von WPF
Um eine Zweiwegeaktualisierung in Echtzeit zu erreichen, geben Sie im Binding-Abschnitt statt Mode das Attribut UpdateSourceTrigger an:
Sehen wir uns abschließend noch die Einstellung Explicit an. Hierbei wird der Anstoß zur Aktualisierung der Werte von außen gegeben. Das kann beispielsweise das Drücken der (Enter)-Taste sein oder auch das Click-Ereignis einer Schaltfläche. Die beiden folgenden Codefragmente zeigen, wie das Drücken der (Enter)-Taste in der TextBox namens unten (dem »Ziel«) zum Aktualisieren einer anderen TextBox namens oben (der »Quelle«) führt.
Die Bindung erfolgt im XAML-Code. Daher muss im Visual Basic-Code zuerst mit der Methode GetBindingExpression ein BindingExpression-Objekt abgerufen werden, um darauf die Methode UpdateSource aufzurufen. Private Sub Gedrückt(sender As Object, e As KeyEventArgs) If e.Key = Key.Enter Then _ unten.GetBindingExpression(TextBox.TextProperty).UpdateSource() End Sub
22.7.2 Datenbindungsquellen Bindung an beliebige Objekte Angenommen, Sie haben eine Klasse Person implementiert. Objekte dieses Typs sollen als Datenquelle dienen und in einem Window-Objekt eingebunden werden. Damit werden auch Forderungen an die Implementierung der Klasse gestellt: 왘
Implementierung der Schnittstelle System.ComponentModel.INotifyPropertyChanged
왘
Definition eines parameterlosen Konstruktors
왘
Datenquellen sind nur Eigenschaften mit öffentlicher Eigenschaftsmethode, dagegen können öffentliche Felder (Variablen auf Klassenebene) nicht gebunden werden.
Über die Schnittstelle INotifyPropertyChanged wird das Ereignis PropertyChanged eingeführt, das immer dann ausgelöst wird, wenn sich eine bindungsfähige Eigenschaft ändert. Es folgt nun die Definition der Klasse Person. Bei Änderung der Eigenschaften Zuname oder Alter wird die Methode OnPropertyChanged aufgerufen, die ihrerseits das Ereignis PropertyChanged auslöst.
1086
Bindungsarten
'...\WPFKonzepte\Datenbindung\Person.xaml
Imports System.ComponentModel Public Class Person : Implements INotifyPropertyChanged ' Ereignis aus der Schnittstelle INotifiyPropertyChanged Public Event PropertyChanged(sender As Object, e As PropertyChangedEventArgs) _ Implements INotifyPropertyChanged.PropertyChanged Private zu As String Public Property Zuname() As String Get Return zu End Get Set(ByVal value As String) zu = value OnPropertyChanged("Zuname") End Set End Property Private alt As Integer Public Property Alter() As Integer Get Return alt End Get Set(ByVal value As Integer) alt = value OnPropertyChanged("Alter") End Set End Property Protected Sub OnPropertyChanged(ByVal prop As String) RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(prop)) End Sub End Class
Im XAML-Code wird Person aus der aktuellen Anwendung benötigt sowie Typen aus dem Namensraum System. Beide Namensräume werden in der XAML-Datei angegeben. xmlns:clr="clr-namespace:System;assembly=mscorlib" xmlns:src="clr-namespace:Datenbindung"
Nun kann die Klasse Person auch in der XAML-Datei verwendet werden. Um Person in der XAML-Datei zu instanziieren, verwendet man den Ressourcenabschnitt. Optional können den Eigenschaften sofort Werte zugewiesen werden.
Danach können Sie die Möglichkeiten der Datenbindung benutzen. In unserem Beispiel werden Zuname und Alter der Person in zwei Textboxen angezeigt.
1087
22.7
22
Konzepte von WPF
Zuname: Alter:
Eine alternative Variante der Datenbindung bietet sich an, wenn das Objekt zahlreiche Eigenschaften hat und mehrere Komponenten an diese Eigenschaften gebunden werden. Weisen Sie dazu der Eigenschaft DataContext des Containers die Ressource zu. Wird in einem untergeordneten Element keine Datenquelle angegeben, sucht das Element in der Hierarchie aufwärts nach einer DataContext-Spezifikation. ... Zuname: Alter:
Zugriff auf eine Instanz im XAML-Code Wollen Sie im Visual Basic-Code auf ein im XAML-Code erzeugtes Objekt zugreifen, zum Beispiel um den Wert einer Eigenschaft zu ändern, dann müssen Sie sich zuerst die Referenz auf das im XAML-Code erzeugte Objekt besorgen. Hier hilft die Methode FindResource der Klasse FrameworkElement weiter, einer Basisklasse von Window. Der Methode wird als Argument das im XAML-Code erzeugte Objekt übergeben. Der Rückgabewert muss noch in den richtigen Typ konvertiert werden. Private Sub Ändern(sender As Object, e As RoutedEventArgs) Dim pers As Person = CType(FindResource("pers"), Person) MessageBox.Show("Name und Alter: " & pers.Zuname & ", " & pers.Alter) pers.Zuname = "Schmidt-Gerhards" End Sub
Umgekehrt können Sie im Code ein Objekt erzeugen und an DataContext übergeben. Private Sub Erzeugen(sender As Object, e As RoutedEventArgs) Dim pers As New Person() pers.Zuname = "Fischer"
1088
Bindungsarten
pers.Alter = 55 grid1.DataContext = pers End Sub
Der passende XAML-Code dazu sieht so aus: '...\WPFKonzepte\Datenbindung\Instanz.xaml
... Zuname: Alter: Datenerzeugung Datenzugriff
22.7.3 Auflistungen anbinden Analog zu den im vorigen Abschnitt beschriebenen Objekten müssen Auflistungen die Schnittstelle INotifyCollectionChanged implementieren. Am einfachsten leiten Sie die generische Klasse System.Collections.ObjectModel.ObservableCollection ab, die die Schnittstelle bereits implementiert. Wir definieren eine Auflistung, die Person-Objekte des vorigen Abschnitts verwaltet. Alle Personen der Auflistung werden in einer ListBox angezeigt (siehe Abbildung 22.7). Da die Auflistung sofort bei der Instanziierung mit Person-Objekten gefüllt werden soll, leiten wir ObservableCollection ab und erzeugen im Konstruktor Person-Objekte.
1089
22.7
22
Konzepte von WPF
Abbildung 22.7
Beispiel für Datenschablonen
'...\WPFKonzepte\Datenbindung\Personenliste.xaml
Imports System.Collections.ObjectModel Public Class Personenliste : Inherits ObservableCollection(Of Person) Public Sub New() Add(New Person() With {.Zuname = "Fischer", .Alter = 52}) Add(New Person() With {.Zuname = "Müller", .Alter = 32}) Add(New Person() With {.Zuname = "Schmitz", .Alter = 85}) Add(New Person() With {.Zuname = "Meier", .Alter = 13}) Add(New Person() With {.Zuname = "Mayer", .Alter = 30}) End Sub End Class
In XAML füllen wir die ListBox mittels einer Schablone. Sie definiert eine Zeile, die statt konkreter Daten eine Bindung zur darzustellenden Eigenschaft benutzt. Die Gestaltung der Zeile erfolgt genau so, als stünde sie direkt im Dokument. Eine solche Schablone lässt sich später der ListBox (oder beispielsweise auch einer ComboBox) zuordnen. DataTemplate beschreibt eine solche Schablone. Sie wird im Ressourcenbereich der XAML-
Datei definiert, in dem zudem ein Objekt vom Typ Personenliste erzeugt wird:
Was bleibt, ist die Zuordnung der Ressourcen zu der ListBox. In ItemSource wird die Datenquelle genannt, aus der die Informationen zu beziehen sind. ItemTemplate beschreibt das Muster (DataTemplate), mit dem die in ItemSource spezifizierten Daten angezeigt werden.
1090
Bindungsarten
Das Fenster enthält noch zwei Buttons. Beim Klicken auf den ersten Button wird das aktuell selektierte Listboxelement aus der Liste gelöscht, mit dem zweiten Button wird ein neues Element hinzugefügt. Hier sehen Sie zunächst den kompletten Code der XAML-Datei: Person löschen Person hinzufügen
Was noch fehlt, ist der Code in den Ereignishandlern der beiden Schaltflächen: Partial Public Class Liste Public liste As Personenliste Private Sub Laden(ByVal sender As Object, ByVal e As RoutedEventArgs) liste = CType(FindResource("Persliste"), Personenliste) End Sub Private Sub Löschen(ByVal sender As Object, ByVal e As RoutedEventArgs) liste.RemoveAt(listbox1.SelectedIndex) End Sub Private Sub Hinzufügen(ByVal sender As Object, ByVal e As RoutedEventArgs) liste.Add(New Person() With {.Zuname = "Kleinen", .Alter = 45}) End Sub End Class
1091
22.7
Bevor Sie auf Daten einer Datenbank zugreifen können, müssen Sie sich erst einmal mit der Datenbank verbinden. Dieses Kapitel zeigt wie.
23
Datenbankverbindung mit ADO.NET
Vor der Einführung von ADO.NET im Jahr 2002 hat Microsoft verschiedene Datenzugriffstechnologien für den Zugriff auf und das Speichern von Daten eingesetzt. Der direkte Vorgänger von ADO.NET war Microsofts ActiveX Data Objects (ADO), eine verbindungsorientierte Datenzugriffstechnologie, der allerdings Schlüsselfunktionen fehlen, um auch sehr große, verteilte Anwendungen zu erstellen. ADO.NET integriert sich mit seinen Klassen in mehrere Namensräume des .NET-Frameworks. Von der Idee her soll ADO.NET den Entwicklern dabei helfen, effiziente mehrschichtige Datenbankanwendungen über Intranets und das Internet hinweg zu erstellen. Daraus resultiert eine zweischichtige Klassenarchitektur: Es gibt Klassen, deren Objekte mit der Datenbank verbunden sind, und Klassen, deren Objekte als unverbundene Objekte bezeichnet werden. Zu den verbundenen Objekten zählen unter anderem die Klassen Connection, Command und DataAdapter, zu den unverbundenen die Klasse DataSet und DataTable. Auf alle werden wir im weiteren Verlauf noch eingehen. Sie können nicht erwarten, dass in diesem und den folgenden Kapiteln sämtliche Aspekte von ADO.NET erläutert werden. Dafür ist das Objektmodell mit allen seinen Möglichkeiten einfach zu komplex. Es würde für sich allein ein ganzes Buch füllen. Ich werde aber versuchen, Ihnen die meiner Ansicht nach wichtigsten Klassen vorzustellen, und Ihnen zeigen, wie Sie Daten abrufen und eine Datenbank aktualisieren können.
23.1
ADO.NET-Provider
Bevor man auf die Dateninformationen in einer Datenbank zugreifen kann, muss man eine Verbindung zu der Datenquelle herstellen. ADO.NET stellt dazu passende Klassen zur Verfügung, mit denen eine Verbindung aufgebaut und gesteuert werden kann. Zuerst muss der Typ des Datenspeichers bekannt sein. Er legt den Datenprovider fest. Das .NET Framework stellt vier zur Verfügung: 왘
SqlClient-Provider
왘
OleDb-Provider
왘
Odbc-Provider
왘
Oracle-Provider
1093
23
Datenbankverbindung mit ADO.NET
Ein Datenprovider ist ein Satz von Klassen, die den Zugriff auf einen bestimmten Datenspeichertyp ermöglichen. Jeder .NET-Datenprovider implementiert dabei die gleichen Basisklassen, beispielsweise DbConnection, DbCommand und DbDataAdapter. Der Name der konkreten Implementation hängt vom gewählten Provider ab. So bietet der SqlClient-Provider beispielsweise die Klasse SqlConnection an und der OleDb-Datenprovider die Klasse OleDbConnection. Unabhängig davon, für welchen Datenprovider Sie sich entscheiden, bleiben die Schnittstellen und damit die Funktionalitäten gleich. Nahezu unabhängig von der Providerwahl ist auch der Programmcode. Sollten Sie zu einem späteren Zeitpunkt den Provider wechseln, brauchen Sie möglicherweise den Programmcode überhaupt nicht zu überarbeiten. Häufig sind Sie nicht auf einen einzigen Datenprovider festgelegt, sondern können für den Zugriff auf eine Datenquelle zwischen mehreren auswählen. Ist die Datenquelle ein Microsoft SQL Server in der Version 7.0 oder höher, empfiehlt sich der SqlClient-Datenprovider, weil dieser für die genannten Versionen des SQL Servers optimiert ist.
SQL Server Jeder .NET-Datenprovider hat einen eigenen Namensraum, der ein Unternamensraum von System.Data ist und mit Imports bekannt gegeben werden sollte. In den Beispielen dieses Kapitels werden wir ausschließlich den SqlClient-Datenprovider benutzen. Für Ihre Programme können Sie zum Testen eine kostenlose Variante des SQL Servers nutzen (siehe http:// www.microsoft.com/sql/editions/express/default.mspx). In den Beispielen wurde der SQL Server 2005 verwendet. Die Verwaltung der Software ist am einfachsten mit den Management Tools möglich (in SQL Server Express with Advanced Services enthalten). Sie ersetzen den Query Analyzer und Enterprise Manager früherer Versionen. Hinweis Wird nach der Installation einer Express Edition, zum Beispiel durch Visual Studio, ein anderer SQL Server installiert, kann es passieren, dass einige Zusatzprogramme kommentarlos nicht installiert werden.
In einigen Newsgroups wird über Probleme durch diese Parallelinstallation geklagt. Sie können die Express Edition über die Systemsteuerung inklusive der gemeinsamen Komponenten deinstallieren (alle anderen Lösungsvorschläge scheiterten in meiner Installation). Nach Druck auf die (æ_)-Taste wird eine Auswahl der Serverinstanzen angeboten. Nach der Deinstallation der parallelen Express Edition installieren Sie die Tools explizit mit \GERMAN\ SQL2005\DEVELOPER\SQL Server x86\Tools\Setup\SqlRun_Tools.msi. Bei Schwierigkeiten sollten Sie den Rechner vor der Tools-Installation neu starten. In Extremfällen können Sie die Express Edition wie unter http://support.microsoft.com/kb/ 909967 beschrieben, manuell deinstallieren und gemäß http://msdn.microsoft.com/en-us/ library/ms144259.aspx die Tools manuell installieren. Hinweis Hat ein Windows-Benutzer keine Administratorrechte, kann eine integrierte Windows-Authentifizierung scheitern.
1094
Verbindungen mit dem Datenprovider
Im SQL Server Management Studio können Sie (als Windows- oder SQL Server-Administrator) über den Knoten /Security/Logins im Objekt-Explorer einen Benutzer hinzufügen. In dessen Eigenschaften müssen Sie noch Berechtigungen für zu verarbeitende Datenbanken vergeben und Serverrollen zuweisen (siehe Tabelle 23.1). Rolle
Berechtigung
dbo db_owner
Rollen Mitglieder hinzufügen, Datenbanken sichern/wiederherstellen, Checkpoint setzen, Objektberechtigungen in der Datenbank erteilen
db_ddladmin
Nur DDL-Befehle ausführen
db_accessadmin
Datenbankbenutzer verwalten
db_securityadmin
Zugriffsberechtigung steuern: GRANT, REVOKE
db_backupoperator
Datensicherungen
db_datareader
Abfrage: SELECT
db_datawriter
Änderung: INSERT, UPDATE, DELETE
db_denydatareader
Kein SELECT
db_denydatawriter
Kein INSERT, UPDATE, DELETE
Tabelle 23.1
Vordefinierte Datenbankrollen in SQL Server
Beispieldatenbank »Northwind« Für unsere Beispiele in den folgenden Kapiteln benötigen wir eine Datenbank. Die altbekannte Northwind soll uns als Beispieldatenbank dienen. Laden Sie sich deshalb aus dem Internet die Datei Northwind.mdf herunter, und installieren Sie sie unter dem SQL Server (siehe http://technet.microsoft.com/de-de/library/ms144235.aspx). Die Datei instnwnd.sql öffnen Sie im SQL Server Management Studio und führen die Abfrage aus. Hinweis Zur Ausführung braucht der Benutzer db_datareader/writer-Rechte in der Datenbank master und und muss die Serverrolle dbcreator haben.
23.2
Verbindungen mit dem Datenprovider
Da jeder Datenbanktyp seinen eigenen Provider hat, muss dieser zuerst festgelegt werden (siehe Abschnitt 23.1, »ADO.NET-Provider«). Die Klassen jedes .NET-Datenproviders sind jeweils in einem eigenen Namensraum in der .NET-Klassenbibliothek untergebracht. In diesem Abschnitt bauen wir eine Verbindung zum SQL Server unter Einsatz des SqlClientDatenproviders auf. Um einen eventuellen Providerwechsel zu vereinfachen, werden wir mit Referenzen auf allgemeinere Typen arbeiten. Sie sollten mit Imports System.Data.Common Imports System.Data.SqlClient
1095
23.2
23
Datenbankverbindung mit ADO.NET
die entsprechenden Namensräume einbinden, um nicht jedes Mal vollqualifizierte Namen tippen zu müssen. Hinweis Wenn möglich wird anstelle von SqlConnection die Klasse DbConnection als Referenztyp verwendet, um einen eventuellen Providerwechsel zu erleichtern.
23.2.1 DbConnection-Objekt Die Verbindung zu einer Datenbank wird durch ein DbConnection-Objekt beschrieben. Die Klasse ist abstrakt, und das konkrete Objekt wird durch eine Klasse erzeugt, in deren Namen das Präfix Db so ersetzt wird, dass es den verwendeten .NET-Datenprovider kennzeichnet. Benutzen Sie den SqlClient-Datenprovider, heißt die Klasse SqlConnection, beim OleDbDatenprovider heißt sie OleDbConnection. Der Einfachheit halber wird aber im Folgenden oft einfach nur vom Connection-Objekt die Rede sein. Damit wird die Allgemeingültigkeit dieses Typs unterstrichen. Die folgenden Abschnitte werden zeigen, dass sich die providerspezifischen Connection-Objekte nur geringfügig unterscheiden. Um auf eine Datenquelle wie Microsoft SQL Server zuzugreifen, werden mehrere Informationen benötigt: 왘
der Name des Rechners, auf dem die SQL Server-Instanz läuft
왘
der Name der Datenbank, deren Dateninformationen verarbeitet werden sollen
왘
die Anmeldeinformationen, mit denen sich der Anwender authentifiziert
Diese Verbindungsinformationen werden nach einem bestimmten Muster in einer sogenannten Verbindungszeichenfolge zusammengefasst. Grundsätzlich haben Sie drei Möglichkeiten, die Verbindungsinformationen zu einer Datenquelle anzugeben: 왘
Sie rufen den parameterlosen Konstruktor der Connection-Klasse auf und übergeben dem erzeugten Objekt die Verbindungsinformationen.
왘
Sie rufen einen parametrisierten Konstruktor auf.
왘
Sie benutzen die Klasse DbConnectionStringBuilder.
23.2.2 Die Verbindungszeichenfolge Sehen wir uns zuerst den parameterlosen Konstruktor an: Dim con As DbConnection = new SqlConnection()
Dieses Verbindungsobjekt ist noch sehr dumm, da ihm sämtliche Informationen fehlen, die zum Aufbau einer Verbindung zu einer Datenquelle notwendig sind. Diese müssen der Eigenschaft ConnectionString des DbConnection-Objekts zugewiesen werden: Dim con As DbConnection = New SqlConnection() con.ConnectionString = ""
1096
Verbindungen mit dem Datenprovider
Dem parametrisierten Konstruktor wird die Verbindungszeichenfolge als Argument direkt übergeben: Dim con As DbConnection = New SqlConnection("")
Attribute einer Verbindungszeichenfolge Alle Informationen, die zum Aufbau einer Verbindung zu einer Datenquelle erforderlich sind, werden in der Verbindungszeichenfolge beschrieben. Sie besteht aus einer Reihe von Attributen (bzw. Schlüsseln), denen Werte zugewiesen werden. Die Attribute sind untereinander durch ein Semikolon getrennt. Die allgemeine Syntax lässt sich wie folgt beschreiben: Dim strCon As String = "Attribut1=Wert1;Attribut2=Wert2;..."
Die Bezeichner der einzelnen Attribute sind durch den verwendeten .NET-Datenprovider festgelegt. Weder die Groß-/Kleinschreibung noch die Reihenfolge der Attribute sind von Bedeutung. Beachten Sie, dass es meistens mehrere Attributbezeichner gibt, die gleichwertig eingesetzt werden können (siehe Tabelle 23.2). Schlüssel
Beschreibung
Connect Timeout, Connection Timeout
Zeitdauer in Sekunden, die auf eine Verbindung zum Server gewartet werden soll, bevor der Versuch abgebrochen und ein Fehler generiert wird. Der Standardwert beträgt 15 Sekunden.
Data Source, Server, Address, Addr, Network Address
Name oder Netzwerkadresse der Instanz des SQL Servers, mit denen eine Verbindung hergestellt werden soll.
Initial Catalog, Database
Name der Datenbank, mit der begonnen wird.
Integrated Security, Trusted_Connection
Bei false (no) werden die Benutzer-ID und das Kennwort für die Verbindung angegeben. Bei true (yes, sspi) werden die aktuellen Anmeldeinformationen des Windows-Kontos für die Authentifizierung verwendet.
Packet Size
Größe der Netzwerkpakete in Bytes zur Kommunikation mit einer Instanz des SQL Server: 512-32767, Standard ist 8192.
Password, Pwd
Das Kennwort für das SQL Server-Konto
User ID
Das SQL Server-Anmeldekonto
Workstation ID
Der Name des Computers, der mit dem SQL Server eine Verbindung aufbauen möchte
Tabelle 23.2
Attribute der Verbindungszeichenfolge des SQL-Datenproviders
Tipp Die Verbindungszeichenfolgen hängen sowohl vom gewählten Datenprovider als auch vom Datenbankserver-Typ ab. Hier auf alle gängigen Datenbankserver einzugehen, wäre nicht sinnvoll. Andere Verbindungszeichenfolgen, beispielsweise für eine MySQL- oder Informix-Datenbank, werden auf der Internetseite www.connectionstrings.com beschrieben.
1097
23.2
23
Datenbankverbindung mit ADO.NET
23.2.3 Die Verbindung zu einer bestimmten SQL Server-Instanz Befindet sich der SQL Server auf dem lokalen Rechner und wollen Sie auf die Beispieldatenbank Northwind zuzugreifen, könnte die Verbindungszeichenfolge so lauten: Dim con As DbConnection = New SqlConnection() con.ConnectionString = "Data Source=(local);" & _ "Initial Catalog=Northwind;" & _ "Integrated Security=sspi"
Data Source beschreibt den Rechner, auf dem sich die laufende SQL Server-Instanz befindet.
Hier können Sie den Rechnernamen oder eine TCP/IP-Adresse eintragen. Für einen lokalen Rechner dürfen Sie anstatt des Rechnernamens auch (local), localhost oder einfach nur einen Punkt angeben – die beiden Letztgenannten ohne runde Klammern. Auf einem Computer können durchaus mehrere Instanzen von SQL Server installiert sein. Das Codefragment oben greift auf die sogenannte Standardinstanz zu. Zum Zugriff auf eine benannte Instanz geben Sie zuerst den Rechnernamen und danach einen Backslash (»\«) an. Dahinter folgt die Angabe der SQL Server-Instanz. Wollen Sie sich beispielsweise mit der Instanz SQLExpress auf der lokalen Maschine verbinden, sieht das Data Source-Attribut wie folgt aus: Data Source=.\SQLExpress
Hinter Initial Catalog ist die Datenbank angegeben, und zum Schluss folgen noch Informationen zur Authentifizierung. Gleichwertig können Sie auch dem parametrisierten Konstruktor des Connection-Objekts die Verbindungszeichenfolge übergeben: Dim con As DbConnection = New SqlConnection("Data Source=.;" & _ "Initial Catalog=Northwind;Integrated Security=sspi")
Sie müssen nicht zwangsläufig alle Attribute verwenden. Das Attribut Packet Size wird beispielsweise nicht benutzt. Somit werden alle Daten auf der Verbindung in 8192 Bytes großen Paketen verschickt. Wenn große Datenmengen vom Server geladen werden sollen, zum Beispiel Bilder, können größere Pakete die Leistung durchaus deutlich steigern.
Authentifizierung Soll die Verbindung zu einer Datenbank aufgebaut werden, muss sich der Anwender bei der Datenbank anmelden. Das Connection-Objekt benutzt hierfür die Authentifizierungsinformationen, die in der Verbindungszeichenfolge enthalten sind. Diese werden vom Datenbankserver überprüft. SQL Server kennt zwei Verfahren zur Authentifizierung: 왘
Integrierte Windows-Authentifizierung: Zur Authentifizierung benutzt der SQL Server das Authentifizierungssystem von Windows (NT/2000/XP/2003/…). Mit Ausnahme der Benutzer mit administrativen Rechten muss der Datenbankadministrator jeden Benutzer explizit hinzufügen.
1098
Verbindungen mit dem Datenprovider
왘
SQL Server-Authentifizierung: Diese nutzt die SQL Server-interne Benutzerliste, die keine Windows-Benutzer beinhaltet. Benutzer werden mithilfe des SQL Server Management Studios erstellt und konfiguriert. Den Benutzern werden die gewünschten Berechtigungen für die entsprechende Datenbank eingerichtet. (Hinweis: Bei der SQL Server Express Edition ist nur die Windows-Authentifizierung möglich.)
Die Authentifizierungsart können Sie bereits bei der Installation von SQL Server festlegen. Die SQL Server-Authentifizierung ist standardmäßig deaktiviert, Sie können aber auch einen gemischten Modus aus beiden Authentifizierungen wählen. Eine nachträgliche Änderung der Serverauthentifizierung erfolgt im SQL Server Management Studio. Markieren Sie hierzu die SQL Server-Instanz, öffnen Sie über deren Kontextmenü die Eigenschaftsliste, und wählen Sie den Reiter Sicherheit. Bei der integrierten Windows-Authentifizierung muss weder ein Benutzername noch ein Passwort explizit gesendet werden. Mit der Angabe von Integrated Security=sspi reicht das System den Benutzernamen und das Passwort des aktuellen Windows-Benutzers an den SQL Server weiter. Vorausgesetzt der Kontoinhaber hat ausreichende Rechte, kann damit die Verbindung zur Datenbank hergestellt werden. Die SQL Server-Authentifizierung setzt voraus, dass der Administrator des SQL Servers ein Benutzerkonto mit Passwort eingerichtet hat. Sowohl der Benutzername als auch das Passwort müssen bei diesem Authentifizierungsverfahren in der Verbindungszeichenfolge stehen, beispielsweise so: Dim con As DbConnection = New SqlConnection(); con.ConnectionString = "Data Source=DBServer;Initial Catalog=Northwind;" & _ "User ID=Testuser;Password=26gf28"
SQL Server führt die Authentifizierung durch, indem er überprüft, ob ein SQL Server-Anmeldekonto mit diesem Namen eingerichtet ist und ob das angegebene Kennwort stimmt. Falls die übermittelten Anmeldeinformationen falsch sind, misslingt die Authentifizierung, und der Benutzer erhält eine Fehlermeldung. Es ist natürlich grundsätzlich nicht empfehlenswert, die Daten zur Benutzerauthentifizierung statisch in der Verbindungszeichenfolge zu speichern. Besser ist es, in einem Dialog den Anwender zur Eingabe von Benutzernamen und Passwort aufzufordern und mit diesen Informationen zur Laufzeit die Verbindungszeichenfolge zu bilden.
23.2.4 Änderung des Passworts bei der SQL Server-Authentifizierung Bei der SQL Server-Authentifizierung bilden Benutzername und Passwort eine Einheit, die den Zugriff auf Datenressourcen ermöglicht. Seit ADO.NET 2.0 – und auch nur im Zusammenspiel mit SQL Server 2005 – kann der Benutzer sein Passwort ändern, ohne dass der Datenbankadministrator eingreifen muss. Hier hilft die statische Methode ChangePassword der Klasse SqlConnection weiter. Aus einer aktiven Verbindung heraus kann damit unter Angabe der alten Authentifizierungsinformationen (Benutzername und Kennwort) ein neues Kennwort festgelegt werden.
1099
23.2
23
Datenbankverbindung mit ADO.NET
Dim con As DbConnection = New SqlConnection(); con.ConnectionString = "Data Source=DBServer;Initial Catalog=Northwind;" & _ "User ID=Testuser;Password=26gf28" con.Open() SqlConnection.ChangePassword("User ID=Testuser;PWD=26gf28", "4711password")
Diese Technik bietet sich besonders an, wenn das alte Kennwort abgelaufen ist.
23.2.5 Verbindungszeichenfolgen mit DbConnectionStringBuilder Wenn Sie den Anwender dazu auffordern, seine Authentifizierungsinformationen, die aus Benutzername und Passwort bestehen, in einem Dialog einzutragen, besteht die Gefahr, dass »böse Buben« im Login- oder Kennwortfeld zusätzliche Parameter eintragen. In Abbildung 23.1 wird das gezeigt. Im Extremfall kann dies zu Sicherheitsproblemen führen.
Abbildung 23.1
Manipulation der Verbindungszeichenfolge
Neben der beabsichtigten Böswilligkeit könnte der Anwender aber aus Unwissenheit auch Zeichen gewählt haben, die in der Verbindungszeichenfolge eine besondere Bedeutung haben, beispielsweise »;« oder »=». Eine Eingabe dieser Zeichen würde zu einer Fehlermeldung führen. Die Klasse DbConnectionStringBuilder vermeidet diese Probleme. Jedem Element der Verbindungszeichenfolge wird ein Wert zugeordnet. Das Ergebnis wird der Eigenschaft ConnectionString des DbConnectionStringBuilder-Objekts übergeben. Sie müssen diese Eigenschaft am Ende nur noch dem Konstruktoraufruf von Connection übergeben. Dim conBuilder As DbConnectionStringBuilder = _ New SqlConnectionStringBuilder() conBuilder.Add("Data Source", ".") conBuilder("Initial Catalog") = "Northwind" 'oder Add conBuilder.Add("User ID", " Testuser") conBuilder.Add("Password", "26gf28") Dim con As DbConnection = New SqlConnection(conBuilder.ConnectionString)
Die Klasse SqlConnectionStringBuilder stellt für alle Attribute der Verbindungszeichenfolge Eigenschaften zur Verfügung, denen Sie die passenden Werte zuweisen: Dim conBuilder As SqlConnectionStringBuilder = _ New SqlConnectionStringBuilder() conBuilder.DataSource = "." conBuilder.InitialCatalog = "Northwind"
1100
Verbindungen mit dem Datenprovider
conBuilder.UserID = "Testuser" conBuilder.Password = "26gf28" Dim con As DbConnection = New SqlConnection(conBuilder.ConnectionString)
Wenn Sie sich die erzeugte Verbindungszeichenfolge im Befehlsfenster ausgeben lassen, wird Folgendes angezeigt (con.ConnectionString ohne Passwort nach con.Open()): Data Source=.;Initial Catalog=Northwind;User ID=Testuser;Password=26gf28
23.2.6 Öffnen und Schließen einer Verbindung Verbindung öffnen Das Instanziieren der Klasse SqlConnection und Zuweisen der Verbindungszeichenfolge sind noch nicht ausreichend, um die Verbindung zu einer Datenbank zu öffnen und auf die in ihr enthaltenen Daten zuzugreifen. Dazu muss noch die Methode Open des DBConnection-Objekts aufgerufen werden: Dim con As DbConnection = new SqlConnection("Data Source=localhost;" & _ "Initial Catalog=Northwind;Trusted_Connection=yes") con.Open()
Weist die Verbindungszeichenfolge keinen Fehler auf, können Sie nun auf die Daten von Northwind zugreifen. Es gibt allerdings eine Reihe potenzieller Fehlerquellen, die zu einem Laufzeitfehler beim Verbindungsaufbau führen können: 왘
In der Verbindungszeichenfolge befindet sich ein Fehler.
왘
Der Anwender hat keine Zugriffsrechte auf die Datenbank.
왘
Der Datenbankserver ist nicht gestartet.
왘
Der Rechner, auf dem der Datenbankserver läuft, ist im Netzwerk nicht erreichbar.
Sie sollten daher das Öffnen einer Datenbankverbindung immer in einen Fehlerbehandlungsblock einschließen, der durch einen ergänzenden Finally-Zweig sicherstellt, dass im Fehlerfall offene Verbindungen geschlossen werden: Try Dim con As DbConnection = New SqlConnection(...); con.Open(); ' Anweisungen Catch ex As Exception ' Anweisungen Finally ' wenn offen, Verbindung schließen End Try
Um den Programmcode übersichtlich zu halten, wird in diesem Buch in den folgenden Codebeispielen auf die Fehlerbehandlung verzichtet.
1101
23.2
23
Datenbankverbindung mit ADO.NET
Wenn Sie versuchen, ein bereits geöffnetes DbConnection-Objekt ein zweites Mal zu öffnen, wird die Ausnahme InvalidOperationException ausgelöst. In Zweifelsfällen sollten Sie vor dem Öffnen die Eigenschaft State abfragen: If con.State = ConnectionState.Closed Then con.Open()
Obwohl die Enumeration ConnectionState insgesamt sechs verschiedene Zustände beschreibt, können aktuell nur zwei, nämlich Closed und Open, abgefragt werden. Alle anderen sind für zukünftige Versionen reserviert.
Verbindung schließen Man könnte der Meinung sein, dass eine geöffnete Verbindung geschlossen wird, wenn das DbConnection-Objekt aufgegeben wird. Das wäre zum Beispiel der Fall, wenn die Referenz des DbConnection-Objekts auf Nothing gesetzt wird oder die Objektvariable ihren Gültigkeitsbereich verlässt. Das stimmt aber nur aus Sicht des zugreifenden Prozesses, denn tatsächlich werden auch auf dem Datenbankserver Ressourcen für die Verbindung reserviert, die nicht freigegeben werden, wenn das DbConnection-Objekt nur aufgegeben, aber noch nicht vom Garbage Collector bereinigt wird. Anhand des folgenden Codes, der das Click-Ereignis einer Schaltfläche behandelt, soll die Problematik erörtert werden: Private Sub button1_Click(sender As Object, e As EventArgs) Dim con As DbConnection = New SqlConnection(...) con.Open() End Sub
Das DbConnection-Objekt wird innerhalb des Ereignishandlers erzeugt. Anschließend wird die Verbindung geöffnet. Mit dem Öffnen werden auch Ressourcen auf dem Datenbankserver für die Verbindung reserviert. Obwohl das clientseitige Objekt nach dem Verlassen des Handlers Nothing ist, nimmt der Datenbankserver von dieser Tatsache keine Notiz. Er wird weiterhin die Verbindung als geöffnet betrachten. Sie können das sehr schön sehen, wenn Sie im SQL Server Management Studio das Tool SQL Server Profiler öffnen und eine Ablaufverfolgung starten. Klicken Sie mehrfach auf die Schaltfläche, wird der Datenbankserver auch mehrere Verbindungen als Login registrieren, ohne dass es zu einem Logout kommt. Erst nach dem Schließen der Anwendung werden auch alle Verbindungen seitens der Datenbank geschlossen (siehe Abbildung 23.2). Sie sollten daher immer so schnell wie möglich eine geöffnete Verbindung wieder schließen, indem Sie die Close-Methode des Connection-Objekts aufrufen: ... con.Open() ' nur Anweisungen, die eine geöffnete Verbindung benötigen con.Close()
In unserem Beispiel mit der Schaltfläche wurde zu keinem Zeitpunkt Close aufgerufen. Dass dennoch spätestens beim Beenden der Anwendung die Datenbankressourcen für die Verbindungen freigegeben werden, liegt daran, dass der Garbage Collector mit dem Schließen der Windows-Anwendung implizit die Close-Methode aufruft. Der Aufruf von Close auf einer geschlossenen Verbindung löst übrigens keine Ausnahme aus.
1102
Verbindungen mit dem Datenprovider
Abbildung 23.2
Ablaufverfolgung im SQL Server Profiler-Tool
Die Möglichkeiten zum Schließen einer Datenbankverbindung sind damit aber noch nicht ausgeschöpft. Sie können auch die Methode Dispose des DbConnection-Objekts aufrufen, die ihrerseits implizit Close aufruft. Sie sollten sich aber darüber im Klaren sein, dass das Verbindungsobjekt damit endgültig aus dem Speicher entfernt wird. Kurzlebige Ressourcen können auch innerhalb eines Using-Blocks geöffnet werden, so auch das DbConnection-Objekt: Using con As DbConnection = New SqlConnection() ... con.Open() ... End Using
Using stellt sicher, dass die Dispose-Methode am Ende des Blocks aufgerufen wird, selbst wenn eine Ausnahme auftritt, die nicht behandelt wird (siehe Abschnitt 3.16.3, »Dispose und Using«).
Dauer des Verbindungsaufbaus Standardmäßig wird 15 Sekunden lang versucht, die Verbindung erfolgreich aufzubauen. Ist nach dieser Zeit keine Verbindung zum Datenbankserver vorhanden, wird eine Ausnahme ausgelöst. Äußere Umstände wie die Netzwerk- oder Serverbelastung können dazu führen, dass diese Zeitspanne zu knapp bemessen ist. In der Verbindungszeichenfolge kann daher mithilfe des Attributs Connect Timeout (bzw. Connection Timeout) eine andere Zeitspanne festgelegt werden. Die Angabe erfolgt in Sekunden. Dim con As DbConnection = new SqlConnection("Data Source=localhost;" & _ "Initial Catalog=Northwind;Trusted_Connection=yes;Connect Timeout=30;")
1103
23.2
23
Datenbankverbindung mit ADO.NET
Das DbConnection-Objekt verfügt auch über eine Eigenschaft ConnectionTimeout. Da sie schreibgeschützt ist, kann darüber keine neue Zeitspanne festgelegt werden. Somit ist die Verbindungszeichenfolge die einzige Stelle, wo Sie diese Zeitspanne festlegen können.
Wie lange sollte eine Verbindung geöffnet bleiben? Grundsätzlich sollte eine Verbindung so schnell wie möglich wieder geschlossen werden, um die dafür beanspruchten Ressourcen eines Datenbankservers schnell für andere Anwendungen frei zu machen. Dies ist besonders wichtig im Zusammenhang mit mehrschichtigen Anwendungen (ASP.NET, Webservices), bei denen zu einem gegebenen Zeitpunkt sehr viele User gleichzeitig Dateninformationen bearbeiten wollen. Etwas anders kann die Argumentation ausfallen, wenn es sich bei dem Client um ein Windows-Programm handelt, aus dem heraus die Datenbank direkt ohne Zwischenschaltung einer weiteren Schicht auf die Datenressourcen zugreift. Nehmen wir an, dass zur Laufzeit des Programms immer wieder Daten abgerufen und geändert werden und nicht sehr viele Anwender gleichzeitig dieses Programm einsetzen. Sie haben dann die Wahl, sich zwischen zwei Strategien zu entscheiden: 왘
Sie lassen die Verbindung offen. Damit beansprucht das Programm während der gesamten Laufzeit den Datenbankserver, ist aber hinsichtlich der Performance optimal.
왘
Sie öffnen die Verbindung nur, wenn Sie Befehle gegen die Datenbank absetzen, und schließen die Verbindung anschließend umgehend. Die Datenbank ist dann nicht so belastet wie bei einer permanent geöffneten Verbindung, Sie bezahlen diesen Vorteil aber mit einem Performance-Verlust.
Bitte beachten Sie, dass einige ADO.NET-Objekte Ihnen nur eine eingeschränkte Entscheidungsfreiheit zugestehen. Hier sei die Fill-Methode des DbDataAdapter-Objekts exemplarisch angeführt, die Sie später noch kennenlernen werden. Es kann keinen auf alle denkbaren Einsatzfälle zutreffenden Tipp geben, um Ihnen die Entscheidung abzunehmen. Zu viele Kriterien können dafür entscheidend sein. Wenn Sie keine Entscheidungstendenz erkennen können, sollten Sie das Verhalten von Anwendung und Datenbankserver zumindest in einer simulierten Realumgebung einfach testen. Hinweis In den folgenden Beispielprogrammen wird nicht immer wieder die Verbindungszeichenfolge mit angegeben. Zudem kann es natürlich sein, dass die Beispielprogramme auf der Buch-DVD bei Ihnen nicht funktionieren, weil Sie Ihre eigene Verbindungszeichenfolge angeben müssen.
23.2.7 Testrahmen In der Praxis ist eine geordnete Fehlerbehandlung unerlässlich. Das folgende Codefragment können Sie als Ausgangsbasis für Ihre Programme nehmen:
1104
Verbindungspooling
'...\ADO\Verbindung\Rahmen.vb
Option Strict On Imports System.Data.Common Imports System.Data.SqlClient Namespace ADO Module Rahmen Sub Test() Using con As DbConnection = New SqlConnection() Try con.ConnectionString = "Data Source=(local);" & _ "Initial Catalog=Northwind;Integrated Security=sspi" ' Arbeiten mit der Datenbank con.Open() Catch ex As Exception Console.WriteLine("Datenbankfehler: {0}", ex.Message) Finally If (con.State And ConnectionState.Open) > 0 Then con.Close() End Try End Using Console.ReadLine() End Sub End Module End Namespace
23.3
Verbindungspooling
Stellen Sie sich eine Datenbank im Internet vor, zum Beispiel für Angebote eines Touristikunternehmens. Innerhalb einer kurzen Zeitspanne werden sich mehrere Anwender über die Angebote informieren wollen. Das ständige Auf- und Abbauen der Verbindungen ist jedoch nachteilig, denn mit jedem Aufbau und Abbau einer physischen Verbindung werden die Ressourcen belastet, was zu einer schlechteren Antwortzeit des Datenbankservers führt. Um die Leistung von Datenbankanwendungen zu verbessern, unterstützt ADO.NET das Konzept der Verbindungspools. Bisher wurde gesagt, dass der Aufruf der Methode Close die Verbindung zur Datenbank schließt. Ganz exakt stimmt diese Aussage nicht (wenn man von den Standardeinstellungen ausgeht). Close bewirkt lediglich, dass die Verbindung in einen Pool geschoben wird. Die physische Verbindung bleibt auch dann bestehen, wenn das SqlConnection-Objekt aufgegeben wird. Ein Verbindungspool beherbergt nur Verbindungen, die exakt dieselbe Verbindungszeichenfolge haben. Selbst bei kleinsten Abweichungen wird ein neuer Pool erzeugt. Versucht ein Client, die Verbindung mit einer Datenbank herzustellen, werden zunächst alle vorhandenen Pools nach einer passenden Verbindung durchsucht. Passt eine, wird sie dem anrufenden Client zugeordnet, wenn nicht, wird die angeforderte Verbindung neu erstellt. Der Client
1105
23.3
23
Datenbankverbindung mit ADO.NET
bearbeitet auf dieser Verbindung die Daten und kann sie am Ende mit Close wieder aufgeben. In jedem Fall wird die Verbindung danach einem Pool zugeführt. Für jeden Client, der nicht aus einem vorhandenen Verbindungspool versorgt werden kann, wird eine neue Verbindung erstellt. Bei stark frequentierten Datenbanken führt das auf Dauer zu einem inakzeptablen Anwachsen des Pools. Daher wird eine Verbindung aus dem Pool nach einer bestimmten Zeit der Inaktivität gelöscht, standardmäßig nach circa fünf Minuten. ADO.NET gestattet es Ihnen, das Poolen der Verbindungen zu steuern, um es möglichst gut den spezifischen Anforderungen anzupassen. Sie können sowohl die maximale als auch die minimale Poolgröße festlegen sowie gepoolte Verbindungen manuell freigeben und das Verbindungspooling sogar deaktivieren.
23.3.1 Beispiel für das Verbindungspooling Im folgenden Codefragment wird als Beispiel eine Verbindung zehnmal angefordert: '...\ADO\Verbindung\Verbindungspooling.vb
Option Strict On Imports System.Data.Common Imports System.Data.SqlClient Namespace ADO Module Verbindungspooling Sub Test() Dim con As DbConnection = New SqlConnection( _ "Data Source=(local);Initial Catalog=Northwind;" & _ "Integrated Security=sspi") ' Verbindung 10-mal öffnen und schließen For i As Integer = 0 To 9 con.Open() con.Close() System.Threading.Thread.Sleep(100) Next i Console.ReadLine() End Sub End Module End Namespace
Weiter oben in diesem Kapitel haben wir bereits das Tool SQL Server Profiler aus dem SQL Server Management Studio eingesetzt, um uns von den Auswirkungen der Methode Close zu überzeugen. Natürlich spielte auch bei diesen Beispielen das Verbindungspooling eine Rolle, war aber zum grundlegenden Verständnis der Close-Methode nicht erforderlich. Nun verwenden wir den Profiler, um das Poolen von Verbindungen live zu erleben. In Abbildung 23.3 sehen Sie die Aufzeichnung nach dem Ausführen der Beispielprozedur Verbindungspooling. Obwohl zehnmal eine Verbindung aufgebaut wird, ist dennoch nur ein Loginund ein abschließendes Logout-Ereignis zu sehen. Dies geschieht, weil jede Verbindung nach
1106
Verbindungspooling
dem Öffnen und dem sich anschließenden Schließen mit Close zwar aus Sicht des Clients geschlossen wird, tatsächlich jedoch in einen Pool wandert, aus dem sie bei jedem weiteren Schleifendurchlauf mit Open wieder in Anspruch genommen wird.
Abbildung 23.3
Ablaufverfolgungsprotokoll des SQL Server-Profilers beim Poolen
23.3.2 Verbindungspooling deaktivieren Standardmäßig ist das Pooling aktiviert. Um es zu deaktivieren, ergänzen Sie die Verbindungszeichenfolge wie folgt: Dim con As DbConnection = New SqlConnection("Data Source=wsak\\SQL2005;" & _ "Initial Catalog=Northwind;Integrated Security=sspi;" & _ "Pooling=False")
Wenn Sie die Verbindungszeichenfolge mit einem SqlConnectionStringBuilder-Objekt erzeugen, legen Sie dessen Eigenschaft Pooling auf False fest. Im SQL Server Profiler kann das Abschalten des Poolings wieder anschaulich nachverfolgt werden. Für jedes Verbindungsgesuch wird ein Login- und ein Logout-Ereignis protokolliert, wie Abbildung 23.4 zeigt.
Abbildung 23.4
Ablaufverfolgungsprotokoll des SQL Server-Profilers bei abgeschaltetem Poolen
1107
23.3
23
Datenbankverbindung mit ADO.NET
23.3.3 Verbindungspoolgröße manipulieren Sowohl die Maximalgröße als auch die Minimalgröße eines Verbindungspools lassen sich steuern. Der Standard für die Minimalgröße ist 0, für die Maximalgröße sind 100 gepoolte Verbindungen vorsehen. Betrachten wir zuerst die Minimalgröße etwas genauer, hier mit dem Wert 10. Die ersten 10 parallelen Verbindungen lassen den Pool auf 10 Verbindungen anwachsen. Diese bedienen eventuell anfordernde Clients. Sind mehr Verbindungen notwendig, wird der Pool vergrößert, aber die Mindestanzahl wird nicht mehr unterschritten, auch wenn zeitweise keine Verbindung mehr benötigt wird. Die Lebensdauer von ca. fünf Minuten, die ansonsten für gepoolte Verbindungen gilt, betrifft nicht die zehn Verbindungen, die zur Sicherung der Mindestpoolgröße erforderlich sind. Die Festlegung der Maximalpoolgröße begrenzt die Auslastung eines Datenbankservers. Angenommen, zu einem gegebenen Zeitpunkt ist der Pool ausgeschöpft, weil alle darin enthaltenen Verbindungen aktiv von Clients beansprucht werden. Kommt es dann zu einem weiteren Verbindungsgesuch, wird versucht, innerhalb der Zeitspanne, die in Connect Timeout festgelegt ist, dem anfordernden Client eine Verbindung bereitzustellen. Gelingt das nicht, wird eine InvalidOperationException ausgelöst. Zur Festlegung der minimalen und maximalen Verbindungspoolgröße dienen uns wieder zwei Attribute in der Verbindungszeichenfolge: Min Pool Size und Max Pool Size. Passend dazu werden von einem SqlConnectionStringBuilder-Objekt die beiden Eigenschaften MinPoolSize und MaxPoolSize angeboten. Dim con As DbConnection = New SqlConnection( _ "Data Source=wsak\\SQL2005;Initial Catalog=Northwind;" & _ "Integrated Security=sspi;" & _ "Min Pool Size=5;Max Pool Size=200")
23.3.4 Freigabe gepoolter Verbindungen Gepoolte Verbindungen können mit den beiden statischen Methoden ClearPool und ClearAllPools freigegeben werden. ClearPool wird ein SqlConnection-Objekt übergeben: SqlConnection.ClearPool(con)
Das Connection-Objekt ist notwendig, um über dessen Verbindungszeichenfolge den Pool zu bestimmen, in dem Verbindungen aufgegeben werden sollen. Es werden ausschließlich inaktive Verbindungen entfernt, von Clients aktuell beanspruchte bleiben erhalten. Die parameterlose Methode ClearAllPools löscht alle freien Verbindungen in den Pools.
23.4
Ereignisse eines Connection-Objekts
DbConnection definiert das Ereignis StateChange, und SqlConnection fügt InfoMessage
dazu.
1108
Ereignisse eines Connection-Objekts
23.4.1 StateChange Das Ereignis StateChange tritt auf, wenn sich die State-Eigenschaft ändert. Im Ereignishandler können Sie die Eigenschaften OriginalState und CurrentState des Args-Objekts auswerten, um den alten und neuen Zustand der Verbindung zu überprüfen. '...\ADO\Verbindung\StateChange.vb
Option Strict On Imports System.Data.Common Imports System.Data.SqlClient Namespace ADO Module StateChange Sub Test() Dim con As DbConnection = New SqlConnection( _ "Data Source=(local);Initial Catalog=Northwind;" & _ "Integrated Security=sspi") AddHandler con.StateChange, AddressOf con_StateChange con.Open() con.Close() Console.ReadLine() End Sub Sub con_StateChange(ByVal obj As Object, _ ByVal ev As StateChangeEventArgs) Console.Write("Zustand: von {0}", ev.OriginalState) Console.WriteLine(" nach {0}", ev.CurrentState) End Sub End Module End Namespace
23.4.2 InfoMessage Bei auftretenden Problemen gibt SQL Server eine Informationsmeldung an den Aufrufer zurück, die das Problem beschreibt. Ein Problem kann mehr oder weniger schwerwiegend sein. Um das genauer zu beschreiben, unterscheidet SQL Server Fehler nach ihrem Schweregrad und definiert dazu 25 Stufen. Die Schweregrade 0 bis 10 stehen ausschließlich für Informationsmeldungen zur Verfügung. Fehler des Schweregrads 11 bis 16 kann ein Anwender selbst beheben, ab Schweregrad 17 muss der Datenbankadministrator aktiv werden. Das InfoMessage-Ereignis wird ausgelöst, wenn vom SQL Server eine Meldung mit einem Schweregrad von 10 oder weniger zurückgegeben wird. Im folgenden Beispiel wird die Anweisung PRINT an den SQL Server geschickt. Die auf PRINT folgende Zeichenfolge wird von der Datenbank als Informationsquelle an den Client gesendet, was ein InfoMessage-Ereignis auslöst. Die Servermeldung wird der Eigenschaft Message des Args-Objekts entnommen. Auf die Verwendung von ExecuteNonQuery gehen wir im nächsten Kapitel ein.
1109
23.4
23
Datenbankverbindung mit ADO.NET
'...\ADO\Verbindung\InfoMessage.vb
Option Strict On Imports System.Data.Common Imports System.Data.SqlClient Namespace ADO Module InfoMessage Sub Test() Dim con As SqlConnection = New SqlConnection( _ "Data Source=(local);Initial Catalog=Northwind;" & _ "Integrated Security=sspi") AddHandler con.InfoMessage, AddressOf con_InfoMessage con.Open() Dim cmd As DbCommand = con.CreateCommand() cmd.CommandText = "PRINT 'Informationsmeldung'" cmd.ExecuteNonQuery() con.Close() Console.ReadLine() End Sub Sub con_InfoMessage (ByVal obj As Object, _ ByVal ev As SqlInfoMessageEventArgs) Console.WriteLine("Meldung vom Server: {0}", ev.Message) End Sub End Module End Namespace
Das InfoMessage-Ereignis wird normalerweise nur bei Informations- und Warnmeldungen des Servers ausgelöst. Bei einem tatsächlichen Fehler wird eine Ausnahme ausgelöst, zum Beispiel in Zusammenhang mit den Methoden ExecuteNonQuery oder ExecuteReader, die wir im nächsten Kapitel behandeln. Wollen Sie die Verarbeitung trotz Serverfehlern fortsetzen, legen Sie die FireInfoMessageEventOnUserErrors-Eigenschaft des SqlConnection-Objekts auf True fest. Dadurch löst die
Verbindung beim Auftreten von Fehlern das InfoMessage-Ereignis aus, anstatt eine Ausnahme auszulösen und die Verarbeitung zu unterbrechen.
23.5
Verbindungszeichenfolgen aus einer Konfigurationsdatei abrufen
Bisher haben wir die Verbindungszeichenfolgen immer im Code geschrieben (und wir werden das auch weiter tun). Das spiegelt die Anforderungen in der täglichen Praxis jedoch nur schlecht wider, denn Sie werden nur selten eine Datenbankanwendung entwickeln, die unter Einbeziehung der Produktionsserver-Datenbank getestet wird. Stattdessen werden Sie bestenfalls mit einer Kopie der Datenbank arbeiten, die sich auf einem anderen Rechner befindet und somit eine andere Verbindungszeichenfolge erfordert als die Produktionsdatenbank.
1110
Verbindungszeichenfolgen aus einer Konfigurationsdatei abrufen
Nach dem bisherigen Kenntnisstand bedeutet dies, dass Sie nach dem erfolgreichen Testen und vor der Auslieferung und Installation der Anwendung die Verbindungsinformationen abschließend ändern und noch einmal kompilieren müssen. Auch ein anderes typisches Szenario ist denkbar: Die Produktionsdatenbank wird »verschoben«, beispielsweise auf einem anderen Rechner installiert oder dieser erhält eine andere Netzwerkadresse. Auch hier muss die Anwendung neu kompiliert werden, um mit der neuen Verbindungszeichenfolge den Zugriff auf die Dateninformationen zu gewährleisten. Besser ist es, die Verbindungszeichenfolge isoliert zu betrachten. .NET bietet mit den Konfigurationsdateien dazu die passende Lösung an. Konfigurationsdateien gibt es auf mehreren Ebenen, beispielsweise die Maschinenkonfigurationsdatei für eine lokale Maschine oder die Anwendungskonfigurationsdatei für ein bestimmtes Programm. Konfigurationsdateien werden, soweit vorhanden, vor dem Starten einer .NET-Anwendung ausgewertet. Werden Verbindungszeichenfolgen in Konfigurationsdateien hinterlegt, können sie ohne Neukompilierung der Anwendung geändert werden, und das sogar mit jedem einfachen Texteditor, denn Konfigurationsdateien sind XML-Dateien. An einem Beispiel möchte ich Ihnen zeigen, wie Sie nicht nur eine Anwendungskonfigurationsdatei hinsichtlich der Verbindungszeichenfolge auswerten können, sondern auch, wie Sie mittels Programmcode in die Konfigurationsdatei schreiben: '...\ADO\Verbindung\Konfigurationsdateien.vb
Option Strict On Imports System.Data.Common Imports System.Data.SqlClient Imports System.Configuration Namespace ADO Module Konfigurationsdateien Sub Test() Dim setting As ConnectionStringSettings = _ ConfigurationManager.ConnectionStrings("SQL2005") If setting Is Nothing Then 'kein Eintrag 'SQL2005' setting = New ConnectionStringSettings() setting.Name = "SQL2005" setting.ConnectionString = "Data Source=.;" & _ "Initial Catalog=Northwind;Integrated Security=sspi" Dim config As Configuration = _ ConfigurationManager.OpenExeConfiguration _ (ConfigurationUserLevel.None) config.ConnectionStrings.ConnectionStrings.Add(setting) config.Save() End If Dim con As DbConnection = New SqlConnection(setting.ConnectionString) con.Open()
1111
23.5
23
Datenbankverbindung mit ADO.NET
Console.WriteLine("Verbindung geöffnet") con.Close() Console.ReadLine() End Sub End Module End Namespace
Hinweis In das Projekt muss die Bibliothek System.Configuration.dll eingebunden werden.
Im Code wird zuerst überprüft, ob es in der Anwendungskonfigurationsdatei einen Eintrag namens SQL2005 gibt. Wenn nicht, wird er angelegt und eine Verbindungszeichenfolge definiert. Sollte es noch keine Anwendungskonfigurationsdatei geben, wird diese im Code erzeugt. Danach wird der entsprechende Eintrag aus der Konfigurationsdatei als Argument dem SqlConnection-Konstruktoraufruf übergeben. Nun sollten wir uns auch noch die Anwendungskonfigurationsdatei ansehen:
Anwendungskonfigurationsdateien werden standardmäßig im Verzeichnis der ausführbaren Programmdatei (.exe-Datei) gespeichert. Der Dateibezeichner lautet genauso wie der Dateibezeichner der ausführbaren Datei, ergänzt um .config. Innerhalb des Stammelements können, wie bereits in Kapitel 8, »Anwendungen: Struktur und Installation«, erläutert wurde, eine Vielzahl auswertbarer untergeordneter Elemente definiert werden, zu denen auch zählt. Jeder Eintrag einer Verbindungszeichenfolge steht in einem -Element innerhalb eines -Elements. Die Attribute connectionString und name von müssen angegeben werden, providerName ist optional und legt den Datenprovider für die Verbindungszeichenfolge fest. Die Standardeinstellung ist System.Data.SqlClient. Ändert sich im laufenden Betrieb die Verbindungszeichenfolge, beispielsweise wegen einer Änderung der Netzwerkadresse des Datenbankservers, passen Sie die Verbindungszeichenfolge in der Konfigurationsdatei entsprechend an. Eine Neukompilierung der Anwendung mit erneuter Installation ist nicht notwendig.
1112
Connection im Überblick
23.6
Connection im Überblick
In diesem Abschnitt stelle ich Ihnen alle wichtigen Eigenschaften und Methoden der Klassen DBconnection und SqlConnection vor, die mit dem Aufbau einer Datenbankverbindung in Zusammenhang stehen. Darüber hinaus verfügt die Klassse über Fähigkeiten, die Struktur einer Datenbank zu erkunden. Diesem Thema widmen wir uns aber noch im Kapitel 26, »Offline mit DataSet«.
23.6.1 Vererbungshierarchie Der folgende Ausschnitt aus der Klassenhierarchie zeigt, dass Sie leicht auf andere Datenbankprovider umsteigen können, wenn Sie sich auf Funktionen von DbConnection und DbConnectionStringBuilder beschränken. System.Object ÃSystem.Data.Common.DbConnectionStringBuilder ³ ÃSystem.Data.Odbc.OdbcConnectionStringBuilder ³ ÃSystem.Data.OleDb.OleDbConnectionStringBuilder ³ ÃSystem.Data.OracleClient.OracleConnectionStringBuilder ³ ÀSystem.Data.SqlClient.SqlConnectionStringBuilder ÀSystem.MarshalByRefObject ÀSystem.ComponentModel.Component ÀSystem.Data.Common.DbConnection ÃSystem.Data.Odbc.OdbcConnection ÃSystem.Data.OleDb.OleDbConnection ÃSystem.Data.OracleClient.OracleConnection ÃSystem.Data.SqlClient.SqlConnection ÀSystem.Data.SqlServerCe.SqlCeConnection
23.6.2 Ereignisse eines SqlConnection-Objekts Ereignis
Beschreibung
StateChange As StateChangeEventHandler
Aufruf bei Änderungen von State
InfoMessage As SqlInfoMessageEventHandler
Wird ausgelöst, wenn der SQL Server Warnoder Informationsmeldungen schickt.
Tabelle 23.3 Ereignisse von »System.Data.Common.DbConnection« (kursiv = nur »System.Data.SqlClient.SqlConnection«)
23.6.3 Eigenschaften eines SqlConnection-Objekts »Eigenschaft
Beschreibung
Art
ConnectionString() As String
Zeichenfolge, die beim Verbindungsaufbau an den % Datenbankserver gesendet wird.
ConnectionTimeout() As Integer
Maximalzeit zum Verbindungsaufbau.
R
Tabelle 23.4 Eigenschaften von »System.Data.Common.DbConnection« (kursiv = nur »System.Data.SqlClient.SqlConnection«, R = ReadOnly, % = »MustOverride in DbConnection«)
1113
23.6
23
Datenbankverbindung mit ADO.NET
Eigenschaft
Beschreibung
Art
Database() As String
Aktuell offene Datenbank.
R%
DataSource() As String
Name des Datenbankservers.
R%
ServerVersion() As String
Versionsdaten des Dantenbank-Servers.
R%
State() As ConnectionState
Verbindungsstatus.
R%
WorkstationId() As String
(Netzwerk-)Name des Clients.
PacketSize() As Integer
Paketgröße in Bytes des Netzwerkverkehrs.
StatisticsEnabled() As Boolean
Sammlung von Statistikdaten erlauben.
FireInfoMessageEventOnUserErrors() As Boolean
InfoMessage Ereignisse im Fehlerfall statt Ausnahmen.
Tabelle 23.4 Eigenschaften von »System.Data.Common.DbConnection« (kursiv = nur »System.Data.SqlClient.SqlConnection«, R = ReadOnly, % = »MustOverride in DbConnection«) (Forts.)
23.6.4 Methoden des Connection-Objekts Die primäre Aufgabe eines Connection-Objekts ist das Öffnen und Schließen einer Datenbankverbindung mit den Methoden Open und Close. Einige Datenbankserver, beispielsweise SQL Server, unterstützen mehrere Datenbanken. Wenn Sie mit dem SQL Server arbeiten, müssen Sie den USE-Befehl verwenden, um die Datenbank zu wechseln: USE Northwind
Im folgenden Kapitel werden Sie lernen, wie ein Befehl gegen eine Datenbank abgesetzt wird. Dies soll an dieser Stelle schon einmal vorab und ohne weitere Erklärung gezeigt werden: Dim cmd As DbCommand = con.CreateCommand() cmd.CommandText = "USE Northwind" cmd.ExecuteNonQuery()
Mit der Methode ChangeDatabase des Connection-Objekts können Sie das Gleiche deutlich einfacher erreichen. Vorausgesetzt wird dabei nur eine geöffnete Verbindung: con.Open() con.ChangeDatabase("Northwind")
In Tabelle 23.5 der Methoden eines Connection-Objekts tauchen unter anderem BeginTransaction, CreateCommand und GetSchema auf, die Fähigkeiten eines Verbindungsobjekts bezeichnen, die über das Erstellen einer Verbindung hinausgehen. Methode
Beschreibung
New([connectionString])
Konstruktor
Open()
Verbindung öffnen
Art %
Tabelle 23.5 Methoden von »System.Data.Common.DbConnection« (kursiv = nur »System.Data.SqlClient.SqlConnection«, S = Shared, % = »MustOverride in DbConnection«, [ ] = optional)
1114
Verbindungen mit dem OleDb-Datenprovider
Methode
Beschreibung
Art
ChangeDatabase(databaseName)
Datenbank einer offenen Verbindung setzen
%
Close()
Verbindung schließen
%
CreateCommand() As DbCommand CreateCommand() As SqlCommand
Datenbankkommando erzeugen
BeginTransaction([isolationLevel]) As DbTransaction BeginTransaction([isolationLevel, ] transactionName) As SqlTransaction
Start einer untrennbaren Aktionsfolge (Transaktion)
EnlistTransaction(transaction) EnlistDistributedTransaction(iTransaction)
Registrierung in einer verteilten Transaktion
GetSchema([collectionName [, restrictionValues]]) As DataTable
Struktur einer Datenbank ermitteln
ClearAllPools()
Inaktive Verbindungen entfernen
S
ClearPool(sqlConnection)
Inaktive Verbindungen entfernen
S
ChangePassword(connectionString, newPassword)
Änderung des Kennworts für den Zugriff auf den SQL Server
S
ResetStatistics()
Zurücksetzen von Statistikdaten
RetrieveStatistics() As IDictionary
Abruf von Statistikdaten
Tabelle 23.5 Methoden von »System.Data.Common.DbConnection« (kursiv = nur »System.Data.SqlClient.SqlConnection«, S = Shared, % = »MustOverride in DbConnection«, [ ] = optional) (Forts.)
23.7
Verbindungen mit dem OleDb-Datenprovider
Im Gegensatz zum SqlClient-Datenprovider, der nur den Zugriff auf SQL Server ab Version 7.0 ermöglicht, ist der OleDb-Datenprovider sehr flexibel einsetzbar. Er unterstützt nicht nur SQL Server, sondern auch alle OLE DB-Datenbanken, beispielsweise Oracle und Access. Der SqlClient-Datenprovider und der OleDb-Provider unterscheiden sich nur wenig. Sie sollten aber daran denken, vorher den richtigen Namensraum einzubinden: Imports System.Data.OleDb
Zum Aufbau einer Verbindung benötigt auch der OleDb-Provider ein Connection-Objekt. Der Name der Klasse, OleDbConnection, lehnt sich an den ausgewählten Provider an. Die Verbindungszeichenfolge wird ebenfalls entweder über den parametrisierten Konstruktor oder über die Eigenschaft ConnectionString bereitgestellt. Die Attribute der Verbindungszeichenfolge gleichen denen des SqlClient-Datenproviders, werden jedoch noch um das Attribut Provider ergänzt, mit dem die Datenquelle genauer zu spezifizieren ist. In Tabelle 23.6 sind die wichtigsten Attributwerte aufgeführt.
1115
23.7
23
Datenbankverbindung mit ADO.NET
Wert
Beschreibung
SQLOLEDB
SQL Server-Datenprovider
Microsoft.Jet.OLEDB.4.0
Datenprovider der Jet-Datenbanken (MS-Access)
MSDAORA
OleDb-Datenprovider für Oracle
Tabelle 23.6
Werte des Attributs Provider (Auszug)
In der Tabelle ist ein Wert für den Zugriff auf ODBC-Datenquellen nicht angegeben, denn für diese sollten die Klassen des Namensraums System.Data.Odbc benutzt werden.
23.7.1 Verbindungsaufbau zu einer SQL Server-Datenbank Das folgende Codefragment zeigt, wie eine Verbindung zur Beispieldatenbank Northwind einer SQL Server-Instanz hergestellt wird, die sich auf dem Rechner wsak befindet. Als OleDbProvider dient der Providername SQLOLEDB. Der Authentifizierungsmodus ist bei diesem Codefragment die SQL Server-Authentifizierung. Dim strCon As String = "Provider=SQLOLEDB;Data Source=wsak;" & _ "Initial Catalog=Northwind;" & _ "User ID=testuser;Password=2zz6sl3" Dim con As DbConnection = New OleDbConnection(strCon) con.Open() ' mit der Datenbank arbeiten con.Close()
23.7.2 Verbindungsaufbau zu einer Access-Datenbank Um die Verbindung zu einer Access-Datenbank herzustellen, wird der spezifische Datenprovider Microsoft.Jet.OLEDB.4.0 benutzt. Die Ziffern geben die Version der Datenbank an. Die Verbindungszeichenfolge sieht etwas anders aus als die, mit der die Verbindung zum SQL Server hergestellt wird. Hinter dem Attribut Data Source wird nun nicht mehr der Rechnername angegeben, sondern die Pfadangabe zur .mdb-Datei, da es sich um eine dateibasierte Datenbank handelt. Dim con As DbConnection = New OleDbConnection() con.ConnectionString = "Provider=Microsoft.Jet.OLEDB.4.0;" & _ "Data Source=C:\FPNWIND.mdb" con.Open()
Um die Verbindung genauer zu beschreiben, steht eine Reihe weiterer Schlüsselwörter zur Verfügung. Sie können diese der Microsoft Data Access SDK entnehmen.
23.7.3 Authentifizierung mit dem OleDb-Provider Der OleDb-Datenprovider bietet für SQL Server eine weitere interessante Möglichkeit zur Authentifizierung des Anwenders. Dazu muss weder der Benutzername noch das Kennwort in der Verbindungszeichenfolge angegeben werden. Ergänzen Sie diese vielmehr um prompt=prompt, also beispielsweise so:
1116
Verbindungen mit dem OleDb-Datenprovider
con.ConnectionString = "Provider=SQLOLEDB;Data Source=wsak;" & _ "Initial Catalog=Northwind;prompt=prompt"
Beim Verbindungsaufbau mit der Methode Open wird daraufhin ein Dialog geöffnet, wie in Abbildung 23.5 gezeigt.
Abbildung 23.5
Anmeldedialog des OleDb-Datenproviders
Beabsichtigt der Anwender, sich über sein aktuelles Windows-Benutzerkonto zu authentifizieren, wird die Auswahlbox Vertrauenswürdige Verbindung verwenden gewählt. Soll die Verbindung über die spezifische SQL Server-Authentifizierung hergestellt werden, muss der Anwender den entsprechenden Benutzernamen und das passende Passwort eingeben.
1117
23.7
In diesem Kapitel wird gezeigt, wie eine Datenbank verändert und ausgelesen wird. Weitere Themen sind das Auslesen der Datenbankstruktur und gespeicherte Prozeduren auf dem Datenbankserver.
24
Datenbankabfragen mit ADO.NET
Voraussetzung jeder Datenbankabfrage ist die Verbindung zu der Datenquelle. Wie Sie das Connection-Objekt dazu erzeugen, habe ich im letzten Kapitel gezeigt. Nun gehen wir den nächsten Schritt und widmen uns dem Abruf von Daten aus der Datenbank. Ich erkläre auch, wie Daten in der Originaldatenbank verändert, hinzugefügt und gelöscht werden. Für solche Operationen stellt ADO.NET eine weitere Klasse zur Verfügung, die je nach eingesetztem Datenprovider SqlCommand, OleDbCommand oder OdbcCommand heißt. Command-Objekte sind auf die Verbindung zum Datenbankserver angewiesen. Der folgende Ausschnitt aus der Vererbungshierarchie zeigt, dass Sie durch eine Beschränkung auf die Funktionen der Klassen DbCommand und DbCommandBuilder leicht zu einem anderen Datenbankprovider wechseln können. System.Object ÀSystem.MarshalByRefObject ÀSystem.ComponentModel.Component ÃSystem.Data.Common.DbCommand ³ ÃSystem.Data.Odbc.OdbcCommand ³ ÃSystem.Data.OleDb.OleDbCommand ³ ÃSystem.Data.OracleClient.OracleCommand ³ ÃSystem.Data.SqlClient.SqlCommand ³ ÀSystem.Data.SqlServerCe.SqlCeCommand ÀSystem.Data.Common.DbCommandBuilder ÃSystem.Data.Odbc.OdbcCommandBuilder ÃSystem.Data.OleDb.OleDbCommandBuilder ÃSystem.Data.OracleClient.OracleCommandBuilder ÃSystem.Data.SqlClient.SqlCommandBuilder ÀSystem.Data.SqlServerCe.SqlCeCommandBuilder
Neben der Klasse DbCommand werden Sie weitere wichtige Klassen kennenlernen, allen voran die Klasse DbDataReader, die die Datensätze einer Ergebnisliste durchläuft oder Schemainformationen einer Tabelle abruft. DbDataReader ist tatsächlich in der gesamten ADO.NET-Klassenbibliothek die einzige Klasse, die Dateninformationen abrufen kann. Auch wenn wir uns später mit der Klasse DbDataAdapter beschäftigen, die über die Methode Fill ein DataSet zu füllen vermag, hält der DbDataReader im Hintergrund die Fäden in der Hand. Von außen betrachtet, können wir das allerdings nicht direkt erkennen. Am Ende des ADO.NET-Teils dieses Buches werden Sie noch einmal dem DbCommand-Objekt begegnen, wenn wir abweichend
1119
24
Datenbankabfragen mit ADO.NET
von der Standardvorgabe unsere eigene Aktualisierungslogik codieren. Auch bei diesen anderen Klassen lohnt sich eine Beschränkung auf Klassen des Namensraums System.Data. Common, um einen Wechsel des Datenbankproviders zu erleichtern. System.Object ÀSystem.MarshalByRefObject ÃSystem.ComponentModel.Component ³ ÀSystem.Data.Common.DataAdapter ³ ÀSystem.Data.Common.DbDataAdapter ³ ÃMicrosoft.AnalysisServices.AdomdClient.AdomdDataAdapter ³ ÃSystem.Data.Odbc.OdbcDataAdapter ³ ÃSystem.Data.OleDb.OleDbDataAdapter ³ ÃSystem.Data.OracleClient.OracleDataAdapter ³ ÃSystem.Data.SqlClient.SqlDataAdapter ³ ÀSystem.Data.SqlServerCe.SqlCeDataAdapter ÀSystem.Data.Common.DbDataReader ÃSystem.Data.DataTableReader ÃSystem.Data.Odbc.OdbcDataReader ÃSystem.Data.OleDb.OleDbDataReader Ã3System.Data.OracleClient.OracleDataReader ÃSystem.Data.SqlClient.SqlDataReader ÀSystem.Data.SqlServerCe.SqlCeDataReader ÀSystem.Data.SqlServerCe.SqlCeResultSet
Hinweis Wenn möglich werden die Db- statt der Sql-Typen verwendet. Alle Aussagen, die sich im Folgenden auf die Db-Typen beziehen, gelten auch für die analogen Sql-Typen.
24.1
DbCommand
Das DbCommand-Objekt repräsentiert einen SQL-Befehl oder eine gespeicherte Prozedur. In der Eigenschaft CommandText wird die SQL-Anweisung bzw. die gespeicherte Prozedur festgelegt. Die Ausführung wird mit einer der Execute-Methoden gestartet. Die Klasse DbCommand selbst ist abstrakt, und die Objekterzeugung erfolgt über eine providerspezifische Klasse, zum Beispiel SqlCommand. Als kleinen Vorgeschmack möchte ich Ihnen ein Beispiel zeigen. Darin wird die Verbindung zu der Beispieldatenbank Northwind des SQL Servers aufgebaut. In der Tabelle Products, in der alle Artikel geführt sind, ist unter anderem ein Artikel mit der Bezeichnung Chai (Spalte ProductName) gespeichert. Nehmen wir an, dieser sei falsch und soll nun in Tee geändert werden. Dazu übergeben wir der Eigenschaft CommandText des DbCommand-Objekts ein entsprechendes UPDATE-Kommando und führen es mit ExecuteNonQuery aus.
1120
DbCommand
'...\ADO\Datenbankabfragen\Command.vb
Option Strict On Imports System.Data.Common Imports System.Data.SqlClient Namespace ADO Module Command Sub Test() Dim con As DbConnection = New SqlConnection() con.ConnectionString = "Data Source=(local);" & _ "Initial Catalog=Northwind;Integrated Security=sspi" Dim cmd As DbCommand = New SqlCommand() cmd.CommandText = "UPDATE Products " & _ "SET ProductName='Tee' WHERE ProductName='Chai'" cmd.Connection = con con.Open() cmd.ExecuteNonQuery() con.Close() Console.ReadLine() End Sub End Module End Namespace
Hinweis Wenn Sie in UPDATE die WHERE-Klausel vergessen, werden alle Zeilen geändert.
Vom Erfolg der Operation können Sie sich auf verschiedene Weisen überzeugen. Sie könnten sich einerseits mit dem Tool SQL Server Management Studio von SQL Server den Inhalt der nun geänderten Tabelle anzeigen lassen. Sie können das aber auch aus dem Server-Explorer des Visual Studio heraus, den Sie über das Menü Ansicht öffnen. Fügen Sie über das Kontextmenü des Knotens Datenverbindungen die Verbindung zu der Datenbank Northwind hinzu. Ein Assistent, den wir uns später noch genauer ansehen werden, führt Sie durch den gesamten Verbindungsprozess, an dessen Ende Sie die Möglichkeit haben, sich den aktuellen Inhalt der Tabelle Products in Visual Studio anzeigen zu lassen.
24.1.1 Ein DbCommand-Objekt erzeugen Um ein Kommando gegen eine Datenbank abzusetzen, wird ein DbCommand-Objekt benötigt. Dies trifft für eine Auswahlabfrage (SELECT) ebenso zu wie für eine Aktionsabfrage (INSERT, UPDATE oder DELETE). Das SQL-Kommando wird der Eigenschaft CommandText des DbCommand-Objekts zugewiesen. Zusätzlich zum Befehl muss das DbCommand-Objekt auch den Datenbankserver und die Datenbank kennen, das heißt, es muss wissen, welches DbConnection-Objekt die Verbindung zur Datenbank beschreibt.
1121
24.1
24
Datenbankabfragen mit ADO.NET
Um diese Anforderungen zu erfüllen, stehen Ihnen mehrere Konstruktoren zur Verfügung. Verwenden Sie den parameterlosen Konstruktor, müssen Sie der Eigenschaft Connection des DbCommand-Objekts die Referenz auf ein DbConnection-Objekt zuweisen. Einer anderen Konstruktorüberladung können Sie neben der Referenz auf das SqlConnection-Objekt auch das abzusetzende Kommando als Zeichenfolge übergeben. Dim cmd As DbCommand = New SqlCommand("UPDATE Products " & _ "SET ProductName='Tee' WHERE ProductName='Chai'")
Eine zweite Variante zur Erzeugung eines DbCommand-Objekts ruft die Methode CreateCommand eines DbConnection-Objekts auf. Dim con As DbConnection = New SqlConnection("...") Dim cmd As DbCommand = con.CreateCommand()
Hinweis Wenn Sie eine provider-spezifische Verbindung verwenden, ist das Command-Objekt auch providerspezifisch.
24.1.2 Ausführen des DbCommand-Objekts Die CommandText-Eigenschaft legt das Kommando fest, das ausgeführt werden soll. Es kann sich dabei um ein SQL-Kommando oder eine gespeicherte Prozedur handeln. Bei den SQLKommandos werden zwei Kategorien unterschieden: 왘
Auswahlabfragen
왘
Aktionsabfragen
Eine Auswahlabfrage verwendet die SELECT-Klausel und liefert immer ein Ergebnis zurück, im Fall einer Aggregatfunktion wie SUM oder COUNT liefert sie aber nur einen Ergebniswert. Eine typische Auswahlabfrage wäre zum Beispiel: SELECT ProductName, UnitPrice FROM Products WHERE UnitPrice < 100
Das Resultat dieser Abfrage bilden alle Datensätze der Tabelle Northwind, die diejenigen Produkte beschreiben, deren Preis kleiner 100 ist. Eine Aktionsabfrage manipuliert die Datenbank. Dabei kann es sich um die 왘
Aktualisierung der Daten (DML-Abfrage = Data Manipulation Language-Abfrage) oder um die
왘
Änderung der Datenbankstruktur (DDL-Abfrage = Data Definition Language-Abfrage)
handeln. Mit UPDATE Products SET ProductName='Tee' WHERE ProductName='Chai'
1122
DbCommand
haben wir eingangs eine DML-Abfrage abgesetzt, die zwar einen Datensatz in Products ändert, aber die geänderten Daten nicht zurückgibt, sondern nur die Anzahl Änderungen. Wie Sie sehen, führt das Absetzen eines Befehls zu ganz unterschiedlichen Reaktionen des Datenbankservers. Das SqlCommand-Objekt trägt dem Rechnung und stellt mit 왘
ExecuteNonQuery
왘
ExecuteReader
왘
ExecuteScalar
왘
ExecuteXmlReader
vier Methoden zur Verfügung, die speziell auf die einzelnen Abfragen abgestimmt sind. Die ersten drei sind auch in DbCommand enthalten. Alle Methoden werden synchron ausgeführt. Synchron bedeutet, dass die Clientanwendung nach dem Methodenaufruf so lange wartet, bis das Ergebnis der Anfrage vom Datenbankserver eintrifft. Gegebenenfalls kann das eine längere Zeitspanne beanspruchen. Sie können aber Datenbankabfragen auch asynchron ausführen. Der Client muss dann nicht warten, bis die Abfrageausführung beendet ist, sondern kann weiterarbeiten, bis ihm signalisiert wird, dass die Ergebnisse vollständig vorliegen. Entsprechende asynchrone Methoden werden vom Command-Objekt bereitgestellt.
24.1.3 Begrenzung der Abfragezeit durch CommandTimeout Wird eine Abfrage mit ExcuteScalar, EcexuteNonQuery oder ExcuteReader ausgeführt, wartet das SqlCommand-Objekt per Vorgabe 30 Sekunden auf das Eintreffen der ersten Abfrageergebnisse (andere Provider können unterschiedliche Zeiten festlegen). Das Überschreiten der eingestellten Zeit hat zur Folge, dass eine Ausnahme ausgelöst wird. Mit der Eigenschaft CommandTimeout kann die Voreinstellung verändert werden. Mit der Einstellung »0« wartet das SqlCommand-Objekt eine unbegrenzte Zeit. Dies ist problematisch, da eine Abfrage durch einen Fehler »unendlich« lange dauern kann. Die Zeit für eine Abfrage beginnt mit dem Senden des Kommandos an den Server und endet mit der Serverantwort.
24.1.4 Aktionsabfragen absetzen Abfragen, die Änderungen an den Originaldaten der Datenbank nach sich ziehen (UPDATE, DELETE, INSERT) oder die Struktur einer Datenbank verändern (z. B. CREATE TABLE), werden mit der Methode ExecuteNonQuery abgesetzt. Public MustOverride Function ExecuteNonQuery() As Integer
Handelt es sich bei dem Befehl um ein UPDATE-, INSERT- oder DELETE-Kommando, stellt der Rückgabewert die Anzahl der von der Anweisung betroffenen Datenzeilen dar.
1123
24.1
24
Datenbankabfragen mit ADO.NET
Datensätze hinzufügen In der folgenden SQL-Syntax passen Sie die kursiven Namen Ihren Bedürfnissen an (optionale Teile stehen in eckigen Klammern, und Alternativen werden durch | getrennt). INSERT [INTO] Tabelle[(Spalten)] (VALUES(Werte) | SELECT... | EXECUTE...)
Das folgende Beispielprogramm fügt der Tabelle Products einen Datensatz hinzu. Dabei wird der parametrisierte Konstruktor der Klasse SqlCommand verwendet, der im ersten Parameter den SQL-Befehl und im zweiten die Referenz auf das SqlConnection-Objekt entgegennimmt (DbConnection geht nicht). '...\ADO\Datenbankabfragen\Insert.vb
Option Strict On Imports System.Data.Common, System.Data.SqlClient Namespace ADO Module Insert Sub Test() Dim SqlCon As SqlConnection = New SqlConnection("Data Source=.;" & _ "Initial Catalog=Northwind;Integrated Security=sspi") Dim SQL_Befehl As String = _ "INSERT INTO Products(ProductName, Discontinued) " & _ "VALUES('Schweizer Käse',0)" Try Dim cmd As DbCommand = New SqlCommand(SQL_Befehl, SqlCon) SqlCon.Open() Dim zeilen As Integer = cmd.ExecuteNonQuery() Console.WriteLine("{0} Zeile(n) geändert.", zeilen) Catch ex As Exception Console.WriteLine("Fehler: {0}", ex.Message) Finally SqlCon.Close() End Try Console.ReadLine() End Sub End Module End Namespace
Die Ausgabe bestätigt das Einfügen: 1 Zeile(n) geändert.
Datensätze löschen In der folgenden SQL-Syntax passen Sie die kursiven Namen Ihren Bedürfnissen an (optionale Teile stehen in eckigen Klammern): DELETE [FROM] Tabelle [WHERE Bedingung]
1124
DbCommand
Der Datensatz aus dem vorhergehenden Beispiel soll nun wieder gelöscht werden, und zwar zweifach: '...\ADO\Datenbankabfragen\Delete.vb
Option Strict On Imports System.Data.Common, System.Data.SqlClient Namespace ADO Module Delete Sub Test() Dim SqlCon As SqlConnection = New SqlConnection("Data Source=.;" & _ "Initial Catalog=Northwind;Integrated Security=sspi") Dim SQL_Befehl As String = _ "DELETE FROM Products WHERE ProductName='Schweizer Käse'" Try Dim cmd As DbCommand = New SqlCommand(SQL_Befehl, SqlCon) SqlCon.Open() Console.WriteLine("{0} Zeile(n) geändert.", cmd.ExecuteNonQuery()) Console.WriteLine("{0} Zeile(n) geändert.", cmd.ExecuteNonQuery()) Catch ex As Exception Console.WriteLine("Fehler: {0}", ex.Message) Finally SqlCon.Close() End Try Console.ReadLine() End Sub End Module End Namespace
Hinweis Wenn Sie in DELETE die WHERE-Klausel vergessen, wird alles gelöscht.
Nach dem ersten Löschbefehl befindet sich kein der WHERE-Klausel entsprechender Datensatz mehr in der Tabelle, und es wird nichts weiter gelöscht. 1 Zeile(n) geändert. 0 Zeile(n) geändert.
Datensätze ändern In der folgenden SQL-Syntax passen Sie die kursiven Namen Ihren Bedürfnissen an (optionale Teile stehen in eckigen Klammern, und Alternativen werden durch | getrennt). UPDATE Tabelle SET Spalte=Wert [(WHERE (Bedingung | CURRENT OF cursor) | FROM TabelleX WHERE Bedingung)]
Zu Beginn dieses Abschnitts wurde in dem Beispiel Command bereits gezeigt, wie Sie Datensätze in der Datenbank editieren können.
1125
24.1
24
Datenbankabfragen mit ADO.NET
24.1.5 Abfragen mit einem Einzelergebnis Mit der SELECT-Anweisung können Sie Datensätze nach bestimmten Auswahlkriterien aus einer Datenbank abrufen. Der Befehl SELECT wird aber auch dann benutzt, wenn eine Aggregatfunktion definiert werden soll, die ein einzelnes Ergebnis zurückliefert. Beispielsweise können Sie mit dem Aggregat SELECT COUNT(*) FROM Products
die Anzahl der Artikel in der Tabelle Products ermitteln und mit SELECT COUNT(*) FROM Products WHERE CategoryID = 1
feststellen, wie viele Artikel zur Kategorie 1 gehören. Tabelle 24.1 zeigt die fünf Standardaggregatfunktionen: Funktion(Spalten)
Beschreibung
COUNT
Anzahl der Werte in der Spalte
SUM
Summe der Werte in der Spalte
AVG
Mittelwert der Spalte
MAX
Größter Wert in der Spalte
MIN
Kleinster Wert in der Spalte
Tabelle 24.1
Aggregatfunktionen
Um den Rückgabewert einer Aggregatfunktion entgegenzunehmen, rufen Sie die Methode ExecuteScalar des SqlCommand-Objekts auf. Der Typ der Rückgabe ist Object, daher muss das Ergebnis noch in den passenden Datentyp konvertiert werden. Dim strSQL As String = "SELECT COUNT(*) FROM Products WHERE CategoryID=1" Dim cmd As DbCommand = new SqlCommand(strSQL, SqlCon) Dim anzahlDS As Integer = Convert.ToInt32(cmd.ExecuteScalar())
24.2
SqlDataReader
Mit der Methode ExecuteNonQuery des DbCommand-Objekts können Sie Datensätze in der Originaldatenbank manipulieren, und mit ExecuteScalar können Sie ein einzelnes Abfrageergebnis abrufen. Möchte man sich die Datensätze einer Tabelle in einer Anwendung anzeigen lassen, ruft man die Methode ExecuteReader des DbCommand-Objekts auf. Public Function ExecuteReader() As DbDataReader
Das Ergebnis des Methodenaufrufs ist ein Objekt vom Typ DbDataReader. Dieses ähnelt den anderen Reader-Objekten des .NET-Frameworks (TextReader, StreamReader usw.). Ein DbDataReader-Objekt liest aus einer Ergebnisliste, die schreibgeschützt ist und sich in einem serverseitigen Puffer befindet, also auf der Seite der Datenbank. Sie sollten daher beherzigen, die Ergebnisliste so schnell wie möglich abzurufen, damit die beanspruchten Ressourcen wieder freigegeben werden.
1126
SqlDataReader
Beabsichtigen Sie, an den Datensätzen Änderungen vorzunehmen, ist der DbDataReader völlig ungeeignet. In einer von einem DbDataReader-Objekt bereitgestellten Datensatzliste kann immer nur zum folgenden Datensatz navigiert werden. Damit hat ein DbDataReader nur sehr eingeschränkte Möglichkeiten, aber eine sehr gute Performance. Er bietet sich insbesondere dann an, wenn Komponenten wie List- oder Comboboxen gefüllt werden sollen. Das Erzeugen eines DataReader-Objekts funktioniert nur, indem man die Fabrikmethode ExecuteReader einer DbCommand-Referenz aufruft, denn keine providerspezifische DataReader-Klasse hat einen öffentlichen Konstruktor. Dim dr As DbDataReader = cmd.ExecuteReader()
24.2.1 Das SQL-Kommando SELECT Das wichtigste SQL-Kommando zum Auslesen von Daten aus der Datenbank ist SELECT. Aufgrund der zentralen Stellung ist die Syntax recht komplex. Sie wird hier nur als Referenz wiedergegeben und soll nicht näher erläutert werden. Die Länge soll Ihnen einen Eindruck von der Mächtigkeit des Befehls geben. Optionale Teile sind in eckige Klammern gesetzt und Alternativen durch | getrennt. Kursive Namen passen Sie Ihren Bedürfnissen an. Nicht alle Datenbankserver unterstützen den kompletten Funktionsumfang, andere kennen zusätzliche Varianten. Häufig dürfen Spalten berechnet sein. [WITH Tabelle(Spalte(n)) AS (Select)] SELECT [DISTINCT|All][TOP Anzahl [PERCENT] [WITH TIES]] Spalte(n) [INTO NeueTabelle] FROM Tabelle(n) | join(s) | OPENROWSET(...) | OPENQUERY(...) | OPENXML(...) [WHERE SelektionsBedingung] [[GROUP BY Spalte(n)] [HAVING GruppenSelektionsBedingung]] [ORDER BY Spalte(n)|SpaltenNummer(n) [ASC|DESC]] [[COMPUTE SkalarFunktion] [BY SpaltenAusOrderBy]] [FOR BROWSE|XMLOptionen]
24.2.2 Datensätze einlesen Im folgenden Beispielprogramm wird ein DbDataReader dazu benutzt, alle Artikel zusammen mit ihrem Preis nach dem Preis sortiert auszugeben. '...\ADO\Datenbankabfragen\DataReader.vb
Option Strict On Imports System.Data.Common, System.Data.SqlClient Namespace ADO Module DataReader Sub Test() Dim con As DbConnection = New SqlConnection("Data Source=.;" & _ "Initial Catalog=Northwind;Integrated Security=sspi")
1127
24.2
24
Datenbankabfragen mit ADO.NET
Dim cmd As DbCommand = New SqlCommand( _ "SELECT ProductName, Unitprice FROM Products ORDER BY UnitPrice") cmd.Connection = con Try con.Open() Dim dr As DbDataReader = cmd.ExecuteReader() ' Einlesen und ausgeben der Datenzeilen While (dr.Read()) Console.WriteLine("{0,-35}{1}",dr("ProductName"),dr("UnitPrice")) End While dr.Close() Catch ex As Exception Console.WriteLine("Fehler: {0}", ex.Message) Finally con.Close() End Try Console.ReadLine() End Sub End Module End Namespace
Hinweis Ein DbDataReader kann nur bei geöffneter Verbindung erzeugt werden.
Nachdem das DbCommand-Objekt mit der SELECT-Abfrage an das DbConnection-Objekt gebunden wurde, wird die Verbindung geöffnet. Danach erzeugt das Kommando mit ExecuteReader einen Cursor auf dem Datenbankserver zum Auslesen. Eine Referenz auf den Cursor wird in der Variablen dr vom Typ DbDataReader gespeichert. DbDataReader liefert alle Datensätze, die der Reihe nach durchlaufen werden müssen. Nur mit
der Methode Read des DataReader-Objekts kann auf die Datensätze zugegriffen werden. Public MustOverride Function Read() As Boolean
Jeder Aufruf von Read legt die Position des DbDataReaders neu fest. Die Ausgangsposition vor dem ersten Read-Aufruf befindet sich vor dem ersten Datensatz. Nach dem Aufruf von Read ist der Rückgabewert True, falls noch eine weitere Datenzeile abgerufen werden kann. Ist der Rückgabewert False, ist kein weiterer Datensatz mehr verfügbar. Damit eignet sich Read, um die Datensatzliste in einer While-Schleife zu durchlaufen. Für jeden Durchlauf durch die Daten müssen Sie ExecuteReader erneut aufrufen.
Spaltenindizierung Mit Read wird die Position des DbDataReaders auf die folgende Datenzeile verschoben. In unserem Beispiel hat jede Datenzeile zwei Feldinformationen, nämlich die der Spalten Pro-
1128
SqlDataReader
ductName und UnitPrice. Die einzelnen Spalten einer Abfrage werden in einer Auflistung geführt, auf die über den Index des DbDataReader-Objekts zugegriffen werden kann. dr(0)
Sie können aber auch den Spaltenbezeichner angeben, also: dr("ProductName")
Bezüglich der Performance gibt es einen Unterschied. Geben Sie den Spaltennamen an, muss das DbDataReader-Objekt zuerst die Spalte suchen, und das bei jeder Datenzeile. While (dr.Read()) Console.WriteLine("{0,-35}{1}", dr("ProductName"), dr("UnitPrice")) End While
Das Programm läuft schneller, wenn Sie den Index der betreffenden Spalte angeben: While (dr.Read()) Console.WriteLine("{0,-35}{1}", dr(0), dr(1)) End While
Ist Ihnen nur der Spaltenbezeichner, jedoch nicht der dazugehörige Index bekannt, haben Sie mit der Methode GetOrdinal der Klasse DbDataReader unter Angabe des Spaltenbezeichners die Möglichkeit, vor dem Aufruf von Read den Index zu ermitteln: Dim intLname As Integer = dr.GetOrdinal("au_lname") Dim intFname As Integer = dr.GetOrdinal("au_fname") Dim intCity As Integer = dr.GetOrdinal("city") While (dr.Read()) Console.WriteLine("{0,-20}{1,-20}{2,-20}", _ dr(intLname), dr(intFname), dr(intCity)) End While
Typspezifische Auslesemethoden Der Indexer der Methode ExecuteReader gibt die Spaltenwerte als Object zurück. Das hat Leistungseinbußen zur Folge, weil der tatsächliche Typ erst in Object umgewandelt werden muss. Anstatt über den Indexer die Daten auszuwerten, können Sie auch eine der vielen GetXxx-Methoden anwenden, die für die wichtigsten .NET-Datentypen bereitgestellt werden, beispielsweise GetString, GetInt32 oder GetBoolean. Sie müssen nur die passende Methode aus einer langen Liste auswählen und beim Aufruf die Ordinalzahl der entsprechenden Spalte übergeben. Wenn Sie eine nicht typgerechte Methode auswählen, kommt es zur Ausnahme InvalidCastException. Dim dr As DbDataReader = cmd.ExecuteReader() While (dr.Read()) Console.WriteLine("{0,-35}{1}", dr.GetString(0), dr.GetDecimal(1)) End While
Auch wenn der Programmieraufwand größer ist – zur Laufzeit werden Sie dafür mit einem besseren Leistungsverhalten belohnt.
1129
24.2
24
Datenbankabfragen mit ADO.NET
NULL-Werte behandeln Spalten einer Tabelle können NULL-Werte enthalten, soweit sie für die jeweilige Spalte zugelassen sind. In der Tabelle Products betrifft das zum Beispiel die Spalte UnitPrice. Wenn Sie die Datenwerte über eine der typisierten Methoden abrufen und der Spaltenwert NULL ist, führt das zu einer Ausnahme. Daher sollten Sie mit der Methode IsDBNull des DbDataReaders prüfen, ob die entsprechende Spalte einen gültigen Wert oder NULL enthält. Dim dr As DbDataReader = cmd.ExecuteReader() While (dr.Read()) Console.WriteLine("{0,-35}{1}", _ If(dr.IsDBNull(0), "-", dr.GetString(0)), dr.GetDecimal(1)) End While
In diesem Codefragment wird demnach geprüft, ob die Spalte mit dem Index 0 den Inhalt Nothing hat. Wenn nicht, wird der Wert über GetString abgerufen.
24.2.3 DataReader schließen Jeder DbDataReader blockiert standardmäßig das DbConnection-Objekt. Solange DbDataReader durch den Aufruf von ExecuteReader geöffnet ist, können keine anderen Aktionen auf Basis der Verbindung durchgeführt werden, auch nicht das Öffnen eines zweiten DbDataReader-Objekts. Daher sollte die Sperre so schnell wie möglich mit dr.Close()
aufgehoben werden.
24.2.4 MARS (Multiple Active Resultsets) Mit SQL Server 2005 wurde ein Feature eingeführt, das es gestattet, mehrere Anforderungen auf einer Verbindung auszuführen. Damit wird eine Verbindung nicht mehr blockiert, wenn diese einem geöffneten SqlDataReader zugeordnet ist. Diese Neuerung in der aktuellen Version von SQL Server wird als Multiple Active Resultsets, kurz MARS, bezeichnet. MARS ist per Vorgabe deaktiviert und muss zuvor aktiviert werden, um es zu nutzen. Sie erreichen das entweder, indem Sie die Verbindungszeichenfolge um MultipleActiveResultSets=True;
ergänzen oder indem Sie die gleichnamige Eigenschaft im SqlConnectionStringBuilder setzen. MARS bietet sich an, wenn auf Basis der Ergebnismenge eines SqlDataReaders eine untergeordnete Tabellenabfrage gestartet werden soll. Das folgende Beispiel demonstriert dies. Dazu soll zu jedem Artikel auch der dazugehörige Lieferant ausgegeben werden. Damit stehen die beiden Tabellen Products und Suppliers im Mittelpunkt unserer Betrachtung. Für jede Tabelle werden ein SqlCommand-Objekt sowie ein SqlDataReader-Objekt benötigt. Das erste DataReader-Objekt durchläuft die Artikeltabelle. Mit der in der Spalte SupplierID enthaltenen ID des Lieferanten wird eine untergeordnete Ergebnisliste, nämlich die der Tabelle Suppliers,
1130
SqlDataReader
durchlaufen. Hier wird die ID des Lieferanten gesucht und dessen Firmenbezeichnung zusätzlich zum Artikel ausgegeben. '...\ADO\Datenbankabfragen\MARS.vb
Option Strict On Imports System.Data.Common, System.Data.SqlClient Namespace ADO Module MARS Sub Test() Dim con As DbConnection = New SqlConnection("Data Source=.;" & _ "Initial Catalog=Northwind;Integrated Security=sspi;" & _ "MultipleActiveResultSets=true") Dim cmdProducts As DbCommand = New SqlCommand( _ "SELECT ProductName, UnitsInStock, SupplierID FROM Products") Dim cmdSupplier As DbCommand = New SqlCommand( _ "SELECT CompanyName FROM Suppliers WHERE SupplierID=@SupplierID") cmdProducts.Connection = con : cmdSupplier.Connection = con Dim param As DbParameter = _ New SqlParameter("@SupplierID", SqlDbType.Int) cmdSupplier.Parameters.Add(param) con.Open() Dim drProducts As DbDataReader = cmdProducts.ExecuteReader() While (drProducts.Read()) Console.Write("{0,-35}{1,-6}", _ drProducts("ProductName"), drProducts("UnitsInStock")) param.Value = drProducts("SupplierID") Dim drSupplier As DbDataReader = cmdSupplier.ExecuteReader() While (drSupplier.Read()) Console.WriteLine(drSupplier("Companyname")) End While drSupplier.Close() End While drProducts.Close() con.Close() Console.ReadLine() End Sub End Module End Namespace
Der Vorteil von MARS wird in diesem Beispiel deutlich: Es genügt eine Verbindung, um mit den zwei DbDataReader-Objekten zu operieren. Selbstverständlich kann die dem Programmcode zugrunde liegende Aufgabe auch ohne die Nutzung von MARS erfüllt werden. Allerdings wären dazu zwei Verbindungen notwendig, die einen gewissen Overhead verursachen. Die SQL-Statements sind als parametrisierte Abfragen ausgebildet. DbCommand-Objekte unterstüt-
1131
24.2
24
Datenbankabfragen mit ADO.NET
zen diese durch eine spezielle Parameterauflistung. Weiter unten werden wir uns den parametrisierten Abfragen im Detail widmen.
24.2.5 Batch-Abfragen mit NextResult durchlaufen Bei einigen Datenbanken, zum Beispiel SQL Server, können Sie mehrere Abfragen hintereinander in einer Batch-Abfrage bündeln. Nehmen wir an, Sie benötigen alle Datensätze sowohl der Tabelle Orders als auch der Tabelle Customers. Um eine syntaktisch korrekte Batch-Abfrage zu generieren, formulieren Sie die beiden SELECT-Statements und trennen diese durch ein Semikolon: SELECT * FROM Orders;SELECT * FROM Customers
Der Vorteil einer Batch-Abfrage ist, dass Sie nicht mehrfach die Methode ExecuteReader aufrufen und zwischendurch den DataReader schließen müssen. Selbstverständlich können Sie in Batch-Abfragen mehr als zwei SELECT-Anweisungen formulieren. Das von einer Batch-Abfrage gefüllte DbDataReader-Objekt enthält nach dem Aufruf der ExecuteReader-Methode mehrere Ergebnislisten. Um zwischen diesen zu wechseln, verwendet
man die Methode NextResult. Sie arbeitet ähnlich wie Read und liefert True, wenn eine Datensatzliste durchlaufen wurde und sich noch eine weitere im DataReader befindet. Do While dr.Read() Console.WriteLine("{0} {1} {2}", dr(0), dr(1), dr(2)) End While Loop While dr.NextResult()
Die Überprüfung mit NextResult muss in jedem Fall im Schleifenfuß erfolgen, da sie zur nächsten Datensatzliste springt und dann ein Ergebnis liefert. Eine Prüfung im Schleifenkopf hätte zur Folge, dass die erste Datensatzliste überhaupt nicht durchlaufen wird.
Gemischte Batch-Abfragen Manchmal ist es erforderlich, eine Batch-Abfrage zu definieren, die sich aus einer oder mehreren Auswahl- und Aktionsabfragen zusammensetzt. Vielleicht möchten Sie eine SELECT-, eine DELETE- sowie eine UPDATE-Abfrage in einer Batch-Abfrage behandeln? Kein Problem. Erstellen Sie eine solche Abfrage genauso wie jede andere, also beispielsweise mit: SELECT * FROM Products; UPDATE Products SET ProductName='Senfsauce' WHERE ProductName='Chai'
Solche gemischten Abfragen rufen Sie ebenfalls mit der Methode ExecuteReader auf.
24.2.6 Informationen über eine Tabelle Meistens wird das DbDataReader-Objekt sicherlich für die Abfrage von Daten benutzt. Darüber hinaus ist es aber auch möglich, Metadaten abzufragen. Das sind Informationen über die einzelnen Spalten. Im Einzelnen handelt es sich dabei um folgende Fähigkeiten:
1132
SqlDataReader
왘
Abrufen der Schemadaten der Spalten mit der Methode GetSchemaTable. Die gelieferten Informationen beschreiben unter anderem, ob eine Spalte Primärschlüsselspalte ist, ob sie schreibgeschützt ist, ob der Spaltenwert innerhalb der Tabelle eindeutig ist oder ob die Spalte einen NULL-Wert zulässt.
왘
Der Name einer bestimmten Spalte lässt sich mit der Methode GetName ermitteln.
왘
Die Ordinalposition einer Spalte lässt sich anhand des Spaltenbezeichners ermitteln. Die Methode GetOrdinal liefert den entsprechenden Index.
GetSchemaTable Der Rückgabetyp der Methode GetSchemaTable ist ein Objekt vom Typ DataTable. An dieser Stelle wollen wir diesen Typ nicht weiter betrachten, denn das folgt später noch detailliert. Es genügt am Anfang zu wissen, dass sich ein DataTable-Objekt aus Datenzeilen und Spalten zusammensetzt, ähnlich einer Excel-Tabelle. Dieser Tabelle liegt eine SELECT-Abfrage zugrunde, die mit ExecuteReader gegen die Datenbank ausgeführt wird. Dazu wird ExecuteReader ein Parameter vom Typ der Enumeration CommandBehavior übergeben. Der Wert CommandBehavior.SchemaOnly sorgt dafür, dass die Abfrage nur Spalteninformationen zurückliefert. Dim dr As DbDataReader = cmd.ExecuteReader(CommandBehavior.SchemaOnly)
Auf der DbDataReader-Referenz rufen wir anschließend die Methode GetSchemaTable auf. Das ist vorteilhaft, denn die übermittelten Metadaten werden nun für alle Spalten, die im SELECT-Statement angegeben sind, in der Tabelle eingetragen. Dabei wird für jede im SELECT-Statement angegebene Spalte der Originaltabelle eine Datenzeile geschrieben. Dim table As DataTable = dr.GetSchemaTable()
Die Spalten in der Schematabelle werden durch festgelegte Bezeichner in einer bestimmten Reihenfolge ausgegeben. Die erste Spalte ist immer ColumnName, die zweite ColumnOrdinal, die dritte ColumnSize. Insgesamt werden 28 Spalten zur Auswertung bereitgestellt. Falls Sie nähere Informationen benötigen, sehen Sie sich in der .NET-Dokumentation die Hilfe zur Methode GetSchemaTable an. Das folgende Beispiel untersucht die Spalten ProductID, ProductName und UnitsInStock der Tabelle Products. Es soll dabei genügen, nur die ersten vier Metainformationen zu ermitteln. '...\ADO\Datenbankabfragen\GetSchemaTable.vb
Option Strict On Imports System.Data.Common, System.Data.SqlClient Namespace ADO Module GetSchemaTable Sub Test() Dim con As DbConnection = New SqlConnection("Data Source=.;" & _ "Initial Catalog=Northwind;Integrated Security=sspi") Dim cmd As DbCommand = New SqlCommand( _ "SELECT ProductID, ProductName, UnitsInStock FROM Products")
1133
24.2
24
Datenbankabfragen mit ADO.NET
cmd.Connection = con con.Open() Dim dr As DbDataReader = _ cmd.ExecuteReader(CommandBehavior.SchemaOnly) Dim table As DataTable = dr.GetSchemaTable() For i As Integer = –1 To table.Rows.Count – 1 For j As Integer = 0 To 3 Console.Write("{0,-15}", _ If(i < 0, table.Columns(j).ColumnName, table.Rows(i)(j))) Next j Console.WriteLine() Next i dr.Close() con.Close() Console.ReadLine() End Sub End Module End Namespace
Die Ausgabe zeigt die ermittelten Metainformationen: ColumnName ProductID ProductName UnitsInStock
ColumnOrdinal 0 1 2
ColumnSize 4 40 2
NumericPrecision 10 255 5
Bezeichner einer Spalte Möchten Sie den Bezeichner einer bestimmten Spalte in der Ergebnisliste ermitteln, rufen Sie die Methode GetName des DbDataReader-Objekts auf und übergeben dabei den Index der betreffenden Spalte in der Ergebnisliste. Der Rückgabewert ist eine Zeichenfolge. Console.WriteLine(dr.GetName(3))
Index einer Spalte Ist der Index einer namentlich bekannten Spalte in der Ergebnisliste nicht bekannt, können Sie diesen mit GetOrdinal unter Angabe des Spaltenbezeichners ermitteln. Console.WriteLine(dr.GetOrdinal("UnitPrice"))
Datentyp einer Spalte Sie können sowohl den .NET-Datentyp als auch den Datenbank-Datentyp eines bestimmten Feldes im DbDataReader abfragen. Wenn Sie sich für den .NET-Datentyp interessieren, rufen Sie die Methode GetFieldType des DataReaders auf, ansonsten GetDataTypeName. Console.WriteLine(dr.GetFieldType(4)) Console.WriteLine(dr.GetDataTypeName(0))
Beide Methoden erwarten den Ordinalwert der betreffenden Spalte.
1134
Parametrisierte Abfragen
24.3
Parametrisierte Abfragen
Die Suche nach einem bestimmten Datensatz einer Tabelle wird durch die WHERE-Klausel einer SELECT-Abfrage bestimmt: SELECT ProductName FROM Products WHERE ProductName='Tunnbröd'
Die Hartcodierung dieser Abfrage ist nicht praxisgerecht. Was ist, wenn der Benutzer nicht nach dem Artikel Tunnbröd suchen möchte, sondern die Informationen über den Artikel Tofu benötigt? Die Abfrage sollte allgemeiner formuliert werden, und zwar so, dass der Anwender zur Laufzeit des Programms den Artikel beliebig bestimmen kann. Die Lösung ist eine parametrisierte Abfrage. Berücksichtigen Sie bei den folgenden Ausführungen jedoch, dass die Wahl des .NET-Datenproviders maßgeblich die Syntax des SELECTStatements und des Programmcodes einer parametrisierten Abfrage beeinflusst.
24.3.1 SqlClient-Datenprovider Für den SqlClient-Datenprovider könnte das Statement wie folgt lauten: SELECT * FROM Products WHERE ProductName = @Productname OR CategoryID = @CatID
@ProductName und @CatID sind benannte Parameter, denen das »@«-Zeichen vorangestellt wird. Dieses gilt jedoch nur für den SqlClient-Datenprovider. Die Datenprovider OleDb und Odbc unterstützen benannte Parameter nicht, sondern nur den generischen Parametermarker, das Fragezeichen »?«. Der Grund für diese Abweichung der Datenprovider ist sehr einfach. Während der OleDb- bzw. Odbc-Datenprovider eine datenbankunabhängige Syntax erlaubt, ist der SqlClient-Provider nur für den SQL Server gedacht, der benannte Parameter mit diesem Präfix unterstützt. Die Parameter einer parametrisierten Abfrage werden vom Command-Objekt gesammelt. Dieses besitzt dazu eine Parameters-Auflistung, der die einzelnen Parameter hinzugefügt werden. Wenn Sie den SqlClient-Datenprovider verwenden, handelt es sich um den Typ SqlParameter. Sie können einen Parameter hinzufügen, indem Sie entweder die Add-Methode der Auflistung oder die Methode AddWithValue von SqlCommand aufrufen. Das Beispiel Parametrisiert verwendet zum Hinzufügen die Methode Add. Gesucht wird dabei nach allen Artikeln, die der Kategorie 1 zugeordnet sind, und zusätzlich nach dem Artikel mit der Bezeichnung Konbu. Die beiden Parameter werden mit statischen Werten gefüllt. In der Praxis würden Sie die Werte beispielsweise dem Eingabestrom oder einer Textbox entnehmen. '...\ADO\Datenbankabfragen\Parametrisiert.vb
Option Strict On Imports System.Data.Common, System.Data.SqlClient Namespace ADO Module Parametrisiert Sub Sql()
1135
24.3
24
Datenbankabfragen mit ADO.NET
Dim con As DbConnection = New SqlConnection() con.ConnectionString = "Data Source=(local);" & _ "Initial Catalog=Northwind;Integrated Security=sspi" Dim cmd As DbCommand = New SqlCommand() cmd.CommandText = "SELECT * FROM Products " & _ "WHERE ProductName = @Productname OR CategoryID = @CatID" cmd.Connection = con cmd.Parameters.Add(New SqlParameter("@Productname", "Konbu")) cmd.Parameters.Add(New SqlParameter("@CatID", "1")) con.Open() Dim dr As DbDataReader = cmd.ExecuteReader() While (dr.Read()) Console.WriteLine("{0,-5}{1,-5}{2}", _ dr("CategoryID"), dr("ProductID"), dr("ProductName")) End While dr.Close() con.Close() Console.ReadLine() End Sub ... End Module End Namespace
Wenn Sie den SqlClient-Provider benutzen, ist die Reihenfolge der Parameter innerhalb der Parameters-Auflistung des Command-Objekts unbedeutend, da sie anhand ihres Bezeichners eindeutig identifiziert werden können. Bei beiden Parametern handelt es sich in diesem Beispiel um Zeichenfolgen, die auch als solche an die Datenbank weitergeleitet werden. Sie können Daten beliebigen Typs angeben, denn die jeweils zweiten Argumente von SqlParameter und AddWithValue sind vom Typ Object. Diese Flexibilität hat jedoch einen Preis: Wenn Sie bei der Wertübergabe einen ungeeigneten Datentyp verwenden, behandelt die Datenbank die im Parameter gespeicherte Information nicht so, wie Sie es erwarten. Unter Umständen gibt der SQL Server sogar eine Ausnahme vom Typ SqlException zurück, weil der übermittelte Parameter mit der Typdefinition der entsprechenden Spalte nicht übereinstimmt. Sie können das sehr leicht selbst testen, indem Sie im Code des Beispiels anstelle des Artikelbezeichners Konbu eine Integerzahl eintragen. Der Datenbank diese Verantwortung zu übertragen, ist keine gute Lösung. Der richtige Datentyp sollte bereits im Code des Clients sichergestellt sein. Dazu bieten sich andere Überladungen von SqlCommand.Add und SqlParameter an, die über den Parameterbezeichner hinaus auch den an die Datenbank übergebenen Datentyp steuern. Zudem gibt es noch die Möglichkeit, den Datentyp genauer zu spezifizieren. Beispielsweise können Zeichenfolgen eine unterschiedliche Länge aufweisen. Die Länge kann als drittes Übergabeargument bekannt gegeben werden. Das Kommando in unserem Beispiel oben könnte wie folgt geändert werden:
1136
Parametrisierte Abfragen
Dim SqlCmd As SqlCommand = New SqlCommand(...) SqlCmd.Parameters.Add("@Productname", SqlDbType.VarChar, 40).Value = "Konbu" SqlCmd.Parameters.Add("@CatID", SqlDbType.Int).Value = 1
Alternativ kann der Konstruktor von SqlParameter verwendet werden: Dim SqlPar As New SqlParameter("@Productname", SqlDbType.VarChar, 40) SqlPar.Value = "Konbu" SqlCmd.Parameters.Add(SqlPar)
Übergeben Sie einem der beiden Parameter einen Integer, wird keine Ausnahme ausgelöst. Das Ergebnis erscheint im ersten Moment ernüchternd, und es scheint der vorher gemachten Aussage zu widersprechen, dass die Methode Add eine Typüberprüfung gewährleistet. Die Ursache ist allerdings einfach zu erklären: Die Integer-Zahl wird implizit als Zeichenfolge im Parameter eingetragen. Anders sieht es jedoch aus, wenn ein Parameter als Integer festgelegt wird und Sie versuchen, diesem eine Zeichenfolge zuzuweisen: SqlCmd.Parameters.Add("@Param", SqlDbType.Int).Value = "White"
Beim Aufruf von ExecuteReader wird die Ausnahme FormatException ausgelöst. Diese stammt nicht vom SQL Server, sondern wird von ADO.NET in der Clientanwendung ausgelöst. Damit haben wir ein Ziel erreicht: die Entlastung der Datenbank. Der Datentyp, den Sie der Add-Methode übergeben, stammt aus der Enumeration SqlDbType, die die Datentypen umfasst, die SQL Server standardmäßig bereitstellt.
24.3.2 SqlParameter Solange nicht ausdrücklich Parameter hinzugefügt werden, ist die Parameters-Auflistung des Command-Objekts leer. Die Referenz auf die Auflistung erhalten Sie über die Eigenschaft Parameters. Ein Parameter wird durch Aufruf der Methode Add oder AddWithValue hinzugefügt. Alle anderen Methoden der Auflistung gleichen denen aller anderen üblichen Auflistungen von .NET: Mit Count ruft man die Anzahl der Parameter ab, mit Remove wird ein Parameter gelöscht usw. SqlParameterCollection überlädt die Methode Add vielfach, AddWithValue aber nicht.
Jedoch liefern beide als Rückgabewert die Referenz auf das hinzugefügte SqlParameterObjekt (Ausnahme: die einparametrige von DbParameterCollection übernommene Variante, die die Einfügeposition zurückgibt). Die beiden letzten Parameter von Add sind optional. Public Function Add(parameterName As String, sqlDbType As SqlDbType, size As Integer, sourceColumn As String) As SqlParameter
Meistens können Sie den Rückgabewert ignorieren. Er ist dann interessant, wenn man die Eigenschaften des Parameters auswerten oder vor dem Absetzen des SQL-Kommandos ändern möchte. Zum Füllen des Parameters wird der Eigenschaft Value des DbParameter-Objekts der gewünschte Wert zugewiesen: cmd.Parameters("@ParameterName").Value = "Chai"
1137
24.3
24
Datenbankabfragen mit ADO.NET
Sie rufen den Indexer der DbParameterCollection auf und übergeben den Bezeichner des Parameters. Alternativ können Sie auch den Index des entsprechenden Parameter-Objekts in der Auflistung verwenden.
24.3.3 OleDb-Datenprovider An dieser Stelle möchte ich Ihnen auch noch zeigen, wie eine parametrisierte Abfrage auf Basis des OleDb-Datenproviders formuliert wird. '...\ADO\Datenbankabfragen\Parametrisiert.vb
Option Strict On Imports System.Data.Common, System.Data.SqlClient Namespace ADO Module Parametrisiert ... Sub OleDb() Dim con As DbConnection = New OleDbConnection( _ "Data Source=(local);Provider=SQLNCLI;" & _ "Initial Catalog=Northwind;Integrated Security=sspi") Dim cmd As DbCommand = New OleDbCommand() cmd.CommandText = "SELECT * FROM Products " & _ "WHERE ProductName = ? OR CategoryID = ? " cmd.Connection = con ' oder OleDbCmd.Parameters.Add("Art", OleDbType.VarChar, 40) cmd.Parameters.Add(New OleDbParameter("Art", OleDbType.VarChar, 40)) cmd.Parameters.Add(New OleDbParameter("ID", OleDbType.Integer)) cmd.Parameters("Art").Value = "Konbu" cmd.Parameters("ID").Value = 1 con.Open() Dim dr As DbDataReader = cmd.ExecuteReader() While (dr.Read()) Console.WriteLine("{0,-5}{1,-5}{2}", _ dr("CategoryID"), dr("ProductID"), dr("ProductName")) End While dr.Close() con.Close() Console.ReadLine() End Sub End Module End Namespace
Zunächst muss die Verbindungszeichenfolge um Provider=SQLNCLI
ergänzt werden, um die Vorschriften des OleDb-Datenproviders zu erfüllen. Die Parameter der Abfrage werden nur durch »?« beschrieben und sind daher nur über ihre Reihenfolge
1138
Asynchrone Abfragen
identifizierbar, sodass beim Hinzufügen der Parameter zur Parameter-Auflistung des DbCommand-Objekts die Reihenfolge unbedingt beachtet werden muss. Beim Hinzufügen erhalten die Parameter Bezeichner und können dann entweder über den Index oder den Bezeichner angesprochen werden.
24.4 Asynchrone Abfragen Die Methoden ExecuteReader, ExecuteNonQuery oder ExecuteXmlReader blockieren die Anwendung, sodass der Programmfluss erst nach der Antwort des SQL Servers weitergeht. Dauert die Operation eine längere Zeit, wirkt die Clientanwendung wie eingefroren. Daher stellt ADO.NET außerdem asynchrone Methoden bereit, jedoch nur im SqlClient-Provider für SQL Server ab Version 7.0. Andere Provider kennen nur synchrone Abfragen. Jede der drei Execute-Methoden erhält je eine Begin- und eine End-Methode. Für ExecuteReader sind es z. B. BeginExecuteReader und EndExecuteReader. In der folgenden Syntax sind optionale Parameter in eckige Klammern gesetzt: Public Function BeginExecuteReader([beh As CommandBehavior]) As IAsyncResult Public Function BeginExecuteReader(call As AsyncCallback, state As Object [, beh As CommandBehavior]) As IAsyncResult Public Function EndExecuteReader(result As IAsyncResult) As SqlDataReader
Mit BeginExecuteReader wird die asynchrone Operation gestartet. Der aufrufende Code initiiert die Aktion und fährt sogleich mit dem Programmfluss fort, ohne auf den Abschluss der Abfrage zu warten. Woher weiß das Clientprogramm, wann wie die Daten abgerufen werden können? Dazu gibt es zwei Möglichkeiten an, die ich in Beispielen vorstellen werde: 왘
Sie fragen in einer Schleife permanent ab, ob die asynchrone Operation bereits beendet ist. Dieses Verfahren wird als Polling bezeichnet.
왘
Sie definieren eine Rückrufmethode (Callback-Methode), die aufgerufen wird, sobald das Ergebnis vorliegt.
Asynchrone Operationen sind per Vorgabe nicht aktiviert. Damit das SqlConnection-Objekt auch asynchrone Abfragen ermöglicht, muss die Verbindungszeichenfolge um Asynchronous Processing=true
ergänzt werden. Alternativ weisen Sie der Eigenschaft AsynchronousProcessing eines SqlConnectionStringBuilder-Objekts den Wert True zu. Unsere bisherigen einfachen Abfragen sind zu schnell, um die Effekte asynchroner Operationen wahrzunehmen. Daher simulieren wir eine längere Abfrage mit der Anweisung WAITFOR DELAY, die den SQL Server um eine gewisse Zeit verzögert, zum Beispiel um zwei Sekunden: WAITFOR DELAY '00:00:02'
1139
24.4
24
Datenbankabfragen mit ADO.NET
24.4.1 Polling Sehen wir uns zuerst den gesamten Beispielcode an: '...\ADO\Datenbankabfragen\Polling.vb
Option Strict On Imports System.Data.Common, System.Data.SqlClient Namespace ADO Module Polling Sub Test() Dim con As SqlConnection = New SqlConnection("Data Source=.;" & _ "Initial Catalog=Northwind;Integrated Security=sspi;" & _ "Asynchronous Processing=true") Dim cmd As SqlCommand = New SqlCommand( _ "WAITFOR DELAY '00:00:01';SELECT TOP 10 * FROM Products", con) con.Open() Dim result As IAsyncResult = cmd.BeginExecuteReader() ' asynchron While (Not result.IsCompleted) ' warten Arbeiten() End While Console.WriteLine(Environment.NewLine & "Und nun das Ergebnis:") Dim dr As SqlDataReader = cmd.EndExecuteReader(result) While dr.Read() Console.WriteLine(dr("ProductName")) End While dr.Close() : con.Close() : Console.ReadLine() End Sub Sub Arbeiten() Console.Write(DateTime.Now.Millisecond & " ") Threading.Thread.Sleep(50) End Sub End Module End Namespace
Beim Polling prüft der Client, ob die durch die Begin-Methode angestoßene Operation abgeschlossen ist. Die Kommunikation mit dem Thread, in dem die Begin-Methode läuft, findet über die Statuseigenschaft IsCompleted statt. Sie wird von dem DbAsynchResult-Objekt bereitgestellt, das die Begin-Methode als Ergebnis zurückliefert und das die Schnittstelle IAsyncResult implementiert. Der Status kann nur ausgelesen werden und ist während der Ausführung der Operation False. Beachten Sie, dass in diesem Beispiel die parameterlose Methode BeginExecuteReader eingesetzt wird. Dies hat zur Folge, dass die Operation sangund klanglos endet, ohne uns automatisch, außer über den Status, über ihr Ende zu informieren.
1140
Asynchrone Abfragen
Solange die Ergebnismenge noch nicht vorliegt, hat IsCompleted den Wert False und die Clientanwendung erledigt in Arbeiten eine andere Aufgabe und gibt einige Zahlen aus (Sleep simuliert eine ausgedehntere Aktivität). Ist die Anfrage an den Datenbankserver beendet, kann das Ergebnis geholt werden. Dazu dient die Methode EndExecuteReader, die ihrerseits die Referenz auf ein SqlDataReader-Objekt bereitstellt, das wir zur Ausgabe der Spalte ProductName benutzen.
24.4.2 Bereitstellen einer Rückrufmethode Während beim Polling fortwährend der eigene Code prüft, ob der Datenbankserver die Anfrage bearbeitet hat, obliegt es durch das Bereitstellen einer Rückrufmethode der BeginMethode, die Rückrufmethode nach getaner Arbeit aufzurufen. Die Adresse der Rückrufmethode, die im folgenden Beispiel Rückruf heißt, wird dem ersten Parameter der überladenen Methode BeginExecuteReader übergeben. Die Rückrufmethode ist vom Typ des Delegates AsyncCallback, das einen Parameter vom Typ IAsyncResult hat und keinen Wert zurückgibt. Im zweiten Parameter erhält BeginExecuteReader ein beliebiges Objekt, auf das in der Rückrufmethode über die Eigenschaft AsyncState des Methodenparameters zugegriffen werden kann. In unserem Beispiel speichern wir das Kommando zur Erzeugung eines DataReaders und die Verbindung zum Aufräumen mit Close nach getaner Arbeit. Wie Sie die Informationen organisieren, bleibt Ihnen überlassen. Das Beispiel nutzt ein Array von Objekten. Nach Beendigung der asynchronen Operation wird die Rückrufmethode ausgeführt, aus der heraus EndExecuteReader aufgerufen wird. Das dazu notwendige SqlCommand-Objekt steckt in dem Objekt, das dem zweiten Parameter der Methode BeginExecuteReader übergeben wurde, und ist in der Eigenschaft AsyncState des IAsyncResult-Parameters gespeichert. Da AsyncState vom Typ Object ist, müssen noch einige Konvertierungen mit CType erfolgen. '...\ADO\Datenbankabfragen\Callback.vb
Option Strict On Imports System.Data.Common, System.Data.SqlClient Namespace ADO Module Callback Sub Test() Dim con As SqlConnection = New SqlConnection("Data Source=.;" & _ "Initial Catalog=Northwind;Integrated Security=sspi;" & _ "Asynchronous Processing=true") Dim cmd As SqlCommand = New SqlCommand( _ "WAITFOR DELAY '00:00:01';SELECT TOP 3 * FROM Products", con) con.Open() Dim callback As New AsyncCallback(AddressOf Rückruf) Dim info() As Object = {cmd, con} cmd.BeginExecuteReader(callback, info) ' asynchron For i As Integer = 0 To 25 : Arbeiten() : Next
1141
24.4
24
Datenbankabfragen mit ADO.NET
Console.ReadLine() End Sub Sub Rückruf(ByVal result As IAsyncResult) Dim info() As Object = CType(result.AsyncState, Object()) Dim cmd As SqlCommand = CType(info(0), SqlCommand) Dim con As SqlConnection = CType(info(1), SqlConnection) Dim dr As SqlDataReader = cmd.EndExecuteReader(result) Console.WriteLine(Environment.NewLine & "Und nun das Ergebnis:") While dr.Read() : Console.WriteLine(dr("ProductName")) : End While dr.Close() : con.Close() End Sub Sub Arbeiten() Console.Write(DateTime.Now.Millisecond & " ") Threading.Thread.Sleep(50) End Sub End Module End Namespace
Die Ausgabe zeigt, wie die Hauptroutine durch die Rückrufmethode unterbrochen wird. Ohne dass der Hauptthread mit Sleep für den asynchronen Thread Platz macht, kann es sein, dass alle Arbeiten-Aufrufe vor der Ergebnisausgabe erfolgen. 144 204 Und nun Tee Chang Aniseed 156 206
254 304 354 405 455 505 555 605 655 705 755 805 855 905 955 5 55 106 das Ergebnis:
Syrup 256 306 356 406
24.5
Gespeicherte Prozeduren (Stored Procedures)
Eine gespeicherte Prozedur (Stored Procedure, SP) ist eine Gruppe von SQL-Anweisungen, die kompiliert auf dem Datenbankserver als Teil einer Datenbank gespeichert sind. Durch die Kompilierung und den geringeren Netzwerkverkehr (die Prozedur läuft auf dem Server) ist sie oft schneller als explizite SQL-Anweisungen. Ihr Aufbau ist einfach: CREATE PROCEDURE Produktsuche ( @Preis money, @Menge smallint ) AS SELECT * FROM Products WHERE UnitPrice < @Preis AND UnitsOnOrder = @Menge
Hinweis Anders als bei Visual Basic ist eine logische Zeile nicht durch einen Zeilenumbruch festgelegt.
1142
Gespeicherte Prozeduren (Stored Procedures)
Diese gespeicherte Prozedur beschreibt eine Auswahlabfrage, die alle Artikel der Tabelle Products liefert, die eine bestimmte Preisgrenze unterschreiten und eine bestimmte Anzahl von Bestelleinheiten haben. Es ist einerlei, ob Sie den Teil hinter der As-Klausel als SqlClientDatenprovider-Befehl schicken oder die gespeicherte Prozedur aufrufen. Gespeicherte Prozeduren bieten sich an, wenn Kommandos sehr häufig ausgeführt werden sollen. Sie sind nicht nur leistungsfähiger als normale SQL-Kommandos, sondern bieten auch darüber hinaus weitergehende Möglichkeiten: Gespeicherte Prozeduren können Berechnungen ausführen, Ein- und Ausgabeparameter entgegennehmen (ähnlich wie Wert- und Referenzparameter) oder ein Resultat an den Aufrufer liefern. Es gibt ausführliche Literatur zu gespeicherten Prozeduren. Ich werde Ihnen später an einem komplexeren Beispiel zeigen, wie gespeicherte Prozeduren mit ADO.NET-Code behandelt werden. Hier soll die Syntax (fast) kommentarlos angegeben werden. Optionale Teile sind in eckige Klammern gesetzt, und Alternativen werden durch | getrennt. Kursiv gesetzte Bestandteile können Sie Ihren Bedürfnissen entsprechend anpassen. Nicht immer sind alle Datentypen einer Datenbank als Rückgabetyp einer Funktion erlaubt, und Änderungen an Tabellen sowie einige Funktionsaufrufe in der Funktion können verboten sein. CREATE|ALTER PROCEDURE name [@Parameter Typ [=NULL | Konstante] [OUTPUT]] [WITH [RECOMPILE] [ENCRYPTION]] AS Anweisung(en) [RETURN GanzeZahl] CREATE|ALTER FUNCTION Name (@Parameter Typ [=Standardwert], ...) RETURNS Typ AS BEGIN Anweisung(en) RETURN END | RETURNS TABLE AS RETURN (select) | RETURNS @Tabelle TABLE(Spalte(n)) AS BEGIN Anweisung(en) RETURN END
24.5.1 Gespeicherte Prozeduren in Visual Studio 2008 erstellen Ein herkömmlicher SQL-Befehl wird vom Client gegen die Datenbank abgesetzt. Gespeicherte Prozeduren sind, sofern die Datenbank diese unterstützt, Elemente der Datenbank selbst, so wie beispielsweise Tabellen oder Sichten. Wenn Sie wollen, können Sie sehr einfach aus Visual Studio heraus gespeicherte Prozeduren zu einer Datenbank hinzufügen. Öffnen Sie dazu den Server-Explorer in Visual Studio. In diesem finden Sie den Knoten Datenverbindungen. Im Kontextmenü dieses Knotens wählen Sie Verbindung hinzufügen. Der Dialog ist in Abbildung 24.1 zu sehen. Tragen Sie im oberen Kombinationslistenfeld den Namen des Servers ein, auf dem die SQL Server-Datenbank installiert ist, zu der Sie Verbindung aufnehmen wollen. Achten Sie darauf, dass im Feld Datenquelle der Microsoft SQL Server eingetragen ist. Wenn nicht, können Sie die Einstellung ändern bzw. an den gewünschten Datenbankserver anpassen. Haben Sie die Installationsvorgabe Microsoft SQL Server übernommen, ist die WindowsAuthentifizierung eingestellt und Sie müssen, soweit Sie mit entsprechenden administrativen Rechten ausgestattet sind, keine Änderungen an den Anmeldeinformationen vornehmen.
1143
24.5
24
Datenbankabfragen mit ADO.NET
Anschließend wählen Sie die gewünschte Datenbank aus und können die eingestellten Verbindungsdaten testen.
Abbildung 24.1
Dialog zum Hinzufügen einer Datenbankverbindung
Im Server-Explorer wird die neue Verbindung zur Datenbank eingetragen. Unter den datenbankspezifischen Knoten finden Sie nun auch Gespeicherte Prozeduren (siehe Abbildung 24.2). Klicken Sie dann im Kontextmenü des Knotens auf Neue gespeicherte Prozedur hinzufügen. Im Codeeditor wird daraufhin ein weiteres Fenster mit einer gespeicherten Prozedur geöffnet (siehe Abbildung 24.3). Jede gespeicherte Prozedur wird mit CREATE PROCEDURE eingeleitet. Dem schließt sich der Bezeichner an. Einige Teile der Struktur sind mit /*...*/ auskommentiert. Dazu gehört auch der Block, in dem alle Übergabeparameter angegeben werden. Hinter AS folgen die SQL-Anweisungen. Eine gespeicherte Prozedur wird mit einem optionalen RETURN abgeschlossen, das den Rückgabewert der gespeicherten Prozedur angibt. Der Name der Prozedur sollte möglichst selbsterklärend sein, z. B. Produktsuche. Parameter werden zwischen CREATE PROCDURE und AS definiert. Dabei wird zuerst der Parametername angegeben, der das Präfix »@« haben muss. Dahinter folgt der Datentyp. Mehrere Parameter einer gespeicherten Prozedur werden durch Kommata getrennt.
1144
Gespeicherte Prozeduren (Stored Procedures)
Abbildung 24.2
Gespeicherten Prozeduren der Datenbank Northwind
Abbildung 24.3
Codeeditor für gespeicherte Prozeduren
Standardmäßig sind alle Parameter Eingabeparameter, die von der gespeicherten Prozedur zur Ausführung benötigt werden, selbst aber kein Resultat zurückliefern. Gespeicherte Prozeduren kennen aber auch Ausgabeparameter, die mit Referenzparametern vergleichbar sind. Diese liefern dem Aufrufer ein Ergebnis und werden mit OUTPUT gekennzeichnet. Nachdem Sie den SQL-Code im Codeeditor eingetragen haben, speichern Sie die Prozedur. Sie wird allerdings nicht im Projekt abgelegt, sondern in der Datenbank, wie Sie im Server-Explorer sehen. Beim Speichern wird die Syntax überprüft, und Fehler werden mit einer Meldung quittiert. Damit hört die Unterstützung durch Visual Studio aber nicht auf. Sie können Ihre neue gespeicherte Prozedur auch in der Entwicklungsumgebung testen. Dazu sollte sich der Mauszeiger über dem Codefenster der gespeicherten Prozedur befinden. Öffnen Sie dann das Kontextmenü, und wählen Sie Ausführen. Es öffnet sich der Dialog aus Abbildung 24.4. In ihm weisen Sie in der Spalte Wert den Parametern die gewünschten Daten zu. Nach einem Klick auf OK sehen Sie im Fenster Ausgabe das Ergebnis des Aufrufs (siehe Abbildung 24.5).
1145
24.5
24
Datenbankabfragen mit ADO.NET
Abbildung 24.4 Parameterdialog für gespeicherte Prozeduren
Abbildung 24.5 Ergebnis des Aufrufs der gespeicherten Prozedur »Produktsuche«
Hinweis Gespeicherte Prozeduren können auch mit ExecuteNonQuery im Code definiert oder geändert werden (bei ausreichenden Berechtigungen). Achten Sie bei der Stringverkettung darauf, Zwischenräume (Zeilenvorschübe oder Leerzeichen) an den syntaktisch relevanten Stellen einzufügen (Fehlermeldungen sind nicht immer offensichtlich).
24.5.2 Gespeicherte Prozeduren aufrufen Die soeben entwickelte gespeicherte Prozedur Produktsuche soll nun aufgerufen werden. Prinzipiell ähnelt der Weg dem, den wir beim Aufruf einer parametrisierten Abfrage beschrit-
1146
Gespeicherte Prozeduren (Stored Procedures)
ten haben. Es gibt aber einen ganz wichtigen Unterschied: Wir müssen dem SqlCommandObjekt über die Eigenschaft CommandType ausdrücklich mitteilen, dass es kein SQL-Kommando, sondern eine gespeicherte Prozedur ausführen soll. cmd.CommandType = CommandType.StoredProcedure
Die Eigenschaft ist vom Typ der gleichnamigen Enumeration (siehe Tabelle 24.2), die angibt, wie das unter der Eigenschaft CommandText angegebene Kommando zu interpretieren ist. Konstante
CommandText-Interpretation
StoredProcedure
Enthält den Namen einer gespeicherten Prozedur.
TableDirect
Enthält den Namen einer Tabelle.
Text
Enthält ein SQL-Kommando (Standard).
Tabelle 24.2
Konstanten der Enumeration »CommandType«
Mit der Einstellung CommandType.TableDirect repräsentiert die Eigenschaft CommandText einen Tabellennamen, das Äquivalent zum SQL-Befehl SELECT * FROM . Bisher haben wir die Eigenschaft CommandType nicht benutzt, weil wir immer ein SQLKommando abgesetzt haben, das durch die Standardeinstellung Text beschrieben wird. Da wir nun eine gespeicherte Prozedur ausführen wollen, müssen wir CommandType den Wert CommandType.StoredProcedure zuweisen. Das SqlCommand-Objekt benutzt diese Information, um die Syntax für den Aufruf der gespeicherten Prozedur zu generieren. '...\ADO\Datenbankabfragen\Prozedur.vb
Option Strict On Imports System.Data.Common, System.Data.SqlClient Namespace ADO Module Prozedur Sub Test() Dim con As DbConnection = New SqlConnection() con.ConnectionString = "Data Source=(local);" & _ "Initial Catalog=Northwind;Integrated Security=sspi" ' SqlCommand vorbereiten Dim cmd As DbCommand = New SqlCommand() cmd.CommandType = CommandType.StoredProcedure cmd.CommandText = "Produktsuche" cmd.Connection = con ' Parameter-Auflistung füllen cmd.Parameters.Add(New SqlParameter("@Preis", SqlDbType.Money)) cmd.Parameters.Add(New SqlParameter("@Menge", SqlDbType.SmallInt)) cmd.Parameters("@Preis").Value = 10 cmd.Parameters("@Menge").Value = 0 ' SqlCommand ausführen con.Open()
1147
24.5
24
Datenbankabfragen mit ADO.NET
Dim dr As DbDataReader = cmd.ExecuteReader() While (dr.Read()) Console.WriteLine("{0,-35}{1}", dr("ProductName"), dr("UnitPrice")) End While dr.Close() : con.Close() Console.ReadLine() End Sub End Module End Namespace
Hinweis Parameter mit Standardwerten sind nur am Ende der Parameterliste optional.
Die SQL-Variante, cmd.CommandText = "EXECUTE Produktsuche 10, 0" und cmd.ExecuteReader(), ruft dieselbe Prozedur auf. Die allgemeine Syntax lautet: EXECUTE [@Rückgabe =] name [Wert | Variable OUTPUT] EXECUTE [@Rückgabe =] name [@Parameter = Wert | Variable OUTPUT]
24.5.3 Komplexe gespeicherte Prozeduren Eine gespeicherte Prozedur ist nicht immer so einfach aufgebaut wie Produktsuche. Eine gespeicherte Prozedur kann sowohl über die Parameterliste als auch über RETURN Werte an den Aufrufer zurückliefern. Dazu ein Beispiel: CREATE PROCEDURE Produkt ( @id int, @Was varchar(40) OUTPUT, @Preis money OUTPUT ) AS SELECT @Was=ProductName, @Preis=UnitPrice FROM Products WHERE ProductID=@id RETURN @@ROWCOUNT
Die gespeicherte Prozedur definiert neben dem Eingabeparameter @id mit @zuname und @vorname auch zwei Ausgabeparameter, denen beim Aufruf zwar kein Wert übergeben wird, die aber ein Resultat zurückliefern. Der Rückgabewert @@ROWCOUNT ist eine Systemfunktion von SQL Server, die die Anzahl der Zeilen angibt, auf die sich die letzte Anweisung ausgewirkt hat. '...\ADO\Datenbankabfragen\ProzedurMitReturn.vb
Option Strict On Imports System.Data.Common, System.Data.SqlClient Namespace ADO
1148
Gespeicherte Prozeduren (Stored Procedures)
Module ProzedurMitReturn Sub Test() Dim con As DbConnection = New SqlConnection() con.ConnectionString = "Data Source=(local);" & _ "Initial Catalog=Northwind;Integrated Security=sspi" ' DbCommand vorbereiten Dim cmd As DbCommand = New SqlCommand() cmd.CommandType = CommandType.StoredProcedure cmd.CommandText = "Produkt" cmd.Connection = con ' Parameters-Auflistung füllen cmd.Parameters.Add(New SqlParameter("@RetValue", SqlDbType.Int)) cmd.Parameters.Add(New SqlParameter("@id", SqlDbType.Int)) cmd.Parameters.Add(New SqlParameter("@Was", SqlDbType.VarChar, 40)) cmd.Parameters.Add(New SqlParameter("@Preis", SqlDbType.Money)) cmd.Parameters("@id").Value = 1 ' Richtung der Parameter spezifizieren cmd.Parameters("@RetValue").Direction = ParameterDirection.ReturnValue cmd.Parameters("@Was").Direction = ParameterDirection.Output cmd.Parameters("@Preis").Direction = ParameterDirection.Output ' Kommando ausführen con.Open() cmd.ExecuteNonQuery() ' DbParameterCollection auswerten If CType(cmd.Parameters("@RetValue").Value, Integer) = 1 Then Console.WriteLine("Artikel: {0}", cmd.Parameters("@Was").Value) Console.WriteLine("Preis:{0}", cmd.Parameters("@Preis").Value) End If Console.WriteLine("{0} Datensatz gefunden.", _ cmd.Parameters("@RetValue").Value) con.Close() Console.ReadLine() End Sub End Module End Namespace
Im ersten Schritt wird nach dem Öffnen der Verbindung das DbCommand-Objekt definiert. Anschließend wird für jeden Parameter der gespeicherten Prozedur der Parameters-Auflistung ein SqlParameter-Objekt hinzugefügt. Als Parameter wird auch der von RETURN gelieferte Rückgabewert verstanden, also braucht der Aufruf insgesamt vier Parameter-Objekte. DbParameter können unterschiedliches Verhalten haben; standardmäßig beschreiben sie
einen Eingabeparameter. Abweichungen davon werden in der Direction-Eigenschaft des Parameter-Objekts festgelegt, die vom Typ ParameterDirection ist. Dabei handelt es sich um eine Enumeration mit den in Tabelle 24.3 gezeigten vier Konstanten.
1149
24.5
24
Datenbankabfragen mit ADO.NET
Konstante
Beschreibung
Input
Der Parameter ist ein Eingabeparameter.
InputOutput
Der Parameter unterstützt sowohl die Eingabe als auch die Ausgabe.
Output
Der Parameter ist ein Ausgabeparameter.
ReturnValue
Der Parameter stellt einen Rückgabewert dar.
Tabelle 24.3
Konstanten der Enumeration »ParameterDirection«
Jetzt muss die Parameterliste gefüllt werden, um das DbCommand-Objekt anschließend auszuführen. Dazu wird dem Parameter @id die Spalte ProductID zugewiesen, anhand derer der gesuchte Artikel identifiziert werden soll. Weil die gespeicherte Prozedur keine Datensatzliste zurückgibt, genügt es, die Methode ExecuteNonQuery auf dem DbCommand-Objekt aufzurufen. Das Ergebnis des Aufrufs kann danach ausgewertet werden, indem sowohl der Inhalt des Rückgabewertes als auch der Inhalt der Ausgabeparameter abgerufen werden.
1150
Die Verbindung zwischen den lokal gespeicherten Daten und der Datenbank stellt der DataAdapter her. Dieses Kapitel beschreibt, wie Sie Daten auslesen und Tabellen benennen.
25
DataAdapter
Im letzten Kapitel haben Sie erfahren, wie Sie ein SQL-Kommando gegen eine Datenbank absetzen. Sie wissen, dass mit der Methode ExecuteNonQuery des Command-Objekts eine Aktionsabfrage ausgeführt werden kann und dass von ExecuteReader ein DataReader-Objekt zurückgeliefert wird, in dem wir eine Datenzeile nach der anderen durchlaufen können. Für ganz einfache Anforderungen mag das durchaus genügen, bei objektiver Betrachtung aber werden damit die Bedürfnisse der täglichen Praxis völlig unzureichend abgedeckt. Was ist, wenn wir es dem Anwender ermöglichen wollen, beliebig zwischen den einzelnen Datensätzen zu navigieren? Wie kann ein Anwender die eingelesenen Datensätze aktualisieren? Wie kann seitens der Anwendung sichergestellt werden, dass bei der Aktualisierung Einschränkungen (constraints) berücksichtigt werden? Grundsätzlich ließen sich diese und viele weitere Aufgaben mit dem Gespann Command- und DataReader-Objekt erledigen. Aber denken wir einen Schritt weiter. Beide Objekte sind von einer geöffneten Verbindung zur Datenbank abhängig. Wollen wir es einem Anwender ermöglichen, durch die Datensätze zu navigieren, müssen wir die Verbindung zur Datenquelle über einen längeren Zeitraum geöffnet halten. Eine Verbindung länger als unbedingt notwendig geöffnet zu halten, ist aus vielerlei Hinsicht nicht akzeptabel. Stellen Sie sich nur eine Datenbank im Internet vor. Eine geöffnete Verbindung kostet Geld, und die Netzwerkressourcen werden belastet. Zudem ist die Anzahl der gleichzeitigen Zugriffe auf eine Datenbank begrenzt. Ein DataAdapter vermeidet diese Probleme. Hinweis Wenn möglich werden die Db- statt der Sql-Typen verwendet. Alle Aussagen, die sich im Folgenden auf die Db-Typen beziehen, gelten auch für die analogen Sql-Typen.
25.1
Was ist ein DataAdapter?
Eine gute Lösung sollte alle Datensätze einlesen und im lokalen Speicher ablegen. Wir brauchen diesen Ansatz jedoch nicht selbst zu programmieren, ADO.NET stellt mit der (abstrak-
1151
25
DataAdapter
ten) Klasse DbDataAdapter ein Bindeglied zwischen der Datenquelle und dem lokalen Speicher zur Verfügung. Es kann Daten aus einer Datenquelle abfragen und in einer oder mehreren Tabellen im lokalen Speicher halten. Darüber hinaus kann ein DataAdapter-Objekt auch lokale Änderungen an den Tabellen an die Datenquelle übermitteln. Um die Netzwerkund Datenbankbelastung so gering wie möglich zu halten, baut das DataAdapter-Objekt nur dann eine Verbindung zur Datenbank auf, wenn dies notwendig ist. Sind alle Operationen beendet, wird die Verbindung wieder geschlossen. Im Zusammenhang mit einem DataAdapter spielen auch Connection- und Command-Objekte eine wichtige Rolle. Alle drei sind provider-spezifisch (zum Beispiel SqlDataAdapter, SqlConnection und SqlCommand) und werden zu den verbundenen Typen des ADO.NET-Objektmodells gezählt. Die Datentypen, die die Daten im lokalen Speicher automatisch verwalten und organisieren, werden zu den unverbundenen Typen des ADO.NET-Objektmodells gerechnet. Ein DataAdapter kann daher als Bindeglied zwischen den verbundenen und den unverbundenen Objekten angesehen werden. Ein DataAdapter spielt in zwei Szenarien eine wichtige Rolle: 왘
beim Füllen eines DataSets oder einer DataTable
왘
beim Aktualisieren des geänderten Inhalts von DataSet bzw. DataTable
In diesem Kapitel werden wir uns ausschließlich mit dem Abrufen von Dateninformationen und dem sich daran anschließenden Füllen der lokalen Objekte beschäftigen. Das »andere Gesicht« des DataAdapters sehen wir uns nach dem Studium von DataSet an.
25.1.1 Ein Programmbeispiel Ehe wir uns mit dem DataAdapter genauer beschäftigen, möchte ich Ihnen ein Beispiel zeigen, das mithilfe eines DataAdapters den lokalen Speicher mit den Spalten ProductName und UnitPrice aller Datensätze der Tabelle Products füllt. Die lokale Datensatzliste wird anschließend in einer Schleife an der Konsole ausgegeben. Auf die genaue Erklärung des Codes soll hier noch verzichtet werden. Auffällig ist, dass Open und Close fehlen. '...\ADO\DataAdapter\Beispiel.vb
Option Strict On Imports System.Data.Common, System.Data.SqlClient Namespace ADO Module Beispiel Sub Test() Dim con As DbConnection = New SqlConnection() con.ConnectionString = "Data Source=(local);" & _ "Initial Catalog=Northwind;Integrated Security=sspi" Dim cmd As DbCommand = New SqlCommand() cmd.CommandText = "SELECT ProductName, UnitPrice FROM products" cmd.Connection = con
1152
Initialisierung
Dim da As DbDataAdapter = New SqlDataAdapter() da.SelectCommand = cmd Dim tbl As New DataTable() ' lokaler Speicher da.Fill(tbl) For Each row As DataRow In tbl.Rows Console.WriteLine("{0,-35} {1} ", row(0), row(1)) Next Console.ReadLine() End Sub End Module End Namespace
25.2
Initialisierung
Der DataAdapter muss wissen, auf welcher Verbindung er einen Befehl absetzen soll, und er muss selbstverständlich auch den Befehl kennen. Wird dies nicht im Konstruktor spezifiziert, muss die Eigenschaft SelectCommand entsprechend gesetzt werden.
25.2.1 Konstruktoren Der DataAdapter stellt die Verbindung zwischen einer Datenquelle und einem DataSet bzw. einer DataTable her und füllt diese mit den angefragten Daten. Die DataAdapter-Klassen (OleDbDataAdapter, SqlDataAdapter, OracleDataAdapter und OdbcDataAdapter) verfügen jeweils über vier Konstruktoren. Das Xxx in der folgenden Syntax kann die korrespondierenden Werte OleDb, Sql, Oracle oder Odbc annehmen. Public Public Public Public
Sub Sub Sub Sub
New() New(selectCommand As XxxCommand) New(selectCommandText As String, connection As XxxConnection) New(selectCommandText As String, connectionString As String)
Konstruktor und Verbindungen Wenn Sie in Ihrer Anwendung mehrere DataAdapter-Objekte verwenden, sollten Sie mit Bedacht den Konstruktor wählen. Wenn Sie den Konstruktoraufrufen der DataAdapterObjekte nämlich eine Zeichenfolge übergeben, beispielsweise Dim strCon As String = "Data Source=wsak\\SQL2005;" & _ "Initial Catalog=northwind;Trusted_Connection=Yes" Dim da1 As DbDataAdapter = New SqlDataAdapter(strSQL1, strCon) Dim da2 As DbDataAdapter = New SqlDataAdapter(strSQL2, strCon)
dann wird für jedes DataAdapter-Objekt eine neue Verbindung eingerichtet. War das von Ihnen beabsichtigt, gibt es daran nichts zu kritisieren. Reicht Ihnen aber eine Verbindung aus, sollten Sie stattdessen den Konstruktor verwenden, der neben der Abfragezeichenfolge die
1153
25.2
25
DataAdapter
Referenz auf das Connection-Objekt erwartet, oder Sie sollten nach dem parameterlosen Konstruktor die Verbindung über die Eigenschaft SelectCommand festlegen.
25.2.2 Die Eigenschaft SelectCommand Wenn Sie den parameterlosen Konstruktor verwenden, müssen Sie der Eigenschaft SelectCommand die Referenz auf ein Command-Objekt zuweisen. Dim da As DbDataAdapter = New SqlDataAdapter() da.SelectCommand = cmd
Die DataAdapter-Klassen stellen keine Möglichkeit bereit, mit der wir eine Verbindungszeichenfolge oder ein Connection-Objekt festlegen können. Das ist aber unwichtig, da das Command-Objekt seinerseits selbst alle Verbindungsinformationen enthält. Dim strCon As String = "..." Dim con As SqlConnection = New SqlConnection(strCon) Dim cmd As DbCommand = New SqlCommand("SELECT * FROM Products", con)
25.3
Den lokalen Datenspeicher füllen
Es lässt sich trefflich darüber streiten, welche Methode eines bestimmten Typs die wichtigste ist. Bei einem DataAdapter-Objekt ist das auch nicht anders, aber meiner Meinung nach sind es zwei Methoden, die den Kern dieses Typs ausmachen: 왘
Fill
왘
Update
Mit der Methode Fill wird der lokale Datenspeicher mit dem Ergebnis einer SELECT-Abfrage gefüllt. Dazu wird für die Dauer der Abfrageoperation eine Verbindung zur Datenquelle geöffnet und nach der Beendigung wieder geschlossen. Die empfangenen Daten werden in einem DataTable-Objekt vorgehalten, das sich in einem DataSet befindet. DataTable beschreibt alle Spalten, die in der SELECT-Abfrage angegeben sind. Die Spaltenbezeichner werden aus der Originaldatenbank übernommen. Der Anwender kann die Daten ändern, Datensätze löschen oder neue hinzufügen. Da währenddessen kein Kontakt zur Datenbank besteht (DataTable ist ein unverbundenes Objekt), wird die Datenbank nichts von den Änderungen im DataSet bzw. der DataTable mitbekommen. Zu irgendeinem Zeitpunkt sollen die Änderungen natürlich in die Originaldatenquelle zurückgeschrieben werden. Dazu muss die Methode Update des DataAdapters aufgerufen werden. Der DataAdapter sorgt dann dafür, dass die Verbindung erneut aufgebaut wird und die geänderten Daten in die Originaldatenbank geschrieben werden. Ist die Aktualisierung beendet, wird die Verbindung automatisch geschlossen. Die ersten beiden Überladungen von Fill möchte ich Ihnen näher vorstellen:
1154
Den lokalen Datenspeicher füllen
Public Function Fill(ds As DataSet) As Integer Public Function Fill(dt As DataTable) As Integer Public Function Fill(ds As DataSet, srcTable As String) As Integer Public Function Fill(startRecord As Integer, maxRecords As Integer, _ ParamArray dts As DataTable()) As Integer Public Function Fill(ds As DataSet, startRecord As Integer, _ maxRecords As Integer, srcTable As String) As Integer
Im einfachsten Fall wird entweder ein DataTable- oder ein DataSet-Objekt übergeben. Beide Typen sind unabhängig vom .NET-Datenprovider und befinden sich im Namensraum System.Data. Ein DataTable-Objekt entspricht einer Tabelle in der Datenbank. Es hat die Spalten, die in der SELECT-Abfrage angegeben worden sind, und enthält die Datensätze, die das Ergebnis der SELECT-Abfrage bilden. Der Rückgabewert der Fill-Methode gibt an, wie viele Datenzeilen dem DataSet oder der DataTable hinzugefügt wurden. Ein DataSet-Objekt können Sie sich als einen Container für mehrere DataTable-Objekte vorstellen. Im Beispiel weiter oben hätten wir auch anstelle eines DataTable-Objekts ein DataSet füllen können. Der Code in der Schleife muss dann entsprechend angepasst werden. ... Dim ds As DataSet = New DataSet() da.Fill(ds) For Each row As DataRow In ds.Tables(0).Rows Console.WriteLine("{0,-35} {1} ", row(0), row(1)) Next
Nach dem Füllen einer DataTable oder eines DataSets gibt es keine Verbindung mehr zum DataAdapter. Das bedeutet: Der DataAdapter hat keine Referenz auf das Objekt, das er gefüllt hat, und das gefüllte Objekt weiß nicht, von wem es gefüllt worden ist.
25.3.1 Verbindungen öffnen und schließen Kommen wir noch einmal auf das einführende Beispiel von oben zurück. Mit Dim da As DbDataAdapter = New SqlDataAdapter() da.SelectCommand = cmd
wird das DataAdapter-Objekt erzeugt, und mit dem Kommando cmd wird auch die Verbindung con festgelegt. Es fällt auf, dass die Open-Methode nicht aufgerufen wird, um die Abfrage zu übermitteln. Das ist auch nicht nötig, denn mit da.Fill(tbl)
öffnet der DataAdapter die Verbindung selbstständig, fragt die Ergebnisse ab und schließt die Verbindung auch selbstständig. Das steht ganz im Gegensatz zu den Execute-Methoden des Command-Objekts, die auf das explizite Öffnen der Verbindung angewiesen sind. Sie können allerdings explizit eine Verbindung mit Open öffnen und erst danach Fill aufrufen. Der DataAdapter wird die geöffnete Verbindung bemerken und lehnt im Weiteren auch
1155
25.3
25
DataAdapter
die Verantwortung dafür ab, die Verbindung nach dem Eintreffen der Abfrageresultate zu schließen. Es liegt dann bei Ihnen, die offene Verbindung zu schließen. ... con.Open() da.Fill(tbl) con.Close()
25.3.2 Doppelter Aufruf der Fill-Methode Angenommen, Sie rufen zweimal hintereinander die Fill-Methode auf, ohne vor dem zweiten Aufruf das DataSet oder die DataTable zu leeren, also: ... da.Fill(tbl) da.Fill(tbl)
Die Idee, die dem doppelten Aufruf zugrunde liegt, könnte die Aktualisierung des DataSets sein. Allerdings werden nun die Datensätze in der Tabelle doppelt auftreten. Mit dem ersten Aufruf der Fill-Methode wird das DataTable-Objekt erzeugt, und es werden die Datensätze hineingeschrieben, und mit dem zweiten Aufruf werden die Datensätze einfach noch einmal aus der Datenquelle bezogen und in die schon vorhandene Tabelle kopiert. Der Grund für dieses auf den ersten Blick etwas sonderbare Verhalten ist, dass die Primärschlüsselspalte der Originaltabelle nicht zur Primärschlüsselspalte der DataTable wird. Primärschlüssel dienen unter anderem zur Vermeidung von duplizierten Datensätzen und müssen in der Datenquelle festgelegt werden. Die DataTable übernimmt diese jedoch nicht automatisch.
25.3.3 Mehrere DataAdapter-Objekte aufrufen Wird die Methode Fill hintereinander auf verschiedene DataAdapter aufgerufen, wird jeweils eine neue Verbindung benötigt. Daran ändert sich auch nichts, wenn allen Aufrufen dieselbe Verbindungszeichenfolge zugrunde liegt. ... Dim daProducts As DbDataAdapter = New SqlDataAdapter(strSQL1, con) Dim daCategories As DbDataAdapter = New SqlDataAdapter(strSQL2, con) Dim dsProducts As DataSet = New DataSet() Dim dsCategories As DataSet = New DataSet() daProducts.Fill(dsProducts) ... daCategories.Fill(dsCategories)
Zuerst wird das DataSet namens dsProducts gefüllt. Dazu wird die Verbindung con geöffnet und nach Abschluss der Operation wieder geschlossen. Anschließend landet con im Verbindungspool. Der DataAdapter namens daCategories bedient sich der Verbindung im Verbindungspool, muss aber seinerseits die Verbindung selbst öffnen und nach dem Empfangen der Abfrageergebnisse wieder schließen. Damit sind Leistungseinbußen verbunden.
1156
Tabellenzuordnung mit TableMappings
Wollen Sie sicherstellen, dass eine Verbindung von beiden DataAdapter-Objekten gleichermaßen benutzt wird, müssen Sie die Steuerung selbst übernehmen und die Verbindung mit der Open-Methode vor dem Füllen des ersten DataDets bzw. DataTable öffnen. ... con.Open() daProducts.Fill(dsProducts) daCategories.Fill(dsCategories) con.Close()
Da Sie nun der Fill-Methode die Verantwortung für die Beständigkeit der Verbindung entrissen haben, dürfen Sie am Ende nicht vergessen, die Verbindung mit Close zu schließen.
25.3.4 Der Spalten- und der Tabellenbezeichner einer DataTable Intern bedient sich ein DataAdapter des DataReader-Objekts, um die Ergebnisse einer Abfrage abzurufen. Bevor die Resultate der Abfrage in der DataTable gespeichert werden, benutzt der DataAdapter das DataReader-Objekt, um sich elementare Schemainformationen zu besorgen. Dazu gehören die Spaltenbezeichner und die Datentypen. Aus diesem Grund können Sie über die Spaltenbezeichner auf bestimmte Spalten zugreifen, wenn Sie die Datenzeilen auswerten. Der DataReader ist jedoch nicht in der Lage, den Tabellennamen zu liefern. Standardmäßig heißt die erste Tabelle Table, die zweite Table1, die dritte Table2 usw. Sie können aber auch eine Überladung der Fill-Methode benutzen, der Sie im zweiten Parameter den Tabellennamen übergeben: daProducts.Fill(dsProducts, "Artikel")
Nun wird die Tabelle im DataSet unter dem Namen Artikel angesprochen, nicht mehr unter Table.
25.3.5 Paging mit der Fill-Methode Eine interessante Überladung der Fill-Methode möchte ich Ihnen noch vorstellen. Diese gestattet es, die DataTable mit nur einem Teil des Abfrageergebnisses zu füllen. daProducts.Fill(dsProducts, 0, 10, "Artikel")
Dieser Aufruf bewirkt, dass nur die ersten zehn Datenzeilen des nullbasierten Abfrageergebnisses im DataSet gespeichert werden. Tatsächlich werden dabei aber immer noch alle Datenzeilen von der Abfrage zurückgegeben. Der DataAdapter, der sich intern des DataReaders bedient, ruft dabei aber nur zehnmal die Read-Methode des DataReaders auf.
25.4
Tabellenzuordnung mit TableMappings
Um ein DataSet mit mehreren Tabellen zu füllen, können Sie eine Batch-Abfrage absetzen: Dim strSQL As String = _ "SELECT * FROM Products;SELECT * FROM Suppliers;SELECT * FROM Categories"
1157
25.4
25
DataAdapter
Dim da As DbDataAdapter = New SqlDataAdapter(strSQL, con) Dim ds As DataSet = New DataSet() da.Fill(ds)
Das DataSet enthält nun drei Tabellen. In jeder sind alle Datensätze der Originaltabellen Products, Suppliers und Categories enthalten. Wie können Sie eine bestimmte Tabelle im DataSet ansprechen, wenn darin mehrere Tabellen enthalten sind? Ein DataSet verwaltet in der Eigenschaft Tables alle in ihm enthaltenen Tabellen in einer Auflistung vom Typ DataTableCollection. Public ReadOnly Property Tables As DataTableCollection
Jedes DataTable-Objekt speichert seinen Tabellennamen in der Eigenschaft TableName. Mit diesen Kenntnissen können wir jetzt die Namen der Tabellen im DataSet abfragen. For Each table As DataTable In ds.Tables Console.WriteLine(table.TableName) Next
Die Ausgabe wird nicht – wie vielleicht zu vermuten wäre – Products, Supplieres und Categories lauten, sondern, wie schon vorher behauptet: Table Table1 Table2
Die Zuordnung von Table zu Products, Table1 zu Suppliers und Table2 zu Categories ist aber in den meisten Fällen nicht wünschenswert. Besser geeignet wären sprechende Bezeichner, die zur Lesbarkeit des Programmcodes beitragen. Der DataAdapter bietet daher einen Mechanismus, um den Tabellen im Abfrageergebnis einen anderen Namen zuzuordnen: die Eigenschaft TableMappings, die die Referenz auf ein DataTableMappingCollection-Objekt liefert. Public ReadOnly Property TableMappings As DataTableMappingCollection
Jedes DataTableMapping-Objekt einer DataTableMappingCollection ordnet einer Tabelle im DataSet einen Tabellennamen zu. Die Auflistung ist einfach mit der Add-Methode zu füllen.
Der erste Parameter spezifiziert den Namen der Tabelle innerhalb von DataSet, der zweite gibt den neuen Namen an. Public Function Add(sourceTable As String, dataSetTable As String) _ As DataTableMapping
Das folgende Codefragment zeigt, wie Sie die DataTableMappingCollection eines DataAdapter-Objekts füllen können. Wir verwenden dieselbe Batch-Abfrage wie oben. Die Zuordnung muss vor dem Füllen des DataSets mit Fill erfolgen, sonst bleibt sie wirkungslos.
1158
Tabellenzuordnung mit TableMappings
Dim strSQL As String = _ "SELECT * FROM Products;SELECT * FROM Suppliers;SELECT * FROM Categories" Dim da As DbDataAdapter = new SqlDataAdapter(strSQL, con) da.TableMappings.Add("Table", "Artikel") da.TableMappings.Add("Table1", "Lieferanten") da.TableMappings.Add("Table2", "Kategorien") Dim ds As DataSet = new DataSet() da.Fill(ds) ...
Add ruft implizit den DataTableMapping-Konstruktor auf. Sie können das natürlich auch selbst in die Hand nehmen, müssen dann aber jeder Tabelle über die Eigenschaft SourceTable sagen, welchen Standardnamen sie im DataSet hat, und über DataSetTable mitteilen, welcher Bezeichner der Tabelle neu zugeordnet werden soll. Das folgende Beispiel zeigt, wie der Code dazu aussieht: Dim dtm1 As DataTableMapping = New DataTableMapping() dtm1.SourceTable = "Table" dtm1.DataSetTable = "Artikel" da.TableMappings.Add(dtm1) Dim dtm2 As DataTableMapping = New DataTableMapping() dtm2.SourceTable = "Table1" dtm2.DataSetTable = "Lieferanten" da.TableMappings.Add(dtm2) Dim dtm3 as DataTableMapping = New DataTableMapping() dtm3.SourceTable = "Table2" dtm3.DataSetTable = "Kategorien" da.TableMappings.Add(dtm3) Dim ds As DataSet = New DataSet() da.Fill(ds) ...
Die Klasse DataTableMapping gehört zum Namensraum System.Data.Common, der vorher mit Imports bekannt gegeben werden sollte. Deutlich ist zu sehen, dass diese Art der Zuordnung mehr Programmieraufwand bedeutet.
25.4.1 Spaltenzuordnungen in einem DataSet Jeder Spalte der SELECT-Abfrage wird eine Spalte in der DataTable zugeordnet. Als Spaltenbezeichner verwendet ADO.NET dabei den Spaltennamen der Originaltabelle in der Datenbank. Fragen Sie die Datenquelle mit SELECT ProductName, UnitPrice FROM Products
ab, lauten die Spalten in der DataTable ebenfalls ProductName und UnitPrice. Wünschen Sie andere Spaltenbezeichner, können Sie im SELECT-Statement für die einzelnen Spalten einen Alias angeben, zum Beispiel: SELECT ProductName AS Artikelname, UnitPrice As Einzelpreis FROM Products
1159
25.4
25
DataAdapter
Nun lauten in der DataTable die Spaltenbezeichner Artikelname und Einzelpreis. Sie können alternativ einen anderen Mechanismus einsetzen. Ein DataTableMapping-Objekt hat eine eigene Auflistung, mit der den vordefinierten Spaltenbezeichnern neue zugeordnet werden können. Diese Auflistung ist vom Typ DataColumnMappingCollection und enthält DataColumnMapping-Objekte. Jedes DataColumnMapping-Objekt beschreibt für sich eine Neuzuordnung eines Spaltenbezeichners in einer DataTable. Die ein wenig komplex anmutenden Zusammenhänge zwischen DataAdapter, DataTableMapping und DataColumnMapping sind in Abbildung 25.1 dargestellt.
DataAdapter-Objekt DataTableMapping DataTableMappingCollection DataColumnMapping
DataColumnMappingCollection
Abbildung 25.1
Hierarchie der Zuordnungsklassen
Die Referenz auf die DataColumnMappingCollection stellt die Eigenschaft ColumnMappings der Klasse DataTableMapping bereit: Public ReadOnly Property ColumnMappings As DataColumnMappingCollection
Um eine Neuzuordnung festzulegen, bietet sich auch hier der Weg über die Add-Methode des DataColumnMappingCollection-Objekts an: Public Function Add(sourceColumn As String, dataSetColumn As String) _ As DataColumnMapping
Analog zur Add-Methode der DataTableMappingCollection wird dem ersten Parameter der ursprüngliche Spaltenbezeichner und dem zweiten Parameter der neue Spaltenbezeichner übergeben. Das folgende Codefragment zeigt den Code, der notwendig ist, um neben dem Tabellennamen auch die Spaltenbezeichner einer Abfrage neu festzulegen. Zum Schluss werden die Spaltenneuzuordnungen zur Bestätigung an der Konsole ausgegeben. Der Code im Schleifenkopf zur Ausgabe der Spaltenbezeichner dürfte auch ohne weitere Erläuterungen verständlich sein.
1160
Tabellenzuordnung mit TableMappings
'...\ADO\DataAdapter\Mapping.vb
Option Strict On Imports System.Data.Common, System.Data.SqlClient Namespace ADO Module Mapping Sub Test() Dim con As DbConnection = New SqlConnection() con.ConnectionString = "Data Source=(local);" & _ "Initial Catalog=Northwind;Integrated Security=sspi" Dim cmd As DbCommand = New SqlCommand() cmd.CommandText = "SELECT ProductName, UnitPrice FROM products" cmd.Connection = con Dim da As DbDataAdapter = New SqlDataAdapter() da.SelectCommand = cmd ' Neuzuordnung des Tabellennamens Dim dtm As DataTableMapping = da.TableMappings.Add("Table", "Artikel") ' Neuzuordnung der Spaltenbezeichner dtm.ColumnMappings.Add("ProductName", "Artikelname") dtm.ColumnMappings.Add("UnitPrice", "Einzelpreis") ' lokaler Speicher Dim ds As New DataSet() da.Fill(ds) ' Bezeichner ausgeben Console.WriteLine("Tabelle {0}", ds.Tables(0).TableName) For Each col As DataColumn In ds.Tables(0).Columns Console.WriteLine("Spalte {0} ", col.ColumnName) Next Console.ReadLine() End Sub End Module End Namespace
25.4.2 Spaltenzuordnungen einer DataTable Übergeben Sie der Fill-Methode anstelle eines DataSet-Objekts ein DataTable-Objekt, müssen Sie ein wenig anders vorgehen, um die Spalten mit eigenen Bezeichnern im lokalen Datenspeicher anzusprechen. Dazu erzeugen Sie auch wieder ein DataTableMapping-Objekt, dem Sie die gewünschten Spaltenbezeichner zuordnen. Bei der Instanziierung von DataTable rufen Sie allerdings den parametrisierten Konstruktor auf, dem der im DataTableMappping zugeordnete Tabellenname übergeben wird. ... Dim dtm As DataTableMapping = da.TableMappings.Add("Table", "Artikel") ' Neuzuordnung der Spaltenbezeichner dtm.ColumnMappings.Add("ProductName", "Artikelname")
1161
25.4
25
DataAdapter
dtm.ColumnMappings.Add("UnitPrice", "Einzelpreis") Dim tbl As DataTable = New DataTable("Artikel") da.Fill(tbl) ...
25.4.3 Die Eigenschaft MissingMappingAction des DataAdapters Die Neuzuordnung der Tabellen- und Spaltenbezeichner ist eine Option, die vor dem Aufruf der Methode Fill wahrgenommen werden kann oder nicht. Der DataAdapter prüft vor dem Füllen des DataSets, ob die Zuordnungsauflistungen gefüllt sind. Dabei interessiert er sich besonders für die Spaltenzuordnungen. Für jede Spalte des Abfrageergebnisses überprüft der DataAdapter, ob dafür eine Zuordnung in der DataColumnMappingCollection angegeben ist. Existiert sie nicht, überprüft er im nächsten Schritt seine MissingMappingAction-Eigenschaft, die vom Typ der gleichnamigen Enumeration ist (siehe Tabelle 25.1). Ihr Standardwert sorgt dafür, dass die Spaltennamen der Originaltabelle in die DataTable übernommen werden. Alternativ kann der DataAdapter angewiesen werden, alle Spalten zu ignorieren, die nicht in der Zuordnungstabelle enthalten sind. Eine dritte Möglichkeit ist die Auslösung einer Ausnahme, wenn keine Zuordnung angegeben ist. Konstante
Effekt bei fehlender Spaltenzuordnung
Error
Eine Ausnahme wird ausgelöst.
Ignore
Die Spalte in der DataTable wird ignoriert.
Passthrough
Die Spalte wird unter ihrem ursprünglichen Namen der DataTable hinzugefügt (Standard).
Tabelle 25.1
25.5
Konstanten der Enumeration »MissingMappingAction«
Das Ereignis FillError des DataAdapters
Sollte beim Füllen des DataSets oder der DataTable ein Fehler auftreten, löst der DataAdapter das Ereignis FillError aus. Sie können das Ereignis dazu benutzen, um zum Beispiel die Ereignisursache zu protokollieren. Standardmäßig wird nach Beendigung des Ereignisses eine Ausnahme ausgelöst. Statt diese Ausnahme in Ihrem Code abzufangen, sollten Sie besser einen eigenen Ereignishandler bereitstellen und darin der Eigenschaft Continue des zweiten Handlerparameters den Wert True zuweisen. Im folgenden Programmbeispiel wird ein Fehler beim Füllen des DataSets ausgelöst, indem die Eigenschaft MissingMappingAction des DataAdapters auf Error gesetzt wird. Im Ereignishandler wird die Folgeausnahme mit ev.Continue=True unterdrückt.
1162
Das Ereignis FillError des DataAdapters
'...\ADO\DataAdapter\FillError.vb
Option Strict On Imports System.Data.Common, System.Data.SqlClient Namespace ADO Module FillError Sub Test() Dim con As DbConnection = New SqlConnection() con.ConnectionString = "Data Source=(local);" & _ "Initial Catalog=Northwind;Integrated Security=sspi" Dim cmd As DbCommand = New SqlCommand() cmd.CommandText = "SELECT ProductName, UnitPrice FROM products" cmd.Connection = con Dim da As DbDataAdapter = New SqlDataAdapter() da.SelectCommand = cmd da.MissingMappingAction = MissingMappingAction.Error AddHandler da.FillError, AddressOf Fehler Dim ds As New DataSet() da.Fill(ds) Console.ReadLine() End Sub Sub Fehler(ByVal sender As Object, ByVal ev As FillErrorEventArgs) Console.WriteLine(ev.Errors.Message) ev.Continue = True End Sub End Module End Namespace
1163
25.5
Die Daten der Datenbank werden lokal in einem DataSet gespeichert. Dieses Kapitel zeigt, wie Sie (auch ohne Verbindung zur Datenbank) mit den Daten arbeiten – unter Beibehaltung der Konsistenz der Daten. Der Abgleich der lokal geänderten Daten mit der Datenbank ist auch Thema des Kapitels. Der Fall mehrerer Tabellen gleichzeitig wird auch behandelt. Schließlich wird gezeigt, wie die Daten gefiltert werden können.
26
Offline mit DataSet
Die Klasse DataSet nimmt die zentrale Stellung in ADO.NET ein, da Objekte dieses Typs die aus einer Datenbank gelesenen Daten speichern. Organisiert und verwaltet werden die Daten in Form von Tabellen. Wenn Sie sich darunter Tabellen ähnlich denen von MS-Excel vorstellen, liegen Sie gar nicht ganz so falsch. Ob es sich um eine oder auch mehrere Tabellen handelt, hängt von der zugrunde liegenden Abfrage ab, die durch das Command-Objekt beschrieben wird. Enthält das DataSet mehrere Tabellen, können zwischen den Tabellen Beziehungen eingerichtet werden – ganz so wie in der Originaldatenbank. Im vorletzten Kapitel haben Sie den Typ DataReader und seine provider-spezifischen Variationen kennengelernt. Mit einem Objekt dieses Typs können Sie Daten basierend auf einer Abfrage abrufen. Ein DataReader kann aber nicht die üblichen Aufgaben einer Datenbankanwendung erfüllen. Sie können nur vorwärts navigieren, zudem sind die Daten schreibgeschützt. Damit ist der DataReader in seiner Funktionalität sehr eingeschränkt, aber zum Ausgleich ist er sehr schnell. Ein DataSet hingegen ist im Vergleich dazu deutlich leistungsfähiger. Die Daten im DataSet stehen in keinem Kontakt zur Datenbank. Nachdem das DataSet über das DataAdapter-Objekt gefüllt worden ist, gibt es zwischen DataSet und Datenbank keine Verbindung mehr. Nimmt ein Anwender Änderungen an den Daten vor, schreiben sich diese nicht in die Originaldatenbank zurück, sondern werden vielmehr lokal im DataSet gespeichert. Das Zurückschreiben der geänderten Daten muss explizit angestoßen weren. Häufig kann man sich dazu wieder des DataAdapters bedienen, der die notwendige Aktualisierungslogik bereitstellt. Sie werden korrekterweise einwenden, dass damit Konfliktsituationen vorprogrammiert sind, wenn ein zweiter Anwender zwischenzeitlich Änderungen am gleichen Datensatz vorgenommen hat. ADO.NET gibt uns aber alle Mittel an die Hand, um eine benutzerdefinierte Konfliktsteuerung und Konfliktanalyse zu codieren. Mit der Aktualisierung der Originaldatenbank werden wir uns im nächsten Kapitel beschäftigen. Ein DataSet kann aber noch mehr. Sie können mit ihm die Ansicht der Abfrageergebnisse ändern, und Sie können die Daten basierend auf einer oder mehreren Spalten sortieren oder nach bestimmten Kriterien filtern. Außerdem ist die Zusammenarbeit mit XML ausgezeichnet. Der Inhalt eines DataSets kann als XML-Dokument in einer Datei gespeichert werden bzw.
1165
26
Offline mit DataSet
kann der Inhalt einer XML-Datei in ein DataSet eingelesen werden. Darüber hinaus lassen sich die Schemainformationen eines DataSets in einer XML-Schemadatei speichern.
26.1
Das DataSet-Objekt verwenden
26.1.1 Ein DataSet-Objekt erzeugen Die Klasse DataSet befindet sich, wie viele andere Klassen, die nicht provider-spezifisch sind, im Namespace System.Data. In den meisten Fällen ist der parameterlose Konstruktor vollkommen ausreichend, um ein DataSet-Objekt zu erzeugen: Dim ds As DataSet = New DataSet()
Der einfach parametrisierte Konstruktor gibt dem DataSet einen Namen: Dim ds As DataSet = New DataSet("DSAutoren")
Der Name kann auch über die Eigenschaft DataSetName festgelegt oder abgerufen werden.
26.1.2 Anatomie einer DataTable Zum Leben erweckt wird ein DataSet-Objekt nicht durch die Instanziierung der Klasse, sondern vielmehr durch den Aufruf der Fill-Methode des DataAdapters: ... Dim strSQL As String = "SELECT * FROM Products" Dim da As DbDataAdapter = New SqlDataAdapter(strSQL, con) Dim ds As DataSet = New DataSet() da.Fill(ds) ...
Das Ergebnis der Abfrage enthält alle Datensätze der Tabelle Products in einer Tabelle, die durch ein DataTable-Objekt beschrieben wird. Jeder Spalte, die im SELECT-Statement der Abfrage angegeben ist, entspricht ein Objekt vom Typ DataColumn. Alle Spalten sind in der Eigenschaft Columns vom Typ DataColumnCollection zusammengefasst. In ähnlicher Weise sind auch die Daten der Abfrage organisiert. Jeder von der Datenbank zurückgelieferte Datensatz wird in einem Objekt vom Typ DataRow gespeichert. Die Auflistung der DataRowCollection enthält alle Datenzeilen in einer Tabelle, auf die Sie über die DataTable-Eigenschaft Rows zugreifen können. Eine DataTable hat also eine DataColumnCollection und eine DataRowCollection. Da ein DataSet nicht nur eine, sondern beliebig viele Tabellen enthalten kann, muss auch der Zugriff auf eine bestimmte DataTable im DataSet möglich sein. Wie zu erwarten, werden auch alle Tabellen in einem DataSet in einer Auflistung gespeichert. Diese ist vom Typ DataTableCollection, deren Referenz die Eigenschaft Tables des DataSets liefert. Das klingt alles ziemlich komplex. Die grafische Darstellung in Abbildung 26.1 zeigt den eigentlich einfachen logischen Aufbau.
1166
Das DataSet-Objekt verwenden
DataSet DataTable-Objekte DataTableCollection
DataColumnCollection
DataColumn-Objekte
DataRowCollection
DataRow-Objekte Abbildung 26.1
Struktur eines DataSet
26.1.3 Zugriff auf eine Tabelle im DataSet Wenn ds ein DataSet-Objekt beschreibt, genügt eine Anweisung wie die folgende, um auf eine bestimmte Tabelle im DataSet zuzugreifen: ds.Tables(2)
Enthält das DataSet mehrere Tabellen, lassen sich die Indizes oft nur schwer einer der Tabellen zuordnen. Wie Sie im letzten Kapitel erfahren haben, weist der DataAdapter den Tabellen im DataSet auch Bezeichner zu (Table, Table1, Table2 usw.). Sowohl die Indizes als auch die Standardbezeichner sind aber wenig geeignet, um den Code gut lesbar zu gestalten. Das Kapitel beschreibt, wie mit einer DataTableMappingCollection sowie einer DataColumnMappingCollection lesbare Tabellen- und Spaltennamen zugeordnet werden. Sie sollten diese Angebote nutzen, denn die Anweisung ds.Tables("Artikel")
wird Ihnen später eher dabei helfen, den eigenen Programmcode zu verstehen, als die Angabe eines Index, der sich nur schlecht zuordnen lässt.
26.1.4 Zugriff auf die Ergebnisliste Ein DataRow-Objekt stellt den Inhalt eines Datensatzes dar und kann sowohl gelesen als auch geändert werden. Um in einer DataTable von einem Datensatz zum anderen zu navigieren, benutzen Sie deren Eigenschaft Rows, die die Referenz auf das DataRowCollection-Objekt der Tabelle zurückgibt. Dieses Objekt enthält alle Datensätze, die die Abfrage zurückgibt. Die einzelnen DataRows sind über den Index der Auflistung adressierbar.
1167
26.1
26
Offline mit DataSet
Mit der folgenden Anweisung wird der Verweis auf die dritte Datenzeile in der Tabelle Artikel des DataSets namens ds der Variablen row zugewiesen: Dim row As DataRow = ds.Tables("Artikel").Rows(2)
Über die Indexierung können Sie den Inhalt einer oder mehrerer Spalten der betreffenden Datenzeile auswerten. Dem Indexer übergeben Sie entweder den Namen der Spalte, deren Index in der DataColumnCollection der DataTable (die Ordinalposition) oder eine Referenz auf die gewünschte Spalte. Damit sind ds.Tables(0).Rows(4)("ProductName") ds.Tables(0).Rows(4)(1)
gültige Ausdrücke, um den Inhalt einer Datenzelle auszuwerten. Der Rückgabewert ist immer vom Typ Object und enthält die Daten der angegebenen Spalte. Häufig ist eine anschließende Konvertierung in den richtigen Datentyp notwendig. Die Indexierung über den Spaltenbezeichner bezieht sich immer auf die Spaltenzuordnungen in der DataColumnMappingCollection. Die implizite Zuordnung reicht die Spaltenbezeichner der Originaldatenbank an die DataTable weiter, sodass Sie ohne eigene Definitionen diese im Indexer verwenden können. Sonst sind es die von Ihnen definierten Spaltennamen. Dazu wollen wir uns ein Beispiel ansehen. '...\ADO\DataSet\DataRows.vb
Option Strict On Imports System.Data.Common, System.Data.SqlClient Namespace ADO Module DataRows Sub Test() Dim con As DbConnection = New SqlConnection() con.ConnectionString = "Data Source=(local);" & _ "Initial Catalog=Northwind;Integrated Security=sspi" Dim cmd As DbCommand = New SqlCommand() cmd.CommandText = _ "SELECT ProductName, UnitPrice FROM Products WHERE UnitsOnOrder > 0" cmd.Connection = con Dim ds As New DataSet() Dim da As DbDataAdapter = New SqlDataAdapter() da.SelectCommand = cmd da.Fill(ds, "Artikel") Dim tbl As DataTable = ds.Tables("Artikel") For no As Integer = 0 To tbl.Rows.Count – 1 Console.WriteLine("{0,-35}{1}", _ tbl.Rows(no)("ProductName"), tbl.Rows(no)("UnitPrice")) Next Console.ReadLine() End Sub
1168
Das DataSet-Objekt verwenden
End Module End Namespace
Gefragt ist nach allen Artikeln, zu denen aktuell Bestellungen vorliegen. Nach dem Füllen des DataSets wird die Ergebnisliste in einer Schleife durchlaufen. Der Schleifenzähler wird dabei als Index der Datenzeile genutzt. Um die Anweisungen kurz zu halten, wird vor Beginn des Schleifendurchlaufs die DataTable im DataSet in einer Variablen gespeichert. Dim tbl As DataTable = ds.Tables("Artikel")
Da alle Datenzeilen von einer Auflistung verwaltet werden, stehen die üblichen Methoden und Eigenschaften zur Verfügung. In diesem Code wird die Eigenschaft Count abgefragt, um festzustellen, wie viele Datenzeilen sich in der Ergebnisliste befinden. Sie können auch statt der For-Schleife eine For Each-Schleife einsetzen. Der folgende Codeausschnitt ersetzt daher vollständig die For-Schleife unseres Beispiels: For Each row As DataRow in tbl.Rows Console.WriteLine("{0,-35}{1}", row("ProductName"), row("UnitPrice")) Next
26.1.5 Dateninformationen in eine XML-Datei schreiben Sie können die Dateninformationen eines DataSets in eine XML-Datei schreiben und später wieder laden. Hierzu stehen Ihnen in DataSet mit WriteXml und ReadXml zwei Methoden zur Verfügung. Beiden Methoden übergeben Sie als Parameter den Ort, an dem die Daten gespeichert bzw. aus dem die XML-Daten gelesen werden sollen, zum Beispiel: ds.WriteXml("C:\Daten\ContentsOfDataset.xml") ... ds.ReadXml("C:\Daten\ContentsOfDataset.xml")
Der Parameter beschränkt sich nicht nur auf Dateien. Sie können auch einen TextReader, einen Stream oder einen XmlReader angeben. Nachfolgend sehen Sie den Teilausschnitt eines XML-Dokuments, dem die Abfrage SELECT ProductID, ProductName FROM Products
zugrunde liegt.