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!
Die Wahl für professionelle Programmierer und Softwareentwickler. Anerkannte Experten wie z.B. Bjarne Stroustrup, der Erfinder von C++, liefern umfassendes Fachwissen zu allen wichtigen Programmiersprachen und den neuesten Technologien, aber auch Tipps aus der Praxis. Die Reihe von Profis für Profis!
Hier eine Auswahl: .NET 3.0 Jürgen Kotz, Rouven Haban, Simon Steckermeier 400 Seiten € 29,95 [D], € 30,80 [A] ISBN 978-3-8273-2493-1
Mit diesem Buch erhalten Sie einen Überblick über die neuen .NET 3.0Technologien Windows Presentation Foundation, Windows Communication Foundation und Windows Workflow Foundation. Anhand eines durchgängigen Beispiels beschreiben die Autoren die wichtigsten Features und wie diese praktisch eingesetzt werden können.
Visual C# 2005 Frank Eller 1104 Seiten € 49,95 (D), € 51,40 (A) ISBN 978-3-8273-2288-2
Fortgeschrittene und Profis erhalten hier umfassendes Know-how zur Windows-Programmierung mit Visual C# in der Version 2. Nach einer Einführung ins .NET-Framework und die Entwicklungsumgebung geht der Autor ausführlich auf die Grundlagen der C#-Programmierung ein. Anhand zahlreicher Beispiele zeigt er die verschiedenen Programmiertechniken wie z.B. Anwendungsdesign, Grafikprogrammierung oder das Erstellen eigener Komponenten. Besondere Schwerpunkte liegen auf der umfangreichen .NET-Klassenbibliothek und Windows Forms sowie auf dem Datenbankzugriff mit ADO.NET.
Dirk Frischalowski
Windows Presentation Foundation Grafische Oberflächen entwickeln mit .NET 3.0
An imprint of Pearson Education München • Boston • San Francisco • Harlow, England Don Mills, Ontario • Sydney • Mexico City Madrid • Amsterdam
Bibliografische Information Der Deutschen Bibliothek Die Deutsche Bibliothek verzeichnet diese Publikation in der Deutschen Nationalbibliografie; detaillierte bibliografische Daten sind im Internet über abrufbar. Die Informationen in diesem Produkt werden ohne Rücksicht auf einen eventuellen Patentschutz veröffentlicht. Warennamen werden ohne Gewährleistung der freien Verwendbarkeit benutzt. Bei der Zusammenstellung von Abbildungen und Texten wurde mit größter Sorgfalt vorgegangen. Trotzdem können Fehler nicht vollständig ausgeschlossen werden. Verlag, Herausgeber und Autoren können für fehlerhafte Angaben und deren Folgen weder eine juristische Verantwortung noch irgendeine Haftung übernehmen. Für Verbesserungsvorschläge und Hinweise auf Fehler sind Verlag und Herausgeber dankbar. Alle Rechte vorbehalten, auch die der fotomechanischen Wiedergabe und der Speicherung in elektronischen Medien. Die gewerbliche Nutzung der in diesem Produkt gezeigten Modelle und Arbeiten ist nicht zulässig. Fast alle Hardware- und Softwarebezeichnungen und weitere Stichworte und sonstige Angaben, die in diesem Buch verwendet werden, sind als eingetragene Marken geschützt. Da es nicht möglich ist, in allen Fällen zeitnah zu ermitteln, ob ein Markenschutz besteht, wird das ®-Symbol in diesem Buch nicht verwendet.
Umwelthinweis: Dieses Produkt wurde auf chlorfrei gebleichtem Papier gedruckt.
Vorwort Geschafft. Nach knapp neun Monaten »Entwicklungszeit« ist das Ihnen vorliegende Buch fertig geworden. Dabei hatte ich schon einen viel früheren Erscheinungstermin geplant. Im Nachhinein denke ich aber, dass dies dem Buch gutgetan hat, da gerade in den letzten Monaten viele neue Informationen zur WPF verfügbar wurden und ich diese natürlich in den jeweiligen Kapiteln berücksichtigt habe. Sie werden es unter anderem an der Vielfalt der Themen bemerken, die behandelt werden. Das Buch ist ein insgesamt recht umfangreicher Rundumschlag zu (fast) allen Dingen, welche die WPF zu bieten hat. Schwerpunkte gibt es keine, ich habe vielmehr versucht, zu allen Themen das Wesentliche vorzustellen. Dieses breitere Wissen sollte Ihnen bei allen Entscheidungen hilfreich sein. In 20 Kapiteln erhalten Sie somit einen recht tief gehenden Überblick. Auch Randthemen wie UI Automation, 3D-Grafik und ein erster Blick auf Expression Blend kommen nicht zu kurz. Sie wissen noch gar nichts mit der WPF anzufangen? Dann sollen ein paar einführende Worte die Aufgabe der WPF umreißen. Die WPF stellt als Bestandteil des .NET Frameworks 3.0 ein API zur Verfügung, um anspruchsvolle grafische Benutzeroberflächen zu erzeugen. Sie können darin Animationen erzeugen, über Data Binding die UI-Elemente an Daten binden oder über sogenannte Stile die Benutzeroberfläche anpassen. Als Programmiersprachen werden die .NET-Sprachen unterstützt, wobei in diesem Buch durchgängig C# verwendet wird.
Vorwort
Als Voraussetzung sollte der Leser gute Kenntnisse in C# und der Bedienung des Visual Studios mitbringen. Für einige Kapitel sind auch XML- bzw. XPath-Kenntnisse notwendig bzw. hilfreich. Für die Programmierung mit 2D- und 3D-Grafik sollte Ihr Mathematikwissen noch nicht vollständig eingerostet sein. Sie werden hier zwar nicht völlig im Stich gelassen, allerdings wird auf eine umfangreiche Erläuterung der Grundlagen verzichtet, um den Vorgehensweisen in der WPF mehr Raum zu lassen. Der Sprachstil des Buches ist so gewählt, dass Sie schnell einen Überblick zum aktuellen Thema gewinnen, Zusatzinformationen und Tipps an passenden Stellen erhalten und das Ganze mit vielen Beispielen untermauert wird. Dabei wird versucht, schnell zum Punkt zu kommen und Überflüssiges wegzulassen. Jedes Kapitel besteht aus einem einführenden Teil, dem dann unterschiedliche fortschrittlichere Bereiche folgen. Sollten Sie diese nicht sofort verstehen, lesen Sie erst einmal weiter und kehren später noch einmal dorthin zurück. Ich wollte in jedem Fall vermeiden, dass sich ein Thema über zu viele Stellen verteilt erstreckt. Ab und zu ließ es sich nicht vermeiden, Dinge einzusetzen, die zu diesem Zeitpunkt noch nicht besprochen wurden. Dazu sind einige Techniken in der WPF einfach zu sehr miteinander verwebt. Es wurde dabei aber immer darauf geachtet, dass das eigentliche Thema nicht untergeht. Auf der CD finden Sie neben dem .NET Framework 3.0 Redistributable Package noch ein paar Tools und natürliche alle Beispiele aus dem Buch. Auf der Webseite http:// www.gowinfx.de/wpf-buch-1/ werden weitere Informationen zum Buch, nützliche Links und die Errata bereitgestellt, sofern notwendig bzw. verfügbar. Konstruktives Feedback ist jederzeit erwünscht. Senden Sie es bitte an die Adresse [email protected].
Danksagungen Auch an diesem Buch haben wieder einige Personen geholfen, es Wirklichkeit werden zu lassen und es zu verbessern, sei es im Inhalt, bei der Wahl der Formulierungen oder beim Druck. Mein Dank geht darum an den Verlag Addison-Wesley und meine Lektorin Frau Hasselbach für ihre Unterstützung von der Idee bis zur Abgabe. Der Fachlektor Rouven Haban sorgte mit seinen Hinweisen für die Beseitigung der (hoffentlich) letzten Unstimmigkeiten. Nicht zuletzt geht mein Dank an meine Frau, die wieder einmal viel Geduld aufbrachte, da so manches Wochenende daran glauben musste. Natürlich bin ich auch meinem Zwergkaninchen zu Dank verpflichtet, das für einige meiner im Buch verwendeten Bilder Modell stand. Ich wünsche nun den Lesern viel Spaß und viel Erfolg beim Durcharbeiten des Buches. Ich hoffe, Sie lernen die WPF dadurch so gut kennen, dass Ihre nächsten Projekte ein voller Erfolg werden. Dirk Frischalowski
14
1
Das .NET Framework 3.0
1.1 Einführung Ende 2006 war es so weit. Das vormals mit WinFX bezeichnete und als Aufsatz zum .NET Framework 2.0 propagierte .NET Framework 3.0 wurde von Microsoft freigegeben. Vormals nur für die Verwendung unter Windows Vista gedacht, ist es nun auch für Windows XP und Windows 2003 verfügbar. Bis zum Mai 2006 firmierte das Framework noch unter dem Namen WinFX, was häufig als Windows Framework Extensions (WinFX) bezeichnet wurde. Andere Quellen sprechen davon, dass WinFX auch als »Win Effects« ausgesprochen werden sollte, aber das sind alles alte Hüte. Im Mai 2006 entschied man sich für einen neuen Namen und auch für eine andere Vermarktung. Das .NET Framework 3.0, der neue Name für WinFX, sollte den Fortschritt des .NET Frameworks nicht nur inhaltlich, sondern auch in der Versionsnummer dokumentieren. Ein paar Konsequenzen hat diese Umbenennung allerdings schon. Während WinFX noch als Aufsatz zum bisherigen .NET Framework fungierte, ist das .NET Framework 3.0 eine echte neue und vollständige Version. Sie enthält deshalb auch das .NET Framework 2.0, das die Basis für das neue Framework ist. .NET 3.0 ist aber nur für Windows Vista, Windows 2003 und Windows XP verfügbar. Windows 2000 und ältere Windows-Versionen werden nicht mehr unterstützt. Damit dürfte für die Zukunft das .NET Framework 2.0 das letzte sein, das noch mit den
Kapitel 1
älteren Windows-Versionen eingesetzt werden kann. Sie müssen dies zumindest für Ihre Anwendungsentwicklung im Auge behalten.
Früher Testbeginn Microsoft begann schon recht früh, Vorabversionen des .NET 3.0 Frameworks einer größeren Entwicklergemeinde zur Verfügung zu stellen. Diese Vorabversionen wurden auch als CTP (Community Technology Preview) bezeichnet. Dies führte dazu, dass man sich schon sehr früh mit den Fähigkeiten des nächsten Meilensteins im .NET Framework vertraut machen konnte, und es begannen bereits recht früh einige Firmen mit der Entwicklung erster Software, die auf dem .NET Framework 3.0 basierte. Deshalb wurde die Verwendung von .NET 3.0-Anwendungen in Live-Systemen von Microsoft frühzeitig über sogenannte Go-Live-Lizenzen erlaubt. Diese standen anfangs nur für die WCF (Windows Communication Foundation) und WF (Windows Workflow Foundation) zur Verfügung. Mittlerweile sind sie natürlich überflüssig geworden. Für zukünftige Frameworks kann dies allerdings wieder interessant werden. Die Homepage für die .NET 3.0-Entwicklung ist momentan unter http://msdn2. microsoft.com/en-us/netframework/default.aspx zu finden. Dort finden Sie unter verschiedenen Kategorien Wissenswertes über die Bestandteile von .NET 3.0. Die Säulen des .NET Frameworks 3.0 stellen die folgenden vier Bestandteile dar, wobei in einigen Fällen Windows Cardspace nicht immer dazu gezählt wird: Windows Presentation Foundation (WPF) Windows Communication Foundation (WCF) Windows Workflow Foundation (WF) Windows CardSpace
Windows
Windows
Windows
Presentation
Communication
Workflow
Windows
Foundation
Foundation
Foundation
Cardspace
(WPF)
(WCF)
(WF)
.NET Framework 2.0 Abbildung 1.1: Säulen des .NET Frameworks 3.0
16
Das .NET Framework 3.0
> >
>
HINWEIS
Das .NET Framework 3.0 beinhaltet nur die in der Abbildung gezeigten neuen APIs. Spracherweiterungen wurden in dieser Version nicht vorgenommen. Dies ist erst der nächsten Version vorbehalten, die noch dieses Jahr (2007) erscheinen soll.
1.1.1
Windows Presentation Foundation
Die Windows Presentation Foundation, kurz WPF, war früher unter dem Codenamen Avalon bekannt. Sie stellt den grafischen Aspekt bei der Entwicklung von .NET 3.0Anwendungen dar und ist das Hauptthema dieses Buches. Sie enthält unter anderem Klassen für die Darstellung von 2D- und 3D-Grafiken, Animationen, der Definition von Stilen (z. B. für Komponenten) und vieles mehr. Eine Besonderheit ist die neue deskriptive Sprache XAML (eXtensible Application Markup Language), über die neben C#- oder Visual Basic-Code auch mittels einer XML-Syntax eine Oberfläche beschrieben werden kann. Dies hat insbesondere Vorteile für die Trennung von Logik und Darstellung in einer Anwendung. Während Grafikdesigner sich um die Aspekte der Benutzeroberfläche kümmern können, hat die WPF für den Entwickler mehrere Vorteile, beispielsweise: Die WPF ist ein leistungsfähiges Framework für anspruchsvollste Benutzerschnittstellen. Es kann eine striktere Trennung der Entwicklungsaufgaben zwischen Programmierern und Designern erfolgen, der auch über separate Tools Genüge getan wird. Die WPF stellt die Möglichkeit zur Entwicklung von allein ablauffähigen WindowsAnwendungen und Anwendungen, die im Browser ausgeführt werden bereit, wobei beide auf der gleichen Codebasis aufsetzen.
1.1.2 Windows Communication Foundation Für die Kommunikation zwischen mehreren Anwendungen, Prozessen und/oder Rechnern bietet die Windows Communication Foundation, kurz WCF, das notwendige Rüstzeug. Vorher war das Framework auch unter dem Namen Indigo bekannt. Die WCF setzt dabei auf vorhandenen Technologien wie ASP.NET Web Services, MSMQ, .NET Remoting oder Enterprise Services auf. Sie vereinfacht deren Nutzung und erlaubt, die besten bzw. die geeignetsten Technologien für eine Problemstellung zu nutzen, da Sie nun nicht mehr an eine einzige Technologie gebunden sind. Weiterhin vereinfacht sie die Nutzung der vorhandenen Technologien durch ein einheitliches Framework. Folgende Dienste können Sie z. B. verwenden: Gesicherte Nachrichtenversendung über Message Queuing Transaktionsunterstützung
1.1.3 Windows Workflow Foundation In einem Unternehmen sind optimale Workflows im Arbeitsprozess die Basis für den Erfolg. Müssen Workflows über Programme nachgebildet werden, stellen sich verschiedene Probleme. Was passiert, wenn ein Teilnehmer eines Workflows nicht verfügbar ist? Wie wartet eine Anwendung auf eine Nachricht? Wie werden parallele Arbeitsschritte wieder zusammengeführt? All dies und mehr wird durch ein neues Framework vereinfacht, die Windows Workflow Foundation, kurz WF (nicht WWF, um Verwechslungen mit anderen Abkürzungen aus dem Wege zu gehen). Im Gegensatz zur WPF und WCF war die WF nie unter einem anderen Codenamen populär. Basis der WF sind eine Workflow Engine sowie ein API. Die Bestandteile eines Workflows werden durch Aktivitäten bereitgestellt, wobei auch eigene Aktivitäten erstellt werden können. Eine Aktivität könnte z. B. der Aufruf eines Web Services sein, eine andere der Versand einer E-Mail. Es werden zwei grundsätzliche Arten von Workflows unterstützt. Zustandsbehaftete Workflows wechseln durch äußere Einflüsse von einem Zustand in einen anderen. So kann ein Workflow z. B. auf die Bestätigungs-E-Mail eines verantwortlichen Mitarbeiters warten, um eine Bestellung auszulösen. Sequenzielle Workflows arbeiten einen Workflow ähnlich wie eine »normalen« C#-Anwendung ab, wobei hier aber auch äußere Einflüsse steuernd einwirken können. Microsoft verwendet in den eigenen Produkten wie Office 2007, BizTalk Server oder SharePoint ebenfalls die WF. Außerdem lassen sich diese Produkte in eigene Workflows einbinden.
> >
>
HINWEIS
Wie schon die vorigen Versionen des .NET Frameworks ist auch die neue Version kostenfrei zu verwenden, was bei der Leistungsfähigkeit der einzelnen Bestandteile nicht unbedingt selbstverständlich ist.
1.1.4 Weitere Technologien und Tools Die folgenden Tools gehören entweder mit zum .NET Framework, erleichtern die Arbeit damit oder waren einmal als Bestandteil des Frameworks im Gespräch.
18
Das .NET Framework 3.0
Framework/Technologie
Beschreibung
Windows Cardspace
Zur Autorisierung (z. B. durch Web-Formulare) bietet Windows Cardspace (früher Infocard) die einfache Verwaltung und Übertragung der benötigten Daten an. Dabei wird nur die Bereitstellung und Selektion der Daten durch Cardspace erledigt. Die Autorisierung selbst wird durch bereits vorhandene Implementierungen vorgenommen.
WinFS
Das neue Windows Filesystem sollte ein datenbankbasiertes Dateisystem werden, um die Verwaltung und Suche nach Dateien sowie deren Inhalten zu verbessern. Der geplante Einsatz in Windows Vista ist nicht erfolgt, und nach dem aktuellen Stand wird es WinFS, wie es einmal geplant war, nicht mehr geben.
Visual Studio 2005
Obwohl nicht dafür vorgesehen, kann das Visual Studio 2005 mithilfe der Visual Studio Extensions zur Entwicklung von Anwendungen für das .NET Framework 3.0 verwendet werden.
Expression Blend
Dieses Tools ist Bestandteil von Microsoft Expression, einer Tool Suite bestehend aus den vier Produkten Expression Web für die Entwicklung von Web-Seiten, Expression Design für die Erstellung von Grafiken, Expression Media zur Verwaltung digitaler Medien wie Bildern und Videos sowie Expression Blend zur Entwicklung von Benutzeroberflächen von .NET-Anwendungen. Alle Tools basieren auf .NET 3.0 und nutzen z. B. die Vorteile der WPF.
Tabelle 1.1: Übersicht weiterer Technologien
> >
>
HINWEIS
Die aktuelle deutsche Seite zu Microsoft Expression finden Sie unter http://www.microsoft.com/ products/expression/de/default.mspx.
1.2 Installation Die Installation von .NET 3.0 unterscheidet sich nicht grundlegend von .NET 2.0. Für die Entwicklung von .NET 3.0-Anwendungen ist allerdings etwas mehr Aufwand zu treiben. Die Vorgehensweise der Installation soll jetzt kurz beschrieben werden. Das .NET Framework 3.0 ist bereits in Windows Vista vorinstalliert, sodass sie hier lediglich die Tools zur Anwendungsentwicklung installieren müssen. Unter Windows XP und Windows 2003 muss vorher noch das Framework installiert werden. Grundsätzlich würde dies auch ausreichen, allerdings stehen dann keine geeigneten Entwicklungstools und keine Dokumentation zur Verfügung. Als Entwicklungstool kann momentan das Visual Studio 2005 eingesetzt werden, bis die neue Version (Codename Orcas) verfügbar wird. Dies könnte Ende 2007 der Fall sein, sodass sich der Name Visual Studio 2007 anbietet.
19
Kapitel 1
Die Installation als Entwickler von Anwendungen für .NET 3.0 erfolgt in mehreren Schritten. Voraussetzung für eine sinnvolle Arbeit mit .NET 3.0 und hier im Speziellen mit der WPF ist die Installation des Visual Studios 2005, momentan mindestens in der Standard Edition. Die Express Edition und deutschen Versionen des Visual Studios werden zwar nicht offiziell unterstützt, allerdings gab es in vielen Fällen damit keine Probleme. Die Verwendung erfolgt auf Ihr Risiko.
> >
>
HINWEIS
Aus dem eben genannten Grund wird in diesem Buch die englische Version des Visual Studios 2005 unter Windows XP verwendet. Damit gab es während der gesamten Entstehungsphase des Buches keine Probleme.
1.3 Installationsdateien beschaffen Für eine Installation der .NET 3.0-Komponenten benötigen Sie in jedem Fall einen schnellen Internetzugang. Ansonsten laden Sie wahrscheinlich immer noch Dateien herunter, wenn bereits das Framework durch das nächste abgelöst wird. Startpunkte für die .NET 3.0-Entwicklung sind die Web-Seiten http://msdn2.microsoft.com/en-us/netframework/default.aspx http://msdn2.microsoft.com/en-us/windowsvista/aa904955.aspx http://www.netfx3.com/ Die erste Seite ist die Startseite in die .NET 3.0-Welt. Über die zweite URL erhalten Sie alle Links zur benötigten Software zur Entwicklung von .NET 3.0-Anwendungen unter Windows Vista. Die letzte URL gehört zur .NET Framework 3.0 Community.
> >
>
HINWEIS
Die angegebenen URLs waren ca. Ende Januar 2007 aktuell. Da sich doch ab und zu Änderungen ergeben, ist die einfache Suche über Google die sicherste Lösung.
Voraussetzungen Basis für die Anwendungsentwicklung mit .NET 3.0 ist momentan die Installation des Visual Studios 2005. Dies schließt automatisch das ebenfalls benötigte .NET Framework 2.0 ein. Da das .NET Framework 2.0 auch Bestandteil von .NET 3.0 ist, spielt die Reihenfolge Visual Studio 2005/.NET 3.0 Redistributable Package keine Rolle. Als Systemvoraussetzungen werden entweder Windows 2003 (mit seinen verschiedenen Server-Versionen) und aktuellem SP (Service Pack), Windows XP mit SP2 oder Windows Vista benötigt. Wenn Sie bereits eine ältere Version irgendeiner der folgen-
20
Das .NET Framework 3.0
den Komponenten installiert haben, sollte diese vorher vollständig entfernt werden. Als Sprache sollten Sie momentan nur den englischen Versionen vertrauen. Aktuell gibt es lediglich eine deutsche Version der WF, die ich allerdings nicht mit den englischen Versionen mischen würde. Damit ergibt sich ungefähr folgende Installationsliste: Windows Vista oder eine andere Windows-Version laut den Systemvoraussetzungen Visual Studio 2005 (ab Standard – für alle Features) .NET 3.0 Redistributable Package (enthält aber auch die .NET 2.0 Runtime) Windows SDK (schließt das .NET 3.0 Framework SDK ein) Visual Studio 2005 – Erweiterungen für Orcas Optional die Windows Workflow Foundation-Erweiterungen für das Visual Studio 2005 Optional MS Office 2007
> >
>
HINWEIS
Auf unterschiedlichen Veranstaltungen wurden auch die unterschiedlichsten Varianten der Software genutzt. So kamen auch Express Editions in Deutsch und Englisch sowie gemischte englische und deutsche Versionen der Visual Studio Extensions zur WF zum Einsatz. In den meisten Fällen gab es damit keine Probleme, allerdings waren auch nicht alle vom Erfolg gekrönt. Wenn Sie beispielsweise Visual C# Express nutzen, dann können Sie z. B. keine WCF-Anwendungen erstellen, da diese Web-Features benötigen. Diese würden Sie z. B. in der Visual Web Developer Express Edition vorfinden.
> >
>
HINWEIS
Für das Visual Studio 2005 wird es ab sofort keine Extensions mehr für das .NET Framework 3.0 geben. Microsoft stellt wahrscheinlich Vorabversionen des nächsten Visual Studios 2007 zur Verfügung, die dann bereits über diese Erweiterungen verfügen.
Installation des .NET 3.0 Redistributable Package Der erste Schritt ist die Installation des .NET Framework 3.0 Redistributable Package. Dieses liegt in Form der Datei dotnetfx3.exe vor, die ca. 50 MB groß ist. Die Installation ist unspektakulär, da es nach dem Bestätigen der Lizenzbedingungen sofort losgeht. Statt eines Dialogs wird während der Installation ein Symbol im rechten Infobereich der Taskleiste angezeigt. Über das Kontextmenü des Symbols können Sie wieder das Dialogfenster zur Anzeige der Fortschrittsanzeige öffnen. Dies ist spätestens zum Ende
21
Kapitel 1
der Installation notwendig, um diese abzuschließen. Eine »Neuerung« gab es dann doch noch. Erstmals musste der Computer neu gestartet werden. Dies war bei früheren Installationen nicht notwendig. Es steht außerdem auch eine Installationsvariante über das Web zur Verfügung. In diesem Fall laden Sie nur eine ca. 3 MB große Datei herunter, die den Installationsvorgang startet und nur die benötigten Dateien über das Web lädt. Möchten Sie mehrere Installationen durchführen, ist diese Variante sicher nicht geeignet. Auf den Download-Seiten finden Sie meist etwas weiter unten einen weiteren Link für die komplette Installation (z. B. X86 REDIST PACKAGE im Falle des .NET Framework 3.0 Redistributable Package). Jetzt können Sie alle anderen Komponenten installieren, die auf dieser Runtime basieren. Neben den Entwicklungstools können dies auch die Produkte von Microsoft Expression sein, die Sie z. B. unter http://www.microsoft.com/products/expression/en/ default.mspx oder für die deutsche Version unter http://www.microsoft.com/products/ expression/de/default.mspx finden. Diese Tools sind z. B. nützlich, um Oberflächen für Anwendungen zu erstellen, die dann von den Entwicklern in die Anwendung integriert werden können. Die Runtime von .NET 3.0 vereinigt vier Komponenten. Sie bietet die Ausführungsschicht für Windows Cardspace-, WPF-, WCF- und WF-Anwendungen. Möchten Sie die sprachabhängigen Teile der Runtime in einer anderen Sprache anzeigen, können Sie Language Packs für Deutsch und Japanisch laden.
Installation des Windows SDK Das Windows SDK umfasst neben dem »eigentlichen« SDK auch das .NET Framework 3.0 SDK. Es enthält die Dokumentation, Beispiele und Tools. Die Größe beträgt momentan knapp 1.2 GB, womit es das gewichtigste Teil ist. Der Download ist über ein ISO-Image oder als Web-Download möglich. Da ich davon ausgehe, das SDK auf mehreren Entwicklungs- und Testrechnern zu installieren, entscheide ich mich in der Regel für den kompletten Download. Ein ISO-Image ist dabei ein komplettes Abbild einer CD/DVD und kann von aktuellen Brennprogrammen direkt als Eingabe zum Schreiben einer CD/DVD verwendet werden. Um das Brennen zu vermeiden, können ISO-Images auch wie ein Verzeichnis gemounted (eingebunden) werden. Dazu stehen verschiedene Tools zur Verfügung. Entweder Sie verwenden das von Microsoft empfohlene Tool Virtual CD-ROM Control Panel for Windows XP Tool unter der URL http://download.microsoft.com/download/7/b/6/7b6abd84-78414978-96f5-bd58df02efa2/winxpvirtualcdcontrolpanel_21.exe, oder Sie nutzen die DAEMON Tools unter http://www.daemon-tools.cc/dtcc/download.php?mode=ViewCategory&catid=5. Beide sind virtuelle CD-/DVD-ROM-Emulatoren, d.h., sie stellen ein ISO-Image wie ein CD-/DVD-Laufwerk bereit.
22
Das .NET Framework 3.0
Nachdem Sie also auf die Dateien des ISO-Images zugreifen können, steht eine Datei setup.exe im Wurzelverzeichnis zur Verfügung. Starten Sie die Datei setup.exe, um die Installation des Windows SDK zu starten. Klicken Sie bei Erscheinen des Start-Dialogs und der Lizenzbedingungen auf NEXT. Je nach Vorliebe können Sie einzelne Optionen deaktivieren, die Sie vermutlich nicht benötigen. Da es hier aber auch nicht mehr auf 100–200 MB ankommt, würde ich dies aus »Sicherheitsgründen« besser vermeiden. Noch zwei Klicks auf NEXT trennen Sie vom Beginn der Installation. Standardmäßig erfolgt die Installation in das Verzeichnis [LW]:\Programme\Microsoft SDKs. Darin finden Sie ein Unterverzeichnis ...\Windows\v6.0. Darunter befinden sich sämtliche Bestandteile des SDK. Als erste Tat nach der Installation des SDK sollten Sie die mitgelieferten Beispiele im Unterverzeichnis ..\Samples auspacken. Diese werden über fünf Zip-Dateien bereitgestellt und sollten in separate Unterordner ausgepackt werden, da sie kein eigenes Wurzelverzeichnis enthalten.
Installation der Visual Studio Extensions Spätestens bei der Installation dieser Erweiterungen zum Visual Studio 2005 rächt es sich eventuell, wenn Sie nicht die richtigen Sprachversionen eingesetzt haben. Deutsche und englische Versionen, schon beginnend beim Betriebssystem, vertragen sich hier eventuell nicht mehr. Dies zeigt sich z. B. darin, dass im Visual Studio englische und deutsche Bezeichner vermischt werden (z. B. direkt im Hautmenü) und dass Projektvorlagen nicht mehr verfügbar sind. Die Installationsdatei für die Extensions heißt vsextwfx.exe und ist momentan ca. 4 MB groß und damit ein Winzling im Vergleich mit den bisherigen Dateien. Auch wenn die Dateigröße relativ klein ist, dauert die Installation ungewöhnlich lange. Einzustellen gibt es während der Installation nichts weiter. Als Ergebnis erhalten Sie im Visual Studio: IntelliSense-Unterstützung für .NET 3.0 und XAML Projektvorlagen für WPF- und WCF-Anwendungen die Integration der Dokumentation in die Visual Studio-Hilfe die Bereitstellung eines grafischen Designers für die WPF (WPF Designer for Visual Studio – früher Cider), allerdings momentan mit recht limitierten Design-Möglichkeiten (und eventuell nicht in den Express Editions verfügbar)
23
Kapitel 1
1.4 Weitergabe von .NET 3.0-Anwendungen Haben Sie eine .NET 3.0-Anwendung erstellt, möchten Sie diese vielleicht auch vertreiben. Dazu muss das .NET Framework 3.0 Redistributable Package installiert werden. Da das .NET Framework 3.0 auf dem .NET Framework 2.0 basiert, müssen Sie das .NET Framework 2.0 natürlich auch bereitstellen. Das .NET Framework 2.0 ist allerdings im Redistributable Package des .NET Frameworks 3.0 enthalten (deshalb auch die Größe von mehr als 50 MB), sodass Sie beide Frameworks in einem Rutsch installieren können. Die .NET 3.0-Runtime ist unter Windows Vista bereits vorinstalliert. Auf älteren Systemen wie Windows XP muss sie allerdings nachträglich bereitgestellt werden. Mittlerweile werden 64-Bit-Systeme immer beliebter, sodass es die Runtime einmal für 32-Bitund einmal für 64-Bit-Systeme gibt. Für die Bereitstellung ist es wichtig, dass der Benutzer über Administratorrechte verfügt. Die von Microsoft geforderten Hardwareeigenschaften sind sehr bescheiden ausgelegt. So wird ein 1-GHz-Prozessor mit 256 MB RAM empfohlen. Wenn Sie beides verdoppeln und noch eine aktuelle Grafikkarte für 50 Euro hinzuzählen, sollte es für eine vernünftige Ausführung tatsächlich ausreichen. Wie auch das .NET Framework ist die Weitergabe der Runtime kostenfrei. Es sind lediglich einmal von Ihnen die Lizenzbestimmungen für die Weitergabe zu akzeptieren.
1.5 Welchen Nutzen bringt die WPF? Nachdem Sie nun etwas über das .NET Framework 3.0, seine Bestandteile und dessen Installation gelesen haben, noch ein paar Worte zur WPF. Wenn Sie beginnen, mit der WPF zu arbeiten, werden Sie feststellen, dass sich viele Dinge sehr einfach realisieren lassen, an deren Implementierung Sie früher nicht einmal im Traum gedacht hätten. Um dreidimensionale Objekte zu bewegen und auf deren Oberfläche ein Video abzuspielen sind nur relativ wenige Handgriffe notwendig. Animationen oder die zentrale Konfiguration der Darstellungsweise aller Komponenten sind Dinge, die früher entweder gar nicht oder nur mit viel Aufwand zu erledigen waren. Nach einiger Zeit werden Sie dann aber feststellen, dass der Nachbau einer existierenden Windows Forms-Anwendung nicht problemlos vonstatten geht. So besitzen viele Komponenten in der WPF keine Eigenschaften mehr, um zusätzlich eine Grafik anzuzeigen. Ein Beispiel ist der Treeview. Stattdessen bietet Ihnen die WPF die Möglichkeit, so ziemlich alles in einem Treeview anzuzeigen, was allerdings anfangs mit mehr Aufwand verbunden ist.
24
Das .NET Framework 3.0
Damit sind wir beim entscheidenden Vorteil der WPF gegenüber den traditionellen Frameworks. Jeder Entwickler erhält die Möglichkeit, mit minimalem Aufwand (rechnen Sie hier aber bitte nicht in Tagen, sondern in Wochen und Monaten) eine sehr anspruchsvolle Benutzeroberfläche zu schaffen – und wie man so schön sagt: Das Auge isst mit. Um sich von den Mitbewerbern abzuheben, ist es oft nicht mehr notwendig, mehr in die Anwendung zu packen, sondern das Vorhandene einfach attraktiver darzubieten – man denke an Tools mit Hunderten von Einstellmöglichkeiten. Statt weniger anzubieten und dies dann optimal zu gestalten, legen viele Wert auf Masse. Mit der WPF zielen Sie eher in Richtung Bedienerfreundlichkeit und Attraktivität. Technologisch wurden ebenfalls erstmals neue Wege gegangen. Es wurden nicht mehr die Relikte zehn Jahre alter Windows-APIs mitgeschleift, sondern viele Funktionen neu verpackt. So werden ähnlich dem Swing-Framework in Java Komponenten in der WPF nicht mehr über das Betriebssystem gezeichnet, sondern über die WPF. Wichtige Änderungen sind unter anderem: In Win32-Anwendungen konnten Sie bereits über Ressourcendateien Teile der Oberfläche wie Dialoge oder Menüs beschreiben. Diese wurden dann in die Anwendung eingebunden und darin angesprochen. Über separate Tools konnten diese Ressourcen sogar direkt in einer Anwendung bearbeitet werden. Über diese Ressourcen konnten Sie allerdings nur eine geringe Menge an UI-Elementen definieren, und die Konfigurationsmöglichkeiten waren gering. Mit der Einführung von XAML (Extensible Application Markup Language, sprich Semmel) in der WPF kommen die Ressourcen wieder zu neuen Ehren. Die Vielfalt, die XAML allerdings bietet, übertrifft Ressourcen um Welten. Der Vorteil von XAML ist die Trennung der Benutzeroberfläche von der dahinter liegenden Programmlogik. Im Idealfall kann sich ein Grafikdesigner um die Erstellung der Oberfläche kümmern, während der Programmierer die notwendige Programmlogik an diese anbindet. Gleich hier sei angemerkt, dass Sie XAML nicht zwingend benötigen. Die Benutzeroberfläche lässt sich wie bisher in .NET üblich über die von Ihnen verwendete Programmiersprache (C# oder Visual Basic) erzeugen. UI-Komponenten werden nicht mehr durch das Betriebssystem, sondern durch die WPF gezeichnet. Dadurch fallen sämtliche Limitierungen weg, die früher z. B. das Erstellen farbiger Schaltflächen oder Treeviews, die nur aus Bildern bestanden, erschwerten. Weiterhin kann so die Grafikausgabe effizienter durch die WPF gesteuert werden. Wenn Sie z. B. eine eigene Komponente entworfen haben und diese selbst zeichnen, verwaltet die WPF die Zeichnungselemente in einem Elementbaum. Ändern Sie nur die Farbe einer Linie, kann die Darstellung sehr effizient aktualisiert werden. In Win32- und .NET-Anwendungen werden letztendlich die Komponenten vom Betriebssystem dargestellt. In der WPF geschieht dies allerdings durch die WPF.
25
Kapitel 1
Dabei erlaubt die WPF, dass Sie innerhalb einer Komponente eine oder mehrere andere Komponenten verschachteln können. So können Sie innerhalb eines Buttons problemlos eine Grafik und eine TextBox darstellen. In einem Treeview oder einer ListBox lässt sich z. B. jede UI-Komponente als Element unterbringen. WPF-Anwendungen sind völlig unabhängig von der Auflösung des Endgeräts. Entwickeln Sie eine Anwendung bei einer Auflösung von 96 dpi, so sieht diese bei 72 dpi genauso aus wie bei 300 dpi. Insbesondere bleiben die Größen erhalten. Dies wird durch die Tatsache möglich, da die Grafikausgaben auf Vektorbasis erfolgen. Außerdem wird dadurch ein verlustfreies Zoomen möglich.
WPF im Vergleich zu Windows Forms Wenn ein neues Framework zur Erstellung von Benutzeroberflächen verfügbar ist, stellt sich natürlich die Frage, was mit den bisher verwendetem Windows Forms geschieht. Wann sollte man Windows Forms noch einsetzen, wann die WPF? Betrachtet man die Entwicklung von .NET, so erinnern sich vielleicht noch einige, wie es mit dem Erscheinen von .NET 1.0 war. Es fehlten einfach Dinge, an die man sich bereits gewöhnt hat. Die Auswahl an Komponenten war gering, deren Fähigkeiten gegenüber den bekannten Komponenten aus Visual Basic oder Delphi eher bescheiden. Mittlerweile wird sich darüber keiner mehr Gedanken machen, da sich das .NET Framework bereits deutlich weiterentwickelt hat. Mit der WPF ist es ähnlich. In der vorliegenden ersten Version haben Sie zwar durch die beliebige Verschachtelung von Komponenten alle Fäden in der Hand, allerdings ist das zum Teil mit einem hohen Aufwand verbunden. Auch die grafische Ausrichtung der Komponenten oder der Beschriftungen ist manuell durchzuführen, dabei wurde gerade in diesem Bereich im Visual Studio 2005 so viel getan. Das führt zu der Empfehlung, Benutzeroberflächen für Standardanwendungen und Anwendungen, bei denen die Benutzer schnell die vertrauten Elemente wieder finden möchten, mit Windows Forms zu entwickeln. Anwendungen, die jedoch eine anspruchsvolle Grafik benötigen, sollten die WPF verwenden. Meine persönliche Meinung ist, dass es einen längeren Umstieg von Windows Forms zur WPF geben wird, genau wie es momentan noch beim Umstieg von Win32- hin zu .NET-Anwendungen der Fall ist.
Voraussetzungen im Buch Das Buch wurde mit dem englischen Visual Studio 2005 Professional und der endgültigen Version des .NET Frameworks 3.0 geschrieben. Als Betriebssystem liegt Windows XP SP 2 zugrunde. Obwohl dies in den seltensten Fällen eine Rolle spielen wird, soll es zumindest als Hinweis dienen, wenn einmal etwas nicht wie erwartet funktioniert.
26
Das .NET Framework 3.0
Meine eigenen Erfahrungen waren seit dem ersten CTP vom November 2004, das ich verwendet habe, sehr positiv. Dennoch gab es zwei oder drei Problemchen, bei denen sich Komponenten nicht installieren ließen oder bei dem auf einem neu installiertem Rechner die Videos in der MediaElement-Komponente nicht korrekt abgespielt werden. Statt lange zu probieren oder zu verzweifeln, wurde meist eine Lösung schon nach wenigen Minuten durch eine Suche mit Google gefunden. Das Einzige, was Sie momentan noch nicht so ausgiebig finden, sind umfangreiche Beispiele und Tutorials, die im Internet frei verfügbar gemacht werden. Da halten sich die Autoren noch etwas rar. Das einst von mir begonnene Avalon-Tutorial (jetzt WPFTutorial) konnte ich aus Zeitgründen nicht fortführen. Mal sehen, ob ich nach dem Fertigstellen des Buches wieder dazu komme. Die Startseite finden Sie unter http:// www.gowinfx.de/. Die Beispiele zum Buch finden Sie alle auf der beiliegenden CD. Updates, sofern notwendig, werden unter http://www.gowinfx.de/wpf-buch-1/ bereitgestellt.
27
2
Das Programmiermodell der WPF
2.1 Einführung Dieses einleitende Kapitel erläutert die grundlegende Vorgehensweise bei der Anwendungsentwicklung mit der WPF. Es werden die verschiedenen Anwendungstypen, einige Änderungen gegenüber Windows Forms vorgestellt und Standardaufgaben rund um das Starten, Beenden und Konfigurieren einer Anwendung erläutert.
2.1.1 Namespaces Die wichtigsten Typen der WPF befinden sich in den Assemblies PresentationCore.dll, PresentationFramework.dll sowie WindowsBase.dll. Diese werden in der Regel automatisch (neben einigen weiteren Assemblies) beim Erstellen eines neuen WPF-Projekts im Visual Studio referenziert. Die WPF-spezifischen Namespaces finden Sie im Namespace System.Windows und seinen untergeordneten Namespaces. Achten Sie darauf, dass viele Typen ein Äquivalent im Namespace System.Windows.Forms besitzen, z. B. wenn Sie mit der MSDN-Hilfe arbeiten.
2.1.2 Anwendungstypen Bei den Anwendungstypen soll zwischen WPF-Anwendungstypen und den Typen, die beim Erstellen eines neuen Projekts im Visual Studio erzeugt werden können, unterschieden werden. Beide überschneiden sich aber etwas.
Kapitel 2
Die WPF stellt die folgenden drei Anwendungstypen bereit: Windows-Anwendungen – auch Windows Application (WPF) genannt, typische grafische Anwendungen XAML-Browseranwendungen – Anwendungen, die über Click Once installiert und im Browser ausgeführt werden Loose XAML – Anwendungen, die nur auf XAML-Code basieren Während die ersten beiden Varianten typische grafische Anwendungen sind, die auf MSIL-Code basieren, lässt sich Loose XAML direkt im Browser ausführen, ohne dass eine Übersetzung dazu notwendig wäre.
Übersicht der Anwendungstypen im Visual Studio Nach der Installation der Visual Studio Extensions des .NET Frameworks 3.0 in das Visual Studio 2005 stehen dort neue Projekttypen zur Verfügung, die Sie über FILE – NEW – PROJECT verwenden können.
Abbildung 2.1: WPF-Projekttypen im Visual Studio
30
Das Programmiermodell der WPF
Anwendungstyp
Beschreibung
Windows-Anwendung
Hierbei handelt es sich um eine typische Windows-basierte Anwendung, die allerdings auf den Komponenten und Möglichkeiten der WPF basiert.
Windows Application (WPF) Web-Browser-Anwendung XAML Browser Application (WPF) Steuerelementebibliothek Custom Control Library (WPF)
Hier handelt es sich um »echte« Anwendungen, die allerdings im Browser ausgeführt werden und mit Internet-Rechten ausgestattet sind (d.h., weniger Befugnisse besitzen). In dieser Bibliothek werden Steuerelemente speziell für WPF-Anwendungen hinterlegt.
Tabelle 2.1: Übersicht der Anwendungstypen im Visual Studio
2.2 Aufbau eines Projekts Für Liebhaber der Konsole und geduldige Entwickler bietet sich die Entwicklung von WPF-Anwendungen rein über die Tools des .NET Frameworks wie »MSBuild« sowie mit dem Windows SDK und den WPF-Komponenten an. Eine professionelle und schnellere Entwicklung ist dagegen nur mit einer Entwicklungsumgebung wie dem Visual Studio möglich. Aus diesem Grund wird das Visual Studio auch durchgängig in diesem Buch verwendet. Aber auch in diesem Fall ist es nützlich, den Grundaufbau eines Projekts zu kennen und zu wissen, welche Dinge automatisch an welcher Stelle generiert werden. Die verschiedenen Anwendungstypen wurden ja bereits vorgestellt. Allerdings gibt es unterschiedliche Varianten, eine solche Anwendung bzw. um überhaupt etwas zu erstellen, was irgendwie genutzt werden kann. Es sind folgende Vorgehensweisen möglich: Reine XAML-Anwendungen ohne Code, z. B. eine einzelne XAML-Datei (z. B. zum Download im Internet Explorer oder für einfache Beispielanwendungen) Reine Code-Anwendungen in C# oder Visual Basic Gemischte Anwendungen, die aus XAML- und C#- bzw. Visual Basic-Code bestehen (dies ist sicherlich die am meisten verwendete Variante)
!
!
!
ACHTUNG
Innerhalb dieser drei Anwendungstypen werden jetzt die Bestandteile eines WPF-Projekts vorgestellt. Lesen Sie deshalb auch alle drei Varianten einmal durch.
31
Kapitel 2
2.2.1 Reine Code-Anwendungen Auch dieser Anwendungstyp hat seine Berechtigung. Wenn Sie keine grafische Oberfläche benötigen (in einer WPF-Anwendung ist das natürlich irgendwie witzlos), diese dynamisch generieren müssen oder einfach auf XAML verzichten wollen, um nicht noch eine Sprache bzw. Syntax erlernen zu müssen, dann können Sie eine Anwendung auch rein im Code erzeugen. Sie haben im Code immer alle Ausdrucksmöglichkeiten zur Verfügung, während XAML einige Einschränkungen besitzt. Wie schon seit der ersten Version des .NET Frameworks üblich, wird dazu ein Application-Objekt benötigt, das aus dem Namespace System.Windows stammt. Statt eines Form- wird nun allerdings ein Window-Objekt erzeugt, um ein Fenster anzuzeigen. Der Rest sollte von Windows Forms-Anwendungen her bekannt sein. Neben den zum Teil anderen Klassen müssen Sie natürlich auch andere Namespaces verwenden. Obwohl diese Vorgehensweise relativ einfach und bekannt aussieht, soll hier gleich darauf hingewiesen werden, dass es bei der Verwendung und Konfiguration der WPFKomponenten etwas komplizierter wird, zumindest anfangs.
BEISPIEL AUF DER CD Erstellen Sie im Visual Studio ein Windows Application(WPF)-Projekt, und entfernen Sie die beiden Dateien App.xaml und Window1.xaml aus dem Projekt im Projektmappen-Explorer. Fügen Sie dem Projekt dann eine Klasse über den Kontextmenüpunkt ADD – NEW ITEM des Projekts hinzu. Jetzt kann es mit der Eingabe des Codes weitergehen. Zuerst einmal müssen die benötigten Namespaces eingebunden werden. Der Namespace System. Windows stellt die Window- sowie die Application-Klasse bereit. Um den Hintergrund des Fensters einzufärben, wird ein Brush (Pinsel) benötigt. Diese Klasse befindet sich im Namespace System. Windows.Media. In der auch in WPF-Anwendungen benötigten Main()-Methode (wenn es auch Varianten gibt, in der Main() nicht explizit angegeben werden muss, z. B. in Anwendungen, die XAML nutzen) wird ein Application-Objekt erstellt und mit dem Aufruf von Run() die Verarbeitung der Nachrichtenschleife gestartet (über welche die Anwendung Nachrichten verarbeitet wie das Klicken einer Schaltfläche oder die Eingabe von Text in ein Textfeld). In der Methode Run() wird dann eine Instanz des Hauptformulars übergeben. Am Ende der Verarbeitung in Run() wird das Ereignis Startup ausgelöst, in dem Sie Ihren Initialisierungscode für die gesamte Anwendung unterbringen können. Vor der Methode Main() muss über das Attribut STAThread das Single-Thread-Modell aktiviert werden. Lassen Sie diese Angabe weg, kommt es relativ zügig zu einer Exception, da dies für die korrekte Verwendung einiger UI-Komponenten zwingend notwendig ist (z. B. bei der Kommunikation mit der Zwischenablage oder den Systemdialogen, mit denen über COM Interop kommuniziert wird).
32
Das Programmiermodell der WPF
Auch die Ereignisbehandlung funktioniert wie unter .NET 2.0. Lediglich die Argumente der Ereignishandler sind etwas anders, dies wird aber später erläutert.
Abbildung 2.2: Das Ergebnis der Anwendung using System.Windows; using System.Windows.Media; namespace MinimalCode
{ class FrmMain: Window
{ public FrmMain()
{ Title = "Hallo von der WPF"; Width = 230; Height = 100; Background = Brushes.AliceBlue; StackPanel sp = new StackPanel(); this.Content = sp; Button btn = new Button(); btn.Content = "Klick mich"; btn.Click += OnClick; sp.Children.Add(btn); } private void OnClick(object sender, RoutedEventArgs e)
{ MessageBox.Show("Funzt"); } [System.STAThread()] public static void Main() { new Application().Run(new FrmMain()); } } } Listing 2.1: Beispiele\Kap02\MinimalCodeProj\FrmMain.cs
33
Kapitel 2
2.2.2 Reine XAML-Anwendungen Um eine reine XAML-Anwendung zu erstellen, können Sie wieder eine WindowsAnwendung für die WPF erzeugen und löschen aus dieser die Code-Behind-Dateien App.xaml.cs und Window1.xaml.cs für die Dateien App.xaml und Window1.xaml. Da diese Dateien keine Anwendungslogik enthalten, ist das Löschen völlig unproblematisch.
BEISPIEL AUF DER CD Die letzte Anwendung wird nun nur noch mithilfe von XAML erzeugt. Die »Hauptanwendung« besteht aus dem XML-Element Application, das später ein Application-Objekt erzeugt. Das Fenster wird über ein Window-Element beschrieben, in dem mehrere andere Elemente verschachtelt sind, unter anderem der angezeigte Button. Alle Eigenschaften, die Sie vorher im Code gesetzt haben, werden jetzt über Attribute in der XAML-Datei zugewiesen. Zugehörigkeit wird dabei durch Verschachtelung erreicht. Listing 2.2: Beispiele\Kap02\MinimalXamlProj\App.xaml
2.2.3 Gemischte XAML-Code-Anwendungen In gemischten Anwendungen, die XAML-Dateien und dazugehörige Code-Dateien, sogenannte Code-Behind-Dateien, enthalten, besteht in der Regel eine strikte Trennung zwischen Anwendungslogik und Darstellung. Dieses Vorgehen sollte auch die Standardvorgehensweise für Ihre WPF-Anwendungen sein. Während in der XAMLDatei die Oberfläche (ohne jeglichen Code) beschrieben wird, wird in der CodeBehind-Datei (dahinter liegende Code-Datei) die Anwendungslogik programmiert. Die Verknüpfung zwischen beiden Dateien besteht in der Verwendung von partiellen Klassen, wie sie bereits unter .NET 2.0 eingeführt wurden. Eine Standard-WPF-Windows-Anwendung besteht aus den Anwendungsdateien App.xaml und App.xaml.cs sowie einem vordefinierten Fenster, das durch die Dateien Window1.xaml und Window1.xaml.cs erzeugt wird. Das Visual Studio stellt die Zusammengehörigkeit von XAML- und C#-Dateien auch grafisch im Projektmappen-Explorer dar.
Abbildung 2.3: Zusammengehörige Dateien werden verschachtelt dargestellt
Nach dem Erzeugen der WPF-Windows-Anwendung erhalten Sie den folgenden unveränderten Code, der diesmal als Abbildung vorliegt, um die Verknüpfungen besser darzustellen. Der Code wurde lediglich etwas umformatiert, Kommentare und using-Anweisungen wurden entfernt.
35
Kapitel 2
Abbildung 2.4: Beziehungen zwischen den XAML- und Code-Behind-Dateien
2.2.4 Erstellungsprozess einer WPF-Anwendung Auch wenn es auf den ersten Blick nicht so scheint, wird nichts vor Ihnen verborgen. Sie können den gesamten Erstellungsprozess einer WPF-Anwendung vollständig nachvollziehen. Begonnen wird dazu mit der Projektdatei *.csproj. Diese enthält ein Skript, das direkt von MSBuild, dem mit .NET 2.0 eingeführten Build-System, genutzt werden kann.
36
Das Programmiermodell der WPF
Der folgende Code zeigt eine verkürzte, aber voll funktionsfähige Projektdatei. Es soll hier allerdings nicht der gesamte Aufbau erläutert werden sondern nur einige interessante Stellen.
!
!
!
ACHTUNG
Zum Verständnis der folgenden Erläuterungen sind schon Grundkenntnisse über XAML und WPF-Anwendungen notwendig. Der Abschnitt befindet sich der Vollständigkeit halber an dieser Stelle und ist für Interessierte gedacht, die einmal einen Blick hinter die Kulissen werfen wollen.
Der Eintrittspunkt einer Anwendung ist die Datei, die im Element ApplicationDefinition angegeben wird, in diesem Fall App.xaml. Damit wäre schon einmal klar, wo es losgeht. Jede XAML-Datei, die ein Fenster definiert und aus einem XAML- und einem Code-Teil besteht, muss in einem Page-Element angegeben werden. Die zu kompilierenden Dateien werden in einer zweiten Gruppe über Compile-Elemente definiert. Die verschachtelten Elemente DependentUpon und SubType bei der Datei Window1.xaml sorgen dafür, dass das Visual Studio die Zusammengehörigkeit beider Dateien erkennt und diese wie in Abbildung 2.3 darstellt. Die beiden importieren *.target-Dateien, eine für die Sprache (CSharp) und eine für die WPF (WinFX) sorgen für die Übersetzungslogik. Sie finden MSBuild im Verzeichnis [WinDir]\Microsoft.NET\Framework\v2.0.50727. Im gleichen Verzeichnis befinden sich auch die *.target-Dateien. Um eine solche Projektdatei direkt mit MSBuild zu starten, geben Sie auf der Kommandozeile ein: msbuild .csproj. MinimalXAML winexe .
37
Kapitel 2 Window1.xaml Code Listing 2.4: Verkürzte Projektdatei einer WPF-Anwendung
Die Datei App.xaml ist also der Einsprungpunkt der Anwendung. Das ApplicationElement bewirkt, dass später ein Application-Objekt erzeugt wird. Über das Attribut x:Class geben Sie an, dass es sich bei der generierten Klasse, die beim Übersetzen für die XAML-Datei erstellt wird, um eine partielle Klasse handelt. Als Wert werden der Namespace und der Klassenname angegeben. In der Code-Behind-Datei App.xaml.cs befindet sich ebenfalls eine partielle Klasse, die von Application abgeleitet ist. In ihr können Sie bei Bedarf die anwendungsspezifische Logik unterbringen. Damit die Anwendung weiß, welches Fenster sie zu Beginn erzeugen und anzeigen soll, wird in der XAML-Datei das Attribut StartupUri angegeben. Als Wert wird die XAML-Datei Window1.xaml des Hauptfensters zugewiesen. In der Datei Window1.xaml befindet sich zu Beginn wiederum ein x:Class-Attribut mit dem Namen der Klasse der Code-Behind-Datei. Auf diese Weise werden die XAML- und die Code-BehindDateien miteinander verknüpft. Die Code-Behind-Klasse muss dabei von der Klasse abgeleitet sein, die in der XAML-Datei als Wurzelelement verwendet wurde. Besitzt die XAML-Datei als Wurzelelement das Element Window, dann muss auch die Klasse in der C#-Datei von Window abgeleitet werden. Allerdings kann die Ableitung auch weggelassen werden. In diesem Fall erfolgt die Ableitung automatisch. In der Datei Window1.xaml können Sie die Benutzeroberfläche definieren, während Sie in der Datei Window1.xaml.cs die Programmlogik unterbringen. In Letzterer befindet sich lediglich der Konstruktor mit dem wichtigen Aufruf von InitializeComponent(). Was vermissen Sie? Es befindet sich im Code weder eine Methode Main() noch die Methode InitializeComponent(). Beide werden während der Übersetzung generiert und in die Assembly eingebunden. Für die Klasse App vom Typ Application der Testanwendung wird beispielsweise der folgende Code erzeugt. Um diesen herauszubekommen, wurde das überaus nützliche Tool Reflector von Lutz Roeder (http://www.aisto.com/roeder/dotnet/) verwendet.
38
Das Programmiermodell der WPF
Abbildung 2.5: Und hier ist die vermisste Main()-Methode.
In der Main()-Methode wird ein neues App-Objekt erzeugt, über InitializeComponent() das Startfenster definiert (vgl. folgender Code) und zum Abschluss die Nachrichtenschleife gestartet. [DebuggerNonUserCode] public void InitializeComponent() { base.StartupUri = new Uri("Window1.xaml", UriKind.Relative); }
Der Aufruf von InitializeComponent() ist allerdings noch nicht die Methode aus dem Konstruktor der Fensterklasse. Sie befindet sich an anderer Stelle und kann auch ohne den Reflector betrachtet werden (wie auch die eben gezeigten Sourcen). Beim Übersetzen einer WPF-Anwendung, die auch XAML-Dateien verwendet, werden einige interessante Dateien im Verzeichnis ..\obj\Debug erzeugt. So finden Sie hier eine Datei App.g.cs, die den gesamten automatisch generierten Code für die Hauptanwendung enthält, also auch die Methoden Main() und InitializeComponent().
> >
>
HINWEIS
Das Ergebnis der Übersetzung einer XAML-Datei finden Sie in der Datei mit der Endung *.baml (Binary Application Markup Language). Diese enthält den XAML-Code in binärer und effizienter verarbeitbarer Form, allerdings nicht in MSIL. Dann wird sie als Ressource in die fertige Assembly eingebunden.
Ferner finden Sie unter den generierten Dateien eine Datei Window1.g.cs, die den generierten Code für das Fenster enthält (das Zeichen g im Dateinamen steht für genera-
39
Kapitel 2
ted). Hier befindet sich nun auch die Methode InitializeComponent(), die im Konstruktor der Fensterklasse aufgerufen wird. Diese hat zwei Aufgaben. Zuerst wird ein Uri-Objekt für die in der Assembly eingebettete XAML-Ressource erzeugt. Danach wird diese über die Methode LoadComponent() geladen. In der Methode Connect() des Interface IComponentConnector werden zum Abschluss Beziehungen für die in der XAML-Datei mit einem Namen versehenen Komponenten und in der Klasse bereits vorgehaltenen Instanzvariablen hergestellt. Außerdem werden die Ereignishandler der Code-Behind-Klasse mit den Komponenten verknüpft. namespace WindowsApplication1
{ public partial class Window1: Window, IComponentConnector
{ case 1: this.Tb1 = ((TextBox)(target)); this.Tb1.Loaded += new RoutedEventHandler(this.OnLoad); return; case 2: this.Btn1 = ((Button)(target)); return;
} this._contentLoaded = true;
} } } Listing 2.5: Auszug aus der generierten Datei Window1.g.cs
40
Das Programmiermodell der WPF
2.3 Nützliche Anwendungseigenschaften Müssen Sie einmal auf das Anwendungsobjekt der aktuellen Anwendung (konkret der aktuellen AppDomain) zugreifen, verwenden Sie die statische Eigenschaft Current der Klasse Application. Application current = Application.Current;
Eine Anwendung beenden Standardmäßig wird eine WPF-Anwendung beendet, wenn das letzte Fenster geschlossen wird. Dies liegt am Standardwert OnLastWindowClose der Eigenschaft ShutdownMode. Es reicht also, das Hauptfenster mit Close() zu schließen unter der Voraussetzung, dass keine weiteren Fenster geöffnet sind. Ein anderer Wert ist OnMainWindowClose. In diesem Fall wird die Anwendung beim Schließen des Hauptfensters beendet. Möchten Sie die Anwendung selbst beenden, setzen Sie den Wert OnExplicitShutdown. In diesem Fall rufen Sie die Methode Shutdown() des aktuellen Application-Objekts auf. Application.Current.Shutdown();
Kommandozeilenparameter verarbeiten Im Ereignis Startup wird ein Parameter vom Typ StartupEventArgs übergeben. Dieser besitzt eine Eigenschaft Args, über die Sie Zugriff auf die Parameter erhalten. private void OnStartup(object sender, StartupEventArgs e)
Gemeinsame Verwendung von Anwendungsinformationen Das Application-Objekt einer Anwendung, das über Application.Current jederzeit abgerufen werden kann, besitzt eine Aufzählung Properties, über die anwendungsweite Einstellungen verwaltet und bereitgestellt werden können. Application.Current.Properties.Add("Autor", "Dirk Frischalowski"); TbInfo.AppendText(Application.Current.Properties["Autor"] + "\r"); Listing 2.7: Beispiele\Kap02\ApplicationPropsProj\App.xaml.cs und Window1.xaml.cs
41
Kapitel 2
2.4 Fenster und Dialoge Fenster und Dialoge werden auch in der WPF nichtmodal (es kann bei geöffnetem Fenster/Dialog weitergearbeitet werden) über die Methode Show() und modal (Fenster/Dialog muss erst geschlossen werden, bevor weitergearbeitet werden kann) über die Methode ShowDialog() geöffnet. Der Rückgabewert von ShowDialog() kann ausgewertet werden, um festzustellen, wie der Dialog geschlossen wurde. Er entspricht der im Dialog gesetzten Eigenschaft DialogResult, die allerdings gegenüber Windows Forms nun vom Typ bool? ist. Sie können also nur die Werte null, true und false zurückliefern. Die Eigenschaft DialogResult besitzt den Wert false, wenn Sie den Dialog über dessen Systemmenü, das Schließfeld oder (Alt) (F4) schließen. Weisen Sie der Eigenschaft IsCancel eines Buttons den Wert true zu, weist diese automatisch DialogResult den Wert false zu. Im Gegensatz dazu erhalten Sie beim Betätigen eines Buttons mit dem Wert true in der Eigenschaft IsDefault den Wert true in DialogResult. Die Zuweisung eines Wertes an DialogResult schließt außerdem den Dialog sofort.
BEISPIEL AUF DER CD In der kleinen Beispielanwendung werden mehrere Fenster geöffnet. Der Code zeigt exemplarisch, wie ein zweites Fenster, das sich in der Klasse Window2 befindet, modal geöffnet und der Rückgabewert ausgewertet wird. private void ZeigeModalDlgClick(object sender, RoutedEventArgs e)
Standarddialoge Einige Standarddialoge werden noch einmal explizit durch die WPF bereitgestellt. Es handelt sich hier um die Dialoge OpenFileDialog, SaveFileDialog und PrintDialog, die sich im Namensraum Microsoft.Win32 befinden. Der Vorteil liegt unter anderem darin, dass Sie nicht den Namensraum System.Windows.Forms für die Windows Forms-DialogVariante einbinden müssen und somit keine Schwierigkeiten mir doppelten Typbezeichnern erhalten. Die Funktionsweise ist fast identisch. Allerdings ist der Rückgabewert der Methode ShowDialog() vom Typ bool?, sodass Sie explizit auf den Wert true prüfen müssen, um festzustellen, ob der Dialog z. B. über ÖFFNEN beendet wurde.
42
Das Programmiermodell der WPF OpenFileDialog ofd = new OpenFileDialog(); ofd.Filter = "Textdateien(*.txt)|*.txt|Alle Dateien(*.*)|*.*"; bool? res = ofd.ShowDialog(); if(res == true) MessageBox.Show("Ausgewählte Datei: " + ofd.FileName); Listing 2.9: Beispiele\Kap02\ApplicationPropsProj\Window1.xaml.cs
2.5 Ressourcensuche Die WPF verwendet intern das Pack URI Schema zum Auffinden von Ressourcen wie zum Beispiel der Startseite (Startfenster) einer Anwendung. Die Kurzschreibweise beinhaltet einfach den Namen der XAML-Datei, z. B.: StartupUri="Window1.xaml"
Im Pack URI Schema ausgeschrieben wäre die Referenz folgendermaßen anzugeben: StartupUri="pack://application:,,,/Window1.xaml"
Die URI-Angabe besteht dabei aus drei Teilen: dem Schema – hier pack:// der Autorität – hier application dem Pfad zur Ressource – hier /Window1.xaml Die Autorität gibt insbesondere das Package bzw. den Container an, in dem sich die Ressource befindet. Der Pfad bezieht sich dann auf dieses Package – in diesem Fall die Anwendung bzw. die Assembly, welche die Anwendung enthält. Die Suche beginnt in der Assembly und wird dann in einer lokalen Datei weitergeführt. Möchten Sie z. B. ein Bild anzeigen, das sich im Anwendungsverzeichnis befindet, verwenden Sie die folgende Angabe:
Befindet sich das Bild im Unterverzeichnis ...\images der Anwendung, sieht der Verweis so aus:
Befindet sich das Bild schließlich in einer anderen Assembly, die von der aktuellen Assembly referenziert wird, gibt man diese vor dem Verzeichnis an.
43
Kapitel 2
Zip-Dateien verarbeiten Mit dem Package System.IO.Packaging können Sie Daten innerhalb eines Containers, dem Package, verwalten. Insbesondere steht eine Klasse ZipPackage zur Verfügung, über die Sie (etwas umständlich) auch Zip-Dateien erstellen können.
BEISPIEL AUF DER CD Der folgende Beispielcode wird ohne größere Erläuterungen angegeben und soll für eigene Experimente dienen. Es wird darin die Anwendungsdatei gezippt und im gleichen Verzeichnis in der Zip-Datei Appl.zip abgelegt. In den Methoden Package.Open() sowie File.OpenRead() können Sie übrigens auch vollständige Pfadangaben verwenden. private void OnCreateZIP(object sender, RoutedEventArgs e)
{ ZipPackage zipPkg = (ZipPackage)Package.Open("Appl.zip", FileMode.Create); Uri uri = new Uri("/ApplicationPropsProj.exe", UriKind.Relative); ZipPackagePart zipPkgPrt = (ZipPackagePart)zipPkg.CreatePart( uri, MediaTypeNames.Application.Octet, CompressionOption.Fast); Stream zipStr = zipPkgPrt.GetStream(); FileStream origStr = File.OpenRead("ApplicationPropsProj.exe"); byte[] data = new byte[1024]; int readCount; while((readCount = origStr.Read(data, 0, data.Length)) > 0)
Während die Version 9.0 von WinZip mit der Zip-Datei noch ihre Probleme hatte, kam WinRAR in der Version 3.62 gut damit klar.
44
3
Einführung in XAML
3.1 Einführung Mit der WPF wurde über XAML (Extensible Application Markup Language) eine neue Sprache eingeführt, die der Beschreibung einer Benutzeroberfläche dient. Ziel ist es dabei nicht, dass nun sämtliche Benutzeroberflächen ausschließlich mit XAML erstellt werden. Stattdessen bietet XAML die Möglichkeit, zur Entwicklung einer Anwendung einen (Grafik-)Designer für die Oberfläche hinzuzuziehen, der gegebenenfalls geschickter in der Verwendung von Grafiktools ist als ein Programmierer. Grafische Tools können z. B. XAML-Code exportieren und auch direkt Visual Studio-Projekte nutzen (z. B. Expression Blend). Der XAML-Code wird dann in ein Projekt importiert (oder das Projekt wurde direkt bearbeitet), und der Entwickler heftet die Programmlogik an die bereits erstellte Oberfläche. So weit zur Theorie. XAML basiert auf der Syntax von XML. Dies bedeutet, eine XAML-Datei besitzt ein Wurzelelement, das alle anderen Elemente einschließt. Weiterhin müssen alle Elemente korrekt verschachtelt sein, z. B.: ...
statt
Kapitel 3 ...
Attribute werden in doppelte Anführungszeichen gesetzt, und die Namen der Elemente müssen die Groß- und Kleinschreibung beachten.
Obwohl XAML die XML-Syntax verwendet, ist zumindest der typische XML-Prolog
nicht notwendig. Achten Sie weiterhin darauf, dass XAML case-sensitiv ist, d.h., die Groß- und Kleinschreibung wird beachtet.
XAML oder Code Bei der Entwicklung von WPF-Anwendungen stellt sich von vornherein die Frage, ob Sie besser eine Oberfläche in XAML oder im Code erzeugen. Das hängt aber hauptsächlich von den Erfordernissen an die Anwendung und deren Entwicklung ab. Es folgen einige Punkte, die als Kriterium für Ihre Entscheidung dienen können, aber keinen Anspruch auf Vollständigkeit erheben: Die Menge an Code für eine Beschreibung einer Oberfläche über XAML ist wesentlich kompakter, als wenn Sie das gleiche Ergebnis mittels C#-Code erreichen wollen. XAML-Code lässt sich dynamisch zur Laufzeit aus Dateien laden. Ein Grafikdesigner kann attraktive Oberflächen für Anwendungen oder einzelne Komponenten erstellen und diese in XAML speichern (oder direkt in XAML erzeugen). Über Code lassen sich besser umfangreichere Benutzeroberflächen, Animationen oder 3D-Grafiken generieren. Die Ausdrucksmöglichkeiten über Code überwiegen die von XAML. Insbesondere sind die Integrationsmöglichkeiten von Code direkt in eine XAML-Datei begrenzt (und auch nicht sinnvoll).
3.1.1 XAML-Mapping Als Wurzelelement kommt in der Regel ein Windows- oder ein Page-Element zum Einsatz, wenn es sich um ein Fenster oder eine Seite in einer Navigationsanwendung handelt, oder ein ResourceDictionary und Application-Element, wenn Sie eine Ressourcen-
46
Einführung in XAML
oder die Anwendungsdatei mit XAML beschreiben. Theoretisch könnte als Wurzelelement sogar Button oder TextBox verwendet werden, allerdings macht dies für eine Anwendungsentwicklung sicherlich nicht viel Sinn. Einige Elemente verfügen über eine Eigenschaft Content, die genau ein beliebiges anderes Element aufnehmen kann. Die Klasse Window und die Klasse Button besitzen z. B. eine solche Eigenschaft. Damit in das Wurzelelement mehrere Elemente eingefügt werden können, fügt man zuerst ein Containerelement wie ein Grid oder ein StackPanel ein. Darin können weitere Elemente eingebettet werden, da diese Komponenten über eine Eigenschaft Children verfügen, die eine Collection von UIElement-Objekten verwaltet (d.h., sie können mehrere Komponenten aufnehmen). Andere Containerelemente sind z. B. Listen und Menüs. Allerdings besitzen diese keine Eigenschaft Children, sondern sie verwenden eine Eigenschaft Items, die vom Typ ItemCollection ist und mehrere Menü- und Listeneinträge verwaltet. Die Bestandteile einer XAML-Datei sind Namespaces, Elemente und Attribute. Diese haben eine spezielle Bedeutung. Die Elemente (Tags) entsprechen immer einer WPF-Klasse. Von dieser Klasse wird dann eine Instanz über den Standardkonstruktor der Klasse erzeugt. Geben Sie in XAML z. B. ein Element an, wird an dieser Stelle eine Button-Instanz erzeugt. Die Attribute eines Elements werden auf die Eigenschaften oder die Ereignisse der betreffenden Klasse des neuen Objekts gemappt. Die Eigenschaften einer Klasse müssen public sein und Werttypen darstellen. Für alle anderen Typen muss ein sogenannter Typkonvertierer bereitgestellt werden. Die WPF liefert bereits eine Anzahl solcher Typkonvertierer mit.
!
!
!
ACHTUNG
Im gesamten Buch werden die Begriffe »Element« und »Klasse« sowie »Eigenschaft« und »Attribut« synonym verwendet, wobei sich die Bezeichnungen »Element« und »Attribut« in der Regel auf den Einsatz in einer XAML-Datei beziehen.
> >
>
HINWEIS
Da die Angabe eines Elements in XAML zum Aufruf des Standardkonstruktors einer Klasse führt, erkennen Sie an dieser Stelle schon einige Einschränkungen bei der Verwendung von XAML. Sie können z. B. keinen beliebigen Konstruktor aufrufen. Es ist ebenfalls nicht möglich, neue, eigenständige Klassen zu definieren, da sich diese immer in der partiellen Klasse befinden würden, die durch die XAML-Datei definiert wird.
47
Kapitel 3
Namen Damit auf ein Element in einer XAML-Datei, also ein Objekt vom Typ einer Klasse, zugegriffen werden kann, muss ihm ein Bezeichner zugewiesen werden. Von der Klasse FrameworkElement erben alle Komponenten eine Eigenschaft Name, die für das Element einen eindeutigen Namen festlegt. Dieser Name kann im Code wie der Name einer Variablen verwendet werden. Besitzt ein Element in XAML keine Eigenschaft Name, können Sie dennoch einen Namen vergeben. Dazu verwenden Sie das Attribut x:Name. Sie erhalten dann ebenfalls einen allgemeingültigen Bezeichner für das Element. Weiterhin sind einige Elemente nur innerhalb anderer Elemente verfügbar. Die IntelliSense-Hilfe zeigt dies in den meisten Fällen auch korrekt an. In einigen Ausnahmen dürfen Sie aber auch Elemente/Attribute verwenden, die nicht angeboten und im Visual Studio-Editor unterstrichen dargestellt werden. Im folgenden XAML-Code wird beispielsweise ein Button durch ein Button-Element definiert. Er entspricht dabei der Button-Klasse aus dem .NET Framework (und dabei nicht aus dem Namespace System.Windows.Forms, sondern System.Windows.Controls). Des Weiteren wird im Element Button ein Attribut Width verwendet, um die Breite des Buttons festzulegen. Die Werte für ein Attribut werden immer in Anführungszeichen eingeschlossen. Eine notwendige Typumwandlung der Zeichenkette »100« in den Double-Wert 100.0 (es ist tatsächlich eine Gleitkommazahl) findet über einen Typkonvertierer automatisch statt. Hallo
3.1.2 Schreibweise von Elementen und Attributen Die Elemente innerhalb einer XAML-Datei definieren eine Hierarchie, die als Ergebnis die Benutzeroberfläche festlegt. Dazu müssen z. B. Elemente in andere Elementen verschachtelt werden. Dies geht in der WPF sogar so weit, dass Sie in einem Button wiederum weitere Elemente verschachteln können, sodass sich ein Button z. B. aus einem Textfeld und einem Bild zusammensetzt. Bei der Verschachtelung ist zu beachten, dass sich mehrere Elemente in der Regel immer in einem Containerelement befinden müssen. Layoutcontainer sind von der Klasse Panel abgeleitet und besitzen eine Eigenschaft Children, über die sie ihre Kindelemente verwalten. Öffnen
48
Einführung in XAML
Attributschreibweise von Eigenschaften Um die Eigenschaften eines Elements zu setzen, werden die Eigenschaften als Attribute angegeben (Attribute-Syntax). Über das Attribut Background wird beispielsweise die Hintergrundfarbe des Buttons definiert.
Diese Schreibweise ist sehr kompakt, birgt aber einen Nachteil. Da einem Attribut nur Strings zugewiesen werden können, sind die möglichen Zuweisungen begrenzt. Es muss aber eine Möglichkeit geben, einen Hintergrund auch als komplexen Farbverlauf zu definieren. In diesem Fall kommt die folgende Schreibweise zum Einsatz.
Eigenschaftselementschreibweise Die sogenannte Property-Element-Syntax verwendet zur Definition eines Eigenschaftswertes eine Schreibweise, die aus dem Namen des Elements und der durch einen Punkt getrennten Eigenschaft besteht. Das folgende Beispiel setzt beispielsweise ebenfalls den Hintergrund eines Buttons. Unterhalb des Elements Button.Background wird dadurch ein LinearGradientBrush-Objekt erzeugt und der Eigenschaft Background des Buttons zugewiesen.
3.1.3 Namespaces Damit Sie in einer XAML-Datei auf die Typen der WPF und auf spezielle XAMLSchlüsselwörter zugreifen können, werden diese im Wurzelelement jeder XAML-Datei über zwei XML-Namespaces verfügbar gemacht. Der folgende Namespace wird als Default-Namespace definiert und stellt die meisten WPF-Typen zur Verfügung. Da er kein Präfix definiert, können Sie auf einen Button direkt zugreifen, z. B. . xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
Es wäre allerdings auch möglich, hier ein Präfix zu vergeben. Allerdings ist dies nicht üblich und würde zu einer umständlicheren Schreibweise führen. xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation" ...
49
Kapitel 3
Der zweite Namespace definiert für den XAML-Namespace das Präfix x. Natürlich könnte man auch hier einen anderen Namespace-Alias dafür verwenden, was aber auch nicht üblich ist. Das Mapping macht den Namespace System.Windows.Markup in XAML verfügbar. xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
!
!
!
ACHTUNG
In den Beispielen in diesem Buch wird auf die Angabe dieser Namespaces im Wurzelelement in der Regel verzichtet. Stattdessen werden im Wurzelelement drei Punkte als Platzhalter für diese Angaben und meist noch den Titel des Fensters angegeben.
Eigene Namespace-Mappings Neben den vordefinierten Namespaces und Typen können Sie auch andere CLRNamespaces in XAML verfügbar machen. Dies können eigene oder die CLR-Namespaces sein. Um einen weiteren Namespace einzubinden, beginnen Sie zuerst die XML-Namespace-Deklaration über xmlns. Durch einen Doppelpunkt getrennt geben Sie nun einen beliebigen Alias an, z. B. clr. xmlns:clr
Jetzt weisen Sie über zwei Name-Wert-Paare dem XML-Namespace einen CLR-Namespace und die ihn enthaltende Assembly zu. Der Name des Namespace wird über clr-namespace eingeleitet. Um beispielsweise den Namespace System einzubinden, schreiben Sie: xmlns:clr="clr-namespace:System"
Danach wird, getrennt durch ein Semikolon, die Assembly angegeben, in welcher der Namespace definiert wird. Befindet sich der Namespace in der gleichen Assembly wie das Projekt, kann die Angabe der Assembly weggelassen werden. Die Assembly wird ohne die Endung .dll und ohne Pfadangaben angegeben. Optional können Versionsinformationen etc. angegeben werden. xmlns:clr="clr-namespace:System;assembly=mscorlib"
Die partielle Klasse, die hinter der XAML-Datei liegt, muss nicht auf den Namespace des Projekts gemappt werden. Verwenden Sie aber andere Klassen aus dem aktuellen Projekt, muss auch der Projekt-Namespace separat eingebunden werden.
50
Einführung in XAML
BEISPIEL AUF DER CD In der Code-Behind-Datei wird eine Klasse ZeichenFueller deklariert, die eine öffentliche Eigenschaft Anzahl besitzt. Darüber wird angegeben, wie oft der Buchstabe (A) (hier einfach nur beispielhaft) über die überschriebene Methode ToString() zurückgegeben werden soll. Diese Klasse soll später in XAML verwendet werden, namespace XAMLNamespacesProj
{ public partial class Window1: System.Windows.Window
{ public Window1()
{ InitializeComponent(); } } public class ZeichenFueller
In die XAML-Datei des Fensters werden nun zwei zusätzliche Namespaces eingebunden. Statt den Wert der Eigenschaft Width über eine Zeichenkette anzugeben, soll er über den Datentyp Double aus dem Namespace System zugewiesen werden. Beachten Sie, dass auch in diesem Fall die Typkonvertierer zum Einsatz kommen, denn auch die Angabe 200 im clr:Double-Element ist letztendlich ein String. Der Namespace System befindet sich in der Assembly mscorlib, sodass zum Einbinden die Angabe clr-namespace:System;assembly=mscorlib verwendet wird. Als Präfix für den Namespace wird clr verwendet. Um die selbst definierte Klasse zu verwenden, muss nur der Namespace der Klasse eingebunden werden, da er sich in der gleichen Assembly befindet. Als Präfix wird diesmal das Kürzel prj verwendet. Der öffentlichen Eigenschaft Anzahl wird der Wert 10 übergeben, sodass 10x der Buchstabe A über die Methode ToString() zurückgegeben wird. Zur Auswertung des erstellten ZeichenFueller-Objekts (es wird ein String an
51
Kapitel 3
dieser Stelle erwartet) wird automatisch die Methode ToString() des Objekts aufgerufen, die ja in der Klasse überschrieben wurde. 200 Hallo Listing 3.2: Beispiele\Kap03\XAMLNamespacesProj\Window1.xaml
3.2 Markup- und Spracherweiterungen Wenn Sie einem Attribut eines Elements in XAML einen Wert zuweisen, wird versucht, der entsprechenden Eigenschaft des erzeugten Objekts diesen Wert zuzuweisen. Ist der Typ der Eigenschaft nicht String, konvertiert ein Typkonvertierer die angegebene Zeichenkette in den benötigten Typ. Im folgenden Element wird der Hintergrund eines Buttons gefärbt. Da die Eigenschaft Background vom Typ Brush ist, wird der Typkonvertierer aktiv, der eine Zeichenkette in ein Brush-Objekt überführt.
Jetzt kann es aber erforderlich sein, dass der Wert eines Attributs nicht als String interpretiert werden soll. So kann es z. B. wünschenswert sein, nicht immer ein neues BrushObjekt zu erzeugen, sondern stattdessen ein bereits vorhandenes zu verwenden. Über Data Binding können Sie den Wert eines Attributs auf den Attributwert (d. h. einer Eigenschaft) eines anderen Elements setzen. Um dies dem XAML-Parser mitzuteilen, wird eine andere Schreibweise benötigt (der XAML-Parser ist für das Interpretieren des XAML-Codes verantwortlich).
52
Einführung in XAML
Diese spezielle Schreibweise wird Markup-Erweiterung genannt. Dazu wird die Zeichenkette, deren Aufbau abhängig von der Erweiterung ist, in geschweifte Klammern gesetzt. Bei den beiden folgenden Buttons wird die Hintergrundfarbe nur beim ersten Button auf einen konkreten Wert gesetzt. Im zweiten Button wird über ein Binding die Farbe des ersten Buttons verwendet. Der Elementname entspricht dabei dem Namen des ersten Buttons und der Wert von Path der Eigenschaft, an die angebunden werden soll.
In der Attributschreibweise werden die Markup-Erweiterungen also in geschweifte Klammern eingeschlossen. Die konkrete Erweiterung wird dann durch das erste Wort nach der öffnenden Klammer identifiziert, z. B. Binding wie im folgenden Beispiel. ... Background="{Binding ...}"
In der Eigenschaftselementschreibweise wird die Erweiterung wie ein XAML-Element formuliert.
Eine Verschachtelung von solchen Erweiterungen ist ebenfalls möglich. Dabei wird die innerste Ebene zuerst ausgewertet. Eine öffnende geschweifte Klammer leitet als Wert eines Attributs immer eine Markup-Erweiterung ein. Möchten Sie die Klammer als normalen Text interpretieren, setzen Sie das Klammerpaar {} davor.
Sämtliche Markup-Erweiterungen sind Klassen, die von der Klasse MarkupExtension aus dem Namespace System.Windows.Markup abgeleitet sind. Die Erweiterung Binding wird z. B. durch die Klasse System.Windows.Data.Binding implementiert.
3.2.1 Markup-Erweiterungen von XAML Es gibt zwei Typen von Markup-Erweiterungen, XAML- und WPF-spezifische. XAML selbst definiert einige Erweiterungen, die völlig unabhängig von der WPF sind. Sie werden über den Alias des XAML-Namespace eingeleitet, z. B. x:Type.
53
Kapitel 3
Erweiterung
Beschreibung
x:Array
Hiermit können Sie Arrays in XAML definieren. Wenn Sie CLR-Typen wie String oder Double als Array-Elemente verwenden wollen, müssen Sie noch den Namensraum System einbinden und ein Präfix festlegen (z. B. clr). Über das Attribut Type wird der Typ der Array-Elemente angegeben. >
>
HINWEIS
Um in einem Elementinhalt die Whitespace-Zeichen zu erhalten, wird das Attribut xml:space mit dem Wert preserve angegeben. Der Standardwert default entfernt die Leerzeichen. Dieses Attribut wird allerdings über XML und nicht durch XAML definiert.
Hallo
3.3 XAMLPad Um schnell das Ergebnis eines XAML-Codes zu betrachten, gibt es verschiedene Möglichkeiten. Sie können im Visual Studio den WPF Designer verwenden, allerdings ist dessen Geschwindigkeit, gerade zwischen der Umschaltung zwischen XAML und Designer, nicht sehr berauschend, und auch seine Designmöglichkeiten sind recht beschränkt. Natürlich können Sie eine Anwendung übersetzen und ausführen, um das Ergebnis zu sehen, aber auch das ist ziemlich umständlich. Bliebe noch die Lösung, ein externes Tool wie Expression Blend zu nutzen, aber erstens wird es in Zukunft nicht kostenfrei sein, und zweitens müssen Sie immer zwischen der grafischen Darstellung und Anzeige des XAML-Codes umschalten. Aushilfe bietet hier das Tool XAMLPad. Sie finden XAMLPad nach der Installation des Windows SDK im Programmordner START – PROGRAMME – MICROSOFT WINDOWS SDK – TOOLS. Im unteren Bereich geben Sie den XAML-Code in einem eher rudimentären Editor ein, der leider keine
55
Kapitel 3
Syntaxhilfe unterstützt. Nach dem Start wird immer der zuletzt beim Schließen eingegebene XAML-Code angezeigt. Dazu speichert XAMLPad den zuletzt gültigen Code im Verzeichnis [LW]:\Programme\Microsoft SDKs\Windows\v6.0\Bin in der Datei Xaml-Pad_Saved.xaml. Haben Sie gültigen Code eingegeben, wird dieser sofort interpretiert und das Ergebnis im oberen Bereich angezeigt. Als Wurzelelement wird normalerweise ein Page-Element verwendet, damit es in XAML-Pad eingebettet werden kann. Sie können aber auch einen Layoutcontainer wie Grid oder StackPanel als Wurzelelement verwenden. Verwenden Sie Window als Wurzelelement, wird ein neues Fenster mit dem Inhalt geöffnet. Fehler werden ganz unten in einem Statusbereich angezeigt. Sie können diese Vorgehensweise allerdings durch das Deaktivieren der Schaltfläche AUTO PARSE links oben aufheben. Über die Schaltfläche SHOW VISUAL TREE (in der Abbildung 3.1 nicht sichtbar) kann die komplette interne Verwaltung einer XAML-Seite betrachtet werden (1) und (2).
Abbildung 3.1: XamlPad mit Editor, Ausgabebereich und geöffnetem Visual Tree
56
Einführung in XAML
3.4 Loose XAML Normalerweise wird eine XAML-Anwendung in eine Assembly kompiliert und wie eine »normale« .NET-Anwendung ausgeführt. Außerdem ist es einer Anwendung möglich, auch zur Laufzeit XAML-Code nachzuladen und zu interpretieren. Eine weitere Variante steht mit Loose XAML zur Verfügung. Damit wird der Internet Explorer durch ein Plug-In befähigt, eine einzelne XAML-Datei zu laden und anzuzeigen. Voraussetzung ist, dass im Wurzelelement der WPF- und bei Bedarf auch der XAMLNamespace angegeben werden. So erhalten Sie mit dem folgenden Code die wahrscheinlich kleinste »XAML-Anwendung« (auf jeden Fall wenn man auch noch das Setzen der Hintergrundfarbe und der Beschriftung weglässt).
Üblicherweise wird das Element Page als Wurzelelement verwendet (da die XAML-Ausgabe dadurch in das Hostfenster eingebettet werden kann), Window ist nicht möglich. Außerdem darf sich kein Programmcode in der XAML-Datei befinden, da dieser durch das integrierte Plug-In nicht interpretiert und schon gar nicht ausgeführt werden kann. Wozu ist das nützlich? Wenn Sie einfache Beispiele für XAML-Code erstellen und anzeigen möchten und diese vielleicht noch über das Internet bereitstellen, ist dies sicher eine sehr geeignete Lösung (Sorry Firefox-User). Statt einfacher Beispiele lassen sich aber auch komplexe 3D-Grafiken mit Animationen in einer XAML-Datei verpacken. So könnten Sie eine Kursentwicklung oder eine Wetterkarte mit XAML erstellen und im Internet bereitstellen. Innerhalb einer HTML-Seite können Sie über IFrames auch mehrere XAML-Dateien gleichzeitig laden und anzeigen. Dazu wird jede XAML-Datei in ein iframe-Element eingeschlossen. Diese Vorgehensweise macht natürlich auch nur im Internet Explorer Sinn, da momentan nur dieser Browser den XAML-Code über das Plug-In ausführen kann. Loose XAML - Beispiele
Überschrift 1
Überschrift 2
57
Kapitel 3
Listing 3.3: Beispiel für das Einbinden von mehreren XAML-Dateien in eine HTML-Seite
3.5 Verwendung des WPF Designers Der integrierte WPF Designer im Visual Studio dient momentan der rudimentären Bearbeitung einer XAML-Datei auf grafischem Wege. Er besitzt allerdings noch keine Funktionalität, die sich hier zu beschreiben lohnt. Standardmäßig wird eine XAML-Datei im Visual Studio mit dem WPF Designer geöffnet. Wenn Sie einen anderen Standardeditor definieren wollen, öffnen Sie den Kontextmenüpunkt OPEN WITH einer XAML-Datei und wählen einen anderen Standardeditor aus.
Abbildung 3.2: Einen anderen Standard-XAML-Editor auswählen
3.6 Dependency und Attached Properties Das Eigenschaftssystem von .NET 3.0 wurde mit den neu eingeführten Dependency Properties (abhängigen Eigenschaften) erweitert. Sie basieren zwar auf den CLREigenschaften und deren Schreibweise, werden aber anders deklariert. Die Dependency Properties stellen weitere Funktionalitäten zur Verfügung.
58
Einführung in XAML
eine automatische Aktualisierung eine integrierte Validierung die Deklaration von Standardwerten den Aufruf von Callback-Methoden, wenn Wertänderungen aufgetreten sind Die Gründe für die Einführung der Dependency Properties und damit deren Einsatzgebiete sind vielfältig. Viele Aufgaben in der WPF hängen davon ab zu überwachen, ob sich im System oder innerhalb der Anwendung Eigenschaftswerte geändert haben. Die Benachrichtigung über Änderungen dieser Werte und die automatische Aktualisierung der abhängigen Objekte gehören z. B. dazu. Bestimmte Features stehen nur für diese Eigenschaften zur Verfügung wie z. B.: Animationen Data Binding Styles Der Wert einer Eigenschaft ist somit auch vom verwendeten Stil, eingesetztem Data Binding oder Animationen abhängig. Der konkrete Wert muss dann von der WPF berechnet werden. Aufgrund des höheren Aufwands sind nicht alle Eigenschaften Dependency Properties. In eigenen Komponenten sollten Sie ebenfalls nur die Eigenschaften als Dependency Properties definieren, welche die speziellen Features der WPF nutzen.
Attached Properties Attached Properties (angehangene Eigenschaften) sind eine spezielle Variante der Dependency Properties. Das Besondere daran ist, dass die Eigenschaften zu einem Elternelement (meist einem Layoutcontainer) gehören, aber die Werte in den Kindelementen gesetzt werden. Der Vorteil dabei ist, dass die Kindelemente nicht mit Informationen überlastet werden, keine überflüssigen Eigenschaften enthalten (für jeden Layoutcontainer müssten z. B. spezielle Eigenschaften bereitgestellt werden) und das ganze System dadurch auch erweiterbar ist. Durch den folgenden Code wird ein Button in einem Tabellengitter in der 1. Spalte und 2. Zeile positioniert. Die Eigenschaften Column und Row werden aber nicht durch die Button-Klasse, sondern durch die Klasse Grid definiert. Aus diesem Grund muss vor den Attributen auch der Klassenname angegeben werden, z. B. Grid.Column.
bzw. im Code: Grid.SetColumn(BtnImGrid, 0); Grid.SetRow(BtnImGrid, 1);
59
Kapitel 3
> >
>
HINWEIS
Wie Dependency und Attached Properties definiert werden, wird im Kapitel zum Erstellen eigener Komponenten erläutert.
3.7 Logischer und visueller Elementbaum Die folgenden Themen enthalten schon »etwas« fortgeschrittenere Techniken, sollen aber an dieser Stelle untergebracht werden. Es handelt sich in diesem Abschnitt um die interne Darstellung der Elemente einer Benutzeroberfläche. Diese werden in zwei Baumstrukturen verwaltet, dem logischen und dem visuellen Elementbaum. Der logische Elementbaum enthält alle Elemente, wie sie durch Verschachtelung und Enthaltensbeziehungen in XAML oder im Code definiert wurden und in der Benutzerschnittstelle angezeigt werden. Er kann im Prinzip immer über Eigenschaften wie Content, Children oder Items bestimmt werden. So enthält das in Listing 3.4 definierte Stack-Panel einen Button und eine TextBox. Diese werden im logischen Baum als »Blätter« des Stack-Panels betrachtet. Der logische Baum enthält allerdings keine Elemente wie selbst definierte Füllmuster oder Animationen. Die Darstellung der WPF-Komponenten wird intern ebenfalls über XAML-Code bzw. WPF-Elemente definiert, durch sogenannte Control Templates. Ein Button besteht dann aus einem ButtonChrome-Element (eine Art Verzierung), in dem sich ein ContentPresenter und darin wieder eine TextBox befinden. Der visuelle Baum enthält somit alle Elemente, mit denen letztendlich die Benutzeroberfläche gezeichnet wird. Im visuellen Baum sind nur die Elemente enthalten, die von den Klassen Visual und Visual3D abgeleitet sind, da nur diese Elemente sich selbst darstellen können. Beide Bäume können relativ einfach durchlaufen werden. Dazu stellt die WPF die Hilfsklassen LogicalTreeHelper und VisualTreeHelper zur Verfügung. Die Klasse LogicalTreeHelper besitzt eine Methode GetChildren(), die ein IEnumerable-Objekt zurückgibt. Dieses kann wiederum durchlaufen werden, um die Kinder dieser Elemente zu ermitteln. Die Klasse VisualTreeHelper verhält sich hier etwas anders. Über die Methode GetChildCount() ermitteln Sie die Kinder und lassen sich über die Methode GetChild() ein bestimmtes Kindelement zurückgeben. Beide Baumstrukturen sind zwar für die meisten Anwendungen nicht relevant, werden aber von Hardcore-Komponentenentwicklern für Manipulationen genutzt. Für sie stellen beide Bäume eine Hilfe zum besseren Verständnis der internen Arbeitsweise der WPF dar. Nicht zuletzt können auch Komponenten und Benutzeroberflächen analysiert werden.
60
Einführung in XAML
BEISPIEL AUF DER CD Die Anwendung definiert in der XAML-Datei des Fensters einige Komponenten. Es sollen der visuelle und der logische Elementbaum dargestellt werden. Dazu ist es erst einmal wichtig, dass Sie den Baum in einer separaten Komponente erstellen und erst nach der Analyse in den vorhandenen Elementbaum einhängen, um Endlosschleifen zu vermeiden. Als Ausgabemedium wird eine TreeView-Komponente verwendet. Die Beschriftung eines Baumelements wird über die Eigenschaft Header durchgeführt. Mittels der Eigenschaft IsExpanded werden die Knoten aufgeklappt dargestellt. Jede Ebene einer Verzweigung wird durch ein TreeViewItem-Objekt erzeugt. Über die Methode Items.Add() werden einem solchen Objekt Unterelemente hinzugefügt. Die Methode DoTrees() welche die Analyse startet, wird über das Ereignis Loaded aufgerufen. Zur Erstellung jedes Baums wird eine separate Methode aufgerufen. Danach werden beide Hilfsklassen zur Analyse der Bäume verwendet. Zur Durchführung der Typumwandlungen wird in den Schleifen noch sichergestellt, dass die zurückgegebenen Elemente vom Typ FrameworkElement sind. In diesem Fall kann nämlich der Name des Elements (sofern vorhanden) ausgegeben werden.
Abbildung 3.3: Visueller und logischer Elementbaum
{ FrameworkElement fe = obj; TreeViewItem tviChild = new TreeViewItem(); tviChild.Header = fe.GetType().ToString() + ": " + fe.Name;
62
Einführung in XAML tvi.Items.Add(tviChild); tviChild.IsExpanded = true; IEnumerable children = LogicalTreeHelper.GetChildren(fe); foreach(Object fe2 in children) { if(fe2 is FrameworkElement) ViewLogicalTree((FrameworkElement)fe2, tviChild); } } Listing 3.5: Beispiele\Kap03\TreesProj\Window1.xaml.cs
3.8 XAML lesen und schreiben Dieses Kapitel soll ein kurzer Ausflug in die Fähigkeiten der Klassen XamlReader und XamlWriter werden und auch auf potenzielle Sicherheitsprobleme hinweisen. Wie schon erwähnt wurde, kann XAML zur Laufzeit nachgeladen werden. Dies wird bereits von jeder WPF-Anwendung genutzt, die in der generierten Methode InitializeComponent() die Methode LoadComponent() einsetzt, um den binären XAML-Code aus der Assembly zu laden. Innerhalb der Methode werden dazu letztendlich die Klasse XamlReader und deren Methode LoadBaml(), die als internal deklariert ist, genutzt. Über die statische Methode Load() können Sie XAML-Code aus einem Stream oder einem XamlReader lesen. Zurückgeliefert wird ein Objekt vom Typ des Wurzelelements der XAML-Datei. Da der Rückgabetyp der Methode allerdings Object ist, müssen Sie ihn noch entsprechend umwandeln. Der folgende Code lädt den Inhalt der Datei Content.xaml, der als Wurzelelement ein Stack-Panel enthält. Ein Objekt vom Typ des Wurzelelements wird dann einer Variablen zugewiesen. Das Stack-Panel kann nun in den vorhandenen Elementbaum eingehangen werden. FileStream fs = new FileStream("Content.xaml", FileMode.Open, FileAccess.Read); StackPanel sp = (StackPanel)XamlReader.Load(fs);
Umgekehrt können Sie den Elementbaum ausgehend von einem Objekt als XAMLCode serialisieren. Dazu besitzt die Klasse XamlWriter mehrere überladene statische Save()-Methoden.
> >
>
HINWEIS
Die nächsten Beispiele nutzen bereits verschiedene WPF-Elemente, die erst später beschrieben werden. Es bot sich meiner Meinung nach aber kein anderer geeigneter Punkt an, diese Beispiele unterzubringen. Sehen Sie die Beispiele als eine etwas zu umfangreich geratene Einführung an.
63
Kapitel 3
BEISPIEL AUF DER CD In einer separaten Datei Content.xaml wird der gesamte Inhalt eines Fensters definiert. In einem Stack-Panel befinden sich dazu ein Button und ein Textfeld. Damit beide angesprochen werden können, wurde ihnen über die Eigenschaft Name ein Bezeichner zugewiesen. Das Laden der externen XAML-Datei erfolgt im Ereignis Loaded, das nach dem erfolgreichen Initialisieren aller Oberflächenelemente ausgelöst wird. Da der Name des Buttons aus der XAML-Datei bekannt ist, kann über die Methode FindName() des StackPanel-Objekts eine Referenz darauf ermittelt werden. Danach kann der Button mit einer Ereignisbehandlung verknüpft werden. In der Ereignisbehandlung wird eine Referenz auf die TextBox geholt und ein Text ausgegeben. Die einzigen Voraussetzungen an die externe XAML-Datei sind somit die verwendeten Komponenten und deren Namen. Das gesamte Layout kann vollkommen individuell sein. Beim Schließen der Anwendung wird das Ereignis Closing ausgelöst (die Verknüpfungen für die Ereignisse Loaded und Closing werden in der XAML-Datei hergestellt). Darin wird ein XmlWriter mit XmlWriterSettings und der Ausgabe in die Datei NewContent.xaml erzeugt. Die Verwendung der XmlWriterSettings sind notwendig, damit der XAML-Code einigermaßen formatiert in die Datei geschrieben wird. Der XamlWriter schreibt dann mithilfe des XmlWriters den XAMLCode für das Hauptfenster in die Datei. Der Inhalt der Datei wird im letzten Listing angegeben. Witzigerweise wird für den WPF-Namespace hier das Präfix av definiert. Das Resultat für die erzeugte XAML-Datei ist im Listing Window1.xaml dargestellt, allerdings zusätzlich mit dem nachgeladenen Inhalt der Datei Content.xaml verknüpft.
Sicherheitsbedenken XAML ist für jeden lesbar. Das sollte Ihnen immer bewusst sein. Eine kompilierte .NET-Anwendung liegt in MSIL-Code vor, der dekompiliert werden kann, sodass der Quellcode mehr oder weniger wieder zum Vorschein tritt. Mit XAML ist es genauso, nur dass Sie selbst den Code extrahieren können. Sie verwenden dazu zuerst die Methode LoadComponent() der Klasse Application, die ein Objekt vom Typ des XAMLWurzelelements zurückgibt. Der XAML-Code kann sich dazu auch in kompilierter Form in einer Assembly befinden (also als BAML-Code), d.h., er muss sich nicht in einer separaten XAML-Datei befinden. Nachdem Sie das Objekt erst einmal erstellt haben, können Sie wie im letzten Beispiel den XAML-Code in eine Datei oder einen Stream serialisieren. Viel Spaß.
BEISPIEL AUF DER CD Die Beispielanwendung verwendet die Anwendung MinimalXamlProj.exe aus dem Kapitel 2, um deren XAML-Code für das Hauptfenster auszuspionieren. Dazu muss ein Uri-Objekt erzeugt werden, das auf die externe Assembly und darin auf die Datei Window1.xaml verweist. Nachdem das WindowObjekt aus der XAML-Datei erzeugt wurde, wird es über den XamlWriter in einen Memory-Stream geschrieben. Dieser wird wiederum genutzt, um seinen Inhalt in einer RichTextBox anzuzeigen.
Abbildung 3.5: Aus einer anderen Assembly ausgelesener XAML-Code
66
Einführung in XAML Listing 3.10: Beispiele\Kap03\XamlStolenProj\Window1.xaml using System.IO; using System.Xml; using System.Windows.Markup; private void OnClick(object sender, RoutedEventArgs e)
{ Uri uri = new Uri("/MinimalXamlProj;component/window1.xaml", UriKind.Relative); Window w = (Window)Application.LoadComponent(uri); Stream xamlStream = new MemoryStream(); XmlWriterSettings set = new XmlWriterSettings(); set.Indent = true; set.NewLineChars = "\r"; set.OmitXmlDeclaration = true; set.NewLineOnAttributes = true; set.Encoding = Encoding.ASCII; XmlWriter xmlW = XmlWriter.Create(xamlStream, set); XamlWriter.Save(w, xmlW); w.Close(); TextRange tr = new TextRange(RtbXamlCode.Document.ContentStart, RtbXamlCode.Document.ContentEnd); tr.Load(xamlStream, DataFormats.Text); xamlStream.Close(); } Listing 3.11: Beispiele\Kap03\XamlStolenProj\Window1.xaml
> >
>
HINWEIS
Damit Sie nicht sagen können, ich hätte es nicht erwähnt, verstecke ich an dieser Stelle noch einen kleinen Hinweis. Die Projekte im Visual Studio erhalten von mir durchgängig das Suffix Proj. Die Bezeichner in den Anwendungen wurden in der Mehrzahl englisch gewählt, aber es kann hin und wieder auch ein deutscher (oder schlimmer denglischer) Bezeichner auftauchen. Das nächste Mal wird alles besser, und dann werde ich die Bezeichner durchgängig in Sächsisch wählen.
67
4
Ereignisbehandlung
4.1 Grundlagen Ein wesentliches Merkmal von XAML ist die Möglichkeit, Oberflächen getrennt von der Anwendungslogik zu erstellen. Man kann sich eine XAML-Datei wie eine Ressourcendatei für die Benutzeroberfläche vorstellen, die mit speziell dafür vorgesehenen Tools bearbeitet werden kann, z. B. mit dem WPF Designer oder Expression Blend. Da die Programmlogik normalerweise nicht in XAML verankert ist, muss es möglich sein, mit einer Programmiersprache wie C# oder Visual Basic diese zu formulieren und mit einer XAML-Datei zu verknüpfen. Da mit XAML eine weitere Komponente bei der Ereignisbehandlung hinzugekommen ist, existieren gleich mehrere Varianten, um auf Ereignisse zu reagieren: Direkt in XAML über x:Code-Abschnitte. In XAML wird ein Ereignis einer Komponente mit einer Routine in der Code-Behind-Datei verknüpft. Die Ereignisbehandlung erfolgt nur im Code. Innerhalb von Stilen werden sogenannte Event-Setter und Event-Trigger verwendet (diese werden im Kapitel zu Styles beschrieben).
Kapitel 4
4.2 Code in der XAML-Datei Eine Lösung, XAML- und C#-Code miteinander zu verbinden, besteht darin, beides direkt in der XAML-Datei einzusetzen. Dadurch ist keine Code-Behind-Datei mehr notwendig. Ein Tool, das XAML-Dateien direkt verarbeitet, muss nun in der Lage sein, den Programmcode zu übersetzen und auszuführen. Der Internet Explorer, der ja XAML-Dateien laden und anzeigen kann, ist z. B. nicht in der Lage, den Code zu übersetzen. Folglich wird die XAML-Datei von ihm nicht interpretiert. Das Visual Studio hingegen übersetzt den Code in der XAML-Datei mitsamt dem Code einer möglichen Code-Behind-Datei und bindet ihn als binäre Ressource in die Anwendung. Ein Code-Abschnitt innerhalb von XAML muss in ein x:Code-Element eingebettet werden. Da der Code beliebig sein kann, also auch Operatoren wie < und > enthalten kann, und er sich in einer XML-Datei befindet (XAML ist ja letzten Endes XML), sollte dieser Code-Abschnitt in einen CDATA-Abschnitt eingeschlossen werden. Dadurch wird der Inhalt durch einen XML-Parser (also auch bei der Interpretation der XAML-Datei) nicht als XML-Daten ausgewertet. Ansonsten ist die Verwendung des CDATA-Abschnitts optional. Eine Beschränkung dieser Lösung ist beispielsweise, dass nur Code für die partielle Klasse eingefügt werden kann, die zur XAML-Datei gehört. Separate Klassen können z. B. nicht in einem solchen Abschnitt deklariert werden (ausgenommen innere Klassen).
BEISPIEL AUF DER CD Innerhalb einer XAML-Datei werden ein Button und eine TextBox innerhalb eines Stack-Panels eingefügt. Der Button ist mit dem Text Klick mich beschriftet, die TextBox enthält den Text Hallo. Um auf das Klicken des Buttons zu reagieren, muss das Attribut Click, das hier dem Click-Ereignis entspricht, mit dem Namen einer Methode verknüpft werden, die auf dieses Ereignis reagiert. In diesem Fall heißt die Methode ClickMich(). Durch den Klick auf den Button soll der Inhalt der TextBox geändert werden. Damit Sie das TextBox-Element ansprechen können, müssen Sie einen Namen für die TextBox vergeben. Es wird der Name TbInfo über das Attribut Name zugewiesen. Um Code in der XAML-Datei unterzubringen, wird das Element x:Code verwendet. In einem CDATA-Abschnitt wird dann der Code für die Ereignisbehandlung untergebracht, der selbsterklärend sein sollte.
Die Verwendung von Code innerhalb der XAML-Datei ist zwar eine mögliche Lösung für eine Ereignisbehandlung, allerdings sollte davon eher weniger Gebrauch gemacht werden. Ziel sollte es stattdessen sein, eine saubere Trennung zwischen dem Design der Oberfläche und der dahinter liegenden Programmlogik zu schaffen.
4.3 Getrennte Ereignisbehandlung Der übliche Weg der Ereignisbehandlung ist die Verknüpfung eines Ereignisses mit einer Komponente in XAML und die Kodierung der Methode in der Code-BehindDatei. In der XAML-Datei muss dazu in der Komponente, im Attribut, das den Namen des Ereignisses trägt, der Name der Ereignisroutine angegeben werden – wie auch schon bei der Verarbeitung innerhalb von XAML. Der Code wird jetzt aber in der Code-Behind-Datei hinterlegt. Dazu muss die Methode entsprechend der Signatur des Ereignisses erstellt und in der partiellen Klasse der Code-Behind-Datei, die der XAML-Datei zugeordnet ist, implementiert werden.
BEISPIEL AUF DER CD Das folgende Beispiel verknüpft wiederum das Click-Ereignis eines Buttons mit einer Methode namens ClickMich(). Diesmal wird der Code allerdings in die Code-Behind-Datei des Fensters ausgelagert. Klick mich Hallo Listing 4.2: Beispiele\Kap04\EreignisCodeInCodeBehindProj\Window1.xaml
71
Kapitel 4
Die Methode ClickMich() wird in der Fensterklasse Window1 deklariert. Der Name der TextBox steht ebenfalls im Code zur Verfügung, auch wenn er in der XAML-Datei über das Attribut Name vergeben wurde. namespace EreignisCodeInCodeBehindProj
{ public partial class Window1: System.Windows.Window
4.3.1 Anwendungsereignisse Der Rahmen einer .NET 3.0-Windows-Anwendung besteht aus einem Element bzw. einem Objekt vom Typ Application aus dem Namespace System.Windows. In der XAML-Datei mit dem Standardnamen App.xaml befindet sich ein wesentlicher Eintrag. Gemeint ist das Attribut StartupUri, das den Namen der Fensterklasse enthält, von der eine Instanz nach dem Start angezeigt werden soll, das Hauptfenster sozusagen. Man kann hier aber auch auf andere Anwendungsereignisse reagieren, z. B. wenn die Anwendung geladen oder beendet wird. Die Klasse Application enthält dazu verschiedene Ereignisse, von denen einige in der folgenden Tabelle gezeigt werden. Ereignis
Beschreibung
Activated
Die Anwendung gelangt in den Vordergrund.
Deactivated
Die Anwendung ist keine Vordergrundanwendung mehr.
DispatcherUnhandledException
Wird ausgelöst, wenn eine nicht behandelte Exception aufgetreten ist.
Exit
Die Anwendung wird beendet.
SessionEnding
Soll die aktuelle Windows-Sitzung beendet werden, wird dieses Ereignis ausgelöst. Die Anwendung hat noch die Möglichkeit, das Beenden zu unterbinden. Beachten Sie allerdings, dass es Verfahren gibt, auch ohne Rücksicht auf andere Anwendungen Windows zu beenden (z. B. durch einen Blue Screen).
Startup
Die Run()-Methode der Anwendung wurde aufgerufen, d.h., die Anwendung wurde gestartet.
Tabelle 4.1: Einige Ereignisse der Klasse Application
72
Ereignisbehandlung
BEISPIEL AUF DER CD Wenn Sie eine Windows-Anwendung mit der WPF erzeugen, wird das Hauptfenster über den XAML-Code StartupUri="Window1.xaml" in der Datei App.xaml festgelegt. Sie können das Hauptfenster aber auch anders erzeugen, z. B. manuell im Ereignis Startup. Dazu wird die Angabe der StartupUri entfernt und stattdessen eine Verknüpfung zum Ereignis Startup hinzugefügt. Alternativ lassen Sie auch diese Ereignisverknüpfung weg und überschreiben stattdessen die Methode OnStartup() der Klasse Application, die automatisch beim Ereignis Startup aufgerufen wird. In diesem Fall gibt es keinen Parameter vom Typ object, und Sie müssen zum Überschreiben protected override angeben. Kurz gesagt gibt es zahlreiche Varianten, Initialisierungscode unterzubringen. Um nicht behandelte Exceptions abzufangen, kann auf das Ereignis DispatcherUnhandledException reagiert werden. Beim Auftreten einer solchen unbehandelten Exception wird jetzt die Methode MainExHandler() aufgerufen. Darin kann die Exception ausgewertet und durch das Setzen der Eigenschaft Handled des Parameters vom Typ DispatcherUnhandledExceptionEventArgs auf true als behandelt markiert werden. Die Exception selbst wird im Hauptfenster der Anwendung durch einen Klick auf einen Button ausgelöst. Startup="OnStartup" DispatcherUnhandledException="MainExHandler"> Listing 4.4: Beispiele\Kap04\AnwendungsEreignisseProj\MyApp.xaml
Entweder Sie implementieren eine Ereignisbehandlung, wie sie im folgenden Listing gezeigt wird, oder Sie entfernen die Verknüpfung zwischen dem Startereignis Startup und der Methode OnStartup(), kommentieren die Methode OnStartup() im folgenden Code-Teil aus und verwenden die überschriebene Methode OnStartup(), die hier denselben Namen besitzt. public partial class App: System.Windows.Application
4.3.2 Fensterereignisse Zur Initialisierung einer Anwendung und für zahlreiche weitere Situationen besitzt auch ein Fenster vom Typ Window einige interessante Ereignisse. Insbesondere kann für die Initialisierung der Oberfläche auf das Ereignis Loaded zurückgegriffen werden. Hier können z. B. noch weitere Komponenten dynamisch der Oberfläche hinzugefügt werden, oder Sie können vorhandene Komponenten konfigurieren. Die folgende Tabelle listet einige Ereignisse der Klasse Window auf. Ereignis
Beschreibung
Activated
Das Fenster gelangt in den Vordergrund.
Closed
Das Fenster wird geschlossen.
Closing
Wird aufgerufen, wenn das Fenster geschlossen werden soll. Dies kann über die Eigenschaft Cancel des Parameters vom Typ CancelEventArgs aber verhindert werden. Weisen Sie ihm dazu den Wert true zu.
Deactivated
Das Fenster wurde deaktiviert, d.h., ein anderes Fenster wurde ausgewählt.
Tabelle 4.2: Einige Ereignisse der Klasse Window
74
Ereignisbehandlung
Ereignis
Beschreibung
Initialized
Direkt nach der Erzeugung des Objekts durch den Konstruktor wird dieses Ereignis ausgelöst. Es sollten hier allerdings noch keine Zugriffe auf die Werte der Komponenten durchgeführt werden, da diese zum Teil noch unbestimmt sein können.
Loaded
Bringen Sie hier den Code unter, der direkt nach dem Initialisieren aller Elemente eines Fensters ausgeführt werden soll. Das Ereignis Initialized ist dazu meist ungeeignet, da das Layout der Komponenten noch nicht abgeschlossen ist.
LocationChanged
Die Position des Fensters hat sich geändert.
SizeChanged
Die Größe des Fensters hat sich geändert.
StateChanged
Der Fensterstatus (minimiert, maximiert, normal) hat sich geändert.
Unloaded
Das Fenster ist zerstört. Ein Zugriff auf die Elemente ist nicht mehr möglich.
Tabelle 4.2: Einige Ereignisse der Klasse Window (Fortsetzung)
4.3.3 Ereignishandler dynamisch zuweisen Eine Verknüpfung einer Komponente, die innerhalb von XAML oder im Code erzeugt wurde, mit einem Ereignishandler kann auch dynamisch zur Laufzeit einer Anwendung erfolgen. Dazu stellen Sie einen Ereignishandler mit den geforderten Parametern bereit und fügen sie dem entsprechenden Ereignis hinzu. Die Vorgehensweise entspricht damit genau der Vorgehensweise wie in Windows Forms-Anwendungen.
BEISPIEL AUF DER CD Die Anwendung verfügt über zwei Schaltflächen, von denen eine bereits mit einem Click-Ereignishandler verknüpft ist. Die andere Schaltfläche verfügt über den Namen BtnEreignis, unter dem sie später angesprochen werden kann. In der Methode ErzeugeVerknuepfung() wird ein neuer Delegate vom Typ RoutedEventHandler erzeugt, dem als Parameter ein Verweis auf die Methode übergeben wird, welche die Ereignisbehandlung implementiert. Dieser Ereignishandler wird dem Click-Ereignis hinzugefügt. Danach wird die Beschriftung der Schaltfläche nach Jetzt verknüpft geändert. Die Methode BtnEreignisClick() besitzt wieder den Standardaufbau der Click-Ereignisbehandlung und zeigt eine MessageBox an. Verknüpfung herstellen Keine Verknüpfung Listing 4.8: Beispiele\Kap04\DynamischeEreignisseProj\Window1.xaml
4.4 Ereignisweiterleitung Wenn z. B. auf einen Button geklickt wird, wird ein Ereignis ausgelöst. Dieses Ereignis könnte nun direkt vom Button verarbeitet werden, danach wäre es nicht mehr verfügbar. Die WPF erlaubt es aber, dass ein Button weitere untergeordnete Elemente besitzt. Ein Klick auf solch einen Button kann auf den Button (z. B. den Randbereich), aber auch auf ein untergeordnetes Element (z. B. ein Bild und eine Beschriftung) erfolgen. Die Frage, die sich nun für den Entwickler stellt, ist, wie kann er auf alle Möglichkeiten reagieren, wenn auf den Button geklickt wird. Eine Lösung wäre, den Klick an die untergeordneten Elemente weiterzureichen. Genau dafür sind die sogenannten RoutedEvents der WPF da. Sie haben die Parameter vom Typ RoutedEventArgs bereits mehrfach in den Ereignishandlern verwendet. Bei einem RoutedEvent wird ein Klick entweder an das jeweils untergeordnete oder übergeordnete Element weitergereicht, oder es wird wie bisher direkt am auftreffenden Element konsumiert. Die WPF verwendet drei verschiedene Mechanismen zum Event Routing, d.h., auf welchem Wege Events durch den Elementbaum durchgereicht werden. Auf direktem Wege werden Ereignisse z. B. wie bei Windows Forms verarbeitet, d. h. von genau dem Element, bei dem das Ereignis aufgetreten ist – und nur von diesem Element. Sogenannte Bubbling Events (Bubbling = sprudeln, blubbern) werden vom Erzeuger jeweils an das nächste übergeordnete Elternelement weitergereicht, das nun ebenfalls darauf reagieren kann. Ein typisches Beispiel ist das Ereignis Click. Der andere Weg wird über Tunneling Events (getunnelte Ereignisse) gegangen. Hier wird zuerst das Wurzelelement des gesamten Elementbaums über das Ereignis informiert. Dann wird das Ereignis in entgegengesetzter Richtung bis zum Auslöser durchgereicht. Die Ereignisnamen werden bei getunnelten Events mit dem Präfix Preview versehen, allerdings ist diese Namensgebung keine Pflicht (z. B. bei der Erstellung eigener Ereignisse). Auf diese Weise kann z. B. ein übergeordnetes
76
Ereignisbehandlung
Element sämtliche Tastatureingaben überprüfen, bevor sie im konkreten untergeordneten Element verarbeitet werden. Die Abbildung 4.2 zeigt die Ereignisweiterleitung am Beispiel eines Klicks auf einen Button. Das erste Ereignis, das ausgelöst wird, ist ein getunneltes Ereignis, das beim Wurzelelement des Fensters startet. Danach wird es an den Layoutcontainer weitergegeben und zuletzt an den Button. Vom Button startet dagegen ein Bubbled Event. Wie Wasserblasen blubbert es zunächst zum StackPanel und von dort zum Fenster, dem Wurzelelement. Die TextBox bekommt von alldem nichts mit, da sie sich nicht innerhalb des Elementbaums zwischen Button und Wurzelelement befindet. Routed Events sind demnach eine Spezialform von Ereignissen in der WPF. Es werden hier alle Handler aufgerufen, die sich bei dem Ereignis registriert haben und zwischen dem Element, bei dem das Ereignis aufgetreten ist, und der Wurzel liegen.
Abbildung 4.2: Ereignisweiterleitung
Die meisten RoutedEvents haben auch ein korrespondierendes Preview-Event. So hat z. B. ein gebubbeltes MouseLeftButtonDown-Ereignis ein korrespondierendes getunneltes Ereignis PreviewMouseLeftButtonDown. Beide basieren auf demselben Ereignistyp MouseButtonEventHandler. Einige spezielle Ereignisse sind vom Typ Direct wie z. B. die Ereignisse MouseEnter und MouseLeave. Diese Ereignisse werden direkt an der auftretenden Komponente verarbeitet (oder auch nicht). Sie werden weder gebubbelt noch
77
Kapitel 4
getunnelt. Bei den beiden hier erwähnten Ereignissen wäre das auch nicht sonderlich sinnvoll, denn das Ereignis MouseEnter soll ja gerade dann eintreten, wenn die Maus eine ganz bestimmte Komponente beginnt zu überfahren. Erwähnenswert ist auch das Ereignis Click. Dieses Ereignis wird in der Klasse ButtonBase bereitgestellt und basiert auf den Ereignissen MouseLeftButtonDown und MouseLeftButtonUp bzw. wird in diesen Ereignissen ausgelöst. Es ist ein Bubbled Event und besitzt kein äquivalentes getunneltes Event. Da es in den Ereignissen MouseLeftButtonDown und MouseLeftButtonUp ausgelöst wird und diese eigentlich ersetzen soll, wird es als behandelt markiert. Das heißt, die beiden Ereignisse MouseLeftButtonDown und MouseLeftButtonUp werden nicht bis zur Wurzel »durchgebubbelt«. Um festzustellen, von welchem Typ ein Ereignis ist, verwenden Sie die MSDN-Hilfe. Im Abschnitt Routed Event Information werden der Ereignistyp, die Routing-Strategie sowie der Typ des Delegates angegeben. Gibt es ein korrespondierendes Ereignis, wird dies unter den Standardinformationen ebenfalls angegeben. Kann außerdem eine Methode überschrieben werden, die beim Auftreten des Ereignisses aufgerufen wird, steht diese ebenfalls hier.
Abbildung 4.3: Informationen zum Ereignistyp ermitteln
BEISPIEL AUF DER CD Zur Veranschaulichung der Ereignisweiterleitung verschachtelt das Beispiel einige Komponenten. Insbesondere besteht der oben angedeutete Button aus verschiedenen Komponenten. Wird nun beispielsweise auf das Label mit dem Text Hallo geklickt, ergibt sich ein recht umfangreicher Ereignisfluss. Die Komponenten werden mit jeweils einem Bubbled- und einem Tunneled-Ereignis verknüpft. Außerdem werden das Window-, das äußere StackPanel- sowie das Button-Element noch mit dem Ereignis Click verknüpft. Im Ergebnis sehen Sie nach einem Klick auf den Text Hallo die Ereignisfolge aus . Insbesondere fällt dabei auf, dass plötzlich nach den MouseDown-Ereignissen Click-Ereignisse auftreten. Dies ist genau die Stelle, an welcher der Button das Klicken mit der linken Maustaste in ein Click-Ereignis umwandelt und das originale Ereignis als behandelt (Handled=true) markiert.
Den Ereignisfluss unterbrechen Über die Eigenschaft Handled des Ereignisparameters können Sie durch das Zuweisen von true jederzeit die weitere Verarbeitung beenden. Wird z. B. beim ersten auftretenden getunnelten Ereignis Handled auf true gesetzt, werden weder weitere getunnelte noch gebubbelte Ereignisse ausgelöst. Der Ereignisfluss ist somit unterbrochen. private void TunnelClick(object sender, MouseButtonEventArgs e)
{ e.Handled = true; }
Aber auch hier gibt es wieder einmal keine Regel ohne Ausnahme. Wenn Sie beispielsweise bei dem Button des letzten Beispiels auf das Ereignis MouseDown reagieren möchten, stehen Ihnen zwei Möglichkeiten offen: Sie fangen das getunnelte Ereignis am Button ab und reagieren auf diese Weise auf das Ereignis. Sie registrieren im Code über die Methode AddHandler()einen Ereignishandler, wobei diese Methode von der Klasse UIElement geerbt wird. Diese besitzt eine Spezialform, die nicht über die einfache Ereignisverknüpfung mit += zur Verfügung steht. Neben dem Ereignistyp und dem Delegate auf die Handler-Methode kann im optionalen dritten Parameter (handledEventsToo) durch Übergabe von true angegeben werden, dass auch schon behandelte Ereignisse verarbeitet werden sollen. Als Nebeneffekt wird das Ereignis auch wieder an die übergeordneten Elemente weitergegeben.
80
Ereignisbehandlung
BEISPIEL AUF DER CD Das Beispielprojekt RoutedEventsProj wird nun etwas erweitert. Im Konstruktor der Fensterklasse wird am Button mit dem Namen ButtonAussen ein Handler für das unterschlagene MouseDownEreignis registriert. Wird die Anwendung jetzt neu gestartet und auf den Text Hallo geklickt, erscheinen nun auch alle MouseDown-Ereignisse in der Liste.
Abbildung 4.5: Erweiterter Ereignisfluss public Window1()
{ InitializeComponent(); ButtonAussen.AddHandler(MouseDownEvent, new MouseButtonEventHandler(RoutedClick), true); } Listing 4.12: Beispiele\Kap04\RoutedEventsProj\Window1.xaml.cs
Gemeinsame Verarbeitung Getunnelte Ereignisse werden vom übergeordneten Element bzw. dem Wurzelelement bis hin zum tatsächlichen, das Ereignis auslösenden Element durchlaufen. Damit kann in einem übergeordneten Element eine zentrale Behandlung für Ereignisse erfolgen. Ein Problem gibt es aber dabei. Wie soll herausgefunden werden, welches Element nun letztendlich das Ziel des Ereignisses ist? Dazu wird vom Ereignis die Eigenschaft Source ausgewertet. Der Parameter sender vom Typ objekt liefert in den Ereignishandlern die Komponente, bei welcher der Ereignishandler registriert ist. Der letzte Parameter in einem Ereignishandler ist bei Routed Events in jedem Fall von der Klasse RoutedEventsArgs abgeleitet (z. B. auch MouseLeftButtonDown). Diese Klasse definiert einige interessante Eigenschaften (wie z. B. Source), die in jeder Situation zur Verfügung stehen. Der Vorteil dieser
81
Kapitel 4
Basisklasse liegt auch darin, dass Ereignishandler für unterschiedliche Ereignisse bereitgestellt werden können, solange Sie sich nur auf den Typ RoutedEventsArgs im Parameter beschränken. Eigenschaft
Beschreibung
Handled
Wenn true, gilt das Ereignis als verarbeitet, ansonsten wird es weitergeleitet.
OriginalSource
Liefert die tatsächliche, originale Quelle für ein Ereignis.
RoutedEvent
Enthält den Ereignistyp, der das Ereignis ausgelöst hat. Dies ist dann nützlich, wenn ein Handler mehrere Ereignisse verarbeitet, welche die gleiche Signatur im Ereignishandler besitzen.
Source
Über diese Eigenschaft wird der Empfänger bzw. der Auslöser des Ereignisses definiert.
Tabelle 4.3: Eigenschaften des Typs RoutedEventArgs
BEISPIEL AUF DER CD Das folgende Beispiel zeigt nach einem Klick auf einen Button sämtliche Informationen an, die in einem Ereignishandler ausgewertet werden können, z. B. die auslösende Komponente, den Namen der Komponente und den Ereignistyp.
Abbildung 4.6: Informationen zu einem RoutedEvent auswerten
Ereignishandler können an mehreren Komponenten gemeinsam verwendet werden. Allerdings müssen sie dazu mit jeder einzelnen Komponente verknüpft werden. Eine besondere Form der gemeinsamen Verarbeitung von Ereignissen ist die Verwendung von Ereignishandlern in über- oder untergeordneten Elementen, die aber Ereignisse für andere Elemente verarbeiten – im Spezialfall sogar Ereignisse, die sie selbst gar nicht besitzen. Diese Form der Ereignisverarbeitung wird auch als Attached Events (angehängte Ereignisse) bezeichnet. Im Code kann diese Zuordnung über die Methode AddHandler() erfolgen. Darüber können beliebige RoutedEvents an Komponenten registriert werden, auch solche, die von der Komponente nicht direkt unterstützt werden. In diesem Fällen muss lediglich der vollständige Ereignistyp angegeben werden, z. B. ButtonBase.Click. Das StackPanel im folgenden Beispiel stellt beispielsweise einen Ereignishandler für alle untergeordneten Buttons bereit. Dadurch werden Ereignisse abgefangen, die auf diese Weise im StackPanel gar nicht verarbeitet werden, denn das StackPanel verfügt über kein Click-Ereignis. Aus diesem Grund muss auch der Typ des Ereignisses vollständig angegeben werden. Button 1 ...
Eine andere Schreibweise wäre die Angabe der Klasse ButtonBase, die das Click-Ereignis bereitstellt und an die Klasse Button vererbt. Button 1 ...
83
Kapitel 4
BEISPIEL AUF DER CD In der XAML-Datei werden drei Buttons innerhalb eines Stack-Panels eingefügt. Die Ereignisbehandlung wird zentral im übergeordneten Stack-Panel vorgenommen. Button 1 Button 2 Button 3 Listing 4.15: Beispiele\Kap04\BubbleEreignisProj\Window1.xaml
Innerhalb der Ereignisbehandlung wird lediglich der Auslöser identifiziert und dessen Name ausgegeben. private void HandleButtonClick(object sender, MouseButtonEventArgs e)
BEISPIEL AUF DER CD Ein weiteres Beispiel zeigt die Verwendung eines übergeordneten Handlers, der erstens dynamisch bereitgestellt wird und andererseits die Weiterleitung des Ereignisses steuert. Dazu werden in einem StackPanel drei TextBox-Elemente eingefügt. Das StackPanel verarbeitet für alle TextBoxen das PreviewKeyDown-Ereignis. Wird ein Buchstabe in eine beliebige TextBox eingegeben, wird dies bereits beim getunnelten Ereignis bemerkt und die Verarbeitung des Ereignisses als erledigt betrachtet. Im Konstruktor der Klasse Window1 wird dem StackPanel SpNumbers über die Methode AddHandler() ein Ereignishandler für das PreviewKeyDown-Ereignis hinzugefügt. Innerhalb der Methode CheckNumbers() wird die Eigenschaft Key auf den Wertebereich 0..9 und die (Tab)-Taste geprüft. Aufgrund des Ergebnisses wird der Wert der Eigenschaft Handled gesetzt.
84
Ereignisbehandlung Listing 4.17: Beispiele\Kap04\ZahlTextBoxenProj\Window1.xaml public Window1()
Grundsätzlich spielt es keine Rolle, wenn Sie eine Komponente in einem Container positionieren bzw. eine solche vererbte Eigenschaft nutzen, die Komponente sich dann aber gar nicht in einem solchen Container befindet. Es könnten höchstens unangenehme Nebeneffekte entstehen, wenn die Komponente von einem Container in einen anderen verschoben wird und plötzlich die »verborgenen« Eigenschaften greifen.
5.2 Übersicht der Layoutcontainer Die WPF stellt bereits eine ausreichende Anzahl von Layoutcontainern bereit. Einige können zur Verwaltung beliebiger UI-Elemente herangezogen werden, andere stehen nur in einem bestimmten Kontext zur Verfügung. Die folgende Tabelle beinhaltet die Container, die zur Positionierung beliebiger Komponenten genutzt werden können. Layoutcontainer
Beschreibung
Canvas
Die Komponenten in einem Canvas werden an der angegebenen Position in der über die Komponente festgelegten Größe angezeigt.
DockPanel
Die enthaltenen Komponenten können an den Rändern oder den Innenbereich ausfüllend positioniert werden.
Grid
Ein Grid stellt ein Tabellengitter zur Verfügung, in dessen Zellen Komponenten positioniert werden können (entspricht dem TableLayoutPanel von .NET 2.0).
Panel
Diese abstrakte Klasse stellt die Vorlage für die vordefinierten und eigenen Layouts dar.
StackPanel
Hierin können Elemente vertikal oder horizontal gestapelt werden, d.h., sie werden unter- oder nebeneinander angeordnet.
UniformGrid
Stellt die enthaltenen Komponenten in einem Gitter mit gleich großen Zellen dar. Die Größe des Gitters wird dabei dynamisch bei Bedarf erweitert.
VirtualizingPanel
Über diese abstrakte Klasse können Container erzeugt werden, die sehr effizient eine variable Anzahl Elemente aufgrund einer dahinter liegenden Datenstruktur anzeigen.
VirtualizingStackPanel
Dies ist eine konkrete Implementierung eines virtuellen Panels und ordnet die aktuelle Auswahl wie ein StackPanel vertikal oder horizontal an.
WrapPanel
Mittels dieses Containers werden die enthaltenen Elemente vertikal oder horizontal angeordnet. Reicht die Breite oder Höhe zur Anzeige aller Komponenten nicht aus, wird die Anzeige darunter oder daneben fortgesetzt (entspricht dem FlowLayoutPanel von .NET 2.0).
Tabelle 5.2: Übersicht der Layoutcontainer
94
Layoutcontainer
Es gibt noch weitere Panels, die aber nur in einem bestimmten Kontext verwendet werden können. Bei diesen Panels handelt es sich um das TabPanel, ToolBarOverflowPanel und ToolBarPanel. Weitere Containerkomponenten sind z. B. der Frame (zur Anzeige von XAML-Code) und die Viewbox (zur Dehnung/Skalierung der enthaltenen Komponente).
5.2.1 Canvas Das sicherlich für erfahrene Windows-Programmierer am einfachsten zu handhabende Panel ist das Canvas (Leinwand). Die Positionierung erfolgt über absolute Koordinaten in einem kartesischen X-Y-Koordinatensystem. Dazu stellt das Canvas den darin eingefügten Komponenten die Attached Properties Left, Right, Top und Bottom zur Verfügung. Es kann dabei immer nur ein Paar aus Right oder Left und Top oder Bottom verwendet werden. Werden beispielsweise die Eigenschaften Left und Top verwendet, wird die Komponente immer links oben verankert. Verwenden Sie stattdessen die Eigenschaften Bottom und Right, wird die Komponente immer rechts unten ausgerichtet. Werden dennoch alle Eigenschaften angegeben, überwiegen Left und Top vor Right und Bottom. Im Gegensatz zur Eigenschaft Anchor in einem Windows Form bedeutet dies also, dass durch die Angabe von Left und Right keine Verankerung an beiden Seiten stattfindet. Da jede Komponente in ihrer vorgegebenen Größe dargestellt wird, kann es auch zu Überschneidungen kommen. Die initiale Reihenfolge der Komponenten ergibt sich dabei durch die Reihenfolge beim Einfügen der Komponenten in die Children-Auflistung bzw. die Reihenfolge in der XAML-Datei.
!
!
!
ACHTUNG
Damit ein Canvas sichtbar ist, muss je nach Einsatz dessen Breite und Höhe mit einem Wert belegt werden, sofern diese nicht automatisch durch den umgebenden Layoutcontainer gesetzt werden. Ansonsten kann es sein, dass als Standardmaße 0,0 verwendet wird und das Canvas dadurch nicht sichtbar ist.
BEISPIEL AUF DER CD Das Beispiel verwendet ein Fenster, dessen Standard-Layoutcontainer ein Canvas ist (Hintergrund hellgelb). Darin werden weitere Canvas-Elemente eingefügt, die verschiedene Hintergrundfarben verwenden. Außerdem kommen noch zwei Buttons hinzu. Die Canvas überdecken sich standardmäßig anhand ihrer Einfügereihenfolge. Beim Klick auf den linken, gedrehten Button (Transformationen werden später noch ausführlicher behandelt) wird über C#-Code ein weiteres Canvas eingefügt (1). Der rechte Button zeigt die Verwendung der Attribute Right und Bottom in Aktion. Wird das Fenster vergrößert oder verkleinert, bleibt seine rechte untere Position unverändert.
95
Kapitel 5
Abbildung 5.2: Anordnung von Komponenten in einem Canvas
In der Ereignisbehandlung des linken, unteren Buttons wird ein weiteres Canvas über C#-Code in der Code-Behind-Datei der XAML-Datei erzeugt. Dazu muss der Namespace System.Windows.Controls eingebunden werden. Der Code hat zwei Besonderheiten. Die Attached Properties Top und Left werden nicht direkt über das Canvas-Objekt, sondern über entsprechende Methoden der Canvas-Klasse gesetzt. Der erste Parameter ist das betreffende Objekt, für das die Einstellung gesetzt werden soll, der zweite Parameter ist der Wert selbst. Über die Methode SetTop() wird also die obere Position des Elements im übergeordneten Canvas gesetzt. Zum Abschluss muss das neue Canvas
96
Layoutcontainer
(oder sonstige Komponenten) in die Children-Liste über die Methode Add() aufgenommen werden. Um die Eigenschaft ZIndex zu setzen und das Canvas damit in den Vordergrund zu bringen, wird die Methode ZIndex() verwendet, siehe folgender Abschnitt. private void AddNewCanvas(object Sender, RoutedEventArgs e)
Der Z-Index In den Komponenten, die in einem Canvas enthalten sind, kann in XAML über das Attribut ZIndex die Reihenfolge innerhalb des Canvas, d. h. die Überdeckung, konfiguriert werden. Umso höher der Wert dieser Eigenschaft ist, umso weiter vorn liegt die Komponente (die Komponente mit dem ZIndex 3 überdeckt z. B. die Komponenten mit dem ZIndex 1 und 2). Es sind auch negative Werte und Lücken zwischen den Werten erlaubt. Es gilt einzig, dass ein höherer Index weiter vorn liegt. Haben mehrere Komponenten denselben ZIndex, werden sie entsprechend der Einfügereihenfolge eingefügt, d.h., die zuletzt eingefügte Komponente überdeckt die anderen. Es werden dabei beide Schreibweisen in den Unterelementen eines Canvas, nämlich Panel.ZIndex oder einfach nur ZIndex akzeptiert. Allerdings ist die letzte Schreibweise
nicht ganz korrekt, sodass das Attribut mit einer Wellenlinie im Visual Studio markiert wird.
BEISPIEL AUF DER CD Es wird noch einmal das vorherige Beispiel herangezogen und einfach das Attribut Panel.ZIndex bei einigen Unterelementen des äußeren Canvas (die Unterelemente sind hier wiederum CanvasKomponenten) gesetzt. Da z. B. der ZIndex des zweiten, roten Canvas mit 1 belegt ist und damit den niedrigsten Wert aufweist, liegt diese Komponente hinter allen anderen Canvas, die einen höheren ZIndex besitzen.
Das Canvas stellt für Windows Forms-Entwickler den einfachsten Einstieg in die Verwendung von Layoutcontainern in der WPF dar. Obwohl aufgrund der anderen Layoutcontainer jetzt häufiger das automatische Layout favorisiert werden sollte, stellt das Canvas im Windows-Bereich ausreichend Funktionalität bereit. Werden allerdings Funktionalitäten wie der beidseitige Anker benötigt, müssen Sie z. B. auf ein Stack- oder DockPanel ausweichen.
Clipping Da Sie innerhalb eines Canvas absolute Koordinaten für die Positionierung der Komponenten verwenden können, lassen sich Komponenten auch über die vom Canvas eingenommene Fläche hinaus positionieren. In der Standardeinstellung können dadurch die Komponenten benachbarte Panel überlagern, vorausgesetzt die Einfügereihenfolge erlaubt es.
98
Layoutcontainer
Steuern lässt sich dies über die Eigenschaft ClipToBounds, die standardmäßig den Wert false besitzt, d.h., es wird kein Clipping durchgeführt. Setzen Sie dagegen den Wert auf true, werden Komponenten, die über den Rand des Canvas hinausgehen, abgeschnitten. In anderen Layoutcontainern wird dies durch die Layoutlogik des Containers verhindert, zumal darin keine Angaben zur Positionierung gemacht werden können, die eine Anzeige der enthaltenen Komponenten außerhalb des Containers erlauben würden.
BEISPIEL AUF DER CD Innerhalb eines StackPanels werden drei weitere Layoutcontainer eingebettet. Dabei wird explizit die Höhe der einzelnen Container mit 30 festgelegt. Im ersten Canvas wird nun ein Button eingefügt und dessen obere Position mit einer negativen Koordinate festgelegt. Dadurch ragt der Button in das darüber liegende StackPanel. Dies ist eine Besonderheit, die nur in einem Canvas möglich ist. Im zweiten Canvas wird genauso vorgegangen, allerdings wird hier im Canvas das Attribut ClipToBounds auf True gesetzt. Dadurch wird der Button an der Grenze des Canvas abgeschnitten.
Abbildung 5.4: Canvas ohne und mit aktiviertem Clipping
5.2.2 DockPanel Neben dem Grid und dem Stack-Panel, die beide noch vorgestellt werden, dient das DockPanel zur Definition des Grundlayouts von Anwendungen, die keine feste Positionierung wie beim Canvas verwenden. In einem DockPanel können die enthaltenen Komponenten links, rechts, oben und unten positioniert werden. Die zuletzt eingefügte Komponente nimmt standardmäßig den gesamten restlichen Teil ein, unabhängig davon, ob eine Dockingposition angegeben wurde oder nicht. Um diesen Automatismus zu deaktivieren, muss im DockPanel die Eigenschaft LastChildFill auf False gesetzt werden. In diesem Fall wird die Komponente entsprechend der angegebenen Dockposition (die jetzt Pflicht ist) positioniert. Der Standardwert für LastChildFill ist jedoch true, sodass die automatische Füllung voreingestellt ist. Wird für eine Komponente (ausgenommen der letzten) keine Dockingeigenschaft gesetzt, wird sie automatisch links angeordnet. In einem DockPanel lassen sich auch mehrere Komponenten an einem Rand hintereinander anordnen. Die Reihenfolge hängt wie bisher von der Einfügereihenfolge ab. Wird der Inhalt eines DockPanels verkleinert oder vergrößert, werden zuerst die äußeren Elemente sichtbar bzw. zuerst die inneren Elemente verdeckt, wenn nicht genügend Platz zur Verfügung steht. Die nicht durch das DockPanel verwalteten Größeneigenschaften einer Komponente (z. B. die Höhe bei der Positionierung oben durch die Verwendung des Attributs DockPanel.Dock="Top") bleiben also erhalten.
BEISPIEL AUF DER CD Das nächste Beispiel verwendet zur Veranschaulichung wieder Canvas-Komponenten, um die Positionierung in einem DockPanel zu zeigen. Das erste Canvas (1) wird durch Angabe des Attributs DockPanel.Dock="Top" oben angeordnet. Damit es sichtbar wird, muss dessen Höhe gesetzt werden. Das Canvas nimmt ohne Breitenangabe die gesamte Breite im DockPanel an der betreffenden Position ein. Wenn Sie allerdings auch die Breite einer Komponente setzen und diese oben oder unten ausrichten, wird auch diese berücksichtigt. Das zweite Canvas (2) wird links angeordnet. Seine Höhe wird allerdings durch das bereits oben eingefügte Canvas beeinflusst. Auf diese Weise werden noch weitere Canvas-Komponenten eingefügt. Da das Attribut LastChildFill standardmäßig den Wert True besitzt, füllt das letzte Canvas den restlichen Bereich aus.
Breite, Höhe und Ausrichtung Wenn ein Canvas oder eine andere Komponente an eine Seite eines DockPanels ohne Angabe einer Breite (oben/unten) oder Höhe (links/rechts) gedockt wird, wird sie in voller Breite angezeigt. Um dies zu verhindern, kann z. B. bei einer Anordnung oben im DockPanel die Breite explizit gesetzt werden. Diese wird in diesem Fall berücksichtigt, die Komponente standardmäßig zentriert angezeigt und der restliche Bereich ungenutzt (aber für die betreffende Komponente reserviert) gelassen. In der folgenden Abbildung wird ein Canvas oben in einem DockPanel angeordnet und mit einer festen Breite versehen. Wenn danach ein weiteres Canvas links angeordnet wird, reicht dessen obere Grenze nur bis zu der Stelle, an der das oben angeordnete Canvas sich bei voller Breite befinden würde (hier durch eine Linie gekennzeichnet).
Abbildung 5.6: Anordnung im DockPanel oben mit fester Breite
101
Kapitel 5 Listing 5.7: Verbindung von Layout und expliziten Größenangaben
Mittels des Attributs HorizontalAlignment kann bei einer geringeren Breite die Ausrichtung festgelegt werden. Standardmäßig erfolgt diese zentriert. Im folgenden Beispiel wird sie links durchgeführt. Analog erfolgt die Festlegung der vertikalen Ausrichtung (vgl. später).
Abbildung 5.7: Anordnung im DockPanel oben mit fester Breite und Ausrichtung links
... Listing 5.8: Verbindung von Layout, Größenangaben und Ausrichtung
Genauso verhält es sich beim Einfügen des letzten Elements, wenn die Eigenschaft LastChildFill den Wert true besitzt. Dann steht zwar der restliche Platz zur Verfügung, ist ihre Komponente aber »kleiner«, da Sie die Eigenschaften Width und Height belegt haben, wird die Komponente nur in dieser Größe eingefügt.
Codezugriff Um ein Canvas oder eine andere Komponente per C#-Code in einem DockPanel einzufügen, benötigt es weniger Anweisungen. Voraussetzung ist, dass das DockPanel über einen Namen (hier DpDockPanel) verfügt, über das es angesprochen werden kann. Alternativ besteht auch die Möglichkeit, sich durch den Komponentenbaum zu hangeln.
102
Layoutcontainer
Beispiel Im Projekt DockPanelProj.csproj wird über C#-Code ein weiteres Canvas (1) eingefügt. Dies würde natürlich auch mit einem Button oder einer TextBox funktionieren. Mittels der statischen Methode SetDock() der Klasse DockPanel wird die Attached Property Dock auf Left gesetzt, sodass das gelbgrüne Canvas links im verbleibenden Bereich eingefügt wird. Zum Abschluss wird das Canvas noch der Children-Auflistung des DockPanels hinzugefügt.
Abbildung 5.8: Über Code einem DockPanel eine Komponente hinzufügen public Window1()
5.2.3 Grid Mittels des Grid-Layoutcontainers haben Sie die flexibelste Lösung für die Herstellung eines Layouts zur Verfügung. Demgegenüber stehen der höhere Konfigurationsaufwand des Grids und die zusätzlich benötigte Rechenleistung zur Umsetzung des Layouts. Ein Grid (Gitter) unterteilt einen Bereich in Zeilen und Spalten wie in einer Tabelle. Im Ergebnis entstehen eine oder mehrere Zellen, in die jeweils eine Komponente, z. B.
103
Kapitel 5
erneut ein Container, platziert werden kann. Geben Sie keine Spalten- und Zeilendefinitionen an, besteht das Grid nur aus einer einzigen Zelle. Mehrere Elemente werden dann (ohne weitere Angaben) übereinander angeordnet, wobei sich das zuletzt eingefügte Element an oberster Position befindet und den gesamten Platz der Zelle einnimmt. Um mehrere Spalten und Zeilen in einem Grid zu erzeugen, wird ein entsprechender Bereich definiert. Unterhalb eines Grids wird zur Spaltendefinition der Bereich Grid.ColumnDefinitions begonnen. Darin wird für jede Spalte ein ColumnDefinitionElement eingefügt. Um mehrere Zeilen zu erzeugen, wird ein Grid.RowDefinitionsElement verwendet und darin für jede Zeile ein RowDefinition-Element angegeben. Die Reihenfolge der Spalten- und Zeilendefinitionen ist nicht vorgegeben. Der folgende Code erzeugt demnach eine Tabelle mit zwei Spalten und zwei Zeilen. Listing 5.10: Beispiel für eine Spalten- und Zeilendefinition eines 2x2-Gitters
Über weitere Attribute können Sie die Breite und die Höhe einer Zelle steuern. Die Angaben erfolgen entweder in geräteunabhängigen Pixeln, mit dem Wildcard-Zeichen * (Star) für »den Rest« oder der Zeichenfolge Auto für eine automatische Ausrichtung anhand der durch das Element benötigten Größe. Es können auch Verhältnisse bzgl. der Breite und Höhe gebildet werden, indem vor den Stern * Zahlen vom Typ double angegeben werden. Eine Angabe 2* bedeutet demnach, dass die Breite oder Höhe dem zweifachen Wert einer mit * markierten Spalte oder Zeile betragen soll. Bei einer Angabe in drei Spalten von *, 2* und 3* wird die gesamte Breite in sechs Einheiten unterteilt, wobei die erste Spalte eine Einheit, die zweite Spalte zwei Einheiten und die dritte Spalte drei Einheiten erhält. Da Gleitkommazahlen erlaubt sind, können Sie auch Verhältnisse der Form 2.5* und 3.5* schaffen. Um ein Element in eine Zelle des Grids einzufügen, verwenden Sie die Attribute (Attached Properties) Grid.Row und Grid.Column im betreffenden Element. Der Index läuft hier von 0 bis n–1. Werden in einer Komponente die Attribute Grid.Row="3" und Grid.Column="2" verwendet, wird sie in der vierten Zeile und dritten Spalte eingefügt. Werden keine Angaben zur Spalten- und Zeilenposition gemacht, wird die betreffende
104
Layoutcontainer
Komponente standardmäßig in die erste Spalte und erste Zeile, d. h. in die erste Zelle des Grids, eingefügt. Soll eine Komponente mehrere Spalten bzw. Zeilen umfassen, wird der Spalten- und Zeilenindex der linken oberen Zelle angegeben, und es werden außerdem die Eigenschaften Grid.ColumnSpan und Grid.RowSpan genutzt, um die belegten Spalten und Zeilen festzulegen.
BEISPIEL AUF DER CD Das Tabellengitter wird durch drei Spalten und drei Zeilen gebildet. Die Breite der ersten Spalte ist mit 100 Pixeln fest vorgegeben. Die Breite der letzten Spalte wird durch die Breite der »breitesten« Komponente bestimmt, hier ein einfacher Button mit einer Beschriftung. Der erste Button im XAML-Code wird in der zweiten Zeile eingefügt. Er nimmt die ersten beiden Spalten ein, da er in der ersten Spalte beginnt (Grid.Column="0") und zwei Zellen einnimmt (Grid.ColumnSpan="2"). Der zweite Button wird rechts unten eingefügt. Damit bestimmt seine Breite die Breite der dritten Spalte, denn es gibt keine weiteren Komponenten mehr in dieser Spalte. Zum Abschluss wird noch ein Button links oben eingefügt, dessen Breite sich durch die festgelegte Spaltenbreite ergibt. Allerdings könnte eine Breitenangabe im Button diese wiederum überschreiben.
Abbildung 5.9: Verschiedene Spaltenbreiten im Grid
Elemente in einer Zelle anordnen Innerhalb einer Zelle lassen sich durchaus auch mehrere Elemente anordnen. So kann in einer Zelle z. B. ein weiterer Layoutcontainer untergebracht werden, der eine erneute Unterteilung der Zelle vornimmt. Eine andere Möglichkeit steht in der Ausnutzung der Attribute HorizontalAlignment und VerticalAlignment zur Verfügung. So kann in einer Zelle ein Label beispielsweise links und eine Textbox rechts angeordnet werden.
Grids konfigurieren Zur Visualisierung des Layouts oder einer »echten« Tabellendarstellung können die Gitternetzlinien im Grid angezeigt werden. Setzen Sie dazu das Attribut ShowGridLines des Grids auf den Wert True. Die Linien werden standardmäßig gestrichelt dargestellt. Eine Konfigurationsmöglichkeit für die Gitternetzlinien besteht nicht, da dieses Features nur zu Testzwecken vorgesehen ist. Sinnvoll wäre es aber allemal, gerade bei der Definition einer »echten« Tabelle mit Überschrift und Einträgen.
Eine weitere Konfigurationsmöglichkeit besteht darin, die Breite der Spalten und die Höhe der Zeilen in mehreren Grids gemeinsam zu verwalten. Die betreffenden Grids müssen wiederum von einem Container eingeschlossen und dessen Attribut Grid. IsSharedSizeScope muss auf den Wert True gesetzt werden. Danach wird in den GridDefinitionen, in den Spalten und/oder Zeilen, die abgeglichen werden sollen, das Attribut SharedSizeGroup mit dem gleichen Spalten-/Zeilennamen angegeben. Der Spaltenname muss sich an die Anforderungen eines Bezeichners halten, d.h., er darf z. B. keine Leerzeichen enthalten. Besitzt nun ein Element eine feste Breite, wird diese Breite auch auf die Spalten oder Zeilen eines anderen Grids übertragen. Breiten- und Höhenangaben über das Wildcard-Zeichen * werden durch die Verwendung dieses Features ungültig und wie die Angabe Auto behandelt. Der Hintergrund eines Grids kann nur für das gesamte Grid gesetzt werden, also direkt im Grid-Element. Um eine oder mehrere Zellen anders zu färben, können Sie darin z. B. ein Canvas oder ein weiteres Grid unterbringen und es mit einer anderen Hintergrundfarbe versehen.
106
Layoutcontainer
BEISPIEL AUF DER CD In einem Stack-Panel werden untereinander zwei Grids angeordnet. Über das Attribut Grid. IsSharedSizeScope="True" im StackPanel-Element kann zwischen den beiden Modi umgeschaltet werden. Die beiden Spalten beider Grids werden über das Attribut SharedSizeGroup mit dem gleichen Namen versehen und damit in Beziehung gesetzt. Die Festlegung der Breite der beiden Buttons im ersten Grid wird später zur Breitenbestimmung der Spalten in beiden Grids herangezogen. Eine Breitenangabe in den -Elementen hätte dagegen keine Auswirkung. Zur besseren Kennzeichnung der Spaltenränder wird die Anzeige der Gitternetzlinien über das Attribut ShowGridLines in beiden Grids aktiviert.
Abbildung 5.10: Links sehen Sie die ausgeschaltete, rechts die aktivierte Größenanpassung
Zellgrößen ändern Die Breiten der Spalten und Höhen der Zeilen werden entweder durch die Column- und RowDefinition-Elemente festgelegt, oder sie ergeben sich aus den enthaltenen Inhalten. Mittels eines GridSplitters lassen sich diese auch über die Maus ändern. Für den GridSplitter sollten Sie dazu eine eigene Spalte oder Zeile bereithalten. Damit der Splitter angezeigt wird, setzen Sie z. B. die Breite auf die Anzahl der Spalten (ColumnSpan) und die horizontale Ausrichtung auf Center. Mittels der Eigenschaft ShowsPreview wird bei Übergabe von True eine Vorschau auf die mögliche Änderung gezeigt, bei Übergabe von False wird die Spaltenbreite bzw. Zeilenhöhe schon während des Ziehens mit der Maus geändert.
> >
>
HINWEIS
Ein GridSplitter muss nicht zwangsläufig in eine eigene Zelle eingefügt werden. Wenn Sie allerdings einen Splitter in eine bereits belegte Zelle einfügen, muss darauf geachtet werden, dass er sichtbar ist. Dies können Sie durch verschiedene Vorgehensweisen erreichen. Setzen Sie den Randabstand (Margin) der Hauptkomponente der Zelle auf einen Wert, sodass der Splitter am Rand sichtbar wird. Fügen Sie die Komponente als letzte ein, oder verwenden Sie die Eigenschaft ZIndex, um den Splitter in den Vordergrund zu bringen. Nachteilig an den beiden letzten Varianten ist allerdings, dass dadurch Teile der anderen Komponente überdeckt werden.
BEISPIEL AUF DER CD Das folgende Beispiel definiert in einem Grid drei Spalten und vier Zeilen. In der zweiten Spalte und dritten Zeile wird jeweils ein GridSplitter eingefügt. In den Gittern werden mehrere Canvas zur besseren Darstellung verwendet. Die Splitter werden außerdem für eine gesamte Spalte bzw. Zeile eingerichtet.
Abbildung 5.11: Spaltenbreite und Zeilenhöhe im Grid ändern
Benötigen Sie die Möglichkeit, alle Zellen in der Breite und Höhe mittels eines Splitters in ihrer Größe anzupassen, sollten Sie eine geeignetere Komponente dazu in Betracht ziehen (z. B. eines Drittherstellers oder eine eigene Implementierung). Ansonsten müssten Sie für jede Zeile und Spalte einen Splitter in das Grid einfügen.
Codezugriff Das Erstellen eines Grids im Code stellt etwas mehr Fleißarbeit dar als dieselbe Definition in XAML. Beim Setzen der Breite eines Grids in den Spaltendefinitionen bzw. der Höhe in den Zeilen muss beachtet werden, dass der Typ der Eigenschaften Width und Height der Klassen Column- und RowDefinition GridLength und nicht einfach double ist. Als erster Parameter im Konstruktor von GridLength wird der Zahlenwert, im zweiten der Typ der verwendeten Einheit angegeben. Die Einheiten werden über die Aufzählung GridUnitType festgelegt. Der Wert Auto bedeutet eine Anpassung der Spaltenbreite/-höhe an die enthaltenen Komponente, und der Wert Star entspricht dem Wildcard-Zeichen *. Der folgende Code erzeugt ein Grid mit drei Spalten und zwei Zeilen und fügt darin sechs Buttons ein. Zum Abschluss wird das Grid der Eigenschaft Content einer anderen Komponente, z. B. einem Window-Objekt, zugewiesen.
109
Kapitel 5
Abbildung 5.12: Über Code erzeugtes Grid mit Buttons in den Zellen
// Grid erzeugen Grid grd = new Grid(); // Spaltendefinitionen ColumnDefinition cd1 = new ColumnDefinition(); cd1.Width = new GridLength(2.3, GridUnitType.Auto); grd.ColumnDefinitions.Add(cd1); ColumnDefinition cd2 = new ColumnDefinition(); cd2.Width = new GridLength(100, GridUnitType.Pixel); grd.ColumnDefinitions.Add(cd2); ColumnDefinition cd3 = new ColumnDefinition(); cd3.Width = new GridLength(2, GridUnitType.Star); grd.ColumnDefinitions.Add(cd3); // Zeilendefinitionen grd.RowDefinitions.Add(new RowDefinition()); grd.RowDefinitions.Add(new RowDefinition()); // Zellen mit Buttons füllen for(int column = 0; column < 3; column++) { for(int row = 0; row < 2; row++) { Button btn = new Button(); btn.Content = column.ToString() + ", " + row.ToString(); Grid.SetColumn(btn, column); Grid.SetRow(btn, row); grd.Children.Add(btn); } } // Das fertige Grid als Inhalt eines Windows verwenden Content = grd; Listing 5.14: Grid im Code erzeugen
110
Layoutcontainer
5.2.4 StackPanel Die Elemente eines Stapels (Stack) werden über- oder nebeneinander angeordnet. Mittels des Attributs Orientation wird festgelegt, ob der Stapel die enthaltenen Elemente neben- oder untereinander anordnet. Standardmäßig ist die vertikale Anordnung eingestellt. Neue Elemente werden unterhalb des vorigen Elements in den Stapel eingefügt.
Abbildung 5.13: Positionierung in einem StackPanel
Standardmäßig werden die Komponenten dabei in voller Breite eingefügt, da der Standardwert der beiden Attribute HorizontalAlignment und VerticalAlignment der Wert Stretch ist. Um beispielsweise die Buttons nur in einer bestimmten Breite anzuzeigen, setzen Sie mindestens bei einem Button seine Eigenschaft Width auf einen festen Wert oder das Attribut HorizontalAlignment auf einen Wert ungleich Stretch. In der folgenden Abbildung wurde beispielsweise der Wert Center gewählt, sodass die Buttons in einer ausreichenden Breite (abhängig von ihrer Beschriftung) und zentriert dargestellt werden.
Abbildung 5.14: Zentrierte Darstellung im StackPanel
Stapeln heißt im Falle des StackPanels also, dass die Elemente nicht übereinander, sondern neben- oder untereinander angeordnet werden. Die Elemente werden beliebig hoch oder tief bzw. nebeneinander gestapelt. Es erfolgt kein automatisches Clipping, Umbrechen oder die Weiterführung in einer weiteren Spalte/Reihe (dafür gibt es speziell das WrapPanel, das gleich vorgestellt wird). Hat das Fenster eine zu geringe Breite oder Höhe, sind die Komponenten am Ende des Stapels nicht mehr sichtbar. Die Breite der Komponenten orientiert sich an den übergeordneten Komponenten, falls dazu keine expliziten Angaben gemacht werden.
111
Kapitel 5
BEISPIEL AUF DER CD Standardmäßig werden die Komponenten in einem Stack in voller Breite oder Höhe dargestellt, wenn keine weiteren Angaben gemacht werden. Die Anordnung in diesem Beispiel wird einerseits durch die Breite der TextBox und andererseits durch die Verwendung der Attribute HorizontalAlignment und VerticalAlignment beeinflusst. Beachten Sie, dass der Stapel zwar von unten nach oben wächst, neue Elemente aber nicht oben, sondern von unten angefügt werden.
Abbildung 5.15: StackPanel mit nach oben wachsendem Stack
Button 1 Button 3 Button 4 Das ist eine TextBox Listing 5.15: Beispiele\Kap05\StackPanelProj\Window1.xaml
> >
>
HINWEIS
Beachten Sie bei der Verwendung der Alignment-Attribute, dass deren Auswirkung je nach Einsatzgebiet unterschiedlich ist. Wird z. B. ein horizontales Alignment im StackPanel verwendet, werden alle Komponenten im StackPanel, d. h. der gesamte Stapel, links ausgerichtet. Dabei werden aber standardmäßig alle Komponenten in der gleichen Breite angezeigt. Wird dagegen die horizontale Ausrichtung in einem Button-Element verwendet, wird dieser Button innerhalb des StackPanels ausgerichtet und besitzt dann eine Größe entsprechend seines Inhalts.
Abbildung 5.16: Linke horizontale Ausrichtung beim Button 1
112
Layoutcontainer
Anzeige umfangreicher Daten Das StackPanel besitzt keinen eingebauten Mechanismus, um bei umfangreichen Daten Scrollbalken anzuzeigen. Mithilfe einer ScrollViewer-Komponente kann dies aber schnell nachgeholt werden. Dazu wird das StackPanel in einen ScrollViewer eingebettet und im ScrollViewer mittels der Eigenschaften HorizontalScrollBarVisibility und VerticalScrollBarVisibility die Anzeige der Scrollbalken konfiguriert. Die Eigenschaften können die Werte Auto, Disable, Hidden, Visible annehmen (automatisch, deaktiviert, verborgen, sichtbar). Achten Sie bei der Verwendung darauf, dass die vertikale Scrollbar standardmäßig sichtbar, die horizontale dagegen verborgen ist.
BEISPIEL AUF DER CD In diesem Beispiel wird im Projekt ein weiterer Ordner images angelegt, und darin werden acht Bilder von meinen Zwerghasen untergebracht. Diese werden innerhalb eines StackPanels in Form einer Bildergalerie eingefügt. Da sich das StackPanel in einem ScrollViewer befindet, kann nun der Inhalt des StackPanels gescrollt werden. Die Angabe der Eigenschaft VerticalScrollBarVisibility mit dem Wert Visible ist eigentlich nicht notwendig, da dies der Standardwert ist.
Abbildung 5.17: Bei umfangreichen Komponenten im StackPanel kann nun gescrollt werden.
5.2.5 UniformGrid Dieser Container wird häufig etwas stiefmütterlich behandelt, obwohl er eine einfache Möglichkeit darstellt, mehrere Komponenten in einem Gitter darzustellen. Eine Zeilenoder Spaltenzahl wie beim Grid muss nicht angegeben werden. Stattdessen wird diese dynamisch erhöht, wenn es notwendig ist. Die erste Komponente nimmt beispielsweise den gesamten Platz ein. Wird eine weitere eingefügt, wird diese in die nächste freie Zelle eingefügt. Ist keine Zelle mehr verfügbar, wird die Spalten- und Zeilenzahl um jeweils eins erhöht, siehe Abbildung 5.18.
Abbildung 5.18: Dynamische Erweiterung der Spalten- und Zeilenzahl
Das Verhalten kann aber noch etwas gesteuert werden. Setzen Sie den Wert der Eigenschaft FlowDirection auf den Wert RightToLeft, befindet sich die erste Komponente rechts oben, und es wird rechts beginnend aufgefüllt. Durch die Verwendung der Eigenschaft FirstColumn kann eine bestimmte Anzahl Zellen übersprungen werden, bevor das Auffüllen beginnt. Diese Einstellung hat allerdings erst dann eine Bedeutung, wenn die Eigenschaft Columns verwendet wird. Darüber kann die Anzahl fixer Spalten definiert werden. Mittels der Eigenschaft Rows lässt sich wiederum die Anzahl der Zeilen festlegen, die von Beginn an verwendet werden. Werden beispielsweise die Werte Columns="2" und Rows="2" verwendet, werden genau vier Zellen angezeigt. Fügen Sie allerdings fünf Komponenten ein, wird die letzte Komponente nicht mehr dargestellt. Hätte FirstColumn beispielsweise in der rechten Anzeige in Abbildung 5.18 den Wert 2 und Columns den Wert 3, würde der erste Button anstelle des Buttons mit der Beschriftung Hallo 3 dargestellt und die gesamte rechte Spalte einnehmen.
114
Layoutcontainer
BEISPIEL AUF DER CD In der XAML-Datei wird lediglich ein UniformGrid mit einem einzelnen Button als Inhalt erzeugt. Beim Klick auf den Button wird in der Methode AddButton() jeweils ein neuer Button erzeugt, beschriftet und im Grid eingefügt. Auf diese Weise erzeugen Sie durch vier Klicks die Darstellungsreihenfolge aus der . Hallo 1 Listing 5.17: Beispiele\Kap05\UniformGridProj\Window1.xaml private void AddButton(object sender, RoutedEventArgs e)
5.2.6 VirtualizingStackPanel Obwohl diese Klasse weder abstrakt ist noch direkt in einer anderen Komponente direkt eingesetzt wird, nutzt man dieses Panel oft im Zusammenhang mit der Anzeige umfangreicher Daten wie z. B. in einer ListBox (die intern auch tatsächlich dieses Panel verwendet). Den Namen verdankt das Panel zwei seiner wichtigen Eigenschaften. Die Daten werden wie in einem StackPanel angeordnet, also beispielsweise untereinander wie in einer ListBox. Der »virtuelle« Teil des Namens deutet an, dass immer nur ein Teil der im Panel enthaltenen Daten dargestellt wird. Im Falle einer ListBox also beispielsweise nur zehn von möglichen 10.000 Einträgen. Der Vorteil der Verwendung dieses Panels liegt in der performanten Darstellung der Einträge, wenn sich diese aus einer Datenbindung ergeben. Für die Anzeige werden nur die Einträge beachtet, die auch tatsächlich angezeigt werden müssen. Nicht mehr benötigte Elemente werden wieder zerstört, benötigte Elemente erzeugt. Um die Virtualisierung zu aktivieren, wird die Eigenschaft IsVirtualizing aktiviert. Im Falle einer ListBox ist dies bereits die Standardeinstellung. Wird der Wert auf False gesetzt, arbeitet die Anzeige der ListBox-Elemente wie ein einfaches StackPanel. Wenn Sie eine eigene Virtualisierung in einer Komponente zur Anzeige der Elemente implementieren möchten, ist das schon etwas mehr Aufwand, da z. B. die Berechnung,
115
Kapitel 5
welche Elemente aktuell angezeigt werden und welche Elemente temporär entsorgt werden können, von Ihnen erledigt werden muss.
BEISPIEL AUF DER CD Das folgende Beispiel zeigt die Daten einer XML-Dateninsel in einer ListBox an. Hier kommen bereits mehrere weitere Techniken zum Einsatz, wie z. B. das Data Binding. Dies ist notwendig, damit die Virtualisierung verwendet wird. In der XML-Dateninsel, die durch das Element eingeschlossen wird, werden die Daten von einigen Kunden verwaltet. Ebenso könnten die Daten auch aus einer externen XML-Datei stammen. Mit einer Datenvorlage (Data Template) wird die Anzeige eines Datensatzes in der ListBox konfiguriert. In der ListBox wird die Virtualisierung explizit aktiviert, was sich natürlich erst bei wesentlich größeren Datenmengen bemerkbar macht. Weiterhin wird der Inhalt der ListBox aus den XML-Daten bezogen (Attribut ItemSource) und als Vorlage für die Anzeige die Datenvorlage KundenListe (Attribut ItemTemplate) verwendet.
Abbildung 5.19: Die Elemente der ListBox werden über ein VirtualizingStackPanel angezeigt.
5.2.7 WrapPanel Ähnlich einem mehrzeiligen Fließtext, der bei ungenügendem Platz am Ende einer Zeile umbrochen wird, arbeitet das WrapPanel. In ein WrapPanel werden also mehrere Komponenten eingefügt, die dann, je nach Einstellung, horizontal oder vertikal nebeneinander angeordnet werden, bis für die benötigte Breite bzw. Höhe der letzten Komponente kein Platz mehr ist. Dann wird die Anordnung einfach auf der nächsten Zeile bzw. in der nächsten Spalte fortgesetzt. Die Komponenten in einem WrapPanel erben keine Attached Properties. Sämtliche Einstellungen zur Anordnung werden im WrapPanel durchgeführt. Im .NET Framework 2.0 wird durch das FlowPanel bereits eine solche Funktionalität angeboten.
BEISPIEL AUF DER CD Als Komponenten werden in diesem Beispiel einige Buttons verwendet, die in ein WrapPanel verpackt werden. Wird das Fenster verkleinert, werden die Buttons, die nicht mehr auf eine Zeile passen, in der Zeile darunter angeordnet. Passen nicht mehr alle Buttons in die Anzeige, wie im rechten Fenster der , werden sie auch nicht angezeigt. Eine Änderung/Anpassung der Größe der Komponenten findet in diesem Fall nicht statt. Eine Lösung wäre in diesem Fall wieder der Einsatz eines ScrollViewers.
Abbildung 5.20: Ändern der Fenstergröße in einem WrapPanel
Ausrichtung im WrapPanel Standardmäßig werden die Komponenten im WrapPanel beginnend mit der linken oberen Ecke im Container neben- oder untereinander angeordnet. Durch die Verwendung der Attribute HorizontalAlignment und VerticalAlignment kann das Wachstum ausgehend vom rechten unteren Rand gesteuert werden. Das heißt allerdings nicht, dass die zuerst eingefügte Komponente nun auch dort erscheint. Diese befindet sich immer noch rechts oben.
Abbildung 5.21: Ausrichtung im WrapPanel rechts unten und vertikale Erweiterung
Button 1 Button 2 Button 3 a b c Button 4 ... Button 9 Listing 5.21: WrapPanel mit Ausrichtung rechts unten
Nachteilig an der vorigen Lösung sind die unterschiedlichen Komponentengrößen. Diese könnten nun einzeln festgelegt werden. Alternativ werden über die Attribute ItemHeight und ItemWidth diese Angaben zentral direkt im -Element gesetzt.
118
Layoutcontainer
Abbildung 5.22: Feste Komponentengröße
Button 1 ... Listing 5.22: WrapPanel mit festen Elementgrößen
Die Krönung der Konfiguration und endlich auch der Beginn der Anordnung mit der ersten Komponente von rechts kann über das Attribut FlowDirection erreicht werden. Dieses Attribut kann die Werte LeftToRight und RightToLeft besitzen. Allerdings ist der Start mit der ersten Komponente von unten auch darüber nicht möglich.
Abbildung 5.23: Neue Komponenten werden rechts beginnend angefügt
Button 1 ... Listing 5.23: Fließrichtung von rechts nach links
> >
>
HINWEIS
Eine Lösung aus dem Dilemma, dass die erste Komponente nicht direkt rechts unten eingeordnet wird, bietet nur eine andere Einfügereihenfolge der Komponenten. Allerdings muss dies später auch bei Layoutänderungen im WrapPanel berücksichtigt werden.
119
Kapitel 5
5.3 Ausrichtung, Ränder und Innenraumabstand 5.3.1 Ausrichtung Die Layoutcontainer sind in erster Linie für die Standardausrichtung der Elemente verantwortlich. Oft können durch weitere Attribute noch zusätzliche Einstellungen vorgenommen werden, wie z. B. die Festlegung der Abstände zum nächsten Element oder dem Zellrand bei einem Grid. Mittels der Attribute HorizontalAlignment und VerticalAlignment legen Sie fest, ob ein Element in ihm zugewiesenen Bereich rechtsoder linksbündig (Right, Left), oben oder unten (Top, Bottom), ausfüllend (Stretch) oder zentriert (Center) dargestellt wird. Die einzelnen Werte stammen aus den gleichnamigen Aufzählungen HorizontalAlignment und VerticalAlignment aus dem Namespace System.Windows. Bei beiden Attributen ist der Standardwert Stretch. Wird bei einem mit den Attributen HorizontalAlignment und VerticalAlignment versehenen Element zusätzlich eine Breiten- bzw. Höhenangabe über die Attribute Width und Height durchgeführt, überwiegen diese Angaben die Verwendung bzw. der Vorgabe des Alignment-Wertes Stretch.
5.3.2 Randeinstellungen Der Außenrand Die Eigenschaft Margin dient der Festlegung von Rändern zum Nachbarelement oder dem Rand des umgebenden Layoutcontainers. Diese Randeinstellung betrifft immer den Außenrand der Komponente. Geben Sie nur einen Wert an, wird der Rand nach allen vier Seiten gleich angewendet, z. B. Margin="10". Werden zwei Werte, jeweils durch Komma getrennt, angegeben, setzt der erste Wert den linken und rechten Rand und der zweite Wert den oberen und unteren Rand (z. B. Margin="10, 20"). Geben Sie für jeden Rand eine separate Einstellung an, z. B. Margin="10, 20, 10, 20", setzen diese den Rand in der Reihenfolge links, oben, rechts, unten.
BEISPIEL AUF DER CD Ohne weitere Konfiguration werden die Komponenten in einem StackPanel vertikal von oben nach unten eingefügt. Durch die Angabe des Attributs HorizontalAlignment in den Button-Komponenten wird die Komponente nicht über die gesamte Breite angezeigt, sondern an der zugewiesenen Position, also rechts, mittig und links. Das letzte Element wird zwar ausgefüllt dargestellt, besitzt aber einen 10 Pixel breiten Rand nach allen Richtungen.
120
Layoutcontainer
Abbildung 5.24: Komponenten in einem StackPanel ausrichten
Der Innenrand Die Eigenschaft Padding schafft ebenfalls einen Rand an einer Komponente, der sich aber vom Außenrand zum Inhalt der Komponente erstreckt. Das Padding kann nur bei den Komponenten Block, Border und Control sowie davon abgeleiteten Komponenten (also auch bei einem Button oder einer TextBox) verwendet werden. Außen vor bleiben z. B. Container wie StackPanel oder WrapPanel. Möchten Sie einen Rahmen um einen Container festlegen bzw. die Eigenschaft Padding verwenden, setzen Sie im Container die Border-Komponente ein. Neben dem Padding kann auch noch ein zusätzlicher Rahmen über die Eigenschaft BorderThickness gesetzt werden. Da die Border-Komponente kein Container ist, muss darin wiederum ein Layoutcontainer eingefügt werden, z. B. ein Grid. Der Innenabstand kann gleichermaßen nach allen Seiten durch einen Wert oder für jeden Rand einzeln durch vier Werte für den linken, oberen, rechten und unteren Rand gesetzt werden.
121
Kapitel 5
Beispiel Der Rand (1) entsteht durch die Margin-Angabe im StackPanel. Er wird durch den weißen Hintergrund des umgebenden Window-Elements erzeugt. (2) stellt den Hintergrund des StackPanels dar. Dieser entsteht durch die Margin-Angabe im Border-Element. Der weiße Hintergrund (3) entsteht durch die Padding-Angabe im Border-Element. (4) entsteht wiederum durch die Margin-Angabe im Button, und der Button selbst erhält einen Innenraumabstand zum Text (5) durch die zusätzliche Padding-Angabe. Außenränder werden demnach von der umschließenden Komponente verwaltet, Innenränder von der aktuellen. Der Hintergrund von (6) stammt vom StackPanel.
Abbildung 5.25: Ränder über Margin und Padding einstellen
Klick mich Listing 5.25: Innen- und Außenrand konfigurieren
122
Layoutcontainer
BEISPIEL AUF DER CD In einem Grid mit blauem Hintergrund (2) werden einige Komponenten eingefügt. Das Grid selbst hat zum Fensterrand (1) einen linken und rechten Abstand von 30 und einen oberen und unteren Abstand von 10. Verkürzt ließe sich das also auch durch Margin="30,10" darstellen. Über die beiden Alignment-Attribute lassen sich in einer Zelle auch mehrere Komponenten anordnen (3, 4), vorausgesetzt es ist genügend Platz. In der zweiten Zelle der zweiten Zeile wird über ein Border-Element (5) ein gelber Rand für eine TextBox erzeugt. Die Größe des Randes wird durch ein Padding-Attribut festgelegt. Wird das Fenster zu sehr verkleinert, überwiegt das Border-Element, und die TextBox (6) wird nicht mehr bzw. ohne Inhalt angezeigt, da dieser nicht mehr dargestellt werden kann.
Abbildung 5.26: Innenränder über Padding einstellen
Reicht der Platz zur Darstellung der Elemente in einem Container nicht aus, kann wie bereits gezeigt ein ScrollViewer-Element eingesetzt werden, um im gesamten Inhalt zu scrollen. Zusammen mit den Layoutcontainern erreichen Sie damit eine optimale Anpassung und Ausrichtung der Fensterelemente. Dies spielt besonders dann eine Rolle, wenn eine Anwendung auf Displays mit unterschiedlichster Auflösung, z. B. 640 x 480 oder 1600 x 1200, noch einigermaßen gut dargestellt werden soll.
5.3.3 Codezugriff Der Zugriff auf die Ausrichtungseigenschaften über C#-Code besitzt eigentlich keine besonderen Eigenarten. Einige Einstellungen wie z. B. die Werte einzelner MarginBestandteile wie Margin.Bottom sind nur lesbar. Um die Werte zu setzen, muss der Margin-Eigenschaft einer Komponente ein Thickness-Objekt zugewiesen werden. Dieses besitzt drei Konstruktoren leer, keine Wertzuweisung bzw. 0 mit einem Parameter, der den Wert allen vier Margin-Bestandteilen zuweist mit vier Parametern, über die jeder Wert separat eingestellt werden kann Das bedeutet, dass die spezielle Schreibweise Margin="20, 20" mit der Verwendung von nur zwei Werten intern von XAML umgewandelt wird, da es keinen solchen Konstruktor gibt. Die gleiche Vorgehensweise ist auch beim Setzen des Paddings zu wählen. Zum Setzen der horizontalen oder vertikalen Ausrichtung des Elements innerhalb des Containers wird den Eigenschaften HorizontalAlignment bzw. VerticalAlignment ein statischer Wert der gleichnamigen Aufzählung HorizontalAlignment bzw. VerticalAlignment zugewiesen, z. B. VerticalAlignment.Bottom.
Beispiel Die folgende Anwendung enthält zwei Schaltflächen, die sich vorerst weitestgehend unformatiert in einem StackPanel befinden. Der erste Button enthält einen Ereignishandler, der beim Klicken darauf aktiviert wird.
124
Layoutcontainer
Beim Klick auf den ersten Button wird zuerst der Hintergrund des umgebenden Containers auf eine andere Farbe gesetzt. Danach werden hintereinander die Ausrichtungs- und Randeigenschaften Margin, HorizontalAlignment und Padding auf neue Werte gesetzt. Obwohl es so aussieht, als sei auch der zweite Button konfiguriert worden, liegt seine neue Lage nur an den neuen Randeinstellungen für den Button BtnTest.
Abbildung 5.27: Standarddarstellung und Änderung des Layouts über C#-Code
Um den Hintergrund des Panels zu ändern, in dem sich der Button BtnTest befindet, wird die Elternkomponente des Buttons bestimmt (Eigenschaft Parent), in ein Panel umgewandelt (hier muss natürlich sichergestellt werden, dass die Elternkomponente tatsächlich vom Typ Panel ist) und dann dessen Hintergrund geändert. private void BtnTestClick(object sender, RoutedEventArgs e)
{ ((Panel)btnTest.Parent).Background = Brushes.BlanchedAlmond; btnTest.Margin = new Thickness(10, 20, 10, 20); btnTest.HorizontalAlignment = HorizontalAlignment.Right; btnTest.Padding = new Thickness(10, 10, 30, 10); } Listing 5.27: Randeinstellungen und Ausrichtung festlegen
Layoutinformationen Über die Klasse LayoutInformation können einige Informationen zum Layout einer Komponente ermittelt werden. Mittels der statischen Methode GetLayoutSlot() wird beispielsweise ein Rect-Objekt für ein UIElement-Objekt zurückgegeben, das den für die Komponente reservierten Bereich im Layoutcontainer angibt.
125
Kapitel 5
BEISPIEL AUF DER CD Um den reservierten Bereich darzustellen, wird dieser nach dem Klick auf einen Button für genau diesen Button bestimmt, und die ermittelten Koordinaten 1erden in einer TextBox angezeigt. Außerdem wird um den Button ein gestricheltes Rechteck gezeichnet, das den Bereich noch einmal grafisch darstellt. Bei den zurückgelieferten Koordinaten müssen Sie darauf aufpassen, wie diese interpretiert werden. Da sich die Koordinaten auf das gesamte Grid beziehen, muss die Koordinate der linken, oberen Ecke auf 0 gesetzt werden, wenn das Rechteck in der gleichen Zelle eingefügt wird wie der Button. Die Koordinaten beziehen sich dann nämlich auf die Zelle.
Abbildung 5.28: Layoutinformationen in Textform und grafisch markiert
5.4 Komplexe Layouts erzeugen In der Regel reicht es für umfangreichere Anwendungen nicht aus, nur auf ein oder zwei Layouts zurückzugreifen. Meist müssen für ein gut designtes Fenster mehrere Layouts verwendet werden, die zudem ein- oder mehrfach ineinander verschachtelt sind. Damit sich bei Größenänderungen nicht die gesamte Anordnung aller Komponenten verschiebt, sollten auch einige feste Maßangaben verwendet werden bzw. sich einzelne Größen automatisch aus der Größe der enthaltenen Komponenten ergeben. Letzteres hat den Vorteil, dass Sie nahezu vollkommen unabhängig von benutzerdefinierten Einstellungen wie Schriftgröße etc. werden, da sich das Layout immer an die Gegebenheiten anpasst. Das folgende, etwas umfangreichere Beispiel zeigt die Entstehung eines Layouts, von der Skizze bis zum fertigen Fenster. In der Abbildung 5.29 sehen Sie die Skizze und welche Container zur Anordnung verwendet werden sollen. In der Abbildung 5.30 wird das fertige Fenster dargestellt.
5.5 Benutzerdefinierte Layouts Was wäre die WPF, wenn es nicht die Möglichkeit gäbe, eigene Layoutcontainer zu entwickeln! Neben den vordefinierten Layouts können Sie also auch individuelle Anordnungen erzeugen. Einen eigenen Container können Sie am einfachsten durch die Ableitung einer eigenen Klasse von der Klasse Panel oder einer davon abgeleiteten Klasse implementieren. Dazu müssen die Methoden ArrangeOverride() und MeasureOverride() überschrieben werden, die für die Umsetzung der Anordnung und die Berücksichtigung der durch die Komponenten gelieferten Maßangaben verantwortlich sind. Der Layoutvorgang wird durch zwei hintereinander folgende Vorgänge gesteuert, die jeweils alle Kindelemente des Containers berücksichtigen. Im ersten Durchlauf werden über einen Messvorgang mittels der Methode MeasureOverride() die von den Kindelementen benötigten Maße ermittelt. Der Methode wird der für den Container verfügbare Platz übergeben, wobei sich dieser später noch ändern kann. Werden dabei die Werte double.PositiveInfinity in den Eigenschaften Height und Width des SizeObjekts verwendet, heißt das so viel, als dass es vorerst keine Größenbeschränkung gibt. Dies betrifft später auch den Aufruf der Methode Measure() für die Kindelemente. Für jedes Kindelement aus der Auflistung Children wird nun die Methode Measure() aufgerufen. Dazu wird der Methode ein Size-Objekt übergeben, das den für das Kindelement verfügbaren Platz angibt. Später bei der Durchführung des Layouts können die Wunschmaße der Kindelemente getrost ignoriert werden. Dies hängt letztendlich von der Implementierung Ihres Layoutcontainers ab. Innerhalb der Methode Measure() kann das Kindelement seinerseits seine Ausmaße aktualisieren und stellt diese über die Eigenschaft DesiredSize zur Verfügung, die sofort im Anschluss ausgewertet werden sollte. Im zweiten Vorgang erfolgt die Anordnung der Kindelemente über die Methode ArrangeOverride(). Dazu müssen die Position und Größe jedes Kindelements berechnet und diesem dann die neue Lage und Größe über die Methode Arrange() mitgeteilt werden. Der Methode kann dazu zum Beispiel ein Rect-Objekt übergeben werden.
Von den beteiligten Komponenten werden deren Eigenschaften Width, Height, Margin und Style für das Layout berücksichtigt, wobei dies natürlich von der Implementierung des Layoutcontainers abhängt. Immer wenn eine Komponente neu gezeichnet werden muss, z. B. bei Veränderungen der Fenstergröße, wird das Layoutsystem aktiv. Bedenken Sie dies auch bei der Implementierung Ihres Algorithmus. Das Erstellen eigener Layouts erfolgt im einfachsten Fall wie im Folgenden gezeigt durch eine Klasse, die von der Klasse Panel abgeleitet wird. Die Verwendung des Layoutcontainers wird dann ebenfalls durch C#-Code demonstriert. Da es sich nicht um eine vollwertige Komponente handelt, kann sie nicht ohne Weiteres in einer XAML-
130
Layoutcontainer
Datei verwendet werden. Dazu ist dann doch mehr Aufwand notwendig (vgl. das Kapitel zur Erstellung eigener Komponenten).
BEISPIEL AUF DER CD Es soll ein neues Layout erzeugt werden, das die Komponenten erst in einer Reihe am oberen Rand anordnet und bei zu wenig Platz am unteren Rand fortfährt. Ist dort wiederum kein Platz mehr, geht es in der zweiten Reihe von oben weiter usw. Die Linien in der Abbildung zeigen den Beginn einer neuen Zeile an, die sich immer an der höchsten Komponente der vorigen Zeile orientiert.
Abbildung 5.31: Das neue Layout ordnet die Komponenten abwechselnd oben und unten an.
Die XAML-Datei enthält diesmal nur einen spärlichen Inhalt. Es wird darin die Größe des Fensters festgelegt und das Ereignis Loaded mit einem Handler verknüpft. Natürlich wäre auch die Angabe von einigen Komponenten möglich gewesen, diese sollen aber im C#-Code erzeugt werden. Listing 5.31: Beispiele\Kap05\ObenUntenCustomPanelProj\Window1.xaml
Im ersten Teil der Code-Behind-Datei wird der Code der Fensterklasse um den Handler OnLoad für das Ereignis Loaded erweitert. Darin wird zuerst eine Instanz des neuen Layoutcontainers ObenUntenPanel erzeugt. Dann werden in einer Schleife 15 Buttons erzeugt und mit unterschiedlichen, zufälligen Größenangaben versehen. Diese Buttons werden dann dem neuen Layoutcontainer hinzugefügt. Zum Abschluss wird der Eigenschaft Content des Fensters der Layoutcontainer zugewiesen. using System; using System.Windows; using System.Windows.Controls; namespace ObenUntenCustomPanelProj
{ ObenUntenPanel oup = new ObenUntenPanel(); Random rnd = new Random(); for(int i = 0; i < 15; i++) { Button b = new Button(); b.Content = "Test " + i.ToString(); b.Width = rnd.Next(30) + 60; b.Height = rnd.Next(20) + 20; oup.Children.Add(b); } this.Content = oup; } } } Listing 5.32: Beispiele\Kap05\ObenUntenCustomPanelProj\Window1.xaml.cs
Das neue Panel wird von der Klasse Panel abgeleitet. Zuerst wird die Methode MeasureOverride() überschrieben. Als Parameter wird der Methode der verfügbare Platz im Container übergeben. Dann werden alle Kindelemente der Children-Auflistung durchlaufen. Über die Methode Measure(), welcher der noch verfügbare Platz übergeben wird, werden die Kindelemente aufgefordert, ihrerseits den von ihnen benötigten Platz zu ermitteln. Zu beachten ist hier, dass der verfügbare Platz nur eine relative Angabe ist, z. B. wenn der Container Scrolling unterstützt. Des Weiteren wird nicht direkt innerhalb von Measure() der Size-Wert geändert. Stattdessen ermittelt man die vom Kindelement gewünschten Ausmaße über dessen DesiredSize-Eigenschaft. Um dem Kindelement völlige Freiheit zu seinen Größenanforderungen zu lassen, können als verfügbare Größe auch die Werte double.PositiveInfinity übergeben werden. Beim Rückgabewert ist zu beachten, dass er die gewünschten Ausmaße des Containers liefern soll. Allerdings ist der Wert double.PositiveInfinity nicht erlaubt, sodass der Parameter availableSize nicht einfach durchgereicht werden darf. Als Lösung kann z. B. der maximal von den Kindelementen benötigte Platz errechnet werden, indem Sie alle Höhen- und Breitenangaben addieren. public class ObenUntenPanel: Panel
Das Arbeitstier bei der Durchführung des Layouts ist die Methode ArrangeOverride(). Hier wird das Layout hergestellt, was zum Teil sehr umfangreiche Berechnungen erfordern kann. Als Parameter wird der Methode der zur Verfügung stehende Platz für die Anordnung der Komponenten übergeben. In einer foreach-Schleife werden nun alle Kindelemente durchlaufen. Zuerst wird geprüft, ob die noch verfügbare Breite zur Aufnahme der Komponente ausreicht. Falls nicht, wird der obere (nextPointTop) bzw. untere (nextPointBottom) Punkt zur Ausrichtung der linken oberen Ecke der nächsten Komponente an die Variable actPoint zugewiesen. Dann wird je nach aktueller Position (oben oder unten) zuerst die Komponente ausgerichtet und dann die Position für die nächste Zeile aktualisiert. Dabei muss berücksichtigt werden, dass die Komponente mit dem größten Wert in der Eigenschaft Height einer Zeile den Beginn der nächsten Zeile bestimmt. Nach der Fertigstellung des Layouts wird als Rückgabewert die eingenommene Größe zurückgegeben. Diese kann durchaus auch kleiner als der verfügbare Platz sein, z. B. wenn sich nur ein Button im Container befindet. protected override Size ArrangeOverride(Size finalSize)
Einige Beispiele für benutzerdefinierte Layouts werden bereits mit dem Windows SDK mitgeliefert. Das RadialPanel finden Sie unter ...\WPFSamples\Layout\RadialPanel.
Abbildung 5.32: RadialPanel aus dem Windows SDK
134
Layoutcontainer
Im Internet finden Sie sich ebenfalls einige Beispiele für Layouts von denen besonders das FishEyePanel und das FanPanel sehr anspruchsvolle Realisierungen darstellen, die frei genutzt werden dürfen. Unter http://www.codeproject.com/WPF/Panels.asp werden beide vorgestellt, der Code befindet sich ebenfalls auf der beiliegenden CD. Das FanPanel zeigt Bilder, die gedreht auf einem Haufen liegen und die beim Überfahren mit der Maus horizontal ausgerichtet werden. Klickt man dann auf den Haufen, öffnet sich ein Fenster, in dem die Bilder größer und innerhalb einer WrapPanels angezeigt werden. Beim Fischauge werden innerhalb einer horizontalen Bildergalerie das unter der Maus liegende und angrenzende Bilder hervorgezoomt.
Abbildung 5.33: FishEyePanel
135
6
Komponenten
6.1 Grundlagen Wie jedes Framework, das zur Erstellung von grafischen Benutzeroberflächen dient, stellt auch die WPF zahlreiche Standardkomponenten zur Verfügung. Diese reichen von normalen Schaltflächen hin zu TreeViews (Baumstrukturen) und speziellen Betrachtern für Textdokumente. Die WPF erweitert das Angebot von .NET 2.0 um einige neue Komponenten, ohne es dabei aber zu übertreiben. Die WPF implementiert dabei diese Komponenten selbst noch einmal. Wenn Sie also zukünftig die MSDN-Hilfe durchstöbern, achten Sie immer auf darauf, die WPF-Variante auszuwählen. Die Doppelung der Komponenten ist in der unterschiedlichen Klassenhierarchie und der unterschiedlichen Funktionsweise der Frameworks begründet. Die .NET 2.0-Komponenten unterstützen zum Beispiel keine Stile. Obwohl sich die Funktionsweise der Komponenten für den Benutzer in den meisten Fällen nicht ändert, unterscheidet sich die Handhabung der WPF-Komponenten für den Programmierer häufig von den Pendants der Windows Forms-Komponenten. Beispielsweise benötigt ein WPF-TreeView keine Image-List mehr, um vor den Einträgen Bilder anzuzeigen. Dies liegt wie auch bei den anderen Komponenten daran, dass der Aufbau jedes Eintrags frei konfigurierbar ist. So lassen sich in allen Listenkomponenten in den Einträgen beliebige Kombinationen von UI-Elementen integrieren, seien es komplexe Grafiken, Bilder oder wiederum andere Komponenten.
Kapitel 6
Die WPF-Komponenten lassen sich in die folgenden Kategorien einordnen, wobei diese Einordnung willkürlich von mir vorgenommen wird. Kategorie
6.1.1 Die Klasse Control Bis auf einige Ausnahmen sind die meisten Komponenten, die in der Benutzeroberfläche eingesetzt werden können, direkt oder indirekt von der Klasse System.Windows. Controls.Control abgeleitet und können deren Eigenschaften und Methoden nutzen. Beachten Sie insbesondere, dass sich gleichnamige Komponenten von Windows Forms auch im Namespace System.Windows.Forms befinden können. Dies sollte zumindest beim Durchstöbern der MSDN-Hilfe beachtet werden, die im Falle des Windows SDK beide Frameworks, also Windows Forms und das der WPF, enthält. Im Namespace System.Windows.Controls finden Sie außerdem zahlreiche weitere Klassen, die z. B. als Unterelemente in anderen Elementen eingesetzt werden. So dient ein GridView z. B. als Erweiterung eines ListViews, um eine tabellarische Darstellung zu ermöglichen.
Vererbungshierarchie Die meisten Kontrollelemente erben über zahlreiche Stufen. Als erste erwähnenswerte Klasse soll die Klasse System.Windows.Media.Visual betrachtet werden. Diese liefert z. B. die Unterstützung für das Hit-Testing, also ob eine Komponente angeklickt wurde, Clipping und die Darstellung (Rendering) der Komponente. Von dieser Klasse ist unter anderem die Klasse System.Windows.UIElement abgeleitet. Sie enthält die Funktionalität zur Durchführung des Layouts und bietet bereits zahlreiche Ereignisse und Eigenschaften an, z. B. für Maus- und Tastenoperationen. Die einzige davon abgeleitete Klasse System.Windows.FrameworkElement stellt weitere Basiseigen-
138
Komponenten
schaften, Methoden und Ereignisse zur Verfügung, z. B. die Eigenschaften Width und Height für Größenangaben, Style für die Definition eines Darstellungsstils oder die Methoden BeginInit() und EndInit(), die den Start- und Endzeitpunkt der Initialisierung des Elements kennzeichnen. Von dieser Klasse werden nun »endlich« die ersten wirklich verwendbaren Komponenten abgeleitet, z. B. Image, InkCanvas, Panel, aber auch die Klasse Control. Von Letzterer werden die Komponenten erweitert, die eine Interaktion mit dem Anwender ermöglichen, z. B. List- und TextBoxen. Dazwischen können wieder einige weitere Klassen liegen. Eigenschaft
Beschreibung
Background
Hintergrundfarbe
Cursor
Mauszeiger
FontFamily
Schriftart
FontSize
Schriftgröße
FontStyle
Schriftschnitt: Kursiv, Normal
Foreground
Vordergrundfarbe
Height/Width
Höhe und Breite
Margin
Außenrand
Name
Name der Komponente
Opacity
Transparenz
Padding
Innenrahmen
Parent
Übergeordnetes Element
Resources
Verweis auf ein Resource Dictionary
Style
Verweis auf eine Stildefinition
TabIndex
Index innerhalb der Tabulatorreihenfolge
Tag
Frei verfügbare Eigenschaft für individuelle Informationen
ToolTip
Objekt, das als Hinweisfenster an der Komponente angezeigt wird
Visbility
Steuert die Sichtbarkeit
Tabelle 6.2: Auswahl von Eigenschaften der Klasse Control
BEISPIEL AUF DER CD Mittels zweier TextBoxen und zweier ineinander verschachtelter Canvas-Elemente wird die Wirkungsweise einiger Eigenschaften der Klasse Control gezeigt. Wenn Sie die Maus über die TextBox mit dem Text Hallo bewegen, werden der eingestellte Mauszeiger und der Tooltipp angezeigt. Da die Eigenschaft Visibility der zweiten TextBox auf Hidden steht, wird sie in der Abbildung nicht angezeigt. Ändern Sie den Wert der Eigenschaft, lassen sich Komponenten abhängig vom Zustand der Anwendung ein- und ausblenden.
139
Kapitel 6
Abbildung 6.1: Einige Eigenschaften der Klasse Control verwenden
Die in diesem Kapitel vorgestellten Komponenten werden meist in ihrer Standardanzeige dargestellt. Da es die WPF gestattet, den Inhalt völlig neu zu gestalten, kann die Darstellung jeder Komponente auch völlig anders erfolgen.
6.2 Standardkomponenten Die hier als Standardkomponenten kategorisierten Komponenten stellen die Grundsteine einer grafischen Oberfläche dar. Sie stehen in nahezu jedem grafischen System zur Verfügung und sind in der Regel einfach zu konfigurieren.
140
Komponenten
Abbildung 6.2: Standardkomponenten Komponente
Beschreibung
Button
Schaltfläche, die eine Aktion ausführt.
CheckBox
Über diese Komponente stellen Sie unabhängig voneinander konfigurierbare Optionen bereit. Die Markierung einer CheckBox wird über die Eigenschaft IsChecked gesetzt (true – an, false – aus). Über die Eigenschaft IsThreeState kann noch ein Mittelwert hinzugefügt werden (Zuweisung von true). In diesem Fall entspricht der Wert null in der Eigenschaft IsChecked (oder der leeren Zeichenkette "" in XAML) einem Zwischenwert.
ComboBox
Enthält wie eine ListBox mehrere Elemente, von denen aber nur eins ausgewählt werden kann. Der angezeigte Inhalt steht über die Eigenschaft Text zur Verfügung. Darüber können auch neue Elemente eingefügt werden.
Label
Dient der Beschriftung anderer Komponenten. Der Text des Labels kann vom Benutzer nicht geändert werden.
ListBox
Eine ListBox enthält mehrere Einträge (vom Typ ListBoxItem), von denen einer oder mehrere ausgewählt werden können (Eigenschaft SelectionMode). Über verschiedene SelectedXXX-Eigenschaften kann das aktuell ausgewählte Element oder dessen Index bestimmt werden. Prüfen Sie immer, ob tatsächlich ein Eintrag ausgewählt ist, indem Sie z. B. die Eigenschaft SelectedIndex auf –1 und SelectedItem auf null prüfen.
Tabelle 6.3: Übersicht der Standardkomponenten
141
Kapitel 6
Komponente
Beschreibung
PasswortBox
Die Eingabe wird hier maskiert, d.h., statt der eingegebenen Zeichen wird ein Platzhalterzeichen ausgegeben. Das dazu verwendete Zeichen kann über die Eigenschaft PasswordChar geändert werden. Der tatsächlich eingegebene Text kann über die Eigenschaft Password ausgelesen werden.
RadioButton
Durch die Verwendung mehrere RadioButtons wird eine Auswahl mehrerer Optionen angeboten, von denen eine (oder keine) ausgewählt werden kann (z. B. die Bildschirmauflösung). Mittels der Eigenschaft IsChecked kann ein RadioButton aktiviert werden. Mittels der Eigenschaft GroupName können mehrere RadioButtons zusammengefasst werden, wobei immer nur einer ausgewählt werden kann. Dazu wird der Wert von GroupName bei allen betreffenden RadioButtons auf den gleichen Wert gesetzt.
ScrollBar
Eine ScrollBar stellt einen unabhängig verwendbaren Scrollbereich dar, der manuell mit dem zu scrollenden Inhalt verbunden werden muss. Dazu wird auf das Ereignis Scroll reagiert und der neue Wert des Schiebereglers aus der Eigenschaft NewValue des Parameters vom Typ ScrollEventArgs ausgewertet.
ScrollViewer
Diese Komponente kann den Inhalt eines Containers scrollen, der ihr über die Eigenschaft Content zugewiesen wird und nicht groß genug für die Anzeige seines Inhalts ist. Die Anzeige der Scrollbalken kann automatisch oder manuell erfolgen (Eigenschaften HorizontalScrollBarVisibility und VerticalScrollBarVisibility).
TextBox
Dient der Ein- und Ausgabe von Text. Über die Eigenschaft TextWrapping kann durch Übergabe der Werte NoWrap, Wrap und WrapWithOverFlow der Aufzählung TextWrapping eine einzeilige oder mehrzeilige TextBox erstellt werden. Durch das Setzen der Eigenschaft AcceptReturn auf true werden bei Eingaben von (Enter) Zeilenumbrüche erzeugt.
Tabelle 6.3: Übersicht der Standardkomponenten (Fortsetzung)
BEISPIEL AUF DER CD Das Beispiel zeigt den XAML-Code, der die Anzeige des Fensters aus bewirkt. Aus dem Quelltext wurde zur besseren Darstellung bei den Komponenten die Angabe der Eigenschaft Horizontal Alignment="Left", die zur linken Ausrichtung im übergeordneten StackPanel dient, entfernt (wie auch Margin="5" für die Verwendung eines Rahmens um die Komponenten herum). Geben Sie einige Buchstaben in die PasswordBox ein, wird das Fragezeichen als Platzhalter für jeden Buchstaben angezeigt. In der ComboBox wird neben zwei einfachen Texteinträgen auch ein Eintrag bestehend aus einer Grafik und einem Text hinzugefügt. Der ScrollViewer wird dazu verwendet, ein zu groß geratenes Canvas (600 x 200 Pixel), das einen Farbverlauf enthält, zu scrollen. Farbverläufe werden im folgenden Kapitel behandelt. Klick mich Geben Sie einen Text ein
6.3 Containerkomponenten Die Layoutcontainer Canvas, DockPanel, Grid, StackPanel und WrapPanel wurden bereits im vorigen Kapitel besprochen. Hier sollen jetzt weitere Komponenten vorgestellt werden, die im weiteren Sinne auch als Container für andere Komponenten dienen.
Zur Anzeige von Textdokumenten werden drei Anzeigevarianten zur Verfügung gestellt, die sich hauptsächlich in ihrer Ausstattung (Lupe, Suchfunktion, Seitenansicht usw.) unterscheiden. Diese Komponenten werden im Kapitel zur Textdarstellung genauer betrachtet.
Frame
Innerhalb eines Frames lassen sich andere XAML-Elemente anzeigen, die auch aus einer Datei geladen werden können. Frames unterstützen außerdem Navigationsmethoden, um neue Seiten (andere XAML-Dokumente) zu laden. Mehr zu Frames und Navigationsanwendungen erfahren Sie im gleichnamigen Kapitel.
Viewbox
Über eine Viewbox lassen sich XAML-Elemente strecken und skalieren, wobei die Viewbox genau ein Kindelement besitzen darf.
Tabelle 6.4: Übersicht der Containerkomponenten
BEISPIEL AUF DER CD Dieses Beispiel erzeugt die Ausgabe aus . Als Rahmen wird ein StackPanel zur Anordnung verwendet. Im FlowDocument wird hier nur ein einfacher Text angezeigt. Im Kapitel zur Textverarbeitung wird diese Komponente intensiver betrachtet. In den beiden Frames wird einmal eine externe Quelle in Form der Datei FrameContent.xaml verwendet, ein anderes Mal wird der Inhalt direkt in XAML bereitgestellt. Zum Abschluss werden in einer Viewbox zwei Ellipsen mit unterschiedlichen Stretch-Modi angezeigt.
144
Komponenten Dies ist ein FlowDocumentReader-Container. Bla Bla Bla Bla Bla Listing 6.3: Beispiele\Kap06\ContainerKomponentenProj\Window1.xaml
Lorem ipsum has indoctum principes ea, an tempor sapientem qui, illud eligendi tincidunt his et. Vel sale audiam ... Listing 6.4: Beispiele\Kap06\ContainerKomponentenProj\FrameContent.xaml
6.3.1 Viewbox Eine Viewbox kapselt ein Kindelement, das wiederum weitere Elemente enthalten kann. Ziel der Kapselung ist es, dass Sie mit einer ViewBox die Größe der eingebetteten Elemente automatisch an die Größe der Viewbox anpassen können. Dazu besitzt eine Viewbox die speziellen Eigenschaften Stretch und StretchDirection.
145
Kapitel 6
Der Eigenschaft Stretch, die festlegt, wie sich der Inhalt der Viewbox an deren Größe anpasst, können die Werte Fill, None, Uniform oder UniformToFill zugewiesen werden. Mittels der Eigenschaft StretchDirection steuern Sie, ob der Inhalt der Viewbox die Eigenschaft Stretch berücksichtigt (Both), ob der Inhalt kleiner skaliert wird, wenn er größer als die Viewbox ist (DownOnly), oder ob der Inhalt größer skaliert wird, wenn er kleiner als die Viewbox ist (UpOnly). Eine ViewBox hat selbst keine Hintergrundfarbe, sodass Sie diese mit einem eingebetteten Container oder der enthaltenen Komponente festlegen müssen.
BEISPIEL AUF DER CD Das Beispiel zeigt die Verwendung der verschiedenen Stretch-Werte. Dazu werden vier gleich große Viewbox-Objekte innerhalb eines StackPanels erstellt und horizontal dargestellt. Innerhalb der Viewbox-Objekte befindet sich eine Ellipse, die für die Anzeige innerhalb der kleineren ViewBox zu groß ist. Durch die unterschiedlichen Stretch-Werte kann die Ellipse mit und ohne Beibehaltung ihrer Proportionen in die ViewBox eingepasst werden.
Abbildung 6.4: Verschiedene Stretch-Werte zum Füllen des Bereichs
6.4 Dekorationskomponenten Die jetzt vorgestellten Komponenten dienen der Hervorhebung bzw. Einrahmung anderer Komponenten, um diese besser von anderen abzugrenzen. Dabei sind sie in der Lage, Rahmen oder Markierungen (Aufzählungszeichen) darzustellen.
Abbildung 6.5: Dekorationskomponenten
Komponente
Beschreibung
Border
Die Border-Komponente zieht einen Rahmen um das enthaltene Containerelement. Der Rahmen kann in Farbe/Füllmuster (BorderBrush), Breite (BorderThickness) und der Darstellung der Ecken (CornerRadius) angepasst werden. Außerdem können die Innenfarbe (Background) und der Innenabstand (Padding) eingestellt werden.
BulletDecorator
Dient der Anzeige von Aufzählungszeichen.
GroupBox
Wie auch die Border-Komponente besitzt eine GroupBox die Eigenschaften Background, BorderThickness, BorderBrush und Padding, allerdings keine Eigenschaft CornerRadius. Zusätzlich kommen die Eigenschaften Header zur Anzeige einer Beschriftung und HeaderTemplate zur Definition einer Vorlage zur Anzeige des Headers hinzu.
Tabelle 6.5: Übersicht der Dekorationskomponenten
BEISPIEL AUF DER CD Das Beispiel zeigt die Dekorationskomponenten aus. Damit sich die Komponenten etwas vom Innenrand des Fensters abheben, wurde der Randabstand über die Eigenschaft Margin auf 10 festgelegt. Damit Sie in den Komponenten mehr als eine untergeordnete Komponente unterbringen können, wird zuerst ein Layoutcontainer hinzugefügt.
6.4.1 Border Die zwei herausragenden Eigenschaften der Klasse Border sind Padding und CornerRadius. Mittels des Paddings kann der Innenabstand des Rahmens zu den enthaltenen Elementen festgelegt werden. Dies ist sicher immer sinnvoll, damit die Elemente nicht zu sehr an den Rand stoßen. Über die Eigenschaft CornerRadius legen Sie die Rundungen an den Ecken des Rahmens fest. Durch die Angabe eines Wertes werden alle Ecken gleichermaßen konfiguriert. Sie können aber auch vier Werte angeben und damit jede Ecke separat abrunden. Die Reihenfolge ist dabei oben links, oben rechts, unten rechts und unten links. Wenn Sie die Werte in XAML angeben, können diese durch Leerzeichen und optional zusätzlich durch Kommata getrennt werden.
148
Komponenten
Genau wie für die Ecken können Sie auch die Anzeige der Ränder einzeln konfigurieren. Weisen Sie dazu in XAML der Eigenschaft BorderThickness unterschiedliche Werte zu (links, oben, rechts, unten). Im Code können Sie ein Thickness-Objekt mit dem Konstruktor mit vier Parametern verwenden.
BEISPIEL AUF DER CD Die Ecken der Border-Komponente werden über die Eigenschaft CornerRadius unterschiedlich abgerundet. In den Rahmen der Anwendung werden im nächsten Abschnitt die Aufzählungszeichen mit dem BulletDecorator eingefügt. Hier ist zu beachten, dass zwar die Ecken unterschiedlich abgerundet werden können, die Größe des umschlossenen Inhalts aber nicht beeinflusst wird. Sie werden dies im nächsten Beispiel sehen.
Abbildung 6.6: Rahmen mit unterschiedlich abgerundeten Ecken
6.4.2 BulletDecorator Für Aufzählungen, die aus einem Symbol, dem Aufzählungszeichen und weiteren Elementen bestehen sollen, bietet sich der BulletDecorator an. Diese Komponente ist in der Regel eine Mischung aus einer Grafik und einer Beschriftung, wobei beide auch aus einem einzelnen UI-Element oder einem Container, der mehrere UI-Elemente enthält, bestehen können. Das Aufzählungszeichen wird über die Eigenschaft Bullet defi-
149
Kapitel 6
niert, der Inhalt über die Eigenschaft Child. In XAML wird der Inhalt einfach in einem BulletDecorator-Element verschachtelt. Die Breite des BulletDecorators bestimmt dabei die Gesamtbreite der Komponente. Verwenden Sie eine einfache Beschriftung, kann der Text z. B. umbrochen (Eigenschaft TextWrapping) oder mit einer anderen Hintergrundfarbe hinterlegt werden. Nachteilig am BulletDecorator ist die Tatsache, dass man für jeden Aufzählungspunkt ein weiteres BulletDecorator-Element benötigt. Hier hilft die Verwendung von Stilen weiter, welche die Basiskonfiguration des Aufzählungszeichens einmalig definieren können.
BEISPIEL AUF DER CD Wenn Sie beginnen, in das letzte Beispiel einige Aufzählungspunkte einzufügen, werden Sie bemerken, dass zwar die linke obere Ecke korrekt abgerundet ist, dies aber keinen Einfluss auf das Layout hat. Aus diesem Grund sollten bei der Border-Komponente der Innenabstand (Padding) und gegebenenfalls bei den Aufzählungspunkten der Außenrand (Margin) gesetzt werden. Um im Border-Element mehrere Aufzählungspunkte unterzubringen, wird ein StackPanel verwendet. Da standardmäßig kein Abstand zwischen dem Aufzählungszeichen und dem zugehörigen Text vorgesehen ist, muss auch hier mit Abständen gearbeitet werden.
Abbildung 6.7: Links ohne Innen- und Außenrand, rechts mit
6.5 Menükomponenten Im .NET Framework 2.0 wurden bereits die bisher eher einfachen Menüs durch neue ToolStrip- und MenuStrip-Komponenten ersetzt. Diese erlauben z. B. die Nachbildung der bekannten Office 2003-Menüs. In der WPF gibt es dafür kein direktes Äquivalent. Dafür können Sie mit den durch die WPF angebotenen Menüs wesentlich mehr »Unfug« treiben. Ob Änderung des Hintergrunds oder einer speziellen Formatierung des angezeigten Inhalts, der Erstellung von farbenfrohen bzw. ausgefallenen Menüs steht nun nichts mehr im Wege. Allerdings ist für einfachere Dinge wie die Zuordnung von Grafiken in Menüs wieder etwas mehr Arbeit notwendig.
6.5.1 Hauptmenüs Ein Hauptmenü wird durch das Menu-Element definiert. Darin werden wiederum ein oder mehrere MenuItem-Elemente angeordnet. Die Verschachtelung von Menüs wird durch die Verschachtelung mehrerer MenuItem-Elemente erreicht. Menütrenner (Separatoren) erzeugen Sie über ein -Element, das zwischen die MenuItem-Elemente eingefügt wird. Die Beschriftung wird bei jedem Menüeintrag über das Attribut Header oder bei komplexeren Menüeinträgen über ein MenuItem.Header-Element erzeugt. Möchten Sie in einem MenuItem mehr als ein Kontrollelement anordnen, wie z. B. eine Grafik und einen Text, sind diese in ein Container-Element einzuschließen. Um auf die Auswahl eines Menüpunktes zu reagieren, implementieren Sie eine Ereignisbehandlung für das Click-Ereignis. Mittels der Eigenschaft IsMainMenu kann bei einem Menu-Element festgelegt werden, dass die Betätigung der (Alt)-Taste in Verbindung mit einem Buchstaben die Menüeinträge aktiviert. Der Standardwert ist true. Die Änderung des Werts ist aber nur dann von Belang, wenn Sie mehrere Menu-Elemente innerhalb einer XAML-Datei oder innerhalb eines Fensters verwenden. Ansonsten wäre es nicht klar, welches Menü aktiviert werden soll. Verwenden Sie nur ein Menu-Element, wird immer das vorhandene MenuElement aktiviert, unabhängig vom Wert der Eigenschaft IsMainMenu. Markierbare Menüpunkte werden über die Eigenschaften IsCheckable und IsChecked erstellt und ausgewertet. Die erste Eigenschaft erlaubt es dem Anwender, die Markierung an einem Menüpunkt zu verwenden, und die zweite zeigt die Markierung an. Einem Menüpunkt können Sie über die Eigenschaft Icon ein Bild zuordnen. Dazu können Sie z. B. ein Image-Element verwenden, und geben Sie über dessen Attribut Source die Position zu einer Bilddatei an.
151
Kapitel 6
Beispiel Öffnen ... Listing 6.9: Ein Hauptmenü erstellen
6.5.2 Kontextmenüs Neben Hauptmenüs lassen sich über das ContextMenu-Element auch Kontextmenüs erzeugen. Diese können außerhalb eines Kontrollelements zur gemeinsamen Nutzung definiert werden (ähnlich Stilen), oder Sie erzeugen sie direkt im jeweiligen Element. Dazu erstellen Sie beispielsweise bei einem Button ein Unterelement und darunter wieder ein -Element. Darin können Sie wie in einem Hauptmenü Ihre Menüpunkte unterbringen.
Beispiel Listing 6.10: Einer Komponente ein Kontextmenü zuordnen
6.5.3 Zugriffstasten und Tastenkürzel Menüpunkte lassen sich auch mit Zugriffstasten belegen. Dadurch kann ein Menüpunkt durch Drücken der (Alt)-Taste und eines bestimmten Buchstabens, der für die-
152
Komponenten
sen Menüpunkt definiert ist, aktiviert werden. In der WPF muss dazu vor dem Buchstaben ein Unterstrich angegeben werden. Wenn Sie eine komplexere Beschriftung für einen Menüpunkt benötigen, können Sie das Element AccessText verwenden. Der in diesem Element eingeschlossene Text enthält vor der Zugriffstaste wiederum den Unterstrich (siehe folgendes Beispiel). Einige Menüpunkte können auch über Tastenkombinationen wie (Strg)+(K) oder (Strg)+(ª)+(A) aktiviert werden. Dazu müssen Sie in der WPF aber deutlich mehr ausholen, als das früher der Fall war. Über die Eigenschaft InputGestureText im MenuItem-Element können Sie lediglich die Tastenkombination (oder einen beliebigen anderen Text!) im Menüpunkt anzeigen, die ihn aktivieren soll. Es besteht dadurch aber keine Verknüpfung mit dieser Tastenkombination, da wie gesagt jeder beliebige Text angegeben werden kann. Nützlich ist diese Eigenschaft dann, wenn Sie das automatisch angezeigte Tastenkürzel überschreiben wollen, z. B. zur Anzeige in Deutsch oder um es vollständig zu verbergen. _WPF ist Cool Listing 6.11: Eine Zugriffstaste festlegen und eine Tastenkombination angeben
Um einen Menüpunkt mit einer Tastenkombination zu verknüpfen, fangen Sie entweder alle Tastaturereignisse im Fenster ab (siehe dazu auch das Kapitel zur Ereignisbehandlung) und führen dann die passende Ereignisbehandlung durch, oder Sie erstellen ein neues Kommando (siehe Kapitel Kommandos), dem auch eine Tastenkombination zugewiesen werden kann, und weisen es dem Menüpunkt zu.
BEISPIEL AUF DER CD Die Anwendung besitzt ein Haupt- und ein Kontextmenü. Letzteres ist der TextBox unter dem Hauptmenü zugeordnet. Das Hauptmenü besitzt einige Besonderheiten. Der erste Menüpunkt besteht aus einem Symbol sowie einer Beschriftung. Der zweite Menüpunkt wird markiert dargestellt und erhält einen Tooltipp. Im dritten Menüpunkt wird ein Tastenkürzel verwendet, und der vorletzte Menüpunkt des Hauptmenüs wird aus anderen WPF-Komponenten zusammengesetzt. Damit das Hauptmenü unter der Titelleiste angezeigt wird, muss es manuell dort positioniert werden, z. B. über ein Dock- oder ein StackPanel. Beachten Sie, dass die Tastenkombinationen in diesem Beispiel wirkungslos sind. Lediglich der letzte Menüpunkt zum Beenden der Anwendung verfügt über eine Click-Ereignisbehandlung. Dem Schließen-Menüpunkt wird im Konstruktor der Fensterklasse ein Kommando zugewiesen, sodass ein funktionierendes Tastenkürzel angezeigt ((Strg)+(Shift)+(S)) und ein Ereignishandler zugewiesen wird.
Die Methode InitMenuCommands() wird im Konstruktor der Fensterklasse aufgerufen. Darin wird zuerst eine neue Tastenkombination erzeugt. Danach wird ein CommandObjekt erzeugt, dem im Konstruktor die Beschriftung, ein Name, der Typ des Besitzers sowie die Tastenkombination übergeben werden. Zum Abschluss wird eine Verknüpfung eines Kommandos mit einem Ereignishandler über die CommandBindings-Auslistung hergestellt und das Kommando dem Menüpunkt hinzugefügt. Dadurch wird die Beschriftung aus den Menüpunkt übertragen. Wird die Tastenkombination (Strg)+(ª)+(S) gedrückt, wird das Kommando aktiviert und aufgrund der vorher erstellten Verknüpfung der Ereignishandler ausgeführt. Die beiden Ereignishandler zeigen einmal nur eine MessageBox an, bei Auswahl des Menüpunktes BEENDEN wird die Anwendung tatsächlich beendet. public partial class Window1: Window
6.6 Hilfskomponenten Um die Kategorisierung nicht ausufern zu lassen, werden verschiedene Komponenten in die Kategorie »hilfreich« eingegliedert. Komponente
Beschreibung
Expander
Mittels eines Expanders können Bereiche auf- und zugeklappt werden. Die Bereiche können über eine Überschrift verfügen.
GridSplitter
Um die Breite einer Spalte oder die Höhe einer Zeile in einem Grid zu ändern, können Sie den GridSplitter einsetzen. Die GridSplitter-Komponente wird im Kapitel zu Layoutcontainern bei der Beschreibung der Grid-Komponente vorgestellt.
Image
Um Bilder vom Typ BMP, GIF, ICO, JPG, PNG oder TIFF anzuzeigen, verwenden Sie diese Komponente.
MediaElement
Diese Komponente dient der Anzeige von Audio- und Videodateien, die in der Eigenschaft Source angegeben werden. Über Methoden wie Play(), Pause(), Stop() starten, pausieren und beenden Sie die Wiedergabe. Im Kapitel zu Kommandos wird die Komponente in einem Beispielprojekt verwendet.
Popup
Hiermit können Sie ein Popup-Fenster öffnen, das einen beliebigen Inhalt haben kann. Ein Popup-Fenster wird durch die Zuweisung von True an die Eigenschaft IsOpen der Komponente angezeigt. Die Eigenschaft Child enthält den Inhalt des Fensters. Über die Eigenschaften PlacementTarget und Placement können Sie die Anzeigeposition festlegen, allerdings nur für die erste Darstellung – bewegen Sie das Fenster, wird die Position des Popups nicht verändert.
ProgressBar
Zur Anzeige von Fortschrittsbalken verwenden Sie diese Komponente. Mit den Eigenschaften Minimum und Maximum stellen Sie den kleinsten und größten Wert und über Value den aktuellen Wert ein. Setzen Sie die Eigenschaft IsIndeterminate auf True, um eine animierte, generische Animation anzuzeigen, oder auf False, um einen wachsenden Fortschrittsbalken zu zeigen. Mittels der Eigenschaft Orientation kann die ProgressBar vertikal oder horizontal angezeigt werden.
RepeatButton
Löst nach einem einmaligen Anklicken eine Folge von Click-Ereignissen aus. Das Intervall kann über die Eigenschaft Interval in Millisekunden eingestellt werden.
Separator
Mittels eines Separators können in Menüs, ListBoxen und ToolBars Trennstriche eingefügt werden.
Tabelle 6.6: Übersicht der Hilfskomponenten
156
Komponenten
Komponente
Beschreibung
Slider
Mittels eines Sliders können Sie auf einer Skala einen Wert innerhalb eines Wertebereichs einstellen. Über die Eigenschaften Minimum und Maximum legen Sie den kleinsten und den größten Wert fest. Über die Eigenschaft Value lesen oder setzen Sie den aktuellen Wert. Die Ausrichtung wird über die Eigenschaft Orientation gesetzt.
StatusBar
Um eine Statuszeile zu erzeugen, verwenden Sie diese Komponente. Die Positionierung müssen Sie allerdings selbst vornehmen. Um einen Bereich in die StatusBar einzufügen, verwenden Sie StatusBarItem-Elemente. In diese können Sie wiederum einzelne Elemente oder Container einbetten.
ToggleButton
Um einen Zustand an- oder auszuschalten, lässt sich ein solcher Button verwenden. Die Klasse dient als Basisklasse für die Klassen CheckBox und RadioButton, kann aber auch selbstständig verwendet werden. Über die Eigenschaft IsChecked kann zwischen den möglichen drei (die Eigenschaft IsIndeterminate ist true) oder standardmäßig zwei Eigenschaften umgeschaltet werden. Dazu können Sie die Werte True, False und null (für den dritten Zustand) angeben. Während er im Zustand IsChecked gedrückt dargestellt wird, gibt es für den dritten Zustand keine spezielle Darstellung. Über Ereignisse können Sie allerdings auf die Zustandswechsel reagieren und den Inhalt des Buttons entsprechend gestalten.
ToolBar
In einer ToolBar werden verschiedene Komponenten verwaltet, die einen Schnellzugriff auf andere Programmteile herstellen sollen. Die Komponenten werden dazu einfach als Unterelemente der ToolBar-Komponente eingefügt. Ein oder mehrere ToolBars können wiederum Bestandteil eines ToolBarTrays, d. h. eines Sammelbehälters für einzelne ToolBars, sein. Mittels der Eigenschaften Band und BandIndex können die Zeile (Band) und die Position innerhalb der Zeile (BandIndex) festgelegt werden.
ToolTip
Ein ToolTip wird angezeigt, wenn die Maus eine bestimmte Zeit über einer Komponente verweilt. Dazu wird die Eigenschaft ToolTip der Komponente gesetzt. Über die Eigenschaft HasDropShadow kann ein Schatten aktiviert werden (Standard ist an). Mittels der Eigenschaften InitialShowDelay und ShowDuration der Klasse ToolTipService können z. B. die Verzögerung in Millisekunden bei der Anzeige und die Anzeigedauer eingestellt werden. Diese beiden Eigenschaften müssen aber an der Komponente gesetzt werden, an denen die Tooltipps angezeigt werden sollen.
Tabelle 6.6: Übersicht der Hilfskomponenten (Fortsetzung)
6.6.1 Expander Der Expander stellt so etwas wie eine ComboBox für die Anzeige eines Bereichs dar, der unterschiedlichste Komponenten beinhalten kann. Über eine Pfeilschaltfläche kann der Bereich zu- oder aufgeklappt werden, der durch ein Containerelement oder die Anzeige einer einzelnen größeren Komponente definiert wird. Neben dem (Expander-)Symbol können Sie auch noch eine Erläuterung/Überschrift (Eigenschaft Header) für den Bereich angeben. Diese wird in keinem Fall eingeklappt.
157
Kapitel 6
Die Aufklapprichtung kann über die Eigenschaft ExpandedDirection definiert werden (Down, Left, Right, Up). Über die Eigenschaft IsExpanded kann geprüft werden, ob die Anzeige momentan auf- oder zugeklappt ist. Durch die Konfiguration des Rahmens können sehr attraktiv aussehende Bereiche geschaffen werden.
BEISPIEL AUF DER CD Das Beispiel zeigt zwei Expander mit etwas unterschiedlicher Konfiguration. Der obere Expander verwendet eine Vorlage (DataTemplate, siehe Kapitel zu Stilen), über die der Header-Bereich konfiguriert wird, um beispielsweise eine andere Schriftart zu verwenden. Über das Attribut x:Key wird diese Vorlage benannt und später im Expander über das Attribut HeaderTemplate (damit wird eine Vorlage zur Anzeige der Beschriftung ausgewählt) referenziert. Über den Rahmen und die Innenfarbe sowie die Rahmeneinstellungen wird das Äußere des Expanders gestaltet. Der Inhalt des Expanders wird über ein StackPanel gebildet, in dem weitere Komponenten eingebettet werden. Der Inhalt des Textblocks wird mit einem Fülltext (http://www.lorem-ipsum.info/ blindtext-lorem-ipsum) versehen. Im zweiten Expander wird eine andere Aufklapprichtung verwendet sowie auf die Vorlage für die Beschriftung verzichtet. Der rechte Teil der Abbildung zeigt den zugeklappten Bereich, in dem nur noch die Beschriftungen sichtbar sind. Das Expandersymbol kennzeichnet dabei die Aufklapprichtung.
Abbildung 6.9: Auf- und zugeklappter Expander
Ein kleiner Dummy-Text Excepteur sint occaecat cupidatat non proident, ... Ein weiterer Dummy-Text Excepteur sint occaecat cupidatat non proident, ... Listing 6.14: Beispiele\Kap06\ExpanderProj\Window1.xaml
6.6.2 Image Zum Anzeigen von Grafiken eignet sich die Image-Komponente. Diese besitzt eine Eigenschaft Source, der Sie den relativen oder absoluten Pfad zur Grafik übergeben. Mittels der Eigenschaften Stretch und StretchDirection (Both, DownOnly, UpOnly) können Sie festlegen, wie die Grafik gestreckt werden soll, wenn die Größe der ImageKomponente sich nicht mit der Größe der Grafik deckt. Geben Sie zur Einhaltung der Proportionen der Grafik entweder die Höhe oder die Breite für die Image-Komponente, aber nicht beide an. Alternativ setzen Sie die Stretch-Eigenschaft entsprechend.
BEISPIEL AUF DER CD Zur Anordnung der Bilder wird ein WrapPanel verwendet. Für ein Bild wird nur die Breite der Image-Komponente angegeben. Dadurch wird das Bild automatisch mit korrekten Proportionen in die Komponente eingepasst. Beim zweiten Bild wurde die Größe der Image-Komponente mit einer Breite und Höhe von 150 Pixel festgelegt. Ohne die Angabe von Stretch="Fill" würde die rechte Ausgabe der des ersten Bildes entsprechen.
159
Kapitel 6
Abbildung 6.10: Image in Originalgröße und in einen Bereich eingepasst
6.6.3 Popup und ToolTip Popup Es gibt verschiedene Formen, eine Hilfestellung in eine Anwendung zu integrieren. Eine Variante ist die Verwendung von Popups, die eigenständige Fenster darstellen und wie ein Tooltipp beim Überfahren einer Komponente angezeigt werden können. Obwohl Sie die Anzeigeposition festlegen können, ist dies nur für die erste Anzeige relevant. Verschieben Sie danach das Hauptfenster, bleibt das Popup an der angezeigten Stelle stehen. Da es sich bei einem Popup um ein »richtiges« Fenster handelt, ist dieses Verhalten zwar verständlich, aber für die Handhabung anfangs gewöhnungsbedürftig. Ein Popup wird nicht automatisch geöffnet. Stattdessen müssen Sie explizit dessen Eigenschaft IsOpen auf den Wert true setzen, um es anzuzeigen. In einem Popup können Sie der Eigenschaft Child einen Layoutcontainer oder eine einzelne Komponente zuordnen. In XAML fügen Sie einem Popup-Element ein Unterelement hinzu. Mittels verschiedener Eigenschaften legen Sie die Anzeigeposition des Fensters fest. Erster Anlaufpunkt ist hier die Eigenschaft Placement. Dieser Eigenschaft können Sie einen der zahlreichen Werte der Aufzählung PlacementMode zuweisen, z. B. Absolute, Left, Right oder Relative. Wählen Sie Left oder Right, wird das Popup-Fenster links bzw. rechts von der Komponente angezeigt, der das Popup zugeordnet ist. Wählen Sie stattdessen Absolute oder Relative, legen Sie über die Eigenschaften HorizontalOffset und VerticalOffset die X- bzw. Y-Position relativ zum Bildschirm bzw. zur Bezugs-
160
Komponenten
komponente des Popups fest. Die Bezugskomponente wird über die Eigenschaft PlacementTarget definiert. Diese wird für die Angaben in der Eigenschaft Placement (Left, Right, Center) als Bezugspunkt verwendet. Als letztes Schmankerl für die Anzeige dienen die Eigenschaften PopupAnimation und AllowsTransparency. Letztere Eigenschaft wird benötigt (d.h., sie wird mit True belegt), wenn die Komponente animiert werden soll. Dies erfolgt entweder automatisch über die Einstellungen in der Eigenschaft PopupAnimation oder wenn Sie eine Transformation mit einem Popup ausführen. Die Eigenschaft PopupAnimation kann die Werte Fade, None, Scroll und Slide annehmen. Dadurch kann ein Popup z. B. langsam eingeblendet und ausgeblendet werden (Fade). Das folgende Popup verwendet eine TextBox-Komponente als Bezug. Die Anzeige erfolgt über relative Koordinaten bezogen auf diese Komponente. Außerdem wird das Popup zur Anzeige eingeblendet. Das PlacementTarget wird über ein Data Binding angegeben (siehe Kapitel zu Data Binding). Über den Elementnamen wird der Name der Bezugskomponente festgelegt. ... Listing 6.16: Ein Popup, das eine TextBox als Bezugskomponente verwendet
ToolTip Ein ToolTip (Hinweisfenster) wird dann angezeigt, wenn die Maus einen Moment über einer Komponente verharrt. Ein Vorteil der WPF ist auch hier wieder, dass sich der Inhalt des Tooltipps völlig frei definieren lässt. Jede Komponente besitzt ihren eigenen Tooltipp in Form der Eigenschaft ToolTip, die sie von der Klasse FrameworkElement erbt. Die Anzeige erfolgt standardmäßig beim Mauszeiger und muss deshalb nicht festgelegt werden. Allerdings stehen Ihnen die gleichen Eigenschaften HorizontalOffset, VerticalOffset, Placement, PlacementRectangle und PlacementTarget zum Festlegen des Anzeigeortes zur Verfügung. Obwohl ein Tooltipp automatisch eingeblendet und nach einiger Zeit automatisch auch wieder ausgeblendet wird, kann dieses Verhalten konfiguriert werden. Mittels der Eigenschaft IsOpen können Sie wie bei der Popup-Komponente die Anzeige umschalten. Zur Konfiguration der Anzeigeeigenschaften eines Tooltipps einer Anwendung verwenden Sie die Klasse ToolTipService. Darin können Sie z. B. auch alle Positionierungsangaben wie Placement oder VerticalOffset setzen. Darüber hinaus können die
161
Kapitel 6
Anzeigedauer (ShowDuration), die Verzögerung, bis ein Tooltipp angezeigt wird (InitialShowDelay), oder die Anzeige der Tooltipps überhaupt (IsEnabled) festgelegt werden. Die Zeitangaben erfolgen hier immer in Millisekunden. In XAML werden diese Angaben direkt im entsprechenden Element oder zentral im Wurzelelement vorgenommen. Wenn Sie diese Angaben im Wurzelelement oder in einem Layoutcontainer einstellen, werden sie nicht automatisch für alle untergeordneten Tooltipps verwendet. Eine einfache Lösung wäre die Verwendung von Stilen. Darin können für alle Tooltipps diese Einstellungen vorgenommen werden.
Im Gegensatz zur Popup-Komponente können Sie bei einem Tooltipp über die Eigenschaft HasDropShadow die Anzeige des Schattens direkt setzen. Diese wird bei dessen erster Anzeige automatisch auf True gesetzt.
BEISPIEL AUF DER CD Über die beiden Schaltflächen wird in der Ereignisbehandlung die Anzeige des Popups gesteuert (ChangePopupStatus()). Im Gegensatz zum Tooltipp wird ein Popup weder automatisch angezeigt noch geschlossen. Verschieben Sie das Fenster, bewegt sich ein Popup allerdings nicht mit. Es eignet sich damit z. B. als separates Hilfefenster, muss dann aber auch entsprechend kenntlich gemacht werden. Als Bezugselement wird für das Popup die TextBox unter den Schaltflächen verwendet. Dass ein Separator nicht nur als Trenner innerhalb von Listen dienen kann, zeigt dessen Verwendung zur Trennung der beiden Textboxen. Die untere TextBox verfügt über einen Tooltipp, dessen Anzeigeeigenschaften über die Klasse ToolTipService geändert wurden (schnelle Anzeige, lange Anzeigedauer).
Abbildung 6.11: Popups und Tooltipps
162
Komponenten Zeige Popup Verberge Popup Was guckst Du? Textboxinhalt Excepteur sint occaecat cupidatat non proident, ... Listing 6.17: Beispiele\Kap06\PopupToolTipProj\Window1.xaml private void ChangePopupStatus(object sender, RoutedEventArgs e)
6.6.4 ProgressBar und Slider ProgressBar Zur Fortschrittsanzeige bei länger andauernden Operationen wird häufig die ProgressBar-Komponente eingesetzt. Über die Eigenschaften Minimum und Maximum stellen Sie den kleinsten und größten Wert ein, den die Eigenschaft Value annehmen kann. Diese Eigenschaft enthält den aktuellen Wert, aufgrund dessen ein Fortschrittsbalken angezeigt wird. Zur Ausrichtung können Sie eine ProgressBar vertikal oder horizontal anzeigen, setzen Sie dazu die Eigenschaft Orientation auf einen der Werte Horizontal oder Vertical. Standardmäßig wird der Fortschrittsbalken von links beginnend dargestellt. Setzen Sie die Eigenschaft IsIndeterminate auf den Wert True, vergrößert sich einerseits die Länge des Fortschrittsbalkens, andererseits bewegt sich dieser von links nach rechts rotierend. Der Sinn dieser Darstellungsart ist eine noch stärkere Visualisierung.
Slider Während Sie mit einer ProgressBar den Fortschritt einer Aktion darstellen, wird der Slider (Schieberegler) zur Einstellung eines Wertes durch den Benutzer eingesetzt. Die Ausrichtung kann wieder über die Eigenschaft Orientation eingestellt werden. Den minimalen und maximalen Wert legen Sie mit den Eigenschaften Minimum und Maximum fest. Verstellen Sie den Slider, wird ein Wert aus diesem Intervall in der Eigenschaft Value geliefert. Beachten Sie, dass die Eigenschaft Value vom Typ double ist, d.h., Sie erhalten keine ganzzahligen Werte. Standardmäßig befindet sich am Slider links bzw. unten der minimale Wert. Um die Laufrichtung des Sliders umzukehren, setzen Sie die Eigenschaft IsDirectionReversed auf den Wert True. Wenn Sie mit der Maus auf den Wertebereich des Sliders klicken, wird dieser normalerweise immer nur ein kleines Stück in Richtung des Mausklicks bewegt. Setzen Sie dagegen die Eigenschaft IsMoveToPointEnabled auf den Wert True, wird der Slider direkt zu dem Punkt bewegt, auf den der Mausklick erfolgte. Damit der Anwender beim Bewegen des Sliders den aktuellen Wert verfolgen kann, verwenden Sie die Eigenschaften AutoToolTipPlacement und AutoToolTipPrecision. Über die erste Eigenschaft geben Sie an, an welcher Stelle ein Tooltipp mit dem aktuellen Wert angezeigt werden soll (None, BottomRight, TopLeft). Mittels der zweiten Eigenschaft legen Sie die Anzahl der angezeigten Nachkommastellen fest. Die Standardwerte sind None und 0. Über einen Auswahlbereich kennzeichnen Sie einen Abschnitt des Sliders, der z. B. den optimalen Wertebereich darstellt. Denken Sie z. B. an die Übertaktung einer Grafikkarte. Ein roter Bereich könnte einen Bereich kennzeichnen, ab dem mit einer hohen
164
Komponenten
Fehlerwahrscheinlichkeit zu rechnen ist. Den Bereich stellen Sie über die Eigenschaften SelectionStart und SelectionEnd ein. Beide Werte müssen jeweils größer oder gleich dem Minimum und kleiner oder gleich dem Maximum sein. Damit der Bereich angezeigt wird, ist außerdem noch die Eigenschaft IsSelectionRangeEnabled auf den Wert True zu setzen. Ein letzter Schliff kann noch an der Beschriftung des Sliders durchgeführt werden. Durch das Setzen der Eigenschaft TickPlacement (None, Both, BottomRight, TopLeft) kann die Anzeige von Markierungen aktiviert werden. Der Wert BottomRight zeigt die Markierung bei einer horizontalen Ausrichtung unten, bei einer vertikalen Ausrichtung rechts an. Die Markierung wird standardmäßig für jeden ganzzahligen Wert gesetzt, d. h. bei einem Wertebereich von 0 bis 100 genau einhundert Mal. Über die Eigenschaft TickFrequency legen Sie einen anderen Wert fest. Besitzen Sie einen Wertebereich von 0 bis 100 und stellen eine Tick-Frequency von 20 ein, werden Markierungen bei 0, 20, 40, 60, 80 und 100 angezeigt. Über die Eigenschaft Ticks (vom Typ DoubleCollection) lassen sich auch individuelle Positionen für die Markierungen angeben, z. B. 20, 35, 50, 85. Damit ein Anwender auch nur die vorgegebenen Markierungen auswählen kann bzw. der Slider automatisch auf diese springt, setzen Sie außerdem die Eigenschaft IsSnapToTickEnabled auf den Wert True.
BEISPIEL AUF DER CD Das Beispiel vereinigt die unterschiedlichen Einstellungen zur Fortschrittsanzeige und zum Schieberegler. Zusätzlich werden unter dem Slider zwei Textboxen bereitgestellt, die den aktuell eingestellten Wert des Sliders anzeigen. Dabei wird bereits wieder das Data Bindung verwendet, was erst in einem späteren Kapitel vorgestellt wird. In diesem Fall wird der Inhalt der TextBox an den Wert der Eigenschaft Value (Path=Value) der Komponente Slidi1 (ElementName=Slidi1) gebunden. Beim unteren Slider wird gerade mit der Maus ein neuer Wert eingestellt. Es ist dabei der Tooltipp mit dem aktuellen Wert zu sehen.
Abbildung 6.12: Fortschrittsanzeige und Schieberegler
6.6.5 RepeatButton Viele Anwendungen besitzen Schaltflächen, die zum Verschieben/Positionieren bzw. zum schrittweisen Ändern von irgendwelchen Dingen dienen. Für jede Aktion ist ein Klick notwendig, bis Ihnen die Finger taub werden. Die WPF hat hier ein Einsehen und gibt Ihnen den RepeatButton an die Hand. Dieser wird einmal geklickt, und danach wird die Maustaste gedrückt gehalten. Jetzt wird anhand der Einstellung in der Eigenschaft Interval nach Ablauf der zugewiesenen Zeit jeweils ein Click-Ereignis ausgelöst. Statt Ausdauertraining zum Klicken ist nun also nur noch ein Training zum Dauerdrücken notwendig. Um die Zeitverzögerung zwischen dem Anklicken und dem Start der Ereignisgenerierung einzustellen, setzen Sie die Eigenschaft Delay. Die Einstellungen in Interval und Delay erfolgen in Millisekunden.
BEISPIEL AUF DER CD Um die Größe einer Ellipse stetig zu vergrößern und zu verkleinern, ist der RepeatButton eine ausgezeichnete Wahl. Die Anordnung der Komponenten erfolgt diesmal in einem DockPanel. Oben werden die Steuerungsschaltflächen, unten eine Statuszeile und in der Mitte die Ellipse
166
Komponenten
angezeigt. Um sich wiederholende Einstellungen für die RepeatButtons zu kapseln, werden diesein einem Stil untergebracht (siehe Kapitel zu Stilen) und dieser Stil auf alle RepeatButtons angewandt (TargetType...). Beide Buttons werden mit einem Ereignishandler verbunden, der die Größe der Ellipse ändert und die aktuelle Größe in der TextBox anzeigt, die in der Statuszeile eingefügt wurde. Damit eine flüssige Änderung der Ellipse erfolgt, wurde das Klick-Intervall auf 100 Millisekunden festgelegt.
6.6.6 Separator Wie der Name schon sagt, dient der Separator dazu, Dinge voneinander zu trennen. In der Regel sind dies Listen- oder Menüeinträge. Sie können ihn aber auch dazu nutzen, eine horizontale oder vertikale Trennlinie in einem Fenster einzufügen. In diesem Fall wird er wie eine »normale« Komponente, z. B. ein Button, eingesetzt. Er besitzt keine speziellen Eigenschaften, reagiert nicht auf Maus- und Tastaturereignisse und wird immer als 1 Pixel breite Linie gezeichnet. Durch das Setzen der Eigenschaft Background können Sie ihm noch eine andere Hintergrundfarbe zuweisen.
Beispiele für die Verwendung des Separators finden Sie mehrere in diesem Kapitel, z. B. bei der Beschreibung der ToolBar und der Menükomponenten.
168
Komponenten
6.6.7 StatusBar und ToolBar StatusBar Statusleisten dienen dazu, dem Anwender im unteren Bereich eines Fensters Informationen zum aktuellen Zustand der Anwendung anzuzeigen. In einem Texteditor könnte dies die aktuelle Position des Cursors (Zeile, Spalte) sein, bei einem DownloadManager die Fortschrittsanzeige und die Größe eines Downloads. Einzelne Bereiche der StatusBar werden durch StatusBarItem-Elemente erzeugt. Darin können wiederum beliebige Komponenten eingebettet werden. Die Breite dieser Bereiche ergibt sich entweder durch die enthaltenen Komponenten, oder Sie geben explizit eine Breite über das Attribut Width an. Intern verwendet die StatusBar-Komponente ein DockPanel für die Anordnung der enthaltenen Bereiche. Möchten Sie innerhalb der StatusBar eine Komponente rechts ausrichten, setzen Sie einfach die Attached Property des DockPanels auf Right. Autom. Neustart in 3s
Wo Sie eine StatusBar positionieren und wie viele StatusBar-Komponenten Sie verwenden, ist Ihnen überlassen. Idealerweise wird für die Anordnung einer StatusBar ein DockPanel verwendet und die StatusBar darin unten angeordnet. Die StatusBar-Komponente besitzt keinerlei besondere Eigenschaften zur Konfiguration. Sie wird als einfarbiger Bereich dargestellt, dem noch ein Rahmen (Eigenschaften BorderThickness, BorderBrush) zugewiesen werden kann. Um den Rahmen etwas umfangreicher zu gestalten, können Sie die StatusBar z. B. in eine Border-Komponente einbetten. Des Weiteren lässt sich z. B. der Separator über einen Stil konfigurieren, sodass er sich mehr hervorhebt. Die Breite der Elemente kann im Element gesetzt werden, oder Sie setzen sie im StatusBarItem-Element und legen außerdem darin die Eigenschaft HorizontalAlignment fest, sonst wird das Element in der Statusleiste zentriert dargestellt.
> >
>
HINWEIS
Anstatt die einzelnen Elemente einer StatusBar innerhalb von XAML in StatusBarItem-Elemente einzuschließen, können diese auch direkt unter dem StatusBar-Element eingefügt werden. Sie werden dann automatisch in der Collection der Eigenschaft Items hinzugefügt.
169
Kapitel 6
BEISPIEL AUF DER CD Die StatusBar wird hier in ein Border-Element eingebettet, das die Ränder konfiguriert. Innerhalb der StatusBar werden verschiedene Komponenten, mit und ohne Verwendung von StatusBarItemElementen, eingefügt. Für den Separator wird im Ressource-Abschnitt des Window-Elements ein spezieller Darstellungsstil definiert und später auf jeden Separator angewendet. Auf diese Weise erhalten Sie auf einfache Weise eine zentrale Stelle zur Konfiguration von Komponenten. Das rechte Ausrichten von Komponenten in der StatusBar ist kein Problem, wie am Label (Anzahl: 1) zu sehen ist. Setzen Sie lediglich die Eigenschaft DockPanel.Dock auf den Wert Right. Beim Klick auf den Button wird über Code eine ProgressBar erzeugt und in die StatusBar aufgenommen. Weiterhin wird noch ein Separator erzeugt und vor der ProgressBar in die StatusBar eingefügt. Dem Separator wird der bereits definierte Stil SbSeparator zugewiesen. Dabei hilft die Methode FindResource(), die im Ressourcenabschnitt des Fensters nach der – nach dem übergebenen Parameter benannten – Ressource sucht. Außerdem wird in der TextBox die Anzahl der Elemente in der Eigenschaft Items der StatusBar angezeigt. Wie man sieht, sind es genau acht Elemente, inklusive der Separatoren, d.h., auch die Elemente, die sich in XAML nicht innerhalb von StatusBarItem-Elementen befinden, wurden mitgezählt.
Abbildung 6.14: StatusBar mit verschiedenen Komponenten
ToolBar Um einen schnellen Zugriff auf häufig verwendete Funktionen in einer Anwendung zu ermöglichen, werden meist eine oder mehrere ToolBars im oberen Bereich, unterhalb des Hauptmenüs, angezeigt. Als Elemente einer ToolBar können Buttons oder Auswahlfelder verwendet werden, innerhalb der WPF sind aber auch alle anderen Komponenten möglich. Die Positionierung einer ToolBar muss von Ihnen durchgeführt werden, normalerweise über ein DockPanel. ToolBars (Symbolleisten) werden über die gleichnamige Komponente ToolBar erzeugt. Für die in der ToolBar angezeigten Komponenten gibt es keine speziellen Komponen-
171
Kapitel 6
ten. Sie fügen stattdessen die »normalen« WPF-Komponenten ein. Um beispielsweise einen ToolBar-Button zu kreieren, verwenden Sie die folgende Syntax:
Mehrere Komponenten können durch einen Separator optisch voneinander getrennt werden. Reicht der Bereich einer ToolBar nicht für die Anzeige der darin enthaltenen Komponenten aus, wird ein sogenannter Überlaufbereich erzeugt. Am Ende einer ToolBar wird dazu neben einem Button ein Pfeil angezeigt. Darüber wird ein Menü aufgeklappt, in dem die überschüssigen Elemente ausgewählt werden können.
Abbildung 6.15: Überlaufbereich öffnen
Mittels der Eigenschaften HasOverflowItems, IsOverflowOpen und IsOverflowItem können Sie prüfen, ob sich in einer ToolBar Elemente im Überlaufbereich befinden, ob dieser Bereich gerade sichtbar ist und ob sich ein Element im Überlaufbereich befindet (letztere Eigenschaft ist eine angehängte Eigenschaft). Viele Anwendungen nutzen diese Informationen, um ein im Überlaufbereich angeklicktes Element neu zu positionieren (z. B. am Anfang), damit es beim nächsten Aufruf sofort verfügbar ist. Für einzelne Elemente einer ToolBar kann noch eingestellt werden, wie sie sich bei einer Verkleinerung der ToolBar verhalten. Dazu wird ihnen über die zugeordnete Eigenschaft OverflowMode einer der Werte Never (niemals im Überlaufbereich), Always (immer darin) oder AsNeeded (bei Bedarf) übergeben. Möchten Sie mehrere ToolBars anzeigen, bietet sich die Verwendung einer ToolBarTray-Komponente an. Diese ist ein Container für mehrere ToolBars und für die Positionierung und Drag&Drop der enthaltenen ToolBars verantwortlich. ToolBars können hintereinander oder in mehreren Zeilen angezeigt werden. Wollen Sie das Verschieben der ToolBars innerhalb der ToolBarTray-Komponente verhindern, setzen Sie die Eigenschaft IsLocked auf den Wert True. Um eine ToolBar in den Container einzuordnen, verfügt diese über zwei wichtige Eigenschaften. Mittels der Eigenschaft Band legen Sie fest, in welcher Zeile (Band) die ToolBar erscheinen soll. Innerhalb einer Zeile geben Sie über die Eigenschaft BandIndex die Position von links beginnend an. Der Index beginnt bei beiden Eigenschaften bei 0.
172
Komponenten
Haben Sie eine vertikale Orientierung in der Eigenschaft Orientation gewählt, werden die Bänder von links nach rechts angeordnet, und die Reihenfolge innerhalb eines Bandes läuft von oben nach unten. Über die Eigenschaft Header, die eine ToolBar von ihrer übergeordneten Klasse HeaderedItemsControl erbt, kann eine ToolBar beschriftet werden. Dies macht eigentlich nur bei einer vertikalen Orientierung Sinn, da man im Falle einer horizontalen ToolBar auch ein Label-Element zu Beginn anzeigen könnte. Die Anbindung der Ereignishandler an die Komponenten erfolg wie bisher. So wird ein Button z. B. mit einem Click-Ereignis verknüpft, eine ComboBox mit dem Ereignis SelectionChanged.
BEISPIEL AUF DER CD Die Grafiken für die Buttons finden Sie im Verzeichnis [Laufwerk]:\Programme\Microsoft Visual Studio 8\Common7\VS2005ImageLibrary in Ihrer Visual Studio-Installation. Entpacken Sie die Datei VS2005ImageLibrary.zip, und wechseln Sie dann in das Verzeichnis ...\VS2005ImageLibrary\bitmaps\commands\pngformat. Weitere Grafiken finden sich in den anderen Ordner, die sich in der Zip-Datei befinden. Das Beispiel verwendet einen ToolBarTray-Container, um drei ToolBars darin zu verwalten. Als Besonderheit wurde für zwei Buttons der ersten ToolBar der OverflowMode auf Always gesetzt, d.h., sie befinden sich immer im Überlaufbereich. In der unteren ToolBar wurde eine ComboBox eingesetzt, die sich nahtlos darin einbettet.
Abbildung 6.16: Drei ToolBars in einem ToolBarTray
6.7 Komplexe Komponenten Die hier vorgestellten Komponenten sind in der Regel etwas komplexer und damit auch »schwieriger« einzusetzen. Komponente
Beschreibung
GridView
Ein GridView kann nicht allein verwendet werden. Es wird stattdessen als Tabellenansicht innerhalb eines ListViews verwendet. Dazu muss es der Eigenschaft View des ListViews zugewiesen werden.
InkCanvas
Stellt eine Zeichenfläche und automatisch einen Zeichenstift bereit.
ListView
Ein ListView erlaubt die Darstellung von Daten über verschiedene Listenansichten. Allerdings stellt die WPF nur eine Standardansicht über ein GridView bereit. Alle anderen benötigten Ansichten müssen manuell definiert werden. Die Ansicht muss der Eigenschaft View zugewiesen werden, die vom Typ ViewBase ist (die Komponente GridView ist von ViewBase abgeleitet). Wird keine ViewBase angegeben, verhält sich der ListView wie eine ListBox. Der Vorteil gegenüber einem GridView ist demnach, dass man die Ansichten wechseln kann, sofern man über eine andere Ansicht verfügt.
RichTextBox
Mithilfe dieser Komponente können Sie formatierten Text anzeigen und bearbeiten sowie in verschiedenen Formaten laden und speichern.
TabControl
In einem TabControl können Sie wie bei Registerkarten Inhalte verwalten. Jeder Registerkarte kann ein anderer Inhalt zugewiesen werden.
TreeView
Ein TreeView stellt eine Baumansicht zur Verfügung. Durch die Verschachtelung von TreeViewItem-Elementen wird diese Baumstruktur festgelegt. Der angezeigte Text wird entweder einfach durch die Eigenschaft Header festgelegt, oder es kann wieder ein Container mit weiteren Elementen eingefügt werden.
Tabelle 6.7: Übersicht der komplexen Komponenten
6.7.1 ListView und GridView Ein ListView dient der Darstellung von Daten in Form einer Liste. Da ListView von ListBox abgeleitet ist, entspricht seine Standarddarstellung tatsächlich dem einer ListBox, d.h., die Inhalte werden einfach untereinander dargestellt. Als Erweiterung zu einer ListBox besitzt ein ListView die Eigenschaft View, über die Sie zur Anzeige der Daten eine spezielle Ansicht zuweisen können. Einzig die GridView-Komponente stellt eine solche vordefinierte Ansicht bereit. Dies ist auch die einzige Möglichkeit, eine GridView-Komponente überhaupt zu nutzen. Die GridView-Ansicht entspricht dabei einer Tabelle mit mehreren Spalten, die in der Breite verändert und auch verschoben werden können. Mit GridViewColumn-Elementen definieren Sie die Anzahl und die Beschriftung (Attribut Header) der Spalten. Der Inhalt wird allerdings nicht durch spezielle Elemente, sondern durch Data Binding (vgl. entsprechendes Kapitel) bereitgestellt. Die Verknüpfung erfolgt innerhalb eines GridViewColumn-Elements über die Eigenschaft DisplayMemberBinding.
175
Kapitel 6
Um eigene Darstellungen (Views) für einen ListView zu erstellen, leiten Sie eine eigene Klasse von der Klasse ViewBase ab. Die Konfiguration der Darstellung eines GridViews erfolgt über Vorlagen (Templates), die für die Anzeige der Spaltenüberschriften und der Einträge definiert werden.
BEISPIEL AUF DER CD Basis der Anzeige sind die Daten einer sogenannten XML-Dateninsel (XML-Daten, die in der XAML-Datei eingebettet sind). Diese Dateninsel wird in einem XmlDataProvider innerhalb der XAML-Datei definiert und enthält drei Datensätze. Über das Attribut x:Key erhält die Dateninsel den Namen Kunden. Der ListView wird dann über das Attribut ItemSource an diese Dateninsel gebunden. Innerhalb des ListViews wird dann unter der Eigenschaft View ein GridView für die Anzeige der Daten eingesetzt. Innerhalb des Grids (der Tabelle) werden nun einige Spalten definiert. Für jede Spalte werden eine Überschrift (Header), die Breite und die Verknüpfung mit der Datenquelle hergestellt (DisplayMemberBinding). Über XPath-Ausdrücke selektieren Sie die Daten aus der XML-Dateninsel.
Abbildung 6.17: Tabellarische Darstellung im ListView
Insbesondere die beiden Komponenten ListView und TreeView verlangen dem Umsteiger von Windows Forms einiges ab, da jetzt viele Dinge manuell implementiert werden müssen. In den mit dem Windows SDK mitgelieferten Beispielen befinden sich glücklicherweise auch einige, welche die Grundeigenschaften der bekannten Komponenten nachbilden. Sie finden die Beispiele in den Verzeichnissen \Controls\ListViewCustomView, \Controls\ListViewSort oder \Controls\TreeViewDataBinding.
6.7.2 InkCanvas Möchten Sie ein einfaches Zeichenprogramm erstellen, gibt es das nun nahezu codefrei über die InkCanvas-Komponente. Gedacht ist die Verwendung dieser Komponente für Tablet PCs, die über einen Eingabestift (Stylus) verfügen. Mittels dieses Eingabestifts kann der Tablet PC bedient werden, es stehen sogar Möglichkeiten zur Verfügung, die Schrifteingabe zu analysieren. Allerdings kann diese Technik genauso gut mit einer Maus auf Standard-PCs verwendet werden und z. B. zum Zeichnen oder zur Anbringung von Markierungen innerhalb von Zeichnungen dienen. Basis eines InkCanvas ist die InkCanvas-Komponente und darin eingebettete Striche (Strokes), die durch Zeichenpunkte (StylusPoints) definiert werden. Die Striche können mit dem Eingabegerät, aber auch über Code erzeugt werden. Außerdem können Sie in ein InkCanvas wie in einen Layoutcontainer beliebige UI-Elemente einfügen, da die Komponente ebenfalls über eine Eigenschaft Children verfügt. Indem Sie ein InkCanvas verwenden, können Sie sofort mit der Maus (oder eben dem Eingabestift) darin zeichnen. Die Größe des Canvas richtet sich nach dem umgebenden Layoutcontainer und den Werten in den Eigenschaften Height und Width. Innerhalb des InkCanvas können Sie die Komponenten mittels der angehängten Eigenschaften Left, Top, Right und Bottom positionieren. Im folgenden Code werden eine TextBox und eine Linie in ein InkCanvas eingefügt (was aus noch zu erläuternden Gründen durchaus sinnvoll sein kann). Geben Sie keine Positionierung innerhalb des InkCanvas an, werden die Komponenten links oben ausgerichtet.
177
Kapitel 6
Damit etwas Leben in die Zeichnungen kommt, lassen sich verschiedene Attribute konfigurieren, z. B. die Zeichenfarbe (vom Typ Color!) sowie die Form und die Größe des Zeichenstifts. Um diese zu ändern, verwenden Sie die Eigenschaft Default DrawingAttributes vom Typ DrawingAttributes. Diese Klasse besitzt unter anderem die folgenden Eigenschaften: Eigenschaft
Beschreibung
Color
Stiftfarbe vom Typ Color.
FitToCurve
Falls True, wird die Abrundung der Linienzüge durch Bézierkurven aktiviert.
Height, Width
Setzen die Höhe und Breite des Zeichenstiftes.
StylusTip
Über die Werte Rectangle (Standard) und Ellipse können ein rechteckiger und runder Zeichenstift verwendet werden.
Tabelle 6.8: Eigenschaften der Klasse System.Windows.Ink.DrawingAttributes
Im Folgenden wird die Stiftfarbe des InkCanvas mit dem Namen InkC1 in XAML und im Code auf die Farbe Blau gesetzt. ... InkC1.DefaultDrawingAttributes.Color = Brushes.Blue.Color;
Neben einfachen Zeichenfunktionen bietet das InkCanvas aber noch wesentlich mehr. Über die Eigenschaft EditingMode (im Zusammenhang mit Tablet PCs spielen auch die Eigenschaften ActiveEditingMode und EditingModeInverted eine Rolle) können Sie zwischen mehreren Bearbeitungsmodi wechseln. Die Werte stammen aus der Aufzählung InkCanvasEditingMode. Bearbeitungstyp
Beschreibung
EraseByPoint
Schaltet in den Löschmodus um, der einen Bereich um den Eingabestift löscht.
EraseByStroke
Wenn Sie auf einen eigenständigen Zeichenabschnitt (aber keine anderen UIElemente) klicken, wird dieser vollständig entfernt.
GestureOnly
Es wird nur auf die Stiftbewegung reagiert. Dazu wird auf einem Standard-PC z. B. eine Hilfslinie angezeigt, die nach dem Loslassen der Maustaste wieder gelöscht wird.
Tabelle 6.9: Bearbeitungsmodi eines InkCanvas
178
Komponenten
Bearbeitungstyp
Beschreibung
Ink
Hiermit werden die Zeicheneingaben erfasst.
InkAndGesture
Dies ist eine Verknüpfung zwischen Ink und GestureOnly.
None
Es wird überhaupt nicht auf die Eingabe reagiert.
Select
Jetzt können Sie mit der Maus Eingaben selektieren. Diese werden dann mit einem Rahmen und Ziehpunkten versehen dargestellt. Durch das Ziehen einer Linie können Sie auch Bereiche markieren. Die markierten Elemente (damit können wirklich alle enthaltenen Elemente markiert werden) können Sie verschieben oder mittels der (Entf)-Taste löschen. Über den Ziehrahmen lassen sich wiederum nur die Zeicheneingaben vergrößern oder verkleinern.
Tabelle 6.9: Bearbeitungsmodi eines InkCanvas (Fortsetzung)
Über einen InkPresenter können Zeichendaten vorher gesammelt und dann vollständig ausgegeben werden. Dies ist ein dreistufiger Schritt. Zuerst werden StylusPointObjekte erstellt, die jeweils einen Punkt der Zeichenlinie darstellen. Diese werden dann einer StylusPointCollection hinzugefügt. Die Collection wird wiederum an ein Stroke-Objekt übergeben, sodass ein Linienzug entsteht. Diesen Linienzug können Sie nun mittels Strokes.Add() einem InkCanvas oder einem InkPresenter hinzufügen. Der InkPresenter wird dann als eigenständige UI-Komponente angezeigt. InkPresenter ip = new InkPresenter(); StylusPoint sp1 = new StylusPoint(10, 10); StylusPoint sp2 = new StylusPoint(50, 50); ... StylusPointCollection sps = new StylusPointCollection(); sps.Add(sp1); sps.Add(sp2); ... Stroke str = new Stroke(sps); ip.Strokes.Add(str); // dem InkPresenter hinzufügen InkC1.Strokes.Add(str); // oder einem InkCanvas hinzufügen SpToolbar.Children.Add(ip); // InkPresenter im StackPanel einf. Listing 6.26: Beispiele\Kap06\InkCanvasProj\Window1.xaml.cs
Anwendungsbeispiel Ein Einsatzgebiet eines InkCanvas ist die Verwendung als Folie, die über einen Hintergrund gelegt wird. Der Hintergrund kann z. B. eine Landkarte darstellen, die als ImageKomponente in ein InkCanvas geladen wird. Mittels der Zeichenfunktionen können Sie in der Karte beliebige Zeichenoperationen durchführen. Dabei stehen Ihnen wieder mehrere Optionen zur Verfügung. Sie können z. B. alle Zeichnungen löschen oder aber auch speichern oder laden. Zeichnungen gehen also nicht verloren, sodass Skizzen dauerhaft aufgehoben werden können. Das Speicherformat ist ausnahmsweise nicht XAML, sondern ein Binärformat.
179
Kapitel 6 FileStream fs = new FileStream(sfd.FileName, FileMode.Create); InkC1.Strokes.Save(fs); fs.Close();
Damit wäre die Kurzeinführung in das InkCanvas beendet. In einem etwas umfangreicheren Beispiel wird noch einmal alles zusammengefasst.
BEISPIEL AUF DER CD Innerhalb einer Toolleiste am linken Rand werden verschiedene Einstellmöglichkeiten für die Arbeit mit dem InkCanvas angeboten. Als Besonderheit zum Layout wird auf die Auswahl der Zeichenfarbe verwiesen. Dazu wird eine ListBox eingesetzt, deren Elemente in einem UniformGrid angeordnet werden. Die aktuell ausgewählte Farbe befindet sich in der Eigenschaft SelectedValue. Dazu wurde der Eigenschaft SelectedValuePath der ListBox der Name der Eigenschaft Fill zugewiesen. Die Funktionalität steckt diesmal größtenteils in der Code-Behind-Datei. Dazu wird auf verschiedene Ereignisse der ListBox, von Buttons, ComboBoxen und CheckBoxen reagiert. Im InkCanvas werden beim Start der Anwendung zwei Linien und etwas Text angezeigt. Außerdem wird ein Ausschnitt eines »Stadtplans« dargestellt. Jetzt können Sie mit den Einstellungen experimentieren. Wurde als Bearbeitungsmodus INK gewählt, können Sie zeichnen. Die beiden Halbkreise rechts oben wurden einmal mit und einmal ohne aktivierte Abrundung (FitToCurve) gezeichnet. Deshalb wird der obere Kreisbogen abgerundeter dargestellt. Weiterhin wurde im Stadtplan ein Weg eingezeichnet. Dieser könnte nun mit allen anderen Zeichnungen (nur die Zeichnungen, keine anderen UI-Elemente) gespeichert und später wieder geladen werden. Über den Button LADE VORDEF. kann eine Zeichnung über einen InkPresenter geladen werden. Außerdem wird die Zeichnung dabei auch in das InkCanvas übertragen. Im Bearbeitungsmodus SELECT wurde ein gezeichneter Kreis markiert und soll nun verschoben werden.
Abbildung 6.18: Selektieren und verschieben einer Zeicheneingabe
Über die ComboBox wählen Sie den Bearbeitungsmodus aus. Je nach ausgewähltem Eintrag wird dieser in der Methode OnEditingModeChanged() gesetzt. Die Prüfung InkC1 != null verhindert, dass bei der Initialisierung der Seite die Ereignisbehandlung aufgerufen wird, obwohl InkC1 noch nicht erzeugt wurde. private void OnEditingModeChanged(object sender, SelectionChangedEventArgs e)
Die folgenden Methoden dienen dazu, die Verarbeitung im InkCanvas zu steuern. In der ListBox steht in der Eigenschaft SelectedValue der ausgewählte Eintrag, d. h. die Füllfarbe des betreffenden Rechtecks, zur Verfügung. Da die Stiftfarbe vom Typ Color
182
Komponenten
ist, muss diese Farbe von einem SolidColorBrush (ein einfarbiges Füllmuster) über die Eigenschaft Color bestimmt werden. In der Methode OnStylusTipChanged() wird die Umstellung der Stiftart (Rechteck, Ellipse) bewerkstelligt. Dies ist nur dann erkennbar, wenn Sie eine größere Stiftbreite, z. B. 5, verwenden. Die Maße des Eingabestiftes werden in der Methode OnNewStylusWidth() gesetzt. Dazu werden die Breite und die Höhe auf das gleiche Maß gesetzt, sie könnten aber auch getrennt bearbeitet werden. Über die CheckBox kann zum Abschluss zwischen abgerundeten und unveränderten Kurven gewählt werden. private void OnColorChanged(object sender, SelectionChangedEventArgs e)
{ SolidColorBrush b = LbColor.SelectedValue as SolidColorBrush; InkC1.DefaultDrawingAttributes.Color = b.Color; } private void OnStylusTipChanged(object sender, SelectionChangedEventArgs e)
{ if(InkC1 != null)
{ switch(CbStylusTip.SelectedIndex)
{ case 0: InkC1.DefaultDrawingAttributes.StylusTip = StylusTip.Ellipse; break; case 1: InkC1.DefaultDrawingAttributes.StylusTip = StylusTip.Rectangle; break;
Die beiden letzten Methoden der Code-Behind-Datei dienen zum Laden und Speichern der Zeichnungen in einem InkCanvas. Zum Laden werden die Daten aus der Datei in eine StrokeCollection eingebettet, die wiederum der Eigenschaft Strokes des InkCanvas zugewiesen wird. Alternativ könnte man die StrokeCollection auch über die Methode Add() der Eigenschaft Strokes den bereits vorhandenen Eingaben hinzufügen. Das Speichern geht ebenfalls einfach vonstatten, da die Eigenschaft Strokes über eine Methode Save() verfügt, welche die Daten in einen Stream speichern kann. private void LoadInk(object sender, RoutedEventArgs e)
6.7.3 RichTextBox Die RichTextBox ist Ihnen sicher vom gleichnamigen .NET Framework-Kontrollelement her bzw. als Windows-Standardkomponente bekannt. Allerdings wird der Inhalt in der WPF auf wesentlich leistungsfähigere Weise verarbeitet. Statt über RichTextSteuerzeichen kann die Formatierung über Tags, ähnlich HTML-Tags, vorgenommen werden. Über diese Tags können Sie Text auf einfache Weise fett oder kursiv darstellen oder mittels
- und -Tags als Tabelle oder als Liste formatieren. Der Zugriff über Code ist ebenfalls möglich und zur Bearbeitung von Textinhalten auch notwendig. Basis dieser Textformatierung ist ein FlowDocument-Objekt, das als einziges Kindelement einer RichTextBox-Komponente zugelassen ist und sich hinter der Eigenschaft Document verbirgt. Die Verwendung von FlowDocument wird im Kapitel zur Textverarbeitung umfangreich erläutert.
184
Komponenten
Als Datenformate werden RTF, TEXT, XAML und XAML-Packages unterstützt. Zum Laden und Speichern der verschiedenen Dokumentformate verwenden Sie ein TextRangeObjekt. Dieses wird es mit einem bestimmten Bereich des FlowDocuments verbunden. Um bestimmte Textbereiche zu formatieren, werden diese z. B. in einer Textverarbeitung markiert, und darauf wird dann eine Formatierung angewandt. In einer RichTextBox gibt es dazu eine Eigenschaft Selection (vom Typ TextSelection). Diese Eigenschaft erlaubt die Bearbeitung des markierten Textes. Die Klasse TextSelection besitzt eine Eigenschaft ApplyPropertyValue, über die Sie eine Eigenschaft des Textes mit einem neuen Wert versehen können. Im folgenden Codeabschnitt wird z. B. die Schriftstärke auf »fett« gesetzt. Selection.ApplyPropertyValue(FlowDocument.FontWeightProperty, FontWeights.Bold);
BEISPIEL AUF DER CD Das Beispiel zeigt einige Vorgehensweisen zur Implementierung eines einfachen Texteditors. Es existieren Menüpunkte zum Laden, Speichern und Erstellen eines neuen leeren Dokuments. Der selektierte Text kann fett und kursiv formatiert werden. Außerdem werden in einer ComboBox mehrere Textformate zur Auswahl angeboten. Diese müssen vor dem Laden und Speichern der Dokumente ausgewählt werden, um den jeweiligen Dokumenttyp zu laden bzw. um ein Dokument in diesem Format zu speichern. Die aktuelle Textlänge wird in der Statusleiste angezeigt.
Abbildung 6.19: Editor mit Unterstützung verschiedener Textformate
185
Kapitel 6 Punkt 1 Punkt 2 Listing 6.31: Beispiele\Kap06\RichTextBoxProj\Window1.xaml
Ein neues Dokument wird auf die Weise erstellt, indem Sie ein neues FlowDocument erzeugen und der Eigenschaft Document der RichTextBox zuweisen. Möchten Sie gleich etwas Inhalt einfügen, wird ein neuer Textblock erzeugt und über die Methode Add() der Collection Block hinzugefügt. Als Block wird ein neuer Absatz (Paragraph) erzeugt. Darin wird ein Fließtext (Run) eingefügt, der fett (Bold) formatiert wird. private void NewDocument(object sender, RoutedEventArgs e)
{ RtbMain.Document = new FlowDocument(); RtbMain.Document.Blocks.Add(new Paragraph( new Bold(new Run("... neues Dokument ...")))); } Listing 6.32: Beispiele\Kap06\RichTextBoxProj\Window1.xaml.cs
186
Komponenten
Zur Verwendung der Dialoge zum Öffnen und Speichern einer Datei werden die neuen Dialogklassen der WPF verwendet. Je nach ausgewähltem Dateiformat wird der Filterindex gesetzt (d.h., Sie können nur eine Datei des ausgewählten Formats in der ComboBox laden). Wurde eine Datei ausgewählt, wird ein FileStream erzeugt, der die Datei zum Lesen öffnet. Danach wird ein TextRange-Objekt erzeugt, welches das gesamte Dokument als Inhalt besitzt. In das Dokument wird dann über die Methode Load() der Inhalt der Datei geladen. Wichtig ist die Angabe des Datenformats der Datei, das dem ausgewählten Eintrag der ComboBox entspricht (CbMain.Text). public void OpenDocument(object sender, RoutedEventArgs e)
Das Speichern eines Dokuments erfolgt genau auf umgekehrte Weise zum Öffnen. Die Datei wird diesmal zum Schreiben geöffnet und über die Methode Save() des TextRangeObjekts der Inhalt des Dokuments darin gespeichert. public void SaveDocument(object sender, RoutedEventArgs e)
Zum Formatieren von markiertem Text wird die Methode ApplyPropertyValue() der Selection-Eigenschaft der RichTextBox-Komponente verwendet. Achten Sie immer darauf, die passenden Typen (Eigenschaftstyp und Eigenschaftswert) miteinander zu verwenden. Der erste Parameter ist der Typ der Eigenschaft, die bearbeitet werden soll. Im Falle der Fettschrift ist dies die Eigenschaft FontWeightProperty. Der neue Wert Bold stammt aus der Klasse FontWeights. public void SetBold(object sender, RoutedEventArgs e)
{ RtbMain.Selection.ApplyPropertyValue(FlowDocument.FontWeightProperty, FontWeights.Bold); } public void SetItalic(object sender, RoutedEventArgs e)
Um in der Statusleiste die aktuelle Zeichenzahl im Dokument anzuzeigen, muss wieder einmal der Umweg über das TextRange-Objekt gegangen werden. Dazu wird das gesamte Dokument markiert, über die Eigenschaft Text.Length die Dokumentlänge ausgelesen und dem Label in der Statusleiste zugewiesen. public void ShowInfo(object sender, TextChangedEventArgs e)
6.7.4 TabControl Ein TabControl besteht aus ein oder mehreren TabItems (Registerkarten). Ein TabItem besteht wiederum aus einer Beschriftung (Eigenschaft Header) und einem Inhaltsbereich (Eigenschaft Content), in dem die Elemente für diese Registerkarte eingefügt werden. Wie bei fast allen anderen Komponenten können die Beschriftung des Registers sowie der Inhalt völlig individuell gestaltet werden.
188
Komponenten
Über die Eigenschaft TabStripPlacement des TabControls geben Sie an, wo die Registerkarten angezeigt werden sollen (Left, Right, Top, Bottom). Mit der Eigenschaft IsSelected eines TabItems lässt sich ermitteln (oder setzen), ob ein TabItem aktiviert ist. Einfacher geht es, wenn Sie die Eigenschaften SelectedIndex bzw. SelectedItem des TabControls auswerten.
BEISPIEL AUF DER CD Im Beispiel wird ein einfaches TabControl mit zwei Seiten angelegt, die zweite Seite wurde bereits selektiert (IsSelected="True" im TabItem). Der Inhalt des Tabs kann direkt oder über ein -Element gefüllt werden. Im zweiten Tab wurde nicht nur ein Text, sondern auch eine kleine Grafik eingefügt.
Abbildung 6.20: TabControl, in dem die zweite Registerkarte selektiert ist
Excepteur sint ... Tab 2
189
Kapitel 6 Excepteur sint ... Listing 6.37: Beispiele\Kap06\TabControlProj\Window1.xaml
In der Statusleiste wird in einer Label-Komponente der aktuelle Index der ausgewählten Registerkarte angezeigt. Um einzelne Registerkarten zu bearbeiten, kann die Eigenschaft Items genutzt werden. Um eine Registerkarte zu bearbeiten, muss ein Item in den Typ TabItem umgewandelt werden. private void OnTabChanged(object sender, SelectionChangedEventArgs e)
6.7.5 TreeView Eine Baumansicht wird sehr häufig für die strukturierte Darstellung von Informationen genutzt. Die Elemente eines Baums werden dazu beliebig verschachtelt dargestellt, wobei die einzelnen Ebenen zu- oder aufgeklappt werden können. Zur Darstellung dieser Baumansicht wird die TreeView-Komponente verwendet. Darin werden die Elemente beliebig verschachtelt in TreeViewItem-Komponenten untergebracht. Die Klasse TreeView stellt mit der Eigenschaften SelectedItem den Zugriff auf den aktuell gewählten Eintrag bereit. Die TreeViewItems können über die Eigenschaft IsSelected daraufhin überprüft werden, ob sie ausgewählt sind. Mittels der Eigenschaft IsExpanded prüfen Sie, ob sie aufgeklappt sind, bzw. setzen diesen Status entsprechend. Über die Eigenschaft Header eines TreeViewItems konfigurieren Sie seine Darstellung. Damit stehen Sie vor einem kleinen Problem, denn ein TreeView-Element kann dadurch ein beliebiges Aussehen besitzen, es gibt außerdem keinerlei vordefinierte Attribute wie Text oder Image/ImageIndex. Die Darstellungsfrage ist demnach absolut Ihre Aufgabe. Um auf die Auswahl eines TreeView-Elements zu reagieren, schreiben Sie einen Ereignishandler für das Ereignis SelectedItemChanged des TreeViews.
190
Komponenten ...
Statt die Elemente einzeln zu konfigurieren, können Vorlagen definiert werden, oder Sie schreiben eigene Klassen, die bereits den Aufbau eines TreeViewItem-Elements festlegen und mehr Informationen liefern. Als einfache Lösung kann auf die Eigenschaft Tag zurückgegriffen werden, der Sie ein beliebiges Objekt zuweisen können (die Eigenschaft ist zur freien Verwendung gedacht). Darin können Sie für einen TreeViewEintrag weitere Informationen hinterlegen, die ihn auch später wieder eindeutig identifizieren.
BEISPIEL AUF DER CD Das Beispiel zeigt die Herstellung eines Grundaufbaus eines TreeViews, mit und ohne Verwendung komplexerer Einträge. Möchten Sie Grafiken unterbringen, müssen Sie diese selbst mit dem Text anordnen und ausrichten. Dazu eignen sich später Templates oder eigene Klassen besser. Beim Klick auf ein TreeViewItem-Element wird das Ereignis SelectedItemChanged ausgelöst und ausgewertet. In der Statusleiste werden dann die String-Repräsentationen der Header-Eigenschaften entsprechend der Hierarchie und der Elementindex (hier über die Eigenschaft Tag ermittelt) ausgegeben.
Abbildung 6.21: Aufgeklappter TreeView mit ausgewähltem Element
Wenn ein Eintrag selektiert wird, wird innerhalb der Hierarchie so lange aufwärts (Eigenschaft Parent) gesucht, bis der TreeView gefunden wird. Die String-Repräsentationen der Header-Eigenschaft der TreeViewItem-Elemente werden zusammengefügt, um die Verschachtelung darzustellen. Dabei wird ersichtlich, dass sich die Verwendung komplexer Header hier eher negativ auswirkt. Statt einer Beschriftung wird der Typ des Containers angezeigt. Zur Lösung können der Eigenschaft Tag (per Code) Objekte zugewiesen werden, die ein TreeViewItem-Element besser identifizieren und mehr Informationen liefern, unabhängig vom Aufbau und der Darstellung der Elemente. Im Beispiel wird über die Eigenschaft Tag der Index des Elements bestimmt, der hier im XAML-Code hinterlegt wurde (und im wahren Leben sicher per Code zugewiesen wird). private void OnNewTvItem(object sender, RoutedEventArgs e)
{ TreeViewItem tvi = TvMain.SelectedItem as TreeViewItem; String position = tvi.Header.ToString(); String index = tvi.Tag.ToString(); while(tvi.Parent.GetType() != TvMain.GetType()) { tvi = tvi.Parent as TreeViewItem; position = position + "> " + tvi.Header.ToString(); } TbSelItem.Text = position + ", Index: " + index; } Listing 6.40: Beispiele\Kap06\TreeViewProj\Window1.xaml.cs
193
7
2D-Grafik
7.1 Grundlagen Der Hauptgrund, mit der WPF zu arbeiten, liegt sicher in den besonderen grafischen Fähigkeiten dieses Frameworks. Da alle Dinge, die Sie mit der WPF tun, letztendlich auf den Grafikfähigkeiten des ausführenden Rechners basieren, wurden hier die meisten Erweiterungen vorgenommen. Die grafischen Fähigkeiten der WPF beschränken sich nicht nur auf einfache Zeichenfunktionen, sondern beinhalten auch Bibliotheken für die Sound- und Videowiedergabe, die Anzeige von formatiertem Text, Animationen und 3D-Grafiken. Dabei verwendet die WPF eine GrafikEngine, die unabhängig von der Auflösung und dem Ausgabegerät ist. Es wird in sogenannten geräteunabhängigen Pixeln (Device Independend Pixel) gerechnet, deren Größe 1/96 Inch (Zoll = 2,54 cm) betragen. Hat ein Rechteck beispielsweise auf einem Bildschirm, der mit 96 dpi (dot per inch – Punkte per Inch) arbeitet, eine Länge von 96 Pixel, würde es bei einer 25% höheren dpi-Zahl von 120 dpi über physikalische 120 Pixel dargestellt werden. Der Vorteil dieser Vorgehensweise ist, dass die Größe des Rechtecks unabhängig von der dpi-Zahl immer gleich ist. Warum wurde nun ausgerechnet 1/96 Inch als Einheit verwendet? Sicher deshalb, weil die typischen Windows-PCs genau mit dieser dpi-Zahl arbeiten. Hier entspricht demnach 1 geräteunabhängiger Pixel genau einem physikalischen Pixel.
Kapitel 7
Für eine noch bessere Genauigkeit sorgt die Verwendung von double-Werten für die Angabe von Koordinaten, die dadurch genauere Transformationen erlauben. Diese werden später bei der Anzeige auf dem Bildschirm wieder auf eine ganze Zahl abgebildet. Für die Entwicklung von GUIs (Graphical User Interface – grafische Benutzerschnittstelle) ist von Bedeutung, dass Grafiken praktisch überall eingesetzt werden können. Wenn Sie einen geschickten Grafiker kennen, lassen Sie sich eine Grafik erstellen und diese als XAML-Datei abspeichern (z. B. über Expression Design). Dann binden Sie diesen Code in Ihre Anwendung ein, z. B. als Hintergrund eines Fensters, einer ToolBar oder als Symbol in einem Button. Ein UI-Element kann sich prinzipiell auch aus mehreren Grafikelementen wie Rechtecken, Ellipsen und Linien zusammensetzen. In anderen Frameworks wie Windows Forms ist dies z. B. nicht möglich. Ein weiterer Vorteil besteht darin, dass sich die Grafikelemente später auch im UIBaum befinden, der für die Erzeugung der Darstellung auf dem Bildschirm verwendet wird. Dadurch können Sie jederzeit auf einzelne Elemente über Programmcode zugreifen und diese manipulieren. Zum Update der Darstellung auf dem Bildschirm müssen Sie nicht mehr spezielle Methoden aufrufen, stattdessen kümmert sich die WPF darum und zeichnet die betreffenden Bereiche automatisch neu.
Beispiel Öffnen Sie die Anwendung XAMLPad über START – PROGRAMME – MICROSOFT WINDOWS SDK – TOOLS – XAMLPAD. Fügen Sie den folgenden Code ein. Hier öffnen Listing 7.1: Beispielcode für einen Button mit Text und Grafik
196
2D-Grafik
Der Inhalt eines Elements, in diesem Fall eines Buttons, kann über das Attribut Content (fast) individuell gefüllt werden. Möchten Sie mehrere Elemente verwenden, nutzen Sie die Property-Element-Syntax Button.Content. Da der Inhalt aus zunächst einem Element bestehen darf, wird zuerst ein Container eingesetzt. Jetzt können Sie Komponenten, Text- und Grafikelemente beliebig mischen, um den Inhalt zu erstellen. Die Grafikelemente sind hier nochmals in einem Canvas eingeschlossen, um sie besser positionieren zu können. Die verschiedenen Margin-Randangaben wurden verwendet, damit die Elemente horizontal ausgerichtet angezeigt werden.
Abbildung 7.1: Der Inhalt des Buttons besteht aus verschiedenen Elementen
7.2 Farbverwaltung Farben lassen sich in XAML über ihren Namen in den entsprechenden Eigenschaften angeben. Im Code erhalten Sie über die Struktur Color aus dem Namespace System. Windows.Media Zugriff auf die literalen Farbdefinitionen, z. B. Color.Red. Hier ist es zu Beginn wichtig, auf den Unterschied zwischen einem Füllmuster (Brush) und einer Farbe (Color) zu achten. Beide verwenden dieselben Literale für die Farbnamen (z. B. Red), allerdings handelt es sich nicht um denselben Typ. Um z. B. der Eigenschaft Fill eines Rechtecks ein Füllmuster zuzuweisen, muss ein Brush-Objekt verwendet werden. Geben Sie ein Literal oder eine Farbcodierung in XAML an, sorgen Wertkonvertierer für die korrekte Datentypzuweisung. Eine alternative Angabe einer Farbe ist die Verwendung der hexadezimalen Kodierung, z. B. Fill="#11223344", wobei die Reihenfolge der Werte in diesem Beispiel ARGB (Alpha, Red, Green, Blue: Transparenz, Rot, Grün, Blau) ist. Jeder Wert wird durch den Typ Byte angegeben, also mit einem Wert zwischen 0 und 255. Die Verwendung der hexadezimalen Codierung besitzt mehrere Schreibweisen, bei denen Teilinformationen weggelassen werden dürfen. #AARRGGBB – vollständige Angabe #ARGB – nur 1 Byte pro Farbanteil und Transparenz, entspricht #AARRGGBB #RRGGBB – keine Transparenzangabe #RGB – nur jeweils 1 Byte pro Farbanteil, ohne Transparenz, entspricht #RRGGBB
Weitere Kodierungsvarianten stehen über die folgenden Schreibweisen zur Verfügung, in denen die Farbwerte einmal im Bereich von 0.0 bis 1.0 (scRGB, nach IEC Standard 61966-2-2) und einmal im Bereich von 0 bis 255 angegeben werden.
197
Kapitel 7
Die Verwendung einer simplen Farbe ist damit auch der einfachste Weg, ein grafisches Elemente farbig darzustellen.
BEISPIEL AUF DER CD In der XAML-Datei werden sechs verschiedene Varianten zur Farbauswahl verwendet. Im Ereignis Loaded des Fensters wird für ein fünftes Rechteck die Farbauswahl im Code durchgeführt. Um das Rechteck ansprechen zu können, wurde ihm der Name CodeRect gegeben. Um die Farbwerte über das Element Color in XAML zu setzen, muss etwas weiter ausgeholt werden. Da die Eigenschaft Fill mit einem komplexen Wert belegt werden soll, wird die Property-Element-Schreibweise verwendet. Danach wird ein einfarbiges Füllmuster mit der gewünschten Farbe definiert.
Abbildung 7.2: Verschiedene Varianten der Farbauswahl
Systemfarben Über die Klasse SystemColors aus dem Namespace System.Windows stehen über zahlreiche statische Eigenschaften auch die aktuellen Systemfarben zur Verfügung. Die statischen Eigenschaften stehen für jeweils eine Systemfarbe, z. B. die Farbe der Titelleiste eines Fensters. Wenn Sie die Farben in XAML nutzen wollen, müssen Sie darauf wie auf eine Ressource zugreifen (vgl. entsprechendes Kapitel). Um auf Änderungen der Systemfarben zu reagieren, binden Sie diese als dynamische Ressource ein. Das statische Einbinden erfolgt dagegen nur beim Programmstart und benötigt deshalb geringfügig weniger Systemressourcen in Ihrer Anwendung. Welche Variante Sie wählen, hängt von den Erfordernissen Ihrer Anwendung ab. Da es sich bei der Systemfarbe um eine statische Eigenschaft der Klasse SystemColors handelt, muss die Markup-Erweiterung x:static zum Zugriff angegeben werden. Jetzt fehlt nur noch die Angabe der Farbe. Da Sie auf die Farbe in Form einer Ressource verweisen, dürfen Sie nicht den Farbnamen angeben, sondern müssen einen Namen verwenden, der die Ressource bezeichnet, die wiederum auf die Farbe verweist. Handelt es sich um eine Ressource, die eine Farbe bezeichnet, erkennen Sie dies am Suffix Color (z. B. ActiveCaptionColor). Den Namen der Ressource erhalten Sie durch das Anhängen des Suffixes Key (ActiveCaptionColorKey).
7.3 Grafikprimitive 7.3.1 Einführung Wie schon das Windows API und die ersten Versionen des .NET Frameworks besitzt auch die WPF die Fähigkeit, einfache Grafikprimitive wie Linien, Kreise oder Rechtecke darzustellen. Sämtliche Klassen dazu befinden sich im Namespace System. Windows.Shapes. Ein wichtiges Merkmal ist die Tatsache, dass innerhalb der Vererbungshierarchie die Grafikprimitive (Shapes) indirekt von UIElement abgeleitet sind. Dadurch können Sie genau wie Buttons oder TextBoxen verwendet werden. Konkret sind alle Grafikprimitive von der abstrakten Klasse Shape abgeleitet und besitzen somit eine identische Grundfunktionalität, was das Festlegen von Füllmustern oder Stifteigenschaften angeht.
Abbildung 7.3: Vererbungshierarchie der Grafikprimitive
200
2D-Grafik
Die Klasse Shape stellt einige neue Eigenschaften für die Zeichenelemente bereit. Mittels der Eigenschaft Fill kann ein Füllmuster (bzw. ein Pinsel, engl. Brush) angegeben werden, der bei geschlossenen Flächen deren Farbe festlegt. Damit sich ein Grafikelement an den umgebenden Bereich einpassen kann, können Sie die Eigenschaft Stretch verwenden. Außerdem werden für die Definition des Rahmens bzw. für Linien und Linienzüge mehrere StrokeXXX-Eigenschaften bereitgestellt, mittels derer Sie diese konfigurieren können. Mehr dazu später. Die WPF kennt sechs Grafikprimitive. Über weitere Elemente lassen sich noch wesentlich komplexere Strukturen darstellen. Grafikelement
Beschreibung
Ellipse
Es können Ellipsen und Kreise dargestellt werden, die durch ihre Höhe und Breite parametrisiert sind.
Line
Es wird eine Linie zwischen zwei Punkten gezogen, die durch ihre X- und Y-Koordinaten definiert sind.
Path
Über einen Pfad kann in einem Geometrieelement eine komplexe Struktur gezeichnet werden, die aus Linien, Bögen oder Rechtecken besteht. Dazu werden Koordinaten mit Kommandos versehen.
Polygon
Ein Polygon wird durch eine Serie von Punktkoordinaten definiert, wobei automatisch der letzte mit dem ersten Punkt verbunden wird. Auf diese Weise entsteht immer mindestens eine geschlossene Fläche. Diese kann mit einem Füllmuster gefüllt werden.
Polyline
Im Gegensatz zum Polygon werden in einer Polyline der erste und letzte Punkt nicht automatisch miteinander verbunden.
Rectangle
Hiermit erzeugen Sie Rechtecke oder Quadrate, die durch ihre Höhe und Breite definiert sind.
Tabelle 7.1: Übersicht der Grafikprimitive
In einem Canvas-Layoutcontainer lassen sich UI-Elemente frei positionieren. Entweder wird als Ausgangspunkt die linke obere Ecke verwendet, oder Sie geben mittels Canvas.Left und Canvas.Top eine andere Ausgangskoordinate an. In diesem Fall summieren sich die Werte der oberen linken Ecke und beispielsweise der Breite und Höhe eines Grafikelements. Die Überdeckung der Elemente hängt dabei von der Einfügereihenfolge ab.
Geben Sie in den Grafikelementen keine Position innerhalb des Canvas an, werden diese ausgehend von der linken oberen Ecke positioniert. Statt direkt innerhalb eines Canvas bzw. eines anderen Layoutcontainers können Grafikelemente wie bereits erwähnt auch in UI-Elementen zum Einsatz kommen. So lassen sich z. B. die Elemente einer ListBox oder ComboBox auch aus Grafikelementen zusammenstellen.
BEISPIEL AUF DER CD Innerhalb einer ComboBox-Komponente werden statt Texteinträgen einige Grafikelemente verwendet. Dazu wird in den ComboBoxItem-Elementen einfach ein Grafikelement als Inhalt des Eintrages verwendet. Es wären auch komplexere Zeichnungen möglich, in diesem Fall müssten Sie die Grafikelemente wiederum in einen Container wie z. B. ein Canvas einbetten.
Abbildung 7.5: ComboBox mit Elementen aus Grafikprimitiven
Rechtecke und Ellipsen Die wichtigsten Eigenschaften für eine Ellipse und ein Rechteck sind Width (die Breite), Height (die Höhe), Stroke (Rahmenlinie) und Fill (das verwendete Füllmuster). Bei den Füllmustern wird im folgenden Beispiel eine bereits vordefinierte Farbe verwendet. Später in diesem Kapitel werden Sie lernen, wie selbst definierte Farbverläufe erstellt und verwendet werden. Alle Grafikelemente, also auch Linien und Polylinien, besitzen keine Eigenschaften, um die Position zu setzen. Diese ergibt sich aus dem verwendeten, umgebenden Layoutcontainer. Wird beispielsweise ein Rechteck in einem StackPanel eingefügt, wird es in seiner Größe in den dafür vorgesehenen Bereich eingefügt. Die Angabe der linken oberen Ecke ist in einem StackPanel nicht möglich, es sei denn, Sie setzen die Eigenschaft Margin ein und positionieren das Rechteck über die Randeinstellungen. Zur genauen Positionierung ist demnach ein Canvas der ideale Kandidat, als Layoutcontainer für Grafikelemente zu dienen. Die Größe ergibt sich aus den beiden Eigenschaften Width und Height oder dem verwendeten Layoutcontainer. Verwenden Sie z. B. ein StackPanel und geben keine Breite an, wird diese bei einer vertikalen Ausrichtung auf die gesamte Breite des StackPanels ausgedehnt. Eine Besonderheit beim Rechteck sind seine beiden Eigenschaften RadiusX und RadiusY, über die Sie abgerundete Ecken erzeugen können. Geben Sie keinen Wert in den Eigenschaften Fill oder Stroke an, wird die Fläche nicht mit einer Farbe gefüllt (auch nicht mit Weiß oder Schwarz), und es wird auch kein Rahmen gezeichnet.
203
Kapitel 7
Abbildung 7.6: Eine gefüllte Ellipse in einem abgerundeten Rechteck
> >
>
HINWEIS
Geben Sie in einem Canvas für ein Rechteck oder eine Ellipse keine Breite und Höhe an und setzen den Rahmen auf eine Farbe (Stroke="Blue"), wird dennoch ein Punkt in der Rahmenfarbe gezeichnet.
Linien Eine Linie wird über zwei Punkte definiert, die über die Eigenschaftspaare (X1, Y1) und (X2, Y2) festgelegt werden. Durch den Einsatz von speziellen Stiften (Pen) können Sie z. B. Strich-Punkt-Linien erzeugen oder die Linienenden abgerundet oder als Spitze darstellen. Die Verwendung von Stiften wird im nächsten Abschnitt erläutert. Damit Sie von der gezeichneten Linie auch etwas sehen, müssen Sie die Stiftfarbe und die Stiftbreite festlegen. Die Stiftfarbe legen Sie über die Eigenschaft Stroke fest, die Stiftbreite mittels der Eigenschaft StrokeThickness. Obwohl auch eine Linie eine FillEigenschaft besitzt, die sie nämlich von der Klasse Shape erbt, hat deren Wert keine Bedeutung. Dies nimmt man allerdings in Kauf, um die Klassenhierarchie nicht weiter aufzublähen.
Abbildung 7.7: Zwei sich kreuzende, drei Punkte breite Linien
204
2D-Grafik
Polyline und Polygon Möchten Sie mehrere Linien hintereinander zeichnen, die zudem miteinander verbunden sind, bietet sich die Verwendung einer Polyline (Mehrfachlinie) oder eines Polygons an. Die einzelnen Punkte werden durch Koordinaten vom Typ Point innerhalb einer PointCollection angegeben. Die Collection ist wiederum über die Eigenschaft Points zugänglich. Bei einem Polygon werden zusätzlich der erste und der letzte Punkt automatisch miteinander verbunden, sodass immer ein geschlossenes Gebilde entsteht. Ansonsten ist die Verwendung der beiden Grafikelemente identisch. In XAML werden die einzelnen Punkte über Wertepaare angegeben, die jeweils mit Komma getrennt sind. Die Kommata sind allerdings optional und werden hier und auch in anderen derartigen Angaben vollständig ignoriert. Sie können aber zur besseren Lesbarkeit dienen. Zwischen den Paaren bzw. Werten müssen sich Leerzeichen befinden.
Um eine Strichzeichnung zu erstellen, ist es wie für das folgende Beispiel sicher sinnvoll, eine Skizze zu verwenden und darin die Koordinaten einzutragen bzw. aus geeignetem Papier daraus abzulesen. Geben Sie nur einen Wert für die Farbe des Zeichenstiftes an, wird der Inhalt der umschlossenen Flächen nicht gefüllt.
Abbildung 7.8: Eine Strichzeichnung
205
Kapitel 7
Füllregeln Die beiden Grafikelemente Polyline und Polygon sowie später auch Path besitzen noch eine interessante Eigenschaft FillRule. Damit wird festgelegt, ob umschlossene Flächen bei gesetzter Eigenschaft Fill ausgefüllt werden sollen oder nicht. Mögliche Werte für die Eigenschaft sind EvenOdd und Nonzero. Der Standardwert ist EvenOdd. Die Möglichkeit, pauschal alle umschlossenen Flächen zu füllen, existiert nicht. In diesem Fall müssten Sie die Grafik in weniger komplexe Grafiken aufteilen, die ihrerseits gefüllt dargestellt werden. Ist der Linienzug geschlossen (bei einem Polygon ist dies immer der Fall), wird die umschlossene Fläche durch die Füllfarbe in der Eigenschaft Fill gefüllt. Bei jeder von einer Polyline bzw. einem Polygon umschlossenen Fläche wird Fill berücksichtigt. Ist eine Polyline allerdings nicht geschlossen, hat die Angabe von Fill keine Auswirkung. Betrachten Sie einmal das Ergebnis der Strichzeichnung, wenn die Eigenschaft Fill mit den beiden Auffüllregeln verwendet wird. Links in der Abbildung 7.9 wird das Ergebnis gezeigt, das mit der Standardregel EvenOdd erzeugt wird, rechts sehen Sie das Ergebnis von Nonzero.
Die Füllregel EvenOdd arbeitet nach der folgenden Regel. Ausgehend von einem Punkt werden zwei Strahlen in zwei Richtungen gegen unendlich gezogen und dann die Anzahl der Schnittpunkte mit den Linienzügen je Strahl gezählt. Befindet sich der Punkt in der weißen Fläche (siehe oberer Punkt in Abbildung 7.10), schneiden seine beiden Strahlen die Linien je zweimal. Die Anzahl der Schnittmenge ist gerade, und es wird nicht gefüllt. Befindet sich der Punkt in einer ausgefüllten Fläche (der untere markierte Punkt der Abbildung), schneiden beide davon ausgehenden Strahlen die Linien in einer ungeraden Anzahl.
Abbildung 7.10: Füllregel »EvenOdd«
Etwas komplizierter ist die Anwendung der Füllregel Nonzero. Hier werden ebenfalls von jedem Punkt aus zwei Strahlen in zwei Richtungen gegen unendlich gezogen. Allerdings wird hier die Anzahl der dabei gekreuzten Linien im (+1) und gegen (–1) den Uhrzeigersinn gezählt. Ist die Summe ungleich null, wird die betreffende Fläche (bzw. der Punkt) gefüllt dargestellt, sonst nicht. In der Abbildung 7.11 schneidet der Punkt in der weißen Fläche die Linien 1 und 2, die entgegen dem Uhrzeigersinn gezeichnet werden, und die Linien 4 und 8, die im Uhrzeigersinn verlaufen. Die Summe ist damit null (wenn man die Gewichtungen +1 für »im Uhrzeigersinn« und –1 für »dem Uhrzeigersinn entgegengesetzt« annimmt), und der Punkt wird nicht gezeichnet. Die Strahlen des Punktes in der gefüllten Fläche schneiden nur eine Linie, und die Summe ist damit immer ungleich null. Der Punkt wird gezeichnet. Es sollte noch kurz erwähnt werden, dass nun nicht für jeden Punkt eine solche Berechnung durchgeführt wird, sondern mithilfe der analytischen Geometrie dies deutlich effizienter erfolgt.
207
Kapitel 7
Abbildung 7.11: Füllregel »Nonzero«
BEISPIEL AUF DER CD Es gibt noch zahlreiche weitere interessante Effekte, die Sie mit diesen Füllregeln erzeugen können. Sie entstehen durch die Verzweigungen und Schnitte, die neue Flächen entstehen lassen. In einem weiteren Beispiel werden drei Linienzüge über zwei Polyline- und ein Polygon-Element gezogen. Da diese geschlossen sind, können Sie auch mit einer Füllfarbe versehen werden. Ja nach Fülltyp (Attribut FillRule) wird eine bestimmte Regel zum Füllen angewendet.
7.3.2 Pfade Neben den Grafikelementen, die bisher betrachtet wurden, gibt es noch sogenannte Geometrieklassen, die grundsätzlich die gleichen Elemente wie die primitiven Grafikelemente darstellen können. Der Unterschied zu den anderen Shapes besteht hier nicht
208
2D-Grafik
nur im Darstellungsergebnis, sondern auch in der Handhabung. Shapes wie Linien oder Rechtecke können sich selbst zeichnen. Geometrieobjekte benötigen eine zusätzliche Klasse wie z. B. Path, die sie zeichnet. Mit anderen Worten definiert ein Geometrieobjekt nur das geometrische Aussehen eines Objekts, während ein Shape auch die Rand- und Füllfarbe, individuell für jedes Grafikelement, festlegt und sich selbst zeichnen kann. Geometrieobjekte haben gegenüber Shapes den Vorteil, dass sie ClippingRegionen definieren und zum Hit-Testing (war ein Mausklick in einem bestimmten Bereich) eingesetzt werden können. Ein Path-Objekt ist zwar ein einfaches Grafikelement, wird aber intern durch ein oder mehrere Geometrieklassen definiert. Da sich die Geometrieobjekte nicht selbst zeichnen können, verwenden Sie die im Path-Element eingestellte Füllfarbe und den Zeichenstift. Das heißt, alle Geometrieobjekte innerhalb eines Pfades verwenden dieselben Farben. Die korrespondierenden Klassen zu den Shapes Line, Rectangle und Ellipse sind LineGeometry, RectangleGeometry und EllipseGeometry. Äquivalente Klassen für Polylinien gibt es dagegen nicht. Die Geometrieobjekte werden der Eigenschaft Data des Path-Elements zugefügt, die vom Typ Geometry ist. Die Klasse Geometry entspricht hier der Klasse Shape bei den Grafikprimitiven, d.h., die Geometrieklassen sind alle von der Klasse Geometry abgeleitet. Dabei gibt es allerdings verschiedene Anwendungsgebiete. Während LineGeometry, RectangleGeometry und EllipseGeometry zur Definition eines Grafikelements dienen, verwenden Sie die anderen Klassen, um Grafiken zusammenzufassen.
Abbildung 7.13: Vererbungshierarchie der Geometrieklassen
209
Kapitel 7
Zu beachten ist, dass die Namen der Eigenschaften von denen eines einfachen Grafikelements (Shape) abweichen. Im Falle einer Linie sind es statt der Koordinatenangaben wie X1 oder Y1 nun die Eigenschaften StartPoint und EndPoint. Im Falle eines Rechtecks wird keine Breite und Höhe angegeben, sondern eine Rect-Struktur, in der die Koordinaten in der Form X, Y, Width, Height angegeben werden.
Geometriegruppen Sollen mehrere Elemente in einem Pfad verwaltet werden, müssen Sie diese in ein GeometryGroup-Element einschließen, ansonsten können Sie der Eigenschaft Data von Path nur ein Geometrieobjekt zuweisen. Das Besondere an der Gruppierung von Geometrieobjekten ist allerdings, dass auch hier die FillRule-Regel zum Einsatz kommt und bei zwei ineinander verschachtelten Elementen beispielsweise der innere Teil nicht gezeichnet wird, wie Sie gleich im nächsten Beispiel sehen können. Wenn Sie stattdessen mehrere separate Pfade verwenden würden, wäre die FillRule-Eigenschaft wirkungslos.
BEISPIEL AUF DER CD Das Beispiel zeigt zwei einzelne Pfade sowie eine Schaltfläche, die durch die Verwendung des Clippings (vgl. nächster Abschnitt) ein elliptisches Aussehen besitzt. Insbesondere der zweite Pfad ist interessant, da er zeigt, wie Sie Bereiche schaffen können, die keine vollständige Fläche einnehmen (das untere Rechteck). Dies ist wieder auf die Eigenschaft FillRule zurückzuführen, die im GeometryGroup-Element gesetzt werden kann und hier mit dem Standardwert EvenOdd verwendet wird.
Clipping Von der Klasse UIElement erben die grafischen Klassen insbesondere eine Eigenschaft Clip. Diese ist vom Typ Geometry, und ihr können wie der Eigenschaft Data eines Pfades Geometrieobjekte zugewiesen werden. Dieser Pfad wird dann als Clipping-Bereich verwendet, d.h., es ist dann nur der Teil der UI-Komponente sichtbar, die sich innerhalb des Pfades befindet. Zu beachten ist, dass dies nur die Darstellung der Komponente betrifft. Die Komponente selbst ist immer noch in ihrer vollen Größe vorhanden.
BEISPIEL AUF DER CD Statt nur Komponenten durch das Clipping in der Darstellung zu beeinflussen, geht das natürlich auch mit dem Hauptfenster einer Anwendung. Die Klasse Window ist ebenfalls von der Klasse UIElement abgeleitet und besitzt demnach auch eine Eigenschaft Clip. Hier müssen Sie allerdings etwas mehr ausholen, da bestimmte Fenstereigenschaften noch deaktiviert werden müssen. Zuerst schalten Sie den Rand des Fensters durch die Angabe WindowStyle="None" ab. Die Größenänderung wird über ResizeMode="NoResize" deaktiviert, ist aber ohne Rand sowieso nicht möglich. Damit der ausgeschnittene Inhalt (das Loch im Kringel) entsteht, muss über AllowsTransparency="True" die Unterstützung für transparente Bereiche für den Clientbereich des Fensters aktiviert werden. Die Angabe der Position des Fensters in der Bildschirmmitte ist nicht unbedingt notwendig. Innerhalb der Eigenschaft Clip wird nun eine Geometriegruppe und darin werden zwei unterschiedlich große Ellipsen erzeugt. Da wieder die FillRule zum Einsatz kommt, entsteht das Loch in der Mitte. Der Button dient lediglich zum Beenden der Anwendung über die Methode Close() im Ereignishandler. Außerdem soll er zeigen, dass eben auch der Button durch das Clipping abgeschnitten wird, sodass für eine solche Benutzeroberfläche noch etwas Arbeit auf Sie zukommen würde.
Pfadgeometrien Um mehrere komplexere Geometrieobjekte zu nutzen, die rein auf das Zeichnen von Linien und Kurven beschränkt sind, wird ein PathGeometry-Element verwendet. Darin werden wiederum ein oder mehrere PathFigures-Elemente eingebettet. Die verwendeten Grafikelemente werden durch Segmente spezifiziert, z. B. durch Linien- oder Bogensegmente. Basis aller Segmente ist die gleichnamige Klasse PathSegment aus dem Namespace System.Windows.Media. Die Klasse bringt nur zwei neue Eigenschaften mit. Über IsStroked kann festgelegt werden, ob ein Segment gezeichnet werden soll. Über die Eigenschaft IsSmoothJoin legen Sie fest, ob die Verbindung zum vorherigen PathSegment abgerundet (false) oder nicht abgerundet (true) erfolgen soll. Der Standardwert ist hier false, bei IsStroked dagegen true.
Figuren erstellen Die Segmente werden im Element PathFigure definiert. Dazu besitzt diese Klasse eine Aufzählung Segments vom Typ PathSegmentCollection. Das Ende eines Segments ist dabei gleichzeitig der Anfangspunkt des nächsten Segments. Im Element PathFigure
212
2D-Grafik
wird über das Attribut StartPoint der Startpunkt für die Zeichenaktionen angegeben. Zeichnen Sie danach beispielsweise eine Linie, wird die Linie von diesem Startpunkt zum Punkt, der durch das Attribut Point im LineSegment angegeben ist, gezogen. Über die Eigenschaft IsFilled können Sie steuern, ob umschlossene Flächen gefüllt werden sollen. Dabei ist eine Fläche auch dann umschlossen, wenn der Anfangs- und der Endpunkt nicht explizit miteinander verbunden sind. Setzen Sie den Wert von IsClosed auf True, wird außerdem das letzte mit dem ersten Segment verbunden. ... ... Listing 7.11: Rahmen einer Pfadgeometrie mit zwei Figuren
Von der Klasse PathSegment sind sieben Klassen abgeleitet, die zum Erstellen von Grafikelementen verwendet werden können. Für einige der primitiven Typen gibt es hier kein direktes Äquivalent wie z. B. ein Rechteck oder eine Ellipse. Beide können aber mit den hier gebotenen Mitteln nachgebildet werden.
Abbildung 7.16: Vererbungshierarchie der Pfadsegmente
ArcSegment Kreisbögen werden über ArcSegment-Objekte erzeugt und mittels fünf Eigenschaften konfiguriert. Ausgehend vom aktuellen Zeichenpunkt, d. h. entweder dem Punkt, der
213
Kapitel 7
über die Eigenschaft StartPoint im PathFigure-Element gesetzt wurde, oder dem Endpunkt des letzten Segments, wird zum Punkt, der mit der Eigenschaft Point definiert wird, der Kreisbogen gezogen. Die Größe des Kreisbogens wird über die Eigenschaft Size festgelegt, wobei die Werte in Size für den X- und Y-Radius stehen. In der folgenden Abbildung wird ein Kreisbogen beginnend vom Punkt 100,100 zum Punkt 200,100 gezogen. Durch die verwendeten unterschiedlichen Radien für die X- und Y-Achse entstehen die verschiedenen Kreisbögen.
Abbildung 7.17: Verschiedene Bogenradien (50,50), (50,25), (50,100)
Der folgende Code zeigt, wie Sie einen vollständigen Kreis erstellen.
Standardmäßig werden die Kreisbögen entgegen dem Uhrzeigersinn gezeichnet. Über die Eigenschaft SweepDirection können Sie dies durch Zuweisung der Werte Clockwise (im Uhrzeigersinn) und CounterClockwise (entgegen dem Uhrzeigersinn – Standardwert) steuern. Beide stammen aus der gleichnamigen Aufzählung SweepDirection. Dies hat dann einen Effekt, wenn der Durchmesser der Ellipse größer als der Abstand der beiden Endpunkte ist. In diesem Fall kann die Ellipse auf vier Arten gezeichnet werden, als kurzer oder langer Boden und oberhalb bzw. unterhalb der X-Achse (wenn man einmal in diesem Fall davon ausgeht, dass sich die beiden Endpunkte auf der X Achse befinden). Mit den bisherigen Kenntnissen können Sie aber nur die kurzen Kreisbögen zeichnen, dies ist die Standardeinstellung der nächsten Eigenschaft IsLargeArc. Setzen Sie deren Wert auf True, wird dagegen der größere Kreisbogen gezeichnet. Die Abbildung 7.18 zeigt alle vier Varianten.
Abbildung 7.18: Die vier möglichen Kreisbögen zwischen zwei Punkten
214
2D-Grafik Listing 7.12: Anwendung der Eigenschaften SweepDirection und IsLargeArc
Über die Eigenschaft RotationAngle stellen Sie zum Abschluss noch die Drehung des Kreisbogens bezogen auf die X-Achse ein.
Abbildung 7.19: Unterschiedliche Drehungen bezogen auf die X-Achse (0°, 45°, 90°)
215
Kapitel 7 Listing 7.13: Drehung eines Kreisbogens
BezierSegment Zur Darstellung komplexer Kurven werden häufig die nach Bézier benannten Bézierkurven verwendet. Die Kurve wird dabei durch die beiden Endpunkte und zwei zusätzliche Stützpunkte definiert. Der Startpunkt wird wieder vom Endpunkt des vorigen Segments oder vom Wert der Eigenschaft StartPoint des PathFigure-Elements übernommen. Der Endpunkt wird über die Eigenschaft Point3 festgelegt. Die beiden Stützpunkte definieren Sie über die Eigenschaften Point1 und Point2. Grundsätzlich können Sie es sich so vorstellen, dass eine Linie vom Startpunkt nach Punkt 3 gezogen wird, und die beiden Stützpunkte 1 und 2 ziehen diese Linien in ihre Richtungen. Allerdings geht die Linie (Kurve) nicht direkt durch die Stützpunkte. Abbildung 7.20 zeigt einige Varianten. Im ersten Fall liegen alle Punkte auf einer Geraden. In diesem Fall wird nur eine Linie gezogen. Im mittleren Bild ziehen die Stützpunkte die Linie nach oben und entgegengesetzt dem Start- und Endpunkt. Es entsteht so etwas wie eine Ellipse. Interessant ist die rechte Variante. Darin zieht der erste Stützpunkt die Linie so weit nach rechts und der zweite nach links, dass eine Schleife entsteht.
Abbildung 7.20: Bézierkurven mit unterschiedlichen Stützpunkten
Listing 7.14: Auswahl einiger Bézierkurven
216
2D-Grafik
Eine weitere Form von Bézierkurven wird durch das QuadraticBezierSegment beschrieben. Darin wird nur ein Stützpunkt angegeben, sodass das Zeichnen der Kurve effizienter wird. Zur Erstellung von »Mehrfachkurven« gibt es mit den Segmenttypen PolyBezierSegment und PolyQuadraticBezierSegment entsprechende Lösungen. Beide besitzen eine Eigenschaft Points, der jeweils drei bzw. zwei Punkte pro Kurve übergeben werden. Der Endpunkt ist dann jeweils wieder der Startpunkt der nächsten Kurve.
Abbildung 7.21: Links eine quadratische und links Mehrfachbézierkurven
Listing 7.15: Quadratische und Mehrfachbézierkurven
LineSegment und PolyLineSegment Zum Abschluss können Sie natürlich auch noch einfache Linien ziehen. Dazu wird ein LineSegment oder für Mehrfachlinien ein PolyLineSegment verwendet. Um eine Linie zu ziehen, geben Sie lediglich über die Eigenschaft Point den Endpunkt der Linie an. Mehrfachlinien besitzen eine Eigenschaft Points, wobei hier jeweils eine Linie zum nächsten Punkt der Liste gezogen wird. Liniensegmente können auch sehr einfach dazu genutzt werden, den aktuellen Startpunkt zu versetzen. Dazu wird die Eigenschaft IsStroked auf den Wert False gesetzt, sodass die Linie nicht gezogen wird.
Abbildung 7.22: Einfache und Mehrfachlinien
217
Kapitel 7 Listing 7.16: Einfache und Mehrfachlinien
Path Markup-Syntax Die Definition eines Pfades in XAML über die zahlreichen verschachtelten Elemente ist weder gut lesbar noch kompakt. Dies macht sich z. B. dann bemerkbar, wenn über Grafiktools derartige Pfade erzeugt und als XAML-Datei abgespeichert werden. Deshalb gibt es unter XAML noch eine andere Syntax, welche die Zeicheninformationen in kompakterer Form dem Attribut Data im Path-Element zuweist. Mittels der folgenden XAML-Anweisung wird z. B. eine rote Linie gezogen.