DirectX の構成要素

DirectWrite によるテキストの書式設定とスクロール

Charles Petzold

コード サンプルのダウンロード

コンピューター グラフィックスにおけるテキストの表示にはいつも悩まされます。ジオメトリやビットマップなどの 2 次元グラフィックス程度の容易さでテキストを表示できれば良いのですが、テキストには昔からの習慣や読みやすさなどいくつか非常に明確なニーズがあります。

グラフィックスにおけるテキストの特殊性から、DirectX ではテキストを扱う作業を Direct2D と DirectWrite という 2 つの主要サブシステムに分けています。ID2D1RenderTarget インターフェイスは、他の 2D グラフィックスに添えてテキストを表示するメソッドを宣言するのに対し、IDWrite で始まるインターフェイスは表示するテキストの準備を支援します。

グラフィックスとテキストの境界にはいくつかの興味深い手法が用いられます。たとえば、テキスト文字の輪郭の取得、表示途中のテキストをインターセプトして操作する IDWriteTextRenderer インターフェイスの実装などがあります。ただし、テキストの書式設定の基礎については事前に十分理解しておく必要があります。つまり、見た目の美しさよりも、読まれることを意識したテキストの表示について理解します。

最もシンプルなテキスト出力

最も基本的なテキスト表示インターフェイスは IDWriteTextFormat で、フォント ファミリ (Times New Roman、Arial など)、書式 (italic や oblique)、太さ (bold や light)、伸縮 (narrow や expanded)、およびフォント サイズを組み合わせます。おそらく、次のように、IDWriteTextFormat オブジェクトの参照カウントされるポインターをヘッダー ファイルでプライベート メンバーとして定義することになります。

Microsoft::WRL::ComPtr m_textFormat;

オブジェクトは、IDWriteFactory によって定義されたメソッドを使って作成します。

dwriteFactory->CreateTextFormat(
   L"Century Schoolbook", nullptr,
   DWRITE_FONT_WEIGHT_NORMAL,
   DWRITE_FONT_STYLE_ITALIC,
   DWRITE_FONT_STRETCH_NORMAL,
   24.0f, L"en-US", &m_textFormat);

通常、アプリケーションではさまざまなフォント ファミリ、サイズ、および書式を使用するために、複数の IDWriteTextFormat オブジェクトを作成することになります。これらのオブジェクトはデバイスに依存しないリソースなので、DWriteCreateFactory を呼び出して IDWriteFactory オブジェクトを取得した後ならいつでも作成でき、アプリケーションの実行中はそのまま保持されます。

フォント ファミリ名の綴りを間違えたり、その名前のフォントがシステム上に存在しない場合は、既定のフォントが使用されます。2 つ目の引数は、フォントを検索するフォント コレクションを示します。この引数に nullptr を指定すると、システム フォント コレクションを探します。また、プライベート フォント コレクションを作成することもできます。

サイズは、1 インチあたり 96 単位の解像度を基にしたデバイスに依存しない単位で計測され、サイズ 24 は 18 ポイントのフォントに相当します。言語インジケーターは、フォント ファミリ名の言語を表しますが、空文字列にすることもできます。

いったん IDWriteTextFormat オブジェクトを作成したら、指定した情報はすべて変更できなくなります。フォント ファミリ、書式、またはサイズを変更する場合は、オブジェクトを作り直します。

IDWriteTextFormat オブジェクト以外にも、テキストをレンダリングするのに必要なものがあります。テキスト自体はもちろんですが、テキストを表示する画面上の位置と色も必要です。これらの項目は、テキストをレンダリングするときに指定します。テキストの表示場所は、座標点ではなく D2D1_RECT_F 型の四角形で示します。テキストの色は、グラデーション ブラシやイメージ ブラシなど、任意の種類のブラシを使って指定します。

以下に、典型的な DrawText の呼び出しを示します。

deviceContext->DrawText(
   L"This is text to be displayed",
   28,    // Characters
   m_textFormat.Get(),
   layoutRect,
   m_blackBrush.Get(),
   D2D1_DRAW_TEXT_OPTIONS_NONE,
   DWRITE_MEASURING_MODE_NATURAL);

