はじめに
今更ながら、Win32apiの「リージョン」とは何か、パスとの違いを調べてみました。
私は、WIndows 3.0の頃からWin32のプログラムを作ってきましたが、漠然とパスは「線」、リージョンは「面」を示すデータくらいの認識を持っていました。
ただ、パスには開いたパスと閉じたパスがあり前者はパスに囲まれた領域を示し、実際に塗りつぶしたり「面」としての処理もできるのですから、上記の区別ではパスとリージョンの違いを言い表せていないことになります。
ネットで検索しても特にリージョンの説明は何故だか少ないのです。
という事で、今更ながらちゃんと調べてようと一念発起したというわけですが、結果は今まで思い込んでいたのとは些か違っていました。
私と同様に漠然とリージョンを使っていたという方、是非最後まで読んでください。
リージョンの基本(GDI編)
まずはリージョンの使い方などをおさらいします。実はGDIとGDI+で違うのですが、しばらくはGDIの場合で説明します。
リージョンの作り方
GDIで新規にリージョンを作成するAPI関数には以下のようなものがあります。
作成されるとHRGN ハンドルが返されます。
MFCで作成する場合は CRgn クラスのメソッドの実行という形になります。
リージョン用の描画命令が一式用意されているのが、パスと違います。パスはBeginPathとEndPathの間で一般の描画命令を実行するという方式です。
- CreateEllipticRgn 楕円
- CreateEllipticRgnIndirect 楕円
- CreatePolygonRgn 多角形
- CreatePolyPolygonRgn 多角形
- CreateRectRgn 矩形
- CreateRectRgnIndirect 矩形
- SetRectRgn 矩形
- CreateRoundRectRgn 角の丸い矩形
- CreateFromPath パスから作る
- CreateFromData GetRegionDataで取得したデータから作る
例えば、楕円のリージョンを作るには以下のようにします。
HRGN hRgn = ::CreateEllipticRgn(10, 10, 600, 400)
MFCであれば同じことを以下のように書きます。
CRgn rgn;
rgn.CreateEllipticRgn(10, 10, 600, 400)
ところで、MFCとは「Microsoft Foundation Class」の事でWin32apiをC++のクラス群に置き換えたラッパークラスです。以後プログラムはMFCで記載しますが、MFCでも「素」のwin32apiでも機能は同じですので、頭の中で変換して頂ければと思います。
こんな感じです。
(MFC)
dc.LineTo(10, 10, 100, 200);
↓
(Win32api)
LineTo(hdc, 10, 10, 100, 200);
リージョンの描画
リージョンは「面」ですので描画と言えばまず塗りつぶしですが、それ以外の命令もあります。。
- FillRgn 塗りつぶし
- PaintRgn 塗りつぶし
- FrameRgn 周囲に罫線を描画
- InvertRgn 領域内の色を反転
FrameRgnは「線 line」と言わず「罫線 border」と説明するのが意味深です。ここにリージョンとパスの違いの一端が現れていると言えるでしょう。説明は後にとっておきます。
リージョンを塗りつぶすプログラムを示します。MFCのOnPrintメソッドです。
void CChildView::OnPaint()
{
CPaintDC dc(this);
CRgn rgn;
rgn.CreateEllipticRgn(10, 10, 400, 300);
CBrush brRed(RGB(255, 0, 0));
dc.FillRgn(&rgn, &brRed);
}
実行結果は下記の通りです。

リージョンの塗りつぶし
念のため、同じ処理を「素」のWin32apiで書けば以下のようになります。こちらはWM_PRINTメッセージの処理となります。
やっている事はMFCと完全にイコールです。
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(NULL, &ps);
HRGN hrgn = CreateEllipticRgn(10, 10, 400, 300);
HBRUSH hbrRed = CreateSolidBrush(RGB(255, 0, 0));
FillRgn(hdc, hrgn, hbrRed);
DeleteObject(hbrRed);
DeleteObject(hrgn);
EndPaint(hwnd, &ps);
return 0;
}
クリップ領域
塗りつぶしは一般の描画命令やパスを使ってする事が多いので、リージョンの実際的な用途はそれ自体を描画するより、後から描画を制限する、すなわちクリップ領域を設定するのに使われる事が多いのではないでしょうか。
クリップ領域の設定は下記のようなAPI関数を使います。
- SelectClipRgn
- ExtSelectClipRgn
- SetMetaRgn
等
プログラム例を示します。変数の text には長い文章の文字列が格納されています。
まず楕円のリージョン(rgn)を作成し、デバイスコンテキスト(dc)にクリップ領域として設定します。
それに続いて画面いっぱいに表示したテキストの表示が楕円のクリップ領域内に抜き取られるという訳です。
void CChildView::OnPaint()
{
CPaintDC dc(this);
CRgn rgn;
rgn.CreateEllipticRgn(10, 10, 400, 300);
dc.SelectClipRgn(&rgn);
CRect rc;
GetClientRect(&rc);
dc.DrawText(text, -1, &rc, DT_WORDBREAK);
}
実行すると以下のようになります。

