Tips of VC++ > ビットマップ > ビットマップをスムーズにドラッグして移動
前へ戻る次へ進む


このドキュメントにはサンプルプログラムが含まれています。
ワークスペース
ソースファイル(テキスト)


ビットマップをスムーズにドラッグして移動

Windows標準ゲームであるソリティアやフリーセル等の カードゲームを見ていると、どうしてこんなにスムーズに ビットマップがドラッグできるんだと思うことあります。
実は、WM_PAINTとマウスイベントの処理の連携で スムーズな処理が実現するのです。
概略を説明します。
ビットマップを早く移動させるためには、 要は描画する面積が小さければいいのです。 現在位置の画像を消して新たにビットマップを書くのは無駄です。 1ピクセルずつ動かすのであれば、 動く前と動く後で重なる部分があると思います。 そこに二度も描画するようなプログラムを書いてませんか?
領域の簡単な足し算・引き算で可能になるはずです。 100%保証するものではありませんが。(逃げ
実際にサンプルを作ってみます。 ワークスペースが落とせますよ〜このページの一番上を見てくださいな。

プログラムを書くにあたって、ビットマップにはこのサイトのバナーを、 描画や読み込みにはDIBLIB を使用してみます。
まず、グローバル変数の定義。

POINT     dibpos= {10,10};
POINT     offset;
CDIBitmap dib;

dibposは現在のビットマップの左上隅の位置。 適当な値で初期化しておきます。そう、初期位置。 offsetは別に初期化しなくてもいいです。 dibはコンストラクタではなくWM_CREATEで初期化してください。
とりあえずプロシージャに以下のメッセージを追加。
描画の表向きの処理です。俗に言う社交辞令ですね。(全然違う

case WM_CREATE:
    hDC=::CreateCompatibleDC(NULL);
    dib.Create(hDC,IDB_BITMAP1,hAppModule);
    ::DeleteDC(hDC);
    break;
case WM_PAINT:
    PAINTSTRUCT ps;
    hDC=::BeginPaint(in_hWnd,&ps);
    dib.BitBlt(hDC,dibpos.x,dibpos.y,SRCCOPY);
    ::EndPaint(in_hWnd,&ps);
    break;

次にWM_LBUTTONDOWN,WM_LBUTTONUP,WM_MOUSEMOVEメッセージを それぞれ追加していきます。 その前に次の関数を記述してください。(肩透かし

# じらしてないか?オイ

void GetDIBRect(LPRECT in_pRect)
{
    SIZE s;
    dib.GetBitmapSize(&s);
    ::SetRect(in_pRect,dibpos.x,dibpos.y,
        dibpos.x+s.cx,dibpos.y+s.cy);
}

現在のビットマップの四隅の位置をRECT構造体に代入します。 CDIBitmapの画像のサイズを求めるやり方が面倒なので このような関数を作っておくと楽です。 左上の座標にビットマップサイズを足してるだけです。

次にWM_LBUTTONDOWNメッセージを追加します。 ここでは、マウスのキャプチャとクリックされた位置の 画像の左上隅からのオフセットの保存です。 下図を見ればオフセットについて分かりやすくなるかもしれません。

オフセットの概念(のつもり)
case WM_LBUTTONDOWN:
    pt.x= GET_X_LPARAM(in_lParam);
    pt.y= GET_Y_LPARAM(in_lParam);

    GetDIBRect(&rct);

    if ( ::PtInRect(&rct,pt) )
    {
        ::SetCapture(in_hWnd);
        offset.x= pt.x-dibpos.x;
        offset.y= pt.y-dibpos.y;
    }
    break;

GET_X_LPARAMを使うのでwindowsx.hをインクルードしておいてください。 まず画像の位置を取得して(GetDIBRect)、カーソルの位置が そこに含まれているかどうかチェックします。 画像上にカーソルがあればマウスをキャプチャーして オフセットを保存します。
点が、特定の長方形上にあるかどうかを調べるには PtInRectという便利な関数を使えます。

BOOL PtInRect(
  CONST RECT *lprc,  // 長方形
  POINT pt           // 点
);

点が長方形の中に含まれていればtrue、 含まれていなければfalseが返されます。

次にWM_MOUSEMOVE。これがプログラムの山場です。 ちょっと長いですね。 やってることは、新たに画像を描いて、 リージョンをちょこちょこっと引き算して、 はみ出した部分にWM_PAINTを送る作業です。

case WM_MOUSEMOVE:
    if ( ::GetCapture()!=in_hWnd )
        return 0;

    HRGN hRgn1,hRgn2,hRgn3;

    // 移動前の位置を取得
    GetDIBRect(&rct);
    hRgn1= ::CreateRectRgnIndirect(&rct);

    // 移動後の新しいビットマップ位置を代入
    pt.x= GET_X_LPARAM(in_lParam);
    pt.y= GET_Y_LPARAM(in_lParam);
    dibpos.x= pt.x-offset.x;
    dibpos.y= pt.y-offset.y;

    // 移動後の位置を取得
    GetDIBRect(&rct);
    hRgn2= ::CreateRectRgnIndirect(&rct);

    // 関数に渡す前に初期化が必要
    hRgn3= ::CreateRectRgnIndirect(&rct);

    // リージョンの引き算
    ::CombineRgn(hRgn3,hRgn1,hRgn2,RGN_DIFF);

    hDC=::GetDC(in_hWnd);
    dib.BitBlt(hDC,dibpos.x,dibpos.y,SRCCOPY);
    ::ReleaseDC(in_hWnd,hDC);

    ::InvalidateRgn(in_hWnd,hRgn3,TRUE);
    ::UpdateWindow(in_hWnd);

    ::DeleteObject(hRgn1);
    ::DeleteObject(hRgn2);
    ::DeleteObject(hRgn3);

    break;

まず、マウスボタンが押されているかどうかは マウスがキャプチャされているかどうかで識別できます。 (あまりこのやり方がいいとは思えませんが…)
次にメインとなる作業――画像の位置をずらす――ですが、 以下のような仕組みを利用します。
下図を参照。 最初は灰色(網掛けだが…)の長方形だけがあると思ってください。 そして、画像がドラッグされます。
最初の手順として、まず新しい位置に画像を単純に書きます。 それが白い長方形です。そして網掛けの残りの部分を消せば完了、です。 では、どうやってこの逆L字型というような形を塗りつぶすか?
塗りつぶすのではいろいろな場合に応用できないから、 できれば逆L字型の部分だけに再描画処理を適用したい…。 といろいろ考えますね。
ここでリージョンの登場です。 リージョンを使うと矩形以外の領域を扱えるので 逆L字型だろうが多角形だろうがなんでも扱えます。 工夫すれば長方形以外の画像のドラッグにも使えるということです。
InvalidateRgnで無効にする領域をリージョンで指定できるので、 WM_PAINTの処理を転用できるのです。

仕組み

リージョンの結合関連の処理にはCombineRgnというAPIを使用します。

int CombineRgn(
  HRGN hrgnDest,      // 結合先リージョンのハンドル
  HRGN hrgnSrc1,      // 結合元リージョンのハンドル
  HRGN hrgnSrc2,      // 結合元リージョンのハンドル
  int fnCombineMode   // リージョンの結合モード
);

このAPIは1と2のリージョンを結合させてDestに新しいリージョンを作成します。 また、結合方法もいくつかあります。

value explanation
RGN_AND 2つのリージョンの重なり合う領域を新しいリージョンとします。
(hrgnDest=hrgnSrc1+hrgnSrc2)
RGN_COPY hrgnSrc1で識別されているリージョンのコピーを作成します。
RGN_DIFF hrgnSrc1リージョンのうち、hrgnSrc2リージョンの一部ではない領域を、 新しいリージョンとします。
(hrgnDest=hrgnSrc1-hrgnSrc2)
RGN_OR 両方のリージョンのユニオン (結び、つまり両方のリージョンを完全に含む最小の長方形)を、 新しいリージョンとします。
RGN_XOR 両方のリージョンのユニオンから、 両方のリージョンが重なり合う領域を除いた領域を、 新しいリージョンとします。

以上をふまえてさっきの図をご覧ください。 灰色の部分を取り出すにはRGN_DIFFを使えばいいのです。 すなわち、白い長方形から灰色の長方形ではない部分を 新しいリージョンとするからです。
まず、動かす前の矩形の位置をhRgn1に代入します。 次に座標を動かして、再度矩形の位置を取得しhRgn2とhRgn3に代入します。 CombineRgnに渡すリージョンには何か領域が指定されていないといけないので、 とりあえずhRgn3にもhRgn2と同じ値を代入しておくのです。 別にhRgn1と同じでも構いません。
ConbineRgnでhRgn3を算出したら(灰色長方形-白色長方形)、 まず新しい位置に画像を描きます。 次にhRgn3の領域だけを無効にして再描画させます。

最後にWM_LBUTTONUPメッセージを追加します。 ここではキャプチャしたマウスを開放するだけでいいでしょう。

case WM_LBUTTONUP:
    ::ReleaseCapture();
    break;

May 17, 2002