Silverlight Snippet: Freihand zeichnen

In Silverlight ist es mit der InkPresenter-Klasse möglich, mit der Maus auf einer virtuellen Leinwand freihand zu zeichnen. Wir müssen dazu lediglich einige Maus-Events des InkPresenters abfangen und entsprechend verarbeiten. Dazu definieren wir ersteinmal einen InkPresenter im XAML:

<Grid x:Name="LayoutRoot">

        <InkPresenter x:Name="inkPresenter"
                      Background="Beige"
                      MouseLeftButtonDown="OnMouseDown"
                      MouseLeftButtonUp="OnMouseUp"
                      MouseMove="OnMouseMove" />

</Grid>

Im Code-Behind definieren wir folgende private Variablen:

private DrawingAttributes attributes;
private Stroke currentStroke;

Die DrawingAttributes-Klasse repräsentiert die Eigenschaften unseres virtuellen Pinsels wie Farbe oder Strichstärke.
Ein Stroke stellt die aktuell gezeichnete Figur (bzw. den gezeichneten Strich) dar, die aus einer Sammlung von Punkten besteht.

Im Konstruktor der MainPage legen wir die Eigenschaften unseres Pinsels fest:

public MainPage()
        {
            InitializeComponent();

            attributes = new DrawingAttributes();
            attributes.Color = Colors.Blue;
            attributes.Height = 10;
            attributes.Width = 2;
        }

Neben der Farbe legen wir auch die Größe des Pinsels fest. Dabei wählen wir in diesem Beispiel eine unterschiedliche Höhe und Breite, um einen kalligrafischen Effekt ähnlich eines Füllfederhalters zu erzielen.

Wenn der Benutzer die Maustaste drückt, erzeugen wir einen neuen (bisher aus keinen Punkten bestehenden) Strich, geben ihm die gewünschten Pinseleigenschaften mit und fügen ihn der Liste von Strichen des InkPresenters hinzu:

 private void OnMouseDown(object sender, MouseButtonEventArgs e)
        {
            currentStroke = new Stroke();
            currentStroke.DrawingAttributes = attributes;;
            inkPresenter.Strokes.Add(currentStroke);
        }

Lässt der Benutzer die Maustaste wieder los, setzen wir den aktuellen Strich auf null, um im nächsten Event zu merken, dass gerade nicht gezeichnet werden soll:

private void OnMouseUp(object sender, MouseButtonEventArgs e)
        {
            currentStroke = null;
        }

Im MouseMove-Event schließlich fügen wir alle Punkte, die seit der letzten Mausbewegung gezeichnet wurden, unserem Strich hinzu. Dies tun wir aber nur, wenn die Maus noch immer gedrückt wird (currentStroke also nicht null ist). Die seit dem letzten Event hinzugekommenen Punkte erhalten wir aus einem der Argumente (MouseEventArgs) des Event-Handlers:

private void OnMouseMove(object sender, MouseEventArgs e)
        {
            if (currentStroke == null)
                return;

            foreach (StylusPoint point in e.StylusDevice.GetStylusPoints(inkPresenter))
                    currentStroke.StylusPoints.Add(point);
        }

Wenn ihr das Programm jetzt ausführt, solltet ihr wie gewünscht mit der Maus auf der Silverlight-Page zeichnen können.

Silverlight Snippet: Dateien speichern und laden

Seit Silverlight in der dritten Version erschienen ist, gibt es neben der Möglichkeit, vom Benutzer ausgewählte Dateien in die Silverlight-Anwendung zu laden auch eine Möglichkeit, Dateien lokal auf dem Rechner des Benutzers zu speichern. Die beiden dafür zuständige Klassen sind OpenFileDialog und SaveFileDialog, deren Verwendung ich hier kurz demonstrieren möchte.

Im XAML definieren wir folgendermaßen eine TextBox und einen Laden- sowie einen Speichern-Button, um geschriebene Textdateien in die TextBox zu laden oder als Datei abzuspeichern:

 <Grid x:Name="LayoutRoot">
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>

        <TextBox Grid.Row="0" Margin="16"
                 x:Name="textBox"
                 AcceptsReturn="True" />

        <StackPanel Grid.Row="1" Margin="16"
                    Orientation="Horizontal">
            <Button Content="Laden" Click="OnLoad" />
            <Button Content="Speichern" Click="OnSave"
                    Margin="16,0,0,0"/>
        </StackPanel>

    </Grid>