既定では、四角形の幅 (上から下に向けて読む言語の場合は四角形の高さ) に基づいて複数の行に分割され、折り返されます。テキストが長すぎて四角形に収まらない場合は底辺からはみ出して表示されます。最後から 2 番目の引数はオプションのフラグです。このフラグを利用して、四角形の外側にはみ出すテキストをクリッピングする、ピクセルの境界線に文字を揃えない (これはテキストでアニメーションを実行する場合に役立ちます)、または Windows 8.1 ではマルチカラーのフォント文字を有効にするといった操作が可能です。

今回のコラムのダウンロード コードには、IDWriteTextFormat と DrawText を使って Lewis Carroll の『不思議の国のアリス』の第 7 章を表示する Windows 8.1 プログラムを含めてあります (テキストは Project Gutenberg Web サイトから取得しましたが、原書の文字体裁に近づけるためにいくらか修正しました)。このプログラムに PlainTextAlice という名前を付け、Windows 8.1 用 Visual Studio Express 2013 Preview の DirectX App (XAML) テンプレートを使用して作成しました。このプロジェクト テンプレートは、SwapChainPanel とそこに DirectX グラフィックスを表示するために必要なすべてのオーバーヘッドを含んだ XAML ファイルを生成します。

テキストを収めたファイルはプロジェクトのコンテンツに含まれています。各段落は単一行で、それぞれ空白行で区切られています。DirectXPage クラスは、Loaded イベント ハンドラーでテキストを読み込み、PlainTextAliceMain クラス (プロジェクトの一環として作成されます) に転送します。PlainTextAliceMain クラスは、受け取ったテキストを PlainTextAliceRenderer クラスに転送します。このクラスが今回の中心です。

このプログラムで表示するグラフィックスは静的なものなので、CompositionTarget::Rendering イベント用のハンドラーをアタッチしないことで、DirectXPage でレンダリング ループを無効にしています。代わりに、PlainTextAliceMain はグラフィックスを再描画するタイミングを判断します。再描画するのは、テキストが読み込まれたとき、またはアプリケーション ウィンドウのサイズや向きが変わったときです。このタイミングで、PlainTextAliceMain は PlainTextAliceRenderer の Render メソッドと DeviceResources の Present メソッドを呼び出します。

PlainTextAliceRenderer クラスの C++ 部分を図 1に示します。わかりやすくするために、HRESULT のチェックは省略しています。

図 1 PlainTextAliceRenderer.cpp ファイル

#include "pch.h"
 #include "PlainTextAliceRenderer.h"
 using namespace PlainTextAlice;
 using namespace D2D1;
 using namespace Platform;
 PlainTextAliceRenderer::PlainTextAliceRenderer(
   const std::shared_ptr& deviceResources) :
   m_text(L""),
   m_deviceResources(deviceResources)
 {
   m_deviceResources->GetDWriteFactory()->
     CreateTextFormat(L"Century Schoolbook",
                      nullptr,
                      DWRITE_FONT_WEIGHT_NORMAL,
                      DWRITE_FONT_STYLE_NORMAL,
                      DWRITE_FONT_STRETCH_NORMAL,
                      24.0f,
                      L"en-US",
                      &m_textFormat);
   CreateDeviceDependentResources();
 }
 void PlainTextAliceRenderer::CreateDeviceDependentResources()
 {
   m_deviceResources->GetD2DDeviceContext()->
     CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::Black),
                           &m_blackBrush);
   m_deviceResources->GetD2DFactory()->
     CreateDrawingStateBlock(&m_stateBlock);
 }
 void PlainTextAliceRenderer::CreateWindowSizeDependentResources()
 {
   Windows::Foundation::Size windowBounds =
     m_deviceResources->GetOutputBounds();
   m_layoutRect = RectF(50, 0, windowBounds.Width - 50,
       windowBounds.Height);
 }
 void PlainTextAliceRenderer::ReleaseDeviceDependentResources()
 {
   m_blackBrush.Reset();
   m_stateBlock.Reset();
 }
 void PlainTextAliceRenderer::SetAliceText(std::wstring text)
 {
   m_text = text;
 }
 void PlainTextAliceRenderer::Render()
 {
   ID2D1DeviceContext* context = 
     m_deviceResources->GetD2DDeviceContext();
   context->SaveDrawingState(m_stateBlock.Get());
   context->BeginDraw();
   context->Clear(ColorF(ColorF::White));
   context->SetTransform(m_deviceResources->GetOrientationTransform2D());
   context->DrawText(m_text.c_str(),
                     m_text.length(),
                     m_textFormat.Get(),
                     m_layoutRect,
                     m_blackBrush.Get(),
                     D2D1_DRAW_TEXT_OPTIONS_NONE,
                     DWRITE_MEASURING_MODE_NATURAL);
   HRESULT hr = context->EndDraw();
   context->RestoreDrawingState(m_stateBlock.Get());
 }

