JavaFXのPrintAPIを使ってはがきの印刷をしてみた。(文面)

このエントリーは じゃばえふえっくす Advent Calendar 2018 の12月22日分です。

JavaFX 8でPrintAPIが追加されて随分経ちます。

ネット上にもいろんな情報が共有されるようになりました。

そこではがき印刷をJavaFXのPrintAPIを使ってやってみようと思い立ちました。

宛名のほうは位置決めとか縦書きとかめんどくさいのでパスとします。(^_^;)

あっ・・・ めんどくさいじゃなくて解らないって言った方が近いもしれません。

縦書きなんかは一文字ずつ分解してFontMetricsを取得して構築するのかな?

さて、とりあえずはがき印刷のゴールは文面サイドだけにします。

シンプルにイメージファイルと賀詞を入れるだけのものとします。

まずJavaFXのPrintAPIについてざっくりと調べてみることにします。

 

javafx.graphicsモジュールにあるjavafx.printパッケージに九つのクラスと七つの列挙型があります。

クラス
JobSettings
PageLayout
PageRange
Paper
PaperSource
Printer
PrinterAttributes
PrinterJob
PrintResolution

列挙型
Collation
PageOrientation
PrintColor
Printer.MarginType
PrinterJob.JobStatus
PrintQuality
PrintSides

ぱっと見、馴染みやすそうな構成になっています。

まずPrinterクラスがどんなものなのか見てみます。

Printerクラス

このクラスにはデフォルトプリンター、全てのプリンター、プリンター名を取得するメソッドなどが用意されています。

それとプリンターのデフォルトのページ・レイアウトを取得するメソッドや指定されたパラメータを使って新しいPageLayoutインスタンスを取得するものがあります。

あとPrinterAttributesクラスを返すメソッドが一つあります。

public PrinterAttributes getPrinterAttributes() 

プリンタの属性および機能をカプセル化する委譲オブジェクトを取得します。

このメソッドを利用してプリンターの各種属性値を取得可能となります。

それでは次に上記のメソッドで返されるプリンターの属性値をカプセルかするPrinterAttributesクラスを見てみます。

PrinterAttributesクラス

このクラスは、ジョブ印刷機能に関連するプリンタ属性とその他の属性をカプセル化します。

デフォルトプリンターのデフォルトの属性値の取得、サポートされている属性値を取得することができます。

丁合い設定、印刷部数、用紙の向き、用紙サイズ、給紙方法、色設定、品質設定、印刷解像度、両面印刷設定、最大部数、ページ範囲など。

属性値にはあらかじめ列挙型が次の7種類用意されています。

列挙型
Collation
PageOrientation
PrintColor
Printer.MarginType
PrinterJob.JobStatus
PrintQuality
PrintSides

それでは次にPaperクラスを見てみます。

Paperクラス

このクラスはプリンター用紙のサイズをカプセル化するクラスになります。

用紙のサイズを指定するのに20個のstaticフィールド値があらかじめ用意されています。

ただ、使用するプリンターでそれがサポートされているかの確認は必要でしょう。

あと、プリンター用紙のサイズ取得のメソッドと用紙の名前を取得するメソッドが用意されています。

サイズはポイント(1/72インチ)単位となっています。

DTPやってる人はご存じかもしれませんが一般人にはあまり馴染みのない単位です。

次はPaperSourceクラスを見ていきましょう。

PaperSourceクラス

プリンター用紙に使用される給紙トレイ、給紙方法を扱うクラスになります。

9個のstaticフィールド値があらかじめ用意されています。

あと給紙方法の名前を返すメソッドが一つ用意されています。

次はPrintResolutionクラスを見ます。

PrintResolutionクラス

プリンターのサポートされている解像度(送り方向と前後送り方向)を1インチ当たりのドット数(DPI)で表すクラスです。

用紙の前後送り方向の解像度(dpi単位)、用紙の送り方向の解像度(dpi単位)を返すメソッドが二つ用意されています。

次はPrinterJobクラスです。

PrinterJobクラス

PrinterJobは、JavaFXシーングラフ印刷のルートとなります。

これには、次のものが含まれます。

  • プリンタの検出
  • ジョブの作成
  • サポートされるプリンタの機能に基づいたジョブの構成
  • ページの設定
  • ノード階層のページへのレンダリング。

APIドキュメントには非常にシンプルに印刷がされるように記述されています。

注意事項としては印刷と並行してノードを更新するのは避けてください。

と記述されています。

これはそうだろうなと誰もが思うだろうけど、ちょっと解りづらいというか・・・結局どうなの?って記述が下記のようにされてます。

FXアプリケーション・スレッドで印刷を実行する際の必要条件はありません。 ノードの印刷準備やジョブの呼出しはどのスレッドでも行うことができます。 ただし、アプリケーションUIの応答性に影響が及ばないように、FXアプリケーション・スレッドで実行される処理の量を最小限に抑えるのが一般的に望ましいと言えます。 したがって、印刷は新しいスレッドで実行し、実装内部のスケジューリングによって、FXスレッドで実行する必要のあるすべてのタスクがそのスレッドで実行されるようにすることをお薦めします。

FXアプリケーション・スレッドで印刷をできなくもないけど印刷用の新しいスレッドを用意して実行してくれってことでいいのかな?

今回のはがき印刷のプログラムではお勧めの印刷用の新しいスレッドを用意させていただくとしよう。

さて、このPrinterJobクラスには14個のメソッドと印刷ジョブのステータスをレポートする際に使われるネストされたstaticクラス列挙型PrinterJob.JobStatusがあります。

印刷ジョブのステータスを確認するにあたり、この列挙型PrinterJob.JobStatusの列挙型定数はありがたいです。

CANCELED     ジョブはアプリケーションによって取り消されました。
DONE   ジョブが印刷を開始し、その後endJob()を呼び出したところ、成功とレポートされました。
ERROR    ジョブの実行中にエラーが発生しました。
NOT_STARTED  新しいジョブのステータス。
PRINTING   ジョブは1ページ以上の印刷をリクエストし、まだ印刷を終了していません。

ジョブの一般的なライフサイクルは次のとおりです:

  • ジョブは、ステータスNOT_STARTEDで作成され、ダイアログなどでの構成中、このステータスにとどまります。
  • ジョブは、最初のページが印刷されると、PRINTING状態になります。
  • ジョブは、取り消されたり、エラーが発生したりすることなく正常に完了すると、DONE状態になります。 これでジョブが完了しました。
  • エラーが発生したERRORのジョブやCANCELEDのジョブも完了したと見なされます。

ジョブがライフサイクル中に前のステータスに戻ることはできず、現在のジョブ状態は実行可能な操作に影響を及ぼします。

たとえば、すでに印刷状態を過ぎて終了状態のいずれかになったジョブが再度印刷を開始することはできません。

PrinterJobクラス14個のメソッドはジョブの作成、取り消しステータス取得などのメソッドがあります。

あとジョブの構成オプションを取得するものや、ジョブに使われるプリンターを取得するもの、ジョブに使うプリンターの変更やネイティブな印刷ダイアログ、ページ設定ダイアログを表示するものなどがあります。

それではJobSettingsクラスを見ていきましょう。

JobSettingsクラス

JobSettingsクラスは、印刷ジョブの構成のほとんどをカプセル化します。

アプリケーションでJobSettingsインスタンスのセットを直接作成したり設定したりすることはありません(できません)。

印刷ジョブが作成された時点で、すでにそのジョブに1つインストールされています。

このクラスは印刷ジョブを構成するためのメソッドが30個あります。

ジョブの名前の設定、取得ページレイアウト、印刷品質、部数などだいたい必要なものがそろっています。

このJobSettingsクラスでジョブ構成を設定してもネイティブな印刷ダイアログを使用すれば設定を変更できてしまうようです。

次はPageLayoutクラスを見てみます。

PageLayoutクラス

PageLayoutは、コンテンツのレイアウトに必要な情報をカプセル化します。

8個のメソッドが用意されています。

それらを使ってページ・レイアウトのマージン(ポイント単位)、用紙、ページの印刷可能領域のデータ、向きを取得できます。

最後にPageRangeクラスを見ます。

PageRangeクラス

PageRangeは、印刷するジョブ印刷ストリーム・ページを選択または制限するために使用されます。

ページ番号は1から始まり、ユーザーの指定内容に対応します。

開始ページはゼロより大きく、終了ページ以下である必要があります。

開始と終了が同一の場合、範囲は単一ページを表します。

ジョブのページ数を超える値は、印刷時に単に無視されます。

各クラスの関連は下図のような感じですね。

b1

 

APIドキュメントを読んでいるだけでは面白くないのでとりあえず超シンプルなものを組んで印刷してみよう。

テキストエリアにデフォルトプリンターの名前、プリンター一覧の表示、そして印刷ジョブに使用するプリンターの設定、プリンターの属性の一部を表示させます。

UIはこれから作成予定のはがき印刷のものを流用しているのでちょっと変なことなってますがそれはきにしないでくださいませ。

これらの処理をトグルボタンが押されたら開始するように次のようにプログラムを組んでみました。

 


    @FXML
    void handlePrinterInfoToggleButtonAction(ActionEvent event) {
        if (printerInfoToggleButton.isSelected()) {

            // デフォルトプリンター EPSON EP-806A Series
            var defaultPrinter = Printer.getDefaultPrinter();
            printerTextArea.setText("DefaultPrinter: " + defaultPrinter.getName() + CR);
            // プリンター一覧
            printerTextArea.appendText(CR + "[プリンター一覧]" + CR);
            Printer.getAllPrinters()
                    .stream()
                    .forEach(e -> {
                        printerTextArea.appendText(e.getName() + CR);
                    });

            // プリンター選択 (デフォルトでないプリンターを選択しています)
            var targetPrinter = Printer.getAllPrinters()
                    .stream()
                    // プリンター一覧に存在するが指定できないのでデフォルトプリンターが返される。
                    // OSのプリンターの設定で"EP-808A"のようにリネームすれば指定できた。
                    .filter(p -> p.getName().matches("EP-808A Series(ネットワーク)"))
                    .findFirst()
                    .orElseGet(() -> {
                        printerTextArea.appendText(CR + "指定されたプリンターが見つからなかったのでデフォルトプリンターを使います。");
                        return defaultPrinter;
                    });

            printerTextArea.appendText(CR + "使用するプリンター: " + targetPrinter.getName() + CR);

            // デフォルトプリンターの属性
            var attributes = defaultPrinter.getPrinterAttributes();
            printerTextArea.appendText(CR + "[サポートされる向き]" + CR);
            attributes.getSupportedPageOrientations()
                    .forEach(a -> {
                        printerTextArea.appendText(a.name() + CR);
                    });

            printerTextArea.appendText(CR + "[サポートされる給紙方法(給紙ビンや給紙トレイ)]" + CR);
            attributes.getSupportedPaperSources()
                    .forEach(a -> {
                        printerTextArea.appendText(a.getName() + CR);
                    });

            printerTextArea.setVisible(true);
            printerInfoToggleButton.setText("Hide Info");
        } else {
            printerTextArea.clear();
            printerTextArea.setVisible(false);
            printerInfoToggleButton.setText("Printer Info");
        }
    }

プログラムの実行結果は次のようになります。

1

まず、デフォルトプリンターを取得するためにPrinterクラスのstaticメソッドgetDefaultPrinter()を使います。

そしてデフォルトプリンターの名前を取得するためにPrinterクラスのString getName()メソッドを使いました。

次にプリンター一覧を表示させるために全てのプリンターを取得します。

