星を描く


Canvasに星型の図形を描く場合,予め星の元絵となるビットマップリソースを用意しておき, それをDrawメソッドで描くのが普通だろう. ただ,元絵の拡大縮小によって画質が悪くなるうえ,大きな元絵を持たせると 実行バイナリが大きくなる.

今回,あるところから依頼されたアプリケーションでたくさんの星を描く必要性に迫られたため, 元絵を使わずにコードで星を描く方法を検討した. 成果をまとめておく. 下図は,以下で示すDrawStar関数を使ってフォームの任意位置に星を100個描いたところである.

図は円に内接する星型を示している. CanvasのPolygonメソッドに,頂点AからJまでの点の座標を与えれば 星型を描くのは簡単である. 図中のA,B,C,DおよびEの座標は円に内接する正五角形の頂点であるから, 中心Oの座標と円の半径が与えられれば決定するが, F,G,H,IおよびJの座標の決定はかなり面倒である. ちなみに,Oの座標を(x0,y0)とし円の半径をRとすれば,A,B,C,DおよびEの座標は以下のようになる(yは下向きが正).

A: x = x0 , y = y0 - R
B: x = x0 + R sin θ, y = y0 - R cos θ
C: x = x0 + R sin( 2θ ), y = y0 - R cos( 2θ )
D: x = x0 - R sin( 2θ ), y = y0 - R cos( 2θ )
E: x = x0 - R sin θ, y = y0 - R cos θ

ここでθ=72*π/180である.

実は,PolygonメソッドにA,B,C,DおよびEの各座標をA,C,E,B,Dの順で与えるだけで 一筆描きの要領で星型を描いてくれる. また,そのときのBrushの色で内部を塗りつぶしてくれる. 例えば,星を描きたい領域がARectならば以下のようにすればよい.

var
  Theta: Double;
  Org: TPoint;
  Points: array[ 0..4 ] of TPoint;

(中略)

   Theta := Pi * 72 / 180;
   R := Round( ( ARect.Right - ARect.Left ) / 2 );
   Org.x := R;
   Org.y := R;
   Points[ 0 ] := Point( Org.X, Org.Y - R );
   Points[ 1 ] := Point( Org.X + Round( R * Sin( 2 * Theta ) ),
                         Org.Y - Round( R * Cos( 2 * Theta ) ) );
   Points[ 2 ] := Point( Org.X - Round( R * Sin( Theta ) ),
                         Org.Y - Round( R * Cos( Theta ) ) );
   Points[ 3 ] := Point( Org.X + Round( R * Sin( Theta ) ),
                         Org.Y - Round( R * Cos( Theta ) ) );
   Points[ 4 ] := Point( Org.X - Round( R * Sin( 2 * Theta ) ),
                         Org.Y - Round( R * Cos( 2 * Theta ) ) );
   Canvas.Brush.Color := clRed;
   Canvas.Pen.Color := clRed;
   Canvas.Polygon( Points );

これでうまくいけば実に簡単なのであるが,残念ながら,そううまくはいかない. 上のコードを実行した結果は次のようになる.

星の内部が塗りつぶされない. APIのドキュメント等を調べてみると, 内部が塗りつぶされないのは,Canvasが対象としている デバイスコンテキスト(DC)の塗りつぶしモードALTERNATEになっているためということがわかった. 内部が全部塗りつぶされるようにするには,塗りつぶしモードをWINDINGに変更すればよい. 塗りつぶしモードの変更にはSetPolyFillModeを用いるが, このAPIのヘルプには,

In general, the modes differ only in cases where a complex, overlapping polygon must be filled (for example, a five-sided polygon that forms a five-pointed star with a pentagon in the center). In such cases, ALTERNATE mode fills every other enclosed region within the polygon (that is, the points of the star), but WINDING mode fills all regions (that is, the points and the pentagon).

と書かれている. まさに今問題としていることである. 上のコードでPolygonメソッドを呼び出す前に,

SetPolyFillMode( Canvas.Handle, WINDING );

と書けば,内部もすべて塗りつぶされた星を描くことができる.

余談になるが,VCLのソースを見ると,CanvasのPolygonメソッドやPolylineメソッドは受け取ったTPoint型の配列を APIのPolygonやPolylineに渡しているだけである. 例えば,Polygonのプロトタイプは

function Polygon( DC: HDC; var Points; Count: Integer ): BOOL; stdcall;

のように宣言されており,各引数はそれぞれDCのハンドル,TPoint型配列およびその要素数である. 呼び出し側でTPoint型配列を渡す際には,

Polygon( Canvas.Handle, Points, 5 ):

のように2番目には単に配列名を書けばよいと思うのだが,VCLのソースではこれが,

type
  PPoints = ^TPoints;
  TPoints = array[0..0] of TPoint;

Polygon( Canvas.Handle, PPoints( @Points )^, 5 ):

のように,配列のアドレスをポインタ型にキャストし,それを逆参照している. なぜわざわざそういうことをしているのかは不明である.

さて,以上で一応の目的は達せられたわけであるが,今度は星の輪郭を描くことを考えてみる. 単純に,上のコードで

Canvas.Brush.Color := clRed;
Canvas.Pen.Color := clBlack;

などとすれば,輪郭を描けそうであるが,これもそう簡単ではない.

のように内部にまで線が引かれてしまうのである.

いろいろと試行錯誤してみたが,輪郭を描くにはリージョンを使うしかなさそうだ. リージョンとはDCの描画領域を制限する(クリッピングする)形状のことである. 例えば,円形のリージョンを作成しDCに選択すると,FillRectなどでDC全体を 塗りつぶしても円形リージョンの中だけが塗りつぶされる. ここに置いてある「アイ」もリージョンを使っている.

