Share via


第 15 章 直線、曲線、領域塗りつぶし

Euclidによれば、「1本の線は、横幅を持たない長さである」と定義されます (1)。この定義では、「横幅を持たない」という形容が私たちの関心を引き付けます。Euclidは古代ギリシャの数学者ですから、彼の定義には当時の数学者の発想が表現されています。つまり、彼らは、物事を高度に抽象化する技術を既に身に付けていたわけです。同時に、古代ギリシャ人たちは、コンピュータグラフィックスについて何も知らなかったこともはっきり示しています。コンピュータグラフィックスの素養を身に付けていれば、ピクセルが横幅を持っていることを理解できたでしょう。ピクセルは、コンピュータグラフィックスでは重要な意味を持ち、使い方によっては困った問題を引き起こします。たとえば、1ピクセルずれただけでエラーとなります。本章では、このような微妙な問題を取り上げます。

(1) Sir Thomas L. Heath編著『The Thirteen Books of Euclid's Elements』(Dover 1956年)

コンピュータグラフィックスの世界は、次のような2つの分野に大別できます。

  • ベクタグラフィックス ― 解析幾何の応用であり、直線や曲線の描画、および領域塗りつぶしの操作に応用される。
  • ラスタグラフィックス ― ビットマップや実世界のイメージ表現に応用される。

これらに加えてテキスト表現もあります。こちらは、コンピュータグラフィックスの世界では特有の位置を占めていますが、最近登場したアウトラインフォントなどにより、ベクタグラフィックスの一部とみなされるようになっています。

本章では、Microsoft WindowsフォームとGDI+に実装されているベクタグラフィックスについて紹介します。本章で触れるすべての描画機能は、Graphicsクラスのメソッドであり、DrawあるいはFillというプレフィックスを持っています。Draw系メソッドは、直線や曲線を描く機能を提供しています。一方、Fill系メソッドは、領域を塗りつぶす機能を実装しています(輪郭はもちろん直線と曲線で定義されます)。本書で紹介するすべてのDraw系メソッドに渡す最初の引数は、Penオブジェクトです。Fill系メソッドに渡す第1の引数は、Brushオブジェクトです。

15.1 | Graphicsオブジェクトの取得方法

描画機能のほとんどは、Graphicsオブジェクトのメソッドとして実装されています。ControlPaintクラスにもいくつかの描画機能がありますが、それらは特殊な場面で使われます。何かを描画する場合、Graphics型のオブジェクトが必要になります。しかし、Graphicsオブジェクトのコンストラクタは、public属性を持っていません。つまり、次のようにして、Graphicsオブジェクトを作成することはできないのです。

Graphics grfx = new Graphics(); // コンパイルエラー!

Graphicsクラスもまたシールで覆われています(sealed属性を持つ)。私たちは、この属性を持つクラスからは、直接独自のクラスを派生させることはできません。

class MyGraphics: Graphics // コンパイルエラー!

このため、他の方法でGraphicsオブジェクトを取得する必要があります。次に紹介するのは、Graphicsオブジェクトを取得するための完全なリストです。使用頻度の高い順に紹介しています。

  • Controlクラスからの派生クラス(たとえばForm)内で、OnPaintメソッドをオーバーライドしたり、Paintイベントハンドラをインストールしている場合、GraphicsオブジェクトはPaintEventArgsクラスのプロパティの1つとして用意されます。
  • OnPaintメソッドやPaintイベントハンドラ内ではなく、他の箇所でコントロールやフォームに何かを描画する必要も出てきます。そのような場面では、コントロールのCreateGraphicsメソッドを使用することができます。クラスによっては時折、自分のコンストラクタ内でCreateGraphicsを呼び出し、情報を取得し、初期化処理を行っています。第4章で紹介したプログラムのいくつかは、そのようなクラスを持っていました。クラスは、コンストラクタ内で描画処理を行うことはできませんが、他のイベント処理時には行えるようになります。たとえば、コントロールやフォームは、第5章、第6章、第7章で紹介するように、キーボード、マウス、あるいはタイマイベント発生時に、何らかの描画処理を行うのが一般的です。プログラム内でGraphicsオブジェクトを使用する場合、イベント発生時に取得し、その内部でのみ使用することが重要です。つまり、Graphicsオブジェクトをクラス内のフィールドに保存しておくべきではない、ということです。さらに、Graphicsオブジェクトを使用した後は、必ず、そのオブジェクトのDisposeメソッドを呼び出すことも忘れないようにしましょう。
  • 印刷時には、PrintPageイベントハンドラをインストールし、PrintPageEvent Args型のオブジェクトを取得します。このオブジェクトには、プリンタ用のGraphicsオブジェクトが含まれています。詳細については後述します。
  • メニュー、リストボックス、コンボボックス、ステータスバーなどのコントロールのいくつかは、「オーナー描画」という機能を持っています。プログラム内でこの権利を取得すると、コントロール上のいくつかの要素を動的に描画できるようになります。MeasureItemとDrawItemイベントは、MeasureItemEventArgsとDrawItemEventArgs型のオブジェクトを用意してくれます。これらのオブジェクトは、イベントハンドラ内で使用できるGraphicsオブジェクトを持っています。
  • ビットマップやメタファイルを描画する場合、Graphics.FromImageという静的メソッドを呼び出して、特殊なGraphicsオブジェクトを取得する必要があります。詳細については第18章と第24章で取り上げます。
  • 実際には印字しないまでも、プリンタ関連のGraphicsオブジェクトの情報が必要な場合、PrinterSettingsクラスのCreateMeasurementGraphicsメソッドを使用することができます。
  • Win32コードを直接使用している場合、Graphics.FromHwndとGraphics.FromHdcという静的メソッドを呼び出し、Graphicsオブジェクトを取得することができます。

15.2 | ペンについて一言

紙の上に手書きで直線を描くとき、私たちは鉛筆、クレヨン、万年筆、ボールペン、あるいはフェルトペンなどを使用します。選んだ筆記用具に応じて少なくとも、描こうとする直線の色と幅が決まります。これらの描画要素は、Penクラスとして定義されているため、直線を描くときには必ずPenオブジェクトを指定します。

ここでは、ペンについて詳細に説明するつもりはありません。ペンはブラシから作成できるため、ペンの詳細説明はブラシの説明を抜きにしてはできないのです。また、ブラシはビットマップイメージやグラフィックスパスから作成でき、そちらの方面の解説はどちらかといえば、高度なグラフィックプログラミング技術の範疇に入ります。第21章では、ペンとブラシを隅々まで解説することになっています。

第3章で触れたように、次のようなコードを記述すれば、特定の色からペンを作成することができます。

Pen pen = new Pen(color);

colorはColor型のオブジェクトです。Pensクラスを活用してもよいでしょう。このクラスは、Penオブジェクトを返してくれる、141種類の読み取り専用の静的プロパティを持っています。このため、Pens.HotPinkは、直線描画メソッドの第1の引数として渡すことができます(もちろん、使う場面を考える必要はあります)。色名の全リストは、本書の巻末を参照してください。

SystemPensというクラスも利用できます。このクラスは、システムカラーに応じたPenオブジェクトを返す、15種類の読み取り専用の静的プロパティを持っています。しかし、既に背景色を設定し、その色とかちあわないことがわかっているのであれば、次のように、現在のForeColorプロパティからぺンオブジェクトを作成してもよいでしょう。

Pen pen = new Pen(ForeColor);

ペンについて、もう1点述べておきたいことがあります。それはペンの幅です。ペンの幅は、読み取り/書き込みプロパティとなっています。

▼ Penプロパティ(抜粋)

プロパティ名 アクセス状態  
float Width get/set  

ペン幅を受け取るPenコンストラクタもありますから、これまでに紹介した2つのPenコンストラクタを次の表に示しておきます。

▼ Penコンストラクタ(抜粋)

Pen(Color color)
Pen(Color color, float fWidth)

情報の出し惜しみをしていると思われないように、これ以外にも2つのPenコンストラクタがあることを明確にしておきます。それらは、Brushオブジェクトを第1の引数として受け取るだけで、ここに紹介したコンストラクタと変わりありません。上に示した最初のコンストラクタを使用すると、幅1ピクセルのペンが作成されます。PensとSystemPensクラスから取得できるPenオブジェクトの幅も1ピクセルです。しばらくの間は、ペンは1ピクセルの幅を持っていると考えるようにしてください。しかし、第16章で説明するように、この幅は「ワールド座標」で表現されますから、各種の変換の影響を受けます。

各種の変換に関係なく、常に幅1ピクセルのペンを作成することも可能です。次のように、幅として0をコンストラクタに渡します。

Pen pen = new Pen(color, 0);

あるいは、次のようにWidthプロパティを0に設定することもできます。

pen.Width = 0;

15.3 | 直線

1本の直線を描く場合、GraphicsクラスのDrawLineメソッドを使用します。このメソッドは、4種類のオーバーロードバージョンがありますが、受け取る情報はすべて同じです。つまり、直線の始点と終点の座標値と使用するペンを渡します。

▼ GraphicsのDrawLineメソッド

void DrawLine(Pen pen, int x1, int y1, int x2, int y2)
void DrawLine(Pen pen, float x1, float y1, float x2, float y2)
void DrawLine(Pen pen, Point point1, Point point2)
void DrawLine(Pen pen, PointF point1, PointF point2)

このように、4個のintあるいはfloat型の値を座標値として渡すことができます。また、2個のPointかPointF構造体を渡すことができます。

DrawLineメソッドは、始点から終点を含む2点間に直線を引きます。これは、終点を含めないWin32 GDI関数の動作と多少異なります。DrawLineメソッドのコード例を次に示します。

grfx.Drawline(pen, 0, 0, 5, 5);

このコードが実行されると、5ピクセル長の黒い直線が描かれます。直線を構成する座標は、(0, 0)、(1, 1)、(2, 2)、(3, 3)、(4, 4)、および(5, 5)となります。2つの座標を渡す順序は問題ではありませんから、次のようなコードを記述することもできます。