PrinterクラスのstaticメソッドObservableSet<Printer> getAllPrinters()を使います。

プリンターの名前を取得するために先ほどと同様にPrinterクラスのString getName()メソッドを使います。

そしてプリンターの選択をしてみます。

現在のシステムに組み込まれているプリンターは下図のようになっています。

printer

デフォルトプリンターは”EPSON EP-806A Series”で間違いないのでEP-808A Series(ネットワーク)を指定してみました。

ところが予期せぬ結果となりました。

なんと、プログラムのプリンター一覧の表示結果では確かにあるのに指定できません。

T orElseGet(Supplier<? extends T> supplier)でデフォルトプリンターが返されます。

これの原因は調べてないので解りませんが別のPCでも同じでしたのでJavaFX側の問題かもしれません。

この解決策はOS側のプリンター設定で”EP-808A”のようにリネームすれば指定できました。

次にデフォルトプリンターの属性を取得しています。

var attributes = defaultPrinter.getPrinterAttributes();

PrinterクラスのPrinterAttributes getPrinterAttributes()メソッドでプリンタの属性および機能をカプセル化する委譲オブジェクトを取得します。

そしてそれを使い各種属性値を取得表示させています。

PrinterAttributesクラスのSet<PageOrientation> getSupportedPageOrientations()でサポートされる向きを取得、

Set<PaperSource> getSupportedPaperSources()でサポートされる給紙方法(給紙ビンや給紙トレイ)を取得しています。

解りやすくフレンドリーなAPIがそろってます。

それではこれを印刷するコードを次のように追加しました。

注意: ステータスを確認するのにスレッドを少しスリープさせています。

 


    private void printPrinterInfo(TextArea printerTextArea) {

        jobStatusLabel.setText("Create a printer job.");

        // ジョブステータスラベル確認用スリープ
        if (DEBUG) {
            jobSleep();
        }

        // デフォルトプリンター
        var defaultPrinter = Printer.getDefaultPrinter();
        // デフォルトプリンターを使用してプリンタージョブを作成
        var job = PrinterJob.createPrinterJob(defaultPrinter);
        // ジョブステータス確認用
        System.out.println("JobStatus: " + job.jobStatusProperty().asString().get());
        jobStatusLabel.setText(job.jobStatusProperty().asString().get());
        // ジョブステータスラベル確認用スリープ
        if (DEBUG) {
            jobSleep();
        }
        // プリンタージョブステータス
        job.jobStatusProperty().addListener(e -> {
            System.out.println("JobStatus: " + job.jobStatusProperty().asString().get());
            jobStatusLabel.setText(job.jobStatusProperty().asString().get());
            // ジョブステータスラベル確認用スリープ
            if (DEBUG) {
                jobSleep();
            }
        });

        // ジョブ設定
        var jobSettings = job.getJobSettings();
        // ジョブの名前
        jobSettings.setJobName("Greeting");

        if (job != null) {

            // ネット上でよく見る駄目な印刷ダイアログの使用方法
            // これだとキャンセルしても印刷開始してしまう。
//            job.showPrintDialog(anchorPane.getScene().getWindow());

            // 印刷ダイアログ
            var nativeJob = job.showPrintDialog(anchorPane.getScene().getWindow());
            // 印刷ダイアログでキャンセルした場合の処理
            if (!nativeJob) {
                System.out.println("Job has been canceled.");
                job.cancelJob();
            }

            // ノード(TextArea printerTextArea)をプリント
            boolean success = job.printPage(printerTextArea);
            if (success) {
                // プリンタのキューに正常にスプールされたら true 
                job.endJob();
            } else {
                System.out.println("Printing failed.");
            }

        } else {
            jobStatusLabel.setText("Could not create printer job.");
        }

    }

    private void jobSleep() {
        try {
            Thread.sleep(1_500);
        } catch (InterruptedException ex) {
            // ナイスキャッチ!
            Logger.getLogger(FXMLDocumentController.class.getName()).log(Level.SEVERE, null, ex);

        }
    }

 

PrinterJobクラスのstatic final PrinterJob createPrinterJob(Printer printer)メソッドでデフォルトプリンターを引数にとり新しいPrinterJobを作成します。

印刷に必要ではないけどJobStatus確認用にリスナーつけたり、ラベル、標準出力への表示などをしています。

PrinterJobクラスのsynchronized JobSettings getJobSettings()メソッドでジョブ構成オプション(部数、丁合いオプション、両面オプションなど)をカプセル化します。

初期値はプリンターの現在の設定に基づいています。

次にJobSettingsクラスのvoid setJobName(String name)メソッドでジョブの名前を設定します。

このプログラムではOSネイティブの印刷ダイアログを使用しました。

PrinterJobクラスのsynchronized boolean showPrintDialog(Window owner)を使います。

私はJavaFX 8がリリースされた当時このPrintAPIを早く試してみたい(いじってみたい)あまりにとんでもない間違いを犯していました。

当時、私は印刷ダイアログを呼び出して使うのに次のようにコードを書いていました。

job.showPrintDialog(anchorPane.getScene().getWindow());

これでも動くことは動くけどキャンセルしても印刷が実行されてしまいます。

twitterでこのことをつぶやいて何人かの方にリツイートされてしまい悪いことをしていまいました。

よくよく考えたらなんのためにboolean型の戻り値があるのかってことですね。

ネット上では印刷ダイアログを呼び出し使う方法がよくありますがほとんどキャンセル処理をしていません。

よって正しく使うためには印刷ダイアログとキャンセル処理は抱き合わせて使用することをお勧めします。

// 印刷ダイアログ
var nativeJob = job.showPrintDialog(anchorPane.getScene().getWindow());
// 印刷ダイアログでキャンセルした場合の処理
if (!nativeJob) {
    System.out.println(“Job has been canceled.”);
    job.cancelJob();
}

 

そして最後に印刷を開始するためにPrinterJobクラスのsynchronized boolean printPage(Node node)メソッドを使います。

引数のNodeが印刷対象となっています。

印刷に成功すればtrueが返されます。

印刷に成功したことを確認したらPrinterJobクラスのsynchronized boolean endJob()メソッドでジョブをプリンタのキューに正常にスプールされたらtrueを返します。

falseを返されたらプリンタージョブが失敗なのでそれなりの対処をします。

 

2

 

 

3

実際に印刷されたものがこのようになります。

a6

簡単ですね!

これで基本的なPrinter APIを扱うことが可能となったのではがき印刷のプログラムを組むことにします。

下図のようなGUIのプログラムを組んでみました。

21

Printボタンをクリックすると印刷のためのワーカースレッドが起動して印刷をはじめます。

いちおうキャンセルボタンもつけました。

Printer Infoトグルボタンはシステムのプリンターの属性を表示させています。

Open Imageボタンは画像を変更するためのファイルチューザーを出します。

賀詞を入れるためのテキストフィールドとジョブステータス表示用のラベルも用意しました。

印刷に関するコードを下記に示します。

 


    private void printGreeting(Group greeting) {

        printTask = new Task<>() {

            private PageLayout pageLayout;

            @Override
            public Boolean call() {

                Platform.runLater(() -> {
                    jobStatusLabel.setText("Create a printer job.");
                });

                // ジョブステータスラベル確認用スリープ
                if (DEBUG) {
                    jobSleep();
                }

                // プリンタージョブを作成
//                // デフォルトプリンターを使ってプリンタージョブを作成
//                var job = PrinterJob.createPrinterJob();
                // プリンターを指定してプリンタージョブを作成
                var job = PrinterJob.createPrinterJob(targetPrinter);

                // ジョブステータス確認用
                System.out.println("JobStatus: " + job.jobStatusProperty().asString().get());
                Platform.runLater(() -> {
                    jobStatusLabel.setText(job.jobStatusProperty().asString().get());
                });

                // ジョブステータスラベル確認用スリープ
                if (DEBUG) {
                    jobSleep();
                }

                // プリンタージョブステータス
                job.jobStatusProperty().addListener(e -> {
                    System.out.println("JobStatus: " + job.jobStatusProperty().asString().get());
                    Platform.runLater(() -> {
                        jobStatusLabel.setText(job.jobStatusProperty().asString().get());
                    });

                    // ジョブステータスラベル確認用スリープ
                    if (DEBUG) {
                        jobSleep();
                    }

                    // JobStatus が PRINTING の状態で強制的にキャンセル
                    if (CANCEL_DEBUG && PrinterJob.JobStatus.PRINTING == job.jobStatusProperty().get()) {
                        System.out.println("JobStatus が PRINTING の状態で Cancel");
                        job.cancelJob(); // JobStatus: CANCELED -> JobStatus: ERROR となる。
                    }
                });

                // ジョブ設定
                var jobSettings = job.getJobSettings();
                // ジョブの名前
                jobSettings.setJobName("Greeting");
                // 自動ではがきに印刷
                jobSettings.setPaperSource(PaperSource.AUTOMATIC);
//                // この設定でもはがきに印刷
//                jobSettings.setPaperSource(PaperSource.TOP);
                // ページレイアウト設定
                pageLayout = targetPrinter.createPageLayout(Paper.JAPANESE_POSTCARD, PageOrientation.LANDSCAPE, Printer.MarginType.HARDWARE_MINIMUM);
                // ジョブにページレイアウトを設定
                jobSettings.setPageLayout(pageLayout);
                // 印刷カラーを設定
                jobSettings.setPrintColor(PrintColor.COLOR);
                // 印刷品質を設定
                jobSettings.setPrintQuality(PrintQuality.NORMAL);
                // 印刷部数を設定
                jobSettings.setCopies(1);
                // 部単位で印刷設定
                jobSettings.setCollation(Collation.UNCOLLATED);

                if (job != null) {

//                        // 用紙設定ダイアログ
//                        var nativePageSetup = job.showPageSetupDialog(anchorPane.getScene().getWindow());
//                        // 用紙設定ダイアログキャンセル
//                        if (!nativePageSetup) {
//                            System.out.println("PageSetup has been canceled.");
//                            job.cancelJob();
//                            printTask.cancel();
//                        }

//                        // 印刷ダイアログ
//                        var nativeJob = job.showPrintDialog(anchorPane.getScene().getWindow());
//                        // 印刷ダイアログでキャンセルした場合の処理
//                        if (!nativeJob) {
//                            System.out.println("Job has been canceled.");
//                            job.cancelJob();
//                            printTask.cancel();
//                        }

                    // タスクのキャンセル処理
                    if (isCancelled()) {
                        if (PrinterJob.JobStatus.PRINTING == job.jobStatusProperty().get() 
                                || PrinterJob.JobStatus.NOT_STARTED == job.jobStatusProperty().get()) {
                            // プリンタージョブをキャンセル
                            job.cancelJob();
                            System.out.println("Cancel printing.");
                            return false;
                        } else {
                            System.out.println("Job has already been canceled.");
                        }
                        return false;
                    }

                    // ノード(Group greeting)をプリント
                    boolean success = job.printPage(greeting);
                    if (success) {
                        // プリンタのキューに正常にスプールされたら true 
                        return job.endJob();
                    } else {
                        System.out.println("Printing failed.");
                        return false;
                    }
                } else {
                    Platform.runLater(() -> {
                        jobStatusLabel.setText("Could not create printer job.");
                    });
                    return false;
                }
            }

            private void jobSleep() {
                try {
                    Thread.sleep(1_500);
                } catch (InterruptedException ex) {
                    // ナイスキャッチ!
                    if (isCancelled()) {
                        System.out.println("Canceled by InterruptedException: sleep interrupted");
                    } else {
                        Logger.getLogger(FXMLDocumentController.class.getName()).log(Level.SEVERE, null, ex);
                    }
                }
            }

        };

        executorService.submit(printTask);

        // WorkerStateEvent を使ってWorkerオブジェクトの状態を監視
        printTask.setOnScheduled(wse -> {
            System.out.println("WorkerState: Scheduled");
        });
        printTask.setOnRunning(wse -> {
            System.out.println("WorkerState: OnRunning");
        });
        printTask.setOnSucceeded(wse -> {
            System.out.println("WorkerState: Succeeded");
        });
        printTask.setOnCancelled(wse -> {
            System.out.println("WorkerState: Cancelled");
        });
        printTask.setOnFailed(wse -> {
            System.out.println("WorkerState: Failed");
        });

        // disableProperty と Workerオブジェクトの stateProperty をバインド
        gashiTextField.disableProperty().bind(printTask.stateProperty().isEqualTo(RUNNING));
        printButton.disableProperty().bind(printTask.stateProperty().isEqualTo(RUNNING));
        cancelButton.disableProperty().bind(printTask.stateProperty().isNotEqualTo(RUNNING));
        openImageButton.disableProperty().bind(printTask.stateProperty().isEqualTo(RUNNING));
        printerInfoToggleButton.disableProperty().bind(printTask.stateProperty().isEqualTo(RUNNING));

    }

