Edição especial do Windows 10 - 2015

Volume 30 - Número 11

Gráficos e Animação - Opções de Composição do Windows 10

Por Kenny Kerr | Windows 2015

O mecanismo de composição do Windows, também conhecido como o DWM (Gerenciador de Janelas da Área de Trabalho), tem uma nova API para o Windows 10. DirectComposition era a interface principal para composição, mas, como uma API COM clássica, era em grande parte inacessível para o desenvolvedor de aplicativos médio. A nova API de composição do Windows é fundamentada no WinRT (Tempo de Execução do Windows) e fornece a base para a renderização de alto desempenho, combinando o mundo dos gráficos de modo imediato oferecidos pelo Direct2D e pelo Direct3D a uma árvore visual retida que agora conta com recursos de animação e efeitos muito melhorados.

Escrevi pela primeira vez sobre o DWM em 2006, quando o Windows Vista estava na versão beta (goo.gl/19jCyR). Ele permitia controlar a extensão do efeito de desfoque para determinada janela e criar um cromado personalizado que se combinava muito bem à área de trabalho. A Figura 1 ilustra a amplitude dessa conquista no Windows 7. Era possível produzir renderização com aceleração de hardware com Direct3D e Direct2D para criar visuais deslumbrantes para seu aplicativo (goo.gl/IufcN1). Você podia até mesmo combinar o antigo mundo dos controles GDI e USER ao DWM (goo.gl/9ITISE). Ainda assim, qualquer observador atento poderia ver que o DWM tinha mais a oferecer — muito mais. O recurso Flip 3D do Windows no Windows 7 era a prova convincente disso.

Windows Aero
Figura 1 Windows Aero

O Windows 8 introduziu uma nova API para o DWM chamada DirectComposition. Seu nome é uma homenagem à família DirectX de APIs COM clássicas que inspiraram seu design. O DirectComposition começou a dar aos desenvolvedores uma visão mais clara do que o DWM era capaz de fazer. Ele também oferecia terminologia melhorada. O DWM é realmente o mecanismo de composição do Windows e foi capaz de produzir os efeitos deslumbrantes no Windows Vista e no Windows 7 porque mudou fundamentalmente a maneira como as janelas da área de trabalho eram renderizadas. Por padrão, o mecanismo de composição criava uma superfície de redirecionamento para cada janela de nível superior. Descrevi isso em detalhes em minha coluna de junho de 2014 (goo.gl/oMlVa4). As superfícies de redirecionamento faziam parte de uma árvore visual, e o DirectComposition permitia que os aplicativos fizessem uso dessa mesma tecnologia para fornecer uma API leve de modo retido para gráficos de alto desempenho. O DirectComposition oferecia uma árvore visual e gerenciamento de superfície que permitia que o aplicativo transferisse a produção de efeitos e animações para o mecanismo de composição. Descrevi esses recursos em minhas colunas de agosto (goo.gl/CNwnWR) e setembro de 2014 (goo.gl/y7ZMLL). Até produzi um curso sobre renderização de alto desempenho com o DirectComposition para a Pluralsight (goo.gl/fgg0XN).

O Windows 8 estreou o DirectComposition, junto com melhorias impressionantes para o restante da família de APIs DirectX, mas também marcou o início de uma nova era para a API do Windows que mudaria para sempre a maneira como os desenvolvedores encaram o sistema operacional. A introdução do Tempo de Execução do Windows ofuscou tudo o resto. A Microsoft prometeu uma nova maneira de compilar aplicativos e acessar serviços do sistema operacional que levou à eventual aposentadoria da chamada API Win32, que tinha sido por muito tempo a forma dominante de compilar aplicativos e interagir com o sistema operacional. O Windows 8 teve um começo difícil, mas o Windows 8.1 corrigiu muitos problemas, e o Windows 10 oferece agora uma API muito mais abrangente que vai satisfazer a muito mais desenvolvedores interessados em compilar aplicativos de primeira classe, ou melhor, aplicativos sérios para o Windows.

