次の方法で共有


Windows with C++

DirectComposition: すべてを牛耳る保持モード API

Kenny Kerr

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

Kenny Kerrグラフィックス API を分類すると、ほとんどは大きく異なる 2 つのモードのいずれかに振り分けられます。1 つはイミディエイト モード API で、よく知られているのは Direct2D や Direct3D などです。もう 1 つは保持モード API で、Windows Presentation Foundation (WPF)、XAML、宣言型 API などがその例です。最近のブラウザーは、保持モード API を提供するのがスケーラブル ベクター グラフィックス、イミディエイト モード API を提供するのが Canvas 要素と、2 つのグラフィックス モードを明確に区別します。

保持モードでは、グラフィックス API がオブジェクトのグラフやツリーなどのシーンの一部の表現を保持し、これを長時間操作できることを想定します。この想定は便利で、対話型アプリケーションの開発が簡単になります。これに対して、イミディエイト モード API には組み込みのシーン グラフはありません。代わりに、アプリケーションを利用し、描画コマンドのシーケンスを使ってシーンを構築します。これはパフォーマンス面で非常に大きなメリットがあります。Direct2D などのイミディエイト モード API は、通常、複数のジオメトリから頂点データをバッファーに保存し、大量の描画コマンドを結合します。テキストのレンダリング パイプラインは、最初にグリフをテクスチャに書き込み、低解像度処理した後に Clear-type フィルタリングを適用して、テキストをレンダー ターゲットに変換するため、描画コマンドのシーケンスを結合する特性は特にメリットがあります。そのため、他の多くのグラフィックス API や、徐々に増えつつある多くのサードパーティ製アプリケーションのテキスト レンダリングで Direct2D や DirectWrite が使用される理由の 1 つになっています。

これまでは、イミディエイト モードと保持モードのどちらを選択するかを考える場合、パフォーマンスと生産性とのトレードオフになっていました。最高のパフォーマンスを求める開発者は Direct2D イミディエイト モード API を選択し、生産性や利便性を求める開発者は WPF 保持モード API を選択する傾向がありました。DirectComposition はこのような考え方を変え、開発者がこれらの 2 つのモードを非常に自然に組み合わせることができるようにします。DirectComposition はグラフィックスに保持モードを提供しますが、メモリやパフォーマンスのオーバーヘッドがかからないため、イミディエイト モード API と保持モード API の線引きがあいまいになります。DirectComposition は他のグラフィックス API に対抗しようとするのではなく、ビットマップ構成に注目することでこれを実現しています。DirectComposition は、他のテクノロジによってレンダリングされたビットマップを簡単に操作して合成できるように、ビジュアル ツリーと構成インフラストラクチャのみを提供します。また、WPF とは異なり、OS グラフィックス インフラストラクチャに不可欠な要素で、これまで WPF アプリケーションで発生していたパフォーマンスと空域の問題をすべて回避できます。

DirectComposition に関する以前の 2 つのコラム (msdn.microsoft.com/magazine/dn745861 および msdn.microsoft.com/magazine/dn786854) をお読みになっていれば、合成エンジンで実行可能な処理について既にご理解いただいていると思います。今回は、保持モード API を使い慣れている開発者にとっても非常に魅力的に思えるように、、Direct2D で描画されたビジュアルを DirectComposition を使用して操作する方法を紹介することで、多くの機能を明らかにしていきます。ここでは例として、作成して移動できる円を "オブジェクト" として表示するシンプルなウィンドウの作成方法を示します。このウィンドウでは、ヒット テストや Z オーダーの変更を完全にサポートします。このウィンドウの外観については、図 1 をご覧ください。

円のドラッグ
図 1 円のドラッグ

