Este artigo foi traduzido por máquina.

Windows com C++

Renderização em uma aplicação Desktop com Direct2D

Kenny Kerr

 

Kenny KerrNo meu última coluna, mostrei-lhe como realmente é fácil criar uma aplicação desktop com C++ sem qualquer biblioteca ou framework.Na verdade, se você foram sentindo especialmente masoquista, você poderia escrever um aplicativo de desktop inteiro de dentro de sua função WinMain como eu fiz Figura 1.Evidentemente, essa abordagem simplesmente não escala.

 

 

Figura 1: A masoquista janela

int __stdcall wWinMain(HINSTANCE module, HINSTANCE, PWSTR, int)
{
  WNDCLASS wc = {};
  wc.hCursor = LoadCursor(nullptr, IDC_ARROW);
  wc.hInstance = module;
  wc.lpszClassName = L"window";
  wc.lpfnWndProc = [] (HWND window, UINT message, WPARAM
    wparam, LPARAM lparam) -> LRESULT
  {
    if (WM_DESTROY == message)
    {
      PostQuitMessage(0);
      return 0;
    }
    return DefWindowProc(window, message, wparam, lparam);
  };
  RegisterClass(&wc);
  CreateWindow(wc.lpszClassName, L"Awesome?!",
    WS_OVERLAPPEDWINDOW | WS_VISIBLE, CW_USEDEFAULT, CW_USEDEFAULT,
    CW_USEDEFAULT, CW_USEDEFAULT, nullptr, nullptr, module, nullptr);
  MSG message;
  BOOL result;
  while (result = GetMessage(&message, 0, 0, 0))
  {
    if (-1 != result) DispatchMessage(&message);
  }
}

Mostrei também como a Biblioteca ATL (Active Template) fornece uma abstração de C++ agradável para esconder muita esta maquinaria e como a biblioteca de modelos do Windows (WTL) leva isso mesmo ainda mais, principalmente para aplicações fortemente investido nas abordagens usuário e GDI para desenvolvimento de aplicativos (consulte minha coluna de fevereiro em msdn.microsoft.com/magazine/jj891018).

É o futuro do processamento de aplicativo no Windows hardware -­aceleração Direct3D, mas que realmente é impraticável para trabalhar diretamente com se tudo o que você quer fazer é processar um jogo ou aplicativo bidimensional. É aí que entra o Direct2D. Apresentei brevemente Direct2D quando ele foi anunciado pela primeira vez alguns anos atrás, mas eu vou passar os próximos meses, tendo um olhar muito atento para desenvolvimento de Direct2D. Confira em minha coluna de junho de 2009, "Introduzindo o Direct2D" (msdn.microsoft.com/magazine/dd861344), para uma introdução à arquitetura e fundamentos do Direct2D.

Dentre os fundamentos de projeto-chave do Direct2D é que se concentra na renderização e deixa os outros aspectos de desenvolvimento de aplicativos do Windows para você ou outras bibliotecas que você pode empregar. Embora o Direct2D foi projetado para processar em uma janela do desktop, cabe a você fornecer esta janela e otimizá-lo para renderização de Direct2D realmente. Então este mês, eu vou concentrar a única relação entre Direct2D e a janela do aplicativo de desktop. Você pode fazer muitas coisas para otimizar a janela tratamento e processo de renderização. Você quer reduzir a pintura desnecessária e evitar tremulação e apenas fornecer a melhor experiência possível para o usuário. Claro, você também vai querer fornecer um quadro gerenciável desenvolver seu aplicativo. Eu vou abordar estas questões aqui.

A janela do Desktop

O exemplo de ATL no mês passado, eu dei o exemplo de uma classe de janela, derivando o modelo de classe ATL CWindowImpl. Bem, tudo está contido dentro da classe de janela do aplicativo. No entanto, o que acaba acontecendo é que um monte de janela e renderização encanamento acaba intercaladas com a janela application-specific processamento e manipulação de eventos. Para resolver este problema, que tendem a empurrar tanto este código clichê como é prático em uma classe base, usando polimorfismo em tempo de compilação para chegar até a classe de janela do aplicativo, quando esta classe base precisa de sua atenção. Esta abordagem é usada bastante por ATL e WTL, então por que não estender que para suas próprias classes?

