リージョンとパス


星を描くでは,リージョンを使って中身が塗りつぶされた星を描く方法を紹介した. リージョンを使えばウィンドウの描画領域をクリッピングする(描画できない領域を作る)ことができ, いろいろと面白いことができる. 拙作のアイ(下図)もリージョンを使って目玉の形をしたウィンドウを 作っている.

リージョンを作成するAPIとしては,CreateEllipticRgn(楕円形),CreatePolygonRgn(多角形),CreateRectRgn(矩形) およびCreateRoundRectRgn(角の丸い矩形)がある. また,これらを使って作成したリージョンをCombineRgnで結合することもできる.


穴があいたウィンドウ

穴があいていて,そこから背景が覗けるようなウィンドウを作成してみる. まず,CreateRectRgnでウィンドウのサイズと同じ矩形リージョンを作成する. 次に,CreateEllipticRgnでそれよりも小さな円形リージョンを作成し, 両者をCombineRgnで結合する.CombineRgnのプロトタイプは以下のようになっている.

function CombineRgn(p1, p2, p3: HRGN; p4: Integer): Integer; stdcall;

p1は結合後のリージョンのハンドルであり,p2およびp3は結合する二つのリージョンのハンドルである. p1とp2あるいはp3は同一であっても良い. p1とp2に矩形リージョンのハンドルを,p3に円形リージョンのハンドルをそれぞれ指定する. p4は二つのリージョンを結合する際の方法であるが, ここではRGN_XORを指定するのがミソである.

p4にRGN_XORを指定すると,二つのリージョンを結合した場合に重なり合う部分が除外される. 今の場合は,結合後のリージョンは,最初に作成した矩形リージョンから後で作成した円形リージョンを除外した ものになる. これをSetWindowRgnでウィンドウに選択すれば,円形リージョンの内部はウィンドウ再描画の対象外となり, 背景がそのまま残る. 一旦リージョンをウィンドウに選択すれば,そのリージョンは破棄して構わない.

具体的なコードは以下のようになる.

procedure TForm1.FormCreate(Sender: TObject);
var
  RGN1, RGN2: HRGN;
begin
   RGN1 := CreateRectRgn( 0, 0, Width, Height );
   RGN2 := CreateEllipticRgnIndirect( Rect( 30, 40, 150, 160 ) );
   CombineRgn( RGN1, RGN1, RGN2, RGN_XOR );
   SetWindowRgn( Handle, RGN1, True );
   DeleteObject( RGN2 );
   DeleteObject( RGN1 );
end;

実行結果は以下の通り. 穴の中をクリックするとその下にあるウィンドウがフォアグランドウィンドウ(一番手前のウィンドウ)になる. つまり,単に背景が見えているだけでなく,穴の中はウィンドウが無いことになっているのだ.


矩形以外のウィンドウ

今度は複数のリージョンを結合してちょっと複雑なリージョンを作ってみる. まず,適当なビットマップをFormのOnPaintイベントでCanvasに描画するようなコードを書く.

procedure TForm1.FormCreate(Sender: TObject);
begin
   FBitmap := TBitmap.Create;
   FBitmap.LoadFromFile( 'hina.bmp' );
   ClientWidth := FBitmap.Width;
   ClientHeight := FBitmap.Height;
end;

procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
begin
   FBitmap.Free;
end;

procedure TForm1.FormResize(Sender: TObject);
var
  TheRect: TRect;
begin
   TheRect := ClientRect;
   InvalidateRect( Handle, @TheRect, False );
end;

procedure TForm1.FormPaint(Sender: TObject);
begin
   Canvas.StretchDraw( Rect( 0, 0, ClientWidth, ClientHeight ), FBitmap );
end;

これを実行すると,

のようになる. ただしこれでは面白くも何とも無いので,顔の辺りだけを描画領域に設定する. OnPaintイベントを以下のように変更する.

procedure TForm1.FormPaint(Sender: TObject);
const Theta = 4e-1 * Pi;
var
  R, X, Y, Index: Integer;
  Org: TPoint;
  Angle: Double;
  NewRgn, EntireRgn: HRGN; 
