JavaFX 9 で FontMetrics を取得する

このエントリーは、JavaFX Advent Calendar 2017 の 20 日目です。

現時点でもエントリーが無いので急遽先日遭遇した問題に解決策を教えていただいたのでそれを記事にしました。

昨日は @aoetk さんの「Bean ValidationのJavaFX対応」でした。

明日も、この記事を書いている現時点ではまだ空いてます。(^_^; きっと誰かが素敵な記事を投稿してくれると楽しみにしています。

11 月から IBM Cloud (Bluemix)ライト・アカウントが気楽に使えるようになり Watson Personality Insights を利用した人格診断プログラムを組んで楽しんでいました。

pi 

その際に Canvas に文字を描くときの位置決めに難儀したので FontMetrics を取得する方法をφ(..)メモメモ

まず、Java で FontMetrics を取得するとしたら java.awt.FontMetrics クラスを使うことができます。

Abstract Window Toolkit を使って下記のようなプログラムを組んでみました。


package jp.yucchi.textofcanvas;

import java.awt.BorderLayout;
import java.awt.Canvas;
import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.GraphicsEnvironment;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.stream.Stream;
import jp.yucchi.textofcanvas.TextOfCanvas.Configuration;

/**
 *
 * @author Yucchi
 */
public class TextOfCanvas extends Frame {

    protected enum Configuration {
        WIDTH(800),
        HEIGHT(300),
        FONT_SIZE(100);

        int settingValue;

        private Configuration(int settingValue) {
            this.settingValue = settingValue;
        }

        public int getSettingValue() {
            return settingValue;
        }
    }

    public static void main(String[] args) {
        TextOfCanvas textOfCanvas = new TextOfCanvas();
//        // 使用可能なフォント       
//        Stream.of(GraphicsEnvironment.getLocalGraphicsEnvironment()
//                .getAvailableFontFamilyNames())
//                .forEach(System.out::println);
    }

    private Frame frame;

    TextOfCanvas() {
        initGUI();
    }

    private void initGUI() {
        frame = new Frame("Java AWT Text of Canvas");
        frame.setSize(Configuration.WIDTH.settingValue, Configuration.HEIGHT.settingValue);
        frame.setLayout(new BorderLayout());
        frame.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent we) {
                System.exit(0);
            }
        });
        TextCanvas textCanvas = new TextCanvas();
        frame.add(textCanvas, BorderLayout.CENTER);
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }

}

class TextCanvas extends Canvas {

    @Override
    public void paint(Graphics g) {
        g.setColor(Color.LIGHT_GRAY);
        g.drawLine(0, Configuration.HEIGHT.settingValue / 2, Configuration.WIDTH.settingValue, Configuration.HEIGHT.settingValue / 2);
        g.drawLine(Configuration.WIDTH.settingValue / 4, 0, Configuration.WIDTH.settingValue / 4, Configuration.HEIGHT.settingValue);
//        Font font = new java.awt.Font("あくびん", java.awt.Font.BOLD, Configuration.FONT_SIZE.settingValue); // Leading: 0.0
        Font font = new java.awt.Font("SansSerif", java.awt.Font.BOLD, Configuration.FONT_SIZE.settingValue);
        g.setFont(font);
        g.setColor(Color.RED);
        g.drawString("Japan 日本", Configuration.WIDTH.settingValue / 4, Configuration.HEIGHT.settingValue / 2);

        FontMetrics fontmetrics = g.getFontMetrics();
        double height = fontmetrics.getHeight(); // テキスト1行の標準の高さ Height = Ascent + Descent + Leading
        double width = fontmetrics.stringWidth("Japan 日本");
        double ascent = fontmetrics.getAscent(); // ベースラインからの高さ
        double descent = fontmetrics.getDescent(); // ベースラインから下にはみ出る量
//        double maxDecent = fontmetrics.getMaxDecent(); // スペルミスによる @Deprecated (^_^;
        double leading = fontmetrics.getLeading(); // 前の行の descent のラインと次の行の ascent のラインの間に必要な「行間」の量
        System.out.println("Height: " + height);
        System.out.println("Width: " + width);
        System.out.println("Ascent: " + ascent);
        System.out.println("Descent: " + descent);
        System.out.println("Leading: " + leading);

        Font currentFont = g.getFont();
        Font shrinkFont = currentFont.deriveFont(currentFont.getSize() * 0.3F);
        g.setFont(shrinkFont);
        g.setColor(Color.GREEN);
        g.drawLine(0, (int) (Configuration.HEIGHT.settingValue / 2 - ascent),
                Configuration.WIDTH.settingValue, (int) (Configuration.HEIGHT.settingValue / 2 - ascent));
        g.drawString("Ascent: " + ascent, 0, (int) (Configuration.HEIGHT.settingValue / 2 - height + descent));
        g.setColor(Color.BLUE);
        g.drawLine(0, (int) (Configuration.HEIGHT.settingValue / 2 + descent),
                Configuration.WIDTH.settingValue, (int) (Configuration.HEIGHT.settingValue / 2 + descent));
        g.drawString("Descent: " + descent, 0, (int) (Configuration.HEIGHT.settingValue / 2 + descent - leading));
        g.setColor(Color.magenta);
        g.drawLine(0, (int) (Configuration.HEIGHT.settingValue / 2 + leading + descent),
                Configuration.WIDTH.settingValue, (int) (Configuration.HEIGHT.settingValue / 2 + leading + descent));
        g.drawString("Leading: " + leading,
                0, (int) (Configuration.HEIGHT.settingValue / 2 + leading * 2 + descent + g.getFontMetrics().getAscent() - g.getFontMetrics().getDescent()));
    }
}

