DOBON.NET DOBON.NETプログラミング掲示板過去ログ

DrawPathとFillPathの結果の違いについて教えて下さい

環境/言語:[XP / C# / Framework 2.0]
分類:[.NET]

いつも参考にさせて頂いております、"まこと"と申します。

同一の GraphicsPath を用いた DrawPath と FillPath の処理結果の違いについて
ご存知の方がいらっしゃいましたらご教授願います。

もっか角の丸いフォームを作り、更に縁取りをしたいのですが、GraphicsPath を
使って以下の様な処理記述しても、4隅の円弧がそれぞれ微妙に異なっており
上下・左右に反転させてもピッタリと円弧が合致しません。
※Rectangleを微調整しても円弧は揃いませんでした。

尚、Win32 APIの CreateRoundRectRgn でリージョンを作成し、SetWindowRgn で
フォームの形状を変更し、FrameRgn を使って縁取りをすると目的の結果を得る事
が出来るのですが、.NET Framework で同一の結果を得るにはどの様にすれば
宜しいのでしょうか?

以上 宜しくお願い致します。


【検証用コード】

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
    }

    protected override void OnPaint(PaintEventArgs e)
    {
        base.OnPaint(e);

        float w = 45.0f;
        Rectangle rect = new Rectangle(0, 0, this.ClientSize.Width, this.ClientSize.Height);

        using (System.Drawing.Drawing2D.GraphicsPath gp = new System.Drawing.Drawing2D.GraphicsPath())
        {
            gp.StartFigure();
            gp.AddArc(rect.Right - w, rect.Bottom - w, w, w, 0.0f, 90.0f);  // 右下
            gp.AddArc(rect.Left, rect.Bottom - w, w, w, 90.0f, 90.0f);      // 左下
            gp.AddArc(rect.Left, rect.Top, w, w, 180.0f, 90.0f);            // 左上
            gp.AddArc(rect.Right - w, rect.Top, w, w, 270.0f, 90.0f);       // 右上
            gp.CloseFigure();

            // 実際の結果(塗り潰し) ※分かりやすいように半透明化してます
            using (SolidBrush brs = new SolidBrush(Color.FromArgb(128, Color.Blue)))
            {
                e.Graphics.FillPath(brs, gp);
            }
        }

        using (System.Drawing.Drawing2D.GraphicsPath gp = new System.Drawing.Drawing2D.GraphicsPath())
        {
            gp.StartFigure();
            gp.AddArc(rect.Right - w - 1, rect.Bottom - w - 1, w, w, 0.0f, 90.0f);  // 右下
            gp.AddArc(rect.Left, rect.Bottom - w - 1, w, w, 90.0f, 90.0f);          // 左下
            gp.AddArc(rect.Left, rect.Top, w, w, 180.0f, 90.0f);                    // 左上
            gp.AddArc(rect.Right - w - 1, rect.Top, w, w, 270.0f, 90.0f);           // 右上
            gp.CloseFigure();

            // 期待する形状(枠線のみ、これで塗り潰された領域が欲しい)
            e.Graphics.DrawPath(Pens.Red, gp);
        }
    }
}
■No23878に返信(まことさんの記事)
> 同一の GraphicsPath を用いた DrawPath と FillPath の処理結果の違いについて
> ご存知の方がいらっしゃいましたらご教授願います。

枠線のペン幅を 2 以上にする事はできませんか?
そうすれば、同じパスを使えるかも知れません。

ペンの太さが 1 の時、右下に 1 ドット大きく描画されてしまいますが、
ペンの太さが 2 以上なら、指定した領域内に描画されるようなので。

----------
Rectangle rect;
using (Pen p = new Pen(Color.Red, 2))
{
p.Alignment = PenAlignment.Inset;
e.Graphics.DrawRectangle(p, 0, 0, 10, 10);
e.Graphics.DrawRectangle(p, 12, 12, 10, 10);
}
using (Pen p = new Pen(Color.Blue, 1))
{
p.Alignment = PenAlignment.Inset;
e.Graphics.DrawRectangle(p, 0, 12, 10, 10);
}
using (Pen p = new Pen(Color.Green, 1))
{
p.Alignment = PenAlignment.Inset;
e.Graphics.DrawRectangle(p, 12, 0, 10, 10);
}
----------