begin
   Org := Point( 250, 170 );
   R := 70;
   for Index := 0 to 4 do
   begin
      Angle := Theta * Index;
      X := Round( Org.x + R * Sin( Angle ) );
      Y := Round( Org.y - R * Cos( Angle ) );
      NewRgn := CreateEllipticRgn( X - R, Y - R, X + R, Y + R );
      if Index <> 0 then
      begin
         CombineRgn( EntireRgn, EntireRgn, NewRgn, RGN_OR );
         DeleteObject( NewRgn );
      end
      else EntireRgn := NewRgn;
   end;
   SetWindowRgn( Handle, EntireRgn, True ); 
   Canvas.StretchDraw( Rect( 0, 0, ClientWidth, ClientHeight ), FBitmap );
   DeleteObject( EntireRgn );
end;

このコードでは,正五角形のそれぞれの頂点に円を描き,それらをすべて結合したリージョンを作成している. このリージョンをウィンドウに選択すれば,リージョンの外側にはすべてクリッピングされ,何も描画されない. 実行結果は以下のようになる.

ただし,タイトルバーまで消えてしまうのでドラッグして移動することができない. そこで,FormのMouseDownイベントに以下のように書いておく.

procedure TForm1.FormMouseDown(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
begin
   if ssLeft in Shift then
   begin
      ReleaseCapture;
      SendMessage( Handle, WM_SYSCOMMAND, SC_MOVE or 2, MakeLong( X, Y ) );
   end;
end;

こうすれば,フォームをドラッグしてウィンドウを移動することができる.


パスを使う

リージョンは領域を表すのに対し,パスは軌跡を表す. BeginPathEndPathで挟まれたGDI関数の呼び出しがそのDCのパスとして記録されるので. 相当複雑な図形をパスとして記録することも可能である.

しかしながら,SDKのヘルプを見ると, Windows NT/2000ではパスの記録に使えるGDI関数の制限はほとんど無いものの, Windows 9Xでは使えるGDI関数がかなり制限されている.

Windows NT/2000 : AngleArc,LineTo,Polyline,Arc,MoveToEx,PolylineTo, ArcTo,Pie,PolyPolygon,Chord,PolyBezier,PolyPolyline,CloseFigure,PolyBezierTo, Rectangle,Ellipse,PolyDraw,RoundRect,ExtTextOut,Polygon,TextOut

Windows 9X : CloseFigure,ExtTextOut,LineTo,MoveToEx,PolyBezier,PolyBezierTo, Polygon,Polyline,PolylineTo,PolyPolygon,PolyPolyline,TextOut

このように,Windows 9XではEllipseやRectangleが使えないので,かなり不便ではある. 矩形や円はLineToとMoveToを組み合わせて描くしかない.

ただし,パスの記録にはTextOutが使える(これはNT/2000と9Xの両方で使える). これを利用すれば,中抜き文字や縁取り文字を書くことができる. 例えば,フォームのOnPaintイベントに次のように書く.

procedure TMainForm.FormPaint(Sender: TObject);
var
  S: String;
begin
   S := 'HINA!';
   with Canvas.Font do
   begin
      Name := 'Arial Black';
      Charset := DEFAULT_CHARSET;
      Style := Style + [ fsBold ];
      Size := 96;
   end;
   SetBkMode( Canvas.Handle, TRANSPARENT );

   BeginPath( Canvas.Handle );
   Canvas.TextOut( ( ClientWidth - Canvas.TextWidth( S ) ) div 2,
                   ( ClientHeight - Canvas.TextHeight( S ) ) div 2, S );
   EndPath( Canvas.Handle );

   Canvas.Pen.Width := 3;
   StrokePath( Canvas.Handle );
end;

BeginPathとEndPathで挟まれた部分の描画操作は(上のコードではTextOutメソッドを使った テキスト描画)はフォームに反映されない. StrokePathはDCに選択されたパスの輪郭を現在のペンで描くAPIである. 実行結果は次のようになる.

留意事項としては,

上のコードのStrokePathをStrokeAndFillPathに変えると, 以下のような結果となる(Canvas.Brush.ColorをclYellowにしている).

StrokeAndFillPathは輪郭をペンで,内部をブラシで塗りつぶすAPIである.

さらに,パスはリージョンに変換できるので,パスを使えば複雑なリージョンを作成することができる.

(以下製作中)


お問い合わせはメールにて: akasaka@klc.ac.jp

戻る
SEO [PR] 爆速!無料ブログ 無料ホームページ開設 無料ライブ放送