Compartir a través de



Edición especial de Windows 10 de 2015

Volumen 30, número 11

Gráficos y animación: La nueva composición para Windows 10

Por Kenny Kerr | Windows 2015

El motor de composición de Windows, también conocido como Administrador de ventanas de escritorio (DWM), tiene una API nueva para Windows 10. DirectComposition fue la interfaz principal para la composición, pero, como API COM clásica, resultaba muy inaccesible para la mayoría de desarrolladores de aplicaciones. La nueva API de composición de Windows se basa en Windows Runtime (WinRT) y proporciona una base para la representación de alto rendimiento, ya que combina el mundo de los gráficos de modo inmediato que ofrecen Direct2D y Direct3D con un árbol visual retenido que ahora admite unas funcionalidades de efectos y animación mejoradas.

La primera vez que escribí sobre DWM fue en el año 2006, cuando Windows Vista estaba en fase beta (goo.gl/19jCyR). Permitía controlar el alcance del efecto de desenfoque para una venta concreta y crear un cromo personalizado fácil de combinar con el escritorio. La Ilustración 1 muestra el alcance de este logro en Windows 7. Era posible producir una representación con aceleración de hardware con Direct3D y Direct2D para crear objetos visuales increíbles para la aplicación (goo.gl/IufcN1). Incluso se podía combinar el antiguo mundo de controles GDI y USER con DWM (goo.gl/9ITISE). Sin embargo, cualquier atento observador podía darse cuenta de que DWM podía ofrecer más. Mucho más. La característica Windows Flip 3D de Windows 7 fue una prueba convincente.

Windows Aero
Ilustración 1 Windows Aero

Windows 8 incorporaba una API nueva para DWM llamada DirectComposition, un nombre dedicado a la familia DirectX de API COM clásicas que inspiró su diseño. DirectComposition empezó a ofrecer a los diseñadores una imagen más clara de lo que podía hacer DWM. También ofrecía una terminología mejorada. En realidad, DWM es el motor de composición de Windows y podía producir efectos deslumbrantes en Windows Vista y Windows 7 porque, básicamente, cambió la forma de representar las ventanas de los equipos de escritorio. De forma predeterminada, el motor de composición creaba una superficie de redirección para cada ventana de nivel superior. Lo describí detalladamente en mi columna de junio de 2014 (goo.gl/oMlVa4). Estas superficies de redirección formaban parte de un árbol visual, y DirectComposition permitía a las aplicaciones usar esta misma tecnología para proporcionar una API de modo retenido ligera para gráficos de alto rendimiento. DirectComposition ofrecía una administración de superficie y árbol visual que permitía a la aplicación descargar la producción de efectos y animaciones en el motor de composición. Describí esta funcionalidad en mis columnas de agosto (goo.gl/CNwnWR) y septiembre de 2014 (goo.gl/y7ZMLL). Incluso produje un curso sobre representación de alto rendimiento con DirectComposition para Pluralsight (goo.gl/fgg0XN).

Windows 8 debutó con DirectComposition, junto con las impresionantes mejoras del resto de la familia de API de DirectX, pero también supuso el inicio de una nueva era para la API de Windows que cambiaría para siempre la forma de ver los SO por parte de los desarrolladores. La incorporación de Windows Runtime eclipsó todo lo demás. Microsoft prometió una forma nueva y original de crear aplicaciones y acceder a los servicios de los SO que aceleró la retirada final de la denominada API Win32 que fue, durante mucho tiempo, la forma dominante de crear aplicaciones e interactuar con el SO. Windows 8 tuvo un comienzo difícil, pero 8.1 corrigió muchos problemas y, ahora, Windows 10 proporciona una API mucho más completa que satisfará a muchos más desarrolladores interesados en crear apps avanzadas, no, aplicaciones serias para Windows.

