DirectWrite и шрифты в WinRT

163

Direct Write - подсистема DirectX, предназначенная для высокопроизводительного вывода текста. Кроме того, DirectWrite предоставляет пару возможностей, отсутствующих в Windows Runtime, а именно получение списка установленных шрифтов и получение метрик шрифтов.

Я решил, что для работы с DirectWrite классы моей библиотеки DirectXWrapper будут однозначно соответствовать интерфейсам DirectWrite. Имена всех интерфейсов начинаются с префикса IDWrite; имена соответствующих классов начинаются просто с Write. Поначалу вы неизбежно будете путаться в именах. Ниже приведены соответствия интерфейсов и классов в порядке их обсуждения:

Интерфейс DirectWrite Класс DirectXWrapper
IDWriteFactory WriteFactory
IDWriteFontCollection WriteFontCollection
IDWriteFontFamily WriteFontFamily
IDWriteFont WriteFont
IDWriteLocalizedString WriteLocalizedStrings

Во многих случаях имена методов интерфейсов DirectWrite (например, метода GetMetrics из IDWriteFont) были просто продублированы: мой класс WriteFont тоже содержит метод GetMetrics. Я не пытался продублировать все методы интерфейсов.

Программа, которая желает использовать DirectWrite, сначала вызывает функцию DWriteCreateFactory для получения объекта типа IDWriteFactory. Среди множества других методов этот интерфейс IDWriteFactory определяет метод GetSystemFontCollection для получения списка шрифтов, установленных в системе. Я упаковал IDWriteFactory в мой собственный класс с именем WriteFactory. Заголовочный файл C++ выглядит так:

#pragma once

#include "WriteFontCollection.h"

namespace DirectXWrapper
{
    public ref class WriteFactory sealed
    {
    private:
        Microsoft::WRL::ComPtr<IDWriteFactory> pFactory;

    public:
        WriteFactory();
        WriteFontCollection^ GetSystemFontCollection();
        WriteFontCollection^ GetSystemFontCollection(bool checkForUpdates);
    };
}

Класс определяется с ключевыми словами ref и sealed, как требуется для открытых классов C++ в библиотеках Windows Runtime Component. Ключевое слово ref означает, что экземпляры класса должны создаваться конструкцией ref new вместо простого new, а конструктор вместо указателя возвращает дескриптор (handle), участвующий в подсчете ссылок.

Объект IDWriteFactory, полученный от DWriteCreateFactory, сохраняется в закрытом поле как тип ComPtr, определяемый в пространстве имен Microsoft.wri (или Microsoft::WRL в синтаксисе C++). Тип ComPtr (сокращение от «Common Object Model pointer») преобразует указатель на объект COM (такой, как IDWriteFactory) в «умный указатель», участвующий в подсчете ссылок и корректно освобождающий свои ресурсы. Для хранения указателей на объекты COM в коде DirectX для Windows 8 следует использовать именно такой способ.

В заголовочном файле также определяются три открытых метода: конструктор и две версии метода GetSystemFontCollection. Эти методы возвращают объект типа WriteFontCollection, который не является типом Direct Write. Более того, он не может им быть, потому что открытые методы Windows Runtime Component могут возвращать только типы Windows Runtime; это еще один класс библиотеки DirectXWrapper. Крышка (^) означает, что WriteFontCollection представляет собой дескриптор, а не указатель, из чего следует, что тип определяется с ключевым словом ref, а его экземпляры в C++ создаются конструкцией ref new вместо new.

Присутствие класса WriteFontCollection в этом заголовочном файле требует включения заголовочного файла WriteFontCollection.h в начале файла. Ниже приведена реализация класса WriteFactory в файле WriteFactory.cpp:

#include "pch.h"
#include "WriteFactory.h"

using namespace DirectXWrapper;
using namespace Platform;
using namespace Microsoft::WRL;

WriteFactory::WriteFactory()
{
    HRESULT hr = DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED, 
                                     __uuidof(IDWriteFactory), 
                                     &pFactory);

    if (!SUCCEEDED(hr))
        throw ref new COMException(hr);
}

WriteFontCollection^ WriteFactory::GetSystemFontCollection()
{
    return GetSystemFontCollection(false);
}

WriteFontCollection^ WriteFactory::GetSystemFontCollection(bool checkForUpdates)
{
    ComPtr<IDWriteFontCollection> pFontCollection;

    HRESULT hr = pFactory->GetSystemFontCollection(&pFontCollection, checkForUpdates);

    if (!SUCCEEDED(hr))
        throw ref new COMException(hr);

    return ref new WriteFontCollection(pFontCollection);
}