Figura 2 ilustra essa separação. A classe base é o Desktop­modelo de classe de janela. O parâmetro do modelo dá a classe base a capacidade de chamar a classe concreta sem o uso de funções virtuais. Neste caso, é utilizar esta técnica para esconder um monte de render específicas de pré e pós-processamento ao chamar até a janela do aplicativo para executar as operações de desenho reais. Eu vou expandir o modelo de classe de DesktopWindow em um momento, mas em primeiro lugar, seu registro de classe de janela precisa um pouco de trabalho.

Figura 2 a janela área de trabalho

template <typename T>
class DesktopWindow :
  public CWindowImpl<DesktopWindow<T>, CWindow,
    CWinTraits<WS_OVERLAPPEDWINDOW | WS_VISIBLE>>
{
  BEGIN_MSG_MAP(DesktopWindow)
    MESSAGE_HANDLER(WM_PAINT, PaintHandler)
    MESSAGE_HANDLER(WM_DESTROY, DestroyHandler)
  END_MSG_MAP()
  LRESULT DestroyHandler(UINT, WPARAM, LPARAM, BOOL &)
  {
    PostQuitMessage(0);
    return 0;
  }
  LRESULT PaintHandler(UINT, WPARAM, LPARAM, BOOL &)
  {
    PAINTSTRUCT ps;
    VERIFY(BeginPaint(&ps));
    Render();
    EndPaint(&ps);
    return 0;
  }
  void Render()
  {
    ...
static_cast<T *>(this)->Draw();
    ...
}
    ...
};
struct SampleWindow : DesktopWindow<SampleWindow>
{
  void Draw()
  {
    ...
}
};

Otimizando a classe de janela

Uma das realidades da API para aplicativos de desktop do Windows é que ele foi projetado para simplificar o processamento com recursos tradicionais do usuário e GDI . Alguns destes "conveniências" precisam ser desabilitada para permitir que o Direct2D para assumir, para evitar desnecessária pintura levando a cintilação unsightly. Outros esses padrões também deve ser mexido para trabalhar de uma forma que melhor se adapte às Direct2D renderização. Muito disso pode ser alcançado ao alterar as informações de classe de janela antes ele é registrado, mas você deve ter notado que o ATL esconde isso do programador. Felizmente, ainda há uma maneira de conseguir isso.

No mês passado, mostrei como a API do Windows espera que uma estrutura de classe de janela a ser registrado antes de uma janela é criada com base em suas especificações. Um dos atributos de uma classe de janela é o seu pincel de plano de fundo. O Windows usa esse pincel GDI para limpar a área cliente da janela antes que a janela começa a pintura. Isto foi conveniente nos dias do usuário e GDI , mas é desnecessário e a causa de alguma cintilação para aplicações de Direct2D. Uma maneira simples de evitar isso é definindo a pega da escova de fundo a estrutura de classe de janela para um nullptr. Se desenvolver em um computador relativamente rápido do Windows 7 ou o Windows 8, você pode pensar que isto é desnecessário porque você não notar qualquer cintilação. Isso é só porque o moderno Windows desktop é composto de forma eficiente em gráficos de processamento unidade (GPU) que é difícil de pegá-lo. No entanto, é bastante fácil de esticar o pipeline de renderização para exagerar o efeito que você pode experimentar em máquinas mais lentas. Se o pincel de plano de fundo da janela é branco e, em seguida, pintar pincel de área com um contraste preto de cliente da janela, adicionando um pouco de atraso de sono — em qualquer lugar entre 10 ms e 100 ms — você não vai ter nenhum problema pegando o impressionante flicker. Assim como evitá-la?