Windows 10 se publicó en julio de 2015 con una vista previa de la API de composición que aún no estaba lista para la producción. Seguía sujeta a cambios y, por lo tanto, no se podía usar en las aplicaciones universales de Windows que se enviaban a la Tienda Windows. Esto también se debe a que la API de composición que ahora está disponible para la producción ha cambiado significativamente y es mejor. Con esta actualización de Windows 10, es la primera vez que la misma API de composición está disponible para todos los factores de forma, lo que otorga mayor credibilidad a la parte universal de la Plataforma universal de Windows. La composición funciona de la misma forma, tanto si se orienta a su escritorio multipantalla como si lo hace al pequeño smartphone que lleva en el bolsillo.

Naturalmente, lo que gusta a todo el mundo de Windows Runtime es que, finalmente, cumple la promesa de un Common Language Runtime para Windows. Si prefiere programar en C#, puede usar Windows Runtime directamente a través de la compatibilidad integrada en Microsoft .NET Framework. Si, como yo, prefiere usar C++, puede usar Windows Runtime sin intermediarios ni caras abstracciones. Windows Runtime se basa en COM, en lugar de .NET, y, por tanto, resulta ideal para usarlo con C++. Usaré C++ moderno para Windows Runtime (moderncpp.com), la proyección estándar del lenguaje C++, pero puede usar el lenguaje que prefiera, ya que la API es la misma en cualquier caso. Incluso ofreceré algunos ejemplos en C# para mostrar que Windows Runtime puede admitir distintos lenguajes fácilmente.

La API de composición de Windows se distancia de sus raíces de DirectX. Mientras DirectComposition proporcionaba un objeto de dispositivo, modelado después de los dispositivos Direct3D y Direct2D, la nueva API de composición de Windows empieza con un compositor. Sin embargo, cumple el mismo objetivo: actuar como fábrica para los recursos de composición. Más allá de esto, la composición de Windows es muy parecida a DirectComposition. Hay un objetivo de composición que representa la relación entre una ventana y su árbol visual. Las diferencias se hacen más evidentes al observar de cerca los objetos visuales. Un objeto visual de DirectComposition tenía una propiedad de contenido que proporcionaba un mapa de bits de algún tipo. El mapa de bits era una de estas tres cosas: una superficie de composición, una cadena de intercambio DXGI o la superficie de redirección de otra ventana. Una aplicación típica de DirectComposition constaba de objetos visuales y superficies, con superficies que actuaban como contenido o mapas de bits para distintos objetos visuales. Como se muestra en la Ilustración 2, un árbol visual de composición es otra cosa ligeramente distinta. El nuevo objeto visual no tiene ninguna propiedad de contenido y se representa con un pincel de composición. Esto resulta ser una abstracción más flexible. Aunque el pincel puede representar un mapa de bits como antes, se pueden crear pinceles de colores sólidos de una forma muy eficiente y se pueden definir pinceles más elaborados, al menos conceptualmente, del mismo modo que Direct2D proporciona efectos que se pueden tratar como imágenes. Una abstracción correcta marca la diferencia.

Árbol visual de composición de Windows
Ilustración 2 Árbol visual de composición de Windows

Veamos algunos ejemplos prácticos para mostrar cómo funciona y darle una idea de lo que puede hacer. De nuevo, puede elegir su proyección de lenguaje WinRT favorito. Puede crear un compositor con C++ moderno, como se muestra a continuación:

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

Del mismo modo, puede hacer lo mismo con C#:

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

Incluso puede usar la vistosa sintaxis que ofrecen C++ o CX:

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

Todos son equivalentes desde la perspectiva de la API y solo reflejan las diferencias en la proyección de lenguaje. Básicamente, hay dos formas de crear una aplicación de Windows universal hoy en día. Puede que el enfoque más común sea usar el espacio de nombres Windows.UI.Xaml de SO. Si XAML no es importante para su aplicación, también puede usar el modelo de aplicación subyacente directamente, sin dependencia de XAML. Describí el modelo de aplicaciones WinRT en mi columna de agosto de 2013 (goo.gl/GI3OKP). Si usa este enfoque, solo necesita una implementación mínima de las interfaces IFrameworkView y IFrameworkViewSource para empezar. La Ilustración 3 proporciona un esquema básico en C# que puede usar para empezar. La composición de Windows también ofrece una sólida integración con XAML, pero, para empezar, usemos una aplicación sencilla sin XAML que nos proporcione un escenario sencillo en el que aprender sobre la composición. Volveré a XAML más adelante.