コードが長くなっていますが基本的なことは先ほどのテキストエリアの文字列を印刷するものと何ら変わりはありません。

ただ、APIドキュメントでお勧めされているように印刷用のスレッドを別に起動させて印刷をしています。

今回のプログラムでは印刷ダイアログは使用せずJavaFXがはじめから用意してあるものを使用してみます。

まず、PrinterJobクラスのsynchronized JobSettings getJobSettings()を使いをカプセル化されたジョブ構成オプションを取得します。

JobSettingsクラスのsetterメソッドを使い設定します。

void setJobName(String string)メソッドで名前をつけます。

void setPaperSource(PaperSource ps)メソッドで給紙方法を設定します。

void setPageLayout(PageLayout pl)メソッドで使用するPageLayoutを設定します。

void setPrintColor(PrintColor pc)メソッドで印刷カラーを設定します。

void setPrintQuality(PrintQuality pq)メソッドで印刷品質を設定します。

final void setCopies(int i)メソッド印刷部数を設定します。

void setCollation(Collation cltn)メソッドで部単位で印刷設定をします。

カプセル化されているのでgetter、setterメソッドでアクセスできるのは楽ですね

ページレイアウトの設定はPrinterクラスのPageLayout createPageLayout​(Paper paper, PageOrientation orient, Printer.MarginType mType)メソッドを使い設定します。

印刷に関するものはこの程度ですね。

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

プリンターの印刷される音が鳴り止んではがきをとりにいって唖然としました。

こんなふうに印刷されていました。

a1

左上から少しだけ画像が切り取られて印刷されただけです。

これでは印刷はできたけどこれをもらった人はなんだろう?と首をかしげるに違いない!

プログラムを修正することにします。

何故このようになったのか?

はがきの印刷領域より画像が大きすぎたからと思われる。

左上の隅はJavaの2Dでは原点(0, 0)になるからそこから印刷可能な範囲だけ画像が印刷されたのだろう。

では残りの部分を印刷可能領域分だけ画像を位置変更して必要なだけ印刷すれば画像全体が印刷できるんじゃないか。

では早速コードを修正してみよう。

 


    private void printGreeting(Group greeting) {

        printTask = new Task<>() {

            private PageLayout pageLayout;

            @Override
            public Boolean call() {

                Platform.runLater(() -> {
                    jobStatusLabel.setText("Create a printer job.");
                });

                // ジョブステータスラベル確認用スリープ
                if (DEBUG) {
                    jobSleep();
                }

                // プリンタージョブを作成
//                // デフォルトプリンターを使ってプリンタージョブを作成
//                var job = PrinterJob.createPrinterJob();
                // プリンターを指定してプリンタージョブを作成
                var job = PrinterJob.createPrinterJob(targetPrinter);

                // ジョブステータス確認用
                System.out.println("JobStatus: " + job.jobStatusProperty().asString().get());
                Platform.runLater(() -> {
                    jobStatusLabel.setText(job.jobStatusProperty().asString().get());
                });

                // ジョブステータスラベル確認用スリープ
                if (DEBUG) {
                    jobSleep();
                }

                // プリンタージョブステータス
                job.jobStatusProperty().addListener(e -> {
                    System.out.println("JobStatus: " + job.jobStatusProperty().asString().get());
                    Platform.runLater(() -> {
                        jobStatusLabel.setText(job.jobStatusProperty().asString().get());
                    });

                    // ジョブステータスラベル確認用スリープ
                    if (DEBUG) {
                        jobSleep();
                    }

                    // JobStatus が PRINTING の状態で強制的にキャンセル
                    if (CANCEL_DEBUG && PrinterJob.JobStatus.PRINTING == job.jobStatusProperty().get()) {
                        System.out.println("JobStatus が PRINTING の状態で Cancel");
                        job.cancelJob(); // JobStatus: CANCELED -> JobStatus: ERROR となる。
                        printTask.cancel();
                    }
                });

                // ジョブ設定
                var jobSettings = job.getJobSettings();
                // ジョブの名前
                jobSettings.setJobName("Greeting");
                // 自動ではがきに印刷
                jobSettings.setPaperSource(PaperSource.AUTOMATIC);
//                // この設定でもはがきに印刷
//                jobSettings.setPaperSource(PaperSource.TOP);
                // ページレイアウト設定
                pageLayout = targetPrinter.createPageLayout(Paper.JAPANESE_POSTCARD,
                        PageOrientation.LANDSCAPE, Printer.MarginType.HARDWARE_MINIMUM);
                // ジョブにページレイアウトを設定
                jobSettings.setPageLayout(pageLayout);
                // 印刷カラーを設定
                jobSettings.setPrintColor(PrintColor.COLOR);
                // 印刷品質を設定
                jobSettings.setPrintQuality(PrintQuality.NORMAL);
                // 印刷部数を設定
                jobSettings.setCopies(1);
                // 部単位で印刷設定
                jobSettings.setCollation(Collation.UNCOLLATED);

                // 印刷用紙に印刷可能な幅と高さ
                double printableWidth = pageLayout.getPrintableWidth();
                double printableHeight = pageLayout.getPrintableHeight();

                // 印刷エリアの設定(印刷対象ノードをそのままの大きさで設定)
                printAreaRectangle = new Rectangle(greeting.getBoundsInParent().getWidth(),
                        greeting.getBoundsInParent().getHeight(), null);
                // 印刷エリアの位置と大きさ
                double printRectX = printAreaRectangle.getX();
                double printRectY = printAreaRectangle.getY();
                double printRectWidth = printAreaRectangle.getWidth();
                double printRectHeight = printAreaRectangle.getHeight();

                // 印刷に必要な用紙枚数(行、列)
                int rows = (int) Math.ceil(printRectHeight / printableHeight);
                int columns = (int) Math.ceil(printRectWidth / printableWidth);

                // ノードのクリップを保存
                var oldClip = greeting.getClip();
                var oldTransforms = new ArrayList<>(greeting.getTransforms());
                // printAreaRectangle のエリアをクリップとして設定する
                greeting.setClip(new javafx.scene.shape.Rectangle(printRectX, printRectY,
                        printRectWidth, printRectHeight));
                // 0,0 に移動
                greeting.getTransforms().add(new Translate(-printRectX, -printRectY));
                // 印刷ページに適合するようにノードを移動する変換
                var gridTransform = new Translate();
                greeting.getTransforms().add(gridTransform);

                if (job != null) {

//                        // 用紙設定ダイアログ
//                        var nativePageSetup = job.showPageSetupDialog(anchorPane.getScene().getWindow());
//                        // 用紙設定ダイアログキャンセル
//                        if (!nativePageSetup) {
//                            System.out.println("PageSetup has been canceled.");
//                            job.cancelJob();
//                            printTask.cancel();
//                        }

//                        // 印刷ダイアログ
//                        var nativeJob = job.showPrintDialog(anchorPane.getScene().getWindow());
//                        // 印刷ダイアログでキャンセルした場合の処理
//                        if (!nativeJob) {
//                            System.out.println("Job has been canceled.");
//                            job.cancelJob();
//                            printTask.cancel();
//                        }

                    // タスクのキャンセル処理
                    if (isCancelled()) {
                        if (PrinterJob.JobStatus.PRINTING == job.jobStatusProperty().get()
                                || PrinterJob.JobStatus.NOT_STARTED == job.jobStatusProperty().get()) {
                            // プリンタージョブをキャンセル
                            job.cancelJob();
                            System.out.println("Cancel printing.");
                            return false;
                        } else {
                            System.out.println("Job has already been canceled.");
                        }
                        return false;
                    }

                    boolean success = true;
                    // 印刷ページごとに、ノードを移動
                    for (int row = 0; row < rows; row++) {
                        for (int col = 0; col < columns; col++) {
                            gridTransform.setX(-col * printableWidth);
                            gridTransform.setY(-row * printableHeight);
                            if (DEBUG) {
                                jobSleep();
                            }
                            success &= job.printPage(pageLayout, greeting);
                        }
                    }
                    if (success) {
                        // ノードを元に戻す
                        restoreNode(greeting, oldTransforms, oldClip);
                        return job.endJob();

                    } else {
                        // ノードを元に戻す
                        restoreNode(greeting, oldTransforms, oldClip);
                        System.out.println("Printing failed.");
                        return false;
                    }
                } else {
                    Platform.runLater(() -> {
                        jobStatusLabel.setText("Could not create printer job.");
                    });
                    // ノードを元に戻す
                    restoreNode(greeting, oldTransforms, oldClip);
                    return false;
                }
            }

            private void jobSleep() {
                try {
                    Thread.sleep(1_500);
                } catch (InterruptedException ex) {
                    // ナイスキャッチ!
                    if (isCancelled()) {
                        System.out.println("Canceled by InterruptedException: sleep interrupted");
                    } else {
                        Logger.getLogger(FXMLDocumentController.class.getName()).log(Level.SEVERE, null, ex);
                    }
                }
            }

            // ノードを元に戻す
            private void restoreNode(Group greeting, List<Transform> oldTransforms, Node oldClip) {
                greeting.getTransforms().clear();
                greeting.getTransforms().addAll(oldTransforms);
                greeting.setClip(oldClip);
            }

        };

        executorService.submit(printTask);

        // WorkerStateEvent を使ってWorkerオブジェクトの状態を監視
        printTask.setOnScheduled(wse -> {
            System.out.println("WorkerState: Scheduled");
        });
        printTask.setOnRunning(wse -> {
            System.out.println("WorkerState: OnRunning");
        });
        printTask.setOnSucceeded(wse -> {
            System.out.println("WorkerState: Succeeded");
        });
        printTask.setOnCancelled(wse -> {
            System.out.println("WorkerState: Cancelled");
        });
        printTask.setOnFailed(wse -> {
            System.out.println("WorkerState: Failed");
        });

        // disableProperty と Workerオブジェクトの stateProperty をバインド
        gashiTextField.disableProperty().bind(printTask.stateProperty().isEqualTo(RUNNING));
        printButton.disableProperty().bind(printTask.stateProperty().isEqualTo(RUNNING));
        cancelButton.disableProperty().bind(printTask.stateProperty().isNotEqualTo(RUNNING));
        openImageButton.disableProperty().bind(printTask.stateProperty().isEqualTo(RUNNING));
        printerInfoToggleButton.disableProperty().bind(printTask.stateProperty().isEqualTo(RUNNING));

    }