懐かしいコードですね。

83 行目で FontMetrics を Graphics コンテキストより取得してフォントの構造データをそれぞれの取得メソッドで取得しています。

原点の X, Y 座標に薄いグレーでラインをひいてます。

フォントの高さ関するデータもそれぞれラインをひいてみました。

awt

awt2

Abstract Window Toolkit を使って FontMetrics を得ることは簡単にできることが確認できました。

それでは JavaFX 9 ではどうでしょうか?

同じようなプログラムを組んでしました。


package jp.yucchi.textcoordinates;

import com.sun.javafx.tk.FontMetrics;
import com.sun.javafx.tk.Toolkit;
import java.awt.GraphicsEnvironment;
import java.util.stream.Stream;
import javafx.application.Application;
import javafx.geometry.VPos;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.scene.text.FontSmoothingType;
import javafx.scene.text.FontWeight;
import javafx.scene.text.Text;
import javafx.stage.Stage;

/**
 *
 * @author Yucchi
 */
public class TextCoordinates extends Application {

    private enum Configuration {
        WIDTH(800),
        HEIGHT(300),
        FONT_SIZE(100);
        private final int settingValue;

        private Configuration(int settingValue) {
            this.settingValue = settingValue;
        }

        public int getSettingValue() {
            return settingValue;
        }
    }

    @Override
    public void start(Stage primaryStage) {

//        // 使用可能なフォント       
//        Stream.of(GraphicsEnvironment.getLocalGraphicsEnvironment()
//                .getAvailableFontFamilyNames())
//                .forEach(System.out::println);

        Group root = new Group();
        Canvas canvas = new Canvas(Configuration.WIDTH.settingValue, Configuration.HEIGHT.settingValue);
        GraphicsContext gc = canvas.getGraphicsContext2D();
        drawShapes(gc);
        root.getChildren().add(canvas);
        Scene scene = new Scene(root, Configuration.WIDTH.settingValue, Configuration.HEIGHT.settingValue);
        primaryStage.setTitle("JavaFX Text of Canvas");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }

    private void drawShapes(GraphicsContext gc) {

        Text text = new Text("Japan 日本");

//        Font font = Font.font("あくびん", FontWeight.BOLD, Configuration.FONT_SIZE.settingValue); // Leading: 0.0
        Font font = Font.font("SansSerif", FontWeight.BOLD, Configuration.FONT_SIZE.settingValue);
        text.setFont(font);

        double width = text.getBoundsInLocal().getWidth();
        double height = text.getBoundsInLocal().getHeight();
        double baselineOffset = text.getBaselineOffset();
        double minY = text.getLayoutBounds().getMinY();
        double maxY = text.getLayoutBounds().getMaxY();
        System.out.println("Width: " + width);
        System.out.println("Height: " + height); // Ascent + Descent
        System.out.println("BaselineOffset: " + baselineOffset);  // Ascent
        System.out.println("MinY: " + minY); // -Ascent
        System.out.println("MaxY: " + maxY); // Descent

        final FontMetrics fontMetrics = Toolkit.getToolkit().getFontLoader().getFontMetrics(font);
//        float fontMetricsWidth = fontMetrics.computeStringWidth(text.getText()); // JavaFX9 では無くなった?
        // このフォントでのテキスト行のための最大行高 
        float fontMetricsLineHeight = fontMetrics.getLineHeight();
        // 平均の小文字の最上部へのベースラインから距離
        float fontMetricsXHeight = fontMetrics.getXheight();
        // 平均の最大文字高さまでベースラインからの距離。 この値は常に正の値です 
        float fontMetricsAscent = fontMetrics.getAscent();
        // ベースラインから最大文字高さまでの距離。 この値は常に正の値です 
        float fontMetricsMaxAscent = fontMetrics.getMaxAscent();
        // ベースラインは、デセンダーのない文字(例えば、小文字の「a」)が置かれている仮想線です。 
        // フォントメトリックに関しては、この点から他のすべてのメトリックが導出されます。 この点は暗黙的にゼロとして定義されています。 
        int fontMetricsBaseline = fontMetrics.getBaseline();
        // ベースラインから最低平均値までの距離。 ディセンダー。 この値は常に正の値です
        float fontMetricsDescent = fontMetrics.getDescent();
        // ベースラインから絶対値の最も低いディセンダーまでの距離。 この値は常に正の値です 
        float fontMetricsMaxDescent = fontMetrics.getMaxDescent();
        // このフォントのテキスト行間のスペース量。 
        // これは、1行のmaxDecentと次のmaxAscentの間のスペース量です。 
        // この数値は、lineHeightに含まれています。 
        float fontMetricsLeading = fontMetrics.getLeading();
//        System.out.println("FontMetricsWidth: " + fontMetricsWidth);
        System.out.println("FontMetricsLineHeight: " + fontMetricsLineHeight); // Ascent + Descent + Leading
        System.out.println("FontMetricsXHeight: " + fontMetricsXHeight);
        System.out.println("FontMetricsAscent: " + fontMetricsAscent);
        System.out.println("FontMetricsMaxAscent: " + fontMetricsMaxAscent);
        System.out.println("FontMetricsBaseline: " + fontMetricsBaseline);
        System.out.println("FontMetricsDescent: " + fontMetricsDescent);
        System.out.println("FontMetricsMaxDescent: " + fontMetricsMaxDescent);
        System.out.println("FontMetricsLeading: " + fontMetricsLeading);

        gc.setFontSmoothingType(FontSmoothingType.LCD);

        gc.setStroke(Color.LIGHTGRAY);
        gc.setLineWidth(1.0);
        gc.strokeLine(0.0, Configuration.HEIGHT.settingValue / 2.0, Configuration.WIDTH.settingValue, Configuration.HEIGHT.settingValue / 2.0);
        gc.strokeLine(Configuration.WIDTH.settingValue / 4.0, 0.0, Configuration.WIDTH.settingValue / 4.0, Configuration.HEIGHT.settingValue);
        // テキスト
        gc.setFont(font);
        gc.setFill(Color.RED);
        gc.setTextBaseline(VPos.BASELINE);
        gc.fillText(text.getText(), Configuration.WIDTH.settingValue / 4.0, Configuration.HEIGHT.settingValue / 2.0);
        // FontMetrics データ表示用フォント
        gc.setFont(Font.font("SansSerif", FontWeight.BOLD, Configuration.FONT_SIZE.settingValue * 0.3));
//        gc.setFont(Font.font("あくびん", FontWeight.BOLD, Configuration.FONT_SIZE.settingValue * 0.3));
        final FontMetrics shrinkFontMetrics = Toolkit.getToolkit().getFontLoader().getFontMetrics(gc.getFont());
        // Ascent
        gc.setStroke(Color.GREEN);
        gc.strokeLine(0.0, Configuration.HEIGHT.settingValue / 2.0 - fontMetricsAscent,
                Configuration.WIDTH.settingValue, Configuration.HEIGHT.settingValue / 2.0 - fontMetricsAscent);
        gc.setFill(Color.GREEN);
        gc.fillText("Ascent: " + String.format("%.1f", fontMetricsAscent), 0.0, Configuration.HEIGHT.settingValue / 2.0 - fontMetricsAscent - fontMetricsLeading);
        // XHeight
        gc.setStroke(Color.PURPLE);
        gc.strokeLine(0.0, Configuration.HEIGHT.settingValue / 2.0 - fontMetricsXHeight,
                Configuration.WIDTH.settingValue, Configuration.HEIGHT.settingValue / 2.0 - fontMetricsXHeight);
        gc.setFill(Color.PURPLE);
        gc.fillText("XHeight: " + String.format("%.1f", fontMetricsXHeight), 0.0, Configuration.HEIGHT.settingValue / 2.0 - fontMetricsXHeight - fontMetricsLeading);
        // Descent 
        gc.setStroke(Color.BLUE);
        gc.strokeLine(0.0, Configuration.HEIGHT.settingValue / 2.0 + fontMetricsDescent,
                Configuration.WIDTH.settingValue, (int) (Configuration.HEIGHT.settingValue / 2.0 + fontMetricsDescent));
        gc.setFill(Color.BLUE);
        gc.fillText("Descent: " + String.format("%.1f", fontMetricsDescent), 0.0, Configuration.HEIGHT.settingValue / 2.0 + fontMetricsDescent - fontMetricsLeading);
        // Leading
        gc.setStroke(Color.MAGENTA);
        gc.strokeLine(0.0, (Configuration.HEIGHT.settingValue / 2.0 + fontMetricsLeading + fontMetricsDescent),
                Configuration.WIDTH.settingValue, Configuration.HEIGHT.settingValue / 2.0 + fontMetricsLeading + fontMetricsDescent);
        gc.setFill(Color.MAGENTA);
        gc.fillText("Leading: " + String.format("%.1f", fontMetricsLeading),
                0.0, Configuration.HEIGHT.settingValue / 2.0 + fontMetricsDescent + shrinkFontMetrics.getAscent() + shrinkFontMetrics.getLeading());

    }

}

