VCLの TCanvas は、画面に描画する機能、あるいは既に描画されている図形や画像にアクセスするための機能をカプセル化した非常に便利なクラスである。APIの第3章 GDI 入門とスピードボタンで扱ったように、スクリーンのデバイスコンテクストをカプセル化し、いくつかの描画オブジェクトと描画関数を備えている。しかし、TCanvas はリージョンとパスを扱うときは無力である。ここでは、Windows の描画オブジェクトであるリージョンを試してみよう。また、ストロークをオブジェクト化するパスについても調べてみよう。そしてこれらをクリッピングに応用してみよう。
TCanvas には描画のためのメソッドとして、矩形、楕円、多角形、折れ線、孤、扇形、ベジェ曲線、角の丸い矩形などの基本的な描画機能が使える。これらを組み合わせるとどんな図形も描画可能だろうか。原理的にはそうかもしれないが、例えば、三日月形はどうやって描くのだろうか?これを閉じた図形として描くのはなかなか難しそうである。ハート形も一筆書きとして閉じた図形として描くのは難しいだろう。右図は、リージョンを使ってクリッピング領域を設定し、MoveTo( ) と LineTo( ) を使って格子を描いた後、クリッピングを解除して、同じリージョンの輪郭を描いたものである。リージョンは覚えてしまうと非常に便利で、広い用途に使えることがすぐに分かるだろう。
第1章 リージョンとパス |
1−1 リージョンとは何か? |
1−2 リージョンの作成、移動、描画 |
1−3 リージョンのラッパークラス |
1−4 リージョンによるクリッピング |
1−5 パスを使ってみる |
1−6 パスの作成・描画・変換・破棄 |
1−7 ウィンドウのクリッピング |
1−8 まとめ |
リージョンは、英語の region のことであり、辞書によると地域・領域・分野などと訳される。リージョンは先に挙げた矩形や楕円や多角形などの基本的な閉じた図形、またはそれらの組み合わせで作成され、ハンドルで識別される Win32 のオブジェクトである。リージョンそのものは一つの閉じた図形とは限らない。相互に重ならない円と矩形をひとつのリージョンとすることもできる。この節では簡単なリージョンを作成してみよう。
まずは下の図を見て頂きたい。
一番左の図では赤い正方形のなかに青い三角形がめり込んでいる。下のほうはそのアウトラインである。これは正方形のリージョンと三角形のリージョンを一つずつ作って描画したものである。左から二番目の図形は、この二つのリージョンの OR をとって、あらたに作った一つのリージョンである。単に同じ色で重ねて描画したのではないことは下の輪郭を見れば明らかであろう。同様にして AND、XOR、DIFF(正方形から三角形を引いたもの)も示している。これらは、それぞれが一つのリージョンである。XOR のリージョンは二つの図形が重なっていないことに注目してほしい。このように複数の「離れ小島」の図形も一つのリージョンである。ここでは OR 図形を描いてみよう。
実行時の様子を右図に示す。Image1 と Button1 を置いてから、以下のようにする。
List 1
procedure TForm1.Button1Click(Sender: TObject); var hSq,hTr,hOR:HRGN; P:array[0..2] of TPoint; begin hSq := CreateRectRgn(20,40,50,70); P[0] := Point(30,60);P[1] := Point(80,30); P[2] := Point(90,70); hTr := CreatePolygonRgn(P,3,Winding); hOR := CreateRectRgn(0,0,0,0); CombineRgn(hOR,hSq,hTr,RGN_OR); Image1.Canvas.Brush.Color := clRed; PaintRgn(Image1.Canvas.Handle,hOR); DeleteObject(hSq); DeleteObject(hTr); DeleteObject(hOR); end;
すでに述べたようにリージョンはハンドルで識別されるGDIオブジェクトである。ハンドルの型は Windows.pas に以下のように定義されている。
HRGN = type LongWord;
ここでは3つのリージョンを作成する。まず、CreateRectRgn( ) で矩形のリージョンを作成する。次に、3つの点を TPoint の配列に定義し、それを使って CreatePolygonRgn( ) で三角形のリージョンを作る。つぎに、この二つのリージョンを使って OR をとって新しいリージョンを作りたいのであるが、そのためには CombineRgn( ) を使う。結果は hOR になるのだがこの関数を使って合成結果のリージョンを受け取るためには、予め hOR が有効なリージョンでなければならないのである。このため、CreateRectRgn(0,0,0,0) でダミーのリージョンを作成しておかなければならない。そして、CombineRgn( ) によりOR合成のリージョンを受け取り、赤い色のブラシで PaintRgn( ) により塗りつぶす。最後に、DeleteObject( ) により三つのリージョンを削除する。
この節では、もっとも簡単なリージョン操作について試してみた。次節では、ここで使ったAPI関数について調べ、また、リージョンの移動についても試してみよう。
この節では、リージョンに関するAPIについて調べよう。
基本的なリージョンを作成するには以下の関数を使う。
function CreateEllipticRgn(nLeftRect, nTopRect, nRightRect, nBottomRect: Integer): HRGN; stdcall; function CreateEllipticRgnIndirect(const Rect: TRect): HRGN; stdcall; function CreatePolygonRgn(const Points; Count, FillMode: Integer): HRGN; stdcall; function CreatePolyPolygonRgn(const pPtStructs; const pIntArray; nCount, fnPolyFillMode: Integer): HRGN; stdcall; function CreateRectRgn(nLeftRect, nTopRect, nRightRect, nBottomRect: Integer): HRGN; stdcall; function CreateRectRgnIndirect(const Rect: TRect): HRGN; stdcall; function CreateRoundRectRgn(nLeftRect, nTopRect, nRightRect, nBottomRect, nWidthEllipse, nHeightEllipse: Integer): HRGN; stdcall;
おのおのの関数のパラメータは、対応する描画関数と同一なのでいちいち説明しない。基本的には楕円、多角形、矩形、角の丸い矩形だけである。少々物足りない気がするだろうが、後に示すようにパスを使って多様な図形をリージョンにすることができる。
さて、次に基本的なリージョンを操作する関数について調べよう。
最初に、既に使用した CombineRgn( ) についてその宣言を見てみよう。
function CombineRgn(hrgnDest, hrgnSrc1, hrgnSrc2: HRGN; fnCombineMode: Integer): Integer; stdcall;
hrgnDest は、演算結果を得るための既に有効なリージョンハンドルを、hrgnSrc1 と hrgnSrc2 には演算対象のリージョンハンドルを指定する。 fnCombineMode は演算の種類を以下の定数で指定する。
const RGN_AND = 1; RGN_OR = 2; RGN_XOR = 3; RGN_DIFF = 4; // hrgnDest = hrgnSrc1 - hrgnSrc2; RGN_COPY = 5; // hrgnDest = hrgnSrc1; コピー RGN_MIN = RGN_AND; RGN_MAX = RGN_COPY;
演算の結果については既に例で示した。RGN_COPY は、代入ではなく複製であることに注意すること。
この関数の戻り値を以下に示す。
const ERROR = 0; // エラー、リージョンは作成されない NULLREGION = 1; // 空リージョンが作成された SIMPLEREGION = 2; // 矩形リージョンが作成された COMPLEXREGION = 3; // 矩形以外のリージョンが作成された RGN_ERROR = ERROR;
リージョンを平行移動するには、
function OffsetRgn(RGN: HRGN; nXOffset nYOffset: Integer): Integer; stdcall;
を使って nXOffset で X軸方向、nYOffset でY方向に対する移動量を指定する。
そのほかの関数として、リージョンの同一性を判断する
function EqualRgn(p1, p2: HRGN): BOOL; stdcall;
や、既存のリージョンを矩形リージョンに変更する
function SetRectRgn(Rgn: HRgn; X1, Y1, X2, Y2: Integer): BOOL; stdcall;
さらに、リージョンの外接矩形を取得する
function GetRgnBox(RGN: HRGN; var p2: TRect): Integer; stdcall;
などがある。
最後にリージョンの描画関数を調べよう。
function FillRgn(DC: HDC; Rgn: HRGN; Brsh: HBRUSH): BOOL; stdcall;
は、Rgn ハンドルで指定されたリージョンを Brsh ハンドルで指定されたブラシで塗りつぶす。
function FrameRgn(DC: HDC; Rgn: HRGN; Brsh: HBRUSH; nWidth, nHeight: Integer): BOOL; stdcall;
は、Rgn ハンドルで指定されたリージョンの輪郭を Brsh ハンドルで指定されたブラシで描く。nWidth と nHeight は輪郭を描くブラシのX方向とY方向の「太さ」を指定する。これらに等しくない値を指定するとおもしろい効果をあげる。
function InvertRgn(DC: HDC; Rgn: HRGN): BOOL; stdcall;
は、Rgn ハンドルで指定されたリージョン領域の色を反転させる。二度行なえば元に戻る。
function PaintRgn(DC: HDC; Rgn: HRGN): BOOL; stdcall;
は、現在DCに選択されているブラシを使って Rgn ハンドルで指定されたリージョンを塗りつぶす。
リージョンを平行移動させるだけでなく、回転・縮小・拡大・変形などができたらよいと思うだろう。これは当然で、このために ExtCreateRegion( ) というAPIが用意されている。しかし、Win95/98/Me では、この関数の一部の機能しか利用できない。ここではこの関数の使い方には触れないことにする。
それでは、ここまでの関数のうちのいくつかを実際に使ってみよう。
Form1 に Button を三つ用意する。
List 2
procedure TForm1.FormCreate(Sender: TObject); begin Color := clWhite; // オブジェクトインスペクタで設定できる end; procedure TForm1.Button1Click(Sender: TObject); var hRgn1,hRgn2:HRGN; i:integer; begin hRgn1 := CreateRectRgn(20,20,60,60); hRgn2 := CreateRectRgn(20,20,60,60); OffsetRgn(hRgn2,-10,-10); CombineRgn(hRgn1,hRgn1,hRgn2,RGN_XOR); for i := 0 to 9 do begin if Odd(i) then Canvas.Brush.Color := clRed else Canvas.Brush.Color := clGreen; OffsetRgn(hRgn1,20,20); PaintRgn(Canvas.Handle,hRgn1); end; DeleteObject(hRgn1); DeleteObject(hRgn2); end; procedure TForm1.Button2Click(Sender: TObject); var P:array[0..2] of TPoint; hRgn1:HRGN; i:integer; begin P[0] := Point(0,0); P[1] := Point(50,25); P[2] := Point(25,50); hRgn1 := CreatepolygonRgn(P,3,WINDING); for i := 0 to 9 do begin if Odd(i) then Canvas.Brush.Color := clLime else Canvas.Brush.Color := clBlue; OffsetRgn(hRgn1,20,20); InvertRgn(Canvas.Handle,hRgn1); end; DeleteObject(hRgn1); end; procedure TForm1.Button3Click(Sender: TObject); begin Refresh; end;
Button1 を押したときの結果を右図に示す。移動と描画を繰り返すとおもしろい効果を挙げるのが分かる。右図のような描画は、普通の描画関数を用いても描画することができるだろうが、リージョンを使うと簡単である。
Button1を押し、さらに Button2 を押すと右図のようになる。InvertRgn( ) の効果がわかるだろう。
Button3 を押すと Form1 が再描画されて図形が消える。Button2、Button1、Button2 の順で押すと右図のようになる。これもなかなかおもしろい。
前節までに調べたリージョンに関する機能を包み込むクラスを作成しよう。リージョンは、ペンやブラシ、フォントのようなハンドルで識別するオブジェクトであるので、コンポーネントパレットに置くほどのものではない。したがってここでは TObject から直接派生して作ることにしよう。
以下に TRegion を作成した APIRegion ユニットの全リストをしめす。
List 3
unit APIRegion; interface uses Windows,Graphics,Classes; type TPoints = array[0..20] of TPoint; TRegion = class(TObject) private FHandle:HRGN; procedure SetHandle(const Value: HRGN); protected function Combine(WithRgn:TRegion;Mode:integer):Boolean; function GetBox:TRect; public constructor Create;virtual; destructor Destroy;override; function CreateRect(Left,Top,Right,Bottom:integer):Boolean; function CreateElliptic(Left,Top,Right,Bottom:integer):Boolean; function CreatePolygon(Plgn:TPoints;NumPnts,FillMode:integer):Boolean; function CreateRoundRect(nLeftRect,nTopRect,nRightRect,nBottomRect, nWidthEllipse,nHeightEllipse: Integer):Boolean; function Offset(cx,cy:integer):Boolean; function CombineOR(Rgn:TRegion):Boolean; function CombineAND(Rgn:TRegion):Boolean; function CombineXOR(Rgn:TRegion):Boolean; function CombineDIFF(Rgn:TRegion):Boolean; function CopyFrom(Rgn:TRegion):Boolean; function Fill(Cvs:TCanvas):Boolean; function Paint(Cvs:TCanvas):Boolean; function IsPointIn(x,y:integer):Boolean; function IsRectIn(Rct:TRect):Boolean; function IsEqual(Rgn:TRegion):Boolean; function Frame(Cvs:TCanvas;wdx,wdy:integer):Boolean; function Invert(Cvs:TCanvas):Boolean; property Handle:HRGN read FHandle write SetHandle; property Box:TRect read GetBox; end; implementation constructor TRegion.Create; begin end; destructor TRegion.Destroy; begin if FHandle <> 0 then DeleteObject(FHandle); inherited; end; function TRegion.CreateRect(Left,Top,Right,Bottom:integer):Boolean; var RET:HRGN; begin RET := CreateRectRgn(Left,Top,Right,Bottom); if RET <> 0 then begin result := true; if FHandle <> 0 then DeleteObject(FHandle); FHandle := RET; end else result := false; end; function TRegion.CreateElliptic(Left,Top,Right,Bottom:integer):Boolean; var RET:HRGN; begin RET := CreateEllipticRgn(Left,Top,Right,Bottom); if RET <> 0 then begin result := true; if FHandle <> 0 then DeleteObject(FHandle); FHandle := RET; end else result := false; end; function TRegion.CreatePolygon(Plgn:TPoints;NumPnts,FillMode:integer):Boolean; var RET:HRGN; begin RET := CreatePolygonRgn(Plgn,NumPnts,FillMode); if RET <> 0 then begin result := true; if FHandle <> 0 then DeleteObject(FHandle); FHandle := RET; end else result := false; end; function TRegion.CreateRoundRect(nLeftRect, nTopRect, nRightRect, nBottomRect, nWidthEllipse, nHeightEllipse: Integer): Boolean; var RET:HRGN; begin RET := CreateRoundRectRgn(nLeftRect, nTopRect, nRightRect, nBottomRect, nWidthEllipse, nHeightEllipse); if RET <> 0 then begin result := true; if FHandle <> 0 then DeleteObject(FHandle); FHandle := RET; end else result := false; end; function TRegion.Offset(cx,cy:integer):Boolean; begin result := OffsetRgn(FHandle,cx,cy) <> ERROR; end; function TRegion.Combine(WithRgn:TRegion;Mode:integer):Boolean; begin result := CombineRgn(FHandle,FHandle,WithRgn.Handle,Mode) <> ERROR; end; function TRegion.CombineOR(Rgn:TRegion):Boolean; begin result := Combine(Rgn,RGN_OR); end; function TRegion.CombineAND(Rgn:TRegion):Boolean; begin result := Combine(Rgn,RGN_AND); end; function TRegion.CombineXOR(Rgn:TRegion):Boolean; begin result := Combine(Rgn,RGN_XOR); end; function TRegion.CombineDIFF(Rgn:TRegion):Boolean; begin result := Combine(Rgn,RGN_DIFF); end; function TRegion.CopyFrom(Rgn:TRegion):Boolean; begin if FHandle = 0 then CreateRect(0,0,1,1); result := CombineRgn(FHandle,Rgn.Handle,0,RGN_COPY) <> ERROR; end; function TRegion.Fill(Cvs:TCanvas):Boolean; begin result := Boolean(FillRgn(Cvs.Handle,FHandle,Cvs.Brush.Handle)); end; function TRegion.Paint(Cvs:TCanvas):Boolean; begin result := Boolean(PaintRgn(Cvs.Handle,FHandle)); end; function TRegion.Frame(Cvs:TCanvas;wdx,wdy:integer):Boolean; begin result := Boolean(FrameRgn(Cvs.Handle,FHandle,Cvs.Brush.Handle,wdx,wdy)); end; function TRegion.GetBox:TRect; begin if FHandle <> 0 then GetRgnBox(FHandle,result) else result := Rect(0,0,10,10); end; function TRegion.IsPointIn(x,y:integer):Boolean; begin if FHandle = 0 then result := false else result := Boolean(PtInRegion(FHandle,x,y)); end; function TRegion.IsRectIn(Rct:TRect):Boolean; begin if FHandle = 0 then result := false else result := Boolean(RectInRegion(FHandle,Rct)); end; function TRegion.IsEqual(Rgn:TRegion):Boolean; begin if FHandle = 0 then result := false else result := Boolean(EqualRgn(FHandle,Rgn.Handle)); end; function TRegion.Invert(Cvs:TCanvas):Boolean; begin result := Boolean(InvertRgn(Cvs.Handle,FHandle)); end; procedure TRegion.SetHandle(const Value: HRGN); begin if FHandle <> 0 then DeleteObject(FHandle); FHandle := Value; end; end.
ここで作成した TRegion クラスの実装コードについては、ほとんど前節の説明どおりであるので解説の必要はないだろう。ただ、特定の矩形領域、または点がリージョンの中にあるかどうかを返す IsRectIn( ) と IsPointIn( ) メソッドがラップしているAPIについてはまだ説明していなかった。
function RectInRegion(RGN: HRGN; const Rct: TRect): BOOL; stdcall;
この関数は Rct が Rgn と交差部分があるときは true を返す。
function PtInRegion(RGN: HRGN; x, y: Integer): BOOL; stdcall;
この関数は、点 (x,y) が RGN に含まれているときに true を返す。
それでは、このクラスのいくつかの機能をテストするプログラムを作成しよう。以下にそのリストを示す。
List 4
uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, APIRegion; type TForm1 = class(TForm) procedure FormCreate(Sender: TObject); procedure FormDestroy(Sender: TObject); procedure FormPaint(Sender: TObject); private { Private 宣言 } public Rgn1,Rgn2,Rgn3:TRegion; end; var Form1: TForm1; implementation {$R *.DFM} procedure TForm1.FormCreate(Sender: TObject); var P:TPoints; begin Rgn1 := TRegion.Create; Rgn2 := TRegion.Create; Rgn3 := TRegion.Create; Rgn1.CreateRect(10,20,40,50); P[0] := Point(20,40);P[1] := Point(70,10); P[2] := Point(80,50); Rgn2.CreatePolygon(P,3,Winding); end; procedure TForm1.FormDestroy(Sender: TObject); begin Rgn1.Free; Rgn2.Free; Rgn3.Free; end; procedure TForm1.FormPaint(Sender: TObject); begin with Canvas do begin Brush.Color := clRed; Rgn1.Fill(Canvas); Rgn3.CopyFrom(Rgn1); Rgn3.Offset(0,50); Rgn3.Frame(Canvas,1,1); Brush.Color := clBlue; Rgn2.Fill(Canvas); Rgn3.CopyFrom(Rgn2); Rgn3.Offset(0,50); Rgn3.Frame(Canvas,1,1); Brush.Color := clPurple; Rgn3.CopyFrom(Rgn1); Rgn3.CombineOR(Rgn2); Rgn3.Offset(100,0); Rgn3.Fill(Canvas); Rgn3.Offset(0,50); Rgn3.Frame(Canvas,1,1); Rgn3.CopyFrom(Rgn1); Rgn3.CombineAnd(Rgn2); Rgn3.Offset(200,0); Rgn3.Fill(Canvas); Rgn3.Offset(0,50); Rgn3.Frame(Canvas,1,1); Rgn3.CopyFrom(Rgn1); Rgn3.CombineXOR(Rgn2); Rgn3.Offset(300,0); Rgn3.Fill(Canvas); Rgn3.Offset(0,50); Rgn3.Frame(Canvas,1,1); Rgn3.CopyFrom(Rgn1); Rgn3.CombineDIFF(Rgn2); Rgn3.Offset(400,0); Rgn3.Fill(Canvas); Rgn3.Offset(0,50); Rgn3.Frame(Canvas,1,1); Brush.Color := Color; TextOut(20,110,'Original'); TextOut(140,110,'OR'); TextOut(220,110,'AND'); TextOut(340,110,'XOR'); TextOut(420,110,'Diff'); end; end;
実行時の様子を右図にしめす。最初に示したものと同じである。
描画関数をつかって描画するとき、描画範囲としてプログラマが定めた範囲だけに描画領域を限定することができる。この領域をクリッピング領域と言う。リージョンを使ってクリッピング領域の設定を行なうことができる。これはなかなか素晴らしい効果を挙げる。リージョンを使ってクリッピング領域をデバイスコンテキストに設定するには
function SelectClipRgn(DC: HDC; Rgn: HRGN): Integer; stdcall;
を使う。設定を解除するには、第二パラメータをゼロにしてこの関数を実行する。戻り値は CombineRgn( ) の場合と同じである。
右図は、ビットマップを TImage に描画するときにハート形のクリップリージョンを設定したときのものである。このプログラムのリストを以下に示す。
List 5
uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, ExtCtrls, APIRegion; type TForm1 = class(TForm) Image1: TImage; Button1: TButton; procedure FormCreate(Sender: TObject); procedure Button1Click(Sender: TObject); private { Private 宣言 } public { Public 宣言 } end; var Form1: TForm1; implementation {$R *.DFM} procedure TForm1.FormCreate(Sender: TObject); begin with Image1.Picture.Bitmap do begin Width := Image1.Width; Height := Image1.Height; Canvas.Brush.Color := clNavy; Canvas.FillRect(Image1.ClientRect); end; end; procedure TForm1.Button1Click(Sender: TObject); var BM:TBitmap; SourceRgn,TargetRgn:TRegion; Rgn1,Rgn2:TRegion; P:TPoints; begin Rgn1 := TRegion.Create; Rgn2 := TRegion.Create; Rgn1.CreateElliptic(0,0,120,120); Rgn2.CopyFrom(Rgn1); Rgn2.Offset(90,0); Rgn1.CombineOR(Rgn2); P[0] := Point(10,80); P[1] := Point(200,80); P[2] := Point(105,230); Rgn2.CreatePolygon(P,3,WINDING); Rgn2.Offset(0,12); Rgn1.CombineOR(Rgn2); SourceRgn := TRegion.Create; TargetRgn := TRegion.Create; SourceRgn.CopyFrom(Rgn1); SourceRgn.Offset(80,70); TargetRgn.CopyFrom(SourceRgn); TargetRgn.Offset(-50,-50); BM := TBitmap.Create; try BM.LoadFromFile('e:\test.bmp');// true-color SelectClipRgn(Image1.Picture.Bitmap.Canvas.Handle, TargetRgn.Handle); Image1.Picture.Bitmap.Canvas.CopyRect(TargetRgn.Box, BM.Canvas,SourceRgn.Box); SelectClipRgn(Image1.Picture.Bitmap.Canvas.Handle,0); finally BM.Free; Rgn1.Free; Rgn2.Free; TargetRgn.Free; SourceRgn.Free; end; end;
Button1 を押すと Rgn1 と Rgn2 を使ってハート形のリージョンを Rgn1 に作成する。次に Bitmap を使って画像をロードするまえに、画像のどの部分を表示するかを指定するために Rgn1 を参照して SourceRgn1 を作る。さらに、Image1 のどの部分に表示したいかを TargetRgn2 で指定する。ビットマップオブジェクトを使って画像ファイルをロードし、クリッピング領域を Image1 の DC について設定した後、CopyRect( ) で転送描画する。そしてクリッピング領域の設定を解除する。CopyRect( ) は、その名前の通り矩形領域を転送して描画するので、描画元と描画先のリージョンの Box プロパティーでおのおのの矩形領域を指定している。
以上は、Canvas 間で画像を転送するときに転送先にクリッピング領域を設定する例であった。もちろん、TCanvas の描画メソッドで描画するときにもクリッピング領域を設定できる。本章の冒頭で示した図形を右に再掲する。そしてこの時のコードを以下に示す。ここでは TRegion クラスを使っていない。
List 6
procedure TForm1.Button1Click(Sender: TObject); var RGN1,RGN2:HRGN; i:integer; begin RGN1 := CreateEllipticRgn(10,10,160,160); RGN2 := CreateEllipticRgn(10,10,120,130); OffsetRgn(RGN2,50,0); CombineRgn(RGN1,RGN1,RGN2,RGN_DIFF); with Canvas do begin SelectClipRgn(Handle,RGN1); Pen.Color := clRed; i := 10; repeat MoveTo(i,10);LineTo(i,160); MoveTo(10,i);LineTo(160,i); Inc(i,5); until (i>160); SelectClipRgn(Handle,0); Brush.Color := clBlue; FrameRgn(Handle,RGN1,Brush.Handle,2,2); Brush.Color := clBtnFace; end; DeleteObject(RGN1); DeleteObject(RGN2); end;
クリッピングリージョンを設定するときに使ったリージョンの輪郭を FrameRgn( ) で描くためには、そのクリッピング領域に描かれないのでここではクリッピングを解除してから描画している。
パスは、輪郭または塗りつぶされた一つまたは複数の図形および曲線・直線の集まりのことである。パスは、リージョンと違ってハンドルで識別されるオブジェクトではなく、描画関数を実行することによりデバイスコンテキストに一つだけ作成されるのである。パスを使ってクリッピング領域を設定できるし、また、リージョンに変換することもできる。右図の左側は、パスを使って描いた図形の例である。右側はこれをリージョンに変換し同様に描いたものである。パスとリージョンは特に線が交差するところと折り返すところに差が現れているのがわかるだろう。パスはその名のとおり、図形をストロークの集まりとして描く。これに対し、リージョンは、多角形の集まり、つまり領域として描くわけである。このプログラムのリストを以下に示す。
List 7
unit Main; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs; type TForm1 = class(TForm) procedure FormCreate(Sender: TObject); procedure FormDestroy(Sender: TObject); procedure FormPaint(Sender: TObject); private { Private 宣言 } public RGN:HRGN; end; var Form1: TForm1; implementation {$R *.DFM} procedure TForm1.FormCreate(Sender: TObject); begin with Canvas do begin BeginPath(Handle); MoveTo(10,10); LineTo(200,50); LineTo(150,70); LineTo(70,250); LineTo(30,180); LineTo(220,150); CloseFigure(Handle); MoveTo(90,110); LineTo(260,110); LineTo(260,210); LineTo(90,210); CloseFigure(Handle); EndPath(Handle); RGN := PathToRegion(Handle); OffsetRgn(RGN,300,0); end; end; procedure TForm1.FormDestroy(Sender: TObject); begin DeleteObject(RGN); end; procedure TForm1.FormPaint(Sender: TObject); begin with Canvas do begin BeginPath(Handle); MoveTo(10,10); LineTo(200,50); LineTo(150,70); LineTo(70,250); LineTo(30,180); LineTo(220,150); CloseFigure(Handle); MoveTo(90,110); LineTo(260,110); LineTo(260,210); LineTo(90,210); CloseFigure(Handle); EndPath(Handle); Brush.Color := RGB(50,230,100); Pen.Color := clBlue; Pen.Width := 8; //SetPolyFillMode(Handle,WINDING); StrokeAndFillPath(Handle); PaintRgn(Handle,RGN); Brush.Color := clBlue; FrameRgn(Handle,RGN,Brush.Handle,8,8); end; end; end.
前節までに取り扱ったリージョンは、デバイスコンテキスト(DC)とは直接には関係なく、論理座標上の単純な図形の演算により作成できた。そして、そのハンドルで管理することができた。したがって、このプログラムでは FormCreate( ) で一度だけ作成すれば十分である。
procedure TForm1.FormCreate(Sender: TObject); begin with Canvas do begin BeginPath(Handle); MoveTo(10,10); LineTo(200,50); LineTo(150,70); LineTo(70,250); LineTo(30,180); LineTo(220,150); CloseFigure(Handle); MoveTo(90,110); LineTo(260,110); LineTo(260,210); LineTo(90,210); CloseFigure(Handle); EndPath(Handle); RGN := PathToRegion(Handle); OffsetRgn(RGN,300,0); end; end;
ここでは、パスを作成してそれをリージョンに変換している。パスを作成するには、BeginPath( ) と EndPath( ) の間に、何回かの描画を行なえばパスとして記録される。BeginPath( ) と EndPath( ) で囲まれたコード領域を、パスブラケット(パス括弧)、という。
function BeginPath(DC: HDC): BOOL; stdcall; function EndPath(DC: HDC): BOOL; stdcall;
どちらも、デバイスコンテキストハンドルを必要とする。パスはデバイスコンテキストのなかに作られ、そのなかでしか生きられない。ブラケットの中の描画関数はパスに変換され、DCには描かれない。ここでは、LineTo( ) を繰り返して適当なポリゴンとそれと交わる矩形を描画している。LineTo( ) は、それ自体独立した描画関数であるが、パスブラケットのなかで、現在のペン位置から始めると、折れ線のように前の線とつながっていると認識される。途中で、MoveTo( ) を入れると、この認識は途絶える。そして、ここではリージョンに変換され、できたリージョンを平行移動する。リージョンに変換する PathToRegion( ) を実行することにより、パスは破棄される。同様なパスは、FormPaint( ) の中でも作成され、StrokeAndFillPath( ) で描画されて破棄されている。したがって、FormPaint( ) イベントが起こるたびに作成しなければならない。これに対して、リージョンは一度だけ作成すると何度でも描画できる。
パスを作成するにはパスブラケットのなかで以下の描画関数を実行する。
AngleArc Arc ArcTo Chor CloseFigure * Ellipse ExtTextOut * LineTo * MoveToEx * Pie PolyBezier * PolyBezierTo * PolyDraw Polygon * Polyline * PolylineTo * PolyPolygon * PolyPolyline * Rectangle RoundRect TextOut *
これらの描画関数は、WindowsNT/2000 ではパスの作成のためにすべて使うことができる。ところが Windows95/98/Me では * 印のついた関数しか使えない。Rectangle は Polygon で代用できるとしても、Ellipse が使えないのはとても残念である。現状では、すべての Win32 OSに互換性があるプログラムでパスを有効に使うのは限界がある。
上記の関数のなかでパス特有なものは CloseFigure( ) である。
function CloseFigure(DC: HDC): BOOL; stdcall;
この関数はそれまでに折れ線と認識できる一連の描画の最初の点と現在のペン位置を結んで閉じた図形にする。これ以後の描画は、あらたな図形または線の始まりと認識される。
パスを描画したり変換したり削除するには以下の関数を使う。
function AbortPath(DC: HDC): BOOL; stdcall;// パスを破棄する function FillPath(DC: HDC): BOOL; stdcall; // カレントブラシで塗りつぶす function PathToRegion(DC: HDC): HRGN; stdcall; // リージョンに変換する function StrokeAndFillPath(DC: HDC): BOOL; stdcall; // カレントペン・ブラシで塗りつぶし・輪郭を描く function StrokePath(DC: HDC): BOOL; stdcall; // カレントペンで輪郭を描く
これらの関数は、その唯一のパラメータとしてデバイスコンテキストのハンドル(TCanvas の Handle プロパティーに相当する)をとり、実行後にパスを破棄する。
パスを使ってクリッピング領域を設定できる。
function SelectClipPath(DC: HDC; Mode: Integer): BOOL; stdcall;
Mode パラメータには、カレントのクリッピング領域との演算を指定する。これは、CombineRgn( ) のときのパラメータと同じ物を使う。
const RGN_AND = 1; RGN_OR = 2; RGN_XOR = 3; RGN_DIFF = 4; RGN_COPY = 5;
あたらしくこのパスで設定したいときは RGN_COPY を使う。
さて、上で挙げたパスブラケットのなかで実行できる描画関数のなかに TextOut( ) があることは、すこし意外である。TrueType フォントを使ったテキストの描画は、実は LineTo( ) や PolyBezierTo( ) などを実行するのと等価なのである。アウトラインフォントというのは、実際にフォントデータに文字を描画するためのアウトラインデータを持っていて、それを展開しているのである。これを試してみよう。右図のように Button を三つ配置する。以下にリストを示す。
List 8
unit Main; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, APIRegion, StdCtrls, ExtCtrls; type TForm1 = class(TForm) Button1: TButton; Button2: TButton; Button3: TButton; procedure FormCreate(Sender: TObject); procedure FormDestroy(Sender: TObject); procedure Button1Click(Sender: TObject); procedure Button2Click(Sender: TObject); procedure Button3Click(Sender: TObject); private { Private 宣言 } public RGN:TRegion; end; var Form1: TForm1; implementation {$R *.DFM} uses Math; procedure TForm1.FormCreate(Sender: TObject); begin RGN := TRegion.Create; with Canvas do begin Font.Name := 'Symbol'; Font.Size := 72; SetBkMode(Handle,TRANSPARENT); BeginPath(Handle); TextOut(0,0,Char(167)+Char(168)+Char(169)+Char(170)); EndPath(Handle); RGN.Handle := PathToRegion(Handle); RGN.Offset(-RGN.Box.Left+30,-RGN.Box.Top+30); end; end; procedure TForm1.FormDestroy(Sender: TObject); begin RGN.Free; end; procedure TForm1.Button1Click(Sender: TObject); begin Refresh; Canvas.Brush.Color := clBlue; RGN.Frame(Canvas,2,2); end; procedure TForm1.Button2Click(Sender: TObject); var Rgn1:TRegion; i:integer; begin Rgn1 := TRegion.Create; Rgn1.CopyFrom(RGN); Refresh; for i := 0 to 10 do begin Canvas.Brush.Color := RGB(25*i,25*i,25*i) or $02000000; Rgn1.Fill(Canvas); Rgn1.Offset(-1,-1); end; Rgn1.Free; end; procedure TForm1.Button3Click(Sender: TObject); var i:integer; c:integer; begin Refresh; SelectClipRgn(Canvas.Handle,RGN.Handle); for i := 0 to Width do begin c := Round(((Sin(DegToRad(i*10))+1)*80)); Canvas.Pen.Color := RGB(30,40+c,90+c) or $02000000; Canvas.MoveTo(i,0); Canvas.LineTo(i,Height); end; SelectClipRgn(Canvas.Handle,0); end; end.
実行して、各ボタンを押したときの描画の結果を右に示す。
パスは FormCreate( ) で作成されている。フォントは、'Symbol' を選び、適当な大きさでそのなかの四つのトランプマークを出力している。そしてできたパスをリージョンに変換している。
Button1 を押すと青色のブラシでリージョンのアウトラインを描く。Button2 を押すと、リージョンを複製し、そのリージョンを左上に動かしながらブラシの明度を変えてリージョンを塗りつぶしていく。Button3 では、リージョンを使ってクリッピング領域を設定し、色を周期的に変化させながら縦線を描いた。
リージョンを使って、ウィンドウそのものの表示をクリッピングすることができる。これにより、矩形領域以外の形のウィンドウを表示することができる。
function SetWindowRgn(hWnd: HWND; hRgn: HRGN; bRedraw: BOOL): Integer; stdcall;
hWnd には、クリッピングするウィンドウのハンドルを、hRgn にはクリッピング領域を指定するリージョンのハンドルを指定する。このときのリージョンの座標は、ウィンドウのクライアント領域の座標ではなく、ウィンドウ全体の左上隅を原点としていることに注意する。このリージョンはOSが管理するので、ウィンドウが生きている間はこのリージョンを操作してはいけない。bRedraw は、true にするとすぐに再描画する。
さっそくこれを試してみよう。パスを使ってリージョンをつくり、それでクリッピングすることにしよう。
List 9
uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, APIRegion; type TForm1 = class(TForm) procedure FormCreate(Sender: TObject); procedure FormDestroy(Sender: TObject); procedure FormDblClick(Sender: TObject); procedure FormPaint(Sender: TObject); private procedure WMNCHitTest(var Msg:TWMNCHitTest);message WM_NCHITTEST; public RGN:TRegion; end; var Form1: TForm1; implementation {$R *.DFM} procedure TForm1.FormCreate(Sender: TObject); begin RGN := TRegion.Create; with Canvas do begin Font.Name := 'Times New Roman'; Font.Style := [fsBold,fsItalic]; Font.Size := 80; SetBkMode(Handle,TRANSPARENT); BeginPath(Handle); TextOut(0,0,'Halbow'); EndPath(Handle); RGN.Handle := PathToRegion(Handle); RGN.Offset(-RGN.Box.Left+30,-RGN.Box.Top+30); end; SetWindowRgn(Handle,RGN.Handle,false); Randomize; end; procedure TForm1.FormDestroy(Sender: TObject); begin RGN.Free; end; procedure TForm1.FormDblClick(Sender: TObject); begin Close; end; procedure TForm1.WMNCHitTest(var Msg: TWMNCHitTest); begin if (GetAsyncKeyState(VK_SHIFT) and $8000) <> 0 then inherited else Msg.Result := HTCAPTION; end; procedure TForm1.FormPaint(Sender: TObject); var i:integer; begin with Canvas do for i := 0 to Height do begin Pen.Color := $AAAAAA+Random($555555); MoveTo(0,i);LineTo(Width,i); end; end;
実行結果を右図にしめす。ウィンドウの位置は、マウスによりドラグして変更できる。シフトキーを押しながらダブルクリックするとウィンドウを閉じる。
本章では、Win32 の描画機能のうち VCL の TCanvas では直接扱えないリージョンとパスについて調べた。リージョンは、ペンやブラシ、フォントなどと同じようにハンドルで管理されるGDIオブジェクトであると見なすことができる。基本的なリージョンは簡単に作成でき、それらを組み合わせることにより一見複雑な図形も簡単に作成できる。ペンやブラシより属性が少ないので、リージョンそのものの使用方法はむしろペンやブラシより簡単である。ここでは、リージョンの機能とハンドルを管理するクラスを作成した。
パスはリージョンと違ってデバイスコンテキストのなかに動的に作成されるストロークを記録した擬似的なオブジェクトであると見なせるだろう。Win9X 系ではパスを作成するときに使用できる描画関数が制限されているので、現状ではあまり役に立たない。しかし、アウトラインフォントを用いた文字列の描画により。そのアウトラインをパスとして記録できる。パスは一回使うと消えてなくなるので、閉じた図形として記録するにはリージョンに変換しておくのが便利だろう。
パスとリージョンはクリッピングに使用できる。ここでは、描画関数の実行に際してのデバイスコンテキストにクリッピングを設定する例と、ウィンドウを矩形以外の外見にするためにクリッピング領域を設定する例について示した。