リージョンによるクリップ
ここまでのリージョンを使用した例でやった事は、実はリージョンを使わずパスを使ってもなんなくできます。上に示した楕円のテキスト表示であれば、以下のパスを使ったプログラムでも全く同じ結果になります。
void CChildView::OnPaint()
{
CPaintDC dc(this);
dc.BeginPath();
dc.Ellipse(10, 10, 400, 300);
dc.EndPath();
dc.SelectClipPath(RGN_COPY);
CRect rc;
GetClientRect(&rc);
dc.DrawText(text, -1, &rc, DT_WORDBREAK);
}
パスの作成はBeginPathとEndPathの間で描画命令を実行するとデバイスコンテキストにパスが保管されるという方式で、領域の作成よりも癖があります。GDI+ではパスもオブジェクトとして整理されて、実用性が格段に増しています。それはさておき。
上のプログエラムを実行すると下図のようになりますが、要するに領域でなくパスを使っても結果は同じです。

パスによるクリップ
リージョンの結合
複数のリージョンをAND/OR/XOR/COPY/DIFFの演算で結合する事です。
APIは下記となります。
以下のプログラムは複数の楕円をXORで結合したクリップ領域を作る例です。
void CChildView::OnPaint()
{
CPaintDC dc(this);
CRgn rgn1;
rgn1.CreateEllipticRgn(10, 10, 400, 300);
CRgn rgn2;
rgn2.CreateEllipticRgn(210, 10, 600, 300);
CRgn rgn;
rgn.CreateRectRgn(0, 0, 0, 0);
rgn.CombineRgn(&rgn1, &rgn2, RGN_XOR);
dc.SelectClipRgn(&rgn);
CRect rc;
GetClientRect(&rc);
dc.DrawText(text, -1, &rc, DT_WORDBREAK);
}

リージョンによるクリップの結合
パスでもやってみましょう。パスはデバイスコンテキストの中で結合させるというやり方になりますが、やはりリージョンと同じ結果になります。
void CChildView::OnPaint()
{
CPaintDC dc(this);
dc.BeginPath();
dc.Ellipse(10, 10, 400, 300);
dc.EndPath();
dc.SelectClipPath(RGN_COPY);
dc.BeginPath();
dc.Ellipse(210, 10, 600, 300);
dc.EndPath();
dc.SelectClipPath(RGN_XOR);
CRect rc;
GetClientRect(&rc);
dc.DrawText(text, -1, &rc, DT_WORDBREAK);
}

パスによるクリップの結合
ひとまずの纏め
ここまで示した通り、プログラムの外形としてはパスでもリージョンでも同じ。これだけ見ると2つは要らない。リージョンは「面」だけで、パスは「線」も「面」も可能ですから、パスだけあれば良いように思えてしまいます。
しかしリージョンにちゃんと役割があります。パスでもクリップ領域は作れると書きましたが、内部的にはパスで設定したクリップ領域は内部的にはリージョンに置き換えられています。要するに描画は兎も角、クリップはリージョンでなければ駄目なのです。
という事を踏まえて、以降が本題、リージョンの中身の話をします。
リージョンの中身
リージョンはラスター
リージョンは、プログラムの外形としてはリージョンは多角形とか楕円とか閉じたパスとか、ベクトル図形を描画しているのだから内部には描画したベクトル図形データが蓄積されているのだろうと思ってしまいそうです。あるいは当然そうであるべきと期待してしまいます。
しかし実はそうではなくリージョンはCreateXxxx関数の指定は「ベクトル図形的」でも、それを内部ではラスター図形のデータとして保管します。
まずはAPI関数の仕様を確認してください。リージョンの内部データを取り出すAPI関数は、GetRegionData ですが、これはリージョンを構成する矩形の配列を取得する仕様となっているのです。
learn.microsoft.com
「矩形であればラスターではなくベクトル図形でしょう」、と言われそうですが、例えばCreateEllipticRgn で作成した円のリージョンデータをGetRegionData で取得すると以下の図のようなデータが返されます。

GDIにおける円のリージョンの保有データ
このギザギザ図形はベクトル図形ではなくラスター図形の特徴である事が分かっていただけるでしょうか。矩形の集合という形式は謂わばビットマップのランレングス圧縮と本質的には同じで、その意味で矩形の配列というのはラスターデータの圧縮形式と考えることができます。
論より証拠
腹落ちしない人もいるかもしれませんが、論より証拠です。
小さな円(半径30)のリージョンを作って10倍に拡大して描画させてみます。拡大のためにGM_ADVANCEDを使用します。
void CChildView::OnPaint()
{
CPaintDC dc(this);
dc.SetGraphicsMode(GM_ADVANCED);
XFORM xf = { 10.0f, 0, 0, 10.0f, 0, 0 };
dc.SetWorldTransform(&xf);
CRgn rgn;
rgn.CreateEllipticRgn(1, 1, 31, 31);
CBrush brRed(RGB(255, 0, 0));
dc.FillRgn(&rgn, &brRed);
}

