Neue Benutzeroberflächentechnologien

Lissajous-Animationen in Silverlight

Charles Petzold

Beispielcode herunterladen .

In der Regel stellen wir uns Software als flexibler und vielseitiger als Hardware vor. Dies trifft sicherlich in vielen Fällen zu, da Hardware häufig auf eine einzige Konfiguration festgelegt ist, während Software für die Ausführung vollständig verschiedener Aufgaben neu programmiert werden kann.

Einige ziemlich prosaische Hardwaregeräte sind jedoch tatsächlich ziemlich vielseitig. Denken Sie an die verbreiteten – oder in diesen Tagen nicht mehr so verbreiteten – Elektronenstrahlröhren. Dies ist eine Vorrichtung, bei der ein Elektronenstrahl auf die Innenseite eines Glasbildschirms geschossen wird. Der Bildschirm ist mit einem fluoreszierenden Material beschichtet, das auf diese Elektronen durch kurzes Aufleuchten reagiert.

In den traditionelle Fernsehgeräten und Computermonitoren bewegt sich das Elektronengewehr in einem festgelegten Muster. Es fährt wiederholt horizontal über den Bildschirm, während es sich langsamer von oben nach unten bewegt. Die Intensität der Elektronen bestimmt die Helligkeit des Punktes an dieser Stelle. Für Farbbildschirme werden getrennte Elektronengewehre verwendet, um die Primärfarben rot, grün und blau zu erzeugen.

Die Richtung des Elektronengewehrs wird durch Elektromagnete gesteuert, und es kann auf jede Stelle auf der zweidimensionalen Glasoberfläche gerichtet werden. Auf diese Weise werden Elektronenstrahlröhren in einem Oszilloskop verwendet. In der Regel wird der Strahl bei konstanter Geschwindigkeit horizontal über den Bildschirm geführt, in der Regel mit einer bestimmten Wellenform synchronisiert. Die vertikale Ablenkung zeigt die Amplitude dieser Wellenform an dieser Stelle. Die vergleichsweise hohe Dauerhaftigkeit des in Oszilloskopen verwendeten fluoreszierenden Materials ermöglicht die Anzeige der gesamten Wellenform, sodass die Wellenform für die visuelle Prüfung „eingefroren“ wird.

Oszilloskope verfügen außerdem über einen X-Y-Modus, mit dem die horizontale und vertikale Ablenkung des Elektronengewehrs durch zwei unabhängige Eingaben gesteuert werden kann; in der Regel sind dies Wellenformen wie z. B. Sinuskurven. Mit zwei Sinuskurven als Eingaben, wenn der Punkt (x, y) aufleuchtet, wobei x und y durch die parametrischen Gleichungen angegeben werden:

parametric equations

Die A-Werte sind Amplituden, die ω-Werte sind Frequenzen, und die k-Werte sind Phaseneffekte.

Das Muster aus der Interaktion dieser beiden Sinuswellen ist eine Lissajous-Kurve, benannt nach dem französischen Mathematiker Jules Antoine Lissajous (1822-1880), der als erste diese Kurven visuell durch das Abstrahlen von Licht zwischen zwei Spiegeln erzeugte, die mit vibrierenden Stimmgabeln verbunden waren.

Sie können auf meiner Website mit einem Silverlight-Programm experimentieren, dass Lissajous-Kurven generiert (charlespetzold.com/silverlight/LissajousCurves/LissajousCurves.html). Abbildung 1 zeigt eine typische Anzeige.

image: The Web Version of the LissajousCurves Program

Abbildung 1 Die Webversion des LissajousCurves-Programms

Obwohl dies in einem statischen Screenshot nicht sehr deutlich zu sehen ist, bewegt sich ein grüner Punkt auf dem dunkelgrauen Bildschirm und hinterlässt eine Spur, die innerhalb von vier Sekunden verblasst. Die horizontale Position des Punktes wird von einer einzelnen Sinuskurve festgelegt, und die vertikale Position von einer anderen. Wiederholungsmuster treten dann auf, wenn es sich bei den beiden Frequenzen um einfache integrale Verhältnisse handelt.