Im Code-Behind sehen die Event-Handler der Buttons wie folgt aus:

   private void OnLoad(object sender, RoutedEventArgs e)
        {
            OpenFileDialog dialog = new OpenFileDialog();
            dialog.Filter = "Textdatei (*.txt)|*.txt";
            bool? result = dialog.ShowDialog();

            if (result.HasValue && result.Value)
            {
                StreamReader reader = dialog.File.OpenText();
                textBox.Text = reader.ReadToEnd();
                reader.Close();
            }
        }

        private void OnSave(object sender, RoutedEventArgs e)
        {
            SaveFileDialog dialog = new SaveFileDialog();
            dialog.Filter = "Textdatei (*.txt)|*.txt";
            bool? result = dialog.ShowDialog();

            if (result.HasValue && result.Value)
            {
                StreamWriter writer = new StreamWriter(dialog.OpenFile());
                writer.Write(textBox.Text);
                writer.Close();
            }
        }

Um das Beispiel minimal zu halten, habe ich nur die Filter-Eigenschaft der beiden Dialoge belegt, die angibt, welche Art von Dateien gelesen und gespeichert werden können. Es ist hier auch möglich, beim Filter mehrere Dateiendungen anzugeben: „Textdateien (*.txt)|*.txt|JPEG-Bilder (*.jpg)|*.jpg“. Dabei ist jeweils die erste Angabe vor dem vertikale Strich die Beschreibung des Dateiformates, die man im Klartext im Dialog sieht, und die zweite Angabe die letztendlich erwartete Dateiendung.

Zudem hat der OpenFileDialog eine Multiselect-Eigenschaft, mit der man mehr als eine Datei auswählbar macht. Nach dem Öffnen wäre dann anstatt der File-Eigenschaft Files belegt, was die Liste der geöffneten Dateien enthält, die man beispielsweise in einer Schleife durchlaufen könnte.

Der Aufruf von ShowDialog() liefert einen besonderen Datentyp zurück: Einen Nullable<bool>. Dieser Datentyp kann durch bool? abgekürzt werden. Nullable ist eine Wrapper-Struktur, die es auch Wertetypen wie bool oder int erlaubt, wie Referenztypen den Wert null anzunehmen. Dessen HasValue-Eigenschaft fragt ab, ob ein (nicht null) Wert gesetzt wurde und Value enthält den eigentlichen bool-Wert. War der Rückgabewert von ShowDialog() ein bool mit dem Wert true, hat der User Dateien ausgewählt und wir können uns bei beiden Dialogen einen Stream auf die Datei(en) zurückgeben lassen. Wir bekommen je nach Wahl des Dialoges einen nur les- oder schreibbaren Stream.

Aus Sicherheitsgründen hat man in Silverlight nur auf den Namen der geöffneten Datei zugriff, nicht hingegen auf weitere Informationen wie den vollen Dateipfad auf dem lokalen Rechner.

Für die Verwendung von StreamReader und -Writer muss mit using der Namespace System.IO eingebunden werden. Ich werde hier nicht weiter auf die Verwendung von Streams eingehen. Interessierte können hier über ihre Verwendung nachlesen.

Silverlight Snippet: Bild-Reflexion a la CoverFlow

Mit dem folgenden Trick können wir in Silverlight die Reflexion eines Bildes auf einer Glasoberfläche simulieren, wie es auch iTunes im CoverFlow-Modus tut. Kopiert dazu ersteinmal ein beliebiges Bild in euer Projekt und passt die beiden Bild-Pfade im folgenden Code entsprechend an:

<Grid x:Name="LayoutRoot">
        <Grid.Background>
            <LinearGradientBrush StartPoint="0.5,0" EndPoint="0.5,1">
                <GradientStop Offset="0" Color="Black" />
                <GradientStop Offset="0.4" Color="Black" />
                <GradientStop Offset="0.5" Color="#181818" />
                <GradientStop Offset="0.6" Color="Black" />
                <GradientStop Offset="1" Color="Black" />
            </LinearGradientBrush>
        </Grid.Background>

        <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
            <!-- Original-Bild -->
            <Image Source="image.jpg" Width="320" Height="240"
                   Stretch="UniformToFill" Margin="0.5" />

            <!-- Reflexion -->
            <Image Source="image.jpg" Width="320" Height="240"
                   Stretch="UniformToFill" Margin="0.5"
                   RenderTransformOrigin="0.5,0.5">
                <Image.RenderTransform>
                    <ScaleTransform ScaleY="-1" />
                </Image.RenderTransform>
                <Image.OpacityMask>
                    <LinearGradientBrush StartPoint="0.5,1" EndPoint="0.5,0">
                        <GradientStop Offset="0" Color="#60FFFFFF" />
                        <GradientStop Offset="0.9" Color="#00FFFFFF" />
                    </LinearGradientBrush>
                </Image.OpacityMask>
            </Image>
        </StackPanel>
    </Grid>