さぁ、これで理論上は問題ないはず!

さっそくプログラムを実行して印刷してみよう。

a2

印刷結果はこうなりました。

無事に印刷できたけど9枚のはがきが消費されました。

これを年賀状でだしたら受け取った人はパズルをすることになります。(^_^;)

わたしはなんと愚かな発想をしてしまったのだろう・・・

印刷可能領域に合わせるように画像をスケーリングしてしまえば良いだけのことじゃないですか!

せっかくだからこの複数枚印刷の機能を残したまま、縮小印刷機能を実装することにします。

使う用紙の印刷可能領域に画像をあわせるだけだから簡単ですね。

 


                // Single page printing /////////////////////////////////////////////
                if (scaling.get()) {
                    if (job != null) {
                        // 印刷対象ノードを印刷範囲内にスケーリング
                        fit2PrintableSize();

//                        // 用紙設定ダイアログ
//                        var nativePageSetup = job.showPageSetupDialog(anchorPane.getScene().getWindow());
//                        // 用紙設定ダイアログキャンセル
//                        if (!nativePageSetup) {
//                            System.out.println("PageSetup has been canceled.");
//                            job.cancelJob();
//                            printTask.cancel();
//                        }

//                        // 印刷ダイアログ
//                        var nativeJob = job.showPrintDialog(anchorPane.getScene().getWindow());
//                        // 印刷ダイアログでキャンセルした場合の処理
//                        if (!nativeJob) {
//                            System.out.println("Job has been canceled.");
//                            job.cancelJob();
//                            printTask.cancel();
//                        }

                        // タスクのキャンセル処理
                        if (isCancelled()) {
                            if (PrinterJob.JobStatus.PRINTING == job.jobStatusProperty().get() 
                                    || PrinterJob.JobStatus.NOT_STARTED == job.jobStatusProperty().get()) {
                                // プリンタージョブをキャンセル
                                job.cancelJob();
                                System.out.println("Cancel printing.");
                                if (scaling.get()) {
                                    // 印刷対象ノードを元の大きさにスケーリング
                                    fit2ImageSize();
                                }
                                return false;
                            } else {
                                System.out.println("Job has already been canceled.");
                            }
                            return false;
                        }

                        // ノード(Group greeting)をプリント
                        boolean success = job.printPage(greeting);
                        if (success) {
                            // 印刷対象ノードを元の大きさにスケーリング
                            fit2ImageSize();
                            // プリンタのキューに正常にスプールされたら true 
                            return job.endJob();
                        } else {
                            System.out.println("Printing failed.");
                            // 印刷対象ノードを元の大きさにスケーリング
                            fit2ImageSize();
                            return false;
                        }
                    } else {
                        Platform.runLater(() -> {
                            jobStatusLabel.setText("Could not create printer job.");
                        });
                        return false;
                    }
                } 

複数枚印刷のコードに印刷領域にあわせて印刷対象のノードをスケーリングして印刷するだけのコードを使いするだけです。

どちらの印刷方法にするかはチェックボックスを使って分岐させています。

印刷が終わったら元の大きさにもどしています。もちろん成功、失敗、キャンセルでも。

この自動スケーリングで印刷した結果が下の画像になります。

31

これでクリスマスカード、年賀状もシンプルなものならJavaFXで印刷できるようになった。

完全に自己満足の世界にひたっている。

いちおう動いてるのとキャンセルしたときのGifアニメを貼っておきます。

postcarf1

下のGifアニメではJobStatusがNOT_STARTEDのときにキャンセルボタンによるキャンセルをかけています。

あともう一つ、キャンセルボタンではタイミング合わせるのが難しかったのでJobStatusがPRINTINGに変更されたら強制的キャンセルをするようにしてあります。

JobStatusがPRINTINGの時にキャンセルかけるとCANCELED -> ERROR となるんですね。

なんでCANCELEDで終了しないのだろう?これは印刷リクエストがされたあとだからERRORで終了という解釈でいいんだろうか?

postcard2

最後にこのプログラムのコードを載せておきます。

 


module NewYearCard {
    requires javafx.controls;
    requires java.desktop;
    requires javafx.fxml;
    requires java.logging;
    exports jp.yucchi.newyearcard;
    opens jp.yucchi.newyearcard to javafx.fxml;

}

 


<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.Group?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.CheckBox?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextArea?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.control.ToggleButton?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.text.Font?>
<?import javafx.scene.text.Text?>

<AnchorPane id="AnchorPane" fx:id="anchorPane" minHeight="730.0" minWidth="1000.0" prefHeight="730.0" prefWidth="1000.0" style="-fx-background-color: white;" xmlns="http://javafx.com/javafx/11.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="jp.yucchi.newyearcard.FXMLDocumentController">
   <children>
      <Label id="GashiLabel" fx:id="gashiLabel" alignment="CENTER_RIGHT" layoutX="195.0" layoutY="730.0" prefHeight="31.0" prefWidth="36.0" text="賀詞: " AnchorPane.bottomAnchor="11.0" AnchorPane.leftAnchor="200.0">
         <font>
            <Font size="14.0" />
         </font>
      </Label>
      <TextField id="GashiTextField" fx:id="gashiTextField" alignment="CENTER_RIGHT" focusTraversable="false" layoutX="236.0" layoutY="730.0" prefHeight="31.0" prefWidth="270.0" text="Merry Christmas!" AnchorPane.bottomAnchor="11.0" AnchorPane.leftAnchor="236.0" AnchorPane.rightAnchor="493.6">
         <font>
            <Font size="14.0" />
         </font>
      </TextField>
      <Label id="StatusLabel" fx:id="statusLabel" layoutX="14.0" layoutY="731.0" prefHeight="31.0" prefWidth="46.0" text="Status:" AnchorPane.bottomAnchor="11.0" AnchorPane.leftAnchor="10.0">
         <font>
            <Font size="14.0" />
         </font>
      </Label>
      <Label id="JobStatusLabel" fx:id="jobStatusLabel" layoutX="59.0" layoutY="725.0" prefHeight="31.0" prefWidth="135.0" AnchorPane.bottomAnchor="11.0" AnchorPane.leftAnchor="60.0">
         <font>
            <Font size="14.0" />
         </font>
      </Label>
      <Button id="PtintButton" fx:id="printButton" layoutX="879.0" layoutY="731.0" mnemonicParsing="false" onAction="#handlePrintButtonAction" prefHeight="31.0" prefWidth="87.0" style="-fx-background-color: ghostwhite;" text="Print" AnchorPane.bottomAnchor="11.0" AnchorPane.rightAnchor="10.0">
         <font>
            <Font size="14.0" />
         </font>
      </Button>
      <Button id="CancelButton" fx:id="cancelButton" disable="true" layoutX="824.0" layoutY="731.0" mnemonicParsing="false" onAction="#handleCancelButtonAction" prefHeight="31.0" prefWidth="87.0" style="-fx-background-color: ghostwhite;" text="Cancel" AnchorPane.bottomAnchor="11.0" AnchorPane.rightAnchor="107.0" />
      <CheckBox id="ScalingCheckBox" fx:id="scalingCheckBox" layoutX="511.0" layoutY="737.0" mnemonicParsing="false" prefHeight="20.0" prefWidth="70.0" selected="true" style="-fx-background-color: ghostwhite;" text="Scaling" AnchorPane.bottomAnchor="16.0" AnchorPane.rightAnchor="414.6">
         <font>
            <Font size="14.0" />
         </font>
      </CheckBox>
      <Button id="OpenImageButton" fx:id="openImageButton" layoutX="594.0" layoutY="730.0" mnemonicParsing="false" onAction="#handleOpenImageButtonAction" style="-fx-background-color: ghostwhite;" text="Open Image" AnchorPane.bottomAnchor="11.0" AnchorPane.rightAnchor="309.2">
         <font>
            <Font size="14.0" />
         </font>
      </Button>
      <ToggleButton id="PrinterInfoToggleButton" fx:id="printerInfoToggleButton" layoutX="703.0" layoutY="730.0" minHeight="31.0" minWidth="90.0" mnemonicParsing="false" onAction="#handlePrinterInfoToggleButtonAction" prefHeight="31.0" prefWidth="90.0" style="-fx-background-color: ghostwhite;" text="Printer Info" AnchorPane.bottomAnchor="11.0" AnchorPane.rightAnchor="207.4">
         <font>
            <Font size="14.0" />
         </font>
      </ToggleButton>
      <Group id="Greeting" fx:id="greeting">
         <children>
            <StackPane id="StackPane" fx:id="stackPane" alignment="TOP_LEFT" prefHeight="675.0" prefWidth="1000.0">
               <children>
                  <ImageView id="ImageView" fx:id="imageView" pickOnBounds="true" preserveRatio="true" />
                  <Text id="GashiText" fx:id="gashiText" strokeType="OUTSIDE" strokeWidth="0.0" textAlignment="RIGHT" translateX="210.0" translateY="70.0" wrappingWidth="720.0">
                     <font>
                        <Font size="50.0" />
                     </font>
                  </Text>
                  <TextArea id="PrinterTextArea" fx:id="printerTextArea" prefHeight="675.0" prefWidth="1000.0" visible="false">
                     <font>
                        <Font size="14.0" />
                     </font>
                  </TextArea>
               </children>
            </StackPane>
         </children>
      </Group>
   </children>
</AnchorPane>

 


package jp.yucchi.newyearcard;

import javafx.print.Printer;
import javafx.print.PrinterAttributes;

/**
 *
 * @author Yucchi
 */
class PrinterInfomation {

    private static final String CR = System.lineSeparator();

    String showPrinterDetails() {

        var printerDetails = new StringBuilder();

        // デフォルトプリンター
        var defaultPrinter = Printer.getDefaultPrinter().getName();

        printerDetails.append(CR).append("DefaultPrinter: ").append(defaultPrinter).append(CR);

        printerDetails.append(CR).append("[AllPrinters]").append(CR);

        Printer.getAllPrinters()
                .stream()
                .forEach(e -> {
                    printerDetails.append(e.getName()).append(CR);
                });

        printerDetails.append(CR).append("[PrinterAttributes]").append(CR);

        Printer.getAllPrinters()
                .stream()
                .forEach(e -> {
                    printerDetails.append(e.getName()).append(CR);

                    PrinterAttributes attributes = e.getPrinterAttributes();

                    printerDetails.append("サポートされている最大部数").append(CR).append(attributes.getMaxCopies()).append(CR);

                    printerDetails.append("サポートされる丁合い設定").append(CR);
                    attributes.getSupportedCollations()
                            .forEach(a -> {
                                printerDetails.append(a).append(CR);
                            });

                    printerDetails.append("サポートされる向き").append(CR);
                    attributes.getSupportedPageOrientations()
                            .forEach(a -> {
                                printerDetails.append(a).append(CR);
                            });

                    printerDetails.append("サポートされる用紙サイズ").append(CR);
                    attributes.getSupportedPapers()
                            .forEach(a -> {
                                printerDetails.append(a).append(CR);
                            });

                    printerDetails.append("サポートされる給紙方法(給紙ビンや給紙トレイ)").append(CR);
                    attributes.getSupportedPaperSources()
                            .forEach(a -> {
                                printerDetails.append(a).append(CR);
                            });

                    printerDetails.append("サポートされる色設定").append(CR);
                    attributes.getSupportedPrintColors()
                            .forEach(a -> {
                                printerDetails.append(a).append(CR);
                            });

                    printerDetails.append("サポートされる品質設定").append(CR);
                    attributes.getSupportedPrintQuality()
                            .forEach(a -> {
                                printerDetails.append(a).append(CR);
                            });

                    printerDetails.append("サポートされる印刷解像度").append(CR);
                    attributes.getSupportedPrintResolutions()
                            .forEach(a -> {
                                printerDetails.append(a).append(CR);
                            });

                    printerDetails.append("サポートされる両面設定").append(CR);
                    attributes.getSupportedPrintSides()
                            .forEach(a -> {
                                printerDetails.append(a).append(CR);
                            });

                    printerDetails.append("ページ範囲がサポートされるかどうか").append(CR).append(attributes.supportsPageRanges()).append(CR).append(CR);
                });

        return printerDetails.toString();

    }

}

 