Конструктор вызывает функцию DWriteCreateFactory для получения объекта IDWriteFactory. Оператор __uuidof получает код GUID, идентифицирующий данный объект. Методы и функции DirectX очень часто возвращают значения типа HRESULT. Это простой числовой признак успеха или неудачи, но игнорировать его не следует. Стандартное решение в программах Windows 8 - инициирование исключения типа COMException при возникновении ошибки. Обратите внимание на использование ref new для создания экземпляра класса COMException, являющегося типом Windows Runtime.

Метод GetSystemFontCollection в моем классе WriteFactory использует объект IDWriteFactory для вызова метода GetSystemFontCollection этого интерфейса с целью получения указателя на интерфейс DirectWrite - IDWriteFontCollection. Указатель передается конструктору WriteFontCollection (снова обратите внимание на ref new). Заголовочный файл WriteFontCollection:

#pragma once

#include "WriteFontFamily.h"

namespace DirectXWrapper
{
    public ref class WriteFontCollection sealed
    {
    private:
        Microsoft::WRL::ComPtr<IDWriteFontCollection> pFontCollection;

    internal:
        WriteFontCollection(Microsoft::WRL::ComPtr<IDWriteFontCollection> pFontCollection);

    public:
        bool FindFamilyName(Platform::String^ familyName, int * index);
        int GetFontFamilyCount();
        WriteFontFamily^ GetFontFamily(int index);
    };
}

Конструктор определяется как внутренний (internal) по отношению к библиотеке. Он не может быть закрытым, потому что в этом случае он будет недоступен за пределами класса (а классу WriteFontFactory, конечно, нужно его вызывать). Но он не может быть и открытым, потому что аргумент конструктора не является типом Windows Runtime. Также обратите внимание на использование класса String, определенного в пространстве имен Platform. Класс String является типом Windows Runtime; он эквивалентен классу C# String, определенному в пространстве имен System. Ниже приведена реализация WriteFontCollection:

#include "pch.h"
#include "WriteFontCollection.h"
#include "WriteFontFamily.h"

using namespace DirectXWrapper;
using namespace Platform;
using namespace Microsoft::WRL;

WriteFontCollection::WriteFontCollection(ComPtr<IDWriteFontCollection> pFontCollection)
{
    this->pFontCollection = pFontCollection;
}

bool WriteFontCollection::FindFamilyName(String^ familyName, int * index)
{
    uint32 familyIndex;
    BOOL exists;
    HRESULT hr = this->pFontCollection->FindFamilyName(familyName->Data(), &familyIndex, &exists);

    if (!SUCCEEDED(hr))
        throw ref new COMException(hr);

    *index = familyIndex;
 
    return exists != 0;
}

int WriteFontCollection::GetFontFamilyCount()
{
    return pFontCollection->GetFontFamilyCount();
}

WriteFontFamily^ WriteFontCollection::GetFontFamily(int index)
{
    ComPtr<IDWriteFontFamily> pfontFamily;

    HRESULT hr = pFontCollection->GetFontFamily(index, &pfontFamily);

    if (!SUCCEEDED(hr))
        throw ref new COMException(hr);

    return ref new WriteFontFamily(pfontFamily);
}

Получение конкретного семейства шрифтов из коллекции происходит в два этапа. Сначала вызов FindFamilyName с конкретным именем (например, «Times New Roman») получает индекс в коллекции. Этот индекс передается GetFontFamily для получения объекта IDWriteFontFamily (при использовании DirectWrite) или объекта WriteFontFamily (при использовании библиотеки DirectXWrapper).

Также возможен перебор всех шрифтов в коллекции с передачей GetFontFamily индексов вплоть до значения, возвращенного GetFontFamilyCount. Заголовочный файл WriteFontFamily выглядит так:

#pragma once

#include "WriteLocalizedStrings.h"
#include "WriteFont.h"

namespace DirectXWrapper
{
    public ref class WriteFontFamily sealed
    {
    private:
        Microsoft::WRL::ComPtr<IDWriteFontFamily> pFontFamily;

    internal:
        WriteFontFamily(Microsoft::WRL::ComPtr<IDWriteFontFamily> pFontFamily);

    public:
        WriteLocalizedStrings^ GetFamilyNames();
        WriteFont^ GetFirstMatchingFont(Windows::UI::Text::FontWeight fontWeight,
                                        Windows::UI::Text::FontStretch fontStretch, 
                                        Windows::UI::Text::FontStyle fontStyle);
    };
}

