Freigeben über


Windows mit C++

High-Performance Window Layering mit dem Windows Kompositionsmodul

Kenny Kerr

Kenny KerrIch bin von überlappenden Fenstern fasziniert, seit sie mir das erste Mal in Windows XP begegnet sind. Die Möglichkeit, den rechteckigen – oder beinahe rechteckigen – Grenzen des traditionellen Desktop-Fensters zu entkommen, hat mich schon immer gefesselt. Und dann kam Windows Vista. Diese viel geschmähte Version von Windows deutete den Beginn von etwas an, das spannender und viel flexibler ist. Mit Windows Vista hat etwas angefangen, das wir erst jetzt mit Windows 8 so richtig schätzen lernen. Aber damit begann auch der Niedergang des überlappenden Fensters.

Mit Windows Vista wurde ein Dienst namens Desktopfenster-Manager eingeführt. Der Name war und ist irreführend. Stellen Sie sich diesen Dienst als Windows Kompositions- oder Gestaltungsmodul vor. Dieses Kompositionsmodul hat komplett die Art und Weise verändert, wie Anwendungsfenster auf dem Desktop gerendert werden. Statt jedem Fenster das direkte Rendern auf dem Monitor oder der Grafikkarte zu erlauben, geschieht das auf einer Oberfläche außerhalb des Bildschirms oder in einen Puffer. Das System weist eine solche Oberfläche pro oberstem Fenster zu und alle GDI-, Direct3D- und natürlich Direct2D-Grafiken werden auf diese Oberflächen gerendert. Diese Oberflächen außerhalb des Bildschirms werden als Umleitungsoberflächen bezeichnet, da die GDI-Zeichenbefehle und sogar die Direct3D-Swapchain-Präsentationsanforderungen (innerhalb der GPU) an die Umleitungsoberfläche weitergeleitet oder kopiert werden.

Irgendwann entscheidet das Kompositionsmodul unabhängig von irgendeinem bestimmten Fenster, dass es an der Zeit ist, den Desktop anhand der letzten Reihe von Änderungen zu gestalten. Das beinhaltet die gemeinsame Gestaltung sämtlicher Umleitungsoberflächen, bei der die Nicht-Clientbereiche (häufig auch Window Chrome genannt) und vielleicht Schatten und andere Effekte hinzugefügt werden. Das Endergebnis wird dann an die Grafikkarte weitergeleitet.

Dieser Kompositionsprozess hat viele wunderbare Vorteile, die ich in den nächsten Monaten erklären werde, wenn wir uns die Komposition in Windows genauer ansehen. Er hat aber auch eine potenziell schwerwiegende Einschränkung: Diese Umleitungsoberflächen sind nicht transparent. Die meisten Anwendungen sind damit recht zufrieden und das macht bestimmt viel Sinn im Hinblick auf die Performance, denn Alphablending ist teuer. Doch damit stehen überlappende Fenster im Regen.

Wenn ich ein überlappendes Fenster möchte, dann muss ich Einbußen bei der Performance in Kauf nehmen. Ich beschreibe die speziellen architektonischen Einschränkungen in meinem Artikel „Übereinander angeordnete Fenster mit Direct2D” (msdn.microsoft.com/magazine/ee819134). Zusammenfassend lässt sich also sagen, dass überlappende Fenster von der CPU hauptsächlich als Unterstützung für Treffertests von Pixeln mit Alphablending verarbeitet wird. Das heißt, die CPU braucht eine Kopie der Pixel, die den Oberflächenbereich des überlappenden Fensters ausmachen. Entweder rendere ich auf der CPU, was normalerweise deutlich langsamer als GPU-Rendering ist, oder ich mache das auf der GPU. In diesem Fall muss ich die „Bandbreitensteuer” in Kauf nehmen, denn alles, was gerendert wird, muss vom Videospeicher in den Systemspeicher kopiert werden. Im bereits erwähnten Artikel habe ich außerdem gezeigt, wie man das meiste aus Direct2D macht und soviel Leistung wie möglich aus dem System holt, denn nur mit Direct2D hat man die Wahl zwischen CPU- und GPU-Rendering. Der Clou ist, dass – obwohl sich das überlappende Fenster notwendigerweise im Systemspeicher befindet – das Kompositionsmodul es sofort in den Videospeicher kopiert, und zwar so, dass die tatsächliche Komposition des überlappenden Fensters immer noch hardwarebeschleunigt ist.