O Windows 10 foi lançado em julho de 2015 com uma visualização da nova API de composição que ainda não estava pronta para a produção. Ela ainda estava sujeita a alterações e, assim, não podia ser usada em aplicativos Universais do Windows enviados à Windows Store. Foi melhor assim, pois a API de composição que está disponível agora para a produção mudou substancialmente e para melhor. Essa atualização do Windows 10 também é a primeira vez que a mesma API de composição está disponível em todos os fatores forma, dando ainda mais credibilidade à parte universal da Plataforma Universal do Windows. A composição funciona da mesma forma, independentemente de você a utilizar para seu poderoso desktop com várias telas ou o pequeno do smartphone em seu bolso.

Naturalmente, aquilo de que todos gostam no Tempo de Execução do Windows é que ele finalmente cumpre a promessa de um tempo de execução de linguagem comum para o Windows. Se preferir escrever código em C#, você poderá usar o Tempo de Execução do Windows diretamente por meio do suporte interno no Microsoft .NET Framework. Se, como eu, você preferir usar C++, poderá usar o Tempo de Execução do Windows sem abstrações intermediárias ou dispendiosas. O Tempo de Execução do Windows se baseia em COM em vez de .NET e, assim, é ideal para o consumo de C++. Vou usar o Modern C++ para o Tempo de Execução do Windows (moderncpp.com), a projeção padrão da linguagem C++, mas você pode usar sua linguagem favorita, pois a API é a mesma, independentemente disso. Vou até mesmo oferecer alguns exemplos em C# para ilustrar como o Tempo de Execução do Windows pode dar suporte a diferentes linguagens sem problemas.

A API de composição do Windows se distancia de suas raízes do DirectX. Enquanto o DirectComposition fornecia um objeto de dispositivo, modelado de acordo com os dispositivos Direct3D e Direct2D, a nova API de composição do Windows começa com um compositor. No entanto, ela tem a mesma finalidade, atuando como a malha para os recursos de composição. Além disso, a composição do Windows é muito semelhante ao DirectComposition. Há um destino de composição que representa a relação entre uma janela e sua árvore visual. As diferenças tornam-se mais evidentes à medida que você examina mais de perto os elementos visuais. Os elementos visuais do DirectComposition tinham uma propriedade de conteúdo que fornecia um bitmap de algum tipo. O bitmap era uma destas três coisas: uma superfície de composição, uma cadeia de troca de DXGI ou a superfície redirecionamento de outra janela. Uma aplicação típica do DirectComposition consistia em recursos visuais e superfícies, com as superfícies atuando como o conteúdo ou bitmaps para os diferentes elementos visuais. Como representado na Figura 2, uma árvore visual de composição é ligeiramente diferente. O novo objeto visual não tem a propriedade de conteúdo e, em vez disso, é renderizado com um pincel de composição. Essa acaba sendo uma abstração mais flexível. Embora o pincel possa renderizar apenas um bitmap como antes, pincéis simples de cor sólida podem ser criados de forma muito eficiente, e pincéis mais elaborados podem ser definidos, pelo menos conceitualmente, de uma maneira não muito diferente de como o Direct2D fornece efeitos que podem ser tratados como imagens. A abstração certa faz toda a diferença.

A árvore visual de composição do Windows
Figura 2 A árvore visual de composição do Windows

Vamos examinar alguns exemplos práticos para ilustrar como tudo isso funciona e lhe dar uma noção do que é possível. Mais uma vez, você pode escolher sua projeção de linguagem do WinRT favorita. Você pode criar um compositor com Modern C++, como indicado a seguir:

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

De forma semelhante, você pode fazer o mesmo com C#:

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

Você pode até mesmo usar a sintaxe mais extravagante oferecida pelo C++/CX:

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

