Windows com C++

Abraçando o mecanismo de composição do Windows

Kenny Kerr

Kenny KerrO mecanismo de composição do Windows representa a saída de um mundo no qual cada aplicativo DirectX exige sua própria cadeia de troca para um onde, mesmo que tão fundamental, uma construção é desnecessária. Claro, você pode continuar a criar aplicativos Direct3D e Direct2D usando uma cadeia de troca para apresentação, mas não é mais necessário. O mecanismo de composição nos leva muito mais perto do metal—o GPU—permitindo que os aplicativos criem superfícies de composição diretamente.

O único objetivo do mecanismo de composição é compor diferentes bitmaps em conjunto. Você pode solicitar que vários efeitos, transformações e até animações sejam produzidos, mas no final do dia está tudo relacionado à composição de bitmaps. O próprio mecanismo não tem capacidades de renderização de gráficos como aqueles fornecidos pelo Direct2D ou Direct3D e não define vetores ou texto. Ele se preocupa sobre sua composição. Forneça uma coleção de bitmaps e ele fará coisas incríveis para combinar e compor.

Esses bitmaps podem ter várias formas. Um bitmap pode realmente ser uma memória de vídeo. Pode até ser uma cadeia de troca, como eu ilustrei na minha coluna de junho (msdn.microsoft.com/magazine/dn745861). Mas se você realmente deseja iniciar a utilizar o mecanismo de composição, é necessário analisar as superfícies de composição. Uma superfície de composição é um bitmap fornecido diretamente pelo mecanismo de composição e, como tal, permite determinadas otimizações que seriam difíceis de atingir com outras formas de bitmaps.

Neste mês, irei pegar a janela de mistura de alfa da minha coluna anterior e mostrar como pode ser reproduzida usando uma superfície de composição em vez de uma cadeia de troca. Há alguns benefícios interessantes—e também alguns desafios exclusivos, particularmente sobre o tratamento de perda do dispositivo e dimensionamento DPI por monitor. Mas, primeiro eu preciso rever o problema do gerenciamento da janela.

Nas minhas colunas anteriores, eu usei o ATL para gerenciamento de janela ou ilustrei como registrar, criar e bombear mensagens de janela diretamente com o API do Windows. Há prós e contras em ambas abordagens. O ATL continua a funcionar bem para o gerenciamento de janela, mas está perdendo lentamente o compartilhamento do desenvolvedor e a Microsoft certamente parou de investir há muito tempo atrás. Por outro lado, criar uma janela diretamente com RegisterClass e CreateWindow tende a ser um problema porque não pode associar facilmente um objeto C++ com um identificador de janela. Se você pensou em organizar uma união, pode estar tentado a dar uma olhada no código-fonte do ATL para ver como isso pode ser realizado, apenas para perceber que há alguma magia negra nele com coisas chamadas “trunks” e até mesmo linguagem assembly.

A boa notícia é que isso não precisava ser tão difícil. Embora o ATL certamente produz um despacho de mensagens muito eficiente, uma solução simples envolvendo apenas o C++ padrão funcionará bem. Eu não quero me desviar muito com as mecânicas dos procedimentos da janela, portanto, eu simplesmente direciono você para a Figura 1, que fornece um modelo de classe simples que faz as organizações necessárias para associar um ponteiro “este” com uma janela. O modelo usa a mensagem WM_NCCREATE para extrair o ponteiro e armazenar com o identificador da janela. Subsequentemente recupera o ponteiro e envia mensagens para o identificador de mensagem mais derivado.

Figura 1 Um modelo de classe de janela simples

