ユーザーによる入力: 拡張の例

ユーザーによる入力について学んだことをすべて組み合わせて、簡単な描画プログラムを作成しましょう。 プログラムのスクリーンショットを次に示します。

描画プログラムのスクリーン ショット

ユーザーは、複数の異なる色で省略記号を描画し、省略記号を選択、移動、または削除できます。 UI をシンプルに保つために、プログラムではユーザーが省略記号の色を選択することはできません。 代わりに、プログラムは色の定義済みリストを自動的に循環します。 プログラムでは、省略記号以外の図形はサポートされていません。 明らかに、このプログラムはグラフィックス ソフトウェアの賞を受賞することはないでしょう。 それでも、学ぶ上で役立つ例です。 完全なソース コードは、 Simple Drawing Sample からダウンロードできます。 このセクションでは、いくつかのハイライトについて説明します。

省略記号は、省略記号データ (D2D1_ELLIPSE) と色 (D2D1_COLOR_F) を含む構造体によってプログラムで表されます。 この構造体では、省略記号を描画するメソッドとヒット テストを実行するメソッドといった 2 つのメソッドも定義されています。

struct MyEllipse
{
    D2D1_ELLIPSE    ellipse;
    D2D1_COLOR_F    color;

    void Draw(ID2D1RenderTarget *pRT, ID2D1SolidColorBrush *pBrush)
    {
        pBrush->SetColor(color);
        pRT->FillEllipse(ellipse, pBrush);
        pBrush->SetColor(D2D1::ColorF(D2D1::ColorF::Black));
        pRT->DrawEllipse(ellipse, pBrush, 1.0f);
    }

    BOOL HitTest(float x, float y)
    {
        const float a = ellipse.radiusX;
        const float b = ellipse.radiusY;
        const float x1 = x - ellipse.point.x;
        const float y1 = y - ellipse.point.y;
        const float d = ((x1 * x1) / (a * a)) + ((y1 * y1) / (b * b));
        return d <= 1.0f;
    }
};

このプログラムでは、同じ純色ブラシを使用して、すべての省略記号の塗りつぶしと輪郭を描画し、必要に応じて色を変更します。 Direct2D では、単色ブラシの色を変更することは効率的な操作です。 そのため、純色ブラシのオブジェクトは、 SetColor メソッドをサポートします。

省略記号は、STL list コンテナーに格納されます。

    list<shared_ptr<MyEllipse>>             ellipses;

Note

shared_ptr は、TR1 で C++ に追加され、C++0x で形式化されたスマート ポインター クラスです。 Visual Studio 2010 では、 shared_ptr やその他の C++0x 機能のサポートが追加されました。 詳細については、MSDN マガジンの記事「 Visual Studio 2010 の新しい C++ および MFC 機能の概要を参照してください。

 

このプログラムには、次の 3 つのモードがあります。

  • 描画モード。 ユーザーは新しい省略記号を描画できます。
  • 選択モード。 ユーザーは省略記号を選択できます。
  • ドラッグ モード。 ユーザーは、選択した省略記号をドラッグできます。

ユーザーは、「Accelerator Tables」で説明されているのと同じキーボード ショートカットを使用して、描画モードと選択モードを切り替えることができます。 選択モードから、ユーザーが省略記号をクリックすると、プログラムはドラッグ モードに切り替わります。 ユーザーがマウス ボタンを離すと、選択モードに戻ります。 現在の選択範囲は、省略記号の一覧に反復子として格納されます。 ヘルパー メソッド MainWindow::Selection は、選択した省略記号へのポインター、または選択されていない場合は値 nullptr を返します。

    list<shared_ptr<MyEllipse>>::iterator   selection;
     
    shared_ptr<MyEllipse> Selection() 
    { 
        if (selection == ellipses.end()) 
        { 
            return nullptr;
        }
        else
        {
            return (*selection);
        }
    }

    void    ClearSelection() { selection = ellipses.end(); }

次の表は、3 つの各モードでのマウス入力の効果をまとめたものです。

マウス入力 描画モード Selection Mode ドラッグ モード
左ボタンの下 マウス キャプチャを設定し、新しい省略記号の描画を開始します。 現在の選択範囲を解除し、ヒット テストを実行します。 省略記号がヒットした場合は、カーソルをキャプチャし、省略記号を選択して、ドラッグ モードに切り替えます。 NO ACTION
マウスの移動 左ボタンが下にある場合は、省略記号のサイズを変更します。 NO ACTION 選択した省略記号に移動します。
左ボタンの上 省略記号の描画を停止します。 NO ACTION [選択] モードに切り替えます

 

MainWindow クラスの次のメソッドは、WM_LBUTTONDOWN を処理します。

