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 タグ: ,,

今さらだけど JavaFX で非同期処理

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

昨日は @planet-az さんの「簡単なミュージックプレーヤーを作ってみた」でした。

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

今さらですが JavaFX の非同期処理を復習がてら簡単にみていきたいと思います。

では、次のようなプログラムを作ってみます。

1

long result = 1;

for (int i = 0; i < repeatProcessingNumber; i++) {

result += result;

初期値 1 で10回ループするプログラムです。

計算終了後は 1024 と結果を表示します。

これだけでは時間のかかる処理とはならないので

TimeUnit.MILLISECONDS.sleep(500);

とスレッドをスリープさせています。

プログラムの状態、計算中の値の表示やキャンセルなどのメッセージ表示、プログレスバーによるプログラムの進捗状態の可視化などを実装します。

思考停止状態でギナギナっと作ったプログラムは次のようなものです。

Warning! This code will make you headache.

You do not have to read it.

package jp.yucchi.badasynchronousprocessing4javafx;

import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressBar;
import javafx.scene.layout.FlowPane;
import javafx.stage.Stage;

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

    private Label statusLabel;
    private Label messageLabel;
    private Label interimResult;
    private Label resultLabel;
    private ProgressBar progressBar;
    private Button startButton;
    private Button cancelButton;
    private boolean cancelFlag;

    @Override
    public void start(Stage primaryStage) {
        System.out.println(cancelFlag);

        statusLabel = new Label("STATUS");
        statusLabel.setMinWidth(200);
        messageLabel = new Label("MESSAGE");
        messageLabel.setMinWidth(200);
        interimResult = new Label("INTERIM RESULT");
        interimResult.setMinWidth(200);
        resultLabel = new Label("RESULT");
        resultLabel.setMinWidth(200);
        progressBar = new ProgressBar(0);
        progressBar.setMinWidth(200);
        startButton = new Button("START");
        startButton.setMinWidth(200);
        cancelButton = new Button("CANCEL");
        cancelButton.setMinWidth(200);
        cancelButton.setDisable(true);

        FlowPane root = new FlowPane();
        root.setPadding(new Insets(10));
        root.setHgap(10);
        root.setVgap(10);
        root.getChildren().addAll(statusLabel, messageLabel, interimResult, resultLabel, progressBar, startButton, cancelButton);

        Scene scene = new Scene(root, 220, 225);

        primaryStage.setTitle(this.getClass().getSimpleName());
        primaryStage.setScene(scene);
        primaryStage.show();

        primaryStage.setOnCloseRequest(we -> {

        });

        startButton.setOnAction(ae -> {
            resultLabel.setText("RESULT");
            executeTask();
        });

        cancelButton.setOnAction(ae -> {
            startButton.setDisable(false);
            cancelButton.setDisable(true);
            statusLabel.setText("CANCELED");
            messageLabel.setText("Cancelled!");
            progressBar.setProgress(0.0);
            interimResult.setText("INTERIM RESULT");
            cancelFlag = true;
        });
    }

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        launch(args);
    }

    private void executeTask() {

        startButton.setDisable(true);
        cancelButton.setDisable(false);

        int repeatProcessingNumber = 10;
        long result = 1;
        statusLabel.setText("RUNNING");
        messageLabel.setText("Start!");
        interimResult.setText(String.valueOf(result));
        progressBar.setProgress(0);

        for (int i = 0; i < repeatProcessingNumber; i++) {

            if (cancelFlag) {
                System.out.println("This program has been canceled.");
                break;
            }

            try {
                TimeUnit.MILLISECONDS.sleep(500);

            } catch (InterruptedException ex) {
                Logger.getLogger(BadAsynchronousProcessing4JavaFX.class.getName()).log(Level.SEVERE, null, ex);
                if (cancelFlag) {
                    System.out.println("This program has been canceled.");
                    break;
                }
            }

            result += result;
            messageLabel.setText(String.format("%d/%d", i + 1, repeatProcessingNumber));
            interimResult.setText(String.valueOf(result));
            progressBar.setProgress(i);

        }

        if (!cancelFlag) {
            statusLabel.setText("SUCCEEDED");
            resultLabel.setText(String.valueOf(result));
        } else {
            cancelFlag = false;
        }

        startButton.setDisable(false);
        cancelButton.setDisable(true);

    }

}

