GUI-Programmierung 2 - Windows Presentation Foundation

Transcription

GUI-Programmierung 2 - Windows Presentation Foundation
GUI-Programmierung 2 - Windows Presentation
Foundation (WPF)
Alexander Aumann
[email protected]
Abstract: Seit .NET 3.0 ist WPF als Nachfolger von Windows Forms das Mittel der
Wahl zum Erstellen grafischer Oberflächen mit .NET. Diese Ausarbeitung stellt die
Unterschiede und ein paar der interessanteren Features von WPF vor.
WPF wurde mit Version 3.0 des .NET-Frameworks von Microsoft als die neue Programierschnittstelle zur Erstellung grafischer Benutzeroberflächen (engl.: graphical user interfaces
= GUIs) eingeführt. Es ist somit der Nachfolger der seit .NET 1.0 verwendeten Windows
Forms, und hat es mittlerweile - was Microsofts eigene Projekte angeht - auch weitgehend abgelöst: so ist das neue Visual Studio 2010 mit WPF entwickelt worden [Sch09].
Im Rahmen dieses Dokuments werden daher die Vorzüge des neuen Systems im Vergleich
zu Windows Forms aufgezeigt, sowie einige der interessanteren Neuerungen vorgestellt.
1
Windows Forms oder WPF? Ein Vergleich
Ein erster großer Unterschied zwischen WPF und Windows Forms (kurz: WinForms) ist
die Art und Weise wie das einzelne Standardsteuerelement (zum Beispiel ein Textfeld
oder Button) auf den Bildschirm gezeichnet wird. WinForms greift zur Grafikausgabe auf
GDI/GDI+ zurück und bietet somit praktisch nur eine Einbettung der schon seit Windows
95 grundsätzliche vorhandenen Zeichenfunktionen in Managed Code an. WPF dagegen
zeichnet alle Steuerelemente unter Zuhilfenahme von DirectX selbst. Das ermöglicht auf
der einen Seite eine sehr flexible Anpassung der Steuerelemente (siehe Abschnitt 4: Styles
und Control Templates) und dank DirectX außerdem hardwarebeschleunigtes Zeichnen.
Das zweite große Plus von WPF ist die Art und Weise, wie man als Designer den Aufbau
der grafischen Oberfläche definiert. Betrachten wir dazu kurz die Vorgehensweise unter
WinForms: Selbst wenn man zur Oberflächenerstellung ein grafisches Designprogramm,
wie zum Beispiel den im Visual Studio enthaltenen Designer verwendet, wird der Entwurf
im Hintergrund in C#-Programmcode umgesetzt und die Oberflächendefinition auf diese
Art und Weise festgehalten.
Als WPF-Designer dagegen definiert man Oberflächen in XAML (vergleiche Abschnitt
2) komplett getrennt von der für die Programmlogik verwendeten Programmiersprache.
Dadurch ist es beispielsweise einem unabhängigen Designteam möglich, eine ausgefallene
GUI zu entwerfen, ohne sich Gedanken über die verwendete Programmiersprache machen
zu müssen.
Mit WPF wurden auch viele der im Hintergrund wirksamen Mechanismen überarbeitet,
so wurde das Eventsystem durch Routed Events deutlich verbessert, Commands erlauben
es häufig vorkommende Befehle wie Drucken oder Copy & Paste zentral zu verwalten.
Viele im Verlauf der kommenden Seiten vorgestellte Features funktionieren nur Dank
sogenannter Dependency Properties. All diese Weiterentwicklungen und Neuerungen in
WPF machen es erst so mächtig, wie es letztlich ist. Sie seien hier jedoch nur kurz der
Vollständigkeit halber erwähnt, da eine ausführliche Behandlung den Rahmen dieser Arbeit sprengen würde.
Bevor im nächsten Kapitel XAML näher vorgestellt wird, sollen auch einige Nachteile
von WPF im Vergleich zu Windows Forms nicht unerwähnt bleiben. Zum Ersten erfordert
gerade die Tatsache, dass WPF so umfangreiche Möglichkeiten bietet, eine gewisse Einarbeitungszeit, alleine schon, um sich einen Überblick zu verschaffen. Da WPF im Vergleich
zu WinForms recht jung ist, fehlen auch (noch) ein paar der mächtigeren vorgefertigten
Steuerelemente, wie zum Beispiel die DataGridView.
2
XAML
XAML, beziehungsweise ausgeschrieben die Extensible Application Markup Language,
wurde von Microsoft speziell für WPF entwickelt und ist im wesentlichen ein XMLDialekt, der zur Oberflächenbeschreibung (und Ressourcenverwaltung) dient. Zu Oberflächenbeschreibung zählen bei WPF nicht nur das einfache Layout beziehungsweise die
Anordnung der Steuerelemente, sondern auch Styles, verschiedene Templates zum noch
feineren Anpassen der einzelnen Steuerelemente und sogar Animationen. Auf diese Features wird in den späteren Abschnitten noch ausführlicher eingegangen, in diesem Kapitel
werden zunächst einige Grundlagen vorgestellt.
2.1
XAML-Dateien in einer frischen WPF-Anwendung
Wenn man beispielsweise mit dem Visual Studio 2010 ein neues WPF-Projekt erstellt,
enthält es folgende Dateien, die zur Bearbeitung durch den Entwickler primär vorgesehen
sind:
• App.xaml
• App.xaml.cs
• MainWindow.xaml
• MainWindow.xaml.cs
Die App.* Dateien sind quasi leer (bis auf die Festlegung des Anwendungshauptfensters
in App.xaml) und dienen später beispielsweise der Definition globaler Ressourcen. Die
Datei MainWindow.xaml sieht zum Beispiel wie folgt aus:
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid.RowDefinitions>
<RowDefinition /> <RowDefinition />
</Grid.RowDefinitions>
<TextBox Name="text1" Grid.Row="0"/>
<Button Name="button1" Content="OK" Grid.Row="1"/>
</Grid>
</Window>
Die erste Zeile legt dabei fest, dass eine Klasse MainWindow im Namespace WpfApplication1 erstellt werden soll, die von Window erbt.
Die beiden folgenden Zeilen dienen zur Einbindung der für XAML nötigen XML-Namensräume, wobei in .../xaml/presentation zum Beispiel alle WPF-Klassen und somit alle Steuerelemente liegen. Dadurch, dass dieser Namensraum direkt dem des aktuellen Dokuments zugewiesen wird, können Elemente daraus ohne qualifizierenden Namen
direkt verwendet werden (zum Beispiel Window oder Grid). Im Hintergrund sucht sich der
XAML-Compiler automatisch die korrespondierende .NET-Klasse, im Fall von Window
zum Beispiel System.Windows.Window.
In ../xaml liegen speziellere Elemente, die dazu dienen genauer festzulegen, wie das
XAML-File interpretiert werden soll. Auf diese Elemente kann mit "x:Elementname"
zugreifen.
Was in den verbliebenen Zeilen passiert, wird im nächsten Abschnitt genauer erläutert,
zunächst wollen wir uns kurz der zugehörigen C#-Quelldatei MainWindow.xaml.cs widmen. Sie enthält den Teil der Klasse MainWindow, der zur Bearbeitung durch den Programmierer vorgesehen ist und ist bis auf einen Aufruf der InitializeComponent()Methode im Konstruktor anfangs leer. Diese Datei ist später der ideale Ort um beispielsweise Code zum Event-Handling et cetera unterzubringen.
Ein großer Unterschied zu WindowsForms ist dabei, dass der automatisch generierte zweite Teil der Klasse MainWindow, der in der Datei MainWindow.d.cs liegt, nur Membervariablen zuweist, um dem Programmierer die Möglichkeit zu geben auf einzelne Steuerelemente aus dem Programmcode zuzugreifen. Das Layout hingegen wird darin nicht
festgelegt, sondern zur Laufzeit aus der zugehörigen XAML-Datei konstruiert (genauer
gesagt aus einer zur Kompilierzeit erzeugten binären Repräsentation des XAML-Codes,
sogenanntem Binary-Application-Markup-Language-Code (BAML)).
2.2
Steuerlemente und ihre Eigenschaften
Als Steuerelement (engl.: control) bezeichnet man die einzelnen Bausteine einer GUI,
zum Beispiel Buttons, Labels oder Textfelder. Jedes Steuerelement hat gewisse Eigen-
schaften (engl.: properties), die in WPF in Form von Dependency Properties umgesetzt
werden (eine Erweiterung des Property-Systems von WinForms, mehr sei dazu an dieser
Stelle nicht gesagt). Durch das Setzen dieser Eigenschaften kann man das Verhalten beziehungsweise Aussehen einzelner Steuerelemente beeinflussen, zum Beispiel indem man
die Text-Eigenschaft einer TextBox festlegt.
In XAML werden Eigenschaften festgelegt, indem man im Start-Tag des Elements nach
dem Typ des Elements alle Eigenschaften, die man setzen möchte, aufzählt und ihnen
den entsprechenden Wert zuweist. So erhält die Titel -Eigenschaft des Hauptfensters im
obigen Beispiel den Wert "MainWindow" und zeigt diesen String somit in der Titelleiste an, außerdem werden noch Höhe und Breite des Fensters gesetzt (dazu sei kurz
erwähnt, dass WPF aus Flexibilitätsgründen nicht in Pixeln rechnet, sondern in sogenann1
inch ).
ten geräteunabhängigen Einheiten der Größe 96
Einem Fenster fügt man Steuerelemente in XAML einfach dadurch hinzu, dass man sie
an der gewünschten Stelle im XML-Baum auflistet, im Regelfall eingebettet in ein Layout
(mehr dazu im nächsten Kapitel). Im Beispiel enthält das Hauptfenster ein Grid-Layout,
welches wiederum einen Button und eine TextBox enthält.
Erwähneswert sind noch sogenannte Attached Properties: Hierbei handelt es sich um Eigenschaften, die eigentlich nicht zum Steuerelement selbst gehören, sondern zum Container, der es beeinhaltet. Alle Elemente, die in ein Grid eingebettet werden, erhalten so
zum Beispiel eine zusätzliche Row-Eigenschaft. Um diese Attached Properties zu ändern,
muss man vor die Eigenschaft noch den Namen des Elements stellen, zu der sie gehört:
Grid.Row="0" im Beispiel setzt diese Eigenschaft für das Textfeld auf 0.
Nachdem nun ein grober Überblick über XAML erfolgt ist, beschäftigt sich der nächste
Abschnitt näher mit den verschiedenen Möglichkeiten eine Oberfläche zu strukturieren.
3
Layout
Als Layout bezeichnet man allgemein die Anordnung von Steuerelementen auf der Benutzeroberfläche. Interessant wird die Angelegenheit vor allem, wenn beispielsweise der
Benutzer die Fenstergröße ändert oder eine Labelbeschriftung zu groß wird und die Oberfläche zur Laufzeit angepasst werden muss, um trotzdem alle Informationen in angemessener Weise zu repräsentieren. An dieser Stelle kommen die WPF-Layout-Panels ins Spiel.
Sie ermöglichen es Elemente im Fenster relativ zueinander anzuordnen und kümmern
sich zum Beispiel darum, dass wenn sich die Aufschrift eines Labels ändert alle nötigen
Größenverhältnisse der Oberfläche angepasst werden.
Daraus resultierend ist es in WPF nicht üblich einzelne Elemente, wie noch bei WinForms ursprünglich vorgesehen, nach absoluten Koordinaten anzuordnen. Stattdessen ist
der Standardansatz immer die Verwendung eines passenden Layoutpanels. Die folgenden
Abschnitte stellen die verfügbaren Panels jeweils kurz vor.
3.1
Das StackPanel
Das StackPanel ordnet Elemente entweder in einem horizontalen oder vertikalen Stapel
an. Mit der Orientation-Eigenschaft des Stackpanels lässt sich festlegen, ob die Elemente vertikal oder horizontal ausgerichtet werden. Zur Beeinflussung der Anordnung eines
Steuerelements im Beispiel des vertikalen Stapels dient unter anderem die sogenannte
HorizontalAlignment-Eigenschaft des jeweiligen Controls dazu, es links, rechts, zentriert
oder gestreckt auszurichten.
3.2
Das WrapPanel
Ein weiterer Layout-Container ist das sogenannte WrapPanel, dass es ermöglicht Elemente in einer einzigen Reihe anzuordnen, wobei Elemente, die nicht mehr Platz haben, automatisch in die nächste Reihe umgebrochen werden. Jede Zeile wird dabei groß genug
gemacht, um das höchste Steuerelement aufnehmen zu können, niedrigere Elemente werden entsprechend ihres Alignments darin ausgerichtet.
3.3
Das DockPanel
Das DockPanel ermöglicht es, Elemente an eine der vier Seiten des Containers anzuheften.
Ein Standardbeispiel in einer echten Anwendung wären ToolBox-Leisten, die sich häufig
am oberen oder linken Rand eines Fensters befinden. Mit Hilfe des Attached Properties
Dock des DockPanels, können die einzelnen Steuerelemente entsprechend angeordnet
werden. Hierbei spielt die Reihenfolge der Steuerelemente eine Rolle: Fügt man zuerst
ein Steuerelement ein, das oben andockt, nimmt es die gesamte Breite des Fensters ein.
Danach eingefügte Steuerelemente, die zum Beispiel links angedockt werden, bekommen
den restlichen Platz auf dieser Seite und so weiter.
3.4
Das Grid
Das Grid-Layout ist der mächtigste aller Layout-Container. Er teilt den zu verwaltenden
Platz in Zeilen und Spalten auf und jedem Kindelement können schließlich ein oder mehrere Zellen in diesem Gitter zugeordnet werden. Auf diese Art und Weise sind ausgefallene Layouts möglich, vor allem durch Schachteln von Grids und den einfacheren LayoutContainern können so nahezu beliebige Anordnungen erreicht werden.
Abbildung 1 zeigt das Layout einer einfachen Beispielanwendung ”Filmsammlung”.
Wie durch die roten Linien angedeutet, besteht das Layout aus einem äußeren 3x3-Grid.
Die gesamte mittlere Spalte wird von einem GridSplitter beansprucht, der es dem Benutzer
ermöglicht das Verhältnis von linker zu rechter Seite zu ändern. Links und rechts des
Abbildung 1: Das Grid-Layout
Splitters sind in der obersten Zeile Labels eingefügt, in der linken Spalte füllt eine ListBox
die verbliebenen zwei Zeilen. Rechts ist unter dem Label ein weiteres Grid eingefügt,
welches die nötigen Textfelder und Labels enthält, darunter befindet sich ein FlowPanel
zur Ausrichtung der beiden Buttons.
So sieht der Anfang einer Grid-Layout-Definition für gewöhnlich in XAML aus:
<Grid Name="gridMain" Margin="3">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="3*"/>
</Grid.ColumnDefinitions>
...Steuerelemente...
</Grid>
Im Grid.Row(/Column)Definitions-Abschnitt wird die benötigte Zeilen- und
Spaltenzahl festgelegt, die Breite der Spalten (beziehungsweise analog Höhe der Zeilen)
kann auf drei unterschiedliche Arten festgelegt werden: Auto legt fest, dass die Spalte
genau so breit ist, dass alle Elemente Platz haben. Das Zusammenspiel der Breitenangaben 1* und 3* sorgt dafür, dass die dritte Spalte, wenn mehr Platz entsteht (zum Beispiel
durch eine Vergrößerung des Fensters) dreimal so stark wächst wie die erste. Zuletzt gäbe
es die Möglichkeit eine feste Zahl an Einheiten für die Breite anzugeben.
Die einzelnen Kindelemente können mit Hilfe der Attached Properties Row, Column,
RowSpan und ColumnSpan angeordnet werden. Die ersten legen dabei fest welcher Zelle
das Element zugeordnet ist, die anderen beiden, über wie viele Zeilen und Spalten es sich
ausbreitet.
3.5
UniformGrid und Canvas
UniformGrid und Canvas sind spezialisierte Layout-Container, die im Vergleich zu den
bereits erwähnten seltener Verwendung finden. Das UniformGrid teilt ein Gebiet in ein
Raster aus lauter gleich großen Zeilen und Spalten auf, das Canvas dient zur Anordnung
von Elementen nach absoluten Koordinaten, was nur in Spezialfällen (zum Beispiel im
Zusammenhang mit komplexen Animationen) Sinn macht.
4
Styles und Control Templates
Nach den grundsätzlichen Möglichkeiten zur Anordnung von Steuerelementen kommen
wir nun zu den Möglichkeiten, die WPF anbietet um die einzelnen Elemente nahezu beliebig anzupassen: Styles und Control Templates. Damit sind umfangreiche Änderungen
möglich, ohne dazu die zugehörige Programmlogik neu schreiben zu müssen.
4.1
Styles
Styles ermöglichen es dem Designer in XAML einzelne Eigenschaften eines Steuerelements zu ändern und dem Element so ein neues Aussehen zu geben. Als Beispiel betrachten wir zunächst einen einfachen Style, der die Schrift eines Labels ändert:
<Style x:Key="LabelHeaderStyle">
<Style.Setters>
<Setter Property="Label.FontSize" Value="14"/>
<Setter Property="Label.FontWeight" Value="Bold"/>
<Setter Property="Label.FontFamily" Value="TimesNewRoman"/>
<Setter Property="Label.Foreground">
<Setter.Value>
<LinearGradientBrush >
<GradientStop Color="Black" Offset="0"/>
<GradientStop Color="Black" Offset="0.66"/>
<GradientStop Color="Black" Offset="1"/>
</LinearGradientBrush>
</Setter.Value>
</Setter>
</Style.Setters>
</Style>
In der ersten Zeile wird dem Style mit x:Key="LabelHeaderStyle" ein Name
zugewiesen. Ansonsten enthält die Styledefinition eine Reihe von sogenannten Settern,
die der Style.Setters-Sammlung hinzugefügt werden. Jeder der Setter enthält ein
Property-Attribut, das festlegt, welche Eigenschaft geändert werden soll und ein ValueAttribut, das den neuen Wert bestimmt. Dieses kann auch komplex sein, und wird dazu in
ein eigenes Setter.Value-Tag eingebaut. Im Beispiel ist das ein LinearGradientBrush
für die Foreground -Eigenschaft, der einen gleichmäßigen Farbverlauf zwischen den Farben an bestimmten Offsets ermöglicht, in diesem Fall allerdings nur von schwarz über
schwarz nach schwarz. Warum das so Sinn machen kann, dazu gleich mehr.
Zunächst aber zur Frage, wo denn nun der Nutzen von Styles liegt, man könnte die Eigenschaften wie Schriftgröße und -farbe schließlich auch direkt in der Oberflächendefinition
des Layouts ändern, was sogar weniger Schreibaufwand verursacht. Der große Vorteil von
Styles ist deren Wiederverwendbarkeit: den einmal definierten Style kann man beliebig
vielen Steuerelementen zuweisen, zum Beispiel allen, die eine Überschrift darstellen. Und
wenn man sich späer zu einer Umgestaltung entschließt, muss man die Änderungen nur an
einer zentralen Stelle - dem Style - vornehmen.
Styles werden als Ressourcen angelegt. Auf das Ressourcenmanagement wird an dieser
Stelle nicht näher eingegangen, näheres dazu findet sich zum Beispiel unter [WEB10a].
Wie weist man nun aber einem Steuerelement einen speziellen Style zu? Dazu gibt es zwei
grundsätzliche Ansätze: die erste Möglichkeit ist, die Style-Eigenschaft des Steuerelements zu setzen:
<Label Content="Filmdaten" (...)
Style="{StaticResource LabelHeaderStyle}"/>
Auf diese Art und Weise wird der LabelHeaderStyle als Ressource geladen und dem Label
zugewiesen. Die zweite Möglichkeit ist, Styles automatisch allen Elementen eines Typs
zuzuweisen. Dazu muss die TargetType-Eigenschaft gesetzt werden, und der Style darf
keinen Namen erhalten:
<Style TargetType="Button">
(...)
</Style >
Styles können allerdings mehr als nur einzelne Eigenschaften statisch verändern, sie können
nämlich durch sogenannte Style-Trigger auf verschiedene Events reagieren und auf diese
Art und Weise zum Beispiel einen MouseOver-Effekt erzeugen:
<Style>
...Setter...
<Style.Triggers>
<EventTrigger RoutedEvent="Mouse.MouseEnter">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<ColorAnimation Storyboard.TargetProperty=
"Foreground.GradientStops[1].Color"
To="BurlyWood" Duration="0:0:0.5"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
<EventTrigger RoutedEvent="Mouse.MouseLeave">
...rückgängig machen...
</EventTrigger>
</Style.Triggers>
</Style>.
Dieser Style enthält einen EventTrigger der ausgelöst wird, sobald der Mauszeiger über
dem Steuerelement ist. Als ausgeführte Aktion wird eine Animation festgelegt (in XAML
sind Animationen immer in ein Storyboard eingebettet), die die Farbe des mittleren Offset
(vom ursprünglichen Schwarz) in 0.5 Sekunden nach BurlyWood ändert. Dadurch lässt
sich eine Art Aufglüheffekt erzeugen. Um diesen (dauerhaften) Effekt wieder rückgängig
zu machen, ist es sinnvoll auch den MouseLeave-Event zu behandeln.
Styles bieten bereits vielfältige Möglichkeiten zur Anpassung einzelner Controls. Will
man ein Steuerelement jedoch komplett umgestalten und nicht nur einzelne Eigenschaften ändern, greift man auf Control Templates zurück, die im nächsten Kapitel vorgestellt
werden.
4.2
Control Templates
In WPF sind alle Steuerelemente intern aus kleineren Teilelementen zusammengesetzt,
so besteht ein Button aus einem Rahmen, in den ein ContentPresenter eingebettet ist, der
die Aufschrift darstellt. Dieser interne Zusammenbau wird im Gegensatz zum logischen
Aufbau einer Oberfläche (ein Fenster enthält ein Layout, welches Steuerelemente enthält)
als Visual Tree bezeichnet (näheres dazu zum Beispiel hier: [Sam10]).
Möchte man ein Steuerelement also grundlegend umbauen muss es eine Möglichkeit geben diesen Visual Tree zu verändern. Control Templates bieten diese und wir werden sie
im Folgenden benutzen um den Standardbutton anzupassen. Konkret soll der Button deutlich abgerundete Ecken erhalten sowie eine andere Rahmen- und Hintergrundfarbe, die
sich dynamisch anpasst: So soll zum Beispiel ein Button über dem der Mauszeiger ist
dunkler werden und auch auf Klicks optisch reagieren.
Das nötige Control Template sieht wie folgt aus:
<Style TargetType="Button">
...beliebige (Style)Setter möglich...
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border TextBlock.Foreground=
"{TemplateBinding Foreground}"
x:Name="Border" CornerRadius="8"
BorderThickness="1">
<Border.BorderBrush> ... </Border.BorderBrush>
<Border.Background> ... </Border.Background>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualStateGroup.Transitions>
<VisualTransition GeneratedDuration=
"0:0:0.5"/>
<VisualTransition GeneratedDuration="0"
To="Pressed" />
</VisualStateGroup.Transitions>
<VisualState x:Name="Normal" />
<VisualState x:Name="MouseOver">
...MouseOver-Aussehen festlegen...
</VisualState>
... "Disabled" und "Pressed"...
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<ContentPresenter
Margin="{TemplateBinding Padding}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
RecognizesAccessKey="True" />
</Border>
<ControlTemplate.Triggers>
...auch Trigger möglich...
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Das erste, was hierbei vielleicht auffällt ist, dass das Control Template in einen Style eingebettet ist. Das ist gängige Praxis und ermöglicht vor allem eine automatische TemplateZuweisung an alle Buttons (TargetType-Eigenschaft des Styles). Das neue Control Template wird der Template-Eigenschaft des Steuerelements zugewiesen. Im tatsächlichen
Control Template wird nun der Aufbau des Buttons festgelegt, in diesem Fall genügt uns
ein Rahmen mit abgerundeter Ecke und den entsprechenden Farbeigenschaften. Der Rah-
men enthält wiederum einen ContentPresenter und außerdem einen VisualStateManager,
dazu gleich mehr.
Sogenannte Template Bindings erlauben es eine interne Eigenschaft, wie zum Beispiel die
TextBlock.Foreground -Eigenschaft an die externe Eigenschaft Foreground des Buttons zu
binden: das bedeutet, wenn der Designer in XAML dem Button einen anderen Vordergrund
zuweist, wird diese Zuweisung an den TextBlock weitergeleitet.
Der VisualStateManager schließlich bietet eine einfache Möglichkeit für einzelne Zustände
wie MouseOver oder Clicked entsprechende Eigenschaften zu setzen, über Transitions
können die Übergänge zwischen diesen Zuständen feiner geregelt werden. Im Beispiel
sollen die Übergänge zwischen den einzelnen Zuständen jeweils eine halbe Sekunde dauern, außer bei einem Klick (To:Pressed ), der sofort angezeigt wird.
Abbildung 2 zeigt die angepasste Benutzeroberfläche mit Überschriftlabels und neuen
Buttons, sowie einem angepassten Hintergrund mit Farbverlauf und leicht durchsichtigen
Textfeldern.
Abbildung 2: Filmsammlung mit benutzerdefiniertem Style
Zum Abschluss bleibt noch zu sagen, dass das Buttonbeispiel noch ein recht einfaches
Control Template ist, da ein Button prinzipiell nur eine logische Funktion hat. Möchte
man dagegen zum Beispiel eine ScrollBar umgestalten muss man deutlich mehr beachten,
da sie aus mehreren Komponenten, wie einem Balken, einem Slider und den Pfeiltasten zusammengesetzt ist und somit von der Anwendungslogik gewisse Vorgaben an ein
eigenes Control Template gestellt werden. Ein sehr guter Einstiegspunkt für eigene Templates ist das Microsoft SimpleStyles-Beispielprojekt, das für jedes Standardcontrol ein
übersichtliches, angepasstes Control Template enthält. Das Beispiel steht hier zum Download bereit: [WEB10b].
5
Data Binding und Data Templates
Nachdem wir nun schon gesehen haben was WPF im Gegensatz zu Windows Forms in
Sachen Oberflächenanpassung alles kann, soll nun auch noch ein Blick auf Datenbindung
in WPF geworfen werden. Datenbindung ist ein recht weitläufiger Begriff, im aktuellen
Zusammenhang bedeutet es so viel wie irgendeine Art von Daten an ein Steuerelement
auf der Benutzeroberfläche zu binden.
5.1
Data Binding
Die einfachste Art von Data Binding in WPF wäre es beispielsweise eine bestimmte (Dependency) Property eines Controls an eine andere Eigenschaft eines zweiten Controls zu
binden. So ist es zum Beispiel möglich ein Slider-Control mit der Schriftgröße eines Labels so zu verknüpfen, dass sich die Schriftgröße des Labels automatisch ändert, sobald
der Benutzer den Slider bewegt.
Die interessantere Art der Datenbindung ist es, im Hintergrund von der Programmlogik gespeicherte Daten mit einem oder mehreren Steuerelementen zu verknüpfen. Hierzu wollen
wir uns die Filmsammlung noch einmal ansehen und konkret die Textfelder auf der rechten
Seite mit den richtigen Werten füllen, je nachdem, welcher Film in der ListBox ausgewählt
wurde. Das funktioniert in WPF überraschend einfach, wie im Folgenden dargestellt wird.
Die Anwendung soll im Hintergrund Film-Objekte bereitstellen, die alle für einen einzelnen Film nötigen Daten kapseln. In diesem einfachen Beispiel seien das fullName, shortName, producers, director, writers, year und number. Um eine Datenbindung zu einem
Film-Objekt nutzen zu können, müssen all diese Eigenschaften als public property
zur Verfügung gestellt werden, der nötige C#-Code sieht wie folgt aus:
namespace VideoLibrary
{
class Film
{
private string fullName;
public string FullName
{
get { return fullName; } set { fullName = value;}
}
//... restliche Properties (shortName, director...)
}
}
Diese Daten sind zum Beispiel in einer Datenbank abgelegt, die Zugriffsfunkionalität darauf sei in einer Klasse DataStore abgelegt. Um sie nun mit der Oberfläche zu verknüpfen
muss in der MainWindow-Klasse noch ein wenig Code geschrieben werden:
public partial class MainWindow : Window
{
DataStore ds;
List<Film> films;
public MainWindow()
{
InitializeComponent();
ds = new DataStore();
films = ds.GetFilms();
listBoxFilms.ItemsSource = films;
}
}
Das DataStore-Objekt liefert eine Liste von Filmen zurück, die der ListBox als Datenquelle zugewiesen wird.
Als nächstes wird in XAML eine DataBinding-Verknüpfung festgelegt, um die Textfelder
mit der jeweiligen Filmeigenschaft zu verknüpfen:
<TextBox Name="textBoxTitleComplete"
(...)
Text="{Binding Path=FullName}"
/>
Anstatt also der Texteigenschaft einen statischen String zuzuweisen wird festgelegt, dass
sie mit dem FullName-Property der aktuellen Datenquelle verbunden werden soll. Bleibt
zu guter Letzt noch das Setzen der Datenquelle. Das ginge entweder über die Angabe
einer Quelle im Binding-Ausdruck, oder aber über einen DataContext. Der große Vorteil
des DataContext ist, dass Kindelemente (zum Beispiel in einem Grid), die selber keinen
DataContext festlegen, automatisch im Kontext des Containers stehen. Kurz: durch setzen
eines DataContext für das Grid, haben alle Textfelder die richtige Datenquelle:
<Grid Name="gridMain"
DataContext=
"{Binding ElementName=listBoxFilms,
Path=SelectedItem}"
>
(...)
</Grid >
Das war auch schon alles: wenn nun der Benutzer in der Liste einen Film auswählt, werden
die Textfelder mit den richtigen Werten gefüllt. Und es geht noch mehr: alle Änderungen,
die an den Texten vorgenommen werden, werden automatisch in die Filmliste im Hintergrund übernommen. Das einzige Problem ist noch, dass die Liste selbst nicht weiß, wie
sie ein Film-Objekt darstellen soll, sie ruft deshalb einfach auf jedes einzelne Objekt die
.ToString()-Methode auf, und zeigt jeweils nur VideoLibrary.Film an. Eine erste
Abhilfe wäre es diese Methode für die Film-Klasse zu überschreiben. WPF bietet jedoch
mit sogennanten Data Templates ein deutlich mächtigeres Werkzeug für diese Aufgabe.
5.2
Data Templates
Mit Data Templates lässt sich für das einzelne Item einer Liste (oder auch eines TreeViews)
ganz detailliert der Aufbau festlegen. Im Rahmen eines einfachen Beispiels soll dazu in
der Liste von jedem Film der Kurzname und leicht eingerückt der Regisseur dargestellt
werden, eingebettet in einen hübschen Rahmen. Das Template in XAML sieht wie folgt
aus:
<DataTemplate x:Key="FilmDataTemplate">
<Border CornerRadius="4" BorderThickness="1" Margin="3">
<Grid Margin="2">
(...Grid.Row Definitionen...)
<TextBlock Grid.Row="0" FontWeight="Bold"
Text="{Binding Path=ShortName}"/>
<TextBlock Grid.Row="1" Text="{Binding Path=Directors}"
Margin="4, 0, 0, 0" HorizontalAlignment="Left"/>
</Grid>
</Border>
</DataTemplate>
Es werden die zwei nötigen Textfelder mit Hilfe eines Grids in den Rahmen eingebettet.
Die Text-Eigenschaft wird jeweils an die entsprechende Filmeigenschaft gebunden, nur so
kann jedes Listenelement die richtigen Daten anzeigen. Abschließend muss der ListBox
nun noch dieses Data Template zugewiesen werden:
<ListBox Name="listBoxFilms"
ItemTemplate="{StaticResource FilmDataTemplate}"
HorizontalContentAlignment="Stretch"
(...)
/>
Als Ergebnis erhalten wir nun also eine vollständige, editierbare Filmauflistung, wie in
Abbildung 3 dargestellt.
6
Zusammenfassung
Die im Rahmen dieser Arbeit vorgestellten Features sollen einen ersten Überblick darüber
geben, welche Möglichkeiten WPF im Vergleich zu WinForms bietet und als Anregungung
Abbildung 3: Filmsammlung mit überarbeiteter Liste
zu eigenen Experimenten dienen. Eine halbwegs vollständige und tiefgehende Abhandlung von WPF ist an dieser Stelle nicht möglich und würde Bücher füllen. Hier verweise
ich deshalb auf die angegebene Literatur (hauptsächlich [Mac97]) und Recherche im Internet und hoffe, dass ich zumindest ein wenig das Interesse an der Windows Presentation
Foundation wecken konnte.
7
Literaturverzeichnis
Literatur
[Mac97]
Matthew MacDonald. Pro WPF in C#2008. Apress, 1997.
[Sam10]
Jitendra Sampathirao. Logical Tree and Visual Tree in WPF.
http://www.c-sharpcorner.com/UploadFile/jitendra1987/4046/
Default.aspx, Stand: 15.12. 2010.
[Sch09]
Holger Schwichtenberg. Microsofts Visual Studio 2010 Beta 2 - eine Querschau.
http://www.heise.de/developer/artikel/
Microsofts-Visual-Studio-2010-Beta-2-eine-Querschau-868144.
html, 12 2009.
[WEB10a] WEBSITE. Resources Overview.
http://msdn.microsoft.com/en-us/library/ms750613.aspx, Stand:
15.12. 2010.
[WEB10b] WEBSITE. WPF Documentation Samples. http://code.msdn.microsoft.
com/wpfsamples\#controlcustomization, Stand: 15.12. 2010.