Ganz oben haben wir dem Grid einen leichten Gradienten verpasst, damit die Reflexion mehr zur Geltung kommt.

Der eigentliche Trick ist, zwei Image-Objekte mit demselben Bild untereinander in einem StackPanel zu verwenden. Wichtig ist dabei nur, dass die beiden Images exakt gleich groß sind. Während das obere Image ganz normal dargestellt wird, flippen wir das untere Image vertikal und lassen es langsam ausfaden.

Das Flippen bzw. Spiegeln erledigen wir mit einem ScaleTransform. Diese Transformation kann nicht nur Objekte skalieren, sondern bei der Verwendung von negativen Werten für ScaleX oder ScaleY auch horizontal oder vertikal spiegeln. Dabei darf man nicht vergessen, für das Zielobjekt RenderTransformOrigin auf den relativen Mittelpunkt des Objektes (0.5,0.5) zu setzen, da es sonst nicht wie gewünscht um dessen Mittelpunkt gespiegelt werden würde.

Für das Ausfaden verwenden wir die OpacityMask-Eigenschaft des unteren Images. Diese erwartet eine beliebige Brush, deren Alpha-Farbwerte die Opazität des Objektes an verschiedenen Stellen bestimmen. Wir geben hier eine LinearGradientBrush an, die zwischen den Farben #60FFFFFF und #00FFFFFF, also von oben angefangen vom Alpha Wert #60 bis nach unten zu #00, interpoliert und damit die Opazität des Images langsam bis zur vollständigen Transparenz verändert. Die OpacityMask ignoriert dabei die Rot-, Grün- und Blauwerte der Farben und verwendet ausschließlich den ersten Alpha-Wert.

Wenn ihr das Programm nun startet, sollte es etwa so aussehen:

snippet_reflection

Silverlight Snippet: Vollbildmodus aktivieren

Um eine Silverlight-Webseite in den Vollbildmodus zu bringen, reicht eine Zeile C#-Code aus. Zu beachten ist aber, dass der Moduswechsel nur funktioniert, wenn die Ausführung des Codes vom Benutzer initiiert, also z.B. durch Klick auf einen Button aufgerufen wurde.

Im folgenden Beispiel steht der Code deswegen im Event-Handler eines Button Click-Events.
Wir erzeugen dazu zuerst einen Button in XAML und lassen uns einen Event-Handler ins Code-Behind generieren.

 <Grid x:Name="LayoutRoot">
        <Button Click="Button_Click"
                Content="In den Vollbildmodus wechseln!" />
 </Grid>

Im Event-Handler schreiben wir dann folgenden Code, um in den Vollbildmodus zu wechseln:

private void Button_Click(object sender, RoutedEventArgs e)
{
       Application.Current.Host.Content.IsFullScreen = true;
}

Wenn ihr nun auf den Button klickt, sollte die Silverlight-Webseite im Vollbildmodus angezeigt werden.

Silverlight Tutorial: Interaktion via Code-Behind

Willkommen zum dritten Silverlight Tutorial!

Diesmal werden wir lernen, wie man XAML- und C#-Code kombiniert, um etwas Interaktivität in unsere Webseite zu bringen.

Das Code-Behind

Im letzten Tutorial haben wir gelernt, wie man eine einfache Benutzeroberfläche in XAML erstellt. Leider war die noch sehr statisch und konnte nicht auf Benutzereingaben reagieren.

Während XAML sehr gut dafür geeignet ist, eine Oberfläche zu definieren, ist C#-Code das Mittel der Wahl, wenn es um die gesamte Applikations-Logik und das Reagieren auf Benutzereingaben geht.

Erstellt wie im letzten Tutorial beschrieben ein neues Silverlight Projekt.

Klickt jetzt im Solution-Explorer auf die Datei MainPage.xaml.cs. Ihr seht nun folgenden C#-Code:

public partial class MainPage : UserControl
{
        public MainPage()
        {
            InitializeComponent();
        }
}

Im Solution-Explorer ist erkennbar, dass die .cs Datei und die .xaml Datei durch einen Strich verbunden sind. Während in vielen Sprachen eine Klasse nur in genau einer Datei definiert werden kann, ist es in C# mit dem partial Schlüsselwort möglich, die Definition einer Klasse auf mehrere Dateien aufzuteilen.