それではこのプログラムを実行してみましょう。

2

「START」 ボタンを押した瞬間プロブラムがフリーズしたようになりました。

そして暫くすると結果が表示されました。

3

これは駄目ですね。(>_<。)

プログラムがビジー状態で応答しなくなってしまっては使い物になりません。

進捗状態がわからないのは致命的です。

そして唐突に計算が終了してプログラムが完了する。

そう言えば遠い昔にこのような状態のプログラムが当たり前のように存在していたような気がする。

今の時代こんなのは絶対に許されません。

何がいけないのか?

それは時間のかかる処理を JavaFX アプリケーションスレッド上で行っているからです。

NetBeans のプロファイラで確認してみます。

4

JavaFX Application Thread タイムラインを見てください。

START ボタンを押した瞬間に時間のかかる処理(スレッドスリープ)が走ってます。(緑色から紫色に変わっているところ)

これでは UI の更新処理などはできませんね。

時間のかかる処理をしている間プログラムの応答は無くなり、ビジー状態のようになってしまいます。

つまり、時間のかかる処理用にスレッドをもう一つ起こせばこの問題は解決するはずです。

この問題を解決するのに最適な方法は javafx.concurrentパッケージを使用することです。

javafx.concurrent パッケージは、Worker インタフェースと、2つの具体的な実装である Task および Service クラスで構成されています。

Worker インタフェースは、バックグラウンドスレッド上の Worker オブジェクトの状態、進捗を監視可能なプロパティで公開しています。

状態は ReadOnlyObjectProperty<Worker.State> stateProperty で確認できます。

次のように Enum Worker.State で定義されています。

  • READY     Workerがまだ実行されておらず、実行の準備ができているか、またはWorkerが再初期化されたことを示します。
  • SCHEDULED   Workerの実行がスケジュールされているが、現在は実行中ではないことを示します。
  • RUNNING        このWorkerが実行中であることを示します。
  • SUCCEEDED   このWorkerが正常に完了しており、valueプロパティから読み取る準備ができている有効な結果があることを示します。
  • CANCELLED     このWorkerがWorker.cancel()メソッドによって取り消されたことを示します。
  • FAILED         通常は予期しない条件が発生したことによって、このWorkerが失敗したことを示します。

これらは JavaFX アプリケーションスレッドから使用できます。

Worker オブジェクトによって実行される処理の進捗は、totalWork、workDone、progress など、3つの異なるプロパティを通じて取得できます。

Worker オブジェクトの状態が変化するときに発生するイベントは WorkerStateEventクラスによって指定されます。

これは Task クラスと Service クラスの両方に EventTarget インタフェースが実装され状態イベントのリスニングがサポートされているからです。

今回は再利用の必要が無い Worker オブジェクトを生成するので Task クラスを利用します。

上記のように JavaFX の Task クラスは FutureTask クラスの完全に監視可能にした実装となっています。

この Task クラスを拡張しバックグラウンドスレッドで実行するロジックを実装します。

懐かしのSwingWorker クラスを思い出させてくれますね。(もう、ほとんど覚えてないけどね)

SwingWorker では doInBackground() メソッドにてバックグラウンドスレッドロジックを呼び出します。

JavaFX の場合は call() メソッドです。

doInBackground() メソッド、call() メソッドともにどちらも抽象メソッドなのでオーバーライドして使います。

call() メソッドは Task クラスが実行されるときに呼び出され、call() メソッドに実装されたバックグラウンドスレッドロジックを実行します。

つまり、仕組み的には SwingWorker とほとんど変わりはないので馴染みやすいかも知れません。

さて、JavaFX のために用意されたのにこれだけではあまり意味がありません。

この Task クラスの実行にあたり updateProgress() メソッド、updateMessage() メソッド、updateValue() メソッド、updateTitle() メソッドを呼び出すことができます。