Auch wenn ich keine Hoffnungen auf die Rückkehr der traditionellen überlappenden Fenster machen kann, habe ich doch gute Nachrichten. Traditionelle überlappende Fenster haben zwei spezielle Eigenschaften, die interessant sind. Die erste ist Pro-Pixel-Alphablending. Was immer ich auch für das überlappende Fenster rendere, wird mit dem Schreibtisch alphageblendet sowie mit dem, was sich im jeweiligen Moment hinter dem Fenster befindet. Die zweite besteht darin, dass User32 Treffertests mit überlappenden Fenstern auf Grundlage von Pixel-Alphawerten durchführen kann. Damit können Mausmeldungen durchkommen, wenn das Pixel an an einem bestimmten Punkt transparent ist. Mit Windows 8 und 8.1 hat sich User32 nicht wesentlich verändert. Was sich aber verändert hat ist die Möglichkeit, Pro-Pixel-Alphablending rein über die CPU zu unterstützen und ohne die Kosten, die mit der Übertragung der Fensteroberfläche an den Systemspeicher verbunden ist. Damit kann ich jetzt den Effekt eines überlappenden Fensters ohne Leistungseinbußen verwirklichen, sofern ich keine Pro-Pixel-Treffertests brauche. Das gesamte Fenster wird gleichförmig einem Treffertest unterzogen. Mal abgesehen von den Treffertests bin ich davon begeistert, denn das ist etwas, dass das System offensichtlich kann. Es war einfach nur für Anwendungen nie möglich, das anzuzapfen. Wenn das für Sie verlockend klingt, dann lesen Sie weiter und ich zeige Ihnen, wie man das macht.

Der Schlüssel dafür liegt in der Nutzung des Kompositionsmoduls von Windows. Dieses Kompositionsmodul gab es erstmals in Windows Vista als Desktopfenster-Manager mit einer eingeschränkten API und dem beliebten Aero-Glaseffekt. Dann kam Windows 8 und damit die DirectComposition-API. Das ist eine umfassendere API für dasselbe Kompositionsmodul. Mit dem Release von Windows 8 erlaubte Microsoft Drittentwicklern schließlich, die Möglichkeiten dieses Moduls zu nutzen, die es schon so lange gab. Und natürlich braucht man dazu eine auf Direct3D basierende Grafik-API wie etwa Direct2D. Aber zuerst muss man sich um die nicht transparente Umleitungsoberfläche kümmern.

Wie ich bereits erwähnt habe, weist das System jedem Fenster auf oberster Ebene eine Umleitungsoberfläche zu. Ab Windows 8 können Sie nun ein Fenster auf oberster Ebene erstellen und festlegen, dass es ohne eine Umleitungsoberfläche angelegt wird. Genaugenommen hat das nichts mit überlappenden Fenstern zu tun, also verwenden Sie nicht den erweiterten Fensterstil WS_EX_LAYERED. (Die Unterstützung für überlappende Fenster hat tatsächlich in Windows 8 eine kleine Verbesserung erfahren, aber das sehen wir uns in einem künftigen Artikel genauer an.) Stattdessen setzen Sie den erweiterten Fensterstil WS_EX_NOREDIRECTIONBITMAP ein, der das Kompositionsmodul anweist, dem Fenster keine Umleitungsoberfläche zuzuweisen. Ich fange mit einem einfachen, traditionellen Desktop-Fenster an. Abbildung 1 zeigt ein Beispiel, bei dem man das Ausfüllen einer WINDCLASS-Struktur, die Registrierung der Fensterklasse, das Erstellen des Fenster und das Pumpen von Fenstermeldungen ausfüllt. Hier gibt es nichts Neues, diese Grundlagen sind weiterhin essentiell. Die Fenstervariable bleibt unbenutzt, aber die brauchen wir gleich. Man kann das in ein Visual C++-Projekt in Visual Studio kopieren oder es einfach aus der Eingabeaufforderung kompilieren wie folgt:

cl /W4 /nologo Sample.cpp

Abbildung 1: Erstellen eines traditionellen Fensters

