DirectWrite — невероятно мощный API разметки текста. Он используется практически всеми ведущими Windows-приложениями и технологиями — от реализации XAML в Windows Runtime (WinRT) и Office 2013 до Internet Explorer 11 и др. Сам по себе он не является механизмом рендеринга, но тесно связан с родственным ему Direct2D в семействе DirectX. Direct2D, конечно, полностью ускоряется на аппаратном уровне и является графическим API прямого режима (immediate-mode API).
Вы можете использовать DirectWrite с Direct2D для аппаратно-ускоренного рендеринга текста. Чтобы избежать путаницы, я не стал преждевременно много писать о DirectWrite. Мне не хотелось, чтобы вы подумали, будто Direct2D — это лишь механизм рендеринга для DirectWrite. Direct2D — это нечто гораздо большее. Тем не менее, DirectWrite есть, что предложить, и на этот раз я покажу некоторые возможности DirectWrite и рассмотрю, как современный C++ способен помочь упростить модель программирования.
DirectWrite API
Я буду использовать DirectWrite для исследования набора системных шрифтов. Для начала мне нужно получить объект фабрики DirectWrite. Это отправная точка для любого приложения, где нужно задействовать впечатляющую «типографию» DirectWrite. DirectWrite, как и многие Windows API, опирается на фундамент COM. Я должен вызвать функцию DWriteCreateFactory, чтобы создать объект фабрики DirectWrite. Эта функция возвращает COM-интерфейс, указывающий на объект фабрики:
ComPtr<IDWriteFactory2> factory;
Интерфейс IDWriteFactory2 — новейшая версия интерфейса фабрики DirectWrite, введенная в Windows 8.1 и DirectX 11.2 в начале этого года. IDWriteFactory2 наследует от IDWriteFactory1, который в свою очередь наследует от IDWriteFactory. Последний является исходным интерфейсом фабрики DirectWrite, который предоставляет доступ к множеству возможностей фабрики.
Получив предыдущий шаблон класса ComPtr, я вызову функцию DWriteCreateFactory:
HR(DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED,
__uuidof(factory),
reinterpret_cast<IUnknown **>(factory.GetAddressOf())));
DirectWrite включает Windows-службу Windows Font Cache Service (FontCache). Этот первый параметр указывает, будет ли полученная фабрика использовать этот межпроцессный кеш. Здесь возможно два варианта: DWRITE_FACTORY_TYPE_SHARED и DWRITE_FACTORY_TYPE_ISOLATED. Как SHARED-фабрики, так и ISOLATED могут использовать преимущества данных шрифта, которые уже находятся в кеше. Только SHARED-фабрики передают данные шрифта в этот кеш. Второй параметр сообщает, какую именно версию интерфейса фабрики DirectWrite вы хотели бы получить в третьем (последнем) параметре.
Располагая объектом фабрики DirectWrite, я могу сразу же перейти к делу и запросить набор системных шрифтов:
ComPtr<IDWriteFontCollection> fonts;
HR(factory->GetSystemFontCollection(fonts.GetAddressOf()));
У метода GetSystemFontCollection есть необязательный второй параметр, указывающий, надо ли проверять обновления или изменения в наборе установленных шрифтов. К счастью, по умолчанию этот параметр равен false, поэтому мне незачем думать о нем, если только не требуется уверенность в том, что последние изменения отражены в полученном наборе. Количество семейств шрифтов в этом наборе можно получить так:
unsigned const count = fonts->GetFontFamilyCount();
Затем с помощью метода GetFontFamily я получаю индивидуальные объекты семейства шрифтов, используя индекс, который отсчитывается от нуля. Объект семейства шрифтов (font family object) представляет шрифты, которые имеют одно название и, конечно, дизайн, но различаются по толщине, стилю и длине символов (stretch):
ComPtr<IDWriteFontFamily> family;
HR(fonts->GetFontFamily(index, family.GetAddressOf()));
Интерфейс IDWriteFontFamily наследует от интерфейса IDWriteFontList, поэтому я могу перечислить индивидуальные шрифты в семействе. Возможность получения названия семейства шрифтов логична и полезна. Однако эти названия локализуются, поэтому данная возможность не столь прямолинейна, как вы могли подумать. Сначала нужно запросить объект локализованных строк, который будет содержать по одному названию семейства на каждый поддерживаемый локализующий идентификатор (locale):
ComPtr<IDWriteLocalizedStrings> names;
HR(family->GetFamilyNames(names.GetAddressOf()));
Я могу также перечислить названия семейств, но обычно просто ищу то название, которое соответствует локализующему идентификатору в системе по умолчанию. Интерфейс IDWriteLocalizedStrings предоставляет метод FindLocaleName для получения индекса локализованного названия семейства. Я начинаю с вызова функции GetUserDefaultLocaleName, чтобы получить локализующий идентификатор по умолчанию:
wchar_t locale[LOCALE_NAME_MAX_LENGTH];
VERIFY(GetUserDefaultLocaleName(locale, countof(locale)));
Затем передаю это в метод FindLocaleName интерфейса IDWriteLocalizedStrings, чтобы определить, локализовано ли название этого семейства шрифтов для текущего пользователя:
unsigned index;
BOOL exists;
HR(names->FindLocaleName(locale, &index, &exists));
Если запрошенный локализующий идентификатор отсутствует в наборе, я должен переключиться на некое значение по умолчанию, например «en-us». Если же он есть, можно получить копию методом GetString интерфейса IDWriteLocalizedStrings:
if (exists)
{
wchar_t name[64];
HR(names->GetString(index, name, _countof(name)));
}
Если вам важна длина строки, можете сначала вызвать метод GetStringLength. Просто убедитесь, что копия поместится в ваш буфер. На рис. 1 приведен полный листинг, показывающий, как свести все воедино для перечисления установленных шрифтов.
Рис. 1. Перечисление шрифтов с помощью DirectWrite API
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);
}
}
Чуточка современного C++
Если вы постоянный читатель моей рубрики, то знаете, что я работаю с DirectX и, в частности Direct2D, на современном C++. Заголовочный файл dx.h (dx.codeplex.com) охватывает и DirectWrite. Вы можете использовать его, чтобы существенно упростить код, представленный ранее. Вместо вызова DWriteCreateFactory достаточно вызвать функцию CreateFactory из пространства имен DirectWrite:
auto factory = CreateFactory();
Так же легко получить набор системных шрифтов:
auto fonts = factory.GetSystemFontCollection();
По-настоящему блестяще dx.h справляется с перечислением этого набора. Мне не требуется писать традиционный цикл for. Мне незачем вызывать методы GetFontFamilyCount и GetFontFamily. Можно просто написать современный цикл for на основе диапазона:
for (auto family : fonts)
{
...
}
Фактически это тот же код, что и раньше. Компилятор (с помощью dx.h) генерирует его за меня, и я могу прибегнуть к гораздо более естественной модели программирования, которая облегчает написание корректного и эффективного кода. Предыдущий метод GetSystemFontCollection возвращает класс FontCollection, включающий итератор, который извлекает объекты семейства шрифтов только по мере необходимости («ленивое» извлечение). Это позволяет компилятору эффективно реализовать цикл на основе диапазона. Полный листинг дан на рис. 2. Сравните его с кодом на рис. 1, чтобы оценить четкость и потенциал для повышения производительности труда разработчиков.
Рис. 2. Перечисление шрифтов с помощью 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);
}
}
Просмотр шрифтов с помощью Windows Runtime
DirectWrite делает куда больше, чем просто перечисляет шрифты. Я намерен взять то, что показал к этому моменту, скомбинировать это с Direct2D и создать несложное приложение для просмотра шрифтов. В своей рубрике за август 2013 г. (msdn.microsoft.com/magazine/dn342867) я объяснил, как написать фундаментальную инфраструктуру модели WinRT-приложений на стандартном C++, а в рубрике за октябрь того же года (msdn.microsoft.com/magazine/dn451437) — как выполнять рендеринг в таком приложении на основе CoreWindow с помощью DirectX и особенно Direct2D. Теперь я покажу, как расширить этот код, чтобы задействовать Direct2D для рендеринга текста средствами DirectWrite.
С момента написания статей в тех рубриках была выпущена Windows 8.1, и это изменило некоторые вещи в том, как теперь обрабатывается масштабирование DPI в современных и настольных приложениях. Подробно рассказать о DPI я планирую в будущем, поэтому разговор об этих изменениях я оставлю до той поры. Сейчас я сосредоточусь на дополнении класса SampleWindow, писать который я начал в августе и продолжил в октябре, для поддержки рендеринга текста и простого просмотра шрифтов.
Первым делом надо добавить DirectWrite-класс Factory2 как переменную-член:
DirectWrite::Factory2 m_writeFactory;
В методе CreateDeviceIndependentResources из SampleWindow я могу создать фабрику DirectWrite:
m_writeFactory = DirectWrite::CreateFactory();
Кроме того, я могу получить набор системных шрифтов и локализующий идентификатор для пользователя по умолчанию, чтобы подготовиться к перечислению семейств шрифтов:
auto fonts = m_writeFactory.GetSystemFontCollection();
wchar_t locale[LOCALE_NAME_MAX_LENGTH];
VERIFY(GetUserDefaultLocaleName(locale, _countof(locale)));
Я сделаю так, что при нажатии пользователем клавиш со стрелками вверх и вниз приложение проходило в цикле по шрифтам. Но вместо постоянного перечисления набора через COM-интерфейсы я просто скопирую названия семейств шрифтов в стандартный контейнер set:
set<wstring> m_fonts;
Теперь я могу использовать тот же цикл for на основе диапазона, что и на рис. 2, в CreateDeviceIndependentResources для добавления названий в set:
m_fonts.insert(name);
Заполнив set, я устанавливаю в приложении итератор так, чтобы он указывал на начало set. Итератор хранится как переменная-член:
set<wstring>::iterator m_font;
Метод CreateDeviceIndependentResources в SampleWindow завершает свою работу инициализацией итератора и вызовом метода CreateTextFormat, который я вскоре определю:
m_font = begin(m_fonts);
CreateTextFormat();
Прежде чем Direct2D сможет нарисовать для меня какой-либо текст, я должен создать объект формата текста. Для этого нужно название семейства шрифтов и желательный размер шрифта. Я позволю изменять размер шрифта клавишами со стрелками влево и вправо, поэтому начну с добавления переменной-члена, которая будет отслеживать размер:
float m_size;
Компилятор Visual C++ вскоре разрешит мне инициализировать нестатические поля, подобные этому, в рамках класса. А пока ему нужно присвоить разумное значение по умолчанию в конструкторе SampleWindow. Затем я должен определить метод CreateTextFormat. Это просто оболочка одноименного метода фабрики DirectWrite, но он обновляет переменную-член, которую Direct2D может использовать для определения формата текста, подлежащего рисованию:
TextFormat m_textFormat;
Метод CreateTextFormat просто получает название семейства шрифтов из итератора set и объединяет его с текущим размером шрифта для создания нового объекта формата текста:
void CreateTextFormat()
{
m_textFormat = m_writeFactory.CreateTextFormat(m_font->c_str(),m_size);
}
Я обернул его, чтобы, помимо изначального вызова в конце CreateDeviceIndependentResources, его можно было вызывать всякий раз, когда пользователь нажимает одну из клавиш со стрелками для изменения размера или семейства шрифта. Это подводит нас к вопросу насчет того, как обрабатывать нажатия клавиш в модели WinRT-приложений. В настольном приложении это требует обработки сообщения WM_KEYDOWN. К счастью, CoreWindow предоставляет событие KeyDown, которое является современным эквивалентом этого сообщения. Я начну с определения интерфейса IKeyEventHandler, который нужно будет реализовать в моем SampleWindow:
typedef ITypedEventHandler<CoreWindow *, KeyEventArgs *> IKeyEventHandler;
Далее этот интерфейс можно добавить к списку наследуемых интерфейсов SampleWindow и соответственно обновить реализацию QueryInterface. Собственно, нужно просто предоставить реализацию его Invoke:
auto __stdcall Invoke(
ICoreWindow *,IKeyEventArgs * args) -> HRESULT override
{
...
return S_OK;
}
Интерфейс IKeyEventArgs сообщает во многом ту же информацию, что и LPARAM с WPARAM в сообщении WM_KEYDOWN. Его метод get_VirtualKey соответствует WPARAM, указывая, какая несистемная клавиша была нажата:
VirtualKey key;
HR(args->get_VirtualKey(&key));
Аналогично его метод get_KeyStatus соответствует LPARAM в WM_KEYDOWN. Это дает массу информации о состоянии, связанном с событием нажатия клавиши:
CorePhysicalKeyStatus status;
HR(args->get_KeyStatus(&status));
Для удобства я буду поддерживать ускорение при нажатии с последующим удерживанием клавиши-стрелки для быстрого изменения размера визуализируемого шрифта. Для этого мне понадобится другая переменная-член:
unsigned m_accelerate;
Теперь состояние клавиши в событии можно использовать, чтобы определять, как изменять размер шрифта — разовым приращением или на нарастающую величину:
if (!status.WasKeyDown)
{
m_accelerate = 1;
}
else
{
m_accelerate += 2;
m_accelerate = std::min(20U, m_accelerate);
}
Я ввел ограничения, поэтому ускорение не будет слишком большим. Теперь можно обрабатывать различные нажатия клавиши индивидуально. Сначала клавиша со стрелкой влево для уменьшения размера шрифта:
if (VirtualKey_Left == key)
{
m_size = std::max(1.0f, m_size - m_accelerate);
}
Я блокирую возможность того, что размер шрифта станет недопустимым. Далее займемся клавишей со стрелкой вправо для увеличения размера шрифта:
else if (VirtualKey_Right == key)
{
m_size += m_accelerate;
}
Потом я обрабатываю клавишу со стрелкой вверх, переходя к предыдущему семейству шрифтов:
if (begin(m_fonts) == m_font)
{
m_font = end(m_fonts);
}
--m_font;
Затем я перебираю шрифты, пока итератор не дойдет до начала последовательности. Клавиша со стрелкой вниз позволяет перейти к следующему семейству шрифтов:
else if (VirtualKey_Down == key)
{
++m_font;
if (end(m_fonts) == m_font)
{
m_font = begin(m_fonts);
}
}
Шрифты перебираются, пока итератор не достигнет конца последовательности. В конце обработчика события можно вызвать мой метод CreateTextFormat для создания объекта формата текста заново.
Остается лишь обновить метод Draw в SampleWindow для рисования текста в текущем формате. Нужно сделать вот что:
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);
Метод DrawText мишени рендеринга в Direct2D напрямую поддерживает DirectWrite. Теперь DirectWrite может обрабатывать разметку текста и визуализировать текст очень быстро. Рис. 3 дает представление о том, что вы можете ожидать. Я нажимаю клавиши со стрелками вверх и вниз для циклического перебора семейств шрифтов, а клавиши со стрелками влево и вправо — для изменения размера шрифта. Direct2D обеспечивает автоматическую перерисовку в соответствии с текущим выбором.
Рис. 3. Средство просмотра шрифтов
Цветные шрифты
В Windows 8.1 появилась новая функциональность — цветные шрифты, и это позволяет избавиться от ряда не оптимальных решений для реализации многоцветных шрифтов. Естественно, задача возлагается на DirectWrite и Direct2D. К счастью, все, что от вас требуется, — указать константу D2D1_DRAW_TEXT_OPTIONS_ENABLE_COLOR_FONT при вызове Direct2D-метода DrawText. Можно модифицировать метод Draw в SampleWindow, чтобы использовать соответствующее перечислимое значение:
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);
На рис. 4 вновь показано средство просмотра шрифтов, но на этот раз с некоторыми смайликами Unicode.
Рис. 4. Цветные шрифты
Цветные шрифты по-настоящему впечатляют — их можно автоматически масштабировать без потери качества. Нажимая клавишу со стрелкой вправо в моем приложении для просмотра шрифтов, вы можете приблизить текущий шрифт и детально его разглядеть. Результат показан на рис. 5.
Рис. 5. Масштабируемые цветные шрифты
DirectWrite, который предоставляет цветные шрифты, аппаратно-ускоренный рендеринг текста, элегантный и эффективный код, раскрывает свои возможности благодаря Direct2D и современному C++.