Sonderausgabe 2015 zu Windows 10

Band 30, Nummer 11

Grafiken und Animation – Das Windows-Kompositionsmodul wird 10

Von Kenny Kerr | Windows 2015

Das Windows-Kompositionsmodul, auch als Desktop Window Manager (DWM) bekannt, erhält für Windows 10 eine neue API. DirectComposition war die primäre Schnittstelle für die Komposition, doch als klassische COM-API war sie für den durchschnittlichen Anwendungsentwickler außer Reichweite. Die neue Windows-Kompositions-API basiert auf der Windows-Runtime (WinRT) und bietet die Grundlage für ein leistungsstarkes Rendering. Dazu wird die Welt der von Direct2D und Direct3D gebotenen Immediate-Mode-Grafiken mit einer visuellen Retained-Mode-Struktur kombiniert, die nun wesentlich verbesserte Animations- und Effektfunktionen bietet.

2006 habe ich zum ersten Mal über den DVM geschrieben, als Windows Vista in der Betaphase war (goo.gl/19jCyR). Er ermöglichte Ihnen das Steuern des Ausmaßes des Weichzeichnereffekts für ein bestimmtes Fenster und Erstellen von benutzerdefiniertem Chrom für ein ansprechendes Vermischen mit dem Desktop. Abbildung 1 zeigt dies in der praktischen Umsetzung unter Windows 7. Es war möglich, mit Direct3D und Direct2D ein hardwarebeschleunigtes Rendering zu generieren, um beachtliche visuelle Objekte für Ihre Anwendung zu schaffen (goo.gl/IufcN1). Sie konnten sogar die alte Welt von GDI- und USER-Steuerelementen mit dem DWM (goo.gl/9ITISE) mischen. Dennoch war jedem aufmerksamen Beobachter klar, dass der DWM wesentlich mehr zu bieten hatte. Das Windows-Flip-3D-Feature in Windows 7 war ein überzeugender Nachweis.

Windows Aero
Abbildung 1: Windows Aero

In Windows 8 wurde eine neue API für den DWM mit dem Namen DirectComposition eingeführt, deren Name der DirectX-Familie klassischer COM-APIs Anerkennung erweist, die ihren Entwurf inspiriert hat. Dank DirectComposition konnten sich Entwickler ein klareres Bild der Möglichkeiten des DWM verschaffen. Zudem wurde eine verbesserte Terminologie geboten. Der DWM ist das eigentliche Windows-Kompositionsmodul, das in der Lage war, die umwerfenden Effekte in Windows Vista und Windows 7 dadurch zu erzeugen, dass das Rendering von Desktopfenstern grundlegend verändert wurde. Standardmäßig erzeugte das Kompositionsmodul für jedes Fenster auf oberster Ebene eine Umleitungsoberfläche. Die Details dazu habe ich in meiner Kolumne vom Juni 2014 beschrieben (goo.gl/oMlVa4). Diese Umleitungsoberflächen bildeten eine visuelle Struktur, und DirectComposition ermöglichte Anwendungen das Nutzen derselben Technologie, um eine funktionsreduzierte Retained-Mode-API für Hochleistungsgrafiken bereitzustellen. DirectComposition bot eine visuelle Struktur und Oberflächenverwaltung, die es der Anwendung ermöglichte, die Erzeugung von Effekten und Animationen an das Kompositionsmodul auszulagern. Ich habe diese Möglichkeiten in meinen Kolumnen vom August (goo.gl/CNwnWR) und September 2014 (goo.gl/y7ZMLL) beschrieben. Ich habe sogar einen Kurs zum Hochleistungsrendering mit DirectComposition für Pluralsight entwickelt (goo.gl/fgg0XN).

DirectComposition gab in Windows 8 seinen Einstand zusammen mit beeindruckenden Verbesserungen am Rest der DirectX-Familie von APIs. Doch es läutete auch eine neue Ära für die Windows-API ein und veränderte für immer die Weise, in der Entwickler das Betriebssystem betrachten. Die Einführung der Windows-Runtime überschattete alles andere. Microsoft versprach eine frische neue Art der Anwendungsentwicklung und des Zugriffs auf Betriebssystemdienste, die die letztendliche Ausmusterung der sog. Win32-API bedeutete, die für lange Zeit die dominierende Form der Entwicklung von Anwendungen und Interaktion mit dem Betriebssystem darstellte. Windows 8 hatte einen ruckeligen Start, doch mit Windows 8.1 wurden zahlreiche Probleme behoben. Und Windows 10 bietet nun eine wesentlich umfassendere API, die weitaus mehr Entwickler zufrieden stellen wird, die sich für das Erstellen erstklassiger, oder besser gesagt, ernstzunehmender Anwendungen für Windows interessieren.

Windows 10 wurde im Juli 2015 mit einer Vorschau auf die neue Kompositions-API veröffentlicht, die noch nicht bereit für die Produktion war. Es waren noch Änderungen vorbehalten, weshalb sie nicht in universellen Windows-Apps genutzt werden konnte, die in den Windows Store übertragen wurden. Das war ganz recht so, da die Kompositions-API, die nun für die Produktion verfügbar ist, wesentlich geändert wurde, und zwar zum Besseren. Dieses Windows 10-Update ist zudem das erste Mal, dass dieselbe Kompositions-API für alle Formfaktoren zur Verfügung steht, wodurch das "universelle" bei der universellen Windows-Plattform weiter an Glaubwürdigkeit gewinnt. Die Komposition funktioniert identisch unabhängig davon, ob Sie für einen Hochleistungs-PC mit mehreren Bildschirmen oder das kleine Smartphone in Ihrer Jackentasche entwickeln.