図 1 の円は Direct2D で描画していますが、アプリケーションは合成サーフェスに円を一度しか描画していません。その後、この合成サーフェスが、ウィンドウにバインドされているビジュアル ツリー内の複数の合成ビジュアル間で共有されます。各ビジュアルは、自身のコンテンツ (合成サーフェス) の位置を決めるオフセットをウィンドウとの相対で定義します。各ビジュアルのコンテンツは、最終的に合成エンジンによってレンダリングされます。ユーザーは、新しい円を作成し、マウス、ペン、または指で動かすことができます。円は選択されるたびに Z オーダーの最上位に移動するため、ウィンドウの他のすべての円の上に表示されます。このような単純な効果を表現するのに保持モード API はまったく必要ありませんが、DirectComposition API がどのように Direct2D と連携して高度な視覚効果を実現するかを示す良い例になります。ここでは、WM_PAINT ハンドラーによってウィンドウのピクセルを最新状態に保つ必要のない、対話型アプリケーションを構築することが目標です。

まずは、前回のコラムで紹介した Window クラス テンプレートから派生して、新しい SampleWindow クラスを作成します。Window クラス テンプレートによって、C++ におけるメッセージのディスパッチが簡略化されます。

struct SampleWindow : Window<SampleWindow>
{
};

最新の Windows アプリケーションと同様に、DPI の動的スケール変換を処理する必要があります。そこで、2 つの浮動小数点メンバーを追加して、X 軸と Y 軸の DPI スケール変換要素を追跡できるようにします。

float m_dpiX = 0.0f;
float m_dpiY = 0.0f;

前回のコラムで説明したように、これらのメンバーは必要に応じて、または WM_CREATE メッセージ ハンドラー内で初期化することができます。いずれにしても、MonitorFromWindow 関数を呼び出して、新しいウィンドウが重なり合う広い領域を備えたモニターを特定する必要があります。次に、GetDpiForMonitor 関数を単純に呼び出して、有効な DPI 値を取得します。この方法については、前回のコラムやコースで何度も紹介しましたので、ここでは繰り返し説明しません。

Direct2D の楕円ジオメトリ オブジェクトを使用して描画する円を指定し、同じジオメトリ オブジェクトを後のヒット テストで使用できるようにします。ジオメトリ オブジェクトより D2D1_ELLIPSE 構造体を描画する方が効率的ですが、ジオメトリ オブジェクトではヒット テストを実行でき、描画結果が保持されます。ここでは Direct2D ファクトリと楕円ジオメトリを両方追跡します。

ComPtr<ID2D1Factory2> m_factory;
ComPtr<ID2D1EllipseGeometry> m_geometry;

前回のコラムでは、Direct2D ファクトリではなく D2D1CreateDevice 関数を使用して、Direct2D デバイス オブジェクトを直接作成する方法について説明しました。この方法を使用すれば確実に作業を続けることができますが、この方法には難点があります。Direct2D ファクトリ リソースは、デバイスに依存しないため、デバイスが利用できなくなっても再作成する必要はありませんが、同じ Direct2D ファクトリで作成された Direct2D デバイスでしか使用できません。ここでは楕円ジオメトリを最初に作成するので、作成時に Direct2D ファクトリ オブジェクトが必要です。場合によっては、D2D1CreateDevice 関数を使用して Direct2D デバイスが作成されるまで待機することになります。その後、GetFactory メソッドを使用して基盤となるファクトリを取得します。続いて、このファクトリ オブジェクトを使用してジオメトリを作成しますが、この方法は不自然に感じられます。代わりに、Direct2D ファクトリを作成し、これを使用して必要に応じて楕円ジオメトリとデバイス オブジェクトの両方を作成します。図 2 は、Direct2D ファクトリとジオメトリ オブジェクトの作成方法を示しています。

図 2 Direct2D ファクトリとジオメトリ オブジェクトの作成

void CreateFactoryAndGeometry()
{
  D2D1_FACTORY_OPTIONS options = {};
  #ifdef _DEBUG
  options.debugLevel = D2D1_DEBUG_LEVEL_INFORMATION;
  #endif
  HR(D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED,
                       options,
                       m_factory.GetAddressOf()));
  D2D1_ELLIPSE const ellipse = Ellipse(Point2F(50.0f, 50.0f),
                                       49.0f,
                                       49.0f);
  HR(m_factory->CreateEllipseGeometry(ellipse,
                                      m_geometry.GetAddressOf()));
}