これらを使用するとプログラムの状態、進捗状況、実行中の処理の結果の一部を返すなどを可視化できます。

ちょっと updateValue() メソッドの実装をみてみましょう。

    protected void updateValue(V value) {
        if (isFxApplicationThread()) {
            this.value.set(value);
        } else {
            // As with the workDone, it might be that the background thread
            // will update this value quite frequently, and we need
            // to throttle the updates so as not to completely clobber
            // the event dispatching system.
            if (valueUpdate.getAndSet(value) == null) {
                runLater(() -> Task.this.value.set(valueUpdate.getAndSet(null)));
            }
        }
    }

呼び出しが JavaFX アプリケーションスレッドかそうでないかで分岐処理をおこなっています。

JavaFX アプリケーションスレッドからだったらそのまま値を更新します。

    void runLater(Runnable r) {
        Platform.runLater(r);
    }

でなければ runLater()メソッドで javafx.application.Platform クラスの runLater(Runnable runnable) メソッドを呼び出して値を更新します。

何故なら、JavaFX Sceneグラフは、スレッドセーフではなく、JavaFXアプリケーションスレッドのみアクセスおよび変更できます。

これは Swing では javax.swing.SwingUtilities クラスの invokeLater(Runnable doRun) メソッドと同じようなものです。

JDK1.3 からは java.awt.EventQueue.invokeLater() を呼び出すように作られています。

このinvokeLater(Runnable doRun) メソッドはどのスレッドからも呼び出しが可能となっています。

javafx.application.Platform クラスの runLater(Runnable runnable) メソッドも任意のスレッドから呼び出し可能となっています。

updateProgress() メソッド、updateMessage() メソッド、updateValue() メソッド、updateTitle() メソッドの API ドキュメントには「このメソッドは、任意のスレッドから安全に呼び出すことができます。」と記載されています。

ちなみに updateValue() メソッドは JavaFX 8 からなので上記のようにラムダ式を使い綺麗に書かれています。

古くからある updateMessage() メソッドは次のように Java SE 7 の時代のコードのままとなっています。

    protected void updateMessage(String message) {
        if (isFxApplicationThread()) {
            this.message.set(message);
        } else {
            // As with the workDone, it might be that the background thread
            // will update this message quite frequently, and we need
            // to throttle the updates so as not to completely clobber
            // the event dispatching system.
            if (messageUpdate.getAndSet(message) == null) {
                runLater(new Runnable() {
                    @Override public void run() {
                        final String message = messageUpdate.getAndSet(null);
                        Task.this.message.set(message);
                    }
                });
            }
        }
    }

このようなのを目にすると Java は開発者のために進化し続けている言語なんだなぁって感慨深いものがありますね。

SwingWorker のように publish() メソッド、process() メソッドなんてのは JavaFX には必要なくなってます。

このようにありがたい機能を標準で実装されている Task クラスですがさらに便利なプロパティを持っています。

これから先程のプログラムを改善するのにいくつかの Task クラスのプロパティを使用します。

それらプロパティは JavaFX のバインドという機能により快適に使用することが可能です。

JSR 295 Beans Binding と同じようなものです。

実際にどのように利用するかというと Task クラスの実行状態(Worker オブジェクト)のプロパティを利用して「START」ボタン、「CANCEL」ボタンの活性化、非活性化をおこないます。

また、バックグラウンドスレッド(Worker オブジェクト)の状態、計算結果の一部を返す処理、プログレスバーによるプログラムの進捗状態の表示もおこないます。

これらの対応を施したプログラムは次のようになります。