Natürlich ist der Aspekt, den alle an der Windows-Runtime mögen, dass schlussendlich das Versprechen einer Common Language Runtime für Windows erfüllt wird. Wenn Sie lieber in C# programmieren, können Sie die Windows-Runtime dank der in das Microsoft .NET Framework integrierten Unterstützung direkt verwenden. Wenn Sie wie ich lieber C++ verwenden, können Sie die Windows-Runtime ohne zwischengeschaltete oder aufwendige Abstraktionen nutzen. Die Windows-Runtime basiert auf COM statt auf .NET und ist deshalb ideal für die Nutzung in C++ geeignet. Ich arbeite für die Windows-Runtime mit Modern C++ (moderncpp.com), der standardmäßigen C++-Sprachprojektion, doch Sie können weiter in Ihrer bevorzugten Sprache arbeiten, da die API trotzdem gleich ist. Ich stelle sogar einige Beispiele in C# bereit, um zu veranschaulichen, wie reibungslos die Windows-Runtime verschiedene Sprachen unterstützen kann.

Die Windows-Kompositions-API entfernt sich von ihren DirectX-Wurzeln. Während DirectComposition ein Geräteobjekt bereitgestellt hat, das Direct3D- und Direct2D-Geräte zum Vorbild hatte, beginnt die Windows-Kompositions-API mit einem Compositor. Dieser dient allerdings demselben Zweck, indem er als Factory für Kompositionsressourcen fungiert. Darüber hinaus ist die Windows-Komposition DirectComposition sehr ähnlich. Es gibt ein Kompositionsziel, das die Beziehung zwischen einem Fenster und seiner visuellen Struktur darstellt. Die Unterschiede werden offenkundiger, wenn Sie sich die visuellen Objekte einmal näher anschauen. Ein visuelles DirectComposition-Objekt hatte eine Inhaltseigenschaft, die eine Art von Bitmap bereitgestellt hat. Das Bitmap war eines von drei Dingen: eine Kompositionsoberfläche, eine DXGI-Swapchain oder die Umleitungsoberfläche eines anderen Fensters. Eine typische DirectComposition-Anwendung bestand aus visuellen Objekten und Oberflächen, wobei die Oberflächen als Inhalt oder Bitmaps für die verschiedenen visuellen Objekte fungierten. Wie Abbildung 2 zeigt, sieht die visuelle Struktur einer Komposition etwas anders aus. Das neue visuelle Objekt hat keine Inhaltseigenschaft und wird stattdessen mit einem Kompositionspinsel gerendert. Dies erweist sich als wesentlich flexiblere Abstraktion. Während der Pinsel wie zuvor nur ein Bitmap rendern kann, können einfache Pinsel mit Volltonfarbe sehr effizient erstellt und aufwendigere Pinsel, zumindest konzeptuell, auf eine Weise definiert werden, die vergleichbar damit ist, wie Direct2D Effekte bereitstellt, die als Bilder behandelt werden können. Die richtige Abstraktion macht den ganzen Unterschied aus.

Die visuelle Struktur der Windows-Komposition
Abbildung 2: Die visuelle Struktur der Windows-Komposition

Lassen Sie uns einen Blick auf einige praktische Beispiele werfen, um zu zeigen, wie das alles funktioniert und was alles möglich ist. Sie können wiederum Ihre bevorzugte WinRT-Sprachprojektion wählen. Sie können mit Modern C++ wie folgt einen Compositor erstellen:

using namespace Windows::UI::Composition;
Compositor compositor;

Dies ist auch in C# möglich:

using Windows.UI.Composition;
Compositor compositor = new Compositor();

Sie können sogar die überladenere Syntax von C++/CX verwenden:

using namespace Windows::UI::Composition;
Compositor ^ compositor = ref new Compositor();

Aus API-Sicht sind diese alle gleich und geben lediglich die Unterschiede bei der Sprachprojektion wieder. Es gibt derzeit im Wesentlichen zwei Möglichkeiten, eine universelle Windows-App zu schreiben. Der vielleicht gängigste Ansatz ist das Verwenden des "Windows.UI.Xaml"-Namespace des Betriebssystems. Falls XAML für Ihre App nicht so wichtig ist, können Sie auch das zugrunde liegende Anwendungsmodell direkt ohne Abhängigkeit von XAML verwenden. Ich habe das WinRT-Anwendungsmodell in meiner Kolumne vom August 2013 beschrieben (goo.gl/GI3OKP). Bei diesem Ansatz benötigen Sie nur eine Minimalimplementierung der Schnittstellen "IFrameworkView" und "IFrameworkViewSource" und können dann loslegen. Abbildung 3 zeigt einen Basisentwurf in C#, der sich für die ersten Schritte eignet. Die Windows-Komposition bietet auch eine umfassende Integration in XAML, doch lassen Sie uns mit einer einfachen XAML-freien Anwendung starten, die eine einfachere Umgebung bietet, in der Sie mehr über Komposition erfahren können. Ich werde auf XAML an späterer Stelle in diesem Artikel eingehen.