Взгляните на аргументы GetFirstMatchingFont: все они являются типами Windows Runtime, потому что определены в пространстве имен Windows.UI.Text.FontWeight - структура, тип статических свойств класса FontWeights, a FontStretch и FontStyle являются перечислениями. В методе GetFirstMatchingFont, реализованном интерфейсом IDWriteFontFamily, аргументы относятся к типам DWRITE_FONT_WEIGHT, DWRITE_FONT_STRETCH и DWRITE_FONT_STYLE; все они представляет собой перечисления. Интересно, что для FontStretch и FontStyle возможно прямое преобразование: перечисления содержат одинаковые значения, это наводит на мысль, что DirectWrite лежит в основе системы вывода текста Windows Runtime.

#include "pch.h"
#include "WriteFontFamily.h"

using namespace DirectXWrapper;
using namespace Platform;
using namespace Microsoft::WRL;
using namespace Windows::UI::Text;

WriteFontFamily::WriteFontFamily(ComPtr<IDWriteFontFamily> pFontFamily)
{
    this->pFontFamily = pFontFamily;
}

WriteLocalizedStrings^ WriteFontFamily::GetFamilyNames()
{
    ComPtr<IDWriteLocalizedStrings> pFamilyNames;

    HRESULT hr = pFontFamily->GetFamilyNames(&pFamilyNames);

    if (!SUCCEEDED(hr))
        throw ref new COMException(hr);

    return ref new WriteLocalizedStrings(pFamilyNames);
}

WriteFont^ WriteFontFamily::GetFirstMatchingFont(FontWeight fontWeight, 
                                                 FontStretch fontStretch, 
                                                 FontStyle fontStyle)
{
    // Преобразование насыщенности шрифта из Windows Runtime в DirectX
    DWRITE_FONT_WEIGHT writeFontWeight = DWRITE_FONT_WEIGHT_NORMAL;

    if (fontWeight.Equals(FontWeights::Black))
        writeFontWeight = DWRITE_FONT_WEIGHT_BLACK;

    else if (fontWeight.Equals(FontWeights::Bold))
        writeFontWeight = DWRITE_FONT_WEIGHT_BOLD;

    else if (fontWeight.Equals(FontWeights::ExtraBlack))
        writeFontWeight = DWRITE_FONT_WEIGHT_EXTRA_BLACK;

    else if (fontWeight.Equals(FontWeights::ExtraBold))
        writeFontWeight = DWRITE_FONT_WEIGHT_EXTRA_BOLD;

    else if (fontWeight.Equals(FontWeights::ExtraLight))
        writeFontWeight = DWRITE_FONT_WEIGHT_EXTRA_LIGHT;

    else if (fontWeight.Equals(FontWeights::Light))
        writeFontWeight = DWRITE_FONT_WEIGHT_LIGHT;

    else if (fontWeight.Equals(FontWeights::Medium))
        writeFontWeight = DWRITE_FONT_WEIGHT_MEDIUM;

    else if (fontWeight.Equals(FontWeights::Normal))
        writeFontWeight = DWRITE_FONT_WEIGHT_NORMAL;

    else if (fontWeight.Equals(FontWeights::SemiBold))
        writeFontWeight = DWRITE_FONT_WEIGHT_SEMI_BOLD;

    else if (fontWeight.Equals(FontWeights::SemiLight))
        writeFontWeight = DWRITE_FONT_WEIGHT_SEMI_LIGHT;

    else if (fontWeight.Equals(FontWeights::Thin))
        writeFontWeight = DWRITE_FONT_WEIGHT_THIN;

    // Convert font stretch from Windows Runtime to DirectX
    DWRITE_FONT_STRETCH writeFontStretch = (DWRITE_FONT_STRETCH)fontStretch;

    // Convert font style from Windows Runtime to DirectX
    DWRITE_FONT_STYLE writeFontStyle = (DWRITE_FONT_STYLE)fontStyle;

    ComPtr<IDWriteFont> pWriteFont = nullptr;
    HRESULT hr = pFontFamily->GetFirstMatchingFont(writeFontWeight, 
                                                   writeFontStretch, 
                                                   writeFontStyle, 
                                                   &pWriteFont);
    if (!SUCCEEDED(hr))
        throw ref new COMException(hr);

    return ref new WriteFont(pWriteFont);
}