grfx.DrawLine(pen, 5, 5, 0, 0);

このコードの実行結果は先のコードと同じです。次に紹介するコードは、(2, 2)と(3, 3)を通過する2ピクセル長の直線を描きます。

grfx.DrawLine(pen, 2, 2, 3, 3);

しかし、次のコードでは直線は描かれません。

grfx.DrawLine(pen, 3, 3, 3, 3);

既にご存知のように、FormのClientSizeプロパティを使用すれば、クライアント領域の幅と高さを知ることができます。水平軸上のピクセル数は、ClientSize.Widthに格納され、0からClientSize.Width - 1までの数値として表現されます。同じように垂直軸上のピクセル数は、0からClientSize.Height - 1の数値として表現されます。

次に紹介するXMarksTheSpotプログラムは、クライアント領域に文字Xを描画します。


//----------------------------------------------
// XMarksTheSpot.cs (C) 2001 by Charles Petzold
//----------------------------------------------
using System;
using System.Drawing;
using System.Windows.Forms;
class XMarksTheSpot: Form
{
    public static void Main()
    {
    Application.Run(new XMarksTheSpot());
    }
    public XMarksTheSpot()
    {
        Text = "X Marks The Spot";
        BackColor = SystemColors.Window;
        ForeColor = SystemColors.WindowText;
        ResizeRedraw = true;
    }
    protected override void OnPaint(PaintEventArgs pea)
    {
        Graphics grfx = pea.Graphics;
        Pen      pen  = new Pen(ForeColor);
        grfx.DrawLine(pen, 0, 0,
                      ClientSize.Width - 1, ClientSize.Height - 1);
        grfx.DrawLine(pen, 0, ClientSize.Height - 1,
                      ClientSize.Width - 1, 0);
    }
}

最初のDrawLine呼び出しでは、左上隅から右下隅に向かって直線が引かれます。2番目の呼び出しでは描画始点が逆になり、座標(0, ClientSize.Height - 1)から座標(ClientSize.Width - 1, 0)に向かって引かれます。

15.4 | 印刷入門

本章や以降の章で紹介するサンプルプログラムの多くは、XMarksTheSpotと似たような構成になっています。XMarksTheSpotほどぶっきらぼうにする必要はありませんが、一部を除き、クライアント領域に静的なイメージを表示するだけです。要するに、グラフィックプログラミング技術の基本を紹介しているにすぎません。

イメージをクライアント領域に表示するだけではなく、冷蔵庫のドアに貼り付け、それを自慢したいと考える人もいるでしょう(好ましいことであるかどうかは別ですが)。そこで、印刷機能を追加してみます。グラフィックを印刷してみるとわかりますが、グラフィックプログラミングシステムはデバイスに依存していません。印刷機能の追加は、システム自体への理解を深める点でも意味のあることなのです。

印刷は、どちらかといえば、プログラミング書籍の最後の方に追いやられるトピックです。書籍によっては、一切触れることもないでしょう。最大の理由は、説明が難しくなってしまうからです。私は、第23章を現在利用できるすべての印刷機能やオプション機能の解説に当てています。しかし、現時点では、簡単な印刷機能を使って、Windowsフォームアプリケーションから印刷処理を実行するにとどめます。簡単に言えば、デフォルト設定を使って、ユーザーのデフォルトプリンタに1ページ分のデータを出力しています。

実は、この時点で印刷機能に触れるのに躊躇しました。印刷機能を呼び出すユーザーインターフェイスをどのようにするか、という問題を解決しなければならないからです。印刷機能を持つほとんどのプログラムは、[ファイル]メニューに[印刷]コマンドが付いています。メニューについての説明は第9章で行いますから、ここでメニューに言及するのは好ましいことではありません。また、私は、単純なキーボードインターフェイスを実装することを考えました。おそらく、Print ScreenキーかCtrl+Pキーを使用することになったでしょう。しかし、こちらも時期尚早です。このため、私は最終的に、OnClickメソッドをオーバーライドする方法を採用することに決めました。

OnClickはControlクラスに実装され、Controlクラスから派生するすべてのクラス(Formも含む)に継承されています。このメソッドは、ユーザーがマウスボタンでフォームのクライアント領域をクリックしたときに、必ず呼び出されます。マウスとOnClickの関係については第6章で詳述します。

デフォルトプリンタに印字出力するには、PrintDocument型のオブジェクトをまず作成する必要があります。PrintDocumentは、System.Drawing.Printing名前空間に定義されています。

PrintDocument prndoc = new PrintDocument();

このクラスの詳細については第23章で取り上げます。現時点では、このクラスが持つプロパティ、イベント、およびメソッドをそれぞれ1つ紹介するにとどめます。

PrintDocumentオブジェクトのDocumentNameプロパティにテキスト文字列を設定します。この文字列は、グラフィック出力がプリンタにスプールされるときに、プリンタウィンドウでジョブを識別するための表示テキストとなります。設定コードは次のように記述します。

prndoc.DocumentName = "My print job";

ドキュメントを操作するプログラムは、ドキュメントの名前をこのテキスト文字列として使用するのが一般的です。本章では、プログラムのキャプションバーのテキストを使用することにします。

グラフィック出力を行うには、クラス内にメソッドを1つ作成する必要があります。ここでは、次のように、メソッド名をPrintDocumentOnPrintPageとします。

void PrintDocumentOnPrintPage(object obj, PrintPageEventArgs ppea) {     ... }

このハンドラを次のように、PrintDocumentオブジェクトのPrintPageイベントにアタッチします。

prndoc.PrintPage += new PrintPageEventHandler(PrintDocumentOnPrintPage);

これは、第2章で紹介したいくつかのサンプルプログラム内で、Paintイベントハンドラをインストールしたのと同じコードです。第4章で取り上げたSysInfoPanelプログラムでも同じコードを使っています。

印刷を開始するには、次のように、PrintDocumentオブジェクトのPrintメソッドを呼び出す必要があります。

prndoc.Print();

このメソッドは、動作開始後すぐに復帰するわけではありません。小さなメッセージボックスが短時間表示され、指定したドキュメント名を知らせてくれます。つまり、印刷ジョブを取り消すかどうかを尋ねてきます。

Printメソッドはまた、PrintPageイベントハンドラが呼び出される背景を用意します。既に説明したように、このハンドラはPrintDocumentOnPrintPageという名称を持っています。ハンドラが受け取るobject引数は、事前に作成されているPrintDocumentオブジェクトです。さらに、PrintPageEventArgs引数は、プリンタ情報を提供してくれるプロパティを持っています。しかし、プロパティの中で最も重要なのは、Graphicsという名称のプロパティで、PaintEventArgsの同名のプロパティと似た役割を持っています。ただし、PrintPageEventArgsオブジェクトが持つGraphicsプロパティは、フォームのクライアント領域ではなく、印刷ページのGraphicsオブジェクトへのアクセスを可能としてくれています。

このようなことから、PrintDocumentOnPrintPageメソッドは次のように実装できます。

void PrintDocumentOnPrintPage(object obj, PrintPageEventArgs ppea) {     Graphics grfx = ppea.Graphics;     ... }

私たちは、Graphicsオブジェクトを経由して、印刷ページにグラフィックを表示するためのメソッドを呼び出します。

複数のページを印刷するような場合、PrintPageEventArgsのHasMorePagesプロパティをtrueに設定します。しかし、1ページしか印刷しない場合には、プロパティをデフォルトのfalseにしておき、PrintDocumentOnPrintPageから復帰します。

PrintDocumentOnPrintPageがデフォルトのHasMorePagesプロパティ値を持って復帰した場合、Printメソッド呼び出しそのものも復帰します。この時点で、プログラムは印刷ジョブを完了したことになります。グラフィック出力をプリンタに実際に送信するのは、他のだれかの仕事です。紙詰まり、インクカートリッジの不備、トナー切れ、ケーブル接続の不良などの検出も他のだれかの仕事です。

複数のプリンタをマシンに接続していることもあります。先に紹介した印刷処理の手順は、デフォルトのプリンタを使っています。コントロールパネルなどから開くことができるプリンタウィンドウは、[プリンタ]メニューからデフォルトプリンタを設定する項目を提供しています。

ご存知のように、フォームのClientSizeプロパティは、フォームのクライアント領域のピクセルサイズを知らせてくれますから、クライアント領域で無難に描画処理を行ううえでは十分な情報を得られます。印刷ページの同様のプロパティは、どちらかといえば、よりプログラム的といえます。

印刷ページは、3種類の領域で定義されます。まず、ページ全体の大きさがあります。この情報は、PrintPageEventArgsクラスのPageBoundsプロパティから取得できます。PageBoundsプロパティはRectangle構造体で、XとYプロパティは0、WidthとHeightプロパティはデフォルトの用紙情報を保持しています(0.01インチ単位)。たとえば、8と2分の1×11インチの用紙の場合、PageBoundsのWidthとHeightプロパティは850と1100という数値を保持しています。デフォルトプリンタの設定が縦ではなく、横となっている場合、WidthとHeightプロパティは1100と850というぐあいに逆に設定されます。

2番目の領域は、ページの印字可能な領域です。この領域は通常、印字ヘッドなどが移動してこないマージンを除く、ページ全体領域とほぼ同じです。マージンは、ページの上と下あるいは左と右で異なる値を持つことも可能です。GraphicsクラスのVisibleClipBoundsプロパティはRectangleF構造体であり、ページの印字可能な領域の大きさを保持しています。この構造体のXとYプロパティは、0に設定されています。WidthとHeightプロパティは、ページの印字可能な領域の大きさを示します。単位はプリンタ用紙と同じです。

3番目の領域は、ページの四隅に設定される1インチのマージンです。これらのマージンは、ユーザーが印字したいと考えている境界を示します。四隅のマージン情報は、PrintPageEventArgsオブジェクトのMarginBoundsプロパティ経由でRectangle構造体として取得できます。

