ホーム < ゲームつくろー! < Programming TIPs編

その11 ビットマップでリージョンを作成する


 Windowsで言うリージョン(Region:領域)とはHRGNによって定義される囲いです。ウィンドウは標準で長方形のリージョンを持っていますが、SetWindowRgn関数で独自のリージョンを設定すると、その形にくり貫かれてしまいます。メディアプレイヤーなどのスキンはリージョンを利用して自由でポップなウィンドウを提供しているわけです。

 さてこのリージョンですが、Create***Rgn関数群で作るのが普通です。しかし、これらは長方形や円などの単純なリージョンしか扱えません。CreatePolygonRgn関数だと多角形を扱えるのですが、その頂点は自前で与える必要があります。中空のリージョンや複雑な形状のリージョンだと、これら関数ではお手上げになってしまうわけです。

 そこで、この章ではリージョンの形を現したビットマップから複雑な形状のリージョンを生成してくれるAutoRegionクラスを作ってみる事にします。とても重宝しますよ!



@ リージョンの合成

 リージョンは合成する事ができます。四角いリージョンと丸いリージョンを重ねて複雑な形状にすることもできますし、穴あきにする(引き算する)事もできます。それを担うのがCombineRgn関数です:

CombineRgn関数
CombineRgn {
   HRGN hrgnDest,
   HRGN hrgnSrc1,
   HRGN hrgnSrc2,
   int fnCombineMode
);

hrgnDestは合成後の領域を格納するリージョンハンドルです。ポインタでも参照でもないので何だか不思議な感じがしますが、ハンドルはぶっちゃげ「整数」でして、きっと内部で該当するハンドルが指すリージョンをゴリゴリと変更しているんだと思います。そのためhrgnDestにはすでに何らかの有効なリージョンが設定されているハンドルを渡す必要があります。
hrgnSrc1及びhrgnSrc2には合成するリージョンハンドルを渡します。
fnCmpineModeにはどう合成するかをフラグで設定します。設定可能なフラグは以下の通り:

説明
RGN_AND 2つのリージョンが重なった部分だけを新しいリージョンとします。
RGN_COPY hrgnSrc1をコピーします。
RGN_DIFF hrgnSrc1からhrgnSrc2を除いた部分を新しいリージョンとします。重なっていない場合は無作用です。
RGN_OR 2つのリージョンを単純に足し合わせます。
RGN_XOR 2つのリージョンの重なっていない部分を新しいリージョンとします。重なっている部分は取り除かれてしまいます。

このフラグを駆使する事で、複雑なリージョンを作っていく事ができます。



A ビットマップからリージョンを作る姑息な手

 ビットマップ(白黒アルファ画像)からリージョンを作る事を考えます。今回は「真っ黒い部分がリージョン」とします。例えば次のような画像です:

なぜ迎春なのかはまぁ気にしないで頂きたいのですが、これを多角形リージョンで作るのはきっと無理もしくは無謀です。

 上のような複雑なリージョンの作り方は至って姑息でシンプルです。ビットマップを1ラインずつチェックしていって、白い部分を抜いていくだけです。姑息ですが、確実な方法です。

 ではまずビットマップの扱いから参りましょう。Windowsは何かとビットマップとは相性が良く、操作するための関数も沢山用意されています。色々と考えたのですが、確実に簡単なのはビットマップをデバイスコンテキストに描画して、そのピクセルをチェックする方法だと思います。

 ファイルにあるビットマップからHBITMAPを作成するにはLoadImage関数を用います。これについては前の章で紹介しておりますので、そちらを参照下さい。一応典型的なコードを示します:

LoadImage関数によるビットマップハンドル取得関数
HBITMAP LoadBMP( HWND hWnd, TCHAR* file ) {
   return (HBITMAP)LoadImage(
                    (HINSTANCE)(LONG64)GetWindowLong( hWnd, GWL_HINSTANCE ),
                    file,
                    IMAGE_BITMAP,
                    0, 0,
                    LR_LOADFROMFILE
                 );
}

 この関数を使えば何も考えずにファイルからHBITMAPを取得できます。

 取得後にする事はビットマップのサイズ(縦横)を調べる事です。これはGetObject関数を使うのが楽です:

GetObject関数でビットマップの幅と高さを得る
BITMAP bi;
GetObject( hBMP, sizeof(BITMAP), &bi );

上の関数が成功すればbiに幅と高さのピクセル数が格納されます。

 後はHBITMAPをデバイスコンテキストに適用します:

デバイスコンテキストにビットマップを適用
HDC hDC = GetDC( NULL );
HDC memDC = CreateCompatibleDC( hDC );
SelectObject( hDC, hBMP );

