Windows と C++

Windows 合成エンジンを使用する

Kenny Kerr

Kenny KerrWindows 合成エンジンは、DirectX アプリケーションごとに専用のスワップ チェーンが必要な世界から、コンストラクトなどのきわめて基本的な要素さえ不要な世界への飛躍を体現しています。もちろん、Windows 合成エンジンを使用する場合も、スワップ チェーンを表示に使用する Direct3D アプリケーションや Direct2D アプリケーションを作成できますが、必須ではなくなります。Windows 合成エンジンを使用すると、合成サーフェスをアプリケーションで直接作成できるようになるので、開発者とハードウェア (GPU) の距離がぐっと縮まります。

Windows 合成エンジンの唯一の目的は、異なるビットマップを 1 つに合成することです。さまざまな効果、変形、およびアニメーションの生成を要求する場合もあるでしょうが、最終的な目標はビットマップを合成することです。Windows 合成エンジン自体には、Direct2D や Direct3D で提供されているようなグラフィックス レンダリング機能がなく、ベクトルもテキストも定義しません。Windows 合成エンジンの役割は、合成です。Windows 合成エンジンにビットマップのコレクションを渡せば、優れた処理によってビットマップが組み合わされ、合成されます。

合成エンジンに渡すビットマップの種類数に、制限はありません。ビットマップが本当はビデオ メモリの場合もあれば、スワップ チェーンの場合もあります。スワップ チェーン形式のビットマップの詳細については、6 月号のコラム (msdn.microsoft.com/magazine/dn745861) を参照してください。しかし、本気で Windows 合成エンジンを使用するつもりの場合は、合成サーフェスについて知る必要があります。合成サーフェスは、Windows 合成エンジンから直接提供されるビットマップです。このため、合成サーフェスを使用すると、他の形式のビットマップでは実現が難しい最適化を実行できます。

今月は、前回のコラムで説明したアルファ ブレンドが行われたウィンドウを取り上げて、スワップ チェーンの代わりに合成サーフェスを使用してこのウィンドウを再現する方法を示します。この手法には興味深いメリットもあれば、固有の課題 (特に、デバイスを利用できない状況やモニターごとの DPI スケールに関する課題) もあります。しかし、まずはウィンドウ管理の問題を再検討する必要があります。

以前のコラムでは、ウィンドウ管理に ATL を使用したことも、Windows API で直接ウィンドウ メッセージを登録、作成、および表示する方法を説明したこともありました。どちらのアプローチにも、メリットとデメリットがあります。ATL は今でも問題なくウィンドウ管理用に使用できますが、開発者の支持を徐々に失いつつあり、マイクロソフトもかなり前に投資を打ち切っています。一方、RegisterClass 関数と CreateWindow 関数を使用して直接ウィンドウを作成すると、C++ オブジェクトとウィンドウ ハンドルの関連付けが簡単ではないので問題が発生しやすくなります。C++ オブジェクトとウィンドウ ハンドルを関連付けようと考えたことがある方は、ATL のソース コードを参照してその実現方法を確認しようとしたものの "サンク" と呼ばれる代物やさらにはアセンブリ言語を利用して難解な処理を行っていることしかわからなかった、という経験をお持ちではないでしょうか。

さいわい、これほど難解な処理は必要ありません。確かに ATL からは非常に効率的なメッセージのディスパッチが生成されますが、この処理を実現するには、標準的な C++ だけを使用するシンプルなソリューションで十分です。ウィンドウ プロシージャのしくみの説明で大幅に脱線するつもりはないので、単刀直入に図 1 を示します。この図は、"this" ポインターとウィンドウの関連付けに必要な準備を行う、シンプルなクラス テンプレートを表しています。このテンプレートでは、WM_NCCREATE メッセージを使用してポインターを抽出し、ウィンドウ ハンドルを使用してポインターを保存します。続いて、ポインターを取得し、最も派生階層が深いメッセージ ハンドラーにメッセージを送信します。

図 1 シンプルな Window クラス テンプレート