このようなことの詳細については、第23章で取り上げます。ここでは、GraphicsクラスのVisibleClipBoundsプロパティを使用することが最適であることを強調しておきます(私の経験から)。PrintPageEventArgsオブジェクトから取得できるGraphicsオブジェクトは、VisibleClipBoundsプロパティと整合性を持っています。つまり、座標(0, 0)はページの印字可能な領域の左上隅を表しているのです。

これまで、ビデオディスプレイの可視色を使用する際の注意点などをことさら強調してきましたが、それらは当然プリンタには当てはまりません。プリンタの場合、Color. Blackを選択することが一番最適です。ペンはPens.Black、ブラシはBrushes.Blackを選択すれば問題がありません。真っ黒な用紙をプリンタに入れる人はまずいませんから、これらの色選択で十分なはずです。

次に紹介するプログラムは、「Click to print」という文字列をクライアント領域に表示し、ボタンがクリックされた時点で印刷を開始します。


//---------------------------------------------
// HelloPrinter.cs (C) 2001 by Charles Petzold
//---------------------------------------------
using System;
using System.Drawing;
using System.Drawing.Printing;
using System.Windows.Forms;
class HelloPrinter: Form
{
    public static void Main()
    {
        Application.Run(new HelloPrinter());
    }
    public HelloPrinter()
    {
        Text = "Hello Printer!";
        BackColor = SystemColors.Window;
        ForeColor = SystemColors.WindowText;
    }
    protected override void OnPaint(PaintEventArgs pea)
    {
        Graphics     grfx   = pea.Graphics;
        StringFormat strfmt = new StringFormat();
        strfmt.Alignment = strfmt.LineAlignment = StringAlignment.Center;
        grfx.DrawString("Click to print", Font, new SolidBrush(ForeColor),
                        ClientRectangle, strfmt);
    }
    protected override void OnClick(EventArgs ea)
    {
        PrintDocument prndoc = new PrintDocument();
        prndoc.DocumentName = Text;
        prndoc.PrintPage +=
            new PrintPageEventHandler(PrintDocumentOnPrintPage);
        prndoc.Print();
    }
    void PrintDocumentOnPrintPage(object obj, PrintPageEventArgs ppea)
    {
        Graphics grfx = ppea.Graphics;
        grfx.DrawString(Text, Font, Brushes.Black, 0, 0);
        SizeF sizef = grfx.MeasureString(Text, Font);
        grfx.DrawLine(Pens.Black, sizef.ToPointF(),
                      grfx.VisibleClipBounds.Size.ToPointF());
    }
}

ソースコードからもわかるように、私はフォームのTextプロパティを、印刷ドキュメント名と、PrintDocumentOnPrintPageメソッド内のDrawStringとMeasure Stringに渡すテキスト文字列の引数として使用しています。プログラムは、「Hello Printer!」というテキストをページの印字可能な領域の左上隅に表示し、その後にテキスト文字列の右下隅からページの印字可能な領域の右下隅まで1本の線を描画します。このサンプルプログラムを検討すれば、VisibleClipBoundsがGraphicsと整合性のある情報を提供してくれていることがわかると思います。

おそらく、一部の読者は、冷ややかな笑みを浮かべていることでしょう。DrawStringとMeasureString呼び出しを見ればわかりますが、私はフォームのFontプロパティを何の考えもなく使っています。プリンタの解像度は、インチあたり300、600、720、1200、1440、2400、あるいは2880ドットとなっています。しかし、このようなことは一切考慮していません。フォームのFontプロパティ経由で利用できるフォントは、ビデオディスプレイに応じてシステムが勝手に選択しているものです。おそらく、インチあたり100ドット程度の解像度が選択されているはずです。これでは、印刷される文字はどことなく間が抜けた印象を与えてしまいます。

このサンプルプログラムをコンパイルし、実行してみてください。テキストは、尊敬すべき8ポイントのフォントで印刷されます。また、プログラムが描く斜線の幅が1ピクセル以上となっていることにも注意してください。今日の高解像度プリンタでは、1ピクセル幅の直線はほとんど見えません。このため、Windowsフォームは固定直線を採用しているのです。この点では、すばらしい選択ではありますが、その反面、現状ではわかりにくいともいえます。なぜ固定直線か? この回答については第16章と第17章で用意しています。

それでは、フォームのクライアント領域と印刷ページに同じ文字列を出力するプログラムを作成してみましょう。OnPaintメソッドのコードをPrintDocumentOnPrintPageメソッドにコピーアンドペーストするように言っているのではありません。ここでは、グラフィックの出力コードを独立した名称を持つDoPageメソッドに記述し、プログラミングすることの意味なども含めて考えてみます。DoPageメソッドは、OnPaintとPrintDocumentOnPrintPage両方のメソッドから呼び出されます。次に紹介するサンプルコードは、XMarksTheSpotサンプルコードの一部を変更したものです。


//----------------------------------------------
// PrintableForm.cs (C) 2001 by Charles Petzold
//----------------------------------------------
using System;
using System.Drawing;
using System.Drawing.Printing;
using System.Windows.Forms;
class PrintableForm: Form
{
    public static void Main()
    {
        Application.Run(new PrintableForm());
    }
    public PrintableForm()
    {
        Text = "Printable Form";
        BackColor = SystemColors.Window;
        ForeColor = SystemColors.WindowText;
        ResizeRedraw = true;
    }
    protected override void OnPaint(PaintEventArgs pea)
    {
        DoPage(pea.Graphics, ForeColor,
               ClientSize.Width, ClientSize.Height);
    }
    protected override void OnClick(EventArgs ea)
    {
        PrintDocument prndoc = new PrintDocument();
        prndoc.DocumentName = Text;
        prndoc.PrintPage +=
            new PrintPageEventHandler(PrintDocumentOnPrintPage);
        prndoc.Print();
    }
    void PrintDocumentOnPrintPage(object obj, PrintPageEventArgs ppea)
    {
        Graphics grfx  = ppea.Graphics;
        SizeF    sizef = grfx.VisibleClipBounds.Size;
        DoPage(grfx, Color.Black, (int)sizef.Width, (int)sizef.Height);
    }
    protected virtual void DoPage(Graphics grfx, Color clr, int cx, int cy)
    {
        Pen pen = new Pen(clr);
        grfx.DrawLine(pen,      0, 0, cx - 1, cy - 1);
        grfx.DrawLine(pen, cx - 1, 0,      0, cy - 1);
    }
}

プログラムの最後に置かれているDoPageメソッドは、実際のグラフィック出力を担当しています。このメソッドが受け取る引数は、Graphicsオブジェクト、デバイス用の適切な色、出力領域の幅と高さとなっています。DoPageは、OnPaintとPrint DocumentOnPrintPageという2種類のメソッド内から呼び出されます。OnPaintでは、DoPageに渡す最後の3個の引数は、ForeColor、フォームのクライアント領域の幅と高さに設定されています。一方、PrintDocumentOnPrintPage内では、これらの引数はColor.BlackとVisibleClipBoundsの幅と高さに設定されています。

私は、DoPageに渡す最後の2個の引数にcxとcyという名称を付けています。cはcountを表し、xとyは座標に使われますから、cxとcyは座標の「個数」、つまり幅と高さを指定していると考えてしまうとよいでしょう。

クライアント領域のGraphicsオブジェクトを取得している場合、たいへんおもしろいことに、VisibleClipBoundsプロパティはクライアント領域の幅と高さと同じ値を保持しています。私は、cxとcy引数を使用せずに、DoPage内でVisibleClip Boundsプロパティを使用することもできたと思います(このプロパティから画面とプリンタ用の表示情報を取得できるため)。しかし、幅と高さの情報を事前に変数に用意し、簡単に利用できるようにしておく方法を選びました。また、DoPageメソッドはオーバーライド可能なように、protectedとvirtualになっていることにも注意してください。1つのグラフィック画面だけを表示するプログラムを作成したい場合、Formではなく、PrintableFormから派生させ、印刷機能をそのプログラムに組み込んでみるとよいでしょう。

私は、本章と以降の章で紹介するほとんどのプログラムに、この方法で印刷機能を組み込んでいます。

15.5 | プロパティと状態

グラフィックプログラミング環境によっては、現在位置という概念を採用しているものもあります。現在位置というのは、描画関数の内部で開始点として使用される座標であり、環境自体により管理されます。一般的には、グラフィックスシステムは1つの関数を定義し、その内部で現在位置を設定できるようにしています。その位置から特定の位置まで直線を引く場合には、別の関数を使用してもらうわけです。その上で、描画関数は現在位置を新しい座標に設定します。

GDI+は、現在位置という概念を持っていません。これは、経験を積んだWindowsプログラマにはちょっとショックかもしれません。Windows GDIで直線を描く場合、2つの関数を呼び出し、それぞれが座標を指定します。より具体的に説明すれば、MoveToで現在位置を設定し、LineToでその位置(終点は含まず)までの直線を引きます。

GDI+ではまた、DrawLineとDrawString呼び出し時には、フォント、ブラシ、ペンなどを指定するための引数が必要になります。Windows GDIでは、これらの引数は不要でした。GDI+とWindows GDIの設計上の相違がここまで違わなければ、フォント、ブラシ、およびペンは、Graphicsオブジェクトのプロパティとなっていたでしょう。StringFormatは、テキストを表示するためのかなり詳細な情報を指定していました。覚えているでしょうか? StringFormatはまた、Graphicsオブジェクトのプロパティではなく、DrawStringの引数として使われていました。

これらの理由により、GDI+の設計者たちは、GDI+を状態を持たない(ステートレス)グラフィックプログラミング環境と位置付けました。もちろん、まったく状態を持たないということではありませんが。完全に状態を持たないようなら、Graphicsクラスは読み取り/書き込みプロパティさえ持っていなかったでしょう。実際には、Graphicsクラスは12個の読み取り/書き込みプロパティと、6個の読み取り専用のプロパティを持っています。