Ilustración 3 Modelo de aplicaciones de Windows Runtime en 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() { }
}

El compositor se debe construir dentro del método SetWindow (consulte la Ilustración 3) de la aplicación. De hecho, es el primer punto del ciclo de vida de la aplicación en que puede ocurrir, ya que el compositor depende del distribuidor de la ventana y este es el punto en que tanto la ventana como el distribuidor coexisten. A continuación, se puede crear un destino de composición para establecer la relación entre el compositor y la vista de aplicación:

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

es fundamental que la aplicación conserve el destino de composición, por lo que debe asegurarse de convertirlo en variable miembro de la implementación de IFrameworkView. Como ya he mencionado, el destino de composición representa la relación entre la ventana o vista y su árbol visual. Lo único que puede hacer con un destino de composición es definir el objeto visual raíz. Normalmente, se trata de un objeto visual de contenedor:

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

Aquí he usado C++, que no tiene compatibilidad con lenguajes para las propiedades, de modo que la propiedad Root se proyecta como métodos de descriptor de acceso. C# es muy similar con la adición de la sintaxis adecuada:

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

DirectComposition solo proporcionaba un tipo de objeto visual que admitía distintos tipos de superficies para representar contenido de mapa de bits. La composición de Windows ofrece jerarquías de clases pequeñas que representan distintos tipos de objetos visuales, pinceles y animaciones, aunque solo hay un tipo de superficie y solo se puede crear con C++ porque forma parte de la API de interop de composición de Windows diseñada para que la usen marcos de trabajo como XAML y desarrolladores de aplicaciones más sofisticados.

La jerarquía de clases de objetos visuales se muestra en la Ilustración 4. CompositionObject es un recurso que el compositor respalda. Las propiedades de todos los objetos de composición se pueden animar, potencialmente. Un objeto visual proporciona un montón de propiedades para controlar muchos aspectos de la posición relativa, el aspecto, el recorte y las opciones de representación del objeto visual. Incluye una propiedad de matriz de transformación, así como accesos directos para la escala y la rotación. Esta clase base es bastante útil. En cambio, ContainerVisual es una clase relativamente sencilla que, simplemente, agrega una propiedad Children. Aunque puede crear objetos visuales directamente, SpriteVisual permite asociar un lápiz de modo que el objeto visual pueda representar sus propios píxeles.

Objetos visuales de composición
Ilustración 4 Objetos visuales de composición

A partir de un objeto visual de contenedor raíz, puedo crear cualquier número de objetos visuales secundarios:

VisualCollection children = root.Children();

También pueden ser objetos visuales de contenedor, pero es más probable que sean objetos visuales sprite. Puedo agregar tres objetos visuales como secundarios del objeto visual raíz mediante un bucle for en C++:

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);
}

Puede imaginar fácilmente la ventana de la aplicación en la Ilustración 5, pero este código no generaría ninguna representación, ya que no hay ningún pincel asociado con estos objetos visuales. La jerarquía de clases de pinceles visuales se muestra en la Ilustración 6. CompositionBrush es, simplemente, una clase base para pinceles y no tiene ninguna funcionalidad propia. El tipo más sencillo es CompositionColorBrush, que solo ofrece una propiedad de color para representar objetos visuales de color sólido. Puede que no parezca muy emocionante, pero no olvide que se pueden conectar animaciones a esta propiedad de color. Las clases CompositionEffectBrush y CompositionSurfaceBrush están relacionadas, pero son pinceles más complejos, ya que los respaldan otros recursos. CompositionSurfaceBrush representa una superficie de composición para los objetos visuales adjuntos. Cuenta con una variedad de propiedades que controlan el trazado de mapas de bits, como la interpolación, la alineación y el estiramiento, por no hablar de la propia superficie. Composition­EffectBrush usa varios pinceles de superficie para producir varios efectos.

