文字列を描画したときの大きさを正確に計測する †
.NET Frameworkで用意されている3つの方法 †
Graphics.DrawStringメソッドやTextRenderer.DrawTextメソッドを使って文字列を描画した時の大きさを計測するには、Graphics.MeasureStringメソッドやGraphics.MeasureCharacterRangesメソッド、あるいはTextRenderer.MeasureTextメソッドを使用します。しかしこれらのメソッドはどれも上下左右に余白を入れますので、通常は実際に描画される大きさよりも大きく計測されます。
Graphics.MeasureStringメソッドを呼び出す時にStringFormat.GenericTypographicを渡す、Graphics.MeasureCharacterRangesメソッドを使用する、TextRenderer.MeasureTextメソッドを呼び出す時にTextFormatFlags.NoPaddingを指定するといった方法で左右の余白をほぼなくすことができます。しかし、それでも正確な大きさは計測できません。
実際に描画することで、正確に計測する †
実際に描画される大きさを正確に計測するには、やはり実際に描画してみるしかないようです。そのようにして計測する例が、「MeasureString and DrawString difference」にあります。
これを参考にさせていただいて、私も以下のようなコードを書いてみました。このメソッドは、Graphics.DrawStringメソッドで文字列を描画した時に描画される範囲(大きさと位置)を計測しています。
コード †
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
| |
Public Shared Function MeasureStringPrecisely(g As Graphics, _
text As String, font As Font, proposedSize As Size, _
stringFormat As StringFormat) As Rectangle
Dim bmp As New Bitmap(proposedSize.Width, proposedSize.Height, g)
Dim bmpGraphics As Graphics = Graphics.FromImage(bmp)
bmpGraphics.TextRenderingHint = g.TextRenderingHint
bmpGraphics.TextContrast = g.TextContrast
bmpGraphics.PixelOffsetMode = g.PixelOffsetMode
Dim backColor As Color = bmp.GetPixel(0, 0)
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
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
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
| |
public static Rectangle MeasureStringPrecisely(Graphics g,
string text, Font font, Size proposedSize, StringFormat stringFormat)
{
Bitmap bmp = new Bitmap(proposedSize.Width, proposedSize.Height, g);
Graphics bmpGraphics = Graphics.FromImage(bmp);
bmpGraphics.TextRenderingHint = g.TextRenderingHint;
bmpGraphics.TextContrast = g.TextContrast;
bmpGraphics.PixelOffsetMode = g.PixelOffsetMode;
Color backColor = bmp.GetPixel(0, 0);
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;
}
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));
}
|
使用例 †
上記MeasureStringPreciselyメソッドを使用した例を示します。この例では、まずMeasureStringメソッドで文字列の大きさを計測して、赤い四角で描画しています。次に上記MeasureStringPreciselyメソッドで文字列が描画される範囲を計測して、青い四角で描画します。最後に文字列を黒で描画します。
なおこの例では、「PictureBox1」というPictureBoxコントロールが存在しているものとして、そこに描画した結果を表示しています。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
| |
Dim s As String = "DOBON.NET"
Dim canvas As New Bitmap(PictureBox1.Width, PictureBox1.Height)
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)
Dim sf As New StringFormat()
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.Image = canvas
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
| |
string s = "DOBON.NET";
Bitmap canvas = new Bitmap(PictureBox1.Width, PictureBox1.Height);
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 sf = new StringFormat();
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.Image = canvas;
|
上記コードを実行した結果は、例えば以下の画像のようになります。
コードの説明 †
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パラメータについて †
MeasureStringPreciselyメソッドのproposedSizeパラメータには、文字列の大きさがこれ以上大きいことはないというサイズを指定するようになっています。しかし上記の説明のように、この大きさでBitmapオブジェクトを作成し、さらにGetPixelメソッドでしらみつぶしに調べますので、あまりに大きな値を指定してしまうと大変なことになります。
それではどの位の大きさが適当なのかについては、Graphics.MeasureStringメソッドの結果を参考にするのが良いのではないかと思います。MeasureStringメソッドを呼び出す時にStringFormat.GenericTypographicを指定するのでなければ、十分な余白が取られますので、実際に描画される大きさより小さくなることはまずないでしょう。しかしその可能性も考慮して、さらに余白を追加してもよいかもしれません。
万全を期すならば、さらにMeasureStringPreciselyメソッドが返した結果を確認して、その大きさがproposedSizeと同じだったらproposedSizeをもう少し大きくしてもう一度MeasureStringPreciselyメソッドで計測するといったことも必要になるでしょう。
最後に †
上記のメソッドはまだテストが十分とは言えませんので、不完全かもしれません。もしお気づきの点がございましたら、ご連絡ください。よろしくお願いいたします。
参考 †
コメント †
|