Abbildung 3: Windows-Runtime-Anwendungsmodell in C#

using Windows.ApplicationModel.Core;
using Windows.UI.Core;
class View : IFrameworkView, IFrameworkViewSource
{
  static void Main()
  {
    CoreApplication.Run(new View());
  }
  public IFrameworkView CreateView()
  {
     return this;
  }
  public void SetWindow(CoreWindow window)
  {
    // Prepare composition resources here...
  }
  public void Run()
  {
    CoreWindow window = CoreWindow.GetForCurrentThread();
    window.Activate();
    window.Dispatcher.ProcessEvents(CoreProcessEventsOption.ProcessUntilQuit);
  }
  public void Initialize(CoreApplicationView applicationView) { }
  public void Load(string entryPoint) { }
  public void Uninitialize() { }
}

Der Compositor muss in der "SetWindow"-Methode (siehe Abbildung 3) erstellt werden. Dies ist tatsächlich der früheste Punkt im Lebenszyklus der Anwendung, an dem dies erfolgen kann, da der Compositor vom Verteiler des Fensters abhängt. Dies ist auch der Punkt, an dem sowohl Fenster als auch Verteiler schließlich vorhanden sind. Die Beziehung zwischen dem Compositor und der App-Ansicht kann anschließend durch Erstellen eines Kompositionsziels eingerichtet werden:

CompositionTarget m_target = nullptr;
// ...
m_target = compositor.CreateTargetForCurrentView();

Es ist wichtig, dass die Anwendung das Kompositionsziel aktiv hält. Deshalb müssen Sie zu einer Membervariablen Ihrer "IFrameworkView"-Implementierung machen. Wie zuvor erwähnt, stellt das Kompositionsziel die Beziehung zwischen dem Fenster oder der Ansicht und seiner/ihrer visuellen Struktur dar. Alles, was Sie mit einem Kompositionsziel anstellen können, ist die Festlegung des visuellen Stammobjekts. In der Regel ist dies ein visuelles Containerobjekt:

ContainerVisual root = compositor.CreateContainerVisual();
m_target.Root(root);

Hier verwende ich C++, das keine Sprachunterstützung für Eigenschaften bietet, weshalb die "Root"-Eigenschaft als Accessormethoden projiziert wird. C# verhält sich bei der Syntax zum Hinzufügen von Eigenschaften sehr ähnlich:

ContainerVisual root = compositor.CreateContainerVisual();
m_target.Root = root;

DirectComposition bot nur eine Art von visuellem Objekt, das verschiedene Typen von Oberflächen zur Darstellung von Bitmapinhalten unterstützte. Die Windows-Komposition bietet kleine Klassenhierarchien, die verschiedene Arten von visuellen Objekten, Pinseln und Animationen darstellen. Es gibt jedoch nur eine Art von Oberfläche, die auch nur mit C++ erstellt werden kann, da sie zur Interoperabilitäts-API für die Windows-Komposition gehört, die von Frameworks wie XAML und erfahreneren Anwendungsentwicklern verwendet werden soll.

In Abbildung 4 sehen Sie die visuelle Klassenhierarchie. Ein "CompositionObject" ist eine vom Compositor unterstützte Ressource. Alle Kompositionsobjekte können potenziell über animierte Eigenschaften verfügen. Ein visuelles Objekt bietet eine Vielzahl von Eigenschaften zum Kontrollieren vieler Aspekte der relativen Position, Darstellung, des Clippings und der Renderingoptionen des visuellen Objekts. Es enthält eine Transformationsmatrixeigenschaft sowie Verknüpfungen für Skalierung und Drehung. Dies ist eine leistungsstarke Basisklasse. Im Gegensatz dazu ist "ContainerVisual" eine relativ einfache Klasse, die lediglich eine "Children"-Eigenschaft hinzufügt. Während Sie visuelle Containerobjekte direkt erstellen können, fügt ein "SpriteVisual" die Fähigkeit zum Zuordnen eines Pinsels dergestalt hinzu, dass das visuelle Objekt Pixel tatsächlich selbst rendern kann.

Visuelle Kompositionsobjekte
Abbildung 4: Visuelle Kompositionsobjekte

Bei einem gegebenen visuellen Stammcontainerobjekt kann ich eine beliebige Anzahl untergeordneter visueller Objekte erstellen:

VisualCollection children = root.Children();

Diese können auch visuelle Containerobjekte sein, doch es ist wahrscheinlicher, dass es sich um visuelle Spriteobjekte handelt. Mithilfe einer "for"-Schleife in C++ kann ich drei visuelle Objekte als untergeordnete Objekte des visuellen Stammobjekts hinzufügen:

using namespace Windows::Foundation::Numerics;
for (unsigned i = 0; i != 3; ++i)
{
  SpriteVisual visual = compositor.CreateSpriteVisual();
  visual.Size(Vector2{ 300.0f, 200.0f });
  visual.Offset(Vector3{ 50 + 20.0f * i, 50 + 20.0f * i });
  children.InsertAtTop(visual);
}

