Java の Font 周りの比較的ディープな話(前編)

技研の (あ) です。
ちょこちょことプロトタイピングするのによく Java を使います。
プロトタイピングといえど見栄えも整えたいので、
表示するテキストのフォントの大きさなどを調整したくなります。
「この高さ・幅で収まる範囲でなるべく大きい字で」とか。
ウィンドウや部品のサイズが固定であれば、
適当に手動で調整したりもしますがなかなか面倒です。
さらに、可変だったらどうしよう? 自動で合わせたいよね、
どうせなら洒落たフォントも使いたいよね、
ということで Java のフォント周りをいろいろ調べて試してみました。

通り一遍のことならちょっとぐぐれば出てくるのですが、
「実際の挙動はどんな感じ?」というのはなかなか無いようですね。
そんなわけで、Java でフォント周りで凝りたい方の参考になれば幸いです。

以下、リファレンスマニュアル等で確認できる部分と、
どこにも書いてないけど実際に実行してみるとこうなるという部分、
実際の挙動から推測するとこういうことのようだ、という部分があります。
実行してみた環境は、Windows 7 + Java 1.8.0_25 です。
本筋から外れる細かい部分は端折ったりしますので、
そこは適宜リファレンスマニュアル等をご参照ください。

基本的なテキスト描画の手順

さて、まずは Java AWT でウィンドウにテキストを描画するときの基本的な手順です。

Font font = new Font(フォント名, スタイル, サイズ);

という感じで Font のオブジェクトを作り、これを paint メソッドにやってくる
Graphics オブジェクトにセットし、drawString メソッドで描画します。

graphics.setFont(font);
graphics.drawString(文字列, x座標, y座標);

このとき、y座標は画像を表示するときなどとは違って、
ベースラインの位置を指定します。
文字を並べるときの基準線のことです。
中学校一年生で初めて英語を教わるときのノートで、
横線が四本書かれているものがあったと思いますが、
あれの下から二番目の線、色違いやちょっと太い線で書かれるあの線と思って下さい。
指定する座標がそれなので、左上にテキストを表示しようと思って座標に (0, 0) を
指定すると上にはみ出してほとんど見えない、という結果になります。
ベースラインからの「高さ」の分だけずらしてやらねばなりません。

一方、左下に表示しようと (0, 描画スクリーンの縦幅) を指定すると、
ベースラインより下にはみ出る部分が画面の外になってしまいます。
はみ出る部分がなかったり少なかったりするときにはそれでもよいかもしれませんが、
ちゃんと収めようと思うと、ずらす必要があります。

このあたりの事情は、欧文のレタリングの話がベースになっているので
日本語をメインで使う身としてはややこしいなぁ、と思ってしまいますよね。
このあと、もっとややこしい、面倒な話が出てきます…w。

フォントの各種情報

現在セットされているフォントで文字を描画した場合、
どういうサイズになるか、下にはみ出る長さはどれくらいか、などの情報は
FontMetrics というクラスのオブジェクトを経由して調べます。

FontMetrics fontmetrics = graphics.getFontMetrics();

「高さ」と、ある文字列を表示するのに必要な幅はそれぞれ

fontmetrics.getHeight();
fontmetrics.stringWidth(文字列);

で取れます。
さて、ここらへんが面倒な話の入り口。
「高さ」と書きましたが、getHeight() が返す数値は何を表しているか?
マニュアルを見ると「テキスト1行の標準の高さ」と書いてありますが…。

FontMetrics のメソッド一覧を見ると、耳慣れない (人が多いであろう)
単語がいくつか並んでいます。
このうち、フォントアセント (ascent) というのが「ベースラインからの高さ」を意味します。
フォントディセント (descent) が「ベースラインから下にはみ出る量」です。
標準レディング (leading) が前の行の descent のラインと次の行の ascent の
ラインの間に必要な「行間」の量です。
それぞれ

