Willkommen im dritten Teil dieser Einführungsserie.
Für die weiteren Artikel brauchen wir eine Basis auf die wir aufbauen können.
Deshalb werde ich eine kleinen Minianwendung vorstellen die noch so wenig wie möglich Features der WPF benutzt.
Außerdem werden das Content Model von WPF und Events angesprochen.
Im Laufe der Artikelreihe werden wir dann immer mehr neue Features einbauen, so dass es am Ende eine richtige WPF Anwendung wird.
Kurze Beschreibung
Die Beispielanwendung ist ein kleiner Bildbetrachter der die Bildnamen aus einem Verzeichnis das angegeben wird ausliest und sie in einer Liste anzeigt. Nun kann man ein Bild in der Liste auswählen und es wird angezeigt. Klingt unspektakulär, ist es im Moment auch noch

Aber es bietet viel Potential.
Download
Der Quellcode
XML-Code: |
<Window x:Class="XPic.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="XPic" MaxHeight="600" MaxWidth="800" Margin="0" Padding="0"
>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="25"/>
<RowDefinition Height="200"/>
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="40"/>
<ColumnDefinition Width="650"/>
<ColumnDefinition Width="100"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0" Grid.Row="0" HorizontalAlignment="Right" Margin="2">Pfad:</Label>
<TextBox x:Name="DirectoryTextBox" Grid.Column="1" Grid.Row="0" Margin="2" HorizontalContentAlignment="Left" HorizontalAlignment="Stretch" Text="Bitte Pfad eingeben" />
<Button x:Name="PfadButton" Content="wählen" Width="Auto" Grid.Column="2" Grid.Row="0" Margin="2" Click="PfadButton_Click"/>
<Grid Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3" Background="DarkSeaGreen">
<ListView x:Name="ImageListView" Background="Gainsboro" Margin="10" SelectionChanged="ImageListView_SelectionChanged"/>
</Grid>
<Grid Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="3" Background="CornflowerBlue">
<Image x:Name="ImagePlace" />
</Grid>
</Grid>
</Window>
|
Vom Aufbau her sollte das Programm keine Schwierigkeiten bereiten, es werden nur einfache Elemente benutzt und verschiedene Attribute gesetzt. Wir haben wieder ein Fenster in dem sich erstmal ein Grid als Layout Element befindet. Wir legen über die Row- und GridDefinitions fest wieviele Spalten und Zeilen unser Grid haben soll. Dann platzieren wir einige Elemente in diesem Grid. Das werde ich auch mal visuell darstellen um das Grid näher zu erläutern weils schon ein häufig genutztes Layout Element ist.
Es ist im Prinzip eine 3*3 Tabelle und in den einzelnen Feldern platzieren wir nun unsere Elemente
Hier sind nun die einzelnen Felder so wie wir sie verwenden farbig hervorgehoben.
Oben links in Blau ist unser Label. Die Platzierung erfolgt einfach durch Angabe der entsprechenden Indizes
XML-Code: |
Grid.Column="0" Grid.Row="0"
|
Das gleiche machen wir nun mit einer TextBox(Rot) und einem Button(Gelb) - jeweils durch Angabe der passenden Grid.Column. Was dieses Grid.Column zu bedeuten hat werde ich später erläutern. Unter diesen drei Elementen wollen wir in der 2. Zeile unser ListBox mit den Dateinamen anzeigen. Durch Angabe der ersten Spalte als Column in der unsere Liste platziert werden soll und ColumnSpan="3" erreichen wir dass sich unsere Liste nun über alle 3 Columns und somit über die volle Breite des Programms erstreckt. Das gleiche geben wir auch bei unserem Image an. Auffällig an der ganzen Sache ist, dass wir innerhalb unseres großen Grids, für die Liste un das Bild jeweils erst ein eigenes Grid als Layoutelement benutzen und dort dann unsere Steuerelemente platzieren. Ein weiteres Property was oft benutzt wird, ist das Margin Property. Damit wird der Abstand der Steuerelemente zum Rand des Containers bestimmt. Ich habe hier nur eine Zahl als Abstand für jede Seite angegeben. Dieser kann aber auch für jede Seite einzeln in der Form Margin="1,2,3,4" angegeben werden. Webdesigner dürften sich recht wohl fühlen mit XAML oder?
Ein Bild wie es bei unserem Beispielprogramm jetzt ungefähr aussieht sehen sie jetzt. Die weißen Felder skizzieren unsere Controls und angedeutet ist der Rand zum sie enthaltenen Container.
Somit sind wir bei obrigen Quellcode schon wieder durch. Schuldig bin ich aber noch die Erklärung z.B. dieser Grid.Column Properties. Diese Properties, oder besser diese Art der Angabe von Properties, nennen sind Attached Properties und sind ein Feature von XAML.
Attached Properties erlauben es Werte von Properties in Elementen zu setzen die gar nicht zum eigentlichen Element, sondern zu einem Elternelement, gehören.
Wir setzen in unserem Fall immer die Spalte und Reihe in der unser Control im Grid erscheinen soll, dies ist aber wie gesagt keine Eigenschaft unseres Controls(es kann ja nicht wissen das es in einem Grid ist und nicht in einem anderen Layoutcontainer), sondern des Grids. Klarer wird dies wenn wir und das kleine Beispiel aus der XAML Einführung nochmal anschaun - wie solch ein Code in C# übersetzt wird.
XAML:
XML-Code: |
<Slider x:Name="SizeSlider" Grid.Row="1" Maximum="280" Margin="3" Value="100" />
|
C#:
C#-Code: |
Slider mySlider = new Slider();
mySlider.Name = SizeSlider;
mySlider.Maximum = 280;
mySlider.Margin = 3;
mySlider.Value = 100;
Grid.SetRow(mySlider,1);
|
Das Setzen der Grid.Row wird in einen extra Funktionsaufruf umgewandelt, und hat nichts mehr mit irgend nem Property unseres Sliders zu tun. Trotzdem entspricht es viel eher dieser deklarativen Syntax direkt bei unserem Steuerelement die entsprechende Reihe und Spalte einzustellen, statt bei dem übergeordneten Grid - deshalb diese attached Properties. Solche Properties kann man natürlich auch selber definieren, aber das soll nicht Teil dieser Einführung sein.
Wie sind Controls aufgebaut in der WPF? / Content Model
Bevor wir jetzt weiter fortfahren möchte ich kurz erläutern wie Controls in der WPF eigentlich aufgebaut sind - und auch erklären wieso ein Button keine Texteigenschaft hat
In Windows Forms ist man bei den Controls recht beschränkt. Man ist doch recht festgelegt was Inhalt und Aussehen der Controls betrifft. Möchte man dort was ändern muss man i.d.R. ableiten und selbst implementieren. Bei der WPF ist es zum Glück um einiges leichter, aber auch vieles mächtiger - Problem ist nur dass man erstmal das Prinzip dahinter erkennen muss. Es gibt nur wenige Arten von Controls in der WPF - Layoutcontrols, Headered/ContentControl und Headered/ItemsControls. Das reicht völlig aus