template <typename T>
struct Window
{
  HWND m_window = nullptr;
  static T * GetThisFromHandle(HWND window)
  {
    return reinterpret_cast<T *>(GetWindowLongPtr(window,
                                                  GWLP_USERDATA));
  }
  static LRESULT __stdcall WndProc(HWND   const window,
                                   UINT   const message,
                                   WPARAM const wparam,
                                   LPARAM const lparam)
  {
    ASSERT(window);
    if (WM_NCCREATE == message)
    {
        CREATESTRUCT * cs = reinterpret_cast<CREATESTRUCT *>(lparam);
        T * that = static_cast<T *>(cs->lpCreateParams);
        ASSERT(that);
        ASSERT(!that->m_window);
        that->m_window = window;
        SetWindowLongPtr(window,
                         GWLP_USERDATA,
                         reinterpret_cast<LONG_PTR>(that));
    }
    else if (T * that = GetThisFromHandle(window))
    {
      return that->MessageHandler(message,
                                  wparam,
                                  lparam);
    }
    return DefWindowProc(window,
                         message,
                         wparam,
                         lparam);
  }
  LRESULT MessageHandler(UINT   const message,
                         WPARAM const wparam,
                         LPARAM const lparam)
  {
    if (WM_DESTROY == message)
    {
      PostQuitMessage(0);
      return 0;
    }
    return DefWindowProc(m_window,
                         message,
                         wparam,
                         lparam);
  }
};

前提として、CreateWindow 関数または CreateWindowEx 関数を呼び出す場合には、派生クラスでウィンドウを作成し、最後のパラメーターとしてこのポインターを渡すことを想定しています。派生クラスは、ウィンドウを登録して作成したら、MessageHandler のオーバーライドでウィンドウ メッセージに応答できます。このオーバーライドはコンパイル時のポリモーフィズムを利用しているので、仮想関数は必要ありません。ただし、効果は同じなので、やはり再入を考慮する必要があります。図 2 に、Window クラス テンプレートを利用する具体的なウィンドウ クラスを示します。このクラスは、コンストラクターでウィンドウを登録して作成しますが、クラスで使用するウィンドウ プロシージャは基底クラスから提供されています。

図 2 具体的なウィンドウ クラス

struct SampleWindow : Window<SampleWindow>
{
  SampleWindow()
  {
    WNDCLASS wc = {};
    wc.hCursor       = LoadCursor(nullptr, IDC_ARROW);
    wc.hInstance     = reinterpret_cast<HINSTANCE>(&__ImageBase);
    wc.lpszClassName = L"SampleWindow";
    wc.style         = CS_HREDRAW | CS_VREDRAW;
    wc.lpfnWndProc   = WndProc;
    RegisterClass(&wc);
    ASSERT(!m_window);
    VERIFY(CreateWindowEx(WS_EX_NOREDIRECTIONBITMAP,
                          wc.lpszClassName,
                          L"Window Title",
                          WS_OVERLAPPEDWINDOW | WS_VISIBLE,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          nullptr,
                          nullptr,
                          wc.hInstance,
                          this));
    ASSERT(m_window);
  }
  LRESULT MessageHandler(UINT message,
                         WPARAM const wparam,
                         LPARAM const lparam)
  {
    if (WM_PAINT == message)
    {
      PaintHandler();
      return 0;
    }
    return __super::MessageHandler(message,
                                   wparam,
                                   lparam);
  }
  void PaintHandler()
  {
    // Render ...
  }
};

図 2 のコンストラクターで注意していただきたいのですが、m_window という継承メンバーが、CreateWindowEx 関数の呼び出し前は初期化されていなかった (nullptr) のに CreateWindowEx 関数が戻り値を返した後は初期化されています。これは不思議な状況に思えるかもしれませんが、メッセージの受信開始時、つまり CreateWindowEx 関数が値を返すよりもはるかに早い時点で、ウィンドウ プロシージャによってこの初期化が行われています。この注意事項が重要な理由は、図 2 のようなコードを使用すると、コンストラクターでの仮想関数呼び出しに匹敵する危険な結果が再現されるおそれがあるためです。さらに派生を重ねる場合は、この種の再入による問題が発生しないように、念のためウィンドウ作成処理をコンストラクターの外部に取り出します。以下に、ウィンドウを作成してウィンドウ メッセージを表示できる、シンプルな WinMain 関数を示します。

int __stdcall wWinMain(HINSTANCE, HINSTANCE, PWSTR, int)
{
  SampleWindow window;
  MSG message;
  while (GetMessage(&message, nullptr, 0, 0))
  {
    DispatchMessage(&message);
  }
}

では、今回の本題に戻りましょう。シンプルなウィンドウ クラスの抽象化が完成したので、この抽象化を使用して、DirectX アプリケーションの構築に必要なリソースのコレクションを管理しやすくします。また、DPI スケールの適切な調整方法も説明します。DPI スケールについては 2014 年 2 月号のコラム (msdn.microsoft.com/magazine/dn574798) で詳しく説明しましたが、DPI スケールを DirectComposition API と組み合わせる場合には固有の課題があります。最初から説明しましょう。次に示すように、シェル スケール API を含める必要があります。