Objetos visuales en una ventana
Ilustración 5 Objetos visuales en una ventana

Pinceles de composición
Ilustración 6 Pinceles de composición

El proceso de crear y aplicar un pincel de color es muy sencillo. Aquí se muestra un ejemplo en C#:

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

La estructura de color está incorporada por cortesía del espacio de nombres Windows.UI y tiene alfa, rojo, verde y azul como valores de color de 8 bits, una desviación de la preferencia de DirectComposition y Direct2D por los valores de color de punto flotante. Una característica positiva de este enfoque hacia los objetos visuales y pinceles es que la propiedad de color se puede cambiar en cualquier momento y cualquier objeto visual que haga referencia al mismo lápiz se actualizará automáticamente. Además, como ya he dado a entender antes, la propiedad de color se puede animar. ¿Cómo funciona? Esto nos lleva a las clases de animación.

La jerarquía de clases de animación se muestra en la Ilustración 7. La clase base CompositionAnimation permite almacenar valores designados para usarlos con expresiones. En unos instantes, seguiré con las expresiones. KeyFrameAnimation proporciona propiedades de animación basadas en fotogramas clave, como la duración, iteración y comportamiento de detención. Las distintas clases de animación de fotogramas clave ofrecen métodos específicos por tipos para insertar fotogramas clave, así como propiedades de animación específicas por tipos. Por ejemplo, ColorKeyFrameAnimation permite insertar fotogramas clave con valores de color y una propiedad para controlar el espacio de color para la interpolación entre fotogramas clave.

Animaciones de composición
Ilustración 7 Animaciones de composición

Crear un objeto de animación y, a continuación, aplicar dicha animación a un objeto de composición concreto es sorprendentemente fácil. Supongamos que quiero animar la opacidad de un objeto visual. Puedo definir la opacidad del objeto visual al 50 por ciento directamente con un valor escalar en C++ como se indica a continuación:

visual.Opacity(0.5f);

De forma alternativa, puedo crear un objeto de animación escalar con fotogramas clave para producir una variable de animación de 0,0 a 1,0, en representación del 0 al 100 por ciento de opacidad:

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

El primer parámetro de InsertKeyFrame es el desplazamiento relativo desde el principio de la animación (0,0) al final de esta (1,0). El segundo parámetro es el valor de la variable de animación en ese punto de la escala de tiempo de la animación. De modo que esta animación tendrá una transición suave de valor de 0,0 a 1,0 durante la animación. Puedo definir la duración general de esta animación como se describe a continuación:

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

Con la animación preparada, solo queda conectarla a la propiedad y objeto de composición que prefiera:

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

El método StartAnimation se hereda de la clase base CompositionObject, lo que significa que puede animar las propiedades de una variedad de clases distintas. Esto también supone una desviación respecto a DirectComposition, donde cada propiedad que se puede animar proporcionaba sobrecargas para valores escalares, así como objetos de animación. La composición de Windows ofrece un sistema de propiedades mucho más avanzado que abre una puerta a algunas funcionalidades muy interesantes. En concreto, permite escribir expresiones textuales que reducen la cantidad de código que se debe escribir para conseguir animaciones y efectos más interesantes. Estas expresiones se analizan en tiempo de ejecución, se compilan y, a continuación, el motor de composición de Windows las ejecuta eficientemente.

Imaginemos que necesitamos girar un objeto visual a lo largo del eje Y y darle aspecto de profundidad. La propiedad RotationAngle del objeto visual, medida en radianes, no es suficiente, ya que no produce ninguna transformación que incluya perspectiva. A medida que el objeto visual gira, el extremo más cercano al ojo humano será mayor, mientras que el extremo opuesto será menor. La Ilustración 8 muestra varios objetos visuales giratorios que ilustran este comportamiento.