m_layoutRect メンバーはアプリケーションの画面サイズに基づいて計算されますが、左右に 50 ピクセルの余白を残します。結果を図 2 に示します。


図 2 PlainTextAlice プログラム

まず、このプログラムが優れている点にいくつか気付きます。明らかに、オーバーヘッドが最小限に抑えられ、テキストは個々の段落で適切に折り返されます。

PlainTextAlice プロジェクトで不十分な点も明らかです。段落間に余白があるのは、段落を分けるために元のテキスト ファイルに空白行が挿入されているためです。段落間の隙間を狭めたり、広げたりしたくてもできません。さらに、テキスト内の斜体文字や太字文字を示す方法がありません。

しかし、最大の問題は章の冒頭しか見えないことです。スクロール ロジックを実装することはできますが、スクロールする距離をどのように判断すればよいでしょう。IDWriteTextFormat と DrawText の本当に大きな問題は、書式を設定し、レンダリングしたテキストの高さを利用できないことです。

結論としては、DrawText を使用する意味は、テキストに統一の書式を設定でき、指定した四角形にテキストが十分に収まるか、少しぐらいはみ出しても問題がない場合のみということになります。より洗練された用途にはもっと優れた方法が必要です。

話を進める前に、CreateTextFormat メソッドで指定した情報は IDWriteTextFormat オブジェクト内では不変ですが、インターフェイスが宣言するいくつかのメソッドを使えば、テキストの表示方法は変更できます。たとえば、SetParagraphAlignment は DrawText で指定した四角形内でテキストの縦方向の位置を変更し、SetTextAlignment は段落の行を四角形の左揃え、右揃え、中央揃え、または両端揃えに設定できます。これらのメソッドの引数は、上から下、または右から左に読むテキストのために一般化され、Near (近く、上端)、Far (遠く、下端)、Leading (先頭、左端)、Trailing (末尾、右端) などの単語を使用します。行間、テキストの折り返し、およびタブ位置も制御できます。

テキストのレイアウト方法

IDWriteTextFormat の次のステップは、ヒットテストを含めて、テキスト出力の標準ニーズほぼすべてに最適な IDWriteTextLayout 型のオブジェクトです。このオブジェクトは IDWriteTextFormat から派生されているだけではなく、インスタンスが作成されるときに IDWriteTextFormat オブジェクトを組み込みます。

以下に、IDWriteTextLayout オブジェクトの参照カウントされるポインターを示します。これは、おそらくヘッダー ファイルで宣言することになります。

Microsoft::WRL::ComPtr m_textLayout;

たとえば、次のようなオブジェクトを作成します。

dwriteFactory->CreateTextLayout(
   pText, wcslen(pText),
   m_textFormat.Get(),
   maxWidth, maxHeight,
   &m_textLayout);

IDWriteTextFormat インターフェイスとは異なり、IDWriteTextLayout にはテキスト自体と、テキストの書式を設定するためにテキストを囲む四角形の高さと幅を指定します。IDWriteTextLayout オブジェクトを表示する DrawTextLayout メソッドには、書式設定したテキストの左上隅の位置を示す 2D 座標のみが必要です。