Delphiではリージョンがサポートされていないので, リージョンの作成やDCへの選択はAPIを直接使う必要がある. 以下の例はフォームのOnPaintイベントの中で円形のリージョンを作成してフォームのDCに選択し, ビットマップを描画するものである.

procedure TForm1.FormPaint(Sender: TObject);
var
  RGN: HRGN;
  Bmp: TBitmap;
begin
   Bmp := TBitmap.Create;
   Bmp.LoadFromFile( 'otoha3.bmp' );

   RGN := CreateEllipticRgnIndirect( Rect( 30, 20, 150, 140 ) );
   SelectClipRgn( Canvas.Handle, RGN );
   Canvas.Draw( 0, 0, Bmp );
   SelectClipRgn( Canvas.Handle, HRGN( nil ) );
   DeleteObject( RGN );

   Bmp.Free;
end;

円形のリージョンを作成するにはCreateEllipticRgnIndirectを用いる(CreateEllipticRgnでも良い). リージョンをDCに選択するにはSelectClipRgnを用いる. また,選択を解除するにはやはりSelectClipRgnにnilのリージョンハンドルを渡す. 使い終わったリージョンはDeleteObjectで破棄しなければならない. 実行結果は次のようになる.

星の輪郭を描く具体的な手順は以下のようになる.

1. CreatePolygonRgnで星型のリージョンを作成する
2. PaintRgnでリージョン内部を塗りつぶす
3. FrameRgnでリージョンの輪郭を描く
4. リージョンを解放する.

CreatePolygonRgnのプロトタイプは以下のようになっている.

function CreatePolygonRgn( const Points; Count, FillMode: Integer ): HRGN; stdcall;

Pointsは多角形の頂点を表すTPoint型配列へのポインタであり,Countは要素数である. また,FillModeは塗りつぶしモードであり,今の場合WINDINGを指定すればよい.

リージョンの内部を塗りつぶしたり輪郭を描く場合は,いちいちSelectClipRgnでDCに選択/解除しなくても, PaintRgnやFrameRgnにDCのハンドルとリージョンのハンドルを両方渡せばよいようだ. おそらくこれらのAPIの中で選択/解除を行っているのだろう.

星型を描画する汎用的な手続きDrawStarを作ってみた. 引数として,描画対象となるCanvas,描画領域のRect,星の内部色,背景色,輪郭色 および背景を透明にするか否かのフラグを受け取る. 背景を透明にする場合は,背景色として指定された色が透過色となる. Canvasに直接描画するのではなく, テンポラリなメモリビットマップを作成し,これに描画したのち対象となるCanvasに転送する, いわゆるダブルバッファリングを使っている.

procedure DrawStar( Canvas: TCanvas; ARect: TRect;
                    ForeColor, BackColor, FrameColor: TColor;
                    Transparent: Boolean );
var
  Theta: Double;
  Org: TPoint;
  R, W, H: Integer;
  RGN: HRGN;
  Bmp: TBitmap;
  Points: array[ 0..4 ] of TPoint;
begin
   Theta := Pi * 72 / 180;
   R := Round( ( ARect.Right - ARect.Left ) / 2 );
   Org.x := R;
   Org.y := R;
   Points[ 0 ] := Point( Org.X, Org.Y - R );
   Points[ 1 ] := Point( Org.X + Round( R * Sin( 2 * Theta ) ),
                         Org.Y - Round( R * Cos( 2 * Theta ) ) );
   Points[ 2 ] := Point( Org.X - Round( R * Sin( Theta ) ),
                         Org.Y - Round( R * Cos( Theta ) ) );
   Points[ 3 ] := Point( Org.X + Round( R * Sin( Theta ) ),
                         Org.Y - Round( R * Cos( Theta ) ) );
   Points[ 4 ] := Point( Org.X - Round( R * Sin( 2 * Theta ) ),
                         Org.Y - Round( R * Cos( 2 * Theta ) ) );

   W := ARect.Right - ARect.Left;
   H := ARect.Bottom - ARect.Top;
   Bmp := TBitmap.Create;
   try
     // Canvasと互換性のあるビットマップを作成する
     Bmp.Handle := CreateCompatibleBitmap( Canvas.Handle, W, H );
     Bmp.Transparent := Transparent;
     Bmp.TransparentColor := BackColor;
     Bmp.Canvas.Brush.Color := BackColor;
     Bmp.Canvas.FillRect( Rect( 0, 0, W, H ) );

     // 星型のリージョンを作成し,塗りつぶす
     RGN := CreatePolygonRgn( Points, 5, WINDING );
     Bmp.Canvas.Brush.Color := ForeColor;
     PaintRgn( Bmp.Canvas.Handle, RGN );

     // リージョンの輪郭を描く
     Bmp.Canvas.Brush.Color := FrameColor;
     FrameRgn( Bmp.Canvas.Handle, RGN, Bmp.Canvas.Brush.Handle, 2, 2 );

     // リージョンを破棄し,ビットマップをCanvasに描画する
     DeleteObject( RGN );
     Canvas.Draw( ARect.Left, ARect.Top, Bmp );
   finally
     Bmp.Free;
   end; 
end;

以下は,フォームのOnPaintイベントに

DrawStar( Canvas, Rect( 0, 0, 200, 200 ), clRed, clOlive, clBlack, True );

と書いた場合の結果である.

プログラミングをよく知らない人からみれば,星を描くなんて簡単なことのように思われるかもしれないが, コードで書くのは案外難しいのである.


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

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