Objetos visuales giratorios
Ilustración 8 Objetos visuales giratorios

¿Cómo podría conseguir este efecto animado? Empecemos con una animación de fotograma clave escalar para el ángulo de rotación:

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

La función de aceleración linear reemplaza la función de aceleración o deceleración para producir un movimiento giratorio continuo. A continuación, debo definir un objeto personalizado con una propiedad a la que pueda hacer referencia desde un expresión. El compositor proporciona un conjunto de propiedades para este fin:

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

Un conjunto de propiedades también es un objeto de composición, de modo que puedo usar el método StartAnimation para animar mi propiedad personalizada del mismo modo que con cualquier otra propiedad incorporada:

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

Ahora tengo un objeto con la propiedad Angle en movimiento. A continuación, debo definir una matriz de transformación para producir el efecto deseado, al mismo tiempo que se delega en esta propiedad animada el propio ángulo de rotación. Especifique las expresiones:

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

Una animación de expresión no es un objeto de animación de fotograma clave, de modo que no existe ningún desplazamiento de fotograma clave relativo en el que puedan cambiar las variables de animación (a partir de cierta función de interpolación). En su lugar, las expresiones hacen referencia a parámetros que se pueden animar en el sentido más tradicional. A continuación describiré qué son los parámetros “pre”, “axis”, “rotation” y “post”. Empecemos por el parámetro axis:

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

El método CreateFromAxisAngle dentro de la expresión espera un eje para girar a su alrededor y esto define el eje alrededor del eje Y. También espera un ángulo de rotación y, para eso, podemos diferir a la propiedad de rotación definida con su propiedad “Angle” animada:

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

Para garantizar que la rotación tiene lugar a través del centro del objeto visual, en lugar del extremo izquierdo, debo multiplicar previamente la matriz de rotación creada por CreateFromAxisAngle con una traducción que cambie el eje de forma lógica al punto de rotación:

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

Recuerde que la multiplicación de matrices no es conmutativa, de modo que las matrices pre y post son exactamente eso. Finalmente, después de la matriz de rotación, puedo agregar cierta perspectiva y, a continuación, restaurar el objeto visual a su ubicación original:

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

Esto satisface todos los parámetros a los que hace referencia la expresión. Ahora solo tengo que usar la animación de expresiones para animar el objeto visual a través de la propiedad TransformMatrix:

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

Hasta ahora, he explorado varias formas de crear, rellenar y animar objetos visuales, pero, ¿y si necesito representar objetos visuales directamente? DirectComposition ofrecía superficies preasignadas y mapas de bits asignados de forma dispersa llamados superficies virtuales que se asignaban bajo demanda y se podían cambiar de tamaño. Al parecer, la composición de Windows no permite crear superficies. Hay una clase CompositionDrawingSurface, pero no hay ninguna forma de crearla sin ayuda externa. La respuesta procede de la API de interop de composición de Windows. Las clases de WinRT pueden implementar interfaces COM adicionales que no sean visibles directamente si solo tiene los metadatos de Windows de un componente. Debido a la información que contienen estas interfaces escondidas, se pueden consultar fácilmente en C++. Naturalmente, esto va a suponer un poco más de trabajo a medida que se desvía de las simples abstracciones que proporciona la API de composición de Windows a los desarrolladores convencionales. Lo primero que hay que hacer es crear un dispositivo de representación. Usaré Direct3D 11 porque la composición de Windows aún no admite Direct3D 12:

ComPtr<ID3D11Device> direct3dDevice;

A continuación, prepararé las marcas de creación de dispositivos:

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

La compatibilidad con BGRA me permite usar la API Direct2D más accesible para la representación con este dispositivo. A continuación, la función D3D11CreateDevice crea el propio dispositivo de hardware:

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