Sie können sich das Anwendungsfenster in Abbildung 5 einfach vorstellen. Dennoch führt dieser Code zu keinem Rendering, da diesen visuellen Objekten kein Pinsel zugeordnet ist. In Abbildung 6 sehen Sie die Pinselklassenhierarchie. Ein "CompositionBrush" ist einfach eine Basisklasse für Pinsel und bietet selbst keine Funktionalität. Ein "CompositionColorBrush" stellt die einfachste Art dar und bietet nur ein Farbeigenschaft für das Rendering visueller Objekte mit Vollfarben. Das mag sich jetzt nicht sehr aufregend anhören, doch vergessen Sie nicht, dass Sie Animationen mit dieser Farbeigenschaft verbinden können. Die Klassen "CompositionEffectBrush" und "CompositionSurfaceBrush" stehen in Beziehung, sind aber komplexere Pinsel, da sie von anderen Ressourcen unterstützt werden. Ein "CompositionSurfaceBrush" rendert eine Kompositionsoberfläche für alle zugeordneten visuellen Objekte. Diese Klasse hat eine Vielzahl von Eigenschaften zum Steuern der Bitmapzeichnung, wie z. B. "Interpolation", "Alignment" und "Stretch", ganz zu schweigen von der Oberfläche selbst. Ein "Composition­EffectBrush" verwendet verschiedene Oberflächenpinsel zum Erzeugen diverser Effekte.

Untergeordnete visuelle Objekte in einem Fenster
Abbildung 5: Untergeordnete visuelle Objekte in einem Fenster

Kompositionspinsel
Abbildung 6: Kompositionspinsel

Das Erstellen und Anwenden eines Farbpinsels ist unkompliziert. Hier ein Beispiel in C#:

using Windows.UI;
CompositionColorBrush brush = compositor.CreateColorBrush();
brush.Color = Color.FromArgb(0xDC, 0x5B, 0x9B, 0xD5);
visual.Brush = brush;

Die "Color"-Struktur stammt aus dem "Windows.UI"-Namespace und weist Alpha, Rot, Grün und Blau als 8-Bit-Farbwerte auf. Dies ist eine Abweichung von der DirectComposition- und Direct2D-Vorliebe für Gleitkommafarbwerte. Ein nützliches Merkmal dieses Ansatzes für visuelle Objekte und Pinsel ist, dass die "Color"-Eigenschaft jederzeit geändert werden kann und dass alle visuellen Objekte, die auf denselben Pinsel verweisen, automatisch aktualisiert werden. Die "Color"-Eigenschaft kann, wie zuvor angedeutet, sogar animiert werden. Wie funktioniert das? Damit kommen wir zu den Animationsklassen.

In Abbildung 7 sehen Sie die Animationsklassenhierarchie. Die "CompositionAnimation"-Basisklasse ermöglicht das Speichern benannter Werte für die Verwendung mit Ausdrücken. Auf Ausdrücke werde ich in Kürze weiter eingehen. Eine "KeyFrameAnimation" bietet typische keyframebasierte Animationseigenschaften wie Dauer, Iteration und Stoppverhalten. Die verschiedenen Keyframeanimationsklassen bieten typspezifische Methoden für das Einfügen von Keyframes sowie typspezifische Animationseigenschaften. Beispielsweise ermöglicht "ColorKeyFrameAnimation" Ihnen das Einfügen von Keyframes mit Farbwerten und einer Eigenschaft zum Steuern des Farbraums für die Interpolation zwischen Keyframes.

Kompositionsanimationen
Abbildung 7: Kompositionsanimationen

Das Erstellen eines Animationsobjekts und anschließende Anwenden dieser Animation auf ein bestimmtes Kompositionsobjekt ist überraschend einfach. Angenommen, ich möchte die Deckkraft eines visuellen Objekts animieren. Dazu kann ich die Deckkraft des visuellen Objekts mithilfe eines Skalarwerts in C++ wie folgt auf 50 % festlegen:

visual.Opacity(0.5f);

Alternativ können Sie ein skalares Animationsobjekt mit Keyframes erstellen, um eine Animationsvariable von 0,0 bis 1,0 zu generieren, die eine Deckkraft von 0-100 % darstellt.

ScalarKeyFrameAnimation animation =
  compositor.CreateScalarKeyFrameAnimation();
animation.InsertKeyFrame(0.0f, 0.0f); // Optional
animation.InsertKeyFrame(1.0f, 1.0f);

Der erste Parameter von "InsertKeyFrame" ist der relative Versatz vom Start der Animation (0,0) bis zum Ende der Animation (1,0). Der zweite Parameter ist der Wert der Animationsvariablen an diesem Punkt auf der Animationszeitachse. Bei dieser Animation wird der Wert im Verlauf der Animation glatt von 0,0 in 1,0 überführt. Ich kann anschließend die Gesamtdauer dieser Animation wie folgt festlegen:

using namespace Windows::Foundation;
animation.Duration(TimeSpan::FromSeconds(1));

Da die Animation startklar ist, muss ich sie nur mit dem Kompositionsobjekt und der Eigenschaft meiner Wahl verbinden:

visual.StartAnimation(L"Opacity", animation);

Die "StartAnimation"-Methode wird tatsächlich von der "CompositionObject"-Basisklasse geerbt, was bedeutet, dass Sie die Eigenschaften einer Vielzahl verschiedener Klassen animieren können. Dies ist eine weitere Abweichung von DirectComposition, bei der jede animierbare Eigenschaft Überladungen für Skalarwerte sowie Animationsobjekte bereitstellt. Die Windows-Komposition bietet ein wesentlich umfassenderes Eigenschaftssystem, das einigen sehr interessanten Funktionen die Tür öffnet. Insbesondere wird die Fähigkeit zum Schreiben von Textausdrücken unterstützt, mit denen die Codemenge reduziert wird, die für interessantere Animation und Effekte geschrieben werden muss. Diese Ausdrücke werden zur Laufzeit analysiert, kompiliert und dann vom Windows-Kompositionsmodul effizient ausgeführt.

Angenommen, Sie müssen ein visuelles Objekt entlang der Y-Achse drehen und ihm den Anschein von Tiefe geben. Die in Bogenmaßen gemessene "RotationAngle"-Eigenschaft des visuellen Objekts reicht nicht aus, da sie keine Transformation generiert, die die Perspektive einschließt. Während sich das visuelle Objekt dreht, sollt der dem menschlichen Auge am nächste Rand größer und der gegenüberliegende Rand kleiner erscheinen. Abbildung 8 zeigt verschiedene sich drehende visuelle Objekte, die dieses Verhalten veranschaulichen.

Sich drehende visuelle Objekte
Abbildung 8: Sich drehende visuelle Objekte

Wie lässt sich ein solcher animierter Effekt erreichen? Lassen Sie uns mit einer skalaren Keyframeanimation für den Drehwinkel beginnen:

ScalarKeyFrameAnimation animation = compositor.CreateScalarKeyFrameAnimation();
animation.InsertKeyFrame(1.0f, 2.0f * Math::Pi,
  compositor.CreateLinearEasingFunction());
animation.Duration(TimeSpan::FromSeconds(2));
animation.IterationBehavior(AnimationIterationBehavior::Forever);

Die lineare Beschleunigungsfunktion überschreibt die standardmäßige Beschleunigungs-/Verzögerungsfunktion, um eine kontinuierliche Drehbewegung zu erzeugen. Ich muss anschließend ein benutzerdefiniertes Objekt mit einer Eigenschaft definieren, auf die ich in einem Ausdruck verweisen kann. Der Compositor bietet einen Eigenschaftensatz für genau diesen Zweck:

CompositionPropertySet rotation = compositor.CreatePropertySet();
rotation.InsertScalar(L"Angle", 0.0f);

Ein Eigenschaftensatz ist auch ein Kompositionsobjekt. Deshalb kann ich die "StartAnimation"-Methode zum Animieren meiner benutzerdefinierten Eigenschaft wie bei einer beliebigen integrierten Eigenschaft verwenden:

rotation.StartAnimation(L"Angle", animation);

Ich habe nun ein Objekt, dessen "Angle"-Eigenschaft in Bewegung ist. Nun muss ich eine Transformationsmatrix zum Erzeugen des gewünschten Effekts definieren, während eine Delegierung zu dieser animierten Eigenschaft für den Drehwinkel selbst erfolgt. Jetzt kommen Ausdrücke ins Spiel:

ExpressionAnimation expression =
  compositor.CreateExpressionAnimation(
    L"pre * Matrix4x4.CreateFromAxisAngle(axis, rotation.Angle) * post");

Eine Ausdrucksanimation ist kein Keyframe-Animationsobjekt. Deshalb gibt es keine relativen Keyframeversätze, an denen sich Animationsvariablen (basierend auf einer Art von Interpolationsfunktion) ändern könnten. Stattdessen verweisen Ausdrücke einfach auf Parameter, die ggf. im herkömmlicheren Sinn selbst animiert werden. Dennoch liegt die Festlegung von "pre", "axis", "rotation" und "post" weiter bei mir. Beginnen wir mit dem "axis"-Parameter:

expression.SetVector3Parameter(L"axis", Vector3{ 0.0f, 1.0f, 0.0f });

Die "CreateFromAxisAngle"-Methode im Ausdruck erwartet eine Achse, um die die Drehung erfolgt, und damit wird die Achse um die Y-Achse definiert. Diese Methode erwartet außerdem einen Drehwinkel. Hierfür können wir dem Eigenschaftensatz "rotation" mit seiner animierten "Angle"-Eigenschaft den Vortritt lassen:

expression.SetReferenceParameter(L"rotation", rotation);

Um sicherzustellen, dass die Drehung durch die Mitte des visuellen Objekts anstatt den linken Rand erfolgt, muss ich die von "CreateFromAxisAngle" erstellte Drehmatrix mit einer Übersetzung vorab multiplizieren, die die Achse logisch zum Drehpunkt verschiebt:

expression.SetMatrix4x4Parameter(
  L"pre", Matrix4x4::Translation(-width / 2.0f, -height / 2.0f, 0.0f));

