ユーザー入力: 拡張例

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

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

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

楕円は、楕円データ (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<shared_ptr<MyEllipse>>             ellipses;

Note

shared_ptr は、TR1 で C++ に追加され、C++0x で形式化されたスマート ポインター クラスです。 Visual Studio 2010 では、 shared_ptr およびその他の C++0x 機能のサポートが追加されています。 詳細については、「MSDN MagazineVisual Studio 2010 での C++ および MFC の新機能の探索」を参照してください。 (このリソースは、一部の言語や国では使用できない場合があります)。

 

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

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

ユーザーは、「 アクセラレータ テーブル」で説明されているのと同じキーボード ショートカットを使用して、描画モードと選択モードを切り替えることができます。 選択モードから、ユーザーが省略記号をクリックすると、プログラムはドラッグ モードに切り替わります。 ユーザーがマウス ボタンを離すと、選択モードに戻ります。 現在の選択範囲は、反復子として省略記号の一覧に格納されます。 ヘルパー メソッド 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 つの各モードでのマウス入力の効果をまとめたものです。

マウス入力 描画モード 選択モード ドラッグ モード
左ボタンを下へ マウス キャプチャを設定し、新しい楕円の描画を開始します。 現在の選択範囲を解放し、ヒット テストを実行します。 楕円にヒットした場合は、カーソルをキャプチャし、楕円を選択してドラッグ モードに切り替えます。 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 とDevice-Independent ピクセル」を参照してください。

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) アーキテクチャの方が優れた設計になる可能性があります。 この種のアーキテクチャでは、ユーザー入力を処理する コントローラーは、アプリケーション データを管理する モデルから分離されます。

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

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;

まとめ

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