GetDCにNULLを指定するとデスクトップウィンドウのデバイスコンテキストを取得できます。さらに互換性のあるデバイスコンテキストを作り、そこにビットマップを適用します。後はピクセル情報を調べる事でリージョンを作成できます:

全ピクセルチェック
HRGN hRgn = CreateRectRgn( hDC, 0, 0, bi.bmWidth+1, bi.bmHeight+1 );

int x, y;
for ( y = 0; y < bi.bmHeight ; y++ ) {
   for ( x = 0; x < bi.bmWidth ; x+; ) {
      DWORD color = GetPixel( memDC, x, y );
      if ( color != 0 ) {
         HRGN DiffRgn = CreateRectRgn( x, y, x+1, y+1 );
         CombineRgn( hRgn, hRgn, DiffRgn, RGN_DIFF );   // 黒じゃない部分を取り除く
      }
   }
}

こうする事で、最初長方形だったリージョンはどんどん削れ、最終的に「迎春」が浮き彫りになります。とても原始的な方法ですが、唯一確実な方法です。CreateRectRgnによる矩形リージョンは右端(right)と下端(bottom)を含みません。よって引数に「+1」が入っているわけです。



B 迎春ウィンドウ

 試しにこの方法で取得したリージョンでウィンドウをくり貫いて「迎春ウィンドウ」を作ってみました:

このウィンドウはつまんで動かせるようにしました。だからどうしたなんですが…(^-^;



C 迎春縁取り

 FrameRgn関数(リージョンを縁取る関数)とFillRgn関数(リージョン内を塗りつぶす)を使うとこんな事もできます:

 

これはPOPUPウィンドウに描きました。リージョンは色々と遊べるわけです。前章のカスタムボタンの範囲を決める時には、この方法が大活躍してくれるはずです。



D AutoRegionクラス

 白黒のビットマップからリージョンハンドルを取得できるAutoRegionクラスを最後に公開してこの章を閉めたいと思います。短めなので全ソース公開です。

AutoRegion.h
// AutoRegion.h
#ifndef IKD_AUTOREGION_H
#define IKD_AUTOREGION_H

// 自動リージョン生成クラス

#include <tchar.h>

namespace IKD {
   class AutoRegion {
   public:
      AutoRegion();
      ~AutoRegion();
      HRGN getRgnFromBMPFile( TCHAR* file, HWND hWnd );
      HRGN getRgnFromHBITMAP( HBITMAP hBMP, HWND hWnd );
   };
}

#endif
AutoRegion.cpp
// AutoRegion.cpp

#include <windows.h>
#include "AutoRegion.h"

using namespace IKD;

AutoRegion::AutoRegion() {
}

AutoRegion::~AutoRegion() {
}


HRGN AutoRegion::getRgnFromHBITMAP( HBITMAP hBMP, HWND hWnd ) {
   BITMAP bi;
   GetObject( hBMP, sizeof(BITMAP), &bi );
   HRGN hRgn = CreateRectRgn( 0, 0, bi.bmWidth, bi.bmHeight );

   // コンパチブルDCに一度描画してから点を検査
   HDC hDC = GetDC( NULL );
   HDC memDC = CreateCompatibleDC( hDC );
   SelectObject( memDC, hBMP );
   int x, y;
   for( y=0; y<bi.bmHeight; y++ ) {
      for( x=0; x<bi.bmWidth; x++ ) {
         // ラインを走査して0x00000000以外はリージョン範囲からはずす
         COLORREF color = GetPixel( memDC, x, y );
         if( color != 0x00000000 ) {
            HRGN DiffRgn = CreateRectRgn( x, y, x+1, y+1 );
            CombineRgn( hRgn, hRgn, DiffRgn, RGN_DIFF);
            DeleteObject( DiffRgn );
         }
      }
   }
   DeleteDC( memDC );
   ReleaseDC( hWnd, hDC );

   return hRgn;
}


HRGN AutoRegion::getRgnFromBMPFile( TCHAR* file, HWND hWnd ) {
   // ファイルチェック
   HINSTANCE hInst = (HINSTANCE)(LONG64)GetWindowLong( hWnd, GWL_HINSTANCE );
   HBITMAP hBMP = (HBITMAP)LoadImage( hInst, file, IMAGE_BITMAP, 0, 0, LR_CREATEDIBSECTION | LR_LOADFROMFILE );
   if ( !hBMP ) {
      return 0;
   }
   HRGN hRgn = getRgnFromHBITMAP( hBMP, hWnd );
   DeleteObject( hBMP );
   return hRgn;
}