#include <ShellScalingAPI.h>
#pragma comment(lib, "shcore")

これで、ウィンドウ作成に必要なリソースの組み立てを始められるようになりました。ウィンドウ クラスがあれば、ウィンドウ作成に必要なこのクラスのメンバーを作成できます。最初のメンバーは、Direct3D デバイスです。

ComPtr<ID3D11Device> m_device3d;

次のメンバーは、DirectComposition デバイスです。

ComPtr<IDCompositionDesktopDevice> m_device;

前回のコラムでは、IDCompositionDevice インターフェイスを使用して合成デバイスを表しました。このインターフェイスは Windows 8 で導入されたインターフェイスですが、その後 Windows 8.1 で IDCompositionDesktopDevice インターフェイスに置き換えられました。IDCompositionDesktopDevice インターフェイスは、IDCompositionDevice2 という別の新しいインターフェイスから派生しています。2 つのインターフェイスには元のインターフェイスとの関係がありません。IDCompositionDevice2 インターフェイスは、ほとんどの合成リソースを作成し、トランザクション合成も制御します。IDCompositionDesktopDevice インターフェイスは、ウィンドウ固有の特定の合成リソースを作成する機能を追加します。

また、合成ターゲット、合成ビジュアル、および合成サーフェスもウィンドウ作成に必要です。

ComPtr<IDCompositionTarget>  m_target;
ComPtr<IDCompositionVisual2> m_visual;
ComPtr<IDCompositionSurface> m_surface;

合成ターゲットは、デスクトップ ウィンドウとビジュアル ツリーの間における一対一のバインドを表します。実際には 2 つのビジュアル ツリーを特定のウィンドウと関連付けることもできますが、詳細については今後のコラムで説明します。ビジュアルは、ビジュアル ツリーのノードを表します。ビジュアルについては次回のコラムで説明するので、今回のところは単一のルート ビジュアルを想定します。この例では、前回のコラムで使用した IDCompositionVisual インターフェイスから派生している、IDCompositionVisual2 インターフェイスを使用しています。最後は、ビジュアルに関連付けられているコンテンツまたはビットマップを表すサーフェスです。前回のコラムではビジュアルのコンテンツとして単にスワップ チェーンを使用しましたが、今回は、スワップ チェーンの代わりに合成サーフェスを作成する方法について説明していきます。

実際に何かをレンダリングする方法とレンダリング リソースを管理する方法について説明するには、メンバー変数がもう少し必要です。

ComPtr<ID2D1SolidColorBrush> m_brush;
D2D_SIZE_F                   m_size;
D2D_POINT_2F                 m_dpi;
SampleWindow() :
  m_size(),
  m_dpi()
{
  // RegisterClass / CreateWindowEx as before
}

Direct2D 単色ブラシの作成はかなり簡単ですが、他の多くのレンダリング リソースの作成はそれほど簡単ではありません。この Direct2D 単色ブラシを使用して、レンダリング ループの外部でレンダリング リソースを作成する方法について説明しましょう。DirectComposition API には、Direct2D レンダー ターゲットを作成するオプション機能もあります。この機能を使用すると、Direct2D で合成サーフェスをターゲットに指定できますが、コンテキスト情報が少し失われることにもなります。具体的には、該当する DPI の倍率を DirectComposition で必要に応じて作成するので、レンダー ターゲットにこの倍率をキャッシュできなくなります。また、レンダー ターゲットの GetSize メソッドを使用してウィンドウのサイズを報告することもできなくなります。しかし、心配無用です。このようなデメリットを埋め合わせる方法についてこれから説明します。

Direct3D デバイスに依存するすべてのアプリケーションと同様に、デバイスを利用できなくなる可能性が常にあると想定して、物理デバイス上のリソースを慎重に管理する必要があります。GPU には、ハング、リセット、取り外し、または単なるクラッシュが発生する可能性があります。さらに、デバイス スタックの作成前に受信する可能性があるウィンドウ メッセージに対して、不適切に応答しないよう注意する必要もあります。ここでは、Direct3D デバイス ポインターを使用して、デバイスが作成されているかどうかを示します。

bool IsDeviceCreated() const
{
  return m_device3d;
}

このコードは問い合わせを明示的にしているだけです。また、このポインターは、デバイス スタックのリセットを開始してすべてのデバイス依存リソースを強制的に再作成するためにも使用します。

void ReleaseDeviceResources()
{
  m_device3d.Reset();
}