Es ist mittlerweile eine allgemein anerkannte Erkenntnis, dass ein Silverlight-Programm nach Windows 7 portiert werden muss, um die Qualität zu gewährleisten, da so Leistungsprobleme bekannt werden, die ansonsten von leistungsstarken Desktopcomputern verdeckt werden. Dies war mit diesem Programm der Fall, und ich bespreche die Leistungsprobleme an späterer Stelle in diesem Artikel. Abbildung 2 zeigt das Programm auf dem Windows Phone 7-Emulator.

image: The LissajousCurves Program for Windows Phone 7

Abbildung 2 Das LissajousCurves-Programms für Windows Phone 7

Der herunterladbare Code enthält eine einzelne Visual Studio-Lösung mit dem Namen „LissajousCurves“. Die Webanwendung besteht aus den Projekten LissajousCurves und LissajousCurves.Web. Die Windows Phone 7-Anwendung hat den Projektnamen LissajousCurves.Phone. Die Lösung enthält auch zwei Bibliotheksprojekte: Petzold.Oscilloscope.Silverlight und Petzold.Oscilloscope.Phone. Die beiden Projekte verwenden jedoch die gleichen Codedateien.

Push oder Pull?

Abgesehen von den TextBlock- und Slider-Steuerelementen ist das einzige andere visuelle Element in diesem Programm eine Klasse namens Oscilloscope, die von UserControl abgeleitet wird. Die Daten für Oscilloscope werden von zwei Instanzen einer Klasse namens SineCurve bereitgestellt.

SineCurve verfügt selbst über keine visuellen Elemente. Ich habe die Klasse jedoch von FrameworkElement abgeleitet, sodass ich die beiden Instanzen in der visuellen Struktur anordnen und Bindungen für sie definieren konnte. Alles im Programm wird mittels Bindungen verbunden, von den Slider-Steuerelementen zu den SineCurve-Elementen und von SineCurve zu Oscilloscope. Die Datei „MainPage.xaml.cs“ für die Webversion des Programms hat keinen Code außer dem Code, der per Voreinstellung bereitgestellt wird, und die entsprechende Datei in der Phone-Anwendung implementiert lediglich Tombstoning-Logik.

SineCurve definiert zwei Eigenschaften (unterstützt durch Abhängigkeitseigenschaften) namens Frequency und Amplitude. Eine SineCurve-Instanz stellt die horizontalen Werte für Oscilloscope bereit, und die andere die vertikalen Werte.

Die SineCurve-Klasse implementiert auch eine Schnittstelle, die ich IProvideAxisValue genannt habe:

public interface IProvideAxisValue {
  double GetAxisValue(DateTime dateTime);
}

SineCurve implementiert diese Schnittstelle mittels einer vergleichsweise einfachen Methode, die auf zwei Felder und die beiden Eigenschaften verweist:

public double GetAxisValue(DateTime dateTime) {
  phaseAngle += 2 * Math.PI * this.Frequency * 
    (dateTime - lastDateTime).TotalSeconds;
  phaseAngle %= 2 * Math.PI;
  lastDateTime = dateTime;

  return this.Amplitude * Math.Sin(phaseAngle);
}

Die Klasse Oscilloscope definiert zwei Eigenschaften (ebenfalls von Abhängigkeitseigenschaften unterstützt) namens XProvider und YProvider vom Typ IProvideAxisValue. Zur Auslösung der Aktion installiert Oscilloscope einen Handler für das Ereignis CompositionTarget.Rendering. Dieses Ereignis tritt synchron mit der Aktualisierungsrate der Videoanzeige ein und dient so als ein komfortables Tool für die Durchführung von Animationen. Bei jedem Aufruf des Handlers CompositionTarget.Rendering ruft Oscilloscope GetAxisValue auf den beiden SineCurve-Objekten für die XProvider- und YProvider-Eigenschaften auf.

Mit anderen Worten, das Programm implementiert ein Pull-Modell. Das Oscilloscope-Objekt legt fest, wenn es Daten benötigt, und ruft anschließend die Daten von den beiden Datenanbietern ab. (Die Art und Weise, wie diese Daten angezeigt werden, bespreche ich demnächst.)