template <typename T> struct Window { HWND m_window = nullptr; static T * GetThisFromHandle(HWND window) { return reinterpret_cast<T *>(GetWindowLongPtr(window, GWLP_USERDATA)); } static LRESULT __stdcall WndProc(HWND   const window, UINT   const message, WPARAM const wparam, LPARAM const lparam) { ASSERT(window); if (WM_NCCREATE == message) { CREATESTRUCT * cs = reinterpret_cast<CREATESTRUCT *>(lparam); T * that = static_cast<T *>(cs->lpCreateParams); ASSERT(that); ASSERT(!that->m_window); that->m_window = window; SetWindowLongPtr(window, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(that)); } else if (T * that = GetThisFromHandle(window)) { return that->MessageHandler(message, wparam, lparam); } return DefWindowProc(window, message, wparam, lparam); } LRESULT MessageHandler(UINT   const message, WPARAM const wparam, LPARAM const lparam) { if (WM_DESTROY == message) { PostQuitMessage(0); return 0; } return DefWindowProc(m_window, message, wparam, lparam); } };

O pressuposto é que uma classe derivada criará uma janela e passará este ponteiro como o último parâmetro ao chamar as funções Create­Window ou CreateWindowEx. A classe derivada pode simplesmente registrar e criar a janela e responder às mensagens da janela com uma substituição do MessageHandler. Essa substituição confia no polimorfismo de tempo de compilação, portanto, não há necessidade de funções virtuais. No entanto, o efeito é o mesmo. Portanto, você ainda precisará se preocupar com a reentrância. A Figura 2 mostra uma classe de janela concreta que confia no modelo de classe Window. Essa classe registra e cria a janela em seu construtor, mas confia no procedimento da janela fornecido pela classe base.

Figura 2 Uma classe de janela concreta