このコードも再作成操作を明示的にしているだけです。ここですべてのデバイス依存リソースを解放することもできますが、必須ではありません。さまざまなリソースの追加と削除を繰り返すと、たちまち保守で問題になる可能性があります。デバイス作成処理の本体は、別のヘルパー メソッドにあります。

void CreateDeviceResources()
{
  if (IsDeviceCreated()) return;
  // Create devices and resources ...
}

この CreateDeviceResources メソッドこそ、デバイス スタック、ハードウェア デバイス、およびウィンドウに必要な、さまざまなリソースを作成または再作成できる場所です。まず、他のすべての項目を格納する Direct3D デバイスを作成します。

HR(D3D11CreateDevice(nullptr,    // Adapter
                     D3D_DRIVER_TYPE_HARDWARE,
                     nullptr,    // Module
                     D3D11_CREATE_DEVICE_BGRA_SUPPORT,
                     nullptr, 0, // Highest available feature level
                     D3D11_SDK_VERSION,
                     m_device3d.GetAddressOf(),
                     nullptr,    // Actual feature level
                     nullptr));  // Device context

作成されたインターフェイス ポインターを m_device3d メンバーで取得する方法に注目してください。次に、デバイスの DXGI インターフェイスを問い合わせる必要があります。

ComPtr<IDXGIDevice> devicex;
HR(m_device3d.As(&devicex));

前回のコラムではこの段階で、合成に使用する DXGI ファクトリとスワップ チェーンをこの段階で作成しました。つまり、スワップ チェーンを作成して Direct2D ビットマップでラップし、デバイス コンテキストでこのビットマップをターゲットにするなどの処理を行いました。今回は、まったく異なる方法を使用します。Direct3D デバイスを作成したので、今度はこの Direct3D デバイスを参照する Direct2D デバイスを作成し、さらに Direct2D デバイスを参照する DirectComposition デバイスを作成します。Direct2D デバイスの作成は次のとおりです。

ComPtr<ID2D1Device> device2d;
HR(D2D1CreateDevice(devicex.Get(),
                    nullptr, // Default properties
                    device2d.GetAddressOf()));

なじみ深い Direct2D ファクトリ オブジェクトではなく、Direct2D API に用意されているヘルパー関数を使用しています。作成した Direct2D デバイスは DXGI デバイスのスレッド モデルを継承しているだけですが、オーバーライドしてデバッグ トレースを有効にすることもできます。DirectComposition デバイスの作成は次のとおりです。

HR(DCompositionCreateDevice2(
   device2d.Get(),
   __uuidof(m_device),
   reinterpret_cast<void **>(m_device.ReleaseAndGetAddressOf())));

デバイスが利用できなくなった後のデバイス スタックの再作成をサポートするように、m_device メンバーの ReleaseAndGetAddressOf メソッドを慎重に使用しています。また、合成デバイスが作成済みなので、前回のコラムと同じ方法で合成ターゲットを作成できます。

HR(m_device->CreateTargetForHwnd(m_window,
                                 true, // Top most
                                 m_target.ReleaseAndGetAddressOf()));

ルート ビジュアルの作成は次のとおりです。

HR(m_device->CreateVisual(m_visual.ReleaseAndGetAddressOf()));

では、スワップ チェーンに代わる合成サーフェスに目を向けましょう。CreateSwapChainForComposition メソッドを呼び出す時点ではスワップ チェーンのバッファーに必要なサイズを DXGI ファクトリで判断できないように、DirectComposition デバイスでも、基盤となるサーフェスに必要なサイズを判断できません。ウィンドウのクライアント領域のサイズを問い合わせ、この情報を使用してサーフェスの作成を通知する必要があります。

RECT rect = {};
VERIFY(GetClientRect(m_window,
                     &rect));

RECT 構造体には left メンバー、top メンバー、right メンバー、および bottom メンバーがあり、これら 4 つのメンバーを使用すると、作成するサーフェスの適切なサイズを物理ピクセル単位で特定できます。

HR(m_device->CreateSurface(rect.right - rect.left,
                           rect.bottom - rect.top,
                           DXGI_FORMAT_B8G8R8A8_UNORM,
                           DXGI_ALPHA_MODE_PREMULTIPLIED,
                           m_surface.ReleaseAndGetAddressOf()));

要求したサイズより実際のサーフェスが大きくなる可能性があることに注意してください。なぜなら、合成エンジンでは効率向上のために割り当てをプールしたり丸めたりするためです。これは問題ではありませんが、GetSize メソッドを使用できなくなるので、結果のデバイス コンテキストに影響します。この詳細については後で説明します。