Als ich damit begann, dem Programm weitere Features hinzuzufügen, insbesondere zwei Instanzen eines zusätzlichen Steuerelements, das die Sinuskurven anzeigt (das ich jedoch später wieder entfernte, da es lediglich eine Ablenkung war), bekam ich Zweifel hinsichtlich der Sinnhaftigkeit dieses Modells. Ich hatte drei Objekte, die die gleichen Daten von zwei Anbietern bezogen. Ich fand, dass ein Push-Modell wahrscheinlich besser geeignet wäre.

Ich restrukturierte das Programm so, dass die SineCurve-Klasse einen Handler für CompositionTarget.Rendering installiert und über Eigenschaften namens X und Y des Typs Double Daten zum Oscilloscope-Steuerelement verschiebt.

Ich hätte wahrscheinlich den grundlegenden Fehler in diesem Push-Modell erkennen müssen: Das Oscilloscope-Steuerelement erhielt nun zwei getrennte Änderungen in X und Y und konstruierte daher keine glatte Kurve, sondern eine Folge von Treppenstufen, wie in Abbildung 3 gezeigt.

image: The Disastrous Result of a Push-Model Experiment

Abbildung 3 Das desaströse Ergebnis eines Push-Modell-Experiments

Es fiel mir nicht schwer, zum Pull-Modell zurückzukehren!

Rendern mit WriteableBitmap

Von dem Moment an, an dem ich dieses Programm entwickelt hatte, hatte ich keinen Zweifel daran, dass die Verwendung von WriteableBitmap die beste Lösung für die Implementierung des tatsächlichen Oszilloskop-Bildschirms war.

WriteableBitmap ist eine Silverlight-Bitmap, die die Pixeladressierung unterstützt. Sämtliche Pixel der Bitmap werden als ein Array aus 32-Bit-Integern dargestellt. Programme können diese Pixel nach dem Zufallsprinzip erhalten und festlegen. WriteableBitmap verfügt außerdem über eine Render-Methode, mit der die visuellen Elemente jedes Objekts vom Typ FrameworkElement zur Bitmap gerendert werden können.

Wenn Oscilloscope nur eine einfache statische Kurve anzeigen müsste, würde ich Polyline oder Path verwenden und noch nicht einmal an WriteableBitmap denken. Auch wenn diese Kurve die Form verändern muss, wären Polyline oder Path weiterhin die bevorzugte Wahl. Die von Oscilloscope angezeigte Kurve muss jedoch an Größe zunehmen und ungewöhnliche Farben anzeigen. Die Linie muss fortschreitend verblassen. Kürzlich angezeigte Abschnitte der Linie sind heller als ältere Abschnitte. Wenn ich eine einzelne Kurve verwenden würde, müsste diese verschiedene Farben verwenden. Dieses Konzept wird in Silverlight nicht unterstützt!

Ohne WriteableBitmap müsste das Programm mehrere hundert verschiedene Polyline-Elemente erstellen, die alle unterschiedliche Farben aufweisen und verteilt werden müssen. Außerdem müssen sie Layoutübergaben nach jedem CompositionTarget.Rendering-Ereignis auslösen. Nach allem, was ich über die Silverlight-Programmierung wusste, würde WriteableBitmap eine wesentlich bessere Leistung bereitstellen.

In einer frühen Version der Oscilloscope-Klasse wurde das CompositionTarget.Rendering-Ereignis verarbeitet, indem neue Werte aus den beiden SineCurve-Anbietern bezogen wurden. Diese wurden an die Größe von WriteableBitmap angepasst, und anschließend wurde ein Line-Objekt vom vorherigen Punkt zum aktuellen Punkt konstruiert. Dies wurde einfach an die Render-Methode von WriteableBitmap übergeben:

writeableBitmap.Render(line, null);

Die Oscilloscope-Klasse definiert eine Eigenschaft Persistence, die die Anzahl der Sekunden anzeigt, die eine Farbe oder eine Alphakomponente eines Pixels für die Abnahme von 255 zu 0 benötigt. Für die Verblassung dieser Pixel ist die direkte Pixeladressierung erforderlich. Dieser Code wird in Abbildung 4 gezeigt.

Abbildung 4 Code für das Verblassen von Pixelwerten

accumulatedDecrease += 256 * 
  (dateTime - lastDateTime).TotalSeconds / Persistence;
int decrease = (int)accumulatedDecrease;