私は、グラフィック図形の表現に深く関与する4個の設定可能なGraphicsプロパティを重視しています。これらのプロパティを次に示します。

  • PageScaleとPageUnitは描画単位を管理します。デフォルトではピクセル単位となります。より詳細については第16章で取り上げます。
  • Transformプロパティは、Matrix型のオブジェクトであり、すべてのグラフィック出力のマトリックス変換を定義します。変換というのは、座標変換、スケール変換、傾斜、回転を指します。詳細については第16章で取り上げます。
  • Clipはクリップする領域を管理します。クリップ領域を設定しておくと、呼び出す描画関数はすべてその領域に対してのみ作業を行うようになります。詳細ついては第20章で取り上げます。

15.6 | アンチエイリアシング

グラフィック出力に密接に関与する4個のプロパティに加えて、Graphicsクラスはちょっとした出力調整を行うプロパティを提供しています。このようなプロパティの中では、SmoothingModeとPixelOffsetModeが重要です。

▼ Graphicsプロパティ(抜粋)

プロパティ名 アクセス状態 意味
SmoothingMode SmoothingMode get/set 直線のアンチエイリアシング
PixelOffsetMode PixelOffsetMode get/set 高度なアンチエイリアシング

これらのプロパティは、「アンチエイリアシング」として知られているグラフィック描画技術を提供しています。この場合の「エイリアス」とは、サンプリング理論から拝借された概念です。アンチエイリアシングは、色の濃淡を応用して、表示されるグラフィック形状のジャギーを滑らかにしようとする技術です。

次に紹介するサンプルプログラムは短い直線を描きます。ソースコードを見るとわかるように、SmoothingModeとPixelOffsetModeプロパティを設定するためのステートメントも含まれています。


//------------------------------------------
// AntiAlias.cs (C) 2001 by Charles Petzold
//------------------------------------------
using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms;
class AntiAlias: Form
{
    public static void Main()
    {
        Application.Run(new AntiAlias());
    }
    public AntiAlias()
    {
        Text = "Anti-Alias Demo";
        BackColor = SystemColors.Window;
        ForeColor = SystemColors.WindowText;
    }
    protected override void OnPaint(PaintEventArgs pea)
    {
        Graphics grfx = pea.Graphics;
        Pen      pen  = new Pen(ForeColor);
        grfx.SmoothingMode   = SmoothingMode.None;
        grfx.PixelOffsetMode = PixelOffsetMode.Default;
        grfx.DrawLine(pen, 2, 2, 18, 10);
    }
}

プロパティの設定をさまざまに変えて再コンパイルし、実行を繰り返してみてください。そして、時には、画面イメージをキャプチャし、ペイントプログラムなどで表示し、違いを確認してみるとよいでしょう。私が行った結果をこれから紹介します。

デフォルトでは、直線は次のように描画されます。

Dd297678.05-01(ja-jp,MSDN.10).gif

この図では、左端と上端にクライアント領域以外のフォームの一部も含めてあります。直線の始点が、ピクセル位置(2, 2)となっていることがはっきりわかると思います。

SmoothingMode列挙体は、System.Drawing.Drawing2D名前空間に定義されています。

▼ SmoothingMode列挙体

メンバ コメント
Default 0 アンチエイリアシング無効
HighSpeed 1 アンチエイリアシング無効
HighQuality 2 アンチエイリアシング有効
None 3 アンチエイリアシング無効
AntiAlias 4 アンチエイリアシング有効
Invalid -1 例外発生

このように、選択肢は3種類しかありません。つまり、アンチエイリアシングを使いたいのか、使いたくないのか、あるいは例外を発生させたいのか、この3つの中のいずれかを選択します。デフォルトはNoneとなっています。

SmoothingModeプロパティをSmoothingMode.HighQualityあるいはSmoothing Mode.AntiAliasに設定して、アンチエイリアシングを有効にすると、次のような直線が描かれます。

Dd297678.05-02(ja-jp,MSDN.10).gif

身近で見るとよくわかりませんが、少し距離をおいて眺めると、滑らかな直線が浮かび上がってきます。すべての人がそうではありませんが、人によっては、アンチエイリアシングはグラフィック表示を「ファジー」にしていると主張しています。

この図からわかることは、直線はピクセル(2, 2)の中心から始まり、ピクセル(18, 10)の中心で終了していることです。直線の幅は1ピクセルです。グラフィックシステムがアンチエイリアシングを使用している場合、ピクセルの黒色の濃淡は、論理的な直線がピクセルを交錯する度合いによって決まります。

アンチエイリアシングを有効にしている場合、PixelOffsetModeプロパティを使用することにより、描画精度を改善することができます。PixelOffsetModeプロパティには、次に示すようなSystem.Drawing.Drawing2D名前空間に定義されている列挙体値を設定できます。

▼ PixcelOffsetMode列挙体

メンバ 意味
Default 0 ピクセルオフセット設定なし
HighSpeed 1 ピクセルオフセット設定なし
HighQuality 2 ハーフピクセルオフセット設定
None 3 ピクセルオフセット設定なし
Half 4 ハーフピクセルオフセット設定
Invalid -1 例外発生

ここでも、選択肢は3種類しか用意されていません。そのうちの1つは意味がありません。PixelOffsetModeプロパティをHalfあるいはHighQualityに設定している場合、直線は次のように描かれます。

Dd297678.05-03(ja-jp,MSDN.10).gif

ピクセルオフセットを設定することは、解析幾何を利用した、より精度の高い描画処理を行うことを意味します。直線の座標は、ハーフピクセル分減少しています。つまり、直線は左上隅から2ピクセル位置から始まりますが、実際には、ピクセル間の隙間から始まっています。

15.7 | 複数の直線の連結

既に触れたように、いくつかのグラフィックプログラミング環境には「現在位置」という考え方があります。一部の読者は、1本の直線を描くために2つの関数が必要となることもあり、それは奇妙な概念であると考えているかもしれません。しかし、現在位置は複数の直線を描き、それらを連結するような場面ではそれなりの意味を持っているのです。各関数は、1個の座標を必要とするだけです。GDI+は、これほど効率的な機能を持っていません。たとえば、次のサンプルコードでは、4つのDrawLine呼び出し、プログラムのクライアント領域周辺に四角いボックスを描いています。

grfx.DrawLine(pen, 0, 0, cx - 1, 0); grfx.DrawLine(pen, cx - 1, 0, cx - 1, cy - 1); grfx.DrawLine(pen, cx - 1, cy - 1, 0, cy - 1); grfx.DrawLine(pen, 0, cy - 1, 0, 0);

個々の呼び出しの終点は、次の呼び出しの始点として繰り返し使われていることに注目してください。

こうした点やこの後で紹介する各種の理由により、Graphicsクラスには複数の直線を連結する(「ポリライン(折れ線)」と呼ばれている)メソッドが実装されています。このメソッドはDrawLines(sが付き複数となっている点に注意)と呼ばれ、次のような2つのバージョンが用意されています。

▼ GraphicsのDrawLinesメソッド

void DrawLines(Pen pen, Point[] apt)
void DrawLines(Pen pen, PointF[] aptf)

このメソッド定義からわかるように、整数型のPoint座標配列か浮動小数点型のPointF座標配列が必要になります。

次に紹介するサンプルプログラムは、クライアント領域を囲うように4本の直線を描きます。


//------------------------------------------------
// BoxingTheClient.cs (C) 2001 by Charles Petzold
//------------------------------------------------
using System;
using System.Drawing;
using System.Windows.Forms;
class BoxingTheClient: PrintableForm
{
    public new static void Main()
    {
        Application.Run(new BoxingTheClient());
    }
    public BoxingTheClient()
    {
        Text = "Boxing the Client";
    }
    protected override void DoPage(Graphics grfx, Color clr, int cx, int cy)
    {
        Point[] apt = {new Point(     0, 0),
                       new Point(cx - 1, 0),
                       new Point(cx - 1, cy - 1),
                       new Point(     0, cy - 1),
                       new Point(     0, 0)};
        grfx.DrawLines(new Pen(clr), apt);
    }
}

ソースコードを見るとわかるように、このクラスはPrintableFormクラスから派生していますから、印刷することができます。

Point構造体配列は、DrawLinesメソッド内に直接定義できます。次に紹介するサンプルプログラムは、そのアプローチを採用しています。このサンプルは、ペンや鉛筆を使わない、家の形をした子供向けのパズルのようです。


//------------------------------------------
// DrawHouse.cs (C) 2001 by Charles Petzold
//------------------------------------------
using System;
using System.Drawing;
using System.Windows.Forms;
class DrawHouse: PrintableForm
{
    public new static void Main()
    {
        Application.Run(new DrawHouse());
    }
    public DrawHouse()
    {
        Text = "Draw a House in One Line";
    }
    protected override void DoPage(Graphics grfx, Color clr, int cx, int cy)
    {
        grfx.DrawLines(new Pen(clr),
                       new Point[]
                       {
                       new Point(    cx / 4, 3 * cy / 4), // 左下
                       new Point(    cx / 4,     cy / 2),
                       new Point(    cx / 2,     cy / 4), // ピーク
                       new Point(3 * cx / 4,     cy / 2),
                       new Point(3 * cx / 4, 3 * cy / 4), // 右下
                       new Point(    cx / 4,     cy / 2),
                       new Point(3 * cx / 4,     cy / 2),
                       new Point(    cx / 4, 3 * cy / 4), // 左下
                       new Point(3 * cx / 4, 3 * cy / 4)  // 右下
                       });
    }
}

