Windows com C++

Explorando fontes com o DirectWrite e o C++ moderno

Kenny Kerr

Kenny KerrO DirectWrite é uma API de layout de texto incrivelmente poderosa. Potencializa praticamente todos os aplicativos e tecnologias líderes do Windows, da implementação do Tempo de Execução do Windows (WinRT) do XAML e do Office 2013 ao Internet Explorer 11 e muito mais. Não é um mecanismo de renderização, mas tem um relacionamento estreito com o Direct2D, seu irmão na família DirectX. Naturalmente, o Direct2D é a principal API gráfica acelerada por hardware e de modo imediato.

Você pode usar o DirectWrite com o Direct2D para fornecer renderização de texto acelerada por hardware. Para evitar confusão, não escrevi muito sobre o DirectWrite no passado. Eu não queria que você pensasse que o Direct2D fosse o único mecanismo de renderização do DirectWrite. O Direct2D é muito mais do que isso. O DirectWrite ainda tem muito a oferecer e, na coluna deste mês, mostrarei um pouco do que é possível com o DirectWrite e examinarei como o C++ moderno pode ajudar a simplificar o modelo de programação.

A API do DirectWrite

Usarei o DirectWrite para explorar a coleção de fontes do sistema. Primeiro, precisarei utilizar o objeto de fábrica do DirectWrite. Esse é o ponto de partida de qualquer aplicativo que deseje usar a impressionante tipografia do DirectWrite. O DirectWrite, como muito da API do Windows, conta com os elementos essenciais do COM. Preciso chamar a função DWriteCreateFactory para criar o objeto de fábrica do DirectWrite. Essa função retorna uma interface do COM que aponta para o objeto de fábrica:

ComPtr<IDWriteFactory2> factory;

A interface do IDWriteFactory2 é a versão mais recente da interface de fábrica do DirectWrite introduzida com o Windows 8.1 e o DirectX 11.2 no início deste ano. O IDWriteFactory2 herda do IDWrite­Factory1 que, por sua vez, herda de IDWriteFactory. O último é a interface de fábrica do DirectWrite que expõe a maior parte dos recursos de fábrica.

Dado o modelo de classe ComPtr anterior, chamarei a função DWriteCreateFactory:

HR(DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED,
  __uuidof(factory),
  reinterpret_cast<IUnknown **>(factory.GetAddressOf())));

O DirectWrite inclui um serviço do Windows chamado Serviços de Cache de Fontes do Windows (FontCache). O primeiro parâmetro indica se a fábrica resultante contribui com o uso de fontes para este cache entre processos. As duas opções são DWRITE_FACTORY_TYPE_SHARED e DWRITE_FACTORY_TYPE_ISOLATED. As fábricas SHARED e ISOLATED podem tirar proveito dos dados de fontes que já estão no cache. Apenas as fábricas SHARED contribuem com dados de fontes de volta no cache. O segundo parâmetro indica especificamente qual versão da interface de fábrica do DirectWrite desejo que seja retornada no terceiro e último parâmetro.

Dado o objeto de fábrica do DirectWrite, posso ir direto ao ponto e pedi-lo para a coleção de fontes do sistema:

ComPtr<IDWriteFontCollection> fonts;
HR(factory->GetSystemFontCollection(fonts.GetAddressOf()));

O método GetSystemFontCollection tem um segundo parâmetro opcional que informa se ele deve verificar se há atualizações ou alterações no conjunto de fontes instaladas. Felizmente, esse parâmetro usa o padrão de false, e não preciso pensar a respeito, a menos que eu queira garantir que as alterações recentes sejam refletidas na coleção resultante. Dada a coleção de fontes, posso obter o número de famílias de fontes na coleção da seguinte maneira:

unsigned const count = fonts->GetFontFamilyCount();

Em seguida, uso o método GetFontFamily para recuperar objetos da família de fontes individual usando um índice baseado em zero. Um objeto de família de fontes representa um conjunto de fontes que compartilham um nome e, naturalmente, um design, mas que são diferenciadas por peso, estilo e ampliação.