// If integral decrease, sweep through the pixels
if (decrease > 0) {
  accumulatedDecrease -= decrease;

  for (int index = 0; index < 
    writeableBitmap.Pixels.Length; index++) {

    int pixel = writeableBitmap.Pixels[index];

    if (pixel != 0) {
      int a = pixel >> 24 & 0xFF;
      int r = pixel >> 16 & 0xFF;
      int g = pixel >> 8 & 0xFF;
      int b = pixel & 0xFF;

      a = Math.Max(0, a - decrease);
      r = Math.Max(0, r - decrease);
      g = Math.Max(0, g - decrease);
      b = Math.Max(0, b - decrease);

      writeableBitmap.Pixels[index] = a << 24 | r << 16 | g << 8 | b;
    }
  }
}

An diesem Punkt der Programmentwicklung unternahm ich die erforderlichen Schritte, um die Ausführung auf dem Telefon zu ermöglichen. Das Programm wurde anscheinend sowohl im Web als auch auf dem Telefon problemlos ausgeführt. Ich wusste jedoch, dass es noch nicht ganz fertig war. Ich konnte auf dem Oszilloskop-Bildschirm keine Kurven sehen: Alles, was ich sah, war eine Reihe von miteinander verbundenen geraden Linien. Und nichts zerstört die Illusion einer digital simulierten Analogeinrichtung mehr als eine Reihe äußerst gerader Linien!

Interpolierung

Der CompositionTarget.Rendering-Handler wird synchron mit der Aktualisierung der Videoanzeige aufgerufen. Für die meisten Videoanzeigen, einschließlich der Anzeige für Windows Phone 7, bedeutet dies 60 Bilder pro Sekunde. Mit anderen Worten, der Ereignishandler CompositionTarget.Rendering wird ungefähr alle 16 oder 17 ms aufgerufen. (Sie werden sehen, dass dies lediglich die optimale Situation ist.) Auch wenn die Sinuswellen nur über einen einzigen Zyklus pro Sekunde verfügen würden, würde dies bedeuten, dass im Fall eines Oszilloskops, das 480 Pixel breit ist, die Pixelkoordinaten möglicherweise um 35 Pixel getrennt sind.

Das Oszilloskop müsste eine Kurve zwischen zwei aufeinander folgenden Pixeln interpolieren. Aber welche Art von Kurve?

Meine erste Wahl war eine kanonische Splinekurve (auch unter dem Namen kardinale Splinekurve bekannt). Für eine Folge von Kontrollpunkten p1, p2, p3 und p4 stellt die kanonische Splinekurve eine kubische Interpolierung zwischen p2 und p3 bereit, deren Kurvigkeitsgrad auf einem „Spannungsfaktor“ beruht. Dies ist eine Lösung für allgemeine Zwecke.

Die kanonische Splinekurve wird in Windows Forms unterstützt, jedoch nicht in Windows Presentation Foundation (WPF) oder Silverlight. Glücklicherweise war ich im Besitz eines WPF- und Silverlight-Codes für die kanonische Splinekurve, den ich 2009 für einen Blogbeitrag entwickelt hatte: „Canonical Splines in WPF and Silverlight“ (bit.ly/bDaWgt, Kanonische Splinekurven in WPF und Silverlight“).

Nach der Generierung einer Polyline durch Interpolierung wird die CompositionTarget.Rendering-Verarbeitung nun durch den folgenden Aufruf abgeschlossen:

writeableBitmap.Render(polyline, null);

Die kanonische Splinekurve funktionierte zwar, aber es war immer noch nicht ganz richtig. Wenn die Frequenzen der beiden Sinuskurven einfache Integralmehrfache sind, sollte sich die Kurve als festes Muster stabilisieren. Das geschah jedoch nicht, und ich erkannte, dass die interpolierte Kurve leicht unterschiedlich war, je nach den tatsächlich erfassten Punkten. 

Das Problem wurde auf dem Telefon überdeutlich. Dies lag vor allem daran, dass der kleine Telefonprozessor die zahlreichen Anforderungen durch mein Programm nur schwer bewältigen konnte. Bei höheren Frequenzen sahen die Lissajous-Kurven auf dem Telefon zwar glatt und wie Kurven aus, bewegten sich jedoch anscheinend in beinahe zufällig ausgewählten Mustern!