deviceContext->DrawTextLayout(
   point,
   m_textLayout.Get(),
   m_blackBrush.Get(),
   D2D1_DRAW_TEXT_OPTIONS_NONE);

IDWriteTextLayout オブジェクトには、テキストをレンダリングする前に改行数を計算するために必要な情報がすべて含まれているので、レンダリング後のテキストのサイズもわかります。IDWriteTextLayout には、GetMetrics、GetOverhangMetrics、GetLineMetrics、GetClusterMetrics など複数のメソッドがあり、このテキストを効果的に操作するのに役立つ豊富な情報を提供します。たとえば、GetMetrics は書式設定後のテキストの合計幅と高さ、行数などの情報を提供します。

CreateTextLayout メソッドには最大幅と高さの引数がありますが、これらの値は後から他の値に変更できます (ただし、テキスト自体は変更できません)。表示領域が変化する場合 (タブレットの向きが横長から縦長に変わるなど)、IDWriteTextLayout オブジェクトを作り直す必要はありません。インターフェイスによって宣言された SetMaxWidth メソッドと SetMaxHeight メソッドを呼び出すだけです。実は、最初に IDWriteTextLayout オブジェクトを作成するときに最大幅と高さの引数を 0 に設定してもかまいせん。

ParagraphFormattedAlice プロジェクトは、IDWriteTextLayout と DrawTextLayout を使用します。結果を図 3に示します。まだ、テキストの残りの部分を表示するためにスクロールすることはできませんが、一部の行が中央揃えになり、段落の 1 行目がインデントされています。タイトルには、他の段落よりも大きなサイズのフォントが使用されており、いくつかの単語が斜体になっています。


図 3 ParagraphFormattedAlice プログラム

このプロジェクトのテキスト ファイルは、最初のプロジェクトとは少し異なります。つまり、このプロジェクトでも各段落は 1 行ですが、段落を分ける空白行がありません。テキスト全体に 1 つの IDWriteTextFormat オブジェクトを使用するのではなく、ParagraphFormattedAlice の各段落は別の DrawTextLayout の呼び出しでレンダリングした固有の IDWriteTextLayout オブジェクトです。そのため、段落間の隙間は自由に広げたり、狭めたりできます。

テキストを操作するために、Paragraph という構造体を定義しています。

struct Paragraph
    {
      std::wstring Text;
      ComPtr<IDWriteTextLayout> TextLayout;
      float TextHeight;
      float SpaceAfter;
    };

AliceParagraphGenerator というヘルパー クラスは、テキストの行に基づいて Paragraph オブジェクトのコレクションを生成します。

IDWriteTextLayout には、個別の単語やテキストのブロックに書式を設定するメソッドがたくさんあります。たとえば、テキストのオフセット 23 から始まる 5 つの文字を斜体にするには以下のようにします。

DWRITE_TEXT_RANGE range = { 23, 5 };
 textLayout-&gt;SetFontStyle(DWRITE_FONT_STYLE_ITALIC, range);

フォント ファミリ、コレクション、サイズ、太さ、伸縮、下線、および取り消し線にも、同様のメソッドがあります。実際のアプリケーションでは、テキストの書式設定は通常マークアップ (HTML など) で定義しますが、AliceParagraphGenerator クラスではシンプルに保つためプレーンテキスト ファイルで作業し、斜体にする単語の位置はハードコーディングしています。

図 4に示すように、AliceParagraphGenerator には、新しい表示幅を設定する SetWidth メソッドもあります (わかりやすくするために、HRESULT のチェックは省略しています)。表示幅は、ウィンドウ サイズが変化したときか、タブレットの向きが変わったときに変更します。SetWidth はすべての Paragraph オブジェクトをループし、TextLayout の SetMaxWidth を呼び出し、TextHeight に保存する段落の書式設定後の新しい高さを取得します。これまでに、SpaceAfter フィールドはほとんどの段落を 12 ピクセルに、見出しを 36 ピクセルに、定型文の数行を 0 に設定しています。そのため、各段落の高さと、章のすべてのテキストの高さの合計を容易に取得できます。