DrawLinesは、もちろん、子供向けのパズルプログラムを作成するために、用意されたものではありません。第21章では、ドットや点線を持つペンを作成するテクニックを紹介します。太いペンを作成するときには、直線の終点の見かけを整えたり(丸みを帯びたりあるいは尖らせたりなど)、連結される2つの直線の表示形式を指定することができます。これらのプログラミング要素は、「終点」や「連結」などと呼ばれています。終点と連結の操作を正常に行うために、GDI+は座標を共有する2つの直線が相互に独立しているのか、あるいは、結び付けられるのかを理解する必要があります。DrawLineではなく、DrawLinesを使用すると、このような情報をGDI+に提供することができます。

DrawLinesを使用するもう1つの理由は、パフォーマンスです。このパフォーマンスの改善は、これまで紹介してきたようなサンプルプログラムではそれほど重要ではありませんし、また、実感できるものでもありません。しかし、このメソッドの真の目的は、直線を描くことではないのです。曲線を描くことがこのメソッドの目的なのです。この意味では、私たちはまだDrawLinesメソッドを使っていないといってもよいでしょう。個々の直線を短めに描き、それらを有機的に結び付けるのがこのメソッドの真の使い方です。数学的に定義できるすべての曲線は、DrawLinesメソッドで描くことが可能です。

1回のDrawLines呼び出しで、数百あるいは数千のPointやPointF構造体を使用するのをためらう必要などありません。このメソッドは多数の構造体を受け取り、目的の処理を行ってくれます。DrawLinesメソッドに百万のPointやPointF構造体を渡しても、描画に要する時間は1、2秒です。

ある曲線を描画するために、どれくらいの数の座標が必要でしょうか? おそらく、百万も必要となることはないでしょう。少なくとも、座標数とピクセル数が同じなら、その曲線は滑らかに描かれます。大まかな概算値を出して、それを基に描画していることが多いと思います。

次に紹介するサンプルプログラムは、クライアント領域に等しい大きさを持つ、1周期分の正弦曲線を描きます。


//------------------------------------------
// SineCurve.cs (C) 2001 by Charles Petzold
//------------------------------------------
using System;
using System.Drawing;
using System.Windows.Forms;
class SineCurve: PrintableForm
{
    public new static void Main()
    {
        Application.Run(new SineCurve());
    }
    public SineCurve()
    {
        Text = "Sine Curve";
    }
    protected override void DoPage(Graphics grfx, Color clr, int cx, int cy)
    {
        PointF[] aptf = new PointF[cx];
        for (int i = 0; i < cx; i++)
        {
            aptf[i].X = i;
            aptf[i].Y = cy / 2 * (1 -(float)
                                Math.Sin(i * 2 * Math.PI / (cx - 1)));
        }
        grfx.DrawLines(new Pen(clr), aptf);
    }
}

このプログラムは、本書で最初に三角関数関連メソッドを使用しています。三角関数関連メソッドは、System名前空間に定義されている重要なクラスの1つである、Mathクラスから公開されています。Mathクラスについては付録Bで詳述しています。三角関数関連メソッドへの引数は、度数ではなく、ラジアンで渡します。Mathクラスはまた、PIとEという名称の便利なconstフィールドを持っています。これらのフィールドは、三角関数関連メソッド内で使用できます。今後、プログラムの先頭に次のようなコードを記述する必要はありません。

#define PI 3.14159 // まったく不要!

ここで、注意点を1つ述べておく必要があります。Mathメソッドのほとんどは、double型の値を返すということなのです。このため、それらの値は、PointFや同類の構造体で使用するときには、明示的にfloat型にキャストしておく必要があります。

PointF配列のYプロパティへの代入ステートメントを詳細に解析してみるとよいでしょう。Math.Sinメソッドに渡す引数はラジアンです。完全な円(360°)は、2πラジアンです。このため、引数の範囲は0(iが0のとき)~2π(iがClientSize. Width - 1のとき)となります。Math.Sinメソッドの値は、-1と+1の間に入ります。通常、この値はクライアント領域の半分の高さを基準にして目盛り付けされ、負のClientSize.Height / 2から正のClientSize.Height / 2までの範囲となり、その後にクライアント領域の高さの半分が差し引かれ、最終的に0からClientSize.Heightとなります。しかし、私はSinメソッドの負の結果に1を加算し、値が0~2となるように調整し、その後にクライアント領域の半分の高さを掛けることにより、演算を効率化しています。サンプルプログラムの実行結果は、次のようになります。

Dd297678.05-04(ja-jp,MSDN.10).gif

15.8 | 曲線とパラメータ式

正弦曲線をコーディングするのは比較的簡単です。yの値は簡単なx関数から取得できるからです。しかし、一般的には、曲線のコーディングはそれほど簡単ではありません。たとえば、中心を(0, 0)に持つ単位円(半径1)は、次のような式で表現できます。

x2 + y2 = 1

半径rの円は、同じように次の式で表されます。

x2 + y2 = r2

この式を、yはxの関数であるという形に変換すると、次のような式が得られます。

実は、この式はいくつかの問題を顕在化してくれています。まずは、yはすべてのx値に対して2つの値を取ることができることを示しています。2番目の問題は、x値には無効な値もあるということです。xは-r~+rの範囲に入っている必要があります。最後の問題は、これはかなり現実的です。この式に基づいて円が描画されますが、解像度に偏りが出てしまうことです。xが0周辺にある場合、xの変化は比較的小さなyの変化を生み出します。ところが、xがrあるいは-rに近づくと、yの値は大きく変化してしまいます。

曲線を描画する場面では、パラメータ式を使用するのが一般的です。パラメータ式では、すべての点のxとy座標値は3番目の変数を持つ関数から算出されます。この3番目の変数は、tと呼ばれることがあります。読者の中には、直感的に、tは時間(timeの「t」)か、曲線全体を定義するための抽象的なインデックスではないか、と考える人がいるでしょう。Windowsフォームを使ったグラフィックプログラムでは、tは0から(配列内のPointF構造体数 - 1)の範囲の値と考えてください。

単位円を定義するパラメータ式は、次のようになります。

x(t) = cos(t)
y(t) = sin(t)

tは、0~2πまでの範囲を取るため、これらの式は(0, 0)を中心とする半径1の円を定義しています。

同様に、楕円は次のように定義できます。

x(t) = RX cos(t)
y(t) = RY sin(t)

楕円の2つの軸は、水平および垂直軸にそれぞれ平行となります。楕円の水平軸は2×RX長となり、垂直軸は2×RYとなります。中心は(0, 0)のままです。中心を(CX, CY)に設定するには、式を次のように変換します。

x(t) = CX + RX cos(t)
y(t) = CY + RY sin(t)

次に紹介するサンプルプログラムは、表示領域全体をカバーする楕円を描きます。


//--------------------------------------------
// PolyEllipse.cs (C) 2001 by Charles Petzold
//--------------------------------------------
using System;
using System.Drawing;
using System.Windows.Forms;
class PolyEllipse: PrintableForm
{
    public new static void Main()
    {
        Application.Run(new PolyEllipse());
    }
    public PolyEllipse()
    {
        Text = "Ellipse with DrawLines";
    }
    protected override void DoPage(Graphics grfx, Color clr, int cx, int cy)
    {
        int      iNum = 2 * (cx + cy);
        PointF[] aptf = new PointF[iNum];
        for (int i = 0; i < iNum; i++)
        {
            double dAng = i * 2 * Math.PI / (iNum - 1);
            aptf[i].X = (cx - 1) / 2f * (1 + (float)Math.Cos(dAng));
            aptf[i].Y = (cy - 1) / 2f * (1 + (float)Math.Sin(dAng));
        }
        grfx.DrawLines(new Pen(clr), aptf);
    }
}

描かれる楕円の中心は、表示領域の幅と高さの半分の位置に置かれ、楕円の幅と高さは、表示領域の幅と高さと等しくなっています。このため、式を単純化することができました。私は、配列内に座標の概算数を整理しています。ソースコードを見ればわかると思いますが、その概算数は表示領域周辺に引かれる矩形を表現するために、十分な値となっているはずです。

Dd297678.05-05(ja-jp,MSDN.10).gif

既に本章すべてに目を通して、GraphicsクラスがDrawEllipseメソッドを持っていることを知っている人は、なぜこのような面倒なことをしているのだろうと不思議に思っていることでしょう。実は、このサンプルプログラムは、次のプログラムを理解するための練習なのです。次に紹介するサンプルプログラムは、Graphicsクラス内の単純なメソッドでは提供されていない機能を使っています。


//---------------------------------------
// Spiral.cs (C) 2001 by Charles Petzold
//---------------------------------------
using System;
using System.Drawing;
using System.Windows.Forms;
class Spiral: PrintableForm
{
    public new static void Main()
    {
        Application.Run(new Spiral());
    }
    public Spiral()
    {
        Text = "Spiral";
    }
    protected override void DoPage(Graphics grfx, Color clr, int cx, int cy)
    {
        const int iNumRevs   = 20;
        int       iNumPoints = iNumRevs * 2 * (cx + cy);
        PointF[]  aptf       = new PointF[iNumPoints];
        float     fAngle, fScale;
        for (int i = 0; i < iNumPoints; i++)
        {
            fAngle = (float)(i * 2 * Math.PI /(iNumPoints / iNumRevs));
            fScale = 1 - (float)i / iNumPoints;
            aptf[i].X = (float)(cx / 2 * (1 + fScale * Math.Cos(fAngle)));
            aptf[i].Y = (float)(cy / 2 * (1 + fScale * Math.Sin(fAngle)));
        }
        grfx.DrawLines(new Pen(clr), aptf);
    }
}

実行結果は次のようになります。

Dd297678.05-06(ja-jp,MSDN.10).gif

15.9 | ありふれた矩形

自然の中に矩形を見出すことはそれほどありませんが、私たち人間社会では、最も一般的に使用されるオブジェクトの1つといってよいでしょう。矩形は至る所にあります。今、読んでいるページも矩形です。現在、読んでいる段落も矩形の形式に整理されています。先ほど示した実行画面の例も矩形です。コンピュータが置かれている机や疲れた体を横たえるベッドも、おそらく矩形のはずです。気分転換のために、外界を眺める窓も矩形でしょう。