Nur allmählich wurde mir klar, dass ich zeitabhängig interpolieren kann. Zwei aufeinander folgende Aufrufe des Ereignishandlers CompositionTarget.Rendering sind um ungefähr 17 ms voneinander getrennt. Ich könnte einfach eine Schleife durch sämtliche zwischenliegenden Millisekundenwerte bilden und die GetAxisValue-Methode in den beiden SineCurve-Anbietern aufrufen, um eine glattere Polyline zu konstruieren.

Dieser Ansatz funktionierte wesentlich besser.

Bessere Leistung

Eine wesentliche Lektüre für alle Windows Phone 7-Programmierer ist „Performance Considerations in Applications for Windows Phone“ unter bit.ly/fdvh7Z (Überlegungen zur Leistung von Anwendungen für Windows Phone). Abgesehen von den zahlreichen nützlichen Hinweisen zur Verbesserung von Telefonanwendungen finden Sie hier auch Informationen zur Bedeutung der Zahlen, die seitlich auf dem Bildschirm angezeigt werden, wenn Sie das Programm unter Visual Studio ausführen, wie in Abbildung 5 gezeigt.

image: Performance Indicators in Windows Phone 7

Abbildung 5 Leistungsanzeigen in Windows Phone 7

Diese Zahlenreihe wird durch die Festlegung der Eigenschaft Application.Current.Host.Settings.EnableFrameRateCounter als „True“ aktiviert. Dies wird durch die App.xaml.cs-Datei durchgeführt, wenn das Programm unter dem Visual Studio-Debugger ausgeführt wird.

Die ersten beiden Zahlen sind die wichtigsten: Manchmal sind diese beiden Zahlen Null, wenn keine Ereignisse stattfinden. Beide Zahlen sollen jedoch die Bildraten anzeigen. Das bedeutet, sie zeigen die Anzahl der Bilder pro Sekunde an. Ich habe erwähnt, dass die meisten Videoanzeigen mit einer Geschwindigkeit von 60 Bildern pro Sekunde aktualisiert werden. Es kann jedoch sein, dass eine Anwendung Animationen ausführt, bei denen die einzelnen Bilder mehr als 16 oder 17 ms für die Verarbeitung benötigen.

Nehmen Sie zum Beispiel an, dass ein CompositionTarget.Rendering-Handler 50 ms benötigt, um die jeweilige Aufgabe durchzuführen. In diesem Fall aktualisiert das Programm die Videoanzeige 20-mal pro Sekunde. Das ist die Bildrate des Programms.

20 Bilder pro Sekunde sind nun keine erschreckende Bildrate. Denken Sie daran, dass Kinofilme mit 24 Bildern pro Sekunde ausgeführt werden. Normale Fernsehsendungen werden in den USA mit einer effektiven Bildrate (unter Berücksichtigung von Interlacing) von 30 Bildern/Sekunde ausgeführt; in Europa sind dies 25 Bilder/Sekunde. Sobald die Bildrate auf 15 oder 10 Bilder/Sekunde abfällt, wird dies erkennbar.

Silverlight for Windows Phone kann einige Animationen in die Graphics Processing Unit (GPU) verschieben, sodass es einen sekundären Thread gibt (manchmal als Kompositions- oder GPU-Thread bezeichnet), der mit der GPU interagiert. Die erste Zahl stellt die Bildrate dar, die mit diesem Thread verknüpft ist. Die zweite Zahl ist die Bildrate der Benutzeroberfläche und bezieht sich auf den primären Thread der Anwendung. Das ist der Thread, in dem die CompositionTarget.Rendering-Handler ausgeführt werden.

Bei der Ausführung des LissajousCurves-Programms auf meinem Telefon wurden die Zahlen 22 und 11 für den GPU- und den Benutzeroberflächenthread angezeigt. Wenn ich die Frequenz für die Sinuskurven erhöhte, nahmen diese Zahlen leicht ab. Konnte ich dies verbessern?

Ich begann mich zu fragen, wie viel Zeit diese wichtige Anweisung in meiner CompositionTarget.Rendering-Methode für die Ausführung erforderte:

writeableBitmap.Render(polyline, null);

Diese Anweisung sollte 60-mal pro Sekunde für eine Polyline aus 16 oder 17 Linien aufgerufen werden. Tatsächlich jedoch wurde sie eher 11-Mal pro Sekunde für Polylines mit 90 Linien aufgerufen.