図 4 AliceParagraphGenerator の SetWidth メソッド

float AliceParagraphGenerator::SetWidth(float width)
 {
   if (width &lt;= 0)
     return 0;
   float totalHeight = 0;
   for (Paragraph& paragraph : m_paragraphs)
   {
     HRESULT hr = paragraph.TextLayout-&gt;SetMaxWidth(width);
     hr = paragraph.TextLayout-&gt;SetMaxHeight(FloatMax());
     DWRITE_TEXT_METRICS textMetrics;
     hr = paragraph.TextLayout-&gt;GetMetrics(&textMetrics);
     paragraph.TextHeight = textMetrics.height;
     totalHeight += paragraph.TextHeight + paragraph.SpaceAfter;
   }
   return totalHeight;
 }

ParagraphFormattedAliceRenderer の Render メソッドは、Paragraph オブジェクトをすべてループし、テキストの累積した高さに基づき異なる原点を指定して DrawTextLayout を呼び出します。

1 行目のインデントの問題

図 3に示すように、ParagraphFormattedAlice プログラムは、段落の 1 行目をインデントしています。こうしたインデントを行う最も簡単な方法は、テキスト文字列の先頭にスペースを挿入することです。Unicode の標準では、全角スペース (ポイント サイズと同じ幅を持つ)、半角スペース (全角スペースの半分)、全角の 4 分の 1 のスペース、およびそれより小さいスペースのコードが定義されているため、必要なインデント量だけこれらのスペースを組み合わせます。便利なのは、インデントをフォントのポイント サイズに比例させることです。

ただし、この方法はぶら下げインデント (先頭行が段落の残りの行よりも左から始まるインデント) と呼ばれる負のインデントには使用できません。さらに、1 行目のインデントは一般にポイント サイズとは無関係のメトリックス (半インチなど) で指定されます。

これらの理由から、ここでは IDWriteTextLayout の SetInlineObject メソッドを使って、1 行目のインデントを実装しています。このメソッドでは、テキストを含むグラフィカル オブジェクト インラインを配置できるようにしていて、このインラインは行の折り返しを考慮したサイズを持つ個別の単語のように扱います。

SetInlineObject メソッドは、一般にテキストに小さなビットマップを挿入するために使用します。SetInlineObject メソッドをこの目的や他の目的で使用するために、3 つの標準の IUnknown メソッドに加えて、GetMetrics、GetOverhangMetrics、GetBreakConditions、および Draw の各メソッドを宣言する IDWriteInlineObject インターフェイスを実装するクラスを作成します。基本的に、このクラスはテキストの測定中またはレンダリング中に呼び出されます。FirstLineIndent クラスでは、目的のインデントをピクセル単位で示す引数を受け取るコンストラクターを定義します。インデント値は基本的には埋め込みオブジェクトのサイズを示すために GetMetrics 呼び出しによって返されます。クラスの Draw 実装は何も行いません。負の値を指定すると、ぶら下げインデントとして問題なく機能します。

IDWriteFontLayout の SetInlineObject は、SetFontStyle やテキストの範囲に基づく他のメソッドと同様に呼び出せますが、SetInlineObject の使用を開始してみて、範囲に長さ 0 を指定できないことがわかりました。つまり、インライン オブジェクトだけを挿入することはできません。インライン オブジェクトは、少なくともテキストの 1 文字と置き換える必要があります。このために、Paragraph オブジェクトの定義中に、コードは各行の先頭に幅のない空白文字 (‘\x200B’) を挿入します。これには目に見えませんが、SetInlineObject が呼び出されたときに置き換えられます。

DIY スクロール

ParagraphFormattedAlice プログラムはスクロールしませんが、特にレンダリングされたテキストの合計の高さなど、スクロールを実装するのに必要な情報はすべて含まれています。ScrollableAlice プロジェクトは、スクロールの 1 つの方法をデモします。このプログラムでも、SwapChainPanel にプログラムのウィンドウ サイズを出力しますが、ユーザーのマウスまたはタッチ入力を基にレンダリングをオフセットします。ここでは、この方法を DIY (Do It Yourself) スクロールと呼んでいます。