struct SampleWindow : Window<SampleWindow> { SampleWindow() { WNDCLASS wc = {}; wc.hCursor       = LoadCursor(nullptr, IDC_ARROW); wc.hInstance     = reinterpret_cast<HINSTANCE>(&__ImageBase); wc.lpszClassName = L"SampleWindow"; wc.style         = CS_HREDRAW | CS_VREDRAW; wc.lpfnWndProc   = WndProc; RegisterClass(&wc); ASSERT(!m_window); VERIFY(CreateWindowEx(WS_EX_NOREDIRECTIONBITMAP, wc.lpszClassName, L"Window Title", WS_OVERLAPPEDWINDOW | WS_VISIBLE, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, nullptr, nullptr, wc.hInstance, this)); ASSERT(m_window); } LRESULT MessageHandler(UINT message, WPARAM const wparam, LPARAM const lparam) { if (WM_PAINT == message) { PaintHandler(); return 0; } return __super::MessageHandler(message, wparam, lparam); } void PaintHandler() { // Render ... } };

Observe que no construtor da Figura 2, o membro herdado m_window não é inicializado (um nullptr) antes de chamar CreateWindow, mas é inicializado quando esta função retorna. Isso pode parecer mágica, mas é o procedimento da janela que conecta isso conforme as mensagens começam a chegar, muito antes do CreateWindow retornar. O motivo pelo qual é importante lembrar disso é que, usando o código como este, é possível reproduzir o mesmo efeito perigoso que chamar funções virtuais de um construtor. Se você terminar derivando ainda mais, apenas certifique-se de retirar a criação da janela do construtor para que esta forma de reentrância não atrapalhe você. Aqui está uma função WinMain simples que pode criar a janela e bombear as mensagens da janela:

int __stdcall wWinMain(HINSTANCE, HINSTANCE, PWSTR, int) { SampleWindow window; MSG message; while (GetMessage(&message, nullptr, 0, 0)) { DispatchMessage(&message); } }

OK, voltando para o assunto principal. Agora que eu tenho uma abstração de classe de janela simples, posso usar para gerenciar mais facilmente a coleção de recursos necessária para criar um aplicativo DirectX. Também irei mostrar como lidar corretamente com o dimensionamento de DPI. Embora eu tenha abordado o dimensionamento DPI em detalhes na minha coluna de fevereiro de 2014 (msdn.microsoft.com/magazine/dn574798), aqui estão alguns desafios exclusivos ao combinar com o API DirectComposition. Vou começar do início. Eu preciso incluir o API de dimensionamento de shell:

#include <ShellScalingAPI.h> #pragma comment(lib, "shcore")

Agora eu posso começar a montar os recursos necessários para dar vida à minha janela. Dado que eu tenho uma classe de janela, eu posso apenas tornar estes membros da classe. Primeiro, o equipamento Direct3D:

ComPtr<ID3D11Device> m_device3d;

Em seguida, o equipamento DirectComposition:

ComPtr<IDCompositionDesktopDevice> m_device;

Na minha coluna anterior, eu usei a interface IDCompositionDevice para representar o equipamento de composição. Essa é a interface originada do Windows 8, mas foi substituída no Windows 8.1 com a interface IDCompositionDesktopDevice, que é derivada de outra nova interface chamada IDComposition­Device2. Não estão relacionadas ao original. A interface IDComposition­Device2 serve para criar a maioria dos recursos de composição e também controla a composição transacional. A interface IDCompositionDesktopDevice adiciona a habilidade de criar determinados recursos de composição específicas da janela.

Também precisarei de uma composição alvo, visual e de superfície:

ComPtr<IDCompositionTarget>  m_target; ComPtr<IDCompositionVisual2> m_visual; ComPtr<IDCompositionSurface> m_surface;

A composição alvo representa a associação entre a janela da área de trabalho e uma árvore visual. Eu posso realmente associar duas árvores visuais com uma determinada janela, mas falarei mais sobre isso em uma coluna futura. O visual representa um nó em uma árvore visual. Eu irei explorar visuais em uma coluna subsequente, portanto, agora haverá apenas um visual de raiz. Aqui, estou apenas usando a interface IDCompositionVisual2, que é derivada da interface IDCompositionVisual usada em minha coluna anterior. Por fim, há uma superfície representando o conteúdo ou bitmap associado com o visual. Na minha coluna anterior, eu apenas usei uma cadeia de troca como o conteúdo de um visual, mas em breve irei mostrar como criar uma superfície de composição.

Para ilustrar como realmente renderizar algo e gerenciar os recursos de renderização, precisarei de mais algumas variáveis do membro:

ComPtr<ID2D1SolidColorBrush> m_brush; D2D_SIZE_F                   m_size; D2D_POINT_2F                 m_dpi; SampleWindow() : m_size(), m_dpi() { // RegisterClass / CreateWindowEx as before }

O pincel de cor sólida Direct2D é muito barato de criar, mas vários outros recursos de renderização não são tão leves. Eu usarei esta escova para ilustrar como criar recursos de renderização fora do ciclo de renderização. O API DirectComposition também poderá assumir a criação do alvo de renderização do Direct2D. Isso permite direcionar uma superfície de composição com o Direct2D, mas também significa perder um pouco de informação contextual. Especificamente, não é possível mais armazenar em cache o fator de dimensionamento de DPI aplicável no alvo de renderização porque o DirectComposition cria para você sob demanda. Além disso, não é mais possível confiar no método GetSize do alvo de renderização para relatar o tamanho da janela. Mas não se preocupe, eu irei mostrar como compensar estes retornos em breve.

Como com qualquer aplicativo que confia em um equipamento Direct3D, eu preciso ser cuidadoso ao gerenciar os recursos que residem no dispositivo físico, pressupondo que o dispositivo pode ser perdido a qualquer momento. O GPU pode travar, reiniciar, ser removido ou apenas falhar. Além disso, eu preciso ter cuidado para não responder inadequadamente a mensagens da janela que podem chegar antes da pilha do dispositivo ser criada. Eu usarei o ponteiro do dispositivo Direct3D para indicar se o dispositivo foi criado:

bool IsDeviceCreated() const { return m_device3d; }

Isso apenas ajuda a tornar a consulta explícita. Também usarei este ponteiro para iniciar uma reinicialização da pilha do dispositivo para forçar todos os recursos que dependem do dispositivo a serem recriados:

void ReleaseDeviceResources() { m_device3d.Reset(); }

Novamente, isso apenas ajuda a tornar esta operação explícita. Eu posso liberar todos os recursos que dependem do dispositivo aqui, mas isso não é estritamente necessário e pode rapidamente se tornar um problema de manutenção porque diferentes recursos são adicionados ou removidos. O problema do processo de criação do dispositivo está em outro método de ajuda:

void CreateDeviceResources() { if (IsDeviceCreated()) return; // Create devices and resources ... }

Está aqui no método CreateDeviceResources que eu posso criar ou recriar a pilha de dispositivos, o dispositivo de hardware e os vários recursos que a janela exige. Primeiro, eu crio o dispositivo Direct3D no qual tudo mais permanece:

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

Observe como o ponteiro da interface resultante é capturado pelo membro m_device3d. Agora, eu preciso consultar a interface DXGI do dispositivo:

ComPtr<IDXGIDevice> devicex; HR(m_device3d.As(&devicex));

Na minha coluna anterior, este foi o ponto em que eu criei a fábrica DXGI e a cadeia de troca a ser usada para composição. Eu criei uma cadeia de troca, envolvi em um bitmap do Direct2D, orientei o bitmap com um contexto de dispositivo e assim por diante. Aqui, irei fazer as coisas de uma forma um pouco diferente. Depois de criar o dispositivo Direct3D, irei criar um dispositivo Direct2D apontando para ele e criar um dispositivo DirectComposition apontando para o dispositivo Direct2D. Aqui está o dispositivo Direct2D:

ComPtr<ID2D1Device> device2d; HR(D2D1CreateDevice(devicex.Get(), nullptr, // Default properties device2d.GetAddressOf()));

Eu uso uma função de ajuda fornecida pelo API Direct2D em vez do objeto de fábrica Direct2D familiar. O dispositivo Direct2D resultante apenas herda o modelo de segmentação do dispositivo DXGI, mas pode substituir isso e ativar também o rastreamento de depuração. Aqui está o dispositivo DirectComposition:

HR(DCompositionCreateDevice2( device2d.Get(), __uuidof(m_device), reinterpret_cast<void **>(m_device.ReleaseAndGetAddressOf())));

Eu tenho o cuidado de usar o método ReleaseAndGet­AddressOf do membro m_device para suportar a recriação da pilha de dispositivo depois da perda do dispositivo. E tendo em conta o dispositivo de composição, agora posso criar a composição de destino como fiz na minha coluna anterior:

HR(m_device->CreateTargetForHwnd(m_window, true, // Top most m_target.ReleaseAndGetAddressOf()));

E o visual raiz:

HR(m_device->CreateVisual(m_visual.ReleaseAndGetAddressOf()));

Agora é momento de focar na superfície de composição que substitui a cadeia de troca. Da mesma forma que a fábrica DXGI não tem ideia do quão grande o armazenamento de uma cadeia de troca deve ser quando eu uso o método CreateSwapChainForComposition, o dispositivo DirectComposition não tem ideia de quão grande a superfície subjacente deve ser. Eu preciso consultar o tamanho da área do cliente da janela e usar essa informação para informar a criação da superfície:

RECT rect = {}; VERIFY(GetClientRect(m_window, &rect));

A estrutura RECT tem membros no lado esquerdo, superior, direito e inferior e eu posso usar para determinar o tamanho desejado da superfície a ser criada usando pixéis físicos:

HR(m_device->CreateSurface(rect.right - rect.left, rect.bottom - rect.top, DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_ALPHA_MODE_PREMULTIPLIED, m_surface.ReleaseAndGetAddressOf()));

Lembre-se de que a superfície pode ser maior do que o tamanho solicitado. Isso ocorre porque o mecanismo de composição pode agrupar ou arredondar alocações para maior eficiência. Isso não é um problema, mas afeta o contexto do dispositivo resultante porque não será possível confiar no seu método GetSize, mas falarei sobre isso em um momento.

Os parâmetros para o método CreateSurface são, felizmente, uma simplificação dos vários botões e teclas da estrutura DXGI_SWAP_CHAIN_DESC1. Depois do tamanho, eu especifico o formato de pixel e o modo alfa, e o dispositivo de composição retorna um ponteiro para a superfície de composição recentemente criada. Eu posso simplesmente definir esta superfície como o conteúdo do meu objeto visual e definir o visual como a raiz do meu destino de composição:

HR(m_visual->SetContent(m_surface.Get())); HR(m_target->SetRoot(m_visual.Get()));

No entanto, eu não preciso chamar o método Commit do dispositivo de composição neste estágio. Eu irei atualizar a superfície de composição em meu ciclo de renderização, mas essas mudanças terão efeito apenas quando o método Commit é chamado. Neste ponto, o mecanismo de composição está pronto para começar a renderizar, mas ainda tenho alguns terminais soltos para prender. Não tem nada a ver com a composição, mas tudo com usar de forma correta e eficiente o Direct2D para renderizar. Primeiro, quaisquer recursos específicos do destino de renderização como bitmaps e escovas devem ser criados fora do ciclo de renderização. Isso pode ser um pouco estranho porque o DirectComposition cria o destino de renderização. Felizmente, o único requisito é que esses recursos podem ser criados no mesmo espaço de endereço que o destino de renderização eventual, portanto, eu posso apenas criar um contexto de dispositivo descartável aqui para criar esse recurso:

ComPtr<ID2D1DeviceContext> dc; HR(device2d->CreateDeviceContext(D2D1_DEVICE_CONTEXT_OPTIONS_NONE, dc.GetAddressOf()));

Eu posso usar esse destino de renderização para criar a única escova do aplicativo:

D2D_COLOR_F const color = ColorF(0.26f, 0.56f, 0.87f, 0.5f); HR(dc->CreateSolidColorBrush(color, m_brush.ReleaseAndGetAddressOf()));

O contexto do dispositivo é descartado, mas a escova permanece reutilizável no ciclo de renderização. Isso é um pouco confuso, mas fará sentido em um momento. A última coisa que preciso fazer antes de renderizar é preencher as duas variáveis membros m_size e m_dpi. Tradicionalmente, um método GetSize do destino de renderização do Direct2D fornece o tamanho do destino de renderização em pixéis lógicos, anteriormente conhecidos como pixéis independentes do dispositivo. Este tamanho lógico já conta para o DPI efetivo, portanto, irei lidar com ele primeiro. Como ilustrado na minha coluna de fevereiro de 2014 sobre aplicativos de DPI alto, eu posso consultar o DPI real para uma determinada janela, determinando primeiro o monitor no qual uma janela específica predominantemente reside e obtendo o DPI efetivo para esse monitor. Aqui está como ele parecerá:

HMONITOR const monitor = MonitorFromWindow(m_window, MONITOR_DEFAULTTONEAREST); unsigned x = 0; unsigned y = 0; HR(GetDpiForMonitor(monitor, MDT_EFFECTIVE_DPI, &x, &y));

Eu posso armazenar em cache esses valores no meu membro m_dpi para que possa atualizar facilmente o contexto do dispositivo fornecido pelo API DirectComposition dentro do meu ciclo de renderização:

m_dpi.x = static_cast<float>(x); m_dpi.y = static_cast<float>(y);

Agora, calcular o tamanho lógico da área do cliente em pixels lógicos é apenas retirar a estrutura RECT que já mantém o tamanho em pixéis físico e fatorar nos valores de DPI efetivo que tenho em mãos:

m_size.width  = (rect.right - rect.left) * 96 / m_dpi.x; m_size.height = (rect.bottom - rect.top) * 96 / m_dpi.y;

Isso conclui o método CreateDeviceResources e toda a sua responsabilidade. Agora é possível ver como tudo é reunido na Figura 3, que mostra o método CreateDeviceResources inteiramente.

Figura 3 Criando a pilha de dispositivo

void CreateDeviceResources() { if (IsDeviceCreated()) return; HR(D3D11CreateDevice(nullptr,    // Adapter D3D_DRIVER_TYPE_HARDWARE, nullptr,    // Module D3D11_CREATE_DEVICE_BGRA_SUPPORT, nullptr, 0, // Highest available feature level D3D11_SDK_VERSION, m_device3d.GetAddressOf(), nullptr,    // Actual feature level nullptr));  // Device context ComPtr<IDXGIDevice> devicex; HR(m_device3d.As(&devicex)); ComPtr<ID2D1Device> device2d; HR(D2D1CreateDevice(devicex.Get(), nullptr, // Default properties device2d.GetAddressOf())); HR(DCompositionCreateDevice2( device2d.Get(), __uuidof(m_device), reinterpret_cast<void **>(m_device.ReleaseAndGetAddressOf()))); HR(m_device->CreateTargetForHwnd(m_window, true, // Top most m_target.ReleaseAndGetAddressOf())); HR(m_device->CreateVisual(m_visual.ReleaseAndGetAddressOf())); RECT rect = {}; VERIFY(GetClientRect(m_window, &rect)); HR(m_device->CreateSurface(rect.right - rect.left, rect.bottom - rect.top, DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_ALPHA_MODE_PREMULTIPLIED, m_surface.ReleaseAndGetAddressOf())); HR(m_visual->SetContent(m_surface.Get())); HR(m_target->SetRoot(m_visual.Get())); ComPtr<ID2D1DeviceContext> dc; HR(device2d->CreateDeviceContext(D2D1_DEVICE_CONTEXT_OPTIONS_NONE, dc.GetAddressOf())); D2D_COLOR_F const color = ColorF(0.26f, 0.56f, 0.87f, 0.5f); HR(dc->CreateSolidColorBrush(color, m_brush.ReleaseAndGetAddressOf())); HMONITOR const monitor = MonitorFromWindow(m_window, MONITOR_DEFAULTTONEAREST); unsigned x = 0; unsigned y = 0; HR(GetDpiForMonitor(monitor, MDT_EFFECTIVE_DPI, &x, &y)); m_dpi.x = static_cast<float>(x); m_dpi.y = static_cast<float>(y); m_size.width  = (rect.right - rect.left) * 96 / m_dpi.x; m_size.height = (rect.bottom - rect.top) * 96 / m_dpi.y; }

Antes de implementar os manipuladores de mensagem, eu preciso substituir o MessageHandler do modelo de classe do Window para indicar quais mensagens eu gostaria de lidar. Como mínimo, eu preciso lidar com a mensagem WM_PAINT onde irei fornecer os comandos de desenho; a mensagem WM_SIZE onde irei ajustar o tamanho da superfície; e a mensagem WM_DPICHANGED onde irei atualizar o DPI efetivo e o tamanho da janela. A Figura 4 mostra o MessageHandler e, como era de se esperar, simplesmente encaminha as mensagens nos manipuladores adequados.

Figura 4 Despachando mensagens

LRESULT MessageHandler(UINT message, WPARAM const wparam, LPARAM const lparam) { if (WM_PAINT == message) { PaintHandler(); return 0; } if (WM_SIZE == message) { SizeHandler(wparam, lparam); return 0; } if (WM_DPICHANGED == message) { DpiHandler(wparam, lparam); return 0; } return __super::MessageHandler(message, wparam, lparam); }

O manipulador WM_PAINT é onde eu crio os recursos de dispositivo sob demanda antes de entrar na sequência de desenho. Lembre-se de que CreateDeviceResources não faz nada se o dispositivo já existir:

void PaintHandler() { try { CreateDeviceResources(); // Drawing commands ... }

Desta forma, eu posso apenas reagir para a perda de dispositivo soltando o ponteiro do dispositivo Direct3D pelo método ReleaseDeviceResources e na próxima vez o manipulador WM_PAINT irá recriar tudo. Todo o processo é colocado em um bloco de teste para que qualquer falha do dispositivo possa ser tratada de forma confiável. Para começar a desenhar para a superfície de composição, eu preciso chamar seu método BeginDraw:

ComPtr<ID2D1DeviceContext> dc; POINT offset = {}; HR(m_surface->BeginDraw(nullptr, // Entire surface __uuidof(dc), reinterpret_cast<void **>(dc.GetAddressOf()), &offset));

O BeginDraw retorna um contexto do dispositivo—o destino de renderização do Direct2D—que usarei para agrupar os reais comandos de desenho. O API DirectComposition usa o dispositivo Direct2D originalmente fornecido ao criar o dispositivo de composição para criar e retornar o contexto do dispositivo aqui. Eu posso opcionalmente fornecer uma estrutura RECT nos pixéis físicos para prender a superfície ou especificar um nullptr para permitir acesso ilimitado para a superfície de desenho. O método BeginDraw também retorna uma compensação, novamente nos pixéis físicos, para indicar a origem da superfície de desenho pretendida. Isso não irá necessariamente estar no canto superior esquerdo da superfície e deve-se tomar cuidado ao ajustar ou transformar qualquer comando de desenho para que eles sejam compensados corretamente.

A superfície de composição também tem um método EndDraw e estes dois tomam o lugar dos métodos BeginDraw e EndDraw do Direct2D. Você não deve chamar os métodos correspondentes no contexto do dispositivo porque o API DirectComposition faz isso para você. Obviamente, o API DirectComposition também garante que o contexto do dispositivo tem a superfície de composição selecionada como seu destino. Além disso, é importante não segurar o contexto do dispositivo, mas liberar prontamente depois da conclusão do desenho. Além disso, a superfície não garante reter os conteúdos de qualquer estrutura anterior que pode ser desenhada, portanto, é necessário ter cuidado para limpar o destino ou desenhar novamente cada pixel antes de concluir.

O contexto do dispositivo resultante está pronto, mas não tem o fator de dimensionamento DPI efetivo da janela aplicado. Eu posso usar os valores DPI calculados anteriormente dentro do meu método CreateDeviceResources para atualizar o contexto do dispositivo agora:

dc->SetDpi(m_dpi.x, m_dpi.y);

Também basta usar uma matriz de transformação da conversão para ajustar os comandos do desenho dada a compensação exigida pelo API DirectComposition. Eu apenas preciso ter cuidado ao converter a compensação para pixéis lógicos porque isso é o que o Direct2D assume:

dc->SetTransform(Matrix3x2F::Translation(offset.x * 96 / m_dpi.x, offset.y * 96 / m_dpi.y));

Agora eu posso limpar o destino e desenhar algo específico do aplicativo. Aqui, eu desenho um retângulo simples com a escova dependente do dispositivo criada anteriormente no meu método CreateDeviceResources:

dc->Clear(); D2D_RECT_F const rect = RectF(100.0f, 100.0f, m_size.width - 100.0f, m_size.height - 100.0f); dc->DrawRectangle(rect, m_brush.Get(), 50.0f);

Estou confiando no membro m_size armazenado em cache em vez de qualquer tamanho relatado pelo método GetSize, porque os posteriores relatam o tamanho da superfície subjacente em vez da área do cliente.

Concluir a sequência de desenho envolve várias etapas. Primeiro, eu preciso chamar o método EndDraw na superfície. Isso diz ao Direct2D para concluir qualquer comando de desenho e gravar na superfície de composição. A superfície está pronta para ser composta—mas não antes do método Commit ser chamado no dispositivo de composição. Neste ponto, qualquer mudança na árvore visual, incluindo qualquer superfície atualizada, é agrupada e tornada disponível para o mecanismo de composição em uma única unidade transacional. Isso conclui o processo de renderização. A única pergunta restante é se o dispositivo Direct3D foi perdido. O método Commit irá relatar qualquer falha e o bloco de captura liberará o dispositivo. Se tudo ocorrer bem, posso dizer ao Windows que pintei com sucesso a janela validando toda a área do cliente da janela com a função ValidateRect. Caso contrário, eu preciso liberar o dispositivo. Aqui está como poderia ficar:

// Drawing commands ... HR(m_surface->EndDraw()); HR(m_device->Commit()); VERIFY(ValidateRect(m_window, nullptr)); } catch (ComException const & e) { ReleaseDeviceResources(); }

Eu não preciso repintar explicitamente, porque o Windows simplesmente continuará a enviar mensagens WM_PAINT se não responderem validando a área do cliente. O manipulador WM_SIZE é responsável por ajustar o tamanho da superfície de composição e também para atualizar o tamanho armazenado em cache do alvo renderizado. Eu não reagiria se o dispositivo não tiver sido criado ou se a janela for minimizada:

void SizeHandler(WPARAM const wparam, LPARAM const lparam) { try { if (!IsDeviceCreated()) return; if (SIZE_MINIMIZED == wparam) return; // ... }

Uma janela geralmente recebe uma mensagem WM_SIZE antes de ter uma oportunidade para criar uma pilha de dispositivo. Quando isso acontece, eu apenas ignoro a mensagem. Eu também ignoro a mensagem se a mensagem WM_SIZE é um resultado de uma janela minimizada. Eu não desejo ajustar desnecessariamente o tamanho da superfície neste caso. Como com o manipulador WM_PAINT, o identificador WM_SIZE abrange suas operações em um bloco de teste. Redimensionar, ou neste caso recriar, a superfície pode falhar devido à perda do dispositivo e isso pode resultar na pilha de dispositivo ser recriada. Primeiro, eu posso extrair o novo tamanho da área do cliente:

unsigned const width  = LOWORD(lparam); unsigned const height = HIWORD(lparam);

E atualizar o tamanho em cache em pixéis lógicos:

m_size.width  = width  * 96 / m_dpi.x; m_size.height = height * 96 / m_dpi.y;

A superfície de composição não pode ser redimensionada. Estou usando o que pode ser chamado de superfície não virtual. O mecanismo de composição também oferece superfícies virtuais que são redimensionáveis, mas falarei mais sobre isso na próxima coluna. Aqui, eu posso apenas liberar a superfície atual e recriá-la. Como as mudanças na árvore visual não são refletidas até serem confirmadas, o usuário não irá enfrentar qualquer oscilação enquanto a superfície for descartada e recriada. Aqui está como poderia ficar:

HR(m_device->CreateSurface(width, height, DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_ALPHA_MODE_PREMULTIPLIED, m_surface.ReleaseAndGetAddressOf())); HR(m_visual->SetContent(m_surface.Get()));

Eu posso responder a qualquer falha liberando os recursos do dispositivo para que a próxima mensagem WM_PAINT faça com que eles sejam recriados:

// ... } catch (ComException const & e) { ReleaseDeviceResources(); }

E isto conclui o manipulador WM_SIZE. A etapa final necessária é implementar o manipulador WM_DPICHANGED para atualizar o DPI efetivo e o tamanho da janela. O WPARAM da mensagem fornece novos valores de DPI e o LPARAM fornece o novo tamanho. Eu apenas atualizo a variável do membro m_dpi da janela e chamo o método SetWindowPos para atualizar o tamanho da janela. A janela receberá outra mensagem WM_SIZE que meu manipulador WM_SIZE usará para ajustar o membro m_size e recriar a superfície. A Figura 5 fornece um exemplo de como lidar com essas mensagens WM_DPICHANGED.

Figura 5 Lidar com atualizações do DPI

void DpiHandler(WPARAM const wparam, LPARAM const lparam) { m_dpi.x = LOWORD(wparam); m_dpi.y = HIWORD(wparam); RECT const & rect = *reinterpret_cast<RECT const *>(lparam); VERIFY(SetWindowPos(m_window, 0, // No relative window rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, SWP_NOACTIVATE | SWP_NOZORDER)); }

Estou feliz em ver os membros da família DirectX desenharem mais proximamente com a melhor interoperabilidade e desempenho, graças à profunda integração entre o Direct2D e DirectComposition. Espero que você esteja tão feliz como eu sobre as possibilidade da criação de aplicativos nativos avançados com o DirectX.

Kenny Kerr é programador de computador, assim como autor da Pluralsight e Microsoft MVP que mora no Canadá. Ele mantém um blog em kennykerr.ca e pode ser seguido no Twitter em twitter.com/kennykerr.

Agradecemos aos seguintes especialistas técnicos da Microsoft pela revisão deste artigo: Leonardo Blanco e James Clarke