#ifndef UNICODE #define UNICODE #endif #include <windows.h> #pragma comment(lib, "user32.lib") int __stdcall wWinMain(HINSTANCE module, HINSTANCE, PWSTR, int) { WNDCLASS wc = {}; wc.hCursor       = LoadCursor(nullptr, IDC_ARROW); wc.hInstance     = module; wc.lpszClassName = L"window"; wc.style         = CS_HREDRAW | CS_VREDRAW; wc.lpfnWndProc = [] (HWND window, UINT message, WPARAM wparam, LPARAM lparam) -> LRESULT { if (WM_DESTROY == message) { PostQuitMessage(0); return 0; } return DefWindowProc(window, message, wparam, lparam); }; RegisterClass(&wc); HWND const window = CreateWindow(wc.lpszClassName, L"Sample", WS_OVERLAPPEDWINDOW | WS_VISIBLE, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, nullptr, nullptr, module, nullptr); MSG message; while (BOOL result = GetMessage(&message, 0, 0, 0)) { if (-1 != result) DispatchMessage(&message); } }

Abbildung 2 zeigt, wie das auf meinem Desktop aussieht. Wie Sie sehen, gibt es hier nichts Außergewöhnliches. Im Beispiel gibt es keine Pixelbild- und Renderbefehle, der Clientbereich des Fensters ist nicht transparent und das Kompositionsmodul fügt Nicht-Clientbereich, Rahmen und Titelleiste hinzu. Den erweiterten Fensterstil WS_EX_NOREDIRECTIONBITMAP einzusetzen, um damit die nicht transparente Umleitungsoberfläche loszuwerden, die diesen Clientbereich darstellt, geht ganz einfach, indem man die Funktion CreateWindow für die Funktion CreateWindowEx mit dem führenden Parameter ausschaltet, der erweiterte Fensterstile akzeptiert:

 

HWND const window = CreateWindowEx(WS_EX_NOREDIRECTIONBITMAP, wc.lpszClassName, L"Sample", WS_OVERLAPPEDWINDOW | WS_VISIBLE, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, nullptr, nullptr, module, nullptr);

A Traditional Window on the Desktop
Abbildung 2: Ein traditionelles Fenster auf dem Desktop

Es haben sich nur folgende Dinge verändert: Das führende Argument wurde hinzugefügt, der erweiterte Fensterstil WS_EX_NOREDIRECTIONBITMAP und natürlich der Einsatz der Funktion CreateWindowEx statt der einfacheren Funktion CreateWindow. Die Ergebnisse auf dem Desktop sind allerdings deutlich radikaler. Abbildung 3 zeigt, wie das auf meinem Desktop aussieht. Beachten Sie, dass der Clientbereich des Fensters jetzt komplett transparent ist. Das Bewegen des Fensters illustriert diese Tatsache. Ich kann sogar ein Video im Hintergrund abspielen und es wird nicht im geringsten verdeckt. Andererseits wird das gesamte Fenster gleichförmig per Treffertest verarbeitet und der Fensterfokus geht beim Klicken im Clientbereich nicht verloren. Das liegt daran, dass das für die Treffertests verantwortliche Subsystem und die Mauseingabe nicht wissen, dass der Clientbereich transparent ist.

A Window Without a Redirection Surface
Abbildung 3: Fenster ohne Umleitungsoberfläche

Natürlich ist die nächste Frage: Wie kann man denn dann irgendetwas im Fenster rendern, wenn es keine Umleitungsoberfläche gibt, die an das Kompositionsmodul liefert? Die Antwort kommt aus der DirectComposition-API und seiner tiefen Integration in die DirectX Graphics Infrastructure (DXGI). Es ist dieselbe Technik, die hinter der Windows 8.1 XAML-Implementierung steht und eine unglaublich leistungsstarke Komposition der Inhalte in einer XAML-Anwendung ermöglicht. Das Trident-Renderingmodul des Internet Explorers nutzt DirectComposition ebenfalls in großem Umfang für die Verschiebung mit den Fingern und das Zoomen sowie CSS3-Animationen, Übergänge und Transformationen.