fontmetrics.getAscent();
fontmetrics.getDescent();
fontmetrics.getLeading();

で取得できます。そして、この三つの値の合計が「標準の高さ」になります。
リファレンスを見ると、合計値と getHeight() で得られる値は
「四捨五入の都合上、一致しないことがある」とのこと (各メソッドは int を返すが、
内部的には小数点以下のある量も扱いうるということ?) ですが
それは気にする必要はないかと思います。

実例

言葉だけで説明していても判りづらいので、
実際に描画させてみたものを載せます。Calibri というフォントと
Times New Roman というフォントの例です。

FontMetrics_Calibri

FontMetrics_TimesNewRoman

このように、文字列は Ascent のラインと Descent のラインの間に収まってま…せんね。
あれ???

getAscent()/getDescent() の説明をよくよく見ると
「Font の文字によっては…はみ出す場合があります」。
えぇぇ?!
あ、大丈夫、こっちに「はみ出すことはありません」と書かれた
getMaxAscent()/getMaxDescent() が!

カタカタカタ。ターン!

あれ? getAscent()/getDescent() と値が変わらない…。
調べてみた範囲では、これらが違う値を返すフォントは無いようです…orz。
実装されていないのか、それともフォント側がそのデータを持っていないのか…。
違う環境ではきちんと使えるのかもしれません。

気を取り直して上の図を見ると Ascent/Descent に Leading を足せば、
収まってくれそうな感じです。
実際、標準でインストールされているフォントだとだいたい大丈夫そうです
(確認しきれてないので全てかどうかは断言はできず)。
しかし、何かのアプリケーションに付属していたり自分でインストールした
非標準のフォントの中には、しっかりはみ出してくれるものをいくつか確認しました(;_;)。

まとめると、実用的な範囲では

一番上に表示したい

graphics.drawString(文字列, x, fontmetrics.getAscent())

はみ出しにくくしたい場合は

graphics.drawString(文字列, x, fontmetrics.getAscent() + fontmetrics.getLeading())

一番下に表示したい

graphics.drawString(文字列, x, スクリーンの高さ - fontmetrics.getDescent())

はみ出しにくくしたい場合は

graphics.drawString(文字列, x, スクリーンの高さ - fontmetrics.getDescent() - fontmetrics.getLeading())

ということになります。
次の行は、fontmetrics.getHeight() だけずらした位置に描画します。

ちなみに Leading は 0 に設定されているフォントも多いです。
一方で字の大きさに対してとても大きな Leading が設定されているフォントもあるので、
Leading を含めると妙に空白部分が広くなってしまうこともあります。
それを考慮に入れて調整したい場合、
Leading の相対的な大きさである程度は判別できるかもしれませんが、
主にヒューリスティックに頼らざるをえない感じです。

さて、高さの話ばかりしてきましたが、幅のほうはどうでしょう?

はい、ご期待のとおり、fontmetrics.stringWidth(文字列) で得られる幅からはみ出すこと、
あります(;_;)。
特に、Font のスタイルでイタリック (Font.ITALIC) を指定してるときに…。

日本語フォントの場合

日本語の文字の場合は基本的な構造として「ベースラインから下へ大きくはみ出す」
というのはありませんが、欧文の文字と合わせたりする都合もあり(?)、
ベースライン、ascent/descent などが設定されています。
メイリオ、HG明朝B、HGS祥南行書体の例を挙げます。

FontMetrics_Meiryo

FontMetrics_HGMinchoB

FontMetrics_HGSShonanGyosho

かなはベースラインにちょうど乗っかるくらいの位置で、
漢字が少し下にはみ出る (descent には収まる) 設定のものが多い感じです。
ascent/descent の幅の取り方はまちまちですね。

つづく…

長くなったのでこの辺で一旦終わりにして、
別エントリで「使えるフォントの種類」などを書きたいと思います。

>>Java の Font 周りの比較的ディープな話(後編)はコチラ

  • このエントリーをはてなブックマークに追加