LayoutControls sind Controls wie das GridControl oder FlowLayoutControl die einfach beschreiben wie unsere anderen Controls innerhalb dieser LayoutControls angeordnet werden. Contentcontrols dienen dazu einzelne Daten anzuzeigen und ItemsControls dazu Listen anzuzeigen - die entsprechenden Headerpendants zeigen noch eine Überschrift an.
In WindowsForms war ein Label darauf beschränkt Text anzuzeigen - in der WPF ist es nun ein ContentControl und beschränkt sich darauf Content anzuzeigen.
Dies kann einfacher Text sein, aber damit wäre das Label schier unterfordert

Content bedeutet in der WPF nämlich nichts anderes als irgend ein beliebiges anderes Control. Somit ist es uns in der WPF endlich möglich jegliche Controls in anderne Controls einzubetten und das auch noch beliebig komplex. Ein einfaches Beispiel wäre z.b. ein MenuItem das nicht nur Text enthält, sondern bspw. ein Stackpanel(ein weiteres nützliches LayoutControl) das wiederum ein Image, Label, TextBox und Button enthält. Der Button kann seinerseits wieder Bild und Text enthalten usw. Der Kreativität der Entwickler sind nun kaum noch Grenzen gesetzt.
Leicht vorstellbar dürfte dann auch sein das die ganze GUI als Baum beschrieben wird. In unserem Fall wäre es z.b. ein Window mit einem Menü als Child. Das Menu hat wiederum ein MenuItem als Child und das Menuitem hat wie oben beschrieben auch mehrere Childs.
Nun lassen sich nicht nur selber Controls so zusammensetzten, sondern auch die fertigen Controls wie z.b. die ListBox, oder TabControl sind so aus mehreren Controls zusammengesetzt. Jedes Control hat so einen eigenen Elementbaum wie eben beschrieben. In der MSDN Doku ist zu fast jede Control ein umfangreiches Beispiel gebracht für so ein ControlTemplate. Genau, Template hört sich nach auswechselbar an oder? Denn das sind sie auch. Diese ControlTemplates lassen sich fast beliebig anpassen und auswechseln, auch für die Standardcontrols.
Und da sieht man dann auch hoffentlich ein, dass nen Label keine Texteigenschaft mehr hat sondern nur noch ein Content Property - immerhin muss das Label gar kein Steuerelement mehr enthalten was Text anzeigen kann. Wofür sollte dann eine Text Eigenschaft dienen?
Das ganze ist natürlich ziemlich komplex und bringt ein paar neue Konzepte was z.b. Events angeht. Es ermöglicht aber eine extrem flexible Anpassung auch vorhanderener Controls. Klarer dürft das ganze in einem zuküntigen Artikel werden wo Templates und Styles bsprochen werden.
Events in der WPF
Die oben vorgestellte Anwendung wie sie momentan ist, könnte man fast komplett in XAML schreiben, aber ein wichtiges Mittel fehlt uns dazu noch - das Databinding und das besprechen wir im nächsten Artikel. Jetzt müssen wir uns noch mit ein wenig C# Code rumschlagen, aber der ist sehr simpel. Es ist die Implementation unser 2 Eventhandler die wir oben in XAML festgelegt haben.
Es handelt sich konkret dabei um
XML-Code: |
<Button x:Name="PfadButton" ... Click="PfadButton_Click"/>
|
und
XML-Code: |
<ListView x:Name="ImageListView" ... SelectionChanged="ImageListView_SelectionChanged"/>
|
Die Syntax dürfte Recht einleuchtend sein, einfach der Eventname, gefolgt vom Namen des Eventhandlers. Wir müssen nun einfach den Eventhandler implementieren, das abbonieren der Events hingegen wird vom automatisch generierten Code übernommen, den wir im Idealfall(nämlich dann wenn alles funktioniert) nie zu Gesicht bekommen. Aber bevor wir zum Code kommen noch ein paar allgemeine Sache zu Events.
Windows Forms kennt einen Routing Mechanismus für Events, nämlich praktisch keinen