Todos esses itens são equivalentes da perspectiva da API e refletem apenas as diferenças na projeção da linguagem. Existem basicamente duas maneiras pelas quais você pode escrever um aplicativo Universal do Windows atualmente. Talvez a abordagem mais comum seja usar o namespace Windows.UI.Xaml do sistema operacional. Se O XAML não for tão importante para seu aplicativo, você também poderá usar o modelo de aplicativo subjacente diretamente, sem dependências em relação ao XAML. Descrevi o modelo de aplicativo WinRT em minha coluna de agosto de 2013 (goo.gl/GI3OKP). Usando essa abordagem, você precisa apenas de uma implementação mínima das interfaces IFrameworkView e IFrameworkViewSource, e pronto. A Figura 3 fornece um esquema básico em C# que você pode usar para começar. A composição do Windows também oferece integração profunda com XAML, mas vamos começar com um aplicativo simples sem XAML, pois ele proporciona um exemplo mais simples para aprender sobre composição. Retornarei ao XAML um pouco mais adiante neste artigo.

Figura 3 Modelo de aplicativo do Tempo de Execução do Windows em 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() { }
}

É dentro do método SetWindow do aplicativo (consulte a Figura 3) que o compositor deve ser construído. De fato, este é o ponto mais inicial no ciclo de vida do aplicativo em que isso pode ocorrer, pois o compositor depende do dispatcher da janela, e esse é o ponto em que a janela e o dispatcher finalmente existem. A relação entre o compositor e a exibição do aplicativo pode então ser estabelecida por meio da criação de um destino de composição:

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

É vital que o aplicativo mantenha o destino de composição ativo. Portanto, torne-o uma variável membro de sua implementação do IFrameworkView. Como mencionei antes, o destino de composição representa a relação entre a janela ou exibição e sua árvore visual. Tudo o que você pode fazer com um destino de composição é definido na raiz visual. Tipicamente, esse será um contêiner visual

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

Aqui, estou usando C++, que não tem suporte de linguagem para propriedades. Portanto, a propriedade Root é projetada como métodos de acessador. O C# é muito semelhante, com a adição da sintaxe da propriedade:

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

O DirectComposition fornecia apenas um tipo de elemento visual, que dava suporte a diferentes tipos de superfícies para representar o conteúdo de bitmap. A composição do Windows oferece pequenas hierarquias de classes que representam diferentes tipos de elementos visuais, pincéis e animações. Porém, há apenas um tipo de superfície e ela só pode ser criada usando C++ porque faz parte da API de interoperabilidade de composição do Windows para uso por estruturas como XAML e desenvolvedores de aplicativos mais sofisticados.

A hierarquia de classes visuais é mostrada na Figura 4. Um CompositionObject é um recurso com suporte do compositor. Todos os objetos de composição podem, potencialmente, ter suas propriedades animadas. Um elemento visual fornece uma enorme quantidade de propriedades para controlar muitos aspectos da posição relativa do elemento visual, bem como opções de aparência, recorte e renderização. Ele inclui uma propriedade de transformação de matriz, além de atalhos para escala e rotação. Essa é uma classe base poderosa. Por outro lado, ContainerVisual é uma classe relativamente simples que apenas adiciona uma propriedade Children. Embora você possa criar diretamente elementos visuais de contêiners, um SpriteVisual adiciona a capacidade de associar uma pincel para que o elemento visual possa realmente processar seus próprios pixels.

Elementos visuais de composição
Figura 4 Elementos visuais de composição

Com um elemento visual de contêiner raiz, posso criar diversos elementos visuais filho:

VisualCollection children = root.Children();

Eles também podem ser elementos visuais contêiners, mas é mais provável que sejam elementos visuais sprite. Eu poderia adicionar três recursos visuais como filhos do elemento visual raiz usando um loop for em 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);
}