package jp.yucchi.asynchronousprocessing4javafx;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.application.Application;
import javafx.concurrent.Task;
import static javafx.concurrent.Worker.State.RUNNING;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressBar;
import javafx.scene.layout.FlowPane;
import javafx.stage.Stage;

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

    private final ExecutorService executorService = Executors.newSingleThreadExecutor();
    private Label statusLabel;
    private Label messageLabel;
    private Label interimResult;
    private Label resultLabel;
    private ProgressBar progressBar;
    private Button startButton;
    private Button cancelButton;

    @Override
    public void start(Stage primaryStage) {

        statusLabel = new Label("STATUS");
        statusLabel.setMinWidth(200);
        messageLabel = new Label("MESSAGE");
        messageLabel.setMinWidth(200);
        interimResult = new Label("INTERIM RESULT");
        interimResult.setMinWidth(200);
        resultLabel = new Label("RESULT");
        resultLabel.setMinWidth(200);
        progressBar = new ProgressBar(0);
        progressBar.setMinWidth(200);
        startButton = new Button("START");
        startButton.setMinWidth(200);
        cancelButton = new Button("CANCEL");
        cancelButton.setMinWidth(200);
        cancelButton.setDisable(true);

        FlowPane root = new FlowPane();
        root.setPadding(new Insets(10));
        root.setHgap(10);
        root.setVgap(10);
        root.getChildren().addAll(statusLabel, messageLabel, interimResult, resultLabel, progressBar, startButton, cancelButton);

        Scene scene = new Scene(root, 220, 225);

        primaryStage.setTitle(this.getClass().getSimpleName());
        primaryStage.setScene(scene);
        primaryStage.show();

        primaryStage.setOnCloseRequest(we -> {
            executorService.shutdownNow();
        });

        startButton.setOnAction(ae -> {
            resultLabel.setText("RESULT");
            executeBackgroundTask();
        });

    }

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

    private void executeBackgroundTask() {

        Task<Long> task = new Task<>() {
            @Override
            public Long call() {

                int repeatProcessingNumber = 10;
                long result = 1;
                updateMessage("Start!");
                updateValue(result);
                updateProgress(0, repeatProcessingNumber);

                for (int i = 0; i < repeatProcessingNumber; i++) {
                    if (isCancelled()) {
                        System.out.println("This program has been canceled.");
                        break;
                    }

                    try {
                        TimeUnit.MILLISECONDS.sleep(500);
                    } catch (InterruptedException ex) {
                        Logger.getLogger(AsynchronousProcessing4JavaFX.class.getName()).log(Level.SEVERE, null, ex);
                        if (isCancelled()) {
                            System.out.println("Canceled by InterruptedException.");
                            break;
                        }
                    }

                    result += result;
                    updateMessage(String.format("%d/%d", i + 1, repeatProcessingNumber));;
                    updateValue(result);
                    updateProgress(i + 1, repeatProcessingNumber);
                }

                return result;

            }

            // Workerオブジェクトの状態が変化したときのコールバックメソッド(5種類)
            @Override
            protected void scheduled() {
                super.succeeded();
                updateMessage("scheduled!");
                System.out.println("Call scheduled()");
            }

            @Override
            protected void running() {
                super.running();
                updateMessage("running!");
                System.out.println("Call running()");
            }

            @Override
            protected void succeeded() {
                super.succeeded();
                updateMessage("Succeeded!");
                System.out.println("Call succeeded()");
                resultLabel.setText("RESULT: " + getValue().toString());
            }

            @Override
            protected void cancelled() {
                super.cancelled();
                updateMessage("Cancelled!");
                System.out.println("Call cancelled()");
                progressBar.progressProperty().unbind();
                progressBar.setProgress(0.0);
                interimResult.textProperty().unbind();
                interimResult.setText("INTERIM RESULT");
            }

            @Override
            protected void failed() {
                super.failed();
                updateMessage("Failed!");
                System.out.println("Call failed()");
            }
        };

        // WorkerStateEven を使ってWorkerオブジェクトの状態を監視
        task.setOnScheduled(wse -> System.out.println("setOnScheduled"));
        task.setOnRunning(wse -> System.out.println("setOnRunning"));
        task.setOnSucceeded(wse -> System.out.println("setOnSucceeded"));
        task.setOnCancelled(wse -> System.out.println("setOnCancelled"));
        task.setOnFailed(wse -> System.out.println("setOnFailed"));

        // textProperty と Workerオブジェクトの stateProperty をバインド
        statusLabel.textProperty().bind(task.stateProperty().asString());
        // textProperty と Workerオブジェクトの messageProperty をバインド
        messageLabel.textProperty().bind(task.messageProperty());
        // textProperty と Workerオブジェクトの valueProperty をバインド
        interimResult.textProperty().bind(task.valueProperty().asString());
        // progressProperty と Workerオブジェクトの progressProperty をバインド
        progressBar.progressProperty().bind(task.progressProperty());
        // disableProperty と Workerオブジェクトの stateProperty をバインド
        startButton.disableProperty().bind(task.stateProperty().isEqualTo(RUNNING));
        cancelButton.disableProperty().bind(task.stateProperty().isNotEqualTo(RUNNING));

        cancelButton.setOnAction(ae -> {
            task.cancel();
        });

        // ExecutorService を利用してバックグラウンドスレッドを開始
        executorService.submit(task);

    }

}