A continuación, debo consultar la interfaz DXGI del dispositivo, que es lo que necesitaré para crear un dispositivo Direct2D:

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

Ha llegado el momento de crear el propio dispositivo Direct2D:

ComPtr<ID2D1Device> direct2dDevice;

Aquí, de nuevo, habilitaré la capa de depuración para mejorar el diagnóstico:

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

En primer lugar, podría crear una fábrica de Direct2D para crear el dispositivo. Eso resultaría útil si necesitara crear recursos independientes del dispositivo. Aquí usaré simplemente el acceso directo que proporciona la función D2D1­CreateDevice:

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

El dispositivo de representación está listo. Tengo un dispositivo Direct2D que puedo usar para representar cualquier cosa que pueda imaginar. Ahora debo informar al motor de composición de Windows acerca de este dispositivo de representación. En este punto entran en acción las interfaces escondidas. Con el compositor que he estado usando, puedo consultar la interfaz ICompositorInterop:

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

ICompositorInterop proporciona métodos para crear una superficie de composición a partir de una superficie DXGI, lo que resultaría útil si quisiéramos incluir una cadena de intercambio existente en un árbol visual de composición, pero también ofrece algo mucho más interesante. El método CreateGraphicsDevice creará un objeto CompositionGraphicsDevice a partir de un dispositivo de representación concreto. La clase CompositionGraphicsDevice es una clase normal de la API de composición de Windows, en lugar de una interfaz escondida, pero no proporciona ningún constructor, por lo que debe usar C++ y la interfaz ICompositorInterop para crearlo:

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

Dado que CompositionGraphicsDevice es un tipo de WinRT, puedo volver a usar C++ moderno, en lugar del control manual de errores y punteros. Finalmente, CompositionGraphicsDevice me permite crear una superficie de composición:

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

Aquí he creado una superficie de composición con un tamaño de 100 x 100 píxeles. Tenga en cuenta que esto representa píxeles físicos, en lugar de las coordenadas lógicas y con reconocimiento de PPP que asume y proporciona el resto de la composición de Windows. La superficie también proporciona una representación con combinación alfa de 32 bits compatible con Direct2D. Por supuesto, Direct3D y Direct2D aún no se ofrecen a través de Windows Runtime, de modo que hay que recurrir a las interfaces escondidas para dibujar esta superficie:

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

Tal como sucedía con DirectComposition, la composición de Windows proporciona métodos BeginDraw y EndDraw en la interfaz IComposition­DrawingSurfaceInterop que incorporan y sustituyen las llamadas típicas al método Direct2D con los mismos nombres:

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

La composición de Windows usa el dispositivo de representación original que se proporcionó al crear el dispositivo de composición y lo usa para crear un contexto de dispositivo o destino de representación. Opcionalmente, puedo proporcionar un rectángulo de recorte en píxeles físicos, pero aquí he optado por un acceso sin restricciones a la superficie de representación. El método BeginDraw también devuelve un desplazamiento, de nuevo en píxeles físicos, para indicar el origen de la superficie de dibujo prevista. Esta no será necesariamente la esquina superior izquierda del destino de representación y es necesario ajustar o transformar atentamente cualquier comando de dibujo para que el desplazamiento sea correcto. De nuevo, no invoque BeginDraw en el destino de representación, ya que la composición de Windows ya lo ha hecho. Este destino de representación pertenece lógicamente a la API de composición y es necesario prestar atención para no aferrarse a este después de invocar EndDraw. El destino de representación ya está listo, pero no reconoce los PPP lógicos o efectivos para la vista. Puedo usar el espacio de nombres Windows::Graphics::Display para obtener los PPP lógicos para la vista actual y definir los PPP que usará Direct2D para la representación:

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

El último paso para que pueda empezar la representación consiste en controlar de alguna forma el desplazamiento de la composición. Una solución sencilla consiste en usar el desplazamiento para producir una matriz de transformación. Recuerde que Direct2D funciona con píxeles lógicos, así que no solo debo usar el desplazamiento, sino también el valor de PPP recién definido:

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

