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

JavaFX

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

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

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

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

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

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

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

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

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

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

 

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

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

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

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

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

Printerクラス

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

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

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

public PrinterAttributes getPrinterAttributes() 

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

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

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

PrinterAttributesクラス

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

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

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

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

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

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

Paperクラス

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

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

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

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

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

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

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

PaperSourceクラス

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

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

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

次はPrintResolutionクラスを見ます。

PrintResolutionクラス

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

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

次はPrinterJobクラスです。

PrinterJobクラス

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

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

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

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

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

と記述されています。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

JobSettingsクラス

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

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

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

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

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

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

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

PageLayoutクラス

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

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

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

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

PageRangeクラス

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

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

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

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

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

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

b1

 

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

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

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

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

 

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

1

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

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

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

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

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

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

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

printer

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

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

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

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

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

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

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

var attributes = defaultPrinter.getPrinterAttributes();

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

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

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

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

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

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

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

 

 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

 

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

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

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

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

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

 

2

 

 

3

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

a6

簡単ですね!

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

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

21

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

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

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

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

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

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

 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

a1

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

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

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

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

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

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

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

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

 

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

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

a2

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

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

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

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

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

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

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

 

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

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

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

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

31

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

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

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

postcarf1

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

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

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

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

postcard2

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

 

 

 

 

 

Hatena タグ:

« »

Leave a Reply

* が付いている項目は、必須項目です!

次の HTML タグと属性を利用できます: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code class="" title="" data-url=""> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong> <pre class="" title="" data-url=""> <span class="" title="" data-url="">

*

Trackback URL