Como já referi, se o seu registro de classe de janela não possui um pincel de plano de fundo, o Windows não terá qualquer escova com que limpar sua janela. No entanto, você deve ter notado nos exemplos ATL que o registro de classe de janela é completamente escondido. Uma solução comum é manipular a mensagem WM_ERASEBKGND que padrão de processamento de janela alças — cortesia a função DefWindowProc — pintando a área de cliente da janela com o pincel de base de classe de janela. Se você lida com esta mensagem, retornando true, em seguida, nenhuma pintura ocorre. Esta é uma razão­solução capaz, como esta mensagem é enviada para uma janela independentemente se a classe de janela tem uma escova de fundo válido ou não. Outra solução é apenas evitar esse manipulador não-op e retire a escova de fundo a classe de janela em primeiro lugar. Felizmente, o ATL torna relativamente simples para substituir esta parte da criação da janela. Durante a criação do ATL chama o método GetWndClassInfo na janela para obter essas informações de classe de janela. Você pode fornecer sua própria implementação deste método, mas a ATL fornece uma útil macro que implementa-lo para você:

DECLARE_WND_CLASS_EX(nullptr, CS_HREDRAW | CS_VREDRAW, -1);

O último argumento para essa macro deve ser uma constante de escova, mas o valor de-1 engana-lo limpar esse atributo da estrutura de classe de janela. Uma maneira infalível para determinar se o plano de fundo da janela foi apagado é verificar o PAINTSTRUCT preenchido pela função BeginPaint dentro de seu manipulador de WM_PAINT. Se seu membro fErase for false, então você sabe que Windows cancelou o plano de fundo da sua janela, ou pelo menos que algum código respondeu a mensagem WM_ERASEBKGND e pretendia desmarcá-la. Se o manipulador de mensagem WM_ERASEBKGND não ou não é capaz de limpar o fundo, então é até o manipulador de mensagem WM_PAINT para fazê-lo. No entanto, aqui pode empregar Direct2D completamente tomar sobre o processamento da área cliente da janela e evitar esta pintura dupla. Apenas certifique-se de chamar a função EndPaint, assegurando o Windows que você na verdade pintar sua janela, caso contrário o Windows irá continuar a importunar você com um fluxo desnecessário de WM_PAINT mensagens. Isso, claro, iria prejudicar o desempenho do aplicativo e aumentar o poder global consumo.

O outro aspecto das informações de classe de janela que merece nossa atenção é os estilos de classe de janela. Isto é, na verdade, o que é o segundo argumento para a macro anterior para. Os estilos CS_HREDRAW e CS_VREDRAW com que a janela a ser invalidado sempre que a janela é redimensionada verticalmente e horizontalmente. Isso certamente não é necessário. Você poderia, por exemplo, manipular a mensagem WM_SIZE e invalidar a janela lá, mas eu sempre estou contente quando o Windows vai poupar-me de escrever algumas linhas extras de código. De qualquer forma, se você negligencia invalidar a janela, em seguida, Windows não vai enviar sua janela quaisquer mensagens WM_PAINT quando o tamanho da janela é reduzido. Isso pode ser bom se você está feliz para o conteúdo da janela a ser recortado, mas é comum estes dias pintar vários ativos da janela em relação ao tamanho da janela. O que você prefere, isso é uma decisão explícita que você precisa fazer para a janela do aplicativo.

Enquanto eu estou sobre o tema de fundo do janela, muitas vezes é desejável para invalidar uma janela explicitamente. Isso permite que você mantenha o processamento da sua janela, enraizado na mensagem WM_PAINT, em vez de ter que lidar com pintura em lugares diferentes e em caminhos de código diferentes através do seu aplicativo. Você pode querer pintar algo em resposta a um clique do mouse. Você poderia, evidentemente, fazer a renderização ali mesmo no manipulador de mensagens. Alternativamente, você poderia simplesmente invalidar a janela e deixe o manipulador WM_PAINT processar o estado atual do aplicativo. Este é o papel da função InvalidateRect. ATL fornece o método Invalidate que envolve apenas a esta função. O que muitas vezes confunde os desenvolvedores sobre esta função é como lidar com o parâmetro "apagar". Sabedoria popular parece ser que a dizer "sim" para apagar fará com que a janela a ser redesenhado imediatamente e dizendo "não" irá adiar isso de alguma forma. Isso não é verdade, e a documentação diz tanto. Invalidar uma janela fará com que a ser redesenhado imediatamente. A opção de apagar susbtitui a função DefWindowProc, que normalmente seria limpar o fundo da janela. Se apagar, então a chamada subseqüente para BeginPaint irá limpar o fundo da janela. Aqui, então, é uma outra razão para evitar o pincel de base de classe de janela inteiramente em vez de depender de um manipulador de mensagem WM_ERASEBKGND. Sem um pincel de plano de fundo, BeginPaint novamente nada tem para pintar com, então a opção de apagar não tem efeito. Se você deixar o ATL definir um pincel de plano de fundo para sua classe de janela e, em seguida, você precisa ser cuidadoso ao invalidar sua janela porque isto introduzirá novamente a cintilação. Eu adicionei este membro protegido para o modelo de classe DesktopWindow para essa finalidade:

void Invalidate()
{
  VERIFY(InvalidateRect(nullptr, false));
}

Também é uma boa idéia para manipular a mensagem WM_DISPLAYCHANGE para invalidar a janela. Isso garante que a janela é redesenhada corretamente deve algo sobre o display mudança afetam a aparência da janela.

Executar o aplicativo

Eu gosto de manter WinMain função meu aplicativo relativamente simples. Para atingir este objetivo, eu adicionei um método de execução público para o modelo de classe de DesktopWindow para esconder a janela inteira e criação de fábrica de Direct2D, bem como o loop de mensagem. O DesktopWindow executar método é mostrado na Figura 3. Isto deixa-me escrever o WinMain função meu aplicativo muito simplesmente:

int __stdcall wWinMain(HINSTANCE, HINSTANCE, PWSTR, int)
{
  SampleWindow window;
  return window.Run();
}

Figura 3 o método Run DesktopWindow

int Run()
{
  D2D1_FACTORY_OPTIONS fo = {};
  #ifdef DEBUG
  fo.debugLevel = D2D1_DEBUG_LEVEL_INFORMATION;
  #endif
  HR(D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED,
                       fo,
                       m_factory.GetAddressOf()));
  static_cast<T *>(this)->CreateDeviceIndependentResources();
  VERIFY(__super::Create(nullptr, nullptr, L"Direct2D"));
  MSG message;
  BOOL result;
  while (result = GetMessage(&message, 0, 0, 0))
  {
    if (-1 != result)
    {
      DispatchMessage(&message);
    }
  }
  return static_cast<int>(message.wParam);
}

Antes de criar a janela, me preparar as opções de fábrica Direct2D, permitindo que a camada de depuração para compilações de depuração. Eu recomendo que você faça o mesmo, pois permite Direct2D traçar todos os tipos de diagnóstico útil ao desenvolver seu aplicativo. A função D2D1CreateFactory retorna o ponteiro de interface de fábrica que eu entregue ao ponteiro na biblioteca de tempo de execução do Windows excelente ComPtr inteligente, um membro protegido da classe DesktopWindow. Eu então chamar o método CreateDeviceIndependentResources para criar os recursos de dispositivo independente — coisas como geometrias e estilos de traçado que pode ser reutilizado durante toda a vida do aplicativo. Apesar de eu permitir que as classes derivadas para substituir esse método, eu forneço um esboço vazio no modelo de classe de DesktopWindow se isso não é necessário. Finalmente, o método Run conclui bloqueando com um loop de mensagem simples. Confira a coluna do mês passado para uma explicação sobre o loop de mensagem.

O destino de processamento

O destino de processamento do Direct2D deve ser criado sob demanda dentro o Render método chamado como parte do manipulador de mensagem WM_PAINT. Ao contrário de alguns outros destinos de processamento de Direct2D, é perfeitamente possível que o dispositivo — uma GPU na maioria dos casos — que fornece a renderização acelerada por hardware para uma janela do desktop pode desaparecer ou mudar de alguma forma a tornar todos os recursos alocados pelo alvo inválido. Devido à natureza do processamento de modo imediato no Direct2D, o aplicativo é responsável por controlar quais recursos são específicas do dispositivo e talvez precise ser recriado de vez em quando. Felizmente, isso é bastante fácil de gerenciar. Figura 4 fornece o método DesktopWindow Render completo.

Figura 4 o método DesktopWindow Render