さいわい、CreateSurface メソッドに渡すパラメーターは、DXGI_SWAP_CHAIN_DESC1 構造体の要素の多くを単純化したものです。サイズに続いてピクセル形式とアルファ モードを指定すると、合成デバイスから、新しく作成された合成サーフェスへのポインターが返されます。これで、このサーフェスをビジュアル オブジェクトのコンテンツとして設定でき、ビジュアルを合成ターゲットのルートとして設定できます。

HR(m_visual->SetContent(m_surface.Get()));
HR(m_target->SetRoot(m_visual.Get()));

ただし、この時点で合成デバイスの Commit メソッドを呼び出す必要はありません。合成サーフェスの更新はレンダリング ループで実行しますが、更新結果は Commit メソッドを呼び出すときに初めて適用されます。この時点では、合成エンジンでレンダリングを開始する準備は整っていますが、必要な作業がまだいくらか残っています。残っている作業は合成とは無関係ですが、どれも Direct2D を適切かつ効率良くレンダリングに使用することに関係しています。まず、ビットマップやブラシなどのレンダー ターゲット固有のリソースをレンダリング ループ外で作成する必要があります。DirectComposition でレンダー ターゲットを作成するので、この作業は少し厄介になる場合があります。さいわい、レンダー ターゲット固有のリソースを最終的なレンダー ターゲットと同じアドレス空間で作成することだけが要件なので、ここでは、このようなリソースを作成するために使い捨てのデバイス コンテキストを作成できます。

ComPtr<ID2D1DeviceContext> dc;
HR(device2d->CreateDeviceContext(D2D1_DEVICE_CONTEXT_OPTIONS_NONE,
                                 dc.GetAddressOf()));

これで、このレンダー ターゲットを使用してアプリケーション で 1 つのブラシを作成できます。

D2D_COLOR_F const color = ColorF(0.26f,
                                 0.56f,
                                 0.87f,
                                 0.5f);
HR(dc->CreateSolidColorBrush(color,
                             m_brush.ReleaseAndGetAddressOf()));

この結果、デバイス コンテキストは破棄されますが、ブラシは残るのでレンダリング ループで再利用します。これは直感では理解しにくいかもしれませんが、すぐに納得していただけるはずです。レンダリングの前に必要な最後の作業は、m_size と m_dpi という 2 つのメンバー変数を設定することです。Direct2D レンダー ターゲットの GetSize メソッドは、通常は論理ピクセル (別名、デバイスに依存しないピクセル) 単位でレンダー ターゲットのサイズを提供します。この論理サイズは既に実際の DPI に含まれているので、まずは論理サイズに対処しましょう。高 DPI アプリケーションに関する 2014 年 2 月号のコラムで説明したように、特定のウィンドウに対する実際の DPI 値を問い合わせるには、指定のウィンドウの大部分が存在するモニターを特定してから、そのモニターの実際の DPI 値を取得します。処理は次のとおりです。

HMONITOR const monitor = MonitorFromWindow(m_window,
                     MONITOR_DEFAULTTONEAREST);
unsigned x = 0;
unsigned y = 0;
HR(GetDpiForMonitor(monitor,
                    MDT_EFFECTIVE_DPI,
                    &x,
                    &y));

取得した値を m_dpi メンバーにキャッシュできるようになったので、DirectComposition API から提供されるデバイス コンテキストをレンダリング ループ内で簡単に更新できます。

m_dpi.x = static_cast<float>(x);
m_dpi.y = static_cast<float>(y);

これで、クライアント領域の論理サイズを論理ピクセル単位で計算するには、既に物理ピクセル単位でサイズを保持している RECT 構造体を取得し、取得済みの実際の DPI 値を考慮して計算するだけで十分になりました。

m_size.width  = (rect.right - rect.left) * 96 / m_dpi.x;
m_size.height = (rect.bottom - rect.top) * 96 / m_dpi.y;

以上で、CreateDeviceResources メソッドとそのすべての処理は完了です。各処理がどのように連携しているか把握できるように、図 3 に CreateDeviceResources メソッド全体を示します。

図 3 デバイス スタックの作成