package jp.yucchi.newyearcard;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

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

    public static void main(String[] args) {
        launch(args);
    }
    
    @Override
    public void start(Stage stage) throws Exception {

        FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("FXMLDocument.fxml"));
        Parent root = fxmlLoader.load();
        FXMLDocumentController controller = fxmlLoader.getController();

        Scene scene = new Scene(root);
        stage.setScene(scene);

        stage.setOnCloseRequest(e -> {
            controller.stageClose();
            stage.close();
        });

        stage.setTitle("NewYearCard");
        stage.resizableProperty().setValue(Boolean.FALSE);
        stage.show();
        
    }
    
}

 


package jp.yucchi.newyearcard;

import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.ResourceBundle;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.concurrent.Task;
import static javafx.concurrent.Worker.State.RUNNING;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.geometry.Point2D;
import javafx.print.Collation;
import javafx.print.PageLayout;
import javafx.print.PageOrientation;
import javafx.print.Paper;
import javafx.print.PaperSource;
import javafx.print.PrintColor;
import javafx.print.PrintQuality;
import javafx.print.Printer;
import javafx.print.PrinterJob;
import javafx.scene.Cursor;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.scene.control.ToggleButton;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Text;
import javafx.scene.transform.Scale;
import javafx.scene.transform.Transform;
import javafx.scene.transform.Translate;
import javafx.stage.FileChooser;

/**
 *
 * @author Yucchi
 */
public class FXMLDocumentController implements Initializable {

    private static final boolean DEBUG = true;

    private static final boolean CANCEL_DEBUG = true;

    private final ExecutorService executorService = Executors.newSingleThreadExecutor();

    private Rectangle printAreaRectangle;

    @FXML
    private ResourceBundle resources;

    @FXML
    private URL location;

    @FXML
    private AnchorPane anchorPane;

    @FXML
    private Label gashiLabel;

    @FXML
    private TextField gashiTextField;

    @FXML
    private Label statusLabel;

    @FXML
    private Label jobStatusLabel;

    @FXML
    private Button printButton;

    @FXML
    private Button cancelButton;

    @FXML
    private CheckBox scalingCheckBox;

    @FXML
    private Button openImageButton;

    @FXML
    private ToggleButton printerInfoToggleButton;

    @FXML
    private Group greeting;

    @FXML
    private StackPane stackPane;

    @FXML
    private ImageView imageView;

    @FXML
    private Text gashiText;

    @FXML
    private TextArea printerTextArea;

    private Task<Boolean> printTask;

    private Printer targetPrinter;

    private final BooleanProperty scaling = new SimpleBooleanProperty(true);

    @FXML
    void initialize() {
        assert anchorPane != null : "fx:id=\"anchorPane\" was not injected: check your FXML file 'FXMLDocument.fxml'.";
        assert gashiLabel != null : "fx:id=\"gashiLabel\" was not injected: check your FXML file 'FXMLDocument.fxml'.";
        assert gashiTextField != null : "fx:id=\"gashiTextField\" was not injected: check your FXML file 'FXMLDocument.fxml'.";
        assert statusLabel != null : "fx:id=\"statusLabel\" was not injected: check your FXML file 'FXMLDocument.fxml'.";
        assert jobStatusLabel != null : "fx:id=\"jobStatusLabel\" was not injected: check your FXML file 'FXMLDocument.fxml'.";
        assert printButton != null : "fx:id=\"printButton\" was not injected: check your FXML file 'FXMLDocument.fxml'.";
        assert cancelButton != null : "fx:id=\"cancelButton\" was not injected: check your FXML file 'FXMLDocument.fxml'.";
        assert scalingCheckBox != null : "fx:id=\"scalingCheckBox\" was not injected: check your FXML file 'FXMLDocument.fxml'.";
        assert openImageButton != null : "fx:id=\"openImageButton\" was not injected: check your FXML file 'FXMLDocument.fxml'.";
        assert printerInfoToggleButton != null : "fx:id=\"printerInfoToggleButton\" was not injected: check your FXML file 'FXMLDocument.fxml'.";
        assert greeting != null : "fx:id=\"greeting\" was not injected: check your FXML file 'FXMLDocument.fxml'.";
        assert stackPane != null : "fx:id=\"stackPane\" was not injected: check your FXML file 'FXMLDocument.fxml'.";
        assert imageView != null : "fx:id=\"imageView\" was not injected: check your FXML file 'FXMLDocument.fxml'.";
        assert gashiText != null : "fx:id=\"gashiText\" was not injected: check your FXML file 'FXMLDocument.fxml'.";
        assert printerTextArea != null : "fx:id=\"printerTextArea\" was not injected: check your FXML file 'FXMLDocument.fxml'.";
    }