private static GraphicsPath CreateRoundRect(Rectangle rect, float w)
{
GraphicsPath gp = new GraphicsPath();
gp.StartFigure();
gp.AddArc(rect.Right - w, rect.Bottom - w, w, w, 0.0f, 90.0f); // 右下
gp.AddArc(rect.Left, rect.Bottom - w, w, w, 90.0f, 90.0f); // 左下
gp.AddArc(rect.Left, rect.Top, w, w, 180.0f, 90.0f); // 左上
gp.AddArc(rect.Right - w, rect.Top, w, w, 270.0f, 90.0f); // 右上
gp.CloseFigure();
return gp;
}

protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
float w = 45.0f;

Rectangle rect = new Rectangle(0, 0, this.ClientSize.Width, this.ClientSize.Height);
rect.Inflate(-1, -1); // 領域確認のため、周囲に1ドット余白を空ける
using (GraphicsPath gp = CreateRoundRect(rect, w))
using (SolidBrush brs = new SolidBrush(Color.FromArgb(128, Color.Blue)))
using (Pen p = new Pen(Color.Red, 2)) // 1 にすると、重ならない部分が出てしまう…
{
p.Alignment = PenAlignment.Inset;
e.Graphics.DrawPath(p, gp);
e.Graphics.FillPath(brs, gp);
}
}
魔界の仮面弁士さん、返信ありがとうございます。

■No23879に返信(魔界の仮面弁士さんの記事)
> 枠線のペン幅を 2 以上にする事はできませんか?
> そうすれば、同じパスを使えるかも知れません。
> 
> ペンの太さが 1 の時、右下に 1 ドット大きく描画されてしまいますが、
> ペンの太さが 2 以上なら、指定した領域内に描画されるようなので。

ご提示頂いたコード・動作は確認致しました。

ペン幅については盲点でした…

四隅の円弧が、上下左右で対称で無い点については原因がわからないまま
なのですが、1ピクセルの境界線については以下の方法で実現出来ましたので
ソースを載せておきます。

四隅の円弧の問題については引き続き、ご存知の方がいらっしゃいましたら
宜しくお願い致します。


private static GraphicsPath CreateRoundRect(Rectangle rect, float w)
{
    GraphicsPath gp = new GraphicsPath();
    gp.StartFigure();
    gp.AddArc(rect.Right - w, rect.Bottom - w, w, w, 0.0f, 90.0f); // 右下
    gp.AddArc(rect.Left, rect.Bottom - w, w, w, 90.0f, 90.0f); // 左下
    gp.AddArc(rect.Left, rect.Top, w, w, 180.0f, 90.0f); // 左上
    gp.AddArc(rect.Right - w, rect.Top, w, w, 270.0f, 90.0f); // 右上
    gp.CloseFigure();
    return gp;
}

protected override void OnPaint(PaintEventArgs e)
{
    base.OnPaint(e);
    float w = 45.0f;

    Rectangle rect = new Rectangle(0, 0, this.ClientSize.Width, this.ClientSize.Height);
    rect.Inflate(-1, -1);
    using (GraphicsPath gp = CreateRoundRect(rect, w))
    using (SolidBrush brs = new SolidBrush(Color.Black))
    {
        rect.Inflate(-1, -1);
        gp.AddPath(CreateRoundRect(rect, w), true);
        e.Graphics.FillPath(brs, gp);
    }
}
ベーシックしかやってないけどさぁ。
特定の色を消すってのがあるけど。
バックグラウンドイメージをフォームのイメージにして、消したい色を指定すれば
簡単にできるようです。
vbヘルプの
あまり知られていない方法より。
# もし同じことをすでに試されていたら、スルーしてください