確かに、DrawLineやDrawLinesメソッドを使用すれば、矩形を描くことができます。事実、これらのメソッドを使って、クライアント領域一杯に矩形を描いてきました。しかし、より簡単に矩形を描くためのメソッドが用意されています。そのメソッドはDrawRectangleという名称を持っています。DrawRectangleメソッドンは3つのバージョンがありますが、矩形はすべて、左上隅を示す座標、幅、そして高さで定義されます。

つまり、Rectangle構造体の定義内容は同じであり、3つのバージョンはいずれもこの構造体を内部で使用しているわけです。

▼ GraphicsのDrawRectangleメソッド

void DrawRectangle(Pen pen, int x, int y, int cx, int cy)
void DrawRectangle(Pen pen, float x, float y, float cx, float cy)
void DrawRectangle(Pen pen, Rectangle rect)

この表を見るとわかるように、RectangleF構造体を使用するバージョンがありません。驚いた人もいるのではないでしょうか? プログラマがpublic修飾子を付けるのを忘れてしまったのかもしれません。将来のバージョンでは、おそらく使えるようになるでしょう。

矩形の幅と高さは0以上である必要があります。それらの数値が負となっている場合、例外が発生し、描画処理は行われません。

矩形を描画するときには、1ピクセルずれエラーがよく発生します。これは、矩形自身の四辺が最小のピクセル幅であることに起因しています。矩形の幅と高さが両側の幅かその一方の幅だけを含むのか、それともいずれの側の幅も含まないのかなどに応じて異なる結果が出てしまうのです。

第16章で詳述するデフォルトペンプロパティを使用すると、DrawRectangleに渡される3ピクセルの高さと幅は、次のような結果となります(サイズ的に膨れ上がる)。

Dd297678.05-07(ja-jp,MSDN.10).gif

この図の左上隅はピクセル(x, y)となります。2ピクセルの幅と高さは3×3ピクセルとなり、内部に1ピクセルを持つ矩形が描かれます。

Dd297678.05-08(ja-jp,MSDN.10).gif

1ピクセルの幅と高さは、2×2ピクセルブロックとして描かれます。クライアント矩形の外縁を表示するために、フォームのClientRectangleプロパティを、次のように、DrawRectangle呼び出し内に記述してしまいたいと考える人もいるはずです。

grfx.DrawRectangle(pen, ClientRectangle); // 避けること!

これではうまくいきません。矩形の右側と下側は見えなくなってしまいます。次に紹介するサンプルプログラムは、クライアント領域とプリンタに完全な矩形を適切に出力します。画面表示を鮮明にするために、赤色を使っています。


//-------------------------------------------------------
// OutlineClientRectangle.cs (C) 2001 by Charles Petzold
//-------------------------------------------------------
using System;
using System.Drawing;
using System.Windows.Forms;
class OutlineClientRectangle: PrintableForm
{
    public new static void Main()
    {
        Application.Run(new OutlineClientRectangle());
    }
    public OutlineClientRectangle()
    {
        Text = "Client Rectangle";
    }
    protected override void DoPage(Graphics grfx, Color clr, int cx, int cy)
    {
        grfx.DrawRectangle(Pens.Red, 0, 0, cx - 1, cy - 1);
    }
}

DrawRectangleに渡す最後の2つの引数として、1を減じないでcxとcyをそのまま渡してみてください。DrawRectangleにClientRectangleプロパティを渡したときと同じように、描画される矩形の右端と下端はクライアント領域から見えなくなるはずです。

Graphicsクラスは、複数の矩形を描画するための次のような2つのメソッドも公開しています。

▼ GraphicsのDrawRectanglesメソッド

void DrawRectangles (Pen pen, Rectangle[] arect)
void DrawRectangles (Pen pen, RectangleF[] arectf)

これらの2つのメソッドは、DrawLinesほど有効ではありません。しかし、たとえば、rectfという名称のRectangleF構造体を定義し、この構造体を基に1個の矩形を描画したいとしましょう。このとき、DrawRectangleメソッドには、定義した構造体を受け取るオーバーロードメソッドがないとしたらどうでしょう。そのとおりです。このDrawRectanglesメソッドが使えるのです。

grfx.DrawRectangles(pen, new RectangleF[] { rectf });

15.10 | 汎用ポリゴン

数学的には、ポリゴンは三辺あるいは四辺で構成される閉じた図形です。たとえば、三角形、四辺形、五角形、六角形、八角形、九角形、十角形、十一角形、十二角形などがポリゴンです。Graphicsは、次のような2種類のポリゴンを描くメソッドを提供しています。

▼ GraphicsのDrawPolygonメソッド

void DrawPolygon(Pen pen, Point[] point)
void DrawPolygon(Pen pen, PointF[] point)

DrawPolygonメソッドは、機能的にはDrawLinesにきわめて似ていますが、描画される図形は自動的に直線によって閉じられます。つまり、開始点と終了点が接続されるわけです。たとえば、次のようなPoint構造体配列を考えてみましょう。

Point[] apt = {new Point (0, 0), new Point (50, 100), new Point (100, 0)};

この配列を次のように使用すると、2つの直線が引かれ、V字となります。

grfx.DrawLines(pen, apt);

しかし、DrawPolygonメソッドに渡すと、三角形が描かれます。

grfx.DrawPolygon(pen, apt);

場面によっては、DrawLinesとDrawLineを呼び出してDrawPolygonメソッド呼び出しをシミュレートしてもよいでしょう。

DrawLines(pen, apt); DrawLine(pen, apt[apt.Length-1], apt[0]);

しかし、終了点と連結点を持つ幅のある直線を処理している場合には、DrawPolygonと同じような効果を得ることはできません。この点は理解しておいてください。

15.11 | より簡単な楕円描画

既に学習したように、DrawLinesを使用すれば、楕円を描くことができます。ところが、より簡単な方法があるのです。DrawEllipseという名称の専用メソッドを使用することになりますが、渡す引数はDrawRectangleと同じなのです。

▼ GraphicsのDrawEllipseメソッド

void DrawEllipse(Pen pen, int x, int y, int cx, int cy)
void DrawEllipse(Pen pen, float, x, float y, float cx, float cy)
void DrawEllipse(Pen pen, Rectangle rect)
void DrawEllipse(Pen pen, RectangleF rectf)

DrawEllipseメソッドとDrawRectangleメソッド間には整合性があります。たとえば、3の幅と高さを持つ楕円は次のように描かれます。

Dd297678.05-09(ja-jp,MSDN.10).gif

1の幅と高さは、固定2ピクセルの正方形として描かれます。

これは、DrawRectangleと同じように、幅cxピクセルで高さcyピクセルの領域に楕円を描くには、幅と高さを1つ減らす必要があることを意味します。

//----------------------------------------------
// ClientEllipse.cs (C) 2001 by Charles Petzold
//----------------------------------------------
using System;
using System.Drawing;
using System.Windows.Forms;
class ClientEllipse: PrintableForm
{
    public new static void Main()
    {
        Application.Run(new ClientEllipse());
    }
    public ClientEllipse()
    {
        Text = "Client Ellipse";
    }
    protected override void DoPage(Graphics grfx, Color clr, int cx, int cy)
    {
        grfx.DrawEllipse(new Pen(clr), 0, 0, cx - 1, cy - 1);
    }
}

DrawEllipseに渡す最後の2つの引数がcxとcyに設定されている場合は、右側と下側の縁がわずかに欠けてしまいます。

15.12 | 円弧と円グラフ

円弧は、少なくともWindowsフォームに関する限り、楕円の一部分です。円弧を定義するには、楕円を定義するために必要な情報と同じ情報が必要になり、さらに、円弧の開始位置と終了位置を明示する必要もあります。このため、4種類のDrawArcメソッドバージョンを使用するときには、DrawEllipseメソッドに渡す引数のほかに、2つの引数を追加指定する必要があります。

▼ GraphicsのDrawArcメソッド

void DrawArc(Pen pen, int x, int y, int cx, int cy, int iAngleStart, int iAngleSweep)
void DrawArc(Pen pen, float x, float y, float cx, float cy, float fAngleStart, float fAngleSweep)
void DrawArc(Pen pen, Rectangle rect, float fAngleStart, float fAngleSweep)
void DrawArc(Pen pen, RectangleF rectf, float fAngleStart, float fAngleSweep)

追加指定する2つの引数は、円弧の開始点と長さを決める角度です。角度は、負の値にも正の値にもなれます。つまり、角度は、以下のように時計の3時の位置から時計回りに計測される度数といえます(方向を持っている点に注意してください)。

Dd297678.05-10(ja-jp,MSDN.10).gif

次に紹介するサンプルプログラムは、点線の円周を持つ楕円を描きます。点線の線の1つは10°の円弧で、線と線の間の空きは5°の円弧です。

//----------------------------------------------
// DashedEllipse.cs (C) 2001 by Charles Petzold
//----------------------------------------------
using System;
using System.Drawing;
using System.Windows.Forms;
class DashedEllipse: PrintableForm
{
    public new static void Main()
    {
        Application.Run(new DashedEllipse());
    }
    public DashedEllipse()
    {
        Text = "Dashed Ellipse Using DrawArc";
    }
    protected override void DoPage(Graphics grfx, Color clr, int cx, int cy)
    {
        Pen        pen = new Pen(clr);
        Rectangle rect = new Rectangle(0, 0, cx - 1, cy - 1);
        for (int iAngle = 0; iAngle < 360; iAngle += 15)
             grfx.DrawArc(pen, rect, iAngle, 10);
    }
}

実行画面は次のようになります。

Dd297678.05-11(ja-jp,MSDN.10).gif

Win32 APIには、RoundRectという名称の関数があり、丸みを帯びた角を持つ矩形を描画する機能を提供しています。この関数は、矩形の左上隅と右下隅、そして角を切り取るための楕円の幅と高さを設定する4個の引数を受け取ります。