CreateFactoryAndGeometry メソッドを SampleWindow コンストラクターで呼び出して、このようなデバイスに依存しないリソースを準備できます。図からわかるように、楕円は X = 50、Y = 50 を中心に定義し、半径 X と Y を 49 ピクセルにしています。そのため、この楕円は円形になります。合成サーフェスは 100x100 で作成していることになります。半径に 49 ピクセルを指定したのは、Direct2D で描画される既定のストロークが境界を含むためです。49 ピクセルを指定しなければクリップされます。

ここからは、デバイス固有のリソースについて説明します。今回の例では、Direct3D 補助デバイス、変更をビジュアル ツリーに適用する合成デバイス、ビジュアル ツリーを保持する合成ターゲット、すべての円形ビジュアルの基となるルート ビジュアル、および共有する合成サーフェスが必要です。

ComPtr<ID3D11Device> m_device3D;
ComPtr<IDCompositionDesktopDevice> m_device;
ComPtr<IDCompositionTarget> m_target;
ComPtr<IDCompositionVisual2> m_rootVisual;
ComPtr<IDCompositionSurface> m_surface;

これらのさまざまなオブジェクトについては以前の DirectX 記事、特に、DirectComposition に関する前 2 回のコラムで紹介しました。デバイスの作成方法とデバイスを利用できない場合の対処方法についても説明済みですので、ここでは繰り返しません。今回は、上記で作成した Direct2D ファクトリを使用するため更新する必要がある、CreateDevice2D メソッドを呼び出すだけです。

ComPtr<ID2D1Device> CreateDevice2D()
{
  ComPtr<IDXGIDevice3> deviceX;
  HR(m_device3D.As(&deviceX));
  ComPtr<ID2D1Device> device2D;
  HR(m_factory->CreateDevice(deviceX.Get(), 
    device2D.GetAddressOf()));
  return device2D;
}

ここで、共有するサーフェスを作成します。ComPtr クラス テンプレートの ReleaseAndGetAddressOf メソッドを使用する場合は、デバイスを利用できなくなった後や DPI スケールの変更後、サーフェスを安全に再作成できるように注意する必要があります。また、サイズを DirectComposition API 用の物理ピクセルに変換する際、アプリケーションで使用する論理座標系を保持するように注意が必要です。

HR(m_device->CreateSurface(
  static_cast<unsigned>(LogicalToPhysical(100, m_dpiX)),
  static_cast<unsigned>(LogicalToPhysical(100, m_dpiY)),
  DXGI_FORMAT_B8G8R8A8_UNORM,
  DXGI_ALPHA_MODE_PREMULTIPLIED,
  m_surface.ReleaseAndGetAddressOf()));

次に、合成サーフェスの BeginDraw メソッドを呼び出して、描画コマンドをバッファーに保存するために使用する Direct2D デバイス コンテキストを取得します。

HR(m_surface->BeginDraw(
  nullptr,
  __uuidof(dc),
  reinterpret_cast<void **>(dc.GetAddressOf()),
  &offset));

続いて、描画コマンドのサイズ調整方法を Direct2D に指示する必要があります。

dc->SetDpi(m_dpiX,
           m_dpiY);

また、DirectComposition によって提供されるオフセットに出力を変換することも必要です。

dc->SetTransform(Matrix3x2F::Translation(PhysicalToLogical(offset.x, m_dpiX),
                                         PhysicalToLogical(offset.y, m_dpiY)));

PhysicalToLogical は、DPI のスケール変換にいつも好んで使用するヘルパー関数で、DPI のスケール変換を異なるレベルでサポートする (またはまったくサポートしない) API を組み合わせる際に使用します。PhysicalToLogical 関数と対応する LogicalToPhysical 関数については、図 3 を参照してください。

図 3 論理ピクセルと物理ピクセルの変換