> 四隅の円弧が、上下左右で対称で無い点については原因がわからないまま
円弧部分の点の座標は小数になります。
(推測ですが)画面に表示するとき小数→整数に変換されます。
切捨て、四捨五入、切上げのいずれを使っても、
画面上の円弧の形状は点対称になりません。
これが原因だと思います。
そこで、円弧の左上は小数を切上げ、右下は小数を切捨てて1ドットずつ円弧を描いてみました。
私のPCではうまく描けました。まことさんのPCでも試してみてください。
# コードがVBですがご了承下さいm(_ _)m

Public Class GraphicsPathTestForm_ver3
    Inherits Form
    ' 角丸四角形の画像を保持する変数
    Dim m_image As Bitmap

    Protected Overrides Sub OnLoad(ByVal e As System.EventArgs)
        Me.Text = Me.GetType.Name
        Dim r As Rectangle = Me.ClientRectangle
        r.Inflate(-2, -2)
        Me.m_image = Me.CreateRoundBitmap(r, 30)
        MyBase.OnLoad(e)
    End Sub

    Protected Overrides Sub OnPaint(ByVal e As System.Windows.Forms.PaintEventArgs)
        e.Graphics.DrawImage(Me.m_image, 2, 2)
        MyBase.OnPaint(e)
    End Sub

    ' 角丸四角形の画像を返す
    Private Function CreateRoundBitmap(ByVal rect As Rectangle, ByVal radius As Integer) As Bitmap
        Dim borderColor As Color = Color.Red
        Dim bgColor As Color = Color.Blue

        Dim bmp As New Bitmap(rect.Width, rect.Height)
        Using g As Graphics = Graphics.FromImage(bmp)
            ' bmpの色を初期化
            g.Clear(SystemColors.Control)
            ' 直線
            Using p As New Pen(borderColor)
                Dim x0 As Integer = radius
                Dim y0 As Integer = radius
                Dim x1 As Integer = rect.Width - 1 - radius
                Dim y1 As Integer = rect.Height - 1 - radius
                g.DrawLine(p, x0, 0, x1, 0)
                g.DrawLine(p, 0, y0, 0, y1)
                g.DrawLine(p, x0, rect.Height - 1, x1, rect.Height - 1)
                g.DrawLine(p, rect.Width - 1, y0, rect.Width - 1, y1)
            End Using
        End Using

        ' 右上円弧(小数点以下:x切り上げ、y切り捨て)
        Dim center As New Point(rect.Width - 1 - radius, radius)
        For i As Integer = 0 To 90
            Dim x As Integer = CInt(Math.Ceiling(center.X + radius * Math.Cos(Me.GetRadian(i))))
            Dim y As Integer = CInt(Math.Floor(center.Y - radius * Math.Sin(Me.GetRadian(i))))
            bmp.SetPixel(x, y, borderColor)
        Next
        ' 左上円弧(小数点以下:x、y共切り捨て)
        center = New Point(radius, radius)
        For i As Integer = 90 To 180
            Dim x As Integer = CInt(Math.Floor(center.X + radius * Math.Cos(Me.GetRadian(i))))
            Dim y As Integer = CInt(Math.Floor(center.Y - radius * Math.Sin(Me.GetRadian(i))))
            bmp.SetPixel(x, y, borderColor)
        Next
        ' 左下円弧(小数点以下:x切り捨て、y切り上げ)
        center = New Point(radius, rect.Height - 1 - radius)
        For i As Integer = 180 To 270
            Dim x As Integer = CInt(Math.Floor(center.X + radius * Math.Cos(Me.GetRadian(i))))
            Dim y As Integer = CInt(Math.Ceiling(center.Y - radius * Math.Sin(Me.GetRadian(i))))
            bmp.SetPixel(x, y, borderColor)
        Next
        ' 右下円弧(小数点以下:x、y共切り上げ)
        center = New Point(rect.Width - 1 - radius, rect.Height - 1 - radius)
        For i As Integer = 270 To 360
            Dim x As Integer = CInt(Math.Ceiling(center.X + radius * Math.Cos(Me.GetRadian(i))))
            Dim y As Integer = CInt(Math.Ceiling(center.Y - radius * Math.Sin(Me.GetRadian(i))))
            bmp.SetPixel(x, y, borderColor)
        Next

        ' 塗りつぶし
        For y As Integer = 0 To bmp.Height - 1
            Dim flg As Flag = Flag.Outside
            For x As Integer = 0 To bmp.Width - 1
                Dim c As Color = bmp.GetPixel(x, y)
                If Me.Color_Equals(c, borderColor) = True Then
                    If flg = Flag.Outside Then
                        ' 左側境界線に入った
                        flg = Flag.FirstBorder
                    ElseIf flg = Flag.Inside Then
                        ' 右側境界線に入った
                        Exit For
                    End If
                ElseIf Me.Color_Equals(c, SystemColors.Control) = True Then
                    If flg = Flag.FirstBorder Then
                        If x >= bmp.Width \ 2 Then
                            ' 上下境界線の場合は、ここに来る
                            Exit For
                        Else
                            ' 領域内に入った
                            flg = Flag.Inside
                        End If
                    End If
                    If flg = Flag.Inside Then
                        bmp.SetPixel(x, y, bgColor)
                    End If
                End If
            Next
        Next

        Return bmp

    End Function

    ' Bitmap塗りつぶし用フラグ
    Private Enum Flag
        Outside = 0
        FirstBorder
        Inside
    End Enum

    ' 以下、ヘルパーメソッド
    Private Function GetRadian(ByVal degree As Integer) As Double
        Return degree / 180 * Math.PI
    End Function

    Private Function Color_Equals(ByVal c1 As Color, ByVal c2 As Color) As Boolean
        Return c1.A = c2.A AndAlso c1.R = c2.R AndAlso _
           c1.G = c2.G AndAlso c1.B = c2.B
    End Function