void Render()
{
  if (!m_target)
  {
    RECT rect;
    VERIFY(GetClientRect(&rect));
    auto size = SizeU(rect.right, rect.bottom);
    HR(m_factory->CreateHwndRenderTarget(RenderTargetProperties(),
      HwndRenderTargetProperties(m_hWnd, size),
      m_target.GetAddressOf()));
    static_cast<T *>(this)->CreateDeviceResources();
  }
  if (!(D2D1_WINDOW_STATE_OCCLUDED & m_target->CheckWindowState()))
  {
    m_target->BeginDraw();
    static_cast<T *>(this)->Draw();
    if (D2DERR_RECREATE_TARGET == m_target->EndDraw())
    {
      m_target.Reset();
    }
  }
}

O método Render começa verificando se o ComPtr gestão interface de COM o destino é válido. Desta forma, ele apenas recria o destino de processamento quando necessário. Isso vai acontecer pelo menos uma vez a primeira vez que a janela é processada. Se algo acontecer para o dispositivo subjacente, ou por qualquer motivo o render destino precisa ser recriado, em seguida, o método de EndDraw no final do método Render Figura 4 retornará a constante D2DERR_RECREATE_TARGET. O método ComPtr Reset é usado para simplesmente liberar o destino de processamento. Da próxima vez que a janela é convidada a pintar-se, o Render método atravessarão os movimentos de criação de um novo Direct2D alvo processado.

Ele começa fazendo com que a área cliente da janela em pixels físicos. Direct2D para a maior parte usa pixels lógicas exclusivamente para permitir a sustentação exibe alta PPP naturalmente. Este é o ponto em que inicia o relacionamento entre a exibição física e seu sistema de coordenadas lógico. Ele chama a Direct2D fábrica para criar o objeto de destino de processamento. É neste ponto que ele chama a classe de janela do aplicativo derivado criar quaisquer recursos específicos do dispositivo — coisas como escovas e bitmaps dependentes de dispositivo subjacente o destino de processamento. Novamente, um esboço de vazio é fornecido pela classe DesktopWindow, se isso não é necessário.

Antes de desenho, o Render método verifica que a janela é realmente visível e não completamente obstruído. Isso evita qualquer processamento desnecessário. Normalmente, isso só acontece quando a cadeia de permuta de DirectX subjacente é invisível, como quando o usuário bloqueia ou switches desktops. Os métodos BeginDraw e EndDraw straddle, em seguida, a chamada ao método de sorteio da janela do aplicativo. Direct2D aproveita a oportunidade para lote de geometrias em um buffer de vértice, coalescem desenho comandos, e assim por diante, para proporcionar a maior produtividade e desempenho na GPU.

O último passo crítico para integrar o Direct2D corretamente com uma janela do desktop é redimensionar o processamento de destino quando a janela é redimensionada. Eu já falei sobre como a janela é automaticamente invalidada para garantir que ele é prontamente repintado, mas o destino de render-se não tem idéia que dimensões da janela foram alterados. Felizmente, isso é bastante fácil de fazer, como Figura 5 ilustra.

Figura 5 alvo de redimensionamento

MESSAGE_HANDLER(WM_SIZE, SizeHandler)
LRESULT SizeHandler(UINT, WPARAM, LPARAM lparam, BOOL &)
{
  if (m_target)
  {
    if (S_OK != m_target->Resize(SizeU(LOWORD(lparam),
      HIWORD(lparam))))
    {
      m_target.Reset();
    }
  }
  return 0;
}

Assumindo que a ComPtr detém actualmente um ponteiro de interface de destino COM render válido, método de redimensionar o destino é chamado com o novo tamanho, em conformidade com LPARAM a janela de mensagem. Se por algum motivo que o destino de processamento não é possível redimensionar todos os seus recursos internos, e o ComPtr é simplesmente redefinir, forçar o destino de processamento para ser recriado o processamento de tempo seguinte é solicitada.

E isso é tudo que eu tenho espaço para na coluna deste mês. Agora você tem tudo que você precisa para criar e gerenciar uma janela do desktop, bem como para usar a GPU para renderizar na janela do seu aplicativo. Se junte a mim no próximo mês como eu continuar a explorar o Direct2D.

Kenny Kerr é programador de computador, 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 ao seguinte especialista técnico pela revisão deste artigo: Worachai Chaoweeraprasit