template <typename T>
static float PhysicalToLogical(T const pixel,
                               float const dpi)
{
  return pixel * 96.0f / dpi;
}
template <typename T>
static float LogicalToPhysical(T const pixel,
                               float const dpi)
{
  return pixel * dpi / 96.0f;
}

ここで、このためだけに作成した単色ブラシを使用して、青色の円を描画します。

ComPtr<ID2D1SolidColorBrush> brush;
D2D1_COLOR_F const color = ColorF(0.0f, 0.5f, 1.0f, 0.8f);
HR(dc->CreateSolidColorBrush(color,
                             brush.GetAddressOf()));

続いて、レンダー ターゲットをクリアしてから、楕円ジオメトリを塗りつぶし、別のブラシで輪郭をストロークまたは描画します。

dc->Clear();
dc->FillGeometry(m_geometry.Get(),
                 brush.Get());
brush->SetColor(ColorF(1.0f, 1.0f, 1.0f));
dc->DrawGeometry(m_geometry.Get(),
                 brush.Get());

最後に、EndDraw メソッドを呼び出して、サーフェスを合成する準備が整ったことを示します。

HR(m_surface->EndDraw());

いよいよ円を作成します。前回のコラムでは単一のルート ビジュアルのみ作成しましたが、今回のアプリケーションでは必要に応じてビジュアルを作成する必要があるため、ここでは作成処理を便利なヘルパー メソッドにまとめます。

ComPtr<IDCompositionVisual2> CreateVisual()
{
  ComPtr<IDCompositionVisual2> visual;
  HR(m_device->CreateVisual(visual.GetAddressOf()));
  return visual;
}

DirectComposition API の興味深い側面の 1 つは、この API が事実上、合成エンジンへの書き込み専用のインターフェイスである点です。DirectComposition API はウィンドウのビジュアル ツリーを保持しますが、ビジュアル ツリーへの問い合わせで使用できる getter は用意されていません。ビジュアルの位置や Z オーダーなどの情報は、アプリケーションが直接保持しなければなりません。これにより、不要なメモリのオーバーヘッドを防ぎ、アプリケーションの状態と合成エンジンのトランザクション状態の間に競合が発生する可能性がなくなります。そのため、Circle 構造体を作成して、円のそれぞれの位置を追跡します。

struct Circle
{
  ComPtr<IDCompositionVisual2> Visual;
  float LogicalX = 0.0f;
  float LogicalY = 0.0f;
};

合成ビジュアルは事実上、円の setter を表し、LogicalX と LogicalY フィールドは getter になります。IDCompositionVisual2 インターフェイスを使用してビジュアルの位置を設定して保持し、後から他のフィールドにその位置を取得することができます。この位置は、ヒット テストや、デバイスを利用できなくなった後に円を復元する場合に必要です。これらの同期ミスを回避するため、ここでは論理位置に基づいてビジュアル オブジェクトを更新するヘルパー メソッドを作成します。DirectComposition API ではコンテンツの位置やサイズ調整の方法についての考え方は示されていません。そのため、必要な DPI 計算を自身で作成する必要があります。

void UpdateVisualOffset(float const dpiX,
                        float const dpiY)
{
  HR(Visual->SetOffsetX(LogicalToPhysical(LogicalX, dpiX)));
  HR(Visual->SetOffsetY(LogicalToPhysical(LogicalY, dpiY)));
}

円の論理オフセットを実際に設定するため、別のヘルパー メソッドを追加します。Circle 構造体とビジュアル オブジェクトが同期されるように、このオフセットは UpdateVisualOffset を利用します。

void SetLogicalOffset(float const logicalX,
                      float const logicalY,
                      float const dpiX,
                      float const dpiY)
{
  LogicalX = logicalX;
  LogicalY = logicalY;
  UpdateVisualOffset(dpiX,
                       dpiY);
}

最後に、円をアプリケーションに追加するため、IDCompositionVisual2 参照の所有権を受け取り、構造体を初期化する簡単なコンストラクターが必要です。