Семейству шрифтов обычно присваивается имя - например, «Times New Roman», но в Direct Write семейства шрифтов могут обладать несколькими именами для разных локальных контекстов и языков. Метод GetFamilyNames возвращает не одно имя, а коллекцию имен в объекте IDWriteLocalizedStrings. Строки идентифицируются стандартными обозначениями локальных контекстов, например «en-us» для английского языка (США):

#pragma once

namespace DirectXWrapper
{
    public ref class WriteLocalizedStrings sealed
    {
    private:
        Microsoft::WRL::ComPtr<IDWriteLocalizedStrings> pLocalizedStrings;

    internal:
        WriteLocalizedStrings(Microsoft::WRL::ComPtr<IDWriteLocalizedStrings> pLocalizedStrings);

    public:
        int GetCount();
        Platform::String^ GetLocaleName(int index);
        Platform::String^ GetString(int index);
        bool FindLocaleName(Platform::String^ localeName, int * index);
    };
}

Вот как выглядит реализация:

#include "pch.h"
#include "WriteLocalizedStrings.h"

using namespace DirectXWrapper;
using namespace Platform;
using namespace Microsoft::WRL;

WriteLocalizedStrings::WriteLocalizedStrings(ComPtr<IDWriteLocalizedStrings> pLocalizedStrings)
{
    this->pLocalizedStrings = pLocalizedStrings;
}

int WriteLocalizedStrings::GetCount()
{
    return this->pLocalizedStrings->GetCount();
}

String^ WriteLocalizedStrings::GetLocaleName(int index)
{
    UINT32 length = 0;
    HRESULT hr = this->pLocalizedStrings->GetLocaleNameLength(index, &length);

    if (!SUCCEEDED(hr))
        throw ref new COMException(hr);

    wchar_t* str = new (std::nothrow) wchar_t[length + 1];

    if (str == nullptr)
        throw ref new COMException(E_OUTOFMEMORY);

    hr = this->pLocalizedStrings->GetLocaleName(index, str, length + 1);

    if (!SUCCEEDED(hr))
        throw ref new COMException(hr);

    String^ string = ref new String(str);
    delete[] str;
    return string;
}

String^ WriteLocalizedStrings::GetString(int index)
{
    UINT32 length = 0;
    HRESULT hr = this->pLocalizedStrings->GetStringLength(index, &length);

    if (!SUCCEEDED(hr))
        throw ref new COMException(hr);

    wchar_t* str = new (std::nothrow) wchar_t[length + 1];

    if (str == nullptr)
        throw ref new COMException(E_OUTOFMEMORY);

    hr = this->pLocalizedStrings->GetString(index, str, length + 1);

    if (!SUCCEEDED(hr))
        throw ref new COMException(hr);

    String^ string = ref new String(str);
    delete[] str;
    return string;
}

bool WriteLocalizedStrings::FindLocaleName(String^ localeName, int * index)
{
    uint32 localeIndex = 0;
    BOOL exists = false;
    HRESULT hr = this->pLocalizedStrings->FindLocaleName(localeName->Data(), 
                                                         &localeIndex, &exists);
    if (!SUCCEEDED(hr))
        throw ref new COMException(hr);

    *index = localeIndex;

    return exists != 0;
}

Некоторая громоздкость этого кода объясняется необходимостью выделения строк C++ (которые представляют собой массивы символов) для вызова методов DirectWrite, с последующим преобразованием в объекты Windows Runtime String для возвращения из реализации DirectXWrapper. Заголовочный файл WriteFont:

#pragma once

#include "WriteFontMetrics.h"

namespace DirectXWrapper
{
    public ref class WriteFont sealed
    {
    private:
        Microsoft::WRL::ComPtr<IDWriteFont> pWriteFont;

    internal:
        WriteFont(Microsoft::WRL::ComPtr<IDWriteFont> pWriteFont);

    public:
        bool HasCharacter(UINT32 unicodeValue);
        bool IsSymbolFont();
        WriteFontMetrics GetMetrics();
    };
}

И реализация:

#include "pch.h"
#include "WriteFont.h"

using namespace DirectXWrapper;
using namespace Platform;
using namespace Microsoft::WRL;

WriteFont::WriteFont(ComPtr<IDWriteFont> pWriteFont)
{
    this->pWriteFont = pWriteFont;
}