Você pode facilmente imaginar a janela do aplicativo na Figura 5, no entanto, esse código não resultará na renderização de nada, pois não há um pincel associado a esses elementos visuais. A hierarquia de classes de pincéis é mostrada na Figura 6. Um CompositionBrush é simplesmente uma classe base para pincéis e não fornece nenhuma funcionalidade própria. Um CompositionColorBrush é o tipo mais simples, oferecendo apenas uma propriedade de cor para renderização de elementos visuais de cores sólidas. Isso pode não parecer muito empolgante, mas não se esqueça de que você pode conectar animações a essa propriedade de cor. As classes CompositionEffectBrush e CompositionSurfaceBrush estão relacionadas, mas são pincéis mais complexos, pois têm o suporte de outros recursos. Um CompositionSurfaceBrush renderizará uma superfície de composição para quaisquer elementos visuais anexados. Ele tem diversas propriedades que controlam o desenho de bitmaps, como interpolação, alinhamento e alongamento, para não mencionar a própria superfície. Um CompositionEffectBrush utiliza vários pincéis de superfície para produzir vários efeitos.

Elementos visuais filho em uma janela
Figura 5 Elementos visuais filho em uma janela

Pincéis de composição
Figura 6 Pincéis de composição

É simples criar e aplicar um pincel de cor. Aqui está um exemplo em C#:

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

A estrutura de cor é fornecida pelo namespace da interface do usuário do Windows.e tem alfa, vermelho, verde e azul como valores de cores de 8 bits, o que difere da preferência do DirectComposition e do Direct2D por valores de cor de ponto flutuante. Um recurso interessante dessa abordagem para elementos visuais e pincéis é que a propriedade de cor pode ser alterada a qualquer momento, e quaisquer elementos visuais referentes ao mesmo pincel serão atualizados automaticamente. De fato, como eu já tinha sugerido antes, a propriedade de cor pode até ser animada. Então, como isso funciona? Isso nos leva às classes de animação.

A hierarquia de classes de animação é mostrada na Figura 7. A classe base CompositionAnimation fornece a capacidade de armazenar valores nomeados para uso com expressões. Vou falar mais sobre expressões daqui a um instante. Um KeyFrameAnimation fornece propriedades típicas de animação baseada em quadros-chave, como duração, iteração e comportamento de parada. As várias classes de animação de quadro-chave oferecem métodos específicos de tipos para a inserção de quadros-chave, bem como propriedades de animação específicas de tipos. Por exemplo, ColorKeyFrameAnimation permite inserir quadros-chave com valores de cor e uma propriedade para controlar o espaço de cor para interpolação entre quadros-chave.

Animações de composição
Figura 7 Animações de composição

É surpreendentemente fácil criar um objeto de animação e aplicar essa animação a um objeto de composição específico. Digamos que eu queira animar a opacidade de um elemento visual. Eu poderia definir a opacidade do elemento visual como 50% diretamente com um valor escalar em C++, como indicado a seguir:

visual.Opacity(0.5f);

Como alternativa, posso criar um objeto de animação escalar com quadros-chave para produzir uma variável de animação de 0,0 a 1,0, o que representa opacidade de 0% até 100%:

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

O primeiro parâmetro de InsertKeyFrame é o deslocamento relativo do início (0,0) até o fim da animação (1,0). O segundo parâmetro é o valor da variável de animação nesse ponto na linha do tempo da animação. Portanto, essa animação terá uma transição suave do valor de 0,0 a 1,0 ao longo da duração da animação. Posso definir a duração total da animação, como mostrado a seguir:

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

Com a animação pronta, simplesmente preciso conectá-la ao objeto de composição e à propriedade de minha escolha:

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

O método StartAnimation é realmente herdado da classe base CompositionObject, o que significa que você pode animar as propriedades de várias classes diferentes. Essa é outra diferença em relação a DirectComposition, em que cada propriedade animável fornecida tem uma sobrecarga de valores escalares, bem como objetos de animação. A composição do Windows oferece um sistema de propriedades muito mais avançado que abre as portas para alguns recursos muito interessantes. Em particular, ele dá suporte à capacidade de escrever expressões textuais que reduzem a quantidade de código que precisa ser escrito para criar animações e efeitos mais interessantes. Essas expressões são analisadas em tempo de execução, compiladas e executadas de forma eficiente pelo mecanismo de composição do Windows.