void CreateDeviceResources()
{
  if (IsDeviceCreated()) return;
  HR(D3D11CreateDevice(nullptr,    // Adapter
                       D3D_DRIVER_TYPE_HARDWARE,
                       nullptr,    // Module
                       D3D11_CREATE_DEVICE_BGRA_SUPPORT,
                       nullptr, 0, // Highest available feature level
                       D3D11_SDK_VERSION,
                       m_device3d.GetAddressOf(),
                       nullptr,    // Actual feature level
                       nullptr));  // Device context
  ComPtr<IDXGIDevice> devicex;
  HR(m_device3d.As(&devicex));
  ComPtr<ID2D1Device> device2d;
  HR(D2D1CreateDevice(devicex.Get(),
                      nullptr, // Default properties
                      device2d.GetAddressOf()));
  HR(DCompositionCreateDevice2(
     device2d.Get(),
     __uuidof(m_device),
     reinterpret_cast<void **>(m_device.ReleaseAndGetAddressOf())));
  HR(m_device->CreateTargetForHwnd(m_window,
                                   true, // Top most
                                   m_target.ReleaseAndGetAddressOf()));
  HR(m_device->CreateVisual(m_visual.ReleaseAndGetAddressOf()));
  RECT rect = {};
  VERIFY(GetClientRect(m_window,
                       &rect));
  HR(m_device->CreateSurface(rect.right - rect.left,
                             rect.bottom - rect.top,
                             DXGI_FORMAT_B8G8R8A8_UNORM,
                             DXGI_ALPHA_MODE_PREMULTIPLIED,
                             m_surface.ReleaseAndGetAddressOf()));
  HR(m_visual->SetContent(m_surface.Get()));
  HR(m_target->SetRoot(m_visual.Get()));
  ComPtr<ID2D1DeviceContext> dc;
  HR(device2d->CreateDeviceContext(D2D1_DEVICE_CONTEXT_OPTIONS_NONE,
                                   dc.GetAddressOf()));
  D2D_COLOR_F const color = ColorF(0.26f,
                                   0.56f,
                                   0.87f,
                                   0.5f);
  HR(dc->CreateSolidColorBrush(color,
                               m_brush.ReleaseAndGetAddressOf()));
  HMONITOR const monitor = MonitorFromWindow(m_window,
                                             MONITOR_DEFAULTTONEAREST);
  unsigned x = 0;
  unsigned y = 0;
  HR(GetDpiForMonitor(monitor,
                      MDT_EFFECTIVE_DPI,
                      &x,
                      &y));
  m_dpi.x = static_cast<float>(x);
  m_dpi.y = static_cast<float>(y);
  m_size.width  = (rect.right - rect.left) * 96 / m_dpi.x;
  m_size.height = (rect.bottom - rect.top) * 96 / m_dpi.y;
}

メッセージ ハンドラーを実装するには、Window クラス テンプレートの MessageHandler をオーバーライドして、処理対象のメッセージを示す必要があります。少なくとも、描画コマンドの提供先となる WM_PAINT メッセージ、サーフェスのサイズを調整する WM_SIZE メッセージ、および実際の DPI とウィンドウのサイズを更新する WM_DPICHANGED メッセージを処理する必要があります。図 4 に MessageHandler を示します。ご想像どおり、MessageHandler は適切なハンドラーにメッセージを送信しているだけです。

図 4 メッセージのディスパッチ

LRESULT MessageHandler(UINT message,
                       WPARAM const wparam,
                       LPARAM const lparam)
{
  if (WM_PAINT == message)
  {
    PaintHandler();
    return 0;
  }
  if (WM_SIZE == message)
  {
    SizeHandler(wparam, lparam);
    return 0;
  }
  if (WM_DPICHANGED == message)
  {
    DpiHandler(wparam, lparam);
    return 0;
  }
  return __super::MessageHandler(message,
                                 wparam,
                                 lparam);
}

WM_PAINT ハンドラー内では、描画シーケンスに入る前に必要に応じてデバイス リソースを作成します。CreateDeviceResources は、デバイスが既に存在する場合は何も処理を行わないことに留意してください。