WriteFontMetrics WriteFont::GetMetrics()
{
    DWRITE_FONT_METRICS fontMetrics;
    this->pWriteFont->GetMetrics(&fontMetrics);

    WriteFontMetrics writeFontMetrics = 
    {
        fontMetrics.designUnitsPerEm, 
        fontMetrics.ascent,
        fontMetrics.descent,
        fontMetrics.lineGap,
        fontMetrics.capHeight,
        fontMetrics.xHeight,
        fontMetrics.underlinePosition,
        fontMetrics.underlineThickness,
        fontMetrics.strikethroughPosition,
        fontMetrics.strikethroughThickness
    };

    return writeFontMetrics;
}

bool WriteFont::HasCharacter(UINT32 unicodeValue)
{
    BOOL exists = 0;
    HRESULT hr = this->pWriteFont->HasCharacter(unicodeValue, &exists);

    if (!SUCCEEDED(hr))
        throw ref new COMException(hr);

    return exists != 0;
}

bool WriteFont::IsSymbolFont()
{
    return this->pWriteFont->IsSymbolFont() != 0;
}

DirectWrite-версия метода GetMetrics заполняет структуру типа DWRITE_FONT_METRICS. Конечно, Windows Runtime Component не может вернуть структуру напрямую, поэтому я определил собственную версию этой структуры:

#pragma once

namespace DirectXWrapper
{
    public value struct WriteFontMetrics
    {
        UINT16 DesignUnitsPerEm;
        UINT16 Ascent;
        UINT16 Descent;
        INT16  LineGap;
        UINT16 CapHeight;
        UINT16 XHeight;
        INT16  UnderlinePosition;
        UINT16 UnderlineThickness;
        INT16  StrikethroughPosition;
        UINT16 StrikethroughThickness;
    };
}

Мы рассмотрели весь код Direct Write, реализованный в библиотеке DirectXWrapper. Разумеется, в своей программе C# я использовал лишь небольшое подмножество возможностей Direct Write, но сейчас у меня есть все необходимое для выполнения двух основных операций.

Начнем с перебора установленных шрифтов. EnumerateFonts - вполне обычный проект C# для Windows 8, если не считать того, что в Solution Explorer я щелкнул правой кнопкой на имени решения, выбрал команду Add --> Existing Project и добавил проект DirectXWrapper. Как обычно, при использовании ссылки на проект библиотеки я также щелкнул правой кнопкой в разделе References проекта EnumerateFonts, выбрал команду Add Reference, после чего в диалоговом окне Add Reference выбрал слева Projects и DirectXWrapper.

Файл XAML проекта EnumerateFonts содержит элемент управления ListBox:

<Page ...>

    <Grid Background="#FF1D1D1D">
        <ListBox Name="lbx">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding}" FontFamily="{Binding}"
                               FontSize="24" />
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </Grid>
</Page>

Конечно, шаблон ItemTemplate предполагает, что список ListBox будет заполнен именами семейств шрифтов. Каждое имя отображается шрифтом соответствующего семейства. Список заполняется в конструкторе из файла фонового кода:

using DirectXWrapper;
using Windows.UI.Xaml.Controls;

namespace WinRTTestApp
{
    public sealed partial class MainPage : Page
    {
        public MainPage()
        {
            this.InitializeComponent();

            WriteFactory writeFactory = new WriteFactory();
            WriteFontCollection writeFontCollection =
                        writeFactory.GetSystemFontCollection();

            int count = writeFontCollection.GetFontFamilyCount();
            string[] fonts = new string[count];

            for (int i = 0; i < count; i++)
            {
                WriteFontFamily writeFontFamily =
                            writeFontCollection.GetFontFamily(i);

                WriteLocalizedStrings writeLocalizedStrings =
                            writeFontFamily.GetFamilyNames();
                int index;

                if (writeLocalizedStrings.FindLocaleName("en-us", out index))
                {
                    fonts[i] = writeLocalizedStrings.GetString(index);
                }
                else
                {
                    fonts[i] = writeLocalizedStrings.GetString(0);
                }
            }

            lbx.ItemsSource = fonts;
        }
    }
}

Как видите, программа использует классы и методы DirectXWrapper так, как если бы они были обычными классами Windows Runtime. Программа пытается найти шрифт с локальным контекстом «en-us»; если это сделать не удается, она берет первый шрифт из коллекции. Как правило, шрифты Windows 8 имеют только одно имя, но некоторым восточным шрифтам присваиваются альтернативные имена на китайском, корейском или японском языке. Начало списка в вашей системе может выглядеть так:

Список шрифтов
Пройди тесты
Лучший чат для C# программистов