Imagine que você precise girar um elemento visual ao longo do eixo Y e dar-lhe a aparência de profundidade. A propriedade RotationAngle do elemento visual, medida em radianos, não é suficiente, pois ela não produzirá uma transformação que inclua a perspectiva. À medida que o elemento visual gira, o lado mais próximo do olho humano deve parecer maior, enquanto a extremidade oposta deve parecer menor. A Figura 8 mostra vários elementos visuais giratórios que ilustram esse comportamento.

Elementos visuais giratórios
Figura 8 Elementos visuais giratórios

Como você conseguiria obter esse efeito animado? Bem, vamos começar com uma animação de quadro-chave escalar para o ângulo de rotação:

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

A função de atenuação linear substitui a função de aceleração/desaceleração padrão para produzir um movimento giratório contínuo. Preciso então definir um objeto personalizado com uma propriedade à qual eu possa fazer referência de dentro de uma expressão. O compositor fornece um conjunto de propriedades exatamente para essa finalidade:

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

Um conjunto de propriedades também é um objeto de composição. Portanto, posso usar o método StartAnimation para animar minha propriedade personalizada tão facilmente quanto qualquer propriedade interna:

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

Agora tenho um objeto cuja propriedade Angle está em movimento. Preciso então definir uma matriz de transformação para produzir o efeito desejado, delegando a essa propriedade animada o próprio ângulo de rotação. Entram em cena as expressões:

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

Uma animação de expressão não é um objeto de quadro-chave de animação, assim, não há deslocamentos de quadro-chave relativo em que as variáveis possam mudar de animação (com base em alguma função de interpolação). Em vez disso, as expressões simplesmente se referem a parâmetros que podem ser animados no sentido mais tradicional. Ainda assim, fica por minha conta definir o que são "pré", "eixo", "rotação" e "pós". Vamos começar com o parâmetro de eixo:

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

O método CreateFromAxisAngle dentro da expressão espera que um eixo para girar, e isso define o eixo em torno do eixo Y. Ele também espera um ângulo de rotação e, para isso, podemos recorrer à propriedade de rotação definida com sua propriedade "Angle" animada:

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

Para assegurar que a rotação ocorra no centro do elemento visual, em vez de ocorrer na borda esquerda, preciso pré-multiplicar a matriz de rotação criada por CreateFromAxisAngle com uma conversão que desloca logicamente o eixo para o ponto de rotação:

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

Lembre-se de que a multiplicação de matrizes não é comutativa, assim, as pré-matrizes e pós-matrizes realmente são apenas isso. Finalmente, depois da matriz de rotação, posso adicionar perspectiva e restaurar o elemento visual a seu local original:

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

Isso satisfaz todos os parâmetros referenciados pela expressão, e agora posso simplesmente usar a expressão de animação para animar o elemento visual usando sua propriedade TransformMatrix:

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

Portanto, explorei várias formas de criar, preencher e animar elementos visuais, mas e se eu precisar renderizar elementos visuais diretamente? O DirectComposition oferecia superfícies pré-alocadas e bitmaps escassamente alocados, chamados de superfícies virtuais, que eram alocados sob demanda e também eram redimensionáveis. A composição do Windows aparentemente não fornece recursos para criar superfícies. Há uma classe CompositionDrawingSurface, mas não há maneira de criá-la sem ajuda externa. A solução é fornecida pela API de interoperabilidade de composição do Windows. As classes WinRT podem implementar interfaces COM adicionais não diretamente visíveis, se tudo o que você tem são os metadados do Windows de um componente. Dado o conhecimento dessas interfaces encobertas, você pode facilmente consultá-las em C++. Naturalmente, isso será um pouco mais trabalhoso, pois você está lidando com algo diferente das abstrações puras fornecidas aos desenvolvedores convencionais pela API de composição do Windows. A primeira coisa que preciso fazer é criar um dispositivo de renderização. Vou usar o Direct3D 11 porque a composição do Windows ainda não dá suporte ao Direct3D 12:

ComPtr<ID3D11Device> direct3dDevice;

Prepararei então os sinalizadores de criação de dispositivo:

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