残念ながらコンパイルエラーです。

error

「名前のないモジュールにエクスポートされていません」ってなんのことですか?

JavaFX 9 では java.awt.FontMetrics クラスは使えないようなので com.sun.javafx.tk.FontMetrics, com.sun.javafx.tk.Toolkit を使用しました。

確か、JavaFX 8 では使えていたような記憶があるんだけど・・・

そう言えば、Project Jigsaw の影響で com.sun ではじまるパッケージが使えないものがあるようです。

このプログラムで使っている com.sun.javafx.tk.FontMetrics, com.sun.javafx.tk.Toolkit も JDK 内部(モジュール)にちゃんとあるのにデフォルトで使えなくしてあります。

困りました!

Windows 環境なら JavaFX 9 だったら HiDPI 対応の恩恵を享受することが可能なのに。

ちなみに JavaFX 8 だったら問題なく動きました。

さて、どうしたものか。。。

以前 Twitter で 「hoge は fuga ができないからクソッ!」もしくは女子高生を装ってヘルプをつぶやくと優秀なプログラマが解決策を提案してくれるという法則を学んだ。

私には役者の才能も無いし、小賢しいことをするのは面倒なので素直に Twitter でつぶやいたところ心優しい Java プログラマーが助けてくれました。

ありがとうございます!

早速 NetBeans に javacとjavaのオプションを設定してコンパイル、実行をしたところ無事に動きました。(^_^)

s

s2

fx

fx2

ちなみに、テキストの挿入位置の Y 座標の位置はデフォルトでは VPos.BASELINE となっています。

gc.setTextBaseline(VPos.BASELINE);

これは次のように変更することができます。

gc.setTextBaseline(VPos.TOP);

top

gc.setTextBaseline(VPos.CENTER);

center

gc.setTextBaseline(VPos.BOTTOM);

bottom

ああっ・・・ しまった。(>_<) 左のテキストの位置の修正忘れた。見なかったことにしてください。(ごめんなさい)

ついでに Leading が無いフォントの表示も見ておきます。

awt3

awt4

fx3

fx4

AWT と JavaFX では FontMetrics の扱い方に違いがあるので注意が必要ですね。

 

まとめ