リージョンの拡大描画
リージョンがラスターである事を示すギザギザができました。
パスでも試してみましょう。
void CChildView::OnPaint()
{
CPaintDC dc(this);
dc.SetGraphicsMode(GM_ADVANCED);
XFORM xf = { 10.0f, 0, 0, 10.0f, 0, 0 };
dc.SetWorldTransform(&xf);
dc.BeginPath();
dc.Ellipse(1, 1, 31, 31);
dc.EndPath();
CBrush brRed(RGB(255, 0, 0));
CBrush* pBrOld = (CBrush*)dc.SelectObject(&brRed);
dc.FillPath();
dc.SelectObject(pBrOld);
}

パスの拡大描画
リージョンとは違い、パス描画の拡大ではギザギザはできません。内部に格納された図形データがベクトルデータであるためです。
リージョンの存在意義
リージョンは何故このような仕様なのか、というより、何のためにリージョンが存在するのか、パスがあるから要らないでしょう、と言う思いが脳裏をよぎっているかと思う。
リージョンのこうした仕様の狙いは、クリップ領域の判定における「スピード」であるに違いありません。ある点がクリップ領域に含まれているか否か、ベクトル図形データで数学的に判定するより、矩形配列で判定した方が早いに決まっています。
そしてそのスピードは少なくても一昔前のWindowsでは非常に重要な事でした。Windowsのウィンドウの描画は常にクリップ領域に対して行われるからです。
ウィンドウの描画は例えばウィンドウからはみ出す大きな円を描画しても、はみ出た部分は描画されずにクリップされます。ウィンドウの外に描画しても何も起こらないの当然でしょ、とつい思えてしまいますが、ウィンドウもWindowsがスクリーン上に描画している図形に過ぎず、ウィンドウの外に描画されないのはクリップ領域が設定されているからです。
あるいは、ウィンドウの裏に部分的に隠れていた別のウィンドウを前面に表示したとき、再度描画するのはウィンドウに隠されていた部分だけ、つまり「無効領域」だけであるのが、Windowsの描画の大基本です。
無効領域はクリップ領域として設定され、WM_PAINTメッセージが発出されて、ここのプログラムは無効領域を意識せず再描画をする仕組になっています。みなさんが今使っているWindowsに表示されている何十というウィンドウも一番後ろのデスクトップもこの仕組みで表示されています。
つまりクリップはWindowsの「幹」であり、領域内外の判定のスピードはWindowsの全体の表示スピードに直結しているのです。
GDI+のリージョン
GDI+のリージョンはベクトルデータ
これまでのリージョンの説明は全てGDIでの話です。
GDI+のリージョンは内部データがベクトルデータに一新されています。ハードウェアの能力が向上して、当初の「スピード」の目的は優先順位が下がったという事なのでしょう。
プログラムを動かして確認しましょう。また円を拡大した上で塗りつぶしますので、ベクトルでなければギザギザができるはずです。
void CChildView::OnPaint()
{
CPaintDC dc(this);
Graphics g(dc);
Matrix m;
m.SetElements(10.0f, 0, 0, 10.0f, 0, 0);
g.SetTransform(&m);
GraphicsPath path;
path.AddEllipse(1, 1, 30, 30);
Region rgn;
rgn.Intersect(&path);
SolidBrush brRed(Color::Red);
g.FillRegion(&brRed, &rgn);
}

GDI+のリージョンの塗りつぶし
上図の通り、ギザギザはなくGDI+のリージョンはベクトルデータである事は確認できました。データの実体は GetDataメソッドで取得できます。
earn.microsoft.comこのRegion::GetDataメソッド、バイト列を返す仕様となっていますが、どのようなバイト列を返すのかの説明がどこにもありません。ネットを探しいれば見つかるかもしれませんが、取り敢えずバイトデータを解析するとEMF+のリージョンオブジェクトが格納されている事が分かりました。
EMF+の仕様は以下をご確認ください。
learn.microsoft.com
EMF+のリージョンオブジェクトは木構造で演算と共にパスや矩形データを保有する形式であり、これであれば紛れも無くベクトルデータです。
GDI+のリージョンクラスとパスクラス
既にプログラムを示しましたが、GDI+のリージョンクラスはGDIのリージョンのように、直接リージョンオブジェクトに描画するのではなく、あらかじめ作成しておいたパスをリージョンに結合していくという方式です。或いは矩形であれば直接に結合できます。
パスはGDIのBeginPath、EndPathの方式は改められ、GDI+ではGraphicsPathクラスのオブジェクトなり、このGraphicsPathオブジェクトに対する各種描画メソッドが設けられてます。非常にすっきりしたと言えます。
GDI+のリージョンははパスの集合として整理されたという事になります。
関連リンク
関連ありませんが興味ある方ご覧ください