VB.netで文字を正確な位置に描く。

.net Frameworkで文字列を描写しようと思ったら、Graphics.DrawString を使おうと思うわけですが、このメソッドは、座標を指定すると、その座標を左上隅に文字列を描写してくれます。さらに、一番目の文字の左側に空白を入れてくれます。

まあ、通常の文字列を描画する際にはこれで問題ないのかもしれませんが、ある程度正確な位置に描写したいとなると、結構やっかいです。

ここでは、指定座標をベースラインの左端として文字列を描画する方法を解説します。

超長文なので、面倒な方は一番下の関数をコピペして使ってください。

では、まず最初に用語の定義。

フォントで使われる単位

あるフォントの左上端から右下端までを「emスクウェア(.netのヘルプではem四角形)」と呼びます。ここで、下端というのはベースラインでなく、あらゆる文字をひっくるめた下端、たとえばpの字の下端です。そして、emスクウェアの縦の長さを「1em」とします。

DrawStringをはじめ、多くのアプリケーションでフォントサイズを指定したときは、この1emをどの大きさで描画するかを指定しています。※そうとも限らないようです。現在調査中。あるいは、emスクウェアに収まらない文字があるのか。

フォントファイルでは、抽象的(論理的ともいう)な単位で文字の形を記述しています。この単位はフォント単位(font unitまたはfunit)、デザイン単位、フォントデザイン単位などと呼ばれます。1emが何フォント単位かは、フォントによって異なります。32の倍数ならいくつでもOKのようです。

上端からベースラインまでの長さをアセント、ベースラインから下端までの長さをディセントといいます。アセント/ディセントは当然、フォント単位で示されます。

ポイントについて

フォントサイズは普通「ポイント(pointまたはpt)」という単位で指定されます。

ポイントはmmやインチと同じような(ピクセルとは違う)長さの単位です。日本では1ポイント=0.35mmとされてるようですが、コンピュータ上では1ポイント=1/72インチ(逆に言えば1インチ=72ポイント)として扱うのが一般的です。

一方、ディスプレイはピクセル(ドット)単位で描画を行います。ディスプレイのサイズ、解像度によって1ピクセルがどれだけの大きさになるかは変わるわけですが、それも面倒なので(?)Windowsでは96dpi(1インチ=96ピクセル)Machintoshでは72dpi(1インチ=72ピクセル)としているようです。(ただし、WinXPなどは133dpiなどより高いdpiに設定することが可能です。Macは残念ながら知らない。)

よってポイントとピクセルとは以下の式で変換できます(Windowsの場合)

ピクセル = ポイント * (96 / 72)ポイント = ピクセル * (72 / 96)(Macだと96の代わりに72を入れるので、そもそも変換の必要なし。)

なお、.netではGraphics.DpiXおよびGraphics.DpiYでGraphicsオブジェクトのdpiが得られます。

上端からベースラインまでの長さを得る

DrawStringでは指定した座標を文字列の左上端として描画するので、ベースラインの位置で描画するためには、上端からベースラインまでの長さを知る必要があります。

が、上端からベースラインの長さ=アセントはフォント単位でしか得られませんので、そのままでは使えません。そこで、1emとアセントの比がわかれば、フォントサイズ(1emと対応)を掛ければアセントの長さがわかります。

アセントの値を得るには、FontFamily.GetCellAscentを使います(Fontインスタンスを作った場合、Font.FontFamily.GetCellAscentでOK)。

1emの長さをフォント単位で得るにはFontFamily.GetEmHeightを使います。(GetCellAscent、GetEmHeightには引数としてフォントのスタイル情報 - 標準か、太字か斜体か... - を指定します)

フォントサイズはFont.Sizeで得られますが、ここで得られる値はFont.Unitに設定された単位です。ポイント単位で得られるFont.SizeInPointsもあります。

よって、あるFontインスタンス f のアセントの長さは以下の式で得られます。

AscentLength = f.Size * (f.FontFamily.GetCellAscent(FontStyle.Regular) / _
                         f.FontFamily.GetEmHeight(FontStyle.Regular))

ただしこれは、フォントサイズの単位を考慮していないので、得られる値はf.Unitに設定された単位です。上でピクセルとポイントの変換式を作ったので、フォントサイズをポイントで得て、ピクセルでアセントの長さを返す式を作ってみましょう。

AscentLengthInPixel = (f.SizeInPoints * (96/ 72)) * _
                      (f.FontFamily.GetCellAscent(FontStyle.Regular) / _
                       f.FontFamily.GetEmHeight(FontStyle.Regular))

あとはDrawStringを使うときにY座標からこの値を引けば、指定したY座標をベースラインに描画してくれます。

右にずれる

こうして描画してみると縦位置はいいのですが、横位置は指定座標より少し右にずれて描画されてしまいます。普通に文字列を描画するときは、左端ぎりぎりより見やすくていいのかもしれませんが、正確な位置に描画したい場合には余計なお世話・・・。

この「ずれ」の量について、ヘルプを見ても(あんまり見てない)見つからなかったので、乱暴にも実測しました。

その結果、ずれの量は1emのおおよそ0.17倍とわかったので、以下の式でずれの量を得ます。

ずれ = Font.Size * 0.17

例によってポイント - ピクセル変換を入れます。

ずれ = Font.SizeInPoints * (96 / 72) * 0.17

で、DrawStringを使うときにX座標からこの値を引けば、指定したX座標ぎりぎりに描画してくれます。

関数を作る

はぁ(疲れた)。これで、やっと関数が作れます。

以下は、文字列s、ブラシbrush、描画するGraphicsインスタンスg、フォントf、描画位置ptを引数に取り、描画位置をベースライン・左端ぎりぎりにして文字列を描画するサププロシージャです。

ピクセル - ポイント変換は面倒なので定数を作ってます。ほんとはGraphics.dpiXなどを使って、縦横別々に計算すべきですが、面倒なので省略。

'指定座標をベースライン・左端として文字列を描画します。
Sub DrawStringB(ByVal s As String, ByVal font As Font, _
                ByVal brush As Brush, ByVal pt As Point, _
                ByVal g As Graphics)
    Dim ascent As Integer
    Const pixelperpoint = 96 / 72

    ascent = (font.Font.SizeInPoints * pixelperpoint) * _
             (font.FontFamily.GetCellAscent(FontStyle.Regular) / _
              font.FontFamily.GetEmHeight(FontStyle.Regular))
    g.DrawString(s, font, brush,
                 New Point(pt.X - (font.SizeInPoints * pixelperpoint * 0.17),
                           pt.Y - ascent))
End Function

はぁ。フォントは単位がいっぱいで頭がこんがらがります。

フォントについてもっと詳しく知りたい方はこちらへ。

あと、FontFamilyクラスのメンバを見るといろいろあるみたいです。

あー、疲れた。

追記:

参考URL

さらに追記:

.net Framework 3.0に実装されたWPF関係(?)のクラスSystem.Windows.Media.GlyphTypefaceを使うとx-heightやcap-heightなど細かなフォント情報がわかる模様。現在、いろいろテスト中。