ユーザー入力: 発展的な例

ユーザー入力に関してこれまでに学んだことの総括として、簡単な描画プログラムを作成してみましょう。このプログラムのスクリーン ショットを以下に示します。

 

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

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

 

ユーザーは複数の異なる色で楕円を描画するほかに、楕円を選択、移動、削除できます。UI をシンプルにするため、このプログラムではユーザーが楕円の色を選択することはできません。代わりに、定義済みのリストの順番で、プログラムが自動的に色を割り当てます。このプログラムは楕円以外の図形をサポートしません。当然ながら、このプログラムはグラフィックス ソフトウェアとしては優れていませんが、学習例としては有用です。完全なソース コードは、「シンプルな描画のサンプル」からダウンロードできます。このセクションでは、重要な一部の箇所のみを扱います。

このプログラムでは楕円を表すのに、楕円データ (D2D1_ELLIPSE) および色 (D2D1_COLOR_F) からなる構造体を使用します。この構造体には、2 つのメソッドも定義されています。1 つは楕円を描画するためのメソッドで、もう 1 つはヒット テストを実行するためのメソッドです。


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;


注: shared_ptr は、TR1 で C++ に追加されたスマートポインター クラスで、C++0x から正式に導入されました。Visual Studio 2010 では、shared_ptr およびその他の C++0x の機能が新たにサポートされます。詳細については、『MSDN マガジン』の「Visual 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 つの各モードにおけるマウス入力の効果をまとめます。

マウス入力描画モード選択モードドラッグ モード
左ボタンを押すマウス キャプチャを設定し、新しい楕円の描画を開始します現在の選択項目を解放し、ヒット テストを実行します。楕円がヒットする場合はカーソルをキャプチャし、楕円を選択して、ドラッグ モードに切り替えますアクションなし
マウスを移動する左ボタンが押されている場合は、楕円のサイズを変更しますアクションなし選択された楕円を移動します
左ボタンを離す楕円の描画を停止しますアクションなし選択モードに切り替えます

 

WM_LBUTTONDOWN メッセージを処理する MainWindow クラスのメソッドを以下に示します。


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);
        
            // 新しい楕円を開始する
            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 と DIP (デバイス非依存ピクセル)」を参照してください。

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)
        {
            // 楕円のサイズを変更する
            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)
        {
            // 楕円を移動する
            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;

    // カーソルを更新する
    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;


まとめ

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

 

 

このトピックに関するご意見をお寄せください (英語のみ)。

作成日: 2010 年 10 月 5 日

コミュニティの追加

追加
表示:
© 2014 Microsoft