void PaintHandler()
{
  try
  {
    CreateDeviceResources();
    // Drawing commands ...
}

このようにすると、ReleaseDeviceResources メソッドを利用して Direct3D デバイス ポインターを解放するだけで、デバイスを利用できなくなった状況に対処でき、次のメッセージで WM_PAINT ハンドラーによってデバイス リソース全体が再作成されます。デバイスの障害に確実に対処できるように、処理全体を try ブロックに含めています。合成サーフェスへの描画を始めるには、次のように合成サーフェスの BeginDraw メソッドを呼び出す必要があります。

ComPtr<ID2D1DeviceContext> dc;
POINT offset = {};
HR(m_surface->BeginDraw(nullptr, // Entire surface
                        __uuidof(dc),
                        reinterpret_cast<void **>(dc.GetAddressOf()),
                        &offset));

BeginDraw メソッドは、実際の描画コマンドをバッチにまとめるために使用する予定の、デバイス コンテキスト (Direct2D レンダー ターゲット) を返します。DirectComposition API は、もともと合成デバイスの作成時に提供した Direct2D デバイスを使用して、ここでデバイス コンテキストを返します。必要に応じて、物理ピクセル単位の RECT 構造体を指定してサーフェスをクリップすることも、nullptr を指定して描画サーフェスへの無制限のアクセスを許可することもできます。BeginDraw メソッドは同じく物理ピクセル単位のオフセットも返すことで、目的の描画サーフェスの原点を示します。原点がサーフェスの左上隅にあるとは限らないので、描画コマンドに適切なオフセットが適用されるよう慎重にコマンドを調整または変換する必要があります。

合成サーフェスでは EndDraw メソッドも使用します。これら 2 つのメソッドは、Direct2D の BeginDraw メソッドと EndDraw メソッドに代わるものです。デバイス コンテキストでは、対応するメソッドを呼び出さないでください。なぜなら、この呼び出しは DirectComposition API で実行されるためです。当然ながら、DirectComposition API では、ターゲットとして選択している合成サーフェスがデバイス コンテキストに保持されているかどうかも確認されます。さらに、デバイス コンテキストを保持し続けず、描画が完了したらすぐに適切に解放することが重要です。加えて、描画済みの可能性もある以前のフレームの内容がサーフェスに保持されているとは限らないので、描画の完了前に慎重にターゲットをクリアするか、すべてのピクセルを再描画する必要があります。

結果のデバイス コンテキストは使用できる状態になっていますが、ウィンドウの実際の DPI 倍率が適用されていません。CreateDeviceResources メソッド内で既に計算した DPI 値を使用すると、この時点でデバイス コンテキストを更新できます。

dc->SetDpi(m_dpi.x,
           m_dpi.y);

また、平行移動変換行列を使用して、DirectComposition API に必要なオフセットを考慮しながら描画コマンドを調整します。Direct2D では論理ピクセルが想定されているので、オフセットを論理ピクセルに変換するよう注意する必要があります。

dc->SetTransform(Matrix3x2F::Translation(offset.x * 96 / m_dpi.x,
                                         offset.y * 96 / m_dpi.y));

これでターゲットをクリアして、アプリケーション固有のコンテンツを描画できるようになりました。ここでは、先ほど CreateDeviceResources メソッドで作成したデバイス依存のブラシを使用して、シンプルな四角形を描画します。

dc->Clear();
D2D_RECT_F const rect = RectF(100.0f,
                              100.0f,
                              m_size.width - 100.0f,
                              m_size.height - 100.0f);
dc->DrawRectangle(rect,
                  m_brush.Get(),
                  50.0f);

ここでは GetSize メソッドから報告されるサイズではなく、キャッシュされた m_size メンバーを使用しています。なぜなら、GetSize メソッドからは、クライアント領域のサイズではなく基盤となるサーフェスのサイズが報告されるためです。

描画シーケンスを完了するには、いくつもの手順が必要です。まず、サーフェスで EndDraw メソッドを呼び出す必要があります。EndDraw メソッドは Direct2D に対して、バッチ描画コマンドを完了して合成サーフェスに書き込むよう指示します。これでサーフェスを合成する準備が完了しましたが、実際の合成は、合成デバイスで Commit メソッドを呼び出すまで実行されません。Commit メソッドを呼び出すと、更新されたサーフェスなどのビジュアル ツリーに対する変更点がバッチにまとめられ、単一のトランザクション ユニット内の Windows 合成エンジンで利用できるようになります。これでレンダリング プロセスは完了です。唯一残っている不明点は、Direct3D デバイスを利用できなくなったかどうかです。Commit メソッドから障害が報告されたら、catch ブロックでデバイスを解放します。何も問題がない場合は、ValidateRect 関数を使用してウィンドウのクライアント領域全体を検証することで、ウィンドウ描画の成功を Windows に通知できます。問題があった場合は、デバイスを解放する必要があります。この処理の例は次のとおりです。

// Drawing commands ...
  HR(m_surface->EndDraw());
  HR(m_device->Commit());
  VERIFY(ValidateRect(m_window, nullptr));
}
catch (ComException const & e)
{
  ReleaseDeviceResources();
}

