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.
Abbildung 2.1: WPF-Projekttypen im Visual Studio
| Anwendungstyp | Beschreibung |
| Windows-Anwendung Windows Application (WPF) | Hierbei handelt es sich um eine typische Windows-basierte Anwendung, die allerdings auf den Komponenten und Möglichkeiten der WPF basiert. |
| WebBrowser-Anwendung XAML Browser Application (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). |
| Steuerelementbibliothek Custom Control Library (WPF) | In dieser Bibliothek werden Steuerelemente speziell für WPF-Anwendungen hinterlegt. |
Die verschiedenen Anwendungstypen wurden ja bereits vorgestellt. Allerdings gibt es unterschiedliche Varianten, eine solche Anwendung zu erstellen bzw. um überhaupt etwas zu erstellen, was irgendwie genutzt werden kann. Es sind folgende Vorgehensweisen möglich:
Wie schon seit der ersten Version des .NET Frameworks üblich wird dazu ein Application-Objekt benötigt, welches aus dem Namespace System.Windows stammt. Statt einem 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 WPF-Komponenten etwas komplizierter wird, zumindest anfangs.
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 Projektmappenexplorer. 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 das 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).
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
Die letzte Anwendung wird nun nur noch mit Hilfe von XAML erzeugt. Die "Hauptanwendung" besteht aus dem XML-Element Application, welches 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.
<Application x:Class="MinimalXAML.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="Window1.xaml">
</Application>
Listing 2.2: Beispiele\Kap02\ MinimalXamlProj\App.xaml
<Window x:Class="MinimalXamlProj.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Hallo von der WPF" Height="100" Width="230"
Background="AliceBlue">
<StackPanel>
<Button Click="OnClick">Klick mich</Button>
<x:Code>
void OnClick(object sender, RoutedEventArgs e)
{
MessageBox.Show("Funzt");
}
</x:Code>
</StackPanel>
</Window>
Listing 2.3: Beispiele\Kap02\ MinimalXamlProj\Window1.xaml
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.
Abbildung 2.4: Beziehungen zwischen den XAML- und Code-Behind-Dateien
ACHTUNG: Zum Verständnis der folgenden Erläuterungen sind schon Grundkenntnisse zu 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, welche 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 <ProjektName>.csproj.
<Project DefaultTargets="Build"
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<AssemblyName>MinimalXAML</AssemblyName>
<OutputType>winexe</OutputType>
<OutputPath>.</OutputPath>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="WindowsBase" />
<Reference Include="PresentationCore" />
<Reference Include="PresentationFramework" />
</ItemGroup>
<ItemGroup>
<ApplicationDefinition Include="App.xaml" />
<Page Include="Window1.xaml" />
</ItemGroup>
<ItemGroup>
<Compile Include="App.xaml.cs" />
<Compile Include="Window1.xaml.cs">
<DependentUpon>Window1.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
</ItemGroup>
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets"/>
<Import Project="$(MSBuildBinPath)\Microsoft.WinFX.targets"/>
</Project>
Listing 2.4: Verkürzte Projektdatei einer WPF-Anwendung
Die Datei App.xaml ist also der Einsprungpunkt der Anwendung. Das Application-Element 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 wird der Namespace und der Klassename 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 weis, 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-Behind-Dateien miteinander verknüpft.
Die Code-Behind-Klasse muss dabei von der Klasse abgeleitet sein, welche 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.
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, welche 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, welche den generierten Code für das Fenster enthält (das Zeichen g im Dateinamen
steht für generated). 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 Interfaces 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
{
internal TextBox Tb1;
internal Button Btn1;
private bool _contentLoaded;
public void InitializeComponent()
{
if(_contentLoaded)
{
return;
}
_contentLoaded = true;
Uri resourceLocater = new Uri("/WindowsApplication1;component/window1.xaml", UriKind.Relative);
System.Windows.Application.LoadComponent(this, resourceLocater);
}
void IComponentConnector.Connect(int connectionId, object target)
{
switch(connectionId)
{
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
Current der Klasse Application.
Application current = Application.Current;
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();
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)
{
foreach(string arg in e.Args)
cmdArgs.Add(arg);
}
Listing 2.6: Beispiele\Kap02\ ApplicationPropsProj\App.xaml.cs
Application-Objekt einer Anwendung, welches ü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