2016/04/25

[C#]GraphicsPathから各点の情報を取得する

このエントリーをはてなブックマークに追加
GraphicsPathはおなじみのパスデータを扱うクラスです。
描画時には、GraphicsPathはそのまま、GraphicsのDrawPathメソッド/FillPathメソッドの引数として利用することができるので、中身を意識する必要はありませんね。

GraphicsPathの各点の情報は、PathDataプロパティで取得することができます。
後述しますが、GraphicsPath.PathPointsプロパティとGraphicsPath.PathTypesプロパティで点を拾っても同じなのですが、GraphicsPath.PathDataプロパティを使うほうが効率的です。

GraphicsPathのPathDataプロパティを使う

PathDataには、Points(PointFの配列)とTypes(byteの配列)を持ちます。PointsとTypesの配列数は必ず同じになります。
Pointsで座標点を表し、Typesでは点の種類を表します。Typeとして持つのは、直線であるか曲線であるかというような情報です。

0始点
1直線
3ベジェ曲線(3点セット)
128終点

0は始点。128は終点です。128はビットフラグですので、直線からの終点は129、曲線からの終点は131となります。
ベジェ曲線は、3つ続きで表現されます。直前の点から伸びるハンドル、次の点へ伸びるハンドル、次の点座標の3つが1つのセットとなります。

各点を描画するコード

理屈はさておき、各点を描画してみます。

ソースコード
文字のアウトラインパスに、イラストレータ風にいうところのパスとハンドルを描画してみます。
Form上にPictureBoxを置いて、Buttonをトリガーにしています。

// using System.Drawing;
private void button1_Click(object sender, EventArgs e) {
    string str = "永";

    Graphics g = this.pictureBox1.CreateGraphics();
    g.SmoothingMode = SmoothingMode.HighQuality;
    

    // 1.文字のアウトラインの描画
    float fontSize = 200f;
    FontFamily fontFamily = new FontFamily("メイリオ");

    GraphicsPath path = new GraphicsPath();
    StringFormat format = new StringFormat();
    path.AddString(str, fontFamily, (int)FontStyle.Bold, fontSize, Point.Empty, format);

    g.FillPath(Brushes.LightGray, path);


    // 2.パスデータをとりだす
    PathData pathData = path.PathData;

    PointF[] points = pathData.Points;
    byte[] types = pathData.Types;
    int pointCount = points.Length;
    
    // 3.パスとハンドルを描画する
    PointF prev = PointF.Empty;
    for (int i = 0; i < pointCount; i++) {
        PointF p = points[i];
        byte t = types[i];

        if ((t & 3) == 3) { // 曲線
            this.DrawHandle(g, prev, p);

            i++;
            p = points[i];
            this.DrawHandle(g, points[i + 1], p);

            i++;
            p = points[i];
            this.DrawPathPoint(g, p);
        } else if ((t & 1) == 1 || t == 0) { // 直線
            this.DrawPathPoint(g, p);
        }

        prev = p;
    }

    g.Dispose();
}

// ポイントのサイズ
private const float POINT_SIZE = 5.0f;

// パスを描画する
private void DrawPathPoint(Graphics g ,PointF p) {
    g.FillRectangle(Brushes.Red, p.X - POINT_SIZE * 0.5f, p.Y - POINT_SIZE * 0.5f, POINT_SIZE, POINT_SIZE);
}

// ハンドルを描画する
private void DrawHandle(Graphics g, PointF p1, PointF p2) {
    g.DrawLine(Pens.Blue, p1,p2);
    g.FillEllipse(Brushes.Blue, p2.X - POINT_SIZE * 0.5f, p2.Y - POINT_SIZE * 0.5f, POINT_SIZE, POINT_SIZE);
}

このコードを実行するとこのようになります。
いい感じですね。

コードの解説
上のコードは、1.文字のアウトラインの描画、2.パスデータをとりだし、3.パスとハンドルの描画という3つの工程から出来ています。

1.文字のアウトラインの描画
「永」という文字を描画するためのコードです。
GraphicsPathのAddStringメソッドで、座標の取得対象になるパス情報を生成しています。
生成したGraphicsPathは、GraphicsのFillPathメソッドで描画しています。

2.パスデータをとりだし
PathDataプロパティにより、パス情報を取得します。
パス情報は、座標点のPointsプロパティと、点の種類を示すTypesプロパティに分かれます。

3.パスとハンドルの描画
PathData.Typesにより点の種類を判別しながら、パスとハンドルを描画しています。
パスは四角、ハンドルは丸と線を描画しています。
曲線の座標点は、一つ前の点から伸びるハンドル、次の点から伸びるハンドル、次の点座標の3つが1セットとして記録されています。

点の種類を判別するために古典的なビット判定を使っています。

(t & 1) == 1 →直線
(t & 3) == 3 →曲線

注意点:GraphicsPath.PathData.PointsプロパティとGraphicsPath.PathPointsプロパティ

GraphicsPathにはPathDataプロパティの他に、GraphicsPath.PathPointsプロパティとGraphicsPath.PathTypesプロパティがあります。
GraphicsPath.PathData.Pointsプロパティも、GraphicsPath.PathPointsプロパティも、まったく同じなのですが、ここで注意してもらいたいことがあります。

プロパティというものの性質から、単に保持している値を拾ってくるだけという印象がありますが、GraphicsPathの内部では、動的に持っている値を必要に応じて座標点の配列として出力するようにできているようです。そのため、参照するだけでもものすごく重たいのです。

例えば、ループ内で次のようなコードを書いてしまうと、参照のたびに処理が入るので結構な非効率なのです。

for (int i = 0; i < pointCount; i++) {
    PointF p = path.PathPoints[i];
    byte t = path.PathTypes[i];
  
    // ... 処理
}

GraphicsPathのPathDataプロパティを拾っておいて、そこからPointsとTypesを参照するのがよいでしょう。

PathData pathData = path.PathData;
PointF[] points = pathData.Points;
byte[] types = pathData.Types;
int pointCount = points.Length;

for (int i = 0; i < pointCount; i++) {
    PointF p = points[i];
    byte t = types[i];
  
    // ... 処理
}

以上です。

0 件のコメント :

コメントを投稿