• 追加された行はこの色です。
  • 削除された行はこの色です。
#title(文字列を描画したときの大きさを正確に計測する)

#navi(.NETプログラミング研究)

#contents

*文字列を描画したときの大きさを正確に計測する [#x9b591fe]

**.NET Frameworkで用意されている3つの方法 [#f8c110bc]

Graphics.DrawStringメソッドやTextRenderer.DrawTextメソッドを使って文字列を描画した時の大きさを計測するには、Graphics.MeasureStringメソッドやGraphics.MeasureCharacterRangesメソッド、あるいはTextRenderer.MeasureTextメソッドを使用します。しかしこれらのメソッドはどれも上下左右に余白を入れますので、通常は実際に描画される大きさよりも大きく計測されます。

Graphics.MeasureStringメソッドを呼び出す時にStringFormat.GenericTypographicを渡す、Graphics.MeasureCharacterRangesメソッドを使用する、TextRenderer.MeasureTextメソッドを呼び出す時にTextFormatFlags.NoPaddingを指定するといった方法で左右の余白をほぼなくすことができます。しかし、それでも正確な大きさは計測できません。

**実際に描画することで、正確に計測する [#fc0ee2f2]

実際に描画される大きさを正確に計測するには、やはり実際に描画してみるしかないようです。そのようにして計測する例が、「[[MeasureString and DrawString difference>http://stackoverflow.com/questions/3811916/measurestring-and-drawstring-difference]]」にあります。

これを参考にさせていただいて、私も以下のようなコードを書いてみました。このメソッドは、Graphics.DrawStringメソッドで文字列を描画した時に描画される範囲(大きさと位置)を計測しています。

**コード [#f43ce5e5]

#code(vbnet){{
'Imports System.Drawing

''' <summary>
''' Graphics.DrawStringで文字列を描画した時の大きさと位置を正確に計測する
''' </summary>
''' <param name="g">文字列を描画するGraphics</param>
''' <param name="text">描画する文字列</param>
''' <param name="font">描画に使用するフォント</param>
''' <param name="proposedSize">これ以上大きいことはないというサイズ。
''' できるだけ小さくすること。</param>
''' <param name="stringFormat">描画に使用するStringFormat</param>
''' <returns>文字列が描画される範囲。
''' 見つからなかった時は、Rectangle.Empty。</returns>
Public Shared Function MeasureStringPrecisely(g As Graphics, _
        text As String, font As Font, proposedSize As Size, _
        stringFormat As StringFormat) As Rectangle
    '解像度を引き継いで、Bitmapを作成する
    Dim bmp As New Bitmap(proposedSize.Width, proposedSize.Height, g)
    'BitmapのGraphicsを作成する
    Dim bmpGraphics As Graphics = Graphics.FromImage(bmp)
    'Graphicsのプロパティを引き継ぐ
    bmpGraphics.TextRenderingHint = g.TextRenderingHint
    bmpGraphics.TextContrast = g.TextContrast
    bmpGraphics.PixelOffsetMode = g.PixelOffsetMode
    '文字列の描かれていない部分の色を取得する
    Dim backColor As Color = bmp.GetPixel(0, 0)
    '実際にBitmapに文字列を描画する
    bmpGraphics.DrawString(text, font, Brushes.Black, _
        New RectangleF(0.0F, 0.0F, proposedSize.Width, proposedSize.Height), _
        stringFormat)
    bmpGraphics.Dispose()
    '文字列が描画されている範囲を計測する
    Dim resultRect As Rectangle = MeasureForegroundArea(bmp, backColor)
    bmp.Dispose()

    Return resultRect
End Function

''' <summary>
''' 指定されたBitmapで、backColor以外の色が使われている範囲を計測する
''' </summary>
Private Shared Function MeasureForegroundArea(bmp As Bitmap, _
        backColor As Color) As Rectangle
    Dim backColorArgb As Integer = backColor.ToArgb()
    Dim maxWidth As Integer = bmp.Width
    Dim maxHeight As Integer = bmp.Height

    Dim x As Integer
    Dim y As Integer

    '左側の空白部分を計測する
    Dim leftPosition As Integer = -1
    For x = 0 To maxWidth - 1
        For y = 0 To maxHeight - 1
            '違う色を見つけたときは、位置を決定する
            If bmp.GetPixel(x, y).ToArgb() <> backColorArgb Then
                leftPosition = x
                Exit For
            End If
        Next
        If 0 <= leftPosition Then
            Exit For
        End If
    Next
    '違う色が見つからなかった時
    If leftPosition < 0 Then
        Return Rectangle.Empty
    End If

    '右側の空白部分を計測する
    Dim rightPosition As Integer = -1
    For x = maxWidth - 1 To leftPosition + 1 Step -1
        For y = 0 To maxHeight - 1
            If bmp.GetPixel(x, y).ToArgb() <> backColorArgb Then
                rightPosition = x
                Exit For
            End If
        Next
        If 0 <= rightPosition Then
            Exit For
        End If
    Next
    If rightPosition < 0 Then
        rightPosition = leftPosition
    End If

    '上の空白部分を計測する
    Dim topPosition As Integer = -1
    For y = 0 To maxHeight - 1
        For x = leftPosition To rightPosition
            If bmp.GetPixel(x, y).ToArgb() <> backColorArgb Then
                topPosition = y
                Exit For
            End If
        Next
        If 0 <= topPosition Then
            Exit For
        End If
    Next
    If topPosition < 0 Then
        Return Rectangle.Empty
    End If

    '下の空白部分を計測する
    Dim bottomPosition As Integer = -1
    For y = maxHeight - 1 To topPosition + 1 Step -1
        For x = leftPosition To rightPosition
            If bmp.GetPixel(x, y).ToArgb() <> backColorArgb Then
                bottomPosition = y
                Exit For
            End If
        Next
        If 0 <= bottomPosition Then
            Exit For
        End If
        y -= 1
    Next
    If bottomPosition < 0 Then
        bottomPosition = topPosition
    End If

    '結果を返す
    Return New Rectangle(leftPosition, topPosition, _
        rightPosition - leftPosition, bottomPosition - topPosition)
End Function

Private Shared Function MeasureForegroundArea(bmp As Bitmap) As Rectangle
    Return MeasureForegroundArea(bmp, bmp.GetPixel(0, 0))
End Function
}}

#code(csharp){{
//using System.Drawing;

/// <summary>
/// Graphics.DrawStringで文字列を描画した時の大きさと位置を正確に計測する
/// </summary>
/// <param name="g">文字列を描画するGraphics</param>
/// <param name="text">描画する文字列</param>
/// <param name="font">描画に使用するフォント</param>
/// <param name="proposedSize">これ以上大きいことはないというサイズ。
/// できるだけ小さくすること。</param>
/// <param name="stringFormat">描画に使用するStringFormat</param>
/// <returns>文字列が描画される範囲。
/// 見つからなかった時は、Rectangle.Empty。</returns>
public static Rectangle MeasureStringPrecisely(Graphics g,
    string text, Font font, Size proposedSize, StringFormat stringFormat)
{
    //解像度を引き継いで、Bitmapを作成する
    Bitmap bmp = new Bitmap(proposedSize.Width, proposedSize.Height, g);
    //BitmapのGraphicsを作成する
    Graphics bmpGraphics = Graphics.FromImage(bmp);
    //Graphicsのプロパティを引き継ぐ
    bmpGraphics.TextRenderingHint = g.TextRenderingHint;
    bmpGraphics.TextContrast = g.TextContrast;
    bmpGraphics.PixelOffsetMode = g.PixelOffsetMode;
    //文字列の描かれていない部分の色を取得する
    Color backColor = bmp.GetPixel(0, 0);
    //実際にBitmapに文字列を描画する
    bmpGraphics.DrawString(text, font, Brushes.Black,
        new RectangleF(0f, 0f, proposedSize.Width, proposedSize.Height),
        stringFormat);
    bmpGraphics.Dispose();
    //文字列が描画されている範囲を計測する
    Rectangle resultRect = MeasureForegroundArea(bmp, backColor);
    bmp.Dispose();

    return resultRect;
}

/// <summary>
/// 指定されたBitmapで、backColor以外の色が使われている範囲を計測する
/// </summary>
private static Rectangle MeasureForegroundArea(Bitmap bmp, Color backColor)
{
    int backColorArgb = backColor.ToArgb();
    int maxWidth = bmp.Width;
    int maxHeight = bmp.Height;

    //左側の空白部分を計測する
    int leftPosition = -1;
    for (int x = 0; x < maxWidth; x++)
    {
        for (int y = 0; y < maxHeight; y++)
        {
            //違う色を見つけたときは、位置を決定する
            if (bmp.GetPixel(x, y).ToArgb() != backColorArgb)
            {
                leftPosition = x;
                break;
            }
        }
        if (0 <= leftPosition)
        {
            break;
        }
    }
    //違う色が見つからなかった時
    if (leftPosition < 0)
    {
        return Rectangle.Empty;
    }

    //右側の空白部分を計測する
    int rightPosition = -1;
    for (int x = maxWidth - 1; leftPosition < x; x--)
    {
        for (int y = 0; y < maxHeight; y++)
        {
            if (bmp.GetPixel(x, y).ToArgb() != backColorArgb)
            {
                rightPosition = x;
                break;
            }
        }
        if (0 <= rightPosition)
        {
            break;
        }
    }
    if (rightPosition < 0)
    {
        rightPosition = leftPosition;
    }

    //上の空白部分を計測する
    int topPosition = -1;
    for (int y = 0; y < maxHeight; y++)
    {
        for (int x = leftPosition; x <= rightPosition; x++)
        {
            if (bmp.GetPixel(x, y).ToArgb() != backColorArgb)
            {
                topPosition = y;
                break;
            }
        }
        if (0 <= topPosition)
        {
            break;
        }
    }
    if (topPosition < 0)
    {
        return Rectangle.Empty;
    }

    //下の空白部分を計測する
    int bottomPosition = -1;
    for (int y = maxHeight - 1; topPosition < y; y--)
    {
        for (int x = leftPosition; x <= rightPosition; x++)
        {
            if (bmp.GetPixel(x, y).ToArgb() != backColorArgb)
            {
                bottomPosition = y;
                break;
            }
        }
        if (0 <= bottomPosition)
        {
            break;
        }
    }
    if (bottomPosition < 0)
    {
        bottomPosition = topPosition;
    }

    //結果を返す
    return new Rectangle(leftPosition, topPosition,
        rightPosition - leftPosition, bottomPosition - topPosition);
}

private static Rectangle MeasureForegroundArea(Bitmap bmp)
{
    return MeasureForegroundArea(bmp, bmp.GetPixel(0, 0));
}
}}

**使用例 [#p1a49a00]

上記MeasureStringPreciselyメソッドを使用した例を示します。この例では、まずMeasureStringメソッドで文字列の大きさを計測して、赤い四角で描画しています。次に上記MeasureStringPreciselyメソッドで文字列が描画される範囲を計測して、青い四角で描画します。最後に文字列を黒で描画します。

なおこの例では、「PictureBox1」というPictureBoxコントロールが存在しているものとして、そこに描画した結果を表示しています。

#code(vbnet){{
'Imports System.Drawing

'表示する文字列
Dim s As String = "DOBON.NET"

'描画先とするImageオブジェクトを作成する
Dim canvas As New Bitmap(PictureBox1.Width, PictureBox1.Height)
'ImageオブジェクトのGraphicsオブジェクトを作成する
Dim g As Graphics = Graphics.FromImage(canvas)
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality
g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAlias

'フォントオブジェクトの作成
Dim fnt As New Font("Arial", 25, FontStyle.Italic)
'StringFormatオブジェクトの作成
Dim sf As New StringFormat()

'幅の最大値が1000ピクセルとして、文字列を描画するときの大きさを計測する
Dim stringSize As SizeF = g.MeasureString(s, fnt, 1000, sf)
'取得した文字列の大きさを使って四角を描画する
g.DrawRectangle(Pens.Red, 0, 0, stringSize.Width, stringSize.Height)

'より正確に大きさを計測する
Dim rect As Rectangle = MeasureStringPrecisely(g, s, fnt, canvas.Size, sf)
g.DrawRectangle(Pens.Blue, rect.Left, rect.Top, rect.Width, rect.Height)

'文字列を描画する
g.DrawString(s, fnt, Brushes.Black, 0, 0, sf)

'リソースを解放する
fnt.Dispose()
sf.Dispose()
g.Dispose()

'PictureBox1に表示する
PictureBox1.Image = canvas
}}

#code(csharp){{
//using System.Drawing;

//表示する文字列
string s = "DOBON.NET";

//描画先とするImageオブジェクトを作成する
Bitmap canvas = new Bitmap(PictureBox1.Width, PictureBox1.Height);
//ImageオブジェクトのGraphicsオブジェクトを作成する
Graphics g = Graphics.FromImage(canvas);
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAlias;

//フォントオブジェクトの作成
Font fnt = new Font("Arial", 25, FontStyle.Italic);
//StringFormatオブジェクトの作成
StringFormat sf = new StringFormat();

//幅の最大値が1000ピクセルとして、文字列を描画するときの大きさを計測する
SizeF stringSize = g.MeasureString(s, fnt, 1000, sf);
//取得した文字列の大きさを使って四角を描画する
g.DrawRectangle(Pens.Red, 0, 0, stringSize.Width, stringSize.Height);

//より正確に大きさを計測する
Rectangle rect = MeasureStringPrecisely(g, s, fnt, canvas.Size, sf);
g.DrawRectangle(Pens.Blue, rect.Left, rect.Top, rect.Width, rect.Height);

//文字列を描画する
g.DrawString(s, fnt, Brushes.Black, 0, 0, sf);

//リソースを解放する
fnt.Dispose();
sf.Dispose();
g.Dispose();

//PictureBox1に表示する
PictureBox1.Image = canvas;
}}