End Class
  • 題名: Re[4]: どもども。
  • 著者: まこと
  • 日時: 2009/02/02 14:18:06
  • ID: 23883
  • この記事の返信元:
  • この記事への返信:
    • (なし)
  • ツリーを表示
レオ♪さん、返信ありがとうございます。

■No23881に返信(レオ♪さんの記事)
> 特定の色を消すってのがあるけど。
> バックグラウンドイメージをフォームのイメージにして、消したい色を指定すれば
> 簡単にできるようです。

単純にフォームの形状を変更するだけでしたら、その方法でも問題は無いと思うの
ですが、実際に作ろうとしているプログラムがデスクトップ等を拡大してフォーム
に表示する拡大鏡(ルーペ)プログラムなので、特定の色で枠を消す方法はいささか
不安が残ります。

またフォームのサイズを変更できる様にしたいので、矩形から計算して出せる様な
処理が望ましく、冒頭の様な処理を思いついた次第です。
H.K.R.さん、返信ありがとうございます。

■No23882に返信(H.K.R.さんの記事)
> 画面上の円弧の形状は点対称になりません。
> これが原因だと思います。

はい、私も少数→整数への丸め処理で何らかの誤差が生じているのではと考えは
したのですが、それを立証する方法すら思いつかずに.NETだけで綺麗な角の丸め
をする事を半ば諦め始めてます(~_~;

> 私のPCではうまく描けました。まことさんのPCでも試してみてください。

サンプルコードの動作は確認致しました、私の環境下でも対照的な円弧が描かれ
ておりました。

ただWinAPIでは1発で出来る事も、.NET環境ではこれ程までに大量のコードを
書かなければならないのかと思うと、昔の様にちょっと標準から逸脱したフォーム
や動作を実現するのは相当難しい事なのだと実感しました。

取りあえず角の丸めに関しては CreateRoundRectRgn を使って回避しようと思い
ます、今回の件で色々と勉強になりました、ありがとうございました。
解決済み!

DOBON.NET | プログラミング道 | プログラミング掲示板