Beachten Sie, dass die Matrixmultiplikation nicht kommutativ ist, weshalb die "pre"- und "post"-Matrizen genau dies sind. Schließlich kann ich hinter der Drehmatrix eine Art von Perspektive hinzufügen und dann das visuelle Objekt an seinem ursprünglichen Ort wiederherstellen:

expression.SetMatrix4x4Parameter(
  L"post", Matrix4x4::PerspectiveProjection(width * 2.0f) *
    Matrix4x4::Translation(width / 2.0f, height / 2.0f, 0.0f));

Dies erfüllt alle Parameter, auf die der Ausdruck verweist, und ich kann nun einfach die Ausdrucksanimation nutzen, um das visuelle Objekt mithilfe seiner "TransformMatrix"-Eigenschaft zu animieren:

visual.StartAnimation(L"TransformMatrix", expression);

Bislang habe ich verschiedene Möglichkeiten zum Erstellen, Füllen und Animieren visueller Objekte untersucht. Doch was ist, wenn ich visuelle Objekte direkt rendern muss? DirectComposition bot sowohl vorab zugeordnete Oberflächen als auch spärlich zugeordnete Bitmaps (als virtuelle Oberflächen bezeichnet), die auf Anforderung zugeordnet wurden und deren Größe änderbar war. Die Windows-Komposition bietet anscheinend keine Möglichkeit zum Erstellen von Oberflächen. Es gibt eine "CompositionDrawingSurface"-Klasse, aber keine Möglichkeit, sie ohne externe Hilfe zu erstellen. Die Antwort liefert die Interoperabilitäts-API für die Windows-Komposition. WinRT-Klassen können zusätzliche COM-Schnittstellen implementieren, die nicht direkt sichtbar sind, wenn alles, was Sie haben, die Windows-Metadaten einer Komponente sind. Sofern Sie diese verdeckten Schnittstellen kennen, können Sie sie in C++ einfach abfragen. Dies bringt freilich einen größeren Aufwand mit sich, da Sie sich von den bequemen Abstraktionen entfernen, die normalen Entwicklern von der Windows-Kompositions-API zur Verfügung gestellt werden. Mein erster Schritt ist das Erstellen eines Rendergeräts. Ich verwende Direct3D 11, da die Windows-Komposition Direct3D 12 noch nicht unterstützt:

ComPtr<ID3D11Device> direct3dDevice;

Anschließend bereite ich die Geräterstellungsflags vor:

unsigned flags = D3D11_CREATE_DEVICE_BGRA_SUPPORT |
                 D3D11_CREATE_DEVICE_SINGLETHREADED;
#ifdef _DEBUG
flags |= D3D11_CREATE_DEVICE_DEBUG;
#endif

Die BGRA-Unterstützung erlaubt mir die Nutzung der zugänglicheren Direct2D-API für das Rendering mit diesem Gerät. Anschließend erstellt die "D3D11CreateDevice"-Funktion das Hardwaregerät selbst:

check(D3D11CreateDevice(nullptr, // Adapter
                        D3D_DRIVER_TYPE_HARDWARE,
                        nullptr, // Module
                        flags,
                        nullptr, 0, // Highest available feature level
                        D3D11_SDK_VERSION,
                        set(direct3dDevice),
                        nullptr, // Actual feature level
                        nullptr)); // Device context

Dann muss ich die DXGI-Schnittstelle des Geräts abfragen, da ich sie zum Erstellen eines Direct2D-Geräts benötige:

ComPtr<IDXGIDevice3> dxgiDevice = direct3dDevice.As<IDXGIDevice3>();

Nun kann das Direct2D-Gerät selbst erstellt werden:

ComPtr<ID2D1Device> direct2dDevice;

Hier aktiviere ich wiederum die Debugebene für eine zusätzliche Diagnose:

D2D1_CREATION_PROPERTIES properties = {};
#ifdef _DEBUG
properties.debugLevel = D2D1_DEBUG_LEVEL_INFORMATION;
#endif

Ich könnte zuerst eine Direct2D-Factory erstellen, um das Gerät zu erstellen. Dies wäre nützlich, wenn ich geräteunabhängige Ressourcen erstellen müsste. Hier verwende ich lediglich die Verknüpfung, die von der "D2D1­CreateDevice"-Funktion geboten wird:

check(D2D1CreateDevice(get(dxgiDevice), properties, set(direct2dDevice)));

Das Rendergerät ist bereit. Ich habe nun ein Direct2D-Gerät, mit dem ich rendern kann, was ich mir vorstellen kann. Nun muss ich das Windows-Kompositionsmodul über dieses Rendergerät informieren. Nun kommen die verdeckten Schnittstellen ins Spiel. Mithilfe des Compositors, den ich durchgängig verwendet habe, kann ich die "ICompositorInterop"-Schnittstelle abfragen:

namespace abi = ABI::Windows::UI::Composition;
ComPtr<abi::ICompositorInterop> compositorInterop;
check(compositor->QueryInterface(set(compositorInterop)));