    @FXML
    private void handleOpenImageButtonAction(ActionEvent event) {
        var fileChooser = new FileChooser();
        fileChooser.setTitle("Change Image");
        fileChooser.setInitialDirectory(new File(System.getProperty("user.home")));
        fileChooser.getExtensionFilters().addAll(new FileChooser.ExtensionFilter(
                "All Images", "*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif"));
        var image = fileChooser.showOpenDialog(anchorPane.getScene().getWindow());
        if (image != null) {
            try {
                imageView.setImage(new Image(image.toURI().toURL().toString(),
                        1_000, 675, true, true, false));
            } catch (MalformedURLException ex) {
                Logger.getLogger(FXMLDocumentController.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
    }

    @FXML
    private void handlePrinterInfoToggleButtonAction(ActionEvent event) {
        if (printerInfoToggleButton.isSelected()) {
            var printerInformation = new PrinterInfomation();
            var printerDetails = printerInformation.showPrinterDetails();
            printerTextArea.setText(printerDetails);
            printerTextArea.setVisible(true);
            printerInfoToggleButton.setText("Hide Info");
        } else {
            printerTextArea.clear();
            printerTextArea.setVisible(false);
            printerInfoToggleButton.setText("Printer Info");
        }
    }

    @FXML
    private void handlePrintButtonAction(ActionEvent event) {
        printGreeting(greeting);
    }

    @FXML
    private void handleCancelButtonAction(ActionEvent event) {
        if (printTask != null) {
            printTask.cancel();
        }
    }

    @Override
    public void initialize(URL url, ResourceBundle rb) {

        scaling.bind(scalingCheckBox.selectedProperty());

        // デフォルトプリンター
        var defaultPrinter = Printer.getDefaultPrinter();
        // プリンター選択 (デフォルトでないプリンターを選択しています)
        targetPrinter = Printer.getAllPrinters()
                .stream()
                .filter(p -> p.getName().matches("EP-806A"))
                .findFirst()
                .orElseGet(() -> {
                    return defaultPrinter;
                });

        // イメージファイルを設定
        imageView.setImage(new Image(this.getClass()
                .getResource("resources/ChristmasCard.jpg")
                .toExternalForm(), 1_000, 675, true, true, false));

        // 賀詞テキストのバインドとテキスト設定
        gashiText.textProperty().bind(gashiTextField.textProperty());
        gashiText.setFill(Color.LIGHTPINK);
        gashiText.setStrokeWidth(2);
        gashiText.setStroke(Color.RED);

        // 賀詞テキストをドラッグ可能とする
        var anchor = new SimpleObjectProperty<>(new Point2D(gashiText.getTranslateX(), gashiText.getTranslateY()));

        gashiText.setOnMousePressed(me -> {
            gashiText.setCursor(Cursor.MOVE);
            double x = me.getSceneX() - anchor.get().getX();
            double y = me.getSceneY() - anchor.get().getY();
            anchor.set(new Point2D(x, y));
        });

        gashiText.setOnMouseDragged(me -> {
            double x = me.getSceneX() - anchor.get().getX();
            double y = me.getSceneY() - anchor.get().getY();
            gashiText.setTranslateX(x);
            gashiText.setTranslateY(y);
        });

        gashiText.setOnMouseReleased(me -> {
            gashiText.setCursor(Cursor.HAND);
            double x = me.getSceneX() - anchor.get().getX();
            double y = me.getSceneY() - anchor.get().getY();
            anchor.set(new Point2D(x, y));
        });

        gashiText.setOnMouseEntered(me -> {
            gashiText.setCursor(Cursor.HAND);
        });

    }

    private void printGreeting(Group greeting) {

        printTask = new Task<>() {

            private PageLayout pageLayout;

            @Override
            public Boolean call() {

                Platform.runLater(() -> {
                    jobStatusLabel.setText("Create a printer job.");
                });

                // ジョブステータスラベル確認用スリープ
                if (DEBUG) {
                    jobSleep();
                }

                // プリンタージョブを作成
//                // デフォルトプリンターを使ってプリンタージョブを作成
//                var job = PrinterJob.createPrinterJob();
                // プリンターを指定してプリンタージョブを作成
                var job = PrinterJob.createPrinterJob(targetPrinter);

                // ジョブステータス確認用
                System.out.println("JobStatus: " + job.jobStatusProperty().asString().get());
                Platform.runLater(() -> {
                    jobStatusLabel.setText(job.jobStatusProperty().asString().get());
                });

                // ジョブステータスラベル確認用スリープ
                if (DEBUG) {
                    jobSleep();
                }

                // プリンタージョブステータス
                job.jobStatusProperty().addListener(e -> {
                    System.out.println("JobStatus: " + job.jobStatusProperty().asString().get());
                    Platform.runLater(() -> {
                        jobStatusLabel.setText(job.jobStatusProperty().asString().get());
                    });

                    // ジョブステータスラベル確認用スリープ
                    if (DEBUG) {
                        jobSleep();
                    }

                    // JobStatus が PRINTING の状態で強制的にキャンセル
                    if (CANCEL_DEBUG && PrinterJob.JobStatus.PRINTING == job.jobStatusProperty().get()) {
                        System.out.println("JobStatus が PRINTING の状態で Cancel");
                        job.cancelJob(); // JobStatus: CANCELED -> JobStatus: ERROR となる。
                        printTask.cancel();
                    }
                });

                // ジョブ設定
                var jobSettings = job.getJobSettings();
                // ジョブの名前
                jobSettings.setJobName("Greeting");
                // 自動ではがきに印刷
                jobSettings.setPaperSource(PaperSource.AUTOMATIC);
//                // この設定でもはがきに印刷
//                jobSettings.setPaperSource(PaperSource.TOP);
                // ページレイアウト設定
                pageLayout = targetPrinter.createPageLayout(Paper.JAPANESE_POSTCARD, 
                        PageOrientation.LANDSCAPE, Printer.MarginType.HARDWARE_MINIMUM);
                // ジョブにページレイアウトを設定
                jobSettings.setPageLayout(pageLayout);
                // 印刷カラーを設定
                jobSettings.setPrintColor(PrintColor.COLOR);
                // 印刷品質を設定
                jobSettings.setPrintQuality(PrintQuality.NORMAL);
                // 印刷部数を設定
                jobSettings.setCopies(1);
                // 部単位で印刷設定
                jobSettings.setCollation(Collation.UNCOLLATED);

                // Single page printing /////////////////////////////////////////////
                if (scaling.get()) {
                    if (job != null) {
                        // 印刷対象ノードを印刷範囲内にスケーリング
                        fit2PrintableSize();

//                        // 用紙設定ダイアログ
//                        var nativePageSetup = job.showPageSetupDialog(anchorPane.getScene().getWindow());
//                        // 用紙設定ダイアログキャンセル
//                        if (!nativePageSetup) {
//                            System.out.println("PageSetup has been canceled.");
//                            job.cancelJob();
//                            printTask.cancel();
//                        }

//                        // 印刷ダイアログ
//                        var nativeJob = job.showPrintDialog(anchorPane.getScene().getWindow());
//                        // 印刷ダイアログでキャンセルした場合の処理
//                        if (!nativeJob) {
//                            System.out.println("Job has been canceled.");
//                            job.cancelJob();
//                            printTask.cancel();
//                        }

                        // タスクのキャンセル処理
                        if (isCancelled()) {
                            if (PrinterJob.JobStatus.PRINTING == job.jobStatusProperty().get() 
                                    || PrinterJob.JobStatus.NOT_STARTED == job.jobStatusProperty().get()) {
                                // プリンタージョブをキャンセル
                                job.cancelJob();
                                System.out.println("Cancel printing.");
                                if (scaling.get()) {
                                    // 印刷対象ノードを元の大きさにスケーリング
                                    fit2ImageSize();
                                }
                                return false;
                            } else {
                                System.out.println("Job has already been canceled.");
                            }
                            return false;
                        }

                        // ノード(Group greeting)をプリント
                        boolean success = job.printPage(greeting);
                        if (success) {
                            // 印刷対象ノードを元の大きさにスケーリング
                            fit2ImageSize();
                            // プリンタのキューに正常にスプールされたら true 
                            return job.endJob();
                        } else {
                            System.out.println("Printing failed.");
                            // 印刷対象ノードを元の大きさにスケーリング
                            fit2ImageSize();
                            return false;
                        }
                    } else {
                        Platform.runLater(() -> {
                            jobStatusLabel.setText("Could not create printer job.");
                        });
                        return false;
                    }
                } else {
                    // Multi-page printing /////////////////////////////////////////

                    // 印刷用紙に印刷可能な幅と高さ
                    double printableWidth = pageLayout.getPrintableWidth();
                    double printableHeight = pageLayout.getPrintableHeight();

                    // 印刷エリアの設定(印刷対象ノードをそのままの大きさで設定)
                    printAreaRectangle = new Rectangle(greeting.getBoundsInParent().getWidth(), 
                            greeting.getBoundsInParent().getHeight(), null);
                    // 印刷エリアの位置と大きさ
                    double printRectX = printAreaRectangle.getX();
                    double printRectY = printAreaRectangle.getY();
                    double printRectWidth = printAreaRectangle.getWidth();
                    double printRectHeight = printAreaRectangle.getHeight();

                    // 印刷に必要な用紙枚数(行、列)
                    int rows = (int) Math.ceil(printRectHeight / printableHeight);
                    int columns = (int) Math.ceil(printRectWidth / printableWidth);

                    // ノードのクリップを保存
                    var oldClip = greeting.getClip();
                    var oldTransforms = new ArrayList<>(greeting.getTransforms());
                    // printAreaRectangle のエリアをクリップとして設定する
                    greeting.setClip(new javafx.scene.shape.Rectangle(printRectX, printRectY,
                            printRectWidth, printRectHeight));
                    // 0,0 に移動
                    greeting.getTransforms().add(new Translate(-printRectX, -printRectY));
                    // 印刷ページに適合するようにノードを移動する変換
                    var gridTransform = new Translate();
                    greeting.getTransforms().add(gridTransform);

                    if (job != null) {

//                        // 用紙設定ダイアログ
//                        var nativePageSetup = job.showPageSetupDialog(anchorPane.getScene().getWindow());
//                        // 用紙設定ダイアログキャンセル
//                        if (!nativePageSetup) {
//                            System.out.println("PageSetup has been canceled.");
//                            job.cancelJob();
//                            printTask.cancel();
//                        }

//                        // 印刷ダイアログ
//                        var nativeJob = job.showPrintDialog(anchorPane.getScene().getWindow());
//                        // 印刷ダイアログでキャンセルした場合の処理
//                        if (!nativeJob) {
//                            System.out.println("Job has been canceled.");
//                            job.cancelJob();
//                            printTask.cancel();
//                        }

                        // タスクのキャンセル処理
                        if (isCancelled()) {
                            if (PrinterJob.JobStatus.PRINTING == job.jobStatusProperty().get() 
                                    || PrinterJob.JobStatus.NOT_STARTED == job.jobStatusProperty().get()) {
                                // プリンタージョブをキャンセル
                                job.cancelJob();
                                System.out.println("Cancel printing.");
                                return false;
                            } else {
                                System.out.println("Job has already been canceled.");
                            }
                            return false;
                        }

                        boolean success = true;
                        // 印刷ページごとに、ノードを移動
                        for (int row = 0; row < rows; row++) {
                            for (int col = 0; col < columns; col++) {
                                gridTransform.setX(-col * printableWidth);
                                gridTransform.setY(-row * printableHeight);
                                if (DEBUG) {
                                    jobSleep();
                                }
                                success &= job.printPage(pageLayout, greeting);
                            }
                        }
                        if (success) {
                            // ノードを元に戻す
                            restoreNode(greeting, oldTransforms, oldClip);
                            return job.endJob();

                        } else {
                            // ノードを元に戻す
                            restoreNode(greeting, oldTransforms, oldClip);
                            System.out.println("Printing failed.");
                            return false;
                        }
                    } else {
                        Platform.runLater(() -> {
                            jobStatusLabel.setText("Could not create printer job.");
                        });
                        // ノードを元に戻す
                        restoreNode(greeting, oldTransforms, oldClip);
                        return false;
                    }
                }
            }

            private void jobSleep() {
                try {
                    Thread.sleep(1_500);
                } catch (InterruptedException ex) {
                    // ナイスキャッチ!
                    if (isCancelled()) {
                        System.out.println("Canceled by InterruptedException: sleep interrupted");
                    } else {
                        Logger.getLogger(FXMLDocumentController.class.getName()).log(Level.SEVERE, null, ex);
                    }
                }
            }

            // 印刷対象ノードを印刷範囲内にスケーリング
            private void fit2PrintableSize() {
                greeting.getTransforms()
                        .add(new Scale(pageLayout.getPrintableWidth() / imageView.getBoundsInParent().getWidth(),
                                pageLayout.getPrintableHeight() / imageView.getBoundsInParent().getHeight()));
            }

            // 印刷対象ノードを元の大きさにスケーリング
            private void fit2ImageSize() {
                greeting.getTransforms()
                        .add(new Scale(imageView.getBoundsInParent().getWidth() / pageLayout.getPrintableWidth(),
                                imageView.getBoundsInParent().getHeight() / pageLayout.getPrintableHeight()));
            }

            // ノードを元に戻す
            private void restoreNode(Group greeting, List<Transform> oldTransforms, Node oldClip) {
                greeting.getTransforms().clear();
                greeting.getTransforms().addAll(oldTransforms);
                greeting.setClip(oldClip);
            }

        };

        executorService.submit(printTask);

        // WorkerStateEvent を使ってWorkerオブジェクトの状態を監視
        printTask.setOnScheduled(wse -> {
            System.out.println("WorkerState: Scheduled");
        });
        printTask.setOnRunning(wse -> {
            System.out.println("WorkerState: OnRunning");
        });
        printTask.setOnSucceeded(wse -> {
            System.out.println("WorkerState: Succeeded");
        });
        printTask.setOnCancelled(wse -> {
            System.out.println("WorkerState: Cancelled");
        });
        printTask.setOnFailed(wse -> {
            System.out.println("WorkerState: Failed");
        });

        // disableProperty と Workerオブジェクトの stateProperty をバインド
        gashiTextField.disableProperty().bind(printTask.stateProperty().isEqualTo(RUNNING));
        printButton.disableProperty().bind(printTask.stateProperty().isEqualTo(RUNNING));
        cancelButton.disableProperty().bind(printTask.stateProperty().isNotEqualTo(RUNNING));
        scalingCheckBox.disableProperty().bind(printTask.stateProperty().isEqualTo(RUNNING));
        openImageButton.disableProperty().bind(printTask.stateProperty().isEqualTo(RUNNING));
        printerInfoToggleButton.disableProperty().bind(printTask.stateProperty().isEqualTo(RUNNING));

    }

    void stageClose() {
        executorService.shutdownNow();
    }

}

Hatena タグ:

Jigsaw のお勉強 その4 JavaFX (Spinner がほんの少しだけ良くなった)

このエントリーは じゃばえふえっくす Advent Calendar 2018 の初日(12月4日)です。

未だにJigsawのお勉強をしています。(^_^;)

今回はJavaFXを使ってみます。

そんなJavaFXですが正式リリース前からさらにリリースされてからもいろいろありました。

現在はJavaにバンドルされていません。

正確にはいろいろなところからJDKが提供されているのでひょっとしたらJavaFXをバンドルしたJavaがあるのかもしれません。

今回はOpenJDKとOpenJFXを使います。

JavaFX 11でjavafx.scene.control.Spinner<T>クラスにほんの少し優しい変更が追加されたのでそれを試してみました。

矢印ボタンをマウスにて押し続けたときの入力時間がJavaFX 8の750ms固定だったのが自由に変更できるようになりました。

InitialDelayとRepeatDelayが設定可能となります。

デフォルトの設定時間はそれぞれ300ms、60ms となっています。

まず、Spinnerについて少しだけ復習しておきましょう。

SpinnerのAPIドキュメントに下記のように書かれています。

順序付けられたシーケンスからユーザーが数値またはオブジェクト値を選択できるようにする単一行のテキスト・フィールド。

通常、スピナーはシーケンスの要素間を移動するための小さな矢印ボタンのペアを提供します。

キーボードの上/下矢印キーでも要素間を自由に移動できます。

ユーザーがスピナーに直接(有効な)値を入力することもできます。

コンボ・ボックスも同様の機能を提供しますが、スピナーの方が好まれる場合があるのは、重要なデータを不明瞭化する可能性があるドロップ・ダウン・リストが不要であり、また、他の多くのJavaFX UIコントロールのようにObservableListデータ・モデルを使用せずに、wrappingなどの機能や、より単純な’無限’データ・モデルの仕様(SpinnerValueFactory)を使用できるためです。

Spinnerのシーケンス値はSpinnerValueFactoryで定義します。

value factoryはコンストラクタ引数として指定し、value factory propertyを使用して変更できます。

JavaFXには、次に示す一般的なタイプのSpinnerValueFactoryクラスが用意されています。

•SpinnerValueFactory.IntegerSpinnerValueFactory

•SpinnerValueFactory.DoubleSpinnerValueFactory

•SpinnerValueFactory.ListSpinnerValueFactory

Spinnerには、Spinnerの現在のvalueの表示および変更を行う、editorと呼ばれるTextField子コンポーネントがあります。

Spinnerはデフォルトで編集不可能ですが、editable propertyをtrueに設定すると、入力を受け入れることができます。

Spinnerエディタは、値ファクトリのvalue propertyに対する変更をリスニングすることにより、値ファクトリとの同期を保ちます。 ユーザーがeditorに表示された値を変更した場合、Spinnerのvalueとeditorの値が異なってしまう可能性があります。

モデルの値をeditorの値と同じにするには、ユーザーが[Enter]キーを使用して編集をコミットする必要があります。

APIドキュメントによると一般的なタイプのSpinnerValueFactoryクラスと使ってSpinnerインスタンスを作成できると書かれていました。

Spinner​(double min, double max, double initialValue) // SpinnerValueFactory.DoubleSpinnerValueFactory

Spinner​(double min, double max, double initialValue, double amountToStepBy) // SpinnerValueFactory.DoubleSpinnerValueFactory

Spinner​(int min, int max, int initialValue) // SpinnerValueFactory.IntegerSpinnerValueFactory

Spinner​(int min, int max, int initialValue, int amountToStepBy) // SpinnerValueFactory.IntegerSpinnerValueFactory

Spinner​(ObservableList<T> items) // SpinnerValueFactory.ListSpinnerValueFactory

Spinner​(SpinnerValueFactory<T> valueFactory) // 指定されたvalue factoryを設定する場合

Spinnerインスタンスを作成するのは容易にできることが確認できたので次はSpinnerの定数フィールド値を使ってスタイルの設定方法を調べてみます。

public static final String STYLE_CLASS_ARROWS_ON_LEFT_HORIZONTAL

水平方向(左向きと右向き)の矢印がSpinnerの左側に配置されます。

public static final String STYLE_CLASS_ARROWS_ON_LEFT_VERTICAL

垂直方向(上向きと下向き)の矢印がSpinnerの左側に配置されます。

public static final String STYLE_CLASS_ARROWS_ON_RIGHT_HORIZONTAL

水平方向(左向きと右向き)の矢印がSpinnerの右側に配置されます。

public static final String STYLE_CLASS_SPLIT_ARROWS_HORIZONTAL

Spinnerの左側に減分矢印、右側に増分矢印が配置されます。

public static final String STYLE_CLASS_SPLIT_ARROWS_VERTICAL

スピナーの幅全体にわたって上下に伸びた矢印が配置されます。

何も指定をしない場合はデフォルトの垂直方向(上向きと下向き)の矢印がSpinnerの右側に配置されます。

Spinnerインスタンスの作成、スタイルの設定方法が解ったので試してみます。

Spinnerのデザインをデフォルトの垂直方向(上向きと下向き)の矢印がSpinnerの右側に配置

1

Spinnerのデザインを水平方向(左向きと右向き)の矢印がSpinnerの左側に配置
spinner.getStyleClass().add(Spinner.STYLE_CLASS_ARROWS_ON_LEFT_HORIZONTAL);

2

Spinner のデザインを垂直方向(上向きと下向き)の矢印がSpinnerの左側に配置
spinner.getStyleClass().add(Spinner.STYLE_CLASS_ARROWS_ON_LEFT_VERTICAL);

3

Spinnerのデザインを水平方向(左向きと右向き)の矢印がSpinnerの右側に配置
spinner.getStyleClass().add(Spinner.STYLE_CLASS_ARROWS_ON_RIGHT_HORIZONTAL);

4

Spinnerのデザインを左側に減分矢印、右側に増分矢印が配置
spinner.getStyleClass().add(Spinner.STYLE_CLASS_SPLIT_ARROWS_HORIZONTAL);

5

Spinnerのデザインをスピナーの幅全体にわたって上下に伸びた矢印が配置
spinner.getStyleClass().add(Spinner.STYLE_CLASS_SPLIT_ARROWS_VERTICAL);

6

// 最小値 0、最大値 100、初期値 50、増分または減分するステップの量のSpinnerインスタンス作成
Spinner<Integer> spinner = new Spinner<>(0, 100, 50, 10);

javafx.​scene.​Nodeパッケージのpublic final ObservableList<String> getStyleClass()でノードを論理的にグループ化するために使用できる文字列識別子のリストを取得しava.​util.​Listパッケージのpublic abstract boolean add(E arg0)メソッドでSpinnerの定数フィールド値を追加して設定します。

Spinnerの増分、減分の矢印ボタンの配置デザインの設定はこれで可能となりました。

次に Spinnerに表示されている値のアライメントとカスタムフォントを設定してみます。

//  カスタムフォント、アライメントを設定
Font f = Font.loadFont(this.getClass().getResourceAsStream(“resources/AKUBIN1.34.ttf”), 28);
spinner.getEditor().setStyle(“-fx-text-fill: black; -fx-font: 18px ‘Akubin’; -fx-alignment: CENTER_RIGHT;”);

SpinnerExample

上の行でカスタムフォントを読み込んでその下の行でSpinnerにそのフォントを設定、テキストのサイズ、文字色、アライメントを設定しています。

そしてついでに

// Spinneのエディタをユーザー入力可能とする
spinner.setEditable(true);

としてユーザー入力可能としてます。

増分、減分のステップ量は変わらずユーザーによって入力された値からの増分値、減分値となります。

また、最大値、最小値を超えることはありません。

ここまでのプログラムのソースコードは次のようになります。


package jp.yucchi.spinnerexample;

import javafx.application.Application;
import static javafx.application.Application.launch;
import javafx.scene.Scene;
import javafx.scene.control.Spinner;
import javafx.scene.layout.StackPane;
import javafx.scene.text.Font;
import javafx.stage.Stage;

/**
 *
 * @author yucchi
 */
public class SpinnerExample extends Application {
   @Override
    public void start(Stage primaryStage) {

        // 最小値 0、最大値 100、初期値 50、増分または減分するステップの量
        Spinner<Integer> spinner = new Spinner<>(0, 100, 50, 10);

//        // Spinner のデザインを水平方向(左向きと右向き)の矢印がSpinnerの左側に配置
//        spinner.getStyleClass().add(Spinner.STYLE_CLASS_ARROWS_ON_LEFT_HORIZONTAL);

//        // Spinner のデザインを垂直方向(上向きと下向き)の矢印がSpinnerの左側に配置
//        spinner.getStyleClass().add(Spinner.STYLE_CLASS_ARROWS_ON_LEFT_VERTICAL);

//        // Spinner のデザインを水平方向(左向きと右向き)の矢印がSpinnerの右側に配置
//        spinner.getStyleClass().add(Spinner.STYLE_CLASS_ARROWS_ON_RIGHT_HORIZONTAL);

//        // Spinner のデザインを左側に減分矢印、右側に増分矢印が配置
//        spinner.getStyleClass().add(Spinner.STYLE_CLASS_SPLIT_ARROWS_HORIZONTAL);

//        // Spinner のデザインをスピナーの幅全体にわたって上下に伸びた矢印が配置
//        spinner.getStyleClass().add(Spinner.STYLE_CLASS_SPLIT_ARROWS_VERTICAL);
        
        // Spinneのエディタをユーザー入力可能とする
        spinner.setEditable(true);
        //  カスタムフォント、アライメントを設定
        Font f = Font.loadFont(this.getClass().getResourceAsStream("resources/AKUBIN1.34.ttf"), 28);
        spinner.getEditor().setStyle("-fx-text-fill: black; -fx-font: 18px 'Akubin'; -fx-alignment: CENTER_RIGHT;");
        // チェンジリスナーをつけて値を確認する
        spinner.valueProperty().addListener((ov, oldValue, newValue) -> {
            System.out.println("oldValue: " + oldValue);
            System.out.println("newValue: " + newValue);
            System.out.println("");
        });

        StackPane root = new StackPane();
        root.getChildren().add(spinner);

        Scene scene = new Scene(root, 250, 100);

        primaryStage.setTitle("SpinnerExample");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

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

それではSpinnerにInitialDelayとRepeatDelayを設定するプログラムを組んでみます。

せっかくだからJDK 12 Early-Access Builds (build: 21)とJavaFX Early-Access Builds (openjfx-12-ea+2)を使ってみます。

さらに、モジュラjarを作ってモジュール化します。

そして、jlinkで配布用にカスタムJREをバンドルしたアプリケーションを作成します。

NetBeans IDE 10.0-vc4を使ってお気楽に組もうとしたがこのバージョンには未対応なので古典的なツールを使いました。

ちなみにNetBeans IDE 10.0 Devでプロジェクトをビルドし、jlinkでカスタムJRE組み込みアプリケーションの作成はできました。

ただ、エラーバッジの嵐のエディタは心が痛みます。

それではお気に入りのエディタか心を痛めながらNetBeans IDE 10.0-vc4を使って下記ファイルを作成します。


module IncludedJavaFX {
    requires javafx.controls;
    requires java.desktop;
    requires javafx.fxml;
    exports jp.yucchi.includedjavafx;
    opens jp.yucchi.includedjavafx to javafx.fxml;
}

 


<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.Label?>
<?import javafx.scene.control.Spinner?>
<?import javafx.scene.layout.AnchorPane?>

<AnchorPane id="AnchorPane" fx:id="anchorPane" prefHeight="70.0" prefWidth="700.0" style="-fx-background-color: white;" xmlns="http://javafx.com/javafx/10.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="jp.yucchi.includedjavafx.FXMLDocumentController">
   <children>
      <Spinner id="VersionSpinner" fx:id="versionSpinner" layoutX="97.0" layoutY="22.0" prefHeight="50.0" prefWidth="230.0" AnchorPane.bottomAnchor="10.0" AnchorPane.leftAnchor="10.0" AnchorPane.topAnchor="10.0" />
      <Label id="ResultLabel" fx:id="resultLabel" alignment="CENTER_RIGHT" layoutX="185.0" layoutY="27.0" prefHeight="50.0" prefWidth="383.0" text="" AnchorPane.bottomAnchor="10.0" AnchorPane.leftAnchor="240.0" AnchorPane.rightAnchor="10.0" AnchorPane.topAnchor="10.0" />
   </children>
</AnchorPane>

 


package jp.yucchi.includedjavafx;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

/**
 *
 * @author yucchi
 */
public class IncludedJavaFX extends Application {

    @Override
    public void start(Stage stage) throws Exception {
        Parent root = FXMLLoader.load(getClass().getResource("FXMLDocument.fxml"));
        Scene scene = new Scene(root);
        stage.setScene(scene);
        stage.setTitle("JavaFX is included?");
        stage.show();
    }

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

}

 


package jp.yucchi.includedjavafx;

import java.awt.Toolkit;
import java.net.URL;
import java.util.ResourceBundle;
import javafx.collections.FXCollections;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Label;
import javafx.scene.control.Spinner;
import javafx.scene.control.SpinnerValueFactory;
import javafx.scene.layout.AnchorPane;
import javafx.scene.text.Font;
import javafx.util.Duration;

/**
 *
 * @author yucchi
 */
public class FXMLDocumentController implements Initializable {

    @FXML
    private ResourceBundle resources;

    @FXML
    private URL location;

    @FXML
    private AnchorPane anchorPane;

    @FXML
    private Spinner<String> versionSpinner;

    @FXML
    private Label resultLabel;

    @FXML
    void initialize() {
        assert anchorPane != null : "fx:id=\"anchorPane\" was not injected: check your FXML file 'FXMLDocument.fxml'.";
        assert versionSpinner != null : "fx:id=\"versionSpinner\" was not injected: check your FXML file 'FXMLDocument.fxml'.";
        assert resultLabel != null : "fx:id=\"resultLabel\" was not injected: check your FXML file 'FXMLDocument.fxml'.";
    }

    private static final double INTIAL_DELAY_TIME = 1_500;
    private static final double REPEAT_DELAY_TIME = 500;

    @Override
    public void initialize(URL url, ResourceBundle rb) {
        // システム情報
        System.out.println(`
/` + System.getProperty("os.name") + " " + System.getProperty("os.version") + " " + System.getProperty("os.arch") + `\
|` + "JDK " + System.getProperty("java.version") + `                    | 
\` +  "JavaFX " + System.getProperty("javafx.version") + `                 /
 -----------------------------
          \
           \
            \          __---__
                    _-       /--______
               __--( /     \ )XXXXXXXXXXX\v.
             .-XXX(   O   O  )XXXXXXXXXXXXXXX-
            /XXX(       U     )        XXXXXXX\
          /XXXXX(              )--_  XXXXXXXXXXX\
         /XXXXX/ (      O     )   XXXXXX   \XXXXX\
         XXXXX/   /            XXXXXX   \__ \XXXXX
         XXXXXX__/          XXXXXX         \__---->
 ---___  XXX__/          XXXXXX      \__         /
   \-  --__/   ___/\  XXXXXX            /  ___--/=
    \-\    ___/    XXXXXX               --- XXXXXX
       \-\/XXX\ XXXXXX                      /XXXXX
         \XXXXXXXXX   \                    /XXXXX/
          \XXXXXX      >                 _/XXXXX/
            \XXXXX--__/              __-- XXXX/
             -XXXXXXXX---------------  XXXXXX-
                \XXXXXXXXXXXXXXXXXXXXXXXXXX/
                  VXXXXXXXXXXXXXXXXXXV`       
        );
        //  カスタムフォント、アライメントを設定
        Font f = Font.loadFont(this.getClass().getResourceAsStream("resources/AKUBIN1.34.ttf"), 28);
        resultLabel.setStyle("-fx-text-fill: black; -fx-font: 28px 'Akubin'; -fx-alignment: CENTER_RIGHT;");
        versionSpinner.getEditor().setStyle("-fx-text-fill: black; -fx-font: 28px 'Akubin'; -fx-alignment: CENTER_RIGHT;");
        // Spinner の値リスト
        var versionNumber = FXCollections
                .observableArrayList("JDK 1.0", "JDK 1.1", "J2SE 1.2", "J2SE 1.3", "J2SE 1.4",
                        "J2SE 5.0", "Java SE 6", "Java SE 7", "Java SE 8",
                        "Java SE 9", "Java SE 10", "Java SE 11", "Java SE 12", "Java SE 13");
        // Spinner の生成
        var valueFactory = new SpinnerValueFactory.ListSpinnerValueFactory<String>(versionNumber);
        versionSpinner.setValueFactory(valueFactory);
        // タイマーの時間を設定
        versionSpinner.setInitialDelay(Duration.millis(INTIAL_DELAY_TIME));
        versionSpinner.setRepeatDelay(Duration.millis(REPEAT_DELAY_TIME));
        // 値リスト変更に対応するためここで初期値を設定
        includeFX(versionSpinner.getValue());
        // Spinner にチェンジリスナーを使って値の変更を検出
        versionSpinner.valueProperty().addListener((ov, oldValue, newValue) -> {
            // 解りやすいようにビープ音を鳴らす
            Toolkit.getDefaultToolkit().beep();
            // 更新された値を引数として処理する
            includeFX(newValue);
        });

    }

    // Label に結果を表示させる
    private void includeFX(String versionName) {

//            // JDK12 JEP 325: Switch Expressions
//            // (まだ String Switch に未対応 (ノД`)シクシク) <- enum, int なんかは対応していた。
//            switch (versionName){
//                case "Java SE 7" -> resultLabel.setText("JavaFX is included from Update 2.");
//                case "Java SE 8", "Java SE 9", "Java SE 10" -> resultLabel.setText("JavaFX is included.");
//                case "Java SE 13" ->  resultLabel.setText("Not yet been released.");
//                default -> resultLabel.setText("JavaFX is not included.");
//            }

        switch (versionName) {
            case "Java SE 7":
                resultLabel.setText("JavaFX is included from Update 2.");
                break;
            case "Java SE 8":
            case "Java SE 9":
            case "Java SE 10":
                resultLabel.setText("JavaFX is included.");
                break;
            case "Java SE 13":
                resultLabel.setText("Not yet been released.");
                break;
            default:
                resultLabel.setText("JavaFX is not included.");
        }

    }

}

 

プロジェクトの構成は下図のようにします。

11

カスタムフォントを使うのでお気に入りのフォントをご用意ください。

コンパイル時にはJDK 12の新機能を使えるように–release 12 –enable-preview VMオプションをつけます。

Java SE 11からJavaFXはバンドルされなくなったのでそれらライブラリをダウンロードしてコンパイル、実行時にモジュールパスを指定します。

JavaFXはこちらから入手できます。

https://gluonhq.com/products/javafx/

JavaFX Linux SDKとJavaFX Linux jmodsがあります。

JavaFX Linux SDKだけでいいのですがjlinkで配布用にカスタムJREをバンドルしたアプリケーションを作成する場合はJavaFX Linux jmodsが必要となります。

-d オプションでbin/IncludeeJavaFXディレクトリにコンパイルによって作成されるクラスファイルが格納されるようにしています。

–module-pathオプションでJavaFX SDKのパスを指定します。

–add-modulesオプションでアプリケーションで必要とされるモジュールを指定します。

12

FXMLDocument.fxmlはコンパイルを実行しても出力ディレクトリbin/IncludeeJavaFXには移動、コピーされないので存在しません。

FXMLDocument.fxmlをコンパイルして作成されたクラスファイルのある場所にコピーします。

13

フォントを格納しているresourcesディレクトリもコピーします。

14

プロジェクトの構成は下図のようになります。

15

それでは実行してみましょう。

–enable-preview VMオプションを忘れずに。

–module-path、–add-modules、–moduleオプションも必要です。

16

ちゃんとコンパイルされて実行できました。

17

と、思いきや

18

何やら妙な警告がでました。(>_<。)

GTK libraries version 3を使うとだめそうなので-Djdk.gtk.verbose=true -Djdk.gtk.version=2とVM オプションをあたえます。

19

これってJavaFX 12のリリースまでには修正されるのかな?

20

それではこのプログラムをモジュラjar化してみます。

modsディレクトリを作成してそこにモジュラjarを格納します。

–module-versionでこのモジュールのバージョンを設定します。

実行時メインクラスの指定をしなくていいように–main-classオプションを使ってメインクラスを指定してアプリケーションエントリポイントを設定します。

21

モジュラjarがちゃんとできたか確認します。

22

問題無さそうなのでプログラムを走らせてみます。

–module-pathにはこのアプリケーションを格納しているmodsディレクトリとJavaFX SDKのパスを指定します。

–moduleオプションはメインクラスの指定を省力できるようにしたのでモジュール名だけで実行できます。

モジュラjar作成時にメインクラスの指定をしなかった場合は–module モジュール名/モジュールのメインクラス名とアプリケーションエントリポイント指定しなければいけません。

今回の場合だと、–module IncludedJavaFX/jp.yucchi.includedjavafx.IncludedJavaFXとします。

23

アプリケーションのモジュール化に成功し、ちゃんと動くことが確認できました。

24

最後にjlinkで配布用にカスタムJREをバンドルしたアプリケーションを作成します。

includedjavafx-imageディレクトリを作成しそこに成果物を格納します。

–module-pathオプションでJDKとJavaFXのモジュールを指定します。JavaFX Linux SDKのパスじゃなくてJavaFX Linux jmodsのパスを指定することに注意してください。

–add-modulesオプションでこのアプリケーション自体を指定します。

–launcherオプションを次のように設定します。

–launcher <コマンド名>=<モジュール名><メインクラス>

–outputオプションで出力先を設定します。

25

jlinkによって作成されたランチャーファイルにVMオプションを記述します。

26

27

これを忘れると少し焦ります。(^_^;)

28

動きましたね。

JavaがインストールされてないPCでも動くことを確認しました。(Linux)

ただしWindowsでは動きません。

Windowsで動く配布用アプリはWindowsで作成しなくてはいけないようです。

Write once, run anywhere ってのは遠い過去の夢になってしまったのでしょうか・・・

Spinner

肝心のことを忘れていました。

上のGifアニメで増分、減分ボタンを押しっぱなしにしている時のSpinnerの更新時間に注目してください。

// タイマーの時間を設定
versionSpinner.setInitialDelay(Duration.millis(INTIAL_DELAY_TIME));
versionSpinner.setRepeatDelay(Duration.millis(REPEAT_DELAY_TIME));

setInitialDelay​(Duration value) メソッドで押し続けて最初に更新される時間を設定しています。

このプログラムでは解りやすくするために1500ミリ秒に設定しました。

そのまま押し続けた時の更新時間(リピート時間)は

setRepeatDelay​(Duration value) メソッドで 500ミリ秒と設定しました。

Java SE 8 u40 では750ミリ秒に固定されていて変更することができなかったので少しうれしいアップデートです。

Swingも悪くはないけど近い将来にはJavaの標準 GUI はJavaFXになってほしい。

 

Jigsawにもほんの少しだけ慣れてきたけど難しいですね。

モジュールの依存関係を調べるのがお手軽にできるツールがあればいいのだけど・・・

jdepsは万能ではないので落とし穴に落ちます。(>_<。)

今回のプログラムではFXMLを利用しています。

JavaFXでGUIの構築にはとても便利です。

で、それを使うためにmodule-info.javaにrequires javafx.fxml;とrequiresモジュールディレクティブ設定します。

ところがそれだけではjava.lang.reflect.InvocationTargetExceptionが投げられます。

FXMLを利用したJavaFXアプリケーションではjavafx.fxmlモジュール側からこのプログラムのFXMLのコントローラークラスに対してリフレクションによるアクセスがあります。

モジュールのカプセル化はリフレクションにも適用されるからです。

JDK 9より前はリフレクションを使えばパッケージ内の全てのクラスやクラスの全てのメンバーにアクセスできました。

つまりリフレクションは開発者の本意と関係なくカプセル化を破壊することが可能です。

Jigsawでは強力で繊細なカプセル化が提供されています。

デフォルトでは外部モジュールからモジュール内のクラスにアクセスすることはできません。

外部モジュールからアクセスできるのはexportsされたパッケージの公開クラス、公開メンバだけなのです。

非公開なメンバへのアクセスは外部モジュールから禁止されています。

こういった所でもモジュール化のメリットである依存性、公開範囲の制限が絡んでくるんですね。

なのでこのリフレクションによるjavafx.fxmlモジュール側からのアクセスを許可する必要があります。

それにはopens…toモジュール・ディレクティブを設定します。

module-info.javaにopens jp.yucchi.includedjavafx to javafx.fxml;とopensモジュールディレクティブを付け加えるだけです。

module-info.javaにこのモジュールディレクティブを加えることによって実行時にのみ特定のモジュールに対してパッケージへのアクセスを許可することが可能です。

つまり、特定のパッケージ内のpublic型(およびネストされているpublic型とprotected型)に対して実行時にのみアクセスできることを指定可能となります。

指定したパッケージのすべての型(およびその型のすべてのメンバ)にリフレクションを使ってアクセスすることもできます。

Jigsawの強力で繊細なカプセル化はJDK 9より前のものより開発者には喜ばれるはずですね。

そうなるとjdepsのような依存性を解決する強力なツールの必要性が求められます。

そのうち統合開発環境なんかに乗っかってくることを期待します。

 

Jigsaw のお勉強 その1

Jigsaw のお勉強 その2

Jigsaw のお勉強 その3

Hatena タグ: ,