Circle(ComPtr<IDCompositionVisual2> && visual,
       float const logicalX,
       float const logicalY,
       float const dpiX,
       float const dpiY) :
  Visual(move(visual))
{
  SetLogicalOffset(logicalX,
                   logicalY,
                   dpiX,
                   dpiY);
}

これで、標準リスト コンテナーを使用して、アプリケーションのすべての円を追跡できるようになります。

list<Circle> m_circles;

このとき、選択した円を追跡するメンバーも追加します。

Circle * m_selected = nullptr;
float m_mouseX = 0.0f;
float m_mouseY = 0.0f;

自然な動きを実現するには、マウス オフセットも役に立ちます。最終的に円を作成して動かす実際のマウス操作を確認する前に、ハウスキーピング処理に対応します。デバイスが利用できない状況が発生した場合は、以前作成した円に基づいて、CreateDeviceResources メソッドでビジュアル オブジェクトを再作成します。円が表示されていない場合はビジュアル オブジェクトを再作成する必要はありません。ルート ビジュアルや共有するサーフェスを作成または再作成した直後に、このリストを反復処理して、新しいビジュアル オブジェクトを作成し、既存の状態に合うように再配置します。図 4 は、ここまで説明した処理を使用して、これらすべてを連携する方法を示しています。

図 4 デバイス スタックとビジュアル ツリーの作成

void CreateDeviceResources()
{
  ASSERT(!IsDeviceCreated());
  CreateDevice3D();
  ComPtr<ID2D1Device> const device2D = CreateDevice2D();
  HR(DCompositionCreateDevice2(
      device2D.Get(),
      __uuidof(m_device),
      reinterpret_cast<void **>(m_device.ReleaseAndGetAddressOf())));
  HR(m_device->CreateTargetForHwnd(m_window,
                                   true,
                                   m_target.ReleaseAndGetAddressOf()));
  m_rootVisual = CreateVisual();
  HR(m_target->SetRoot(m_rootVisual.Get()));
  CreateDeviceScaleResources();
  for (Circle & circle : m_circles)
  {
    circle.Visual = CreateVisual();
    HR(circle.Visual->SetContent(m_surface.Get()));
    HR(m_rootVisual->AddVisual(circle.Visual.Get(), false, nullptr));
    circle.UpdateVisualOffset(m_dpiX, m_dpiY);
  }
  HR(m_device->Commit());
}

ハウスキーピング処理には、DPI のスケール変換に関連する部分もあります。Direct2D でレンダリングされた円のピクセルを含む合成サーフェスは、再作成してサイズを調整する必要があるため、ビジュアル自体の位置も再設定する必要があります。したがって、ビジュアルのオフセットは相互に比例し、また表示されるウィンドウとも比例します。まずは、WM_DPICHANGED メッセージ ハンドラーで、CreateDeviceScaleResources メソッドと連携して合成サーフェスを再作成します。続いて、円ごとのコンテンツと位置を更新します。

if (!IsDeviceCreated()) return;
CreateDeviceScaleResources();
for (Circle & circle : m_circles)
{
  HR(circle.Visual->SetContent(m_surface.Get()));
  circle.UpdateVisualOffset(m_dpiX, m_dpiY);
}
HR(m_device->Commit());

次に、ポインター操作を構成します。ユーザーが Ctrl キーを押した状態でマウスの左ボタンを押すと、新しい円を作成できるようにします。WM_LBUTTONDOWN メッセージ ハンドラーは次のようになります。

if (wparam & MK_CONTROL)
{
  // Create new circle
}
else
{
  // Look for existing circle
}
HR(m_device->Commit());

新しい円を作成する必要があるとして、まず新しいビジュアルを作成し、共有するコンテンツを設定してからルート ビジュアルの子として追加します。

ComPtr<IDCompositionVisual2> visual = CreateVisual();
HR(visual->SetContent(m_surface.Get()));
HR(m_rootVisual->AddVisual(visual.Get(), false, nullptr));