Für mein Buch „Programming Windows Phone 7“ (Programmierung für Windows Phone 7, Microsoft Press 2010) schrieb ich XNA-Logik für die Wiedergabe von Linien. Diese konnte ich in Silverlight für die hier behandelte Oscilloscope-Klasse anpassen. Nun rief ich die Render-Methode von WriteableBitmap überhaupt nicht auf, sondern änderte Pixel direkt in der Bitmap, um die Polylines zu zeichnen.

Leider fielen beide Bildraten auf Null ab! Das zeigte mir, dass Silverlight Linien in einer Bitmap sehr viel schneller rendern kann, als es mein Programm konnte. (Ich sollte auch anmerken, dass mein Code nicht für Polylines optimiert war.)

An diesem Punkt stellte sich mir die Frage, ob ein anderer als der Ansatz mit WriteableBitmap gewählt werden sollte. Ich ersetzte die WriteableBitmap- und Image-Elemente durch ein Canvas-Element und fügte während der Konstruierung der einzelnen Polylines einfach dieses Canvas-Element hinzu.

Dies können Sie natürlich nicht unendlich durchführen. Sie möchten kein Canvas-Element mit Hunderttausenden von untergeordneten Objekten. Und außerdem mussten diese untergeordneten Polyline-Elemente verblassen! Ich versuchte zwei verschiedene Ansätze. Der erste Ansatz bestand darin, dass ich jeder Polyline eine Farbanimation hinzufügte, um den Wert für den Alphakanal der jeweiligen Farbe zu senken. Nach Abschluss der Animation wird die Polyline vom Canvas-Element entfernt. Der zweite Ansatz bestand in einem eher manuellen Verfahren für die Enumerierung der einzelnen untergeordneten Polyline-Elemente, die manuelle Reduzierung des Werts für den Alphakanal der Farbe und die Entfernung des untergeordneten Elements, wenn der Wert für den Alphakanal Null erreichte.

Diese vier Methoden sind in der Oscilloscope-Klasse weiterhin vorhanden, und sie werden durch vier #define-Anweisungen am Anfang der C#-Datei aktiviert. Abbildung 6 zeigt die Bildraten für die einzelnen Ansätze.

Abbildung 6 Bildraten für die vier Methoden für die Aktualisierung des Oszilloskops

  Kompositionsthread Benutzeroberflächenthread
WriteableBitmap mit Polyline-Rendering 22 11
WriteableBitmap mit manuellen Umrissfüllungen 0 0
Canvas-Element mit Polyline-Element und Verblassung durch Animation 20 20
Canvas-Element mit Polyline-Element und manueller Verblassung 31 15

Abbildung 6 zeigt, dass meine ursprüngliche Annahme in Bezug auf WriteableBitmap falsch war. In diesem Fall ist es wirklich besser, eine Reihe von Polyline-Elementen in einem Canvas-Element zu platzieren. Die beiden Verfahren für die Verblassung sind interessant. Bei Verwendung der Animation erfolgt die Verblassung im Kompositionsthread mit 20 Bildern pro Sekunde. Bei manueller Durchführung erfolgt die Verblassung im Benutzeroberflächenthread mit 15 Bildern pro Sekunde. Neue Polyline-Elemente werden jedoch stets im Benutzeroberflächenthread hinzugefügt, und diese Bildrate beträgt 20, wenn die Verblassungslogik in die GPU verschoben wird.

Zusammenfassend lässt sich sagen, dass die dritte Methode insgesamt das beste Ergebnis aufweist.

Was haben wir also heute gelernt? Um das beste Ergebnis zu erzielen, müssen Sie experimentieren! Versuchen Sie verschiedene Ansätze, und vertrauen Sie niemals Ihren anfänglichen Instinkten.

Charles Petzold schreibt seit langem redaktionelle Beiträge für das MSDN Magazin*. Sein neues Buch „Programming Windows Phone 7“ (Programmierung für Windows Phone 7, Microsoft Press 2010) ist als kostenloser Download auf bit.ly/cpebookpdf verfügbar*.

Unser Dank gilt dem folgenden technischen Experten für die Durchsicht dieses Artikels: Jesse Liberty