Windows と C++
DirectWrite と最新の C++ を使用したフォントを探る
Kenny Kerr
DirectWrite は非常に強力なテキスト レイアウト API です。この API は、XAML の Windows ランタイム (WinRT) 実装や Office 2013 から Internet Explorer 11 まで、実質的にすべての主要な Windows アプリケーションやテクノロジを強化します。DirectWrite そのものはレンダリング エンジンではありませんが、DirectX ファミリの関連テクノロジである Direct2D と密接に関係しています。もちろん、Direct2D はハードウェア アクセラレータを使用した優れたイミディエイト モード グラフィック API です。
DirectWrite と Direct2D を併用すると、ハードウェア アクセラレータを使用したテキスト レンダリングを実現できます。混乱を避けるために、このコラムではこれまで DirectWrite についてはあまり扱ってきませんでした。Direct2D を単なる DirectWrite のレンダリング エンジンだと考えないでください。Direct2D にはそれ以上の機能があります。ただし DirectWrite で提供される機能もたくさんあります。今月のコラムでは、DirectWrite で実現できる処理の例を紹介し、最新の C++ がプログラミング モデルの簡略化にどのように役立つかについて説明します。
DirectWrite API
では、DirectWrite を使用して、システム フォント コレクションを調査しましょう。まず、DirectWrite ファクトリ オブジェクトを取得する必要があります。これは、DirectWrite の魅力的な文字体裁を使用するすべてのアプリの出発点です。DirectWrite では、多くの Windows API と同様に COM の基本要素を使用しています。DirectWrite ファクトリ オブジェクトを作成するには、DWriteCreateFactory 関数を呼び出す必要があります。この関数は、ファクトリ オブジェクトを参照する COM インターフェイスを返します。
ComPtr<IDWriteFactory2> factory;
IDWriteFactory2 インターフェイスは、Windows 8.1 と DirectX 11.2 に今年導入された最新バージョンの DirectWrite ファクトリ インターフェイスです。IDWriteFactory2 は IDWriteFactory1 を継承し、IDWriteFactory1 は IDWriteFactory を継承しています。IDWriteFactory は、ファクトリの機能を大量に公開している元の DirectWrite ファクトリ インターフェイスです。
先ほどの ComPtr クラス テンプレートを指定したら、DWriteCreateFactory 関数を呼び出します。
HR(DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED,
__uuidof(factory),
reinterpret_cast<IUnknown **>(factory.GetAddressOf())));
DirectWrite には、Windows フォント キャッシュ サービス (FontCache) という Windows サービスが付属しています。1 つ目のパラメーターは、生成したファクトリによって、このプロセス間キャッシュでフォントの使用を共有できるようにするかどうかを指定します。指定できる値は、DWRITE_FACTORY_TYPE_SHARED と DWRITE_FACTORY_TYPE_ISOLATED の 2 つのうちいずれかです。共有 (SHARED) と分離 (ISOLATED) のどちらのファクトリも、既にキャッシュされているフォント データを利用できます。フォント データをキャッシュに戻せるのは共有ファクトリだけです。2 つ目のパラメーターは、3 つ目の (最後の) パラメーターで返す DirectWrite ファクトリ インターフェイスのバージョンを具体的に指定します。
DirectWrite ファクトリ オブジェクトのインスタンスを作成すると、直接そのインスタンスにシステム フォント コレクションを要求できます。
ComPtr<IDWriteFontCollection> fonts;
HR(factory->GetSystemFontCollection(fonts.GetAddressOf()));
GetSystemFontCollection メソッドには、インストール済みフォントのセットに対する更新や変更を確認するかどうかを指定する、省略可能な 2 つ目のパラメーターがあります。さいわい、このパラメーターは既定で false に設定されているので、生成したコレクションに最近の変更を反映する必要がない限り検討不要です。フォント コレクションを呼び出したら、次のように、そのコレクションに含まれているフォント ファミリの数を取得できます。
unsigned const count = fonts->GetFontFamilyCount();
次に、GetFontFamily メソッドを使用して、0 から始まるインデックスを使用する個別のフォント ファミリ オブジェクトを取得します。フォント ファミリ オブジェクトは、名前が同じ (もちろんデザインも同じ) でも太さ、スタイル、および伸縮が異なるフォントのセットを表します。
ComPtr<IDWriteFontFamily> family;
HR(fonts->GetFontFamily(index, family.GetAddressOf()));
IDWriteFontFamily インターフェイスは IDWriteFontList インターフェイスを継承しているので、フォント ファミリに含まれている個別のフォントを列挙できます。フォント ファミリの名前を取得できると、合理的で便利です。ただし、ファミリ名はローカライズされているため、想像するほど簡単な処理ではありません。まず、ローカライズした文字列オブジェクトをフォント ファミリから取得する必要があります。ローカライズされた文字列オブジェクトには、サポートされているロケールごとにファミリ名が 1 つ用意されています。
ComPtr<IDWriteLocalizedStrings> names;
HR(family->GetFamilyNames(names.GetAddressOf()));
ファミリ名を列挙することもできますが、単にユーザーの既定のロケールに対応する名前を参照する方が一般的です。実際、IDWriteLocalizedStrings インターフェイスには、ローカライズされたファミリ名のインデックスを取得する FindLocaleName メソッドが用意されています。まず、GetUserDefaultLocaleName 関数を呼び出して、ユーザーの既定のロケールを取得します。
wchar_t locale[LOCALE_NAME_MAX_LENGTH];
VERIFY(GetUserDefaultLocaleName(locale, countof(locale)));
次に、取得した値を IDWriteLocalizedStrings インターフェイスの FindLocaleName メソッドに渡し、現在のユーザー向けにローカライズされた名前がフォント ファミリに存在するかどうか特定します。
unsigned index;
BOOL exists;
HR(names->FindLocaleName(locale, &index, &exists));
要求したロケールがコレクションに存在しない場合は、en-us などの既定のロケールにフォールバックすることもあります。ロケールが存在していれば、IDWriteLocalizedStrings インターフェイスの GetString メソッドを使用して、コピーを取得できます。
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++ の概要
このコラムを定期的に読んでいただいている方は、このコラムでは最新の C++ の機能である、 DirectX と特に Direct2D を扱ってきたことをご存じでしょう。dx.h ヘッダー (dx.codeplex.com、英語) でも DirectWrite を取り上げています。dx.h を使用すると、前のセクションで説明したコードを大幅に簡略化できます。DWriteCreateFactory を呼び出す代わりに、DirectWrite 名前空間の CreateFactory 関数を呼び出すだけになります。
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 ランタイムを使用したフォント ブラウザー
DirectWrite の機能は、フォントの列挙だけではありません。これまでに紹介した内容を取り上げて Direct2D と組み合わせながら、シンプルなフォント ブラウザー アプリを作成しましょう。2013 年 8 月号のコラム (msdn.microsoft.com/magazine/dn342867) では、標準の C++ で基本的な WinRT アプリ モデルを作成する方法を紹介しました。2013 年 10 月号のコラム (msdn.microsoft.com/magazine/dn451437) では、DirectX と特に Direct2D を使用して、この CoreWindow ベースのアプリでレンダリングする方法を紹介しました。今回は、同じコードを拡張して、DirectWrite の力を借りながら Direct2D を使用してテキストをレンダリングする方法を示します。
これらのコラムの執筆後、Windows 8.1 がリリースされ、最新のアプリとデスクトップ アプリの両方で DPI スケールの処理方法がいくらか変更されました。DPI の詳細については今後取り上げるつもりなので、差し当たってはこれらの変更点について説明しません。今回は、8 月に作成し始めて 10 月に拡張した SampleWindow クラスを強化して、テキスト レンダリングとシンプルなフォント ブラウズのサポートを追加することに重点を置きます。
まず、DirectWrite の Factory2 クラスをメンバー変数として追加します。
DirectWrite::Factory2 m_writeFactory;
SampleWindow クラスの CreateDeviceIndependentResources メソッド内で、DirectWrite ファクトリを作成できます。
m_writeFactory = DirectWrite::CreateFactory();
また、このメソッド内ではフォント ファミリを列挙する準備として、作成した DirectWrite ファクトリのシステム フォント コレクションとユーザーの既定のロケールも取得できます。
auto fonts = m_writeFactory.GetSystemFontCollection();
wchar_t locale[LOCALE_NAME_MAX_LENGTH];
VERIFY(GetUserDefaultLocaleName(locale, _countof(locale)));
このアプリでは、ユーザーが上下の方向キーを押したら、フォントを順番に表示します。COM インターフェイスを使用して継続的にコレクションを列挙するのではなく、フォント ファミリ名を標準の set コンテナーにコピーします。
set<wstring> m_fonts;
これで、CreateDeviceIndependentResources メソッド内で図 2と同じ範囲ベースの for ループを使用するだけで、set コンテナーにフォントの名前を追加できます。
m_fonts.insert(name);
set コンテナーに名前を追加したら、set コンテナーの最初の名前を参照する反復子を使用して、アプリを実行します。この反復子はメンバー変数として格納します。
set<wstring>::iterator m_font;
SampleWindow クラスの CreateDeviceIndependentResources メソッドでは、最後に反復子を初期化し、CreateTextFormat メソッドを呼び出します。CreateTextFormat メソッドについてはこの後すぐに定義します。
m_font = begin(m_fonts);
CreateTextFormat();
Direct2D でテキストを描画するには、テキスト形式オブジェクトを作成する必要があります。そのためには、フォント ファミリ名と目的のフォント サイズの両方が必要です。今回はユーザーが左右の矢印キーを使用してフォント サイズを変更できるようにするので、まずはサイズを追跡するメンバー変数を追加します。
float m_size;
Visual C++ コンパイラには、近々クラス内の this など静的でないデータ メンバーの初期化機能が追加される予定です。ただし今のところ、このようなデータ メンバーには 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 では、WM_KEYDOWN メッセージに相当する最新機能、KeyDown イベントが提供されています。今回はまず、SampleWindow で実装する必要がある IKeyEventHandler インターフェイスを定義します。
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 メソッドは、WM_KEYDOWN メッセージの WPARAM に相当し、システム キー以外のうちどのキーが押されたのかを示します。
VirtualKey key;
HR(args->get_VirtualKey(&key));
同様に、このインターフェイスの get_KeyStatus メソッドは WM_KEYDOWN メッセージの LPARAM に相当します。get_KeyStatus メソッドは、キー操作イベント関連の状態について豊富な情報を提供します。
CorePhysicalKeyStatus status;
HR(args->get_KeyStatus(&status));
ユーザーが使いやすいよう、このアプリではユーザーがいずれかの方向キーを長押ししたときの加速をサポートして、レンダリングされるフォントのサイズ変更速度を上げます。この機能をサポートするには、別のメンバー変数が必要です。
unsigned m_accelerate;
続いて、このイベントのキー状態を使用して、フォントサイズを 1 単位ずつ拡大するのか、またはサイズの拡大を徐々に加速するのかを指定します。
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 メソッドを呼び出して、テキスト形式オブジェクトを再作成します。
後は、SampleWindow の Draw メソッドを更新して、現在のテキスト形式が適用された任意のテキストを描画するだけです。これで処理は完了です。
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);
Direct2D レンダー ターゲットの DrawText メソッドは、DirectWrite を直接サポートしています。これで DirectWrite によりテキスト レイアウトが処理され、すばらしい速さでレンダリングされます。これだけです。図 3に、表示される画面の例を示します。上矢印キーと下矢印キーを押すとフォント ファミリを順番に表示でき、左矢印キーと右矢印キーを押すとフォント サイズを変更できます。Direct2D によって、現在選択している設定で自動的に再レンダリングされます。
図 3 フォント ブラウザー
カラー フォント (Color Font) について
Windows 8.1 では、カラー フォント (color font) という新機能が導入され、複数色のフォントの実装に必要だった多数の次善策が廃止されました。もちろん、結局は DirectWrite と Direct2D を使用することになります。さいわい、カラー フォントを使用するには Direct2D DrawText メソッドの呼び出し時に定数 D2D1_DRAW_TEXT_OPTIONS_ENABLE_COLOR_FONT を使用するだけです。今回は SampleWindow の Draw メソッドを更新して、対応するスコープ列挙値を使用します。
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++ の力を借りて真価を発揮します。
Kenny Kerr は、カナダを拠点とするコンピューター プログラマであり、Pluralsight の執筆者です。また、Microsoft MVP でもあります。彼のブログは kennykerr.ca(英語) で、Twitter は twitter.com/kennykerr(英語) でフォローできます。
この記事のレビューに協力してくれた技術スタッフの Worachai Chaoweeraprasit (マイクロソフト) に心より感謝いたします。
Worachai Chaoweeraprasit は、Direct2D と DirectWrite の開発を指揮しています。彼は、2D ベクター グラフィックスのスピードと品質に加えて、テキストの画面における可読性に夢中になっています。余暇には、自宅で 2 人の子供に追い詰められることを楽しんでいます。