このプログラムの実行結果を見てみましょう。

5

ついでに NetBeans のプロファイラでバックグラウンドスレッドが生成されているのを確認しましょう。

6

pool-2-thread-1 がバックグラウンドスレッドとして起動されています。

これで JavaFX アプリケーションスレッドはイベント処理が可能となります。

画面が固まることなくバックグラウンドスレッドの状態、計算結果の一部(途中経過)、プログレスバーも表示されています。

また、「START」ボタン、「CANCEL」ボタンの活性化、非活性化もちゃんとできています。

まず、Task クラスをみていきましょう。

今回はcall() メソッドを実行するために java.util.concurrent の ExecutorService インタフェースを利用しました。

184行目です。

executorService.submit(task);

タスクをパラメータとして指定したスレッドの開始としては次の方法もあります。

Thread th = new Thread(task);
th.setDaemon(true);
th.start();

call() メソッドに時間のかかる処理を記述します。バックグラウンドスレッドロジックです。

call() メソッドが呼ばれたら初期値をセットします。

updateMessage() メソッド、updateValue() メソッド、updateProgress() メソッドはプロパティバインドを利用します。

バックグラウンドスレッドが開始されたらキャンセル操作がリクエストされたかも監視しなければいけません。

キャンセルは java.​util.​concurrent.​FutureTask クラスの isCancelled() メソッドを使います。

今回、スレッドをスリープさせているので InterruptedException をスローする場合があります。

また InterruptedException は Task のキャンセルの結果として発生する場合があるため、確実に InterruptedException を処理し、キャンセル状態を調べる必要があります。

それに対応するために 102 行目の catch 節のところにも isCancelled() メソッドを使用しています。

このようにバックグラウンドスレッドロジック内にブロッキング・コールがある場合は注意が必要となります。

バックグラウンドスレッドでの処理の結果の一部を返すために 110 行目に updateValue() メソッドを使います。

同様に 109 行目で messageLabel 、111 行目で progressBar の更新も行っています。

これらの更新は JavaFX のバインドという便利な機能を使います。

Task クラスのプロパティと Label のテキストプロパティ、ProgressBar の プログレスプロパティとバインドします。

168 行目から次のようにバインドしています。

// textProperty と Workerオブジェクトの stateProperty をバインド
statusLabel.textProperty().bind(task.stateProperty().asString());
// textProperty と Workerオブジェクトの messageProperty をバインド
messageLabel.textProperty().bind(task.messageProperty());
// textProperty と Workerオブジェクトの valueProperty をバインド
interimResult.textProperty().bind(task.valueProperty().asString());
// progressProperty と Workerオブジェクトの progressProperty をバインド
progressBar.progressProperty().bind(task.progressProperty());

これで自動的に Task クラス(Workerオブジェクト)のプロパティ updateXXX() メソッドで更新されたら Label 、ProgressBar も更新されます。

もし、キャンセル操作が実行され、プログラムの実行結果の一部が表示されたままになるのが嫌なら

141 行目の Worker オブジェクトの状態が変化したときのコールバックメソッドである cancelled() メソッドの処理でいったん unbind() メソッドによりバインドを解除して初期値を設定し直すといいでしょう。