Ich verwende es einfach zum Erstellen einer Swapchain, die Transparenz mit vormultiplizierten Alphawerten auf einer Pro-Pixel-Basis unterstützt und mische es mit dem Rest des Desktops. Traditionelle DirectX-Anwendungen erstellen typischerweise eine Swapchain mit der Methode CreateSwapChainForHwnd der DXGI-Factory. Hinter dieser Swapchain steht ein Paar oder eine Reihe von Puffern, die im Endeffekt während der Präsentation ausgetauscht werden, was es der Anwendung ermöglicht, den nächsten Frame zu rendern, während der vorherige kopiert wird. Die Swapchain-Oberfläche, auf die die Anwendung rendert, ist ein nicht transparenter Puffer außerhalb des Bildschirms. Wenn die Anwendung die Swapchain präsentiert, kopiert DirectX die Inhalte aus dem Hintergrundpuffer der Swapchain in die Umleitungsoberfläche des Fensters. Wie bereits erwähnt vereint das Kompositionsmodul schließlich alle Umleitungsoberflächen und produziert den Desktop als Ganzes.

In diesem Fall besitzt das Fenster keine Umleitungsoberfläche, sodass sich die Methode CreateSwapChainForHwnd der DXGI-Factory nicht anwenden lässt. Ich brauche allerdings dennoch eine Swapchain, damit Direct3D- und Direct2D-Rendering unterstützt wird. Dafür gibt es die Methode CreateSwapChainForComposition der DXGI-Factory. Ich kann diese Methode einsetzen und eine fensterlose Swapchain zusammen mit deren Puffer erstellen. Doch deren Präsentation kopiert nicht die Bits zur Umleitungsoberfläche (die nicht existiert), sondern stellt sie stattdessen direkt dem Kompositionsmodul zur Verfügung. Das Kompositionsmodul kann diese Oberfläche dann verwenden und sie direkt und anstatt der Umleitungsoberfläche des Fensters verwenden. Weil diese Oberfläche nicht transparent ist, sondern sein Pixelformat stattdessen die pro Pixel vormultiplizierten Alphawerte voll unterstützt, ist das Ergebnis ein auf den Pixel genaues Alphablending auf dem Desktop. Das ist außerdem unglaublich schnell, denn es findet kein unnötiges Kopieren innerhalb der GPU statt und sicher auch keine Kopien über den Bus in den Systemspeicher.

Soviel die Theorie. Jetzt wollen wir das mal praktisch umsetzen. Bei DirectX dreht sich alles um die wesentlichen Elemente von COM, weshalb ich die Klassenvorlage ComPtr C++-Vorlagenbibliothek für Windows-Runtime zur Verwaltung der Schnittstellenzeiger verwende. Ich muss auch die APIs von Direct3D, Direct2D und DirectComposition einschließen und auf sie verlinken. Der folgende Code zeigt die entsprechende Vorgehensweise:

#include <wrl.h> using namespace Microsoft::WRL; #include <dxgi1_3.h> #include <d3d11_2.h> #include <d2d1_2.h> #include <d2d1_2helper.h> #include <dcomp.h> #pragma comment(lib, "dxgi") #pragma comment(lib, "d3d11") #pragma comment(lib, "d2d1") #pragma comment(lib, "dcomp")

Normalerweise baue ich sie in meine vorkompilierte Headerdatei ein. In diesem Fall lasse ich die using-Direktive weg und füge sie nur in die Quelldatei der Anwendung ein.

Ich hasse Codebeispiele, bei denen die Fehlerbehandlung die Details des eigentlichen Themas begräbt und davon ablenkt. Also packen wir das mit einer Ausnahmeklasse weg und einer HR-Funktion für die Fehlerüberprüfung. Eine einfach Implementierung finden Sei in Abbildung 4, aber natürlich können Sie selbst Ihre Richtlinien für die Fehlerbehandlung festlegen.

Abbildung 4: HRESULT-Fehler in Ausnahmen umwandeln

struct ComException { HRESULT result; ComException(HRESULT const value) : result(value) {} }; void HR(HRESULT const result) { if (S_OK != result) { throw ComException(result); } }