In Silverlight wird dies verwendet, um eine Klassendefinition in XAML-Code (für die Benutzeroberfläche) und dem sogenannten Code-Behind (beispielsweise für das Behandeln von Benutzereingaben) aufzuteilen.

Wir werden diesmal zwei Möglichkeiten kennen lernen, wie XAML- und C#-Code miteinander kommunizieren können.

XAML-Objekte im Code-Behind ansprechen

Erzeugt wie folgt eine einfache Border in der MainPage.xaml:

<Grid x:Name="LayoutRoot">
        <Border x:Name="myBorder"
                Width="256" Height="256"
                Background="Black" />
</Grid>

Wenn ihr das Programm nun mit F5 ausführt, seht ihr unsere schwarze Border im Zentrum der Seite. Wir wollen nun als Beispiel die Hintergrundfarbe (also die Background-Eigenschaft) der Border aus dem Code-Behind ändern.

Geht dazu zurück zur MainPage.xaml.cs und erweitert den Konstruktor der Klasse um folgende Zeile:

public partial class MainPage : UserControl
    {
        public MainPage()
        {
            InitializeComponent();

            myBorder.Background = new SolidColorBrush(Colors.Orange);
        }
    }

Wenn ihr das Programm jetzt ausführt, ist die Border orange anstatt schwarz.

Im XAML-Code haben wir dem Border-Objekt mit x:Name einen Namen zugewiesen, über den wir das Objekt im Code-Behind der XAML-Datei ansprechen können. Zu beachten ist, dass x:Name keine Eigenschaft der Border-Klasse ist, sondern eher eine Meta-Information, die man nur in XAML festlegen kann. Zwar haben viele Klassen eine richtige Name-Eigenschaft, jedoch eben nicht alle, weswegen man der Konsistenz wegen immer x:Name verwenden sollte.

Wenn wir das Programm ausführen, wird zuerst der Konstruktor der Klasse MainPage aufgerufen. Dort steht standardmäßig nur der Aufruf der Methode InitializeComponent(), die nichts weiter macht, als unseren XAML-Code in C# Code umzuwandeln, der zur Laufzeit die in XAML definierten Objekte erzeugt (wir haben ja gelernt, dass XAML und C# größtenteils äquivalent und damit austauschbar ist).

Ihr könnt diesen generierten C#-Code sehen, indem ihr rechts auf die Methode klickt und dann “Gehe zu Definition” auswählt.

In der nächsten Zeile sprechen wir die in XAML definierte Border mit ihrem Namen an und weisen ihr eine andere Hintergrundfarbe zu.

Hier könnte man sich Fragen, warum wir ein SolidColorBrush-Objekt zuweisen, und nicht einfach soetwas wie Colors.Orange. Das hat den Grund, dass die Background Eigenschaft ein Objekt vom Typ Brush erwartet, Orange aber den Typ Color hat.

In Silverlight gibt es verschiedene von Brush abgeleitete Klassen, die von einfachen Farben (SolidColorBrush) über verschiedene Gradienten (z.B. LinearGradientBrush) bis hin zu Bildern (ImageBrush) oder sogar Videos (VideoBrush) reichen.

Das ermöglichst es, all das als “Hintergrundfarbe” von Elementen oder beispielsweise als Schriftfarbe von Texten zu verwenden, was ein viel flexibleres System ist, als nur einfache Farben (Color-Structs) zuweisen zu können.

Auf die Verwendung der verschiedenen Brushes werde ich in späteren Tutorials eingehen.

Im Code-Behind auf Events reagieren

Was uns jetzt noch fehlt, ist eine Möglichkeit, auf Benutzereingaben zu reagieren.

GUI-Elemente wie Buttons und auch unsere Border definieren in ihren Klassen Events, die unter bestimmten Bedingungen vom Objekt ausgelöst werden. So löst beispielsweise ein Button-Objekt das Click-Event aus, wenn der User auf den Button klickt.

Um im Code auf das Event zu reagieren, muss man einen Event-Handler bei dem Event registrieren. Das kann man sich ähnlich wie die Anmeldung für einen Newsletter vorstellen (was natürlich eine arg konstruierte Metapher ist 😉 ). Wenn der Anbieter einen neuen Newsletter herausgibt (ein Event auslöst), werden alle angemeldeten Benutzer (Event-Handler) und nur diese Benutzer informiert.