"ICompositorInterop" bietet Methoden zum Erstellen einer Kompositionsoberfläche anhand einer DXGI-Oberfläche. Dies wäre bestimmt praktisch, wenn Sie eine vorhandene Swapchain in eine visuelle Kompositionsstruktur einschließen wollten. Doch "ICompositorInterop" bietet noch etwas, das wesentlich interessanter ist. Seine "CreateGraphicsDevice"-Methode erstellt ein "CompositionGraphicsDevice"-Objekt, sofern ein Rendergerät vorhanden ist. Die "CompositionGraphicsDevice"-Klasse ist eine normale Klasse in der Windows-Kompositions-API und keine verdeckte Schnittstelle. Sie bietet jedoch keinen Konstruktor, weshalb Sie C++ und die "ICompositorInterop"-Schnittstelle verwenden müssen, um einen zu erstellen:

CompositionGraphicsDevice device = nullptr;
check(compositorInterop->CreateGraphicsDevice(get(direct2dDevice), set(device)));

Da "CompositionGraphicsDevice" ein WinRT-Typ ist, kann ich wiederum Modern C++ anstelle von Zeigern und manueller Fehlerbehandlung einsetzen. Und es ist die "CompositionGraphicsDevice"-Klasse, die mir schließlich ermöglicht, eine Kompositionsoberfläche zu erstellen:

using namespace Windows::Graphics::DirectX;
CompositionDrawingSurface surface =
  compositionDevice.CreateDrawingSurface(Size{ 100, 100 },
    DirectXPixelFormat::B8G8R8A8UIntNormalized,
    CompositionAlphaMode::Premultiplied);

Hier erstelle ich eine Kompositionsoberfläche der Größe 100 x 100 Pixel. Beachten Sie, dass es sich hier um physische Pixel und nicht um logische und DPI-fähige Koordinaten handelt, die vom Rest der Windows-Komposition angenommen und bereitgestellt werden. Die Oberfläche bietet auch das von Direct2D unterstützte 32-Bit-Alpha-Blending-Rendering. Direct3D und Direct2D werden freilich noch nicht über die Windows-Runtime angeboten, weshalb es wiederum die verdeckten Schnittstellen sind, die diese Oberfläche tatsächlich zeichnen:

ComPtr<abi::ICompositionDrawingSurfaceInterop> surfaceInterop;
check(surface->QueryInterface(set(surfaceInterop)));

Fast genauso wie bei DirectComposition zuvor bietet die Windows-Komposition die Methoden "BeginDraw" und "EndDraw" für die "IComposition­DrawingSurfaceInterop"-Schnittstelle, die zusammenfassen und an die Stelle der typischen Aufrufe an die Direct2D-Methoden treten, die dieselben Namen haben:

ComPtr<ID2D1DeviceContext> dc;
POINT offset = {};
check(surfaceInterop->BeginDraw(nullptr, // Update rect
                                __uuidof(dc),
                                reinterpret_cast<void **>(set(dc)),
                                &offset));

Die Windows-Komposition verwendet das ursprüngliche Rendergerät, das zum Zeitpunkt der Erstellung des Kompositionsgerät erstellt wird, und verwendet es zum Erstellen eines Gerätekontexts oder Renderziels. Ich kann optional ein Clippingrechteck in physischen Pixeln bereitstellen, doch hier entscheide ich mich für einen uneingeschränkten Zugriff auf die Renderoberfläche. Die "BeginDraw"-Methode gibt ebenfalls einen Versatz zurück, wiederum in physischen Pixeln, um den Ursprung der vorgesehenen Zeichnungsoberfläche anzugeben. Dabei handelt es sich nicht unbedingt um die linke obere Ecke des Renderziels, und alle Zeichenbefehle müssen sorgfältig angepasst oder transformiert werden, damit sie den richtigen Versatz aufweisen. Rufen Sie wiederum "BeginDraw" nicht für das Renderziel auf, da dies bereits von der Windows-Komposition für Sie erledigt wurde. Das Renderziel ist im logischen Besitz der Kompositions-API, weshalb darauf geachtet werden muss, dass dies im Anschluss an den Aufruf von "EndDraw" nicht mehr der Fall ist. Das Renderziel ist nun startklar, doch es kennt nicht die logischen oder effektiven DPI-Werte für die Ansicht. Ich kann den "Windows::Graphics::Display"-Namespace zum Abrufen des logischen DPI-Werts für die aktuelle Ansicht nutzen und den DPI-Wert festlegen, der von Direct2D für das Rendering verwendet wird:

using namespace Windows::Graphics::Display;
DisplayInformation display = DisplayInformation::GetForCurrentView();
float const dpi = display.LogicalDpi();
dc->SetDpi(dpi, dpi);

Der letzte Schritt, bevor das Rendering beginnen kann, ist das Behandeln des Kompositionsversatzes. Eine einfache Lösung ist das Verwenden des Versatzes zum Generieren einer Transformationsmatrix. Erinnern Sie sich daran, dass Direct2D mit logischen Pixeln arbeitet, weshalb ich nicht nur den Versatz, sondern auch den neu festgelegten DPI-Wert verwenden muss:

dc->SetTransform(D2D1::Matrix3x2F::Translation(offset.x * 96.0f / dpi,
                                               offset.y * 96.0f / dpi));