Graphicsクラスは、RoundRectメソッドを持っていませんが、このAPIをシミュレートする各種のメソッドを備えています。

//------------------------------------------
// RoundRect.cs (C) 2001 by Charles Petzold
//------------------------------------------
using System;
using System.Drawing;
using System.Windows.Forms;
class RoundRect: PrintableForm
{
    public new static void Main()
    {
        Application.Run(new RoundRect());
    }
    public RoundRect()
    {
        Text = "Rounded Rectangle";
    }
    protected override void DoPage(Graphics grfx, Color clr, int cx, int cy)
    {
        RoundedRectangle(grfx, Pens.Red,
                         new Rectangle(0, 0, cx - 1, cy - 1),
                         new Size(cx / 5, cy / 5));
    }
    void RoundedRectangle(Graphics grfx, Pen pen, Rectangle rect, Size size)
    {
        grfx.DrawLine(pen, rect.Left  + size.Width / 2, rect.Top,
                           rect.Right - size.Width / 2, rect.Top);
        grfx.DrawArc(pen, rect.Right - size.Width, rect.Top,
                          size.Width, size.Height, 270, 90);
        grfx.DrawLine(pen, rect.Right, rect.Top + size.Height / 2,
                           rect.Right, rect.Bottom - size.Height / 2);
        grfx.DrawArc(pen, rect.Right  - size.Width,
                          rect.Bottom - size.Height,
                          size.Width, size.Height, 0, 90);
        grfx.DrawLine(pen, rect.Right - size.Width / 2, rect.Bottom,
                           rect.Left  + size.Width / 2, rect.Bottom);
        grfx.DrawArc(pen, rect.Left, rect.Bottom - size.Height,
                          size.Width, size.Height, 90, 90);
        grfx.DrawLine(pen, rect.Left, rect.Bottom - size.Height / 2,
                           rect.Left, rect.Top + size.Height / 2);
        grfx.DrawArc(pen, rect.Left, rect.Top,
                          size.Width, size.Height, 180, 90);
    }
}

このサンプルプログラム内にあるRoundRectangleメソッドは、矩形の位置と大きさを明示するRectangle引数と、丸みのある角を描く楕円の幅と高さを指定するSize引数を受け取っています。私は、このメソッドを作成するとき、DrawRectangleによって描かれる矩形の大きさと整合性をとろうと考えました。つまり、幅と高さをクライアント領域の幅と高さより1ピクセル少なくなるように設定して、図形全体がきちんと見えるようにしたわけです。このメソッド内では、DrawLineとDrawArcを交互に呼び出しています。ソースコードを見ればわかるように、図形の最上端位置から直線描画を開始し、時計回りに処理を行っています。

Dd297678.05-12(ja-jp,MSDN.10).gif

しかし、私は、このテクニックを丸みを帯びた角を持つ矩形を描画する汎用的な手段として勧めるのには、ためらいを感じています。それぞれの直線と円弧は、DrawLineとDrawArcを個別に呼び出すことにより描かれます。これは、8個の図形がそれぞれ連結されているのではなく、終了点を持っていることを意味します。直線と曲線を結び付け、1つの図形を描く正しい方法は、グラフィックパスを使用することです。詳細については第20章で詳述します。

DrawPieメソッドは、DrawArcメソッドと同じ引数を受け取りますが、描かれる直線の方向が異なります。DrawPieメソッドは、円弧の終了点から楕円の中心に向かって直線を描き、閉じた領域を作り出します。

▼ GraphicsのDrawPieメソッド

void DrawPie(Pen pen, int x, int y, int cx, int cy, int iAngleStart, int iAngleSweep)
void DrawPie(Pen pen, float x, float y, float cx, float cy, float fAngleStart, float fAngleSweep)
void DrawPie(Pen pen, Rectangle rect, float fAngleStart, float fAngleSweep)
void DrawPie(Pen pen, RectangleF rectf, float fAngleStart, float fAngleSweep)

円グラフは、ビジネスグラフィックでは定番ツールです。しかし、完全な円グラフを実現するコードを書くとなると、問題が出てきます。それは、3D効果を使ったりして、印象的なグラフに仕上げる場面で出てきます。実は、DrawPieメソッドは、このような便利な機能を提供していないのです。次に紹介するサンプルプログラムは、私が用意した配列値を使って円グラフを描画します。配列値はフィールドとして保存されています。

//-----------------------------------------
// PieChart.cs (C) 2001 by Charles Petzold
//-----------------------------------------
using System;
using System.Drawing;
using System.Windows.Forms;
class PieChart: PrintableForm
{
    int[] aiValues = { 50, 100, 25, 150, 100, 75 };
    public new static void Main()
    {
        Application.Run(new PieChart());
    }
    public PieChart()
    {
        Text = "Pie Chart";
    }
    protected override void DoPage(Graphics grfx, Color clr, int cx, int cy)
    {
        Rectangle rect   = new Rectangle(50, 50, 200, 200);
        Pen       pen    = new Pen(clr);
        int       iTotal = 0;
        float     fAngle = 0, fSweep;
        foreach(int iValue in aiValues)
            iTotal += iValue;
        foreach(int iValue in aiValues)
        {
            fSweep = 360f * iValue / iTotal;
            DrawPieSlice(grfx, pen, rect, fAngle, fSweep);
            fAngle += fSweep;
        }
    }
    protected virtual void DrawPieSlice(Graphics grfx, Pen pen,
                                        Rectangle rect,
                                        float fAngle, float fSweep)
    {
        grfx.DrawPie(pen, rect, fAngle, fSweep);
    }
}

DoPageメソッド内のRectangle定義に注意してください。絶対座標値と大きさが使用されています。このようなコーディングは、本章の他のプログラムには見られません。このテクニックを使用した理由は、楕円の円グラフは人の目を引くほどの魅力を持っていないことです。DoPageメソッドは、値配列の合計を求め、各値をその合計で割り、出てきた商に360度を掛けて、最終的に個々の分割角度を求めています。実行結果は次のようになります。

Dd297678.05-13(ja-jp,MSDN.10).gif

ここで一言述べておきますが、この円グラフは私の最高傑作ではありません。ソースコードをじっくり見るとわかりますが、PieChart内の仮想関数内にDrawPie呼び出しを分離しています。私には先見の明があったわけです。つまり、仮想関数はオーバーライド可能ですから、その一部を改善すれば、さらに立派なプログラムができます。次に紹介するBetterPieChartサンプルプログラムは改良版です。

//-----------------------------------------------
// BetterPieChart.cs (C) 2001 by Charles Petzold
//-----------------------------------------------
using System;
using System.Drawing;
using System.Windows.Forms;
class BetterPieChart: PieChart
{
    public new static void Main()
    {
        Application.Run(new BetterPieChart());
    }
    public BetterPieChart()
    {
        Text = "Better " + Text;
    }
    protected override void DrawPieSlice(Graphics grfx, Pen pen,
                                         Rectangle rect,
                                         float fAngle, float fSweep)
    {
        float fSlice = (float)(2 * Math.PI * (fAngle + fSweep / 2) / 360);
        rect.Offset((int)(rect.Width  / 10 * Math.Cos(fSlice)),
                    (int)(rect.Height / 10 * Math.Sin(fSlice)));
        base.DrawPieSlice(grfx, pen, rect, fAngle, fSweep);
    }
}

プログラム内で用意したfSlice変数は、ラジアンに変換される分割の中心角度を保持しています。私はこの変数値を使って、個々の分割パイの大きさと位置を定義する矩形に適応されるxとyオフセット値を算出しています。個々のパイは、中心から少しずれた位置に表示されるため、相互に切り離された感じに見えます。

Dd297678.05-14(ja-jp,MSDN.10).gif

このプログラムは少し改良されたとはいえ、Graphicsクラスが提供する直線描画メソッドのすべてを使い切っているわけではありません。実は、DrawBezier、BrawBeziers、DrawCurve、DrawClosedCurveメソッドなどの専用メソッドを使用すれば、これまで以上の完成度を誇る曲線を描くことができます。詳細については第18章で取り上げます。直線と曲線の集合を組み合わせることにより、グラフィックパスを構成し、DrawPathメソッドを使用して、そのパスを描画することができます。このパスを応用した描画処理については、第20章で取り上げます。

15.13 | 矩形、楕円、円グラフの塗りつぶし

これまで取り上げてきたGraphicsメソッドのいくつかは、指定したペンで領域輪郭を描き、その領域の内部を塗りつぶすようなことはしていませんが、閉じた領域を定義していました。閉じた領域を定義するDrawプレフィックスを持つこれまでのメソッドは、内部を塗りつぶす機能を持つFillで開始するメソッドを持っています。これらのメソッドには、内部を塗りつぶすために使用される第1の引数としてブラシを渡します。

次に4種類のFillRectangleメソッドバージョンを示します。

▼ GraphicsのFillRectangleメソッド

void FillRectangle(Brush brush, int x, int y, int cx, int cy)
void FillRectangle(Brush brush, float x, float y, float cx, float cy)
void FillRectangle(Brush brush, Rectangle rect)
void FillRectangle(Brush brush, RectangleF rectf)

これらのメソッドが描く図形の幅と高さは、引数に指定される幅と高さと同じになります。たとえば、幅と高さが3である場合、FillRectangle呼び出しは、ピクセル位置(x, y)を左上隅とする3ピクセルの正方形ブロックを描きます。描画と同時に特定領域を塗りつぶしたい場合には、FillRectangleをまず呼び出し、いずれかの直線を上書きしないように引数を設定する必要があります。

Graphicsクラスは、さらに次のような2つのFillRectanglesメソッドを持っています。

▼ GraphicsのFillRectanglesメソッド

void FillRectangles(Brush brush, Rectangle[] arect)
void FillRectangles(Brush brush, RectangleF[] arect)

これらの2種類のFillRectanglesメソッドは、FillRectangleメソッドを複数回呼び出すのと同じ効果をもたらします。