ScrollableAlice プロジェクトは、以前のプロジェクトに使用したのと同じ Visual Studio 2013 Preview の DirectX App (XAML) テンプレートを使用して作成しましたが、このテンプレートの他の興味深い側面を活用しています。テンプレートの DirectXPage.cpp には、実行のセカンダリ スレッドを作成し SwapChainPanel の Pointer イベントを処理するコードがあります。こうすることで、この入力で UI スレッドが待機状態になることを回避します。

当然、セカンダリ スレッドを導入することによって複雑になります。スクロールになんらかの慣性を導入するために、Pointer イベントではなく Manipulation イベントを使用します。このためには、(このセカンダリ スレッドでも) DirectXPage を使用して GestureRecognizer オブジェクトを作成する必要があります。このオブジェクトは、Pointer イベントから Manipulation イベントを生成します。

前の ParagraphFormattedAlice プログラムは、テキストを読み込むときと、ウィンドウ サイズが変化するときにウィンドウを再描画していました。ScrollableAlice もこれと同じタイミングで、再描画を UI スレッドで連続的に行います。ScrollableAlice は ManipulationUpdated イベントが発生したときもウィンドウを再描画しますが、これはポインター入力のために作成したセカンダリ スレッドで行います。

テキストを指でうまくフリックして、慣性でスクロールを続けるようにし、テキストのスクロール中にウィンドウのサイズを変更したらどうなるでしょう。オーバーラップする DirectX 呼び出しが同時に 2 つの異なるスレッドから行われる可能性が少なからずあり、これは問題です。

必要なのはなんらかのスレッド同期と、Concurrency 名前空間の critical_section クラスを利用する、適切かつ簡単なソリューションです。ヘッダー ファイルで、次のコードを宣言します。

Concurrency::critical_section m_criticalSection;

critical_section クラスには、scoped_lock という埋め込みクラスがあります。次のステートメントは、critical_section オブジェクトを指定してコンストラクターを呼び出すことで、scoped_lock 型の lock というオブジェクトを作成します。

critical_section::scoped_lock lock(m_criticalSection);

コンストラクターは m_criticalSection オブジェクトの所有権を想定します。つまり、m_criticalSection オブジェクトが他のスレッドに所有されている場合は実行をブロックします。この scoped_lock クラスが優れている点は、lock オブジェクトがスコープ外になったときにデストラクターが m_criticalSection の所有権をリリースすることです。これにより、同時に呼び出されるあるさまざまなメソッドで、これらを多数簡単に使用できます。

このクリティカル セクションは、DirectXPage で実装するのが最も簡単だと判断しました。ここには、実行中に他のスレッドをブロックする必要がある DeviceResources クラスの重要な呼び出し (UpdateForWindowSizeChanged など) がいくつか含まれます。UI スレッドをブロックする (ポインター イベントが発生したときに起こります) のはあまりよくありませんが、このようなブロックはごく短期間です。

スクロール情報を ScrollableAliceRenderer クラスに提供するまでは、そのスクロール情報を m_scrollOffset という変数に浮動小数点形式で格納します。この値は、0 からテキストの章全体の高さとウィンドウの高さの差に相当する最大値の間に保たれます。Render メソッドは、この値を使って段落の表示の開始と終了の方法を決めます (図 5 参照)。

図 5 ScrollableAlice におけるスクロールの実装

std::vector paragraphs =
   m_aliceParagraphGenerator.GetParagraphs();
 float outputHeight = m_deviceResources-&gt;GetOutputBounds().Height;
 D2D1_POINT_2F origin = Point2F(50, -m_scrollOffset);
 for (Paragraph paragraph : paragraphs)
 {
   if (origin.y + paragraph.TextHeight + paragraph.SpaceAfter &gt; 0)
     context-&gt;DrawTextLayout(origin, paragraph.TextLayout.Get(),
     m_blackBrush.Get());
   origin.y += paragraph.TextHeight + paragraph.SpaceAfter;
   if (origin.y &gt; outputHeight)
     break;
 }