An dieser Stelle können Sie nach Herzenslust zeichnen, bevor Sie die "EndDraw"-Methode für die Interoperabilitätsschnittstelle der Oberfläche aufrufen, um sicherzustellen, dass etwaige Direct2D-Batchzeichenbefehle verarbeitet und Änderungen an der Oberfläche in der visuellen Struktur der Komposition wiedergegeben werden:

check(surfaceInterop->EndDraw());

Freilich habe ich die Oberfläche noch keinem visuellen Objekt zugeordnet. Und wie bereits erwähnt, bieten visuelle Objekte keine "Content"-Eigenschaft mehr und müssen deshalb mit einem Pinsel gerendert werden. Glücklicherweise erstellt der Compositor einen Pinsel zum Darstellen einer vorher vorhandenen Oberfläche:

CompositionSurfaceBrush brush = compositor.CreateSurfaceBrush(surface);

Ich kann dann einen normalen Spritepinsel erstellen und mit diesem das visuelle Objekt als Licht bringen:

SpriteVisual visual = compositor.CreateSpriteVisual();
visual.Brush(brush);
visual.Size(Vector2{ ... });

Wenn Ihnen diese Interoperabilität noch nicht reicht, können Sie sogar ein XAML-Element verwenden und das zugrunde liegende visuelle Kompositionsobjekt abrufen. Hier ein Beispiel in C#:

using Windows.UI.Xaml.Hosting;
Visual visual = ElementCompositionPreview.GetElementVisual(button);

Trotz des scheinbar temporären Status ist "ElementCompositionPreview" bereit für die Produktion und kann von Apps verwendet werden, die an den Windows Store übermittelt wurden. Für ein angegebenes UI-Element gibt die statische "GetElementVisual"-Methode das visuelle Objekt aus der zugrunde liegenden visuellen Kompositionsstruktur zurück. Beachten Sie, dass "Visual" und nicht "ContainerVisual" oder "SpriteVisual zurückgegeben wird, weshalb Sie nicht direkt mit untergeordneten visuellen Objekten arbeiten oder einen Pinsel anwenden können. Doch Sie können viele von der Windows-Komposition gebotene visuelle Eigenschaften anpassen. Die "ElementCompositionPreview"-Hilfsklasse bietet einige zusätzliche statische Methoden zum kontrollierten Hinzufügen untergeordneter visueller Objekte. Sie können den Versatz des visuellen Objekts ändern, woraufhin Dinge wie UI-Treffertests auf XAML-Ebene weiter funktionieren. Sie können sogar eine Animation direkt mithilfe der Windows-Komposition anwenden, ohne die XAML-Infrastruktur zu stören, die darauf aufbaut. Lassen Sie uns eine einfache skalare Animation zum Drehen des Knopfs erstellen. Ich muss den Compositor aus dem visuellen Objekt abrufen und dann wie zuvor ein Animationsobjekt erstellen:

Compositor compositor = visual.Compositor;
ScalarKeyFrameAnimation animation = compositor.CreateScalarKeyFrameAnimation();

Lassen Sie uns eine einfache Animation erstellen, um den Knopf ununterbrochen mit einer linearen Beschleunigungsfunktion langsam zu drehen:

animation.InsertKeyFrame(1.0f, (float) (2 * Math.PI),
  compositor.CreateLinearEasingFunction());

Ich kann dann angeben, dass eine einzelne Drehung 3 Sekunden dauern und ununterbrochen fortgesetzt werden soll:

animation.Duration = TimeSpan.FromSeconds(3);
animation.IterationBehavior = AnimationIterationBehavior.Forever;

Schließlich kann ich die Animation einfach mit dem von XAML bereitgestellten visuellen Objekt verbinden und das Kompositionsmodul anweisen, seine "RotationAngle"-Eigenschaft zu animieren:

visual.StartAnimation("RotationAngle", animation);

Wenngleich Sie dies auch mit XAML bewerkstelligen könnten, bietet das Windows-Kompositionsmodul wesentlich mehr Leistung und Flexibilität angesichts der Tatsache, dass es sich auf einer viel niedrigeren Abstraktionsebene befindet. Als weiteres Beispiel stellt die Windows-Komposition Quaternion-Animationen bereit, die derzeit von XAML nicht unterstützt werden.

Wenn es um das Windows-Kompositionsmodul geht, mangelt es nicht an Themen. Meiner bescheidenen Meinung nach ist dies die bislang bahnbrechendste WinRT-API. Der Leistungsumfang, der Ihnen zur Verfügung steht, ist erstaunlich. Und dennoch bringt sie im Gegensatz zu so vielen anderen UI- und Grafik-APIs keine Leistungseinbußen eine zu bewältigende Lernkurve mit sich. In vielerlei Hinsicht ist die Windows-Komposition repräsentativ für alles, was an der Windows-Plattform gut und aufregend ist.

Sie können das Team hinter der Windows-Komposition auf Twitter erreichen: @WinComposition.


Kenny Kerrist Programmierer aus Kanada sowie Autor bei Pluralsight und Microsoft MVP. Er veröffentlicht Blogs unter kennykerr.ca, und Sie können ihm auf Twitter folgen: @kennykerr.

Unser Dank gilt den folgenden technischen Experten von Microsoft für die Durchsicht dieses Artikels: Mark Aldham, James Clarke, John Serna, Jeffrey Stall und Nick Waggoner