さらに、4種類のFillEllipseメソッドが用意されています。これらのメソッドは、DrawEllipseメソッドと同じ引数を受け取ります。

▼ GraphicsのFillEllipseメソッド

void FillEllipse(Brush brush, int x, int y, int cx, int cy)
void FillEllipse(Brush brush, float x, float y, float cx, float cy)
void FillEllipse(Brush brush, Rectangle rect)
void FillEllipse(Brush brush, RectangleF rectf)

FillEllipseの動作は、これまで紹介してきたどのメソッドとも異なります。たとえば、(0, 0)という位置を指定し、幅と高さが20ピクセルの矩形を描くとしましょう。既に触れたように、DrawEllipseは0~20のピクセルを覆うような図形を描きます。つまり、幅と高さは実質的には21ピクセルになります。

基本的には、FillEllipseがカバーする領域は、水平と垂直の両方向で1~19となり、実質19ピクセルの図形となります。ここで私は、「基本的に」と述べていることに注意してください。これは、ピクセル位置0となる左端には、常に1ピクセル存在するということなのです。DrawEllipseが描く楕円とFillEllipseが塗りつぶす領域間には、オーバーラップが発生します。楕円を描いてその内部を塗りつぶし、かつ輪郭も描きたい場合、DrawEllipseの前にFillEllipseを呼び出すようにしてください。

FillPieという名称の3種類のメソッドを次に示します。

▼ GraphicsのFillPieメソッド

void FillPie(Brush brush, int x, int y, int cx, int cy, int iAngle, int iSweep)
void FillPie(Brush brush, float x, float y, float cx, float cy, float fAngle, float fSweep)
void FillPie(Brush brush, Rectangle rect, float fAngle, float fSweep)

15.14 | 1ピクセルのずれ

矩形と楕円を処理するすべてのメソッドの説明は終了しましたから、1ピクセルエラーの回避という角度から、それらのメソッドを比較してみましょう。次に紹介するサンプルプログラムは、DrawRectangle、DrawRectangles、DrawEllipse、FillRectangle、FillRectangles、およびFillEllipseの6種類のメソッドを使って、4×4ピクセルの矩形と楕円を描画します。

//--------------------------------------------
// FourByFours.cs (C) 2001 by Charles Petzold
//--------------------------------------------
using System;
using System.Drawing;
using System.Windows.Forms;
class FourByFours: PrintableForm
{
    public new static void Main()
    {
        Application.Run(new FourByFours());
    }
    public FourByFours()
    {
        Text = "Four by Fours";
    }
    protected override void DoPage(Graphics grfx, Color clr, int cx, int cy)
    {
        Pen   pen   = new Pen(clr);
        Brush brush = new SolidBrush(clr);
        grfx.DrawRectangle(pen, new Rectangle(2, 2, 4, 4));
        grfx.DrawRectangles(pen, new Rectangle[]
                                        {new Rectangle(8, 2, 4, 4)});
        grfx.DrawEllipse(pen, new Rectangle(14, 2, 4, 4));
        grfx.FillRectangle(brush, new Rectangle(2, 8, 4, 4));
        grfx.FillRectangles(brush, new Rectangle[]
                                        {new Rectangle(8, 8, 4, 4)});
        grfx.FillEllipse(brush, new Rectangle(14, 8, 4, 4));
    }
}

サンプルプログラムを実行すると、次のような画面が表示されます。各図形の大きさがわかるように、拡大表示しています。

Dd297678.05-15(ja-jp,MSDN.10).gif

このように、DrawRectangle、DrawRectangles、DrawEllipseメソッド間には描画の整合性があります。つまり、描かれる図形は、指定された大きさより、幅と高さの両方で1ピクセル分増えています。FillEllipseメソッドは、左側の小さな突起を別にすれば、FillRectangleとFillRectanglesにより描かれる4×4ピクセルの図形に比べて、幅と高さとも1ピクセル少ない図形を描画しています。

15.15 | ポリゴンと塗りつぶしモード

ここで、本章で紹介する最後のメソッドであるFillPolygonを説明しておきます。ポリゴンと他の塗りつぶし領域との違いは、ポリゴンを定義する直線が相互に交差し、オーバーラップしていることです。これは、ポリゴン内部が2種類の方法で塗りつぶせることになり、事情がより複雑になることを意味します。FillPolygonには、次のような4種類のバージョンが用意されています。

▼ GraphicsのFillPolygonメソッド

void FillPolygon(Brush brush, Point[] apt)
void FillPolygon(Brush brush, PointF[] apt)
void FillPolygon(Brush brush, Point[] apt, FillMode fm)
void FillPolygon(Brush brush, PointF[] apt, FillMode fm)

このように、すべてのメソッドはDrawPolygonメソッドに似ていますが、任意指定の引数が含まれていることに注意してください。FillModeは、System.Drawing.Drawing2D名前空間に定義されている列挙体であり、次のような2つの値を取ります。

▼ FillMode列挙体

メンバ コメント
Alternate 0 デフォルト。塗りつぶし領域と非塗りつぶし領域を交互に生成する。
Winding 1 内部のほぼすべての領域を塗りつぶす。

塗りつぶしモードは、ポリゴンを定義する直線がオーバーラップしているときにのみ有効となります。つまり、このモードは、閉じた領域内のどこを塗りつぶし、どこを塗りつぶさないかを決定しているわけです。塗りつぶしモードを指定しない場合、FillMode.Alternateがデフォルトで有効となります。その場合、閉じた領域は、外界との間に奇数個の境界を持っている場合にのみ塗りつぶされます。

それではここで、5つの頂点で構成される星を描く、古典的なサンプルを紹介しましょう。内部の五角形は、Windingモード選択時には塗りつぶされますが、Alternateモード選択時には塗りつぶされません。

//---------------------------------------------------
// FillModesClassical.cs (C) 2001 by Charles Petzold
//---------------------------------------------------
using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms;
class FillModesClassical: PrintableForm
{
    public new static void Main()
    {
        Application.Run(new FillModesClassical());
    }
    public FillModesClassical()
    {
        Text = "Alternate and Winding Fill Modes (The Classical Example)";
        ClientSize = new Size(2 * ClientSize.Height, ClientSize.Height);
    }
    protected override void DoPage(Graphics grfx, Color clr, int cx, int cy)
    {
        Brush   brush = new SolidBrush(clr);
        Point[] apt   = new Point[5];
        for (int i = 0; i < apt.Length; i++)
        {
            double dAngle = (i * 0.8 - 0.5) * Math.PI;
            apt[i] = new Point(
                            (int)(cx *(0.25 + 0.24 * Math.Cos(dAngle))),
                            (int)(cy *(0.50 + 0.48 * Math.Sin(dAngle))));
        }
        grfx.FillPolygon(brush, apt, FillMode.Alternate);
        for (int i = 0; i < apt.Length; i++)
             apt[i].X += cx / 2;
        grfx.FillPolygon(brush, apt, FillMode.Winding);
    }
}

最初のforループでは、クライアント領域の左半分に表示される星を構成する5個のピクセル位置を定義しています。塗りつぶしモードはAlternateとなっています。2番目のforループでは、星の表示位置をクライアント領域の右半分にシフトし、Windingモードで内部を塗りつぶしています。

Dd297678.05-16(ja-jp,MSDN.10).gif

ほとんどの場合、Windingモードを指定すると、すべての閉じた領域が塗りつぶされます。しかし、事情はそれほど単純ではありません。実は、いくつかの例外があるのです。Windingモードでの閉じた領域の塗りつぶし状態を知りたい場合、その領域内のある点から外界まで引かれた直線を想像してみるとよいでしょう。その想像上の直線が奇数の境界線と交差している場合、その領域は間違いなく、Alternateモードと同じように塗りつぶされます。ところが、交差する境界線が偶数の場合、塗りつぶされることもあれば、塗りつぶされないということもあります。さらに、想像上の直線を基準にして、ある一方の方向に伸びる境界線数と別の方向に伸びる境界線数が等しくない場合、領域は塗りつぶされます。

ちょっとした工夫をすれば、塗りつぶされない内部を持つ図形をWindingモードで描くことができます。サンプルプログラムを次に紹介しておきます。

//------------------------------------------------
// FillModesOddity.cs (C) 2001 by Charles Petzold
//------------------------------------------------
using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms;
class FillModesOddity: PrintableForm
{
    public new static void Main()
    {
        Application.Run(new FillModesOddity());
    }
    public FillModesOddity()
    {
        Text = "Alternate and Winding Fill Modes (An Oddity)";
        ClientSize = new Size(2 * ClientSize.Height, ClientSize.Height);
    }
    protected override void DoPage(Graphics grfx, Color clr, int cx, int cy)
    {
        Brush    brush = new SolidBrush(clr);
        PointF[] aptf = { new PointF(0.1f, 0.7f), new PointF(0.5f, 0.7f),
                          new PointF(0.5f, 0.1f), new PointF(0.9f, 0.1f),
                          new PointF(0.9f, 0.5f), new PointF(0.3f, 0.5f),
                          new PointF(0.3f, 0.9f), new PointF(0.7f, 0.9f),
                          new PointF(0.7f, 0.3f), new PointF(0.1f, 0.3f)};
        for (int i = 0; i < aptf.Length; i++)
        {
            aptf[i].X *= cx / 2;
            aptf[i].Y *= cy;
        }
        grfx.FillPolygon(brush, aptf, FillMode.Alternate);
        for (int i = 0; i < aptf.Length; i++)
             aptf[i].X += cx / 2;
        grfx.FillPolygon(brush, aptf, FillMode.Winding);
    }
}

実行結果は次のようになります。

Dd297678.05-17(ja-jp,MSDN.10).gif

この他に、3種類の塗りつぶしメソッドがあります。第13章ではFillClosedCurveメソッド、第20章ではFillRegionとFillPathメソッドを取り上げます。