クライアント領域の検証でメッセージに応答しなかった場合は Windows から WM_PAINT メッセージが送信され続けるため、明示的に再描画する必要はありません。WM_SIZE ハンドラーの役割は、合成サーフェスのサイズを調整することと、レンダー ターゲットのキャッシュされたサイズを更新することです。デバイスが作成されていない場合、またはウィンドウが最小化されている場合は、メッセージに対処しません。

void SizeHandler(WPARAM const wparam,
                 LPARAM const lparam)
{
  try
  {
    if (!IsDeviceCreated()) return;
    if (SIZE_MINIMIZED == wparam) return;
    // ...
}

ウィンドウでは、デバイス スタックを 1 回も作成していない段階で WM_SIZE メッセージを受信することがよくあります。その場合は、メッセージを無視します。また、ウィンドウを最小化した結果 WM_SIZE メッセージを受信した場合も、メッセージを無視します。この場合は、不要なサーフェスのサイズ調整を避けるためです。WM_PAINT ハンドラーと同様に、WM_SIZE ハンドラーの操作は try ブロックに含まれています。サイズ変更 (または、この場合は再作成) の際は、デバイスを利用できないためにサーフェスに問題が発生することがあり、問題が発生した場合は最終的にデバイス スタックを再作成することになります。ただし、まずはクライアント領域の新しいサイズを抽出できます。

unsigned const width  = LOWORD(lparam);
unsigned const height = HIWORD(lparam);

また、論理ピクセル単位のキャッシュされたサイズを更新できます。

m_size.width  = width  * 96 / m_dpi.x;
m_size.height = height * 96 / m_dpi.y;

合成サーフェスのサイズは変更できません。今回は、非仮想サーフェスとも呼ばれるものを使用しています。Windows 合成エンジンにはサイズ変更が可能な仮想サーフェスも用意されていますが、詳細については今後のコラムで説明します。ここでは、現在のサーフェスを解放して再作成できます。ビジュアル ツリーへの変更は、変更がコミットされるまで反映されないので、サーフェスの破棄中や再作成中にちらつきが表示されることはありません。この処理の例は次のとおりです。

HR(m_device->CreateSurface(width,
                           height,
                           DXGI_FORMAT_B8G8R8A8_UNORM,
                           DXGI_ALPHA_MODE_PREMULTIPLIED,
                           m_surface.ReleaseAndGetAddressOf()));
HR(m_visual->SetContent(m_surface.Get()));

続いて、次の WM_PAINT メッセージでデバイス リソースを再作成できるようにデバイス リソースを解放すれば、障害に対応できます。

// ...
}
catch (ComException const & e)
{
  ReleaseDeviceResources();
}

WM_SIZE ハンドラーについては以上です。最後の必要な手順は、WM_DPICHANGED ハンドラーを実装して、実際の DPI とウィンドウのサイズを更新することです。メッセージの WPARAM には新しい DPI 値が格納され、LPARAM には新しいサイズが格納されています。ウィンドウの m_dpi メンバー変数を更新してから、SetWindowPos メソッドを呼び出してウィンドウのサイズを更新します。その後、ウィンドウで受信した新しい WM_SIZE メッセージを使用して、WM_SIZE ハンドラーで m_size メンバーを調整してサーフェスを再作成します。図 5 に、このような WM_DPICHANGED メッセージを処理する方法の例を示します。

図 5 DPI 更新の処理

void DpiHandler(WPARAM const wparam,
                LPARAM const lparam)
{
  m_dpi.x = LOWORD(wparam);
  m_dpi.y = HIWORD(wparam);
  RECT const & rect = *reinterpret_cast<RECT const *>(lparam);
  VERIFY(SetWindowPos(m_window,
                      0, // No relative window
                      rect.left,
                      rect.top,
                      rect.right - rect.left,
                      rect.bottom - rect.top,
                      SWP_NOACTIVATE | SWP_NOZORDER));
}

めでたく、Direct2D と DirectComposition の密接な統合によって DirectX ファミリのメンバーの相互連携が深まり、相互運用性とパフォーマンスが向上しました。DirectX を使用したリッチなネイティブ アプリケーションを構築する可能性に、皆さんも私と同じように興味を持っていただければさいわいです。

Kenny Kerr は、カナダを拠点とするコンピューター プログラマであり、Pluralsight の執筆者です。また、Microsoft MVP でもあります。彼のブログは kennykerr.ca (英語) で、Twitter は twitter.com/kennykerr (英語) でフォローできます。

この記事のレビューに協力してくれたマイクロソフト技術スタッフの Leonardo Blanco および James Clarke に心より感謝いたします。