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から始まり、ユーザーの指定内容に対応します。
開始ページはゼロより大きく、終了ページ以下である必要があります。
開始と終了が同一の場合、範囲は単一ページを表します。
ジョブのページ数を超える値は、印刷時に単に無視されます。
各クラスの関連は下図のような感じですね。
APIドキュメントを読んでいるだけでは面白くないのでとりあえず超シンプルなものを組んで印刷してみよう。
テキストエリアにデフォルトプリンターの名前、プリンター一覧の表示、そして印刷ジョブに使用するプリンターの設定、プリンターの属性の一部を表示させます。
UIはこれから作成予定のはがき印刷のものを流用しているのでちょっと変なことなってますがそれはきにしないでくださいませ。
これらの処理をトグルボタンが押されたら開始するように次のようにプログラムを組んでみました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
@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"); } } |
プログラムの実行結果は次のようになります。
まず、デフォルトプリンターを取得するためにPrinterクラスのstaticメソッドgetDefaultPrinter()を使います。
そしてデフォルトプリンターの名前を取得するためにPrinterクラスのString getName()メソッドを使いました。
次にプリンター一覧を表示させるために全てのプリンターを取得します。
PrinterクラスのstaticメソッドObservableSet<Printer> getAllPrinters()を使います。
プリンターの名前を取得するために先ほどと同様にPrinterクラスのString getName()メソッドを使います。
そしてプリンターの選択をしてみます。
現在のシステムに組み込まれているプリンターは下図のようになっています。
デフォルトプリンターは”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がそろってます。
それではこれを印刷するコードを次のように追加しました。
注意: ステータスを確認するのにスレッドを少しスリープさせています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
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を返されたらプリンタージョブが失敗なのでそれなりの対処をします。
実際に印刷されたものがこのようになります。
簡単ですね!
これで基本的なPrinter APIを扱うことが可能となったのではがき印刷のプログラムを組むことにします。
下図のようなGUIのプログラムを組んでみました。
Printボタンをクリックすると印刷のためのワーカースレッドが起動して印刷をはじめます。
いちおうキャンセルボタンもつけました。
Printer Infoトグルボタンはシステムのプリンターの属性を表示させています。
Open Imageボタンは画像を変更するためのファイルチューザーを出します。
賀詞を入れるためのテキストフィールドとジョブステータス表示用のラベルも用意しました。
印刷に関するコードを下記に示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 |
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)メソッドを使い設定します。
印刷に関するものはこの程度ですね。
ではこのプログラムを実行してみましょう。
プリンターの印刷される音が鳴り止んではがきをとりにいって唖然としました。
こんなふうに印刷されていました。
左上から少しだけ画像が切り取られて印刷されただけです。
これでは印刷はできたけどこれをもらった人はなんだろう?と首をかしげるに違いない!
プログラムを修正することにします。
何故このようになったのか?
はがきの印刷領域より画像が大きすぎたからと思われる。
左上の隅はJavaの2Dでは原点(0, 0)になるからそこから印刷可能な範囲だけ画像が印刷されたのだろう。
では残りの部分を印刷可能領域分だけ画像を位置変更して必要なだけ印刷すれば画像全体が印刷できるんじゃないか。
では早速コードを修正してみよう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 |
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)); } |
さぁ、これで理論上は問題ないはず!
さっそくプログラムを実行して印刷してみよう。
印刷結果はこうなりました。
無事に印刷できたけど9枚のはがきが消費されました。
これを年賀状でだしたら受け取った人はパズルをすることになります。(^_^;)
わたしはなんと愚かな発想をしてしまったのだろう・・・
印刷可能領域に合わせるように画像をスケーリングしてしまえば良いだけのことじゃないですか!
せっかくだからこの複数枚印刷の機能を残したまま、縮小印刷機能を実装することにします。
使う用紙の印刷可能領域に画像をあわせるだけだから簡単ですね。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
// 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; } } |
複数枚印刷のコードに印刷領域にあわせて印刷対象のノードをスケーリングして印刷するだけのコードを使いするだけです。
どちらの印刷方法にするかはチェックボックスを使って分岐させています。
印刷が終わったら元の大きさにもどしています。もちろん成功、失敗、キャンセルでも。
この自動スケーリングで印刷した結果が下の画像になります。
これでクリスマスカード、年賀状もシンプルなものならJavaFXで印刷できるようになった。
完全に自己満足の世界にひたっている。
いちおう動いてるのとキャンセルしたときのGifアニメを貼っておきます。
下のGifアニメではJobStatusがNOT_STARTEDのときにキャンセルボタンによるキャンセルをかけています。
あともう一つ、キャンセルボタンではタイミング合わせるのが難しかったのでJobStatusがPRINTINGに変更されたら強制的キャンセルをするようにしてあります。
JobStatusがPRINTINGの時にキャンセルかけるとCANCELED -> ERROR となるんですね。
なんでCANCELEDで終了しないのだろう?これは印刷リクエストがされたあとだからERRORで終了という解釈でいいんだろうか?
最後にこのプログラムのコードを載せておきます。
1 2 3 4 5 6 7 8 9 10 11 |
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; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
<?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> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 |
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(); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
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(); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 |
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(); } } |
TAGS: JavaFX | 2018年12月22日4:20 PM | Comment : 0