O suporte a BGRA me permite usar a API Direct2D mais acessível para renderização com esse dispositivo e, em seguida, a função D3D11CreateDevice cria o próprio 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

Preciso então consultar a interface DXGI do dispositivo, pois precisarei dela para criar um dispositivo Direct2D:

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

Agora é hora de criar o próprio dispositivo Direct2D:

ComPtr<ID2D1Device> direct2dDevice;

Aqui, novamente, habilitarei a camada de depuração para diagnósticos adicionais:

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

Eu poderia criar primeiro uma malha Direct2D para criar o dispositivo. Isso seria útil se eu precisasse criar recursos independentes de dispositivo. Aqui, vou usar o atalho fornecido pela função D2D1CreateDevice:

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

O dispositivo de renderização está pronto. Tenho um dispositivo Direct2D que posso usar para renderizar tudo o que eu possa imaginar. Agora preciso informar o mecanismo de composição do Windows sobre esse dispositivo de renderização. É aqui que entram as interfaces encobertas. Dado o compositor que usei por toda parte, posso consultar a interface de ICompositorInterop:

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

ICompositorInterop fornece métodos para criar uma superfície de composição com base em uma superfície DXGI, o que certamente seria útil se você quisesse incluir uma cadeia de troca existente em uma árvore visual de composição, mas ele fornece outra coisa que é muito mais interessante. Seu método CreateGraphicsDevice criará um objeto CompositionGraphicsDevice, dado um dispositivo de renderização. A classe CompositionGraphicsDevice é uma classe normal na API de composição do Windows, em vez de uma interface encoberta, mas não fornece um construtor. Portanto, você precisa usar C++ e a interface de ICompositorInterop para criá-lo:

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

Como CompositionGraphicsDevice é um tipo WinRT, posso usar novamente o Modern C++, em vez de ponteiros e tratamento de erros manual. E é CompositionGraphicsDevice que finalmente me permite criar uma superfície de composição:

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

Estou criando uma superfície de composição com tamanho de 100 x 100 pixels. Observe que isso representa pixels físicos, em vez das coordenadas lógicas e com reconhecimento de DPI presumidas e fornecidas pelo restante da composição do Windows. A superfície também fornece renderização de 32 bits com mesclagem alfa com suporte no Direct2D. Claro, o Direct3D e o Direct2D ainda não são oferecidos por meio do Tempo de Execução do Windows, por isso, voltamos às interfaces encobertas para desenhar de fato nessa superfície:

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

De forma muito parecida com o que o DirectComposition fazia antes dela, a composição do Windows fornece métodos BeginDraw e EndDraw na interface ICompositionDrawingSurfaceInterop que incluem e tomam o lugar das chamadas típicas para as chamadas de método Direct2D que têm os mesmos nomes:

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

A composição Windows utiliza o dispositivo de renderização original fornecido no momento em que o dispositivo de composição foi criado para criar um contexto de dispositivo ou renderizar o destino. Opcionalmente, posso fornecer um retângulo de recorte em pixels físicos, mas optei aqui pelo acesso irrestrito à superfície de renderização. BeginDraw também retorna um deslocamento, novamente em pixels físicos, que indica a origem da superfície de desenho pretendida. Esse não será necessariamente o canto superior esquerdo do destino de renderização, e é preciso ter cuidado para ajustar ou transformar comandos de desenho para acomodar corretamente esse deslocamento. Mais uma vez, não chame BeginDraw no destino de renderização, pois a composição do Windows já fez isso para você. Este destino de renderização é logicamente de propriedade da API de composição, e é preciso ter cuidado para não retê-lo após a chamada a EndDraw. O destino de renderização agora está pronto, mas não está ciente da DPI lógico ou efetiva para a exibição. Posso usar o namespace Windows:: Graphics::Display para obter o DPI lógico para a exibição atual e definir o DPI que será usado pelo Direct2D para renderização:

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