Es gibt nur direkte Events. Ein Button Click wird von einem Button ausgelöst und man kann beim Button einen Eventhandler anfügen der auf dieses Event reagieren soll.
In WPF gibts nun dieser 3: Direkt, Bubbling und Tunneling. Um es vorneweg zu nehmen, unsere beiden obigen Events sind Bubbling. Aus den Namen lässt sich auch schon ungefähr die Funktionsweise erahnen.
Direkte Events sind wie gehabt - Click wird vom Button ausgelöst und der Button könnte drauf reagieren. Tunneling Events werden nun vom Root Element angestoßen und wandern den Elementbaum runter bin zum Element das das Event ausgelöst hat! Klingt erstmal seltsam, ist aber so. Als Beispiel sei beim Button in WPF mal das PreviewKeyDown Event genannt. Alle Tunneling Events haben als Prefix "Preview" um sie von Bubbling Events zu unterscheiden. Diese wandern vom auslösenden Element den Elementbaum hoch zum Root.
Für die meisten Events gibt es deshalb solche PreviewABCXXX und ABCXXX Eventpaare - einmal als Tunneling Event und einmal als Bubbling Event.
die Tunneling Events treten dabei immer vor dem Bubbling Event auf.
Als Beispiel nehmen wir mal an wir haben ein Window mit einem Grid und darin befindet sich unser Button. Nun wir eine Taste gedrückt. Dann wirft unser Window das PreviewKeyDown Event, dann wird das Grid das entsprechende Event, dann erst der eigentliche Button wo es aufgetreten ist. Dann wirft der Button nach dem PreviewKeyDown das KeyDown Event das hochwandert zum Grid und von diesem geworfen wird und dann wird das Window das entsprechende Event.
Events können jederzeit auf dieser Route als "handled" makiert werden und werden dann nicht weiterverarbeitet. Auch zu beachten ist dass wenn ein Preview Ereigniss gehandled wird, das entsprechende Bubbling event nicht mehr geworfen wird!
Das war jetzt glaube ich arg viel der Theorie und die möchte ich jetzt auch nicht weiter vertiefen. Es wird ein Artikel folgen der Commands behandelt und dort werde ich auf dieses Thema nochmal eingehen - für alle anderen sei wie immer die MSDN Library Doku dazu angeraten.
Der C# Code
C#-Code: |
public void PfadButton_Click(object sender, RoutedEventArgs e) {
ImageListView.Items.Clear();
FileInfo[] files = null;
if (Directory.Exists(DirectoryTextBox.Text)) {
DirectoryInfo di = new DirectoryInfo(DirectoryTextBox.Text);
files = di.GetFiles("*.jpg");
}
if (files != null) {
for (int i = 0; i < files.Length; i++) {
ImageListView.Items.Add(files[i].FullName);
}
}
}
|
Der Code ist recht simpel, sucht einfach aus dem in der TextBox angegebenen Verzeichnis alle Jpg's raus und fügt den Dateipfad in die ListBox ein. Auffällig bei der Signatur des Eventhandlers dürften die Eventargs sein. Die sind vom Typ RoutedEventArgs - dies ist der Basistyp für alle Events in der WPF. Es werden Eigenschaften wie z.b. Source und OriginalSource mitgegeben die es so noch nicht in WindowsForms gab. Um bei unserem Beispiel mit dem PreviewKeyDown Event zu bleiben, wäre Quelle jetzt unser Window(das Rootelement) und OriginalSource unser Button.
C#-Code: |
public void ImageListView_SelectionChanged(object sender, SelectionChangedEventArgs e) {
if (e.AddedItems.Count > 0) {
BitmapImage bmp = new BitmapImage(new Uri(e.AddedItems[0].ToString()));
ImagePlace.Source = bmp;
}
}
|
Statt eines RoutedEventArgs wird bei SelectionChanged direkt eine abgeleitete Klasse angegeben, immerhin brauchen wir ja auch die Informationen was sich nun geänert hat. Wir erstellen aus dem Dateinamen ein neues Bild und weisen es unserem ImageControl zu. An sich nichts daramtisches, trotzdem dürfte die Zeile mit dem Bitmap Konstruktor Fragen aufwerfen. Ich arbeite mit lokalen Pfaden und verwende eine URI? Ja - in WPF macht man das so

Und nicht nur beim Bitmap, eigentlich überall wo Pfade erwartet werden können URIs angegeben werden(was im Endeffekt ja auch nichts anderes als Pfade sind). Es gibt z.b. auch noch die Möglichkeit zwischen verschiedenen NavigationWindows in der WPF zu navigieren(ein Thema welches ich nicht ansprechen werde) und dort wird der Name der Page zu der navigiert werden soll, auch als URI angegeben.
Damit wäre auch dieser Artikel geschafft. Ich habe eine kleine Anwendung gezeigt die uns als Basis für die weiteren Artikel dienen soll und die wir um einige WPF Features erweitern wollen. Außerdem wurden in diesem Artikel erklärt was es mit dem Content Model so auf sich hat und wie Events in der WPF funktionieren.
Ich hoffe er hat euch gefallen und über Rückmeldung freue ich mich natürlich gerne.