新しいビジュアルは、既存のビジュアルの前面に追加されます。これは、AddVisual メソッドの 2 つ目のパラメーターが作用します。このパラメーターを true に設定すると、新しいビジュアルは既存のビジュアルの背面に配置されます。次に、後でヒット テスト、デバイスが利用できない状況、および DPI スケール変換に対応できるように、Circle 構造体をリストに追加する必要があります。

m_circles.emplace_front(move(visual),
       PhysicalToLogical(LOWORD(lparam), m_dpiX) - 50.0f,
       PhysicalToLogical(HIWORD(lparam), m_dpiY) - 50.0f,
       m_dpiX,
       m_dpiY);

ビジュアル ツリーと同じ順番で自然にヒット テストを実行できるように、新しく作成した Circle をリストの先頭に配置します。また、マウス位置を中心として、ビジュアルの初期位置を設定します。最後に、ユーザーがマウス操作をすぐに終えないものとして、マウスをキャプチャし、円の移動を追跡します。

SetCapture(m_window);
m_selected = &m_circles.front();
m_mouseX = 50.0f;
m_mouseY = 50.0f;

マウス オフセットを利用すると、マウス ポインターで最初に円上のどの場所を選択しても、スムーズにドラッグすることができえうようになります。既存の円を見つける処理はやや複雑です。ここで再び、DPI 対応を手動で適用する必要があります。さいわい、Direct2D ではこの処理を非常に簡単に行うことができます。まず、通常の Z オーダーで各円を反復処理する必要があります。さいわい、既に新しい円をリストの先頭に配置しているため、先頭から末尾に向かって簡単に反復処理を実行できます。

for (auto circle = begin(m_circles); circle != end(m_circles); ++circle)
{
}

この場合は、実際に反復子を使用した方が便利であるため、範囲ベースの for ステートメントは使用しません。この時点での円の位置を確認すると、各円は、ウィンドウ左上隅を基準として論理位置を保持しています。また、マウス メッセージの LPARAM には、ウィンドウ左上隅を基準としたポインターの物理位置も含まれます。ただし、ヒット テストを実行する必要がある形状は単純な長方形ではないため、円を一般的な座標系に変換するだけでは十分ではありません。形状はジオメトリ オブジェクトによって定義され、Direct2D からヒット テストを実装するための FillContainsPoint メソッドが提供されます。ここで問題なのは、ジオメトリ オブジェクトによって円の形状のみが定義され、位置は定義されないことです。ヒット テストが効率的に機能するには、まずマウス位置をジオメトリ オブジェクトに関連付けるように変換する必要があります。この処理は非常に簡単です。

D2D1_POINT_2F const point =
  Point2F(LOWORD(lparam) - LogicalToPhysical(circle->LogicalX, m_dpiX),
          HIWORD(lparam) - LogicalToPhysical(circle->LogicalY, m_dpiY));

ただし、FillContainsPoint メソッドを呼び出す準備は完全には終わっていません。ここで問題がもう 1 つあります。ジオメトリ オブジェクトは、レンダー ターゲットに関する情報を何も認識しません。ジオメトリ オブジェクトを使用して円を描画したとき、ターゲットの DPI 値に合うようにジオメトリのサイズ調整を行ったのはレンダー ターゲットでした。そこで、ヒット テストを実行する前に、円のサイズを反映して画面上でユーザーに実際に表示されるものと一致するように、ジオメトリのサイズ調整を行う方法が必要です。ここでも Direct2D が役に立ちます。FillContainsPoint はオプションの 3x2 行列を受け取り、ジオメトリを変換してから、指定の点が形状内部に含まれているかどうかをテストします。ここでは、ウィンドウの DPI 値を指定して、サイズ変換を簡単に定義します。

D2D1_MATRIX_3X2_F const transform = Matrix3x2F::Scale(m_dpiX / 96.0f,
                                                      m_dpiY / 96.0f);

FillContainsPoint メソッドによって、点が円内部に含まれるかどうか確認します。

BOOL contains = false;
HR(m_geometry->FillContainsPoint(point,
                                 transform,
                                 &contains));
if (contains)
{
  // Reorder and select circle
  break;
}