@Override
protected void cancelled() {
    super.cancelled();
    updateMessage(“Cancelled!”);
    System.out.println(“Call cancelled()”);
    progressBar.progressProperty().unbind();
    progressBar.setProgress(0.0);
    interimResult.textProperty().unbind();
    interimResult.setText(“INTERIM RESULT”);
}

このようなコールバックメソッドはこの他にもあり、全部で5種類あります。

必要に応じて便利に使えます。

118 行目から 158 行目を参照ください。

バックグラウンドタスクが無事に終了したら 134 行目の succeeded() メソッドがコールバックされます。

@Override
protected void succeeded() {
    super.succeeded();
    updateMessage(“Succeeded!”);
    System.out.println(“Call succeeded()”);
    resultLabel.setText(“RESULT: ” + getValue().toString());
}

ここで Task クラスに戻り値がある場合それを取得します。

戻り値の型はジェネリクスの型パラーメターで指定されたものです。

今回のプログラムでは Long となっています。

必然的に Call() メソッドの戻り値も Long です。

計算結果は 138 行目で javafx.​concurrent.​Task クラスの getValue() メソッドで取得しています。

                resultLabel.setText(“RESULT: ” + getValue().toString());

そして、計算結果を Label のテキストとしてセットしています。

これらのコールバックメソッドは JavaFX アプリケションスレッド上で実行されるので RuntimeException も出ません。

コールバックメソッドを利用する以外にも WorkerStateEvent を使ってイベント処理として扱うこともできます。

160 行目から 165 行目のようにイベントハンドラを登録して使用します。

// WorkerStateEven を使ってWorkerオブジェクトの状態を監視
task.setOnScheduled(wse -> System.out.println(“setOnScheduled”));
task.setOnRunning(wse -> System.out.println(“setOnRunning”));
task.setOnSucceeded(wse -> System.out.println(“setOnSucceeded”));
task.setOnCancelled(wse -> System.out.println(“setOnCancelled”));
task.setOnFailed(wse -> System.out.println(“setOnFailed”));

では、このプログラムを動かしたときの Worker オブジェクトの state プロパティ がどのように変化するか確認します。

プログラムが完する場合次のように出力されます。

setOnScheduled
Call scheduled()
setOnRunning
Call running()
setOnSucceeded
Call succeeded()

この出力結果から

SCHEDULED
RUNNING
SUCCEEDED

と遷移しているのが解ります。

プログラムを途中でキャンセルしてみる場合も確認します。

setOnScheduled
Call scheduled()
setOnRunning
Call running()
setOnCancelled
Call cancelled()
Canceled by InterruptedException.

予想通りの結果ですね。

SCHEDULED
RUNNING
CANCELLED

上記のように遷移しています。これらの動作は想像通りでした。

コールバックメソッドより WorkerStateEvent を使ってイベントハンドラを登録したほうが早い結果となっています。

だからといってメリット、デメリットがあるかどうかは私には解りません。

さて、最後に、「START」ボタン、「CANCEL」ボタンの活性化、非活性化をみてみましょう。

175 行目から 177 行目です。

// disableProperty と Workerオブジェクトの stateProperty をバインド
startButton.disableProperty().bind(task.stateProperty().isEqualTo(RUNNING));
cancelButton.disableProperty().bind(task.stateProperty().isNotEqualTo(RUNNING));

Button の disableProperty と Workerオブジェクトの stateProperty をバインドしているだけです。

startButton は Workerオブジェクトの stateProperty が RUNNING なら非活性化となります。

cancelButton は Workerオブジェクトの stateProperty が RUNNING でなければ非活性化となります。

つまり、Workerオブジェクトの stateProperty が RUNNING の場合だけ活性化します。

これでいちいち操作を行うたびに Button を活性化、非活性化処理を記述しなくてすみます。

このように JavaFX には非同期処理を簡単に扱うことができるようになっています。

SwingWorker を使って非同期処理プログラムを組んでいるのなら JavaFX の javafx.concurrentパッケージを使ってみて時代の流れを感じ取ってみてはいかがでしょうか。

お終い!

I wish you a Merry Christmas.