上記コードを実行した結果は、例えば以下の画像のようになります。

&ref(measurestring4.png);

**コードの説明 [#l3ebed98]

MeasureStringPreciselyメソッドで何をしているのか、簡単に説明します。

まず十分な大きさのBitmapオブジェクトを作成します。ここに文字列を描画して、大きさを計測しようという訳です。

Bitmapオブジェクトを作成する時に、文字列の描画先であるGraphicsオブジェクトをコンストラクタの引数に渡しています。このようにすると、Bitmapの解像度がGraphicsの解像度と同じになります。Graphicsを指定せずにコンストラクタを呼び出した場合は、後でBitmap.SetResolutionメソッドを使って解像度を設定することもできます。

次に、BitmapのGraphicsを作成しています。そして、Bitmapに文字列を描画する時にその大きさに影響を及ぼすと思われるプロパティの値を描画先のGraphicsのと同じにしておきます。ここでは、TextRenderingHint、TextContrast、PixelOffsetModeプロパティのみを同じにしています。もしかしたら他にも同期させるべきプロパティがあるかもしれませんし、今後の.NET Frameworkのバージョンアップでそのようなプロパティが新たに出てくるかもしれません。

準備が整ったところで、Bitmapに文字列を実際に描画します。しかしその前に、Bitmapの背景色を取得しておきます。この背景色は、どこに文字列が描画されているかを判断するのに使用します。

ここでいよいよ計測ですが、これは別のメソッド(MeasureForegroundAreaメソッド)で行っています。ここで行っていることは実に単純で、Bitmapの色を1ピクセルずつGetPixelメソッドで取得して、それが背景色と同じか調べているだけです。そして、背景色と違ったら、文字列が描画されていると判断します。この方法で左右上下の余白部分を計測して、文字列が描画されている範囲を決定します。

ちなみに色を比較する時は、Color.ToArgbメソッドでARGB値に変換した値を比較します。これは、単純にColor同士を比較すると、名前付きか、名前付きでないかなどのARGB値以外の要因で等しくないと判断されてしまう可能性があるためです。

**MeasureStringPreciselyメソッドのproposedSizeパラメータについて [#j910b606]

MeasureStringPreciselyメソッドのproposedSizeパラメータには、文字列の大きさがこれ以上大きいことはないというサイズを指定するようになっています。しかし上記の説明のように、この大きさでBitmapオブジェクトを作成し、さらにGetPixelメソッドでしらみつぶしに調べますので、あまりに大きな値を指定してしまうと大変なことになります。

それではどの位の大きさが適当なのかについては、Graphics.MeasureStringメソッドの結果を参考にするのが良いのではないかと思います。MeasureStringメソッドを呼び出す時にStringFormat.GenericTypographicを指定するのでなければ、十分な余白が取られますので、実際に描画される大きさより小さくなることはまずないでしょう。しかしその可能性も考慮して、さらに余白を追加してもよいかもしれません。

万全を期すならば、さらにMeasureStringPreciselyメソッドが返した結果を確認して、その大きさがproposedSizeと同じだったらproposedSizeをもう少し大きくしてもう一度MeasureStringPreciselyメソッドで計測するといったことも必要になるでしょう。

**最後に [#se73f406]

上記のメソッドはまだテストが十分とは言えませんので、不完全かもしれません。もしお気づきの点がございましたら、ご連絡ください。よろしくお願いいたします。

**参考 [#b8b2305c]

-[[文字列を描画したときの大きさを計測する>http://dobon.net/vb/dotnet/graphics/measurestring.html]]
-[[文字列を描画したときの大きさを計測する>https://dobon.net/vb/dotnet/graphics/measurestring.html]]

**コメント [#baef593e]
#comment

//これより下は編集しないでください
#pageinfo([[:Category/.NET]] [[:Category/ASP.NET]],2014-06-23 (月) 04:09:56,DOBON!,2014-06-23 (月) 04:13:41,DOBON!)


[ トップ ]   [ 新規 | 子ページ作成 | 一覧 | 単語検索 | 最終更新 | ヘルプ ]