Bei uns wird ein Event-Handler eine einfache Methode im Code-Behind sein, die aufgerufen wird, sobald ein Objekt ein Event auslöst. Es gibt noch weitere Methoden wie anonyme Delegates, um auf Events zu reagieren, aber die sollen uns hier noch nicht beschäftigen.

Geht wieder zum XAML-Code und erweitert das Border-Tag wie folgt:

<Border x:Name="border"
                Width="256" Height="256"
                Background="Black"
                MouseLeftButtonDown="border_MouseLeftButtonDown"/>

Wenn ihr anfangt, im Border-Tag zu tippen, öffnet sich eine Liste mit allen Eigenschaften und Events des Objekts. Events sind mit einem Blitz gekennzeichnet. Sobald ihr das Event MouseLeftButtonDown per Doppelklick oder Druck auf die Return-Taste ausgewählt habt, sollte ein neues Kontext-Menü erscheinen, dass euch anbietet, einen neuen Event-Handler zu erstellen. Hier klickt ihr wieder doppelt auf “<Neuer Ereignishandler>”, worauf das Visual Studio automatisch eine Methode im Code-Behind generiert und ihren Namen im XAML einträgt.

Somit wurde für das Event MouseLeftButtonDown die Methode border_MouseLeftButtonDown als Event-Handler registriert und das Code-Behind sieht nun so aus:

 public partial class MainPage : UserControl
    {
        public MainPage()
        {
            InitializeComponent();

            border.Background = new SolidColorBrush(Colors.Orange);
        }

        private void border_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {

        }
    }

In diese Methode können wir nun Code schreiben, der ausgeführt werden soll, wenn jemand auf die Border klickt. Typischerweise haben solche Event-Handler Methoden zwei Parameter: Im ersten Parameter sender übergibt sich das Objekt, das das Event auslöst, selbst. Der zweite Parameter ist meist von einem für das Event spezifischen Typ, der weitere Informationen über das Event bereitstellt. Beim MouseDown-Event werden beispielsweise auch die Koordinaten übergeben, an denen der Klick stattfand.

Eine weitere Möglichkeit, einen Event-Handler zu registrieren, ist dies direkt im Code-Behind zu tun. Auch hier hilft uns das VisualStudio und generiert die entsprechende Methode automatisch. Schreibt folgendes in den Konstruktor der MainPage:

border.MouseLeftButtonUp += new MouseButtonEventHandler(border_MouseLeftButtonUp);

Nachdem ihr += getippt habt, könnt ihr zwei mal die Tab-Taste betätigen, um automatisch den Zuweisungscode und die Methode border_MouseLeftButtonUp generieren zu lassen.

Der komplette Code sieht nun so aus:

public partial class MainPage : UserControl
    {
        public MainPage()
        {
            InitializeComponent();

            border.Background = new SolidColorBrush(Colors.Orange);
            border.MouseLeftButtonUp += new MouseButtonEventHandler(border_MouseLeftButtonUp);

        }

        void border_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            throw new NotImplementedException();
        }

        private void border_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {

        }
    }

Wir haben jetzt jeweils eine Methode die aufgerufen wird, wenn der Benutzer auf die Border klickt (border_MouseLeftButtonDown), und die Maustaste wieder löslässt (border_MouseLeftButtonUp).

Wir wollen jetzt, um die Events zu visualisieren, die Hintergrundfarbe der Border je nach Event verändern, und erweitern den Code folgendermaßen:

public partial class MainPage : UserControl
    {
        public MainPage()
        {
            InitializeComponent();

            border.Background = new SolidColorBrush(Colors.Orange);
            border.MouseLeftButtonUp += new MouseButtonEventHandler(border_MouseLeftButtonUp);
        }

        void border_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            border.Background = new SolidColorBrush(Colors.Green);
        }

        private void border_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            border.Background = new SolidColorBrush(Colors.Red);
        }
    }

Wenn ihr das Programm jetzt ausführt, sollte die Border beim Gedrückthalten der linken Maustaste rot, und beim Loslassen grün werden.

Ihr könnt jetzt gerne mit anderen Events experimentieren, und auch mal andere Eigenschaften als die Hintergrundfarbe verändern, um ein Gefühl für das Event-System zu kriegen.

Im folgenden Beispielprojekt werden noch einmal verschiedene Events und deren Verarbeitung demonstriert. Das war’s dann soweit zu diesem Thema. Im nächsten Tutorial werden wir uns einen weiteren wichtigen Aspekt von Silverlight ansehen: DataBinding.

Beispielprojekt herunterladen

« geh zurückweiter umsehen »