ComPtr<IDWriteFontFamily> family;
HR(fonts->GetFontFamily(index, family.GetAddressOf()));

A interface IDWriteFontFamily herda da interface IDWriteFontList, portanto, posso enumerar as fontes individuais dentro da família de fontes. É razoável e útil ser capaz de recuperar o nome da família de fontes. No entanto, os nomes de famílias são localizados e, portanto, isso não é tão simples como você pode esperar. Primeiro solicito a família de fontes para um objeto de sequências de caracteres localizado que conterá um nome de família por localidade suportada:

ComPtr<IDWriteLocalizedStrings> names;
HR(family->GetFamilyNames(names.GetAddressOf()));

Também posso enumerar os nomes das famílias, mas é comum simplesmente procurar o nome da localidade padrão do usuário. Na verdade, a interface IDWriteLocalizedStrings fornece o método FindLocaleName para recuperar o índice do nome localizado da família. Começarei chamando a função GetUserDefaultLocaleName para obter a localidade padrão do usuário:

wchar_t locale[LOCALE_NAME_MAX_LENGTH];
VERIFY(GetUserDefaultLocaleName(locale, countof(locale)));

Em seguida, passarei isso para o método FindLocaleName de IDWriteLocalizedStrings para determinar se a família de fontes tem um nome localizado para o usuário atual:

unsigned index;
BOOL exists;
HR(names->FindLocaleName(locale, &index, &exists));

Se a localidade solicitada não existir na coleção, posso retornar para algum padrão, como “en-us.” Supondo que exista, posso usar o método GetString de IDWriteLocalizedStrings para obter uma cópia:

if (exists)
{
  wchar_t name[64];
  HR(names->GetString(index, name, _countof(name)));
}

Se você estiver preocupado com o comprimento, poderá chamar primeiro o método GetString­Length. Basta ter certeza de que você tem um buffer grande o suficiente. A Figura 1 apresenta uma lista completa que mostra como tudo isso é reunido para enumerar as fontes instaladas.

Figura 1 Enumerando fontes com a API DirectWrite

ComPtr<IDWriteFactory2> factory;
HR(DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED,
  __uuidof(factory),
  reinterpret_cast<IUnknown **>(factory.GetAddressOf())));
ComPtr<IDWriteFontCollection> fonts;
HR(factory->GetSystemFontCollection(fonts.GetAddressOf()));
wchar_t locale[LOCALE_NAME_MAX_LENGTH];
VERIFY(GetUserDefaultLocaleName(locale, _countof(locale)));
unsigned const count = fonts->GetFontFamilyCount();
for (unsigned familyIndex = 0; familyIndex != count; ++familyIndex)
{
  ComPtr<IDWriteFontFamily> family;
  HR(fonts->GetFontFamily(familyIndex, family.GetAddressOf()));
  ComPtr<IDWriteLocalizedStrings> names;
  HR(family->GetFamilyNames(names.GetAddressOf()));
  unsigned nameIndex;
  BOOL exists;
  HR(names->FindLocaleName(locale, &nameIndex, &exists));
  if (exists)
  {
    wchar_t name[64];
    HR(names->GetString(nameIndex, name, countof(name)));
    wprintf(L"%s\n", name);
  }
}

Um toque do C++ moderno

Se você for um leitor habitual, saberá que dei ao DirectX e ao Direct2D, em particular, o tratamento do C++ moderno. O cabeçalho dx.h (dx.codeplex.com) também cobre o DirectWrite. Você pode usá-lo para simplificar o código que apresentei até aqui muito radicalmente. Em vez de chamar o DWriteCreateFactory, posso simplificar a função CreateFactory no namespace do DirectWrite:

auto factory = CreateFactory();

Obter a coleção de fontes do sistema também é muito simples:

auto fonts = factory.GetSystemFontCollection();