点が円内部に含まれる場合、選択した円のビジュアルが Z オーダーの最上位にくるように、合成ビジュアルを並べ替えます。そのためには、子のビジュアルを削除し、既存のすべてのビジュアルの前面に追加します。

HR(m_rootVisual->RemoveVisual(circle->Visual.Get()));
HR(m_rootVisual->AddVisual(circle->Visual.Get(), false, nullptr));

また、円をリストの先頭に移動して、リストを最新状態に保ちます。

m_circles.splice(begin(m_circles), m_circles, circle);

ここでは、ユーザーが円をドラッグするものとします。

SetCapture(m_window);
m_selected = &*circle;
m_mouseX = PhysicalToLogical(point.x, m_dpiX);
m_mouseY = PhysicalToLogical(point.y, m_dpiY);

ここで、選択した円を基準に、マウス位置のオフセットを計算します。その結果、円はドラッグされたときにシームレスに動き、マウス ポインターの中心に "スナップ" されるようには見えません。WM_MOUSEMOVE メッセージに応答することで、円を選択している間は、選択した円でこの動きを継続することができます。

if (!m_selected) return;
m_selected->SetLogicalOffset(
  PhysicalToLogical(GET_X_LPARAM(lparam), m_dpiX) - m_mouseX,
  PhysicalToLogical(GET_Y_LPARAM(lparam), m_dpiY) - m_mouseY,
  m_dpiX,
  m_dpiY);
HR(m_device->Commit());

Circle 構造体の SetLogicalOffset メソッドによって、円で保持される論理位置と、合成ビジュアルの物理位置が更新されます。また、通常の LOWORD マクロと HIWORD マクロではなく、GET_X_LPARAM マクロと GET_Y_LPARAM マクロを使用して、LPARAM を処理するようにします。WM_MOUSEMOVE メッセージによって報告される位置はウィンドウの左上隅が基準となっていますが、マウスが取得され、円がウィンドウの上部または左側にドラッグされる場合は、負の座標が含まれます。通常、ビジュアル ツリーに対する変更は確定させる必要があります。マウスから指を放し、m_selected ポインターをリセットすることで、WM_LBUTTONUP メッセージ ハンドラー内での動きが止まります。

ReleaseCapture();
m_selected = nullptr;

最後に、最も優れた機能を紹介して締めくくります。その機能が保持モード グラフィックスを示すことは、図 5 の WM_PAINT メッセージ ハンドラーを見ていただくとよくわかります。

図 5 保持モードの WM_PAINT メッセージ ハンドラー

void PaintHandler()
{
  try
  {
    if (IsDeviceCreated())
    {
      HR(m_device3D->GetDeviceRemovedReason());
    }
    else
    {
      CreateDeviceResources();
    }
    VERIFY(ValidateRect(m_window, nullptr));
  }
  catch (ComException const & e)
  {
    ReleaseDeviceResources();
  }
}

CreateDeviceResources メソッドは、デバイス スタックを最初に作成します。問題が起こらない限り、ウィンドウの検証を除いて、これ以上 WM_PAINT メッセージ ハンドラーで実行する処理はありません。デバイスを利用できない状況が検出されると、さまざまな catch ブロックでデバイスを解放し、必要に応じてウィンドウを無効にします。次に WM_PAINT メッセージを受け取ると、デバイス リソースを再作成します。次回のコラムでは、ユーザー入力によって直接実現されない視覚効果を生み出す方法について説明します。合成エンジンはアプリケーションが関与することなくレンダリングを実行するため、アプリケーションが認識しいないうちにデバイスが利用できない状況が発生する可能性があります。このような場合、GetDeviceRemovedReason メソッドが呼び出されます。合成エンジンでデバイスが利用できない状況が検出された場合、Direct3D デバイスの GetDeviceRemovedReason メソッドを呼び出してその状況を確認できるように、アプリケーション ウィンドウに単純に WM_PAINT メッセージが送信されます。付属のサンプル プロジェクトを使用して、DirectComposition をぜひお試しください。


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

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