さて、ここで JavaFX で FontMetrics を扱うために com.sun.javafx.tk.FontMetrics, com.sun.javafx.tk.Toolkit を使用します。

これを JavaFX 9 で使うためには javacとjavaのオプションに –add-exports=javafx.graphics/com.sun.javafx.tk=ALL-UNNAMED の設定が必要です。

参考

JEP 261: Module System

カプセル化を破る

モジュールシステムによって定義されたアクセス制御境界に違反し、コンパイラと仮想マシンによって強制されて、
あるモジュールが別のモジュールの一部の非通知タイプにアクセスできるようにする必要があることがあります。
これは、例えば、内部型のホワイトボックステストを可能にするため、
またはサポートされていない内部APIをそれらに依存するようになったコードに公開するために望ましいことがある。
これを行うには、コンパイル時と実行時の両方で–add-exportsオプションを使用できます。
構文は次のとおりです。

–add-exports <source-module>/<package>=<target-module>(,<target-module>)*

<source-module>と<target-module>はモジュール名で、<package>はパッケージ名です。

–add-exportsオプションは、複数回使用できますが、ソースモジュールとパッケージ名の特定の組み合わせに対して最大で1回使用できます。
各インスタンスの効果は、指定されたパッケージの修飾されたエクスポートをソースモジュールからターゲットモジュールに追加することです。
これは基本的に、モジュール宣言内のエクスポート句のコマンドライン形式、またはModule :: addExportsメソッドの無制限な形式の呼び出しです。
結果として、ターゲットモジュールがソースモジュールの名前付きパッケージ内のパブリックタイプにアクセスできるようになります。
ターゲットモジュールはソースモジュールをモジュール宣言のrequires節、Module :: addReadsメソッド、または–add-readsオプションのインスタンスです。
たとえば、jmx.wbtestモジュールに、java.managementモジュールの非エクスポートcom.sun.jmx.remote.internalパッケージのホワイトボックス・テストが含まれている場合、それが必要とするアクセスはオプションを使用して許可することができます。

–add-exports java.management/com.sun.jmx.remote.internal=jmx.wbtest

特殊なケースとして、<target-module>がALL-UNNAMEDの場合、ソースパッケージは、最初に存在するか、後で作成されるかに関係なく、名前のないすべてのモジュールにエクスポートされます。
したがって、java.managementモジュールのsun.managementパッケージへのアクセスは、オプションを介してクラスパス上のすべてのコードに与えることができます。

–add-exports java.management/sun.management=ALL-UNNAMED

–add-exportsオプションを使用すると、指定されたパッケージのパブリックタイプにアクセスできます。
コアリフレクションAPIのsetAccessibleメソッドを使用して、非公開のすべての要素にさらにアクセスしてアクセスできるようにする必要があることがあります。
これを行うには、実行時に–add-opensオプションを使用することができます。
–add-exportsオプションと同じ構文です:

–add-opens <source-module>/<package>=<target-module>(,<target-module>)*

<source-module>と<target-module>はモジュール名で、<package>はパッケージ名です。
–add-opensオプションは複数回使用できますが、ソースモジュールとパッケージ名の特定の組み合わせに対して最大で1回使用できます。
各インスタンスの効果は、名前付きパッケージの修飾されたオープンをソースモジュールからターゲットモジュールに追加することです。
これは基本的に、モジュール宣言のopens節のコマンドライン形式、またはModule :: addOpensメソッドの無制限な形式の呼び出しです。
結果として、ターゲットモジュール内のコードは、ターゲットモジュールがソースモジュールを読み取る限り、
ソースリフレクションAPIを使用して、ソースモジュールの名前付きパッケージ内のパブリックなどのすべてのタイプにアクセスできます。
オープンパッケージは、コンパイル時にエクスポートされていないパッケージと区別できないため、
–add-opensオプションはそのフェーズでは使用できません。

–add-exportsと–add-opensオプションは、細心の注意を払って使用する必要があります。
それらを使用して、ライブラリモジュールの内部API、またはJDK自体のアクセス権を取得することはできますが、自己責任で行ってください。
内部APIが変更または削除された場合、ライブラリまたはアプリケーションは失敗します
。

 

((((;゚Д゚)))))))

I wish you a Merry Christmas.

Hatena タグ: ,,