void MainWindow::OnLButtonDown(int pixelX, int pixelY, DWORD flags)
{
    const float dipX = DPIScale::PixelsToDipsX(pixelX);
    const float dipY = DPIScale::PixelsToDipsY(pixelY);

    if (mode == DrawMode)
    {
        POINT pt = { pixelX, pixelY };

        if (DragDetect(m_hwnd, pt))
        {
            SetCapture(m_hwnd);
        
            // Start a new ellipse.
            InsertEllipse(dipX, dipY);
        }
    }
    else
    {
        ClearSelection();

        if (HitTest(dipX, dipY))
        {
            SetCapture(m_hwnd);

            ptMouse = Selection()->ellipse.point;
            ptMouse.x -= dipX;
            ptMouse.y -= dipY;

            SetMode(DragMode);
        }
    }
    InvalidateRect(m_hwnd, NULL, FALSE);
}

マウスの座標は、このメソッドにピクセル単位で渡された後、DIP に変換されます。 これら 2 つのユニットを混同しないことが重要です。 たとえば、 DragDetect 関数ではピクセルが使用されますが、描画とヒット テストでは DIP が使用されます。 一般的なルールは、ウィンドウまたはマウス入力に関連する関数はピクセルを使用し、Direct2D と DirectWrite では DIP を使用します。 常に高い DPI 設定でプログラムをテストし、プログラムを DPI 対応としてマークすることを忘れないでください。 詳細については、「DPI とデバイスに依存しないピクセル」を参照してください。

WM_MOUSEMOVE メッセージを処理するコードを次に示します。

void MainWindow::OnMouseMove(int pixelX, int pixelY, DWORD flags)
{
    const float dipX = DPIScale::PixelsToDipsX(pixelX);
    const float dipY = DPIScale::PixelsToDipsY(pixelY);

    if ((flags & MK_LBUTTON) && Selection())
    { 
        if (mode == DrawMode)
        {
            // Resize the ellipse.
            const float width = (dipX - ptMouse.x) / 2;
            const float height = (dipY - ptMouse.y) / 2;
            const float x1 = ptMouse.x + width;
            const float y1 = ptMouse.y + height;

            Selection()->ellipse = D2D1::Ellipse(D2D1::Point2F(x1, y1), width, height);
        }
        else if (mode == DragMode)
        {
            // Move the ellipse.
            Selection()->ellipse.point.x = dipX + ptMouse.x;
            Selection()->ellipse.point.y = dipY + ptMouse.y;
        }
        InvalidateRect(m_hwnd, NULL, FALSE);
    }
}

省略記号のサイズを変更するロジックについては、「例: 描画サークル」セクションで説明しました。 また、InvalidateRect の呼び出しにも注意してください。 これにより、ウィンドウが再描画されます。 次のコードは、WM_LBUTTONUP メッセージ を処理します。

void MainWindow::OnLButtonUp()
{
    if ((mode == DrawMode) && Selection())
    {
        ClearSelection();
        InvalidateRect(m_hwnd, NULL, FALSE);
    }
    else if (mode == DragMode)
    {
        SetMode(SelectMode);
    }
    ReleaseCapture(); 
}

ご覧のように、マウス入力のメッセージ ハンドラーには、現在のモードに応じて分岐コードがあります。 これは、この非常に単純なプログラムで許容される設計です。 ただし、新しいモードが追加されると、すぐに複雑になる可能性があります。 大規模なプログラムの場合、モデル ビュー コントローラー (MVC) アーキテクチャの方が優れた設計になる場合があります。 この種のアーキテクチャでは、ユーザーによる入力を処理する controller は、アプリケーション データを管理する model から分離されます。

プログラムがモードを切り替えると、カーソルが変更され、ユーザーにフィードバックが送信されます。

void MainWindow::SetMode(Mode m)
{
    mode = m;

    // Update the cursor
    LPWSTR cursor;
    switch (mode)
    {
    case DrawMode:
        cursor = IDC_CROSS;
        break;

    case SelectMode:
        cursor = IDC_HAND;
        break;

    case DragMode:
        cursor = IDC_SIZEALL;
        break;
    }

    hCursor = LoadCursor(NULL, cursor);
    SetCursor(hCursor);
}

最後に、ウィンドウが WM_SETCURSOR メッセージを受信したときにカーソルを設定することを忘れないでください。

    case WM_SETCURSOR:
        if (LOWORD(lParam) == HTCLIENT)
        {
            SetCursor(hCursor);
            return TRUE;
        }
        break;

まとめ

このモジュールでは、マウスとキーボードの入力を処理する方法、キーボード ショートカットを定義する方法、プログラムの現在の状態を反映するようにカーソル イメージを更新する方法を学習しました。。