このエントリーは じゃばえふえっくす 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はこれから作成予定のはがき印刷のものを流用しているのでちょっと変なことなってますがそれはきにしないでくださいませ。
これらの処理をトグルボタンが押されたら開始するように次のようにプログラムを組んでみました。
@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がそろってます。
それではこれを印刷するコードを次のように追加しました。
注意: ステータスを確認するのにスレッドを少しスリープさせています。
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ボタンは画像を変更するためのファイルチューザーを出します。
賀詞を入れるためのテキストフィールドとジョブステータス表示用のラベルも用意しました。
印刷に関するコードを下記に示します。
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)になるからそこから印刷可能な範囲だけ画像が印刷されたのだろう。
では残りの部分を印刷可能領域分だけ画像を位置変更して必要なだけ印刷すれば画像全体が印刷できるんじゃないか。
では早速コードを修正してみよう。
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枚のはがきが消費されました。
これを年賀状でだしたら受け取った人はパズルをすることになります。(^_^;)
わたしはなんと愚かな発想をしてしまったのだろう・・・
印刷可能領域に合わせるように画像をスケーリングしてしまえば良いだけのことじゃないですか!
せっかくだからこの複数枚印刷の機能を残したまま、縮小印刷機能を実装することにします。
使う用紙の印刷可能領域に画像をあわせるだけだから簡単ですね。
// 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で終了という解釈でいいんだろうか?
最後にこのプログラムのコードを載せておきます。
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();
}
}