バウンスを伴うスクロール

ScrollableAlice プログラムは慣性を伴うスクロールは実装しますが、上部または下部を超えてスクロールする場合に、Windows 8 のバウンス特性を備えていません。この (これまでのところ) 使い慣れたバウンスは ScrollViewer に組み込まれており、自身のコードで ScrollViewer バウンスに相当する特性を組み込むのも興味深いとは思いますが、今回は独自には作成しません。

スクロール可能なテキストは長くなることがあるため、すべてを 1 つのサーフェイスやビットマップにレンダリングしようとしない方が良いでしょう。DirectX ではこのようなサーフェイスのサイズが制限されています。ScrollableAlice プログラムでは SwapChainPanel への表示をプログラムのウィンドウ サイズに制限することで、これらの制限を回避しています。この方法は問題なく機能します。ScrollViewer を機能させるには、書式設定後のテキストの完全な高さを反映するレイアウトでのコンテンツのサイズが必要です。

さいわい、Windows 8 は、まさにこのために必要な要素をサポートしています。VirtualSurfaceImageSource クラスは SurfaceImageSource から派生し、SurfaceImageSource は ImageSource から派生するため、VirtualSurfaceImageSource クラスが ScrollViewer の Image 要素のビットマップ ソースとして機能します。VirtualSurfaceImageSource は、任意のサイズに設定でき (また、作り直さなくてもサイズ変更できます)、サーフェイス領域を仮想化して、オンデマンドの描画を実装することで、DirectX のサイズ制限を回避します (ただし、SurfaceImageSource と VirtualSurfaceImageSource は、高いパフォーマンスが求められるフレームベースのアニメーションには最適ではありません)。

VirtualSurfaceImageSource は、Windows ランタイムの ref クラスです。これを DirectX と併用するには、IVirtualSurfaceImageSourceNative 型のオブジェクトにキャストする必要があります。このインターフェイスは、オンデマンドの描画を実装するために使用するメソッドを公開します。これらのメソッドは、どの四角形領域を更新する必要があるかを報告し、IVirtualSurfaceUpdatesCallbackNative を実装するクラスを提供することで、新しく更新する四角形をプログラムに通知できるようにします。

BounceScrollableAlice プロジェクトはこの手法を示しており、SwapChainPanel が必要なかったため、Blank App (XAML) テンプレートに基づいて Visual Studio 2013 Preview で作成しました。IVirtualSurfaceUpdatesCallbackNative を実装する必須のクラスのために、VirtualSurfaceImageSourceRenderer というクラスを作成します。これは、DirectX オーバーヘッドの大半を提供します。AliceVsisRenderer クラスは VirtualSurfaceImageSourceRenderer から派生し、Alice 固有の描画を提供します。

IVirtualSuraceImageSourceNative で利用できる更新四角形は、VirtualSurfaceImageSource の全サイズに対して相対ですが、描画座標は更新四角形に対して相対です。これは、BounceScrollableAlice の DrawTextLayout の呼び出しは、初期の原点がスクロールのオフセットではなく更新四角形の上部に負の値で設定されており、outputHeight 値が更新する四角形の底辺と上辺の差であることを除けば、図 5 で示したものと事実上同じです。

ScrollViewer を併用することで、テキストの表示は真に Windows 8 のようになり、ピンチ ジェスチャーを使って拡大縮小することもできます。ここからも、DirectX と XAML を混合したこの Windows 8 は、両方にとって最高に近いものを提供することがわかります。

Charles Petzold は MSDN マガジンの記事を長期にわたって担当しており、Windows 8 向けのアプリケーション開発についての書籍『Programming Windows, 6th Edition』(Microsoft Press、2012 年) の著者でもあります。彼の Web サイトは charlespetzold.com(英語) です。

この記事のレビューに協力してくれた技術スタッフの Jim Galasyn (マイクロソフト) および Justin Panian (マイクロソフト) に心より感謝いたします。