A enumeração dessa coleção é o local onde o dx.h realmente se destaca. Não preciso escrever um loop for tradicional. Não preciso chamar os métodos GetFontFamilyCount e GetFontFamily. Posso simplesmente escrever um loop for moderno baseado em intervalo:

for (auto family : fonts)
{
  ...
}

Esse é realmente igual ao código anterior. O compilador (com a ajuda de dx.h) o está gerando para mim e eu uso um modelo de programação muito mais natural, facilitando a escrita de código correto e eficiente. O método GetSystemFontCollection anterior retorna uma classe FontCollection que inclui um iterador que irá buscar indolentemente objetos de famílias de fontes. Isso permite que o compilador implemente eficientemente o loop baseado em intervalo. A Figura 2 fornece uma lista completa. Compare-a com o código da Figura 1 para apreciar a clareza e o potencial para produtividade.

Figura 2 Enumerando fontes com dx.h

auto factory = CreateFactory();
auto fonts = factory.GetSystemFontCollection();
wchar_t locale[LOCALE_NAME_MAX_LENGTH];
VERIFY(GetUserDefaultLocaleName(locale, _countof(locale)));
for (auto family : fonts)
{
  auto names = family.GetFamilyNames();
  unsigned index;
  if (names.FindLocaleName(locale, index))
  {
    wchar_t name[64];
    names.GetString(index, name);
    wprintf(L"%s\n", name);
  }
}

Uma navegador de fontes com o Tempo de Execução do Windows

O DirectWrite faz muito mais do que simplesmente enumerar fontes. Vou utilizar o que mostrei a você até agora, combinar com o Direct2D e criar um aplicativo simples de navegador de fontes. Em minha coluna de agosto de 2013 (msdn.microsoft.com/magazine/dn342867), mostrei como escrever os detalhes técnicos básicos do modelo de aplicativo do WinRT no C++ padrão. Em minha coluna de outubro de 2013 (msdn.microsoft.com/magazine/dn451437), mostrei como renderizar dentro desse aplicativo baseado no CoreWindow com o DirectX e especificamente o Direct2D. Agora vou mostrar como estender esse código para usar o Direct2D para renderizar texto com a ajuda do DirectWrite.

Desde que escrevi essas colunas, o Windows 8.1 foi liberado, e isso mudou algumas coisas na maneira como o ajuste de DPI é tratado em aplicativos modernos e de área de trabalho. Vou abordar o DPI em detalhes no futuro, portanto, não falarei sobre essas alterações por enquanto. Vou me concentrar simplesmente em ampliar a classe SampleWindow que comecei em agosto e estendi em outubro para dar suporte à renderização de texto e à navegação simples de fontes.

A primeira coisa a fazer é adicionar a classe Factory2 do DirectWrite como uma variável de membro:

DirectWrite::Factory2 m_writeFactory;

Dentro do método CreateDeviceIndependentResources de SampleWindow, posso criar a fábrica do DirectWrite:

m_writeFactory = DirectWrite::CreateFactory();

Também posso obter a coleção de fontes do sistema e a localidade padrão do usuário ali, em preparação para a enumeração das famílias de fontes:

auto fonts = m_writeFactory.GetSystemFontCollection();
wchar_t locale[LOCALE_NAME_MAX_LENGTH];
VERIFY(GetUserDefaultLocaleName(locale, _countof(locale)));

Vou fazer o aplicativo percorrer as fontes conforme o usuário pressionar as teclas de seta para cima e para baixo. Em vez de enumerar a coleção continuamente por meio das interfaces do COM, simplesmente copiarei os nomes das famílias de fontes em um contêiner de conjuntos padrão:

set<wstring> m_fonts;

Agora posso simplesmente usar o mesmo loop for baseado em intervalo da Figura 2 em CreateDeviceIndependentResources para adicionar os nomes ao conjunto:

m_fonts.insert(name);

Com o conjunto populado, iniciarei o aplicativo com um iterador apontando para o início do conjunto. Armazenarei o iterador como uma variável de membro:

set<wstring>::iterator m_font;

O método CreateDeviceIndependentResources de SampleWindow conclui inicializando o iterador e chamando o método CreateTextFormat, que definirei daqui a pouco:

m_font = begin(m_fonts);
CreateTextFormat();

Para que o Direct2D possa desenhar algum texto para mim, preciso criar um objeto de formato de texto. Para isso, preciso do nome da família de fontes e do tamanho da fonte desejado. Vou permitir que o usuário altere o tamanho da fonte com as teclas de seta para a direita e para a esquerda, portanto, começarei adicionando uma variável membro para controlar o tamanho:

float m_size;

O compilador do Visual C++ logo permitirá que eu inicialize membros de dados não estáticos, como os dessa classe. Por enquanto, preciso defini-los para algum padrão razoável no construtor de SampleWindow. Em seguida, preciso definir o método CreateTextFormat. É apenas um wrapper em torno do método de fábrica do DirectWrite do mesmo nome, mas ele atualiza uma variável de membro que o Direct2D pode usar para definir o formato do texto a ser desenhado:

TextFormat m_textFormat;

Em seguida, o método CreateTextFormat simplesmente recupera o nome da família de fontes do iterador de conjuntos e combina-o com o tamanho da fonte atual para criar um novo objeto de formato de texto:

void CreateTextFormat()
{
  m_textFormat = m_writeFactory.CreateTextFormat(m_font->c_str(),m_size);
}

Eu o encapsulei, de forma que, além de chamá-lo inicialmente no fim de CreateDeviceIndependentResources, também posso chamá-lo toda vez que o usuário pressionar uma das teclas de seta para alterar a família de fontes ou o tamanho. Isso leva à questão de como tratar pressionamentos de teclas no modelo de aplicativos WinRT. Em um aplicativo de área de trabalho, isso envolve o manuseio da mensagem de WM_KEYDOWN. Felizmente, o CoreWindow fornece o evento KeyDown, que é o equivalente moderno dessa mensagem. Começarei definindo a interface IKeyEventHandler que meu SampleWindow precisará implementar:

typedef ITypedEventHandler<CoreWindow *, KeyEventArgs *> IKeyEventHandler;

Em seguida, posso simplesmente adicionar essa interface à minha lista de interfaces herdadas de SampleWindow e atualizar minha implementação de QueryInterface de maneira correspondente. Em seguida, preciso fornecer sua implementação de Invoke:

auto __stdcall Invoke(
  ICoreWindow *,IKeyEventArgs * args) -> HRESULT override
{
  ...
  return S_OK;
}

A interface IKeyEventArgs fornece muitas das mesmas informações que o LPARAM e o WPARAM forneciam para a mensagem de WM_KEYDOWN. Seu método get_VirtualKey corresponde ao WPARAM do último, indicando que uma tecla que não é do sistema foi pressionada:

VirtualKey key;
HR(args->get_VirtualKey(&key));

De maneira semelhante, seu método get_KeyStatus corresponde ao LPARAM de WM_KEYDOWN. Isso fornece uma grande quantidade de informações sobre estado em torno do evento de pressionamento de tecla.

CorePhysicalKeyStatus status;
HR(args->get_KeyStatus(&status));

Como uma conveniência para o usuário, darei suporte à aceleração quando o usuário mantiver pressionada uma das teclas de seta para redimensionar a fonte renderizada mais rapidamente. Para isso, preciso de outra variável de membro:

unsigned m_accelerate;

Posso então usar o status da tecla do evento para determinar se o tamanho da fonte deve ser alterado por um único incremento ou por uma quantidade maior:

if (!status.WasKeyDown)
{
  m_accelerate = 1;
}
else
{
  m_accelerate += 2;
  m_accelerate = std::min(20U, m_accelerate);
}

Eu limitei isso para que a aceleração não vá muito além. Agora posso simplesmente tratar os vários pressionamentos de teclas individualmente. A primeira é a tecla de seta para a esquerda para reduzir o tamanho da fonte:

if (VirtualKey_Left == key)
{
  m_size = std::max(1.0f, m_size - m_accelerate);
}

Tomei cuidado para não deixar que o tamanho da fonte se torne inválido. Em seguida, há a tecla de seta para a direita para aumentar o tamanho da fonte:

else if (VirtualKey_Right == key)
{
  m_size += m_accelerate;
}

Agora, tratarei a tecla de seta para cima movendo para a família de fontes anterior:

if (begin(m_fonts) == m_font)
{
  m_font = end(m_fonts);
}
--m_font;

Em seguida, faço um loop cuidadosamente para a última fonte, caso o iterador atinja o início da sequência. Agora, tratarei a tecla de seta para baixo movendo para a próxima família de fontes:

else if (VirtualKey_Down == key)
{
  ++m_font;
  if (end(m_fonts) == m_font)
  {
      m_font = begin(m_fonts);
  }
}

Aqui, também tenho o cuidado de fazer um loop de volta para o início desta vez, caso o iterador atinja o final da sequência. Finalmente, posso simplesmente chamar meu método CreateTextFormat no final do manipulador de eventos para recriar o objeto de formato de texto.

Resta apenas atualizar meu método Draw de SampleWindow para desenhar algum texto com o formato de texto atual. Isso deve ser suficiente:

wchar_t const text [] = L"The quick brown fox jumps over the lazy dog";
m_target.DrawText(text, _countof(text) - 1,
  m_textFormat,
  RectF(10.0f, 10.0f, size.Width - 10.0f, size.Height - 10.0f),
  m_brush);

O método DrawText do destino da renderização do Direct2D dá suporte direto ao DirectWrite. Agora o DirectWrite pode manipular o layout do texto com renderização surpreendentemente rápida. Isso é tudo. A Figura 3 dá uma ideia do que esperar. Posso pressionar as teclas de seta para cima e para baixo para percorrer as famílias de fontes e pressionar as teclas de set para a esquerda e para a direita para ajustar o tamanho da fonte. O Direct2D refaz a renderização automaticamente com a seleção atual.

The Font Browser
Figura 3 O Navegador de fontes

Você falou em fontes coloridas?

O Windows 8.1 introduziu um novo recurso chamado fontes coloridas, eliminado várias soluções não ideais para implementação de fontes multicoloridas. Naturalmente, tudo depende do DirectWrite e do Direct2D para que isso aconteça. Felizmente, isso é tão simples como usar a constante D2D1_DRAW_TEXT_OPTIONS_ENABLE_COLOR_­FONT ao chamar o método DrawText do Direct2D. Posso atualizar meu método Draw de SampleWindow para usar o valor de enumeração do escopo correspondente:

 

m_target.DrawText(text, _countof(text) - 1,
  m_textFormat,
  RectF(10.0f, 10.0f, size.Width - 10.0f, size.Height - 10.0f),
  m_brush);
DrawTextOptions::EnableColorFont);

A Figura 4 mostra o navegador de fontes novamente, desta vez com alguns emoticons Unicode.

Color Fonts
Figura 4 Fontes coloridas

As fontes coloridas realmente se destacam quando você percebe que pode ajustá-las automaticamente sem perder a qualidade. Posso pressionar a tecla de seta para a direita em meu aplicativo navegador de fontes e dar uma olhada melhor nos detalhes. Você pode ver o resultado na Figura 5.

Scaled Color Fonts
Figura 5 Fontes de cores ajustadas

Fornecendo fontes coloridas, renderização de texto acelerada por hardware e código elegante e eficiente, o DirectWrite ganha vida com a ajuda do Direct2D e do C++ moderno.

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 (Microsoft)
Worachai Chaoweeraprasit é o líder de desenvolvimento do Direct2D e do DirectWrite. É fanático pela velocidade e qualidade de gráficos vetoriais 2D, bem como pela legibilidade de textos na tela. Em seu tempo livre, gosta de ficar ao lado de seus dois filhos em casa.