A etapa final para que a renderização possa começar é lidar com o deslocamento de composição de alguma forma. Uma solução simples é usar o deslocamento para produzir uma matriz de transformação. Basta lembrar que o Direct2D lida com pixels lógicos, assim, preciso usar não só o deslocamento, mas também o valor DPI recém-criado:

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

Nesse ponto, você pode desenhar o que quiser antes de chamar o método EndDraw na interface de interoperabilidade de superfície para garantir que quaisquer comandos de desenho Direct2D em lote sejam processados e as alterações na superfície sejam refletidas na árvore visual de composição:

check(surfaceInterop->EndDraw());

Naturalmente, ainda não associei a superfície a um elemento visual e, como já mencionei, os elementos visuais não fornecem mais uma propriedade de conteúdo e devem ser renderizados usando um pincel. Felizmente, o compositor criará um pincel para representar uma superfície pré-existente:

CompositionSurfaceBrush brush = compositor.CreateSurfaceBrush(surface);

Posso criar um pincel sprite normal e usá-lo para apresentar o elemento visual:

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

Se essa interoperabilidade não é suficiente para você, é possível até usar um elemento XAML e recuperar o elemento visual de composição subjacente. Aqui está um exemplo em C#:

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

Apesar de seu status aparentemente temporário, ElementCompositionPreview de fato está pronto para a produção e pode ser usado por aplicativos enviados à Windows Store. Dado qualquer elemento da interface do usuário, o método GetElementVisual estático retornará o elemento visual da árvore visual de composição subjacente. Observe que ele retorna um elemento visual em vez de um ContainerVisual ou SpriteVisual. Portanto, você não pode trabalhar diretamente com filhos visuais nem aplicar um pincel, mas pode ajustar as muitas propriedades visuais oferecidos pela composição do Windows. A classe auxiliar ElementCompositionPreview fornece alguns métodos estáticos adicionais para adicionar elementos visuais filho de uma forma controlada. Você pode alterar o deslocamento do elemento visual, e itens como o teste de cliques da interface do usuário continuarão a funcionar no nível do XAML. Você pode até mesmo aplicar uma animação diretamente com a composição do Windows sem interromper a infraestrutura XAML fundamentada nela. Vamos criar uma animação escalar simples para girar o botão. Preciso recuperar o compositor do elemento visual. Depois, a criação de um objeto de animação funciona como antes:

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

Vamos criar uma animação simples para girar lentamente o botão para sempre, com uma função de atenuação linear:

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

Posso então indicar que uma única rotação deve levar três segundos e continuar para sempre:

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

Finalmente, posso simplesmente conectar a animação visual fornecida pelo XAML, instruindo o mecanismo de composição a animar sua propriedade RotationAngle:

visual.StartAnimation("RotationAngle", animation);

Embora talvez você consiga fazer isso apenas com o XAML, o mecanismo de composição do Windows oferece muito mais capacidade e flexibilidade, já que ele reside em um nível muito mais baixo de abstração e pode, sem dúvida, proporcionar um melhor desempenho. Como em outro exemplo, a composição do Windows fornece animações quatérnion para as quais, atualmente, não há suporte no XAML.

Há muito mais a ser dito sobre o mecanismo de composição do Windows. Em minha humilde opinião, essa é a API WinRT mais inovadora até hoje. A quantidade de recursos à sua disposição é impressionante e, contudo, diferentemente de tantas outras excelentes APIs gráficas e de interface do usuário, ela não causa uma redução de desempenho ou sequer uma curva de aprendizado proibitiva. Em muitos aspectos, a composição do Windows representa tudo o que há de bom e empolgante na plataforma Windows.

Você pode contatar a equipe da Composição do Windows no Twitter: @WinComposition.


Kenny Kerré programador de computador, autor da Pluralsight e Microsoft MVP, e mora no Canadá. Ele mantém um blog em kennykerr.ca e pode ser seguido no Twitter: @kennykerr.

Agradecemos aos seguintes especialistas técnicos da Microsoft pela revisão deste artigo: Mark Aldham, James Clarke, John Serna, Jeffrey Stall and Nick Waggoner