Jetzt kann ich anfangen, den Renderingstapel zusammenzustellen, und das beginnt naturgemäß mit einem Direct3D-Gerät. Das behandle ich nur ganz kurz, denn ich habe die DirectX-Infrastruktur bereits in meinem Artikel vom März 2013 beschrieben, „Einführung in Direct2D 1.1” (msdn.microsoft.com/magazine/dn198239). Hier kommt der Direct3D 11-Schnittstellenzeiger:

ComPtr<ID3D11Device> direct3dDevice;

Das ist der Schnittstellenzeiger für das Gerät und die D3D11Create-Gerätefunktion kann für das Erstellen des Geräts verwendet werden:

HR(D3D11CreateDevice(nullptr,    // Adapter D3D_DRIVER_TYPE_HARDWARE, nullptr,    // Module D3D11_CREATE_DEVICE_BGRA_SUPPORT, nullptr, 0, // Highest available feature level D3D11_SDK_VERSION, &direct3dDevice, nullptr,    // Actual feature level nullptr));  // Device context

Im Prinzip ist hier nichts sonderlich überraschend. Ich erstelle ein Direct3D-Gerät, das von einer GPU unterstützt wird. Das Kennzeichen D3D11_CREATE_DEVICE_BGRA_SUPPORT ermöglicht die Interoperabilität mit Direct2D. Die DirectX-Familie wird mithilfe von DXGI zusammengehalten, das eine gemeinsame Einrichtung für die Verwaltung von GPU-Ressourcen in den verschiedenen DirectX-APIs bietet. Deshalb muss ich das Direct3D-Gerät im Hinblick auf seine DXGI-Schnittstelle abfragen:

ComPtr<IDXGIDevice> dxgiDevice; HR(direct3dDevice.As(&dxgiDevice));

Die Methode ComPtr As ist einfach ein Wrapper für die Methode QueryInterface. Mit dem erstellten Direct3D-Gerät kann ich dann die Swapchain anlegen, die für die Komposition verwendet wird. Dafür benötige ich erst einmal den Zugriff auf die DXGI-Factory:

ComPtr<IDXGIFactory2> dxFactory; HR(CreateDXGIFactory2( DXGI_CREATE_FACTORY_DEBUG, __uuidof(dxFactory), reinterpret_cast<void **>(dxFactory.GetAddressOf())));

Hier entscheide ich mich für zusätzliche Debuginformationen – eine unschätzbare Hilfe während der Entwicklung. Der schwierigste Teil beim Erstellen einer Swapchain besteht darin, herauszufinden, wie man die gewünschte Swapchain der DXGI-Factory gegenüber beschreibt. Diese Debuginformationen ist extrem hilfreich beim Feinabstimmung der nötigen DXGI_SWAP_CHAIN_DESC1-Struktur:

DXGI_SWAP_CHAIN_DESC1 description = {};

Damit wird die Struktur initialisiert und auf Null gesetzt. Nun kann ich damit anfangen, interessante Eigenschaft einzusetzen:

description.Format           = DXGI_FORMAT_B8G8R8A8_UNORM; description.BufferUsage      = DXGI_USAGE_RENDER_TARGET_OUTPUT; description.SwapEffect       = DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL; description.BufferCount      = 2; description.SampleDesc.Count = 1; description.AlphaMode        = DXGI_ALPHA_MODE_PREMULTIPLIED;

Das besondere Format, ein 32-Bit-Pixelformat mit 8 Bit für jeden Farbkanal sowie einer vormultiplizierten 8-Bit-Alphakomponente ist nicht die einzige Option, bringt aber geräte- und API-übergreifend die beste Leistung und Kompatibilität.

Die Puffernutzung durch die Swapchain muss festgelegt werden, damit die Ausgabe des Renderziels dorthin weitergeleitet werden kann. Das ist nötig, damit der Direct2D-Gerätekontext ein Bitmap erstellen kann, mit dem die DXGI-Oberfläche mit Zeichenbefehlen angesprochen werden kann. Die Direct2D-Bitmap selbst ist lediglich eine Abstraktion, die von der Swapchain unterstützt wird.

Kompositionsswapchains unterstützen nur den Swapeffekt flip-sequential. So steht die Swapchain ohne eine Umleitungsoberfläche mit dem Kompositionsmodul in Verbindung. Im Flip-Modell werden alle Puffer direkt mit dem Kompositionsmodul geteilt. Das Kompositionsmodul kann dann direkt den Desktop aus dem Hintergrundpuffer der Swapchain erstellen, ganz ohne zusätzliches Kopieren. Das ist normalerweise das effizienteste Modell. Es ist auch für die Komposition erforderlich, deshalb verwende ich es. Das Flip-Modell erfordert auch mindestens zwei Puffer, unterstützt aber kein Multisampling. BufferCount wird also auf zwei gesetzt und SampleDesc.Count auf eins. Diese Anzahl ist die Anzahl der Multisamples pro Pixel. Wird sie auf eins gesetzt, dann ist damit im Endeffekt das Multisampling deaktiviert.

Schließlich ist der Alphamodus entscheidend. Für nicht transparente Swapchains würde das normalerweise ignoriert, doch in diesem Fall möchte ich wirklich, dass das Transparenzverhalten enthalten ist. Vormultiplizierte Alphawerte liefern typischerweise die beste Leistung, und das ist auch die einzige Option, die vom Flip-Modell unterstützt wird.

Als letzte Zutat vor dem Erstellen der Swapchain muss ich schließlich die gewünschte Größe des Puffer festlegen. Normalerweise kann ich beim Aufrufen der Methode CreateSwapChainForHwnd die Größe ignorieren und die DXGI-Factory fragt das Fenster nach der Größe des Clientbereichs ab. In diesem Fall weiß DXGI nicht, was ich mit der Swapchain vorhabe, also muss ich ihm genau die Größe vorgeben. Nachdem das Fenster erstellt wurde, geht es einfach nur noch darum, den Clientbereich des Fensters abzufragen und die Swapchain-Beschreibung entsprechend zu aktualisieren:

RECT rect = {}; GetClientRect(window, &rect); description.Width  = rect.right - rect.left; description.Height = rect.bottom - rect.top;

Jetzt kann ich die Kompositions-Swapchain mit dieser Beschreibung erstellen und einen Zeiger auf das Direct3D-Gerät anlegen. Es können entweder die Schnittstellenzeiger von Direct3D oder DXGI verwendet werden:

ComPtr<IDXGISwapChain1> swapChain; HR(dxFactory->CreateSwapChainForComposition(dxgiDevice.Get(), &description, nullptr, // Don’t restrict swapChain.GetAddressOf()));

Nachdem nun die Swapchain erstellt wurde, kann ich jeden beliebigen Direct3D- oder Direct2D-Grafik-Rendering-Code für das Zeichnen der Anwendung verwenden und dabei Alphawerte nach Bedarf für das Erstellen der gewünschten Transparenz verwenden. Es gibt hier nichts neues, deswegen verweise ich noch einmal auf meinen Artikel vom März 2013 wegen der Details zum Rendern einer Swapchain mit Direct2D. Abbildung 5 zeigt im Anschluss ein einfaches Beispiel. Vergessen Sie aber nicht die Unterstützung von DPI pro Monitor, wie ich Sie in meinem Artikel vom Februar 2014 beschrieben habe, „Erstellung von High-DPI-Apps für Windows 8.1” (msdn.microsoft.com/magazine/dn574798).

Abbildung 5: Zur Swapchain zeichnen mit Direct2D

// Direct2D-Factory mit Singlethread mit Debuginformationen erstellen ComPtr<ID2D1Factory2> d2Factory; D2D1_FACTORY_OPTIONS const options = { D2D1_DEBUG_LEVEL_INFORMATION }; HR(D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, options, d2Factory.GetAddressOf())); // Das Direct2D-Gerät erstellen, das mit dem Direct3D-Gerät verknüpft ist ComPtr<ID2D1Device1> d2Device; HR(d2Factory->CreateDevice(dxgiDevice.Get(), d2Device.GetAddressOf())); // Den Direct2D-Gerätekontext erstellen, der das eigentliche Renderziel ist  // und die Zeichenbefehle aufdecken  ComPtr<ID2D1DeviceContext> dc; HR(d2Device->CreateDeviceContext(D2D1_DEVICE_CONTEXT_OPTIONS_NONE, dc.GetAddressOf())); // Den Hintergrundpuffer der Swapchain abrufen ComPtr<IDXGISurface2> surface; HR(swapChain->GetBuffer( 0, // index __uuidof(surface), reinterpret_cast<void **>(surface.GetAddressOf()))); // Ein Direct2D-Bitmap erstellen, das auf die Swapchain-Oberfläche zeigt D2D1_BITMAP_PROPERTIES1 properties = {}; properties.pixelFormat.alphaMode = D2D1_ALPHA_MODE_PREMULTIPLIED; properties.pixelFormat.format    = DXGI_FORMAT_B8G8R8A8_UNORM; properties.bitmapOptions         = D2D1_BITMAP_OPTIONS_TARGET | D2D1_BITMAP_OPTIONS_CANNOT_DRAW; ComPtr<ID2D1Bitmap1> bitmap; HR(dc->CreateBitmapFromDxgiSurface(surface.Get(), properties, bitmap.GetAddressOf())); // Den Gerätekontext auf die Bitmap für das Rendering verweisen dc->SetTarget(bitmap.Get()); // Etwas zeichnen dc->BeginDraw(); dc->Clear(); ComPtr<ID2D1SolidColorBrush> brush; D2D1_COLOR_F const brushColor = D2D1::ColorF(0.18f,  // red 0.55f,  // green 0.34f,  // blue 0.75f); // alpha HR(dc->CreateSolidColorBrush(brushColor, brush.GetAddressOf())); D2D1_POINT_2F const ellipseCenter = D2D1::Point2F(150.0f,  // x 150.0f); // y D2D1_ELLIPSE const ellipse = D2D1::Ellipse(ellipseCenter, 100.0f,  // x radius 100.0f); // y radius dc->FillEllipse(ellipse, brush.Get()); HR(dc->EndDraw()); // Die Swapchain für das Kompositionsmodul verfügbar machen HR(swapChain->Present(1,   // sync 0)); // flags

Jetzt kann ich endlich damit beginnen, die DirectComposition-API einzusetzen und alles zusammenzubringen. Während das Kompositionsmodul von Windows mit Rendering und Komposition des Desktops als Ganzem betraut ist, ermöglicht die DirectComposition-API die Nutzung derselben Technologie für die Komposition der visuellen Objekte für Ihre Anwendungen. Anwendungen erstellen zusammen unterschiedliche Elemente, die sogenannten visuellen Objekte, und erzeugen damit das Erscheinungsbild des eigentlichen Anwendungsfensters. Diese visuellen Objekte lassen sich auf vielfältige Weise animieren und transformieren, was attraktive und flüssige UIs ermöglicht. Der Kompositionsprozess selbst wird ebenfalls mit der Komposition des Desktops als Ganzem durchgeführt. Es wird also noch mehr von der Präsentation der Anwendung aus dem Anwendungsthread genommen, was für bessere Reaktionsfähigkeit sorgt.

Bei DirectComposition geht es hauptsächlich darum, unterschiedliche Bitmaps miteinander zu kombinieren. Wie bei Direct2D ist das Konzept einer Bitmap hier eher eine Abstraktion, sodass die verschiedenen Renderingstapel kooperieren und reibungslose und ansprechende UX's produzieren.

Wie Direct3D und Direct2D ist DirectComposition eine DirectX-API, die von der GPU unterstützt und angetrieben wird. Ein DirectComposition-Gerät wird durch das Verweisen auf das Direct3D-Gerät erstellt. Das geschieht weitgehend auf dieselbe Art und Weise, wie ein Direct2D- auf das zugrundeliegende Direct3D-Gerät verweist. Ich verwende dasselbe Direct3D-Gerät, das ich schon vorher für das Erstellen der Swapchain eingesetzt habe, sowie das Direct2D-Renderziel, und erstelle ein DirectComposition-Gerät:

ComPtr<IDCompositionDevice> dcompDevice; HR(DCompositionCreateDevice( dxgiDevice.Get(), __uuidof(dcompDevice), reinterpret_cast<void **>(dcompDevice.GetAddressOf())));

Die Funktion DCompositionCreateDevice erwartet die DXGI-Schnittstelle des Direct3D-Geräts und gibt einen IDCompositionDevice-Schnittstellenzeiger an das neu erstellte DirectComposition-Gerät aus. Das DirectComposition-Gerät fungiert als Factory für die anderen DirectComposition-Objekte und stellt die hochwichtige Commit-Methode bereit, die den Batch mit den Renderbefehlen an das Kompositionsmodul übergibt.

Als nächstes muss ich Kompositionsziel anlegen, das die visuellen Objekte verbindet, die mit dem Ziel erstellt werden; das ist das Anwendungsfenster:

ComPtr<IDCompositionTarget> target; HR(dcompDevice->CreateTargetForHwnd(window, true, // Oberste Ebene target.GetAddressOf()));

Der erste Parameter der Methode CreateTargetForHwnd ist das Fensterhandle, das von der Funktion CreateWindowEx ausgegeben wird. Der zweite Parameter gibt an, wie die visuellen Objekte mit anderen Fensterelementen kombiniert werden. Das Ergebnis ist ein IDCompositionTarget-Schnittstellenzeiger, dessen einzige Methode SetRoot genannt wird. Damit kann ich die visuellen Stammobjekte in einer möglichen Baumstruktur visueller Objekte festlegen, die zusammen erstellt werden. Ich brauche keinen komplette visuelle Struktur, aber wenigstens ein visuelles Objekt, und dafür kann ich noch einmal auf das DirectComposition-Gerät zurückgreifen:

ComPtr<IDCompositionVisual> visual; HR(dcompDevice->CreateVisual(visual.GetAddressOf()));

Das visuelle Objekt enthält eine Referenz zu einer Bitmap und liefert ein Set von Eigenschaften, die sich auf das Rendering des visuellen Objekts sowie seine Komposition im Verhältnis zu anderen visuellen Objekten in der Baumstruktur und dem Ziel selbst auswirken. Ich habe bereits den Inhalt, den dieses visuelle Objekt an das Kompositionsmodul weiterleiten soll. Das ist die Swapchain, die ich vorher erstellt habe:

HR(visual->SetContent(swapChain.Get()));

Das visuelle Objekt ist bereit und ich kann es einfach als Stamm des Kompositionsziel festlegen:

HR(target->SetRoot(visual.Get()));

Schließlich kann ich, nachdem die Form der visuelle Struktur festgelegt ist, einfach das Kompositionsmodul informieren, dass ich fertig bin. Das tue ich mit dem Commit-Methode auf dem DirectComposition-Gerät:

HR(dcompDevice->Commit());

Bei dieser speziellen Anwendung, bei der sich die visuelle Struktur nicht verändert, muss ich Commit nur einmal zu Beginn der Anwendung aufrufen und dann nie mehr. Ich war ursprünglich davon ausgegangen, dass die Commit-Methode nach der Präsentation der Swapchain aufgerufen werden müsste. Aber das ist nicht der Fall, denn die Swapchain-Präsentation wird nicht mit den Änderungen an der visuellen Struktur synchronisiert.

Abbildung 6 zeigt, wie das Anwendungsfenster jetzt aussieht, das Direct2D zur Swapchain gerendert hat. DirectComposition hat die teilweise transparente Swapchain dem Kompositionsmodul bereitgestellt.

Direct2D Drawing on a DirectComposition Surface
Abbildung 6: Direct2D-Zeichnung auf einer DirectComposition-Oberfläche

Mich begeistert, dass ich endlich eine Lösung für ein altes Problem habe: Die Möglichkeit, leistungsstarke Fenster zu produzieren, die Alphablending mit dem Rest des Desktops verwenden. Und genauso freue ich mich über die Möglichkeiten, die sich aus der DirectComposition-API ergeben und was das für das künftige UX-Design von Anwendung und der Entwicklung in systemeigenem Code bedeutet.

Sie möchten Ihr eigenes Window Chrome zeichnen? Kein Problem. Ersetzen Sie einfach den Fensterstil WS_OVERLAPPEDWINDOW durch WS_POPUP beim Erstellen des Fensters. Viel Spaß beim Programmieren!

Kenny Kerr ist Programmierer aus Kanada sowie Autor bei Pluralsight und Microsoft MVP. Er veröffentlicht Blogs unter kennykerr.ca, und Sie können ihm auf Twitter unter twitter.com/kennykerr folgen.

Unser Dank gilt dem folgenden technischen Experten für die Durchsicht dieses Artikels: Leonardo Blanco (Microsoft)