En este punto, puede dibujar tanto como quiera antes de invocar el método EndDraw en la interfaz interop de la superficie para garantizar que cualquier comando de dibujo de Direct2D en lotes se procesa y que los cambios de la superficie se reflejan en el árbol visual de composición:

check(surfaceInterop->EndDraw());

Por supuesto, aún no he asociado la superficie a ningún objeto visual y, como he mencionado, los objetos visuales ya no proporcionan ninguna propiedad de contenido y se deben representar mediante un pincel. Por suerte, el compositor creará un pincel para representar una superficie preexistente:

CompositionSurfaceBrush brush = compositor.CreateSurfaceBrush(surface);

A continuación, puedo crear un pincel sprite normal y usarlo para dar visibilidad al objeto visual:

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

Si quiere una mayor interoperabilidad, también puede recuperar el objeto visual de composición subyacente de un elemento XAML. Aquí se muestra un ejemplo en C#:

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

A pesar de su aparente estado temporal, ElementCompositionPreview está listo para producción y las aplicaciones enviadas a la Tienda Windows lo pueden usar. Para cualquier elemento de IU concreto, el método estático GetElementVisual devolverá el objeto visual del árbol de objetos visuales de composición subyacente. Tenga en cuenta que devuelve un objeto Visual, en lugar de un objeto ContainerVisual o SpriteVisual, así que no puede trabajar directamente con los elementos secundarios del objeto visual ni aplicar ningún pincel, pero puede ajustar las numerosas propiedades visuales que ofrece la composición de Windows. La clase auxiliar ElementCompositionPreview proporciona algunos métodos estáticos adicionales para agregar objetos visuales secundarios de forma controlada. Puede cambiar el desplazamiento del objeto visual y cosas como las pruebas de visitas a la IU seguirán funcionando a nivel de XAML. Incluso puede aplicar una animación directamente con la composición de Windows sin descomponer la infraestructura XAML creada a partir de esta. Creemos una animación escalar sencilla para girar el botón. Necesito recuperar el compositor del objeto visual y, a continuación, la creación de un objeto de animación funcionará como antes:

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

Creemos una sencilla animación para girar el botón lentamente y para siempre con una función de aceleración lineal:

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

A continuación, puedo indicar que un único giro debería tardar 3 segundos y continuar para siempre:

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

Finalmente, puedo conectar la animación al objeto visual que proporciona XAML y dar instrucciones al motor de composición para que anime la propiedad RotationAngle:

visual.StartAnimation("RotationAngle", animation);

Aunque puede que sea capaz de sacar esto adelante solo con XAML, el motor de composición de Windows proporciona mucha más eficacia y flexibilidad, ya que reside en un nivel de abstracción inferior y, sin duda, puede ofrecer un rendimiento mejor. Como otro ejemplo, la composición de Windows proporciona animaciones de cuaterniones que XAML no admite actualmente.

Hay tantas cosas que decir sobre el motor de composición de Windows. En mi humilde opinión, es la API de WinRT más revolucionaria hasta la fecha. Las posibilidades a su alcance son asombrosas, pero, a diferencia de otras grandes API de gráficos e IU, no conlleva una reducción de rendimiento ni una curva de aprendizaje prohibitiva. En muchos aspectos, la composición de Windows representa todas las cosas buenas y emocionantes de la plataforma Windows.

El equipo de la Composición de Windows está en Twitter: @WinComposition.


Kenny Kerr es programador informático radicado en Canadá, además de autor para Pluralsight y MVP de Microsoft. Tiene un blog en kennykerr.ca y puede seguirlo en Twitter: @kennykerr.

Gracias a los siguientes expertos técnicos de Microsoft por revisar este artículo: Mark Aldham, James Clarke, John Serna, Jeffrey Stall y Nick Waggoner